C专家编程

一、C:穿越时空的迷雾

二、这不是Bug,而是语言特性

switch中是以case开始,case之前即使有代码也不会执行,default可放在任意位置,也需要break不然也会fall down。case中的标签对C来说不能是const定义的,如const int two=2,而对于C++这是可以的。。。。(亲测)。 以前字符串超过一行时可用\连接,现在可直接对每行字符串加双引号则会自动连接起来,这样除最后一行会自动加'\0'外其余行末尾不会加,坏处是如果对字符型指针数组中间忘了加逗号会被连成一个。函数变量前加static则只会在本地可见(Include也不会见到)。sizeof、是类型时必须加括号,是变量时可不加如sizeof p。优先级:p.f中.高于*,int *ap[],int fp()中[],()高于,==,=!高于位操作如(val & mask!=0)会先算!=,==,=!高于赋值,算术运算符高于移位msb<<4+lsb会先算加,逗号运算符最低,如i=1,2结果为1,逗号运算会的值为最右边的操作数的值,但这里赋值的优先级更高因此是(i=1),2,即i先赋值1,接着执行常量2的运算计算结果丢弃,赋值运算是右结合的,而位操作是左结合的。在函数调用时各参数的计算顺序是不确定的。Linux下删除以-开头的文件加命名删除或加--表忽略后面的参数。输出long double用%Ld,大写的L。K&R C采用无符号保留,当一个无符号与int或更小的整型混合使用时,结果类型是无符号型,ANSI C标准采用值保留原则,结果有可能是有符号数也有可能是无符号数,取决于操作数的类型是相对大小,如-1<(unsigned char)1对K&R C中-1会变成很大的数,而对ANSI C原来的-1还是-1则会成立,而对于int a[4]; int d=-1,对(d

三、分析C语言的声明

const int中const在左侧时指针所指的对象是只读的但指针可指向其他对象,而int* const是指针所指的位置固定但其所指的值(p的值)可改。下面声明是合法的:int(fun())(),int(foo())[],int(foo[])其中第一个是函数的返回值允许是一个函数指针,第二个是函数的返回值允许是一个指向数组的指针,第三个是数据里面允许有函数指针。有些C语言说“在调用函数时,参数按照从右到左的次序压到堆栈里”这种说法过于简单,参数在传递时首先尽可能地存放到寄存器中(追求速度),对基本数据类型一般会被传递到寄存器中而结构参数则很可能被传递到堆栈中(此说法未验证正确性)。const,volatile关键字后面紧跟类型说明符如int,long等那么它作用于类型说明符,在其他情况下,它作用于它左边紧邻的指针星号。 void (signal() (int)分析void (signal())(int)可看出signal是一个函数,返回一个函数指针,指向一个参数int返回void的函数,signal的参数void(func)(int)也是一个函数指针,可typedef void(ptr_to_func)(int),然后可ptr_to_func(int, ptr_to_func); 对typedef int ptr, (fun)(), arr[5];分别是指向int型指针ptr(可ptr a=&b;)、函数指针、长度为5的int型数组(可arr a;a[3]=8;)。typedef看成是一种彻底的“封装”类型,在声明它之后不能再往里面增加别的东西,它和宏的区别体现在两个方面,可以用其他类型说明符对宏类型名进行扩展但对typedef所定义的类型名却不能这样,如#define peach int可unsigned peach i;但typedef int banana; unsigned banana i;这样就不行;其次在连续几个变量的声明中,用typedef定义的类型能保证声明中所有变量均为同一种类型而define不能保证,如#define int_ptr int然后int_ptr a,b;中a是int,而b是int,而typedef两个都是int*。

四、令人震惊的事实,数组和指针并不相同

以文件1中定义int mango[100];文件2中声明extern int* mango;为例,对应声明也是数组的情况,由于内存分配是在定义处而非声明处因此并不需要长度信息,只需extern int mango[]即可(对多维数组需要提供除最左边一维之外其他维的长度),对赋值语句,可修改左值表示左值允许出现在赋值语句的左边,区别与数组名,数组名也用于确定对象在内存中的位置,也是左值(左值是地址,右值是内容),但它不能作为赋值的对象,因此数组名是个不可修改的左传。编译器为每个变量分配一个地址(左值),这个地址在编译时可知,相反存储于变量中的值(它的右值)只有在运行时才可知,如果需要用到变量中存储的值,编译器就发出指令从指定地址读入变量值并将它存于寄存器中。对于数组编译时就可知道它的地址(声明时用extern int mango[]就是这种情况),而指针必须首先在运行时取得它的当前值然后才能对它进行解引用操作(作为以后进行查找的步骤之一),因此对于extern int mango;它将告诉编译器mango是一个指针,指向一个int对象,为取得这个int值必须得到地址p的内容,把它作为int对象地址并从此地址中取出int值,指针访问灵活但要增加一次额外的提取。反之若定义int mango[],然后声明extern int mango;也会出现问题,本来定义的是指针,应该从指针指向地址取值,结果声明了数组,少了间接取值的那一步。 定义指针时并不为指针所指向对象分配空间,它只分配指针本身空间除非定义同时赋值给指针一个字符串常量进行初始化如char *p="break";但只对字符串常量才如此,并不是浮点数之类的常量也分配空间,因此float *pip=3.141;就错误。拿破仑回文:Able was i, ere i saw Elba.另一个A man, a plan, a canal--panama!

第五章 对链接的思考

即使在静态链接中,整个.a文件也并没有全部被载入到可执行文件中,所装入的只是所需要的函数。动态链接是一种“just-in-time(JIT)”链接,意味着程序在运行时必须能找到它们所需要的函数库,链接器通过把库文件名或路径名植入可执行文件中来做到这一点,这意味着函数库的路径不能随意移动。静态库被称作archive通过ar来创建和更新。 Interpositioning是通过编写与库函数同名的函数来取代该库函数的行为(此时任何调用此系统函数的系统调用也都被更改),用此不会错但不要用,不要让程序中的任何符号成为全局的,除非有意把它们作为程序的接口之一。

第六章 运动的诗意:运行时数据结构

默认的输出a.out其实是assembler output即汇编程序输出(其实是链接器的输出,名字与早期语言不存在链接器有关)。在Intel x86的内存模型中,地址空间并非一个整体,而分成一些64K大小的区域,称为段。在Linux中也有段的概念,在一个可执行中运行size命令时,会显示3个段(文本段、数据段和bss段)的大小,BSS是Block Started By Symbol(由符号开始的块),它保存的是没有值的变量所以它并不需要保存这些变量的映像,运行时所需要的BSS段的大小记录在目标文件中,但BSS段(不像其他段)并不占据目标文件的任何空间,假如生成a.out,在函数(如main函数)里定义的局部变量是不会进入a.out的,而是在运行时创建的。a.out以段的形式组织:段可以方便地映射到链接器在运行时可以直接载入到对象中,载入器只是取文件中每个段的映像,并直接将它们放入内存中。
setjmp(jmp_buf j);表示使用j记录现在的位置,返回0,longjmp(jmp_buf j, int i);表回到j所记录的位置,返回i使代码知道它是实际上通过longjmp返回的,当使用longjmp时j的内容被销毁。goto语句不能跳出C语言当前的函数(这也是longjmp取名的由来),用longjmp只能跳回到曾经到过的地方,setjmp有两个作用,保存调用函数的栈环境返回0,作为longjmp的返回目标地,返回longjmp的第二个参数。保证局部变量在longjmp过程中一直保持它的值的唯一可靠方法是把它声明为volatile。

七、对内存的思考

在Microsoft C中far表示指针存储了段寄存器的内容和偏移地址,near表只存储了16位的偏移地址,它的段使用当前数据段或堆栈段寄存器中的值。磁盘制造商都是使用十进制而不是二进制来表示磁盘容量。calloc与malloc类似但它返回指针之前先把分配好的内存的内容都清空为零。alloca()分配的内存当离开调用alloca的函数时它所分配的内存就会被自动释放,它分配在栈上,不具可移植性, 而且在没有传统堆栈机器上很难实现。可通过top -p PID查看当前进程内存,可也cat proc/PID/status。总线错误几乎都是由于未对齐的读或写引起的,之所以称为总线错误是因为出现未对齐的内存访问请求时被堵塞的组件就是地址总线。对于union{char a[10];int i;}u;然后int p=(int)&(u.a[1]);最后p=17;中可能导致总线错误,因为数组和int的联合确保数组a是按照int的4字节对齐的,所以a+1是未按int对齐的,这种对char强转int*可能会出现这种错误。段错误是由于内在管理单元的异常所致,而该异常则通常是由于解除引用一个未初始化或非法值的指针所引起的,如int *p=0; *p=0;会引起段错误,如果未初始化的指针恰好具有未对齐的值它将会产生总线错误,而不是段错误,因为CPU先看到的地址然后再把它发送给MMU(Memory Management Unit)。对for(p=start;p;p=p->next){free(p);}这样的链表,第一次free(p);后p就不可用了,会出现段错误。对goto的标签部分代码,会直接被执行(并不是只占个位置,真正goto时才执行)。

八、为叙程序员无法分清万圣节和圣诞节

Oct 31(10月31,八进制)是Dec 25(12月25,十进制),printf("%ld\n", sizeof 'a');时会进行类型转换输出4,枚举类型可被隐式提升为int。函数指针数组:extern int a(), b(), c(), d();int (*state[])()={a, b, c, d};

九、再论数组

像加法一样,取下标操作符的操作数是可以交换的,如a[10]中可a[6]也要6[a]使用。把形参的数组和指针等同起来是出于效率原因的考虑(只给出地址即可)。数组不能赋值给另一个数组,因为数组作为一个整体不能成为赋值的对象。对多维数组初始化最右边可加逗号也可不加,如int a[][2]={{1}, {2, 3},}大小为224=16个字节。

十、再论指针

当传递是实参是数组所匹配的形参类型如下,当传递char c[][]匹配char ()[],传递char c[]匹配char ,传递char ()[]匹配不变。对于多维数组的传递要提供除第一维外所有大小都匹配的参数,有了这些信息就可一次“跳过”一个完整的行,对char a[][]转为char (a)[]也是这个道理,对指针加加操作也是跳过一行,否则无法编译。无法直接从函数返回一个数组但可让函数返回一个指向任何数据结构的指针,如int (paf())[20]返回一个指向有20个int元素数组的指针,使用时可result=paf();(result)[3]=12;。

十一、你懂得C,所以C++不在话下

附录

库函数调用和系统调用的区别何在:函数库调用是语言或应用程序的一部分,而系统调用是操作系统的一部分,系统调用是在操作系统内核发现一个“trap”或中断后进行的,说详细如下:函数库调用与用户程序相联系,在用户地址空间执行,运行时间属于“用户”时间,属于过程调用开销较小,在C函数库libc中约有300个程序,如system,fprintf,malloc,而系统调用是操作系统的一个进入点,在内核地址空间执行,它的运行时间属于“系统”时间,需要在切换到内核上下文环境然后切换回来开销较大,在UNIX中约有90个系统调用,如chdir,fork,write,brk。 文件描述符并不是ANSC C的一部分不会存在于非UNIX环境,可移植性有问题,尽量用文件指针即fopen,fclose等指向FILE结构的指针。 判断一个变量有有符号还是无符号:https://blog.csdn.net/summer00072/article/details/80903398 打印一棵二叉树的值的时间复杂度是O(N),因为要逐个访问,而不是查找的O(log2N),对execve函数执行成功时没有返回值,执行失败时的返回值为-1,虽然定义时int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

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