{
const int x = 12;
const int y{13};
}
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 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;
}
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,参数传值不会影响传入参数原来的值
void func(const int a)
{
cout << a << endl;
// a++; //编译错误
}
不考虑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
void fun(const int& x)
{
// x = 2; // 尝试通过const引用改变变量,编译错误
cout << x << endl;
}
函数的返回值如果不是引用,则都会做一次拷贝,并且作为右值使用。函数的返回值,用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;
}
指针返回值,对于指针返回值,返回时也会做一次拷贝,返回的指针也只能作为右值使用,但可以对返回值解引用,修改返回值指针指向的内容。
用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修饰成员变量,与修饰普通变量唯一的区别在与可以通过初始化列表进行初始化。
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修饰,其作用是防止成员改变成员变量。
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 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;
}
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类型。
指针的字面值常量只有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 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中,放宽了这一限定,只保留了“函数的返回值类型及所有形参的类型都是字面值类型”,也就是说,这些值都在编译期能确定了就行。