我为什么学C(1)---开篇

我的C语言

本章更新于 2015/6/21 11:36:01

正式接触编程,大概是一年连六个月吧,这真是一个令人着迷的领域

0x00-C语言前续工作

正所谓,工欲善其事,必先利其器,把握住当下最强大的工具,能让我们在学习的道路上少走许多弯路,多吸取前人的失败经验,能让自己快速成长,因为成长总是在消耗我们的耐心以及生命。

入门或者精通或者应用,不管哪一方面,对于一个编程语言而言,最方便的还是使用一个IDE作为你的有力助手,什么事IDE?通俗而专业的说叫做集成开发环境,这个通过字面就能理解到了,就是所有其他的事情都不需要程序员操心,你需要操心的就是写出代码,至于代码完成之后的一系列工作,都不需要你来管,IDE一键帮你搞定。

当然,会有许多前辈告诉你,如果你想理解C语言,那你一定要使用最基层的东西来写,比如XXX编辑器配上XXX编译器,晕头转向之后更加茫然,本来就支离破碎的小心肝,又被粉碎了一次,撇开那些与当下不符合的幻想,活在现实中,选择一个适合你的IDE,逐渐适应它。

  • 讲几个著名的IDE,并给出建议,利器第一步:

    1. 宇宙级的IDE: Visual Studio(2010~2015),之所以说宇宙级,因为这是市面上最强大的集成开发环境,由微软公司出品,但是放在开头不是为了推荐他,而是为了警示大家不要使用它作为C语言的集成开发环境,因为它使用的是微软公司自己定制的C++编译器,也就是说,你的C语言代码会在C++的标准下编译运行,这就是一个十分不好的现象,即便是C++我依旧不喜欢使用Visual Studio,因为它的C++编译器总是和普通的标准有所出入。

      原归正传,Visual Studio的确不是一个好的C语言开发IDE,所以请另外选择一个。

    2. 老牌IDE: DevC++,这又是一个大家耳熟能详,经常能在老师手里看见的C语言教学利器,但是,它是C++的IDE,记住C于C++完全是两个世界的人,虽然C++宣称能全面兼容C程序,但是有些东西依旧是有所区别,体现在语法的兼容性上,后文会有提及。那为什么大学老师喜欢使用它呢?因为一本由清华大学出版社出版的《数据结构》,让无数人为之折服,其中赫然写着由于性能我们不能拘泥于小细节,故对于C++的特性&引用,我们可以将其使用在C语言的语法中,就是这句话,让无数无知的学子扑向其中,再也分不清C与C++,看成谭浩强之后的,清华大学出版社又一诲人不倦的力作。

      所以,真爱编程,远离清华大学出版社,也请大家注意,不要使用DevC++这个IDE进行C语言程序的开发以及练习。

    3. 知名IDE: Code::Blocks,是一款非常优秀的开源跨平台集成开发环境,体积并不大,适合作为C语言的IDE,并且功能齐全,有兴趣的人可以深究,这是首推的C语言开发环境选择。

    4. 知名IDE: CodeLite,是一款非常优秀的开源跨平台集成开发环境,体积并不大,适合作为C语言的IDE,并且功能齐全,有兴趣的人可以深究,这是次推的C语言开发环境选择,因为使用起来稍微也有些额外的工作要做。

    5. 著名IDE: Xcode,是一个苹果电脑上的史诗级集成开发环境,虽然脱胎于C语言,但是由于某些原因,并不太建议使用其作为C语言的开发环境。

  • IDE的基本配置
    利器第二步是对所选的IDE进行一些基本的配置,以及小科普。

    1. 对于一个练习C语言的开发环境来说,选择合适的标准和编译器是很重要的,在Windows以及Linux操作系统下,我们还是使用GCC这个家伙比较多,开源,免费,且极其强大。当然你也可以选择clang,当然整个计算机领域中支持C语言的编译器并不止这两个,只不过这两个是开源免费,而且功能强大,十分适合作为个人开发以及无特殊需求的企业开发的选择。

      选择了编译器,我们开始讲标准:

      对于GCC 5.1以下的所有版本,都默认对C语言使用C89标准,但是我建议使用C99两者的差距,有一个极其明显的地方,便是for循环的使用

      /*C89:*/
      int i;
      for(i = 0;i < 10;++i)
      /*...*/
      

      /*C99*/
      for(int i = 0;i < 10;++i)
      /*...*/
      

      这只是其中的一种差别,但是C99需要人为手动的开启,但是很多人有疑问,为什么有时候没有配置什么也能使用后面的语法?吴老师告诉你,这是因为你用了C++的文件进行C语言的开发,就像挂羊皮卖狗肉的道理。

    2. 开启C99

      一般IDE的顶部都是一系列的标签,找到工具/设置,因为不同的IDE可能有不同的标签,总之在其中找到一个叫(编译器)Compiler之后,在其中的other option中加入以下:-std=c99,这便是开启C99的选项代码,完事之后保存即可。虽然说我们是中国人,但是毕竟这东西的外国人发明的,我们能看英文就看英文吧。

    • 至此,利器成功配置。

    • 当然最重要的还是内在,所以加油吧,虽然是一门很古老的语言,但是存在既有其道理。

0x01-C语言序言

倒是觉得写代码首先不是语法,而是格式,任何时候任何地点,要是自己的代码难以理解,要么你是故意的,要么你就是菜菜

一个难以被人理解的代码在我看来是没有太多的潜力的,但不排除故意为之的情况,也许很多人说这是强迫症,但是无论打开哪一个开源代码,你看到的都将是一个拥有规范的代码文件

也许有人说人不应该被限制,不应该拘泥于小节,但是当一个工程超过一千行,也许不用只需要不到五百行,就能完全暴露出代码规范的重要性,包括缩进,变量命名,接口存放,接口参数的规范之类,听起来似乎很虚谷歌代码规范(翻墙后查看)

在我看来C语言的内建语法真是无比简洁,几乎存在既有道理,简洁不代表着不强大,强大的某些地方在近来渐渐复苏的Lisp身上也有体现。

if, for, while, switch

组成了每个C程序的半壁江山

“ + “ “ - “ “ * “ “ / “ “ % “ “ = “

组成了各式各样的算法计数

“>>” “<<” “|” “&” “^” “~” “!”

让C语言有了更高效的算法以及更奇妙的思路

struct enum union #define return

而这些则让C语言在这乱世纷争中站稳了脚跟,并且一枝独秀

“{}” “()”

让代码不再无序混乱

“type * “ “&” “()” “->”

让C语言在这个世界无处不在
“ . “ “[ ]” “ < “ “ > “ “ == “

还记得他们吗?我想这一辈子都忘不了了

0x02-编程带给我的

是快乐而不是痛苦,如果你觉得编程痛苦,请放下你手头的工作,找找自己真正想要的,无论从什么角度来看,你都应该放弃令你痛苦的事情,花上三杯茶的时间,看看自己的心到底喜欢什么。

C语言可谓是让一个程序员最难以感受到自己进步的编程语言,一个黑窗口就让无数程序员再也走不出来。或者迷失,或者停滞不前,或者放弃,一个人最恐惧无助,甚至彷徨的时候,就是在努力之后却感受不到自己在进步,努力的白费是所有人不愿意看到的,但是太多人就着所谓前途而奋不顾身的投入这个事业,他们也许对计算机完全没有喜爱之心,埋头苦干,世人皆称爱读书的好孩子,但是这意义又在何处?即使最后你领着你觉得高的工资,站在了同学,朋友的前方,依然发现自己并没有得到满足,在我看来,让自己开心的才是最好的,不适合你的永远是最差的,即便能带来利益?何不花三杯茶的时间,想想自己到底适合何处。

在C语言的道路上,囊括了许多道天堑,并不是说这门语言比其他语言难,相反它十分符合人类的思维逻辑,但就是因为它存在的时间太久远,普通的使用于它于世界已经无甚大用,在现在这个高级语言遍地走的时代里,有用的只是那些将C语言发挥到极限的工程,不再是小窗口中写一个数据结构,一个算法,也许你觉得徒手写出一棵红黑树很了不起了?那也就是做成一个字典树,在一个浩大的工程中,一个虽重要却不起眼的小部分罢了,学完所有语法,却不知所措接下去该怎么做?有心人在无尽的探索之后发觉,啊!标准库!啊算法!嗯对了,还有各种各样的第三方扩展,以后呢?啊!操作系统!然而自学的路上充满着坎坷,艰辛,无助,烦恼,那又如何?喜欢就好。

所谓师傅领进门,修行在个人,这句话在我看来有两个重要点,却是现在大学生几乎缺失的。师傅一词告诉我们,要不耻下问,要善于询问,而不是伸手即来思想,”提问的智慧”在我看来是一门很重要的课程,特别是在当今信息时代。而更重要的是,先入为主的思想是极其可怕的,在这两年的自学历程里,见过太多后来者居上的事迹,当你一直认为自己一定比后辈强时,你就注定输了,所以不耻下问才是最重要的。但是如果师傅是那么容易找到的,那就不会有学校了,个人指的并不是孤军奋战,而是要善于自己发现问题,努力解决问题,这个过程可能少不了请教他人

编程可以是一种信仰,至少在我认为是这样的,把它当作信仰的人,它就能给你快乐,给你充实,当然也不要忘了现实,虽然现实中总是少不了加班的羁绊,但是如果是真心喜爱编程,又怎么会被这些困难所打败?但是C语言真的不是一门容易精通的语言。

0x03-C代码

#include <stdio.h>
int main(void)
{
    printf("That is Right Style\n");
    return 0;
}

在一个标准的C语言程序中,最特殊的莫过于main函数了,而说到底它就是一个函数而已,仅仅因为它地位特殊拥有第一执行权力,换句话说,难道因为一个人是省长它就不是人类了?所以函数该有的它都应该有,那么函数还有什么呢?

函数大体上分为内联函数(C99)(内联函数并非C++专属,C语言亦有,具体见前方链接)和非内联的普通函数,它们之间有一个很明显的特点(一般情况下),那就是不写原型直接在main函数上方定义,即使不加’inline’关键字,也能被编译器默认为内联函数,但之后带来的某些并发问题就不是编译器考虑的了。

普通函数正确的形式应该为声明与定义分离,声明就是一个函数原型,函数原型应该有一个函数名字,一个参数列表,一个返回值类型和一个分号。定义就是函数的内在,花括号内的就是函数的定义:

//...
int function(int arg_1, float arg_2);
//...
int main(int argc, char* argv[])
{
  int output = function(11, 22.0);
  printf("%d\n",output);
  return 0;
}

int function(int arg_1, float arg_2)
{
 int    return_value  = arg_1;
 float  temp_float    = arg_2;
 return return_value;
}    

依上所述,当非必要时,在自己编写函数的时候请注意在开头(main函数之前)写上你的函数的原型,并且在末尾(main函数之后)写上你的函数定义,这是一个很好的习惯以及规范。所谓代码整洁之道,就是如此。

函数的另一种分类是,有返回值和无返回值,返回值的类型可以是内建(build-in)的也可以是自己定义的(struct, union之类),无返回值则是void

  1. 为什么我们十分谴责void main()这种写法?因为这完全是中国式教育延伸出来的谭式写法,main函数的返回值看似无用,实际上是由操作系统接收,在Windows操作系统下也许无甚”大碍”(实际上有),当你使用Linux的过程中你会清晰的发现一个C语言程序的main返回值关系到一个系统是否能正常,高效的运行,这里稍微提一句,0在Linux程序管道通信间代表着无错可行的意思。所以请扔掉void main这种写法。
  2. 为什么我们对 main()这种省略返回值的写法置有微词?能发明这种写法的人,必定是了解了,在C语言中,如果一个函数不显式声明自己的返回值,那么会被缺省认为是int,但这一步是由编译器掌控,然而C语言设计之初便是让我们对一切尽可能的掌握,而一切不确定因子我们都不应该让它存在。其次有一个原则,能自己做的就不要让编译器做。

  3. 为什么我们对参数放空置有不满(int main())?在C语言中,一个函数的参数列表有三种合法形态:

    int function();
    int function(void);
    int function(int arg_n);
    int function(int arg_n, ...);
    

    第一种代表拥有未知个参数,第二种代表没有参数,第三种代表有一个参数,第四种代表拥有未知个参数,并且第一个参数类型为int,未知参数在C语言中有一个解决方案就是,可变长的参数列表,具体参考C标准库,在此我们解释的依据就是,我们要将一切都掌控在自己的手中,我们不在括号内填写参数,代表着我们认为一开始的意思是它为空,正因此我们就应该明确说明它为void,而不该让它成为一个未知参数长度的函数,如此在你不小心传入参数的时候,编译器也无法发现错误。

  4. int main(int argc, char* argv[])int main(void)才是我们该写的C语言标准形式

    对于缩进,除了编译器提供的符号缩进之外,我们可以自己给自己一个规范(请少用或者不用Tab),比如每一块代码相教上一个代码块有4格的缩进。

    对于学习C语言,请使用.c文件以及C语言编译器练习以及编写C程序,请不要再使用C++的文件编写C语言程序,并且自圆其说为了效率而使用C++的特性在C语言中,我们是祖国的下一代,是祖国的未来,请不要让自己毁在当下,珍爱编程,远离清华大学出版社

    之所以如此叙述,并不是因为情绪,而是当真如此,下方代码:

    /*file: test.c*/
    #include <stdio.h>
    #define SIZES 5
    int main(void)
    {
        int* c_pointer = malloc(SIZES * sizeof(int));
        /*发生了一些事情*/
        free(c_pointer);
        return 0;
    }
    

    这是一段标准的C语言程序,但是它能在C++个编译器下编译运行吗?换句话说当你将文件扩展名由.c改为.cpp之后,它能编译通过吗?答案是不能。

    为什么?答案是C++并不支持void*隐式转换为其他类型的指针,但是C语言允许。还有许许多多C于C++不相同的地方,兴许有人说C++是C的超集,但我并不这么认为,一门语言的出现便有它的意义所在,关键在于我们如何发挥它的最大优势,而不是通过混淆概念来增强实用性

  5. 程序式子的写法

    一个人活在世界上,时时刻刻都注意着自己的言行举止,而写程序也是如此,对于一个规范的能让别人读懂的程序而言,我们应该尽可能减少阻碍因子,例如:

    int main(void)
    {int complex_int=100;
    int i,j,k,x;
    for(int temp=0;temp<complex_int;++temp){k=temp;
    x=k+complex_int;}
    printf(complex_int="%d is k=%d x=%d\n",complex_int,k,x);
    return 0;}
    

    对于上述的代码,我总是在班级里的同学手下出现,但这段代码除了让别人困惑以外,自己在调试的时候也是十分不方便,每每遇到问题了,即便IDE提示了在某处错误,你也找不到问题所在,经常有人来问我哪里错了,大部分情况都是少了分号,括号,或者作用域超过,原因在哪?

    要是一开始将代码写清楚了,这种情况简直是凤毛麟角,想遇上都难。对于一个代码而言,我们应该注意让其变得清晰。

    • 等号两边使用空格:

      int complex_int = 100;
      
    • 使用多个变量的声明定义,或者函数声明定义,函数使用时,注意用空格分开变量:

      int i, j, k, x;//但是十分不建议这么声明难以理解意义的变量
      printf("complex_int = %d is k = %d x = %d\n", complex_int, k, x);
      void present(int arg_1, double arg_2);
      
    • 对于一个清晰的程序而言,我们要让每一个步骤清晰且有意义,这就要求我们在编写程序的时候尽量能让代码看起来结构化,或者整体化。尽量让每个程序式子为一行,如果有特别的需要让多个式子写在同一行,可以使用,操作符进行组合,但是会让程序更难理解,日后调试的时候也更难发现错误。

      /*Style 1*/    
      for(int temp = 0;temp < complex_int;++temp)
      {
          k = temp;
          x = k + complex_int;
      }
      /*Style 2*/
      for(int temp = 0;temp < complex_int;++temp){
          k = temp;
          x = k + complex_int;
      }
      

      对于上方的代码,是C语言代码花括号的两种风格,最好能选择其中一种作为自己的编程风格,这样能让你的程序看起来更加清晰,混合使用的利弊并不好说,关键还是看个人风格。

    • 对于作用域而言,在C语言中有一个经常被使用的特例,当一个条件语句,或者循环只有一条语句的时候,我们常常省略了花括号{},而是仅仅使用一个分号作为结尾,这在很多情况下让代码不再啰嗦:

      if(pointo_int == NULL)
          fprintf(stderr, "The pointer is NULL!\n");
      else
      {
          printf("%d\n",*pointo_int);
          pointo_int = pointo_int->next;
      }
      

      在这段代码中if语句下方的代码并没有使用{}运算符进行指明,但是根据语法,该语句的确是属于if语句的作用范围内,如果我们此时写上了{}反而会令代码看起来过于啰嗦。但是有的时候,这条特性并不是那么的有趣,当使用嵌套功能的时候,还是建议使用{}进行显式的范围规定,而不是使用默认的作用域:

      for(int i = 0;i< 10;++i)
          for(int k = 0;k < 10;++k)
              while(flag != 1)
                  set_value(arr[i][k]);
      

      这段代码,看起来十分简洁,但是确实是一个很大的隐患,当我们要调试这段代码的时候,总是需要修改它的构造,而这就带来了潜在的隐患。所以建议在使用嵌套的时候,无论什么情况,都能使用{}进行包装。

综上所述,在开始编写一个标准C语言程序的时候,请先把下面这些东西写上:

#include <stdio.h>

int main(void)
{
    return 0;
}

对于main的参数,有兴趣的可以查阅我的文章,或者自行谷歌,在此问题上百度也是可以的。

0x04-C语言变量

C语言在明面上将数的变量分为两类,整型变量以及浮点数,对应着现实世界的整数和小数。

  • 首先是整数,使用了这么多的C语言之后,每当在使用整数之时都会将其想象成二进制的存在,而不是十进制。原因在于,这是程序的本质所在,稍有研究编译器工作原理的都会发现,在编译器处理乘法乃至除法的时候,优秀的编译器总会想方设法的加快程序的速度,毫无疑问在所有运算中移位运算是最快速的”乘法”以及”除法”:

    1<<2 == 4 ,8>>2 == 2

    而正常一个乘法相当于十数次的加法运算的时间消耗,移位则不用(除法的消耗更大,但是随着CPU的进步,这些差距正在逐渐缩小,就目前来看依旧是有着不小的差距但无论如何优化,乘法时间都会大于加法)。正如前面所说,C语言设计之初便是给了程序员所有的权利,而程序员要做的就是掌控所有能掌控的,即便是数的计算亦是如此,比如在优秀的编译器看来:

    2*7 ====> (2<<3) - 2

    5*31 ====> (5<<5) - 5

    毫无疑问经过编译器优化后的代码此前者要快许多。这就是为什么我们要将一个数看作二进制,这不仅仅是表面,而是要在深层次的认为它是二进制,总体来说C语言的整型是非常简洁明了的总体分为 有符号无符号,很好理解只需要注意不要让无符号数进行负数的运算,这里有一个原则,可以很好的规避这种无意之过,不把无符号类型变量和有符号类型变量放于同一运算中,时刻记得保持式子的类型一致是设计时的保障。

  • 浮点数,由于实数域可以看作稠密的,故除了整数以外,还有无数的小数,而小数在计算机中如何表示?一种无限的状态是无法在计算机中被精确表示,所以有了浮点法,关于浮点法可以参考书籍《深入理解计算机系统》
    这里介绍的是在C语言中我们应该如何正确使用浮点数?很多人(包括我)在初作之时总是想当然的以为计算机是无所不能的,连人类都无法完全表达出来的小数计算机一定可以,实际上并非如此,在这里我可以说,计算机只是近似表达,而最大的忌讳的便是将两个浮点数进行比较,此处介绍一种浮点数常用的比较方法,精确度法:

    #define DISTANCE 0.00000001
    ...
    float f_x_1 = 20.5;
    float f_x_2 = 19.5;
    if(f_x_1 - f_x_2 < DISTANCE)
        printf("They are Equal\n");
    else
        printf("Different\n");
    

    所以说,在很大程度上,当你在程序中使用了浮点数,又直接使用浮点数进行比较,却发现始终无法达到预期效果,那么你可以检查一下,是否是这个原因,在这一点上,不得不说是C语言的一个缺憾。

  • 指针变量,是一种比较特别的变量,以至于总是对它进行特别对待。这里有几个原则:

    • 两个不相关的指针进行加减操作是无意义的
    • 始终确保自己能够找到分配的内存
    • 无论何时何地何种情况,都要记住,不使用未初始化的指针,不让未使用的内存持续存在。

    指针在不同位的操作系统上的大小是不一样的,但是在同一个操作系统下,无论什么类型的指针都是相同大小,这涉及到指针的寻址问题,(题外话:C语言的寻址实际上使用了汇编语言的间接寻址,有兴趣的可以自行尝试,方法之一,使用gcc编译器的汇编选项,产生汇编代码,进行一一比对),对于寻址一个笼统一些的说法便是

    4Byte = 32bit

    2^32 = 4G

    所以32位的操作系统下C语言指针:

    ...
    size_t what = sizeof(void*);
    printf("%d", what);
    ...
    

    输出:$root@mine: 4

    对于大部分使用者来说,指针主要用来降低内存消耗以及提高运算效率的,这里设计许多学问,我也无法一一展示,比较有意思也常用的两个东西便是递增以及语法糖:++, ->

    ...
    int dupli_of_me[10] = {0};//也可以使用库函数memset()进行置0
    int *point_to_me = dupli_of_me;
    int me = 100;
    while(point_to_me < (dupli_of_me + 10))
        *point_to_me++ = me;
    

    其中*point_to_me++ = me;在C语言应用广泛它相当于是:

    *point_to_me = me;
    point_to_me++;
    

    的语法糖,对于++,在非必要的情况下,请使用前缀递增,而非后缀递增,原因是消耗问题,仔细想想这两种递增的区别在何处?
    前缀递增总是在原数上进行递增操作,然而后缀递增呢?它首先拷贝一份原数放于别处,并且递增这份拷贝,在原数进行的操作完毕后,将这份拷贝再拷贝进原数取代它,此中的操作涉及的更多,所以在非必要的情况下,请使用前缀递增而不是后缀递增(递减也是同样的道理)

    ->则是在结构体上使用的非常广泛:

    typedef struct data{
        int test;
        struct data* next;
    }my_struct;
    ...
    my_struct temp;
    my_struct *ptemp = &temp;
    ptemp->test = 100;
    ptemp->next = NULL;
    if(temp.test == 100)
        printf("Correctly!\n");
    else
        printf("That is impossible!\n");
    ...
    

    可以很清楚的看出其实ptemp->test便是(*ptemp).test的语法糖

  • 变量限定

    const 是最常用的变量限定符,它的意思是告诉编译器,这个变量或者对象在初始化以后不能被改变,常用它来保护一些必要的返回值,参数以及常量的定义。

    volatile 这个关键字常常被C语言教材所忽略,它很神秘。实际上确实如此,他的作用的确很神秘:一旦使用了,就是告诉编译器,即使这个变量没有被使用或修改其他内存单元,它的值也可能发生变化。通俗的说就是,告诉编译器,不要把你的那一套优化策略用在我身上

    /* 此时我们将编译器优化等级提高到 -O2 */
    int          test_num   = 100; //测试一个迭代加法
    int          nor_result = 0;
    volatile int vol_result = 0;
    /* 测试无volatile限定下,该程序的耗时 */            
    for(int i = 0;i < 10000;++i)
        for(int j = 0;j < 10000;++j)
                nor_result += test_num;
    

    接下来就是测试volatile限定下的代码

    for(int i = 0;i < 10000;++i)
        for(int j = 0;j < 10000;++j)
                vol_result += test_num;
    

    在使用一些手段后,得到运行时间,可以很清晰的看出差别,在我的机器上,i5-4CPU,得到的结果是后者比前者慢大概十五倍。
    从某一些方向上证明了,volatile的一些作用,比如调试的时候,或者一些特殊用途。涉足不多,故不记录。

  • 变量说明

    extern 用于将不同文件的,带有外部链接性的变量引用到本文件中。所谓外部链接性就是可以被除本文件外的其他文件“看见”的变量,如全局变量,使用方法:

    /* 以下为一个工程内可见 */
    /*file1.c*/
    int glo_show;//对于该全局变量来说,它们在声明时无初始化,则默认初始为0
    int glo_print = 10;//声明定义完成后,自动分配内存以存储信息    
    ...
    
    /* file2.c */
    extern glo_print; //仅仅是引用名字,并不会额外分配空间
                      //所以,只需要写正确变量名字即可,后方的初始化无须完全
                      //因为变量的初始化定义只能有一次。
    
    void print()
    {
        printf("The Globle Value is %d \n", glo_print);
    }
    

    auto 可以姑且忽略,因为没有什么实际意义。

    Create By WuShengXin @ 2015