C++11(原名C++0x)是C++编程语言的一次重大更新,于2011年发布。它是自1998年C++98标准后的第一个重大修订,引入了许多新特性,显著提升了语言的表现力、性能和易用性。
标准化时间线
核心新特性
标准库增强
原名为什么叫“C++0x”?
最初计划在2000年代完成,但因复杂性拖延到2011年,最终命名为C++11。
{}
的初始化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;
}
{}
进行初始化我前面在类和对象的文章中讲过,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 );
std::initializer_list
(初始化列表)上面 C++11
的列表初始化已经很方便了,但是如果只是这样,我们在对容器对象进行初始化的时候还是不方便。比如一个 vector
对象,要实现很多构造函数才能支持,vector
,vector
,有没有什么办法能让它像数组一样进行初始化吗?
初始化列表!
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;
}
C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
个人见解
说说我个人的理解吧,为什么左值和右值的核心区别是能否取地址?左值它是具有明确存储位置的对象,它是可以直接进行取地址的;但是右值,它的存储位置可能是指令、寄存器、栈空间、静态区,寄存器中存储的数据是无法被取地址的;而且即使它存储在内存中(临时对象),编译器也可能未为其分配稳定的地址。而且,右值是将亡值,若是对它进行取地址,在它的生命周期结束之后会被销毁,这就是野指针,是不被允许的。具体的情况下面讲。
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;
}
C++11 引入了右值引用和移动语义后,右值进一步细分为纯右值(prvalue)和将亡值(xvalue)。
std::string("temp")
、func()
(返回值为值类型的函数调用)。static_cast(obj)
、std::move(obj)
。std::move(func())
。func() &&
(右值引用绑定的临时对象)。prvalue
在表达式结束后销毁,xvalue
的资源会被转移。补充说明
xvalue
(如 std::move(obj)
),也可以绑定到 prvalue
(如 int&& x = 42
)。prvalue
或 xvalue
)用于初始化对象或赋值时,优先调用移动构造 / 赋值函数(如果存在)。Type& r1 = x; Type && rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。template typename remove_reference::type&& move (T&& arg);
move
是库里面的一个函数模板,本质内部是进行强制类型转换,当然它还涉及到一些引用折叠的知识,这个我们后面会细讲。template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
(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”) 会修改一个临时字符串,但调用者无法感知,这是不安全的。
std::move(左值)
?(1)std::move
的本质
std::move
并不真正移动数据,它只是将左值强制转换为右值引用(static_cast
例如:
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) 为什么右值引用不能直接绑定左值?
std::string s1 = "hello";
std::string&& s2 = s1; // 错误!右值引用不能直接绑定左值
必须用 std::move
显式标记,表示程序员明确知道 s1 可以被移动。
特性 | const T& (常量左值引用) |
T&& (右值引用) |
---|---|---|
可绑定的值 | 左值、右值(临时对象) | 仅右值(包括 std::move(左值) ) |
是否允许修改 | 否(const 限定) | 是(通常用于移动或销毁) |
典型用途 | 接受只读参数,避免拷贝 | 实现移动语义,优化资源转移 |
是否需要 std::move | 不需要 | 需要(如果源是左值) |
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
示例代码
#include
#include
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 对象销毁
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;
}
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如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;
}
};
string/vector
这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的 zkp:string
样例实现了移动构造和移动赋值,我们需要结合场景理解。移动构造函数:
ClassName(ClassName&& obj); // 参数为右值引用
ClassName&&
是一个右值引用,专门绑定右值所以拷贝构造是用来复制资源的,而移动构造是用来转移资源的,所以在某些情况下使用移动构造能减少复制,直接将资源转移,从而优化时间。
右值初始化时的调用规则
当用右值(如临时对象、函数返回值)初始化新对象时:
ClassName&&
参数,调用移动构造函数(转移资源)编译器生成规则与注意事项
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;
}
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
进行移动构造。str
对象的资源直接给 ret
,减去了构造临时对象的部分,变成了直接构造
规则上面已经说过了:
若用户定义了拷贝构造函数,编译器不会自动生成移动构造函数,此时若未显示定义移动构造函数,右值初始化会调用拷贝构造函数。
后面就不再举例了。
引用折叠(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&&
,返回右值引用。typedef
或 auto
定义引用类型时:template<typename T>
using ref = T&&;
typedef int& LRef;
typedef int&& RRef;
ref<LRef> a = x; // LRef 是 int&,ref 折叠为 int&
ref<RRef> b = 42; // RRef 是 int&&,ref 保持为 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
实现参数值类别的完美转发。
完美转发(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
的左值版本,无法利用右值的移动语义。
完美转发的实现
完美转发通过以下机制解决上述问题:
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);
}
示例:完美转发的应用
#include
#include
// 接收左值的重载
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 void func(T&& arg)
是转发引用。std::forward
转发参数:进阶应用:转发多个参数
完美转发常用于可变参数模板,转发任意数量和类型的参数:
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++ 中实现高效泛型编程的关键技术,它允许函数模板在转发参数时保持原始值类别,从而避免不必要的拷贝和移动操作。核心要点:
std::forward
:根据类型推导结果,将参数转换回原始值类别。...
声明一个接受任意数量参数的模板参数包。...
声明参数包。// 可变参数模板函数
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...
,直到参数包为空,调用终止函数。
包扩展是将参数包展开为多个独立参数的过程,常见的扩展方式有以下几种:
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
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
#include
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
的优势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; // 禁止拷贝赋值
};
通过 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)
};
允许构造函数调用同一个类的其他构造函数:
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) {} // 委托给主构造函数
};
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 不能被重写
};
允许根据对象的左值 / 右值属性重载成员函数:
class MyClass {
public:
void func() & { // 左值对象调用
std::cout << "左值调用" << std::endl;
}
void func() && { // 右值对象调用
std::cout << "右值调用" << std::endl;
}
};
MyClass obj;
obj.func(); // 左值调用
MyClass().func(); // 右值调用
C++11 新增了两个特殊成员函数:
array
、forward_list
、unordered_map
、unordered_set
这几个容器,用的最多的就是unordered_map
和unordered_set
。push
/insert
/emplace
系列接口和移动构造和移动赋值,还有initializer_list
版本的构造等。Lambda 表达式的基本形式为:
[capture list] (parameter list) -> return type { 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
捕获列表用于访问 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
默认情况下,值捕获的变量在 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(值捕获的副本被修改)
多数情况下,Lambda 的返回类型可由编译器自动推导。但在复杂情况下,需显式指定返回类型:
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
作为函数参数
常用于标准库算法,如 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
Lambda 表达式本质上是编译器自动生成的匿名函数对象(即仿函数)。例如:
auto square = [](int x) { return x * x; };
// 等价于一个仿函数类
struct CompilerGenerated {
int operator()(int x) const {
return x * x;
}
};
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
auto init_capture = [value = 42]() { return value; };
C++11 引入了三种函数包装器(Function Wrappers):std::function
、std::bind
和 std::mem_fn
,用于统一处理不同类型的可调用对象(函数、函数指针、成员函数、Lambda 等)。
std::function
功能
std::function
是一个通用的多态函数包装器,可存储、复制和调用任何可调用对象(函数、Lambda、函数对象等)。
基本语法
#include
// 存储返回类型为 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
std::bind
功能
std::bind
用于创建函数适配器,它可以绑定可调用对象的参数,或将参数重排序,生成一个新的可调用对象。
基本语法
auto new_callable = std::bind(
callable, // 原始可调用对象
arg1, arg2, ... // 参数绑定值或占位符
);
占位符
std::placeholders::_1
, std::placeholders::_2
, … 表示新可调用对象的第 1、2、… 个参数。
#include
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
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
特性 | std::function |
std::bind |
std::mem_fn |
---|---|---|---|
用途 | 通用函数包装器 | 参数绑定和重排序 | 成员函数包装 |
存储方式 | 类型擦除,存储任何可调用对象 | 生成新的可调用对象 | 生成轻量级成员函数适配器 |
参数绑定 | 不支持,需配合 std::bind |
支持绑定参数和占位符 | 不直接支持,需结合 std::bind |
成员函数调用 | 需要显式指定对象 | 需要显式指定对象 | 自动处理对象或指针 |
后续单独出一个讲解,因为还要讲如何简单实现,太长了,就不放在这里说了。
C++11 对 const 限定符进行了扩展和细化,引入了顶层 / 底层 const 的概念,并新增了 constexpr 关键字以支持编译时常量计算。
const
和底层 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 int& ref = x; // 底层const:不可通过 ref 修改 x
// ref = 20; // 错误
int x = 10;
const int* ptr1 = &x; // 合法:非const → const
int* ptr2 = ptr1; // 错误:const → 非const
int* const ptr3 = &x;
int* ptr4 = ptr3; // 合法:顶层const被忽略
constexpr int square(int x) { return x * x; }
constexpr int a = 10; // 编译时常量
constexpr int b = square(5); // 编译时计算
// constexpr int c = a + rand(); // 错误:rand() 不是编译时函数
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1); // 递归计算阶乘
}
constexpr int f5 = factorial(5); // 编译时计算 5! = 120
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 | constexpr |
---|---|---|
编译时求值 | 不保证 | 必须在编译时求值 |
变量初始化 | 运行时或编译时初始化 | 只能编译时初始化 |
函数限制 | 无特殊限制 | 必须足够简单以支持编译时计算 |
用途 | 声明只读变量 | 声明编译时常量和函数 |
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::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 auto x = 10
中,x
是 const int
,但 auto
本身会忽略顶层 const
。auto
默认不会推导为引用类型,需显式指定 auto& 或 const auto&
。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))
(带括号的表达式)通常会推导出引用类型。typedef
和 using
typedef
typedef int MyInt;
typedef std::vector<int> IntVector;
IntVector vec = {1, 2, 3};
using
别名声明using
作为更灵活的别名语法:using MyInt = int;
using IntVector = std::vector<int>;
// 等价于 typedef
using IntPtr = int*;
typedef int* IntPtr;
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)>;
特性 | 用途 | 示例 |
---|---|---|
auto |
自动推导变量类型 | auto x = 10; |
decltype |
查询表达式类型 | decltype(x) y = 20; |
using |
定义类型别名(更灵活) | using IntVec = std::vector |
typedef |
传统类型别名 | typedef std::vector |
// 模板函数中使用 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 排序
}