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修飾指針(*):

const int* a = & [1]          //非常量數據的常量指針
int const *a = & [2]          //非常量數據的常量指針
int* const a = & [3]          //常量數據的非常量指針指針常量 常量指針 a is a constant pointer to the (non-constant) char variable
const int* const a = & [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修飾引用(&):

   int const &a=x;
   const int &a=x;
   int &const a=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.使用枚舉類型 

class test
{
     enum { SIZE1 = 10, SIZE2 = 20}; // 枚舉常量
     int array1[SIZE1];  
     int array2[SIZE2];
};

2.使用const 

     不能在類聲明中初始化const數據成員。以下用法是錯誤的,因爲類的對象未被創建時,編譯器不知道SIZE的值是什麼。 

  class test
  {
        const int SIZE = 100;     // 錯誤,企圖在類聲明中初始化const數據成員
        int array[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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章