C++学习笔记-继承与派生
继承方式分类
公有继承
class Student: public Person // 声明为类Person的公有派生类
{
……
};
当一个类派生自共有基类时, 基类的公有成员是派生类的公有成员, 基类的保护成员是派生类的保护成员,基类的私有成员不能被派生类所直接访问。
私有继承
class Student: private Person // 声明为类Person的私有派生类
{
……
};
基类的保护成员和公有成员都将成为派生类的私有成员
保护继承
class Student: protected Person // 声明为类Person的私有派生类
{
……
};
基类的共有和保护成员都将成为派生类的保护成员
如果不给出继承方式,系统自动判定为私有继承(那就完了)。
派生类的初始化
class Person
{
protected:
char name[18]; // 姓名
int age; // 年龄
char sex[3]; // 性别
public:
Person(char nm[], int ag, char sx[]): age(ag) // 构造函数
{ strcpy(name, nm); strcpy(sex, sx); } // 设置姓名
};
class Student: public Person // 声明为类Person的派生类
{
protected:
int num; // 学号
public:
Student(int n, char nm[], int ag, char sx[]):
Person(nm, ag, sx), num(n) {} // 构造函数
};
派生类具有自己这些函数,派生类不能够继承基类的构造函数(包括拷贝构造函数), 析构函数和重载运算符
派生类的参数初始化表要包含**“基类构造函数名(参数表)”**,表示调用基类的构造函数初始化基类的数据成员(初始化派生类要先初始化基类)
如果基类构造函数是无参构造函数,有默认值的构造函数,那么在初始化派生类的时候可以不不使用基类构造函数
系统在创建派生类对象时,首先去调用基类的构造函数初始化基类数据成员,然后是派生类对象数据的初始化。所以构造函数的执行顺序可以形象地描述为“先执行基类构造函数,后执行派生类构造函数”
同名覆盖原则
派生类中一旦重置基类成员函数,基类中所有同名成员均被覆盖。
这时候如果想调用基类的同名函数,必须在该函数前面加上基类名字和作用域运算符::
派生类对象的析构
析构函数的调用顺序和构造函数相反,先调用派生类的析构函数再调用基类的析构函数
对象参数传递与赋值(赋值兼容原则)
1.参数传递时如果形参是引用或者类对象指针,那么可以传递同类对象和派生类对象,不能传递祖先类对象
2.参数传递时如果形参是类对象(传值), 那么只能传递同类对象,不能传递祖先类对象,也不能传递派生类对象(存在切片问题)关于切片问题
3.在对象赋值时的情况和对象参数传递类似
当基类和派生类有同名函数时利用赋值兼容原则正确调用函数的两个条件:同名函数是虚函数, 且传值或参数传递是遵守以上规则。缺一不可
多级继承
设由类A派生出类B,再由类B派生出类C,这种继承关系称为多级继承
如果B私有继承A后又派生出C,那么A中的公有和保护成员在B中都是私有成员,因此A的成员在C中都是不可访问的,也就是对A功能的继承元函数所访问,因此A的功能可以被C间在B中就终止了
而如果B保护继承A后又派生出C,那么A中的公有和保护成员在B中都是保护成员,A的公有成员和保护成员可以被C的成员函数或友接继承
多继承
如果一个派生类有两个或更多个基类,那么这种行为称为多继承,多继承的派生类的声明一般格式如下:
class 派生类名: 继承方式1 基类名1, 继承方式2 基类名2……
{
};
例如已声明了类A和类B,可按如下方式声明多继承的派生类C:
class C: public A, private B
{
}
构造函数
构造函数初始化时和单继承一样, 不过是多写基类构造函数名1(基类构造函数参数表1), 基类构造函数名2(基类构造函数参数表2),
构造函数和析构函数的执行顺序
各基类可按任意顺序排列,派生类构造函数的执行顺序为:先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序进行调用。析构函数的执行顺序与构造函数的执行顺序正好相反。
多继承引起的多义性问题
菱形继承会引起多义性
B类和C类都从A类派生出来,而D类通过多继承由B与C共同派生。如果类A有成员函数Show(),通过D的对象去访问A类的成员函数Show(),这时由于类B和类C都有继承来自于类A的Show()的副本,编译器无法确定使用哪个副本,从而将发生编译时错误信息
解决方案:(1)添加作用域运算符
派生类对象名.基类名::数据成员名; // 访问数据成员
派生类对象名.基类名::成员函数名(参数表); // 访问成员函数
D obj; // 定义对象
obj.B::Show(); // 输出信息, 指定调用B中来自于A的Show()的副本
system("PAUSE"); // 输出系统提示信息
(2)虚基类
虚基类是解决多义性问题的一种简便而有效的方法。使用虚基类的方法,使得在继承间接共同基类时只保留一份成员副本,这样将不会出现多义性问题
虚基类由关键字virtual标识,一般语法格式如下:
class 派生类名: virtual 继承方式 基类名
虚基类不是在声明基类时声明,而是在声明派生类时,在继承方式前加关键字vitual加以声明。
class A
{
public:
// 公有函数
void Show() const { cout << "多继承示例" << endl; }
};
class B: virtual public A
{ };
class C: virtual public A
{ };
class D: public B, public C
{ };
int main()
{
D obj;
obj.Show();
return 0;
}
虚基类的构造函数先于非虚基类的构造函数执行
虚函数
虚函数要解决的问题是:当基类和派生类中有同名函数或者同名数据时, 而在主函数中又是利用赋值兼容原则赋值基类对象(基类的指针指向派生类地址, 或者引用的传递), 这时被赋值的基类对象在调用同名函数时应该调用派生类的同名函数, 而不是基类的同名函数
在类继承时,可以在派生类中重定义基类函数原型相同的函数,也就是在基类与派生类中可以同时定义函数名相同,参数个数和类型以及返回值类型都相同的函数
由于采用虚函数通过基类指针指向不同类对象实现多态性时,与指针指向的具体对象有关,因此C++规定虚函数不能声明为静态成员函数(因为静态成员函数与具体对象无关)
指针指向一个具体对象后,不能通过指针直接或间接调用构造函数,所以C++还规定构造函数也不能声明为虚函数。
虚函数的声明增加了关键字virtual,成员函数在类体外定义时不加关键字virtual
虚函数在基类声明时一定要加关键字virtual,在派生类中声明虚函数时可以省略关键字virtual
虚析构函数
如果定义了一个指向该基类的指针变量指向new运算符建立的派生类临时对象。在程序中用delete运算符撤销对象时,如果析构函数不是虚函数,由于是基类指针,只是指向派生类对象中的基类部分,从而系统会只执行基类的析构函数,不执行派生类的析构函数
如果将基类的析构函数声明为虚函数,由基类所派生的派生类的析构函数也都自动成为虚函数(即使派生类的析构函数与基类的析构函数名不相同)
在C++程序设计时最好把基类的析构函数都声明为虚函数。这样当在程序中用delete运算符释放一个对象时,系统会调用相应类的析构函数
纯虚函数与抽象类
有时在基类中将某一成员函数声明为虚函数,是考虑到派生类的需要,只在基类中预留了一个函数名,让具体功能在派生类中根据需要去实现。这时适合将基类的成员函数声明为纯虚函数,声明纯虚函数的一般形式是
virtual返回值类型 函数名(形参表) = 0;
纯虚函数没有函数体,所以纯虚函数不能被调用,包含纯虚函数的类是无法建立对象,而只作为一种用作继承的基类,称含有纯虚函数的类为抽象类或抽象基类, 只要含有纯虚函数就是抽象类
虽然抽象类不能定义具体对象,但可以定义抽象类的指针变量,用指针变量指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。也可以通过函数形参是抽象类基类的引用而实参是派生类对象实现多态性