Skip to content

换个姿势学C语言

0. 说明

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

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

这是一本非常不错的书!

4. 获取和显示外汇实时牌价

绝大多数程序只做四件事:输入数据、存储数据、处理数据和输出数据。 在没有编程思路的时候考虑这样几个问题:

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

把这几个问题考虑清楚了,并按照这个顺序来编程就是常规的编程思路。甚至不用一次把这四个问题都考虑清楚,有时候先解决一个问题,下一个问题的答案就自动浮现了。

对于外汇牌价看板程序来说,第一个问题是:汇率数据从何处来?

4.1 如何获取实时牌价数据

很显然,程序员不可能写一个程序自动计算出实时汇率,汇率只能从权威发布渠道获取。

银行或金融机构通过专用网络或者安装在楼顶的卫星天线收发数据,程序员不太可能连接到它们的网络来获取最新的外汇牌价数据。

有些数据服务商会提供外汇牌价数据。但这是要花钱的。

作者开发了一个函数库供大家调用,这个库就是【牌价接口库】。这种库就是第三方库。

  • 第三方库,既不是编译器自带的函数,也不是读者自己写的。

4.2 下载和引用外汇牌价接口库

4.2.1 下载外汇牌价接口库

何老师在书的前言中给出了清华大学出版社的下载二维码。

规划项目目录结构 中已经给出了目录结构的规划。

我们将下载好的接口库文件存放到 D:\BC101\Libraries\BOCRates目录下,该目录下一共有两个文件。

Snipaste_2024-08-27_23-14-15.png

  • BOCRates.h , 包含接口库函数声明的头文件,在你的程序中需要包含它才可以调用相关的函数。
  • BOCRates.lib, 接口库函数代码编译后的二进制代码,在链接时其中的代码将与你的程序一起生成可执行文件。

注意,本外汇牌价接口库返回的小数部分会进行随机处理,不保证每一次调用返回的结果都与中行公布的实时汇率完全一致。

4.2.2 显示美元的中行折算价

完整的外汇牌价包含多种货币的多种牌价(现汇买入价、现钞买入价、现汇卖出价、现钞卖出价、银行折算价),一开始就要完整地显示它们颇有难度,但我们可以先解决一个小问题——显示美元的中行折算价。

此处可以看到,将复杂问题进行拆分,先解决简单的一部分,再解决其它比较难的部分。

4.2.2.1 显示美元中行折算价的例子

我们下载好第三方库后,就可以着手编写一个程序了。

创建一个空项目:

  • 项目名称:L04_01_RATES_EXAMPLE
  • 位置:D:\BC101\Examples\L04

并编写以下代码:

cpp
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")

int main()
{
    double r = ConvertCurrency(1, "USD", "CNY", 100);
    printf("%.2f\n", r);

    return 0;
}

使用Debug x86模式来编译代码。

修改项目属性,设置【链接器】--【常规】--【强制文件输出】选择【已启用(/FORCE)】:

Snipaste_2024-08-27_23-44-46.png

请务必牢记未来创建的所有用到外汇牌价接口库的程序,都应修改【强制文件输出】选择【已启用(/FORCE)】。

此时,我们还是报异常了。

Snipaste_2024-08-27_23-48-12.png

提示无法打开nafxcw.lib文件。

我们搜索一下,发现系统中有这个文件:

Snipaste_2024-08-27_23-49-10.png

可以知道其路径是D:\ProgramFiles\MicrosoftVisualStudio\2022\Enterprise\VC\Tools\MSVC\14.16.27023\atlmfc\lib\x86

点击【链接器】--【常规】--【附加库目录】,增加这个lib文件所在目录:

可知VS有自带的宏地址$(VCToolsetsDir),其对应路径是D:\ProgramFiles\MicrosoftVisualStudio\2022\Enterprise\VC\Tools\MSVC\:

Snipaste_2024-08-27_23-56-02.png

所以我们可以配置一个附加库目录$(VCToolsetsDir)14.16.27023\atlmfc\lib\x86

Snipaste_2024-08-27_23-59-39.png

然后再次运行项目:

Snipaste_2024-08-28_00-03-25.png

终于运行成功了!

可以看到,此时得到美元折算价是712.49元。

此时,在中国银行外汇牌价 https://srh.bankofchina.com/search/whpj/search_cn.jsp 上面查看:

Snipaste_2024-08-28_00-06-16.png

可以看到,我们计算的结果与中国银行外汇牌价上面的显示结果是一致的。

注意,你运行本程序时应以你运行当天时实际中国银行外汇牌价上面的显示结果为准。

4.2.3 分析显示美元中行折算价程序

现在我们来分析上一节中运行的程序。

4.2.3.1 引用的头文件BOCRates.h

在之前的程序中,只包含过stdio,h这个头文件,而在本案例中包含了新的、非编译器自带的头文件BOCRates.h

cpp
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"

编译器自带的用以下形式:

cpp
#include <stdio.h>

打开BOCRates.h可以看到其内容:

cpp
#pragma once
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;			//中行折算价
}; 

typedef struct EXCHANGE_RATE ExchangeRate;

double ConvertCurrency(int real, const char* from, const char* to, double amount);
int GetRatesByCode(const char* code, double* rates);
int GetRatesAndCurrencyNameByCode(const char* code, char* name, char* publishTime, double* rates);
int GetRateRecordByCode(const char* code, ExchangeRate* results);
int GetAllRates(ExchangeRate** result);

现在我还不知道这个头文件是什么意思。

但可以看到,其中声明了一些函数。头文件中并不包含这些函数的具体实现。实现函数的功能被编译后生成了库文件BOCRates.lib

4.2.3.2 引用的库文件BOCRates.lib

案例的第3行代码:

cpp
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")

指示链接器到D:\BC101\Libraries\BOCRates\BOCRates.lib文件中寻找相关函数的二进制代码,并与目标文件一起合并成可执行文件。

在3.3节中已经讲过,在生成可执行文件时,编译器首先编译程序形成【目标文件】,链接器将库文件中的代码(函数的实现代码)和目标文件一起合并形成可执行文件。程序包含了头文件,引用了库文件,接下来就可以使用库的函数了。

4.2.3.3 使用ConvertCurrency函数-引入if条件判断

ConvertCurrency函数实现根据中行折算价将指定的货币金额转换成另一种货币金额的功能。

下面来看一下它的用法。

cpp
double r = ConvertCurrency(1, "USD", "CNY", 100);

ConvertCurrency是函数名,可以看到这个函数有4个参数。与以前调用函数时不考虑返回值不同,在这个程序中定义了一个变量r来存储函数的返回值,也就是货币转换的结果。

ConvertCurrency函数的第2、3个参数值分别是"USD"和"CNY",大约也能猜出这是要将美元转换成人民币,而第4个参数则是要转换的金额是100美元。

如果计算日元的中行折算价,日元的货币代码是JPY

cpp
double j = ConvertCurrency(1, "JPY", "CNY", 100);
printf("%.2f\n", j);

此时,运行代码得到的结果不一定,就是100日元折合人民币约4.91元。

ConvertCurrency函数的第1个参数,指定是否返回实时价格。当用户没有网络时,可以将该参数设置为0,则返回离线版固定的汇率数据。书上说是上返回2021年6月12日10:30:00时中行外汇牌价数据。

如果我将第一个参数改成0,再运行程序,得到的结果是100日元折合人民币约4.94元。

与中行显示的结果不一致。此处不用在意这个细节,能够在离线下获取到数据就行。

正常情况下,我们都是网络环境正常,因此第1个参数保持1即可。

在经验的程序员会根据函数调用的子程序或者开发工具的智能提示猜测函数的用法。但负责任的函数开发者会提供一份详细的函数使用说明,程序员可以在其中找到调用这个函数需要包含的头文件、函数的原型、参数和返回值说明。

ConvertCurrency函数说明如下图所示:

Snipaste_2024-09-02_22-58-17.png

对应excel文件路径: docs/public/scripts/c/newstylec/function_readme/ConvertCurrency.xlsx

后面可以使用该文件作为模板,编写其他函数的说明。

ConvertCurrency函数不一定总能成功运行,当网络或服务器发生故障时它就无法获取数据。此时会返回负数(-1表示服务器不正常,-2表示客户端网络不正常,这是函数作者的约定)。因此可以根据函数的返回值是否为负数,让程序输出不同的结果。

cpp
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")

int main()
{
    double r = ConvertCurrency(1, "USD", "CNY", 100);
    if (r > 0) {
        printf("%.2f\n", r);
    } else {
        printf("网络或服务器异常\n");
    }

    return 0;
}

我尝试断开网络,再运行时,并没有达到预期的效果,提示0xC0000005: 读取位置 0x00000000 时发生访问冲突。异常。

Snipaste_2024-09-02_23-37-50.png

搜索相关异常, 这是一个内存访问冲突错误,表明程序尝试访问无效的内存地址。

暂时不知道如何处置,先忽略。

4.3 数据类型与变量

在上一个程序中,使用了变量r来存储ConvertCurrency函数的返回值,然后使用printf显示它的值。

cpp
	double r = ConvertCurrency(1, "USD", "CNY", 100);
    printf("%.2f\n", r);

那么,什么是变量?该如何使用它?

4.3.1 数据类型与变量声明

  • 【变量】这个词本意是指可能会发生变化的数值或信息。
  • 程序设计中的【变量】则是用于存储可变化的数值或信息的一种方式。绝大多数的语言都通过“声明变量”的方式先在内存中分配一块空间,然后将一个名称(变量名)映射到这块空间,便于程序员读写这块内存空间。
  • 在内存中存储信息的最简单方式是使用变量,变量名会被映射到一块内存空间,通过变量名可以方便地访问这块内存空间以实现信息存取。变量中可以存放的信息包括数值、字符的编码或其他可以被转换成二进制的信息。
  • 在C语言中要存储一条可能发生变化的信息,必须首先声明变量,而声明变量必须要确定变量要使用的数据类型。
4.3.1.1 数据类型
  • 在为变量分配内存时,没有必要、也不可能为一个变量分配无穷大的内存空间。在大多数编程语言中还须说明使用这块空间存储何种类型的数据,这是计算机系统和编程语言的技术限制和约定。
  • 为了给程序员提供方便,编程语言为声明变量预设了一些【模板】,这种【模板】被称为【数据类型】。声明变量时根据需要选择一种类型即可。
  • 在C语言中,定义了多种数据类型用于存储数值,有些只可以存储整数,有些可以存储小数,C语言中最常见的数值类型是intfloatdouble
  • int整型,int数据类型简称整型数或整型,用于表示一个范围有限的整数。在大多数现代编译器中,一个整型变量占用32个二进制位(4字节)。
  • float单精度浮点数,单精度浮点数占用32个二进制位(4字节),但只支持6位小数精度(最多保留小数点后6位)。如果赋值给float型变量的值小数位数超过6位,则超出的部分会损失。
  • double双精度浮点数,用于存储小数,占用64个二进制位(8字节),提供至少15位小数精度。
  • C语言标准支持的数据类型不仅限于上述3种,此外程序员还可以自定义数据类型。程序员根据需要选择合适的数据类型后,就可以声明变量了。
4.3.1.2 声明变量

在C语言中声明变量的方法如下:

数据类型 变量名;

如:

cpp
int x;

此处的int表示整型数据类型,x是变量名。

选择数据类型的原则:

  • 选择数据类型的原则是精度和范围能满足需求(不会产生溢出或精度不够的情况),又尽量不浪费。
  • 在程序处理小规模的数据时浪费一点存储空间无关紧要,但在处理海量数据时,如果数据类型选择不当会造成内存空间的浪费太大以及数据在传输、存储和处理时的性能下降。

变量名:

  • 变量名是个标识符,它映射着为变量分配的内存区域,对变量名的操作会映射成对该内存区域的操作。
4.3.1.3 变量的命名规则

和函数名一样,变量名也应该是“见名知意”的,不建议使用x、y或者a、b这样的变量名,一般可以使用英文单词、短信来更明确地表示变量的作用。

变量中允许使用字符:

  • 字母A~Za~z
  • 数字0~9,但是不能在变量名开头。
  • 下划线_

例如,stop_signloop3_pause都是合法的变量名。

变量中不允许使用字符:

  • 算术运算符
  • 点号.
  • 单引号'
  • 特殊符号,如星号*、at符号@、井号#、问号?等。

给变量命名也可以使用驼峰命名法、下划线命名法等。使用中文、汉语拼音来给变量命名会显示非常不专业,很多编译器也不支持中文变量名。

本首页参考 https://notes.fe-mm.com/ 配置而成