开学了,有C++面向对象编程课程,然觉学校教材并不足合胃口,遂循🚀意见,阅此经典。略记些许笔记,加深印象(记性不好)只记我没学过的
预备知识
前置知识,问题不多,详细请自己阅读
关于过程性编程和面向对象编程:
采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑 如何表示这些数据。 如果换成一位OOP程序员,又将如何呢?首先考虑数据——不仅要 考虑如何表示数据,还要考虑如何使用数据。 用户与数据交互的方式有三种:初始化、更新和报告——这就是用户接口。
总之,采用OOP方法时,首先从用户的角度考虑对象——描述对象 所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述 后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
开始学习C++
类介绍
类时用户定义的数据类型,要定义类,需要描述如何表示信息和可对数据执行哪些操作
类之于对象就像
类型之于变量
类描述一种数据类型的全部属性
对象是类的实例化
之后我们会更详细的讲解
处理数据
内置的c++类型对象有基本类型和复合类型,当然程序还需要一种表示存储数据的方法
简单变量
程序要记录三个基本属性
- 信息将存储在哪里
- 要存储什么值
- 存储何种类型的信息
|
|
这些语句告诉程序,它正在存储整数,程序也将找到一块内存用于存储,并将单元标记为counts。
实际上,我们可以用&
来检索其在内存中的位置
变量和变量名
命名方式类似于c,注意一下
- 只能字母字符、数字和下划线
- 名称第一个字符不是数字
- 区分大小写
- 无C++关键字
- 以两个下划线或下划线和大写字母打头的名称被保留用于实现,以一个下划线开头的名称将被用作全局辨识符
- 名称长度无限制
整型
short/int/long
计算机内存由bit单元组成,c++确保最小长度
- short至少16位
- int至少与short一样长
- long至少32位,且至少与int一样长
位与字节
位(bit)可看作电子开关的,0表示关,1表示开,8位内存块便有256种组合了,每增加一位可以增加一倍组合数
字节(byte)通常是八位内存单元,字节指的是描述计算机内存量的度量单位
我们也可以在头文件climits导入后查看关于整型限制的信息,具体地说它表示的各种限制的符号名称。
例如INT_MAX
为int最大取值,CHAR_BIT
为字节位数;
用size_of
操作符我们就可以得到具体的一种类型名的大小
无符号
前面的三种类型都可以通过无符号扩大范围,但前提是无负数要求的情况下
数字类型控制符
dec十进制
hex十六进制
oct八进制
输出是跟在变量后面
例如cout<<"chest = " << " (decimal)" <<endl;
char类型:字符和小证书
char类型专门为存储字符而设计,但是在cin和cout时发生过改变,cin时输入的字符在计算机内转化为数字,cout时则又转化为字符
成员函数cout.put()
cout.put()是同一个重要c++OOP概念——成员函数的第一个例子。例如类ostream有一个put()成员函数,用来输出字符。必须用句点将对象名和函数名称连接起来。句点被称为成员操作符。
而cout.put()的意思就是,通过类对象cout来使用函数put(),提供了另一种显示字符的方法可以替代«操作符,而将字符常量’M’和’N’显示为数字
重要复合类型——指针和自由存储空间
声明和初始化
int* p_new
C++程序员多采用这种格式,强调声明一个int指针
我们可以在声明语句中初始化指针,在这种情况下,被初始化的是 指针,而不是它指向的值
|
|
指针和数字
指针不是整型,所以我们不能在声明指针,要给指针赋值,要将数字值作为地址来使用,通过强制类型转换将数字变为合适的地址类型
|
|
使用new来分配内存!💘
new
可以根据程序的需要分配内存大小,而我们程序员所需要做的,就是告诉new,需要为哪种数据类型分配内存;new会找到一个长度正确的内存块,并返回内存块地址
之后我们要学会释放指针的内存
|
|
使用new创建动态数组!'
|
|
new操作符返回第一个元素的地址,如果我们要使用,可以直接通过psome[0],如果我们用p3=p3+1,那么p3的零基地址就+1,之后再次使用psome[0]就是原来的psome[1]了
尽可能使用const
将指针参数声明为指向常量数据的指针有两条理由
- 可以避免由于无意见修改数据导致的编程错误(比如先声明
const float gm=1.6
,再声明float * pm =&g_m
是不可行的 - 使用const使得函数可以处理const和非const实参,否则将只能接受非const数据,如条件允许,则应将指针形参声明为指向const的指针
函数探幽
内联与宏
为提高程序运行速度所做的一项改进,由于编译的最终产品是可执行程序,而 内联函数提供了另一种选择,内联函数的汇编代码与其他代码内联
起来了。也就是,编译器将使用函数代码替换调用。
对于内联代码,程序无需跳到另一个位置执行代码又再调回来,但相对的占用内存变多了
而我们要使用这项特性,必须采取下列措施
- 函数声明前加上关键字inline
- 函数定义前加上关键字inline
|
|
程序输出,内联函数和常规函数一样,也通过值来传递参数,如果函数为表达式,那么也就传递表达式的值,这使得c++内联函数功能十分强大,内联函数常用在简单、行数少的函数上
宏
一个计算平方的宏
#define SQUARE(X)X*X
这并不是通过传递参数实现的,而是通过文本替换实现的,X
就是参数的符号标记
|
|
实际上上述范例只有第一个可以正常工作,可以通过使用括号来进行改进:
#define SQUARE(x) ((x)*(x))
引用变量
C++新增一种复合类型——引用变量。
例如,将twin作为element变量的引用,则可以交替使用twin和element来表示该变量。
那么,这种别名有何作用?——用作函数形参,通过引用变量用作参数
|
|
&不是地址操作符,而是类型标识符的一部分,上面允许rodents和rats互换——它们指向相同的值和内存单元
——注意!
可以通过初始化声明设置引用,但通过赋值设置是不行的
如
|
|
rodents初始化指向了rats。接下来pt改为指向bunnies并不能改变引用
在函数中传入引用参数的话,我们就可以真正意义上修改参数的值了
const和引用传参
由于上文所说函数对引用的操作实际上是作用在引用所引的对象上,通过使用引用形参,允许函数改变一个或多个实参的值
所以我们要使用引用避免拷贝,这时候我们又不想影响对象本身的值,所以我们只要使用 const ...
进行常量形参引用就可以了
- 比如我们编写一个函数比较两个字符串对象长度,考虑到可能很长,所以就应该尽量避免拷贝,使用引用形参最合适,一个实例代码如下
|
|
我们再看一个const常量在函数对象中的引用传递
|
|
总之,由于值传递是实参的拷贝,在内存分配和时间消耗上就会产生较大的影响,所以我们借此机会去减少函数调用过程中的时间消耗去提高系统效率
内存模型和名称空间
再谈const
C++(但不是在C语言)中,const
限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const
全局变量的链接性为内部的。也就是说,在C++看来,全局 const
定义就像使用了 static
说明符一样。
C++修改了常量类型的规则,让程序员更轻松。例如,假设将一组 常量放在头文件中,
那么,预处理器将头文件内容包含到源文件中后,所有源文件都包含类似下面的定义
|
|
如果全局const声明的链接性像常规变量那样是外部的。按单定义规则就出错了(但并没有)
由于外部定义的const
数据的链接性为内部的,因此可以在所有文件中使用相同的声明。
名称空间
名称可以是变量、函数、结构体、枚举、类以及类和结构体的成员。C++标准提供了名称空间工具,以便更好地控制名称的作用域。
传统派
C++关于全局变量和局部变量的规则定义了一种名称空间层次。每 个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的 名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部 变量发生冲突。
嘛就是花括号区域是定义区域,但是潜在作用域还是要根据定义来的新名称空间特性
c++中可以通过 定义一种新的声明区域来创建命名的名称空间,目的在于:
- .提供声明名称区域,名称空间之间不会因为相同名称发生冲突(如果要多个文件兼容就不会出问题了)
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性 为外部的
- using声明和using编译指令
没错就是你们最属性的using namespace std;
,using声明由被限定的名称和关建字(using)组成:
main()中一个上面这样的声明将fetch
添加到main()
的声明区域中,之后就可以用fetch 代替Jill::fetch了,我们就这样获得了一个fetch
名称。
using编译指令则是为了使所有的名称可用。由名称空间名和关建字using namespace组成。它使得名称空间中所有名称可用,而不用再添加作用域解析运算符(没错就是你们最熟悉的using namespace std;
- using编译指令和using声明对比
鉴定为声明肯定更安全,声明只导入指定名称,改名称与局部名称发生冲突,编译器发出指示。而编译指令则导入所有名称、包括可能不需要的名称。甚至在与局部名称冲突时,局部名称覆盖名称空间也不会让编译器发出警告——属于是过于开放了
- 其他特性
可以将名称空间声明嵌套 flame所指的就是element::fire::flame了
名称空间意义和原则
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量和静态全局变量
- 开发一个函数库/类库。放在一个名称空间中
- 导入名称还是首选作用域解析运算或using声明(量大&确定的情况下才使用using编译)
对象、继承和引用
使用引用参数的原因我们已经知道了
- 能够修改调用函数中的数据对象;
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。 所以综上我们可以总结:
数据对象 | 使用 | 原因 |
---|---|---|
内置数据,小型数据结构 | 值传递 | 内存小 |
数组 | 指针 | 唯一的选择,并且要声明为const |
较大的结构题 | cosnt指针/const引用 | 提升效率 |
类对象 | const引用 | c++新增这项特性的主要元音就是为了传递类对象参数 |
ostream
和ofstream
类突显了引用的一个有趣属性。ofstream
对象将可以使用ostream
类方法,文件输入/输出格式就与控制台输入/输出相同
而我们也将特性从一个类传递给另一个类的语言特性称为继承(将在后文提到)
简单来说:ofstream
是从ostream
派生的类,派生来当然可以继承基类的特性啦
继承的另一个特征是,基类引用可以指向派生类对象而不需要强制类型转换。结果就是可以定义一个接受基类引用作为参数的函数,调用该函数时,基类对象就可以作为参数传递了,后面我们会更详尽的学习这一方法
函数重载
函数多态是c++在c上新增的功能。我们为函数设置的默认参数让我们能够使用不同数目的函数调用同一个函数,而函数多态(函数重载)让我们能够使用多个同名的函数,它们使用不同参数列表完成相同的工作
书中举了这么个例子:
重载函数就像是有多种含义的动词。例如,Piggy小姐可以在棒球场为家乡球队助威(root),也可以在地里种植(root)菌类作物。根据上下文可以知道在每一种情况下,root的含义是什么。同样,C++使用上下文来确定
——函数重载的关键是函数的参数列表——也称为函数特征标 (function signature)
如果两个函数参数数目和类型相同,同时函数排列顺序相同,则特征标相同。变量名无关紧要,c++允许定义名称相同的函数,条件是它们特征标不同。如果参数数目/参数类型不同,特征标不同,我们看下面的print()函数
|
|
答案是这样会出现错误,只要一句cout<<cube(x)
,后面的x会跟两个原型都匹配,编译器就无法确定要使用哪个原型了,编译器在检查函数特征标时,把类型引用和类型本身视为同一个特征标,注意把 类型引用和类型本身
看作一个特征标
同时注意,特征标才可以队函数进行重载,而函数类型不可以
|
|
以上两种是互斥的
同时注意,匹配函数并不区分const
和非const
变量,看以下原型
|
|
函数重载虽然看似非常迷人,但是不要滥用,仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
例如使用两个重载函数代替面向字符串的left()函数
|
|
番外:名称修饰(请自行查阅,在246页第五版)
函数模板
c++编译器实现了c++的另一个新增特性——函数模板。函数模板是通用函数描述,也就是说,它们使用泛型来定义函数,泛型可用具体类型(int或double等)替换,再将类型作为参数传递给模板
我们举个例子吧,我们定义了一个交换两个int值的函数,如果我们现在想交换两个double值,一种方法是复制原来的代码,用double替换所有的int。如要交换两个char值,可再次使用同样的技术。手工修改很麻烦甚至还可能遗漏。
那么函数模板的功能就在这里展现出来了,它可以帮助我们自动完成这一过程,可以节省时间还更可靠
AnyType
。关建字template和typename是这里必需的。另外必须使用尖括号<>
,类型名可以自选(比如Any,但是注意遵守c++命名规则)模板不会创建函数,但是告诉编译器如何定义函数。需要交换
int
函数时,编译器会按模板模式创建这样的函数然后用int去代替AnyType
。同样,double也可以代替而最终的函数将不会包含模板,只会包含为了程序生成的实际函数,模板使这些函数的定义更为简单和可靠
显示具体化
由于C++允许将一个结构赋给另一个结构,因此即使T是一个
job
结构,上述代码也适用。然而,假设只想交换salary
和floor
成员,而不交 换name
成员,则需要使用不同的代码,但Swap()
的参数将保持不变 (两个job
结构的引用),因此无法使用模板重载来提供其他的代码。
但是我没看懂这块,简单来说一个函数应该是有非模板函数、模板函数和显式具体化模板函数以及重载版本。
如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。
编译器使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个 定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)
- 第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 第2步:使用候选函数列表创建可行函数列表。这些都是参数数目 正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应 的形参类型完全匹配的情况。例如,使用float参数的函数调用可以 将该参数转换为double,从而与double形参匹配,而模板可以为 float生成一个实例
- 第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该 函数调用出错。
通常有以下那么个顺序:
- 完全匹配,但常规函数优先于模板;
- 提升转换(例如,char和shorts自动转换为int,float自动转换为 double);
- 标准转换(例如,int转换为char,long转换为double);
- 用户定义的转换,如类声明中定义的转换。
如果有多个匹配的原型,那编译器将无法完成重载解析过程;如果没有最佳函数,那么编译器可能用
ambiguous(二义性)
这样的词语来反馈
本章总结
因为新东西多了所以总结一下
C++扩展了C语言的函数功能。通过将 inline 关键字用于函数定义, 并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用(相当于复制进去)。只有在函数很短时才能采用内联方式。
引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。
C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。
函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。
类与对象
主要内容概括
- 过程性编程和面向对象编程;
- 类概念;
- 如何定义和实现类;
- 公有类访问和私有类访问;
- 类的数据成员;
- 类方法(类成员函数);
- 创建和使用类对象;
- 类的构造函数和析构函数;
- const 成员函数;
- this 指针;
- 创建对象数组;
- 类作用域;
- 抽象数据类型。
正文
类规范通常由两个部分组成
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
- 类方法定义:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口
- 什么是接口
接口 是一个共享框架,是两个系统(如在计算机和打印机之间或者用户或者计算机程序之间)交互时去使用。 简单的举个例子吧
在使用字处理器时,我们不能直接将脑子里的想到的词传输到计算机内存中对吧,所以我们就需要有一个程序提供给我们的 交互接口 才可以在敲打键盘的时候将字符显示到屏幕上;移动鼠标也是,点击鼠标也是;
——程序接口将我们的意图转化到了存储在计算机中的具体信息
而对于类,我们说public
接口,就是指使用类的程序。接口能让程序员编写与类对象交互的代码,从而让程序能够使用类对象;
我们要使用string并计算其中有多少字符时,我们不用打开对象,只需使用string类提供的size()方法,我们不能直接访问类,但是方法就是我们和string类对象之间的公共接口的组成部分
我们来看一个代码,它是Stock类的类声明(注意我们将在下面经常使用到这个Stock类,所以对类尽量记熟悉)
|
|
首先,C++关键字class指出这些代码定义一个类设计,Stock
是新类的类型名,这个声明使我们能够开始声明Stock
类型变量 Stock sally;Stock Solly;
等等
类对象程序都可以直接访问公有部分,但只能通过公有成员函数来访问私有成员。如果要修改Stock类的shares对象,我们就只能通过其中的成员函数来访问;
公有对象就成了程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口
类设计尽可能将公有接口与实现细节分开,公有接口表示设计的抽象组件,有实现细节放在一起并将它们与抽象分开就是封装,数据隐藏(即将数据放在类的私有部分中)也是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_got()所做的一样,封装
公有还是私有
无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP主要目标之一,因此 数据项 通常放在私有部分,组成类接口的成员函数放在公有部分;否则,就无法从程序中调用这些函数
注意,我们不必在类声明中使用关键字private,因为这是类对象的默认访问控制
实现类成员函数
类描述的第二部分,为由类来声明的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征
- 定义成员函数,使用作用域解析运算符
::
来标识函数所属类 - 类方法可以访问
private
组件
|
|
如上,用作用域解析操作符指出函数所属类,update()函数是Stock类的成员,这不仅将update()标识为了一个成员函数,还意味着我们可以将另一个类成员函数也明明为update
之后 stock类的其他成员函数不必使用作用域解析操作符就可以使用update()方法, 因为 同属一类
那么第一个特点:作用域解析运算符确定了方法定义对应的类的身份讲完了
类方法的第二个特点,方法可以直接访问类的私有成员,如同访问一个已经声明好的常用变量一样。例如,show( ) 方法可以使用这样的代码:
|
|
company
和shares
等都是Stock类私有数据成员。 在成员函数中可以访问,但是如果使用非成员函数访问数据成员就要禁止了。
另外,类声明还将短小的成员函数作为内联函数在头文件中随类声明一起定义
内联函数的特殊规则要求在每个使用它们的文件中对其进行定义,确保内联定义对多文件程序中的所有文件可用,简单意义上来讲的话——把内联定义在类的头文件中
类的构造函数和析构函数
C++的目标之一就是让使用类对象像使用标准类型一样,然而,上面我们学到的部分还不能让我们像初始化int
那样舒适而惬意的初始化对象,也就是像下面这样
一般来说,最好在创建对象时初始化
gift的company的值开始时是没有的,所以为了避开这种问题,最好就是在创建对象时自动对它初始化。为此,C++就提供了一个特殊的成员函数——类构造函数来专门用于构造新对象、并将值赋给它们的数据成员。
声明和定义构造函数
现在我们需要构造Stock的构造函数。构造函数也提供三个参数,原型如下
字符串指针——company初始化。n和pr为shares 和share_val提供了值。
下面我们提供一种构造函数的可能定义 注意!,我们不能将类成员名称用作构造函数的参数名,
这样的话构造函数参数表示的不是类成员,而是赋给类成员的值。我想你看到这个就明白了
shares=shares;
这必然会导致一种混乱,如果想取用,可以加上_后缀使得不造成上面这种糗事
使用构造函数
- 显式调用构造函数
- 隐式调用构造函数
这与下面的显式调用等价
每次创建类对象(使用new动态分配内存)时,C++都使用类构造函数。下面是一个实例
|
|
使用构造函数后,我们一般使用对象调用方法
默认构造函数
当我们不提供显式初始值,我们用默认构造函数来创建对象。如下
正如
int x;
一样,创建对象但没有提供默认值注意,如果我们提供了非默认构造函数,如
Stock(const char* co,int n,double pr)
但没有提供默认构造函数,那么下面的声明会出错
|
|
综上,定义默认构造函数的方式有两种。
- 给已有构造函数的所有参数提供默认值:
|
|
- 通过函数重载定义另一个没有参数的构造函数:
|
|
看一个默认构造函数:
我们在设计类时,就通常应该对所有类成员做隐式初始化的默认构造函数
当我们使用上面任意一种方式创建了默认构造函数后,就可以声明对象变量了,而不需要再对它们进行显示初始化:
|
|
析构函数
析构函数,使用delete来释放构造函数使用new所分配的内存,只需在类名前加上~
就是析构函数,析构函数不需要参数,原型如下:
|
|
我们如果想看出其何时调用,可以编写提示信息
|
|
const成员函数
|
|
我们看以上代码片段,由于C++编译器无法确保调用对象不被修改,因此第二行会被拒绝。
但是这里show()方法没有任何参数,所以我们需要一个新的语法来确保函数不修改调用对象
|
|
就可以实现
同样,函数定义开头应该
|
|
以上面这种方式声明和定义的类函数就是const成员函数。只要类方法没有修改调用对象,就应该声明为const
小结
构造函数是一种特殊的类成员函数,在创建类对象时被调用。我们可以通过函数重载创建多个同名构造函数(控制函数特征值使函数不同)
通常来说,构造函数用于初始化类对象成员。
默认构造函数没有参数,创建对象没有进行显式初始化时我们就调用默认构造函数。如果程序中没有提供,编译器会为程序定义一个默认构造函数;否则就得自己提供默认构造函数了
对象被删除,程序调用析构函数,每个类只有一个析构函数,析构函数即在类名称前加上~
注意,构造函数若使用new
,则提供使用delete
的析构函数
比如:
|
|
正确实例:
|
|
this指针
Stock类还没有结束,到目前为止,每个类成员函数都只涉及一个对象,即调用它的对象。但有时候如果涉及到多个对象呢?我们需要使用 this 指针
如何将方法答案回传给调用的程序?例如,从show()输出我们可以知道持有的哪一支股票价格最高,但由于程序无法直接访问total_val,因此无法判断。最直接的方法是让方法返回一个引用。为此看到下例
|
|
通过将该函数添加到类声明,程序可以查看一系列股票,找到价格最高的一支,
但是我们可以使用定义一个成员函数,并了解this指针的用法。
我们定义一个成员函数,查看两个Stock对象,并返回股价较高的对象的引用,实现方法时,将出现有趣的问题。
我们定义一个比较的类方法原型topval
:
|
|
该函数隐式访问一个对象,显式访问另一个对象,并返回其中一个对象的引用。括号中const表示,该函数不会修改被显式访问的对象。括号后的const表示该函数不会修改被隐式访问的对象。这里返回的也是const引用
使用以上两条语句我们对两个对象比较并将股价总值较高的赋给top对象。但是这样的表示方法有些混乱,如果可以使用>运算符来直接完成,就更清晰了。同时注意,topval的实现有问题 c++通过this指针来解决这个问题,this指针指向用来调用成员函数的对象,过程中this作为隐藏参数传递给方法。函数调用stock1.topval(stock2)将this设置为了stock1对象的地址,让这个指针可用于topval()方法
是不是觉得有点抽象,实际上所有类方法都将this指针设置为调用它的对象的地址。topval()中的total_val就是this->total_val的简写,见下图
|
|
对象数组
声明对象数组方法和声明标准类型数组相同
|
|
可以用构造函数方便快捷的初始化数组元素,这种情况下每个元素都必须调用构造函数