C语言学习笔记(四)——内存管理

开始今天的课程吧ヾ(◍°∇°◍)ノ゙

1.作用域

#include<stdio.h>
int a=2;//文件作用域
int main()
{
    int a = 0;//函数作用域
    {
        int a=1;//代码块作用域
        printf("a=%d/n,a");//现在输出1,代码变量会把函数变量和全局变量屏蔽
    }
    printf("a=%d/n,a");//此时输出a=0,若将函数作用域注释掉,输出a=2,再注释掉文件作用域就会报错(局部变量全局变量同名,全局会被屏蔽掉)
}
//不同 作用域的变量名称可以相同

自动变量与静态变量

首先是自动变量:

int main()
{
    auto signed int a=0;//在代码块中,直接写int会自动默认auto,自动出现在内存中,一般都省略了
    register int b = 0;//出现在寄存器中
    static int c = 0;//静态变量是指内存位置在程序执行期间一直不改变的变量。一个代码块内部的静态变量只能被这个代码内部访问,静态变量是程序刚加载到内存就出现,所以和定义静态变量的大括号无关,一直到程序结束才从程序消失,同时静态变量的值只初始化一次
}

下面写一个例子:

int test()
{
int a=0;
static b=0;
a++;
b++;
printf("%d,%d/n",a,b); 
}
int main ()
{
    for(i=0;i<10;i++)
    test();//此时输出的a值全为1。而b的值为12345...因为每次test的大括号内容执行完毕,a的值就会消失(默认auto),但是静态变量的值和大括号是否执行完毕无关,且只初始化一次,因此值一直增加。
}

下面来看代码块之外的静态变量

#include<stdio.h>
int b = 0;//全局变量
static int a = 0; //全局变量的存储方式和静态变量相同,但可以被多个文件访问,定义在代码块之外的变量就是全局变量,全局变量即使不在一个文件中也不能重名,可以在另一个文件中用extern int a声明,编译的时候一起编译就行(对于文件外的函数也可以用extern声明,例如extern test();可以省略extern()。没有static所有的函数默认是全局的。代码块之外的静态全局变量只能在定义它的文件内部访问,对于外部的其他文件是不可使用的(函数前加static,函数是静态函数,也不能在文件外被调用)。C语言默认写在代码块的函数或变量都是全局的。C语言中extern int b;//明确声明了一个变量,一定不是定义变量。int b ;//如果这个变量定义过了,这里就代表声明,如果没定义过这里就是定义。

int main()
{
    printf("a=%d\n",a);
    return 0;
}

2.代码区的静态区与栈区

代码区code,程序被操作系统加载到内存的时候,所有的可执行代码加载到代码区,也叫代码段,这段内存不能在运行期间修改,只能执行。
静态区是程序加载到内存的时候就确定了,程序退出的时候从内存消失。所有的全局变量和静态变量在程序运行期间都占用内存。
栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参,函数的返回值都是由编译器自动放出栈中,当一个自动变量超出其作用域时(大括号),自动从栈中弹出。下面是个例子

#include<stdio.h>
void test (int n)
{
    printf("%p,n=%d\n",&n,n);
    if (n<10)
    test(n+1);
    printf("%p,n=%d\n",&n,n);

}
int main ()
{
    test (0);
    return 0;
}

结果输出如下(注意观察地址):
这里写图片描述

不同的系统栈的大小是不一样的,即使相同的系统,栈的大小也是不一样的,windows程序在编译的时候就可以指定栈的大小,linux栈的大小是可以通过环境变量设置的。
堆heap和栈一样,也是一种在程序运行中而已随时修改的内存区域,但没有栈那样的先进后出的顺序。
堆是一个大容器,它的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。

#inchude<stdio.h>
int a=0;
static int b = 1;

int main()
{
    static int c = 2;
    int d = 3;
    int d = 4;
    printf("%p,%p,%p,%p,%p",&a,&b,&c,&d,&e);
    //输出的abc 在一块,d不在一块 d e 在一起,都在栈中。

}

3.堆的分配和释放

1malloc

void * malloc(size_t _Size);

需要

#include<stdio.h>
#inchude<stdlib.h>
int main()
{
    char *s = malloc(10);//在对重分配了10个字节的空间
    一个栈里面的自动指针变量指向了一个堆地址空间
    strcpy(s,"abcd");
    printf("%s\n");
    free(s);
    s=malloc(100);//因为s是自动变量(auto),可以重新使用,这个时候重新又指向了一个新的堆空间
    free(s);//free(s)并不是把变量s释放了,而是释放了s指向的那块内存空间
    int a[10];//定义了一个数组,这个数组在内存的栈里面。
    一个程序的栈大小是有限的,如果一个数组特别大,会导致栈溢出,所以在程序中不要用太大的数组(具体多大不确定,超出范围会报错)。如果一个数组定义的时候大小不能确定,适合用堆不适合用栈。
    int *p=malloc(100000000*sizeof(int));//使用这种方法防止栈溢出
    free(p);//记得free
    return 0;
}

动态变化大小的数组示例:

int main()
{
    int i;
    scanf("%d",&i);
    int *p=malloc(i*sizeofint());
    int a ;
    for(a=0;a<i;a++)
    {
        printf("%d\n",p[a]);//可以把这个指针默认为一个数组。
    }
    free(p);//如果malloc没有free,那么叫内存泄露,是非常容易犯的错误。
}

1.1函数返回一个指针

#include<stdio.h>
#include<stdlib.h>
int *test()
{
    int a = 10;//a是一个auto变量,在栈里面
    return &a;
}
int *test1()
{
int *p = malloc(1*sizeof(int));
*p=10;
return p;
}
char *test2()
{
//char a[100]="hello";//函数不能直接返回一个auto类型的地址
char *a = malloc(100);
strcpy(a,"hello");
return a ;
}
char *tesr3(char *arg)
{
    retuan arg;
}
char *test4(char *arg)
{
    return &arg[5];
}
char *test5()
{
    char *p=malloc(100);
    *p='a';
    //*(p+1)='b';
    //*(p+2)=0;//最终输出ab没有问题
    p++;
    *p='b';
    p++;//这个时候会报错,因为在此过程中p本身的值发生改变了,不再指向首地址了,那么free(p)向后释放100个字节内存时,向后多释放了2个字节(没分配的空间被释放了)。
    *p=0;

}
int main()
{
    //int *p = test();//test内部的变量a已经不在内存了,所以p指向一个无效的空间(会warning,但是还是能打印出a的值)
    int *p = test1();//不会warning,记得free
    char *p1 = test2();//此时,a是一个局部的变量(auto),调用完之后在内存消失了。p1指向无效的内存,所以是一个野指针。所以需要用堆
    char a [100]="hahaheiheiqieqie";
    p2=test3(a);//没有问题
    p3=test4(a);//没有问题
    printf("%d,%s,%s,%s\n",p,p1,p2,p3);
    free(p);
    free(p1);
    return 0;
}

很有很多返回值为指针的情况,写在一起太混乱了,额外写一个程序吧

include<stdio.h>

char *test()
{
    static char a[100]="hello";//静态的变量,在静态区,所以可用
    char *p=a;
    p++;
    //return p;//此时的输出是有效的、
    return a;
}
const char *test1()
{
    const char *s ="hello";//意思是将s指向一个常量的地址,常量在程序运行期间是一直有效的。
    return s;
}
const char *test2()//如果把const去掉,就是把一个常量的地址给一个变量的地址,函数定义的地址和返回的地址类型不符(修改这个常量的值,不会报错,但是运行会出错),常量区静态区类似,程序运行期间有效,但常量区是只读的。
{
    return "hello world";//和test1一样是合法的没有问题,返回的是一个常量的地址。
}
int main()
{
    char *str = test();
    const char *str1 = test1();

    printf("%s%s\n",str,str1);
    return 0;
}

2free

void free(void *p);

free负责在堆中释放malloc分配的内存。参数p为malloc返回的堆中的内存地址

3calloc:

void * calloc(size_t _Count, size_t _Size);

calloc与malloc类似,负责在堆中分配内存。Malloc只分配,但不负责清理内存,
calloc分配内存的同时把内存清空
第一个参数是所需内存单元数量,第二个参数是每个内存单元的大小(单位:字节),calloc自动将分配的内存置0
int p = (int )calloc(100, sizeof(int));//分配100个int
用malloc分配的内存,用memset清空,用calloc分配的不需要额外清空。
这四个函数都在

4realloc

重新分配用malloc或者calloc函数在堆中分配内存空间的大小。

void * realloc(void *p, size_t _NewSize);

第一个参数 p为之前用malloc或者calloc分配的内存地址,_NewSize为重新分配内存的大小,单位:字节。其实可以用malloc实现,但是realloc如果当前的空前拓展时,后面的空间被占用了,无法拓展时,它会自动找一个新的空间,然后自动释放原空间,比较方便智能。
成功返回新分配的堆内存地址,失败返回NULL.
如果参数p等于NULL,那么realloc与malloc功能一致
Realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。

4.堆栈的使用

例如两个字符串操作strcat,两个字符串长度不确定,此时用栈无法满足,可以用堆,用malloc(strlen(a)+strlen(b)+1) 不用sizeof,+1是为了放最后的0。记得free
小总结(把课程PPT写到这里算一个记录吧)
论空间分配速度:
栈区速度要快于堆区。
使用栈时,是直接从地址读取数据到寄存器,然后放到目标地址;
使用堆时,第一步将分配的地址放到寄存器,然后取出这个地址的值,然后放到目标地址。大概是这样,堆的数据读出要多一

论空间访问速度:
    对于CPU来说是一样的,都是一个直接寻址过程。

1.每个线程都有自己专属的栈(stack),先进后出(LIFO)
2.栈的最大尺寸固定,超出则引起栈溢出
3.变量离开作用范围后,栈上的数据会自动释放
4.堆上内存必须手工释放(C/C++),除非语言执行环境支持GC
栈还是堆?
明确知道数据占用多少内存
数据很小
大量内存
不确定需要多少内存

Code Area:程序代码指令、常量字符串,只可读
Static Area:存放全局变量/常量、静态变量/常量
Heap:由程序员控制,使用malloc/free来操作
Stack:预先设定大小,自动分配与释放
堆栈的大小是动态变化的,静态区程序加载后就不变了
这里写图片描述
下面是栈中常犯的错误

const char test()
{
    const char a[]="hello";//错误的,即使限定数组a是个常量,但它始终处于栈中,大括号结束,地址就无效了。
    return a ;
}
int main ()
{
    const char *s =test();
    printf ("%s\n",s);
    return 0;
}
const char test()
{
    static char a[]="hello";//数组a在静态区里面,地址是一直有效的。
    return a ;
}
int main ()
{
    const char *s =test();
    printf ("%s\n",s);
    return 0;
}

对于如下代码:
这里写图片描述
可以得到在堆栈内存的映射关系
这里写图片描述
注意list_buf变量本身在栈中,但是用malloc分配的空间是在堆中
再看一个代码
这里写图片描述
这里写图片描述

栈的原理:
这里写图片描述
栈顶从高地址向低地址方向增长
存储非静态局部变量、函数参数、返回地址
int abc(int a,int b){}//c语言形参是从右到左入栈的,先入栈b再入栈a (其他语言不一定支持)

再用下面的代码和图分析一下栈
这里写图片描述
首先可以看到数组a放在静态区
这里写图片描述
这里写图片描述
注意free(b)只是释放了在堆中的那段内存,b本身还是在栈中的。

5.通过指针形参分配堆内存的说明

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char *s)
{

    strcpy(s,"hello");
}
int main()
{
    char *p = calloc(10,1);//堆中分配了10个char 这样的执行结果是正确的

    test(p);
    printf("%s\n",p);
    free(p);
    return 0;
}
int main()
{
    test("hello");//错误,相当于在栈里面有i="hello",是个常量,不能改变
    return 0;
}

那么换一种方法是否还是正确的呢?

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char *s)
{
    s = calloc(10,1); 
    strcpy(s,"hello");
}
int main()
{
    //char *p = calloc(10,1);//堆中分配了10个char 这样的执行结果是正确的
    char *p = NULL;
    test(p);
    printf("%s\n",p);
    free(p);
    return 0;
}

上述的代码是特别容易犯的错误。
这里写图片描述
代码在调用函数时,p的值并没有改变,p依然是空的,那么首先printf无法输出,而free(p)也free一个空的指针,而s的内存在执行完就泄露了,而s本身在调用完之后就不见了,再也无法找到这段内存。
那么如何对上面的代码改进呢?使用下面的代码,改变了指针的级数,改成了二级指针。

include<stdio.h>
include<stdlib.h>
include<string.h>
viod test (char **s)
{
    *s = calloc(10,1); 
    strcpy(*s,"hello");//strcpy的第一个参数是指针,所以用*s
}
int main()
{
    //char *p = calloc(10,1);//堆中分配了10个char 这样的执行结果是正确的
    char *p = NULL;
    test(&p);
    printf("%s\n",p);
    free(p);
    return 0;
}

为什么上述的代码就可以用了呢?可以参见下图:
s的值为0x123,然后当分配一段堆内存后,*s就代表0x123这块内存的值,此时,p就由空变成了0x100成功指向了堆。
这里写图片描述

操作系统分配内存的最小单位说明
在windows系统下

#include <stdio.h>
#include <stdlib.h>
#pragma warning(disable:4996)//在VS中防止scanf等warning
int main()
{
    while (1)
    {
        char *s = malloc(1024);
        getchar();//执行到这里暂停一下
    }
    return 0;
}

编译运行上述程序,在任务管理器中找到它,按四次回车,内存值才会变化,从任务管理器可以发现它占用的内存为504 508 512 516 520,也就是每次堆的变化是4K,如果需要1k空间,操作系统会给4K,如果要5K,操作系统会给8k;
4K就是内存最小页。减少了内存的操作,效率增加,只是会浪费一些内存。
在实际程序中,malloc(1024)和malloc(4*1024)占用的内存是一样的,写成后面这种可以提高效率

猜你喜欢

转载自blog.csdn.net/qq_21747841/article/details/78225265
今日推荐