C 面向对象编程

封装

c 语言使用的 struct 没有访问控制,任何程序都能访问 struct 的成员。为了隐藏 struct 里的成员名称和位置,可以将具体的 struct 定义放在 .c 文件中,而在头文件中增加一个指向该结构体的指针。因为无法得知指针类型的具体定义,对这个私有结构体成员的访问只能通过提供的 set() 和 get() 函数。

下面是头文件 test.h 的定义:

#ifndef __TEST_H__
#define __TEST_H__

struct test {
    struct _test* t;
};

int test_constructor(struct test*);
int test_get(struct test*);
void test_set(struct test*, int);
void test_destructor(struct test*);

#endif

对外提供的结构体信息只有一个指向 struct _test 的指针,由于 struct _test 在 .c 文件中定义,其它程序无法知道它的具体定义。下面是实现具体功能的 test.c:

#include <stdlib.h>
#include "test.h"

struct _test {
    int v;
};

int test_constructor(struct test* t)
{
    t->t = calloc(1, sizeof(struct _test));
    if (!t->t)
        return -1;

    return 0;
}

int test_get(struct test* t)
{
    return t->t->v;
}

void test_set(struct test* t, int v)
{
    t->t->v = v;
}

void test_destructor(struct test* t)
{
    free(t->t);
}

像 c++ 和 java 这些提供访问权限控制的语言直接把私有变量的定义暴露在头文件中也没关系,因为其它程序不能直接访问这些私有变量。而在 c 中就只能通过隐藏具体细节来实现这样的功能了,但是如果知道 struct _test 的结构定义细节,那么完全可以绕过所提供的 set() 和 get() 函数。

继承

c 中没有像 c++ 那样的继承功能,如果一个类要继承另一个类,一个办法是将另一个类作为它的一个成员,也就是 c++ 中所说的组合:

struct base1 {
    int i1;
};

struct base2 {
    double d1, d2;
};

struct derive {
    int mine;
    struct base1 b1;
    struct base2 b2;
};

多态

c 中实现多态的功能利用了结构体的内存布局,即结构体内每个域的内存位置是按照定义的顺序从低地址往高地址顺序存放的(可能会有字节对齐的情况,但不影响对每个域的访问)。在 Linux 内核中用到了这个特性,下面两个宏的使用在代码里随处可见:

#define offset_of(type, member) \
    ((unsigned long)(&(((type*)0)->member)))

#define container_of(ptr, type, member) \
    ((type*)((unsigned long)(ptr) - offset_of(type, member)))

宏 offset_of() 的作用是返回某个成员 member 在结构体 type 中的偏移量。先看后面括号的部分:((type)0)->member。这部分的意思是把位置 0 转换成一个指向 type 的指针,然后访问成员 member 的值。如果直接执行这句话肯定会段异常,因为位置 0 是 NULL 指针。但是在前面加了取地址符号:&(((type)0)->member),这样就变成了“取位于地址 0 的结构体 type 的成员 member 的地址”。这里只是取 member 的地址,而不是访问 member 的内容,因此并不会触发访问异常。把结构体的起始位置设为 0,得到的地址就是 member 在结构体内的偏移量:

+---------+------------+-----------+
|   ...   |   member   |    ...    |
+---------+------------+-----------+
^         ^
|         |
0         offset_of(type, member)

最后把地址值转换成 unsigned long 就是 member 的相对偏移量(c 中规定指针的值就是一个 unsigned long 类型的整数,sizeof(void*) == sizeof(unsigned long))。

这样第二个宏 container_of() 的作用就好理解了:取得当前指向 member 的指针 ptr,减去 member 的相对偏移量,得到的就是结构体 type 的起始位置:

container_of(ptr, type, member)
|
|        ptr
|         |
v         v
+---------+------------+-----------+
|   ...   |   member   |    ...    |
+---------+------------+-----------+
 \---v---/    
     |
     offset_of(type, member)

了解了结构体的布局之后来看一个例子:三角形,圆形,正方形等都可以看成是一种形状,它们都有一个名字属性 name,还有一个操作 draw()。除此之外它们还有各自的特殊属性:三角形有三条边,圆形有半径,正方形有边长。我们先定义一个基类 shape 和对应的虚函数表 shape_operations:

struct shape {
    const char* name;
    struct shape_operations* sops;
};

struct shape_operations {
    void (*draw)(struct shape*);
};

然后派生出一个三角形的类:

struct triangle {
    int a, b, c;
    struct shape base;
};

static void draw_triangle(const struct shape* s)
{
    struct triangle* this = container_of(s, struct triangle, base);
    printf("draw a %s: %d\t%d\t%d\n", s->name, this->a, this->b, this->c);
}

static struct shape_operations triangle_operations = {
    .draw = draw_triangle,
};

void triangle_constructor(struct triangle* t, int a, int b, int c)
{
    t->a = (a > 0) ? a : 1;
    t->b = (b > 0) ? b : 1;
    t->c = (c > 0) ? c : 1;
    t->base.name = "triangle";
    t->base.sops = &triangle_operations;
}

其中把派生类实现的 draw_triangle() 的地址赋值给虚函数表对应的入口 ->draw(),在 draw_triangle() 中使用 container_of() 通过指向基类的指针 s 获得指向实际的 struct triangle 结构体的指针 this。类似地还有圆形:

struct circle {
    int r;
    struct shape base;
};

static void draw_circle(const struct shape* s)
{
    struct circle* this = container_of(s, struct circle, base);
    printf("draw a %s: r = %d\n", s->name, this->r);
}

static struct shape_operations circle_operations = {
    .draw = draw_circle,
};

void circle_constructor(struct circle* c, int r)
{
    c->r = (r > 0) ? r : 1;
    c->base.name = "circle";
    c->base.sops = &circle_operations;
}

和正方形:

struct square {
    int e;
    struct shape base;
};

static void draw_square(const struct shape* s)
{
    struct square* this = container_of(s, struct square, base);
    printf("draw a %s: e = %d\n", s->name, this->e);
}

static struct shape_operations square_operations = {
    .draw = draw_square,
};

void square_constructor(struct square* s, int e)
{
    s->e = (e > 0) ? e : 1;
    s->base.name = "square";
    s->base.sops = &square_operations;
}

最后是测试:

void draw_shape(const struct shape* s)
{
    s->sops->draw(s);
}

int main(void)
{
    struct triangle t;
    struct circle c;
    struct square s;

    triangle_constructor(&t, 5, 5, 5);
    draw_shape(&t.base);
    circle_constructor(&c, 5);
    draw_shape(&c.base);
    square_constructor(&s, 3);
    draw_shape(&s.base);

    return 0;
}

除了实现单一继承外还可以实现多重继承,基类成员可以放在结构体中的任何位置。还有就是程序中没出现 void*,看起来算是比较顺眼。

另外有一本叫 《Object-Oriented Programming With ANSI-C》 的小册子,有兴趣的可以看一下。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注