谁说C语言不能面向对象(之二:封装)

本文详细探讨了如何在C语言中实现面向对象编程,通过结构体和函数指针模拟类和对象,展示了构造函数、封装及方法调用的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      从这节开始,我们就要正式开始用C语言实现面向对象了。不过,受限于C语言的语法,实现OO还是需要很多编程技巧的。在此之前,我想先介绍一种可以算得上是捷径的方法吧。

    其实用C语言来实现OO,我们并不是第一个。说起来,这也算挺成熟的技术了,成熟到都已经过时了。有一个很著名的程序语言,就是利用C语言,来实现OO的。这就是大名鼎鼎的Objective-C,苹果公司曾经的御用开发语言,直到Swift诞生这么些年,OC的风骚才逐渐开始衰退。OC其实本质上就是C,所以,我们在体验用C来进行OO的时候,OC可以称得上是一个捷径。

    编译Objective-C的编译器首先需要将OC转化成C源码,然后再使用C的编译器进行编译。有一种方法可以取出这种中间件,下面来介绍这种方法:

    首先,我们需要有一个可以通过编译的.m文件,例如说:

#import <Foundation/NSObject.h>
#import <stdio.h>

@interface Test : NSObject {
    int _mem;
}
@property int mem;
- (void)func;
+ (void)class_func:(int)arg;
@end

@implementation Test
@synthesize mem = _mem;
- (void)func {
    printf("%d\n", self.mem);
}
+ (void)class_func:(int)arg {
    printf("%d\n", arg);
}
@end

int main(int argc, const char * argv[]) {
    Test *test = [[Test alloc] init];
    test.mem = 5;
    [test func];
    [Test class_func:3];
    return 0;
}

    我们叫它test.m,然后,利用gcc可以将其转化为C代码。在控制台输入以下命令,可以将其转化为C代码:

gcc -rewrite-objc test.m

    之后,我们会得到test.cpp。注意,这里虽然是cpp结尾的,但是中间是纯C的代码,不含C++代码。由于这个文件中含有头文件内容以及动态链接库的很多代码,大概有2000多行,所以这里我仅仅把主要的一小部分贴出来,细节如果大家感兴趣可以自己看。 

// @implementation Test
// @synthesize mem = _mem;
static int _I_Test_mem(Test * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Test$_mem)); }
static void _I_Test_setMem_(Test * self, SEL _cmd, int mem) { (*(int *)((char *)self + OBJC_IVAR_$_Test$_mem)) = mem; }


static void _I_Test_func(Test * self, SEL _cmd) {
    printf("%d\n", ((int (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("mem")));
}

static void _C_Test_class_func_(Class self, SEL _cmd, int arg) {
    printf("%d\n", arg);
}
// @end

int main(int argc, const char * argv[]) {
    Test *test = ((Test *(*)(id, SEL))(void *)objc_msgSend)((id)((Test *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Test"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)test, sel_registerName("setMem:"), 5);
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("func"));
    ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)objc_getClass("Test"), sel_registerName("class_func:"), 3);
    return 0;
}

    大致上来说,我们的类成员属性、方法都被转化为了函数。这也进一步说明,用C来实现OO是完全可行的,因为OO只是一种编程思维,而并非语言的特性。

    详细的OC源码我就不介绍了,接下来我们摆脱OC,自己来用C实现一套OO机制。首先需要分析的是,要想实现OO,我们应当实现哪些功能?OO和PO(面向过程)相比,其最突出的特性莫过于封装、继承和多态。封装,就是将“属性”和“动作”结合起来,共同实现在一个“类”中。而继承和多态则相对来说复杂一些,并且,多态基于继承,而继承又基于封装。所以,我们需要先实现封装。

    既然要实现封装,我们就需要一种结构,能够即存储属性,又能存储动作。存储属性的话,可以使用C语言的结构体,存储动作的话可以使用函数。但是问题就在于,表示“动作”的这个函数,应当是对象专有的。举例而言,在A类中有一个方法fa,那么只有A的对象(或是A子类的对象)才可以调用fa方法,而其他无关的对象无法调用。因此,我们就需要采用一种方法,来限定这个fa只能是A类型的变量才能调用,最简单的方法,就是在函数的第一个参数上传一个固定A类型的参数,这样,一方面,不是这种类型的参数无法传入该函数,也就间接做到的对象专有,另一方面,我们还可以在函数内部通过第一个参数来访问对象的属性。

    为了方便说明,以后举例子之前,我会先采用和C语言语法相似,但支持OO的C++代码先表示一下,然后再用C代码来重写。假如我用一个类来表示二维坐标中的点,那么用C++代码应当是这样的:

class Point {
public:
    // 属性
    double x, y;
    // 动作
    void move(Point offset) { // 平移
        x += offset.x;
        y += offset.y;
    }
    double distanceFromOrigin() { // 到原点的距离
        return sqrt(x * x + y * y);
    }
};

    这里需要解释一下为什么我用public,因为暂时,我们还没办法在C中控制变量的作用域,因此,我们暂且都按照公有属性。我们定义了一个Point类,它的属性有x和y,方法有两个,一个是平移,一个是计算到原点距离。分析一下,平移这个操作其实是要改变对象的属性值,而计算距离对属性是只读的。

    接下来,就按照我们之前的方法,将这段代码转化为C语言,用结构体表示对象属性,用第一个参数是对象本身的函数表示对象动作,示例如下:

// class Point
struct point_property {
    double x, y;
};
void point_move(struct point_property *self, struct point_property *offset) {
    self->x += offset->x;
    self->y += offset->y;
}
double point_distanceFromOrigin(struct point_property *self) {
    return sqrt(self->x * self->x + self->y * self->y);
}
// end class Point

    就像这样,函数的第一个参数传入的是这个对象的属性,像是move这个函数中,传入了另一个对象,其实相当于传入了另一个对象的属性,所以,我们仍然传入struct point_property即可。

    另外一个问题就是,为什么传入指针而不是结构体本身。这是因为,在OO中,传入对象本身会得到一个对象的拷贝,所以应当实现拷贝构造函数的,这个在后面研究,所以offset传入了指针。而至于为什么self要传指针,原因很简单,如果你需要修改对象的属性,那你肯定不能用值传递,而要用引用传递,传指针也就理所应当了。

    那么我们试着调用一下吧,还是先展示C++代码:

int main(int argc, const char * argv[]) {
    Point p;
    p.x = 3;
    p.y = 4;
    printf("%lf\n", p.distanceFromOrigin());
    Point off;
    off.x = 1;
    off.y = 2;
    p.move(off);
    printf("(%lf, %lf)\n", p.x, p.y);
    return 0;
}

    输出结果请自行验证。用C来实现的话,定义对象的时候,我们就定义对象属性的结构体变量,给对象成员赋值时,我们就给结构体变量赋值,调用方法时我们就调用对应的函数,并且把结构体变量传入函数,示例如下:

int main(int argc, const char * argv[]) {
    struct point_property p;
    p.x = 3;
    p.y = 4;
    printf("%lf\n", point_distanceFromOrigin(&p));
    struct point_property off;
    off.x = 1;
    off.y = 2;
    point_move(&p, &off);
    printf("(%lf, %lf)\n", p.x, p.y);
    return 0;
}

    这样确实是实现了一个初始的OO,不过离真正的OO还差的很远。我们知道,对象中的成员,大多数应该是私有的,也就是说,对外无感知。在类之外的地方,是不能够对对象的私有变量进行直接的调用或更改的。成员变量应当让类内部来管理,而外部能调用的,就只有接口而已。接口中有一个很重要的,就是构造函数。也就是说,当我们初始化一个对象的时候,我们应当调用构造函数来初始化,而并不是像现在这样通过手动给属性赋值。所以,我们的代码需要进行再次的封装,真正能够做到,在类之外操作对象,只需要操作接口。

    那么我们就需要实现构造函数,然后将成员变量进行隐藏,C++代码如下:

class Point {
private:
    double x, y;
public:
    Point(double x, double y): x(x), y(y) {
        printf("a point (%lf, %lf) has been created\n", x, y);
    }
    void move(Point offset) { // 平移
        x += offset.x;
        y += offset.y;
    }
    double distanceFromOrigin() { // 到原点的距离
        return sqrt(x * x + y * y);
    }
    void show() { // 打印
        printf("(%lf, %lf)\n", x, y);
    }
};

int main(int argc, const char * argv[]) {
    Point p(3, 4);
    printf("%lf\n", p.distanceFromOrigin());
    Point off(1, 2);
    p.move(off);
    p.show();
    return 0;
}

    这里由于将x和y设为私有变量了,因此,为了打印方便,又定义了一个show方法用于打印点的坐标。现在的问题就在于,如何用C语言来实现构造函数?这我们得了解构造函数的作用。构造函数就是用于构造一个对象,然后初始化对象中的成员,之后再完成一些其他的操作。从C++的构造函数的语法就能看出,参数传参,之后的初始化列表进行成员初始化,然后函数体做其他的操作。既然是要构造一个对象,那么使用C语言的时候,返回值就一定是一个对象属性的结构体。示例如下:

// class Point
struct point_property {
    double x, y;
};
struct point_property point_construct(double x, double y) {
    struct point_property res; // 创建对象
    res.x = x;
    res.y = y; // 初始化成员
    printf("a point (%lf, %lf) has been created\n", x, y); // 其他操作
    return res;
}
void point_move(struct point_property *self, struct point_property *offset) {
    self->x += offset->x;
    self->y += offset->y;
}
double point_distanceFromOrigin(struct point_property *self) {
    return sqrt(self->x * self->x + self->y * self->y);
}
void point_show(struct point_property *self) {
    printf("(%lf, %lf)\n", self->x, self->y);
}
// end class Point

int main(int argc, const char * argv[]) {
    struct point_property p = point_construct(3, 4);
    printf("%lf\n", point_distanceFromOrigin(&p));
    struct point_property off = point_construct(1, 2);
    point_move(&p, &off);
    point_show(&p);
    return 0;
}

    这样看起来倒是没什么问题了,功能确实都实现了,但是,这样的代码看起来的确不让人很舒服,感觉上,没有C++那种整体感,那种强烈的封装感,并且,point类型的方法用point_这样的前缀来命名,其实是软约束,有没有办法让这些函数真的可以存在在对象里,建立一个对象与函数的硬约束呢?当然有,我们需要对我们的代码进行整改,用一个函数列表来存储所有与point相关的方法。于此同时处理一些C的语法,利用点编程技巧让我们的代码看起来更舒服一些。在此之前,我们先将代码的声明和实现进行拆分,提供头问题,C++的示例如下:

// Point.hpp
#ifndef Point_hpp
#define Point_hpp

class Point {
private:
    double x, y;
public:
    Point(double x, double y);
    void move(Point offset);
    double distanceFromOrigin();
    void show();
};

#endif /* Point_hpp */
// Point.cpp
#include "Point.hpp"

Point::Point(double x, double y): x(x), y(y) {
    printf("a point (%lf, %lf) has been created\n", x, y);
}
void Point::move(Point offset) { // 平移
    x += offset.x;
    y += offset.y;
}
double Point::distanceFromOrigin() { // 到原点的距离
    return sqrt(x * x + y * y);
}
void Point::show() { // 打印
    printf("(%lf, %lf)\n", x, y);
}

    改良后的C语言代码如下:

// Point.h
#ifndef Point_h
#define Point_h

// class Point
struct point;
// 属性定义
struct point_property {
    double x, y;
};
// 方法声明
struct point_property point_construct(double, double); // 构造函数

void point_move(struct point *, struct point *);
double point_distanceFromOrigin(struct point *);
void point_show(struct point *);

//类本体
typedef struct point {
// 属性
    struct point_property property;
// 方法列表
    void (*move)(struct point *self, struct point *offset);
    double (*distanceFromOrigin)(struct point *self);
    void (*show)(struct point *self);
} Point;
Point point(double x, double y); // 初始化函数
// end class Point

#endif /* Point_h */
// Point.c
#include "Point.h"
#include <stdio.h>
#include <math.h>

struct point_property point_construct(double x, double y) {
    struct point_property res; // 创建对象
    res.x = x;
    res.y = y; // 初始化成员
    printf("a point (%lf, %lf) has been created\n", x, y); // 其他操作
    return res;
}
void point_move(struct point *self, struct point *offset) {
    self->property.x += offset->property.x;
    self->property.y += offset->property.y;
}
double point_distanceFromOrigin(struct point *self) {
    return sqrt(self->property.x * self->property.x + self->property.y * self->property.y);
}
void point_show(struct point *self) {
    printf("(%lf, %lf)\n", self->property.x, self->property.y);
}

Point point(double x, double y) {
    Point res;
    res.property = point_construct(x, y);
    res.move = point_move;
    res.distanceFromOrigin = point_distanceFromOrigin;
    res.show = point_show;
    return res;
}

    调用测试:

#include "Point.h"

int main(int argc, const char * argv[]) {
    Point p = point(3, 4);
    printf("%lf\n", p.distanceFromOrigin(&p));
    Point off = point(1, 2);
    off.move(&p, &off);
    p.show(&p);
    return 0;
}

    之所以这样写,是为了让函数本身能够进到对象里,这样,我们不用再根据类名去找全局函数,而是在对象中就拥有函数指针,可以直接调用。但是这样还是存在一个问题,那就是,虽然对象里封装了函数,但是,仅仅是全局函数的指针的而已,不同对象间还是可以相互调用,例如上面代码中,p.show(&p)和off.show(&p)其实是一样的,真正决定对象的并不是前面函数指针的拥有者,而是实际函数传参。所以这样的设计,就显得有点臃肿,因为每一个对象都需要携带所有的函数指针,不划算,我们其实只需要一份函数列表就可以了。因此,需要换一种设计思路,把函数列表存到一个全局变量中,然后在每个对象中只需要拥有这个全局变量就可以了。示例代码如下:

// Point.h
#ifndef Point_h
#define Point_h

// class Point
struct point;
// 属性定义
struct point_property {
    double x, y;
};
// 方法声明
struct point_property point_construct(double, double); // 构造函数

void point_move(struct point *, struct point *);
double point_distanceFromOrigin(struct point *);
void point_show(struct point *);

struct point_method_t {
    // 方法列表
    void (*move)(struct point *self, struct point *offset);
    double (*distanceFromOrigin)(struct point *self);
    void (*show)(struct point *self);
}; 
//类本体
typedef struct point {
// 属性
    struct point_property property;
    struct point_method_t *method;
} Point;
Point point(double x, double y); // 初始化函数
// end class Point

#endif /* Point_h */
//Point.c
#include "Point.h"
#include <stdio.h>
#include <math.h>

struct point_property point_construct(double x, double y) {
    struct point_property res; // 创建对象
    res.x = x;
    res.y = y; // 初始化成员
    printf("a point (%lf, %lf) has been created\n", x, y); // 其他操作
    return res;
}
void point_move(struct point *self, struct point *offset) {
    self->property.x += offset->property.x;
    self->property.y += offset->property.y;
}
double point_distanceFromOrigin(struct point *self) {
    return sqrt(self->property.x * self->property.x + self->property.y * self->property.y);
}
void point_show(struct point *self) {
    printf("(%lf, %lf)\n", self->property.x, self->property.y);
}

struct point_method_t point_method = { // 全局的方法列表
    .move = point_move,
    .distanceFromOrigin = point_distanceFromOrigin,
    .show = point_show,
};

Point point(double x, double y) {
    Point res;
    res.property = point_construct(x, y);
    res.method = &point_method;
    return res;
}

    调用测试代码:

#include "Point.h"

int main(int argc, const char * argv[]) {
    Point p = point(3, 4);
    printf("%lf\n", p.method->distanceFromOrigin(&p));
    Point off = point(1, 2);
    off.method->move(&p, &off);
    p.method->show(&p);
    return 0;
}

    可能这样看上去,倒还不如刚才那种方式更简洁,至少从语法上来说是这样。但是这样做其一,复用了空间,所有的对象不再全都拥有完整的函数列表,而是仅仅只有一个指针指向那个全局的函数列表。其二就是,把函数单独管理,这样有利于我们后面的继承和多态的实现。

    关于封装的内容就先讲这么多,请大家继续关注后续连载。

【本文为逗比老师全权拥有,允许转载,但务必在开头标注作者信息和转载源连接,禁止恶意的复制与修改。】

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

borehole打洞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值