抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

前提:为什么要写这篇文章

指针是C语言的一大特色,也是C语言的重要组成部分。

我在学习指针的时候,发现大家对于指针的理解和说法都不一样,有些人的讲解甚至有错,导致同学们对指针的理解在根本上就产生了偏差或者错误,影响了后期的学习。

这里,我希望写一篇文章,来让初学者对指针有个基本的认识,仅此而已。

废话不多说,我们开始!


Part 1:指针的定义

如果你现在对于指针的理解还是一团糟,我希望你能够忘掉你之前学的所有有关指针的东西,听听我的描述。

指针是地址,储存他的变量称为指针变量

蒙了吗?我们从变量的储存来来一看这句话到底是什么意思。

众所周知,我们的变量,是要储存在计算机里的。

那么,我们要储存在什么地方呢?简单的理解,我们可以说他储存在内存里。

举个例子。定义int a时,编译器分配4个字节内存,并命名该4个字节的空间名字为a(即变量名)。

当用到变量名a时,就是在使用那4个字节的内存空间。

Tips:关于指针和指针变量
其实我们自己都没有意识到一点:我们在谈到指针的时候,绝大多数情况都指的是指针变量而非地址。
所以本文如果没有特殊说明,所有的指针都成为指针变量

所以讲道理,变量名只是一个符号,他并不能告诉我们这个变量到底储存在哪里,如果我们需要直接对这个变量进行操作,我们必须要知道这个变量背后所代替的内存到底是那一块。

这时候,大名鼎鼎的取址符&就登场了。

我们可以用&+变量名来获取一个变量的地址。

那么我们总得把这个地址存下来吧?

3+2int a = 3+2存,3.2+2.2double p = 3.2+2.2存,那你&a自然也得找个地方存,找谁呢?找指针存。

指针的定义和一个变量的定义只有一个不同。一个指针的定义应该是类型名 *变量名
例如:

1
2
int *point;
double *dou;

Tips:关于指针的定义写法
有的资料书上的写法是int* a,有的写法是int *a
个人认为,两个写法均可。他们都代表a是一个int类型的指针
但要搞清楚,如果出现下面的语句:

1
int* p1, p2

这时候,p2不是int*,而是int类型

那他的赋值应该是怎样的呢?也是一样,一个指针所储存的变量地址,取决于他的类型名。int*的指针只能指向int类型的变量,doube*的指针只能指向double类型的变量。

例如:

1
2
3
int *a;
int b = 20;
a = &b; // 一定要注意!直接使用b提取出的是b的值,而使用&b提取出的是变量b的地址

Part 2:指针的使用

指针的使用和一个普通的变量没有多大的差别,我们这里额外的提一个运算符*,他的作用是提取抽一个指针所指向地址的值。

艾玛,好拗口,到底是什么意思呢?我们来类比一下:

假设我们有int a = 1,如果你直接使用a,意思就是使用a所储存的值,即为1;
假设我们有int *p = &a,如果直接使用p,意思就是使用p所储存的值,即为a的地址

Tips:我们一般把地址看做一个 代表内存地址的长的十六进制数

指针的“骚操作”来了,如果你尝试用%d输出*p,会发先:结果为1!

这事情是怎么发生的呢?你可以这么理解:

p的值即为p所指向的地址,也就是&a

*又是提取抽一个指针所指向地址的值,相当于提取出&a的值,相当于提取出a的值,也就是1

通过*运算符,我们可以很快乐的对变量进行“远程操控”,例如:

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

void swap(int a, int b) {
int c;
c = a;
a = b;
b = c;
}
int main() {
int a = 5, b = 10;
swap(a, b);
printf("a = %d, b = %d", a, b);
return 0;
}

这样子的交换函数看起来貌似好像大概应该也许可能没问题,但你一看输出傻了眼:

a = 5, b = 10

为什么会这样?当你在调用swap(a, b)的时候,你干的其实和这句话没什么区别:swap(5, 10)

为什么没区别?我们前面说过,当你调用a的时候,和调用a的值没有区别。这就导致你的swap换来换去,换的只是个值,并没有对main函数里的变量进行操控。

正确的写法应该是:

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

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

这回,我们可以在swap函数中根据传入的参数,直接操控main函数里的ab了。


Part 3:空指针和野指针

一些同学怀着激动地心写下了如下代码:

1
2
int* p;
printf("%p",p);

哦我的老天爷,你知道p指向的是什么吗?

不知道?不知道你还敢这么写!

当你没有让p指向某个地方的时候,你还把他用了!这个时候就会产生野指针。
野指针的危害是什么?
第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;

第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;

第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。

不论如何,我们都不希望看到这些发生。
于是,养成好习惯:变量先赋值。

指针你可以这么做:int *p =NULL;让指针指向空。NULL是个宏,它相当于(void *)0void *是个比较特殊的指针类型,他可以强制转化为任意一种类型的指针。


Part 4:指针与数组&字符串的梦幻联动

我们用*可以对指针指向地址的值进行提取,那么这个值自然也可以运算,例如:

1
2
3
int a = 5;
int *p = &a;
(*p) ++; // a的值++

Tips:关于这个括号
我个人是习惯打个括号的,当然,你也可以不打
不打就一定要注意运算符的优先级问题!

指针还可以和数组梦幻联动,例如用指针遍历整个数组:

1
2
3
4
5
6
int  var[] = {10, 100, 200};
int i, *ptr;
ptr = var;
for ( i = 0; i < MAX; i++){
ptr++;
}

这里有两句话很核心,一是ptr = var,二是ptr ++

问题来了:为什么ptr = var而不是ptr = &var呢?

答案:因为数组名本身就是数组首元素的地址

Tips: 虽然数组名本身就是数组首元素的地址,但是数组和指针绝不等价!

数组在存储的时候,每一个元素都是紧密相连的。这样我们可以只需要通过移动指针指向的位置就可以知道下一个元素的值。

那么怎么移动呢?ptr ++就是干这活的。

++对于指针来说,可以理解为移动到下一个地址,如果把内存比作一个个小房间,你可以理解++的意思就是从房间502移动到房间503(假设是相邻的)。而数组正好就像一个长走廊,从1号房间到n号房间,他们都顺次排列,没有间隔。

同样,你也可以用--来回到上一个“房间”,用+ n来跳到后面的第n个房间。

不过请注意,如果是这样的情况:

1
2
3
4
int a;
int *p = &a;
p ++; // 危
printf("%d", *p); // 大危

这句话其实是很危险的。因为你也不知道p++会搞到那里去,从而诞生了野指针。

你要是能够理解数组名就是指向首元素的指针,那么你应该不难理解C风格字符串char *char []了——他们从意义上来说可以看做等价的——你要是出去跟别人说“char*就是char[]!”被打一顿你可别找我。字符串是依靠字符数组,char[]来存储的,不代表字符数组就是字符串!

哈?为啥?因为编译器眼中的字符串其实长这样:
|下标| 0 | 1 | 2 | 3 | 4 | 5 |
|–|–|–|–|–|–|–|–|–|–|
| char[6] | l | u|o| g|u|'\0'|
看到了吗?结尾还有一个空字符\0来表示这个字符串有结尾。而普普通通的一个字符数组是可以没有'\0'的哦!


Part 5:指针与函数的梦幻联动

我们上面简单的介绍了数组与指针的梦幻联动,现在我们让指针和函数在一起,看看又能有什么新花样。

Part 5.1:把指针作为函数的参数

这个我们其实前面讲到过,也就是swap函数:

1
2
3
4
5
6
void swap(int *a, int *b) {
int c;
c = *a;
*a = *b;
*b = c;
}

当然,我们前面说过,一个数组名就是指向首元素的指针,也就是说:

1
2
3
void avg(int *a);
int arr[5] = {0};
int *a = NULL;

我们调用avg(arr)或者avg(a)都是合法的!

Part 5.2:函数返回一个指针

我们来看菜鸟教程中的这一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int * getRandom(){
static int r[10];
int i;
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i){
r[i] = rand();
printf("%d\n", r[i] );
}
return r;
}
int main ()
{
int *p;
int i;
p = getRandom();
for ( i = 0; i < 10; i++ ) printf("*(p + [%d]) : %d\n", i, *(p + i) );
return 0;
}

在这里,getRandom()生成了一个由10个随机数组成的数组,并返回了一个int*指针作为这个数组的首元素地址,我们可以用Part 4里边讲到的方式去遍历这个数组。

问题来了,这里面最关键的一句话是:static int r[10];,再重点一点,是:static

有时候,我们希望函数中局部变量的值在函数调用结束之后不会消失(例如这里的r数组),在下一次调用该函数时,其局部变量的值仍然存在,也就是上一次函数调用结束时的值。这时候,我们就应该将该局部变量用关键字 static 声明为“静态局部变量”。他和全局变量一样,初始值都为0。

Part 5.3:函数指针

我们可以通过指针来调用一个变量,我们自然也可以试试函数吃不吃这套。

看下面这个经典的比大小:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
return a>b ? a : b;
}
int main(){
int x = 5, y = 10, maxval;
int (*p)(int, int) = max;
maxval = (*pmax)(x, y);
printf("Max value: %d\n", maxval);
return 0;
}

这里,int (*p)(int, int) = max;创建了一个指向函数的指针。他所指向的类型,可以看做一个返回值为int且有两个int类型作为参数的函数。

以此类推,double (*p)(double, char)所指向的类型,可以看做一个返回值为double且有一个double、一个char类型作为参数的函数。

至于他的用法,你后面就可以完全把(*p)当做一个函数的名字来用,后面直接跟空号加参数。

Tips:这里(*p)(int, int)的括号是必要的!不然会出运算符优先级的错误!


本文引用资料

评论