• C++11列表初始化+右值引用+类的新功能


    一. 列表初始化

    c++11中扩大了用大括号括起来的列表(初始化列表的使用范围),使其可以用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
    在没接触c++11之前,对于一个value或object的初始化有很多方法,也许会用到大括号,小括号,等号来进行各种初始化。
    已经有两个类

    class Date
    {
    public:
    	//explicit Date(int year, int month, int day)
    	Date(int year, int month, int day)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{
    		cout << "Date(int year, int month, int day)" << endl;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    class A
    {
    public:
    	A(int a)
    		:_a(a)
    	{}
    
    private:
    	int _a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    eg:

    //c++98支持
    	A aa(1);
    	A a = 1;//单参数的构造函数支持隐式类型转换
    	Date d1(2022, 11, 11);
    	int z = 0;
    	int arr[3] = { 1,2,3 };
    	vector<int>zjt;//初始化一个空的vector
    	vector<int>v1(5, 10);//指明vector中元素的个数并赋值
    	vector<int>v2(v1.begin(), v1.end());//利用v2进行初始化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    (1). 使用{}进行初始化的原理分析

    可以看到上述的初始化方法非常多,但是对value或object的初始化缺乏统一的方法,对某些初始化不支持,eg:我想在vector容器初始化的时候直接插入1 2 3 4 5,之前写法是先定义一个vector,然后依次遍历数组push_back,但这是两个步骤,太麻烦了。c++11引入了一致性的初始化方法,对初始化操作进行了统一的处理。

    int a{};//这种初始化的方式就比较友好,{}中为空,a会被默认的初始化为0
    int array[]{0,1,2,3,4,5};
    vector<int> v{1,2,3,4,5};
    vector<string> cities{"BeiJing","ShangHai","GuangZhou","ShenZhen"};
    complex<double> com{4.0,3.0};//complex是数学中的复数,第一个和第二个分别代表实数位和虚数位
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译器在看到{t1,t2…tn}时便做出了一个initializer_list,它关联到一个array.调用函数时,该array内的元素可以被编译器分解逐一传递给函数,但是如果函数参数是一个initializer_list,这“包”数据也就是{t1,t2…tn},将整体传入到函数中。
    eg:
    上述cities,{}内会形成一个initializer_list,背后有个array,调用vector的构造函数时,编译器找到了vector其中有一个构造函数是支持initializer_list,而对于com而言,{}也会形成initializer_list,背后有一个array,但是其本身类complex没有initializer_list的构造函数,所以此时会将array中的元素拆解开来传递给com,而事实上,STL的所有容器的构造函数都支持initializer_list传参。

    vector (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
    list (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
    deque (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
    map (initializer_list<value_type> il,const key_compare& comp = key_compare(),
    		const allocator_type& alloc = allocator_type());
    set (initializer_list<value_type> il,const key_compare& comp = key_compare(),
         	const allocator_type& alloc = allocator_type());
    //上述为常见容器中含inisializer_list的构造函数	
    
    	
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    (2). Initializer_list的源码定义(vs2019
    template <class _Elem>
    class initializer_list {
    public:
        using value_type      = _Elem;
        using reference       = const _Elem&;
        using const_reference = const _Elem&;
        using size_type       = size_t;
    
        using iterator       = const _Elem*;
        using const_iterator = const _Elem*;
    
        constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {}
    
        constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept
            : _First(_First_arg), _Last(_Last_arg) {}
    
        _NODISCARD constexpr const _Elem* begin() const noexcept {
            return _First;
        }
    
        _NODISCARD constexpr const _Elem* end() const noexcept {
            return _Last;
        }
    
        _NODISCARD constexpr size_t size() const noexcept {
            return static_cast<size_t>(_Last - _First);
        }
    
    private:
        const _Elem* _First;
        const _Elem* _Last;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    源码分析:
    根据大佬文章,编译器首先会调用constexpr initializer_list()进行构造,但是在调用前initializer_list的背后已经关联好了array数组,而函数中的 _First,_Last,分别指向数组的起始和末尾的结尾元素。
    需要注意的是,这些元素都被包含在array数组中,initializer_list中并没有包含这些元素,它提供的是指向array的指针。

    int a;//只是被定义没有被初始化
    int b{};//{}为空,默认被初始化为0
    char* p;//只是被定义没有初始化
    char* q{};//{}为空,默认初始化为NULL
    int x {5.0};//ERROR,VS2017报告错误,double转int需要进行收缩转换
    double y {5};//可行
    char a(65);//会对应ASCII码自动进行转换为A
    char a {65};//ERROR,不能进行转换
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    //注意,使用{}时不能进行收缩转换

    (3). 模拟实现initializer_list实现vector

    在这里插入图片描述
    总结:{}花括号括起来就相当于是被识别成Initializer_list,然后再隐式类型转换成相应的类型。

    (4).关键字decltype
    const int x = 1;
    	double y = 2.2;
    	decltype(x * y) ret; // ret的类型是double
    	decltype(&x) p; // p的类型是int*
    	cout << typeid(ret).name() << endl;
    	cout << typeid(p).name() << endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    typeid:获取这个类型的字符串,但是不能用这个字符串去定义这个类型
    想要用推导出来的类型再定义一个对象:decltype;
    array这个容器几乎和静态数组差不读,只是数组是抽样检查,但这个容器只要越界了就一定会检查。

    二.右值引用

    (1).左值与右值

    先搞懂一个问题:什么是左值?
    左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现再赋值符号左边, const修饰后的左值,不能赋值,但是可以取地址,左值引用就是给左值去别名。

    int main()
    {
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    所以,什么是右值?
    右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值(这个不能是左值引用的返回)等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址 右值引用就是对右值取别名,给右值取值。

    int main()
    {
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    右值是不能取地址的,但是给右值取别名后,会导致右值被储存到特位置,且可以取到该位置的地址,eg:不能直接取字面常量1的地址,但是int &&ret = 1;此时就可以对ret取地址,也可以修改ret,如果不想ret被修改,直接从const int && ret = 1;
    区分左右值:左值能取地址,右值不能取地址

    a.左值引用与右值引用的比较

    左值:
    1.左值引用只能引用左值,不能引用右值
    2.但是const左值引用既可以引用左值也可以引用右值
    eg void test(string &s);该函数只能引用左值
    但是如果是void test(const string &s)此时既可以引用左值也可以引用右值
    右值:
    1、右值引用只能引用右值,不能引用左值
    2.但是如果move(右值),那么该右值拥有了左值的属性

    (2)右值引用使用场景

    左值引用是为了减少拷贝构造,提高效率,右值引用其实也一样,解决那些左值引用无法解决的问题。
    eg:

    string T_striing(int value)//将一个整数转变成字符串
    //string& T_striing(int value)//将一个整数转变成字符串
    //string&& T_striing(int value)//将一个整数转变成字符串
    
    {
    	bool flag = true;
    	if (value < 0)
    	{
    		flag = false;
    		value = 0 - value;
    	}
    	string ret;
    	while (value > 0)
    	{
    		int x = value % 10;
    		value /= 10;
    		ret += (x + '0');
    
    
    	}
    	if (flag == false)
    	{
    		ret += '-';
    	}
    	reverse(ret.begin(), ret.end());
    	return ret;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    此时不能用左值引用返回,因为此时返回值实际上是一个临时变量,出了作用域就会被销毁,但是此时直接右值引用也:不行,因为此时即使return move(ret)使得临时变量具有了右值的属性,但是move出了作用域还是会被销毁。
    此时怎么办?怎样提高效率?
    再看一些代码:
    string s1(“12345”) to_string(1234) s1+“hello world” 这些被称为单参数都被称为将亡值,即对象的声明周期只在一行代码,将亡值其生命周期都走到头了,如果此时还要对其进行拷贝构造然后再析构的话,不如直接将他的资源拿过来。
    eg:上述的string(“12345”) 本质是,"12345"先构造成一个临时变量+拷贝构造给s1,但编译器优化:直接将"12345"深拷贝给s1,但是"12345"之前构造出来的临时变量string还是会被析构。所以不如资源转移,而完成资源的转移需要的是移动构造,移动构造的参数必须是右值引用
    移动构造:将参数右值的资源窃取过来,占为己有,此时就不用再去做深拷贝了。
    该string是我自己模拟实现,可以参考
    string模拟实现

    // 移动构造
    		string(string&& s)
    			:_str(nullptr)
    			, _size(0)
    			, _capacity(0)
    		{
    			cout << "string(string&& s) -- 资源转移" << endl;
    			swap(s);
    		}
    
    		// 拷贝构造
    		string(const string& s)
    			:_str(nullptr)
    			, _size(0)
    			, _capacity(0)
    		{
    			cout << "string(const string& s) -- 深拷贝" << endl;
    
    			string tmp(s._str);
    			swap(tmp);
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看出深拷贝还创建了一个临时变量再与this交换,但是移动
    构造是直接将
    this与s交换,因为s此时就是一个右值引用,也许其声明周期出了函数作用域就没了,所以直接这样也不会有问题。
    有移动构造肯定有移动赋值

    // 移动赋值
    		string& operator=(string&& s)
    		{
    			cout << "string& operator=(string&& s) -- 资源转移" << endl;
    			swap(s);
    
    			return *this;
    		}
    
    		// 赋值重载
    		string& operator=(const string& s)
    		{
    			cout << "string& operator=(string s) -- 深拷贝" << endl;
    			string tmp(s);
    			swap(tmp);
    
    			return *this;
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    下面用一张图涉及到编译器的优化彻底搞懂他们的区别;

    在这里插入图片描述
    注意移动构造只是将资源进行了转移,而不是延长了资源的声明周期,而且不要随意使用move使左值获得了右值的属性,因为变成右值真正意义在于拷贝的时候可以直接掠夺资源,不要轻易使用move,否则资源被掠夺了都不知道。
    确定这个对象后续不要了,才可以move。
    在这里插入图片描述
    在这里插入图片描述
    通过官网的源代码可以看到,c++11之后swap函数也支持移动构造,大大提高了深拷贝的效率。
    注意c++11之后STL的容器都提供了移动构造和移动赋值
    所以:

    vector<string> vv;
    string v1 = "zbc";
    vv.push_back(v1)//调用的是左值引用也就是深拷贝
    vv.push_back("zbc")//此时是右值引用,也就是移动构造。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3).完美转发(了解即可)

    模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
    模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
    但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
    我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
    eg:
    在这里插入图片描述
    完美转发的应用:在模板参数中,保证该移动构造和移动赋值。

    三.类的新功能与可变参数

    (1).类的新功能

    a.移动构造和移动赋值重载

    c++11除了原本的6个默认成员函数,还增加了上述提到的移动构造和移动赋值运算符重载。
    1.如果你没有自己实现移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载中的任意一个函数,那么编译器会自动生成一个默认的移动构造,这个默认的移动构造,对于内置类型会执行逐字节拷贝,自定义类型成员,则要看这个成员是否实现移动构造,实现了就调用该成员的移动构造,没有实现就调用拷贝构造。
    2.如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值,同上。
    3.我们自己写了移动构造和移动赋值的话,编译器是不会自动生成的。

    b缺省参数+强制生成默认函数的关键字default/delete:

    c++11允许在类定义时给成员变量初始缺省参数,默认生成构造函数会使用这些缺省值初始化。
    eg

    private:
    int a = 9;
    
    • 1
    • 2

    这是给内置类型不初始化打的补丁,有的编译器会默认初始化成随机值,有的不会,而c++11的标准是不会的。
    强制生成默认函数的关键字default:
    强制生成默认函数的关键字default:
    c++11可以让我们更好的控制要使用的默认函数,假设我们要使用某个默认的构造函数,但是由于某些原因编译器没有默认生成,例如我们自己生成了构造函数,那么构造函数就不会生成,此时我们可以搭配关键字default使用。
    或者是我们不写拷贝构造函数,但是我们也不希望编译器默认生成,此时搭配关键字delete使用。
    在这里插入图片描述

  • 相关阅读:
    扫地机器人地图与用户终端的同步
    嵌入式开发:为什么无触摸手势对嵌入式GUI开发团队至关重要
    使用二手 gopro 做行车记录仪
    学校网页设计成品 基于HTML+CSS+JavaScript仿山东财经大学官网 学校班级网页制作模板 校园网页设计成品
    卷积网络的发展历史-LeNet
    桂林电子科技大学计算机考研资料汇总
    Oracle数据库开发者工具
    DMA方式
    STM32微控制器实现无人机智能导航与控制(内附资料)
    linux中操作服务器常用命令
  • 原文地址:https://blog.csdn.net/cxy_zjt/article/details/127832211