6.1指针的定义
6.1.1指针的本质(间接访问原理)
- 内存区域中每字节对应一个编号,这个编号就是“地址”。如果在程序中定义了一个变量,那么在对程序进行编译时,系统会给这个变量分配内存单元。
- 按变量存取变量值的方式称为“直接访问”,如printf("%d\n",i); scanf("%d",%i);等。
- 将变量i的地址存放到另一个变量中,称为“间接访问”,例如指针用来存放变量地址。
指针变量定义格式:
基类型 *指针变量名;
例如:
//第一种写法,其中,i_pointer是变量名(不是*i_pointer),即指针变量
int *i_pointer; //更推荐第一种写法
//第二种写法
int* i_pointer;
【注意】
- 指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。(地址=指针)
- 如果有一个变量专门用来放另一变量的地址(即指针),那么称它为“指针变量”。
- 若编写的程序都是64位的应用程序,寻址范围为64位即8字节,那么sizeof(i_pointer)=8;如果编写的程序是32位,那么寻址范围就是4字节(考研通常强调程序为32位程序)
- 说某个变量的地址时,说的都是它的起始地址
6.1.2取地址操作符& 与取值操作符* 及指针本质
- 取地址操作符(也称引用):&
- 取值操作符(也称解引用):*
【取地址与取值】
#include <stdio.h> int main() { int i = 5; //指针变量的初始化是某个变量取地址来赋值,不能随机写个数 //必须保证同一类型变量的地址赋给同一类型的指针 int *i_pointer; //定义了一个指针变量,i_pointer是指针变量 i_pointer = &i; //取地址,指针变量初始化一定是某个变量取地址 //line 7 和 line 8 可以一起写成 int *i_pointer = &i; printf("i=%d\n", i);//直接访问 printf("*i_pointer=%d\n", *i_pointer);//间接访问 *单目运算符从右至左结合 return 0; }
【运行结果】
【运行机制】
【注意】
- 指针变量前面的“*”表示该变量为指针型变量,例如:float *pointer_1; 指针变量名是pointer_1而不是*pointer_1。
- 定义指针变量时必须定义其类型,注意只有同一类型变量的地址才能放到指向同一类型的指针变量当中。
【拓展】
如果已经执行了语句
i_pointer = &i; //取地址
- 再执行 &*i_pointer 是什么含义:
&*i_pointer(先取值*再取地址&)与&i相同,都表示变量i的地址,也就是i_pointer,因为先执行*pointer_1就相当于拿到i(此时为i的地址),再去&又是拿地址。(不要用这种写法)
- *&i含义是什么:
首先进行&i运算,得到i的地址,再进行*运算,得到还是i的地址,*&i与*pointer_1作用一致,都等价于变量i,即*&i与i等价。
为什么要让*号写在指针变量前面:
因为如果要声明三个指针变量时正确的语句是
int *a,*b,*c;
6.2指针的传递使用场景
指针的使用场景只有两个,即:传递和偏移
6.2.1指针的传递
【指针传递引例】
在mian函数中定义了变量i并初始化为10,然后通过子函数change修改整型变量i的值,但发现在执行语句print("after change i=%d\n",i);后,打印的i的值仍为10,子函数change并未改变i的值。
#include <stdio.h> //在子函数内去改变主函数中某个变量的值 void change(int j) {//j是形参 j = 5; } int main() { int i = 10; printf("before change i=%d\n", i); change(i);//c语言的函数调用是值传递:实参赋值给形参,即j=i printf("after change i=%d\n", i); return 0; }
【内存运行机制】
为什么是值传递
- 查看变量i的地址
- 查看变量j的地址
- 变量i和变量j的地址不一样
【运行效果】
当我们在change函数栈空间内修改变量j的值后,change函数执行结束,其栈空间就会释放,j就不再存在,i的值不会改变。
原理图(进程地址空间)
【指针传递例子】
#include <stdio.h> //在子函数内去改变主函数中某个变量的值 void change(int *j) { //*j等价于变量i,只是间接访问(相当于*&i,所以为i) *j = 5;//此时*j是i的地址,将5赋值给i的地址 } int main() { int i = 10; printf("before change i=%d\n", i); //c语言的函数调用是值传递:实参赋值给形参,即j=&i change(&i);//将i的地址传递给指针变量j printf("after change i=%d\n", i); return 0; }
【运行效果】
【内存运行机制】
- 变量i的地址为61fe1c
- 执行change函数,变量j的值为i的地址
- 执行后i的值为5
6.2.2指针的偏移
指针即地址,指针的偏移即是对指针进行加和减。
- 指针加就是向后偏移
- 指针减就是向前偏移
【指针的偏移使用场景】
#include <stdio.h> //指针的偏移,也就是对指针进行加和减 //加:往后偏移 减:往前偏移 //定义一个符号常量N #define N 5 int main() { int a[N] = {1, 2, 3, 4, 5}; //数组名内存储了数组的起始地址,a中存储的就是一个地址值 int *p;//定义指针变量p p = a; int i; for (i = 0; i < N; i++) {//顺序输出 //printf("%3d",a[i]);//访问数组 printf("%3d", *(p + i));//*(p+i)与a[i]等价 } printf("\n----------------\n");//换行 p = &a[4]; //让p指向最后一个元素 for (int i = 0; i < N; i++) {//逆序输出 printf("%3d", *(p - i)); } printf("\n");//换行 return 0; }
【运行结果】
【内存视图查看】
注意:
- 数组名中存储着数组的起始地址0x61fdf0,其类型为整型指针,所以可以将其赋值给整型指针变量p。
- p+1的值为0x61fdf4而不是0x61fdf1,因为指针变量+1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过*(p+1)就可以得到元素a[1]。
- 编译器编译时,数组取下标的操作正是转换为指针偏移来完成的。
6.2.3指针与一维数组
一维数组在函数调用进行传递时,它的长度子函数无法知道,是因为一维数组名中存储的是数组的首地址。
【数组传递给子函数例子】
#include <stdio.h> //指针传递与偏移 //指针与一维数组的传递 //数组名作为实参传递给子函数时,是弱化为指针的 void change(char *d) { //形参写成char *d与char d[]等价,但是写成char *d *d = 'H'; d[1] = 'E';//*(d+1)='E'与其等价 *(d + 2) = 'L'; } int main() { char c[10] = "hello"; //数组名无法把数组传递给子函数,数组名内存的数组的起始地址,是一个指针值 //函数调用是值传递,所以并不能把数组类型传递给子函数,只能把值传递给子函数 //所以子函数的形参只会有指针变量,没有数组变量一说 change(c);//因为形参是char *d,所以不管数组多大,传递过去都是8字节 puts(c); return 0; }
【运行效果】
【原理图示】
注意:
- 数组名c中存储的是一个起始地址,所以子函数change中其实传入了一个地址。
- 定义指针变量时,指针变量的类型要和数组的数据类型保持一致。
6.3指针与malloc动态内存申请
- c语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。
- 如果使用的空间不确定,那么就要使用堆空间。
定义的整型变量i、指针变量p均在main函数的栈空间中,通过malloc申请的空间(是向操作系统申请)会返回一个堆空间的首地址,我们把首地址存入变量p。
知道首地址后就可以通过strcpy函数往对应的空间存储字符数据。
【动态内存申请例子】
#include <stdio.h> #include <stdlib.h> //malloc和free使用的头文件 #include <string.h> //strcpy使用的头文件 int main() { int size;//size代表我们要申请多大字节的空间 char *p;//不能定义成void *p,因为void*类型的指针不能偏移 void是一个空类型 //使用strcpy函数,将某个字符串复制到字符数组中,会覆盖原数组内容 scanf("%d", &size);//输入要申请的空间大小 //malloc返回的的void*表示无类型指针 p = (char *) malloc(size);//需要给malloc传递的参数是一个整型变量 //用(char *)将其强制转换为跟定义个指针变量一致的类型 strcpy(p, "malloc success"); puts(p); free(p);//释放申请的malloc空间,给的地址必须是最初malloc返回给我们的地址 //即不可以写成free(p+1)之类的 printf("free success\n"); return 0; }
【运行效果】
malloc函数,在执行:
#include <stdlib.h>
void*malloc(size_t size);
时,需要给malloc传递的参数只能是一个整型变量,因为这里的size_t即为int,返回值为void*类型的指针。
注意:
- void*类型的指针只能用来存储一个地址,不能进行偏移,因为malloc不知道申请的空间用来存放什么类型的数据。
- 在确定用来存储什么类型的数据后,需要将void*类型的指针强转为对应的类型。
- 指针本身大小为8字节,是固定不变的,因为是64位的程序编写,和其指向的空间大小是两码事。
- 堆空间的效率比栈低得多。(了解)
栈空间由系统自动管理,堆空间的申请和释放需要自行管理,需要通过free函数释放堆空间
#include <stdlib.h> //free函数的头文件格式
void free(void *ptr);
其传入的参数为void类型指针,任何指针均可自动转为void*类型指针,所以把p传递给free函数时,不需要强制转换,p的地址值必须是malloc当时返回的地址值,不能进行偏移。
6.4栈空间与堆空间的差异
【栈空间与堆空间差异的例子】
#include <stdio.h> //栈和堆的差异 char *print_stack() { char c[100] = "I am print_stack func"; char *p; p = c; //c数组名中存放了起始地址,即指针 puts(p); return p; } int main() { char *p; p = print_stack(); //print_stack返回一个char*类型 puts(p); return 0; }
【运行结果】
【分析】
上述代码运行puts(p)语句时出现乱码,原因是ptint_stack函数的栈空间用完后就被释放,这时把栈空间上的数据的起始地址返回回来,下面接着打印就会出现乱码,因为下一个栈空间给了puts函数。
【改进版】
#include <stdio.h> #include <stdlib.h>//malloc函数的头文件 #include <string.h>//strcpy函数的头文件 //栈和堆的差异 char *print_stack() { char c[100] = "I am print_stack func"; char *p; p = c; //c数组名中存放了起始地址,即指针 puts(p); return p; } char *print_malloc() { //堆空间在整个进程中一直有效, char *p = (char *) malloc(100); strcpy(p, "I am print malloc func"); puts(p); return p; } int main() { char *p; p = print_stack(); //print_stack返回一个char*类型 puts(p); p = print_malloc(); puts(p); free(p);//只有free时,堆空间才会释放 return 0; }
【运行结果】