C++11详解

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11详解相关的知识,希望对你有一定的参考价值。

C++11

文章目录

C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

列表初始化

C++98中的初始化问题

在C++98中,标准允许使用花括号对数组元素进行统一的列表初始值设定。比如:

int a[] = 1,2,3,4;
int b[5] = 0;

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v1,2,3,4,5;

这样在C++98中无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

C++11内置类型的列表初始化

int main()

    // 内置类型变量
    int x1 = 10;
    int x210;
    int x3 = 1+2;
    int x4 = 1+2;
    int x51+2;
    // 数组
    int a[5] 1,2,3,4,5;
    int b[]1,2,3,4,5;
    // 动态数组,在C++98中不支持
    int* p1 = new int[5]1,2,3,4,5;
    // 标准容器
    vector<int> v1,2,3,4,5;
    map<int, int> m1,1, 2,2,,3,3,4,4;
    return 0;	

总结:

C++11里面扩展了初始化使用,基本都可以使用他来初始化,建议还是按旧的用法使用,一般new[]建议这样来初始化

自定义类型的列表初始化

  1. 标准库支持单个对象的列表初始化
class A

public:
	A(int x = 0, int y = 0): _x(x), _y(y)
	
private:
    int _x;
    int _y;
;
int main()

    A a1,2;
    return 0;

自定义类型对象可以使用初始化,必须要有对应参数类型和个数的构造函数,因为用初始化,会调用对应的构造函数。

STL当中的列表初始化

多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即
可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器
以及获取区间中元素个数的方法size()。

#include<map>
#include<vector>
#include<list>
int main()

    vector<int> v1 = 1,2,3,4,5;
    vector<int> v11,2,3,4,5;//可以省略=号
    
    auto lt1 = 1,2,3,4;//lt1的类型为initializer_list<int>
    //所以使用1,2,3,4初始化本质上调用了initializer_list作为参数的构造函数
    initializer_list<int> lt1 = 1,2,3,4;
    
    map<string,int> dict = pair<string,int>("sort",1),pair<string,int>("sort",1);
    map<string,int> dict = "sort",1,"sort",1;
    
    return 0;

C++11用初始化好像是万能的,一个自定义类型调用初始化,本质是调用对应的构造函数

模拟实现vector当中的initializer_list作为参数的构造函数:

vector(initializer_list<T> lt)
    :_start(nullptr)
    ,_finish(nullptr)
    ,_endofstorage(nullptr)

    typename initializer_list<T>::iterator it = lt.begin();
    auto it = it.begin();
    while(it != lt.end())
    
        push_back(*it);
        ++it;
    

本质上下面这种初始化是调用了一个initializer_list作为参数的构造函数来初始化:

vector<int> v1 = 1,2,3,4,5;

总结:

  • 自定义类型对象可以使用初始化,必须要有对应参数类型和个数的构造函数,因为用初始化,会调用对应的构造函数
  • STL容器支持初始化,容器里面有支持一个initializer_list作为参数的构造函数

变量类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂

auto

int main()

    int i = 0;
    auto p = &i;//只能用来推导类型
    auto pf = strcpy;
    cout<<typeid(p).name()<<typeid(pf).name()<<endl;
    return 0;

auto自动推导对象类型:

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型

decltype和auto的区别:

int main()

    int i = 0;
    auto p = &i;//只能用来推导类型
    auto pf = strcpy;
    decltype(pf) pf1;//可以作为推导类型来创建对象或者变量
    cout<<typeid(pf).name()<<endl<<typeid(pf1).name()<<endl;
    return 0;

可以作为推导类型来创建对象或者变量

STL当中的变化

array

固定大小的数组容器

下面这两个有什么区别呢?

#include<array>
int main()

    array<int,10> a1;
    int a2[10];
    return 0;

这两基本没有什么区别,唯一最大的区别是:

a1[13] = 1;
a2[13] = 1;

a1只要越界一定报错,而a2只要越界不一定报错,[]对越界检查使用了assert,更严格安全,还有就是a1是有迭代器的,C++11增加这个感觉没什么用

forward_list

单链表,这个容器里面没有尾插尾删,因为尾插尾删效率低,并且实现了insert_after,C++11增加这个属实没感觉到有什么用

unordered_map和unordered_set

对于unordered_map和unordered_set的添加还是让人高兴的,它们的底层使用哈希表实现的,它们的介绍请前往博主的C++专栏阅读

针对旧容器,基本都增加了移动构造,移动赋值,所有插入数据接口函数,都增加右值引用版本这些接口都是用来提高效率,下面我们来看一下什么是右值引用:

右值引用和移动语义

左值和右值

在说右值引用之前,我们得先谈谈什么是左值,什么是右值,可能大家理解的左值和右值的概念是赋值号左边的就是左值,赋值号右边的就是右值,其实不然,左值也可能出现在右边,比如:

int main()

    int a = 10;
    a = 20;
    int b = 10;
    b = a;

这里的a是左值,b也是左值,b = a,发现左值也可以出现在赋值号右边,所以这些说法是不正确的,正确的说法应该是:左值可以获取地址,而右值不能获取地址。

左值都是可以获取地址,左值基本都可以出现在赋值符号的左边,可以修改,但是const修饰的左值,只能获取地址,不能赋值,右值不能出现赋值符号的左边,也就是不能修改右值,不能取地址

左值引用是给左值取别名,左值引用不能引用右值,const左值引用可以引用左值,也可以引用右值:

int main()

    int a = 10;
    int& rt = a;
    //int& rt = 10;//error,左值引用不能引用右值
    const int& rt = 10;//const左值引用可以引用右值
    return 0;

当形参是const修饰的左值引用时,实参既可以是左值也可以是右值:

void push_back(const T& x)

右值匹配了右值引用,左值匹配了左值引用,需要注意的是,我们说当形参是const修饰的左值引用时,实参既可以是左值也可以是右值,有人说那这里为什么f(1)不匹配第一个函数呢?是因为编译器会去找最匹配的。

右值引用只能引用右值不能引用左值:

int a = 10;
int&& r2 = a;//error

右值引用可以引用move以后的左值:

int&& r3 = std::move(a);

右值引用作为形参时,实参只能传右值:

void push_back(T&& x)

右值引用不可以连续引用:

int&& r1 = 10;
int&& r4 = r1;//error,右值引用只能引用右值,不能引用左值

右值引用可以引用move后的左值:

int&& r4 = std::move(r1);

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。
  5. 右值引用可以引用move后的左值

总结:

  • 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断
  • 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

  • C语言中的纯右值,比如:a+b, 100
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

左值引用和右值引用

左值引用的使用场景

在传参时:

左值引用的场景,引用传参可以减少拷贝

在引用返回时:

左值引用,引用返回可以减少拷贝,但是效果不明显,不使用引用返回,编译器会优化一次拷贝,使用引用返回,编译器不会优化

本质上引用都是用来减少拷贝,提高效率

1、左值引用解决大部分的场景(做参数,做返回值)

2、右值引用是左值引用一些盲区的补充

比如我们写一个to_string函数来讲解(这是一个整形转字符串的函数):

string to_string(int val)

    string str;
    while(val)
    
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    
    reverse(str.begin(),str.end());//逆置
    return str;

int main()

    string s = to string(1234);
    cout<<s.c_str()<<endl;
    return 0;

此时的to_string函数只能值返回,如果是引用返回就会出问题,因为临时对象出了作用域就销毁了,相当于s成了野指针,我们只能传值返回,传值返回会有一次拷贝构造,本来是str去拷贝构造临时对象,然后临时对象再去拷贝构造s,但是编译器做了优化,直接用str去拷贝构造s,这个临时对象小的话放在寄存器,大的话放在调用它的函数的栈帧当中。还记不记得前面说的将亡值:函数按照值的方式进行返回时产生的临时对象就是将亡值

当这个变量或者对象出了作用域在,这种场景就可以用左值引用,在这里我们可以硬用左值引用返回,将里面的string对象写成静态的就可以引用返回了,因为这个变量出了作用域还在:

string& to_string(int val)

    //线程安全问题
    static string str;
    str.clear();
    while(val)
    
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    
    reverse(str.begin(),str.end());//逆置
    return str;

但是这样写会有多线程安全问题,而且每次进来还需要将str清理一次,所以左值引用无法解决局部对象返回,只能传值返回

那么右值引用返回呢?

string&& to_string(int val)

    //线程安全问题
    string str;
    while(val)
    
        int i = val % 10;//取到最低的十进制位
        str += ('0' + i);
        val /= 10;
    
    reverse(str.begin(),str.end());//逆置
    return (move)str;

右值引用并不会改变局部变量的生命周期,返回的也是str的别名,出了作用域str也就销毁了,所以几乎没有使用右值引用返回的场景。

总结:str在按照值返回时,必须创建一个临时对象,临时对象创建好之后,str就被销毁了,最后使用返回的临时对象构造s,s构造好之后,临时对象就被销毁了。仔细观察会发现:str、临时对象、s每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?

下面就用到了右值引用的场景:

右值引用的场景

移动语义

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解在值传递时空间的浪费问题,在C++11中如果需要实现移动语义,必须使用右值引用。string类的移动构造函数

//移动构造
String(String&& s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)

    this->swap(s);

我们自己模拟实现string,下面的场景就用到了右值引用:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
class String

public:
    typedef char* iterator;
    iterator begin()
    
        return _str;
    

    iterator end()
    
        return _str + _size;
    
    String(const char* str = "")
        :_size(strlen(str))
        , _capacity(_size)
    
        //cout << "string(char* str)" << endl;

        _str = new char[_capacity + 1];
        strcpy(_str, str);
    
    //拷贝构造
    String(const String& s)
        :_str(nullptr)
        , _size(0)
        , _capacity(0)
    
        cout << "拷贝构造:String(const String& s)"<< endl;
        String tmp(s._str);
        swap(tmp);
    
    //移动构造
    String(String&& s)
        :_str(nullptr)
        , _size(0)
        , _capacity(0)
    
        cout << "移动构造:String(String && s)" << endl;
        this->swap(s);
    
    // 拷贝赋值
    String& operator=(const String& s)
    
        cout << "String& operator=(const string& s) -- 深拷贝" << endl;
        String tmp(s);
        swap(tmp);

        return *this;
    
    // s1.swap(s2)
    void swap(String& s)
    
        ::swap(_str, s._str);
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);
    
    void reserve(size_t n)
    
        if (n > _capacity)
        
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;

            _capacity = n;
        
    
    void push_back(char ch)
    
        if (_size >= _capacity)
        
            size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
            reserve(newcapacity);
        

        _str[_size] = ch;
        ++_size;
        _str[_size] = '\\0';
    

    String& operator+=(char ch)
    
        push_back(ch);
        return *this;
    
    String to_string(int val安卓和ios应用的机型适配难度有何差别

C++11移动构造函数详解

Acunetix 11 配置详解

[C++11新特性] 智能指针详解

用不同的Xcode版本开发出的ios应用有差别吗?

C语言int和float有啥差别?