C++11特性补充

目录

lambda表达式

定义

捕捉的方式

可变模板参数

递归函数方式展开参数包

数组展开参数包

移动构造和移动赋值

包装器

绑定bind

智能指针

RAII

auto_ptr

unique_ptr

shared_ptr

循环引用

weak_ptr

补充

总结

特殊类的设计

不能被拷贝的类

只能在堆上创建类

将构造函数私有

将析构函数私有

只能在栈上创建对象

只能创建一个对象(单例模式)

饿汉模式

懒汉模式

单例对象的释放


本文是对上一篇文章《C++11新特性》的补充,将会介绍过很多的C++11特性。

C++11新特性-CSDN博客文章浏览阅读987次,点赞30次,收藏30次。本文对C++11重要特性进行介绍,重点解析了C++中的左值引用和右值引用的区别,帮助读者快速了解C++11新特性以及这些特性的使用方法。https://blog.csdn.net/2401_87944878/article/details/147116766

lambda表达式

定义

C++中可调用对象有三种:函数,仿函数,以及lambda表达式。

函数的类型写起来太繁琐了,所以一般都会使用仿函数来替代函数,但是对于实现简单的函数实现,使用仿函数代码长,比较笨重;因此引出了lambda表达式来解决这一问题。

lambda表达式包含三个部分:1)捕捉列表;2)函数参数;3)函数返回值类型;4)函数体。

cpp 复制代码
struct Add1
{
	int operator()(int x, int y)
	{
		return x + y;
	}
};
auto Add2 = [](int x, int y)->int {return x + y  ;};
Add1()(1, 1);
Add2(1, 1);

以上分别是仿函数和lambda表达式实现加法函数,可以看出lambda表达式的使用更简约。

对于lambda表达式,如果没有参数,参数部分可以省略;返回值类型也可以不写,让编译器自动推导。

cpp 复制代码
auto Add3 = [](int x, int y) {return x + y; };

函数指针---在C++中人不用就不用,其类型写起来不太方便;

仿函数---是一个类,重载了operator();

lambda---表达本质上是一个局部匿名函数对象。

lambda表达式的函数体内可以直接调用全局变量,但是不能直接调用局部变量或局部函数。如果需要使用局部对象就要将其添加至捕捉列表中。

cpp 复制代码
void test_02()
{
	double rate = 0.5;
	auto Add3 = [rate](int x, int y) {return (x + y)*rate; };

}

如上图,rate就实现了捕捉,在lambda表达式中可以只用rate。

捕捉的方式

捕捉的方式有四种:

1)var,对var进行值捕捉;

2)\&var,对var进行引用捕捉,lambda表达式中var的改变会影响其外部的var;

3) = ,对局部变量和函数进行全部值捕捉;

4) \& ,对局部变量和函数进行全部引用捕捉。

++当然捕捉列表也可以混合起来使用。++

cpp 复制代码
int a = 1, b = 2;
int c = 10;
auto Add4 = [&, c] { a++, b++;
					cout << c; };

以上就是混合捕捉:对所有局部变量进行引用捕捉,除了c使用值捕捉。

lambda表达式的底层实际上还是仿函数。

lambda表达式在进行值捕捉的时候,默认捕捉后的类型是const修饰的,也就是说进行值捕捉后的参数是不能进行修改的,如果想要修改需要添加mutable关键字。

cpp 复制代码
int a = 1, b = 2;
auto Add4 = [a, b]()mutable { a++, b++;
							cout << a << b; };

使用mutable后,值捕捉的变量就就可以实现修改了。


可变模板参数

在C语言中就已经有可变参数的概念了,比如printf和scnaf的参数都属于可变参数;C++引入了模板概念,自然也有了可变模板参数概念。

在C++11新增了可变模板参数,使得模板中参数可以不是固定的了,但是可变模板参数使用起来不太方便,此处我们进行简略介绍。

模板的参数是类型,函数的参数是对象,要将这两点分开。

cpp 复制代码
template<class ... Args>
void Show(Args ... args)
{
	sizeof...(args);
	//....
}

模板中的Args指的是模板参数包,函数中的args指的是函数参数包;其中...表示其是可变参数,此处需要注意省略号的位置;通过sizeof...(args)可以打印出参数的个数。

参数包的语法不支持直接使用argsi,所以参数包是不能直接使用的,需要先展开再使用;

递归函数方式展开参数包

通过递归依次减少参数,直到参数为零,其与参数递归类似,只不过递归停止的条件是参数。

cpp 复制代码
template<class T>
void Show(T val)
{
	cout << val << endl;
}
template<class T ,class ... Args> 
void Show(T val ,Args ... args)
{
	cout << val << " ";
	Show(args...);
}
void test_03()
{
	Show(1);
	Show(1,"x");
	Show(1, "x", 1.3);
}

如图,上面通过两个重载函数来实现参数包的展开,参数大于1会优先匹配void Show(T val ,Args ... args);当参数等于1的时候就去匹配void Show(T val),此时递归结束。

数组展开参数包

cpp 复制代码
template<class T>
int Show(T val)
{
	cout << val << " ";
	return 0;
}
template<class ... Args> 
void Show(Args ... args)
{
	int arr[] = { Show(args)... };
	cout << endl;
}
void test_03()
{
	Show(1);
	Show(1,"x");
	Show(1, "x", 1.3);
}

int arr\[\] = { Show(args)... };数组元素个数是没有给定的所以需要通过对数组元素进行计数,此时就需要将参数包展开;


移动构造和移动赋值

C++11新增了两个默认成员函数:移动构造和移动赋值。

移动构造和移动赋值都属于移动语义,其是为了解决对于返回值的参数无法直接引用问题。

在无析构函数+无拷贝构造+无赋值重载+无移动构造的情况下,编译器会生成默认移动构造:对于内置类型进行浅拷贝,对于内置类型会去调用其自己的拷贝构造。

移动赋值也同理。

一般要写析构函数的内是深拷贝的类,需要写拷贝构造和赋值重载;


包装器

C++11引入了包装器function,通过包装器可以实现将函数,仿函数以及lambda表达式类型统一。

cpp 复制代码
void Print1()
{
	cout << "hello world" << endl;
}
struct Print2
{
	void operator()()
	{
		cout << "hello world" << endl;
	}
};
void test_04()
{
	auto Print3 = [] {cout << "hello world" << endl; };

	cout << typeid(Print1).name() << endl;
	cout << typeid(Print2()).name() << endl;
	cout << typeid(Print3).name() << endl;
}

以上三个可调用对象实现的功能都是一样的,此处可以使用typeid().name打印出其三个可调用对象的类型。

可以看出三者类型是完全不同的,C++11引入了包装器function使得可以通过函数返回值,参数将不同的可调用对象类型统一。

cpp 复制代码
function<void()> arr[] = { Print1,Print2(),Print3 };

以上将三个可调用对象都放入到了数组中,其类型都是function<void()>,关于function的使用方法就是:function<函数返回值(函数参数)>,function使用时的头文件是<functional>。

包装器function的本质是一个适配器,可以对函数指针,仿函数以及lambda表达式进行包装。


绑定bind

对于库中的有些接口,参数传递很多并且有些参数是固定的;此时就可以通过bind绑定将接口参数进行绑定。

cpp 复制代码
double RAdd(int x, int y, double rate)
{
	return (x + y) * rate;
}

void test_05()
{
	//如果rate始终是固定的,不需要进行修改次数就可以使用绑定bind
	auto RAdd2 = bind(RAdd, placeholders::_1, placeholders::_2, 0.54);
	cout << RAdd2(10, 4) << endl;
}

bind的参数:可调用对象;参数匹配的位置:10就和_1位置匹配,4就和_2位置匹配,_1和_2有分别和函数参数匹配即x和y;

通过对_1和_2位置的调换,可以出现不同的结果。

如下图所示:通过对_1和_2位置的调换可能会导致函数的调用发生变化。

补充:对于类成员函数的绑定是不同的:对于非静态成员函数要加&,并且要给出对象或对象地址;对于静态成员函数可以不加&,但是建议加上。

cpp 复制代码
class A
{
public:
	static int Add1(int x, int y)
	{
		return x + y;
	}

	int Add2(int x, int y)
	{
		return x + y;
	}

private:
	int _a;
};

void test_05()
{
	//非静态成员函数的绑定
	auto CAdd1 = bind(&A::Add2, A(), placeholders::_1, placeholders::_2);
	A aa;
	auto CAdd2 = bind(&A::Add2, &aa, placeholders::_1, placeholders::_2);
	//静态成员函数的绑定
	auto CAdd3 = bind(&A::Add1, placeholders::_1, placeholders::_2);

}

智能指针

C++中对于错误的处理是使用抛异常的方式解决的,抛异常就会导致进程流被修改,当进程流改变后,原本需要释放的内存没有走到delete就会导致内存泄漏。

为了解决这一问题可以采用多次抛异常,在抛异常前进行空间的释放,如果一个函数中有多个位置进行了空间开辟,当抛异常后还需要考虑哪一个需要释放,这就导致了情况多,代码繁琐的问题。

能不能一个空间开辟后,出作用域就销毁呢???此时就可以使用智能指针来实现。

智能指针通过将指针交给对象,让对象进行资源的管理,在对象出作用域时调析构函数进行资源的释放。

cpp 复制代码
template<class T>
class Ptr
{
public:
	Ptr(T* ptr)
		:_ptr(ptr)
	{
	}

	Ptr(const Ptr& p)
		:_ptr(p._ptr)
	{
	}


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

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

	~Ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

以上是对智能指针的简单模拟实现,但是在使用的时候,对指针进行拷贝构造时就会出现对同一块区域的多次释放。在C++中有不同的智能指针,不同的指针处理方式也是不同的。

RAII

资源获取即初始化,利用对象生命周期来控制资源,在构造函数时获取资源,在对象析构时销毁资源。

优势:1)不需要显式释放空间,出作用域自动销毁;

2)对象所需的资源在其生命周期类持续有效;

auto_ptr

auto_ptr是C++98时推出的一个智能指针;auto_ptr对于指针拷贝的方法是:将空间的管理权转移,在完成拷贝构造之后,将被拷贝对象置空。其底层实现如下:

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{
	}

	auto_ptr(const auto_ptr& p)
		:_ptr(p._ptr)
	{
		p._ptr = nullptr;
	}
    
    auto_ptr operator=(const auto_ptr& p)
    {
	    _ptr = p._ptr;
	    p._ptr = nullptr;
		return *this;
    }

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

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

	~auto_ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

可以看到,在拷贝完成后,被拷贝对象悬空导致其无法使用,这使得auto_ptr不被广泛使用。

unique_ptr

unique_ptr对于拷贝和赋值的处理方法更加暴力,unique_ptr不支持赋值和拷贝构造。

所以在实现的时候,需要将拷贝和赋值只声明不定义,并且置为私有防止其在外面被定义,为防止编译器自动生成在函数后面加上delete。

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{
	}

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

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

	~unique_ptr()
	{
		delete _ptr;
	}
private:
	unique_ptr(const unique_ptr& p) = delete;
	unique_ptr operator=(const unique_ptr& p) = delete;
	T* _ptr;
};

shared_ptr

shared_ptr通过对指向每个空间的指针进行计数来实现对空间的释放,当一个指向一个空间的指针数量为0是对空间进行释放;

所以shared_ptr需要添加一个成员变量来实现计数。

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{
	}

	shared_ptr(const shared_ptr& p)
		:_ptr(p._ptr)
		,_pcount(p._pcount)
	{
		++(*_pcount);
	}

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

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

	shared_ptr operator=(const shared_ptr& p)
	{
		_ptr = p._ptr;
		_pcount = p._pcount;
		++(*_pcount);
		return *this;	
    }


	~shared_ptr()
    {
	    if (--(*_pcount) == 0)
	    {
	    	delete _ptr;
	    }
    }
private:
	T* _ptr;
	int* _pcount;
};

shared_ptr确实解决了何时释放的问题,但是又出现了循环引用的问题。

循环引用

当一个对象有前后指针的时候就会出现问题,比如一下代码。

cpp 复制代码
template<class T>
struct Node
{
	Node()
		:_a(T())
		,_next(nullptr)
		,_prev(nullptr)
	{ }


	T _a;
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
};

void test_06()
{
	shared_ptr<Node<int>> aa(new Node<int>);
	shared_ptr<Node<int>> bb(new Node<int>);

	aa->_next = bb;
	bb->_prev = aa;
}

当aa的后继指针指向bb的时候,bb的引用计数就变成了2,同理aa的引用计数也编程了2。当对aa进行销毁的时候,其引用计数是2无法调用Node的析构,也就不能进行_next和_prev的析构了,此时就出现了空间泄露。

为了解决这一问题添加了weak_ptr。

weak_ptr

weak_ptr与shared_ptr的使用是一样的,只是在对shared_ptr拷贝的时候不会对引用计数进行改变。weak_ptr不遵循RAII,其是专门为解决循环引用而出现的。

cpp 复制代码
template<class T>
class weak_ptr
{
public:
	weak_ptr(T* ptr)
		:_ptr(ptr)
	{
	}

	weak_ptr(const shared_ptr<T>& p)
		:_ptr(p._ptr)
	{
	}

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

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

	weak_ptr operator=(const shared_ptr<T>& p)
	{
		_ptr = p._ptr;
		return *this;
	}
private:
	T* _ptr;
};

上面Node类定义变为:

cpp 复制代码
template<class T>
struct Node
{
	Node()
		:_a(T())
		,_next(nullptr)
		,_prev(nullptr)
	{ }


	T _a;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
};

补充

cpp 复制代码
shared_ptr<int> aa(new int[10]);

当我们开辟一个数组的空间时,释放时再使用delete就不能进行空间释放了,所以对于shared_ptr和unique_ptr的参数增加了一个:释放对象。

结合lambda表达式使用起来更加简单。Del的类型可以使用function表示。

cpp 复制代码
template<class T, class Del=function<void(T*)>>
class shared_ptr
{
public:
	shared_ptr(T* ptr, Del del = [](T* ptr) {delete ptr; })
		:_ptr(ptr)
		,_pcount(new int(1))
		,_del(del)
	{
	}

	shared_ptr(const shared_ptr<T>& p)
		:_ptr(p._ptr)
		,_pcount(p._pcount)
	{
		++(*_pcount);
	}

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

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

	shared_ptr operator=(const shared_ptr<T>& p)
	{
		_ptr = p._ptr;
		_pcount = p._pcount;
		++(*_pcount);

		return *this;
	}


	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
		}
	}
private:
	T* _ptr;
	int* _pcount;
	Del _del;
};
cpp 复制代码
shared_ptr<int> a(new int[10], [](int* ptr) {delete[] ptr; });
shared_ptr<FILE> b(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

总结

auto_ptr:管理权转移,被拷贝的对象会悬空;

unique_ptr:不支持赋值和拷贝构造;

shared_ptr:通过引用计数控制内存的释放;

weak_ptr:解决shared_ptr的循环引用问题,拷贝和赋值的时候不增加引用计数。


特殊类的设计

不能被拷贝的类

C++11增加了delete的功能,不仅能对new的空间进行释放,放在默认成员函数后面还能表示:不让编译器生成该函数。

实现:将拷贝和赋值置为私有,且使用delete不让编译器生成即可。

cpp 复制代码
template<class T>
class NoCopy
{
public:
	//...
private:
	NoCopy(const NoCopy&) = delete;
	NoCopy operator=(const NoCopy&) = delete;
	T _val;
};

只能在堆上创建类

也就是不能再栈上创建对象,栈上创建对象必须能够调用构造函数和析构函数,所以对于类的实现有两种方法:1)将构造函数私有;2)将析构函数私有;还需要将拷贝构造和赋值删除。

将构造函数私有

将构造函数私有之后,new也不能调用构造函数了,所以需要增加接口专门返回在对上创建的对象。

cpp 复制代码
template<class T>
class HeapOnly
{
	static HeapOnly* Get()
	{
		return new HeapOnly;
	}


private:
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly operator=(const HeapOnly&) = delete;
	HeapOnly()
	{
		//...
	}
	T _val;
};

将析构函数私有

将析构函数私有之后,还需要提供接口进行空间的释放。

cpp 复制代码
template<class T>
class HeapOnly
{
	
	HeapOnly()
	{
		//...
	}

private:
	~HeapOnly()
	{
		//...
	}
	HeapOnly(const HeapOnly&) = delete;
	HeapOnly operator=(const HeapOnly&) = delete;
	
	T _val;
};

只能在栈上创建对象

对于只能在栈上创建对象,就需要不能让new正常使用,new分为两步:1)调用operator new;2)调用构造函数;

实现:重载operator new,将其删除并将构造函数私有;向外提供构造接口。

cpp 复制代码
template<class T>
class StackOnly
{
	static StackOnly Creat()
	{
		StackOnly st;
		return st;
	}

	~StackOnly()
	{
		//...
	}
private:
	void* operator new(size_t size) = delete;
	StackOnly()
	{
		//...
	}

	T _val;
};

只能创建一个对象(单例模式)

创建一个类只能创建一个对象,这中类称为单例模式。

实现方法:1)只创建一个对象,将这个对象设为类的及静态成员,在每次获取的时候都将这一静态成员返回;2)将构造函数设为私有,防构造;3)防拷贝,将拷贝和赋值删除。

该类分为两种:饿汉模式和懒汉模式。

饿汉模式

饿汉模式指的是在main函数之前就创建出对象。

cpp 复制代码
class Hungry_class
{
public:
	Hungry_class& Get()
	{
		return _date;
	}

private:
	Hungry_class(const Hungry_class&) = delete;
	Hungry_class operator=(Hungry_class) = delete;

	Hungry_class()
	{
		//...
	}
	static Hungry_class _date;
};
Hungry_class Hungry_class::_date;

函数自始至终都只有一个对象_date。

饿汉模式的劣势:

1)在main函数之前就进行对象的创建,当有多个单例模式需要被创建时就会影响程序的启动效率。

2)如果两个单例类A和B之间相互联系,B的创建需要A,如果还是使用饿汉模式就会导致无法控制哪一个单例对象先创建。

懒汉模式

懒汉模式与饿汉不同的是:懒汉模式是在需要单例对象时才去创建。

实现方法:与饿汉模式一样,但是懒汉的成员变量是指针而不是对象。

cpp 复制代码
class Lazy_class
{
public:
	Lazy_class& Get()
	{
		if (_date == nullptr)
		{
			Lazy_class* _date = new Lazy_class;
		}
		return *_date;
	}

private:
	Lazy_class(const Lazy_class&) = delete;
	Lazy_class operator=(Lazy_class) = delete;

	Lazy_class()
	{
		//...
	}
	T _val;
	static Lazy_class* _date;
};
Lazy_class* Lazy_class::_date;

单例对象的释放

一般单例对象是不需要进行释放的,但是如果中途需要进行显示释放或程序需要进行一些特殊动作(如持久化)等;

单例对象通常是在多个进程中使用的,所以单例对象的释放不能简单的根据作用域让其自己释放,需要提供专门的接口释放。

单例模式析构可以手动调用,也可以像智能指针一样自动调用。

可以通过设计一个内部类实现自动调用析构函数。

cpp 复制代码
class Lazy_class
{
public:
	static Lazy_class& Get()
	{
		if (_date == nullptr)
		{
			Lazy_class* _date = new Lazy_class;
		}
		return *_date;
	}

	static void Destory()
	{
		//...
	}

	struct GC
	{
		~GC()
		{
			Lazy_class::Destory();
		}
	};

private:
	Lazy_class(const Lazy_class&) = delete;
	Lazy_class operator=(Lazy_class) = delete;

	Lazy_class()
	{
		//...
	}
	static Lazy_class* _date;
	static GC _gc;
};
Lazy_class* Lazy_class::_date;
Lazy_class::GC Lazy_class::_gc;
相关推荐
地平线开发者4 小时前
profiler debug 工具用法与高一致性策略
算法·自动驾驶
编程大师哥4 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog4 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008114 小时前
FastAPI APIRouter
开发语言·python
Benszen4 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆4 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木4 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌4 小时前
算法解题思路指南
算法
MC皮蛋侠客4 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者5 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶