转载请注明出处:http://blog.youkuaiyun.com/enyusmile/article/details/46611925
本章内容包括:
- 过程性编程和面向对象编程
- 类概念
- 如何定义和实现类
- 公有类访问和私有类访问
- 类的数据成员
- 类方法(类函数成员)
- 创建和使用类对象
- 类的构造函数和析构函数
- const成员函数
- this指针
- 创建对象数组
- 类作用域
- 抽象数据类型
10.1 过程性编程和面向对象编程
- 采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据(并不需要程序一直运行,用户可能希望能够将数据存储在一个文件中,然后从这个文件中读取数据).
- 采用OOP方法时,首先从用户的角度考虑对象—描述对象所需的数据以及描述用户与数据交互所需的操作.完成对接口的描述后,需要确定如何实现接口和数据存储.最后,使用新的设计方案创建出程序.
10.2 抽象和类
- 抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计.
10.2.1 类型是什么
10.2.2 C++中的类
- 一般来说,类规范由两个部分组成.
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口.
- 类方法定义:描述如何实现类成员函数.
- 简单的说,类声明提供了类的蓝图,而方法定义则提供了细节.
- 什么是接口:接口是一个共享框架,供两个系统交互时使用;对于类,我们说公共接口.在这里,公众public是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成.如果希望更人性化,不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户.然而,要使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口.
- 通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中.
- 程序清单10.1 stock00.h
- 1.访问控制
- 类设计尽可能将公有接口与实现细节分开.公有接口表示设计的抽象组件.将实现细节放在一起并将他们与抽象分开被称为封装.
- OOP和C++: C++中包括了许多专门用来实现OOP方法的特性,因此它使程序员更进一步.首先,将数据表示和函数原型放在一个类声明中(而不是放在一个文件中),通过将所有内容放在一个类声明中,来使描述称为一个整体.其次,让数据表示称为私有,使得数据只能被授权的函数访问.
- 2.控制对成员的访问:公有还是私有
- 通常,程序员使用私有成员函数来处理不属于公有接口的实现细节.
- 不必在类声明中使用关键字private,因为这是类对象的默认访问控制.
- 类和结构:类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明.实际上,C++对结构进行了扩展,使之具有与类相同的特性.他们之间唯一的区别是,结构的默认访问类型是public,而类为private.C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象(常被称为普通老式数据(POD,Plain Old Data)结构).
10.2.3 实现类成员函数
- 两个特殊的特征
- 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private组件.
- 程序清单10.2 stock00.cpp
- 1.成员函数说明
- 2.内联方法
- 其定义位于类声明中的函数都将自动称为内联函数.类声明常将短小的成员函数作为内联函数.
- 如果愿意,也可以在类声明之外定义成员函数,并使其称为内联函数.为此,只需在类实现部分中定义函数时使用inline限定符即可.
- 将内联定义放在定义类的头文件中(有些开发系统包含只能链接程序,允许将内联定义放在一个独立的实现文件).根据盖写规则(rewrite rule),在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数.
- 3.方法使用哪个对象
- 注意:调用成员函数时,它将使用被用来调用它的对象的数据成员.
- 所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本.
10.2.4 使用类
- 程序清单10.3 usestock0.cpp
- 客户/服务器模型: OOP程序员常依照客户/服务器模型来讨论程序设计.在这个概念中,客户是使用类的程序.类声明(包括类方法)构成了服务器,它是程序可以使用的资源.客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口.服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行.服务器设计人员只能修改类设计的实现细节,而不能修改接口.这样程序员独立地对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外的影响.
10.2.5 修改实现
- 将修改限定在实现文件汇总,以免影响程序的其他方面.
10.2.6 小结
- 典型的类声明的格式如下:
class className
{
private:
data member declarations
public:
member function prototypes
};
10.3 类的构造函数和析构函数
10.3.1 声明和定义构造函数
- 注意,没有返回类型.原型位于类声明的公有部分.
- 成员名和参数名:参数名不能与类成员相同,为避免类似:shares = shares;这种混乱,一种常见的做法是在数据成员名中使用m_前缀;另一种常见的做法是,在成员名中使用后缀_;
10.3.2 使用构造函数
- C++提供了两种使用构造函数来初始化对象的方式.
- 第一种方式是显示地调用构造函数
- 另一种方式是隐式地调用构造函数
- 在构造函数构造出对象之前,对象是不存在的.因此构造函数被用来创建对象,而不能通过对象来调用.
10.3.3 默认构造函数
- 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数.为类定义了构造函数后,程序员就必须为它提供默认构造函数.这样做的原因可能是想禁止创建未初始化的对象.然而,如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数.
- 定义默认构造函数的方式两种
- 一种是给已有构造函数的所有参数提供默认值.
- 另一种方式是通过函数重载来定义另一个构造函数—一个没有参数的构造函数
- 提示:在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数.
- 隐式地调用默认构造函数时,不要使用圆括号.
10.3.4 析构函数
- 和构造函数一样,析构函数的名称也很特殊:在类名前加上~.和构造函数一样,析构函数也可以没有返回值和声明类型.与构造函数不同的是,析构函数没有参数.
- 什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显示地调用析构函数(也有例外情形的,参阅第12章的”再谈定位new运算符”).如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用.
- 如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义.
10.3.5 改进Stock类
- 头文件
- 程序清单10.4 stock10.h
- 实现文件
- 程序清单10.5 stock10.cpp
- 客户文件
- usestock2.cpp
- 提示:在main()的开头和末尾多了一个大括号.诸如stock1和stock2等自动变量将在程序推出其定义所属代码块时消失.如果没有这些大括号,代码块将为整个main(),因此仅当main()执行完毕后,才会调用析构函数.在窗口环境中,这意味着将在两个析构函数调用前关闭,导致您无法看到最后两条消息.但添加这些大括号,最后两个析构函数调用将在到达返回语句前执行,从而显示相应的消息.
- 程序说明
- 注意:在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中.
- 提示:如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式.通常这种方式的效率更高.
- C++11列表初始化
- 在C++11中,可将列表初始化语法用于类么?可以,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起.
- 另外,C++11还提供了名为std::initialize_list的类,可将其用作函数参数或方法参数的类型.这个类可表示任意长度的列表,只要所有列表项的类型都相同或可转换为相同的类型.
- const成员函数
- C++的解决方法是将const关键字放在函数的括号后面,函数定义也是如此操作.
10.3.6 构造函数和析构函数小结
- 警告:接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值:Classname objcet = value;这种特性可能导致问题,11章将介绍,可关闭这项特性.
10.4 this指针
- 使用被称为this的特殊指针.this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法).
- 注意:每个成员函数(包括构造函数和析构函数)都有一个this指针.this指针指向调用对象.如果方法需要引用整个调用对象,则可以使用表达式this.在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值.然而,要返回的并不是this,因为this是对象的地址,而是对象本身,即*this(将解除引用运算符用于指针,将得到指针指向的值).现在可以将*this作为调用对象的别名来完成前面的方法定义.
- 程序清单10.7 stock20.h
- 程序清单10.8 stock20.cpp
10.5 对象数组
- 程序清单10.9 usestock2.cpp
10.6 类作用域
10.6.1 作用域为类的常量
- C++提供了另一种在类中定义常量的方式—使用关键字static
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
...
- 在C++98中,只能使用这种技术声明值为整型或枚举的静态常量,而不能存储double常量.C++11消除了这种限制.
10.6.2 作用域内枚举(C++11)
- 默认情况下,C++11作用域内枚举的底层类型为int.另外,还提供了一种语法,可用于做出不同的选择:
//underlying type for pizza is short
enum class : short pizza{Small, Medium, Large, XLarge};
- :short将底层类型制定为short.底层类型必须为整型.在C++11中,也可使用这种语法来制定常规枚举的底层类型,但如果没有制定,编译器选择的底层类型将随实现而异.
10.7 抽象数据类型(abstract data type,ADT)
- 私有部分必须表明数据存储的方式.
- 程序清单10.10 stack.h
- 程序清单10.11 stack.cpp
- 程序清单10.12 stacker.cpp
10.8 总结
- 面向对象编程强调的是程序如何表示数据.使用OOP方法解决变成问题的第一步是根据它与程序之间的接口来描述数据,从而制定如何使用数据.然后,设计一个类来实现该接口.一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径.类将数据和方法组合成一个单元,其私有性实现数据隐藏.
- 通常,将类声明分成两部分组成,这两部分通常保存在不同的文件中.类声明(包括由函数原型表示的方法)应放到头文件中.定义成员函数的源代码放在方法文件中.这样便将接口描述与实现细节分开了.从理论上说,只需指导公有接口就可以使用类.当然,可以查看实现方法(除非只提供了编译形式),但程序不应该依赖于其实现细节.只要程序和类只通过定义接口的方法进行通信,程序员就可以随意地对任何部分做独立的改进,而不必担心这样做会导致意外的不良影响.
- 如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它.如果方法需要显式地引用调用它的对象,则可以使用this指针.由于this指针被设置为调用对象的地址,因此*this是该对象的别名.
10.9 复习题
10.10 编程练习
附件:本章源代码下载地址