文章向导
深入探索指针与数组
数组指针与指针数组
一、深入探索指针与数组
1.指针的运算
~~~~ ~~~ 指针是一种特殊的变量,在编程工作中往往会用到:指针与整数进行运算,以及指针间的运算和比较。接下来逐个分析这几项问题。
1) 指针与整数进行运算
p + n = (unsigned int)p + n*sizeof(*p);
~~~~ ~~~ 上式为指针与整数的运算规则,其中(unsigned int)p代表指针在系统内部的地址,而n*sizeof(*p)则代表增加或减少的字节数。
2) 指针与指针之间的运算
~~~~
~~~
首先必须明确的是指针之间只支持减法运算,且参与运算的指针类型必须相同。另外,只有当两个指针都指向同一数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差值。具体的运算公式如下:
(
P
1
,
P
2
为
两
个
给
定
指
向
的
指
针
)
。
(P_1,P_2为两个给定指向的指针)。
(P1,P2为两个给定指向的指针)。
P
1
−
P
2
⇔
(
(
u
n
s
i
g
n
e
d
i
n
t
)
P
1
−
(
u
n
s
i
g
n
e
d
i
n
t
)
P
2
)
/
s
i
z
e
o
f
(
p
o
i
n
t
e
r
_
t
y
p
e
)
P_1\,\,-\,\,P_2\,\,\Leftrightarrow \,\,\left( \left( unsigned\,\,int \right) P_1\,\,-\,\,\left( unsigned\,\,int \right) P_2 \right) /sizeof\left( pointer\_type \right)
P1−P2⇔((unsignedint)P1−(unsignedint)P2)/sizeof(pointer_type)
看到这儿,某些读者可能回想如果我非要进行乘除运算或者用不同类型间的指针参与运算,那会发生什么?动手试验一下便知,下面已经列出了一部分可能会操作到的指针运算。
#include <stdio.h>
int main()
{
char s1[] = {'H', 'e', 'l', 'l', 'o'};
int i = 0;
char s2[] = {'W', 'o', 'r', 'l', 'd'};
char* p0 = s1;
char* p1 = &s1[3];
char* p2 = s2;
int* p = &i;
printf("%d\n", p0 - p1); //元素下标差,故结果为-3
printf("%d\n", p0 + p2); //error,指针之间只支持减法运算
printf("%d\n", p0 - p2); //无意义
printf("%d\n", p0 - p); //error,指针类型不同
printf("%d\n", p0 * p2); //error
printf("%d\n", p0 / p2); //error
return 0;
}
3) 指针的比较
~~~~ ~~~ 指针的比较,即对指针使用关系运算符(<, > ,== 等)。此时一般是将指针应用于某种特定的场合中,比如指针遍历数组。下面已经给出了关系运算符在指针使用中的一个经典例子:
#include <stdio.h>
#define DIM(a) (sizeof(a) / sizeof(*a))
int main()
{
char s[] = {'H', 'e', 'l', 'l', 'o'};
char* pBegin = s;
/* Key point:指向数组 s[4]的后一个位置,仍有意义,pEnd 相当于指向 &a+1;其中&a 为数组的地址,指示长度为整个数组。*/
char* pEnd = s + DIM(s);
char* p = NULL;
printf("pBegin = %p\n", pBegin);
printf("pEnd = %p\n", pEnd);
printf("Size: %d\n", pEnd - pBegin); //5
/*指针遍历数组*/
for (p=pBegin; p<pEnd; p++) {
printf("%c", *p); //Hello
}
printf("\n");
return 0;
}
//程序结果:
pBegin = 0x7ffc273ffc30
pEnd = 0x7ffc273ffc35
Size: 5
Hello
2. 数组的两种访问方式
~~~~
~~~
在使用数组时我们经常会碰到两种访问数组的方式:1)数组下标形式;2)指针形式。两种访问形式可以互相转化,但效率上后者优于前者。
a
[
n
]
⇔
∗
(
a
+
n
)
⇔
∗
(
n
+
a
)
⇔
n
[
a
]
a\left[ n \right] \Leftrightarrow *\left( a+n \right) \Leftrightarrow *\left( n+a \right) \Leftrightarrow n\left[ a \right]
a[n]⇔∗(a+n)⇔∗(n+a)⇔n[a]
上式即为两种方式的等价形式,理解上可能会有些抽象,所以还是根据一个实际的例子来继续加深理解吧。
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = a;
int i = 0;
for (i=0; i<5; i++) {
p[i] = i + 1; //
}
for (i=0; i<5; i++) {
printf("a[%d] = %d\n", i, *(a + i)); //输出数组a首次改变后的值
}
printf("\n");
for (i=0; i<5; i++) {
i[a] = i + 10; // 《==》 a[i] = i +10
}
for (i=0; i<5; i++) {
printf("p[%d] = %d\n", i, p[i]); //输出数组a第二次改变后的值
}
return 0;
}
3.深入探索数组名与指针的关系
数组名可视为常量指针(即指针的值(存放在指针中的那个地址)不可修改,但地址处所存储的内容可以修改)。
容易与其相混淆的一个概念则为指针常量(指向常量的指针)(即地址处所存储的内容为常量不可修改,但指针的指向却可以修改)。
虽然数组名可视为常量指针,可本质上数组与指针是两个不同的事物。现通过下面一个例子,来细化两者的差别:
/*main.c*/
#include <stdio.h>
extern int print();
int main()
{
extern int *a; /*数组名, 指针?*/
printf("In main: &a = %p\n", &a); //0x601040
print(); //0x601040
printf("a = %p\n", a); /* 取出的内容与32机或64位机有关 */
printf("*a = %d\n", *a); /*segment fault, 访问低地址空间*/
return 0;
}
/*extern.c*/
#include <stdio.h>
int a[] = {1, 2, 3, 4, 5};
int print()
{
printf("In extern: &a = %p\n", &a);
return 0;
}
~~~~
~~~
从上面的例子中可看出,在main.c中外部声明了一指针变量a,而a为在extern.c文件中定义的数组名,可视为常量指针。听起来有点绕,好像逻辑上也存在一点问题,但我们可以先来观察下程序运行的具体表现。
测试结果:
首先,前两条打印语句的结果是在意料之中的,但后两条的打印结果却让人匪夷所思,不仅结果奇奇怪怪而且程序竟然还出现了段错误。现在我们把视角集中于printf(“a = %p\n”, a); 这条语句,该语句表示按照指针的方式来解析a,而指针的地址在32位机上占用4个字节,在64位机上则占用8个字节(笔者测试环境为64为机)。
于是,编译器按照转换说明符%p的意义,取出a存储的数值(地址为8个字节,就去数组a中取出8个字节的内容作为地址)作为打印结果,具体可以按照下图理解:
由上图可知,取出来的前8个字节排列后正好就与第三条打印语句的结果相吻合。另外,在分析取出的字节数据时应注意系统的大小端问题不清楚的读者点此。
显然,图中呈现的是小端系统,而笔者测试的linux系统也为小端系统。为了说明,数组中的数据在内存中真的就是如此排列的,读者准备了如下的小case来说服哪些依然保持怀疑态度的读者。
最后,来谈谈printf("*a = %d\n", a); 语句为何产生段错误。既然数组a存放的是低地址,那么a则表示试图访问低地址空间的内容,一般在操作系统中这是不允许的,自然也就是产生了段错误。
二、数组指针与指针数组
1.数组类型与数组指针
1)重命名数组类型
~~~~
~~~
当使用一个数组比如int array[10]的时候,有时会考虑到数组的类型是什么,数组的类型由元素类型和数组大小共同决定,即仅需去掉数组名则可到数组类型为int[10]。
使用数组类型的目的是为了方便定义数组指针,首先我们可以通过typedef关键字来为数组类型重命名,如typedef int(AINT10)[10]; 这样AINT10就是数组类型int[10]的别名。从而可通过别名AINT10定义任意符合该类型的数组。
2)数组指针(用于指向一个数组)
使用数组类型定义数组指针的标准格式为:ArrayType* pointer,当然也可以采用直接定义的方式:type(*pointer)[n],其中pointer为指针变量,type为指向的数组元素的类型,n为指向的数组的大小。
下面的实例完整地演示了数组类型的用法,以及数组指针使用时的一些特性:
#include<stdio.h>
/*数组类型*/
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];
int main()
{
AINT5 a1; //等价于 int a1[5]
float fArray[10];
AFLOAT10 *pf = &fArray; //使用数组类型来定义数组指针,等价于 float(*pf)[10]
ACHAR9 cArray; //等价于 char cArray[9]
char(*pc)[9] = &cArray; //直接定义数组指针
char(*pcw)[4] = cArray; //warning: 指针类型赋值不匹配
int i = 0;
printf("sizeof(AINT5) = %d, sizeof(a1) = %d\n", sizeof(AINT5), sizeof(a1)); //20 20
for (i=0; i<10; i++) {
(*pf)[i] = i; //等价于 fArray[i] = i;
}
for (i=0; i<10; i++) {
printf("%f\n",fArray[i]);
}
//pc+1 = (unsigned int)pc + sizeof(*pc) = (unsigned int)pc + 9
//pcw+1 = (unsigned int)pcw + sizeof(*pc) = (unsigned int)pc + 4
printf("%p, %p, %p\n", &cArray, pc+1, pcw+1);
return 0;
}
程序运行结果:
2.指针数组(本质为数组)
指针数组其实就只是一个普通的数组,只不过其中的每一个元素都为指针。形如type* pArray[n]则为指针数组的定义,其中type*为数组中每个元素的类型,pArray为数组名,n为数组大小。简单来说,数组包含了n个指针变量p[0],p[1],…,p[n-1]。
知道了指针数组的定义后,接下来则是考虑其实际的用途。
#include<stdio.h>
#include<string.h>
#define DIM(a) (sizeof(a)/sizeof(*a))
int lookup_keyword(const char* key, const char* table[], const int size)
{
int ret = -1;
int i = 0;
for (i=0; i<size; i++) {
if (strcmp(key, table[i]) == 0) {
ret = i;
break;
}
}
return ret;
}
int main()
{
const char* keyword[] = { //数组中每个元素的类型为 const char*
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
"case",
"static"
};
printf("%d\n", lookup_keyword("register", keyword, DIM(keyword))); //3
printf("%d\n", lookup_keyword("main", keyword, DIM(keyword))); //-1
return 0;
}
~~~~ ~~~ 上述程序中定义的指针数组,其中每个元素都为字符串(char*指针)。然后利用这一特性,则可使用诸如strcmp之类的字符串系列函数。
参阅资料
狄泰软件学院—C进阶剖析教程
C primer plus
高质量嵌入式Linux C编程
你必须知道的495个C语言问题