1. Hello World!
依照惯例首先Hello World镇楼:
C源文件组成:
(1) 预处理指令(不是c语句)
(2) 函数和外部变量声明(c语句)
(3) 函数定义
1) 函数头部
2) 函数体
2 . 数据类型
C的数据类型分为基本类型和构造类型。其中基本类型包括字节型(char)、整型(int)、浮点型(float)、双精度浮点型(double)。构造类型是指在基本类型上构造的数组,指针和结构体,枚举,联合等数据类型。
1. 基本类型
(1)整型
1. 整型(int):32bit OS下一般为4 bytes,INT_MAX宏标记了最大的int数值:2147483647,(2^31 - 1)。
2. 字符型(char):依据C标准定义,在任何环境下sizeof(char)都是1。存储字符的ASCII码(二者无条件等价),可以参与算术运算。
3. 短整型(short int):C标准规定short int长度不比int长,具体长度由编译器自定。
4. 长整型(long int):C标准规定long int长度不比int短,具体长度由编译器自定。
包含<stdint.h>头文件后可以使用int32_t、int_64t等类型,可以实现32位,64位整数的运算。
(2)浮点型
1. 单精度浮点型(float):4 bytes,6位有效数字,绝对值范围3.4E38。在计算时自动提升为double型。
2. 双精度浮点型(double):8 bytes,15位有效数字,绝对值范围:1.7E308。用于存储实数,由小数部分和指数部分组成(均为二进制),由于小数点的位置可以浮动(调节指数保证恒等)所以称为浮点数,但在计算机中用规范浮点类型(小数点前为0,小数点右第一位不为0)。
浮点误差:
因为计算机中采取二进制指数存储浮点数,并因为指数计算或者截断导致存在误差。通常定义一个很小的正数eps作为浮点误差限,当浮点数小于它时就认为浮点数为0。
由于浮点误差的存在,应尽量避免使用浮点数进行流程控制,如果必须使用浮点数则要避免使用相等(==)或不等(!=)关系运算。
2.构造类型
1)结构体
struct Node { int val; } node_a;
在成员表列中声明结构体成员,格式同声明变量。成员可以是任何数据类型(变量,数组,指针也可以是其它构造类型),但不能是其自身(这样会无限嵌套)。
结构体类型是其成员的集合,成员结构体类型的长度不小于成员长度之和(参见“内存对齐”)。
只能访问或修改结构体中基本类型成员,不能直接修改结构体自身。
结构体是一种重要而灵活的构造类型,对于结构体的拓展产生了类(class)这一伟大的概念。
2)联合
几个不同的变量共享同一段内存的结构称为共用体类型(union,联合)。因为在每一个时段,同一段内存只能存放唯一内容,也就是说union变量中只能存放一个值。
可以将union理解为VB中的变体型,通过引用不同的成员而变为不同类型的变量。
可以对union初始化,但初始化表中只能用有一个常量。
union UnionName Object={.member = var};
当省略成员名时对第一个成员初始化。
同类的struct/union对象可以互相赋值。struct,union同样也有数组。
在函数调用过程中,对于struct通常传递其指针而非对象本身以减少开销。
3)枚举
enum EnumName { member1,member2 = var, ... } object1,...,objectn;
枚举元素表列由几个枚举元素名(枚举常量名)组成,中间用逗号分隔(类似初始化表列),每一个元素代表一个整数,按定义顺序默认为0,1,2…。也可以用赋值语句进行强制赋值,如{a = 1 , b = 2 }。可以声明枚举对象,枚举元素也可以直接使用。
#include<stdio.h> enum Num {zero, one}; int main(void) { enum Num n = one; printf("%d %d ",n,one); return 0; }
枚举变量,简单宏,常变量(const)是C中常用的使用符号常量的方法。
通常将struct、union和enum的第一个字母用大写表示以和系统定义的类型名区别(这不是规定只是习惯)。
3. typedef关键字
typedef关键字用于为一个类型生成一个别名:
typedef Old New;
在使用typedef后Old和New均可以作为类型关键字定义变量。Old是在使用typedef之前已经存在的类型关键字,它可以是基本类型(如 int),构造类型(如int *)或者自定义类型(如struct node)也可以带有关键字修饰(如const,static)。
typedef可以定义一个类型名代替一个定长数组 typedef int arr[Size]; ,arr a可以像int a[Size]一样定义数组。
因为自定义类型需要struct等关键字修饰,通常使用typedef关键字简化,如:
typedef struct Node { int val; } Node;
使用上述语句后,Node 即可代替struct Node作为类型关键字。实际上struct Node连同其定义一起充当了Old类型名。
这种定义在链式存储结构中常见,第3行中的struct关键字不能省略因为此时typedef语句尚未定义Node作为类型别名,而struct Node则在花括号开始处即生效。
typedef关键字定义函数指针类型:
typedef (*ptr)(...);
调用 (*ptr)(...);
简单宏也可以实现类型别名的功能,但是typedef定义的功能更为强大。如 typedef int* ptr; ptr p,q;
#define int* ptr { ptr p, q; } 则定义int指针变量p,和int变量q。
建议使用typedef关键字定义类型别名而不是使用宏。C++继承了C中typedef关键字的用法,由于类模板和命名空间使得名称复杂typedef起到了更为关键的作用。
4. 字面值
直接书写于源代码中的值称为字面值,如0,1等。字符串也常以字面值的形式出现,如"Hello World! "。字符串字面值将于字符串一节说明,其他类型的字面值往往会使阅读者无法得知其含义(所谓魔数magic)。除了0,1等含义明确的字面值外,其余字面值应使用宏或者常量,以提高代码可读性和可维护性。
3.变量(对象)
在C中,内存中的对象一般被称为变量,而在大多数面向对象语言中它们被称为对象。以对象的观点理解变量比较容易理解诸如常变量之类的概念。
C为静态类型语言,对象的类型在定义后不能改变。C使用类型关键字+标识符来定义对象,如int a;。标识符可以由字母、数字或下划线(_)组成,不能以数字开头,区分大小写,不能与关键字相同。 定义变量后C不会自动初始化,必须显式的初始化int a = 0;。
作用域是指一个变量有效的范围,C变量的作用域包括文件作用域和代码块作用域(函数体也是一个代码块)。变量的生存期则是指对象内存空间从开辟到释放的周期,一般非静态变量的生存期是其作用域代码块执行期。
具体变量的使用方式和特点如下表:
<1>自动变量 / 寄存器变量:
定义:函数内auto或缺省关键字声明;
函数内register声明。
作用域:函数内(空链接)
生存期:自动(函数调用期)
<2>空链接静态变量
定义:函数内static关键字声明
作用域:函数内(空链接)
生存期:静态(程序运行期)
<3>外部链接的静态变量
定义:函数外extern或空关键字声明
作用域:所有程序文件(外部链接)
生存期:静态(程序运行期)
<4>内部链接的静态变量
定义:函数外static关键字声明
作用域:本源文件(内部链接)
生存期:静态
寄存器变量位于CPU寄存器中,调用较快。编译器会将调用频繁的变量自动存入寄存器中以提高效率。静态变量在函数调用结束后不释放,下一次调用保持原值,外部变量不需要static生存期即为静态。
常对象与const关键字
对变量使用const声明,则此变量只允许调用不允许改变它的值。
1)对指针使用const
位于*左边任意位置的const使得指针指向的数据成为常量,位于*右边的const使得指针本身成为常量。靠近变量的使指针变量成为常量,靠近类型的让指向类型成为常量。
在函数原型和函数头部,参量(const 类型名 数组名[])(const 类型名 *指针名)表明数组中的元素是不允许改变的。使用const关键字可对数组提供保护(就像传值对基本类型提供保护一样),避免数组被意外修改。
2)对外部变量使用const
使用外部变量时容易因为变量意外被修改而造成不易察觉的错误,使用const将为外部变量提供保护。外部常变量可用于重置变量,特别是重置指针变量。
4. 运算符、表达式和语句
C表达式由操作数(operand)和运算符(operator)组成, 每一个表达式有且只有一个值。表达式可以结合,复杂表达式的求解顺序由运算符的优先级和结合性来确定。
运算符可以粗略地分为初等运算符(() . -> [] ),单目运算符(! ++ -- sizeof & * cast运算符…),算术运算符(+ - * / %),关系和条件运算符(== != > < ?:…),赋值运算符(= +=…),逗号运算符(,)。优先级从高到低,除单目运算符,赋值运算符和条件运算符从右向左结合外,其余运算符都是从左到右结合的。
短路运算符
双目关系运算符和条件运算符均为短路运算符。以逻辑与(&&)运算为例,表达式0&&(i++),因为左值为假,表达式一定为假,此时右值表达式不求解,i的值不自增。为了避免短路运算符产生的错误,应避免将具有副作用的表达式写入短路运算符的表达式中。
副作用(side effect)与顺序点
副作用是对数据对象或文件的修改。从C的角度来看,主要目的是对表达式求值。自增(减)运算符和赋值运算符主要因为副作用而被使用。
顺序点是程序执行中的一个点,在该点处所有副作用都在进入下一步前被计算。分号和完整的表达式(即该表达式不是更大表达式的一部分)都标记了顺序点。
当在一个表达式中存在多个有副作用的运算时,C标准不规定副作用生效的次序只保证在该语句结束后所有副作用均已生效。
常用运算符
(1) 赋值运算符(=,+=,…)
左值: 赋值运算符左侧标识对象或表达式
右值: 可以赋给左值的常量,变量或表达式
复合赋值运算符 +=,-=,*=,/=,%=,^=: 对左值和右值进行+,-,*,/,%,^运算,并把结果赋给左值。所有算术运算符均具有对应的赋值运算符。
在C中,赋值是一种运算而不是特殊的指令,赋值表达式的返回值是赋值后的左值。运算的属性允许更灵活的操作,如连续赋值,利用返回值等。由于副作用顺序的不确定性滥用赋值运算将会导致严重错误,尽量使用简单、单义的赋值运算,严禁赋值运算与自增(减)运算符同时使用。
(2)sizeof
以字节为单位返回操作数的大小(在C中,一个字节被定义为char类型所占空间的大小)。
操作数可以是一个具体的数据对象(例如变量名),也可以是一个类型名。如果它是一个类型,操作数必须被括在"()"中。
(3)自增(减)运算符(++,--)
使操作数加1(++)或减1(--)。在前缀模式下(++a)先改变值再调用(即表达式的值为原值),后缀模式下,先调用再改变值。何时改变由顺序点决定,C只保证在语句执行完时一定。
(4)逗号运算符
用于将多个表达式并列,表达式值为右侧表达式的值。优先级最低,从左向右结合。常用于for循环等语句中。
(5)强制类型转换与指派运算符
完成强制类型转换的方法称为指派(cast)。圆括号与类型名组成指派运算符。
(type)operand
用于临时转换类型,不对变量造成影响。降级运算采用截断(直接舍弃)的方式进行。
(6) 函数调用运算符
没错,函数调用时包含参数表的那对圆括号也是运算符(初等运算符,最高优先级)。
将函数调用看作运算符将会便于以后的理解,特别是函数指针。
C标准将函数调用视为一种运算,C++标准明确将其作为运算符并允许重载(详见C++中关于运算符重载的说明。
2. C语句
语句是C程序最基本的单位,C语句以分号(;)作为结束标志,一个语句可以写多行,一行可以写多个语句。
为了保证程序可读性,尽量每行写一个语句;C语句的嵌套关系与缩进无关。
(1)声明语句
声明语句用于声明(定义)类型,对象和函数。声明与定义有所差别,声明只是告知编译器对象或函数的存在和标识符;定义则是指对象和函数已经处于可用状态,类型的所有成员已定义,对象已开辟内存空间并初始化,函数已经实现可以调用。
方括号([]),指针(*)这些符号在声明语句和执行语句中的含义有所不同,但依旧具有运算符的一些特性,这些特性有助于理解一些声明。这一观点将在《C指针与内存》中说明
(2)执行语句
C的执行语句绝大多数都是表达式语句,即表达式加“;”。
函数调用和赋值也可以认为是表达式语句。
(3)流程控制语句
包括条件语句if-else,switch,循环语句while,do-while,for辅助语句break,continue,goto语句以及return语句。
(4)复合语句
用花括号{}括起来的语句组成一条复合语句(代码块)。在复合语句中声明的变量的作用域为代码块级,即只在代码块中有效,并屏蔽同名的函数级局部变量和全局变量。
(5)空语句
只有一个分号的语句一般起占位的作用,如表示空循环体。
5. 流程控制
(1)选择结构
1) if语句
if(condition)
statement;
else if (condition)
statement;
else
statement;
statement表示一条C语句,可以是简单语句也可以是由花括号{}括起的复合语句。
即使只有一条简单语句也应尽量使用花括号避免二义性或修改后产生错误。
else自动与最近的未配对的if配对,与缩进无关。if与else数量不同时,注意使用花括号保证匹配正确。
2)条件表达式
condition?true_expr:false_expr
当condition的值为真时,条件表达式的值为true_expr的值;否则为flase_expr的值。条件表达式为短路运算符,当判断为真时false_expr不计算,为假时true_expr不计算,当表达式中存在有副作用的运算时需多加注意。
3)switch语句
switch (condition){
case flag:
statement
...
case flag:
statement
default:
statement
}
condition与某一flag相等时,从此case开始向下执行至switch结尾 , 不再进行判断(包括default后的语句),一般用break语句来跳出switch结构。
当表达式的值与所有case标号都不符时执行default标号后的语句,无default则跳出。
2.循环结构
1) while循环
while (condition)
statement
计算condition若为真则执行statement,再次计算condition直至condition为假时结束循环。
2)do while 循环
do {
statement
} while(condition);
先循环后判断。
3) for循环
for (init;condition;update)
statement
循环变量增值常用自增(减)运算符,多用于计次循环。
三个语句可以是逗号表达式语句或空语句,但不允许缺失或多个语句且语句顺序不允许颠倒。
for语句流程说明:
<1>执行init,即赋初值。
<2>计算condition,为真则执行循环体,假则结束循环。
<3>执行update,返回<2>。
3.辅助流程控制
break;
该语句跳出循环或switch语句。
continue;
跳至循环体末端,接着执行下一循环。
goto 标号;
标号: 语句;
尽量避免使用goto语句,唯一可被大多数人接受的goto是用来跳出嵌套的循环。
一些必须注意的细节:
(1)循环嵌套时内层循环每一次进入均需考虑是否对其中变量再次初始化。
(2)在循环语句中, 当循环变量恰满足条件时还要执行最后一次循环并且更新。 当循环结束后,循环变量是恰好不满足条件的值而非恰好满足条件的值。
一些有用的小技巧:
(1)while(1)与if(){break;}及更新语句的搭配可以任意的设计循环流程而不必拘泥于三种循环语句的流程。
(2)在搜索等的函数中设置多个出口,在循环体中设置匹配时出口,循环体外设置不匹配时的出口。可以绕过通过返回的循环变量判断搜索结果这一易错环节。
示例:判断一个大于2的整数是否为素数
(3)逻辑标记:
在搜索过程中常要求找到目标后就跳出循环,若根据返回的循环变量判断则难以区分是未找到还是最后一个成员即为目标。可以设置一个每次进入循环都被设置为真(1)的变量,如果发现不符合条件则置其为假(0)利用该标记判断匹配结果。
示例:
int main() { int i,n,m,t; scanf("%d",&m); for (n=3;n<m;n++) { t=1; for (i=2;i<n-1;i++) { if (n%2 == 0) { t=0; break; } } if (t) { printf("%d ",n); } } return 0; }
(4)灵活的退出方式
(1)与if(){break;}及更新语句的搭配可以任意的设计循环流程而不必拘泥于三种循环语句的流程。
(5)跟踪变量
在遍历单链表等问题中存在更新后就无法取得上一个值的变量(ptr=ptr->next;),此时可以设置另一个变量记录更新前的值:
void traver(Node *head) { Node * ptr = head, * prior ; while (ptr->next != NULL) { prior = ptr; ptr = ptr->next; use(ptr,prior); } }
6. 函数
函数是C程序的基本模块,函数可以将功能封装只通过接口来使用,提高开发效率和程序安全性。
函数需要先声明(定义)再调用,C允许先声明函数而不同时定义。
函数声明包括返回值类型、函数名、参数表以及修饰关键字组成。
如包含于<math.h>中的double pow(double x, double y)接受两个double值作为参数,返回一个作为结果的double值。
编译器只关注形参的数量和类型不关注形参的具体名称,也就是说前置声明与后置定义的形参名称允许不同。当然,定义时实参名称与函数体之间必须是对应的。
冯诺依曼体系结构的计算机中指令与数据同样以二进制存储于存储器中,C函数在被调用前形参和局部变量没有开辟相应的内存空间,在函数被调用后才会开辟空间。
参数及其传递
C函数中参数传递采用传值的方式,即形参是实参的一个副本,形参的修改不会影响实参。传值的方式使得只要类型符合的值均可以作为实参,除了变量外还可以是字面值,表达式,函数返回值;传址方式下只有存在于内存中的对象才有地址,只有对象才可以作为参数。
传址方式编写函数更加方便自由,C通过传递指针(对象的地址)的方式来实现传址调用。传址调用的说明详见指针。
void关键字置于形参表,表示函数没有参数int fun(void);;也可以使用一个空括号表示int fun();。在调用时只能采用空括号fun();而不能使用void关键字fun(void); 。
C函数将数组作为指针处理,在传递数组(非字符串)时一般需要同时传递数组长度等参数表示数组长度。在将数组作为参数时可以使用[]或*修饰,如 fun(int *a)与 fun (int a[])是等价的,编译器将数组作为指针处理,不关注数组长度。
在传递高维数组时可以使用指针,也可以用数组的形式如fun (int a[][8]),编译器不关注第一维的长度它将自行计算。
函数返回
函数中的return语句将会终止函数执行返回主调函数,返回值类型非void的函数,在return语句后加一个与返回值类型兼容的值作为返回值,int fun(void) {return 0;};;返回值类型为void的函数不返回任何值,return ;只起到提前终止函数执行的作用。
修饰关键字
函数可以是外部的(extern或缺省关键字)或静态(static)。外部函数可以被所有程序文件调用,静态(内部)函数只能在定义它的文件中调用,static void fun(void)声明了一个静态函数。
内联(inline)函数在调用时将函数代码嵌入调用点,而普通函数在调用时需要一系列耗时的操作。内联函数是一种典型的以空间换时间的策略。inline关键字是建议性而不是指令性的,函数是否是inline的由编译器最终决定,没有inline修饰的函数可能被优化为inline,使用inline修饰的函数可能不会被优化。
除了使用参数传递和返回值外,函数也可以通过外部变量进行通信。局部变量与外部变量重名时,局部变量将屏蔽外部变量。 所以在自定义函数时不再次声明全局变量作形参(形参表列中不含函数中调用的全局变量)。必须谨慎使用外部变量,它可能导致程序可移植性降低并增加因为外部变量修改而出错的概率。
C中函数名在自己的作用域内是唯一的,而在C++或Java等允许函数重载的语言中允许函数名重复,但是函数名+参数表是唯一的。
递归过程
一些书籍将递归过程定义为:一个函数调用其自身的过程称为递归过程。这个定义……
我们可以从执行过程的角度理解递归,递归过程分为回归、递推两个过程。以上述Fibonacci数列为例,调用Fibo(3)时它会调用Fibo(1)和Fibo(2),Fibo(2)会调用Fibo(0)和Fibo(1)。Fibo(0)和Fibo(1)到达递归底部无需继续递归,上述过程称为回归;Fibo(0)与Fibo(1)的返回值使得Fibo(2)取得结果,Fibo(1)和Fibo(2)使得Fibo(3)取得结果,递推结束这个过程称为递推。
通过模仿调用栈(call stack)的行为所有的递归算法均可以转换为迭代算法。递归可以使程序简单,但是函数调用需要大量时间和空间开销所以应尽量使用迭代而非递归算法。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。大多数编译器可以将尾递归优化为迭代,提高运行效率。