在C++11之前,即C++98的时候,C++还被人说是C语言的变体,两个区别不是很大,因为多的继承多态类和引用重载等东西,然后将一些数据结构进行了封装,在C语言里面也是可以粗糙的实现出来的。真正开始区别C++和C语言的是从C++11开始的。
C++11概括
新增了类型处理(decltype,auto)、constexpr、类型萃取、tuple、static_assert、强枚举类型、并发支持库、强enum类型、智能指针、范围for、using、移动语义、emplace、noexcept、delete/default、变长模板、完美转发、lambda表达式、新数据结构unordered_map/set、tuple、包装器、列表初始化等等
类容多到这份博客根本讲不完,随便拿出一个东西都可以将很多东西,本博客只会将最最最重要的东西拿出来讲(这里面的都很重要,都是要学的,后续C++11的东西会在后面博客说)
范围for
for就是一个语法糖,只有具备begin(),end(),operator==,operator++迭代器接口的容器才能使用范围for
拿string举例
cpp
for (std::string::iterator it = l.begin(); it != l.end(); ++it);
for (ch& d : l) //相当于上面的
using
using就是一个typedef的另一种代替
cpp
typedef int I;
using I = int;
但是using有一个好处,可以给模板起别名,typedef是不可以的
cpp
template<class T>
class Vector {
using Self = Vector<T>;
};
列表初始化
在C++98中,我们可以用这样的进行初始化,当然也是C语言的东西:
cpp
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中,尝试让任何的对象都可以用{}进行初始化
这里我们创建一个类来辅助我们学习:
cpp
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
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;
};
ostream& operator<<(ostream& out, const Date& d) {
out << d._year <<" " << d._month << " " << d._day << endl;
return out;
}
在构造中,我们可以使用{}来构造:

这三种初始化是等价的,而且在一些场景下,使用括号初始化比较方便

第二种明显就省事一些,特别在有些类名比较长的情况下更方便
初始化设定项列表
对于下面这种情况是否都是列表初始化呢?
cpp
int main() {
vector<Date> vec = { {2026,1,1},{2004,10,24},{1949,10,1} };
}
内层的大括号确实是列表初始化,但是外层的就不是了,外层是接下来要学的初始化设定项列表。别看很长一个,其实不难。
initializer_list
它相当于一个只读的数据结构,支持迭代器,因此支持范围for。
它只有三个函数,分别是begin(),end(),size()
那么就很好使用了:
cpp
void func(const std::initializer_list<Date>&l) {
cout << "大小" << l.size() << endl;
for (std::initializer_list<Date>::iterator it = l.begin(); it != l.end(); ++it);
for (const Date& d : l) {//相当于上面的
cout << d;
}
}
int main() {
func({ {2026,1,1},{2004,10,24},{1949,10,1} });
// vector<Date> vec = { {2026,1,1},{2004,10,24},{1949,10,1} };
}
//Date(int year, int month, int day)
//Date(int year, int month, int day)
//Date(int year, int month, int day)
//大小3
//2026 1 1
//2004 10 24
//1949 10 1
通过这种列表初始化和初始化设定项列表,就可以一切都可以用{}来构造了,省事很多
开胃菜结束,开始来个大的
右值引用和移动语义
这个是C++里面极其重要的语法,它优化了很多地方传参的开销,同时我们还要了解一下在没有它之前编译器是怎么来优化的。
左值和右值
左值是存储在内存中的数据,我们可以进行地址的获取 ,它是一个持久化的类型,会有自己的变量类型。右值是构造出来或者被转换出来的临时变量,它们的生命周期短暂,没有地址没有变量名。
因此核心区别就是能否进行取地址
例如以下常见的左值右值
cpp
int main() {
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
Date(2026, 1, 1);
return 0;
}
左值引用和右值引用
我们能给左值取别名,同样也可以给右值取别名
左值引用
在没有右值引用之前,右值是怎么被引用的呢?
cpp
const Date& d = { 2026,1,1 };
const int& x = 10;
所以为什么有些函数用的不是Date &传参,而是const Date &传参,这样可以传右值又可以穿左值
右值引用
&&就是右值引用,可以直接给右值取别名
cpp
Date now{2026,1,1};
Date&& d = { 2026,1,1 };
Date&& d_ = move(now);
Date&& d__ = static_cast<Date&&>(now);
右值引用可以直接引用右值,左值需要使用std::move来强转,底层就是一个显示的强转
右值引用可以延长临时对象的生命周期,和左值是一个道理。
右值引用作为表达式被使用的时候,会变成左值,看起来很奇怪,但是下面就知道为什么了
左值引用右值引用参数匹配
那么传参的时候,如何匹配参数呢?下面我们来做个实验:

确实是符合预期的,其中如果第二个test调用在没有右值test函数情况下会走 cosnt&
移动语义
下面我们就来看这个东西到底是干嘛用的
左值引用做不到的地方
下面这种情况是做不到的:
cpp
string add(const string& x, const string& y) {
return x + y;
}
这种返回值返回一个大型的类对象,将会产生极大的拷贝,性能消耗是比较大的。在此之前是使用的这种方式规避的
cpp
void add(const string& x, const string& y, string& ret) {
ret = x + y;
}
int main() {
string ret;
add("1", "2", ret);
return 0;
}
但是说实话这样写可读性是相当的不好,写的也是不舒服。就跟你有引用你要去用指针一样。
移动构造和移动赋值
那么大的东西来了,移动构造移动赋值来解决以上问题:
这里我们换成string来研究:
cpp
#include<assert.h>
#include<string>
#include<cstring>
namespace dgj{
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];
memcpy(_str, str,_size+1);
}
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)
{
memcpy(tmp, _str,_size);
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;
};
}
这里我们看出来,移动构造移动赋值就是直接将获取到的右值资源和要构造的对象资源进行调换,因为右值是临时对象,不需要的对象,所以直接通过swap函数夺取其内容,然后用其自己的析构函数让右值自生自灭。
那么就可以解决左值解决不了的问题了:
cpp
string add(const string& x, const string& y) {
string ret = x;
for (auto ch : y) {
//cout << "pushback" << ch<<endl;
ret.push_back(ch);
}
return ret;
}
int main(){
string ret = add("111", "222");
cout << ret.c_str() << endl;
return 0;
}
add函数返回的ret是一个右值,因此在main函数构造的时候会调用移动构造,因此极大加快了构造的速度。这里我们看看运行结果:

首先构造了两个传参,然后add的ret通过拷贝构造构造,然后进行for,然后返回ret在外部构造一个临时对象string,调用移动构造,然后ret析构了,然后临时对象移动构造给main里面的ret对象,然后析构临时对象和add的两个传参对象。最后打印,main结束析构main的ret。
编译器优化
上面的执行结果是在拒绝一切优化下生成的,这里我们去掉优化看看:

是不是都看不懂了,我们加一个打印的日志

我们发现了很奇怪的现象,咋构造ret后面就没有构造了?其实是直接用main里面的ret进行构造了,这样就免去了add的ret和临时对象的创建,是不是很厉害。
他会有两种优化形式
临时对象优化
直接不产生临时对象,直接用返回值进行构造

一步到位
有些情况下,例如这里ret,他会直接构造外面的ret给内部的ret使用,本质和add传参多传一个外部的引用是一个道理的,但是编译器帮你做了

有些情况是不能被编译器优化的

以上情况如果没有移动构造就会走拷贝构造了,为什么不能被编译器优化呢?原因是vector底层已经有了对应的内存空间了(会预开辟内存),所以编译器不能直接在底层构造了,只能将上方的数据下传拷贝或者移动。
类型分类
官方还将右值进行了细化,对于构造出来的临时对象,被命名为纯右值,如果是move的左值,那么叫做将亡值
引用折叠
C++中是不能定义这种类型的 int & && x的,但是通过typedef或者using是可以的:
cpp
int main() {
using lval = int&;
using rval = int&&;
int x = 0;
lval& k1 = x; //推导为int&
lval&& k2 = x;//推导为int&
rval& k3 = x;//推导为int&
rval&& k4 = std::move(x);//推导为int&&
return 0;
}
虽然不能直接声明int&&&这种的,但是在传参的时候会出现其情况。那么就要引用折叠了。
我们在上面的测试就能看出来,如果是&那么无论传左值右值都会被推到成左值,如果是&&那么左值推导为左值,右值推导为右值。对于加了const也是一样的,直接在前面加const就行了
因此我们可以产生万能引用
cpp
void functi(int&& x) {
//接收const int& int& int&& const int&&
}
但是在函数里面用的可能比较少,因为要对不同值类型做不同的处理。所以这个是用于以下情况的

如果传入的是右值,那么最后ret底层会走移动构造吗?
答案是没有,为什么呢?因为右值引用作为对象使用的时候是左值属性,往下面传的时候就是左值了。因此我需要一个东西来转发一下:
因此有了forward:
cpp
// 1. 基础模板声明(不定义,仅用于特化)
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept;
// 2. 核心特化(简化版,对应 C++11 标准)
template <typename T>
inline T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
// 核心:static_cast 强制转换为 T&& 类型(依赖引用折叠)
return static_cast<T&&>(arg);
}
// 3. const 版本特化(处理 const 类型,可选)
template <typename T>
inline T&& forward(typename std::remove_reference<T>::type const& arg) noexcept {
return static_cast<T&&>(const_cast<typename std::remove_reference<T>::type&>(arg));
}
本质就是用remove_reference去除引用,获取存储变量,然后通过完美转发来往下传递其本来的类型。forward非常重要,因为很多情况会有函数层层调用,没有完美转发可能移动构造移动赋值不会起效果
emplace/emplace_back接口
我们知道了insert push_back,emplace和emplace_back和其是一个作用的,那他们的区别是什么呢?
emplace接口支持直接传构造参数,然后不会在表层构造然后往下传进行拷贝构造或者移动构造,而是会把构造的参数往下传直接在底层构造,从而免去一次构造

新的类功能
默认移动构造/赋值
因为新增了移动赋值和移动构造,所以默认的移动构造移动赋值在什么情况下才会有呢?
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造 。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
给成员变量声明时给缺省值
cpp
template<class T>
class Vector {
using Self = Vector<T>;
public:
private:
T* _begin = nullptr;
};
default delete
default是将某个构造让编译器强制默认生成,delete是将某个构造禁止
cpp
template<class T>
class Vector {
using Self = Vector<T>;
public:
Vector() = default;//默认构造默认生成
Vector(const Vector&) = delete;//不能拷贝
private:
T* _begin = nullptr;
};
final override
override给虚函数修饰可以判断当前虚函数是否形成重写,final修饰类的时候,表示类无法被继承,final修饰函数的时候表示函数不能被重写
STL新增
STL新增unordered_map/set 不用多说了,不知道可以看我的博客
lambda表达式
lambda表达式本质就是operator()的仿函数。
我们先复习一下仿函数:
cpp
int main() {
string a("123");
struct func {
func(string&x)
:str(x)
{}
string operator()(const string& x, const string& y) {
return x + y + str;
}
string& str;
};
func fun(a);
cout<<fun("123", "123").c_str();
return 0;
}
这样写真是太挫了,所以有了lambda表达式
cpp
auto f = [&a](const string& x, const string& y) {
return x + y + a;
};
cout<<f("123", "123").c_str();
语法
lambda表达式的格式: [capture-list] (parameters)-> return type {function boby }
capture-list]:捕捉列表,该列表总是出现在lambda 函数的开始位置,编译器根据\[来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略
-\>return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
### 捕捉列表
lambda表达式中默认只能用lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。\[x,y,\&z\]表示x和y值捕捉,z引用捕捉。
```cpp
auto f = [&a](const string& x, const string& y) {
return x + y + a;
};
```
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个\&表示隐式引l用捕捉,这样我们lambda表达式中用了那些变量,编译器就会自动捕捉那些变量。
```cpp
auto f = [&](const string& x, const string& y) {
return x + y + a;
};
auto f = [=](const string& x, const string& y) {
return x + y + a;
};
```
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。\[=,\&x\]表示其他变量隐式值捕捉,x引l用捕捉;\[\&,×,y\]表示其他变量引l用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是\&或=,并且\&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引引用捕捉。
#### 全局变量和静态变量自动捕捉
静态局部变量和全局变量不需要捕捉,lambda表达式中可以直接使用。这也意味着 lambda表达式如果定义在全局位置,捕捉列表必须为空
#### 拷贝捕捉默认为const
拷贝捕捉默认为const无法被修改,只有加了mutable才可被修改
```cpp
auto f = [a]()mutable {
a.push_back('d');
};
```
## 包装器
C语言的函数指针太不好用了,因此有了包装器
### function
std::function 是一个类模板,也是一个包装器。 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。 若 std::function 不含目标, 则称它为空。调用空std::function 的目标导致抛出 std:bad_function_call 异常。他被定义\