面向对象程序设计的核心思想是数据抽象、继承和动态绑定(也称之为动态联编)。通过数据抽象将类的接口与实现分离;使用继承可以定义相似的类型并对相似的关系建模;使用动态绑定可以在一定程度上忽视类型的区别,使用统一的方式使用他们的对象。
类是C++实现面向对象编程的手段,一个类把一类事物的相同属性通过数据成员和成员函数的形式囊括起来,方便直接使用。前面的几篇博文中讲述了关于类的一些基本性质,今天介绍一下类的继承。
某个学校为了统计学生信息(包括姓名、年纪)设计了一个Student类,这个类的成员变量包括姓名和年纪。而生物医学工程系也想统计自己学院学生的信息(包括姓名、年纪、年级、加权成绩),这是就可以利用类的继承,通过BME_Student类继承Student类,只需要在BME_Student类中添加年级、加权成绩这两个属性即可,这无疑减小了工作量。
上面只是一个举例,是想说明当遇到需要扩展某一个类的功能时,C++提供了一种比修改和扩展类更好的方法叫做继承。下面我们还是以这两个类为例进行介绍。
被继承的类称为基类,也可以叫父类。对应地,直接或者间接从基类继承得到的类称为派生类,也可以称之为子类。一般基类比较抽象,用于定义其所有子类的公共属性,派生类继承了这些公有属性并且添加自己的属性从而使特征和功能更加具体化。
在C++中支持一个派生类继承多个基类,其形式如下;
class 派生类名: 派生方式基类名1, 派生方式 基类名2,...
{
数据成员和成员函数声明
};
这里的派生方式包括公有的派生(public)、私有的派生(private)、保护的派生(protect)。无论哪种派生方式,基类中的private成员在派生类中都是不可见的,基类中的private成员不允许外部函数或派生类中的任何成员访问,派生类想要访问基类的私有成员只能通过基类的公有和保护的方法访问。
公用的派生方式表示派生类从基类公有地继承,基类中的成员在派生类中的访问属性除了私有的不可见以外,其他的保持不变;而私有的继承方式,让基类中公有成员和保护成员在派生类中都变成私有的;保护的继承方式让基类中公有成员和保护成员在派生类中都变成受保护的。具体的访问形式如下面的表格所示。
派生方式 |
Private |
Public |
Protect |
||||||
基类成员 |
private |
public |
protect |
private |
public |
protect |
private |
public |
protect |
派生类 |
不可见 |
可见 (private) |
可见 (private) |
不可见 |
可见 (public) |
可见 (protect) |
不可见 |
可见 (protect) |
可见 (protect) |
外部函数 |
不可见 |
不可见 |
不可见 |
不可见 |
可见 |
不可见 |
不可见 |
不可见 |
不可见 |
Student类记录了学生的姓名及年龄。另外还定义了一个公有的成员函数show();
class Student
{
public:
Student();
Student(string fn,string ln,int ages);
~Student();
Student(const Student &stu);
void show();
//int s = 5;
private:
string firstname="Zhengyu";
string lastname="Pan";
int age=20;
};
//----------实现基类的函数;
Student::Student()
{
cout << "调用基类默认构造函数" << endl;
}
Student::Student(string fn, string ln, int ages) :firstname(fn), lastname(ln), age(ages)
{
cout << "调用基类构造函数" << endl;
}
Student::~Student()
{
cout << "调用基类析构函数" << endl;
}
Student::Student(const Student &stu)
{
firstname = stu.firstname;
lastname = stu.lastname;
age = stu.age;
cout << "调用拷贝构造函数" << endl;
}
void Student::show()
{
cout << "学生 " << lastname << " " << firstname <<" "<<"的年龄是 "<
class BME_Student
:public Student
{
public:
BME_Student();
~BME_Student();
BME_Student(int grade, double score, string fn, string ln, int ages);
void show();
private:
int grade_rank=2; //添加新的数据成员年级和加权成绩
double weight_score=90.0;
};
BME_Student::BME_Student()
{
cout << "调用派生类默认构造函数" << endl;
}
BME_Student::~BME_Student()
{
cout << "调用派生类析构函数" << endl;
}
BME_Student::BME_Student(int grade, double score, string fn, string ln, int ages)
:grade_rank(grade), weight_score(score), Student(fn,ln,ages)
{
cout << "调用派生类的构造函数" << endl;
}
void BME_Student::show()
{
Student::show();
cout << "年级是 " << grade_rank << "加权成绩是 " << weight_score << endl;
}
在这里BME_Student类公有地继承Student类,所以BME_Student类对象具有了Student类的属性(即数据成员),并且可以使用Student类的成员函数,说明了一下两点;
派生类对象储存了基类的数据成员(派生类继承了基类的实现);
派生类对象可以使用基类的方法(派生类继承了基类的接口)。
在上述代码中,BME_Student类添加了自己新增的数据成员(年级、加权成绩),并且也定义了自身的构造函数,析构函数及show()函数。
在创建一个派生类的对象时,程序首先创建基类的对象,如上面代码中BME_Student类的构造函数所示,通过显式地调用Student类的构造函数进行基类成员的初始化;
Student(fn,ln,ages)
如果不显式地调用基类的构造函数,则派生类的构造函数会自动调用基类的默认构造函数进行基类成员的初始化。总的来说,派生类的构造函数有以下几个要点;
1. 首先创建基类对象(首先调用基类构造函数初始化基类成员);
2. 通过成员初始化列表将基类信息传递给基类构造函数;
3. 应该初始化自身新增的数据成员。
相对应的,在对派生类对象过期时,首先调用派生类析构函数,然后再调用基类析构函数,这与构造函数的调用是一个相反的过程。
在生成派生类对象时,派生类构造函数的执行顺序如下:
- 调用基类构造函数;
- 调用数据成员中类对象的构造函数;
- 调用子类的构造函数;
1. 派生类对象可以使用基类的方法,条件是方法不是私有的。
2. 基类指针可以指向派生类对象,派生类指针不可以指向基类对象。因为当派生类指针指向基类对象时,如果利用指针调用派生类的方法往往是非法的,因为派生类中新增了一些方法,基类对象不能使用。
3. 与2 中同样的道理,基类引用可以在不进行类型转换的情况下引用派生类对象。派生类引用不能引用基类对象。
在上面的代码中,我们给基类和派生类分别定义了show()函数,当使用两个类的对象调用show()函数时,程序会自动调用对应的函数。但是有一种情况会出乎我们的意料,当使用基类的指针或引用作用于派生类的对象时,这时候调用的是基类的show()函数,明显不满足我们的要求。
BME_Student PAN;
Student *p_Stu=&PAN;
p_Stu->show(); //此时调用基类的show()函数
这时候就需要虚函数。
virtual void fun();
虚函数在类中声明时在函数前加上了virtual关键字,当他在类外实现时,不需要再添加这个关键字。
void Student::fun()
{
函数体;
}
- 虚函数并不是说这个函数可以不被实现,定义虚函数是为了允许用基类的指针来调用子类的这个函数,实现动态多态,要与纯虚函数分开。
- 某成员函数在基类中被定义为虚函数,在派生类中该函数无需添加virtual也默认为虚函数;若没有在派生类重新定义,则将使用基类的版本;
- 构造函数不能使虚函数,析构函数应当是虚函数。因为,如果基类指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。如果析构函数不是虚函数,则不会调用派生类的虚函数。而且,当基类的析构函数声明为虚函数后,即便子类的析构函数与其名字不同,也一样自动成为虚函数。
- 友元不是成员函数,只有成员函数才可以是虚的,因此友元不能是虚函数。但可以通过让友元函数调用虚成员函数来解决友元的虚问题。
- 只有通过指针和引用才能展现虚函数的特性。
- 重新定义虚函数不是重载,而是覆盖(或是隐藏),即使派生类中重新定义了虚函数,无论函数列别是否相同,都将隐藏同名的基类方法。所以当基类中虚函数被重载时,在派生类中应该定义所有的版本。
- 静态成员函数不能是虚函数; 内联函数不能为虚函数;
成员函数被重载是发生在同一个类中,特征是:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类函数必须有virtual 关键字。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
将源代码中的函数调用解释为特定函数代码块的行为被称为函数名联编(binding),在C++中,编译器必须查看函数参数以及函数名才能确定使用哪个函数,编译器在编译过程完成联编,在编译过程中完成的联编称为静态联编(static binding)。然而虚函数无法在编译时确定使用哪个函数,所以编译器只能在程序运行时选择正确的虚函数代码,这种联编称为动态联编(dynamic binding)。
注意:动态联编发生在基类指针调用虚函数时,因为此时无法判断调用的是哪个函数,但是当对象调用虚函数是,编译器可以直接判断出调用哪个函数,因此是静态联编。
/*
//---head01.h
//---类的继承与虚函数
//---不用先生2018.04.21
*/
#ifndef HEAD01_H
#define HEAD01_H
#include
#include
using namespace std;
//-----------基类为学生类--------------//
class Student
{
public:
Student();
Student(string fn,string ln,int ages);
virtual ~Student();
Student(const Student &stu);
virtual void show();
//int s = 5;
private:
string firstname="Zhengyu";
string lastname="Pan";
int age=20;
};
//----------实现基类的函数;
Student::Student()
{
cout << "调用基类默认构造函数" << endl;
}
Student::Student(string fn, string ln, int ages) :firstname(fn), lastname(ln), age(ages)
{
cout << "调用基类构造函数" << endl;
}
Student::~Student()
{
cout << "调用基类析构函数" << endl;
}
Student::Student(const Student &stu)
{
firstname = stu.firstname;
lastname = stu.lastname;
age = stu.age;
cout << "调用拷贝构造函数" << endl;
}
void Student::show()
{
cout << "学生 " << lastname << " " << firstname <<" "<<"的年龄是 "<
/*
//---test.cpp
//---类的继承与虚函数
//---不用先生2018.04.21
*/
#include
#include "head01.h"
using namespace std;
void main()
{
{
BME_Student PAN;
BME_Student NIHONG(3, 83.2, "Hong", "Ni", 25);
PAN.show();
NIHONG.show();
Student *p_Stu=&PAN;
p_Stu->show();
}
{
Student ZhangZou("Zhang", "Zou", 24);
ZhangZou.show();
}
system("pause");
return;
}
从结果中不难看出,在创建派生类对象时,首先调用基类构造函数,然后再调用派生类构造函数,而在进行析构时,先调用派生类对象,再调用基类构造函数。
因为是公用的继承,所以Student类中的show()函数可以直接被BME_Student类中的show( )函数调用,但是一定要注以Student::,否则将陷入无限循环。
由于show( )被定义为虚函数,所以指针p_Stu调用的是派生类的show()函数。
已完。。
参考书籍《C++ Primer 第五版》、《C++ Primer Plus 第六版》
【C++】C++类的学习(一)——初识类
【C++】C++类的学习(二)——构造函数、析构函数、拷贝构造函数以及this指针
【C++】C++类的学习(三)——运算符重载与友元函数
【C++】C++类的学习(五)——纯虚函数与模板类