C++ Prime Plus 知识点整理 - 第十章 对象和类 、第十一章 使用类

  • OOP的特性:
  1. 抽象
  2. 封装和数据隐藏
  3. 多态
  4. 继承
  5. 代码的可重用性

1. 过程性编程和面向对象编程

  • 面向过程编程的方法,首先考虑的是要遵循的步骤,然后考虑如何表示这些数据
  • 面相对象编程的方法,首先考虑数据,还考虑如何表示这些数据

2. 抽象和类

  • 对于复杂的问题,可以采用简化和抽象的方法,将问题的本质抽象出来,并根据特征来描述解决方案;
  • 指定类型需要完成三项工作;内置类型的操作硬件内置到编译器里,而用户自定义类型需要解决这三个问题;
  1. 决定数据对象需要的内存数量;
  2. 决定如何解释内存中的位;
  3. 决定可使用数据对象执行的操作或方法;
  • :用户定义的类型的定义;类指明了数据将如何存储,如何访问和操纵这些数据;
  • 抽象:用类的方法的公有接口对类对象执行的操作
  • 数据隐藏
  • 封装

2.1 类声明

  • 类是一种将抽象转换为用户定义类型的C++工具,他将数据表示和操作数据的方法组合成一个简洁的包;

  • 类规范有两个部分组成:

  1. 类声明:以数据成员的方式描述数据部分,以成员函数(或称方法)的方式描述共有接口;
  2. 类方法定义:描述如何实现类成员函数;

接口
接口是一个共享框架,共两个系统交互时使用;
程序接口将您的意图转换为存储在计算机中的具体信息;
对类而言,指的是公共接口,公共是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类独享交互的代码,从而让程序能够使用类对象;
然而,要使用某各类,必须了解其公共接口,要编写类,必须创建其公共接口;

  • 通常,C++程序员将接口(类定义)放在头文件中,将实现(类方法的代码)放在源代码文件中;
  • 类定义:
class className
{
	private:
		data member declarations;
	public:
		memeber function prototypes;
};

2.2 访问控制

  • 关键字privatepublic描述了对类成员的访问控制;使用类对象的程序都可以访问public的部分,但只能通过public函数(或友元函数)访问对象的private部分;公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口;防止程序直接访问数据被称为数据隐藏;还有第三种访问控制,关键字protected,用于类继承;
  • 类设计尽量将共有接口和实现细节分开;公有接口表示设计的抽象组件;将实现细节放在一起并将它们与抽象分开称为封装;数据隐藏是一种封装,将实现的细节放在私有部分中也是一种封装,函数类定义和类声明分开放也是一种封装;
  • 数据隐藏不仅可以防止直接访问数据,还可以让开发者无需了解数据时如何被表示的,只需要知道类方法能够做什么,而不需要知道类方法之间的区别;这个方法也便于以后的修改,维护代码而无需修改程序的接口;
  • 通常数据项放在私有部分,组成类接口的成员函数放在公有部分,私有成员函数用于处理不属于公有接口的实现细节类默认的访问控制为private
  • 实际上,C++对结构进行了扩展,使之与类有相同的结构,不过结构默认的访问控制为public,一般用于纯粹的数据对象结构;

2.3 类成员函数定义

  • 实现类成员函数,类成员函数有两个特征:
  1. 定义类成员函数时,应该使用作用域解析运算符::来标识函数所属的类:作用域运算符确定了方法定义对应的类的身份;ClassName::FunctionName()是函数的限定名(qualified name);而Function()是函数的全名的缩写(非限定名 qualified name);
  2. 类方法可以访问类的private组件
  • 一般类声明中的短小的函数自动成为内联函数,也可以再类外声明,并使其成为内联函数,要使用inline限定符;内联函数的特殊规则要求每个使用它们的文件中都对其进行定义,确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是在类头文件中定义;根据改写规则,类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数;
  • 多创建的每个对象都有自己的存储空间,用于存储内部变量和类成员,但同类的所有对象共用一组类方法;

C++ Prime Plus 知识点整理 - 第十章 对象和类 、第十一章 使用类_第1张图片

  • C++的目标是使得使用类与使用基本内置类型尽可能相同,要创建类,可以声明类,也可以使用new运算符;

客户/服务器模型
客户是使用类的程序,类声明构成了服务器,它是程序可使用的资源;
客户只能通过公有方法定义的接口使用服务器,这意味着客户唯一的责任是了解该接口;
服务器的责任是确保服务器根据该接口可靠并准确的执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口;
独立的对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外影响;

  • 类成员函数(方法)可以通过类对象来调用,需要使用据点运算符.

3. 类的构造函数和析构函数

  • 析构函数和构造函数是类的标准函数

构造函数

  • 为了使类对象和常规对象一样,但私有数据不可直接访问,不能违背数据隐藏规则,为了要实习创建对象时初始化,C++因此定义了构造函数;
  • 构造函数没有返回值,但也没被声明为void,构造函数实际上没有声明类型;程序定义对象时,将自动调用构造函数也可以显式调用构造函数;构造函数的参数是要赋值给类成员的,并不能与类成员重名;无法使用对象调用构造函数;
  • C++提供了两种构造函数来初始化对象的方式,如#1 #2所示;法#1将创建一个临时对象,然后将对象赋值给定义的类对象;应该尽量使用法#2,这种方法效率高;C++11提供了列表初始化,如#3所示,使用大括号扩起;C++11还提供了std::initialize_list的类;
  1. 显式地调用构造函数
  2. 隐式地调用构造函数
// #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将不在其作用,因为类声明时不会完成常量的定义,即对象创建之前不给分配内存;可采用两种方法:
  1. 类中声明一个枚举:类中声明的枚举作用域为整个类,可以用枚举为整型常量提供作用域为类的符号名称;这种方法并不会创建类成员,因此对象中都不包含该枚举;由于只是创建符号常量,因此不需要提供枚举名;
  2. 使用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)
  • 重载运算符的限制:
  1. 重载后的运算符必须至少有一个操作数是用户自定义的类型,这防止用户为标准类型重载运算符;
  2. 使用运算符时不能违背运算符原来的句法规则;如原来运算符的用途、运算符的优先级等,如果要定义用途不一样的操作,应该直接定义类方法,而不是重载运算符;
  3. 不能创建新的运算符
  4. 不能重载下面的运算符

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)

你可能感兴趣的:(c++)