数组由数据类型相同的一系列元素组成。
需要使用数组时,通过声明告诉编译器数组内含有多少个元素和这些元素的类型。编译器根据这些数据正确地创建数组。
普通变量可以使用的类型,数组元素都可以使用。
示例:
int main(void)
{
float candy[365];
char code[12];
int states[50];
}
方括号 [] 表明 candy、code、states 都是数组,方括号中的数字表明数组中的元素个数。
要访问数组中的元素,通过使用数组下标(也叫索引)表示数组中的各个元素。
数组元素的索引从 0 开始,如 candy[0] 表示数组 candy 的第一个元素。
声明了数组时就分配了空间,如:
#include
int main(void)
{
char a[10];
printf("%zd\n" , sizeof(a));
return 0;
}
结果:
10
初始化列表:用花括号括起来,用逗号分隔值。从ANSI C开始支持初始化列表。
示例:
#include
int main(void)
{
int a[3] = {1 , 2 , 3}; // 这是完整初始化,给每一个数组元素都用初始化列表初始化了一个值
for (int index = 0 ; index < 3 ; index++)
printf("%d " , a[index]);
printf("\n");
return 0;
}
结果:
1 2 3
初始化之后数组还可以改变:
#include
int main(void)
{
int a[3] = {1 , 2 , 3};
for (int index = 0 ; index < 3 ; index++)
printf("%d " , a[index]);
printf("\n");
a[0] = 10;
for (int index = 0 ; index < 3 ; index++)
printf("%d " , a[index]);
printf("\n");
return 0;
}
结果:
1 2 3
10 2 3
只能在声明数组时用初始化列表:
#include
int main(void)
{
int a[3];
a[3] = {1 , 2 , 3};
for (int index = 0 ; index < 3 ; index++)
printf("%d " , a[index]);
printf("\n");
return 0;
}
结果:
5:9: error: expected expression before '{' token
a[3] = {1 , 2 , 3};
用const声明的数组是只读数组,不能修改值,所以只能用初始化列表赋值。
示例:
#include
int main(void)
{
const int a[3] = {1 , 2 , 3};
a[1] = 2;
return 0;
}
结果:
5:7: error: assignment of read-only location 'a[1]'
a[1] = 2;
声明数组而没有初始化,则数组元素是当前内存中已有的值。
示例:
#include
int main(void)
{
int a[3];
for (int index = 0 ; index < 3 ; index++)
printf("%d\n" , a[index]);
return 0;
}
结果:
4200800
0
2334720
示例:
#include
#define SIZE 4
int main(void)
{
int a[SIZE] = {9 , 8};
for (int index = 0 ; index < SIZE ; index++)
printf("%d\n" , a[index]);
return 0;
}
结果:
9
8
0
0
后面没有初始化的默认设为0,但这只适用于自动存储类别的数组。
如果初始化列表元素多余数组元素,编译器未报错,只是将前几个匹配的数初始化上了。
#include
#define SIZE 4
int main(void)
{
int a[SIZE] = {9 , 8 , 7 , 6 , 100 , 1000};
for (int index = 0 ; index < SIZE ; index++)
printf("%d\n" , a[index]);
return 0;
}
结果:
9
8
7
6
如果省略数组声明中的方括号内的数字,编译器自动匹配数组元素个数和初始化列表中元素个数相等。
示例:
#include
#define SIZE 4
int main(void)
{
int a[] = {9 , 8 , 7 , 6 , 100 , 1000};
for (int index = 0 ; index < sizeof(a) / sizeof(a[0]) ; index++)
printf("%d\n" , a[index]);
// 整个数组的大小除以单个元素的大小就是数组元素的个数
return 0;
}
结果:
9
8
7
6
100
1000
初始化字符数组有一个特殊方法:
#include
int main(void)
{
char a[] = "China";
for (int index = 0 ; index < sizeof(a) / sizeof(a[0]) ; index++)
printf("%c\n" , a[index]);
return 0;
}
结果:
C
h
i
n
a
注意最后有一个空行,因为这个数组被自动匹配成6个字符,即sizeof(a) = 6
,最后一个是空字符,打印出来什么也没有,是一个空行。
下面程序可以去掉这个空行:
#include
int main(void)
{
char a[] = "China";
for (int index = 0 ; index < sizeof(a) / sizeof(a[0]) - 1; index++)
printf("%c\n" , a[index]);
return 0;
}
结果:
C
h
i
n
a
这时没有打印最后一个字符,即空字符。
自动匹配字符数组长度时最后会自动加上空字符,这时字符数组也是字符串。如:
#include
int main(void)
{
char a[] = "China";
for (int index = 0 ; index < sizeof(a) / sizeof(a[0]) ; index++)
printf("%c\n" , a[index]);
printf("%s\n" , a);
return 0;
}
结果:
C
h
i
n
a
China
这里a不光是字符数组,也是字符串。所以既可以用字符数组的方式输出,也可以当作字符串输出。
如果指定字符数组长度且长度刚好匹配初始化字符串的长度或数组长度小于初始化字符串的长度,字符数组最后一个字符就不是空字符,此时字符数组也就不再是字符串。
示例:
#include
int main(void)
{
char a[3] = "China";
for (int index = 0 ; index < 3 ; index++)
printf("%c\n" , a[index]);
printf("%s\n" , a);
return 0;
}
结果:
C
h
i
Chi
这里a只是字符数组,不是字符串。当把a作为字符串输出时,最后一个字符出错。
指定字符数组的长度是初始化字符串的字符个数加一时,最后一个字符数组元素会自动是空字符。字符数组储存的元素是字符,而空字符的ASCII码值为0.如:
#include
int main(void)
{
char a[6] = "China";
for (int index = 0 ; index < 6 ; index++)
printf("%c\n" , a[index]);
printf("%s\n" , a);
return 0;
}
结果:
C
h
i
n
a
China
指定字符数组的长度比初始化字符串的字符个数加一还要大时,后面的字符数组元素全部都会自动是空字符。如:
#include
int main(void)
{
char a[8] = "China";
for (int index = 0 ; index < 8 ; index++)
printf("%c\n" , a[index]);
printf("%s\n" , a);
return 0;
}
结果:
C
h
i
n
a
China
C99新增了指定初始化器,利用它可以初始化指定的数组元素。
使用方法:在初始化列表中用带方括号的下标指明待初始化的元素。未被初始化的元素默认为0.如:
#include
int main(void)
{
int a[5] = {[3] = 100};
for (int index = 0 ; index < 5 ; index++)
printf("%d\n" , a[index]);
return 0;
}
结果:
0
0
0
100
0
示例2:
#include
int main(void)
{
int a[15] = {10 , 11 , [4] = 100 , 99 , 98 , [9] = 1 , 2 , 3 , [1] = 88};
for (int index = 0 ; index < 15 ; index++)
printf("%d\n" , a[index]);
return 0;
}
结果:
10
88
0
0
100
99
98
0
0
1
2
3
0
0
0
程序中
int a[15] = {10 , 11 , [4] = 100 , 99 , 98 , [9] = 1 , 2 , 3 , [1] = 88};
初始化列表的10
和11
是初始化a[0]
和a[1]
,[4] = 100
是初始化a[4]
,后面的99 , 98
是初始化紧接着的a[5]
和a[6]
,中间跳过的a[2]
和a[3]
自动为0.[9] = 1 , 2 , 3,
同理。[1] = 88
又将a[1]
初始化为88,覆盖了初始化列表前面的初始化。
如果未指定数组元素个数,则编译器会自动初始化刚好能装得下所有元素。如:
int a[] = {[6] = 6}; // 数组a有7个元素,下标从0到6
int a[] = {1 , [6] = 6}; // 数组a有7个元素,下标从0到6
int a[] = {[6] = 6 , 7 , 8 , [12] = 12 , 20 , 100}; // 数组a有15个元素,下标从0到14
不可以把数组作为一个整体赋值给另一个数组,除了初始化之外不可以用初始化列表即花括号列表的形式赋值。
不检查
int a[10][20]; // 定义一个二维数组
int a[2][3]=
{
{1 , 2 , 3},
{4 , 5 , 6}
}; // 初始化一个二维数组,完全匹配
以上代码等价于:
int a[2][3]=
{ 1 , 2 , 3, 4 , 5 , 6 }; // 初始化一个二维数组,完全匹配
不完全匹配:
int a[2][3]=
{ 1 , 2 , 3 }; // 初始化一个二维数组,不完全匹配
此时按照初始化列表中的元素的顺序依次初始化,然后后面的元素为0
int a[4][3]=
{
{1 , 2 },
{4 , 5 , 6}
}; // 初始化一个二维数组,不完全匹配,缺失部分全部默认为0
int a[4][3]=
{
{1 , 2 },
{4 , 5 , 6 , 7 , 8 , 9}
}; // 初始化一个二维数组,初始化列表元素多了编译器也未报错
数组名是数组首元素的地址。
示例:
#include
#define SIZE 4
void fun(int * u);
int main(void)
{
int a[SIZE];
printf("%zd\n" , sizeof(a)); // 打印数组的大小
fun(a);
return 0;
}
void fun(int * u)
{
printf("%zd\n" , sizeof(u)); // 打印指针本身的大小
}
结果:
16
4
说明本系统用四字节存储指针.
如果有:
int a[10];
那么有:
a == &a[0];
a和&a[0]都是常量
声明了一个数组之后,它在内存中的位置就确定了.
可以把a或&a[0]赋给指针,但必须是指向这个数组元素的类型的指针,即让指针指向这个数组的首元素,然后改变指针的值,让指针指向这个数组中的其他元素.
a本身也就是一个指针,完整地说,a是一个指向数组元素类型的指针,指向数组a的首元素,但指针a的值不能改变,即不能指向其他任何位置,只能一直指向数组a的首元素.
示例:
#include
#define SIZE 4
int main(void)
{
int a[SIZE];
double b[SIZE];
int index;
int * pointer_a;
double * pointer_b;
printf("%22s %11s\n" , "int" , "double");
for (index = 0 ; index < SIZE ; index++)
{
printf("pointer + %d : %10p %10p\n" , index , pointer_a + index , pointer_b + index);
}
return 0;
}
结果:
int double
pointer + 0 : 00000000 003E4000
pointer + 1 : 00000004 003E4008
pointer + 2 : 00000008 003E4010
pointer + 3 : 0000000C 003E4018
地址按字节编码,这个系统中,int占4字节,double占8字节.一个占多个字节的对象的地址是所占字节中第一个字节的地址.
在C语言中,指针加1是以字节为单位增加一个存储单元,对数组而言就是把指针指向下一个元素,即这是指针的值是下一个数组元素的地址.
a[n]=*(a+n)
将数组名作为实参进行传递时,因为数组名是地址,所以可以在被调函数中修改原来数组的元素.且形参必须是指向数组元素类型的指针.
如函数声明:
int fun(int * a);
在函数声明和函数定义中,声明形参处,可以这样替换:
int fun(int a[]);
这里的int * a和int a[]都表示a是一个指向int的指针,而int a[]不仅表示指针a指向一个int类型的值,还表示这个值是一个int类型数组的元素.
这四种函数原型都是等价的:
int fun(int * a);
int fun(int a[]);
int fun(int *);
int fun(int []);
这两种函数定义等价:
int fun(int * a)
int fun(int a[])
如果a是指针,那么a++成立,如果a是数组名,那么a++不成立.数组名是常量,不能改变.
在把数组名作为实参传递时,被调函数可以改变数组元素,也可以保护数组元素不被改变。
被调函数的指针形参用const修饰,可以防止被调函数改变数组元素。示例:
#include
void change(const int * p);
int main(void)
{
int a[4]={1,2,3,4};
change(a);
for (int i = 0 ; i < 4 ; i++)
printf("%d " , a[i]);
printf("\n");
return 0;
}
void change(const int * p)
{
for (int i = 0 ; i < 4 ; i++)
{
p[i]++;
}
}
结果:
19:7: error: increment of read-only location '*(p + (sizetype)((unsigned int)i * 4u))'
p[i]++;
^~
如果只是在主调函数中定义数组时用const修饰,将不能防止被调函数修改数组元素。如下:
#include
void change(int * p);
int main(void)
{
const int a[4]={1,2,3,4};
change(a);
for (int i = 0 ; i < 4 ; i++)
printf("%d " , a[i]);
printf("\n");
return 0;
}
void change(int * p) // 传函时是将const数据传递给非指向const的指针,导致这个指针可以修改const数据
{
for (int i = 0 ; i < 4 ; i++)
{
p[i]++;
}
}
结果:
2 3 4 5
可以将非const数据的地址赋值给非指向const的指针,用这个指针改变非const数据不会出错。
C标准规定,用非const标识符改变const数据导致的结果是未定义的。
于是,不能直接修改const数据,通过别的非const标识符改变const数据结果不确定。
修饰符const可以防止数组元素在定义这个数组的函数中再改变数组元素。如:
#include
int main(void)
{
const int a[4]={1,2,3,4};
a[1]=10;
return 0;
}
结果:
6:6: error: assignment of read-only location 'a[1]'
a[1]=10;
^
对于指向const的指针,不能用这个指针改变它所指向的值,如果这个值不是const,那么这个值可以改变,只是不能通过这个指针来改变,但指向const的指针的值可以变,即指针可以指向别处,如:
#include
int main(void)
{
int a[4]={1,2,3,4};
int b[2]={1,2};
const int * p = a;
*p=10; // 指针表示法,不可以
p[2]=10; // 数组表示法,不可以,p是指向数组第一个元素,但也不能用p来改变数组其余元素的值
a[2]=10; // 可以,因为a不是const
p++; // 可以,让p指向a[1]
p=b; // 改变p的指向
return 0;
}
于是,可以将指向const的指针用于函数形参中,表示这个函数不会用这个指针形参改变主调函数的值。这样,指向const的指针可以接受const数组作为实参。
可以将const数据或非const数据的地址赋给指向const的指针。
可以声明并初始化一个只能指向一个位置、不能指向别的位置的指针,如:
int a[3]={1,2,3};
int * const p = a; // 必须同时初始化,此时p指向a[0]
p = &a[1]; // 尝试让p指向a[1],错误,const指针不能指向别处,只能指向初始化时指向的位置
*p = 10; // 可以改变const指针的值,因为这里的a不是const,这里是将a[0]改变值为10
声明指针时可以使用两次const,该指针既不能指向除了初始化之外的其他位置,也不能用这个指针来改变指向的地址上的值。如:
int a[3]={1,2,3};
const int * const p = a;
p = &a[2]; // 不可以将p指向别处
*p = 10; // 不可以用p修改p指向的位置的值
总结一下:
指向const的指针:
const int * p;
不能通过p来改变p指向的内存位置的值,如果这个位置的值不是const,那么可以通过别的方法改变这个值。
const指针:
int a[3]={1,2,3};
int * const p=a;
const指针必须在声明的同时初始化,const指针的值不能变,即只能指向初始化时指定的位置。
指向const的const指针:
const int * const p;
同时具备上述两种性质。
声明一个指针:
int a[2][3];
数组名a是数组a的首元素的地址,数组a有两个元素a[0], a[1],都是含有三个int值的数组,于是a的首元素是一个含三个int值的数组,于是a是这个含三个int值的数组的地址,即a是一个数组的地址。于是有:
a=&a[0];
a[0]是一个含三个int值的数组,则a[0]是一个数组名,即是一个指针,a[0]的值是数组a[0]的首元素的地址,于是有:
a[0]=&a[0][0];
即a[0]是指向一个int类型的指针,a[0]的值是一个int类型的对象的地址。a是指向一个内含三个int类型值的数组的指针,a的值是一个数组的地址。
数组a[0]
和整数a[0][0]
开始于同一个地址,所以有:
a=a[0];
a和a[0]都是数组名,即都是指针,都是const指针,不能指向别处。
a指向a[0],a+1指向数组a[1],这里一个数组占用3个int类型内存空间,所以这里指针a加1是移动了三个int类型的内存空间。
a[0]指向a[0][0]
,a[0]+1是指向a[0][1]
,这里指针加1移动了一个int类型的内存空间。
所以有:
a = a [ 0 ] ; a=a[0]; a=a[0];
但:
a + 1 ≠ a [ 0 ] + 1 ; a+1\ne a[0]+1; a+1=a[0]+1;
a指向a[0],a[0]指向a[0][0]
,于是a是指向指针的指针.
a=a[0]; // 两边都表示指针,两边指针仅是值在数字本身上是相等的
a[0]=&a[0][0];
*a=a[0]; // 可理解为左边是对指针解引用,右边是数组,即解引用得到的值是一个数组,或者理解为两边都是指针
// *a 和 a[0]等价,*(a+1)和a[1]等价,等等
*a=a;
*a[0]=a[0][0]; // 左边是对一个指针解引用
*a=&a[0][0];
**a=*&a[0][0]=a[0][0];
// 对a解引用一次得到的是a指向的指针的值,即一个地址,再解引用这个地址,得到这个地址指向的内存位置的值,解引用是对指针或地址进行运算,找到地址对应的内存位置储存的值
// a是地址的地址,必须解引用两次才能获得内存位置储存的值,
// 地址的地址或指针的指针叫做双重间接(double indirection)
示例:
#include
int main(void)
{
printf("One int has %zd bytes.\n\n" , sizeof(int));
int a[4][2]=
{
{1,2},
{3,4},
{5,6},
{7,8}
};
printf("a = %p , a+1 = %p\n" , a , a+1);
printf("a[0] = %p , a[0]+1 = %p\n" , a[0] , a[0]+1);
printf("*a = %p , *a + 1 = %p\n" , *a , *a + 1);
printf("a[0][0] = %d\n" , a[0][0]);
printf("*a[0] = %d\n" , *a[0]);
printf("**a = %d\n" , **a);
printf("a[3][1] = %d\n" , a[3][1]);
printf("*(*(a+2)+1) = %d\n" , *(*(a+2)+1));
return 0;
}
结果:
One int has 4 bytes.
a = 0061FF00 , a+1 = 0061FF08
a[0] = 0061FF00 , a[0]+1 = 0061FF04
*a = 0061FF00 , *a + 1 = 0061FF04
a[0][0] = 1
*a[0] = 1
**a = 1
a[3][1] = 8
*(*(a+2)+1) = 6
分析:
一个int占用4个字节.
a指向第一个数组,a的值是第一个数组的地址,a+1指向第二个数组,a+1的值是第二个数组的地址,一个数组有两个int值,一个数组占用空间是两个int的空间,即8字节.
一个数组的地址是数组第一个元素的地址.
a[0]指向第一个数组的第一个元素,a[0]的值是第一个数组的第一个元素的地址.所以a的值(第一个数组的地址)和a[0]的值(第一个数组的第一个元素的地址)相等,即a=a[0]
.
a指向数组,a+1移动一个数组的字节数,这里是8字节.
a[0]指向int类型值,a[0]+1移动一个int类型占用的字节个数,这里是4字节.
*a
是指针a指向的内存位置存储的值,指针a指向指针a[0],则*a
是a[0]的值,即*a=a[0]
*a
也是一个指针,和a[0]一样,且都指向同一种数据类型,这里是都指向int类型.*a
也指向第一个数组的第一个元素.
a+2是一个指针,指向第三个数组,*(a+2)
是一个指针,指向第三个数组的第一个元素,*(a+2)+1
是一个指针,指向第三个数组的第二个元素,*(*(a+2)+1)
是第三个数组的第二个元素的值.*(*(a+2)+1)
和a[2][1]
相等,即:
*(*(a+2)+1) = a[2][1];
使用两个解引用运算符或者使用两对方括号都能获得值,也可以使用一个解引用运算符和一对方括号也能获得值.
声明一个指向含有两个int类型值的数组的指针:
int (* p)[2];
p是一个指针,指向一个数组,这个数组含有两个元素,这两个元素是int类型.
声明一个含有两个指针元素的数组:
int * p[2];
[]优先级比*高
,这样就定义了p是一个有两个元素的数组,*表示
这两个元素是指针,int表示指针指向int类型.或者看作int *
一体的,int *
是指向int的指针类型.
示例:
#include
int main(void)
{
int a[4][2]=
{
{1,2},
{3,4},
{5,6},
{7,8}
};
int (*p)[2];
p=a;
printf("p = %p , p+1 = %p\n" , p , p+1);
printf("p[0] = %p , p[0] + 1 = %p\n" , p[0] , p[0] + 1);
printf("*p = %p , *p+1 = %p\n" , *p , *p+1);
printf("p[0][0] = %d\n" , p[0][0]);
printf("*p[0]=%d\n",*p[0]);
printf("**p=%d\n",**p);
printf("p[1][1]=%d\n",p[1][1]);
printf("*(*(p+2)+1) = %d\n" , *(*(p+2)+1));
return 0;
}
虽然p是一个指针,不是数组名,但也可以用p[1][1]
这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名。
可以将指向同类型的指针相互赋值:
int *a , *b;
int c=1;
a=&c;
b=a; // 同类型指针赋值
如果指针a指向含三个元素的数组,指针b指向含4个元素的数组,则不能互相赋值。或者数组元素个数相等但类型不同也不能互相赋值。指针互相赋值要求很严格,必须完全一样。
声明一个指向指针指针:
int **p;
p的值即这个地址指向的内存位置的值也是一个地址,且这个地址指向的内存位置必须存放一个int值。
即p指向一个指向int的指针,*p是指向int的指针。
主要是定义形参。
假如有一个数组如下:
int a[3][4];
那么a的每一个元素都是含有4个int类型元素的数组,对应的处理这个数组的函数的形参应定义为:
void fun(int (*p)[4]);
或定义为:
void fun(int p[][4]);
只有定义形参时即函数头或函数声明中第二种写法正确。
第二种写法中的空的方括号表明p是一个指针,就像在第一种写法里用*表示
p是一个指针一样。
第二个方括号和里面的数字4表示指针p指向一个含有4个元素的数组,前面的int表示这个数组的元素是int类型。
在函数原型中,形参变量的名称可以省略。即:
void fun(int [][4]);
C99新增,C11改为可选项。
] + 1);
printf(“*p = %p , *p+1 = %p\n” , *p , *p+1);
printf(“p[0][0] = %d\n” , p[0][0]);
printf(“p[0]=%d\n",p[0]);
printf(“**p=%d\n”,**p);
printf(“p[1][1]=%d\n”,p[1][1]);
printf("((p+2)+1) = %d\n” , ((p+2)+1));
return 0;
}
虽然p是一个指针,不是数组名,但也可以用`p[1][1]`这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名。
可以将指向同类型的指针相互赋值:
```c
int *a , *b;
int c=1;
a=&c;
b=a; // 同类型指针赋值
如果指针a指向含三个元素的数组,指针b指向含4个元素的数组,则不能互相赋值。或者数组元素个数相等但类型不同也不能互相赋值。指针互相赋值要求很严格,必须完全一样。
声明一个指向指针指针:
int **p;
p的值即这个地址指向的内存位置的值也是一个地址,且这个地址指向的内存位置必须存放一个int值。
即p指向一个指向int的指针,*p是指向int的指针。
主要是定义形参。
假如有一个数组如下:
int a[3][4];
那么a的每一个元素都是含有4个int类型元素的数组,对应的处理这个数组的函数的形参应定义为:
void fun(int (*p)[4]);
或定义为:
void fun(int p[][4]);
只有定义形参时即函数头或函数声明中第二种写法正确。
第二种写法中的空的方括号表明p是一个指针,就像在第一种写法里用*表示
p是一个指针一样。
第二个方括号和里面的数字4表示指针p指向一个含有4个元素的数组,前面的int表示这个数组的元素是int类型。
在函数原型中,形参变量的名称可以省略。即:
void fun(int [][4]);
C99新增,C11改为可选项。