了解Const、指针、引用
const
- 常量定义与替换:
- 使用
const
定义变量时,如果赋值的是一个立即数(字面量),则该变量是一个常量。编译器会在编译时将所有用到这个常量的地方替换为它的初始值,这称为常量折叠。若想在运行时改变这个值,需要使用volatile
关键字,这会告诉编译器不要对这个变量进行优化。 - 当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。所以如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
- 使用
- 成员函数的
const
修饰:- 当
const
用在成员函数的后面时,表示该成员函数不会修改类的任何成员变量。这被称为常量成员函数。实际上,它表示成员函数保证不会修改this
指针指向的对象。
- 当
- 指针与
const
:const
可以用来修饰指针,根据const
位置的不同,分为顶层const
(指针常量)和底层const
(常量指针)。- 顶层
const
:表示指针本身是一个常量,即不能改变指针的指向。并不意味着不能通过指针修改其所指对象的值 - 底层
const
:表示指针指向的是一个常量,即不能通过这个指针改变指向对象的值。没有规定其所指对象必须是一个常量,仅仅是不能通过该指针修改对象的值,也就是说对象的值可以通过别的途径改变。 - 底层
const
指针不能赋值给非常量指针,因为这可能会允许通过非常量指针修改常量对象的值。 - 二级const(指向常量的常量指针):指针本身和指向的内容都不能被修改。
需要注意的是顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型部分有关。所有修饰引用的const都是底层const。
const int ci = 42;//不能改变ci的值,这是一个顶层const
- 引用与const
- 常量引用绑定非常量是合法的行为,但是不允许通过常量引用修改非常量
const
引用本身不能被用作左值,即不能被赋值。这是因为const
引用本身是常量,其值不能改变。- 注意不可以把引用绑到临时量上
- constexpr
constexpr 变量
constexpr int max_size = 10; // max_size 是一个编译期常量
这里,max_size
是一个 constexpr
变量,它的值在编译时就已经确定。
constexpr函数
constexpr int get_max_size() {
return 10; // 返回一个编译期常量
}
int main() {
constexpr int size = get_max_size(); // size 也是一个编译期常量,这里的前提在于get_max_size()是常量表达式
}
get_max_size()
是一个 constexpr
函数,它的返回值在编译时就可以确定。因此,size
也是一个编译期常量。
constexpr对象
struct Point {
constexpr Point(int x, int y) : x(x), y(y) {}
int x, y;
};
constexpr Point p1(3, 4); // p1 是一个编译期常量对象
必须注意一点,在constexpr声明中如果定义一个指针,限定符constexpr仅仅对指针有效,与指针所指的对象无关。
constexpr int *q= nullptr //这里的q是一个指针常量
constexpr int i = 42;
constexpr const int *p = &i; //这里的p是二级const
指针和引用
- 引用分为左值和右值还有万能引用
- 单说左值引用:
(1)引用可以说是更安全的指针,两者定义和修改时的汇编指令一样。
(2)指针的指向在非顶层const的情况是可变的,引用就像指针常量,不可修改指向。所以引用必须初始化,且不能为空,而指针可以不初始化和指向空。
(3)指针可以是多级的,引用只能一级
左值引用、右值引用:
先说一下左值,右值的区别:
左值是存储在内存中,有明确地址的数据,也就是说可以对其取地址。
右值不可取地址,但可以提供数据值的数据或者是将亡值。比如123这种常数或者是函数的值返回类型和move函数。
区分左值引用和右值引用,其实我们可以把右值引用当成左值引用的补丁:
左值引用的作用是:传参或者做函数返回值时,使用左值引用可以避免对象调用拷贝构造。
但是呢,左值引用有较大的局限性。
常见的比如:我们要vector扩容,要拷贝里面存着的十万个元素,或者接受一个较大的临时对象,以前的话虽然有深拷贝解决内存重复释放的问题,但肯定会消耗大量的拷贝时间,性能浪费,而且最主要的是拷贝完这些东西就不需要了。
所以右值引用就解决了这个问题,而且在STL的容器中,大量的出现了右值引用为参数的移动构造函数和其他函数,比如vector的push_back,emplace_back都能接受右值了,避免了多一次拷贝构造的开销,unique_ptr因为能拷贝右值而更方便了,最重要的是扩容时,大量的元素复制,而现在呢,只要我们的类中的移动构造函数声明为noexcept,这是因为vector扩容时要判断,如果写了移动构造并且是noexcept时,就会挨个调用移动构造,而不是拷贝构造,当对象里有很多的成员变量时,这样就大大的节省了性能开销。
Move这个函数其实非常有迷惑性,它并不是移动,仅仅是类型转换,其实底层的实现就是static_cast<&&>强制转换成右值。因为static cast在编译期间就完成了,所以运行时不会生成任何代码,连一个字节都不会生成。
而forward和move又很像,在普通情况下,两者的转换是一样的,都是强转为右值。但在函数模板且有引用折叠的时候,Move是强制把传入值转换为右值,而forward是根据传入参数是左值还是右值,返回其传入的类型。
两者都是先用remove_reference去除引用,怎么去除的呢,定义一个结构体模板,在里面重命名一下参数类型即可,为什么要去除引用呢,这是为了保证static_cast的双&&符号作用于非引用类型上,因为move和forward的模板都是万能引用,如果传入的参数恰好是一个左值引用,那么万能引用就会变成左值引用。
右值引用、移动语义、完美转发:
万能引用看起来和右值引用一样。给万能引用传左值时,就变成了左值引用,传右值变成右值引用。
什么时候不会成为万能引用呢?
但万能引用只在函数模板中,且发生了模板类型推断才会有,还不能加const限定符(volatile?)