C语言
教材:《C语言程序设计语言 第二版》Brian W.Kernighan... https://book.douban.com/author/175847/
注意,仅包含难点、易错点和容易忘记的点,数据结构与算法层面的东西在后续几篇中。
目录
有关宏定义(或宏替换)#define
定义格式:
#define 名字 替换文本(结尾无分号)
在该定义之后,除被引号括起来的和作为其他单词一部分的“名字”以外的所有“名字”都会被替换为定义中的“替换文本”。
“名字”的命名规则与变量相同:字母或下划线打头的字母数字序列。
“替换文本”的构成规则只要求是任何字符序列,用来替换”名字“。
C预处理器
理论上预处理器是编译过程中单独执行的第一个步骤,理解为一个独立功能的小程序。
介绍几个常见预处理指令:
1.#include指令:用于在编译期间将指定文件的内容包括进当前文件(简单理解为复制),更方便处理大量的#define指令以及声明。
形如#include "文件名" 和#include <文件名> ,如果用引号,编译器理解为该文件与当前编译源文件在同一位置,如果是尖括号则按照相应规则查找,这个规则与具体的实现有关(也就是几个默认位置依次查找)。被 包含的文件本身也可包含include指令。(这里没有指定文件名的后缀必须是.h,且文件内容可以是#define语句、extern声明和库函数的原型声明)
在大的程序中include指令是把所有声明捆绑在一起的比较好的方法,保证所有源文件具有相同的定义与变量声明。
如果某个包含文件的内容发生了变化,所有依赖于该文件的源文件必须重新编译。
2.#define指令:(见前一部分)
补充,#define指令占一行,如果替换文本太长,在末尾加上反斜杠\到下一行继续写。该指令的作用域从定义点开始到文件末尾结束。
带参数的宏定义:可以对不同的宏调用使用不同的替换文本,例如:#define max(A,B) ((A)>(B) ? (A):(B)) 看起来很像函数调用,但宏调用直接将替换文本插入到代码中,形式参数A、B的每次出现都将被替换成对应的实际参数。
使用带参数的宏定义要注意两点:
- 1,括号很必要,避免替换后运算优先级等错误
- 2,如果参数A、B要替换的实际参数带有自增、自减等运算,因为替换时会计算两次或以上将导致错误,而且难以查出。
宏定义的取消:#undef,可以保证后续的调用时函数调用而非宏调用。 例如:#undef max(A,B)
详见教材77页,另有内容,如##运算符等。
3.条件编译
这种方式为在编译过程中根据计算所得条件值选择性的包含不同代码提供了一种手段。下面为语法要求和语义内容。
#if语句对其中的常量整型表达式(不含sizeof、类型转换或者enum常量)进行求值,若表达式值非0,则包含其后各行,直到遇见#endif或者#elif或者#else语句为止。
在#if语句中可以使用表达式defined(名字),该表达式遵循以下规则:当名字已经定义时,值为1,否则为0.
例如:
#if !defined(HDR)
#define HDR
/*这里存放hdr.h文件的原本内容*/
#endif
意思为第一次包含hdr.h头文件时,定义名字HDR,以后再包含该文件的时候发现已经定义,便不再定义。这也为头文件的直接或者间接包含自身提供保障。
C语言专门提供了两个预处理语句#ifdef和#ifndef,分别指“如果定义”和“如果未定义”,用来测试某个名字是否定义,等价于上面的第一行。
如:
#ifndef HDR
#define HDR
/* .... */
#endif
有关输入输出
输入、输出功能不是C语言本身的一部分。
C标准库提供的IO模型是:
无论文本从哪到哪,都是按照字符流的方式处理。
文本流由多行字符构成,每行由0至多个字符组成,行末是一个换行符。
字符的输入输出之getchar()/putchar() :进行文件的复制&对单词的计数。
- 每次调用时,getchar函数从文本流中读入下一个输入字符,作为返回值。 c=getchar();若遇到文件末尾返回EOF(定义于stdio.h头文件中,值为-1)
- 每次调用时,putchar函数将打印一个字符。 putchar(c);
上面两种都是默认从键盘读取或者命令行打印,在许多环境中可以使用<符号进行getchar输入重定向,使用>符号进行putchar输出重定向。例如:(假设prog是一个使用了getchar/putchar函数的程序)
在系统命令行中,运行“prog<inputfile”将会使得程序中的getchar返回内容来自于文件inputfile而非用户键盘输入,同理,打印。
有关数组
字符串数组:
C语言使用字符’\0‘插入到字符序列末尾,表示字符串的结束。'\0'占用一个字符位置,但不算在字符串长度中。
有关函数
程序可以看成是变量定义和函数定义的集合,函数之间的通信可以通过参数、返回值和外部变量进行。
函数定义:
返回类型 函数名(0或多个参数声明,逗号分开){
声明部分;
语句序列;
}
函数定义可以按照任意次序出现在一个或多个源文件中,但同一函数不能分割存放在多个文件中。
如果源程序分散在多个文件中,在编译和加载的时需要做更多的工作,这不是语言的属性决定的,是操作系统的原因。
不带表达式的return 语句将控制权返回给调用者,但不返回值。对main函数末尾的return来说,向其调用者也就是程序的执行环境返回一个值,当返回0时表示正常结束,否则报错。
函数声明(出现在main函数之前):
返回类型 函数名(0或多个参数声明,可以只写参数类型,逗号分开);
关于参数传递
C语言中所有函数参数都是通过”传值调用“传递的。也就是参数值放在临时变量中,而不使用原来的变量直接处理。
也就是说,C语言中,被调函数不能直接修改主调函数中变量的值,只能修改其私有的临时副本的值。
间接修改:通过将变量地址作为参数值传递给函数,函数通过地址修改主调函数中变量的值。
当数组名作为函数参数时,传递给函数的是数组首元素的地址,并不复制数组本身。
自动变量
函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失,所以也称自动变量。
(并非所有局部变量都是自动变量,例如被static修饰的局部变量可以在多次函数调用之间保持值不变。见静态变量部分)
每次进入函数都要为自动变量显式赋值,否侧为无效值(某些编译器会自动赋值为0)。
自动变量(包括形式参数)可以隐藏同名的外部变量与函数。
关于外部变量和作用域
在函数中,除了可以使用函数内的自动变量外,还可以使用定义于所有函数外的变量,称外部变量。外部变量可以在全局范围内被访问到,只能被定义一次,初始化只能出现在定义中,由编译程序分配存储单元,在程序执行期间一直存在。
每个需要访问外部变量的函数中,必须声明相应的外部变量(为了在函数内说明其类型),有两种办法声明:
- 使用extern 语句显式声明,例如,extern int a;
- 通过上下文隐式声明(也就是某些情况下可以省略extern声明语句),例如,外部变量的定义出现在该函数定义所在文件中位置之前,则可以省略。
外部变量和函数的作用域从声明位置开始到其所在文件的末尾结束,即extern语句为所在文件的剩余部分声明资源。
也就是说不可省略的情况分为程序包含多个文件的情况和变量的使用位置在定义位置之前的情况。
如果某个变量在file1中定义,在file2和file3中使用,则在file2和file3中就需要使用extern声明建立该变量与其定义之间的联系。
通常把变量和函数的extern声明语句放在一个单独的文件中,称为头文件,在每个源文件的开头把需要使用的头文件使用#include语句包含进来。
外部变量不建议大量使用,否则导致:
- 程序中的数据关系模糊不清
- 造成某些函数失去通用性
- (也有好处,如果逻辑正确,会减少参数长度和开销)
头文件
考虑将程序源文件分割到多个文件中的情况,这样做的原因是在实际的程序中,不同文件可能来自于单独编译的库。
分割必须考虑的问题;定义和声明在这些文件之间的共享问题。
原则:尽可能把共享的部分集中在一起,这样只需要一个副本,易于后期的改进。
静态变量&函数
1.修饰外部对象(外部变量及函数)
使用static声明限定外部变量与函数,可以将被声明对象的作用域限定为被编译源文件的剩余部分,而非整个程序包含的所有被编译源文件。可以达到隐藏外部对象的目的。(详见参考教材70页,上方有文件布局图便于理解)
2.修饰内部变量(被修饰的叫静态内部变量,普通的内部变量称为自动变量,这是在概念上的区别)
使用static修饰的内部变量,不管函数是否被调用,它一直存在,而不像自动变量那样随着函数的被调用和退出而存在和消失。即,static类型的内部变量是一种只能在某个特定函数中使用(同自动变量)但一直占据存储空间的变量(不同于自动变量)。
寄存器变量register
register声明告诉编译器该变量在程序中使用频率较高,最高安排在寄存器中便于取出和放入。好处是可以提高程序的速度,减小程序使用的内存资源。但编译器可以选择忽略,所以即便给所有变量进行此声明都没问题。
注意:无论编译器是否选择忽略,只要变量进行了register声明,它的地址都是不能访问的。
不同机器对寄存器变量的数据和类型的具体限制也不同。
变量初始化
在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0,而自动变量和寄存器变量的初值未定义,即无用值(随机值)。
在显式初始化的情况下,
外部变量和静态变量的初始化表达式必须是常量表达式,且只初始化一次。(理论上说是在程序开始执行前进行初始化)
对于自动变量和寄存器变量来说,在每次进入函数或者程序块时都将被初始化,初始化表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。
数组的初始化,
数组的初始化可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,各初始化表达式之间通过逗号分隔。例如:int days[]={31,28,31,30,31,30,31,31,30,31,30,31};
如果初始化表达式的个数少于数组元素数目,对于外部变量、静态变量和自动变量来说,没有被初始化的元素将被初始化为0。如果多于数组元素数目是错误的。不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素。
字符数组(包括字符串数组)的初始化,可以用一个字符串代替使用花括号+逗号的初始化表达式,例如:
char p[]={'o','u','r','\0'}; 等价于char p[]="our"; '\0'结束符占用一个元素位置,但在实际中不作为字符串长度的一部分。
递归
即,函数可以直接或者间接调用自身,分别对应函数的直接递归和间接递归。 应用,数字转字符串,快速排序,图、树的遍历。
递归不节省存储器开销,执行速度不快,因为必须要维护一个存储处理值得栈,但递归的代码紧凑,可读性强。
定义&声明
- 定义表示创建变量或者分配存储单元
- 声明是指说明变量的性质,并不分配存储单元。
const常量
任何变量都可以使用关键字const修饰,指定变量的值不能被修改。如:
const double e=2.71828;
const char msg[]="warning:";
假设想要在某个函数外可以修改,而在函数内不可修改,则可以在函数声明和定义时对形参使用const修饰。
int a(const char[]);
表示函数不能修改数组元素的值,如果试图在函数内修改,结果取决于具体的实现。
C语言部分暂时就更新到这里,教材后面的附录A、和附录B的内容十分丰富详细,可以用作参考手册。