C++ 内存管理

本文详细介绍了C++中的内存分区,包括堆、栈、自由存储区、全局/静态存储区和常量存储区,并讲解了常见的内存错误类型如未初始化使用、访问越界和内存泄漏。同时,讨论了数组和指针的区别,以及如何通过指针参数传递内存。此外,还阐述了野指针的危害,malloc/free与new/delete的区别和使用注意事项,以及如何处理内存耗尽的情况。最后,文章通过实例展示了如何避免野指针和内存泄漏的问题。
摘要由CSDN通过智能技术生成

1、简介

在C++中,内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

  • 堆:由new分配的内存,它们的释放编译器不管,由用户手动释放,如果用户不手动释放,就由操作系统自动回收
  • 栈:在执行函数时,函数内部局部变量可以在栈上创建内存,退出函数时局部变量的地址自动被回收
  • 自由存储区:由malloc分配的内存块,与堆十分相似
  • 全局/静态存储区:全局变量和静态变量被分配到同一块存储区
  • 常量存储区:存放的是常量,不允许被修改

2、常见的内存错误

  1. 内存分配未成功便使用它。申请了内存,但系统不一定会分配内存给你,这个时候就需要进行判断,判断指向所申请的内存的指针是否为NULL
  2. 内存分配虽然成功,但没有初始化就使用它。内存的缺省初值不一定为0,关于缺省初值并没有统一的标准,所以要养成每次定义变量都初始化的习惯。
  3. 内存分配成功且已初始化,但访问越界。常见于数组中
  4. 忘记释放内存,导致内存泄漏。调用”申请内存却没有释放内存“的函数,会造成内存丢失。调用次数多了会导致内存耗尽,从而使得程序中的一些应用功能关闭,导致程序崩溃
  5. 释放内存但仍使用它。一般有三种情况。

1、程序中的对象调用关系过于复杂,导致不清楚对象的内存是否已经被释放
2、函数的return值为在栈区的指针/引用。因为该指针/引用在退出函数的时候已经被自动摧毁。
3、使用free和delete释放了内存后,立即将指针设置为NULL,防止产生“野指针”。

3、数组和指针的区别

修改内容

  • 将常量字符串放在一个数组中,可以修改数组里面的内容
  • 用一个指针指向一个常量字符串,不能通过指针更改常量字符串中的内容
	char a[12] = "hello world";
    a[0] = 'x';
    cout << a << endl;    // "xhello world"
    char* p = "hello world";
    p[0] = 'x';
    cout << p << endl;  // "Segmentation fault"

计算内存容量

	char a[12] = "hello world"; 
	char *b[10];
    char*p = a;
    cout << sizeof(a) << endl; //往sizeof()传入数组名可计算数组的容量       12
    cout << sizeof(p) << endl;	//往sizeof()传入数组指针则是计算指针的大小,
    							//相当于计算sizeof(char*)		
    							//64位系统下sizeof(指针)大小为8 Byte
    							//32位系统下sizeof(指针)大小为4 Byte
    cout << sizeof(b) << endl;	//b是一个指针数组,数组里面存放10个char*型的指针   
    							// 64位系统下:10*8 = 80
    cout << sizeof(b[0]) << endl;  // 相当于 sizeof(char*);    8

4、指针参数如何传递内存

错误示例:

void getmemory(char* p , int num)
{
	p = malloc(sizeof(char) * num);
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	getmemory(p, 100);  //错误
	strcpy(p, "hello world"); // "Segmentation fault"
	cout << p << endl;
	return 0;
}

出错的原因在于函数getmemory(),因为传入的是char* p,在函数里面它是形参,形参是通过实参拷贝而成的,里面的内容与实参一样,但是修改形参指针的地址不会改变实参指针的地址,(函数内的形参不会改变函数外的实参)而修改形参指针指向的内容则会改变实参指针指向的内容。
解决方法
方法一:修改形参指向的内容

void getcontent(char* p , int num)
{
	strcpy(p, "hello world");
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	p = (char*)malloc(100);
	getmemory(p, 100);  
	cout << p << endl;
	free(p);
	p = NULL;
	return 0;
}

方法二:函数的形参指针为引用型指针。当形参指针为引用型指针时,形参的任何变化都会同步到函数外的实参。

#include <iostream>
#include <string.h>
#include <strings.h>
using namespace std;

void getmemory(char* &p , int num)
{
	p = (char*)malloc(sizeof(char) * num);
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	getmemory(p, 100);  
    strcpy(p, "hello world");
	cout << p << endl;
	free(p);
	p = NULL;
	return 0;
}

方法三:将指针的地址作为形参传入函数。此时形参和实参都是指向同一片内容(指针所指向的地址)(实、形参的值一样),对形参指针的地址(char** p)取址(*p)并修改为堆内存对应的地址,即可改变实参指针指向的地址

#include <iostream>
#include <string.h>
#include <strings.h>
using namespace std;

void getmemory(char** p , int num)
{
    *p = (char*)malloc(sizeof(num));
    if(*p == NULL)      //  判断防止产生野指针
        cout << "malloc error!" << endl;
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	getmemory(&p, 100);  
    strcpy(p, "hello");  
	cout << p << endl;  // hello
    free(p);    //释放堆内存,防止内存泄漏
    p = NULL;
	return 0;
}

方法四:将形参的地址返回给实参,从而改变实参的地址

#include <iostream>
#include <string.h>
#include <strings.h>
using namespace std;

char* getmemory(char* p , int num)
{
	p = (char*)malloc(sizeof(char) * num);
    return p;
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	p = getmemory(p, 100);  
    strcpy(p, "hello world");
	cout << p << endl;
	free(p);
	p = NULL;
	return 0;
}

小插曲:返回指针是一个比较简单好用的方法,但是有时候容易犯错(返回野指针)。当用指针在函数内申请一片栈空间后,往栈空间里面放东西,然后在函数结束时将该指针返回给函数外的指针,此时返回的指针是野指针,因为在退出函数时该指针申请的栈空间被系统回收,指针所指向的地址便是未知地址。而上个例子函数中的指针申请的是堆空间,在函数退出时堆内存不会被系统收回,只有在用户手动收回或者系统结束时被系统自动收回时才会被收回,所以即使函数结束,函数内的指针仍然能够返回堆内存的地址。
错误示例1:

#include <iostream>
#include <string.h>
#include <strings.h>
using namespace std;

char* getmemory(char* p , int num)
{
    char* temp = "hello";  //申请栈空间
	p = (char*)malloc(sizeof(char) * num);
    return temp;  //栈内存被回收,返回野指针
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	p = getmemory(p, 100);  
	cout << p << endl;   // ”hello“ 虽然可以正常输出,但是它已经是一个野指针了
    strcpy(p, "hello world");  // Segmentation fault
	return 0;
}

错误示例2:

char* getmemory(char* p , int num)
{
    char temp[] = "hello";  //数组申请栈空间,存放字符串
    p = temp;	//p指向数组首元素地址
    return p;  //栈内存被回收,返回野指针
}

int main(int argc, char* argv[])
{
	char* p = NULL;
	p = getmemory(p, 100); 
    cout << p << endl;    // "hello"
    strcpy(p, "world");  
	cout << p << endl;   // "world"   虽然程序看似可以正常运行,但p已经是一个野指针,存在
						//            潜在的危险
	return 0;
}

5、野指针的危害

  • 如果野指针指向不可访问的地址就会触发段错误
  • 如果指向一个可用的,但是没有明确意义的空间,虽然程序可以正确运行,然而事实上就是有问题存在,这样就掩盖了我们程序上的错误。
  • 如果指向一个可用的,而且正在被使用的空间 ,通常这样的程序都会崩溃,或者数据被损坏
    :野指针不是NULL指针
    容易出现“野指针”的例子:
class Person{
    public:
    void func(void)
    { 
        cout << "hello" << endl;
    }
};
void Test(void){
    Person *p;
    {               // 大括号里面申请的变量为局部变量,该变量离开大括号后会被系统自动回收
        Person a;
        p = &a; // a离开大括号后被系统回收,p便成为野指针
    }
    p->func(); 
}

6、malloc/free 与 new/delete

  • 共同点:都能申请堆空间(动态内存
  • 不同点:malloc/free是C/C++库中的函数,new/delete 是C++的运算符。new/delete 能够自动调用构造、析构函数,而且能够创建非内部数据类型(自己创建的类)的堆空间(int、char等属于内部数据类型)。这些malloc/free都办不到。
  • 既然new/delete看起来更“高级”,那为什么不废除malloc/free?因为C++经常要调用C函数,而C函数申请动态内存只能通过malloc/free实现
  • new和delete,malloc/free必须成对出现,如果malloc和free搭配,会导致程序找不到析构函数而出错,new和free搭配也会出现错误

new/delete使用要点

  • new 自动帮我们完成 sizeof、类型转换、安全检查。对于非内部类型的对象,还自动帮我们初始化对象
  • 使用例子:
class Person{
public:
	Person(void){
		cout << "1" << endl;		//	无参构造函数
	}
	person(int a){
		cout << a << endl;			// 有参构造函数
	}
};
int main(int argc, char* argv[])
{
	Person *a = new Person;
	Person *b = new Person(1);
	Person *c = new Person[100];    //产生100个动态对象
	//Person *d = new Person[100](1);	产生100个动态对象并赋初值1 错误做法×

	delete a;
	delete b;
	delete []c;    // 正确的删除做法,会调用所有析构函数     
	delete c; 	   //(×)只调用第一个析构函数,会报错“munmap_chunk(): invalid pointer.
				   //							Aborted (core dumped)”
	a = NULL;
	b = NULL;
	c = NULL;
}

7、内存耗尽

  • 当申请的内存块系统无法提供时,malloc/new 会返回NULL,这种情况便是内存耗尽(系统内存不够用)。
  • 遇到内存耗尽,要么用if判断然后用return返回,要么用 exit(1) 退出程序。
  • 对于32位或以上的系统,内存够用,一般不会出现内存耗尽,但为了提高代码的质量,要求还是要进行内存耗尽的判断与处理。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值