C++ 内存问题

介绍

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
  栈: 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  堆: 就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  自由存储区: 就是那些由malloc等分配的内存块,它是用free来结束自己的生命的(和 堆 很相似)。
  全局/静态存储区: 全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  常量存储区: 这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。


## 明确区分堆与栈
void function() {  int* p = new int[10]; }

这条短短的一句话就包含了堆与栈,看到 new ,我们首先就应该想到,我们分配了一块堆内存,那么指针 p 呢?
他分配的是一块栈内存,所以这句话的意思就是:
栈内存 中存放了一个指向一块堆内存的指针 p 。在程序会先确定在堆中分配内存的大小,然后调用 operator new 分配内存,然后返回这块内存的首地址,放入栈中。

为了简单并没有释放内存,那么该怎么去释放呢?是 delete p么?澳,错了,应该是delete []p

这是为了告诉编译器:我删除的是一个数组,编译器就会根据相应的Cookie信息去进行释放内存的工作。


管理方式不同

  1. 管理方式不同
  2. 空间大小不同
  3. 能否产生碎片不同
  4. 生长方向不同
  5. 分配方式不同
  6. 分配效率不同
区分
管理方式对于栈来讲,是由编译器自动管理,无需我们手工控制对于堆来说,释放工作由程序员控制,容易产生memory leak
空间大小一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。
碎片问题因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出对于堆来讲,频繁的new / delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低
生长方向生长方向是向下 ,是向着内存地址减小的方向增长生长方向向上,也就是向着内存地址增加的方向
分配方式栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是动态分配由alloca函数进行分配
分配效率栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回
内存碎片很多

常见的内存错误及其对策

内存分配未成功,却使用了它:

内存分配可能会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用mallocnew来申请内存,应该用if(p==NULL)
if(p!=NULL)进行防错处理。

内存分配虽然成功,但是尚未初始化就引用它

一是没有初始化的观念; 二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

内存分配成功并且已经初始化,但操作越过了内存的边界

例如在使用数组时经常发生下标 “多1” 或者 “少1”
的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

忘记了释放内存,造成内存泄露

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中mallocfree的使用次数一定要相同,否则肯定有错误(new/delete同理)。

释放了内存却继续使用它

deletefree之后还在使用


有三种情况:

  1. 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
  2. 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  3. 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

那么如何避免产生野指针呢?这里列出了5条规则,平常写程序时多注意一下,养成良好的习惯。

规则1:用mallocnew申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
规则2:不要忘记为数组和动态内存 赋初值 。防止将未被初始化的内存作为右值使用。
规则3:避免数组或指针的 下标越界 ,特别要当心发生“多1”或者“少1”操作。
规则4:动态内存的申请与释放必须 配对 ,防止内存泄漏。
规则5:用freedelete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。


如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

void GetMemory(char *p, int num){
    p = (char *)malloc(sizeof(char) * num);
}
void Test(void){
    char *str = NULL;
    GetMemory(str, 100); // str 仍然为 NULL
    strcpy(str, "hello"); // 运行错误
}

毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p=p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把 _p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
  如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例:

void GetMemory2(char **p, int num){
    *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void){
    char *str = NULL;
    GetMemory2(&str, 100); // 注意参数是 &str,而不是str
    strcpy(str, "hello");
    cout<< str << endl;
    
    free(str);
}

由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例:

char *GetMemory3(int num){
    char *p = (char *)malloc(sizeof(char) * num);
    return p;
}
void Test3(void){
    char *str = NULL;
    str = GetMemory3(100);
    
    strcpy(str, "hello");
    cout<< str << endl;
    free(str);
}

用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例:

char *GetString(void){
    char p[] = "hello world";
    return p; // 编译器将提出警告
}
void Test4(void){
    char *str = NULL;
    str = GetString(); // str 的内容是垃圾
    cout<< str << endl;
}

用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。
  如果把上述示例改写成如下示例,会怎么样?

char *GetString2(void){
    char *p = "hello world";
    return p;
}
void Test5(void){
    char *str = NULL;
    str = GetString2();
    cout<< str << endl;
}

函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。


“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。
“野指针”的成因主要有三种:

指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如:

char *p = NULL;
char *str = (char *) malloc(100);

指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

指针操作超越了变量的作用域范围。这种情况让人防不胜防,示例程序如下:

class A{
    public:
    void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void){
    A *p;
    {
    A a;
    p = &a; // 注意 a 的生命期
    }
    p->Func(); // p是“野指针”
}

函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了 “野指针” 。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。

参考:https://chenqx.github.io/2014/09/25/Cpp-Memory-Management/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值