[C++] C++中使用new和delete动态内存分配

本文详细介绍了C++中new和delete运算符在动态内存分配中的应用,包括基本数据类型和类的使用场景。讨论了如何在构造函数中使用new,以及new初始化对象和定位new运算符的注意事项。强调了new和delete必须正确匹配,并通过实例展示了错误使用可能导致的问题及解决方案。最后提到了拷贝构造函数和深拷贝、浅拷贝的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      在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初始化:

1.C+11之前,可以初始化简单变量:
int *pi = new int(12);
double *pd = new double(4.5);
2.C++11之后,可使用列表初始化:
struct where{double x,int y,doble z};
where *pw = new where{1,2,3};
int *arr = new int []{1,2,3,4};
3.new失败时:之前是返回空指针,现在引发异常std:bad_alloc.

1.2.new和delete运算符、函数、替换函数

使用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运算符

可以指定一块内存要使用的位置,通过定位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);
这里使用char 数组,其内部会进行强制转换;
需要注意的是,对于常规new运算符,使用delete进行释放内存,但是delete不能用于释放定位new运算符的指针,也就是说:delete只能释放指向常规运算符分配的内存的指针。

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相关的内容总结完毕,还有一点相关内容就是拷贝构造函数和深拷贝和浅拷贝问题,这点在下一篇文件进行总结。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值