C语言学习第012课——内存管理和内存操作函数
内存布局
数据存储位置
前面学过的变量和常量全部定义一下,分别打印地址,简单的区分一下他们的存储位置
#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赋值内容只和字节数有关。
memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,
memmove()仍然能处理,不过执行效率比memcpy()低些。
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