换个姿势学C语言 第7章 获取全部外币牌价数据并保存为文件
0. 说明
《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

这是一本非常不错的书!
7. 获取全部外币牌价数据并保存为文件
变量、数组都是存储在内存RAM中的,这些数据所占用的内存在程序结束以后会被操作系统回收,其中的数据也就丢失了。因此我们需要将数据保存到外部存储器上(通常是硬盘),以便下次使用。
在一些较底层的语言里,程序员可以直接访问硬盘的某个扇区并进行数据读写,但这种方式一般不被推荐,因为这种方式除了效率比较低外还具有较大的危险,不恰当的磁盘访问可能会引起严重的故障(例如操作系统崩溃或者数据丢失)。
因此通常是以“文件”来组织磁盘上的数据。文件系统由操作系统管理,程序员通过操作系统间接地访问磁盘上的数据,不恰当的文件访问会被操作系统阻止(例如文件被其他程序占用或程序没有访问这个文件的权限),这样一来就安全得多,同时操作系统也会采取一些机制来提高文件访问的效率。
本章将会将取得的外汇牌价数据保存到磁盘文件中,但是在学习磁盘文件访问之前先学习结构体的使用方法。
结构体可以将多种不同类型的数据“组合”到一起,然后再将其存储到磁盘文件中。
7.0 结构体
在学习课本内容前,我们先自己倒腾一下结构体的使用。
之前已经学习过 C 语言的许多基本数据类型,如整型int、浮点float、字符型 char 等,还学习了数组这种构造类型。数组中,所有的数据都是同一类型,调用起来非常方便。
除此以外,有时我们需要定义一些复杂的数据类型,它可能包括多个不同属性,每个属性需要用不同的类型来表示。该怎么实现呢?
C语言中,可以把一些有内在联系的不同变量组织起来,封装成一个整体,即定义成一个结构体(structure),以此来表示一种新的数据类型。之后,就可以像处理基本数据类型那样,对结构体类型进行各种操作。
结构体是一种构造类型,它由若干成员组成。其成员可以是一个基本数据类型,也可以是另一个构造类型。声明一个结构体的过程,就是创建一种新的类型名的过程。
1
2
3
4
5
6
| struct 结构体名称
{
类型 成员名称1;
类型 成员名称2;
...
};
|
- 关键字 struct 表示声明的是一个结构体,“结构体名称”表示要创建的新类型名,大括号中是“成员列表”,一行一个定员,需要包括构成该结构体的所有成员。注意,声明结构体时大括号后的分号“;”不能遗漏。
现在以Book书构建结构体,如书名,作者,出版社,出版时间,价格等属性,并创建几个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| // L07_00_BOOK_STRUCT.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
// 附加包含目录已经添加 D:/BC101/Libraries/Mars
// 此处引入自定义的静态库的头文件需要相对路径
#include "str.h"
#include "input.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
// lib:表示我要链接一个静态库
// 路径/Mars.lib":库文件在哪里
#pragma comment(lib,"Mars.lib")
// 定义结构体:描述一本书的信息
// 书名,作者,出版社,出版时间,定价等属性,
struct Book {
char title[50]; // 书名。如:换个姿势学C语言
char author[15]; // 作者。如:何旭辉
char press[100]; // 出版社。如:清华大学出版社
char date[20]; // 出版时间,如:2022-09-01
float price; // 定价。如:118.00
};
// 1. 初始化书籍
// struct Book* book:这里的 book 是【指针】,不是普通变量!
void initBook(struct Book* book, const char* title, const char* author,
const char* press, const char* date, float price)
{
// -> 解释:
// book 是指针,不能用 book.price
// 必须用 book->title 表示:指针指向的结构体变量的成员
// 等价于 (*book).title,但是 -> 更简洁
// strCopy是Mars中自定义的静态函数
strCopy(book->title, title);
strCopy(book->author, author);
strCopy(book->press, press);
strCopy(book->date, date);
book->price = price;
}
// 2. 打印书籍信息
void printBook(struct Book* book)
{
printf("书名: %s\n", book->title);
printf("作者: %s\n", book->author);
printf("出版社: %s\n", book->press);
printf("出版时间: %s\n", book->date);
printf("定价: %.2f\n\n", book->price);
}
// 3. 修改书籍定价
void setPrice(struct Book* book, float newPrice)
{
book->price = newPrice;
}
int main()
{
// 定义一个普通结构体变量 book1
struct Book book1;
// &book1:取变量的地址,传给函数
// 函数接收的是指针,所以传 &
initBook(&book1, "换个姿势学C语言", "何旭辉", "清华大学出版社", "2022-09-01", 118.00);
printf("===== 书籍1 =====\n");
printBook(&book1);
setPrice(&book1, 120.00);
printf("===== 书籍1修改定价后信息如下 =====\n");
printBook(&book1);
return 0;
}
|
运行程序,输出如下:

以上我们知道了结构体的简单用法,下面回到书中,跟作者一起学习如果获取外币牌价信息并保存到文件中。
7.1 使用结构体存储不同类型的多项数据
在第6章中,我们使用两个字符数组分别存储货币的名称和发布时间,,用一个double型数组分别存储某种外币的现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和中行折算价 。函数GetRatesAndCurrencyNameByCode从服务器上获取最新的牌价并存入这些数组中。
详见5.4节 第5章 获取完整的牌价数据 5.4节 获取和显示货币名称,当时的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // L05_19_GET_RATES_AND_CURRENCY_NAME_BY_CODE.cpp
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")
int main()
{
// 存储货币的五种价格
double rates[5] = { 0 };
// 货币名称
char currencyName[33] = { 0 };
// 发布时间
char publishTime[20] = { 0 };
int result = GetRatesAndCurrencyNameByCode("USD", currencyName, publishTime, rates);
printf("%d\n", result);
if (result == 1) {
printf("货币名称:%s\n", currencyName);
printf("发布时间:%s\n", publishTime);
printf("现汇买入价:%.2f\n", rates[0]);
printf("现钞买入价:%.2f\n", rates[1]);
printf("现汇卖出价:%.2f\n", rates[2]);
printf("现钞卖出价:%.2f\n", rates[3]);
printf("中行折算价:%.2f\n", rates[4]);
}
else {
printf("网络或服务器异常\n");
}
return 0;
}
|
这3个数组在逻辑上是相关的,但在代码上是独立的。如果需要向一个函数传递这3个数组,就必须要给函数设计3个参数,这会让程序变得冗长。更麻烦的是,如果未来需要新增一个变量或数组用于描述货币的其他信息,几乎需要修改所有相关的函数——为它们增加参数。
在设计程序的数据结构时,我们应尽量将一组相关的数据作为一个整体来处理,尽量避免“散装”。数组是将多项相关类型的数据集合在一起的方式,而结构体是将多个不同类型的数据项“打包”到一起的方法。
一种外币的信息可以用4个数组来描述:
1
2
3
4
5
6
7
8
| // 存储货币的五种价格
double rates[5] = { 0 };
// 货币名称
char currencyName[33] = { 0 };
// 发布时间
char publishTime[20] = { 0 };
// 货币代码
char currencyCode[4] = { 0 };
|
我们可以使用结构体将这4个数组组合到一起定义成一种新的数据类型,新的数据可以将这些数组整合成一个整体以便对一组数据进行操作。可以把结构体理解成新的变量模板,就和之前的int、float和double一样,所不同的是在这种模板中可以存储多项数据。
7.1.1 定义结构体
定义结构体相当于定义一种数据类型的模板。未来可以基于这个模板来声明变量。定义结构体的方法很简单:
1
2
3
4
5
6
| struct 结构体名称
{
类型 成员名称1;
类型 成员名称2;
...
};
|
定义结构体的规则是:
- 以struct开始,其后是结构体名称。结构体名称根据用途确定,例如存储学生数据就叫
Student,存储员工数据就用Employee,存储牌价数据的结构体就可以叫ExchangeRate。 - 接下来是一对大括号,大括号内列出结构体成员。结构体成员可以是变量,数组,每一个成员占用一行,行末用分号结束。
- 结构体结束的右大括号后应有分号。
在大括号内应像平时一样声明结构体变量的成员。
下面的结构体用于存储某种货币的牌价信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // L07_01_STRUCT.cpp
#include <stdio.h>
struct EXCHANGE_RATE
{
char CurrencyCode[4]; // 货币代码
char CurrencyName[33]; // 货币名称(中文)
char PublishTime[20]; // 发布时间
double BuyingRate = 0; // 现汇买入价
double CashBuyingRate = 0; // 现钞买入价
double SellingRate = 0; // 现汇卖出价
double CashSellingRate = 0; // 现钞卖出价
double MiddleRate = 0; // 中行折算价
};
int main()
{
struct EXCHANGE_RATE er;
er.BuyingRate = 12.3;
printf("现汇买入价: %f\n", er.BuyingRate);
return 0;
}
|
这里说明一下,以上代码中定义的结构体变量struct EXCHANGE_RATE变量名是全大写的,实际工程实践不建议这样。
ℹ️ 信息
- 课程里大量用全大写(STUDENT、BOOK)
正常,因为:
- 国内很多 C 语言教材、老教师、老课程习惯用全大写
- 他们觉得大写醒目、好区分
- 教学场景怎么清楚怎么来,不讲究工程规范
1
2
3
| typedef struct BOOK {
...
} BOOK;
|
因此,上面这样定义 非常正常,完全是教学风格。
- 但企业 / 真实项目(Redis、Linux、Nginx)
几乎不用全大写!
- 全大写约定只给宏(#define)用
- 结构体大写会被认为不规范、容易冲突
企业真实风格是:
1
2
3
4
5
| // 公共结构体
typedef struct Book { ... } Book;
// 内部底层结构体
typedef struct book_s { ... } book_t;
|
并且在Visual Studio 2022中直接输入struct关键字,然后按Tab自动补全是这样的:
1
2
3
4
| struct MyStruct
{
};
|
即结构体名称也没有全用大写,而是用的驼峰首字母大写的形式。
为了与课本一致,我测试也用全大写作为结构体名称!!
需要说明的是,在定义这个结构体时:
- 不再使用
double rates[5]双精度型数组存储5个价格,而是单独定义了5个双精度型变量分别描述这5个价格,这样做是为了提高程序的可读性。 - 为货币代码、货币名称和发布时间定义了3个字符型数组。
- 结构体的名称全部采用大写字母,单词之间用下划线分隔。
结构体的详细说明如下:
CurrencyCode货币代码,货币代码为3个字母(如EUR、CNY等),可以用字符数组存储。为了输出的方便,应为字符串终止符"\0"预留一字节,因此字符数组长度应为4。CurrencyName货币名称,货币名称采用汉字,可以使用字符数组存储,字符数组长度设为33字节,最多允许16个汉字,为字符串终止符"\0"预留一字节。PublishTime发布时间,发布时间为字符串,原始数据格式如“2020-12-12 00:00:05”,这种日期格式需要19字节存储,同时为字符串终止符"\0"预留一字节,所以字符数组长度应为20。BuyingRate现汇买入价,采用双精度浮点型。CashBuyingRate现钞买入价,采用双精度浮点型。SellingRate现汇卖出价,采用双精度浮点型。CashSellingRate现钞卖出价,采用双精度浮点型。MiddleRate中行折算价,采用双精度浮点型。
至此,我们就完成了结构体的定义!
⚠️ 警告
- 注意结构体定义右大括号后面要有分号
;。 - 不要把结构体定义写在任何函数内部,它应该是独立的,不被任何函数包含的!