以前,我只使用过面向对象的编程语言(C ++,Ruby,Python,PHP),现在正在学习C。我发现很难找出用一种没有概念的语言来做事的正确方法。 '宾语'。我意识到可以在C语言中使用OOP范例,但是我想学习C语言惯用的方式。
解决编程问题时,我要做的第一件事是想象一个可以解决该问题的对象。使用非OOP命令式编程范例时,我可以用哪些步骤替换它?
以前,我只使用过面向对象的编程语言(C ++,Ruby,Python,PHP),现在正在学习C。我发现很难找出用一种没有概念的语言来做事的正确方法。 '宾语'。我意识到可以在C语言中使用OOP范例,但是我想学习C语言惯用的方式。
解决编程问题时,我要做的第一件事是想象一个可以解决该问题的对象。使用非OOP命令式编程范例时,我可以用哪些步骤替换它?
Answers:
struct
。而已。
你是怎么写的?这几乎就是您编写.C文件的方式。当然,您不会得到方法多态性和继承之类的东西,但是无论如何您都可以模拟具有不同函数名和组成的函数。
要铺平道路,请学习函数式编程。 如果没有类,您可以做的事真是太神奇了,而且有些事情实际上在没有类开销的情况下效果更好。
进一步阅读
ANSI C中的面向对象
typedef
这样struct
做,并制作类似class的东西。和typedef
-ed类型可以包含在struct
本身可以被typedef
-ed的其他s中。用C不能得到的是运算符重载以及在C ++中获得的类及其成员的表面上简单的继承。而且您不会得到C ++带来的许多奇怪和不自然的语法。我真的很喜欢OOP的概念,但是我认为C ++是OOP的丑陋实现。我喜欢C,因为它是一种较小的语言,并且从该函数最好的语言中省略了语法。
a lot
of things actually work better without the overhead of classes
a lot of things actually work better with addition of class-based OOP
。资料来源:TypeScript,Dart,CoffeeScript以及业界试图摆脱功能性/原型OOP语言的所有其他方式。
阅读SICP并学习Scheme,以及抽象数据类型的实践思路。然后,使用C进行编码就很容易了(由于使用了SICP,一些C,以及一些PHP,Ruby,等等。。。您的想法将会足够广泛,并且您将了解,面向对象的编程可能不是最佳的编程风格。所有情况,但仅适用于某些程序)。注意C动态内存分配,这可能是最难的部分。该C99或C11编程语言标准和C标准库实际上是相当差的(不知道TCP或目录!),并且你会经常需要一些外部库或接口(例如,POSIX,libcurl中的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中查看一些现有免费软件的源代码(请参阅github&sourceforge 来找到一些)。也许安装和使用Linux发行版将很有用:它几乎仅由自由软件组成,它具有出色的自由软件C编译器(GCC,Clang / 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程序生成器(例如yacc或gnu 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的保守垃圾收集器。
程序的构建方式基本上是为了解决问题而定义必须执行的操作(功能)(这就是为什么将其称为过程语言)的原因。每个动作将对应一个功能。然后,您需要定义每个函数将接收什么类型的信息以及它们需要返回什么信息。
程序通常分为文件(模块),每个文件通常具有一组相关的功能。在每个文件的开头,您声明(在任何函数之外)该文件中所有函数将使用的变量。如果您使用“静态”限定符,则这些变量将仅在该文件内部可见(而其他文件则不可见)。如果您不在函数外部定义的变量上不使用“静态”限定符,那么它们也可以从其他文件访问,并且这些其他文件应将变量声明为“ extern”(但不定义变量),以便编译器查找它们在其他文件中。
简而言之,您首先要考虑过程(功能),然后确保所有功能都可以访问其所需的信息。
如果您以正确的方式看待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语言的基本实践。
鼓励人们学习C的主要原因之一是,它是高级编程语言中最低的语言之一。OOP语言使人们更容易考虑数据模型以及对代码和消息传递进行模板化,但是最终,微处理器逐步执行代码,跳入和跳出代码块(C中的函数)并移动引用变量(C中的指针),以便程序的不同部分可以共享数据。将C视为英语的汇编语言-为您的计算机微处理器提供逐步说明-不会出错。另外,大多数操作系统接口的工作方式类似于C函数调用,而不是OOP范例,
uint16_t blah(uint16_t x) {return x*x;}
将在unsigned int
16位或33位或更大的计算机上相同地工作。unsigned int
但是,某些用于17到32位机器的编译器可能会考虑对该方法的调用……
uint16_t
,将产生9的值,但uint16_t
在17位至32位平台上乘以type的值时,标准不会强制执行此类行为。
我也是本地人(通常是C ++),有时必须在C语言世界中生存。对我而言,最大的障碍是处理错误处理和资源管理。
在C ++中,我们抛出了一个错误,错误从发生的地方一直返回到可以处理该错误的顶层,并且有析构函数可以自动释放内存和其他资源。
您可能会注意到,许多C API都包含一个init函数,该函数为您提供了一个typedef'd void *,它实际上是一个指向结构的指针。然后,将其作为每个API调用的第一个参数传递。从本质上讲,这成为C ++的“ this”指针。它用于隐藏的所有内部数据结构(非常面向对象的概念)。您还可以使用它来管理内存,例如,有一个名为myapiMalloc的函数,该函数对您的内存进行malloc分配并将其记录在this指针的C版本中,以便您可以确保在API返回时将其释放。同样,正如我最近发现的那样,您可以使用它来存储错误代码,并使用setjmp和longjmp来使您的行为与抛出捕获非常相似。结合这两个概念,可以为您提供C ++程序的许多功能。
现在您确实说过,您不想学习将C强制转换为C ++。这并不是我要描述的(至少不是故意的)。这只是(希望)精心设计的利用C功能的方法。确实确实具有一些面向对象的风格-也许这就是为什么开发面向对象语言的原因,它们是一种形式化/强制/促进一些人认为是最佳实践的概念的方法。
如果您认为这对您来说是OO的感觉,那么替代方法是让几乎每个函数返回错误代码,您必须认真地确保在每个函数调用之后检查并向上传播调用堆栈。您必须确保不仅在每个函数的末尾而且在每个返回点都释放了所有资源(这可能在任何函数调用之后释放,该函数可能返回表明您无法继续的错误)。它可能变得非常乏味,并且会导致您认为我可能不需要处理潜在的内存分配失败(或文件读取或端口连接...),我只是假设它可以工作,否则我会现在将编写“有趣的”代码,然后返回并处理错误处理-永远不会发生。
qux = foo.bar(baz)
变成了qux = Foo_bar(foo, baz)
。