C语言程序设计阅读笔记
C语言程序设计阅读笔记
第一章 导言
浮点数知识:
- http://www.jb51.net/article/103412.htm 原码、反码、补码、移码的作用
- 浮点数结构 http://blog.csdn.net/whzhaochao/article/details/1288587
- https://jingyan.baidu.com/article/425e69e6e93ca9be15fc1626.html 小数转二进制
在程序中使用300、20等"幻数"并不是好习惯,它们几乎不能给以后阅读程序的人提供什么信息,而且使程序的修改更加困难。处理这类幻数的一种方法是赋与它们有意义的名字。
如果程序中的幻数都以符号常量的形式出现,对程序进行大量修改就会相对容易很多。
#define 指令行的未尾没有分号。
标准库提供的输入/输出模型非常简单:无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。
语句++nc比nc=nc+1更精炼一些,且通常效率更高一些。
%.0f强制不打印小数点和后面的小数部分,因此小数部分的位数为0。
单独的分号称为空语句。
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。
但是,它只不过是小的整型数的另一种写法而已。
在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。
char类型的字符是小整数,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。
之所以getchar的返回值得是int 而不是char, 是因为它需要包含int类型的EOF。
程序中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失,这也是其他语言通常把这类变量称为 自动变量 的原因。
如果 自动变量 没有赋值,则其中存放的是无效值。
外部变量 必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时声明其类型。
声明时可以用 extern 语句显示声明,也可以通过上下文隐式声明。
在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern声明。
在通常的做法中,所有外部的变量的定义都是放在源文件中的开始处,这样就可以省略extern声明。
如果程序包含在多个源文件中,而某个变量在file1中定义,在file2和file3文件中使用,那么在文件file2与file3中就需要使用 extern 声明来建立该变量与其定义之间的联系。
"定义"表示创建变量或分配存储单元,而"声明"指的是说明变量的性质,但并不分配存储单元。
有赋值语句叫定义,没有赋值说句叫声明.
int a=5;//定义
int b;//声明
过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清–外部变量的值可能会被意外地或不经意地修改,而程序的修改又变得十分困难。
第二章 类型、运算与表达式
标准函数strlen(s)可以返回字符串参数s的长度,但长度不包括末尾的'\0'。
枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define语句来说,它的优势在于常量值可以自动生成。
字符常量'\0'表示值为0的字符,也就是空字符(null)。我们通常用'\0'的形式代替0,以强调某些表达式的字符属性,但其数字值是0。
所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。
如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。
默认情况下,外部变量与静态变量将被初始化为0。未经显式初始化的自动变量的值为未定义的(即无效值)。
对数组而言,const限定符指定数组所有元素的值都不能被修改。
const限定符也可配合数组参数使用,它表明函数不能修改数组元素的值。
如果试图修改const限定的值,其结果取决于具体的实现。
逻辑非运算符!的作用是将非0操作数转换为0,将操作数0转换为1。
需要注意的是, C语言没有指定同一运算符中多个操作数据的计算顺序。(&&、|| 、?:和,运算符除外).
类似的, C语言也没有指定函数各参数的求值顺序。
像下面的语句:
a[i] = i++;//数组下标i是引用的旧值还是新值,编译器会有不同的解释,因为最佳的求值顺序同机器结构有很大的关系。。实际开发中,需要避免这样的代码。
在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。
类型转换
一般来说,自动转换是指把"比较窄"操作数转换为"比较宽的"操作数,并且不丢失信息的转换。
C语言没有指定char类型的变量是无符号变量还是有符号变量。
C语言定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是,存储在字符变量中的位模式在某些机器中可能是负的,
而在另一些机器中可能是正的。为了保证程序的可移植性,如果要在char类型的变量中存储非字符数据,最好指定signed或unsigned限定符。
注意,表达式中的float类型的操作数不会自动转换为double类型,这一点与最初的定义有所不同。一般来说,数学函数使用double的变量。
使用float类型主要是为了在使用较大的数组时节省存储空间,有时也是为了节省机器执行时间(双精度算术运算特别费时)。
带符号与无符号值之间的比较运算是与机器相关的,因为它们取决于不同整数类型的大小。譬如-1L<1U, -1L>1UL(-1L被转换成UL,转成了正数)
当把较长的整数转换为较短的整数或char类型时,超出的高位部分将被丢弃。
当被double类型转换为float类型时,是进行四舍五入还是截取取决于具体的实现。
即使调用函数的参数为char或float类型, 我们也把函数参数声明为int或者double类型。(在没有函数原型的情况下,char与short类型者将被转换为
int类型, float类型将被转换为double类型。)
通常情况下,参数是通过函数原型声明的。
在对signed类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出来的部分用0填补(即“逻辑位移”)
x = x & ~077
注意,表达式x&~077与机器字长无关,它比形式为x&0177700的表达式要好,因为后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,因为
~077是常量表达式,可以在编译时求值。
赋值语句具有值,且可以用在表达式中,在这类表达式中,赋值表达式的类型是它的左操作数的类型,
其值是赋值操作完成后的值。
运算符优先级与求值次序
*同大多数语言一样,C语言没有指定同一运算符中多个操作数的计算顺序。* 如 x=f()+g(), f()是先于g(),还是后于g()调用,是未定义的。
*类似地,C语言也没有指定函数各参数的求值顺序。* printf("%d %d\n", ++n, power(2,n)), 执行结果取决于编译器。
*注意*
a[i] = i++; 此问题,i是引用旧值,还是新值,C语言是未定义的,执行结果由编译器决定,因为最佳的求值顺序同机器结构有很大的关系。
在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都不是好的程序设计风格。
需要记忆的优先级
() [] . -> 从左至右
! ~ ++ – + – * & (type) sizeof 从右至左
第三章 控制流
标号可以位于对应的goto语句所在函数的任何语句的前面。标号的作用域是整个函数。
在深度潜逃的场景下,跳出循环。用goto语句还是有用的。但不应该滥用。
第四章 函数与程序结构
如果函数定义中省略了返回值类型,则默认为int类型。
程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值、外部变量进行。
return 表达式。
在必要时,表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号,此处的括号是可选的。
如果某个函数从一个地方返回时有返回值,而从另一个地方返回时没有返回值,该函数并不非法,但可能是一种出问题的征兆。
在任何情况下,如果函数没有成功地返回一个值,则它的"值"肯定是无用的。
如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明。
例如:
sum += atof(line)
atof被假设为返回值为int值,同时为了兼容旧版本,并不会对参数做假设。 并且,如果函数声明中不包含参数,编译程序不会对参数做任何假设,并会关闭所有的参数检查。
规范的做法是:
*如果函数带有参数,则要声明它们;如果没有参数,则使用void进行声明*
由于C语言不允许在一个函数中定义其他函数,因此函数本身是“外部”的。默认情况下,外部变量与函数具有下列性质:
通过同一个名字对外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用的同一个对象(标准中把这一性质称为外部链接)
名字的作用域指的是程序中可以使用该名字的部分。
外部变量 或函数的作用域从声明它的地方开始,到其所在的(待编译)文件的末尾结束。
如果要在外部变量的定义之前使用该变量,或者外部变量的定义与声明不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字 **extern**。
变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此以外还将引起存储器的分配。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其它文件可以通过extern声明来访问它。
用 static 声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译文件的剩余部分。
static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。
register 变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。
register变量只适用于自动变量以及函数的形式参数。
无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。
在一个好的程序设计中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很可能引起混乱和错误。
在不进行显示初始化的情况下,外部变量与静态变量都将被初始化为0, 而自动变量及寄存器变量的初值则没有定义(即初值为无用的信息)
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)
int days[13] = {1,2}
如果初始化表达的个数比数组元素少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。
递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归的代码更易于编写与理解。
C预处理器
宏替换
# 与 ##
如果在替换文本中,参数名以 # 作为前缀则结果将是被扩展 为 *由实际参数替换为该参数的带引号的字符串*。
#define dprint(expr) printf(#expr " = %g\n", expr)
使用语句
dprint(x/y);
调用该宏时,该宏将被扩展为:
printf("x/y" " = %g\n", expr) 等价于printf("x/y = %g\n", x/y)
如果替换文本中的参数与 ## 相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果 *重新扫描*。
#define paste(front, back) front ## back // paste(name,1) 将建立记号name1, 可用作动态变量?
第五章 指针与数组
C语言中,指针使用很广泛:
- 指针常常是表达某个计算的唯一途径。
- 使用指针通常可以生成更高效、更紧凑的代码。
ANSI C使用类型void*(指向void的指针)代替char*作为通用指针的类型。
指针与地址
指针是能够存放一个地址的一组存储单元(通常是2个或4个字节)
地址运算符&只能应用于内存中的对象,即变量与数组元素。 它不能作用于表达式、常量或register类型的变量。
一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。
int *ip;
上述声明语句表明*ip指向的对象类型是int。
每个指针都必须指向某个特定类型的数据类型。(void类型指针例外,但它不能间接引用其自身)
指针与函数参数
- 由于C语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中变量的值。如果修改形参的值,实际修改的是副本。
- 指针参数使得被调用函数能够访问和修改主调函数中对象的值。因为形参指向的地址和实参指向的地址一样。
指针与数组
通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但更难理解。
根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。因此pa = &a[0] 和 pa = a是相同的。
对数组元素a[i]的引用也可以写成*(a+i)这种形式。
在计算数组a[i]时,C语言实际上先将其转换为*(a+i)的形式,然后再进行求值,因此在程序中这两种形式是等价的。
数组名和指针之间有一个不同之处。
指针是一个变量,因此pa=a 和 pa++都合法。
但数组名不是变量,因此类型于a = pa 和a++形式的语句是非法的。
在函数定义中,形式参数
char s[];
和
char *s;
是等价的。更习惯用后一种。
地址算术运算
通常,对指针有意义的初始化只能是0或者是表示地址的表达式。
指针与整数之间不能相互转换,但0是惟一例外,因为把0定义为指针的一个特殊值,常用符号常量NULL代替常量0,表示指针还未指向合适的地址。
有效的指针运算有以下情况:
- 相同类型指针之间的赋值运算;
- 指针同整数之间的加法或减法运算;数组位移
- 指向相同数组中元素的两个指针间的减法或比较运算;位置关系
- 将指针赋值为0或指针与0之间的比较运算。 判断指针的值是否有效
字符指针与函数
C语言没有提供将整个字符串作为一个整体进行处理的运算符。
注意以下声明的区别:
char amessage[] = "now is the time";
char *pmessage = "now is the time";
amessage是一个仅仅足以存放初始化字符串以及空字符'\0'的一维数组。数组中的内容可以修改,但amessage始终指向同一个存储地址。
pmessage是一个指针,其初值指向一个字符串常量,之后它可以指向其它地址,但如果试图修改字符串的内容,结果是没有定义的
指针数组以及指向指针的指针
区分指针数组和数组指针:
int (*p)[n]
()的优先级最高,所以这说明定义的是一个指针。接下来是int [n], *p返回的是一个int[n], 即p指向一个整形的一维数组,它的长度是n。
int *p[n]
[]的优先级更高。所以这说明定义的是一个数组。接下来是int *p,说明这个数组存放的是整型指针。
命令行参数
ANSI标准要求argv[argc]的值必须为一空指针。
复杂声明
复杂的声明让人难以理解,原因在于:
C语言的声明不能从左至右阅读,而且使用了太多的圆括号。
规则如下:
dcl: 前面带有可选的*的direct-dcl
direct-dcl: name
(dcl)
direct-dcl()
direct-dcl[]
理解的方法有:右左方法。
几个关键运算符: *、 []、 ()。
从规则不难推导出右左法则。
- 首先找到name, 为direct-dcl
2.然后看右边是()还是[], 如果是(),说明声明的函数,如果是[],说明声明的数组。
3.看完右边看左边,如果左边是*,则说明内容是指针。
4.按此解析直到解析完。
第六章 结构
ANSI标准在结构方面的主要变化是字义了结构的赋值操作– **结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。
在ANSI标准中,自动结构和数组现在也可以进行初始化。
可以理解是一个复合类型, 高级语言里面的类和对象就是通过结构体实现的。
结构的基本知识
struct point {
int x;
int y;
}
关键字struct 引入结构声明。结构声明由包含在花括内的一系列声明组成。
struct 后面的名字是可选的,称为结构标识。结构标记用于为结构命名,在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。
struct 声明定义了一种数据类型。
结构与函数
结构的合法操作只有几种:
- 作为一个整体复制和赋值
- 通过&运算符取地址。
- 访问其成员
如果传递给函数的结构很大,使用指针的方式的效率通常比复制整个结构的效率要高。
注意运算符的优先级和结合顺序:
在所有运算符中,结构运算符"." 和 "->"、用于函数调用的"()"以及用于下标的"[]",因此,它们同操作数之间的结合也最紧密。
注意,他们都是从左至右的结合顺序。
结构数组
条件编译语句#if中不能使用 sizeof, 因为预处理不对类型名进行分析。
但预处理器并不计算#define中的表达式,因此在#define中使用 sizeof是合法的。
指向结构的指针
自引用结构
struct tnode {
char \*word;
int count;
struct tnode \*left;
struct tnode \*right;
};
一个包括其自身实例的结构是非法的,但是,下列声明是合法的。
struct tnode *left;
类型定义(typedef)
typedef用来建立新的数据类型名。
从任何意义上讲,typedef并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。
typdef声明也并没有增加任何新的语义:通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。
实际上,typedef 类似于#define 语句,但由于typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。
除了表达方式更简洁之外,使用typedef还有另外两个重要原因。
- 它可以使程序参数化,以提高程序的可移值性。如果typdef声明的数据类型同机器有关,那么,当程序移植到其他机器上时,只需改变typedef类型定义就可以了。
- 为程序提供更好的说明性。
联合
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。
联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息。
联合的目的:一个变量可以合法地保存多种数据类型中任何一种类型的对象。其语法基于结构,如下所示:
union tag {
int ival;
float fval;
char *sval;
}
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0, 此结构空间要大到足够容纳最"宽"的成员,并且,其对齐方式要适合于联合中所有类型的成员。
联合只能用其第一个成员类型的值进行初始化。
ps:不知道怎么用。能节省能内存,但真正使用的时候,还得知道最后存的值是什么类型,管理也挺麻烦的。
关于对齐,参考文章: http://blog.sina.com.cn/s/blog_715de2f50100pgs3.html
位字段
多个对象保存在一个机器字中,取和设置的时候只操作字的部分位。
这其实可以通过移位运算、屏蔽运算、补码运算进行简单的位操作来实现。
#define EXTERNAL 02
#define STATIC 04
利用|来实现置1操作
flags |= EXTERNAL | STATIC;
利用&来实现置0操作
flags &= ~(EXTERNAL | STATIC);
相对于以上方法,C语言提供了另一种可替代的方法,即直接定义和访问一个字中位字段的能力,而不需要通过按位逻辑运算符。
位字段,或简称字段,是"字"中相邻位的集合。"字"是单个的存储单元,它同具体的实现有关。
示例如下:
struct {
unsigned int iskeyword : 1;
unsigned int isextern : 1;
unsigned int isstatic : 1;
}
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖边界由具体的实现定义。
字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界上对齐。
某些机器 上字段的分配是从字的左端至右端进行的,而某些机器则相反。
位字段不是数组,并且没有地址,因此对它们不能使用&运算符。
第七章 输入输出
标准库函数并不是C语言本身的组成部分。
ANSI标准精确定义了这些库函数,所以,只用库函数完成的程序是可移植的。
标准输入/输出
文本流由一系列的行组成,每一行的未尾是一个换行符。
如果系统没有遵循这种模式,则标准库将通过一些措施使得该系统适应这种模式。
格式化输出 – printf函数
printf转换说明:
负号,用于指定被转换的参数按照左对齐的形式输出。
变长参数表
使用流程一致:
- va_list ap;//声明
- va_start(ap, 最后一个有名参数);//初始化,将ap指向第一个无名参数
- value = va_arg(ap,参数类型);//获取参数的值,需要把类型传进行,才可以取得正确的值以及正确地移动到下一个无名参数。
- va_end(ap);//释放资源
格式化输入 – scanf函数
两个注意点:
- 输入字段定义为一个不包括空白符的字符串,其边界定义为下一个空白符或达到指定的字段宽度。
这表明scanf函数将越过行边界读取输入,因为换行符也是空白符。(空白符包括空格符、横向制表符、纵向制表符、换行符、回车符、换页符) - 如果转换说明中有赋值禁止字符*,则跳过该输入字段,不进行赋值。
文件访问
关于linux的标准输入、标准输出以及重定向。
0 表示标准输入(默认键盘
1 表示标准输出 (默认屏幕)
2表示标准错误。(默认屏幕)
通过重定向,可以控制以上3个的默认值。
cat file > fil2 等同于 cat file 1>fil2, 输出重定 。1>中的1可以省略不写,默认就是标准输出。
&[n] 代表已经存在的文件描述符。&1代表输出,&2代表错误。&-表示关闭与它绑定的描述符。
所以有下面两种方式放弃标准输出和标准错误。
cat file > /dev/null 2>&1
cat file 1>&- 2>&-
/dev/null是黑洞文件,所有到它的输出都会丢弃。
错误处理 – stderr 和 exit
在main程序中return xx; 相当于exit(xx);
尽管输出错误很少出现,但还是存在(例如磁盘满时);
行输入和行输出
库函数gets在读取输入时,会把换行符'\n'删除。
puts则会在行尾添加换行符。
其它函数
int ungetc(char c, FILE \*fp), 每个文件只能有一个写回字符。
system(char \*s)执行命令。