linux编写c语言并运行,linux下如何调试C语言代码
冯云的博客:一个C界面设计的问题
C接口设计的一个问题。本质上,参数传递就是C语言中的值传递。不像Pascal和C,可以引用。这使得C语言一方面保持了简单的设计,另一方面却饱受诟病。
由于性能问题,必须引入指针。可以说,用C语言实现的软件,90%以上的bug都来自指针,这应该不为过。当然,设计导致的问题可能更关键,那些和指针无关。
纠结性能问题,水平比较低。但C语言是活跃在较低层次的语言。一旦你选择使用它,你不得不关心性能。反过来,把C模仿成更高级的语言就有点画蛇添足了。好了,我们来看一个涉及参数传递的实际相关问题,如何用C语言设计。
最近,我的同事正在研究类似于协议缓冲区的东西。做好这个东西不容易,尤其是在设计上。其中设计难点:设计一个合适的DSL(领域特定语言)讨论了很久,分析了好几天,今天不打算讲了。挑一件小事说:当我们解析一个二进制结构化数据块,并将其转移到C语言中,让C语言方便地访问该数据结构时,如何设计接口?
当目标语言不是C而是更高级的语言(尤其是有gc机制的语言)时,这个问题就不是问题了。但是C语言本身没有对象概念。
c语言有结构,但不具备描述动态长度的能力;没有字符串,只有定长字符数组;没有多维数组,只有一维数组。
C函数的参数和返回值可以是结构,但是在接口设计中,也许是因为值的传递和ABI的简单性,经常使用结构指针。返回结构指针经常受到生命周期管理的困扰。即使在C中,也是允许返回结果/对象的,但是所谓的返回值优化是相当令人困扰的(如果你打算完全放弃语言的细节,忽略细微的性能问题。所以,为什么不考虑用Java或者Python,什么都比选C好)。
返回一组复杂数据的常用方法有哪些?
最常见的方法是调用者分配空间并将其传递给处理程序。处理函数反向填充结构内容。这样做的好处是调用者可以选择是在堆栈上还是在堆上分配空间。一点提示:从语法上来说,C语言允许你传递一个数组作为指针。所以可以定义一个长度为1的结构数组类型。用起来比较好看。参见标准库中setjmp的定义。不过作为我个人的想法,我并不提倡为了减少几倍的键盘输入而在C语言设计的软件中使用过多的语言特性。
这种方法的缺点是调用者很难定义一个不确定的数据结构。特别是在结构中,有对其他结构的引用。
与此类似,会收到一个字符串。最典型的例子是标准库中的fgets,它提供了接收缓冲区的地址指针和缓冲区大小。(注:gets是一个失败的设计)同样在Windows的API中,这样的例子随处可见。
第二,函数本身分配内存,交给调用者释放。你只需要在内存管理的接口上达成一致。标准库中的Strdup就是这样做的,readline库中的readline也是如此。C语言使用malloc统一管理内存,不像C提供了更多的灵活性(更难控制,更容易出问题?)被新运算符重载。所以,给这个协议不会增加太多麻烦。顺便说一句,由于微软VC的CRT对malloc的实现很差,很多Windows软件都是自己实现内存管理器的。或者打开库中自定义内存管理器注入的接口。其实有点过了。在gcc提供的CRT中,malloc性能相当不错。
缺点呢?只能从堆中分配内存;而且增加了内存泄漏的隐患;从设计上看,也不是很干净。对于复杂的数据结构,这种方法是无能为力的。C语言里没有析构函数这种东西。
作为第二点的补充,很少有人用。那就是给你的系统添加gc。其实就是商定另一种内存管理方式。我们项目的部分模块正在使用中,效果还不错。Gc库已经开源,请参考这里。如果不信任这一套东西,可以考虑COM的机制:增加或减少引用。COM旨在建立一个对象模型,但遗憾的是,C语言中没有对象的概念。C级用COM有点痛苦。对于粒度更小的东西,性能也会是个问题。
第三,用户多。就是在函数内部开辟一个静态空间,用于数据返回。可以保证返回的指针所指向的数据的生存期,直到下一次调用同一个函数。静态空间可以在数据段声明,也可以在程序初始化的时候从堆中分配,这样在空间不够的时候可以扩展。至于这个静态空间什么时候发布,不用太担心。即使你不释放他们,你也不必感到内疚。操作系统会帮你回收,它会比你做的更好。c的诞生就是为了实现UNIX,UNIX的哲学就是写简单的程序做自己的事,让更高级的程序(通常是shell或者动态语言)组合起来,让操作系统来管理。在Windows上,Unix编程哲学可能没什么用,但原理是正确的。
这个方案还有一个问题,就是函数不能重入,程序安全。可以避免重新进入的问题。线程安全可以通过TLS解决。说实话,我个人并不喜欢用多线程来解决C语言中的问题。多线程也违背了Unix哲学。如果你有几件事需要协调,就用多流程;如果你同时有几百件事要做,可以考虑换个思路,玩玩二郎什么的。
回到我们今天面临的问题。用一个DSL来描述一个数据结构(比C的结构更有表现力),然后生成相应语言的解析库。如果目标语言是C,我们生成的代码如何返回对C程序员友好的结构化数据?
这让我想起了MySQL的C语言接口。很多初学C的程序员喜欢把那些C接口“包装”成“漂亮”的C接口。直接返回矢量地图的多层模板实例。不知道有多少人做过。几年前带实习生的时候,反正见过不少。同学,如果你现在清醒过来,明白这是一件很愚蠢的事情,那么握个手,我们的想法是一样的;否则(C)封装后不是很“酷”吗?),我们暂时没有共同语言。
我不是说MySQL的C接口设计的很好,但是还是比较满意的。只是C不是C,C也不是C。(换句话说,我不认为上面提到的C包是C的正确用法)我反复提到C,是因为我发现今天很大比例的C程序员其实是从C开始启蒙的,而不是相反。用C当C的危害其实小于用C当C,前者只是把车开到自行车的速度,至少不怕摔跤,跑起来可以更安全;后者,如果你非要在高速路中间踩自行车,迟早会被撞死。
C程序使用最方便的方法是传入一个结构指针,这样库就可以解析数据,填充这个结构。
但如果结构中有不定长的字符串和数组(通常后面的长度会根据之前解析的数据来确定),对于C的编程技巧来说,允许将结构最后一个数组的长度设置为0,假设它是不定长的,这样就减少了一个间接的指针引用。但是,这种技术不能用于结构中多个不定长度的数组。)等等,很难避免指针。
一旦数据中出现指针(间接引用其他数据),就会出现内存管理问题。
一开始考虑了非常C的方案,引入了内存管理器。这种设计在STL里。所有的STL容器都可以为灵活的内存管理指定一个分配器。几年前,我觉得这是一件相当聪明的事情。不经过深思熟虑,定制分销商到底有多大意义?自定义内存管理器很大程度上是由效率因素造成的。但是性能问题从来都不是根本问题。软件是为了达到特定的目的而制作的,软件开发的问题更多的是为了解决复杂性问题。通常复杂性导致的性能问题更加严重。然后为了解决复杂度带来的性能问题,引入更高的复杂度,恶性循环的可能性非常大。
即使我们传入内存管理器(或者直接在CRT中使用malloc,但是之后就不可能使用堆栈来分配空间了),我们还是会面临新的问题,如何回收结构中间接引用的数据。引入析构函数指针?OMG .
后来,我们假设使用一个内部静态空间,所有分析结果都在内部分发,由我们自己管理。这些空间也可以重复使用。大部分分析结果都是临时用的,方便。而且调用者不必太在意数据的生存期。
但是,一旦调用方需要将结果(一个复杂的结构)保留一段时间,就会遇到困难。
当然,可能并不难。当我们面对这个设计难点的时候,我们都应该向上考虑一层。这是个问题吗?我们需要这样使用它吗?
调用者可以自己遍历这个数据结构,用自己的方式复制和组织他需要的数据。他们需要的是数据,而不是数据结构的完整拷贝。
经过深思熟虑,我们仍然发现保持完整的数据结构是有意义的。不像C,它没有重载对象赋值操作符的语法糖,我不喜欢用宏来模拟一个。添加一个复制函数指针其实和添加一个析构函数是一样的,对于c来说就没那么好看了(当然同时增加了开发量,我们需要编写更多的自动代码生成器)
最后,我们采用了调用者传入缓冲区指针的方案。解析器生成的数据结构需要放在连续的内存空间中。这样,调用者可以直接将指针定义为最终可访问的结构或联合。但是它提供了更多的内存空间来存储内部引用的数据(如字符串)。
因为结果数据区是调用者提供的,所以不存在数据复制移动带来的指针调整问题(调用者可以自己先分配)。
最后一个问题是,呼叫者如何估算数据接收区域的大小?
许多Windows API可以被调用两次。第一次空调算出需要的缓冲区大小,第二次就真的把数据填好了。在分析了实际需求后,我认为这在我们模块的应用中是多余的。我们可以让用户给出一个估计的大小来处理数据,一旦空间不够就返回一个错误消息。让用户扩展缓冲区并再次调用它。
顺便说一句,连续重试是我们最终批准的最便宜的解决方案。一开始我们觉得还是让处理器自己分配内存,自己用realloc比较好。后来发现设计完全多余。因为解析二进制流是O(1)运算,不比估计长度慢;但是调用者可以正确估计接收区域的长度,即使通过每次两次的简单方法来扩大接收区域的大小,也不会浪费太多的处理时间。即使他们需要准确地分配结果所需的内存块,他们也可以用一个足够大的公共缓冲区接收它们,然后获得长度信息,并在特定的内存上重新开始。
我厌倦了写作。我想表达的也表达了。今天到此为止。D
赞美诗前几天写了一篇关于对汉字更环保的Unicode编码方案的文章。昨晚我花了两个小时写了一个简单的C实现。你可以暂时把UTF-8或者UTF-16转换成我自己命名为UTF-C的代码,也可以把它转回来。我用了比预期更多的代码行,因为我低估了UTF-8的处理复杂度(其实并不复杂)。
有兴趣的同学可以看看这里。
由冯云于2009年1月6日晚上11: 41提交固定链接
评论
今年以来我读的第一篇散文,如饮烈酒,甘甜苍劲。
发布者:
寒流 (24)
2011年2月27日凌晨1点28分
Strdup是posix,不是C库。呵呵
发布者:
cc (23)
2010年8月6日晚上10点16分
哈哈,我们有共同语言。我大胆猜测,风云哥更愿意用codegen来解决语言的不足,而不是依靠某一种语言的特性。
发布者:
artizc (22)
2009年2月14日下午3点
第三,方法稍加修改。每个线程在函数中都有一个静态空间,它与当前线程(ID)捆绑在一起。在实现上,线程ID用作键,缓存存储在数组或HashMap中。实现相当简单,性能几乎没有变化。也能解决本文的大部分问题,只是不能重入。然而,正如冯云所说,这个问题可能不是问题。
发布者:
富 (21)
2009年1月30日上午11点04分
C语言标准没有明确限制返回值不能是什么,所以C允许将return struct作为返回复杂数据的方案。
不知道之前看过哪本书,我觉得returning struct是c的扩展,(其实不是)
感谢指出这个错误的朋友,我今天仔细检查了ISO C90和ISO C99的标准。D
发布者:
云 (20)
2009年1月10日下午08:01
你的语言技能需要提高。
发布者:
Avin (19)
2009年1月9日下午1点40分
这让我想起了MySQL的C语言接口。很多初学C的程序员喜欢把那些C接口“包装”成“漂亮”的C接口。直接返回矢量地图的多层模板实例。不知道有多少人做过。几年前带实习生的时候,反正见过不少。同学,如果你现在清醒过来,明白这是一件很愚蠢的事情,那么握个手,我们的想法是一样的;否则(C)封装后不是很“酷”吗?),我们暂时没有共同语言。
呵呵,我以前做过这个。然而,我也没办法。我那时才学会的。
不管是什么,用我的小斧子试试。
发布者:
匿名 (18)
2009年1月9日下午1点14分
Windows api调用总是这样做。
发布者:
匿名 (17)
2009年1月8日下午05:05
为什么C函数不能返回结构?
发布者:
匿名 (16)
2009年1月8日下午3点42分
为什么不看看xdr?sun rpc中的数据序列化方法,如果你觉得它在进程安全性上有太多问题,那就看看cdr?c和java都在用。corba的Idl和sun rpc的xdr就是用DSL定义数据,然后交给C语言解析的两个很好的例子。我认为你们所讨论的在他们的实现中有所有的答案。
至于allocator,我只在loki中看到它是为小对象设计的,它专门用于分配小于64位的小对象。我觉得这方面是华立可能不讨好的地方。有时间可以做一些其他的优化。哦,您可以将它用作内存泄漏的调试器。
至于数据接收区的大小,我觉得stl的stringstream和string/vector都做得很好,根据需求自动增长,也让程序员灵活预留空间。但遗憾的是没有realloc。不知道你申请的具体情况是怎样的。如果每次都在没有realloc的情况下进行delete/new,会对性能产生很大的影响。
发布者:
snnn (15)
2009年1月8日下午2点49分
我感觉我说的是fgets。
发布者:
zii (14)
2009年1月8日中午12时09分
本文提出一个问题:如何用C语言接收被调用函数返回的数据块?
答:调用者分配内存,然后传递一个指向调用函数的指针。被呼叫者填写数据。
简单来说:去超市要自带购物袋。
发布者:
D.K (13)
2009年1月8日上午10点19分
一开始我以为指针的值等于指针指向的地址。
后来发现两者不对等。
我还是没搞清楚这两者的关系。
只有当指针数值的地址包含指针头数据时,才能到达指针实际数据的地址。
有没有一个语法或者函数来获取指向数据长度的指针?
发布者:
dvaknheo (12)
2009年1月7日晚上11点50分
我觉得很困惑,但又不是很懂。我说的是二进制数据的描述?
bioware的很多游戏文件都是用二进制描述的,比如。tlk,从Bod1开始就没有淘汰过。它可以描述文本(字符串)、配音文件(字符串)、时间轴(浮点)、颜色等。它的实现很简单,就像元数据一样。首先用一个数据结构来描述数据结构的结构,然后逐字节填充。
发布者:
黑色 (11)
2009年1月7日下午8点13分
我们公司大部分模块之间的数据接口都是类似fread的方案。但是,如果你看RADVISON的代码,你喜欢传输分配器。
GLib有点像用C写C。
发布者:
zelor (10)
2009年1月7日下午6时26分
分配器还可以“要求解析器生成的数据结构放在连续的内存空间中”。啊,只是真的有点奇怪。明明有分配器,却规定只能调用一次-_-!或者派一个类似realloc的发行商?
发布者:
隔壁天堂 (9)
2009年1月7日下午05:25
呵呵,最终解决无忧。时间真的很宝贵。省下来的时间可以用来泡妞,灌溉水。这就叫生活。
发布者:
lbaby (8)
2009年1月7日下午2点35分
Pool是一个内部解决方案,可以暴露而不暴露。否则,你需要记住两件事:1。结构的地址,2。游泳池。并记住它们之间的关系。
如果要用,最好和其他部分正交。例如,首先创建它,然后将缓冲区指针传递到池中。
至于pool的拉伸能力,这里就不用说了。文中已有描述。如果你遵循能减则减的原则,你就能摆脱它。
其实上栈或者上栈的问题,即使是分布式的,也是可以考虑的:)只要你愿意搞点花样。
分配器的大问题是复杂的数据结构被分配了很多次,最终需要被释放一次。这限制了分配器的设计(它必须被设计成池,而不是C标准的malloc)
发布者:
云 (7)
2009年1月7日下午1点43分
虽然内存管理器等方案还不错,但是从KISS来看,内存转移确实是最简单最灵活好用的。(注意,冯云一再强调,内存可以在堆中或堆栈上划分。似乎转移记忆是达到这个目的的唯一途径。)
发布者:
天堂的隔壁 (6)
2009年1月7日下午12:45
哈哈,新年快乐,特别感谢上周冯云大哥帮我画的豆浆机。豆浆好喝,呵呵。
平时一直用C语言,我来说一下:
1.c用来做一些对底层和性能要求很高的事情。如果性能不是很重要,就没必要用了。
2.c可以传递到结构上。对于不定结构,你可以使用空指针。
4.对于nginx这样的代码,可以借鉴一下里面的池的概念。也就是说,为具有不同生存期的对象定义了不同的池。在对象的开始,分配一定量的内存。当对象处于活动状态时,可以从该池中分配内存。如果还不够,请扩展这个池。当一个对象死亡时,释放这个对象的池。事实上,malloc负责pool的底层实现。但是,它的具体环境是session等具有特定生存期的对象,这与程序的原始设计有很大关系。如果是一般的内存分配,这样的事情是做不到的。
发布者:
尧卫斌 (5)
2009年1月7日上午10点43分
GLib的许多模块传递构造函数和析构函数指针,或者至少是析构函数指针。
发布者:
匿名 (4)
2009年1月7日上午9点10分
关于析构函数指针的引入,一般来说,内存是由内存分配器管理的,不同生存期的对象使用不同的管理器。如果只涉及内存的话,我觉得注册析构函数是很少见的。
发布者:
流放 (3)
2009年1月7日凌晨1点40分
我认为引入内存管理器是一个好方法。比如nginx这样的内存管理,有点类似Apache的apr_pool,但是更适合一般的应用。除了性能的提升,更重要的是使用更简单,最终统一发布,不容易造成内存泄露。
发布者:
流放 (2)
2009年1月7日凌晨1点28分
不懂?学习!
发布者:
星航 (1)
2009年1月7日上午12点23分
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。