• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

C++ 11实用特性

武飞扬头像
Fxzbed
帮助1

CPP 11实用特性总结


1. 字符串原始字面量

使用R "xxx(....)xxx"输出原始字符串

cout << R"(/t)";
cout << R"(/t
		232
		//
		/n)"

2. 指针空值类型 nullptr

在C中指针的初始化通常赋值NULL 在C 中我们赋值nullptr 那么他们有什么区别?

NULL 是一个宏常量

#define NULL 0;
#define NULL (void*)0;

nullptr是一个std::nullptr_t类型的的空指针常量

因此使用nullptr可以防止出现强制类型转换导致的问题

null和nullptr都可以用来表示空指针,但nullptr更加安全和规范。在C 11及以后的版本中,建议使用nullptr来表示空指针。


3. constexpr 修饰常量表达式

在C中我们常用const 赋值常量 在C 中我们使用constexpr 那么他们有什么区别?

不容置疑的是无论是const还是constexpr他们都是表示常量的类型。

  • 主要区别在于const声明的常量,编译器无法判断,无法在编译阶段就确定。例如 const int a = 10;

  • 而constexpr定义的常量 可以在编译阶段就确定 这可以提高程序运行效率。例如 constexpr int a = 10;

另外constexper也可以作为函数返回类型。例如

constexpr int returnconst(int x) { return x* x; }

==注意== 在constexpr返回值的函数中,不得出现逻辑运算,这是因为该函数会在编译阶段运行,所以只能执行简单语句 例如 数学运算,赋值等


4. 使用auto的自动类型推导

使用auto 自动类型推导可以帮助我们节省开发时间,提高开发效率

auto语法简单实用,例如

int x[10];

for (auto x_ : x) cout << x << ' ';

但值得一提的是要注意auto的使用场景


5. decltype的使用

一些场景中我们需要使用decltype帮助我们推导变量数据类型

decltype的简单使用

int a = 10;

decltype(a) x = 114514;

int func (int x) {...}

decltype(func(x)) x_ = 114514;

==如果函数返回一个纯粹的右值==

例如 const int func() {...}

decltype(func())的推导即为int (因为此时const int返回了一个纯粹的数值 会被推导为int)

  • decltype在范型编程中的应用
#include <list>
using namespace std;

template <class T>
class Container
{
public:
    void func(T& c)
    {
        for (m_it = c.begin(); m_it != c.end();   m_it)
        {
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    ??? m_it;  // 这里不能确定迭代器类型
};

int main()
{
    list<int> lst;
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}

在private中的迭代器因为传入类的不同迭代器会分为两种 T::const_iteratorT::iterator,在decltype下我们就可以解决问题,通过decltype类型推导我们就可以得到想要的类型

当然我们可以基于auto改写for循环并且取消私有迭代器,类中的代码就会变成这样

template <class T>
class Container
{
public:
	void func(T& c)
	{
		for (auto m_it = c.begin(); m_it != c.end();   m_it)
		{
			cout << *m_it << ' ';
		}
		cout << endl;
	}
}

6.返回值类型后置

返回值类型后置也是decltype的一种用法

在范型编程中我们经常遇到我们不明确知道模板函数返回值类型的情况

例如:

template <typename R, typename T, typename U>
R add(T t, U u)
{
    return t   u;
}

此时R的类型应当是 t u 并且类型转换的类型,为使得函数模板支持所有类型我们必须要使得返回类型是动态的,使用decltype可以做到这一点

template <typename T, typename U>
decltype(t u) add(T t, U u)
{
    return t   u;
}

我们发现直接使用这种写法,是无法通过编译的,因为编译器并不知道我们后面会有t,u两个参数,所以使用类型后置语法改写

template <typename T, typename U>
auto add(T t, U u) -> decltype(t   u)
{
	return t   u;
}

通过上述语法分析我们可以得到结论:auto会自动追踪decltype()推导出的类型。


7. using的使用

对于using我们常用命名空间std,可以说是很熟悉了,但是在cpp11中using有什么样的新特性?

  • 使用using定义别名

    这种用法类似typedef,代码形如 using xxx = 类型名称

    可以定义类型的别名,被重新定义的类型是原有类型的别名,不是创造新类型

    使用using进行别名的定义更加具有可读性,例如

typedef int(*function)(int, string) //函数指针

using function = int(*)(int, string)
  • 为模板定义别名

    使用typedef重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,对于using是很简单的

tamplate <typename T>
using mytamplate = map<int, T>

在之后可以使用mytamplate 这样的形式为T指定类型


8. 列表初始化

关于C 中的变量中有不同的初始化方法,但没有一种方法能够适用于所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,在C 11中提出了列表初始化的概念

  • 直接在变量名后面跟上初始化列表,进行对象、变量的初始化
double a = {1.123};
int* p = new int{1};
int* array = new int{1, 2, 3};
  • 初始化列表还可以用在函数返回值中
class person
{
	public:
		person(int id, string name)
		{
			cout << "id: " << id << ' ' << name < '\n';
		}
};

person func()
{
	return {1, "name"};
}
  • 聚合体的初始化列表

因为列表初始化的使用范围大大增强,一些模糊的概念也随之而来,在前面的例子可以得知对于一个自定义类型的初始化可能有两种执行结果

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

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象b并没有被初始化列表中的数据初始化。

对象1是一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表的数据来初始化结构体中的成员。

对象2自定义了一个构造函数,实际的初始化是通过这个构造函数完成的

==如果使用列表初始化时,还需要判断初始化的对象是否是聚合体,如果是初始化列表的数据就会拷贝到对象中==

什么类型会被认为是聚合体呢?
  1. 普通数组
int x[] = {1,2,3,4,5,6};
double y[3][3] = {
    {1.23, 2.34, 3.45},
    {4.56, 5.67, 6.78},
    {7.89, 8.91, 9.99},
};
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};
  1. 无用户自定义构造函数、无基类、无虚函数、无private、protected的非静态数据成员的(class、struct、union)可以被看做一个聚合类型(初始化列表只能初始化非静态成员变量)
struct T1
{
    int x;
    long y;
protected:
    int z;
}t{ 1, 100, 2};		// error

struct T2
{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 100, 2};		// error

struct T2
{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 100};		// ok
// 静态成员的初始化
int T2::z = 2;
//结构体中的静态变量 z 不能使用列表初始化进行初始化,它的初始化遵循静态成员的初始化方式。
  • 非聚合体的变量的初始化方法——使用类中的构造函数
  • 聚合类型的定义是非递归 ,也就是说==一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型==
#include <iostream>
#include <string>
using namespace std;

struct T1
{
    int x;
    double y;
private:
    int z;
};

struct T2
{
    T1 t1;
    long x1;
    double y1;
};

int main(void)
{
    T2 t2{ {}, 520, 13.14 };
    return 0;
}

在示例中,T1并非聚合类型因为它有一个private z,但是T2仍然可以使用初始化列表的方式进行初始化。

T2对象的初始化过程,对于非聚合类型的成员T1做初始化时,可以直接写成=={ }==,这就相当于调用T1的无参构造函数

  • std::initializer_list

STL容器中,我们可以实现任意长度数据的初始化,使用初始化列表也只能固定参数的初始化,如果想要有任意长度参数初始化的能力,可以使用std::initializer_list 这样的轻量级类模板实现

std::initializer_list 模板的一些特点

  1. 它是一个轻量级的容器类型,内部定义了iterator等必须的概念,遍历时得到的iterator是只读的
  2. std::initializer_list而言,它可以接收任意长度的初始化列表,但是要求元素必须是同类型的T
  3. 在std::initializer_list内部有三个成员接口:size() 、 begin() 、 end().
  4. std::initializer_list 对象只能被整体初始化或者赋值

如果想要自定义一个函数接受任意个数的同类型参数,只需要把函数的参数指定为std::initializer_list,使用初始化列表=={ }==作为实参传递即可

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

void traversal(std::initializer_list<int> a)
{
    for (auto it = a.begin(); it != a.end();   it)
    {
        cout << *it << " ";
    }
    cout << endl;
}

int main(void)
{
    initializer_list<int> list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}

std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为遍历这种类型的容器时只得到一个只读迭代器,我们不能修改里面的数据,只能通过值覆盖进行数据的修改,std::initializer_list的效率是非常高的,它的内部并不保存初始化列表中元素的拷贝,仅仅储存了初始化列表中元素的引用。

自定义的类与上述同理,我们在构造函数参数指定为std::initializer_list类型,在自定义类的内部使用容器来接受多个实参

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

class Test
{
public:
    Test(std::initializer_list<string> list)
    {
        for (auto it = list.begin(); it != list.end();   it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector<string> m_names;
};

int main(void)
{
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}

9. 可调用对象

可调用对象是C 中的一种概念

可调用对象一般有如下定义
  • 是一个函数指针
void print(string a) {
	cout << a << endl;
}
using funcptr = void(*)(string);
  • 一个仿函数
#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Test
{
	void operate() (string msg)
	{
		cout << "msg: " << msg << endl;
	}
};

int main(void)
{
	Test t;
	t("Test func");
	return 0;
}
  • 一个可重载为函数指针的类对象
#include <iostream>
#include <string>
#include <vector>
using namespcae std;

using funcptr = void(*)(string);
struct Test
{
	static void print(string a) {
		cout << a << endl;
	}
	operator funcptr()
	{
		return print;
	}
};

int main(void)
{
	Test t;
	t("Test func"); 
	
	return 0;
}
  • 一个类成员函数指针或者类成员指针
#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Test
{
    void print(int a, string b)
    {
        cout << "name: " << b << ", age: " << a << endl;
    }
    int m_num;
};

int main(void)
{
    // 定义类成员函数指针指向类成员函数
    void (Test::*func_ptr)(int, string) = &Test::print;
    // 类成员指针指向类成员变量
    int Test::*obj_ptr = &Test::m_num;

    Test t;
    // 通过类成员函数指针调用类成员函数
    (t.*func_ptr)(19, "Monkey D. Luffy");
    // 通过类成员指针初始化类成员变量
    t.*obj_ptr = 1;
    cout << "number is: " << t.m_num << endl;

    return 0;
}

在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。C 中的可调用类型虽然具有比较统一的操作形式,但定义方式五花八门,这样在我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。现在,C 11通过提供std::function 和 std::bind统一了可调用对象的各种操作

可调用对象包装器

std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们

下面的实例代码中演示了可调用对象包装器的基本使用方法:

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

int add(int a, int b)
{
    cout << a << "   " << b << " = " << a   b << endl;
    return a   b;
}

class T1
{
public:
    static int sub(int a, int b)
    {
        cout << a << " - " << b << " = " << a - b << endl;
        return a - b;
    }
};

class T2
{
public:
    int operator()(int a, int b)
    {
        cout << a << " * " << b << " = " << a * b << endl;
        return a * b;
    }
};

int main(void)
{
    // 绑定一个普通函数
    function<int(int, int)> f1 = add;
    // 绑定以静态类成员函数
    function<int(int, int)> f2 = T1::sub;
    // 绑定一个仿函数
    T2 t;
    function<int(int, int)> f3 = t;

    // 函数调用
    f1(9, 3);
    f2(9, 3);
    f3(9, 3);

    return 0;
}

通过测试代码可以得到结论:std::function可以将可调用对象进行包装,得到一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同,通过包装器对象就可以完成对包装的函数的调用了。

绑定器

std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:

  1. 将可调用对象与其参数一起绑定成一个仿函数
  2. 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数

绑定器使用示例:

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

void callFunc(int x, const function<void(int)>& f)
{
    if (x % 2 == 0)
    {
        f(x);
    }
}

void output(int x)
{
    cout << x << " ";
}

void output_add(int x)
{
    cout << x   10 << " ";
}

int main(void)
{
    // 使用绑定器绑定可调用对象和参数
    auto f1 = bind(output, placeholders::_1);
    for (int i = 0; i < 10;   i)
    {
        callFunc(i, f1);
    }
    cout << endl;

    auto f2 = bind(output_add, placeholders::_1);
    for (int i = 0; i < 10;   i)
    {
        callFunc(i, f2);
    }
    cout << endl;

    return 0;
}

在上面的程序中,使用了std::bind绑定器,在函数外部通过绑定不同的函数,控制了最后执行的结果。std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。

placeholders::_1是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5等……

有了占位符的概念之后,使得std::bind的使用变得非常灵活 (出现bind中已经定义的参时,优先使用bind中的)

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

void output(int x, int y)
{
    cout << x << " " << y << endl;
}

int main(void)
{
    // 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数
    bind(output, 1, 2)();
    bind(output, placeholders::_1, 2)(10);
    bind(output, 2, placeholders::_1)(10);

    // error, 调用时没有第二个参数
    // bind(output, 2, placeholders::_2)(10);
    // 调用时第一个参数10被吞掉了,没有被使用
    bind(output, 2, placeholders::_2)(10, 20);

    bind(output, placeholders::_1, placeholders::_2)(10, 20);
    bind(output, placeholders::_2, placeholders::_1)(10, 20);


    return 0;
}

在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。f1的类型是function<void(int, int)>,通过使用std::bind将Test的成员函数output的地址和对象t绑定,并转化为一个仿函数并存储到对象f1中。

使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为function<int&(void)>的包装器对象f2中,并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型,并且允许修改绑定的变量,因此需要指定为变量的引用,由于没有参数因此参数列表指定为void。

示例程序中是使用function包装器保存了bind返回的仿函数,如果不知道包装器的模板类型如何指定,可以直接使用auto进行类型的自动推导,这样使用起来会更容易一些。


10. Lambda表达式

许多现代编程语言中都存在Lambda表达式,Lambda简洁、灵活的特点赋予C 更多的活力

lambda表达式定义了一个==匿名函数==,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:

[capture](params) opt -> ret {body;};

其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。

  1. 捕获列表[ ]: 捕获一定范围内的变量
  2. 参数列表( ): 和普通的参数列表一样(没有参数传入的情况下可以省略)
  3. opt选项(不需要时也可以省略)
  • mutable : 捕获的变量将在Lambda表达式内修改
  • exception : 指定函数抛出的异常,如抛出的整数类异常,可以使用throw( );
  1. 返回值类型:在c 11中,Lambda表达式的返回值是通过返回值后置语法定义(可以使用auto自动推导此时就可以省略返回值后置——前提不返回一个不被显性定义的初始化列表)
  2. 函数体:函数内容
捕获列表

Lambda表达式的捕获列表可以捕获一定范围内的变量,使用方法如下

  • [ ] - 不捕捉任何变量
  • [ & ] - 捕获外部作用域中的所有变量,做引用在函数体内使用
  • [ = ] - 捕获外部作用域中的所有变量,值传递入函数体内使用(==在值捕获的参数在Lambda表达式函数体中默认为只读==)
  • [ =, &arg ] - 按值捕获外部作用域所有的变量,并且按照引用捕获外部变量arg
  • [ bar ] - 按值捕获bar变量,同时不捕获其他变量
  • [ &bar ] - 上述捕获方式的引用版本
  • [ this ] - 捕获当前类中的this指针 (Lambda表达式将获得当前类成员同等的访问权限,如果使用了 & 或 = 默认捕获 this 指针)
函数本质

使用Lambda表达式捕获外部变量,如果我们希望修改捕获的变量,那么如何处理呢?,使用mutable-opt

为什么值拷贝捕获的变量是只读的?
  1. lambda表达式类型在C 11中被看做是一个带operator()的类
  2. 按照C 标准,Lambda的operator()是默认为const, const成员函数是无法修改成员变量值的
  3. mutable作用在于取消const属性

因为Lambda在C 中被看做为一个仿函数,我们可以使用std::function std::bind 进行包装与绑定


11. 左值和右值引用

C 11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解什么是左值和右值

右值
  • lvalue 是loactor value的缩写,rvalue 是 read value的缩写
  • 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
  • 右值是指可以提供数据值的数据(不可取地址);

可以简单的以盒子和盒子里的东西区分

通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。

C 11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

  • 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
  • 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。
右值引用

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

右值引用代码参考如下

#include <iostream>
using namespace std;

int&& value = 520;
class Test
{
public:
    Test()
    {
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj()
{
    return Test();
}

int main()
{
    int a1;
    int &&a2 = a1;        // error
    Test& t = getObj();   // error
    Test && t = getObj();
    const Test& t = getObj();
    return 0;
}
  • 在上面的例子中int &&value = 520 其中的520是纯右值,value是对字面量520这个右值的引用
  • 在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
  • 在Test& t = getObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值。
  • 在Test && t = getObj();中getObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。
  • const Test& t = getObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值

在c 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test()
    {
        delete m_num;
        cout << "destruct Test class ..." << endl;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数


12. std::move

在C 11添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助std::move()函数,使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。

从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);,函数原型如下

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{	// forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}

std::move可以实现使用一个已经赋值的右值引用的变量为一个未赋值的右值引用的初始化

也可以使用ls做性能优化

list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls;        // 需要拷贝, 效率低
list<string> ls2 = move(ls);

如果不使用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用move()就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。


13. forword完美转发

右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用C 11提供的std::forward()函数,该函数实现的功能称之为完美转发。

template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精简之后的样子
std::forward<T>(t);
  • T是左值引用类型时,t将被转换为T类型的左值
  • 当T不是左值引用类型时,t将被转换为T类型的右值

14. shared_ptr

在C 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C 11中提供了三种智能指针,使用这些智能指针时需要引用头文件

  • std::shared_ptr:共享的智能指针
  • std::unique_ptr:独占的智能指针
  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
shared_ptr的初始化

共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:

long use_count() const noexcept;

如果智能指针被初始化了一块有效内存,那么这块内存的引用计数 1,如果智能指针没有被初始化或者被初始化为nullptr空指针,引用计数不会 1。另外,不要使用一个原始指针初始化多个shared_ptr。

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

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

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

通过C 提供的std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:

shared_ptr<T> make_shared( Args&&... args );

使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:

void reset() noexcept;
template< class Y >
void reset( Y* ptr );

template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );

template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );
  • ptr:指向要取得所有权的对象的指针
  • d:指向要取得所有权的对象的指针
  • aloc:内部存储所用的分配器

对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的get()方法,其函数原型如下:

T* get() const noexcept;
--------------------------
#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // 得到指针的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

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

// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p)
{
    delete p;
    cout << "int 型内存被释放了...";
}

int main()
{
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}
--------------------------------------
int main()
{
    shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
    return 0;
}

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhbjka
系列文章
更多 icon
同类精品
更多 icon
继续加载