1.1数组
数组由数据类型相同的一系列元素组成。
char code[20];
int arr[12];
1.1.1初始化数组
只存储单个值的变量也称为标量变量。
初始化:int powers[8]={1,2,3,4,5,6,7,8};
/*day_mon1.c --打印每个月的天数*/
#include <stdio.h>
#define MONTHS 12
int main()
{int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};int index;for(index = 0; index < MONTHS; index++)printf("%d月有%d天", index, days[index]);return 0;
}
在使用数组之前,必须赋初值。
部分初始化数组,剩余元素会被初始化为0;初始化数组超过数组元素个数,则会出现错误。
省略方括号中的数字,编译器会自动匹配数组大小和初始化列表项数。
1.1.2数组元素赋值
赋值:循环赋值
int counter, evens[SIZE]
for(counter = 0; counter < SIZE; counter++)
{evens[counter] = 2 * counter;
}
声明数组后,借助数组下标给数组元素赋值。
不允许把数组作为一个单元赋给另一个数组,除了初始化以外不允许在其他地方使用花括号列表形式赋值。
1.1.3数组边界
在使用数组时,要防止数组下标越界,必须确保下标是有效的。在编译器中,不会检查数组下标是否得当,数组越界会导致程序改变其他变量的值或者使程序中断。数组的编号从0开始,最好在声明数组时使用符号常量来表示数组的大小。
1.1.4指定数组大小
在C99之前,声明数组时只允许使用整型常量表达式,C99后,允许使用变长数组(VLA)
int array[4]; //整型常量表达式
int array[m]; //C99后允许
1.2多维数组
/*计算每年总降水量、年平均降水量和5年中每月的平均降水量*/
#include <stdio.h>
#define MONTHS 12 //一年的月份
#define YEARS 5 //年数
int main(void)
{//用2010~2014年的降水量数据初始化数组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, 6.4},{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 year, month;float subtot, total;printf("YEAR RAINFALL (inches)\n");for (year = 0, total = 0; year < YEARS; year++){for (month = 0, subtot = 0; month < MONTHS; month++)subtot += rain[year][month];printf("%5d %15.1f\n", 2010 + year, subtot);total += subtot; //5年的总降水量}printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);printf("MONTHLY AVERAGES:\n\n");printf("Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");printf("Nov Dec\n");for (month = 0; month < MONTHS; month++){//每个月,5年的总降水量for (year = 0, subtot = 0; year < YEARS; year++)subtot += rain[year][month];printf("%4.1f ", subtot / YEARS);}printf("\n");return 0;
}
程序输出:
使用两个嵌套for循环。第一个嵌套for循环的内层循环,在year不变的情况下,遍历month计算某年降水量,外层循环在month不变的情况下,遍历year计算5年总降水量。
嵌套循环常用来处理二维数组,一个循环处理第一个下标,一个处理第二个下标。
1.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, 6.4},
{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个数,只会初始化数组的第一行10个元素,后面的元素将默认初始化为0。初始化超过时,会出错。初始化可省略内部花括号。
1.2.2其他多维数组
int box[10][20][30];
三维数组,将10个二维数组堆叠起来。处理三维数组要使用三层嵌套循环。
……
1.3指针和数组
指针提供一种以符号形式使用地址的方法。指针能有效地处理数组,数组表示法其实是变相的使用指针。
//pnt_add.c --指针地址
#include <stdio.h>
#define SIZE 4int main()
{short dates[SIZE];short* pti;short index;double bills[SIZE];double* ptf;pti = dates; //把数组地址赋给指针ptf = bills;printf("%23s %15s\n", "short", "double");for (index = 0; index < SIZE; index++)printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);return 0;
}
该示例中,第二行打印的是数组开始的地址,下一行打印的是指针加1后的地址。
在C语言中,指针加1指的是增加一个存储单元。于数组而言,加1后的地址是下一个元素的地址,而不是下一个字节的地址。这是为什么必须声明指针所指向对象类型原因之一。
注:
1.指针的值是它所指向对象的地址。
2.在指针前使用*可以得到该指针所指向对象的值。
3.指针加1,指针的值递增它所指向类型的大小。
1.4函数、数组和指针
编写一个处理数组的函数,函数返回数组中所有元素的和,待处理名为marbles的int类型数组。
数组名是该数组首元素的地址,所以实际参数marbles是一个存储int地址,把它赋值给一个指针形式参数,该参数是一个指向int的指针:
int sum (int *ar); //函数原型
注:该函数原型只获得了数组首元素地址,但是没有包含数组元素个数信息。使用以下两个方法:
(1)int sum(int *ar) //函数定义
{
int i;
int total = 0;
for(i = 0; i < 10; i++) //数组有10个元素
total += ar[i]; //ar[i]和*(ar + i)相同
return total;
}
该方法限制了函数数组元素
(2)int sum(int *ar , int n)
{
int i;
int total = 0;
for(i = 0; i < n; i++) //数组有10个元素
total += ar[i]; //ar[i]和*(ar + i)相同
return total;
}
第1个形参表示数组地址和数据类型,第2个形参表示数组中的个数。
注:
数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。
下面函数原型都是等价的:
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int n);
下列函数定义等价:
int sum(int *ar, int n)
{}
int sum(int ar[], int n)
{}
程序输出如下:
//sum_arr1.c --数组元素之和
#include <stdio.h>
#define SIZE 10int sum(int ar[], int n);int main()
{int marbles[SIZE] = { 20,10,5,39,4,16,19,26,31,20 };long answer;answer = sum(marbles, SIZE);printf("The total number of marbles is %d.\n", answer);printf("The size of marbles is %zd byte.\n", sizeof marbles);return 0;
}int sum(int ar[], int n)
{int i;int total = 0;for (i = 0; i < n; i++)total += ar[i];printf("The size of ar is %zd bytes.\n", sizeof ar);return total;
}
1.4.1 使用指针形参
函数要处理数值必须知道何时开始,何时结束。一种方法是使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数;另一种方法是直接传递两个指针。
/*sum_arr2.c --数组元素之和*/
#include <stdio.h>
#define SIZE 10int sump(int* start, int* end);int main(void)
{int marbles[SIZE] = { 20,10,5,39,4,16,19,26,31,20 };long answer;answer = sump(marbles, marbles + SIZE);printf("The total number of marbles of marbles is %ld.\n", answer);return 0;
}//指针算法
int sump(int* start, int* end)
{int total = 0;while (start < end){total += *start;start++;}return total;
}
代码解析:指针start开始指向marbles数组的首元素,则,total += *start表示把首元素加给total然后start++递增指针变量start,使其指向数组的下一个元素,因为start是指向int的指针,start+1相当于递增int类型的大小。
1.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 value: \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;
}
程序结果:
解析:
(1)赋值:可以把地址赋给指针,如数组名,带地址运算符(&)的变量名、另一个指针进行赋值。例:把urn数组的首地址赋给ptr1;注:地址和指针类型兼容,不可进行类型转换。
(2)解引用:*运算符给出指针指向地址上存储的值。如*ptr1的初值是100,该值存储在编号为000000BA93B4F4D8地址上。
(3)取址:&给出指针本身的地址。如:ptr1的地址编号是000000BA93B4F508,该存储单元存储的内容是000000BA93B4F4D8,即urn的地址。&ptr1是指向ptr1的指针,ptr1是指向urn[0]的指针。
(4)指针与整数相加:可以使用+把指针与整数相加,或整数与指针相加,即:整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加结果超出了初始指针指向的数组范围,计算结果则未定义。
(5)递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。如:ptr++=ptr的值+4,ptr指向urn[1]。此时,ptr1的值是000000BA93B4F4DC(数组的下一个元素地址),*ptr1的值为200(urn[1]的值),此时ptr1本身地址仍然是000000BA93B4F508。变量不会因为值发生变化就移动。
(6)指针减去一个数:指针必须是第一个操作对象,整数是第二个操作对象。整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相减。如果相减结果超出了初始指针指向的数组范围,计算结果则未定义。
(7)递减指针:与递增指针相似。
(8)指针求差:求差的两个指针分别指向同一个数组的不同元素,求出两个元素的距离,单位与数组类型相同。如果指向两个数组的指针求差会得出一个值或运行错误。
(9)比较:关系运算符比较两个指针的值前提是两个指针具有相同的类型对象。
注:在递增或者递减指针时,编译器不会检查指针是否指向数组元素。
不要解引用未初始化的指针。因为未初始化的指针其值是一个随机值。
在创建指针时,系统只分配存储指针本身的地址,但是未分配存储数据的内存。
1.6保护数组中的数据
在程序中,只有需要在函数中改变该数值时,才会传递指针,否则都是直接传递数据,但是,传递数组时,必须传递指针。
但是传递地址会导致一些问题,C通常按值传递数据,可以保证数据的完整性。如果函数使用原始数据的副本,就不会意外修改原始数据。
1.6.1对形式参数使用const
如果函数意图不是修改数组内容,在函数原型和函数定义中声明形式参数使用const。
例:
int sum(const int arr[], int n); //函数原型int sum(const int arr[], int n) //函数定义
{....}
表明arr指向的数组中的内容不可被修改。此时使用const并不是要求原数组是常量,而是函数在处理数组时将其视为常量,不可更改。
1.6.2const其他内容
可以创建const数组,const指针和指向const的指针。
const int days[Months] = {31,28,31,30,31,30,31,31,30,31,30,31}; days[9] = 0; //错误
double rates[5] = {11.1, 22.2, 33.3, 44.4, 55.5};
const double* pd = rates; //pd指向数组首元素 不能使用pd来改变它指向的值//不论是使用指针表示法还是数组表示法,都不允许使用pd修改指向数据的值。
//
*pd = 100; //错误
pd[2] = 1; //错误
rate[0] = 0; //允许,rate未被const限定
pd++; //正确,让pd指向rates[1]
//指向const的指针通常用于函数形参,表明该函数不会使用指针改变数据
void show_array(const double* arr, int n);
//声明一个不能指向别处的指针
double* const pc = rates;pc = &rates[2]; //错误
*pc = 1; //允许
//这个指针可以修改它所指向的值,但是不能修改指向的地址。
//既不能修改指向地址,也不能修改指向地址上的值
const double* const pc = rates;pc = &rate[2]; //不允许
*pc = 1; //不允许
1.7指针和多维数组
1.7.1指向多维数组的指针
如何声明一个指针变量pz指向一个二维数组?答案是pz指向一个内含两个int类型的数组,如下
int(* pz) [2]; //int指向一个内含两个int类型值的数组
【注】使用()的原因是[]的优先级高于*
int * pz [2]; pz先于[]结合成为一个内含两个元素的数组,*表示pz数组内含两个指针,int表示这两个指针是int类型。
1.7.2指针的兼容性
指针之间的赋值严格,不能像数值类型转换可以进行强制转换。
int n = 5;double x;int* p1 = &n;double* pd = &x;x = n; //隐式转换pd = p1; //编译错误
int* pt;int(*pa)[3];int ar1[2][3];int ar2[3][2];int** p2; //一个指向指针的指针pt = &ar1[0][0]; //都是指向int的指针pt = ar1[0]; //都是指向int的指针pt = ar1;pa = ar1; //都是指向内含3个int类型元素数组的指针pa = ar2;p2 = &pt;*p2 = ar2[0];p2 = ar2;
pt = ar1; //pt是指向int的值,ar1指向一个内含3个int类型元素的数组。两个指针指向不同类型,无效。
pa = ar2; //pa是指向一个内含3个int类型元素的数组,ar2是指向一个内含2个int类型元素的数组,无效。
pa = &pt; //p2是指向指针的指针,它指向的指针指向int,ar2是指向数组的指针,该数组含2个int类型元素,所以p2和ar2类型不同。
*p2 = ar2[0]; //*p2是指向int的指针,ar2[0]指向该数组首元素的指针,所以ar2[0]也是指向int的指针,所以二者兼容。
1.7.3函数和多维数组
编写处理二维数组的函数,通常使用数组表示法进行相关操作。
使用for循环把处理一维数组的函数应用到二维数组的每一行。
int jnk[3][4] = { {2, 4, 5, 8}, {3, 5, 6, 9}, {12, 10, 8, 6} };
int i, j;
int total = 0;
for(i = 0; i < 3; i++)total += sum(junk[i], 4); //junk[i]是一维数组
缺点:无法记录行和列的信息。
解决方法:
声明正确类型的形参变量,正确传递数组。
void somefunc(int (*pt)[4]);
当且仅当pt是一个函数的形参时,如下声明:
void somefunc(int pt[][4]); //第一个[]是空的,表明pt是一个指针
声明一个指向N维数组的指针,只能省略最左边的[]中的值:
void sum4d(int ar[][12][20][30], int rows;
第一个[]只表示这是一个指针,其他[]用于描述指针所指向数据对象的类型。
1.8变长数组
C99新增变长数组,允许使用变量表示数组的维度,如下:
int q = 4;
int e = 5;
double sales[q][e]; //变长数组
变长数组的限制:必须是自动存储类别,无论在函数中声明还是作为函数形参声明,都不能使用static和extern存储类别说明符,且不能在声明中初始化。
C11把变长数组作为可选特性。
【注】变长数组不能改变大小
变长数组中的“变”是指在创建数组时,可以使用变量指定数组的维度,不是指可以修改已创建数组大小。一旦创建变长数组,大小保持不变。
C99/C11标准规定:可以省略原型中的形参名,但是必须用星号代替
int sum2(int, int, int ar[*][*]);
普通C数组都是静态内存分配,即在编译时确定数组的大小,但是变长数组允许动态内存分配,说明可以在程序运行时指定数组大小。
1.9复合字面量
C99新增,是除符号常量外的常量,如5是int类型字面量。
对于数组,复合字面量类似于数组初始化列表,前面是()括起来的类型名。
普通数组声明:int diva[2] = {10, 20};
复合字面量创建和diva数组相同的匿名数组:(int [2]) {10, 20}
int[2]就是复合字面量的类型名。
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小。
复合字面量是匿名的,所以必须在创建的同时使用它。如:
int * pt1;
pt1 = (int [2]) {10, 20};
复合字面量的类型名也代表首元素的地址,所以可以把它赋给指向int指针。
#include <stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS], int rows);
int sum(const int ar[], int n);
int main()
{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} };total = 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;for(r = 0; r < rows; r++)for(c = 0; c< COLS; c++)tot += ar[r][c];return tot;
}
输出如下:(需要支持C99的编译器)
total1 = 30
total2 = 4
total3 = 27
复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域。即,一旦离开定义复合字面量,程序将无法保证字面量是否存在,也就是,复合字面量的定义在最内层的花括号内。