C++ Prime Plus 知识点整理 - 第十章 对象和类 、第十一章 使用类
- OOP的特性:
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码的可重用性
1. 过程性编程和面向对象编程
- 面向过程编程的方法,首先考虑的是要遵循的步骤,然后考虑如何表示这些数据
- 面相对象编程的方法,首先考虑数据,还考虑如何表示这些数据
2. 抽象和类
- 对于
复杂的问题
,可以采用简化和抽象
的方法,将问题的本质抽象
出来,并根据特征来描述
解决方案; - 指定类型需要完成三项工作;内置类型的操作硬件内置到编译器里,而用户自定义类型需要解决这三个问题;
- 决定数据对象需要的内存数量;
- 决定如何解释内存中的位;
- 决定可使用数据对象执行的操作或方法;
- 类:用户定义的类型的定义;类指明了数据将如何存储,如何访问和操纵这些数据;
- 抽象:用类的方法的公有接口对类对象执行的操作
- 数据隐藏:
- 封装:
2.1 类声明
-
类是一种将抽象转换为用户定义类型的C++工具,他将数据表示和操作数据的方法组合成一个简洁的包;
-
类规范有两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(或称方法)的方式描述共有接口;
- 类方法定义:描述如何实现类成员函数;
接口
接口是一个共享框架,共两个系统交互时使用;
程序接口将您的意图转换为存储在计算机中的具体信息;
对类而言,指的是公共接口,公共是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类独享交互的代码,从而让程序能够使用类对象;
然而,要使用某各类,必须了解其公共接口,要编写类,必须创建其公共接口;
- 通常,C++程序员将接口(类定义)放在头文件中,将实现(类方法的代码)放在源代码文件中;
- 类定义:
class className
{
private:
data member declarations;
public:
memeber function prototypes;
};
2.2 访问控制
- 关键字
private
和public
描述了对类成员的访问控制;使用类对象的程序都可以访问public的部分
,但只能通过public函数(或友元函数)访问对象的private部分
;公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口;防止程序直接访问数据
被称为数据隐藏
;还有第三种访问控制,关键字protected,用于类继承; - 类设计尽量将共有接口和实现细节分开;公有接口表示设计的抽象组件;
将实现细节放在一起并将它们与抽象分开
称为封装
;数据隐藏是一种封装,将实现的细节放在私有部分中也是一种封装,函数类定义和类声明分开放也是一种封装; - 数据隐藏不仅可以防止直接访问数据,还可以让开发者无需了解数据时如何被表示的,只需要知道类方法能够做什么,而不需要知道类方法之间的区别;这个方法也便于以后的修改,维护代码而无需修改程序的接口;
-
通常数据项放在私有部分,组成类接口的成员函数放在公有部分,私有成员函数用于处理不属于公有接口的实现细节
;类默认的访问控制为private
; - 实际上,C++对结构进行了扩展,使之与类有相同的结构,不过结构默认的访问控制为public,一般用于纯粹的数据对象结构;
2.3 类成员函数定义
- 实现类成员函数,类成员函数有两个特征:
-
定义类成员函数时,应该使用作用域解析运算符::来标识函数所属的类
:作用域运算符确定了方法定义对应的类的身份;ClassName::FunctionName()是函数的限定名
(qualified name);而Function()是函数的全名的缩写(非限定名
qualified name); -
类方法可以访问类的private组件
;
- 一般类声明中的短小的函数自动成为内联函数,也可以再类外声明,并使其成为内联函数,要使用inline限定符;内联函数的特殊规则要求每个使用它们的文件中都对其进行定义,确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是在类头文件中定义;根据改写规则,类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数;
- 多创建的每个对象都有自己的存储空间,用于存储内部变量和类成员,但同类的所有对象共用一组类方法;
- C++的目标是使得使用类与使用基本内置类型尽可能相同,要创建类,可以声明类,也可以使用new运算符;
客户/服务器模型
客户是使用类的程序,类声明构成了服务器,它是程序可使用的资源;
客户只能通过公有方法定义的接口使用服务器,这意味着客户唯一的责任是了解该接口;
服务器的责任是确保服务器根据该接口可靠并准确的执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口;
独立的对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外影响;
- 类成员函数(方法)可以通过类对象来调用,需要使用
据点运算符.
;
3. 类的构造函数和析构函数
-
析构函数和构造函数是类的标准函数
;
构造函数
- 为了使类对象和常规对象一样,但私有数据不可直接访问,不能违背数据隐藏规则,为了要实习创建对象时初始化,C++因此定义了构造函数;
-
构造函数没有返回值,但也没被声明为void,构造函数实际上没有声明类型
;程序定义对象时
,将自动调用构造函数
,也可以显式调用构造函数
;构造函数的参数是要赋值给类成员的,并不能与类成员重名;无法使用对象调用构造函数; - C++提供了两种构造函数来初始化对象的方式,如#1 #2所示;法#1将创建一个临时对象,然后将对象赋值给定义的类对象;应该尽量使用法#2,这种方法效率高;C++11提供了列表初始化,如#3所示,使用大括号扩起;C++11还提供了std::initialize_list的类;
- 显式地调用构造函数
- 隐式地调用构造函数
// #1
Stock fruit = Stock("apple", 100, 3);
// #2
Stock food("tomato", 300, 8);
// #3
Stock vegetable = {"tomato", 300, 8};
Stock test {"123",3,4};
Stock test2 {};
// #
-
默认情况下,对象赋值给另一个对象,C++将对象的每个数据成员的内容赋值到目标对象中;
-
默认构造函数为未提供构造函数是自动创建的
;默认构造函数没有任何参数,声明中不包含任何值,不做任何工作
;如果定义了带参数的构造函数,则定义不初始化的对象将出错(如 Stock test;),所以如果要这样应该定义一个不带参数的重载的构造函数
或者给所有参数提供默认值
,但不要同时采用两种方法;用户定义的
默认构造函数通常会给所有成员提供隐式初始化值
;
Stock first();// 错误,调用默认构造函数不能带括号,否则为定义了一个函数
Stock second("app", 123, 3);
析构函数
- 对象过期时,将调用一个特殊的成员函数,来完成清理工作,这个函数就是析构函数;
- 析构函数是
无返回值的声明类型
,且类名前加上~
,但析构函数不能有参数,因此析构函数不能有重载; - 至于什么时候调用析构函数,由编译器决定,通常不显式调用析构函数;对于静态存储类对象,其析构函数将在程序结束时自动被调用;对于自动存储类对象,其析构函数将在程序执行完成时被调用;对于new创建的类对象,调用delete时,自动调用析构函数;
其他
- 如下#1的代码将出错,因为C++无法保证调用的函数不改变对象的数据的值,解决方法是
在函数后加上const关键字
;不改变类成员变量的成员函数尽量使用const限定符;
// #1
const Stock food = {"test",2,3};
food.show();
4. this指针
- 如果类方法涉及两个对象,则应该使用this指针;
-
this指针指向用来调用函数的对象,即值为调用它的对象的地址
,*this表示此对象本身
;
5. 对象数组
- 定义对象数组与定义标准类型数组相同;使用构造函数来初始化数组,如果有多个构造函数,初始化时可以使用不同的构造函数;
- 实现原理,先使用默认构造函数创建数组,然后根据花括号的值定义临时类对象,将临时对象的值复制给相应元素,因此定义对象数组
必须有默认构造函数
;
6. 类作用域
- 类中定义的名称作用域为整个类,因此类中名称旨在该类中可见,外部不可见,同时也意味着不可以直接访问类成员,必须通过对象,定义类成员函数时也必须使用作用域运算符;
- 要在类中定义常量,只加const将不在其作用,因为类声明时不会完成常量的定义,即对象创建之前不给分配内存;可采用两种方法:
- 类中声明一个枚举:类中声明的枚举作用域为整个类,可以用枚举为整型常量提供作用域为类的符号名称;这种方法并不会创建类成员,因此对象中都不包含该枚举;由于只是创建符号常量,因此不需要提供枚举名;
- 使用static关键字:该常量与其他静态变量存在一起,即编译时即创建,而不是存储在对象中;
class Stock
{
private:
static const int one = 1;
enum {two = 2, three, four};
};
- 作用域内枚举(C++11新增)解决了包含相同名称的枚举的冲突,
枚举名前加上class或者struct关键字
;常规枚举自动转换为整型,可赋值给整型变量或表达式,作用域内枚举不能隐式地转换为整型,但必须要是可进行显式转换;常规枚举用某种底层类型表示,长度随系统而异,二作用域内枚举底层类型为int,可显示指定类型,如#2所示,底层类型必须为整型;
// #1
enum test1 {abc, def};
enum class test2 {abc, def};
int a = abc; // 正确
int b = test2::abc; // 错误
int c = int(test2::abc);// 正确
// #2
enum class : short pizza{sma, med, lar, xlar};
// #
7. 抽象数据类型
- 抽象数据类型(ADT),类概念就非常适合ADT方法;
第十一章 使用类
1. 运算符重载
- 运算符重载也属于C++多态;C++根据操作数的数目和类型来决定采用哪种操作,允许扩展到用户定义的类型;重载运算符可以使自定义类型实现和基本类型一样的操纵,隐藏了内部机理,强调了实质,这是OOP的另一个目标;
- 运算符重载格式如下;其中operator是关键字,op表示运算符,arguement-list表示参数列表;
// 基本格式
operatorop(arguement-list)
- 运算符重载原理,实质就是前面的元素调用公有方法并把后面的元素当做实参传递,如#1所示;如#2所示,可以将两个以上的对象相加
//#1
t = t1.operator+(t2); // 函数表示法
t = t1 + t2; // 运算符表示法
//#2
t = t1 + t2 + t3; // 等同于t1.operator+(t2.operator+t3)
- 重载运算符的限制:
- 重载后的运算符必须至少有一个操作数是用户自定义的类型,这防止用户为标准类型重载运算符;
- 使用运算符时不能违背运算符原来的句法规则;如原来运算符的用途、运算符的优先级等,如果要定义用途不一样的操作,应该直接定义类方法,而不是重载运算符;
- 不能创建新的运算符
- 不能重载下面的运算符
sizeof
.
.*
::
?:
typeid
const_cast
dynamic_cast
reinterpret_cast
static_cast
- 大多数运算符既可以是成员函数,也可是非成员函数,但是如下运算符只能是成员函数;
=
()
[]
->
- 可重载的运算符如下:
2. 友元
- 除了公有类方法外,友元可以访问类的私有部分;友元分为三种
1. 友元函数 2. 友元类 3.友元成员函数
; - 友元函数常用于运算符重载,由前面的运算符重载可知,运算符前的元素一定是调用对象,但如果运算符前的不是调用对象,则会出错,如下#1所示,解决方法就是使用非成员函数,但非成员函数不可以使用类的私有对象,然而友元函数可以做到;
//#1
//对象a和b使用类ABC
//运算符重载定义:ABC operator*(const double a) const;
a = b * 2.75; // 解释为 a = b.operator+(2.75)
a = 2.75 * b; // 出错
//#2
ABC operator*(const double a, const ABC &b) const;
- 创建友元函数,
需要在类内声明,并加上关键字friend,定义时不能使用friend关键字
;友元函数不属于类作用域,因此定义时不能用作用域运算符,即类的友元函数是非成员函数,但是它与类成员函数访问权限相同;当然也可以定义为非友元函数,如#2所示,不过定义为友元可以方便以后扩展,如添加对私有数据访问的代码;
//#1
//声明
friend ABC operator*(const double a, ABC & b);
//定义
ABC operator*(const double a, ABC & b)
{
//...
}
//#1
ABC operator*(const double a, ABC & b)
{
return b*a; // 仅仅交换次序
}
- 友元是否有悖于OOP?否定的,友元函数可以看做类接口的扩展,因为友元只有类内声明,因此类依然控制了友元函数的使用,只是类方法和友元只是表达类接口的两种不同机制;
- 常用友元,重载<<运算符,如果使用非友元函数,会出现#1的问题,因为运算符前面的为调用对象,因此使用友元更好,如#2;调用cout应该使用它本身,所以使用引用;如果要如#3一样连续输出,返回值应该是cout引用
//#1
abc << cout;
//#2
cout << abc;
//#3
cout << abc << def << ghi;
3. 重载运算符:成员OR非成员
- 如果重载操作符的两个参数类型一样,则只能定义成员函数重载或者友元函数重载中的1种,不可同时定义两种格式,会导致二义性错误;某些运算只能使用成员函数,而另一些使用非成员函数更好,如类定义的类型转换;
-
- 如果返回值为一个新的类,则应该考虑使用构造函数返回,如下所示;
//
Vector Vector::operator+(const Vecotr &a) const
{
return Vector(x + b.x, y + b.y);
}
4. 扩展—随机数
- 随机函数库,需要使用头文件cstdlib,其中包含了rand()函数和srand()函数,头文件ctime包含了time()函数,rand()函数获得随机值,srand()函数使用种子值,来生成一个随机数序列,time(0)返回当前时间,即从某时刻开始的秒数;C++11头文件radom提供了更强大的随机数支持;
5. 类的自动转换和强制类型转换
- C++不自动转换不兼容的类型,但可以将类定义为与基本类型或另一个类相关,从而使类型转换变得有意义,这是C++将可进行自动转换,也可以使用强制转换;
5.1 当前类型 = 其他类型
- 只接受一个参数的构造函数可以将参数直接转换为类,这成为隐式转换,因为它是自动进行的,如#1;如果提供了多个参数,且除了第一个其他都有默认值,则可以用于类型转换,如#2;然后有时候自动转换将造成麻烦,因此可用explicit关键字,关闭这种自动特性,如#3,但是可以使用强制转换;用户定义的自动类型转换可用于如下情况;1. 初始化;2. 赋值;3. 传参数;4. 返回值;5. 上述任一情况下,使用可转换为那个类型的内置类型时且仅当不产生二义性时,如#4,;
//#1
Stonewt(double lbs);
Stonewt tmp = 1.23;
//#2
Stonewt(int stn, double lbs = 0);
Stonewt tmp2 = 3;
//#3
explicit Stonewt(double lbs);
Stonewt tmp3 = Stonewt(1.23);
//#4
Stonewt tmp4 = 1000; // int to double
5.2 其他类型 = 当前类型
- 要实现这种转换,不能使用构造函数,而是用特殊的C++运算符函数:转换函数;转换函数是用户定义的强制类型转换,可以像使用强制类型转换一样使用它们;转换函数必须是类方法,且不能有参数,也不能指定返回类型,其实定义的类型就是转换后的类型,如下#1;
//#1
operator int() const; // 返回int型
- 如果类只定义了一个类型转换,则会自动转换,如果定义了多个类型转换,只能使用强制类型转换,否则会报二义性错误;可以使用explicit避免隐式转换,或者使用功能相同的普通类方法;
- 转换函数和友元函数,有些情况下,同一个操作都可以和转换函数和友元函数匹配,如#1;因此如果有这个需要可以使用两种办法:1. 友元函数,如#2,2. 运算符重载为显式使用转换类型参数的函数,如#3,第一种方法编码少,但开销大,第二种方法相反;
//#1
Stonewt::Stonewt(double a) // 构造函数
{}
Stonewt Stonewt::operator+(const Stonewt &st) const // 成员函数
{}
Stonewt operator+(const Stonewt &st1, const Stonewt &st2) // 友元函数
{}
Stonewt a(9, 12);
double b = 3.12;
c = a + b; // 成员函数和友元函数都匹配
c = b + a; // 只有友元函数才匹配
//#2
friend Stonewt operator+(const Stonewt &st1, const Stonewt &st2)
//#3
Stonewt operator+(double x);
friend Stonewt operator+(double a, const Stonewt &st)