[C++](15)继承


面向对象三大特性

  • 封装

  • 继承

  • 多态

通过之前的学习,我们对封装已经有了一定了解:

  1. C++ 的 Stack 类设计和 C 的 Stack 设计对比:封装的更好,访问限定符和类的设计,使用更规范。
  2. 迭代器设计:封装了容器底层结构,提供了同一的访问容器的方式。如果没有迭代器,容器暴露底层结构,访问元素更复杂,使用成本高。
  3. stack、queue、priority_queue 的设计:适配器模式

继承

概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

比如学生类和老师类,学生类和老师类中同样有姓名,年龄,地址等成员变量。学生类还有学号,老师类有工号这些不同的成员变量。

如果单独设计这两个类,对于两个角色都有的数据和方法,设计重复了。为了增加复用,这里就可以引出继承这个概念。

img

像这样,上面的类叫做父类/基类,下面的类叫做子类/派生类。父类实现共有的成员,子类实现各自独有的成员。

子类会继承父类的成员,比如,这里的子类 Student 就有了 4 个成员变量:string _name; int _age; string _address; string _stuid

定义

语法

img

继承方式与访问限定符

img

继承方式和访问限定符可以组成 9 种不同的情况

类成员\继承方式public 继承protected 继承private 继承
基类的 public 成员派生类的 public 成员派生类的 protected 成员派生类的 private 成员
基类的 protected 成员派生类的 protected 成员派生类的 protected 成员派生类的 private 成员
基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见

总结

  1. 基类的 private 成员在派生类中不可见
  2. 基类的其他成员在子类中的访问方式就是 min(成员在基类中的访问权限,继承方式),public > protected > private

  • 不可见:基类的成员被继承到了派生类中,但在派生类里面还是不能访问。

protected 和 private 在继承中有了区别

  • protected 和 private 成员对于基类来说是一样的:类外不可访问,类里面可以访问
  • 基类的 protected 和 private 成员对于派生类:private 成员不可访问,protected 成员可以访问

注意

  • 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显式写出继承方式
  • 在实际中一般都是使用 public 继承,几乎很少使用 protected/private 继承,基类中的成员一般都是 public 和 protected
  • 如果派生类中我们手动地使用访问限定符来控制,则权限按照显式的访问限定符的来

基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这个过程也可以叫切片或切割,意为把派生类中基类那部分切下来赋值过去。

注意这种赋值建立在 public 继承的基础之上。因为其他继承会使派生类的基类部分成员的权限缩小,无法赋值给高权限的基类成员。

img

例子:

先设计一个基类和一个派生类

class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};

class Student : public Person
{
public:
	string _stuid;
};

赋值

int main()
{
	Person p;
	Student s;

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

👆注意:

  1. 这种赋值的两个对象虽然类型不同,但是不会发生类型转换,中间也不存在临时对象,它们就是一种天然的赋值。
  2. 引用 rp 就是将派生类对象 s 中基类的部分切割出来取了个别名 rp
  3. 指针 pp 指向的是派生类对象 s 中基类的部分。或者说,pp 指向的空间只有 Person 类对象的大小

打印它们的地址:

	cout << &s << endl << &rp << endl << pp << endl;
//结果:
//000000075D8FF790
//000000075D8FF790
//000000075D8FF790

可以看到是一样的。

  1. 基类对象不可以赋值给派生类对象。

继承中的作用域

  1. 在继承体系中,基类派生类都有独立的作用域
  2. 如果基类和派生类中有同名成员,派生类成员将屏蔽对基类部分同名成员的直接访问,这种情况叫做隐藏也叫做重定义(在派生类成员函数中,也可以使用 :: 对基类部分的成员显式访问)

例子:

class Person
{
protected:
	string _name = "张三";
	int _num = 111;		//基类中的_num
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "派生类学号:" << _num << endl;
		cout << "基类学号:" << Person::_num << endl; //显式访问
	}
protected:
	int _num = 999;
};

void test3()
{
	Student s;
	s.Print();
}
//结果:
//姓名:张三
//派生类学号:999
//基类学号:111

👆:类似于局部优先,直接访问的是派生类独有的 _num 成员


对于重名函数也是如此:

class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};

class B : public A
{
public:
	void fun()
	{
		cout << "B::func()" << endl;
	}
};

void test4()
{
	B b;
	b.fun();	//优先调用派生类特有的
	b.A::fun();	//指定调用基类的
}
//结果:
//B::func()
//A::func()

注意🕳:

class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		cout << "B::func()" << endl;
	}
};

👆:这两个 fun 函数虽然参数类型不同,但不是函数重载,因为函数重载要求在同一作用域。

  1. 对于派生类中和基类中同名的成员函数,只要函数名相同就构成隐藏

  2. 实际中在继承体系里最好不要定义同名的成员

派生类的默认成员函数

派生类构造函数原则:

  1. 调用基类构造函数初始化继承自基类的成员
  2. 自己再初始化自己的成员

析构、拷贝构造、赋值重载也类似。

例子:

先在 Person 类中写下默认构造、拷贝构造、赋值重载、析构四个成员函数。

class Person
{
public:
	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 operatpr=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}

	~Person()
	{
		cout << "~Person" << endl;
	}
protected:
	string _name;
};

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

void test5()
{
	Student s;
}
//结果:
//Person()
//~Person

通过结果可以发现,只是创建一个 Student 类型的对象会一起调用它的基类 Person 的构造函数和析构函数。

默认构造

Student(const char* name = "", int num = 0, const char* addrss = "")
	: Person(name)
	, _num(num)
{}

Student 类的初始化列表中,可以通过 Person("李四") 来初始化基类部分的成员。

也就是说,基类成员在派生类中也要当成一个整体,这里的Person 类看起来就像是 Student 类的一个成员变量。

拷贝构造

基类成员需要调用基类的拷贝构造,直接传入 s 切片即可,然后派生类自己的成员自己初始化。

Student(const Student& s)
	: Person(s) //传入s,基类中的拷贝构造引用接收可以切片
	, _num(s._num)
{}

赋值重载

同样需要调用基类的赋值重载。Person::operator= 传入 s 会被切片。

Student& operator=(const Student& s)
{
    if (this != &s)
    {
        Person::operator=(s);
        _num = s._num;
    }
    return *this;
}

析构函数

注意

  • 基类和派生类的析构函数构成隐藏关系,由于多态的需要,析构函数名会被统一处理成 destructor
  • 派生类析构函数完成后会自动调用基类析构函数,所以基类的析构不需要我们显式调用
~Student()
{}

小题:如何设计一个不能被继承的类?

把基类的构造函数设成 private。派生类就无法构造了:

class A
{
private:
	A()
	{}
};

class B : public A
{

};

void test6()
{
	B b;	//此处报错:无法引用 "B" 的默认构造函数 -- 它是已删除的函数
}

那么要单独创建一个 A 类的对象怎么办?

A 类内部提供一个静态成员函数即可:

class A
{
public:
	static A CreateObj()
	{
		return A();
	}
private:
	A()
	{}
};

void test6()
{
	A a = A::CreateObj();
}

继承与友元

友元关系不能继承

基类的友元不一定是派生类的友元。

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

class Student : public Person
{
	friend void Display(const Person& p, const Student& s);
protected:
	int _num;
};

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

👆:Display 必须同时声明为基类和派生类的友元才能访问这二者的保护成员。

继承与静态成员

基类定义的静态成员,则整个继承体系里面只有一个这样的成员。

例子:

统计一个继承体系共创建了多少个对象

class Person
{
public:
	static int _count;
	Person()
	{
		++_count;
	}
protected:
	string _name;
};
int Person::_count = 0;

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

class Graduate : public Student
{
protected:
	string _seminarCourse;
};

void test7()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate g1;
	Graduate g2;

	cout << "人数:" << Person::_count << endl;
}
//结果:人数:5

复杂的菱形继承及菱形虚拟继承

单继承:一个派生类只有一个直接基类的继承关系

多继承:一个派生类有多个直接基类的继承关系

img

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

img

菱形继承的问题:菱形继承有数据冗余和二义性问题

冗余:在 Assistant 的对象中 Person 的成员会有 2 份

二义性

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

void test8()
{
	Assistant a;
	a._name = "张三";	//此处报错:"Assistant::_name" 不明确
}

👆:不知道要访问从 Student 继承来的 _name 还是从 Teacher 继承来的 _name

这个其实很好解决,只要指定好类域即可:

	a.Teacher::_name = "张三";
	a.Student::_name = "李四";

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

关键字virtual

使用方法:在腰部,也就是在 StudentTeacher 继承 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 _majorCoures;
};

虚拟继承是如何解决问题的

为了研究这个问题,我们先设计一个简单的菱形继承:

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

不使用虚拟继承的情况下,创建一个 D 类型的对象 d 并进行访问,通过内存窗口查看 d 对象内的数据:

img

👆:通过这张图可以看出,从 BC 类继承下来的成员和 D 类独有的成员依次排列。A 类的数据有 2 份,出现冗余。

使用了虚拟继承的情况下:

img

👆:

  • 可以看到原本存放 A 类成员的地方变成了指针,我们叫它虚基表指针A 类成员被单独存储到了最后。

  • 虚基表指针指向的位置存放的是偏移量,表示该指针的位置到 A 类成员存储位置的距离。

到这为止,其实数据冗余和二义性的问题已经解决了,但是原来的 A 位置为什么还要搞个虚基表指针呢,直接空着不好吗?

这里就涉及到切片问题了:

如果要把派生类对象 d 赋值给基类对象,比如 C c = d; d 会把 C 类部分切出来,然后赋值过去,但是从 C 类继承下来的 A 类成员找不到了,所以需要用虚基表指针找到偏移量然后根据偏移量去找 A 类成员。

建议:在实际中尽量不设计菱形继承

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值