c++11新特性篇-列表初始化

列表初始化

概述

列表初始化是 C++11 引入的一个新特性。**它允许使用花括号 {} 来初始化变量、数组、结构体等。**列表初始化具有更加统一和直观的语法,同时也提供了一些优势,例如防止窄化转换(narrowing conversion)等。

下面是一些列表初始化的例子:

int x{42};  // 初始化整数变量 x 为 42

int arr[]{1, 2, 3};  // 初始化整数数组 arr 为 {1, 2, 3}

struct Point {
    int x;
    int y;
};

Point p{10, 20};  // 初始化结构体 Point 的对象 p 的成员 x 和 y

#include 
std::vector vec{1, 2, 3};  // 初始化整数向量 vec 为 {1, 2, 3}

// 编译错误,禁止窄化转换
int narrowError{3.14};  

窄化转换

窄化转换(Narrowing Conversion)指的是在类型转换时可能导致信息的损失或不准确的转换具体来说,当将一个具有更大范围的值转换为一个较小范围的类型时,可能导致丢失精度或截断

在 C++ 中,窄化转换是一种类型转换的错误,因为它可能导致数据丢失或错误的结果。C++11 引入了列表初始化,部分是为了解决窄化转换的问题,因为使用花括号 {} 进行列表初始化时,会禁止窄化转换,提高了代码的类型安全性。

以下是一个示例,展示了窄化转换的情况:

#include 

int main() {
    // 编译通过,但可能导致窄化转换
    int x = 3.14;  // x 为 3,发生了从 double 到 int 的窄化转换

    // 编译错误,禁止窄化转换
    int y{3.14};  // 无法将 double 转换为 int,发生窄化转换

    std::cout << x << std::endl;

    return 0;
}

第一个赋值语句虽然可以通过编译,但它进行了从 doubleint 的窄化转换,导致小数部分被截断。而第二个赋值语句使用了列表初始化,由于列表初始化禁止窄化转换,所以会导致编译错误。在实践中,推荐使用列表初始化语法,以提高代码的类型安全性。

列表初始化细节

聚合体

先看这段代码:

#include 
#include 
using namespace std;

struct T1
{
    int x;
    int y;
}a = { 123, 321 };

struct T2
{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = { 123, 321 };

int main(void)
{
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

程序执行结果:

a.x: 123, a.y: 321
b.x: 10, b.y: 20

可以看出,T2中由于有了构造函数,初始化列表中的数据优先级就低了,对象是通过构造函数进行的初始化。

解释:如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。

什么是**聚合体**?

在C++中,聚合体(Aggregate)是一种特殊的数据类型,用于将多个数据成员组合成一个单一的数据单元。C++11标准对聚合体进行了一些修订和扩展。

一个聚合体必须满足以下条件:

  1. 所有非静态数据成员都是公有的。
  2. 没有定义任何构造函数、析构函数、拷贝构造函数、拷贝赋值运算符或者移动构造函数。
  3. 没有基类、虚函数或者虚基类。

在C++11之前,聚合体通常用于数组初始化。**在C++11及之后,聚合体的概念被扩展,它还可以通过初始化列表进行初始化。**这意味着在C++11及以后,符合聚合体条件的类型可以使用更灵活的初始化方式。

以下是一个简单的示例:

// 在C++11之前的聚合体
struct Point {
    int x;
    int y;
};

// 在C++11及以后的聚合体
struct Rectangle {
    int width;
    int height;
    
    // 在C++11之后,可以包含构造函数
    Rectangle(int w, int h) : width(w), height(h) {}
};

int main() {
    // C++11之前,使用聚合体初始化数组
    Point points[] = { {1, 2}, {3, 4}, {5, 6} };

    // C++11及以后,使用聚合体初始化变量
    Rectangle rect{10, 20};

    return 0;
}

在上述示例中,Point 是一个C++11之前的聚合体,可以用花括号初始化数组。而Rectangle 是一个C++11及以后的聚合体,可以使用花括号初始化变量。这种初始化方式使得代码更为简洁,并提高了可读性。

非聚合体

对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:

#include 
#include 
using namespace std;

struct T1
{
    int x;
    double y;
    // 在构造函数中使用初始化列表初始化类成员
    T1(int a, double b, int c) : x(a), y(b), z(c) {}
    virtual void print()
    {
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(void)
{
    T1 t{ 520, 13.14, 1314 };	// ok, 基于构造函数使用初始化列表初始化类成员
    t.print();
    return 0;
}

另外,需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:

#include 
#include 
using namespace std;

struct T1
{
    int x;
    double y;
private:
    int z;
};

struct T2
{
    T1 t1;
    long x1;
    double y1;
};

int main(void)
{
    T2 t2{ {}, 520, 13.14 };
    return 0;
}

可以看到,T1并非一个聚合类型,因为它有一个Private的非静态成员。但是尽管T2有一个非聚合类型的非静态成员t1,T2依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。

最后强调一下t2对象的初始化过程,对于非聚合类型的成员t1做初始化的时候,可以直接写一对空的大括号{},这相当于调用是T1的无参构造函数

对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用列表初始化将会调用它对应的构造函数。

std::initializer_list

在C++的STL容器中,可以进行任意长度的数据的初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和STL一样有任意长度初始化的能力,可以使用std::initializer_list这个轻量级的类模板来实现

std::initializer_list 的定义如下:

namespace std {
    template 
    class initializer_list {
    public:
        typedef T value_type;
        typedef const T& reference;
        typedef const T& const_reference;
        typedef size_t size_type;

        initializer_list() noexcept;
        size_t size() const noexcept;
        const T* begin() const noexcept;
        const T* end() const noexcept;
    };
}

先来介绍一下这个类模板的一些特点:

  • 它是一个轻量级的容器类型,内部定义了迭代器iterator等容器必须的概念,遍历时得到的迭代器是只读的。
  • 对于std::initializer_list而言,它可以接收任意长度的初始化列表,但是要求元素必须是同种类型T
  • 在std::initializer_list内部有三个成员接口:size(), begin(), end()。
  • std::initializer_list对象只能被整体初始化或者赋值。
作为普通函数参数

如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为std::initializer_list,使用初始化列表{ }作为实参进行数据传递即可

#include 
#include 
using namespace std;

void traversal(std::initializer_list a)
{
    for (auto it = a.begin(); it != a.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
}

int main(void)
{
    initializer_list list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    
    // 直接通过初始化列表传递数据 //
    
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}

输出结果为:

current list size: 0

current list size: 10
1 2 3 4 5 6 7 8 9 0

current list size: 5
1 3 5 7 9

2 4 6 8 0

11 12 13 14 15 16

std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。

作为构造函数参数

自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为std::initializer_list类型,在自定义类的内部还是使用容器来存储接收的多个实参

#include 
#include 
#include 
using namespace std;

class Test
{
public:
    Test(std::initializer_list list)
    {
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector m_names;
};

int main(void)
{
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}

输出的结果:

jack lucy tom
hello world nihao shijie

你可能感兴趣的:(c++,算法,开发语言)