第一章 程序的基本概念
1.程序和编程语言
程序(Program)是一个精确说明如何进行计算的指令序列。这里的计算可以是一些数学上的计算,比如解方程或者求多项式的根,也可以是符号运算,一个简单的例子是查找和替换文档中的词,一个复杂的例子是搜索引擎。
程序由一系列指令(Instruction)组成,指令是指示计算机做某种运算的命令,通常包括以下几类:
输入(Input)
从键盘、文件或者其它设备获取数据。
输出(Output)
把数据显示到屏幕,或者存入一个文件,或者发送到其它设备。
基本运算
执行最基本的数学运算(加减乘除)和数据存取,其实输入和输出也属于数据存取。
测试和分支(Branch)
测试某个条件,然后根据不同的测试结果执行不同的后续指令。
循环(Loop)
重复执行一系列操作。
解释执行的语言相比编译执行的语言有什么优缺点?
3.程序的调试
“Bug”这个称谓的由来
编译时错误:语法错误
运行时错误:
逻辑错误和语义错误:
有一种观点认为:编程和调试时一回事,编程就是逐步调试直到获得期望结果为止的过程
4.第一个程序
C程序总是从main里面的第一条语句开始执行
C语言规定每条语句末尾都要有一个;号,C语言用{}号把语法结构分成组;
缩进不是必须的,但是这样使我们更容易看出子语句是属于哪个函数中;
gcc是linux平台的C编译器,一个好的习惯是打开gcc的-Wall选项,也就是让gcc提示所有的警告信息,不管事严重的还是不严重的,然后把这些问题从代码中全部消灭,因为有些警告几乎一定是表明程序中有Bug,而另一些警告只表明程序写得不够规范,所以为了使程序不出现运行时错误,则应该尽量消除警告。
第二章常量、变量和表达式
1.继续hello world
使用注释需要注意:
注释不能嵌套使用;即在/*…*/中间不能再出现/*和*/
可以用// comment来进行单行注释
C标准规定的转义字符
\’ 单引号’
\” 双引号”
\? 问号?
\\ 反斜线\
\a 响铃
\b 退格
\f 分页符
\n 换行符
\r 回车
\t 水平制表符
\v 垂直制表符
可见转义序列有两个作用:
一是把普通字符转义成特殊字符,如把字母n转义为换行符;
二是把特殊字符转义成普通字符,如\和”是特殊字符,转义后取字面值
2.常量
3.变量
变量命名规则除了规定必须以字母或下划线_(Underscore)开头,后面可以跟若干个字母、数字、下划线,不能有其它字符以外,还有一点要注意:一般来说应避免使用以下划线开头的标识符,以下划线开头的标识符只要不和C语言关键字冲突的都是合法的,但是往往被编译器用作一些功能扩展,C语言库的实现也定义了很多以下划线开头的名字,很容易造成名字冲突,所以除非你对编译器和C语言库特别清楚,一般应避免使用这种标识符。
定义与声明的区别:定义(Definition)和声明(Declaration)之间的关系是:定义是声明的一种,如果一个声明要求分配存储空间,则称为定义。
4.赋值
变量一定要先定义再使用,编译器必须先看到定义语句,才知道XXX是变量名,代表一块储存空间,下面使用时才知道去哪儿找这个变量的储存空间。
定义一个变量,就是分配一块储存空间并给它命名,给一个变量赋值,就是把这个值存到了这块储存空间中。
变量的定义和赋值也可以一步完成,这称为变量的初始化。
5.表达式
我们把常量、变量、表达式和语句统一起来:常量可以赋值给变量,也可以和变量、运算符一起组成表达式,最简单的表达式由单个常量或变量组成,任何表达式都有一个值,表达式加上;号构成表达式语句。
等号左边不能是任意组合的表达式,因为等号左边表示的不是一个值而是一个存储位置,称为左值;等号的右边表示要储存的值,可以是任意组合的表达式,所以通常所说的表达式的值也称为右值。
6.字符类型和字符编码
计算机为什么能处理符号?
因为符号在计算机内部是用数字表示的,每个字符在计算机内部是用一个整数表示的,称为字符编码,即现在通用的ASCII码。
之前说“整型”是指int型,而char型本质上也是整数,只不过取值范围比int型小。
第三章简单函数
1.数学函数
使用数学函数必须要包含math.h头文件,即#include <math.h>
再有,在用gcc编译的时候要加上-lm选项:数学函数谓语libm.so库文件中,通常在/lib目录下,-lm选项告诉编译器,我们程序中用到的数学函数要到这个库文件中去找
printf()函数至少有一个参数,即格式化字符串,是字符串类型的,多于一个参数的话,后面的参数为要打印的值,整个printf就是一个函数调用,也就是一个表达式。
2.自定义函数
我们也可以定义自己的函数来用,事实上我们已经这么做了:我们定义了main这个函数。main函数的特殊之处在于执行程序时它自动被系统调用,系统就认准了“main”这个名字,除了名字特殊之外,main函数和别的函数没有区别
返回值类型 函数名(参数列表)
{
语句列表
}
由于我们定义的main函数不带任何参数,参数列表应写成void,main函数的返回值是int类型的,return 0这个语句就表示返回值是0,main函数的返回值是返回给操作系统看的,因为main函数是被操作系统调用的,通常程序执行成功就返回0,在执行过程中出错就返回一个非零值。
怎么查看上一个运行结束的程序的退出状态?
# echo $?
$?是Shell中的一个特殊变量,表示上一个运行结束的程序的退出状态
其实系统在调用main函数时是传参数的,以后会详细解释,所以main函数最标准的形式应该是int main(int argc, char *argv[])。C标准也规定了int main(void)这种形式,如果不使用系统传进来的两个参数也可以写成这种形式。但除了这两种形式之外,以其它形式定义main函数都是错误的或不可移植的。
函数声明、函数定义、函数原型(Prototype)这几个概念
void threeline (void)这一行,声明了一个函数的名字、参数类型和个数、返回值类型,这称为函数原型。
在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体:
void threeline(void);
这种只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。上一章讲过,只有分配存储空间的变量声明才叫变量定义,其实函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也是要占存储空间的。那么没有函数体的函数声明有什么用呢?它为编译器提供了有用信息,编译器在处理代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,然后在碰到函数调用时才知道怎么生成相应的指令,所以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则。
如果使用不带函数体的声明,则可以改变函数的定义顺序,就可以把main()函数写在自己定义的函数之前
如果在调用函数之前没有声明它会怎么样呢?
这里涉及到的规则称为函数的隐式声明(ImplicitDeclaration),在main函数中调用threeline时并没有声明它,则编译器认为此处隐式声明了int threeline(void);,然后为这个调用生成相应的指令,隐式声明的参数类型和个数根据函数调用代码来确定,隐式声明的返回值类型总是int。然后编译器接着往下看,看到threeline函数的原型是void threeline(void),和先前的隐式声明的返回值类型不符,所以才报这个警告。好在我们也没用到这个函数的返回值,所以执行结果仍然正确。
3.形参和实参
定义变量时可以把同样类型的变量列在一起,而定义参数却不可以。
怎么分别形参和实参?
记住这条基本原理:形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化。
C语言传递参数的规则为按值传递(call by value),在调用函数时,每个参数都需要得到一个值,函数定义中有几个Parameter,在调用中就需要传几个Argument,不能多也不能少,每个参数的类型也必须对应上。通常我们说函数提供了一个接口(Interface),调用函数就是使用这个接口,使用的前提是必须和接口保持一致
4.局部变量与全局变量
局部变量:我们把函数中定义的变量称为局部变量(Local Variable),由于形参相当于函数中定义的变量,所以形参也相当于局部变量。在这里“局部”有两个含义:
1. 某个函数中定义的变量不能被另一个函数使用
2. 每次调用函数时局部变量都表示不同的存储空间。局部变量是在每次函数调用时分配存储空间,每次函数返回时释放存储空间的
全局变量和局部变量的区别:
全局变量定义在所有的函数体(包括main函数)之外,它们在整个程序开始之前分配存储空间,在程序结束时释放存储空间,所有函数都可以通过全局变量名访问它们。
main()中定义的也为局部变量。
用全局变量需要注意(不明觉历):
全局变量在整个程序的所有函数中都可以访问,所以在整个程序运行过程中全局变量被读写的顺序从源代码中看不出来(源代码的书写顺序并不能反映函数的调用顺序),出现了Bug往往就是因为在某个不起眼的地方对全局变量的读写顺序不正确,如果代码规模很大,这种错误是很难找出来的。另一方面,对于局部变量的访问不仅局限在一个函数内部,而且局限在一次函数调用之中,从函数的源代码也很容易看出访问的先后顺序是怎样的,所以比较容易找到Bug。因此,虽然全局变量用起来很方便,但一定要慎用,能用函数传参代替的就不要用全局变量。
在C语言中,每个标识符都有特定的作用域(Scope),全局变量是定义在所有函数体之外的标识符,它的作用域从定义的位置开始直到源文件结束,而main函数局部变量的作用域仅限于main函数之中。
如上图所示,设想整个源文件是一张大纸,也就是全局变量的作用域,而main函数是贴在这张大纸上的一张小纸,也就是main函数局部变量的作用域。在小纸上用到标识符hour和minute时应该参考小纸上的定义,因为大纸(全局变量的作用域)被盖住了,如果在小纸上用到某个标识符却没有找到它的定义,那么再去翻看下面的大纸。从这里可以看出:小纸的优先级别比大纸高。
关于局部变量和全局变量的初始化的区别:
1.局部变量可以用任意类型相符的表达式来初始化,而全局变量只能用常量表达式初始化。
全局变量pi这样初始化是合法的:
double pi = 3.14+ 0.0016;
但这样初始化是不合法的:
double pi =acos(-1.0);
然而局部变量这样初始化却是可以的。全局变量的初始值要求保存在编译生成的目标代码中,所以必须在编译时就能计算出来,然而上面第二种Initializer的值必须在生成了目标代码之后在运行时调用acos函数才能知道,所以不能用来初始化全局变量。请注意区分编译时和运行时的概念。为了方便编译器实现这一限制,C语言从语法上规定了全局变量只能用常量表达式来初始化,因此下面这种全局变量初始化也是不合法的:
int minute =360;int hour = minute / 60;
虽然在编译时也可以计算出hour的初始值,但minute / 60不是常量表达式,不符合语法规定。
2.如果全局变量在定义时不初始化,则初始值是0,也就是说,整型的就是0,字符型的就是'\0',浮点型的就是0.0。如果局部变量在定义时不初始化,则初始值是不确定的,所以,局部变量在使用前一定要先赋值,不管是通过初始化还是赋值运算符,如果读取一个不确定的值来使用肯定会引入Bug。
以上2点总结起来就是:全局变量的在定义时可以不用初始化,但是一旦初始化则只能用常量表达式;局部变量在定义时必须初始化,初始化可以用任意类型相符的表达式来初始化。