C++零基础实践教程 指针与内存 类与对象入门 (面向对象基础)
模块六:指针与内存 (C++ 核心,谨慎引入)
欢迎来到 C++ 中最强大但也最需要小心处理的部分之一:指针和内存管理。理解这部分内容能让你更深入地了解 C++ 的底层工作方式,但错误使用也可能导致难以追踪的 Bug。
1. 内存地址 (&
运算符)
想象一下计算机的内存就像一条很长的街道,每个存储数据的单元(比如一个 int
或 double
变量)都住在这条街上的某个房子里。每个房子都有一个唯一的地址来标识它的位置。
在 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
)。*
在这里表示“这是一个指针变量”。
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)”成一个指向数组第一个元素 (
C++[0]
) 的指针。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): 可以对指针进行整数加减运算。给指针加上整数
C++n
,意味着将指针移动n * sizeof(指向的数据类型)
个字节,使其指向后续的第n
个元素。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. 动态内存分配 (new
和 delete
)
我们之前声明的变量(如 int a;
或 int arr[10];
)要么在函数内部(栈上),要么在全局区,它们的大小在编译时就已经确定了。但有时,我们需要在程序运行时根据需要才决定分配多少内存(例如,用户输入一个数字 n
,然后我们才创建一个大小为 n
的数组)。这就是动态内存分配 (Dynamic Memory Allocation) 的用武之地。动态分配的内存来自一个叫做堆 (Heap) 或自由存储区 (Free Store) 的内存区域。
-
new
运算符: 用于在堆上请求分配内存。- 分配单个变量:
数据类型 *指针变量 = new 数据类型;
- 分配数组:
数据类型 *指针变量 = new 数据类型[数组大小];
(这里的数组大小
可以是变量) new
会返回一个指向已分配内存块起始地址的指针。如果内存分配失败(例如内存不足),new
默认会抛出一个异常 (std::bad_alloc
)。
// 分配一个 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[] 指针变量;
(注意方括号[]
!)
// 释放之前分配的内存 delete p_int; // 释放单个 int p_int = nullptr; // 好习惯:释放后将指针设为 nullptr,避免悬挂指针delete[] p_array; // 释放 double 数组,必须用 delete[] p_array = nullptr; // 设为 nullptr
警告:
new
和delete
/delete[]
必须配对使用。 每个new
都需要一个对应的delete
(或delete[]
)。- 不要
delete
同一块内存两次 (Double Free),这会导致未定义行为。 - 不要
delete
指向栈内存或全局内存的指针。delete
只能用于new
分配的堆内存。 - 释放数组必须使用
delete[]
,释放单个对象必须使用delete
。 用错形式会导致未定义行为。
- 释放单个变量 (
-
悬挂指针 (Dangling Pointer): 当你
delete
了一个指针指向的内存后,该指针变量本身仍然存储着那个(现在无效的)内存地址。如果之后不小心再次通过这个指针访问内存,就会发生错误。将指针在delete
后立即设为nullptr
是一个避免悬挂指针的好习惯。
手动内存管理的风险总结:
- 内存泄漏: 忘记
delete
。 - 二次释放: 多次
delete
同一块内存。 - 悬挂指针: 使用
delete
后的指针。 - 用错
delete
形式:delete
用于数组或delete[]
用于单个对象。
这些错误都可能导致程序崩溃或难以预测的行为,并且调试起来非常困难。
6. 与 std::vector
对比 (及智能指针简介)
现在你体会到手动管理内存的复杂性和风险了吧?这正是我们之前学习的 std::vector
的价值所在!
-
std::vector
的优势:- 自动内存管理:
vector
在其内部为你处理了new
和delete[]
的所有细节。当你创建一个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_ptr
和std::shared_ptr
。 - 智能指针也是 RAII 的体现: 它们像普通指针一样使用,但它们是对象,负责自动管理其指向的动态内存。当智能指针本身被销毁时(例如离开作用域),它会自动调用
delete
(或delete[]
) 来释放它所拥有的内存。 std::unique_ptr
: 实现独占所有权,同一时间只有一个unique_ptr
可以指向某个对象。它轻量高效,是替代裸指针管理单个new
对象的首选。std::shared_ptr
: 实现共享所有权,允许多个shared_ptr
指向同一个对象,并通过引用计数来跟踪,当最后一个指向该对象的shared_ptr
被销毁时,内存才会被释放。- 结论: 智能指针是现代 C++ 中管理动态内存的推荐方式,可以极大减少内存泄漏和悬挂指针的风险。虽然本入门教程不深入讲解,但你应该知道它们的存在,并在后续学习中重点掌握。
- 对于那些
7. 实战练习
-
使用指针交换两个变量的值:
- 编写一个函数
void swap_ptr(int *p1, int *p2)
。 - 函数接收两个指向
int
的指针作为参数。 - 在函数内部,通过解引用这两个指针,交换它们所指向的内存地址中的值。你需要一个临时变量来辅助交换。
- 在
main
函数中,定义两个int
变量,获取它们的地址,然后调用swap_ptr
函数,并验证变量的值确实被交换了。 - 对比思考: 这个函数与我们之前可能写的
void swap_ref(int& a, int& b)
(使用引用)有什么异同?(效果相同,但调用方式和内部实现略有不同)。
#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 指向的地址 }
- 编写一个函数
-
new
和delete[]
演示:- 编写一个程序:
- 提示用户输入一个整数
size
。 - 使用
new int[size]
创建一个动态整数数组,用一个指针变量int *dynamicArray
指向它。检查size
是否大于 0。 - 使用
for
循环填充这个数组,例如,让dynamicArray[i] = i * i;
。 - 使用另一个
for
循环打印数组的内容。 - 最重要的一步: 使用
delete[] dynamicArray;
释放分配的内存。 - 将指针设为
nullptr
:dynamicArray = nullptr;
。 - 在代码中添加注释,强调为什么用
new[]
就必须用delete[]
,并说明std::vector
是更推荐的方式。
- 提示用户输入一个整数
#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) 是一种以“过程”为中心的编程思想。它将程序看作是一系列按顺序执行的步骤(函数或过程)。解决问题时,我们关注的是“怎么做”,将问题分解为一系列函数调用。
举个例子: 假设我们要计算一个学生的平均成绩。在面向过程的思维中,我们可能会有以下步骤:
- 获取学生姓名。
- 获取学生各科成绩。
- 计算总成绩。
- 计算平均成绩。
- 输出结果。
每个步骤都可能对应一个函数。这种方式对于解决简单问题很直观。
面向对象 (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
是类名。width
和height
是Rectangle
类的成员变量,用来存储矩形的宽度和高度。calculateArea()
和printDetails()
是Rectangle
类的成员函数,分别用于计算矩形的面积和打印矩形的详细信息。
3. 访问修饰符: public
和 private
(封装)
访问修饰符 (Access Modifiers) 用于控制类成员的可见性和可访问性。C++ 中常用的访问修饰符有 public
和 private
。
public
(公有):public
修饰的成员可以在类的内部、类的外部(通过对象)以及派生类中被访问。它们是类的对外接口。private
(私有):private
修饰的成员只能在类的内部被访问。类的外部和派生类都不能直接访问私有成员。
封装 (Encapsulation) 是面向对象编程的核心概念之一。它指的是将数据(成员变量)和操作数据的函数(成员函数)捆绑在一起,并对数据的访问进行控制。通过使用访问修饰符,我们可以实现封装。
封装的优点:
- 数据隐藏:
private
成员变量隐藏了类的内部实现细节,防止外部直接修改,提高了数据的安全性。 - 接口控制:
public
成员函数提供了与对象交互的接口,外部只能通过这些接口来操作对象,保证了对象状态的有效性和一致性。 - 代码维护性: 当类的内部实现发生变化时,只要公有接口保持不变,就不会影响到使用该类的其他代码。
继续上面的 Rectangle
例子,我们可以将 width
和 height
设置为 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;}
};
现在,width
和 height
只能在 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;
}
在上面的例子中,rect1
和 rect2
都是 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
。然后,我们使用点运算符来设置它的公有成员变量 width
和 height
,并调用它的公有成员函数 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()
: 没有参数,将width
和height
初始化为 0.0。 - 带参数的构造函数
Rectangle(double w, double h)
: 接受两个double
类型的参数,用于初始化width
和height
。
在 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;
用于存储学生的分数。
- 公有成员函数:
- 构造函数: 接收学生的姓名和分数作为参数,并使用这些参数初始化
name
和score
成员变量。 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
类来封装学生的数据,使得代码更加清晰和易于管理。这只是面向对象编程的冰山一角,在接下来的模块中,我们将学习更多关于面向对象的特性。