C++析构函数:对象销毁时的内存释放
🧑💻 User
In this lecture, we’re going to learn about the structures.The structures are spatial functions or methods in a class that are called by the compiler to destroyyour object.And these are going to be especially useful if you are, for example, allocating memory in your constructors.Let’s look at a simple example Here we have a class called Dog.It has two sections the public section and the private section.In the public section we have two constructors, a default constructor that doesn’t take any parameterand a constructor that takes two parameters and the parameters are going to be used to initialize ourmember variables and they are passed by value here for simplicity.And they are going to be used to initialize these member variables dog name, dog breed and dog age.The special thing about this class is that it has a destructor and you declare the destructor usingthis syntax here.It puts a tilde character in front.You say the name of the class and then you put an empty parameter list.It is also possible to declare a destructor and put in its entire body in the header.And this is the syntax to do this inside the body, you’re going to do whatever it is you need to destroyyour object.For example, if the class here is allocating some memory in the constructor, the destructor is thebest place to release that.And this is an example of how you can do that.Here is a more direct example of how we allocate for dynamic memory in the constructor and release thatmemory in the destructor.We have our three variables, but we have chosen to allocate our dog age variable on the heap usingdynamic memory allocation.And after we dynamically allocate a piece of memory to store our age variable, we’re going to set thatto zero because we are in a constructor.If we are using the three parameter constructor here, we might use the past parameter to initializeour age variable.But the important thing here is that it is being dynamically allocated memory on the heap here.And when our object dies, we need to remember to release that memory and the destructor is the bestplace to release that memory.As you see here, we say delete dog age when our object dies and the compiler is going to call thisfunction when it’s about to kill the object.So you might ask, when are Destructors really called?We are going to start by looking at obvious cases here.For example, when a local stack object goes out of scope, the destructor is going to be called becausethat object is going to be wiped out from memory.So this is an obvious case in which the compiler will need to call a destructor.Also, when a heap object is released from memory using delete, the destructor is also going to becalled.But there are some indirect ways in which the structures are also called.For example, when an object is passed by value to a function, this is going to call the destructorto kill the object when the object goes out of scope in the function.Because when an object is passed by value, we’re going to have a copy in the local scope of the function.And when the function exits, that local copy is going to be killed.And we’re going to see an example of that.Another case where you might have the structure called is when a local object is returned from a function,another copy is going to be created to be used outside the function and the copy inside the functionis going to be killed.But this is not guaranteed as some compiler may optimize your return value and use some kind of referenceso you’re not guaranteed to see this behavior, but it is possible to see it now that you have an ideaabout Destructors, it’s time we headed over to Visual Studio Code and play with us a little more.Okay, here we are in our working folder.The current project is Destructors.We’re going to grab our template files pretty quick and we’re going to put them in place.And after that we’re going to open our folder in Visual Studio code.By dragging and dropping here, this is going to open our folder and we will see our main CPP file here.We can open it and a clean up and remove what we don’t need.We have an unneeded breakpoint here.We can remove that and we’re going to clean up the main function.The first thing we want to do is to put in our dog class.I am going to just put this in here because I don’t want you to watch me type all this.That’s just going to waste your time.We have two sections in the dog class.We have a public section which is going to now contain our constructors and we have a private sectionwhich is going to contain three member variables.We have two strings, one for.The name and the other for the breed of dog.And we have another one which is an int pointer, which is going to store the age of the dog and yousee that this is initialized to null pointer.One thing you should notice is that for our parameter taking constructor, we are passing the stringinformation here using string views to avoid copies with the STD string view type.You already know this because we learnt about this, but for this to work we will need to include thestring view.So let’s do that.And now we have our class and we can use it however we want.So but before we do, we need to put in the implementations for this functions.One way we can do that is do that directly in the function here, but I want you to see that you canalso do that directly outside the class here.I don’t think I have shown you this, but it is possible to do that.So we’re going to grab the prototype for our constructor and say the scope resolution operator and we’regoing to just paste in what we have copied, which is the prototype here, and we’re going to go inside,put in our body and in the body we’re going to initialize these three guys.So the name is going to be initialized to name Param and the breed is going to be initialized to breedparam.We can do that and the age is going to be dynamically allocated from the heap.You see that it is a pointer.So we’re going to say p equals new and because we’re going to allocate dynamic space to store an INTand we’re going to put data in using our good old dereference operator, we’re going to say P equalswhatever page parameter that you passed.And I think we should say age here because it’s not a pointer.So we’re going to say age.I think this is going to do better.H Param This is probably going to be better and we’re going to say age param.Okay, so now we have our constructor and, and it’s going to allocate memory from the heap and we’regoing to put data in that new memory location that has just been allocated to our class here.So this is our class and we can go in the main function and really use it however we want.For example, we can set up a dog object, so we’re going to say my dog and we’re going to give it aname.The name is going to be fluffy and we’re going to put the breed in and it’s going to just be a shepherdand we’re going to put in the age, which is going to be, for example, a two let’s say our dog istwo years old.This is a valid C plus plus code.It’s going to be setting up an object of our class here and we can build to see that this program isgoing to build without a problem.So both is good.But if we run the program, we’re not going to see anything.But we don’t really need to see our constructor here called because we already know about constructors.What we want to learn about are Destructors, and these are special methods that are called by the compiler.When your object dies.Let’s set up a destructor and see it called When Our Dog Objects Die.The syntax to do that is to use the tilde character here.You’re going to say the name of the class.You’re going to put an empty parameter list.And if you want, you can put a body and do whatever you want to do in the body of your destructor here.But again, we’re going to move the entire definition of the destructor to the outside.So let’s go down and save that.We’re going to say the scope resolution operator.We’re going to say dog and we’re going to put in our body.And the first thing we want to do in our destructor is to release this memory that was dynamically allocatedfor us.If we don’t release it when this dog object dies, we’re going to leak memory.And that’s really bad.So let’s do that directly in the destructor.We can say delete and say page.This is going to release the memory that was allocated in the constructor here.And we have access to this page variable because of the destructor is part of our class.Any member function of your class is going to have access to any member variable, even if it happensto be private.Okay, so we forgot our tilde character.Let’s put that in here and this is going to be valid C plus plus code.But to really see things happening here, let’s put in an Stdcout statement.We’re going to say dog destructor called for and we’re going to say the name of the dog, let’s sayname, and we’re going to say the same thing in the constructor.We’re going to say Stdcout and say dog constructor called for and we’re going to say the dog name goingto use our member variable.And now we have our thing in place.We have the destructor declared in the class definition, and this is the syntax again, this is whatwe really want to learn about in this lecture.We have the tilde character, we have the class name and we put an empty parameter list.Let me tell you that it is not possible to pass parameters to your destructor.If you do that, you’re going to get a compiler error.Destructors were always have no parameters in C plus plus.Here we have the definition of our destructor and it is going to release whatever memory we have dynamicallyallocated from the constructor here.If we run this program, we’re going to see our constructor called as a result of this line here, becausewe are constructing a dog object and we’re going to see the destructor called when the main functionis about to end.Let’s put an stdcout statement to the main function here and say done.Just to prove that we have hit the end of our main function here.I think this is useful and we’re going to build.So let’s do that.We’re going to build with GCC.You’re going to see that the build is good so we can clear and run our program.Let’s hit enter here and clear and run, Rooster.You’re going to see that the constructor was called.We are going to say done when the main function is about to end.But before the main function ends, we’re going to destroy any stack data that we have in the main functionand our dog object is on the stack.So it’s going to be destroyed as part of that process.And the destructor for dog is going to be called and we’re going to release our memory and we’re goingto say that dog destructor was called for whatever name we have assigned to our dog here.So this is how Destructors are called when your objects are killed.And we didn’t put in the correct name, we said Fluffly, let’s say Fluffy, because that’s what I wanted.And now you see that our destructor is really being called and releasing memory however we want.But we can also go a little further and show you another example of a case where Destructors are calledand we’re going to look at a function that sets up a local object of a dog.We’re going to see the dog constructor and we’re going to see it destroyed when the function is aboutto end.Let’s do that.We’re going to say void Somefunc and it’s going to be just some function to play with us.We’re going to move our dog declaration and some func here and this is really it.We’re going to call this function and when we call it, we’re going to construct the object here.When the function is about to end, the compiler is going to call the destructor because it’s goingto destroy this object here.And we’re going to see the destructor called.By that point, the somefunc function is going to be popped off the top of the stack.We’re going to hit the main function here and we’re going to say done.And the program is going to end.Let’s call some function main here.So we’re going to say some func and we’re going to world to really see this happening.So we have built successfully, we can clear, clear properly and run Rooster we’re going to see constructorcalled for Fluffy destructor called for fluffy and you see done.We have hit the main function and we finished executing our program here.We can even see this through the debugger.We’re going to set up three break points.One in the main function here before we call the Somefunc function, we’re going to set up another onein our destructor before we print the message here.And we’re going to put another one in the constructor when we are about to print this message here online 20.After we set up these break points, we can kill the terminal here and minimize a little bit.Let’s kill these things here so that we have a clean we have a lot of windows here.Okay.We have our editor here.And now if we go to the tab here that says start or run, we’re going to be able to click the greenbutton here to start our debugging session.We are going to have our terminal here.So let’s bring that to the side so that we can see things printed out nicely.Let’s do like this.And now we have hit the breakpoint.In the main function we can step into the somefunc function and we’re going to do that using the stampingtwo button here.Let’s click that.We’re going to jump into some func.Now we have the option to step into, but this is going to jump us into the logic of transforming thestring literal here into a string view.Because if you see the constructor is going to take string view parameters, we can actually see this.Let’s try and hit the step into button.You see that we are in the string view class and we don’t really want this.So let’s step out and we’re going to step over this so that we can hit the next breakpoint.And you see, the next breakpoint is in our constructor.And this proves that the compiler is going to call our constructor here to build our object.If we step over this, we’re going to print the message from the constructor.And if we.We keep going, we’re going to get out of our constructor and you see where this is going to go.This is going to go in the destructor because before we leave the Somefunc function, our data objectis going to be destroyed.So we’re going to release our memory and we’re going to print the message from our constructor.And if we hit next and step out of this, I think we can do that because this is logic we don’t want.We’re going to try and step over this.And now if we hit next, we’re going to come back in some func and some func is already going to befinished.And if we hit next, we’re going to come back in main and we’re going to print done and the programis basically going to be done.Hopefully you can see that the compiler is going to call our destructor after it leaves some func herebecause it’s going to be destroying the local object that we have inside some func here.So this is one case where the destructor is going to be called by the compiler and it is if you havea local object that needs to be destroyed when function scope finishes.Another case where the destructor may be called is if you pass a class object parameter by value.So let’s change some func here and make it take something by value.So we’re going to pass a dog inside and we’re going to say dog param, this is going to do okay.Now that we have changed some func here, we can really do all kinds of crazy things with it.For example, we can set up our dog object inside our main function here.So let’s do that.We can say dog and dog.The variable name is going to be dog.We can give it a name which is going to be fluffy, it’s going to have a breed of shepherd and we cangive it the age of two like we have been doing all along.We can call our function with the dog object here, but be careful about what is going to happen herewhen we do something like this, we’re going to be passing by value and the compiler is going to seethat it needs to copy a dog object.Now, we haven’t really told our class here how to copy dog objects, and we do that through constructorswe call copy constructors we haven’t really learnt about yet.But what the compiler is going to do by default is copy these things, member by member.So it’s going to set up a dog copy object which is going to be initialized with empty strings and anull pointer here.And then it’s going to copy from our dog object that we have here.It’s going to take the name and copy that in the name member variable of our dog copy.It’s going to take the breed and copy that in in the second member variable here.And it’s going to take the age from our dog and copy that in.The problem is going to come to this pointer here.It’s going to copy the address and it’s not going to copy what is pointed to.And this is going to be a recipe for disaster.I don’t really want to go through this right now, but if we try to run this program, it’s not goingto work like we expect.For example, if we pass Fluffy here, we’re going to see things that we don’t expect.So let’s try to build.We’re going to build with GCC and the build is good.So what we expect is to have the constructor of Fluffy here called and we’re going to copy the dog objectfor use in Somefunc here.And to set up this copy, it’s not this constructor here that is going to be called, so it’s not ourconstructor here, which is going to be called.So we’re not going to see this called for the copy that we set up inside Somefunc.Another thing is because now these two dog objects are pointing to the same thing because what we justcopied is the address as we see in our member variable here, the two dog objects are going to be pointingto the same thing in memory.And when we hit the destructor, we’re going to call delete twice.And this is going to be a recipe for disaster.Our application could really crash.You already know that deleting a pointer twice is really bad.We shouldn’t do this and we are doing this here because we are copying.I realize this is a slightly confusing example, but what I want you to know is that if you pass a classobject by value like this, the copy you have inside is going to code for the destructor to be calledwhen the function exits because the copy is going to be scoped inside the function here.So let’s take out this example because it really is confusing, but I hope you note that and I am goingto show you that in other case, where you might see the destructor called is when you call the destructorexplicitly using the delete operator on your object so we can set up some func again, let’s do thatsome func and we’re going to repurpose it.And this side we’re going to set up a dog object.So let’s grab this and.And we use that in our function here and we’re going to say dog pointer.So let’s say dog pointer, we’re going to say P dog P underscore dog.It’s going to be equals to new dog and we’re going to pass in what we need to initialize our dog objectwith.This is going to create a dog object on the heap and it is going to be pointed to by p dog here.Now, I want you to see that if we don’t explicitly release the memory that we allocate through thenew operator, we’re going to leak memory.So we created an object on the heap here, but we’re not deleting that in some func.When some func exits, we’re going to lose this pointer, this variable because it is on the stack itselfand we won’t have any way to release this memory.Let’s call some func here and really show you this.So we’re going to call some func and we’re going to build.Let’s use GCC for that.The build is good.We can bring this up a little bit and clear and run Rooster.We’re going to see that the constructor for Fluffy was called because of this new operator, but wedidn’t see the destructor called and this is really bad.So for the destructor to be called, we need to explicitly release the memory in this function here.And we do that by using the delete keyword and saying the pointer that is pointing to the memory thatwe want to release.If we do this, this is going to cause for the destructor of dog to be called.And if we run, we’re going to see our destructor called.Let’s do that.We’re going to use GCC.The build is good.We can clear and run Rooster.Now you see that the constructor is called Full of Fluffy and we have the destructor called.This is really all I wanted to share with you in this lecture.I apologize if you found it confusing, but if you have any problem, I am always available.Please do ask and I will do the best I can to help you out.So the destructor is a special method which is going to be called when your object Dies.It is going to be called when an object is passed by value to a function.We have seen an example of that.It’s going to be called when a local object is returned from a function by value.But this is going to be compiler dependent because some compilers are going to optimize this out andmake it look like we returned by reference.So I don’t want to go into this because this is subject to compiler optimization, but other cases whereyou’re going to have your destructor called is when a local stack object goes out of scope and whena heap object is deleted through the delete keyword.This is really all I had to share in this lecture.I hope you found it interesting.We are going to stop here in this one and the next one we’re going to see the order in which constructorsand destructors are called and you really need to be aware of this.Go ahead and finish up here and meet me there.
🤖 Assistant
好的,同学你好!非常开心能作为你的C++向导。今天我们要学习C++中一个非常重要的概念——析构函数。别担心,我会一步一步带你了解它。
C++ 析构函数:对象的优雅告别
简要总结:
析构函数 (Destructor) 是类中的一种特殊成员函数。它的主要任务是在对象生命周期结束时,执行一些清理工作,比如释放对象在构造时动态分配的内存 (dynamic memory allocation)。编译器会自动调用析构函数。
详细解释:
想象一下,你开了一家玩具店。
- 构造函数 (Constructor):就像你进货并把玩具摆上货架的过程。当你创建一个对象时,构造函数负责初始化这个对象,比如给它的成员变量赋值,或者为它申请一些资源(比如内存)。
- 析构函数 (Destructor):就像你关店前,需要把没卖掉的、租来的展示架还回去,打扫卫生等。当一个对象的生命周期结束时(比如它离开了自己的作用域 (scope),或者被显式删除),析构函数就会被自动调用,来执行一些“善后”工作。
1. 为什么需要析构函数?
最常见的场景是管理动态分配的资源。如果在构造函数中使用了 new
关键字在堆 (heap) 上为对象分配了内存,那么在对象销毁时,你就需要在析构函数中使用 delete
关键字来释放这块内存。如果不这么做,这块内存就会一直被占用,无法被其他程序使用,造成内存泄漏 (memory leak)——这就像你借了书不还,图书馆的书越来越少一样,最终会导致程序因内存耗尽而崩溃。
2. 析构函数的语法
析构函数的声明非常特殊:
- 它没有返回类型(连
void
都不写)。 - 它的名字必须是类名的前面加上一个波浪号
~
。 - 它不能有任何参数。
例如,如果你的类名是 Dog
,那么析构函数就是 ~Dog()
。
1 | class Dog { |
3. 析构函数何时被调用?
编译器会在以下几种主要情况下自动调用析构函数:
栈 (Stack) 对象的生命周期结束:当一个在函数内部定义的局部对象(栈对象)离开其作用域时(例如,函数执行完毕),它的析构函数会被调用。
cpp1
2
3
4void myFunction() {
Dog myDog("Buddy", 3); // myDog 是一个栈对象
// ... myDog 在这里被使用 ...
} // 当myFunction结束时,myDog的析构函数会被自动调用堆 (Heap) 对象被
delete
操作符 (delete operator) 删除时:当你使用new
创建一个堆对象,并使用delete
删除它时,该对象的析构函数会在内存被释放前调用。cpp1
2
3
4
5
6
7int main() {
Dog* pDog = new Dog("Charlie", 5); // pDog 指向一个堆对象
// ... pDog 在这里被使用 ...
delete pDog; // 首先调用pDog指向对象的析构函数,然后释放内存
pDog = nullptr;
return 0;
}重要:如果你
new
了一个对象但忘记delete
,析构函数将不会被调用,资源(如内存)也不会被释放,从而导致资源泄漏。对象按值传递给函数时(副本的销毁):
当一个对象按值传递给一个函数时,会创建该对象的一个副本。当函数结束时,这个副本的生命周期也就结束了,它的析构函数会被调用。cpp1
2
3
4
5
6
7
8
9
10void processDog(Dog d) { // d 是 aDog 的一个副本
std::cout << "Processing dog in function..." << std::endl;
} // 当 processDog 结束时,副本 d 的析构函数会被调用
int main() {
Dog aDog("Rex", 2);
processDog(aDog); // aDog 被按值传递,创建副本
std::cout << "Back in main." << std::endl;
return 0; // aDog 的析构函数在这里被调用
}注意:按值传递对象,尤其是包含动态分配内存且没有正确定义拷贝构造函数和拷贝赋值运算符的类对象,可能会导致问题(比如浅拷贝 (shallow copy) 导致的双重释放 (double free)),我们之后会学到如何正确处理这种情况(通常涉及“三/五法则”)。
从函数按值返回局部对象时(临时副本的销毁,可能被优化):
当函数返回一个局部对象时,可能会创建一个临时对象用于返回值。这个临时对象的析构函数在其不再需要时会被调用。但是,现代编译器通常会进行返回值优化 (Return Value Optimization, RVO) 或命名返回值优化 (Named Return Value Optimization, NRVO),这可能会消除临时对象的创建和销毁,所以你可能不会总是观察到这种行为。cpp1
2
3
4
5
6
7
8
9Dog createDog() {
Dog tempDog("Lucy", 1);
return tempDog; // 返回一个局部对象
} // tempDog 的析构函数通常在这里被调用,但RVO可能改变行为
int main() {
Dog mainDog = createDog();
return 0;
}
4. 析构函数的实现细节
- 定义位置:析构函数可以在类定义内部直接定义,也可以在类外定义(像上面的
Dog::~Dog()
例子)。 - 无参数:析构函数绝对不能有参数。如果你尝试给它参数,编译器会报错。
- 隐式析构函数:如果你没有为类显式定义析构函数,编译器会自动生成一个默认的析构函数。这个默认析构函数通常什么也不做,除非类成员本身是其他类的对象且那些类有自己的析构函数(这种情况下,默认析构函数会调用成员对象的析构函数)。对于动态分配的内存,默认析构函数不会做任何释放操作。
代码示例:
让我们看一个完整的例子,演示构造函数和析构函数如何配合工作来管理动态分配的内存。我们将使用课程中提到的 Dog
类。
1 |
|
预期输出分析:
你会看到构造函数和析构函数的 std::cout
语句交替出现。特别注意 some_function_stack
和 some_function_heap
中对象的创建和销毁顺序,以及 main
函数中对象的销毁时机。
QA闪卡 (QA Flash Cards):
问:什么是析构函数 (Destructor)?
答: 类中的一个特殊成员函数,在对象生命周期结束时自动调用,用于执行清理工作,如释放动态分配的资源。问:如何声明一个析构函数?
答: 使用波浪号~
加上类名,例如~MyClass()
。它没有返回类型,也没有参数。问:析构函数最常见的用途是什么?
答: 释放在构造函数中或对象生命周期内动态分配的内存 (dynamic memory allocation) 或其他资源(如文件句柄、网络连接)。问:什么时候析构函数会被调用?(至少列举两种情况)
答:- 当栈 (stack) 上的局部对象离开其作用域 (scope) 时。
- 当堆 (heap) 上的对象被
delete
操作符 (delete operator) 删除时。 - (可选)当按值传递给函数的对象副本在函数结束时。
问:如果我创建了一个对象
MyClass* obj = new MyClass();
但忘记delete obj;
,会发生什么?
答: 该对象的析构函数不会被调用,它占用的堆内存也不会被释放,导致内存泄漏 (memory leak)。
常见误解或新手易犯错误:
- 忘记释放动态分配的内存:这是最常见的错误。如果在构造函数中用了
new
,就必须在析构函数中用delete
(如果是数组用new[]
,则用delete[]
)。否则会导致内存泄漏。- 错误示例:析构函数为空,但构造函数中
p_dog_age_ = new int;
- 错误示例:析构函数为空,但构造函数中
- 对非动态分配的内存使用
delete
:delete
只能用于通过new
分配的指针。对栈上对象的地址或已释放的指针使用delete
会导致未定义行为(通常是程序崩溃)。- 错误示例:
int x; delete &x;
(x是栈变量)
- 错误示例:
- 重复释放内存 (Double Free):同一个指针被
delete
两次。这通常发生在多个指针指向同一块动态分配的内存,并且都尝试释放它时,或者不小心拷贝了原始指针并分别删除。这也会导致程序崩溃。- 这与课程中提到的“按值传递类对象”时的潜在问题有关。如果类有一个原始指针成员,并且你没有定义自己的拷贝构造函数,那么按值传递时,会创建一个“浅拷贝”,使得副本和原始对象的指针成员指向同一块内存。当副本销毁时,它会
delete
这块内存;当原始对象销毁时,它会再次delete
已经被释放的内存,Boom!
- 这与课程中提到的“按值传递类对象”时的潜在问题有关。如果类有一个原始指针成员,并且你没有定义自己的拷贝构造函数,那么按值传递时,会创建一个“浅拷贝”,使得副本和原始对象的指针成员指向同一块内存。当副本销毁时,它会
- 认为指针变量离开作用域就会自动释放其指向的堆内存:必须显式调用cpp
1
2
3void leaky_func() {
int* num_ptr = new int(10);
} // num_ptr (栈上的指针变量) 消失了,但它指向的堆内存 (存储10的那块) 还在,并且无法访问了 -> 内存泄漏delete num_ptr;
才能释放堆内存并调用析构函数(如果num_ptr
指向的是类对象)。 - 给析构函数添加参数或返回类型:析构函数有严格的语法规定:无参数,无返回类型。
编码练习:
下面是一个 ResourceHolder
类,它在构造时获取一些“资源”(这里用动态分配的 int
模拟)。请你为它补全析构函数,确保资源被正确释放。
1 |
|
请你思考一下,// TODO:
部分应该填入什么代码,然后把你的实现方案告诉我。我会根据你的答案给出反馈。加油!
🧑💻 User
1 |
|
🤖 Assistant
非常好!你已经成功地为 ResourceHolder
类实现了析构函数,并且正确地在 main
函数中处理了堆对象的删除。做得不错!
7. 用户解答评估、正确答案及解释
用户解答评估:
析构函数实现:
delete m_resource;
: 正确。你正确地使用了delete
来释放m_resource
指向的动态分配的内存。std::cout << "ResourceHolder memory has been released" << std::endl;
: 基本正确。打印了释放消息。为了更清晰,最好能指明是哪个对象的资源被释放了,比如包含m_name
。- 小建议:在
delete m_resource;
之后,通常的好习惯是加上m_resource = nullptr;
。虽然在这个特定的析构函数场景下,对象即将被销毁,m_resource
这个成员变量本身也将不复存在,所以悬挂指针的风险相对较小。但在其他可能多次操作指针的场景下(比如一个release()
方法),这是一个非常重要的防止悬挂指针 (dangling pointer) 的措施。
main
函数中的TODO
:delete rh2;
: 正确。你正确地删除了在堆上创建的rh2
对象。- 注释
// 不delete会内存泄漏, 并且不会调用析构函数
: 理解完全正确! 如果忘记delete rh2;
,rh2
指向的ResourceHolder
对象的析构函数将不会被调用,其内部通过new int()
分配的内存也不会被释放,从而导致内存泄漏。
综合评价:优秀!核心功能都已正确实现。
正确答案与解释:
这是修改后更完善的版本:
1 |
|
解释关键点:
析构函数
~ResourceHolder()
:delete m_resource;
: 这是核心。它告诉C++运行时系统,m_resource
指针所指向的那块在堆上分配的内存现在可以被回收了。m_resource = nullptr;
: 将m_resource
设置为nullptr
是一个防御性编程措施。如果在析构函数被调用后(尽管不太可能对已销毁对象做操作)或者在类的其他方法中有可能在资源释放后意外访问m_resource
,将其设为nullptr
可以防止使用一个无效的(悬挂的)指针。对空指针进行delete
是安全的(什么也不做),但对悬挂指针进行操作是未定义行为。- 更详细的析构信息:
std::cout << "ResourceHolder '" << m_name << "' is being destroyed..." << std::endl;
能够帮助我们清晰地追踪是哪个对象的析构函数被调用了。
main
函数中的delete rh2;
:rh2
是一个指向在堆上创建的ResourceHolder
对象的指针。delete rh2;
做了两件事:- 调用
rh2
所指向的ResourceHolder
对象的析构函数 (~ResourceHolder()
)。 - 释放该对象本身占用的堆内存。
- 调用
rh2 = nullptr;
: 在delete rh2;
之后,rh2
本身仍然存储着之前那个对象的内存地址(现在那个地址上的内容是无效的了)。将rh2
设置为nullptr
可以防止后续代码不小心通过rh2
访问已经被释放的内存。
8. 建议与扩展问题
进一步的建议:
- RAII (Resource Acquisition Is Initialization):你刚才写的其实就是 RAII 思想的一个简单体现!RAII 是一种强大的C++编程范式,即在对象的构造函数中获取资源,在析构函数中释放资源。这样可以确保资源在对象生命周期内被妥善管理,即使发生异常也能正确释放。
- 智能指针 (Smart Pointers):对于动态内存管理,C++标准库提供了智能指针(如
std::unique_ptr
和std::shared_ptr
),它们可以自动管理所指向对象的生命周期,大大减少忘记delete
导致的内存泄漏。- 可以尝试将
int* m_resource;
修改为std::unique_ptr<int> m_resource;
,这样析构函数甚至不需要显式delete
了!
- 可以尝试将
扩展问题供你思考:
- 健壮性考虑:如果在
ResourceHolder
的构造函数中m_resource = new int(value);
失败并抛出异常(例如,内存不足时new
会抛出std::bad_alloc
),会发生什么?析构函数会被调用吗?(提示:如果构造函数中途失败,对象未完全构造,析构函数通常不会被调用。) - 拷贝问题:如果我们允许
ResourceHolder
对象被拷贝,例如:默认的拷贝行为(浅拷贝 (shallow copy))会导致cpp1
2
3
4
5ResourceHolder rh_original("Original", 50);
ResourceHolder rh_copy = rh_original; // 拷贝构造
// 或者
// ResourceHolder rh_another("Another", 60);
// rh_copy = rh_another; // 拷贝赋值rh_original.m_resource
和rh_copy.m_resource
指向同一块内存。当其中一个对象析构并释放内存后,另一个对象的m_resource
就成了悬挂指针,再次析构时会导致双重释放 (double free) 错误。如何解决这个问题?(提示:深拷贝 (deep copy),或者禁用拷贝,或者使用智能指针处理所有权。这通常涉及到“Rule of Three/Five/Zero”,即如果你自定义了析构函数、拷贝构造函数或拷贝赋值运算符中的一个,你可能需要自定义其他的。) - 移动语义 (Move Semantics):除了拷贝,C++11引入了移动语义。如果一个
ResourceHolder
对象的资源可以被“转移”给另一个对象,而不是拷贝,这会更高效。这涉及到移动构造函数和移动赋值运算符。 - 数组资源:如果
m_resource
指向的不是单个int
,而是一个动态分配的int
数组(例如m_resource = new int[10];
),那么在析构函数中应该如何释放它?(提示:使用delete[] m_resource;
)
这些问题会引导你学习更多关于C++资源管理和对象生命周期的重要概念。继续探索,你会越来越熟练!如果你对这些扩展问题有任何想法或疑问,随时可以提出!