【C++】继承的那些事儿~

目录

1.继承的概念及定义

[1.1 继承的概念](#1.1 继承的概念)

[1.2 继承的定义](#1.2 继承的定义)

1.3继承类模板

2.基类和派生类的转换

3.继承中的作用域

[3.1 隐藏](#3.1 隐藏)

4.派生类的默认成员函数

[4.1 四个常见的默认成员函数](#4.1 四个常见的默认成员函数)

[4.2 如何实现一个不能被继承的类](#4.2 如何实现一个不能被继承的类)

5.多继承和菱形继承问题

[5.1 继承模型](#5.1 继承模型)

[5.2 虚继承](#5.2 虚继承)

[5.3 虚继承的原理](#5.3 虚继承的原理)

[5.4 多继承的指针偏移问题](#5.4 多继承的指针偏移问题)


1.继承的概念及定义

1.1 继承的概念

继承(inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法和属性,这样产生新的类称派生类

什么时候用继承呢?我们看这样一个场景:假设我们设计了两个类,Teacher和Student,他们都有的属性如姓名、居住地址、电话、年龄等,也都有Identity身份认证的成员函数,这样的话设计在两个类里面就显得冗余了。当然他们也会有自己的特性,老师授课和学生学习等。像这样,两个类具有相同的一些属性和方法时,我们就可以考虑使用继承手段。

我们将上述这个场景中相同的属性和方法放在Person这个类当中。具体代码实现如下:

cpp 复制代码
class Person
{
public:
	// ⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name;	 //姓名
	string _address; //地址
	string _tel;	 //电话
	int _age = 18;	 //年龄
};
class Student : public Person //共有继承
{
	
public:
	//学习
	void Study()
	{
		//...
	}
protected:
	int _stuid; //学号
};
class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string title;
	// 职称
};

1.2 继承的定义

上面的继承中,Person是基类,也称作父类;Student是派生类,也称作子类。

继承方式一共有三种:public、protected、private;不同的继承方式访问方式也不同。

|----------------|----------------|-----------------|--------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类public成员 | 派生类的protected成员 | 派生类private成员 |
| 基类的protected成员 | 派生类protected成员 | 派生类的protected成员 | 派生类private成员 |
| 基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |

通过上表我们可以发现,基类成员在派生类里所处的地位是Min(基类里的地位,派生类里的地位),总是取小的那一种。(private < protected < public)

基类private成员无论以什么样的继承方式在派生类当中都是不可见的。这里的不可见指的是派生类对象不管在类外还是类里面都不能去访问,但还是继承到了派生类对象。但如果基类成员不想在类外直接被访问,但是想要在派生类中被范围,就可以定义成protected。

继承方式不写的话,**class默认继承方式是private,struct默认是public,**这里的class和struct指的是派生类。实际中一般都是public继承。

另外说明:有元关系不能继承 ,也就是说基类的有元函数不能访问派生类的私有和保护成员;如果基类定义了static静态成员,那么整个继承体系里面也只会有这样一个成员,无论有多少个派生类,都只有一个static成员实例。

1.3继承类模板

派生类访问基类时,建议指定类域,否则会编译报错。必须指定类域的三种场景:

a.访问基类中的类型成员(typedef、using)

b.访问基类中的静态成员或枚举

c.访问基类中的成员函数(依赖模板参数时)

cpp 复制代码
template<typename T>
class Base {
public:
    void func() { cout << "Base" << endl; }
};

template<typename T>
class Derived : public Base<T> {
public:
    void test() {
        // func();  //  某些编译器可能报错(依赖查找失败)
        Base<T>::func();  //  明确告诉编译器去基类中找
        this->func();     //  另一种方式:通过 this 指针
    }
};

2.基类和派生类的转换

通常情况下,我们把一个类型的对象赋值给另一个类型的引用或者指针时,存在类型转换,中间有临时变量产生,需要加const;但public继承中就有一个例外,派生类对象可以赋值给基类的指针/引用,而不需要加const,这里绑定的时派生类对象中的基类部分,也就是说基类的指针或引用既可以指向基类对象,也可以指向派生类对象。基类对象不能赋值给派生类对象。

派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成,这个过程就像派生类自己定义部分成员切掉了⼀样,所以也被叫做切割或者切片。

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time TypeInformation)的dynamic_cast 来进行识别后进行安全转换。

3.继承中的作用域

3.1 隐藏

在继承体系中基类和派生类都有独立的作用域。

如果派生类和基类中有同名成员,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使同基类::基类成员 显示访问)

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。编译器会对析构函数名进行特殊处理,统一处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

cpp 复制代码
class Person
{
protected:
    string _name = "张三"; // 姓名
    int _num = 111;        // ⾝份证号
};
class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:" << _name << endl;
        cout << " ⾝份证号:" << Person::_num << endl;
        cout << " 学号:" << _num << endl;
    }
protected:
    int _num = 999; // 学号
};
int main()
{
    Student s1;
    s1.Print();
    return 0;
};

4.派生类的默认成员函数

和类的默认成员函数差不多,这里还是着重讲前四个常用的。

4.1 四个常见的默认成员函数

派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。派生类对象初始化先调用基类构造再调派生类构造。

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	Student(const char* name = "张三", int num = 18, const char* address = "成都")
		:Person(name)
		, _num(num)
		, _address(address)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{
		// 涉及深拷贝,需要自己实现
	}

	Student& operator=(const Student& s)
	{
		if(this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_address = s._address;
		}

		return *this;
		// 涉及深拷贝,需要自己实现
	}

	~Student()
	{
		// 不用显示调用基类析构,编译器会在派生类析构结束后自动调用析构
		// Person::~Person();
		//...
		cout << "~Student()" << endl;
	}

protected:
	int _num;		 // 学号
	string _address; // 地址
};

// 派生类一般要自己实现构造,不需要显示写析构、拷贝构造,赋值重载,除非派生类有深拷贝的资源需要处理

int main()
{
	Student s1;
	return 0;
}

Student(const Student& s)

:Person(s) 为什么这里直接传s就可以了呢?

因为 s 是 Student 对象,当它被传递给 Person 的构造函数时,编译器会自动将 Student 对象"切片"为 Person 对象 ,只提取 Person 基类部分(即 _name)来构造。C++允许派生类引用/指针隐式转换为基类引用/指针(向上转型,Upcasting)。

4.2 如何实现一个不能被继承的类

方法一:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法二:C++11新增了一个关键字final,final修饰基类,派生类就不能继承了。

cpp 复制代码
// C++11的⽅法
class Base final
{
public:
    void func5() { cout << "Base::func5" << endl; }
    protected:
    int a = 1;
private:
    // C++98的⽅法
    /*Base()
    {}*/
};

5.多继承和菱形继承问题

5.1 继承模型

单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承。也就是说哪怕是连续多个只要一个继承的派生类也依然是单继承。

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后继承的基类在后面,派生类成员在放到最后。

菱形继承:菱形继承是多继承的⼀种特殊情况。从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。

5.2 虚继承

对于菱形继承出现的问题,就需要虚继承来解决,菱形虚拟继承的底层实现会比较复杂,性能也会有所损失。

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
	/*int _tel;
	int _age;
	string _gender;
	string _address;*/
	// ...
};
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:
	int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
	int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性
	Assistant a;
	a._name = "peter";
	return 0;
}

5.3 虚继承的原理

通过监视窗口看到,Student和Teacher中也把Person"继承"下来了,不利于分析,接下来我们从内存窗口来看。

为方便研究,我们讲上述问题简化,其本质还是一样的。

cpp 复制代码
class A
{
public:
	int _a = 0;
};
class B : virtual public A
{
public:
	int _b = 1;
};
class C : virtual public A
{
public:
	int _c = 2;
};
class D : public B , public C
{
public:
	int _d = 3;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._a = 3;
	d._b = 4;
	d._c = 5;
	d._d = 6;

    B* p2 = &b;
    B* p1 = &d;

	return 0;
}

接下来,我们就深入分析上面这个菱形继承。调内存窗口我们可以看见:

p1 和 p2 里存储的偏移量就是来访问共同拥有的_a这个变量。

也就是说,被虚继承的类在vs里面,会放在末尾,各对象通过基类的指针指向的地址中存储的偏移量来访问,这就避免了数据冗余和二义性了。

5.4 多继承的指针偏移问题

下面说法正确的是?

A p1 == p2 ==p3 B.p1 < p2 < p3 C.p1 == p3 != p2 D.p1 ! =p2 != p3

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    return 0;
}

答案:C,通过上面的菱形继承原理,很容易就分析出答案了。

完~

相关推荐
ShineWinsu3 小时前
对于Linux:模版方法类的解析以及socket、TcpSocket的封装
linux·c++·面试·socket·模板方法模式·封装·tcpsocket
hold?fish:palm4 小时前
9 找到字符串中所有字母异位词
c++·算法·leetcode
啦啦啦啦啦zzzz5 小时前
算法:回溯算法
c++·算法·leetcode
Lzg_na5 小时前
订阅发布模块事例
c++
攻城狮Soar5 小时前
C++子类访问父类成员
c++·算法
郝学胜-神的一滴5 小时前
[简化版 GAMES 104] 现代游戏引擎 02:拆解现代游戏引擎5+1层级架构,吃透引擎底层核心逻辑
c++·unity·架构·游戏引擎·图形渲染·unreal engine·系统设计
汉克老师6 小时前
2026年CSP-J初赛分数线-----预测
c++·csp-j·小学生·学c++编程
脱胎换骨-军哥6 小时前
C++数据库存储引擎内核开发:B+树索引与MVCC并发控制的完整实现
数据库·c++·b树
小保CPP6 小时前
OpenCV C++将多张图像合并为webp动图
c++·人工智能·opencv·计算机视觉