C专家编程笔记

时间:2024-03-06 14:17:57

chapter 1 ——“C诡异离奇,缺陷重重,缺获得了巨大的成功”
只写可移植的代码:1、只使用已经确定的特性;2、不突破任何由编译器实现的限制;3、不产生任何依赖由编译器定义的或者未确定的或未定义的特性的输出;
例如定义了int a[10]; 即使在某个编译器上会默认赋初值,但是可移植的代码会希望我加上memset(a,0,sizeof(0))
作者建议始终加上必要的类型转换以及返回值等等。

ANSI C的重要特性是有了原型,原型是函数生命的扩展,不仅函数名和返回类型已知,所有的形参类型也是已知的。这就允许编译器在参数的使用和声明之间检查一致性。

question: 实参 char* s 与 形参 const char *p 相容, 为何实参char **s 与 const char **p 不相容呢?
我一度以为自己搞明白了,今天再看,原来还是浆糊……

先来看看const:const是一个限定符
符号之前加上const只是表示这个符号不能被赋值,对于这个符号来讲,这个值是只读的。但它并不能防止程序以其它方法修改这个值,例如指针等。const的最有用的地方时限定函数的形参,const int &可以避免实参的数据在内部被改变。

例如 const int * p = 10;
const int * p is “a poniter point to a constant-integer.  ” 意思就是说 p指向的值,不能通过这个指针p修改。但是,p指向的地址可以进行改变,因此解引用以后会得到不同的值。从“但是”往后的内容可知,我们其实是可以通过获得p的地址,然后修改该常量的,我还真去试了……

int a= 10 , b = 20;
const int * p = &a;

a = 15;
//*p = 16;  //error, assignment of read-only location

int addr = (int)p;   //type convert is necessary
*(int*)addr = 16;
cout << *(int*)addr << endl;  //16

p = &b; //accepatable
//p = 25; //.......
cout << *p << endl; //20
return 0;

后面还讲到const其实还真不是一个常量。证据?用const定义一个整形,放case语句里,你看编译器跟你说啥话来着?


那么再回来看:
char *cp;
const char * ccp;
ccp = cp;
可行吗? 可行…… 对指针来说,只是ccp指向的内容只读,ccp的内容(指针位置本身)还是可以变的。(const修饰的是指针指向的类型,而不是指针本身)

然后再来看下面的代码:

int a= 10 , b = 20;

const int *p = &a;
const int *r = &b;
const int **q = &p;
//**q = b  //error
*q = r;   //this work, things remain constant is only the **q, q and *q can be assigned to another value
cout << **q <<endl;  // 20

所以,char* 和 const char* 指向的都是char,类型是相同的,区别只是有没有常量限定符而已。而char**和const char** 指向的 一个是char* 一个是const char*,类型是不同的,因此传递实参的时候就不能直接传递啦。

书里还说了这么一句,赋值形式合法,必须满足以下条件之一:
两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符。

注意相容性是不能传递的。

一个ANSI C与 K&R C的标准问题
K&R使用的C是无符号保留的,当一个无符号类型与int混合使用时,结果类型是无符号类型。
ANSI C使用的方式是值保留,当把几个整形操作数混合使用时,结果有可能是无符号,有可能有符号,取决于操作数的类型的相对大小。
弄明白也没意思,不用unsigned就行了。不得不用的时候,结果也加上强制类型转换就能避免出问题了。

Chapter 2 --“BUG是迄今为止地球上最庞大最成功的实体类型,有近百万种(何止)已知的品种……”
C语言中有太多的含糊之处或近似含糊。

第一个我不知道的事情:NUL用于结束ASCII字符串(\'\0\')。 (想想,NULL表示什么都不指向……)

第二个,又是我不敢肯定的事情,break究竟能break到哪里?答案是最近的循环语句或者switch语句。现在想想setjmp和longjmp还是有道理的……

第三个,缺省可见性。
C里面,函数缺省全局可见。extern其实是无效的,只是为了可读,函数对于链接它所在的目标文件的任何东西都是可见的。static的功用是函数在本文件以外不可见。作者还说,当用户编写的函数与库函数重名的时候,我们就能看到别的东西了……
当然extern用于变量是指变量在其它地方被定义,static用于函数内部表示变量的值在各个调用之间一直保持延续性……

第四个,奇奇怪怪的操作符重载
apple = sizeof(int) * p;
究竟是apple =  (sizeof(int)) * p? 还是apple = sizeof((int)*p)?
猜猜看……把p定义为int*的时候DEV-C++编译不过,定义成int就过了,看来前面靠谱……

第五个,优先级问题。
关于运算符的东西还是比较好解决的,加括号就行了。例如, “msb << 4 + lsb” equals “msb << (4 + lsb)”,BOSS不会因为你的代码长怀疑你的水平的~同时P40的小启发里面的问题也是比较有意思 ,加括号也不是万能的,唯一方法就是不要养成依赖于计算先后次序的代码逻辑。
比较让人感兴趣的是这几个:
.的优先级高于*:*p.f == *(p.f)      *p.f != (*p).f 所以-> 这玩意出来了
[]高于*: int *ap[] == int*(ap[])   int *ap[] != int (*ap)[]
()高于*:int *fp() 这是一个函数,返回的是int* // int * ( fp())
而不是函数指针//  int (*fp)()
总结就是,*的优先级蛮低的。

看到这里一个想法:一件事情没有在开头就做好,修复的成本就是在滚一个以全世界用户数为基数的雪球。

第六个,用fgets()代替gets(),缓冲区溢出,没啥好讲的了。

第七个,ANSI C采取最大匹配策略 z=y+++x 被解析成 z= y++ + x 而不是 z= y+ ++x,括号解决一切……

Chapter 3 C语言的声明
多年来,程序员、学生和教师们都在努力寻找一种更好的记忆方法和法则来搞清楚恐怖的C语言声明语法。
声明包括:
至少得一个类型说明符:void char short int long signed unsigned float double
可能包括存储类型:extern static register
类型限定符:const volatile

有且只有一个声明器(declarator) :指针 直接声明器(identifier / identifier[num] / identifier(parameter) / (identifier)) 初始化内容( = init-value)等

更多的声明器: , 声明器  如int a,b ,b就是“更多的声明器”

一个分号;
//  new content
结构、联合、枚举的小秘密
在结构体中,可以通过设置位值大小节省空间。当然Union也可以节省空间,但是它的存在意义在于两个成员不可能同时存在(而不是压缩体积)。enum在 缺省情况下从零开始,为后面的标识符赋值,每个加1,但一旦对某个标识符进行了赋值,后面的标识符的值就会比前面的大1。
enum sizes { small, medium = 3, large};  //0,3,4

优先级规则
i feel so lucky to see it from this book.
理解C语言声明的优先级规则:
RULE A:取最左边的标识符,然后按照优先级顺序依次读取。
RULE B:优先级从高到低排序:
B1 声明中被括号括起来的部分
B2 后缀操作符:包括()表示函数、[]表示数组
B3 前缀操作符:大名鼎鼎的 *
RULE C:如果const 或 volatile关键字后面紧跟类型说明符(int long等),那么它作用于类型说明符,其他情况下const和volatile作用于它左边紧邻的指针星号(这翻译得怪怪的)。

演练:char * const *(*next)()

A  :char * const *(*next)()    next是一个……  (去掉next)
B1:char * const *(*      )() (已经去掉next) 指向……的指针(去掉(* ))
B2:char * const *          ()   返回……的函数
B3:char * const *                指向……的指针
c  :char * const                   只读的
c  :char *                            ……的指针
a  :char                              字符


//B3: *(*next)() -> next是一个指向函数的指针,该函数返回一个指针
//C: const 修饰 (char * ) 所以 *(*next)() 指向的函数返回的指针指向一个类型为char的常量指针。

合起来就是,声明了next,它是一个函数指针,该函数返回一个指针,这个指针指向一个只读的指向字符的指针。

再来一个 char *(* c[10])(int **p)
1、c[10] :一个有10个元素的……数组                                   余下 char *(*)(int **p)
2、(*)(int **p) :一个指向形参为2级整型指针的函数指针         余下 char *
3、char* 函数返回char*类型。
// 1 2省略了一点 不过还是比较容易看出来的
声明了一个有10个元素的数组,元素的类型是指向形参为2级整形指针的函数指针,这个函数返回类型是指向字符的指针。

在数组中北函数指针所指向的所有函数,都把一个2级指针作为它们的唯一参数。为什么?

typedef:设计的初衷是用来为数据类型创建别名。在结构的定义中使用结构标签是好的,即 struct fruit { };使用 typedef struct fruit{} FRUIT 只能让我们少写一个struct而已。

Chapter 4 数组和指针并不相同
首先来看看左值和右值的区别:
X = Y 这里X的含义是X代表的地址。 Y的含义是Y代表的地址的内容;左值在编译的时候就可以确定,表示存储结果的地方,右值只有运行的时候才知道。但是C引入了可以变化的左值。

什么时候不同?来看
char *p = "abcdefg"; char a[] = "abcdefg"; 取p[3]与a[3]都能得到d,但是方式很不一样。
p[3]是根据编译器的符号表,存着符号p的地址,获得这个地址(addr1)(p本身在addr1这个位置放着),再去取p的内容(就是“abcdefg”的存放位置addr2),然后再加上addr2+偏移量i得出p[i]
a[3]会根据编译器的符号表,得到数组的开始位置,然后再加上偏移量得出a[i]

所以假如出现: char a[9]; extern char *a; 那么取a[i]的过程中,实质上就出现了上面两种情况的组合。

step 1 取出符号表中a的位置,提取存储在这里的指针。
step 2 把下标所表示的偏移量与指针的值相加,产生一个地址。
step 3 访问地址,获取字符
step 4 获得了这个ASCII字符以后,会把它解释为地址

这里还是看书吧 说的不是特别清楚P84-P85:

Chapter 5  对链接的思考

如何编译:通过-shared -fPIC 生成lib*.so

如何链接:通过-L 定义so的存放位置 -R定义运行时位置 -l定义库名

如何检测是否成功链接:ldd命令

假如ldd表示not found:可以试试export LD_LIBRARY_PATH=so存放位置

如何找到应该链接哪个库:nm命令可以看到这个库包含了哪些函数,请用linux命令组合

编译的时候有次序吗:静态链接有顺序——若需要解析所需的符号,必须先让文件包含未解析的引用。因此建议始终将-l函数库放在编译命令的最右边

防止interpositioning的危害:尽量不要定义全局的符号(例如通过static使函数在文件外不可见)。ld中使用-m参数可以产生被interposition的参数列表


Chapter 6

a.out是assembly output的缩写被跳过了

在a.out中,BSS段仅记录未初始化全局变量和静态变量的所需空间。程序在加载的时候在内存中开辟相应的空间,因此bss也被理解为better space saved。


auto变量是函数中默认的变量类型

每个线程有自己的堆栈

 

 

 

 

 

 

Chapter 7  对内存的思考
被跳过了

Chapter 8 万圣节与圣诞节
类型变化与提升
表达式中char /bit /enum /unsigned char/ short /  unsigned short / 都会被提升成int
float会被提升成 double

参数传递也会导致类型提升。ANSIC中,如果使用了恰当的函数原型,就不会发生,否则也会发生,在被调用函数的内部,提升后的参数会被裁减为原先声明的 大小。而函数内部,被提升的参数会被裁减为原先声明的大小。这就是printf("%d")的参数能接char int short的原因

而函数原型就是为了避免发生缺省的参数提升

后面还有一些比较艰深晦涩的……也被我毅然决然的跳过了,实在没有机会遇到

Chapter 9  再论数组 续chapter4
什么时候数组和指针是一致的?让我们从声明和使用两种情况开始我们的讨论吧。
声明本身可以被分为3种情况:
1 数组的外部声明(external array)    2 数组的定义(定义是特殊的(开辟空间)的声明) 3 函数参数声明。

所有作为函数参数的数组名总是可以通过编译器转换为指针。在其它情况下,数组的声明就是数组,指针的声明就是指针,不能混淆。但在使用数组的时候,数组总是可以写成指针的形式,两者可以互换。也就是说,对于下面的代码,去掉前两行注释中的任意一行,都能够得到正确的答案15

//int sum(int s[])
//int sum( int *s)
{
int res = 0;
for ( int i = 0 ; i < 5 ; i++)
res +=s[i];
return res;
}

int main()
{
int a[5] = {1,2,3,4,5};
cout << sum(a) <<endl;  //15/
return 0;
}

为何产生混淆:程序员会发现 char array[10] 与 char * ptr都可以用在strlen( )里面,甚至
char b[] = "Hector";
printf("array at location 0X%x hold string %s", b,b); 也是有效的。
// output: "array at location 0X22ff10 hold string Hector"

什么情况下,数组和指针是相同的?
标准给的答案:
1、表达式中的数组名(不是声明)被编译器当做一个指向该数组的第一个元素的指针。
2、下标总是与指针的偏移量相同。
3、函数参数的声明中,数组名被编译器当做指向该数组的第一个元素的指针。

一些钻牛角尖的例外:
数组作为sizeof()操作数时,显然返回的是数组的大小,而不是第一个元素的大小。不过请注意了,sizeof()是一个运算符,不是函数。
使用&操作符取数组的地址时。
数组是字符串常量时。

在编译时,对数组的引用a[i]总是被编译成*(a+i)。所以,在表达式中,指针和数组是可以互换的,因为他们编译以后的形式都是指针;所以就出现了这个让我毛骨悚然的例子。
int main()
{
int a[10];
a[6] = 1;
cout << a[6] << endl; //1
6[a] = 2;                   // 6[a] 即 *(6+a) == *(a+6)
cout << a[6] << endl; //2
return 0;
}

为什么C语言把数组形参当做指针?
理解了这个问题,相信前面的知识就可以更好地被记忆了。这是出于效率的原因,C中,所有的非数组形式数据的数据实参均以值形式传输,假如要拷贝一个数组,时空上的开销都可能很大。绝大部分的情况下,其实并不需要整个数组的拷贝。(当然数据也可以传址调用,只要在它前面加上取地址操作符(&)就可以了。

try this if you wanna know something relavant.
void fun(int *ca)
{
cout << &ca << endl << ca << endl << &ca[0] << endl << &ca[1] << endl;
}

void fun(char *pa)
{
printf("%d\t%d\t%d\t%d\t", &pa ,&(pa[0]),&(pa[1]) ,++pa);
}
这里++pa对应的输出和&pa[0]是一样的,为什么呢?,我猜是因为printf从右到左处理……

数组片段的下标a[-1],完全为标准所不容……

数组的数组:
int c[3][4][5]; 定义了一个多维数组,只能采用c[i][j][k]的形式去访问,或者通过计算i,j,k距离首元素的距离用指针访问。c[i,j,k]返回的是c[k](逗号表达式……)。

int a[2][3][5];
int (*r)[5] = a[0];
int *t = a[0][0];
printf ("0x%x 0x%x", r , t);
printf ("0x%x 0x%x", ++r , ++t);
如果觉得多维数组已然了然于心了,测试一下这段代码,看看和你预期的一样不?

第十章和第十一章也被我华丽地跳过了……
第十章的东西比较浅显,十一章讲C++,非我所欲也~

附录:
* 如何测试一个数是无符号数?
有符号数的特征是对最左边一个位取补将会改变它的符号。
#define ISUNSIGNED(a) ( a >= 0 && ~a >=0)
you know what i mean
* 打印一棵二叉树的值的时间复杂度是多少?(二叉树中所有操作的时间复杂度都是O(logn)
所有结点,显然是n


最后我只能说本书里面的八卦的精彩程度实在超出了我的想象,“除了把加号写成减号导致卫星爆炸以外,这个程序是完美的”、大型可乐机、回文“a man, a plan, a canal -- panama!"、“把C的矩阵传递给Fortran程序,该矩阵就会被自动转置——这是一个非常厉害的邪门密技”、用于“软、硬件平衡的幽灵手指”,以 及“如何用气压计测量建筑物的高度”。程序员的幽默真的是让人心旷神怡。毫无疑问,这本书可以给十分……