C++修炼:继承

发布于:2025-05-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

        Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》《数据结构与算法之美》《题海拾贝》《C++修炼之路》

欢迎点赞,关注!

       

目录

1、继承引入

 2、访问方式

3、继承类模板

4、基类和派生类间的转换

5、继承中的作用域

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

7、继承与友元

8、继承与静态成员

9、菱形继承

10、继承和组合

 

        C++三大特性:封装、继承、多态。封装我们了解过了,这一期我们就来介绍一下继承。

1、继承引入

         继承的核心思想是允许一个类(派生类)基于另一个类(基类)扩展功能,实现代码复用和层次化设计。继承是类设计层次的复用。

        我举个例子帮助大家了解一下这个东西的意义。比方说我定义一个person类,这个类的成员变量包括这个人的姓名,身高,体重,性别...,我们再定义一些类,其中每个类代表一个特殊的人。那么每个人类包含的成员变量有姓名,职业,身高,体重,性别......那么我们发现姓名身高体重性别这些是所有人共有的。但是职业不是,职业可以有可以没有,一个人可以无业,可以是学生。那么我们在设计这个人的类的时候是不是会出现很多重复的操作?

        通过继承我们可以这样做:我们设计一个person类,这个类作为父类,或者叫基类(以下都成基类)。然后每个人类都继承一下这个person类,就可以把基类的私有变量继承下来。对于每个继承基类的类叫做派生类,或者子类(以下都称为派生类)。

        现在我们实现以上操作:

class person //基类
{
private:
	string name;
	int height;
	int weight;
	string sex;
};
//    基类   继承方式  派生类
class person1: public person//继承
{
private:
	string occupation;
};
class person2 : public person
{
private:
	string occupation;
	string religion;//这个人有宗教信仰
};

 2、访问方式

        继承有很多种访问方式,我们继承方式有三种:public,private,protected,对于基类的对象也有这三种,组合起来有九种访问方式。

        对于基类的private成员,派生类以什么方式继承都不可见。但需要注意这些private成员实际上继承下来了,只不过是不可见。

        对于这九种继承方式我们简单记一下,访问方式就是取最小,就是从成员在基类的访问限定符和继承方式之间取权限较小的,其中权限public>protected>private。

        举两个例子,如果我们public继承,对于基类的public成员,继承下来他的访问限定符就是public,对于protected成员,继承下来访问限定符就是protected。这也是protected和private的区别。protected的基类成员继承下来可以访问,private的基类成员继承下来无法访问。可以说protected就是因继承而产生的。

        private和protected继承同理,也是取权限较小。

class person //基类
{
public:
	string name;
	int height;
	int weight;
	string sex;
};
//    基类   继承方式  派生类
class person1: public person//继承
{
public:
	string occupation;
};
class person2 : public person
{
public:
	string occupation;
	string religion;//这个人有宗教信仰
};
int main()
{
	person1 h;
	h.name = "小张";//如果person类成员为private成员,编译报错该对象不可访问
	h.height = 180;
	h.weight = 130;
	h.sex = "male";
	return 0;
}

         一般我们都是用public继承。

3、继承类模板

        以下代码就实现了一个继承类模板:

template<class T>
class stack :public vector<T>
{
public:
	void push(const T& x)
	{
		vector<T>::push_back(x);
	}
	void pop()
	{
		vector<T>::pop_back();
	}
	const T& top()
	{
		return vector<T>::back();
	}
	bool empty()
	{
		return vector<T>::empty();
	}

};
int main()
{
	stack<int> st;
	st.push_back(10);
	st.pop();
	return 0;
}

        我们可以这样模拟实现一个stack。我们在实例化st的时候,其实也实例化出了一份vector<int>,所以我们才能使用vector里面的各种函数。

4、基类和派生类间的转换

        public继承的派生类对象(注意必须是public继承)可以复制给基类的指针/基类的引用。这种现象叫切片或者切割。

class Person
{
protected:
	string _name; // 姓名 
	string _sex; // 性别 
	int _age; // 年龄 
};
class Student : public Person
{
public:
	int _No; // 学号 
};

int main()
{
	Student s;

	Person& p1 = s;
	Person* p2 = &s;

	return 0;
}

        切割下来的一部分是派生类中基类的那部分。

p1._age = 11;//访问的是切割出来的一部分

        注意,基类对象不能直接复制给派生类对象。并且如果将派生类对象直接复制给基类对象不会发生切片现象。这样的话是会调用基类的拷贝构造,但其实还是会涉及到切片。

#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	Person()
	{
		cout << 1 <<" ";
	}
	Person(Person& p)
	{
		cout << 3 << " ";
		_age = p._age;
	}
protected:
	int _age; // 年龄 
};
class Student : public Person
{
public:
	Student()
	{
		cout << 2 << " ";
	}
	int _No; // 学号 
};

int main()
{
	Student s;
	Person p = s;//调用拷贝构造
	
	return 0;
}

        首先我们p还是在定义节点直接被s赋值,所以说走的是Person的拷贝构造。咱们拷贝构造必须是传引用(传值会导致无限拷贝),而我们传过去的是派生类对象,所以说会触发切片,p相当于是复制了s中的基类那一部分的值。

5、继承中的作用域

        首先明确一点,虽然说派生类继承了父类,但归根结底他们算两个类,所以说有两个不同的作用域。

        如果派生类和基类中有同名成员,派生类将屏蔽基类的同名成员。这种情况叫隐藏。同名函数也可以隐藏。当我们访问这个函数或者成员时,会优先在自己的作用域里找,也就是派生类作用域找,如果没有才回去查找继承下来的基类那一部分。

class Person
{
public:

	void Print()
	{
		cout << 10 << endl;
	}
	string _name; // 姓名 
	string _sex; // 性别 
	int _age; // 年龄 
};
class Student : public Person
{
public:
	void Print()
	{
		cout << 5 << endl;
	}

	int _No; // 学号 
};

int main()
{
	Student s;

	Person& p1 = s;
	Person* p2 = &s;
	s.Print();//5
	p1.Print();//10
	p2->Print();//10
	return 0;
}

        下面我们来看到小题熟悉一下:

        以下代码的编译运行结果是什么:

class A
{
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A
{
public:
 void fun(int i)
 {
 cout << "func(int i)" <<i<<endl;
 }
};
int main()
{
 B b;
 b.fun(10);
 b.fun();
 
 return 0;
};

        我就直接说结果了,运行以上代码会编译报错,因为两个函数构成隐藏,我们掉不到基类的fun,所以说第二次调用传入参数太少报错。

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

        首先派生类的构造函数必须调用基类的构造函数来初始化继承下来的那一部分成员。如果基类没有默认构造,那必须在派生类的初始化列表中显示调用。对于派生类的内置类型,初始化为随机值,自定义类型调用其构造函数(和之前一样)。基类没有默认构造会报错!

        在实例化派生类的时候一定是先调用基类的默认构造。就算显示写的时候把基类的构造函数写在初始化列表的最下边也是会先去调用基类的默认构造。我们可以这样理解。其实初始化列表走的顺序本来就和初始化列表的先后没关系,和那几个成员声明的先后有关系。我们的基类成员始终是在最上面的。

        对于析构,正确的析构应该是先析构派生类再析构基类。对于析构来说,由于多态的原因,编译器会把基类和派生类的析构函数的名字底层都处理为destructor。所以说两个析构函数实际上是构成隐藏关系的。那怎么办?难道需要我们手动调用父类的析构吗?其实不用。编译器为了保证先析构派生类后析构基类,会在派生类析构函数后面自己去偷偷调用基类的析构函数。编译器铺好了路,我们就不用管它啦。

        但其实析构这个地方还是有坑的,什么坑我先卖个关子,下一期多态我会再说。

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

7、继承与友元

        友元关系不能继承。比方说A和B是朋友,C继承了A,但是不能说B和A是朋友。

我们来看下面这串代码:

class Student;

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

// 友元关系不能继承
class Student : public Person
{
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;

	//cout << s._stuNum << endl;无法访问
}

        首先友元关系不能继承,所以说display函数不能访问s的私有变量。其次这串代码由于我们在声明友元的时候Student这个类还没有定义,所以说我们在最上面声明一下。这里只能这样解决,因为如果student放在person上面student又继承不到person了。

        声明的时候不需要加继承。

8、继承与静态成员

        如果基类定义了static静态成员,那么整个继承体系里面只有一个这样的成员,不论派生出多少类也只有这一个static成员。

#include<iostream>
#include<vector>
using namespace std;
class A
{
public:
	int _b;
	static int _a;
};
int A::_a=0;//静态成员要在类内声明,类外定义!
class B :public A
{
public:
	int _c;
};
class C :public A
{
public:
	int _d;
};
int main()
{
	A aa;
	B bb;
	C cc;
	bb._a = 10;
	cout << cc._a << endl;//10

	return 0;
};

9、菱形继承

        C++支持多继承,就是一个派生类继承两个基类

class A
{
public:
	int _b;
	static int _a;
};
int A::_a=0;//静态成员要在类内声明,类外定义!
class B 
{
public:
	int _c;
};
class C :public A ,public B//多继承
{
public:
	int _d;
};

        但是这也带来了一个问题,就是菱形继承。

        如果A继承了B和C,B和C又都继承了D,此时就构成了菱形继承。菱形继承会带来一些问题,第一就是数据冗余。因为A中有两份D的变量。第二就是二义性。A访问继承的D的变量需要指定访问的路径。

#include<iostream>
#include<vector>
using namespace std;
class D
{
public:
	int _d;
};

class C :public D
{
public:
	int _c;
};
class B:public D
{
public:
	int _b;
};
class A:public B,public C
{
public:
	int _a;
};
int main()
{
	A a;
	a._d = 0;
	return 0;
};

        如果直接访问会报错:

        需要按照以下方式访问:

	A a;
	a.B::_d = 0;
	a.C::_d = 1;

        那么解决办法是什么呢?第一就是虚继承。在继承A的时候加上virtual关键字 ,这样D中的成员变量只会实例化出一份。并且不需要访问方式限定,直接正常访问就行了。

class D
{
public:
	int _d;
};

class C : virtual public D
{
public:
	int _c;
};
class B: virtual public D
{
public:
	int _b;
};
class A:public B,public C
{
public:
	int _a;
};
int main()
{
	A a;
	a._d = 0;//正常访问
	return 0;
};

        但是这种方式会造成额外的开销,所以说第二个解决办法就是不要写出菱形继承。当然也有的情况是不得不写菱形继承。那么这种情况再考虑用virtual关键字。

        接下来我们看一道小题:

        多继承中指针偏移问题?下面说法正确的是()

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

#include<iostream>
using namespace std;
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。我把这道题的图画出来大家就理解了。 

        在类中,先继承的在上面,后继承的在下面,最后是自己的成员变量。所以说这道题选C。

        图中这个类包含的三部分每部分之间都有一些空隙,其实这个空隙可能有,可能没有,受内存对齐规则的影响。

10、继承和组合

        public继承是一种is-a的关系,每一个派生类对象都是一个基类对象。而组合是一种has-a的关系,假设B组合A,每个B对象都有一个A对象。

class A
{
public:
	int _a;
};
//is-a
class B :public A
{
public: 
	int _b;
};

//has-a
class C
{
public:
	int _c;
	class A;
};

         继承这种复用方式叫白箱复用。另外友元也是一种白箱复用。白和黑是相对于可见性来说的。与白箱复用相对的黑箱复用,不如组合和接口,都是黑箱复用。

        白箱复用耦合度高,比方说基类和派生类,更改基类会影响到派生类。而组合耦合度低,因为我们不关心被组合的类是如何实现的,我们只需调一下他的接口就行(比如之前模拟实现stack和queue)。

        写代码的时候我们要优先使用组合,因为使用继承其实是会破坏封装的。但是必要时也得用继承,包括下一篇我们要说的多态也是在继承的基础上实现的。

        好了,今天的内容就分享到这,我们下期再见!

 


网站公告

今日签到

点亮在社区的每一天
去签到