【C++】从基类到派生类:关于C++继承的务实梳理

🏆浮世尘弦:************个人主页****************

⭐个人专栏:《C语言》、《数据结构与算法》、《C++》

💎:非淡薄无以明志,非宁静无以致远


前言

在 C++ 中,继承是面向对象编程的重要机制之一 。它允许我们基于已有的类定义新的类,从而复用现有代码,并在原有基础上扩展新的属性和行为。被继承的类通常称为基类或父类,继承得到的新类称为派生类或子类。不过,继承并不只是"把成员拿过来"这么简单。继承方式会直接影响父类成员在子类中的访问权限,对象的构造和析构也有固定顺序。理解这些规则,是正确使用 C++ 继承的基础。接下来让我们详细学习一下继承的相关内容吧!

目录

一,继承的介绍

1,引入继承

2,继承的概念

二,基类访问方式变化

1,访问变化表

2,stack继承底层容器

三,基类和派生类之间的转换

四,子类的默认成员函数

1,默认成员函数

2,实现一个不能被继承的类

五,继承中地友元和静态成员

1,继承与友元函数

2,继承与静态成员

六,多继承及菱形继承问题

1,继承模式

2,虚继承

3,多继承中的指针偏移问题

七,继承和组合


一,继承的介绍

1,引入继承

首先我们看一下,下面这个案例:

定义这两个类时,这两个类中有许多重复的部分:成员和函数。这一部分的存在时冗余的。

有什么方法可以帮助我们去除这些重复的部分?就像之前的函数模版,类模版一样

C++提出了他的方法:继承

测试:

设计一个Person类,把重复的部分都放进去,左边的Person为父类,右边的student和Teacher为子类,子类直接继承父类的成员属性和函数,不用在单独进行实现。去除了重复的部分。

接下来重新来详细认识一下:继承

2,继承的概念

继承是面向对象编程(OOP)的三大核心特征之一(封装,继承,多态)。允许我们在一个已有类(父类/基类)上增加成员函数和成员变量再创建一个新类(子类/派生类)。

定义格式:

二,基类访问方式变化

1,访问变化表

父类成员访问权限 public 继承 protected 继承 private 继承
父类的 public 成员 ++子类的 public 成员++ 子类的 protected 成员 子类的 private 成员
父类的 protected 成员 ++子类的 protected 成员++ 子类的 protected 成员 子 类的 private 成员
父类的 private 成员 ❌ 在子类中不可见 ❌ 在子类中不可见 ❌ 在子类中不可见

1,基类private成员在派⽣类中无论以什么⽅式继承都是不可见的。这⾥的不可见是指基类的私有成员无论在子类中还是在子类外都是无法进行访问使用 ,但其还是被继承到了派⽣类对象中。即子类中还是有这一个成员的。

2,子类中继承到的成员变量的访问方式取决于Min(父类的成员访问限定方式,子类继承方式),public > protected > private

3,使用关键字class时默认的继承方式是private,使用关键字struct时默认的继承方式是public,可以进行省略,但是建议写出来。

4,实际使用时,使用最多的时public继承,protected和private继承使用到的很少。因此实际重要的只有public继承的2个。

补充:

父类和子类中的成员函数,函数名相同,调用子类同名函数时,父类同名函数就会隐藏。

一般调用的是子类B中的fun,如果想要在子类对象中调用父类对象当中的同名函数,需要用:对象.父类名::函数名() 这种形式。

代码演示:

cpp 复制代码
class Person
{
protected:
	string name = "小明";
	string ID = "2331323";
};
class Student : public Person
{
public:
	void Print()
	{
		cout << name << endl;
		//这里需要指定一下才能访问父类同名成员
		cout << Person::name << endl;
		//直接进行访问
		cout << ID << endl;
	}
protected:
	string name = "超正经小李";
};
int main()
{
	Student st1;
	st1.Print();
	return 0;
}

运行结果:

最好还是不要在子类起同名的函数。

2,stack继承底层容器

之前直接在stack<T,container>,在container处直接调用容器,这种方式称为组合。

现在可以使用继承的方式使用容器适配器stack

演示一下:

cpp 复制代码
//之前使用stack底层的模板的方式叫做组合
// 这种叫继承
//#define CON deque
//#define CON list
#define CON vector
namespace att
{
    //stack继承底层容器
    template<class T>
    class stack : public CON<T> //stack继承底层容器
    {
    public:
        void push(const T& x)
        {
            CON<T>::push_back(x);//需要指定容器实例化
        }                        //不指定实例化,就找不到实例化的push_back();
        void pop()
        {
            CON<T>::pop_back();
        }
        const T& top()
        {
            return CON<T>::back();
        }
        bool empty()
        {
            return CON<T>::empty();
        }
    };
}
int main()
{
    att::stack<int> st;
    st.push(3);
    st.push(6);
    st.push(9);
    st.push(2);
    while (!st.empty())
    {
        cout << st.top() << " ";
        st.pop();
    }
    return 0;
}

运行结果:

三,基类和派生类之间的转换

public继承出来的子类对象可以赋值给父类的++对象/指针/引用。++ 这种方式称为切片。

反过来,父类对象不能赋值给子类

原因:子类对象中可能存在父类对象中不存在的成员;而父类的成员,子类则是全部继承所以可以赋值。

代码演示:

四,子类的默认成员函数

1,默认成员函数

1,子类的构造函数必须调用父类的构造函数。如果父类没有默认构造函数,则需要在子类构造函数中的初始化列表中显式调用。构造时先调用父类构造再调用子类。

2,子类的拷贝构造函数必须调用父类的拷贝构造完成初始化。

3,子类的赋值重载operator=必须调用父类的operator=完成父类的复制。由于重名的隐藏问题,需要指定作用域。

4,子类的析构函数会在清理完自己独有的成员后,自动调用父类析构函数,清理共有的父类中的成员。析构时先调用子类析构再调用父类析构。

5,子类的析构和父类的析构构成隐藏关系。

代码演示:

父类:

cpp 复制代码
class Person
{
public:
	Person(const string& _name = "雀巢")//默认构造
		:name(_name)
	{
		cout << "Person(const string& _name)" << endl;
	}
	//拷贝构造
	Person(const Person& p1)
		:name(p1.name)
	{
		cout << "Person(const Person& p1)" << endl;
	}
	Person operator=(const Person& p1)
	{
		cout << "Person operator=(const Person& p1)" << endl;
		if (this != &p1)
		{
			name = p1.name;
		}
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string name;
};

子类:

构造函数 :

cpp 复制代码
public:
	//如果使用默认构造函数
	//1,内置类型不确定
	//2,自定义类型,调用默认构造
	//3,继承父类的成员看成一个整体,调用父类构造
	//构造函数
	Student(const string& name, int _age, const string& _address)
		:Person(name) //使用父类初始化
		,age(_age)
		,address(_address)
	{}

拷贝构造:

cpp 复制代码
	//拷贝构造函数--可以不写使用默认的就可以
	//但是有资源的开辟(深拷贝)就要自己写
	Student(const Student& s1)
		:Person(s1)
		,age(s1.age)
		,address(s1.address)
	{
		//深拷贝
	}

赋值重载operator=:

cpp 复制代码
	//赋值重载函数--可以不写使用默认的就可以
    //但是有资源的开辟(深拷贝)就要自己写
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			age = s.age;
			address = s.address;
		}
		return *this;
	}

析构函数:

cpp 复制代码
	//Student()析构默认生成的就够用了,只有有资源要释放才手动写
	~Student()
	{
		//子类析构和父类析构构成隐藏关系
		//子类调用后会自动调用父类析构
	}

测试案例和结果:

结果分析:

打印的结果为什么调用了两次拷贝构造 ,四次析构?明明只有三个对象?

原因出在父类上的赋值重载上:

2,实现一个不能被继承的类

1,C++98做法:让父类构造函数私有,由于子类的构造函数必须调用父类的构造函数,父类构造 函数私有化以后,子类看不见就不能进行调用,无法实例化对象。

2,C++11:新增加一个关键字final,final放在父类名字的后面,这样子类就不能进行继承。

代码演示 :

cpp 复制代码
//实现一个不能被继承的类
//1,父类构造函数私有,子类的构造函数必须调用父类的构造函数(C++98)
//2,C++11,final关键字,final修改父类,派生类就不能继承了
class A //final  //第二种方法
{
public:
	void Func1() 
	{
		cout << "adls" << endl;
	}
private:
	//第一种方法
	//A()
	//{}
};
class B : public A
{
public:
	void Func2()
	{
		cout << "Func2" << endl;
	}
};
int main()
{
	B b1;
	b1.Func1();
	return 0;
}

运行结果:

A类中除了中文,去掉任意一个注释,B类都不能正常初始化 。

补充一个有趣的现象:按需实例化

当主函数不进行实例化时(如下图):

左边展开final依旧会报错,但是右边使用第一种方法私有化却不会进行报错,这就是一种按需实例化。

原因解释:不进行实例化 ,编译器就不会进行仔细地检查,只会大致检查是否有明显地错误。

五,继承中地友元和静态成员

1,继承与友元函数

友元关系不能进行继承,基类友元不能访问子类私有和保护成员

代码演示:

cpp 复制代码
class D //需要提前进行声明否则不知道C中的D是什么
//继承与友元
//友元关系不能进行继承,基类友元不能访问子类私有和保护成员
class C
{
public:
	friend void play(const C& s1, const D& s2);
protected:
	string name;
};
class D : public C
{
public:
	//friend void play(const C& s1, const D& s2);
protected:
	int age;
};
void play(const C& s1, const D& s2)
{
	cout << s1.name << endl;
	cout << s2.age << endl;
}
int main()
{
	return 0;
}

这里友元函数play中的age会报错

解决方法:让子类当中也包括上友元函数

2,继承与静态成员

父类定义了 staic静态成员,整个继承体系中只有这一个静态成员 。无论继承出多少子类,都只有这一个staic成员实例化。

代码演示:

cpp 复制代码
//静态成员与继承
class Person
{
public:
	string name;
	static int age;
};
int Person::age = 18;
class Student : public Person
{
protected:
	string ID;
};
int main()
{
	Person p1;
	Student s2;
	//name是不同的,父子类各用一份
	cout << &p1.name << endl;
	cout << &s2.name << endl;
	//age是同一个,父子类共用一个
	cout << &p1.age << endl;
	cout << &s2.age << endl;
	//public前提下,可以直接指定类域进行访问
	cout << Person::age << endl;
	cout << Student::age << endl;
	Person::age++;
	cout << p1.age << endl;
	cout << s2.age << endl;
	return 0;
}

运行展示:

六,多继承及菱形继承问题

1,继承模式

单继承:一个子类只有一个直接父类,这样的继承关系称之为单继承。

多继承:一个子类有两个或两个以上的直接父类,这种继承关系称为多继承。先继承的父类在前面,后继承的父类在后面。

菱形继承:菱形继承是多继承的一种特殊情况。菱形继承有数据冗余和二义性的问题。实践一般不使用菱形继承。结构如下所示:

解释:Student和Person继承了Person,Assistant有继承了Student和Tercher,那么从Person中继承到Assistant中的成员就有了两份,造成了数据冗余和二义性。

2,虚继承

虚继承是为了解决菱形继承,专门设计的一种机制。

在继承前面加上virtual

代码演示:

cpp 复制代码
//菱形继承与虚函数
class Person
{
public:
	Person(const string& name)
		:_name(name)
	{}
	string _name;
};
class Student : virtual public Person
{
public:
	Student(const string& name,int num)
		:Person(name)
		,_num(num)
	{}
private:
	int _num;//学号
};
class Teacher : virtual public Person
{
public:
	Teacher(const string& name,const string& Id)
		:Person(name)
		,ID(Id)
	{}
private:
	string ID;//工号
};
class Assistant :public Student, public Teacher
{
public:
	//对象的构造顺序永远是:虚基类Person -> 非虚基类(Student和Tercher) -> 自身成员变量
	Assistant(int num,const string& id,const string& name)
		:Person(name)//编译器只认这里的name
		,Student(name,num)//虚继承后,这里的name会被编译器忽略
		,Teacher(name,id)//同样被忽略
	{}
private:
	int age;
};

运行测试:

总结与补充:

1,只要直接的父类是同一个就加上virtual

2,假设有五层继承A->B->C->D->E

如果全是普通继承,E的初始化列表中只能有D的初始化和自己成员的初始化。

如果发生了虚继承,需要初始化虚基类B,上层直接父类,自身成员。

虚基类并不是一种特殊的类,而是在虚继承关系中,被 virtual 关键字修饰的那个共享基类。

E中的初始化列表中会包含B,C,D以及自身成员的初始化。

3,多继承中的指针偏移问题

结果:C

分析:直接父类在子类中的初始化的顺序和继承的顺序相同。

先继承Base1,再继承Base2,因此Base1在前,Base2在后。

七,继承和组合

1,public继承是⼀种is-a的关系。每个子类对象都是⼀个父类对象。

2,组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派⽣类的复用通常被称为白箱复用。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依 赖关系很强,耦合度高。

白箱:在继承方式中,基类的内部细节对派生类可见。

对象组合实现新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用被称为黑箱复用。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。

黑箱:对象的内部细节是不可见的。

总结:

1,黑箱测试相对简单,不用了解底层实现,从功能的角度测试,白箱测试相对较难,要了解底层实现,从代码运行逻辑测试。

2,优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承也适合组合,就用组合。


结尾

本篇文章中所涉及到的代码:

gitee链接:继承1 菱形继承与虚继承

以上就是本文的全部内容了,如果对你有所帮助的话,请大佬不妨给博主来个"一键三连",这对我是大大的支持,也能让博主产出更优质的内容。

往期回顾:

把值写进类型:C++ 模板进阶的非类型参数、特化与分离编译

总结:本文系统讲解C++继承机制,涵盖继承概念、访问控制变化、基类与派生类转换、默认成员函数处理、友元与静态成员特性,重点剖析多继承及菱形继承问题,并引入虚继承解决方案。强调继承与组合的对比。

⚡把上面的内容吃透就休息一下吧⚡

相关推荐
十年Java程序媛2 小时前
Java 接口和抽象类对比|抛弃老旧八股,正确选型
java·开发语言·spring boot·后端
sanqima2 小时前
MFC里的线程本地存储(TLS)与模块状态(Module State)
c++·mfc·tls·module state
kyle~2 小时前
C++ 智能指针:RAII、所有权与对象生命周期
开发语言·c++
y1su2 小时前
Leetcode 单调栈模板
java·开发语言·数据结构·算法·leetcode
2601_962885722 小时前
如何用 Python 计算索提诺比率和卡玛比率?
开发语言·python
郑州光合科技余经理2 小时前
出海外卖平台:支付对接为什么要和下单收款分开
android·java·开发语言·前端·后端·php·ai编程
kimnoic2 小时前
Python操作MySQL数据库的两种方式实例分析【pymysql和pandas】
开发语言·python
苏辰澈2 小时前
C++独属于自己的变身:多态
c语言·开发语言·c++·visualstudio·编辑器
谢亮_vipxieliang2 小时前
PHP 异常处理全解析:try-catch、自定义异常与全局异常处理器
开发语言·php