C++11新特性之列表初始化的具体使用

目录
  • 统一的初始化方法
  • 列表初始化的一些使用细节
  • 初始化列表
    • 1、任何长度的初始化列表
    • 2、std::initialzer-list的使用细节
  • 列表初始化防止类型收窄

在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体:

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

这些不同的初始化方法都有各自的适用范围和作用,且对于类来说不能用这种初始化的方法,最主要的是没有一种可以通用的初始化方法适用所有的场景,因此C++11中为了统一初始化方式,提出了列表初始化(list-initialization)的概念。

统一的初始化方法

在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:
数组的初始化列表: int arr[3] = {1,2,3}

POD类型的初始化列表:

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

在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:

class Foo
{
public:
    Foo(int) {}
private:
    Foo(const Foo &);
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo a1(123); //调用Foo(int)构造函数初始化
    Foo a2 = 123; //error Foo的拷贝构造函数声明为私有的,该处的初始化方式是隐式调用Foo(int)构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化

    Foo a3 = { 123 }; //列表初始化
    Foo a4 { 123 }; //列表初始化

    int a5 = { 3 };
    int a6 { 3 };
    return 0;
}

由上面的示例代码可以看出,在C++11中,列表初始化不仅能完成对普通类型的初始化,还能完成对类的列表初始化,需要注意的是a3 a4都是列表初始化,私有的拷贝并不影响它,仅调用类的构造函数而不需要拷贝构造函数,a4,a6的写法是C++98/03所不具备的,是C++11新增的写法。
同时列表初始化方法也适用于用new操作等圆括号进行初始化的地方,如下:

int* a = new int { 3 };
double b = double{ 12.12 };
int * arr = new int[] {1, 2, 3};

让人惊奇的是在C++11中可以使用列表初始化方法对堆中分配的内存的数组进行初始化,而在C++98/03中是不能这样做的。

列表初始化的一些使用细节

虽然列表初始化提供了统一的初始化方法,但是同时也会带来一些使用上的疑惑需要各位苦逼码农需要注意,比如对下面的自定义类型的例子:

struct A
{
    int x;
    int y;
}a = {123, 321};
 //a.x = 123 a.y = 321

struct B
{
    int x;
    int y;
    B(int, int) :x(0), y(0){}
}b = {123,321};
//b.x = 0  b.y = 0

对于自定义的结构体A来说模式普通的POD类型,使用列表初始化并不会引起问题,x,y都被正确的初始化了,但看下结构体B和结构体A的区别在于结构体B定义了一个构造函数,并使用了成员初始化列表来初始化B的两个变量,,因此列表初始化在这里就不起作用了,b采用的是构造函数的方式来完成变量的初始化工作。
那么如何区分一个类(class struct union)是否可以使用列表初始化来完成初始化工作呢?关键问题看这个类是否是一个聚合体(aggregate),首先看下C++中关于类是否是一个聚合体的定义:
(1)无用户自定义构造函数。

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

(3)无基类

(4)无虚函数

(5)无{}和=直接初始化的非静态数据成员。下面我们逐个对上述进行分析。

1、首先存在用户自定义的构造函数的情况,示例如下:

struct Foo
{
    int x;
    int y;
    Foo(int, int){ cout << "Foo construction"; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo{ 123, 321 };
    cout << foo.x << " " << foo.y;
    return 0;
}

输出结果为:Foo construction -858993460 -858993460

可以看出对于有用户自定义构造函数的类使用初始化列表其成员初始化后变量值是一个随机值,因此用户必须以用户自定义构造函数来构造对象。

2、类包含有私有的或者受保护的非静态数据成员的情况

struct Foo
{
    int x;
    int y;
    //Foo(int, int, double){}
protected:
    double z;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo{ 123,456,789.0 };
    cout << foo.x << " " << foo.y;
    return 0;
}

实例中z是一个受保护的成员变量,该程序直接在VS2013下编译出错,error C2440: 'initializing' : cannot convert from 'initializer-list' to 'Foo',而如果将z变量声明为static则,可以用列表初始化来,示例:

struct Foo
{
    int x;
    int y;
    //Foo(int, int, double){}
protected:
    static double z;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo{ 123,456};
    cout << foo.x << " " << foo.y;
    return 0;
}

程序输出:123 456,因此可知静态数据成员的初始化是不能通过初始化列表来完成初始化的,它的初始化还是遵循以往的静态成员的额初始化方式。

3、类含有基类或者虚函数

struct Foo
{
    int x;
    int y;
    virtual void func(){};
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo {123,456};
    cout << foo.x << " " << foo.y;
    return 0;
}

上例中类Foo中包含了一个虚函数,该程序也是非法的,编译不过的,错误信息和上述一样cannot convert from 'initializer-list' to 'Foo'。

struct base{};
struct Foo:base
{
    int x;
    int y;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo {123,456};
    cout << foo.x << " " << foo.y;
    return 0;
}

上例中则是有基类的情况,类Foo从base中继承,然后对Foo使用列表初始化,该程序也一样无法通过编译,错误信息仍然为cannot convert from 'initializer-list' to 'Foo',

4、类中不能有{}或者=直接初始化的费静态数据成员

struct Foo
{
    int x;
    int y= 5;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo {123,456};
    cout << foo.x << " " << foo.y;
    return 0;
}

在结构体Foo中变量y直接用=进行初始化了,因此上述例子也不能使用列表初始化方法,需要注意的是在C++98/03中,类似于变量y这种直接用=进行初始化的方法是不允许的,但是在C++11中放宽了,是可以直接进行初始化的,对于一个类来说如果它的非静态数据成员使用了=或者{}在声明同时进行了初始化,那么它就不再是聚合类型了,不适合使用列表初始化方法了。
在上述4种不再适合使用列表初始化的例子中,需要注意的是一个类声明了自己的构造函数的情形,在这种情况下使用初始化列表是编译器是不会给你报错的,操作系统会给变量一个随机的值,这种问题在代码出BUG后是很难查找到的,因此这种情况不适合使用列表初始化需要特别注意,而其他不适合使用的情况编译器会直接报错,提醒你这些场景下使用列表初始化时不合法的。

那么是否有一种方法可以使得在类不是聚合类型的时候可以使用列表初始化方法呢?相信你肯定猜到了,作为一种很强大的语言不应该也不会存在使用上的限制。自定义构造函数+成员初始化列表的方式解决了上述类是非聚合类型使用列表初始化的限制。看下面的例子:

struct Foo
{
    int x;
    int y= 5;
    virtual void func(){}
private:
    int z;
public:
    Foo(int i, int j, int k) :x(i), y(j), z(k){ cout << z << endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo {123,456,789};
    cout << foo.x << " " << foo.y;
    return 0;
}

输出结果为 789 123 456 ,可见,尽管Foo中包含了私有的非静态数据以及虚函数,用户自定义构造函数,并且使用成员列表初始化方法可以使得非聚合类型的类也可以使用列表初始化方法,因此在这里给各位看官提个建议,在对类的数据成员进行初始化的时候尽量在类的构造函数中用成员初始化列表的方式来对数据成员进行初始化,这样可以防止一些意外的错误。

初始化列表

在上面的使得一个类成为非聚合类的例子2、3、4中,这些非法的用法编译器都报出的错误是cannot convert from 'initializer-list' to 'Foo',那么这个initializer-list是什么呢?为什么使用列表初始化方法是将initializer-list转换成对应的类类型呢?下面我们就来看看这个神秘的东西

1、任何长度的初始化列表

在C++11中,对于任意的STL容易都与和为显示指定长度的数组一样的初始化能力,如:

int arr[] = { 1, 2, 3, 4, 5 };
std::map < int, int > map_t { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
std::list<std::string> list_str{ "hello", "world", "china" };
std::vector<double> vec_d { 0.0,0.1,0.2,0.3,0.4,0.5};

STL容易跟数组一样可以填入任何需要的任何长度的同类型的数据,而我们自定义的Foo类型却不具备这种能力,只能按照构造函数的初始化列表顺序进行依次赋值。实际上之所以STL容易拥有这种可以用任意长度的同类型数据进行初始化能力是因为STL中的容器使用了std::initialzer-list这个轻量级的类模板,std::initialzer-list可以接受任意长度的同类型的数据也就是接受可变长参数{...},那么我们是否可以利用这个来改写我们的Foo类,是的Foo类也具有这种能力呢?看下面例子:

struct Foo
{
    int x;
    int y;
    int z;
    Foo(std::initializer_list<int> list)
    {
        auto it= list.begin();
        x = *it++;
        y = *it++;
        z = *it++;
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Foo foo1 {123,456,789};
    Foo foo2 { 123, 456};
    Foo foo3{ 123};
    Foo foo4{ 123, 456, 789,258 };
    cout << foo1.x << " " << foo1.y << " " << foo1.z<<endl;
    cout << foo2.x << " " << foo2.y << " " << foo2.z << endl;
    cout << foo3.x << " " << foo3.y << " " << foo3.z << endl;
    cout << foo4.x << " " << foo4.y << " " << foo4.z << endl;
    return 0;
}

程序的输出结果为:
123 456 789
123 456 -858993460
123 -858993460 -858993460
123 456 789

在上面的例子中我们用std::initialzer-list将类改写,是的类Foo也具有了接受可变长参数的能力,在Foo类中定义了三个变量,分别在main函数中使用1个2个3个4个参数的列表初始化方法来初始化foo变量,可见,由程序的输出结果可知,对于这种拥有固定数目的数据成员来说使用std::initialzer-list来改写后,如果列表初始化的参数刚好是3个则数据成员完全初始化,如果列表初始化的个数小于3个则未给予的值是一个随机值,而大于3个的话超出的列表初始化参数将被抛弃。虽然std::initialzer-list可以改写我们自定义的类,但是对于用于固定的数据成员的类来说这种改写意义不大,若列表初始化个数少于数据成员个数则会导致某些数据成员未被初始化,容易引起程序出BUG,BUT如果我们自定义的类也是一个容器类情况呢?

class FooVec
{
public:
    std::vector<int> m_vec;

    FooVec(std::initializer_list<int> list)
    {
        for (auto it = list.begin(); it != list.end(); it++)
            m_vec.push_back(*it);
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    FooVec foo1 { 1, 2, 3, 4, 5, 6 };
    FooVec foo2 { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return 0;
}

可见,用std::initialzer-list改写我们自定义的容器类可以使得我们自定义的容器类和STL中的容器类一样拥有接受可变长相同数据类型的数据的能力,注意数据类型必须相同。
std::initialzer-list不仅可以用于自定义类型的列表初始化方法,也可以用于传递相同类型数据的集合:

void func(std::initializer_list<int> list)
{
    for (auto it = list.begin(); it != list.end(); it++)
    {
        cout << *it << endl;
    }

}
int _tmain(int argc, _TCHAR* argv[])
{
    func({});//传递一个空集
    func({ 1, 2, 3 });//传递int类型的数据集
    return 0;
}

因此在以后碰到需要使用可变长度的同类型的数据时,可以考虑使用std::initialzer-list。

2、std::initialzer-list的使用细节

简单了解了initialzer-list后,看看它拥有哪些特点呢?
1、它是一个轻量级的容器类型,内部定义了迭代器iterator等容器必须的一些概念。
2、对于initialzer-list<T>来说,它可以接受任意长度的初始化列表,但是元素必须是要相同的或者可以转换为T类型的。
3、它只有三个成员接口,begin(),end(),size(),其中size()返回initialzer-list的长度。
4、它只能被整体的初始化和赋值,遍历只能通过begin和end迭代器来,遍历取得的数据是可读的,是不能对单个进行修改的。
类似下面的操作时不合法的

std::initializer_list<int> list_t ={ 1, 2, 3, 4 };

int _tmain(int argc, _TCHAR* argv[])
{
    for (auto it = list_t.begin(); it != list_t.end; it++)
        (*it) = 1;
    return 0;
}

此外initialzer-list<T>保存的是T类型的引用,并不对T类型的数据进行拷贝,因此需要注意变量的生存期。比如我们不能这样使用:

std::initializer_list<int> func(void)
{
    auto a = 2, b = 3;
    return{ a, b };
}

虽然看起来没有任何问题,且能正常编译通过,但是a,b是在func内定义的局部变量,但程序离开func时变量a,b就销毁了,initialzer-list却保存的是变量的引用,因此返回的将是非法未知的内容。

列表初始化防止类型收窄

C++11的列表初始化还有一个额外的功能就是可以防止类型收窄,也就是C++98/03中的隐式类型转换,将范围大的转换为范围小的表示,在C++98/03中类型收窄并不会编译出错,而在C++11中,使用列表初始化的类型收窄编译将会报错:

int a = 1.1; //OK
int b{ 1.1 }; //error

float f1 = 1e40; //OK
float f2{ 1e40 }; //error

const int x = 1024, y = 1;
char c = x; //OK
char d{ x };//error
char e = y;//error
char f{ y };//error

上面例子看出,用C++98/03的方式类型收窄并不会编译报错,但是将会导致一些隐藏的错误,导致出错的时候很难定位,而利用C++11的列表初始化方法定义变量从源头了遏制了类型收窄,使得不恰当的用法就不会用在程序中,避免了某些位置类型的错误,因此建议以后再实际编程中尽可能的使用列表初始化方法定义变量。

到此这篇关于C++11新特性之列表初始化的具体使用的文章就介绍到这了,更多相关C++11 列表初始化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++11中列表初始化机制的概念与实例详解

    目录 概述 实现机制详解 POD类型的列表初始化 含有构造函数的类的列表初始化(C++11) 列表初始化用于函数返回值 引入std::initializer_list 代码验证 应用 列表初始化防止类型收窄 总结 概述 定义:列表初始化是C++11引入的新标准,目的是统一初始化方式 C++11以前只能使用列表初始化来初始化内置类型数组和POD类型对象,C++11中列表初始化可以用于初始化任何类型对象 POD(plain old data)类型:仅由内置类型变量构成且不含指针的类,简单来说是可以直

  • 简述C++11就地初始化与列表初始化

    1.就地初始化 1.1简介 在C++11之前,只能对结构体或类的静态常量成员进行就地初始化,其他的不行. class C { private: static const int a=10; //yes int a=10; //no } 在C++11中,结构体或类的数据成员在申明时可以直接赋予一个默认值,初始化的方式有两种,一是使用等号"=",二是使用大括号列表初始化的方式.注意,使用参考如下代码: class C { private: int a=7; //C++11 only int

  • C++11特性小结之decltype、类内初始化、列表初始化返回值

    作用:返回表达式或变量的类型 返回值规则: 若e是一个左值(lvalue,即"可寻址值"),则decltype(e)将返回T& 若e是一个临终值(xvalue),则返回值为T&& 若e是一个纯右值(prvalue),则返回值为T decltype()不会执行括号内的表达式,decltype返回的类型是用于声明的,不能用于单纯的判断.比如decltype(a)==int,是不可以的,只能是在定义新的变量.返回值的地方使用: int a=1; decltype(a)

  • C++11新特性之列表初始化的具体使用

    目录 统一的初始化方法 列表初始化的一些使用细节 初始化列表 1.任何长度的初始化列表 2.std::initialzer-list的使用细节 列表初始化防止类型收窄 在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体: struct A { int x; int y; }a={1,2}; 这些不同的初始化方法都有各自的适用范围和作用,且对于类来说不能

  • C++ 11新特性之大括号初始化详解

    本文主要给大家介绍了关于C++11新特性之大括号初始化的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: C++11之前,C++主要有以下几种初始化方式: //小括号初始化 string str("hello"); //等号初始化 string str="hello"; //大括号初始化 struct Studnet{ char* name; int age; }; Studnet s={"dablelv",18}; //

  • C++11新特性中auto 和 decltype 区别和联系

    C++11新特性中auto 和 decltype 区别和联系 一. auto简介 编程时候常常需要把表达式的值付给变量,需要在声明变量的时候清楚的知道变量是什么类型.然而做到这一点并非那么容易(特别是模板中),有时候根本做不到.为了解决这个问题,C++11新标准就引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型.和原来那些只对应某种特定的类型说明符(例如 int)不同.auto 让编译器通过初始值来进行类型推演.从而获得定义变量的类型,所以说 auto 定义的变量必须有初始

  • 浅析C++11新特性的Lambda表达式

    lambda简介 熟悉Python的程序员应该对lambda不陌生.简单来说,lambda就是一个匿名的可调用代码块.在C++11新标准中,lambda具有如下格式: [capture list] (parameter list) -> return type { function body } 可以看到,他有四个组成部分: 1.capture list: 捕获列表 2.parameter list: 参数列表 3.return type: 返回类型 4.function body: 执行代码

  • C++11新特性之自定义字面量

    1.示例 C++11新标准中引入了用户自定义字面量,也叫自定义后缀操作符,即通过实现一个后缀操作符,将申明了该后缀标识的字面量转化为需要的类型.考察如下代码: long double operator"" _mm(long double x) { return x / 1000; } long double operator"" _m(long double x) { return x; } long double operator"" _km(

  • c++11新特性多线程操作实战

    c++11多线程操作 线程 thread int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); thread t3 = t1; thread t4(t1); thread t5 = std::move(t1); thread t6(std::move(t1)); return 0; } t3,t4创建失败,因为thread的拷贝构造和赋值运算符重载的原型是: thread(const thread&) = d

  • C++11新特性std::tuple的使用方法

    1. 引入头文件 #include <tuple> 2. std::tuple初始化 std::tuple<int, std::string, float> t1(10, "Test", 3.14); 这里要注意,不是所有的C++ 11编译器都支持copy-list-initialization的方式.如下代码所示. std::tuple<int, int> foo_tuple() { return {1, -1}; // Error until N

  • C++11新特性std::make_tuple的使用

    std::tuple是C++ 11中引入的一个非常有用的结构,以前我们要返回一个包含不同数据类型的返回值,一般都需要自定义一个结构体或者通过函数的参数来返回,现在std::tuple就可以帮我们搞定. 1.引用头文件 #include <tuple> 2. Tuple初始化 std::tuple的初始化可以通过构造函数实现. // Creating and Initializing a tuple std::tuple<int, double, std::string> resul

  • c++11 新特性——智能指针使用详解

    c++11添加了新的智能指针,unique_ptr.shared_ptr和weak_ptr,同时也将auto_ptr置为废弃(deprecated). 但是在实际的使用过程中,很多人都会有这样的问题: 不知道三种智能指针的具体使用场景 无脑只使用shared_ptr 认为应该禁用raw pointer(裸指针,即Widget*这种形式),全部使用智能指针 初始化方法 class A { public: A(int size){ this->size = size; } A(){} void Sh

  • 详解c++11新特性之模板的改进

    C++11关于模板有一些细节的改进: 模板的右尖括号 模板的别名 函数模板的默认模板参数 模板的右尖括号 C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误. int main() { std::vector<std::vector<int>> a; // error std::vector<std::vector<int> > b; // ok } 这个我之前都不知道,我开始学编程的时候就已经是C

随机推荐