专题:类的构造函数与拷贝控制
一个类必然包含的函数有:默认构造函数、拷贝构造函数、拷贝赋值函数和析构函数。
类(class)与结构体(struct)的位移区别在于:默认情况下,类的派生方式和访问权限是private的,struct的派生方式和访问权限是public的。
构造函数
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
特点1:不同于其他成员函数,构造函数不能被声明为const的(参见7.1.2,P231)。
当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量属性”。因此,构造函数在const对象的构造过程中可以向其写值。
const Sales_data obj(); //const对象
const成员函数:
const的作用是修改隐式this指针的类型。默认情况下,this 的类型是指向类类型非常量版本的常量指针,如在Sales_data成员函数中,this 的类型是 Sales_data *const。这意味着,在默认情况下,不能把this绑定到一个常量对象上。所以,应该将那些“不会修改类对象”的成员函数定义为const 的。如string isbn();
1 string Sales_data::isbn() const //this 类型为 const Sales_data *const 2 {return this->isbn;}
const相当于将隐式参数this增加了底层const,能够引起重载!
构造函数初始值列表
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
//构造函数的初始值有时必不可少 /*初始化和赋值是两码事*/ class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; ConstRef::ConstRef(int ii) {//赋值 i=ii; ci=ii; //错误:不能给const赋值 ri=i; //错误:ri没被初始化 } //正确:显式的初始化引用和const成员 ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i) { }
//构造函数,版本1 Sales_data::Sales_data(const string &s,unsigned n,double p) { bookNo=s; units_sold=n; revenue=n*p; } //构造函数,版本2 Sales_data(const string &s,unsigned n,double p): bookNo(s),units_sold(n),revenue(p*n){}
版本2采用了初始值列表,两个版本效果相同。区别在于:版本2初始化了他的数据成员,二版本1对数据成员执行了赋值操作。
成员初始化的顺序:与它们在类定义中的出现顺序一致,第一个成员先被初始化,然后第2个,以此类推。构造函数初始值列表中初始值的前后位置不会影响实际的初始化顺序。
合成默认构造函数
如果没有显式的定义构造函数,那么编译器会为我们隐式的定义一个合成的默认构造函数。
初始化规则:1,如果存在类内的初始值(有默认值),用他来初始化成员;2,否则,默认初始化该成员。
某些类(比如包含不能依赖默认初始化的类型成员时)不能依赖于合成的默认构造函数。一般要自定义默认构造函数。
默认构造函数(=default)
1.一般要为类定义一个默认构造函数(=default):
1 struct Sales_data
2 {
3 Sales_data()=default; //他的作用完全等同于合成默认构造函数
4 Sales_data(const string &s):bookNo(s){}
5 ...
6 }
有一种情况必须去掉“=defalut”的默认构造函数,就是:如果一个构造函数为所有的参数都提供了默认实参,则它就成了默认构造函数。如果还有原来的“=default”版本,在“Sales_data A;”时,编译器不知道调用哪个版本的构造函数,将出现“二义性”。
2.默认构造函数的作用(P262)
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化在以下情况发生:当我们在块作用域内不适用任何初始值定义一个非静态变量或者数组时;当一个类本身含有类类型成员且使用合成的默认构造函数时;当类类型的成员没有在构造函数初始值列表中显式地初始化时。
值初始化在以下情况发生:在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时;当我们不适用初始值定义一个局部静态变量时;当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名。
在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
Sales_data obj(); //定义了一个函数obj(),返回类型为Sales_data Sales_data obj; //定义了一个Sales_data对象,使用默认构造函数初始化
委托构造函数(c++11新标准)
委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数(受委托构造函数)匹配。
class Sales_data { public: //非委托构造函数,使用对应的实参初始化成员 Sales_data(string s,unsigned cnt,double price): bookNo(s),units_sold(cnt),revenue(cnt*price){ } //委托构造函数 Sales_data():Sales_data("",0,0){ } Sales_data(string s):Sales_data(s,0,0){ } Sales_data(istream &is):Sales_data(){read(is,*this);} ... };
接受istream&的构造函数,委托给了默认构造函数,默认构造函数又委托给三参数构造函数。
当一个构造函数委托给另一个构造函数时,受委托构造函数的初始值列表和函数体被依次执行。假如函数体包含有代码的话,将先执行完这些代码,然后控制权才会交还给委托者的函数体内。
转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转换机制,有时我们把这种构造函数 称作转换构造函数。
1.隐式的类类型转换
NOTE:能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
string null_book="9-999-9999"; //Sales_data& combine(const Sales_data&); Sales_data item; //使用默认构造函数 item.combine(null_book); //隐式类类型转换
编译器用给定的string自动创建了一个Sales_data对象。新生成的这个(临时)Sales_data对象被传递给combine。因为combine的参数是一个常量引用,所以可以给该参数传递一个临时量。
实际上,我们构建了一个对象,先将它的值加到item中,随后将其丢弃。
NOTE:只允许一次转换
//错误,字面值"9-999-9999"需要先转换为string,然后再转换为类类型 item.combine("9-999-9999"); //正确做法 item.combine(Sales_data("9-999-9999")); item.combine(string("9-999-9999"));
2.抑制构造函数定义的隐式转换(explicit)
explicit Sales_data(const string &s):bookNo(s){ } //
关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数本来就不能用于执行隐式转换。只能在类内声明构造函数时使用explicit关键字,在类外部定于时不应重复。
NOTE:explicit构造函数只能用于直接初始化,
Sales_data item1(null_book); //正确
Sales_data item2=null_book; //错误:explicit构造函数不能用于拷贝形式的初始化过程
总结:
当我们用explicit关键字声明构造函数时:1,将只能以直接初始化的形式使用;2,编译器将不会在自动转换过程中使用该构造函数。
3.为转换显式地使用构造函数(显式转换)
//显式构造的Sales_data对象 item.combine(Sales_data(null_book)); //static_cast可以使用explicit的构造函数 item.combine(static_cast<Sales_data>(cin));
第13章 拷贝控制
五种特殊的成员函数:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。(P440)
拷贝构造函数和移动构造函数定义了【当用同类型的另一个对象初始化本对象时做什么】。拷贝和移动赋值运算符定义了【将一个对象赋予同类型的另一个对象时做什么】。析构函数定义了【当此类型对象销毁时做什么】。
区分拷贝构造函数、拷贝赋值运算符:
1 class test 2 { 3 public: 4 test()=default; 5 ~test()=default; 6 test(const test&) 7 { 8 cout<<"拷贝构造函数"<<endl; 9 } 10 test& operator=(const test&) 11 { 12 cout<<"拷贝赋值运算符"<<endl; 13 } 14 }; 15 16 int main() 17 { 18 test t; 19 test t1=t; //调用拷贝构造函数 20 21 test t2; //调用默认构造函数 22 t2=t; //调用拷贝赋值运算符 23 24 return 0; 25 }
记住:如果一个新对象被定义(例如以上第18和19行),一定有一个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义(例如以上第22行),就不会有构造函数被调用,那么当然就是赋值操作被调用。
根据“是/否有新对象被定义”,可以判断是调用copy构造/copy赋值。
————————————————————————————————