C++ 之 继承

发布于:2025-05-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

1.继承的概念及定义

1.1继承的引入

我们设计一个person类,类中包含姓名、年龄、身高....等数据成员

我们再设计一个student类,类中也需要包含姓名、年龄、身高...等数据成员

我们再设计一个teacher类,类中也需要包含姓名、年龄、身高...等数据成员

显然,姓名、年龄、身高等数据成员重复了

这时候就可以使用继承的语法,让teacher、student类分别继承person类

以简化代码,达到复用的效果

class person
{
public:
	//成员函数
protected:
	string _name;
	int _age;
	int _length;
};

class student : public person
{

protected:
	int _id;//学号
};

class teacher : public person
{

protected:
	string _subject;//学科
};

 

1.2继承概念

在C++中,继承(Inheritance) 是一种面向对象编程(OOP)的核心机制,允许一个类(称为派生类子类)基于另一个类(称为基类父类)来构建,从而共享基类的属性和方法                                                                                                                            ---文心一言

1.3继承的定义

1.3.1定义格式

Person是父类,也称作基类。Student是子类,也称作派生类

1.3.2继承关系与访问限定符

有三种继承关系:public、protected、private

有三种访问限定符:public、protected、private

一共有9组组合,组合的结果如下

总结:

(1)基类的 private 成员无论以什么形式继承都不可见

不可见的意思是,派生类确实继承了基类的private成员,但是无论是在派生类类中还是在派生类外(通过派生类对象)都不可访问这些private成员

(1)基类的成员想要被派生类访问而不能被类外访问,就需要用protected限定

(1)基类的其他成员(public、protected)在子类的访问方式 == 

Min(成员在基类的访问限定符,继承方式),

其中,public > protected > private

(1)使用关键字class默认的继承方式是private,使用struct默认的继承方式是public,不过 最好显示的写出继承方式

2.基类和派生类对象的赋值转换

(1)基类对象不能赋值给派生类对象

我们可以理解为派生类极大可能拥有基类没有的成员,

强行将基类对象赋值给派生类对象可能会导致一些数据丢失等问题

所以C++语法上禁止了将基类对象赋值给派生类对象的操作

(2)派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用

int main()
{
	//基类对象
	person p;
	//派生类对象
	teacher t;

	p = t;
	person* ptrp = &t;
	person& rp = t;
	return 0;
}

这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去 

 

此时,基类类型的指针指向只是派生类中包含的基类成员

此时,基类类型的引用只是派生类中包含基类成员那一部分的引用

(3)基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针指向派生类对象时才是安全的

3.继承中的作用域

(1)在继承体系中基类和派生类都有独立的作用域

class A
{
protected:
	int _a = 10;
};

class B : public A
{
public:
	void func()
	{
		cout << _a << endl;
	}
protected:
	int _b;
	int _a = 1;
};

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

A类与B类中都有同名成员 _a,因为两个类具有各自的作用域,所以同名成员可以共存

但是,此时在派生类B类里类外都无法直接访问A类中的_a。

这就引出了一个新概念:隐藏

(2)子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义

 

(3)在子类成员函数中,可以使用 基类::基类成员 显示访问被隐藏的成员

(4)

class A
{
public:
	void func()
	{
		cout << _a << endl;
	}
protected:
	int _a = 10;
};

class B : public A
{
public:
	void func(int x)
	{
		cout << _a << endl;
		cout << A::_a << endl;

	}
protected:
	int _b;
	int _a = 1;
};

int main()
{
	B b;
	b.func();//wrong 编译报错
	return 0;
}

在继承体系中,只需要基类和派生类的函数名相同就构成隐藏

B类中的func函数与A类中的func函数不是形成函数重载(函数重载要求在同一个作用域中)

所以,上述代码中编译器只能访问B类中的func函数,

但是用户没有传参数,就会报错

所以基类和派生类定义同名成员有风险,尽量不要这样做

4.派生类的默认成员函数

(1)构造函数

  • 基类构造函数由派生类构造函数间接调用:通过初始化列表或隐式机制触发。
  • 调用顺序:基类构造函数 → 成员变量初始化 → 派生类构造函数体

如果基类没有默认构造函数,需要在派生类构造函数的初始化列表阶段显示调用

class person
{
public:
	person(const char* name")
		:_name(name)
	{ }
private:
	string _name;
};

class student : public person
{
public:
	student(int id = 10)
		:_id(id)
        ,person("李四")
	{}
private:
	int _id;
};

person类中没有默认构造,student类中就需要显示调用person类的构造函数

(2)拷贝构造函数

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

student(const student& s)
	:person(s)
	,_id(s._id)
{}

注意,调用基类的拷贝构造函数时,传入的是一个派生类对象

如果不显示调用基类的拷贝构造函数,编译器会自动尝试调用基类的拷贝构造函数

如果基类没有定义拷贝构造函数,编译器会为基类生成一个默认的拷贝构造函数(按成员拷贝),此时基类的默认构造函数不会被调用,而是按成员拷贝基类部分

(3)赋值重载函数

派生类的operator=必须要调用基类的operator=完成基类的复制

	student& operator=(const student& s)
	{
		if(this != &s)
		{
			person::operator=(s);
			_id = s._id;
		}
	}

(1)两个类中的赋值重载函数构成了隐藏,调用基类的赋值重载函数时,需要指明类域

(2)调用基类的赋值重载函数时,传入的是一个派生类对象

(4)析构函数        

~student()
{}

在派生类的析构函数中不用显示调用基类的析构函数,派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。

因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

5.继承和友元

友元关系不能继承,基类友元不能访问子类私有和保护成员

6.继承和静态成员

基类定义了static静态成员,则整个继承体系里面(子类父类中)只有一个这样的成员

7.菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

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

指一个派生类通过两条或以上路径间接继承了同一个基类,从而形成类似“菱形”的继承结构

菱形继承存在两个问题,

(1)数据冗余:assistant 对象中包含两份 person类的成员

(2)二义性问题: 既然assistant 对象中包含两份 person类的成员

那么 直接通过 assistant 对象 访问 person类的成员就会出现二义性问题

因为编译器无法确定访问的

到底是student中的person还是

teacher中的person

 

虚拟继承可以解决菱形继承的二义性和数据冗余的问题

class Person
 {
 public :
 string _name ; // 姓名
};
 class Student : virtual public Person
 {
 protected :
 int _num ; //学号
};
 class Teacher : virtual public Person
 {
 protected :
 int _id ; // 职工编号
};
 class Assistant : public Student, public Teacher
 {
 protected :
 string _majorCourse ; // 主修课程
};
 void Test ()
 {
 Assistant a ;
 a._name = "peter";
 }

在"腰部"位置实现虚拟继承,即继承方式前面加上 virtual 关键字

(1)虚拟继承解决数据冗余和二义性的原理

class A
{
public:
	int _a;
 };
 // class B : public A
 class B : virtual public A
 {
 public:
	 int _b;
 };
 // class C : public A
 class C : virtual public A
 {
 public:
	 int _c;
 };
 class D : public B, public C
 {
 public:
	 int _d;
 };
 int main()
 {
	 D d;
	 d.B::_a = 1;
	 d.C::_a = 2;
	 d._b = 3;
	 d._c = 4;
	 d._d = 5;
	 return 0;
 }

上述代码中对象的内存分布

虚拟继承以后,对象d只包含一份A的成员a

而原来B、C存放A的成员的位置现在存放着一个地址(虚基表指针)

虚基表指针指向的东西叫作虚基表

虚基表存放着B、C分别与A的成员a的偏移量,这样通过B、C也可以找到A的成员

所以前面的虚拟继承以后的样子就是

8.继承的反思与总结

(1)可以实现多继承,但一定不要设计出菱形继承。否则在复杂度及性能上都有问题

继承与组合

(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象

组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象

(2)在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响

组合类之间没有很强的依赖关系,实际尽量多去用组合

// Car和BMW Car和Benz构成is-a的关系
class Car{
 protected:
 string _colour = "白色";  // 颜色
string _num = "陕ABIT00";    
// 车牌号
   };
 class BMW : public Car{
 public:
 void Drive() {cout << "好开-操控" << endl;}
   };
 class Benz : public Car{
 public:
 void Drive() {cout << "好坐-舒适" << endl;}
   };
 // Tire和Car构成has-a的关系
class Tire{
 protected:
 string _brand = "Michelin";  // 品牌
size_t _size = 17;            
   };
 // 尺寸
class Car{
 protected:
 string _colour = "白色";      
string _num = "陕ABIT00";     
Tire _t;                     
   }; 


网站公告

今日签到

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