1. 初始化列表
在 C++98 标准中,允许使用花括号 { } 对数组或者结构体元素进行统一的列表初始化。
例如:
struct Point{ int a; int b; };
int main(){
int arry[] = {1,2,3,4}; // 数组初始化
Point p = {1,2}; // 结构体初始化
return 0;
}
而 C++11 扩大了使用花括号 { } 进行初始化的使用范围,使其可以用于所有的内置类型和用户自定义的类型。
// 对数组初始化
int arry1[]{1,2,3,4,5};
// 对内置类型初始化
int x{9};
// 对用户自定义类型初始化
Point p{9,9};
// 对 vector 容器对象进行初始化
vector<int> vec = {1,2,3,4,5,6,7,8,9};
注意:
使用初始化列表时,可以添加等号 =,也可以不添加。
2. std::initializer_list
std::initializer_list 是一个轻量级的模板类,用于在函数参数中传递初始化列表,从而使编译器能够正确解析和处理使用花括号 {} 进行初始化的对象。
它在诸多容器的构造中都可以看见它的身影,例如 vector 和 list 的构造。它是定义在 <initializer_list> 头文件中的一个模板类,用于表示一个初始化列表。
基本形式如下:
std::initializer_list<T> init_list = { /* 初始化元素 */ };
std::initializer_list 提供了一种轻量级的、只读的视图来访问初始化列表中的元素。它常用于构造函数、函数参数以及返回类型,以支持统一初始化语法。
并不是所有的类型都会有 std::initializer_list 的构造函数,也并不是有 std::initializer_list 才能使用花括号 { } 进行初始化。但只要你显式写了使用 std::initializer_list 的构造函数,则进行 { } 初始化时,会优先调用该函数进行初始化。
3. auto
在 C++98 中 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以 auto 没有什么价值。
C++11 废弃 auto 的原本用法,将其用于实现自动类型推断,这一功能极大地简化了代码编写,特别是在处理复杂类型时。
举个例子:
当一个类型很复杂时,可以使用 auto 进行简写。
std::pair<int, std::string> get_user_info() { return {42, "Alice"}; }
int main() {
// 使用 auto 简化返回类型的声明
auto user = get_user_info();
return 0;
}
4. decltype
在 C++11 标准中,decltype 是一个新增的关键字,用于在编译时推导表达式的类型。与 auto 关键字类似,decltype 也用于类型推导,但两者的工作机制和应用场景有所不同。
decltype 的基本语法
decltype(expression) variable_name;
说明:
expression是任何有效的 C++ 表达式,decltype会根据这个表达式的类型来推导出variable_name的类型。
示例:
int a = 5;
double b = 3.14;
decltype(a) c = a; // c 的类型为 int
decltype(b+b) d = b; // b+b 表达式的结果类型为 double,则 d 的类型为 double
注意:
decltype 不会实际计算表达式的值,它只在编译时进行类型推导。
5. nullptr
由于 C++ 中的 NULL 被定义为字面量 0,但这样可能会带来一些问题。因为 NULL 既能表示指针常量,又能表示整形常量。
出于清晰和安全的考虑,C++11 新增了 nullptr,用于表示空指针。
6. 左值引用和右值引用
传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用的语法特性。现在,引用就分为两种:
- 左值引用
- 右值引用
什么是左值,什么是左值引用? 左值是一个表示数据的表达式,如变量名、指针等。左值一般出现在赋值符号的左边。
// 下面的指针 p,变量 b、c 均为左值
int* p = new int(0);
int b = 1;
const int c = 2;
左值引用就是给左值的引用,给左值取别名。
int*& rp = p;
int& rb = b;
const int& rc = c;
注意:
左值引用的类型一定是 左值类型+&,必须严格匹配。
什么是右值?什么是右值引用 右值也是一个表示数据的表达式,如字面常量、表达式返回值、函数返回值等等。右值只能出现在赋值符号的右边。
// 常见右值
10; // 常量
x+y; // 表达式
add(x,y); // 函数返回值
右值引用就是对右值的引用,给右值取别名。
int&& rr1=10;
double&& rr2 = x + y;
double&& rr3 = fmin(x,y);
右值是不能取地址的,但给右值取别名后,会导致右值被存储到特定的位置(一般是栈区),且可以取到该位置的地址。
也就是说:不能取字面量 10 的地址,但 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。
误区:
习惯使用左值引用后,我们对右值引用也会有一个误解:可以通过修改右值引用来修改右值。这是不切实际的,我们修改的从来只是右值引用,而并非右值!如上面的 rr1,令 rr1 = 11,并不会让 10 = 11!
巧记
区分左值和右值:左值在 赋值操作符的左边,右值在 赋值操作符的右边。区分左值引用和右值引用:左值引用是 类型+&,右值引用是 类型+&&。
6.1 右值引用的真面目
右值不能被修改,这是公认的,例如 10 永远不可能变成 11。
但为什么被引用后,就可以被修改?10 不是在常量区吗?难道被移到栈区了?
右值引用一个右值,右值本身并没有被'移动'到某个新的区域,而是右值引用的变量本身存储了右值的内容。
具体来说:
- 右值引用的变量:右值引用本身是一个左值,它有自己的存储位置(通常是栈上的某个位置)。这个变量存储了右值的内容。
- 右值的原始位置:右值的本身的原始位置(如字符常量存储在常量区)并不会因为右值引用而改变。右值引用只是提供了一个访问右值内容的途径。
右值引用的真面目——左值,左值可以被取地址,非 const 左值可以被修改。
6.2 左值引用和右值引用比较
左值引用总结:
- 普通左值引用只能绑定到左值,不能绑定到右值
- const 左值引用可以引用左值,也可以引用右值
右值引用总结:
- 右值引用可以引用右值,不能引用左值,但可以引用 move 后的左值
6.3 右值引用的意义
const 左值引用既可以引用左值又可以右值,那为什么 C++11 还要提出右值引用呢?是不是画蛇添足?并不是,左值引用在一起场景下存在短板,而右值引用恰恰能解决这个短板。
举个例子:
先今,我们自定义了一个 string 类。该类中有一个拷贝构造函数,并在类外定义了一个 to_string 函数。
to_string 函数
bit::string to_string(int value) {
bit::string str;
return str;
}
string 的拷贝构造函数
string(const string& s) : _str(nullptr) {
std::cout << "MyString(const MyString& s) -- 深拷贝" << std::endl;
string tmp(s.str);
swap(tmp);
}
用 to_string 的返回值去构造一个 string 对象 这就体现左值引用的短板了。当函数返回对象是一个局部对象,出了函数作用域就不存在了,就会调用拷贝构造创建一个新的临时对象,再用这个临时对象去拷贝构造。一共会发生两次拷贝构造,其中,创建临时对象的时候,涉及开辟新的空间。开辟新空间,很浪费资源,太多次受不了。
6.3.1 移动构造
有没有什么简单,且不吃操作的方法规避深拷贝?有的。
to_string 的返回值是一个右值,用这个右值构造 ret2,如果没有移动构造,调用就会匹配调用拷贝构造,因为 const 左值引用是可以引用右值的,这里就是一个深拷贝。
此时我们在 bit::string 中增加一个移动构造,就是用右值引用充当形参。
具体造型如下
// 移动构造
string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) {
swap(s);
}
此时再次构造 ret2,如果既有拷贝构造,又有移动构造,则会调用更为匹配的移动构造。而移动构造中没有新开空间,拷贝数据,效率提高。
移动构造的本质:将参数右值的资源窃取过来,占为己有,就不用创建临时对象,开辟空间,而是直接窃取别人的资源来构造自己。
为什么可以这样呢? 右值引用可以延长右值的生命周期,使其生命周期向右值引用看齐。这样函数中的返回值本来出了函数作用域就会被销毁,回收资源,右值引用后,就延长了生命周期,不会被回收,可以直接拿来构造新的对象,不用再创建临时对象,造成额外开销。
6.4 万能引用
'万能引用'(Universal Reference)是 C++ 编程中的一个术语,主要用于描述一种能够同时绑定到左值(lvalue)和右值(rvalue)的引用类型。
万能引用通常出现在模板编程中,允许函数模板接受任意类型的参数,从而实现更灵活和通用的代码设计。
来看以下例子
// 接受左值引用
void Fun(int& x) { std::cout << "Fun(int&): "<< x << std::endl; }
// 接受右值引用
void Fun(int&& x) { std::cout << "Fun(int&&): "<< x << std::endl; }
// 万能引用的模板函数,调用相应的 Fun 函数
template<typename T> void CallFun(T&& arg) { Fun(arg); }
在 main 中进行调用模版函数
int a = 10;
const int b = 20;
CallFun(a); // 传递左值,调用 Fun(int&)
CallFun(30); // 传递右值,调用 Fun(int&&)
运行结果:
按理说,应该会调用一个 Fun(int&&) 和一个 Fun(int&),但事实并不是这样。
为什么?
模版的万能引用只是提供了能够同时接收左值引用和右值引用的能力。但在后续的使用中,统一当成左值来使用。即进入模版函数体中,只有左值,没有右值。所以,会调用两次 Fun(int&)。
我们希望事情按照我们的期望发展。在模版函数体中,右值和左值也会保持原来的属性,不会变化。这时候,就需要完美转发。
6.5 完美转发——forward
完美转发是 C++11 引入的一项重要特性,旨在函数模板中将参数以其原始的值类别(左值或右值)传递给其他函数,确保在转发过程中不改变参数的性质。
基本造型:
std::forward<T>(arg)
说明:
arg即为要保持原始值类比的对象。
示例: 在万能转发的模版函数中,添加完美转发
template<typename T> void CallFun(T&& arg) {
Fun(std::forward<T>(arg));
}
main 照常
int main() {
int a = 10;
const int b = 20;
CallFun(a); // 传递左值,调用 Fun(int&)
CallFun(30); // 传递右值,调用 Fun(int&&)
return 0;
}
运行一下: 和我们的预期一致。
右值引用和左值引用各有各的用法,并不是随意创造的,具体怎么用,需要结合实际情况进行分析选择。


