换个姿势学C语言
0. 说明
《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!
这是一本非常不错的书!
- 第一、二、三章总结参考换个姿势学C语言 第1-3章
- 第四章总结参考换个姿势学C语言 第4章
5. 获取完整的牌价数据
在第4章的程序中已经实现了显示一项数据的功能——100美元折算人民币的金额,这项计算是根据【中行折算价】计算的,但外汇牌价还包括现汇买入价、现钞买入价、现汇卖出价、现钞卖出价。
存储这5种价格需要声明5个double型变量吗?在中行网站上面可以看到,有35种外汇可以选择:
<select name="pjname" id="pjname">
<option value="0">选择货币</option>
<option value="英镑">英镑</option>
<option value="港币">港币</option>
<option value="美元" selected="">美元</option>
<option value="瑞士法郎">瑞士法郎</option>
<option value="德国马克">德国马克</option>
<option value="法国法郎">法国法郎</option>
<option value="新加坡元">新加坡元</option>
<option value="瑞典克朗">瑞典克朗</option>
<option value="丹麦克朗">丹麦克朗</option>
<option value="挪威克朗">挪威克朗</option>
<option value="日元">日元</option>
<option value="加拿大元">加拿大元</option>
<option value="澳大利亚元">澳大利亚元</option>
<option value="欧元">欧元</option>
<option value="澳门元">澳门元</option>
<option value="菲律宾比索">菲律宾比索</option>
<option value="泰国铢">泰国铢</option>
<option value="新西兰元">新西兰元</option>
<option value="韩国元">韩元</option>
<option value="卢布">卢布</option>
<option value="林吉特">林吉特</option>
<option value="新台币">新台币</option>
<option value="西班牙比塞塔">西班牙比塞塔</option>
<option value="意大利里拉">意大利里拉</option>
<option value="荷兰盾">荷兰盾</option>
<option value="比利时法郎">比利时法郎</option>
<option value="芬兰马克">芬兰马克</option>
<option value="印度卢比">印度卢比</option>
<option value="印尼卢比">印尼卢比</option>
<option value="巴西里亚尔">巴西里亚尔</option>
<option value="阿联酋迪拉姆">阿联酋迪拉姆</option>
<option value="印度卢比">印度卢比</option>
<option value="南非兰特">南非兰特</option>
<option value="沙特里亚尔">沙特里亚尔</option>
<option value="土耳其里拉">土耳其里拉</option>
</select>
如果这35种货币都定义5个变量,则需要定义35 * 5 = 175
个变量。
实际上,在C语言中对于相同类型的一组变量,可以使用【数组】来存储。本章主要包含以下内容:
- 使用数组存储多种价格;
- 处理数组中的数据;
- 字符和字符串处理;
- 获取和显示货币名称。
在完成本章程序编写后,你的程序可以完整地获取和显示某种货币的全部牌价信息。
5.1 使用数组存储数据
ConvertCurrency
函数是根据中行折算价进行货币金额转换的,得到的是单个金额 。但是外汇牌价看板程序需要获取多个价格,包括现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和中行折算价。
这5项数据该如何存储?读者当然可以定义5个double
双精度浮点数 ,然后用5个不同的函数获取不同的价格,但这种方式会大大提高程序的复杂度(35种货币都定义5个变量的话,则需要定义35 * 5 = 175
个变量)。
想像一下,如果你要从超市买10个鸡蛋回家,最好的办法显然不是每个口袋装几个,而是用一种叫“蛋托”的容器装好以更方便和妥当地携带。就像下图这样:
- 在程序设计中,如果要存储多个类型相同、用途相关的一组数据时,可以使用【数组】。
- 数组的实质就是多个相同类型的变量的集合,集合的每一个元素就是一个变量,并且所有元素在内存地址上都是相邻的,这样就为处理数据带来便利。
接下来,我们将介绍如何定义和使用数组,以及如何对数组中存储的数据进行处理。
5.1.1 数据的声明方法
和变量一样,要使用一个数组,必须先声明它。
5.1.1.1 在程序中声明数组
声明变量时,我们是这样做的:
double r = 0;
声明数据的方法是这样的:
数据类型 数组名[元素数量];
如:
double rates[5];
声明了一个名叫rates
的数组,用于存储某种货币的5种价格。double
表示这个数组中的所有元素类型是双精度浮点型。方括号中的5表示数组一个有5个元素。
- 数组占用内存空间大小的计算方法如下:
数据元素的数量 * 单个数组元素的字节大小
。 - 数组占用内存空间大小在数组声明时就已经确定,未来不能修改数组的大小。
例如,上面的数组rates
,每个双精度浮点型元素占用8个字节,5个数组元素一共占用40个字节。
- 如果程序在声明数组时没有初始化其中的元素,数组中的元素的值是不确定的,这一点和变量一样。
- 定义数组时,方括号中只能是一个常量,而不能是变量。
像下面的程序则无法正常编译:
#include <stdio.h>
int main()
{
int size = 5;
int rates[size];
}
此时提示异常表达式必须含有常量值
。
而像下面这样则是正常的:
#include <stdio.h>
int main()
{
int rates[5];
}
5.1.1.2 数组的初始化
有时我们希望在定义数组时就给它的元素赋值,这时可以通过下面的方法初始化数组中的元素,即在一对大括号中依次给出数组中每一个元素的值。
#include <stdio.h>
int main()
{
double rates[5] = { 10, 20, 30, 40, 50 };
for (int i = 0; i < 5; i++) {
printf("数组元素序号:%d\t rates[%d] = %.2f\t内存地址为:%p\n", i, i, rates[i], &rates[i]);
}
return 0;
}
以上程序,初始化数组,然后使用for
循环来读取数据元素,并输出每个数组元素的内存地址,运行结果如下:
数组元素序号:0 rates[0] = 10.00 内存地址为:004FFB34
数组元素序号:1 rates[1] = 20.00 内存地址为:004FFB3C
数组元素序号:2 rates[2] = 30.00 内存地址为:004FFB44
数组元素序号:3 rates[3] = 40.00 内存地址为:004FFB4C
数组元素序号:4 rates[4] = 50.00 内存地址为:004FFB54
书中P101页下面这段描述是错的:
如果初始化数组时要将所有元素都设置为同一元素,也可以这样做:
cppdouble rates[5] = -1;
这行代码将数组
rates
数组中的每一个元素都被赋值为-1。
测试:
#include <stdio.h>
int main()
{
//double rates[5] = { 10, 20, 30, 40, 50 };
double rates[5] = { -1 };
for (int i = 0; i < 5; i++) {
printf("数组元素序号:%d\t rates[%d] = %.2f\t内存地址为:%p\n", i, i, rates[i], &rates[i]);
}
return 0;
}
运行结果如下:
数组元素序号:0 rates[0] = -1.00 内存地址为:00DCF9FC
数组元素序号:1 rates[1] = 0.00 内存地址为:00DCFA04
数组元素序号:2 rates[2] = 0.00 内存地址为:00DCFA0C
数组元素序号:3 rates[3] = 0.00 内存地址为:00DCFA14
数组元素序号:4 rates[4] = 0.00 内存地址为:00DCFA1C
可以看到,仅数组中第一个元素被赋值为-1,其他元素都是0。
5.1.2 将外汇牌价数据存入数组
知道了数组的定义和基本使用方法之后,就可以使用一个数组来存储某种货币的5种牌价。例如:
double rates[5];
然后每个元素表示的牌价信息不一样:
数组元素 | 用途 |
---|---|
rates[0] | 现汇买入价 |
rates[1] | 现钞买入价 |
rates[2] | 现汇卖出价 |
rates[3] | 现钞卖出价 |
rates[4] | 中行折算价 |
牌价接口库中提供了GetRatesByCode
函数,它可以按照上面的次序将某种货币的全部牌价存入指定的数组中,只需要调用一次这个函数,就可以一次性将某种货币的5种价格存入数组。
创建一个空项目:
- 项目名称:L05_01_GET_RATES_BY_CODE
- 位置:D:\BC101\Examples\L05
注意,创建项目后,需要像第4章 4.2.2.1 显示美元中行折算价的例子 一样,配置【强制文件输出】和【附加库目录】:
并编写以下代码:
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")
int main()
{
double rates[5];
int result = GetRatesByCode("USD", rates);
printf("%d\n", result);
return 0;
}
此时运行代码,输出结果是1。
以上代码中,输出了结果1,但我不知道牌价信息是否写入到数组rates
中,因此需要访问数组元素。
5.1.3 访问数组元素
数组是用于存储数据的,每一个数组元素相当于一个变量。
5.1.3.1 访问数组元素的基本方法
- 声明数组以后,可以将其中每个元素当作当作的变量来赋值和取值。
- 访问某个元素的方法是在数组名后加一对中括号和索引值。如
rates[0] = 20;
。 - 中括号间的索引值表示程序要访问数组中的第几个元素,索引也被称为【下标】。
- 数组元素从0开始计数,所以
rates[0]
表示数组中的第1个元素。
优化代码:
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")
int main()
{
double rates[5];
int result = GetRatesByCode("USD", rates);
printf("%d\n", result);
if (result == 1) {
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;
}
依次获取数组元素的值,并打印出来。
运行结果如下:
1
现汇买入价:710.96
现钞买入价:710.96
现汇卖出价:713.80
现钞卖出价:713.80
中行折算价:712.20
而此时中国银行官网上面显示结果如下:
可以看到,我们程序运行的结果,与中行官网上面显示的是一致的。
这个时候,我们可以看到,我们代码中使用int result = GetRatesByCode("USD", rates);
时,GetRatesByCode
函数的第一个参数,我们写成了一个固定的值"USD"
,如果我要使用英镑或者 泰国铢 等其他货币,我们该使用什么货币代码?
5.1.3.2 遍历数组
访问数组中所有元素的值则是最常见的操作,这种操作被称为【遍历数组】。
- 当数组元素少时,就可以像上一节那样,通过依次访问
rates[0]
、rates[1]
、rates[2]
、rates[3]
、rates[4]
来遍历数组。 - 推荐使用
for
循环语句来遍历数组。 - 建议所有的循环体都使用大括号的格式,哪怕要循环执行的语句只有1行。
- 在C语言中,可以使用
sizeof
运算符计算数组类型、变量和数组所占用内存空间的大小(结果以字节为单位)。 sizeof
运算符的运算结果为无符号整型数(unsigned int
,该类型不支持存储负数,sizeof
运算的结果也不可能为负数)。- 应时刻牢记:如果不小心访问了不存在的数组元素,编译器不一定会警告或阻止你,而这种操作可能会带来结果不正确,程序崩溃或者安全问题。
直接对上一节的代码进行修改:
#include "D:/BC101/Libraries/BOCRates/BOCRates.h"
#include <stdio.h>
#pragma comment(lib, "D:/BC101/Libraries/BOCRates/BOCRates.lib")
int main()
{
double rates[5];
int result = GetRatesByCode("USD", rates);
printf("%d\n", result);
if (result == 1) {
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]);
// 获取数组长度
int size = sizeof(rates) / sizeof(rates[0]);
printf("size:%d\n", size);
for (int i = 0; i < size; i++) {
printf("%.2f\n", rates[i]);
}
} else {
printf("网络或服务器异常\n");
}
return 0;
}
运行代码:
1
现汇买入价:710.96
现钞买入价:710.96
现汇卖出价:713.80
现钞卖出价:713.80
中行折算价:712.20
size:5
710.96
710.96
713.80
713.80
712.20
可以看到,通过for
循环获取到的值,与当前使用索引号一个个的获取数组元素的值是一样的。
第17行也可以使用以下代码来实现计算数组的长度:
int size = sizeof(rates) / sizeof(double);
double
型数据占用8个字节,为什么不直接使用8,而是使用sizeof
运算符来计算?虽然我们知道在目前使用的编译器
double
类型占用8个字节,但并不能保证所有编译器中double
类型都是8个字节,使用sizeof
运算符计算得出它们实际的字节数更安全。
5.1.4 突破数组大小的限制
- 在C语言中声明数组时对数组的大小是有限制的,换句话说,程序中声明的数组元素不能超过某个特点值。
创建一个空项目:
- 项目名称:L05_06_ARRAY_LIMIT
- 位置:D:\BC101\Examples\L05
编译程序:
#include <stdio.h>
int main()
{
int arr[4095];
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
arr[i] = i;
printf("arr[%d] = %d\n", i, arr[i]);
}
}
此时,程序可以正常运行。
如果将数组的大小调整成409500
,则程序会发生异常:
#include <stdio.h>
int main()
{
int arr[409500];
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
arr[i] = i;
printf("arr[%d] = %d\n", i, arr[i]);
}
}
提示了Stack overflow
异常,即为堆栈溢出错误。
C程序里所有函数中的变量、数组都是使用一块被称为【栈】的内存空间。不同的编译器分配的栈空间大小不同,如果程序在访问栈时发生了越界行为,则会造成Stack overflow
栈溢出异常。
可通过以下访问查看visual studio中栈默认值。
项目属性-->链接器-->系统-->堆栈保留大小,单位为字节。系统默认为1M,即1024*1024=1048576字节。
在Visual Studio中,如果堆栈大小限制为1MB,则理论上最多可创建的int数组大小取决于int类型的大小(在C语言中通常为4字节)以及堆栈上可能存在的其他开销。
堆栈大小与数组容量
堆栈大小:1MB(即1,048,576字节)。
Int类型大小:在C语言中,
int
类型占用4字节。 计算最大数组大小理论最大数组元素数:1MB除以每个
int
元素的大小(4字节),即262,144个int
元素。 注意事项 实际可用空间:堆栈上可能还需要为其他变量、方法调用等预留空间,因此实际可创建的int
数组大小可能会小于这个理论值。堆栈溢出:如果尝试在堆栈上分配超过其容量的数组,将会导致堆栈溢出异常。
当尝试将第4行的int arr[409500];
修改成int arr[262144];
,然后运行程序,一样会提示Stack overflow
堆栈溢出错误。
不断进行二分法测试最大可以设置多大的int
数组:
255000 ok
255875 ok
256312 ok
256339 ok
256352 ok
256359 ok
256362 ok
256364 ok
256366 ok
256393 ok
256421 ok
256476 ok
256500 ok
256501 error
256503 error
256504 error
256506 error
256510 error
256517 error
256531 error
256750 error
256750 error
257500 error
260000 error
当设置数组长度为256504
时,有警告,运行时也会提示堆栈溢出错误:
当设置数组长度为256500
时,又可以正常运行:
也就是说实际上可分配int
类型数组容量是256500
,比理论的262144
小得多。
5.1.4.1 使用malloc函数动态分配内存
通过上面的实验,我们知道数组的大小是有限制的,然而程序要处理的数据“体积”经常会超出这个限制,此时应该怎么做呢?
虽然编译器允许我们调整栈的限制大小,但总归是有限度的。
- 在C语言中,可以使用
malloc
函数分配一块内存空间。 malloc
的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。malloc
函数分配的内存位于【堆区】,是一块比【栈区】大得多的空间。malloc
函数是C标准库提供的函数,要使用malloc
函数需要包含头文件stdlib.h
,malloc
函数的参数就是要分配的内存的大小(以字节为单位)。
创建一个空项目:
- 项目名称:L05_07_MALLOC
- 位置:D:\BC101\Examples\L05
编译程序:
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 1GB = 1024 * 1024 * 1024 字节
int my_memory = 1024 * 1024 * 1024;
printf("1GB = %d 字节\n", my_memory);
printf("正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存\n");
malloc(my_memory);
getchar();
return 0;
}
运行程序:
1GB = 1073741824 字节
正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存
查看可用内存的变化:
# 在运行程序前查看可用内存
C:\>systeminfo | findstr "可用的物理内存"
可用的物理内存: 17,294 MB
# 运行程序后查看可用内存,可以看到
C:\>systeminfo | findstr "可用的物理内存"
可用的物理内存: 16,005 MB
# 程序退出后再次查看可用内存
C:\> systeminfo | findstr "可用的物理内存"
可用的物理内存: 17,192 MB
可以看到,启动程序后,消耗内存17294-16005 = 1289 MB,即除了分配1GB的动态内存外,程序本身还消耗了些内存。
此时,虽然程序能正常执行,但有两个警告:
提示C6031警告,返回值被忽略。
由于malloc
函数分配内存并不是每次都会成功,所以有两种返回值:
- 如果分配内存失败,就返回0;
- 如果分配内存成功,则返回这块内存区域的第1个字节的地址。
优化一下程序:
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 1GB = 1024 * 1024 * 1024 字节
int my_memory = 1024 * 1024 * 1024;
printf("1GB = %d 字节\n", my_memory);
printf("正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存\n");
unsigned int address = (unsigned int)malloc(my_memory);
if (address == 0) {
printf("内存分配失败\n");
} else {
printf("分配到的内存区域首地址:%d\n", address);
}
getchar();
return 0;
}
运行程序:
1GB = 1073741824 字节
正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存
分配到的内存区域首地址:15675456
但是,使用无符号整型变量来存储内存地址是不妥当的,在C语言中有专门用于存储内存地址的数据类型--【指针】。
5.1.4.2 使用指针变量存储内存地址
- 为了给程序员提供方便,C语言提供了专门的数据类型用于存储内存地址,这种数据类型称为【指针型】。
- 使用这种类型的变量称为【指针变量】,简称为【指针】。
- 指针变量也是变量的一种,也需要占用内存空间。
- 指针变量用于存放数据的地址,而数据都是有类型的(
int
、double
和float
等),为了更方便地操作不同类型的数据,C语言提供了多种指针类型,通过指针类型来明确这个指针变量将存储何种类型数据的地址。
不同类型的指针:
- 指针变量声明也是先指定数据类型然后指定变量名,如
int* ptr_i;
- 定义指针变量是为了通过它间接操作数据,而要操作的数据是有不同类型的,因此在定义指针变量时区分不同的指针类型可以为未来操作数据提供方便。
5.1.4.3 给指针变量赋值
- 和普通变量一样,指针变量在被声明时如果未赋初值,则它的值是不确定的。换句话说它可能指向内存中任意区域,这种值不确定的指针被称为【野指针】。
- 如果程序不小心通过野指针访问了不确定的内存区域,则可能会引起程序崩溃或者程序、系统数据被破坏的情况。因此,给指针变量正确赋值是非常重要的。
- 如果暂时不能确定指针变量的值,可以给它赋初值
NULL
(等同于0),使之成为指向地址为0的内存空间,此时的指针被称为【空指针】。即使不小心访问了地址为0的内存空间,也只会引起当前程序崩溃,而不会造成更大的破坏。
示例:
char* ptr_c = NULL; // 将NULL赋值给指针变量,使其成为一个空指针
- 指针变量是要指向数据的,我们可以将变量、数组的首地址、
malloc
函数分配的内存空间的首地址赋值给指针变量。 - 要取得变量的首地址,可以使用
&
运算符。
创建一个空项目:
- 项目名称:L05_08_ADDRESS_OF_VARIABLE
- 位置:D:\BC101\Examples\L05
编译程序:
#include <stdio.h>
int main()
{
int x = 0;
int* ptr_x = &x;
printf("变量x的值是: %d\n", x);
printf("变量x的地址是: %p\n", &x);
printf("指针变量ptr_x的值是: %p\n", ptr_x);
printf("指针变量ptr_x的地址是: %p\n", &ptr_x);
return 0;
}
程序运行结果:
变量x的值是: 0
变量x的地址是: 0062F7C4
指针变量ptr_x的值是: 0062F7C4
指针变量ptr_x的地址是: 0062F7B8
注意,每次运行时,变量x的地址、指针变量ptr_x的值和指针变量ptr_x的地址都会不一样。
绘制变量x与ptr_x的关系:
说明:
- 整型变量x和整型指针变量ptr_x在内存中都占用4字节的内存空间,从程序运行结果上可以看这出这两块空间是不相邻的。
- 变量x所在内存空间的首地址是0062F7C4,目前这块空间中存储的值是0。
- 变量ptr_x所在内存空间的首地址是0062F7B8,目前这块空间中存储的值是0062F7C4,也就是变量x的地址。