左值引用和右值引用
左值引用:左值表示数据的表达式(变量名或者解引用的指针),可以使用取地址+赋值的方式对左值进行取别名使用
1.左值引用只能引用左值,不能引用右值;
2.const左值引用即可引用左值,又可以引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用:右值是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值(不能是左值引用返回),右值可以出现赋值符号的右边,不能出现在赋值符号的左边,右值不能取地址,右值引用就是对右值的引用,给右值取别名。
1.右值引用只能引用右值,不能引用左值。
2.右值引用可以引用move过后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
右值引用的使用场景和意义:对于编译器而言,当你调用函数进行传值返回的时候,编译器会进行一个入栈的操作,把临时变量的空间开好,放在这里,然后再进行函数的入栈,所以对于传值返回中间会产生临时变量,就会进行两次拷贝构造(新编译器进行优化后是一次拷贝构造)。但是当有了右值引用之后,我们可以使用移动构造,本质就是将参数右值的资源窃取过来,占为己有,那么就不用做深拷贝了,相当于直接窃取别人的资源构造自己。
//拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp)
}
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
string to_string(int value)
{
string str;
//....
return str;
}
int main()
{
string ret2 = to_string(-1234);
return 0;
}
上述代码的to_string的返回值是一个右值,用这个二右值构造ret2,如果既有拷贝构造又有移动构造,调用就会匹配移动构造,编译器就会选择最匹配的参数调用,那么这里就是一个移动语义。
除了移动构造还有移动赋值:
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
当然右值引用也可以引用move后的左值。
智能指针
内存泄漏:指的是因为有一些资源没有被释放,而这一段内存无法被使用的情况下,叫做内存泄漏。
主要是C++中的内存申请忘记释放问题,主要关心以下两种方式的内存泄漏:
1.堆内存泄漏:堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏
2.系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针使用及原理
RAII:利用对象的生命周期去管理对应的内存资源的周期,也就是将内存资源和智能指针对象进行绑定,并且在绑定的时候就开始进行初始化。既可以不需要显示地释放资源,也可以在对象所需资源在其生命周期内始终保持有效。
智能指针其实就是一个类,具有RAII的性质,像指针一样使用,解决拷贝问题。
auto_ptr
auto_ptr是c++98标准出来的智能指针,主要是资源管理权转移,容易造成指针对象悬空,当再次访问该指针的时候,则会访问空指针,报错。不能使用!!!
unique_ptr
为了解决auto_ptr的资源管理权转移的问题,unique_ptr则是采用delete掉了拷贝构造函数和赋值构造函数。
template<class T>
class unique_ptr
{
public:
//RAII 保存资源
unique_ptr(T* ptr) :_ptr(ptr)
{}
//释放资源
~unique_ptr()
{
//delete[] _ptr;
delete _ptr;
cout << _ptr << endl;
}
//
unique_ptr(unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& p) = delete;
//像指针一样
T& operator*()
{
return *_ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
shared_ptr
shared_ptr加上了引用计数(int*类型的变量)的功能,允许多个对象指向同一份资源。这个引用计数的变量可以是常量嘛?static变量可以嘛?首先引用计数不能是常量,常量会导致指向同一份资源的指针无法完成一起++ --,也不能是static 静态变量,因为静态变量是所有的对象共享着一份,当不同指针指向不同的资源的时候,就会在同一个static上进行++ --。所以可以是int* 变量,当然也可以是map<T,int>的对象,指向同一份资源的指针进行++。
template<class T>
class shared_ptr
{
public:
//RAII 保存资源
shared_ptr(T* ptr) :_ptr(ptr),_pcount(new int(1)),_pmtx(new mutex)
{}
//释放资源
~shared_ptr()
{
//delete[] _ptr;
Release();
}
//
void Release()
{
_pmtx->lock();
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
cout << _ptr << endl;
}
_pmtx->unlock();
}
int use_count() const
{
return *_pcount;
}
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr)
,_pcount(p._pcount)
,_pmtx(p._pmtx)
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
//赋值的情况需要注意以下两种特殊情况
//sp1 = sp2
//sp1 = sp1
shared_ptr<T>& operator=(shared_ptr<T>& p)
{
//因为很多指的是同一块儿资源,所以需要比较资源的地址
if (_ptr != p._ptr)
{
//需要先释放
int flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
delete _pcount;
delete _ptr;
flag = true;
}
_pmtx->unlock();
_pcount = p._pcount;
_ptr = p._ptr;
++(*p._pcount);
if (flag)
delete _pmtx;
return *this;
}
}
T* Get() const
{
return _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //不能是静态的,如果是两块资源的话,静态的属于整个类,属于类的所有对象,其中一块资源只有一个指针指向
mutex* _pmtx;
};
对于shared_ptr而言,本身是线程安全的,拷贝和析构时,加上锁之后,引用计数++,是线程安全的,shared_ptr管理资源的访问不是线程安全的,例如多线程中,同时对智能指针管理的类里面的资源进行++,--操作此时就不是线程安全的,需要在临界区进行自行加锁保护。
但是shared_ptr存在一个大问题,循环引用的问题,当自定义类型是节点的时候,就会出现循环引用,从而导致内存泄漏。
如下图所示
循环引用分析
1.node1和node2两个智能指针对象指向2个节点,引用计数变为1,不需要手动delete
2.node1的next指向node2,node2的prev指向node1,此时引用计数都变成2
3.node1和node2析构,引用计数减1,但是next和prev还分别指向上下两个节点
4.即当next析构和prev析构之后,node1和node2才会被释放。
5.但是next是node的成员,按理来说,node1释放了,_next才会析构,而node1由prev管理,prev属于node2的成员,所以这就叫循环引用,谁也不会释放。
weak_ptr
为了解决shared_ptr中的循环引用的问题,引入weak_ptr协助shared_ptr解决此类的问题,weak_ptr不进行资源的管理,只负责指向,不增加引用计数,不接受指针,不支持RAII。
//weak_ptr
template<class T>
class weak_ptr
{
public:
weak_ptr(const shared_ptr<T>& p) :_ptr(p.Get()) {}
weak_ptr() :_ptr(nullptr) {}
weak_ptr<T>& operator=(const shared_ptr<T>& p)
{
_ptr = p.Get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
定制删除器
对于上述的智能指针而言,库里实现的智能指针可以完成对数组的释放
std::shared_ptr<int> sp1(new int[10]);
std::shared_ptr<string> sp2(new string);
std::shared_ptr <FILE> sp4(fopen("smart_ptr.cpp", "r"), [](FILE* ptr) {fclose(ptr); }); //可以将lamda表达式传给智能指针,作为析构函数
std::shared_ptr <string> sp5(new string[10], [](string* ptr) {delete[] ptr; }); //可以将lamda表达式传给智能指针,作为析构函数
对于自己设计的指针,则需要加上删除器的操作,因为对于数组的删除
template<class T>
class default_delete {
public:
void operator()(T* ptr)
{
delete ptr;
cout << "default_delete" << endl;
}
};
//传一个默认的
template<class T,class D= default_delete<T>>
class shared_ptr
{
public:
//RAII 保存资源
shared_ptr(T* ptr) :_ptr(ptr),_pcount(new int(1)),_pmtx(new mutex)
{}
//释放资源
~shared_ptr()
{
//delete[] _ptr;
Release();
}
//释放资源
void Release()
{
_pmtx->lock();
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
_pmtx->unlock();
}
int use_count() const
{
return *_pcount;
}
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr)
,_pcount(p._pcount)
,_pmtx(p._pmtx)
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
//赋值的情况需要注意以下两种特殊情况
//sp1 = sp2
//sp1 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
//因为很多指的是同一块儿资源,所以需要比较资源的地址
if (_ptr != p._ptr)
{
//需要先释放
int flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
delete _pcount;
//delete _ptr;
_del(_ptr);
flag = true;
}
_pmtx->unlock();
_pcount = p._pcount;
_ptr = p._ptr;
++(*p._pcount);
if (flag)
delete _pmtx;
return *this;
}
}
T* Get() const
{
return _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //不能是静态的,如果是两块资源的话,静态的属于整个类,属于类的所有对象,其中一块资源只有一个指针指向
mutex* _pmtx;
D _del;
};
struct Fclose
{
void operator()(FILE* ptr)
{
fclose(ptr);
}
};
对于删除数组或者文件等智能指针管理的资源的时候,需要在传一个参数去构造
yc:: shared_ptr<ListNode,DeleteArray<ListNode>> n2(new ListNode[10]);
//注意lamda是一个匿名函数对象 ,decltype 是在运行的时候推导对象 而模板则是需要在编译的时候进行推导的
yc::shared_ptr<FILE, Fclose> n3(fopen("smart_str.cpp", "r"));
列表初始化
{}初始化
c++11可以使用花括号括起来的列表进行初始化,可用于内置类型和用户自定义类型
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
lambda表达式
lamda表达式实际是是一个匿名函数
lamda表达式语法:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。 (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略 mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量 性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
一般使用的时候, [capture-list] (parameters) { statement }
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
//可以借助auto 变量接收一个lamda表达式
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
捕捉列表描述了上下文的那些数据是可以被lambda使用的,以及使用的方式是传值还是传引用。
[var]:表示值传递的方式捕捉var;
[=]:表示的值传递的方式捕获所有父作用域中的变量,包括this;
[&var]:表示引用传递捕捉变量;
var [&]:表示引用传递捕捉所有父作用域中的变量(包括this);
[this]:表示值传递方式捕捉当前的this指针。
在使用lambda表达式的时候注意:
a. 父作用域指包含lambda函数的语句块。 b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 。
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 。c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
d. 在块作用域以外的lambda函数捕捉列表必须为空。 e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。 f. lambda表达式之间不能相互赋值,即使看起来类型相同。
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
函数对象与lambda表达式
函数对象又叫仿函数,既可以像函数一样使用的对象,就是在类里面重载了operator()运算符的函数对象。
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
看底层代码,这是函数对象的底层
这是lambda表达式的底层
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如 果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。