【C++】继承

🚀个人主页:奋斗的小羊 🚀所属专栏:C++ 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

目录


💥1、继承

💥1.1 继承的概念

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

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用

cpp 复制代码
class Student
{
public :
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	} 

	// 学习
	void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};

class Teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	} 

	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};

上面的StudentTeacher中成员函数和成员变量有相同的地方,下面我们将公共的部分放到Person中,然后让StudentTeacher都继承Person,就可以复用这些成员,使代码不再冗杂。

cpp 复制代码
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	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成员 子类的public成员 子类的protected成员 子类的private成员
父类的protected成员 子类的protected成员 子类的protected成员 子类的private成员
父类的private成员 子类中不可见 子类中不可见 子类中不可见
  1. 父类private成员在子类中无论以什么方式继承都是不可见的 。不可见是指父类的私有成员还是被继承到了子类中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它
  2. 父类private成员在子类中是不能被访问的,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行总结就会发现,父类的私有成员在子类都是不可见的,而父类的其他成员在子类中的访问方式== Min(成员在父类的访问限定符,继承方式),public > protected > private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  5. 在实际运用中一般使用都是public继承 ,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承 ,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强

💥2、父类和子类对象赋值兼容转换

  • public继承的子类对象可以赋值给父类的对象 / 父类的指针 / 父类的引用 。有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去
  • 父类对象不能赋值给子类对象
  • 父类的指针或者引用 可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的
cpp 复制代码
class Person
{
protected:
	string _name;
	string _sex;
	size_t _age;
};

class Student : public Person
{
public:
	int _no;//学号
};

int main()
{
	Student s;

	Person p = s;
	Person* pp = &s;
	Person& rp = s;
	return 0;
}

💥3、继承中的作用域

  1. 在继承体系中父类和子类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,在子类成员函数中,可以使用 父类::父类成员 显示访问
  3. 如果是成员函数,只要是函数名相同就构成隐藏
  4. 在实际的继承体系中最好不要定义同名成员
cpp 复制代码
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;
};
  • A类和B类中的两个fun函数构成的是隐藏关系,不是重载
  • 上面的程序编译运行的结果是编译报错 ,因为A类中的fun函数被隐藏,B类中的fun函数形参没有缺省值,所以主函数中的代码b.fun()会报错,如果想要调用A类中的fun函数,需要指定作用域:b.A::fun();

💥4、子类的默认成员函数

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

  1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用
  2. 子类的拷贝构造函数必须调用父类的拷贝构造函数完成父类的拷贝初始化。一般来说子类的拷贝构造函数默认生成的就够用了,如果有需要深拷贝的资源才需要我们自己实现
  3. 子类的赋值重载必须调用父类的赋值重载完成父类的赋值。需要注意的是子类的operator=隐藏了父类的operator= ,所以显示调用父类的operator=需要指定父类作用域
  4. 规定 :子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。这样做是为了保证子类对象先清理子类成员再清理父类成员的顺序
  5. 子类对象初始化先调用父类构造再调用子类构造
  6. 子类对象析构清理先调用子类析构再调用父类析构
  7. 因为多态中一些场景析构函数需要构成重写 ,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
cpp 复制代码
#include <iostream>
using namespace std;

class Person
{
public:
	Person(const char* name = "peter")
		:_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)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	} 
	
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	} 
	
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			// 构成隐藏,所以需要显示调用
			Person::operator=(s);
			_num = s._num;
		} 
		return *this;
	} 

	~Student()
	{
		//子类的析构和父类的析构也构成隐藏关系,如果调用需要指定类域
		//但是子类析构后会自动调用父类析构,所以不需要显示调用
		//Person::~Person();
		cout << "~Student()" << endl;
	}

protected:
	int _num; //学号
};

int main()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
	return 0;
}

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

  1. 父类的构造函数私有,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化以后,子类看不见就不能调用了,那么子类就无法实例化出对象
  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()
	{}*/
};

class Derive : public Base
{
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

💥5、继承与友元

  • 友元关系不能被继承,也就是说父类友元不能访问子类私有和保护成员
cpp 复制代码
//前置声明
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;
} 

int main()
{
	Person p;
	Student s;
	// 编译报错:error C2248: "Student::_stuNum": ⽆法访问 protected 成员
	// 解决⽅案:Display也变成Student 的友元即可
	Display(p, s);
	return 0;
}

💥6、继承与静态成员

  • 父类定义了静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个静态成员实例化
cpp 复制代码
class Person
{
public:
	string _name;
	static int _count;
};

int Person::_count = 0;

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

int main()
{
	Person p;
	Student s;

	// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
	// 说明⼦类继承下来了,⽗⼦类对象各有⼀份

	cout << &p._name << endl;
	cout << &s._name << endl;

	// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
	// 说明⼦类和⽗类共⽤同⼀份静态成员

	cout << &p._count << endl;
	cout << &s._count << endl;

	// 公有的情况下,⽗⼦类指定类域都可以访问静态成员

	cout << Person::_count << endl;
	cout << Student::_count << endl;

	return 0;
}

💥7、多继承及菱形继承问题

💥7.1 继承模型

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
  • 多继承 :一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型是:先继承的父类在前面,后面继承的父类在后面,子类成员在放到最后面。
  • 菱形继承 :菱形继承是多继承的一种特殊情况。菱形继承有数据冗余和二义性 的问题,支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,实践中不建议设计出菱形继承这样的模型

单继承:

多继承:

菱形继承:

cpp 复制代码
class Person
{
public :
	string _name; // 姓名
};

class Student : public Person
{
protected :
	int _num; //学号
};

class Teacher : public Person
{
protected :
	int _id; // 职⼯编号
};

class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse; // 主修课程
};

int main()
{
	// 编译报错:error C2385: 对"_name"的访问不明确
	Assistant a;
	a._name = "peter";

	// 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

💥7.2 虚继承

  • 在继承方式前加上virtual
  • 一个爷爷派生出两个儿子,再由一个孙子继承这两个儿子,形成菱形继承,注意是在两个儿子的继承方式前+virtual
cpp 复制代码
class Person
{
public :
	string _name; // 姓名
};

// 使⽤虚继承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;
}

💥7.3 多继承中指针偏移问题

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;
}
  • 先继承的在前面,所以三个指针的关系是:p1 == p3 != p2

💥8、继承和组合

  • public继承是一种is-a的关系,也就是说每个子类对象都是一个父类对象
  • 组合是一种has-a的关系,如果B组合了A,则每个B对象中都有一个A对象
  • 继承允许我们根据父类的实现来定义子类的实现,这种通过生成子类的复用通常被称为 "白箱复用" 。白箱是相对可视性而言,在继承方式中,父类的内部细节对子类可见,所以继承一定程度上破坏了父类的封装,父类的改变,对子类有很大的影响,子类和父类之间的依赖关系很强,耦合度高
  • 对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为 "黑箱复用" ,因为对象的内部细节是不可见的,对象只以黑箱的形式出现。组合类之间没有很强的依赖关系,耦合度低
  • 优先使用组合,而不是继承 。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合
cpp 复制代码
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire 
{
protected:
	string _brand = "Michelin"; // 品牌
	size_t _size = 17; // 尺⼨
};

class Car 
{
protected:
	string _colour = "⽩⾊"; // 颜⾊
	string _num = "陕ABIT00"; // ⻋牌号
	Tire _t1; // 轮胎
	Tire _t2; // 轮胎
	Tire _t3; // 轮胎
	Tire _t4; // 轮胎
};

class BMW : public Car 
{
public:
	void Drive() { cout << "好开-操控" << endl; }
};

// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:
	void Drive() { cout << "好坐-舒适" << endl; }
};

// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class vector
{};

template<class T>
class stack : public vector<T>
{};

template<class T>
class stack
{
public :
	vector<T> _v;
};

int main()
{
	return 0;
}

相关推荐
蜀黍@猿21 分钟前
【C++ 基础】从C到C++有哪些变化
c++
Am心若依旧40922 分钟前
[c++11(二)]Lambda表达式和Function包装器及bind函数
开发语言·c++
花生糖@25 分钟前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
是程序喵呀29 分钟前
MySQL备份
android·mysql·adb
casual_clover31 分钟前
Android 之 List 简述
android·list
zh路西法32 分钟前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(一):从电梯出发的状态模式State Pattern
c++·决策树·状态模式
轩辰~1 小时前
网络协议入门
linux·服务器·开发语言·网络·arm开发·c++·网络协议
lxyzcm1 小时前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
蜀黍@猿1 小时前
C/C++基础错题归纳
c++
雨中rain2 小时前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++