avatar

目录
278 - cpp 指针管理类对象示例

C++ 指针管理类对象示例

🧑‍💻 User

In this lecture, we’re going to see how we can manage our class objects through pointers.And in most cases, we need to manage our class object through pointers if we are using some form ofdynamic memory allocation.Here is a simple example.We have our cylinder class included and down here you see that we can create the cylinder objects onthe stack.If we use the syntax like we do here, the objects are going to be created on the stack and we haveseen that we can access the behaviors or data inside our class object using the dot notation like wedo here.For example, here we are calling our volume function, but sometimes we want to manage our objectsthrough pointers.For example, if you look at the syntax we have here, we are saying cylinder and using the asteriskhere, this is going to say that this is going to be a pointer to a cylinder type and we’re going togive it a name C two and we can allocate our object on the heap using the new operator.This is going to dynamically allocate space from the heap, and that memory space is going to be usedto store our cylinder object.Now, the interesting thing is going to happen when you try to access behaviors from your cylinder object.Here, for example, if we look at this line here, we are trying to print the volume of this cylinderand you notice that we have to go through ugly dereferencing here.We have to dereference the object and then call our method through the dot operator here.But what I want you to see is that we can avoid this ugly dereferencing we are doing here and use thepointer access notation.This dash greater than symbol you see here is going to allow us to access stuff directly using the pointerto an object that is dynamically allocated on the heap.Okay.After we are done using our cylinder object, remember it is memory that was dynamically allocated fromthe heap.We have to release the memory so that it can be used by the operating system and we release the memoryusing the delete operator as we do here.Okay.The idea I wanted to introduce here is that we can use pointers to manage our cylinder objects and thisis one way we can do this.And once we are using a pointer as a handle to our cylinder object, we can use the pointer access operatorhere to access stuff from our class objects easily.And another thing you should know is that we can actually create a cylinder object and storing the addressof objects that we have on the stack.And we’re going to see this in a minute when we hit Visual Studio Code.Now that you have an idea about this, let’s head over to Visual Studio code and play with this a littlemore.Okay.Here we are in our working folder.The current project is managing class objects through pointers.We can see the entire name here and now we’re not going to grab our template project because we wantto start from the project we had in this lecture on class across multiple files.So we’re going to go in that project and grab everything from there except for the executable becausewe obviously don’t want this and we’re going to go in our current project and paste in the code andthis is going to give us a good starting point so we can grab this and open this project in Visual Studiocode, it’s going to open the folder and we’re going to have everything.We want to start doing what we want to do in this lecture here.So now we should have access to our cylinder class.And if you look here, we have an object of cylinder type already created by this line on line numbersix here.And we have seen that we can use the syntax like cylinder volume here using the dot call operator toaccess stuff from our class object.And this is going to work, but we want to see that we can use pointers to manage our object here.And the first thing I want you to see is that we can manage a stack object through pointers and to reallyshow this off, let’s set up a another variable.It’s going to be a cylinder pointer and it’s going to be p cylinder one.It’s going to be a pointer to cylinder one here.That’s what I mean.And we can give it an address of cylinder one as a value.So we can do that, for example, and put in the address of operator.And this is going to work.Now we can go through our pointer here and do stuff with this cylinder object, but the syntax is goingto be pretty ugly.So for example, let’s try and print the volume of a cylinder.We’re going to say stdcout and say volume can save that and we’re going to have to go through the dereferenceoperator.So if we do something like p cylinder one and try to use the dot operator.Later, you’re going to see that we have the volume function.But if we try to call it let’s do that.Let’s make sure we are using the dot operator here.If we try to compile this program, it’s not going to compile because we are trying to call stuff froma pointer.And remember a pointer is just going to store an address.It doesn’t really have information about the type that it is pointing to.If you’re trying to compile this program, you’re going to get a compiler error.So let’s use GCC to do that and we’re going to get request for member volume in cylinder one.Remember, p cylinder one is just a pointer.It’s not an object.So we can’t really call the volume function to it.If we want to use the dot call operator here we have to dereference and actually get to the actual objectthat the pointer is pointing to and we do that using the star operator.You already know how to dereference a pointer so we can dereference our pointer and then call the volumefunction on what the pointer is pointing to.And this is going to work.It’s going to print our volume.Let’s build now, now that we are dereferencing correctly and you see that this is going to work.And if we try to print this, let’s bring up a terminal so that we can run this program.Let’s run Rooster.You’re going to see that we have our volume here.So to really get to what is pointed to, you need to use the dereference operator and then call whatevermethod you want to call on your object.But we can do things even easier.There is a syntax we can use to use the pointer directly here because going through all this dereferencingugliness is really not cool.So we’re going to grab this and show you that we can use the dash greater than operator.That’s how we call it.And if we use that, we don’t need to dereference.All we need to do is use the dash greater than symbol here and it’s going to directly tell the compilerdon’t call stuff on the pointer directly, do dereference the pointer internally and then call the volumefunction on whatever you get after Dereferencing.So this is basically going to push the dereferencing ugliness here behind the curtains of the compilerand we can use a nice syntax like this.And I think this is much better than what we were doing here.So if we try to build now, we’re going to build and print our volume.You’re going to see the world is good.We can clear and run Rooster.You see, we see our volume here.So this is what I want you to see.It is possible to manage your cylinder objects through pointers, and this is one way we can do thisby managing a stack object through pointers.And this is what we are doing here.If you want to call stuff on your class object, you can either do the ugly stuff we do here or youcan use the nice pointer access operator and it is going to be cool.Okay.Now that we have seen this, let’s see another use case in which we’re going to be creating a cylinderobject on the heap through the new operator.Okay.We can go down and say that we’re going to say cylinder pointer.It’s going to be P cylinder two.We can call it like this, and then we’re going to say new cylinder and you’re going to see that wecan call a constructor.We have two types of constructors we can call.We’re going to call our constructor.That takes two parameters and we can pass in 101 hundred.Why not?Oh, let’s put the two.After here, we can put in whatever we want.This is going to create a new cylinder object.The space for the cylinder object is going to be allocated on the heap.So this is a heap object.And because we are using the new operator here, the other thing you should have in mind is that youwill need to release this memory and let’s go and do that directly.This is a good practice because you will forget to release your memory if you don’t really keep thisin mind.So we are going to delete p cylinder two because that’s what we used to allocate our heap memory here.Okay.Now that we have taken care of memory management, we can go in and use our p cylinder to object.And for example, we can print its volume by using the stdcout statement here.So let’s go down and save that.Let’s align this properly and we’re going to say volume cylinder two to really make this super clear,and then we’re going to say p cylinder two and call the volume function here.This is going to work.Let’s try and world the world is good.We can clear and run Rooster.This is going to give us our volume.And if we try and use this information to create a cylinder, we’re going to see that the volume iswhat we see here.We can even try and print its base radius.Let’s do that.I’m going to say Stdcout.We’re going to say p cylinder two and we’re going to say base radius.We can hit the dot operator here.You’re going to see we’re going to have a few options in Visual Studio code here.If we double click on get base radius here, you’re going to see that Visual Studio code is going touse the pointer access operator here.And this is really cool.So we can hit Stdendl.We can close the left pane here because we need some more breathing space.And if we try to build and run this program and we have a compiler error here and the reason is getbase radius is a function.If we go back in our cylinder class, we see that we have a function called get base radius.So if we want to call this in the main function, we need to call that as a function.So we need to put a pair of parentheses here.And if we build, we should build without a problem.Let’s do that again.You see, the build is good and if we run the program, we’re going to see exactly what we expect.Let’s run a rooster.We’re going to see what we expect here.And the message here was really that you can manage your class object through pointers.One case is when you have a pointer that is pointing to a stack object.And we saw that we could do that here.We can also use pointer notation to allocate objects on the heap and point to them with our pointerhere.And whenever we are using a pointer we have the option to use the pointer call operator here to do thingseasily.I am going to be calling this the pointer call operator to mend this from here on.If we happen to knit this and you should know what I mean by that.This is really all I set out to share in this lecture.I hope you found it interesting.We are going to stop here in this lecture and the next one we’re going to learn about Destructors.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,很高兴能成为你的C++启蒙老师!C++是一门功能强大且应用广泛的语言,掌握它会让你受益匪浅。别担心,我会一步一步地引导你,确保你能够理解每一个知识点。

让我们开始今天的学习:通过指针管理类对象。


1. 标题和摘要

标题: C++进阶:使用指针管理类对象——深入理解动态内存与访问机制

摘要:
本节课我们将学习如何使用 指针 (pointer) 来管理C++中的类对象。这在进行 动态内存分配 (dynamic memory allocation) 时尤为重要。我们将探讨在 栈 (stack) 上创建对象和在 堆 (heap) 上动态创建对象的区别,以及如何通过指针(特别是使用 -> 箭头运算符 (arrow operator))来访问对象的成员。同时,我们也会强调使用 newdelete 进行内存管理的必要性。


2. 详细解释

在C++中,我们创建和使用对象的方式有多种。之前你可能已经熟悉了直接创建对象的方式,比如 Cylinder c1;。这种方式创建的对象通常位于内存的“栈”区域。现在,我们要学习另一种更灵活的方式:通过指针来管理对象,尤其是那些在内存“堆”区域动态创建的对象。

步骤 1:回顾栈对象的创建和访问

当你像下面这样写代码时:

cpp
1
Cylinder cylinder1(10, 5); // 假设Cylinder类有一个接受半径和高度的构造函数
  • cylinder1 这个对象是在 栈 (stack) 上创建的。
  • 栈是一块内存区域,用于存储局部变量和函数调用的信息。它的特点是管理自动化:当变量超出其作用域(例如,函数执行完毕),它所占用的栈内存会自动释放。
  • 访问这类对象的成员(比如调用它的方法或访问它的公共数据成员),我们使用点运算符 (.):
cpp
1
2
double vol = cylinder1.volume(); // 调用volume方法
std::cout << "Cylinder 1 radius: " << cylinder1.get_radius(); // 调用get_radius方法

步骤 2:为什么需要用指针管理对象?——动态内存分配

有时候,我们希望对象的生命周期不局限于创建它的那个函数的作用域。或者,我们需要在程序运行时才决定要创建多少个对象,或者创建非常大的对象时,栈空间可能不够用。这时,我们就需要在 堆 (heap) 上动态地创建对象。

  • 堆 (heap) 是另一块内存区域,与栈不同,堆上的内存分配和释放必须由程序员手动管理。
  • 在堆上创建对象,我们需要使用 new 运算符 (new operator)

步骤 3:在堆上创建对象并使用指针指向它

cpp
1
2
3
Cylinder* p_cylinder2; // 声明一个指针,它可以指向一个Cylinder类型的对象
p_cylinder2 = new Cylinder(7, 3); // 使用 new 在堆上创建一个Cylinder对象,
// 并将其地址赋值给指针 p_cylinder2
  • Cylinder* p_cylinder2; 这里的星号 * 表示 p_cylinder2 是一个指针,它存储的是一个 Cylinder 类型对象的内存地址。
  • new Cylinder(7, 3) 这部分做了三件事:
    1. 在堆内存中找到一块足够大的空间来存放一个 Cylinder 对象。
    2. 调用 Cylinder 类的构造函数(这里是带两个参数的构造函数)来初始化这块内存空间,使其成为一个有效的 Cylinder 对象。
    3. 返回这个新创建对象的内存地址。
  • 这个返回的地址就被存储在了指针变量 p_cylinder2 中。现在,p_cylinder2 就“指向”了堆上的那个 Cylinder 对象。

步骤 4:通过指针访问堆上对象的成员——两种方法

现在我们有了一个指向堆上对象的指针 p_cylinder2,如何访问该对象的成员(比如调用 volume() 方法)呢?

  • 方法一:解引用 (dereference) 和点运算符 (不推荐,繁琐)
    指针本身存储的是地址,不是对象本身。要获取指针指向的对象,我们需要先对指针进行 解引用 (dereference),使用星号 * 操作符。解引用后,我们就得到了对象本身,然后就可以使用点运算符 .了。

    cpp
    1
    2
    3
    4
    // (*p_cylinder2) 首先解引用指针,得到对象本身
    // 然后 .volume() 调用该对象的成员函数
    double vol2 = (*p_cylinder2).volume();
    std::cout << "Volume of cylinder 2: " << vol2 << std::endl;

    注意这里的括号 (*p_cylinder2) 非常重要!因为点运算符 . 的优先级高于解引用运算符 *。如果不加括号,*p_cylinder2.volume() 会被编译器理解为 *(p_cylinder2.volume()),它会尝试对 p_cylinder2(一个指针)使用点运算符,这是错误的,因为指针没有名为 volume 的成员。

  • 方法二:箭头运算符 -> (推荐,简洁)
    C++ 提供了一个更简洁、更常用的方式来通过指针访问对象的成员:箭头运算符 (arrow operator) ->。它等效于“解引用然后访问成员”。

    cpp
    1
    2
    3
    double vol2_arrow = p_cylinder2->volume(); // 等价于 (*p_cylinder2).volume()
    std::cout << "Volume of cylinder 2 (using arrow): " << vol2_arrow << std::endl;
    std::cout << "Cylinder 2 radius: " << p_cylinder2->get_radius() << std::endl;

    这种方式更易读,也是通过指针访问对象成员的标准做法。

步骤 5:释放堆内存——delete 运算符

当你在堆上使用 new 分配了内存后,这块内存会一直被占用,直到你明确地告诉系统你不再需要它了。如果你忘记释放,就会发生 内存泄漏 (memory leak),即程序占用的内存越来越多,最终可能导致程序或系统崩溃。

为了释放之前用 new 分配的内存,我们使用 delete 运算符 (delete operator)

cpp
1
2
3
4
delete p_cylinder2; // 释放 p_cylinder2 指向的堆内存
p_cylinder2 = nullptr; // 这是一个好习惯:释放后将指针设为 nullptr,
// 防止它成为“悬垂指针” (dangling pointer),
// 即指向一块不再有效或已被回收的内存区域。
  • delete p_cylinder2; 会做两件事:
    1. 首先,它会调用 p_cylinder2 所指向对象的析构函数(我们稍后会学习析构函数,它用于对象销毁前的清理工作)。
    2. 然后,它会释放该对象所占用的堆内存,使得这块内存可以被操作系统重新分配给其他程序或当前程序的其他部分使用。
  • 重要规则:new 就必须有对应的 deletenewdelete 必须成对出现(对于数组,是 new[]delete[],我们以后会学)。

步骤 6:指针也可以指向栈上的对象 (了解即可)

虽然指针主要用于管理堆上的动态对象,但它们也可以指向栈上已经存在的对象。这在某些特定场景下可能有用,但不如管理堆对象那么常见。

cpp
1
2
3
4
5
6
7
8
Cylinder cylinder3(2, 8);        // cylinder3 在栈上创建
Cylinder* p_cylinder3 = &cylinder3; // p_cylinder3 指向栈上的 cylinder3
// & 是取地址运算符

// 同样可以使用 -> 运算符
std::cout << "Volume of cylinder 3: " << p_cylinder3->volume() << std::endl;
// 或者使用 (*).
std::cout << "Radius of cylinder 3: " << (*p_cylinder3).get_radius() << std::endl;

需要注意的是,对于指向栈对象的指针,绝对不能 对其使用 delete!因为栈对象的内存是由系统自动管理的,delete 只能用于通过 new 分配的堆内存。对栈对象使用 delete 会导致未定义行为,通常是程序崩溃。

总结一下核心思想:

  1. 直接声明的对象(如 Cylinder c1;)在栈上,使用 . 访问成员,生命周期由作用域决定,自动管理。
  2. 使用 new 创建的对象(如 Cylinder* p = new Cylinder();)在堆上,返回一个指向该对象的指针。
  3. 通过指针访问堆上对象的成员,推荐使用箭头运算符 ->(如 p->volume();)。
  4. 堆上用 new 创建的对象,必须在不再需要时使用 delete 手动释放(如 delete p;),否则会导致内存泄漏。

3. 代码示例

为了让这些概念更清晰,我们需要一个简单的 Cylinder 类。假设我们有以下文件:

cylinder.h (头文件,类的声明)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef CYLINDER_H
#define CYLINDER_H

#include <iostream> // 为了 std::cout

const double PI {3.1415926535897932384626433832795};

class Cylinder {
public:
// 构造函数 (Constructors)
Cylinder() = default; // 默认构造函数
Cylinder(double rad_param, double height_param);

// Getter 方法
double get_radius() const;
double get_height() const;

// Setter 方法
void set_radius(double rad_param);
void set_height(double height_param);

// 功能方法
double volume() const;

private:
double base_radius {1.0};
double height {1.0};
};

#endif // CYLINDER_H

cylinder.cpp (源文件,类的实现)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "cylinder.h"

// 构造函数定义
Cylinder::Cylinder(double rad_param, double height_param) {
base_radius = rad_param;
height = height_param;
}

// Getter 方法定义
double Cylinder::get_radius() const {
return base_radius;
}

double Cylinder::get_height() const {
return height;
}

// Setter 方法定义
void Cylinder::set_radius(double rad_param) {
base_radius = rad_param;
}

void Cylinder::set_height(double height_param) {
height = height_param;
}

// 功能方法定义
double Cylinder::volume() const {
return PI * base_radius * base_radius * height;
}

main.cpp (主程序,演示指针用法)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include "cylinder.h" // 引入 Cylinder 类的声明

int main() {
// 1. 在栈上创建对象,并使用点运算符 . 访问
std::cout << "--- 栈对象 ---" << std::endl;
Cylinder c1(3.0, 7.0); // 在栈上创建
std::cout << "c1 (栈) 半径: " << c1.get_radius() << std::endl;
std::cout << "c1 (栈) 高度: " << c1.get_height() << std::endl;
std::cout << "c1 (栈) 体积: " << c1.volume() << std::endl;
std::cout << std::endl;

// 2. 指针指向栈上的对象 (了解即可,不常用)
std::cout << "--- 指针指向栈对象 ---" << std::endl;
Cylinder* p_c1 = &c1; // p_c1 指向栈上的 c1
// 使用箭头运算符 ->
std::cout << "通过 p_c1 (指向栈) 访问 c1 半径: " << p_c1->get_radius() << std::endl;
// 使用解引用和点运算符 (*).
std::cout << "通过 p_c1 (指向栈) 访问 c1 体积: " << (*p_c1).volume() << std::endl;
// 注意:不能 delete p_c1,因为 c1 是栈对象!
std::cout << std::endl;

// 3. 在堆上动态创建对象,并使用指针管理
std::cout << "--- 堆对象 (通过 new 创建) ---" << std::endl;
Cylinder* p_c2 = new Cylinder(5.0, 2.0); // 在堆上创建对象,p_c2 指向它

if (p_c2) { // 检查 new 是否成功 (在现代C++中,如果new失败会抛出异常,但检查是个好习惯)
// 使用箭头运算符 -> 访问成员
std::cout << "p_c2 (堆) 半径: " << p_c2->get_radius() << std::endl;
std::cout << "p_c2 (堆) 高度: " << p_c2->get_height() << std::endl;
p_c2->set_height(4.0); // 修改对象的状态
std::cout << "p_c2 (堆) 修改后高度: " << p_c2->get_height() << std::endl;
std::cout << "p_c2 (堆) 体积: " << p_c2->volume() << std::endl;

// 使用解引用和点运算符 (*). 访问成员 (较繁琐)
(*p_c2).set_radius(6.0);
std::cout << "p_c2 (堆) 修改后半径 (使用 (*). ): " << (*p_c2).get_radius() << std::endl;
std::cout << "p_c2 (堆) 新体积 (使用 (*). ): " << (*p_c2).volume() << std::endl;
std::cout << std::endl;

// 4. 释放堆内存
std::cout << "--- 释放堆内存 ---" << std::endl;
delete p_c2; // 释放 p_c2 指向的堆内存
p_c2 = nullptr; // 将指针设为 nullptr,防止悬垂指针
std::cout << "p_c2 指向的内存已释放。" << std::endl;

// 尝试访问已释放的内存会导致未定义行为 (通常是崩溃)
// if (p_c2) { // 如果不设为 nullptr,这里可能仍不为0,但访问是危险的
// std::cout << p_c2->volume() << std::endl; // 危险操作!
// }
} else {
std::cout << "内存分配失败!" << std::endl;
}

return 0;
}

编译和运行说明:
你需要将 cylinder.h, cylinder.cpp, 和 main.cpp放在同一个目录下。
使用C++编译器(如g++)编译:
g++ main.cpp cylinder.cpp -o program
然后运行:
./program

你会看到程序输出各个对象的信息,并演示了不同访问方式。


4. QA闪卡 (QA Flash Cards)

Q1: 什么是类对象的 指针 (pointer)
A1: 指针是一个变量,它存储的是另一个变量(在这里是类对象)的内存地址。

Q2: new 运算符 (new operator) 的作用是什么?
A2: new堆 (heap) 内存中为对象分配空间,调用对象的构造函数进行初始化,并返回该对象的地址(一个指针)。

Q3: 为什么使用 new 分配的内存后需要使用 delete
A3: new 分配的堆内存不会自动释放。如果不使用 delete 运算符 (delete operator) 手动释放,会导致 内存泄漏 (memory leak),程序会持续消耗内存。

Q4: 如何通过指针访问类对象的成员?给出两种方法。
A4:

1.  解引用和点运算符:`(*pointer_name).member_name` 或 `(*pointer_name).method_name()`
2.  **箭头运算符 (arrow operator)**:`pointer_name->member_name` 或 `pointer_name->method_name()` (推荐)

Q5: 点运算符 (.) 和箭头运算符 (->) 的主要区别是什么?
A5: 点运算符 (.) 用于直接通过对象名访问其成员。箭头运算符 (->) 用于通过指向对象的指针访问其成员。ptr->member 等价于 (*ptr).member

Q6:栈 (stack) 上创建对象和在 堆 (heap) 上创建对象有什么主要区别?
A6:

*   **栈对象:** 内存自动管理(随作用域结束而释放),创建速度快,但空间有限。通过对象名直接访问。
*   **堆对象:** 内存手动管理(需 `new` 创建,`delete` 释放),空间较大,创建销毁有开销。通过指针访问。生命周期可控。

5. 常见误解或易犯错误

  1. 忘记 delete:最常见的错误。对每个 new 出来的对象,都必须有一个对应的 delete 操作,否则会导致内存泄漏。

    cpp
    1
    2
    3
    4
    // 错误示范
    Cylinder* c = new Cylinder(1,1);
    // ... 使用 c ...
    // 忘记 delete c; 程序结束时这块内存没有被释放
  2. 对非 new 创建的指针使用 deletedelete 只能用于通过 new 分配的内存。对栈对象的地址或未初始化的指针使用 delete 会导致程序崩溃或未定义行为。

    cpp
    1
    2
    3
    4
    // 错误示范
    Cylinder c_stack(1,1);
    Cylinder* p_stack = &c_stack;
    // delete p_stack; // 严重错误!p_stack 指向栈内存
  3. 重复 delete:对同一个指针 delete 两次或多次会导致未定义行为,通常是程序崩溃。

    cpp
    1
    2
    3
    4
    5
    // 错误示范
    Cylinder* c = new Cylinder(1,1);
    delete c;
    // ... 一些代码 ...
    // delete c; // 严重错误!重复释放

    将指针在 delete 后立即设为 nullptr (如 c = nullptr;) 可以帮助避免这个问题,因为 delete nullptr; 是安全无害的。

  4. 使用已 delete 的指针 (悬垂指针):当一个指针指向的内存被 delete 后,该指针就变成了悬垂指针。再通过这个指针去访问或修改内存是非常危险的。

    cpp
    1
    2
    3
    4
    5
    // 错误示范
    Cylinder* c = new Cylinder(1,1);
    delete c;
    // c 现在是悬垂指针
    // std::cout << c->volume(); // 严重错误!访问已释放的内存

    同样,delete 后将指针设为 nullptr 是个好习惯。

  5. 混淆 .->

    • 对对象使用 ->Cylinder c1; c1->volume(); (错误)
    • 对指针使用 .Cylinder* p_c2 = new Cylinder(); p_c2.volume(); (错误,除非你写成 (*p_c2).volume(); )
  6. 解引用指针时忘记括号:当使用 (*ptr).member 形式时,括号是必需的,因为 . 运算符的优先级高于 *。写成 *ptr.member 会被解释为 *(ptr.member),这通常不是你想要的,且会导致编译错误,因为指针 ptr 没有名为 member 的成员。

  7. new 失败未检查 (老旧代码中可能存在):在非常早期的C++标准或特定编译环境下,new 失败可能返回 nullptr 而不是抛出异常。现代C++中,new 失败默认会抛出 std::bad_alloc 异常。如果使用 new (std::nothrow) Cylinder(...) 形式,则失败时会返回 nullptr,此时需要检查指针是否为 nullptr


6. 编码练习

现在,轮到你动手实践了!下面是一段不完整的代码。你需要根据注释的提示,填补 // TODO: 部分的代码,使其能够正确地在堆上创建 Cylinder 对象,设置其属性,计算并打印其体积,最后正确释放内存。

假设 cylinder.hcylinder.cpp 文件已按上面示例提供。

exercise.cpp:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include "cylinder.h" // 确保 cylinder.h 和 cylinder.cpp 在同一目录或正确配置包含路径

int main() {
Cylinder* p_my_cylinder = nullptr; // 声明一个 Cylinder 指针并初始化为 nullptr

// TODO 1: 使用 new 运算符在堆上创建一个 Cylinder 对象。
// 让其半径为 4.0,高度为 6.0。
// 将返回的地址赋值给 p_my_cylinder。
// p_my_cylinder = new ... ;


// 检查 p_my_cylinder 是否成功分配了内存
if (p_my_cylinder) {
std::cout << "堆对象创建成功!" << std::endl;

// TODO 2: 使用箭头运算符,调用对象的 set_radius 方法,将其半径修改为 5.5。
// p_my_cylinder->... ;

// TODO 3: 使用箭头运算符,调用对象的 set_height 方法,将其高度修改为 10.2。
// p_my_cylinder->... ;

// 打印修改后的半径和高度
std::cout << "修改后半径: " << p_my_cylinder->get_radius() << std::endl;
std::cout << "修改后高度: " << p_my_cylinder->get_height() << std::endl;

// TODO 4: 计算并打印 p_my_cylinder 指向的圆柱体的体积。
// 使用箭头运算符调用 volume() 方法。
// double vol = ... ;
// std::cout << "圆柱体体积: " << vol << std::endl;


// TODO 5: 使用 delete 运算符释放 p_my_cylinder 指向的堆内存。
// delete ... ;

// TODO 6: (好习惯) 将 p_my_cylinder 设置为 nullptr,防止悬垂指针。
// p_my_cylinder = ... ;

std::cout << "堆内存已释放。" << std::endl;

} else {
std::cout << "内存分配失败!程序退出。" << std::endl;
return 1; // 返回错误码
}

// 验证指针是否为 nullptr
if (p_my_cylinder == nullptr) {
std::cout << "p_my_cylinder 现在是 nullptr。" << std::endl;
} else {
std::cout << "警告: p_my_cylinder 在释放后不是 nullptr!" << std::endl;
}

return 0;
}

请你尝试填写 // TODO: 部分的代码。完成后,将你的解决方案告诉我,我会帮你检查并给出反馈。
祝你编码愉快!


🧑‍💻 User

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include "cylinder.h" // 确保 cylinder.h 和 cylinder.cpp 在同一目录或正确配置包含路径

int main() {
Cylinder* p_my_cylinder = nullptr; // 声明一个 Cylinder 指针并初始化为 nullptr

// TODO 1: 使用 new 运算符在堆上创建一个 Cylinder 对象。
// 让其半径为 4.0,高度为 6.0。
// 将返回的地址赋值给 p_my_cylinder。
p_my_cylinder = new Cylinder(4.0, 6.0);


// 检查 p_my_cylinder 是否成功分配了内存
if (p_my_cylinder) {
std::cout << "堆对象创建成功!" << std::endl;

// TODO 2: 使用箭头运算符,调用对象的 set_radius 方法,将其半径修改为 5.5。
p_my_cylinder->set_radius(5.5) ;

// TODO 3: 使用箭头运算符,调用对象的 set_height 方法,将其高度修改为 10.2。
p_my_cylinder->set_height(10.2);

// 打印修改后的半径和高度
std::cout << "修改后半径: " << p_my_cylinder->get_radius() << std::endl;
std::cout << "修改后高度: " << p_my_cylinder->get_height() << std::endl;

// TODO 4: 计算并打印 p_my_cylinder 指向的圆柱体的体积。
// 使用箭头运算符调用 volume() 方法。
double vol = p_my_cylinder->volume() ;
std::cout << "圆柱体体积: " << vol << std::endl;


// TODO 5: 使用 delete 运算符释放 p_my_cylinder 指向的堆内存。
delete p_my_cylinder ;

// TODO 6: (好习惯) 将 p_my_cylinder 设置为 nullptr,防止悬垂指针。
p_my_cylinder = nullptr ;

std::cout << "堆内存已释放。" << std::endl;

} else {
std::cout << "内存分配失败!程序退出。" << std::endl;
return 1; // 返回错误码
}

// 验证指针是否为 nullptr
if (p_my_cylinder == nullptr) {
std::cout << "p_my_cylinder 现在是 nullptr。" << std::endl;
} else {
std::cout << "警告: p_my_cylinder 在释放后不是 nullptr!" << std::endl;
}

return 0;
}

🤖 Assistant

太棒了!你完成得非常好!你的代码几乎是完美的,完全掌握了我们今天学习的知识点。


7. 用户解答评估、正确答案及解释

用户解答评估:

  • 评分:5/5 (优秀!)

  • 评价:

    • TODO 1 (创建对象): p_my_cylinder = new Cylinder(4.0, 6.0); 非常正确!你正确地使用了 new 关键字,并调用了 Cylinder 类的构造函数来在堆上创建对象,同时传递了初始的半径和高度。
    • TODO 2 (设置半径): p_my_cylinder->set_radius(5.5); 完全正确!你熟练地运用了箭头运算符 -> 来通过指针调用对象的成员函数 set_radius
    • TODO 3 (设置高度): p_my_cylinder->set_height(10.2); 同样完美!继续正确使用箭头运算符。
    • TODO 4 (计算体积): double vol = p_my_cylinder->volume();std::cout << "圆柱体体积: " << vol << std::endl; 非常好!准确调用了 volume() 方法,并将结果存储和打印。
    • TODO 5 (释放内存): delete p_my_cylinder; 正确无误!这是至关重要的一步,你正确地使用了 delete 来释放之前通过 new 分配的内存。
    • TODO 6 (设置nullptr): p_my_cylinder = nullptr; 优秀的习惯!在释放内存后将指针设为 nullptr,有效防止了悬垂指针的产生。

你的代码不仅功能正确,而且遵循了良好的编程实践。继续保持!

正确答案与解释:

你的答案就是标准答案!这里为了完整性,我再贴一次完整的正确代码,和你提供的一样:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include "cylinder.h" // 确保 cylinder.h 和 cylinder.cpp 在同一目录或正确配置包含路径

int main() {
Cylinder* p_my_cylinder = nullptr; // 声明一个 Cylinder 指针并初始化为 nullptr

// TODO 1: 使用 new 运算符在堆上创建一个 Cylinder 对象。
// 让其半径为 4.0,高度为 6.0。
// 将返回的地址赋值给 p_my_cylinder。
p_my_cylinder = new Cylinder(4.0, 6.0);


// 检查 p_my_cylinder 是否成功分配了内存
if (p_my_cylinder) { // 在现代C++中,如果new失败会抛出std::bad_alloc异常
// 但检查指针是否为nullptr对于 new(std::nothrow) 或旧代码仍然有用
std::cout << "堆对象创建成功!" << std::endl;

// TODO 2: 使用箭头运算符,调用对象的 set_radius 方法,将其半径修改为 5.5。
p_my_cylinder->set_radius(5.5);

// TODO 3: 使用箭头运算符,调用对象的 set_height 方法,将其高度修改为 10.2。
p_my_cylinder->set_height(10.2);

// 打印修改后的半径和高度
std::cout << "修改后半径: " << p_my_cylinder->get_radius() << std::endl;
std::cout << "修改后高度: " << p_my_cylinder->get_height() << std::endl;

// TODO 4: 计算并打印 p_my_cylinder 指向的圆柱体的体积。
// 使用箭头运算符调用 volume() 方法。
double vol = p_my_cylinder->volume();
std::cout << "圆柱体体积: " << vol << std::endl;


// TODO 5: 使用 delete 运算符释放 p_my_cylinder 指向的堆内存。
delete p_my_cylinder;

// TODO 6: (好习惯) 将 p_my_cylinder 设置为 nullptr,防止悬垂指针。
p_my_cylinder = nullptr;

std::cout << "堆内存已释放。" << std::endl;

} else {
// 这种情况在现代C++中,如果使用标准的 new,通常不会发生,
// 因为 new 失败会抛出 std::bad_alloc 异常,而不是返回 nullptr。
// 如果使用的是 new(std::nothrow),则需要这样的检查。
std::cout << "内存分配失败!程序退出。" << std::endl;
return 1; // 返回错误码
}

// 验证指针是否为 nullptr
if (p_my_cylinder == nullptr) {
std::cout << "p_my_cylinder 现在是 nullptr。" << std::endl;
} else {
// 这段代码在正常情况下不应该执行,因为上面已经将 p_my_cylinder 设为 nullptr
std::cout << "警告: p_my_cylinder 在释放后不是 nullptr!" << std::endl;
}

return 0;
}

解释关键点:

  1. new Cylinder(4.0, 6.0):

    • new 是C++中用于在 自由存储区 (free store)(通常实现为堆内存)动态分配内存的关键字。
    • Cylinder(4.0, 6.0) 调用了 Cylinder 类的构造函数,并传递了参数 4.06.0 来初始化新创建的对象。
    • 整个表达式的结果是新创建的 Cylinder 对象的内存地址,该地址被赋值给指针 p_my_cylinder
  2. p_my_cylinder->set_radius(5.5):

    • -> (箭头运算符 (arrow operator)) 是通过指针访问对象成员的便捷方式。
    • p_my_cylinder->set_radius(5.5) 等价于 (*p_my_cylinder).set_radius(5.5)。它首先对指针 p_my_cylinder 解引用 (dereference) 以获取它所指向的 Cylinder 对象,然后调用该对象的 set_radius 方法。
  3. delete p_my_cylinder:

    • delete 用于释放在自由存储区(堆)上由 new 分配的单个对象的内存。
    • 在释放内存之前,如果对象有 析构函数 (destructor)(我们下一课会学到),delete 会先调用该对象的析构函数,用于执行任何必要的清理工作。
    • 然后,内存被返还给系统,可供后续分配使用。
  4. p_my_cylinder = nullptr:

    • nullptr 是C++11引入的关键字,表示空指针。
    • delete 一个指针后,指针变量本身仍然存储着之前那个(现在无效的)内存地址。这个指针被称为 悬垂指针 (dangling pointer)
    • 将指针设置为 nullptr 是一个好习惯,因为:
      • 它可以防止意外地通过悬垂指针访问已释放的内存。
      • nullptr 调用 delete 是安全无害的(什么也不做)。
      • 可以明确地检查指针是否有效(例如 if (p_my_cylinder != nullptr))。

你做得非常出色,准确地理解并应用了这些核心概念!


8. 建议与扩展问题

你已经很好地掌握了通过指针管理单个动态对象的基础知识。为了进一步深化理解和拓展你的技能,可以思考和研究以下问题:

  1. 动态数组 (Dynamic Arrays):

    • 如果我想在堆上创建一个包含5个 Cylinder 对象的数组,应该如何做?(提示:使用 new[]
    • 创建了动态数组后,又该如何释放它们的内存呢?(提示:使用 delete[]
    • 如何访问动态数组中的特定元素并调用其方法?
  2. this 指针 (this pointer):

    • 在类的成员函数内部,有时需要引用调用该函数的对象本身。C++提供了一个特殊的指针叫做 this。研究一下 this 指针是什么,以及它如何工作。
    • 例如,在 Cylinder 类的 set_radius 方法中,如果参数名也叫 base_radius,你将如何区分成员变量和参数?(this->base_radius = base_radius;
  3. 智能指针 (Smart Pointers):

    • 手动管理 newdelete 很容易出错(忘记 delete 导致内存泄漏,重复 delete 导致崩溃等)。C++标准库提供了 智能指针 (smart pointers) (如 std::unique_ptr, std::shared_ptr, std::weak_ptr) 来帮助自动管理动态内存的生命周期,大大减少内存管理错误。
    • 尝试了解 std::unique_ptr 是如何工作的,以及它如何能帮助我们避免手动调用 delete
  4. 内存泄漏的危害和检测 (Memory Leaks):

    • 更深入地理解内存泄漏为什么是个严重的问题。
    • 了解一些可以帮助检测内存泄漏的工具(如 Valgrind on Linux/macOS,或 Visual Studio 内置的调试工具)。
  5. RAII 原则 (Resource Acquisition Is Initialization):

    • RAII 是C++中一个非常重要的编程范式,它主张资源的生命周期应与对象的生命周期绑定。智能指针就是RAII的一个典型应用。了解RAII如何帮助编写更安全、更易于管理的代码。
  6. 指针和引用的对比 (Pointers vs. References):

    • 我们已经学习了指针。C++还有 引用 (reference) 的概念。它们都可以间接访问对象,但有什么区别和各自的适用场景呢?

这些扩展问题会引导你进入C++更高级也更实用的领域。不要急于求成,一步一个脚印地去探索它们。

你今天的表现非常棒!期待你继续学习C++的下一课内容。如果你对这些扩展问题有任何疑问,或者想深入探讨某个点,随时都可以问我!


评论