Skip to content

Latest commit

 

History

History
355 lines (315 loc) · 17.1 KB

Lecture03.md

File metadata and controls

355 lines (315 loc) · 17.1 KB

The code is placed in GDB and Make.


代码格式

  1. 函数声明与定义分离,声明写在头文件里,实现写在源文件里。 变量声明后在第一时间初始化,避免出错。

  2. 不要写 void main(),在linux系统中,一个 C 语言程序的 main 返回值关系到一个系统是否能正常,高效的运行, 其中 0 在 Linux 程序管道通信间代表着无错可行的意思。
    也不要省略 int 写 main(),虽然 C 语言缺省认为不显式声明返回值的函数返回值为 int , 但是这一步受编译器影响,有不确定因素。
    不要写 int main() 要写 int main(int argc, char* argv[])int main(void), 这样才能避免本来应该是 void 的情况传入了参数无法报错。

  3. 不要把 C 语言代码当作 C++ 代码来编写,也不要把 C 的代码放在 cpp 文件中, 如下代码不能在 C++ 编译器下编译运行,因为 C++ 不支持 void* 指针隐式转换为其他类型的指针。

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

    1. 函数命名
      1. 前缀
        1. set可以表示设置某个参数为某值;
        2. get可以表示获取某个参数的值;
        3. is可以表示询问是否是这种情况;
      2. 后缀
        1. max/min 可以表示某种操作的最大(小)次数;
        2. cnt 可以表示当前操作次数;
        3. key 某种关键值;
      3. 命名要简洁,具体操作可以留给文档说明;
      4. 常用词组:(不讲)
        add / remove
        begin / end 
        create / destroy
        insert /delete 
        first / last 
        get / release
        increment /decrement 
        put / get
        add / delete
        lock / unlock 
        open / close
        min / max
        old / new 
        start / stop
        next /previous 
        source / target 
        show / hide
        send /receive 
        source / destination
        cut / paste
        up / down
        
    2. 变量命名
      1. 所有字符使用小写;
      2. 含义多的用下划线 _ 辅助分隔单词;
      3. 以 = 为标准进行对齐;
      4. 类型,变量名各自左对齐;
      5. 少用全局变量,如果要用,考虑添加前缀 g_
    3. #define 命名
      1. 所有字符都用大写,并用 _ 分割单词
      2. 如果多于一个语句,使用 do{…}while (0)进行包裹,防止 ; 错误。
    4. enum 命名
      1. 所有字符都是用大写,并用 _ 分割单词;
      2. 与 define 相比, enum 适用于同一类型的变量声明,而不是单一独立的常量。往往出现都是成组。
  5. 花括号 {}

    1. {} 包裹的范围超过了一个屏幕时,可以适当使用注释来指明作用范围。
    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;
      }
      
      例子:
      while (1){
      if (tmp == NULL){
      break;
      }
      else if (fanny == 1){
          ... 大概超过了一个屏幕的代码
      } /*else if fanny*/
      }/*end while*/
      
  6. 空行与空格

    1. 空行起着分隔程序段落的作用。空行得体将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序会多消耗一些纸张,但是值得。
      1. 定义变量后要空行。尽可能在定义变量的同时初始化该变量,即遵循就近原则。如果变量的引用和定义相隔比较远,那么变量的初始化就很容易被忘记。若引用了未被初始化的变量,就会导致程序出错。
      2. 每个函数定义结束之后都要加空行。
      3. 两个相对独立的程序块、变量说明之后必须要加空行。比如上面几行代码完成的是一个功能,下面几行代码完成的是另一个功能,那么它们中间就要加空行。这样看起来更清晰。
    2. 等号两边使用空格 int a = 100;
    3. 多个变量的声明定义,或者函数定义,函数使用时,空格分开变量:
      int i, j, k, x;
      printf("a int = %d is k = %d x = %d\n", complex_int, k, x);
      void present(int arg_1, double arg_2);
      
    4. 关键字之后要留空格。像 constcase 等关键字之后至少要留一个空格,否则无法辨析关键字。像 ifforwhile 等关键字之后应留一个空格再跟左括号 (,以突出关键字。
    5. 函数名之后不要留空格,应紧跟左括号(,以与关键字区别。
    6. (向后紧跟。而 ) , ; 这三个向前紧跟。紧跟处不留空格。
    7. , 之后要留空格。如果 ; 不是一行的结束符号,其后要留空格。注释 // 后留空格。
    8. 赋值运算符、关系运算符、算术运算符、逻辑运算符、位运算符等双目运算符的前后应当加空格。
      注意,运算符 % 是求余运算符,与 printf 中 %d% 不同,所以 %d 中的 % 前后不用加空格。
    9. 单目运算符前后不加空格。
    10. 像数组符号[]、结构体成员运算符.、指向结构体成员运算符->,这类操作符前后不加空格。
    11. 对于表达式比较长的 for 语句和 if 语句,为了紧凑起见,可以适当地去掉一些空格。但 for 和 if 后面紧跟的空格不可以删,其后面的语句可以根据语句的长度适当地去掉一些空格。
  7. 成对书写
    成对的符号一定要成对书写,如 (){}。不要写完左括号然后写内容最后再补右括号,这样很容易漏掉右括号,尤其是写嵌套程序的时候。

  8. 缩进
    缩进不要通过键盘上的 Tab 键实现的,统一用四个空格,因为不同的环境下 Tab 的空格数量可能不一样,导致无法对齐。缩进可以使程序更有层次感。原则是:如果地位相等,则不需要缩进;如果属于某一个代码的内部代码就需要缩进。

  9. 对齐
    对齐主要是针对大括号 {} 说的:

    1. {} 分别都要独占一行。互为一对的 {} 要位于同一列,并且与引用它们的语句左对齐。(两种花括号风格选一种)
    2. {} 之内的代码要向内缩进四个空格,且同一地位的要左对齐,地位不同的继续缩进。
    3. 还有需要注意的是,很多编程软件是会“自动对齐”的, 此外编程软件还有“对齐、缩进修正”功能。就是按 Ctrl+A 全选,然后按 Alt+F8,这时程序中所有成对的大括号都会自动对齐,未缩进的也会自动缩进。
  10. 代码行

    1. 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且便于写注释。
    2. ifelseforwhiledo 等语句自占一行,执行语句不得紧跟其后。此外,非常重要的一点是,不论执行语句有多少行,就算只有一行也要加 {},并且遵循对齐的原则,这样可以防止书写失误。
  11. 注释
    C 语言中一行注释一般采用 // comment,多行注释必须采用 /* comment */。注释通常用于重要的代码行或段落提示。在一般情况下,源程序有效注释量必须在 20% 以上。虽然注释有助于理解代码,但注意不可过多地使用注释。

    1. 注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多会让人眼花缭乱。
    2. 如果代码本来就是清楚的,则不必加注释。例如:
      i++;  //i加1
      
      这个就是多余的注释。注释要解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
    3. 边写代码边注释,修改代码的同时要修改相应的注释,以保证注释与代码的一致性,不再有用的注释要删除。
    4. 当代码比较长,特别是有多重嵌套的时候,应当在段落的结束处加注释,这样便于阅读。
    5. 每一条宏定义的右边必须要有注释,说明其作用。
  12. 其他杂项

    1. switch 最后一定要放一个 default
    2. case 中如果要定义新变量,那这个 case 内部的语句整体要用 {} 包裹起来
    3. 不要将 _ 作为宏的开头或结尾
    4. 函数尽可能短小简洁,最好不超过一个屏幕;多功能函数难以理解、测试、维护;特别是不要把关联不强的两件事情放到同一个函数里面,使得修改其中一个功能时可能影响另一个功能。
    5. 在多重循环中尽量将最忙的循环放在内层;
    6. 某个循环即使是空语句也最好用 {} 包裹,以免出错
    7. 不要让函数返回值直接作为条件语句的判断,if (is_eod(file) == 0) 好于 if (!is_eod(file))
    8. 赋值一步一步来,不要写 num = (add = add + thr) + 20;
      也不要写 num = i++ + ++i; 这样没有意义结果又无法确定的等式。
    9. 不要比较两个浮点数是否相等或不等 (要比较的话得用一个极小值epison限定范围),也不要将浮点数用于离散的计数(本应该用 int 的场合)。事实上如果先把一个整数转换为浮点数,然后用log函数试图去取出这个浮点数对应多少位,是可能会出问题的,与原来的整数的位并不一定一致
    10. 代码文件最后都留一个空行。因为有些C代码编译的时候,不留空行可能出现问题
    11. 对于有含义的数字(如某些数组的大小)不要使用纯数字,如:int a[100]。 这样再读源代码不知道它的意思,要用 #define 给它一个名字。如
      #define SIZE 100
      int a[SIZE]
      

调试器的进阶使用

注意GDB调试时,并不是直接把源代码缓存起来了,而是根据行号和符号表去读源代码。 所以如果在调试时源代码发生变化,list命令可能会受到影响。

条件断点

break if i = 50

表示当i为50时停住。

ignore <bnum> <count>

表示忽略断点号为 bnum 的停止条件 count 次

自动执行命令

commands [bnum]
...
end

用于在断点bnum处自动执行命令,如:

break func if i > 1
commands
printf "i is %d\n",i
continue
end

表示在进入 func 后,如果 i 大于 1 就自动打印出值并自动继续运行

修改变量

set i = 49

改变 i 的值为 49

查看数组

对于动态数组

p *array@len

对于静态数组

p array_static

自动显示内容

display 用于在停住或者单步前进时自动显示内容,如:

display array_static[j]

undisplaydelete display 后面跟 display 号码可以删除自动显示。

delete display 1-3 表示删除前三个 display

  • info display
  • set print pretty on 显示结构体会比较漂亮,不演示了

设置环境变量

set $foo1 = result
set $foo2 = &result
set result = -1
p $foo1
p *$foo2

其他

  • jump <linespec> 用于跳转,乱序执行程序
  • return 强制返回。如在func里面使用:return 110
  • call 强制调用函数并显示返回值。如在main里面使用 call func(6)

使用 core 文件

  • 程序因为内存泄漏等原因骤停时,报错core dump,core文件记录这一刻内存等的状态,通过查看可以了解内存错误的位置等重要信息
  • 启用 core 文件记录
    ulimit -c unlimited
    
  • 进入 core 文件
    gdb test_for_core core.XXXX
    
  • 在里面不能 r , s , n ,但是可以进行 p , bt ,where,list 等单纯查看类的操作。也可查看当时内存中的变量。

gdb如何调试没有符号表(未加-g选项的编译)的程序

https://blog.csdn.net/yygydjkthh/article/details/43318523?ref=myread


make

基本规则

目标 : 依赖
[TAB]命令(在shell中运行的)

目标

  • 第一个目标是执行 make 的终极目标。
  • 目标可以不是文件,如 clean ,但是为了避免相关目录下有名为 clean 的文件, 因此用 .PHONY 目标的特殊规则说明 clean 是虚拟目标
  • 确定了 makefile 的情况下,shell 执行 make ,发生如下步骤:
    首先是检查终极目标是否存在或者是否需要更新(其依赖时间戳是否新于目标);若存在且“最新”,则执行结束,否则把每一个依赖作为目标进行类似的检查,直到最底端或报错为止;此基础上若未报错(如最基本的依赖不存在之类错误),则反向依次重建或创建对应的依赖,直到实现终极目标。(其中任何一步报错,make过程都会终止)
  • 类似的,shell 执行 make xxx.yyy 其中 xxx.yyy 为 Makefile 中的目标时, 就把 xxx.yyy 当作类似过程的“终极目标”来执行。

依赖

  • 允许一个目标多个依赖或者多对一、多对多的依赖,但是后两者缺乏易读性,不推荐
  • 依赖列表太长可以用 \ 换行,但是 \ 后面不能有空格
  • 可以使用通配符 * ? ...
  • order-only 依赖:
    a : b | c
        ....
    
    在a不存在时,b,c参与规则执行;在a存在需要重建时,只有b参与规则执行,c被忽略; 如果一个依赖在 | 两边出现,就当作常规依赖处理。

描述规则

  • 实际是在 shell 中运行,可以使用通配符如:-rm *.o
  • 隐含规则
    对于 xxx.o 这类目标,在没有规则时会自动调用默认隐含规则
    cc -c -o xxx.o xxx.c
    

因此对于目标文件为 xxx.o 依赖为 xxx.c 的目标可以省略规则的命令行,留下依赖的头文件等特定的依赖

定义变量

objects = main.o command.o

使用同 shell,用 $(objects) 这种格式调用。

注释

  • # 后跟的为注释,如果结尾是 \ ,那下一行也是注释
  • 需要用 # 字符的时候用 \# 来转义代替

makefile 文件的命名(讲隐含规则)

默认的情况下, make 会在工作目录(执行 make 的目录)下按照文件名顺序寻找 makefile 文件读取并执行,查找的文件名顺序为 : GNUmakefile、 makefile、 Makefile。

通常应该使用 makefile 或者 Makefile 作为一个 makefile 的文件名 (我们推荐使用 Makefile,首字母大写而比较显著,一般在一个目录中和当前目录的一些重要文件 (README , Changelist等)靠近,在寻找时会比较容易的发现它)。而 GNUmakefile 是不推荐使用的文件名,这是因为以此命名的文件只有 GNU make 才可以识别,而其他版本的 make 程序只会在工作目录下寻找 makefile 和 Makefile 这两个文件。

如果 make 程序在工作目录下无法找到以上三个文件中的任何一个,它将不读取任何其他文件作为解析对象。 但是根据 make 隐含规则的特性,我们可以通过命令行指定一个目标, 如果当前目录下存在符合此目标的依赖文件,那么这个命令行所指定的目标将会被创建或者更新,参见注释。

当 makefile 文件的命名不是这三个任何一个时,需要通过 make 的 -f 或者 --file 选项来指定 make 读取的 makefile 文件。给 make 指定 makefile 文件的格式为:-f NAME 或者 --file=NAME,它指定文件 NAME 作为执行 make 时读取的 makefile 文件。 也可以通过多个 -f 或者 --file 选项来指定多个需要读取的 makefile 文件, 多个 makefile 文件将会被按照指定的顺序进行链接并被 make 解析执行。当通过 -f 或者 --file 指定 make 读取 makefile 的文件时, make 就不再自动查找这三个标准命名的 makefile 文件。

注释:通过命令指定目标使用 make 的隐含规则:

当前目录下不存在以 GNUmakefile、makefile、Makefile 命名的任何文件时,

  • 当前目录下存在一个源文件 foo.c 的,我们可以使用 make foo.o 来使用 make 的隐含规则自动生成 foo.o。当执行 make foo.o 时,实际执行命令为:
    cc -c -o foo.o foo.c
    
  • 如果当前目录下没有 foo.c 文件时,即 make.o 文件目标的隐含规则中依赖文件不存在。 如果使用命令 make foo.o 时,将回到如下提示:
    make : *** No rule to make target 'foo.o'. Stop.
    
  • 如果直接使用命令 make 时,提示信息如下:
    make : *** No targets specified and no makefile found. Stop.
    

文档规范

参考 Linux 中国的 翻译规范