类继承
整理自《C++ Primer Plus》
1. 一个简单的基类
- 从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
- 公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
- 需要在继承特性中添加什么呢?
派生类需要自己的构造函数。
派生类可以根据需要添加额外的数据成员和成员函数。 - 构造函数:访问权限的考虑 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类构造函数必须使用基类构造函数。
派生类构造函数的要点如下:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员。
注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数
2.多态公有继承
当希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可用于实现多态公有继承:
-
在派生类中重新定义基类的方法
-
使用虚方法
virtual关键字。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
基类声明了一个虚析构函数。这样做是为了确保释放对象时,按正确的顺序调用析构函数。
注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。 -
为何需要虚析构函数
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。因此,虚析构函数可以确保正确的析构函数序列被调用。
3.静态联编和动态联编
在编译过程中进行联编称为静态联编,又称为早期联编。然而,虚函数使这项工作变得更困难。因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。指向基类的引用或指针
- 虚成员函数和动态联编
BrassPlus ophelia; // derived-class object
Brass * bp; // base-class pointer
bp = &ophelia; // Brass pointer to BrassPlus object
bp->ViewAcct(); // which version
// 编译器对虚方法使用动态联编
为什么有两种类型的联编以及为什么默认为静态联编?
如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个——效率和概念模型。
提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
虚函数的工作原理
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。
调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向对应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中第三个虚函数,程序将使用地址为数组中第三个元素的函数。
总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
每个对象都将增大,增大量为存储地址的空间
对于每个类,编译器都创建一个虚函数地址表(数组);
对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
- 有关虚函数注意事项
-
在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
-
如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
-
如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
-
构造函数。构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
-
析构函数。析构函数应当是虚函数,除非类不用做基类。
-
友元。友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
-
4. 访问控制:protected
关键字protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
5. 抽象基类(abstract base class,ABC)
C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数的结尾处为=0。
当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包括一个纯虚函数。
6. 继承和动态内存分配
-
第一种情况:派生类不适用new
不需要为派生类定义析构函数,复制构造函数和赋值运算符。 -
第二种情况:派生类使用new
在这种情况下,必须为派生类定义显示析构函数,复制构造函数和赋值运算符。
派生类析构函数自动调用基类的构造函数,故其自身的职责是对派生类构造函数执行工作进行清理
7. 类设计回顾
-
编译器生成的成员函数
-
默认构造函数
默认构造函数要么没有参数,要么所有参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,让您能够创建对象。
自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
另外,如果派生类构造函数的成员初始化列表中没有显示调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译错误。
如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
提供构造函数的动机之一是确保对象总能正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显示默认构造函数,将所有的类数据成员都初始化为合理的值。 -
复制构造函数
复制构造函数接受其所属类的对象作为参数。
在下述情况下,将使用复制构造函数:
将新对象初始化为一个同类对象
按值将对象传递给函数
函数按值返回对象
编译器生成临时对象。如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,此程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
-
赋值运算符
默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值:
-
Star sirius;
Star alpha = sirius; // initialization
Star dogstar;
dogstar = sirius; // assignment
-
其他的类方法
-
构造函数
构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。 -
析构函数
一定要定义显示析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。 -
转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
-
Star(const char *); // convert char* to Star
Star(const Spectral &, int members = 1); // convert Spectral to Star
-
按值传递对象与传递引用
通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。 -
返回对象和返回引用
有时方法必须返回对象,但如果可以不返回对象了,则应返回引用。
首先,在编码方面,直接返回对象和返回引用之间的唯一区别在于函数原型和函数头。
其次,应返回引用而不是返回对象的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间成本包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可节省时间和内存。
然而,并不总是可以返回引用。函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。在这种情况下,应返回对象,以生成一个调用程序可以使用的副本。
通用的规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。 -
使用const
使用const可以确保方法不修改参数:
Star::Star( const char * s) {......} // won't change the string to which s points
使用const来确保方法不修改调用它的对象:
void Star::show() const {......} // won't change invoking object
这里const表示const Star * this,而this指向调用的对象。
通常,可以将返回引用的函数放在赋值语句的左侧,这实际意味着可以将值赋给引用的对象。但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据:
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s; // argument object
else
return *this; // involking object
}
该方法返回对this或s的引用。因为this和s都被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const。
注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。