@TOC(内存四区模型,宏函数,调用惯例,内存存储方式)
1. 内存四区及其使用注意
内存四区:代码区,全局静态区,栈区,堆区
代码区
代码区存放的是CPU执行的二进制指令
特点:
- 只读
- 共享
栈区
特点:
- 先进后出
- 由编译器自动管理申请和释放
- 函数运行结束后,函数在栈上开辟的空间会被自动释放掉
- 栈上的空间较小,不适合存放大量的数据
注意事项:
- 不要返回局部变量的地址:
int* func()
{
int a = 10;
return &a;
}
void test01()
{
int *p = func();
printf("%d\n", *p);
}
结果:
结果已不重要,虽然输出的是10,但这是编译器对数据进行的保护,防止错误操作,实则a已被释放,p是个野指针
char* func2()
{
char str[] = "hello world";
return str;
}
void test02()
{
char* p = func2();
printf("%s\n", p);
}
结果:
返回乱码,在输出的时候str已被释放
堆区
- 堆区:手动申请,手动释放
- 基本使用方式:
int* getSpace()
{
int *p = malloc(sizeof(int)*5);
if (p == NULL)
{
printf("malloc error");
return;
}
for (int i = 0; i < 5; ++i)
{
p[i] = 100 + i;
}
return p;
}
void test01()
{
int* p = getSpace();
for (int i = 0; i < 5; ++i)
{
printf("%d\n", p[i]);
}
if (p != NULL)
{
free(p);
p = NULL;
}
}
结果:
100
101
102
103
104
-
注意事项:
- 主调函数的指针没有分配内存,被调函数中用同级指针无法对其进行描述
//pp是拷贝了p的值,pp和p没什么关系
void allocateSpace1(char* pp)
{
char* temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
pp = temp;
}
void test02()
{
char* p = NULL;
allocateSpace1(p);
printf("%s\n", p);
}
结果:
(null)
- 解决方法1:高级指针
//解决方法1 利用二级指针
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);
}
结果:
hello world
- 解决方法2:被调函数返回值返回地址
//解决方法2 利用返回值
char* allocateSpace3()
{
char* temp = malloc(100);
memset(temp, 0, 100);
strcpy(temp, "hello world");
return temp;
}
void test04()
{
char* p = NULL;
p = allocateSpace3();
printf("%s\n", p);
}
结果:
hello world
calloc 和 relloc的使用
calloc
void *calloc(int num_elems, int elem_size)
-
与malloc对比:
- calloc会为创建的内存赋初值0,malloc不会
- calloc的参数1是开辟的空间内的元素个数,参数2是开辟的空间内的元素大小
-
代码演示:
void test01()
{
int* p = calloc(10, sizeof(int));
for (int i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
for (int i = 0; i < 10; ++i)
{
p[i] = i+10;
}
for (int i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
free(p);
}
结果:
10个 0
10-19
realloc
void *realloc(void *mem_address, unsigned int newsize)
- 作用:在原有堆区内存的基础上再开辟一块容量为newsize大小的内存
- 参数:参1:指向原堆区空间的指针,参2:新的堆区大小
- 代码演示:
void test02()
{
int* p = (int*)malloc(10 * sizeof(int));
printf("%p\n", p);
for (int i = 0; i < 10; ++i)
{
p[i] = i + 10;
}
for (int i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
p = (int*)realloc(p, 20 * sizeof(int));
if (p == NULL)
return;
printf("%p\n", p);
for (int i = 0; i < 15; i++)
{
printf("%d\n", p[i]);
}
free(p);
}
结果:
重新开辟后的p的地址和原先的一样
- realloc的机制:扩容时,如果原来的空间后面有连续的空间可以进行扩容,那么则直接扩容,不修改原地址;如果原来的空间后面没有足够的内存大小,那么在其他位置开辟一个新的newsize大小的空间,并且将原来的内存中的数据拷贝到新的内存中,如此堆区的地址就发生了改变
void test03()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
return;
printf("%p\n", p);
p = (int*)realloc(p, 200 * sizeof(int));
if (p == NULL)
return;
printf("%p\n", p);
free(p);
}
结果:
013B78B8
013C0828
全局变量和静态变量
- extern关键字修饰的变量在同一个项目下的不同文件中可以被使用
test.c
extern int gl_a = 10;
work.c
//外部可访问
void test01()
{
extern gl_a;
printf("%d\n", gl_a);
}
- 静态变量只能在本文件中使用,作用域是本文件
- 静态变量只初始化一次
//静态变量只初始化一次
void func_s()
{
static int a = 10;
a++;
printf("%d\n", a);
}
void func()
{
int a = 10;
a++;
printf("%d\n", a);
}
void test02()
{
func_s();
func_s();
func_s();
func();
func();
func();
}
打印结果:
11 12 13
11 11 11
- const关键字修饰的变量
- const修饰全局变量时,直接修改和间接修改都不可以
- const修饰局部变量时,是一个伪常量,不能直接修改,可以间接修改
//const修饰的全局变量不能修改,包括直接修改和间接修改
const int num = 10;
void test03()
{
//num = 20; 编译器报错
int* p = #
*p = 20; //编译器不报错,运行报错
}
//const修饰的局部变量可以修改,是一个伪常量,不能直接修改,但是可以间接修改
void test04()
{
const int num2 = 10;
//num2 = 20; 编译器报错
int* p = &num2;
*p = 20; //编译器不报错,运行报错
printf("%d\n", num2);
}
- 字符串常量
- 值无法被修改,并且使用char*的变量指向,不同的变量里面的地址值相同
- 使用char[]p 这样的变量去存字符串是一个变量,可以被修改,且内存地址不相同
//字符串常量不能修改,它的值也不能修改
void test05()
{
char* p1 = "helloworld";
char* p2 = "helloworld";
char* p3 = "helloworld";
printf("%d\n", p1);
printf("%d\n", p2);
printf("%d\n", p3);
//p[0] = 'w'; 报错
char p4[] = "helloworld"; //这个可以修改
char p5[] = "helloworld"; //地址不同,是不同的变量
char p6[] = "helloworld";
p4[0] = 'w';
printf("%d\n", p4);
printf("%d\n", p5);
printf("%d\n", p6);
printf("%s\n", p4);
}
结果:
17595192
17595192
17595192
11795536
11795516
11795496
welloworld
2.宏函数
- 宏函数在预处理阶段会替换成为宏定义所替代的内容,省去了调用函数频繁进出栈的时间,用时间换取了空间,执行效率更高。通常在函数体较简单的时候使用宏函数
- 注意:因为宏函数只是会替换成定义的数值,而不是替换成函数计算后的结果,所以宏函数的函数体要加括号,保证不会被运算符的优先级影响
#define ADD(x,y) ((x)+(y))
int main(void)
{
printf("%d\n", ADD(1, 2));
system("pause");
return EXIT_SUCCESS;
}
3.调用惯例
函数执行流程
观察下面的代码,会产生两个问题:
- 参数传递是从右往左还是从左往右?
- 函数在栈上申请的空间是主调函数去释放还是该函数自己去释放?
int func(int a, int b)
{
int testa = a;
int testb = b;
return testa + testb;
}
void test01()
{
int ret = 0;
ret = func(10, 20);
printf("%d\n", ret);
}
想必上面这段代码是如何在栈上开辟空间的都应该了解,那么解决上面遗留的两个问题。
于是引入了调用惯例的概念:调用惯例解决出栈方,形参入栈顺序,名称修饰三个问题
在这里描述C语言的调用惯例:cdecl
- 参数传递:从右往左
- 出栈方:函数调用方
- 名称修饰:_函数名
4.栈的生长方向
内存四区小红的栈存储数据时栈底在高地址,栈底是在低地值,用一个demo来验证
void test01()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
printf("%d\n", &a);
printf("%d\n", &b);
printf("%d\n", &c);
printf("%d\n", &d);
}
结果为:
7338552
7338540
7338528
7338516
观察到从a到d他们的地址是递减的,那么也就得出结论栈底在高地址,栈顶在低地值
5.内存存储方式
我们已经知道了栈的生长方向是栈底在高地址,栈顶在低地值,那么在栈中存放数据的话,低位在高地址还是在低地值呢
用一个程序来解答:
void test02()
{
int a = 0x11223344;
char* p = &a;
printf("%x\n", *p);
printf("%x\n", *(p+1));
printf("%x\n", *(p+2));
printf("%x\n", *(p+3));
}
结果:
44
33
22
11
p指向低位44,p+1指向33,说明44存在低地值,33存在高地址
结论:
- 在内存四区中的栈上存放数据的话,低位存放在低地值,高位存放在高地址
- 上述存放方法称为小端模式:常用于家用电器
- 与小端模式相反的是大端模式:低位存放在高地址,高位存放在低地值,常用于大型服务器和大型设备