explicit构造函数
前面是扫盲部分,可直接看后面的精华部分。
/* 扫盲
一、隐式的类类型转换
1. 转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
2. 从参数类型向类类型隐式转换的实例
//在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则。 //即,在需要使用Sales_data的地方,我们可以使用string或istream作为替代。 Sales_data total("9-999-99999-9", 2, 25.0); string book = "9-999-99999-9"; //构造一个临时的Sales_data对象,其units_sold和revenue等于0,bookNo等于book total.combine(book); //我们用一个string实参调用了Sales_data的combine成员。 //在该调用中,编译器用给定的string自动创建了一个Sales_data对象,这个新生成的临时对象被传递给combine
【从istream到Sales_data的转换】
//使用istream构造函数创建一个函数传递给combine total.combine(cin); //隐式地把cin转换成Sales_data,这个转换执行了接受一个istream的Sales_data构造函数 //该构造函数通过读取标准输入创建了一个临时的Sales_data对象,随后将得到的对象传递给combine
3. 编译器只会自动地执行一步类型转换
//下面的代码隐式地使用了两种转换规则(错误) //错误:需要用户定义的两种转换 //(1):把"9-999-99999-9"转换成string //(2):再把这个临时的string转换成Sales_data total.combine("9-999-99999-9"); //可以显式地把字符串转换成string或者Sales_data对象 //正确:显式地转换成string,隐式地转换成Sales_data total.combine(string("9-999-99999-9")); //正确:隐式地转换成string,显式地转换成Sales_data total.combine(Sales_data("9-999-99999-9"));
二、抑制构造函数定义的隐式转换
1. 将构造函数声明为explicit 来阻止类类型隐式转换
构造函数将不能隐式地创建Sales_data对象
2. 关键字explicit 只对一个实参的构造函数有效
需要多个实参的构造函数不能用于执行隐式转换
3. explicit 构造函数只能用于直接初始化
发生隐式转换的一种情况是:执行拷贝形式的初始化时(使用=)
string book = "9-999-99999-9"; //正确:直接初始化 Sales_data total(book); //错误:不能将explicit构造函数用于拷贝形式的初始化过程 Sales_data total = book;
4. 在类内声明构造函数时使用explicit 关键字,在类外部定义时不应重复
class Sales_data{ public: Sales_data() = default; Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n); explicit Sales_data(const string &s): bookNo(s) {} explicit Sales_data(istream &) };
三、例题
1. 说明接受一个string 参数的Sales_data 构造函数是否应该是explicit 的,并解释这样做的优缺点。
答:这个随便,因为这是一把双刃剑
不带explicit的优点:可以从构造函数的参数类型向类类型隐式转换
带explicit的优点:任何构造函数(尤其是带一个参数的)都不能隐式地创建类对象
带explicit的缺点:该构造函数只能以直接初始化的形式使用
2. vector 将其单参数的构造函数定义成explicit 的,而string则不是,你觉得原因何在?
答:string接受的单参数是const char*类型,如果我们得到了一个常量指针,则把它看做string对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为explicit。
与string相反,vector接受的单参数是int类型,这个参数的原意是指定vector的容量。如果我们在本来需要vector的地方提供一个int值并且希望这个int值自动转换成vector,则这个过程显得比较牵强,因此把vector的单参数构造函数定义成explicit的更加合理。
例如:定义为explicit是为了防止隐式的类型转换
void fun(vecor<int> vi); fun(10); //如果允许这样,那就意味不明了
四、更新
1. 如果我们使用的初始化值要求通过一个explicit 的构造函数来进行类型转换,那么使用拷贝初始化还是直接初始化就很关键了:
vector<int> v1(10); //正确:直接初始化 vector<int> v2 = 10; //错误:接受大小参数的构造函数是explicit的 void f(vector<int>); //f的参数进行拷贝初始化 f(10); //错误:不能用一个explicit的构造函数拷贝一个实参 f(vector<int>(10)); //正确,从一个int直接构造一个临时vector
解读:直接初始化v1是合法的,而拷贝初始化v2则是错误的,因为vector的接受单一大小参数的构造函数是explicit的(于是10就不能隐式转换为vector<int>类型,当然不能赋值给v2了)。同理,当传递一个实参或从函数返回一个值时,我们不能隐式使用一个explicit构造函数。如果希望使用explicit构造函数,那就需要显式地使用,正如最后一行代码那样。
2. 我们可以使用new返回的指针来初始化智能指针,但接受指针参数的智能指针构造函数是explicit的,因此我们不能够将一个内置指针隐式转换为一个智能指针,而必须使用直接初始化形式来初始化一个智能指针:
//正确,使用了直接初始化形式,shared_ptr有相应的构造函数 shared_ptr<int> p(new int(1024)); //错误,赋值符右侧是内置指针,不能隐式转换为智能指针 shared_ptr<int> p = new int(1024);
*/
/* 精华 */
一、单参数构造函数——一步类类型转换
- 当类中含有单参数构造函数时,则表明定义了一条从构造函数的参数类型向类类型隐式转换
#include <iostream> using namespace std; class A { public: A() = default; A(string str) : s(str) {} // 单参数 void print(const A &a) { cout << a.i << " " << a.d << " " << a.s << endl; } private: int i = 2; double d = 3.14; string s = "haha"; }; int main() { A a; string ss("trans"); a.print(ss); // string类型转换为A类型 return 0; }
输出:
二、explicit构造函数
- 破坏上面的那条规则
- 只能用于直接初始化
class A { public: A() = default; explicit A(string str) : s(str) {} // 单参数 private: int i = 2; double d = 3.14; string s = "haha"; }; int main() { string ss("explicit"); A a(ss); // A a1 = ss; // 错误 return 0; }
注:发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=,即上面代码行中的),故此处发生错误。