在考虑指针的问题之前,首先我们来回顾一下如何定义一个变量。

1
int x;

int 类型需要四个字节来存储,这四个字节是从内存中分配而来的,内存在逻辑上是一个线性的存储器,它以字节为单位被划分为许多小的单元。为了区分这些单元,计算机给每一个单元都分配了一个唯一的十六进制整数作为编号,叫做内存地址 (简称地址)。由于这些单元啊数量众多,所以这个内存地址本身就需要四个字节 (32 位 bit 位) 来表示。我们所说的 32 位系统就是这个意思。它最大的可寻址范围为 2^32 字节,即 4GB。目前正在占据主流的 64 位系统的最大可寻址范围是 2^64 字节。

理论上说,我们只需要这些内存地址就可以定位到我们所要读写的内存单元,但这些地址不方便理解和记忆,c 语言允许程序员给这些地址起名字,就是我们所说的变量名。

回到 int x 这个声明,它实际上做了两件事:

  1. 从内存中划分出 4 个字节;
  2. 将这个内存单元约定一个名字为 x;

一个固定的内存单元的地址是固定的,但存储的值确是可变的。

指针概念

获取变量的地址

由于有变量名的存在,我们不需要一个具体地址就可以操作它来进行各种运算,但有的时候需要得到其地址,为了满足其需要,c 语言提供了取地址运算符 &

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(void){
int x = 5;
printf("value is % d\n", x);
printf("address is % d\n", &x);
return 0;
}
// 5;
// 2293620

int 变量需要四个字节,每个内存单元只有一个字节的容量,所以程序会分配四个连续的单元给 x。&x 就是第一个单元的地址,即 首地址 。我们可以用一个无符号整型变量来存储它。

1
unsigned int addr = (int)&x;

C 语言处于类型安全的考虑,认为一个地址与一个整数不等价(虽然实际上是一回事),所以想把变量 x 的地址存在一个 int 中,需要使用强制转化。

上面的代码中 addr 也是一个整型变量,它也有自己的地址,那么也可以通过 & 来获取它的地址。

1
unsigned int addr_of_addr = (int)&addr;

那么这样是否可以

1
unsigned int addr_of_addr = (int)&&x; // 编译错误 

为什么会编译报错呢,这是因为取一个变量的地址后,得到的是一个数值,而不是一个变量,取地址只对于变量才有意义。所以只有将这个数值存放于一个变量 addr 中,才能取得这个变量 addr 的地址。

指针变量

由于使用 unsigned int 存储地址会导致无法根据地址得到相应内存单元的值,即取内容,所以 C 语言中用指针变量来存储地址。

1
int* p = &x;

如果要声明一个只想 int 变量的指针是,就使用变量之前加一个 * 来说明这是个指针变量。

如果要定义多个指针,则必须每个变量名前都加星号 *。对一个指针变量前面加上 * 代表取指针指向的地址内存储的变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(){
int x = 5;
int * p = &x;
printf("x = % d\n", x); //5
printf("add_x = % d\n" , &x); //-345631352
*p = 6;
printf("x = % d\n", x); //6
printf("add_x = % d\n" , &x); //-345631352
return 0;
}

上面的代码可以看出,修改 *p 也会直接修改 x 的值,因为这是对 p 直接指向的内存单元进行操作。

如果对指针变量进行加减一个整数,称之为指针的移动。

p+n 等价于 (TYPE*)(int) p+n*sizeof (TYPE)

一个指针加减一个整数 n,存储的地址将会加上 n 倍所指向类型的大小。

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
double val=5.0;
double *p = &val;
printf("d%\n", p); //12344426
printf("d%\n", p + 1); //12344434 增加了 sizeof (double)==8
}

指针类型

指针的类型的含义是: 指针变量的本身的类型

上文我们已经知道了指针变量存储的是一个地址,这个地址是 32 位或者 64 位,取决于操作系统。

那么可以得出结论,一个操作系统中,所有的指针变量的大小都是相等的。

从语法的角度看,你只要 把指针声明语句里的指针名字去掉 ,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:

1
2
3
4
5
int* ptr;// 指针的类型是 int*
char* ptr;// 指针的类型是 char*
int** ptr;// 指针的类型是 int**
int(*ptr)[3];// 指针的类型是 int (*)[3]
int*(*ptr)[4];// 指针的类型是 int*(*)[4]

指针所指向的类型

当通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

从语法上看,只须把指针声明语句中的指针名字和名字左边的指针声明符 * 去掉,剩下的就是指针所指向的类型。例如:

1
2
3
4
5
int*ptr; // 指针所指向的类型是 int
char*ptr; // 指针所指向的的类型是 char
int**ptr; // 指针所指向的的类型是 int*
int(*ptr)[3]; // 指针所指向的的类型是 int ()[3]
int*(*ptr)[4]; // 指针所指向的的类型是 int*()[4]

在指针的算术运算中,指针所指向的类型有很大的作用。

指针的类型 (即指针本身的类型) 和指针所指向的类型是两个概念 。分清这两个概念是理解指针的关键点之一。

指针的值

指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在 32 位程序里,所有类型的指针的值都是一个 32 位整数,因为 32 位程序里内存地址全都是 32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为 sizeof (指针所指向的类型) 的一片内存区。以后,我们说一个指针的值是 XX,就相当于说该指针指向了以 XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。

指针自身占据的内存

因为指针本质上就是一个固定位数整数,在 32 位平台里,指针本身占据了 4 个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。

指针运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。例如:

1
2
3
char a [20];
int *ptr=(int *) a; // 强制类型转换并不会改变 a 的类型
ptr++;

在上例中,指针 ptr 的类型是 int*, 它指向的类型是 int,它被初始化为指向整型变量 a。接下来的第 3 句中,指针 ptr 被加了 1,编译器是这样处理的:它把指针 ptr 的值加上了 sizeof (int),在 32 位程序中,是被加上了 4,因为在 32 位程序中,int 占 4 个字节。由于地址是用字节做单位的,故 ptr 所指向的地址由原来的变量 a 的地址向高地址方向增加了 4 个字节。由于 char 类型的长度是一个字节,所以,原来 ptr 是指向数组 a 的第 0 号单元开始的四个字节,此时指向了数组 a 中从第 4 号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main(void)
{
int array[20] = {0};
int *ptr = array;
for (int i = 0; i < 20; i++)
{
(*ptr)++;
ptr++;
}
for (int i = 0; i < 20; i++)
{
printf("% d ", array[i]);
}
return 0;
}

这个例子将整型数组中各个单元的值加 1。由于每次循环都将指针 ptr 加 1 个单元,所以每次循环都能访问数组的下一个单元。

指针表达式

一个表达式的结果如果是一个指针,那么这个表达式就叫指针表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 int a, b;
int array[10];
int *pa;
pa = &a; //&a 是一个指针表达式。
int **ptr = &pa; //&pa 也是一个指针表达式。
*ptr = &b; //*ptr 和 & amp;b 都是指针表达式。
pa = array;
pa++; // 这也是指针表达式。
char *arr [20];
char **parr = arr; // 如果把 arr 看作指针的话,arr 也是指针表达式
char *str;
str = *parr; //*parr 是指针表达式
str = *(parr + 1); //*(parr+1) 是指针表达式
str = *(parr + 2); //*(parr+2) 是指针表达式

由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。

当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。

数组和指针

数组的数组名可以看作一个指针

1
2
3
4
5
6
7
8
char *str [3] = {
"Hello,thisisasample!",
"Hi,goodmorning.",
"Helloworld"};
char s [80];
strcpy(s, str [0]); // 也可写成 strcpy (s,*str);
strcpy(s, str [1]); // 也可写成 strcpy (s,*(str+1));
strcpy(s, str [2]); // 也可写成 strcpy (s,*(str+2));

上例中,str 是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名 str 当作一个指针的话,它指向数组的第 0 号单元,它的类型是 char *,它指向的类型是 char

str 也是一个指针,它的类型是 char ,它所指向的类型是 char,它指向的地址是字符串 “Hello,thisisasample!” 的第一个字符的地址,即 ‘H’ 的地址。注意:字符串相当于是一个数组,在内存中以数组的形式储存,只不过字符串是一个数组常量,内容不可改变,且只能是右值。如果看成指针的话,他即是常量指针,也是指针常量.

str+1 也是一个指针,它指向数组的第 1 号单元,它的类型是 char*,它指向的类型是 char

(str+1) 也是一个指针,它的类型是 char,它所指向的类型是 char,它指向 “Hi,goodmorning.” 的第一个字符’H’

1
2
3
int array[10];
int(*ptr)[10];
ptr = &array;

上例中 ptr 是一个指针,它的类型是 int (*)[10],他指向的类型是 int [10] ,我们用整个数组的首地址来初始化它。在语句 ptr=&array 中,array 代表数组本身。

本节中提到了函数 sizeof (),那么我来问一问,sizeof (指针名称) 测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?

答案是前者。例如:

1
int(*ptr)[10];

则在 32 位程序中,有:

1
2
3
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

实际上,sizeof (对象) 测出的都是对象自身的类型的大小,而不是别的什么类型的大小。

指针和结构类型的关系

所有的 C/C++ 编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个 “填充字节”,这就导致各个成员之间可能会有若干个字节的空隙。

指针和函数的关系

1
2
3
4
int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a = (*pfun1)("abcdefg",7); // 通过函数指针调用函数。

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。

总结:

  1. 其实,myFun 的函数名与 funP、funA 函数指针都是一样的,即都是函数指针。myFun 函数名是一个函数指针常量,而 funP、funA 是函数数指针变量,这是它们的关系。

  2. 但函数名调用如果都得如 (*myFun)(10) 这样,那书写与读起来都是不方便和不习惯的。所以 C 语言的设计者们才会设计成又可允许 myFun (10) 这种形式地调用(这样方便多了,并与数学中的函数形式一样)。

  3. 为了统一调用方式,funP 函数指针变量也可以 funP (10) 的形式来调用。

  4. 赋值时,可以写成 funP=&myFun 形式,也可以写成 funP=myFun。

  5. 但是在声明时,void myFun (int ) 不能写成 void (myFun)(int )。void (funP)(int ) 不能写成 void funP (int )。

  6. 函数指针变量也可以存入一个数组内。数组的声明方法:int (*fArray [10]) ( int );

附录 - 指针类型判断实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int p; // 这是一个普通的整型变量 
int *p; // 首先从 P 处开始,先与 * 结合,所以说明 P 是一个指针,然后再与 int 结合,说明指针所指向的内容的类型为 int 型。所以 P 是一个返回整型数据的指针

int p [3]; // 首先从 P 处开始,先与 [] 结合,说明 P 是一个数组,然后与 int 结合,说明数组里的元素是整型的,所以 P 是一个由整型数据组成的数组

int *p [3]; // 首先从 P 处开始,先与 [] 结合,因为其优先级比 * 高,所以 P 是一个数组,然后再与 * 结合,说明数组里的元素是指针类型,然后再与 int 结合,说明指针所指向的内容的类型是整型的,所以 P 是一个由返回整型数据的指针所组成的数组

int (*p)[3]; // 首先从 P 处开始,先与 * 结合,说明 P 是一个指针然后再与 [] 结合 (与 & quot;()" 这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组,然后再与 int 结合,说明数组里的元素是整型的。所以 P 是一个指向由整型数据组成的数组的指针

int **p; // 首先从 P 开始,先与 * 结合,说是 P 是一个指针,然后再与 * 结合,说明指针所指向的元素是指针,然后再与 int 结合,说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.

int p(int); // 从 P 处起,先与 () 结合,说明 P 是一个函数,然后进入 () 里分析,说明该函数有一个整型变量的参数,然后再与外面的 int 结合,说明函数的返回值是一个整型数据

int (*p)(int); // 从 P 处开始,先与指针结合,说明 P 是一个指针,然后与 () 结合,说明指针指向的是一个函数,然后再与 () 里的 int 结合,说明函数有一个 int 型的参数,再与最外层的 int 结合,说明函数的返回类型是整型,所以 P 是一个指向有一个整型参数且返回类型为整型的函数的指针

int *(*p (int))[3]; // 可以先跳过,不看这个类型,过于复杂从 P 开始,先与 () 结合,说明 P 是一个函数,然后进入 () 里面,与 int 结合,说明函数有一个整型变量参数,然后再与外面的 * 结合,说明函数返回的是一个指针,,然后到最外面一层,先与 [] 结合,说明返回的指针指向的是一个数组,然后再与 * 结合,说明数组里的元素是指针,然后再与 int 结合,说明指针指向的内容是整型数据。所以 P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数