统一的类成员初始化语法与 stdinitializer_list

Posted vector6_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了统一的类成员初始化语法与 stdinitializer_list相关的知识,希望对你有一定的参考价值。

统一的初始化语法:列表初始化与 std::initializer_list

类成员的列表初始化

假设类 A 有一个成员变量是一个 int 数组,在 C++ 98/03 标准中,如果我们要在构造函数中对其进行初始化,我们需要这样写:

//C++ 98/03 类成员变量是数组时的初始化语法
class A
{
public:
    A()
    {
        arr[0] = 2;
        arr[1] = 0;
        arr[2] = 1;
        arr[3] = 9;
    }

public:
    int arr[4];
};

由上示例可知这种赋值方法非常繁琐,对于字符数组,我们可能就要在构造函数中使用 strcpymemcpy 这一类函数了;再者如果数组元素足够多,初始值又没什么规律,这种赋值代码会有很多行。但是,如果 arr 是一个局部变量,我们在定义 arr 时其实是可以使用如下的语法初始化的:

int arr[4] = {2, 0, 1, 9};

既然 C++98/03 标准中,局部变量数组支持这种语法,为什么在类成员变量语法中就不支持呢?这是旧语法不合理的一个地方,因此在 C++11 语法中类成员变量也可以使用这种语法进行初始化了:

//C++ 11 类成员变量是数组时的初始化语法
class A
{
public:
    A() : arr{2, 0, 1, 9}
    {        
    }

public:
    int arr[4];
};

另外在定义一个类时,若给其成员变量设置一个初始值,在像 Java 这类语言中,可以这样做:

class A
{
    public int a = 1;
    public String string = "helloworld"; 
};

但在 C++ 89/03 标准中要使用这种语法,必须是针对类的 static const 成员,且必须是整型(包括 bool、char、int、long 等)。

//C++ 89/03 在类定义处初始化成员变量
class A
{
public:
    //T 的类型必须整型,且必须是 static const 成员
    static const T t = 某个整型值;
};

C++ 11 标准中,就没有这种限制了,你可以使用花括号(即 {})对任意类型的变量进行初始化,且不用是 static 类型。

//C++ 11 在类定义处初始化成员变量
class A
{
public:
    bool           ma{true};
    int            mb{2019};
    std::string    mc{"helloworld"};     
};

当然,在实际开发的时候,建议还是将这些成员变量的初始化统一写到构造函数的初始化列表中去,方便代码阅读和维护

统一的列表初始化语法

实际上,c++11为了统一变量初始化的方式,提供的列表初始化的方式(一致性初始化),不管是内置类型、自定义类型或者STL容器都可以使用这种初始化方式。

  • 内置类型的初始化方式

    int a{1};                    //c++98支持
    int a(1);					 //c++98支持
    int a = {1};				 //c++98支持
    int array[] = {1,3,6,9,10};	 //c++98支持
    int array[]{1,3,6,9,10};	 //c++98不支持,c++11支持
    
  • STL容器的初始化方式

    vector<int> value = {1,3,6,9,10};
    vector<int> velue{1,3,6,9,10};
    
  • 自定义类型的初始化方式

    1. 对于聚合类:

      聚合类:

      a) 无用户自定义构造函数

      b) 无私有或受保护的非静态数据成员

      c) 无基类

      d) 无虚函数

      e) 无{}和=直接初始化的非静态数据成员

      struct A
      {
          int x;
          int y;
      };
      A a{1,2};
      

      对于聚合类可以直接使用列表初始化的方式对对象进行初始化,但要注意如果我们写成 A a{1} ,x和y的值分别是多少?答案是 x=1,y为随机值。所以要注意使用列表初始化时一定要对可以初始化所有成员都进行初始化。

    2. 对于非聚合类:

      必须要定义对应的构造函数,有2种方式:

      方式一:

      struct A
      {
        	A(int x, int y):x(x),y(y)
          {
              
          }
          int x;
          int y;
      };
      A a{1,2};
      

      这种方式和A a(1,2)是一样的,使用的是构造函数初始化;

      方式二(使用c++11提供的initializer_list):

      struct A
      {
        	A(initializer_list<int> lists)
          {
              auto it = lists.begin();
              x = *it++;
              y = *ot;
          }
          int x;
          int y;
      };
      A a{1,2};
      

      那这时考虑如果类中同时存在以上两种构造方法时优先调用哪种呢?即:

      struct A
      {
        	A(int x, int y):x(x),y(y)
          {
              
          }
          A(initializer_list<int> lists)
          {
              auto it = lists.begin();
              x = *it++;
              y = *ot;
          }
          
          int x;
          int y;
      };
      A a{1,2};
      

      可以验证此时使用的是第二个构造函数,编译器在存在形参为 initializer_list 类型的构造函数时,可以直接传入{}中的元素,不会进行分解,即优先调用 initializer_list 形参的构造函数。

列表初始化的实现原理

通过上面的介绍,在自定义类中也支持这种花括号就需要用到 C++11 引入的新对象 std::initializer_list,由表达式可知这是一个模板对象,接收一个自定义参数类型 TT 既可以是基础数据类型(如编译器内置的 bool、char、int 等)也可以是自定义复杂数据类型。为了使用 std::initializer_list,需要包含头文件 initializer_list

#include <iostream>
#include <initializer_list>
#include <vector>

class A
{
public:
    A(std::initializer_list<int> integers)
    {
        m_vecIntegers.insert(m_vecIntegers.end(), integers.begin(), integers.end());
    }

    ~A()
    {

    }

    void append(std::initializer_list<int> integers)
    {
        m_vecIntegers.insert(m_vecIntegers.end(), integers.begin(), integers.end());
    }

    void print()
    {
        size_t size = m_vecIntegers.size();
        for (size_t i = 0; i < size; ++i)
        {
            std::cout << m_vecIntegers[i] << std::endl;
        }
    }

private:
    std::vector<int> m_vecIntegers;
};

int main()
{
    A a{ 1, 2, 3 };
    a.print();

    std::cout << "After appending..." << std::endl;

    a.append({ 4, 5, 6 });
    a.print();

    return 0;
}

上述代码,我们自定义了一个类 A,为了让 A构造函数append 方法同时支持花括号语法,给这两个方法同时设置了一个参数 integers,参数类型均为 std::initializer_list, 因此就可以使用{}列表来初始化。

std::initializer_list 除了构造函数还提供了三个成员函数,这和 stl 的其他容器的同名方法用法一样:

//返回列表中元素的个数
constexpr size_type size() const;
//返回第一个元素的指针
const T* begin() const;
//返回最后一个元素的下一个位置,代表结束
const T* end() const;

需要注意的是:

  1. initializer_list 的对象是常量,不能被修改
  2. initializer_list 对象中的元素必须完全一致

列表初始化的其他优点

使用列表初始化还有一个很大的优势,可以防止类型收窄。

类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。可能导致类型收窄的场景有:

  1. 从浮点数隐式转换成整数。比如: int a=1.2
  2. 从高精度的浮点数转换成低精度的浮点数,比如 long long double 转换成 long double ,或者 double 转换成 float
  3. 从整形转换成浮点型。如果整形的大小已经超过浮点型的表示范围 ,也属于类型收窄。
  4. 从整形转换成更低长度的整形,如 char a = 1024;
int a = 1024;
int b = 254;

char e = a;  //编译通过
char d{a};	 //编译不通过
char e{b};	 //编译通过

由上示例可以看出,列表初始化方式可以有效防止类型收窄。

以上是关于统一的类成员初始化语法与 stdinitializer_list的主要内容,如果未能解决你的问题,请参考以下文章

内部类

C++的类对象与成员

C++11常用知识点(上)

将成员函数传递给模板函数时出现语法错误

final成员变量

为什么 没有缺省构造函数的类类型成员 必需要在初始化列表 里初始化 ?