文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载

1.回顾变量初始化

在介绍列表初始化之前,让我们先回顾一下初始化变量的传统方法。其中常见的是使用括号和等号在变量声明时对其初始化,例如:

struct C {C(int a) {}
};
int main(int argc, char** argv)
{int x = 5;   int x1(8);  C x2 = 4;C x3(4);return 0;
}

一般来说,我们称使用括号初始化的方式叫作直接初始化,而使用等号初始化的方式叫作拷贝初始化(复制初始化)。请注意,这里使用等号对变量初始化并不是调用等号运算符的赋值操作。实际情况是,等号是拷贝初始化,调用的依然是直接初始化对应的构造函数,只不过这里是隐式调用而已。如果我们将C(int a)声明为 explicit,那么C x2 = 4就会编译失败。

使用括号和等号只是直接初始化和拷贝初始化的代表,还有一些经常用到的初始化方式也属于它们。比如new运算符和类构造函数的初始化列表就属于直接初始化,而函数传参和return返回则是拷贝初始化。前者比较好理解,后者可以通过具体的例子来理解:

#include <map>
struct C {C(int a) {}
};
void foo(C c)
{}
C bar()
{return 5;
}
int main(int argc, char** argv)
{foo(8);        // 拷贝初始化   C c = bar();   // 拷贝初始化return 0;
}

这段代码中foo函数的传参和bar函数的返回都调用了隐式构造函数,是一个拷贝初始化。

​​​​​​​2.使用列表初始化

C++11标准引入了列表初始化,它使用大括号{}对变量进行初始化,和传统变量初始化的规则一样,它也区分为直接初始化和拷贝初始化,例如:

#include <string> struct C {C(string a, int b) {}C(int a) {}
};
void foo(C) {}C bar()
{return { "world", 5 };
}int main(int argc, char** argv)
{int x = { 5 };              // 拷贝初始化   int x1{ 8 };                // 直接初始化  C x2 = { 4 };               // 拷贝初始化  C x3{ 2 };                  // 直接初始化  foo({ 8 });                 // 拷贝初始化  foo({ "hello", 8 });        // 拷贝初始化  C x4 = bar();               // 拷贝初始化  C* x5 = new C{ "hi", 42 };  // 直接初始化return 0;
}

仔细观察以上代码会发现,列表初始化和传统的变量初始化几乎相同,除了foo({"hello", 8})和return {"world", 5}这两处不同。读者应该发现了列表初始化在这里的奥妙所在,它支持隐式调用多参数的构造函数,于是{"hello", 8}和{"world", 5}通过隐式调用构造函数C::C(string a, int b)成功构造了类C 的对象。当然了,有时候我们并不希望编译器进行隐式构造,这时候只需要在特定构造函数上声明explicit即可。

讨论使用大括号初始化变量就不得不提用大括号初始化数组,例如int x[] = { 1,2,3,4,5 }。不过遗憾的是,这个特性无法使用到STL的vector、list等容器中。想要初始化容器,我们不得不编写一个循环来完成初始化工作。现在,列表初始化将程序员从这个问题中解放了出来,我们可以使用列表初始化对标准容器进行初始化了,例如:

#include <vector>
#include <list>
#include <set>
#include <map>
#include <string> int main(int argc, char** argv)
{int x[] = { 1,2,3,4,5 };int x1[]{ 1,2,3,4,5 };vector<int> x2{ 1,2,3,4,5 };vector<int> x3 = { 1,2,3,4,5 };list<int> x4{ 1,2,3,4,5 };list<int> x5 = { 1,2,3,4,5 };set<int> x6{ 1,2,3,4,5 };set<int> x7 = { 1,2,3,4,5 };map<string, int> x8{ {"bear",4}, {"cassowary",2},{"tiger",7} };map<string, int> x9 = { {"bear",4}, {"cassowary",2},{"tiger",7} };return 0;
}

以上代码在C++11环境下可以成功编译,可以看到使用列表初始化标准容器和初始化数组一样简单,唯一值得注意的地方是对x8和x9的初始化,因为它使用了列表初始化的一个特殊的特性。关于这个特性先卖一个关子,后面再做解释。让我们先将注意力放在如何能让容器支持列表初始化的问题上。

​​​​​​​3.initializer_list详解

标准容器之所以能够支持列表初始化,离不开编译器支持的同时,它们自己也必须满足一个条件:支持initializer_list为形参的构造函数。

initializer_list简单地说就是一个支持begin、end以及 size成员函数的类模板,有兴趣的读者可以翻阅STL的源代码,然后会发现无论是它的结构还是函数都直截了当。编译器负责将列表里的元素(大括号包含的内容)构造为一个initializer_list的对象,然后寻找标准容器中支持 initializer_list为形参的构造函数并调用它。而标准容器的构造函数的处理就更加简单了,它们只需要调用initializer_list对象的begin和end 函数,在循环中对本对象进行初始化。

通过了解原理能够发现,支持列表初始化并不是标准容器的专利,我们也能写出一个支持列表初始化的类,需要做的只是添加一个以initializer_list为形参的构造函数罢了,比如下面的例子:

#include <iostream>
#include <string> struct C {C(initializer_list<string> a){for (const string* item = a.begin(); item != a.end(); ++item) {cout << *item << " ";}cout << endl;}
};
int main(int argc, char** argv)
{C c{ "hello", "c++", "world" };return 0;
}

上面这段代码实现了一个支持列表初始化的类 C,类 C 的构造函数为C( initializer_list<string> a),这是支持列表初始化所必需的,值得注意的是,initializer_list的begin和end函数并不是返回的迭代器对象,而是一个常量对象指针const T *。本着刨根问底的精神,让我们进一步探究编译器对列表的初始化处理:

#include <iostream>
#include <string> struct C {C(initializer_list<string> a){for (const string* item = a.begin(); item != a.end(); ++item) {cout << item << " ";}cout << endl;}};int main(int argc, char** argv)
{C c{ "hello", "c++", "world" };cout << "sizeof(string) = " << hex << sizeof(string) << endl;return 0;
}

运行输出结果如下:

0x77fdd0 0x77fdf0 0x77fe10 sizeof(string) = 20

以上代码输出了string对象的内存地址以及单个对象的大小(不同编译环境的string实现方式会有所区别,其对象大小也会不同,这里的例子是使用GCC编译的,string对象的大小为0x20)。仔细观察3个内存地址会发现,它们的差别正好是string所占的内存大小。于是我们能推断出,编译器所进行的工作大概是这样的:

const string __a[3] = { string{"hello"}, string{"c++"}, string{"world"} };
C c(initializer_list<string>(__a, __a + 3));

另外,有兴趣的读者不妨用GCC对上面这段代码生成中间代码GIMPLE,不出意外会发现类似这样的中间代码:

main()
{struct initializer_list D.40094;   const struct basic_string D.36430[3];…__cxx11::basic_string<char>::basic_string(&D.36430[0], "hello", &D.36424);…__cxx11::basic_string<char>::basic_string(&D.36430[1], "c++", &D.36426);…__cxx11::basic_string<char>::basic_string(&D.36430[2], "world", &D.36428);…D.40094._M_array = &D.36430;D.40094._M_len = 3;C::C(&c, D.40094);…
}

4.使用列表初始化的注意事项

使用列表初始化是如此的方便,让人不禁想马上运用到自己的代码中去。但是请别着急,这里还有两个地方需要读者注意。

4.1​​​​​​​隐式缩窄转换问题

隐式缩窄转换是在编写代码中稍不留意就会出现的,而且它的出现并不一定会引发错误,甚至有可能连警告都没有,所以有时候容易被人们忽略,比如:

int x = 12345;
char y = x;

这段代码中变量y的初始化明显是一个隐式缩窄转换,这在传统变量初始化中是没有问题的,代码能顺利通过编译。但是如果采用列表初始化,比如char z{ x },根据标准编译器通常会给出一个错误,MSVC和CLang就是这么做的,而GCC有些不同,它只是给出了警告。

现在问题来了,在C++中哪些属于隐式缩窄转换呢?在C++标准里列出了这么4条规则。

1.从浮点类型转换整数类型。

2.从long double转换到double或float,或从double转换到float,除非转换源是常量表达式以及转换后的实际值在目标可以表示的值范围内。

3.从整数类型或非强枚举类型转换到浮点类型,除非转换源是常量表达式,转换后的实际值适合目标类型并且能够将生成目标类型的目标值转换回原始类型的原始值。

4.从整数类型或非强枚举类型转换到不能代表所有原始类型值的整数类型,除非源是一个常量表达式,其值在转换之后能够适合目标类型。 4条规则虽然描述得比较复杂,但是要表达的意思还是很简单的,结合标准的例子就很容易理解了:

int x = 999;
const int y = 999;
const int z = 99;
const double cdb = 99.9;
double db = 99.9;
char c1 = x;      // 编译成功,传统变量初始化支持隐式缩窄转换
char c2{ x };     // 编译失败,可能是隐式缩窄转换,对应规则4
char c3{ y };     // 编译失败,确定是隐式缩窄转换,999超出char能够适应的范围,对应规则4
char c4{ z };     // 编译成功,99在char能够适应的范围内,对应规则4
unsigned char uc1 = { 5 };      // 编译成功,5在unsigned char能够适应的范围内, // 对应规则4
unsigned char uc2 = { -1 };     // 编译失败,unsigned char不能够适应负数,对应规则4
unsigned int ui1 = { -1 };      // 编译失败,unsigned int不能够适应负数,对应规则4
signed int si1 = { (unsigned int)-1 }; // 编译失败,signed int不能够适应-1所对应的 // unsigned int,通常是4294967295,对应规则4
int ii = { 2.0 }; // 编译失败,int不能适应浮点范围,对应规则1
float f1{ x };    // 编译失败,float可能无法适应整数或者互相转换,对应规则3
float f2{ 7 };    // 编译成功,7能够适应float,且float也能转换回整数7,对应规则3
float f3{ cdb };  // 编译成功,99.9能适应float,对应规则2
float f4{ db };   // 编译失败,可能是隐式缩窄转无法表达double,对应规则2

​​​​​​​4.2列表初始化的优先级问题

通过2和3的介绍我们知道,列表初始化既可以支持普通的构造函数,也能够支持以initializer_list为形参的构造函数。如果这两种构造函数同时出现在同一个类里,那么编译器会如何选择构造函数呢?比如:

vector<int> x1(5, 5);
vector<int> x2{ 5, 5 };

以上两种方法都可以对vector<int>进行初始化,但是初始化的结果却是不同的。变量x1的初始化结果是包含5个元素,且5个元素的值都为5,调用了vector(size_type count, const T& value, const Allocator& alloc = Allocator())这个构造函数。而变量x2的初始化结果是包含两个元素,且两个元素的值为5,也就是调用了构造函数vector(initializer_list<T> init, const Allocator& alloc = Allocator() )。所以,上述问题的结论是,如果有一个类同时拥有满足列表初始化的构造函数,且其中一个是以initializer_list为参数,那么编译器将优先以 initializer_ list为参数构造函数。由于这个特性的存在,我们在编写或阅读代码的时候就一定需要注意初始化代码的意图是什么,应该选择哪种方法对变量初始化。

最后让我们回头看一看9.2节中没有解答的一个问题,map< string, int> x8{ {"bear",4}, {"cassowary",2}, {"tiger",7} }中两个层级的列表初始化分别使用了什么构造函数。其实答案已经非常明显了,内层{"bear",4}、{"cassowary",2}和{"tiger",7}都隐式调用了 pair的构造函数pair(const T1& x, const T2& y),而外层的{…}隐式调用的则是map的构造函数map(initializer_list<value_ type>init, const Allocator&)。

​​​​​​​5.指定初始化

为了提高数据成员初始化的可读性和灵活性,C++20标准中引入了指定初始化的特性。该特性允许指定初始化数据成员的名称,从而使代码意图更加明确。让我们看一看示例:

struct Point {int x;  int y;
};Point p{ .x = 4, .y = 2 };

虽然在这段代码中Point的初始化并不如Point p{ 4, 2 }; 方便,但是这个例子却很好地展现了指定初始化语法。实际上,当初始化的结构体的数据成员比较多且真正需要赋值的只有少数成员的时候,这样的指定初始化就非常好用了:

struct Point3D {int x;  int y;int z;
};Point3D p{ .z = 3 };    // x = 0, y = 0

在上面的代码中Point3D需要3个坐标,不过我们只需要设置z的值,指定.z = 3即可。其中x和y坐标会调用默认初始化将其值设置为0。可能这个例子还是不能完全体现出它相对于Point3D p{ 0, 0, 3 }; 的优势所在,不过读者应该能感觉到,一旦结构体更加复杂,指定初始化就一定能带来不少方便之处。

最后需要注意的是,并不是什么对象都能够指定初始化的。

1.它要求对象必须是一个聚合类型,例如下面的结构体就无法使用指定初始化:

struct Point3D {Point3D() {}   int x;  int y;  int z;
};Point3D p{ .z = 3 };    // 编译失败,Point3D不是一个聚合类型

这里读者可能会有疑问,如果不能提供构造函数,那么我们希望数据成员x和y的默认值不为0的时候应该怎么做?不要忘了,从C++11开始我们有了非静态成员变量直接初始化的方法,比如当希望Point3D的默认坐标值都是100时,代码可以修改为:

struct Point3D {int x = 100;  int y = 100;    int z = 100;
};Point3D p{ .z = 3 };    // x = 100, y = 100, z = 3

2.指定的数据成员必须是非静态数据成员。这一点很好理解,静态数据成员不属于某个对象。

3.每个非静态数据成员最多只能初始化一次:

Point p{ .y = 4, .y = 2 };  // 编译失败,y不能初始化多次

4.非静态数据成员的初始化必须按照声明的顺序进行。请注意,

这一点和C语言中指定初始化的要求不同,在C语言中,乱序的指定初始化是合法的,但C++不行。其实这一点也很好理解,因为C++中的数据成员会按照声明的顺序构造,按照顺序指定初始化会让代码更容易阅读:

Point p{ .y = 4, .x = 2 };  // C++编译失败,C编译没问题

5.针对联合体中的数据成员只能初始化一次,不能同时指定:

union u {int a;const char* b;
};
u f = { .a = 1 };                 // 编译成功
u g = { .b = "asdf" };            // 编译成功
u h = { .a = 1, .b = "asdf" };    // 编译失败,同时指定初始化联合体中的多个数据成员

6.不能嵌套指定初始化数据成员。虽然这一点在C语言中也是允许的,但是C++标准认为这个特性很少有用,所以直接禁止了:

struct Line {Point a;  Point b;
};Line l{ .a.y = 5 }; // 编译失败, .a.y = 5访问了嵌套成员,不符合C++标准

当然,如果确实想嵌套指定初始化,我们可以换一种形式来达到目的:

Line l{ .a {.y = 5} };

7.在C++20中,一旦使用指定初始化,就不能混用其他方法对数据成员初始化了,而这一点在C语言中是允许的:

Point p{ .x = 2, 3 };    // 编译失败,混用数据成员的初始化

8.最后再来了解一下指定初始化在C语言中处理数组的能力,当然在C++中这同样是被禁止的:

int arr[3] = { [1] = 5 };    // 编译失败

C++标准中给出的禁止理由非常简单,它的语法和lambda表达式冲突了。

现代C++新特性 列表初始化相关推荐

  1. 【C++】C++11新特性列表

    我们学习的标准是C++98,我们知道计算机的知识更新非常快,本文旨在大致了解C++11的新特性,如果想要仔细了解,请阅读<C++Primer中文版 第五版>本文的页码也是这本书的页码,这里 ...

  2. Effective Modern C++ 第三章第一节,C++新特性:初始化、nullptr、alias别名、enum

    Chapter 3 part 1, Moving to Modern C++ Item 7: Distinguish between () and {} when creating objects 几 ...

  3. mysql 5.7 初始化数据库_MySQL 5.7 新特性之初始化

    1. 把二进制安装包下载放在/opt 目录下并解压 2. 创建软连接, 并添加运行环境 ln -s /usr/local/mysql /opt/mysql-5.7.18-linux-glibc2.5- ...

  4. 侯捷C++八部曲笔记(四、C++2.0新特性)

    侯捷C++八部曲笔记(四.C++2.0新特性) 关键字 noexcept override final decltype =default, =delete nullptr auto explicit ...

  5. JAVA9 新特性 完整使用

    JAVA9 新特性 完整使用 一.介绍(java9新特性) 1.Oracle JDK9 Documentation(java9新特性) 2.官方提供新特性列表(java9新特性) 3.JDK和JRE ...

  6. 开发者所需要知道的iOS7 SDK新特性

    无论是对于开发者或用户来说iOS7都是非常重要的一次更新,iOS7对UI进行了彻底的重新设计.另外为了创建2D和2.5D游戏,iOS7引进了一套全新的动画系统.在iOS7中,多任务的增强.点对点的连接 ...

  7. 尚硅谷Java入门视频教程第十七章——Java9Java10Java11新特性

    尚硅谷Java入门视频教程第十七章--Java9&Java10&Java11新特性 第17章:Java9&Java10&Java11新特性 17.1 Java 9 的新 ...

  8. C# 3.0/3.5语法新特性示例汇总[转]

    //作者:杨卫国 //时间:2008年2月21日 //说明:C#语法新特型示例 using System; using System.Collections.Generic; using System ...

  9. java8新特性_乐字节-Java8新特性-接口默认方法

    总概 JAVA8 已经发布很久,而且毫无疑问,java8是自java5(2004年发布)之后的最重要的版本.其中包括语言.编译器.库.工具和JVM等诸多方面的新特性. Java8 新特性列表如下: 接 ...

最新文章

  1. CentOS中用top命令CPU负载
  2. 计算机及其系统的泄密渠道之三
  3. python自带ide和pycharm哪个好_排名前三的Python IDE你选择哪个?我选PyCharm
  4. HTML div元素
  5. 小米公布Q1手机出货量:驳斥暴跌谣言
  6. ELKF(Elasticsearch+Logstash+ Kibana+ Filebeat) 部署
  7. 文件上传到部署服务器(添加附件)
  8. 如何在淘宝上利用信息差赚钱
  9. 校园网IPv6免流量上网
  10. dtim 间隔(Delivery Traffic Indication Message)
  11. 孙陶然:昆仑36条创业军规
  12. mysql怎么截取时分秒_mysql获取表中日期的年月日时分秒
  13. 每一题-101(患某种疾病的患者)
  14. 2007年中国地方门户网站市场规模达6.1亿元
  15. centos搭建微信代理服务器 docker
  16. 正常人白手起家挣一千万需要多久?
  17. 华为服务器不做阵列怎么进系统,服务器不做阵列能装系统
  18. Maven 虐我千百遍,我待 Maven 如初恋
  19. DIY TCP/IP IP模块和ICMP模块的实现2
  20. XENU常见问题及中文版英文版下载地址

热门文章

  1. Ubuntu 两步安装 Teamviewer 最新版本(官方方法)
  2. WPF入门0:WPF的基础知识
  3. 卡通渲染技巧(一)——漫反射部分
  4. 前端随录(SPA与MPA和PWA)
  5. SQL注入基础--判断闭合形式
  6. css html 鼠标手型,鼠标形状,鼠标效果,样式
  7. mima.php密码找回,mima.php
  8. Office2019 Office2016 Office2010 Office365 系列各版本下载
  9. java.sql.SQLException: The server time zone value‘xxxxxxxx‘ is unrecognized
  10. rabbitmq安装 虚拟ip_步骤4:配置IPv6地址