const 基本含义
const
属于一种修饰符,是在编译的过程中,告诉编译器const
修饰的对象是只读的、不可修改的。这会使得编译器在后续的编译过程中一旦发现我们尝试对这些对象作为左值进行修改,就会报错,也就是编译错误。使用const
修饰符的意义在于利用编译器帮助我们确认代码的正确性,确保一些我们在编写过程中认为不应该被修改的值在程序运行过程中不会修改。尽可能的使用const
能够帮助梳理代码格式并提高代码正确性,也算是规范代码的一种体现。
const 常见形式
const 修饰的基本数据类型变量
const int value = 10;
这种类型的const
修饰表示 value
的值在其生命周期内不可改变。需要注意的是,通过这种方式创建的变量需要在声明时就进行初始化。
报错原因
在其生命周期内尝试修改
const int value = 10;
value = 1;
/**
error: assignment of read-only variable 'value'
value = 1;
^
*/
在声明时未进行初始化
const int value;
/**
error: uninitialized const 'value' [-fpermissive]
const int value;
^
*/
指向 const 变量的指针
const int value_a = 1;
const int value_b = 2;
const int* p = &value_a; //指向 const 变量的指针
p = &value_b;
指针p
指向一个类型为const int
的变量,此时不能通过*p
对被指的变量的值进行修改,但是可以修改p
使p
指向不同的变量(指针指向的地址值可改变)。也就是说,p
是可变的,而*p
是不可变的。此类指针也叫常量指针,顾名思义:指向常量的指针。
报错原因
尝试修改指针指向的对象的值
const int value = 1;
const int* p = &value;
*p = 2;
/**
error: assignment of read-only location '* p'
*p = 2;
^
*/
const 修饰的指针
int value = 1;
int* const p_1 = &value;//const 修饰的指针
const int const_value = 2;
const int* const p_2 = &const_value;//const 修饰的指向 const 变量的指针
const
修饰的指针p_1
指向value
,此时可以通过指针对value
的值进行修改,但是不能修改p_1
指向的对象(指针指向的地址值不可改变)。也就是说,p
是不可变的,而*p
是根据指向对象的定义来决定可不可变的。像上面的例子中,p_1
指向的value
是普通的int
变量,那么就可以通过*p = 9
来将9
赋值给value
;但p_2
指向的const_value
是const
修饰的int
常量,那么*p
也就是只读的了。此类指针也叫指针常量,顾名思义:这个指针本身是常量。
报错原因
尝试修改指针指向的对象
int value_1 = 1, value_2 = 2;
int* const p_1 = &value_1;
p_1 = &value_2;
/**
error: assignment of read-only variable 'p_1'
p_1 = &value_2;
^
*/
const 修饰的函数参数
int fn(const string x, const int* base) {
return x.length() + *base;
}
const
修饰字符串参数x
表示在这个函数中不能对参数x
进行修改。需要注意的是当参数是一个指针时,是表明参数是一个指向 const
变量的指针,也就是说指针指向的内容不可修改,但是指针本身是可以修改的。上面const int* base
就是一个指向const int
或者int
的对象,原因是普通指针可以被隐式转换为const
修饰的指针,但const
修饰的指针不能被隐式转换为普通指针。
报错原因
尝试修改const修饰的函数参数
int fn(const string x, const int* base) {
x.clear();
return x.length() + *base;
}
/**
In function 'int fn(std::string, const int*)':
error: passing 'const string {aka const std::basic_string<char>}' as 'this' argument of 'void std::basic_string<_CharT, _Traits, _Alloc>::clear() [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]' discards qualifiers [-fpermissive]
x.clear();
^
*/
尝试修改指向const变量的指针指向的对象的值
int fn(const int* base) {
*base = 1;
return *base;
}
/**
In function 'int fn(const int*)':
error: assignment of read-only location '* base'
*base = 1;
^
*/
const 修饰的函数返回值
const int* fn(int* base) {
*base = *base + 1;
return base;
}
这种类型的const
修饰指定返回值不可修改。这个一般很少用的到,主要用于函数返回const
指针或者引用的对象可能会产生进一步修改值的情况,限定函数返回的对象不能作为左值。
报错原因
尝试修改const修饰的返回值指向的指针
const int* fn(int* base) {
*base = *base + 1;
return base;
}
int main() {
int* p = new int(0);
cout << (*fn(p) = 5);
return 0;
}
/**
In function 'int main()':
error: assignment of read-only location '* fn(p)'
cout << (*fn(p) = 5);
^
*/
const 修饰的类与结构体对象
class Test {
public:
int value;
Test() {
value = 10;
}
};
const Test test;
这种类型的const
修饰表示 test
的类或结构体的数据成员在其生命周期内不可改变并且只能调用const
修饰的成员函数(const
修饰函数的形式会在后文讲述),同样的需要在类或结构体的对象创建时初始化所有的数据成员。
报错原因
在其生命周期内尝试修改类中的任何数据成员
class Test {
public:
int value;
Test() {
value = 10;
}
void SetValue(int val) {
value = val;
}
};
const Test test;
test.SetValue(100);
/**
error: passing 'const Test' as 'this' argument of 'void Test::SetValue(int)' discards qualifiers [-fpermissive]
test.SetValue(100);
^
*/
test.value = 100;
/**
error: assignment of member 'Test::value' in read-only object
test.value = 100;
^
*/
调用没有const修饰的成员函数
class Test {
public:
int value;
Test() {
value = 10;
}
void PrintValue() {
cout << value;
}
};
const Test test;
test.PrintValue();
/**
error: passing 'const Test' as 'this' argument of 'void Test::PrintValue()' discards qualifiers [-fpermissive]
test.PrintValue();
^
*/
对象创建时没有初始化所有的数据成员
class Test {
public:
int value;
};
const Test test;
/**
error: uninitialized const 'test' [-fpermissive]
const Test test;
^
*/
这里可以更加深入的说一下,对于上面这种情况有两种解决方案:(此部分内容参考自stackoverflow)
-
给
Test
类添加一个显式的构造函数class Test { public: int value; Test(){}; }; const Test test;
在上述的情况中,定义了一个不完整的构造函数。此时编译器由于在构造函数中找不到对
value
的初始化,则会调用int
的默认构造函数,给value
初始化为0
。这里还需要提一嘴的是,编译器在构造函数中无法对所有非静态数据成员初始化时,会调用其基类的构造函数与非静态成员的默认构造函数(也就是使基本数据类型变为其默认值)。 -
通过空括号组成的初始化器新建一个无名临时对象
class Test { public: int value; }; const Test test = Test();
const Test test = Test()
是调用了Test
类的默认初始化产生一个无名的临时对象,再将这个对象通过拷贝构造初始化test
对象,也就是说在执行完成后test.value
值为0
。这种初始化的过程是先将类对象进行零初始化,再根据类的默认构造函数对内容进行初始化。
const 修饰的成员变量
// 传统风格
class Test_1 {
public:
const int value;
static const int static_value;
Test(): value(100) {
}
};
const int Test_1::static_value = 100;
// c++11风格
class Test_2 {
public:
const int value = 100;
static const int static_value = 100;
};
这种类型的const
仅限制某个成员变量是否可写,与之前的以一整个类对象不同。整体上和 const
修饰基本数据类型变量并无不同,唯一的区别在于初始化。普通的 const
成员变量必须在类的构造函数的初始化列表中或者声明时直接赋值进行进行初始化,静态 const 成员变量需要在类外部单独定义并初始化。
报错原因
未进行普通成员变量的初始化
class Test {
public:
const int value;
static const int static_value;
};
Test test;
/**
note: 'Test::Test()' is implicitly deleted because the default definition would be ill-formed:
class Test {
^
error: uninitialized const member in 'class Test'
note: 'const int Test::value' should be initialized
const int value;
^
*/
未使用正确方式初始化普通成员变量
class Test {
public:
const int value;
static const int static_value;
Test() {
value = 10;
}
};
/**
In constructor 'Test::Test()':
error: uninitialized const member in 'const int' [-fpermissive]
Test() {
^
note: 'const int Test::value' should be initialized
const int value;
^
error: assignment of read-only member 'Test::value'
value = 10;
^
*/
直接使用未使用初始化的静态成员变量
class Test {
public:
const int value = 10;
static const int static_value;
};
cout << Test::static_value;
/**
此处编译器检查时不会报错,但编译过程中会报错:
undefined reference to `Test::static_value'
collect2.exe: error: ld returned 1 exit status
*/
const 修饰的成员函数
class Test {
public:
int value = 1;
int get_value() const {
return value;
}
};
这种类型的const
限制表示在这个函数中不能对任意非mutable
修饰的的成员变量进行修改。同时需要注意的是不能用于静态函数的修饰。
这种类型的修饰的底层实现方法很有意思,记录下来方便结合思考。首先我们知道在调用一个类对象的成员函数时会给成员函数隐式的传递一个指向该对象的指针 this
来方便在该函数中使用这个对象的成员变量(相当于对于编译器是将value = 1
转换成了*this->value = 1
)。而这种类型的const
修饰就是将原本的 Test* this
参数强制转换成了 const Test* const this
,同时限制指针本身和指针指向的对象不可改变。由此一来原本可以在函数中进行的修改value
值这一操作就变成了修改const
修饰的类对象的数据成员,当然是不合法的。
此外,mutable
关键字在多线程中不安全,使用时需要注意。
报错原因
尝试修改非mutable
修饰的数据成员
class Test {
public:
int value = 1;
mutable int mutable_value = 10;
void set_value() const {
mutable_value = 10;
value = 100;
}
};
/**
error: assignment of member 'Test::value' in read-only object
value = 100;
^
*/
尝试修饰静态成员函数
class Test {
public:
int value = 1;
static int get_value() const {
return value;
}
};
/**
error: static member function 'static int Test::get_value()' cannot have cv-qualifier
static int get_value() const {
^
*/
const 与指针
前面已经详细的讲过含有const
的简单指针,接下来会接着进行拓展与深入联系的思考。
含有const的多级指针
这里由于情况较多就不单个举例了直接上表:
类型 | 修改p 值 |
修改*p 值 |
修改**p 值 |
---|---|---|---|
const int** p |
可 | 可 | 不可 |
int* const* p |
可 | 不可 | 可 |
int** const p |
不可 | 可 | 可 |
const int** const p |
不可 | 可 | 不可 |
const int* const* p |
可 | 不可 | 不可 |
int* const* const p |
不可 | 不可 | 可 |
const int* const* const p |
不可 | 不可 | 不可 |
判断const修饰的对象
当const
和指针一起出现时到底是修饰的什么可以用以下方法判断:把某个const
以及它右侧的所有const
从语句中删去,右侧剩下的*
与变量名组成的表达式就是这个const
修饰的对象。以int* const* const p
为例,删去从右往左数第一个const
后右侧剩余p
,则不可修改p
的值(也就是初始化后不能进行p = ptr_arr
之类的操作);删去从右往左数第二个const
后右侧剩余*p
,则不可修改*p
的值(也就是初始化后不能进行*p = ptr
之类的操作);同时可以发现**p
是没有const
修饰的,那么**p = new int(8)
这种操作就是合法的。
const 与引用
由于引用定义后不能再指向其他对象的特性,当出现const int& a = val
的时候const
的限制相当于是const int* const a = &val
。但使用时有许多不同点。首先非常不同的是初始化:引用可以用不同类型的对象初始化(需要满足隐式转换的条件),也可以是不可寻址的值,如常量或者一段结果为确定值的表达式。常见的形式如下:
double val = 1.414;
// 基本用法
const double& ref_double = val; // 1.414
// 隐式转换的初始化
const int& ref_int = val; // 1
// 不可寻址的常量
const int& ref_int_const = 1024; // 1024
// 结果为确定值的表达式
const double& ref_expression = val + 1.0; // 2.414
这里编译器对于常量以及表达式的处理比较有趣:对于不可寻址的值(文字常量和地址值)以及其他类型的对象,编译器为了实现引用,先生成一个临时对象,引用实际上指向该对象,但用户不能直接访问它。同时编译器要求引用临时变量的时候引用必须被 const
修饰,也上述式子中的const double& ref_expression = val + 1.0
如果变成double& ref_expression = val + 1.0
编译器则会报错。
const 与引用与地址
// 对于指针的引用
int val = 1.414;
int* const& ref_pointer = &val;
// 对于指向常量的指针的引用
const int const_val = 14.14;
const int* const& ref_pointer_const = &const_val;
初次见面的时候可能会觉得int* const& ref_pointer
里的const
有点多此一举:指向int
的指针是int*
,那么对于指向int
的指针的引用就应该是double*&
,那么其实直接写int*& ref_pointer
好像就够了?如果你尝试这样写并进行编译,会得到以下报错:
error: invalid initialization of non-const reference of type 'int*&' from an rvalue of type 'int*'
int*& ref_pointer = &val;
^
回想刚才在常量以及表达式的引用的部分中,对于表达式原理的描述,仔细想想,取地址符&
也算一个结果为确定值的表达式,也就是说&val
其实是生成了一个类型为int*
的临时变量,那么按照前述的规则,引用就必须被const
修饰。也就是说这里的const
是不可省略的。剩下对于引用的到底是什么指针就与前面在const
与指针的章节相同了,相信看到这里已经能够融会贯通了。
总结
类型 | 代码形式 | 含义 |
---|---|---|
修饰基本数据类型变量 | const int a |
a 的值不可修改 |
修饰指针指向的对象 (常量指针) | const int* a |
a 指向的对象的值不可修改 |
修饰指针(指针常量) | int* const a |
不可修改a 指向的对象 |
指向常量的常量指针 | const int* const a |
不可修改a 指向的对象 且 a 指向的对象的值不可修改 |
修饰函数参数 | void fn(const int a) {} |
a 的值在函数中不可修改 |
修饰函数返回值(指针) | const int* fn() {} |
函数的返回值不可修改 |
修饰类与结构体对象 | const MyClass a |
a 中任何成员变量都不可修改 且 只能调用const 修饰的成员函数 |
修饰类成员变量 | const int a |
成员变量不可修改 且 必须在初始化列表中完成初始化 |
修饰类静态成员变量 | const static int a |
静态成员变量不可修改 且 需要在类外部单独定义并初始化 |
修饰类成员函数 | int classFn() const {} |
函数不可修改任何未被mutable 修饰的成员变量 |
修饰引用 | const int& a |
a 的值不可修改 |
修饰对于指针的引用 | int* const& a |
a 指向的对象的值不可修改 |
修饰对于常量指针的引用 | const int* const& a |
不可修改a 指向的对象 且 a 指向的对象的值不可修改 |