百姓大小事,一呼百应!

百姓网 | 百姓知道

音乐舞蹈

为什么说指针是 C 语言的精髓?

我实在想不出指针能有什么大作用 难道是我悟性太差-_- 麻烦举几个实例 灰常感谢

2 个回答

  • 张天行 | 2017-10-15 16:27:22

    简单来说,因为C就那点破feature,如果你把指针干掉,那这语言就完了。相反,如果你干掉struct,干掉union,干掉数组,甚至你把if-while都干掉,留下malloc和goto,则最多就是程序难写一点而已。

    所以这就是为什么C语言的精髓是指针了,因为他只有指针可以用了。

    本问答由张天行提供

  • 张天行 | 2017-10-15 15:51:35

    no pains, no gains


    对C来说,指针、无越界检查等等是一切痛苦的根源;但这些痛苦并不是白白付出的。

    可以和汇编比效率(甚至可以做到“编译器自动优化的代码比80%汇编高手手工优化的汇编代码都好”),就是这些付出所应得的收获。


    事实上,任何一门设计合理的语言,给你的限制或提供的什么特性,都不是没有好处/代价的。
    准备在哪方面付出、想要得到什么,这就是选择语言的依据,也是为何会有这么多种语言的原因。

    ——————————————————————

    具体的说指针有什么好处……这很难。要么挂一漏万,要么……其实别的语言也有类似特性,并非C所独有,至多是……有些限制或者稍微多绕了几步而已。

    真要说清楚这个并不容易。或许,其实你应该问的是:“C究竟有什么优势?指针在其中起了什么作用”?或者,C这个老掉牙的奇葩究竟有什么独门秘技、奇特思想,以至于它现在还能牢牢占据编程语言排行榜的首位?


    那么,这里就笼统的、从理论层面答非所问的胡扯几句。

    ——————————————————————

    从哪里开始呢?先说思想吧。


    软件开发/设计行业有这么一句话:没有什么是不能通过增加一个抽象层解决的。

    这句话很对……但抽象层并不是免费的。这点就很少有人想过了:一旦你和什么东西之间被加上了一个抽象层,那你就一定得在每次访问它时受到某种限制、或者付出某些代价


    换句话说,一旦和某个实体之间有了抽象层:
    1、你必须间接访问该实体(如果实现的很好,有时候的确能够无需付出性能代价;但并不能保证任何时候都无代价)
    2、你必须以抽象者所期望的方式访问该实体:即便你知道该实体其实是什么、在处理某些问题时用不着七拐八绕,你也得七拐八绕着访问它。即:封装有时候反而会增加复杂度。

    越是底层,抽象就越难做。因为,其一,稀奇古怪的需求实在太多,总有你想不到的地方;其二,如果你抽象了,那么就必须保证任何情况下,这个抽象都得真的像它所定义的那样工作;而这个往往意味着很多方面的代价。


    后一句话可能有些难以理解。我来举个例子。
    比如说,数组就是对“一系列连续内存单元”的抽象;它对外表现为“一个固定大小的容器”。

    现在问题来了:如果有人访问第(数组大小+1)个元素,那么你就必须阻止他。否则,这个数组就不像容器了——用术语说,就是你没有封装好它,导致细节暴露出来了。

    于是,每次有人访问数组,你都得先检查待访问元素的下标是否越界——这就导致每次访问,你都必须付出几倍的时间代价。


    而对C来说,数组就是一个指向一片内存区域的指针……它并不去封装这个概念;恰恰相反,它鼓励你去了解藏在表象背后的东西。

    于是乎,举例来说,在大量文本中搜索匹配某个模式的字符串(即strstr函数),如果C用3秒能搜完,其它语言再快可能也得9秒。因为每和一个字符比较,其它语言都要多两次索引越界与否的检查动作。

    当然,这个好处并不是白捡到的。C语言用户因此而付出的代价,就是防不胜防的缓冲区溢出问题……


    再看一个例子。

    假设我们实现底层网络包的识别/分析工作(就好像wireshark那样),我们需要:
    1、分析包的来源、去向、类型、控制数据等信息
    2、分析TCP/UDP包的内部信息可能是哪个已知协议(http、https、msn、ssh等等等等)、并输出分析结果(如果无法识别,以16进制数字显示)
    3、可以很容易的扩充支持的协议(比如加入QQ、WOW之类协议的支持)

    如果你用java……尤其是只知道设计模式的那些人,想象下这种程序设计起来得有多麻烦、处理起来效率得多低吧。


    但如果用C,这个工作是意想不到的简单清爽……
    1、按照IP报头规范,读取正确偏移位置的几个字节,识别出包的来源地址和目标地址
    2、根据报头某个位置的标记,识别这是一个TCP包还是一个UDP包
    3、以便于阅读的方式,输出TCP/UDP/IP头携带的信息
    4、拆分出载荷,把载荷首地址、长度等信息丢给一边蹲着的协议分析器链
    6、第一个协议分析器按照已知的网络封包协议定义,识别载荷是不是自己能对付的那种协议
    7、如果是,分析之,并输出分析结果;否则,丢给下一个协议分析器处理
    8、如果所有协议分析器都无法识别,则该包可能是私有格式,按默认格式显示

    整个流程甚至可以直接在指针指向的那片内存上进行,无需任何复制动作——直接就是真正的0 copy。

    其中,协议分析器是一个函数指针,该函数接受三个参数:指向待分析数据头部的指针、待分析数据长度、返回分析结果的数据结构指针;返回值为一个bool值:true表示包已识别,不需要继续在协议分析器链上传递了;false表示无法识别,继续传递给下一个协议分析器。

    至于在协议分析器内部,你只需:检查长度是否足够;把传来的指针强制类型转换成自己支持的数据结构(如 struct msnHead之类);检查数据结构中各项的值是否正确;如果正确,按标准格式输出到分析结果。

    而添加一个协议分析器,只需如此定义一个函数,然后调用register接口,把它挂接在分析器链末尾即可——无需考虑构造/析构时机、无需考虑内存分配与回收、无需什么类工厂、反省等等等等。


    有的时候,数据就是数据、函数就是函数。你封装成类,反而棘手多了。

    尤其是这类偏底层、偏数据和算法方面的应用,和高层的UI开发不同,类经常是个累赘。当别人还在为类体系如何设计、如何利用好反省机制烦恼时,你惫懒的一个msnHead * pHead = (msnHead *) pData,事情已经完美解决了——用C,就是这么任性。


    C并不仅仅把数据当作数据,内存当作内存;它甚至允许你把硬件看成硬件——赤裸裸的插在总线上的、未加封装的硬件。

    你完全可以取局部变量的地址、然后顺藤摸瓜,把整个栈空间打印出来。
    当你玩无锁编程、遭遇ABBA问题时,它允许你把指针看成数据,然后给它附加一个TAG,通过检查TAG来规避ABBA问题;完了需要索引数据时,重新去掉TAG,刚刚还在随意揉捏的数据就又变成了指向合法位置的指针。
    在古老的DOS时代,你可以直接根据中断向量表的起始地址、中断向量号直接计算指向该中断程序的指针所在的地址——然后,或者跟过去把操作系统的秘密全部打印出来、或者用自己写的函数替换掉系统中断服务。
    或者,你可以直接向硬盘所在的IO口写命令字,控制集成在硬盘电路板上的单片机系统执行任务。
    你也可以直接访问显存,然后手动指挥显卡显示哪个页面。和访问普通内存没什么两样。

    只要是硬件允许你做的,你都可以做。


    所以,C的好处就是:没有多余的抽象/封装;一切以硬件界面为准,什么东西是什么,它就是什么。你可以在其上无限的发挥想象力——哪怕搞个自己的类体系也不是是小菜一碟——没有任何限制,没有任何思维负担。


    所有这些,都是围绕着指针实现的。

    当然,这样也不是没有代价的:对java来说,一个对象是什么,它就是什么;一个类说我保证什么、你不能碰什么,你就只能照做。这是语言提供的保证,所以你很难做错事。这就是封装的好处。

    但对C来说,你的所有要求都可能被人“惫懒”的忽略掉,除非你压根不给他碰你的数据;别人给你一堆数据,这些数据也很可能是通过某种方式“惫懒”来的,你最好不要随意动它;操作系统源码里,看起来平平常常几行代码,很可能访问的是了不得的区域;有时候,除非阅读源码、遵循各种编码规范并且祈祷别人也遵循它们,你得不到任何保证——得到“无比犀利、无比直接的解决某些问题”的能力同时,你可能也得到了无比犀利、无比奇葩的BUG……

    要用好C,你必须能够数据的本质、必须能看透别人代码的意图(并不会编译器帮助你、告诉你什么不能碰)、必须知道自己写下的每代码意味着什么并自己为它所可能造成的任何side effect负责(所以对新手来说,单步执行并观察每行代码造成的所有影响,是入门所必不可少的一步):如果做不到,你就会变成团队里的麻烦制造者——在这些要求面前,精通指针只能算刚刚入门罢了。

    C是工程师为自己设计的语言。它是为那些对机器了如指掌的专家设计的。
    C的设计者并不认为需要对工程师做任何约束,因为他们知道自己在干什么。
    C并不是学院派语言。它并不打算为了贯彻什么什么理论、什么什么概念而设置什么条条框框——所以,它不会像pascal、java一样,为了纯粹的结构化/面向对象而排斥其他。相反,结构化是好的,它就拿来用;汇编操纵内存/硬件方便,那就不能丢;至于新兴的面向对象……内存就摆在你面前;C的老基友,unix/linux系,设计之初就贯彻的泛文件思想,就是面向对象的启蒙者之一:你居然还敢说C做不到?它只是没有直接喂到你嘴里而已!

    这就导致,在偏基础的应用领域,C是当仁不让的不二之选;甚至,对能够很好驾驭C的人来说,哪怕是图形界面之类看似不适合C发挥的领域,使用C也不需要支付什么额外代价:你说什么什么高级机制?不就内存里那么点事嘛,随手撸一个GTK给你看看——嗯,除了不太容易找到可靠的人来接手/维护、以及开发商业软件雇人比较贵且困难外,还真没多大麻烦(这已经够麻烦了好嘛)。

    它只管提供最犀利的武器,你来负责用对、用好、用精它们。


    但,这也导致C对使用者的要求居高不下。
    它是很强大,但太难驾驭。以致于它已经很难适应目前开发商业软件的诸多要求了。
    尤其是那些对速度并不非常敏感、相对更在乎开发成本(包括人力、时间、金钱等方面的成本)的领域,C的确太麻烦了——从雇人到开发再到维护,一条龙的麻烦。

    但C的设计目标并不是占领一切领域。自始至终,它都不过是一种最为贴近机器、因而操控起来最为方便的高级语言罢了。就好像python的目标只是方便好用的强力粘合剂一样。
    c是unix/linux的一等公民;但unix/linux并不排斥别的语言。恰恰相反,unix/linux系支持的语言种类才是最多最杂的,而且C可以极为轻易的和任何一种语言协作:一句话,不就是内存里那点事吗。

    这一堆堆语言,哪里合适,你就用;不合适,换别的:就这么简单。C的哲学就是看开一切,这点事你都看不开吗?


    没有什么语言是万能的——除了C++大概能算半拉“万能语言”(但是,有了万能的语言,却不存在万能的人,也没有什么需要万能语言的项目。所以近年业界对C++的共识是:它可以当C用、当C with Class用、当java用、当黑魔法般写泛型库用……但无论如何,别拿它当C++用。嗯,扯远了)——因为现实中没有免费的午餐。想得到什么,就必然要相应的失去点什么。


    编写一个C程序所需要付出的代价,常常是非常昂贵的——无论对开发者的要求、还是写出良好代码需要付出的努力等等方面,它都是代价高昂的;甚至可能是除了汇编外最贵的(当然,这里暂不考虑滥用全部特性/范式的C++)。

    付出了这么多……你买到的其实只有一样东西,那就是指针,以及借助指针而对你彻底开放的……其它语言想尽办法不让你看到的……“可怕”的底层细节。
    和其他热衷于“封装细节”的语言不同,它鼓励你这么做。
    它的哲学和unix一样:做一件事,把一件事做好——专一而精,不大包大揽,这不正是天然的封装吗?

    换句话说,在C的观念里,指针不是什么精髓,它只是一扇门,推开门,后面是整个世界

    这是C最独特的优势,也是这门诞生于上世纪七十年代早期的、老掉牙的语言,至今仍能雄霸最流行语言排行榜之首的……幕后“黑手”。

    本问答由张天行提供

* 本站部分内容来源自网络,仅作分享之用,侵删。