C语言实现面向对象

一、面向对象概念

理解面向对象编程(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 层面的继承方式。实现方法同上。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值