Getting Familiar With C

本文年代久远,其中的表述和倾向可能与现在不同,请留意时效。

本文档由 Markdown「语言」强力驱动


No.0x00. 为什么要学 C 语言 & 什么样的我最应该学 C 语言?

  • 首先 C 语言是一种高级语言,同时它又非常接近底层,C 的编译器是目前世界上能生成最接近汇编极限效率的编译器,学习 C 语言可以了解到很多现代编程语言的特性,同时又能对硬件机制产生一定概念。

  • C 语言的「抽象[1]方式」较少,且没有对内存操作进行高级封装,这意味着开发者需要自行处理数据在内存中的传递,同时也意味着学习者能学习到大量数据与内存的细节,这对以后更高级更抽象语言的理解会很有帮助。

  • 一般来说,一个合格的 programer 需要由内而外地了解程序世界的前世今生,C 就是这样一个契机,尽管它门槛比较高,但跨上去之后也会成为一个比较高的平台。but ofcoz,初学者通常也只关注 语言本身 而不是它带来的其它什么意义,所以我们也会推荐web初学者去看html或者php,对于更注重算法而不是程序实现的py(比如数学生物等非计算机的专业)会先学习脚本(如python)或matlab.


No.0x01. 我应该用什么工具来学习?

  • 首先请弄清编译器IDE(集成环境)的区别。无论选择什么编译器/IDE,基本原则都是 越新越好,只需够用 。编译器常见的有GCCclang和微软的C/C++编译器(msvc),前两者是开源世界(GNU/LLVM)产物,多见于各式各样的小型IDE,后者只会被附带于微软的Visual Studio(VS)中。以前常说的VC或VC6实际上也是Visual Studio 6.0的一个套件(那时候各套件是分开卖的软件包/光盘)。GCC由于是开源的,所以更新比较快 飞快!,对于新的语言标准支持也比较及时,而微软的编译器……你就只能等它发布下一个VS了。所以鉴于轻量时效的原则,推荐大家尽量选择GCC编译器的IDE。

  • 好几年又过去了(本文的第一个版本在2014年创建),原先的推荐列表早已经过时了(如果你想看看以前的说法,在文末)。现在大家电脑的操作系统已经普遍更新到 Win11,Linux阵营也迎来了一大波强力支持(还有WSL),随着各大商业公司逐步对Linux产生重视,特别是对桌面环境产生重视, 我们现在已经有更好的全平台IDE啦!以下是推荐IDE/编辑器的列表:

    • CLion全平台):来自伟大良心的 Jetbrains 公司的C/C++ IDE,地球上最专业的IDE生产商,选他们没错的!而且作为一个 收费商业软件 ,Jetbrains全家桶有针对学生的 免费使用 授权。只要学校提供.edu邮箱,就可以一直免费使用。具体办法百度一下jetbrains 学生就有相当多结果。但也有缺点,对系统要求比较高,32位系统似乎无法体验了。并且,这玩意只有英文没得汉化(笑(喜大普奔,现在有中文语言包了)。另外clion上来就要接触cmake,好在创建工程还可以先不关心它。
    • VS CodeLinux,Mac):微软出品的代码 编辑器。 不过它只是个 编辑器,并不是IDE,要生成可执行文件还需要不少命令,对于初学者来说有点难度,但未来技能储备足够以后会发现这是个相当不错的编辑器。如果真要用它学习 C 语言,推荐一下这个插件
    • Sublime TextLinux):也是一个编辑器,亮点是插件众多速度较快,但称不称得上好用就见仁见智了,推荐度一般。
    • Visual Studio 20xxWindows):自从VS2015将以往的Express版本升级到Community版之后,免费的Community版已经完全能满足所有的日常代码需求,不必再到处找盗版激活码了。虽然登录微软账号可能有点看网速和运气,但一般不至于运气太坏。VS有个缺点是功能太多,上手会迷路(每年都有60%左右的人会被挡在安装和创建工程之外!)。知乎上的这个回答基本包含了运行起helloworld的全部关键点,需要安装的部件和创建C代码的步骤都在图上了。
    • XCode 这种东西只有土豪才玩得到,而且App Store里面拖,无需介绍了吧!
    • QT CreatorLinux):qtcreator是一个开发qt程序的IDE,当然也可以用它写非QT的普通程序,在新建项目的时候选择Non-Qt Project(非QT项目)再选择Plain C Application(简单C应用程序)即可。 QT6 笔者从来没了解过,所以不敢放在推荐上了。
    • 到目前为止,各平台都已经有了首选的开发IDE,Linux/Mac->CLion,Windows->VS,所以基本上遵循这个原则就好了,能少走相当多弯路。上面已经引用起来的旧文档部分也可以稍微参照摸索下,对于想要学习Linux的同学们来说,熟悉gcc命令行以及makefilecmake的编写方式还是相当重要的(还有vim/emacs的用法 __(:з」∠)_)。
    • 顺带一提,在Windows上可以给vs安装Jetbrains的增强插件。vs之所以被戏称为「宇宙第一IDE」,是因为「宇宙第二」仅仅是它的一部分 __(:з」∠)_。

No.0x02. 关于 C 语言标准我有很多疑惑,为什么书上的和我的工程不一样,为什么书上的代码编译不通过

1. void main()与int main(),还有main()有什么不同?main函数括号里的东西是什么?为什么有时候有有时候没?

  • 首先,没有void main()这种东西,无论书和教材怎么教,请先形成个概念,没有 void main() 这种写法!
    • 至于这种写法是怎么来的,StackOverflow上有串专门的讨论,但没得出结论(有人说罪魁祸首是微软),不过可以肯定已经误传非常久了。
    • VC6可以 不报错地编译 (注意我的表述,我不会说它是正常的)void main(),而早期绝大多数程序员都在用VC6,并且编写教材或书本的人也一定在用VC6,结果导致很多没有弄清这个问题的人把这个问题一代代地传播下去了。(谭浩强是不是还这么写? 最近看到一个用vs2019的pdf,上来还是void main,吓了一跳)
  • 为什么不能这么写?
    • 因为标准只定义了两种写法,int main(void)int main(int argc, char **argv),后一种也可以写成int main(int argc, char *argv[]),两个写法等价。
    • 因为事实上,一个程序真正的入口点不是main(),从程序入口到main()函数,操作系统要在背后完成大量的准备工作。(比如调用CRT中的mainCRTStartup(Windows)或libc(glibc)中的__libc_start_main(Linux));如果main()不返回一个值,在程序退出时调用main()的_准备函数_将收不到返回值,这可能破坏栈平衡,甚至引发内存错误。
    • 因为你可能会给程序运行环境返回一个随机值,如果想要检测你的程序是否失败,或者在脚本中调用你的程序,那么后续的指令将无法通过你程序的返回值来判断其执行结果到底是成功还是失败。
  • 括号里这堆东西包含程序启动时的路径和命令行参数,如果不需要接收参数,可以写 int main(void).
  • 那么能不能写int main()
    • 很多人这个点搞不清楚,这个问题在百度上只能找到同一段相同的文字,想弄清楚得google.
    • 在C++里,foo()和foo(void)是相同的,所以C++标准中也可以写int main().
    • 但是在 C 语言中,foo()表示可接收任意参数(参数个数也任意)的函数,而foo(void)限定调用时不能传入任何参数
    • 这篇文章详细解答了两种写法在两种语言中的区别。
    • 所以对于main函数这种由系统来调用的函数,一般不可能传入错误的参数,写成int main()还是int main(void)都没有问题,但对于你自己定义的函数来说,由于foo()能接收任意参数,所以也许能开发出某些奇妙的高级写法,但一般很少见,所以一般不作区分。
  • 所有关于这些问题的资料链接:

2. 各编译器都支持什么标准?相互有什么区别?

  • C 语言标准最先由ANSI(美国国家标准学会)制定,然后被ISO(国际标准化组织)接纳,现在的 C 语言标准是由两个组织共同维护的,ANSI C和ISO C是同一回事,所以也叫Standard C(标准C)。
  • C 语言标准的演变参照知乎,对于各版细节无需知道太多,只要了解C89是第一个正式的C标准就好了,在这个标准以前写成的C程序,被称为Legacy C.
  • VC6不支持任何版本的C标准,哪怕C89,这也是为什么 现在极不推荐用VC6 大家一直对其嗤之以鼻。
  • 最新版的VS2013对于C99已经能比较完善地支持,官方文档给出了目前的支持情况;本文简要列出了还不支持的features.
  • GCC4.5版本已经能支持大部分的C99特性,而GCC4.9C11的部分特性也已支持,剩下多线程和带边界检查的安全版本函数[2]尚未实现。
  • 不过需要注意的一点是,在不对编译器指定使用的C标准时,GCC默认采用自行扩展的GNU C标准,在IDE中应该会有类似的设置。
  • 其实绝大部分写C的人也不关注C99以后的新东西,C的新标准也大多是一些解决瑕疵的缝缝补补和对编译器现有扩展的一些接纳,网上都找不到一个能对比编译器支持程度的列表。如果查阅 msvc/clang/gcc 各自的文档还是能发现一些细节上的不同,不过等有能力发现它们的时候也已经早有能力认识它们,不再需要本文指引了。

No.0x03. 所谓的代码风格是什么,为什么需要良好的代码风格?

1. 代码为什么不是越短越简单越好?

  • 初学 C 语言,最喜欢写的代码可能是这样的:

    #include <stdio.h>  
    main()  
    {int a,b,c;  
    scanf("%d%d",&a,&b);  
    c=a+b;printf("%d",c);  
    }

    因为它比较短,所有的变量都只有一个字母,main函数只需要一个单词加括号,简单。

  • 但是学着学着,你写的代码就会变成这样:

    #include <stdio.h>  
    main()  
    {void func();int a,b,c=10,i,j,k;  
    char d='*',m[]={""},*n;  
    scanf("%d%d",&a,&b);  
    for(i=0;i<a;i++){if(b<c){m[i]=b;}  
    else {n=&d;scanf("%c",n);while(b>0){printf("%c",*n);  
    }}}//……抱歉编不下去了

我瞎编的过程中已经分不清哪个括号是哪一层的,那些变量有什么用是什么类型的了,别说你们自己编的时候 你们回过头修改程序的时候。

  • 一个程序是不太可能一次写完的,特别是越来越大型的程序,当你的程序达到100行(上面这个糟糕的例子只有10行)的时候,这种写法已经非常要命了。不能一次写完,意味着你的代码会被反复地阅读很多很多次,并且可能会被修改很多次。如果按照上面这种写法……每改一行代码都是一种煎熬。
  • 其实有一种比赛是要求选手用最短代码来完成任务的,这种比赛并不看重代码风格,但是非常看重选手挖掘奇技淫巧的能力,**选手需要熟悉各种优化代码长度的方法,除了压缩空白,缩短变量名,还要对程序运行编译机制有大量的了解。**举个例子,加减法你会写吧,请给一段计算a+b的代码,输入输出都在10以内,看你写的能不能比这个[3]短?

2. 怎样才叫良好的代码风格?

  • 自己和别人都能很容易看懂

    • nCount;szInput;fResult;像这些变量名都是单词的变化,不必知道它们出现在哪都能一眼看出是做什么的。
    • if(sum!=nInput) return FALSE;不必看上下文就可能猜出这个程序一定有将 什么输入什么的和 比较 的功能,也许是一个校验过程,也许是一个判断加减法计算的东西,八九不离十。但如果你写return x==n? 1:0则既难看又意义不明。

    上面这种变量命名方法叫匈牙利命名法。当然严格按照这样的命名方法也会出问题,很多大型项目的代码规范文档不提倡这种命名方式。所以还是遵循唯一正确的标准:自己和别人都能很容易看懂

  • 严格遵循代码缩进

    • 常见的代码缩进有这种
    #include <stdio.h>  
    int main(void)  
    {  
        char *pszHint="括号的下一行代码缩进";  
        if(1)  
        {  
            printf("大括号与上一行对齐,%s",pszHint);  
        }  
    }//这种格式多见于VS  
    
    • 和这种
    #include <stdio.h>  
    int main(void){  
        char *pszHint="括号的下一行代码缩进";  
        if(1){  
            printf("左大括号放在一行结尾,%s",pszHint);  
        }  
    }//这种格式多见于其它IDE,结构比较紧凑  
    
  • 不管哪种风格,请记住一个要点,语句块的开始不要跟大括号放在同一行,这是为了能清晰分辨语句块的开始与结束,参见之前那个糟糕的例子,你连现在在哪层嵌套里都弄不清。

    • 现在已经见不到「用记事本写代码」的初学者了,但还是会常常遇到不知道使用代码格式化的人,所以:
      • 如果你用vscode,右键,菜单有个「格式化文档」,记住它的快捷键
      • 如果你用VS,顶部菜单,编辑-高级,有个「设置文档的格式」,记住它的快捷键
      • 如果你用clion,顶部菜单,Code,有个「Reformat Code」,记住它的快捷键
      • 如果你在学习Linux或在用字符终端来写代码,clang-format是你必须了解的东西(在vs和clion中同样能用)
  • 严格区分每个变量的作用域,不要随意定义或使用会跨界的变量

    这个问题有点抽象,最直观的例子就是i,这个特殊的变量名几乎已经成了for的一部分,大部分的for里用作指示循环次数的变量都是i。结果就是,你很可能会写出

    int i=0,j=0,k=0;  
    // scanf,printf,判断scanf进来的东西,etc..  
    for(i=0;i<10;i++)  
    {  
        // do sth.  
        // ...  
        for(j=0;j<i;j++)  
        {  
            // do other things  
            // 很多层for  
            for(i=0;i<k;i++) // oops,你忘记i用过了  
            {  
                // 你以为你写了一个普通的循环嵌套,但外层并不能循环起来  
            }  
        }  
        // 为什么中间的循环跳过了?!  
    }  
    

    远古C标准中,变量的定义必须写在一起,在所有其它语句之前(现在早已没这个限制了)。所以早期教材的例子中会早早地定义i,然后各种输入输出加判断,然后才正式开始循环。初学在抄代码改代码的过程中经常会写出「里层循环改了外层i」这种bug。类似的问题在遇到指针时会更加严重:你可能会想不起来到底哪个函数修改了指针指向的内容,或者意识不到指针传给下个函数会发生意外的变化。有很多“莫名其妙弄不清”的问题跟指针超乎想象的作用范围有关。所以最好让需要的变量定义在最晚的时刻,尤其是,不要定义在用到它的那层大括号(即作用域)的外面


No.0x04. I know a little about C++ …

You know little about c++.

  • 在本文的既往版本,是没有提过 C++ 的,目的是为了缩小初学者的关注范围,以免迷路。但关于「C++ against C」的讨论每年都会见到许多问题,其中不少还是有误区的,所以似乎有必要涉及一下高频话题。

1. 「C++ 是 C 的超集」到底是什么意思?

  • 首先C++并不是C的超集,两种语言事实上是两个相交的集合,C++ 并不能包含C,所以 这句话是错的
  • C++ 由 C 分化而来,语言的分化其实是挺频繁和常见的。C#, Java, JavaScript, D, Go 都能看到 C 的痕迹,但 C++ 跟它们不一样的地方在于,你能写出一段既是 C 又是 C++ 的程序并正常编译运行,其它语言并不这样。不如说 C 和 C++ 都是「某种可编译运行的语言」的超集,这也是 C++ 要「竭力保持兼容」的部分。这部分占 C 语言的比重挺大,但对 C++ 来说大概……只有一半吧。

2. 「我已经学到 C++了,不用再回过头学C了」?

  • 其实我们想让大家学习C的原因不仅仅是因为能实际写程序(而且计算机专业必修),还有一个很重要的理由在于去了解C程序所使用的内存模型和数据组织方式(本文开头就提到了),这不仅对后续安全方向的学习比如逆向也好、二进制漏洞挖掘也好都十分有帮助,对各种高级底层的编程能力比如数据库引擎、操作系统、网络等也都是必要知识储备。许多掌握到C++的同学是从OI等竞赛学科上来的,竞赛选择语言的着重点仅仅是程序执行效率和编程效率的权衡,出于这种目的的话往往只了解语言的一小部分就足够了,甚至都窥不见计算机世界的一角。
  • C和C解决问题的方式往往很不同,因为C有丰富的语言工具可供选用,而C只能在内存组织上动脑筋,因此学到C也不能说明能使用C解决所有问题,还是会有遗漏的东西。一个最明显的例子是「泛型容器」。Cstd::list可以把任意大小和类型的数据组织成链表,但C怎么实现?能写一个 struct List 并用它串联任意类型的数据吗?
  • C 直到今天仍然是世界上 最流行、最通用的定义二进制接口标准的语言。 诸如 Rust, Go, 甚至 Java (JNI), Python(各种机器学习框架)等各种「高级语言」,当它们想要使用能直接调动硬件能力的时候都必须通过一个中间层来代理或解释高级逻辑。这个中间代理层的 事实标准 就是 C 语言。各种高级语言都会想办法提供与 C 语言中的函数相互调用的机制,这使得基于 C 语言定义的二进制接口(ABI)具有非凡的兼容性,这是连 C++ 都望尘莫及的。可以说,但凡只要写的程序想要引入其它语言实现的功能,或者一个程序要用到多种语言合作实现,就必须对 C 语言有相当程度的熟悉。C++ 并不能作为这个目的的替代。

3. 似乎总有谜语人在说我看不懂的 C++ 的梗

  • C++ 是一门奇技淫巧特别多的语言,而且语法非常变态(crazy)(←如果看不懂这个问题,有个简单点的 show off比较直观)。不管你认识它多少年,总能遇到一些惊掉下巴的「特性」(真的是特性)。而且世界上总有其它一些疯子要往里塞更奇怪的东西,所以无论什么掌握程度的人,都能看到 C++「张牙舞爪的扭曲触手」并充分品尝它带来的挫败感。
  • 另一方面,C++ 提供的基础设施又十分朴素,以至于自诩专业写了很多年代码的人想要真正开展工程时还会遇上更多麻烦,比如依赖管理、工程规范约束、没法修的内存bug、测试框架、热调试、构建速度……每个子问题都足以 drive you mad.
  • 「为什么我的代码跑不了」在写任何语言时都很常见,但「为什么我代码写成这样都能跑」则非常罕见 —— 写C++ 时除外。

No.0x05. 更新说明和后记

(2021-8-31)

  • 本文最早完工的时间是2014年9月的开学后。当时我发现大家会问会讨论的问题来去都是那么几个,于是想找一找资料「权威地」把这些问题统一解答并存档。写文档的过程中又发现有许多的相关概念也许可以一并提供解释,所以给大量的关键词加上了链接。我那会甚至刚接触 markdown (也还不流行,挺geek的),第一次正儿八经写文档,于是给文档塞满了各种各样的样式,还在开头放了「由markdown强力驱动」这样的噱头。我不知道今天的新同学之前认不认识 markdown 以及那些可以链接出去的关键词,也许很多已经成了常识,也许没有。
  • 有个概念叫知识的诅咒(虽然我觉得这说法的命名十分中二且词不达意),表达「人们掌握知识后无法想象未掌握知识时的情形」这样一种现象。我至今仍记得幼儿园学写字的时候「来」的两点我怎么都理解不了「向中间斜」,我写了十几次的「//」「、、」「八」才终于写对。虽然我还记得那个场景,但早已无法理解为什么这么难学。有非常多的「新手指引」都会犯这样的错误——比如教材。我自学的时候「数据类型」这章卡了两周,无奈跳过去之后一直看完数组(后面是结构体和指针)总共也才花了一周。
  • 初写本文的时候我脱离「初学者」也还不远,所以我相信它本身能包含不少初学者的视野和关注点。在又回头看的今天,我想说一说「有些不寻常」的地方:
    • 大量概念链接都是百度百科。今天我肯定会直接google,stackoverflow和wikipedia都容易找到准确的答案。但这样的信息渠道并不是一开始就能获得,因此尽量只用到国内的信息源。
    • 引用文基本都是来自中文环境,甚至还有csdn。同上,虽然csdn,但所选文章也已经尽量找了最早最靠谱(maybe?)的。其实也许看本文的人对csdn的糟粕并没有认知,但去掉csdn/cnblog这样的平台就一篇也找不到了,所以粗暴地筛除它们是不可取的。你大可对知识的呈现方式和提供者提出批判,但没理由排斥知识本身。
    • 有一大篇幅在讲语言标准,我今天甚至都想不到会关注这个。Anyway 这是最早写的时候关心的话题,因此我不想(不敢)移除它。这一段正是「掌握知识后无法想象」的部分,因此我选择一直保留这一部分。
  • 文中有一个推荐列表,显然它是有时效性的。每次修订这个推荐列表都有不小的变化,我尽量用划掉的方式保留它的历史样貌,这样能看出「生态环境」是怎样演化的。比如最早的版本居然还有Eclipse这个候选项,现在想找到这个词都难。
  • 还有一些修订时我发现的东西,如果不记录可能会有点可惜。因此:
    • 3大编译器,以前是2大,没有clang。本文初版时clang可能仅仅还是试验品,上次修订也还不算主流,而今天连vs里都可以选用clang作为编译器了(但不能选gcc/mingw)
    • msvc,以前的评价是更新慢,标准支持得更慢。因为以前(尤其是只有vs2013的那时候)vs是没法滚动更新的,只会在换代时更换新的编译器。
    • 2014年似乎知乎还未进入视野。我很诧异许多概念没去知乎上面找……所有知乎链接都是2021年才加的
    • 如果我没记错的话……应该是在 2017 2019分别更新过一次,平台居然没有历史发布版本的记录,有点不可理喻。

(2023-8-21)

  • 补充了一点关于ABI的简介。
  • 2023年开学期,安稳保存了近十年的原文的 host 平台 www.zybuluo.com 遭到了神秘铁拳的打击,因此我为全文重新修改了格式,以便更好地遵循其它 markdown 的规范。

引用和脚注

来自2014年的推荐——

  • 对于Windows7及以下的用户,可以选择CodeBlocks >> Dev-cpp >= 最新版本的VS >> C-Free > VC6,codebloks是一个比较新的开源项目,功能比较全也有汉化包,编译器也很新,强烈推荐,而devcpp和cfree已经很久不更新了,不过devcpp使用mingw的编译器,IDE旧了点就旧了点吧,不影响;CFree则连编译器也很久没更新了,兼容性也较差,已不太推荐,不过还是比**bug遍地的16年前的什么标准都不支持的又旧又破烂的VC6好些。VS摆在中间的位置,因为实在太大了,而且会有60%使用VS入门的初学者被拦在创建工程** (!) 上。
  • 对于Windows8及以上的用户,为避免麻烦请直接去下载VS2013,Win8本来对旧软件的兼容做的就不好(纯64位内核),安装codeblocks的时候可能还会遇到环境变量的问题,不过如果你看到这个词心中有数,还是去下CodeBlocks,完全没头脑,请下VS,然后参照文末引用的文章来创建简单的C程序工程(需要翻译不?)
  • 如果你想学习Linux并想在Linux上写C,你已注定成为一个苦逼的指挥官(command-er),希望这点东西能帮助你快速打造一个还看得过去的vim. 所幸Codeblocks是跨平台的,意味着它也能在Linux下使用,当然你还可以选Eclipse+CDT,不过这么累赘的东西……还不如leafpad记事本手打了。Linux上学习C唯一优越的地方就是编译非常方便快速,不过调试会蛋疼死你……

Using Microsoft Visual Studio for Simple C Programs

To edit your C program:

From the main menu select File -> New -> Project

In the New Project window:
Under Project types, select Win32 - Win32 Console Application

Name your project, and specify a location for your project directory
Click ‘OK’, then ‘next’

In the Application Wizard:
Select Console application
Select Empty project
Deselect Precompiled header

Once the project has been created, in the window on the left hand side you should see three folders:

  • Header Files
  • Resource Files
  • Source Files

Right-click on Source Files and Select Add-> New Item
Select Code, and give the file a name
The default here will be a file with a *.cpp extension (for a C++ file). After creating the file, save it as a *.c file.

To compile and run:

Press the green play button.

By default, you will be running in debug mode and it will run your code and bring up the command window.

To prevent the command window from closing as soon as the program finishes execution, add the following line to the end of your main function:

getchar();

This library function waits for any input key, and will therefore keep your console window open until a key is pressed.



  1. 「抽象」这个词本身比较抽象,在计算机领域,它约等于「创造新概念」。拿「集合」这个大家都熟悉概念举例,有些语言中创造的「集合」能参与运算、能派生新概念(比如「是一种特殊集合」)、能限制内部数据满足数学上集合的定义、甚至还能定义在编译期。而C只能创造「定义集合操作相关的函数」,即所谓「抽象方式少」。 ↩︎

  2. 参见百度百科:C11与C99的对比 ↩︎

  3. main(n){gets(&n);putchar(n%85+5);}是不是很迷幻?看得懂不? ↩︎