Chinaunix首页 | 论坛 | 博客
  • 博客访问: 54323
  • 博文数量: 11
  • 博客积分: 310
  • 博客等级: 一等列兵
  • 技术积分: 100
  • 用 户 组: 普通用户
  • 注册时间: 2009-06-21 16:26
文章分类

全部博文(11)

文章存档

2014年(3)

2011年(1)

2010年(7)

我的朋友

分类: C/C++

2010-10-30 10:06:24

                                  第一章

1.3 词法分析中的贪心法

  所谓贪心法,俗称大嘴法,能多吃就多吃。譬如 a---b ;词法分析器将解释为 a-- - b ;为什么呢?因为--是有意义的符号的最大嘴,如果再多吃一口-,那---就变成无意义的了。需要注意的是,除去字符串与字符常量,符号的中间不能有空白(即空格、制表、换行符)。 例如char ch =' ',是可以的。

  再看一个例子:

  y = x/*p; 依据贪心法,/*被当作一个多字符符号,你的原意是 y = x / *p;现在/*却变成了左注释符。为了避免这些不经意的错误,你最好保持一种良好的编程风格,即y = x / *p 或者 y = x / (*p);

  你肯定不嫌弃再有一个例子:

  int a = 185;

  int b = 15;

  a=-b;

  printf("%d\n",a);

 结果是185-15么 ? 老的编译器可能这么处理,根据贪心法把=-看成一个多字符符号,即看成 a = a-b;可是ANSIC标准却把-号作为b的一个组成成分,结果是-15 。ANSIC要表示a = a - b; 用a -= b;

 书中还给出这样的一个例子 a >> = 1; ANSIC看成是语法错误,如果要表达这种含义的话,a >>= 1 才是正确的方式,而老版编译器却把复合赋值处理成两个符号,譬如 a >> = 1;是正确的,(a >>= 1也是正确的。)

  a+++++b能被现在的编译器通过么? 不能!

  a+++++b 按照贪心法被解释为a ++ ++ + b 即被看成((a++)++) + b,虽然a可以作为左值,但a++产生的结果确是一个常数,不能作为左值。(a++被编译器解释为先应用,即产生的结果作为一个应用值,然后a这个变量再自增),所以会出现编译错误。上述式子唯一合理的解释是(在我们看来),a ++ + ++ b,即 (a++) + (++b),我们可以通过灵活地使用括号做到这一点。

1.4 整型常量

  010被看成八进制数,所以010和10是不同的。另外许多c编译器竟然把8和9也作为八进制处理。(八进制有哪些? 0,1,2...7)不过幸好ANSIC禁止这种用法。

1.5 字符与字符串

   单引号引起的一个字符实际是代表一个整数,整数值对应相应ASCII码中的整数值。用双引号引起的字符串,代表的是指向字符串中第一个字符的地址。

  ‘yes'对于ANSIC标准而言,依次用后一个字符覆盖前一个字符,最后得到的整数值就是最后一个字符的整数值。而borland C++和lcc 等编译器只取第一个字符,忽略其余。

练习:

 某些c编译器允许嵌套注释,请编写一个测试程序,要求:无论是对允许嵌套注释的编译器,还是对不允许的,该程序都能正常过过编译(无错误消息出现),但是这两种情况下程序执行的结果却不一样。

                                     第二章

2.1 理解函数声明

 (*(void(*)())0)() == typedef void (*fun)(); (*(fun)0)();

也可这样声明:

void (*fun)(); //fun是一个指向void类型的函数的指针变量。

fun = (void (*)())0; //把0强制转换为一个指向void类型的函数的指针(地址),转换后0是指针[常量](地址),fun是指针变量。

(*fun)(); //这种写法的代价是多了一个“哑”变量fun。

  任何c变量的声明都由两部分组成:类型以及一组类似表达式的声明符。(注意与表达式含义是不同的,是类似)

  可以声明普通变量,如float f;也可以声明数组 int a[3];还可以声明函数 float *fun();还可以声明指针类型float *f; 还可以声明函数指针,float (*fun)();注意()结合优先级高于*。

  所谓类型转换符,只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号封装起来。(我们可以在声明符中任意使用括号,例如float ((f)); ((f))的类型是浮点类型,(f),f的类型也是。)

  一旦我们知道了如何声明一个给定类型的变量,那么该类型(不是变量)的类型转换符就可以知道了,如float a;那么float类型的类型转换符是(float),又如float (*h)();表示h是一个指向返回值为float类型的函数的指针变量。因此float (*)()就是一个“指向返回值为float类型的函数的指针“的类型转换符。类型转换符主要用于强制类型转换。

  float(*h)();//类型声明。

(float (*)()) h;//强制类型转换,把常量或已经声明过的变量强制类型转换为对应类型的变量和常量。如(float(*)())0;就是把常量0转换为一个地址,这个地址是返回值为float,参数为void的函数的地址。(注意,我这里有意区分指针变量(我统称为指针)和指针的内容(即地址),仅是为了避免歧义。读者可以根据语义自动辨别。)

 

  我们来分析一下signal函数的原型:

 void (*signal(int,void(*)(int)))(int);

  首先简化为void (*signal(something))(int);其中something为参数类型列表。我们可以看到signal(something)这个函数的返回值是一个函数指针,(这个函数返回值)是一个指向返回值为void类型的函数的指针。

也可通过typedef来更清晰地说明上述的声明:

typedef void (*fun)(int);

fun signal(int,fun); //其中fun为指向信号处理函数的指针。

2.3 注意作为语句结束标志的分号

if (x[i] > big);

   big = x[i];

实际上相当于:

if (x[i] > big) {}

  big = x[i];

if (n < 3)

   return

logrec.date = x[0];

logrec.time = x[1];

logrec.code = x[2];

实际上相当于:

if (n < 3)

    return logrc.date = x[0];

logrec.time = x[1];

logrec.code = x[2];

如果这段代码所在的函数声明其返回值为void,编译器会因为实际返回值的类型与声明的不一致而报错。

还有一种情况就是,当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被忽略,编译器可能把声明的类型视作函数的返回值类型。

struct logrec {

   int date;

   int time;

   int code;

}

fun() //函数缺省返回值类型为int,而现在编译器把这个函数的返回值类型看成struct logrec 结构体类型。

{

  int a = 2;

  int b = 3;

  return (a+b);

}

2.4 switch语句

switch (color) {

case 1: printf("red");

case 2:printf("yellow");

case 3:printf("bule“);

}

如果color值为2,那么程序结果是yellowblue

c语言的switch这个特性,因为break容易让程序员忽略而造成程序错误,但优秀的程序员则可以让这个break发挥极大的优势。

case SUBTRACT:

     opnd2 = -opnd2;

     /* 此处没有break语句*/

case ADD:

     ...//相应加法操作

再看一个例子,有一段代码,它的作用是一个编译器在查找符号时跳过空白字符(即那三种字符)

case '\n':

      linecount++;

      /* 此处没有break语句*/

case '\t':

case ' ' :

case 'A' :

      ...

2.5 函数调用

f(); //函数调用

f;   //这个语句计算函数f的地址,但并不调用该函数。

2.6 悬挂else引发的问题

  if (x == 0)

     if (y == 0) error();

  else {

     z = x + y;

     f(&z);

  }

实际上相当于:

 if (x == 0) {

     if ( y== 0) error();

     else {

       z = x + y;

    f(&z);

     }

}

如果要体现编程者本意的话,应该这样写:

if (x == 0) {

    if ( y == 0) {

       error();

    }

} else {

    z = x + y;

    f(&z);

}

有些程序设计语言在if语句中使用收尾定界符来显式地说明

if  x = 0  //algol 68语言

then if y = 0

     then error

     fi

else z:=x + y; //;是algol 68 语言中语句之间的分割符

     f(z)   

fi

用宏定义实现类似的效果

#define IF   if(

#define THEN ){

#define ELSE }else{

#define FI   }

IF x == 0

THEN IF y == 0

     THEN error();

     FI

ELSE  z = x + y;

      f(&z);

FI

                              第三章

3.1 指针和数组

   c语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,c语言中数组的元素可以是任何类型的对象,譬如说结构体数组,当然也可以是另外一个数组,因此”仿真“一个多维数组不是难事。

   关于数组名和指针的区别,左值与右值的区别,我会有专门文章描述。只需要知道数组名作为数组对象的整体意义时是不可修改的左值,但数组名不是c意义上的常量(c意义上的常量不能取地址符),也不一定是c意义上的常量表达式(c意义上的常量表达式的值要能在编译时就能确定,全局或静态数组名的值是常量表达式,但局部数组名不是常量表达式)。但是如果我们在应该出现指针的地方,却采用了数组名来替换,那么数组名就会蜕化(decay)成指向该数组下标的0的元素的符号地址(此时完成了作数组名到地址的转换,也完成了从左值到右值的转换)(注意数组名从左值到右值的转换在c89/c90这样规定:C89/90中,规定左值数组才能进行数组到指针的转换,并且&的操作数要求是左值。但c99修改了这个规定,右值数组也可以进行这样转换)。(详细的介绍请到《再再论指针》修订版中去寻找答案。)

   在 《c expert programming》中对此有比较详细的介绍。

   在一般情况下,数组名都会蜕化成指向该数组下标的0的元素的地址,不具有整体意义,但是在下列情况下,它却具有整体意义:(具有整体意义的时候是一个不可修改的左值)

   . 声明时

   . 数组作为sizeof操作数的时候。

   . 使用&操作符取数组的时候。

   . 数组是一个字符串常量初始值,如char a[] = "hello",字符串字面值初始化数组必须一气呵成,不能分成两个语句。也就是说,字符串字面值只能对数组进行声明初始化,不能用字符串字面值对数组赋值。(因为数组名如果这样使用,char a[]; a = "hello";此时a脱离了我们所说的“数组是一个字符串常量初始值“的环境,此时a是一个指向该数组下标的0的元素的地址,不具有整体意义,数组名退化后就是一个地址,因为变成了右值.(下面是c99对左值和右值,以及数组名和指针做的说明,非常具有权威性)

 .在c++中,当使用typeid(array).name()时也具有整体意义。

   An lvalue is an expression with an object type or an incomplete type other than void; if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const-qualified type.

   Except when it is the operand of the sizeof operator or the unary(一元) & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue.

   关于c中的初始化和赋值,需要注意的是c中的初始化是编译期间进行的,而赋值是在运行期间进行的。作为扩展,全局变量,静态变量在编译期分配内存(即确定变量的逻辑地址),局部变量在运行期分配内存(即确定变量的逻辑地址)。

   另外关于c中的常量,标准这样解释:A constant expression can be evaluated (求值)during translation rather than runtime, and accordingly may be used in any place that a constant may be.因为字符串字符值不是c意义上的常量,是不可修改的左值,因为编译期常量在c中只有四种(整数常量、浮点常量、枚举常量和字符常量)。但c++将字符串字符值称为字符串常量。

  字符串字面量放在rodata区,rodata区和.text区一起构成text segment。有的国内书籍认为字符串字面值放在静态存储区,属于数据段,其实这是不准确的,究竟放在数据段还是代码段,也就是把只读区划在哪里是取决于编译器的(String literals could be stored in the data segment or code segment. It's up to the compiler.),而大部分编译器包括gcc,vs 2005都将字符串字面值放在代码段,例如在 GCC中:

Code:

char writablestring[]="string"; /* This goes in the data seg and is writable */
char *nonwritablestring="string"; /* This goes before the function in the code seg and is not writable, writing to it will cause a seg fault*/

    GCC used to accept the -fwritable-strings option to specify that all strings should be writable, as of 4.0, this is no longer maintained.

 

   继续我们的讨论,看一下下面几条声明和语句:

   int *p,*q;

    int (*ip)[];

    int a[3];

    p = a;// right,数组名经过了从右值到左值的转换

    q = &a;// 非法的,gcc中会产生警告:从不兼容的指针类型赋值。因为此时a碰到&后具有整体意义,&a 是一个指向数组的指针,尽管在值上面p == q,但含义和类型是不同的。

    ip = &a; //okay


    现在开始考虑二维数组,它实际上是以数组为元素的数组,我们可以完全依据指针编写操纵二维数组的程序,但放着用二维数组不用,而用指针来操作二维数组,的确有些不可思议。

  

    下面有几条声明:

    int calendar[12][31];

    int *p;

    int i;

    calendar[4]的含义是什么呢?

    我们知道,calendar是有着12个数组类型元素的数组,calendar[4]就是它的第5个元素,又因calendar[4]是一个有31个元素的一维数组的数组名,但在表达式中转换成了指向这个一维数组首元素的地址

,所以calendar[4]在表达式中就是数组名为calendar[4]的一维数组首元素的地址。 即在表示式中 p =  alendar[4]. 相当于 p = & (*(alendar[4]+0)) = &alendar[4][0]。

    那么p = calendar ;legal?

    当然是非法的,因为calendar在表达式中被转换成了一个指向它第一个数组元素的地址。如果我们声明

    int (*q)[31];

    q = calendar;//okay ,legal,我声明了一个q这样的指针变量,它指向拥有31个元素的一维数组。那么*q就是一个拥有31个元素的一维数组的数组名。(如果*q用于表达式,它依然被转换成一个指向这个数组首元素的地址)

   那么我在上面两条语句后加上: p = *q; 根据上面的解释,这是正确的。

   再看一个例子:

  int calendar[12][31];

  calendar[month][day] = 0;

  上面的语句可以表示为*(*(calendar+month)+day) = 0;

  那么如何理解呢?

  calendar在表达式中已经从二维数组(更准确地说明是一维数组的数组)的“整体意义”转换成了指向它12个数组类型元素的第一个数组类型元素的地址。calendar + month 则是指向他12个数组类型元素中的第(month+1)个数组类型元素的地址,*(calendar+month)从整体意义上是12个数组类型元素中的第(month+1)个数组类型元素的内容,也就是第(month+1)个数组类型元素的具有整体意义的数组名,但它在表达式中转换成了这个具有31个int类型元素的数组的首元素的地址,故*(calendar+month)+day是这个具有31个int类型元素的数组的第(day+1)个元素的地址,故*(*(calendar+month)+day)则是这个具有31个int类型元素的数组的第(day+1)个元素的内容。说到这里,读者的脑海里是不是有一副清晰的图示了呢?)


3.2非数组指针
  char *r;
  strcpy(r,s);
  strcat(r,t);
这是不行的,原因是不能确定r指向何处。我们还应该看到,不仅要让r指向一个地址,而且r所指向的地址处还要有内存空间可供容纳字符串。
 如果这样呢?
 char r[100];
 strcpy(r,s);
 strcat(r,t);
 不幸的是,c语言强制要求我们必须声明数组大小为一个常量,因此我们不能确保r足够大。
 那么这样呢?
/* 头文件 #include */
 char *r;
 r = (char *)malloc(strlen(s)+strlen(t));
 strcpy(r,s);
 strcat(r,t);
这个例子仍然有三个隐患,或者说错误:
首先, malloc可能无法提供请求的内存,你要有判断语句。
其次,没有释放语句,会造成内存泄露。
再次,r = (char*)malloc(strlen(s) + strlen(t) + 1);注意是加1,而不是加2。因为strcat执行时把t字符串的第一个字符替换了r中的那个s带来的'\0',
所以比较完整的结果是:
 /* 头文件 #include */
 char *r;
 r = (char *)malloc(strlen(s)+strlen(t));
 if (!r) {
     printf("can't allocate enough memory\n");
     exit(1);
 }
 strcpy(r,s);
 strcat(r,t);
 ....
 free(r);

3.3 作为参数的数组声明
     char hello[] = "hello";
     printf("%s\n",hello);
 与
   char hello[] = "hello";
   printf("%s\n",&hello[0]);
完全等效。
   将数组名作为函数实参,数组名转换成数组第一个元素的地址。 将数组名作为函数形参,数组名转换为指向数组第一个元素的指针。   
   下面有这样一个例子:
  文件1:
   char hello[]= "hello";
 文件2:
   extern char *hello;
   引用hello[i]的代码;
这样是不对的。具体见《expert c programming》第四章。
 
3.4 避免举隅法
 
  举隅比较通俗的理解是以局部代替整体。书中还有其他理解,不必细究。
  char *p,*q;
  p = "xyz";//是把字符串字面值的第一个元素的地址传给p指针。
  q = p; //赋值的是地址,而非整个字符串字面值。
  如果我们执行下面一条语句:
  q[1] = 'Y';//不同的编译器有不同的处理,ANSIC 标准禁止对string literal 作出修改。如gcc会产生段错误!而某些编译器还允许q[1] = 'Y',如LCC v3.6。但我们不提倡!

3.5 空指针并非空字符串
  在c中,空指针这样定义:
  #define NULL (void*)0;
  空串为"",sizeof("") == 1;
  空指针可以确保不指向任何对象或函数 ,绝对不能企图使用该指针所指向的内存中存储的内容,编译器保证由0转换而来的指针不等于任何有效的指针。



                                   第四章
    lint程序:一种更加严格的编译器。它不仅可以象普通编译器那样检查出一般的语法错误,还可以检查出那些虽然完全合乎语法要求, 但很可能是潜在的、不易发现的错误。 lint工具是一种软件质量保证工具,许多国外的大型专业软件公司,如微软公司,都把它作为程序检查工具,在程序合入正式版本或交付测试之前一定要保证通过了LINT检查 ,他们要求软件工程师在使用LINT时要打开所有的编译开关,如果一定要关闭某些开关, 那么要给出关闭这些开关的正当理由 。
   编译器或汇编器把各个源文件编译成若干个目标模块,连接器把这些目标模块整合成一个被称为载入模块或可执行文件的实体,该实体被操作系统内核的加载器加载到内存然后被cpu调去执行。
   连接器通常把目标模块看成是由一组外部对象(包括函数和外部变量)组成的。每个外部对象代表机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有static声明,则默认是外部对象。某些c编译器会对static函数和static变量的名称做一定改变,将它们也作为外部对象。(这样,虽然说他们是外部变量,但经过名字修饰,不同文件中就可以有相同名字的变量,也符合static类型的internal linked 要求。)(参看pointer on c第3.6节链接属性)
  大部分连接器都禁止同一载入模块中的两个不同外部对象拥有相同的名称,然而在多个目标模块整合成一个载入模块时,这些目标模块可能就包含了同名的外部对象,连接器一个重要工作就是处理这类命名冲突。
  例如一个目标模块中包括了对静态链接库的某个函数的引用,如printf,该引用指向了这个库的外部对象。在连接器生成载入模块时,也必须记录这些外部对象的引用。如果在这个目标模块中定义一个同名的printf函数,连接器就会报错。
 





阅读(1700) | 评论(0) | 转发(0) |
0

上一篇:函数调用约定总结

下一篇:左值和右值

给主人留下些什么吧!~~