前言:菜鸟写博客给自己看,大佬写博客给别人看,我是菜鸟
1.历史背景
C++11是C++的第二个主要版本,第一个主要版本是C++98,它与上一个小版本C++03之间隔了八年,随后每三年更新一次版本。
2.列表初始化
2.1C++98的{}初始化
C++98中一般只有数组和结构体能够使用{}初始化
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; }
2.2C++11中的{}初始化
特点:
①:一切对象皆可用{}初始化,也叫做列表初始化
②:内置类型和自定义类型皆支持
注:对于自定义类型,本质是类型转换,正常来说会先构造临时对象,临时对象在拷贝构造给目标,有些编译器会直接优化成直接构造
③:{}初始化过程中,可以省略=
④:对于某些STL容器,在调用push/insert/push_back多参数构造对象时,列表初始化会很方便
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; } private: int _year; int _month; int _day; }; int main() { vector<Date> v; v.push_back(d1); v.push_back(Date(2025, 1, 1)); // ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐ v.push_back({ 2025, 1, 1 }); }
3.右值引用和移动语义
3.1左值和右值
左值:
一般存储在内存中的值,具有较长的生命周期,可以获取其地址
例如:
int * p = new int ( 0 );int b = 1 ;const int c = b;*p = 10 ;string s ( "111111" );s[ 0 ] = 'x' ;右值:
一般都是指临时对象,不能够获得其地址
例如:
10 ;x + y;fmin (x, y);string ( "11111" );
3.2左值引用和右值引用
左值引用:Type& r1 = x
①:一般情况下,x是一个左值,r1是x的别名,左值引用不能够引用右值。但是const左值引用可以引用右值
引用左值:int & r1 = b;int *& r2 = p;int & r3 = *p;string& r4 = s;char & r5 = s[ 0 ];const左值引用:可以引用右值const int & rx1 = 10 ;const double & rx2 = x + y;const double & rx3 = fmin (x, y);const string& rx4 = string ( "11111" );右值引用:Type&& rr1 = y
①:一般情况下,y是一个右值,rr1是y的别名,右值引用不能够引用左值。但是右值引用可以引用move(左值),相当于将左值进行类型转换
引用右值:int && rr1 = 10 ;double && rr2 = x + y;double && rr3 = fmin (x, y);string&& rr4 = string ( "11111" );move(左值):int && rrx1 = move (b);int *&& rrx2 = move (p);int && rrx3 = move (*p);string&& rrx4 = move (s);string&& rrx5 = (string&&)s;//强制类型转换②:右值引用能够延长临时对象的生命周期直到 右值引用的作用域结束注: 无论是左值引用变量还是右值引用变量(即r1、rr1)他们都属于左值!这意味着:当我写了一个参数类型为右值引用的函数A时(假设该参数为s),如果我想在其内部调用另一个右值引用的函数B,且这个参数为s,那么此时这个s 必须 move(s)变成左值。
3.3左值引用和右值引用的使用场景
3.3.1回顾左值引用的使用场景
主要用于在函数中左值引用传参和左值引用传返回值时减少拷贝。虽然做只拷贝已经能解决大多数拷贝效率问题,但是有些场景不能左值引用返回,例如在addstring函数中,返回的str作为函数内部的临时对象时,不能够使用左值引用返回。
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;//此处的str 不能够使用左值引用返回,因为str为当前函数的临时变量
} //出当前函数作用域时会调用析构
};
问:如果使用右值引用返回能够解决上述问题吗?
答:不能!右值引用也无法改变str出当前作用域被析构销毁的事实
3.3.2移动构造和移动赋值
移动构造和移动赋值类似于拷贝构造和拷贝赋值,只是参数变为了右值引用,
注:如果移动构造有额外参数则额外参数必须有缺省值
移动构造和移动赋值的本质是对引用的右值对象的资源进行窃取。
简单解释一下这个窃取:
假设现在需要将某个字符串拷贝给对象s1,
如果该字符串属于左值(已经属于另一个变量s2) 那么只能老老实实的将s2中的值拷贝给你s1
但是!如果这个字符串仅仅只是一个临时变量(右值),原本的逻辑是把临时变量对应空间的值拷贝给你s1,然后把临时空间销毁。有了移动构造后,反正临时变量的空间都是要销毁的,不如直接拿给s1用,你别销毁了,这样就减少了拷贝的次数,但是还是会调用析构临时对象,哪怕临时对象中已经什么内容都没有了
//移动构造string (string&& s){cout << "string(string&& s) -- 移动构造 " << endl;swap (s);//s是一个左值,前面已经说过了}//移动赋值string& operator =(string&& s){cout << "string& operator=(string&& s) -- 移动赋值 " << endl;swap (s);return * this ;}//在swap进行数据内容的交换,将临时对象的数据交给s1,同时销毁临时对象,这样就减少了拷贝次数,提高了效率void swap (string& s){:: swap (_str, s._str);:: swap (_size, s._size);:: swap (_capacity, s._capacity);}注:如果存在移动构造和移动赋值,在上述返回str时,str将会通过移动构造tmp,tmp在通过移动构造返回给上层变量。
3.4引用折叠
C++中不能定义引用的引用,例如 int& && r = i;这样写会直接报错,通过模板或者typedef中的类型操作可以构成引用的引用,如下代码:
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
}
注:将上述lref/rref看作 type+引用符号 的格式,不难得出→只要type或者引用符号中一个为左值引用,那么引用变量的数据类型就为左值。
进一步完善上述代码:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x) 模板参数传导给f1中的T时,引用类型为左值引用
f1<int>(n); //
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x) 模板参数传导给f1中的T时,type和引用符号都为左值引用,因此为左值引用
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x) 引用符号为左值引用,因此仍为左值引用
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x) 会保留const属性,而const引用既可以引用左值,也可以引用右值
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x) type非引用,而符号为右值引用,所以是右值引用
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x) type为左值引用,而符号为右值引用,所以是右值引用
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x) type为右值引用,而符号为右值引用,所以是右值引用
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
从上面的代码可以看到,如果函数形参为右值引用,那么当模板参数为左值引用时,就会变成左值引用,如果模板参数为右值引用时,就会变成右值引用,因此形参为右值引用的函数,且为函数模板(不是类模板中的某个成员函数!),这类函数模板也被称为万能引用。
对于万能引用:传左值就是左值引用,传右值就是右值引用,const属性会保留
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
// x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
// x不能++
Function(std::move(b)); // const 右值
return 0;
}
3.5完美转发
前面提到过:无论是左值引用变量还是右值引用变量他们都属于左值,这意味着如果将左值引用变量和右值引用变量作为参数进行传递时,他们会调用同一个函数,这显然是有问题的。为了解决上述问题,就需要通过完美转发来完成
完美转发forward<T>:本质上是一个函数模板,作用是将保存原有的属性再传递给下一层函数
4.lambda
4.1语法规则:
lambda表达式语法本身不属于C++的语法,从其他语言借鉴来的。
特点:
①.[]称为捕捉列表,捕捉为空也不能省略
②.()内为参数,参数为空时,()可以省略
③.返回值可以省略,返回类型可以自动推导
④.函数主体不能省略
显示捕捉(以上述代码为例):
对a进行引用捕捉,对b进行值捕捉,引用捕捉的值可以在函数中修改,而值捕捉不行
隐式捕捉:
[&] :隐式引用捕捉
[=] :隐式值捕捉
注:函数主体中用了哪些变量就捕捉哪些。
混合捕捉:
[=, &x] → 表示 其他变量隐式值捕捉, x引用捕捉[&, x, y]→ 表示其他变量隐式捕捉,x和y值捕捉注:使用混合捕捉时,第一值必须是&或=,&混合捕捉时后面必须是值捕捉,=混合捕捉时后面必须是引用捕捉
4.2应用场景
假设这样一个场景:淘宝上的部分商品会按各种各样的方式进行排序,原先的排序需要在自定义类内部写多个结构体实现运算符重载,现在通过lambda表达式即可实现,代码如下:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
return 0;
}
5.智能指针
5.1什么是RAII和智能指针
RAII(Resource Acquisition Is Initialization):本质是利用对象生命周期来管理获取到的动态资源,避免资源泄露。有了对象对动态资源做管理后,该动态资源在对象生命周期内使用有效,最后对象析构时自动释放资源。
智能指针:智能指针除了满足RAII的设计思路外,还要方便资源的访问,所以对象中还包含operator*/operator->/operator
5.2几种智能指针
auto_ptr(了解):C++98
特点:拷贝时,会将被拷贝对象的类中所有信息转移给新对象,同时被拷贝对象悬空,这使得对老对象进行引用/访问操作会报错。
unique_ptr:C++11
特点:不支持拷贝,支持移动,所以涉及非拷贝的情况建议使用这个
shared_ptr:C++11
特点:支持拷贝,也支持移动,底层是用引用计数的方式实现。
weak_ptr:C++11
特点:不同于上面的指针,不支持RAII,本质是用于解决shared_ptr中循环引用导致内存泄漏的问题
5.3简单设计一个shared_ptr
智能指针的本质都是:利用对象生命周期来管理获取到的动态资源,避免资源泄露。因此一定是要创建自定义类对象
对于shared_ptr(共享指针):多个对象对同一个动态资源做管理,为此何时析构这个动态资源就变得尤为重要。
因此需要引用计数来统计当前动态资源被多少个对象所引用:
①:当新增一个对象指向该动态资源时,引用计数就增加,
②:当其中一个对象生命周期结束时,引用计数减少,
③:当计数为零时,需要析构该动态资源
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)//防止隐式类型转换
:_ptr(ptr), _pcount(new int(1))//默认情况下,当一个对象被创建时,他的引用计数就为1
{
}
shared_ptr(const shared_ptr<T>& sp)//进行拷贝构造后,拷贝对象和被拷贝对象指向同一份动态子隐患,因此引用计数++
:_ptr(sp._ptr), _pcount(sp._pcount)
{
(*_pcount)++;
}
void release()
{
if (--(*_pcount) == 0)//因为它本身属于对另一份资源做管理的对象,因此指向新的动态资源时,需要先--,减完后,判断指向该份动态资源的对象是否为0,如果为零,则要虚构该动态资源
{
delete(_ptr);
delete(_pcount);
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr operator=(const shared_ptr<T>& sp)//和拷贝对象同理,但是需要注意一点
{
if (this != sp)
{
release();//如果赋值对象原来也指向另一份动态资源,那么需要对该份资源做处理,即release
_ptr = sp._ptr;//release后就可以放心的把被赋值对象赋值给新的对象
_pcount = sp._pcount;
*(_pcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
};
5.4shared_ptr中的循环引用
假设有两个对象a和b,他们在创建之初各自的引用计数就为1。现在通过代码将a的动态资源交给b来管理,将b的动态资源交给a如下代码:
解决方案:使用weak_ptr,weak_ptr是一种不会影响引用计数的智能指针,只绑定到shared_ptr,绑定到shared_ptr时不会增加其引用计数注:weak_ptr不能访问已经释放了的shared_ptr指向的资源。std::shared_ptr<string> sp1 ( new string( "111111" ));std::weak_ptr<string> wp = sp;wp. expired ()wp. use_count ()可以使用上述两个函数来检查weak_ptr指向的资源是否过期,以及获取引用计数