【C++】继承

目录

[1. 继承的基本使用](#1. 继承的基本使用)

[2. 继承的语法](#2. 继承的语法)

[3. 父类和子类的赋值兼容转换](#3. 父类和子类的赋值兼容转换)

[4. 隐藏 / 重定义](#4. 隐藏 / 重定义)

[5. 子类/派生类的默认成员函数](#5. 子类/派生类的默认成员函数)

构造函数

拷贝构造

赋值重载

析构函数

[6. 继承与友元](#6. 继承与友元)

[7. 继承与静态成员](#7. 继承与静态成员)

[8. 多继承与菱形继承](#8. 多继承与菱形继承)

[9. 继承和组合](#9. 继承和组合)

写在最后:


1. 继承的基本使用

继承是面向对象的三大特性之一,他的重要性自然不言而喻了~

我们来看这样一个经典的场景:

cpp 复制代码
#include <iostream>

using namespace std;

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

protected:
	string _name = "张三";
	int _age = 18;
};

class Student : public Person {
protected:
	int _stuid;
};

class Teacher : public Person {
protected:
	int _jobid;
};

void test1() {
	Person p;
	p.Print();
	
	Student s;
	s.Print();

	Teacher t;
	t.Print();
}

int main()
{
	test1();

	return 0;
}

在这段代码中 Student 类和 Teacher 类都没有 Print 函数和那两个成员变量,

但是他们却可以调用这些,这就是继承的用法,

可以把自己的成员继承给其他的类来使用。

2. 继承的语法

基类我们也可以叫做父类,派生类我们也叫做子类。

然后一共有三种继承方式:

public 公有继承,protected 保护继承,private 私有继承。

然后我们也有三种成员,也就是公有保护私有这三种。

所以就组合成了 9 中情况,也就是这张经典的图:

虽然这些看起来很复杂,但是我们只要理解了还是比较好记忆的,我这里总结了一下:

1. private 成员就是私有的,不会继承,只有父类(基类)自己能用。

(这样我们就不用管 private 成员了)

2. 公有继承就原封不动的继承,保护继承就都变成保护成员,私有继承也都变成私有成员

其实最后就这两大种情况,一下子就能记住了。

说到这里我们就不得不说一下我们的 protected 成员了,

他在其他地方跟 private 成员的效果是一模一样的,就是在继承方面效果不同。

不过在实际的业务场景中啊,我们基本都是使用公有继承,几乎不会用到其他两种继承方式。

3. 父类和子类的赋值兼容转换

来看这个场景:

cpp 复制代码
void test2() {
	Person p;
	Student s;

	// 赋值兼容(切割/切片)
	p = s;
}

子类对象可以把自己给父类,

将子类对象中和父类相同的部分赋值给父类。

而且他这样并不是一种类型转换:

cpp 复制代码
void test2() {
	Student s;
	Person& p = s;
}

他可以支持这样的拷贝构造,

也就是这种行为没有产生临时变量。

4. 隐藏 / 重定义

来看这样一个场景:

cpp 复制代码
class Person
{
protected:
	string _name = "张三";
	int _age = 18;
};

class Student : public Person {
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "李四";
	int _stuid;
};

void test3() {
	Student s;
	s.Print();
}

输出:

当父类和子类用同名的成员的时候,

子类的成员就会隐藏父类的成员(就默认调用自己的成员)

当然不仅仅是成员变量,成员函数也会隐藏:

cpp 复制代码
class Person
{
public:
	void func() { cout << "Person::func()" << endl; }

protected:
	string _name = "张三";
	int _age = 18;
};

class Student : public Person {
public:
	void func() { cout << "Student::func()" << endl; }

	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "李四";
	int _stuid;
};

void test3() {
	Student s;
	s.func();
}

输出:

那如果我们就是要调用父类的可以吗?

我们可以直接把子类的函数注释了,就会直接调用父类的了,

还有一种方法,就是可以指定我们去找这个函数的作用域:

cpp 复制代码
void test3() {
	Student s;
	s.Person::func();
}

输出:

这样找到的就是父类的函数了。

这个时候花活就来了:

cpp 复制代码
class Person
{
public:
	void func() { cout << "Person::func()" << endl; }

protected:
	string _name = "张三";
	int _age = 18;
};

class Student : public Person {
public:
	void func(int i) { cout << "Student::func()" << endl; }

	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "李四";
	int _stuid;
};

如果我们给 func 函数多加一个参数,那子类和父类这两个函数是什么关系呢?

a. 隐藏/重定义 b. 重载 c. 重写/覆盖 d. 编译报错

答案选的是 a,他们构成的是隐藏。

解析:

父子类域中,函数名相同就构成隐藏。

总结:

我们使用继承的时候,最好就不要定义同名的成员。

5. 子类/派生类的默认成员函数

构造函数

我们来看这样一个例子:

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

	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name = "张三", int id = 0)
		: _id(id)
	{
		cout << "Student()" << endl;
	}

protected:
	int _id; 
};

void test4() {
	Student s;
}

我们创建了一个 Student 的对对象,

输出:

结果父类对象也调用了构造函数和析构函数。

继承的语法规定在创建子类的时候,会自动调用父类来初始化父类的成员。

而且父类必须要有默认构造函数。

那如果父类没有默认构造我们该怎么调用呢?

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

	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name = "张三", int id = 0)
		: Person(name)
		, _id(id)
	{
		cout << "Student()" << endl;
	}

protected:
	int _id; 
};

void test4() {
	Student s;
}

我们可以在子类的初始化列表定义这样一个东西。

拷贝构造

拷贝构造也是同理:

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

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name = "张三", int id = 0)
		: Person(name)
		, _id(id)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}

protected:
	int _id; 
};

void test4() {
	Student s1;
	Student s2 = s1;
}

父类的成员也需要调用父类的拷贝构造来完成拷贝,

而子类里面并没有父类的对象,我们就直接传递子类对象给父类:

这就是赋值兼容转换,或者说切片的应用,

我们传子类对象给父类,父类就直接切片成父类了:

赋值重载

赋值重载也是同样的设计思路:

cpp 复制代码
class Person
{
public:
	//Person(const char* name = "peter")
	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 id = 0)
		: Person(name)
		, _id(id)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator=(const Student& s) {
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s) {
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}

protected:
	int _id; 
};

void test4() {
	Student s1;
	Student s2 = s1;
	s1 = s2;
}

父类给自己的成员赋值,

子类调用父类的赋值重载,并实现自己的赋值重载。

析构函数

析构函数不需要我们手动调用,所以就基本不用管。

6. 继承与友元

我就直接说结论了,友元关系不能继承,

说人话就是,父类友元不能访问子类私有和保护成员。

7. 继承与静态成员

你可以认为他继承了,

具体来说,子类并没有把静态成员拷贝一份,

用贴切的话来说就是继承了静态成员的使用权,

让我们可以通过指定子类的类域来调用这个静态成员。

(继承之后,静态成员同时属于父类和子类)

8. 多继承与菱形继承

我们前面的场景都称之为单继承,一脉相传的就是单继承,

而 C++ 还支持多继承,

比如说一个人他可以是学生也可以是老师:

cpp 复制代码
class Teacher {
protected:
	int _tid;
};

class Student {
protected:
	int _sid;
};

class Person : public Teacher, public Student {
protected:
	string _name;
	int _age;
};

这就是一个多继承。

但是有多继承就会可能会出现菱形继承的问题:

cpp 复制代码
class Person {
public:
	int _age;
};

class Teacher : public Person {
protected:
	int _tid;
};

class Student : public Person {
protected:
	int _sid;
};

class Assistant : public Teacher, public Student {
protected:
	string _name;
};

void test5() {
	Assistant a;
	a._age = 18;
}

那编译器就无法判断,这个 age 究竟是谁的 age,出现了二义性,

然后就报错了:

我们可以通过指定类域的方式来解决:

cpp 复制代码
void test5() {
	Assistant a;
	a.Teacher::_age = 30;
	a.Student::_age = 18;
}

但其实这样的设计也不太好,多继承的坑还是太多了,

比如说 Java 他们就直接没支持多继承这个语法。

还有一个解决二义性的方法,就是虚继承:

cpp 复制代码
class Person {
public:
	int _age;
};

class Teacher : virtual public Person {
protected:
	int _tid;
};

class Student : virtual public Person {
protected:
	int _sid;
};

class Assistant : public Teacher, public Student {
protected:
	string _name;
};

void test5() {
	Assistant a;
	a._age = 18;
}

使用虚继承之后,就没有再报错了。

那为什么虚继承之后,就不会报错了呢?这又是什么原理?

我们可以去深入看一下他的对象模型,探索一下原因。

我们以这个菱形继承为例:

cpp 复制代码
class A {
public:
	int _a;
};

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

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

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

void test6() {
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

我们通过内存窗口可以观察他的对象模型:

一二行存的是 B 对象的 _a 和 _b

三四行存的是 C 对象的 _a 和 _c

第五行存的是 D 对象的 _d

所以 D 对象包含了 B 和 C 对象的成员 + 自己的成员。

我们再来看看菱形虚拟继承是什么样的:

cpp 复制代码
class A {
public:
	int _a;
};

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

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

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

void test6() {
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	d._a = 0;
}

来看看对象模型:

我们可以看到这个对象模型是发生了巨大变化的,

不过,B 和 C 对象的位置没变,但是内容变化了,

A 对象中的 _a 变量到了最后。

我们先来探索一下 B,C 对象不一样的东西是什么:

我们看到他们里面一个存着 20,一个存着 12,这又是什么意思呢? (十进制)

发现了没有:

第一个位置到 _a 位置的地址相差 20,

第二个位置到 _a 位置的地址相差 14,

也就是他们里面存的是距离 _a 位置的偏移量,编译器可以通过偏移量找到 _a。

最后:

不建议使用多继承。

9. 继承和组合

这里就是介绍一下组合是什么样的:

cpp 复制代码
class C {
private:
	int _c;
};

// 继承
class B : public C {
private:
	int _b;
};

// 组合
class A {
private:
	C c;
};

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

相关推荐
程序猿-瑞瑞2 分钟前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
qq_433554542 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能11 分钟前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts
一只自律的鸡12 分钟前
C语言项目 天天酷跑(上篇)
c语言·开发语言
程序猿000001号14 分钟前
使用Python的Seaborn库进行数据可视化
开发语言·python·信息可视化
ཌ斌赋ད17 分钟前
FFTW基本概念与安装使用
c++
一个不正经的林Sir19 分钟前
C#WPF基础介绍/第一个WPF程序
开发语言·c#·wpf
带多刺的玫瑰23 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
API快乐传递者24 分钟前
Python爬虫获取淘宝详情接口详细解析
开发语言·爬虫·python
公众号Codewar原创作者26 分钟前
R数据分析:工具变量回归的做法和解释,实例解析
开发语言·人工智能·python