C++11新特性

C++11新特性

1统一列表初始化

1.1{}

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,**使用初始化列表时,可添加等号(=),也可不添加。**创建对象时也可以使用列表初始化方式调用构造函数初始化。主要是为了new的时候能够初始化。

C++98C++11
int x1 = 1;int x1 {1};//可以这样写但是不建议
int array1[] = { 1, 2, 3, 4, 5 };int array1[]{ 1, 2, 3, 4, 5 };//可以这样写但是不建议
int array2[5] = { 0 };int array2[5]{ 0 };//可以这样写但是不建议
struct Point { int _x; int _y;}; //无构造函数 Point* p = new Point[2];//不支持new的时候初始化struct Point { int _x; int _y;}; Point* p = new Point[2]{{1,2}, {3,4}};
class Point { private:int _x; int _y; Point(int x, int y) :_x(x), _y(y){} }; //有构造函数 Point p(1, 2);class Point { private:int _x; int _y;}; Point p{ 1, 2 };//可以这样写但是不建议
int* p1 = new int[4];//不支持new的时候初始化int* p1 = new int[4]{1,1,2,2};

1.2std::initializer_list

自定义类型用{}去初始化,其实是去匹配对应的构造函数,构造函数支持传几个参数在{}里就只能传几个参数!

而stl库里的容器比如vector、list、deque、forward_list等在使用{}初始化时,可以传入多个参数(不指定个数)。怎么做到的呢?C++11中vector和list的构造函数支持了使用initializer_list初始化。

据我们模拟实现vector的构造函数时可知,没有写那么多个区分参数个数的构造函数。是因为在C++11中把{}单独封装成了std::initializer_list这个类型,类似于vector容器,只不过vector是自己开辟空间存储数据,而{}自己不用存,到常量区找数据就行。【{}跟常量字符串一样,其内容存放在常量区,然后initializer_list这个类型里有2个指针分别指向常量区的开始地址和结束地址。】

vector(std::initializer_list<T> il)
:_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
    reserve(il.size()); //提前开辟空间
    auto it = il.begin();
    while (it != il.end())
    {
        push_back(*it);
        ++it;
    }
}

vector<T>& operator=(std::initializer_list<T> l) {
    vector<T> tmp(l);
    std::swap(_start, tmp._start);
    std::swap(_finish, tmp._finish);
    std::swap(_endofstorage, tmp._endofstorage);
    return *this;
  }

2多种简化声明

2.1auto

用auto修饰的变量必须进行显示初始化,方便让编译器将定义对象的类型设置为初始化值的类型。

2.2decltype

declare声明、断言。关键字decltype将变量的类型声明为表达式指定的类型。

typeid(a).name()可以取到变量的类型名,但只能用于输出打印;而typeid(a) ret;不仅可以拿到变量的类型名,还能用来定义一个对象。

2.3nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

3范围for循环

底层就是依靠编译器做了替换,就是迭代器的begin、end和++就能完成。

4stl中的一些变化

  • 新容器:array、forward_list、unordered_map、unorder_set
  • 已有容器的新接口:移动构造、移动赋值、emplace_xxx插入接口、右值引用版本的插入接口

归个类:

  • 序列式容器:vector、list、deque、array,deque适合头尾插入删除多的场景,vector适合尾插尾删多的场景,list适合随机插入和删除的场景。将array看作固定大小的静态数组,检查越界访问更严格,普通数组的越界是抽查。
  • 关联式容器:map、multimap、set、multiset、bitset、unordered_map、unordered_multimap、unorder_set、unordered_multiset
  • 容器适配器:queue、stack、priority_queue

5final与override

final修饰类,表示不允许被继承,用来修饰虚函数,表示不允许被重写。override修饰虚函数,检查子类虚函数是否完成重写

6引用

引用是为了在函数传参函数传返回值的时候减少拷贝,提高效率。

6.1左值&左值引用

左值是一个数据的表达式(如变量名解引用的指针),我们可以获取它的地址,还可以给它赋值。左值可以出现在赋值符号的左边和右边。最重要的区分点是左值可以取地址。比如定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

  1. 左值引用只能引用左值,不能引用右值;
  2. 但是const左值引用既可引用左值,也可引用右值。
const左值引用

const左值引用既可引用左值,也可引用右值。

左值引用的短板

左值引用解决了函数传参的拷贝问题,且在函数的左值引用参数前加上const,就可以同时接收左值和右值。如 void fun(const int& x);但并没有解决传返回值的问题,因为一个函数栈帧里面的局部变量出栈帧就销毁了,无法用引用返回,如int& fun(const int& x);==>这就是左值引用尚未解决的问题。想一下杨辉三角vecot<vector<int>>这样的返回值,在传返回值时会有2次拷贝构造,消耗很大,在没有右值引用之前的解决方法之一是用输出型参数带回数据。如果这个临时变量比较小,4字节这样,就会存放在寄存器里;如果临时变量比较大,寄存器放不下,就会压在上一个栈帧的边缘处(这样的话,就算当前栈帧销毁了也不影响数据的保留)。新一点的编译器只需要一次拷贝构造,直接拿着要销毁的栈帧里的返回值去构造接收该值的变量。

6.2右值&右值引用

右值也是一个数据的表达式,如:字面常量表达式返回值函数返回值(这个不能是左值引用返回)等等。右值不可以取地址。

右值又可以分为纯右值(内置类型表达式的值)和将亡值(自定义类型表达式的值)。

  1. 右值引用只能右值,不能引用左值;
  2. 但是右值引用可以move以后的左值。

**无论左值引用还是右值引用,都是给对象取别名。**左值可以出现赋值符号的左边,右值只能在赋值符号的右边,不能出现在赋值符号左边。

在这里插入图片描述

const右值引用

注意右值是不能取地址的,但是给右值取别名后(即右值引用后),会导致右值被存储到特定位置(比如字面量10本来是在常量区/已初始化数据区,但是会被拷贝一份放到某个区域,比如栈上),且可以取到该位置的地址。虽然局部变量出函数栈帧就销毁了,但是在用右值引用接收函数返回值时,该函数返回值会被存储到特定的地方。

  • int&& rr1 = 10; &rr1; 不能取字面量10的地址,但是rr1对其进行右值引用后,就可以对rr1取地址,也可以修改rr1;==>移动构造和移动赋值函数可以swap的原因
  • const int rr2&& 10; &rr2;此时可以对字面量10取地址,但不能对rr2做修改。

注意!!!右值引用再进行传递,属性就变成左值了!

7新增2个默认成员函数

原来C++类中,有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载。取地址相对而言没那么重要。

C++11 新增了两个:移动构造函数和移动赋值运算符重载

如果用户自己写了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。==>说明如果写了移动构造,就得写拷贝构造。

  • 无拷贝构造,有移动构造==>编译器不生成默认拷贝构造
  • 无移动构造函数、析构函数 、拷贝构造、拷贝赋值重载==>编译器必生成默认移动构造
  • 无移动赋值重载函数、析构函数 、拷贝构造、拷贝赋值重载==>编译器必生成默认移动赋值运算符重载

默认生成的移动构造函数/移动赋值运算符重载函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动构造/移动赋值运算符重载函数,如果实现了就调用移动构造/移动赋值运算符重载函数,没有实现就调用拷贝构造/拷贝赋值

有默认拷贝构造函数、无默认移动构造函数的情况下,如果有右值,就只能走拷贝构造,效率上比较吃亏。

7.1移动构造

如果用户没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动构造。

默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造

// 拷贝构造
string(const string& s) :_str(nullptr) {
    std::cout << "string(const string& s) -- 深拷贝" << std::endl;
    string tmp(s._str);
    swap(tmp);
}
// 移动构造
string(string&& s) {
    std::cout << "string(string&& s) -- 移动构造" << std::endl;
    swap(s);//右值是将亡值,没必要做深拷贝
}
  • 在没有右值引用前 ,本来是两次拷贝构造,优化成一次拷贝构造;
  • 有了右值引用后,本来是拷贝构造+移动构造,优化成一次移动构造。

对于传值返回的函数,要传出的值是出了作用域就要销毁掉的,编译器就会把它move成右值,调用移动构造。对于需要深拷贝的自定义类型就不用担心传值返回了

7.2移动赋值

如果用户没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动赋值。

默认生成的移动赋值运算符重载函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。

// 赋值重载
string& operator=(const string& s)		{
    std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;
    string tmp(s);
    swap(tmp);
    return *this;
}
// 移动赋值
string& operator=(string&& s)//右值是将亡值{
    std::cout << "string& operator=(string&& s) -- 移动语义" << std::endl;
	swap(s);//右值是将亡值,没必要做深拷贝
	return *this;
}

右值引用单独使用没效果,要搭配移动构造和移动赋值使用,能大大降低拷贝的次数。

右值引用和左值引用减少拷贝的原理不一样,左值引用是取别名,直接起作用;而右值引用是间接起作用,实现移动构造和移动赋值,在构造和赋值的场景下,如果是右值或出作用域就要销毁的左值,编译器可以优化识别为右值,就转移资源。

总结右值引用的好处:右值引用的价值之一:就是补齐这个最后一块短板,传值返回的拷贝问题;右值引用的价值之二:对于插入一些插入右值数据,也可以减少拷贝。

7.3强制生成默认函数的关键字default

C++11为了让用户更好地控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成,就可以利用该关键字强制生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

Person(const Person& p) :_name(p._name) ,_age(p._age) {}
Person(Person&& p) = default;//强制生成移动构造

7.4禁止生成默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,做法是将该函数设置成private,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

Person(Person&& p) = delete;//不生成移动构造

8万能引用/引用折叠

通过改造list的代码可知,push_back()增加一个右值引用,就要写两个版本,一个const左值引用版本,一个右值引用版本,才可以实现右值引用。

在这里插入图片描述

若某些普通函数不想区分左值和右值,但是传参的时候又有左值和右值混用,那必然要写两个版本的函数,这挺麻烦的。比如下面这个情况

void fun1(const int& x);
void fun1(int&& x);
//int x = 1; fun1(x); --> 调用左值引用版本
//fun1(10); --> 调用右值引用版本

所以在C++11中又扩展了模板的功能,引入万能引用。注意,万能模板只能在未被实例化的时候才能用!

template<class T>
void fun2(T&& t);

这样既能引用左值、const左值,也能引用右值、const右值。

9完美转发

基于将万能引用作为中转站+右值再次传递就会变成左值这个问题引入的解决措施。就是保持右值属性再次传递。

void fun(int& x);
void fun(const int& x);
void fun(int&& x);
void fun(const int&& x);

template<class T>
void fun2(T&& t)
{
    fun(std::forward<T>(t));
}

10lambda表达式

仿函数的出现是为了取代函数指针,想一下当某个对象有很多的属性(int price; int evaluate; string name;等),我们要对每个属性挨个进行排序的时候,就要针对每个属性写一个对应的用于比较大小的仿函数,这就比较麻烦。

lambda表达式实际是一个可调用对象,是没有具体类型的(编译器在实现的时候会加上一串随机字符串确保每个lambda表达式的唯一性<lambda xxxxxxxxx>),用auto即可,其底层就是仿函数。

10.1书写格式

书写格式为:[capture-list] (parameters) mutable -> return-type { statement
}

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。必写
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。有就写,没有可以不写
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。一般不需要
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。一般不写,可以自动推导返回类型
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。必写

注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为[]{}; ,不过该lambda函数不能做任何事情。

auto compare = [](int x, int y) ->bool { return x > y; };//实现比较功能
//后续调用
compare(1, 2);

10.2捕捉列表

捕捉列表的写法:传值捕捉、引用捕捉、混合捕捉。

捕捉列表描述了哪些数据可以被lambda使用,以及使用的方式传值还是传引用。注意!捕捉不是传参!

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针
//-------------使用捕捉列表传递参数!这里底层是传值捕捉,只不过用const修饰了,称为复制捕捉
int a = 0, b = 1;
auto compare = [b](int x) ->bool { return x > b; };
compare(a);
//-------------使用捕捉列表传递参数!引用捕捉
int a = 0, b = 1;
auto swap = [&a, &b]() { int tmp = a; a = b; b = tmp; };//这个写法像取地址,实际上是引用捕捉
swap(a, b);

要注意lambda表达式的引用捕捉的写法!

  1. 父作用域指包含lambda函数的语句块,只能捕捉父作用域lambda表达式之前的变量
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]表示值传递方式捕捉变量a和this,引用方式捕捉其他变量;
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]中=已经以值传递方式捕捉了所有变量,捕捉a重复;
  4. 在块作用域以外的lambda函数捕捉列表必须为空;
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错;
  6. lambda表达式之间不能相互赋值,即使看起来类型相同

10.3线程与lambda表达式结合使用

a.让子线程执行从0-100的打印工作

int i = 0;
thread t1([&i]
	{
		for(; i < 100; ++i) { cout << "thread1: " << i << endl;}
	});

b.创建100个线程

vector<thread> vThreads;
int n;
cin >> n;
vThreads.resize(n);

int i = 0;
int num = 0;//标记线程编号
for (auto& t : vThreads)
{
    t = thread([&i, num]//这里用的是移动赋值
               {
                   for (; i < 100; ++i)//让每个线程打印1-100
                   {
                       cout << "thread: " << num << "->" << i << endl;
                   }
               });
    ++num;
}

for (auto& t : vThreads)
{
    t.join();
}

10.4函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。从使用方式上来看,函数对象与lambda表达式完全一样。实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

改造list

1、添加右值引用和移动语义

修改前

list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x)	{}//构造函数
void push_back(T x) { insert(end(), x); }
iterator insert(iterator pos, const T& x)
{
    node* newnode = new node(x);
    node* cur = pos._pnode;
    node* prev = cur->_prev;
    //链接prev newnode cur
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;

    ++_size;

    return iterator(newnode);
}

修改后

list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x)	{}//构造函数
list_node(T&& x) :_next(nullptr) ,_prev(nullptr) ,_data(move(x))	{}//移动构造//修改属性
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x)  { insert(end(), move(x); }//虽然接收右值,但在该函数内x是左值,所以要move一下
iterator insert(iterator pos, const T& x);
iterator insert(iterator pos, T&& x)
{
    node* newnode = new node(move(x);//修改属性
    node* cur = pos._pnode;
    node* prev = cur->_prev;
    //链接prev newnode cur
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;

    ++_size;

    return iterator(newnode);
}

注意:用右值参数接收的右值,性质会改变,在该函数作用域内变成左值了【右值引用本身是左值】,想让它保持右值属性,一定要move一下。

2、万能引用+完美转发

修改如下,只需要把强制转换成右值的move替换为std::forward<T>

list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x)	{}//构造函数
list_node(T&& x) :_next(nullptr) ,_prev(nullptr) ,_data(std::forward<T>(x))	{}//把强制转右值改成完美转发
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x)  { insert(end(), std::forward<T>(x); }//把强制转右值改成完美转发
iterator insert(iterator pos, const T& x);
iterator insert(iterator pos, T&& x)
{
    node* newnode = new node(std::forward<T>(x);//把强制转右值改成完美转发
    node* cur = pos._pnode;
    node* prev = cur->_prev;
    //链接prev newnode cur
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;

    ++_size;

    return iterator(newnode);
}

注意:这里不能把左值引用版本的函数删去。因为我们已经实例化了list,模板参数已经确定了,万能引用就起不了作用。

补充

1、不想让某个类对象被拷贝的做法

知识基础:1、将拷贝构造函数设置为私有==>防外部调用。存在的缺陷:无法阻止类内某函数内部去访问拷贝构造函数,在运行的时候才报错;2、只声明不实现==>链接找不到,编译出错

C++98做法:只声明为私有,不实现。

C++11做法:加一个禁止生成默认函数的关键字delete。

  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值