分享

《C专家编程》笔记

 键盘上的青春 2013-08-22
这是前些天看C专家编程时记录下来的笔记,记录了主要是C的一些东西,关注了在以前在学习时无意忽略的或是没有学到的东西。看完后觉得大脑中顿时清晰了许多。
PS:《C专家编程》确实不错。

---------------------------------------------------------
1.
编程挑战  关于time_t
Q:什么时候它会到头重新回到开始?
1)查看位于/include/time.h的time_t的定义
2)编写代码,在类型为time_t的变量中存放time_t的最大值,并传递给ctime()--转换时间函数

2.
C语言的基本类型直接与底层硬件相对应
float被自动扩展为double
空格对宏扩展影响巨大,比如:
#define a(y) a_expanded(y)         a(x)     a_expanded(x)
而#define a (y) a_expanded(y)      a(y)     a_expanded (y) (x)

3.
可移植的代码(portable code):
严格遵循标准的(strictly-conforming) 一个严格遵循标准的程序应该是:
只使用已确定的特性
不突破任何由编译器实现的限制
不产生任何依赖由编译器定义的或未确定的或未定义的特性的输出

4.
两个操作数都是指向由限定符或者无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。
比如
char *cp;
const char *ccp;
ccp=cp合法,而cp=ccp不合法。
类似地,const char**也是一个没有限定符的指针类型,它的类型是“指向有限定符的char类型的指针的指针”。
由于char **和const char **都是无限定符的指针类型,但是它们指向的类型不一样。前者指向char *,后者指向const char *,因此它们不相容。

5.
关键字const并不能把变量变成常量。在一个符号前加上const限定符只是表示这个变量不能被赋值,也就是它的值对于这个符号来说是只读的,但它并不能防止通过程序在内部(甚至是外部)的方法来修改这个值。

6.
#pragma指示符的行为是由编译器定义的

7.
一个‘L’的NUL用于结束一个ASCII字符串
两个‘L’的NULL用于表示什么也不指向

8.
缺省可见性
function apple(){}
extern function pear(){}
static function turnip(){}

9.
当sizeof的操作数是个类型名时,两边必须加上括号,但如果是变量则不必要。
这里有个有趣的例子,关于多义性的
apple = sizeof(int)*p;

10.
gets()函数正式的任务是从流中读入一个字符串。它的调用者会告诉它把读入的字符放在什么地方。但是,gets()函数并不检查缓冲区的空间,事实上它也我发检查缓冲区的空间。
应该把gets(line)替换成:
if(fgets(line,sizeof(line),stdin)==NULL)
exit(1);

11.
'\'字符可以对一些字符进行转义,包括newline(这里指回车键)。被转义的newline在逻辑上把下一行当做上一行的延续,它可以用做连接长字符串。如果在‘\’和回车之间留下空格就会出问题,newline 和 \newline不一样。

12.
z = y+++x;
程序员的意图可能是z = y+ ++x,也可能是z = y++ +x。ANSIC 规定了一种逐渐为人所知的“maximal munch strategy(最大一口策略)”。这种策略表示如果下一个标记有超过一种的解释方案,编译器将选取能组成最长字符串序列的方案。上面的例子将被解析为 z = y++ + x。
而z = y+++++x 将不会通过编译,引起编译错误,错误信息是“++操作符迷失空格间”(我测试得到的错误信息是non-lvalue in increment)。

13.
当控制流离开声明自动变量(即局部变量)的范围时,自动变量便自动失效。这就意味着即便返回一个指向局部变量的指针,当函数结束时,由于该变量已经被销毁,谁也不知道这个指针指向的地址的内容是什么。为了解决这个问题,一个比较好的方案是
由调用者来分配内存保存函数的返回值。为了提高安全性,调用者应该同时制定缓冲区的大小。
void func(char *result,int size)
{
...
strncpy(result,"That'd be the data segment.")
}

buffer = malloc(size);
func(buffer,size);
...
free(buffer);

14.
结构体中允许存在位段、无名字段以及字对齐所需的填充字段。这些都是通过在字段后面加一个冒号以及一个表示字段位长的整数来实现的。
例如:
struct pid_tag{
unsigned int inactive :1;
unsigned int :1;
unsigned int redcount :6;
unsigned int :0;
short pid_id;
struct pid_tag *link;
};
位段的类型只能是int,usigned int,signed int(或加上限定符),至于int位段的值可不可以是负值取决于编译器。

15.
一个比较好的编程习惯是一行代码只做一件事情。比如将变量的声明和定义分开。这样更加有利于阅读。
struct veg{int weight,price_per_lb;};
struct veg onion,radish;

16.
参数在传递时,首先尽可能地放入寄存器里(追求速度),而并非简单地直接压入堆栈。
另外,一个int型的变量i作为参数传递和一个在结构变量s中的int i传递的方式可能完全不同。后者更可能传递到堆栈中。
17.
可以把互斥的字段存储在一个联合中节省空间。
联合也可以把同一个数据解释成两种不同的东西,而不是把两个不同的数据解释为一个数据。
18.
C语言声明的优先级规则

A 声明从他的名字开始读取,然后按照优先级顺序依次读取。
B 优先级从高到低依次是:
B.1 声明中被括号括起来的部分
B.2 后缀操作符:
括号()表示这是一个函数,而
方括号【】表示这是一个数组。
B.3 前缀操作符:
星号*表示“指向...的指针”。
C 如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等),那么它作用于类型说明符。在其他情况下,const和(或)volatile关键字作用于它左边紧邻的指针星号。
19.
操作声明器的一些提示
不要再一个typedef中放入几个声明器,如下:
typedef int *ptr,(fun)(),arr[5];
不要把typedef嵌入到声明的中间部分,如下:
unsigned const long typedef int volatile *kumquat;
20.
编程挑战:编写一个程序把C语言的声明翻译成通俗语言
21.
C语言引入了“可修改的左值”这个术语,它表示左值允许出现在赋值语句的左边,这个奇怪的术语是为了与数组名区分。数组名也应用于确定对象在内存中的位置,也是左值,但是它不能作为赋值的对象。因此,数组名是个左值但是不是可修改的左值,标准规定赋值符必须用可修改的左值作为它左边一个固定的操作数。用通俗的话说,只能给可以修改的东西赋值。
22.
只有对字符串常量才能用指针分配空间,而对于浮点数之类的常量分配空间则无法通过编译。
char *p="breadfruit";
float *pip=3.141;
23.
如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;如果可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们称之为动态链接。
动态链接的主要目的就是把程序与它们使用的特定的函数库版本中分离开来,取而代之的是,我们约定由系统向程序提供一个接口,该接口保持稳定,不随时间和操作系统的后续版本发生变化。
ABI(Application Binary Interface)程序二进制接口
任何人都可以创建静态或动态的函数库,只需要简单编译一些不包含main函数的代码,并把编译所产生的.o文件用正确的实用工具进行处理——如果是静态库,使用“ar”,如果是动态库,使用“ld”。
24.
5点注意事项(UNIX)
1)动态库文件的扩展名是.so,而静态库文件的扩展名是.a
2)例如,你通过-lthread选项,告诉编译链接到libthread.so
3)编译器期望在确定的目录找到库
4)观察头文件,确认所使用的函数库
5)与提取动态库的符号相比,静态库中的符号提取的方法限制更加严格
25.
setjmp/longjmp
26.
C语言工具


-----------用于检查源代码的工具------------
cb
随编译器附带
C程序美化器,在源文件中运行这个过滤器,可以使源文件有标准的布局和缩进格式。来自Berkeley
indent
-
与cb作用相同,来自AT&T
cdecl
本书
分析C语言的声明
cflow
随编译器附带
打印程序中调用者/被调用者的关系
cscope
随编译器附带
一个基于ASCII码的C程序交互式浏览器。我们在操作系统小组中使用,用于检查头文件修改的效果。它提供了对下列问题的快速答案:“有多少命令使用了libthread?”或“阅读了kmem的所有文件是哪些?”
ctage
/usr/bin
创建一个标签文件,供vi编辑器使用。标签文件能加快检查程序源文件的速度,方法是维护一个表,里面有绝大多数对象的位置。
lint
随编译器附带
C程序检查器
sccs
/usr/ccs/bin
源代码版本控制系统
vgrind
/usr/bin
格式器,用于打印漂亮的C列表


--------------用于检查可执行文件的工具---------------
dis
/usr/ccs/bin
目标代码反汇编工具
dump -Lv
/usr/ccs/bin
打印动态链接信息
ldd
/usr/bin
打印文件所需的动态
nm
/usr/ccs/bin
打印目标文件的符号表
strings
/usr/bin
查看嵌入与二进制文件中的字符串。用于查看二进制文件可能产生的错误信息、内置文件名和(有时候)符号名或版本和版权信息
sum
/usr/bin
打印文件的检验和程序块计数。回答下面的问题:“这些可执行文件是同一版本么”“传输是否成功”


------------帮助调试的工具--------------
truss
/usr/bin
trace的SVr4版本,这个工具打印可执行文件所进行的系统调用。它可用于查看二进制文件在干什么,为什么阻塞或者失败。
ps
/usr/bin
显示进程的特征
ctrace
随编译器附带
修改你的源文件,文件执行时按行打印。是一个对小程序非常有用的工具
debugger
随编译器附带
交互式调试器
file
/usr/bin
告诉你一个文件包含的内容(如可执行文件、数据、ASCII、shell script、archive等)


--------------性能优化辅助工具-----------------
collector
随编译器附带
(SunOS独有)在调试器控制下收集运行时性能的数据
analyzer
随编译器附带
(SunOS独有)分析已收集的数据
gprof
/usr/ccs/bin
显示调用图配置数据(确定计算密集的函数)
prof
/usr/ccs/bin
显示每个程序所消耗时间的百分比
tcov
随编译器附带
显示每条语句执行的次数(确定一个函数中计算密集的循环)
time
/usr/bin/time
显示程序所使用的实际时间和CPU时间
27.
一个会发生隐士类型转换的地方是参数传递,在K&R C中,由于函数的参数也是表达式,所以也会发生类型提升。在ANSIC C中,如果使用了适当的函数原型,类型提升便不会发生,否则也会发生。在被提升的函数内部, 提升后的参数被裁减为原先声明的大小。
这就是为什么单个的printf()格式符字串%d能适用于几个不同的类型,而不论实际传递的是上述类型的哪一个。函数从堆栈中(或寄存器中)取出的的参数总是int类型,并在printf或其他被调用函数里按统一的格式处理。
28.
C语言的类型转换远比其他语言更为常见,其他语言往往将类型转换只应用于操作数上,使操作符两端的数据类型一致。C语言也执行这项任务,但它同时提升比规范类型int或double更小的数据类型(即便它们类型匹配)。
29.
int printf(const char *format,...);
表示它是一个接受可变参数个数的函数。
30.
qsort()函数的声明
void qsort(void base,size_t nel,size_t width,
int(*compar)(const void *,const void *));
31.
“数组名被改写成一个指针参数”规则并不是递归定义的。数组的数组会被改写为“数组的指针”,而不是“指针的指针”;
实参 所匹配的形参
数组的数组 char c[8][10]; char (*c)[10]; 数组指针
指针数组 char *c[15]; char **c; 指针的指针
数组指针(行指针) char (*c)[64]; char (*c)[64]; 不变
指针的指针 char **c; char **c; 不变

而之所以能在main()函数中看到char **argv这样的参数,是因为argv是个指针数组(即char *argv[])。这个表达式被编译器改写为指向数组第一个元素的指针,也就是一个指向指针的指针。如果argv参数事实上被声明为一个数组的数组(也就是char argv[10][15]),它将被编译器改写为char (*argv)[15](也就是一个字符数组指针),而不是char **argv。
32.
向printf()函数传递一个NULL指针会引起程序崩溃。
原因在于C标准规定%s说明符的参数必须是一个指向字符数组的指针,而NULL不是这样一个指针(它是一个指针,但它并不指向一个字符数组),所以这个调用将陷入“未定义行为”。
33.
在C++中存在,但在C中不存在的限制有:
1)在C++中,用户代码不能调用main()函数,但在C语言中是允许的(不过这种情况极为罕见)。
2)完整的函数原型声明在C++中是必须的,但在C语言中没有那么严格。
3)在C++中,由typedef定义的名字不能与已经存在的结构标签冲突,但在C语言中是允许的(它们分属不同的名字空间)。
4)当void*指针赋值给另一个类型的指针时,C++规定必须进行强制类型转换,但在C语言中却无必要。

在C++和C语言中含义不一样的特性:
1)C++至少增加了十几个关键字。这些关键字在C语言中可以作为标志符使用,但如果这样做了,用C++编译器编译这些代码时就会产生错误信息。
2)在C++中,声明可以出现在语句可以出现的任意位置。在C语言代码块中,所有的声明必须出现在所有语句的前面。
3)在C++中,一个内层范围的结构名将会隐藏外层空间中相同的对象名。在C语言中则非如此。
4)在C++中,字符常量的类型是char,但是在C语言中,它们的类型是int。也就是说,在C++中,sizeof('a')的结果是1,而在C语言中要大一些。
5)由于C++增加了新的//注释符,有时会在两种语言中产生微妙而怪异的差别。

<finish>

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多