【C++】继承

目录

一、概念及定义

1.1 概念

以前我们的接触过函数复用,而继承就是一种类复用,减少代码的重复性。继承可以在原有类的基础上扩展新的功能,产生新的类叫派生类或子类,原有类叫基类或父类。比如说学生类和老师类,它们共同的成员变量有名字和年龄,这时就可以写个Person类来处理公共的成员变量,不需要学生类和老师类自己再写名字和年龄的成员变量,只要写自己的独有的那部分即可。

1.2 定义

继承的写法:

class B : public A

B是子类,A是父类,中间要加冒号,public是继承方式(继承方式有3种)

如果是多继承:用逗号分开

class C : public A, public B

看下面代码:

cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}
protected:
	string _name = "yss";
	int _age = 19;
};

class Student : public Person
{
public:

private:
	int _stuid;
};

class Teacher :public Person
{
public:

private:
	int _jobid;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

Student类是子类,它的对象可以继承父类的Print函数,同时把父类的名字和年龄也继承下来。如果子类自己有Print函数,根据就近原则,那么只会调用自己的Print函数。

总结一下:

子类没有,父类有,子类对象用父类的;

不管父类有没有,子类有,子类对象用自己的

1.3 继承方式与访问权限

前面说到,继承方式有3种,分别是:

  • public
  • protected
  • private

访问权限也有3种,分别是:

  • public------类内外都可以访问
  • protected------子类和自己可以访问
  • private------只能自己访问

它们之间的关系如下图:

总结为以下几点:

  1. 基类的所有成员都可以被继承,但是私有成员不可被访问,类内类外都不行;保护成员只能继承给子类或者自己使用,在类外或者其他类不可以使用,所以才有保护限定符这个访问权限
  2. 继承下来的成员变量在派生类中的权限是继承方式与该成员变量在基类的访问权限的较小值(public>protected>private)
  3. 一般来说,实际当中主要还是用public继承,很少使用另外两种继承方式
  4. 继承方式如果没有显示的写,class默认的是private,struct默认的是public

二、基类与派生类对象的赋值转换

基类对象与派生类对象之间是可以进行赋值转换的,只能派生类对象赋值给基类,不能基类对象赋值给派生类对象。这种转换也叫切割或者切片,是把派生类中继承基类的那部分切割出来再赋值给基类。派生类的对象可以赋值给基类的对象/指针/引用。

写法:

cpp 复制代码
	B bb;//子类对象
	A a1 = bb;//赋值给基类对象
	A* a2 = &bb;//赋值给基类指针
	A& a3 = bb;//赋值给基类引用

调试证明子类对象赋值给基类对象:

三、继承中的作用域

继承中子类和父类是两个是独立的作用域,父类和子类出现同名的成员,子类会屏蔽父类对同名成员的访问,这种情况叫隐藏,如果是函数,仅需要函数名相同即可。前面谈过,如果子类和父类出现同名的函数或者同名的成员,子类对象优先使用自己的,要使用父类的需要域作用限定符------::,指明调用父类的。

同名成员变量构成隐藏关系

cpp 复制代码
class Person
{
public:
	
protected:
	string _name = "yss";
	int _age = 19;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}
private:
	string _name = "yyy";
	int _age = 29;
	int _stuid;
};

调用父类的:

cpp 复制代码
cout << Person::_name << endl;//类名+::
cout << Person::_age << endl;

同名成员函数构成隐藏关系

cpp 复制代码
class Person
{
public:
	void Print()
	{
		cout << "Person::void Print()" << endl;
	}
protected:
	string _name = "yss";
	int _age = 19;
};

class Student : public Person
{
public:
	void Print()
	{
		Person::Print();
		cout << "Student::void Print()" << endl;
	}
private:
	int _stuid;
};

调用子类的:

cpp 复制代码
//Person::Print();
cout << "Student::void Print()" << endl;

调用父类的:

cpp 复制代码
Person::Print();
//cout << "Student::void Print()" << endl;

四、派生类的默认成员函数

默认成员函数中的默认指没有传参的,或者我们不写编译器自动生成的。默认成员函数,对于内置类型完成值拷贝,对于自定义类型调用它的构造函数。继承中,派生类的有一部分是基类的,要完成这些基类的成员的构造/拷贝/赋值就必须先调用基类的构造函数/拷贝构造/赋值重载,再完成自己的。 析构函数呢?后面再说。

1️⃣构造:

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

class Student : public Person
{
public:
	Student(const char* name, const int num)
		:_num(num)
		, Person(name)//调用父类的构造函数
	{
		cout << "Student(const char* name, const int num)" << endl;
	}
private:
	int _num;
};

int main()
{
	Student s("yss", 122);

	return 0;
}

注:构造顺序与初始化列表的顺序无关,与成员变量声明的顺序有关。所以这里都是先完成父类成员的构造,再完成子类成员的构造

2️⃣拷贝构造:

cpp 复制代码
Person(const Person& p)
	:_name(p._name)
{
	cout << "Person(const Person& p)" << endl;
}

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

注:调用父类的拷贝构造时,传的参数是子类的对象,而父类的形参是父类的对象,这里就用到了切片的知识,即子类的对象可以赋值给父类对象,子类对象转换为父类对象。

3️⃣赋值重载:

cpp 复制代码
Person& operator=(const Person& p)
{
	if (this != &p)
	{
		_name = p._name;
		cout << "Person& operator=(const Person& p)" << endl;
	}
	return *this;
}
//
Student& operator=(const Student& s)
{
	if (this != &s)
	{
		_num = s._num;
		Person::operator=(s);
		cout << "Student& operator=(const Student& s)" << endl;
	}
	return *this;
}

注:父类与子类的赋值重载函数的函数名都是operator=,构成隐藏,因此要用域作用限定符指明调用父类的。

4️⃣析构:

cpp 复制代码
~Person()
{
	cout << "~Person()" << endl;
}
/
~Student()
{
	Person::~Person();
	cout << "~Student()" << endl;
}

注:父类的析构与子类的析构不同名,为什么还构成隐藏,因为多态的一些原因,析构函数被特殊处理,父类的析构和子类的析构都被处理为destructor()

看下运行结果:

这里父类析构了两次,为什么呢?再看看在子类的析构函数里注释掉调用父类析构会怎样。

cpp 复制代码
//Person::~Person();

我们发现析构函数的调用顺序与前面几个都不同,它是先调用子类的析构,再调用父类的析构。因为如果是先调用父类的析构,父类的成员此时就被清理了,假如子类有使用父类的成员(一般来说不会用),不就野指针了吗?所以为了保证不出错,析构的顺序是先子后父。

五、继承与友元

先直接上结论:友元不能被继承

例子:

cpp 复制代码
class B;//声明B类,因为在A类里display函数的参数有B类类型的参数
class A
{
public:
	friend void display(const A& a, const B& b);
protected:
	int _a = 10;
};

class B :public A
{
public:

protected:
	int _b = 20;
};

void display(const A& a, const B& b)
{
	cout << a._a << endl;//可访问
	cout << b._b << endl;//不可访问
}

int main()
{
	A aa;
	B bb;
	display(aa, bb);

	return 0;
}

子类也设置友元函数:

cpp 复制代码
friend void display(const A& a, const B& b);

运行结果:

六、继承与静态成员变量

如果在基类定义一个静态成员变量,那么这个变量将在整个继承体系中起作用。

cpp 复制代码
class A
{
public:
	A()
	{
		count++;
	}
	static int count;
};
int A::count = 0;
class B :public A
{
public:
	B()
	{
		count++;
	}
};
int main()
{
	B b;
	cout << b.count << endl;
	return 0;
}

子类对象先调用父类的构造函数再调用自己的,一共是两次

七、菱形继承与菱形虚拟继承

菱形继承是在有多继承的情况下出现的,具体如下图:

菱形继承有哪些缺陷,看以下代码:

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

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

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

class Mine :public Student, public Teacher
{
protected:
	int _age;
};

int main()
{
	Mine m;
	m._name = "yss";
	return 0;
}

Student类继承了_name,Teacher类也继承了_name,Mine类继承这两个类时不知道用谁的_name,有二义性。

解决二义性的办法:域作用限定符指明用哪个类的

cpp 复制代码
m.Student::_name = "yss";
m.Teacher::_name = "yyy";

但是还要一个问题------数据冗余。既然都是_name,为何不让它存在一个公共的区域呢

这里就需要使用虚拟继承,使用方法是在两个类继承同一个类时在这两个类设置virtual关键字。

代码:

cpp 复制代码
class Student :virtual public Person

class Teacher :virtual public Person

这时不需要指明谁的了(当然,想指明谁也行),因为在同一个区域,后面定义覆盖前面定义的,不仅解决了二义性,而且解决了数据冗余问题。

它们的地址是一样的。

前面我们只是知道是什么,怎么办,但是我们还需要知道为什么,即菱形继承和菱形虚拟继承的原理

菱形继承的原理:

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;
};

int main()
{
	D dd;
	dd.B::_a = 1;
	dd.C::_a = 2;
	dd._b = 3;
	dd._c = 4;
	dd._d = 5;
	return 0;
}


菱形虚拟继承原理:

先加上virtual

cpp 复制代码
class B : virtual public A
class C :virtual public A

打开调试:

虚基表指针分别指向一个虚基表,可以找到表中的偏移量,通过偏移量就可以找到公共区域

定义其他类的对象也是同理的,通过切片转换。

总结:

多继承是C++语法的一个缺陷,有了多继承就会有菱形继承,从而导致一些问题,所以一般都是写单继承,而不是多继承

八、继承与组合

public继承是一种is-a的关系(也叫白箱复用),也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系(也叫黑箱复用),假设B组合了A,每个B对象中都有一个A对象。

继承的耦合度高,改变基类容易影响派生类;组合的耦合度低,两个类的关系不大。所以一般来说能用组合就用组合。

相关推荐
Narutolxy2 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader9 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默20 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
yuyanjingtao22 分钟前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
Code哈哈笑29 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
程序猿进阶33 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184435 分钟前
shell 编程(二)
开发语言·bash·shell
闻缺陷则喜何志丹38 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie1145141911 小时前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满1 小时前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程