C++ 面向对象程序设计:数据抽象、继承、多态

Table of Contents

第一章    数据抽象(访问控制与封装)

一、概述

二、类的访问控制

第二章    继承

一、概述

二、继承说明符

第三章    多态(虚函数与动态绑定)

一、概述

二、多态基本原理

三、使用方法

四、纯虚函数与抽象基类


C++是面向对象的程序设计,即类(class)和由类创建的实例(instance,即对象)。

面向对象的程序设计(Object Oriented Programming)三个重要思想:数据抽象、继承、多态,这三种思想分别实现了操作权限、扩展性和灵活性(代码统一性)的需求

思想 作用
数据抽象 权限
继承 扩展
多态 灵活

C++中类是数据抽象、继承和多态实现的载体,因此说类是C++最基本的特性。类将相关联的数据(数据成员)和操作(成员函数 / 方法)绑定在一起

第一章    数据抽象(访问控制与封装)

本文讲解第一个重要思想——数据抽象。

一、概述

1、数据抽象的概念:类的接口和实现分离。类的接口是用户所能执行的操作(包括public成员函数的声明和非成员接口函数的声明);类的实现包括类的数据成员、私有函数、负责类的接口实现的函数体(注意:a. public访问控制符后的成员函数声明 b.非成员接口函数(包括友员)的声明属于接口,但这些函数的函数体属于实现)。

2、数据抽象的本质体现了一种“权限”的思想。也就是说,类的设计者可以使用全部的成员函数和数据成员;类的用户(即类的使用者,包括: a.将类实例化为对象进而使用类的 b. 以该类为基类通过继承设计该类的派生类的程序开发者)则根据分配的权限,通过接口使用类的成员函数和数据成员

3、数据抽象的实现方法:封装!封装的操作包括两个层面,主要层面是通过访问控制确定类用户的访问权限,另外的层面也包括声明和定义的分离。通过访问控制层面的封装,类的用户只能使用类的接口,而无法访问其实现部分;通过声明和定义分离层面的封装,向用户隐层了类的实现细节。

4、类的封装有诸多好处:

  • 从程序安全性角度,类的封装可以保证类内私有成员的状态不被类的用户修改,在类有多个用户时这一点格外重要;
  • 从程序模块化和升级角度,当类的设计者修改类的实现时,不会影响用户级别的代码。

注意:类的接口与实现分离不等同于C++语言层面的接口和实现分离。

  • 在C++语言层面:接口指的是声明(.h头文件),实现指的是定义(.cpp源文件),接口与实现分离是通过声明和实现分离编译实现的。从而,可以将程序源码cpp编译成库文件(.a, .so),代码使用者只能看到.h文件中的声明,起到代码保密的作用。
  • 在类层面:public下所定义的用户所能执行的操作是接口,private下的数据成员和函数以及实现接口的函数体是实现。封装更多是从开发角度考虑的,是为了程序设计的安全性和模块化的升级考虑的,或者说封装分配的权限不是为了代码保密
  • 注意区分C++语言层面和类层面的抽象。例如:.h文件在C++语言层面属于接口,但.h文件内的类中包含数据成员和私有函数,这些数据成员和私有函数在类层面属于实现范畴(即便.h文件中只有私有函数的声明,私有函数仍然属于类的实现)。再例如:.h文件中public的成员函数如果是定义在类内部的(自动inline),在C++语言层面属于接口和实现未分离,但在类层面这些成员函数的函数体也属于实现。

封装规范tips:

1、尽量将(全部)数据成员设成private;

2、对于非成员的接口函数,函数的生命与类在同一个头文件内。

二、类的访问控制

使用访问说明符实现封装中权限的分配:

public后的成员定义类的接口;

private封装了类的实现细节,即private后的成员只能被类的设计者(成员函数)使用,不能被类的用户(类的对象或派生类)使用。

当继承说明符为public时,类的访问控制为:

访问说明符 类的定义中【1】

类的对象

友元函数
private 可访问

不可访问

可访问
protected 可访问

不可访问

可访问
public 可访问

可访问

可访问

举例:

class Base{
private: 
int a;
protected:
int b;
public: 
int c;

public:
void f(){
int d=a; //Y 类(的定义)中,可访问private
int d=b; //Y 类(的定义)中,可访问protected
int d=c; //Y 类(的定义)中,可访问public
}
};

void main(){
Base objectBase
objectBase.a  //N 类的对象,不可访问private
objectBase.b  //N 类的对象,不可访问protected
objectBase.c  //Y 类的对象,可访问public
};

注释:

【1】类的定义中无需使用 对象.成员/成员函数 或 对象的指针->成员/成员函数 的形式,在类中自动使用隐式的this指针,指向调用函数的对象的成员。

第二章    继承

一、概述

继承实现程序的复用与扩展。

使用类派生列表说明派生类是继承自哪个基类的。同时需要指明访问说明符public, protected或private,以控制派生类从基类继承而来的成员是否对派生类的用户可见。为了区分类内部的访问说明符,我们不妨将派生类列表中的访问说明符称为继承说明符。

二、继承说明符

当继承说明符为public时,类的访问控制为:

访问说明符 派生类的定义中 派生类的对象 友元函数
private 不可访问 不可访问 可访问
protected 可访问【1】 不可访问【2】 可访问
public 可访问 可访问 可访问

举例:

class Base{
private: 
int a;
protected:
int b;
public: 
int c;
};

class Derived:public Base{
public:
void f(){
int e=a;   // N 派生类(的定义)中,不可访问private
int e=b;   // Y 派生类(的定义)中, 可以访问protected
int e=c;   // Y 派生类(的定义)中, 可以访问public
}
};

void main(){
Derived objectDerived
objectDerived.a //N 派生类的对象,不可访问private
objectDerived.b //N 派生类的对象,不可访问protected
objectDerived.c //Y 派生类的对象,可访问public
};

注释:

【1】《C++ primer》中有一句话有歧义:“派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于基类对象中的受保护成员没有任何访问特权”。其中,“只能通过派生类对象来访问基类的受保护成员”容易理解为“不能通过其他方式访问基类的受保护单元”,这显然是不对的。例如,可以通过派生类定义中直接访问基类的受保护成员,如下的访问是正确的:

class Base {
protected:
int prot_mem; // protected member
};
struct Pub_Derv : public Base {
// ok: derived classes can access protected members
int f() { return prot_mem; }
};

该句的英文原文是:“A derived class member or friend may access the protected members of the base class only through a derived object. The derived class has no special access to the protected members of base-class objects."

在英文原文中,动词access前的副词是may,表示的是可以,并非中文版中的“只能”;英文原文中,使用only修饰的是through,只是表示强调的感情色彩,only完全可以删除。所以,更严谨的说法是:“派生类的成员或友元(或概括为:在派生类中)可以通过派生类对象来访问基类的受保护成员。即使是在派生类中,也不能通过基类对象访问基类对象中的受保护成员”。例子如下:

class Base {
protected:
int prot_mem; // protected member
};
class Sneaky : public Base {
friend void clobber(Sneaky&); // can access Sneaky::prot_mem
friend void clobber(Base&); // can't access Base::prot_mem
int j; // j is private by default
};
// ok: clobber can access the private and protected members in Sneaky objects
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// error: clobber can't access the protected members in Base
void clobber(Base &b) { b.prot_mem = 0; }

以上说明:在派生类中,既可以直接使用基类的protected成员,也可以通过派生类对象来访问基类的protected成员。

【2】不在派生类中,派生类对象可以访问基类的受保护成员吗?不可以!

更一般的情况:

继承说明符 基类成员在派生类中访问控制变化【1】 派生类对象【2】
private

private ->private

protected->private

public->private

不可访问
protected

private ->private

protected->protected

public->protected

不可访问
public

private ->private

protected->protected

public->public

可访问

注释:

【1】派生类的访问控制向更私密的方式转变:

  • 如果继承说明符为private,则所有派生类的访问控制转化为private;
  • 如果继承说明符为protected,则public的访问控制转化为protected;
  • 如果继承说明符为protected,则所有派生类的访问控制不发生变化;

【2】派生类对象的访问控制不变

第三章    多态(虚函数与动态绑定)

一、概述

多态是面向对象程序设计的核心思想。

目的:以统一的方式,调用相似类(父类及其子类,以及多个子类)的对象,从而保证代码的统一性和应用的灵活性。

  • 在父类中定义好一种函数接口(虚函数),在子类中实现不同功能(覆盖\重写),并根据需要调用不同版本(动态绑定)。

虚函数: 对于某些函数,基类希望其派生类各自定义相应版本,此时,基类就将这些函数声明成虚函数。为了保证代码的统一性,需要派生类的虚函数的形参必须与基类对应虚函数的形参完全一致;派生类中虚函数的返回类型与基类对应虚函数的返回类型匹配(a. 返回类型完全一致,或者b.返回类本身的指针或引用)。

动态绑定:当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定(dynamic binding)。即基类和派生类中接口一致,在程序执行时,具体调用基类的成员函数还是派生类对应的成员函数取决于调用成员函数的指针所指向的或者调用成员函数的引用所引用的是基类的对象还是派生类的对象,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

在程序开发过程中,设计程序框架时可以在基类中留好接口(a. 输入--函数参数列表,b. 输出--返回值类型,c. 函数名)或实现最基本的功能(如果连最基本的功能也不实现,只留接口,就是纯虚函数和抽象基类),具体的功能可以由其他人在派生类中重写。即基于前人代码的框架,实现新的功能。

二、多态基本原理

1、派生类引用/指针到基类引用/指针的类型可转换性:由于派生类对象都包含基类的部分,因此,可以将基类的引用或指针绑定到派生类对象的基类部分上

2、基类的指针或引用的静态类型(是变量声明的类型,编译时即可确定)与动态类型(内存中对象的类型,运行时才能确定)可能不一致。

3、在使用基类的引用或指针调用一个虚函数时发生动态绑定,被调动的函数的版本时绑定的指针或引用的对象的动态类型相匹配的那个。

三、使用方法

1、基类中添加virtual关键字:把类中需要动态绑定的函数加上virtual关键字,声明为虚函数。(PS:中文把virtual翻译成“虚”,汉语中对“虚”理解一般是“假的”意思,用这种释义理解虚函数总是没法把握虚函数的要领,甚至没法接受“虚函数”的概念,我觉得不妨把“虚”理解成“不定的”、“不确定的”,即动态绑定,这样提到虚函数时马上就明白了它的用途。

2、派生类必须对其内部重写的虚函数进行声明。派生类声明重写的虚函数时前面可加virtual,也可不加。基类中的虚函数,在其所有派生类中隐式地也是虚函数。

派生类中建议把覆盖的虚函数后添加override关键字,提示编译器检测是否按照编程者的本意成功完成覆盖基类中的虚函数。

四、纯虚函数与抽象基类

基类中虚函数定义统一的接口,在派生类中通过重写实现不同版本的功能。

基类可以定义虚函数的功能,但有时只是设计接口,而暂时无法定义具体功能。此时,将虚函数声明后加上=0,即:

virtual int this_is_virtual_function() const = 0

因为纯虚函数只定义接口,没有实现功能,所以含有纯虚函数的类不能创建对象,称为抽象基类。

备注 :重写(override、覆盖 V.S. 函数重载(overload) V.S. 重定义(redefining、隐藏)的区别。

  • 重写(override、覆盖): 子类重写父类中有相同名称和参数的虚函数(virtual)。即重写是发生在类继承过程中的,且要求父类和继承类中的函数名相同、形参一致,最重要的是父类中函数为虚函数(virtual)。通过调用函数的指针或引用动态绑定的对象不同选择不同版本的函数,从而实现多态。
  • 重定义(redefining、隐藏):子类重新定义父类中相同名称的非虚函数(函数列表可同、也可不同)或者相同名称但不同参数列表的虚函数,子类会隐藏其父类的方法。
  • 函数重载(overload): 在一个类的内部,函数名相同,但要求形参不同(数量、类型),通过形参数量或类型的差异调用不同版本。代码中调用表达式是不同的。

参考:https://blog.csdn.net/zhejfl/article/details/90199200

猜你喜欢

转载自blog.csdn.net/Cxiazaiyu/article/details/86610573
今日推荐