目录
[1. 封装:把东西包起来](#1. 封装:把东西包起来)
[2. 继承:复用已有的东西](#2. 继承:复用已有的东西)
[3. 多态:同一个动作,不同表现](#3. 多态:同一个动作,不同表现)
一、一个真实的故事
去年我刚转C++的时候,写了一个学生管理系统。
用C的思路,我定义了一个结构体,然后写了一堆函数:add_student()、find_student()、print_score()......代码跑得挺好,直到有一天,产品经理说:"每个学生需要加一个'手机号'字段。"
我改了结构体,然后满屏找哪些函数用到了学生数据------有6个文件,20多处调用。改完编译,一堆报错,折腾了一下午。
后来我用C++的类重写了一遍,同样的需求,半小时搞定。
这不是技术高低的问题,是组织代码的方式变了。
二、C语言的过程式思维:数据是数据,操作是操作
C语言编程的核心思路是:你有一堆数据,然后写一堆函数去处理它们。
c
// C语言风格
struct Student {
char name[20];
int score;
};
void printStudent(struct Student* s) {
printf("%s: %d\n", s->name, s->score);
}
int main() {
struct Student s = {"张三", 85};
printStudent(&s);
return 0;
}
数据(Student结构体)和操作(printStudent函数)是分开的。代码少的时候很清晰,但项目一大,问题就来了:
-
哪个函数改了哪个数据?很难追踪
-
新增一个字段,所有相关函数都要检查
-
多个模块都用同一个结构体,改一处可能炸一片
这就是过程式的痛点:数据和操作是松散的,维护成本随代码量线性增长。
三、C++的面向对象思维:把数据和操作绑在一起
面向对象告诉你:一个物体的数据和能对它做的事情,应该放在一起。
cpp
// C++风格
class Student {
private:
char name[20];
int score;
public:
void setName(const char* n) { strcpy(name, n); }
void setScore(int s) { score = s; }
void print() const {
cout << name << ": " << score << endl;
}
};
int main() {
Student s;
s.setName("张三");
s.setScore(85);
s.print();
return 0;
}
看起来只是语法变了,但本质不一样:Student这个"类"现在是一个完整的整体,数据藏在里面,外部只能通过public函数去操作。
这就是封装------把数据和操作它的函数包在一起,把内部细节藏起来。
四、三个核心概念:用一个例子串起来
光说概念不好懂,我用一个"动物"的例子把封装、继承、多态串一遍。
1. 封装:把东西包起来
cpp
class Animal {
private:
int age; // 外部不能直接访问
public:
void setAge(int a) {
if (a > 0) age = a; // 可以加校验逻辑
}
int getAge() const { return age; }
void speak() { cout << "动物叫" << endl; }
};
外部的人只能通过setAge()来改年龄,不能直接animal.age = -5。封装保护了数据的一致性。
2. 继承:复用已有的东西
cpp
class Dog : public Animal { // Dog 继承 Animal
public:
void wagTail() { cout << "摇尾巴" << endl; }
};
Dog自动拥有了Animal的所有成员(age、setAge、getAge、speak),还可以加自己的wagTail。不需要重新写一遍。
这就是继承的意义:站在别人的肩膀上。
3. 多态:同一个动作,不同表现
cpp
class Cat : public Animal {
public:
void speak() { cout << "喵喵喵" << endl; } // 重写
};
void letItSpeak(Animal* a) {
a->speak(); // 传Dog就汪汪,传Cat就喵喵
}
同样的speak()调用,传入不同对象,表现不一样。代码写的是一套逻辑,跑出来是多种效果。
五、一个小对比:到底好在哪里?
用C写一个"图形面积计算",你可能这样:
c
// C:需要手动判断类型
struct Circle { float r; };
struct Rect { float w, h; };
float area(void* shape, int type) {
if (type == 1) return ((Circle*)shape)->r * 3.14;
if (type == 2) return ((Rect*)shape)->w * ((Rect*)shape)->h;
}
// 每加一个新形状,修改这个函数 —— 风险大
用C++写:
cpp
class Shape {
public:
virtual float area() = 0; // 纯虚函数
};
class Circle : public Shape {
float r;
public:
float area() override { return r * r * 3.14; }
};
class Rect : public Shape {
float w, h;
public:
float area() override { return w * h; }
};
// 加新形状:加一个新类,已有代码完全不用改
C++的版本更安全、更容易扩展。这就是OOP对大型项目友好的原因。
六、初学者最常陷入的误区
刚学的时候我也犯过这些错,提前给你打预防针:
-
把所有东西都塞进一个类
不是所有代码都需要是类。工具函数用命名空间就行,别硬塞。
-
滥用getter/setter,把私有变量全暴露出去
如果每个私有变量都配一对get/set,那封装等于没做。想想"为什么外部要改它"。
-
以为"面向对象=用class"
用了
class不等于用了OOP思想。OOP核心是职责划分、依赖管理、可维护性,不是语法。
七、这一篇的收获
读完这篇,你应该记住三句话:
-
封装:数据和操作放一起,内部细节藏起来
-
继承:用已有类派生出新类,复用代码
-
多态:同一个接口,不同对象有不同实现
下一篇我们会真正动手写第一个完整的类,从构造函数到析构函数,把对象的生命周期搞明白。
💡 一个小作业:找一个你以前用C写过的程序(比如通讯录、图书管理),想一想------如果变成类,你会怎么拆分?
下一篇预告:第2篇《类与对象(一):定义第一个类------成员变量与成员函数》,我们将真正写代码,从0构建一个可用的类。