×
Featured image of post 换个姿势学C语言第7.4节 获取和保存全部外币牌价数据

换个姿势学C语言第7.4节 获取和保存全部外币牌价数据

换个姿势学C语言 第7章 获取全部外币牌价数据并保存为文件

0. 说明

《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

Snipaste_2024-03-10_14-51-10.png

这是一本非常不错的书!

7. 获取全部外币牌价数据并保存为文件

变量、数组都是存储在内存RAM中的,这些数据所占用的内存在程序结束以后会被操作系统回收,其中的数据也就丢失了。因此我们需要将数据保存到外部存储器上(通常是硬盘),以便下次使用。

在一些较底层的语言里,程序员可以直接访问硬盘的某个扇区并进行数据读写,但这种方式一般不被推荐,因为这种方式除了效率比较低外还具有较大的危险,不恰当的磁盘访问可能会引起严重的故障(例如操作系统崩溃或者数据丢失)。

因此通常是以“文件”来组织磁盘上的数据。文件系统由操作系统管理,程序员通过操作系统间接地访问磁盘上的数据,不恰当的文件访问会被操作系统阻止(例如文件被其他程序占用或程序没有访问这个文件的权限),这样一来就安全得多,同时操作系统也会采取一些机制来提高文件访问的效率。

本章将会将取得的外汇牌价数据保存到磁盘文件中,但是在学习磁盘文件访问之前先学习结构体的使用方法。

结构体可以将多种不同类型的数据“组合”到一起,然后再将其存储到磁盘文件中。

7.4 获取和保存全部外币牌价数据

现在我们已经可以一次获取某一种外币的牌价数据,并将其显示或保存起来,也可以从文件中读取已保存的牌价数据。但是,在外汇牌价看板中需要显示多种(书上说的是26种)外汇牌价,它们的名称和代码是什么?

以下是根据中国银行官网外汇牌价页面整理的常见货币名称及其对应的国际标准货币代码(ISO 4217) 。

人民币的货币代码是CNY。详细可参考 中国银行外汇牌价 https://www.boc.cn/sourcedb/whpj/

序号货币名称货币代码
1美元USD
2港币HKD
3澳大利亚元AUD
4欧元EUR
5日元JPY
6英镑GBP
7加拿大元CAD
8新加坡元SGD
9新西兰元NZD
10泰国铢THB
11韩国元KRW
12澳门元MOP
13瑞士法郎CHF
14瑞典克朗SEK
15卢布RUB(俄罗斯卢布)
16丹麦克朗DKK
17印尼卢比IDR
18挪威克朗NOK
19阿联酋迪拉姆AED
20新台币TWD
21菲律宾比索PHP
22南非兰特ZAR
23沙特里亚尔SAR
24巴西雷亚尔BRL
25土耳其里拉TRY
26印度卢比INR
27越南盾VND
28蒙古图格里克MNT
29林吉特MYR(马来西亚林吉特)
30尼泊尔卢比NPR
31捷克克朗CZK
32卡塔尔里亚尔QAR
33以色列谢克尔ILS
34墨西哥比索MXN
35匈牙利福林HUF
36科威特第纳尔KWD
37巴基斯坦卢比PKR
38柬埔寨瑞尔KHR
39文莱元BND
40塞尔维亚第纳尔RSD

也就是说当前有40种外币!

牌价接口库提供了一次性取得全部外币牌价的函数,但我们现在不打算使用它。

如果只允许使用现有的知识和信息,并且只允许使用GetRateRecordByCode函数,如何实现获得全部外币牌价的功能呢?

7.4.1 使用结构体数组存储多种外币牌价

针对之前没有先例的任务,读者很可能又陷入没有思路的困境。

当编程没有思路的时候考虑这样几个问题:

  • 数据在内存中如何存储?
  • 要处理的数据从哪里来?
  • 如何处理(计算)数据?
  • 如何输出数据?

我2026年查看外汇牌价的时候有40种外币,书上还是26种,我们还是按书上26种货币牌价来思考。

先来思考如何存储26种货币的牌价。

无论如何我们也不愿意定义26个结构体变量来存储26种外币的牌价,读者可能已经想到了——使用数组。

数组的实质就是多个相同类型变量的集合,集合的每一个元素就是一个变量。

我们可以基于结构体变量数组,如:

1
struct EXCHANGE_RATE allRates[26];

如果已经基于结构体定义了新的数据类型名,也可以直接使用新的类型名:

1
ExchangeRate allRates[26];

注意,在BOCRates.h中已经有结构体的定义:

1
typedef struct EXCHANGE_RATE ExchangeRate;

此处的allRates是数组 名,这相当于在内存中分配了26*104字节的内存空间,结构体数组按照顺序存储这26个结构体变量,换句话说,第1-104字节被第1个数组元素占用,第105-208字节被第2个数组元素占用,…,以此类推。

要访问其中任何一个结构体元素,和访问普通数组元素一样使用中括号和索引即可。可以把allRates[0]当作一个结构体变量名来操作。需要记住的是,由于结构体变量是有成员的,所以使用点运算符.来操作结构体变量的成员。

下面的代码表示给结构体数组中第1个元素(索引值为0)的BuyingRate成员变量赋值。

 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
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加 D:/BC101/Libraries/Mars
// 此处引入自定义的静态库的头文件需要相对路径
#include "str.h"

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径/Mars.lib":库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib,"Mars.lib")
#pragma comment(lib, "BOCRates.lib")

int main()
{
    ExchangeRate allRates[26];
    allRates[0].BuyingRate = 654.07;
    strCopy(allRates[0].CurrencyName, "美元");
    printf("货币名称:%s\n", allRates[0].CurrencyName);
    printf("现汇买入价:%.2f\n", allRates[0].BuyingRate);

    return 0;
}

运行程序输出如下:

Snipaste_2026-05-09_20-47-41.png

7.4.2 取得外币牌价并存入结构体数组

现在,已经准备好了可以存储26种外币牌价数据的结构体数组allRates,接下来就要考虑如何获取26种外币牌价数据并依次存入结构体数组allRates中。

不要试图一步到位地写出最终可用代码。

不如先思考如何获取美元牌价并存入allRates[0]中,之前我们学习过,调用GetRateRecordByCode函数时,只要将货币代码和结构体变量的地址作为参数传入,就可以获取这种货币的全部牌价数据并自动填充结构体变量,之前使用它的方式是:

1
2
3
4
5
6
7
// 定义外汇牌价结构体变量
ExchangeRate USDRates;
// 货币代码
char code[4];
strCopy(code, "USD");

int result = GetRateRecordByCode(code, &USDRates);

详见L07_09_SAVE_USD_RATES.cpp文件。

此处的USDRates是结构体变量,在它前面加上&运算符就可以取到它的内存地址,向GetRateRecordByCode函数传入结构体变量USDRates的地址,函数内部的代码负责向结构体变量中存入最新的牌价数据。

现在,我们希望GetRateRecordByCode能够向结构体数组元素allRates[0]中存入最新的美元牌价数据,同样也可以使用&运算符,如下:

1
2
3
4
5
// 定义外汇牌价结构体变量数组
ExchangeRate allRates[26];

// 获取美元牌价数据
GetRateRecordByCode("USD", &allRates[0]);

GetRateRecordByCode("USD", &allRates[0]);中的&allRates[0]取得结构体数组第1个元素的地址,这样就可以将美元牌价数据存入到allRates[0]中。

 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
// L07_11_READ_RATES_TO_STRUCTURE_ARRAY.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void print_rate_info(const EXCHANGE_RATE* rate);

int main()
{
    // 定义外汇牌价结构体数组变量
    ExchangeRate allRates[26];

    // 美元
    GetRateRecordByCode("USD", &allRates[0]);
    print_rate_info(&allRates[0]);

    // 港元
    GetRateRecordByCode("HKD", &allRates[1]);
    print_rate_info(&allRates[1]);

    return 0;
}

// 打印外汇牌价结构体信息
void print_rate_info(const EXCHANGE_RATE* rate)
{
    printf("货币代码:%s\n", rate->CurrencyCode);
    printf("货币名称:%s\n", rate->CurrencyName);
    printf("发布时间:%s\n", rate->PublishTime);
    printf("现汇买入价:%.2f\n", rate->BuyingRate);
    printf("现钞买入价:%.2f\n", rate->CashBuyingRate);
    printf("现汇卖出价:%.2f\n", rate->SellingRate);
    printf("现钞卖出价:%.2f\n", rate->CashSellingRate);
    printf("中行折算价:%.2f\n", rate->MiddleRate);
}

运行程序输出如下:

Snipaste_2026-05-09_22-01-52.png

这个位置我们使用了之前就封闭好的函数print_rate_info,输出外汇牌价结构体信息。

可以看到,正常给第1个元素allRates[0]和第2个元素allRates[1]写入了外汇牌价信息,并正常输出了!

7.4.3 将显示外币牌价的代码封闭成函数

print_rate_info是我们之前做的封闭,考虑到输出外币牌价信息是经常要进行的工作,我们来重新封闭一个函数displayRate来打印外汇牌价信息。

优化后的代码如下:

 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
// L07_11_READ_RATES_TO_STRUCTURE_ARRAY.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate);

int main()
{
    // 定义外汇牌价结构体数组变量
    ExchangeRate allRates[26];

    // 美元
    GetRateRecordByCode("USD", &allRates[0]);
    displayRate(&allRates[0]);

    // 港元
    GetRateRecordByCode("HKD", &allRates[1]);
    displayRate(&allRates[1]);

    return 0;
}

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码
    printf("%s\t", rate->CurrencyCode);
    // 货币名称
    printf("%s\t", rate->CurrencyName);
    // 发布时间
    printf("%s\t", rate->PublishTime);
    // 现汇买入价
    printf("%.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%.2f\n", rate->MiddleRate);
}

然后运行代码:

Snipaste_2026-05-09_22-14-52.png

可以看到,一种外汇牌价信息显示在一行了!

注意,我这里没用用书上的ExchangeRate rate作为displayRate函数的参数,而是用ExchangeRate* rate作为参数,是因为通过传递ExchangeRate* rate结构体指针,有以下特点:

  • 只传 8 字节地址

  • 不复制结构体

  • 速度极快

  • 加了 const → 不能改原数据,安全

  • 访问用 rate->xxx

而如果用书上定义的那样void displayRate(ExchangeRate rate);,则有以下特点:

  • 把整个结构体完整复制一份
  • 结构体越大,浪费越多
  • 函数内部是副本,改了不影响外面
  • 访问用 rate.xxx
  • 低效、不规范

模块化程序设计是指在进行程序设计时将一个“大程序”按照功能划分为若干个“小程序”,每个小程序完成一个确定的功能,并在这些小程序之间建立必要的联系。通过小程序的互相协作完成整个功能的程序设计方法!

7.4.4 获取全部外币牌价

上面的程序,我们输入了两次货币代码,我们尝试将26种都带输出一下试试。

将之前的外汇代码表存放到data.txt中,其内容如下:

 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
序号	货币名称	货币代码
1	美元	USD
2	港币	HKD
3	澳大利亚元	AUD
4	欧元	EUR
5	日元	JPY
6	英镑	GBP
7	加拿大元	CAD
8	新加坡元	SGD
9	新西兰元	NZD
10	泰国铢	THB
11	韩国元	KRW
12	澳门元	MOP
13	瑞士法郎	CHF
14	瑞典克朗	SEK
15	卢布	RUB
16	丹麦克朗	DKK
17	印尼卢比	IDR
18	挪威克朗	NOK
19	阿联酋迪拉姆	AED
20	新台币	TWD
21	菲律宾比索	PHP
22	南非兰特	ZAR
23	沙特里亚尔	SAR
24	巴西雷亚尔	BRL
25	土耳其里拉	TRY
26	印度卢比	INR
27	越南盾	VND
28	蒙古图格里克	MNT
29	林吉特	MYR
30	尼泊尔卢比	NPR
31	捷克克朗	CZK
32	卡塔尔里亚尔	QAR
33	以色列谢克尔	ILS
34	墨西哥比索	MXN
35	匈牙利福林	HUF
36	科威特第纳尔	KWD
37	巴基斯坦卢比	PKR
38	柬埔寨瑞尔	KHR
39	文莱元	BND
40	塞尔维亚第纳尔	RSD

然后使用命令行工具帮我们生成代码。

  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
$ cat data.txt |awk '{print "// "$2"\nGetRateRecordByCode(\""$3"\",&allRates["NR-2"]);\ndisplayRate(&allRates["NR-2"]);"}'
// 货币名称
GetRateRecordByCode("货币代码",&allRates[-1]);
displayRate(&allRates[-1]);
// 美元
GetRateRecordByCode("USD",&allRates[0]);
displayRate(&allRates[0]);
// 港币
GetRateRecordByCode("HKD",&allRates[1]);
displayRate(&allRates[1]);
// 澳大利亚元
GetRateRecordByCode("AUD",&allRates[2]);
displayRate(&allRates[2]);
// 欧元
GetRateRecordByCode("EUR",&allRates[3]);
displayRate(&allRates[3]);
// 日元
GetRateRecordByCode("JPY",&allRates[4]);
displayRate(&allRates[4]);
// 英镑
GetRateRecordByCode("GBP",&allRates[5]);
displayRate(&allRates[5]);
// 加拿大元
GetRateRecordByCode("CAD",&allRates[6]);
displayRate(&allRates[6]);
// 新加坡元
GetRateRecordByCode("SGD",&allRates[7]);
displayRate(&allRates[7]);
// 新西兰元
GetRateRecordByCode("NZD",&allRates[8]);
displayRate(&allRates[8]);
// 泰国铢
GetRateRecordByCode("THB",&allRates[9]);
displayRate(&allRates[9]);
// 韩国元
GetRateRecordByCode("KRW",&allRates[10]);
displayRate(&allRates[10]);
// 澳门元
GetRateRecordByCode("MOP",&allRates[11]);
displayRate(&allRates[11]);
// 瑞士法郎
GetRateRecordByCode("CHF",&allRates[12]);
displayRate(&allRates[12]);
// 瑞典克朗
GetRateRecordByCode("SEK",&allRates[13]);
displayRate(&allRates[13]);
// 卢布
GetRateRecordByCode("RUB",&allRates[14]);
displayRate(&allRates[14]);
// 丹麦克朗
GetRateRecordByCode("DKK",&allRates[15]);
displayRate(&allRates[15]);
// 印尼卢比
GetRateRecordByCode("IDR",&allRates[16]);
displayRate(&allRates[16]);
// 挪威克朗
GetRateRecordByCode("NOK",&allRates[17]);
displayRate(&allRates[17]);
// 阿联酋迪拉姆
GetRateRecordByCode("AED",&allRates[18]);
displayRate(&allRates[18]);
// 新台币
GetRateRecordByCode("TWD",&allRates[19]);
displayRate(&allRates[19]);
// 菲律宾比索
GetRateRecordByCode("PHP",&allRates[20]);
displayRate(&allRates[20]);
// 南非兰特
GetRateRecordByCode("ZAR",&allRates[21]);
displayRate(&allRates[21]);
// 沙特里亚尔
GetRateRecordByCode("SAR",&allRates[22]);
displayRate(&allRates[22]);
// 巴西雷亚尔
GetRateRecordByCode("BRL",&allRates[23]);
displayRate(&allRates[23]);
// 土耳其里拉
GetRateRecordByCode("TRY",&allRates[24]);
displayRate(&allRates[24]);
// 印度卢比
GetRateRecordByCode("INR",&allRates[25]);
displayRate(&allRates[25]);
// 越南盾
GetRateRecordByCode("VND",&allRates[26]);
displayRate(&allRates[26]);
// 蒙古图格里克
GetRateRecordByCode("MNT",&allRates[27]);
displayRate(&allRates[27]);
// 林吉特
GetRateRecordByCode("MYR",&allRates[28]);
displayRate(&allRates[28]);
// 尼泊尔卢比
GetRateRecordByCode("NPR",&allRates[29]);
displayRate(&allRates[29]);
// 捷克克朗
GetRateRecordByCode("CZK",&allRates[30]);
displayRate(&allRates[30]);
// 卡塔尔里亚尔
GetRateRecordByCode("QAR",&allRates[31]);
displayRate(&allRates[31]);
// 以色列谢克尔
GetRateRecordByCode("ILS",&allRates[32]);
displayRate(&allRates[32]);
// 墨西哥比索
GetRateRecordByCode("MXN",&allRates[33]);
displayRate(&allRates[33]);
// 匈牙利福林
GetRateRecordByCode("HUF",&allRates[34]);
displayRate(&allRates[34]);
// 科威特第纳尔
GetRateRecordByCode("KWD",&allRates[35]);
displayRate(&allRates[35]);
// 巴基斯坦卢比
GetRateRecordByCode("PKR",&allRates[36]);
displayRate(&allRates[36]);
// 柬埔寨瑞尔
GetRateRecordByCode("KHR",&allRates[37]);
displayRate(&allRates[37]);
// 文莱元
GetRateRecordByCode("BND",&allRates[38]);
displayRate(&allRates[38]);
// 塞尔维亚第纳尔
GetRateRecordByCode("RSD",&allRates[39]);
displayRate(&allRates[39]);

然后将这些组装好的代码片段放到程序代码中,修改后的代码如下:

  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// L07_11_READ_RATES_TO_STRUCTURE_ARRAY.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate);

int main()
{
    // 定义外汇牌价结构体数组变量
    ExchangeRate allRates[40];

    // 美元
    GetRateRecordByCode("USD", &allRates[0]);
    displayRate(&allRates[0]);
    // 港币
    GetRateRecordByCode("HKD", &allRates[1]);
    displayRate(&allRates[1]);
    // 澳大利亚元
    GetRateRecordByCode("AUD", &allRates[2]);
    displayRate(&allRates[2]);
    // 欧元
    GetRateRecordByCode("EUR", &allRates[3]);
    displayRate(&allRates[3]);
    // 日元
    GetRateRecordByCode("JPY", &allRates[4]);
    displayRate(&allRates[4]);
    // 英镑
    GetRateRecordByCode("GBP", &allRates[5]);
    displayRate(&allRates[5]);
    // 加拿大元
    GetRateRecordByCode("CAD", &allRates[6]);
    displayRate(&allRates[6]);
    // 新加坡元
    GetRateRecordByCode("SGD", &allRates[7]);
    displayRate(&allRates[7]);
    // 新西兰元
    GetRateRecordByCode("NZD", &allRates[8]);
    displayRate(&allRates[8]);
    // 泰国铢
    GetRateRecordByCode("THB", &allRates[9]);
    displayRate(&allRates[9]);
    // 韩国元
    GetRateRecordByCode("KRW", &allRates[10]);
    displayRate(&allRates[10]);
    // 澳门元
    GetRateRecordByCode("MOP", &allRates[11]);
    displayRate(&allRates[11]);
    // 瑞士法郎
    GetRateRecordByCode("CHF", &allRates[12]);
    displayRate(&allRates[12]);
    // 瑞典克朗
    GetRateRecordByCode("SEK", &allRates[13]);
    displayRate(&allRates[13]);
    // 卢布
    GetRateRecordByCode("RUB", &allRates[14]);
    displayRate(&allRates[14]);
    // 丹麦克朗
    GetRateRecordByCode("DKK", &allRates[15]);
    displayRate(&allRates[15]);
    // 印尼卢比
    GetRateRecordByCode("IDR", &allRates[16]);
    displayRate(&allRates[16]);
    // 挪威克朗
    GetRateRecordByCode("NOK", &allRates[17]);
    displayRate(&allRates[17]);
    // 阿联酋迪拉姆
    GetRateRecordByCode("AED", &allRates[18]);
    displayRate(&allRates[18]);
    // 新台币
    GetRateRecordByCode("TWD", &allRates[19]);
    displayRate(&allRates[19]);
    // 菲律宾比索
    GetRateRecordByCode("PHP", &allRates[20]);
    displayRate(&allRates[20]);
    // 南非兰特
    GetRateRecordByCode("ZAR", &allRates[21]);
    displayRate(&allRates[21]);
    // 沙特里亚尔
    GetRateRecordByCode("SAR", &allRates[22]);
    displayRate(&allRates[22]);
    // 巴西雷亚尔
    GetRateRecordByCode("BRL", &allRates[23]);
    displayRate(&allRates[23]);
    // 土耳其里拉
    GetRateRecordByCode("TRY", &allRates[24]);
    displayRate(&allRates[24]);
    // 印度卢比
    GetRateRecordByCode("INR", &allRates[25]);
    displayRate(&allRates[25]);
    // 越南盾
    GetRateRecordByCode("VND", &allRates[26]);
    displayRate(&allRates[26]);
    // 蒙古图格里克
    GetRateRecordByCode("MNT", &allRates[27]);
    displayRate(&allRates[27]);
    // 林吉特
    GetRateRecordByCode("MYR", &allRates[28]);
    displayRate(&allRates[28]);
    // 尼泊尔卢比
    GetRateRecordByCode("NPR", &allRates[29]);
    displayRate(&allRates[29]);
    // 捷克克朗
    GetRateRecordByCode("CZK", &allRates[30]);
    displayRate(&allRates[30]);
    // 卡塔尔里亚尔
    GetRateRecordByCode("QAR", &allRates[31]);
    displayRate(&allRates[31]);
    // 以色列谢克尔
    GetRateRecordByCode("ILS", &allRates[32]);
    displayRate(&allRates[32]);
    // 墨西哥比索
    GetRateRecordByCode("MXN", &allRates[33]);
    displayRate(&allRates[33]);
    // 匈牙利福林
    GetRateRecordByCode("HUF", &allRates[34]);
    displayRate(&allRates[34]);
    // 科威特第纳尔
    GetRateRecordByCode("KWD", &allRates[35]);
    displayRate(&allRates[35]);
    // 巴基斯坦卢比
    GetRateRecordByCode("PKR", &allRates[36]);
    displayRate(&allRates[36]);
    // 柬埔寨瑞尔
    GetRateRecordByCode("KHR", &allRates[37]);
    displayRate(&allRates[37]);
    // 文莱元
    GetRateRecordByCode("BND", &allRates[38]);
    displayRate(&allRates[38]);
    // 塞尔维亚第纳尔
    GetRateRecordByCode("RSD", &allRates[39]);
    displayRate(&allRates[39]);

    return 0;
}

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码
    printf("%s\t", rate->CurrencyCode);
    // 货币名称
    printf("%s\t", rate->CurrencyName);
    // 发布时间
    printf("%s\t", rate->PublishTime);
    // 现汇买入价
    printf("%.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%.2f\n", rate->MiddleRate);
}

然后,再运行代码:

Snipaste_2026-05-09_22-39-22.png

现在,我们通过这种"笨办法"获取到了所有的外汇牌价数据,先不用在乎输出结果中的-1.00以及NaN等信息,但此时可以看出来牌价信息输出比较混乱,列数据没对齐,来优化一下代码,但在printf中限定字符的宽度!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码   (固定6字符,左对齐)
    printf("%-6s\t", rate->CurrencyCode);
    // 货币名称  (固定16字符,左对齐)
    printf("%-16s\t", rate->CurrencyName);
    // 发布时间   (固定20字符,左对齐)
    printf("%-20s\t", rate->PublishTime);
    // 现汇买入价   (8字符,2位小数)
    printf("%8.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%8.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%8.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%8.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%8.2f\n", rate->MiddleRate);
}

然后再次运行程序,可以看到,现在每列数据都正常对齐了!!

Snipaste_2026-05-09_22-58-21.png

此时,可以观察到,从美元开始,到最后的塞尔维亚第纳尔,获取外汇牌价信息,不停的在重复代码:

1
2
GetRateRecordByCode("USD", &allRates[0]);
displayRate(&allRates[0]);

只是将"USD"换成了别的货币代码,以及索引号从0不停的上升,到最后的39

这种情况就最适合做循环处理。

现在只用定义一个数组,将所有的货币代码存储起来,然后再使用循环读取和输出就行了,以下是优化后的代码:

 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
// L07_11_READ_RATES_TO_STRUCTURE_ARRAY.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate);

int main()
{
    // 定义外汇牌价结构体数组变量
    ExchangeRate allRates[40];

    // 定义货币代码数组,是一个指针数组,
    // 每个元素是一个字符串地址,字符串放哪里都行,多长都行
    const char* currencyCodes[] = {
        "USD", "HKD", "AUD", "EUR", "JPY", "GBP", "CAD", "SGD",
        "NZD", "THB", "KRW", "MOP", "CHF", "SEK", "RUB", "DKK",
        "IDR", "NOK", "AED", "TWD", "PHP", "ZAR", "SAR", "BRL",
        "TRY", "INR", "VND", "MNT", "MYR", "NPR", "CZK", "QAR",
        "ILS", "MXN", "HUF", "KWD", "PKR", "KHR", "BND", "RSD"
    };

    int count = sizeof(currencyCodes) / sizeof(currencyCodes[0]);

    // 限制最多输出40行数据
    if (count > 40) {
        count = 40;
    }

    // 循环获取和打印所有的外汇信息
    for (int i = 0; i < count; i++) {
        GetRateRecordByCode(currencyCodes[i], &allRates[i]);
        displayRate(&allRates[i]);
    }

    return 0;
}

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码   (固定6字符,左对齐)
    printf("%-6s\t", rate->CurrencyCode);
    // 货币名称  (固定16字符,左对齐)
    printf("%-16s\t", rate->CurrencyName);
    // 发布时间   (固定20字符,左对齐)
    printf("%-20s\t", rate->PublishTime);
    // 现汇买入价   (8字符,2位小数)
    printf("%8.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%8.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%8.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%8.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%8.2f\n", rate->MiddleRate);
}

再次运行,输出结果与之前一样!!!但代码量少了很多!

但现在又有个问题,我们在运行代码时,可以看到每一行输出中间有停顿,原因是:

  • GetRateRecordByCode获取一个货币代码的外汇数据信息后,又要再次调用GetRateRecordByCode与服务器端再次连接,需要进行40次!中间就有时间差!
  • 如果中行的外汇交易种类发了变化(增加或减少),如书上当时是26中,我测试的时候是40种!此时客户端程序需要修改和重新安装,这就会增加系统出故障的风险和运维成本!!

牌价接口库的作者应该提供更友好的方式帮助我们取得全部外汇牌价信息!

7.4.5 一次获取全部牌价

这时可以去和牌价接口库的作者沟通,表达“我们希望有一个新的函数帮助我们一次性获取全部外币牌价”的愿望。

约定函数名称是GetAllRates,调用它可以实现:

  • 返回外币种类总数(整型值);
  • 将全部外币牌价数据填充到一个结构体数组中。

调用时使用的代码是最简单的:

1
2
ExchangeRate allRates[26];
int count = GetAllRates(allRates);

第1行代码定义一个结构体数组,第2行代码调用GetAllRates函数并将数组首地址传入其中,要求这个函数能将获得的全部牌价填充到数组里,调用完成后将外币种类总数返回给变量count.

但这样做实现上是有问题的,数组allRates的大小是固定的26,即它只能容纳26种外币牌价数据。但上一节我们看到了,2026年05月09日时,牌价数量都到40种了!就有可能出现数组越界的问题,所以这种设计不能满足要求!!!

需要另外想办法,一种妥协的方案是:

  • 先调用一个函数获取外币总数;
  • 再根据这个数量声明结构体数组的大小。

代码如下:

1
2
3
int count = GetCurrencyCount();   // 先获取外币总数
ExchangeRate allRates[count];  // 再根据外币总数声明数组
GetAllRates(allRates);  // 然后将数组地址传给GetAllRates函数

这种思路是正确的,但由于C语言限制,上面的代码实际上不能通过编译——不可能使用变量定义数组的大小。

如,我们将上一节的代码中40换成一个变量num,然后编译马上就会异常:

Snipaste_2026-05-10_12-56-50.png

提示E0028异常,表达式必须含有常量值。

这个问题比较容易解决,因为可以使用动态分配的内存来伪造数组,将代码改成下面这样就行了:

1
2
3
int count = GetCurrencyCount();   // 先获取外币总数
ExchangeRate* allRates = (ExchangeRate*)malloc(sizeof(ExchangeRate)*count); // 使用动态内存分配
GetAllRates(allRates);  // 然后将数组地址传给GetAllRates函数

但这种做法仍然存在缺陷,表现在:

  • 需要分两次调用GetCurrencyCountGetAllRates函数,这需要与服务器之间进行两次数据传输,会浪费时间。
  • 两个函数调用之间存在时间差,不排除调用GetCurrencyCount函数得到结果之后,GetAllRates函数调用之前外币数量发生变化的极端情况,在这种情况下,可能发生调用GetCurrencyCount函数得到的值是26,而GetAllRates函数填充的数据不是26种外币牌价的情况!

这种极端情况在外汇牌价看板系统中发生的概率小到可以忽略,但在其他系统中则比较常见。例如电商系统在秒杀活动时订单总量的变化非常快,如果这样设计程序可能就会出错。程序员对此一定要有风险意识和防范措施!

因此,希望在调用GetAllRates函数前无须获取外币的总数来分配内存,而是让GetAllRates函数能够一次性解决问题——根据实际情况分配内存并填充数据(类似让加油站给我们准备好合适的容器并加满油),㧈指针返回给我们就行了,GetAllRates函数似乎应该这样设计:

1
ExchangeRate* allRates = GetAllRates();

GetAllRates函数自行分配内存并填充数据,将数据的首地址以返回值的形式赋值给指针变量allRates,这么做毫无问题。

但只有数据首地址是不够的,我们要知道外币总数。如何让GetAllRates函数既能返回存储外币牌价的结构体数组首地址,又能返回外币总数呢?我们知道**return语句一次只能返回一个结果**,但我们可以这样考虑:

将一个整型变量的地址传递给GetAllRates函数,GetAllRates函数不就可以改变它的值了吗?这样就无须return语句返回两个值了,原型就变成这样了:

1
ExchangeRate* allRates = GetAllRates(int* count);

调用代码如下:

1
2
int count = 0;
ExchangeRate* allRates = GetAllRates(&count);

这样做确实可以,它突破了“return语句只能返回一个结果”的限制。我们之前写的程序其实也用过这种做法——将数组或变量的地址作为参数传递到函数中,函数内容就可以改造它们的值了!!

但是一般程序员更习惯于让return返回简单的数据,换句话说,外币总数最好用return返回,而指针变量allRates的值则由函数参数来控制,那么我们将函数原型定义为:

1
int GetAllRates(ExchangeRate* result);

调用函数时这样做:

1
2
ExchangeRate* allRates = NULL;
int count = GetAllRates(allRates);

第2行代码㧈指针变量allRates传递给函数GetAllRates,让它在内部分配空间并让allRates指向这块空间就可以了!但是此处又有坑,无论GetAllRates函数内部写什么样的代码,都不可能改变程序中指针变量allRates的值,这是因为传递到函数GetAllRates函数中的内容实际上是指针变量allRates的值,这个值目前是NULL

要想让GetAllRates函数内部可以改变指针变量allRates的值,只能把指针变量allRates的地址传递给它,调用函数时要加上取址运算符:

1
2
ExchangeRate* allRates = NULL;
int count = GetAllRates(&allRates);

此时传递给函数的是指针变量allRates的地址,函数内部就可以改变allRates的值了!

这时GetAllRates函数的原型就变成 了这样:

1
int GetAllRates(ExchangeRate** result);

此处的ExchangeRate**表示这个参数是一个指针变量的地址,使用了连续的两个*,至此我们就相当于和实现牌价接口库的程序员协商了一个接口,函数具体怎么实现就可以交给他了!

需要特别注意的是,GetAllRates函数内部也是用malloc函数分配所需要内存的,分配并填充数据后,我们程序中的指针变量allRates会指向这块空间,因此GetAllRates函数执行完毕不能释放这块空间(空间被释放数据就会被破坏),释放的工作要交给我们的程序来完成。

虽然这违背了**“谁分配,谁释放”的原则**,但这是为了提高程序的效率所做出的妥协,也实在没有更好的办法。我们只要在接口文档里面强调,“调用GetAllRates函数所获得的内存区块,由调用者负责释放”就可以了!

GetAllRates函数的使用说明:

Snipaste_2026-05-10_15-23-28.png

然后编写代码,获取全部外币信息:

 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
// L07_13_GET_AND_DISPLAY_ALL_RATES.cpp

// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
#include <stdlib.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars  D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate);

int main()
{
    // 定义外汇牌价结构体指针变量
    ExchangeRate* allRates = NULL;

    int count = GetAllRates(&allRates);

    if (count > 0) {
        printf("成功获得 %d 种外币牌价\n", count);
        printf("牌价数据被存储在地址 %p 开始的内存区域中dd\n", allRates);
        for (int i = 0; i < count; i++) {
            // allRates 是数组首地址,allRates + i =  i 个元素的地址,等价于 &allRates[i]
            displayRate(allRates + i);
        }
        // 释放内存块  free函数在stdlib.h中定义
        free(allRates);
    }
    else {
        printf("网络异常或服务器返回不正常的结果\n");
    }

    return 0;
}

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码   (固定6字符,左对齐)
    printf("%-6s\t", rate->CurrencyCode);
    // 货币名称  (固定16字符,左对齐)
    printf("%-16s\t", rate->CurrencyName);
    // 发布时间   (固定20字符,左对齐)
    printf("%-20s\t", rate->PublishTime);
    // 现汇买入价   8字符,2位小数)
    printf("%8.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%8.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%8.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%8.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%8.2f\n", rate->MiddleRate);
}

然后运行程序,输出结果如下:

Snipaste_2026-05-10_15-37-53.png

可以看到,输出了26种外汇牌价数据,比我测试的40种少,有可能是接口做了限制!

定义显示全部牌价数据的函数dispalyAllRates

目前我们显示全部牌价数据的代码直接写在main函数中,后期有可能多处需要使用,因此可以模块化为一个函数。

优化后的代码:

 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
// L07_13_GET_AND_DISPLAY_ALL_RATES.cpp

// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
#include <stdlib.h>

// 附加包含目录已经添加  D:/BC101/Libraries/BOCRates/BOCRates.h
// 此处引入作者提供的静态库的头文件
#include "BOCRates.h"
// #pragma comment:VS 编译器专用指令,给编译器发命令
//      lib:表示我要链接一个静态库
//      路径:库文件在哪里
// 链接器--附加库目录,加上 D:/BC101/Libraries/Mars 和 D:/BC101/Libraries/BOCRates目录
#pragma comment(lib, "BOCRates.lib")

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate);
// 打印所有外汇牌价信息
void displayAllRate(const ExchangeRate* allRecords, int count);

int main()
{
    // 定义外汇牌价结构体指针变量
    ExchangeRate* allRates = NULL;

    int count = GetAllRates(&allRates);

    if (count > 0) {
        printf("成功获得 %d 种外币牌价\n", count);
        printf("牌价数据被存储在地址 %p 开始的内存区域中dd\n", allRates);
        displayAllRate(allRates, count);
        // 释放内存块 , free函数在stdlib.h中定义
        free(allRates);
    }
    else {
        printf("网络异常或服务器返回不正常的结果\n");
    }

    return 0;
}

// 打印外汇牌价结构体信息
void displayRate(const ExchangeRate* rate)
{
    // 货币代码   (固定6字符,左对齐)
    printf("%-6s\t", rate->CurrencyCode);
    // 货币名称  (固定16字符,左对齐)
    printf("%-16s\t", rate->CurrencyName);
    // 发布时间   (固定20字符,左对齐)
    printf("%-20s\t", rate->PublishTime);
    // 现汇买入价   (8字符,2位小数)
    printf("%8.2f\t", rate->BuyingRate);
    // 现钞买入价
    printf("%8.2f\t", rate->CashBuyingRate);
    // 现汇卖出价
    printf("%8.2f\t", rate->SellingRate);
    // 现钞卖出价
    printf("%8.2f\t", rate->CashSellingRate);
    // 中行折算价
    printf("%8.2f\n", rate->MiddleRate);
}

// 打印所有外汇牌价信息
void displayAllRate(const ExchangeRate* allRecords, int count)
{
    for (int i = 0; i < count; i++) {
        // allRecords 是数组首地址,allRecords + i = 第 i 个元素的地址,等价于 &allRecords[i]
        displayRate(allRecords + i);
    }
}
7.4.6 保存和打开数据文件

获取全部外汇牌价数据后,就需要将其保存到文件中,以便在网络临时断开时显示数据。

7.4.6.1 保存全部牌价数据

GetAllRates函数已经获取了全部牌价数据并存入内存中,将其保存到磁盘文件的程序就比较容易写了。考虑到保存牌价数据到文件也是经常要进行的工作,因此也应将其设计成为一个单独的函数——saveAllRates

我们在7.3节 将结构体存入磁盘文件 L07_10_READ_RATES_FROM_FILE.cpp文件中已经定义过save_data_to_fileload_data_from_file函数。

1
2
3
4
5
// 通用二进制写入:任意数据原样存文件
bool save_data_to_file(const char* filename, const void* data, int size);

// 通用二进制读取:从文件读取任意数据到内存
bool load_data_from_file(const char* filename, void* data, int size);
Licensed under the GNU General Public License v3.0
最后更新于 2026年05月10日 21:21