C语言真的不能面向对象吗?

一直以来,有关于C++、Java、C#等语言的书总喜欢在开篇介绍中拿C语言来比较一番。在承认C语言无可争议的运行效率的同时,也总爱拿C语言不具备面向对象血统的短板说事。当年在看这些的书的时候,我还觉得深以为然。但经过这些年的学习和见闻,我却越来越强烈地感到这种说法有失偏颇:C语言真的不具备面向对象的能力吗?

考虑这个问题之前,首先要明确一点,什么是面向对象,或者说什么是对象?可以认为,对象=属性+行为。这跟那著名的公式“算法+数据结构=程序”没什么本质区别,但是两者的侧重点不同,抽象层次也不同。面向对象强调的是以数据为核心,在程序中复活事物的抽象本质,让它们“智能地”参与问题的求解,而不是传统的以算法或函数过程为核心去解决问题。所以面向对象绝不只是一种技术,更重要的是一种思想,是一种考虑问题和解决问题的思维方式。既然是一种思想,那么它就不是C#程序员或者是Java程序员的专属。

通常在评价一种语言是否支持面向对象时,首先要看就是这种语言中是否有类的概念。注意这里我说的是“类的概念”,而不是“类”。如果非要死抓住C语言没有class关键字不放,而就此否定C语言面向对象的可能性,那我也就只能呵呵了。抛开这一点,可以宽泛地说任何能从技术上为面向对象提供便利的语言都是面向对象的。当然,就目前来说,实现面向对象的技术手段主要还是类。那些被公认为面向对象的语言正是凭借类这一技术为面向对象提供了强有力的支持。那么类是什么样的呢?

想想当初C++的类是怎么来的,不就是结构体里面定义几个函数,然后把关键字改成class,再加上访问权限和继承机制吗?实际上在C++里连关键字都可以不用改,直接在struct里面就可以定义函数。C语言虽然没有类,可结构体好说啊,只不过C语言的结构体里面不能直接定义函数。但是单从思想的角度来说,对象就是属性和行为的集合,一切行为都围绕属性组成的数据核心展开,所以函数是否能在结构体中定义并不是必不可少的,虽然这对面向对象确实是一种强大的技术支持。实际上,结构体里面可以定义函数指针,而C语言中对函数指针以及各种其他指针出神入化的运用让人拍案叫绝。

所以无论信或不信,C语言实现面向对象编程是可能的。试想对于C语言这样一种如此灵活而底层的语言来说,别的高级语言能做到的事它有什么理由做不到呢?只是做起来的复杂程度的问题罢了,但巧的是,面向对象这事儿对于C语言来说还真不麻烦。实际上这事早就有人做了,只是我不明白的是为什么一直没有这种提法,而我也正是因此才开始思考这个问题的。

当年面向对象的提出,就是因为面向过程的思想无法满足日益膨胀的软件规模所带来的编程效率和维护效率的问题。也就是说,软件工业界普遍认为面向过程的C语言不适合编写大型软件,但是为何如今如火如荼的开源领域却基本都采用了C语言,并且出产了包括Linux系统在内的无数优秀的软件?开源领域选用C语言无非是看中其运行效率,但软件规模的问题是怎么解决的?它们给出的答案就是:把面向对象的思想运用到C语言中去!

我们先来看看最基本的IO操作,通过三个简单的例子对比一下面向对象语言和C语言各自都是怎么做的。

首先是Java的版本,文件读取被封装为FileInputStream类:

byte[] buffer=new byte[4096];

FileInputStream file=new FileInputStream("test.txt");
file.read(buffer, 0, 4096);
file.close();

然后是Linux下的C语言版本,用文件描述符来表示文件:

char buffer[4096];

int file=open("test.txt", O_RDONLY);
read(file, buffer, 4096);
close(file);

还有一个stdio定义的C语言版本,用文件指针表示文件:

char buffer[4096];

FILE *file=fopen("test.txt", "rb");
fread(buffer, 1, 4096, file);
fclose(file);
可以很明显地看到,这三个版本竟然惊人地相似!第一种Java版本的设计宗旨是面向对象,而第二种Linux背后的哲学却是“一切皆是文件”,这两种思想在某些方面不谋而合。深究起来,Linux的“一切皆是文件”其实是高度概括了一类事物在某一方面的共同属性和行为,用面向对象的话来说就是“基类”或者说是“接口”。可以说,C语言的IO操作虽然没有面向对象的“形”,却深得面向对象的“魂”。不仅如此,在C语言这门古老的语言里,也能看到面向对象语言里最时髦的“引用类型”,那就是文件描述符和文件指针,甚至文件描述符里都有“引用计数”。反过来说,“引用类型”本身也是脱胎于指针。

也许第二种Linux的版本没有很明显地看出来面向对象中的“数据”,因为代表文件的只是一个简单的整数,但是第三个stdio的版本就比较明显了。FILE结构体包含了文件操作相关的复杂属性集合,而行为就是fopen、fread和fclose。我们一般都不会试图去直接对FILE结构体进行访问,都是通过定义良好的行为集合来访问文件,行为集为我们屏蔽了所有复杂的细节,这不正是面向对象所追求的的封装吗?只不过真正被称为面向对象的语言通过访问权限控制这种技术机制来强制不让程序员直接访问数据,而C语言需要程序员自己保证。如果稍微了解过Linux内核实现,就会知道第二种版本的文件描述符整数其实是内核中文件结构体数组的下标,所以文件数据也是维护在结构体中的,而且这里更妙的是,系统通过内核保护的方式保证了普通程序员无法直接修改文件数据,这就更加接近面向对象的封装了。

刚才的例子非常简单,另一个稍微复杂一点的例子就是著名开源JPEG图像解码库libjpeg,其中提供了一套函数和数据结构用于bmp图像和jpeg图像的互转。使用libjpeg解码jpeg图像的一个典型过程如下:

FILE * infile;

//错误管理对象
struct jpeg_error_mgr jerr;

//jpeg解压对象
struct jpeg_decompress_struct cinfo;

JSAMPARRAY buffer;

int row_stride;

infile = fopen("test.jpg", "rb");

//为解压对象设置一个错误管理对象,有点对象之间通信的感觉
cinfo.err = jpeg_std_error(&jerr);

//第一步初始化解压对象,像极了构造函数
jpeg_create_decompress(&cinfo);

//第二步设置解压源:来自文件
jpeg_stdio_src(&cinfo, infile);

//第三步读取文件头的jpeg图像参数
(void) jpeg_read_header(&cinfo, TRUE);

//第四步开始准备解压
(void) jpeg_start_decompress(&cinfo);

row_stride = cinfo.output_width * cinfo.output_components;

//用解压对象内含的内存管理对象的内存分配方法分配一块内存
//获得的内存不用手动释放,由库自动回收
buffer = (*cinfo.mem->alloc_sarray)
         ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);

//第六步一行行解压
while (cinfo.output_scanline < cinfo.output_height) {
    (void) jpeg_read_scanlines(&cinfo, buffer, 1);
    put_scanline_someplace(buffer[0], row_stride);
}

//第七步解压的扫尾工作
(void) jpeg_finish_decompress(&cinfo);

//第八步释放解压对象,析构函数的感觉
jpeg_destroy_decompress(&cinfo);

fclose(infile);
上面的这个例子可能第一次看比较生疏,但是这不重要,重要的是库对解压接口的封装方式:一个结构体(属性集)+一堆函数(行为集),这不正是面向对象的思维吗?只不过碍于语言特性的限制,它的调用看起来不是obj.method()而是method(&obj),但这并不妨碍其本质的展现。libjpeg的代码规模并不是开源领域中最庞大的,但它却是在C语言上运用面向对象的思想的典范。另外一些也非常出名的开源代码像libpcap、nginx,都是面向对象思想很好的展现,nginx的设计者甚至获得了2012年度开发者云奖。

C语言出生在一个面向对象尚未兴起的年代,理所当然地被认为不具备面向对象的能力。但面向对象是一种思想,实现它的不是某种语言,而是程序员,是人。

  • 29
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
在C语言中,实现面向对象编程的一种常见方式是通过结构体和函数指针来模拟面向对象的封装和继承。 首先,定义一个结构体,表示一个对象,这个结构体中包含对象的属性和方法(即函数指针)。 例如: ``` typedef struct { int x; int y; void (*move)(void* obj, int dx, int dy); } Point; ``` 其中,x和y是对象的属性,move是对象的方法,它是一个函数指针,指向一个接收一个void*类型的参数和两个int类型的参数的函数。 然后,定义一个函数,用于创建对象,并初始化对象的属性和方法。 例如: ``` void* create_point(int x, int y) { Point* p = (Point*)malloc(sizeof(Point)); p->x = x; p->y = y; p->move = point_move; return p; } ``` 其中,create_point函数返回一个void*类型的指针,表示创建的对象。在函数中,使用malloc函数动态分配了一个Point类型的内存空间,并初始化了对象的属性和方法。注意,这里将对象的方法指向了一个名为point_move的函数。 最后,定义对象的方法,即函数指针所指向的函数。这些方法可以访问对象的属性,并对其进行操作。 例如: ``` void point_move(void* obj, int dx, int dy) { Point* p = (Point*)obj; p->x += dx; p->y += dy; } ``` 在这个函数中,首先将void*类型的参数obj强制转换为Point*类型,然后将对象的属性x和y分别加上dx和dy。 这样,就可以使用C语言实现面向对象编程了。例如,可以这样使用上面定义的Point类型: ``` void* p = create_point(1, 2); ((Point*)p)->move(p, 3, 4); printf("(%d, %d)\n", ((Point*)p)->x, ((Point*)p)->y); ``` 这段代码创建了一个坐标为(1, 2)的点对象,然后调用了它的move方法,将坐标移动了(3, 4),最后输出了移动后的坐标。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值