C++ const和constexpr

constexpr是c++11标准添加的关键字。

相同点:

const和consexpr都是用来定义常量的。
不同点:

const声明的常量,初始值引用的对象不一定是一个常量;

const 所定义的变量,一经初始化便不能修改,但是不要求const所定义变量在编译的时候就能被算出。

constexpr 定义的变量要求,初始值一定是常量表达式,变量在程序被编译的时候就能被判定为常量,否则出错。

 

之所以说const声明的常量,初始值不一定是一个常量,主要是从引用和指针的角度出发的。如果初始化const时候,指定一个固定的字面值常量,那么它引用的肯定是常量。

const int i = 10;
constexpr int j = 20;

这种情况下,i和j都是常量,而且它们引用的也是一个常量(因为是固定的字面值)。那么如果它们引用的不是固定的字面值,而是指针和引用呢?接下来笔者将从引用和指针的角度出发,解释const和constexpr的区别:

const与引用

通过如下语法可以声明一个常量int类型引用:

const int &v;

注意这里的v是一个常量int引用,也就是说,v是肯定是一个int类型的常量引用(值不能改变),应该给它赋值常量int类型,如果我们给它赋值变量int类型会怎么样呢?看下面的案例。
例如:

#include <iostream>
using namespace std;
int main(){
    int a = 20;
    const int &b = a;//引用a,常量int引用b引用了非常量inta
    cout << "a = " << a << ", b = " << b << endl;
    a = 10;//可以通过a改变变量的值
    cout << "a = " << a << ", b = " << b << endl;
    //b = 20;//出错,不可以通过b改变变量的值,因为b一个常量引用,所以不能通过b去改变。
    return 0;
}

结果:

a = 20, b = 20
a = 10, b = 10

上面的案例中,a是一个变量,b是一个常量引用。a变量的值不能通过b来改变,但是可以通过a来改变,因为a不是常量,b是常量引用(b认为自己引用的是一个常量,实际却不是)。

const与指针

可以通过如下的方式,来声明一个常量指针。

int *const p;

p首先是一个常量,然后再是一个指针,并且这个指针指向一个int类型。

下面的案例

#include <iostream>
using namespace std;
int main(){
    int i = 10;
    int *const p = &i;//指向非常量的常量指针p,指向了非常量i
    cout << "i = " << i << ",*p = " << *p << endl;
    i = 20;
    cout << "i = " << i << ",*p = " << *p << endl;
    *p = 30;
    cout << "i = " << i << ",*p = " << *p << endl;
    int j = 0;
    //p = &j;//出错
    return 0;
}

输出

i = 10,*p = 10
i = 20,*p = 20
i = 30,*p = 30

上面的案例中p是一个常量类型的指针,并且指向一个非常量int对象。由于p是指针,所以*p解地址后实际上是变量i,所以可以通过*p改变变量的值。但是p = &j语句,会改变p变量中存储的地址(初始化时存储的是变量i的地址),由于p是常量,所以p中的值是不能改变,因此p = &j会报错。

 

当然可以这样定义

const int *const p;

这样的话,p是一个常量指针,并且指向常量int类型。
例如:

#include <iostream>
using namespace std;
int main(){
    int i = 10;
    const int *const p = &i;//指向常量int的常量指针p,指向了非常量i
    cout << "i = " << i << ",*p = " << *p << endl;
    i = 20;
    cout << "i = " << i << ",*p = " << *p << endl;
    //*p = 30;//出错
    int j = 0;
    //p = &j;//出错
    return 0;
}

输出结果:

i = 10,*p = 10
i = 20,*p = 20

虽然常量指针p应该指向一个常量int类型,但是笔者给它一个非常量int类型的地址,这样一来,p会认为它指向的是一个常量int类型,所以当通过*p = 30改变它的值时,不能通过。但是通过i依然可以修改。

小结:
常量引用可以引用非常量,非常用引用不能引用常用。
指向常量的指针可以指向一个非常量,指向非常量的指针不能指向常量。

从逻辑上可以这样理解:因为非常量具有可读可写的功能,常量只有可读的功能;当常量引用非常量时,常量只期望可以读数据,非常量不仅提供了读数据,而且非常量还可以写数据(修改),因此常量引用非常量可以满足常量的需求,可以通过;返过来,常量不能够满足非常量的需求,所以不能通过。

int a = 10;
const int &b = a;//正确,b只需要读数据,a可以读数据,所以可以通过。

const int c = 10;
int &d = c;//错误,d需要读数据和写数据,c只能提供读数据,所以不通过。

int e = 10;
const int *f = &e;//正确,*f只需要能够读数据,e可以读数据,所以可以通过。

const int g = 10;
int *h = &g;//错误,*h需要读数据和写数据,g只能提供读数据,所以不通过。

上面笔者总结的规律还有一些需要补充,在不改变const对象的操作中还有一种是初始化,如果一个利用对象去初始化另外一个对象(引用和指针除外,这里主要是指拷贝),则他们是不是const都无关紧要:

int i = 42;
const int ci = i; // 正确:i的值被拷贝给了ci
int j = ci; //正确 : ci的值被拷贝给了j

尽管ci是const类型,j是int类型。ci的常量特征仅仅在执行改变ci的操作时才会发挥作用,当用ci初始化j时,更本无需在意ci是不是一个常量。拷贝一个对象的值不会改变它,一旦拷贝完成,新的对象和原来的对象就没什么关系了。

constexpr

在上面的说过了const的特点,可以得出,当const变量引用或指向某个变量时,被引用或指向的变量是不能确定是否是一个常量的。
C++11标准提供了constexpr关键字,被constexpr修饰的变量的初始值必须是常量表达式,也就是说,被constexpr修饰的变量,肯定是常量,而且引用常量表达式。

constexpr int m = 10;//20是常量表达式
constexpr int n = m + 1;//m+1是一个常量表达式

//* & 都不属于常量表达式
constexpr const int *p = &m;//错误
constexpr const int &r = m;//错误

constexpr是C++11中新增的关键字,其语义是“常量表达式”,也就是在编译期可求值的表达式。最基础的常量表达式就是字面值或全局变量/函数的地址或sizeof等关键字返回的结果,而其它常量表达式都是由基础表达式通过各种确定的运算得到的。constexpr值可用于enum、switch、数组长度等场合。

constexpr所修饰的变量一定是编译期可求值的,所修饰的函数在其所有参数都是constexpr时,一定会返回constexpr。

constexpr int Inc(int i) {

    return i + 1;

}

constexpr int a = Inc(1);

constexpr int b = Inc(cin.get());

constexpr int c = a * 2 + 1;

constexpr还能用于修饰类的构造函数,即保证如果提供给该构造函数的参数都是constexpr,那么产生的对象中的所有成员都会是constexpr,该对象也就是constexpr对象了,可用于各种只能使用constexpr的场合。注意,constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。

struct 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的好处:

  1. 是一种很强的约束,更好地保证程序的正确语义不被破坏。
  2. 编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
  3. 相比宏来说,没有额外的开销,但更安全可靠。

const加在指针前面和后面的区别

const在指针前面:
    指针指向的内容不可修改,但是指针本身的内容可以修改。

int a = 10;
int b = 20;
int const *pa = &a; 
*pa = 30; 是错误的
pa = &b; 是对的

const在指针后面:

指针本身的内容不能修改,但是指针指向的内容可以修改。

int a = 10;
int b = 20;
int *const pa = &a;
*pa = 30; 是对的
pa = &b; 是错误的

const在指针的前面和后面都有:

指针本身的内容不能修改,指针指向的内容也不可以修改(但可以通过指向的变量本身修改)。

int a = 10;
int b = 20;
int const * const pa = &a;
*pa = 30; //是错误的
pa = &b;//是错误的
a = 30; //可以通过a修改

C++中const在函数名前面和函数后面的区别

当const在函数名前面的时候修饰的是函数返回值,在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员,只能发生读操作,不能发生写操作。

放在函数名前面

普通函数或成员函数(非静态成员函数)前均可加const修饰,表示函数的返回值为const,不可修改。格式为:

const returnType functionName(param list)

修饰返回值的const,如const   a   fun2(   );   const   a*   fun3(   );  
  这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。比如:

const   rational   operator*(const   rational&   lhs,   const   rational&   rhs)  
{  
    return   rational(lhs.numerator()   *   rhs.numerator(),  
    lhs.denominator()   *   rhs.denominator());  
}  

返回值用const修饰可以防止允许这样的操作发生:

rational a,b;  
radional c;  
(a*b) = c;  

一般用const修饰返回值为对象本身的情况多用于二目操作符重载函数并产生新对象的时候。  

一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对 某个对象引用的情况。  
原因如下:  
       如果返回值为某个对象为const或某个对象的引用为const,则返回值具有const属性,则返回实例只能访问类a中的公有数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。  

放在函数名后面

函数后加const:只有类的非静态成员函数后可以加const修饰,表示该类的this指针为const类型,不能改变类的成员变量的值,即成员变量为read only(例外情况见2),任何改变成员变量的行为均为非法。此类型的函数可称为只读成员函数,格式为:

returnType functionName(param list) const

说明:类中const(函数后面加)与static不能同时修饰成员函数,原因有以下两点①C++编译器在实现const的成员函数时,为了确保该函数不能修改类的实例状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的,也就是说此时const的用法和static是冲突的;
②两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系,因此不能同时用它们。

在类中将成员函数修饰为const表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。

#include <iostream>  
using namespace std;  

class A{  
private:  
    int i;  
public:  
    void set(int n){ //set函数需要设置i的值,所以不能声明为const  
        i = n;  
    }  

    int get() const{ //get函数返回i的值,不需要对i进行修改,则可以用const修饰。防止在函数体内对i进行修改。  
        return i;  
    }  
};  

对于非const成员函数既可以调用const成员函数或变量,又可以调用非const成员函数。

const修饰函数参数

防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义。因为如果是按值传递,传给参数的仅仅是实参的副本,即使在函数体内改变了形参,实参也不会得到影响。如:

void fun(const int i){  
    i = 10;  
}  

在函数体内是不能改变i的值的,但是没有任何实际意义。
const修饰的函数参数是指针时,代表在函数体内不能修改该指针所指的内容,起到保护作用,在字符串复制的函数中保证不修改源字符串的情况下,实现字符串的复制。

void fun(const char * src, char * des){  //保护源字符串不被修改,若修改src则编译出错。  
    strcpy(des,src);  
}  
void main(){  
    char a[10]="china";  
    char b[20];  
    fun(a,b);  
    cout<<b<<endl;  
}  
而且const指针可以接收非const和const指针,而非const指针只能接收非const指针。

const修饰引用时:如果函数参数为用户自定义的类对象如:

void h(A a){  
…………  
…………  
}  

传递进来的参数a是实参对象的副本,要调用构造函数来构造这个副本,而且函数结束后要调用析构函数来释放这个副本,在空间和时间上都造成了浪费,所以函数参数为类对象的情况,推荐用引用。但按引用传递,造成了安全隐患,通过函数参数的引用可以修改实参的内部数据成员,所以用const来保护实参。

void h(const A & a){  
…………  
…………  
}  

 

使用const的一些建议  
  1   要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;  
  2   要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;  
  3   在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;  
  4   const在成员函数中的三种用法要很好的使用;  
  5   不要轻易的将函数的返回值类型定为const;  
  6   除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;  

转载:https://www.cnblogs.com/HDK2016/p/10244389.html

 https://blog.csdn.net/kaida1234/article/details/80403534

原文链接: https://www.cnblogs.com/Glucklichste/p/13279018.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++ const和constexpr

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/364524

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 下午3:36
下一篇 2023年3月2日 下午3:37

相关推荐