C++11
  7mljcwUfRCrR 2023年11月02日 21 0


统一的列表的初始化

在c++11中所有的初始化都可以用大括号{}进行初始化。
感觉挺鸡肋的。
看下面这个代码:

int main()
{
	int a = { 2 };
	int b{ 6 };
	cout << a << endl;
	cout << b << endl;

	return 0;
}


对于a的初始化暂时可以接收,但是b这种初始化是上面玩意。。。。。——看见知道可以这样写就可以,我们写代码的时候应该不会这样写。

这样的初始化在初始化stl容器的时候还是可以进行接受的。
看下面的代码:

vector<int> t = { 1,2,3,4,5 };
for (auto& x : t)
{
    cout << x << ' ';
}


可以直接初始化,不需要像我们之前那样一个一个的插入了。

看这样是多么的方便

vector<string> vs = { "fezrav","cevsdasv" };
for (auto& x : vs)
{
    cout << x << endl;
}

vector等容器为什么可以这样写呢?——C++11又增加了一个新容器initializer_list

initializer_list

initializer_list它本身真正意义上的容器,它没有提供像访问元素的方法、插入或删除元素的方法。它主要是以大括号{}的形式传入一组值。

initializer_list一般是作为构造函数的参数

比如上面所说的那样,为神马vector等容器支持呢,就是它作为参数进行传入了。

vector的构造函数:我们可以看出传的它作为参数的


它是有类型检查的,列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的


auto

C++11中auto可以自动的推导数据的类型。
使用:

auto x = 1;
auto y = 2.3;
cout << "x的类型" << typeid(x).name() << endl;
cout << "y的类型"<<typeid(y).name() << endl;
// 输出:
// x的类型int
// y的类型double


范围for

本质上就是迭代器
使用:

vector<int> t = { 1,2,3,4,5 };
for (auto& x : t)
{
    cout << x << ' ';
}
cout << endl;
// 输出:
// 1 2 3 4 5


decltype

decltype获得表达式的类型
意思就是使用它之后,它就是该类型了。
看下面的代码:

int x = 0;
decltype(x) y = 2;
//decltype(x)就是和int等价,即可以用decltype(x)定义变量的类型


nullptr

C++11引入了空指针,在C语言中我们用NULL来表示空,但是NULL在定义的时候是用整型0来定义的。所以为了突出它是空指针而不是整型——我们把nullptr就设为指针类型。


c++11中又增加了4个容器

array

静态数组0

比使用数组更安全,有数组越界的检查。总结就是鸡肋

forward_list

单链表

还不如用list

unordered_map

底层是哈希表,存储键值对

nice

unordered_set

底层是哈希表,存储键

nice


左值引用和右值引用

在C++11中新增了右值引用,那么什么是左值?什么是右值?它们有什么区别?下面就一一解答。
左值就是一个表示数据的表达式,我们可以获得它的地址和对他赋值。
左值可以出现在赋值符号的左边,也可以出现赋值符号的右边,但是右值不能出现在赋值符号左边。
那么什么是右值呢?
右值也是表示数据的表达式,但是我们不能获得它的地址,也不能对它就行赋值。
右值一般为:字面常量,表达式的返回值,函数返回值(非左值引用返回)
例:

int a=1;//a为左值,1为右值
"abc";//右值
10;//右值


左、右值引用

左值引用就是对左值进行引用,给左值起别名,比如:int& a=x就是给x起了一个别名

右值引用就是对右值进行引用,给右值起别名,比如:int&& b=10就是给10取了别名。

其实现在的b是有地址的


左值引用可以引用左值,在加上const之后就可以引用右值了

右值引用可以引用右值,在加上move函数之后就可以引用左值了。强制把左值变成右值。

为什么要引入右值引用呢?
先说一下左值引用有什么好处呢?

  1. 做参数。提高效率,减少拷贝。
  2. 做返回值。提高效率,减少拷贝。返回值可以在调用结束的时候就行修改里面的值

但是,当它做返回值的时候是有条件的,即不能是局部变量,因为局部变量出了作用域就被销毁了。那么这个就可以用右值引用来解决

由右值引用引出的两种构造函数——移动构造、移动赋值
移动构造的本质就是把右值的资源转移过来变成它的。移动赋值也是这样的。

当右值引用作为参数出现在模板中,那么它就是万能引用,什么意思呢?——左值、右值都可以进行传参。但是,最后的结果都会被识别成左值。那么怎么实现传参之前的属性呢?——forward函数,在传参的过程中保留对象的原生属性。我们称这个叫做完美转发。


C++11新增加的默认成员函数

根据上面所讲,在c++11中,又增加了2个默认的成员函数

移动构造和移动赋值

既然两个可以做默认的成员函数,那么它们成员默认的条件是什么

  • 如果自己没有实现移动构造,且没有实现析构函数、拷贝构造函数、赋值重载的任意一个。那么编译器会自动生成一个默认的移动构造函数。对于默认的移动构造,它对内置类型按成员进行字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果字节没有实现移动赋值,且没有实现析构函数、拷贝构造函数、赋值重载的任意一个。那么编译器会自动生成一个默认的移动赋值函数。对于默认的移动赋值,它对内置类型按成员进行字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
  • 如果自己实现了,编译器就不会自己生成


default关键字

default关键字的作用就是强制生成默认函数。
用法就是在函数的后面加上=default

delete关键字

delete关键字除了是释放开辟的空间的作用外,它的另一个作用就是禁止生成默认函数
用法就是在函数后面加上=delete


可变参数模板

C++11 的新特性可变参数模板能够让我们在创建可以接受可变的参数模板和类模板
基本的形式:

template <class ...Args>
void showlist(Args ... args)
{}

上面的省略号就是可变参数,我们把带省略号的参数称为"参数包"。

查看里面有几个参数:sizeof...(args)

STL容器中的emplace相关的函数接口


比如上面list的接口。我们发现emplace_back支持可变参数模板。

该函数的作用就是在最后加上插入一个元素。

但是用法上基本没有差别,注意是emplace_back是直接进行构造的,

push_back是先构造再进行拷贝——自定义类型)


lambda表达式

lambda表达式语法

[capture-list](parameters)mutable->return-type{statement}
  • [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,捕捉列表能够捕捉上下文中的变量供lambda函数使用
  • (paramenters):参数列表。与普通参数列表一样,如果不需要传递参数,那么可以连同()一起省略
  • mutable:默认情况下,lambda表达式总是const函数,mutable可以取消其常量性
  • ->return-type:返回值类型。用于返回函数的类型。当返回值明确的时候,可以不写,可由编译器自动进行推断
  • {statement}:函数体。在函数体内,除了可以使用其参数外,也可以使用又捕捉列表捕捉的参数。

例:

int main()
{
	auto f = [](int a, int b) {return a + b; };
	cout << f(1, 2) << endl;
	return 0;
}

从上面可以看出来,lambda表达式看似是一个无名函数,该函数无法直接调用,需要赋值给一个变量。
lambda底层就是仿函数,仿函数(Function Object)本质上是一种可调用对象,它可以像函数一样被调用,但并不是函数。
关于捕捉列表,给出以下说明

  • [val]:表示以传递的方式捕捉变量val
  • [=]:表示以值传递的方式捕捉该作用域中的全部变量,以及全局变量
  • [&val]:表示以引用的方式捕捉变量val
  • [&]:表示以引用的方式捕捉该作用域内的全部变量
  • [this]:表示已值传递方式捕捉当前的this指针

当捕捉多个的时候,要注意用逗号,进行隔开,
并且还要注意这种情况不要发生[=,a]——这样写是错误的,因为=就是匹配以值的形式去捕捉全部变量就已经包括了a,但是这样写是正确的[=,&a]——它表示的是除了a是以引用进行捕捉,其余的变量全部都是以值的形式进行捕捉。
例:

int main()
{
	int a, b, c, d, f, g;
	a = b = c = d = f = g = 1;
	auto fun = [=, &a]() {cout << a << b << c << d << f << endl; a++; };
	fun();
	cout << a << endl;
	return 0;
}


包装器


function包装器


C++的function本质的一个类模板

其中Ret是被调函数的返回类型,Args...是被调函数的形参。

它的使用形式要记住一下:例:

int Add(int a, int b)
{
	return a + b;
}
int main()
{
	function<int(int, int)> f = Add;
	cout << f(1, 3) << endl;
	return 0;
}

当然,它也可以包装lambda表达式

function<int(int, int)> f = [](int a, int b) {return a + b; };
cout << f(1, 3) << endl;

要注意包装类成员函数的情况。

class Date
{
	int year;
	int month;
	int day;
public:
	Date()
		:year(1),month(1),day(1){}
	void Print()
	{
		cout << year<< month<<day << endl;
	}
};
int main()
{
	function<void(Date)> f = &Date::Print;

	f(Date());
	return 0;
}

对与类的非静态成员,不仅要指明他的类域,在function里面还要写上类的名字,而且还需要加上一个&看着就不舒服。下面的bind可以修改它
可以把包装器看出一个通用的函数指针。


bind


bind是一个函数模板,接受一个可以调用的对象,生成一个新的可调用对象来适用原对象的参数列表。就是它可以调整参数的位置。

以不太好理解的类的函数为例

class Date
{
	int year;
	int month;
	int day;
public:
	Date()
		:year(1),month(1),day(1){}
	void Print(int a,int b)
	{
		cout <<"a:"<<a<<" b:"<<b << endl;
	}
};
int main()
{
	function<void(int, int)> f = bind(&Date::Print,Date(), placeholders::_2, placeholders::_1);
	f(2,1);
	return 0;
}

上面的_1``_2是什么意思呢?


它们是包含在placeholders命名空间内的,里面包含着参数列表:_1就是第一个参数,_2表示第二个数参数

这里的参数对应原来函数的形参的位置。

对应类的非静态函数,必须要串一个该类的对象(匿名的或者是实体的)。

对于上面的代码,打印的是a:1 b:2为什么不是2 1呢?就是因为交换了参数。


异常

什么是异常呢?
就是当程序发生错误的,我们抛出提示或者对错误进行处理,使程序能够继续运行下去。
关键字:

  • throw:抛出异常
  • catch:捕获异常
  • try:里面包含着可能出错的逻辑


异常的使用

简单的例子:

#include <iostream>
#include <string>
using namespace std;

void fun(int a, int b)
{
	int c;
	if (b == 0)
		throw "除0错误";
	c = a / b;
	return;
}

int main()
{
	int a, b;
	cin >> a >> b;
	try {
		fun(a,b);
	}
	catch (const char* e)
	{
		cout << e << endl;
	}
	catch (...)
	{
		cout << "未知错误" << endl;
	}
	return 0;
}


异常的抛出和捕获


异常的抛出和匹配原则
  1. 异常是通过抛出对象而引发的,对象的类型决定应该激活哪个catch的处理代码
  2. 被选中的处理代码是调用链中于该对象类型匹配且离抛出异常位置最佳的那一个
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝对象会被catch以后销毁。类似于函数的传值返回
  4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么
  5. 可以抛出派生类对象,使用基类捕获


在函数调用链中异常栈展开匹配原则
  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句,如果有匹配的,则调用catch的地方进行处理
  2. 没有匹配的catch则退出当前函数栈,继续在调用的函数栈中进行查找匹配的catch
  3. 如果到达main函数的栈,依然没有匹配的,则终结程序。
  4. 找到匹配的catch子句并处理之后,会继续沿着catch子句后面继续执行


异常安全
  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出异常,导致内存泄漏,在lock和unlock之间抛出异常导致死锁,C++经常使用RAII来解决以上问题。


异常的优点和缺点

以后再说


智能指针


为什么要使用智能指针

看下面这个代码:

#include <iostream>
#include <string>

using namespace std;

class A
{
	int _a;
	int _b;
public:
	A(int a=0,int b=0)
		:_a(a),_b(b){}
	~A()
	{
		cout << "释放资源" << endl;
	}
	int div()
	{
		if (_b == 0)
			throw string("除0错误");
		return _a / _b;
	}
};


int main()
{
	A* p1=new A(1,2);
	A* p2=new A(1, 0);
	try {
		p2->div();
		delete p1;
		delete p2;
	}
	catch (const string& e)
	{
		cout << e << endl;
	}
	return 0;
}

打印结果:
除0错误
当我们new了多个对象,当发生异常的时候,它的资源没有释放,会导致内存的泄漏


怎么避免内存泄漏

采用RAII的思想或者智能指针来管理资源

什么是RAII呢?
RAII是一种利用对象生命周期来控制程序的资源的技术
简单的来将就是把资源教给一个对象去管理,利用对象销毁的时候,自动调用析构函数,在析构函数中对资源进行释放。

内存泄漏是指针丢了还是内存丢了?
答案当然是指针丢了
内存还在,进程正常结束,内存也会释放

  1. 僵尸进程有内存泄漏
  2. 长期运行的程序


智能指针的使用及原理

#include <iostream>
#include <string>

using namespace std;

namespace ml
{
	template<class T>
	class auto_ptr
	{
	T* _ptr;
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}

		auto_ptr<T>& operator = (auto_ptr<T>&sp)
		{
			if (_ptr != sp._ptr)
			{
				delete _ptr;
				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}
			return *this;
		}

		~auto_ptr()
		{
			delete _ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
		T& operator* ()
		{
			return *_ptr;
		}
	};

};

class A
{
	int _a;
	int _b;
public:
	A(int a = 0, int b = 0)
		:_a(a), _b(b) {}
	~A()
	{
		cout << "释放资源" << endl;
	}
	int div()
	{
		if (_b == 0)
			throw string("除0错误");
		return _a / _b;
	}
};


int main()
{
	ml::auto_ptr<A> p1(new A(1, 2));
	ml::auto_ptr<A> p2(new A(1, 0));
	ml::auto_ptr<A> p3=p2;
	//A* p1=new A(1,2);
	//A* p2=new A(1, 0);
	try {
		p2->div();
	}
	catch (const string& e)
	{
		cout << e << endl;
	}
	return 0;
}

当我们把资源的管理给Smartptr,那么当抛出异常的时候Smartptr会进行资源的释放。
当然这个其实就是auto_ptr的功能。但是现在我们不使用它。

因为它还是有缺陷的
auto_ptr中,拷贝构造是进行的资源转移(这种太暴力了,设计的不好)
赋值重载是把资源转移,然后把原来的资源释放。
从第74行就能看出,我们这样直接拷贝构造,最后会导致对空指针解引用
在C++11的时候,又更新了智能指针unique_ptr——它的拷贝构造、赋值重载只进行了生命,没有实现,且加上了关键字delete,禁止生成默认的。

为了解决这样的问题,又引入了一个shared_ptrshared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护着一份计数,用来记录该分资源被几个对象共享
  2. 在对象被销毁的时候(调用析构函数),就说明自己不使用该资源了,对象的引用就是减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,说明还有其他对象使用该资源,那么就不能对资源进行释放。

下面我们进行模拟实现一下

template<class T>
class shared_ptr
{
    T* _ptr;
    int* _pcount;
public:
    shared_ptr(T* ptr)
        :_ptr(ptr)
        ,_pcount(new int(1))
    {}
    shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        ,_pcount(sp._pcount)
    {
        (*_pcount)++;
    }
    shared_ptr<T>& operator=(shared_ptr<T>& sp)
    {
        //自己不对自己
        if (_ptr != sp._ptr)
        {
            //还存在其他用户
            if ((*_pcount) == 1)
            {
                //释放该用户
                delete _ptr;
                delete _pcount;
            }
            else {
                (*_pcount)--;	
            }
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            (*_pcount)++;
        }
        return *this;
    }
    ~shared_ptr()
    {
        if (*_pcount == 1)
        {
            delete _ptr;
            delete _pcount;
        }
        else
            (*_pcount)--;
    }
    T* operator->()
    {
        return _ptr;
    }
    T& operator*()
    {
        return (*_ptr);
    }
};

虽然这样可以解决大部分问题,但是对于循环引用的问题还是没有解决。
下面就是这个问题:

struct Node
{
	int _date;
	ml::shared_ptr<Node> _next;
	ml::shared_ptr<Node> _prev;
	Node()
		:_date(1)
		,_next(nullptr)
		,_prev(nullptr)
	{}
	~Node()
	{
		cout << "释放资源" << endl;
	}
};

int main()
{
	ml:: shared_ptr<Node> p1(new Node);
	ml::shared_ptr<Node> p2(new Node);
	p1->_next = p2;
	p2->_prev = p1;
	return 0;
}

结果是没有对资源进行释放

怎么解决这个问题呢?——用weak_ptrweak_ptr不是真正的意义上的智能指针。它不对资源进行管理

我们来自己手动实现应该weak_ptr


在官方给的构造函数中,其构造函数只是对weak_ptrshared_ptr进行构造。

template<class T>
class weak_ptr
{
    T* _ptr;
public:
    weak_ptr(const weak_ptr<T>& wp)
        :_ptr(wp._ptr)
    {}
    weak_ptr(const shared_ptr<T>& sp)
        :_ptr(sp.get())
    {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        _ptr = sp.get();
        return *this;
    }
    T* operator->()
    {
        return _ptr;
    }
    T& operator*()
    {
        return *_ptr;
    }
};

上面就是简单的实现一下。

当我们再次运行的时候,两个节点的资源都完成了释放。

但是最后还有一个问题就是释放资源的时候,只是释放的new的资源,对于new[]而言就会出现错误

我们自己实现的

ml::shared_ptr<Node> p1(new Node[10]);库里面实现的

shared_ptr<Node> p1(new Node[10]);

都会报错

库里面的构造函数,我们可以传给它一个仿函数


那么在我们的模拟中实现一下和库里面的不一样

库中的模板参数中没有class D = Delete<T>

//删除器
template<class T>
struct Delete
{
    void operator()(T* ptr)
    {
        delete ptr;
    }
};

template<class T, class D = Delete<T>>
class shared_ptr
{
    T* _ptr;
    int* _pcount;
public:
//........................
    ~shared_ptr()
    {
        if (*_pcount == 1)
        {
            D()( _ptr);
            delete _pcount;
        }
        else
            (*_pcount)--;
    }
//....................
};

库中的应该这么用

template<class T>
struct ArryDelete
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
int main()
{
	shared_ptr<Node> p1(new Node[10], ArryDelete<Node>());
    return 0;
}

我们这种实现和库中的unique_ptr差不多

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
7mljcwUfRCrR
作者其他文章 更多

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02

2023-11-02