C语言学习_内存分区
1.1 数据类型
- 数据类型的本质:固定内存大小的别名
数据类型的作用:编译器预算对象(变量)分配的内存空间大小
int a; // 告诉编译器分配4个字节的内存
- 数据类型可以通过
typedef
起别名 - 数据类型可以通过
sizeof()
测类型大小 void
数据类型(无类型,万能类型)如果函数没有返回值,必须用
void
修饰// 对函数返回的限定 void fun(int a);
如果函数没有参数,参数可以用
void
修饰// 对函数参数的限定 int fun(void);
不能定义
void
类型的普通变量void a; // error,不能确定分配内存空间的大小
万能指针
void * p; // ok, 万能指针,指针类型都是4个字节,函数参数,函数有返回值 //1. void* 可以指向任何类型的数据,被称为万能指针 void test03() { int a = 10; void* p = NULL; p=&a; printf("a:%d\n",*(int*)p); char c = 'a'; p=&c; printf("c:%c\n",*(char*)p); } //2. void* 常用于数据类型的封装 void test04() { void * memcpy(void * _Dst, const void * _Src, size_t _Size); }
sizeof
操作符注意事项sizeof
返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;sizeof
返回的数据结果类型是unsigned int
;要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不 多,但是在用
sizeof
的时候差别很大:- 对数组名用
sizeof
返回的是整个数组的大小 - 对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。
- 当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小
- 对数组名用
示例代码:
//1. sizeof基本用法 void test01() { int a = 10; printf("len:%d\n", sizeof(a)); printf("len:%d\n", sizeof(int)); printf("len:%d\n", sizeof a); } //2. sizeof 结果类型 void test02() { unsigned int a = 10; if (a - 11 < 0) { printf("结果小于0\n"); } else { printf("结果大于0\n"); } int b = 5; if (sizeof(b) - 10 < 0) { printf("结果小于0\n"); } else { printf("结果大于0\n"); } } //3. sizeof 碰到数组 void TestArray(int arr[]) { printf("TestArray arr size:%d\n",sizeof(arr)); } void test03() { int arr[] = { 10, 20, 30, 40, 50 }; printf("array size: %d\n",sizeof(arr)); //数组名在某些情况下等价于指针 int* pArr = arr; printf("arr[2]:%d\n",pArr[2]); printf("array size: %d\n", sizeof(pArr)); //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小 TestArray(arr); }
1.2 变量
1.2.1 变量的概念
既能读又能写的内存对象,称为变量;
若一旦初始化后不能修改的对象则称为常量。
变量定义的形式:类型 标识符,标识符,…,标识符
1.2.2 变量的本质
- 变量名的本质:一段连续内存空间的别名;
- 程序通过变量来申请和命名内存空间
int a = 0
; - 通过变量名访问内存空间;
- 不是向变量名读写数据,而是向变量所代表的内存空间中读写数据;
- 变量的三要素:名称、大小、作用域
- 变量的生命周期:结合内存四区模型来看
修改变量的两种方式:
void test() { int a = 10; //1. 直接修改 a = 20; printf("直接修改,a:%d\n",a); //2. 间接修改 int* p = &a; *p = 30; printf("间接修改,a:%d\n", a); }
1.3 程序的内存四区模型
1.3.1 内存分区
程序运行之前:
C程序编译过程
1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:将目标文件链接为可执行程序
可执行程序内部已经分好3段信息,分别为代码区
text
、数据区data
和未初始化数据区bss
3个部分(有些人直接把data
和bss
合起来叫做静态区或全局区)。总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。
- 程序运行之后:
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
总的来说,内存分区模型:
代码区:可执行代码段,不可修改。未初始化数据区(BSS):全局未初始化,静态未初始化数据,数据的生存周期为整个程序运行过程 。
全局初始化数据区/静态数据区(data segment):全局初始化,静态初始化数据,文字常量(只读),数据的生存周期为整个程序运行过程。
栈区(stack):先进后出的内存结构,由编译器自动分配释放 ,存放函数的参数值、返回值、局部变量等
堆区(heap):容量要远远大于栈,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
1.3.2 分区模型
栈区
由系统进行的内存管理,主要存放函数的参数以及局部变量,函数完成后系统自动释放。
#char* func() { char p[] = "hello world!"; //在栈区存储 乱码 printf("%s\n", p); return p; } void test() { char* p = NULL; p=func(); printf("%s\n",p); }
堆区
手工申请,手工释放
char* func() { char* str = malloc(100); strcpy(str, "hello world!"); printf("%s\n",str); return str; } void test01() { char* p = NULL; p=func(); printf("%s\n",p); } void allocateSpace(char* p) { p=malloc(100); strcpy(p, "hello world!"); printf("%s\n", p); } void test02() { char* p = NULL; allocateSpace(p); printf("%s\n", p); }
堆分配内存API:
void *calloc(size_t nmemb, size_t size); /* * 功能: * 在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存置0。 * 参数: * nmemb:所需内存单元数量 * size:每个内存单元的大小(单位:字节) * 返回值: * 成功:分配空间的起始地址 * 失败:NULL */
全局/静态区
全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。
int v1 = 10; //全局/静态区 const int v2 = 20; //常量,一旦初始化,不可修改 static int v3 = 20; //全局/静态区 char *p1; //全局/静态区,编译器默认初始化为NULL //那么全局static int 和 全局int变量有什么区别? void test() { static int v4 = 20; //全局/静态区 } // 加深理解 char* func() { static char arr[] = "hello world!"; //在静态区存储 可读可写 arr[2] = 'c'; char* p = "hello world!"; //全局/静态区-字符串常量区 //p[2] = 'c'; //只读,不可修改 printf("%d\n",arr); printf("%d\n",p); printf("%s\n", arr); return arr; } void test() { char* p = func(); printf("%s\n",p); }
总结:
数据区包括:堆,栈,全局/静态存储区。
全局/静态存储区包括:常量区,全局区、静态区。
常量区包括:字符串常量区、常变量区。
代码区:存放程序编译后的二进制代码,不可寻址区。
可以说,C/C++内存分区其实只有两个,即代码区和数据区。
1.3.3 函数调用模型
int func(int a,int b) { int t_a = a; int t_b = b; return t_a + t_b; } int main() { int ret = 0; ret = func(10, 20); return EXIT_SUCCESS; }
1.3.4 栈的生长方向和内存存放方向
//1. 栈的生长方向 void test01() { int a = 10; int b = 20; int c = 30; int d = 40; printf("a = %d\n", &a); printf("b = %d\n", &b); printf("c = %d\n", &c); printf("d = %d\n", &d); //a的地址大于b的地址,故而生长方向向下 } //2. 内存生长方向(小端模式) void test02() { //高位字节 -> 地位字节 int num = 0xaabbccdd; unsigned char* p = # //从首地址开始的第一个字节 printf("%x\n",*p); printf("%x\n", *(p + 1)); printf("%x\n", *(p + 2)); printf("%x\n", *(p + 3)); }