动态内存分配
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配,函数malloc
和free
可以在运行时从堆中分配存储单元。
1、malloc — 申请动态内存
malloc(sizeof(num))
向系统申请sizeof(num)
字节的动态内存,内存于“堆”里存放,若申请成功,则函数返回(void * 指针类型),失败则返回NULL,并且申请之后的内存中并没有初始化。该函数需要引用头文件—stdlib.h
。
由于“堆”有一个特性——由程序自行管理内存,所以在申请了动态内存之后,需要利用free()
自行释放,这是为了避免出现野指针,并且把指向这块内存的指针指向NULL,防止之后的程序再用到这个指针。如果不自行释放的话,就会造成内存泄露——可用内存越来越少,设备速度越来越慢。
malloc(num)
代码:
void test()
{
int *pArr=malloc(sizeof(int)*10); //堆区申请10个int类型大小的内存
int i;
for(i=0;i<10;i++)
{
pArr[i]=10+i;
}
int j;
for(j=0;j<10;j++)
{
printf("%d ",pArr[j]);
}
free(pArr);
pArr=NULL;
}
2、calloc — 申请动态内存且进行初始化
函数原型:void *calloc(size_t nitems, size_t size)
nitems
– 要被分配的元素个数。size
– 元素的大小。- 返回一个指针,指向已分配的内存。如果请求失败,则返回 NULL。
calloc(nitems,size)
代码:
void test()
{
int *pArr=calloc(10,sizeof(int));
int i=0;
for(;i<10;i++)
{
printf("%d ",pArr[i]); //被初始化为0
}
char *pArr1=calloc(10,sizeof(char));
int k=0;
for(;k<10;k++)
{
pArr1[k]=k+65;
}
int j=0;
for(;j<10;j++)
{
printf("%c ",pArr1[j]); //A B C D E F G H I J
}
}
3、realloc — 对原动态内存块进行扩容
函数原型:void realloc(void ptr, size_t size)
尝试重新调整之前调用 malloc
或 calloc
所分配的ptr
所指向的内存块的大小
ptr
– 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用malloc
、calloc
或realloc
进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。size
– 内存块的新的大小,以字节为单位。如果大小为 0,且ptr
指向一个已存在的内存块,则ptr
所指向的内存块会被释放,并返回一个空指针
realloc(ptr,size)
代码:
void test()
{
int *ptr=malloc(sizeof(int)*5);
int *re_ptr=malloc(sizeof(int)*10);
printf("旧地址:%p\n",ptr); //旧地址:00000000001813E0
printf("新地址:%p\n",re_ptr); //新地址:0000000000181400
}
4、free — 释放内存空间
千万别忘记释放内存块,这会造成内存泄露这样严重的问题
free()
函数只需要将指向该内存块的指针作为参数传入,就可以释放这块内存
**注意:**释放的是这一块内存,而不是指向这一块内存块的指针p,所以在free§之后,有必要将p赋值为NULL,避免之后的程序用到它,程序会崩溃的,因为这是一个野指针。
free(p)
代码:
free(p);
p=NULL;
对象创建
当创建一个c++对象时会发生两件事:
- 为对象分配内存
- 调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。
c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。
1、C++创建对象时C动态分配内存函数的缺陷
- 程序员必须确定对象的长度。
malloc
返回一个void指针,c++不允许将void*赋值给其他任何指针,必须强转。malloc
可能申请内存失败,所以必须判断返回值来确保内存分配成功。- 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。
class Person{
public:
Person(char *name,int age)
{
m_Name=(char *)malloc(strlen(name)+1);
strcpy(m_Name,name);
m_Age=age;
}
void Init()
{
m_Name=(char *)malloc(strlen("卢本伟")+1);
strcpy(m_Name,"卢本伟");
m_Age=35;
}
void Clean()
{
if(m_Name)
{
free(m_Name);
m_Name=NULL;
}
}
char *m_Name;
int m_Age;
};
void test()
{
Person *person=(Person *)malloc(sizeof(Person)); //未调用构造函数,但用户又不能显式调用构造函数(无法初始化对象)
if(!person)
return ;
//调用初始化函数---初始化对象
person->Init();
cout<<"姓名:"<<person->m_Name<<" 年龄:"<<person->m_Age<<endl;
//清理对象成员内存
person->Clean();
//清理对象内存
free(person);
person=NULL;
}
2、new运算符
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。
当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
Person *person=new Person;
//底层实现相当于
Person *person=(Person *)malloc(sizeof(Person));
if(!person)
return ;
person->Init(); //相当于构造函数
New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
new运算符有内置的长度计算、类型转换和安全检查
3、delete运算符
delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。
如果使用一个由malloc
或者calloc
或者realloc
的对象使用delete
,这个行为是未定义的。因为大多数new
和delete
的实现机制都使用了malloc
和free
,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Student{
public:
Student()
{
m_Name=(char *)malloc(strlen("宫本武藏")+1);
strcpy(m_Name,"宫本武藏");
m_Age=45;
cout<<"默认构造调用"<<endl;
}
Student(char *name,int age)
{
m_Name=(char *)malloc(strlen(name)+1);
strcpy(m_Name,name);
m_Age=age;
cout<<"有参构造函数调用"<<endl;
}
~Student()
{
if(m_Name)
{
free(m_Name);
m_Name=NULL;
}
cout<<"析构函数调用"<<endl;
}
char *m_Name;
int m_Age;
};
void test()
{
Student *stu=new Student; //默认构造调用
cout<<"姓名:"<<stu->m_Name<<"年龄:"<<stu->m_Age<<endl; //姓名:宫本武藏年龄:45
Student *stu1=new Student("娜可露露",50); //有参构造函数调用
cout<<"姓名:"<<stu1->m_Name<<"年龄:"<<stu1->m_Age<<endl; //姓名:娜可露露年龄:50
delete stu;
delete stu1;
}
用于数组的new和delete—堆区
1、创建一般类型(int、char…)数组
//创建整型数组
int *IArr=new int[10];
//创建字符数组
char *CArr=new char[50];
//创建整型数组并初始化
int *pArr=new int[5]{1,2,3,4,5};
//释放内存
delete IArr;
delete CArr;
delete pArr;
2、创建对象数组
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
class Person{
public:
Person():m_Name("不知火舞"),m_Age(55){cout<<"默认构造调用"<<endl;}
Person(string name,int age):m_Name(name),m_Age(age){cout<<"有参构造调用"<<endl;}
Print()
{
cout<<"姓名:"<<m_Name<<" 年龄:"<<m_Age<<endl;
}
~Person(){cout<<"析构调用"<<endl;}
string m_Name;
int m_Age;
};
void test()
{
//1.栈上聚合初始化
Person person[]={Person("后羿",45),Person("鲁班七号",41)};
int len=sizeof(person)/sizeof(person[0]);
for(int i=0;i<len;i++)
{
person[i].Print();
}
cout<<"--------------------------------"<<endl;
//2.在堆区通过默认构造初始化
Person *Pperson=new Person[3];
Pperson->Print();
delete[] Pperson;
}
测试结果:
有参构造调用 有参构造调用 姓名:后羿 年龄:45 姓名:鲁班七号 年龄:41 -------------------------------- 默认构造调用 默认构造调用 默认构造调用 姓名:不知火舞 年龄:55 析构调用 析构调用 析构调用 析构调用 析构调用
3、delete void*
如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。
class Student{
public:
Student(char *name,int age)
{
m_Name=(char *)malloc(strlen(name)+1);
strcpy(m_Name,name);
m_Age=age;
cout<<"有参构造调用"<<endl;
}
~Student()
{
if(m_Name)
{
free(m_Name);
m_Name=NULL;
}
cout<<"析构调用"<<endl;
}
char * m_Name;
int m_Age;
};
void test()
{
void * stu=new("安琪拉",30); //有参构造调用
cout<<"姓名:"<<stu->m_Name<<" 年龄"<<stu->m_Age<<endl;
delete stu; //未调用析构函数
}
//[Warning] deleting 'void*' is undefined
4、使用 new 和 delete 采用相同的形式
Student *stu=new Student[10];
delete stu;
-
测试结果:
默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 默认构造调用 析构调用
-
分析:
使用了new也搭配使用了delete,问题在于Student有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。
我们现在清楚使用new的时候发生了两件事:
- 分配内存;
- 调用构造函数。
那么调用delete的时候也有两件事:
- 析构函数;
- 释放内存。
那么刚才我们那段代码的问题在于:
stu
指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,
stu
指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。
当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[ ],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
-
结论:
当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
Student *stu=new Student[3]; delete []stu;
测试结果:
默认构造调用 默认构造调用 默认构造调用 析构调用 析构调用 析构调用
默认构造调用 析构调用 析构调用 析构调用