继承和派生

继承概述

为什么需要继承

使用继承,可以复用已有的代码

继承基本概念

c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。

派生类中的成员,包含两大部分:

  1. 一类是从基类继承过来的,一类是自己增加的成员。
  2. 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
    在这里插入图片描述

派生类定义

派生类定义格式:

   Class 派生类名 :  继承方式 基类名{
         //派生类新增的数据成员和成员函数
   }

从继承源上分:

  1. 单继承:指每个派生类只直接继承了一个基类的特征
  2. 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

派生类访问控制

在这里插入图片描述
在这里插入图片描述

继承中的构造和析构

继承中的对象模型

在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:

class Aclass{
public:
	int mA;
	int mB;
};
class Bclass : public Aclass{
public:
	int mC;
};
class Cclass : public Bclass{
public:
	int mD;
};
void test(){
	cout << "A size:" << sizeof(Aclass) << endl;
	cout << "B size:" << sizeof(Bclass) << endl;
	cout << "C size:" << sizeof(Cclass) << endl;
}

对象构造和析构的调用原则

  1. 子类对象在创建时会首先调用父类的构造函数
  2. 父类构造函数执行完毕后,才会调用子类的构造函数
  3. 当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
  4. 析构函数调用顺序和构造函数相反
    在这里插入图片描述

继承中同名成员的处理方法

  1. 当子类成员和父类成员同名时,子类依然从父类继承同名成员
  2. 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
  3. 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
class Base{
public:
	Base():mParam(0){}
	void Print(){ cout << mParam << endl; }
public:
	int mParam;
};

class Derived : public Base{
public:
	Derived():mParam(10){}
	void Print(){
		//在派生类中使用和基类的同名成员,显示使用类名限定符
		cout << Base::mParam << endl;
		cout << mParam << endl;
	}
	//返回基类重名成员
	int& getBaseParam(){ return  Base::mParam; }
public:
	int mParam;
};

int main(){

	Derived derived;
	//派生类和基类成员属性重名,子类访问成员默认是子类成员
	cout << derived.mParam << endl; //10
	derived.Print();
	//类外如何获得基类重名成员属性
	derived.getBaseParam() = 100;
	cout << "Base:mParam:" << derived.getBaseParam() << endl;

	return EXIT_SUCCESS;
}

如果重新定义了基类中的重载函数,将会发生什么?

class Base{
public:
	//重载函数
	void func1(){
		cout << "Base::void func1()" << endl;
	};
	void func1(int param){
		cout << "Base::void func1(int param)" << endl;
	}
	//非重载函数
	void myfunc(){
		cout << "Base::void myfunc()" << endl;
	}
};

class Derived1 : public Base{};
class Derived2 : public Base{
public:
	void myfunc(){
		//基类myfunc被隐藏,可通过类作用域运算符指定调用基类myfunc函数
		//Base::myfunc();
		cout << "Derived2::void myfunc()" << endl;
	}
};
class Derived3 : public Base{
public:
	//改变成员函数的参数列表
	void func1(int param1, int param2){
		//Base::func1(10);  //类的内部可通过类作用域运算符访问基类重载版本的函数
		cout << "Derived3::void func1(int param1,int param2)" << endl;
	};
};
class Derived4 : public Base{
public:
	//改变成员函数的返回值
	int func1(int param){
		Base::func1(10);
		cout << "Derived4::int func1(int param)" << endl;
		return 0;
	}
};

//和基类非重载函数重名
void test01(){
	Derived1 derived1;
	derived1.myfunc();
	//和基类函数重名
	Derived2 derived2;
	derived2.myfunc();
}

//和基类重载函数重名
void test02(){

	Derived3 derived3;
	//derived3.func1();  //基类重载版本的函数fun1被全部隐藏,子类外部不可访问
	//derived3.func1(10);
	derived3.func1(10,20);

	Derived4 derived4;
	//derived4.func1(); //基类重载版本的函数fun1被全部隐藏,子类外部不可访问
	derived4.func1(10);
}

//结论:任何时候重新定义基类中的任何一个函数,子类中这种函数的任何版本都会被隐藏(非覆盖,可通过类作用域运算符调用)

任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.

非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。
构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。

另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。

在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

继承中的静态成员特性

静态成员函数不能是虚函数(virtual function)

静态成员函数和非静态成员函数的共同点:

  1. 他们都可以被继承到派生类中。
  2. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
  3. 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
class Base{
public:
	static int getNum(){ return sNum; }
	static int getNum(int param){
		return sNum + param;
	}
public:
	static int sNum;
};
int Base::sNum = 10;

class Derived : public Base{
public:
	static int sNum; //基类静态成员属性将被隐藏
#if 0
	//重定义一个函数,基类中重载的函数被隐藏
	static int getNum(int param1, int param2){
		return sNum + param1 + param2;
	}
#else
	//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
	static void getNum(int param1, int param2){
		cout <<  sNum + param1 + param2 << endl;
	}
#endif
};
int Derived::sNum = 20;

多继承

多继承的概念

我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。
在这里插入图片描述

class Base1{
public:
	void func1(){ cout << "Base1::func1" << endl; }
};
class Base2{
public:
	void func1(){ cout << "Base2::func1" << endl; }
	void func2(){ cout << "Base2::func2" << endl; }
};
//派生类继承Base1、Base2
class Derived : public Base1, public Base2{};
int main(){

	Derived derived;
	//func1是从Base1继承来的还是从Base2继承来的?
	//derived.func1(); 
	derived.func2();

	//解决歧义:显示指定调用那个基类的func1
	derived.Base1::func1(); 
	derived.Base2::func1();

	return EXIT_SUCCESS;
}

多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?

解决方法就是显式指定调用那个基类的版本。

菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。

在这里插入图片描述

这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。
  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class BigBase{
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

	Derived derived;
	//1. 对“func”的访问不明确
	//derived.func();
	//cout << derived.mParam << endl;
	cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
	cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

	//2. 重复继承
	cout << "Derived size:" << sizeof(Derived) << endl; //8

	return EXIT_SUCCESS;
}

上述问题如何解决?对于调用二义性,那么可通过指定调用那个基类的方式来解决,那么重复继承怎么解决?

对于这种菱形继承所带来的两个问题,c++为我们提供了一种方式,采用虚基类。那么我们采用虚基类方式将代码修改如下:

class BigBase{
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : virtual public BigBase{};
class Base2 : virtual public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

	Derived derived;
	//二义性问题解决
	derived.func();
	cout << derived.mParam << endl;
	//输出结果:12
	cout << "Derived size:" << sizeof(Derived) << endl;

	return EXIT_SUCCESS;
}

以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。

通过虚继承解决了菱形继承所带来的二义性问题。

虚继承实现原理

  1. BigBase 菱形最顶层的类,内存布局图没有发生改变。
  2. Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
  3. Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量

由此可知编译器帮我们做了一些幕后工作,使得这种菱形问题在继承时候能只继承一份数据,并且也解决了二义性的问题。

当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这和多继承是完全不同的)。

即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,

那到底谁应该负责完成初始化呢?

C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用

class BigBase{
public:
	BigBase(int x){mParam = x;}
	void func(){cout << "BigBase::func" << endl;}
public:
	int mParam;
};
class Base1 : virtual public BigBase{
public:
	Base1() :BigBase(10){} //不调用BigBase构造
};
class Base2 : virtual public BigBase{
public:
	Base2() :BigBase(10){} //不调用BigBase构造
};

class Derived : public Base1, public Base2{
public:
	Derived() :BigBase(10){} //调用BigBase构造
};
//每一次继承子类中都必须书写初始化语句
int main(){
	Derived derived;
	return EXIT_SUCCESS;
}

注意:
虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的.

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值