編寫程式過程中,我們有時不希望改變某個變數的值。此時就可以使用關鍵字 const 對變數的類型加以限定。 初始化和const 因為const對象一旦創建後其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式: const int i = get_size();//正 ...
編寫程式過程中,我們有時不希望改變某個變數的值。此時就可以使用關鍵字 const 對變數的類型加以限定。
初始化和const
因為const對象一旦創建後其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式:
const int i = get_size();//正確:運行時初始化
const int j = 42;//正確:編譯時初始化
const int k;//錯誤:k是一個未經初始化的常量
正如之前反覆提到的,對象的類型決定了其上的操作。與非const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執行不改變其內容的操作。例如,const int和普通的int一樣都能參與算術運算,也都能轉換成一個布爾值,等等 。
在不改變const對象的操作中還有一種是初始化,如果利用一個對象去初始化另外 一個對象,則它們是不是const都無關緊要:
int i = 42;
const int ci = i;//正確: 1的值被拷貝給了ci
int j = ci;//正確:ci的值被拷貝給了J
預設狀態下,const對象僅在文件內有效
當以編譯時 初始化的方式定義 一個const對象時,就如對bufSize的定義 一樣:
const int bufSize = 512;//輸入緩衝區大小
編譯器將在編譯過程中把用到該變數的地方都替換成對應的值。也就是說,編譯器會找到代碼中所有用到 bufSize的地方,然後用512替換。為了執行上述替換,編譯器必須知道變數的初始值 。如果程式包含多個文件,則每個 用了const對象的 文件都必須得能訪問到它的初始值才行。要做到這一點,就必須在每 一個用到變數的文件中都有對它的定義(參見C++Primer2.2.2節,第41頁)。為了支持這一用法, 同時避免對同一變數的重覆定義,預設情況下,const對象被設定為僅在文件內有效。當多個文件中出現了同名的const變數時,其實等同於在不同文件中分別定義了獨立的變數。
某些時候有這樣一種const變數,它的初始值不是一個常旦表達式,但又確實有必要在文件間共用。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變數。相反,我們想讓這類const對象像其他(非常量)對象一樣工作,也就是說,只在一個文件中 定義const,而在其他多個文件中聲明並使用它。
解決的辦法是,對於const變數不管是聲明還是定義都添加extern關鍵字,這樣只需定義一次就可以了:
//file_l.cc定義並初始化了一個常岳,該常量能被其他文件訪問
extern const int bufSize =fen();
//file_l.h頭文件
extern const int bufSize;//與file_l.cc中定義的bufSize是同一個
1.變數中的const
1.1 普通變數
直接在普通變數類型聲明符前加上 const,可以將其聲明為 const 類型:
const int a = 0;
這樣就把 a 聲明成了一個 const 類型的常量,所以我們不能再改變它的值了,所以下麵試圖改變 a 的語句將會編譯報錯:
a = 10;
修改局部變數的值:
1.如果const修飾的局部變數是基礎的類型(int char double等等),並且初始化使用字面常量的話,不會給該變數分配空間。
例如:
void test()
{
const int a = 10;//用字面常量10來初始化
a = 20;//error
}
2.但是,當我們對這個變數進行取地址的操作的時候,系統會為該變數分配空間。
void test()
{
const int a = 10;
//a = 20;//error
int* p = (int*)&a;
*p = 20;
cout << a << endl;
cout << *p << endl;
}
上面的結果是:10和20
這是因為,當我們定義一個被const修飾並且使用字面常量來初始化的局部變數的時候,系統會把這個變數看作是一個符號,放入到符號表中,這麼變數名就是一個符號,值就是這個符號的值,類似於#define的作用。(這就是 C++ 中的常量摺疊 ,因為常量是在運行時初始化的,編譯器對常量進行優化,直接將常量值放在編譯器的符號表中,使用常量時直接從符號表中取出常量的值,省去了訪存這一步驟。)
當我們對這個變數取地址的時候,由於原來沒有空間,就沒有地址,現在需要取地址,所以才被迫分配一塊空間,我們通過地址的解引用可以修改這個空間的值,這也就是為什麼第二個結果為20的原因,但是如果我們還是通過變數名來訪問數據的話,系統會認為這還是一個符號,直接用符號表裡面的值替換。
但是!
3.如果初始化不是用字面常量而是用變數,那麼系統會直接分配空間。
void test()
{
int b = 20;
const int a = b;
}
這時候的a是有空間的,不會被放入到符號表中。
修改全局變數的值
通過指針修改位於靜態存儲區的的const變數,語法上沒有報錯,編譯不會出錯,一旦運行就會報告異常。因為全局變數存儲於靜態存儲區,靜態存儲區中的常量只有讀許可權,不能修改它的值。
與C一樣,當const修飾普通的全局變數的時候,不能通過變數名和地址來修改變數的值。
另外
與C不一樣的是,C語言中的const修飾的普通全局變數預設是外部鏈接屬性的,但是在C++中被const修飾的普通全局變數是內部鏈接屬性的。
也就是說當我們在一個文件中定義了一個如下的全局變數
const int a = 10;//定義全局變數
int main()
{
return 0;
}
我們在另外一個文件中,使用extern來聲明,也是不可以的。
//另外一個文件
extern const int a;//在另外的文件中聲明
上面這種做法是不可以的,C++中被const修飾的全局變數預設是內部鏈接屬性,不能直接在另外的文件中使用,如果想要在另外的文件中使用,就需要在定義該全局的變數的文件中用extern來修飾(另一個文件也需要extern修飾)。
//定義的文件
extern const int a = 10;
//另外一個文件聲明
extern const int a;
原文鏈接:https://blog.csdn.net/weixin_61021362/article/details/121544469
1.2 const 修飾引用
我們還可以對引用使用 const 限定符,在引用聲明的類型聲明符前加上 const 就可以聲明對const的引用,常量引用不能用來修改它所綁定的對象。
引用綁定到同一種類型,並修改值
直接上例子:
int i = 0;
const int j = 0;
const int &r1 = i;
//r1 = 20;//err不能給常量賦值
const int &r2 = j;
//r2 = 20;//err不能給常量賦值
int &r3 = j;
第三行將非常量對象 i 綁定到 const 引用 r1 上,此過程中發生了隱式類型轉換,i 的類型為 int,r1 的類型為 const int &, 所以這個過程 i 就從 int 轉換為了 const int,所以不能通過 r1 改變 i 的值,但可以直接改變 i 的值。但是 const int 類型不能轉換為 int。
可以這樣理解:const int是int的一種,但是範圍更小,將int限定在一個範圍之類,(本身int = const int類型 + 非const類型),沒有問題。但是const int到int範圍擴大,超出許可權。
第五行將常量對象 j 綁定到 const 引用 r2 上,不能直接改變 j 的值也不能通過常量引用改變 j 的值。
第七行將常量對象綁定到 const 引用 r3 上,報錯,不能將常量對象綁定到常量引用上。
綁定到另一種類型,並修改值
直接上例子:
double i= 1.0;
const int &r1 = i;
i = 2.0;
cout << "i = " << i << endl;
cout << "r1 = " << r1 <<endl;
---------------------------------------
out:
i = 2;
r1 = 1;
上面的代碼將 int 型的引用 r1 綁定到 double 型變數 i 上,然後改變 i 的值,我們發現 r1 並沒有改變,它的值反而是綁定 i 時 i 的值。這是因為引用變數的類型與被引用對象的類型不同時,中間會有如下操作:
double i = 1.0;
int temp = i;
const int &r1 = temp;
r1 引用的是臨時量 temp,而不是 i,所以才會出現上面的情況。
1.3 const 修飾指針
當使用const修飾指針變數時,情況就複雜起來了。const可以放置在不同的地方,因此具有不同的含義。來看下麵一個例子:
int age = 39;
const int * p1 = &age;
int const * p2 = &age;
int * const p3 = &age;
const int * const p4 = &age;
二三行是一個意思,表示 p 是指向常量的指針;第四行表示 p 是常量指針;第五行表示 p 是指向常量的常量指針。
上面二三行的賦值同樣發生了類型轉換,從 int * 轉換為 const int *。
指向常量的指針和常量指針
顧名思義:常量指針就是指針本身是常量,指針的值不能改變,也就是指針不能改變指向的對象,所以常量指針必須初始化;
指向常量的指針就是指向的變數時常量,被指變數不能被修改。
也可以將兩者結合,就有了指向常量的常量指針,其具有指向常量的指針和常量指針的共同性質。
修改指向常量的指針和常量指針
int age2 = 20;
*p1 = 20;
*p3 = 20;
p1 = &age2;
p3 = &age2;
第二行會報錯,因為 p1 是指向常量的指針,不能通過指針修改 age 的值;第五行會報錯,因為 p3 是常量指針,只能指向 age,不能指向其他變數。
如果對age2進行修改是不會報錯的。
原文鏈接:https://blog.csdn.net/weixin_45773137/article/details/126297568
1.4頂層與底層const
任意常量對象為頂層const,包括常量指針;指向常量的指針和聲明const的引用都為底層const
頂層const(top-level const)表示指針本身是個常量 int* const ptr=&m;
此時指針不可以發生改變,但是指針所指向的對象值是可以改變的
底層const(low-level const)表示指針所指的對象是常量 const int* ptr=&m;
此時指針可以發生改變,但是指針所指向的對象值是不可以改變的
頂層const可以表示任意的對象是常量(指針、引用、int、double都可以)
於是只有指針和引用等複合類型可以是底層const
執行對象的拷貝構造時,常量是頂層const還是底層const差別明顯
頂層const並不會有任何影響
進行拷貝操作的時候,僅僅只是從右值(頂層const)拷貝一個值並給自己賦值,雖然右值是一個不可變的量,但是貌似對我自己的拷貝完全沒有影響吧
const int m = 10;
int n = m;
int* const ptr2 = &n;
int* ptr3 = ptr2;
int i= 0;
int *const p1 = &i;//不能改變p1的值,這是一個頂層const
const int ci = 42;//不能改變ci的值,這是一個頂層const
const int *p2 = &ci;//允許改變p2的值 這是一個底層const
const int *const p3 = p2;//靠右的const是頂層const, 靠左的是底層
const const int &r = ci;//用於聲明引用的const都是底層const
當執行對象的拷貝操作時, 常量是頂層const還是底層const區別明顯。 其中,頂層const不受什麼影響:
i = ci;//正確:拷貝ci的值,CI是 一個頂層const, 對此操作無影響
p2 = p3;//正確:p2和p3指向的對象類型相同,p3頂層const的部分不影響
執行拷貝操作並不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什麼 影響。
另一方面,底層const的限制卻不能忽視。 當執行對象的拷貝操作時拷入和拷出的對象必須具有相同的底層const資格, 或者兩個對象的數據類型必須能夠轉換。非常量可以轉換成常扯, 反之則不行:
int *p = p3;//錯誤:p3包含底層const的定義,而p沒有
p2 = p3;//正確:p2和p3都是底層const
p2 = &i;//正確:int*能轉換成const int*
int &r = ci;//錯誤:普通的int&不能綁定到int常量上
const int &r2 = 1;//正確:const int&可以綁定到一個普通int上
p3既是頂層const也是底層const,拷貝p3時可以不在乎它是一個頂層const,但是必須清楚它指向的對象得是一個常量。因此,不能用p3去初始化p, 因為p指向的是一 個普通的(非常量)整數。 另一方面,p3的值可以賦給p2,是因為這兩個指針都是底層 const,儘管p3同時也是一個常量指針(頂層const), 僅就這次賦值而言不會有什麼影響。
原文鏈接:https://blog.csdn.net/m0_64860543/article/details/128269607
2.const 函數形參
我們已經瞭解了變數中const修飾符的作用,調用函數就會涉及變數參數的問題,那麼在形參列表中const形參與非const形參有什麼區別呢?
2.1 const 修飾普通形參
同樣,先來看看普通變數:
void fun(const int i)
{
i = 0;
cout << i << endl;
}
void fun(int i)
{
i = 0;
cout << i << endl;
}
int main()
{
const int i = 1;
fun(i);
return 0;
}
形參的頂層 const 在初始化時會被忽略,所以上面定義的兩個函數實際上是一個函數。編譯時會出現void fun(int) previously defined here
錯誤。
-
由於普通變數是拷貝傳值,所以const int實參可以傳給 int 形參。
-
與普通 const 變數一樣,第一個 fun 中的形參 i 只可讀;第二個function中的 i 則可讀可寫。
2.2 const 修飾指針形參
與 const 指針變數一樣,指向常量的指針形參指向的值不能修改;常量指針形參不能指向其他變數;指向常量的常量指針形參指向的值不能被修改,也不能指向其他變數。
#include<iostream>
using namespace std;
void fun(const int* i)
{
cout << *i << endl;
}
void fun(int* i)
{
*i = 0;
cout << *i << endl;
}
int main()
{
const int i = 1;
//調用 fun(const int* i),沒有 fun(const int* i),則會編譯報錯,因為沒有匹配形參的函數。
fun(&i);
int j = 1;
//調用 fun(int* i),沒有 fun(int* i),則會調用 fun(const int* i),此時 j 的值不會被改變
fun(&j);
return 0;
}
p1 指向的值不能修改;p2 不能指向其他變數;p3 指向的值不能被修改,也不能指向其他變數。
此外,形參的底層 const 在初始化時不會被忽略,所以上面的兩個函數是不同的函數,即重載函數,上面例子編譯並不會報錯,若果再加上一個void fun(int *const i)就會報錯,因為這個函數定義裡面 i 是頂層 const。
2.3 const 修飾引用形參
與 const 引用一樣,const 引用不會改變被引用變數的值。
#include<iostream>
using namespace std;
void fun(const int& i)
{
cout << i << endl;
}
void fun(int& i)
{
i = 0;
cout << i << endl;
}
int main()
{
const int i = 1;
//調用 fun(const int& i),沒有 fun(const int& i),則會編譯報錯,因為沒有匹配形參的函數。
fun(i);
int j = 1;
//調用 fun(int& i),沒有 fun(int& i),則會調用 fun(const int& i),此時 j 的值不會被改變
fun(j);
return 0;
}
由於 const 引用也是底層 const ,所以上面兩個函數是不同的函數,即重載函數,編譯並不會報錯。
3.類常量成員函數
面向對象程式設計中,為了體現封裝性,通常不允許直接修改類對象的數據成員。若要修改類對象,應調用公有成員函數來完成。為了保證const對象的常量性,編譯器須區分試圖修改類對象與不修改類對象的函數。例如:
const Screen blankScreen;
blankScreen.display(); // 對象的讀操作
blankScreen.set(‘*’); // 錯誤:const類對象不允許修改
C++中的常量對象,以及常量對象的指針或引用都只能調用常量成員函數。
要聲明一個const類型的類成員函數,只需要在成員函數參數列表後加上關鍵字const,例如:
class Screen
{
public:
char get() const;
};
在類外定義const成員函數時,還必須加上const關鍵字:
char Screen::get() const
{
return screen[cursor];
}
若將成員成員函數聲明為const,則該函數不允許修改類的數據成員。例如:
class Screen
{
public:
int get_cursor() const {return cursor; }
int set_cursor(int intival) const { cursor = intival; }
};
在上面成員函數的定義中,get_cursor()的定義是合法的,set_cursor()的定義則非法。
值得註意的是,把一個成員函數聲明為const可以保證這個成員函數不修改數據成員,但是,如果據成員是指針,則const成員函數並不能保證不修改指針指向的對象,編譯器不會把這種修改檢測為錯誤。例如:
class Name
{
public:
void setName(const string &s) const;
char *getName() const;
private:
char *m_sName;
};
void setName(const string &s) const
{
m_sName = s.c_str(); // 錯誤!不能修改m_sName;
for (int i = 0; i < s.size(); ++i)
m_sName[i] = s[i]; // 不是錯誤的
}
const成員函數可以被具有相同參數列表的非const成員函數重載,例如:
class Screen
{
public:
char get(int x,int y);
char get(int x,int y) const;
};
在這種情況下,類對象的常量性決定調用哪個函數。
const Screen cs;
Screen cc2;
char ch = cs.get(0, 0); // 調用const成員函數
ch = cs2.get(0, 0); // 調用非const成員函數
const成員函數不能修改類對象數據成員的深層解析:
調用成員函數時,通過一個名為this
的隱式參數來訪問調用該函數的對象成員。例如:
Name bozai;
bozai.setName("bozai");
bozai.getName("BOZAI");
原文鏈接:https://blog.csdn.net/weixin_45773137/article/details/126297568