天天看点

C++中继承与虚继承

先谈谈继承的概念

C++中继承与虚继承

面向对象的继承就是如上图这样:         从一个类派生另外一个类,使前者所有的特征在后者中自动可用。它可以声明一些类型,这些类型可以共享部分或全都以前所声明的类型,它也可以从超过一个的基类型共享一些特征。 继承是面向对象复用的重要手段。通过继承定义一个类,继承类型之间的关系模型,共享共有的东西,实现各自本质的不同的东西。 不过是把现实生活中实例用能表现事物特征的关键数据来代替(如现实生活中的汽车,发票;对应的抽象后就可以用车牌号,预定数量这样的数据来表示现实生活中的事物) 继承还有好几种情况:

C++中继承与虚继承

下面看看具体细节

一.继承的关系及继承访问限定符

下面是三种继承关系下基类成员在派生类中的访问变化

C++中继承与虚继承

那么private和protected都是限定直接访问,那么他们有什么区别?

先看下面这个单继承示例代码在作解释:

#include<iostream>
using namespace std;

class person    //父类/基类
{
public:
    person()
	{
		cout << "person" << endl;
	}

protected:
	int id;
private:
	int _age;
};
//class student:protected person
//class student:private person
class student :public person      //class 子类/派生类: 继承关系 父类/基类
{
public:
	student()
	{
		cout<< person::id << endl;
		cout << _num << endl;
	}
public:
	int _num;
};
int main()
{
	person p;
	student s;
	return 0;
}
           

下图发现类可以直接调用父类的id和person(),而不能调用父类中私有的_age成员

C++中继承与虚继承

这里程序可以正常运行,但是子类中不可以输出cout<<person::_age<<endl;,因为_age是父类中的私有成员(private),不可在类外直接访问;而id是父类中的保护成员(protected),id可以被子类继承后直接访问,但不可以在子类和父类外的其它地方直接使用;从而可以看出来protected访问限定符是为继承而产的。

要想访问类中的私有成员,只能通过类中的其它访问权限的方法调用成员,然后在类外使用。

 总结 1.基类的私有成员在派生类中是不能被访问的.如果一些基类成员不想被基类对象直接访问.但需要在派生类中能访问.就定义为保护成员。可以看出保护成员限定符是因继承才出现的。 2. public继承是一个接口继承,保持is-a原则.每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。 3. protected / privat继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是hasa的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。 4.不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员但是基类的私有成员存在但是在子类中不可见(不能访问) 5.使用关键字 class时默认的继承方式是 private.使用 stret时默认的继示方式是 public.不过最好显示的写出继承方式 6.在实际运用中一般使用都使用 public继承,极少场景下才会使用 protected和private继承

二.赋值兼容规则

看下面示例代码:

#include<iostream>
using namespace std;

class person
{
public:
	void Display()
	{
		cout << "person()" << endl;
	}

protected:
	string _name;
private:
	int _age;
};

class student :public person
{
public:
	int _num;
};
int main()
{
	person b;
	student a;
	
	//子类对象可以赋值给父类对象(切片/切割)
	b = a;    //yes

	//父类对象不可以赋值给子类对象
	//a = b; //no

	//父类指针/引用可以指向子类对象
	person* p1 = &a;
	person& p1 = a;
	
	//子类指针/引用不能指向父类对象(可以通过强制类型转换来指向)
	//student* s1 = &b;            //no
	student* s1 = (student*)&b;    //yes
	//student& s2 = b;             //no
	student& s2 = (student&)b;   //yes

	return 0;
}
           

可以总结:

子类对象可以赋值给父类对象(切片/切割)

父类对象不可以赋值给子类对象

父类指针 / 引用可以指向子类对象

子类指针 / 引用不能指向父类对象(可以通过强制类型转换来指向)

下面我们一条一条解释

那什么是切片/切割呢?看下图

C++中继承与虚继承

子类可以赋值给父类,是因为子类里边包含父类里的全部成员变量,所以可以通过切片把父类的成员都赋予相应的值

父类赋给子类为什么不可以呢?是因为父类里面没有子类独有的那部分变量,所以无法给子类独有的成员变量赋值,因此不能用父类给子类赋值

父类指针 / 引用可以指向子类对象

C++中继承与虚继承

现在我们知道子类指针 / 引用不能指向父类对象,因为子类比父类空间大,所以会越界,那强制类型转换会出现什么情况呢?

如果上面代码的main函数内加上以下这两句代码程序会是什么反应呢?

s1->_num = 10;

s2._num =  20;

解释如下图所示:

C++中继承与虚继承

继承体系中的作用域:

1.在继承体系中基类和派生类都有独立的作用域。 2.子类和父类中有同名成员、子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用基类:基类成员访问)-隐藏-重定义 3.注意在实际中在继承体系里面最好不要定义同名的成员。

派生类中的默认成员函数 吓在继承关系里,如果派生类没有显示的定义6个默认成员函数(构造函数,拷贝构造,析构函数,赋值运算符重载,取地址操作符重载,const修饰的取地址操作符重载),编译系统则会默认的的合成这6个成员函数

//派生类构造函数和析构函数的构造规则。 
#include <iostream>
using namespace std;
class First           // 声明基类
{
public:
	First()
		:a(0)
		, b(0)
	{}

	First(int x, int y)
		:a(x)
		, b(y)
	{}

	~First()
	{}

	void print()
	{
		cout << "\n a=" << a << "b = " << b;
	}
private:
	int a, b;
};
class Second : public First   //声明基类Frist的公有派生类Second
{
public:
	Second()
		:First(1,1)
		,c(0)
		,d(0)
	{}

	Second(int x , int y)
		:First(x+1, y+1)
		, c(x)
		, d(y)
	{}

	Second(int x, int y, int m, int n)
		:First(m, n)
		, c(x)
		, d(y)
	{}

	~Second()
	{}
	void print()
	{
		First::print();
		cout << " c=" << c << " d= " << d;
	}
private:
	int c, d;
};
class Third:public Second           //声明Second的公有派生类Third
{
public:
	Third()
		:e(0)
	{}
	Third(int x, int y)
		:Second(x,y)
	{}
	Third(int x, int y, int z,int m, int n)
		:Second(x, y,m,n)
		, e(z)
	{}

	~Third()
	{}

	void print()
	{
		Second::print();
		cout << " e=" << e;
	}

private:
	int e;
};

int main()
{
	Third l(3, 2,1);
	l.print();

}
           

通常情况下,当创建派生类对象时,首先要调用基类的构造函数,随后在调用派生类的构造函数,当撤销派生类对象时,则先调用派生类的析构函数,随后调用基类的析构函数,遵循先调用的后释放,后调用的先释放。

看如上代码总结出以下三点:

(1)“Third()”从这里可以看出,当基类构造函数不带参数时, 派生类不一定需要定义构造面数, 系统会自动的调用基类的无参构造函数; 然而当基类的构造函数那怕只带有一个参数, 它所有的派生类都必须定义构造函数,

甚至所定义的派生类构造函数的函数体可能为空, 它仅仅起参数的传递作用, 例如, 在上面的程序段中" Third(int x, int y)", 派生类 Third就不使用参数x和y, x和y只是被传递给了要调用的基类构造函数Second

(2)若基类使用默认构造函数或不带参数的构造函数, 则在派生类中定义构造函数时可略“:基类构造函数名(参数表)”, 此时若派生类也不需要构造函数, 则可不定义构造函数

3)如果派生类的基类也是一个派生类, 每个派生类只需负责其直接基类数据成员的初始,依次上溯。

多继承与菱形继承会出现二义性和数据冗余现象:

#include<iostream>
using namespace std;

class A
{
public:
	int _a;
};

class B : virtual public A
{
public:
	int _b;
};

class C : virtual public A
{
public:
	int _c;
};

class D : public C, public B
{
public:
	int _d;
};

int main()
{
	D dd;
	cout << sizeof(dd) << endl;

	dd.B::_a = 1;
	dd._b = 3;

	dd.C::_a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;
	cout << sizeof(bb) << endl;

	//bb = dd;
	cc = dd;
	//A* pa = &dd;ⅆ
	//B* pb = &dd;
	//C* pc = &dd;ⅆ
	//D* pd = &dd;

	return 0;
}
           
C++中继承与虚继承

为什么运行结果是这样?看完下面的对象的内存分布也就明白了

C++中继承与虚继承

若要解决二义性和数据冗余就得使用虚继承(在继承基类时,使用关键字virtual,如上面代码所示)

C++中继承与虚继承

从上图可以发现,当使用虚继承后,通过虚基表就解决了数据二义性的问题。多了虚基表的空间所以sizeof(dd)就变成24了。

虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虛继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损牦。 

再看下面,如果main()里面运行此代码,各指针所指向的空间是什么?

int main()
{
	D dd;

	dd.B::_a = 1;
	dd._b = 3;

	dd.C::_a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;

	A* pa = ⅆ
	B* pb = ⅆ
	C* pc = ⅆ
	D* pd = ⅆ

	return 0;
}
           
C++中继承与虚继承

补充几点:

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

继续阅读