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符号@、井号#、问号?等。

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

4.3.2 找到变量在内存中的地址

  • 变量对应着内存中的一块空间,程序运行时会根据数据类型为变量分配对应大小的内存空间。
  • 内存中的每一个字节都有一个唯一的编号,我们称之为【内存地址】。
  • 内存地址在32位操作系统下是一个4字节(32位)的整数,而在64位操作系统下是一个8字节(64位)的整数。
  • C语言允许在变量声明后通过&运算符取得变量所占内存空间的第1个字节的地址,称为变量的首地址。

创建一个空项目:

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

并编写以下代码:

cpp
#include <stdio.h>

int main()
{
    int x = 0;
    printf("变量x所在的内存地址是: %p\n", &x);

    return 0;
}

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

Snipaste_2024-09-10_22-22-18.png

输出如下:

变量x所在的内存地址是: 0115F924
  • &是一个运算符,用于取得变量的首地址。
  • printf函数中使用&p占位符来显示内存地址,使得内存地址会以十六进制数值来显示。
  • 使用十六进制数表示内存地址是大多数程序员的习惯。
  • 注意,每次运行以上程序结果都不一样。
  • 因这大多数情况下操作系统接管了内存,所以在程序中得到的内存地址不是真正的【物理地址】,而是一个【虚拟地址】。我们无须关心变量的绝对地址,只要能找到变量的虚拟地址就可以对它进行进一步的操作了。

4.4 给变量赋值

  • 在声明变量后可以使用赋值运算符=给它赋值。
  • 赋值运算符的作用是将一个内存空间中的数据“复制”到另一个内存空间中。
  • 赋值运算符右边可以是一个变量、常量、算术运算表达式或者函数的返回值。

4.4.1 变量的初值不是默认为0

  • 系统声明变量时只是为变量分配了内存空间,并不会对该内存区域进行任何初始化操作。
  • 除非在声明变量时就给变量赋初值,否则变量的初始值是随机的。
  • 比较新的开发工具会警告甚至直接造成编译错误。如提示【使用了未初始化的内存x】。

如果我们修改以上代码:

cpp
#include <stdio.h>

int main()
{
    //int x = 0;
    int x;

    printf("变量x的值是: %d\n", x);
    printf("变量x所在的内存地址是: %p\n", &x);

    return 0;
}

此时运行则会报错:

Snipaste_2024-09-10_22-45-07.png

  • 在声明变量时给变量赋初值是一个好习惯,甚至是强制的规范。

4.4.2 将常量的值赋值给变量

将常量的值赋值给变量是最常见的操作之一。如:

cpp
int x = 10;

赋值运算符=右边的10就是常量,它的值就是字符所表达的意思,所以称它为【字面常量】,也称作【直接常量】。

  • 需要注意的是,字面常量也有数据类型。编译器会根据常量的字面值决定它的数据类型并为其分配存储空间。对于10,编译器会把它当作整型数int来处理。
  • 而对于包含小数的常量编译器则会默认把它当作双精度浮点型数据。
cpp
float a = 3.1415926535;

以上代码中的3.1415926535会被当作double型数据来处理。

  • 由于数据分为不同的类型,因此即使是最简单的赋值也可能会遇到意外。例如,可能会出现数据截断情况(损失数据精度)。
4.4.2.1 数据截断

我们改下代码,然后再运行:

cpp
#include <stdio.h>

int main()
{
    float a = 3.1415926535;
    printf("变量a的值是: %f\n", a);

    return 0;
}

Snipaste_2024-09-10_23-00-48.png

可以看到程序有输出结果,输出值是3.141593,并且有一个警告:warning C4305: “初始化”: 从“double”到“float”截断

赋值运算符右侧的3.1415926535被编译器识别为了个double型常量(实际包含10位小数),左侧的a则是一个最多可存储6位小数的float型变量。

编译器此时会自动将double型常量的值转换成float型数值并赋值给变量a,这被称为【隐式类型转换】或者【自动类型转换】,但由于它们的小数精度不同会导致部分数据丢失,编译器于是给出了警告来提醒程序员。

4.4.2.2 强制类型转换
  • 有时候程序员明确知道这里要对数据类型进行转换并且能接受精度损失,就可能通过【类型转换运算符】来实现不同数据类型之间的强制转换。
  • 强制类型转换相当于程序员声明了"我明确了解此处的类型转换可能带来的问题,我对此负责",这样编译器就不会再给出警告。
  • 强制类型转换不是用于让编译器闭嘴,它的主要用途是在遇到编译器无法自动进行类型转换时,给程序员一个“明确说明”的机会。
类型转换运算符作用
(int)将其后的值强制转换成整型
(float)将其后的值强制转换成单精度浮点型
(double)将其后的值强制转换成双精度浮点型

以下是一个示例:

cpp
#include <stdio.h>

int main()
{
    float a = (float)3.14;
    int b = (int)a;
    printf("变量a的值是: %f\n", a);
    printf("变量b的值是: %d\n", b);

    return 0;
}

编译运行结果:

sh
变量a的值是: 3.140000
变量b的值是: 3
4.4.2.3 溢出
  • 除了数据截断的问题以外,溢出是给变量赋值时可能发生的另一个问题。
  • int整型表示的整数范围是[-2147483648, 2147483647],如果当给一个变量定义赋值超过了该范围,就会发生数据溢出。 可以通过limits.h中的INT_MAXINT_MIN获取int整型的最大值和最小值。

以下是一个示例:

cpp
#include <limits.h>
#include <stdio.h>

int main()
{
    //  正常定义
    int a = 2147483647;
    // 数据溢出
    int b = 2147483649;
    printf("变量a的值是: %d\n", a);
    printf("变量b的值是: %d\n", b);
    printf("INT类型最大值是: %d\n", INT_MAX);
    printf("INT类型最小值是: %d\n", INT_MIN);

    return 0;
}

运行后,输出结果如下:

sh
变量a的值是: 2147483647
变量b的值是: -2147483647
INT类型最大值是: 2147483647
INT类型最小值是: -2147483648

可以看到,此时变量b的输出并不是我们想象的一个很大的整数,此时却变成了负数!!!

  • 不是所有的溢出都会被编译器发现,因为的些编译器不会进行此项检查,而有些溢出则来自于计算的结果因而不能被编译器发现。
  • 由于每种数据类型的表示范围和精度都是有限的,在给变量赋值时应充分考虑是否存在无意的数据截断和溢出,因为这可能会导致严重的错误和损失。
  • 对于可以接受的数据截断,应采用强制类型转换予以明确;对于可能存在溢出的问题,则应选择表示范围更大的数据类型。

4.4.3 将变量的值赋值给另一个变量

  • 赋值操作的本质是数据在内存中的复制。
  • 需要注意的是,将变量的值赋值给另一个变量,同样也可能因为类型不同而导致发生数据截断、溢出等问题

示例:

cpp
#include <stdio.h>

int main()
{
    int x = 10;
    int y = 20;
    y = x;
    printf("变量y的值为: %d\n", y);

    return 0;
}

运行后,输出结果如下:

sh
变量y的值为: 10

可以看到,将变量x的值赋值给变量y后,变量y的值变成10了。

4.4.4 将算术计算的结果赋值给变量

  • 赋值给变量的值除了来自常量和其他变量以外,也可以来自算术计算。
  • 在C语言中,使用【算术表达式】来描述如何进行计算。
  • 算术表达式中可以包含变量、常量和运算符。
  • 一个表达式可以包含算术运算符如+-*/,这些符号被称为运算符。

下表是运算符及其含义:

符号含义
+
-
*
/
%取除法计算的余数

以下是一个算术表达式:

cpp
(2 + 3) * a;

在这个表达式中,23是常量,a是变量,+*是运算符。

  • 在运算符中,乘法*、除法/和取模%运算符拥有比较高的优先级,加法+和减法-优先级低一些。
  • 将算术表达式的值赋值给变量时也可能会造成溢出、数据截断等问题。
4.4.4.1 算术表达式的类型

请看以下示例程序:

cpp
#include <stdio.h>

int main()
{
    int x = 7;
    float y = x / 2;
    float z = x / (float)2;
    printf("变量y的值为: %f\n", y);
    printf("变量z的值为: %f\n", z);

    return 0;
}

运行后,输出结果如下:

sh
变量y的值为: 3.000000
变量z的值为: 3.500000

在数学逻辑中,如果x值为7,那么x / 2应该等于3.5,但实践看到,直接使用float y = x /2;计算y的值并输出后,输出结果是3.000000,与我们预期不一样。这里记录一下计算逻辑:

在计算算术表达式x / 2时,需要预先为计算结果分配临时存储空间(通常是CPU的寄存器中的空间),然后将计算的结果放入这块临时空间再复制给变量y。分配临时存储空间的行为是在算术运算之前发生的,此时无法推断运算结果是何种类型,只能简单地依据算术表达式中组成元素的类型、大小 ,来决定在临时存储空间中存储数据的方式。

编译器“乐观地预测”算术表达式的类型是组成它的元素中“表达范围最强”的数据类型。换句话说,一个算术表达式中包含有浮点型和整型元素,那么用于存储计算结果的临时空间就采用浮点型。

那么表达式x / 2是什么类型呢?因为x是整型变量,2也被默认地当作整型数,所以表达式x / 2的值也是整型(存储计算结果的存储空间只能存储整型值)。于是3.5的小数部分被丢弃,程序运行的结果就是3.000000

而计算float z = x / (float)2; 中计算z的值时,将2强制转换为float浮点型数,表达式x / (float)2的类型就变成了浮点型,最终计算的z的值就是正常的值3.500000

或者将x变量强制转换成float浮点型,如float z = (float)x / 2;这样也可以正常计算出值。

4.4.4.2 算术运算带来的溢出
  • 不是所有的算术运算造成的溢出都会被编译器发现。

例如下面的程序:

cpp
#include <stdio.h>

int main()
{
    int a = 2147483647;
    int b = 2;
    int c = a + b;
    printf("变量c的值是: %d\n", c);

    return 0;
}

运行后,输出结果如下:

sh
变量c的值是: -2147483647
  • 值得警惕的是这种情况下编译器不会给出任何警告,但运算结果是错误的!!!

要计算2147483647 + 2的和,必须使用更大的数据类型来存储运算结果。

创建一个空项目:

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

并编写以下代码:

cpp
#include <stdio.h>

int main()
{
    int a = 2147483647;
    int b = 2;
    long long c = (long long)a + b;
    printf("变量c的值是: %lld\n", c);

    return 0;
}

运行后,输出结果如下:

sh
变量c的值是: 2147483649

Snipaste_2024-09-21_23-11-24.png

  • 第7行代码中long long类型被称为【超长整型】,占用64字节。
  • 第8行中的printf函数使用了%lld占位符显示超长整型。

4.4.5 将函数的返回值赋值给变量

  • 对于的返回值的函数,可以将其返回值赋值给变量。
  • 存储函数的返回值的变量类型要与函数返回值类型相同。
  • 不是所有函数都的返回值。
  • 对于自己定义的函数,你当然知道它的返回值类型是什么,而对于库函数、第三方函数,可以在头文件、函数的说明文档中查阅它的返回值类型。
  • 将函数的返回值赋值给变量时,需要确保变量的类型与函数的返回值类型相同。

运行以下测试示例:

cpp
#include <stdio.h>

void beep()
{
    printf("%c", 7);
}

int main()
{
    int r = beep();

    return 0;
}

会发现提示异常:

sh
严重性	代码	说明	项目	文件	行	禁止显示状态
错误(活动)	E0144	"void" 类型的值不能用于初始化 "int" 类型的实体	L04_TEST	D:\BC101\Examples\L04\L04_TEST\main.cpp	10	
错误	C2440	“初始化”: 无法从“void”转换为“int”	L04_TEST	D:\BC101\Examples\L04\L04_TEST\main.cpp	10

就是说,不能将void类型转换在int类型。

我们将int r =去掉后,就可以正常运行程序。

可正常运行的代码:

cpp
#include <stdio.h>

void beep()
{
    printf("%c", 7);
}

int main()
{
    beep();

    return 0;
}

该程序会让系统发出声音。

4.4.6 交换两个变量的值

有时我们需要交换两个变量的值,最简单明了的方式就是使用一个额外的变量作为临时变量,然后完成交换。

以下示例是交换两个变量的代码:

cpp
#include <stdio.h>

int main()
{
    int a = 10;
    int b = 20;
    int temp = a;
    a = b;
    b = temp;
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("temp = %d\n", temp);
}

运行后,输出结果如下:

sh
a = 20
b = 10
temp = 10

4.5 选择结构程序

在4.2.3节中,我们根据ConvertCurrency函数的返回值结合if语句决定显示美元的中行折算价或者显示“网络或服务器异常”。

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;
}

这种选择性执行的程序结构被称为【选择结构】或【分支结构】,即根据条件是否成立决定一段代码是否被运行。

要选择性地执行程序,首先要使用包含【关系运算符】判断条件是否成立。

4.5.1 关系运算符和关系表达式

  • 关系运算符用于判断条件是否成立。

在C语言中可以使用下表中的关系运算符:

序号关系运算符含义
1==等于
2>大于
3<小于
4>=大于或等于
5<=小于或等于
6!=不等于

要进行条件判断时应在关系运算符的两侧加上不同的表达式。

  • 表达式可以是单个的变量或常量。如a > 10
  • 也可以是两个变量。如a >= b
  • 也可以是算术表达式。算术运算符优先级高于关系运算符。如 a + 10 != b, 判断a + 10的值是否不等于b
  • 函数的返回值也可以参与关系运算。
  • 当条件不成立时,关系表达式的值为0;当条件成立时,关系表达式的值为非0的值(一般是1)。

创建一个空项目:

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

并编写以下代码:

cpp
#include <stdio.h>

int main()
{
    int a = 0;
    int b = 10;
    printf("关系表达式 a > 10 的值是:%d\n", a > 10);
    printf("关系表达式 a >= b 的值是:%d\n", a >= b);
    printf("关系表达式 a != b 的值是:%d\n", a != b);
    printf("关系表达式 a + 10 != b 的值是:%d\n", a + 10 != b);

    return 0;
}

运行后,输出结果如下:

sh
关系表达式 a > 10 的值是:0
关系表达式 a >= b 的值是:0
关系表达式 a != b 的值是:1
关系表达式 a + 10 != b 的值是:0

Snipaste_2024-09-22_21-29-50.png

4.5.2 使用if语句实现选择结构

实现选择结构程序最常见的方法就是使用if语句。

  • 可以使用if语句实现判断。根据条件是否成立决定一条语句是否被执行。
  • 也可以使用if...else语句实现判断,这个时候就可以在条件成立或不成立时分别执行不同的代码。
  • 也可以使用if...else if语句来实现多个条件的判断。

在4.2.3节中,我们根据ConvertCurrency函数的返回值结合if语句决定显示美元的中行折算价或者显示“网络或服务器异常”,可以对“网络或服务器异常”进行更详细的判断:

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 if (r == -1) {
        printf("服务器异常,请联系管理员\n");
    } else if (r == -2) {
        printf("网络异常,请联系管理员\n");
    } else {
        printf("未知异常,请联系管理员\n");
    }

    return 0;
}

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