explicit关键字作用于类的构造函数。一旦类的构造函数声明了explicit关键字,构造函数就必须使用显示的方式调用。这样做可以防止构造函数被不知不觉地,莫名其妙地调用。

下面举例子。

在普通构造函数中的explicit:

类的定义如下(未声明explicit):

class num {
    int n;

public:
    num(int n) {
        this->n = n;
        cout << "constructor: " << n << endl;
    }
    ~num() {
        cout << "destructor: " << n << endl;
    }
};

此时,由于未声明explicit,因此,可以使用这种方式来初始化:

num a = 1;
num b = 2;
num c(3);

而且,如果你写了多个构造函数,有的有explicit,有的没有,代码就会自动去找没有声明explicit的构造函数去调用,就像是声明了explicit的构造函数不存在一样。例如:

class num {
    int n;

public:
    num(int n) {
        this->n = n;
        cout << "int constructor: " << n << endl;
    }

    explicit num(float n) {
        this->n = int(n);
        cout << "float constructor: " << n << endl;
    }

    num(double n) {
        this->n = int(n);
        cout << "double constructor: " << n << endl;
    }

    ~num() {
        cout << "destructor: " << n << endl;
    }
};

调用:

num a = 1;
num b = 2.0f;
num c = 3.0;

此时,编译器会自动将2.0f转换成double,导致double constructor调用两次。但是,假如声明了explicit的不是float constructor,而是int或者double,就会直接导致编译不通过。因为,如果是int constructor声明了explicit,那么编译器无法决定int该转成float还是double。同理,如果是double constructor声明了explicit,那么编译器无法决定double该转成float还是int。这就很坑。凭什么float就可以义无反顾地转成了double,到了int和double就迷茫了?因此,我建议不要采用这种写法。

在拷贝构造函数中的explicit:

类的定义如下:

class num {
    int n;

public:
    num(int n) {
        this->n = n;
        cout << "constructor: " << n << endl;
    }

    ~num() {
        cout << "destructor: " << n << endl;
    }
    
    num operator++() {
        n = n + 1;
        return *this;
    }
};

调用:

num n(3);
++n;
++n;

需要注意的是,在重载++运算符时,我返回了一个对象。在运行过程中,可以发现,我写的构造函数调用了一次,析构函数却调用了3次。这就是因为,返回的对象,其实是调用了拷贝构造函数,然后返回的一个匿名对象。因此,默认的拷贝构造函数被悄悄调用了两次。在实际中,可以通过返回引用的方法避免生成匿名对象。但是如何让编译器来帮我们检查,以避免这种事情发生呢?方法就是,将拷贝构造函数声明为explicit。如下。

class num {
    int n;

public:
    num(int n) {
        this->n = n;
        cout << "constructor: " << n << endl;
    }

    explicit num(const num& nm) {
        this->n = nm.n;
        cout << "copy constructor: " << n << endl;
    }

    ~num() {
        cout << "destructor: " << n << endl;
    }
    
    num operator++() {
        n = n + 1;
        return *this;
    }
};

像这样写,就会导致编译错误。因为++运算符返回对象需要隐式调用拷贝构造函数,而explicit不让它被隐式调用。由于返回对象的时候,必然会隐式调用拷贝构造函数,所以这么做其实是根本性杜绝了返回对象这种操作(返回引用多好,返回对象吃力不讨好)。如果真的有需求,需要实现返回新的对象,可以在函数中构造一个local的对象,然后返回其引用。像这样:

num& operator++() {
    n = n + 1;
    num temp(*this);
    return temp;
}

当然,这样会产生一个警告,告诉你返回了局部变量。注意,不要在函数中new一个对象,返回其引用。像这样:

num& operator++() {
    n = n + 1;
    num* temp = new num(*this);
    return *temp;
}

这种做法会导致new的那个对象没法delete,对象没法析构,造成内存泄漏。如果运行的话,可以发现,析构函数调用次数会比构造函数少。

题外话:关于直接赋值初始化

前面说到,可以直接用赋值来进行初始化。例如:

num a = 1;
num b = 2.0f;
num c = 3.0;

在测试的时候,我发现,当普通构造函数和拷贝构造函数都自己写,并且拷贝构造函数的参数没有const关键字时,这种赋值的方式是会报错的,跟是否声明explicit无关。如下:

#include <iostream>
using namespace std;
class num {
    int n;

public:
    num(int n) {
        this->n = n;
        cout << "constructor: " << n << endl;
    }

    num(num& nm) {
        this->n = nm.n;
        cout << "copy constructor: " << n << endl;
    }

    ~num() {
        cout << "destructor: " << n << endl;
    }
};
int main(int argc, char* argv[]) {
    num a = 1;
    return 0;
}

报的错是:error: invalid initialization of non-const reference of type 'num&' from an rvalue of type 'num'

OK,你说我不const,还说我是右值是吧?Fine。我改。修改方案1:参数改成const

num(const num& nm) {
    this->n = nm.n;
    cout << "const copy constructor: " << n << endl;
}

修改方案2:改成右值引用

num(num&& nm) {
    this->n = nm.n;
    cout << "right value copy constructor: " << n << endl;
}

经过测试,这两种修改方案,都不会报错。更神奇的是,无论哪种修改方案,输出都显示只调用了普通的构造函数  (╯-_-)╯┴**—**┴ 拷贝构造函数内心:你根本不调用我,还管我管这么宽?经过轮子哥指点,终于明白了。**GCC就是会帮你检查一下num(num(1))是否合法,然后当num(1)来生成代码的。**真是无语呢……