Skip to content

换个姿势学C语言

0. 说明

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

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

这是一本非常不错的书!

5. 获取完整的牌价数据

在第4章的程序中已经实现了显示一项数据的功能——100美元折算人民币的金额,这项计算是根据【中行折算价】计算的,但外汇牌价还包括现汇买入价、现钞买入价、现汇卖出价、现钞卖出价。

存储这5种价格需要声明5个double型变量吗?在中行网站上面可以看到,有35种外汇可以选择:

html
<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函数是根据中行折算价进行货币金额转换的,得到的是单个金额 。但是外汇牌价看板程序需要获取多个价格,包括现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和中行折算价。

Snipaste_2024-03-10_00-12-26.png

这5项数据该如何存储?读者当然可以定义5个double双精度浮点数 ,然后用5个不同的函数获取不同的价格,但这种方式会大大提高程序的复杂度(35种货币都定义5个变量的话,则需要定义35 * 5 = 175个变量)。

想像一下,如果你要从超市买10个鸡蛋回家,最好的办法显然不是每个口袋装几个,而是用一种叫“蛋托”的容器装好以更方便和妥当地携带。就像下图这样:

10619577025_1692599836.jpg

  • 在程序设计中,如果要存储多个类型相同、用途相关的一组数据时,可以使用【数组】。
  • 数组的实质就是多个相同类型的变量的集合,集合的每一个元素就是一个变量,并且所有元素在内存地址上都是相邻的,这样就为处理数据带来便利。

接下来,我们将介绍如何定义和使用数组,以及如何对数组中存储的数据进行处理。

5.1.1 数据的声明方法

和变量一样,要使用一个数组,必须先声明它。

5.1.1.1 在程序中声明数组

声明变量时,我们是这样做的:

cpp
double r = 0;

声明数据的方法是这样的:

数据类型 数组名[元素数量];

如:

cpp
double rates[5];

声明了一个名叫rates的数组,用于存储某种货币的5种价格。double表示这个数组中的所有元素类型是双精度浮点型。方括号中的5表示数组一个有5个元素。

  • 数组占用内存空间大小的计算方法如下:数据元素的数量 * 单个数组元素的字节大小
  • 数组占用内存空间大小在数组声明时就已经确定,未来不能修改数组的大小。

例如,上面的数组rates,每个双精度浮点型元素占用8个字节,5个数组元素一共占用40个字节。

  • 如果程序在声明数组时没有初始化其中的元素,数组中的元素的值是不确定的,这一点和变量一样。
  • 定义数组时,方括号中只能是一个常量,而不能是变量。

像下面的程序则无法正常编译:

cpp
#include <stdio.h>

int main()
{
    int size = 5;
    int rates[size];
}

Snipaste_2024-09-25_22-50-36.png

此时提示异常表达式必须含有常量值

而像下面这样则是正常的:

cpp
#include <stdio.h>

int main()
{
    int rates[5];
}
5.1.1.2 数组的初始化

有时我们希望在定义数组时就给它的元素赋值,这时可以通过下面的方法初始化数组中的元素,即在一对大括号中依次给出数组中每一个元素的值。

cpp
#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循环来读取数据元素,并输出每个数组元素的内存地址,运行结果如下:

sh
数组元素序号: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页下面这段描述是错的:

如果初始化数组时要将所有元素都设置为同一元素,也可以这样做:

cpp
double rates[5] = -1;

这行代码将数组rates数组中的每一个元素都被赋值为-1。

测试:

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

运行结果如下:

sh
数组元素序号: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种牌价。例如:

cpp
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 显示美元中行折算价的例子 一样,配置【强制文件输出】和【附加库目录】:

并编写以下代码:

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];
    int result = GetRatesByCode("USD", rates);
    printf("%d\n", result);

    return 0;
}

此时运行代码,输出结果是1。

Snipaste_2024-10-15_23-29-32.png

以上代码中,输出了结果1,但我不知道牌价信息是否写入到数组rates中,因此需要访问数组元素。

5.1.3 访问数组元素

数组是用于存储数据的,每一个数组元素相当于一个变量。

5.1.3.1 访问数组元素的基本方法
  • 声明数组以后,可以将其中每个元素当作当作的变量来赋值和取值。
  • 访问某个元素的方法是在数组名后加一对中括号和索引值。如rates[0] = 20;
  • 中括号间的索引值表示程序要访问数组中的第几个元素,索引也被称为【下标】。
  • 数组元素从0开始计数,所以rates[0]表示数组中的第1个元素。

优化代码:

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

而此时中国银行官网上面显示结果如下:

Snipaste_2024-10-17_22-39-12.png

可以看到,我们程序运行的结果,与中行官网上面显示的是一致的。

这个时候,我们可以看到,我们代码中使用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运算的结果也不可能为负数)。
  • 应时刻牢记:如果不小心访问了不存在的数组元素,编译器不一定会警告或阻止你,而这种操作可能会带来结果不正确,程序崩溃或者安全问题。

直接对上一节的代码进行修改:

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

运行代码:

sh
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行也可以使用以下代码来实现计算数组的长度:

cpp
 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

编译程序:

cpp
#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]);
    }
}

此时,程序可以正常运行。

Snipaste_2024-10-20_23-11-24.png

如果将数组的大小调整成409500,则程序会发生异常:

cpp
#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]);
    }
}

Snipaste_2024-10-20_23-13-23.png

提示了Stack overflow异常,即为堆栈溢出错误。

C程序里所有函数中的变量、数组都是使用一块被称为【栈】的内存空间。不同的编译器分配的栈空间大小不同,如果程序在访问栈时发生了越界行为,则会造成Stack overflow栈溢出异常。

可通过以下访问查看visual studio中栈默认值。

项目属性-->链接器-->系统-->堆栈保留大小,单位为字节。系统默认为1M,即1024*1024=1048576字节。

Snipaste_2024-10-20_23-18-21.png

在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时,有警告,运行时也会提示堆栈溢出错误:

Snipaste_2024-10-22_23-38-44.png

当设置数组长度为256500时,又可以正常运行:

Snipaste_2024-10-22_23-41-03.png

也就是说实际上可分配int类型数组容量是256500,比理论的262144小得多。

5.1.4.1 使用malloc函数动态分配内存

通过上面的实验,我们知道数组的大小是有限制的,然而程序要处理的数据“体积”经常会超出这个限制,此时应该怎么做呢?

虽然编译器允许我们调整栈的限制大小,但总归是有限度的。

  • 在C语言中,可以使用malloc函数分配一块内存空间。
  • malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
  • malloc函数分配的内存位于【堆区】,是一块比【栈区】大得多的空间。
  • malloc函数是C标准库提供的函数,要使用malloc函数需要包含头文件stdlib.hmalloc函数的参数就是要分配的内存的大小(以字节为单位)。

创建一个空项目:

  • 项目名称:L05_07_MALLOC
  • 位置:D:\BC101\Examples\L05

编译程序:

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

运行程序:

sh
1GB = 1073741824 字节
正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存

查看可用内存的变化:

sh
# 在运行程序前查看可用内存
C:\>systeminfo | findstr "可用的物理内存"
可用的物理内存:   17,294 MB

# 运行程序后查看可用内存,可以看到
C:\>systeminfo | findstr "可用的物理内存"
可用的物理内存:   16,005 MB

# 程序退出后再次查看可用内存
C:\> systeminfo | findstr "可用的物理内存"
可用的物理内存:   17,192 MB

可以看到,启动程序后,消耗内存17294-16005 = 1289 MB,即除了分配1GB的动态内存外,程序本身还消耗了些内存。

此时,虽然程序能正常执行,但有两个警告:

Snipaste_2024-10-23_23-07-59.png

提示C6031警告,返回值被忽略。

由于malloc函数分配内存并不是每次都会成功,所以有两种返回值:

  • 如果分配内存失败,就返回0;
  • 如果分配内存成功,则返回这块内存区域的第1个字节的地址。

优化一下程序:

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

运行程序:

sh
1GB = 1073741824 字节
正在分配1GB内存,按回车键结束程序并由系统自动回收分配的内存
分配到的内存区域首地址:15675456

但是,使用无符号整型变量来存储内存地址是不妥当的,在C语言中有专门用于存储内存地址的数据类型--【指针】。

5.1.4.2 使用指针变量存储内存地址
  • 为了给程序员提供方便,C语言提供了专门的数据类型用于存储内存地址,这种数据类型称为【指针型】。
  • 使用这种类型的变量称为【指针变量】,简称为【指针】。
  • 指针变量也是变量的一种,也需要占用内存空间。
  • 指针变量用于存放数据的地址,而数据都是有类型的(intdoublefloat等),为了更方便地操作不同类型的数据,C语言提供了多种指针类型,通过指针类型来明确这个指针变量将存储何种类型数据的地址。

不同类型的指针:

Snipaste_2024-11-05_21-22-40.png

  • 指针变量声明也是先指定数据类型然后指定变量名,如int* ptr_i;
  • 定义指针变量是为了通过它间接操作数据,而要操作的数据是有不同类型的,因此在定义指针变量时区分不同的指针类型可以为未来操作数据提供方便。
5.1.4.3 给指针变量赋值
  • 和普通变量一样,指针变量在被声明时如果未赋初值,则它的值是不确定的。换句话说它可能指向内存中任意区域,这种值不确定的指针被称为【野指针】。
  • 如果程序不小心通过野指针访问了不确定的内存区域,则可能会引起程序崩溃或者程序、系统数据被破坏的情况。因此,给指针变量正确赋值是非常重要的。
  • 如果暂时不能确定指针变量的值,可以给它赋初值NULL(等同于0),使之成为指向地址为0的内存空间,此时的指针被称为【空指针】。即使不小心访问了地址为0的内存空间,也只会引起当前程序崩溃,而不会造成更大的破坏。

示例:

cpp
char* ptr_c = NULL; // 将NULL赋值给指针变量,使其成为一个空指针
  • 指针变量是要指向数据的,我们可以将变量、数组的首地址、malloc函数分配的内存空间的首地址赋值给指针变量。
  • 要取得变量的首地址,可以使用&运算符。

创建一个空项目:

  • 项目名称:L05_08_ADDRESS_OF_VARIABLE
  • 位置:D:\BC101\Examples\L05

编译程序:

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

程序运行结果:

sh
变量x的值是: 0
变量x的地址是: 0062F7C4
指针变量ptr_x的值是: 0062F7C4
指针变量ptr_x的地址是: 0062F7B8

注意,每次运行时,变量x的地址、指针变量ptr_x的值和指针变量ptr_x的地址都会不一样。

绘制变量x与ptr_x的关系:

Snipaste_2024-11-06_23-29-36.png

说明:

  • 整型变量x和整型指针变量ptr_x在内存中都占用4字节的内存空间,从程序运行结果上可以看这出这两块空间是不相邻的。
  • 变量x所在内存空间的首地址是0062F7C4,目前这块空间中存储的值是0。
  • 变量ptr_x所在内存空间的首地址是0062F7B8,目前这块空间中存储的值是0062F7C4,也就是变量x的地址。
5.1.4.4 用间接运算符*和指针变量访问变量的内存
  • 程序可以通过间接运算符和指针变量改变另一个变量的值。

创建一个空项目:

  • 项目名称:L05_09_CHAGE_VARIABLE_WITH_POINTER
  • 位置:D:\BC101\Examples\L05

编译程序:

cpp
#include <stdio.h>

int main()
{
    int x = 0;
    // 先将变量x的地址存入指针变量ptr_x
    int* ptr_x = &x;
    printf("变量x的值是: %d\n", x);
    printf("变量x的地址是: %p\n", &x);

    *ptr_x = 20;
    printf("变量x的值是: %d\n", x);
    printf("变量x的地址是: %p\n", &x);

    return 0;
}

程序运行结果:

sh
变量x的值是: 0
变量x的地址是: 007FFC04
变量x的值是: 20
变量x的地址是: 007FFC04

可以看到通过间接运算符*ptr_x改变了地址007FFC04开始的4个字节的值。

为什么*ptr_x改变的是地址007FFC04开始的4个字节,而不是5个、10个?

因为*ptr_x是整型指针,就是前面提到的“定义指针变量时区分不同的指针类型可以为未来操作数据提供方便”。如果ptr_x是一双精度型(double)指针,那么它会影响8个字节的内容。

  • 指针变量的第1个用途——通过它可以间接地读写指定内容地址的数据。

但这样做似乎有些多余,直接访问变量多方便,为何要用指针呢?一个重要的原因是——我们要间接访问的不一定是变量的内存地址。

5.1.4.5 用间接运算符*和指针变量访问动态分配的内存
  • 指针除了指向其他变量,还可以指向malloc函数分配的内存空间。

示例:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* ptr = (int*)malloc(40960 * sizeof(int));
    printf("分配到的内存区域首地址:%p\n", ptr);

    return 0;
}

程序运行结果:

sh
分配到的内存区域首地址:007DFFE8

这个程序为存储40960个整型值分配了内存空间,第5行的malloc(40960 * sizeof(int))是先用sizeof运算符计算整型值的字节数,再乘以40960,并将乘积作为分配的内存大小(40960个整型值所需的内存空间为 40960*4 = 163840字节)。

malloc函数的返回值被转换成整型指针,然后赋值给指针变量ptr,未来我们将通过它来访问分配到的内存空间。

也可以使用间接运算符将内存区域的值读出来:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int* ptr = (int*)malloc(40960 * sizeof(int));
    printf("分配到的内存区域首地址:%p\n", ptr);
    *ptr = 10; // 给这块内存空间第1-4字节赋值为10
    int temp = *ptr; // 取出这块内存空间的1-4字节的值,赋值给整型变量temp
    printf("整型变量temp的值是:%d\n", temp);

    return 0;
}

程序运行结果:

sh
分配到的内存区域首地址:00E6FFE8
整型变量temp的值是:10

再次说明,第7、8行代码在写、读内存空间时都是操作4个字节,并且将读写的数据都当作整型数处理,这是由于指针变量ptr是整型指针。

5.1.4.6 像访问数组一样访问动态分配的内存

上一节创建了存储40960个整型值分配了内存空间,并对1-4字节进行了读写操作,那后续5-8、9-12字节该如何操作呢?

  • 由于指针变量的值是一个表示地址的数值,因些指针变量当然是可以进行算术运算的。

如果要对5-8字节进行操作,只需要像下面这样:

cpp
*(ptr + 1) = 10;

即对下一个位置(也就是5-8字节)进行赋值。

要访问第一个整数元素,可以使用*ptr,要访问第二个整数元素,可以使用*(ptr + 1)

要访问第3个元素,可以使用*(ptr + 2), ........ 以此类推。

现在编写程序,对这40960个整数赋值,并打印出每个位置的值:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int N = 40960;
    int* ptr = (int*)malloc(N * sizeof(int));
    // 对分配的整型元素依次赋值
    for (int i = 0; i < N; i++) {
        *(ptr + i) = i + 1;
    }

    // 读取分配的整型元素的值,此处仅显示开始的5个以及最后的5个元素
    for (int i = 0; i < N; i++) {
        if (i < 5 || i >= N - 5) {
            printf("第 %d 个整型元素的值是:%d\n", i + 1, *(ptr + i));
        }
    }

    return 0;
}

程序运行结果:

sh
 1 个整型元素的值是:1
 2 个整型元素的值是:2
 3 个整型元素的值是:3
 4 个整型元素的值是:4
 5 个整型元素的值是:5
 40956 个整型元素的值是:40956
 40957 个整型元素的值是:40957
 40958 个整型元素的值是:40958
 40959 个整型元素的值是:40959
 40960 个整型元素的值是:40960

现在,通过动态分配的内存空间存储了40960个整型值,突破了对数组大小的限制。如果必要的话,还可以分配更大的空间以存储更多的数据。

这个程序有一个告警:

Snipaste_2024-12-08_21-15-44.png

严重性	代码	说明	项目	文件	行	禁止显示状态
警告	C6011	取消对 NULL 指针“ptr+i”的引用。	L05_TEST	D:\BC101\Examples\L05\L05_TEST\main.cpp	9

如果将代码进行修改,使用数组的形式:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int N = 40960;
    int* ptr = (int*)malloc(N * sizeof(int));
    // 对分配的整型元素依次赋值
    for (int i = 0; i < N; i++) {
        // *(ptr + i) = i + 1;
        ptr[i] = i + 1;
    }

    // 读取分配的整型元素的值,此处仅显示开始的5个以及最后的5个元素
    for (int i = 0; i < N; i++) {
        if (i < 5 || i >= N - 5) {
            // printf("第 %d 个整型元素的值是:%d\n", i + 1, *(ptr + i));
            printf("第 %d 个整型元素的值是:%d\n", i + 1, ptr[i]);
        }
    }

    return 0;
}

此时运行程序也可以得到相同的结果。

修改后的程序是完全可以运行的,所不同的时,第10和17行使用和数组元素一样的访问方式——中括号加下标ptr[i],之所以可以这样做是因为之前在访问数组时,编译器实际上也会把ptr[i]转换成*(ptr+i)的形式,所以ptr[i]*ptr(i)是完全等同的。

由此可见,这里的ptr虽然是一个指针变量名,也完全可以把它当作一个数组名来使用。我们终于“伪造”了一个大数组,用于存储超过栈区容量的数据。

警告

虽然我们使用动态内存分配的方式伪造了一个大数组,但ptr毕竟不是数组,而是一个指向内存区域的指针变量。不要试图使用sizeof运算符来获取这块内存区域的总字节数,表达式sizeof(ptr)的结果会是4,而不是164840,因为它计算的是指针变量ptr的大小。

在程序设计中如果进行了动态内存分配,就必须记住这块内存区域的大小,使用一个变量来存储它,或者和上面一样在循环时使用固定的数字(N,或者40960)。

5.1.4.7 动态分配的内存的注意事项

在使用malloc函数动态分配内存时,需要注意以下事项:

  • 必须检查内存分配是否成功;
  • 大多数情况下需要对malloc函数的返回值进行类型转换;
  • 避免内存访问越界;
  • 必须及时释放内存。

1)必须检查内存分配是否成功

在的些编译器里对于上面的例子会产生警告警告 C6011 取消对 NULL 指针“ptr+i”的引用,这是因为我们没有对malloc函数返回的值进行判断,当分配内存失败时,mailloc函数的返回值是0,程序不会终止,这个时候执行for循环时就可能引发错误,编译器发现了这一点并要求改正。

  • 在C语言中,表示内存地址0时,一般使用NULL来代替。

改正的方式就是对malloc函数的返回值进行判断:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int N = 40960;
    int* ptr = (int*)malloc(N * sizeof(int));
    if (ptr != NULL) {
        // 对分配的整型元素依次赋值
        for (int i = 0; i < N; i++) {
            *(ptr + i) = i + 1;
        }

        // 读取分配的整型元素的值,此处仅显示开始的5个以及最后的5个元素
        for (int i = 0; i < N; i++) {
            if (i < 5 || i >= N - 5) {
                printf("第 %d 个整型元素的值是:%d\n", i + 1, *(ptr + i));
            }
        }
    } else {
        printf("动态分配内存失败!");
    }

    return 0;
}

此时可以正常运行程序,并且编译器没有警告了!

Snipaste_2024-12-08_21-43-14.png

温馨提示

通过指针变量访问内存地址前,检查指针变量是否为NULL是必须养成的编程习惯。

2)大多数情况下需要对malloc函数的返回值进行类型转换

  • malloc函数是一个通用的函数,动态分配的内存可以用于存储任何类型的数据。
  • malloc函数返回的内存地址是“无类型”的(表示为void),void是空的、无效的、缺乏的含义,void*表示无类型指针,但和int*一样它也是一个存储内存地址的32位整型值,可以把它转换成你需要使用的指针类型。

如:

cpp
int* ptr = (int*)malloc(N * sizeof(int));

如果要使用这块内存存储其他类型的数据,则也可以进行类似的转换。如:

cpp
double* ptr_d = (double*)malloc(1024);  // 转换为双精度浮点型指针
float* ptr_f = (float*)malloc(sizeof(float)*40960);  // 转换为单精度浮点型指针

3)避免内存访问越界

  • 使用动态分配的内存时同样要避免内存访问越界。
  • 内存访问越界是指访问了分配区域以外的内存。例如你用malloc函数分配了1024个字节的内存,但是由于程序错误而访问了第1024个字节内存空间以外的一个或多个字节,这不会引起编译错误,在运行时有可能也不会看到明显的异常。但从第1025个字节开始的内存空间可能有其他用途,也可能正在被另一个程序应用程序使用。内存越界可能导致不可预料的结果,有时会引起程序崩溃,有时会导致程序运行不正常,有时会引发安全性问题。
  • 相对于其他语言,C语言的程序员对底层资源有更大的访问权限,因此在编程时也要更谨慎!!!

4)必须及时释放内存

  • 使用malloc函数分配的内存空间在程序结束时会自动释放,但如果程序一直没有结束则这块空间会一直被占用。
  • 因此C语言的程序员应养成一个好习惯——及时释放使用完的内存。
  • 如果不停地申请空间却没有及时释放或者干脆不释放,会造成占用的内存不停地增长,这就是所谓的【内存泄露】。

温馨提示

内存泄露指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄露会因为减少可用内存的数量从而降低计算机的性能。在最糟糕的情况下,过多的可用内存被分配掉会导致操作系统或应用程序崩溃。

  • 释放内存使用free函数,它的声明位于stdlib.h头文件中,不需要重复包含。
  • 释放内存时将存储空间首地址的指针变量ptr作为参数调用free函数即可。如free(ptr)
  • 操作系统会根据指针变量ptr的值查询之前内存分配的记录,然后根据记录将这块内存标记为【未使用】,之后其他程序就可以使用这块内存了。

释放内存需要注意的是:

注意

  • 释放内存时只需要向free函数传入内存空间的首地址就可以了,这块内存空间具体有多大,操作系统在分配内存时就已经记录过。
  • 如果传入的不是分配内存空间时得到的首地址,则会发生错误。
  • 调用了free函数后,对应内存的内容不会立刻被破坏,直到该内存被其他程序使用,里面的内容才会被覆盖。

加上free释放内存后的代码:

cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int N = 40960;
    int* ptr = (int*)malloc(N * sizeof(int));
    if (ptr != NULL) {
        // 对分配的整型元素依次赋值
        for (int i = 0; i < N; i++) {
            *(ptr + i) = i + 1;
        }

        // 读取分配的整型元素的值,此处仅显示开始的5个以及最后的5个元素
        for (int i = 0; i < N; i++) {
            if (i < 5 || i >= N - 5) {
                printf("第 %d 个整型元素的值是:%d\n", i + 1, *(ptr + i));
            }
        }

        // 使用完分配的内存后,要及时释放
        free(ptr);
    } else {
        printf("动态分配内存失败!\n");
    }

    return 0;
}

在编码习惯上,建议在输入malloc函数的代码行后,立刻在下面输入一行free函数的调用语句,像下面这样:

cpp
int* ptr = (int*)malloc(N * sizeof(int));
free(ptr);

然后再在两行之间加入使用这块内存空间的代码,这样就不会忘记释放内存了!

  • 如果一个指针变量的值为0(NULL),尝试使用free函数释放它会导致程序崩溃。

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