襄县网站建设,北京计算机培训机构前十名,网站怎么做收录,手机网站建设的价格本章介绍以下内容#xff1a; 关键字#xff1a;static 运算符#xff1a;、*#xff08;一元#xff09; 如何创建并初始化数组 指针#xff08;在已学过的基础上#xff09;、指针和数组的关系 编写处理数组的函数 二维数组 人们通常借助计算机完成统计每月的支出…本章介绍以下内容 关键字static 运算符、*一元 如何创建并初始化数组 指针在已学过的基础上、指针和数组的关系 编写处理数组的函数 二维数组 人们通常借助计算机完成统计每月的支出、日降雨量、季度销售额等任务。企业借助计算机管理薪资、库存和客户交易记录等。作为程序员不可避免地要处理大量相关数据。通常数组能高效便捷地处理这种数据。第 6 章简单地介绍了数组本章将进一步地学习如何使用数组着重分析如何编写处理数组的函数。这种函数把模块化编程的优势应用到数组。通过本章的学习你将明白数组和指针关系密切。
10.1 数组
数组由数据类型相同的一系列元素组成
方括号中的数字表明数组中的元素个数。 要访问数组中的元素通过使用数组下标数也称为索引表示数组中的各元素。数组元素的编号从0开始所以candy[0]表示candy数组的第1个元素
10.1.1 初始化数组
如上所示用以逗号分隔的值列表用花括号括起来来初始化数组各值之间用逗号分隔。在逗号和值之间可以使用空格。
使用数组前必须先初始化它。与普通变量类似在使用数组元素之前必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值
当初始化列表中的值少于数组元素个数时编译器会把剩余的元素都初始化为0。也就是说如果不初始化数组数组元素和未初始化的普通变量一样其中储存的都是垃圾值但是如果部分初始化数组剩余的元素就会被初始化为0。
如果初始化数组时省略方括号中的数字编译器会根据初始化列表中的项数来确定数组的大小。
整个数组的大小除以单个元素的大小就是数组元素的个数
10.1.2 指定初始化器C99
而C99规定可以在初始化列表中使用带方括号的下标指明待初始化的元素 int arr[6] {[5] 212}; // 把arr[5]初始化为212 对于一般的初始化在初始化一个元素后未初始化的元素都会被设置为0。
第一如果指定初始化器后面有更多的值如该例中的初始化列表中的片段[4] 31,30,31那么后面这些值将被用于初始化指定元素后面的元素。也就是说在days[4]被初始化为31后days[5]和days[6]将分别被初始化为30和31。第二如果再次初始化指定的元素那么最后的初始化将会取代之前的初始化。
编译器会把数组的大小设置为足够装得下初始化的值。
10.1.3 给数组元素赋值
声明数组后可以借助数组下标或索引给数组元素赋值。
C 不允许把数组作为一个单元赋给另一个数组除初始化以外也不允许使用花括号列表的形式赋值。
10.1.4 数组边界
10.1.5 指定数组的大小
在C99标准之前声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式是由整型常量构成的表达式。sizeof表达式被视为整型常量但是与C不同const值不是。另外表达式的值必须大于0
10.2 多维数组
在计算机内部这样的数组是按顺序储存的从第1个内含12个元素的数组开始然后是第2个内含12个元素的数组以此类推。
10.2.1 初始化二维数组
如果第1个列表中只有10个数则只会初始化数组第1行的前10个元素而最后两个元素将被默认初始化为0。如果某列表中的数值个数超出了数组每行的元素个数则会出错但是这并不会影响其他行的初始化。
初始化时也可省略内部的花括号只保留最外面的一对花括号。只要保证初始化的数值个数正确初始化的效果与上面相同。但是如果初始化的数值不够则按照先后顺序逐行初始化直到用完所有的值。后面没有值初始化的元素被统一初始化为0。
10.2.2 其他多维数组
通常处理三维数组要使用3重嵌套循环处理四维数组要使用4重嵌套循环。对于其他多维数组以此类推。
10.3 指针和数组
数组名是数组首元素的地址。也就是说如果flizny是一个数组下面的语句成立 flizny flizny[0]; // 数组名是该数组首元素的地址 flizny 和flizny[0]都表示数组首元素的内存地址是地址运算符。两者都是常量在程序的运行过程中不会改变。但是可以把它们赋值给指针变量然后可以修改指针变量的值
我们的系统中地址按字节编址short类型占用2字节double类型占用8字节。在C中指针加1指的是增加一个存储单元。对数组而言这意味着把加1后的地址是下一个元素的地址而不是下一个字节的地址见图10.3。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址不够因为计算机要知道储存对象需要多少字节即使指针指向的是标量变量也要知道变量的类型否则*pt 就无法正确地取回地址上的值。
在指针前面使用*运算符可以得到该指针所指向对象的值。 指针加1指针的值递增它所指向类型的大小以字节为单位。
也就是说定义ar[n]的意思是*(ar n)。可以认为*(ar n)的意思是“到内存的ar位置然后移动n个单元检索储存在那里的值”。
10.4 函数、数组和指针
关于函数的形参还有一点要注意。只有在函数原型或函数定义头中才可以用int ar[]代替int * ar int sum (int ar[], int n); int *ar形式和int ar[]形式都表示ar是一个指向int的指针。但是int ar[]只能用于声明形式参数。第2种形式int ar[]提醒读者指针ar指向的不仅仅一个int类型值还是一个int类型数组的元素。
由于函数原型可以省略参数名所以下面4种原型都是等价的 int sum(int *ar, int n); int sum(int *, int); int sum(int ar[], int n); int sum(int [], int); 但是在函数定义中不能省略参数名。下面两种形式的函数定义等价 int sum(int *ar, int n) { // 其他代码已省略 } int sum(int ar[], int n); { //其他代码已省略 }
10.4.1 使用指针形参
指针start开始指向marbles数组的首元素所以赋值表达式total *start把首元素20加给total。然后表达式start递增指针变量start使其指向数组的下一个元素。因为start是指向int的指针start递增1相当于其值递增int类型的大小。
而sump()函数则使用第2个指针来结束循环 while (start end) 因为while循环的测试条件是一个不相等的关系所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面
10.4.2 指针表示法和数组表示法
使用数组表示法让函数是处理数组的这一意图更加明显。
但是只有当ar是指针变量时才能使用ar这样的表达式。 指针表示法尤其与递增运算符一起使用时更接近机器语言因此一些编译器在编译时能生成效率更高的代码。然而许多程序员认为他们的主要任务是确保代码正确、逻辑清晰而代码优化应该留给编译器去做。
10.5 指针操作
如果编译器不支持%p 转换说明可以用%u 或%lu 代替%p如果编译器不支持用%td转换说明打印地址的差值可以用%d或%ld来代替。
下面分别描述了指针变量的基本操作。 赋值可以把地址赋给指针。例如用数组名、带地址运算符的变量名、另一个指针进行赋值。在该例中把urn数组的首地址赋给了ptr1该地址的编号恰好是0x7fff5fbff8d0。变量ptr2获得数组urn的第3个元素urn[2]的地址。注意地址应该和指针类型兼容。也就是说不能把double类型的地址赋给指向int的指针至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。 解引用*运算符给出指针指向地址上储存的值。因此*ptr1的初值是100该值储存在编号为0x7fff5fbff8d0的地址上。 取址和所有变量一样指针变量也有自己的地址和值。对指针而言运算符给出指针本身的地址。本例中ptr1 储存在内存编号为 0x7fff5fbff8c8 的地址上该存储单元储存的内容是0x7fff5fbff8d0即urn的地址。因此ptr1是指向ptr1的指针而ptr1是指向utn[0]的指针。 指针与整数相加可以使用运算符把指针与整数相加或整数与指针相加。无论哪种情况整数都会和指针所指向类型的大小以字节为单位相乘然后把结果与初始地址相加。因此ptr1 4与urn[4]等价。如果相加的结果超出了初始指针指向的数组范围计算结果则是未定义的。除非正好超过数组末尾第一个位置C保证该指针有效。 递增指针递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此ptr1相当于把ptr1的值加上4我们的系统中int为4字节ptr1指向urn[1]见图10.4该图中使用了简化的地址。现在ptr1的值是0x7fff5fbff8d4数组的下一个元素的地址*ptr的值为200即urn[1]的值。注意ptr1本身的地址仍是 0x7fff5fbff8c8。毕竟变量不会因为值发生变化就移动位置。 图10.4 递增指向int的指针
指针减去一个整数可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象整数是第 2 个运算对象。该整数将乘以指针指向类型的大小以字节为单位然后用初始地址减去乘积。所以ptr3 - 2与urn[2]等价因为ptr3指向的是arn[4]。如果相减的结果超出了初始指针所指向数组的范围计算结果则是未定义的。除非正好超过数组末尾第一个位置C保证该指针有效。 递减指针当然除了递增指针还可以递减指针。在本例中递减ptr3使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意在重置ptr1和ptr2前它们都指向相同的元素urn[1]。 指针求差可以计算两个指针的差值。通常求差的两个指针分别指向同一个数组的不同元素通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如程序清单10.13的输出中ptr2 - ptr1得2意思是这两个指针所指向的两个元素相隔两个int而不是2字节。只要两个指针都指向相同的数组或者其中一个指针指向数组后面的第 1 个地址C 都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值或者导致运行时错误。 比较使用关系运算符可以比较两个指针的值前提是两个指针都指向相同类型的对象。 注意这里的减法有两种。可以用一个指针减去另一个指针得到一个整数或者用一个指针减去一个整数得到另一个指针。 在递增或递减指针时还要注意一些问题。编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。但是如果递增或递减一个指针后超出了这个范围则是未定义的。另外可以解引用指向数组任意元素的指针。但是即使指针指向数组后面一个位置是有效的也能解引用这样的越界指针。
10.6 保护数组中的数据
10.6.1 对形式参数使用const
如果函数的意图不是修改数组中的数据内容那么在函数原型和函数定义中声明形式参数时应使用关键字const。例如sum()函数的原型和定义如下 int sum(const int ar[], int n); /* 函数原型 */ int sum(const int ar[], int n) /* 函数定义 */ { int i; int total 0; for( i 0; i n; i) total ar[i]; return total; } 以上代码中的const告诉编译器该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似ar[i]的表达式编译器会捕获这个错误并生成一条错误信息。
一般而言如果编写的函数需要修改数组在声明数组形参时则不使用const如果编写的函数不用修改数组那么在声明数组形参时最好使用const。
10.6.2 const的其他内容
如果程序稍后尝试改变数组元素的值编译器将生成一个编译期错误消息
无论是使用指针表示法还是数组表示法都不允许使用pd修改它所指向数据的值。但是要注意因为rates并未被声明为const所以仍然可以通过rates修改元素的值。另外可以让pd指向别处
关于指针赋值和const需要注意一些规则。首先把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的
然而只能把非const数据的地址赋给普通指针
const还有其他的用法。例如可以声明并初始化一个不能指向别处的指针关键是const的位置 double rates[5] {88.99, 100.12, 59.45, 183.11, 340.5}; double * const pc rates; // pc指向数组的开始 pc rates[2]; // 不允许因为该指针不能指向别处 *pc 92.99; // 没问题 -- 更改rates[0]的值 可以用这种指针修改它所指向的值但是它只能指向初始化时设置的地址。 最后在创建指针时还可以使用const两次该指针既不能更改它所指向的地址也不能修改指向地址上的值double rates[5] {88.99, 100.12, 59.45, 183.11, 340.5}; const double * const pc rates; pc rates[2]; //不允许 *pc 92.99; //不允许
10.7 指针和多维数组
因为zippo[0]是该数组首元素zippo[0][0]的地址所以*(zippo[0])表示储存在zippo[0][0]上的值即一个int类型的值。与此类似*zippo代表该数组首元素zippo[0]的值但是zippo[0]本身是一个int类型值的地址。该值的地址是zippo[0][0]所以*zippo就是zippo[0][0]。对两个表达式应用解引用运算符表明**zippo与*zippo[0][0]等价这相当于zippo[0][0]即一个int类型的值。简而言之zippo是地址的地址必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接double indirection的例子
要特别注意与 zippo[2][1]等价的指针表示法是*(*(zippo2) 1)。看上去比较复杂应最好能理解。下面列出了理解该表达式的思路 图10.5 数组的数组
10.7.1 指向多维数组的指针
int (* pz)[2]; // pz指向一个内含两个int类型值的数组
int * pax[2]; // pax是一个内含两个指针元素的数组每个元素都指向int的指针
系统不同输出的地址可能不同但是地址之间的关系相同。如前所述虽然pz是一个指针不是数组名但是也可以使用 pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素既可以使用数组名也可以使用指针名
zippo[m][n] *(*(zippo m) n) pz[m][n] *(*(pz m) n)
10.7.2 指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如不用类型转换就可以把 int 类型的值赋给double类型的变量但是两个类型的指针不能这样做
10.7.3 函数和多维数组
一种方法是利用for循环把处理一维数组的函数应用到二维数组的每一行。
可以这样声明函数的形参 void somefunction( int (* pt)[4] ); 另外如果当且仅当pt是一个函数的形式参数时可以这样声明 void somefunction( int pt[][4] );
注意下面的声明不正确 int sum2(int ar[][], int rows); // 错误的声明
int sum2(int ar[][4], int rows); // 有效声明
int sum2(int ar[3][4], int rows); // 有效声明但是3将被忽略
int sum2(arr3x4 ar, int rows); // 与下面的声明相同 int sum2(int ar[3][4], int rows); // 与下面的声明相同 int sum2(int ar[][4], int rows); // 标准形式
一般而言声明一个指向N维数组的指针时只能省略最左边方括号中的值 int sum4d(int ar[][12][20][30], int rows);
10.8 变长数组VLA
鉴于此C99新增了变长数组variable-length arrayVLA允许使用变量表示数组的维度。如下所示 int quarters 4; int regions 5; double sales[regions][quarters]; // 一个变长数组VLA
前面提到过变长数组有一些限制。变长数组必须是自动存储类别这意味着无论在函数中声明还是作为函数形参声明都不能使用static或extern存储类别说明符第12章介绍。而且不能在声明中初始化它们。最终C11把变长数组作为一个可选特性而不是必须强制实现的特性。
注意前两个形参rows和cols用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols所以在形参列表中必须在声明ar之前先声明这两个形参。因此下面的原型是错误的 int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序
C99/C11标准规定可以省略原型中的形参名但是在这种情况下必须用星号来代替省略的维度 int sum2d(int, int, int ar[*][*]); // ar是一个变长数组VLA省略了维度形参名
需要注意的是在函数定义的形参列表中声明的变长数组并未实际创建数组。和传统的语法类似变长数组名实际上是一个指针。这说明带变长数组形参的函数实际上是在原始数组中处理数组因此可以修改传入的数组。
C90标准不允许也可能允许。数组的大小必须是给定的整型常量表达式可以是整型常量组合如20、sizeof表达式或其他不是const的内容。由于C实现可以扩大整型常量表达式的范围所以可能会允许使用const但是这种代码可能无法移植。 C99/C11 标准允许在声明变长数组时使用 const 变量。所以该数组的定义必须是声明在块中的自动存储类别数组。 变长数组还允许动态内存分配这说明可以在程序运行时指定数组的大小。普通 C数组都是静态内存分配即在编译时确定数组的大小。由于数组大小是常量所以编译器在编译时就知道了。第12章将详细介绍动态内存分配
10.9 复合字面量
字面量是除符号常量外的常量。例如5是int类型字面量 81.3是double类型的字面量Y是char类型的字面量elephant是字符串字面量。
下面的复合字面量创建了一个和diva数组相同的匿名数组也有两个int类型的值 (int [2]){10, 20} // 复合字面量 注意去掉声明中的数组名留下的int [2]即是复合字面量的类型名。
初始化有数组名的数组时可以省略数组大小复合字面量也可以省略大小编译器会自动计算数组当前的元素个数 (int []){50, 20, 90} // 内含3个元素的复合字面量
因为复合字面量是匿名的所以不能先创建然后再使用它必须在创建的同时使用它。使用指针记录地址就是一种用法。也就是说可以这样用 int * pt1; pt1 (int [2]) {10, 20};
与有数组名的数组类似复合字面量的类型名也代表首元素的地址所以可以把它赋给指向int的指针。然后便可使用这个指针。例如本例中*pt1是10pt1[1]是20。
还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数
记住复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域第12章将介绍相关内容这意味着一旦离开定义复合字面量的块程序将无法保证该字面量是否存在。也就是说复合字面量的定义在最内层的花括号中。
10.10 关键概念
10.11 本章小结
数组是一组数据类型相同的元素。数组元素按顺序储存在内存中通过整数下标或索引可以访问各元素。在C中数组首元素的下标是0所以对于内含n个元素的数组其最后一个元素的下标是n-1。作为程序员要确保使用有效的数组下标因为编译器和运行的程序都不会检查下标的有效性。 声明一个简单的一维数组形式如下 type name [ size ]; 这里type是数组中每个元素的数据类型name是数组名size是数组元素的个数。对于传统的C数组要求size是整型常量表达式。但是C99/C11允许使用整型非常量表达式。这种情况下的数组被称为变长数组。 C把数组名解释为该数组首元素的地址。换言之数组名与指向该数组首元素的指针等价。概括地说数组和指针的关系十分密切。如果ar是一个数组那么表达式ar[i]和*(ari)等价。 对于 C 语言而言不能把整个数组作为参数传递给函数但是可以传递数组的地址。然后函数可以使用传入的地址操控原始数组。如果函数没有修改原始数组的意图应在声明函数的形式参数时使用关键字const。在被调函数中可以使用数组表示法或指针表示法无论用哪种表示法实际上使用的都是指针变量。 指针加上一个整数或递增指针指针的值以所指向对象的大小为单位改变。也就是说如果pd指向一个数组的8字节double类型值那么pd加1意味着其值加8以便它指向该数组的下一个元素。 二维数组即是数组的数组。例如下面声明了一个二维数组 double sales[5][12]; 该数组名为sales有5个元素一维数组每个元素都是一个内含12个double类型值的数组。第1个一维数组是sales[0]第2个一维数组是sales[1]以此类推每个元素都是内含12个double类型值的数组。使用第2个下标可以访问这些一维数组中的特定元素。例如sales[2][5]是slaes[2]的第6个元素而sales[2]是sales的第3个元素。 C 语言传递多维数组的传统方法是把数组名即数组的地址传递给类型匹配的指针形参。声明这样的指针形参要指定所有的数组维度除了第1个维度。传递的第1个维度通常作为第2个参数。例如为了处理前面声明的sales数组函数原型和函数调用如下 void display(double ar[][12], int rows); ... display(sales, 5); 变长数组提供第2种语法把数组维度作为参数传递。在这种情况下对应函数原型和函数调用如下 void display(int rows, int cols, double ar[rows][cols]); ... display(5, 12, sales); 虽然上述讨论中使用的是int类型的数组和double类型的数组其他类型的数组也是如此。然而字符串有一些特殊的规则这是由于其末尾的空字符所致。有了这个空字符不用传递数组的大小函数通过检测字符串的末尾也知道在何处停止。我们将在第11章中详细介绍。