news 2026/7/15 7:51:57

C++面向对象编程:从封装、继承到多态的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++面向对象编程:从封装、继承到多态的实战指南

1. 从“过程”到“对象”:为什么我们需要面向对象编程?

如果你已经跟着这个系列走过了前三篇,从变量、循环、函数一路打怪升级过来,那么恭喜你,你已经掌握了C++作为一门“高级C语言”的大部分核心玩法。但如果你觉得写代码还是像在写一份冗长的“操作说明书”——先做A,再做B,检查C,然后处理D——那么,是时候推开一扇新世界的大门了:面向对象编程。

我刚开始学C++时,对“面向对象”这个词充满了敬畏和困惑。听起来很高大上,但到底有什么用?直到我接手维护一个几千行的、用纯C风格(或者说,面向过程风格)写的学生管理系统。那个程序里,学生的“数据”(比如姓名、学号、成绩)和“操作”(比如计算平均分、打印信息)是割裂的。数据用一堆全局数组存着,操作它们的函数散落在各个角落。想加一个“班级”的概念?几乎意味着要把整个程序重写一遍。那感觉,就像在管理一个没有文件夹、所有文件都堆在桌面上的电脑,找什么都得靠搜索,改一点东西就可能引发连锁崩溃。

面向对象编程就是为了解决这种混乱而生的。它的核心思想很简单:把数据和操作这些数据的方法,打包成一个整体,这个整体就叫“对象”。比如,一个“学生”对象,它内部既有姓名、学号这些数据,也有“计算绩点”、“打印信息”这些方法。外界想了解这个学生,不用去翻一堆全局变量和函数,直接跟这个“学生对象”打交道就行。这就像现实世界,你想用手机,不需要知道它内部CPU怎么运算、屏幕怎么发光,你只需要知道按哪个键能打电话、划哪里能看视频。手机这个“对象”把复杂的内部细节“封装”起来,只给你提供简单易用的接口。

这种思维方式带来了三大核心武器:封装、继承和多态。封装让你代码更安全、更模块化;继承让你能复用已有的代码,像搭积木一样构建新功能;多态则让你的程序更灵活,能应对未来的变化。接下来,我们就从最基础的“类与对象”开始,一步步拆解这三大利器。别担心,我会用最“人话”的例子,带你绕过我当年踩过的所有坑。

2. 蓝图与实物:理解类与对象

面向对象编程里最基础、也最重要的两个概念就是“类”和“对象”。你可以这样理解:

  • :就像一张设计蓝图。它定义了某一类事物应该长什么样,有什么属性,能做什么事。比如,“汽车”的设计蓝图规定了它有轮子、发动机、方向盘等属性,以及启动、加速、刹车等方法。但蓝图本身不能开,它只是一个抽象的描述。
  • 对象:就是根据蓝图制造出来的具体实物。比如,根据“汽车”蓝图生产出来的你家那台车,车牌是“京A·12345”,颜色是红色。这个具体的车,就是一个“对象”。程序运行时,我们操作的都是一个个具体的对象。

2.1 定义一个类:从设计学生开始

让我们动手定义一个最简单的Student类。假设我们只需要记录学生的姓名和年龄。

class Student { public: // 访问权限,后面会细说 // 成员变量(属性) string name; int age; // 成员函数(方法) void introduce() { cout << "大家好,我叫" << name << ",今年" << age << "岁。" << endl; } };

看,这就是一个最简单的类定义。class是关键字,Student是类名。大括号{}里面定义了类的“成员”:nameage成员变量(也叫属性),introduce成员函数(也叫方法)。public:表示这些成员是公开的,外部可以直接访问。

注意:类定义末尾的分号;千万不能省略!这是很多新手容易忘记的地方,编译器会报一堆你看不懂的错误。

2.2 创建并使用对象:让蓝图变成现实

定义了蓝图,现在我们来“造”几个学生:

int main() { // 创建对象(实例化) Student stu1; // stu1 是一个Student类的对象 Student stu2; // 访问对象的成员:使用点运算符 . stu1.name = "张三"; stu1.age = 18; stu1.introduce(); // 输出:大家好,我叫张三,今年18岁。 stu2.name = "李四"; stu2.age = 20; stu2.introduce(); // 输出:大家好,我叫李四,今年20岁。 return 0; }

Student stu1;这行代码,就叫做“实例化”,它根据Student类这个蓝图,在内存中创建了一个实实在在的stu1对象。stu1stu2虽然都是Student类型,但它们各自拥有独立的内存空间,修改stu1name不会影响stu2。这个“点运算符.”就是你和对象“对话”的方式,用来访问它的内部成员。

实操心得:刚开始,你可能会觉得“这不就是struct(结构体)加了个函数吗?”没错,在C++中,classstruct在底层非常相似。关键区别在于默认的访问权限:class默认成员是private(私有的),而struct默认是public(公共的)。我们通常用class来强调数据封装,用struct来组织纯粹的数据集合。但理解了这个,你就抓住了类与对象的物理本质——它就是一块包含了数据和函数指针的自定义内存区域。

3. 封装:把秘密藏起来,只留一扇门

上面例子中,我们把nameage都设为public,这意味着在main函数里,我可以随意写stu1.age = -5;。这显然不合理,年龄怎么能是负数呢?这就是不封装的坏处:数据完全暴露,毫无安全性可言。

封装的理念就是:将对象的属性和实现细节隐藏起来,仅对外公开有限的接口(函数)进行交互。就像你的银行账户,余额(属性)是隐藏的,你不能直接改,但可以通过“取款”、“查询”这些公开接口(方法)来安全地操作它。

3.1 访问权限控制:public, private, protected

C++用三个关键字来控制类成员的访问权限:

  • public(公有):类内、类外都可以自由访问。就像客厅,谁都能进。
  • private(私有):只有类自己的成员函数可以访问。就像你的卧室,外人不能进。这是封装的基石,类的数据成员通常设为private
  • protected(受保护):介于两者之间,类内和派生类(子类)可以访问。这个我们讲到继承时再说。

我们来改造一下Student类,实现封装:

class Student { private: // 将数据隐藏起来 string name; int age; public: // 对外提供操作接口 // 设置年龄,可以加入合法性检查 void setAge(int newAge) { if (newAge > 0 && newAge < 150) { // 简单的数据验证 age = newAge; } else { cout << "年龄输入不合法!" << endl; } } // 获取年龄 int getAge() { return age; } // 设置姓名 void setName(string newName) { name = newName; } // 获取姓名 string getName() { return name; } void introduce() { cout << "大家好,我叫" << name << ",今年" << age << "岁。" << endl; } }; int main() { Student stu; // stu.age = 18; // 错误!age是private,无法直接访问 stu.setName("王五"); stu.setAge(18); // 通过公共接口安全地设置 // stu.setAge(-5); // 这里会被接口内的检查拦截,年龄不会被修改 cout << stu.getName() << "的年龄是:" << stu.getAge() << endl; // 通过接口获取 stu.introduce(); return 0; }

看到了吗?现在main函数无法直接触碰nameage。所有操作都必须通过setAgegetAge这些公开的成员函数。在setAge函数里,我们加入了简单的数据验证,确保了数据的有效性。这就是封装的力量:数据安全了,代码也更清晰了。你想修改学生信息?去找对应的set函数。你想获取信息?去找对应的get函数。这种函数就是我们常说的“Getter”和“Setter”。

3.2 构造函数与析构函数:对象的生与死

对象被创建时,需要初始化;对象生命结束时,可能需要清理资源(比如关闭文件、释放内存)。这两个特殊时刻由两个特殊的成员函数管理:

  • 构造函数:在对象创建时自动调用,用于初始化对象。函数名与类名相同,没有返回类型。
  • 析构函数:在对象销毁时自动调用,用于清理工作。函数名是类名前加一个波浪线~,没有参数和返回类型。
class Student { private: string name; int age; int* scores; // 假设我们用一个动态数组来存储多门课成绩 public: // 1. 默认构造函数(无参) Student() { name = "未知"; age = 0; scores = nullptr; // 初始化为空指针 cout << "调用默认构造函数" << endl; } // 2. 带参数的构造函数(重载) Student(string stuName, int stuAge) { name = stuName; age = stuAge; scores = new int[5]; // 为5门课成绩分配堆内存 for(int i = 0; i < 5; ++i) scores[i] = 0; // 初始化为0 cout << "调用带参构造函数,姓名为:" << name << endl; } // 3. 析构函数 ~Student() { if (scores != nullptr) { delete[] scores; // 释放动态分配的内存,防止内存泄漏! scores = nullptr; } cout << "析构函数被调用,清理了" << name << "的资源" << endl; } // ... 其他的Getter、Setter和introduce函数 }; int main() { Student stu1; // 调用默认构造函数 Student stu2("赵六", 19); // 调用带参构造函数 { Student stu3("临时学生", 20); // 在作用域内创建 } // 作用域结束,stu3被销毁,自动调用析构函数 cout << "main函数即将结束" << endl; return 0; } // main结束,stu1和stu2被销毁,自动调用析构函数

运行这段代码,你会清晰地看到构造函数和析构函数被调用的顺序。这是C++管理资源生命周期的核心机制。特别是当类中有指针成员并指向动态分配的内存(堆内存)时,必须在析构函数中释放,否则会造成“内存泄漏”。这是新手常踩的大坑!

注意事项

  1. 如果你没写任何构造函数,编译器会生成一个什么都不做的默认构造函数。但只要你定义了一个带参数的构造函数,编译器就不再提供默认构造函数。此时如果你写Student stu;就会编译错误。一个好的习惯是,总是显式地定义一个无参的默认构造函数。
  2. 构造函数可以重载,根据参数不同执行不同的初始化逻辑。
  3. 析构函数只有一个,不能重载。对于管理资源的类,编写析构函数是必须的。

4. 继承:站在巨人的肩膀上写代码

想象一下,你要开发一个游戏,里面有Enemy(敌人)这个类,它有health(血量)、attack()(攻击)等方法。现在你需要两种具体的敌人:Goblin(哥布林)和Dragon(龙)。龙会飞,会喷火,哥布林会偷东西。如果分别从头写这两个类,你会写很多重复的代码(比如health和基础的attack())。

继承就是为了解决代码复用的问题。它允许我们定义一个类(派生类/子类)基于另一个类(基类/父类)。子类会自动获得父类的所有属性和方法(除了构造函数、析构函数和私有成员),并且可以添加自己特有的属性和方法,或者修改父类的方法。

4.1 基础的继承语法

// 基类(父类) class Enemy { protected: // 让子类可以访问 int health; int attackPower; public: Enemy(int h, int ap) : health(h), attackPower(ap) {} // 构造函数初始化列表 void takeDamage(int damage) { health -= damage; if (health < 0) health = 0; cout << "敌人受到" << damage << "点伤害,剩余血量:" << health << endl; } virtual void attack() { // virtual关键字,为多态做准备 cout << "敌人发动了普通攻击,造成" << attackPower << "点伤害!" << endl; } }; // 派生类(子类)Goblin, 公有继承自Enemy class Goblin : public Enemy { private: int stealPower; // 哥布林特有的属性:偷窃能力 public: // 子类构造函数,需要调用父类构造函数初始化继承来的成员 Goblin(int h, int ap, int sp) : Enemy(h, ap), stealPower(sp) { cout << "一只哥布林出现了!" << endl; } // 重写父类的attack方法 void attack() override { // override关键字(C++11),明确表示重写,提高可读性 cout << "哥布林用短刀砍击,造成" << attackPower << "点伤害!" << endl; } // 子类特有的方法 void steal() { cout << "哥布林偷走了你的" << stealPower << "枚金币!" << endl; } }; // 派生类(子类)Dragon class Dragon : public Enemy { public: Dragon(int h, int ap) : Enemy(h, ap) { cout << "一条巨龙降临了!" << endl; } void attack() override { cout << "巨龙喷出炽热的火焰,造成" << attackPower * 2 << "点范围伤害!" << endl; // 伤害加倍 } void fly() { cout << "巨龙腾空而起!" << endl; } }; int main() { Goblin g(50, 10, 5); // 血量50,攻击力10,偷窃力5 Dragon d(200, 25); // 血量200,攻击力25 g.attack(); // 调用Goblin自己的attack g.steal(); // 调用Goblin特有的steal g.takeDamage(30); // 调用继承自Enemy的takeDamage d.attack(); // 调用Dragon自己的attack d.fly(); // 调用Dragon特有的fly d.takeDamage(100); // 调用继承自Enemy的takeDamage return 0; }

语法class Goblin : public Enemy表示Goblin类以公有继承的方式继承Enemy类。这意味着:

  1. Enemy中的public成员,在Goblin中仍然是public
  2. Enemy中的protected成员,在Goblin中仍然是protected
  3. Enemy中的private成员,在Goblin不可直接访问(但可以通过父类的publicprotected方法间接访问)。

初始化列表: Enemy(h, ap)在子类构造函数中至关重要,它用来显式调用父类的构造函数,初始化从父类继承来的成员。子类无法直接初始化父类的私有成员。

4.2 继承中的构造与析构顺序

当创建一个子类对象时:

  1. 调用父类的构造函数。
  2. 调用子类成员对象的构造函数(如果有的话)。
  3. 调用子类的构造函数体。 析构的顺序则完全相反:子类析构 -> 子类成员析构 -> 父类析构。

这个顺序是自动的,理解它有助于你调试和理解对象的生命周期。

常见问题“我该用公有继承、保护继承还是私有继承?”

  • 公有继承:最常用,表示“是一个”的关系。Goblin是一个Enemy。这是逻辑上的继承。
  • 保护/私有继承:很少用,表示“以...实现”的关系。更多是技术上的复用,而非逻辑上的派生。对于新手,99%的情况使用公有继承就够了。如果你不确定,那就用公有继承。

5. 多态:同一操作,不同行为

多态是面向对象最精妙、也最强大的特性。字面意思是“多种形态”。在C++中,多态通常指:通过基类的指针或引用调用一个虚函数时,实际执行的是派生类中重写的那个函数版本。

这有什么用?回到游戏的例子。假设我们有一个Enemy指针数组,里面装着各种实际是GoblinDragon的敌人。在游戏主循环中,我们想让他们全部攻击玩家。如果没有多态,我们需要判断每个指针具体指向哪种敌人,然后分别调用不同的attack函数,代码会非常冗长。

有了多态,我们可以这样写:

int main() { // 创建一个基类指针数组,指向不同的派生类对象 Enemy* enemies[3]; enemies[0] = new Goblin(50, 10, 5); enemies[1] = new Dragon(200, 25); enemies[2] = new Goblin(40, 8, 3); cout << "\n--- 敌人全体攻击!---" << endl; for (int i = 0; i < 3; ++i) { enemies[i]->attack(); // 关键在这里! // 虽然enemies[i]是Enemy*类型,但它实际指向Goblin或Dragon对象 // 因此这里调用的是各自派生类重写的attack版本 } cout << "\n--- 战斗结束,清理战场 ---" << endl; for (int i = 0; i < 3; ++i) { delete enemies[i]; // 记得释放内存 } return 0; }

输出会是:

一只哥布林出现了! 一条巨龙降临了! 一只哥布林出现了! --- 敌人全体攻击!--- 哥布林用短刀砍击,造成10点伤害! 巨龙喷出炽热的火焰,造成50点范围伤害! 哥布林用短刀砍击,造成8点伤害! --- 战斗结束,清理战场 ---

神奇吗?enemies[i]在编译时是Enemy*类型,编译器只知道它指向一个Enemy。但在运行时,它才知道这个指针实际指向的是Goblin还是Dragon,并调用对应类的attack方法。这就是“运行时多态”或“动态绑定”。

5.1 实现多态的关键:虚函数

要实现上述效果,有两个必要条件:

  1. 基类中的函数必须声明为virtual(虚函数)。就像我们之前在Enemy::attack()前加的那样。
  2. 派生类必须重写这个虚函数。最好加上override关键字(C++11),这样如果你不小心写错了函数签名(参数或返回类型),编译器会报错,这是一个很好的安全措施。

虚函数表:这是多态的实现原理。每个有虚函数的类(或从有虚函数的类派生而来)都有一个隐藏的成员——虚函数表指针(vptr),它指向一个虚函数表(vtable)。表中存放了该类所有虚函数的实际地址。当通过基类指针调用虚函数时,程序会通过vptr找到vtable,再找到正确的函数地址进行调用。这个过程发生在运行时,因此有少量性能开销,但对于现代计算机,这点开销在大多数场景下完全可以接受。

5.2 纯虚函数与抽象类

有时候,基类中的某个函数在逻辑上无法给出一个有意义的实现。比如,Shape(形状)基类有一个draw()(绘制)方法。但“形状”本身太抽象了,不知道怎么画。这时,我们可以将它声明为纯虚函数

class Shape { public: virtual void draw() = 0; // = 0 表示这是一个纯虚函数 virtual double area() = 0; // 计算面积,也是纯虚函数 // 可以包含非虚函数和成员变量 void printInfo() { cout << "这是一个形状。" << endl; } };

包含纯虚函数的类叫做抽象类。抽象类不能实例化对象,即你不能写Shape s;。它的作用就是作为一个接口,强制要求它的所有派生类(除非也是抽象类)必须实现这些纯虚函数。

class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} // 必须实现基类的所有纯虚函数,否则Circle也会变成抽象类 void draw() override { cout << "绘制一个半径为" << radius << "的圆形。" << endl; } double area() override { return 3.14159 * radius * radius; } }; class Rectangle : public Shape { private: double width, height; public: Rectangle(double w, double h) : width(w), height(h) {} void draw() override { cout << "绘制一个" << width << "x" << height << "的矩形。" << endl; } double area() override { return width * height; } }; int main() { // Shape s; // 错误!Shape是抽象类,不能创建对象 Shape* shapes[2]; shapes[0] = new Circle(5.0); shapes[1] = new Rectangle(4.0, 6.0); for (int i = 0; i < 2; ++i) { shapes[i]->draw(); cout << "面积是:" << shapes[i]->area() << endl; shapes[i]->printInfo(); // 调用继承来的非虚函数 delete shapes[i]; } return 0; }

抽象类和纯虚函数定义了接口规范,是实现多态和大型项目架构设计的强大工具。它确保了所有派生类都遵循同一套操作标准。

6. 面向对象实战:设计一个简单的银行账户系统

让我们把封装、继承、多态综合运用起来,设计一个迷你银行账户系统。

6.1 基类 Account(账户)

首先,定义一个抽象的Account基类,规定所有账户都必须有存款、取款、查询余额的功能。

#include <iostream> #include <string> using namespace std; // 抽象基类:账户 class Account { protected: string accountNumber; // 账号 string ownerName; // 户主名 double balance; // 余额 public: Account(string accNum, string name, double initBalance) : accountNumber(accNum), ownerName(name), balance(initBalance) {} virtual ~Account() {} // 虚析构函数,确保派生类对象被基类指针删除时能正确调用派生类析构函数 // 纯虚函数,定义接口 virtual void deposit(double amount) = 0; // 存款 virtual bool withdraw(double amount) = 0; // 取款,返回是否成功 virtual void display() const = 0; // 显示账户信息,const表示不修改对象 // 非虚函数,通用功能 double getBalance() const { return balance; } string getAccountNumber() const { return accountNumber; } };

6.2 派生类 SavingsAccount(储蓄账户)

储蓄账户可能有最低余额限制,取款不能低于这个限制。

class SavingsAccount : public Account { private: double minBalance; // 最低余额要求 public: SavingsAccount(string accNum, string name, double initBalance, double minBal) : Account(accNum, name, initBalance), minBalance(minBal) { if (initBalance < minBal) { cout << "警告:初始余额低于最低要求!" << endl; } } void deposit(double amount) override { if (amount > 0) { balance += amount; cout << ownerName << "的储蓄账户存入 " << amount << " 元。当前余额:" << balance << endl; } else { cout << "存款金额必须为正数!" << endl; } } bool withdraw(double amount) override { if (amount > 0 && (balance - amount) >= minBalance) { balance -= amount; cout << ownerName << "的储蓄账户取款 " << amount << " 元。当前余额:" << balance << endl; return true; } else { cout << "取款失败!金额无效或余额将低于最低要求(" << minBalance << "元)。" << endl; return false; } } void display() const override { cout << "--- 储蓄账户信息 ---" << endl; cout << "账号:" << accountNumber << endl; cout << "户主:" << ownerName << endl; cout << "余额:" << balance << " 元" << endl; cout << "最低余额要求:" << minBalance << " 元" << endl; } };

6.3 派生类 CheckingAccount(支票账户)

支票账户可能没有最低余额限制,但每次取款可能会收取小额手续费。

class CheckingAccount : public Account { private: double transactionFee; // 每笔取款手续费 public: CheckingAccount(string accNum, string name, double initBalance, double fee) : Account(accNum, name, initBalance), transactionFee(fee) {} void deposit(double amount) override { if (amount > 0) { balance += amount; cout << ownerName << "的支票账户存入 " << amount << " 元。当前余额:" << balance << endl; } else { cout << "存款金额必须为正数!" << endl; } } bool withdraw(double amount) override { double totalDeduction = amount + transactionFee; if (amount > 0 && balance >= totalDeduction) { balance -= totalDeduction; cout << ownerName << "的支票账户取款 " << amount << " 元,手续费 " << transactionFee; cout << " 元。当前余额:" << balance << endl; return true; } else { cout << "取款失败!金额无效或余额不足(含手续费)。" << endl; return false; } } void display() const override { cout << "--- 支票账户信息 ---" << endl; cout << "账号:" << accountNumber << endl; cout << "户主:" << ownerName << endl; cout << "余额:" << balance << " 元" << endl; cout << "取款手续费:" << transactionFee << " 元/笔" << endl; } };

6.4 使用多态管理多种账户

现在,我们可以用基类指针来统一管理不同类型的账户。

int main() { // 创建不同类型的账户对象,用基类指针指向它们 Account* myAccounts[3]; myAccounts[0] = new SavingsAccount("SA001", "张三", 1000.0, 100.0); // 最低余额100 myAccounts[1] = new CheckingAccount("CA001", "李四", 500.0, 2.0); // 手续费2元 myAccounts[2] = new SavingsAccount("SA002", "王五", 2000.0, 500.0); cout << "\n=== 初始账户状态 ===" << endl; for (int i = 0; i < 3; ++i) { myAccounts[i]->display(); cout << endl; } cout << "\n=== 进行一系列交易 ===" << endl; // 多态的威力:无需知道具体是哪种账户,统一调用接口 myAccounts[0]->deposit(500); // 张三存钱 myAccounts[0]->withdraw(2000); // 张三取钱(会失败,低于最低余额) myAccounts[0]->withdraw(800); // 张三取钱(成功) myAccounts[1]->withdraw(100); // 李四取钱(含手续费) myAccounts[1]->deposit(300); // 李四存钱 myAccounts[2]->withdraw(1600); // 王五取钱 cout << "\n=== 最终账户状态 ===" << endl; for (int i = 0; i < 3; ++i) { myAccounts[i]->display(); cout << endl; } // 清理内存 for (int i = 0; i < 3; ++i) { delete myAccounts[i]; } return 0; }

这个例子展示了面向对象的核心优势:

  1. 封装:每个账户的内部数据(余额、最低限额、手续费)被保护起来,只能通过规定的depositwithdraw方法操作,保证了数据安全。
  2. 继承SavingsAccountCheckingAccount复用Account的代码(账号、户主名、余额等),只需关注自己特有的逻辑(最低余额、手续费)。
  3. 多态main函数中的数组和循环代码完全不用关心具体是哪种账户。无论是储蓄账户还是支票账户,都通过统一的Account指针接口来操作。如果要新增一种“信用卡账户”,只需要从Account派生一个新类并实现那几个纯虚函数,main函数里的管理代码一行都不用改!这就是“对扩展开放,对修改关闭”的开闭原则的体现。

7. 进阶话题与避坑指南

掌握了三大特性,你已经入门了面向对象。但在实际项目中,还有一些细节和陷阱需要特别注意。

7.1 对象的拷贝:深拷贝与浅拷贝

这是C++面试的经典问题,也是实际开发中Bug的高发区。当你用一个对象初始化另一个对象,或者将对象作为参数传值给函数时,会发生“拷贝”。

class SimpleClass { public: int* data; SimpleClass(int value) { data = new int(value); // 在堆上分配内存 } ~SimpleClass() { delete data; // 释放内存 } }; int main() { SimpleClass obj1(10); SimpleClass obj2 = obj1; // 默认拷贝构造:浅拷贝! // 问题来了:obj1.data 和 obj2.data 指向同一块堆内存 // main函数结束时,obj2先析构,delete了那块内存 // 然后obj1再析构,试图delete同一块已经释放的内存 -> 程序崩溃! }

默认的拷贝行为是“浅拷贝”,即只复制指针的值(地址),而不复制指针指向的内容。这会导致多个对象共享同一资源,析构时重复释放,引发未定义行为(通常是崩溃)。

解决方案:自定义拷贝构造函数和拷贝赋值运算符,实现“深拷贝”。

class SafeClass { public: int* data; int size; SafeClass(int value) : size(1) { data = new int(value); } // 1. 拷贝构造函数 SafeClass(const SafeClass& other) : size(other.size) { data = new int[size]; for(int i = 0; i < size; ++i) { data[i] = other.data[i]; // 复制内容,而不是地址 } cout << "调用了拷贝构造函数" << endl; } // 2. 拷贝赋值运算符 SafeClass& operator=(const SafeClass& other) { if (this != &other) { // 防止自我赋值 delete[] data; // 释放原有资源 size = other.size; data = new int[size]; for(int i = 0; i < size; ++i) { data[i] = other.data[i]; } } cout << "调用了拷贝赋值运算符" << endl; return *this; } ~SafeClass() { delete[] data; } };

经验法则:如果你的类管理了动态分配的内存、文件句柄、网络连接等资源,那么你很可能需要自己定义拷贝构造函数、拷贝赋值运算符和析构函数。这被称为“三法则”。在C++11之后,还有“五法则”(加上移动构造函数和移动赋值运算符)。对于新手,一个简单的规避策略是:禁用拷贝,使用unique_ptr等智能指针管理资源。

class NonCopyable { public: NonCopyable() = default; // 删除拷贝构造和拷贝赋值 NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; };

7.2 友元:打破封装的特权

封装是把数据藏起来,但有时候,某些特定的外部函数或类需要直接访问这些私有数据。比如,一个打印函数需要直接读取对象的私有成员来格式化输出。这时可以使用friend(友元)关键字。

class MyClass { private: int secret; public: MyClass(int s) : secret(s) {} // 声明一个全局函数为友元 friend void printSecret(const MyClass& obj); // 声明另一个类为友元 friend class FriendClass; }; void printSecret(const MyClass& obj) { // 这个函数不是MyClass的成员,但可以访问其私有成员 cout << "The secret is: " << obj.secret << endl; } class FriendClass { public: void peek(const MyClass& obj) { cout << "FriendClass sees: " << obj.secret << endl; } };

慎用友元!友元破坏了封装性,降低了代码的可维护性。它应该被视为一种例外手段,而不是常规工具。在大多数情况下,通过提供良好的公有接口(Getter/Setter)来访问数据是更好的选择。

7.3 静态成员:属于类本身的成员

有时候,我们需要一些数据或函数属于整个类,而不是类的某个对象。比如,统计一共创建了多少个某个类的对象。这就需要static(静态)成员。

class Counter { private: static int count; // 静态成员变量声明,记录对象总数 int id; public: Counter() { id = ++count; // 每创建一个对象,count加1,并赋予id cout << "创建了第" << id << "个Counter对象。" << endl; } ~Counter() { cout << "销毁了第" << id << "个Counter对象。" << endl; --count; } static int getTotalCount() { // 静态成员函数 return count; } }; // 静态成员变量必须在类外定义和初始化 int Counter::count = 0; int main() { cout << "当前对象数:" << Counter::getTotalCount() << endl; // 通过类名直接访问静态函数 Counter c1; Counter c2; { Counter c3; cout << "当前对象数:" << Counter::getTotalCount() << endl; } // c3离开作用域被销毁 cout << "当前对象数:" << Counter::getTotalCount() << endl; return 0; }

关键点

  1. 静态成员变量不属于任何对象,它在所有对象间共享,只有一份内存。
  2. 静态成员变量必须在类外(通常是.cpp文件)单独定义和初始化,格式为类型 类名::变量名 = 值;
  3. 静态成员函数没有this指针,因此它不能访问类的非静态成员(因为非静态成员需要通过this指针来访问)。静态成员函数通常用于操作静态成员变量。

面向对象编程是一个庞大而精深的体系,这一篇我们只是打下了最核心的基础。从把数据和函数打包成“对象”,到用“封装”保护内部状态,用“继承”复用已有代码,再用“多态”写出灵活而强大的程序,这条路径是理解现代软件设计的基石。我建议你多动手把文中的例子敲一遍,然后尝试设计一些自己的小类,比如BookLibraryVehicleCar。遇到问题就去查、去调试,这才是学习编程最快的方式。当你开始习惯用对象的视角去思考问题时,你会发现很多复杂的系统设计突然变得清晰起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 7:51:46

CANN/asc-devkit数据类型转换API

asc_int162half 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言&#xff0c;原生支持C和C标准规范&#xff0c;主要由类库和语言扩展层构成&#xff0c;提供多层级API&#xff0c;满足多维场景算子开发诉求。 项目地址: https://gitcode…

作者头像 李华
网站建设 2026/7/15 7:48:43

腾讯混元Hy3开源与GPT-5.6集成:MoE架构与AI编程实践指南

如果你是一名开发者&#xff0c;最近可能被两件事刷屏&#xff1a;腾讯混元Hy3正式版开源&#xff0c;以及OpenAI GPT-5.6 Sol Ultra即将登陆Codex的消息。但抛开媒体标题的喧嚣&#xff0c;真正重要的是——这些技术更新到底能为你带来什么实际价值&#xff1f;是又一个需要重…

作者头像 李华
网站建设 2026/7/15 7:48:16

openGraphScraper 实战:构建自定义元数据抓取器的完整步骤

openGraphScraper 实战&#xff1a;构建自定义元数据抓取器的完整步骤 【免费下载链接】openGraphScraper Node.js scraper service for Open Graph Info and More! 项目地址: https://gitcode.com/gh_mirrors/op/openGraphScraper 想要在 Node.js 中快速获取网页的 Ope…

作者头像 李华
网站建设 2026/7/15 7:47:54

PMCW体制雷达系列文章(3) – PMCW雷达匹配滤波与FMCW雷达FFT的对比分析

1. PMCW与FMCW雷达信号处理的核心差异PMCW&#xff08;相位调制连续波&#xff09;雷达和FMCW&#xff08;调频连续波&#xff09;雷达虽然都属于连续波雷达家族&#xff0c;但在信号处理环节却采用了截然不同的技术路线。PMCW雷达采用匹配滤波技术处理回波信号&#xff0c;而F…

作者头像 李华
网站建设 2026/7/15 7:45:38

openGraphScraper 高级功能揭秘:自定义元标签和 JSON-LD 解析

openGraphScraper 高级功能揭秘&#xff1a;自定义元标签和 JSON-LD 解析 【免费下载链接】openGraphScraper Node.js scraper service for Open Graph Info and More! 项目地址: https://gitcode.com/gh_mirrors/op/openGraphScraper openGraphScraper 是一款强大的 No…

作者头像 李华
网站建设 2026/7/15 7:36:46

3步快速搞定MacBook电池寿命延长:Charge Limiter终极指南

3步快速搞定MacBook电池寿命延长&#xff1a;Charge Limiter终极指南 【免费下载链接】charge-limiter macOS app to set battery charge limit for Intel MacBooks 项目地址: https://gitcode.com/gh_mirrors/ch/charge-limiter 想让你的MacBook电池用得更久吗&#xf…

作者头像 李华