五、指针
5.1 指针的定义
内存区域中的每字节都对应一个编号,这个编号就是“地址”.
在程序中定义一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元.
按变量地址存取变量值的方式称为“直接访问”,如printf(""%d",i);、 scanf("%d",&i);等,
另一种存取变量值的方式称为“间接访问”,即将变量i的地址存放到另一个变量中.
在C语言中,指针变量是一种特殊的变量,它用来存放变量地址。
指针变量的定义格式如下:
基类型 *指针变量名;
例如
int *i_pointer;
指针与指针变量是两个概念,一个变量的“地址”成为该变量的“指针”
用来存放一个变量的地址(即指针)的变量,称为“指针变量”
如下图的i_pointer,在64位应用程序中,sizeof(i_pointer)=8,即占8个字节
如果考研强调程序是32位的,则寻址范围为4字节
5.2 取地址操作符与取值操作符,指针本质
取地址操作符为&,也称引用,通过&可以获取一个变量的地址值;
取值操作符为*,也称解引用,通过*可以得到一个地址对应的数据。
如下例,通过&i获取整型变量i的地址值,然后对整型指针变量p进行初始化, p中存储的是整型变量i的地址值,所以通过*p(printf 函数中的*p)就可以获取整型变量i的值. p中存储的是一个绝对地址值,
#include <stdio.h>
int main() {
int i=5;
//定义了一个指针变量,i_pointer就是指针变量名
//指针变量的初始化一定是某个变量取地址来赋值,不能随机写个数
int *i_pointer;
i_pointer=&i;
printf("i=%d\n",i); //直接访问
printf("*i_pointor=%d",*i_pointer);
return 0;
}
注意:
(1)指针变量前面的“*”表示该变量为指针型变量。
例如,float *pointer_1;
指针变量名是pointer_1 ,而不是*pointer_1.
(2)在定义指针变量时必须指定其类型。只有整型变量的地址才能放到指向整型变量的指针变量中。例如,下面的赋值是错误的:
float a;
int * pointer_1;
pointer_1=&a;//毫无意义而且会出错
(3)如果已执行了语句
pointer_1=&a; 那&*pointer_1与&a相同,都表示变量a的地址,即pointer_1
即虽然”&“和”*“两个运算符的优先级相同,但是要按自右向左的方向结合*&a表示,先进行&a的运算,得到a的地址,在进行*运算,*&a和*pointer_1的作用一样,都等价于变量a,即*&a与a等价
int *a,b,c;表示声明了一个指针变量,两个int变量
要声明三个指针变量需要写成int *a,*b,*c;
指针的使用场景只有两个,即传递和偏移
5.3 指针的传递
下例的主函数通过子函数改变量i的值,但是执行程序后发现i的值并未发生改变
因为i的地址和j的地址实际并不相同,执行change函数改变的并不是之前定义的i的地址
#include <stdio.h>
void change(int j){ //j是形参
j=5;
}
int main() {
int i=10;
printf("before value i=%d\n",i);
//在子函数内去改变主函数的某个变量值
change(i); //C语言的函数调用是值传递,实参赋值给形参,j=i
printf("after value i=%d\n",i);
return 0;
}
//输出结果都是10
原理如下图所示,程序的执行过程其实就是内存的变化过程,当main函数执行时,系统会为其开辟函数栈空间,当程序走到int i时,main函数的栈空间就会为变量i分配4字节大小的空间,调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参变量j分配4字节大小的空间,调用change(i)实际是将i的值赋值给j,这就是值传递,当change()中修改变量j的值后,函数执行结束,其栈空间就会释放,j就不再存在,i的值不会改变。
要在子函数中修改main函数的值,如下所示,通过指针进行操作
#include <stdio.h>
void change(int *j){ //j是形参
*j=5; //间接访问得到变量i *j等价于变量i
}
//指针的传递
int main() {
int i=10;
printf("before value i=%d\n",i);
change(&i); //传递变量i的地址 j=&i
printf("after value i=%d\n",i);
return 0;
}
//执行change()后,打印的i的值为5
变量i的地址传递给change函数时,实际效果是j=&i,依然是值传递,只是这时候的j是一个指针变量,内部存储的是变量i的地址,所以通过*j就间接访问到了与变量i相同的区域,通过*j=5就实现了对变量i的值的改变。
5.4 指针的偏移使用场景
5.4.1 指针的偏移
拿到指针变量后可以对他进行加减,如找到一栋楼是2栋,那往前就是1栋,往后就是3栋
#include <stdio.h>
//指针的偏移使用场景,也就是对指针进行加减
#define N 5 //符号常量N
int main() {
int a[N]={1,2,3,4,5}; //数组名内存中存储了数组的起始地址,a中存储的就是一个地址值
int *p;//定义指针变量p
p=a;//不是取地址a 因为数组名中本来就有地址
int i;
for(i=0;i<N;i++){
printf("%3d",*(p+i)); //这里*(p+i)和a[i]的结果是等价的
}
printf("\n-------------------------------\n");
p=&a[4]; //让p指向最后一个元素
for(i=0;i<N;i++){ //逆序输出
printf("%3d",*(p-i));
}
printf("\n");
return 0;
}
1 2 3 4 5
-------------------------------
5 4 3 2 1
如下图所示,数组名中存储着数组的起始地址Ox61fdf0,其类型为整型指针,所以可以将其赋值给整型指针变量p,可以从监视窗口中看到p+1的值为Ox61fdf4.指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*p+1就可以得到元素a[1]。编译器在编译时,数组取下标的操作正是转换为指针偏移来完成
float *p; p的加减也是偏移4个字节
5.4.2 指针与一维数组
一维数组中存储的是数组的首地址,如下例中数组名c中存储的是一个起始地址,所以子函数change中其实传入了一个地址,定义一个指针变量时,指针变量的类型要和数组的数据类型保持一致,通过取值操作,可以将“h”改为“H”,这种方法称为指针法,获取数组元素时,也可以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法。
#include <stdio.h>
//指针与一维数组的的传递
//数组名作为实参传递给子函数时,是弱化为指针的 (指针默认就是8个字节)
//练习传递与偏移
void change(char *d){
*d='H';
d[1]='E'; //*(d+1)='E'与其等价
*(d+2)='L';
*(d+3)='L';
*(d+4)='O';
}
int main(){
char c[10]="hello";
change(c);
puts(c);
return 0;
}
5.6指针与malloc动态内存申请,栈与堆的差异
5.6.1 指针与动态内存申请
C语言的数组长度固定,因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的,如果使用的空间大小不确定,就要使用堆空间。
#include <stdio.h>
#include <stdlib.h> //malloc使用的头文件
#include <string.h> //strcpy使用的头文件
int main() {
int size;//代表要申请多大字节的空间
char *p;//void*类型的指针是不能偏移的,不能进行加减 ,因此不会定义无类型指针
scanf("%d",&size); //输入要申请的空间大小
//malloc返回的void*代表无类型指针
p= (char*)malloc(size); //malloc返回的是对应空间起始地址 要强转malloc的类型与p一致
strcpy(p,"malloc success");//strcpy传进去的也是指针类型的 所以可以直接copy
puts(p);
free(p);//释放申请的空间时,必须是最初malloc返回给我们的地址 即free的时候p不能变 不能填p+1这种 会异常
printf("free success\n");
return 0;
}
注意:指针本身的大小和其指向空间的大小是两码事,指针本身大小只跟操作系统的位数有关,指向空间的大小是字节定义的,需要定义变量单独存储
5.6.2 栈空间与堆空间的差异
栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,操作都有专门的指令执行,所以栈的效率比较高。
堆则是C/C++函数库提供的数据结构,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间就可能调用系统功能去增加程序数据段的内存空间,堆的效率要比栈低得多。
栈不能动态,所以要动态还是要使用堆,注意堆使用完要用free释放
#include <stdio.h>
#include <stdlib.h> //malloc使用的头文件
#include <string.h> //strcpy使用的头文件
//堆和栈的差异
char* print_stack(){
char c[100]="I am print_stack func"; //c中存了数组的起始地址
char *p;
p=c;
puts(p);
return p;
}
char* print_malloc(){
char *p=(char*) malloc(100); //堆空间在整个进程中一直有效,不因为函数结束消亡
strcpy(p,"I am print_stack func");
puts(p);
return p;
}
int main(){
char *p;
p=print_stack(); //调用函数执行完之后,操作系统就会直接释放掉这个栈空间
puts(p);//p接到了print_stack()的指针 打印乱码或null
p=print_malloc();
puts(p);
free(p);//只有free的时候,堆空间才会释放
return 0;
}
/*
int main() {
int size;//代表要申请多大字节的空间
char *p;//void*类型的指针是不能偏移的,不能进行加减 ,因此不会定义无类型指针
scanf("%d",&size); //输入要申请的空间大小
//malloc返回的void*代表无类型指针
p= (char*)malloc(size); //malloc返回的是对应空间起始地址 要强转malloc的类型与p一致
strcpy(p,"malloc success");//strcpy传进去的也是指针类型的 所以可以直接copy
puts(p);
free(p);//释放申请的空间时,必须是最初malloc返回给我们的地址 即free的时候p不能变 不能填p+1这种 会异常
printf("free success\n");
return 0;
}
*/