在C语言中,我们经常使用char数组来表示一个字符串,如char name[50]表示申请50个字节的内存空间来存储姓名,然而,这种做法有很多缺陷,比如如果有些人姓名长度大于50,那么就要修改内存大小,这样造成了很大的内存浪费。因此,C语言的做法是通过指针来解决这个问题,比如:
char * name;//声明char型指针
//申请内存
name = (char *)malloc(sizeof(char) * len);
//释放内存
free(name);
通过在程序运行期间动态创建内存和释放内存,可以最大化利用内存和减小内存的浪费。
比起C语言,c++中动态创建内存要简单多了,C++中使用new 和delete运算符来动态分配内存,当然,在c++同样可以使用malloc和free来申请和释放内存,但是对于一个类来说,使用malloc和free时不会执行类的构造函数和析构函数。同时,new和delete必须配对使用,new 对应delete,new [] 对应 delete [].
1.new和delete用于基本数据类型
1.1.使用new初始化:
int *pi = new int(12);
double *pd = new double(4.5);
struct where{double x,int y,doble z};
where *pw = new where{1,2,3};
int *arr = new int []{1,2,3,4};
1.2.new和delete运算符、函数、替换函数
void * operator new(std::size_t);
void * operator new[](std::size_t);
void * operator delete(std::size_t);
void * operator delete[](std::size_t);
int *p = new int;
int *p = new (sizeof(int));
1.3.定位new运算符
char buff[50];
int *p = new (buff) int;
int *p = new (buff) int[20];
void *operator new(std;:sizeof,buffer);
void *operator new(20*std;:sizeof,buffer);
2.用于类的new和delete
2.1.在构造函数中使用new
当在用户自定义类中需要动态申请内存时,一般在类的构造方法中通过new来申请,同时在析构方法中通过delete进行释放,以下是一个在构造方法中使用new的示例。
示例1:自定义MyString,实现string的功能:
自定义MyString.h:
#pragma once
#include <iostream>
class MyString
{
private:
static int s_counter;
char * m_str;
int m_len;
public:
MyString();
MyString(const char *);
~MyString();
};
MyString.cpp中:
#define _CRT_SECURE_NO_WARNINGS
#include "MyString.h"
int MyString::s_counter = 0;
MyString::MyString()
{
//每次生成对象,都会申请内存,因此s_counter+1
s_counter++;
//默认构造方法中申请长度为1的内存空间,并使m_str指向它
m_str = new char[1];
m_str[0] = '\0';
//长度为0
m_len = 0;
}
MyString::MyString(const char * ch)
{
int len = strlen(ch);
//每次生成对象,都会申请内存,因此s_counter+1
s_counter++;
//申请ch.len+1的内存空间,并使m_str指向它
m_str = new char[len + 1];
//复制数据到m_str指向的内存
strcpy(m_str, ch);
//长度
m_len = len;
}
MyString::~MyString()
{
//调用析构函数时每次-1
--s_counter;
std::cout << m_str << "was deleted " << s_counter << " left" << std::endl;
//释放内存空间
delete[] m_str;
}
main.cpp中:
int main()
{
//每次实例化对象,都会调用其构造方法,在构造方法中申请内存空间
MyString ms("this is why we play");
return 1;
}
如此一来,每当获取一个MyString对象,就会调用其构造方法,并且在构造方法中申请内存空间存储传入的参数值,当方法执行完毕后,自动调用析构函数 ,在析构函数中进行内存的释放。 在上例中,定义了一个静态成员变量用来记录申请和释放的内存空间次数:
static int s_counter;
之所以使用静态成员,是因为静态成员不和对象关联,无论创建多少个对象,都只有一份变量的副本,同时静态变量不能在类声明时初始化,必须在类外进行初始化,如上例中的:
int MyString::s_counter = 0;
注意事项
如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。
new和delete必须兼容,new对应delete,new[]对应delete[].
如果有多个构造函数,则必须以相同的方式使用new,这是因为析构函数只有一个,要保持new和delele匹配。
2.2.使用new初始化对象
在上例中,是通过构造函数给对象的指针变量分配内存,如果要为一个对象分配内存,可以使用new进行初始化,如:
MyString *myS = new MyString;//调用默认构造
MyString *myS2 = new MyString("get it");//调用参数为const char*的构造
MyString *myS3 = new MyString(*myS2);//调用拷贝构造函数
以上三句都表示为当前对象申请内存空间,对于第一句,会调用默认构造函数生成一个匿名对象,再将该匿名对象的地址赋给myS。对于第二句,会调用参数为const char *的构造函数生成一个匿名对象,再将该匿名对象的地址赋给myS2。
对于第三句,会调用拷贝构造函数生成一个匿名对象,再将该匿名对象的地址赋给myS3。
如果程序不再需要对象时,必须使用delete释放对象(所占的内存空间),这将会释放对象的内存空间,但是不会释放对象的类中成员指针指向的内存,而释放成员指针指向的内存是在析构函数中,如:
//释放myS2所指的内存空间,但不会释放myS2->m_str,当函数执行完毕后,
//自动调用析构函数释放了myS2->m_str的内存空间
delete myS2;
那么什么时候会自动调用析构函数呢,这个对象的存储持续性有关系:
1.如果是自动变量,则在执行完该对象所在的代码块时,调用该对象的析构函数;
2.如果对象时静态变量,则在程序结束时调用析构函数;
3.如果对象时new创建的,则只有显示使用delete时,才会调用其析构函数。
此外,在使用对象指针时,可以有以下几种使用方式:
//1.可以使用常规表示法类声明对象指针:
MyString *myP1;
//2.可以将对象指针初始化为已有对象,这时myP2指向ms的内存空间
MyString ms("Test");
MyString *myP2 = &ms;
//3.可以使用new初始化对象指针,这将通过调用构造函数创建一个新对象,如果要释放对象,则必须使用delete
MyString *myP3 = new MyString("Android");
delete myP3;//删除对象,释放空间
2.3.对象的定位new运算符
对于基本数据类型的定位new运算符已经说过了,就是通过char数组申请指定的内存,对于对象的定位new运算符使用时可能会引起一些问题,下面从示例中使用定义new运算符进行相关操作:
示例2.MyString类使用定位new运算符申请内存空间:
int main()
{
char *buff = new char[512];
MyString *myP1 = new (buff) MyString("Java");
MyString *myP2 = new (buff) MyString("C++");
cout << "myP1 addr:" << (void *)myP1 << endl;
cout << "myP2 addr:" << (void *)myP2 << endl;
cout << "myP1:" << *myP1 << endl;
delete[] buff;
//定位new运算符不能使用delete
//delete myP2;
return 1;
}
运行结果如下:
可以看到,这里有两个问题:
1.覆盖原有的内存空间;
2.使用delete [] buff时,没有调用析构函数,从而无法释放成员指针指向的内存空间;
因此,如果要在对象中使用定位new运算符,程序员必须要对内存单元进行管理。对于第一个问题,可以设置一个内存空间偏移量来避免:
MyString *myP2 = new (buff+sizeof(MyString)) MyString("C++");
对于第二点,之所以不使用delete myP1,是因为delete不能和new定位运算符配合使用,是因为指针myP1并没有收到定位new运算符返回的地址,又因为指针指向的地址和buff相同,而buff是new []申请的,因此使用delete []释放内存,因此就不会调用析构函数了,这种情况下,需要显示地调用析构函数:
myP1->~MyString();
myP2->~MyString();
再次运行,析构函数调用,这样就可以释放对象的成员指针所指向的内存空间了:
现在,对于new和delete相关的内容总结完毕,还有一点相关内容就是拷贝构造函数和深拷贝和浅拷贝问题,这点在下一篇文件进行总结。