用人话讲C++——继承(5)

前言的小介绍

继承机制的主要功能是,对一个已存在的类进行扩充和增加功能,从而得到一个新的类。

继承机制是面向对象程序设计中代码复用的重要手段,可以大大提高编程的效率和可靠性。

但是,继承机制的引入,也带来了一些新的小问题,比如,原有类成员访问属性的变化,构造函数、析构函数的调用,原有类成员与新类成员的同名冲突,等等。

继承与派生的基本概念

继承是社会生活中的一个很普遍的现象。比如,每个人都或多或少的从祖辈和父母那里继承了一些体貌特征。但是,每个人也并不是父母的复制品。因为,总存在一些特性,这些特性是他所特有的,在父母身上并没有体现。

换一个角度聊继承,定义金毛和哈士奇两只狗的类,两个都是狗,在写他们的类的时候会有很多地方都相同,那么创建一个狗的类,再去定义哈士奇和金毛各自的特殊点,那可以少敲好多代码

面向对象程序设计也借鉴了这个思想。把这个过程抽象为:一个新定义的类具有某个或某些旧类的功能与数据成员,但他与旧类又不完全相同,而是额外添加了一些功能或数据成员。

在面向对象程序设计的语境中,旧类称为基类,也称为父类,新类称为派生类,也称为子类。

我喜欢叫父类和子类,基类和派生类不仅“难听”,而且要多打(说)一个字

在C++中,继承与派生这两个词经常一起出现,它们实际上描述的是同一个过程,都是指在已有类的基础上,增加新特性而产生新类的过程。他们的区别在于角度不同,继承是从新类的角度称呼,而派生是从旧类的角度称呼。

在C++这个奇怪的世界里,一个基类可以派生出多个派生类,一个派生类也可以由多个基类派生而成,派生类也可以作为新的基类,继续派生出新的派生类。

就是一个儿子可以有多个爹,一个爹可以有多个儿子(前面那个有点不正常)。儿子下面又可以有孙子…

另外,根据基类数目的不同,继承通常分为单一继承和多重继承两大类。单一继承是指子类只有一个父类,多重继承就是子类有多个父类。

引入继承机制的优势是提高了代码的可重用性。子类可以继承父类的成员而不必再重新设计已测试过的基类代码,使编程的工作量大大减轻。
拿之前举例的金毛和哈士奇

class Dog{
	private:
		int age;
		int weight;
		string name;
	public:
		void kanjia(){
			cout<<name+"正在看家"<<endl;
		}
}
class Husky:public Dog{
	public:
		void chaijia(){
			cout<<name+"正在拆家"<<endl;
		}
}
class Gloden:public Dog{
	public:
		void chaijia(){
			cout<<name+"正在帮助盲人"<<endl;
		}
}

金毛和哈士奇的构造只要继承了狗类,就会都有狗类所包含的东西。这样节省了哈士奇和金毛类中再去定义相同的东西

派生类的定义与访问控制

定义派生类的语法格式如下

class 派生类名: 继承方式 基类名 [ , 继承方式 基类名2 ,...]{
	//派生类新增的数据成员和成员函数定义
};

(1)派生类的定义与普通类的定义类似,只是在类名称与类体之间必须给出继承方式与基类名。对于单一继承,只有一个基类名,对于多重继承,有多个基类名,彼此之间以逗号分割。
(2)继承方式指明派生类是以什么方式继承基类。继承方式公有3种:public(公有)、private(私有)和protected(保护),如果缺省,则默认为私有继承方式。
(3)需要注意的是,基类的构造函数和析构函数不能被继承。

儿子必须表明是谁的儿子,是怎么样的儿子(私生子还是普通那样的儿子),没有表明这个儿子是什么样的,那就是不可告人的私生子(private)。

以下是对子类继承属性的继承
(1)公有继承:基类的公有成员变成派生类的公有成员,基类的保护成员变成派生类的保护成员,基类的私有成员不可被继承,在派生类中不可见。
(2)保护继承:积累的公有成员与保护成员变成派生类的保护成员,基类的私有成员不可被继承,在派生类中不可见。
(3)私有继承:基类的公有成员与保护成员变成派生类的私有成员,基类的私有成员不可被继承,在派生类中不可见。

总结:
(1)基类的private成员不可以被继承,因此在派生类中无法直接访问。
(2)基类的public和protetced成员可以被继承,或者属性保持不变,或者同时变成protected或private,在派生类中可以访问。

派生类的构造和析构

定义一个对象时,必然会调用它的构造函数。对于一个派生类对象而言,新增加成员的初始化可以在派生类的构造函数中完成,其基类成员的初始化则必须在基类的构造函数中完成。

同样,当一个对象的生命期结束时,必然会调用它的析构函数。派生类的析构函数只能完成对新增加数据成员的清理工作,而基类数据成员的的扫尾工作则应由基类的构造函数完成。由于析构函数没有参数,因此派生类的析构函数默认直接调用了基类的析构函数。

下面是代码测试构造函数和析构函数的调用次序

#include<iostream>
using namespace std;
class Member {
public:
	Member() {
		cout << "constructing Member\n";
	}
	~Member() {
		cout << "destructing Member\n";
	}
};
class Base {
public:
	Base() {
		cout << "constructing Base\n";
	}
	~Base() {
		cout << "destructing Base\n";
	}
};
class Derived :public Base {
private:
	Member mem;
public:
	Derived() {
		cout<< "constructing Derived\n";
	}
	~Derived() {
		cout << "destructing Derived\n";
	}
};
int main() {
	Derived obj;
	return 0;
}

运行结果如下
constructing Base
constructing Member
constructing Derived
destructing Derived
destructing Member
destructing Base

从此可以看出,再定义一个派生类的对象时,构造函数的调用顺序如下。
1、基类的构造函数
2、派生类对象成员的构造函数(按定义顺序)。
3、派生类构造函数。
what ismore,析构函数的调用次序正好与构造函数的调用次序相反。

基类与对象成员的构造函数均没有参数。但在实际应用中,大部分的的构造函数都需要传入一定的参数来初始化成员。此时,需要在派生类的构造函数的初始化列表中调用这些构造函数。

//语法如下所示
派生类名 ( 总形式参数表 ) : 基类名1(参数表1)[ , 基类名2(参数表2) , ...]{
	//派生类自身数据成员的初始化
};

(1)派生类只需负责直接基类构造函数的调用。若直接基类构造函数不需要提供参数,则无需在初始化列表中列出,但实际上也是会自动调用基类的构造函数的。
(2)基类构造函数的调用通过初始化列表来完成。当创建一个对象时,实际调用次序为声明派生类时各基类出现的次序,而不是各基类构造函数在初始化列表中的次序。
(3)其他初始化项包括对象成员、常成员和引用成员等。另外普通数据成员的厨师话,也可以放在初始化列表中进行。

//展示基类带参构造函数调用
#include<iostream>
using namespace std;
class Base {
private:
	int x;
public:
	Base(int i) {
		x = i;
		cout << "constructing Base\n";
	}
	void show() {
		cout << " x = " << x << endl;
	}
};
class Derived :public Base {
private:
	Base d;
public:
	Derived(int i) :Base(i), d(i) {
		cout << "constructing Derived\n";
	}
};
int main() {
	Derived obj(100);
	obj.show();
	return 0;
}

代码运行如下:
constructing Base//调用基类构造函数
constructing Base//调用对象成员d所属类Base的构造函数
constructing Derived//调用派生类自己的构造函数
x = 100

下面演示多重继承下构造函数与析构函数的调用实例

#include<iostream>
using namespace std;
class Grand {
private:
	int a;
public:
	Grand(int n) :a(n) {
		cout << "constructing Grand, a = " << a << endl;
	}
	~Grand() {
		cout << "destructing Grand" << endl;
	}
};
class Father :public Grand {
private:
	int b;
public:
	Father(int n1, int n2) :Grand(n2), b(n1) {
		cout << "constructing Father , b = " << b << endl;
	}
	~Father() {
		cout << "detstucting Father" << endl;
	}
};
class Mother {
private:
	int c;
public:
	Mother(int n) :c(n) {
		cout << "constructing Monther, c = " << c << endl;
	}
	~Mother() {
		cout << "destructing Monther" << endl;
	}
};
class Son :public Father, public Mother {
private:
	int d;
public:
	Son(int n1, int n2, int n3, int n4) :Mother(n2), Father(n3, n4), d(n1) {
		cout << "constructing Son , d = " << d << endl;
	}
	~Son() {
		cout << "destructing Son" << endl;
	}
};
int main() {
	Son s(1, 2, 3, 4);
	return 0;
}

运行结果如下所示
constructing Grand, a = 4
constructing Father , b = 3
constructing Monther, c = 2
constructing Son , d = 1
destructing Son
destructing Monther
detstucting Father
destructing Grand

同名冲突及解决方案

基类与派生类的同名冲突

基类与派生类的同名现象源自于派生类在定义新成员时,新成员的名称与基类中的某个成员同名。此时,同名覆盖原则将发挥作用,无论是派生类内部成员函数,还是派生类对象访问同名成员,如果未加任何特殊标识,则访问的都是派生类中新定义的同名成员。

如果派生类内部成员函数或派生类对象需要访问基类的同名成员,则必须在同名成员前面加上“基类名::”进行限定。

上代码演示!

#include<iostream>
using namespace std;
class Base {
public:
	Base(int x) {
		a = x;
	}
	void Print() {
		cout << "Base::a = " << a << endl;
	}
	int a;
};
class Derived :public Base {
public:
	int a;
	Derived(int x, int y) :Base(x) {
		a = y;//派生类内部直接访问的是新增成员a
		Base::a *= 2;//访问基类的同名成员要使用Base::
	}
	void Print() {//同名的成员函数
		Base::Print();//访问基类的同名成员要使用Base::
		cout << "Derived::a = " << a << endl;
	}
};
void f1(Base& obj) {
	obj.Print();
}
void f2(Derived& obj) {
	obj.Print();
}
int main() {
	Derived d(200, 300);
	d.Print();//调用派生类中新增的同名函数
	d.a = 400;//改变派生类中新增的同名数据成员
	d.Base::a = 500;//改变基类中的同名数据成员
	d.Base::Print();//调用基类的同名函数
	Base* pb;
	pb = &d;
	pb->Print();//基类指针调用的是基类的Print()函数
	f1(d);//基类引用调用的是基类的Print()函数
	Derived* pd;
	pd = &d;
	pd->Print();//派生类指针调用的是派生类的Print()函数
	f2(d);//派生类调用的是派生类的Print()函数
	return 0;
}

程序运行如下所示
Base::a = 400————d.print
Derived::a = 300———d.print
Base::a = 500————d.Base::Print
Base::a = 500————pb->Print
Base::a = 500————f1(d)
Base::a = 500————pd->Print
Derived::a = 400———pd->Print
Base::a = 500————f2(d)
Derived::a = 400———f2(d)

1、通过派生类的指针或引用,访问的是派生类的同名成员,此时同名覆盖原则仍发挥作用
2、基类的指针指向派生类对象时,访问的依然时基类中的同名成员。
3、基类的引用成为派生类对象别名时,访问的依然是基类中的同名成员
.
这个其实还是蛮好理解的,是什么类型调用的就是调用哪个类的Print

多重继承中直接基类的同名冲突

多重继承中直接基类的同名现象源自多重继承中,多个直接基类中有同名成员。此时,派生类访问这些同名成员时,将发生同名冲突。其解决方案与基类与派生类的同名冲突类似。在同名成员前指明基类名即可。

用人话说就是多重继承下,如果有多个同名冲突的成员函数,那就和上面一样,在调用前说明调用哪个类的同名函数
这里的冲突指的是多个爸爸共同诞生一个儿子的情况

上代码!

#include<iostream>
using namespace std;
class Base1 {
protected:
	int a;
public:
	Base1(int x) {
		a = x;
		cout << "Base1 a = " << a << endl;
	}
};
class Base2 {
protected:
	int a;
public:
	Base2(int x) {
		a = x;
		cout << "Base2 a = " << a << endl;
	}
};
class Derived :public Base1 ,public Base2{
public:
	Derived(int x, int y) :Base1(x), Base2(y) {
		Base1::a *= 2;//改变从Base1中继承的数据成员a的值
		Base2::a *= 2;//改变从Base2中继承的数据成员的值
		cout << "Derived from Base1::a = ";
		cout << Base1::a << endl;//输出Base1中a的值
		cout << "Derived from Base2::a = ";
		cout << Base2::a << endl;//输出Base2中a的值
	}
};
int main() {
	Derived obj(10, 20);
	return 0;
}

代码运行结果如下所示:
Base1 a = 10
Base2 a = 20
Derived from Base1::a = 20
Derived from Base2::a = 40

前面两行的a输出意料之中,通过子类的数据获取,再给到父类里面去,再父类输出。
后面为什么会变咧?
在继承过后,通过子类调用父类数据修改的方式不会触发定义的输出,而是直接改变父类里面存储的数据,所以后面调用父类的数据会变成20和40

多层继承中共同祖先基类引发的同名冲突

多层继承中共同祖先基类引发的同名冲突主要产生于多层派生中。其主要原因是:派生类有多个直接或间接的基类,在这些基类中,有一个基类是其余某些基类的共同祖先。就好比这个儿子有两个爸爸,两个爸爸有个共同的爹,就是这个儿子的爷爷。

上代码!

#include<iostream>
using namespace std;
class Base {
protected:
	int a;
public:
	Base(int x) :a(x) {
		cout << "Base a = " << a << endl;
	}
	~Base() {
		cout << "Destructing Base" << endl;
	}
};
class Base1 :public Base {
protected:
	int b;
public:
	Base1(int x, int y) :Base(y), b(x) {
		cout << "Base1 from Base a = " << a << endl;
		cout << "Base1 b = " << b << endl;
	}
	~Base1() {
		cout << "Destructing Base1" << endl;
	}
};
class Base2 :public Base {
protected:
	int c;
public:
	Base2(int x, int y) :Base(y), c(x) {
		cout << "Base1 from Base a = " << a << endl;
		cout << "Base1 c = " << c << endl;
	}
	~Base2() {
		cout << "Destructing Base2" << endl;
	}
};
class Derived :public Base1, public Base2 {
public:
	Derived(int x, int y) :Base1(x,y),Base2(2*x,2*y){
		cout << "Derived from Base1::a = ";
		cout << Base1::a << endl;
		cout << "Derived from Base2::a = ";
		cout << Base2::a << endl;
		cout << "Derived from Base1 b = ";
		cout << b << endl;
		cout << "Derived from Base2 c = ";
		cout << c << endl;
//		cout << a << endl;//此行有二义性
//		cout << Base::a << endl;//此行有二义性,但是又好像没有,最新版的能调试出来,2010学习版会报错
	}
	~Derived() {
		cout << "destructing Derived" << endl;
	}
};
int main() {
	Derived obj(10, 20);
	return 0;
}

代码运行结果如下所示:
Base a = 20——————————Base1里面的Base创建
Base1 from Base a = 20————–Base1创建
Base1 b = 10—————————–Base1创建
Base a = 40——————————Base2里面的Base创建
Base1 from Base a = 40————–Base2创建
Base1 c = 20—————————–Base2创建
Derived from Base1::a = 20———-Derived创建调用,显示Base1的a
Derived from Base2::a = 40———-显示Base2的a
Derived from Base1 b = 10———–显示Base1的b
Derived from Base2 c = 20———–显示Base2的c
destructing Derived———————最后创建的先被火化
Destructing Base2———————–Base2创建在Base1后,所以先火化Base2
Destructing Base
Destructing Base1———————–Base1最先被创建,所以最后被火化
Destructing Base————————-为什么会火化两次爷爷,反复鞭尸?

为什么会析构两次Base呢?
不难从代码的调试中看出,这两个Base其实并不是一个Base,两个爹的爹并不是一个爹,是同父异母的。
所以爷爷那边继承来的爹,是爷爷的“一半一半”,爷爷被分成了两份,当Base2结束时,先火化他的那一份爹;当Base1结束时,再火化他的那部分。

再举个奇怪的例子帮助理解虚基类的引入
定义一个家具类,有重量这个数据成员,再定义沙发和床两个类,继承于家具类。那么各自有一个重量,再定义一个沙发床,继承于沙发和床,那么是不是沙发床就有两个重量了,这不符合现实,所以引入了虚基类这玩意

虚基类的定义

class 派生类名: virtual 继承方式 虚基类{
	... //派生类新增的数据成员和成员函数定义
};
//或者
class 派生类名: 继承方式 virtual 虚基类{
	... //派生类新增的数据成员和成员函数定义
};

virtual确保虚基类的构造函数至多被调用一次。程序运行时,系统会检查。如果已经被调用过了,就忽略此次调用。

上代码!

#include<iostream>
using namespace std;
class Base {
protected:
	int a;
public:
	Base(int x) :a(x){
		cout << "Base a = " << a << endl;
	}
	~Base() {
		cout << "Destructing Base" << endl;
	}
};
class Base1 :public virtual Base {
protected:
	int b;
public:
	Base1(int x, int y) :Base(y), b(x) {
		cout << "Base1 from Base a = " << a << endl;
		cout << "Base1 b = " << b << endl;
	}
	~Base1() {
		cout << "Destructing Base1" << endl;
	}
};
class Base2 :virtual public Base {
protected:
	int c;
public:
	Base2(int x, int y) :Base(y), c(x) {
		cout << "Base2 from Base a = " << a << endl;
		cout << "Base2 c = " << c << endl;
	}
	~Base2() {
		cout << "Destructing Base2" << endl;
	}
};
class Derived :public Base1, public Base2 {
public:
	Derived(int x, int y) :Base1(x, y), Base2(2 * x, 2 * y), Base(3 * x) {
		cout << "a = " << a << endl;
		cout << "Base::a = " << Base::a << endl;
		cout << "Base1::a = " << Base1::a << endl;
		cout << "Base2::a = " << Base2::a << endl;
		cout << "b = " << b << endl;
		cout << "c = " << c << endl;
	}
	~Derived() {
		cout << "Detructing Derived" << endl;
	}
};
int main() {
	Derived obj(10, 20);
	return 0;
}

代码结果运行如下
Base a = 30——————————起决定性作用的值,并且后续再赋值不会产生变化
Base1 from Base a = 30—————a已经确定为30
Base1 b = 10——————————输出Base1中的值
Base2 from Base a = 30—————输出Base2中的值
Base2 c = 20
a = 30—————————————a是不会再改变的了,like const
Base::a = 30
Base1::a = 30
Base2::a = 30
b = 10
c = 20
Detructing Derived
Destructing Base2
Destructing Base1
Destructing Base

在例子中,Base1与Base2将Base声明为虚基类,但在Base1、Base2和Derived的构造函数中都出现了对Base构造函数的调用。

根据C++中的规定,实际运行时,只有最后一层派生类对虚基类构造函数的调用发挥作用。因此,例子中只有Derived的调用Base(3*x)发生多种,其余的调用均被忽略。

因此,当创建一个兑现时,其完整的构造函数调用次序如下。
1、所有虚基类的构造函数(按定义顺序)
2、所有直接基类的构造函数(按定义顺序)
3、所有对象成员的构造函数(按定义顺序)
4、派生类自己的构造函数。
析构函数的调用次序与之完全相反

赋值兼容规则

所谓赋值兼容,就是指需要使用基类的地方可以使用其公有派生类来代替,换言之,公有派生类可以当成基类来使用。赋值兼容具有广泛的现实基础。

狼狗可以看成是狗的一个子类,当我们需要狗来看门时,可以使用一只狗来看门,也可以使用一只狼狗来开门,即狼狗完全按可以当成狗来使用。

赋值兼容的理论依据是:公有子类继承了父类中除构造函数、析构函数以外的所有非私有成员,且访问权限也完全相同,因此当外界需要基类时,完全可以用它来代替。


赋值兼容主要有以下四种

  • 基类对象=公有派生类对象
    • 赋值后的基类对象只能获得基类成员部分,派生类中新增加的成员不能被基类对象访问。
  • 指向基类对象的指针 = 公有派生类对象的地址。
    • 利用赋值后的指针可以间接访问派生类中的基类成员。
  • 指向基类对象的指针 = 指向公有派生类对象的指针。
    • 利用赋值后的指针可以间接访问原指针所指向对象的基类成员。
  • 基类的引用 = 公有派生类对象,即派生类对象可以初始化基类的引用。
    • 赋值后的引用只可以访问基类成员部分,不可以访问派生类新增成员。
#include<iostream>
using namespace std;
class Base {
	int b;
public:
	Base(int x):b(x){}
	int getb() {
		return b;
	}
};
class Derived :public Base {
	int d;
public:
	Derived(int x,int y):Base(x),d(y){}
	int getd() {
		return d;
	}
};
int main() {
	Base b1(11);
	Derived d1(22, 33);
	
	b1 = d1;											//第一种赋值兼容
	cout << "b1.getb() = " << b1.getb() << endl;
	//cout<<"b1.getd() = "<<b1.getd()<<endl;
	Base* pb1 = &d1;									//第二种赋值兼容
	cout << "pd1->getb() = " << pb1->getb() << endl;
	//cout<< "pd1->getd() = " << pb1->getd() << endl;
	Derived* pd = &d1;
	Base* pb2 = pd;
	cout << "pb2->getb() = " << pb2->getb() << endl;
	//cout << "pb2->getd() = " << pb2->getd() << endl;

	Base& rb = d1;
	cout << "rb.getb() = " << rb.getb() << endl;
	//cout << "rb.getb() = " << rb.getb() << endl;
	return 0;
}

代码运行结果如下所示:
b1.getb() = 22
pd1->getb() = 22
pb2->getb() = 22
rb.getb() = 22

使用赋值兼容时,必须是公有派生类,因为只用公有继承才会保持访问属性不变。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

优降宁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值