C专家编程 精编之一 第一章~第三章
C的复杂之处 在于它的指针 ,但是比其指针更为复杂的是它的声明 !!!
你能看懂它们的意思 吗?
apple=sizeof(int)*p ; apple=sizeof * p;
j= (char (*)[20])malloc(20);
int const * grape; 与 int * const grape; 的区别
typedef void (*ptr_to_func)(int);
void (*signal(int sig,void (*func)(int )))(int );
几个样例:
一:char *a; const char *b; a=b;//出现警告. why?
二: const int two =2;
switch(i)
{
case 1:printf("case 1 ! /n");
case two :printf("case 2/n");
}
编译出错,说明了.....?
三:switch(){..}中把 default改成 defau1t (无心之过,或其它标签如defavlt,dafault..)都编译通过 . why?
四: apple=sizeof(int)*p ; apple=sizeof * p; //是什么意思? 另外, y=sizeof x; 能编译通过吗?
五: j= (char (*)[20])malloc(20); //怎么样?
六: result=*x/*y ; //出错?why ?
z=y+++x; 即为: z=y++ +x; 但z=y+ + +x; z=y+ ++x; 呢?
七: const int * grape;
int const * grape;
int * const grape; //有什么区别?
八: int (* fun())()
int (* fun())[]
int (* funp[])() //各是什么意思?
九: void (*signal(int sig,void (*func)(int )))(int ); //什么意思??
十: typedef void (*ptr_to_func)(int); //这个用法,你会吗?
十一:struct foo{int foo;}foo; // 合法! 那么,sizeof(foo)又表示哪一个foo呢?
.........
以上几个例子是这篇文章的主要内容,是我根据 C专家编程 前三章 的主要内容整理的.
如果对上面的问题很清楚,那么就没有必要看下去,
如果对C一点都不了解,也没有必要看下去.
如果对C非常感兴趣,同样也没有必要看下去,直接看书吧...
问题一:实参char * s 与形参 const char *p是相容的,但是为什么 实参 char * *argv 与形参 const char **p实际上不能相容?
例:
foo(const char **p){ }
main(int argc,char **argv)
{
foo(argv);
}
//产生warning:argument is incompatible with prototype 即 警告:参数与原型不匹配
问题产生: 实参 char *s 与 形参const char *p应该是相容的,那么为什么 实参 char * *argv 与形参 const char **p实际上不能相容?
答案途径: ANSI C 标准.
6.3.2.2: 每个实参都应该具有自己的类型.这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符). 也就是说参数传递过程类似于赋值.
//即:const char **类型的对象可以赋值给一个类型为 char **的值,否则..... //why?
赋值规则: 6.3.16.1:
要使上面的赋值合法,必须满足下列条件:
两个操作数都是指向有限定符或无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向类型的全部限定符.
//即指向的类型相容且左有右全部限定符.
例:char *a;
const char *b;
若b=a; //可行;
若a=b; //不可行.警告.
6.1.2.5说明:char float * 类型并不是一个有限定符的类型------它的类型是"指向一个具有const限定符的char类型的指针",也就是说const限定符是修饰指针所指向的类型,而不是指针本身. //参见:问题const
因此,由于char **和const char **都是没有限定行的指针类型,但它们所指向的类型不一样(前者指向char * ,后者指向 const char *).故,它们是不相容的.
下面更亲切的解释:
左操作数: p-------->Foo (char *)------------>const char ;
右操作数: argv---->Baz (char *)------------>char ;
若: p=argv;
则:Foo和Baz所指向的类型是相容的,而且它们本身都没有限定符,所以符合标准的约束条件,两之间赋值合法.
但是,Foo和Baz并不相容.故 p=argv不合法. //p19~p20页,再看几遍!!!
问题2:关于char ,int ,long int ,float ,double,long double等计算时的转换.
注意:unsigned类型!
尽量不要使用unsigned类型,以免造成边界问题!!! -1<1000 返回 0
问题3:
#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))
而不是:
#define TOTAL_ELEMENTs (sizeof(array)/sizeof(array(int))
原因:第一个可移植性强.
问题4:NUL 与 NULL
NUL:用于结束一个ASC字符串;
NULL:用于指向空指针.
问题5:关于switch
switch的一个问题是它内部的任何语句都可以加上标签,并在执行时跳转到那里.
并且所有的标签都是可选的,如:若把default打成 defau1t 它能顺利通过编译,不显示错误.
另外,在C中,const并不真正表示常量.如:
const int two =2;
switch(i)
{
case 1:printf("case 1 ! /n");
case two:printf("case 2/n");
}
编译出错,这并不是switch的过错,但是它却展示 了const其实并不是真正的常量. //记住,const 是值不可变的变量
问题6:C中相邻字符串编译时自动合并.
如:printf("I and"
"you");
相当于 printf("I andyou");及printf("I and""you");
问题7:太多的缺省可见性:
定义C函数时,在缺省情况下,函数的名字是全局可见的,可以在前面加上冗余的extern,也可不加,效果一样.
function apple(){/*在任何地方均可见*/}
extern function apple(){/*在任何地方均可见*/}
static function apple(){/*在这个文件之外不可见*/}
所以:尽量用static !!!
注:关于interpositioning ,还要学习很多东西!
问题8:C中的误做之过. 太多的符号重载!!!!
static 在函数内部 ,表示变量的值在各个调用之间,一一直保持延续性.
在函数这一级,表示该函数只对本文件可见.
extern 函数,表示全局(冗余)
变量,表示在其他的地方定义.
void 函数返回类型,
指针声明中,表示通用指针的类型.
位于参数列表中,表示没有参数.
* 乘,
指针:间接引用
声明中,表指针.
& AND ,取址
**
......
() 函数定义中,包围参数表
调用函数
改变运算次序
强制转换.
定义带参数的宏.
包围sizeof操作符的操作数.(注:如果它是 类型名 !!!!)
一个例子:
p=N*sizeof*q;
r=malloc(p); //明白了吧!!
sizeof的操作数是一个类型名时,必须要有括号,但是如果是变量,则不必加.
apple=sizeof(int)*p ;
//什么意思? 由优先级,结合性决定
//另y=sizeof x; y=sizeof(x);y=sizeof (int)都可以,但是y=sizeof int;不行;
优先级及结合性
::::::::::::::::::::::::::::::::::::::::::::::::::
1 () [] . -> 自左至右
2 ! ~ ++ -- - (表示负号), (类型), * (指针) ,&,sizeof 自右至左!!!
3 ,4 数学运算符 先*/%后+,-
5 移位
6 关系 大小(等)
7 关系 == !=
8,9,10 位运算 与,异或,或 &,^,|
11,12 逻辑运算 && ||
13 条件 ? : 自右至左!!!
14 赋值 自右至左!!!
15 逗号
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
问题9: 有些运算符的优先级是错误的!
更正:
.高于 * : *p.f 表示 *(p.f) //即对p取f取偏移,作为指针,然后进行解除引用操作
//解决方案 p->f
[]高于* : int *ap[] //表示指针数组int * (fp[]),不是指向数组的指针. //int (*fp)[]
()高于 * : int*fp() //函数返回值是个指针.
//注:(),[], . ,->的优先级最高!!!
==和!=高于位操作,也高于赋值.
逗号最低 i=1,2 相当于 (i=1),2
问题10:什么是结合性?
结合性是仲裁者,在几个操作符具有相同的优先级时,决定先执行哪一个.....
问题11:早期gets()中的Bug导致了internet蠕虫.
gets()并不检查缓冲区的空间!!!!!
利用多写的东西,覆盖缓冲区之外的空间,甚至高手可改变函数的返回地址!!!!
缓冲区溢出攻击??
解决方案:
fgets() //已经限定了长度.
问题12:C中的空格问题!!!!
最大解析方案:
z=y+++x; 即为: z=y++ +x; 但? z=y+ ++x; 呢?
z=y+++++x; 呢? z=y++ ++ +x;还是 z=y++ + ++x; ? 都错!!!
result=*x/*y ; //出错?why ? 被当作注释!!!
a//*
//*/b;
在C中表示a/b,在C++中表示a
问题13:lint程序!!!非常有用!!
垃圾回收!非常重要!最好在一个代码块中!
注意:函数返回值不要为一个指向局部变量的地址,或者动态分配又释放的地址!
问题14: 分析C语言的声明:
C语言哲学:要求对象的声明形式与它的使用形式尽可能的相仿.
一个int类型的指针数组被声明为int*p[3]; 并以 *p[i]这样的表达式引用或者使用指针所指向的int数据.所以,它的声明形式和使用形式非常相似.
这样的好处是:各种不同操作符的优先级在"声明"和"使用"时是一样的.
缺点很明显, 优先级(15级或者更多)是C中另一个设计不当,过于复杂之处.
声明形式和使用形式相似好像是C的独创,但是把两个没有关系,截然不同的东西做成一个样子又有什么重要意义?
时至今日,在C++中采纳的是:
int &p ; //表示参数的会址调用.
C的声明存在的最大问题是你无法以 一种人们所习惯的自然方式从左向右阅读一个声明.在ANSI C 引入 volatile 和 const
关键字之后,情况更糟糕了,它们只能在声明中,而不能在使用中,从而声明形式和使用形式能完全对上号的越来越少了.
而那些从风格上看像是声明,但却没有标识符的东西(如形式参数声明和强制类型转换)看上去显得滑稽.
如果想要把什么东西的类型强制转换为指向数组的指针,就不得不使用下面的语句来表示这个强制类型转换:
char (*j)[20] ; /*j表示一个指向数组的指针,数组内有20个char元素*/
j= (char (*)[20])malloc(20); //怎么样? 够晕了吧!!!
如果把星号两边看上去明显多余的括号拿掉,代码会变成非法的.
问题15:关于 涉及指针和const的声明
注:const常变量类型,我理解为一次赋值(包括函数参数传递[或曰传值]),终身不变.
然,const 是作用于它指向的对象上.
几种不同的顺序:
const int * grape;
int const * grape; //前两种,指针所指的对象是只读的.
//const作用于星号上,即所指的对象上.
int * const grape; //指针变量是只读的,const作用于变量上.
要使对象和指针都是只读的,下面两种方法都能做到这一点:
const int *const grape;
int const * const grap;
//技巧:const优先作用于左(右可能也可以)边的int, long等,如果没有,则作用于左边星号!!! //const 先类型,后左星 !
问题16:既然连"指向数组的指针",这样概念清晰的语法,它的声明形式也是如此晦涩(问题14),那么对于更复杂的语法形式又将如何.例如下面的声明(取自telnet远程登录程序):
char * const *(next)(); //这就是C!!!! 明白 ?? 后续
问题17:声明是如何形成的?
首先看看一些C的术语以及一些能组合成一个声明的单独语法成份:声明器!!
声明器是所有声明的核心!!!
简单地说,声明器就是标识符以及它组合在一起的任何指针,函数括号,数组下标等.如下:
-------------------------------------------------------------------------
数量 C中名字 出现的形式
---------------------------------------------------------------------------
0个或多个 指针 下列之一:
*const
*volatile
*
*const
*volatile const
-----------------------------------------------------------------------------
有且仅有一个 直接声明 标识符
或: 标识符[下标]
或: 标识符(参数)
或: (声明器)
-----------------------------------------------------------------------------
一个声明器由上表任意组合组成!但是合法的声明中存在限制条件,以下错误:
函数的返回值不能是一个函数: foo()()非法
数组: foo()[]非法
数组里面不能有函数 : foo[]()非法
对这些形式还很迷糊??
下面则合法:
int (* fun())() :函数的返回值允许是一个函数指针.
int (* fun())[] :指向数组的指针.
int (* funp[])() :数组里允许有函数指针.
int foo[][] :数组时允许有其它数组.
//理解: int (*p)() 为函数指针变量,故 int (*fun[])()为函数指针数组.
//对 int (*fun())[]等,从优先级出发,理解更容易,如(*fun())结合之后,其类似 int foo[] ,它是一个数组,当然这个foo是由* fun()返回的:即函数的返回值是一个指向数组的指针.
问题18:关于struct , union,enum.
struct: 结构可嵌套,也可有指向自己类型的指针.
同时,struct中也可有位段(我觉得易于对位操作):
//位段必int ,signed int,unsigned int 三者之一.
unsigned int a:1;
unsigned int : 2; //填充,
在用struct时,最好把 变量类型的声明 与 变量 的声明分开!
union:实际上很少使用,因为它的优点其实并不怎么出色,实际上struct出现的几率是union的一百倍.
问题19:优先级规 :理解一个声明,必须 要懂得其中的优先级规则.
下面是语言律师的定义:
A 声明从它的名字开始读取,然后按照优先级顺序依次读取.
B 优先级从高到低依次是:
B.1 声明中被括号括起来的那部分.
B.2 后缀操作符.后缀()表示一个函数,[]表示一个数组.
B.3 前缀操作符: * 表示指向.....的指针
C 如果 const 和(或)volatile 的后面紧跟类型说明符(int ,long ..),那么,它作用于
类型说明符, 但是一般情况下,它们作用于左边紧邻的指针星号.
例,再说: char * const *(*next)();
A,B.1,B,B2,B3,C
next A,next是一个指针B.1,它指向一个函数B.2,该函数返回一个指针B.3,它指向一个 指向字符的常量指针.
但是下面更易懂:
------------------------------------------------------------------
步骤 怎么阅读
---------------------------------------------------------------------------------
1. 取最左边的标识符 表示"标识符是"
____________________________________________________________
2.如果右边下一个符号是一个方括号 对于每一对,表示"...的数组"
__________________________________________________________
3.如果是一个左括号表示 至右括号为止的内容" 返回...的函数"
___________________________________________________________________
4.如果左边的符号是一个左括号 这个左括号把已经处理的部分组合在一
起直到遇见对应右括号,回到第2步!!!
___________________________________________________________________
5.如果左边的符号是下列三个之一: 继续向左,直到不是左边那三个之一:
const const:"只读"
volatile "volatile "
* * :"指向...的指针"
然后重复第四步!!
________________________________________________________________
6.剩下的符号形成基本类型 剩下的可一起阅读,如: static unsigned
-------------------------------------------------------------------
注:C语言中声明读起来并没有固定方向,一会从左到右,一会又从右读到左.
开始,我们从左到右,直到找到第一个标识符.
然后,当某个符号与上表中匹配时,便把它从声明中处理掉,以后不再考虑.
在具体的每一步上,我们首先查看右边的符号,然后看左边.
当所有处理完时,大功告成.
//技巧:先找标识符,然后 每一步 先右后左,最后处理基本类型.
//注意上表中两个跳转
例: 再说 char * const *(*next)() ;
步骤: 1, "next是..."
(2,3,4), 都不匹配
5, *,表示 "指向...的指针"
4, "("和")"匹配, 转到 2.
//"("和")"把已经处理部分组合起来,转到2
2, 不匹配
3, 表示"返回...的函数"
4, 不匹配
5, 表示"指向..的指针"
5, 表示"只读"
5, 表示"指向...的指针"
6. 表示 "char "
//读完后,把已读的扔掉...
拼在一起: next是一个指向函数的指针,该函数返回另一个指针,该指针指针指向一个只读的char的指针.
下面是一个更复杂的例子: char *(*c[10])(int **p) ;
c是一个指针数组,它的元素类型是函数指针,其所指向的函数返回值是一个指向char的指针.其中,int **p为参数.
问题20 关于 typedef 提示用法: [typedef 基类型 复杂形式];
如:typedef int Count; typedef int Array[10] ;
typedef void (*func)(int);
//这个应该明白吧?
// 可以这样用: func f1,f2;
// 与 声明 void (*f1)(int); void (*f2)(int); 效果一样.
它是一种有用并且有趣的声明形式:它为一种类型引入新的名字,而不是为变量分配空间.
一般情况下,typedef 用于简洁地表示指向其他东西的指针.例如,中断函数 signal(),它用于处理中断,其声明为:
void (*signal(int sig,void (*func)(int )))(int );
用上面的技巧分析,可看出它的意思为: void (*signal( ))(int );
signal是一个函数(具有一些令人胆战惊心的参数),它返回一个函数指针,后者所指向的函数接受一个int参数并返回void .
其中一个恐怖的参数是其本身:
void (*func)(int );它是一函数指针,指向的函数参数为int ,该函数返回类型为void 型.
下面用typedef来对其进行简化:
typedef void (*ptr_to_func)(int);
//此时相当于: ptr_to_func 为 void (*)(int)
// 它表示ptr_to_func是一个函数指针,该函数接受一个int参数,返回值为void.
ptr_to_func signal(int,ptr_to_func);
//它表示signal是一个函数,它接收两个参数,一个是int,另一个是ptr_to_func
问题21 typedef的缺点: 混乱!!!!
它具有与其它声明一样混乱的语法,可以把声明器塞到一个声明中! //缺点之一
typedef 声明不必放在开始位置中! //缺点之二
可在一个typedef 放入几个声明器,如下: //缺点之三
typedef int *ptr,(fun)(),arry[5] ;
/* ptr是"指向int的指针"类型,
* fun是"返回值为int的函数的指针"类型,
* arry是"长度为5的int 型数组"类型
*/
千万不要把typedef嵌入到声明的中间部分://缺点之四
unsigned const long typedef int volatile * kumquat; //??这是什么意思?
注: typedef 仅仅是类型创建别名,而不是创建新的数据类型,可以对任何类型进行 typedef声明.
typedef int (*array_ptr)[100]; //注:为数组指针类型(与指针数组区分开).
问题22 typede int x[10]与#define x int[10]之间的区别:
#define 完全是替换.
typedef 是为已有类型产生一个别名,代表一个完整的类型.
//注:现在好像已经没有必要再去用#define ,可以直接使用 const.
问题23 typedef的用武之地究竟应该在哪里?
C中存在多种名字空间:
::标签名(label name).
::标签(tag):用于所有的结构,联合,枚举.
::成员名:每个结构或者联合都有自己的名字空间.
::其它.
在同一个名字空间里,任何名字必须有唯一性,但是不同空间中,则可相同.
//产生混乱!!!!
一: struct foo{int foo;}foo; 合法!!!
那么,sizeof(foo)又表示哪一个foo呢?
//应该是第三个吧. 第一个可以这样:sizeof(struct foo);
第二个这样:sizeof(foo);
//注: typedef struct foo1{int foo2;}foo3;
foo1:表示结构标签;
foo3:结构类型;
foo2:成员名;
二:这个更古怪 :
typedef struct baz{ int baz;}baz;
struct baz var1;
baz var2;
太多baz了.
提示:不要为了方便对结构使用 typedef ,省去struct 会减少信息的质量.
typedef 应该用在:
数组.结构.指针以及函数的组合类型中.
可移植类型. typedef short Short; typedef int Long;
为强制类型转换提供一个简单的名字,如:
typedef int (*ptr_to_int_fun)(void);
char *p;
............
=(ptr_to_int_fun)p;
始终记住,在结构的定义中使用结构标签,即使并非必须. 目标:清晰,易懂.
问题24: 理解所有分析过程的代码段
编写一个能够分析C语言声明并把它们翻译成通俗语言的程序.
简化: 假定输入合法,假定函数的括号内没有参数列表.
思路:利用堆栈,从左向右读取,把各个标记依次压入堆栈,直到讲到标识符为止.
继续向右读入一个标记,接着观察标识符左边那个标记. P72页.
未完待续