本文深入探讨了面向对象编程中的继承概念,包括基本语法、继承方式、对象模型、构造析构顺序、同名成员处理及多继承。详细阐述了公有、保护和私有继承的区别,并通过实例解析了如何处理同名成员,特别是静态成员的继承。此外,文章还讨论了菱形继承及其可能导致的数据冗余问题,并提出了解决方案——虚继承。 摘要生成于 ,由 DeepSeek-R1 满血版支持,

继承是很大程度上解决了代码重复的问题。比如狗和猫都属于动物,我们需要在猫和狗的类里面写上动物年龄,动物名字等。但是动物都可以有年龄和起名称,这个时候我们写个动物类,把动物都有的属性写进去,狗和猫继承动物类就好了,就不需要对狗和猫的类里面分别写年龄和名字了。
本篇博客主要记录以下问题:
一、基本语法和继承方式
二、继承中的对象模型
三、继承中对象的构造析构顺序
四、继承同名成员的处理方式(包括普通成员、静态成员)
五、多继承语法
六、棱形继承

一、基本语法和继承方式

1.基本语法

继承的基本格式为:
class 子类名 :继承方式 父类名
还允许一个类继承多个类,多继承语法格式为:
class 子类名 : 继承方式 父类名1,继承方式 父类名2…

2.继承方式

继承方式分为公有(public)继承,保护(protect)继承,私有(private)继承。
不同的继承方式,继承的成员的属性会发生变化,如下图:
(1)首先,不管哪种继承方法,父类的私有成员子类都没办法访问。
(2)子类以公有方式继承父类的话,父类的公有成员在子类中依旧是公有属性,保护成员依旧是保护属性。
(3)子类以私有方式继承父类的话,父类的公有成员和保护属性成员都统一为保护属性。

二、继承中的对象模型

class A
public:
	int a_a;
protected:
	int a_b;
private:
	int a_c;
class B :public A
	int Geta_a()
		return a_a;
	int Geta_b()
		return a_b;
	/*//不可访问a_c,因为a_c是父类的私有属性,子类不可访问
	int Geta_c()
		return a_c;
private:
	int b_a;

父类的私有成员,子类无法访问。无法访问不代表没有继承。父类的所有成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。
比如我们用类大小看一下:

int main()
	cout << "sizeof(class B) = " << sizeof(B) << endl;
	return 0;

输出结果如下:
类B的大小为16字节。类B自己有一个int类型成员。那么还有12个字节都是从父类继承下来的。所以父类三个成员都被子类继承了,包括不允许子类访问的私有属性。
我们可以用vs2019的开发者工具进行查看:
发现从A类继承下来的是有私有成员a_c的,只不过编译器对其进行了隐藏不让访问而已。

父类的所有非静态成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。

三、继承中的构造和析构顺序

class A
public:
	A()
		cout << "这里是A构造函数" << endl;
	~A()
		cout << "这里是A析构函数" << endl;
public:
	int a_a;
protected:
	int a_b;
private:
	int a_c;
class B :public A
public:
	B()
		cout << "这里是B构造函数" << endl;
	~B()
		cout << "这里是B析构函数" << endl;
private:
	int b_a;
int main()
	B b;
	return 0;
发现是类A先调用构造函数的,这是因为只有A类先调用构造函数,类B构造的对象才能拿到类A的成员。

四、继承同名成员的处理方式

继承会引发一些问题,比如说如果子类有和父类相同名称的成员,或者说子类继承了两个父类,两个父类有同名的成员,这样的话,使用成员的时候,编译器会犯难,不知道要使用哪一个成员。这里我们拿子类和父类同名成员时的处理方式。

1.继承普通同名成员的处理方式

1.继承普通同名成员变量的处理方式

class A
public:
	int a;
	int b;;
private:
	int c;
class B :public A
public:
	int a;
int main()
	B b;
	b.a = 10;    //B类里面本身的变量a
	b.A::a = 20;//B类继承的成员变量a
	cout << b.a << "   " << b.A::a << endl;
	b.b = 20;    //B类继承的成员变量b
	b.A::b = 30; //B类继承的成员变量b
	cout << b.b << "   " << b.A::b << endl;
	return 0;

运行结果:
(1)当访问的成员变量不是同名成员变量时,直接以对象点的形式访问就可以。
(2)当访问的成员变量是同名成员变量时,以对象点的方式访问本类本身的同名成员变量,以对象点+作用域的方式访问父类的成员变量。

2.继承普通同名成员函数的处理方式

class A
public:
	void fun()
		cout << "this is A fun()" << endl;
	void fun(int tmp)
		cout << "this is fun(int)" << endl;
public:
	int a;
	int b;;
private:
	int c;
class B :public A
public:
	void fun() 
		cout << "this B fun()" << endl;
int main()
	B b;
	b.fun();    //B类fun()
	//b.fun(10);//error
	b.A::fun();//A类fun()
	b.A::fun(10);//A类fun(int)
	return 0;

运行结果:
当子类与父类有同名成员函数的时候,发现哪怕父类的成员函数和子类的成员函数形参列表不同(比如父类的fun(int)和子类的fun()明显不同,构成了函数重载),但是子类对象却不能以对象点的方式调用父类函数fun(int)。这是由于只要子类有同名的成员函数,那么父类的同名成员函数都会被隐藏,统一不能以对象点的方式调用,必须以对象点加作用域的方式调用。
(1)如果子类和父类不存在同名的函数,那么直接以对象点的方式调用即可。
(2)如果子类和父类的成员函数的函数名相同,那么对于父类的所有同名函数,子类必须以对象点+作用域的方式调用父类的同名函数。此时子类以对象点的方式调用子类本身的同名函数。

2.继承同名静态成员的处理方式

首先,要知道静态成员只有一个,继承的话也是只有一个,即子类和父类的对象都共用同一个静态成员。
如下验证代码:

class A
public:
	static int a;
	int b;
int A::a = 10;
class B :public A {};
int main()
	A a;
	cout << a.a << endl;
	B b;
	cout << b.a << endl;
	b.a = 20;
	cout << a.a << "   " << b.a << endl;
	cout << &a.a << "   " << &b.a << endl;
	return 0;
通过类B对象修改静态成员变量的值,类A的静态成员变量值也会修改,打印出来地址,两个类的对象都是用同一个静态成员变量。
1.继承同名静态成员变量的处理方式
class A
public:
	static int a;
	int b;
int A::a = 10;
class B :public A 
public:
	static int a;
int B::a = 20;
int main()
	B b;
	cout << b.a << endl;  //访问的是子类本身的静态成员变量
	cout << b.A::a << endl;//访问的是父类的静态成员变量
	cout << B::A::a << endl;//访问的是父类的静态成员变量 
	return 0;
发现有同名静态成员变量时,和同名普通成员变量处理方式一样。只不过对于静态成员变量,多了一个直接用域名访问的方式B::A::a。

2.继承同名静态成员函数的处理方式

class A
public:
	static void fun()
		cout << "this is A fun()" << endl;
	static void fun(int tmp)
		cout << "this is A fun(int)" << endl;
public:
	int a;
	int b;;
private:
	int c;
class B :public A
public:
	static void fun()
		cout << "this B fun()" << endl;
int main()
	B b;
	//B类本身的静态fun()函数
	b.fun();
	B::fun();
	//调用继承的静态fun函数
	b.A::fun();
	b.A::fun(10);
	B::A::fun();
	B::A::fun(10);
	return 0; 

运行结果:
发现有同名静态成员函数时,和同名普通成员函数处理方式一样。只不过对于静态成员函数,多了一个直接用域名访问的方式B::A::fun()。
但是注意,两个"::"的意义不同,第一个代表用类名访问,第二个是代表A类作用域下。

五、棱形继承(钻石继承)

1.棱形继承的概念

两个派生类继承一个基类,又有一个派生类继承这两个派生类。
可能举例不太恰当,但是就是这么个意思。

2.棱形继承带来的问题

1.羊继承了动物的数据,驼同样继承了动物的数据,羊驼继承了羊和驼的数据,继承本身是有继承性的,那么羊驼就有了两份动物的数据。
2.羊驼继承自动物的数据有两份,造成了资源浪费。
总的来说就是相同作用的数据有两份,就这么个问题。
平常中,是不允许这样继承的,尽量避免多继承和棱形继承的发生。

class Animal 
public:
	int m_age;
class Sheep :public Animal {};
class Tuo :public Animal {};
class SheepTuo :public Sheep, public Tuo {};
int main()
	SheepTuo st;
	//st.m_age;//error,产生二义性,编译器不知道用Sheep的m_age还是Tuo的m_age
	st.Sheep::m_age; //访问Sheep的m_age
	st.Tuo::m_age;     //访问Tuo的m_age
	return 0;

类SheepTuo的m_age有两份,但是只需要一份就够了,所以资源浪费。
可以清楚的看见Sheep 和Tuo 分别从Animal继承了m_age,而SheepTuo 又继承了Sheep 和Tuo,所以SheepTuo有两份不同作用域下的m_age。我们要做的就是让SheepTuo的m_age只有一份。

下面解决数据有两份的问题。

3.解决棱形继承带来的问题

利用虚继承解决棱形继承问题。
虚继承,即在继承方式前面再加一个virtual关键字。

class Animal 
public:
	int m_age;
class Sheep :virtual public Animal {};
class Tuo :virtual public Animal {};
class SheepTuo :public Sheep, public Tuo {};
vbptr是虚指针,由于Sheep和Tuo都是虚继承的方式继承Animal的,所以这俩类只是各多了一个虚指针,指向对应的vbtable,vbtable是虚表,Sheep和Tuo的虚指针会查虚表,记录偏移量,比如Sheep的虚指针地址偏移量为0,Sheep的虚表里面的记录的偏移量是8,0+8就是Animal的m_age的地址偏移量;同理对于Tuo来说4+4也是Animal的m_age的地址偏移量。这样的话,SheepTuo就只包含一个m_age。那么如果把SheepTuo继承Sheep和Tuo的方式都改为虚继承呢?那么SheepTuo自己也会有一个虚指针,这个虚指针指向一个虚表,虚表里放着从Sheep和Tuo还有Animal继承过来的内容。此时仍旧只有一份Animal的m_age,如下图:
继承中与其他成员不同,构造方法不能被子类继承。在创建子类对象时,为了初始化从父类继承来的成员,需要调用父类的构造方法。if(子类没有自定义构造函数){ if(基没有自定义构造函数){ 用子类定义对象时,先自动调用的默认构造函数,再调子类的默认构造函数。 } else if(基有自定义无参构造函数){
构造方法用来初始化的对象,与父类的其它成员不同,它不能被子类继承子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用父类的构造方法。   如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。   构造原则如下:   1. 如果子类没有定义构造方法,则调用父类的无参数的构造方法。   2. 如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。   3. 在创建子
文章目录一、继承二、重写与父类子类区别1、使用继承2、子类访问父类成员3、不能继承父类成员4、访问修饰符5、继承后的初始化顺序6、子类的特性信息三、多种封装关键字对比封装的关键字四、抽象与抽象方法 一、继承 接下来,我们按照小狗的方式,新建马和企鹅的。但是如果我们每一种动物都重写一遍,工作量很大。因为每种动物都有一些节本的共性,因此,我们这里可以采用一种办法,就是继承。 我们先新建一个Pet,然后将共性的信息放到Pet中。 package com.icss.bk.biz; public clas
构造方法用来初始化的对象,与父类的其它成员不同,它不能被子类继承子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用父类的构造方法。     如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。 构造原则如下: