Skip to content

Commit acbe0e8

Browse files
committed
Updated to Chapter 8, Section 2
1 parent 7b8cc41 commit acbe0e8

21 files changed

+525
-105
lines changed

Structure.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -516,19 +516,21 @@
516516

517517
> 先讲运算符重载再讲友元也有我的考虑。这样能破除两种常见的偏见:一是“友元函数是成员函数”;二是“运算符必须定义为友元”。
518518
519-
### 友元函数
519+
### 成员函数与友元函数
520520

521-
#### 为何使用友元
521+
#### 成员函数的声明与定义
522522

523-
非成员函数的访问权限受到制约。
523+
`valarray`类为例,定义一些函数和运算符。
524+
525+
#### 成员形式的运算符重载
524526

525527
#### 编程示例:自建简易`valarray`
526528

527529
基于普通数组实现,属于低配版。
528530

529531
重载如下运算符,边写边讲。
530532

531-
- 重载`=``+=`,这里先不提“自我赋值”的问题(因为是用数组实现的,自我赋值又没什么问题)
533+
- 重载`=``+=`
532534
- 重载`[]`,强调它不能是友元,必须是成员函数。
533535
- 重载`+``*`,既有成员函数版本,又有非成员函数版本。
534536
- 重载`<<(ostream&,const valarray&)`,它是`valarray`的友元,但不是`ostream`的友元。
@@ -541,12 +543,6 @@
541543

542544
> 移动构造函数放精讲篇。
543545
544-
#### `this`指针
545-
546-
这里必须提一下`this`指针的问题。
547-
548-
如果看汇编代码的话,就会发现成员函数都是自带一个隐藏参数`this`的。这说明在构造函数执行之前,对象就已经存在了。
549-
550546
#### 初值列语法
551547

552548
怎么使用初值列,为什么初值列更高效。
@@ -629,8 +625,6 @@
629625

630626
看一下我写出来的`string`类效果如何。
631627

632-
####
633-
634628
## 类的继承
635629

636630
### 概念介绍
@@ -1179,6 +1173,6 @@ Placement new。主要是介绍下布置分配的语法和注意事项(结束
11791173

11801174
这里整理C++中可能用到的相关数学知识。我没必要在主体内容中集中讲它,所以放到这里来。
11811175

1182-
### 进制与进制转换
1176+
### 进制转换
11831177

11841178
### 布尔代数基础

generalized_parts/04_introduction_to_functions/05_recursion.tex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,21 @@ \section{函数递归}
2323
}
2424
\end{lstlisting}
2525
我来解释一下这段代码,并回答一下读者可能关心的问题。\par
26-
\textit{``我用 \lstinline@factorial@ 来定义 \lstinline@factorial@,编译器不会报错吗?''}\par
26+
{\kaishu ``我用 \lstinline@factorial@ 来定义 \lstinline@factorial@,编译器不会报错吗?''}\par
2727
不会的,因为\textbf{函数的定义就是一种声明}。所以编译器在 \lstinline@factorial@ 函数给定参数列表时就已经知道了它的存在(图4.5)。\par
2828
\begin{figure}[htbp]
2929
\centering
3030
\includegraphics[width=0.5\textwidth]{../images/generalized_parts/04_factorial_code_logic_300.png}
3131
\caption{\lstinline@factorial@ 的定义就是一种声明,所以编译器能够找到它}
3232
\end{figure}
33-
\textit{``这段代码会怎么运行?''}\par
33+
{\kaishu ``这段代码会怎么运行?''}\par
3434
想像你就是那个程序,在主函数中,当你遇到 \lstinline@factorial(12)@ 时,你会调用这个函数并代入实参 \lstinline@n=12@ 从而求出它的返回值。而在运行时发现,因为 \lstinline@n==0@ 不满足,所以你不得不再去求 \lstinline@factorial(11)@,然后是 \lstinline@factorial(10)@,以此类推。如果一切顺利的话,你会在求 \lstinline@factorial(0)@ 的时候直接返回 \lstinline@1@,然后用 \lstinline@1*factorial(0)@ 算出 \lstinline@factorial(1)@ 的返回值;用 \lstinline@2*factorial(1)@ 算出 \lstinline@factorial(2)@ 的返回值;用 \lstinline@3*factorial(2)@ 算出 \lstinline@factorial(3)@ 的返回值……一直到你算出 \lstinline@factorial(12)@ 为止。图4.6展示了这一过程。\par
3535
\begin{figure}[htbp]
3636
\centering
3737
\includegraphics[width=\textwidth]{../images/generalized_parts/04_the_process_of_recursion_300.png}
3838
\caption{\lstinline@factorial@ 函数递归的运行过程}
3939
\end{figure}
40-
\textit{``函数递归运行的过程中,难道还能真的像图4.6那样,运行到一半再跑到开头吗?''}\par
40+
{\kaishu ``函数递归运行的过程中,难道还能真的像图4.6那样,运行到一半再跑到开头吗?''}\par
4141
读者可能容易产生这样的误会,认为``既然它是同一个函数,那递归的时候不就是在这一个函数中打转嘛''\par
4242
不然。这个函数在编译时和运行时的概念是不同的。在编译时,编译器会认为它是一个函数;但在运行时,内存中会为程序创建一个调用栈(Call stack)来存储函数调用的信息。每次函数调用(包括一般调用和递归调用)都会在栈中压入一个堆栈帧(Stack frame),这个堆栈帧中存储了本次调用的必要信息(比如,本次调用的实参是什么,返回地址是什么)。在结束调用时,返回值传回给调用它的函数(栈的结构保证了调用它的函数也在调用栈中),然后堆栈帧移除,如图4.7。\par
4343
\begin{figure}[htbp]
@@ -46,7 +46,7 @@ \section{函数递归}
4646
\caption{调用函数时压入堆栈帧,返回值后移除堆栈帧}
4747
\end{figure}
4848
简言之,虽然\textbf{只有一个函数},但\textbf{有很多个堆栈帧},每个堆栈帧存储了各自的调用信息。所以自然不会有什么冲突了。\par
49-
\textit{``为什么一定要有终止条件呢?难道不能像 \lstinline@while(true)@ 语句这样永远循环吗?''}\par
49+
{\kaishu ``为什么一定要有终止条件呢?难道不能像 \lstinline@while(true)@ 语句这样永远循环吗?''}\par
5050
我们可以把递归结构当作是一种循环,而 \lstinline@for@ 或 \lstinline@while@ 可以更具体地说是一种迭代(Iteration)。从功能上讲,递归和迭代是非常相似的,它们可以互相替代,而关于``用递归还是迭代''的讨论也是旷日持久。这里我们先不参与此讨论,只解答原本的问题。\par
5151
递归与迭代有一点不同在于,迭代语法不需要依赖调用栈,比如 \lstinline@for(int i=0;i<1e9;i++)@,无论循环多少次,\lstinline@i@永远占据一个 \lstinline@int@ 的内存空间;但递归是需要依赖调用栈的,并且调用栈的大小有限,一旦内存不够,就会发生\textbf{堆栈溢出(Stack overflow)},直接导致程序故障罢工。正因如此,我们可以写无限循环的迭代,但不能写一个无限循环的递归。\par
5252
希望读者看过上述的讲解以后,已经对递归有了基本的认知。\par

generalized_parts/05_composite_types_and_their_use/01_pointer.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
\section{指针}
2-
{\large``我认为赋值语句和指针变量可以说是计算机科学最具价值的宝藏。''\footnote{原文:I do consider assignment statements and pointer variables to be among computer science's ``most valuable treasures.''}}
2+
{\kaishu \large ``我认为赋值语句和指针变量可以说是计算机科学最具价值的宝藏。''\footnote{原文:I do consider assignment statements and pointer variables to be among computer science's ``most valuable treasures.''}}
33
\begin{flushright}——高德纳\footnote{高德纳(Donald Ervin Knuth),美国计算机科学家和数学家,1974年图灵奖得主。高德纳成就颇丰,其中最负盛名的是它的著作《计算机程序设计艺术》(\textit{The Art of Computer Programming})。}\end{flushright}\par
44
\subsection*{内存与地址}
55
在一个C/C++程序运行时,内存中会存储着有关这个程序的信息。我们可以把这些内存空间分为五个区段(如图5.1所示):

generalized_parts/05_composite_types_and_their_use/05_string_with_arrays.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ \subsection*{字符串的输入}
144144
cout << name1 << endl << name2; //输出第一个人名,再换行,再输出第二个人名
145145
\end{lstlisting}
146146
如果我们真的运行一下,就会发现情况好像不太对劲。\\\noindent\rule{\linewidth}{.2pt}\texttt{
147-
\textbf{Bjarne Stroustrup}\\\textit{(刚按下回车键,准备输入第二个人名,结果程序就开始输出了)}\\
147+
\textbf{Bjarne Stroustrup}\\{\kaishu(刚按下回车键,准备输入第二个人名,结果程序就开始输出了)}\\
148148
Bjarne\\
149149
Stroustrup
150150
}\\\noindent\rule{\linewidth}{.2pt}

generalized_parts/06_custom_types_and_their_use/02_struct.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ \subsection*{结构体与函数}
7373
结构体把数据包装得更好,这样我们就可以把它作为一个完整的单元,传给函数作为参数,或者是作为函数的返回值。举个例子,我们要写一个 \lstinline@rotate_horizontal@ 函数,来水平方向旋转这个长方体,把长度和宽度颠倒过来。
7474
\begin{lstlisting}
7575
void rotate_horizontal(Cuboid &cub) { //引用传递
76-
swap(cub.length, cub.width); //调用标准库中的swap函数,可能需要utility库
76+
swap(cub.length, cub.width); //调用标准库中的swap函数,注意需要utility库
7777
}
7878
\end{lstlisting}
7979
在这里我们可以直接对 \lstinline@cub.length@ 和 \lstinline@cub.weight@ 成员进行交换,因为是引用传参,所以这样就可以直接修改传入的实参。\par

generalized_parts/06_custom_types_and_their_use/05_introduction_to_class.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
\section{\texttt{class}初步}
2-
{\large ``一个设计良好的类能为用户提供简洁易用的接口\footnote{接口(Interface),在信息技术中指能将不同部件连接起来的媒介。通过接口,不同部件之可以进行信息交换。例如,笔记本电脑有USB接口,如果手机通过数据线连到这个接口上,那么笔记本电脑就可以和手机交换信息。},并将其内部结构隐藏起来,用户根本不必了解其内部结构。如果内部结构不应该被隐藏——例如,因为用户需要随意改变类中的任何数据成员——你可以把这种类认为是`普通的老式数据结构'。''\footnote{原文:A well-designed class presents a clean and simple interface to its users, hiding its representation and saving its users from having to know about that representation. If the representation shouldn't be hidden - say, because users should be able to change any data member any way they like - you can think of that class as `just a plain old data structure'.}}
2+
{\kaishu\large ``一个设计良好的类能为用户提供简洁易用的接口\footnote{接口(Interface),在信息技术中指能将不同部件连接起来的媒介。通过接口,不同部件之可以进行信息交换。例如,笔记本电脑有USB接口,如果手机通过数据线连到这个接口上,那么笔记本电脑就可以和手机交换信息。},并将其内部结构隐藏起来,用户根本不必了解其内部结构。如果内部结构不应该被隐藏——例如,因为用户需要随意改变类中的任何数据成员——你可以把这种类认为是`普通的老式数据结构'。''\footnote{原文:A well-designed class presents a clean and simple interface to its users, hiding its representation and saving its users from having to know about that representation. If the representation shouldn't be hidden - say, because users should be able to change any data member any way they like - you can think of that class as `just a plain old data structure'.}}
33
\begin{flushright}——比雅尼·斯特劳斯特鲁普\end{flushright}\par
44
我们在第二节中所介绍的结构体更接近于C风格。它只能实现``把多个数据包装到一个对象中''的功能。而在使用时,我们可以任意地改变它的成员变量。这样做的好处是方便,简单,直接。如果我们要写一个结构体来管理单链表,我们完全可以这样写:
55
\begin{lstlisting}

generalized_parts/07_projecting/02_namespace.tex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ \subsection*{命名空间的定义和使用}
7676
} //定义函数模版min
7777
}; //注意分号结尾
7878
\end{lstlisting}
79-
\lstinline@cppHusky@ 命名空间中定义的 \lstinline@max@ 就不同于 \lstinline@utility@ 库中,在 \lstinline@std@ 命名空间中定义的 \lstinline@max@。
79+
\lstinline@cppHusky@ 命名空间中定义的 \lstinline@max@ 就不同于 \lstinline@algorithm@ 库中,在 \lstinline@std@ 命名空间中定义的 \lstinline@max@。
8080
\begin{lstlisting}
8181
std::cout << std::max(3, 5) << std::endl; //注意endl也在std命名空间中
8282
std::cout << cppHusky::max(3, 5) << std::endl;
@@ -140,7 +140,7 @@ \subsection*{命名空间的定义和使用}
140140
};
141141
\end{lstlisting}
142142
在第一次使用 \lstinline@namespace cppHusky@ 的时候,我们是在定义一个新的命名空间;而在第二次使用 \lstinline@namespace cppHusky@ 的时候,因为 \lstinline@cppHusky@ 已经定义,所以这里新增的内容只是对 \lstinline@cppHusky@ 命名空间的内容扩充。在这两段代码编译完毕之后,\lstinline@cppHusky@ 命名空间中就既有了 \lstinline@Tname@ 的定义,又有了 \lstinline@cin@ 的定义。我们还可以在其它地方继续通过 \lstinline@namespace cppHusky@ 添加新的内容。\par
143-
之所以允许这样做,是因为我们会有``把不同头文件/源文件中定义的名字放在同一空间下''的需求。比如对于 \lstinline@std@ 命名空间来说,C++在 \lstinline@iostream@ 头文件中定义了 \lstinline@std::cin@,在 \lstinline@cstring@ 头文件中定义了 \lstinline@std::strlen@,在 \lstinline@utility@ 头文件中定义了 \lstinline@std::max@ 和 \lstinline@std::min@,诸如此类……它们处于不同文件中,但只要用 \lstinline@namespace std@ 就可以把它们一同加到这个空间中来,这岂不是很方便?\par
143+
之所以允许这样做,是因为我们会有``把不同头文件/源文件中定义的名字放在同一空间下''的需求。比如对于 \lstinline@std@ 命名空间来说,C++在 \lstinline@iostream@ 头文件中定义了 \lstinline@std::cin@,在 \lstinline@cstring@ 头文件中定义了 \lstinline@std::strlen@,在 \lstinline@algorithm@ 头文件中定义了 \lstinline@std::max@ 和 \lstinline@std::min@,诸如此类……它们处于不同文件中,但只要用 \lstinline@namespace std@ 就可以把它们一同加到这个空间中来,这岂不是很方便?\par
144144
\subsection*{\lstinline@using@ 声明}
145145
如果每次都要靠作用域解析运算符 \lstinline@::@ 来访问非全局变量,那就显得太麻烦了。所以初学者一般很喜欢这样写,一劳永逸:
146146
\begin{lstlisting}

generalized_parts/08_a_step_forward_in_classes_and_functions.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ \chapter{类与函数进阶}
33
我的目标是,在本章的结尾带领读者写出一个简化版的 \lstinline@string@ 类。C++的 \lstinline@string@ 库中已经有这个类,按理说不需要我们实现。但是,``会用''``会写''还是不一样的!在用的时候,我们对很多陷阱一无所知,对很多问题浑然不觉——这恰恰是封装良好的优点,我们使用某个功能时无需在这些杂碎问题上浪费不必要的时间——一旦自己从头开始搭建,这些问题就会纷纷暴露出来。\par
44
只有用过了知识,我们才能掌握;只有暴露了问题,我们才能进步。任何一门编程语言都是如此,C++也不例外。\par
55
\import{08_a_step_forward_in_classes_and_functions/}{01_operator_overloading.tex}
6-
\import{08_a_step_forward_in_classes_and_functions/}{02_friend.tex}
6+
\import{08_a_step_forward_in_classes_and_functions/}{02_member_functions_and_friend.tex}
77
\import{08_a_step_forward_in_classes_and_functions/}{03_constructor_and_destructor.tex}
88
\import{08_a_step_forward_in_classes_and_functions/}{04_property_of_member.tex}
99
\import{08_a_step_forward_in_classes_and_functions/}{05_array_of_objects_and_pointer_to_object.tex}

generalized_parts/08_a_step_forward_in_classes_and_functions/01_operator_overloading.tex

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,27 @@ \subsection*{重载中的代码重用}
197197
}
198198
\end{lstlisting}\par
199199
\subsection*{运算符重载的一些规则}
200-
讲了一些比较基础的例子之后,我们最后来解读一下运算符重载的常见规则。这些内容摘自\href{https://zh.cppreference.com/w/cpp/language/operators}{运算符重载 - cppreference}:\par
200+
讲了一些比较基础的例子之后,我们最后来解读一下运算符重载的常见规则。这些内容摘录并整理自\href{https://zh.cppreference.com/w/cpp/language/operators}{运算符重载 - cppreference}:\par
201201
\begin{itemize}
202+
\item 重载运算符接收的参数中,必须至少有一个是自定义类型\footnote{这里的``自定义''类型是指除了指针、数组和内置类型之外的类、枚举等类型,比如 \lstinline@std::vector@。}。如果重载运算符是成员函数,那么本条自动满足。\\——也就是说,我们不能对 \lstinline@int@, \lstinline@double@ 这些类型再添加新的运算规则。它们是封闭的,同时也是安全的。
203+
\item 有些运算符的重载受到限制,它们只能作为成员函数来定义,不能作为非成员函数来定义,包括(复合)赋值运算符、函数调用运算符和下标运算符。\\——到此为止我们都在定义非成员函数,这主要是因为我们不能撬开 \lstinline@vecint@ 的定义来修改它(这也是封装的优点)。在后文中,我们会自己试着写一个简易的 \lstinline@valarray@ 类,这样就可以定义成员函数了。
202204
\item 有些运算符是不允许重载的:作用域解析运算符 \lstinline@::@ 、成员访问运算符 \lstinline@.@ 、成员指针访问运算符 \lstinline@.*@ 、条件运算符 \lstinline@? :@ ,以及 \lstinline@sizeof@ 等。附录A中都有所介绍。\\——毕竟,有些东西还是要保留其本意的,不然就会出现很大的麻烦。C++已经给了我们足够的余裕,剩下的运算符都可以无条件重载,或是在有限条件下重载。
203205
\item 不能创建新的运算符,也即附录A之外的运算符,比如 \lstinline@$@, \lstinline@**@\footnote{在有些情况下,我们可能连用某个一元运算符,比如对于二阶指针写成 \lstinline@**pp@。但这不意味着我们是在``使用 \lstinline@**@ 运算符'';这其实应该叫作``两次使用 \lstinline@*@ 运算符''!}, \lstinline@<>@。\\——这更容易理解,因为我们是在``重载''运算符,当然就是重新定义已经存在的运算符,而不是自行开发新的。
204206
\item 运算符的优先级、结合性和操作数的数量不会发生变化。\\——当然,不然编译器就不知道如何理解代码了。这些属于运算符本来的特怔既不会变化,也不应该变化。
205-
\item 有些运算符的重载受到限制,它们只能作为成员函数来定义,不能作为非成员函数来定义。\\——到此为止我们都在定义非成员函数,这主要是因为我们不能撬开 \lstinline@vecint@ 的定义来修改它(这也是封装的优点)。在后文中,我们会自己试着写一个简易的 \lstinline@valarray@ 类,这样就可以定义成员函数了。
206207
\item 重载的运算符 \lstinline@->@ 必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符 \lstinline@->@ 的对象。
207208
\item 重载的运算符 \lstinline@&&@ 与 \lstinline@||@ 在使用时不会再有短路求值特性。\\——这也好理解,毕竟重载之候它就不一定实现与原来相同的功能了,所以短路求值作为一个``针对原本目的而做的优化''就没有必要,更不应该保留了。
208209
\end{itemize}\par
209210
这些规则可能看上去比较乱,读者未必现在就能理解。没关系,等你经验丰富了之后就自然会掌握了。\par
210-
至于本节中定义的那些语法糖,它们主要是作为教学目的出现的,而且也不是很规范,所以往后我们就不会再用了。如果读者还想试试这些语法糖的效果,可以跑一下这段代码:
211-
\begin{lstlisting}
212-
vecint arr; //定义arr,默认为空数组
213-
arr << 1 << 3 << 5 << 7; //依次向其中填入1, 3, 5, 7
214-
for (int i = 0; i < *arr; i++) { 通过arr的size来确定循环范围
215-
std::cout << arr[i] << ' ';
211+
至于本节中定义的那些语法糖,它们主要是作为教学目的出现的,而且也不是很规范,所以往后我们就不会再用了。如果读者还想试试这些语法糖的效果,可以看一下这段代码:
212+
\begin{lstlisting}
213+
/*重载加法运算符,从而返回两个数组的和
214+
规则是,新数组的每个元素值都是两数组对应位置的值之和
215+
如果两数组长度不等,则忽略较长的部分*/
216+
vecint operator+(const vecint &a, const vecint &b) {
217+
vecint v; //默认为空
218+
for (int i = 0; i < std::min(*a, *b); i++) {
219+
v << a[i] + b[i]; //依次在后端插入a[i]+b[i]的值
216220
}
221+
return v; //作为数组来返回,因为是临时变量,所以不应当按指针/引用返回
222+
}
217223
\end{lstlisting}\par

generalized_parts/08_a_step_forward_in_classes_and_functions/02_friend.tex

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)