一、内存分区及栈、堆区的使用
内存分区
在程序没有加载到内存之前,可执行程序内部已经分好了3段信息:代码区(text)、数据区(data)和未初始化数据区(bss)。
代码区:加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
未初始化数据区(bss段):加载的是可执行文件bss段,**存储于数据段的数据(全局未初始化,静态未初始化数据),生存周期为整个程序运行过程。
全局初始化数据区/静态数据区(data段):加载的是可执行文件数据段,存储与数据段(全局初始化,静态初始化数据,文字常量(只读)),数据生存周期为整个程序运行过程。
程序运行后:
栈区: 是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。 在程序运行过程时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 经典的操作系统中,栈总是向下生长的,压栈操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。
堆区:容量远远大于栈,用于动态内存分配,堆在内存中位于BSS区和栈区之间。一般由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。
栈区的使用
栈区使用时:不要返回局部变量的地址。局部变量在函数执行之后就释放了,我们没有权限操作释放后的内存。
实例1:
int * func()
{
int a = 10;
return &a;
}
void test()
{
//a的内存已经被释放了,我们没有权限操作这块内存
int * p = func();
printf("a = %d\n",*p);
printf("a = %d\n",*p);
}
输出结果为:
由于处于栈区,a的内存在func函数结束时,被系统释放,我们已经无权限操作它了;虽然第一次输出正确,是由于编译器为我们做了一次暂时保留防止我们误操作,但此时我们无权限对该块内存进行操作,所以第二次输出为一个随机值。
栈上函数调用流程: 栈保存一个函数调用所需要维护的信息,通常被称为栈帧或活动记录。一个函数调用过程一般包括以下几个方面:函数的返回地址、函数的参数、临时变量、保存的上下文:包括在函数调用前后需要保持不变的寄存器。 我们来看一段代码:
int func(int a,int b)
{
int ta = a;
int tb = b;
return ta + tb;
}
int main()
{
int sum = 0;
sum = func(10,20);
printf("sum = %d\n",sum);
return 0;
}
产生了3个问题:
①如何知道函数执行完毕后再跳回到printf继续执行?
②a和b传入顺序是从右至左还是从左至右?
③栈上的数据是由主调函数释放还是被调函数释放?
这里我们C/C++中有一个调用惯例,解决了上面的问题。
调用惯例: 函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用。C/C++默认的调用惯例为cdecl约定。
堆区的使用
1.手动开辟内存,手动释放。
int * GetSpace()
{
int *p = (int*)malloc(sizeof(int)*5);//堆区开辟内存
if (p == NULL)
{
return NULL;
}
else
{
for (int i=0; i<5; i++)
{
p[i] = i + 100;
}
}
return p;
}
void test()
{
int* p = GetSpace();
for (int i=0; i<5; i++)
{
printf("%d\n",p[i]);
}
free(p);//手动在堆区创建的数据,必须手动释放
p = NULL;//防止其变为野指针
}
执行成功:
2.如果主调函数中没有给指针分配内存,被调函数需要利用高级指针给主调函数中指针分配内存。
如果主调函数中没有给指针分配内存,被调函数用同级指针是修饰不到主调函数中的指针的。看一个例子:代码如下
void allocateSpace(char * pp)
{
char * temp = (char *)malloc(100);
if (temp == NULL) return;
memset(temp,0,100);//置空
strcpy(temp,"hello world");
pp = temp;
}
void test()
{
char * p = NULL;
allocateSpace(p);
printf("%s\n",p);
}
输出结果为:null。为什么呢?如图:
1.执行char * p 时,p入栈,赋值为NULL;
2.当执行第二句将p传入pp时,pp也入栈为空;
3.进入函数,char * tmep在栈区,记录的是堆区地址:假设地址为0x001;
4.memset将其置为空,strcpy向其写入hello world;
5.pp = temp即赋值为:0x001;
6.最后输出p,p还是为空,并未修饰到pp;
我们对其改进一下:
void allocateSpace2(char ** pp)
{
char * temp = (char *)malloc(100);
if (temp == NULL) return;
memset(temp,0,100);//置空
strcpy(temp,"hello world");
*pp = temp;
}
void test2()
{
char * p = NULL;
allocateSpace2(&p);
printf("%s\n",p);
free(p);
p = NULL;
}
此时:打印成功
主调函数中没有给指针分配内存,被调函数利用高级指针给主调函数中指针分配内存。
二、栈的生长方向及内存存储方式
栈的生长方向: 栈底,高地址;栈顶,低地址。 我们观察如下代码:
void test()
{
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为高地址,依次下降;a先入栈,为栈低,是高地址;d最后入栈,为栈顶,是低地址。
内存存储方式: 低位字节数据放在低地址,高位字节数据放在高地址。
也称为小端:低位字节数据放低地址;高位字节数据放高地址。
大端:低位字节数据放高地址;高位字节数据放低地址
内存如何存储呢?我们先看下图:dd是放在了高地址还是低地址呢?
我们利用代码验证一下:
void test1()
{
int a = 0xaabbccdd;
unsigned char* p = (unsigned char*)&a;//char*步长为一个字节
printf("%x\n",*p);
printf("%x\n",*(p+1));//若打印出来为cc,则高位字节数据---高地址
printf("%x\n",*(p+2));
printf("%x\n",*(p+3));
}
打印出来第一个数据为:dd
如果打印出来第二个数据为cc,那么可以验证高位字节数据对应高地址。
后面同理,打印结果如下:
三、static与extern的使用
静态变量: 在计算机编程领域指在程序执行前系统就为之静态分配(也即在运行时中不再改变分配情况)存储空间的一类变量。
静态变量关键字:static特点:
1.在编译阶段就分配内存,只初始化一次。属于内部链接属性,只能在当前文件中使用。
2.静态变量:作用域只能在当前作用域中;而全局变量可以在全局使用。但他们两个的声明周期是相同的,编译阶段分配好了内存,执行结束声明周期才结束。
static int a = 10;//全局静态变量
void test()
{
static int b = 20;//局部静态变量:作用域只能在当前作用域中
//a和b的生命周期是一样的
}
全局变量: 全局变量既可以是某对象函数创建,也可以是在本程序任何地方创建。全局变量是可以被本程序所有对象或函数引用。
extern特点:C语言下,全局变量前都隐藏了关键字extern,extren属于外部链接属性(在该文件外,别的文件也可以使用它)。
int a = 100;
extern int a = 100;//C语言下,全局变量前都隐藏了关键字extern
这里我们先在另一个文件中定义int b = 1000;
再回到刚才文件区使用它。extern int b;作用是告诉编译器b是外部链接属性变量,下面在使用时不要报错。
执行成功:输出1000
四、C语言中的伪常量const
常量:常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。常量就像是常规的变量,只不过常量的值在定义后不能进行修改。 常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
C语言下的常量为伪常量:因为可以间接修改,伪常量不可以初始化数组。
1.const修饰的常量为全局变量时:不能被修改
直接修改:失败
const int a = 10;
a = 100;//直接修改,失败
间接修改:失败
const int a = 10;
int *p = (int*)&a;
*p = 100;//间接修改,失败
2.const修饰的常量为局部变量时:不能被修改
直接修改:失败
b = 100;//直接修改:失败
间接修改:成功(常量放在了栈上)
void test()
{
const int b = 10;//分配到栈上
//b = 100;//直接修改:失败
int *p = (int*)(&b);
*p = 500;
printf("b = %d\n",b);
}
输出为b=500(注意,这里是在.c文件下操作的),因此C语言下const为伪常量
字符串常量:ANSI对修改结果未定义,在VS下不允许修改字符串常量。但是有些编译器可以修改字符串常量。 但是为了提高代码的健壮性,尽量还是不要尝试去修改字符串常量。
当我们在VS下对字符串进行修改时:程序崩溃。
void test()
{
char * p1 ="hello world";
p1[0] = 'z';
printf("%c\n",p1[0]);
}
五、宏函数及函数变量传递
宏定义:宏,是一种批量处理的称谓。计算机科学里的宏是一种抽象,它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。
例如:我们在使用时,在预处理时所有MAX自动替换为100;
#define MAX 100;
宏函数:在预处理的时候就会替换成相应的语句,十分像c++里面的模板。宏函数在一定程度上会比普通函数效率高。 以空间换时间,我们通常将一些频繁使用且短小的函数封装为宏函数。例如:下述代码即为宏函数的使用,非常方便。
#define MYADD(x,y) x+y
void test()
{
int a = 10;
int b = 20;
int sum = MYADD(a,b);//将a,b替换为x+y
printf("sum = %d\n",sum);
}
输出为:
注意事项:宏函数要保证运算的完整性。
如果我们给它乘20:
int sum = MYADD(a,b)*20;//a+b*20
输出结果预期为600,但实际却输出为:
此时在宏替换时变为:a + b * 20——》10 + 20 * 20为410;
如何保证呢?我们在宏函数处多加一些括号即可
#define MYADD(x,y) ((x)+(y))
这次输出结果与我们预期相同,结果为:600
那么宏函数为什么在一定程度上会比普通函数效率高呢?为什么不直接定义一个函数呢? 如下所示:
int Add(int x,int y)
{
return x + y;
}
原因如下:因为x,y都是存储在栈上的,使用时会有时间上的开销,创建时要入栈,用完要出栈,会比宏函数开销大。
函数变量传递分析:
1.main函数在栈区开辟的内存,所有子函数均可以使用。
2.main函数在堆区开辟的内存,所有子函数均可以使用。
3.子函数1在栈区开辟的内存,子函数1和2均可以使用,main函数不可以使用。
4.子函数1在堆区开辟的内存,子函数1和2均可以使用,main函数可以使用。
5.全局区开辟的数据,哪都可以使用。