C++对象的初始化列表

Last Updated: 2023-12-25 07:47:28 Monday

-- TOC --

C++对象初始化成员变量,推荐用初始化列表这种语法来实现。对于const修饰的成员变量,以及没有定义zero-parameter constructor的class type成员变量,只能用这种方式初始化,在初始化列表中调用基类的构造函数比较自然。

成员初始化列表(Member Initializer List)

C++对象初始化列表代码示例:

#include <iostream>
using namespace std;

class xyz{
    int a;
    int b;
    const int c;
public:
    xyz(int a, int b, int c);
    xyz(int b1, int c1);
    xyz(int c);
};

xyz::xyz(int a, int b, int c):
    a(a),
    b(b),
    c(c) {
    cout << this->a << " " << this->b << " " << this->c << endl;
}

xyz::xyz(int b1, int c1):
    a{123},
    b{b1},
    c{c1} {
    // a = 123;  // initialized above with a constant
    cout << a << " " << b << " " << c << endl;
}

xyz::xyz(int c):
    c(c) {
    a = 11;
    b = 22;  // not recommended
    cout << a << " " << b << " " << this->c << endl;
}

int main(void) {
    xyz x(1,2,3);
    xyz y(234,345);
    xyz z(33);
    return 0;
}

如果不在初始化列表中初始化const int c这个成员,编译器会报错!const表达的是可以在runtime期间赋值的readonly变量。(constexpr必须是编译期的const)

创建一个对象,首先准备好内存,然后执行constructor,member initializer list出现在这两者之间,如const变量,如果在constructor内赋值,感觉就有点不对了,似乎就不具有constness属性。member initializer list只是初始化members,constructor内可以干很多其它的事情。

初始化列表可以用于全部成员变量,也可以只用于部分成员变量。初始化列表只能出现在构造函数的实现上,而且在构造函数体执行之前完成执行。

上面的代码,初始化列表有的用圆括号,但更应该使用大括号{}!C++把所有的这些基础类型也看成对象,建议统一使用{}来初始化,下面有一个在初始化列表中初始化数组的示例,就只能使用大括号。不推荐使用()的原因,它看起来很像function call!容易让人疑惑...而且不能检查narrow conversion。

a{},表示将a初始化为0。

如果申明时定义了const int c {1};,1这个值会在初始化列表中被新的值覆盖。合法的初始化可以覆盖预先定义的const变量的值!(const成员的值,可在定义时指定,可在初始化列表中指定,后者会覆盖前者,const申明本就表达了可以在runtime期间赋值的意思)

在使用初始化列表的方式初始化成员变量时,允许构造函数的入参的名称与成员变量名称一样,他们分别在大括号内或外,不会有歧义。而在其它函数内部代码中,如果入参与成员变量名称相同,区分成员还是入参,就要使用this指针

为什么要设计初始化列表?

初始化列表,英文叫做 Member Initializer List,注意是member。

在一本C++教材中,提到了这种设计的好处:

我觉得还有更重要的原因:

测试代码:

#include <iostream>
using namespace std;

struct bob{
    int a;
    const int c = 1;
    bob(int a): a{a} {}
};

struct tom: bob{
    int d;
    tom(int a, int d):
        bob{a}, d{d} {} // place bob{a} in first place
};

int main(void) {
    tom t{3,5};
    cout << t.a << " " << t.c << " " << t.d << endl;
    return 0;
}
#include <iostream>
using namespace std;

struct bob{
    int a;
    const int c;
    bob(int a, int c): a{a},c{c} {}
};

struct tom{
    bob b;
    int d;
    tom(int a, int c, int d):
        b{a,c}, d{d} {}
    tom(int d): d{d} {} // call bob's zero-parameter constructor
};

int main(void) {
    tom t{7,3,5};
    cout << t.d << " " << t.b.a << " " << t.b.c << endl;
    //tom t3{7};  // compile error, bob has no zero-param constructor
    return 0;
}

初始化列表的顺序

初始化列表有个顺序问题,成员变量的初始化顺序是按照变量申明顺序(从上到下)进行的,如果有继承,基类排在前面。

请看下面代码:

#include <iostream>
using namespace std;

class xyz{
    int a;
    int b;
    const int c;
public:
    xyz(int a1, int b1, int c1);
};

xyz::xyz(int a1, int b1, int c1):
    b{a1},
    a{b},
    c{c1} {
    cout << a << " " << b << " " << c << endl;
}

int main(void) {
    xyz x(1,2,3);
    return 0;
}

把初始化列表按顺序翻译过来,就是:

b = a1;
a = b;
c = c1;

但是,编译器不是这么干的,编译器会按照变量的申明顺序进行初始化赋值:

a = b;
b = a1;
c = c1;

这就导致在b还没有值的时候,用b的值去给a赋初值,a的值就随机了!上面代码执行结果如下:

132654704 1 3

打印出来的a,是个随机数,完全不对。

在使用-Wall -Wextra编译选项编译时,这种情况会出现一个reorder warning!既然有个专门的warning,此时,我们就只能严格按照申明顺序来填写初始化列表。就算有继承关系,也要按照顺序来编写初始化列表:

#include <cstdio>

class aa {
public:
    int va;
    aa(int a):
        va{a} {
    }
};

class bb: public aa {
public:
    int vb;
    bb(int a, int b):
        vb{b},   // reorder warning!But not a bug!
        aa{a} {
    }
};

int main(void) {
    bb b(12, 32);
    printf("%u %u\n", b.va, b.vb);
    return 0;
}

上面这段代码,也会出现reorder的告警,需要将vb{b}和aa{a}上下交换顺序,让基类的初始化在前,就OK了。

编译器按照申明顺序初始化成员,而不管初始化代码中的书写顺序,如果代码书写顺序与申明顺序不一致,会出现Warning,仅仅是个Warning,小心可能因为代码执行顺序与书写顺序不一致而导致的问题。在初始化列表中跳过某些成员也是OK的!

在初始化列表中初始化数组

上面的示例,全部都是单一的对象,如果成员是数组,如何在初始化列表中初始化呢?

#include <iostream>
using namespace std;

class xyz {
public:
    int a;
    char b[8];
    xyz();
};

xyz::xyz():
    a(8),
    b{48,49,50,51,52,53} {
}

int main(void) {
    xyz x;
    cout << x.a << endl;
    for (int i=0; i<8; ++i)
        cout << x.b[i] << " | ";
    cout << endl;
    return 0;
}

b{48,49,50,51,52,53}的效果,就相当于C语言中的b[8] = {48,49,50,51,52,53}。故意只初始化前6个字符,留两个,编译之后,这两个是0。

执行效果:

8
0 | 1 | 2 | 3 | 4 | 5 |  |  |

这就是数组在初始化列表中的初始化方法,只能用大括号{}

推荐学习:C++中的各种初始化方法

指针在初始化列表中的初始化

直接给一个示例代码:

SimpleString(const SimpleString& other)
: max_size{ other.max_size },
  buffer{ new char[other.max_size] },
  length{ other.length } {
    std::strncpy(buffer, other.buffer, max_size);
}

这种对new的用法,可能会导致throw std::bac_alloc的异常。

在初始化列表中调用基类的构造函数

详解C++中的继承

NSDMI,Non-Static Data Member Initialization(C++11)

非静态数据成员初始化或简称NSDMI。上文在介绍const成员的时候,已经涉及,即可以在定义的时候直接赋值。const也属于non static。

#include <iostream>
#include <vector>
using namespace std;

int get_d(){
    return 7;
}

struct you{
    int a = 1;
    string b {"abcdefg"};
    vector<int> c {1,2,3};
    int d { get_d() };  // allowed
    you(){}
    you(int a, string b, vector<int> &&c):
        a{a},
        b{b},
        c{c}{}
};

int main() {
    cout << __cplusplus << endl;

    you y1;
    cout << y1.a << y1.b << endl;
    for(auto &x: y1.c)
        cout << x << " ";
    cout << y1.d << endl;

    you y2 {9, "123456", {4,5,6}};
    cout << y2.a << y2.b << endl;
    for(auto &x: y2.c)
        cout << x << " ";
    cout << y2.d << endl;
    return 0;
}

如果在执行constructor前没有被initializer list覆盖,就使用定义时的值。虽然是定义时的值,但其实也是在对象constructor之前的赋值,如果initialization list没有覆盖,编译器就自动使用定义时的值,通过函数调用赋初值也是可以的。

本文链接:https://cs.pynote.net/sf/c/cpp/202204055/

-- EOF --

-- MORE --