首页 » C/C++ » C++构造函数

C++构造函数

C++构造函数用于创建一个对象。构造函数可以被重载,以实现多种构造方式。

拷贝构造函数

拷贝构造函数(copy constructor)的语法如:

class X {
    public:
        X(const X& x) {}
};

通常在3种情况下会调用拷贝构造函数。

1 初始化一个对象时:

X a;        // default constructor
X b = a;    // copy constructor

在此情况下要注意区分拷贝构造函数和复制的区别:

X a;
X b = a;    // copy constructor

X c;
c = a;      // copy assignment

2 按值传递一个对象时:

void foo(X x);

X a;        // default constructor
foo(a);     // copy constructor

3 函数返回一个对象时:

X foo() {
    X a;
    return a;   // copy constructor
}

C++11中引入了右值引用和move语义,构造函数中再添加一种语法:

template<class T> class clone_ptr 
{
    public:
        clone_ptr(clone_ptr&& p) : _ptr(p._ptr) { p._ptr = 0; }
    private:
        T* _ptr;
};

构造函数语义学

本节笔记来自《深度探索C++对象模型》第2章构造函数语意学(The Semantics of Constructors)。

C++的conversion运算符常会引发一些意想不到的问题。例如iostream最初的实现,为了让cin实现在if语句的判断如:

if(cin) {}

Jerry Schwarz定义了一个conversion运算符:

operator int()

但粗心的程序员可能把cout<<intval;写作cin<<intval;,这样<<就被解析为左移位运算符。这可不是我们想要的结果。后来Jerry用operator void*()取代了operator int()。关键字explicit的主要作用就是阻止将单一的constructor被当作一个conversion运算符。

默认构造函数在需要的时候被编译器生成。这里的关键是需要的时候,谁需要?如果是程序员需要,那麻烦自己动手;如果是编译器需要,才是真正的需要。如下例:

class Foo {
    public:
        int     _val;
        Foo*    _next;
};

int main()
{
    Foo bar;    // 编译器不会生成默认构造函数
    if(bar._val || bar._next) {
        // ...
    }

    return 0;
}

这里程序员应该手工添加默认构造函数:

class Foo {
    public:
        Foo() : _val(0), _next(NULL) {}
        // ...
};

在以下4种情况下,编译器会为程序员构建默认构造函数。

情况1 对象包含带有默认构造函数的成员变量。

class Foo {
    public:
        Foo() { printf("construct Foo"); }
};

class Bar {
    public:
        Foo _foo;   
        int _i;
};

int main()
{
    Bar bar;    // output: construct Foo

    return 0;
}

编译器会构造Bar的默认构造函数,并初始化Bar::_foo(调用Foo::Foo),但Bar::_i的构造由程序员负责,不是编译器的责任。编译器生成的伪代码如下:

inline Bar::Bar() {
    _foo.Foo::Foo();
}

现在如果程序员定义了Bar的默认构造函数:

Bar::Bar() { _i = 0; }

编译器依然有责任初始化_foo,它生成的伪代码如下:

Bar::Bar() {
    _foo.Foo::Foo();    // 编译器生成的代码
    _i = 0;             // 程序员写的代码
}

如果有多个成员变量,则初始化顺序将依循它们在类中的声明顺序:

class Dopey     { public: Dopey() { printf("Dopey\n"); } };
class Bashful   { public: Bashful() { printf("Bashful\n"); } };
class Sneezy    {
    public:
        Sneezy() { printf("Sneezy\n"); }
        Sneezy(int i) { printf("Sneezy with %d\n", i); }
};

class SnowWhite {
    public:
        Dopey _dopey;       // 小矮人
        Sneezy _sneezy;     // 喷嚏精
        Bashful _bashful;   // 害羞鬼

        int _mumble;    // 喃喃 
};

int main()
{
    SnowWhite sw;

    return 0;
}

输出(按声明顺序依次构造):

Dopey
Sneezy
Bashful

如果SnowWhite有构造函数,那编译器会安插一些代码来初始化成员变量:

SnowWhite::SnowWhite() : _sneezy(1024) { _mumble = 1024; }

SnowWhite::SnowWhite() : _sneezy(1024) {
    // 编译器伪代码
    _dopey.Dopey::Dopey();
    _sneezy.Sneezy::Sneezy(1024);
    _bashful.Bashful::Bashful();

    // 用户代码
    _mumble = 1024;
}

情况2 基类带有默认构造函数。

class A {
    public:
        A() { printf("construct A\n"); }
};

class B : public A {
    public:
        B() { printf("construct B\n"); }
};

class C : public B {};

int main()
{
    C c;

    return 0;
}

输出:

construct A
construct B

情况3 带有虚函数的类。

如果用户没有提供默认构造函数,编译器需要合成一个,以创建vptr和vtable。

情况4 虚基类继承体系下的类。

class X { public: int i; };
class A : virtual public X { public: int j; };
class B : virtual public X { public: int k; };
class C : public A, public B { public: int d; };

无法在编译期确定pa->X::i的地址:

void foo(const A* pa) {
    pa->i = 1024;
}

可能被编译器改写为:

void foo(const A* pa) {
    pa->__vbcX->i = 1024;
}

其中__vbcX由编译器在构建对象时生成,指向虚基类X。

构造函数的其他问题

TC++PL中有一个问题,如何不修改main()函数,在hello berlinix前后输出两行:

int main()
{
    std::cout << "hello berlinix!\n";

    return 0;
}

一个解决方案是通过全局对象的构造和析构函数:

truct constr {
    constr()    { std::cout << "initialize\n"; }
    ~constr()   { std::cout << "cleanup\n"; }
} c;

输出:

initialize
hello berlinix!
cleanup

参考

《C++对象模型》

A Brief Introduction to Rvalue References

分享

0