[Linux] Linux C编程一站式学习 Part.1
C语言入门
程序基本概念
- 程序和编程语言
- C语言--(编译器)--汇编语言--(汇编器)--机器语言(目标代码 / 可执行代码)
- 可移植 / 平台无关:平台指计算机体系结构或操作系统,或二者的组合。不同的平台有不同的指令集,可识别不同的机器指令格式
- 直接用某种计算机的汇编或机器指令写出来的程序只能在这种计算机上运行,各种计算机上都有C编译器可以把C程序编译成计算机自己的机器指令
- 编译 / 解释语言:解释语言不需生成目标代码,由解释器一行行地,边翻译边执行
- 自然语言和形式语言
- 自然语言是人类讲的语言,如汉语、英语、法语
- 形式语言是为了特定应用认为设计的语言,如数学符号、分子式、编程语言
- 形式语言有严格的语法(Syntax)规则
- 语法规则有关于符号(Token)和结构(Structure)的规则所组成
- 符号:事先定义好的运算符,关于符号的规则为词法(Lexical)规则
- 结构:符号的排列方式,关于结构的规则为语法(Grammar)规则
- 解析(Parse):分析句子结构的过程
- 自然语言与形式语言的区别
- 歧义性(Ambiguity):形式语言的设计要求清晰、毫无歧义,每个语句都有明确含义而不管上下文如何
- 冗余性(Redundancy):自然语言为消除歧义引入冗余,形式语言极少冗余
- 与字面意思的一致性:自然语言充满隐喻(Metaphor),形式语言字面意思就是真实意思
- 阅读形式语言(包括计算机程序)的建议
- 形式语言远比自然语言紧凑,要多花时间阅读
- 结构很重要,不要从上到下或从左到右地读,而应在大脑里解析,识别Token,分解结构
- 关注细节,如拼写错误和符号错误等
- 程序调试
- Bug的分类
- 编译时错误:语法错误导致编译失败
- 运行时错误:编译器检查不出,但运行时出错导致程序崩溃(注意区分编译时和运行时两个概念)
- 逻辑错误和语义错误:编译和运行都很顺利,但没有实现预计结果
- 编程建议
- 编程=调试:编程就是逐步调试直到获得期望结果
- 总是从一个能正确运行的小规模程序开始,每做一步小的改动就立即调试
- Bug的分类
常量、变量和表达式
- 定义、赋值、初始化
- 定义:分配一块内存空间,并给它命名,如 int hour;
- 赋值:把一个值存储在内存空间中,如 hour = 11;
- 初始化:定义 + 赋值,如 int hour = 11;
- 初始化是一种特殊的变量定义语句,而不是赋值语句
- 变量名除了用在等号左边表示赋值外,其他情况都表示把它的值取出来,替换在那里
- 表达式(Expression)由运算符(Operator)和操作数(Operand)组成
- 赋值语句是表达式的一种
- 运算符有优先级(Precedence),如不希望按默认的优先级运算需加括号(Parenthesis)
- C语言规定等号运算符的结果就是等号左边被赋予的那个值
- 常量可以赋值给变量,也可以和变量、运算符一起组成表达式
- 最简单的表达式由单个常量或变量组成
- 任何表达式都有一个值,表达式可以加 ; 号构成表达式语句
简单函数
- sin是函数(Function),sin(pi/2)是函数调用(Function Call),pi/2是参数(Argument)
- 函数调用sin(pi/2)也是表达式,由函数调用运算符() 和两个操作数组成,表达式的值为函数返回值(Return Value)
- C语言的函数可以有副作用(Side Effect),这是它和数学函数在概念上的根本区别
- 如表达式a=b,返回值是a的值,副作用是a的值被改变
- 很多时候我们关心函数的副作用,而非返回值,如printf(),其返回值是实际打印的字符数,而非打印内容
- 对于完全利用Side Effect的函数,可将其返回值定义为void
- 函数原型(Prototype):函数名 + 参数类型和个数 + 返回值类型
- 函数要先声明后使用
- 定义变量时可以把同类型的变量列在一起,但定义函数参数时不可以,如void print_time(int hour, minute){}; 是错误的写法
- 形参(Parameter)相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并用实参(Argument)的值来初始化
- 函数提供了一个接口(Interface),调用函数就是使用这个接口,使用的前提是必须和接口保持一致
- 能用函数传参代替的就不要用全局变量
- 全局变量只能用常量表达式初始化,而局部变量可以用任意类型的表达式初始化
- C语言规定全局变量的初始值保存在编译生成的目标代码中,所以必须编译时就能计算出来,如全局变量pi的初始化语句double pi = 3.14 + 0.0016;是合法的,而double pi = acos(-1.0);是不合法的
- 若全局变量在定义时不初始化,则初始值是0(或"\0"或"0.0"等)
- 局部变量在定义时不初始化,则初始值不确定,故局部变量在使用前一定要先赋值
- 局部变量的的存储空间在每次函数调用时分配,函数返回时释放
分支语句
- 局部变量的的存储空间在每次进入语句块时分配,退出语句块时释放
- 把语句封装成函数的步骤:把语句放在函数体中,把变量改成函数的参数
- else总是和它上面最近的一个if配对
- case后面跟的必须是常量表达式,类似全局变量,必须在编译时计算出值
深入理解函数
- return语句
- 函数返回值相当于定义一个和函数返回值类型相同的临时变量,并用return后面的表达式来初始化
- 函数的返回值不是左值,不能给它赋值
- 写带有return语句的函数时,要小心检查所有的代码路径(Code Path),在任何条件下都执行不到的代码称为Dead Code
- 增量式(Incremental)开发
- 尽可能复用(Reuse)以前写的代码,避免写重复的代码。封装就是为了复用
- 递归
- 如果定义一个概念需要用到这个概念本身,那么它的定义就是递归的
- 相信你正在写的递归函数是正确的,并调用它,然后在此基础上写完递归函数,那么它就是正确的
- 不要忘记写Base Case
- 递归和循环是等价的
循环语句
- 递归和循环两种思路的区别:递归靠递推关系(如n!=n*(n-1)!),循环是公式展开(如n!=n*(n-1)*(n-2)...3*2*1)。展开公式更容易理解,但当公式过于复杂无法展开时,递推就更直观些
- 递归在整个过程中会分配和释放很多变量,但所有变量都只在初始化时赋值,没有任何变量的值发生过改变,而循环是通过对几个变量多次赋值来达到目的
- 递归的思路称为函数式编程(Functional Programming),循环的思路称为命令式编程(Imperative Programming)
- 递归描述要做什么(Declarative),循环描述具体一步一步怎么做(Imperative)
- 函数式编程的“函数”类似于数学函数的概念,是没有Side Effect的,Imperative方式对变量的多次修改会导致问题,如影响代码的线程安全,故应以一种“一致”的方式进行多次赋值
- do/while语句要在while语句后加分号
- ++i:传入参数,返回值(参数+1),Side Effect:变量i的值+1
- i++:传入参数,返回值(参数),Side Effect:变量i的值+1
- goto:无条件跳转,只能跳到同一个函数的某个标号处,而不能跳到别的函数里
- goto语句只用于在函数末尾处理出错,函数中任何地方出现了错误条件都可立即跳到末尾,处理完后函数返回
结构体
- 数据抽象:类似“提取公因式”,ab+ac=a(b+c),左边如果a变了,两个因子都要修改,右边就只要改一个因子
- 组合使得系统可以任意复杂,而抽象使得系统的复杂度是可控的,任何改动都只局限在某一层,而不会波及整个系统
- All problems in computer science can be solved by another level of indirection(abstraction)--Butler Lampson
- 通过一个复数存储表示抽象层把complex_struct结构体的存储格式和上层的复数运算函数隔开
- 枚举类型:用于让结构体接收不同类型的输入,如 enum coordinate_type { RECTANGULAR, POLAR };struct complex_struct { enum coordinate_type t; double a, b; };,通过定义数据类型标识,使得直角坐标和极坐标的数据都可以适配到complex_struct 结构体中
数组
- 数组(Array)是一种复合数据类型,由一系列相同类型的元素(Element)组成
- 使用数组下标不能超过数组的长度范围,C编译器不检查数组越界错误
- 数组和结构体的不同:数组不能相互赋值,不能作为函数的参数或返回值
- 数组名做右值使用时,自动转换成指向数组首元素的指针
- 使用C标准库得到的随机数其实是伪随机数(Pseudorandom),只不过看起来很随机,且每次运行的结果是一样的
- 通过其他方式得到一个不确定的数作为Seed,然后在此基础上生成伪随机数,如 srand(time(NULL));(当前时间剧1970年1月1日00:00:00的秒数)
- 使用rand()生成伪随机数,头文件stdlib.h,返回值是0和RAND_MAX之间的整数,RAND_MAX是头文件中定义的常量,若想将随机数限定在某个范围内可用%处理,如int x = rand() % 10;(0~9内的随机数)
- define在预处理阶段处理,可避免硬编码(Hard coding)(类似抽象,避免一个地方的改动波及大的范围)
字符串
- 字符串可看做是一个数组,元素是字符型的,末尾有一个字符“\0”表示字符串结束。字符串是只读的,不允许修改
- 做右值使用时,自动转换成指向首元素的指针
- 初始化字符串时,可不指定数组长度,而让编译器自动计算
- 数据驱动的编程(Data-driven Programming):编程最重要的是选择正确的数据结构来组织信息,控制流程和算法尚在其次,只要数据结构选的正确,代码自然容易理解和维护
编码风格
- 代码主要是为了写给人看的,只是顺便也能用机器执行而已
- Linux内核的CodingStyle
- 用缩进(Tab)体现语句块的层次关系
- if/else、while、for等可以带语句块的语句,语句块的 { 和 } 应该和关键字写在一起,用空格隔开,而不是单独占一行
- 函数定义的 { 和 } 单独占一行
- switch和语句块里的case、default对齐写
- 代码中每个逻辑段落之间用一个空行分隔开
- 函数内根据相关性分组,用空行分隔
- 注释
- 整个源文件的顶部注释:文件名、作者、版本历史
- 函数注释:函数功能、参数、返回值、错误码
- 语句注释:写在语句上侧
- 代码简短注释:写在代码右侧,和代码间至少用一个空格隔开,一个源文件中所有的右侧注释最好上下对齐
- 函数内的注释要尽可能少,只说明代码能做什么,而不是怎样做的(只要代码清晰,怎样做是一目了然的,否则就是代码可读性很差)
- 复杂的结构体、宏定义和变量定义需要注释
- 变量
- 清晰明了,可用完整单词和大家易于理解的缩写
- 变量、函数类型采用全小写加下划线,常量(宏定义和枚举)采用全大写加下划线
- 慎用匈牙利命名法,不用汉语拼音
- 函数
- 一个函数只做一件事
- 函数内部缩进层次不多于4层
- 函数不要写的太长
- 执行函数就是执行一个动作,函数名通常应包含动词
- 局部变量不要太多
- indent工具:可把代码格式化为某种风格
gdb
- 调试步骤:分析现象->假设错误原因->产生新的现象验证假设
- 单步执行和跟踪函数调用
- 断点(Breakpoint)
- 观察点(Watchpoint):不知道某一存储单元在哪里被改动的
- 段错误:如果某个函数中发生访问越界,很可能并不立即产生段错误,而在函数返回时产生段错误
排序与查找
- 循环正确性的判断方法 Loop Invariant
- 第一次执行循环体前判断条件为真
- 如果“第N-1次循环后判断条件为真”这个前提成立,则可证明第N次循环后判断条件仍为真
- 所有循环结束后判断条件为真,则该算法正确
栈与队列
- 一个问题中数据的存储、访问方式决定了解决问题可以采用什么样的算法,设计算法就要同时设计相应的数据结构来支持这种算法
- 堆栈--后进先出--深度优先搜索(DFS)--回溯(Backtrack)
- 队列--先进先出(FIFO)--广度优先搜索(BFS)
参考