[TOC]
《C 程序设计语言》。C语言经典版本。必读。 语法之外,主要是1988年出的ANSI标准。
学习一门语言。语法:1%, 标准库:10%, 惯用法:5%, 其他:84%。
导言
C语言通用规则: 在允许使用某种类型变量值的任何场合,都可以使用该类型的更复杂的表达式。
修改"20"等不明其意的幻数的方式是定义一个有意义的名字。
#define 名字 替换文本 # 注意,这里没有分号。。。
#define 指令行末尾没有分号。
标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字节流的方式处理。
字符在机器内部是以
位模式
出现的。char
类型专门存储这类数据。getchar() 返回值之所以是int
, 是因为EOF
是个特殊的int, 在我的mac上是-1
。在windows上Ctrl+Z
输入EOF
, 在mac上是Ctrl+D
。
单词统计程序中的代码值得参考下
if (c == '' || c == '\n' || c == '\t') {
state = OUT;
} else if(state == OUT) { #能进到这个分支,说明c是合法的字符。
state = IN;
++nw;
}
在C语言中,所有函数参数都是"通过值"传递的。也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。
字符串/字符数组 以
\0
标示结束。
自动变量
: 也是局部变量,函数被调用时存在,在函数执行完毕退出时消失。外部变量
: 全局范围内访问,在程序执行期间一直存在。必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用extern
语句显示声明,也可以通过上下文隐式声明。
- 在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中可以省略声明。这也是include 头文件的原因。
- 没有include对应的定义的头文件,此时需要明确
extern 声明
。
类型、运算符与表达式
运算符指定将要进行的操作。
表达式则把变量和常量组合起来生成新的值。
对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。
C语言只提供以下几种基本数据类型:
char
字符型,占用一个字节,可以存放本地字符集中的一个字符
int
整形,通常反映了所用机器中整数的最自然长度。也就是机器字长。
float
单精度浮点型
double
双精度浮点型
其他short
,long
,unsigned
,singed
为限定符,控制内存使用的。
// c 常量
1234 // int
1234L // long int
1234UL // unsigned long int
1234u // unsiged int
123.4 //double
1e-2 // double
123.4f // float
123.4l // long double
037 // 8进制表示
0x1f // 16进制表示
1个字符常量是一个整数,之所以有char
类型是为了方便阅读。
转义字符\n
看起来是2个字符,但只表示一个字符。另外可以用\000
或者\xhh
表示任意的字节大小的位模式。
如
#define VTAB '\013' /* ASCII纵向的制表符
等同于
#define VTAB '\xb'
ANSI C语言中的全部转义字符序列如下表示:
转义字符 | 含义 | 转义字符 | 含义 | 转义字符 | 含义 |
---|---|---|---|---|---|
\a |
响铃符 | \\ |
反斜杠 | \b |
回退符 |
\? |
问号 | \f |
换页符 | \' |
单引号 |
\n |
换行 | \" |
双引号 | \r |
回车 |
\000 |
8进制表示 | \t |
横向制表符 | \xhh |
16进制表示 |
\v |
纵向制表符 |
字符常量
\0
表示值为0的字符,也就是空字符(null)。我们通常用\0
的形式替代0,以强调某些表达式的字符属性,但其数字值为0。
字符串以
\0
标示结尾,这意味着字符串长度没有限制,但是要获取长度必须遍历字符串。标准库的strlen(s)
返回的字符串长度不包含结束标识符\0
。
枚举为建立常量值与名字之间的关联提供了一种便利的方式。
相对
#define
语句来说,他的优势在于常量值可以自动生成。此外,调试程序可以以符号形式打印出枚举变量的值。
如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。
每次进入函数或程序块时,显示初始化的自动变量都将被初始化一次,其初始化表达式可以是任何表达式。
默认情况下,外部变量与静态变量都将被初始化为0。
未经显示初始化的自动变量的值为未定义值(即无效值)
对数组而言,const限定符指定数组中所有元素的值都不能被修改。
const限定符也可配合数组参数使用,它表明函数不能修改数组元素的值。
如果试图修改const限定符限定的值,其结果取决于具体的实现。
在有负操作数的情况下, 整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现。
逻辑运算符
!
的作用是把非0操作数转成成0,把0转换成1。
一般来说,自动转换 是指把"比较窄的"操作数转换为"比较宽的"操作数,并且不丢失信息的转换(参考附录A.6)。
C语言的定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是存储在字符变量中的位模式在某些机器中可能是负的(最左位为1时,为负值),而在另外一些机器中可能是正的(在左边添0,永远是正值)。为了保证程序的可移植性,如果要在char类型的变量中存储非字符数据,最好指定
signed
orunsigned
限定符。
赋值语句具有值,且可以用在表达式中。其类型为做操作数,值为赋值操作完成的值。
运算符 | 结合性 | 运算符 | 结合性 |
---|---|---|---|
() [] -> . |
从左到右 | ^ |
从左到右 |
! ~ ++ -- + - * & (type) sizeof |
从右到左 | | |
从左到右 |
* / % |
从左到右 | && |
从左到右 |
+ - |
从左到右 | || |
从左到右 |
<< >> |
从左到右 | ?: |
从左到右 |
< <= > >= |
从左到右 | = += -= *= /= %= &= |
从右到左 |
== != |
从左到右 | ^= |= <<= >>= |
从右到左 |
& |
从左到右 | , |
从左到右 |
说明:第二行的+
,-
是一元运算符,符号位。
同大多数语言一样,C语言没有指定同一运算符中多个操作数的计算顺序(&&、||、?:、和, 运算符除外)。
例如
x = f()+g()
,f()
和g()
的计算顺序并不确定。
控制流
略
函数与程序结构
如果函数定义中省略了返回值类型,则默认认为是
int
类型。
程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值以及外部变量进行。
在必要时,
return 表达式
中的表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号,此处的括号是可选的。
如果没有函数原型,则函数将在第一次出现时的表达式中被隐式声明。返回值为int, 不对参数做任何假设。
默认情况下,外部变量与函数具有下列性质:通过同一个名字对 外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用同一个对 象(标准中把这一性质称为外部链接)。
外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期。
作用域
名字的作用域指的是程序中可以使用该名字的部分。
外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译的)文件的末尾结束。
将外部变量的声明与定义严格区分开来很重要。变量声明用于说明变量的属性(主要是 变量的类型),而变量定义除此以外还将引起存储器的分配。
如果将下列语句放在所有函数的 外部:
int sp; double val[MAXVAL];
那么这两条语句将定义外部变量 sp 与 val,并为之分配存储单元,同时这两条语句还可以作 为该源文件中其余部分的声明。
而下面的两行语句:
extern int sp; extern double val[];
为源文件的其余部分声明了一个 int 类型的外部变量 sp 以及一个 double 数组类型的外部 变量 val(该数组的长度在其它地方确定),但这两个声明并没有建立变量或为它们分配存储 单元。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其它文件 可以通过 extern 声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的 extern 声明)。
外部变量的定义中必须指定数组的长度,但 extern 声明则不一定要指定数 组的长度。 外部变量的初始化只能出现在其定义中。
头文件
对于 某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需要 使用更多的头文件,我们需要精心地组织它们。
静态变量
用 static 声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。
static 也可用于声明内部变量。static 类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变 量。
寄存器变量
register 声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是,将 register 变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译器可以 忽略此选项。
register int x;
register 声明只适用于自动变量以及函数的形式参数。
无论 寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。
程序块结构
自动变量(包括形式参数)也可以隐藏同名的外部变量与函数。
在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况, 否则,很可能引起混乱和错误。
初始化
在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为 0,而自动变量和寄 存器变量的初值则没有定义(即初值为无用的信息)。
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念 上讲是在程序开始执行前进行初始化)。
对于自动变量与寄存器变量,则在每次进入函数或程 序块时都将被初始化。
对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:表达式中可以包 含任意在此表达式之前已经定义的值,包括函数调用。
数组初始化
数组的初始化可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括 号括起来,各初始化表达式之间通过逗号分隔。
当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度。
如果初始化表达式的个数比数组元索数少,则对外部变量、静态变量和自动变量来说, 没有初始化表达式的元素将被初始化为 0,如果初始化表达式的个数比数组元素数多,则是错 误的。
字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的 初始化表达式序列。
递归
C预处理器
从概念上讲,预处理器是编译过程中单独执行 的第一个步骤。两个最常用的预处理器指令是:
#include 指令(用于在编译期间把指定文 件的内容包含进当前文件中)和#define 指令(用任意字符序列替代一个标记)。
include
在源文件中,任何形如: #include “文件名” 或 #include <文件名> 的行都将被替换为由文件名指定的文件的内容。
在大的程序中,#include 指令是将所有声明捆绑在一起的较好的方法。它保证所有的 源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误。
define 宏替换
宏定义的形式如下:
#define 名字 替换文本
这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本。
#define 指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。
通常情况下, #define 指令占一行,替换文本是#define 指令行尾部的所有剩余部分内容,但也可以把一 个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符\。
#define 指令 定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。
宏定义中也可以使 用前面出现的宏定义。
替换只对记号进行,对括在引号中的字符串不起作用。例如,如果 YES 是一个通过#define 指令定义过的名字,则在 printf(“YES”)或 YESMAN 中将不执行替换。
替换文本可以是任意的,例如:
#define forever for (;;) /* infinite loop */
该语句为无限循环定义了一个新名字 forever。
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。
例如,下列宏定 义定义了一个宏 max:
#define max(A, B) ((A) > (B) ? (A) : (B))
使用宏 max 看起来很像是函数词用,但宏调用直接将替换文本插入到代码中。形式参数(在 此为 A 或 B)的每次出现都将被替换成对应的实际参数。
可以通过
#undef
指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不 是宏调用:
#undef getchar int getchar(void) { ... }
形式参数不能用带引号的字符串替换。但是,如果在替换文本中,参数名以#作为前缀则 结果将被扩展为由实际参数替换该参数的带引号的字符串。
例如,可以将它与字符串连接运 算结合起来编写一个调试打印宏:
#define dprint(expr) printf(#expr " = %g\n", expr)
使用语句
dprint(x/y)
调用该宏时,该宏将被扩展为:
printf("x/y" " = &g\n", x/y);
其中的字符串被连接起来了,这样,该宏调用的效果等价于
printf("x/y = &g\n", x/y);
在实际参数中,每个双引号"将被替换为",反斜杠\将被替换为\,因此替换后的字符串是 合法的字符串常量。 预处理器运算符
##
为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与 ##相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果重新 扫描。例如,下面定义的宏 paste 用于连接两个参数
#define paste(front, back) front ## back
因此,宏调用 paste(name, 1)的结果将建立记号 name1。 ##的嵌套使用规则比较难以掌握,详细细节请参阅附录 A。
条件包含
\#if
语句对其中的常量整型表达式(其中不能包含 sizeof、类型转换运算符或 enum 常 量)进行求值,若该表达式的值不等于 0,则包含其后的各行,直到遇到#endi
f、#elif
或#else
语句为止(预处理器语句#elif 类似于 else if)。在#if 语句中可以使用表达式defined(名字)
,该表达式的值遵循下列规则:当名字已经定义时,其值为 1;否则,其值 为 0。例如,为了保证 hdr.h 文件的内容只被包含一次,可以将该文件的内容包含在下列形式 的条件语句中:
#if !defined(HDR) #define HDR /* hdr.h 文件的内容放在这里 */ #endif
下面的这段预处理代码首先测试系统变量 SYSTEM,然后根据该变量的值确定包含哪个版 本的头文件:
#if SYSTEM == SYSV
#define HDR "sysv.h"
#elif SYSTEM == BSD
#define HDR "bsd.h"
#elif SYSTEM == MSDOS
#define HDR "msdos.h"
#else
#define HDR "default.h"
#endif
#include HDR
C 语言专门定义了两个预处理语句#ifdef 与#ifndef,它们用来测试某个名字是否已经 定义。
指针与数组
在 C 语言中,指针的使用非常广泛,原因之一是,指 针常常是表达某个计算的惟一途径,另一个原因是,同其它方法比较起来,使用指针通常可 以生成更高效、更紧凑的代码。
ANSI C 使用类型
void *
(指向 void 的指针)代替char *
作为通用指针的类型。
指针与地址
地址运算符&只能应用于内存中 的对象,即变量与数组元素。它不能作用于表达式、常量或 register 类型的变量。
int *ip;
这样声明是为了便于记忆。该声明语句表明表达式*ip 的结果是 int 类型。
每个指针都必须指向某 种特定的数据类型。(一个例外情况是指向 void 类型的指针可以存放指向任何类型的指针, 但它不能间接引用其自身。)
指针与函数参数
由于 C 语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修 改主调函数中变量的值。此时只能通过指针参数来达到修改主调函数中变量的值的目的。
指针与数组
通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用 指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解 起来稍微困难一些。
**根据定义,数组类型的变量或表达式的值是 该数组第 0 个元素的地址。**执行赋值语句
pa = &a[0];
后,pa 和 a 具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以, 赋值语句 pa=&a[0]也可以写成下列形式:
pa = a;
&a[i]和 a+i 的含义也是相同的。a+i 是 a 之后第 i 个元素的地址。
但是,我们必须记住,数组名和指针之间有一个不同之处,指针是一个变量,因此,在 C 语言中,语句 pa=a 和 pa都是合法的。但数组名不是变量,因此,类似于 a=pa 和 a形 式的语句是非法的。
在函数定义中,形式参数
char s[];
和
char *s;
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个 指针。
地址算术运算
如果 p 是一个指向数组中某个元素的指针,那么 p++将对 p 进行自增运算并指向下一个 元素,而 p+=i 将对 p 进行加 i 的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。这类运算是指针或地址算术运算中最简单的形式。
一般情况下,同其它类型的变量一样,指针也可以初始化。
通常,对指针有意义的初始 化值只能是 0 或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已 定义的具有适当类型的数据的地址。
指针算术运算有以下几个重要特点。
首先,在某些情况下对指针可以进 行比较运算。例如,如果指针 p 和 q 指向同一个数组的成员,那么它们之间就可以进行类似 于==、!=、<、>=的关系比较运算。(就是就是位置判断。可以理解为下标)
任何指针与 0 进行相等或不等的比较运算都有意义。(是否有效的意思)
其次。我们从前面可以看到,指针可以和整数进行相加或相减运算。(就是移动下标)
有效的指针运算包括
- 相同类型指针之间的赋值运算;
- 指针同整数之间的加法或减法运算;(移动位置,改下标)
- 指向相同数组中元素的两个指针间的减法或比较运算;(理解成下标比较)
- 将指针赋值为 0 或指针与 0 之间的比 较运算。(判断指针是否有效。0位无效,或者说未初始化的。)
- 其它所有形式的指针运算都是非法的,例如两个指针间的加法、乘法、除法、移位 或屏蔽运算;指针同 float 或 double 类型之间的加法运算;不经强制类型转换而直接将指 向一种类型对象的指针赋值给指向另一种类型对象的指针的运算(两个指针之一是 void * 类型的情况除外)。
字符指针与函数
下面两个定义之间有很大的差别:
char amessage[] = "nw is the time"; /* 定义一个数组 */ char *pmessage = "now is the time"; /* 定义一个指针, 指向的是常量字符串的首地址,故不能修改字符串 */
上述声明中,amessage 是一个仅仅足以存放初始化字符串以及空字符’\0’的一维数组。数组中的单个字符可以进行修改,但 amessage 始终指向同一个存储位置。另一方面,pmessage 是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其它地址,但如果试图 修改字符串的内容,结果是没有定义的
/* strcpy: copy t to s; pointer version 3 */ void strcpy(char *s, char *t) { while (*s++ = *t++) ; }
该函数初看起来不太容易理解,但这种表示方法是很有好处的,我们应该掌握这种方法,C 语 言程序中经常会采用这种写法。
下面的两个表达式:
*p++=val; /*将val压入栈*/ val=*--p; /*将栈顶元素弹出到val中*/
是进栈和出栈的标准用法。
指针数组以及指向指针的指针
通常情况下,最好将程序划分成若干个与问题的自然划分相一致的函数,并通过主函数控制 其它函数的执行。
指针数组 lineptr 的声明是新出现的重要概念:
char *lineptr[MAXLINES];
它表示
lineptr
是一个具有 MAXLINES 个元素的一维数组,其中数组的每个元素是一个指 向字符类型对象的指针。也就是说,lineptr[i]
是一个字符指针,而*lineptr[i]
是该指 针指向的第 i 个文本行的首字符。
多维数组
在 C 语言中,二维数组实际上是 一种特殊的一维数组,它的每个元素也是一个一维数组。
数组元素按行存储,因此,当按存储顺序访问数组时,最右边的数组下标(即列)变化得最快。
如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。
如果将数组 daytab 作为参数传递给函数 f,那么 f 的声明应该写成下列形式:
f(int daytab[2][13]) { ... }
也可以写成
f(int daytab[][13]) { ... }
因为数组的行数无关紧要,所以,该声明还可以写成
f(int (*daytab)[13]) { ... }
一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。
指针数组的初始化
略
指针与多维数组
对于 C 语言的初学者来说,很容易混淆二维数组与指针数组之间的区别。
假如有下面两个定义:
int a[10][20]; int *b[10];
那么,从语法角度讲,
a[3][4]
和b[3][4]
都是对一个 int 对象的合法引用。但 a 是一个 真正的二维数组,它分配了 200 个 int 类型长度的存储空间,并且通过常规的矩阵下标计算 公式 20×row+col(其中,row 表示行,col 表示列)计算得到元素 a[row][col]的位置。
但是, 对 b 来说,该定义仅仅分配了 10 个指针,并且没有对它们初始化,它们的初始化必须以显式 的方式进行,比如静态初始化或通过代码初始化。假定 b 的每个元素都指向一个具有 20 个元 素的数组,那么编译器就要为它分配 200 个 int 类型长度的存储空间以及 10 个指针的存储空 间。
指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是说,b 的每个元素不 必都指向一个具有 20 个元素的向量,某些元素可以指向具有 2 个元素的向量,某些元素可以 指向具有 50 个元素的向量,而某些元素可以不指向任何向量。
命令行参数
调用主函数 main 时,它带有两个参数。第一个参数(习惯上称为 argc,用于参数计数)的值表示运行 程序时命令行中参数的数目;第二个参数(称为 argv,用于参数向量)是一个指向字符串数 组的指针,其中每个字符串对应一个参数。我们通常用多级指针处理这些字符串。
按照 C 语言的约定,argv[0]的值是启动该程序的程序名,因此 argc 的值至少为 1。 如果 argc 的值为 1,则说明程序名后面没有命令行参数。
ANSI 标准要求 argv[argc]的值必须为一空指针(这样可以在不用argc的情况下终止遍历argv)
UNIX 系统中的 C 语言程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数。
while (--argc > 0 && (*++argv)[0] == '-') while (c = *++argv[0])
在处理每个可选参数之前,argc 执行自减运算,argv 执行自增运算。
循环语句结束时, 如果没有错误,则 argc 的值表示还没有处理的参数数目,而 argv 则指向这些未处理参数中 的第一个参数。
因此,这时 argc 的值应为 1,而argv 应该指向模式。
注意,
*++argv
是 一个指向参数字符串的指针,因此(*++argv)[0]
是它的第一个字符(另一种有效形式是**++argv
)。因为[]与操作数的结合优先级比和++高,所以在上述表达式中必须使用圆括 号,否则编译器将会把该表达式当做*++(argv[0])
。实际上,我们在内层循环中就使用了 表达式*++argv[0]
,其目的是遍历一个特定的参数串。在内层循环中,表达式*++argv[0]
对指针argv[0]
进行了自增运算。(Ps: 主要就是运算符的优先级和结合性。)
指向函数的指针
在 C 语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被 赋值、存放在数组中、传递给函数以及作为函数的返回值等等。
调用函数 qsort 的语句中,strcmp 和 numcmp 是函数的地址。因为它们是函数,所
以前面不需要加上取地址运算符&,同样的原因,数组名前面也不需要&运算符。
复杂声明
C 语言的声明不能从左至右阅读,而且使用了太 多的圆括号。
dcl: optional *'s direct-dcl direct-dcl name ------ 名字 (dcl) ------ 改变优先级 direct-dcl() ------ 函数 direct-dcl[optional size] ------数组
简而言之,声明符 dcl 就是前面可能带有多个*的 direct-dcl。
direct-dcl 可以是
name
、由一对圆括号括起来的 dc
l、后面跟有一对圆括号的 direct-dcl
、后面跟有用方括号括起来的表示 可选长度的 direct-dcl
。该语法可用来对 C 语言的声明进行分析。
例如,考虑下面的声明符:
(*pfa[])()
按照该语法分析,pfa 将被识别为一个 name,从而被认为是一个 direct-dcl。于是,pfa[]也 是一个 direct-dcl。接着,*pfa[]被识别为一个 dcl,因此,判定(*pfa[])是一个 direct-dcl。 再接着,(*pfa[])()被识别为一个 direct-dcl,因此也是一个 dcl。可以用图 5-12 所示的语法 分析树来说明分析的过程(其中 direct-dcl 缩写为 dir-dcl)。
右左法则:
首先从未定义的标识符开始阅读,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。
以上面的示例说明:
pfa为名字,看右边,是[], 表明pfa是个数组。再看左边,是
*
, 表明数组的内容是指针。再看右边,是)
,返回看左边,是(
,再返回看右边是()
, 表明指针指向的内容是函数。pfa 是个数组,内容为指针,指针指向函数。
结构
结构的基本知识
结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些 变量组织在一个名字之下。由于结构将一 组相关的变量看作一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别 是在大型的程序中。
ANSI 标准在结构方面最主要的变化是定义了结构的赋值操作——结构可以拷贝、赋值、 传递给函数,函数也可以返回结构类型的返回值。多年以前,这一操作就已经被大多数的编 译器所支持,但是,直到这一标准才对其属性进行了精确定义。在 ANSI 标准中,自动结构和 数组现在也可以进行初始化。
struct point { int x; int y; };
关键字
struct
引入结构声明。结构声明由包含在花括号内的一系列声明组成。关键字 struct 后面的名字是可选的,称为结构标记
(这里是 point)。结构标记用于为结构命名, 在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。结构中定义的变量称为成员。结构成员、结构标记和普通变量(即非成员)可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。
struct 声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变 量表,这与其它基本类型的变量声明是相同的。例如:
struct { ... } x, y, z;
如果结构声明中带有标记,那么在以后定义结构实例时便可以使用该 标记定义。例如,对于上面给出的结构声明 point,语句
struct point pt;
定义了一个 struct point 类型的变量 pt。结构的初始化可以在定义的后面使用初值表进
行。初值表中同每个成员对应的初值必须是常量表达式,例如:
struct point maxpt = {320, 200};
在表达式中,可以通过下列形式引用某个特定结构中的成员:
结构名.成员
结构与函数
结构的合法操作只有几种:
作为一个整体复制和赋值,
通过&运算符取地址,
访问其成员。
其中,复制和赋值包括向函数传递参数以及从函数返回值。
结构之间不可以进行比较。可以 用一个常量成员值列表初始化结构,自动结构也可以通过赋值进行初始化。
结构指针的使用频度非常高,为了使用方便,C 语言提供了另一种简写方式。假定 p 是 一个指向结构的指针,可以用
p->结构成员
这种形式引用相应的结构成员。
结构数组
这种结构的初始化方法同前面所述的初始化方 法类似——在定义的后面通过一个用圆括号括起来的初值表进行初始化,如下所示:
struct key { char *word; int count; } keytab[] = { "auto", 0, "break", 0, "case", 0, "char", 0, "const", 0, "continue", 0, "default", 0, /* ... */ "unsigned", 0, "void", 0, "volatile", 0, "while", 0 };
与结构成员相对应,初值也要按照成对的方式列出。更精确的做法是,将每一行(即每个结 构)的初值都括在花括号内,如下所示:
{ "auto", 0 }, { "break", 0 }, { "case", 0 }, ...
但是,如果初值是简单变量或字符串,并且其中的任何值都不为空,则内层的花括号可以省 略。
C 语言提供了一个编译时(compile-time)一元运算符 sizeof,它可用来计算任一对象的长
度。表达式
sizeof 对象
以及
sizeof(类型名)
将返回一个整型值,它等于指定对象或类型占用的存储空间字节数。
指向结构的指针
问题在于,&tab[-1]和&tab[n]都超出了数组 tab 的范围。前者是绝对非法的, 而对后者的间接引用也是非法的。但是,C 语言的定义保证数组末尾之后的第一个元素(即 &tab[n])的指针算术运算可以正确执行。
但是,千万不要认为结构的长度等于各成员长度的和。因为不同的对象有不同的对齐要 求,所以,结构中可能会出现未命名的“空穴“(hole)。例如,假设 char 类型占用一个字节, int 类型占用 4 个字节,则下列结构:
struct { char c; //1个,对齐要求,也会占到8个。为了指针的移动。可以整数倍的移。 int i; // 4个, };
可能需要 8 个字节的存储空间,而不是 5 个字节。使用 sizeof 运算符可以返回正确的对象 长度。
最后,说明一点程序的格式问题:当函数的返回值类型比较复杂时(如结构指针),例如
struct key *binsearch(char *word, struct key *tab, int n)
很难看出函数名,也不太容易使用文本编辑器找到函数名。我们可以采用另一种格式书写上 述语句:
struct key * binsearch(char *word, struct key *tab, int n)
自引用结构
struct tnode { char *word; int count; /* the tree node: */ /* points to the text */ /* number of occurrences */ struct tnode *left; /*leftchild*/ struct tnode *right; /* right child */ };
一个包含其自身 实例的结构是非法的,但是,下列声明是合法的:
struct tnode *left;
它将 left 声明为指向 tnode 的指针,而不是 tnode 实例本身。
表查找
示例代码,忽略。主要是hash表+链表。
类型定义
C 语言提供了一个称为
typedef
的功能,它用来建立新的数据类型名,例如,声明
typedef int Length;
将 Length 定义为与 int 具有同等意义的名字。类型 Length 可用于类型声明、类型转换等,
它和类型 int 完全相同。
这里必须强调的是,从任何意义上讲,typedef 声明并没有创建一个新类型,它只是为 某个已存在的类型增加了一个新的名称而已。
除了表达方式更简洁之外,使用 typedef 还有另外两个重要原因。
首先,它可以使程序 参数化,以提高程序的可移植性。如果 typedef 声明的数据类型同机器有关,那么,当程序移植到其它机器上时,只需改变 typedef 类型定义就可以了。
typedef 的第二个作用是为程序提供更好的说明性——Treeptr 类型显然比一个声明 为指向复杂结构的指针更容易让人理解。
联合
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的 长度和对齐要求。联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要 在程序中嵌入任何同机器有关的信息。
ps: 就是为了省内存空间的…
这就是联合的目的——一个变量可以合法地保存多种数据类型中任何一种类型的对象。其语 法基于结构,如下所示:
union u_tag { int ival; float fval; char *sval; } u;
变量 u 必须足够大,以保存这 3 种类型中最大的一种,具体长度同具体的实现有关。
这 些类型中的任何一种类型的对象都可赋值给 u,且可使用在随后的表达式中,但必须保证是一 致的:读取的类型必须是最近一次存入的类型。程序员负责跟踪当前保存在联合中的类型。 如果保存的类型与读取的类型不一致,其结果取决于具体的实现。
可以通过下列语法访问联合中的成员:
联合名.成员
或
联合指针->成员
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为 0,此结构空间要 大到足够容纳最“宽”的成员,并且,其对齐方式要适合于联合中所有类型的成员。对联合 允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其 中一个成员。
联合只能用其第一个成员类型的值进行初始化,因此,上述联合 u 只能用整数值进行初 始化。
位字段
位字段(bit-field),或简称字 段,是“字”中相邻位的集合。“字”(word)是单个的存储单元,它同具体的实现有关。
struct { unsigned int is_keyword : 1; unsigned int is_extern : 1; unsigned int is_static : 1; } flags;
这里定义了一个变量 flags,它包含 3 个一位的字段。冒号后的数字表示字段的宽度(用二
进制位数表示)。字段被声明为unsigned int类型,以保证它们是无符号量。
单个字段的引用方式与其它结构成员相同。
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界由具体的实现定义。 字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度 0 可以用来强制 在下一个字边界上对齐。
某些机器上字段的分配是从字的左端至右端进行的,而某些机器上则相反。这意味着, 尽管字段对维护内部定义的数据结构很有用,但在选择外部定义数据的情况下,必须仔细考 虑哪端优先的问题。依赖于这些因素的程序是不可移植的。字段也可以仅仅声明为 int,为 了方便移植,需要显式声明该 int 类型是 signed 还是 unsigned 类型。字段不是数组,并 且没有地址,因此对它们不能使用&运算符。
输入与输出
输入/输出功能并不是 C 语言本身的组成部分, 而是标准库函数。ANSI 标准精确地定义了这些库函数,所以,在任何可以使用 C 语言的系统中都有这些函 数的兼容形式。
标准输入/输出
格式化输出–printf函数
变长参数表
函数 printf 的正确声明形式为:
int printf(char *fmt, ...)
其中,省略号表示参数表中参数的数量和类型是可变的。省略号只能出现在参数表的尾部。
标准头文件
<stdarg.h>
中包含一组宏定义,它们对如何遍历参数表进行了定义。该头文件的实现因不 同的机器而不同,但提供的接口是一致的。
va_list
类型用于声明一个变量,该变量将依次引用各参数。在函数 minprintf 中, 我们将该变量称为 ap,意思是“参数指针”。宏
va_start
将 ap 初始化为指向第一个无名参 数的指针。在使用 ap 之前,该宏必须被调用一次。参数表必须至少包括一个有名参数, va_start 将最后一个有名参数作为起点。每次调用
va_arg
,该函数都将返回一个参数,并将 ap 指向下一个参数。va_arg 使用 一个类型名来决定返回的对象类型、指针移动的步长。最后,必须在函数返回之前调用
va_end
,以完成一些必要的清理工作。
#include <stdarg.h> /* minprintf: minimal printf with variable argument list */ void minprintf(char *fmt, ...) { va_list ap; /* points to each unnamed arg in turn, 指向未命名参数的指针 */ char *p, *sval; int ival; double dval; va_start(ap, fmt); /* make ap point to 1st unnamed arg。 使得ap指向第一个未命名参数 */ for (p = fmt; *p; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int);//返回一个参数,并假设参数的类型为int。同时移动到下一个参数 printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) //字符串以'\0'结束,也就是0。 putchar(*sval); break; default: putchar(*p); break; } } va_end(ap); /* clean up when done */ }
UNIX系统接口
略。可以看<unix环境高级编程>。
扩展问题
浮点数的存储
float可以保证十进制科学计数法小数点后6位有效精度和第7位的部分精度
double可以保证十进制科学计数法小数点后15位有效精度和第16位的部分精度。
关于IEEE-754标准
详细可参考百度百科:http://baike.baidu.com/view/1352525.htm,或者维基百科:http://zh.wikipedia.org/zh-cn/IEEE_754
为便于软件的移植,浮点数的表示形式应该有统一标准(定义)。1985年IEEE(Institute of Electrical and Electronics Engineers)提出了IEEE754标准。该标准规定基数为2,阶码E用移码表示,尾数M用原码表示,根据二进制的规格化方法,数值的最高位总是1,该标准将这个1缺省存储,使得尾数表示范围比实际存储的多一位。IEEE754标准中有三种形式的浮点数:短浮点数(又称单精度浮点数)、长浮点数(又称双精度浮点数)、临时浮点数(又称扩展精度浮点数,这种浮点数没有隐含位),它们的具体格式如下表:
类型 | 存储位数 | 偏置值 | ||||
---|---|---|---|---|---|---|
数符(s) | 阶码(e) | 尾数(m) | 总位数 | 十六进制 | 十进制 | |
短浮点数(Single,float) | 1位 | 8位 | 23位 | 32位 | 7FH | +127 |
长浮点数(Double) | 1位 | 11位 | 52位 | 64位 | 3FFH | +1023 |
临时浮点数(扩展精度浮点数) | 1位 | 15位 | 64位 | 80位 | 3FFFH | +16383 |
对于阶码为0或255的情况,IEEE754标准有特别的规定:
如果 e 是0 并且 m 是0,则这个数的真值为±0(正负号和数符位有关) 如果 e = 255 并且 m 是0,则这个数的真值为±∞(同样和符号位有关) 如果 e = 255 并且 m 不是0,则这不是一个数(NaN)
大字节序 vs 小字节序
端序(Endianness),又称字节序、尾序、位序。在计算机领域是指机器存放多字节数据的字节顺序。在涉及到低层数据存储和网络数据传输研究中都会涉及端序。
大端序(Big-Endian,大尾序):高位字节放在内存的低地址,低位字节放在内存的高地址。
小端序(Little-Endian,小尾序):低位字节放在内存的低地址,高位字节放在内存的高地址。
端序是与硬件的体系结构相关而与所使用的操作系统无关的概念,目前基本上所有x86系列的PC机都是小端序。
在32位机器上,对于数0x12345678来说,低层表示的方法因端序而排列不同
小端序: | 0x78 | 0x56 | 0x34 | 0x12 |
低地址 --0----------7-8-------15-16------23-24-------31–> 高地址
大端序: | 0x12 | 0x34 | 0x56 | 0x78 |
低地址 --0----------7-8-------15-16------23-24-------31–> 高地址
判定方法:
方法一:将整形int强制转换char测试
1 bool isBigEndian()
2 {
3 int a = 1;
4 if (((char*)&a)[sizeof(int) - 1] == 1) {
5 return true;
6 } else {
7 return false;
8 }
9 }
对于32位机器来说:
小端序: | 0x01 | 0x00 | 0x00 | 0x00 |
低地址 --0—[0]—7-8–[1]–15-16-[2]-23-24–[3]–31–> 高地址
大端序: | 0x00 | 0x00 | 0x00 | 0x01 |
低地址 --0—[0]—7-8–[1]–15-16-[2]-23-24–[3]–31–> 高地址
方法二:使用union,其所有成员共享同一个最大的内存地址。
1 bool isBigEndian()
2 {
3 union c
4 {
5 int a;
6 char b;
7 };
8 c.a = 1;
9
10 if (c.b == 1) {
11 return true;
12 } else {
13 return false;
14 }
15 }
在编程时使用移位操作,我们未感受到端序对代码的影响,是因为操作系统和编译器低层实现时屏蔽了这些细节,为编程人员提供了逻辑上的一致性操作,但是其实际物理上的操作会根据端序不同做出不同的处理方式。
历史上很多人对端序优劣的争执很情绪化,个人认为没有必要,并且也没有任何论证能证明两者之间的优劣。
相关故事:“端序”(endian)词语起源于《格利佛游记》。书中有一个故事:有一个国家发生了政变,分裂为两个国家,并且连年征战。而整个事件的起因是一派人赞成吃鸡蛋的时候应该先打破小的一端来吃,而另一派则主张应该打破大的一端来吃。双方还纷纷著书立说来阐述各自的道理。这是一个讥讽意味的故事,告诫大家不要做这种没有意义的争执。
原码、补码、反码
一个数在计算机中的二进制表示形式, 叫做这个数的机器数
。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.
将带符号位的机器数对应的真正数值称为机器数的真值
。如: 10000011 的真值是3。
原码
: 原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。原码是人脑最容易理解和计算的表示方式.
反码
:反码的表示方法是: 正数的反码是其本身,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。
补码
: 补码的表示方法是:正数的补码就是其本身,负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
为了使计算机能方便实现加减法,计算机使用补码存储二进制,这也是32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.
常用的位操作
X & 1 == 1 OR == 0 // 判断个位数
X = X & (X-1) // 清零最低位的1
X & -X // 得到最低位的1
X & ~X // 0
x & (~0 << n) //将x最右边的n位清零
(x>>n) & 1// 获取x第n位的值,0/1
x & (1 << (n-1)) // 获取x的第n位的幂值
x | (1 << n) // 将x的第n位置为1
x & (~(1 << n)) // 将x的第n位置为0
x & ((1 << n) -1) // 将x的最高位到第n位清零
x & (~((1 << n) -1)) // 将x的第n位至第0位清零
逆波兰表示法
就是指操作数在前面,操作符放后面的一种表示方法。
(1 – 2) * (4 + 5) 采用逆波兰表示法表示为: 1 2 - 4 5 + *
位、字节、字
内存的最小单位为位(b)。
8位 为一个字节(byte)。
字(word)是单个的存储的单元,和具体的机器有关。譬如32位的机器,字为32位,4字节。在64位的机器上,字为64位,8字节。