C++ 11新特性详解1

简介: C++ 11新特性详解

一、C++11

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(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++11增加的语法特性非常篇幅非常多,这里只介绍实际中比较实用的语法。


二、列表初始化

1. 列表初始化的使用

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

struct Point
{
  int _x;
  int _y;
};
int main()
{
  int array1[] = { 1, 2, 3, 4, 5 };
  int array2[5] = { 0 };
  Point p = { 1, 2 };
  return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

struct Point
{
  int _x;
  int _y;
};
int main()
{
  int x1 = 1;
  int x2{ 2 };
  int array1[]{ 1, 2, 3, 4, 5 };
  int array2[5]{ 0 };
  Point p{ 1, 2 };
  // C++11中列表初始化也可以适用于new表达式中
  int* pa = new int[4]{ 0 };
  return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化。

class Date
{
public:
  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;
};
int main()
{
  Date d1(2022, 1, 1); // old style
  // C++11支持的列表初始化,这里会调用构造函数初始化
  Date d2{ 2022, 1, 2 };
  Date d3 = { 2022, 1, 3 };
  return 0;
}

2. 初始化列表的底层原理

初始化列表的底层套用的是std::initializer_list。std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。std::initializer_list参考文档。

例如:

  • list
  • vector


三、C++11新的声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

1. auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

例如:

int main()
{
  int i = 10;
  auto p = &i;
  auto pf = strcpy;
  cout << typeid(p).name() << endl;
  cout << typeid(pf).name() << endl;
  map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
  //map<string, string>::iterator it = dict.begin();
  auto it = dict.begin();
  return 0;
}

使用auto可以使代码写起来更加的简单、便捷。

2. decltype

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

例如:

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
  decltype(t1 * t2) ret;
  cout << typeid(ret).name() << endl;
}
int main()
{
  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;
  int z = 0;
  decltype(z) tmp;
  cout << typeid(tmp).name() << endl;
  F(1.1, 1.1);
  return 0;
}

3. nullptr

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

#ifndef NULL
#ifdef __cplusplus
#define NULL  0
#else
#define NULL  ((void *)0)
#endif
#endif

四、容器变化

1. 新容器

用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。

unordered_map和unordered_set可见前面的文章:

【C++ unordered_map和unordered_set的使用】

【哈希原理及模拟实现并封装unordered系列关联式容器】


2. 容器中新增方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:


std::vector::emplace_back

std::vector::push_back

std::map::insert

std::map::emplace

使用右值引用的版本可以提高效率,右值引用见下文。

五、右值引用和移动语义

1. 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性。无论左值引用还是右值引用,都是给对象取别名

左值和左值引用:

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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;
}

右值和右值引用:

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

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;
}


需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
  double x = 1.1, y = 2.2;
  int&& rr1 = 10;
  const double&& rr2 = x + y;
  rr1 = 20;
  rr2 = 5.5;  // 报错
  return 0;
}


2. 左值引用和右值引用的比较

  1. 左值引用
  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
  // 左值引用只能引用左值,不能引用右值。
  int a = 10;
  int& ra1 = a;  // ra为a的别名
  //int& ra2 = 10;  // 编译失败,因为10是右值
  // const左值引用既可引用左值,也可引用右值。
  const int& ra3 = 10; // const左值引用, 引用右值
  const int& ra4 = a;  // const左值引用, 引用左值
  return 0;
}
  1. 右值引用
  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
  // 右值引用只能右值,不能引用左值。
  int&& r1 = 10;
  // error C2440: “初始化”: 无法从“int”转换为“int &&”
  // message : 无法将左值绑定到右值引用
  int a = 10;
  int&& r2 = a;
  // 右值引用可以引用move以后的左值
  int&& r3 = std::move(a);
  return 0;
}


3. 右值引用的使用场景及意义

既然左值引用既可以引用左值又可以引用右值,那么为什么C++11还要提出右值引用呢,下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

这里以模拟实现的string为例。

namespace lhf
{
  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);
    }
    // s1.swap(s2)
    void swap(string& s)
    {
      ::swap(_str, s._str);
      ::swap(_size, s._size);
      ::swap(_capacity, s._capacity);
    }
    // 拷贝构造
    string(const string& s)
      :_str(nullptr)
    {
      cout << "string(const string& s) -- 深拷贝" << endl;
      string tmp(s._str);
      swap(tmp);
    }
    // 赋值重载
    string& operator=(const string& s)
    {
      cout << "string& operator=(string s) -- 深拷贝" << endl;
      string tmp(s);
      swap(tmp);
      return *this;
    }
    // 移动构造
    string(string&& s)
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(string&& s) -- 移动语义" << endl;
      swap(s);
    }
    // 移动赋值
    string& operator=(string&& s)
    {
      cout << "string& operator=(string&& s) -- 移动语义" << endl;
      swap(s);
      return *this;
    }
    ~string()
    {
      delete[] _str;
      _str = nullptr;
    }
    char& operator[](size_t pos)
    {
      assert(pos < _size);
      return _str[pos];
    }
    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)
    string& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\0
  };
}

左值引用的使用场景:

做参数和做返回值都可以提高效率。

void func1(lhf::string s)
{}
void func2(const lhf::string& s)
{}
int main()
{
  lhf::string s1("hello world");
  // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
  func1(s1);
  func2(s1);
  // string operator+=(char ch) 传值返回存在深拷贝
  // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
  s1 += '!';
  return 0;
}


左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:

lhf::string to_string(int value) 函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

namespace lhf
{
  lhf::string to_string(int value)
  {
    bool flag = true;
    if (value < 0)
    {
      flag = false;
      value = 0 - value;
    }
    lhf::string str;
    while (value > 0)
    {
      int x = value % 10;
      value /= 10;
      str += ('0' + x);
    }
    if (flag == false)
    {
      str += '-';
    }
    std::reverse(str.begin(), str.end());
    return str;
  }
}
int main()
{
  /*
  在lhf::string to_string(int value)函数中可以看到,这里只能使用传值返回,
  传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
  */
  lhf::string ret1 = lhf::to_string(1234);
  lhf::string ret2 = lhf::to_string(-1234);
  return 0;
}


to_string的返回值是一个右值,用这个右值构造ret2,如果没有移动构造,就会匹配调用string的拷贝构造,因为const左值引用可以调用右值,实现深拷贝。

右值引用和移动语义解决左值引用的短板:

在lhf::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

// 移动构造
string(string&& s)
  :_str(nullptr)
  , _size(0)
  , _capacity(0)
{
  cout << "string(string&& s) -- 移动语义" << endl;
  swap(s);
}
int main()
{
  lhf::string ret1 = lhf::to_string(1234);
  lhf::string ret2 = lhf::to_string(-1234);
  return 0;
}


再运行上面lhf::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

to_string的返回值是一个右值,用这个右值构造ret2,如果既有移动构造又有拷贝构造,因为编译器会选择最匹配的参数调用。那么这里就会调用移动构造,是一个移动语义。

除了移动构造,还有移动赋值:

在lhf::string类中增加移动赋值函数,再去调用lhf::to_string(1234),不过这次是将lhf::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string& operator=(string&& s)
{
  cout << "string& operator=(string&& s) -- 移动语义" << endl;
  swap(s);
  return *this;
}
int main()
{
  lhf::string ret1;
  ret1 = lhf::to_string(1234);
  return 0;
}

运行结果:

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。lhf::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为lhf::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。


C++ 11新特性详解2;https://developer.aliyun.com/article/1383797


目录
相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
123 59
|
17天前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
17 2
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
41 0
|
3月前
|
编译器 C++ 计算机视觉
C++ 11新特性之完美转发
C++ 11新特性之完美转发
54 4
|
3月前
|
Java C# C++
C++ 11新特性之语法甜点1
C++ 11新特性之语法甜点1
34 4
|
3月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
45 3