目录
10.1 数组
数组由数据类型相同的一系列元素组成。使用数组时,通过声明数组告诉编译器数组中含有多少元素和这些元素的类型。
下面是几个数组声明:
float candy[365];
char code[12];
10.1.1 初始化数组
标量变量:只存储单个值的变量
使用{}来初始化数组:
int powers[3] = {1,2,4};
tip:使用const前缀声明数组,可以把数组设置为只读。
例如:const int powers[3] = {1,2,4};
初始化数组时可以省略方括号里的数字,编译器会根据初始化列表中的项数来确定数组大小。
int powers[] = {1,2,4};
使用for循环遍历数组时,可以让计算机计算数组大小,例如一个数组days[], 通过 sizeof days/ sizeof days[0] 可得元素个数。
//sizeof days得到数组大小,sizeof days[0]得到单个数组大小。相除得到个数。
//no_data.c--未初始化数组
#include <stdio.h>
#define SIZE 4
int main(void){
int no_data[SIZE]; //未初始化数组
int i;
printf("%2s%14s\n", "i","no_data[i]");
for(i = 0; i< SIZE;i++){
printf("%2d%14d\n", i,no_data[i]);
}
return 0;
}
示例结果:
乱码原因:残留数据
//no_data.c--未初始化数组
#include <stdio.h>
#define SIZE 4
int main(void){
int no_data[SIZE]={1492,1066}; //未初始化数组
int i;
printf("%2s%14s\n", "i","no_data[i]");
for(i = 0; i< SIZE;i++){
printf("%2d%14d\n", i,no_data[i]);
}
return 0;
}
示例结果:
当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0(编译器会自动匹配数组大小和初始化列表中的项数)
//day_mon2.c--让编译器计算元素个数
#include <stdio.h>
int main(void){
const int days [] ={31,28,31,30,31,30,31,31,30,31,30,31};
int index;
for(index=0;index<sizeof (days)/sizeof (days[0]);index++)
printf("Mouth %2d has %d days.\n",index +1,days[index]);
return 0;
}
示例结果:
PS:
- 编译器会根据初始化列表中的项数来确认数组的大小
- sizeof运算符会给出它所运算对象的大小(字节为单位)sizeof(days)是整个数组的大小,sizeof(days[0])是数组中一个元素的大小 ;sizeof(days)/sizeof(days[0])表示数组元素的个数
弊端:无法察觉初始化列表中的项数有误
10.1.2 指定初始化器
C99可以初始化指定的数组元素:
int arr[6] { [5] = 212}
//designate.c--使用指定初始化器
#include <stdio.h>
#define MONTHS 12
int main(void){
int days[MONTHS]={31,28,[4]=31,30,31,[1]=29};
int i;
for(i=0;i<MONTHS;i++){
printf("%2d %d\n",i+1,days[i]);
}
return 0;
}
示例结果:
重要特性:
- 如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段;[4]=31,30,31;那么后面的这些值将被用于初始化指定元素后面的元素。
- 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
10.1.3 给数组元素赋值
使用元素下标给数组元素赋值。
a[2] = 4;
C不允许把数组作为一个单元赋给另一个数组
10.1.4 数组边界
防止数组下标越界。如果声明数组int arr[N],数组下标范围是0~ N-1
示例:
//bounds.c --数组下标越界
#include <stdio.h>
#define SIZE 4
int main(void){
int valve1=44;
int arr[SIZE];
int valve2=88;
int i;
printf("value1 = %d,value2=%d\n",valve1,valve2);
for(i=-1;i<SIZE;i++){
arr[i]=2*i+1;
}
for(i=-1;i<7;i++){
printf("%2d %d\n",i,arr[i]);
}
printf("value1 = %d,value2 = %d\n",valve1,valve2);
printf("address of arr[-1]:%p\n",&arr[-1]);
printf("address of value1:%p\n",&valve1);
printf("address of value2:%p\n",&valve2);
return 0;
}
示例结果:
在C标准中,使用越界下标的结果是未定义的;使用越界的数组下标会导致程序改变其他变量的值
10.1.5 指定数组的大小
10.2 多维数组
多维数组可以看成是数组的数组。
如:int day[12][31] , 可以看成是day有12个元素,每个元素都是一个包含31个int的数组。
在具体的任务中,基本上分为主数组和其他部分;
无论是二维数组、三维数组还是多维数组,在计算机内存中通常都是顺序存储的。
//计算每年的总的降水量、年平均降水量、5年中每月的平均降水量
#include <stdio.h>
#define MONTHS 12
#define YEARS 5
int main(void)
{
const float rain[YEARS][MONTHS] = {
4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6,
8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3,
9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4,
7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2,
7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2 };
int years, months;
float subtot, total=0;//subtot-每年的降水总量,total-5年降水总量
printf("YEAR RAINFALL(inches)\n");
for (years = 0; years < YEARS; years++)
{
for (subtot=0,months = 0; months < MONTHS; months++)
subtot += *(*(rain + years) + months);
printf("%4d %.1f\n", 2010 + years, subtot);
total += subtot;
}
printf("The yearly average is %.1f inches.\n", total / YEARS);
printf("MONTHLY AVERAGES:\n");
printf("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\n");
for (months = 0; months < MONTHS; months++)
{
subtot = 0;
for (years = 0; years < YEARS; years++)
subtot += *(*(rain + years) + months);
printf("%.1f ", subtot / YEARS);
}
printf("\nDone!\n");
return 0;
}
示例结果:
10.2.1 初始化二维数组
const float rain[YEARS][MONTHS] = {
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6,}
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3,}
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4,}
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2,}
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2 }};
初始化可以省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果和上面相同
10.2.2 其他多维数组
通常我们处理几位数组我们就用几层嵌套循环
10.3 指针和数组
数组是变相的指针。
a == &a[0] 数组名是首元素的地址。
10.4 函数和指针
传递数组给函数:
int sum(int *ar, int n);
等价于
int sum(int ar[], int n);
10.4.1 使用指针形参
int sump(int *start, int * end)
{
...
while(start < end)
{...}
}
10.4.2 指针表示法和数组表示法
10.5 指针操作
操作实例:
// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{
int urn[5] = { 100, 200, 300, 400, 500 };
int * ptr1, *ptr2, *ptr3;
ptr1 = urn; // 把一个地址赋给指针
ptr2 = &urn[2]; // 把一个地址赋给指针
// 解引用指针,以及获得指针的地址
printf("pointer value, dereferenced pointer, pointer address:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
// 指针加法
ptr3 = ptr1 + 4;
printf("\nadding an int to a pointer:\n");
printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
ptr1++; // 递增指针
printf("\nvalues after ptr1++:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
ptr2--; // 递减指针
printf("\nvalues after --ptr2:\n");
printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
--ptr1; // 恢复为初始值
++ptr2; // 恢复为初始值
printf("\nPointers reset to original values:\n");
printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
// 一个指针减去另一个指针
printf("\nsubtracting one pointer from another:\n");
printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
// 一个指针减去一个整数
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
示例结果:
- 赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。在该例中,把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。毕竟,变量不会因为值发生变化就移动位置。
- 指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与&urn[2]等价,因为ptr3指向的是&urn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
- 递减指针:当然,除了递增指针还可以递减指针。在本例中,递减ptr2使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn[1]。
- 指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序清单10.13的输出中,ptr2 - ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
使用指针时,尽量在声明时进行初始化,防止出现
int *pt;
*pt = 5;
这种解引用未初始化的指针会导致未知错误。(因为*pt指向的地址不确定,所以5会被存在一个不确定的地方。)
10.6 保护数组中的数据
传递地址会导致一些问题,原始数据可能会被修改。
10.6.1 对形式参数使用const
如果函数不想修改数组中的数据,可在函数原型和函数定义中使用const:
int sum(const int ar[], int n);
10.6.2 const的其他内容
可以创建const数组、const指针和指向const的指针。
使用const关键字保护数组:
#define MONTHS 12
…
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
指向const的指针不能用于改变值。考虑下面的代码:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates; // pd指向数组的首元素
第2行代码把pd指向的double类型的值声明为const,这表明不能使用pd来更改它所指向的值:
*pd = 29.89; // 不允许
pd[2] = 222.22; //不允许
rates[0] = 99.99; // 允许,因为rates未被const限定
可以让pd指向别处:
pd++; /* 让pd指向rates[1] – 没问题 */
指向const的指针通常用于函数形参中,表明该函数不会使用指针改变数据。
关于指针赋值和const需要注意一些规则。首先,把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};*
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};*
const double * pc = rates; // 有效*
pc = locked; //有效
pc = &rates[3]; //有效 然而,只能把非const数据的地址赋给普通指针:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};*
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
double * pnc = rates; // 有效
pnc = locked; // 无效
pnc = &rates[3]; // 有效
这个规则非常合理。否则,通过指针就能改变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 指针和多维数组
int zippo[4][2]; /* 内含int数组的数组 */
- 然后数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。下面,我们从指针的属性进一步分析。
- 因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同。
- 给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。因此,zippo + 1和zippo[0] + 1的值不同。
- 解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。因为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)的例子。
- /* zippo1.c -- zippo的相关信息 */
#include <stdio.h>
int main(void)
{
int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };
printf(" zippo = %p, zippo + 1 = %p\n",zippo, zippo + 1);
printf("zippo[0] = %p, zippo[0] + 1 = %p\n",zippo[0], zippo[0] + 1);
printf(" *zippo = %p, *zippo + 1 = %p\n",*zippo, *zippo + 1);
printf("zippo[0][0] = %d\n", zippo[0][0]);
printf(" *zippo[0] = %d\n", *zippo[0]);
printf(" **zippo = %d\n", **zippo);
printf(" zippo[2][1] = %d\n", zippo[2][1]);
printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo + 2) + 1));
return 0;
}
/*
zippo = 0x0064fd38, zippo + 1 = 0x0064fd40
zippo[0]= 0x0064fd38, zippo[0] + 1 = 0x0064fd3c
*zippo = 0x0064fd38, *zippo + 1 = 0x0064fd3c
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(*(zippo+2) + 1) = 3
*/
- 输出显示了二维数组zippo的地址和一维数组zippo[0]的地址相同。它们的地址都是各自数组首元素的地址,因而与&zippo[0][0]的值也相同。
- 尽管如此,它们也有差别。在我们的系统中,int是4字节。前面讨论过,zippo[0]指向一个4字节的数据对象。zippo[0]加1,其值加4(十六进制中,38+4得3c)。数组名zippo是一个内含2个int类型值的数组的地址,所以zippo指向一个8字节的数据对象。因此,zippo加1,它所指向的地址加8字节(十六进制中,38+8得40)。
- 该程序演示了zippo[0]和*zippo完全相同,实际上确实如此。然后,对二维数组名解引用两次,得到存储在数组中的值。使用两个间接运算符(*)或者使用两对方括号([])都能获得该值(还可以使用一个*和一对[],但是我们暂不讨论这么多情况)。
- 要特别注意,与zippo[2][1]等价的指针表示法是*(*(zippo+2) + 1)。看上去比较复杂,应最好能理解。
zippo ←二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)
zippo+2 ←二维数组的第3个元素(即一维数组)的地址
*(zippo+2) ←二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)地址
*(zippo+2) + 1 ←二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址
*(*(zippo+2) + 1) ←二维数组的第3个一维数组元素的第2个int类型元素的值,即数组的第3行第2
列的值(zippo[2][1])
10.8 变长数组(VLA)
在 C 语言中,变长数组(Variable Length Arrays,VLA)是一种在 C99 标准中引入的特性。
一、定义和语法
变长数组允许你在程序运行时指定数组的大小,而不是像传统的 C 语言数组那样必须在编译时确定大小。其语法如下:
int n;
scanf("%d", &n);
int arr[n]; // 定义一个变长数组,大小由运行时输入的 n 决定
二、特点和优势
-
灵活性
- 可以根据程序运行时的实际情况动态确定数组的大小,适应不同的输入和场景需求。例如,在处理不同大小的数据集时,可以根据实际数据的数量来创建合适大小的数组,避免了浪费内存或内存不足的问题。
-
局部性
- 变长数组通常在函数内部声明,与自动变量具有相似的存储方式和生命周期。这使得它们在局部作用域内具有更好的内存局部性,可能提高程序的性能。
三、限制和注意事项
- 作用域限制
- 变长数组通常只能在声明它们的块作用域内使用。一旦离开这个作用域,数组的内存将被自动释放。
- 例如:
void someFunction() { int n; scanf("%d", &n); int arr[n]; // 使用 arr // 离开这个函数后,arr 的内存被释放。 }
2.不支持初始化列表
- 变长数组不能像普通数组那样使用初始化列表进行初始化。
- 例如,以下是错误的用法:
int n = 5;
int arr[n] = {1, 2, 3, 4, 5}; // 错误,变长数组不能这样初始化
3.并非所有编译器都完全支持
- 虽然 C99 引入了变长数组,但并不是所有的 C 语言编译器都完全支持这个特性。一些较老的编译器可能不支持变长数组,或者只提供有限的支持。在使用变长数组时,需要确保你的编译器支持这个特性,并了解其具体的实现细节和限制。
10.9 复合字面量
C99新增了复合字面量(compound literal)。字面量是除符号常量外的常量。例如,5是int类型字面量,81.3是double类型的字面量,'Y’是char类型的字面量,"elephant"是字符串字面量。发布C99标准的委员会认为,如果有代表数组和结构内容的复合字面量,在编程时会更方便。
下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:
(int [2]){10, 20} // 复合字面量
注意,去掉声明中的数组名,留下的int [2]即是复合字面量的类型名。
复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:
(int []){50, 20, 90} // 内含3个元素的复合字面量
因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。也就是说,可以这样用:
int * pt1;
pt1 = (int [2]) {10, 20};
还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:
int sum(const int ar[], int n);
…
int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);
// flc.c -- 有趣的常量
#include <stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS], int rows);
int sum(const int ar[], int n);
int main(void)
{
int total1, total2, total3;
int * pt1;
int(*pt2)[COLS];
pt1 = (int[2]) { 10, 20 };
pt2 = (int[2][COLS]) { {1, 2, 3, -9}, { 4, 5, 6, -8 } };
total1 = sum(pt1, 2);
total2 = sum2d(pt2, 2);
total3 = sum((int []){ 4, 4, 4, 5, 5, 5 }, 6);
printf("total1 = %d\n", total1);
printf("total2 = %d\n", total2);
printf("total3 = %d\n", total3);
return 0;
}
int sum(const int ar [], int n)
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
int sum2d(const int ar [][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
tot += ar[r][c];
return tot;
}
-
要支持C99的编译器才能正常运行该程序示例(目前并不是所有的编译器都支持),其输出如下:
-