指针的本质(间接访问原理)
指针的定义
内存区域中的每字节都对应一个编号,这个编号就是“地址”。如果在程序中定义了一个变量,那么在对程序进行编译时,系统就会给这个变量分配内存单元。按变量地址存取变量值的方式称为“直接访问” 如printf(“%d”,i);scanf(“%d”,&i)等;另一种存取变量值的方式称为 “间接访问”,即将变量i的地址存放到另一个变量中,在 C 语言中,指针变量是一种特殊的变量它用来存放变量地址。
指针变量的定义格式如下:
基类型 *指针变量名;
int *i_pointer;
指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。例如,地址 2000 是变量i的指针。如果有一个变量专门用来存放另一变量的地址 (即指针),那么称它为“指针变量”。
那么ipointer 本身占多大的内存空间呢?本章中编写的程序都是 64 位应用程序,寻址范围为 64 位即8 字节,所以对于本章来说sizeof(i_pointer)=8。如果编写的程序是 32 位,那么寻址范围就是 4 字节 (考研中往往会强调程序是 32 位的程序)
取地址操作符与取值操作符,指针本质
取地址操作符为&,也称引用,通过该操作符我们可以获取一个变量的地址值;取值操作符为*,也成解引用,通过该操作符我们可以得到一个地址对应的数据。
(1)指针变量前面的“表示该变量为指针型变量。例如,
float *pointer_1;
注意指针变量名是 pointer_1,而不是*pointer_1.
(2) 在定义指针变量时必须指定其类型,需要注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。例如,下面的赋值是错误的:
float a;
int *pointer_1;
pointer_1=&a; //毫无意义而且会出错,有兴趣的读者可以自行尝试
(3) 如果已执行了语句pointer_1=&a
;那么&*pointer_1
的含义是什么呢?
“&”
和“*”
两个运算符的优先级别相同,但要按自右向左的方向结合。
因此&*pointer_1
与&a
相同,都表示变量 a 的地址,
也就是 pointer_1
。
*&a
的含义是什么呢?
首先进行&a
运算,得到a的地址,再进行*运算,
*&a
和*pointer_1
的作用是一样的,它们都等价于变量 a,
即*&a
与a
等价。
(4)C 语言本质上是一种自由形式的语言,这很容易诱使我们把“*”
写在靠近类型的一侧,如int *a
这个声明与前面一个声明具有相同的意思,而且看上去更清晰,a 被声明成类型为 int*
的指针,但是,这并不是一个好习惯,因为类似int *a,bc
的语句会使人们很自然地认为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此,“*”
实际上是*a
的一部分,只对 a 标识符起作用,但其余两个变量只是普通的整型变量,要声明三个指针变量,正确的语句如下:
int *a,*b,*c;
指针的传递
可以看到变量i的地址是 0x61fe1c,这时变量j的值的确为10,但是&j的值为0x61fdfo,也就是j和i的地址并不相同。运行j=5后,change 函数实际修改的是地址 0x61fdfo上的值,从10 变成了5,接着 change 函数执行结束,变量i的值肯定不会发生改变,因为变量i的地址是0x61fe1c 而非 0x61fdfo 。程序的执行过程其实就是内存的变化过程,需要关注是栈空间的变化。当 main 函数开始执行时,系统会为 main 函数开函数栈空间,当程序走到int i时,main 函数的栈空间就会为变量 i分配4 字节大小的空间,调用 change 函数时,系统会为 change 函数重新分配新的函数栈空间,并为形参变量j分配 4 字节大小的空间在调用change(i)时,实际上是将i的值赋值给j,我们把这种效果称为值传递(C 语言的函数调用均为值传递)。因此,当我们在 change 函数的函数栈空间内修改变量j的值后,change 函数执行结束,其栈空间就会释放,j 就不再存在,i的值不会改变。
我们将变量i的地址传递给 change 函数时,实际效果是 j=&i
依然是值传递,只是这时我们的j是一个指针变量,内部存储的是变量i
的地址所以通过*j
就间接访问到了与变量i
相同的区域,通过*j=5
就实现了对变量的值的改变。
指针的偏移
指针的偏移
对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移。
如上图所示,数组名中存储着数组的起始地址 0x61fdfo,其类型为整型指针,所以可以将其试值给整型指针变量 p,可以从监视窗口中看到 p+1的值为 0x61fdf4。因为指针变量加1后,偏的长度是其基类型的长度也就是偏 sizeof(int)
,这样通过*(p+1)
就可以得到元素 a[1]
。编译器在编译时,数组取下标的操作正是转换为指针偏移来完成。
指针与一维数组
为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢?这是由于一维数组名中存储的是数组的首地址,如下例所示,数组名 c 中存储是一个起始地址 ,所以子函数 change 中其实传人了一个地址。定义一个指针变量时,指针变量的类型要和数组的数据类型保持一致,通过取值操作,就可将“h”改为“H”,这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法。
指针与malloc动态内存申请,栈和堆的差异
指针与动态内存申请
C 语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间。
首先我们来看 malloc 函数。在执行
#include <stdlib.h>
void* malloc(size_t size);
时,需要给 malloc 传递的参数是一个整型变量,因为这里的 size_t
即为 int
; 返回值为void*
类型的指针,void*
类型的指针只能用来存储一个地址而不能进行偏移,因为 malloc
并不知道我们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将 void*
强制转换为对应的类型。
同时需要注意指针本身大小,和其指向的空间大小,是两码事,不能和前面的变量类比去
解。
如下图所示,定义的整型变量i、指针变量 p 均在 main 函数的空间中,通过 malloc 申请的空间会返回一个堆空间的首地址,我们把首地址存入变量 p.知道了首地址,就可以通过 strcpy函数往对应的空间存储字符数据。
栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持: 分配专门的寄存器存放栈的地址,压栈操作、出栈操作都有专门的指令执行,这就决定了栈的效率比较高;堆则是 C/C++函数库提供的数据结构,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,那么就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率要比栈低得多。
栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过 free函数释放堆空间。free 函数的头文件及格式为
#include <stdlib.h>
void free(void *ptr);
其传入的参数为 void
类型指针,任何指针均可自动转为 void*
类型指针,所以我们把p
传递给free
函数时,不需要强制类型转换,p
的地址值必须是 malloc
当时返回的地址值,不能进行偏移,也就是在 malloc
和 free
之间不能进行 p=p+1
等改变变量 p 的操作,原因是申请一段堆内存空间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不上时,进程就会崩溃。如果要偏移进而存储数据,那么可以定义两个指针变量来解决。
栈空间与堆空间的差异
print_stack()
函数中的字符串存放在栈空间中,函数执行结束后,栈空间会被释放,字符数组c
的原有空间已被分配给其他函数使用,因此在调用 print_stack()
函数后,print(“p=%sn",p);
中的p
不能获取栈空间的数据而 print_malloc()
函数中的字符串存放在堆空间中,堆空间只有在执行free
操作后才会释放,否则在进程执行过程中会一直有效。