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修饰指针(*):
int const *a = & [2] //非常量数据的常量指针
可以参考《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修饰引用(&):
const int &a=x;
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.使用枚举类型
{
enum { SIZE1 = 10, SIZE2 = 20}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
2.使用const
不能在类声明中初始化const数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道SIZE的值是什么。
{
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; }