C++笔记——C++11新特性

在以往的编程中仅涉及C++的基础知识,对于C++11的新了解甚少,仅仅使用过nullptr(能隐式转换为任何指针或成员指针的类型)。C++11新特性是对C++的补充和拓展,了解和学习并在以后的编程中使用能更方便地解决一些问题。

nullptr

nullptrstd::nullptr_t 类型的右值常量,专用于初始化空类型指针。 不同类型的指针变量都可以使用 nullptr 来初始化,编译器将 nullptr 隐式转换成对应的指针类型。 优先使用nullptr而非0NULL

类型推导

auto关键字:

auto来做自动类型推导,使用后编译器会在编译期间自动推导出变量的类型。

基本使用语法:

	auto name = value; // name: 变量名, value: 变量初始值

auto仅仅是一个占位符,在编译期间会被真正的类型所替代,c++中变量必须有明确类型。

auto推导类型存在三种情况:

  • 修饰指针或引用,但不是万能引用
	int x = 0;
	auto& r1 = x;   // r1 为 int,auto 为 int
	auto* p1 = &x;  // p1 为 int*,auto 为 int
	auto  p2 = &x;  // p2 为 int*,auto 为 int*
	
	// 数组
	const char arr[] = "123";
	auto  arr1 = arr;     // arr1 为 const char*
	auto& arr2 = arr;     // arr2 为 const char(&)[4]
	// 函数
	void someFunc(int);
	auto  func1 = someFunc;  // func1 为 void(*)(int)
	auto& func2 = someFunc;  // func2 为 void(&)(int)
  • 修饰万能引用
	int  x = 1;
	auto&& r1 = x;   // x 是个左值,r3 为 int&
	auto&& r2 = 1;   // 1 是个右值,r4 为 int&&
  • 修饰既非指针也非引用
	int x = 1;
	auto x1 = x;   // int
	auto x2 = 1;   // int

autoconst 结合的用法:

	int  x = 1;
	const auto cx = x;   // cx 为 const int,auto 被推导为 int
	auto f = cx;         // f 为 const int,auto 被推导为 int(const 属性被抛弃)
	const auto &r1 = x;  // r1 为 const int& 类型,auto 被推导为 int
	auto &r2 = r1;       // r2 为 const int& 类型,auto 被推导为 const int 类型
  • 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
  • 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。

一个特殊情况:

auto声明变量使用列表初始化({})时,推导出的类别为std::initializer_list,如果推导失败({}中类型不唯一),不能通过编译。

	auto x1 = { 1 };   // std::initializer_list<int>,值为{ 1 }
	auto x2 { 1 };     // 同上
	
	auto x3 = { 1, 2, 3.0 };  // Error

auto的限制:

  • 使用auto的变量必须初始化
  • auto不能在函数参数中使用
  • auto不能作用于类的非静态成员变量
  • auto不能用于定义数组
  • auto不能作用于模板参数

decltype关键字:

auto 的功能一样,decltype用来在编译时期进行自动类型推导 。

用法:

	decltype(exp) name = value;  // name: 变量名,value: 初始值,exp: 有类型的表达式
	decltype(exp) name;

decltype根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。,所以 decltype不要求变量必须初始化。

原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void。

推导规则:

  • 如果 exp 是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 如果 exp 是一个左值,或者被括号()包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。

例:

	// 
	// 规则一:普通表达式
	//
	class A {
	public:
		static int staticIntNum;
		string strNum;
		int intNum;
		float floatNum;
	};
	
	int n = 0;
	const int &r = n;
	A a;
	
	decltype(n) da = n;      // n 为 int 类型,da 被推导为 int 类型
	decltype(r) db = n;      // r 为 const int& 类型, db 被推导为 const int& 类型
    decltype(A::staticIntNum) dc = 0;   // staticIntNum 为类 A 的一个 int 类型的成员变量,dc 被推导为 int 类型
    decltype(a.strNum) str = "123456";  // total 为类 A 的一个 string 类型的成员变量, str 被推导为 string 类型
    
    //
    // 规则二:函数调用(带上括号和参数仅仅是形式,并不会真的去执行函数代码。)
    //
	int& func_int_r(int, char);  
	int&& func_int_rr(void);  
	int func_int(double); 
	const int& fun_cint_r(int, int, int);  
	const int&& func_cint_rr(void); 
	
	int n = 100;
	decltype(func_int_r(100, 'A')) a = n;  // a 的类型为 int&
	decltype(func_int_rr()) b = 0;         // b 的类型为 int&&
	decltype(func_int(10.5)) c = 0;        // c 的类型为 int
	decltype(fun_cint_r(1,2,3))  x = n;    // x 的类型为 const int &
	decltype(func_cint_rr()) y = 0;        // y 的类型为 const int&&
	
	//
	// 规则三:exp 是左值,或者被()包围
	//
	int n = 0, m = 0;
    decltype(n + m) c = 0;      // n+m 得到一个右值,符合推导规则一,推导结果为 int
    decltype(n = n + m) d = c;  // n=n+m 得到一个左值,符号推导规则三,推导结果为 int&
    decltype(n) a = 0;    // 一个单独的变量,符合推导规则一,a 的类型为 int
    decltype((n)) b = a;  // 带有括号,符合推导规则三,b 的类型为 int&。
	

返回类型后置(跟踪返回类型)

返回类型后置语法是通过 aut decltype的结合使用完成返回值的推导。语法:

	auto func(...) -> decltype(exp)
	{
		...
	}

例子:

	template <typename T, typename U>
	auto func(T t, U u) -> decltype(t + u)
	{
		return t + u;
	}
	
	int& foo(int& i);
	float foo(float& f);
	template <typename T>
	auto func(T& val) -> decltype(foo(val))
	{
    	return foo(val);
	}

使用using定义别名

using的别名语法覆盖了typedef的全部功能

	// 重定义unsigned int
	typedef unsigned int uint;
	using uint = unsigned int;
	
	// 重定义std::map
	typedef std::map<std::string, int> map_str_int;
	using map_str_int = std::map<std::string, int>;
	
	// 重定义函数指针
	typedef void(*funcPtr)(int, int);
	using funcPtr = void(*)(int, int);
	

using 定义模板别名

	template <typename T>
	using func = void(*)(T);
	func<int> x;
	
	template <typename Val>
	using str_map = std::map<std::string, Val>;
	str_map<int> map;

using 重定义的模板,称为模板别名(alias template)。

可变参数模板

在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。 声明可变参数模板时需要在typenameclass后面带上省略号...

template <typename... Types> 

...可接纳的模板参数个数是0个及以上的任意数量

在可变参数的模板函数中,通过如下方式获得args的参数个数:

template <typename... Types> 
void func(Types... args)
{
	...
	sizeof...(args);
	...
}

当较泛化和较特化的模板函数同时存在的时候,最终程序会执行较特化的那一个

列表初始化

为了统一初始化方式,并且让初始化行为具有确定的效果,C++11 中提出了列表初始化(List-initialization)的概念,使用{}

	//
	// 初始化对象
	//
	// class A
	// {
	// public:
	//     A(int) {}
	// private:
	//     A(const A &);
	//     int x { 0 };
	// };
	A a1(123);
	A a2 = 123;    // Error:A(const A &)为private
	A a3 = { 123 };
	A a4 { 123 };
	
	int a5 = { 123 };
	int a6 { 123 };
	
	std::vector<int> v { 1, 2 };
	
	//
	// 初始化普通数组和 POD 类型
	//
	int arr[3] { 1, 2, 3 };
	struct A
	{
		int x;
		struct B
		{
			int i;
			int j;
		}b;
	} a {1, { 2, 3 } };
	
	//
	// new 操作符
	//
	int* a = new int { 123 };
	double b = double { 12.12 };
	int* arr = new int[3] { 1, 2, 3 };
	
	//
	// 函数返回值
	//
	struct B
	{
    	B(int, double) {}
	};
	B func(void)
	{
    	return { 123, 321.0 };
	}

当使用auto声明变量使用列表初始化({})时,推导出的类别为std::initializer_list。当存在形参为std::initializer_list的构造函数时,{}初始化会强烈地优先调用带有std::initializer_list类型的版本。

class A {
public:
	A (int i, bool b);
	A (int i, double d);
	A (std::initializer_list<long double> il);
	...
};

	A a1(10, true);  // 调用第一个构造函数
	A a2{10, true};  // 调用第三个构造函数,10和true被强制转换为 long double
	A a3(10, 5.0);   // 调用第二个构造函数
	A a4{10, 5.0};   // 调用第三个构造函数,10和5.0被强制转换为 long double

当最优选的带有std::initializer_list类型的构造函数无法被调用时,编译器会报错。

class A {
public:
	A (int i, bool b);
	A (int i, double d);
	A (std::initializer_list<bool> il);
	...
};

	A a{10, 5.0};   // Error,int(10) 和 double(5.0) 向 bool 的强制转型都是窄化的,在{}内部是禁止的

只有在找不到任何办法把实参转化成std::initializer_list模板中的类型时,才会检查普通的构造函数。

class A {
public:
	A (int i, bool b);
	A (int i, double d);
	A (std::initializer_list<string> il);
	...
};

	A a1(10, true);  // 调用第一个构造函数
	A a2{10, true};  // 调用第一个构造函数
	A a3(10, 5.0);   // 调用第二个构造函数
	A a4{10, 5.0};   // 调用第二个构造函数

当对象既支持默认构造函数,又支持带有std::initializer_list类型形参的构造函数,如果{}的意义是“没有实参”,应该执行默认构造;如果意义是“空的std::initializer_list”,应该以一个不含任何元素的std::initializer_list为基础执行构造。

class A {
public:
	A ();
	A (std::initializer_list<string> il);
	...
};

	A a1();    // 默认构造
	A a2{};    // 默认构造
	A a3({});  // 带有std::initializer_list类型形参的构造函数,传入一个空的std::initializer_list
	A a4{{}};  // 同上

删除函数

删除函数(Deleted function)通常用来定义函数不存在以禁止某些调用。例如禁止类的复制:

class A
{
public:
	...
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	...
};

任何函数都可以成为删除函数。所以删除函数的一个用法是创建删除重载版本过滤参数隐式转换的调用:

bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;

删除函数还可以阻止某些模板实现:

template<typename T>
void processPointer(T* ptr);

// 禁止 void* 版本
template<>
void processPointer(void*) = delete;
template<>
void processPointer(const void*) = delete;

右值引用和移动语义

引用我们并不陌生,例如拷贝构造函数的第一个参数就要求是同类型的引用,这里的引用是左值引用(X& x)。左值(loactor value)是存储在内存中、有明确存储地址(可寻址)的数据 ,而有些值属于匿名对象,使用之后无法再访问,这一类成为右值(read value),对于右值的引用就是右值引用(X&& x)。

void fun(A&& a);  // a 是个左值,类型是指向A类型对象的右值引用

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化 。 右值引用还可以对右值进行修改。

移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。在拷贝构造函数中,拷贝的对象是const修饰的,拷贝过程中不会发生改变,而移动构造函数则会对源对象的成员转移到新的对象中,指针类型的数据会被移动到新对象,源对象的指针会被置空。移动构造函数的参数为右值引用,这意味着该对象不会再被引用,可以安全的转移,而对左值得转移却是十分危险的,因为我们可能会访问左值。

class A
{
public:
	A(const A& a);  // 拷贝构造函数
	A(A&& a);       // 移动构造函数
	...
};

std::move 和 std::forward

std::move不进行任何移动,std::forward不进行任何转发。两者都是仅仅执行强制类型转换的函数,std::move无条件地将实参强制转换成右值,std::forward则仅在某个特定条件满足时才执行学相应的强制转换。

std::move的实现类似于:

template<tpyename T>
typename remove_reference<T>::type&& move(T&& parm)    // 返回右值引用,remove_reference保证
{
	using ReturnType = typename remove_reference<T>::type&&;  
	return static_cast<ReturnType>(parm);
}

std::forward是有条件强制类型转换:仅当其参数是使用右值完成初始化时,才会执行向右值类型的强制转换。

// std::forward的类似实现见“引用折叠”

void process(const A& a);
void process(A&& a);

template<typename T>
void Process(T&& t)
{
	process(std::forward<T>(t));
}

A a;
Process(a);             // 调用时传入左值,最终调用 const A& 版本
Process(std::move(a));  // 调用时传入右值,最终调用 A&&  版本

万能引用

通常情况下,T&&的含义是右值引用,但它们还有另一种含义,既可以是右值引用,亦可以是左值引用,这使之既可以绑定到右值,也可以绑定左值,再进一步,它们也可以绑定const对象或非const对象、volatile对象或非volatile对象,它们几乎可以绑定任何对象,所以被称为万能引用

万能引用出现的两种场景:

template<T>
void f(T&& t);

auto&& var2 = var1; 

两个场景的共同之处在于都涉及类型推导,且声明的形式为T&&,这就是成为万能引用的充分必要条件。

若采用右值来初始化万能引用,就会得到一个右值引用;若采用左值,就会得到一个左值引用。

引用折叠

定义模板函数 func:

template <typename T>
void func(T&& t)
{
	...
}

对 func 考虑以下调用:

int n = 10;
func(n);  // T 为 int&
func(10); // T 为 int

对于func(n):T的推导结果为int&,实例化模板为:void func(int& &&),在c++中引用的引用是非法的,为了正确推导,存在引用折叠规则:

  • 如果引用的引用出现在允许的语境(如模板实例化),该双重引用会折叠成单个引用:如果任一引用为左值引用,则结果为左值引用,否则结果为右值引用。

引用折叠是std::forward运作的关键,std::forward的一种类似实现:

template <typename T>
T&& forward(typename remove_reference<T>::type& param)
{
	return static_cast<T&&>(param);
}

当传递左值时,T会被推导为 X&,然后对std::forward的调用会实例化为std::forward<X&>

X& && forward(typename remove_reference<X&>::type& param)
{
	return static_cast<X& &&>(param);
}
// typename remove_reference<X&>::type 结果为 X =>
X& && forward(X& param)
{
	return static_cast<X& &&>(param);
}
// 引用折叠 =>
X& forward(X& param)
{
	return static_cast<X&>(param);
}

当传递右值时,T的推导结果为 X:

X&& forward(typename remove_reference<X>::type& param)
{
	return static_cast<X&&>(param);
}
// typename remove_reference<X&>::type 结果为 X =>
X&& forward(X& param)
{
	return static_cast<X&&>(param);
}

引用折叠会出现的四种语境:

  • 模板实例化

  • auto变量的类型推导

  • 生成和使用typedef和别名声明

    template <typename T>
    class A
    {
    public:
    	typedef T&& RToT;
    	...
    }
    
    A<int&> a;  // 使用左值引用实例化
    
    typedef int& && RToT;
    typedef int& RToT;    // 引用折叠
    
  • decltype

完美转发

完美转发,指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。 结合万能引用、引用折叠以及std::forword<T>()实现。

完美转发的实现:

template <typename T>
void func(T&& t) 
{
    otherFunc(forward<T>(t));
}

完美转发失败的情形:

template <typename T>
void fwd(T&& param)
{
	f(std::forward<T>(param));
}
  • 大括号初始物

    void f(const std::vector<int>& v);
    
    f({1, 2, 3});         // {1, 2, 3} 隐式转换为std::vector<int>
    fwd({1, 2, 3});       // 错误,"非推导语境"
    					// fwd的形参未声明为std::initializer_list
    					// 禁止在fwd调用过程中从{1,2,3}出发推导
    auto il = {1, 2, 3};  // 推导为 std::initializer_list
    fwd(li);              // 正确转发
    
  • 0NULL作为空指针

    0NULL的推导结果为整型(一般为int),而非所传递实参的指针类型。使用nullptr解决。

  • 仅有声明的整型static const成员变量

    class A
    {
    public:
    	static const std::size_t val = 1;
    }
    
    void f(std::size_t val);
    
    f(A::val);    // 正确调用,f(28)
    fwd(A::val);  // 错误,无法链接
    

    对val的取址会发生失败,在编译器生成的机器代码中,引用通常是当指针处理,指针和引用在本质上是同一事物。所以需要准备某块内存以供指针去指涉。按引用传递整型static const成员变量通常要求其加以定义。

    const std::size_t A::val;
    
  • 重载的函数名字和模板名字

    void f(int (*pf)(int));
    
    int processVal(int value);
    int processVal(int value, int priority);
    
    f(processVal);     // 正确,选择匹配形参的版本
    fwd(processVal);   // 错误,作为函数模板不可能决定传递哪个版本的重载
    
    template <typename T>
    T processOnVal(T param)
    {...}
    
    fwd(processOnVal);  // 错误 
    

    想要正确实施完美转发,只有手动指定需要转发的重载版本或实例。

  • 位域

    ”非const引用不得绑定到位域“。C++规定可以指涉的最小实体是char,没有办法创建指涉到任意比特的指针,也就无法把引用绑定到任意比特。

    根据上述不难理解:接受位域实参的任何函数实际上只会接受到位域值的副本,因为可以传递位域的仅有的形参种类就只有按值传递,以及常量引用(标准要求这时常量引用绑定到存储在某种标准整型中的位域值的副本)。依此,利用转发目的函数接收的总是位域值的副本来把位域传递给完美转发函数。

    struct Header
    {
    	std::uint32_t a:4,
    				 b:4,
    				 c:6,
    				 d:2,
    				 e:16;
    	...
    }
    
    void f(std::size_t sz);
    
    f(h.e);    // 正确
    fwd(h.e);  // 错误
    
    auto e = static_cast<std::uint16_t>(h.e);  // 制作副本
    fwd(e);    // 正确
    

Lambda 表达式

Lambda 表达式(Lambda匿名函数)的基本语法如下:

[ caputrue ] ( params ) opt -> ret { body; };
  • capture:捕获列表

    注明 lambda 函数的函数体可以使用的变量, 指定捕获的变量以及捕获是通过值还是通过引用来捕获 ,可捕获范围为和当前 lambda 表达式位于同一作用域内的所有非静态局部变量

  • params:参数表

    如果不需要传递参数,可以连同 () 小括号一起省略

  • opt:函数选项

    • mutable:

      说明 lambda 表达式内部可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。

    • noexcept/throw():

      默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

    如果使用则之前的 () 小括号将不能省略。

  • ret:返回值类型

    如果 lambda 主体只包含一个返回语句,则可以省略 lambda 表达式的返回类型部分。 如果表达式不返回值,则为或。 如果 lambda 体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会将返回类型推导为 void

捕获列表

  • [] 不捕获任何变量。
  • [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用。
  • [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用。
  • [=, &f] 按值捕获外部作用域中所有变量,并按引用捕获 f 变量。
  • [&, f] 按引用捕获外部作用域中所有变量,并按值捕获 f 变量。
  • [bar] 按值捕获 bar 变量,同时不捕获其他变量。
  • [this] 捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

constexpr在编译期计算常量表达式

在 C++ 11 标准中,const用于为修饰的变量添加“只读”属性;而 constexpr关键字则用于指明其后是一个常量(或者常量表达式) 。

constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。

constexpr修饰普通变量,变量必须经过初始化且初始值必须是一个常量表达式 。

    constexpr int num = 1 + 2 + 3;
    int url[num] = {1,2,3,4,5,6};

constexpr修饰函数的返回值,这样的函数又称为“常量表达式函数”。 条件:

  • 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句
  • 函数必须有返回值,即函数的返回值类型不能是 void
  • 函数在使用之前,必须有对应的定义语句
  • return 返回的表达式必须是常量表达式

constexpr修饰类的构造函数

constexpr也可以修饰类中的成员函数,函数必须满足 4 个条件。

class myType {
public:
    constexpr myType(const char *name,int age):name(name),age(age){};
    constexpr const char * getname(){
        return name;
    }
    constexpr int getage(){
        return age;
    }
private:
    const char* name;
    int age;
};

// using
constexpr struct myType mt { "zhangsan", 10 };
constexpr const char * name = mt.getname();
constexpr int age = mt.getage();

constexpr修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。

struct myType {
    const char* name;
    int age;
};

template<typename T>
constexpr T dispaly(T t){
    return t;
}

// using
struct myType stu{"zhangsan",10};
struct myType ret = dispaly(stu);  // 无效
constexpr int ret1 = dispaly(10);

智能指针

智能指针的出现,主要是为了更好的实现RALL(Resource Acquisition Is Initialization)资源管理机制——利用对象生命周期控制资源。

智能指针是利用类的构造和析构来管理内存的分配和释放而设计出的一种特殊的类。在内存分配中,通常使用new/delete来为对象分配和释放内存,且new/delete必须匹配成对使用,如果只有new而没有delete,就会造成内存泄漏。此外,若程序在运行过程中出现异常而终止,delete语句没有执行也会造成内存泄漏。因此,我们需要采用智能指针来帮助我们智能地管理内存。

智能指针在创建后不需要使用delete来释放,它将裸指针(普通指针)封装成栈对象,在栈对象的生命周期结束时自动调用其析构函数释放分配的内存。

最早的智能指针auto_ptr

auto_ptr是对普通指针的一层封装,可以使用它来简单的管理普通指针,但它存在比较明显的缺陷,auto_ptr的拷贝构造函数和赋值运算符的函数体中存在类似的操作:

	// 拷贝构造
	AutoPtr(AutoPtr<T>& a) : ptr(a.ptr) 
	{
        a.ptr = NULL; 
    }

    // 赋值运算符重载
    AutoPtr<T>& operator=(AutoPtr<T>& a) 
    {
        if (this != &a)
        {
            delete ptr;
            ptr = a.ptr;
            a.ptr = NULL;
        }
        return *this;
    }

两个操作都将源指针置空,如果后续再对其进行操作,可能造成访问越界。C++11中已经摒弃了auto_ptr指针,新增加了三种智能指针:unique_ptrshared_ptrweak_ptr, 定义位于<memory>头文件,并位于 std命名空间中 。

unique_ptr智能指针

unique_ptr执行效率和auto_ptr一样高,但比auto_ptr更安全,它使用移动语义来进行资源的转移。在默认情况下,unique_ptr和裸指针拥有相同的尺寸。

unique_ptr实现的是专属所有权语义。一个非空的unique_ptr总拥有所指向的资源。使用unique_ptr时,编译器只允许将临时指针(右值)赋值给另一个指针,禁止对于有一段存在时间的指针的赋值操作,但可以使用std::moveunique_ptr对象转换成右值,再进行赋值。

std::unique_ptr<string> ps1, ps2;
ps1 = new string("...");  // Error
ps1.reset(new string("..."));
ps2 = ps1;            // Error
ps2 = std::move(ps1); // OK

除单对象(unique_ptr<T>)形式外,unique_ptr还提供了数组形式(unique_ptr<T[]>),对单对象形式不提供索引运算符(operator[]),而数组形式则不提供提领运算符(operator*operator->)。

默认地,unique_ptr通过调用所管理资源的析构函数来释放内存资源,析构通过delete运算符实现,但在析构过程中unique_ptr可以被设置自定义析构器:析构资源时所调用的函数(或函数对象),例如:

auto del = [](A* a)
{
	dosomthing();
	delete a;
};

std::unique_ptr<A, decltype(del)> ptr(nullptr, del);  // 类型为 unique_ptr<A, decltype(del)>

若析构器是函数指针,unique_ptr一般会增长1-2字长(word);若修析构器是函数对象,而尺寸变化取决于该函数对象中储存了多少状态,无状态的函数对象(例如,无捕获的 lambda 表达式)不会浪费任何储存尺寸。

unique_ptr可以方便高效地转换为shared_ptr

std::unique_ptr<string> up(new string);
std::shared_ptr<string> sp = up;

shared_ptr智能指针

shared_ptr采用了引用计数的方式,更好地解决赋值与拷贝的问题,使得shared_ptr支持多个指针指向同一对象。

引用计数:使用count计数器记录跟踪指向该资源的shared_ptr数量。shared_ptr的构造函数(通常)会使该计数递增,而析构函数会使该值递减,而复制赋值运算符同时执行两种操作(sp1 和 sp2 指向不同的对象,执行 sp1 = sp2 时,sp1 指向对象引用计数递增,sp2 指向对象引用计数递减)。当实施递减后引用计数为0时,shared_ptr将析构该资源。

从一个已有的shared_ptr移动构造一个新的shared_ptr会将源shared_ptr置空,所以移动构造函数不需要进行引用计数操作。移动赋值同理。

引用计数的存在带来了一些性能方面的影响:

  • shared_ptr的尺寸是裸指针的两倍。因为shared_ptr内部包含一个指向资源的引用计数的裸指针。
  • 引用计数的内存必须动态分配。使用make_ptr创建可以避免动态分配。
  • 引用计数的递增和递减必须是原子操作。因为在不同的线程中可能存在并发的读写操作。

unique_ptr类似,shared_ptr也使用delete运算符作为默认资源析构机制,也同样支持自定义析构器,但与unique_ptr有所不同:

auto del = [](A* a)
{
	dosomthing();
	delete a;
}

std::unique_ptr<A, decltype(del)> up(new A, del);  // 析构器类型是智能指针类型的一部分
std::shared_ptr<A> sp(new A, del);                 // 析构器类型非智能指针类型的一部分

这样的设计更具弹性,拥有不同析构器但具有同一类型的shared_ptr可以放置在同一容器中,也可以相互赋值。

std::std::shared_ptr<A> sp1(new A, del1);
shared_ptr<A> sp2(new A, del2);
std::vector<std::shared_ptr<A>> v { sp1, sp2 };
sp1 = sp2;

另一点与unique_ptr不同的是,自定义析构器不会改变shared_ptr的尺寸,都是裸指针的两倍。前面提到:shared_ptr内部包含一个指向资源的引用计数的裸指针,实际上指向的是包含引用计数在内的一个控制块数据结构,每一个shared_ptr管理的对象都有一个控制块,包含引用计数、弱计数和其他数据(自定义析构器等)。一个对象的控制块由创建首个指向该对象的shared_ptr的函数来确定。控制块的创建规则:

  • std::make_shared总是创建一个控制块
  • 从具专属所有权的指针出发构造一个shared_ptr时会创建一个控制块
  • shared_ptr构造函数使用裸指针作为实参调用时创建一个控制块

使用 this 指针作为shared_ptr构造函数实参时,可能会导致涉及 this 指针的多重控制块,对此,C++标准库提供了std::enable_shared_from_this来安全地由 this 指针创建shared_ptrstd::enable_shared_from_this定义了一个成员函数shared_from_this,它会创建一个shared_ptr但不会重复创建控制块。从实现上看,shared_from_this查询当前对象的控制块,并创建一个指向该控制块的新shared_ptr,这样的设计依赖于当前对象已有一个与其关联的控制块,必须已存在一个指向当前对象的shared_ptr,如果不存在,将导致未定义行为。为避免在shared_ptr产生前调用了shared_from_this,继承自std::enable_shared_from_this的类通常会将构造函数声明为private,并只允许用户通过返回shared_ptr的工厂函数来创建对象。

class A  : public std::enable_shared_from_this<A> 
// (奇妙递归模板模式,The Curiously Recurring Template Pattern,CRTP)
{
public:
	...
	static std::shared_ptr<A> create();  // 使用工厂函数创建对象
	void process();
private:
	... // 构造函数
}

void A::process()
{
	...
	auto sp = shared_from_this();
}
make_shared

make_shared会把一个任意实参集合完美转发给动态分配内存的对象的构造函数,并返回一个指向该对象的shared_ptr指针:

std::shared_ptr<A> sp1(new A);    // 不使用make
auto sp2(std::make_shared<A>());  // 使用make

与new版本相比的优势:

  • 避免重复撰写类型
  • 异常安全。new版本:new 和 shared_ptr的构造两个操作间出现异常时,将发生资源泄漏;make_shared:动态分配的对象会马上被安全存储在shared_ptr中,后续产生异常将正确析构资源对象。
  • 性能提升。new版本会引发两次内存分配:为对象进行一次内存分配,还有为其相关联的控制块再进行一次内存分配;使用make_shared只需一次内存分配,make_shared会分配单块内存既保存对象又保存与其相关联的控制块。

不能使用或不适合使用make_shared的情况:

  • 创建一个使用自定义析构器shared_ptr,make函数不允许使用自定义析构器
  • 使用{}创建对象,不能完美转发大括号初始物
  • 自定义内存管理的类
  • 对象尺寸较大
  • 存在weak_ptr

weak_ptr智能指针

weak_ptrshared_ptr一样方便,但不参与管理所指对象的共享所有权,即不影响其对象的引用计数(弱引用),是shared_ptr的扩充。weak_ptr能够通过跟踪指针何时空悬,判断所指对象已不存在。

weak_ptr一般通过shared_ptr来创建:

auto sp1 = std::make_shared<A>();   // 引用计数为1
std::weak_ptr<A> wp(sp1);           // 引用计数保持为1
...
sp1 = nullptr;   // 引用计数为0,对象析构,wp 空悬

weak_ptr的空悬,也被称作失效(expired),可以直接测试:

wp.expired(); 

weak_ptr缺乏提领操作(*->),且将检验和提领分离可能会带来竞险:在检验和提领之间另一线程析构重新赋值最后一个shared_ptr导致对象被析构,提领将引发未定义行为。因此,需要一个原子操作来完成weak_ptr的检验以及在未失效下提供对所指对象的访问,该操作可以通过由weak_ptr创建shared_ptr来实现。该操作有两种方法:

1、weak_ptr::lock,返回一个shared_ptr,如果weak_ptr失效,返回空shared_ptr

std::shared_ptr<A> sp2 = wp.lock();

2、用weak_ptr作为实参构造shared_ptr,如果weak_ptr失效,抛出异常

std::shared_ptr<A> sp3(wp);

weak_ptr的一个常用场景:A 和 C 共享 B 的所有权,各持有一个指向 B 的shared_ptr

	shared_ptr			shared_ptr
A ----------------> B <----------------C
  <----------------
  	  ?ptr

假设有一个指针从 B 指回 A (类似双向链表),考虑该指针的类型:

  • 裸指针:若 A 被析构,C 仍指向 B,则 B 保留该空悬指针但无法检测,后续可能导致未定义行为
  • shared_ptr:A 和 B 相互保存着指向对方的shared_ptr形成环,引用计数至少为一,两者再也不能被析构,造成内存泄漏。
  • weak_ptr:B 持有的指针不会影响 A 的引用计数,若 A 被析构,B 的回指指针将空悬但可以被检测。

weak_ptr的尺寸和shared_ptr相同,和shared_ptr使用相同的控制块,其构造、析构和赋值操作都包含了对引用计数的操作,当然操作的不是shared_ptr的引用计数,而是卡控制块中的第二个引用计数(弱计数)。

作者:YIMG
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/YIMG/p/13363185.html