C语言实现多态相关话题

结合 System V IPC 内核结构图 ,我们来详细拆解这个问题:C 语言没有 class、没有 virtual 关键字,是如何通过「结构体嵌套 + 指针强转」实现面向对象中的「继承」与「多态」的?

这不仅是一个语法技巧,更是 Linux 内核代码中广泛使用的核心设计思想,System V IPC(消息队列、共享内存、信号量)的统一管理架构,正是建立在这个基础之上的。


一、先搞懂面向对象的核心概念(用 C 语言视角翻译)

在讲 C 语言实现之前,我们先把 C++ 面向对象的核心概念,"翻译" 成 C 语言能理解的逻辑:

C++ 面向对象概念 对应的 C 语言实现思路
封装 struct 把数据(成员变量)和操作数据的函数指针(成员函数)封装在一起。
继承 利用 C 语言结构体的内存布局规则 :如果 子类结构体第一个成员父类结构体,那么它们的首地址完全相同
多态(动态绑定) 父类结构体 中定义函数指针子类 在初始化时,把自己的专属实现函数地址赋值给父类的函数指针。这样用 "父类指针" 调用函数时,实际执行的是 "子类" 的代码。

二、基石:C 语言结构体的内存布局与首地址规则

这是实现 "继承" 的核心前提,必须先理解透。

2.1 核心规则

如果一个结构体的第一个成员是另一个结构体类型,那么外层结构体的首地址,与内层第一个成员的首地址,在数值上是完全相等的。

我们用一个简单的代码验证:

cpp 复制代码
#include <stdio.h>

// 模拟"父类"
struct Base {
    int base_data;
};

// 模拟"子类":第一个成员必须是父类结构体
struct Derived {
    struct Base parent; // 【关键】第一个成员是父类
    int derived_data;
};

int main() {
    struct Derived d;
    printf("Derived 结构体的首地址:   %p\n", (void*)&d);
    printf("父类成员 parent 的首地址: %p\n", (void*)&d.parent);
    
    // 【核心强转】因为首地址相同,所以可以安全强转
    struct Base *base_ptr = (struct Base*)&d; 
    printf("强转后的 Base 指针:       %p\n", (void*)base_ptr);

    return 0;
}

输出结果

bash 复制代码
Derived 结构体的首地址:   0x7ffee3b5c8a0
父类成员 parent 的首地址: 0x7ffee3b5c8a0
强转后的 Base 指针:       0x7ffee3b5c8a0

结论 :三个地址完全一样!这意味着:我们可以把一个 Derived* 指针,安全地强制转换成 Base* 指针,就像 C++ 里的 "向上转型" 一样。


三、第一步:用结构体嵌套实现 "继承"

现在我们结合 System V IPC 内核图 来讲。

3.1 内核中的 "父类":kern_ipc_perm

在那张图里,所有 IPC 资源的最顶端,都有一个公共的头部 kern_ipc_perm。这就是内核定义的 "基类",它存储了所有 IPC 资源都需要的公共属性:

复制代码
// 简化版的内核 kern_ipc_perm 结构(模拟)
struct kern_ipc_perm {
    key_t          key;      // IPC 资源的唯一标识(ftok生成)
    uid_t          uid;      // 所有者用户ID
    gid_t          gid;      // 所有者组ID
    unsigned short mode;     // 访问权限(类似文件权限 0666)
    unsigned long  seq;      // 序列号,用于防止ID复用
    // ... 其他公共字段
};

3.2 内核中的 "子类":msg_queuesem_arrayshmid_kernel

接下来看三个具体的 IPC 资源,它们完美遵守了 "第一个成员是父类" 的规则:

1. 消息队列(子类 1)
cpp 复制代码
// 简化版的消息队列结构
struct msg_queue {
    struct kern_ipc_perm q_perm; // 【关键】第一个成员是父类 kern_ipc_perm
    
    // 下面是消息队列特有的成员
    time_t q_stime;         // 最后发送消息时间
    time_t q_rtime;         // 最后接收消息时间
    unsigned long q_cbytes;  // 当前队列中的字节数
    unsigned long q_qnum;    // 当前队列中的消息数
    // ... 其他消息队列特有字段
};
2. 信号量集(子类 2)
cpp 复制代码
// 简化版的信号量集结构
struct sem_array {
    struct kern_ipc_perm sem_perm; // 【关键】第一个成员是父类 kern_ipc_perm
    
    // 下面是信号量集特有的成员
    struct sem *sem_base;    // 指向信号量数组的指针
    int sem_nsems;           // 信号量的个数
    time_t sem_otime;        // 最后操作时间
    // ... 其他信号量特有字段
};
3. 共享内存(子类 3)
cpp 复制代码
// 简化版的共享内存结构
struct shmid_kernel {
    struct kern_ipc_perm shm_perm; // 【关键】第一个成员是父类 kern_ipc_perm
    
    // 下面是共享内存特有的成员
    struct file *shm_file;   // 关联的文件指针(用于内存映射)
    size_t shm_segsz;         // 共享内存大小
    int shm_nattch;           // 当前挂接的进程数
    // ... 其他共享内存特有字段
};

3.3 设计巧思:内核如何统一管理?

看图里最上面的 struct ipc_idsstruct ipc_id_ary。内核维护了三个全局变量:

  • msg_ids:管理所有消息队列;
  • sem_ids:管理所有信号量集;
  • shm_ids:管理所有共享内存段。

它们的核心是一个柔性数组 ,存的是什么?存的是 struct kern_ipc_perm* 指针!

cpp 复制代码
// 简化版的 ipc_ids 结构
struct ipc_id_ary {
    int size;
    struct kern_ipc_perm *p[0]; // 【核心】存的都是父类指针!
};

struct ipc_ids {
    int in_use;
    struct ipc_id_ary *entries; // 指向上面的数组
    // ...
};

这就是继承的威力 :内核不需要维护三个独立的数组(一个存msg_queue*,一个存sem_array*,一个存shmid_kernel*),只需要维护一个 kern_ipc_perm* 数组。

  • 当创建一个消息队列时,内核把 msg_queue* 强转成 kern_ipc_perm* 存进去;
  • 当创建一个信号量集时,内核把 sem_array* 强转成 kern_ipc_perm* 存进去;
  • 当需要做权限校验查找 ID公共操作 时,内核直接用 kern_ipc_perm* 指针操作,完全不需要关心它具体是消息队列、信号量还是共享内存。

四、第二步:加上函数指针,实现 "多态(动态绑定)"

光有 "继承"(统一管理数据)还不够,真正的多态需要 "用父类指针调用,能执行到子类的专属代码 "。这就需要函数指针登场了。

4.1 原理

我们在 "父类" 结构体里,定义一些函数指针变量

  • 每个 "子类" 在初始化自己的对象时,把自己专属的实现函数的地址,赋值给父类的函数指针;
  • 当我们用父类指针去调用这个函数指针时,实际运行的就是子类赋值进去的那个函数。

4.2 代码模拟:结合 IPC 场景

我们写一个简化版的代码,模拟内核是如何用这个技巧 "删除" 不同类型的 IPC 资源的。

1. 定义父类(包含函数指针)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 先声明父类
struct kern_ipc_perm;

// 定义函数指针类型:删除操作的函数指针
typedef void (*ipc_destroy_func)(struct kern_ipc_perm *);

// 父类结构体
struct kern_ipc_perm {
    int key;
    // 【核心多态】:函数指针,指向具体的删除函数
    ipc_destroy_func destroy; 
};
2. 定义子类,并实现专属函数
cpp 复制代码
// --- 子类1:共享内存 ---
struct shmid_kernel {
    struct kern_ipc_perm shm_perm; // 继承父类
    size_t size; // 特有属性
};

// 共享内存专属的删除函数
void shm_destroy(struct kern_ipc_perm *perm) {
    // 【关键】因为首地址相同,可以安全强转回子类指针
    struct shmid_kernel *shm = (struct shmid_kernel*)perm;
    
    printf("正在执行 [共享内存] 的专属删除逻辑...\n");
    printf("  -> 释放物理内存页: %zu bytes\n", shm->size);
    printf("  -> 清除页表映射...\n");
    free(shm); // 最后释放结构体本身
}

// --- 子类2:信号量集 ---
struct sem_array {
    struct kern_ipc_perm sem_perm; // 继承父类
    int nsems; // 特有属性
};

// 信号量集专属的删除函数
void sem_destroy(struct kern_ipc_perm *perm) {
    // 强转回子类指针
    struct sem_array *sem = (struct sem_array*)perm;
    
    printf("正在执行 [信号量集] 的专属删除逻辑...\n");
    printf("  -> 释放 %d 个信号量的 undo 结构...\n", sem->nsems);
    printf("  -> 唤醒等待队列上的进程...\n");
    free(sem);
}
3. 初始化子类,并绑定函数指针
cpp 复制代码
// 创建并初始化一个共享内存对象
struct kern_ipc_perm* create_shm() {
    struct shmid_kernel *shm = malloc(sizeof(struct shmid_kernel));
    shm->shm_perm.key = 0x1234;
    shm->size = 4096;
    
    // 【多态核心】绑定自己的专属删除函数
    shm->shm_perm.destroy = shm_destroy; 
    
    // 返回父类指针
    return (struct kern_ipc_perm*)shm;
}

// 创建并初始化一个信号量集对象
struct kern_ipc_perm* create_sem() {
    struct sem_array *sem = malloc(sizeof(struct sem_array));
    sem->sem_perm.key = 0x5678;
    sem->nsems = 2;
    
    // 【多态核心】绑定自己的专属删除函数
    sem->sem_perm.destroy = sem_destroy;
    
    // 返回父类指针
    return (struct kern_ipc_perm*)sem;
}
4. 见证多态的时刻:用父类指针统一调用
cpp 复制代码
int main() {
    // 创建两个不同类型的对象,但都用父类指针接收
    struct kern_ipc_perm *ipc1 = create_shm();
    struct kern_ipc_perm *ipc2 = create_sem();

    // 【多态发生】
    // 同样的代码,同样是调用 perm->destroy()
    // 但一个执行的是 shm_destroy,一个执行的是 sem_destroy
    printf("--- 删除第一个 IPC 资源 ---\n");
    ipc1->destroy(ipc1); 

    printf("\n--- 删除第二个 IPC 资源 ---\n");
    ipc2->destroy(ipc2);

    return 0;
}

运行结果

复制代码
--- 删除第一个 IPC 资源 ---
正在执行 [共享内存] 的专属删除逻辑...
  -> 释放物理内存页: 4096 bytes
  -> 清除页表映射...

--- 删除第二个 IPC 资源 ---
正在执行 [信号量集] 的专属删除逻辑...
  -> 释放 2 个信号量的 undo 结构...
  -> 唤醒等待队列上的进程...

我们在 main 函数里,根本不知道也不关心 ipc1ipc2 具体是什么类型,只知道它们是 kern_ipc_perm*。调用同一个 destroy 函数指针,却自动执行了不同的子类逻辑。这就是 C 语言实现的多态

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// ==========================================
// 第一部分:前置声明与类型定义
// ==========================================

// 【前置声明】
// 告诉编译器:"后面我会定义一个叫 struct kern_ipc_perm 的结构体"
// 因为下面的函数指针参数要用到它,必须先声明一下
struct kern_ipc_perm;

// 【1. 定义函数指针类型】(复习重点)
// 语法:typedef 返回值类型 (*新类型名)(参数列表);
// 这里定义了一个叫 ipc_destroy_func 的新类型
// 它代表:"所有返回值是 void,参数是 struct kern_ipc_perm* 指针的函数"
typedef void (*ipc_destroy_func)(struct kern_ipc_perm *);

// 【2. 定义"父类"结构体】
// 这是所有 IPC 资源的公共头部(类比 C++ 里的 Base 类)
struct kern_ipc_perm {
    int key; // 公共数据:IPC 资源的唯一标识(比如钥匙)
    
    // 【核心多态】
    // 在父类里放一个【函数指针变量】
    // 这个变量将来会存储"子类"专属函数的地址
    ipc_destroy_func destroy; 
};

// ==========================================
// 第二部分:定义"子类"与它们的专属函数
// ==========================================

// --- 子类 1:共享内存 (Shared Memory) ---
// 【3. 结构体嵌套实现"继承"】
// 注意:子类的【第一个成员】必须是父类结构体!
// 这保证了:&shm_obj == &shm_obj.shm_perm (首地址完全相同)
struct shmid_kernel {
    struct kern_ipc_perm shm_perm; // 第一个成员是父类
    size_t size; // 子类特有数据:共享内存的大小
};

// 【4. 实现子类的专属函数】
// 这是共享内存特有的删除逻辑
void shm_destroy(struct kern_ipc_perm *perm) {
    // 【5. 向下转型(强转)】
    // 虽然参数是父类指针,但我们心里知道:
    // 这个指针其实指向的是一个完整的 shmid_kernel(子类)对象
    // 因为首地址相同,我们可以安全地把它强转回子类指针
    struct shmid_kernel *shm_ptr = (struct shmid_kernel*)perm;
    
    // 现在我们可以访问子类特有的数据了!
    printf("正在执行 [共享内存] 的专属删除逻辑...\n");
    printf("  -> 步骤1: 释放 %zu 字节的物理内存页\n", shm_ptr->size);
    printf("  -> 步骤2: 清除进程页表中的映射关系\n");
    printf("  -> 步骤3: 释放结构体本身的内存\n");
    
    free(shm_ptr); // 最后释放整个对象
}

// --- 子类 2:信号量集 (Semaphore Array) ---
// 逻辑和上面完全一样,只是特有数据和业务逻辑不同
struct sem_array {
    struct kern_ipc_perm sem_perm; // 第一个成员是父类
    int nsems; // 子类特有数据:信号量的个数
};

// 信号量集专属的删除函数
void sem_destroy(struct kern_ipc_perm *perm) {
    // 同样的强转逻辑
    struct sem_array *sem_ptr = (struct sem_array*)perm;
    
    printf("正在执行 [信号量集] 的专属删除逻辑...\n");
    printf("  -> 步骤1: 释放 %d 个信号量的 undo 撤销记录\n", sem_ptr->nsems);
    printf("  -> 步骤2: 唤醒等待队列上阻塞的进程\n");
    printf("  -> 步骤3: 释放结构体本身的内存\n");
    
    free(sem_ptr);
}

// ==========================================
// 第三部分:工厂函数(创建对象并绑定函数指针)
// ==========================================

// 创建一个"共享内存"对象
struct kern_ipc_perm* create_shm() {
    // 1. 分配内存:注意分配的是【子类】的大小
    struct shmid_kernel *shm_obj = malloc(sizeof(struct shmid_kernel));
    if (shm_obj == NULL) {
        perror("malloc failed");
        exit(1);
    }
    
    // 2. 初始化数据
    shm_obj->shm_perm.key = 0x1234; // 初始化公共数据
    shm_obj->size = 4096;            // 初始化子类特有数据(4KB)
    
    // 【6. 关键绑定】
    // 把【子类专属函数】的地址,赋值给【父类的函数指针成员】
    // 这就像把"子类的实现"挂到了"父类的钩子"上
    shm_obj->shm_perm.destroy = shm_destroy;
    
    // 3. 返回父类指针(向上转型)
    // 对外只暴露父类指针,隐藏具体是"共享内存"的细节
    return (struct kern_ipc_perm*)shm_obj;
}

// 创建一个"信号量集"对象(逻辑同上)
struct kern_ipc_perm* create_sem() {
    struct sem_array *sem_obj = malloc(sizeof(struct sem_array));
    if (sem_obj == NULL) {
        perror("malloc failed");
        exit(1);
    }
    
    sem_obj->sem_perm.key = 0x5678;
    sem_obj->nsems = 2; // 假设有2个信号量
    
    // 绑定信号量的专属函数
    sem_obj->sem_perm.destroy = sem_destroy;
    
    return (struct kern_ipc_perm*)sem_obj;
}

// ==========================================
// 第四部分:主函数(见证多态的发生)
// ==========================================

int main() {
    printf("=== C语言多态模拟演示 ===\n\n");

    // 1. 创建两个不同的"对象"
    // 注意:我们用的全是【父类指针】接收
    // 我们根本不关心 ipc1 具体是啥,只知道它是个 IPC 资源
    struct kern_ipc_perm *ipc_resource_1 = create_shm();
    struct kern_ipc_perm *ipc_resource_2 = create_sem();

    // 2. 【多态发生】
    // 同样的代码,同样的调用方式:ptr->destroy(ptr)
    // 但执行的是完全不同的逻辑!
    
    printf("--- 处理第一个 IPC 资源 ---\n");
    // 因为 ipc_resource_1 里的 destroy 存的是 shm_destroy 的地址
    // 所以这里会自动调用 shm_destroy
    ipc_resource_1->destroy(ipc_resource_1); 

    printf("\n--- 处理第二个 IPC 资源 ---\n");
    // 因为 ipc_resource_2 里的 destroy 存的是 sem_destroy 的地址
    // 所以这里会自动调用 sem_destroy
    ipc_resource_2->destroy(ipc_resource_2);

    printf("\n=== 演示结束 ===\n");
    return 0;
}

五、回到内核图:总结设计巧思

现在再看你那张图,一切都清晰了:

1. 数据层面的统一(继承)

  • 所有 IPC 资源结构体(msg_queue/sem_array/shmid_kernel)的第一个成员都是 kern_ipc_perm
  • 内核的 ipc_ids.entries 数组里,只存 kern_ipc_perm* 指针。
  • 好处:代码高度复用 。比如权限校验、ID 查找、序列号生成这些公共逻辑,只需要写一份代码,操作 kern_ipc_perm* 即可,不需要为三种 IPC 分别写三遍。

2. 操作层面的统一(多态)

虽然实际的 Linux 内核源码中,为了极致性能和复杂逻辑,并没有在 kern_ipc_perm 里直接塞大量函数指针(而是通过 ipc_namespace 等其他机制分发),但设计思想是完全一致的

在更复杂的驱动模型(如 Linux 设备驱动模型 kobjectfile_operations)中,这种 "结构体首成员嵌套 + 函数指针表" 的模式被用到了极致。


六、总结:C 语言实现多态的三要素

  1. 结构体嵌套(模拟继承) :子类结构体的第一个成员必须是父类结构体。这保证了两者首地址相同,子类指针可以安全强转为父类指针。

  2. 指针强转(向上转型):对外只暴露父类指针,隐藏具体的子类细节,实现 "用一个类型管理所有子类"。

  3. 函数指针(动态绑定):在父类中定义函数指针。子类在初始化时,将自己的专属实现函数赋值给父类的函数指针。运行时通过父类指针调用函数指针,自动执行到对应的子类代码。

这就是 System V IPC 图里看到的,Linux 内核最经典的 "用 C 语言写面向对象代码" 的设计哲学。

相关推荐
qwehjk20082 小时前
实时语音处理库
开发语言·c++·算法
2301_804215412 小时前
自定义异常类设计
开发语言·c++·算法
暮冬-  Gentle°2 小时前
C++代码依赖分析
开发语言·c++·算法
糯诺诺米团2 小时前
C++多线程打包成so给JAVA后端(Ubuntu)<3>
java·开发语言·c++
2301_763891952 小时前
泛型编程与STL设计思想
开发语言·c++·算法
j_xxx404_2 小时前
蓝桥杯基础--进制转换
开发语言·数据结构·c++·算法·职场和发展·蓝桥杯
xjdkxnhcoskxbco2 小时前
Kotlin Lambda 变量捕获
android·开发语言
沐知全栈开发2 小时前
ASP TextStream
开发语言