面向对象程序设计
面向对象程序设计
- 面向对象程序设计(Object-oriented Programming)具有以下几个特征:
- 程序由若干对象组成,每个对象是由一些数据以及对这些数据所能实施的操作所构成的封装体;
- 对数据的操作是通过向包含数据的对象发送消息(调用对象对外接口中的操作)来实现的,体现了数据抽象;
- 对象的特征(包含哪些数据与操作)由相应的类来描述;
- 一个类所描述的对象特征可以从其它的类继承(获得)。
- 注意:如果没有“继承”,则称为:基于对象的程序设计(Object-based Programming)
继承会破会封装,有利有弊
- 在面向/基于对象的程序设计中,对象/类体现了数据抽象与封装!

- 面向对象程序的执行过程
- 对象构成了面向对象程序的基本计算单位,程序的执行体现为对象间的一系列消息传递:
- 从程序外部向程序中的某个对象发送第一条消息启动计算过程;
- 该对象在处理这条消息的过程中,又向程序中的其它对象发送消息,从而引起进一步的计算;
- 消息处理可分为两种方式:
- 同步消息处理:消息发送者必须等待消息处理完才能继续执行其它操作(顺序执行)。
- 异步消息处理:消息发送者不必等待消息处理完就能继续执行其它操作(并发执行)
- 本课程只涉及同步消息处理。
- C++是混合语言
- 既有对象,又有过程
- 保留了main函数,第一条消息由main发出
- 面向对象程序设计的基本内容
- 对象/类(Object&Class)
- 对象是由数据以及能对其实施的操作所构成的封装体。
- 类描述了对象的特征(包含什么类型的数据和哪些操作),实现数据抽象。
- 对象属于值的范畴,是程序运行时刻的实体;类则属于类型的范畴,是编译时刻的实体。
- 继承(Inheritance)
- 在定义一个新的类(子类、派生类)时,可以把已有类(父类、基类)的一些特征描述先包含进来,然后再添加新的特征描述。
- 可分为
- 单继承
- 多继承
- 消息的多态与绑定
- 多态(Polymorphism):某一论域中的一个元素存在多种形式和解释。
- 在程序中,多态通常体现为:
- 一名多用: 函数名重载 操作符重载(语言预定义和用户自定义)
- 类属: 类属函数:一个函数能对多种类型的数据进行相同的操作。 类属类型:一个类型可以描述多种类型的数据。
- 面向对象程序特有的多态(继承机制带来的):
- 对象类型的多态:子类对象既属于子类,也属于父类。
- 对象标识的多态:父类的引用或指针可以引用或指向父类对象,也可以引用或指向子类对象。
- 消息的多态:发给父类对象的消息也可以发给子类对象,父类与子类可以给出不同的解释(处理)。
- 绑定(Binding):确定对多态元素的某个使用是多态元素的哪一种形式。可分为静态绑定和动态绑定:
- 静态绑定(Static Binding):在编译时刻确定。
- 动态绑定(Dynamic Binding):在运行时刻确定。
- 大多数形式的多态可采用静态绑定。如函数名重载
- 由于存在对象标识的多态和消息的多态,消息的解释(处理)有时要采用动态绑定!
- 通过父类的引用或指针向对象发送消息,消息处理函数需要到运行时刻决定(属于父类还是子类)
- 多态带来的好处
- 使得程序功能扩充变得容易:程序上层代码不变,只要增加底层的多态元素即可。
- 增强语言的可扩充性,如操作符重载等。
也就是C中的一个union
- 面向对象程序设计带来的好处
- 一个方法的优劣主要看它是否能提高软件开发效率和保证软件质量!
- 影响软件开发效率和软件质量的因素主要包括:
- 抽象(控制复杂度)
- 封装(保护信息)
- 模块化(组织和管理大型程序)
- 软件复用(缩短开发周期)
- 可维护性(延长软件寿命)
- 软件模型的自然度(缩小解题空间与问题空间之间的语义间隙,实现从问题到解决方案的自然过渡)
对象与类
类
- 对象构成了面向对象程序的基本计算单位,而对象的特征则由相应的类来描述。
- 对象是用类来创建的,因此,程序中首先要定义类。在C++中,类是一种用户自定义类型,定义形式如下:
我们看一个定义日期类的class
- 数据成员是对对象所包含的数据描述,它们可以是常量和变量,类型可以是任意的C++类型(void除外)
- 在类中说明一个数据成员的类型时,如果未见到相应类型的定义,或相应的类型未定义完,则该数据成员的类型只能是这些类型的指针或引用类型。例如:
- 成员函数是对对象包含的数据所能实施的操作描述。
- 成员函数的实现(函数体)可以放在类定义中,例如:
- 成员函数的实现也可以放在类定义外,例如:
- 成员函数名是可以重载的,它遵循一般函数名的重载规则
注意:在C++中,也允许在结构(struct)和联合(union)中定义函数,但它们的成员访问控制与类不同!
- 类的访问控制
- 通过一下成员访问控制符来控制外部访问
public随意访问private只能在本类和友元的代码中访问protected只能在本类、派生类和友元的代码中访问- 在C++的类定义中,可以有多个
public、private和protected访问控制说明;默认访问控制是private - 结构
struct和联合union成员的默认访问控制是public - 一般来说,类的数据成员和在类的内部使用的成员函数应该指定为
private,只有提供给外界使用的成员函数才指定为public - 具有
public访问控制的成员构成了类与外界的一种接口(interface) - 在一个类的外部只能访问该类接口中的成员
protected类成员访问控制具有特殊的作用(在派生类中使用)
对象
- 类属于类型范畴的程序实体,它一般存在于静态的程序(编译程序看到的)中
- 而动态的面向对象程序(运行中的)则是由对象构成
- 对象在程序运行时创建
一个变量,如果它的定义是类,那他就是对象
- 对象的创建和标识
- 直接方式:通过在程序中定义一个类型为类的变量来实现
- 间接方式:在程序运行时刻,用
new操作来创建对象(称为动态对象),用delete操作来撤消它(使之消亡) - 动态对象需要通过指针来标识和访问
- 单个动态对象的创建与撤消
- 动态对象数组的创建与撤消
- 注意:动态对象一般不采用C的库函数
malloc、calloc来创建和用free来撤消!
对象在进入相应变量的生存期时创建,通过变量名来标识和访问。相应变量的生存期结束时,对象消亡。
分为:全局对象、局部对象。
- 对象的操作
- 对于创建的一个对象,需要通过向它发送消息(调用对象类中定义的某个public成员函数)来对它进行操作。例如:
- 在类的外部访问类的成员时要受到类成员访问控制的限制
- 可以对同类对象进行赋值
- 取对象地址
- 把对象作为参数传给函数。例如:
- 把对象作为函数的返回值。例如
this pointer
- 类中描述的数据成员(静态数据成员除外)对该类的每个对象分别有一个拷贝。例如:
- 类中的成员函数是不是对该类的每个对象都有一个拷贝呢?否!
- 成员函数对该类所有对象只有一个拷贝!那么在函数g中,怎么知道x是对象a还是对象b的呢?
- 类的每一个成员函数(静态成员函数除外)都有一个隐藏的形参
this,其类型为该类对象的指针;在成员函数中对类成员的访问是通过this来进行的。例如, - 对于前面A类的成员函数g:
void g(int i) { x = i; }编译程序将会把它编译成:void g(A *const this, int i) { this->x = i; };
- 当通过对象访问类的成员函数时,将会把相应对象的地址传给成员函数的参数this。例如,
- 对于下面的成员函数调用:
a.g(1); 和b.g(2); 编译程序将会把它编译成:g(&a,1); 和g(&b,2);
一般
this 不需要我们自己加入,但是但如果成员函数中要把this所指向的对象作为整体来操作,则需要显式地使用this指针。再例如再例如:
构造函数与析构函数
对象的初始化与构造函数
- 当一个对象创建时,它将获得一块内存空间,该内存空间用于存储对象数据成员的值。在使用对象前,需要对对象内存空间中的数据成员进行初始化
- 如何初始化?
- C++提供了一种对象初始化的机制:构造函数
- 它是类的特殊成员函数,名字与类名相同、无返回值类型。
- 创建对象时,构造函数会被自动调用。例如:
- 构造函数可以重载,其中,不带参数的(或所有参数都有默认值的)构造函数被称为默认构造函数
- 在创建对象时,如果没有指定调用对象类中哪一个构造函数,则调用默认构造函数初始化。也可以显式地指定调用对象类的某个构造函数
- 在程序中也可以通过类的构造函数来创建一些临时对象。例如
- 常量和引用数据成员的初始化
- 对于常量和引用数据成员
- 不能在说明它们时初始化(C++11之前的版本)
- 也不能采用赋值操作在构造函数中对它们初始化
- 可以在构造函数的函数头和函数体之间加入一个成员初始化表来对常量和引用数据成员进行初始化。例如:
- 在成员初始化表中,成员的书写次序并不决定它们的初始化次序,它们的初始化次序由它们在类定义中的描述次序来决定。
malloc只是分配内存,没有调用构造函数


在哪儿对它们初始化呢
析构函数
- 在类中可以定义一个特殊的成员函数:析构函数,它的名字为“
~<类名>”,没有返回类型、不带参数、不能被重载
- 一个对象消亡时,系统在收回它的内存空间之前,将会自动调用对象类中的析构函数
- 可以在析构函数中完成对象被删除前的一些清理工作
- 一般情况下,类中不需要自定义析构函数,但如果对象创建后,自己又额外申请了资源(如:额外申请了内存空间),则可以自定义析构函数来归还它们
- 例如,系统为对象s1分配的内存空间只包含len和str(指针)本身所需的空间,str所指向的空间不是由系统分配和归还的,而是由对象作为资源自己申请和归还的
- 再例如用链表实现的栈类,由于链表中的结点空间是push操作申请的,如果pop操作数量不够,则对象消亡时要在析构函数中归还它们!
- 析构函数可以显式调用,这时并不是让对象消亡,而是暂时归还对象额外申请的资源。 例如:
- 在创建包含成员对象的对象时,除了会自动调用本身类的构造函数外,还会自动去调用成员对象类的构造函数。
- 通常是调用成员对象类的默认构造函数。
- 如果要调用成员对象类的非默认构造函数,需要在包含成员对象的对象类的构造函数成员初始化表中显式指出!
- 包含成员对象的对象消亡时,除了会自动调用本身类的析构函数外,还会自动去调用成员对象类的析构函数
- 成员对象初始化和消亡处理的次序
- 包含成员对象的对象创建时,
- 先执行成员对象类的构造函数,再执行本对象类的构造函数。
- 若包含多个成员对象,这些成员对象的构造函数执行次序则按它们在本对象类中的说明次序进行。
- 从实现上说,
- 是先调用本身类的构造函数,但在进入函数体之前,会去调用成员对象类的构造函数,然后再执行本身类构造函数的函数体!
- 也就是说,构造函数的成员初始化表(即使没显式给出)中有对成员对象类的构造函数的调用代码。
- 注意:如果类中未提供任何构造函数,但它包含成员对象,则编译程序会隐式地为之提供一个默认构造函数,其作用就是调用成员对象类的构造函数!
- 包含成员对象的对象消亡时,
- 先执行本身类的析构函数,再执行成员对象类的析构函数。
- 如果有多个成员对象,则成员对象析构函数的执行次序则按它们在本对象类中的说明次序的逆序进行。
- 从实现上说,
- 是先调用本身类的析构函数,本身类析构函数的函数体执行完之后,再去调用成员对象类的析构函数! 也就是说,析构函数的函数体最后有对成员对象类的析构函数的调用代码!
- 注意:如果类中未提供析构函数,但它包含成员对象,则编译程序会隐式地为之提供一个析构函数,其作用就是调用成员对象类的析构函数。
拷贝构造函数
- 若一个构造函数的参数类型为本类的引用,则称它为拷贝构造函数。例如:
在创建一个对象时,若用另一个同类型的对象对其初始化,将会调用对象类中的拷贝构造函数
- 在三种情况下,会调用类的拷贝构造函数:
- 创建对象时显式指出。例如:
- 把对象作为值参数传给函数时。例如:
- 把对象作为函数的返回值时。例如:
- 隐式拷贝构造函数
- 在程序中,如果没有为某个类提供拷贝构造函数,则编译器将会为其生成一个隐式拷贝构造函数。
- 隐式拷贝构造函数将逐个成员进行拷贝初始化
- 对于非对象成员:它采用通常的拷贝操作;
- 对于成员对象:则调用成员对象类的拷贝构造函数来对成员对象进行初始化。(递归定义!)
- 自定义拷贝函数
- 一般情况下,编译程序提供的隐式拷贝构造函数的行为足以满足要求,类中不需要自定义拷贝构造函数。
- 但在一些特殊情况下,必须要自定义拷贝构造函数,否则,将会产生设计者未意识到的严重的程序错误。
- 例如,在下面的类中没有自定义拷贝构造函数:
- 为了解决上面的问题,可以在类String中显式定义一个拷贝构造函数来实现深拷贝(deep copy)
最常见就是字符串

它带来的问题是:
如果对一个对象(s1或s2)操作之后修改了这块空间的内容,则另一个对象将会受到影响。如果不是设计者特意所为,这将是一个隐藏的错误。
当对象s1和s2消亡时,将会分别去调用它们的析构函数,这会使得同一块内存区域将被归还两次,从而导致程序运行错误。
当对象s1和s2中有一个消亡,另一个还没消亡时,则会出现使用已被归还的空间问题!
系统提供的隐式拷贝构造函数实施的是浅拷贝(shallow copy):只拷贝数据成员本身的值
注意:自定义的拷贝构造函数不会自动调用成员对象类的拷贝构造函数!
上面b2.a是用A的默认构造函数初始化的,如果要用b1.a对其初始化,需要在B类的拷贝构造函数的成员初始化表中显式指出!
下面对b2.a进行赋值初始化与拷贝构造的区别是什么?