C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。
当派生类继承了基类之后需要在派生类中添加:
1.派生类需要自己的构造函数;
2.派生类可以根据需要添加额外的数据成员和成员函数。
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
在创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类对象过期时,程序首先调用派生类析构函数,然后再调用基类析构函数。
派生类和基类之间的特殊关系
• 派生类可以使用基类的非私有方法。
• 基类指针和引用可以在不进行显示类型转换的情况下指向和引用派生类对象。
• 基类指针和引用只能用于调用基类方法,而不能调用派生类的方法。
• 不可以将基类对象和地址赋给派生类引用和指针。
• 将派生类引用或指针转换为基类引用或指针被称为向上强制转换。
多态公有继承
两种实现多态公有继承的机制:
1.在派生类中重新定义基类的方法;
2.使用虚方法;
使用virtual
如果没有使用virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual程序将根据引用或指针指向的对象的类型来选择方法。
虚析构函数
如果析构函数不是虚的,则将只调用 对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。
静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C++中,由于函数重载的缘故,这项任务更加复杂,然而c/c++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编。又称早期联编。然而虚函数使这项工作变得更困难。因为编译器不知道用户选择哪种类型的对象,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称晚期联编。编译器对非虚方法使用静态联编,对虚方法使用动态联编。由于静态联编效率更高,因此被设置为C++的默认选择。
结合代码理解一些继承特性
#ifndef _TABTENN1_H_
#define _TABTENN1_H_
#include <string>
using std::string;
class TableTennisPlayer
{
string firstname;
string lastname;
unsigned short age;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none", const string & ln = "none",
unsigned short age = 0, bool ht = false);
void Name() const;
bool HasTable() const {return hasTable;};
void ResetTable(bool v) {hasTable = v;};
virtual char HowAge() const;
virtual ~TableTennisPlayer();
//如果析构函数不是虚的,则将只调用对应于指针类型的的析构函数
//如果析构函数是虚的,则将调用相应对象类型的析构函数。
//比如基类指针指向了派生类对象,在delete的时候就会调用派生类的析构函数再调用基类的析构函数从而销毁派生类对象。
//如果不指定为虚的,在delete时,就只会调用基类的析构函数而销毁派生类中的基类部分。
};
class RatedPlayer : public TableTennisPlayer
{
unsigned int rating;
public:
RatedPlayer(unsigned int r = 0, const string & fn = "none", const string & ln = "none",
unsigned short age = 0, bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const {return rating;};
void ResetRating(unsigned int r) {rating = r;};
virtual char HowAge() const;//
};
#endif
#include "tabtenn1.hpp"
#include <iostream>
TableTennisPlayer::TableTennisPlayer(const string & fn,
const string & ln, unsigned short ag, bool ht) : firstname(fn), lastname(ln),
age(ag), hasTable(ht) {}
void TableTennisPlayer::Name() const
{
std::cout << lastname << "," << firstname << std::endl;
}
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, unsigned short ag, bool ht) : TableTennisPlayer(fn, ln, ag, ht)
{
rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : rating(r), TableTennisPlayer(tp) {}
char TableTennisPlayer::HowAge() const
{
std::cout << "TableTennisPlayer: age = " << age << std::endl;
return 0;
}
char RatedPlayer::HowAge() const
{
std::cout << "RatedPlayer: ->";
TableTennisPlayer::HowAge();
//在派生类中不能直接访问基类的私有成员age,但可以调用基类的公有方法,
//这里必须使用作用域解析运算符来调用基类方法,如果没有使用,编译器将认为HowAge()
//是RatedPlayer::HowAge(),这将是一个无限递归循环
return 0;
}
TableTennisPlayer::~TableTennisPlayer()
{
}
#include <iostream>
#include "tabtenn1.hpp"
void show1(TableTennisPlayer & tp)
{
using std::cout;
cout << "Name: ";
tp.Name();
cout << "\nTable: ";
if (tp.HasTable())
cout << "yes\n";
else
cout << "no\n";
}
void show2(TableTennisPlayer * pt)
{
using std::cout;
cout << "Name: ";
pt->Name();
cout << "\nTable: ";
if (pt->HasTable())
cout << "yes\n";
else
cout << "no\n";
}
int main(int argv, char *argc[])
{
using std::cout;
using std::endl;
TableTennisPlayer player1("Tara", "Boomdea", 18, false);//基类
RatedPlayer rplayer1(1450, "Mallory", "Duck", 21, true);//派生类
TableTennisPlayer & rt = rplayer1;//基类引用可以在不进行显示类型转换的情况下引用派生类对象
cout << "基类引用引用派生类对象输出:";
rt.Name();
TableTennisPlayer * pt = &rplayer1;//基类指针可以在不进行显示类型转换的情况下指向派生类对象
cout << "基类指针指向派生类对象输出:";
pt->Name();
//而基类引用和指针只能用于调用基类方法,不能调用派生类方法,所以不可以将基类对象和地址赋给派生类引用和指针
show1(player1);//show形参是基类引用和指针,它们分别可以引用或指向基类对象和派生类对象
show1(rplayer1);
show2(&rplayer1);
show2(&player1);
//引用兼容性可以将基类对象初始化为派生类对象
RatedPlayer lodf1(1456, "cao", "yancheng", 22, true);
TableTennisPlayer lodf2(lodf1);//程序将调用隐式复制构造函数
//也可以将派生类对象赋给基类对象
TableTennisPlayer lodf3 = lodf1;//这种情况下程序将使用隐式重载赋值运算符
rplayer1.Name();//派生类对象调用基类方法
if (rplayer1.HasTable())
{
cout << ":has a table. \n";
}
else
{
cout << ":hasn't a table. \n";
}
player1.Name();
if (player1.HasTable())
cout << ":has a table. \n";
else
cout << ":hasn't a table. \n";
cout << "Name: ";
rplayer1.Name();
cout << "; Rating: " << rplayer1.Rating() << endl;
RatedPlayer rplayer2(1234, player1);//引用基类对象初始化派生类
cout << "Name: ";
rplayer2.Name();
cout << "; Rating: " << rplayer2.Rating() << endl;
//对于基类和派生类中同名方法virtual HowAge(),如果在两个类中实现的功能一样,则只在基类中声明;
player1.HowAge();//对象调用时,使用对象本身定义的方法;
rplayer1.HowAge();
TableTennisPlayer & ttp1 = player1;
TableTennisPlayer & ttp2 = rplayer1;
TableTennisPlayer * m1_pt = &player1;
TableTennisPlayer * m2_pt = &rplayer1;
//如果没有使用virtual,程序将根据引用类型或指针类型选择方法;
ttp1.HowAge();//TableTennisPlayer::HowAge()
ttp2.HowAge();//TableTennisPlayer::HowAge()
//如果使用了virtual,程序将根据引用或指针指向的对象类型来选择方法;
m1_pt->HowAge();//TableTennisPlayer::HowAge()
m2_pt->HowAge();//RatedPlayer::HowAge()
getchar();
return 0;
}
有关虚函数注意事项
1.构造函数
构造函数不能是虚函数,派生类不继承基类的构造函数。
2.析构函数
析构函数应当是虚函数,除非类不做基类。通常应给基类提供一个虚析构函数。
3.友元
友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
4.没有重新定义
如果派生类没有重新定义虚函数,将使用该函数的基类版本。
5.重新定义将隐藏方法
class Base
{
virtual void show(int a) const;
...
};
class Hovel : public Base
{
virtual void show() const;
...
}
重新定义不会生成函数的两个重载版本,而是隐藏了基类中的版本。总之,重新定义继承的方法并不是重载。
如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类指针或引用,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变。
如果基类声明的方法被重载了,则应在派生类中重新定义所有的基类版本。如果没有定义,派生类将无法使用它们。
访问控制:protected
派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。对于外部世界来说保护成员的行为与私有成员相似,但对派生类来说,保护成员的行为与公有成员相似。将基类成员设置为私有的可以提高安全性,而将它们设置为保护成员则可简化代码编写工作,并提高访问速度。
抽象基类
当类声明中包含纯虚函数时,则不能创建该类的对象。也就是说包含纯虚函数的类只用作基类。
纯虚函数的定义:
virtual double Area() const = 0;
技巧:使用基类指针数组可以同时管理多个基类的 派生类对象。
继承和动态内存分配
class BaseDMA
{
char * lable;
int rating;
public:
BaseDMA(const char * l = "null", int r = 0);
BaseDMA(const BaseDMA & rs);//复制构造
virtual ~BaseDMA();
BaseDMA & operator=(const BaseDMA & rs);//重载赋值
};
class LacksDMA : public BaseDMA
{
char color[40];
...
}
基类使用了动态内存分配,并重新定义赋值和复制构造函数,将对派生类产生如下影响:
1.派生类不使用new时
不需要为LacksDMA类定义显示析构函数、复制构造函数和赋值运算符。
成员复制将根据数据类型采用相应的复制方式。默认数据类型通过常规赋值完成,但复制类成员或继承类组件时,则是使用该类的复制构造函数完成的。也就是BaseDMA的复制构造函数。对于赋值来说也一样使用基类的赋值运算符来对基类组件进行赋值。
2.派生类使用了new
class HasDMA : public BaseDMA
{
char * style;
...
}
这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。派生类析构函数自动调用基类的析构函数。所以HasDMA 析构函数必须释放指针style管理的内存,并依赖于BaseDMA的析构函数来释放指针lable管理的内存。
HasDMA复制构造函数只能访问HasDMA的数据,因此它必须调用BaseDMA复制构造函数来处理共享的HasDMA数据。
HasDMA::HasDMA(const HasDMA & hs) : BaseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(sytle, hs.style)
}
派生类的显式赋值运算符必须负责所有继承的BaseDMA基类对象的赋值,可以通过显式调用基类赋值运算符来完成
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;
}
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显示的调用基类的赋值运算符来完成的。
含有友元的基类继承
因为友元不是成员函数,所以不能通过作用域解析运算符来指出要使用哪个函数,这个问题的解决方法是使用强制类型转换。转换成基类类型之后就可以调用基类中的友元函数。
运算符dynamic_cast<>可以进行强制类型转换。
os << dynamic_cast<const BaseDMA &> (hs);
类设计总结:
编译器生成的成员函数
1.默认构造函数;
默认构造函数要么没有参数,要么所有参数都有默认值。自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数,以及调用本身是对象的成员所属类的默认构造函数。
如果派生类构造函数的成员初始化列表没有显示调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。
2.复制构造函数;
复制构造函数接受其所属类的对象作为参数。
在下述情况下,将使用复制构造函数:
• 将新的对象初始化为一个同类对象;
• 按值将对象传递给函数;
• 函数按值返回对象;
• 编译器生成临时对象;
如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
在有些情况下,成员初始化是不合适的,例如使用new初始化的成员指针,或者类包含需要修改的静态变量。在上述情况下,需要定义自己的复制构造函数。
3.赋值运算符;
默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值和初始化混淆。如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。
默认赋值为成员赋值,如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。如果需要显示定义复制构造函数,那么基于同样的原因,也需要显示定义赋值运算符。
其它的类方法
1.构造函数
构造函数不能被继承。构造函数在完成其工作之前对象并不存在。
2.析构函数
一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需要的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
3.转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
要将类对象转换为其它类型,应定义转换函数。
4.按值传递对象和传递引用
通常编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一就是可以提高效率,因为按值传递需要生成临时拷贝,调用复制构造函数,和析构函数。如果函数不修改对象,应将参数声明为const引用。
按引用传递的另一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
5.返回对象和返回引用
如果函数可以不返回对象,则应返回引用。直接返回对象和按值传递类似;它们都生成临时副本。
函数不能返回在函数中创建的临时对象的引用。
6.使用const
使用const时特别注意,可以用它来确保方法不修改参数。
Star::Star(const char * s) {...}
也可以使用const来确保方法不修改调用它的对象。
void Star::show() const {...}