c语言内存块讲解
文章目录
- 前言
- 一、栈区
- 1、栈区的特点:
- 1.1 自动管理
- 1.2 后进先出
- 1.3 有限大小
- 1.4 高速访问
- 1.5 栈区存储方向
- 2、栈区使用注意事项
- 二、堆区
- 1、堆区的定义
- 2、堆区的特点
- 3、堆区的内存分配与释放
- 4、注意事项:
- 三、全局/静态存储区
- 1、全局存储区
- 1.1 全局变量
- 1.2 静态全局变量
- 2、静态存储区
- 2.1 静态局部变量
- 四、常量存储区
- 1、常量的定义和存储
- 2、常量字符串
前言
在c语言中,内存主要分为4个区域:
栈区、堆区、全局/静态存储区、常量存储区
一、栈区
在c语言中,栈区(Stack)是内存中的一个重要区域,用于存储局部变量、函数参数、返回地址以及函数调用的上下文信息。
1、栈区的特点:
栈区的主要特点就是由系统自动管理其内存分配和释放,遵循后进先出的原则
1.1 自动管理
栈区的内存分配和释放由编译器在编译时和运行时自动处理,程序员无需手动处理。当函数被调用时,会在栈上为其局部变量和参数分配空间;当函数返回时,这些空间会被自动释放掉。
例如:写一个加法函数
#include<stdio.h>
int add(int x, int y)
{int z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int ret = add(a, b);printf("%d\n",ret);return 0;
}
在程序运行时,编译器会给add函数开辟一个内存空间,此时参数部分x,y还有在函数内部创建的变量z都存储在函数的栈区,当main函数运行到下一步打印返回值的时候,此时add函数已经被调用完毕,那么存储在栈区上的函数的变量,参数,返回值等都会随着函数的使用完毕而被自动释放掉还给操作系统。此时就不存在这些变量,也不能对这些变量进行使用,因为这部分空间已经不再给与你使用了,还给操作系统了,使用的时候就没有权限,强制使用就会进行错误报错(没有使用权限)。
1.2 后进先出
栈区的内存分配和释放遵循先进后出的原则。这意味着最后分配的内存最先被释放,最先分配的内存最后被释放。这种特性使得栈非常适用于函数调用与返回的。
其实对于栈区后进先出的这个特性看过函数栈帧的读者们都应该很了解
在函数栈帧中,函数使用的时候都是进行压栈进行存储的。这里小编还是拿上面的加法函数进行说明。
在程序开始运行的时候,都是从main函数进入的,main函数是程序的入口,既然要使用main函数肯定也需要开辟内存空间,所以main函数在函数的最底部进行存放,然后进入main函数内存创建变量a和b继续存放在内存空间,此时存放的位置是由低地址到高地址进行存放,放在main开辟的内存空间的上面,这样一个个变量创建放在上面,就像是把下面的数据进行一直压着一样的。然后当函数运行结束想要取出来的时候,就跟日常去货物仓取货一样,总不能从下面开始拿,那么一下全部都会倒塌,所以从上面开始拿,上面开始拿,那不就是刚存进去的么,也就是后面存进的,所以就是后进先出的原理。
1.3 有限大小
栈区的大小通常由操作系统在程序启动时确定,并且可以在程序运行时通过系统调用进行调整。由于栈区空间的有限,过大的栈区使用(如深度递归和大量的局部变量)可能会导致栈溢出(Stack Overflow),从而导致程序崩溃。
像在这里小编写的一个递归函数一样,在这里递归函数没有停止条件,那么函数就会一直递归下去,不停的调用test函数,不断的为test函数开辟空间,但是栈区是有大小限制的,你不断的栈区上面进行内存使用,达到上限的时候就会超出使用范围,就跟一杯水,已经满了,你还不停的往里面添加,那么水就会溢出来,这和栈溢出是一样的道理的。
1.4 高速访问
栈区通常位于内存的较低地址区域,并且由于栈的线性增长和自动管理特性,使得栈上的数据访问速度非常快。
较低地址区域
栈区通常被分配在内存的较低地址区域。这意味着栈的起始地址较低,随着数据的压入(push),栈顶地址会逐渐向较高的内存地址移动。这种布局有利于快速定位栈顶和栈中的数据。
线性增长
栈是线性增长的,意味着数据只能在一个方向上(通常是向高地址方向)增加或减少。这种简单的增长模式有助于快速管理栈内存,因为不需要复杂的内存分配和释放算法。
自动管理特性
栈的管理是由编译器和运行时系统自动完成的。当函数被调用时,函数的局部变量和参数会被压入栈中;当函数返回时,这些数据会从栈中弹出,并释放相应的内存空间。这种自动管理减少了程序员手动管理内存的需求,同时也减少了内存泄漏和野指针等错误的风险。
数据访问速度快
由于栈区通常位于内存的较低地址区域,并且栈的线性增长和自动管理特性简化了内存访问模式,因此栈上的数据访问速度非常快。此外,栈区通常位于CPU缓存(cache)友好的地址空间内
,这意味着栈上的数据更有可能被缓存,从而进一步提高了访问速度。
1.5 栈区存储方向
栈区的存储方向是向内存地址减小的方向增长,即由高地址向低地址增长。
测试题
不要小看这一道题目,里面涉及了5个知识点:
- 这里main函数内是局部变量,放在栈区。
- 栈区的存储方向是向内存减小的方向增长,即由高地址向低地址增长。
- 数组在内存中的存储是连续的,且由低地址向高地址存储。
- 小端字节序存储:低位字节数据内容存储在内存的低地址出,高位字节数据内容存储在内存的高地址处。
- %x打印为十六进制形式打印,且打印顺序由高位字节数据到低位字节数据。
根据上述5点我们可以画出该数据在内存中的存储
2、栈区使用注意事项
- 避免栈溢出:由于栈区大小有限,程序员应避免使用深度递归或大量局部变量来防止栈溢出。
- 避免栈上分配大数组或结构体:虽然栈区访问速度快,但由于其大小限制,不建议在栈上分配过大的数组或结构体。这些数据结构应优先考虑在堆区(Heap)上分配。
- 注意变量生命周期:由于栈区内存由系统自动管理,程序员应特别注意变量的生命周期。在函数返回后,栈上的局部变量将不再有效,不应被访问。
二、堆区
1、堆区的定义
堆区是c语言中用于动态分配内存的区域,与栈区不同的是,堆区的内存分配和释放是需要程序员自己手动控制的,而不是由系统自动管理的。在上面栈区的内存分配和释放是由编译器编译和运行时自动处理的。
2、堆区的特点
特点1:大小可变
堆区的大小是不固定的,可以根据需要动态地增加和减少。
而关于堆区的内存分配由程序员自己进行分配的话,就需要用到四个函数:malloc,calloc,realloc,free。这四个函数,前面三个函数是用来开辟和调整需要的内存大小,free函数则是释放程序员动态分配的内存。所以在堆区,堆区的大小是不固定的。可以随时根据程序员的需要程序员自己进行调整。
特点2:不连续性
堆区的数据块可以随意的分配和释放,它们的位置是不固定的。
简单的来说,堆区是由程序员自己调用函数进行开辟的,哪里内存满足程序员需要的内存大小都可以进行存储。
特点3:长生命周期
堆区分配的内存空间在程序运行期间一直存在,直到显式地释放。
特点4:手动管理
因为堆区分配的内存空间在程序运行期间一直都存在,那么如果不及时的将它释放他就会一直存放在那里,所以程序员在堆区开辟完内存空间后,不需要使用的时候,需要将它及时的释放。
3、堆区的内存分配与释放
堆区的内存分配
- malloc函数:malloc函数是用于在堆区分配指定大小的内存空间,并且返回一个指向该内存空间的指针。如果分配失败,则返回NULL。
- calloc函数:calloc函数和malloc函数类似,只不过calloc函数在堆区分配指定大小的内存空间的时候,会将分配的内存初始化为0。
- realloc函数:realloc函数用于调整已分配内存的大小。如果新的大小大于原大小,则扩展内存区域;如果新的大小小于原大小,则缩小内存区域并释放多余的内存空间。
堆区的内存释放
free函数:free函数是用于释放之前通过malloc、calloc或者realloc分配的内存。释放后的内存不再被程序使用,直到再次分配。
4、注意事项:
- 避免内存泄漏:
程序员需要确保不再需要堆区内存的时候及时释放它,以避免内存泄漏。内存泄漏会导致程序占用的内存不断增加,最终导致系统崩溃。
- 避免使用野指针
free函数将动态分配的内存释放的时候,应该将指向这块内存的指针置为NULL指针,以避免野指针的出现。野指针是指向已经释放内存的空间,它可能导致程序崩溃或不可预测的行为。
三、全局/静态存储区
1、全局存储区
全局存储区用于存储全局变量和静态全局变量。这些变量在整个程序的整个生命周期内都存在,并且可以在程序的任何地方访问(对于全局变量)或者定义他们文件的内部访问(对于静态局部变量)
1.1 全局变量
- 定义:在函数外部定义的变量
- 作用域:整个程序(所有文件,如果变量被声明为extern)
- 生命周期:从程序开始到程序结束
- 示例:
int a = 10; //a即为全局变量
int main()
{printf("%d",a)return 0;
}
1.2 静态全局变量
- 定义:在函数外部被static关键字定义的变量
- 作用域:定义他们的文件内部(切记只能在它们定义的文件内部,相当于static将它锁死在那个文件中,其他文件中使用会出错)
- 生命周期:从程序开始到结束
- 示例
static int c = 10;
int main()
{printf("%d", c);return 0;
}
2、静态存储区
静态存储区不仅包含静态全局变量,还包含静态局部变量和常量字符串。这些变量在程序的整个生命周期内都存在,但他们的可见性和作用域可能有所不同。
2.1 静态局部变量
- 定义:在函数内部使用static关键字定义的变量
- 作用域:定义它们的函数内部
- 生命周期:从程序开始到程序结束(即使函数执行完毕,变量也不会销毁)。
- 示例:
int test()
{static int i = 0;i++;return i;
}
int main()
{for (int i = 0; i < 5; i++){int ret = test();printf("%d", ret);}return 0;
}
分析
正常来说,像i这种函数变量属于临时变量,存储在栈区,随着函数的使用结束而内存销毁。在这里我们使用static修饰变量i,此时i不再存储在栈区,而是存储在静态存储区,此时i不会随着函数的使用结束而销毁,运行的时候i是逐渐增加的值,而不是进来函数变量就重新开辟一次。这与不用static的结果是截然不同的。
使用static修饰,变量不会随函数使用完毕而销毁
不使用static函数定义的变量i,此时存储在栈区,随着函数的使用结束会销毁,需要使用的时候需要重新创建
四、常量存储区
在c语言中,常量存储区(也称为常量数据段或只读数据段)是内存中的一个特定区域,用于存储程序中的常量值。这些常量值在程序的整个生命周期都不会发生改变,并且通常保存在只读内存中,以防止它们被意外修改。
1、常量的定义和存储
在c语言中,常量可以通过多种方式定义,包括const关键字、#define预处理指令、以及枚举类型
- const关键字:用于声明一个变量为常量,该变量的值在初始化后不能被修改
const int MAX_SIZE = 100;
- #define预处理指令:用于在预处理阶段定义常量。这种方式定义的常量实际上是在编译前进行文本替换,而不是真正的变量。例如
#define PI 3.14159
- 枚举类型(enum):用于定义一组命名的整数常量。例如:
enum Color { RED, GREEN, BLUE };
2、常量字符串
- 定义:常量字符串是用双引号括起来一系列字符,它表示一个
不可变的字符序列
。在c语言中,字符串以空字符(‘\0’)作为结束标志,因此常量字符串在内存中上实际是一个以空字符结尾的字符数组 - 特性:
- 只读性:常量字符串的值在程序运行期间是不可改变的。如果尝试修改常量字符串的内容,将会导致未定义行为,通常是程序崩溃或数据损坏。
- 存储位置:
常量字符串通常存储在程序的只读数据段(也称为常量存储区)
中,这意味着即使程序的其他部分(如堆区或全局\静态存储区)的数据发生改变,常量字符串的内容也不会发生改变。- c语言中,常量字符串是通过字符数组来表示的。然而,与普通的字符数组不同的是,常量字符串的数组元素是不可以被修改的。
- 声明与初始化:
const char *str = "Hello, World!";
这里,str是一个指向常量字符串的指针,而"Hello, World!"则是存储在只读数据段中的常量字符串。
注意事项:
- 当使用指针指向常量字符串时,应该确保不会通过该指针修改字符串的内容。如果确实要修改字符串,应该使用字符数组而不是字符串常量
- 编译器可能会将相同的常量字符串合并为一个单一的存储位置,以节省内存空间。这是编译器优化的一部分,但程序员不应该依赖这种优化来节省内存。
错误示例代码:
#include <stdio.h> int main() { const char *str = "Hello, World!"; printf("%s\n", str); // 尝试修改常量字符串的内容(错误做法) // str[0] = 'h'; // 这将导致未定义行为 return 0;
}
在这个示例中,str是一个指向常量字符串"Hello, World!"的指针。程序通过printf函数打印出这个字符串。然而,如果尝试修改str指向的字符串内容(如注释中所示),将会导致未定义行为。