【C++ 】C++11(上篇)


C++11

相比于C++98,C++11则带来了数量可观的变化,以及对C++03缺陷的修正。C++11语法更加泛化简单化、更加稳定安全,功能更强大,提升开发效率。

cpp11

 

1. 列表初始化

C++11扩大了用{}(初始化列表)的使用范围,可用于所有的内置类型和自定义类型,可以省略赋值符=

// 内置类型变量
int x1 = {10};
int x2{10};//建议使用原来的,不推荐
int x3 = 1+2;
int x4 = ={1+2};
int x5={1+2};
// 数组
int arr1[5] ={1,2,3,4,5};
int arr2[]={1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]={1,2,3,4,5};
// 标准容器
vector<int> v={1,2,3,4,5};//这种初始化就很友好,不用push_back一个一个插入
map<int, int> m={{1,1}, {2,2,},{3,3},{4,4}};

C++11对容器也可以使用列表初始化,这到底是如何做到的呢?

vector (initializer_1ist<value_type> il);
vector<int> v1 = { 1,2,3,4,5,6 };
map<int, int> m={{1,1}, {2,2,},{3,3},{4,4}};

{}的常量数组的类型被C++解释为初始化列表initializer_list。它的底层就是用常量区数组存储列表中的内容。

在这里插入图片描述

template<class T> 
class initializer_list;

auto il = { 10, 20, 30 }; 
initializer_list<int> il = { 10, 20, 30 }; 
cout << typeid(i1).name() << endl;//initializer_list

initializer_list

如何为自定义类型实现列表初始化呢?

initializer_list类型支持beginend接口。

  1. 自定义类型中添加初始化列表构造函数
  2. 在其中用初始化列表的迭代器调用迭代器区间构造函数。
template<class T>
class Vector {
public:
    typedef T* Iterator;

    Vector(std::initializer_list<T> list) {
        _start = new T[list.size()];
        _finish = _start + list.size();
        _endOfStorage = _start + list.size();
        Iterator vt= _start;
        typename std::initializer_list<T>::iterator lt = list.begin();
        while (lt != list.end()) {
            *vt++ = *lt++;
        }
        /*
        for(auto e:list)
        {
        	*vt++=e;
        }
        */
    }

    Vector<T>& operator=(std::initializer_list<T> list) {
        Vector<T> temp(list);
        std::swap(_start, temp._start);
        std::swap(_finish, temp._finish);
        std::swap(_endOfStorage, temp._endOfStorage);
        return *this;
    }

private:
    T* _start;
    T* _finish;
    T* _endOfStorage;
};7

 

2. 变量类型推导

2.1 auto

C++11定义变量时,auto用于自动类型推导,让编译器自动推导变量的类型,使用auto更加便捷省时。

auto i = 0;
cout << typeid(i).name() << endl;

2.2 decltype

关键字decltype使用表达式的类型声明一个新的变量。

// 使用变量的类型创建新变量
decltype(x) i = 1; 
decltype(x * y) i = 1; 

int(*pfunc1)(int) = &func; // 类型过于复杂,使用decltype获取类型
decltype(func) pfunc2;

 

3. 右值引用和移动语义

右值引用和移动语义是C++11中最重要的更新,在根本上减少拷贝,提升效率。

3.1 左值引用和右值引用

左值右值
  • 左值是一个表达式,如变量名或解引用的指针。一般指表达式结束依然存在的持久对象。
  • 右值是一个表达式,如字面常量、表达式返回值、函数返回值。一般指表达式结束就不存在的临时对象。
左值特点右值特点
左值可以取地址可以赋值右值不可被取地址不可赋值
左值可以出现在赋值符的左右右值只能出现在赋值符的右边

左值就是变量,右值就是常量,不完全对。

可以取地址的就是左值,不可以取地址的就是右值

右值的分类
  • 将亡值:指生命周期即将结束的值,通常是将要被销毁被移动的对象。
  • 纯右值:值返回的临时对象、表达式运算产生的临时对象、字面常量和lambda表达式等。
左右值引用

左值引用就是给左值取别名,右值引用就是给右值取别名。左值引用用&表示,右值引用使用&&表示。

给右值取别名后,该右值会被当作变量存储在内存中,且可以取地址

// 左值引用
int& ra = a; 
int& rp = *p;
const int& rb = b;

// 右值引用
int&& r1 = 10;         // 字面常量
int&& r2 = x + y;      // 表达式运算的临时对象
int&& r3 = func(x, y); // 值返回的临时对象
交叉引用
const int& x1 = 10;       // 左值引用 引用 右值
int*&& x2 = std::move(p); // 右值引用 引用 左值
  • 左值引用不能直接引用右值,但const常引用可以。
  • 右值引用不能直接引用左值,但可以引用std::move后的左值。

总结
左值引用和右值引用都旨在减少拷贝以提高效率,但左值引用是直接减少拷贝,而右值引用则是间接减少拷贝间接减少拷贝,识别出是左值还是右值,若识别出是右值,则不再深拷贝,直接移动拷贝(资源转移),提高效率。左值引用适用于需要保留原对象的情况,而右值引用则适用于不需要保留原对象,或者原对象不再需要时的情况,能够实现资源的有效转移,从而提高代码的效率和性能。

3.2 移动构造和移动赋值

移动构造

左值引用无法解决的问题有两点:局部对象返回,接口传参对象拷贝。

右值引用作参数(纯右值)

编译器可以识别表达式是左值还是右值。因此传入的不同属性的表达式会进入不同的构造函数。

移动构造就是单独拎出右值的情况来优化,具体如何进行资源转移还取决于代码。

移动构造减少拷贝的前提是编译器支持识别右值,这便是右值引用的意义。

//拷贝构造
string(const string& s)
    : _size(s._size)
    , _capacity(s._capacity)
{
    _str = new char[_capacity + 1];
    strcpy(_str, s._str);
}
//移动构造
string(string&& s)
    : _size(0)
    , _capacity(0)
{
    swap(s); // 资源转移
}

string ret1 = s1;      // 拷贝构造
string ret2 = s1 + s2; // 移动构造

**move()**也就是将左值强制转化为右值引用,然后直接转移其资源。

在这里插入图片描述

右值引用作返回值(将亡值)

什么是将亡值?

在C++11中,将亡值是与右值引用密切相关的新概念。将亡值表达式包括:

  1. 返回右值引用的函数调用表达式
  2. 转换为右值引用的转换函数的调用表达式

当一个右值准备完成初始化或赋值任务时,它已经“将亡”,这就是将亡值的含义。它常用于移动构造或移动赋值的特殊任务,扮演着资源转移的角色。

将亡值的销毁过程
在这里插入图片描述
在这里插入图片描述

值返回函数会构造出一个即将销毁的临时对象用来返回,编译器会将临时对象视为将亡值,会调用移动构造来构造对象。

string func()
{
    string s("hello");
    return s;
}

int main()
{
	string ret = func();
    return 0;
}
  • 如果编译器完全不做优化,上述代码应该有两次拷贝:

    • 使用s拷贝构造出一个临时对象tmp以供返回。
    • 使用临时对象tmp拷贝构造ret以接收返回值。
  • 一般C++98编译器,会将连续两次的拷贝构造,优化成一次:

    • 直接用s拷贝构造ret
  • C++11编译器支持移动构造后,做到一般优化:

    • 栈变量s是左值,拷贝构造出临时对象tmp
    • 临时对象tmp是将亡值,再调用移动构造,转移tmp资源到ret中。
  • C++11编译器支持移动构造后,做到最大优化:

    • 栈变量s会被识别成将亡值,直接调用移动构造,转移s的资源到ret中。

移动构造和拷贝构造对比图
在这里插入图片描述

移动赋值

类默认成员函数新增一个移动赋值,移动赋值的参数是对象右值引用。当用右值对象赋值给其他对象时,会调用移动赋值。

// 移动赋值
string& operator=(string&& s)
{
   swap(s);
   return *this;
}

用已经存在的对象接受值返回函数返回时:

  1. 先用将亡值对象s构移动构造出一个临时对象tmp
  2. 再用临时对象tmp移动赋值给这个已经存在的对象ret

只有连续的构造可以合二为一,其他不行。

C++11后STL所有容器也新增了移动构造和移动赋值,以及插入接口也新增了右值引用版本。

3.4 万能引用和完美转发

万能引用
template <class T>
void PerfectForward(T&& t) /* 万能引用 */
{}

&&放在具体类型的后面代表右值引用,放在模版类型后面叫做万能引用或引用折叠。

万能引用既能接收左值也能接收右值

完美转发

右值本身是占据空间的,右值引用后会变成左值。因为我们需要能够修改它,转移它的资源。

也就是说,右值引用做参数时会丢失右值属性。如果要维持属性,需要传参时使用完美转发std::forward()

void Func(int& x)        { cout << "左值引用"      << endl; }
void Func(const int& x)  { cout << "const左值引用" << endl; }
void Func(int&& x)       { cout << "右值引用"      << endl; }
void Func(const int&& x) { cout << "const右值引用" << endl; }

template <class T>
void ImperfectForward(T&& t) {
    Func(t);
}
template <class T>
void PerfectForward(T&& t) {
    Func(t);
    //Func(std::forward<T>(t)); /* 完美转发 */
}

int main() 
{
	PerfectForward(10);           // 右值

	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值
	
}

运行一下,跟实际情况不一样
在这里插入图片描述

使用完美转发,保留变量值属性

Func(std::forward<T>(t)); /* 完美转发 */

在这里插入图片描述

为什么会这样呢?

写一个函数 ,无论传过来的参数为左值还是右值,都可以接受 (将左值move后,返回值为右值)

当左值作为参数 时, 会发生引用折叠,调用 fun(t),此时t作为左值,所以会输出 左值引用

在这里插入图片描述

当右值作为参数时,实际上右值接收后,要进行移动拷贝,右值引用 引用后属性会变成左值,否则无法进行资源转移

库或者自行实现的各种容器的右值插入也要支持完美转发。

void push_back(T&& x)
{
    insert(end(), std::forward<T>(x));
}

iterator insert(iterator pos, T&& x)
{
    list_node* prev     = pos._node->_prev;
    list_node* next     = pos._node;
    list_node* new_node = new list_node(std::forward<T>(x));

    new_node->_prev = prev;
    prev->_next = new_node;
    new_node->_next = next;
    next->_prev = new_node;
    return iterator(new_node);
}

 __list_node<T>(T&& t)
    : _data(std::forward<T>(t))
{}

4 默认成员函数

4.1 默认成员函数控制

任何事物的出现都必然有着其出现的理由,伴随着每一个新的概念产生都会带来一系列的便利和价值。C++在不断的演变与发展,与此同时,伴随着许多新的特性和功能产生。=default、=delete 是C++11的新特性,分别为:显式缺省(告知编译器生成函数默认的缺省版本)和显式删除(告知编译器不生成函数默认的缺省版本)

拷贝构造也是构造,如果只实现拷贝构造,编译器也是不会生成默认构造的。

  • 在默认构造函数声明后加=default,可以指示编译器生成该函数的默认版本。
  • 相反,加上=delete可以避免生成该函数的默认版本。
class A
{
public:
    A() = default;
    A(const A& a);
    A operator=(const A& a) = delete;
private:
    // ...
};

C++98没有这样的关键字,那就必须将构造函数至声明不实现并私有化,能防止类外使用和类外实现。

4.2 新增默认成员函数

C++98有六个默认成员函数:构造函数、拷贝构造、拷贝赋值、析构函数以及取地址符重载。C++11新增两个:移动构造、移动赋值。

移动构造的特性
  • 没有实现移动构造,且没有实现析构函数、拷贝构造和拷贝赋值,那编译器会生成默认移动构造。
  • 默认生成的移动构造,对内置类型会逐字节拷贝,对自定义类型如果内部有移动构造就调移动构造,没有就调拷贝构造。
移动赋值的特性
  • 如果没有实现移动赋值,且没有实现析构函数、拷贝构造和拷贝赋值,那编译器会生成默认移动赋值。
  • 默认生成的移动赋值,对内置类型会逐字节拷贝,对自定义类型如果内部有移动赋值就调移动赋值,没有就调拷贝赋值。

默认移动构造和移动赋值的生成规则和成员处理规则一致。

//test
class Person {
public:
    Person(const char* name = "", int age = 18) : _name(name), _age(age)
    {}
    // #define kb 1
#ifdef kb
    Person(const Person& p) : _name(p._name), _age(p._age)
    {}
    Person operator=(const Person& p) {
        if (this == &p) {
            Person tmp(p);
            return tmp;
        }
        return *this;
    }
    ~Person() {}
#endif
private:
    test::string _name;
    int _age;
};
int main()
{
    Person p1("hello", 18);
    Person p2 = std::move(p1); // 移动构造
    p1 = std::move(p2);        // 移动赋值
}
  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuhyOvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值