C语言指针的理解二:指针与数组、数组指针与指针数组
1.深入学习数组
1.1 从内存角度来理解数组
从内存角度讲,数组变量就是一次分配多个同类型的变量,并且这些变量在内存中的存储单元是连续依次分布的。
分开定义多个变量,比如int a, b, c, d;
和一次定义一个数组int a[4];
,这两种定义方法相同点是都定义了4个int
型变量,而且这4个变量都是独立的单个使用的。不同点是单独定义时a
、b
、c
、d
在内存中的地址不一定相连,但是定义成数组后,数组中的4个元素地址肯定是连续依次相连的。
数组中多个变量虽然必须单独访问,但是因为它们的地址是连续分布的,因此很适合用指针来操作,于是数组和指针之间的关系就叫天生纠结在一起的。
1.2 从编译器角度来理解数组
从编译器角度来讲,数组变量也是变量,和普通变量和指针变量并没有本质区别。变量的本质就是一个有地址编号的内存空间,这个地址由编译器决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度。
1.3 数组中几个关键符号的理解
int a[5]={0,1,2,3,4};
a
、a[0]
、&a
、&a[0]
这4个符号搞清楚了,数组相关的很多问题都有答案了。理解这些符号的时候要和左值右值结合起来,也就是搞清楚每个符号分别做左值和右值时的不同含义。
a
就是数组名。a
做左值时表示整个数组的所有空间:5×4=20字节,又因为C
语言规定数组操作时要每个元素独立单个操作,不能整体操作数组,所以a
不能做左值。a
做右值表示数组首元素,也就是a[0]
的首地址,首地址就是起始地址,就是4个字节中最开始第一个字节的地址,a
做右值等同于&a[0]
;a[0]
表示数组的首元素,也就是数组的第0个元素。a[0]
做左值时表示数组第0个元素对应的连续4字节内存空间。做右值时表示数组第0个元素的值,也就是数组第0个元素对应的内存空间中存储的那个数。&a
就是数组名a
取地址,字面意思来看就应该是数组的地址。&a
不能做左值,因为&a
实质是一个常量而不是变量因此不能赋值,所以自然不能做左值。&a
做右值时表示整个数组的首地址。&a[0]
字面意思就是数组第0个元素的首地址,[]
的优先级要高于&
,所以a
先和[]
结合再取地址。做左值时表示数组首元素对应的内存空间,做右值时表示数组首元素的值,也就是数组首元素对应的内存空间中存储的那个数值。做右值时&a[0]
等同于a
。
解释:为什么数组的地址是常量?因为数组是编译器在内存中自动分配的。当我们每次执行程序时,运行时都会帮我们分配一块内存给这个数组,只要完成了分配,这个数组的地址就定好了,本次程序运行直到终止都无法再改了。在程序中只能通过
&a
来获取这个分配的地址,却不能去用赋值运算符修改它。
2.指针与数组的天生姻缘
2.1 指针方式访问数组元素
数组元素使用时不能整体访问,只能单个访问。访问方式有2种:
- 数组形式:
数组名[下标];
,注意下标从0开始。 - 指针形式:
*(指针+偏移量);
,如果指针是数组首元素地址,比如a
或者&a[0]
,那么偏移量就是下标。指针也可以不是首元素地址而是其他哪个元素的地址,这时候偏移量就要结合实际情况考虑了。
举个栗子:
#include<stdio.h>
int main(int argc,char**argv)
{
int a[5]={0,1,2,3,4};
for(int i=0;i<5;++i)
{
//数组形式访问元素
printf("%d. ",a[i]);
}
printf("\n");
int* p = &a[1]; //p指向数组的第2个元素a[1]
*++p = 3; //++优先级高于*,所以p先加1,指向a[2],然后解引用,修改的就是a[2]
for(int i=0;i<5;++i)
{
printf("%d. ",a[i]);
}
printf("\n");
return 0;
}
输出:
0. 1. 2. 3. 4.
0. 1. 3. 3. 4.
数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的,在编译器内部都是用指针方式来访问数组元素的。数组下标方式只是编译器提供给编程者一种壳(语法糖)而已,所以用指针方式来访问数组才是本质的做法。
2.2 从内存角度理解指针访问数组的实质
数组的特点就是:数组中各个元素的地址是连续的,而且数组还有一个很大的特点,这其实也是数组的一个限制:就是数组中各个元素的类型必须相同。类型相同就决定了每个数组元素占几个字节是相同的,比如int
数组中每个元素都占4字节,没有例外。这两个特点就决定了只要知道数组中一个元素的地址,就可以很容易推算出其他元素的地址。
2.3 指针和数组类型的匹配问题
int a[5];
int *p;
p = a; // 类型匹配
p = &a; // 类型不匹配。p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配
&a
、a
、&a[0]
从数值上来看是完全相等的,但是意义来看就不同了。从意义上来看,a
和&a[0]
是数组首元素首地址,而&a
是整个数组的首地址;从类型来看,a
和&a[0]
是元素的指针,也就是int *
类型;而&a
是数组指针,是int (*)[5];
类型。
2.4 指针类型决定指针如何参与运算
指针参与运算时,因为指针变量本身存储的数值是表示地址的,所以运算也是地址的运算。
指针参与运算的特点是:指针变量+n,并不是真的地址加n,而是地址加上n*sizeof(指针指向的类型)
。
比如int *
指针,+1就实际表示地址+4,如果是char *
指针,则+1就表示地址+1;如果是double *
指针,则+1就表示地址+8。指针变量+1时实际不是加1而是加1×sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素,而不希望错位。
2.5 数组在函数传参时的表现
函数传参的时候形参是可以用数组的,但是函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址,也就是说函数传参用数组来传,实际相当于传递的是指针,该指针指向数组的首元素首地址。这里func(b)
中的b
等价于作右值,实际传递的就只有一个指针,所以这里的func()
其实等价于下面func2()
的写法。在一些需要传递数组的函数中,经常看到的写法是void func(int*a,int n);
,其中n
就是数组的大小,这是因为数组大小是不能直接通过传递数组来传递的,必须额外单独传递。
举个栗子:
#include<stdio.h>
void func(int b[])
{
printf("in func,sizeof(b) = %d.\n",sizeof(b));
}
void func2(int* b)
{
printf("in func,sizeof(b) = %d.\n",sizeof(b));
}
int main(int argc,char**argv)
{
int b[100];
func(b);
return 0;
}
输出:
in func,sizeof(b) = 8.
3.指针、数组与sizeof运算符
3.1 sizeof运算符的使用
sizeof
是C
语言的一个运算符,它不是函数,虽然用法很像函数。因为在不同平台下各种数据类型所占的内存字节数不尽相同,比如int
类型在32位系统中为4字节,在16位系统中为2字节。所以程序中可以使用sizeof
来判断变量或者数据类型在当前环境下占几个字节。
举个栗子:
#include<stdio.h>
#include<string.h>
int main(int argc,char**argv)
{
char str[] = "hello";
printf("size of str = %d.\n",sizeof(str));
printf("size of str[0] = %d.\n",sizeof(str[0]));
printf("strlen of str = %d.\n",strlen(str) );
char *p=str;
printf("sizeof(p) = %d.\n",sizeof(p));
printf("sizeof(*p) = %d.\n",sizeof(*p));
printf("strlen(p) = %d.\n", strlen(p));
return 0;
}
输出:
size of str = 6.
size of str[0] = 1.
strlen of str = 5.
sizeof(p) = 8.
sizeof(*p) = 1.
strlen(p) = 5.
32位系统中所有指针占4个字节,64位系统中所有指针占8个字节,不管是什么类型的指针。
strlen()
是一个C
库函数,用来返回一个字符串的长度,字符串的长度是不计算字符串末尾的'\0'
的。
3.2 sizeof使用的一些细节
1.
sizeof
测试一个变量本身,和sizeof
测试这个变量的类型,结果是一样的。
举个栗子:
#include<stdio.h>
int main(int argc,char**argv)
{
int n=10;
printf("sizeof(n) = %d.\n",sizeof(n));//等价于sizeof(int)
printf("sizeof(int) = %d.\n",sizeof(int));
return 0;
}
输出:
sizeof(n) = 4.
sizeof(int) = 4.
inf func,sizeof(b) = 8.
2.
sizeof(数组名)
的时候,数组名不做左值也不做右值,纯粹就是数组名的含义。那么sizeof(数组名)
实际返回的是整个数组所占用以字节为单位的内存空间。
举个栗子:
#include<stdio.h>
int main(int argc,char**argv)
{
int b[100];
printf("sizeof(b) = %d.\n",sizeof(b));
return 0;
}
输出:
sizeof(b) = 400.
3.指针定义与
define
和typedef
结合的时候,定义的类型结果不一样,sizeof
结果自然也不一样。
#include<stdio.h>
#define dpChar char*
typedef char* tpChar;
int main(int argc,char**argv)
{
dpChar p1,p2; //等价于char* p1,p2; p1是char*类型,p2是char类型
tpChar p3,p4; //等价于char* p1;char* p2; p1,p2都是char*类型
return 0;
}
4.数组指针与指针数组
4.1 指针数组与数组指针
- 指针数组的实质是一个数组,这个数组中存储的内容全部是指针变量。
- 数组指针的实质是一个指针,这个指针指向的是一个数组。
4.2 分析指针数组与数组指针的表达式
在定义一个符号时关键在于:
- 首先要搞清楚定义的符号是谁,即第一步:找核心;
- 其次再来看谁跟核心最近、谁跟核心优先结合,即第二步:找结合;
- 以后继续向外扩展,即第三步:继续向外结合直到整个符号完。
如何核心和*
结合,表示核心是指针;如果核心和[]
结合,表示核心是数组;如果核心和()
结合,表示核心是函数。
下面用一般规律来分析下这3个符号:
int *p[5];
int (*p)[5];
int *(p[5]);
- 对于第一个表达式来说,核心是
p
,它的旁边有*
和[]
,那么接下来的问题就是看这两个符号谁的优先级高。在C
语言中[]
优先级高于*
,于是p
是一个数组,数组中有5个元素,并且每个元素都是指向int
类型的指针。总之,整个符号是一个指针数组。 - 对于第二个表达式来说,核心是
p
,这里()
优先级更高,所以p
优先和*
结合,因此它首先是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int
类型。总结一下整个符号的意义就是数组指针。 - 对于第三个表达式来说, 解析方法和结论和第一个相同,因为
[]
优先级高于*
,所以()
在这里是可有可无的。
注意:符号的优先级决定了当两个符号一起作用的时候决定哪个符号先运算,哪个符号后运算。遇到优先级问题记得住更好,记不住就去查优先级表。
4.3 总结
- 1.优先级和结合性是分析符号意义的关键,在分析
C
语言问题时不要胡乱去猜测规律,从已知的规律出发按照既定的规则去做即可。 - 2.学会逐层剥离的分析方法,找到核心后从内到外逐层的进行结合,结合之后可以把已经结合的部分当成一个整体,再去和整体外面的继续进行结合。
- 3.基础理论和原则是关键,没有无缘无故的规则。