一、面向对象概念
理解面向对象编程(OOP)就像学习一种新的思维方式,而不仅仅是语法规则。它的核心是把程序组织成一系列相互协作的“对象”,每个对象都有自己的职责。
让我用一个最经典的现实世界比喻来帮你建立直观感受。
核心思想:把一切都看作“对象”
想象一下你要设计一个“学校管理系统”。
在面向过程的思维中,你可能会想:“我需要先处理学生数据,然后处理课程数据,最后计算成绩...”,这是一系列步骤。
而在面向对象的思维中,你会想:“这个系统里涉及到哪些实体?有学生、老师、课程。好,那我就分别创建这些对象,让它们各司其职。”
[思维模式对比图]
┌─────────────────┐ ┌─────────────────┐
│ 面向过程思维 │ │ 面向对象思维 │
├─────────────────┤ ├─────────────────┤
│ 1. 输入学生数据 │ │ 学生对象 │
│ 2. 处理课程信息 │ │ - 属性:姓名、学号 │
│ 3. 计算成绩逻辑 │→ │ - 行为:选课、学习│
│ 4. 输出结果 │ │ 课程对象 │
│ 5. ... │ │ - 属性:名称、学分 │
└─────────────────┘ │ - 行为:安排、考核 │
└─────────────────┘
四大支柱(理解它们的关键)
面向对象有四个基本概念,这是它的精髓。我们继续用学校的例子。
- 抽象
核心思想:隐藏复杂性,只暴露必要的部分。
就像开车,你只需要知道方向盘、油门、刹车怎么用,而不需要懂发动机的工作原理。
编程体现:我们创建一个学生类。我们只关心学生的相关属性(如姓名、学号、年级)和相关行为(如选课、做作业、查询成绩)。我们不关心学生的心脏如何跳动这种无关细节。
作用:简化世界,让我们能专注于核心问题。
[抽象概念图]
┌─────────────────────────────────────────┐
│ 现实世界的学生 (复杂性) │
│ ┌─────────────────────────────────────┐ │
│ │ 姓名、年龄、身高、体重、爱好、家庭地址... │ │
│ │ 走路、吃饭、睡觉、思考、社交、学习... │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓ 抽象:提取相关特征
┌─────────────────────────────────────────┐
│ 程序中的学生类 (简化模型) │
│ ┌─────────────────────────────────────┐ │
│ │ 属性:姓名、学号、年级、成绩 │ │
│ │ 行为:选课、学习、考试、查询成绩 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
-
封装
核心思想:把数据(属性)和操作数据的方法(行为)捆绑在一起,并控制对内部数据的访问。
就像一个“保险箱”。钱(数据)放在里面,你只能通过特定的锁孔(方法)来存钱或取钱,不能直接把手伸进去拿。
编程体现:学生对象的“成绩”属性通常是私有的。你不能直接 student.grade = 100这样随意修改。你必须通过一个公共方法,比如 student.submitAssignment(assignment, score)来间接修改,这个方法内部可能还有验证逻辑(比如分数不能为负)。
作用:提高安全性、可维护性,内部实现改变了,只要方法接口不变,就不会影响其他代码。
[封装概念图:像保险箱一样保护数据]
┌─────────────────────────────────────────┐
│ 学生对象 (保险箱) │
│ ┌─────────────────────────────────────┐ │
│ │ 私有数据:__grade = 90 │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ 公共方法 (锁孔) │ │ │
│ │ │ setGrade(score) ← 安全入口 │ │ │
│ │ │ getGrade() ← 安全出口 │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ 直接访问:student.__grade = 100 ❌ 错误! │
│ 安全访问:student.setGrade(100) ✅ 正确! │
└─────────────────────────────────────────┘
-
继承
核心思想:基于已有的类创建新类,新类自动拥有父类的属性和方法,并可以添加自己特有的内容。
就像“遗传”。博士生是一种特殊的学生,他拥有学生的所有特征(姓名、学号),但可能还有自己独特的特征(如导师、研究课题)。
编程体现:我们可以创建一个博士生类,让它继承自学生类。这样博士生就自动拥有了姓名、选课等方法,我们只需要为它定义额外的属性和方法即可。
作用:代码复用,避免重复。可以建立清晰的层次结构。
[继承关系:家族树]
┌─────────────────┐
│ Person │ ← 基类/父类
│ - name │ 通用特征
│ - age │
│ + speak() │
└─────────────────┘
△
┌────────────┴────────────┐
│ │
┌─────────────────┐ ┌─────────────────┐
│ Student │ │ Teacher │ ← 派生类/子类
│ - studentId │ │ - teacherId │ 特有特征
│ + study() │ │ + teach() │
└─────────────────┘ └─────────────────┘
△ △
│ │
┌─────────────────────────┐ ┌─────────────────────┐
│ GraduateStudent │ │ SeniorTeacher │
│ - supervisor │ │ - department │ ← 更具体的子类
│ + doResearch() │ │ + manageTeam() │
└─────────────────────────┘ └─────────────────────┘
- 多态
核心思想:同一操作作用于不同的对象,可以产生不同的执行结果。
简单说就是“同一接口,不同实现”。
编程体现:学校系统里既有本科生也有研究生,它们都继承自学生类,都有一个计算学费的方法。但本科生和研究生的学费计算规则完全不同。
当系统通知所有学生计算学费时,它只需要统一调用 student.calculateTuition()。
对于本科生对象,这个方法会执行本科生的计算逻辑。
对于研究生对象,则会执行研究生的计算逻辑。
作用:提高代码的灵活性和可扩展性。添加新的学生类型(如交换生)时,无需修改调用方的代码。
[多态:同一方法,不同行为]
┌─────────────────────────────────────────┐
│ 客户端代码 (统一调用) │
│ student.calculateTuition() │
└─────────────────────────────────────────┘
↓
┌─────────────────┬─────────────────┐
│ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐
│本科生对象 │ │研究生对象 │ │交换生对象 │
│ tuition =│ │ tuition =│ │ tuition =│
│ 5000*学分 │ │ 8000*学分 │ │ 3000*学分 │
└─────────┘ └─────────┘ └─────────┘
│ │ │
└─────────────────┴─────────────────┘
↓
┌─────────────────────────────────────────┐
│ 不同结果,但调用方式相同 │
│ - 本科生:计算本科收费标准 │
│ - 研究生:计算研究生收费标准 │
│ - 交换生:计算交换生收费标准 │
└─────────────────────────────────────────┘
二、C语言实现封装
2.1 实现思想
用模块(.c + .h)、静态变量、结构体 + 函数组合来隐藏实现细节,只暴露必要接口给外部。
也就是:
- 对外暴露函数接口(API)
- 隐藏内部数据/函数(不放进 .h,或使用 static)
- 外部代码不能直接访问内部细节
- 这就是封装,只不过不靠关键字,而靠“文件隔离 + static”。
Account.h ← 对外暴露接口(公共 API)
Account.c ← 内部实现(封装)
main.c ← 外部使用者代码
2.2 Account.h(公共接口 — 对外暴露)
// Account.h
#ifndef ACCOUNT_H
#define ACCOUNT_H
// 只声明,不暴露内部字段
typedef struct Account Account;
/**
* 创建账户
*/
Account* Account_create(double initial);
/**
* 销毁账户
*/
void Account_destroy(Account* acc);
/**
* 存钱
*/
void Account_deposit(Account* acc, double amount);
/**
* 取钱
*/
void Account_withdraw(Account* acc, double amount);
/**
* 获取余额
*/
double Account_getBalance(const Account* acc);
#endif
2.3 Account.c(内部实现 — 封装部分)
// Account.c
#include "Account.h"
#include <stdio.h>
#include <stdlib.h>
// 真正的结构体定义(隐藏在 .c 文件中)
struct Account {
double balance;
};
// 内部的工具函数(私有),外面看不到
static void print_log(const char* msg) {
printf("[Account Log]: %s\n", msg);
}
Account* Account_create(double initial) {
if (initial < 0) initial = 0;
Account* acc = (Account*)malloc(sizeof(Account));
acc->balance = initial;
print_log("Account created");
return acc;
}
void Account_destroy(Account* acc) {
print_log("Account destroyed");
free(acc);
}
void Account_deposit(Account* acc, double amount) {
if (amount > 0) {
acc->balance += amount;
print_log("Deposit success");
}
}
void Account_withdraw(Account* acc, double amount) {
if (amount > 0 && amount <= acc->balance) {
acc->balance -= amount;
print_log("Withdraw success");
}
}
double Account_getBalance(const Account* acc) {
return acc->balance;
}
关键点:
-
struct Account的字段只有本文件能看到 → 完全封装 -
print_log()是static→ 模块私有 -
外部无法直接操作
balance -
所有访问都通过函数进行 → 受控访问
2.4 main.c(外部用户代码 — 使用封装好的模块)
// main.c
#include <stdio.h>
#include "Account.h"
int main() {
Account* acc = Account_create(100);
printf("初始余额: %.2f\n", Account_getBalance(acc));
Account_deposit(acc, 50);
printf("存钱后余额: %.2f\n", Account_getBalance(acc));
Account_withdraw(acc, 30);
printf("取钱后余额: %.2f\n", Account_getBalance(acc));
// ❌ 下面这行做不到:因为结构体字段被封装
// acc->balance = 9999; // 编译错误
Account_destroy(acc);
return 0;
}
三、C语言实现抽象
3.1 核心思想
- 用户只看到接口,不看到实现。
- 通过不透明结构体(Opaque Struct)隐藏内部细节。
实现方法:
-
头文件暴露接口(函数 + 不透明类型)
-
源文件隐藏实现(结构体成员 + 实际逻辑)
3.2 Shape.h(抽象接口层)
// Shape.h
#ifndef SHAPE_H
#define SHAPE_H
typedef struct Shape Shape;
// 抽象行为
double Shape_area(const Shape* shape);
// 创建对象
Shape* Shape_createCircle(double r);
Shape* Shape_createRect(double w, double h);
// 销毁对象
void Shape_destroy(Shape* shape);
#endif
3.3 Shape.c(隐藏细节)
// Shape.c
#include "Shape.h"
#include <stdlib.h>
typedef enum { CIRCLE, RECT } ShapeType;
struct Shape {
ShapeType type;
union {
struct { double r; } circle;
struct { double w, h; } rect;
} data;
};
double Shape_area(const Shape* s) {
switch (s->type) {
case CIRCLE:
return 3.14159 * s->data.circle.r * s->data.circle.r;
case RECT:
return s->data.rect.w * s->data.rect.h;
}
return 0;
}
Shape* Shape_createCircle(double r) {
Shape* s = malloc(sizeof(Shape));
s->type = CIRCLE;
s->data.circle.r = r;
return s;
}
Shape* Shape_createRect(double w, double h) {
Shape* s = malloc(sizeof(Shape));
s->type = RECT;
s->data.rect.w = w;
s->data.rect.h = h;
return s;
}
void Shape_destroy(Shape* s) {
free(s);
}
四、多态
4.1 核心思想
运行时多态的目标是:使用统一类型/接口操作不同具体对象,并在运行时调用该对象相应的实现,例如 shape->area() 在圆和矩形上调用不同函数,而调用点只写一次。
在 C 中,达成目标的手段是:
-
把“操作”表示为函数指针(单个函数或一组函数构成的表),
-
在对象上保存指向该表的指针(每个“类”一个表,类似 C++ vtable),
-
通过表进行间接调用,从而在运行时动态绑定到具体实现。
下面是 用 vtable 实现 Shape 的多态(包含“继承/覆盖”与“析构”)实例
目录结构:
/polymorphism
│
├── Shape.h
├── Shape.c
├── Circle.h
├── Circle.c
├── Rect.h
├── Rect.c
├── main.c
└── Makefile (可选)
4.2 shape.h(抽象基类 + vtable 声明)
// Shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <stddef.h>
typedef struct Shape Shape;
/* 虚函数表(基类接口) */
typedef struct {
double (*area)(const Shape *self);
void (*destroy)(Shape *self);
} ShapeVTable;
/* 基类 */
struct Shape {
const ShapeVTable *vtable;
char name[32];
};
/* 多态接口 */
double Shape_area(const Shape *s);
void Shape_destroy(Shape *s);
#endif
4.3 Shape.c(基类实现,共享函数)
// Shape.c
#include "Shape.h"
double Shape_area(const Shape *s) {
return s->vtable->area(s);
}
void Shape_destroy(Shape *s) {
s->vtable->destroy(s);
}
4.4 Circle.h(子类定义)
// Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
typedef struct {
Shape base;
double r;
} Circle;
Circle* Circle_new(double r, const char *name);
#endif
4.5 Circle.c(子类实现)
// Circle.c
#include "Circle.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
/* 子类实现的虚函数 */
static double Circle_area(const Shape *s) {
const Circle *c = (const Circle*)s;
return 3.141592653589793 * c->r * c->r;
}
static void Circle_destroy(Shape *s) {
free(s);
}
/* vtable */
static const ShapeVTable circle_vtable = {
.area = Circle_area,
.destroy = Circle_destroy
};
/* 构造函数 */
Circle* Circle_new(double r, const char *name) {
Circle *c = malloc(sizeof(Circle));
if (!c) return NULL;
c->base.vtable = &circle_vtable;
strncpy(c->base.name, name ? name : "Circle", sizeof(c->base.name)-1);
c->base.name[sizeof(c->base.name)-1] = '\0';
c->r = r;
return c;
}
4.6 Rect.h(矩形子类定义)
// Rect.h
#ifndef RECT_H
#define RECT_H
#include "Shape.h"
typedef struct {
Shape base;
double w, h;
} Rect;
Rect* Rect_new(double w, double h, const char *name);
#endif
4.7 Rect.c(矩形子类实现)
// Rect.c
#include "Rect.h"
#include <stdlib.h>
#include <string.h>
static double Rect_area(const Shape *s) {
const Rect *r = (const Rect*)s;
return r->w * r->h;
}
static void Rect_destroy(Shape *s) {
free(s);
}
static const ShapeVTable rect_vtable = {
.area = Rect_area,
.destroy = Rect_destroy
};
Rect* Rect_new(double w, double h, const char *name) {
Rect *r = malloc(sizeof(Rect));
if (!r) return NULL;
r->base.vtable = &rect_vtable;
strncpy(r->base.name, name ? name : "Rect", sizeof(r->base.name)-1);
r->base.name[sizeof(r->base.name)-1] = '\0';
r->w = w;
r->h = h;
return r;
}
4.7 main.c(多态调用示例)
// main.c
#include <stdio.h>
#include "Circle.h"
#include "Rect.h"
int main(void) {
Shape *arr[4];
arr[0] = (Shape*)Circle_new(1.0, "unit circle");
arr[1] = (Shape*)Rect_new(3.0, 4.0, "rect 3x4");
arr[2] = (Shape*)Circle_new(2.0, "circle r2");
arr[3] = (Shape*)Rect_new(2.5, 1.5, "rect 2.5x1.5");
for (int i = 0; i < 4; ++i) {
printf("%s area = %.6f\n",
arr[i]->name,
Shape_area(arr[i])); // 多态调用
}
/* 多态销毁 */
for (int i = 0; i < 4; ++i) {
Shape_destroy(arr[i]);
}
return 0;
}
五、继承
5.1 核心思想
把父类结构体作为子类结构体的第一个字段(布局兼容),如此:子类对象的开头就是一个完整的父类对象,父类指针可以指向子类(向上转型 upcast),虚表指针等机制可继续模拟多态,这正是 C++ 在 ABI 层面的继承方式。实现方法同上。
1292

被折叠的 条评论
为什么被折叠?



