• 【《C Primer Plus》读书笔记】第10章:数组和指针


    10.1 一维数组

    C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

    数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、…、runoob99,而是声明一个数组变量,比如 runoob,然后使用 runoob[0]、runoob[1]、…、runoob[99] 来代表一个个单独的变量。

    所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。

    在这里插入图片描述
    数组中的特定元素可以通过索引访问,第一个索引值为 0。

    在这里插入图片描述

    声明数组

    在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:

    type arrayName [ arraySize ];
    
    • 1

    这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:

    double balance[10];
    
    • 1

    现在 balance 是一个可用的数组,可以容纳 10 个类型为 double 的数字。

    初始化数组

    在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:

    double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
    
    • 1

    大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。

    如果您省略掉了数组的大小,编译器会根据初始化列表的项数来确定数组的大小。因此,如果:

    double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
    
    • 1

    您将创建一个数组,它与前一个实例中所创建的数组是完全相同的。

    如果不初始化数组,数组元素存储未初始化的值,是垃圾值。

    当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0。

    有时需要把数组设置为只读,这样,程序只能从数组中检索值,不能把新值写入数组。
    要创建只读数组,应该用 const 声明和初始化数组。例如:

    const int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    
    • 1

    注意:C 不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的行式赋值。

    下面是一个为数组中某个元素赋值的实例:

    balance[4] = 50.0;
    
    • 1

    上述的语句把数组中第五个元素的值赋为 50.0。所有的数组都是以 0 作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去 1。以下是上面所讨论的数组的的图形表示:

    在这里插入图片描述

    下图是一个长度为 10 的数组,第一个元素的索引值为 0,第九个元素 runoob 的索引值为 8:

    在这里插入图片描述

    指定初始化器(C99

    C99 增加了一个新特性:指定初始化器。可以初始化指定的数组元素。例如:

    int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
    
    • 1

    指定初始化器的重要特性:

    1. 如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。
    2. 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。

    访问数组元素

    数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:

    double salary = balance[9];
    
    • 1

    上面的语句将把数组中第 10 个元素的值赋给 salary 变量。

    10.2 多维数组

    C 语言支持多维数组。多维数组声明的一般形式如下:

    type name[size1][size2]...[sizeN];
    
    • 1

    例如,下面的声明创建了一个三维 5 . 10 . 4 整型数组:

    int threedim[5][10][4];
    
    • 1

    二维数组

    多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:

    type arrayName [ x ][ y ];
    
    • 1

    其中,type 可以是任意有效的 C 数据类型,arrayName 是一个有效的 C 标识符。一个二维数组可以被认为是一个带有 x 行和 y 列的表格。下面是一个二维数组,包含 3 行和 4 列:

    int x[3][4];
    
    • 1

    在这里插入图片描述
    因此,数组中的每个元素是使用形式为 a[ i , j ] 的元素名称来标识的,其中 a 是数组名称,i 和 j 是唯一标识 a 中每个元素的下标。

    初始化二维数组

    多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组。

    int a[3][4] = {  
     {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
     {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
     {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    内部嵌套的括号是可选的,下面的初始化与上面是等同的:

    int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
    
    • 1

    访问二维数组元素

    二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。例如:

    int val = a[2][3];
    
    • 1

    上面的语句将获取数组中第 3 行第 4 个元素。您可以通过上面的示意图来进行验证。

    10.3 指针

    学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。

    查找地址:& 运算符

    正如您所知道的,每一个变量都有一个内存位置。而一元 & 运算符给出变量的存储地址。

    如果 a 是变量名,那么 &a 就是变量的地址。可以把地址看成是变量在内存中的位置。

    示例:

    #include 
    #include 
    int main(void)
    {
        // system("chcp 65001"); /* cmd chcp 命令切换字符编码为 utf-8 以显示中文 */
        int var = 10;
        int *p_var = &var;
        printf("var的地址:%p\n", p_var);
    
        system("pause");
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    在这里插入图片描述

    图例:

    在这里插入图片描述

    通过上面的实例,我们了解了什么是内存地址以及如何访问它。接下来让我们看看什么是指针。

    什么是指针?

    指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

    type *var_name;
    
    • 1

    在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:

    int    *ip;    /* 一个整型的指针 */
    double *dp;    /* 一个 double 型的指针 */
    float  *fp;    /* 一个浮点型的指针 */
    char   *ch;    /* 一个字符型的指针 */
    
    • 1
    • 2
    • 3
    • 4

    所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

    不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

    如何使用指针?

    使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:

    示例:

    #include 
    #include 
    int main(void)
    {
        int var = 20; /* 实际变量的声明 */
        int *ip;      /* 指针变量的声明 */
    
        ip = &var; /* 在指针变量中存储 var 的地址 */
    
        printf("var 变量的地址: %p\n", &var);
    
        /* 在指针变量中存储的地址 */
        printf("ip 变量存储的地址: %p\n", ip);
    
        /* 使用指针访问值 */
        printf("*ip 变量的值: %d\n", *ip);
    
        system("pause");
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    运行结果:

    在这里插入图片描述

    C 中的 NULL 指针

    在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。

    NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:

    #include 
    #include 
    int main(void)
    {
        int *ptr = NULL;
    
        printf("ptr 的地址是 %p\n", ptr);
    
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当上面的代码被编译和执行时,它会产生下列结果:

    在这里插入图片描述
    在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

    如需检查一个空指针,您可以使用 if 语句,如下所示:

    if(ptr)     /* 如果 p 非空,则完成 */
    if(!ptr)    /* 如果 p 为空,则完成 */
    
    • 1
    • 2

    10.4 指针和数组

    数组名是一个指向数组中第一个元素的常量指针,即数组首元素的地址。

    因此,在下面的声明中:

    double balance[50];
    
    • 1

    balance 是一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此,下面的程序片段把 p 赋值为 balance 的第一个元素的地址:

    double *p;
    double balance[10];
    
    p = balance;
    
    • 1
    • 2
    • 3
    • 4

    使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

    一旦您把第一个元素的地址存储在 p 中,您就可以使用 p、(p+1)、*(p+2) 等来访问数组元素。下面的实例演示了上面讨论到的这些概念:

    #include 
    #include 
    int main()
    {
        /* 带有 5 个元素的整型数组 */
        double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
        double *p;
        int i;
    
        p = balance;
    
        /* 输出数组中每个元素的值 */
        printf("使用指针的数组值\n");
        for (i = 0; i < 5; i++)
        {
            printf("*(p + %d) : %f\n", i, *(p + i));
        }
    
        printf("使用 balance 作为地址的数组值\n");
        for (i = 0; i < 5; i++)
        {
            printf("*(balance + %d) : %f\n", i, *(balance + i));
        }
    
        system("pause");
        return 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

    当上面的代码被编译和执行时,它会产生下列结果:

    在这里插入图片描述

    在上面的实例中,p 是一个指向 double 型的指针,这意味着它可以存储一个 double 类型的变量。一旦我们有了 p 中的地址,*p 将给出存储在 p 中相应地址的值,正如上面实例中所演示的。

    10.5 指针的算术运算

    C 指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。

    假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:

    ptr++
    
    • 1

    在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。

    我们概括一下:

    • 指针的每一次递增,它其实会指向下一个元素的存储单元。
    • 指针的每一次递减,它都会指向前一个元素的存储单元。
    • 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。

    递增一个指针

    我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。

    下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

    #include 
    #include 
    const int MAX = 3;
    int main(void)
    {
        int var[] = {10, 100, 200};
        int i, *ptr;
    
        /* 指针中的数组地址 */
        ptr = var;
        for (i = 0; i < MAX; i++)
        {
    
            printf("存储地址:var[%d] = %p\n", i, ptr);
            printf("存储值:var[%d] = %d\n", i, *ptr);
    
            /* 指向下一个位置 */
            ptr++;
        }
    
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当上面的代码被编译和执行时,它会产生下列结果:

    在这里插入图片描述

    递减一个指针

    同样地,对指针进行递减运算,即把值减去其数据类型的字节数,如下所示:

    #include 
    #include 
    const int MAX = 3;
    int main(void)
    {
        int var[] = {10, 100, 200};
        int i, *ptr;
    
        /* 指针中最后一个元素的地址 */
        ptr = &var[MAX - 1];
        for (i = MAX; i > 0; i--)
        {
    
            printf("存储地址:var[%d] = %p\n", i - 1, ptr);
            printf("存储值:var[%d] = %d\n", i - 1, *ptr);
    
            /* 指向下一个位置 */
            ptr--;
    
            system("pause");
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当上面的代码被编译和执行时,它会产生下列结果:

    在这里插入图片描述

    指针的比较

    指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。

    下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &var[MAX - 1],则把变量指针进行递增:

    #include 
    #include 
    const int MAX = 3;
    int main(void)
    {
        int var[] = {10, 100, 200};
        int i, *ptr;
    
        /* 指针中第一个元素的地址 */
        ptr = var;
        i = 0;
        while (ptr <= &var[MAX - 1])
        {
    
            printf("存储地址:var[%d] = %p\n", i, ptr);
            printf("存储值:var[%d] = %d\n", i, *ptr);
    
            /* 指向上一个位置 */
            ptr++;
            i++;
        }
    
        system("pause");
        return 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

    当上面的代码被编译和执行时,它会产生下列结果:

    在这里插入图片描述

    10.6 保护数组中的数据

    处理数组的函数通常都需要使用数组的原始数据,会修改原数组的元素的值。例如:

    #include 
    #include 
    const int MAX = 3;
    void add(int a[], int n)
    {
        for (int i = 0; i < n; i++)
            a[i]++;
    }
    int main(void)
    {
        int var[] = {10, 100, 200};
        printf("Before: ");
        for (int i = 0; i < 3; i++)
            printf("%d ", var[i]);
        printf("\n");
        add(var, 3);
        printf("After: ");
        for (int i = 0; i < 3; i++)
            printf("%d ", var[i]);
        printf("\n");
    
        system("pause");
        return 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

    运行结果:

    在这里插入图片描述

    其实,有时候修改数组的原始数据并不是程序的本意。我们可以对形参使用 const 来保护数组中的数据。

    这样使用 const 并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。

    修改上述程序的 add() 函数:

    void add(const int a[], int n)
    {
        for (int i = 0; i < n; i++)
            a[i]++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再次编译和运行,报错:

    在这里插入图片描述

    10.7 变长数组

    定义数组时,如果数组的元素个数通过变量指定,则这样的数组称为变长数组。

    例如:

    int n,m;
    n = 5;
    int a[n]; //a就是一维变长数组
    
    • 1
    • 2
    • 3

    在定义变长数组之前可以通过变量指定元素个数,如果变长数组已经定义好了,则无法再改变元素个数。

    注意:

    1. 变长数组 不能初始化。
    2. 变长数组 不能在函数外定义,也就是不能定义成全局的数组。
    3. 变长数组的赋值和打印与普通数组相同。
      字符数组—通过strcpy赋值,%s打印
      其他类型数组–通过循环语句赋值和打印

    C99:复合字面量

    复合字面量,是除了符号常量之外的常量

    例如:5是int的字面常量,1.2是double的字面常量,'a’是char的字面常量,"hello"是字符串的字面常量。一般来说,字符常量可以方便初始化变量,而且可以用于相应运算。其中较为特殊的还有数组的复合字面常量。形如:(int [2]){1, 2}

    我们可以通过这样的字面常量来初始化数组:

    int a[2] = {1, 3};,这时候我们省略了等号后面的数组类型名是没有问题的。

    还有可以使用这样常量作为需要的数组参数,这时候就需要标注清楚数组的类型名,对于数组内元素都已知情况下,我们也可以省略数组的元素数目:

    (int []){1, 2, 5, 7, 10},编译器将会将其识别为int [5]类型数组。

    但是符合字面量作为一种匿名类型,我们必须在创建时候使用,否则就将会被废弃,我们可以通过一个指针来接受它的地址(它本身就作为它首元素的地址,对于C语言的各种类型的数组都是如此):

    int *a = (int []){1, 2, 3, 5, 10, 15};
    
    • 1
  • 相关阅读:
    【word】设置背景为绿豆沙保护色
    MySQL:日志系统介绍 | 错误日志 | 查询日志 | 二进制日志:bin-log数据恢复实践 | 慢日志查询
    C#/.NET/.NET Core优秀项目和框架2024年6月简报
    Redis 有哪些适合的场景?
    【Java基础】类的定义和对象的使用
    熵基科技深交所上市:市值82亿 应收账款账面价值近3亿
    C++ Reference: Standard C++ Library reference: Containers: array: array: cbegin
    模糊测试面面观 | 模糊测试是如何发现异常情况的?
    PCB测试四大方式你都了解吗?DFM的重要性不容忽视
    【VS2019 Qt5 VTK9.2】临时解决配置相关问题的简单方法
  • 原文地址:https://blog.csdn.net/ProgramNovice/article/details/126867014