- 该系列文章大部分摘自博主白鳯《C++面向对象程序设计》✍千处细节、万字总结(建议收藏)
http://t.csdn.cn/GxZ6U
- 如有不好的影响请联系删除
继承与派生的概念
类的继承是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。类的继承和派生机制较好地解决了代码重用的问题。
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
#include <iostream>
#include <string>
using namespace std;
class Person{
private:
string name;
string id_number;
int age;
public:
Person(string name1, string id_number1, int age1) {
name = name1;
id_number = id_number1;
age = age1;
}
~Person() {
}
void show() {
cout << "姓名: " << name << " 身份证号: " << id_number << " 年龄: " << age << endl;
}
};
class Student:public Person{
private:
int credit;
public:
Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
credit = credit1;
}
~Student() {
}
void show() {
Person::show();
cout << "学分: " << credit << endl;
}
};
int main() {
Student stu("白", "110103**********23", 12, 123);
stu.show();
return 0;
}
从已有类派生出新类时,可以在派生类内完成以下几种功能:
- 可以增加新的数据成员和成员函数
- 可以对基类的成员进行重定义
- 可以改变基类成员在派生类中的访问属性
基类成员在派生类中的访问属性
派生类可以继承基类中除了构造函数与析构函数之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性也有所不同。
|基类中的成员 |继承方式 |基类在派生类中的访问属性|
|private |public、protected、private| 不可直接访问|
|public |public、protected、private| public、protected、private|
|protected |public、protected、private| protected、protected、private|
派生类对基类成员的访问规则
基类的成员可以有public、protected、private
3中访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private
3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。
派生类对基类成员的访问形式主要有以下两种:
- 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
- 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
派生类的构造函数和析构函数
构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。
调用顺序
#include <iostream>
#include <string>
using namespace std;
class A{
public:
A() {
cout << "A类对象构造中..." << endl;
}
~A() {
cout << "析构A类对象..." << endl;
}
};
class B : public A{
public:
B() {
cout << "B类对象构造中..." << endl;
}
~B(){
cout << "析构B类对象..." << endl;
}
};
int main() {
B b;
return 0;
}
代码运行结果如下
A类对象构造中...
B类对象构造中...
析构B类对象...
析构A类对象...
可见:构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
派生类构造函数和析构函数的构造规则
派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
派生类新增成员的初始化语句
}
在定义派生类对象时,构造函数的调用顺序如下:
-
调用基类的构造函数,对基类数据成员初始化。
-
调用子对象的构造函数,对子对象的数据成员初始化。
-
调用派生类的构造函数体,对派生类的数据成员初始化。
说明:
- 当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
- 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
- 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。
调整基类成员在派生类中的访问属性的其他方法
派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“::”,即
基类名::成员名
访问声明
访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。
class B:private A{
private:
int y;
public:
B(int x1, int y1) : A(x1) {
y = y1;
}
A::show; //访问声明
};
注意:
- 数据成员也可以使用访问声明。
- 访问声明中只含不带类型和参数的函数名或变量名。
- 访问声明不能改变成员在基类中的访问属性。
- 对于基类的重载函数名,访问声明将对基类中所有同名函数其起作用。
多继承
声明多继承派生类的一般形式如下:
class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n {
派生类新增的数据成员和成员函数
};
默认的继承方式是private
多继承派生类的构造函数与析构函数:
与单继承派生类构造函数相同,多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。
多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。
虚基类
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
#include <iostream>
#include <string>
using namespace std;
class Base{
protected:
int a;
public:
Base(){
a = 5;
cout << "Base a = " << a << endl;
}
};
class Base1: public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2: public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
class Derived: public Base1, public Base2{
public:
Derived() {
cout << "Base1::a = " << Base1::a << endl;
cout << "Base2::a = " << Base2::a << endl;
}
};
int main() {
Derived obj;
return 0;
}
代码执行结果如下
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25
虚基类的初始化
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:
- 如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
- 建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
- 若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
- 对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
- 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
赋值兼容规则
在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用子类的对象代替。
class Base{
·····
};
class Derived: public Base{
·····
};
根据赋值兼容规则,在基类Base的对象可以使用的任何地方,都可以使用派生类Derived的对象来代替,但只能使用从基类继承来的成员。具体的表现在以下几个方面:
- 派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员。
Base b;
Derived d;
b = d;
- 派生类对象可以初始化基类对象的引用。
Derived d;
Base &br = d;
- 派生类对象的地址可以赋值给指向基类对象的指针
Derived d;
Base *bp = &d;