C++之const&constexpr

const

const修饰普通变量

{
	const int x = 12;
	const int y{13};
}

const 修饰普通变量,要在声明时初始化,即声明并定义。任何修改该变量的行为在编译时就会出错。

const修饰引用

引用正常使用时,在声明时就要初始化,初始化只能指定已经定义过的变量,为其起别名。不能用uint16_t& y = 1;这种方式用字面量来初始化,因为引用是一个变量的别名,不能为字面量取别名。也不能用不同的类型变量来初始化,如double a = 1.5; int& y = a;,会编译报错。

int main()
{
    uint16_t var = 1;
    const uint16_t con_var = 1;
    uint16_t& x = var;
    // uint16_t &con_x = con_var; // 非const引用不能绑定const变量,编译错误
    // uint16_t& y = 1; // 尝试用字面量初始化非const引用,编译报错
    // uint32_t& z = var;// 尝试用不同类型的变量来初始化非const引用,编译报错
    cout << x << endl;
    x = 3;
    cout << x << endl;
    return 0;
}

但如果用const来修饰引用的话,字面量可以初始化const引用,非相同类型但相关的类型的变量也可以初始化const引用。

int main()
{
    uint16_t var = 1;
    uint16_t &x = var;
    const uint16_t &y = 12;
    const uint32_t &z = var;
    cout << "value of x,y,z,var:" << x << " " << y << " " << 
    	z << " " << var << endl;
    x = 3;	// 可以用非const变量修改变量
    // z= 13; // 尝试用const引用修改变量,编译错误
    cout << "value of x,y,z,var:" << x << " " << y << " " << 
    	z << " " << var << endl;
    return 0;
}

输出结果如下,可以看到我们成功的用非const引用修改了var的值,但const引用z的值并没有变化。

value of x,y,z,var:1 12 1 1
value of x,y,z,var:3 12 1 3

const修饰指针变量

const修饰指针变量分为三种情况:

  1. const修饰指向的内容,内容不可变,但指针指向可以变。如const int *p = &var;或者int const *p = &var;这种情况,允许改变指针的指向,但不能通过指针改变指向的内容。
int main()
{
    int var1 = 1;
    int var2 = 2;
    const int* p1 = &var1;
    // int const* p1 = &var1; // 与上一行的作用相同
    p1 = &var2; // 可以改变指针的指向
    cout<<*p1<<endl;
    //*p1 = 3; // 尝试通过指针改变指向的内容,编译错误
    return 0;
}
  1. const修饰指针。即内容可以变,但指针指向不能变。如int *const p = &var;

注意不能用*const int p = &var;这种方式声明定义变量,原因是*前必须要有修饰符,可以是基本类型,也可以是const。

int main()
{
    int var1 = 1;
    int var2 = 2;
    int *const ptr = &var1;
    *ptr = 10;	// 可以通过指针改变指向的内容
    //ptr = &var2; // 尝试改变指针的指向,编译错误
    cout<<*ptr<<endl;
    return 0;
}

3.第三种情况是前面两个的组合,即既不能修改指向内容,又不能修改指向。如const int * const ptr =&var;或者inti const * const ptr =&var; 此时只能读取指针或者指针指向内容的值。

int main()
{
    int var1 = 1;
    int var2 = 2;
    const int * const ptr = &var1;
    //int const * const ptr = &var1; // 与上一行的作用相同
    //*ptr = 10; // 尝试通过指针改变指向的内容,编译错误
    //ptr = &var2; // 尝试改变指针的指向,编译错误
    cout<<*ptr<<" "<<ptr<<endl;
    return 0;
}

根据const修饰符在*左边或者右边,我们可以快速判断const是来修饰指针还是修饰指向的内容。即const*左边,指向内容不能变。const*右边,指向不能变。
口诀是:左定值,右定向,const修饰不变量

const修饰函数参数

const修饰函数参数,分为三种情况。

  1. 修饰传值参数。这种做法意义不大,因为参数传值,本来就会拷贝出一个临时变量,不会改变传入参数的值。

不考虑const,参数传值不会影响传入参数原来的值

void func(const int a)
{
	cout << a << endl;
	// a++; //编译错误
}
  1. 修饰传指针参数。传指针参数,其实与传值相似,只不过拷贝的是一个临时的指针,存放的是地址。除此,其修饰方式参考【const修饰指针变量】这章

不考虑const,参数传指针,不会影响传入指针的值(即指向地址不会改变),但指针指向的内容可以改变

void func1(const int *a)
{
    cout << "[func1]a content:" << *a << endl;
    // *a = 10; // 尝试改变指针指向的内容,编译错误
    int var = 3;
    a = &var; // 这里改变了临时指针变量a的指向,但对输入的指针没有影响
    cout << "[func1]change a pointer,a content:" << *a << endl;
}
void func2(int *const a)
{
    cout << "[func2]a content:" << *a << endl;
    *a = 10; // 可以改变指针指向的内容
    int var = 3;
    // a = &var; //尝试改变指针的指向,编译错误
}
int main()
{
    int var1 = 1;
    int *ptr = &var1;
    func1(ptr);
    cout << "after func1,the content of ptr:" << *ptr << endl;
    func2(ptr);
    cout << "after func2,the content of ptr:" << *ptr << endl;
    return 0;
}

运行结果,可以看到在func1中,我们不能改变临时指针变量a指向的内容,可以改变临时指针变量a的指向,但不影响原来的ptr的指向。在func2中,我们我们不能改变临时指针变量a的指向,但能改变指向的内容,这个内容的改变,也会反映到ptr上。

[func1]a content:1
[func1]change a pointer,a content:3
after func1,the content of ptr:1
[func2]a content:1
after func2,the content of ptr:10
  1. 修饰传引用参数
    如果函数不希望修改传入的参数,又不希望拷贝值,可以用const引用作为参数。
void fun(const int& x)
{
	// x = 2; // 尝试通过const引用改变变量,编译错误
	cout << x << endl;
}

const修饰函数返回值

函数的返回值如果不是引用,则都会做一次拷贝,并且作为右值使用。函数的返回值,用const也分三种情况考虑。

  1. 普通变量(非引用,非指针),返回值只能作为右值使用,这种情况,加不加const意义不大,因为只能作为右值使用,用完即释放。
class A
{
public:
    int m_a{1};
};

const A Cmf()
{
    A tmp_a;
    tmp_a.m_a = 2;
    return tmp_a;
}

A Cpf()
{
    A tmp_a;
    tmp_a.m_a = 3;
    return tmp_a;
}

const int fun()
{
    return 1;
}

int main(void)
{
    // Cmf().m_a=12; // 尝试返回值当做左值使用,编译错误
    // Cpf().m_a=13; // 尝试返回值当做左值使用,编译错误
    cout << fun() << endl; // 基本类型返回值也只能当左值使用,const修饰与否,不影响正常操作
    A x = Cmf();
    A y = Cpf();
    cout << Cmf().m_a << " " << Cpf().m_a << endl;
    x.m_a = 12;
    y.m_a = 13;
    cout << x.m_a << " " << y.m_a << endl;
    return 0;
}
  1. 指针返回值,对于指针返回值,返回时也会做一次拷贝,返回的指针也只能作为右值使用,但可以对返回值解引用,修改返回值指针指向的内容。

    用const修饰指针返回值,参考【const修饰指针变量】这章。但因为返回的值时临时的指针变量,改变临时的指针变量指向没有意义,所以返回值是定向的指针类型没有意义,如int *const fun()或者const int * const fun()都没有意义。只有const int * fun();这种修饰方式,有实际的意义,作用是防止通过返回的指针来修改其指向的内容。

    必须要用对应类型的指针或者缩小权限的类型的指针来接收返回值。

返回指针,不要返回临时变量指针,因为临时变量会在函数返回后释放,相当于使用了野指针,运行时会出错。

int *fun1(int *p)
{
    return p;
}
const int* fun2(int *p)
{
    return p;
}
int main(void)
{
    int var = 1;
    int var2 = 2;
    *(fun1(&var)) = 11;  // 可以通过解引用,来修改指针指向内容
    //*(fun2(&var)) = 111;  // 对于定值的const int *类型指针,不能通过解引用,来修改指针指向内容,编译错误
    // fun(&var) = &var2; // 返回值作为左值,尝试修改指针指向,编译错误
    // int* p = fun2(&var); // 尝试用更大权限类型的指针接收返回值,编译错误
    const int * const ptr = fun2(&var); // 可以用更小权限类型的指针接收返回值
    cout<<*ptr<<endl;
    return 0;
}

const修饰成员变量

const修饰成员变量,与修饰普通变量唯一的区别在与可以通过初始化列表进行初始化。

class A
{
public:
    A(int a) : m_a(a){};
    const int m_a;
    // const int m_b{2.3}; // {}初始化方式,会检查类型,编译失败
    const int m_b{2};
    const int m_c = 3.2; // =方式初始化,会做相应的类型转换,不会编译失败
};
int main(void)
{
    A my_A(1);
    cout << my_A.m_a << " " << my_A.m_b << " " << my_A.m_c << endl;
    return 0;
}

const修饰成员函数

const修饰成员函数,指的是在成员函数声明的末尾加const修饰,其作用是防止成员改变成员变量。

class A
{
public:
    A(int a) : m_a(a){};
    void fun() const;
    const int m_a;
};
void A::fun() const
{
    // m_a = 2; // 尝试在const修饰的成员函数中改变成员变量的值,编译错误
    cout << m_a << endl;
}
int main(void)
{
    A my_A(1);
    my_A.fun();
    return 0;
}

static和const

不修饰成员变量
{
    static int a = 1; // a可以改变
    const int b = 2; // b是常量不可以改变
    static const int c = 2; // b是常量,不能改变
    const static int d = 3; // static和const的顺序可以交换,作用同上
}
修饰成员变量

修饰成员变量要注意的是,static修饰的成员变量无法在声明时初始化,只能在类外初始化。如果再加上const修饰,可以在类中声明的时候初始化整型的变量,不能初始化float或者double类型的变量。

类中static修饰的成员只有一份,所有对象共享,所以不能用初始化列表的方式初始化,也不能在类中初始化,只能在类外进行定义。其访问方式,可以用类名作用域的形式访问,也可以用对象访问。但const修饰的成员可以有很多份,只能用对象来访问。
但const和static两者同时作用的时候,可以在类中声明时初始化,但只能初始化整型的变量,double和float会编译报错。(编译器的问题,没有什么特殊原因)解决方法是用constexpr。

class A
{
public:
    static int m_a;
    // static int m_a = 1; // 编译报错
    const static int m_b = 2;
    static const int m_c{3};
    // const static double m_c = 3.3; // 编译报错
    // const static float m_c = 3.3; // 编译报错
    constexpr static double m_d = 3.14;
};
int A::m_a = 1;
int main(void)
{
	A::m_a = 11; // 非const修饰的静态数据可以改变
    A my_A;
    cout << my_A.m_a << " " << my_A.m_b << " " << 
    	my_A.m_c << " " << my_A.m_d << endl;
    return 0;
}

constexpr

constexpr和常量表达式

C++Primer是这样描述的。

常量表达式(const expression)是指值不会改变,并在编译阶段就能得到计算结果的表达式。显然,字面量是常量表达式,用常量表达式初始化的const对象也是常量表达式。

const int a = 20; // 20是常量表达式,a也是常量表达式
const int b = a + 1; // b是常量表达式
int c = 20 ; // c不是常量表达式
const int sz = get_size(); // sz不是常量表达式

并非所有const对象都是常量表达式,const仅标记对象为只读属性,该对象在初始化后无法再改变。如果const对象所赋初值在编译阶段就可确定,那么此const对象才是常量表达式。const对象和存储位置也没有必然联系,常量可以分布在栈、堆、静态存储区中。对于声明在函数体内的const常量,如果没有被编译优化掉,该常量存储在栈中。全局的const常量存储在全局存储区。

虽然c的初始值是一个字面值常量,但由于它的类型是一个Int类型,初始化是在运行中确定的,所以c不是一个常量表达式。sz虽然类型是一个const int,但其初始化是在运行阶段确定的,所以sz不是一个常量表达式。
C++11新标准规定,允许将变量声明为constexpr类型,以便编译器验证变量的值是否为一个常量表达式。声明为constexpr类型的变量一定是个常量,且必须用常量表达式来初始化

constexpr int a = 20; // 20是常量表达式
constexpr int b = a + 1; // a + 1是常量表达式
constexpr int c = size(); // 只有当size()是个constexpr函数时,才是一个正确的声明语句
const int d = 10;
constexpr int e = d + 1; // d + 1是一个常量表达式
int f = 20;
constexpr int g = f; // f不是一个常量表达式,编译报错

字面值常量和字面值类型

形如12,“hello”,1.2这种值都是字面值常量,字面值类型就是这些常量的类型。并不是所有可以用字面值常量初始化的类型都是字面值类型,如STL中的string类型,虽然可以用"hello"初始化,但他不是字面值类型。
常见的字面值类型是算术类型,算术类型的引用和指针。自定义类型通常不是字面值类型,除非定义constexpr的构造函数。
只有字面值类型才能声明为constexpr

constexpr int a = 12; // 字面值类型是int
constexpr uint16_t b = 12; // 字面值类型是uint16_t
constexpr const char* str = "hello"; // 字面值类型是const char*
// constexpr string str1 = "hello"; // string不是字面值类型,编译错误

对于自定义类型,当让编译器生成默认的构造函数,则类A可以视作字面值类型,可以声明为constexpr,如果显示的声明构造函数,则构造函数必须是constexpr类型函数。

class A {
    public:
    constexpr A(){};
    //A(){}; // 如果构造函数不是constexpr类型,则类A不是字面值类型,编译错误
    int m_a{1};
};
int main(void)
{
    constexpr A my_A1;
    cout << my_A1.m_a << endl;
    // my_A1.m_a =2; // my_A1是常量表达式,不能改变其成员变量,编译错误
    A my_A2;
    my_A2.m_a = 2; // my_A2不是常量表达式,可以改变其成员变量
    cout << my_A2.m_a << endl;
    return 0;
}

为什么string类型不是字面值类型,究其原因,string也是一个类,他的类的构造函数不是constexpr类型。

constexpr和指针

指针的字面值常量只有nullptr和NULL(C++建议使用nullptr)。但这不代表,指针的常量表达式初始化的时候只能用nullptr,只要编译阶段能确定指针存放的地址,即可使用constexpr声明。
如下,调试的时候可以观察,str、str1、str2、str3内的地址都相同,因为"hello"的地址在编译阶段就确定了地址。

int main(void)
{
    constexpr const char* const str = "hello";
    constexpr const char*  str1 = "hello";
    constexpr char * const  str2 = "hello"; // 会报警告,因为"hello"是const char*类型,定值
    constexpr char * str3 = "hello"; // 会报警告,因为"hello"是const char*类型,定值
    return 0;
}

从前面可知,const int * p = &var是一个指向常量的指针,即定值。但constexpr int *p = &var代表的是一个常量指针,即定向。
如下,constexpr声明的指针既可以指向常量也可以指向非常量,但要注意的是,i和j必须定义在函数体外,这样编译阶段才能确定地址。

const 修饰的变量编译阶段可以确定值,但不能确定地址,只有全局变量,在编译阶段才能确定地址。

int j = 0;
constexpr int i = 42; // i的类型是整数常量
int main(void)
{
    constexpr const int *p = &i;
    constexpr int *q = &j; // p1是一个常量指针,指向整数j
    return 0;
}

constexpr函数

constexpr函数是常量表达式的一部分,常常作为常量表达式的右值使用,如下写了个斐波拉契函数,用constexpr声明函数返回值,如果我们传参的是常量表达式(如字面值常量或者编译期间就能确定的值),就可以将函数作为常量表达式的右值,如果传入的参数不是常量表达式,函数被视作普通的内联函数,不能作为常量表达式的右值。

constexpr int fun(uint64_t n)
{
    if (n == 1)
    {
        return 1;
    }
    if (n == 2)
    {
        return 1;
    }
    return fun(n - 1) + fun(n - 2);
}
int main(void)
{
    constexpr int a = fun(4);
    const int i = 5;
    constexpr int b = fun(i);
    int j = 6;
    // constexpr int c = fun(j); // 因为j不是常量表达式,所以编译错误
    int c = fun(j); // 常量表达式函数可以作为正常函数使用
    /* 这里可以看出const和constexpr的区别,const只是标识该变量是个只读
    量,不一定在编译期间确定值,但constexpr必须要在编译期间确定 */
    const int d = fun(j); 
    return 0;
}

在C++11标准中,对于constexpr修饰的函数给了及其苛刻的限定条件:函数的返回值类型及所有形参的类型都是字面值类型,而且函数体内必须有且只有一条return语句。
在C++14中,放宽了这一限定,只保留了“函数的返回值类型及所有形参的类型都是字面值类型”,也就是说,这些值都在编译期能确定了就行。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值