目录
一、统一的列表初始化
1、{}初始化
struct Point
{
int _x;
int _y;
};
int main()
{
// 对数组进行初始化
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
// 对结构体进行初始化
Point p = { 1, 2 };
return 0;
}
随着C++11标准的推出,初始化列表的使用范围得到了显著扩大,不仅限于内置类型和结构体,而且适用于所有用户自定义类型。在使用初始化列表时,既可以包含等号(=),也可以省略。
基本类型的初始化
- 传统初始化方式:
int a = 10; // 赋值初始化
int b(10); // 直接构造初始化
int arr[] = {1, 2, 3}; // 数组初始化
- C++11列表初始化方式:
int a{10}; // 列表初始化,类型安全,避免窄化转换
int b{10}; // 同样是列表初始化,更清晰地表示意图
int arr[]{1, 2, 3}; // 列表初始化数组,更统一
类对象的初始化
假设有一个类 Point
,传统方式可能这样初始化:
class Point {
public:
int x, y;
};
Point p1; // 默认初始化,成员x和y的值不确定
Point p2 = {1, 2}; // 老式初始化,如果类定义了适当的构造函数
- C++11列表初始化 提供了更明确的初始化,尤其是对于有默认值的成员:
class Point {
public:
int x = 0, y = 0; // 成员有默认值
};
Point p1{}; // 明确的值初始化,x和y都被初始化为0
Point p2{1, 2}; // 直接列表初始化,x=1, y=2
自定义类型及构造函数
考虑一个类,其中构造函数接受一个 std::initializer_list
:
class Vector {
public:
Vector(std::initializer_list<int> list) : elements(list.begin(), list.end()) {}
private:
std::vector<int> elements;
};
// 传统方式难以直接实现这样的初始化
- C++11列表初始化 支持直接使用初始化列表构造对象:
Vector v{1, 2, 3, 4}; // 直接使用列表初始化向量
复合类型和聚合体
对于复合类型或聚合体(如结构体),列表初始化更加明显地展示了初始化过程:
struct Color {
int r, g, b;
};
// 传统初始化
Color c1; // 不安全,成员未初始化
Color c2 = {255, 127, 0}; // 初始化,但依赖于编译器支持
// C++11列表初始化
Color c3{}; // 明确的值初始化,所有成员初始化为0
Color c4{255, 127, 0}; // 清晰地初始化每个成员
从上述例子可以看出,C++11列表初始化提供了更统一、明确和类型安全的初始化方式,特别在处理复杂类型和确保所有成员都得到适当初始化方面表现出色。
2、std::initializer_list
std::initializer_list
是C++11引入的一种特殊类型,它能够存储一组指定类型的常量值序列,并提供迭代器访问这些值。下面是一个示例说明其类型:
int main()
{
// il 的类型即为 std::initializer_list<int>
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
std::initializer_list
主要用于简化构造函数参数的传递,尤其是在初始化容器对象时。C++11标准库中的许多容器如 std::vector
, std::list
, std::map
等都增加了接受 std::initializer_list
类型参数的构造函数,使得我们可以方便地用花括号列表来初始化容器。
例如:
int main()
{
// 使用 std::initializer_list 初始化容器
std::vector<int> v = { 1, 2, 3, 4 };
std::list<int> lt = { 1, 2 };
// 使用 std::initializer_list 初始化 map,其中每个元素都会被解释为 pair
std::map<std::string, std::string> dict = { { "sort", "排序" }, { "insert", "插入" } };
// 同样支持大括号对容器进行赋值操作
v = { 10, 20, 30 };
return 0;
}
3、模拟实现vector花括号操作
为了使我们自定义的 `bit::vector` 支持花括号初始化以及赋值操作,可以如下实现:
namespace bit
{
template <class T>
class vector {
public:
typedef T* iterator;
// 构造函数接收 std::initializer_list 参数
vector(std::initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename std::initializer_list<T>::iterator lit = l.begin();
// 遍历并复制 initializer_list 中的元素到新建的 vector 中
while (lit != l.end()) {
*vit++ = *lit++;
}
}
// 赋值运算符重载,接收 std::initializer_list 参数
vector<T>& operator=(std::initializer_list<T> l) {
// 创建临时对象,并用新的 initializer_list 初始化
vector<T> tmp(l);
// 通过 swap 技术完成赋值(避免自我赋值问题)
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
构造函数
- 动态分配足够存储
l
中元素的连续内存空间。 - 初始化
_start
指针指向开始位置,_finish
和_endofstorage
指针指向结束位置(这里假设没有预留额外空间)。 - 使用迭代器遍历
initializer_list
,并将其中的元素依次复制到新分配的内存空间内。
赋值运算符重载
- 创建一个临时
vector<T>
对象tmp
,并使用l
初始化它。 - 通过
std::swap()
函数交换当前对象(*this
)和临时对象tmp
的内部数据成员_start
、_finish
和_endofstorage
。这样,原先对象的数据就被临时对象的新数据所取代。 - 返回当前对象的引用
*this
,以便支持链式赋值。
二、声明
在C++11中,引入了多种简化声明的方式,特别是在模板编程中大大提高了效率和可读性。
1、自动类型推断 - auto
自动类型推断 - auto
在C++98中,auto
关键字表示变量具有局部作用域和自动存储期,但由于局部变量默认就是自动存储类型,所以当时的auto
显得并不重要。然而,在C++11中,auto
的含义发生了改变,它被用来实现自动类型推断。这意味着当你声明一个变量并使用auto
时,编译器会根据初始化表达式自动推断出该变量的类型,因此必须进行显式初始化。
示例:
int main()
{
int i = 10;
auto p = &i; // p 的类型被推断为 int*
auto pf = strcpy; // pf 的类型被推断为 char* (*)(char*, const char*)
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用auto简化迭代器声明
// map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); // it 的类型被推断为 map<string, string>::iterator
return 0;
}
2、类型推导关键字 - decltype
`decltype` 关键字允许你声明变量的类型与指定表达式的结果类型相同,这对于理解复杂的模板类型或推断函数返回类型极其有用。
// decltype 的应用场景
template <class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret; // ret 的类型由 t1 和 t2 相乘决定
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret 的类型被推断为 double
decltype(&x) p; // p 的类型被推断为 int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a'); // 根据传入参数推断 ret 的类型
return 0;
}
3、空指针常量 - nullptr
// 在C++11及以上版本中,建议使用nullptr代替NULL
void* ptr = nullptr;
4、范围for循环
范围for循环 C++11引入了范围for循环,这是一种更简洁的方式来遍历任何可迭代的对象,包括但不限于STL容器。它的语法格式如下:
for (declaration : range-expression)
statement
例如,对于上述提到的dict
容器,我们可以用范围for循环遍历:
for (const auto& pair : dict)
{
cout << pair.first << ": " << pair.second << endl;
}
在这个循环中,编译器会自动获取dict
的迭代器,并按顺序遍历容器中的每个元素,无需手动管理迭代器。每次迭代时,pair
会被隐式声明为一个引用,引用当前迭代到的键值对。
三、右值引用和移动语义
1、左值引用和右值引用
在C++11中,引入了右值引用的概念,对于我们之前熟悉的引用,现在称之为左值引用。无论是左值引用还是右值引用,它们本质上都是为对象取了一个别名。
左值与左值引用
左值是指可以出现在赋值符号左侧的表达式,例如变量名或者解引用后的指针。它们可以有持续的地址,并可以被赋新的值。特别地,被const
修饰后的左值不能被重新赋值,但我们仍可以获取其地址。左值引用正是为这些左值取的一个别名。
常见的左值包括:
- 变量(如
int a;
) - 解引用后的指针(如
*ptr;
) - 引用(如
int& ref = a;
) - 数组名称(如
int arr[5];
,数组名本身是一个左值,指向数组的第一个元素)
示例代码:
int main() {
int* p = new int(0); // p是左值
int b = 1; // b是左值
const int c = 2; // c是左值,虽然不能赋新值,但可以取地址
// 下面是对左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p; // 解引用得到左值
return 0;
}
-
int* p = new int(0);
这行代码创建了一个指向整数的指针p
,并通过new int(0)
分配了一块动态内存,并初始化这块内存的值为0
。p
是一个指向这块内存的指针,是一个左值(即可以出现在赋值表达式的左边)。 -
int b = 1;
这行代码声明了一个整型变量b
并初始化其值为1
。b
是一个左值,可以直接对其进行读写操作。 -
const int c = 2;
这行代码声明了一个const
整型变量c
并初始化其值为2
。c
是一个左值,但由于它是const
的,所以不能改变它的值,但仍然可以取它的地址。 -
int*& rp = p;
这行代码声明了一个指向整型指针的引用rp
,并将它绑定到指针p
上。这里的rp
是一个左值引用,指向的是p
。也就是说,rp
和p
指向相同的内存地址,改变rp
也会改变p
。 -
int& rb = b;
这行代码声明了一个指向整型变量的引用rb
,并将它绑定到变量b
上。这里的rb
是一个左值引用,指向的是b
。改变rb
的值也会改变b
的值。 -
const int& rc = c;
这行代码声明了一个指向const
整型变量的引用rc
,并将它绑定到const
变量c
上。这里的rc
是一个指向const
类型的左值引用,意味着它不能通过rc
改变c
的值,但可以读取c
的值。 -
int& pvalue = *p;
这行代码声明了一个指向整型变量的引用pvalue
,并将它绑定到指针p
所指向的内存地址上的值(即*p
)。这里的pvalue
是一个左值引用,指向的是p
指向的内存中的值。改变pvalue
也会改变*p
的值。
右值与右值引用
右值通常是不能出现在赋值符号左侧的表达式,比如字面量、表达式的计算结果或者是函数的返回值(此处不包括返回左值引用的情况)。右值通常不能取地址。右值引用就是对这些右值的引用,为它们取了别名。
常见的右值包括:
- 字面量(如
10
,"Hello"
) - 表达式的计算结果(如
a + b
) - 函数的返回值(如
std::max(a, b)
)
右值引用是一种特殊的引用类型,它用于引用右值。右值引用的定义语法如下:
Type&& ref;
其中 Type
是引用的类型,ref
是引用的名字。右值引用可以绑定到右值,也可以绑定到左值,但在大多数情况下,它用于绑定到临时对象。
示例代码:
int main() {
double x = 1.1, y = 2.2;
// 下面是常见的右值
10;
x + y;
fmin(x, y);
// 下面是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 下面的尝试会引发编译错误,因为右值不能出现在赋值符号左侧
// 10 = 1;
// x + y = 1;
// fmin(x, y) = 1;
return 0;
}
需要注意,右值本身是不能取地址的,但一旦我们为右值创建了一个右值引用(相当于给右值取了一个别名),这个引用的右值就可以被存储在一个具体位置,从而可以获取其地址,并且可以被修改。如果我们希望防止这种修改,可以通过使用const
修饰的右值引用来实现。
int main() {
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20; // 正确,可以修改rr1
rr2 = 5.5; // 错误,不能修改const修饰的右值引用
return 0;
}
虽然右值引用的概念可能初看起来比较抽象,但它在现代C++编程中,尤其是实现移动语义和优化临时对象的处理方面发挥着重要作用。
2、左值引用与右值引用比较
左值引用要点:
-
左值引用(non-const左值引用)仅能绑定到持久的、可命名的左值对象,例如变量名、数组元素等。例如,在例子中,我们能成功创建一个引用
ra1
指向左值变量a
,但不能将一个左值引用绑定到右值(如字面量10
)上。int a = 10; int& ra1 = a; // 正确,ra1成为了a的别名 // int& ra2 = 10; // 错误,无法将右值绑定到左值引用
-
然而,const左值引用则具有更高的灵活性,它既能绑定到左值对象,也能绑定到右值。在示例中,我们可以创建一个const左值引用
ra3
绑定到右值字面量10
,同时也能绑定到左值变量a
。const int& ra3 = 10; // 正确,ra3绑定到右值 const int& ra4 = a; // 正确,ra4成为a的const别名
右值引用要点:
-
右值引用专为右值设计,它只能绑定到临时对象或者将要销毁的对象(即右值)。例如,我们可以创建一个右值引用
r1
直接绑定到右值字面量10
上。int&&am