当前位置: 首页 > news >正文

第10章 数组和指针

目录

  • 10.1 数组
        • 10.1.1 初始化
        • 10.1.2 只读初始化项目(C99)
        • 10.1.3 为数组赋值
        • 10.1.4 数组边界
        • 10.1.5 指定数组大小
  • 10.2 多维数组
  • 10.3 指针和数组
  • 10.4 函数、数组和指针
  • 10.5 指针操作
  • 10.6 保护数组内容
        • 10.6.1 对形式参量使用const
        • 10.6.2 有关const的其它内容
  • 10.7 指针和多维数组
  • 10.8 变长数组(VLA)
  • 10.9 复合文字
  • 10.13 编程练习
        • 习题12
        • 习题13

10.1 数组

    数组由一系列类型相同的元素构成。数组声明中包括数组元素的数目和元素的类型。[ ]表示标识符为数组,方括号中的数字指明数组所包含的元素数目。

10.1.1 初始化
  • 可以用花括号括起来的一系列数值来初始化数组。数值之间用逗号隔开,在数值和逗号之间使用空格符。
  • 有时需要使用 只读数组,也就是程序从中读取数值,但是程序不向数组中写数据。这种情况下用关键字const声明并初始化数组。和普通变量一样,需要在声明const数组时对其进行初始化,因为在声明后,不能再对它赋值
  • 与普通变量相似,在初始化之前数组元素的数值不定。编译器使用的数值是分配给其的存储单元中已有的不确定数值。
  • 初始化过程中,当数值的数目少于数组元素的个数,多余的数组元素被初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中存储的是无用的数值;但是如果部分初始化数组,未初始化的元素被设置为0
  • 如果初始化列表中项目的个数大于数组大小,编译器会认为这是一个错误。您可以省略[ ]中的数字,从而让编译器自动匹配数组大小和初始化列表中的项目数目。
10.1.2 只读初始化项目(C99)
  • C99增加了一个新特性:指定初始化项目。此特性允许选择对某些元素进行初始化。C99规定,在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素:int arr[6]={[5]=212};把arr[5]初始化为212
  • 指定初始化项目有两个重要特性
    • 第一,如果一个指定初始化项目后跟有不止一个值,例如在序列[4]=31,30,31中这样,则这些数值将用来对后续的数组元素初始化。
    • 第二,如果多次对一个元素进行初始化,则最后的一次有效。
10.1.3 为数组赋值
  • C不支持把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表形式进行赋值(初始化的时候除外)
10.1.4 数组边界
  • 数组索引不能超过数组边界,编译器不会为您检查出这种错误。使用超出数组边界的索引会改变其它变量的数值。
10.1.5 指定数组大小
  • 声明数组时用整数常量表达式指明数组大小。整数常量表达式是由整数常量组成的表达式。sizeof表达式被认为是一个整数常量,而(C++不一样)一个const值却不是整数常量。并且该表达式的值必须大于0
  • C99标准通过变量声明数组,这种称为变长数组,简称VLA。



10.2 多维数组

  • 前面讨论的初始化中,数据个数和数组大小不匹配的问题同样适用于此处每一行。就是数组的部分被初始化时,剩余部分被默认初始化为0
  • 多维数组初始化时可以省略内部的花括号,只保留最外面的一对花括号。只要保证数值个数正确,初始化效果是一样的。如果数值个数不够,先保证前面的元素得到赋值,后面的默认初始化为0。
  • 通常处理三维数组需要3重嵌套循环,处理思维数组需要4重嵌套循环,对于其它多为数组,以此类推。



10.3 指针和数组

  • 数组名同时也是该数组首元素的地址
  • 在C中,对一个指针加1的结果是对该指针增加1个存储单元。对于数组而言,地址会增加到下一个元素的地址,而不是下一个字节。这就是为什么声明指针时必须声明它所指向对象的类型。计算机需要知道存储对象所用的字节数,所以只有地址信息是不够的
  • 在指针前运用运算符*就可以得到该指针所指向的对象的数值
  • 可以使用指针来标记数组;反之亦然,也可以用数组方式来访问指针

    需要提醒自己的一点是,*dates这种形式也可以放在赋值运算符=的左边,解释成对该指针所指向的内存单元赋值。我以前对间接运算符的理解都是取值,把间接运算符称作取值运算符,把*运算符理解狭隘了,谨记!!!


10.4 函数、数组和指针

  • 在函数原型或函数定义头的场合中,可以用int * ar代替int ar[]。无论在任何情况下,形式int * ar都表示ar是指向int的指针。形式int ar[]也可以表示ar是指向int的指针,但只是在声明形式参量时才可以这样使用
使用数组的函数需要知道何时开始和何时结束数组:
(1)使用一个指针参量来确定数组的开始,使用一个整数参量来指明数组的元素个数。
(2)传递两个指针,第一个指针指明数组的起始地址,第二个指针指明数组的结束地址。 第二个指针指向的结束地址是数组最后一个元素之后的地址,不是最后一个元素的地址。C保证为数组分配存储空间的时候,指向数组之后的第一个位置的指针也是合法的,但是对数组之后第一个位置的值不保证
  • 在C中,两个表达式ar[i]和*(ar+i)的意义是等价的。而且不管ar是一个数组名还是一个指针变量,这两个表达式都可以工作。然而只有当ar是一个指针变量时,才可以使用ar++这样的表达式
  • 再次提醒自己,注意*ar不能简单的理解成到ar指定的位置取值,取值只是间接运算符的一部分工作,*ar可以放在赋值运算符=的左边对ar指向的位置进行赋值,还要(*ar)++这样的形式,取出ar指向位置的值,自增,然后再保存到原位置。切记,注意间接运算符*的用法。



10.5 指针操作

  • 赋值:可以把地址赋给指针,地址应该和指针类型兼容。也就是说不能把一个double类型的地址赋给一个指向int类型的指针。
  • 求值或取值:运算符*可取出指针指向地址中存储的数值。再次提醒自己,不能只记得通过*ptr的形式取值,不止这一个用法,*ptr还可以出现在赋值运算符=的左边,对ptr所指向的内存单元进行赋值,(*ptr)++取出ptr所指向内存单元的值,自增后,再存回原内存单元
  • 取指针地址:指针变量同其它变量一样具有地址和数值,使用运算符&可以得到存储指针本身的地址。
  • 将一个整数加给指针:可以使用+运算符来把一个整数加给一个指针,或者把一个指针加给一个正数。两种情况下,这个整数都会和指针所指类型的字节数相乘,然后所得结果会加到初始地址上。
  • 增加指针的值:可以通过一般的加法或增量运算符来增加一个指针的值。
  • 从指针中减去一个整数:可以使用-运算符来从一个指针中减去一个整数。如果相减的结果超出了初始指针所指向的数组的范围,那么这个结果是不确定的。
  • 减小指针的值:指针当然也可以做减量运算。
  • 求差值:可以求出两个指针的差值。通过对分别指向同一数组内两个元素的指针求差值,以求出元素之间的距离。差值的单位是相应类型的大小。有效指针差值运算的前提是参加运算的两个指针是指向同一数组(或是其中之一指向数组后面第一个地址)。指向两个不同数组的指针之间的差值运算可能会得到一个数值结果,但也可能会导致一个运行时错误
  • 比较:可以使用关系运算符来比较两个指针的值,前提是两个指针具有相同的类型。
  • C保证指向数组元素的指针和指向数组后的第一个地址的指针都是有效的。但是如果指针在进行了增量或减量运算后超出了这个范围,后果将是未知的。另外,可以对指向一个数组元素的指针进行取值运算。但不能对指向数组后第一个地址的指针进行取值运算,尽管这样的指针是合法的
  • 使用指针,有一个规则需要特别注意:不能对未初始化的指针取值。指针未初始化,它的值是随机的,可能对系统危害不大,但也许会覆盖程序的数据或者代码,甚至导致程序崩溃。当创建一个指针时,系统只分配了用来存储指针本身的内存空间,并不分配用来存储数据的内存空间。因此在使用指针之前,必须给它赋予一个已分配的内存地址



10.6 保护数组内容

    对于处理数组的函数,只能传递指针,原因是这样能使程序的效率更高。如果通过值向函数传递数组,那么函数中必须分配足够存放一份原数组的拷贝的存储空间,然后把原数组的所有数据复制到这个新数组中。如果简单地把数组的地址传递给函数,然后让函数直接读写原数组,程序的效率会更高

10.6.1 对形式参量使用const
  • 如果设计意图是操作数组时不改变原数组的数据,那么可以在函数原型和定义的形式参量声明中使用关键字const。告知编译器:函数应当把形式参量所指向的数组作为包含常量数据的数组对待。如果你意外的试图改变原数组的数据,编译器会报错。
  • 需要理解这样使用const并不要求原始数组是固定不变的;这只是说明函数在处理数组时,应把数组当作是固定不变的。使用const可以对数组进行保护,就像按值传递可以对基本类型进行保护一样,可以阻止函数修改调用函数中的数据。
10.6.2 有关const的其它内容
  • 前面介绍过可const来创建符号常量,还可以创建数组常量、指针常量以及指向常量的指针。要注意指针常量和指向常量的指针之间的区别,指向常量的指针不能用于修改数值,类似于10.6.1对函数中数组形式参量用const。
  • 可以使用关键字const来声明并初始化指针,以保证指针不会指向别处,关键在于const的位置。double * const pc=rates;pc就是指针常量;const double * pc=rates;pc就是指向常量的指针。 使用两个const来创建指针,这个指针既不可以更改所指向的地址,也不可以修改所指向的数据。const double * const pc=rates;
关于指针赋值和const有一些规则需要注意:
(1)将常量或非常量数据的地址 赋给指向常量的指针是合法的。有一个前提是只进行一层间接运算,
(2) 只有非常量数据的地址才可以赋给普通指针,这是合理的。否则,您就可以使用指针来修改被认为是常量的数据。



10.7 指针和多维数组

  • 二维数组int zippo[4][2];要注意一维数组中数组名是地址,zippo[0]表示数组中的int类型元素,但在多维数组中,数组名还是地址,形如zippo[0]就不能看成int类型的元素了,而是看作一个地址,或者说zippo[0]确实是zippo数组中的元素,只不过zippo数组中的元素还是数组,比如这里zippo[0]代表zippo数组中第一个元素,此元素是包含2个int元素的数组,zippo[0]也就变成了这个包含2个元素的数组的地址。
  • 二维数组名必须两次取值才可以取出数组中存储的数据。这可以两次使用间接运算符()来实现,或两次使用方括号运算符([]),(也可以一次和一次[]来实现,但一般不这么用)。
  • 当您正好有一个指向二维数组的指针并需要取值时,最好不要使用指针符号,而应该用更简单的数组符号
因为数组和指针类似,以下这两种形式都有点像二维数组,要注意以下两种声明方式的区别:
(1) int (* pz) [2]处理规整的二维数组,()优先级更高,所以pz是个指针,指向包含2个int元素的数组。
(2) int * pax[2]不能作为形参处理二维数组,因为指针和数组类似,所以看起来好像能处理二维数组。[]运算符优先级更高,所以pax是数组,数组中包含2个元素,每个元素都是指向int的指针。
  • 对上一节指针赋值和const规则的补充:把const指针赋给非const指针是错误的,因为可能会通过非const指针修改const数据。但是把非const指针赋给const指针是允许的,这样做有一个前提:只进行一层间接运算。进行两次间接运算,这样的赋值也不安全。
  • 函数的定义和声明中,如果形参是多维数组,除了最左边的方括号可以留空之外,其它都需要填写数值。这个首方括号表示这是一个指针,而其它方括号描述的是所指向对象的数据类型。



10.8 变长数组(VLA)

  • C99标准之前要求数组的维数必须是常量,因此不能用一个变量来代替。C99标准引入了变长数组,它允许使用变量定义数组各维。变长数组中的“变”并不表示创建数组后,您可以修改其大小。变长数组创建后就保持不变。“变”的意思是说其维的大小可以用变量来指定
  • C99标准规定,可以省略函数原型中的名称,但是如果省略了名称,则需要用星号来代替省略的维数
  • 变长数组有一些限制。变长数组必须是自动存储类的,这意味着它们必须在函数内部或作为函数参量声明,而且声明时不可以进行初始化
  • 函数定义参量列表中的变长数组声明实际上并没有创建数组。和老语法一样,变长数组名实际上是指针,也就是具有变长数组参量的函数实际上直接使用原数组,因此它有能力修改作为参数传递进来的数组。
  • 变长数组动态分配存储单元。这表示可以在程序运行时指定数组的大小。常规的C数组是静态存储分配的,也就是说数组大小在编译时已经确定

    看过汇编码后就会发现,无论传统数组还是变长数组,它们最后的空间分配都要通过指令执行在栈中分配空间,只不过传统数组编译时已经确定了要分配多少空间,执行时不需要再计算,执行指令即可;变长数组(VLA)编译时无法确定要分配多少空间,执行时才能确定,而且还需要数据对齐,很耗费CPU资源,如果变长数组过大还可能导致栈溢出。所以变长数组通常适用于小数组。

    以下是查询deepseek,得到的区分传统数组和变长数组的内容

步骤传统数组变长数组(VLA)
空间分配sub esp,<固定值>计算大小>对齐>sub esp,eax
地址计算编译时固定分配运行时基于esp实时计算
额外指令大小计算、对齐处理等指令
性能影响的实际表现:
传统数组:纯内存操作,无计算开销。
变长数组:每次进入作用域时需重新计算大小(循环中可能重复计算);对齐操作可能引入额外指令(如and eax, -16);在嵌入式系统中可能影响实时性(非确定性时间开销)。变长数组的栈指针管理也比传统数组更复杂。

  • 从硬件视角看差异
    • 传统数组的硬件行为
      • 编译后生成的指令是静态的、可预测的。
      • CPU只需执行简单的栈指针移动(sub esp)和内存访问。
    • 变长数组(VLA)的硬件行为
      • 需要CPU动态执行整数乘法(n * sizeof(int))、加法对齐等操作。
      • 对低端MCU可能不友好(如无硬件乘法器的8位MCU需软件模拟乘法)。

  • VLA的核心就是在传统数组的基础上增加了运行时计算步骤。但这种"简单的计算"带来了深远影响
    • 安全性问题:传统数组编译时可以发现过大尺寸,可以及时调整;VLA运行时才能确定大小,过大将导致栈溢出。
    • 确定性丧失:实时系统要求严格的时间确定性,VLA的动态计算使其难以满足。
    • 调试难度:GDB等调试器难以显示VLA的完整内容(大小在运行时确定)。

  • 实际工程建议
    • 优先使用传统数组:除非必须动态尺寸且栈空间绝对安全。
    • 替代方案
      • int *arr = malloc(n * sizeof(int)); free(arr);动态分配(堆内存)。
      • #define MAX_N 100 int arr[MAX_N]; 固定最大尺寸(更安全),实际使用部分。
    • 嵌入式开发禁用VLA:许多安全规范(如MISRA C)明确禁止VLA



10.9 复合文字

  • 在C99标准出现之前,数组参数的情况是不同的:可以传递数组,但没有所谓的数组常量可供传递
  • C99新增了复合文字。文字是非符号常量。例如:5是int类型的文字,81.3是double类型的文字,‘Y’是char类型的文字,“elephant”是字符串文字
对于数组来说,复合文字看起来像是在数组的初始化列表前面加上用圆括号括起来的类型名:
普通数组声明方法:int diva[2]={10,20};
复合文字, 创建了一个包含两个int值得无名称数组:(int [2]){10,20},初始化一个命名数组时可以省略数组大小, 初始化一个复合文字时也可以省略数组大小,编译器会自动计算元素的数目:(int []){50,20,90}
  • 由于复合文字没有名称,因此不可能在一个语句中创建它们,然后在另一个语句中使用。而是必须在创建它们的同时通过某种方法来使用它们一种方法是使用指针保存其位置。int * pt1; pt1=(int [2]){10,20};,复合文字也可以作为实际参数被传递给带有类型与之匹配的形式参量的函数。给函数传递信息而不必创建数组的做法,是复合常量的通常使用方法。



10.13 编程练习

编程练习一如既往的简单,只要对指针和数组的基础知识掌握牢靠,这些练习就很简单。多维数组中地址和元素的理解可能要花点时间。编程过程中还有一些个人感受,如下:
(1)编写处理数组的函数时,要对实现的功能有基础认识, 如果实现功能过程中不需要修改数组,那在定义函数形参时最好用const关键字对形参中的数组加以修饰,防止对原数组误操作,破坏数组内容
(2)函数定义时,形参中声明的数组类型变量, 不管是传统数组,还是变长数组,都没有创建新数组,数组名实际上是一个指针,我们通过这个指针,使用原数组

    以下是最后两题代码贴出来,这两题实现的功能一样,只不过一个用的传统数组方式作参数,一个用的是变长数组方式,可以作为对比看看。

习题12
#include <stdio.h>
#include <windows.h>void get_arr(double [],int);
double get_first_double(void);
void get_subave(const double [],int);
void get_ave(const double [][5],int);
void get_max(const double [][5],int);
void show_arr(const double [][5],int);int main(void)
{SetConsoleOutputCP(65001); // 设置为UTF-8  我用的Editplus写代码,需要调用此函数让控制台能支持中文double dec[3][5];printf("请输入3个数集,每个数集包含5个double值!\n");printf("第一个:");get_arr(dec[0],5);printf("\n");printf("第二个:");get_arr(dec[1],5);printf("\n");printf("第三个:");get_arr(dec[2],5);printf("\n");show_arr(dec,3);printf("每个数集的平均值!\n");printf("第一个:");get_subave(dec[0],5);printf("\n");printf("第二个:");get_subave(dec[1],5);printf("\n");printf("第三个:");get_subave(dec[2],5);printf("\n");get_ave(dec,3);get_max(dec,3);return 0;
}void get_arr(double dec[],int size)
{int i;for (i=0;i<size ;i++ ){dec[i]=get_first_double();}}double get_first_double()
{double dec;while(scanf("%lf",&dec)!=1){printf("请输入浮点数!!\n");while(getchar()!='\n'){continue;}}while(getchar()!='\n'){continue;}return dec;
}void get_subave(const double dec[],int size)
{int i;double total=0;for (i=0;i<size ;i++ ){total+=dec[i];}printf("该数集的平均值是:%.2f\n",total/size);
}void get_ave(const double dec[][5],int row)
{int i,j;double total=0;for (i=0;i<row ;i++ ){for (j=0;j<5 ;j++ ){total+=dec[i][j];}}printf("所有数据的平均值:%.2f\n",total/(row*5));
}void get_max(const double dec[][5],int row)
{int i,j;double max=dec[0][0];for (i=0;i<row ;i++ ){for (j=0;j<5 ;j++ ){if (max<dec[i][j]){max=dec[i][j];}}}printf("所有数中的最大值是:%.2f\n",max);}void show_arr(const double dec[][5],int row)
{int i,j;for (i=0;i<row ;i++ ){for (j=0;j<5 ;j++ ){printf("%8.2f",dec[i][j]);}printf("\n");}
}
习题13
#include <stdio.h>
#include <windows.h>void get_arr(int,double [*]);
double get_first_double(void);
void get_subave(int,const double [*]);
void get_ave(int,int,const double [*][*]);
void get_max(int,int,const double [*][*]);
void show_arr(int,int,const double [*][*]);int main(void)
{SetConsoleOutputCP(65001); // 设置为UTF-8  我用的Editplus写代码,需要调用此函数让控制台能支持中文double dec[3][5];printf("请输入3个数集,每个数集包含5个double值!\n");printf("第一个:");get_arr(5,dec[0]);printf("\n");printf("第二个:");get_arr(5,dec[1]);printf("\n");printf("第三个:");get_arr(5,dec[2]);printf("\n");show_arr(3,5,dec);printf("每个数集的平均值!\n");printf("第一个:");get_subave(5,dec[0]);printf("\n");printf("第二个:");get_subave(5,dec[1]);printf("\n");printf("第三个:");get_subave(5,dec[2]);printf("\n");get_ave(3,5,dec);get_max(3,5,dec);return 0;
}void get_arr(int size,double dec[size])
{int i;for (i=0;i<size ;i++ ){dec[i]=get_first_double();}}double get_first_double()
{double dec;while(scanf("%lf",&dec)!=1){printf("请输入浮点数!!\n");while(getchar()!='\n'){continue;}}while(getchar()!='\n'){continue;}return dec;
}void get_subave(int size,const double dec[size])
{int i;double total=0;for (i=0;i<size ;i++ ){total+=dec[i];}printf("该数集的平均值是:%.2f\n",total/size);
}void get_ave(int row,int col,const double dec[row][col])
{int i,j;double total=0;for (i=0;i<row ;i++ ){for (j=0;j<col ;j++ ){total+=dec[i][j];}}printf("所有数据的平均值:%.2f\n",total/(row*5));
}void get_max(int row,int col,const double dec[row][col])
{int i,j;double max=dec[0][0];for (i=0;i<row ;i++ ){for (j=0;j<col ;j++ ){if (max<dec[i][j]){max=dec[i][j];}}}printf("所有数中的最大值是:%.2f\n",max);}void show_arr(int row,int col,const double dec[row][col])
{int i,j;for (i=0;i<row ;i++ ){for (j=0;j<col ;j++ ){printf("%8.2f",dec[i][j]);}printf("\n");}
}
http://www.lqws.cn/news/566767.html

相关文章:

  • 左神算法之螺旋打印
  • SQL Server从入门到项目实践(超值版)读书笔记 19
  • 从GPTs到Real智能体:目前常见的几种创建智能体方式
  • spring:BeanPostProcessor后置处理器介绍
  • 小米路由器 AX3000T自定义子网掩码
  • Mybatis多条件查询设置参数的三种方法
  • stm32hal模块驱动(1)hpdl1414驱动
  • Vue的watch函数实现
  • 华为云 Flexus+DeepSeek 征文|华为云 Flexus 云服务 Dify-LLM 平台深度部署指南:从基础搭建到高可用实践
  • 智能制造——解读西门子数字化工厂规划报告(三年实施计划)【附全文阅读】
  • 机器学习在智能供应链中的应用:需求预测与库存优化
  • 大事件项目记录12-文章管理接口开发-总
  • 设计模式之适配器模式
  • OpenCV读取照片和可视化详解和代码示例
  • MySQL 安装使用教程
  • Java垃圾收集机制Test
  • PL-SLAM: Real-Time Monocular Visual SLAM with Points and Lines
  • Ai工具分享(2):Vscode+Cline无限免费的使用教程
  • XWPFDocument导出word文件
  • Linux中《动/静态库原理》
  • Redis缓存击穿深度解析:从现象到实战的完整解决方案
  • github上传代码步骤(http)
  • Cesium快速入门到精通系列教程十二:Cesium1.74中环绕地球生成​​经线环​​
  • Javaweb - 7 xml
  • 【智能协同云图库】智能协同云图库第三弹:基于腾讯云 COS 对象存储—开发图片模块
  • 日常 AI 工具汇总
  • Oracle 递归 + Decode + 分组函数实现复杂树形统计进阶(第二课)
  • 深入剖析 Linux 内核网络核心:sock.c 源码解析
  • 阿里云ACP-数据湖和机器学习
  • 解锁Ubuntu安装:从新手到高手的通关秘籍