我们已经学习了 C 语言的基本数据类型,了解的它们的声明、初始化、内存占用等,可以处理一些简单的运算!但是编写一个程序,不可避免的要处理大量相关数据;这时对于这些基本数据类型来说就显得力不从心了。通常,数组能高效的处理这种数据。
1、什么是数组(array)?
数组(array)是由数据类型相同的一系列元素组成,这些元素按顺序存储。
如 10 个 char
类型的字符集合 或者 20 个 int
类型的值集合,都可以称为数组。
数组有数组名,通过整数下标访问数组中单独的项或者元素(element)。
在 C 中,数组首元素的下标为 0,所以对于一个内含 n 个元素的数组,其最后一个元素的下标为 n - 1 。作为程序员,要确保正确使用下标,因为编译器和运行的程序都不会检查下标的有效性。
C 语言把数组看做 派生类型 ,因为数组是建立在其它类型的基础之上的。我们无法单独的声明一个数组,在声明一个数组时必须说明其他元素的类型,如 int 型的数组、float 型的数组或者其它类型的数组。所谓的其它类型也可以是数组类型,这时创建的数组为多维数组。
2、数组的声明
一维数组声明的一般形式为:type array[count];
-
type
为基本数据类型,如int
类型、char
类型等,表示数组内的元素的数据类型; -
array
为数组名;也是数组首元素的地址; -
count
:方括号[]中的count
为这个数组内的元素个数,且count
必须是正整数 ;
通过声明数组,告诉编译器数组内含多少个元素,和这些元素的类型,编译器根据这些信息分配内存空间,正确的创建数组。
普通变量可以使用的类型,数组元素都可以使用。如下面代码:
float states[3];
声明了 states
是一个内含 3 个元素的数组,每个元素都可以存储float
类型的值。数组的第一个元素是 states[0]
,以此类推,直到 states[2]
。
2.1、数组的下标
用于识别数组元素的数字被称为 下标 (subscript)、索引 (indice)或者 偏移量 (offset)。 数组的元素依次被存储在内存中相邻的位置。
注意:下标必须是整数,而且从
0
开始;
一个未初始化的数组,其存在是怎么样的呢?我们以刚才声明的数组为例,打印其结果:
float states[3];
for (int i = 0; i < 3; i ++)
{
printf("states[%d]= %f ,地址为:%p \n",i,states[i],&states[i]);
}
//-------------- 打印结果 ----------
//states[0]= 0.000000 ,地址为:0x7ffeefbff50c
//states[1]= -118816176308411065948736323584.000000 ,地址为:0x7ffeefbff510
//states[2]= 0.000000 ,地址为:0x7ffeefbff514
我们可以看到:内存地址间隔4个字节,为三个连续的地址,也就是说编译器已正确创建数组,但是其存储的值都是垃圾值;
2.2、数组的大小
- 数组的元素个数
count
值必须大于 0,而且count
必须是整数; - 声明
count
为 0,这个数组没有存在的意义; -
count
为负整数时编译器报错:'array' declared as an array with a negative size
3、数组的初始化
初始化数组,列表用 {} 括起来,元素之间用逗号 ,分割;
下面演示了一个简单的程序,打印每个月的天数:
void arrayInitialize(void)
{
//初始化数组,列表用 {} 括起来,元素之间用 ,分割
int days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
for (int i = 0; i < 12; i ++)
{
printf("%d 月有 % d天 \n",i + 1,days[i]);
}
}
运行程序,分析打印结果:
1 月有 31天
2 月有 28天
3 月有 31天
4 月有 30天
5 月有 31天
6 月有 30天
7 月有 31天
8 月有 31天
9 月有 30天
10 月有 31天
11 月有 30天
12 月有 31天
3.1、使用数组时的一些陷阱
3.1.1、数组下标越界
在 C 标准中,使用越界下标的结果是未定义的。
假如我们不小心写错索引,索引值大于数组元素个数会发生什么情况?索引值为负,又会发生什么情况?我们通过一个例子来说明:
/*
days[100] 从内存地址方面来讲,就是days[0]元素的内存后第 100 个同样大小的内存,这块内存可能存储 int 型数据,也有可能存储 double 数据(占有的内存大于这块内存),充满了不确定性
*/
days[100];//该数组元素不存在
days[-1];//程序并不报错
在 C 标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是数据被放置在已被其他数据占用的地方,运行结果很奇怪或者异常终止。
C 语言为何允许这种麻烦事发生呢?考虑到程序执行的效率问题,c 编译器不会检查数组的下标是否正确,这样子 C 程序可以执行的更快,编译器没必要捕获所有的下标错误,因为在程序运行之前,数组的下标值可能尚未确定。如果编译器必须在运行时添加额外的代码检查数组的每个下标值,这会降低程序的运行速度。
注意: C 语言保证在为数组分配内存空间时,指向数组后面的第一个位置的指针是有效指针,但是对这个内存位置存储的值未做任何保证。
3.1.2、数组元素类型错误
C 语言在检查类型匹配方面并不是太严格
假如我们在初始化数组时,将错误的数据类型赋值给元素,会发生什么呢?下面我们看一个例子:
int mu[3] = {1 ,2.0 ,3};
for (int i = 0; i < 3; i ++)
{
printf("mu[%d]= %d ,地址为:%p \n",i,mu[i],&mu[i]);
}
我们声明了一个内部包含 3 个int
类型元素的数组,并且初始化这个数组,但是第二个元素由于大意,错写为 浮点型,这时候我们的编译器并未报错;那么这个错误的元素类型,会发生什么呢?看下它的输出:
mu[0]= 1 ,地址为:0x7ffeefbff444
mu[1]= 2 ,地址为:0x7ffeefbff448
mu[2]= 3 ,地址为:0x7ffeefbff44c
观察各个元素的值,可以看到索引为1
的元素,它的前面的值不受影响,正常显示,它的后面的值也不受影响,正常显示;但是这个索引为 1
的值,显示为 2
,明显是个整型!这是为什么呢?
相信大家都可以得出结论:因为我们声明的数组是个包含int
类型元素的数组,系统给这个数组分配了连续三个存储空间,每个存储空间有 4( sizeof(int)
)个字节,这三个存储空间都是存储 int
类型的,所以 2.0
被系统强制转化为int
型 2
存储到内存;这从打印出的地址可以看出,打印的三个地址连续,每相邻地址间隔 sizeof(int)
个字节。
3.1.3、 变量的值与其类型不匹配
在这里,我们回顾一下在 C 基础数据类型里遇到的变量的值与其类型不匹配的问题:
float a = 9;
int b = 9.0;
printf("a = %f , b = %d\n",a,b);
//a = 9.000000 , b = 9
C 语言在检查类型匹配方面并不是太严格,把一个类型的数值初始化给不同类型的变量时,编译器会把值转换成与变量匹配的类型,这会导致部分数据丢失(C 编译器把浮点数转换整数时,会丢弃小数部分,而不进行四舍五入)
通过回顾,我们发现可以使用“编译器会把值转换成与变量匹配的类型”这个规则来解释数组元素类型错误时会发生什么了。
3.2、 数组初始化时元素个数异常
3.2.1 数组元素都没有初始化
数组元素都没有初始化,也就是只声明了数组,这个案例在前面已经讨论过,我们就不再过多叙述了;
3.2.2 数组元素未全部初始化
初始化列表中的项数应与数组的大小一致,如果不一致会怎样?我们还是以一个例子来讲起:
int mu[3] = {1};
for (int i = 0; i < 3; i ++)
{
printf("mu[%d]= %d ,地址为:%p \n",i,mu[i],&mu[i]);
}
我们声明了一个内部包含 3 个 int
类型元素的数组,并且初始化这个数组为第一个元素赋值,但是第二个元素、第三个元素我们并未赋值,我们看下打印结果:
mu[0]= 1 ,地址为:0x7ffeefbff444
mu[1]= 0 ,地址为:0x7ffeefbff448
mu[2]= 0 ,地址为:0x7ffeefbff44c
可以看到,内存地址依旧为三个连续的地址,但是第一个值为 1,其余值均为 0。也就是说:如果部分初始化数组,剩余的元素都会被初始化为0,如果不初始化数组,则内部存储的是垃圾值。
3.2.3 数组初始化的元素个数大于数组元素个数
如果初始化列表的项数多于数组元素个数,会出现什么呢?我们仍然写一个例子
//waring:Excess elements in array initializer
float mun[3] = {1,2,3,4,5,6,7,8};
for (int i = 0; i < 8; i ++)
{
printf("元素 i = %f \n",mun[i]);
}
这时我们的编译器报警告:Excess elements in array initializer
(数组初始化器中有多余元素),既然没有报错,那么我们把这个数组的 8 个值全部打印出来看看:
元素 0 = 1.000000
元素 1 = 2.000000
元素 2 = 3.000000
元素 3 = 0.000000
元素 4 = -105654032.000000
元素 5 = -118816705213457147349000257536.000000
元素 6 = 0.000000
元素 7 = 0.000000
我们可以看到:前三个元素是有保证的,打印的是我们赋予的值;但是后面 5 个元素未做任何保证,都是垃圾值。也就是说:即使初始化列表的项数多于数组元素个数,编译器也只会给数组大小范围内的元素分配内控空间并存储,超出范围的未做任何保证,都是垃圾值。
3.3、自动计数
如果我们不确定数组元素个数怎么办呢?我们可以直接声明一个含有元素个数足够多的数组,这样即使我们使用再大的索引,也不怕数组越界,但是这样做,显然浪费了大量内存;
C 已经给了我们解决办法:在初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小:
float mun[] = {1,2,3};
我们并未给出数组元素个数,并不知道数组的大小,我们使用 sizeof()
函数计算出数组的大小、数组的元素个数
3.4、 指定初始化器(Designated Initializer)
C99 增加了一个新特性:指定初始化器(Designated Initializer),利用该特性,可以初始化指定的数组元素。例如。只初始化数组的最后一个元素。
void designatedInitializerMethod(void)
{
int days[6] = {1,[3] = 22,45,[0] = 76};
for (int i = 0; i < sizeof(days) / sizeof(int); i ++)
{
printf("days[%d]= %d ,地址为:%p \n",i,days[i],&days[i]);
}
}
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为 0。但是上面这个程序明显比较复杂,days
是一个包含 6
个 int
型元素的数组,但是我们只初始化了第一个元素,第三个元素,第三个元素后面还跟了一个元素,那么这个数组各个元素的值是怎么样呢?我们看下打印结果:
days[0]= 76 ,地址为:0x7ffeefbff460
days[1]= 0 ,地址为:0x7ffeefbff464
days[2]= 0 ,地址为:0x7ffeefbff468
days[3]= 22 ,地址为:0x7ffeefbff46c
days[4]= 45 ,地址为:0x7ffeefbff470
days[5]= 0 ,地址为:0x7ffeefbff474
以上输出,揭示了指定初始化器的两个重要特性:
-
如果指定初始化器后面有更多的值,那么后面这些值将被用于指定元素后面的元素
如值 45,位于指定的第三个元素之后,那么它就是第四个元素的值 -
如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
如 days 里第 0 个元素,我们初始化为 1 ,但是之后又指定初始化为 76 ,那么它最后的值为 76
3.5、使用 const
声明数组
有时需要把数组设置为只读,这样程序只能从数组中检索值,不能把新值写入数组,要创建这样的数组,应该使用 const
声明和初始化数组:
const int days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
//dates[2] = 3;//不允许,编译器报错:Read-only variable is not assignable
或者:
int const days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
//dates[2] = 3;//不允许,编译器报错:Read-only variable is not assignable
3.6、给数组元素赋值
声明数组后,可以借助数组下标给数组元素赋值
int days[6] = {1,[3] = 22,45,[0] = 76};
days[1] = 98;
days[2] = 56;
注意:C 不允许在初始化以外使用花括号列表的形式赋值。
int days[6] = {1,[3] = 22,45,[0] = 76};
days = {1,2};//编译错误error : Expected expression
注意:C 不允许把数组作为一个单元赋给另一个数组。
int days[6] = {1,[3] = 22,45,[0] = 76};
int days2[6] = days;
//编译错误error: Array initializer must be an initializer list or wide string literal
4、变长数组(VLA)
对于传统的 C 数组,必须使用常量表达式指明数组的大小,所以数组大小在编译时就已确定。C99/C11 新增了变长数组,可以用变量表示数组的大小。这意味着变长数组的大小延迟到程序运行时才确定。
C99/C11 新增的变长数组,允许使用变量表示数组的维度。变长数组必须是自动存储类别,这意味着在函数中声明或者在函数形参中声明,都不能使用 static
或 extern
存储类别说明符,不能在初始化中声明它们。
注意:变长数组的 变 不是指修改已创建的数组大小,一旦创建了变长数组,它的大小保持不变。这里的 变 指的是:在创建数组时可以使用指定数组的维度
5、多维数组
多维数组:数组的数组
C 语言支持多维数组:数组的声明一般形式为:
type name[size1][size2]...[sizeN];
我们来声明一个 3 维数组:int dogs[3][3][5];
我们声明了一个包含三个元素的数组 dogs
,这个数组的每个元素是一个包含三个项的数组,每个项又是包含 5 个 int
型元素的数组;也就是说这个数组 dogs
,是个数组的数组;
在计算机内部:多维数组是按顺序存储的。从 dogs[0][0][0]
--> dogs[0][0][4]
--> ... dogs[0][2][4]
--> .. dogs[2][2][4]
。
5.1、二维数组
我们以一个图表来讨论二维数组int pigs[4][5]
:
主数组 pigs
有 4
个元素,每个元素是内含 5
个 int
型数据的数组:pigs
的首元素pigs[0]
是一个内含 5
个 int
型数据的数组;pigs[1]
、pigs[2]
、pigs[3]
都是如此。
如果 pigs[0]
是一个数组,那么它的首元素就是 pigs[0][0]
,第二个元素是 pigs[0][1]
,以此类推。
假如要访问三行三列的值,则使用 pigs[2][2]
。
在计算机内部,这样的数组是按顺序存储的:从第一个内含 5
个int
型数据的数组开始,然后是第二个内含 5
个 int
型数据的数组,以此类推
我们给这个二维数组 pigs
初始化
void twoDimensionArray(void)
{
int pigs[4][5] =
{
{1,2,3,4,5},
{10,20,30,40},
{100,200,300,400,500,600},
{1000,2000,3000},
};
for (int i = 0; i < 4; i ++)
{
for (int j = 0; j < 5; j ++)
{
printf("%4d ",pigs[i][j]);
}
for (int j = 0; j < 5; j ++)
{
printf("%p ",&pigs[i][j]);
}
printf("\n");
}
}
这个初始化用了四个数值列表,每个数值列表都用花括号括起来;第一个列表初始化数组的第一行(即上图中的第一行数据),第二个列表初始化数组的第二行(即上图中的第二行数据),以此类推
它的打印结果为:
1 2 3 4 5 0x7ffeefbff420 0x7ffeefbff424 0x7ffeefbff428 0x7ffeefbff42c 0x7ffeefbff430
10 20 30 40 0 0x7ffeefbff434 0x7ffeefbff438 0x7ffeefbff43c 0x7ffeefbff440 0x7ffeefbff444
100 200 300 400 500 0x7ffeefbff448 0x7ffeefbff44c 0x7ffeefbff450 0x7ffeefbff454 0x7ffeefbff458
1000 2000 3000 0 0 0x7ffeefbff45c 0x7ffeefbff460 0x7ffeefbff464 0x7ffeefbff468 0x7ffeefbff46c
数组元素的内存地址:从 pigs[0][0]
-- > pigs[0][4]
--> pigs[3][4]
,内存地址是连续排列的,间隔 4
个字节。
- 假如某一行没有全部初始化,则这一行的剩余元素默认初始化为
0
; - 假如某一行的数值个数超出数组大小,并不会影响其它行的初始化;
思考:
既然二维数组,内存地址是连续分布的,那么是否可以只使用一个花括号来表示所有的值?我们不妨来试试:
void twoDimensionArray(void)
{
int pigs[4][5] =
{
1,2,3,4,5,
10,20,30,40,
100,200,300,400,500,600,
1000,2000,3000,
};
for (int i = 0; i < 4; i ++)
{
for (int j = 0; j < 5; j ++)
{
printf("%4d ",pigs[i][j]);
}
for (int j = 0; j < 5; j ++)
{
printf("%p ",&pigs[i][j]);
}
printf("\n");
}
/*
------------------------------- 打印结果 -------------------------------
1 2 3 4 5 0x7ffeefbff420 0x7ffeefbff424 0x7ffeefbff428 0x7ffeefbff42c 0x7ffeefbff430
10 20 30 40 100 0x7ffeefbff434 0x7ffeefbff438 0x7ffeefbff43c 0x7ffeefbff440 0x7ffeefbff444
200 300 400 500 600 0x7ffeefbff448 0x7ffeefbff44c 0x7ffeefbff450 0x7ffeefbff454 0x7ffeefbff458
1000 2000 3000 0 0 0x7ffeefbff45c 0x7ffeefbff460 0x7ffeefbff464 0x7ffeefbff468 0x7ffeefbff46c
*/
}
我们在第二行、第四行列表各少初始化一个元素,打印结果为最后两个元素为 0;
初始化时可以省略内部的花括号,只保留最外一层。但是这样做,必须保证初始化的数值个数是正确的
6、数组 与 指针
指针(pointer)是一个值为内存地址的变量。
因为计算机内部的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的形式表达。因此,使用指针的程序更有效率。尤其是,数组表示法其实是在变相的使用指针。
我们来举个例子:
void arrayAndPointer(void)
{
//在我们的系统中,地址按字节编址,short 类型占 2 个字节,double 类型占 8 个字节
printf("sizeof(short) : %zd ;sizeof(double) : %zd \n",sizeof(short),sizeof(double));
//sizeof(short) : 2 ;sizeof(double) : 8
short dates[4];//声明了一个含有 4 个 short 类型元素的数组
short * pti;//声明了一个指向 short 类型值的指针
int index;//声明了一个 int 类型的变量
double bills[4];//声明了一个含有 4 个 double 类型元素的数组
double * ptf;//声明了一个指向 double 类型值的指针
pti = dates;//把数组地址赋给指针
ptf = bills;//把数组地址赋给指针
printf("%30s %15s \n","short","double");
for (index = 0 ; index < 4; index ++)
{
//打印指针
printf("pointers + %d : %10p %10p \n",index,pti + index,ptf + index);
//打印数组元素的地址
printf("array element + %d : %10p %10p \n",index,&dates[index],&bills[index]);
}
}
下面是该程序的输出:
sizeof(short) : 2 ;sizeof(double) : 8
short double
pointers + 0 : 0x7ffeefbff470 0x7ffeefbff450
array element + 0 : 0x7ffeefbff470 0x7ffeefbff450
pointers + 1 : 0x7ffeefbff472 0x7ffeefbff458
array element + 1 : 0x7ffeefbff472 0x7ffeefbff458
pointers + 2 : 0x7ffeefbff474 0x7ffeefbff460
array element + 2 : 0x7ffeefbff474 0x7ffeefbff460
pointers + 3 : 0x7ffeefbff476 0x7ffeefbff468
array element + 3 : 0x7ffeefbff476 0x7ffeefbff468
分析打印结果:
- 第一行表明了:在我们系统中,
short
占2
个字节,double
占8
个字节; - 第三、四行分别用指针表示法、数组表示法打印两个数组开始的地址:可以看到:第三行和第四行的结果分别对应相等;
- 第五行是指针加 1 后的地址,与第六行数组表示法下一个值得地址相等;
- 以此类推...
在 C 中,指针加 1 指的是增加一个存储单元(这个存储单元的大小依据变量的类型来确定)。对于数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址。这也是为什么必须声明指针所指向的对象类型的原因之一。只知道内存首地址是不够的,还需要知道这个变量占据多少的字节的存储空间。
数组和指针加法.pngdates + 2 == &dates[2]; //表示相同的地址
*(dates + 2) == dates[2]; //相同的值
数组与指针的关系十分密切,可以使用指针标识数组的元素。从本质上讲,同一个对象有两种表示法。C 语言标准在描述数组表示法时确实借助了指针,array[n]
的意思是 *(array + n)
,即 到内存 array
的位置,然后再移动n
个单元,检索储存在那里的值
注意: *
的运算级别高于 +
*(dates + 2) ;//数组 dates 索引为 2 的值
*dates + 2;//数组 dates 索引为 0 的值 再加上 2
6.1、指针的操作
C 提供了一些基本的指针操作;我们来看程序:
void pointerOperation(void)
{
printf("sizeof(int) = %zd \n",sizeof(int));
// sizeof(int) = 4 (int 型占 4 个字节)
/*
1、声明一个含有5个int 类型元素 的 数组
2、初始化每个元素的值
3、array 数组名,是数组首元素的地址
4、int 占四个字节,这个数组有五个 int 类型元素,占 20 个字节
*/
int array[5] = {100,200,300,400,500};//声明一个整型数组,并初始化
printf("sizeof(array) = %zd \n",sizeof(array));
// sizeof(array) = 20
/*
1、声明三个 int* 类型的指针变量
2、由于未初始化,所以指针变量指向的地址,既指针变量的值不确定是随机值
*/
int *p1 , *p2 , *p3;//声明指针变量
printf("p1 = %p , p2 = %p , p3 = %p \n",p1,p2,p3);
//p1 = 0x2b00000028 , p2 = 0x280000003d , p3 = 0x3d00000024
/*
此时:p1 与 p2 的值 相差 8 个字节,2个 int 元素
指针 p1 指向内存地址的值 为100 ,既数组 array 的首位元素 100 ,是第0位元素的地址
指针 p2 指向内存地址的值 为300 ,既数组 array 的第2位元素 300,是第2位元素的地址
以上说明:说明数组元素的内存地址是连续的
指针变量 p1 的内存地址为 0x7fff5fbff638
指针变量 p2 的内存地址为 0x7fff5fbff630
为什么 p1 比 p2 的地址高呢? 因为栈空间是从 高地址 向 低地址 扩展的,先声明的 p1,那么 p1 的内存地址自然比 p2 的高
*/
p1 = array;//数组名是数组首元素的地址
p2 = &array[2];//把一个地址赋给指针
printf("p1 = %p , *p1 = %d , &p1 = %p \n",p1,*p1,&p1);
//p1 = 0x7fff5fbff640 , *p1 = 100 , &p1 = 0x7fff5fbff638
printf("p2 = %p , *p2 = %d , &p2 = %p \n",p2,*p2,&p2);
//p2 = 0x7fff5fbff648 , *p2 = 300 , &p2 = 0x7fff5fbff630
/*
指针 p1 + 4 = 0x7fff5fbff640 + 4 * sizeof(int) = 0x7fff5fbff640 + 16 = 0x7fff5fbff650
指针减去一个整数:指针必须是减数,整数是被减数
*/
p3 = p1 + 4;//指针加法
printf("p1 + 4 = %p , *(p1 + 4) = %d , &p3 = %p \n",p1 + 4,*(p1 + 4),&p3);
//p1 + 4 = 0x7fff5fbff650 , *(p1 + 4) = 500 , &p3 = 0x7fff5fbff628
printf("p3 = %p , *p3 = %d , &p3 = %p \n",p3,*p3,&p3);
//p3 = 0x7fff5fbff650 , *p3 = 500 , &p3 = 0x7fff5fbff628
/*
递增:让 该指针 移动至数组的下一个元素
此时:变量 p1 的内存地址 仍为 0x7fff5fbff638
注意:变量不会因为值发生变化就移动位置
*/
p1 ++;//递增指针
printf("p1++ = %p , *(p1++) = %d , &p1 = %p \n",p1,*p1,&p1);
//p1++ = 0x7fff5fbff644 , *(p1++) = 200 , &p1 = 0x7fff5fbff638
p2 --;//递减指针
printf("p2-- = %p , *(p2--) = %d , &p2 = %p \n",p2,*p2,&p2);
//p2-- = 0x7fff5fbff644 , *(p2--) = 200 , &p2 = 0x7fff5fbff630
/*
两个指针相减:指针求差 (通常求差的两个指针分别是同一数组的不同元素)
通过求差,计算出两个元素之间的距离
*/
printf("p3 - p2 = %td \n",p3 - p2);
//p3 - p2 = 3
//指针减去整数
printf("p3 - 2 = %p \n",p3 - 2);
//p3 - 2 = 0x7fff5fbff648
}
上面一段程序演示了指针变量的基本操作:
-
赋值: 可以把地址赋值给指针。例如:使用数组名、带地址运算符 & 的变量名、另一个指针进行赋值。
注意:地址应该与指针类型兼容;不能把double
型的地址赋给指向int
的指针。 - 解引用: * 运算符给出指针指向地址上存储的值。
- 取址: 和所有变量一样,指针变量也有自己的地址 和 值。
-
指针与整数相加: 可以使用
+
运算符把指针与整数相加,或者整数与指针相加。整数和指针所指向类型的大小(以字节为单位)相乘,然后把乘积与指针的值相加
注意:在做加法时,编译器不会检查指针是否仍然指向数组元素,C 只能保证指向数组任意元素的指针 和 数组后面第一个位置的指针有效,如果超出了这个范围,则是未定义 - 递增指针: 指向数组元素的指针可以通过递增让该指针移动至数组的下一个元素。
-
指针减去一个整数: 可以使用 - 运算符从一个指针减去一个整数。
必须指针是减数,整数是被减数。如果相减的结果超出初始指针所指向的数组的范围,计算结果是未定义的。 - 递减指针: 指向数组元素的指针可以通过递减让该指针移动至数组的上一个元素。
- 指针求差: 可以计算两个指针的差值。求差的两个指针分别指向同一数组的不同元素,通过求差计算出两个元素之间的距离
- 比较: 使用关系运算符可以比较两个指针的值,前提是这两个指针都指向相同类型的对象。
6.2、指针 与 多维数组
指针与多维数组有什么关系呢?我们先来看一段程序:
void twoDimensionArray(void)
{
int pigs[3][2] = {{1,3},{5,7},{2,4}};
printf("pigs = %p ; pigs + 1 = %p \n",pigs,pigs + 1);//二维数组 pigs 的地址与一维数组 pigs[0] 的地址相同,它们的地址都是各自数组首元素的地址,因此与 &pigs[0][0] 的地址相同
printf("pigs[0] = %p ; pigs[0] + 1 = %p \n",pigs[0],pigs[0] + 1);
printf("*pigs = %p ; *pigs + 1 = %p \n",*pigs,*pigs + 1);
printf("pigs[0][0] = %p \n",&pigs[0][0]);
printf("pigs[0][0] = %d \n",pigs[0][0]);
printf("*pigs[0] = %d \n",*pigs[0]);
printf("**pigs = %d \n",**pigs);
printf("pigs[2][1] = %d \n",pigs[2][1]);
printf("*(*(pigs + 2) + 1) = %d \n",*(*(pigs + 2) + 1));
}
/*
------------------------------- 打印结果 -------------------------------
pigs = 0x7ffeefbff470 ; pigs + 1 = 0x7ffeefbff478
pigs[0] = 0x7ffeefbff470 ; pigs[0] + 1 = 0x7ffeefbff474
*pigs = 0x7ffeefbff470 ; *pigs + 1 = 0x7ffeefbff474
pigs[0][0] = 0x7ffeefbff470
pigs[0][0] = 1
*pigs[0] = 1
**pigs = 1
pigs[2][1] = 4
*(*(pigs + 2) + 1) = 4
*/
我们先来分析一下这个二维数组的声明:
int pigs[3][2] = {{1,3},{5,7},{2,4}};
数组名pigs
是该数组首元素的地址,而该数组首元素又是一个内含两个int
型的数组,所以 pigs
是这个内含两个int
值数组的地址:
- 因为
pigs
是数组首元素的地址,所以pigs
的值和&pigs[0]
的值相同。而pigs[0]
本身是一个内含两个整数的数组,所以pigs[0]
的值和&pigs[0][0]
的值相同。pigs
是一个占用两个int
大小对象的地址,pigs[0]
是一个占用一个int
大小对象的地址。由于这个整数 和 内含两个整数的数组都开始于同一个地址,所以pigs
和pigs[0]
的值相同。 - 我们知道,给指针或者地址加 1,其值会增加对应类型大小的数值。在这里,
pigs
与pigs[0]
不同,因为pigs
指向的对象占用了 2 个int
大小,而pigs[0]
指向的对象占用了 1 个int
大小。因此,pigs + 1
和pigs[0] + 1
的值不同。 - 解引用一个指针,得到引用对象代表的值。因为
pigs
是该数组首元素pigs[0][0]
的地址,所以*(pigs[0])
表示存储在pigs[0][0]
上的值,* pigs
表示存储在pigs[0]
上的值。但是pigs[0]
本身又是一个地址,该地址的值为&pigs[0][0]
。对两个表达式引用解引用运算符表明,**pigs
与*&pigs[0][0]
等价。简而言之,pigs[0][0]
是地址的地址,必须解引用两次才能获得原始值。
我们以一个图来演示数组地址、数组内容和指针的关系:
数组的数组.png6.3、指向多维数组的指针
如何声明一个指针变量p
指向一个二维数组呢?
int *p[2];
由于 []
的优先级高,先与 p
结合,所以 p
是一个内含 2 个元素的数组,然后 *
表示 p
数组内含 2 个指针。也就是说;我们声明了 2 个指向 int
型的指针。
我们再来看看下一种声明:
int (*p)[2];
其中 *
先与 p
结合,声明的是一个指向数组(内含 2 个 int
型数据)的指针。
我们来看看下面一段程序:
void twoDimensionArray(void)
{
int pigs[3][2] = {{1,3},{5,7},{2,4}};
int (* p) [2] = pigs;
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] = %p \n",&p[0][0]);
printf("p[0][0] = %d \n",p[0][0]);
printf("*p[0] = %d \n",*p[0]);
printf("**p = %d \n",**p);
printf("p[2][1] = %d \n",p[2][1]);
printf("*(*(p + 2) + 1) = %d \n",*(*(p + 2) + 1));
}
/*
------------------------------- 打印结果 -------------------------------
p = 0x7ffeefbff420 ; p + 1 = 0x7ffeefbff428
p[0] = 0x7ffeefbff420 ; p[0] + 1 = 0x7ffeefbff424
*p = 0x7ffeefbff420 ; *p + 1 = 0x7ffeefbff424
p[0][0] = 0x7ffeefbff420
p[0][0] = 1
*p[0] = 1
**p = 1
p[2][1] = 4
*(*(p + 2) + 1) = 4
*/
虽然,p
是一个指针不是一个数组,但也可以使用 p[2][1]
这种写法。可以使用数组表示法或者指针表示法表示一个数组元素,既可以使用数组名也可以使用指针名。
6.4、指针的兼容性
指针的赋值比较严格。我们可以使用把int
类型的值赋给 double
型变量,但是两个类型的指针不能这样做。
void pointerCompatibility (void)
{
int a = 3;
double x;
int *p1 = &a;
double *q1 = &x;
x = a;//我们可以把 int 型数据赋值给 double 型变量
q1 = p1;//waring:Incompatible pointer types assigning to 'double *' from 'int *'
}
声明数组将分配存储数据的空间,声明指针只分配存储一个地址的空间
7、函数、数组 与 指针
编写一个处理基本数据类型(如 float
)的函数时,要选择是传递float
类型的值还是传递指向float
的指针。我们的通常做法是直接传递值,只有程序需要在函数中改变该值时才会传递指针。
那么:编写一个处理数组的函数时,我们该如何做呢?
7.1、声明数组形参:
首先我们需要明确:编写一个处理数组的函数时,这个函数必须知道数组何时开始、何时结束。显而易见,我们有多种方法满足它的两个要求。
7.1.1、数组形参中使用 数组名 与 数组大小
/*函数的声明:计算数组内所有元素之和
第一个参数告诉函数,该数组的首元素的指针和数据类型;
第二个参数告诉函数:该数组中元素的个数。
*/
int sumFunction(int array[],int count);
//函数的实现
int sumFunction(int array[],int count)
{
int toast = 0;
for (int i = 0; i < count; i ++)
{
toast += array[i];
}
return toast;
}
下面我们来调用这个函数:
int array[7] = {1,2,3,4,5,6,7};
printf("数组的所有元素之和为 : %d \n",sumFunction(array, 7));
// 打印结果: 数组的所有元素之和为 : 28
可以看到,这种方法满足我们的需求。
注意: 在第一行数组的初始化中array
占有 sizeof(int) * 7
个字节,表示整个数组的大小;在第二行作为形参中,array
占有 8
个字节(一个指针变量占得字节数),因为它并不是一个数组,而是一个指向数组首元素的地址。
7.1..2、 数组形参中使用 指针与 数组大小
在上文中因为数组名是该数组首元素的地址,所以我们可以直接使用一个指针来声明
/*函数的声明:计算数组内所有元素之和
第一个参数告诉函数,该数组的地址和数据类型;
第二个参数告诉函数:该数组中元素的个数。
*/
int sumFunction(int *array,int count);
//函数的实现
int sumFunction(int *array,int count)
{
int toast = 0;
for (int i = 0; i < count; i ++)
{
toast += array[i];
}
return toast;
}
下面我们来调用这个函数:
int array[7] = {1,2,3,4,5,6,7};
printf("数组的所有元素之和为 : %d \n",sumFunction(array, 7));
// 打印结果: 数组的所有元素之和为 : 28
可以看到,这种方法满足我们的需求,而且和 1.1 一样;
注意: int array[]
与 int *array
形式上都表示 array
是一个指向 int
的指针。但是,int array[]
只能用于声明形式参数。
由于函数原型可以省略参数名,所以以下等价:
int sumFunction(int array[],int count);
int sumFunction(int [],int);
int sumFunction(int *array,int count);
int sumFunction(int *,int);
思考一下: 除了使用数组首元素地址 和 数组元素个数这种表示法声明函数形参,我们还可以使用什么办法处理数组(告诉函数这个数组何时开始,何时结束)?
7.1.3、 数组形参中使用 两个指针
我们可以传递两个指针,第一个指针指明数组的开始处,第二个指针指明数组的结束处:
/*函数的声明:计算数组内所有元素之和
第一个参数 start 是一个指针,指向数组首元素
第二个参数 end 是一个指针,指向数组的结尾处
*/
int sumFunction(int *start,int *end);
/*数组的实现:
一元运算符 * 和 ++ 的优先级相同,但结合律从右往左,所以 start++ 先求值;
递增运算符使用后缀形式,意味着先把指针指向的值加到 toast 上,然后再递增指针
start 递增 1 相当于其值递增 int 类型的大小;
end 指向数组最后一个元素的后面( C 语言保证指向数组后面第一个位置的指针是有效指针,但是对于这个位置的值没有做任何保证,程序最好不要访问该位置)
*/
int sumFunction(int *start,int *end)
{
int toast = 0;
while (start < end)
{
toast += *start++;
}
return toast;
}
下面我们来调用这个函数:
int array[7] = {1,2,3,4,5,6,7};
printf("数组的所有元素之和为 : %d \n",sumFunction(array, array + 7));
可以看到,这种方法满足我们的需求,而且和 1.1 与 1.2 一样;
7.1.4、总结
从以上 3 种函数形参的最终结果来看,无论数组表示法 或者 指针表示法,都能打到我们的需求。
使用 数组表示法,可以更清晰的表达函数处理数组的意图;但是对于数组而言:最好传递指针,因为这样做,效率更高。你可以想象一下:假如传递的是数组,则必须在 栈区(stack) 分配足够的内存空间来存储该数组的副本,然后把数组的所有数据拷贝至新的数组中;而如果我们把数组的地址传递给函数,栈区 只需分配一小块的内存空间来存储该地址的副本,则函数直接处理原始数组效率更高。
思考: 编写一个处理数组的函数时传递指针安全嘛?
7.2、形参安全
C 语言通常按值传递数据,传递的是原始数据的副本,不会意外修改原始数据,保证了数据的完整性。但是处理数组时我们为了效率问题传递的是指针,这时如果我们不需要修改数据,那么这份数据在函数中就存在被篡改的风险:如下面程序:
/* 我们的本意是:计算数组中各个元素的和,不改变数组的原始数据
但是,以下函数虽然计算出了数组元素之和,但是却也改变了原始数据,违背我们的初衷
*/
int sumFunction(int array[],int count)
{
int toast = 0;
for (int i = 0; i < count; i ++)
{
toast += array[i] ++;//错误的递增了每个元素的值
}
return toast;
}
7.2.1、对形参使用 const
针对上文的思考,我们可以提高警惕避免错误,但是我们总有疏忽大意的时刻。ANSI C 提供了一种预防手段:如果函数的意图不是修改数组中的数据内容 ,那么在函数原型和函数定义中声明形参时应使用关键字const
。再看下面程序:
//函数声明
int sumFunction(const int array[],int count);
//函数实现
int sumFunction(const int array[],int count)
{
int toast = 0;
for (int i = 0; i < count; i ++)
{
toast += array[i];
}
return toast;
}
我们使用 const
告诉编译器:该函数不能修改 array
指向的数组的内容,这时在函数中再不小心改变数组数据,编译器就会捕获这个错误并生成一条错误信息。
这里我们需要明白:使用 const
并不是要求原始数组是常量,只是在该函数处将数组视为常量,不可更改。
一般而言:如果编写的函数需要修改数组,在声明形参时不使用 const
;如果编写的函数不需要修改数组,在声明形参时最好使用 const
。
7.3、 const 的其它内容
#define A 1
const int a = 1;
虽然我们可以使用#define
达到类似 const
的功能,但是显然 const
更加灵活。可以创建 const
数组、const
指针、指向 const
的指针。
7.3.1、 指向 const 的指针不能用于改变值
考虑下面代码:
int array[7] = {1,2,3,4,5,6,7};
const int *p = array;//指针 p 指向数组 array 的首元素,使用 const 表明不能使用 p 来更改他所指向的值
*p = 10;//编译器error : Read-only variable is not assignable
p[2] = 100;//编译器error : Read-only variable is not assignable
array[2] = 100;//允许,因为 array 未被 const 限定
p ++;//允许指针 p 指向别处,递增指向 array[1]
p = array2; //允许指针 p 指向别处,再次给指针 p 指向新的内存地址
无论使用指针表示法 或者 数组表示法,都不允许修改 p
所指向数据的值,但是并不限定 array
,因为他未被 const
限定。允许指针p
指向新的内存地址。
指向const
的指针一般用于数组的形参,表明该函数不会使用指针改变数据。
7.3.2、 使用 const 声明并初始化一个不能指向别处的指针
int array[7] = {1,2,3,4,5,6,7};
int array2[7] = {1,2,3,4,5,6,7};
int * const p = array;//指针 p 指向数组 array 的首元素,使用 const 表明不能使用 p 指向别处
*p = 10;
p[2] = 100;
p ++;//编译器error : Cannot assign to variable 'p' with const-qualified type 'int *const'
p = array2;//编译器error : Cannot assign to variable 'p' with const-qualified type 'int *const'
array[2] = 100;//允许,因为 array 未被 const 限定
可以使用指针来修改它所指向的值,但是不能让指针重新指向别处;
7.3.3、 既不能指向别处 又不能更改它所指向地址的值 的指针
int array[7] = {1,2,3,4,5,6,7};
int array2[7] = {1,2,3,4,5,6,7};
const int * const p = array;//指针 p 指向数组 array 的首元素,使用 const 表明不能使用 p 来更改他所指向的值
*p = 10;//编译器error : Read-only variable is not assignable
p[2] = 100;//编译器error : Read-only variable is not assignable
p ++;//编译器error : Cannot assign to variable 'p' with const-qualified type 'int *const'
p = array2;
array[2] = 100;//允许,因为 array 未被 const 限定
网友评论