内存分配方式
1、简介
在C++中,内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
- 堆:由new分配的内存,它们的释放编译器不管,由用户手动释放,如果用户不手动释放,就由操作系统自动回收
- 栈:在执行函数时,函数内部局部变量可以在栈上创建内存,退出函数时局部变量的地址自动被回收
- 自由存储区:由malloc分配的内存块,与堆十分相似
- 全局/静态存储区:全局变量和静态变量被分配到同一块存储区
- 常量存储区:存放的是常量,不允许被修改
2、常见的内存错误
- 内存分配未成功便使用它。申请了内存,但系统不一定会分配内存给你,这个时候就需要进行判断,判断指向所申请的内存的指针是否为NULL。
- 内存分配虽然成功,但没有初始化就使用它。内存的缺省初值不一定为0,关于缺省初值并没有统一的标准,所以要养成每次定义变量都初始化的习惯。
- 内存分配成功且已初始化,但访问越界。常见于数组中
- 忘记释放内存,导致内存泄漏。调用”申请内存却没有释放内存“的函数,会造成内存丢失。调用次数多了会导致内存耗尽,从而使得程序中的一些应用功能关闭,导致程序崩溃。
- 释放内存但仍使用它。一般有三种情况。
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位或以上的系统,内存够用,一般不会出现内存耗尽,但为了提高代码的质量,要求还是要进行内存耗尽的判断与处理。