前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)

【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)

作者头像
ephemerals__
发布2024-12-25 10:00:41
发布2024-12-25 10:00:41
25100
代码可运行
举报
运行总次数:0
代码可运行

前言

在c++这门强大的编程语言中,面向对象编程(OOP)是一项核心特性,而继承则是OOP的重要支柱之一。继承机制极大地促进了代码的复用,增强了代码的可维护性和可扩展性。本篇文章,作者将和大家深入探讨C++中的继承机制。

一、什么是继承

继承(inheritance)是面向对象编程当中实现代码复用最重要的手段。继承允许我们在原有的类的基础之上进行扩展,创建一个新的类(叫做子类或派生类),该类可以继承原有类(叫做父类或基类)的属性和方法。继承体现了面向对象编程的层次结构--从简单到复杂的过程。

举个例子,有两个类teacher和student,分别表示“老师”和“学生”。对于这两者而言,可以具有共同的属性(如姓名、年龄、地址等),那么如果我们分别实现两个类,就会造成代码冗余。而继承就可以很好地解决此类问题。我们首先实现一个类叫做“person”,将它们的共同属性封装起来,然后让这两个类继承person类,再将一些特有属性定义在两类当中

二、继承的定义

1. 定义格式

继承的定义格式如下:

代码语言:javascript
代码运行次数:0
复制
class (派生类) : (继承方式) (基类)
{
	//属性和方法
}

接下来我们根据刚才的例子,实现一个简单的继承关系:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
using namespace std;

//基类
class person
{
	string _name;//姓名
	string _address;//地址
	int _age;//年龄
};

//派生类
class teacher : public person
{
	string _title;//职称
};

//派生类
class student : public person
{
	string _id;//学号
};

int main()
{
	person p;
	student s;
	teacher t;
}

调试窗口:

可以看到,student对象和teacher对象都继承了person类的成员。

从内存分布上来讲,派生类对象当中的基类部分位于低地址,派生类自己的成员位于高地址。

特别注意:当基类和派生类都是类模板时,派生类使用基类的成员函数要声明类域。例如:

代码语言:javascript
代码运行次数:0
复制
template<class T>
class Stack : public vector<T>
{
public:
	void push(const T& x)
	{
        //声明类域
		vector<T>::push_back(x);
	}
	//...
};

2. 继承方式

我们刚才在定义student和teacher时,在基类person之前加了一个 “public” 。该 “public” 并不是访问限定符,而是继承方式。与访问限定符相同,继承方式也用public、protected、private来表示。

那么不同的继承方式所带来的效果有何不同呢?

实际上,继承方式访问限定符共同决定了派生类访问基类成员的权限:

对于继承方式,需要注意以下几点:

1. 无论以什么方式继承,基类的私有成员在派生类当中都是无法访问的。这里的“无法访问”并不是指基类私有成员没有继承到派生类当中,而是语法限制导致不能访问。 2. 如果想要基类成员在派生类当中可以访问,而在类外无法访问,就将其设置为保护成员。 3. 定义派生类时,继承方式可以省略。此时若派生类标签是struct,则默认继承方式是public;若是class,则默认继承方式是private。不过最好还是显式注明继承方式。 4. 在实际运用当中,public继承最为常用,而protected继承和private继承使用较少。

三、赋值兼容转换

赋值兼容转换(也叫做切片),指的是派生类的对象可以直接赋值给基类的对象/引用,派生类对象的指针也可以直接赋值给基类的指针,而且赋值过程不会产生临时对象,寓意是将派生类中基类的成员部分切割出来,赋值给基类。

代码示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

class A
{
	int a = 1;
	int b = 2;
};

class B : public A
{
	int c = 3;
};

int main()
{
	B m;//派生类

	A n = m;//派生类对象赋给基类对象,通过调用基类的拷贝构造来完成

	A* p = &m;//派生类的指针赋给基类的指针

	A& r = m;//派生类对象赋给基类的引用
	return 0;
}

需要注意:

1. 基类的对象不能赋值给派生类对象。 2. 基类的引用或指针可以通过强制类型转换赋值给派生类的引用或指针,但只有在基类的引用或指针指向一个派生类的对象时才是安全的。

四、继承当中的作用域问题

继承当中,基类派生类都有各自独立的作用域

当基类和派生类有同名成员时(前提是保证派生类有权限访问基类的该成员),派生类当中就不能直接访问基类的同名成员,这种状况叫做隐藏。当然,你也可以通过声明类域的方式来访问基类同名成员,但是在继承体系当中最好不要使用同名成员。注意:对于成员函数,只要函数名相同就构成隐藏。

代码示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

class A
{
public:
	int m = 1;
	int n = 2;
};

class B : public A
{
public:
	void print()
	{
		cout << "m : " << m << endl;
		cout << "n : " << n << endl;
	}
private:
	int m = 10;//基类中的m被隐藏
};

int main()
{
	B b;
	b.print();
	return 0;
}

五、派生类的默认成员函数规则

接下来我们介绍一下派生类的构造函数、拷贝构造、析构函数、赋值重载的执行规则。

1. 派生类的构造函数需要先调用基类的构造函数来初始化基类部分的成员,然后再初始化自己的成员。如果基类没有默认构造函数,则需要通过初始化列表传参构造,否则会发生编译报错。

2. 与构造函数相同,派生类的拷贝构造函数也需要先调用基类的拷贝构造函数来初始化基类部分的成员

3. 派生类的赋值重载需要调用基类的赋值重载来完成基类部分的拷贝赋值。需要注意的是,两个赋值重载函数构成隐藏, 所以我们在显式实现派生类赋值重载时,调用基类赋值重载需要声明类域。

4. 派生类的析构函数在完成自己成员的清理之后,会调用基类的析构函数清理基类成员。注意:基类析构函数在不加virtual关键字的情况下,与派生类析构函数构成隐藏。

可以发现,派生类的默认成员函数常常需要调用基类的相应函数,以确保基类部分得到适当的构造、拷贝、赋值或销毁

那么我们是否可以利用这一规则,来实现一个不能被继承的类呢?

方法1:将基类的构造函数设置为私有成员,那么派生类就无法调用基类的构造函数,无法实例化出对象。

方法2:使用c++11新关键字final,限制该类不能被继承:

代码语言:javascript
代码运行次数:0
复制
class A final

六、继承与友元

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

举个例子:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

class B;//这里需要进行声明,否则友元函数无法识别类B
class A
{
	friend void fun1(const A& x, const B& y);
protected:
	int a = 1;
};

class B : public A
{
private:
	int b = 2;
};

void fun1(const A& x, const B& y)
{
	cout << x.a << endl;
	cout << y.b << endl;//报错,无法访问
}

七、继承与静态成员

当基类定义了一个静态成员,那么整个继承体系当中只有这一个静态成员。无论有多少个派生类,都只有这一个静态成员的实例。

代码示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

class A
{
public:
	static int a;
};

int A::a = 0;

class B : public A
{

};

int main()
{
	cout << &A::a << endl;
	cout << &B::a << endl;
	return 0;
}

可以看到,它们的地址是相同的。

八、多继承以及菱形继承问题

单继承:一个派生类只由一个直接基类所继承,称之为单继承。

多继承, 指的是一个派生类有两个及以上直接基类

代码示例:

代码语言:javascript
代码运行次数:0
复制
//基类
class B
{
	//...
public:
	int a = 1;
};

//基类
class C
{
	//...
public:
	int b = 2;
};

//派生类
class A : public B, public C
{
	//...
public:
	int c = 3;
};

一般情况下,多继承当中的内存分布是:根据语法层面,先继承的基类部分位于低地址,后继承的基类部分位于高地址,最高地址处是派生类自己的成员。

多继承增加了代码复用性和灵活性,但是该机制看似非常好用,实则十分鸡肋。菱形继承就是一个典型的缺陷。什么是菱形继承呢?

菱形继承是多继承的一种特殊情况,如图:

类A继承了类B和类C,但是B和C又分别继承了类D的成员。此时由于类B和类C都含有一份类D的成员,所以类A当中就会有两份相同成员,造成了数据冗余和二义性的问题。

代码示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
using namespace std;

class A
{
	//...
public:
	int a = 0;
};

class B : public A
{
	//...
public:
	int b = 1;
};

class C : public A
{
	//...
public:
	int c = 2;
};

class D : public B, public C
{
	//...
public:
	int d = 3;
};

int main()
{
	D x;
	cout << x.a << endl;//报错,a不明确
	return 0;
}

对于这种情况,就需要用到虚继承来解决。具体方法是:在菱形继承的腰部(也就是类B和类C处)使用关键字virtual进行继承。代码示例:

代码语言:javascript
代码运行次数:0
复制
//虚继承
class B : virtual public A
{
	//...
public:
	int b = 1;
};

//虚继承
class C : virtual public A
{
	//...
public:
	int c = 2;
};

注意:虚继承不要在其他地方使用

不难发现,菱形继承带来的困扰还是比较棘手。在实际应用当中,如果我们设计出多继承,则一定要仔细检查,尽量不要出现菱形继承的情况。

九、继承与组合

继承在一定程度上破坏了封装性,并且使派生类与基类之间产生了紧密的依赖,耦合度高。考虑到这些不足之处,我们提出“组合”的概念:

继承当中,每个派生类对象都是一个特殊的基类对象,是一种is-a的关系。 而组合指的是:将一个对象作为另一个对象的成员,是一种has-a的关系。

组合是继承之外的另一种复用选择。组合类之间并没有很强的依赖关系,耦合度低,写出的代码更容易维护,并且不会破坏类的封装性。所以说如果类之间的关系同时符合“has-a”和“is-a”,那么就尽量使用组合,而不是继承

总结

本篇文章,我们介绍了c++面向对象编程的重要特性之一--继承。 不难发现,继承使得我们的代码实现更加灵活,提高了代码复用率,但是其缺点也不可忽视。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、什么是继承
  • 二、继承的定义
    • 1. 定义格式
    • 2. 继承方式
  • 三、赋值兼容转换
  • 四、继承当中的作用域问题
  • 五、派生类的默认成员函数规则
  • 六、继承与友元
  • 七、继承与静态成员
  • 八、多继承以及菱形继承问题
  • 九、继承与组合
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档