C++11:声明 & 初始化

简介: C++11:声明 & 初始化

初始化

{ }初始化

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

{ }初始化数组:

int arr[] = { 1, 2, 3, 4, 5 };

{ }初始化结构体:

struct stu
{
    char name[20];
    int age;
};

int main()
{
    stu s1 = { "Jones", 18 };

    return 0;
}

C++11扩大了{ }的适用范围,其可以用于所有的内置类型和自定义类型的初始化

C++11希望通过这个语法,使得所有变量可以以一种统一的方式进行初始化

比如对于内置类型:

int i = { 1 };
double d = { 3.14 };
char c = { 'x' };

int* pf = { nullptr };
int& ri = { i };

我们可以通过大括号初始化整型,浮点型,指针,引用等等。在使用{ }进行初始化时,可以省略掉= ,所以以上代码也可以写为:

int i{ 1 };
double d{ 3.14 };
char c{ 'x' };

int* pf{ nullptr };
int& ri{ i };

但是这个语法其实也比较鸡肋,因为这个写法完全没有直接int i = 1;这样来的直接。

接着就是自定义类型的初始化:

struct stu
{
    char name[20];
    int age;
};

int main()
{
    stu s{ "jack", 18 };
    string str{ "hello world" };

    int arr[5]{ 1, 2, 3, 4, 5 };

    return 0;
}

以上代码中,通过直接在变量后面加一对大括号来实现初始化,数组和结构体初始化时的=也可以省略掉。

在此,我要额外辨析一下现有的类的初始化方式:

现在有如下日期类:

class Date
{
public:
    Date(int year, int month, int day)
        : _year(year)
        ,_month(month)
        ,_day(day)
    {}

private:
    int _year;
    int _month;
    int _day;
};

其含义三个变量,表示年月日,一个三参数的构造函数,初始化这个Date

我们有如下方式对其初始化:

Date d1(2024, 4, 3);
Date d2 = { 2024, 4, 3 };
Date d3{ 2024, 4, 3 };

请问这三个方式,分别是如何初始化一个类的?

  • 对于Date d1(2024, 4, 3);,其就是最基础的构造函数调用语法,也就是直接构造
  • 对于Date d2 = { 2024, 4, 3 };,很多人看到这个写法,再想到我们刚才讲的{ }初始化,以为这个是C++11的新语法,其实并不是的。这个写法是多参数构造函数的类型转化,也就是说这个写法{ 2024, 4, 3 };就是把三个int类型转换为Date类型。如果你用explicit关键字修饰这个构造函数,那么类型转换功能就会被禁止,这个写法就会报错对于
  • Date d3{ 2024, 4, 3 };,这个写法即使用了{ },而且还省略了=,这就是C++11提供的新语法了,当用explicit修饰这个构造函数,这个写法依然有效。因为这个写法也是直接调用构造函数,而不是进行类型转换

其实整体上来说,C++11提供{ }的意图在于提供统一的方式来初始化所有类型,但是奈何大部分程序员已经习惯了之前的写法,{ }既没有带来效率的提高,也没有更加人性化的语法设计(甚至我感觉int i = 1比int i{1};更符合人类的习惯),因此这个语法并没有被广泛接受。


initializer_list

initializer_list是一个新的C++类型,我先为大家创建一个initializer_list类型:

auto li = { 1, 2, 3, 4 };

此时,li的类型就是initializer_list,这个时候有的人就疑惑了,{ 1, 2, 3, 4 }分明是一个整型数组,怎么改了个名字就变成新类型了?initializer_list翻译为中文就是初始化列表,也就是说,这是一个用于初始化的工具。


假设现在你有以下数组:

int arr[5] = { 1, 2, 3, 4 };

你要如何用这个数组来初始化一个vector,初始化一个list,初始化一个set呢?

我们好像只能粗暴的遍历数组,然后一个一个插入数据:

vector<int> v;
for (int i = 0; i < 5; i++)
{
    v.push_back(arr[i]);
}

这着实有点麻烦了,但是C++11后,STL的所有容器都增加了新的构造函数,可以通过initializer_list来初始化容器:

vector<int> v({ 1, 2, 3, 4, 5 });

以上代码中,{ 1, 2, 3, 4, 5 }整体就是一个initializer_list,作为参数传给v,调用vector的构造函数。

当然,我们也可以这样写:

vector<int> v = { 1, 2, 3, 4, 5 };

这个写法,则是单参数的类型转化,因为{ 1, 2, 3, 4, 5 }整体就是一个initializer_list类型的参数。

相同的办法,我们还可以初始化map

map<string, string> m = { {"apple","苹果"}, {"strawberry","草莓"}, {"watermelon", "西瓜"} };

以上代码中,最外层的{ }括起来的就是一个initializer_list,内部的三个{ }则是三个不同的pair<const char*, const char*>,不过const char*可以转为string,因此最后pair<const char*, const char*>会变成pair<string, string>。


最外层的initializer_list内部的三个pair,会依次插入进map中,也就是一次拿多个值初始化map的多个节点。


至此,你应该理解了,initializer_list就是在类构造时,如果我们想要一次性初始化多个节点,就把这些节点放进一个initializer_list内部,这样就能在构造函数中直接构造好。


initializer_list本质上也是一个容器,一个类模板:

因此{ 1, 2, 3, 4, 5 }的准确类型应该是:initializer_list<int>

initializer_list的底层也非常简单,我们看看其仅有的四个接口:

一个构造函数constructor,一个描述长度的接口size,以及迭代器begin,end。也就是说initializer_list本质上是一个通过迭代器访问数组的容器。当其它容器通过initializer_list构造自己,其实就是通过迭代器遍历那个存储了节点的数组,然后把数组元素一个一个插入。

也就是说,以下两种情况,本质是一样的:

initializer_list<int> lt = { 1, 2, 3, 4 };

list<int> l1({ 1, 2, 3, 4 });
list<int> l2(lt.begin(), lt.end());

第一个list通过initializer_list初始化自己,第二个list则通过迭代器初始化自己。不过前者更加方便,是C++11提供的,而后者是C++98提供的。


声明

auto

在C++11中,新增了关键字auto,其可以自动推导类型:

auto i = 1;//整型
auto d = 3.14;//浮点型
auto p = &i;

此时i就会被自动识别为intd就自动识别为doublep自动识别为int*

auto的主要作用在于对于有一些类型,它的长度太长了,我们就可以用auto一笔带过。

比如完整地定义一个迭代器:

vector<int> v;
vector<int>::iterator it = v.begin();

但是我们可以用auto直接自动识别:

vector<int> v;
auto it = v.begin();

在定义迭代器的时候,auto的使用还是比较常见的。


decltype

在C++11以前,有一个关键字typeid,其可以识别一个类型,并且可以通过name成员函数来输出类型名。

比如这样:

int i = 0;
int* pi = &i;

cout << typeid(i).name() << endl;
cout << typeid(pi).name() << endl;

输出结果为:

int
int * __ptr64

也就是说,我们可以通过typeid来检测甚至输出变量类型。

decltype也是用于识别类型的,但是decltypetypeid应用方向不同。

decltype可以检测一个变量的类型,并且拿这个类型去声明新的类型

比如这样:

int i = 0;
decltype(i) x = 5;

decltype(i)检测出i的类型为int,于是decltype(i)整体就变成int,从而定义出一个新的变量x


nullptr

在C++11后,推出了新的空指针nullptr,明明已经有NULL了,为啥还需要nullptr?

NULL在C语言中,表示的是((void*)0),也就是被强制转为void*类型的0。但是在C++中,NULL就是整数0

比如可以用刚才学的typeid验证一下:

cout << typeid(NULL).name() << endl;

输出结果为:int,这下就石锤了NULL在C++中就是int

这会导致不少问题,比如这样:

void func(int x)
{
    cout << "参数为整型" << endl;
}

void func(void* x)
{
    cout << "参数为指针" << endl;
}

int main()
{
    func(NULL);

    return 0;
}

以上代码中,func函数有两个重载,一个是参数为指针,一个是参数为整型。我现在就是想传一个空指针去调用指针版本的func。但是最后还是会调用int类型的。


而nullptr不一样,nullptr不仅不是整型,而且其也不是void*。C++给了nullptr一个专属类型nullptr_t。这个类型有一个非常非常大的优势,该类型只能转化为其它指针类型,不能转化为指针以外的类型。

比如以下代码:

int x1 = NULL;//正确
int x2 = nullptr;//错误

因为NULL本质是0,其可以转化为很多非指针类型,比如intdoublechar。但是nullptrnullptr_t,它只能转化为其他指针。上述代码中,我们把nullptr转化为一个int,此时编译器会直接报错,绝对禁止这个行为。

但是这样是可以的:

void* p1 = nullptr;
int* p2 = nullptr;
char* p3 = nullptr;
double* p4 = nullptr;
相关文章
|
5天前
|
存储 算法 程序员
【C++20 新特性 】模板参数包展开与Lambda初始化捕获详解
【C++20 新特性 】模板参数包展开与Lambda初始化捕获详解
92 3
|
5天前
|
安全 程序员 编译器
C++中的RAII(资源获取即初始化)与智能指针
C++中的RAII(资源获取即初始化)与智能指针
24 0
|
5天前
|
自然语言处理 编译器 C语言
【C++ 20 新特性】参数包初始化捕获的魅力 (“pack init-capture“ in C++20: A Deep Dive)
【C++ 20 新特性】参数包初始化捕获的魅力 (“pack init-capture“ in C++20: A Deep Dive)
46 0
|
5天前
|
安全 编译器 程序员
【C++ 修饰符关键字 explicit 】掌握C++中的explicit :构造函数行为和初始化综合指南
【C++ 修饰符关键字 explicit 】掌握C++中的explicit :构造函数行为和初始化综合指南
122 3
|
5天前
|
设计模式 算法 数据安全/隐私保护
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用(二)
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用
29 0
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用(二)
|
5天前
|
存储 算法 编译器
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用(一)
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用
71 0
|
5天前
|
安全 程序员 编译器
【C++类和对象】初始化列表与隐式类型转换
【C++类和对象】初始化列表与隐式类型转换
|
5天前
|
编译器 C++ 容器
【C++11(一)】右值引用以及列表初始化
【C++11(一)】右值引用以及列表初始化
|
5天前
|
编译器 C++
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
【C++基础(八)】类和对象(下)--初始化列表,友元,匿名对象
|
5天前
|
编译器 C++
C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵
C++编程之美:探索初始化之源、静态之恒、友情之桥与匿名之韵
26 0