这是之前学习过的B站的mycodeSchool的指针教学视频,这篇文章是边学习边记录的一篇文章,个人感觉讲的超级棒,对指针的教学真的是深入浅出,超级推荐大家学习,下方是视频链接
学习视频链接:https://www.bilibili.com/video/BV1bo4y1Z7xf/?spm_id_from=333.337.search-card.all.click&vd_source=33fb97de2a9fae8bce351df45c7d3074
我是以每P来记笔记,进行学习的,大家可以参考B站的每P,对照我的笔记进行观看,比如学习第1p,那么就查看01p
注意了
我记笔记是在语雀进行记录的,上传到CSDN,有些格式错误,如果大家想要PDF版本,可以留下邮箱,那个要好看一些,格式也是正确的
要理解指针,首先要理解不同的数据类型或者不同的变量在计算机的内存中是如何存储的?
计算机的内存(RAM 随机存储器)
段或区,在内存中都代表一个字节,作为一个典型的内存系统,每个字节都有一个地址
当程序中声明一个变量时,比如 int a,定义一个整型变量a
当这个程序执行的时候,计算机会为这个特定的变量分配一些内存空间,具体分配多少内存空间,取决于数据类型,还取决于编辑器。
计算机会有一个内部结构,一张查找表,保存着变量a的信息
int a;
char c;
a++;
指针是一个变量,它存放着另外一个变量的地址
比如有4个字节的一块内存,从地址204开始存放着变量a,另外的一个变量p,它的类型是指向整型变量的指针,这个变量p可以存放a变量的地址
int a;
int *p; //一个指针变量,指向一个整型,换句话说是指向整型变量的地址的变量
为了在p中存放a的地址,需要
p = &a;
//取a的地址 把&放在变量的前面就得到了这个变量的地址
//实际上返回一个指针,指向哪个指定的变量
int a; //a所分配的地址为204
int *p; //p所分配的地址为64
p = &a; //取a的地址
a = 5;
printf p; //204
printf &a; //204
printf &p; //64
如果把一个*放在指针变量的前面,那么就会得到这个指针所指向地址的值。称之为解引用。
printf *p; //5 = a的值
//在p中存放着一个地址,使用解引用操作来获得这个地址中的值
*p = 8; //相当于修改了变量a的值,因为修改了p指向的值,而p又是指向a的地址
printf *p; //8
printf a; //8
当说到指针变量的时候,变量p的值,指的是变量p的地址
总结
int p; //把放在变量名的前面
int p= &a; //把&放在变量名的前面 得到了a的地址
不使用*,或者不使用*对它进行操作,所做的操作都是对它的地址进行操作
使用*时,对它进行操作时,是在操作指针所指向的那个地址的值(内容)
指针是一个变量,它用来存放其他变量的地址。
指针也可以指向用户自定义的结构体,或者指向用户自己定义的类
示例1
指针p没有初始化,并且使用了指针p,出现error,编译阶段出现问题。
即野指针问题
示例2
指针p初始化,指向变量a
每次重新执行程序时,p的地址都会不一样,会给他分配一个新的地址
而*p为负值的原因是因为p指向的变量a未初始化,*p时一些随机值
示例6
当把b的值赋给*p时,指针p会指向b吗?
可以看到指针并没有指向b,只有值改变了,也就是把b值付给了*p,改变了指针指向变量a的地址的值
示例8
指针p,对指针进行加减操作,加或减的是sizeof(变量),会得到下一个整型变量的地址
比如:下图,一个整型的大小是4个字节,为了得到下一个整型数的地址,会跳过4个字节。所以p+1会增加4个字节。
示例9
p+1指向了一个随机值,实际上这是一个垃圾值
我们并没有为这个特定的内存地址分配一个整型变量
所以这个操作很危险,可能会访问
指针是强类型的,意味着需要一个特定类型的指针变量来存放特定类型变量的地址
int * --> int
char --> char*
指向整型的指针来存放整型数据的地址
指向字符类型的指针来存放字符型数据的地址
如果有一个用户自定义的结构体或者指针,那就需要这种特定类型的指针
我们不仅仅使用指针来存储内存地址,同时也用它来解引用那些地址的内容
这样就可以访问和修改这些地址所对应的值了
每个字节在内存中都可以寻址,一个整型变量的4个字节一般都是连续排列的
第一位是符号位,剩下来的31个位用来存储值
float类型参考IEE-754,和其他类型存储数据不同,打印出来的值也不同
示例2
p0是指向字符型的指针,而p是指向整型的指针
把p强置类型转成指向字符的指针,把p的地址存入p0,那么最右边字节(即首字节)的地址将会被存入p0
可以看到下图程序运行结果,由于char类型1个字节,所以p0存储的是int类型的首字节的内容
当解引用p0时,机器会认为它是一个指向字符型的指针,字符型只有一个字节,所以机器只看一个字节
示例3
p+1,跳转到下一个整型变量的位置,即地址加sizeof(类型),整型的大小是4个字节,而p是一个指向整型的指针,而*(p+1)的值是随机值(垃圾)。我们未向这个地址中写入任何东西。
p0+1 --> 跳转到了int类型的第二个字节,通过二进制得出*(p0+1)=4
void类型的指针
p0的地址和p的地址相同,但是没有映射到特定的数据类型,只能打印出其地址,而不能对其进行算数操作,会出现编译错误
总结:
指针类型,类型转换,指针运算
p=&a; p=(char) p0;
跟指针指向变量的地址和变量的类型(字节大小)有关。
p+1; p0+1;
*(p+1); *(p0+1);
p0=p; 不会有编译错误
*p0; 没有映射到特定的数据类型,不能对它进行解引用
是否可以创建一个指针指向变量p(p是一个指向int型的指针)?
创建一个指针变量q,它用来存放p的地址; int **q;
这样q就是一个指向指针的指针
这样q里面存放了p的地址,q指向p,q的类型是**
这里面的关系就是
为了得到x的地址,需要一个int类型的指针p
为了得到p的地址,需要一个指向int类型的指针,所以加*,即int **
/****** 地址对应内容
x 地址-->225 内容 5
p 地址-->215 内容 225
q 地址-->205 内容 215
r 地址-->230 内容 205
*********/
int x=5;
int *p; //p指向x p的地址和x相同
p = &x;
*p = 6;
int **q; //创建了一个指针变量q 指向p
q = &p; //得到了p的地址
int ***r; //r的类型是int ***,因此可以用来存放int **类型的地址
r = &q;
/****** 解引用
*p 6
*q 225
*(*q) 6
*r 215
*(*r) 225
*(*(*r)) 6
*********/
总结
指针,二级指针,三级指针
int x;
int *p=&x;
int **q=&p;
int ***r=&q;
***p=**q=*r=x //解引用相同,都等于x
问题引入,值传参,并不能改变a的值
原因,InCrement里的a是局部变量,main函数里的a也是局部变量,main函数中调用InCrement(a)传入的a只是对a=10,做了一个拷贝。
InCrement里的a改变了,main函数里的a并没有改变。
可以看到a在InCrement函数和在main函数里的地址不同
函数里的在栈中,运行完就消失
main函数里的存在堆中,函数的自增对堆里的a无影响
程序开始运行时,内存中到底发生了什么
当一个应用程序开始启动的时候,计算机会设置一些内存,给这个程序使用
代码段,静态/全局变量段,栈段,这三个是固定的,当程序开始运行的时候,就已经确定了的,但是应用程序在运行时,可以要求在堆区分配为它分配更多的内存
应用程序所使用的内存,通常被划分为四个部分,
第一部分,代码段 用来存储程序的指令
计算机需要把指令加载到内存(比如程序中的自增语句,他们都是串行指令,都会是内存中的一部分)
第二部分,静态/全局变量段 分配静态或全局变量
(如果我们不是在函数中声明变量,就是全局变量,全局变量在程序的任何地方都可以访问和修改)
第三部分,栈(stack) very important
局部变量都放在这个地方
而局部变量,只在特定的函数或者特定的代码块进行访问和修改
第四部分,堆(heap)
程序运行前:代码区,静态/全局变量区,文字常量区
程序运行后:栈区(系统自动分配)和堆区(程序员申请)
程序运行的详细过程:
调用栈或函数调用栈
如果一个函数无限次调用另一个函数,就像是无限递归,那么栈将会溢出,程序会崩溃
了解概念或印象,当一个函数去调用另一个函数时,会发生什么事情
当做函数调用的时候,本质上是把一个变量映射到另一个变量,一个变量里的值拷贝到另一个变量,这种被称为传值调用(call by value)
解决方法:
传址,可以引用这个变量,解引用并且做一些操作,这就是传引用(call by reference)
传引用可以节省很多内存空间
避免复杂数据类型的拷贝可以让我们节省内存
数组在内存中的存储方式
线性存储,数组内元素的地址都是线性递增的
指针本意就是地址,指向整型变量x的地址
如果指向x后,在执行p+1,会因为相邻的整型变量的值是未知的,而出现error
解决方法
如果把指针p指向数组的首元素的地址
这样再执行p+1,p+2… 就可以解引用成功 因为数组在内存中线性存储,我们知道相邻地址里有什么内容
int A[5];
int *p;
p = &A[0];
print P; //200
print *p; //2
print p+1; //204
print *(p+1) //4
如果我们使用数组A等于指针p
还是得到了一个指向数组首元素的指针
int A[5];
int *p;
p=A;
print A; //200
print p; //200
print *A; //2;
print A+1; //204
print *(A+1); //4
所以对于数组中索引是i的元素,为了取得这个特定元素的地址或值
可以使用
7
取地址
&A[i] 或者 A+i
取值
A[i] 或者 *(A+i)
7
数组元素的首地址也可以被称为数组的基地址
可以使用
A 或者 &A[0] 表示数组的首地址
注意
当指针指向数组的基地址后,不可以执行A++
因为A本质上是一个数组,数组首地址应当是不变的,不允许更改
而可以执行p++,因为p是一个int 类型的指针变量
总结:
线性存储 数组内元素的地址都是线性递增的
数组名A 或者 &A[0]得到数组的首地址
或者使用int *p=A; p也表示的是数组的首地址
int *p =A;
*(A) //数组第一个值 *(A+i) //数组的第i个值
*p *(p+i)
数组作为函数参数传入
获取数组的个数
int A[] = {1,2,3,4,5};
int ArraySize = sizeof(A)/sizeof(A[0]); //数组中元素的个数
但是当传入参数,只传数组
结果就会出现error
可以看出在sum函数和main函数里sizeof(A)的数组大小不同
为什么呢?
在sum函数中数组A是一个局部变量,并不是main函数里的数组A
当调用函数时,main函数里的数组A会被拷贝到被调用的函数中
同时在栈中main函数栈帧中的数组A会占据20个字节,拷贝到sum函数的数组A同样也会占据20个字节,其中的元素和main函数里的数组A相同,但事实不是这样
当编译器看到整个数组A作为函数参数的时候,它不会拷贝整个数组
实际上编译器所做的事情是在SOE栈帧中创建一个同名的指针A而不是创建整个数组
编译器只是拷贝主调函数的数组首元素的地址
所以SOE的数组参数不是被解释成数组,而是一个整型的指针
这就是为什么在SOE函数里sizeof(A)等于4的原因了 而在main函数里是一个数组
我们不是拷贝变量的值,而仅仅是拷贝变量的地址,所以这里是传引用,而不是传值
数组总是作为引用来传给函数
如果每次拷贝整个数组,将会浪费大量的内存
所以数组充当函数参数时,本质上是一个指针
这样所得的结果也是相同的
数组作为函数参数时,所返回的本质上是一个数组指针,方式为传引用
因为数组作为函数参数,本质上是一个指针,所以可以在函数内部修改数组的值
数组名 --> 指针常量
总结
size = sizeof(A)/sizeof(A[0])
字符串和字符数组的区别
看末尾是否有\0
“hello" 字符数组 “hello\0” 字符串
字符串必须以’\0’结束 '\0’表示NULL
之所以字符数组很重要,是因为我们用它来存储字符串,然后做一些操作,比如修改,拷贝字符串,连接两个字符串或者找出字符串的属性(找出字符串的长度)
首先明白
如何把字符串传入字符数组 声明并初始化字符数组首先的需求就是字符数组必须足够大,字符数组究竟要多大呢
一个足够大的字符数组的大小大于等于字符的数量+1
字符数组的大小 >= 字符的数量+1
printf函数终止默认最后的NUL字符,即\0,字符串终止符号
在string.h库里,所有的函数都是假定字符串是以\0结尾的
比如求出字符数组的长度,计算长度默认到\0为止
例如:
char c[20]="JOHN";
这会把字符数组c初始化为字符串并且加上\0
这对于字符串字面值来说是隐式的,总是在内存的最后加上一个NUL字符
这种情况下c的大小将会是 5,5个字节,每个字符一个字节
char c[] = "JOHN";
但是字符数组的长度为5
但是长度为4 因为strlen函数并不会计算\0(NUL字符)
数组和指针看起来相似,但是并不是一个类型
char c1[6]="hello";
char *c2;
c2=c1; //c1代表c1字符数组的首地址
print c2[1]; //数组的第二个元素 e
c2[0] = 'A'; //修改后c1就是 "AELLO"
c2[i] -- > *(c2+i) //c2是指向了c1的首地址 c2[i]就相当于c2+i的偏移
c1[i] -- > *(c1+i)
注意
c1 = c2; //invallid
c1 = c1 + 1 //invalid
//invalid 因为c1是数组的首地址 数组的首地址不允许更改 会产生编译错误
c2 = c1; //valid
c2 = c2+1; //c2指向下一个元素
必须明白,什么时候是数组,什么时候是指针,两者的区别以及我们分别可以做什么。
示例
字符数组当函数参数传入的是地址
%c 打印字符
字符串以\0结束
c[i]和*(c+i)是等同的
下图也可以打印出字符数组
为什么呢?
print函数里的 char C 想当于指针,指向了main函数里C【20】字符数组的首地址
所以print函数里的C等于main函数里的c[0]
C++,地址自增,当最后一个字符是\0时,循环终止
总结
字符串 以\0结束
字符数组用来存放字符串
char c[] = “hello”;
char c[20] = “hello”
char c[20]; c1=‘h’; c2=‘e’;c3=‘l’; c4=‘l’; c5=‘o’; c6=‘\0’;
char c[] = {‘h’,‘e’,‘l’,‘l’,‘o’,‘\0’};
%s 打印字符串 %c打印字符
数组作为函数参数的使用 传引用 传的是数组的首地址
char c[20] = “hello”;
大小 sizeof© -->6
长度 strlen© -->5
字符串常量和常量指针
当在写程序的时候,当执行程序时,我们应该总是能够想出来变量放在哪,或者数据放在哪
以及变量或数据的范围放在哪里
栈是一块连续的内存
栈区用来存放函数执行时的信息,以及存放所有的局部变量
讨论指针时应该知道在内存中发生了什么?
任何函数被调用时,都会在栈区开辟一块空间,用来执行那个函数,成为栈帧。
栈
函数的局部变量都会被分配到栈帧,除了局部变量,栈帧还有一些其他的信息
当调用函数的时候,被调函数会分配响应的栈帧,该栈帧会在调用函数main之上,栈顶的函数会先被执行,main函数暂停运行(这里可以看成中断).
指针变量,典型情况下,大小占据四个字节
当被调函数运行结束,被调函数的栈帧就会被清除
main函数会恢复运行直至结束
修改代码如下
定义的不是字符数组,而是一个字符指针
当使用字符数组来进行初始化的时候 字符串就会存在于系统分配给这个数组的内存空间中
这种情况,会被分配到栈上
当定义一个指针指向字符串后,此字符串
存放于
应用程序的代码区
char *p =“helloworld” 1.申请了空间(常量区),2.存放了字符串 3.在字符串后加了\0 返回的地址,赋值给了指针p
但是当定义为字符数组时,就可修改
char *C 指向字符数组c的首地址
故可以修改
当我们定义函数,允许读,但是不允许写
这时就可以定义一个指向常量(只读)字符的指针
const char *c;
总结
指针变量,典型情况下,大小占据四个字节
而字符指针进行初始化,会被分配到代码区(不可写)
*const char c;
指针作为函数参数的深层含义
int A[5];
int *p=A;
只是用数组名A,那么在表达式中,会返回一个指向数组首元素的指针
**可以把数组名当作指针来使用 **
也可以对数组名进行解引用和算术运算
但是和指针变量是不一样的(eg: 可以p++ 不能A++,因为数组名A是数组的基地址)
*(A+i) --> A[i];
A+i --> &A[i];
二维数组在内存中的分配
B[0]和B[1]每个都有三个元素,
B[0]和B[1]不是一个整型数据,而是具有3个整型的一维数组
int B[2][3];
B[0] B[1] //每个都有三个整型数据
int *p = B; //编译错误
//B返回的是一个指向一位数组(其中包含三个整型数据)的指针
指针的类型是很重要的,在解引用时和对它进行算术运算时,不同类型的指针运算结果不同
定义一个指向一维数组的指针(其中一维数组包含3个整型数)
int (*p)[3] = B; //这样赋值是可以的
print B; //和B[0]地址相同 B --> &B[0]
print *B; //和B[0]的值相同,返回三个整型数据 也和B[0][0]的地址相同
B --> &B[0]
*B --> B[0] (包含三个整型数据) &B[0][0]
B[0]是存放三个整形数据的一维数组的名字 所以等同于 B[0][0]的首地址
print B; //400 B返回一个一维数组 包含三个整型
print *B; //400
print B+1; //412
print *(B+1); //412
前提:
int B[2][3];
*int (p)[3] = B; //B --> &B[0]
结论:(自己思考一下)
B = &B[0]
*B = B[0] = &B[0][0]
B+1 = &B[1]
*(B+1)= B[1] = &B[1][0];
*(B+1)+2 = B[1]+2 = &B[1][2];
*(B+1) =(B[0]+1)= *(&B[0][0]) = *(&B[0][1])
这里的B就是一个指向一维数组的指针(有三个元素)
B --> int (*)[3];
**B[0] --> int ***
总结:
int B[2][3]; int (*p)[3]=B;
指针的类型是很重要的,在对指针进行解引用和算术运算时,不同指针类型所得到的结果不同
有时间可以多看看10P
学习了如何使用指针对二维数组进行操作
那如何对多维数组进行操作
同时如何把多维数组作为参数传递给函数
多维数组本质上是数组的数组 很重要
数组可以理解为同类型事物的集合
多维数组可以理解为数组的集合
B[2][3]是一维数组的集合
我们有2个一维数组
其中每个的一维数组都有3个整型元素
他们被分配在连续的内存当中
B是一个二维数组 是一个一维数组(大小为3)的数组
所以*B返回的是一个指向包含3个元素的一维数组的指针 int (p)= B;
*B和B[0]相同,获得了一个完整的一维数组B[0]
B[0] 会返回一个整型指针,指向B[0]的第一个元素B[0][0],即B[0]的地址等于B[0][0]
指针类型的作用
解引用和进行指针算术的时候起作用
B[i][j] = *(B[i]+j)= ((B+i)+j)
什么是指针的指针 什么是数组的数组? 留个悬念
int C[3][2][2];
int (*p)[2][2] = C;
就是二维数组的数组
解引用
C返回一个指向二维数组的指针 但本质上不是指针 C本身是一个数组 这两个类型不同
案例
想想,套娃套娃,一步一步,数组的数组
每次运行,每次运行的时候分配到栈上的内存会发生变化
二维数组作为函数参数
第一种定义方式
第二种定义方式
**error **
对任意维度的数组作为函数参数都是如上,除了第一个维度除外,其余维度都是强制需要给定的
了解
内存的架构
操作系统如何管理内存
以及作为 程序猿应该如何管理内存
动态内存 在c\c++中
代码区(code区,存放需要执行的指令(instuctions)
静态/全局变量区(static/Global区),存放静态或者全局变量(global 不在函数中声明的变量),生命周期贯穿整个应用程序,在应用程序运行期间都可以访问它们。
栈(stack),用来存放函数调用的所有信息(functions Call)和所有的局部变量(local varibles)函数返回地址,局部变量是在函数内部声明的,只在函数执行期间存活。栈后进先出。
这三个区在程序的运行期间的大小是不会增长的
这三个区在程序执行的过程中是如何使用的?
**一个函数的栈帧大小,是在编译期间就决定的 **
在程序执行期间,任何时候都是栈顶的函数在执行,其他的函数会被暂停(中断),等待上面的函数返回一些东西后再恢复执行
嵌套函数调用(嵌套中断)
栈顶出栈
当被调用的函数结束的时候,我们会回到中断的地方(即函数调用的地方),之前被调用函数的栈帧将会被消除(销毁),其他函数恢复执行。
直至main函数结束,程序终止。
最后全局变量也会被销毁。
通常只有当一个变量,需要被很多函数调用,在整个程序的生命周期要存在,否则定义全局变量就是浪费。
程序开始执行的时候,操作系统分配了一些内存
假设os分配了1MB的内存来作为栈,但实际的栈帧和局部变量的分配是在运行时
如果我们的栈增长超出了预留内存的大小。
就会产生栈溢出(stack overflow)
这种情况下程序会error
eg: 写了有问题的递归函数导致的无穷递归
因此,栈有缺陷
内存中预留给栈的空间,在运行期间并不会增长,应用程序不能在运行期间请求更多的栈
假设预留的是1mb,那么当分配给变量和函数的栈大小超过1MB的时候,程序就会崩溃
内存在栈上的分配和销毁都有一定的规则
当一个函数被调用的时候,它被压入堆栈,当结束调用时,弹出堆栈
如果变量是在栈上分配的,那就不能操作变量的范围
另外的一个限制,如果我们要声明一个很大的数据类型,比如一个很大的数组作为局部变量,我们在编译期间就需要知道他们的大小
如果我们,需要在程序运行期间根据参数决定数组的大小,那使用栈就会出现问题
解决方法:
比如分配很大的内存,或者把变量预留在内存中直到我们想用为止
这个时候就需要使用到堆(heap)
应用程序的堆是不固定的,他的大小在应用程序的整个周期中是可变的,也没有特殊的规则来分配和销毁响应的内存,程序猿完全可以控制。例如在堆上分配多少内存,几乎可以任意使用堆上的内存,只要不超出系统自身的内存限制。
但是随意使用堆也是很危险的
有的时候把堆成为内存的空闲池(free store)或者空闲内存区
我们可以从堆获得我们想要的内存
操作系统如何实现堆的方式可能会很不一样,这是系统架构的事情
但是从程序猿的角度来看,仅仅是一块可以自由使用的内存,可以根据需要来灵活的使用
堆也被称之为动态内存,使用堆意味着动态内存分配
堆也是一种数据结构,和数据结构中的堆没有关系,这里的堆只是用来表示空闲的内存池
为了在堆中使用动态内存,我们需要四个函数
/*-------------C语言:-------------------*/
malloc();
calloc();
realloc();
free();
/*-------------C++语言:-------------------*/
new();
delete();
示例1,使用c语言来动态分配内存
在堆中分配一个整型,预留或者获得堆上的一些空间,使用malloc函数
malloc函数 需要在堆上分配多少字节的内存
下就是在堆中分配四字节的内存
int *p;
p = (int*)malloc(sizeof(int));
当调用malloc函数,并且传入的值是一个整型的大小
这样malloc会在堆上申请和分配4个字节的内存
返回一个指向这块内存起始地址的指针
malloc返回的是一个void类型的指针
比如返回4字节的起始地址是200
malloc将会返回200
p是main函数的局部变量,将会被分配在main函数的栈帧中
使用了强制类型转换,因为malloc返回void类型的指针
现在在堆上就有了一块内存,可以用来存储一个整型
可是不知道在堆中的内存中存着什么,可以使用*p来解引用这个地址,然后写入值
使用堆上内存的唯一方式,就是通过引用
malloc函数仅仅所做的事情是在堆上找到空闲的内存,为你预留空间然后通过指针返回
而访问这块内存的方式就是自己定义一个指针,通过解引用的方式去访问这块内存
示例2
再次调用一次malloc,当再次使用malloc时
会在堆上再次分配一个内存,占四个字节
分配了另外一块地址,然后让p指向这块内存
之前分配的那块内存,依然还在堆上,消耗着堆的内存,不会自动回收
如果我们使用malloc在堆上分配和使用了一块内存,用完了就要释放出去,否则就会出现内存泄露(浪费内存)
故一旦使用完堆内地址是200的内存后,就要通过free()函数去释放这块内存
任何时候通过malloc函数分配的内存,最终都会调用free函数释放
使用free函数,把内存起始地址传入free
free(p指针);
故这样,第一块内存将会被释放,然后指向另一块内存
如果分配了内存,之后不再需要使用了,那么就要手动释放(程序猿的职责)
所以任何在堆上分配的内存,在函数调用结束之后并不会向栈一样自动释放
动态分配的内存需要我们手动释放
动态分配的内存并不像全局变量一样存在于程序运行的整个生命周期,可以由程序员来选择什么时候去手动释放堆上的内存。
示例3
如果我们想在堆上分配一个数组
那么如下
只需要传入总的数组的大小字节数
int *p = (int*)malloc(20*sizeof(int));
//在堆上分配一个长度为20的整型数组空间
//堆上会分配一个连续的并且足够存放20个整型的内存
//会得到这块内存的首地址
//这样就可以使用了 p[1] p1[2]
//*p = p[0] //p --> &p[0]
//*(p+1) = p[1]
如果malloc找不到空闲的内存块,就不能在堆中成功分配内存,会返回NULL
示例4:使用c++来动态分配内存
在c++中不用使用类型转换,而在c中malloc返回的是一个void指针
c++中new和delete是安全的
它意味着,他们是带着类型的,返回指向特定类型的指针
总结:
栈有缺陷 操作不当栈会溢出 栈中的变量的范围不能改变
动态分配内存 大小在应用程序的整个周期中是可变的,也没有特殊的规则来分配和销毁响应的内存,程序猿完全可以控制。
malloc() calloc() realloc() free() c语言
new delete c++
动态分配内存的概念
应用程序中堆和栈的含义以及区别
malloc函数详解 很详细
void* malloc(size_t size)
size_t类型类似于(unsigned int)
size 内存块的字节数大小 是一个正整数
大小不能是负数,可以是0和一个正数
返回一个void指针,这个指针指向了堆中分配给我们内存块的第一个字节的地址(首地址)
使用malloc,可以分配一些内存,在内存中预留一些内存
在内存中保存一些数据
实际上在分配内存时,会首先计算我们需要多少内存
通常会计算sizeof()返回的大小*需要的单元数量
总共需要的字节数是: 单元的数量*每个单元的字节数
int *p = (int *)malloc(sizeof(int)) //在堆上分配一个整型数据的内存空间
free(p);
p = malloc(int *)malloc(sizeof(int)*20) //分配连续的20个整型数据的空间
变量的大小取决于编译器,所以应该使用malloc来计算类型的大小
动态内存的操作是基于指针的,会返回一个指向堆中分配的内存的基地址
void* calloc(size_t num,size_t size)
第一个参数是特定类型的元素的数量,第二个参数是类型的大小
如果想在堆中分配三个整型数据的空间
int *p = (int *)calloc(3,sizeof(int))
malloc和calloc都可以在堆区开辟空间
malloc和calloc之间的区别
mallo分配完内存后并不会对其初始化,因此如果没有向申请到的内存中填充数据的话,将会的到一些随机值
使用calloc,其中会初始化并且赋初始值,填充值为0
realloc函数用法解释_Luv Lines的博客-CSDN博客
修改申请到的动态内存的大小
void* realloc(void* Ptr,size_t size)
:::info
第一个参数Ptr,指的是向已分配内存的起始地址的指针
第二个参数size,指的是新的内存块的大小
:::
realloc的使用场景
机器会创建一块新的内存把原来的值拷贝过去
那么可能会直接扩展之前的那块内存
声明一个数组,但是这个数组是用户想要的数组
如果不首先声明数组的大小,就会出现error
enter n;
int A[n]; //会编译错误 必须实现知道数组的大小 否则会erro 括号里的值不能是一个变量
可以使用动态内存分配
于是拥有了一个大小是n的数组
使用malloc
calloc和malloc的区别
如果不初始化
使用calloc函数生成的数组元素将会被初始化为0
但是如果使用malloc,数组元素里的就不会被初始化,数组里面是一些随机值
任何分配了的动态内存在程序结束之前会一直存在(占据内存)
除非显示的释放
使用malloc,calloc,realloc分配的内存,要使用free函数来释放内存
当free(A),A中的数据会被清除也可能不会被清除,取决于编译器或者机器
但是free()之后,那块内存就可以被另一个malloc来分配和使用
随机值出现
如果没有被free,打印初始化了的1,2,3,4,5
尽管使用free释放了内存,我们之后还是可以访问那块内存,这是使用指针的一个危险的地方
如果知道地址,可以查看地址中存放的值,但是你只应该去读写系统分配或者自己分配的内存
如果这个地址不是分配给你的,你不知道你读写的地址上是什么,不知道它的行为是什么,这完全取决于编译器和机器 — 这算是野指针?
free后 , 再向堆中内存访问并且赋值会出现什么?
可以看到内存地址中的值还是发生了改变
这取决于编译器,可能在其他机器中会崩溃
只用分配的内存,其他不属于你的内存不要使用。
假如有一块内存存放n个整型数据,然后我们想要扩展这块内存
比如大小翻倍或者减半
需求:内存大小翻倍或者减半
需要使用到realloc函数
int *A = (int*)malloc(sizeof(int)*n);
free(A);
int B = (int*)realloc(A,2*nsizeof(int)); //内存大小翻倍
free(B);
int C = (int*)realloc(B,(n/2)*sizeof(int)); //内存减半
free(C);
内存翻倍
然后把之前的内存块的内容拷贝进去
它的工作方式:如果请求的新块大于之前的块,如果可以扩展之前的块
如果能够在之前的块的基础上找到连续的内存,那么拓展之前的块。
否则就分配新的内存 ,把之前的块的内容拷贝过去,然后释放之前的内存
使用realloc修改内存的大小后
可以看到B还是指向了内存A的首地址
输入,打印输出,现在的内存地址和之前的内寸地址相同,仅仅是扩展了内存
前五个元素拷贝A的值
后五个元素是随机值
如果把数组的大小减半
那么之前的块就会减小
前两个元素拷贝过来了,实际上不是拷贝(他们本来就在那),其余三个被释放了
如果
int *A = (int*)malloc(sizeof(int)*n);
free(A);
int A = (int*)realloc(A,0); /
free(B);
大多数时候,我们会把realloc返回的地址赋给相同的整型指针
如果想让realloc和malloc一样的效果 realloc不初始化值,即初始化数组内部为0
就需要
int *A = (int*)malloc(sizeof(int)*n);
free(A);
int B = (int*)realloc(NULL,n*sizeof(int));
free(B);
这会创建一个新的内存块,而不会从之前的内存块中拷贝任何数据
因此传入恰当的参数,realloc函数可以作为free和malloc函数的替代
int B = (int*)realloc(NULL,n*sizeof(int)); -->malloc 相同的效果
int B = (int*)realloc(A,0); -->free(A); 释放数组A的内存
总结:
malloc() calloc() realloc() free()
new delete
malloc()在动态申请内存的时候,不会初始化
而calloc()会初始化为0
(int*)realloc(B,nsizeof(int))
A =(int)realloc(A,0) —> free(A);
(int*)realloc(NULL,n*sizeof(int));
动态分配内存的概念
什么是栈
什么是堆
分配个一个应用程序的内存,通常被分为4个段
代码区(code/test),静态/全局变量区(static/Global),栈(stack),堆(heap)
当不正确使用动态内存,可能导致内存泄漏
内存泄漏指的是我们动态申请了内存,但使用完了之后没有释放它
会由不正确的动态内存(堆)的使用而引起
time()函数的返回值作为参数传给了srand()
srand(time(NULL)) 种子值
然后rand()函数根据种子值生成一系列随机数
一个博彩游戏,使用C语言实现
play()函数
猜测的值是一个**字符数组 **
char C[3] = {“J”,“Q”,“K”};
存放在栈中,在程序运行期间不会变化
main函数
运行结果
game.exe消耗系统的内存
一直玩,可以看到内存并没有发生变化
当修改play()函数
把猜测的值 修改成指针类型
char C = (char)malloc(3*sizeof(char));
C[0] = ‘J’; C[1] = ‘Q’; C[2] = ‘K’;
这属于在堆上开辟了内存空间 动态的申请了内存
再次运行
会发现game.exe 占用的内存在随着我们输入的赌注而变化
我们来看看在程序的运行过程中发生了什么
任何函数调用结束的时候,之前分配的内存也会被回收,每个函数调用都对应一个栈帧,一旦调用结束,分配的内存就会被回收
**栈上任何东西的释放都无需程序猿主动释放 **
在函数调用结束的时候这一切都会自动发生
第二种情况,使用malloc在堆上分配内存
char C = (char)malloc(3*sizeof(char));
C[0] = ‘J’; C[1] = ‘Q’; C[2] = ‘K’;
在堆上开辟了一个数组,但是只是在栈上定义了一个字符指针
作为局部变量的字符指针指向堆上特定的内存块
任何堆上的内存都要通过一个指针变量来访问
当调用结束时,分配在栈上的内存就会被释放,但是堆上的内存的数据一直处于未引用的状态
将不会被释放
堆上的内存必须通过free来手动释放或者通过delete来释放
如果玩多次,就会多次在堆上申请空间,堆不是固定大小的
如果不释放不再使用在堆上的内存,就是在浪费宝贵的内存资源,应用程序内存消耗会随时间增长
所以内存泄漏很危险
任何未引用和未使用的堆上内存都是垃圾
在c/c++中,作为程序猿,我们必须确保堆上不产生垃圾,内存泄漏就是在堆上产生垃圾
但是其他语言,比如java和c#,堆上的垃圾会自动回收(自动回收机制)
如果在堆上申请的内存很大
而只使用前几个,如果不手动释放内存的话
将会占据很大的内存空间
所以调用free()函数
所以再次运行,进行输入,查看任务管理器,无论输多少次,game.exe文件都不会变化
因为使用了free,手动释放了在堆上的内存
总结
栈上的内存是自动回收的,栈的大小是固定的,最多只会发生栈溢出
指针只不过是另一种数据类型
一个指针存储了另一个数据的地址
因此函数返回一个指针也是允许的
什么场景下我们会想一个函数返回一个指针
一个实现两个数的加法
值传递值传递 实现两个数的加法
x,y,z都是main函数的局部变量
a,b,c都是add函数的局部变量
将main和add中的局部变量改成一样的名字
可以看出main函数中的a值拷贝到函数add
但两者的a,b不一样
打印这两个函数对应的a的地址,会发现,两者并不是同一个a,地址不同
即变量的名字对于某个函数来说是局部的
当在显示出结果前,打印helloworld
就会导致结果错误
为什么会这样捏?
当返回c的地址,也就是144,然后add就结束了
*ptr = 144;
add函数结束
ptr指针指向了地址为144的内存,但是它的值是不能保证的,因为这块内存(add函数分配的栈帧)已经被释放了。
然后调用helloworld()函数
函数的调用执行还是需要栈分配空间的
栈中的144地址的值被helloworld()函数覆盖了,所以144地址中的值不是6了
所以得到一些垃圾值
为什么在没调用helloworld()的时候就不会出现问题的,运气哈哈,,可能调用完add之后并没调用其他函数,所以机器还没重写144地址中的值
地址传入是没有问题的,因为在栈中被调函数始终在主调函数之上
任何时候被调函数被执行,主调函数仍存在栈的内存中,add执行时,main函数还是能保证在栈中,所以main函数中的变量的地址对于add函数来说是可以访问的
但是如果尝试返回一个被调函数的局部变量的给主调函数,就像要返回一个add函数的局部变量给main函数。当被调函数结束,返回主调函数的时候,此时被调函数的内存那块内存已经释放了
所以栈底向上传递参数是可以的,传递一个局部变量或者一个局部变量的地址也是可以的
但是从栈顶向下传是不可以的
什么情况下,我们会想要从函数返回一个指针呢?
比如在堆上有一个内存地址或者在全局区有一个变量,那我们就可以安全的返回他们的地址
因此堆上分配的内存需要显示释放,由我们来控制他们的释放(不是像栈一样自动释放的)
而全局区的任何东西,比如一个全局变量,他的生命周期是程序的整个周期
所以我们可以使用malloc或者c++的new运算符在堆上开辟内存
这样才是正确且安全的,因为返回的是堆上的指针
是由程序猿手动开辟,手动释放的
内存过程
因此返回堆中开辟内存的地址的值
任何在堆中的内存都要被显式的释放
因此从函数返回指针的时候,要注意他们的作用范围
必须保证地址没有被重用(用来存储其他东西),以及那个地址没有被清除
运用:链表
总结
函数指针用来存储函数的地址
使用指针存放其他变量的地址,基本上指针就是这样一种数据类型
指针指向或者引用内存中的数据(这里的数据也不一定是指变量,也可以是常量)
我们不仅使用指针来存放地址,来可以解引用
我们也可以使用指针来存放函数的地址
一个程序基本上就是一组顺序的计算机的指令的集合,任何需要被执行的程序都要编码为二进制格式
源码 -->机器代码 (可执行代码)
编译器会把源文件作为它的输入,生成一个包含机器码的可执行文件
可执行文件存储在磁盘上或者其他的存储设备中
当说内存的时候,指的是程序运行的上下文,即随机存储器(RAM),称之为主存
一般在讨论应用程序的内存的时候
程序开始运行的时候,会得到一块内存
程序运行结束的时候,他得到的内存将会被回收
实际上,当我们运行一个程序,,或者说程序运行开始的时候,会给他分配一些内存
代码段,是用来存放可执行文件拷贝过来的机器码或者机器指令的,指令不是在第二存储介质上(比如磁盘)直接运行的,首先要先拷贝到主存才能够执行。
不仅使用内存来存储指令,还要用来存放运行期间的很多数据
其他区段主要就是用来存储和管理数据的
一个函数就是一组存储在连续内存块中的指令
基本上一个函数是一组指令用来执行一个子任务
在内存中,一个函数就是一块连续的内存(里面是指令)
函数的地址,也称之为函数的入口点,他是函数的第一条指令的地址
机器语言中的函数调用基本上就是一条跳转指令
跳转到函数的入口点,跳转到函数的第一条指令
函数指针存放函数地址,函数指针存放了函数在内存中的起始地址或者说是入口点。
在c/c++中如何创建函数指针
int (*p)(int,int)
函数指针中声明的参数类型,要和指向的函数的参数是一样的
初始化函数指针,填入函数的地址
p = &Add; //把Add的地址返回给p 初始化函数指针 填入函数的地址
传值
使用了一个函数指针来引用一个函数
使用*解引用来获得这个函数
c = (*p)(2,3); // *p 就相当于 Add函数
如果这么写
int *p(int,int); //声明了一个函数 这个函数返回一个整型的指针
不使用取地址符也可以对函数指针初始化,只使用函数名也可以返回函数的地址
“”可以理解为一个指针,是指针常量,指向常量区的字符串,值等于字符串的地址
总结
函数指针可以用来做为函数的参数
接收函数指针的这个函数,可以回调函数指针所指的那个函数
也可以这么写
因为函数A,函数名A本身返回的就是指针
一个函数的引用传递给另一个函数的时候,那个函数称之为回调函数,比如这里的A
函数B可以通过函数指针(ptr)来回调A
数组绝对值升序排列
在compare比较函数中,带比较的函数是通过引用来传递的,他们的地址通过指针来传递
qsort函数能够对任何数组进行排序,不仅仅是整型数组,需要我们自己给出比较逻辑
回调的思想
事件处理
计算机内存视为字节数组,每个字节都有一个唯一的地址
计算机的内存是字节可寻址的
可寻址的最小数据对象是一个字节
ARM Cortex-M微处理器,每个内存地址都有32位,可以总共寻址4GB
数据对象可能占用内存中的多个字节
例如 一个字在内存中占据4个字节
有两种不同的格式可以存储一个字
最小端,最大端
最小端格式存储一个字时,最高有效字节存储在高位地址,最低有效字节存储在低地址
以最大端格式存储一个字时,最高有效字节存储在低位地址,最低有效字节存储在高位地址
指针的值仅仅是计算机中存储的某些变量的内存地址
如果变量占用内存中的多个字节,则变量的地址定义为它占用的所有字节的最低地址
引用运算符和解引用运算符
引用运算符&x,返回x变量的地址,&称为引用运算符或者地址运算符
解引用运算符*p返回指针p指向的变量的值
总结
引用运算符对变量起作用,可以读取变量的地址,&var即是var的地址
解引用运算符可以处理指针,可以读取指针指向变量的值
字符数组的指针做数学运算
ptr++,加的是sizeof(类型)
整型指针做数学运算
ptr = ptr + sizeof(int);
如何建立一个指向给定的特定内存地址的指针
例如,GPIOA的输出数据寄存器的存储器地址,是由微控制器的设计师在设计的时候确定的
以便建立一个指向它的指针,这样软件就可以轻松访问输出数据寄存器
将常量地址转换为指针,指向一个无符号位的32位整数,然后使用解引用运算符访问指向的值
例如设置GPIOA5为高电平
使用宏定义定义一个指针,此宏将内存地址强制转换为指针,所以可以直接解引用此地址的值
将解引用运算符直接放到宏里面,这样软件可以直接使用解引用的指针来访问内存
最为常用
如果强制编译器每次都读取新值,添加关键字volatile
可以防止编译器在编译过程中进行错误的优化
关于STM32 Cortex-M的处理器的设备头文件中,**外设的内存地址,被强制转换指向一个结构体 **
使用宏将此内存地址强制转换为指针,该指针可以GPIO类型的结构体,这样就可以通过这种方式修改数据输出寄存器
使用结构体,软件可以更加轻松的访问外设的所有寄存器
完结撒花,奥利给!!! 用时5天,其中混合着linux的学习!!!
这是在黑马程序猿学习的指针教学视频,比较浅显,索性一起汇总啦
指针可以间接访问内存
- 定义指针 数据类型 * 指针变量名
- 使用指针 指针前 可以加 * 表示解引用 找到指针指向内存中的数据
#include
using namespace std;
int main()
{
// 1. 定义指针
int a = 10;
int *p = &a;
cout << "a的地址为:" << &a << endl;
cout << "指针p为: " << p << endl;
// 2. 使用指针
// 可以通过解引用的方式来找到指针指向的内存 可以修改也可以访问
// 指针前 可以加 * 表示解引用 找到指针指向内存中的数据
*p = 1000; //指针p通过解引用找到了a的地址 修改a的值为1000
cout << "a的值为:" << a << endl;
cout << "*p的值为:" << *p << endl;
system("pause");
return 0;
}
注意哈,在相同的操作系统下,不管何种数据类型的指针所占字节都是相同的
比如,在64位操作系统中,int * 占8字节,float * 占8字节,double * 占8字节
int *p
在32位系统下,占4位内存空间
在64位系统下,占8位内存空间
#include
using namespace std;
int main()
{
// 指针所占内存空间
int a = 10;
int *p = &a;
// 64位 占8字节
cout << "sizeof(int *) = " << sizeof(int *) << endl;
cout << "sizeof(p) = " << sizeof(p) << endl;
// 在相同的操作系统下,不同数据类型的指针所占字节都是相同的
cout << "sizeof(char) = " << sizeof(char *) << endl;
cout << "sizeof(flaot) = " << sizeof(float *) << endl;
cout << "sizeof(double) = " << sizeof(double *) << endl;
cout << "sizeof(long) = " << sizeof(long *) << endl;
system("pause");
return 0;
}
空指针指向的内存是不可以访问的
#include
using namespace std;
int main()
{
// 1. 空指针用于给指针变量初始化
int *p = NULL;
// 2. 空指针不可以进行访问
// 0-255 之间的内存由系统占用 因此不可以访问
*p = 100;
cout<<*p<<endl; //没有权限
system("pause");
return 0;
}
#include
using namespace std;
int main()
{
//在程序中,尽量避免野指针出现 即指针指向非法的内存空间
int *p = (int *)0x1100;
cout << *p << endl; //访问野指针报错
system("pause");
return 0;
}
const 后跟变量,就称这个变量为常量。
const 修饰指针的三种情况
const int *p = &a;
#include
using namespace std;
int main()
{
int a = 10;
int b = 20;
const int *p = &a;
cout << "为修改指向前*p的值为:" << *p << endl;
p = &b; //常量指针可以修改指向
cout << "修改指向后*p的值为:" << *p << endl;
// *p = 30; 报错 常量指针 不可修改值
system("pause");
return 0;
}
int * const p=&a;
#include
using namespace std;
int main()
{
int a = 10;
int b = 20;
int *const p = &a;
//指针常量可以修改值,不可以修改指针的指向
*p = 100;
cout << "*p = " << *p << endl;
// p = &b; 报错 不可以修改指针的指向
system("pause");
return 0;
}
const int _ const _p=&a;
#include
using namespace std;
int main()
{
int a = 10;
int b = 20;
const int *const p = &a;
// *p = 100; 报错 不可以修改指针的值
cout << "*p = " << *p << endl;
// p = &b; 报错 不可以修改指针的指向
system("pause");
return 0;
}
利用指针访问数组元素
#include
using namespace std;
int main()
{
// 利用指针访问数组中的元素
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "数组的第一个元素的值为:" << arr[0] << endl;
int *p = arr; //指针p指向数组的首地址
cout << "访问指针访问的第一个元素:" << *p << endl;
p++; //指针偏移了int类型变量的地址 即偏移地址4位
cout << "利用指针访问的第二个元素:" << *p << endl;
cout << "访问第三个元素的值:" << *(p + 1) << endl;
// 利用指针遍历数组
int *p1 = arr;
for (int i = 0; i < 10; i++)
{
cout << *(p1 + i) << endl;
}
system("pause");
return 0;
}
利用指针作为函数的参数,可以修改实参的值。
函数的形参为指针,可以减少内存空间。
#include
using namespace std;
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "a的值为:" << a << endl;
cout << "b的值为:" << b << endl;
}
void swap2(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
// 指针和函数
// 1. 值传递
// 值传递不会修改实参
int a = 10, b = 20;
swap1(a, b);
cout << "swap1 a的值为:" << a << endl;
cout << "swap1 b的值为:" << b << endl;
// 2. 地址传递
// 地址传递 可以修改实参的值
swap2(&a, &b);
cout << "swap2 a的值为:" << a << endl;
cout << "swap2 b的值为:" << b << endl;
system("pause");
return 0;
}
封装一个函数,利用冒泡排序,实现对整形数组的升序排序
例如数组,
#include
using namespace std;
void arraySort(int *arr, int length)
{
//冒泡排序
for (int i = 0; i < length - 1; i++)
{
for (int j = 0; j < length - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main()
{
int arr[10] = {4, 3, 6, 9, 1, 2, 10, 8, 7, 5};
int length = sizeof(arr) / sizeof(arr[0]);
// 排序前
cout << "未排序前:" << endl;
for (int i = 0; i < length; i++)
{
cout << arr[i] << " ";
}
cout << endl;
//排序
arraySort(arr, length);
//排序后
cout << "排序后:" << endl;
for (int i = 0; i < length; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}