今天在这里总结下 C++ 面向对象(OOP)的基本知识。
OOP 的最重要的特性是:
- 抽象
- 封装和数据隐藏
- 多态
- 继承
本篇博客将会从什么是类开始讲,随后说一说C++的访问控制和封装,随后是继承。
一、类的构成
首先,我们明确什么是类,或者扩大来说什么是类型。我们先来看一下基本类型,如 int
,指定一个变量为整形其实完成了三项工作:
- 决定数据对象需要的内存数量
- 决定了如何解释内存中的位
- 决定了可使用数据对象执行的操作和方法
类的基本思想是数据抽象和封装。它类似于基本类型,是用户定义并完成了上述功能。一个规范的类由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数的方式描述公有接口
- 类方法定义:描述如何实现类成员函数
1.1 类的声明
如上面所说,类的声明是完成了类的数据成员和成员函数的定义。数据成员不必过多解释,我们需要注意的是两个概念:
- 常数据成员
- 静态数据成员
常数据成员是指在类中定义的不能修改其值的一些数据成员,类似于我们以前学过的常变量,虽然是变量,也有自己的地址,但是一经赋初值,便不能再被修改。他有两个要点:
- 类中的常数据成员只能通过构造函数的初始化参数列表进行初始化。
- 常数据成员是实例化对象的一部分,可以用this指针访问。
静态数据成员存储空间不同于普通的数据成员,它不属于类的任何一个对象,是独立于对象存储的,因此:
- 不可以通过对象的this指针来访问。
- 静态数据成员不可以用参数初始化表进行初始化操作。
另一方面,类的成员函数就比较复杂了,除了一般的成员函数,有一些特殊的成员函数需要注意:
- 构造函数
构造函数的任务是初始化类对象的数据成员。 - 析构函数
与构造函数对应,析构函数负责类对象的销毁。尤其需要注意的是:对于构造函数中使用new
创建的类型在析构函数中必须使用delete
来销毁。 - 自动成员函数
编译器会在需要的时候自动为类生成6个默认成员函数,它们分别是:- 默认构造函数
- 默认析构函数
- 默认复制构造函数
其中需要注意的是默认复制构造函数,C++提供的默认复制构造函数工作的方法是:完成一个成员一个成员的复制,如果成员是类对象,则调用其复制构造函数或者默认复制构造函数。
在默认复制构造函数中,复制的策略是逐个成员依次复制,但是,一个类可能会拥有资源,如果复制构造函数简单地制作了一个该资源的复制,而不对它本身分配,就得面临一个麻烦的局面:两个对象都拥有同一个资源。当对象析构时,该资源将经历两次资源返还。
因此,对于在构造函数中进行了资源申请(new
申请堆空间、打开文件、占用硬件)的类,必须显式定义复制构造函数。 - 赋值运算符
- 地址运算符(一对)
- 静态成员函数
静态成员函数的使用是:静态成员函数只能访问静态成员 - 友元函数
友元函数在类中声明——在函数前加friend
,但是友元函数并不是成员函数。
友元函数可以访问类成员。 - 运算符重载
- 自动类型转换
当构造函数只接受一个参数时,则该类可以与该参数类型相同的值转换。 - 类型转换函数
C++提供类型转换函数来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据。
1.2 类定义
类的定义在这里不展开介绍,简单理解概念:
- 类的定义是类成员函数实现
- 应使用私有函数来处理不属于公有接口的实现细节
- 定义位于类声明中的函数自动成为内联函数
- 同一个类的所有对象共享同一组类方法
二、访问控制和封装
我们在类中使用访问说明符来加强类的封装型:
private
定义在private
之后的成员可以呗类成员访问,但不能被使用该类的代码访问。public
定义在public
之后的成员在整个程序空间内可以被访问。protected
定义在protected
之后的是受保护变量,类内和子类可直接访问,也就是说,基类中有protected
成员,子类继承于基类,那么也可以访问基类的protected
成员,要是基类是private
成员,则对于子类也是隐藏的,不可访问
三、继承
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。使用上面的数据抽象,我们可以将类的接口和实现分离。而使用继承,我们可以定义相似的类型,并对其相似部分建模,通过继承联系在一起的类构成了一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,称之为派生类。
- 派生类继承了基类的实现
- 派生类继承了基类的接口
- 派生类需要自己的构造函数
- 派生类可以根据需要添加额外的数据成员和成员函数
- 派生类不可以直接访问基类的私有成员
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象
- 基类引用可以在不进行显式类型转换的情况下引用派生类对象
C++有三种继承关系:公有继承、保护继承和私有继承。
- 公有继承:公有继承建立一种
is-a
关系,即派生类也是一个基类的对象,可以对基类对象执行的任何操作,都可以对派生类对象执行。 - 保护继承:
- 私有继承:使用私有继承,基类的公有方法将成为派生类的私有方法。在派生类中使用类名和作用域运算符来调用基类方法。
保护继承、私有继承和包含、组合都用于实现 has-a
关系,即新的类将包含另外一个类的对象。
3.1 基类的定义
C++中,基类将类型相关的函数和派生类不加改变直接继承的函数区别对待,对于需要派生类各自定义适合自己的版本的函数,基类将这些函数声明为虚函数:
任何构造函数之外的非静态函数都可以是虚函数,如果一个函数在基类中定义为虚函数,则在派生类中该函数隐式的也是虚函数。
基类中定义为protect
的成员,可以被派生类直接访问,而不能被外部成员访问。
C++可以通过使用纯虚函数来提供未实现的函数:
当类声明中包含有纯虚函数的时候,不能创建该类的对象。包含纯虚函数的类只能用作基类。
当我们需要一种必须实施的接口,我们可以定义ABC(abstract base class)———抽象基类,ABC要求具体派生类覆盖其纯虚函数,迫使派生类遵循ABC设置的接口规则。
3.2 派生类的定义
派生类使用类派生列表明确自己是从哪个(哪些)基类继承而来的:
因为派生类不能直接访问基类的私有成员,所以派生类的构造函数必须使用基类的构造函数,在定义派生类的构造函数时:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
3.3 私有继承与保护继承
进行私有继承时,使用关键字 private
而不是 public
来声明基类。派生类会包含两个无名称的对象成员。派生类对基类的使用是:
- 构造函数中,使用基类类名来初始化相应的成员;
- 派生类的方法中使用基类名和作用域解析符来调用基类方法;
- 派生类中使用强制类型转换来访问基类对象
* 私有继承可以访问保护成员 - 私有继承可以重定义虚函数(非公开)
- 使用using可以定义继承的方法为公开方法
保护继承与私有继承类似,区别是保护继承中,基类的公有办法将成为派生类的保护方法。这样,保护继承继承的方法可以在后续的派生中使用
3.4 多重继承
MI——多重继承,描述的是有多个直接基类的类。
- 为了让派生类从多个基类相同的类继承时只继承一个基类对象,C++定义了虚基类:在类声明时使用
virtual
关键字。 - 使用虚基类的时候,编译器会隐式调用虚基类的默认构造函数,或显式的调用虚基类的相应构造函数。
- 使用虚基类时,需要使用作用域运算符标明使用的方法的类型。