linux c

第一章 程序的基本概念
程序和编程语言

程序(Program)是一个精确说明如何进行计算的指令序列。

程序由一系列指令(Instruction)组成,指令是指示计算机做某种运算的命令,通常包括以下几类:
输入
输出
基本运算
测试和分枝
循环

编写程序可以说就是这样一个过程:把复杂的任务分解成子任务,把子任务再分解成更简单的任务,层层分解,直到最后简单得可以用以上指令来完成。

程序是那么的复杂,而编写程序可以用的指令却只有这么简单的几种,这中间巨大的落差就要由程序员去填了,所以编写程序理应是一件相当复杂的工作。

语言分类:
编译型:先翻译后执行
解释型:边翻译边执行

高级语言:与平台无关性,跨平台执行。

自然语言和形式语言
自然语言:自然形成
形式语言:人为规定
既严格的语法规则

歧义性(Ambiguity)
冗余性(Redundancy)
与字面意思的一致性

程序的调试
bug和debug

编译时错误:语法错误导致编译不通过
运行时错误:没考虑到运行环境和用户输入而导致的错误
逻辑错误和语义错误:编程时逻辑上错误

第一个程序

#include <stdio.h>

/* main: generate some simple output */

int main(void)
{
	printf("Hello, world.\n");
	return 0;
}

gcc:-o重命名 -Wall显示警告信息
默认名为a.out

第 2 章 常量、变量和表达式
继续Hello World

转义字符
’ 单引号’(Single Quote,或Apostrophe)
" 双引号"
? 问号?(Question Mark)
\ 反斜线\(Backslash)
\a 响铃(Alert,或Bell)
\b 退格(Backspace)
\f 分页符(Form Feed)
\n 换行(Line Feed)
\r 回车(Carriage Return)
\t 水平制表符(Horizontal Tab)
\v 垂直制表符(Vertical Tab)

好的代码风格要求缩进整齐,每个语句一行,适当留空行。

常量

常量(Constant)是程序中最基本的元素,有字符常量(Character Constant)、数字常量和枚举常量。

printf("character: %c\ninteger: %d\nfloating point: %f\n", '}', 34, 3.14);

printf中的这个字符串称为格式化字符串(Format String),它规定了后面几个数据以何种格式插入到这个字符串中,%号(Percent Sign)后面加个字母c、d、f在printf中分别解释成字符型、整型和浮点型的转换说明(Conversion Specification),分别用后面的三个常量来替换它们,也就是说它们只是在格式化字符串中占个位置,并不出现在最终的打印结果中,这种用法通常叫做占位符(Placeholder)。这也是一种字面意思与真实意思不同的情况,但是和转义序列又有区别:转义序列是编译器在处理字符串字面值时转义的,而占位符是由printf解释的,格式化字符串实际包含的字符是character: %c换行integer: %d换行floating point: %f换行,其中的%c仍然是字符串中的两个普通字符,而当字符串传给printf处理时,printf却不把它当成是普通字符,而是解释成占位符。事实上前面例子中的"Hello, world.\n"也是格式化字符串,只不过其中不包含占位符。

变量

变量是计算机存储器中的一块命名的空间,可以在里面存储一个值(Value),存储的值是可以随时变的

关键字
auto break case char const continue default do double
else enum extern float for goto if inline int long
register restrict return short signed sizeof static struct switch typedef
union unsigned void volatile while _Bool _Complex _Imaginary

理解一个概念不是把定义背下来就行了,一定要理解它的外延和内涵,也就是什么情况属于这个概念,什么情况不属于这个概念,什么情况虽然属于这个概念但作为一种最佳实践(Best Practice)应该避免这种情况,这才算是真正理解了。

赋值

char firstletter;
int hour, minute;
firstletter = 'a';   /* give firstletter the value 'a' */
hour = 11;           /* assign the value 11 to hour */
minute = 59;         /* set minute to 59 */

定义一个变量,就是分配一块存储空间并给它命名;给一个变量赋值,就是把一个值存到了这块存储空间中。

变量的定义和赋值也可以一步完成,这称为变量的初始化(Initialization)

char firstletter = 'a';
int hour = 11, minute = 59;

初始化是一种特殊的变量定义语句,而不是一种赋值语句。

表达式

常量和变量之间可以做加减乘除运算,例如1+1、hour-1、hour * 60 + minute、minute/60等。这里的±*/称为运算符(Operator),而参与运算的变量和常量称为操作数(Operand),上面四个由运算符和操作数所组成的算式称为表达式(Expression)。

常量可以赋值给变量,也可以和变量、运算符一起组成表达式,最简单的表达式由单个常量或变量组成,任何表达式都有一个值,表达式可以加个;号构成表达式语句。

习题
1、假设变量x和n是两个正整数,我们知道x/n这个表达式的结果是取Floor,例如x是17,n是4,则结果是4。如果希望结果取Ceiling应该怎么写表达式呢?例如x是17,n是4,则结果是5,而x是16,n是4,则结果是4。

取余,若余大于0则+1

字符类型与字符编码

字符型常量或变量也可以参与运算,例如:

printf("%c\n", 'a'+1);

ASCII码

第 3 章 简单函数
数学函数

#include <math.h>
#include <stdio.h>

int main(void)
{
	double pi = 3.1416;
	printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0));
	return 0;
}

例如sin(pi/2)这种形式。在C语言的术语中,pi/2是参数(Argument),sin是函数(Function),sin(pi/2)是函数调用(Function Call)。这个函数调用在我们的printf语句中处于什么位置呢?通过“表达式”一节的学习我们知道,这应该是放表达式的位置。

函数调用也是一种表达式。

这个表达式由函数调用运算符(也就是括号)和两个操作数组成,操作数sin称为Function Designator,是函数类型(Function Type)的,操作数pi/2是double型的。这个表达式的值就是sin(pi/2)的计算结果,在C语言的术语中称为函数的返回值(Return Value)。

现在我们可以完全理解printf语句了:原来printf也是一个函数,上例的printf语句中有三个参数,第一个参数是格式化字符串,是字符串类型的,第二个和第三个参数是要打印的值,是浮点型的,整个printf就是一个函数调用,也就是一个表达式,因此printf语句也是表达式语句的一种。由于表达式可以传给printf做参数,而sin(pi/2)这个函数调用就是一个表达式,所以根据组合规则,我们可以把sin调用套在printf调用里面,同理log调用也是如此。但是printf感觉不像一个数学函数,为什么呢?因为像sin这种函数,我们传进去一个参数会得到一个返回值,我们用sin函数就是为了用它的返回值,至于printf,我们并不关心返回值(事实上它也有返回值,表示实际打印的字符数),我们用printf不是为了用它的返回值,而是为了利用它所产生的副作用(Side Effect)--打印。C语言的函数可以有Side Effect,这一点是它和数学函数在概念上的根本区别。

Side Effect这个概念也适用于运算符组成的表达式。比如a + b这个表达式也可以看成一个函数调用,运算符+是一个函数,它的两个参数是a和b,返回值是两个参数的和,传入两个参数,得到一个返回值,并没有产生任何Side Effect。而赋值运算符是产生Side Effect的,如果把a = b这个表达式看成函数调用,传入两个参数a和b分别做左值和右值使用,返回值就是所赋的值,既是b的值也是a的值,但除此之外还产生了Side Effect,就是a的值被改变了,改变计算机存储单元里的数据或者做输入或输出操作,这些都算Side Effect。

现在也可以详细解释程序第一行#号(Pound Sign,Number Sign或Hash Sign)后面加个include的确切含义了,它后面写在尖括号(Angel Bracket)中的是一个文件名,称为头文件(Header File),其中描述了我们程序中使用的系统函数,因此要使用printf就必须包含stdio.h,要使用数学函数就必须包含math.h,如果什么系统函数都不用就不必包含任何头文件,例如写一个程序int main(void){int a;a=2;return 0;},不需要包含头文件可以编译通过,当然这个程序什么也做不了。

使用math.h中的函数还有一点特殊之处,gcc命令行必须加-lm选项,因为数学函数位于libm.so库文件中(通常在/lib目录下),-lm选项告诉编译器,我们程序中用到的数学函数要到这个库文件里找。本书用到的大部分库函数(例如printf)位于libc.so库文件中,以后称为libc,使用libc中的库函数在编译时不需要加-lc选项,当然加了也不算错,因为这个选项是gcc默认的。关于头文件和函数库目前理解这么多就可以了,以后再详细解释。

自定义函数
通过main函数的定义我们已经了解函数定义的语法了:

返回值类型 函数名(参数列表)
{
	语句列表
}

由于我们定义的main函数不带任何参数,参数列表应写成void,main函数的返回值是int类型的,return 0这个语句就表示返回值是0,main函数的返回值是返回给操作系统看的,因为main函数是被操作系统调用的,通常程序执行成功就返回0,在执行过程中出错就返回一个非零值。

$?是Shell中的一个特殊变量,表示上一个运行结束的程序的退出状态。

#include <stdio.h>

void newline(void)
{
	printf("\n");
}

void threeline(void)
{
	newline();
	newline();
	newline();
}

int main(void)
{
	printf("Three lines:\n");
	threeline();
	printf("Another three lines.\n");
	threeline();
	return 0;
}

现在澄清一下函数声明、函数定义、函数原型(Prototype)这几个概念。比如void threeline(void)这一行,声明了一个函数的名字、参数类型和个数、返回值类型,这称为函数原型。在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体,例如:

void threeline(void);

这种只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。上一章讲过,只有分配存储空间的变量声明才叫变量定义,其实函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也是要占存储空间的。那么没有函数体的函数声明有什么用呢?它为编译器提供了有用信息,编译器在处理代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,然后在碰到函数调用时才知道怎么生成相应的指令,所以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则。

如果函数newline没有返回值,那么表达式newline()不就没有值了吗?然而我在前面却说任何表达式都有一个值。其实这正是设计void这么一个关键字的原因:让没有返回值的函数调用有一个void值。然后再规定,表达式的计算结果可以是void值,但表达式不能使用void值做计算,也就是说,如果一个表达式的值为void,就不能把它当作另一个表达式的一部分来用。从而兼顾语法上的一致(任何表达式都有值,如果一个表达式实在没有值就说它有void值)和语义上的不矛盾(void值是虚构出来的,不能做计算)。

形参和实参

形式参数:左值
实际参数:右值

形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化。
定义变量时可以把同样类型的变量列在一起,而定义参数却不可以。
在调用函数时,每个参数都需要得到一个值,函数定义中有几个Parameter,在调用中就需要传几个Argument,不能多也不能少,每个参数的类型也必须对应上。但是为什么我们调用printf函数时传的Argument数目是变化的,有时一个有时两个甚至更多个?这是因为C语言规定了一种特殊的参数列表格式,例如printf的原型是这样的:

int printf(const char *format, ...);

第一个参数是const char *类型的,后面的…可以代表0个或任意多个参数,这些参数的类型也是不确定的,这称为可变参数(Variable Argument),以后我们再详细讨论这种格式。总之,任何函数的定义既规定了返回值的类型,也规定了参数的类型和个数,即使像printf这样规定为“不确定”也是一种明确的规定,调用函数就要严格遵守这些规定,通常我们说函数提供了一个接口(Interface),调用函数就是使用这个接口,使用的前提是必须和接口保持一致。

习题:
1、定义一个函数increment,它的作用是将传进来的参数加1,然后在main函数中用increment函数来增加变量的值:

void increment(int x)
{
	x = x + 1;
}

int main(void)
{
	int i = 1, j = 2;
	increment(i); /* now i becomes 2 */
	increment(j); /* now j becomes 3 */
	return 0;
}

这个increment函数能奏效吗?为什么?

不能x在increment内是局部变量和j,i没有关系。

局部变量与全局变量

我们把函数中定义的变量称为局部变量(Local Variable),由于形参相当于函数中定义的变量,所以形参也相当于局部变量。

在这里“局部”有两个含义:
1、某个函数中定义的变量不能被另一个函数使用。
2、每次调用函数时局部变量都表示不同的存储空间。

与局部变量的概念相对的是全局变量(Global Variable),全局变量定义在所有的函数体之外,它们在整个程序开始之前分配存储空间,在程序结束时释放存储空间,所有函数都可以通过全局变量名访问它们。

虽然全局变量用起来很方便,但一定要慎用,能用函数传参代替的就不要用全局变量。
则第一次调用print_time打印的是全局变量的值,第二次直接调用printf打印的则是main函数的局部变量的值。

在C语言中,每个标识符都有特定的作用域(Scope),全局变量是定义在所有函数体之外的标识符,它的作用域从定义的位置开始直到源文件结束,而main函数局部变量的作用域仅限于main函数之中。

函数:先查找局部变量,如没有再去查找全局变量。

局部变量可以用任意类型相符的表达式来初始化,而全局变量只能用常量表达式初始化。

全局变量的初始值要求保存在编译生成的目标代码中,所以必须在编译时就能计算出来,然而上面第二种Initializer的值必须在生成了目标代码之后在运行时调用acos函数才能知道,所以不能用来初始化全局变量。请注意区分编译时和运行时的概念。

如果全局变量在定义时不初始化,则初始值是0,也就是说,整型的就是0,字符型的就是’\0’,浮点型的就是0.0。如果局部变量在定义时不初始化,则初始值是不确定的。

局部变量在使用前一定要先赋值,不管是通过初始化还是赋值运算符。

第 4 章 分支语句
if语句

if (x != 0) {
	printf("x is nonzero.\n");
}

其中x != 0表示“x不等于0”这个条件,这个表达式称为控制表达式(Controlling Expression)如果条件成立,则{}中的语句被执行,否则{}中的语句不执行,直接跳到}后面。if和控制表达式改变了程序的控制流程(Control Flow),不再是从前到后顺序执行,而是根据不同的条件执行不同的语句,这种控制流程称为分支(Branch)。上例中的!=号表示“不等于”

关系运算符和相等性运算符:

==	等于
!=	不等于
>	大于
<	小于
>=	大于或等于
<=	小于或等于

注意以下几点:

这里的==表示数学中的相等关系,相当于数学中的=号,初学者常犯的错误是在控制表达式中把==写成=,在C语言中=号是赋值运算符,两者的含义完全不同。

如果表达式所表示的比较关系成立则值为真(True),否则为假(False),在C语言中分别用1和0表示。例如x是-1,那么x>0这个表达式的值为0,x>-2这个表达式的值为1。

在数学中a<b<c表示b既大于a又小于c,但作为C语言表达式却不是这样。以上几种运算符都是左结合的,请读者想一下这个表达式表示什么?a<b ==> 1 || 0 ==> 1<c || 0<c

这些运算符的两个操作数都应该是相同类型的,例如两边都是字符型、都是整型或者都是浮点型,但不能比较两个字符串,以后我们会介绍比较字符串的方法。

==和!=称为相等性运算符(Equality Operator),其余四个称为关系运算符(Relational Operator),相等性运算符的优先级低于关系运算符。

语句块中也可以定义局部变量,就像函数体一样。例如:

void foo(void)
{
	int i = 0;
	{
		int i = 1;
		int j = 2;
		printf("i=%d, j=%d\n", i, j);
	}
	printf("i=%d\n", i); /* cannot access j here */
}

和函数的局部变量同样道理,每次进入语句块时为变量j分配存储空间,每次退出语句块时释放变量j的存储空间。语句块也构成一个作用域,如果整个源文件是一张大纸,foo函数是贴在上面的一张小纸,则函数中的语句块是贴在小纸上面的一张更小的纸。语句块中的变量i和函数的变量i是两个不同的变量,因此两次打印的i值是不同的;语句块中的变量j在退出语句块之后就没有了,因此最后一行的printf不能打印变量j,否则编译器报错。从这个例子也可以看出,语句块可以用在任何允许放语句的地方,不一定非得用在if语句中,单独使用语句块通常是为了定义一些比函数的局部变量更“局部”的变量。

习题
1、以下是程序段编译能通过,执行也不出错,但是执行结果不正确(根据“程序的调试”一节,这是一个语义错误),请分析一下哪里错了。还有,既然错了为什么编译能通过呢?

if (x > 0);
printf("x is positive.\n");

if后面多了个;代表if语句结束了。判断之后什么都不执行。

if/else语句

if语句还可以带一个else子句(Clause),例如:

if (x % 2 == 0)
	printf("x is even.\n");
else
	printf("x is odd.\n");

这里的%是取模(Modulo)运算符,x%2表示x除以2所得的余数(Remainder),%运算符的两个操作数必须是整型。

如果/运算符的两个操作数都是整数,则结果是两数的商(Quotient),余数总是舍去,因此有如下结论成立:a和b是两个整数,b不等于0,(a/b)*b+a%b总是等于a。

if (A)
	if (B)
		C;
	else
		D;

在C语言中缩进只是为了程序员看起来方便,实际上对编译器不起任何作用,你的代码不管写成上面哪一种缩进格式,在编译器看起来都是一样的。那么编译器到底认为是哪一种理解呢?
也就是说,else到底是和if (A)配对还是和if (B)配对?
C语言规定,else总是和它上面最近的一个if配对,因此应该理解成else和if (B)配对,也就是上面第二种理解。

习题
1、写两个表达式,分别取整数x的个位和十位
2、写一个函数,参数是整数x,功能是打印参数x的个位和十位

#include <stdio.h>

void integer_place(int num);

void integer_place(int num)
{
        int ones_place,ten_place;

        ones_place = num%10;
        ten_place = num/10;
        printf("ones_place = %d\nten_place = %d\n", ones_place, ten_place);
}


int main(void)
{
        int num = 87;
        
        integer_place(num);
        return 0;
}

布尔代数

and &&:有假则假,全真为真。
or ||:有真则真,全假为假。
not !:真假互换。

!高于*/%,高于±,高于>、<、>=、<=,高于==、!=,高于&&,高于||

如果记不清楚运算符的优先级顺序一定要套括号。
不过这几个运算符的优先级顺序是应该记住的,因为你需要看懂别人写的不套括号的代码。

习题

1、把代码段

if (x > 0 && x < 10);
else
   printf("x is out of range.\n");
改写成下面这种形式:

if (x <= 0 || x >= 10)
   printf("x is out of range.\n");

2、把代码段:

if (x > 0)
   printf("Test OK!\n");
else if (x <= 0 && y > 0)
   printf("Test OK!\n");
else
   printf("Test failed!\n");
改写成下面这种形式:

if (y <= 0 && x<= 0)
   printf("Test failed!\n");
else
   printf("Test OK!\n");

3、有这样一段代码:

if (x > 1 && y != 1) {
   ......
} else if (x < 1 && y != 1) {
   ......
} else {
   ......
}
要进入最后一个else,x和y需要满足条件 x == 1 || y == 14、以下哪一个if判断条件是多余的可以去掉?

if (x<3 && y>3)
   printf("Test OK!\n");
else if (x>=3 && y>=3)
   printf("Test OK!\n");
else if (z>3 && x>=3)
   printf("Test OK!\n");
else if (z<=3 && y>=3)
   printf("Test OK!\n");
else
   printf("Test failed!\n");

else if (x>=3 && y>=3)

switch语句

switch语句可以产生具有多个分支的控制流程。它的格式是:

switch(控制表达式) {
case 常量表达式:语句序列
case 常量表达式:语句序列
default:语句序列
}


使用switch语句要注意几点:

1.case后面跟的必须是常量表达式,原因同“局部变量与全局变量”一节一样,因为这个值必须在编译时计算出来。

2.以后我们会讲到,浮点型是不能精确比较相等不相等的。因此C语言规定case后面跟的常量表达式的值必须是可以精确比较的整型或字符型。

3.进入case后如果没有遇到break语句就会一直往下执行,后面其它case或default下面的语句也会被执行到,直到遇到break,或者执行到整个switch语句块的末尾。通常每个case后面都要加上break语句,但有时候故意不加break来利用这个特性。

4.break语句,它的作用是跳出整个switch语句块。C语言规定各case的常量表达式必须互不相同,如果控制表达式不等于任何一个常量表达式,则从default分支开始执行,通常把default分支写在最后,但不是必须的。

第 5 章 深入理解函数
return语句

之前我们一直在main函数中使用return语句,现在是时候全面深入地学习一下了。在有返回值的函数中,return语句的作用是提供整个函数的返回值,并结束当前函数的执行。在没有返回值的函数中也可以使用return语句,例如当检查到一个错误时提前结束当前函数的执行:

#include <math.h>

void print_logarithm(double x)
{
	if (x <= 0.0) {
		printf("Positive numbers only, please.\n");
		return;
	}
	printf("The log of x is %f", log(x));
}

函数返回一个值相当于定义一个和函数返回值类型相同的临时变量并用return后面的表达式来初始化。

习题
1、编写一个布尔函数int is_leap_year(int year),判断参数year是不是闰年。如果某一年的年份能被4整除,但不能被100整除,那么这一年就是闰年,此外,能被400整除的年份也是闰年。

#include <stdio.h>
#include <math.h>

int is_leap_year(int year);

int is_leap_year(int year)
{
        if (year % 400 == 0)
                return 1;
        else if ((year % 4 == 0) && (year % 100 != 0))
                return 1;
        else
                return 0;
}

int main(void)
{
        int year = 300;

        if(is_leap_year(year))
                printf("toyear is leap\n");
        else
                printf("toyear is not leap\n");

        return 0;
}

增量式开发

#include <stdio.h>
#include <math.h>

double distance(double x1, double y1, double x2, double y2);
double area(double radius);

double area(double radius)
{
        return 3.1416 * radius * radius;
}

double distance(double x1, double y1, double x2, double y2)
{
        double dx = x2 -x1;
        double dy = y2 -y1;
        double dsquared = dx * dx + dy * dy;
        double result = sqrt(dsquared);

        return result;
}

int main(void)
{

        double radius = distance(1.0, 2.0, 4.0, 6.0);
        printf("distance is %f\n", radius);
        double area1 = area(radius);
        printf("area is %f\n", area1);


        return 0;
}

递归

如果定义一个概念需要用到这个概念本身,我们称它的定义是递归的(Recursive)。

#include <stdio.h>
#include <math.h>

int factorial(int n);

int factorial(int n)
{
        if (n == 0)
                return 1;
        else {
                int recurse = factorial(n - 1);
                int result = n * recurse;
                return result;
        }
}

int main(void)
{
        int n = 5;

        printf("%d! is %d\n", n, factorial(n));

        return 0;
}

Base Case(递归底/基础条件)正确,递推关系正确,则递归正确!

习题
1、编写递归函数求两个正整数a和b的最大公约数(GCD,Greatest Common Divisor),使用Euclid算法:

如果a除以b能整除,则最大公约数是b。

否则,最大公约数等于b和a%b的最大公约数。

Euclid算法是很容易证明的,请读者自己证明一下为什么这么算就能算出最大公约数。

2、编写递归函数求Fibonacci数列的第n项,这个数列是这样定义的:

fib(0)=1
fib(1)=1
fib(n)=fib(n-1)+fib(n-2)

上面两个看似毫不相干的问题之间却有一个有意思的联系:

Lamé定理
如果Euclid算法需要k步来计算两个数的GCD,那么这两个数之中较小的一个必然大于等于Fibonacci数列的第k项。

#include <stdio.h>
#include <math.h>

int euclid(int x, int y);

int euclid(int x, int y)
{
        if (x % y == 0)
                return y;
        else {
                int result = euclid(y, x % y);
                return result;
        }
}

int main(void)
{
        int x = 80, y = 40;
        printf("%d and %d GCD is %d\n", x, y,euclid(x, y));
}
#include <stdio.h>
#include <math.h>

int fib(int n);

int fib(int n)
{
        if (n == 0 || n == 1)
                return 1;
        else {
                int result = fib(n - 1) + fib(n - 2);
                return result;
        }
}

int main(void)
{
        int n = 19;

        printf("%d fib is %d\n",n + 2, fib(n));

        return 0;

}

Base Case(递归底/基础条件)正确,递推关系正确,则递归正确!!!

第 6 章 循环语句
while语句

int factorial(int n)
{
	int result = 1;
	while (n > 0) {
		result = result * n;
		n = n - 1;
	}
	return result;
}

像if语句一样,while由一个控制表达式和一个子语句组成,子语句可以是由若干条语句组成的语句块。如果控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值……这种控制流程称为循环(Loop),子语句称为循环体。如果某一次测试控制表达式的值为假,就跳出循环执行后面的return语句,如果第一次测试控制表达式的值就是假,那么直接跳到return语句,循环体一次都不执行。

变量result在这个循环中的作用是累加器(Accumulator)

习题
1、用循环来解决“递归”一节的练习题,体会递归和循环这两种不同的思路。

2、编写程序数一下1到100的所有整数中出现多少次数字9。在写程序之前先把这些问题考虑清楚:

这个问题中的循环变量是什么?

这个问题中的累加器是什么?用加法还是用乘法累积?

取一个整数的个位和十位在“if/else语句”一节的练习中已经练过了,这两个表达式应该怎样用在程序中?

#include <stdio.h>
#include <math.h>

int euclid(int x, int y);

int euclid(int x, int y)
{
        while (x % y != 0)
        {
                int z = 0;

                z = x;
                x = y;
                y = z % x;
        }
        return y;
}

int main(void)
{
        int x = 80, y = 40;

        printf("%d and %d GCD is %d\n", x, y, euclid(x, y));

        return 0;
}
#include <stdio.h>
#include <math.h>

int numn(int n);

int numn(int n)
{
        int num = 0, max = 100, z = 0;
        while(num < max){
                if(num / 10 == n){
                        z = z + 1;
                }
                if(num % 10 == n && (num / 10 != num % 10)){
                        z = z + 1;
                }
        num = num + 1;
        }
        printf("%d have %d\n", n, z);

        return 0;
}

int main(void)
{
        int n = 9;
        numn(n);

        return 0;

}

do/while语句

do/while语句的格式是:

do
	语句
while(控制表达式);

注意do/while这种形式在while(控制表达式)后面一定要加;号
至少执行一次

for语句

for语句的格式为:

for(控制表达式1;控制表达式2;控制表达式3)
	语句
int factorial(int n)
{
	int result = 1;
	int i;
	for(i = 1; i <= n; ++i)
		result = result * i;
	return result;
}

其中++i这个表达式相当于i = i + 1,++称为前缀自增运算符(Prefix Increment Operator),类似地,–称为前缀自减运算符(Prefix Decrement Operator),–i相当于i = i - 1[11]。如果把++i这个表达式看作一个函数调用,除了传入一个参数返回一个值(等于参数值加1)之外,还产生一个Side Effect,就是把变量i的值增加了1。

++和–运算符也可以用在变量后面,例如i++和i–,为了和前缀运算符区别,称为后缀自增运算符(Postfix Increment Operator)和后缀自减运算符(Postfix Decrement Operator)。如果把i++这个表达式看作一个函数调用,除了传入一个参数返回一个值(就等于参数值)之外,还产生一个Side Effect,就是把变量i的值增加了1,它和++i的区别就在于返回值不同。同理,–i返回减1之后的值,而i–返回减1之前的值,但这两个表达式都产生同样的Side Effect,就是把变量i的值减了1。

break和continue语句

break语句的一种用法,用来跳出switch语句块,这个语句也可以用来跳出循环体。continue语句也用来终止当前循环,和break语句不同的是,continue语句终止当前循环后又回到循环体的开头准备再次执行循环体。对于while和do/while,continue之后测试控制表达式,如果值为真则继续执行下一次循环;对于for循环,continue之后首先计算控制表达式3,然后测试控制表达式2,如果值为真则继续执行下一次循环。

例 6.1. 求1-100的素数

#include <stdio.h>

int is_prime(int n)
{
	int i;
	for (i = 2; i < n; i++)
		if (n % i == 0)
			break;
	if (i == n)
		return 1;
	else
		return 0;
}

int main(void)
{
	int i;
	for (i = 1; i <= 100; i++) {
		if (!is_prime(i))
			continue;
		printf("%d\n", i);
	}
	return 0;
}

习题
1、求素数这个程序只是为了说明break和continue的用法才这么写的,其实完全可以不用break和continue,请读者修改一下循环的结构,去掉break和continue而保持功能不变。

2、在上一节中讲过怎样把for语句写成等价的while语句,但也提到如果循环体中有continue语句,这两种形式就不等价了,想一想为什么不等价了?

#include <stdio.h>

int is_prime(int n)
{
        int i;
        for (i = 2; i < n; i++)
                if (n % i == 0)
                        if (i == n)
                                return 1;
                        else
                                return 0;
}

int main(void)
{
        int i;
        for (i = 1; i <= 100; i++) {
                if (!is_prime(i));
                else{
                        printf("%d\n", i);
                }
        }
        return 0;
}

嵌套循环

习题
1、上面打印的小九九有一半数据是重复的,因为89和98的结果一样。请修改程序打印这样的小九九:

1
2 4
3 6 9
4 8 12 16
5 10 15 20 25
6 12 18 24 30 36
7 14 21 28 35 42 49
8 16 24 32 40 48 56 64
9 18 27 36 45 54 63 72 81

2、编写函数diamond打印一个菱形。如果调用diamond(3, '*'),则打印:

	*
*	*	*
	*
如果调用diamond(5, '+'),则打印:

		+
	+	+	+
+	+	+	+	+
	+	+	+
		+
如果用偶数做参数则打印错误信息。
#include <stdio.h>

int main(void)
{
        int i, j;
        for (i=1; i<=9; i++) {
                for (j=1; j<=i; j++)
                        printf("%d  ", i*j);
                printf("\n");
        }
        return 0;
}

goto语句

分支、循环都讲完了,现在只剩下最后一种影响控制流程的语句了,就是goto语句,实现无条件跳转。我们知道break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳到循环之外的某个地方做出错处理,就可以用goto语句,例如:

for (...)
	for (...) {
		...
		if (出现错误条件)
			goto error;
	}
error:
	出错处理;

滥用goto语句会使程序的控制流程非常复杂,可读性很差。

第 7 章 结构体
复合数据类型--结构体

但字符串是一个例外,它由很多字符组成,像这种由基本类型组成的数据类型称为复合数据类型(Compound Type),正如表达式和语句有组合规则一样,由基本类型组成复合类型也有一些组合规则,例如本章要讲的结构体,以及第 8 章 数组要讲的数组和字符串。
复合数据类型一方面可以从整体上当作一个数据使用,另一方面也可以分别访问它的各组成单元,复合数据类型的这种两面性提供了一种数据抽象(Data Abstraction)的方法。

[SICP]指出,在学习一门编程语言时,要特别注意以下三方面:

这门语言提供了哪些Primitive,比如基本数据类型,比如基本的运算符、表达式和语句。

这门语言提供了哪些组合规则,比如复合数据类型,比如表达式和语句的组合规则。

这门语言提供了哪些抽象机制,例如数据抽象和过程抽象(Procedure Abstraction)。

比如用实部和虚部表示一个复数,我们可以采用两个double型组成的结构体:

struct complex_struct {
	double x, y;
};

这样定义了complex_struct这个标识符,既然是标识符,那么它的命名规则就和变量一样,但它不表示一个变量,而表示一个类型,这种标识符在C语言中称为Tag,struct complex_struct { double x, y; }整个可以看作一个类型名,就像int或double一样,只不过它是一个复合类型[12]

#include <stdio.h>

int main(void)
{
	struct complex_struct { double x, y; } z;//定义类型为complex_struct同时声明变量z
	double x = 3.0;	
	z.x = x;//为complex_struct的成员x赋值
	z.y = 4.0;
	if (z.y < 0)
		printf("z=%f%fi\n", z.x, z.y);
	else
		printf("z=%f+%fi\n", z.x, z.y);

	return 0;
}

注意上例中变量x和变量z的成员x的名字并不冲突,因为变量z的成员x总是用.运算符来访问的,编译器可以区分开哪个x是变量x,哪个x是变量z的成员x,它们属于不同的命名空间(Name Space)。Tag也可以定义在函数外面,就像全局变量一样,这样定义的Tag在其定义之后的各函数中都可以使用。

数据抽象

All problems in computer science can be solved by another level of indirection.

这里要介绍的编程思想称为抽象。其实“抽象”这个概念并没有那么抽象,简单地说就是“提取公因式”:ab+ac=a(b+c)。如果a变了,ab和ac这两项都需要改,但如果写成a(b+c)的形式就只需要改其中一个因子。
组合使得系统可以任意复杂,而抽象使得系统的复杂性是可以控制的,任何改动都只局限在某一层,而不会波及整个系统。

数据类型标志

enum关键字,表示一个枚举(Enumeration)类型。枚举类型的成员是常量,它们的值编译器自动分配,默认从0开始。

嵌套结构体

结构体也是一种递归定义:结构体由数据类型定义,因为结构体的成员具有数据类型,而数据类型由结构体定义,因为结构体本身也是一种数据类型。换句话说,结构体也可以嵌套。

第 8 章 数组
数组的基本操作

和结构体类似,数组(Array)也是一种复合数据类型,它由一系列相同类型的元素(Element)组成

数组类型的长度应该用一个常量表达式来指定[15],而且这个常量表达式的值必须是整数类型的,这一点和case后面跟的常量表达式的要求相同。数组中的元素通过下标(或者叫索引,Index)来访问。例如前面定义的由4个整数组成的数组count图示如下:

数组和结构体虽然有很多相似之处,但也有一个显著的不同:数组不能互相赋值。
既然不能互相赋值,也就不能用数组类型作为函数的参数或返回值。

数组名做右值使用时,自动转换成指向数组首元素的指针。

习题
1、编写一个程序,定义两个类型和长度都相同的数组,将其中一个数组的所有元素拷贝给另一个。既然数组不能直接赋值,想想应该怎么实现。

#include <stdio.h>

int main(void)
{
	int count1[4] = { 3, 2, }, count2[4], i;

	for (i = 0; i < 4; i++){
		count2[i] = count1[i];
		printf("count2[%d] = %d\n", i, count2[i]);
	}		
	
	return 0;
}

数组应用实例:统计随机数

写代码时应尽可能避免硬编码

数组应用实例:直方图

字符串

字符串可以看作一个数组,它的元素是字符型的,例如字符串"Hello, world.\n"图示如下:

printf会从数组str的开头一直打印到’\0’字符为止(’\0’本身不打印)。

多维数组
就像结构体可以嵌套一样,数组也可以嵌套,一个数组的元素可以是另外一个数组,这样就构成了多维数组(Multi-dimensional Array)。

注意,除了第一维的长度可以由编译器自动计算而不需要指定,其余各维都必须明确指定长度。

写代码最重要的是选择正确的数据结构来组织信息,设计控制流程和算法尚在其次,只要数据结构选择得正确,其它代码自然而然就变得容易理解和维护了

习题:
(man - computer + 4) % 3 - 1这个神奇的表达式是如何比较出0、1、2这三个数字在“剪刀石头布”意义上的大小的?

(man - computer + 4) % 3 有3种可能 1.2.3减去1则是0.1.2对应赢平输。

第 9 章 编码风格
缩进和空白

1、关键字if, while, for与其后的控制表达式的(括号之间插入一个空格分隔,但括号内的表达式应紧贴括号。

2、双目运算符的两侧插入一个空格分隔,单目运算符和操作数之间不加空格,例如i␣=␣i␣+␣1、++i、!(i␣<␣1)、-x、&a[1]等。

3、后缀运算符和操作数之间也不加空格,例如取结构体成员s.a、函数调用foo(arg1)、取数组成员a[i]。

4、,号和;号之后要加空格,这是英文的书写习惯,例如for␣(i␣=␣1;␣i␣<␣10;␣i++)、foo(arg1,␣arg2)。

5、以上关于双目运算符和后缀运算符的规则不是严格要求,有时候为了突出优先级也可以写得更紧凑一些,例如for␣(i=1;␣i<10;␣i++)、distance␣=␣sqrt(xx␣+␣yy)等。但是省略的空格一定不要误导了读代码的人,例如a||b␣&&␣c很容易让人理解成错误的优先级。

6、由于标准的Linux终端是24行80列的,接近或大于80个字符的较长语句要折行写,折行后用空格和上面的表达式或参数对齐,

7、较长的字符串可以断成多个字符串然后分行书写,

8、有的人喜欢在变量定义语句中用Tab字符,使变量名对齐,这样看起来也很好,但不是严格要求的。

注释

/**/
//
#

标识符命名
函数
indent工具

第 10 章 gdb

第 11 章 排序与查找

第 12 章 栈与队列
数据结构的概念

数据结构(Data Structure)是数据的组织方式。
程序中用到的数据都不是孤立的,而是有相互联系的,根据访问数据的需求不同,同样的数据可以有多种不同的组织方式。

算法+数据结构=程序

堆栈

堆栈是一组元素的集合,类似于数组,不同之处在于,数组可以按下标随机访问,这次访问a[5]下次可以访问a[1],但是堆栈的访问规则被限制为Push和Pop两种操作,Push(入栈或压栈)向栈顶添加元素,Pop(出栈或弹出)则取出当前栈顶的元素,也就是说,只能访问栈顶元素而不能访问栈中其它元素。

堆栈这种数据结构的特点可以概括为LIFO(Last In First Out,后进先出)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值