【C语言】内存分区
文章目录
一、数据类型
数据类型概念
什么是数据类型?为什么需要数据类型?
数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。
数据类型可以理解为创建变量的模具: 固定大小内存的别名;
typedef
1、给数据类型起别名 typedef 原名 别名
示例代码:
方法一:
struct Person{
char name[64];
int age;
};
typedef struct Person myperson;
void test(){
myperson p; //相当于 struct Person p;
}
方法二:
typedef struct Person
{
char name[64];
int age;
}myPerson;
void test()
{
myPerson p; //相当于 struct Person p;
}
2、区分数据类型
定义变量
char* p1,p2; //实际上定义的变量p1尾char类型,p2为char类型。初学C语言的同学可能会混淆
使用typedef
typrdef char pchar;
pchar p1,p2; //p1,p2均为char*类型
void数据类型
void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据。
void定义变量是没有任何意义的,当你定义void a,编译器会报错。原因是:无法给void无类型变量分配内存。
void真正用在以下两个方面:
对函数返回的限定;
对函数参数的限定;
示例代码:
//1. void修饰函数参数和函数返回
void test01(void){
printf("hello world");
}
//2、void * 万能指针 不管几级指针,任意类型指针都是4个字节
void test02()
{
//printf("size of void* = %d\n", sizeof(void *));//4
void * p = NULL;
int * pInt = NULL;
char * pChar = NULL;
pChar = (char *)pInt;//需要强制类型转化才行
pChar = p; //万能指针 可以不通过强制类型转换就转成其他类型指针
}
sizeof 操作符
sizeof是C语言中的操作符并不是函数,类似于++、–等等。sizeof能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。
sizeof(变量);
sizeof 变量; //这里可以看出sizeof 不是函数
sizeof(类型);
sizeof的返回值类型是 无符号整型 unsigned int
//当unsigned int 和 int做运算,会转换成统一 unsigned int数据类型
void test()
{
if(sizeof(int)-5>0)
{
printf("大于0\n");
}
else
{
printf("小于0\n");
}
}//打印结果为大于0,故可知sizeof的返回值是unsigned int
sizeof 用途
1.统计数组长度
当数组名做函数参数时候,会退化为指针,指向数组中第一个元素的位置
示例代码:
void calculateArray(int arr[])
{
printf("array length = %d\n", sizeof(arr));//4
}
void test()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
//printf("array length = %d\n", sizeof(arr));//32
}
总结
-
数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量;
-
数据类型大小计算(sizeof);
-
可以给已存在的数据类型起别名typedef;
-
数据类型的封装(void 万能类型);
二、变量
变量的概念
既能读又能写的内存对象,称为变量;
若一旦初始化后不能修改的对象则称为常量。
变量定义形式: 类型 标识符, 标识符, … , 标识符
变量的修改方式
void test()
{
//1、直接修改
int a = 10;
a = 20;
//2、间接修改
int * p = &a;
*p = 30;
printf("a = %d\n", a);
}
struct Person
{
char a; //0 ~ 3 //内存对齐
int b; //4 ~ 7
char c; //8 ~ 11 //内存对齐
int d; //12 ~ 15
};
void test()
{
struct Person p = { 'a', 10, 'b', 20 };
//直接修改 d属性
p.d = 1000;
//间接修改 d属性
struct Person * pp = &p;
pp->d = 1000;
char * pp = &p;
*(int*)(pp + 12) = 2000;
printf("d属性为: %d\n", *(int*)(pp + 12)); //指针步长
printf("d属性为: %d\n", *(int*)((int*)pp + 3));
}
三、程序内存分区模型
内存分区
我们要执行我们编写的C程序,那么第一步需要对这个程序进行编译。
1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:将目标文件链接为可执行程序
在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)
总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。、
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
五区特征:
代码区(text segment):可共享,只读。
未初始化数据区(BSS),全局初始化数据区/静态数据区(data segment):生存周期为整个程序运行过程。
栈区(stack):先进后出的内存结构,存放函数的参数值、返回值、局部变量等。生存周期为申请到释放该段栈空间。
堆区:它的容量要远远大于栈(不是无限大),用于动态内存分配。由程序员手动分配和释放。
类型 作用域 生命周期 存储位置
类型 | 作用域 | 生命周期 | 存储位置 |
auto变量 | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
栈区
由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。
char * getString()
{
char str[] = "hello world";
return str;
}
void test()
{
char * p = NULL;
p = getString();
printf("%s\n", p);
}
int main(){
test();
return 0;
}
char * getString()函数返回str的地址函数结束所开辟的栈空间也随之释放str里存放的内容就位置p的内容为0x02所对应的内容也就位置故打印出来的结果不是hello world 而是乱码。
堆区
由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。
示例代码:
int* getSpace()
{
int* p = malloc(sizeof(int) * 5);
if (p == NULL)
{
return;
}
for (int i = 0; i < 5; i++)
{
p[i] = i + 100;
}
return p;
}
void test01()
{
int* p = getSpace();
for (int i = 0; i < 5; i++)
{
printf("%d\n", p[i]);
}
//手动开辟 手动释放
free(p);
p = NULL;
}
int main() {
test01();
return 0;
}
堆区分配内存注意事项(指针分配内存):
//error
void allocateSpace(char * pp)
{
char * temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
pp = temp;
}
void test02()
{
char * p = NULL;
allocateSpace(p);
printf("%s\n", p);
}
int main() {
test02();
return 0;
}
//true
void allocateSpace2(char ** pp)
{
char * temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
*pp = temp;
}
void test03()
{
char * p = NULL;
allocateSpace2(&p);
printf("%s\n", p);
if (p != NULL)
{
free(p);
p = NULL;
}
}
int main() {
test03();
return 0;
}
全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。
//static 静态变量
// 特点:在运行前分配内存,程序运行结束 生命周期结束 ,在本文件内都可以使用静态变量
// 全局作用域 a
static int a = 10;void test01() {
//局部作用域 b
static int b = 20;
}
int main(){
//告诉编译器 下面代码中出现 g_a 不要报错,是外部链接属性,在其他文件中
extern int g_a;
printf(“g_a = %d\n”, g_a);
system(“pause”);
return 0;
}
常量区
1、const修饰的变量
//全局变量
const int a = 10; //常量区 ,间接修改 语法通过,运行失败,原因:受到常量区的保护
void test01()
{
//a = 100;
int * p = &a;
*p = 100;
printf("%d\n", a);
}
void test02()
{
const int b = 10; //存放在栈上,通过间接修改是可以成功的
//b = 20;
int * p = &b;
*p = 20;
printf("%d\n", b);
//在C语言中 const修饰的局部变量 ,不可以初始化数组 ,称为伪常量
//int arr[b];
}
2、字符串常量
ANSI C中规定:修改字符串常量,结果是未定义的。
ANSI C并没有规定编译器的实现者对字符串的处理,例如:
1.有些编译器可修改字符串常量,有些编译器则不可修改字符串常量。
2.有些编译器把多个相同的字符串常量看成一个(这种优化可能出现在字符串常量中,节省空间),有些则不进行此优化。如果进行优化,则可能导致修改一个字符串常量导致另外的字符串常量也发生变化,结果不可知。
所以尽量不要去修改字符串常量!
void test03()
{
char * p1 = "hello world";
char * p2 = "hello world";
char * p3 = "hello world";
printf("%d\n", &"hello world");
printf("%d\n", p1);
printf("%d\n", p2);
printf("%d\n", p3);//用vs下p1,p2,p3的内容是同一个
}
四、函数调用模型
宏函数
#define MYADD(x,y) ((x) + (y))
//1、宏函数需要加小括号修饰,保证运算的完整性
//2、通常会将频繁、短小的函数 写成宏函数
//3、宏函数 会比普通函数在一定程度上 效率高,省去普通函数入栈、出栈时间上的开销
// 优点: 以空间 换时间
void test01()
{
printf("%d\n", MYADD(10, 20) * 20 ); // ((10) + (20)) * 20
}
int main(){
test01();
return 0;
}
函数调用流程
栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).
一个函数调用过程所需要的信息一般包括以下几个方面:
- 函数的返回地址;
- 函数的参数;
- 临时变量;
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
这边抛出两个问题
1:函数形参入栈时候,参数传递数据是从左向右还是从右向左?
2:a和b是由谁管理释放?是main函数(主调函数)管理还是func函数(被调函数)管理?
调用惯例
函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例”
在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是:
int cdecl func(int a,int b); 注意:
cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute((cdecl)).
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
cdecl | 函数调用方 | 从右至左参数入栈 | 下划线+函数名 |
这里就可以回答上面的两个问题了
1.函数形参入栈时候,参数传递数据是从右向左。
2.a和b是由是main函数(主调函数)管理释放。
函数变量传递分析
变量的作用域使用规则
char * func()
{
char * p = malloc(10); //堆区数据,只要没有释放,都可以使用
int c = 10;//在func中可以使用,test01和main都不可以使用
return p;
}
void test01()
{
int b = 10; // 在test01 、func 可以使用,在main中不可以用
func();
}
int main(){
int a = 10; //在main 、test01 、 func中都可以使用
test01();
return 0;
}
栈的生长方向
问题1:栈内数据储存是由高地址到低地址还是由低地址到高地址?
问题2:单个数据的高位字节空间存放在栈区高地址还是低地址?
void test01()
{
int a = 10; //栈底 高地址
int b = 10;
int c = 10;
int d = 10; //栈顶 低地址
printf("%p\n", &a);
printf("%p\n", &b);
printf("%p\n", &c);
printf("%\n", &d);
}
这里可以看出栈内存储是由高地址向低地址方向。
void test02()
{
int a = 0x11223344;
char * p = &a;
printf("%x\n", *p); //44 低位字节数据 低地址
printf("%x\n", *(p+1)); //33 高位字节数据 高地址
}
这里可以看出低位字节数据位于低地址,高位字节数据位于高地址(小端模式)。
总结
到这里这篇文章的内容就结束了,谢谢大家的观看,如果有好的建议可以留言喔,谢谢大家啦!
标签:函数,int,分区,数据类型,C语言,char,内存,printf,void From: https://blog.csdn.net/2301_80035097/article/details/137126449