C++面向对象笔记(4):继承篇

C++继承篇

1.继承的基本概念

被继承的类叫做基类也叫做父类/超类,从其他类继承而来的类叫做派生类也叫做子类。

子类中不仅继承了父类的中的数据成员,也继承了父类的成员函数。

实例化子类时,构造和析构函数的调用:

在子类实例化对象的时候,会先隐式的调用父类的构造函数;在子类对象析构之后,也会隐式的调用父类的析构函数。

举个例子,假如有父类A,子类B,在其构造函数和析构函数中都输出一个标记语句,在这种情况下,如果我们创建一个子类B的对象,那么可以得到以下输出:

// 创建子类B的对象,构造函数和析构函数的调用顺序
A(),This is the constructor
B(),This is the constructor

~B(),This is the destructor
~A(),This is the destructor

2.继承方式-访问限定符

继承时一定别忘了写访问限定符!如果不写,默认是private私有继承。

继承的使用:

分类写法
公有继承class A:public B
保护继承class A:protected B
私有继承class A:private B

继承方式和对应的访问属性(权限):

继承方式基(父)类成员访问属性派生类访问属性
publicpublicpublic
protectedprotected
private无法访问
protectedpublicprotected
protectedprotected
private无法访问
privatepublicprivate
protectedprivate
private无法访问

这个表格我们每一行从左往右看,拿public继承方法来说明:

  1. 如果父类中某成员为public,子类通过public方式将其继承,该成员的访问权限不变依然为 public
  2. 如果父类中某成员为protected,子类通过public方式将其继承,该成员的访问权限变为 protected
  3. 如果父类中某成员为private,子类通过public方式将其继承,该成员的访问权限变为 不可访问。

小总结:

B类从A类派生,那么B类是A类的子类,A类是B类的超类。

B类从A类派生,那么B类中含有A类的所有数据成员

B类从A类公共派生,那么可以通过B类的对象调用到A类的 所有成员函数 public及protected限定符下的成员函数

B类从A类公共派生,那么可以在B类中直接使用A的public及protected的数据成员

B类从A类公共派生,那么A类的公共成员函数成为B类的公共成员函数。

B类从A类公共派生,那么A类的保护成员函数成为B类的保护成员函数。

B类从A类公共派生,那么A类的私有成员函数 成为B类的私有成员函数 不能被B类继承并使用

B类从A类私有派生,那么A类的公共成员函数成为B类的 公共成员函数 私有成员函数

B类从A类保护派生,那么A类的公共成员函数成为B类的保护成员函数。

B类从A类保护派生,那么A类的保护成员函数成为B类的保护成员函数。

#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;

/**
 * 定义人的类: Person
 * 数据成员姓名: m_strName
 * 成员函数: eat()
 */
class Person
{
public:
    string m_strName;
	void eat()
	{
		cout << "eat" << endl;
	}
};

/**
 * 定义士兵类: Soldier
 * 士兵类公有继承人类: public
 * 数据成员编号: m_strCode
 * 成员函数: attack()
 */
class Soldier:public Person
{
public:
	string m_strCode;
	void attack()
	{
		cout << "fire!!!" << endl;
	}
};

int main(void)
{
    // 创建Soldier对象
	Soldier so;
    // 给对象属性赋值
    so.m_strName = "Jim";
	so.m_strCode = "592";
    // 打印对象属性值
	cout << so.m_strName << endl;
	cout << so.m_strCode << endl;
    // 调用对象方法
	so.eat();
	so.attack();

	return 0;
}

3.继承中同名成员的隐藏(父类与子类的继承关系中)

当父类和子类中出现同名成员时,使用子类的对象调用这个同名成员,你会发现只调用了子类中的,而父类中的同名成员,仿佛被隐藏了一样,你无法直接调用,这就是隐藏。

隐藏不仅适用于成员函数,还适用于其它对象成员。

当然,也是有办法可以访问父类的同名成员的,那就是使用父类名::,看下面的例子。

例1,访问同名函数的方法:

// Person为Soldier的父类
int main(){
    
    Soldier so;
    so.play();			// 调用子类中的play()
    so.Person::play();	// 调用父类中的play(),Person是父类的类名
    
    return 0;
}

例2,访问同名属性的方法:

class Persion{
public:
    void play();
protected:
    string code;	// 父类code变量
};

class Soldier:public Person{
public:
    void play();
    void work();
protected:
    int code;		// 子类code变量
}

void Soldier::work(){
    code = 1234;			// 调用子类的code(就是当前的这个类)
    Person::code = "5678";	// 调用父类的code(被当前类继承的类)
}

隐藏与重载:

如果需要调用父类中的同名成员,一定要使用父类名::。当然,你会觉得这很像重载,所以可能会尝试将父子类中的同名函数利用参数进行区分。然而这当然是不行的,隐藏和重载是不同的概念,父子类中的同名成员函数是无法通过函数的参数进行区分的,如果不使用父类名::,就无法调用父类中的同名成员函数。

4.isA语法-将子类赋值给父类

**子类(派生类)是可以赋值给父类(基类)**的。

如果这个时候对

// 人是父类,士兵类是子类
int main(){
    // 正确
    Soldier s1;
    Person p1 = s1;
    Person *p2 = &s1;
    // 错误
    s1 = p1;
    Soldier *s2 = &p1;
    
    return 0;
}

存储结构:

  1. 如果将子类对象 初始化/赋值 给父类:

    子类中属性的数据会赋值给父类的属性,父类没有的属性,会被截断(即丢失),只赋值父类有的属性的值(也就是从父类继承来的那些,非子类独有的部分)。

    对于父类来说,它只能接收自己拥有的数据成员。

    // 初始化:
    Soldier so;
    Person p = so;
    // 赋值:
    Soldier so;
    Person p;
    p = so;
    

    [外链图片转存失败(img-JOKQMFNu-1563352548404)(./images/muke_C++/003.png)]

  2. 如果用父类指针来访问子类对象,那么也只能访问到父类所拥有的数据成员,而无法访问到子类拥有的数据成员和成员函数。

    [外链图片转存失败(img-ZBHD3rs6-1563352548405)(images/muke_C++/004.png)]

isA的常见用法:

既然子类可以赋给父类,那么我们就可以用父类作为形参来接收子类。

void fun1(Person *p){// 指针
    ...
}
void fun2(Person &p){// 引用
    ...
}

int main(){
    Person p1;
    Soldier s1;
    
    fun1(&p1); fun2(p1);
    fun1(&s1); fun2(s1);
    
    return 0;
}

需要使用 虚析构函数 的情况:

当存在继承关系,我们使用父类的指针,去指向堆中的子类的对象,并且我们还想使用父类的指针去释放这块内存,这个时候我们就需要 虚析构函数

为什么?因为在这种情况下之后调用父类的析构函数,而子类的析构函数不会被释放,这样就会造成内容泄露的问题(在return 0;前打断点才能看出,因为程序一旦return退出,所有的内存都会释放)。

这种情况的具体表现请看下面的代码:

类声明:

// Person类声明
class Person {
public:
    Person(string name = "Person001");// 构造函数赋初值
    ~Person();// 析构函数
    void play();
protected:
    string m_strName;
};

// Soldier类声明
class Soldier: public Person {
public:
    Soldier(string name = "soldier001",int age = 20);// 构造函数赋初值
    ~Soldier();// 析构函数
    void work();
protected:
    int m_iAge;
};

类实现:

// Person类实现
Person::Person(string name){
    m_strName = name;
    cout << "Person()" << endl;
}
Person::~Person(){
    cout << "~Person()" << endl;
}
void Person::play(){
    cout << "Person -- play()" << endl;
    cout << "m_strName = " << m_strName << endl;
}

// Soldier类实现
Soldier::Soldier(string name, int age){
    // 将初始值赋值给对象
    m_strName = name;
    m_iAge = age;
    cout << "Solidier()" << endl;
}
Soldier::~Soldier(){
    cout << "~Solidier()" << endl;
}
void Soldier::work(){
    cout << "m_strName = " << m_strName << endl;
    cout << "m_iAge = " << m_iAge << endl;
    cout << "Soldier -- work()" << endl;
}

mian.c:

int main() {

    Soldier so;
    Person *p = &so;

    cout<< "==="<< endl;
    p->play();
    cout<< "==="<< endl;

    delete p;
    p = nullptr;

    return 0;
}
/** 输出:
Person()
Solidier()
===
Person -- play()
m_strName = soldier001
===
~Person()
*/

这里我们使用指针(堆,需要手动释放)来进行资源的释放,可以观察到,最后并没有调用~Solidier(),这样会有内存泄露的问题。

如何解决这个问题,就是使用虚析构函数,这里要使用修饰符virtual

虚析构函数是可以继承的,父类中写了,即使子类中的不写,也是虚析构函数。但是建议全写上,起提示作用,这很重要!

// 拿本例来说明
// 在Person和Solidier类的声明中,在析构函数前面加virtual
// 在实现部分不需要加virtual

转载自:virtual 在哪些情况要用?

个人总结,virtual当前出现的三种地方:

虚析构函数:当父类指针指向子类对象时,释放内存时,若不定义virtual,则仅释放父类内存。

虚继承:防止多继承和多重继承时,一个父类被继承多次,造成内存空间的浪费。

虚函数:当父类指针指向子类对象时,父类指针可以指向子类方法。

附加例子,在实验时发现的,关于子类对象相互赋值与拷贝构造函数的调用:

如果在子类写一个没有赋值操作的拷贝构造函数,然后用子类对象01给子类对象02赋值,会发现对象02中的属性如果是继承于父类的,属性的值就是父类中赋的初始值,如果是子类中添加的,则值为空。代码如下:

关于拷贝构造函数的内容,在笔记第2篇“封装篇(上)”。

类声明:

// Person类
class Person {
public:
    // 构造函数赋初值
    Person(string name = "Person001");
    Person(const Person& pe);
protected:
    string m_strName;
};

// Soldier类
class Soldier: public Person {
public:
    // 构造函数赋初值
    Soldier(string name = "soldier001",int age = 20);
    Soldier(const Soldier& soso);
protected:
    int m_iAge;
};

类实现:

// Person类
Person::Person(string name){
    m_strName = name;
    cout << "Person()" << endl;
}
Person::Person(const Person& pe){
    cout << "Copy Person()" << endl;
}

// Soldier类
Soldier::Soldier(string name, int age){
    // 将初始值赋值给对象
    m_strName = name;
    m_iAge = age;
    cout << "Solidier()" << endl;
}
Soldier::Soldier(const Soldier& soso){
    cout << "Copy Solidier()" << endl;
}
void Soldier::work(){
    cout << "m_strName = " << m_strName << endl;
    cout << "m_iAge = " << m_iAge << endl;
    cout << "Soldier -- work()" << endl;
}

main.c

int main() {
    Soldier so;
    Soldier so1 = so;
    cout << "====" << endl;
    so1.work();
    cout << "====" << endl;
    return 0;
}
/* 输出:
Person()
Solidier()
Person()
Copy Solidier()
====
m_strName = Person001
m_iAge = 0
Soldier -- work()
====
*/

这里来看下输出:

  1. Person()Solidier()是因为 so这个对象 的创建,在调用子类的构造函数前会先调用父类的构造函数。
  2. 之后的Person()Copy Solidier()是因为 so1的创建,但是这里Person这个类并没有调用拷贝构造函数,而是调用了构造函数,也就是在这里赋上了初始值。Solidier类则是调用了拷贝构造函数。
  3. m_strName = Person001m_iAge = 0,因为我们手写了Solidier类的拷贝构造函数,其中又没有赋值操作,所以这里m_strName的值是调用Person构造函数时的初始值,而m_iAge是Solidier类中的属性,所以没有被赋值,输出就是0

如果这里用自动生成的拷贝构造函数(没有手写拷贝构造函数的时候就会自动生成),则so对象中属性的值是会赋值到so1对象的属性中的。

第一种方法直接初始化的为啥比第二种少一个Person()构造函数

用soldier赋值给p的这种操作有什么实际意义?

5.多重继承和多继承

多重继承

当B类从A类派生,C类从B类派生,形成一条继承链,此时成为多重继承。

举个多重继承的例子:

步兵类继承士兵类,士兵类继承人类。

[外链图片转存失败(img-PFOCt8QK-1563352548407)(images/muke_C++/005.png)]

class Person{  
};
class Soldier:public Person{
};
class Infantryman:public Soldier{
};

多继承

多继承是指一个子类继承多个父类。多继承对父类的个数没有限制,继承方式可以是公共继承、保护继承和私有继承。

再举个多继承的例子:

农民工同时继承工人类和农民类,工人类和农民类是平行关系。

[外链图片转存失败(img-nE3H2F6A-1563352548408)(images/muke_C++/006.png)]

[外链图片转存失败(img-F3mlAChm-1563352548410)(images/muke_C++/007.png)]

class Worker{
};
class Farmer{
};
class MigrantWorker:public Worker,public Farmer{  
};

多继承 下的调用初始化列表使用:

这里的继承关系同上,这里只列出构造函数的代码:

// 类的实现部分代码,这里分别是Farmer、Worker和MigrantWorker的构造函数

Farmer::Farmer(string name){
    m_strName = name;
}

Worker::Worker(string code){
   m_strName  = code;
}

// 通过MigrantWorker的构造函数,调用Farmer和Worker的构造函数,同时将name和code的值传递过去
MigrantWorker::MigrantWorker(string name,string code):Farmer(name),Worker(code){
    
}

6.虚继承与菱形继承(环状继承)

虚继承的使用:

在继承的访问限定符之前添加virtual修饰符。

菱形继承

在菱形继承中,类A是类B的父类,类A是类C的父类,类B和类C都是类D的父类,is-A的关系如下:

[外链图片转存失败(img-BddrsI6o-1563352548411)(images/muke_C++/008.png)]

可见,菱形继承中是存在 多重继承多继承 的。

(多重继承:类D继承类B,类B继承类A;多继承:类D继承类B,也继承类C)

菱形继承的数据冗余问题:

这样的继承中会存在类D中存在有2份类A的数据,我们需要使用虚继承来解决,看下面的例子:

[外链图片转存失败(img-hFtdMrvw-1563352548412)(images/muke_C++/009.png)]

// 在类的声明中:
// 之后会被农民工继承,所以这个类是虚基类(父类)
class Worker:virtual public Person{
}
class Farmer:virtual public Person{
}

// 继承上面2个虚基类
class MigrantWorker:public Worker, public Farmer{   
}

解决类的重定义问题:

在菱形继承中,类的重定义是一定会发生的,我们可以在发生重定义的内部写一段来避免重定义。

#ifndef XXX
#define XXX

// 在这里写:类的声明

#endif

本篇为视频教程笔记,视频如下:

C++远征之继承篇

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值