C语言陷阱与技巧28节,模拟“面向对象”编程,怎样定义私有成员

上一节讨论了结合指针和结构体语法,C语言也能实现“面向对象”编程。由此可以看出C语言是一门极其灵活的语言,简洁的语法即可实现复杂的程序。

C语言“对象”的成员变量

不过,在面向对象编程中,对象不仅仅有成员函数,也应该有成员变量。成员变量允许每一个对象都有独立存放数据的能力,各个对象的数据互不干扰。

int val = 0;
struct cfun{
    void (*modify)();
    void (*print)();
};
void modify()
{
    val ++;
}
void myprint()
{
    printf("val = %d\n", val);
}
struct cfun f1 = {modify, myprint};
struct cfun f2 = {modify, myprint};

f1.modify();
f1.print();
f2.print();

在上面这段C语言代码中,为了让“类”cfun 的各个成员函数都能访问变量 val,将 val 定义为全局变量了。但是 val 在内存中只有一份,所以就算是对象 f1 调用 modify() 修改了 val 的值,f2.print() 打印的 val 也会被修改。

当然有些设计期望的结果就是如此。

如果不希望出现这种结果,似乎只能定义两个 val,或者将 val 定义成数组:

int val1 = 0, val2 = 0;
//或者
int val[2] = {0};

然后再修改使用到这些全局变量的C语言代码。但是这么做至少有三个不好的地方:

  • C语言程序常常不能事先知道该“类”究竟会被实例化成多少个对象,若是对象比较多,超过了 val 的数目,就会导致程序崩溃。
  • 这种方式使用起来也不方便,程序员还需再设计出一套映射规则,用于说明不同对象与各个全局变量的对应关系。
  • 全局变量的作用域非常大,使用时必须小心的处理(这可能包括防止误调用,防止数据不同步等)。
可能有些读者看到这里,就会感叹“C语言果然不适合面向对象编程!”。

解决问题

其实要解决上述不足,只需要把变量加入“类”描述结构体就可以了,请看下面这段C语言代码:

struct cfun{
    void (*modify)();
    void (*print)();
    int val;
};
void modify(struct cfun *f)
{
    f->val ++;
}
void myprint(struct cfun *f)
{
    printf("val = %d\n", f->val);
}
struct cfun f1 = {modify, myprint, 0};
struct cfun f2 = {modify, myprint, 0};

f1.modify(&f1);
f1.print(&f1);  // 输出 val = 1
f2.print(&f2); // 输出 val = 0

将 val 加入结构体 cfun,之后每实例化一个对象,就会自动为该对象分配一个 val 变量,各个对象的 val 变量是彼此独立的,互不影响。所以,f1.print() 之后会输出 “val = 1”,而 f2.print() 之后会输出“val = 0”。

现在唯一有些不足的是,在调用成员函数时,需要将对象指针传递进去,这主要是因为C语言没有原生的“对象”语法。当然,也有办法省去这一过程,只需再设计一套额外的处理机制就可以了(这一点以后有机会再说)。

不过,再设计一套额外的处理机制,显然会消耗额外的资源(如内存、cpu等),这与C语言程序的“使用最小的资源,最高效率的办事”精神相违背。而且,省去传递结构体指针的操作,并不会为C语言程序带来质的改变,所以一般不会实现这套机制。

事实上,Linux 内核源码中使用的“对象”也并未使用额外的处理避免传递对象指针。

C语言对象的“私有成员变量”

直接在类结构体中加入变量作为该类的成员变量是方便的,但是这种成员变量显然是 public 的,该类实例化的任意对象都能随意访问该变量。当然,如果本来就是如此设计的,这么做没有什么问题。

不过有时候,我们只希望某个成员变量只供类内部使用,也即希望该成员变量是 private 的,该怎么办呢?当然,最简单的办法就是写下文档告诉调用者不要随意访问该成员,但是这种方法不具备强制性,很多C语言程序员使用的 IDE 甚至会自动联想补全出该成员变量,一不小心,很容易就出现直接访问本来希望是 private 的成员变量。

其实,我们可以将类的私有(private)成员变量再做一次封装,在类定义中只保留一个指针用于索引各个成员变量即可。请看下面这段C语言代码:

struct cfun{
    void (*modify)();
    void (*print)();
    void *private_data;
};

// 不对外开放
struct PRIVATE{
    char        c;
    int         val;
    //...
};

上述C语言代码将“类”cfun 的私有成员变量封装成一个结构体,并且在 cfun 的定义中只保留一个 void * 指针作为入口,解析私有成员变量的结构体 struct PRIVATE 不对外开放,这样一来,只有在 cfun 内部才能解析出具体的私有成员变量。

外部调用者即使能够访问 private_data,也不能轻易的解析出具体的数据,这样就避免了外部调用者通过对象指针随意访问 cfun 的私有成员变量了。

对于 cfun 本身,结构体 struct PRIVATE 是可见的,因此访问 c 和 val 等私有成员变量是方便的,下面是一个示例,请看相关C语言代码:

void modify(struct cfun *f)
{
    ((struct PRIVATE *)(f->private_data))->val ++;
}
void myprint(struct cfun *f)
{
    printf("val = %d\n", ((struct PRIVATE *)(f->private_data))->val);
}

如果觉得 ((struct PRIVATE * )(f->private_data))->val 这样访问 val 太过繁琐,可以使用定义宏的小技巧,这一点我们已经比较熟悉了,例如:

#define PD(pcfun)          \
    ((struct PRIVATE *)((pcfun)->private_data))

这样一来,再写C语言代码就简洁了:

void modify(struct cfun *f)
{
    PD(f)->val ++;
}
void myprint(struct cfun *f)
{
    printf("val = %d\n", PD(f)->val);
}

小结

在使用C语言结构体和指针语法模拟面向对象编程时,也是允许定义结构体的成员变量的,本文讨论了两种方式:借助于全局变量,或者直接在结构体中添加变量。比较推荐后者,不过直接在结构体中添加的变量是 public 的,各个实例对象都能直接访问。如果希望定义类内部使用的 private 变量,可以借助C语言的结构体和指针语法再封装一层。

当然,本文所讨论的内容只属于抛砖引玉,更多技巧和方法这里不可能一一涉及。相信读者在实际C语言项目开发中,必定能够发现更好的编程风格。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif