计算机的内存长什么样子?
1、计算机中的内存就像一叠非常厚的“便签”,一张便签就相当于一个字节的内存,一个字节有8个二进制位
2、每一张“便签”都有自然排序的一个编号,计算机是根据便签的编号来访问、使用"便签"
3、CPU会有若干个金手指,每根金手指能感知高低电平,高电平转换成1,低电平转换成0,我们常说的32位CPU指的是CPU有32个金手指用于感知电平,并计算出“便签”的编号
便签的最小编号: 00000000 00000000 00000000 00000000 = 0 便签的最大编号: 11111111 11111111 11111111 11111111 = 4294967295 所以32位CPU最多能使用4Gb的内存
4、便签的编号就是内存的地址,是一种无符号的整数类型
什么是指针:
1、指针(pointer)是一种特殊的数据类型,使用它可以用于定义指针变量,简称指针
2、指针变量中存储的是内存的地址,是一种无符号的整数类型,
3、通过指针变量中记录的内存地址,我们可以读取对应的内存中所存储的数据、也可以向该内存写入数据
4、可以通过 %p 显示指针变量中存储的地址编号
如何使用指针:
定义指针变量
类型* 指针变量名;
int num; char n; double d; int* nump; // 访问4字节 char* p; // 访问1字节 double* doublep; // 访问8字节 long* lp; // 访问4/8字节
1、一个指针变量冲只记录内存中某一个字节的地址,我们把它当做一块内存的首地址,当使用指针变量去访问内存时具体连续访问多少个字节,指针变量的类型来决定。
2、普通变量与指针变量的用法上有很大区别,为了避免混用,所以指针变量一般以p结尾,以示区分
3、指针变量不能连续定义,一个*只能定义一个指针变量
int n1,n2,n3; // n1 n2 n3都是int int* p1,p2,p3; // int *p1,p2,p3 p1是int* p2 p3是int int *p1,*p2,*p3; // p1 p2 p3都是int*
4、指针变量与普通一样,默认值是随机的(野指针),为了安全尽量给指针变量初始化,如果不知道该初始化为多少,可以先初始化为NULL(空指针)
int* p; // 野指针 int* p = NULL; // 空指针
给指针变量赋值:
指针变量 = 内存地址
所谓的给指针变量赋值,其实就是往指针变量中存储一个内存地址,如果该内存地址是非法的,当使用该指针变量去访问内存时会出现 段错误
// 存储堆内存地址 int* p = malloc(4); // 存储指向num所在内存地址(stack\data\bss) int num; // stack int* p = # // &num 类型是int* 注意:num变量的类型必须与p类型相同
指针变量解引用:
*指针变量名;
给指针变量赋值就是让指针指向某一个内存,对指针变量解引用就是根据指针变量中存储的内存编号,去访问该内存,具体连续访问多少个字节由指针变量定义时的类型决定
int num = 100; // 定义指针变量 int* p = NULL; // 给指针变量赋值 p = # // 查看指针变量的值 printf("%p\n",p); // 对指针变量解引用 printf("%d\n",*p + 10); *p = 88; printf("%d\n",num);
如果指针变量中存储的是非法的内存地址,当程序运行到该指针变量解引用时,会出现段错误
// 定义指针变量 int* p = NULL; *p = 100; // 非法访问内存 会段错误
验证指针变量中存储的就是一个整数
#include <stdio.h> void func(unsigned long addr) { *(int*)addr = 88; } int main(int argc,const char* argv[]) { int num = 10; func(&num); printf("num=%d\n",num); }
为什么要使用指针:
1、函数之间需要共享变量
函数之间的命名空间是相互独立,并且是以赋值的方式进行单向值传递,所以无法通过普通类型形参传参来解决共享变量的问题
全局变量虽然可以在函数之间共享,但是过多地使用全局变量容易造成命名冲突和内存浪费
使用数组是可以共享,但是需要额外传递长度
因此,虽然函数之间的命名空间是相互独立的,但是所使用的是同一条内存,也就是说内存空间是同一个,所以使用指针可以解决函数之间共享变量的问题
#include <stdio.h> void func(int* p) { printf("func p=%p,*p=%d\n",p,*p); *p = 88; printf("func p=%p,*p=%d\n",p,*p); } int main(int argc,const char* argv[]) { int num = 66; func(&num); printf("main &num=%p ",&num); printf("main:%d\n",num); }
当函数需要返回两个以上的数据时,光靠返回值满足不了,可以通过指针共享一个变量,借助该输出型参数,返回多个数据
// put_p输出型参数 int func(int* put_p) { *put_p = 20; return 10; } int main(int argc,const char* argv[]) { int num = 0; int ret1 = func(&num); printf("ret1 = %d ret2=%d\n",ret1,num); }
2、使用指针可以提高函数之间的传参效率
一个指针变量占内存 4 | 8 字节
函数之间传参是以内存拷贝的方式进行,当参数的内存字节数比较大(大于4字节时)的时候,传参的效率就会比较低下,此时使用指针传参可以提高传参效率
#include <stdio.h> void func(long double* f) { } int main(int argc,const char* argv[]) { long double f = 3.14; for(int i=0; i<1000000000; i++) { func(&f); } }
3、使用堆内存时,必须与指针变量配合
堆内存无法像栈、数据段、bss段那样给内存取名字,通过标准库、操作系统提供的管理堆内存的接口函数,来操作堆内存时,是直接返回堆内存的地址给调用者,因此必须使用指针变量配合才能访问堆内存
malloc realloc calloc
学习建议:指针就是一种工具,目的是完成任务,而使用指针是有危险性,所以除了以上三种情况需要使用指针以外,不要轻易使用指针
使用指针需要注意的问题:
空指针:
指针变量中存储的NULL,那么它就是空指针
操作系统规定程序不能访问NULL指向的内存,只要访问必定段错误
当函数的返回值是指针类型时,函数执行出错时一般返回NULL,作为函数的错误标志
NULL也可以作为初始值给指针变量初始化
#include <stdio.h> int* func(void) { return xxx; return NULL; //表示执行出错 } int main(int argc,const char* argv[]) { int* p = NULL; int num= 10; p = # printf("%d\n,",*p); // 必定段错误 }
如何避免空指针产生的段错误?
对来历不明的指针进行解引用前先判断是否是空指针
1、当自己写的函数的参数中有指针类型时,在使用该参数时,需要先判断是否是空指针再使用
2、当使用别人提供的函数时,它的返回值类型是指针类型时,获取返回值后,也需要先判断是否是空指针再使用
int* p = malloc(4); if(NULL == p) printf("内存申请失败\n"); else *p = 100;
if(NULL == p) // 正确写法 if(p == NULL) // 容易漏写= 变成赋值 错误写法 if(!p) // 绝大多数系统中 NULL 是0,少数系统中是1 { // 通用性不够强 } 注意:必须导入 stdio.h 后 NULL才可以使用
野指针:
指针变量中存储的地址,无法确定是哪个地址、是否是合法地址,此时该指针就称为野指针
对野指针解引用的后果:
1、一切正常,刚好指针变量中存储的是空闲且合法的地址
2、段错误,刚好指针变量中存储的是非法的地址
3、脏数据,存储的是其它变量的地址
野指针比空指针的危害性更大
1、空指针可以通过if(NULL==p)判断出来,但是野指针一旦产生,无法通过代码判断,只能通过经验人为判断
2、野指针就算暂时不暴露问题,不代表没有问题,后期可能随时暴露
如何避免产生野指针:
所有的野指针都是人为造成的,因此想要避免野指针的危害,只能通过不人为制造野指针
1、定义指针变量时一定初始化
2、函数不要返回局部变量、块变量的地址,因为当函数执行结束后,该地址指向的内存就会被自动销毁回收,如果非要接收,就接受到了一个野指针
3、与堆内存配合的指针,当堆内存手动释放后,该指针要及时置空
int* p = malloc(4); *p = 100; free(p); // 释放了对堆内存4个字节的使用权,此时p就是野指针 p = NULL; if(NULL==p)
作业:
1、实现一个函数,用于交换两个int变量的值,并调用它实现一个int类型数据排序函数
#include <stdio.h> void swap_int(int* p1,int* p2) { printf("swap:%p %p\n",p1,p2); int temp = *p1; *p1 = *p2; *p2 = temp; } int main(int argc,const char* argv[]) { int n1 = 10,n2 = 20; printf("main:%p %p\n",&n1,&n2); swap_int(&n1,&n2); printf("n1=%d,n2=%d\n",n1,n2); }
2、实现一个函数,用于计算两个正整数的最大公约数和最小公倍数
#include <stdio.h> int max_min(int n1,int n2,int* min) { if(NULL == min) { printf("参数有误\n"); return 0; } int max = 1; for(int i=2; i<=n1; i++) { if(0 == n1%i && 0 == n2%i) { max = i; } } for(int i=n1*n2; i>=n1; i--) { if(0 == i%n1 && 0 == i%n2) { *min = i; } } return max; } int main(int argc,const char* argv[]) { int n1 = 3,n2 = 6; int min = 0; int max = max_min(n1,n2,&min); printf("max=%d,min=%d\n",max,min); }