偏爱OOP语言后如何思考C程序员?[关闭]


38

以前,我只使用过面向对象的编程语言(C ++,Ruby,Python,PHP),现在正在学习C。我发现很难找出用一种没有概念的语言来做事的正确方法。 '宾语'。我意识到可以在C语言中使用OOP范例,但是我想学习C语言惯用的方式。

解决编程问题时,我要做的第一件事是想象一个可以解决该问题的对象。使用非OOP命令式编程范例时,我可以用哪些步骤替换它?


15
我还没有找到一种与我的思维方式非常匹配的语言,因此我必须针对自己使用的任何语言“整理”我的想法。我发现一个有用的概念是“代码单元”的概念,无论它是标签,子例程,函数,对象,模块还是框架:它们中的每一个都应封装并公开定义明确的接口。如果您使用的是自上而下的对象级方法,则在C语言中,您可能首先要绘制一组行为,好像问题已解决一样。设计良好的C API通常看起来像OOP,但qux = foo.bar(baz)变成了qux = Foo_bar(foo, baz)
阿蒙2015年

要回声amon,请重点关注以下内容:类似图形的数据结构,指针,算法,代码(函数)的执行(控制流),函​​数指针。
rwong

1
LibTiff(github上的源代码)是如何组织大型C程序的示例。
rwong

1
作为一名C#程序员,我错过委托(带有一个绑定参数的函数指针)的机会远远超过错过对象的机会。
CodesInChaos

我个人发现大多数C语言都很简单明了,但预处理器除外。如果我必须重新学习C,那将是我将大量精力集中在这一领域上。
biziclop 2015年

Answers:


53
  • AC程序是功能的集合。
  • 函数是语句的集合。
  • 您可以使用封装数据struct

而已。

你是怎么写的?这几乎就是您编写.C文件的方式。当然,您不会得到方法多态性和继承之类的东西,但是无论如何您都可以模拟具有不同函数名和组成的函数。

要铺平道路,请学习函数式编程。 如果没有类,您可以做的事真是太神奇了,而且有些事情实际上在没有类开销的情况下效果更好。

进一步阅读
ANSI C中的面向对象


9
您也可以typedef这样struct做,并制作类似class的东西。和typedef-ed类型可以包含在struct本身可以被typedef-ed的其他s中。用C不能得到的是运算符重载以及在C ++中获得的类及其成员的表面上简单的继承。而且您不会得到C ++带来的许多奇怪和不自然的语法。我真的很喜欢OOP的概念,但是我认为C ++是OOP的丑陋实现。我喜欢C,因为它一种较小的语言,并且从该函数最好的语言中省略了语法。
罗伯特·布里斯托

22
作为母语为C的人,我敢说这一点。a lot of things actually work better without the overhead of classes
haneefmubarak

1
为了扩展,已经开发出许多没有OOP的东西:操作系统,协议服务器,引导加载程序,浏览器等。计算机不考虑对象,也不需要考虑对象。确实,强迫他们这样做通常很慢
edmz 2015年

对应点:a lot of things actually work better with addition of class-based OOP。资料来源:TypeScript,Dart,CoffeeScript以及业界试图摆脱功能性/原型OOP语言的所有其他方式。

要展开,很多事情已经开发 OOP:一切。根据对象和程序的编写,人类自然会思考,以供其他人类阅读和理解。
书斋

18

阅读SICP并学习Scheme,以及抽象数据类型实践思路。然后,使用C进行编码就很容易了(由于使用了SICP,一些C,以及一些PHP,Ruby,等等。。。您的想法将会足够广泛,并且您将了解,面向对象的编程可能不是最佳的编程风格。所有情况,但仅适用于某些程序)。注意C动态内存分配,这可能是最难的部分。该C99C11编程语言标准和C标准库实际上是相当差的(不知道TCP或目录!),并且你会经常需要一些外部库或接口(例如,POSIXlibcurl中的HTTP客户端库,libonion对于HTTP服务器库,GMPlib为大数,一些图书馆像libunistring为UTF-8等)。

您的“对象”通常在C中有一些相关的struct-s,并且您定义了对它们进行操作的函数集。对于简短的函数或非常简单的函数,请考虑使用related定义它们struct,就像static inline在其他-d 头文件foo.h#include一样。

注意,面向对象的编程并不是唯一的编程范例。在某些情况下,其他范例也很有价值(例如Ocaml或Haskell的函数式编程,甚至Scheme或Commli Lisp的编程, Prolog的逻辑编程等。。。另参阅J.Pitrat的有关声明式人工智能的博客)。参见Scott的书:《编程语言语用学》

实际上,使用C或Ocaml的程序员通常不希望以面向对象的编程风格进行编码。当没有用的时候,没有理由强迫自己去思考对象。

您将定义一些struct函数以及对其进行操作的函数(通常通过指针)。您可能需要一些带标签的联合(通常是一个struct带有标签成员的联合,通常在内部有一些enum,而union在内部又有一些),并且在某些-s 末尾具有一个灵活的数组成员可能会很有用struct

C中查看一些现有免费软件的源代码(请参阅githubsourceforge 来找到一些)。也许安装和使用Linux发行版将很有用:它几乎仅由自由软件组成,它具有出色的自由软件C编译器(GCCClang / LLVM)和开发工具。如果要为Linux开发,请参阅高级Linux编程

不要忘了所有警告和调试信息,如编译gcc -Wall -Wextra -g-notably开发和调试phases-过程中,学会使用一些工具,如Valgrind的打猎内存泄漏,该gdb调试器等小心很好地理解什么是未定义行为并强烈避免这样做(请记住,程序可能具有一些UB,有时似乎“起作用”)。

当您真正需要面向对象的构造(尤其是继承)时,可以使用指向相关结构和函数的指针。您可以拥有自己的vtable机制,使每个“对象”以指向struct包含函数指针的指针开头。您可以利用将指针类型转换为另一种指针类型的功能(以及您可以从struct super_st包含与启动a的字段类型相同的字段类型struct sub_st进行模拟的事实)。注意,正如GObject(来自GTK / Gnome)所演示的那样,C足以实现非常复杂的对象系统-特别是通过遵循一些约定 - 。

当您确实需要闭包时通常会使用回调模仿它们,并约定使用回调函数的每个函数都要传递一个函数指针和一些客户端数据(函数指针在调用时会被消耗)。您还可以(通常)拥有自己的类似闭包的struct-s(包含一些函数指针和闭包值)。

由于C是一种非常底层的语言,因此定义和记录您自己的约定(受其他C程序的实践启发)非常重要,尤其是有关内存管理,也可能还有一些命名约定。对指令集体系结构有所了解很有用。不要忘C编译器可能会对您的代码进行很多优化(如果您要求的话),所以不要太在乎手工进行微优化,而应将其留给您的编译器(gcc -Wall -O2对于已发布的版本的优化编译)软件)。如果您关心基准测试和原始性能,则应启用优化(一旦程序已调试)。

别忘了有时元编程很有用。通常,用C编写的大型软件包含一些脚本或临时程序,以生成在其他地方使用的C代码(并且您可能还会玩一些肮脏的C预处理技巧,例如X-macros)。存在一些有用的C程序生成器(例如yaccgnu bison生成解析器,gperf生成完美的哈希函数,等等。)。在某些系统(尤其是Linux和POSIX)上,您甚至可以在运行时在generated-001.c文件中生成一些C代码,通过在运行时运行一些命令(例如gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so)将其编译为共享对象,并使用dlopen动态加载该共享对象&使用dlsym从名称获取函数指针。我正在MELT(一种类似于Lisp的领域特定语言,可能对您有用,因为它可以自定义GCC编译器)上做这些技巧。

请注意垃圾回收的概念和技术(引用计数通常是一种管理C语言中的内存的技术,恕我直言,这是一种糟糕的垃圾回收形式,无法很好地处理循环引用;您可能会缺乏帮助的指针,但这可能很棘手)。在某些情况下,您可能会考虑使用Boehm的保守垃圾收集器


7
坦率地说,毫无疑问,阅读SICP无疑是一个不错的建议,但是对于OP来说,这可能会导致下一个问题“在偏爱SICP之后如何以C程序员的身份思考”。
布朗

1
不可以,因为来自SICP和PHP(或Ruby或Python)的Scheme是如此不同,以至于OP会获得更广泛的思考。和SICP解释了相当不错的是在实践中抽象数据类型,这是了解非常有用的,特别是对于编码C.
巴西莱Starynkevitch

1
SICP是一个奇怪的建议。计划是非常不同的从C
布莱恩·戈登·

但是SICP正在教很多好的习惯,并且知道Scheme在用C进行编码(对于闭包,抽象数据类型等的概念)确实有所帮助
Basile Starynkevitch 2015年

5

程序的构建方式基本上是为了解决问题而定义必须执行的操作(功能)(这就是为什么将其称为过程语言)的原因。每个动作将对应一个功能。然后,您需要定义每个函数将接收什么类型的信息以及它们需要返回什么信息。

程序通常分为文件(模块),每个文件通常具有一组相关的功能。在每个文件的开头,您声明(在任何函数之外)该文件中所有函数将使用的变量。如果您使用“静态”限定符,则这些变量将仅在该文件内部可见(而其他文件则不可见)。如果您不在函数外部定义的变量上不使用“静态”限定符,那么它们也可以从其他文件访问,并且这些其他文件应将变量声明为“ extern”(但不定义变量),以便编译器查找它们在其他文件中。

简而言之,您首先要考虑过程(功能),然后确保所有功能都可以访问其所需的信息。


3

如果您以正确的方式看待C API,通常 - 甚至甚至通常 -确实具有面向对象的接口。

在C ++中:

class foo {
    public:
        foo (int x);
        void bar (int param);
    private:
        int x;
};

// Example use:
foo f(42);
f.bar(23);

在C中:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

// Example use:
foo f = { .x = 42 };
bar(&f, 23);

如您所知,在C ++和其他各种形式的OO语言中,对象方法在幕后采用了第一个参数,该参数是指向对象的指针,与bar()上面的C版本类似。有关这在C ++中浮出水面的示例,请考虑如何std::bind用于使对象方法适合函数签名:

new function<void(int)> (
    bind(&foo::bar, this, placeholders::_1)
//                  ^^^^ object pointer as first arg
);

正如其他人指出的那样,真正的区别是正式的OO语言可以实现多态性,访问控制和各种其他漂亮的功能。但是面向对象程序设计的本质,即离散,复杂数据结构的创建和操作,已经是C语言的基本实践。


2

鼓励人们学习C的主要原因之一是,它是高级编程语言中最低的语言之一。OOP语言使人们更容易考虑数据模型以及对代码和消息传递进行模板化,但是最终,微处理器逐步执行代码,跳入和跳出代码块(C中的函数)并移动引用变量(C中的指针),以便程序的不同部分可以共享数据。将C视为英语的汇编语言-为您的计算机微处理器提供逐步说明-不会出错。另外,大多数操作系统接口的工作方式类似于C函数调用,而不是OOP范例,


2
IMHO C是一种低级语言,但是比汇编程序或机器代码高得多,因为C编译器可以执行许多低级优化。
Basile Starynkevitch 2015年

C编译器也以“优化”的名义迈向抽象的机器模型,该模型可能会在给定输入的情况下抵消时间和因果关系的定律,这将导致未定义行为,即使代码在其上的自然行为也是如此。运行将满足其他要求。例如,该功能uint16_t blah(uint16_t x) {return x*x;}将在unsigned int16位或33位或更大的计算机上相同地工作。unsigned int但是,某些用于17到32位机器的编译器可能会考虑对该方法的调用……
supercat

...作为授予编译器的权限,以推断不会发生可能导致该方法的值超过46340的事件链。即使在任何平台上乘以65533u * 65533u都会产生一个值,当转换为时uint16_t,将产生9的值,但uint16_t在17位至32位平台上乘以type的值时,标准不会强制执行此类行为。
supercat

-1

我也是本地人(通常是C ++),有时必须在C语言世界中生存。对我而言,最大的障碍是处理错误处理和资源管理。

在C ++中,我们抛出了一个错误,错误从发生的地方一直返回到可以处理该错误的顶层,并且有析构函数可以自动释放内存和其他资源。

您可能会注意到,许多C API都包含一个init函数,该函数为您提供了一个typedef'd void *,它实际上是一个指向结构的指针。然后,将其作为每个API调用的第一个参数传递。从本质上讲,这成为C ++的“ this”指针。它用于隐藏的所有内部数据结构(非常面向对象的概念)。您还可以使用它来管理内存,例如,有一个名为myapiMalloc的函数,该函数对您的内存进行malloc分配并将其记录在this指针的C版本中,以便您可以确保在API返回时将其释放。同样,正如我最近发现的那样,您可以使用它来存储错误代码,并使用setjmp和longjmp来使您的行为与抛出捕获非常相似。结合这两个概念,可以为您提供C ++程序的许多功能。

现在您确实说过,您不想学习将C强制转换为C ++。这并不是我要描述的(至少不是故意的)。这只是(希望)精心设计的利用C功能的方法。确实确实具有一些面向对象的风格-也许这就是为什么开发面向对象语言的原因,它们是一种形式化/强制/促进一些人认为是最佳实践的概念的方法。

如果您认为这对您来说是OO的感觉,那么替代方法是让几乎每个函数返回错误代码,您必须认真地确保在每个函数调用之后检查并向上传播调用堆栈。您必须确保不仅在每个函数的末尾而且在每个返回点都释放了所有资源(这可能在任何函数调用之后释放,该函数可能返回表明您无法继续的错误)。它可能变得非常乏味,并且会导致您认为我可能不需要处理潜在的内存分配失败(或文件读取或端口连接...),我只是假设它可以工作,否则我会现在将编写“有趣的”代码,然后返回并处理错误处理-永远不会发生。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.