1、什么是内存?(硬件和逻辑两个角度)
从硬件角度:内存实际上是电脑的一个配件(一般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后来的DDR1、DDR2……、LPDDR)
从逻辑角度:内存是这样一种东西,它可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址)、并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就对应内存中的一个单元)。
2、C语言的代码内存布局
图中红色部分是给用户写的应用程序使用的,以上的内存空间给操作系统使用。用户内存隔离开更为安全。
用户应用程序大致结构图如下所示:
.text即为代码段,只读,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
.bss段包含程序中未初始化的全局变量和static变量。
.data段即为数据段,存放的是程序中已初始化的全局变量、静态变量。
堆(heap):用于存放进程运行中被动态分配的内存段,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。其概念与数据结构中“堆”的概念不同。
栈 (stack):栈又称堆栈, 存放函数内部的局部变量、参数和返回地址,局部变量也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。由于栈的先进先出(LIFO)特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区(因为函数的局部变量存放在此,因此其访问方式应该是栈指针加偏移的方式,否则若通过push、pop操作来访问相当麻烦)
直接搬运代码,容易理解:
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区
static int c =0;//全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
此外,还有realloc(重新分配内存)、calloc(初始化为0)、alloca(在栈上申请内存,自动释放)等。
补充:全局变量与全局静态变量的区别:
(a)若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
(b)若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。
(c)具有外部链接的静态;可以在所有源文件里调用;除了本文件,其他文件可以通过extern的方式引用;
深入浅出: 大小端模式
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
比如十六进制数 : 0x12345678 ,每两位占8个字节。大小端存放方式如图:
3、C语言中的内存分配常见问题
常见三种分配方式:
静态存储区域分配:
内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量,static变量在栈上创建:
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限从堆上分配:
动态内存分配,程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也是最多
内存分配未成功,却使用了它
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用的解决办法是,在使用内存之前检查指针是否为NULL。例如:
t = (struct btree *)malloc(sizeof(struct btree));
if (t == NULL) {
printf("内存分配失败!\n");
exit(1); // 终止整个程序的运行(推荐);或return null终止本函数
}
内存分配成功,但是尚未初始化就引用它
犯这种错误主要由两个起因:
没有初始化的概念
误认为内存的缺省初值全为0,导致引用初值错误。内存的缺省初值究竟是什么并没有统一的标准,所以无论用何种方式创建数组,都别忘了赋初值,即便是赋初值0也不可省略,不要嫌麻烦
释放了内存却继续使用它
程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
函数的return语句写错了,注意不要返回指向”栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁
使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”
杜绝“野指针”
“野指针”不是NULL指针,是指向“垃圾”内存的指针,是很危险的,if语句对它不起作用。“野指针”的成因主要有两种:
(1). 指针变量没有初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应该被初始化,要么将指针设置为NULL,要么让它指向合法的内存,例如:
char *p = NULL;
char *str = (char *)malloc(sizeof(char) * 100);
if (str == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
(2). 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针,指针操作超越了变量的作用范围。好的习惯:
free(p);
p = NULL;
忘记释放内存,导致内存泄漏
含有这种错误的函数每被调用一次就丢失一块内存。刚开始的时候,系统内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽
动态内存的申请与释放必须配对,程序中malloc和free的使用次数一定要相同,否则肯定有错误
规则总结:
用malloc或new申请内存之后,应该立即检查指针是否为NULL。防止使用指针为NULL的内存
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用
避免数组或者指针的下标越界,特别要当心发生“多1”或者“少1”的操作
动态内存的申请与释放必须配对,防止内存泄漏
用free或delete释放内存之后,立即将指针设置为NULL,防止产生“野指针”
指针与数组修改内容
#include <stdio.h>
int main()
{
char a[] = "hello";
a[0] = 'x'; // 正确,数组数据存储于栈区,对它修改是没有任何问题的
printf("%s\n", a);
char *p = "wrold";
p[0] = 'x'; // 错误,字符串常量,存储在静态存储区,内容不可修改
printf("%s\n", p);
return 0;
}
内容复制与比较
不能对数组名进行直接复制与比较。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,应该用标准库函数strcmp进行比较
string.h 库 : strcpy,strcmp
stdlib.h 库 : malloc,free
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char* a = "hello";
//方式一:事先分配好内存后进行复制
char b[10];
strcpy(b, a); //不能用 b = a
//方式二:动态分配内存
int len = strlen(a);
char *p = (char *)malloc((len + 1) * sizeof(char));
if (p == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
strcpy(p, a);
if (strcmp(p, a) == 0) {
printf("p和a是相等的!\n");
}
free(p);
p = NULL;
return 0;
}
计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。
#include <stdio.h>
void funC(char *a);
int main()
{
char a[] = "hello";
char *p = a;
printf("%d\n", sizeof(a)); // 6字节
printf("%d\n", sizeof(p)); // 4字节,sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char *)
funC(a);
return 0;
}
void funC(char *a)
{
printf("%d\n", sizeof(a)); // 4字节而不是6字节,当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针,sizeof(a)始终等于sizeof(char *)
}
C语言中基本数据类型长度:
sizeof(char) 为1个字节
sizeof(short) 为2个字节
sizeof(int) 为4个字节
sizeof(long) 为4或8个字节
sizeof(long long) 为8个字节
sizeof(float) 为4个字节
sizeof(double) 为8个字节
sizeof(void *) 为4个字节
另外,注意 sizeof与strlen的区别:
#include <stdio.h>
#include <string.h>
int main()
{
char s[] = "\\\0";
printf("%d\n", sizeof(s)); /// 为3个字节:'\\','\0','\0'
printf("%d\n", strlen(s)); // 为1个字节:'\\',遇到'\0'即停止
return 0;
}
字节对齐问题:
字节对齐不仅便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
对齐原则:
1.结构体内成员按自身按自身长度自对齐。
自身长度,如char=1,short=2,int=4,double=8,。所谓自对齐,指的是该成员的起始位置的内存地址必须是它自身长度的整数倍。如int只能以0,4,8这类的地址开始
2.结构体的总大小为结构体的有效对齐值的整数倍。有效对齐值的确定:
1)当未明确指定时,以结构体中最长的成员的长度为其有效值
2)当用#pragma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值。
3)当用_attribute_ ((packed))指定长度时,强制按照此值为结构体的有效对齐值
例子:
#include <stdio.h>
struct A{
char a; // 1 -> 4
int b; // 4
short c; // 1 -> 4
};
struct B{
char a; // 1 -> 2
short c; // 2
int b; // 4
}b;
int main()
{
printf("%d\n", sizeof(struct A)); // 12
printf("%d\n", sizeof(struct B)); // 8
return 0;
}
另外,不同的编译器可能会对内存的分布进行优化,例如有些编译器会把 struct A 优化成 struct B 的样子。但这属于编译器的问题,如果要作为编程的参考的话,尽量在保持代码清晰的情况下,自己手动将 struct A 优化成 struct B 的样子
指针参数传递内存
如果函数的参数是一个指针,用该指针去申请动态内存时需要警惕!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void GetMemory1(char *p, int num) // p是str的一个副本
{
p = (char*)malloc(sizeof(char) * num); // p的值改变,但是str的值并没有改变
}
char* GetMemory2(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
return p;
}
void GetMemory3(char **p, int num) // p是str地址的一个副本
{
*p = (char*)malloc(sizeof(char) * num); // p指向的值改变,也就是str的值改变
}
int main()
{
char *str = NULL;
GetMemory1(str, 200);
strcpy(str, "hello world!\n"); // 失败,段错误
str = GetMemory2(str,200);
strcpy(str, "hello world!\n"); // 运行正确
printf("%s", str);
GetMemory3(&str, 200); // 把str的地址传进去
strcpy(str, "hello world!\n"); // 运行正确
free(str);
return 0;
}
GetMemory1错误原因:
调用GetMemory1( str,200 )后, str并未产生变化,依然是NULL,只是改变的str的一个拷贝的内存的变化
在函数GetMemory1中,编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory1并不能输出任何东西。事实上,每执行一次GetMemory1就会泄漏一块内存,因为没有用free释放内存。谨记在malloc后用if ( p == NULL )判断内存是否申请成功。
错误分析:
错认为 GetMemory1(char *p, int num)中的 p 就是 GetMemory1(str,200)中的str。但p不是str,它只是等于str 。 就像:
int a = 100;
int b = a; // 现在b等于a
b = 500; // 现在能认为a = 500 ?
显然不能认为a = 500,因为b只是等于a,但不是a! 当b改变的时候,a并不会改变,b就不等于a了。 因此,虽然p已经有new的内存,但str仍然是null
改进方式:
在GetMemory2中,我们可以用函数返回值来传递动态内存,这种方法更简单,但是常常有人把return语句用错了。这里强调不要用return语句返回指向”栈内存“的指针,因为该内存在函数结束时自动消亡。
在GetMemory3中,使用了双层指针。p是str地址的一个副本 ,p指向的值改变,也就是str的值改变。推荐使用这种方法。