当前位置: 首页 > news >正文

C++零基础实践教程 指针与内存 类与对象入门 (面向对象基础)

模块六:指针与内存 (C++ 核心,谨慎引入)

欢迎来到 C++ 中最强大但也最需要小心处理的部分之一:指针和内存管理。理解这部分内容能让你更深入地了解 C++ 的底层工作方式,但错误使用也可能导致难以追踪的 Bug。

1. 内存地址 (& 运算符)

想象一下计算机的内存就像一条很长的街道,每个存储数据的单元(比如一个 intdouble 变量)都住在这条街上的某个房子里。每个房子都有一个唯一的地址来标识它的位置。

在 C++ 中,你可以使用地址运算符 & 来获取一个变量在内存中的地址。

C++

#include <iostream>int main() {int age = 30;double price = 99.99;std::cout << "变量 age 的值: " << age << std::endl;std::cout << "变量 age 的内存地址: " << &age << std::endl; // 使用 & 获取地址std::cout << "变量 price 的值: " << price << std::endl;std::cout << "变量 price 的内存地址: " << &price << std::endl;return 0;
}

运行输出可能类似 (地址的具体值每次运行可能不同):

变量 age 的值: 30
变量 age 的内存地址: 0x7ffc9a7dcb0c // 一个十六进制表示的内存地址
变量 price 的值: 99.99
变量 price 的内存地址: 0x7ffc9a7dcb10

这个地址值本身通常对我们没有直接意义,但它是变量在内存中的唯一标识。

2. 指针变量 (* 声明指针)

指针 (Pointer) 本身也是一种变量,但它比较特殊:它存储的值不是普通数据(如整数或浮点数),而是一个内存地址

可以把指针想象成一张纸条,上面写着某个变量(房子)的地址。

  • 声明指针: 语法:数据类型 *指针变量名;

    • 数据类型 指定了这个指针将要指向的内存地址存放的是什么类型的数据。这很重要,因为编译器需要知道如何解释该地址处的数据(比如,是 4 个字节的 int 还是 8 个字节的 double)。
    • * 在这里表示“这是一个指针变量”。
    C++

    int *iptr;      // 声明一个指向 int 类型数据的指针变量,名为 iptr
    double *dptr;   // 声明一个指向 double 类型数据的指针变量
    char *cptr;     // 声明一个指向 char 类型数据的指针变量
    
  • 初始化指针: 指针在使用前必须被初始化,让它指向一个有效的地址或者明确表示它不指向任何地方。未初始化的指针(野指针)非常危险!

    • 指向现有变量: 使用 & 获取变量地址来初始化指针。 C++

      int score = 100;
      int *scorePtr = &score; // scorePtr 现在存储了 score 变量的内存地址
      
    • 初始化为 nullptr: nullptr 是 C++11 引入的关键字,表示一个“空指针”,即该指针当前不指向任何有效的内存地址。这是初始化指针(在不知道要指向哪里时)或在使用完指针后重置它的推荐方式。 C++

      int *myPtr = nullptr; // myPtr 不指向任何有效地址
      // 在使用之前,通常需要检查指针是否为 nullptr
      
    • 避免使用 NULL: NULL 是 C 语言遗留下来的宏,通常定义为 0。在 C++ 中应优先使用类型安全的 nullptr

3. 解引用 (* 访问指针指向的数据)

光有地址(纸条上的地址)还不够,我们通常关心的是住在那个地址的“房子”里的东西(变量的值)。解引用 (Dereferencing) 就是通过指针访问它所指向的内存地址中存储的数据。

使用解引用运算符 * (注意:这里的 * 与声明指针时的 * 含义不同,它用在已声明的指针变量之前)。

C++

#include <iostream>int main() {int value = 42;int *ptr = &value; // ptr 存储了 value 的地址// 使用 * 解引用 ptr 来访问 value 的值std::cout << "通过指针访问到的值: " << *ptr << std::endl; // 输出 42// 通过解引用指针来修改 value 的值*ptr = 99; std::cout << "修改后,通过指针访问到的值: " << *ptr << std::endl; // 输出 99std::cout << "修改后,直接访问 value 的值: " << value << std::endl; // 输出 99 (value 本身也被改变了)// --- 处理空指针 ---int *safePtr = nullptr;// std::cout << *safePtr << std::endl; // 危险!解引用空指针会导致程序崩溃!// 在解引用前务必检查指针是否为空if (safePtr != nullptr) {std::cout << "safePtr 指向的值: " << *safePtr << std::endl;} else {std::cout << "safePtr 是一个空指针,不能解引用。\n";}return 0;
}

关键区分:

  • int *ptr = &value; 这里的 *声明的一部分,表示 ptr 是一个指针。
  • *ptr = 99; 这里的 *解引用运算符,表示访问 ptr 指向地址处的数据。

警告: 解引用一个 nullptr 或者一个未初始化的、指向无效内存地址的指针,是严重的错误,通常会导致程序崩溃(段错误 Segmentation Fault)。在使用指针前进行检查是一种良好的防御性编程习惯。

4. 指针与数组

指针和数组在 C++ 中有着非常紧密的联系。

  • 数组名作为指针: 在大多数表达式中,数组名会自动“衰变 (decay)”成一个指向数组第一个元素 ([0]) 的指针。

    C++

    int numbers[5] = {10, 20, 30, 40, 50};// 数组名 numbers 衰变为指向 numbers[0] 的指针
    int *p = numbers; // 等价于 int *p = &numbers[0];std::cout << "第一个元素 (通过指针): " << *p << std::endl; // 输出 10
    
  • 指针算术 (Pointer Arithmetic): 可以对指针进行整数加减运算。给指针加上整数 n,意味着将指针移动 n * sizeof(指向的数据类型) 个字节,使其指向后续的第 n 个元素。

    C++

    int numbers[5] = {10, 20, 30, 40, 50};
    int *p = numbers; // p 指向 numbers[0]std::cout << "第一个元素: " << *p << std::endl;        // 输出 10
    std::cout << "第二个元素: " << *(p + 1) << std::endl;  // 输出 20 (p+1 指向 numbers[1])
    std::cout << "第三个元素: " << *(p + 2) << std::endl;  // 输出 30 (p+2 指向 numbers[2])// 移动指针本身
    p++; // 现在 p 指向 numbers[1]
    std::cout << "移动后 p 指向的元素: " << *p << std::endl; // 输出 20
    
  • 数组访问的等价性: 数组名[索引] 实际上等价于 *(数组名 + 索引)

    C++

    std::cout << numbers[3] << std::endl; // 输出 40
    std::cout << *(numbers + 3) << std::endl; // 也输出 40
    
  • 数组传递给函数: 正是因为数组名会衰变为指针,所以当你将数组传递给函数时,函数接收到的只是指向首元素的指针,它不知道数组的原始大小。这就是为什么我们通常需要将数组大小作为单独的参数传递。

    C++

    // 回顾模块五的例子
    void printArray(int arr[], int size) { // arr[] 实际上是 int* arr// ...
    }
    

5. 动态内存分配 (newdelete)

我们之前声明的变量(如 int a;int arr[10];)要么在函数内部(栈上),要么在全局区,它们的大小在编译时就已经确定了。但有时,我们需要在程序运行时根据需要才决定分配多少内存(例如,用户输入一个数字 n,然后我们才创建一个大小为 n 的数组)。这就是动态内存分配 (Dynamic Memory Allocation) 的用武之地。动态分配的内存来自一个叫做堆 (Heap)自由存储区 (Free Store) 的内存区域。

  • new 运算符: 用于在堆上请求分配内存。

    • 分配单个变量:数据类型 *指针变量 = new 数据类型;
    • 分配数组数据类型 *指针变量 = new 数据类型[数组大小]; (这里的 数组大小 可以是变量)
    • new 会返回一个指向已分配内存块起始地址的指针。如果内存分配失败(例如内存不足),new 默认会抛出一个异常 (std::bad_alloc)。
    C++

    // 分配一个 int
    int *p_int = new int; 
    *p_int = 100; // 使用分配到的内存// 根据用户输入分配数组
    int size;
    std::cout << "请输入数组大小: ";
    std::cin >> size;
    double *p_array = new double[size]; // 在堆上分配可容纳 size 个 double 的内存块// 使用这个动态数组
    if (size > 0) {p_array[0] = 3.14;*(p_array + 1) = 2.71; // 也可以用指针算术访问
    }
    
  • delete 运算符: 极其重要! 通过 new 在堆上分配的内存必须由程序员手动使用 delete 释放。如果你忘记释放,这块内存就会一直被占用,即使你的程序不再需要它,也无法被再次分配给其他部分使用,这叫做内存泄漏 (Memory Leak)。长期运行的程序发生内存泄漏最终可能耗尽系统资源导致崩溃。

    • 释放单个变量 (new DataType): delete 指针变量;
    • 释放数组 (new DataType[size]): delete[] 指针变量; (注意方括号 []!)
    C++

    // 释放之前分配的内存
    delete p_int;    // 释放单个 int
    p_int = nullptr; // 好习惯:释放后将指针设为 nullptr,避免悬挂指针delete[] p_array; // 释放 double 数组,必须用 delete[]
    p_array = nullptr; // 设为 nullptr
    

    警告:

    1. newdelete/delete[] 必须配对使用。 每个 new 都需要一个对应的 delete (或 delete[])。
    2. 不要 delete 同一块内存两次 (Double Free),这会导致未定义行为。
    3. 不要 delete 指向栈内存或全局内存的指针。 delete 只能用于 new 分配的堆内存。
    4. 释放数组必须使用 delete[],释放单个对象必须使用 delete 用错形式会导致未定义行为。
  • 悬挂指针 (Dangling Pointer): 当你 delete 了一个指针指向的内存后,该指针变量本身仍然存储着那个(现在无效的)内存地址。如果之后不小心再次通过这个指针访问内存,就会发生错误。将指针在 delete 后立即设为 nullptr 是一个避免悬挂指针的好习惯。

手动内存管理的风险总结:

  • 内存泄漏: 忘记 delete
  • 二次释放: 多次 delete 同一块内存。
  • 悬挂指针: 使用 delete 后的指针。
  • 用错 delete 形式: delete 用于数组或 delete[] 用于单个对象。

这些错误都可能导致程序崩溃或难以预测的行为,并且调试起来非常困难。

6. 与 std::vector 对比 (及智能指针简介)

现在你体会到手动管理内存的复杂性和风险了吧?这正是我们之前学习的 std::vector 的价值所在!

  • std::vector 的优势:

    • 自动内存管理: vector 在其内部为你处理了 newdelete[] 的所有细节。当你创建一个 vector,它会按需分配内存;当你向 vector 添加元素 (push_back),如果容量不足,它会自动分配更大的内存块并迁移数据;最重要的是,当 vector 变量离开其作用域时(比如函数结束),它会自动释放所占用的所有堆内存。这被称为 RAII (Resource Acquisition Is Initialization) 原则,是 C++ 中管理资源(如内存、文件句柄、网络连接等)的核心思想。
    • 方便性: 提供 .size(), .push_back(), .at() 等方便的接口。
    • 安全性: .at() 提供边界检查。
  • 强烈建议: 尽可能优先使用 std::vector (或其他标准库容器,如 std::string) 而不是手动使用 new[]delete[] 来管理动态数组。

  • 智能指针 (Smart Pointers) 简介:

    • 对于那些 vector 不适用的场景(比如需要管理单个动态分配的对象,或实现特定的所有权语义),现代 C++ 在 <memory> 头文件中提供了智能指针,如 std::unique_ptrstd::shared_ptr
    • 智能指针也是 RAII 的体现: 它们像普通指针一样使用,但它们是对象,负责自动管理其指向的动态内存。当智能指针本身被销毁时(例如离开作用域),它会自动调用 delete (或 delete[]) 来释放它所拥有的内存。
    • std::unique_ptr: 实现独占所有权,同一时间只有一个 unique_ptr 可以指向某个对象。它轻量高效,是替代裸指针管理单个 new 对象的首选。
    • std::shared_ptr: 实现共享所有权,允许多个 shared_ptr 指向同一个对象,并通过引用计数来跟踪,当最后一个指向该对象的 shared_ptr 被销毁时,内存才会被释放。
    • 结论: 智能指针是现代 C++ 中管理动态内存的推荐方式,可以极大减少内存泄漏和悬挂指针的风险。虽然本入门教程不深入讲解,但你应该知道它们的存在,并在后续学习中重点掌握。

7. 实战练习

  1. 使用指针交换两个变量的值:

    • 编写一个函数 void swap_ptr(int *p1, int *p2)
    • 函数接收两个指向 int 的指针作为参数。
    • 在函数内部,通过解引用这两个指针,交换它们所指向的内存地址中的。你需要一个临时变量来辅助交换。
    • main 函数中,定义两个 int 变量,获取它们的地址,然后调用 swap_ptr 函数,并验证变量的值确实被交换了。
    • 对比思考: 这个函数与我们之前可能写的 void swap_ref(int& a, int& b)(使用引用)有什么异同?(效果相同,但调用方式和内部实现略有不同)。
    C++

    #include <iostream>// 函数原型
    void swap_ptr(int *p1, int *p2);int main() {int num1 = 10, num2 = 20;std::cout << "交换前: num1 = " << num1 << ", num2 = " << num2 << std::endl;// 调用函数,传入变量的地址swap_ptr(&num1, &num2); std::cout << "交换后: num1 = " << num1 << ", num2 = " << num2 << std::endl;return 0;
    }// 函数定义
    void swap_ptr(int *p1, int *p2) {// 检查指针是否有效 (好习惯,虽然在此例中调用者保证了有效性)if (p1 == nullptr || p2 == nullptr) {return; }int temp = *p1;  // 读取 p1 指向的值,存入 temp*p1 = *p2;       // 将 p2 指向的值,写入 p1 指向的地址*p2 = temp;      // 将 temp (原 p1 的值),写入 p2 指向的地址
    }
    
  2. newdelete[] 演示:

    • 编写一个程序:
      • 提示用户输入一个整数 size
      • 使用 new int[size] 创建一个动态整数数组,用一个指针变量 int *dynamicArray 指向它。检查 size 是否大于 0
      • 使用 for 循环填充这个数组,例如,让 dynamicArray[i] = i * i;
      • 使用另一个 for 循环打印数组的内容。
      • 最重要的一步: 使用 delete[] dynamicArray; 释放分配的内存。
      • 将指针设为 nullptr: dynamicArray = nullptr;
      • 在代码中添加注释,强调为什么用 new[] 就必须用 delete[],并说明 std::vector 是更推荐的方式。
    C++

    #include <iostream>
    #include <vector> // 包含 vector 以作对比说明int main() {int size;std::cout << "请输入要创建的动态数组的大小: ";std::cin >> size;// 检查输入的 size 是否有效if (size <= 0) {std::cout << "数组大小必须是正数。\n";return 1; // 提前退出程序}// 1. 使用 new[] 分配动态数组int *dynamicArray = nullptr; // 初始化为 nullptrtry {dynamicArray = new int[size]; // 请求在堆上分配内存std::cout << "成功分配了大小为 " << size << " 的动态数组。\n";} catch (const std::bad_alloc& e) {std::cout << "内存分配失败: " << e.what() << std::endl;return 1; // 分配失败,退出}// 2. 填充数组std::cout << "填充数组 (值为索引的平方)...\n";for (int i = 0; i < size; ++i) {dynamicArray[i] = i * i;}// 3. 打印数组内容std::cout << "数组内容:\n";for (int i = 0; i < size; ++i) {std::cout << dynamicArray[i] << " ";}std::cout << std::endl;// 4. 极其重要:使用 delete[] 释放内存!//    因为是用 new int[size] 分配的数组,必须用 delete[]delete[] dynamicArray; std::cout << "动态数组内存已释放。\n";// 5. 将指针设为 nullptr,避免悬挂指针dynamicArray = nullptr; // --- 对比说明 ---// 注意:以上手动管理内存的方式很容易出错(忘记 delete[], 用错 delete 等)。// 在现代 C++ 中,强烈推荐使用 std::vector 来管理动态数组:/*std::vector<int> vec(size); // 创建大小为 size 的 vector,自动管理内存for (int i = 0; i < size; ++i) {vec[i] = i * i;}// 当 vec 离开作用域时,内存会自动释放,无需手动 delete[]// 使用 vector 更安全、更方便!*/return 0;
    }
    

指针和手动内存管理是 C++ 的一个双刃剑。理解它们有助于你写出高性能的代码和理解底层机制,但务必谨慎使用。在你的 C++ 学习和实践中,请养成优先使用标准库容器 (vector, string 等) 和智能指针(当你学到它们时)来管理资源的习惯,这将使你的代码更安全、更健壮、更易于维护。

模块七:类与对象入门 (面向对象基础)

本模块将带你进入面向对象编程 (Object-Oriented Programming, OOP) 的世界。面向对象是一种强大的编程范式,它通过组织代码为“对象”来模拟现实世界,使得程序更加模块化、易于维护和扩展。

1. 面向过程 vs 面向对象: 基本思想差异

在学习面向对象之前,我们先来回顾一下之前我们可能接触更多的面向过程编程。

面向过程 (Procedural Programming) 是一种以“过程”为中心的编程思想。它将程序看作是一系列按顺序执行的步骤(函数或过程)。解决问题时,我们关注的是“怎么做”,将问题分解为一系列函数调用。

举个例子: 假设我们要计算一个学生的平均成绩。在面向过程的思维中,我们可能会有以下步骤:

  1. 获取学生姓名。
  2. 获取学生各科成绩。
  3. 计算总成绩。
  4. 计算平均成绩。
  5. 输出结果。

每个步骤都可能对应一个函数。这种方式对于解决简单问题很直观。

面向对象 (Object-Oriented Programming) 则是一种以“对象”为中心的编程思想。它将程序中的数据和操作数据的方法组合成一个独立的实体,称为“对象”。解决问题时,我们关注的是“谁在做”,将问题分解为多个相互作用的对象。

还是上面的例子: 在面向对象的思维中,我们可能会创建一个“学生 (Student)” 对象。这个对象拥有自己的数据(姓名、各科成绩)和行为(计算总成绩、计算平均成绩、获取姓名、获取成绩等)。

主要思想差异总结:

特征面向过程面向对象
核心概念过程 (函数、步骤)对象 (包含数据和方法)
关注点“怎么做” (解决问题的步骤)“谁在做” (对象及其交互)
程序组织方式一系列函数调用一组相互作用的对象
优点对于简单问题直观、易于理解易于维护、扩展、复用,更贴近现实世界模型
缺点复杂问题难以维护和扩展,代码耦合度高概念相对抽象,学习曲线稍陡峭

Export to Sheets

面向对象编程通过封装继承多态这三大特性,提供了更强大的代码组织和管理能力,尤其适用于开发大型、复杂的应用程序。在本模块中,我们将重点学习封装。

2. 类的定义: class 关键字,成员变量 (属性),成员函数 (方法)

在面向对象编程中,“类 (Class)” 是创建“对象 (Object)” 的蓝图或模板。它定义了一类事物所共有的属性和行为。

在 C++ 中,我们使用关键字 class 来定义一个类。类定义通常包含以下内容:

  • 类名 (Class Name): 用于标识这个类的名称,通常采用驼峰命名法(首字母大写)。
  • 成员变量 (Member Variables / Attributes): 描述对象状态的数据。它们是类中定义的变量。
  • 成员函数 (Member Functions / Methods): 描述对象行为的函数。它们定义了对象可以执行的操作。

类定义的基本语法:

C++

class ClassName {
public: // 公有成员// 成员变量声明DataType memberVariable1;DataType memberVariable2;// 成员函数声明ReturnType memberFunction1(ParameterList);ReturnType memberFunction2(ParameterList);private: // 私有成员DataType privateMemberVariable;ReturnType privateMemberFunction(ParameterList);
}; // 注意类定义末尾的分号

示例:定义一个简单的 Rectangle

C++

#include <iostream>
#include <string>class Rectangle {
public:// 公有成员变量(属性)double width;double height;// 公有成员函数(方法)double calculateArea() {return width * height;}void printDetails() {std::cout << "Width: " << width << ", Height: " << height << std::endl;}
};

在这个例子中:

  • Rectangle 是类名。
  • widthheightRectangle 类的成员变量,用来存储矩形的宽度和高度。
  • calculateArea()printDetails()Rectangle 类的成员函数,分别用于计算矩形的面积和打印矩形的详细信息。

3. 访问修饰符: publicprivate (封装)

访问修饰符 (Access Modifiers) 用于控制类成员的可见性和可访问性。C++ 中常用的访问修饰符有 publicprivate

  • public (公有): public 修饰的成员可以在类的内部、类的外部(通过对象)以及派生类中被访问。它们是类的对外接口。
  • private (私有): private 修饰的成员只能在类的内部被访问。类的外部和派生类都不能直接访问私有成员。

封装 (Encapsulation) 是面向对象编程的核心概念之一。它指的是将数据(成员变量)和操作数据的函数(成员函数)捆绑在一起,并对数据的访问进行控制。通过使用访问修饰符,我们可以实现封装。

封装的优点:

  • 数据隐藏: private 成员变量隐藏了类的内部实现细节,防止外部直接修改,提高了数据的安全性。
  • 接口控制: public 成员函数提供了与对象交互的接口,外部只能通过这些接口来操作对象,保证了对象状态的有效性和一致性。
  • 代码维护性: 当类的内部实现发生变化时,只要公有接口保持不变,就不会影响到使用该类的其他代码。

继续上面的 Rectangle 例子,我们可以将 widthheight 设置为 private,并通过 public 的成员函数来访问和修改它们(虽然在这个简单例子中没有修改的需求,但这是封装的常见做法):

C++

#include <iostream>class Rectangle {
private:double width;double height;public:// 构造函数(稍后会介绍)Rectangle(double w, double h) : width(w), height(h) {}// 公有成员函数获取宽度和高度double getWidth() const { // const 表示该函数不会修改对象的状态return width;}double getHeight() const {return height;}// 公有成员函数设置宽度和高度(如果需要)void setWidth(double w) {if (w >= 0) {width = w;} else {std::cerr << "Error: Width cannot be negative." << std::endl;}}void setHeight(double h) {if (h >= 0) {height = h;} else {std::cerr << "Error: Height cannot be negative." << std::endl;}}double calculateArea() const {return width * height;}void printDetails() const {std::cout << "Width: " << width << ", Height: " << height << std::endl;}
};

现在,widthheight 只能在 Rectangle 类的内部被访问。外部代码需要通过 getWidth(), getHeight(), setWidth(), setHeight() 这些公有成员函数来间接访问或修改它们。这使得我们可以对数据的有效性进行控制(例如,在 setWidth 中检查宽度是否为负数)。

4. 对象的创建 (实例化): ClassName objectName;

对象 (Object) 是类的具体实例。当我们定义了一个类后,就可以创建这个类的对象,也称为实例化 (Instantiation)

在 C++ 中,创建对象的基本语法是:

C++

ClassName objectName;

或者,如果类有构造函数,我们可以在创建对象时传递参数:

C++

ClassName objectName(arguments);

示例:创建 Rectangle 类的对象

C++

int main() {// 使用默认构造函数(如果存在)创建对象Rectangle rect1;// 使用带参数的构造函数创建对象Rectangle rect2(5.0, 3.0);return 0;
}

在上面的例子中,rect1rect2 都是 Rectangle 类的对象。rect2 在创建时通过构造函数初始化了宽度为 5.0,高度为 3.0。

5. 访问成员: . 运算符

一旦我们创建了对象,就可以使用点运算符 (.) 来访问对象的公有成员(成员变量和成员函数)。

访问公有成员变量的语法:

C++

objectName.memberVariable

调用公有成员函数的语法:

C++

objectName.memberFunction(arguments);

示例:访问 Rectangle 对象的成员

C++

#include <iostream>class Rectangle {
public:double width;double height;double calculateArea() {return width * height;}void printDetails() {std::cout << "Width: " << width << ", Height: " << height << std::endl;}
};int main() {Rectangle rect;rect.width = 4.0;rect.height = 2.5;std::cout << "Area: " << rect.calculateArea() << std::endl;rect.printDetails();return 0;
}

在这个例子中,我们首先创建了一个 Rectangle 对象 rect。然后,我们使用点运算符来设置它的公有成员变量 widthheight,并调用它的公有成员函数 calculateArea()printDetails()

注意: 我们不能直接使用点运算符访问对象的私有成员。尝试这样做会导致编译错误。

6. 构造函数: 初始化对象状态

构造函数 (Constructor) 是一种特殊的成员函数,它在创建对象时自动被调用。构造函数的主要作用是初始化对象的状态,即为对象的成员变量赋予初始值。

构造函数的特点:

  • 构造函数的名称与类名完全相同。
  • 构造函数没有返回类型(即使是 void 也没有)。
  • 一个类可以有多个构造函数(通过不同的参数列表实现构造函数重载)。
  • 如果没有显式定义构造函数,编译器会自动生成一个默认的无参构造函数。

示例:为 Rectangle 类添加构造函数

C++

#include <iostream>class Rectangle {
private:double width;double height;public:// 默认构造函数(无参数)Rectangle() : width(0.0), height(0.0) {std::cout << "Default constructor called." << std::endl;}// 带参数的构造函数Rectangle(double w, double h) : width(w), height(h) {std::cout << "Parameterized constructor called." << std::endl;}double getWidth() const {return width;}double getHeight() const {return height;}double calculateArea() const {return width * height;}void printDetails() const {std::cout << "Width: " << width << ", Height: " << height << std::endl;}
};int main() {Rectangle rect1; // 调用默认构造函数Rectangle rect2(5.0, 3.0); // 调用带参数的构造函数return 0;
}

在这个例子中,我们定义了两个构造函数:

  • 默认构造函数 Rectangle(): 没有参数,将 widthheight 初始化为 0.0。
  • 带参数的构造函数 Rectangle(double w, double h): 接受两个 double 类型的参数,用于初始化 widthheight

main 函数中,我们创建 rect1 时调用了默认构造函数,创建 rect2 时调用了带参数的构造函数。

初始化列表: 在构造函数的定义中,冒号 : 后面的部分称为初始化列表 (Initialization List)。它是一种更高效且推荐的方式来初始化成员变量。例如:

C++

Rectangle(double w, double h) : width(w), height(h) {}

这种方式直接在构造函数调用之前初始化成员变量,而不是在构造函数体内部进行赋值操作。对于某些类型的成员变量(例如 const 成员),必须使用初始化列表进行初始化。

7. 析构函数: 对象销毁前清理 (简单介绍)

析构函数 (Destructor) 也是一种特殊的成员函数,它在对象被销毁(例如,超出作用域或使用 delete 关键字显式删除)之前自动被调用。析构函数的主要作用是执行清理工作,例如释放对象占用的内存资源。

析构函数的特点:

  • 析构函数的名称与类名完全相同,但前面有一个波浪号 (~)。
  • 析构函数没有参数,也没有返回类型。
  • 一个类只能有一个析构函数。
  • 如果没有显式定义析构函数,编译器会自动生成一个默认的析构函数。

示例:为 Rectangle 类添加析构函数

C++

#include <iostream>class Rectangle {
public:Rectangle() {std::cout << "Rectangle created." << std::endl;}~Rectangle() {std::cout << "Rectangle destroyed." << std::endl;}
};int main() {{ // 创建一个代码块,限制 rect 的作用域Rectangle rect;std::cout << "Inside the block." << std::endl;} // rect 在这里超出作用域,析构函数会被调用std::cout << "Outside the block." << std::endl;return 0;
}

在这个例子中,当 rect 对象在代码块结束时超出作用域时,Rectangle 类的析构函数 ~Rectangle() 会自动被调用,输出 "Rectangle destroyed."。

注意: 在本教程的入门阶段,我们通常不需要显式地定义析构函数,除非我们的类中涉及到需要手动管理的资源(例如,使用 new 分配的内存)。对于简单的类,默认的析构函数通常就足够了。

8. 实战项目 5: 创建 Student

现在,让我们将所学的知识应用到实际项目中,重构之前的【学生成绩管理】项目。我们的目标是创建一个 Student 类来封装学生的名字和分数。

步骤:

1. 定义 Student 类。

创建一个名为 Student 的类。该类应该包含以下内容:

  • 私有成员变量:
    • std::string name; 用于存储学生的名字。
    • int score; 用于存储学生的分数。
  • 公有成员函数:
    • 构造函数: 接收学生的姓名和分数作为参数,并使用这些参数初始化 namescore 成员变量。
    • getName(): 返回学生的姓名(std::string 类型)。
    • getScore(): 返回学生的分数(int 类型)。

Student 类的代码实现:

C++

#include <iostream>
#include <string>class Student {
private:std::string name;int score;public:// 构造函数Student(const std::string& studentName, int studentScore) : name(studentName), score(studentScore) {}// 获取学生姓名std::string getName() const {return name;}// 获取学生分数int getScore() const {return score;}
};

2. 修改主程序,创建 std::vector<Student>

在你的主程序中,将之前用于存储学生姓名和分数的 std::vector<std::string>std::vector<int> 替换为 std::vector<Student>

3. 在循环中,读取数据并创建 Student 对象添加到 vector 中。

修改读取学生数据的循环。每次读取到一个学生的姓名和分数后,创建一个 Student 对象,并将读取到的姓名和分数作为参数传递给构造函数。然后,使用 push_back() 方法将创建的 Student 对象添加到 std::vector<Student> 中。

修改后的数据读取部分代码示例:

C++

#include <iostream>
#include <vector>
#include <string>// 假设 Student 类的定义在 student.h 文件中或者在当前文件中int main() {int numStudents;std::cout << "请输入学生人数: ";std::cin >> numStudents;std::cin.ignore(); // 消耗掉换行符std::vector<Student> students;for (int i = 0; i < numStudents; ++i) {std::cout << "请输入第 " << i + 1 << " 个学生的姓名: ";std::string name;std::getline(std::cin, name);std::cout << "请输入第 " << i + 1 << " 个学生的分数: ";int score;std::cin >> score;std::cin.ignore(); // 消耗掉换行符// 创建 Student 对象并添加到 vector 中Student student(name, score);students.push_back(student);}// ... 后续的计算和输出部分需要相应修改return 0;
}

4. 通过调用对象的成员函数来获取名字和分数,进行计算和输出。

修改程序中计算平均分和输出学生信息的部分。现在,你需要遍历 std::vector<Student> 中的每个 Student 对象,并使用 getName()getScore() 成员函数来获取学生的姓名和分数。

修改后的计算和输出部分代码示例:

C++

#include <iostream>
#include <vector>
#include <string>
#include <numeric> // 需要包含 numeric 头文件才能使用 std::accumulate// 假设 Student 类的定义在 student.h 文件中或者在当前文件中int main() {// ... (数据读取部分代码) ...if (!students.empty()) {int totalScore = 0;for (const auto& student : students) {totalScore += student.getScore();std::cout << student.getName() << " 的分数是: " << student.getScore() << std::endl;}double averageScore = static_cast<double>(totalScore) / students.size();std::cout << "平均分是: " << averageScore << std::endl;} else {std::cout << "没有学生数据。" << std::endl;}return 0;
}

通过以上步骤,我们成功地使用 Student 类来封装学生的数据,使得代码更加清晰和易于管理。这只是面向对象编程的冰山一角,在接下来的模块中,我们将学习更多关于面向对象的特性。

相关文章:

  • 第五节:React Hooks进阶篇-如何用useMemo/useCallback优化性能
  • eSIM RSP(远程SIM配置)架构笔记
  • Spring Boot整合T-IO实现即时通讯
  • 记录第一次面试的经历
  • 游戏盾是什么?重新定义游戏安全边界
  • Sklearn入门之数据预处理preprocessing
  • Node.js 中的 Buffer(缓冲区)
  • esp-idf:多语言--lv_i18n
  • 状态模式详解与真实场景案例(Java实现)
  • 人脸检测-人脸关键点-人脸识别-人脸打卡-haar-hog-cnn-ssd-mtcnn-lbph-eigenface-resnet
  • 如何将 ESP32 快速接入高德、心知、和风天气API 获取天气信息
  • void MainWindow::on_btnOutput_clicked()为什么我在QT里面没有connect,也能触发点击效果
  • 【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——RTC时钟测试 #内部RTC时钟 #外部时钟模块AT8563
  • 运维面试题(十四)
  • 常见编码面试问题
  • 命令模式 (Command Pattern)
  • 问题记录(四)——拦截器“失效”?null 还是“null“?
  • 【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(一)
  • C++ 核心进阶
  • 探秘串口服务器厂家:背后的故事与应用
  • 网站数据每隔几秒切换怎么做的/seo主要做什么工作内容
  • 网站开发员/如何网络推广
  • 子网站建设并绑定独立域名/百度关键词首页排名怎么上
  • 建建设人才市场官方网站/上海专业seo公司
  • 网站建设哪家服务态度好/网站地址ip域名查询
  • 网站app建站多少钱/网络广告营销经典案例