内存布局
数据存储位置
前面学过的变量和常量全部定义一下,分别打印地址,简单的区分一下他们的存储位置
#include<stdio.h>
int a;
int b = 10;
static int c;
static int d = 10;
int main(void){
int e;
int f = 10;
static int g;
static int h = 10;
char* str = "helloworld";
int arr[] = {1,2,3,4};
int* p = arr;
printf("未初始化全局变量:%p\n",&a);
printf("初始化全局变量:%p\n",&b);
printf("未初始化静态全局变量:%p\n",&c);
printf("初始化静态全局变量:%p\n",&d);
printf("未初始化局部变量:%p\n",&e);
printf("初始化局部变量:%p\n",&f);
printf("未初始化静态局部变量:%p\n",&g);
printf("初始化静态局部变量:%p\n",&h);
printf("字符串常量:%p\n",str);
printf("数组:%p\n",arr);
printf("数组指针:%p\n",p);
printf("指针地址:%p\n",&p);
}
运行结果:
可以简单的总结:
存放在数据区的变量,基本以0040开头
其中,初始化的变量,基本以004040开头
未初始化的变量,基本以004070开头
存放在栈区的变量,基本以0028FF开头
这样的总结不是用来说明什么样的变量,是以什么样的地址开头的,因为程序每一次运行,打印的地址都不一样。
这样的总结的目的是为了说明栈区和数据区,在地址上是有很大分别的,
而在数据区的数据中,初始化数据和未初始化的数据,在存放地址上,也是有很大区别的
上图中需要注意的是,字符串常量的地址,和其他的数据区地址区别比较大,这是因为字符串常量是存放在数据区的常量区,而其他的是存放在数据区中的变量区
定义常量的时候
const int a = 20; 这是安全的常量定义方式
我们经常说常量是不安全的,只是针对于函数里面定义的常量,像:
int main(){
const int a = 20; 这是不安全的常量定义方式
}
内存分区
C代码经过预处理、编译、汇编、链接可以生成一个可执行程序。在Windows下,size命令可以列出一个二进制可执行文件的基本情况
text表示代码区大小
data表示数据区初始化数据和静态数据的大小
bss表示数据区未初始化数据的大小
dec表示可执行文件的八进制大小
hex表示可执行文件的十六进制大小
其实内存不止前面说到的4个区(代码区,数据区,栈区,堆区)
除了0-255是系统占用的之外,内存的高位是被注册表占用的,比如一个视频文件,默认使用暴风影音还是迅雷播放器,都是属于注册表信息
一个应用程序所占的四区:
代码区:存放CPU执行的机指令,通常代码区是可以共享的(即另外的执行程序可以调用他),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外的修改了他的指令
全局初始化数据区/静态数据区(data):存储的数据都是和程序同生共死的,该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)
未初始化数据区(bss区):存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行前被内核初始化为0或者NULL
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存中,除了根据可执行程序分出的代码区text,数据区data和未初始化数据区bss之外,还额外增加了栈区,堆区
栈区:栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值,返回值,局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间
堆区:堆是一个大容器,他的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间,一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
栈区存储模型
代码区 常量区 data区 bss区存放数据都是从低地址到高地址存放,但是栈区是不一样的,看代码
#include<stdio.h>
void swap(int a,int b){
printf("swap a = %p\n",&a);
printf("swap b = %p\n",&b);
int temp = a;
a = b;
b = temp;
}
int main(void){
int a = 10;
int b = 20;
printf("a = %p\n",&a);
printf("b = %p\n",&b);
swap(a,b);
}
运行结果为:
因为以上变量都是局部变量,所以都是存放在栈区的,
a的地址比b的地址位高,所以在栈区存放变量是从高位到低位
int类型的a和b,前后脚入栈,但是步长并不是4,这是因为系统分配的时候故意隔开一段距离的,为了安全,怕坏人顺藤摸瓜乱改数据,造成问题
然后方法swap入栈,函数参数入栈方式是从后向前入栈,所以参数b先入栈,参数a后入栈,
可以看到b变量和参数b变量中间还隔着一段地址,这段地址就是存放函数信息的。
堆区内存的分配和释放
malloc 从堆空间中分配地址
#include <stdlib.h>
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
size:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL
free 释放堆空间中的地址
#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
参数:
ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
返回值:无
使用范例:
#include<stdio.h>
#include<stdlib.h>
int main(void){
int* p = (int*)malloc(sizeof(int)); 开辟堆空间存储数据
*p = 123; 使用堆空间
printf("%d\n",*p); 运行结果 123
free(p); 释放堆空间
}
堆空间释放之后,来看一下p这个指针还能不能用?
#include<stdio.h>
#include<stdlib.h>
int main(void){
int* p = (int*)malloc(sizeof(int));
printf("%p\n",p);
*p = 123;
printf("%d\n",*p);
free(p);
printf("%p\n",p);
*p = 456;
printf("%d\n",*p);
}
以上代码,是在堆内存在释放之后,依然使用p指针进行操作,运行结果也能打印出来地址和*p,
因为p还是指向这块内存的,但是这块内存已经被释放了,可以说,这块内存现在是一个未知区域,p也就是一个野指针了,操作野指针,有可能出错也有可能不出错
所以一般情况下,将内存free之后,也会将指针指向NULL
p = NULL;
使用堆空间创建一个数组
#include<stdio.h>
#include<stdlib.h>
int main(void){
int* p = (int*)malloc(sizeof(int)*10); 开辟一个10*4的内存空间
for(int i = 0;i<10;i++){
*(p+i) = i; 给每一个内存空间赋值
}
for(int i = 0;i<10;i++){
printf("%d\n",*(p+i)); 打印
}
free(p); 释放空间
p = NULL;
}
在堆空间中存放10个随机数,并排序
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define MAX 10
void sort(int* src,int len);
int main(void){
srand((size_t)time(NULL)); 创建一个随机数种子
int* p = (int*)malloc(sizeof(int)*MAX); 开辟堆空间 10*int大小
for(int i = 0;i<MAX;i++){
*(p+i) = rand() % 100; 给每一个空间赋值一个100以内的随机数
printf("%d ",*(p+i));
}
printf("\n");
sort(p,MAX); 给堆空间里的10个数排序
for(int i = 0;i<MAX;i++){
printf("%d ",*(p+i)); 打印
}
printf("\n");
free(p); 释放空间
p = NULL;
}
排序函数
void sort(int* src,int len){
for(int i = 0;i<len-1;i++){
for(int j = 0;j<len-i-1;j++){
if(src[j] > src[j+1]){
int temp = src[j];
src[j] = src[j+1];
src[j+1] = temp;
}
}
}
}
以上代码在最后打印的过程中使用的是指针加偏移量来获取存在堆空间中的数值的,试想一下,如果用以下这种打印方式,会有什么问题呢?
int main(void){
srand((size_t)time(NULL));
int* p = (int*)malloc(sizeof(int)*MAX);
for(int i = 0;i<MAX;i++){
*(p+i) = rand() % 100;
printf("%d ",*(p+i));
}
printf("\n");
sort(p,MAX);
for(int i = 0;i<MAX;i++){
printf("%d ",*p); 每打印一次,指针自增一次
p++;
}
printf("\n");
free(p); 最后释放指针指向的内存空间
p = NULL;
}
这样打印其实是有问题的,因为随着每次打印,指针自增,到最后用完回收指针的时候,指针指向的地址已经不是开辟堆空间的时候分配的地址,
就像是你去酒店办入住人家给了你一把钥匙,等你退房的时候,还回去的是另一把钥匙,这是行不通的,
这样的话,free的是另一片空间,而一开始开辟的空间成了无主空间
内存操作函数
memset()
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
s:需要操作内存s的首地址
c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
n:指定需要设置的大小
返回值:s的首地址
使用范例:创建10个连续的int类型的堆空间,并全部初始化为0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
int* p = (int*)malloc(sizeof(int)*10);
for(int i = 0;i<10;i++){
printf("%d ",p[i]); 运行结果:全是乱码,因为没有初始化
}
printf("\n");
memset(p,0,40); 执行初始化为0,最后一个参数为什么是40?
因为他的单位是字节,10个int类型对应的是40字节e
for(int i = 0;i<10;i++){
printf("%d ",p[i]); 运行结果 10个0
}
printf("\n");
free(p);
p = NULL;
}
运行结果:
试想,既然可以初始化为0,那么是不是可以初始化为任何数呢?
将上面的代码初始化的值从0改为1
memset(p,1,40);
运行结果为
可以看到,两次的打印结果全为16843009
还是因为memset改变的单位是字节,一个整型4个字节,
如果每个字节都改为1的话,每个字节的值就是0x01 0x01 0x01 0x01
换算为十进制就是16843009
所以memset是可以将申请的内存初始化为任何值,但是只有初始化为0的时候,才能达到我们预期的结果。
试想,memset可以初始化栈内存中分配的内存的值吗
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
char ch[10]; 申请一块栈空间
memset(ch,'A',10); 将每一个字节,改变为A
for(int i=0;i<10;i++){
printf("%c ",ch[i]);
}
}
运行结果:
memcpy()
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
dest:目的内存首地址
src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
n:需要拷贝的字节数
返回值:dest的首地址
使用示例:将一个栈内存初始化好的数组复制到堆内存中
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = (int*)malloc(sizeof(int)*10);
memcpy(p,arr,sizeof(int)*10);
for(int i=0;i<10;i++){
printf("%d ",p[i]); 运行结果:1 2 3 4 5 6 7 8 9 10
}
free(p);
p=NULL;
}
以前学过一个字符串操作函数strcpy(),两者有什么区别呢?
#include<stdio.h>
#include<string.h>
int main(void){
char ch[] = "hello \0world";
char str[100];
strcpy(str,ch);
for(int i=0;i<13;i++){
printf("%c ",str[i]);
}
}
运行结果为:
可以看到,hello是正常的,后面就全是乱码了,因为strcpy函数复制的内容遇到\0就停止了,后面的乱码其实是没有复制上
如果使用memcpy呢?
#include<stdio.h>
#include<string.h>
int main(void){
char ch[] = "hello \0world";
char str[100];
memcpy(str,ch,13);
for(int i=0;i<13;i++){
printf("%c ",str[i]);
}
}
运行结果:
可以看到,全复制上了,包括中间的一个空格和一个\0,所以memcpy赋值内容只和字节数有关。
再看下面的例子:
使用memcpy函数将栈内存中的数组中的元素拷贝到其他的角标下
#include<stdio.h>
#include<string.h>
int main(void){
int arr[] = {1,2,3,4,5,6,7,8,9};
memcpy(&arr[3],&arr[5],12); 目的地址为角标3,源地址为角标5,拷贝3个整型内容
也就是将6 7 8 拷贝到原4 5 6的位置
for(int i = 0;i<9;i++){
printf("%d ",arr[i]);
}
}
运行结果:
虽然运行成功了,但是这样的拷贝方式源和目的内存发生了重叠,有可能会出错
就像之前说的,野指针可能会导致报错,但也有可能不报错
那用什么办法解决呢?
memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,
memmove()仍然能处理,不过执行效率比memcpy()低些。但是他是安全的
memmove()会在内存中开辟一块空间,先把src要复制的内容放进这个空间里,然后再把这个空间里的内容放进dest里面
使用示例
#include<stdio.h>
#include<string.h>
int main(void){
int arr[] = {1,2,3,4,5,6,7,8,9};
memmove(&arr[3],&arr[5],12);
for(int i = 0;i<9;i++){
printf("%d ",arr[i]);
}
}
运行结果:
memcmp()
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
s1:内存首地址1
s2:内存首地址2
n:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0
使用示例
#include<stdio.h>
#include<string.h>
int main(void){
int arr1[] = {1,2,3,4,5,6,7,8,9};
int arr2[] = {1,2,3,4,5};
int value = memcmp(arr1,arr2,20); 比较20个字节,也就是5个数是否相同
printf("value = %d\n",value); 运行结果为0
}
比较字符串:
#include<stdio.h>
#include<string.h>
int main(void){
char ch1[] = "hello \0 world";
char ch2[] = "hello \0 world";
int value = memcmp(ch1,ch2,13);
printf("value = %d\n",value); 运行结果为0
}
内存常见问题
内存越界之后的释放问题
如果在函数中直接写:
char ch[11] = "hello world";
直接数组下标越界,因为字符串末尾会有一个\0,总共占12个字节
如果是在堆内存中定义这样的字符串呢?
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(void){
char* p = (char*)malloc(sizeof(char)*11);
strcpy(p,"hello world");
printf("%s\n",p); 打印结果:hello world
}
程序运行是没问题的,但是没有free堆内存,我们在最后面加上
free(p);
p=NULL;
运行发现程序报错了,不free还没事,一free就报错,这是为什么呢?
因为一开始开辟了11个字节的内存,但是使用了12个字节的内存,free的时候到底是要free多少个字节内存?
free11个字节?没有free干净,free12个字节?没有开辟这么大空间,怎么释放。
所以,在写代码过程中,应尽量避免内存越界的情况
开辟内存大小不规范问题
int* p = (int*)malloc(0);
思考:这样开辟内存空间,有没有问题?
int main(void){
int* p = (int*)malloc(0); 开辟大小为0的内存空间
printf("%p\n",p); 可以打印出p的内存地址
*p = 100; 也可以给*p赋值
printf("%d\n",*p); 可以打印*p的值为100
free(p); 这里会出错
p = NULL;
}
因为定义的p指针没有实际大小,所以p就是一个野指针,无法free一个没有实际大小的指针
再看下面的定义方式
int* p = (int*)malloc(10);
int类型的指针,却开辟了10个字节,一个int类型4个字节,也就是开辟了两个半的int内存空间
这样的开辟内存方式,运行起来会怎么样呢?
int main(void){
int* p = (int*)malloc(10);
p[0] = 123;
p[1] = 456;
p[2] = 789;
printf("%d\n",p[0]); 打印结果为123
printf("%d\n",p[1]); 打印结果为456
printf("%d\n",p[2]); 会报错
free(p);
}
因为开辟的两个字节空间,不够存放4个字节的整型数据,
所以在堆内存开辟空间的时候尽量写成sizeof(int)*4这种方式,易懂而且不易出错
int* p = (int*)malloc(sizeof(int)*4);
堆内存多次释放的问题
int main(void){
int* p = (int*)malloc(sizeof(int)*10);
free(p); 第一次释放的是正常开辟的空间
free(p); 第二次释放的就是野指针了,操作野指针是不对的
}
所以说堆内存不可以多次释放
int main(void){
int* p = (int*)malloc(sizeof(int)*10);
free(p);
p = NULL;
free(p); 不会报错
free(p);
free(p);
}
空指针可以多次释放
所以释放内存的时候要多加注意
指针叠加之后的释放问题
看下面代码,开辟堆内存,给每一个内存空间赋值
int main(void){
int* p = (int*)malloc(sizeof(int)*10);
for(int i = 0;i < 10;i++){
*p = i;
p++;
}
free(p); 会出错,因为for循环内p自增了,此处的p已经不是开始开辟空间时候的p了
p = NULL;
}
所以通过指针操作对应的堆空间的时候,尽量不要动开辟空间返回的指针,需要动的时候,给做个备份指针,操作备份指针就可以了
int main(void){
int* p = (int*)malloc(sizeof(int)*10);
int* temp = p; 指针备份
for(int i = 0;i < 10;i++){
*temp = i;
temp++;
}
free(p);
p = NULL;
temp = NULL;
}
指针的值传递和地址传递问题
看看下面的代码有什么问题
void fun(int* p){
p = (int*)malloc(sizeof(int)*10);
}
int main(void){
int* p= NULL;
fun(p);
for(int i=0;i<10;i++){
p[i] = i;
}
free(p);
p = NULL;
}
在main函数中,定义了一个p空指针
然后调用fun将p传进去,在fun中,给p开辟了内存空间,接着fun结束,
开始给p的每个内存赋值,然后释放资源,
看起来没有任何问题,是因为忽略了函数入栈和出栈的问题
重新理一遍:
main中定义一个p指向NULL
栈中开始加载fun函数,会重新定义个fun参数p
给fun中的p分配内存,函数结束,出栈,fun函数中的p被销毁
main函数中,p仍然为NULL,所以后续的操作就都是错的,不能操作NULL指针,
上面的整个过程,就是因为给fun传的参数是值传递,main给了个指针p,fun一个指针p接着
想要程序不出错,可以使用地址传递,main给出一个指针p的地址,fun用一个二级指针接着,看代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void fun(int** p){ 这边用一个二级指针来接着
*p = (int*)malloc(sizeof(int)*10);
}
int main(void){
int* p= NULL;
fun(&p); 将地址传进函数中
for(int i=0;i<10;i++){
p[i] = i;
}
free(p);
p = NULL;
}
打印地址来验证一下:使用值传递方式
void fun(int* p){
printf("fun1 %p\n",p); 00000000
p = (int*)malloc(sizeof(int)*10);
printf("fun2 %p\n",p); 00542E80
}
int main(void){
int* p= NULL;
printf("main1 %p\n",p); 00000000
fun(p);
printf("main2 %p\n",p); 00000000
}
可以看到结果,fun结束之后,main2打印的还是空指针的地址
而使用地址传递之后:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void fun(int** p){ 这里用一个二级指针的形参接着
printf("fun1 %p\n",*p); 这里打印的还是一个空指针的地址
*p = (int*)malloc(sizeof(int)*10); 给一级指针分配内存
printf("fun2 %p\n",*p); 打印一级指针的地址
}
int main(void){
int* p= NULL;
printf("main1 %p\n",p);
fun(&p); 穿给函数指针p的地址,也就是一个二级指针
printf("main2 %p\n",p); 打印一级指针的地址
}
运行结果:
打印结果可以看出,地址传递最后是可以达到想要的效果的
当然,如果觉得地址传递有点绕的话,还可以使用别的方法:返回值
int* fun(int* p){
return (int*)malloc(sizeof(int)*10);
}
int main(void){
int* p= NULL;
p = fun(p);
for(int i=0;i<10;i++){
p[i] = i;
}
for(int i = 0;i<10;i++){
printf("%d ",p[i]);
}
}
运行结果:可以正常打印 0,1,2,3,4,5,6,7,8,9
练习
在堆内存中开辟空间存储5个人3门成绩
思考:分析之后,需要建立一个二级指针,5个int大小,
二级指针的内容存放一级指针的地址,一级指针为3个int大小,内容存放3门课程的成绩
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(void){
//开辟空间
int** p = (int*)malloc(sizeof(int)*5); 开辟一个5个int大小的空间,将他定义为二级指针
for(int i=0;i<5;i++){
p[i] = (int*)malloc(sizeof(int)*3); 给二级指针的5个空间赋值,值为新开辟的3个int大小的指针
}
//赋值
for(int i=0;i<5;i++){
for(int j=0;j<3;j++){
scanf("%d",&p[i][j]);
}
}
//打印
for(int i=0;i<5;i++){
for(int j=0;j<3;j++){
printf("%d ",p[i][j]);
}
printf("\n");
}
}