C++中的const可用於修飾變量、函數,且在不同的地方有著不同的含義,現總結如下。
C++中的const的目的是通過編譯器來保證對象的常量性,強制編譯器將所有可能違背const對象的常量性的操作都視為error。
對象的常量性可以分為兩種:物理常量性(即每個bit都不可改變)和邏輯常量性(即對象的表現保持不變)。C++中采用的是物理常量性,例如下面的例子:
1 2 3 4 5 6 7struct
A {
int
*ptr;
};
int
k = 5, r = 6;
const
A a = {&k};
a.ptr = &r;
// !error
*a.ptr = 7;
// no error
a是const對象,則對a的任何成員進行賦值都會被視為error,但如果不改動ptr,而是改動ptr指向的對象,編譯器就不會報錯。這實際上違背了邏輯常量性,因為A的表現已經改變了!
邏輯常量性的另一個特點是,const對象中可以有某些用戶不可見的域,改變它們不會違背邏輯常量性。Effective C++中的例子是:
1 2 3 4 5 6 7 8 9class
CTextBlock {
public
:
...
std::
size_t
length()
const
;
private
:
char
*pText;
std::
size_t
textLength;
// last calculated length of textblock
bool
lengthIsValid;
// whether length is currently valid
};
CTextBlock對象每次調用length方法後,都會將當前的長度緩存到textLength成員中,而lengthIsValid對象則表示緩存的有效性。這個場景中textLength和lengthIsValid如果改變了,其實是不違背CTextBlock對象的邏輯常量性的,但因為改變了對象中的某些bit,就會被編譯器阻止。C++中為了解決此問題,增加了mutable關鍵字。
本部分總結:C++中const的語義是保證物理常量性,但通過mutable關鍵字可以支持一部分的邏輯常量性。
如上節所述,用const修飾變量的語義是要求編譯器去阻止所有對該變量的賦值行為。因此,必須在const變量初始化時就提供給它初值:
1 2 3const
int
i;
i = 5;
// !error
const
int
j = 10;
// ok
這個初值可以是編譯時即確定的值,也可以是運行期才確定的值。如果給整數類型的const變量一個編譯時初值,那麼可以用這個變量作為聲明數組時的長度:
1 2 3 4const
int
COMPILE_CONST = 10;
const
int
RunTimeConst = cin.get();
int
a1[COMPLIE_CONST];
// ok in C++ and error in C
int
a2[RunTimeConst];
// !error in C++
因為C++編譯器可以將數組長度中出現的編譯時常量直接替換為其字面值,相當於自動的宏替換。(gcc驗證發現,只有數組長度那裡直接做了替換,而其它用COMPILE_CONST賦值的地方並沒有進行替換。)
文件域的const變量默認是文件內可見的,如果需要在b.cpp中使用a.cpp中的const變量M,需要在M的初始化處增加extern:
1 2 3 4 5//a.cpp
extern
const
int
M = 20;
//b.cpp
extern
const
int
M;
一般認為將變量的定義放在.h文件中會導致所有include該.h文件的.cpp文件都有此變量的定義,在鏈接時會造成沖突。但將const變量的定義放在.h文件中是可以的,編譯器會將這個變量放入每個.cpp文件的匿名namespace中,因而屬於是不同變量,不會造成鏈接沖突。(注意:但如果頭文件中的const量的初始值依賴於某個函數,而每次調用此函數的返回值不固定的話,會導致不同的編譯單元中看到的該const量的值不相等。猜測:此時將該const量作為某個類的static成員可能會解決此問題。)
const修飾引用時,其意義與修飾變量相同。但const在修飾指針時,規則就有些復雜了。
簡單的說,可以將指針變量的類型按變量名左邊最近的‘*’分成兩部分,右邊的部分表示指針變量自己的性質,而左邊的部分則表示它指向元素的性質:
1 2 3 4 5 6 7 8const
int
*p1;
// p1 is a non-const pointer and points to a const int
int
*
const
p2;
// p2 is a const pointer and points to a non-const int
const
int
*
const
p3;
// p3 is a const pointer and points to a const it
const
int
*pa1[10];
// pa1 is an array and contains 10 non-const pointer point to a const int
int
*
const
pa2[10];
// pa2 is an array and contains 10 const pointer point to a non-const int
const
int
(* p4)[10];
// p4 is a non-const pointer and points to an array contains 10 const int
const
int
(*pf)();
// pf is a non-const pointer and points to a function which has no arguments and returns a const int
...
const指針的解讀規則差不多就是這些了……
指針自身為const表示不可對該指針進行賦值,而指向物為const則表示不可對其指向進行賦值。因此可以將引用看成是一個自身為const的指針,而const引用則是const Type * const指針。
指向為const的指針是不可以賦值給指向為非const的指針,const引用也不可以賦值給非const引用,但反過來就沒有問題了,這也是為了保證const語義不被破壞。
可以用const_cast來去掉某個指針或引用的const性質,或者用static_cast來為某個非const指針或引用加上const性質:
1 2 3 4int
i;
const
int
*cp = &i;
int
*p =
const_cast
<
int
*>(cp);
const
int
*cp2 =
static_cast
<
const
int
*>(p);
// here the static_cast is optional
C++類中的this指針就是一個自身為const的指針,而類的const方法中的this指針則是自身和指向都為const的指針。
類中的const成員變量可分為兩種:非static常量和static常量。
類中的非static常量必須在構造函數的初始化列表中進行初始化,因為類中的非static成員是在進入構造函數的函數體之前就要構造完成的,而const常量在構造時就必須初始化,構造後的賦值會被編譯器阻止。
1 2 3 4 5 6 7 8class
B {
public
:
B(): name(
"aaa"
) {
name =
"bbb"
;
// !error
}
private
:
const
std::string name;
};
static常量是在類中直接聲明的,但要在類外進行唯一的定義和初始值,常用的方法是在對應的.cpp中包含類的static常量的定義:
1 2 3 4 5 6 7 8// a.h
class
A {
...
static
const
std::string name;
};
// a.cpp
const
std::string A::name(
"aaa"
);
一個特例是,如果static常量的類型是內置的整數類型,如char、int、size_t等,那麼可以在類中直接給出初始值,且不需要在類外再進行定義了。編譯器會將這種static常量直接替換為相應的初始值,相當於宏替換。但如果在代碼中我們像正常變量那樣使用這個static常量,如取它的地址,而不是像宏一樣只使用它的值,那麼我們還是需要在類外給它提供一個定義,但不需要初始值了(因為在聲明處已經有了)。
1 2 3 4 5 6 7 8// a.h
class
A {
...
static
const
int
SIZE = 50;
};
// a.cpp
const
int
A::SIZE = 50;
// if use SIZE as a variable, not a macro
C++中可以用const去修飾一個類的非static成員函數,其語義是保證該函數所對應的對象本身的const性。在const成員函數中,所有可能違背this指針const性(const成員函數中的this指針是一個雙const指針)的操作都會被阻止,如對其它成員變量的賦值以及調用它們的非const方法、調用對象本身的非const方法。但對一個聲明為mutable的成員變量所做的任何操作都不會被阻止。這裡保證了一定的邏輯常量性。
另外,const修飾函數時還會參與到函數的重載中,即通過const對象、const指針或引用調用方法時,優先調用const方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24class
A {
public
:
int
&operator[](
int
i) {
++cachedReadCount;
return
data[i];
}
const
int
&operator[](
int
i)
const
{
++size;
// !error
--size;
// !error
++cachedReadCount;
// ok
return
data[i];
}
private
:
int
size;
mutable
cachedReadCount;
std::vector<
int
> data;
};
A &a = ...;
const
A &ca = ...;
int
i = a[0];
// call operator[]
int
j = ca[0];
// call const operator[]
a[0] = 2;
// ok
ca[0] = 2;
// !error
這個例子中,如果兩個版本的operator[]有著基本相同的代碼,可以考慮在其中一個函數中去調用另一個函數來實現代碼的重用(參考Effective C++)。這裡我們只能用非const版本去調用const版本。
1 2 3int
&A::operator[](
int
i) {
return
const_cast
<
int
&>(
static_cast
<
const
A &>(*
this
).operator[](i));
}
其中為了避免調用自身導致死循環,首先要將*this轉型為const A &,可以使用static_cast來完成。而在獲取到const operator[]的返回值後,還要手動去掉它的const,可以使用const_cast來完成。一般來說const_cast是不推薦使用的,但這裡我們明確知道我們處理的對象其實是非const的,那麼這裡使用const_cast就是安全的。
constexpr是C++11中新增的關鍵字,其語義是“常量表達式”,也就是在編譯期可求值的表達式。最基礎的常量表達式就是字面值或全局變量/函數的地址或sizeof等關鍵字返回的結果,而其它常量表達式都是由基礎表達式通過各種確定的運算得到的。constexpr值可用於enum、switch、數組長度等場合。
constexpr所修飾的變量一定是編譯期可求值的,所修飾的函數在其所有參數都是constexpr時,一定會返回constexpr。
1 2 3 4 5 6 7constexpr
int
Inc(
int
i) {
return
i + 1;
}
constexpr
int
a = Inc(1);
// ok
constexpr
int
b = Inc(cin.get());
// !error
constexpr
int
c = a * 2 + 1;
// ok
constexpr還能用於修飾類的構造函數,即保證如果提供給該構造函數的參數都是constexpr,那麼產生的對象中的所有成員都會是constexpr,該對象也就是constexpr對象了,可用於各種只能使用constexpr的場合。注意,constexpr構造函數必須有一個空的函數體,即所有成員變量的初始化都放到初始化列表中。
1 2 3 4 5 6 7struct
A {
constexpr
A(
int
xx,
int
yy): x(xx), y(yy) {}
int
x, y;
};
constexpr
A a(1, 2);
enum
{SIZE_X = a.x, SIZE_Y = a.y};
constexpr的好處: