C++11——const和constexpr
再說constexpr之前我們先了解下const
const
我是參考這個博主的https://www.cnblogs.com/wintergrass/archive/2011/04/15/2015020.html
const是C語言的一種關鍵字,它所限定的變數是不允許被改變的,從而起到保護的作用!
const關鍵字可以用於修飾變數,引數,返回值,甚至函式體。const可以提高程式的健壯性,減少程式出錯。
const的用法大致可分為以下幾個方面:
(1)const修飾定義常量和修飾變數
(2)const應用到函式中
(3)const在類中的用法
(4)const修飾類物件,定義常量物件
(一)const用於定義常量和修飾變數
當定義某個變數時,用const修飾,則該變數就變為常量,其值定義後就不能再改變了,如:const int x=1;常量x的值不能再改變了。
TYPE const ValueName = value; //TYPE表示資料型別int、long、float等 const TYPE ValueName = value; //TYPE表示資料型別int、long、float等
比如程式碼:
const 修飾變數,表示該變數不能被修改。
1、const char *p 表示指標p指向的內容不能改變
2、char * const p,就是將p宣告為常指標,它的地址不能改變。
const char* p0 = "aaaa"; const char* p1 = "abcd";// 表示指標p指向的內容不能改變 但是p指向的位置是可以變得 p1 = p0; cout << p1 << " " << p0 << endl;
int a = 3; int* const b = &a; //就是將p宣告為常指標,它的地址不能改變。 //但是!我們可以改變p地址裡面的值! a = 5; cout << b << " " << &a << endl; cout << *b << "" << a << endl;
const修飾指標變數*及引用變數&
介紹本部分內容之前,先說說指標和引用的一些基本知識。
指標(pointer)是用來指向實際記憶體地址的變數,一般來說,指標是整型,而且一般的大家會接受十六進位制的輸出格式。
引用(reference)是其相應變數的別名,用於向函式提供直接訪問引數(而不是引數的副本)的途徑,與指標相比,引用是一種受限制的指標型別,或者說是指標的一個子集,而從其功能上來看,似乎可以說引用是指標功能的一種高層實現。
關於運算子&和*:
在C++裡,沿襲C中的語法,有兩個一元運算子用於指標操作:&和*。按照本來的定義,&應當是取址符,*是指標符,也就是說, &用於返回變數的實際地址,*用於返回地址所指向的變數,他們應當互為逆運算。實際的情況也是如此。
在定義變數的引用的時候,&只是個定義引用的標誌,不代表取地址。
舉例:
#include<iostream> using namespace std; int main() { int a; //a is an integer int* aPtr; //aPtr is a pointer to an integer a = 7; aPtr = &a; cout << "Showing that * and & are inverses of " << "each other.\n"; cout << "a=" << a << " *aPtr=" << *aPtr << "\n"; cout << "&*aPtr = " << &*aPtr << endl; cout << "*&aPtr = " << *&aPtr << endl; return 0; }
執行結果:
const修飾指標(*):
constint*a=&[1]//非常量資料的常量指標intconst*a=&[2]//非常量資料的常量指標 int*consta=&[3]//常量資料的非常量指標指標常量 常量指標a isaconstantpointertothe(non-constant)charvariable constint*consta=&[4] //常量資料的常量指標
可以參考《Effective c++》Item21上的做法,
如果const位於星號*的左側,則const就是用來修飾指標所指向的變數,即指標指向為常量;
如果const位於星號的右側,const就是修飾指標本身,即指標本身是常量。
因此,[1]和[2]的情況相同,都是指標所指向的內容為常量,這種情況下不允許對內容進行更改操作,如不能*a = 3;
[3]為指標本身是常量,而指標所指向的內容不是常量,這種情況下不能對指標本身進行更改操作,如a++是錯誤的;
[4]為指標本身和指向的內容均為常量。
可以看著這個
#include <iostream> #include <sstream> #include <vector> using namespace std; int main() { { const char* p0 = "aaaa"; const char* p1 = "abcd";// 表示指標p指向的內容不能改變 但是p指向的位置是可以變得 p1 = p0; cout << p1 << " " << p0 << endl; //(*p1)++; p1++; cout << "----------------------" << endl; } { char const* p0 = "aaaa"; char const* p1 = "abcd";// 表示指標p指向的內容不能改變 但是p指向的位置是可以變得 p1 = p0; //(*p1)++; p1++; cout << p1 << " " << p0 << endl; cout << "----------------------" << endl; } { int a = 3; int* const b = &a; //就是將p宣告為常指標,它的地址不能改變。 //但是!我們可以改變b地址裡面的值! a = 5; (*b)++; //b++; cout << b << " " << &a << endl; cout << *b << " " << a << endl; cout << "----------------------" << endl; } { int a = 3; const int* const b = &a; //(*b)++ 和b++ 都不對 //(*b)++; //b++; cout << b << endl; cout << "----------------------" << endl; } return 0; }
const修飾引用(&):
intconst&a=x;constint&a=x; int&consta=x;//這種方式定義是C、C++編譯器未定義,雖然不會報錯,但是該句效果和int&a一樣。 //這兩種定義方式是等價的,此時的引用a不能被更新。如:a++ 這是錯誤的。
2.const應用到函式中
const在函式中的應用主要有三點:
作為引數的const修飾符;
作為函式返回值的const修飾符
作為函式的const修飾符
不管是作為函式引數的const修飾符還是返回值的修飾符,其實際含義都是一樣的。
const修飾函式引數
比如:void fun0(const A* a ); 或則 void fun1(const A& a);
呼叫函式的時候用相應的變數初始化const常量,則在函式體中,按照const修飾的部分進行常量化。
比如const A* a 則不能對傳遞進來的指標指向的內容修改,保護原指標所指向的內容;
比如const A& a則不能對傳遞進來的引用物件的內容修改,保護原引用物件所指向的內容。
注意:引數const通常用於引數為指標或引用的情況。
const修飾函式返回值
修飾返回值的const,如const A fun2( ); const A* fun3( );
這樣聲明瞭返回值後,const按照"修飾原則"進行修飾,保護函式返回的指標指向的內容或則引用的物件不被修改。
const修飾函式
const作用於函式還有一種情況是,在函式定義的最後面加上const修飾,比如:
A fun4() const;
其意義上是不能修改除了函式區域性變數以外的所在類的任何變數。
三、類中定義常量(const的特殊用法)
在類中實現常量的定義大致有這麼幾種方式實現:
1.使用列舉型別
classtest{
enum{SIZE1=10,SIZE2=20};//列舉常量
intarray1[SIZE1];
intarray2[SIZE2];
};
2.使用const
不能在類宣告中初始化const資料成員。以下用法是錯誤的,因為類的物件未被建立時,編譯器不知道SIZE的值是什麼。
classtest{ constintSIZE=100;//錯誤,企圖在類宣告中初始化const資料成員
intarray[SIZE];//錯誤,未知的SIZE };
正確的使用const實現方法為:const資料成員的初始化只能在類建構函式的初始化表中進行。
class A { A(int size); // 建構函式 const int SIZE ; }; A::A(int size) : SIZE(size) // 建構函式的初始化表 { } //error 賦值的方式是不行的 A::A(int size) { SIZE=size; } void main() { A a(100); // 物件 a 的SIZE值為100 A b(200); // 物件 b 的SIZE值為200 }
注意:對const成員變數的初始化,不能在變數宣告的地方,必須在類的建構函式的初始化列表中完成,即使是在建構函式內部賦值也是不行的。
具體原因請參見 【初始化列表和賦值的區別】
3.使用static const
通過結合靜態變數來實現:
#include <iostream> #include <sstream> #include <vector> using namespace std; class Year { private: int y; public: static int const Inity; static int a; Year() { y = Inity; } }; int const Year::Inity = 1997;//靜態變數的賦值方法,注意必須放在類外定義 int main() { cout << Year::Inity << endl;//注意呼叫方式,這裡是用類名呼叫的。 return 0; }
到這裡就把在類中定義常量的方法都陳列出來了。
四、const定義常量物件,以及常量物件的用法
#include <iostream> #include <sstream> #include <vector> using namespace std; class test { public: test() :x(1) { y = 2; } ~test(){} void set(int yy) { y = yy; } int getx() const { return x; } const int x; int y; }; int main() { const test t;// 定義的一個const物件 t.set(33); //error,set方法不是const修飾的方法,編譯器會報錯。 t.getx(); }
常量物件只能呼叫常量函式,別的成員函式都不能呼叫。
五、使用const的一些建議
<1> 要大膽的使用const,這將給你帶來無盡的益處,但前提是你必須搞清楚原委;
<2> 要避免最一般的賦值操作錯誤,如將const變數賦值,具體可見思考題;
<3> 在引數中使用const應該使用引用或指標,而不是一般的物件例項,原因同上;
<4> const在成員函式中的三種用法(引數、返回值、函式)要很好的使用;
<5>不要輕易的將函式的返回值型別定為const;
<6>除了過載操作符外一般不要將返回值型別定為對某個物件的const引用;
轉載來自:https://www.subingwen.cn/cpp/constexpr/#2-3-%E4%BF%AE%E9%A5%B0%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0
在 C++11 之前只有 const 關鍵字,從功能上來說這個關鍵字有雙重語義:變數只讀,修飾常量,舉一個簡單的例子:
void func(const int num) { const int count = 24; int array[num]; // error,num是一個只讀變數,不是常量 int array1[count]; // ok,count是一個常量 int a1 = 520; int a2 = 250; const int& b = a1; b = a2; // error a1 = 1314; cout << "b: " << b << endl; // 輸出結果為1314 }
函式 void func(const int num) 的引數 num 表示這個變數是隻讀的,但不是常量,因此使用 int array[num]; 這種方式定義一個數組,編譯器是會報錯的,提示 num不可用作為常量來使用。
const int count = 24; 中的 count 卻是一個常量,因此可以使用這個常量來定義一個靜態陣列。
另外,變數只讀並不等價於常量,二者是兩個概念不能混為一談,分析一下這句測試程式碼 const int& b = a1;:
b 是一個常量的引用,所以 b 引用的變數是不能被修改的,也就是說 b = a2; 這句程式碼語法是錯誤的。
在 const 對於變數 a1 是沒有任何約束的,a1 的值變了 b 的值也就變了
引用 b 是隻讀的,但是並不能保證它的值是不可改變的,也就是說它不是常量。
接下來了解下重頭戲constexpr
在 C++11 中添加了一個新的關鍵字 constexpr,這個關鍵字是用來修飾常量表達式的。所謂常量表達式,指的就是由多個(≥1)常量(值不會改變)組成並且在編譯過程中就得到計算結果的表示式。
C++ 程式從編寫完畢到執行分為四個階段:預處理、 編譯、彙編和連結 4 個階段,得到可執行程式之後就可以運行了。
需要額外強調的是,常量表達式和非常量表達式的計算時機不同,非常量表達式只能在程式執行階段計算出結果,但是常量表達式的計算往往發生在程式的編譯階段,這可以極大提高程式的執行效率,
因為表示式只需要在編譯階段計算一次,節省了每次程式執行時都需要計算一次的時間。
那麼問題來了,編譯器如何識別表示式是不是常量表達式呢?
在 C++11 中添加了 constexpr 關鍵字之後就可以在程式中使用它來修改常量表達式,用來提高程式的執行效率。在使用中建議將 const 和 constexpr 的功能區分開,即凡是表達“只讀”語義的場景都使用 const,表達“常量”語義的場景都使用 constexpr。
在定義常量時,const 和 constexpr 是等價的,都可以在程式的編譯階段計算出結果,例如:
C++
constexpr int cef5 = fac(5); //常量表達式 const int cf5 = fac(5);
對於 C++ 內建型別的資料,可以直接用 constexpr 修飾,但如果是自定義的資料型別(用 struct 或者 class 實現),直接用 constexpr 修飾是不行的。
// 此處的constexpr修飾是無效的
constexpr struct Test { int id; int num; };
如果要定義一個結構體 / 類常量物件,可以這樣寫:
struct Test { int id; int num; }; int main() { constexpr Test t{ 1, 2 }; constexpr int id = t.id; constexpr int num = t.num; //t.num += 100;// error,不能修改常量 cout << "id: " << id << ", num: " << num << endl; return 0; }
在 t.num += 100; 的操作是錯誤的,物件 t 是一個常量,因此它的成員也是常量,常量是不能被修改的。
2. 常量表達式函式
為了提高 C++ 程式的執行效率,我們可以將程式中值不需要發生變化的變數定義為常量,也可以使用 constexpr 修飾函式的返回值,這種函式被稱作常量表達式函式,這些函式主要包括以下幾種:普通函式/類成員函式、類的建構函式、模板函式。
2.1 修飾函式
constexpr 並不能修改任意函式的返回值,時這些函式成為常量表達式函式,必須要滿足以下幾個條件:
函式必須要有返回值,並且 return 返回的表示式必須是常量表達式。
// error,不是常量表達式函式 constexpr void func1() { int a = 100; cout << "a: " << a << endl; } // error,不是常量表達式函式 constexpr int func1() { int a = 100; return a; }
函式 func1() 沒有返回值,不滿足常量表達式函式要求
函式 func2() 返回值不是常量表達式,不滿足常量表達式函式要求
函式在使用之前,必須有對應的定義語句。
#include <iostream> using namespace std; constexpr int func1(); int main() { constexpr int num = func1(); // error return 0; } constexpr int func1() { constexpr int a = 100; return a; }
在測試程式 constexpr int num = func1(); 中,還沒有定義 func1() 就直接呼叫了,應該將 func1() 函式的定義放到 main() 函式的上邊。
整個函式的函式體中,不能出現非常量表達式之外的語句(using 指令、typedef 語句以及 static_assert 斷言、return 語句除外)。
// error constexpr int func1() { constexpr int a = 100; constexpr int b = 10; for (int i = 0; i < b; ++i) { cout << "i: " << i << endl; } return a + b; } // ok constexpr int func2() { using mytype = int; constexpr mytype a = 100; constexpr mytype b = 10; constexpr mytype c = a * b; return c - (a + b); }
因為 func1() 是一個常量表達式函式,在函式體內部是不允許出現非常量表達式以外的操作,因此函式體內部的 for 迴圈是一個非法操作。
以上三條規則不僅對應普通函式適用,對應類的成員函式也是適用的:
class Test { public: constexpr int func() { constexpr int var = 100; return 5 * var; } }; int main() { Test t; constexpr int num = t.func(); cout << "num: " << num << endl; return 0; }
2.2 修飾模板函式
C++11 語法中,constexpr 可以修飾函式模板,但由於模板中型別的不確定性,因此函式模板例項化後的模板函式是否符合常量表達式函式的要求也是不確定的。
如果 constexpr 修飾的模板函式例項化結果不滿足常量表達式函式的要求,則 constexpr 會被自動忽略,即該函式就等同於一個普通函式。
#include <iostream> using namespace std; struct Person { const char* name; int age; }; // 定義函式模板 template<typename T> constexpr T dispaly(T t) { return t; } int main() { struct Person p { "luffy", 19 }; //普通函式 struct Person ret = dispaly(p); cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl; //常量表達式函式 constexpr int ret1 = dispaly(250); cout << ret1 << endl; constexpr struct Person p1 { "luffy", 19 }; constexpr struct Person p2 = dispaly(p1); cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl; return 0; }
//普通函式
struct Person ret = dispaly(p); cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;
//常量表達式函式
constexpr int ret1 = dispaly(250); cout << ret1 << endl; constexpr struct Person p1 { "luffy", 19 }; constexpr struct Person p2 = dispaly(p1); cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl; return 0; }
在上面示例程式中定義了一個函式模板 display(),但由於其返回值型別未定,因此在例項化之前無法判斷其是否符合常量表達式函式的要求:
struct Person ret = dispaly(p); 由於引數 p 是變數,所以例項化後的函式不是常量表達式函式,此時 constexpr 是無效的
constexpr int ret1 = dispaly(250); 引數是常量,符合常量表達式函式的要求,此時 constexpr 是有效的
constexpr struct Person p2 = dispaly(p1); 引數是常量,符合常量表達式函式的要求,此時 constexpr 是有效的
2.3 修飾建構函式
如果想用直接得到一個常量物件,也可以使用 constexpr 修飾一個建構函式,這樣就可以得到一個常量構造函數了。常量建構函式有一個要求:建構函式的函式體必須為空,並且必須採用初始化列表的方式為各個成員賦值。
#include <iostream> using namespace std; struct Person { constexpr Person(const char* p, int age) :name(p), age(age) { } const char* name; int age; }; int main() { constexpr struct Person p1("luffy", 19); cout << "luffy's name: " << p1.name << ", age: " << p1.age << endl; return 0; }