函数功能:隐藏操作细节,结构更加清晰,降低修改难度;
4.1 函数基本知识
返回值类型 函数名(参数声明表)
{
声明和语句
}
函数在源文件中出现的次序可以任意;
返回值类型省略则默认int;return可不带表达式,执行到最后右花括号也会返回:都是没有返回值的,合法,但未成功返回的“值”肯定是无用的;
程序可看做变量定义与函数定义的集合;函数通过参数、返回值和外部变量通信;
4.2 返回非整型的函数
函数与调用它的主函数在同一源文件中,并且类型不一致时,编译就会发现该错误;
隐式声明:如果未声明过的一个名字出现在某表达式中,并且其后紧跟左圆括号,那么上下文会认为这是一个函数名,其返回值会被假定为int,其参数不做任何假设;
return (表达式);表达式的值返回时会被转换为函数类型,这可能会丢失信息有些编译器会警告,故可显示类型转换;
4.3 外部变量
C程序可看做一系列外部对象(包括变量和函数)构成;
外部变量定义在函数外;函数不允许定义在其它函数中,故每一个函数也是“外部的”;
??(It)外部变量与函数默认具有“外部链接”性质:即使来自于单独编译的不同函数,通过同一个名字引用的都是同一个对象,据此函数可以借助外部变量通信,尤其函数需要共享大量信息时,同时也会使函数之间产生大量的数据联系,影响程序结构;
后面会介绍怎样定义只使用在一个源文件中的外部变量和函数;后面再讨论怎么把程序分割成多个源文件;
外部变量生存期:永久存在,两次函数调用之间保持不变;
函数共享的“栈”,可定义为外部变量,同时定义一个指针保存下一个空闲栈位置;
4.4 作用域规则
函数与外部变量可分开编译;一个程序可放在几个文件中;已编译过的函数可从库中加载;
作用域:程序中可以使用该名字的部分;局部变量(包括函数参数)作用域在函数内;外部变量或函数作用域从声明处开始,到其所在(待编译的)文件末尾结束,函数调用它们时无需声明直接用;
extern:如果要在外部变量定义之前使用它,或者定义与使用不在同一个源文件中,则声明中须强制使用extern;
??外部变量声明与定义的区分:
放在所有函数外部的int sp;double val[MAX];定义外部变量sp及val,分配内存单元,同时作为该源文件其余部分的声明;
extern int sp;extern double val[MAX]为源文件其余部分声明了外部变量sp及val(数组长度可在其他地方确定),但是并未建立变量或者分配存储单元;
一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次;其他文件必须先进行extern声明后才能访问它;(??也可在定义所在源文件进行extern声明);外部变量初始化只能出现在其定义中;一个变量如果要先使用后定义,使用前也须extern声明一下;
4.5 头文件
实际程序中,考虑到其各部分可能分别来自单独编译的库,所以常常将源程序分割成多个源文件;
考虑定义和声明的共享问题:(It)尽可能在一个头文件中专门集中各个源文件共享/公共部分(宏替换、函数原型等,简洁易修改),然后在各个需要的源文件中通过include "~.h"包含进去;较大规模程序可能需要多个头文件;
4.6 静态变量
外部变量或函数的声明前加上static:该对象作用域限定为被编译源文件剩余部分,对其他文件不可见(同名也不会冲突);
内部变量声明前加static:与自动变量一样只能在该函数内使用;不同之处在于不管该函数是否被调用,它一直存在一直占据存储空间,而非像自动变量那样随着函数的调用和退出而存在和消失;(It)该函数下次调用时可以使用上次调用留下的静态变量值,有时很有用(p57);
4.7寄存器变量
在较高频使用的自动变量或函数形参的声明前可加register,编译器可以将它放入寄存器中从而加快速度,编译器也可以忽略此选项;
实际底层硬件情况的限制:每个函数只有少量某些类型的变量可保存于寄存器;过量寄存器声明是无害的,编译器会忽略过量或不支持的寄存器变量声明;
无论寄存器变量是否真的放在寄存器中,其地址总是不能访问的;
4.8 程序块结构
C语言不能在函数中定义函数,但可以在函数的程序块结构中定义变量;变量声明(包括初始化)可以紧跟在任何标识复合语句开始的左花括号之后,在与之匹配的右花括号之前一直存在,并且它与该程序块以外的同名变量无任何关系;
4.9初始化
不显式初始化:则外部变量和静态变量都将被初始化为0;自动变量和寄存器变量初值未定义(即无用信息);
显式初始化:外部变量和静态变量=常量表达式,且只在程序开始前初始化一次;自动变量和寄存器变量每次进入函数或程序块时都将被初始化,且无需是常量表达式:可以包含之前已定义的值及函数调用;
数组的初始化:
int day[]={31,28,31,30,31,30,31,30,31,30,31};
忽略数组长度时,花括号中初始化表达式的个数将被编译器作为长度;数组长度多于个数,对外部变量、静态变量、自动变量,剩余未初始化的元素都将赋0;长度小于个数则出错;一个初始化表达式不能一次赋给多个元素;不能跳过前面元素直接初始化后面元素;
字符数组初始化:
char pattern[]="ould";等价于char pattern[]={‘o‘,‘u‘,‘l‘,‘d‘,‘\0‘};数组长度是5;
4.10 递归
函数可以直接或间接调用自身;
快排函数:void qsort(int v[], int left, int right) //p74
标准库中的qsort函数可以对任何类型的对象进行排序;
递归并不节省存储开销(必须在某个地方维护一个存储处理值的栈),执行速度并不快;但其代码紧凑易于编写和理解;对于树等递归定义的数据结构使用起来非常方便;
4.11 C预处理器
预处理器是编译过程中单独执行的第一个步骤;
文件包含:
#include "文件名" 或 #include <文件名>
源文件中任何形如上述的行都将被替换为文件名指定的内容;对""则在源文件所在位置查找该文件,对未找到或<>则按相应规则(与具体实现有关)查找;
#include指令常用来包含#define、extern声明、函数原型,在较大程序中这有利于避免错误;若某个被包含文件内容变化,显然所有依赖于它的源文件都必须重新编译;
宏替换:
宏定义:#define 名字 替换文本
名字命名同变量,替换文本可以是任意字符串;替换文本需分行,则在待续行尾加\;作用域从定义点到源文件末尾;宏定义可以使用前面出现的宏定义;替换只对记号(即不在字符串内也不是某名字的一部分)有用;
带参数的宏定义,例如:
#define max(A, B) ((A)>(B) ? (A) : (B))
使用需谨慎:适当用圆括号以保证计算顺序正确;考虑表达式的例如自增自减等副作用;
宏的价值:函数(如getchar)被定义为宏,可避免调用函数所需运行时的开销;
#undef 名字 可取消宏定义;
??名字中的形参不能用带引号字符串替换,若需要,则在替换文本中给参数前加上#;
预处理运算符##:若替换文本中形参与它相邻,则形参被实参替换的同时##及其前后空白符会被删除,替换后的结果会重新扫描;此法可连接实际参数,例如:
#define paste(front, back) front ## back
则宏调用paste(name, 1)将建立记号name1;
条件包含:
#if-#elif-#else-#endif
其中判断表达式必须是常量整型表达式(且不包含sizeof、(type)、enum量),可以是defined(名字)表达式(可用来避免重复包含某一名字或文件,多文件都使用这一方式,将不必考虑各个头文件之间的依赖关系);
#ifdef与#ifndef:专门测试某个名字是否已定义,例如:
#ifndef HDR
#define HDR
//放hdr.h的内容
#endif
它等价于
#if !define(HDR)
#define HDR
//放hdr.h的内容
#endif