C++——C++11

前言:菜鸟写博客给自己看,大佬写博客给别人看,我是菜鸟

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如下代码:

std::shared_ptr<ListNode> n1 ( new ListNode);
std::shared_ptr<ListNode> n2 ( new ListNode);
n1->_next = n2;
n2->_prev = n1;
这样a和b的引用计数就各自为2,当调用a和b的析构时,a和b对象分别被析构一次,这样引用计数就各自又变为1,此时程序已经结束了,而当引用计数不为0时,shared_ptr不会调用析构函数来析构动态资源,因此造成内存泄漏。
解决方案:使用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指向的资源是否过期,以及获取引用计数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值