本篇博客结合学习笔记,梳理 C++11 最重要的特性:string底层模拟、右值引用 & 万能引用、引用折叠、完美转发、可变参数模板、移动语义、emplace 系列、类新特性、lambda、std::function、std::bind。
一、std::string 底层简单模拟
std::string底层本质是动态字符数组,维护三个核心成员变量:
-
_str:char*,指向堆上字符数组 -
_size:有效字符个数,不包含\0 -
_capacity:数组总容量
1. 迭代器实现
string迭代器本质就是原生char*指针。
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
begin()返回首元素地址,end()返回最后有效字符的下一个位置 。范围 for 底层就是调用begin()、end()。
迭代器遍历示例:
string::iterator it4 = s4.begin();
while (it4 != s4.end())
{
cout << *it4 << " ";
++it4;
}
cout << endl;
2.operator\[\]
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
返回引用,支持读写字符串内部字符,例如s2[0] = 'x';。
3.push_back 尾插
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
容量满就扩容,把字符写到_size下标,_size++,手动维护末尾\0。
4.insert 指定位置插入字符
插入需要把 pos 位置之后所有字符向后挪动一位。
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
}
5.find、substr 处理 url 示例
string url = "https://legacy.cplusplus.com/reference/string/string/rfind/";
size_t i1 = url.find(':');
if (i1 != string::npos)
{
string protocol = url.substr(0, i1);
cout << protocol << endl;
size_t i2 = url.find('/', i1+3);
if (i2 != string::npos)
{
string domain = url.substr(i1+3, i2-(i1+3));
cout << domain << endl;
string uri = url.substr(i2 + 1);
cout << uri << endl;
}
}
find找不到返回string::npos;substr(pos, len),第二个参数省略直接截取到末尾。
二、右值引用 && 引用折叠 && 完美转发
1. 左值、右值
-
左值:可以取地址,可以修改,有持久生命周期。普通变量、对象都是左值。
-
右值 :临时对象,字面量,即将销毁,不能取地址。
1+2、string("hello")都是右值。
template<class T>
void f1(T& x) // 只能接收左值
{}
template<class T>
void f2(T&& x) // 万能引用,不是单纯右值引用
{}
2. 引用折叠规则(重点)
模板推导发生引用折叠:
-
右值引用 + 右值引用 → 右值引用
T&& && → T&& -
其余所有组合折叠为左值引用
-
T& && → T& -
T& & → T& -
T&& & → T&
-
模板参数
T&&不是单纯右值引用,叫万能引用。实参传左值,推导后变成左值引用;实参传右值,推导后变成右值引用。
3.std::forward 完美转发
forward保留参数原有的左 / 右值属性。
template<class T>
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
-
如果 x 原来是左值,
forward<T>(x)产出左值; -
如果 x 原来是右值,
forward<T>(x)产出右值。
没有 forward,传入的右值引用形参在函数内部会变成左值。
三、可变参数模板
1. 模板参数包、函数参数包
template<class ...Args>
void Print(Args... args)
{
cout << sizeof...(args) << endl;
}
-
class ...Args:模板参数包,0 个或多个类型 -
Args... args:函数参数包,0 个或多个函数参数 -
sizeof...(args)获取参数包里参数个数。
调用示例:
int main()
{
double x = 2.2;
Print();
Print(1);
Print(1, string("xxxxxx"));
Print(1.1, string("xxxxxx"), x);
}
2. 递归拆解参数包
void ShowList()
{
cout << endl;
}
template<class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...);
}
编译器递归推导,不断拆出第一个参数,剩余参数继续递归,直到走到无参终止函数。
3.push_back vs emplace_back
-
push_back:接收对象,要么拷贝,要么移动。 -
emplace_back:接收构造函数参数包,直接在容器节点原地构造对象,减少拷贝 / 移动。
list<string> lt;
lt.emplace_back(10, 'x'); // 直接调用string(10,'x')在节点构造,不需要临时对象
优先推荐
emplace_back,综合场景下效率更高。直接把构造参数包往下传,去插入,实现直接构造。
四、C++11 新增默认成员函数
C++98 有 6 个默认成员函数:构造、析构、拷贝构造、拷贝赋值、取地址重载、const 取地址重载。 C++11 新增两个:移动构造函数、移动赋值运算符重载。
-
移动构造:资源窃取,把传入对象的资源偷过来,不拷贝堆内存。
-
移动赋值:把 this 旧资源释放,窃取传入对象资源。
编译器自动生成默认移动构造 / 移动赋值条件:
用户没有手写:拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数,编译器才会自动生成默认移动版本。 如果类里面有自定义成员,会调用成员的移动构造,成员不支持移动就回退拷贝。
=default强制生成默认函数;=delete删除该函数。
委托构造函数
同一个类中,一个构造函数调用本类其他构造函数,减少代码重复。
class A
{
public:
A(int a)
{}
A() : A(10) //委托构造
{}
};
委托构造不能循环调用,不能在函数体内调用,只能写在初始化列表。
继承构造函数
派生类使用using 基类::构造函数;直接继承基类全部构造函数。
class Base
{
public:
Base(int x) {}
};
class Derive : public Base
{
public:
using Base::Base;
};
五、lambda 表达式
lambda 本质:编译器生成匿名仿函数类,创建该类对象。
语法格式
[capture-list](params) -> return_type { body };
-
[捕获列表]:捕获外部变量; -
(参数):形参列表; -
->返回类型:返回值类型,可省略,编译器自动推导; -
{函数体}:函数逻辑。
捕获列表
|----------|----------------------|
| 写法 | 说明 |
| [x] | 值捕获,拷贝一份,默认 const |
| [&x] | 引用捕获,拿到变量引用,可以修改外部变量 |
| [=] | 隐式值捕获,所有外部变量值拷贝 |
| [&] | 隐式引用捕获,所有外部变量引用 |
| [=,&x] | 混合捕获:其余值捕获,x 引用捕获 |
| [] | 不捕获任何变量 |
mutable:去掉值捕获变量的 const 属性,允许修改拷贝副本,不会修改外部真实变量。
auto f = [x]() mutable { x++; };
lambda 底层原理
写一个 lambda,编译器生成一个仿函数类:
// 你写的lambda
auto lam = [](int a){return a*2;};
//编译器生成类似:
class __lambda_xxx
{
public:
int operator()(int a) const
{
return a*2;
}
};
捕获的变量变成仿函数类的成员变量。
std::function 包装器
头文件<functional>,包装可调用对象:普通函数、函数指针、lambda、仿函数。
#include <functional>
int add(int a,int b){return a+b;}
int main()
{
function<int(int,int)> f = add;
cout << f(1,2);
f = [](int a,int b){return a*b;};
cout << f(3,4);
}
类型统一,把不同类型可调用对象包装成同一个 function 类型。
std::bind 绑定器
bind 是函数适配器,修改可调用对象参数顺序、绑定固定参数。
int sub(int a,int b)
{
return a-b;
}
int main()
{
auto f1 = bind(sub, 10, placeholders::_1);
cout << f1(5); // 10-5
}
placeholders::_1,_2代表调用时传入的第 1、2 个参数。
六、小结
-
string迭代器本质char*,push_back/insert底层管理堆数组,扩容 + 挪动数据完成增容插入。 -
万能引用
T&&+forward完美转发,是移动语义基础;引用折叠是模板推导底层规则。 -
可变参数模板,递归拆解参数包;
emplace_back直接原地构造对象,性能优于push_back。 -
C++11 新增移动构造、移动赋值;委托构造、继承构造简化类代码。
-
lambda = 编译器生成匿名仿函数;捕获列表捕获外部变量;
std::function统一封装可调用对象,bind调整参数。