细说c++三大特性,不可不知

细说c++三大特性


不管是入门还是进阶,作为c++开发者,都需要了解c++的语言特性,这也是我的复习笔记。

首先,c++的特性包含三大部分:封装继承多态。封装可以使得代码模块化(是基础),继承可以扩展已存在的代码(是关键),而多态的目的则是为了接口重用(是补充

1. 封装

封装:突破了C语言对于函数的限制,封装可以隐藏实现细节,从而使得代码能够模块化。

这有点像在设计电路元器件的时候,把与或门组合成一个加法器然后把这一个整体封装起来当作一个整体,以后再用到加法器的时候就不用一个一个去组合与或门而是直接使用封装好的那一个整体。

在c++中,封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。也就是说把成员变量和成员函数封装起来,对成员变量的访问只能通过成员函数。(类中不再留有public的成员变量,外界对private的成员变量的访问和操作完全通过成员函数)

类是一种复杂的数据类型它是将不同类型的数据和与这些数据相关的操作封装在一起的集合体。因此,类具有对数据的抽象性,隐藏性和封装性。

在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

1.1 数据抽象

数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。仅对用户呈现接口而使具体实现透明化。

是一种依赖于接口和实现分离的编程(设计)技术

这样做有什么好处呢?

扫描二维码关注公众号,回复: 11485346 查看本文章
  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损
  • 类实现可能随着时间的推移而发生变化,而抽象可以规避这种不断变化的需求(不至于牵一发而动全身,只需要修改对应的数据成员就可)

1.2 数据封装

谈到了数据的抽象,那也就引出了数据的封装。数据抽象是一种仅向用户呈现接口而把具体的实现细节隐藏起来的机制,数据封装是一种把数据和操作数据的函数捆绑在一起的机制。

在c++中,我们通过创建类来进行封装和隐藏,通过私有成员(private)来把数据进行隐藏,并通过类的私有成员函数来访问或编辑,通过公有函数(public)来实现外界对于类的访问,相当于接口。

1.3 C++ 接口(抽象类)

  • 接口: 接口描述了类的行为和功能,而不需要完成类的特定实现。
  • 抽象类:如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。抽象类一般是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

举个例子,动物是一个抽象类,从他这里派生出老虎子类,狮子子类。老虎和狮子可以进行实例化(看得见摸得着)成为一头老虎A和一头狮子B,而动物进行实例化好像就有点不那么真实(是一个抽象的概念,不是实体),所以抽象类不能实例化。

纯虚函数是通过在声明中使用 “= 0” 来指定的virtual 虚函数

2.继承

继承:继承可以通过扩展已存在的代码模块(类),达到代码重用的目的。原有的类称为基类或父类,产生的新类称为派生类或子类。

2.1 派生类

如何声明派生类呢?

class 派生类名:继承方式 基类名1, 继承方式 基类名2,...,继承方式 基类名n{
    派生类成员声明;
};

其中继承方式指的是public、protected和private,默认为private。

2.2 三种继承方式

  • 公有继承public:基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有成员和保护成员。派生类的其他成员可以直接访问它们。无论派生类的成员还是派生类的对象都无法访问基类的私有成员。
  • 私有继承private:基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在该类外部通过派生类的对象无法访问。无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承的私有成员。通过多次私有继承后,对于基类的成员都会成为不可访问,因此私有继承比较少用。
  • 保护继承protected:保护继承中,基类的公有成员和私有成员都以保护成员的身份出现在派生类中,而基类的私有成员不可访问。派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但是类外部通过派生类的对象无法访问它们,无论派生类的成员还是派生类的对象,都无法访问基类的私有成员。

2.3 二义性

在单继承下,派生类可以直接访问public和protected成员。
而在多继承下,派生类可以从两个或者更多个基类中继承同名的成员,然而在这种情况下,直接访问是二义的(不知道访问的具体是哪一个成员),将导致编译时刻错误。通过指定成员名,来限定域,消除二义性

举个栗子

#include <iostream>
using namespace std;

class A {
public:
void z();
};

class B {
public:
void z();
void x();
};

class C : public A, public B {
public:
void x();
void y();
};

int main(){
    C c1;
    // c1.z();    产生二义性问题,访问A中的 z()? 还是 B中的 z() ?
    //通过指定成员名,来限定域,消除二义性
    c1.A::z();
    c1.B::z();
}

由于二义性的原因,一个类不可以从同一个类中直接继承一次以上。也就是说假如类A是基类,B继承自A,C继承自B,那么C的实例调用成员时不能限定为A的域,而只能限定到B。

3.多态

多态:允许将子类类型的指针赋值给父类类型的指针(根据不同的对象或消息,选择不同的函数指针来调用),也可以理解为接口,“一个接口,多种方法”。

顺便小结一下前两节,顺便引出多态的作用:

  • 封装可以使得代码模块化
  • 继承可以扩展已存在的代码(为了代码重用)
  • 多态的目的则是为了接口重用(不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法)

3.1 两种多态

C++支持两种多态性:编译时多态性,运行时多态性。

  • 编译时多态性:通过重载函数实现
  • 运行时多态性:通过虚函数实现(在基类建立虚函数允许在子类(派生类)重新定义成员函数,这种方法也陈伟覆盖或重写)

在使用动态多态时(也就是调用重写的派生类成员函数时),通过基类中的指针,指向虚函数,由于每个虚函数都有一个虚函数列表,此时调用的与虚函数同名的重写函数并不是直接调用虚函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同。

3.2 重写 vs 重载 vs 隐藏

  • 重写可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性。
  • 重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。(发生在同一个类中,与基类子类区分)
  • 隐藏是指派生类的函数屏蔽了与其同名的基类函数。通常有两种方法:
    • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual 关键字,基类的函数将被隐藏
    • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏。(如果有virtual关键字则是重写)

3.3 虚函数与抽象类

  • 虚函数:虚函数是在基类中被声明为virtual,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数,可实现成员函数的动态覆盖(Override)也称重写。

不使用虚函数的重写是静态多态(静态链接),函数的调用在程序执行前就已经准备好了。在调用时会使用基类的指针导致调用了基类函数。

猜你喜欢

转载自blog.csdn.net/TalesOV/article/details/107283954