Fenrier Lab

C 语言指针详解

理解指针这一概念是学习 C 语言和 C++ 的重中之重,对于编程初学者,由于不熟悉计算机程序的运行原理,如果用 C 语言作为入门,将会在指针的理解上陷入泥潭。所以尽管现在的大学教育都纷纷将 C 语言作为通识课程,但是我还是认为这门语言并不适合刚接触编程的同学。

不过话又说回来,C 是一门非常值得掌握的语言,原因就在于它足够接近底层,它能让程序员对硬件有足够的掌控,却又不似汇编那样太过底层。

这篇文章的目的就是记录下个人对于指针的理解,并希望能对后人以启发帮助。

基本类型

首先看如下一段代码

//c1.c
#include <stdio.h>
int main() {
  int a = 1;
  printf("%d\n", a);
  printf("%d\n", &a);
}

运行的结果为:

1
6356748

这里的第二行结果可能会不一样,因为 &a 实际上取得的是变量 a 所在的内存地址,不同的机器在分配具体地址时当然会产生差异。

既然知道了 &a ,那么我们就很自然地想知道 *a 到底是个什么东西,于是就很想把它打印出来

#include <stdio.h>
int main() {
  int a = 1;
  printf("%d\n", *a);
}

但是这段代码并不能通过编译,会出现下面的错误信息:

error: invalid type argument of unary '*' (have 'int')

也就是说,这时候编译器把 * 当成了某个一元运算符,并不认识。教材讲的是定义

int *p;

使得 p 指向 int 类型。那么就会疑问 p 究竟是什么东西,于是又想把它打印出来。

#include <stdio.h>
int main() {
  int *p;
  printf("%d\n", p);
}

这段代码的结果是 49,一个很费解的数字,考虑到我们没有对其进行初始化,暂且假设这是系统随机分配的。问题还没完,请问 *p 是什么东西,之前的代码报错,这次是先声明了的,想看看结果怎样

#include <stdio.h>
int main() {
  int *p;
  printf("%d\n", *p);
}

这次倒是编译通过了,但很遗憾地程序崩溃了。难道是没有初始化的原因,于是按照很标准的方法初始化一次

#include <stdio.h>
int main() {
  int a = 10;
  int *p = &a;
  printf("%d\n", *p);
}

这次很顺利,打印出了 10,但是很好奇这时 p 又是什么?

#include <stdio.h>
int main() {
  int a = 10;
  int *p = &a;
  printf("%d\n", p);
}

这时打印出了 6356744,这显然是一个地址,而且就是变量 a 的地址。但这里的问题是我们是将整个 &a 也就是 6356744 赋值给 *p,按理说应该是 *p 等于 6356744 才对啊。可见编译器肯定在这条语句上面做了手脚,于是按下面的方法验证下

#include <stdio.h>
int main() {
  int a = 10;
  int *p;
  *p = 6356744;
  printf("%d\n", p);
}

又崩了,也就是说刚才的赋值语句 *p = &a 并没有把 6356744 传给 *p。既然之前打印出 p 等于 6356744,那我们就手动来赋值嘛

#include <stdio.h>
int main() {
  int a = 10;
  int *p;
  p = 6356744;
  printf("%d\n", p);
}

这时正常打印,但是我还是不知道 *p 是什么,为啥就能分开,还能对 p 单独赋值?还想再看看这时的 *p 是什么。

#include <stdio.h>
int main() {
  int a = 10;
  int *p;
  p = 6356744;
  printf("%d\n", *p);
}

这时打印出了 10,也就是说,语句 int *p = &a 并不是将地址 &a 赋值给 *p, * 号在这里的真正作用只是表明 p 为一个指针,而 = 号的作用则是将地址 &a 复制给 p,即等价于下面两条语句

int *p;
p = &a;

在使用指针的时候,则在其前面加一个 *p 号即可取出值,至于要读取多少位,这就要看具体的指针类型了。可以看出来,在声明指针和使用指针时 * 的作用其实是不一样的,这一点值得注意,也许这就是指针难理解的原因之一。

还有一个疑问是,为什么当 p = 49 时,打印 *p 会崩溃啊,按理说它应该从内存地址 49 的地方获得数据啊。我的猜测是操作系统限制了读取数据的内存段,如果超出了限制,就不让读了,可以通过多试几个地址看看,确实有这种意味在里面,可能还是考虑到安全的原因吧。

指针参数

将指针作为函数的参数是很普遍的做法,例如下面这段代码

int main(){
  void println(int *value);
  int a = 10;
  println(&a);
}
void println(int *value){
  printf("%d", *value);
}

有了前面的基础,我们可以这样理解,将 &a 作为参数传入,相当于在函数调用时进行了绑定 *value = &a ,即 value 是 a 的地址,*value 是这个地址里的值。如果要改变这个值,那么只需要对 *value 进行重新赋值就可以了,这样就实现了地址传递,即可以在函数里面修改外部变量值。这就不难理解下面的值交换函数了

int main() {
  int a = 10;
  int b = 20;
  swap(&a, &b);
  printf("%d,%d",a,b);
}
void swap(int *a, int *b) {
	int c = *a;
	*a = *b;
	*b = c;
}

数组指针

在讨论指针之前,先看一看数组的一些性质

//c1.c
int main() {
  int a[] = {1,2,3,4};
  printf("%d", a);
}

这里打印出了 6356736,也就是说 a 作为数组名称,其值实际上是一个内存地址,如果我们用一个 * 号来取出这个地址上的值

int main() {
  int a = {1,2,3,4};
  printf("%d", *a);
}

打印出了 1 ,这就说明数组名称指向了数组第一个元素的地址。这就感觉 a 是一个指针一样,只不过它指向的数组的第一个元素。为了访问后续的元素,我们可使用两种方法。

int main() {
  int a = {1,2,3,4};
  printf("%d", a[1]);
  printf("%d", *(a+1));
}

其中第一种是传统的通过数组索引访问,而第二种就厉害了,通过将 a 加上 1 ,我们得到了下一元素的地址,然后再前面加上 * 号,取出这个地址的值,即可得到所取元素。事实上,a+1 并非在数值上在 a 的基础上加 1,而是加上一个与数组类型相关的量。对于整数数组来说,由于一个整数占用 4 个字节,于是 a+1 在数值上应该是加上了 4 ,这可以验证。

看起来我好像是把数组名成当成指针在使用了,但是它们间还是有点点差别的,比如 a++ 就不能通过编译。

指向数组的指针和指向基本变量的指针也有些许不同。例如下面的代码

//c1.c
int main() {
  int a[] = {1,2,3,4};
  int *p;
  p = &a[0];
  printf("%d,%d,%d,%d", *p,*(p+1),*(p+2),*(p+3));
}

这将打印出 1,2,3,4。在给指针赋值的时候,没有用 p = &a 。因为 a 本身就是一个地址了,显然 p 并不是要指向 a 的地址,而是要指向 a[0] 的地址。当然也可以使用另一种方式

int *p;
p = a;

数组指针参数

int main() {
  void f(int *a);
  int a = {1,2,3,4};
  int *p;
  *p = a;
  f(a);
  f(&a[0]);
  f(p);
}

void f(int *a){
  printf("%d", *(a+1));
}

有了前面的基础,这点内容都很自然地能理解了。

本文遵守 CC-BY-NC-4.0 许可协议。

Creative Commons License

欢迎转载,转载需注明出处,且禁止用于商业目的。