超详细 - 一文说懂 C++ 继承(上)

目录

0 引言

1. 继承的概念

1.1 继承的本质

1.2 继承的作用 

2. 继承的定义

 2.1 继承的格式

2.2 继承的权限 

2.3  默认继承

 2.5 继承权限的使用

3. 继承的作用域

3.1 隐藏 

4. 基类与派生类对象的赋值转换 

4.1 切片

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

 5.1 隐式调用

5.2 显示调用 


0 引言

从前面我们知道,继承是面向对象的三大特性之一(封装 继承 多态)。今天我们主要一起学习什么是继承。即如何在父类的基础之上去构建更加丰富的子类。在此举一个不完全恰当的例子,例如柑橘类水果的不同品种,实际上继承了其父类品种的某些特性。

1. 继承的概念

那什么是继承呢?是继承遗产还是某些东西呢?答案都不是。

实际上,官方说 继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有基类(父类)特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类(子类)。

因此:被继承的对象:父类/基类

           继承一方:子类/派生类  

1.1 继承的本质

继承的本质实际上就是为了复用代码。

例如:现在需要完成一个学校教务系统代码的编写,单从角色划分上来说,可以简单分为:教职工和学生 这两大类,但如果继续划分的话,还可以分出:校领导、各级院长、辅导员、后勤人员、大一/大二/大三/大四学生等,假设为每个角色都设计一个结构,那么这个工程量也未免太大了,且存在冗余。

所以,为了提高开发效率,我们就可以利用继承的概念。可以从各种角色中选出共同点,组成基类,比如每个人都有姓名、年龄、性别、联系方式等基本信息

而 教职工与学生的区别就在于 管理与被管理,因此可以在基类的基础上加一些特殊信息如教职工号表示教职工,加上学号表示学生,其他细分角色设计也是如此,产生多种子类

因此,我们通过继承的方式,复用基类的代码,进而划分出各种子类。

1.2 继承的作用 

子类基础父类后,可以享有父类中的所有的 公开 / 保护 属性,也就是说,除了 私有 内容外,父类有的,子类全都有 。

举一个例子, 在父类 - 房子 的基础上,派生出 小平层和别墅 这两个子类。 

//父类 - 房子
class house
{
public:
	house(int area = int())
		:_area(area)
	{}
	int getarea()
	{
		return _area;
	}
private:
	int _area;
};

//子类 - 平层
class Flatbed : public house
{
public:
	Flatbed()
		:house(90)
	{
		cout << "小平层,面积是:" << getarea() << "平方米" << endl;
	}
};

//子类 - 别墅
class villa : public house
{
public:
	villa()
		:house(500)
	{
		cout << "别墅,面积是:" << getarea() << "平方米" << endl;
	}
};

int main()
{
	Flatbed f;
	villa v;
	return 0;
}

 

可以看到,两个子类都能具备父类中的 公有和保护 属性,并且互不干扰。

2. 继承的定义

 2.1 继承的格式

继承的格式:子类 :继承方式 父类,比如 class A :public B 就表示 A 继承 B, 且为公有继承

2.2 继承的权限 

继承的权限可分为 公有继承(public)、保护继承(protected)、私有继承(private)。

权限大小:public > protected > private。

public公开的,任何人都可以访问
protected保护的,只有当前类和子类可以访问
private私有的,只允许当前类进行访问

保护 protected 比较特殊,只有在 继承 中才能体现它的价值,否则与 私有 作用一样。

因此存在多种 父类成员权限 与 继承权限 的搭配方案。(所谓的外部其实就是子类对象

public 继承protected 继承private 继承
父类:public 成员外部可见,子类中可见外部不可见,子类中可见外部不可见,子类中可见
父类:protected 成员外部不可见,子类中可见外部不可见,子类中可见外部不可见,子类中可见
父类:private 成员都不可见都不可见都不可见

总结:无论是哪种继承方式,父类中的 private 成员始终不可被 [子类 / 外部] 访问;当外部试图访问父类成员,依据 min(父类成员权限,子类继承权限),只有最终权限为 public  时,外部才能访问。

下面我们来看示例:

    

2.3  默认继承

如果不注明继承权限, class 默认为 private , struct 默认 public ,因此最好是注明继承权限。

那么如何访问父类的私有成员?我们只需在父类中设计相应的函数,间接访问私有成员

 2.5 继承权限的使用

 那么我们如何优雅的使用好权限呢?我们只需要记住以下的点:

对于只想自己类中查看的成员,设为 private ,对于想共享给子类使用的成员,设为 protected, 其他成员都可以设为 public 。

例如在 小明 家中,房子面积可以设置为公开,家庭存款只让家庭成员知道,而隐私则可以设置为私有。

class Home
{
public:
	int area = 120;	//房屋面积
};

class Father : public Home
{
protected:
	int money = 100000;	//存款
private:
	int privateMoney = 100;	//私房钱
};

class xiaoming : public Father
{
public:
	xiaoming()
	{
		cout << "我是小明" << endl;
		cout << "我知道我家房子有 " << area << " 平方米" << endl;
		cout << "我也知道我家存款有 " << money << endl;
		cout << "但我不知道我爸爸的私房钱有多少" << endl;
	}
};

class zhangsan
{
public:
	zhangsan()
	{
		cout << "我是张三" << endl;
		cout << "我只知道张三家房子有 " << Home().area << " 平方米" << endl;
		cout << "其他情况不知道" << endl;
	}
};

int main()
{
	xiaoming x;
	cout << "================" << endl;
	zhangsan z;
	return 0;
}

 因此我们可以看到,权限可以很好的保护成员。

那么我们如何设计一个不能被继承的类?

我们只需要将父类的构造和析构函数设为私有,这样子类就无法创建父类对象,同时也就无法继承

3. 继承的作用域

子类虽然继承自父类,但两者的作用域是不相同的,假设出现同名函数时,默认会将父类的同名函数隐藏调,进而执行子类的同名函数。

隐藏 也叫 重定义,与它类似的概念还有:重写(覆盖)、重载。

3.1 隐藏 

子类中出现父类的 同名 方法或者成员。

//父类
class Base
{
public:
	void func() { cout << "Base val: " << val << endl; }
protected:
	int val = 111;
};

//子类
class Derived : public Base
{
public:
	int func()
	{
		cout << "Derived val: " << val << endl;
		return 0;
	}
private:
	int val = 222;
};

int main()
{
	Derived d;
	d.func();
	return 0;
}

 我们发现,父子类中的方法和成员均被隐藏,执行的是 子类方法,输出的是子类成员。

现在我们将子类中函数修改为 funA

int funA() 
{ 
	cout << "Derived val: " << val << endl;
	return 0;
}

 发现此时 隐藏 消失,并且结果的是 父类方法 + 父类成员。

接着,我们修改子类标识符 val 

int num = 222;

此时 隐藏 也消失,执行结果 子类方法 + 父类成员。

因此, 当子类中的方法出现 隐藏 行为时,优先执行 子类 中的方法;当子类中的成员出现 隐藏 行为时,优先选择当前作用域中的成员(局部优先)。

那么,我们如何显示的调用父类的方法或者成员?

 我们利用域作用限定符 : : 即可。 

总结: 

  • 只要是命名相同,都构成 隐藏 ,与 返回值、参数 无关
  • 隐藏会干扰调用者的意图,因此在继承中,要尽量避免同名函数的出现

4. 基类与派生类对象的赋值转换 

首先提出的是,在继承中,允许将子类对象直接赋值给父类,但不允许父类对象赋值给子类。 

 并且这种 赋值 是非常自然的,编译器直接处理,不需要调用 赋值重载 等函数。

//父类
class Base
{
protected:
	int val = 111;
};

//子类
class Derived : public Base
{
private:
	int num = 222;
};

int main()
{
	Base b;
	Derived d;
	b = d;
	//d = b;	//非法,只允许 子->父
	return 0;
}

 子类对象 在 赋值 给 父类对象 时,触发 切片 机制,丝滑的完成 赋值。

4.1 切片

下图展示了柠檬切片:

在基类与派生类的赋值转换中,切片将父类对象看作一个结构体,子类对象 看作 结构体Plu 版,将子类对象中多余的部分去除,留下父类对象可接收的成员,最后再将对象的指向进行改变就完成了切片。

 

因为整个切片过程是由编译器自己完成的,所以效率很高,并且不会发生借助临时对象构造再赋值的情况。由于父类无法满足子类的需求,切片只在 子类->父类 时发生。

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

派生类(子类)也是 ,同样会生成 六个默认成员函数(用户未定义的情况下)

不同于单一的 子类 是在 父类 的基础之上创建的,因此它在进行相关操作时,需要为父类进行考虑。

 5.1 隐式调用

子类在继承父类后,构建子类对象时 会自动调用父类的 默认构造函数,子类对象销毁前,还会自动调用父类的 析构函数。

class Person
{
public:
	Person() { cout << "Person()" << endl; }
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
	Student() { cout << "Student()" << endl; }
	~Student() { cout << "~Student()" << endl; }
};

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

此时,自动调用是由编译器完成的,前提是父类存在对应的默认成员函数;如果不存在,会报错 

5.2 显示调用 

因为存在 隐藏 的现象,当父子类中的函数重名时,子类无法再自动调用父类的默认成员函数,此时会引发 浅拷贝 相关问题。

class Person
{
public:
	Person() { cout << "Person()" << endl; }
	void operator=(const Person& P) { cout << "Person::operator=()" << endl; }
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
	Student() { cout << "Student()" << endl; }
	void operator=(const Student&) { cout << "Student::operator=()" << endl; }
	~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Student s1;
	cout << "================" << endl;
	Student s2;
	s1 = s2;
	return 0;
}

此时可用通过 域作用限定符 :: 显式调用父类中的函数。 

总的来说,子类中的默认成员函数调用规则可以概况为以下几点: 

  1. 子类的构造函数必须调用父类的构造函数,初始化属于父类的那一部分内容;如果没有默认构造函数,则需要显式调用
  2. 子类的拷贝构造、赋值重载函数必须要显式调用父类的,否则会造成重复析构问题
  3. 父类的析构函数在子类对象销毁后,会自动调用,然后销毁父类的那一部分

 注意:

  • 子类对象初始化前,必须先初始化父类那一部分
  • *子类对象销毁后,必须销毁父类那一部分
  • 不能显式的调用父类的析构函数(因为这不符合栈区的规则),父子类析构函数为同名函数destruct ,构成隐藏,如果想要满足我们的析构需求,就需要将其变为虚函数,构成重写。

 

  • 48
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值