Skip to content

Commit db38cff

Browse files
committed
Updated to Chapter 6, Section 2
1 parent 0e88cd9 commit db38cff

File tree

13 files changed

+269
-49
lines changed

13 files changed

+269
-49
lines changed

Structure.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -428,14 +428,10 @@
428428

429429
(简单水水)
430430

431-
#### 结构体对象作为返回值
431+
#### 结构体与函数
432432

433433
这是很有意义的,因为在此之前我们的函数只能返回单个值,现在我们可以用结构体将多个值封装起来一起返回了。
434434

435-
#### 结构体对象数组、对象指针
436-
437-
正如普通类型那样,结构体也有数组和指针。这里介绍些对象数组和对象指针,以及对象指针的动态内存分配。
438-
439435
#### 编程示例:链表
440436

441437
通过对象指针成员和动态内存分配,写一个简单的单向链表。

generalized_parts/06_custom_types_and_their_use.tex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ \chapter{自定义类型及其使用}
1818
那么言归正传(刚才的内容看不懂也没关系)。在本章,我将讲解如何利用基本数据类型和上一章中讲到的复合数据类型,来自创类型。我们不需要研究到比特这个层次,更不需要讲什么编码,我们拿C++现成的类型来组合就可以了。\par
1919
\import{06_custom_types_and_their_use/}{01_enum.tex}
2020
\import{06_custom_types_and_their_use/}{02_struct.tex}
21-
\import{06_custom_types_and_their_use/}{03_union.tex}
22-
\import{06_custom_types_and_their_use/}{04_introduction_to_class.tex}
21+
\import{06_custom_types_and_their_use/}{03_exercise_example_list.tex}
22+
\import{06_custom_types_and_their_use/}{04_union.tex}
23+
\import{06_custom_types_and_their_use/}{05_introduction_to_class.tex}

generalized_parts/06_custom_types_and_their_use/01_enum.tex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,24 @@ \section{枚举常量}
5151
const SuitsSize Giovanni {XXXL}; //Giovanni的值一经定义,就不可改变
5252
\end{lstlisting}\par
5353
总而言之,我们可以把具名枚举的各项看作是整型数据,但是我们应该先把它当成一种特殊的自定义类型,它的值就是标识符本身,只不过可以转换为整型而已。作为类比,我们可以想一下 \lstinline@bool@ 类型,它的值就是标识符 \lstinline@true@ 或 \lstinline@false@ 本身,只是它可以隐式转换成整型而已。\par
54+
每个枚举项的内存占用默认与 \lstinline@int@ 相同,这种做法有些时候有点浪费空间。比如说,\lstinline@SuitsSize@ 的所有枚举项的值都在 \lstinline@40@ 到 \lstinline@60@ 之间,用一个字节足够存储得下了。为了节约内存空间,我们可以改变它的枚举基,也就是它们基于哪种类型。
55+
\begin{lstlisting}
56+
enum SuitsSize : char {//...}; //省略
57+
\end{lstlisting}
58+
这样,\lstinline@SuitsSize@ 类型的内存占用就变成了一个字节,读者可以用 \lstinline@sizeof@ 检验之。\par
59+
除了 \lstinline@char@ 之外,我们还可以用 \lstinline@bool@ 以及所有的整型类型。例如要表示一个人的性别,我们可以用一个 \lstinline@bool@ 型数据来实现。我们可以记 \lstinline@true@ 表示男性,\lstinline@false@ 表示女性。不过用 \lstinline@true@/\lstinline@false@ 的表示方法不够直接,我们何不自定义一个具名枚举呢?
60+
\begin{lstlisting}
61+
enum Sex : bool {male, female}; //以bool为枚举基
62+
\end{lstlisting}
63+
在这里,我们当然也可以指定 \lstinline@male@/\lstinline@female@ 的布尔值;但是这样做没太大的意义,因为我们真正要表示的信息其实是性别而不是那个布尔值。在应用的时候,我们可以直接这么用:
64+
\begin{lstlisting}
65+
Sex group[5] {male, male, female, male, female}; //定义
66+
for(auto individual : group) { //范围for循环
67+
if(individual == male) { //用individual==male 作为判断条件
68+
//...
69+
}
70+
else {
71+
//...
72+
}
73+
}
74+
\end{lstlisting}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
\section{结构体}
2+
我们之前讲过的各数据类型,除了数组以外,都是只能表示单个数据的。比如说一个 \lstinline@double@ 数据,虽然它有8个字节,但是它的64个比特全都用来表示单个数据了。但是我们很容易想到,现实中的很多东西不是单纯用一个数据就能描述清楚的。比如我要描述一个长方体的信息,我需要三个数据:长、宽、高。这三个数据最好放在一起,作为一个整体存在,所以``定义三个变量''的思路就有点太原始了。\par
3+
所以我们自然会想到定义数组。
4+
\begin{lstlisting}
5+
int cuboid[3];
6+
\end{lstlisting}
7+
这样当然可以,但是它也有几个缺陷,不太方便解决:
8+
\begin{itemize}
9+
\item 数组类型没有排他性。它就是一个 \lstinline@int[3]@ 类型,但是有很多东西都是 \lstinline@int[3]@ 类型的,比如说三维空间坐标,或者是颜色的RGB值。如果说这些东西都算是同一类型的话,那未免有点牵强。
10+
\item 各个维度的数据没有明确的含义。\lstinline@cuboid[0]@,这个数据到底是长度,还是宽度,还是高度?这会造成困惑,所以我们需要事先约定第几个数据代表什么。这样就增加了理解成本,也容易犯错。如果我们可以直接给每个数据命名呢,岂不美哉?\footnote{这点也可以通过枚举常量作下标的方式来强行实现,不过就请读者自行尝试吧。}
11+
\item 数组对数据的包装不够彻底。它们归根到底还是三个数据,我们很难把它当成真正的``整体''来对待。比如说,函数的返回值只能是单个(整体)数据,但很明显函数不能直接返回一个数组。(我们可以让它返回一个指向数组的指针,或都是数组引用,但是那会非常麻烦)这就说明它的集成度还不够,我们需要集成度更高的方案。
12+
\item 数组只能存储同一类型的数据,这是一大硬伤。试想,如果我们描述一个人的特征,我们可能需要很多类型的数据放在一起,做成大杂烩:描述名字要用字符串类型,描述性别要用 \lstinline@bool@ 类型(或者以 \lstinline@bool@ 为枚举基的 \lstinline@Sex@),描述身高体重要用 \lstinline@float@ 类型(如果对精度要求不高),描述年龄要用 \lstinline@unsigned short@ 类型(用 \lstinline@int@ 也行),这么多类型,肯定是无法放在一个数组里的。
13+
\end{itemize}
14+
结构体能很好地解决以上困难,它是一个有排他性,集成度更高,类型支持更丰富的解决方案。有了结构体之后,我们就可以自定义数据类型,并存储我们想要存储的信息了。\par
15+
\subsection*{定义、声明、初始化和使用}
16+
结构体的定义不同于函数的定义。函数不能嵌套定义\footnote{Lambda除外,这个留到精讲篇再谈。},但我们可以在函数内或在函数外定义结构体,也可以嵌套定义结构体。\par
17+
不过就我们的习惯而言,把结构体定义在函数外——也就是全局范围内的情况更普遍。\par
18+
声明/定义一个结构体需要用到 \lstinline@struct@ 关键字。比如我们要定义一个长方体的信息,我们就需要把长、宽、高三条信息都包装到这个结构体中,所以我们可以这样写:
19+
\begin{lstlisting}
20+
struct Cuboid { //这个类型的名字就叫Cuboid了
21+
int length; //length部分数据,用int型
22+
int width; //width部分数据
23+
int height; //height部分数据
24+
}; //注意末尾的分号!
25+
\end{lstlisting}
26+
注意,这里的 \lstinline@length@, \lstinline@width@ 和 \lstinline@height@ 不是``枚举项'',它们不能单独存在,必须是作为 \lstinline@Cuboid@ 数据的一个部分存在的。我们也把这些部分称为\textbf{成员(Member)}。\par
27+
如果我们要声明,直接写成这样就行:
28+
\begin{lstlisting}
29+
struct Cuboid; //声明Cuboid类型
30+
\end{lstlisting}
31+
这样之后我们就可以定义结构体的对象(数据)了。我们来看一下如何初始化它们。
32+
\begin{lstlisting}
33+
Cuboid cub1 {1,2,3}; //length为1,width为2,height为3
34+
\end{lstlisting}
35+
也就是说,在花括号 \lstinline@{}@ 内的初始化数据会一一对应到 \lstinline@Cuboid@ 的三个成员中。那么如何使用它们呢?我们要用到成员访问运算符 \lstinline@.@。\par
36+
\begin{lstlisting}
37+
int volume1 {cub1.length * cub1.width * cub1.height}; //计算其体积
38+
\end{lstlisting}
39+
在这里,\lstinline@cub1.length@ 就是 \lstinline@cub1@ 的 \lstinline@length@ 成员,它的值是 \lstinline@1@;同理,\lstinline@cub1.width@ 的值就是 \lstinline@2@,\lstinline@cub1.height@ 的值就是 \lstinline@3@。所以最后会算得 \lstinline@volume1@ 的值是 \lstinline@6@。\par
40+
我们知道,同一个类型的不同数据可以存储不同的值,这是因为它们在内存中有各自的存储空间,互不干扰。对于结构体的对象来说也是如此,我可以定义若干个对象,并给它们不同的值,这时它们是互不干扰的。
41+
\begin{lstlisting}
42+
Cuboid cub2 {3,5,7}, cub3 {4,6,5}; //再定义两个Cuboid类型的对象
43+
\end{lstlisting}
44+
这就意味着 \lstinline@cub1@, \lstinline@cub2@ 和 \lstinline@cub3@ 有着各自的存储空间,互不干扰。我们可以用取地址运算符 \lstinline@&@ 来返回它的地址——也就是它存储位置中第一个字节的地址。\par
45+
\begin{lstlisting}
46+
Cuboid cub1 {1,2,3}, cub2 {3,5,7}, cub3 {4,6,5};
47+
cout << sizeof (Cuboid) << endl //输出Cuboid类型的内存占用
48+
<< &cub1 << endl << &cub2 << endl << &cub3 << endl; //分别输出地址
49+
\end{lstlisting}
50+
程序的运行结果如下\footnote{输出的地址值和内存占用量可能因设备而异。总而言之,运行结果不唯一。}:\\\noindent\rule{\linewidth}{.2pt}\texttt{
51+
12\\
52+
0x7ffc5cc022c0\\
53+
0x7ffc5cc022d0\\
54+
0x7ffc5cc022e0
55+
}\\\noindent\rule{\linewidth}{.2pt}\\
56+
从这个运行结果中我们能看出,\lstinline@Cuboid@ 类型的内存占用是12个字节,但是我定义的三个 \lstinline@Cuboid@ 类型的对象分别位于 \lstinline@...2c0@, \lstinline@...2d0@ 和 \lstinline@...2e0@ 位置上,每两个地址相差16字节——也就是说,它们在内存中不是紧密排布的,相互之间间隔了4个字节。\par
57+
其实我之前在讲一维数组时就提过,C/C++标准从来就没有保证过``连续定义的若干变量在内存中必须是紧挨着的'',这就是一个绝佳的例证。也正因如此,\lstinline@&cub1+1@ 这种语法就是错误的,因为我们不能保让它还指向有效信息。如果我们希望让它紧密排布,那么我们应该怎么做呢?很简单,定义一个数组。
58+
\begin{lstlisting}
59+
Cuboid cubs[3] {{1,2,3},{3,5,7},{4,6,5}}; //定义一个数组
60+
for (Cuboid cub : cubs) { //范围for循环
61+
cout << "长度 " << cub.length
62+
<< ",宽度 " << cub.width
63+
<< ",高度 " << cub.height
64+
<< endl; //输出长、宽、高,然后换行
65+
}
66+
\end{lstlisting}
67+
这个程序的运行结果就是\\\noindent\rule{\linewidth}{.2pt}\texttt{
68+
长度 1,宽度 2,高度 3\\
69+
长度 3,宽度 5,高度 7\\
70+
长度 4,宽度 6,高度 5
71+
}\\\noindent\rule{\linewidth}{.2pt}
72+
\subsection*{结构体与函数}
73+
结构体把数据包装得更好,这样我们就可以把它作为一个完整的单元,传给函数作为参数,或者是作为函数的返回值。举个例子,我们要写一个 \lstinline@rotate_horizontal@ 函数,来水平方向旋转这个长方体,把长度和宽度颠倒过来。
74+
\begin{lstlisting}
75+
void rotate_horizontal(Cuboid &cub) { //引用传递
76+
swap(cub.length, cub.width); //调用标准库中的swap函数,可能需要utility库
77+
}
78+
\end{lstlisting}
79+
在这里我们可以直接对 \lstinline@cub.length@ 和 \lstinline@cub.weight@ 成员进行交换,因为是引用传参,所以这样就可以直接修改传入的实参。\par
80+
\begin{lstlisting}
81+
Cuboid cubs[3] {{1,2,3},{3,5,7},{4,6,5}}; //定义一个数组
82+
for (Cuboid &cub : cubs) { //在范围for循环中,如果要修改cubs,应该用引用
83+
rotate_horizontal(cub); //引用传参,将会修改实参cub
84+
cout << "长度 " << cub.length
85+
<< ",宽度 " << cub.width
86+
<< ",高度 " << cub.height
87+
<< endl; //输出长、宽、高,然后换行
88+
}
89+
\end{lstlisting}
90+
这个程序的运行结果就是\\\noindent\rule{\linewidth}{.2pt}\texttt{
91+
长度 2,宽度 1,高度 3\\
92+
长度 5,宽度 3,高度 7\\
93+
长度 6,宽度 4,高度 5
94+
}\\\noindent\rule{\linewidth}{.2pt}\\
95+
看起来非常完美地实现了我们的目标。\par
96+
我们还可以做一些其它的功能,比如说写一个函数,每次调用它时就用 \lstinline@new@ 来创 建一个新的长方体,并返回它的地址,这样我们就可以用一个指针来接收它。
97+
\begin{lstlisting}
98+
Cuboid* create_cuboid(int l, int w,int h) { //返回值是指向Cuboid的指针类型
99+
return new Cuboid {l,w,h}; //创建动态对象,并用l,w,h初始化。
100+
}
101+
\end{lstlisting}
102+
于是我们就可以使用它了。
103+
\begin{lstlisting}
104+
Cuboid *pcub {create_cuboid(5,12,13)}; //用create_cuboid创建一个新长方体
105+
//...
106+
delete pcub; //不要忘记!
107+
\end{lstlisting}\par
108+
我们发现,输出 \lstinline@Cuboid@ 对象信息要写很长一串代码,我们也可以写一个函数来来实现这个功能。这样我们就不需要每次很麻烦地写这么多代码了,直接调函数来就好。
109+
\begin{lstlisting}
110+
void output_cuboid(const Cuboid &cub, ostream &out = {cout}){
111+
out << "长度 " << cub.length
112+
<< ",宽度 " << cub.width
113+
<< ",高度 " << cub.height
114+
<< endl; //输出长、宽、高,然后换行
115+
}
116+
\end{lstlisting}\par
117+
这里我们用 \lstinline@const Cuboid &cub@ 的原因是,传引用一般要更节省内存空间\footnote{并不总是如此,比如说对于 \lstinline@char@ 类型来说,传值只需要1个字节的空间临时变量就行,但传引用需要4或8个字节的临时指针(传引用的本质是传指针)。}。而我们在这里不需要修改 \lstinline@cub@,所以把它设成 \lstinline@const@ 可以防止篡改它的值。\par
118+
至于 \lstinline@out@,我们为它设计了一个默认值 \lstinline@cout@。如此,如果我们想用 \lstinline@cout@ 来输出的话,就不需要写第二个参数了。其实,更合理的参数列表写法是先 \lstinline@ostream&@ 再 \lstinline@const Cuboid&@;但是鉴于默认参数必须设置在列表右侧,所以这样是不得已而做出的设计。\par
119+
\subsection*{结构体成员的类型}
120+
刚才的例子比较简单,\lstinline@Cuboid@ 的三个成员都是同一类型的。实际上我们可以用不同类型的数据,把它们组织到同一个结构体中。\par
121+
例如,如果要表示一个人的基本信息,我们可能需要用字符串表示名字,用 \lstinline@Sex@(上一节中自定义的枚举类型)表示性别,用 \lstinline@double@ 身高、体重,用 \lstinline@unsigned@ 表示年龄。那么我们可以这样写:
122+
\begin{lstlisting}
123+
enum Sex : bool{male, female}; //枚举基为bool
124+
struct PersonalInfo { //一个结构体,表示个人信息
125+
char name[32]; //字符串,表示名字
126+
const Sex sex; //性别一般是不会改变的,所以设置成const
127+
double height;
128+
double weight;
129+
unsigned age;
130+
};
131+
\end{lstlisting}
132+
接下来我们可以定义一些函数,比如这个函数可以用来输出某个人的个人信息:
133+
\begin{lstlisting}
134+
void output_info(const PersonalInfo &person, ostream &out = {cout}) {
135+
out << person.name << "," //输出字符串
136+
<< (person.sex == male ? "男" : "女") << "," //条件表达式
137+
<< person.age << "岁" << endl //换行
138+
<< "身高 " << person.height << ","
139+
<< "体重 " << person.weight << endl;
140+
}
141+
\end{lstlisting}
142+
然后我们就可以在主函数中写一些代码来测试它的行为了。
143+
\begin{lstlisting}
144+
int main() {
145+
PersonalInfo group[3]{
146+
{"John Doe", male, 175.5, 70.2, 30},
147+
{"Jane Smith", female, 162.3, 55.8, 25},
148+
{"Bob Johnson", male, 180., 80.5, 35}
149+
}; //定义一个PersnalInfo[3],分别为它们初始化
150+
for (PersonalInfo person : group) {
151+
output_info(person); //在范围for循环中输出每个人的信息
152+
cout << endl; //为了区分,每两人的信息之间多换一行
153+
}
154+
return 0;
155+
}
156+
\end{lstlisting}
157+
这个程序的运行结果如下:\\\noindent\rule{\linewidth}{.2pt}\texttt{
158+
John Doe,男,30岁\\
159+
身高 175.5,体重 70.2\\
160+
\\
161+
Jane Smith,女,25岁\\
162+
身高 162.3,体重 55.8\\
163+
\\
164+
Bob Johnson,男,35岁\\
165+
身高 180,体重 80.5
166+
}\\\noindent\rule{\linewidth}{.2pt}\par
167+
读者可能注意到 \lstinline@person.sex==male?"男":"女"@ 此段中我们使用的条件表达式。如果 \lstinline@person.sex==male@ 为 \lstinline@true@,那么就会返回 \lstinline@"男"@;否则返回 \lstinline@"女"@。\par
168+
看上去无论是内置类型还是自定义类型,我们都可以把它放到 \lstinline@struct@当中,构成一个结构体。那么有什么是不可以放入其中构成结构体的呢?那就是这个结构体本身!在函数定义中我们见过递归定义,但是结构体是不允许递归定义的。
169+
\begin{lstlisting}
170+
struct Data {
171+
int num;
172+
Data next; //不允许
173+
};
174+
\end{lstlisting}
175+
这是因为,如果我们递归定义的话,那么程序就不知道这个类型占用的内存空间有多大了——这个类型的大小等于这个类型的大小加上一些杂七杂八的东西,这是不合理的!\par
176+
但是这个类型的成员中可以有指向这个类型的指针。
177+
\begin{lstlisting}
178+
struct Data {
179+
int num;
180+
Data *next; //可以
181+
}
182+
\end{lstlisting}
183+
\lstinline@Data*@ 与 \lstinline@Data@ 可不是同一个类型,而且 \lstinline@Data*@ 是一个指针,它占用内存空间的大小是确定的,所以程序当然知道 \lstinline@sizeof(Data)@ 是多少,所以在我们定义 \lstinline@Data@ 对象时也就知道要使用多大的内存空间了。\par
184+
基于这个用法,我们可以写一个简单的单链表,用来存储任意量的数据。我们将会在下一节中介绍相关内容。\par

generalized_parts/06_custom_types_and_their_use/03_union.tex renamed to generalized_parts/06_custom_types_and_their_use/03_exercise_example_list.tex

File renamed without changes.

generalized_parts/06_custom_types_and_their_use/04_introduction_to_class.tex renamed to generalized_parts/06_custom_types_and_their_use/04_union.tex

File renamed without changes.

generalized_parts/06_custom_types_and_their_use/05_introduction_to_class.tex

Whitespace-only changes.

0 commit comments

Comments
 (0)