explicit构造函数
考虑一个类Date:
class Date{ int d,m,y; //... }; void my_fact(Date d); void f() { Date d{15}; //似乎合理:x变为{15,today.m,today.y} d = 15; //含混 my_fact(15); //含混 //... }
这最多是一段含混代码,数据15和Date之间并没有清晰的逻辑关联。
但是,我们可以指明构造函数不能用作隐式类型转换。如果构造函数的声明带有关键字explicit,它只能用于初始化和显示类型转换。例如:
class Date{ int d, m, y; public: explicit Date(int dd = 0,int mm=0,int yy = 0); //... }; Date d1{15}; //OK:被看作显式类型转换 Dated2 = Date{15}; //OK:显式类型转换 Date d3 = {15}; //错误: = 方式的初始化不能进行隐式类型转换 Date d4 = 15; //错误: = 方式的初始化不能进行隐式类型转换 void f() { my_fact(15); //错误:参数传递不能进行隐式类型转换 my_fact({15}); //错误:参数传递不能进行隐式类型转换 my_fact(Date{15}); //OK:显式类型转换 }
用 = 进行初始化可以看做 拷贝初始化,一般而言,初始化器的副本会被方式待初始化的对象。但是,如果初始化器是一个右值,这种拷贝可能被优化掉(取消),而采用移动操作,省略 = 会将初始化变为显式初始化。显式初始化也称为 直接初始化。
默认情况下,应该将单参数的构造函数生命为explicit。除非有更好的理由,否则应该按照这种默认的方式去做。如果定义隐士构造函数,最好写下原因,否则代码的维护者可能怀疑你疏忽了,或者不懂这一原则。
如果一个构造函数声明为explicit且定义在类外,则在定义中不能重复explicit:
class Date{ int d,m,y; public: explicit Date(int dd); //.... }; Date::Date(int dd){/*...*/} //OK explicit Date::Date(int dd){/*...*/} //错误
大多数explicit起重要作用的构造函数都接收单一参数。但是,explicit也可以用于五参或多个参数的构造函数。例如:
struct X{ explicit X(); explicit X(int, int); }; X x1 = {}; //错误:隐式的 X x2 = {1,2}; //错误:隐式的 X x3{}; //OK:显式的 X x4{1,2}; //OK:显式的 int f(X); int i1 = f({}); //错误:隐式的 int i2 = f({1,2}); //错误:隐式的 int i3 = f(X{}); //OK:显式的 int i4 = f(X{1,2}); //OK:显式的
More:参考链接
mutable:
在const对象中,一个定义为mutable的成员可以被修改:
class Date{ public: //..... string string_rep() const; private: mutable bool cache_valid; mutable string cache; void compute_cache_value() const; //..... }; string Date::Date string_rep() const { if(cache_valid){ compute_cache_value(); cache_valid = true; } return cache; }
此种情况下,string_rep()即可用于const对象,也可用于非const对象。例如:
void f(Date d,const Date cd) { string s1 = d.string_rep(); string s2 = cd.string_rep(); //OK }
this 自引用:
定义状态更新函数ad_year(),add_month() 和 add_day()没有返回值。对这样一组相关的更新函数,通常有用的技术是令它返回已更新对象的引用。例如:
void f(Date& d) { //... d.add_year(1).add_month(1).add_year(1); //... } //为此,必须将每个函数都声明为返回一个Date引用: class Date{ //... Date& add_year(int n); //增加n年 Date& add_month(int n); //增加n个月 Date& add_day(int n); //增加n天 };
每个(非static)成员函数都知道是哪个对象调用的它,并能显式引用这个对象。例如:
Date& Date::add_year(int n) { if(d == 29 && m == 2 && !leapyear(y + n)) //小心2月29 { d = 1; m = 3; } y += n; return *this; }
表达式 *this 引用的就是调用此成员函数的对象。注意:this是一个右值,无法获得this的地址或对其赋值。this的使用大多是隐式的。this的显式应用一种体现是链表操作。例如:
struct Link{ Link* pre; Link* suc; int data; Link* insert(int x) //在this之前插入x { return pre = new Link(pre,this,x); } void remove() //删除并销毁this { if(pre) pre->suc = suc; if(suc) suc->pre = pre; delete this; } };
另外,从一个派生类模板访问基类的成员也要显式使用this。
显式初始化:
C ++支持两种显式初始化形式。
在括号中提供初始化程序列表:
String sFileName(“FILE.DAT”);
括号列表中的项目被认为是类构造函数的参数。这种初始化形式使得能够初始化具有多个值的对象,并且还可以与新的运算符一起使用。例如:
Rect * pRect = new Rect(10,15,24,97);
使用等号初始化语法提供单个初始化器。例如:
String sFileName =“FILE.DAT”;
-
尽管上述示例的工作方式与第一个列表项中的String所示示例相同,但该语法不适用于在自由存储上分配的对象。
等号右侧的单个表达式作为类的复制构造函数的参数; 因此,它必须是可以转换为类类型的类型。
请注意,由于初始化上下文中的等号(=)与赋值运算符不同,因此重载operator =对初始化没有影响。
等号初始化语法与函数式语法不同,即使生成的代码在大多数情况下相同。不同的是,当使用等号语法时,编译器必须像以下事件序列一样运行:
-
创建与正在初始化的对象相同类型的临时对象。
-
将临时对象复制到对象。