一、C语言篇——08. 内存管理

01. 数据类型

数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。

数据类型基本概念:

  • 类型是对数据的抽象;
  • 类型相同的数据具有相同的表示形式、存储格式以及相关操作;
  • 程序中所有的数据都必定属于某种数据类型;
  • 数据类型可以理解为创建变量的模具: 固定大小内存的别名;

typedef关键字

typedef的作用:

  1. 给数据类型起别名
  2. 用来区分数据类型
  3. 提高移植型
#include <stdio.h>  //标准i input输入 o output输出
#include <string.h> //对字符串处理函数  strcat  strstr  strcmp  strcpy
#include <stdlib.h> //malloc  free

//typedef  1.可以起别名
struct Person
{
    char name[64];
    int age;
}
typedef struct Person myPerson;  //myPerson 就是别名

//mian函数,程序入口
int main()
{
    myPerson p = {"李白",18};
    
    //2.区分数据类型
    char* p1,p2;  //p1是char*类型,p2是char类型。但是很容易误解成他们都是char*类型
    char *p3,*p4; //p3,p4都是char*类型了
    
    typedef char* PCHAR;
    PCHAR p5,p6; //p5,p6都是char*类型了

    
    return 0;
}

void数据类型

void字面意思是”无类型”,不能用void创建变量,因为无法给无类型变量分配内存空间

void* 无类型指针,无类型指针可以指向任何类型的数据。

void的用途:

  • 对函数返回值的限定;
  • 对函数参数的限定;
  • void* 万能指针的使用
#include <stdio.h>

//限定函数的返回值
void func01()
{
    return;  //不加void,return后面能写数据,加上void后,后面不能写数据
}

//限定函数参数
void func02(void) //不加void,能传实参。加上void后,不能传实参
{
    return;
}

int main()
{
    //func02(10); //err
    return 0;
}

注意:void* 万能指针。 多级指针、任意类型指针在32位系统上都是4位

#include <stdio.h>
int main()
{
    int* pInt = NULL;
    char* pChar = NULL;
    
    PChar = (char*)pInt;  //需要强制转换
    
    void* p = NULL;
    PChar = p; //万能指针,不通过强制转换就能换成其他类型指针
    
    return 0;
}

sizeof用法

sizeof本质:它不是一个函数,而是操作符

sizeof的返回值是,unsigned int无符号整型

sizeof的用途:数据类型大小计算、统计数组长度

	printf("%d\n",sizeof(int));  //如果后面是数据类型,则sizeof后面必须更()小括号
	double d = 3.14;
	printf("%d\n",sizeof d);  //如果后面的变量,则sizeof后面可以不加()小括号
#include <stdio.h>

//注意:当数组名做函数参数时,会退化成指针,指针指向的是数组的第一个元素。所以sizeof(arr)为4
void test01(int arr[])
{
    printf("%d\n",sizeof(arr)); //结果是4
}

int main()
{
    int arr[] = {1,2,3,4,5,6,7,8};
    test01(arr);
    retrun 0;
}

02. 作用域和声明周期

局部变量:也是auto变量(auto可省略),定义在函数内部的变量

作用域:从定义开始,到包裹该变量的代码块结束

生命周期:从变量定义开始,到函数调用完成。——当前函数


全局变量:定义在函数外部的变量

作用域:从定义位置开始,默认到文件内部。其他文件如果想使用,可以通过extern关键字 声明全局变量 将作用域导出

生命周期:程序启动开始,到程序终止结束。——程序执行期间


static全局变量:在全局变量定义之前添加 static关键字

作用域:限制在本文件内部,不允许通过 extern关键字声明导出到其他文件

生命周期:程序启动开始,到程序终止结束。——程序执行期间


static局部变量:在局局变量定义之前添加 static关键字

特性:静态局部变量只定义一次,在全局内存中。通常用来做计数器

作用域:从定义开始,到包裹该变量的代码块结束

生命周期:程序启动开始,到程序终止结束。——程序执行期间


全局函数:类型 函数名(形参) + 函数体

作用域:整个程序

生命周期:程序启动开始,到程序终止结束。——程序执行期间


static函数:static 类型 函数名(形参) + 函数体

作用域:static函数只能在本文件内部使用。

生命周期:程序启动开始,到程序终止结束。——程序执行期间

#include <stdio.h>

int variate02;   //全局变量,不赋初值,默认为0
static int svar02;  //静态全局变量

void fun01()  //全局函数
{
    return;
}

static void fun02()  //静态函数
{
    return;
}

int main()
{
    auto int variate01 = 10;   //局部变量
    static int svar01;  //静态局部变量,不赋初值,默认为0
    
    return 0;
}

03. 内存布局

由于Windows操作系统不开放源码,所以不清楚Windows平台下的内存怎么分布

Linux系统开发源码。所以可以看到内部实际存储

内存4区模型:

  • 代码段:.text段。程序源代码(二进制形式)
  • 数据段:
    • 只读数据段 .rodata段,存放 常量
    • 初始化数据段 .data段,存放 初始化为非0的全局变量和静态变量
    • 未初始化数据段 .bss段,存放 未初始化的全局变量和静态变量,程序加载执行前,会将该段整体赋值为0
  • stack:栈。空间小,系统自动管理、自动分配、自动释放。 特性是FILO(先进后出)。 Windows下默认是1M,可提升上限至10M。Linux下默认是8M,可提升上限至16M
  • heap:堆。空间大,用户自己管理、分配、释放。特性是FIFO(先进先出)。约1.3G+

内存4区图如下(Linux版)
Linux下内存四区图

04. 内存分区

程序运行之前

编写C程序,会进行4步的操作:

  1. 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
  2. 编译:检查语法,将预处理后文件编译生成汇编文件
  3. 汇编:将汇编文件生成目标文件(二进制文件)
  4. 链接:将目标文件链接为可执行程序

当我们编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:
Linux文件信息
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、**数据区(data)未初始化数据区(bss)**3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

  • 代码区(text段):存放 CPU 执行的机器指令(二进制),代码区是可共享的,也只读的
  • 全局初始化数据区/静态数据区(data段):存放被初始化的全局变量和静态变量,还有常量数据(如:字符串常量)
  • 未初始化数据区(bss段):存放未初始化的全局变量和静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

程序运行之后

运行可执行程序后,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区堆区

  • 栈区(stack):栈是FILO先进后出的内存结构,容量较小,存放函数的参数值、返回值、局部变量等,由编译器自动分配和释放。因此,局部变量的生存周期为申请到释放该段栈空间。
  • 堆区(heap):堆是FIFO先进先出的内存结构,容量很大,一般由程序员分配(malloc)和释放(free),若程序员不释放,程序结束时由操作系统回收。

05. 堆区管理函数

malloc() 和 free()

#include <stdlib.h>
void *malloc(size_t size);
功能:在堆区内存中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。
参数:需要分配内存大小(单位:字节)
返回值:
	成功:分配空间的起始地址
	失败:NULL

free()函数:释放空间

#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间。
参数:需要释放空间的首地址。
返回值:无

malloc()、free()的使用:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    //开辟堆空间
    int *p = (int*)malloc(sizeof(int)*10);
    if(p == NULL)
    {
        printf("malloc error\n");
        return -1;
    }
    
    //释放堆空间
    free(p);
    p = NULL;  //一般free完后,将该指针置为NULL
    
    return 0;
}

使用heap空间注意事项:

  1. free后,空间不会失效。通常free后,地址置为NULL
  2. free地址必须是 malloc申请地址,否则free时会出错。(如:不要p++)
  3. 如果malloc之后的地址需要变化,那么可以使用临时变量temp保存原地址,然后free(temp);

calloc()

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
功能:在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存置0。
参数:
	参1:所需内存单元数量
	参2:每个内存单元的大小(单位:字节)
返回值:
	成功,分配空间的起始地址
	失败,NULL
int *pp = calloc(10,sizeof(int));  //开辟10个int类型的堆区空间,并且置0

realloc()

#include <stdlib.h>
void *realloc(void *ptr, size_t size);
功能:重新分配用malloc()或calloc()在堆中分配内存空间的大小。
参数:
	参1:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
	参2:为重新分配内存的大小, 单位:字节
返回值:
	成功,新分配的堆内存地址
	失败,NULL
int *pp = calloc(10,sizeof(int));  //开辟10个int类型的堆区空间,并且置0

pp = realloc(pp,sizeof(int)*10);  //扩展堆区空间大小

注意:

  • malloc()和calloc()的区别是,malloc()不会置0,calloc()会置0
  • realloc()申请空间机制:
    • 如果原有空间的后续空间足够大,则直接在后续申请空间
    • 如果原因空间的后续空间不足,则会重新找一块内存,并将原有空间内的数据拷贝到新空间下

06. 存储类型操作函数

memset()

分配的内存空间内容不确定,一般使用memset初始化。

#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
	参1:需要操作内存s的首地址
	参2:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
	参3:指定需要设置的大小
返回值:s的首地址

memset()函数用法:

	int a[10];

	memset(a, 0, sizeof(a));
	memset(a, 97, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%c\n", a[i]);
	}

memcpy()

#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
	参1:目的内存首地址
	参2:源内存首地址,注意:dest和src所指的内存空间不可重叠
	参3:需要拷贝的字节数
返回值:dest的首地址

memcpy()函数用法:

	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10];
	
	memcpy(b, a, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d, ", b[i]);
	}

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个字节
参数:
	参1:内存首地址1
	参2:内存首地址2
	参3:需比较的前n个字节
返回值:
	相等:=0
	大于:>0
	小于:<0

memcmp()函数用法:

	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	int flag = memcmp(a, b, sizeof(a));
	printf("flag = %d\n", flag);

07. 就近原则

#include <stdio.h>

int m = 100;
int main()
{
    int m = 10;
    printf("m = %d\n",m);   //10
    //不建议这样写
    
    return 0;
}

不建议自己这样写。只需要知道有这么个原理,别人这么写代码时看的懂,就够了。
of(a));
printf(“flag = %d\n”, flag);


# 05. 就近原则

```c
#include <stdio.h>

int m = 100;
int main()
{
    int m = 10;
    printf("m = %d\n",m);   //10
    //不建议这样写
    
    return 0;
}

不建议自己这样写。只需要知道有这么个原理,别人这么写代码时看的懂,就够了。

08. static和extern的区别

static静态变量特点:在运行前分配内存,程序运行结束 生命周期结束,所以在本文件都可以使用静态变量

extern:可以提高变量的作用域

//文件1
int a = 100; //在C语言下,全局变量前都隐式加了关键字extern
//文件2
#include <stdio.h>
int main()
{
    extern int a; //告诉编译器,下面的a是外部链接属性,在其他文件中
    printf("%d\n",a);
    
    return 0;
}

09. 全局/静态区

int v1 = 10;  //全局变量,存放在静态区
const int v2 = 20;  //全局常量,存放在常量区,初始化后不可修改
static int v3 = 30;  //静态全局变量,存放在静态区

void test()
{
    const int v2 = 20;  //const局部变量,存放在栈区,通过指针可以修改
    
    char str[] = "hello"; //字符串常量
    str[0] = 'a';  //有些编译器可以修改,有些编译器不能修改
    //没有一个标准,所以尽量不要修改字符串常量的内容
}

10. 宏函数

#include <stdio.h>

//定义宏函数
#define MYADD(x,y) ((x)+(y))

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

注意事项:

  • 宏函数最好加小括号修饰,保证运算的完整性
  • 通常将频繁、短小的函数,写成宏函数
  • 宏函数会比普通函数在一定程度上效率高,省去了函数入栈、出栈的时间
  • 宏函数后面 没有分号;

优点:以空间换时间

函数调用流程:

  • 局部变量、函数形参、函数返回地址… 都要入栈和出栈

调用惯例:主调函数和被调函数必须有一致约定,才能正确的调用函数,这个约定就是调用惯例

  • 调用惯例包含:出栈方、参数传递顺序、函数名称修饰
  • C/C++默认调用惯例:cdecl——从右到左,主调函数惯例出栈

11. 栈的生长方向和内存存放方向

#include <stdio.h>

int main()
{
    //栈的生长方向
    int a = 10;  //栈底,高地址
    int b = 20;
    int c = 30;
    int d = 40;	 //栈顶,低地址
 
    //内存存放方向
    int i = 0x11223344;
    char* p = &a;
    
    printf("%x\n",*p);  //44  低位地址数据
    printf("%x\n",*(p+1));  //33
    printf("%x\n",*(p+2));  //22
    printf("%x\n",*(p+3));  //11
    
    return 0;
}

高位数据存放高地址,低位数据存放低地址,这种存储方式 称为小端存储方式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值