C++继承(最详细)

目录

1.继承的概念以及定义

[1.1 继承的概念](#1.1 继承的概念)

[1.2 继承的定义](#1.2 继承的定义)

​编辑

2.继承中的作用域

3.基类和派生类间的转换

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

5.实现不被继承的类

6.継承与友元

​编辑

7.继承与静态成员

8.多继承及其菱形继承问题

[8.2 虚继承](#8.2 虚继承)

[8.3 来看一个小题](#8.3 来看一个小题)

9.继承和组合


学完STL后,咱们再转回来看语法,今天要讲的语法是继承,这也是一个算是比较难的语法,做好了?发车喽,来跟博主一起学习吧!

1.继承的概念以及定义

1.1 继承的概念

什么叫继承呢?继承算是属于对代码的复用。即在保持原有的特性上面,增加一些新的方法以及变量,就叫做继承。那么原有的类叫做基类(父类),增加新的方法以及变量之后的类叫做派生类(子类)。继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂 的认知过程。以前我们接触的函数层次的 复用,继承是类设计层次的复用

来看这一段代码,那么观察可以发现,Student与Teacher这两个类都是属于派生类(因为它们都继承了基类Person的特性),而Person类属于基类。并且,派生类定义的对象也可以调用基类中的成员函数(这个待会讲)。OK,接下来看定义:

1.2 继承的定义

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

那么到这就会有人问了:限定符有三种,那么继承方式是不是也有三种?没错,很聪明,确实有三种。

那么访问限定符加上继承方式一组合,是不是有9种组合方式呀。

当然,这里,基类的private成员,基本都是不可见的,(是不可见,不是没继承下来,别搞错了)。那么继承方式怎么看呢?

1.基类的其他成员 在派生类的访问方式==Min(成员在基类的访问限定符,继承方式),public >protected>private。

所以通过这个,咱们之前设计一个类,都是不知道啥设计成public,啥设计成protected,现在可能有点眉目了。

2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

但这里涉及到一个问题,既然private,都不能访问,那我要它做什么?或者说我要private継承有什么用吗?所以说,在这,真正用到的也就两个,即public继承下的两个(基类public成员与基类protected成员)。

3.在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实 际中扩展维护性不强。

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显 示的写出继承方式。

2.继承中的作用域

隐藏规则:

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

  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用基类::基类成员显示访问)

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

看上面的代码图片,可以看出,如果只定义派生类的对象,用派生类的对象调用_num,**那么默认调用的是派生类中的_num(就近原则)。**若是想调用基类中的_num,必须得在调用的那个函数里指定作用域。还有一些注意事项,都在上面的代码图片中。反正如果说基类与派生类中有同名的成员函数或者成员变量,优先调用派生类中的(就近原则)。

在这里需要强调一个东西就是:重载:重载是函数名相同,参数类型或者参数个数不同,并且构成重载的函数必须在同一个作用域中。比如:基类跟派生类是两个不同的作用域,所以不构成重载。

隐藏:函数名相同并且作用域不在同一个作用域就构成了隐藏。

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()函数构成了隐藏(注意不是重载)。并且这个程序执行不了,因为虽然第一个函数可以调用,调用了派生类中的fun函数,但是第二个函数在派生类中找不到,那么找不到就无法调用,程序就无法运行。

3.基类和派生类间的转换

1.public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切 割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

2.基类对象不能赋值给派生类对象。

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

废话少说,上代码:

class Person

{

protected :

string _name; // 姓名

string _sex; // 性别

int _age; // 年龄

};

class Student : public Person

{

public :

int _No ; // 学号

};

int main()

{

Student sobj ; // 1. 派生类对象可以赋值给基类的指针 / 引用

Person* pp = &sobj;

Person& rp = sobj;

// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的

Person pobj = sobj;

//2. 基类对象不能赋值给派生类对象,这里会编译报错

sobj = pobj;

return 0;

}

这个切片的作用就是将派生类中属于基类的那一部分给切出来,怎么切呢,就通过上面所说的方法即可。 这个切片后面代码有作用,再带大家来进一步的理解。

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

这个默认成员函数,还是类的那六个,因为派生类也属于类嘛。

先来看一段代码吧:

class Person

{

public:

Person(const char*name="peter",const char *telephone="1234")

: _name(name)

,_telephone(telephone)

{

cout << "Person()" << endl;

}

Person(const Person& p)

: _name(p._name)

,_telephone(p._telephone)

{

cout << "Person(const Person& p)" << endl;

}

Person& operator=(const Person& p)

{

cout << "Person operator=(const Person& p)" << endl;

if (this != &p)

_name = p._name;

_telephone = p._telephone;

return *this;

}

~Person()

{

cout << "~Person()" << endl;

}

protected:

string _name="kate"; // 姓名//有初始化列表先走初始化列表,无初始化列表就用

//缺省值,反正,最后都类似的要走初始化列表,并且,若派生类中有对基类的初始化

//还必须得看派生类中的。

string _telephone="0000";

};

class Student : public Person

{

public:

Student(const char* name, const char* telephone, int num)

: Person(name,telephone)//如将基类中的初始化列表屏蔽了再去执行这个

//会发现执行不通,是因为这个就是得调用基类的初始化列表,但是基类的初始化

//列表被咱屏蔽了呀。那肯定会报错的

,_num(num)

{

cout << "Student()" << endl;

}

Student(const Student& s)

: Person(s)//这个地方要把派生类中的基类那一部分给拿出来

//那么切片,就是将派生类对象赋值给基类的引用或指针,叫切片

//并且这里只需要这么写person(s)。因为上面的基类中是&p,

//所以说意思就是将派生类中的基类的那一部分给切下来给基类,交给他们进行

//初始化即可。当然,若基类中只有一个成员变量,可能你说有点多余,但是基类中

//有好多个成员变量呢?直接把他们切下来就非常好用了。

, _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()

{

cout << "~Student()" << endl;

}

protected:

int _num; //学号

};

int main()

{

Student s1("jack","23456", 18);

Student s2(s1);

Student s3("rose","56789" ,17);

s1 = s3;//按理说派生类的拷贝构造与析构都是不需要自己去写的,只有一个构造需要自己写

//一般写一个类后,最好还是自己实现一下构造函数,即初始化

//若其默认构造函数的行为不符合需求(例如需要特定参数),需显式调用其他构造函数

//但一般默认构造函数也就会将它们初始化为空(string)

//接下来,我需要确认std::vector的默认构造函数的定义。根据C++标准,

// 默认构造函数会创建一个空的vector,没有元素,容量为0。

// 这意味着它不会分配任何内存,只是初始化内部指针为nullptr,大小和容量为0。

//cplusplus网站上的构造的第一个就是默认构造函数的行为

return 0;

}

1.关于构造函数部分:由于派生类中有基类的那一部分,子类继承父类,创建一个子类对象的时候,这个子类对象中有父类的部分的,是需要调用父类的构造来构造这部分内容的。所以说要先调用基类的构造函数,先完成对基类的构造,之后再构造派生类。

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

3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。

4.来看它的析构函数:

这是上面那个代码的执行结果,观察这个我们发现,有三个一样的~student(),~person(),这个是因为有三个对象,需要析构三次。关键的问题是不是只析构一次吗?为什么会出现两个内容?(~student()与~person())原因是:

这里先调用了派生类的析构函数,之后调用了基类的析构函数,那么为什么先调用派生类的析构函数呢?因为,派生类是依靠基类来实现的吧,所以说,如果先析构了基类,那么依靠基类的派生类中的资源是不是就是不安全的了(因为基类都被析构了),所以为了确保派生类的安全,就先析构派生类,这样可以避免资源安全的问题。

还有一个问题就是,我能不能在这里显示的写一个基类的析构函数,不可以的,原因是,你只要显示写了,大概率会先调用基类的析构函数,那么经过咱们上面的解释,是不是先调用基类的析构函数不安全。并且,子类的析构函数是比较特殊的一个函数,我们不需要显示调用父类析构函数,每个子类析构函数后面,会自动调用父类析构函数。所以说析构函数不需要咱们去管就行了。

还有一个问题:基类的析构函数要与派生类的析构函数名字保持一致。因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个多态章节会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

5.实现不被继承的类

这里咱们讲C++11的用法,因为C++11的用法较C++98更简单。

在基类后面加上final关键字,那么这个基类就不可以被继承了

6.継承与友元

基类的友元是不可以被继承到派生类中的(就像父亲的朋友不是你的朋友)

这里有两个问题:

1.基类中的友元声明,并不知道student类在哪。那有同学又说了要是把派生类放基类前面不就可以了吗?那请问你这样的话,这个継承还怎么继承呢?所以最好的办法就是在开头加上class student,让编译器知道有这个类。

2.如果不在student类中加上友元声明,那么下面的友元函数中的第二个就无法打印,因为类外无法访问类中的protected限定的成员变量,除非也在student类中加上友元声明 。

7.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个派生类,都 只有⼀个static成员实例。

8.多继承及其菱形继承问题

单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派生类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就 ⼀定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议 设计出菱形继承这样的模型的。

这种就是菱形继承问题,那么就会导致Assistant中有两个person中的成员变量,会冗余。并且,你要是调用比如student类继承的person变量,不可以,因为编译器不知道你要调用哪个(因为teacher中,person中都有这个成员变量)。

其实这种也是菱形继承:B继承了A,C继承了B,D继承了A,E继承了C和D,那么E就有了两份A

只要派生类中有两份相同的变量时,都叫做菱形继承,那么在B和D的位置放virtual,原因下面有。

解决办法就是:

1.指定作用域是哪个类。

2.采用虚继承的方式。

8.2 虚继承

在继承那个双份的成员变量的第一个派生类中加上virtual即可。virtual加在:后,继承方式前。

那么加上这个后,你再去访问那个双份的成员变量,访问到的就只有最一开始的基类中的成员变量。要是还想访问类中的那个成员变量,加上作用域即可。

8.3 来看一个小题

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;

}

看这个题,main函数里的看着熟悉不,没错,就是咱们说的切片,那么这个问题的本质就是,这个切片在内存中是怎么存储的,就是先先继承的内存更靠上 。

p3是指向派生类的指针,自然指向最上面,地址的起始处。而p1是最先继承的,所以也在地址最高处,而p3与p1指向同一个地方,纯属巧合。自然p2的位置就相对于靠后点。

所以说它们几个的关系是: p3==p1!=p2

9.继承和组合

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

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

3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复⽤ (white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对派生类可 见。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依 赖关系很强,耦合度高

4.对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复⽤风格被称为黑箱复用(black-boxreuse), 因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关 系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

5.优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合用继承(is-a)也适合组合(has-a),就用组合。

继承的写法

template<class T>

class stack :public vector<T>

{};

组合的写法

template<class T>

class stack

{

public:

vector<T> _v;

};

这里有个耦合度的概念:可以把耦合度理解为联系,耦合度高就是联系紧密,耦合度低就是联系松散。那么咱们写一个项目,有几百个类,那么使用耦合度低的,还是耦合度高的?肯定是耦合度低的,你想想,如果说,你在写项目的时候,一个类写错了,那么入过这个项目的耦合度高,会牵连到其他的几个类,甚至说整个项目,导致整个项目会出现修不完的bug,严重的时候,可能要重写。这个代价成本可是非常大的。所以说有耦合度低的肯定都会选择耦合度低的。

而上文提到的白箱与黑箱,就是白箱就是可视化程度很高,即 继承,因为继承的话,基类中的所有派生类都可以看到,也就是耦合度高。而黑箱,就是可视化很低,也就说明每个类之间耦合度低,所以说,没有啥联系,互相之间可看到的代码也就很少,符合类的封装。

OK,可算是讲完了,大家若是发现错误,还请指出,谢谢啦!

本篇完.........................

相关推荐
Bunury37 分钟前
element-plus添加暗黑模式
开发语言·前端·javascript
XiaoyaoCarter1 小时前
每日两道leetcode
c++·算法·leetcode·职场和发展·贪心算法
LIU_Skill1 小时前
SystemV-消息队列与责任链模式
linux·数据结构·c++·责任链模式
矛取矛求1 小时前
STL C++详解——priority_queue的使用和模拟实现 堆的使用
开发语言·c++
Non importa2 小时前
【C++】新手入门指南(下)
java·开发语言·c++·算法·学习方法
pp-周子晗(努力赶上课程进度版)2 小时前
【C++】特殊类的设计、单例模式以及Cpp类型转换
开发语言·c++
海洋与大气科学2 小时前
【matlab|python】矢量棍棒图应用场景和代码
开发语言·python·matlab
海码0073 小时前
【Hot100】 73. 矩阵置零
c++·线性代数·算法·矩阵·hot100
菜鸟学编程o3 小时前
C++:继承
开发语言·c++
wiseyao12193 小时前
c#操作excel
开发语言·c#·excel