文章目录
1. C++11 的发展史
C++11(原名C++0x)是C++编程语言的一次重大更新,于2011年发布。它是自1998年C++98标准后的第一个重大修订,引入了许多新特性,显著提升了语言的表现力、性能和易用性。
标准化时间线
- 2003年:C++03发布(小修小补版本),随后ISO委员会启动C++0x项目(原计划200x年完成)。
- 2005年:C++委员会提出“Concepts”(概念,后因复杂度被推迟到C++20)和初始提案。
- 2007年:草案初稿完成,但因特性过多延期。
- 2010年:特性冻结,最终草案(FDIS)提交。
- 2011年8月:ISO正式批准为ISO/IEC 14882:2011(即C++11)。
- 2011年9月:标准正式发布。
核心新特性
- 自动类型推导(auto)
- 范围for循环
- 移动语义与右值引用(std::move、T&&)
解决资源复制开销,支持高效转移语义。 - Lambda表达式
- 强类型枚举(enum class)
- nullptr替代NULL
避免指针与整型的歧义。 - 变长模板(Variadic Templates)
- 委托构造函数与继承构造函数
简化构造函数的重用。
标准库增强
- 智能指针(std::shared_ptr, std::unique_ptr)
自动管理内存,避免泄漏。 - 多线程支持(, , )
原生支持并发编程。 - 正则表达式()
- 哈希表容器(std::unordered_map, std::unordered_set)
- std::function与std::bind
增强函数对象的灵活性。
原名为什么叫“C++0x”?
最初计划在2000年代完成,但因复杂性拖延到2011年,最终命名为C++11。
2. 列表初始化
2.1 C++98 中使用 {}
的初始化
C++98 中,一般的数组和结构体可以使用 {}
进行初始化
struct A
{
int _x;
int _y;
};
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[5] = { 0 };
A a = { 1, 2 };
return 0;
}
2.2 C++11 中使用 {}
进行初始化
- C++11以后想统一初始化方式,试图实现一切对象皆可用 {} 初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- 初始化的过程中,可以省略掉=
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{} 初始化会很方便
我前面在类和对象的文章中讲过,C++ 在使用=
进行初始化时,其实并不是直接使用 =
右边所给的值进行初始化的,而是先通过 =
右边的值先构造出一个临时对象,再通过拷贝构造给我们所要创建的对象的。C++11 通过优化可以减少临时对象的产生。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = { 2004, 4, 2 };
// =可以被省略掉
Date d2 { 2004, 4, 2 };
return 0;
}
运行结果:
可以看到,这里只调用了两次构造函数,而没有调用拷贝构造。如果是在 C++98 的环境下,应该是先调用一次构造函数、一次拷贝构造,再调用一次构造函数、一次拷贝构造。但是在 C++11 的优化之后,我们减少了临时对象的构造。
在容器中插入的时候
在插入的时候,我们直接使用 {}
来构造对象会更方便点。
int main()
{
vector<Date> v;
v.reserve(5);
Date d1{ 1,2,3 };
v.push_back(d1);
cout << "========" << endl;
v.push_back(Date{ 1,2,3 });
cout << "========" << endl;
v.push_back({ 3,2,1 });
return 0;
}
运行结果:
可以看到,不管是有名对象、匿名对象还是直接使用 {}
进行构造,都是调用一次构造函数,一次拷贝构造,所以使用 {}
在插入的时候效率不变,但是更方便。
当然,我前面在讲容器的时候讲到过 emplace
、emplace_back
,使用这两个函数都可以减少临时对象的产生,在容器中直接进行构造。
v.emplace_back( 3,2,1 );
2.3 std::initializer_list
(初始化列表)
上面 C++11
的列表初始化已经很方便了,但是如果只是这样,我们在对容器对象进行初始化的时候还是不方便。比如一个 vector
对象,要实现很多构造函数才能支持,vector<int> v1 = {1,2,3}
,vector<int> v2 = {1,2,3,4,5}
,有没有什么办法能让它像数组一样进行初始化吗?
初始化列表!
- C++11库中提出了一个
std:initializer_list
的类,auto il={10,20,30};//the type of il isaninitializer_list
,这个类的本质是底层开一个数组,将数据拷贝过来,std:initializer_list
内部有两个指针分别指向数组的开始和结束。 - 容器支持一个
std:initializer_list
的构造函数,也就支持任意多个值构成的{x1,x2,x3...}
进行初始化。STL中的容器支持任意多个值构成的[x1,x2,x3...}
进行初始化,就是通过std:initializer_list
的构造函数支持的。
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
for (auto e : l)
push_back(e)
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
此外,容器的赋值也支持了std:initializer_list
的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
使用就类似于数组的使用
int main()
{
vector<Date> v = { {1,2,3},{4,5,6},{7,8,9} };
return 0;
}
3. 右值引用与移动语义
C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
3.1 左值与右值
- 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,Ivalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而rvalue被解释为readvalue,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
个人见解
说说我个人的理解吧,为什么左值和右值的核心区别是能否取地址?左值它是具有明确存储位置的对象,它是可以直接进行取地址的;但是右值,它的存储位置可能是指令、寄存器、栈空间、静态区,寄存器中存储的数据是无法被取地址的;而且即使它存储在内存中(临时对象),编译器也可能未为其分配稳定的地址。而且,右值是将亡值,若是对它进行取地址,在它的生命周期结束之后会被销毁,这就是野指针,是不被允许的。具体的情况下面讲。
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10; // 字面量
x + y; // 表达式
fmin(x, y); // 函数调用表达式
string("11111"); // 函数调用表达式
return 0;
}
3.1.1 右值分类
C++11 引入了右值引用和移动语义后,右值进一步细分为纯右值(prvalue)和将亡值(xvalue)。
- 纯右值(Pure Rvalue, prvalue)
纯右值是传统意义上的右值,通常是临时对象或字面量,不能被赋值或取地址。
常见形式:
- 字面量表达式:如 42、true、“hello”(字符串字面量是数组类型的 prvalue)。
- 临时对象:如
std::string("temp")
、func()
(返回值为值类型的函数调用)。 - 算术 / 逻辑表达式结果:如 a + b、!flag。
- lambda 表达式:如 []{}()。
- 将亡值(Expiring Value, xvalue)
将亡值是 C++11 新增的概念,代表资源所有权即将转移的对象。它通常由 右值引用(&&) 生成,用于移动语义。
常见形式:
- 强制类型转换为右值引用:如
static_cast<T&&>(obj)
、std::move(obj)
。 - 返回值为右值引用的函数调用:如
std::move(func())
。 - 即将被销毁的临时对象:如
func() &&
(右值引用绑定的临时对象)。
- 右值的核心特点
- 不能取地址:右值没有持久的内存地址(如 &(a + b) 是错误的)。
- 可被移动:右值可以绑定到右值引用(T&&),触发移动构造 / 赋值。
- 生命周期短暂:
prvalue
在表达式结束后销毁,xvalue
的资源会被转移。
补充说明
- 泛左值(glvalue):包含左值和将亡值,代表一个对象的身份(可识别的内存地址)。
- 右值引用(T&&):既可以绑定到
xvalue
(如std::move(obj)
),也可以绑定到prvalue
(如int&& x = 42
)。 - 移动语义的触发:当右值(
prvalue
或xvalue
)用于初始化对象或赋值时,优先调用移动构造 / 赋值函数(如果存在)。
3.2 左值引用与右值引用
Type& r1 = x; Type && rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
template <class T> typename remove_reference<T>::type&& move (T&& arg);
move
是库里面的一个函数模板,本质内部是进行强制类型转换,当然它还涉及到一些引用折叠的知识,这个我们后面会细讲。- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
- 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
3.2.1 const 左值引用为什么可以绑定右值?
(1)语言规则允许
C++标准规定,const T&(常量左值引用)可以绑定到右值(包括纯右值和将亡值),这是为了支持以下场景:
- 延长临时对象的生命周期(临时对象在引用存在期间不会被销毁)。
- 允许函数接受临时对象作为参数(避免不必要的拷贝)。
(2)代码演示
void print(const std::string& s) {
std::cout << s << std::endl;
}
int main() {
print("hello"); // "hello" 是右值(纯右值),但可以绑定到 const std::string&
return 0;
}
"hello"
是一个右值(临时字符串),但 const std::string&
可以引用它,编译器会隐式构造一个临时 std::string
对象。
(3)为什么必须是 const?
如果允许非 const 左值引用绑定右值,可能导致意外修改临时对象,而临时对象通常是不可见的,这会造成逻辑错误:
void modify(std::string& s) { s += " world"; }
modify("hello"); // 错误!非 const 左值引用不能绑定右值
如果允许,modify(“hello”) 会修改一个临时字符串,但调用者无法感知,这是不安全的。
3.2.2 右值引用为什么可以绑定 std::move(左值)
?
(1)std::move
的本质
std::move
并不真正移动数据,它只是将左值强制转换为右值引用(static_cast<T&&>),告诉编译器:“这个对象可以被移动”。
例如:
std::string s1 = "hello";
std::string s2 = std::move(s1); // s1 被转换为右值引用,触发移动构造
这里 std::move(s1) 返回 std::string&&,它是一个将亡值(xvalue),因此可以绑定到右值引用。
(2) 右值引用的设计目的
右值引用(T&&)的引入是为了支持移动语义:
- 移动构造函数和移动赋值运算符的参数是
T&&
,它们“窃取”资源,避免深拷贝。 - 例如:
class MyString {
public:
MyString(MyString&& other) { // 移动构造函数
data_ = other.data_; // 直接“偷”指针
other.data_ = nullptr; // 原对象置空
}
private:
char* data_;
};
只有右值引用能绑定到 std::move(左值)
,从而触发移动操作。
(3) 为什么右值引用不能直接绑定左值?
- 右值引用(T&&)默认不能绑定左值,因为左值可能仍在后续代码中使用,直接移动会导致错误:
std::string s1 = "hello";
std::string&& s2 = s1; // 错误!右值引用不能直接绑定左值
必须用 std::move
显式标记,表示程序员明确知道 s1 可以被移动。
3.2.3 对比总结
特性 | const T& (常量左值引用) | T&& (右值引用) |
---|---|---|
可绑定的值 | 左值、右值(临时对象) | 仅右值(包括 std::move(左值) ) |
是否允许修改 | 否(const 限定) | 是(通常用于移动或销毁) |
典型用途 | 接受只读参数,避免拷贝 | 实现移动语义,优化资源转移 |
是否需要 std::move | 不需要 | 需要(如果源是左值) |
3.3 引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
示例代码
#include <iostream>
#include <string>
class MyClass {
public:
MyClass(std::string str) : data(std::move(str)) {
std::cout << "构造临时对象: " << data << std::endl;
}
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
std::cout << "移动构造: " << data << std::endl;
}
~MyClass() {
std::cout << "销毁对象: " << (data.empty() ? "空" : data) << std::endl;
}
void print() const {
std::cout << "对象内容: " << data << std::endl;
}
private:
std::string data;
};
void test_rvalue_ref() {
// 右值引用绑定临时对象 MyClass("临时数据")
MyClass&& rref = MyClass("临时数据");
rref.print(); // 安全调用,临时对象生命周期延长至函数结束
// 移动构造新对象(演示临时对象资源可被窃取)
MyClass moved = std::move(rref);
moved.print(); // 输出移动后的内容
}
int main() {
test_rvalue_ref();
return 0;
}
输出结果
构造临时对象: 临时数据
对象内容: 临时数据
移动构造: 临时数据
对象内容: 临时数据
销毁对象: 空 // 原临时对象(rref)资源被移动后为空
销毁对象: 临时数据 // moved 对象销毁
3.4 左值和右值的参数匹配
- C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的
f
函数,那么实参是左值会匹配f
(左值引|用),实参是const左值会匹配f(const左值引|用),实参是右值会匹配f
(右值引用)。 - 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉跟怪,下一小节我们讲右值引用的使用场景时,就能体会这样设计的价值了
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
return 0;
}
3.5 右值引用和移动语义的适用场景
3.5.1 左值引用主要使用场景回顾
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings
和generate
函数,C++98中的解决方案只能是被迫使用输出型参数解决。那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
class Solution {
public:
// 传值返回需要拷贝
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if(next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
class Solution {
public:
// 这里的传值返回拷贝代价就太大了
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
};
3.5.2 移动构造和移动赋值
- 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像
string/vector
这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的zkp:string
样例实现了移动构造和移动赋值,我们需要结合场景理解。
3.5.2.1 移动构造
移动构造函数:
ClassName(ClassName&& obj); // 参数为右值引用
- 参数:
ClassName&&
是一个右值引用,专门绑定右值 - 作用:将右值对象的资源(如指针、句柄)“转移”给当前对象,而非复制,从而避免深拷贝的开销
所以拷贝构造是用来复制资源的,而移动构造是用来转移资源的,所以在某些情况下使用移动构造能减少复制,直接将资源转移,从而优化时间。
右值初始化时的调用规则
当用右值(如临时对象、函数返回值)初始化新对象时:
- 若存在移动构造函数,编译器优先匹配
ClassName&&
参数,调用移动构造函数(转移资源) - 若不存在移动构造函数,退而求其次,调用拷贝构造函数(复制资源,即使传入的是右值)
编译器生成规则与注意事项
- C++11 中,若类未定义拷贝构造、拷贝赋值、析构函数或移动构造函数,编译器会自动生成默认的移动构造函数(仅当所有成员均支持移动时)
- 若用户定义了拷贝构造函数,编译器不会自动生成移动构造函数,此时若未显示定义移动构造函数,右值初始化会调用拷贝构造函数。
namespace zkp
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return*this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
zkp::string s1("xxxxx");
// 拷贝构造
zkp::string s2 = s1;
// 构造+移动构造,优化后直接构造
zkp::string s3 = zkp::string("yyyyy");
// 移动构造
zkp::string s4 = std::move(s1);
cout << "******************************" << endl;
return 0;
}
3.5.3 右值引用和移动语义解决传值返回问题
namespace zkp
{
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << "******************************" << endl;
return str;
}
}
// 场景1
int main()
{
zkp::string ret = zkp::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
zkp::string ret;
ret = zkp::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
右值对象构造,只有拷贝构造,没有移动构造的场景
在 C++ 中,当一个类没有移动构造函数时,使用右值进行构造会回退到使用拷贝构造函数。这种设计保证了向后兼容性,但也可能导致性能损失。
namespace zkp
{
string addString(string str1, string str2)
{
string str;
int end1 = str1.size() - 1, end2 = str2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? str1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? str2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
}
int main()
{
zkp::string ret = zkp::addString("111", "222");
cout << ret << endl;
return 0;
}
- 这里调用
addString
函数之后,函数的返回值str
会先移动构造出一个临时对象,再使用这个临时对象对ret
进行移动构造。 - 不过具体的情况还是得看编译器的优化程度,像我使用的 vs2022 会将构造出来的
str
对象的资源直接给ret
,减去了构造临时对象的部分,变成了直接构造
规则上面已经说过了:
若用户定义了拷贝构造函数,编译器不会自动生成移动构造函数,此时若未显示定义移动构造函数,右值初始化会调用拷贝构造函数。
后面就不再举例了。
3.7 引用折叠
引用折叠(Reference Collapsing)是 C++11 引入的一种类型推导机制,主要用于处理模板和自动类型推导中的多重引用问题。它是移动语义和完美转发(Perfect Forwarding)的基础。
核心规则
当出现多重引用时,C++ 遵循以下折叠规则:
- 左值引用 + 左值引用 → 左值引用
示例:T& & → T&
- 左值引用 + 右值引用 → 左值引用
示例:T& && → T&
- 右值引用 + 左值引用 → 左值引用
示例:T&& & → T&
- 右值引用 + 右值引用 → 右值引用
示例:T&& && → T&&
简而言之:只要有左值引用参与,结果就是左值引用;只有两个都是右值引用时,结果才是右值引用。
引用折叠主要在以下场景中起作用:
- 模板类型推导
在模板函数中,若参数为T&&
(转发引用),根据传入的值类别(左值或右值),T 会被推导为不同类型,进而触发引用折叠。
template<typename T>
void forward(T&& arg) {
// 根据 arg 的值类别,T 可能被推导为 T& 或 T
process(std::forward<T>(arg)); // 完美转发
}
int x = 42;
forward(x); // 传入左值:T 被推导为 int&,T&& 折叠为 int&
forward(123); // 传入右值:T 被推导为 int,T&& 保持为 int&&
std::forward
的实现
std::forward
利用引用折叠实现参数的完美转发:
template<typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}
- 若
T
是int&
,则T&&
折叠为int&
,返回左值引用。 - 若
T
是int
,则T&&
保持为int&&
,返回右值引用。
- 类型别名与 auto
当使用typedef
或auto
定义引用类型时:
template<typename T>
using ref = T&&;
typedef int& LRef;
typedef int&& RRef;
ref<LRef> a = x; // LRef 是 int&,ref<int&> 折叠为 int&
ref<RRef> b = 42; // RRef 是 int&&,ref<int&&> 保持为 int&&
为什么需要引用折叠?
引用折叠是为了支持移动语义和完美转发。例如:
template<typename T>
void wrapper(T&& arg) {
// 若直接传递 arg,无论传入左值还是右值,arg 本身都是左值
// 使用 std::forward 保持参数的原始值类别
process(std::forward<T>(arg));
}
- 当传入左值时,
T
被推导为T&
,T&&
折叠为T&
,std::forward
返回左值引用。 - 当传入右值时,
T
被推导为T
,T&&
保持为T&&
,std::forward
返回右值引用。
总结
引用折叠是 C++ 类型系统中的一个重要机制,它确保了模板函数能够根据传入参数的值类别正确推导类型,并通过 std::forward
实现参数值类别的完美转发。
3.8 完美转发
完美转发(Perfect Forwarding)是 C++11 引入的一项重要特性,用于在函数模板中保持参数的原始值类别(左值或右值),从而避免不必要的拷贝或移动操作。它结合了引用折叠、** 转发引用(Universal References)** 和 std::forward
来实现这一目标。
核心问题:普通转发的缺陷
考虑一个包装函数 wrapper
,它接收参数并转发给另一个函数 process
:
template<typename T>
void wrapper(T arg) {
process(arg); // 问题:arg 始终是左值
}
int x = 42;
wrapper(x); // 传入左值
wrapper(123); // 传入右值(临时对象)
问题:
无论传入的是左值还是右值,arg 作为函数参数都是左值。因此,process(arg)
总是调用 process
的左值版本,无法利用右值的移动语义。
完美转发的实现
完美转发通过以下机制解决上述问题:
- 转发引用(Universal References)
使用T&&
作为模板参数类型,它可以绑定到任意类型的参数(左值或右值):
template<typename T>
void wrapper(T&& arg) { // T&& 是转发引用,而非右值引用
process(std::forward<T>(arg));
}
- 左值参数:
T
被推导为T&
,T&&
折叠为T&
(左值引用)。 - 右值参数:
T
被推导为T
,T&&
保持为T&&
(右值引用)。
std::forward
的作用
std::forward
用于根据T
的推导类型,将参数arg
转换回原始值类别:
template<typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}
- 若 T 是 int&(传入左值),则 T&& 折叠为 int&,返回左值引用。
- 若 T 是 int(传入右值),则 T&& 是 int&&,返回右值引用。
示例:完美转发的应用
#include <iostream>
#include <utility>
// 接收左值的重载
void process(int& x) {
std::cout << "左值版本: " << x << std::endl;
}
// 接收右值的重载
void process(int&& x) {
std::cout << "右值版本: " << x << std::endl;
}
// 完美转发包装器
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int x = 42;
wrapper(x); // 转发左值,调用 process(int&)
wrapper(123); // 转发右值,调用 process(int&&)
}
输出:
左值版本: 42
右值版本: 123
完美转发的条件
要实现完美转发,需满足以下条件:
- 模板参数为转发引用:
函数模板的参数必须是T&&
,且T
是模板参数(如 template)。- 错误示例:
void func(int&& arg)
是右值引用,不支持转发左值。 - 正确示例:
template<typename T> void func(T&& arg)
是转发引用。
- 错误示例:
- 使用
std::forward
转发参数:
在转发时必须使用 std::forward(arg),否则参数会被视为左值。 - 参数类型推导正确:
转发引用的类型推导规则依赖于传入参数的值类别(左值推导为 T&,右值推导为 T)。
进阶应用:转发多个参数
完美转发常用于可变参数模板,转发任意数量和类型的参数:
template<typename... Args>
void relay(Args&&... args) {
target(std::forward<Args>(args)...); // 转发所有参数
}
例如,std::make_unique 和 std::make_shared 就使用了完美转发来构造对象:
auto ptr = std::make_unique<MyClass>(arg1, arg2); // 完美转发参数给 MyClass 构造函数
总结
完美转发是 C++ 中实现高效泛型编程的关键技术,它允许函数模板在转发参数时保持原始值类别,从而避免不必要的拷贝和移动操作。核心要点:
- 转发引用(T&&):捕获参数的原始值类别。
std::forward
:根据类型推导结果,将参数转换回原始值类别。- 应用场景:包装函数、工厂函数、容器插入操作等需要保持参数值类别的场景。
4. 可变参数模板
4.1 基础语法及原理
- 可变参数模板的定义
- 模板参数包:使用
...
声明一个接受任意数量参数的模板参数包。 - 函数参数包:在函数参数列表中使用
...
声明参数包。
// 可变参数模板函数
template<typename... Args>
void print(Args... args) {
// 处理参数包
}
// 可变参数模板类
template<typename... Types>
struct Tuple {};
- 参数包的展开(解包)
参数包必须展开后才能使用,常见的展开方式是递归或初始化列表。
递归展开示例:
// 终止函数(递归终点)
void print() {
std::cout << std::endl;
}
// 递归展开参数包
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first;
if constexpr (sizeof...(args) > 0) { // C++17 的 if constexpr
std::cout << ", ";
}
print(args...); // 递归调用,展开参数包
}
// 使用示例
print(1, 2.5, "hello"); // 输出:1, 2.5, hello
原理:
每次递归将参数包拆分为 first
和 args...
,直到参数包为空,调用终止函数。
4.2 包扩展(Pack Expansion)
包扩展是将参数包展开为多个独立参数的过程,常见的扩展方式有以下几种:
- 初始化列表展开
通过初始化列表一次性展开参数包:
template<typename... Args>
void printAll(Args... args) {
// 初始化列表展开参数包
int dummy[] = { (std::cout << args << ", ", 0)... };
std::cout << std::endl;
}
展开过程:
printAll(1, 2.5, "hello");
// 展开为:
int dummy[] = { (std::cout << 1 << ", ", 0),
(std::cout << 2.5 << ", ", 0),
(std::cout << "hello" << ", ", 0) };
- 表达式展开
在任意表达式中展开参数包:
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // C++17 折叠表达式
}
// 使用示例
auto result = sum(1, 2, 3, 4); // 结果:10
- 模板参数包展开
在模板定义中展开参数包:
template<typename... Args>
struct MyTuple {
static constexpr size_t size = sizeof...(Args); // 参数包大小
};
// 使用示例
MyTuple<int, double, char>::size; // 结果:3
4.3 emplace 系列接口
emplace
系列接口的作用
emplace
系列接口(如emplace_back
、emplace
、emplace_hint
等)是 C++11 为容器引入的新特性,用于直接在容器中构造对象,避免临时对象的拷贝或移动。- 原理:完美转发 + 可变参数模板
emplace
接口使用可变参数模板和完美转发接收构造参数,直接在容器内存中构造对象:
template<typename... Args>
void push_back(Args&&... args) {
// 完美转发参数到对象构造函数
allocator_traits::construct(alloc, p, std::forward<Args>(args)...);
}
- 示例:
vector
的emplace_back
#include <vector>
#include <string>
struct Person {
std::string name;
int age;
// 构造函数
Person(std::string n, int a) : name(std::move(n)), age(a) {
std::cout << "构造 Person: " << name << ", " << age << std::endl;
}
};
int main() {
std::vector<Person> people;
// 使用 push_back:需要先构造临时对象,再移动到容器中
people.push_back(Person("Alice", 25)); // 两次构造(临时对象 + 移动构造)
// 使用 emplace_back:直接在容器中构造对象
people.emplace_back("Bob", 30); // 一次构造
}
输出:
构造 Person: Alice, 25 // push_back 的临时对象
构造 Person: Alice, 25 // push_back 的移动构造
构造 Person: Bob, 30 // emplace_back 直接构造
emplace
的优势
- 效率更高:避免了临时对象的创建和移动。
- 支持不可移动 / 拷贝的对象:可直接构造无法移动或拷贝的对象。
- 语法更简洁:无需显式创建对象。
5. 新的类功能
5.1 默认函数控制
C++11 允许显式控制特殊成员函数的生成:
= default
强制编译器生成默认版本的特殊成员函数:
class MyClass {
public:
MyClass() = default; // 强制生成默认构造函数
MyClass(const MyClass&) = default; // 强制生成拷贝构造函数
MyClass& operator=(const MyClass&) = default; // 强制生成拷贝赋值运算符
~MyClass() = default; // 强制生成析构函数
};
= delete
禁止生成某些特殊成员函数(如禁止拷贝):
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造
NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};
5.2 继承构造函数
通过 using
声明继承基类的构造函数:
class Base {
public:
Base(int x) {}
Base(double x, int y) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承 Base 的所有构造函数
// 无需手动定义 Derived(int x) 和 Derived(double x, int y)
};
5.3 委托构造函数
允许构造函数调用同一个类的其他构造函数:
class MyClass {
private:
int x, y, z;
public:
MyClass(int a, int b) : x(a), y(b), z(0) {} // 主构造函数
MyClass(int a) : MyClass(a, 0) {} // 委托给主构造函数
MyClass() : MyClass(0, 0) {} // 委托给主构造函数
};
5.4 final 和 override 关键字
override
显式声明函数重写基类虚函数,防止意外错误:
class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override {} // 明确表示重写基类虚函数
};
final
阻止类被继承或函数被重写:
class Base final { // Base 不能被继承
public:
virtual void func() final {} // func 不能被重写
};
5.5 右值引用成员函数
允许根据对象的左值 / 右值属性重载成员函数:
class MyClass {
public:
void func() & { // 左值对象调用
std::cout << "左值调用" << std::endl;
}
void func() && { // 右值对象调用
std::cout << "右值调用" << std::endl;
}
};
MyClass obj;
obj.func(); // 左值调用
MyClass().func(); // 右值调用
5.6 新增的特殊成员函数
C++11 新增了两个特殊成员函数:
- 移动构造函数:T(T&& other)
- 移动赋值运算符:T& operator=(T&& other)
6. STL 中一些变化
- C++11 新增了
array
、forward_list
、unordered_map
、unordered_set
这几个容器,用的最多的就是unordered_map
和unordered_set
。 - 接口方面新增了右值引用和移动语义相关的
push
/insert
/emplace
系列接口和移动构造和移动赋值,还有initializer_list
版本的构造等。 - 容器的范围 for 遍历,这个在前面的容器也讲过了,这里不再过多赘述。
7. lambda
7.1 基本语法
Lambda 表达式的基本形式为:
[capture list] (parameter list) -> return type { function body }
- 捕获列表(capture list):捕获外部变量,可值捕获或引用捕获。
- 参数列表(parameter list):与普通函数的参数列表类似。
- 返回类型(type):可省略,由编译器自动推导(使用 auto)。
- 函数体(function body):实现具体功能的代码。
示例:
// 无参数、无捕获、返回 int
auto add = []() -> int { return 3 + 4; };
std::cout << add() << std::endl; // 输出 7
// 简化形式:省略返回类型,自动推导
auto multiply = [](int a, int b) { return a * b; };
std::cout << multiply(3, 4) << std::endl; // 输出 12
7.2 捕获列表
捕获列表用于访问 Lambda 外部的变量,有以下几种形式:
值捕获
按值捕获外部变量,复制一份到 Lambda 内部:
int x = 10;
auto func = [x]() { return x + 5; }; // 捕获 x 的值
std::cout << func() << std::endl; // 输出 15
引用捕获
按引用捕获外部变量,直接访问外部变量:
int y = 20;
auto ref_func = [&y]() { y += 5; }; // 引用捕获 y
ref_func();
std::cout << y << std::endl; // 输出 25
混合捕获
同时使用值捕获和引用捕获:
int a = 5, b = 10;
auto mixed = [a, &b]() { return a + (b *= 2); }; // a 值捕获,b 引用捕获
std::cout << mixed() << std::endl; // 输出 5 + 20 = 25
std::cout << b << std::endl; // 输出 20(b 被修改)
隐式捕获
使用 [=] 表示值捕获所有外部变量,[&] 表示引用捕获所有外部变量:
int total = 0, count = 5;
auto sum = [&]() { total += count; }; // 引用捕获所有变量
sum();
std::cout << total << std::endl; // 输出 5
7.3 可变 Lambda
默认情况下,值捕获的变量在 Lambda 内部是只读的。使用 mutable 关键字可以修改值捕获的变量:
int value = 10;
auto mutable_lambda = [value]() mutable {
value += 5; // 允许修改值捕获的变量
return value;
};
std::cout << mutable_lambda() << std::endl; // 输出 15
std::cout << value << std::endl; // 外部 value 仍为 10(值捕获的副本被修改)
7.4 返回类型推导
多数情况下,Lambda 的返回类型可由编译器自动推导。但在复杂情况下,需显式指定返回类型:
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
7.5 实际应用场景
作为函数参数
常用于标准库算法,如 std::sort、std::find_if 等:
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
// 使用 Lambda 自定义排序规则
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
// 使用 Lambda 查找第一个大于 5 的元素
auto it = std::find_if(nums.begin(), nums.end(), [](int x) {
return x > 5;
});
延迟执行
将 Lambda 存储在变量中,稍后执行:
auto delayed = [](int x) { return x * x; };
// ... 其他代码 ...
std::cout << delayed(4) << std::endl; // 输出 16
闭包示例
Lambda 可以捕获上下文状态,形成闭包:
auto counter = [count = 0]() mutable {
return ++count; // 每次调用递增计数
};
std::cout << counter() << std::endl; // 输出 1
std::cout << counter() << std::endl; // 输出 2
7.6 Lambda 的底层实现
Lambda 表达式本质上是编译器自动生成的匿名函数对象(即仿函数)。例如:
auto square = [](int x) { return x * x; };
// 等价于一个仿函数类
struct CompilerGenerated {
int operator()(int x) const {
return x * x;
}
};
7.7 C++14 及以后的扩展
- 泛型 Lambda(C++14):使用 auto 参数:
auto generic = [](auto a, auto b) { return a + b; };
std::cout << generic(1, 2) << std::endl; // 输出 3
std::cout << generic(1.5, 2.5) << std::endl; // 输出 4.0
- 初始化捕获(C++14):捕获时初始化新变量:
auto init_capture = [value = 42]() { return value; };
8. 包装器
C++11 引入了三种函数包装器(Function Wrappers):std::function
、std::bind
和 std::mem_fn
,用于统一处理不同类型的可调用对象(函数、函数指针、成员函数、Lambda 等)。
8.1 std::function
功能
std::function
是一个通用的多态函数包装器,可存储、复制和调用任何可调用对象(函数、Lambda、函数对象等)。
基本语法
#include <functional>
// 存储返回类型为 R,参数为 Args... 的可调用对象
std::function<R(Args...)> func;
示例
// 存储普通函数
int add(int a, int b) { return a + b; }
std::function<int(int, int)> func1 = add;
std::cout << func1(3, 4) << std::endl; // 输出 7
// 存储 Lambda
auto multiply = [](int a, int b) { return a * b; };
std::function<int(int, int)> func2 = multiply;
std::cout << func2(3, 4) << std::endl; // 输出 12
// 存储成员函数
struct MyClass {
int value;
int getValue() const { return value; }
};
std::function<int(const MyClass&)> func3 = &MyClass::getValue;
MyClass obj{42};
std::cout << func3(obj) << std::endl; // 输出 42
8.2 std::bind
功能
std::bind
用于创建函数适配器,它可以绑定可调用对象的参数,或将参数重排序,生成一个新的可调用对象。
基本语法
auto new_callable = std::bind(
callable, // 原始可调用对象
arg1, arg2, ... // 参数绑定值或占位符
);
占位符
std::placeholders::_1
, std::placeholders::_2
, … 表示新可调用对象的第 1、2、… 个参数。
#include <functional>
using namespace std::placeholders; // 引入占位符
// 绑定普通函数参数
int add(int a, int b) { return a + b; }
auto add5 = std::bind(add, 5, _1); // 绑定第一个参数为5,第二个参数由调用时提供
std::cout << add5(3) << std::endl; // 等价于 add(5, 3),输出 8
// 重排序参数
auto subtract = [](int a, int b) { return a - b; };
auto reversed = std::bind(subtract, _2, _1); // 交换参数顺序
std::cout << reversed(5, 3) << std::endl; // 等价于 subtract(3, 5),输出 -2
// 绑定成员函数
struct MyClass {
void print(int x) { std::cout << x << std::endl; }
};
MyClass obj;
auto bound_member = std::bind(&MyClass::print, &obj, _1); // 绑定对象和成员函数
bound_member(42); // 输出 42
8.3 std::mem_fn
功能
std::mem_fn
专门用于生成成员函数的包装器,简化成员函数的调用。
基本语法
auto mem_func = std::mem_fn(&Class::member);
// 可通过对象或指针调用:mem_func(obj, args...)
示例
struct MyClass {
int value;
int getValue() const { return value; }
void setValue(int x) { value = x; }
};
// 包装常量成员函数
auto get = std::mem_fn(&MyClass::getValue);
MyClass obj{42};
std::cout << get(obj) << std::endl; // 输出 42
// 包装非常量成员函数
auto set = std::mem_fn(&MyClass::setValue);
set(obj, 99); // 等价于 obj.setValue(99)
std::cout << obj.value << std::endl; // 输出 99
8.4 三者对比
特性 | std::function | std::bind | std::mem_fn |
---|---|---|---|
用途 | 通用函数包装器 | 参数绑定和重排序 | 成员函数包装 |
存储方式 | 类型擦除,存储任何可调用对象 | 生成新的可调用对象 | 生成轻量级成员函数适配器 |
参数绑定 | 不支持,需配合 std::bind | 支持绑定参数和占位符 | 不直接支持,需结合 std::bind |
成员函数调用 | 需要显式指定对象 | 需要显式指定对象 | 自动处理对象或指针 |
9. 智能指针
后续单独出一个讲解,因为还要讲如何简单实现,太长了,就不放在这里说了。
10. const 限定符
C++11 对 const 限定符进行了扩展和细化,引入了顶层 / 底层 const 的概念,并新增了 constexpr 关键字以支持编译时常量计算。
10.1 顶层 const
和底层 const
- 概念区分
- 顶层 const(Top-level const):指针 / 引用本身是常量,不可修改指向 / 引用的对象。
- 底层 const(Low-level const):指针 / 引用指向的对象是常量,不可通过该指针 / 引用修改对象。
- 示例
int x = 10;
// 顶层const:指针本身不可修改
int* const ptr1 = &x; // ptr1 不可指向其他地址,但 *ptr1 可修改
*ptr1 = 20; // 合法
// ptr1 = &y; // 错误:ptr1 是常量
// 底层const:指针指向的对象不可修改
const int* ptr2 = &x; // *ptr2 不可修改,但 ptr2 可指向其他地址
// *ptr2 = 20; // 错误:*ptr2 是常量
ptr2 = &y; // 合法
// 同时包含顶层和底层const
const int* const ptr3 = &x; // ptr3 和 *ptr3 均不可修改
- 引用的底层 const
引用本身必须初始化,无法重新绑定,因此引用的 const 总是底层 const:
const int& ref = x; // 底层const:不可通过 ref 修改 x
// ref = 20; // 错误
- 类型转换规则
- 底层 const:允许将非底层 const 对象转换为底层 const(安全),但反之不行(不安全)。
- 顶层 const:赋值 / 初始化时会被忽略(因为只影响对象本身,不影响数据)。
int x = 10;
const int* ptr1 = &x; // 合法:非const → const
int* ptr2 = ptr1; // 错误:const → 非const
int* const ptr3 = &x;
int* ptr4 = ptr3; // 合法:顶层const被忽略
10.2 constexpr
- 功能与目的
constexpr 用于声明可以在编译时计算的变量或函数,提高性能并支持编译时编程。 - 常量表达式变量
constexpr int square(int x) { return x * x; }
constexpr int a = 10; // 编译时常量
constexpr int b = square(5); // 编译时计算
// constexpr int c = a + rand(); // 错误:rand() 不是编译时函数
- 常量表达式函数
- 要求:
- 函数体必须足够简单(通常只有单一 return 语句)。
- 参数和返回值必须是字面量类型(如 int, double, 自定义字面量类)
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1); // 递归计算阶乘
}
constexpr int f5 = factorial(5); // 编译时计算 5! = 120
- 构造函数与类
- 常量表达式构造函数:允许创建编译时对象。
- 字面量类:所有成员变量都是字面量类型,且有 constexpr 构造函数。
struct Point {
int x, y;
constexpr Point(int a, int b) : x(a), y(b) {} // 常量表达式构造函数
constexpr int distance() const { return x * x + y * y; } // 常量表达式成员函数
};
constexpr Point p(3, 4);
constexpr int d = p.distance(); // 编译时计算距离 25
- 与 const 的区别
特性 | const | constexpr |
---|---|---|
编译时求值 | 不保证 | 必须在编译时求值 |
变量初始化 | 运行时或编译时初始化 | 只能编译时初始化 |
函数限制 | 无特殊限制 | 必须足够简单以支持编译时计算 |
用途 | 声明只读变量 | 声明编译时常量和函数 |
11. 处理类型
11.1 auto
关键字
- 自动类型推导
auto
让编译器根据初始化表达式自动推导变量类型,避免显式类型声明。
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin(); // std::vector<int>::iterator
- 函数返回值推导
// 简化冗长的类型声明
std::map<std::string, std::vector<int>> myMap;
// 传统写法
std::map<std::string, std::vector<int>>::iterator it = myMap.begin();
// 使用 auto
auto it = myMap.begin();
- 注意事项
- 必须初始化:
auto 变量
必须在定义时初始化。 - 顶层 const 丢失:
const auto x = 10
中,x
是const int
,但auto
本身会忽略顶层 const
。 - 引用推导:
auto
默认不会推导为引用类型,需显式指定auto& 或 const auto&
。
11.2 decltype
关键字
- 类型查询
decltype
用于获取表达式的类型,可在编译时确定类型。
int x = 42;
decltype(x) y = 10; // y 的类型为 int
const int& ref = x;
decltype(ref) z = x; // z 的类型为 const int&
- 与
auto
结合
// 函数返回值类型推导(C++14 前)
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
- 处理复杂表达式
struct MyStruct {
static int value;
};
decltype(MyStruct::value) var; // var 的类型为 int
- 注意事项
- 类型保留:
decltype
会保留表达式的所有类型属性(如 const、引用)。 - 表达式分类:
decltype((x))
(带括号的表达式)通常会推导出引用类型。
11.3 typedef
和 using
- 传统
typedef
用于定义类型别名:
typedef int MyInt;
typedef std::vector<int> IntVector;
IntVector vec = {1, 2, 3};
using
别名声明
C++11 引入using
作为更灵活的别名语法:
using MyInt = int;
using IntVector = std::vector<int>;
// 等价于 typedef
using IntPtr = int*;
typedef int* IntPtr;
- 模板别名(Template Aliases)
using
支持模板别名,typedef
无法实现:
// 模板别名
template<typename T>
using Vec = std::vector<T>;
Vec<int> intVec = {1, 2, 3};
Vec<double> dblVec = {1.1, 2.2};
- 函数指针别名
// 传统 typedef
typedef void (*Callback)(int);
// using 语法
using Callback = void (*)(int);
// 更简洁的写法(结合 std::function)
using Callback = std::function<void(int)>;
11.4 三者对比与应用场景
特性 | 用途 | 示例 |
---|---|---|
auto | 自动推导变量类型 | auto x = 10; |
decltype | 查询表达式类型 | decltype(x) y = 20; |
using | 定义类型别名(更灵活) | using IntVec = std::vector<int>; |
typedef | 传统类型别名 | typedef std::vector<int> IntVec; |
11.5 进阶应用
- 泛型编程
// 模板函数中使用 auto 和 decltype
template<typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b;
}
- 元编程
// 使用 decltype 和模板别名实现类型转换
template<typename T>
struct RemoveConst {
using type = typename std::remove_const<T>::type;
};
template<typename T>
using RemoveConst_t = typename RemoveConst<T>::type; // C++14 风格
const int x = 10;
RemoveConst_t<decltype(x)> y = 20; // y 的类型为 int
- 函数对象类型别名
// 使用 using 简化函数对象类型
using Comparator = std::function<bool(int, int)>;
void sortVector(std::vector<int>& vec, Comparator comp) {
// 使用 comp 排序
}