类的继承与派生,继承与派生只是从不同的角度来说而已,其本质是一样的。在子类向父类的角度来看,就是子类继承于父类,而在父类向子类的角度来看,就是父类派生出子类。其中被继承的已有类被称为“基类”或“父类”,派生出的新类称为派生类。
而继承的目的是为了实现代码重用,而派生的目的是,当新问题出现时,原有的程序无法解决或不能完全解决问题,此时需要对原有的程序进行改造,以适应新的要求。
继承方式
派生类的声明:class 派生类名: 继承方式 基类名{ 派生类的主体 };
其中继承方式分为三种:公有继承、私有继承、保护继承
而不同的继承方式的影响有1、派生类成员对基类成员的访问权限;2、通过派生类对象对基类成员的访问权限
1、公有继承(public):
- 基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可被派生类直接访问;
- 派生类中的成员函数可以直接访问基类中的public和protected成员,不能访问private成员;
- 通过派生类对象只能访问到基类的public成员,而protected和private成员都不可以直接访问。
2、私有继承(private)
- 基类的public和protected成员都以private身份出现在派生类中,但基类private成员不能被直接访问;
- 派生类中的成员函数可以直接访问基类中的public和protected成员,无法访问private成员;
- 通过派生类的对象不能直接访问基类中的任何成员。
3、保护继承(protected)
- 基类的public和protected成员以protected的身份出现在派生类中,但基类private成员不能被直接访问;
- 派生类的成员函数可以直接访问基类中public和protected成员,无法访问private成员;
- 通过派生类的对象不能直接访问基类中的任何成员。
其中,类型兼容规则为:一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止;主要表现为:
- 派生类的对象可以隐含转换为基类对象
- 派生类的对象可以初始化基类的引用
- 派生类的指针可以隐含转换为基类的指针
因此通过基类对象名,指针只能使用从基类继承的成员
其中要注意的是,基类的指针,无论传入的是哪个派生类的相同函数,最终只能调用到基类中对应的函数,为了解决这个问题,可以通过使用虚函数(另外博客有说明)的方法。
基类与派生类的对应关系
基类与派生类的对应关系有:
- 单继承:派生类只从一个基类派生
- 多继承:派生类从多个基类派生
- 多重派生:由一个基类派生出多个不同的派生类
- 多层派生:派生类又作为基类,继续派生出新的类
其中多继承的声明方式为:class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...,继承方式n 基类名n { 派生类主体 };
派生类的构造函数
继承时的构造函数:
- 基类的构造函数不能被继承,派生类中需要声明自己的构造函数
- 定义构造函数时,只需要对本类中新增成员进行初始化,而对于继承来的基类成员的初始化,会自动调用基类的构造函数完成
- 派生类的构造函数需要给基类的构造函数传递参数
1.1 单一继承时的构造函数(多继承类似,逗号隔开即可)
派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(参数表){
本类新增成员初始化赋值语句;
}
1.2 多继承且有内嵌对象时的构造函数
派生类名::派生类名(形参表):基类名1(参数),基类名2(参数),..,新增成员对象(参数){
本类成员初始化赋值语句;
}
其中构造函数有自己特定的执行顺序:
- 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
- 对成员对象进行初始化,初始化顺序按照它们在类中声明的顺序
- 执行派生类的构造函数体中的内容
继承时的拷贝构造函数
若建立派生类对象时没有编写拷贝构造函数,编译器会生成一个隐含的拷贝构造函数,若有,则调用基类的拷贝构造函数,再为派生类新增的成员对象执行拷贝
例如:C::C(C &c1): B(c1){...}
,其中C为派生类,其基类为B,c1为拷贝对象
如下举例:类Rectangle可以看为矩形类,矩形类中有长和宽,为了求出一个长方体的体积,显然类Rectangle不满足条件,没有高的数据,因此我们从类Rectangle派生一个类为
Cuboid作为长方体类,同时在类中新增成员高high,此时,求长方体体积只需要将矩形面积乘以高即可。
class Rectangle {
public:
Rectangle(int _width, int _length) {//基类构造函数
width = _width;
length = _length;
}
Rectangle(const Rectangle& r) {//基类的拷贝构造函数
printf("执行到基类的拷贝构造函数\n");
width = r.width;
length = r.length;
}
int getArea() {
return width * length;
}
private:
int width;//宽
int length;//长
};
class Cuboid:public Rectangle{//以public继承方式
public:
Cuboid(int _high, int _width, int _length) :Rectangle(_width, _length) {
//派生类的拷贝构造函数
printf("执行到派生类的拷贝构造函数\n");
high = _high;
}
Cuboid(const Cuboid& c) :Rectangle(c) {
high = c.high;
}
int getVolume() {
return getArea() * high;
}
private:
int high;//高
};
int main() {
Cuboid c(1, 1, 1);
printf("c体积为:%d\n", c.getVolume());
Cuboid c1 = c;//会调用拷贝构造函数
printf("c1体积为:%d\n", c1.getVolume());
}
/*输出:
执行到派生类的拷贝构造函数
c体积为:1
执行到基类的拷贝构造函数
c1体积为:1
*/
继承时的析构函数
析构函数满足以下性质:
- 析构函数也不被继承,派生类自行声明
- 声明方法与一般(无继承关系时)类的析构函数相同
- 不需要显式地调用基类的析构函数,系统会自动隐式调用
- 析构函数的调用次序与构造函数相反
派生类成员的标识与访问:遵循同名隐藏规则
同名隐藏规则:当派生类与基类有相同成员时
1、若未强行指明,则通过派生类对象使用的是派生类中的同名成员
2、如要通过派生类对象访问基类中被隐藏的同名成员,应适应其类名限定(通过作用域运算符)
如下举例,类B继承于类A,两者都有text函数成员,则需要类名限定,才能访问到基类A的text函数。
class A {
public:
void text() {
printf("此函数为基类成员\n");
}
};
class B :public A {
public:
void text() {
printf("此函数为派生类成员\n");
}
};
int main() {
B b;
b.text();
b.A::text();//访问到基类的同名成员
}
/*输出:
此函数为派生类成员
此函数为基类成员
*/
二义性问题与虚基类
什么是二义性问题:
1、在多继承时,基类与派生类之间,或基类之间出现同名成员时,将出现访问同名成员的二义性(不确定性),则需要采用同名隐藏规则或虚函数来解决。
2、当派生类从多个基类派生,而这些基类又从同一个基类派生,则访问此共同基类中的成员时,将产生二义性,只能由虚基类来解决。
什么是虚基类:就是以virtual修饰说明的基类。用于解决共同基类的场合。
例如:class B1 : virtual public B
,其基类B就被当做是虚基类。
虚基类的作用是什么:
1、主要用来解决多继承时可能发生的对同一基类继承多次产生的二义性问题
2、为最远的派生类提供唯一的基类成员,而不产生多次拷贝
使用时注意,最好在第一次继承的时候就使用虚基类,防止后来不断继承出现的二义性问题。举例如下,可以看出类B1和类B2都是从类A中继承来的,作为两个类的共同基类,而类C从类B1和类B2继承而来,要想从类C的对象访问到其共同基类A的成员a,需要在声明类B1和类B2时使用virtual关键字,将类A作为虚基类,否则将会报错为不明确。
class A {
public:
int a;
};
class B1 :virtual public A {
public:
int b1;
};
class B2 :virtual public A {
public:
int b2;
};
class C :public B1, public B2 {
public:
int c;
};
int main() {
C c;
c.a;//可以成功访问
}
讨论虚基类及其派生类的构造函数问题:
1、建立对象时所指定的类称为最(远)派生类;
2、虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的;
3、在整个继承结构中,直接或间接继承及虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用,如果未列出,则表示调用该虚基类的默认构造函数;
4、在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略。
如下举例,类C的构造函数中对类A的构造函数进行了调用,而其类B1和类B2选择了无参数的构造函数。
class A {
public:
A() {};
A(int _a) :a(_a) {};
int a;
};
class B1 :virtual public A {
public:
B1() {};
B1(int _a, int _b1) :A(_a), b1(_b1) {};
int b1;
};
class B2 :virtual public A {
public:
B2() {};
B2(int _a, int _b2) :A(_a), b2(_b2) {};
int b2;
};
class C :virtual public B1, virtual public B2 {
public:
C(int _a, int _c) :A(_a), c(_c) {};
int c;
};
int main() {
C c(1, 1);
c.a;
}