14

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

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A728%E8%8A%82-%E6%A8%A1%E6%8B%9F%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B-%E6%80%8E/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一节讨论了结合指针和结构体语法,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语言项目开发中,必定能够发现更好的编程风格。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK