《C专家编程》

1. C:穿越时空的迷雾

  • 在编译器中,效率几乎就是一切。编译器的效率包括两个方面:运行效率(代码运行速度)和编译效率(产生可执行代码的速度)。
  • 两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。
    const char *类型并不是一个有限定符的类型——它的类型是“指向一个具有const限定符的char类型的指针”,也就是说const限定符修饰指针所指向的类型,而不是指针本身。
char *cp;
const char *ccp;
对 ccp = cp;
错 cp = ccp;
  • const 最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据。
  • 关于unsigned int 和 int
    下面TOTAL_ELEMENTS时unsigned int,-1是int,-1转为unsigned int变为一个最大的整数,正确做法是对unsigned int进行强制类型转换。
int array[] = { 23, 34, 12, 17, 204, 99, 16 };
#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))

int _tmain(int argc, _TCHAR* argv[])
{
    int d = -1, x = 0;
    if (d <= TOTAL_ELEMENTS - 2)
        x = array[d + 1];
    cout << x << endl;
    if (d <= (int)TOTAL_ELEMENTS - 2)
        x = array[d + 1];
    cout << x << endl;

    return 0;
}
  • 尽量不要在代码中使用无符号类型,只有在使用位段或二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或无符号数,这样就不必由编译器来选择结果类型。

2. 这不是Bug,而是语言特性

  • 分析编程语言缺陷的一种方法就是把所有的缺陷归于3类:不该做的做了(多做之过);该做的没做(少做之过);该做的做得不合适(误做之过)。
  • NUL用于结束一个ASCII字符串,NULL用于表示空指针。

2.1 多做之过

  • switch的fall through。break跳出的是最近的那层循环或switch语句。
  • C语言可见性的局限性:all-or-nothing,一个符号要么全局可见,要么对其他文件都不可见。

2.2 误做之过

《C专家编程》_第1张图片

《C专家编程》_第2张图片
  • 大部分编程语言并未明确规定操作数计算的顺序,之所以未定义,是想让编译器充分利用自身架构的特点,或者充分利用存储于寄存器中的值。
  • 在涉及除了算术运算符之外的所有操作符时,一律加括号。
  • gets() -> fgets(),gets()函数并不检查缓冲区空间,可能会造成堆栈溢出,覆盖堆栈原先的内容。

2.3 少做之过

  • 需要使用函数返回值(数组)的方法
    在同一代码块中同时进行malloc和free操作,内存管理是最为轻松的。
void func(char *result, int size) {
    ...
    strncpy(result, " xxx ", size);
}

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

3. 分析C语言的声明

《C专家编程》_第3张图片
  • 用优先级分析char * const (next) ();声明
    《C专家编程》_第4张图片

    next是一个指针,该指针指向一个函数,该函数返回一个指针,返回的指针指向一个类型为char的常量指针。
    《C专家编程》_第5张图片
  • typedef的使用
void (*signal (int sig, void (*func)(int))) (int);
可以使用typedef:
typedef void (*ptr_to_func) (int);
ptr_to_func signal(int, ptr_to_func);
  • typedef和宏之间的区别
    1)可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型却不可以
    2)在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而宏定义的类型则无法保证
1)修饰
#define peach int
unsigned peach i; // ok
typedef int banana;
unsigned banana i; // wrong

2)连续声明
#define int_ptr int *;
int_ptr chalk, cheese; // cheese不是指针
typedef char * cha_ptr;
char_ptr Bentley, Rolls_Royce;// 都是指针
  • typedef建议应用场景:
    1)数组、结构、指针以及函数的组合类型
    2)可移植类型
    3)为后面的强制类型转换提供一个简单的名字
    另外,应该始终在结构的定义中使用结构标签,即使它并非必须。这种做法可以使代码更为清晰。

  • 编写一个cdecl程序

4. 令人震惊的事实:数组和指针并不相同

  • 由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。对于多维数组,需要提供除最左边一维以外其他维的长度——这就给编译器足够的信息产生相应的代码。
  • 左值编译时可知,左值表示存储结果的地方。右值直到运行时才知,右值表示地址的内容。(编译时可知也不太准确,局部变量存储在栈中的位置是不确定的!!)

5. 对链接的思考

  • 收集模块准备执行的三个阶段的规范名称是链接-编辑、载入和运行时链接。静态链接的模块被链接编辑并载入以便运行。动态链接的模块被链接编辑后载入,并在运行时进行链接以便运行。程序执行时,在main()函数调用前,运行时载入器把共享的数据对象载入到进程的地址空间。外部函数被真正调用之前,运行时载入器并不解析他们。所以即使链接函数库,如果并没有实际调用,也不会带来额外开销。(Q.动态链接共享库映射到进程的地址空间是个什么概念?)
    即使是静态链接,整个libc.a文件也并没有被全部装入到可执行文件中,所装入的只是所需要的函数。


    《C专家编程》_第6张图片
  • 动态链接的主要目的是把程序与它们使用的特定的函数库版本分离开来。取而代之的是,我们约定由系统向程序提供一个接口,该接口保持稳定,不随时间和操作系统的后续版本发生变化。
    由于该接口介于应用程序和函数库二进制可执行文件所提供的服务之间的接口,所以称之为应用程序二进制接口(Application Binary Interface, ABI)。
  • 动态链接的优势
    1)函数库只有在需要时才被映射到进程中
    2)所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。
  • mmap()把文件映射到进程的地址空间中。文件的内容可以通过读取连续的内存地址来获得。当文件包含可执行文件的指令时,这种方法尤为适宜。在SVr4系统中,文件系统被当作虚拟内存系统的一部分,而mmap就是一种把文件映射到内存的机制。(这种映射是怎样实现的?这种映射意味着什么?跟加载到内存有什么区别?)
  • 动态链接时一种JIT(just-in-time)链接,意味着程序在运行时必须能够找到它们所需要的函数库。链接器通过把库文件或路径名植入可执行文件中来做到这一点。这意味着,函数库的路径不能随意移动。
  • 创建静态或动态的函数库。只需简单地编译一些不包含main函数的代码,并把编译所产生的.o文件用正确的实用工具进行处理——如果是静态库,使用ar,如果是动态库,使用ld。
  • 对于函数库应该始终使用与位置无关代码,对于共享库,与位置无关的代码显得格外有用,因为每个使用共享库的进程一般都会把它映射到不同的虚拟地址(尽管共享同一份物理拷贝)。
  • 函数库链接的5个特殊秘密
    1)动态库文件的扩展名.so,静态库的扩展名是.a
    2)通过lthread选项,告诉编译器链接到libthread.so。-lname链接到libname.so
    3)编译器期望在确定的目录找到库。编译器选项-Lpathname告诉链接器一些其他目录,如果命令中加入-l选项,链接器就往这些目录查找函数库。系统中存在几个环境变量,LD_LIBRARY_PATH和LD_RUN_PATH。出于安全性、性能和创建/运行独立性方面的考虑,使用环境变量的做法现在已经不提倡。一般还是在链接时使用-Lpathname和-Rpathname选项。
    4)观察头文件,确认所使用的函数库
    5)与提取动态库中的符号相比,静态库中的符号提取的方法限制更严
    动态链接中,所有的库符号进入输出文件的虚拟地址空间,所有的符号对于链接在一起的所有文件都是可见的。相反的,对于静态链接,在处理archive时,它只是在archive中查找载入器当时所知道的未定义符号。也即,在编译器命令行中各个静态库出现的顺序是非常重要的。
    如果在自己的代码之前引入静态库,此时尚未出现未定义符号,所以不会从函数库中提取任何符号。
    函数库选项的位置:始终将-l函数库选项放在编译命令行的最右边。
怎样在函数库中观察一个符号?
链接程序时遇到下面的错误:
ld : undefined symbol
  _xdr_reference
*** Error code 2
make: Fatal error: Command failed for target 'prog'

解决方法:
通过符号xdr_reference找到需要链接的库。基本想法是使用nm命令在/urs/lib的每个函数库中浏览所有的符号,从中寻找所丢失的符号。
cd /usr/lib
foreach i (lib?*)
echo $i
nm $i | grep xdr_reference | grep -v UNDEF
end
在当前目录中的所有函数库上运行nm程序,它显示函数库中已知的符号列表。通过grep设定需要搜索的符号,并过滤掉标记为"UNDEF"的符号(在函数库中有引用,但并不是在此定义)。
  • 警惕Interpositioning,也即自己代码中某个符号的定义取代函数库中的相同符号的意外。
    准则:不要让程序中的任何符号成为全局的,除非有意把它们作为程序的接口之一。


    《C专家编程》_第7张图片

    《C专家编程》_第8张图片

    《C专家编程》_第9张图片
  • ld程序使用-m选项,让链接器产生一个内存映射或列表,现实在可执行文件中的什么地方放入哪些符号。同时显示同一个符号的多个实例,通过查看报告的内容,用户可以判断是否发生了Interpositioning.
    -D 可以监视从archive中提取对象的过程,同时可用于显示运行时绑定信息。
    ldd
    ld -Dhelp

6. 运动的诗章:运行时数据结构

  • 学习运行时系统的三个理由:
    1)有助于优化代码,获得最佳效率
    2)有助于理解更高级的材料
    3)陷入麻烦时,它可以使分析问题变得更加容易

  • 段(segments)是目标文件中简单的区域,里面保存了和某种特定类型(如符号表条目)相关的所有信息。一个段一般包含几个section。


    《C专家编程》_第10张图片
  • 段可以方便地映射到链接器在运行时可以直接载入的对象中。载入器只是取文件中每个段的映像,并直接将它们放入内存中。从本质上说,段在正在执行的程序中是一块内存区域,每个区域都有特定的目的。
    1)文本段包含程序的指令。链接器把指令直接从文件拷贝到内存中(一般使用mmap()系统调用),以后便不再管它,因为文本不会改变。
    2)数据段包含经过初始化的全局和静态变量以及它们的值。
    3)BSS段从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段之后。当这个内存区进入程序的地址空间后全部清零。包括数据段和BSS段的整个区段此时通常称为数据区。
    4)栈段用于保存局部变量、临时数据、传递到函数中的参数
    5)堆空间用于动态分配的内存
    6)虚拟地址空间的最低部分未被映射。位于进程的地址空间内,但并未赋予物理地址,所以任何对它的引用都是非法的。在典型情况下,它是从地址零开始的几K字节。用于捕捉使用空指针和小整型值的指针引用内存的情况。


    《C专家编程》_第11张图片

    7)当考虑共享库是,进程的地址空间如下:


    《C专家编程》_第12张图片
  • C语言运行时数据结构有好几种:栈、活动记录(activation record)、数据、堆等。
    1)栈
    可以对栈顶进行操作,也可以修改位于栈中位置的值。
    栈的作用是为了允许递归调用,这意味着必须找到一种方法,在同一时刻允许局部变量的多个实例存在,但只有最近被创建的那个才能被访问。
    栈有三个主要用途:
    a.栈为函数内部声明的局部变量提供存储空间
    b.进行函数调用时,栈存储与此有关的一些维护性信息。这些信息被称为栈帧(stack frame),另外一个更常用的名字是过程活动记录(procedure activation record)。包括函数返回地址、任何不适合装入寄存器的参数以及一些寄存器值。
    c.栈可以用作暂时存储区。有时候程序需要一些临时存储,比如计算一个很长的算术表达式时,可以把部分计算结果压到栈中,当需要时再把它从栈中取出。通过alloca()分配的内存就是位于栈中。
    2)过程活动记录
    在自己的地址空间如何管理程序。C语言自动提供的服务之一就是跟踪调用链——哪些函数调用哪些函数,当下一个return语句执行后,控制将返回何处等。解决这个问题的经典机制是栈中的过程活动记录。结构的具体细节在不同编译器中各不相同,次序也不同,可能还存在一个在调用函数前保存寄存器值的区域。


    《C专家编程》_第13张图片

    程序的词法布局:
    静态链接——指向从词法上讲属于外层过程的活动记录,由编译时决定。(C语言本身不允许嵌套函数,在其数据中没有上层引用,所以在它的活动记录中不需要静态链接)
    动态链接——活动记录指针链,在运行时指向最靠近自己的前一个过程调用的活动记录。


    《C专家编程》_第14张图片
    栈向下生长

    查看/usr/include/sys/frame.h了解过程活动记录的布局
    尽可能将过程活动记录放到寄存器中会使函数调用速度更快,效果更好。
    进程中支持不同的控制线程,只要简单地为每个线程分配不同的栈即可。
  • setjmp和longjmp——通过操纵过程活动记录实现的
    a)setjmp(jmp_buf j) 必须首先调用。表示使用变量j记录现在的位置。函数返回零。
    b)longjmp(jmp_buf j, int i)可以接着被调用。表示回到j所记录的位置,让它看上去像是从原先的setjmp()函数返回一样。但是函数返回i,使代码能够知道它实际上是通过longjmp()返回的。当使用longjmp()时,j的内容被销毁。
    setjmp保存了一份程序的计数器和当前的栈顶指针。longjmp恢复这些值,有效地转移控制并把状态重置会保存状态的时候,成为“展开栈”。
    和goto的不同:
    goto不能跳出C语言的当前函数,longjmp可以跳的很远;
    longjmp只能跳回到曾经到过的地方,longjmp更像是“从何处来come frome”而不是“往哪里去go to”。
    保证局部变量在lonjmp过程中一直保持它的值的唯一可靠的方法是把它声明为volatile(适用于那些值在setjmp执行和longjmp返回之间会改变的值)。
    setjmp/longjmp最大的用途是错误恢复,在C++中变异为异常处理机制catch和throw。

  • 有用的C语言工具


    《C专家编程》_第15张图片

    《C专家编程》_第16张图片

    《C专家编程》_第17张图片

    《C专家编程》_第18张图片

    《C专家编程》_第19张图片
  • 可以将汇编代码嵌入到C代码之中。通常只用于深入操作系统核心非常依赖机器的任务。例如设置某个特别的寄存器,把系统的状态从管理员模式转变为用户模式。

7. 对内存的思考

  • 段的第二种含义:在Intel 80x86内存模型中,段是内存模型设计的结果,在80x86的内存模型中,各处理器的地址空间并不一致(因为要保持兼容性),但它们多被分割成以64k为单位的区域,每个这样的区域便称为段,由一个段寄存器所指向。

  • 虚拟内存:操作系统使每个进程都以为自己拥有整个地址空间的独家访问权。所有进程共享机器的物理内存,当内存用完时就用磁盘保存数据。在进程运行时,数据在磁盘和内存之间来回移动。内存管理硬件负责把虚拟地址翻译为物理地址,并让一个进程始终运行于系统的真正内存中。


    《C专家编程》_第20张图片

    虚拟内存通过页的形式组织,页就是操作系统在磁盘和内存之间移来移去或进行保护的单位。可以通过/usr/ucb/pagesize来查看。
    如果进程可能不会马上运行,操作系统可以暂时收回所有分配给它的物理内存资源,将该进程的所有相关信息都备份到磁盘上。这个进程就被“换出”,在磁盘中有一个特殊的“交换区”,用于保存从内存中换出的进程。
    当进程引用一个不在物理内存中的页面时,MMU就会产生一个页错误,内核对此事件作出响应,并判断该引用是否有效。如果无效,内核像进程发出一个“segmentation violation”信号。如果有效,内核从磁盘取回该页,换入到内存中,一旦页面进入内存,进程便被解锁——进程本身并不知道它曾因为页面换入时间等待了一会。
    所有的虚拟内存操作都出于同样的设计哲学,就是把文件区域映射到内存区域中。

  • cache的组成


    《C专家编程》_第21张图片

    充分利用cache会提高程序运行速度

*数据段和堆
堆中所有东西都是匿名的——不能按名字直接访问,只能通过指针间接访问。


《C专家编程》_第22张图片

堆的末端由一个称为break的指针来标识,当堆管理器需要更多内存时,它可以通过系统调用brk和sbrk来移动break指针。一般情况下,不必由自己显式地调用brk,如果分配的内存很大,brk最终会被自动调用。

  • 如何检测内存泄漏
    step1.使用swap观察还有多少可用的交换空间/usr/sbin/swap -s
    在一两分钟内键入该命令三到四次,看看可用的交换区是否在减少
    还可以使用其他一些/usr/bin/*stat工具如netstat、vmstat,如果发现不断有内存被分配且从不释放,一个可能就是有个进程出现了内存泄漏
    step2.确定可疑的进程

  • 在现代的计算机架构中,尤其是RISC架构,都需要数据对齐,因为与任意的对齐有关的额外逻辑会使整个内存系统更大且更慢。通过迫使每个内存访问局限在一个Cache行或一个单独的页面内,可以极大地简化(并加速)如Cache控制器和内存管理单元这样的硬件。

  • 段错误是由于内存管理单元的异常所致,而该异常则通常由于
    1)解引用一个未初始化
    2)解引用非法值的指针引起的
    3)在未得到正确的权限时进行访问
    4)用完了栈或堆空间

8. 为什么程序员无法分清万圣节和圣诞节

  • 隐式类型转换是语言中的一种临时机制,起源于简化最初编译器的想法。把所有的操作数转换为统一的长度极大地简化了代码的生成。这样压到栈中的参数都是同一长度的,所以运行时系统只需要知道参数的数目,而不需要知道它们的长度。

9. 再论数组

《C专家编程》_第23张图片
  • 什么时候数组和指针是相同的?
    C语言标准对此作了如下说明:
    1)表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针
    2)下标总是与指针的偏移量相同
    3)在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针
  • 数组形参是如何被引用的?


    《C专家编程》_第24张图片

10. 再论指针

《C专家编程》_第25张图片
  • 将多维数组作为函数参数
    1)一维数组——需要包括一个计数值或者是一个能够标识越界位置的结束符。
    2)二维数组——对于字符串来说,可以,对于其他类型,需要增加一个计数值或者能够标识越界位置的结束符。以利于调用函数和被调用函数之间的约定。
    3)多维数组——无法使用指针
    对于多维数组作为参数传递的支持缺乏是C语言存在的一个内在限制。这使得C语言编写某些特定类型的程序非常困难(如数值分析算法)。(一个问题:其他支持数值分析算法的语言是怎么支持的?)

11. 你懂得C,所以C++不在话下

11.1 抽象——提取事物的本质特性

  • 抽象的作用
    1)隐藏不相关的细节,把注意力集中在本质特征上
    2)向外部世界提供一个“黑盒子”接口。接口确定了施加在对象上的有效操作的集合,但它并不提示对象在内部是怎样实现它们的
    3)把一个复杂的系统分解为几个相互独立的组成部分。这样可以做到分工明确,避免组件之间不符合规则的相互作用
    4)重用和共享代码

11.2 封装——把相关的类型、数据和函数组合在一起

11.3 继承——复用已经定义的操作

11.4 重载——作用于不同类型的同一操作具有相同的名字

11.5 多态——运行时绑定(覆盖)

  • 重载——函数的原型不同,这样编译器才能通过查看参数的类型判断需要调用哪个函数
    覆盖——函数的原型必须相同,由运行时系统进行解析调用哪个函数

11.6 其他

  • 编程语言的主要目标是提供一个框架,用计算机能够处理的方式表达问题的解决方法。
    一门语言,如果它的结构是有用的“建构块”,便于堆积起来解决某个特定领域的问题,它就能获得成功。

12.面试相关的一些问题

  • 怎样才能检测到链表中存在循环
  • C语言中不同的增值语句的区别何在
  • 库函数调用和系统调用区别何在
  • 读取一个字符串,并输出它里面字符的所有组合
    给定一个数,要求列出所有不大于N的素数
    编写一个子程序,进行两个任意大小矩阵乘法运算
  • 文件描述符和文件指针有何不同
  • 编写一些代码,确定一个变量是有符号数还是无符号数
  • 打印一棵二叉树的值的时间复杂度
  • 从文件中随机提取一个字符串

你可能感兴趣的:(《C专家编程》)