编译器生成的成员函数
-
默认构造函数
默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,则编译器将定义默认构造函数,让您能够创建对象。
自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属的类的默认构造函数。
另外,如果派生类构造函数的成员初始化列表中没有显示调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有默认构造函数,将导致编译阶段出错。
如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
提供构造函数的动机之一是确保对象总能被正确的初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显示默认构造函数,将所有的类数据成员都初始化为合理的值。
-
复制构造函数
复制构造函数接收其所属类的对象作为参数。例如,Star类的复制构造函数的原型如下:
Star(const Star&);在下述情况下,将使用复制构造函数:
将新对象初始化为一个同类对象;
按值将对象传递给函数;
函数按值返回对象;
编译器生成临时对象;
如果程序没有使用(显示或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否i则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
在某些情况下,成员初始化时不合适的。例如,使用new初始化的成员指针通常要求深度复制。或者,类可能包含需要修改的静态变量。在上述情形下,需要定义自己的复制构造函数。
-
赋值运算符
默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有的对象,则是赋值:
Star sirius; Star alpha = sirius; Star dogstar; dogstar = sirius;默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显示定义复制构造函数,则基于相同的原因,也需要显示定义赋值运算符。Star类的赋值运算符的原型如下:
Star& Star::operator=(const Star&);赋值运算符函数返回一个Star对象引用,baseDMA类演示一个典型的显式赋值运算符实例。
编译器不会生成将一种类型赋给另一个类型的赋值运算符。如果希望将字符串赋给Star对象,则方法之一时显示定义下面的运算符:
Star& Star::operator(const char*);另一种方法是使用转换函数,将字符串转换成Star对象,然后使用将Star赋给Star的赋值函数。第一种方法的运行速度比较快,但需要的代码较多,而是用转换函数可能导致编译器出现混乱。
其他的类方法
定义类时,还需要注意其他几点:
-
构造函数
构造函数不同于其它类方法,因为他创建新的对象,而其他类方法指示被现有的对象调用。这是构造函数不能被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。
-
析构函数
一定要定义显示析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊清理工作。对于基类,即使它并不需要析构函数,也应提供一个虚虚构函数。
-
转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。例如,下述Star类的构造函数原型:
Star(const char*); Star(const Spectral&,int members=1);将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。例如,在下面代码中:
Star north; north="polaris";第二条语句将调用Star::operator=(const Star&)函数,使用Star::Star(const char)生成一个Star对象,该对象将被用作上述赋值运算符函数的参数。这里假设没有定义将char * 赋给star的赋值运算符。
在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显示转换:
clss Star { ... public: explicit Star(const char*); ... }; ... Star north; north="polaris"//not allowed; north = Star("polaris");//allowed;要将类转换为其他来行,应定义转化函数。转换函数可以是没有参数的类成员函数,也可以时返回类型被声明为目标类型的成员函数。即使没有声明返回类型,函数也应返回所需的转换值。下面是一些实例:
Star::operator double(){...} Star::operator const char*(){...}应理智的使用这样的函数,仅当他们有帮助时才使用。另外,对于某些类,包含转换函数将增加代码的二义性。例如,假设已经在第11章的Vector类型定义了double转换,并编写了下面的代码:
Vector ius(6.0,0.0); Vector lux=ius + 20.2;编译器可以将ius转换为double并使用double加法,或将20.2转换成vector(使用构造函数之一)并使用vector加法。但除了指出二义性之外,他什么也不做。
C++11支持将关键字explicit用于转换函数。与构造函数一样,explicit允许使用强制类型转换进行显示转换,但不允许隐式转换。
-
按值传递对象与传递引用
通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一为了提高效率。按值传递对象涉及到生成临时拷贝、即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多。如果函数不修改对象,应将参数声明为const引用。
按引用传递对象的另外一个原因为:在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
-
返回对象和返回引用
有些方法返回对象。您可能注意到了,有些成员函数直接返回对象,而另一些则返回引用。又是方法必须返回对象,但如果可以不返回对象,则应返回引用。具体来看一下:
首先,在编码方面,直接返回对象与返回引用之间的唯一区别在于函数原型和函数头。
其次,应返回引用而不是返回对象的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可以节省时间和内存。直接返回对象与按值传递对象相似:他们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被掉用的函数对同一个对象进行操作。
然而,并不总是可以返回引用。函数不能返回在函数中创建的临时变量的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。
通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。下面的方法是使用构造函数创建一个新对象,然后返回该对象的副本:
Vector Vector::operator+(const Vector &b)const { return Vector(x+b.x,y+b.y); }如果函数返回的通过引用或指针传递给他的对象,则应按引用返回对象。例如,下面的代码按引用返回调用函数的对象或作为参数传递给函数的对象:
const Stock& Stock::topval(const Stock &s)const { if(s.total_val>total_val) return s; else return *this; }
-
使用const
使用const时应特别注意。可以用它来确保方法不会修改参数;
可以使用const来确保方法不会修改调用它的对象;
通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象。但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据:
const Stock& Stock::topval(const Stock &s)const { if(s.total_val>total_val) return s; else return *this; }该方法返回对*this或s的引用。因为 *this和s都被声明为const,所以函数不能对他们进行修改,这意味着返回的引用也必须声明为const。
注意,如果函数将参数声明为指向const的引用或指针,则不能将参数传递给另一个参数,除非后则会也确保了参数不会被修改。
公有继承的考虑因素
通常,在程序中使用继承时,有很多问题需要注意。
-
is-a关系
要遵循is-a关系。如果派生类不是一种特殊的基类,则不要使用公有派生。例如,不应从Brain类派生出Programmer类。如果要指出程序员有大脑,应将Brain类对象作为Progarmmer类的成员。
在某些情况下,最好的方法可能是创建包含纯虚函数的抽象数据类,并从他派生出其他的类。
请记住,表示is-a关系的方式之一是,无需i及逆行显式类型转换,基类指针就可以指向派生类对象,基类引用就可以引用派生类对象。另外,反过来是·行不通的,既不能在不进行显示类型转换的情况下,及那个派生类指针或引用指向基类对象。这种显式类型转换(向下强制转换)可能有意义,也可能没有,这取决于类声明。
-
什么不能被继承
构造函数是不能继承的,也就是说,创建派生类对象时,必须调用派生类的构造函数。然而,派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。如果派生类构造函数没有使用成员初始化列表语法显式调用基类构造函数,将使用基类的默认构造函数。在继承链中,每个类都可以使用成员初始化列表将传递给相邻的基类。C++11新增了一种让您能够继承构造函数的机制,但默认仍不能继承构造函数。
析构函数也是不能继承的。然而,在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚的。
赋值运算符是不能继承的。原因很简单。派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。赋值运算符确实有一些有趣的特征,下面介绍他们。
-
赋值运算符
如果编译器发现程序将一个对象赋给同一个类的另一个对象,他将自动为这个类提供一个赋值运算符。这个运算符的默认或隐式版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。然而,如果对象属于派生类,编译器将使用基类赋值运算符来处理派生对象中基类部分的赋值。如果显示的为基类提供了赋值运算符,将使用该运算符。与此相似,如果成员是另一个里的对象,则对于该成员,将使用其所属的赋值运算符。
正如多次提到的,如果类构造函数使用new来初始化指针,则需要提供一个显示赋值运算符。因为对于派生对象的基类部分,C++将使用基类的赋值运算符,所以不需要为派生类重新定义赋值运算符,除非它添加了需要特备留意的数据成员。例如,baseDMA类显示地定义了赋值,但派生类lackDMA使用为它生成的隐式赋值运算符。
然而,如果派生类使用了new,则必须提供显示运算符。必须为类的每个成员提供赋值运算符,而不仅仅是新成员。
hasDMA & hasDMA::operator(const hasDMA & hs) { if(this==&hs) return *this; baseDMA::operator=(hs); delete[]style; style =new char[std::strlen(hs.style)+1]; std::strcpy(style,hs.style); return *this; }将派生类对象赋给基类对象将会如何呢?请看下面的例子:
Brass blips; BrassPuls snips("Rafe Plosh",91191,3993.19,600.0,0.12); blips=snips;这将使用那个赋值运算符呢?赋值语句将被转换成左边的对象调用的一个方法:
blips.operator=(snips);其中左边的对象是Brass对象,因此他将调用Brass::operator=(const Brass&)函数。is-a关系允许Brass引用指向派生类对象,如snips。赋值运算符只处理基类成员,所以上述赋值操作将忽略Snips的maxLoan成员和其他BrassPlus成员。总之,可以将派生类对象付给基类对象,但这只设计基类成员。
如果将基类对象赋给派生类对象则不可行,除非有下面的转换函数;
BrassPlus(const Brass& );与BrassPlus类的情况相同,转换构造函数可以接受一个类型为基类的参数和其他参函数,条件是其他参数有默认值:
BrassPlus(const Brass& ba,doubel m1=500,double r=0.1);如果有转换构造函数,程序将通过它根据gp来创建一个临时BrassPlus对象,然后将他作用赋值运算符。
另一种方法是,定义一个用于将基类赋给派生类的赋值运算符:
BrassPlus & BrassPlus::operator=(const Brass&){...}该赋值运算符的类型与赋值语句完全匹配,因此无须进行类型转化。
-
虚方法
设计类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用晚期联编(动态联编);如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新方法,但表达了这样的意思:您不希望他被重新定义。
请注意,不适当的代码将阻止动态联编。例如,请看下面的两个函数:
void show(const Brass& rba) { rba.ViewAcct(); cout<<endl; } void inadequte(Brass ba) { ba.ViewAcct(); cout<<endl; }第一个函数按引用传递参数,第二个按值传递参数。
现在,假设将派生类参数传递给上述两个参数:
BrassPlus buzz("Buzz Parsec",00001111,4300); show(buzz); inadequate(buzz);show()函数调用使参数成为BrassPlus对象buzz的引用,因此,rab.ViewAcct()被解释为BrassPlus版本,正如应该的那样。但在inadequte()函数中,ba是Brass(const Brass&)构造函数创建的一个Brass对象。因此,在inadequte()中,ba.ViewAcct()使用的是Brass版本,所以只有buzz的Brass部分被显示。
-
析构函数
正如前面介绍的那样,基类的析构函数应当是虚的。这样,当通过指向对象的基类指针或引用来删除派生对象是,对象将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。
-
友元函数
由于友元函数并非是类成员,因此不能继承。然而,您可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数:
ostream & operator<<(ostream& os,const hadDMA& hs) { os(const baseDMA &)hs; os<<"Style: "<<hs.style<<endl; return os; }
-
有关使用基类方法的说明
以共有方式派生的类的对象可以通过多种方式来使用基类的方法。
-
派生类对象自动使用继承而来的基类方法,如果派生类没有重新定义该方法。
-
派生类的构造函数自动调用基类的构造函数。
-
派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。
-
派生类构造函数显示调用成员初始化列表中指定的基类构造函数。
-
派生类方法可以使用作用域解析符来调用公有的和受保护的基类方法。
-
派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数。