指针这一部分在传统C语言中应该算是比较难理解的一部分了,这篇文章就来讨论一下C语言中指针的用法,顺便再说说C++中对指针的扩展的特性。
什么是指针?
在介绍指针之前,先推荐一个网站:cppreference,在这个网站里面能找到C和C++的所有标准和特性。不过我给的连接是英文的,在网站下方可以找到中文站的入口。不过阅读这些材料还是推荐阅读英文原版。
在本篇内容当中,所有C和C++一致的内容,均以C语言为例。
首先介绍一下指针,简单来说,指针就是保存的一个内存地址。我们都知道,C/C++的变量在运行时需要保存到栈内存中,如果我们创建一个变量b,来保存a这个变量的值所在的内存地址,那么我们就可以说b是a的指针。
指针怎么用?
在这里,我们需要介绍两个运算符,其分别是&和*。&符号在C语言中一般使用在变量名前面,其作用是取内存地址。也就是说,假如我们创建了一个变量int a=5,那么我们可以知道,a的值为5。如果我们在a前面加个&,则表示取a(也就是5这个值)在内存中的地址。也就是说,&a是一段内存地址,同样,我们也可以将其称为指针。
当然,我们可以把&a这个"内存地址"保存到一个变量中,这个变量我们称之为指针变量。指针变量的申请方式需要用到*符号,具体如:int *b,我们在申请完b这个指针变量时,就可以用它来保存&a了。下面用实际代码来进行说明:
1 2 3 4 5 6 7 8 | #include <stdio.h> int main() { int a = 5; // 申请一个变量a int *b; // 申请一个int型指针变量 b = &a; // 用b来保存a值所在地址 printf("%p\n", b); // 看看b是多少 } |
上面这段代码我们得到的运行结果为0x7fff535d85c4,顺便说一下,这段程序我是在菜鸟工具的在线C语言编辑器(下同)中运行的,使用的C11标准。
如果我们有一个指针变量(比如上面的b),想使用其值(对应的a的5),那么我们只需要使用*b即可。尝试运行下面这一段代码:
1 2 3 4 5 6 7 8 | #include <stdio.h> int main() { int a = 5; // 申请一个变量a int *b; // 申请一个int型指针变量 b = &a; // 用b来保存a值所在地址 printf("%d\n", *b); // 输出指针变量b指向的值 } |
运行结果为5。
指针的类型
在上面的示例代码中,申请指针变量时我提到了int型指针这个概念。
实际上,指针变量也是变量,它和普通变量类似,也有自己的类型。不过有的同学可能会想,指针变量里面保存的不都是"指针"吗?指针不应该都是一个64位地址(在64位平台及操作系统下)吗?没错,指针确实都是一个64位地址,所以实际上,在申请指针变量时所描述的类型并非指针变量值的类型,而是指针变量里面保存的地址的数据的类型。
这么说可能会有一些抽象,我们还是以上面那段代码为例进行说明。了解C语言变量类型的同学都知道,int类型的范围是0x00000000到0xffffffff(32位,也就是-2的31次方到2的31次方,或是无符号的0到2的32次方)。而我们上面给出的例子,在最后输出的结果为0x7fff535d85c4,直接给干到了48位,远远超过了int的范围。这是因为在申请指针变量b时的int描述的并非b的类型,因为b一定是一个地址(指针),其描述的实际上是b地址的值的类型,在该例中也就是a(值为5)的类型。
在使用指针变量时,如果我们不指定数据类型,那么系统也不会知道这个指针的地址应该是int还是char还是long。因为在内存中,数据都是连续的,所以如果类型出现错误,刚好遇上两个变量在内存中的地址相邻,就很可能覆盖带下一个变量的地址。所以我们在申请指针变量时指定了所在内存位置的值的类型,才能避免上述情况的发生。
同样的道理,再举个例子。long number = 1234;那么如果我们要为这个变量申请一个指针变量,并将number在内存中的地址的值赋给它,我们需要这样创建:long *ptr_number = &number;如果我们创建的指针变量是int *ptr_number = &number;那么我们在使用这个指针时,就会把number当作一个int型的变量来使用,在内存中就只会操作其中32位。
有一种比较特殊的指针类型:无类型指针(void *),因为这个类型的用法比较特殊,所以我们放在后面单独用一节来讲。
NULL指针
如果在创建指针变量时,没有值可以赋给它,那么我们可以将其赋值为NULL,也就是int *ptr = NULL;赋值为NULL后,其地址为0。一般情况下来说,操作系统会认为指针为0的变量是不指向任何值的指针,所以我们也可以在一个指针使用完毕后将其赋值为NULL。
void指针
在C语言(以及其他很多语言)中,void表示无类型,比如在声明函数时使用void fun();则表示该函数无返回值类型,同样,在指针中void *表示无类型指针。
在上面我们提到了指针是又类型的(比如int,char之类的,但实际上也存在一种没有类型的指针,也就是void指针。void指针可以接受任何类型的指针。比如:
1 2 3 4 5 | int a = 5; void *b = &a; char c = 15; b = &c; |
而在使用void指针时,需要先转换成相应的指针类型类型,比如:
1 2 3 4 5 | int d = 10; int *e; void *f = &d; e = (int *)f; |
C语言标准库中的内存分配函数malloc的返回值就是(void *),所以在调用这个函数时需要先转换成有类型的指针变量。
由于其特殊性,在对其进行使用时要特别小心,防止因类型错误出现问题。
指针运算操作
指针的内容本质上也是一个值(也可以理解成数字),所以,我们可以使用运算符对其进行运算操作。指针有四种运算符,分别为+、-、++、--。
这些符号与普通的数值操作的意思差不多,不过对于不同类型的指针来说,其运算有所不同。在普通的数值运算中,++和--表示自增和自减,在指针变量中也是类似,不过不同的是,指针变量的增减与其类型有关,每次增减的值是其类型的字节数。举个例子,有一个int类型的指针ptr,其初始值为1000,在执行完一次ptr++后,其值将变为1004。同样,如果ptr的类型是long *,则运行一次ptr++后将变成1008。其余的运算符也是同样的道理。这一点我们还会在介绍数组时再次介绍。
同样,指针还支持使用关系运算符(==、>、<)进行比较操作,这一点与数值运算相同,不再加以阐述。
值得注意的是,根据ANSI标准,void类型的指针无法进行运算操作。
数组
把数组和指针一起讲是因为这两者有些地方比较相似。
首先来复习一下数组的用法。首先是数组的声明方式,一般是按照这种规则:
类型 变量名[数组空间大小];
比如我需要申请一个能保存4个int类型数字的数组,就是如下语句:
1 | int arr[4]; |
或者也可以直接在声明变量时赋值(也就是初始化),此时就不再需要单独写数组空间大小,如下:
1 | int arr[] = {1, 2, 3, 4}; |
数组的访问党法为变量名[下标索引]。 不过需要注意的是,与大多数语言相同,C语言数组的下标索引也是从0开始。
以我们上面初始化的数组的代码为例,其数据在内存中的状态如下: