C语言--内存操作与管理

1、什么是内存?(硬件和逻辑两个角度)

从硬件角度:内存实际上是电脑的一个配件(一般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后来的DDR1、DDR2……、LPDDR)

从逻辑角度:内存是这样一种东西,它可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址)、并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就对应内存中的一个单元)。

2、C语言的代码内存布局

这里写图片描述

图中红色部分是给用户写的应用程序使用的,以上的内存空间给操作系统使用。用户内存隔离开更为安全。

用户应用程序大致结构图如下所示:

这里写图片描述

  1. .text即为代码段,只读,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

  2. .bss段包含程序中未初始化的全局变量和static变量

  3. .data段即为数据段,存放的是程序中已初始化的全局变量、静态变量

  4. 堆(heap):用于存放进程运行中被动态分配的内存段,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。其概念与数据结构中“堆”的概念不同。

  5. 栈 (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)       为48个字节
 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的值改变。推荐使用这种方法。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值