一、指针的概念
1.1 数据存储--地址
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
4G 内存中每个字节的编号(以十六进制表示):
0x00->0x01->0x02->........->0xFFFFFFFFE->0xFFFFFFFF
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
#include <stdio.h>
int main(void)
{
int a = 50;
char str[20] = "gec-edu";
//&a 获取a变量空间的地址 str获取数组空间的地址
printf("%#X, %#X", &a, str);
return 0;
}
输出地址:
0XFFFFCBFC, 0XFFFFCBE0
%#X表示以十六进制形式输出,并附带前缀0X。a 是一个变量,用来存放整数,需要在前面加&来获得它的地址;str 本身就表示字符串的首地址,不需要加&。
c语言还有一个%p专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带0x前缀,有的不带。
1.2 一切都是地址
C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。
数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存
变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址。
需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身(变量地址:&a),而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
二、一级指针
2.1 指针概念
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量。
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。
现在假设有一个 char 类型的变量 c,它存储了字符 'K'(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。
2.2 定义一级指针变量
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:datatype *name;
或者 datatype *name = addr; addr地址,初始化为NULL,或者为&变量、数组名、函数名
*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。例如:int *p1;
p1 是一个指向 int 类型数据的指针变量,至于 p1 究竟指向哪一份数据,应该由赋予它的值决定。再如:int a = 100; int *p_a = &a;
在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 需要的一个地址,a 前面必须要加取地址符&,否则是不对的。
和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:
float a=99.5, b = 10.6;
char c = '@', d = '#';
float *p1 = &a;
char *p2 = &c;
p1 = &b; //给指针变量赋值 ,只能赋地址
p2 = &d;
*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
指针变量也可以连续定义,例如: int *a, *b, *c; a、b、c 的类型都是 int*
注意每个变量前面都要带*。如果写成:int *a, b, c 形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量。
2.3 通过指针变量取得数据
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*pointer; //*指针变量
这里的*称为指针运算符(也称为指针解引脚),用来取得某个地址上的数据,请看下面的例子:
#include <stdio.h>
int main(void)
{
int a = 15;
int *p = &a; //*p表示定义里标识为指针变量
//*p 指针运算,指针解引用 *p == a
printf("a value:%d, *p value:%d\n", a, *p);
if(*p == a)
{
printf("*p 是对等的哦!\n");
}
return 0;
}
运行结果:
a value:15, *p value:15
*p 是对等的哦!
假设 a 的地址是 0X1000,p 指向 a 后,p 本身的值(空间)也会变为 0X1000,*p 表示获取地址 0X1000 上的数据,也即变量 a 的值。从运行结果看,*p 和 a 是等价的。
CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 *p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 *p 要经过两次运算,多了一层“间接”。
程序被编译和链接后,a、p 被替换成相应的地址。使用 *p 的话,要先通过地址 0XF0A0 取得变量 p 本身的值(0x1000),这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。
也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:
#include <stdio.h>
int main(void)
{
int a = 15, b = 99, c = 222;
int *p = &a; //指针变量初始化
*p = b; //通过指针变量修改内存上的数据
c = *p; //通过指针变量获取内存上的值,并赋值给c
printf("a:%d,b:%d,c:%d, *p:%d\n", a, b, c , *p);
return 0;
}
运行结果:
99, 99, 99, 99
*p 代表的是 a 中的数据,它等价于 a,可以将另外的一份数据赋值给它,也可以将它赋值给另外的一个变量。
*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。
2.4 关于 * 和 & 的谜题
假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a和&*pa分别是什么意思呢?
*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。
&*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa( == &a)。
2.5 对星号*的总结
星号*主要有三种用途:
- 表示乘法,例如int a = 3, b = 5, c; c = a * b;,这是最容易理解的。
- 表示定义一个指针变量,以和普通变量区分开,例如int a = 100; int *p = &a;。
- 表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a; *p = 100; b = *p;
2.6 C语言指针变量的运算
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,请看下面的代码:
#include <stdio.h>
int main(void)
{
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);
printf("-----------------------------------\n");
//加法运算
pa++; pb++; pc++;
printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);
printf("-----------------------------------\n");
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%p, pb=%p, pc=%p\n", pa, pb, pc);\
printf("-----------------------------------\n");
//比较运算
if(pa == paa){
printf("*paa:%d\n", *paa);
}else{
printf("*pa:%d\n", *pa);
}
return 0;
}
运行结果:
&a=0xffffcbdc, &b=0xffffcbd0, &c=0xffffcbcf
pa=0xffffcbdc, pb=0xffffcbd0, pc=0xffffcbcf
-----------------------------------
pa=0xffffcbe0, pb=0xffffcbd8, pc=0xffffcbd0
-----------------------------------
pa=0xffffcbd8, pb=0xffffcbc8, pc=0xffffcbce
-----------------------------------
*pa:0
从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。
这很奇怪,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?
以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。
我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,也就是说指针在数组里才有意义。
不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
2.7 关于指针变量长度问题
指针变量所占的空间多少?在32位系统里,指针变量所占的空间大小永远是4,在64位系统,指针变量所占的空间大小永远是:8。指针变量空间大小与所指向的类型无关,只与系统对地址分配有空,32位系统,分配给地址空间大小为:4字节。64位系统,分配给地址空间大小为:8字节。
64位系统下:
#include <stdio.h>
int main(void)
{
char *p1;
short *p2;
int *p3;
long *p4;
float *p5;
double *p6;
printf("指向char 类型指针长度:%d\n", sizeof(p1));
printf("指向short 类型指针长度:%d\n", sizeof(p2));
printf("指向int 类型指针长度:%d\n", sizeof(p3));
printf("指向long 类型指针长度:%d\n", sizeof(p4));
printf("指向float 类型指针长度:%d\n", sizeof(p5));
printf("指向double 类型指针长度:%d\n", sizeof(p6));
return 0;
}
输出:
指向char 类型指针长度:8
指向short 类型指针长度:8
指向int 类型指针长度:8
指向long 类型指针长度:8
指向float 类型指针长度:8
指向double 类型指针长度:8
2.8 C语言指针变量作为函数参数
在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。
有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。
先看一个经典的错误案例:
#include <stdio.h>
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
int main(void)
{
int a = 66, b =99;
//这是一个函数
swap(a, b);
printf("a:%d, b:%d\n", a, b);
return 0;
}
运行结果:
a:66, b:99
从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的x、y 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,swap() 交换的是它内部 x、y 的值,不会影响它外部(main() 内部) a、b 的值。
改用指针变量作参数后就很容易解决上面的问题:
#include <stdio.h>
void swap(int *p1, int *p2)
{
int temp;
printf("p1:%p, p2:%p\n", p1, p2);
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(void)
{
int a = 66, b =99;
printf("&a:%p, &b:%p\n", &a, &b);
//这是一个函数
swap(&a, &b);
printf("a:%d, b:%d\n", a, b);
return 0;
}
输出:
&a:0xffffcbfc, &b:0xffffcbf8
p1:0xffffcbfc, p2:0xffffcbf8
a:99, b:66
调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、*p2 代表的就是变量 a、b 本身,交换 *p1、*p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。
需要注意的是临时变量 temp,它的作用特别重要,因为执行*p1 = *p2;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了。
这就好比拿来一瓶可乐和一瓶雪碧,要想把可乐倒进雪碧瓶、把雪碧倒进可乐瓶里面,就必须先找一个杯子,将两者之一先倒进杯子里面,再从杯子倒进瓶子里面。这里的杯子,就是一个“临时变量”,虽然只是倒倒手,但是也不可或缺。
2.9 用数组作函数参数
数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:
在函数内,使用p[i]表示数组的元素,是其中的一种表达方式。
#include <stdio.h>
//数组名名字被做参数里,在函数在默认转化为指针,指针长度永远为:8
int max(int *p, int len)
{
int max;
int i;
printf("指针长度:%d\n", sizeof(p));
printf("intarr value:%p\n", p);
printf("在函数当中打印数组\n");
for(i=0; i<len; i++)
{
printf("p[%d] = %d\n", i, p[i]); //在函数里,数组传过来的指针,可以:指针[下标] 来表示元素
}
//假设p[0]是最大值
max = p[0];
for(i=1; i<len; i++)
{
if(max < p[i])
{
max = p[i];
}
}
return max;
}
int main(void)
{
int nums[6], i, maxvalue;
//按字节来算
printf("数组长度:%d\n", sizeof(nums));
printf("请输出数组的值:");
for(i=0; i<6; i++)
{
scanf("%d", &nums[i]);
}
printf("数组地址:%p\n", nums);
//数组名字就是指针
maxvalue = max(nums, 6);
printf("数组最大值:%d\n", maxvalue);
return 0;
}
运行结果:
数组长度:24
请输出数组的值:25 41 36 85 41 22
数组地址:0xffffcbe0
指针长度:8
intarr value:0xffffcbe0
在函数当中打印数组
p[0] = 25
p[1] = 41
p[2] = 36
p[3] = 85
p[4] = 41
p[5] = 22
数组最大值:85
参数p 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数。
用数组做函数参数时,参数也能够以“真正”的数组形式给出。例如对于上面的 max() 函数,它的参数可以写成下面的形式:
int max(int *p, int len)
改
int max(int intarr[], int len)
int intArr[6]好像定义了一个拥有 6 个元素的数组,调用 max() 时可以将数组的所有元素“一股脑”传递进来。
int intArr[]虽然定义了一个数组,但没有指定数组长度,好像可以接受任意长度的数组。
实际上这两种形式的数组定义都是假象,不管是int *p还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。
需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。
C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。
三、二级指针
指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量:
int a = 100;
int *p1 = &a;
int **p2 = &p1;
指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。
如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:int ***p3 = &p2;
四级指针也是类似的道理:int ****p4 = &p3;
实际开发中使用一级指针和二级指针足以解决绝大部分问题。
例子:
#include <stdio.h>
int main(void)
{
int a = 100;
int *p1 = &a;
int **p2 = &p1;
printf("&a addr:%p\n",&a);
printf("&p1 addr:%p\n",&p1);
printf("&p2 addr:%p\n",&p2);
printf("a变量空间的值: %d\n", a);
printf("p1变量空间的值:%p\n", p1);
printf("p2娈量空间的值:%p\n", p2);
printf("a = %d, *p1 = %d, **p2 = %d\n", a, *p1, **p2);
printf("*p2 = %p\n", *p2);
return 0;
}
输出:
&a addr:0xffffcbfc
&p1 addr:0xffffcbf0
&p2 addr:0xffffcbe8
a变量空间的值: 100
p1变量空间的值:0xffffcbfc
p2娈量空间的值:0xffffcbf0
a = 100, *p1 = 100, **p2 = 100
*p2 = 0xffffcbfc
以二级指针 p2 为例来分析上面的代码。**p2等价于*(*p2))。*p2 得到的是 p1 的值(地址),也即变量a的地址;*(*p2) 得到的是a 的值。