C陷阱与缺陷寻找


一:词法错误


1. == 与= 的不同


2. 逻辑运算符与按位运算符
&& &
|| |
! ~




3.词法分析中的 贪心法:


编译器将程序分解为符号的方法:
从左到右扫描,每一个符号应该包含尽可能多的字符。
如:a---b;等价于 (a--)-b;
如:y=x/*p;本意是y除以指针p所指的 内容
实际上:编译器将/*认为是注释的标识。




4.整形常量.
整形常量的第一个字符是数字0,那么该数为八进制数。
如:010 标识的8.




5.字符与字符串


1)字符串:


(0)用双引号引起来。


(1)用双引号引起来的字符串,代表的是包含串中所有字符以及
串的结束标识'\0'的数组的首地址。
(即:字符串是strlen+1个字节的内存的首地址。)


如:printf("hello\n");
等效于
char hello[]={'h','e','l','l','o','\n',0};
printf(hello);


(2)存储字符串:
因为字符串是包含所有串,以及串的结束标识'\0'的数组首地址。
所以存储字符串,开辟的空间为:
strlen()+1;


2)字符:


(1)用单引号引起来。


(2)用单引号引起来的字符代表的是一个整数 。不同于字符串为
一个指针。
所以:printf('\n');是错误的。










--------------------------------


二:语法错误


1.运算符优先级
 1):单目运算符


 (),[],->,.
 ! , ~ , ++ , -- , - , (type) , * , & , sizeof


 2):算术运算符


 *, /, %, +,-


 3):移位运算符


 << ,>>


 4):关系运算符


 >,<,==,!=


 5):按位运算符


 & , | , ~ ,^


 6):逻辑运算符


 && , || , !
 7):条件运算符


  ?:
 8):赋值运算符
  =
 9):逗号运算符
,


2.求值顺序 && , || , ?: ,  ,


1) &&, || (逻辑与,逻辑或)


&& , || 首先对左侧操作数进行求值,只有在需要时才对右侧
操作数求值。


如:a<b&&c<d;
当a<b为假时,c<d不会执行 判断。
如:10||f();
则f()不会被执行。


2) 条件运算符:()?():();


如:(a)?(b):(c);
操作数a 先被求值,然后根据a的值,来选择执行b还是c.


3) 逗号运算符


先对左侧操作数求值,然后该值被丢弃,再对右侧操作数求值。


逗号运算符作用:在一个语句中,集成几个语句。


注意:函数参数中的分隔符逗号不是逗号运算符,两个参数的求值顺序
是不定的。


3.分支语句:switch-case-break ,if-else


1)switch-case-break结构


2)if-else 的匹配


else总是与邻近的上一个未匹配的if进行匹配。


4.按位运算符&,|,~,^


1).&
(1)清零
一个数与0按位与,则该数清零。


(2)特定为置0.
哪些位需要置0,则& 这些位为0,其余为1的数。


(3)保留特定位(取特定位)
保留特定位,只需要所取的位置1.其余位为0.
如:b=a&3;
取a的低两位。




2).|
特定位置1
哪些位需要置1,则| 上 这些位为1的数。
3).~
二进制数整个取反。
4).异或^
(异或的特点,交换律,以及两个变量的交换)


(1)异或的定义:


两个值'相同为假,不同为真'


(2)异或的3个特点:


   (1) 0^0=0,0^1=1  0异或任何数=任何数
   (2) 1^0=1,1^1=0  1异或任何数=任何数取反
   (3)  任何数异或自己=把自己置0


(3)异或的运算法则:
1.交换律
2.结合律


1. a ^ b = b ^ a
    2. a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c


    3. d = a ^ b ^ c 可以推出 a = d ^ b ^ c
    (两边同时^b^c)


    4. a ^ b ^ a = b


    (4) 作用:


    1. 特定位取反。


    特定位需要取反,则这些位取1,其余位为0,然后异或。


    2. 两个整数的交换,不用中间变量。
    (不适用于小数,以及字符串)


    a = a^b;   
         b = b^a;   //b=b^(a^b)=b^b^a=a
         a = a^b;   //


3.在汇编语言中经常用于将变量置零.
    xor   a,a


    4.判断两个值是否相等.
    return ((a ^ b) == 0)


    5.加密解密


    那加密的过程就是逐个字符跟那个secret字符异或运算. 
解密的过程就是密文再跟同一个字符异或运算 。


下面这个程序用到了“按位异或”运算符:


classE 
{ public static void main(String args[ ]) 

  char  a1='十' ,  a2='点' ,  a3='进' ,  a4='攻' ; 
  char secret='8' ; 
  a1=(char) (a1^secret); 
  a2=(char) (a2^secret); 
  a3=(char) (a3^secret); 
  a4=(char) (a4^secret); 
  System.out.println("密文:"+a1+a2+a3+a4); 
  a1=(char) (a1^secret); 
  a2=(char) (a2^secret); 
  a3=(char) (a3^secret); 
  a4=(char) (a4^secret); 
  System.out.println("原文:"+a1+a2+a3+a4); 

}


5.移位运算符<<,>>:

1). 移位 (<<,>>)与 *,/, %(乘,除,模)


a*(2^n); a<<n
a/(2^n); a>>n
a%(2^n);  a-(a>>n)<<n 或者 a-a&(低n位为0,其他位为1)




范例:
int i, j;
i = 879 / 16;
j = 562 % 32;
等价于:
int i, j;
i = 879 >> 4;
j = 562 - (562 >> 5 << 5);
j = 562 - (562 & ~0x1F);


2).负数的除法  与  负数右移的区别


在数字没有溢出的前提下,对于正数和负数,
左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。


负数的除法,与负数的右移区别:


a=-3;
       b1=a/2;
       b2=a>>1;
      printf("-3/2=%d\n",b1);
      printf("-3>>1=%d\n",b2);


      结果:-3/2=-1
           -3>>1=-2


           一个余数为正,一个余数为负,具体与编译器有关。




3)当要移位的位数大于被操作数对应数据类型所能表示的最大位数.


先将要求移位数对该类型所能表示的最大位数求余后,再将被操作数移位所得余数对应的数值,效果不变。
比如100>>35=100>>(35%32)=100>>3=12
1<<35=1<<(35%32)=1<<3=8


4)移位的类型转换


byte、short、char,int 在做移位运算之前,会被自动转换为int类型,然后再进行移位运算, 
结果都为int型。


long经过移位运算后结果为long型。 




6. 负数除法运算的截断  (负数求摸,余数可能为负)


假设a除以b,商为q,余数为r;
q=a/b;
r=a%b;
(假设b>0)


条件一:
q*b+r=a;


条件二:
改变a的符号,希望改变q的符号,但是q的绝对值不变。


条件三:


当b>0时,希望保证0<r<b.
如: 如果余数用于hash table的索引,或者数组的下标。


上诉三个条件不会同时成立;


如:3/2 ,商为1, 余数为1
(-3)/2, 如果商为-1,则余数为-1(不满足条件三)
如果上尉-2,则余数为1.(不满足条件二)


由于三个条件不可能同时成立,大多数程序语言,要求余数和被除数
的符号相同,即放弃了条件三。满足条件一和条件二。




如:有一个数n,它代表标示符中的某个字符经过某种函数运算后的
结果,我们希望通过除法运算得到hash table的条目
h,满足0<=h<HASHSIZE.


所以:h=n%HASHSIZE;
    if(h<0)
    h+=HASHSIZE;


更好的 做法是:在程序设计时,就应该避免n为负数的情况,
将n声明为无符号数。






------------------------------------------
       三:语义错误


  1. sizeof()函数的使用.


  1) 原型:(size_t) sizeof(参数);
  在C中,typedef unsigned int size_t;


  注意:
  sizeof是单目操作符,不是函数,
  编译期间执行,所以不能得到动态分配的内存空间的大小。


  2) sizeof的使用方法:


  1.用于基本数据类型
  sizeof( type_name ); // sizeof( 类型 );


  基本数据类型short ,int ,long,float,double ,
  它们都是和系统相关的,所以在不同的系统下占的字节数
  可能不同。
  在32位编译环境中,sizeof(int)的取值为4。


  2.用于变量、常量


  参数时变量或常量,转换为相应的数据类型,计算大小。


  sizeof( object ); // sizeof( 对象 );


  如: sizeof(8)    = 4;  //自动转化为int类型
          sizeof(8.8)  = 8;  //自动转化为double类型,注意,不是float类型


          sizeof("ab") = 3   //自动转化为数组类型,


  3.用于指针


  当操作数是指针时,sizeof依赖于编译器。
  32位系统,则地址是32的,所以sizeof(指针)=4;
  如下:
  char* pc = "abc";
int* pi;
string* ps;
char** ppc = &pc;
void (*pf)(); // 函数指针
sizeof( pc ); // 结果为4
sizeof( pi ); // 结果为4
sizeof( ps ); // 结果为4
sizeof( ppc ); // 结果为4
sizeof( pf ); // 结果为4


  如: 
  char s*="abcde";
  sizeof(s)=4;
  但是sizeof("abcded")=6;


  解释:


  字符串是包含了所有字符以及串的结束标识'\0'
  的数组的首地址。
  此中char s*="abcde";
  实际上是,开辟了一个数组的内存空间,其中存储
  adcde以及'\0'
  然后又开辟了一个内存空间,来存储变量s.
  所以:sizeof(s)=4;
  sizeof("abcde")=6;


  4.用于数组


  当操作数具有数组类型时,其结果是数组的总字节数。


  例如: char a[5];
          int  b[5];
          char c[]="abc"
          sizeof(a) = 5;
          sizeof(b) = 20;
          sizeof(c)=4;


          (1)应用:使用sizeof求数组元素的个数
          如: int b[5];
              char c[]="abcd";
               numOfB=sizeof(b)/sizeof(b[0]);
               numOfc=sizeof(c)/sizeof(c[0]);




  5.sizeof()用于字符串与strlen区分。


  char s*="abcde";


  sizeof(s)=4;
  sizeof("abcde")=6;
  strlen(s)=5;


  char s[]="abcde";
 
  sizeof(s)=6;
  strlen(s)=5;


  sizeof()用于字符串 自动转换为 sizeof 存储该字符串的数组。


  6.用于函数中的数组形参。


  数组形参等价于相应指针。


  void foo3(char a3[3])
{
   int c3 = sizeof( a3 ); // c3 == 4
}
void foo4(char a4[])
{
   int c4 = sizeof( a4 ); // c4 == 4
}


解释:
调用函数foo1时,程序会在栈上分配一个大小为3的数组吗?不会!数组是“传址”的,调用者只需将实参的地址传递过去,所以a3自然为指针类型(char*),c3的值也就为4。




  7.用于结构体:


  1)原则:


    (1) 整体空间是 最宽基本数据成员(数组成员,和结构体成员 ,需要展开)所占字节数的整数倍。


    此中的最宽基本数据成员,当有数组成员以及其他结构体
    成员时,则需要将数组一个一个展开,以及将其他结构体成员展开,
    来判断最宽基本数据类型的成员。


  (2) 数据对齐原则----内存按结构成员的先后顺序排列,当排到该成员变量时,其前面已摆放的空间大小必须是该成员类型大小的整倍数,如果不够则补齐。


  原因:编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。


  (3) 数组按照单个变量一个一个的摆放,而不是看成整体。如果成员中有自定义的类、结构体,也是将结构体成员中的成员一个一个展开,而不是将结构体成员看为一个整体。


  2) 范例一:


  struct s1
{
char a;
double b;
int c;
char d; 
};


struct s2
{
char a;
char b;
int c;
double d;
};


cout<<sizeof(s1)<<endl; // 24
cout<<sizeof(s2)<<endl; // 16


解析: int     4
 char    1
 double  8


对于s1:char a,占用了一个字节即1,下一个为double b;
前面已经分配的内存必须是8的整数倍,所以前面占用的内存为1-8,double b 占用8
个字节,是从9-16,int c ,需要是4 的倍数,16是4的倍数,
所以int c 分配的是17-20. 然后下一个是 char d,
占用 21.由于结构体必须是最大成员 的整数倍。
所以是24.


3) 范例二:


结构体所占的空间为最大




struct s1
{
char a[8];
};


struct s2
{
double d;
};


struct s3
{
s1 s;
char a;
};


struct s4
{
s2 s;
char a; 
};
cout<<sizeof(s1)<<endl; // 8
cout<<sizeof(s2)<<endl; // 8
cout<<sizeof(s3)<<endl; // 9
cout<<sizeof(s4)<<endl; // 16;
 s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。




4) 结构体中成员变量的排放顺序:


将数组成员,以及其他结构体成员展开,然后较窄的成员变量排在前面。
较宽的数据成员排在后面。
 


  3) sizeof()结果:


  sizeof()的结果为size_t,即unsigned int ;


  4) sizeof()的作用:


  1、主要用途是与存储分配和I/O系统那样的例程进行通信。
 
    例如: void *malloc(size_t size);


        size_t fread(void *ptr, size_t size,            size_t nmemb, FILE * stream);    


     2、另一个的主要用途是计算数组中元素的个数。
 
    例如: void *memset(void *s, int c, size_t n);


    如:memset() 的作用是将一个以及开辟内存空间的数组s,
    的前 n 个字节的值 赋值为 c.


    如:一般数组 的初始化:
    void *memset(void *s, int c,sizeof(s));


    6. How many bytes will be occupied for the variable (definition: int **a[3][4])? 
A. 64 B.12 C.48 D.128 


答案为C


       
  5) sizeof()与strlen()比较:


  1.区别一:


  strlen()是一个函数,程序运行时执行。计算字符数组的字符数,以"\0"为结束判断,不计算为'\0'的数组元素。
  strlen(char * s)的参数是字符型以及指针。


      而sizeof计算数据(包括数组、变量、类型、结构体等)所占内存空间,是单目运算符。由于sizeof()在编译期间就执行了,所以不能用sizeof()来得到动态分配的内存空间的大小。


       2.区别二:


       数组做sizeof的参数不退化,传递给strlen就退化为指针了。


       如:char str[20]="0123456789"; 
int a=strlen(str); //a=10; 
int b=sizeof(str); //而b=20; 








  2. getchar()函数的使用:


  1)   getchar()函数原型:


  int getchar(void);


  返回值为 int 类型。即用户输入的 字符的ASCII 码,如果出错则
  返回-1 即EOF .






  2)  用法:


  getchar()首先从缓冲区中得到一个字符,


  如果缓存区中没有字符,则从键盘上输入,回车或者EOF(Ctrl+d)表示输入结束。




  所以,从键盘上输入,会有回车进入到缓冲区。


  如: int main(){
  int i=65;
  int j=66;
  i=getchar();
  printf("i= ");
  putchar(i);
  j=getchar();
  printf("\n j=");
  putchar(j);


  }
  结果:输入 cde回车
   i=c
   j=d


  3)  getchar()的使用方法:


  错误:  char c;
   c=getchar();


  正确: int c;
   c=getchar();


   因为getchar()的返回值可能为-1  
   即EOF (在键盘上输入Ctrl+D ,则返回EOF)
   因为ASCII码的取值范围为0-255,不可能为-1.


  4) EOF 与getchar():


  EOF虽然是文件结束符,但并不是在任何情况下输入Ctrl+D(Windows下Ctrl+Z)都能够实现文件结束的功能,只有在下列的条件下,才作为文件结束符。


(1)遇到getcahr函数执行时,要输入第一个字符时就直接输入Ctrl+D,就可以跳出getchar(),去执行程序的其他部分;
(2)在前面输入的字符为换行符时,接着输入Ctrl+D;
(3)在前面有字符输入且不为换行符时,要连着输入两次Ctrl+D,这时第二次输入的Ctrl+D起到文件结束符的功能,第一次的Ctrl+D表示输入结束,而不是文件结束。
(4) 输入结束,可以使用回车或者Ctrl+D, 文件结束标识就是Ctrl+D;


  5) getchar() 与 getch() 区别:


  (1)


  getch直接从键盘获取键值,不等待用户按回车.
  只要用户按一个键就代表输入结束。
  getch就立刻返回,getch 函数用户的输入不会显示。


  (2)


  原型: int getch();
  getch返回值是用户输入的ASCII码,出错返回-1












  3.作为参数的 数组声明  等价于  相应的指针。




  如:int strlen(char s[]){}
  等价于 int strlen(char * s){}


  此中,数组作为参数,并没有为数组开辟内存空间。
  所以数组作为参数,自动转换为相应的指针。


  但是,如果数组不是作为参数,则不同于指针。
  如: extern char s[];
  不同于 extern char *s;


  4.连接两个字符串


  字符串是包含所有字符以及串的结束标识'\0'
  的数组的首地址。


  所以连接两个字符串s,t,注意内存空间大小的开辟。


  错误一:
  char *r;
  strcpy(r,s);
  strcat(r,t);


  r所指没开辟内存。


  错误二:


  char * r=malloc(strlen(s)+strlen(t));
  strcpy(r,s);
  strcat(r,t);


  原因:1.没有包含串的结束标识.开辟的内存不够
  2.malloc 之后一定要free
  3.malloc 之后没有判断是否为null


  正确:
  char * r= (char *)malloc(strlen(s)+strlen(t)+1);
  if(!r){
  complain();
  exit(1);
  }


  strcpy(r,s);
  strcat(r,t);


  /*
   some code 
  */
  free(r);


  注:malloc函数原型extern void *malloc(unsigned int num_bytes);


  1)参数为无符号型整数,sizeof 结果也是无符号型整数。
  2)malloc函数返回值为void *,所以需要强制类型转换。
  3)malloc结果要判断是否为null。
  4)开辟一个数组的内存空间,一般为malloc(sizeof(element)*num);




  5.空指针null 与 空字符串"" :


  1) 空指针null:


  (1)#define null 0


  (2)无法访问空指针所指向的内存中 内容。
  如:if(strcmp(p, (char *)0 )==0) 是错误的。
  如: char *p=null;
  printf("%s",p); 是错误的。


  (3)null是空指针,没有为其指向分配内存,

所以不可以访问空指针所指内容。


  (4)注意:变量指针的值为null,只是不可以通过该变量指针访问其所指的内容,
但是可以继续对该变量指针取地址,因为一个变量开辟内存空间了,就可以给它取地址。


如:NODEPTR pnode=NULL;


       initLinkList(&pnode);
其实变量指针的值为null,就相当于变量指针的值为0.






 

2) 空字符串""  :




 

(长度为0,占用一个字节)


 

存储一个空字符串""也需一个字节的内存空间,所以空字符串


 

分配了内存,可以访问内容中的内容,只是没内容而已。


 

strlen("")=0;








 

3) 空指针null 与 空字符串的区别:




 

空指针null ,没为其所指向分配内存,不可以访问其所指向。




 

空字符串"",分配了一个字节的内存,长度为0,可以访问内存中内容,只是没任何内容。










  6.有符号整数运算溢出

:




 

1) 无符号整数运算不会溢出:




 

无符号整数运算是以 2^n 为模的




 

2) 无符号数与有符号数运算不会发生溢出:


 

无符号数与有符号数运算,则有符号数转换为无符号数,


 

也不会发生溢出。




 

3) 两个有符号数运算可能发生溢出,且溢出结果未知。




 

4) 判断有符号数运算是否溢出:




 

(1)错误1:


 

if(a+b<0)


 

complain();




 

原因: 有符号数运算发生溢出,结果是未知的。




 

(2)正确1 :转换为无符号数


 

if( (unsigned )a +( unsigned)b >INT_MAX )


 

complain();


 

<limits.h>中定义了INT_MAX




 

(3) 正确2:


 

if(a>INT_MAX-b)


 

complain();








  7.文件的同时读与写 :




 C 中规定,文件中,一个输入操作不能直接紧跟着一个输出操作,


 反之依然。




 错误范例:


 FILE * fp;


 struct record rec;


 .....




 while(fread( (char *)&rec , sizeof(rec), 1, fp )==1){


 /* 对fp执行一些操作 */




 fseek(fp,-(long) sizeof(rec),1 );


 fwrite( (char *)&rec, sizeof(rec),1, fp );


 }


 错误:


 fwrite()后面紧跟着fread()函数。




 注意: 




 fseek函数原型:




 第二个参数为 long 类型,而sizeof()在编译期间执行,


 其结果为unsigned int 。






正确范例:




在fwrite ()函数后面加上:




fseek(fp,0L,1);


0L 表示long 型的0.






  8.函数指针 :




 

1)函数指针类型的定义  typedef :






    如:


    函数原型为:int Func( int a );


 

typedef int (*PtrFunType) ( int aPara );




 

PtrFunType 为函数指针类型。


 

定义一个变量为 PtrFunType ptrfun;






 


 

2)函数指针变量的定义 :




 

如:


 

int (*pFun2) ( int a ); 


 

// pFun2也是函数指针变量名






 

3)函数指针变量的赋值 :




 

函数指针变量=函数名;


 

或函数指针变量=&函数名;


 

如:


 

fptr=&Function;


        fptr=Function;








 

char (*pFun)(int); 


char glFun(int a){ return;} 


void main() 




     pFun = glFun; 


     (*pFun)(2); 


     或者pFun(2);








 

4)函数指针变量的使用 :




 

函数指针变量(实参表);


 

或者 (*函数指针变量)(实参表);


 

如: x=(*fptr)();


        x=fptr();






 

5) 函数指针作为参数 :




 

如:设计一个CallMyFun函数,这个函数可以通过参数中的函数指针值不同来分别调用MyFun1、MyFun2、MyFun3这三个函数(注:这三个函数的定义格式应相同)。


实现:代码如下:


//自行包含头文件 


void MyFun1(int x);  


void MyFun2(int x);  


void MyFun3(int x);  


typedef void (*FunType)(int ); //②. 定义一个函数指针类型FunType,与①函数类型一至


void CallMyFun(FunType fp,int x);




int main(int argc, char* argv[])


{


   CallMyFun(MyFun1,10);    //⑤. 通过CallMyFun函数分别调用三个不同的函数


   CallMyFun(MyFun2,20);   


   CallMyFun(MyFun3,30);   


}






 

6) 函数指针作为函数返回值 :略






 

7) 函数指针数组 :




 

比如:




int (*pFuncArray[10])();




将上面的声明转换为typedef格式,会使程序可读性增加:




typedef int(*pFunc)();




pFunc pFuncArray[10];




[] 的优先级高于*;


注意不可以写成::int ( (*pFuncArray) [10] ) ();








  9. EOF  与 feof()




 

1) EOF(End Of File)




 

(1)EOF 宏的定义:




 

EOF 是宏,定义为:#define  EOF  (-1)




 

在 UNIX中, EOF表示能从交互式 shell (终端) 送出 Ctrl+D (习惯性标准)。


 

在微软的 DOS 与 Windows 中能送出 Ctrl+Z。




 

(2) EOF的理解:




 

以EOF作为 文件结束标志的文件,必须是文本文件.


 

在文本文件中,数据都是以字符的ASCII代码值的形式存放。


 

我们知道,ASCII代码值的范围是0~255,不可能出现-1,


 

因此可以用EOF作为文件结束标志。






 

但是对于二进制文件不同,很可能读到的一个字节的数据就是0xFF,


 

那么返回值此时就是-1,但是此时还未到达文件末尾,造成错误的判断。






 

很多人认为在文件的末尾存在这个结束标志EOF,


 

这种观点是错误的。事实上在文件的末尾是不存在这个标志的。








2)  feof()函数:




(1)函数定义:


int feof( FILE *stream );




当到达结尾时,返回非0;


一个文本文件 包含 一直到文件最后一个字符的所有内容 以及 文件结束标记。




(2)feof 函数 读取文件的问题 :




范例一:


int ch ;


while(feof(fp)==0)


   {


       ch=fgetc(fp);


       printf("%x\n",ch);


   }




   假设:文件中为 65 66 


   则输入为41 42 FFFFFFFF


   输出FFFFFFFF 原因:




   改进:int ch;


   

ch=fgetc(fp);


   while(feof(fp)==0)


   {


       printf("%0X\n",ch);


       ch=fgetc(fp);


   }






10 . fgetc() 函数 :




 

1. fgetc() 原型:




 

int fgetc(FILE *fp);




 

2. fgetc() 的理解:




 

fgetc函数每次都是读取一个字节的数据,而且这一个字节的数据是以unsigned char 即无符号型处理的,然后将这一个字节的数据赋给一个int型变量作为返回值返回。




 

所以: fgetc() 函数 读取的数据 的范围为 0-255.(0x00~0xff)


 

所以使用 fgetc() 只能判断文本文件是否到了文件末尾,不可以判断二进制文件。




 

3.读取文件的问题:






 

对于文本文件:




 

int c=0;




while(!feof(fp))


{


int c=fgetc(fp);


printf("%c:/t%x/n",c,c);


}


解析:假设文件指针fp指向某个文本文件,文件中有字符串“hello”,下面的代码将输出hello外,还将输出一个结束字符EOF(EOF是fgetc函数的返回值,并不是文件中存在EOF):






改进:

int c;


        c=fgetc(fp);    


while(!feof(fp))


{



printf("%c:/t%x/n",c,c);


c=fgetc(fp);


}




4. fgetc() 函数返回值赋值问题:




fgetc读取的数为0~255(0x00~0xff),返回值为int类型。




(1)正确赋值:




int c;


c=fgetc(fp);


while(c!=EOF){


printf("%c",c);


c=fgetc(fp);


}




分析:即使是遇到字符0xFF(255),while循环也不会结束,因为0xFF会被转化0x000000FF。


而EOF=-1=0xffffffff




(2)  错误赋值1:




上例中 char c; 其他不变。




分析:假定下一个读取的字符为0xFF , 


fgetc(rfp)的值为 0x000000FF, 然后强制转化为char类型:c = 0xFF。


此时字符c 与 EOF 比较。c 被带符号(signed)扩展为0xFFFFFFFF。条件成立,文件复制提前退出,故遇到空格字符时就退出,不能完成复制。




(3) 错误赋值2:




  unsigned char c ; 其他不变。




  解析:当读到文件末尾,返回 EOF 也就是 -1 时,fgetc (rfp)的值为EOF,即-1,即0xFFFFFFFF,然后强制转化为uchar类型, c=0xFF。unsigned char (oxff)与 int (-1) 比较, c 被扩展为 0x000000FF, 永远不会等于 0xFFFFFFFF。




  所以:虽然能正确复制 0xFF, 但却不能判断文件结束.




11.

文本文件 与  二进制文件的区别:




在Unix和其它一些系统中,没有文本方式和二进制方式的区分。




1) 文本文件:




(1) 定义:




文本文件是基于字符编码的文件,常见的编码有ASCII编码,UNICODE编码。


Windows和DOS系统中,扩展名为txt的文件,C源程序文件,HTML超文本,XML


都是文本文件。因此,基于字符嘛,每个字符在具体编


码中是固定的,ASCII码是8个比特的编码,UNICODE一般占16个比特。


文本文件基本上是定长编码的、




2) 二进制文件:




(1) 定义:二进制文件是基于值编码的文件,二进制文件可看成是变长编码的,


因为是值编码嘛,多少个比特代表一个值,完全由你决定。


常见的BMP 文件, word 中的doc 文件, jpg 图像文件都是二进制文件。


在二进制文件中存储的数据是用二进制形式来表示的。




如: BMP 文件是二进制文件,其就是变成编码的。其头部是较为固定长度的文件头信息,


前2字节用来记录文件为BMP格式,接下来的8个字节用来记录文件长度,


再接下来的4字节用来记录bmp文件头的长度。




3) 文本文件与 二进制文件的 优缺点:




 (1)可读性:




 (2)译码:


 

文本文件编码基于字符


定长,译码容易些;二进制文件编码是变长的,所以它灵活,


存储利用率要高些,译码难。




  (3)效率:


  文本文件的可读性要好些,存储要花费转换时间(读写要编译码)


,而二进制文件可读性差,存储不存在转换时间(读写不要编解码,直接写值)




 (4)linux中文本方式的读写与二进制方式的读写无差


别,不存在回车换行间的转换.这样当直接在windows和linux中共享文件时,将会出现


与回车换行相关的问题.





----------------------------------------------------




四:连接




1.连接器程序执行的过程




1)连接器:




将编译器编译后的目标文件模块连接成可执行文件实体或载入模块,


该实体可以直接被操作系统执行。




输入:目标文件以及库文件


输出:可执行文件。






2)执行过程:


预处理(预编译)-->编译-->连接




2.变量的定义与声明 :




1)变量的定义与声明区别:




 (1)变量的定义需要开辟内存空间,变量的声明不会开辟内存空间。


 (2)变量 的定义有且只有一次,变量的声明可以有多次。




2)变量的声明:




extern int a;




在一个文件a.c中定义的全局变量a,


在b.c文件中如果使用则需要在b.c中加上extern int a;




方法二:


在a.h中声明全局变量,以及函数。


在a.c文件#include"a.h"




如果b.c文件需要使用a.c 文件中的全局变量,或者函数。


只需要 #include"a.h"即可。




3)变量的定义:




如:


int a;


int b=1;


static int c;


static int c=2;


extern int d=3;




变量的初始化也是变量的定义。












3.类型转换 :




 1)大内存填小数据 :




 如:int i='a'; 




 实际上等价于: 


 int i ; // 开辟一个内存空间。


 i='a'; 


 // int 内存空间 (4 字节=32 位,填写 8 位数据。)




 如: int i=3.14;


 就会发生精度丢失。


 (应为4个字节的 内存空间需要填8个字节 的数据。)


         


 如:int i;


  char c=i;


  只要 i 的值,在0~255范围, 就不会发生错误。






 2)大内存指针赋值给小内存指针 :




 在大内存指针所指的大内存中,填写小内存指针所指的内容。




 如: 多态中子类的对象指针赋值给父类的对象指针。




 如:


 

int main(){


 

int i=256;


 

char c;


 

scanf("%d",&c);


 

printf("%d ",i);


 

printf("\n");


 

}




 

输入256,则输出1.




 

因为256=2^8,


 

开辟内存时,是----这四个字节是int i;


 

然后紧接着的一个字节-是char c;




 

scanf("%d",&c);


 

是将char* 赋值给 int *;


 

在char (一个字节)的内存上写int数据;


 

如果数据在0~255.则没有问题。


 

如果数据>255,则会占用char c 前面开辟的内存,


 

即 int i 的内存。








4.static描述变量:




(1)C中 :




1.隐藏 ,避免发生命名冲突:




文件内可见,文件外不可见,。所以可以在不同 的文件中


定义相同的变量。




一个完整的程序,在内存中的分布情况如下:


 


代码区  //  代码


全局数据区  // 全局变量以及static 变量


堆区   // 动态分配的变量


栈区   // 局部变量




注:全局数据区 又包含: 


(1)初始化全局数据区:




在程序中所有赋了初值的全局变量。


(2)非初始化全局数据区:


在程序中未初始化的全局变量,内核将此段初始化为0




2.持久 :




大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。




3.初始化 :


(初始化一次,且默认初始化为0)




4.static 变量的使用: 




1. 统计次数功能




(2)C++中




1.类内声明,类外定义并初始化。




在类内static修饰成员变量,仅仅是声明;


所以计算类的大小,不包含static成员变量。




在类外进行定义,(开辟空间),初始化。


即使该成员变量是private,也要在类外进行初始化。




2.static成员变量类内直接进行定义并初始化 :




静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。




它的空间分配有三个可能的地方,


一是作为类的外部接口的头文件,那里有类声明;


二是类定义的内部实现,那里有类的成员函数定义;


三是应用程序的main()函数前的全局数据声明和定义处。


推荐第三个,其他忽略。




3.static成员变量在内存中只有一份拷贝,为类的成员变量 :




静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。


所有对象的static成员变量相同。




4.static 成员变量的使用 :


静态数据成员主要用在各个对象都有相同的某项属性的时候,减少空间开销。




5.静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;




5. static 描述函数。




1) C 中 :




 隐藏,避免发生命名冲突。




2) C++中 :




无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数以及访问静态成员变量。






--------------------------------------------------------


五: 预处理




1.预处理


0)预处理定义以及好处

:




(1)定义:


以符号#开头的编译指令,这些指令称为预处理命令。


(2)好处:


在代码的移植性强以及代码的修改方便等方面


(3)规则:


   1. 指令都是以#开始


   2. 指令总是第一个换行符处结束,如果一行


   如法写完可以加上"\"换行标识。




1)预处理包含

:




(1)宏定义

#define , #undef :


#define 宏替换。#define 指令定义一个宏,#undef指令删除一个宏定义。




(2)文件包含

#include :


#include指令导致一个指定文件的内容被包含到程序中。




(3)条件编译  #ifdef,#ifndef,#if, #elif,#else:








2)程序执行过程

:


预处理(预编译)->编译->连接->执行




2) 文件包含 #include :




(1)作用:


#include命令的作用是把指定的文件模块内容插入到#include所在的位置。




(2)使用:


对于库文件,则使用 #include<> ;使用#include""也可以;


对编程自己编写的文件,则使用双引号即#include""。如果自己编写的文件不是存放在当前工作文件夹,可以在#include命令后面加在路径。






3)条件编译 #ifdef ,#ifndef :




(1)作用 :




照不同的条件去编译程序的不同部分,从而得到不同的目标代码。使用条件编译,可方便地处理程序的调试版本和正式版本,也可使用条件编译使程序的移植更方便。




1.减少翻译语句,减少目标程序长度。


2.处理程序的调试版本与正式版本。


3.便于不同系统之间的移植。


4.程序维护与升级




(2) 使用 #if :




与C语言的条件分支语句类似,在预处理时,也可以使用分支.




格式: 




#if 常量表达式


  程序段


#else


 程序段


#endif




或者:


#if 常量表达式 1


   程序段 1


#elif 常量表达式 2


   程序段 2


… …


#elif 常量表达式 n


   程序段 n


#else


   程序段 m


#endif




范例:


输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输出。 


   #define LETTER 1 


   main() 


   { 


   char str[20]="C Language",c; 


   int i="0"; 


   while((c=str[i])!='/0'){ 


   i++; 


   #if LETTER 


   if(c>='a'&&c<='z') c="c-32"; 


   #else 


   if(c>='A'&&c<='Z') c="c"+32; 


   #endif 


   printf("%c",c); 


   } 


   } 


    


   运行结果为:C LANGUAGE 




   有人会问:不用条件编译命令而直接用if语句也能达到要求,用条件编译命令有什么好处呢?的确,此问题完全可以不用条件编译处理,但那样做目标程序长(因为所有语句都编译),而采用条件编译,可以减少被编译的语句,从而减少目标的长度。当条件编译段比较多时,目标程序长度可以大大减少。








(3)使用 #ifdef ,#ifndef :




#ifdef ,#ifndef 只是判断是否定义了该符号常量。




1.格式:




#ifdef 标识符


程序段 1


#else


   程序段 2


#endif




区分:




就是#if后面的是一个表达式,而不是一个简单的标识符: 


   #if 表达式 


   程序段1 


   #else 


   程序段2 


   #endif 




解释:如果#ifdef后面的标识符已被定义过,则对“程序段1”进行编译;如果没有定义标识符,则编译“程序段2”。




2.可以在头文件中使用 #ifndef 等来避免头文件的重复包含。




一般格式是这样的:




#ifndef <标识> 


#define <标识>





/*头文件中内容*/




#endif




标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,


如:stdio.h




#ifndef _STDIO_H_ 


#define _STDIO_H_


/*头文件中内容*/


#endif




注意:




变量一般不要定义在.h文件中:


<x.h>


#ifndef __X_H__


#define __X_H__


extern int i;


#endif //__X_H__


<x.c>


int i;














3. 程序通用性使用





我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译: 


   #ifdef WINDOWS 


   #define MYTYPE long 


   #else 


   #define MYTYPE float 


   #endif  




    如果在Windows上编译程序,则可以在程序的开始加上 


   

#define WINDOWS 




   

4. #ifdef/ #ifndef 


   

#else 


   

#endif 在 调试时使用 :






   

 例如,在调试程序时,常常希望输出一些所需的信息,而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:


   #ifdef DEBUG 


   print ("device_open(%p)/n", file); 


   #endif 


    


   如果在它的前面有以下命令行: 


   #define DEBUG 




    则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可。有人可能觉得不用条件编译也可达此目的,即在调试时加一批printf语句,调试后一一将printf语句删除去。的确,这是可以的。但是,当调试时加的printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删改printf语句,只需删除前面的一条“#define DEBUG”命令即可,这时所有的用DEBUG作标识符的条件编译段都使其中的printf语句不起作用,即起统一控制的作用,如同一个“开关”一样。 








4)宏定义 #define  :


(0) 关于宏:


1. 格式




#define 宏名 宏体




(类似于变量的定义,#define 左值 右值




使用时,使用左值,预编译时用右值替换左值。




但是没分号,因为如果有分号,


替换时也会有分号。








2. 预编译时宏名被宏体替换:




(1) 变量式宏(无参数宏) :




#define 宏名 宏体




宏不是类型定义,定义一个类型使用typedef ,而不是用宏。




如:


#define T1 struct foo*


typedef struct foo* T2;


T1 a1,b1;


T2 a2,b2;




宏仅仅是替换,所以 a1为指针类型,b1 不是指针类型;


a2,b2均是指针类型。




(2) 函数式宏(有参数宏) : 用 do {} while(0)来包住宏体




1. 每一个参数,以及每一个表达式结果用()括起来:




例如:




错误函数式宏:


#define abs(x) x>0?x:-x


解析:


如果abs(a-b),则宏替换为a-b>0?a-b:-a-b


显然不对。




正确:


#define abs(x) ((x)>0)?(x):(-(x))






2. 用do{}while(0)包住宏体:




例如:


/* c3.c: 交换两个整型变量的值 */  


/* #define swap(x,y) { int temp=x; x=y; y=temp; } */  


#define swap(x,y)  \   


   do { int temp=x; x=y; y=temp; } while(0)   


#include <stdio.h>   


int main(){   


   int x=4,y=3;   


   if(x>y) swap(x,y); /* 用第一个swap时会出错,导致{ }后面有一个分号,  


                              用第二个swap则没问题 */  


   else x=y;   


   printf("x=%d, y=%d\n",x,y);   


   return 0;   


}  




如果用注释中定义那个swap,则if {...};后面会一个分号,单独的分号是一个空语句,这导致if与else之间有两个单独的语句不合法。而用do{ }while(0)套住语句时则不会有这样的问题。




(3) 常见的宏定义技巧 :




1.防止一个头文件被重复包含




  #ifndef _COMDEF_H




  #define _COMDEF_H




  //头文件内容




  #endif




2.得到指定地址上的一个字节或字




  #define MEM_B( x ) ( *( (byte *) (x) ) )




  #define MEM_W( x ) ( *( (word *) (x) ) )




3.求最大值和最小值




  #define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) )




  #define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )



4. 得到一个大于等于X 且又最接近X的8的倍数




  #define RND8( x ) ((((x) + 7) / 8 ) * 8 )




解析: 该数肯定在X~X+7  的范围内。




扩展: 得到一个小于等于X 且又最接近X的8的倍数:




((x)-7)/8*8




5.将一个字母转换为大写




  #define UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )




               6.判断字符是不是10进值的数字




  #define DECCHK( c ) ((c) >= '0' && (c) <= '9')




               7.判断字符是不是16进值的数字




  #define HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') ||\




  ((c) >= 'A' && (c) <= 'F') ||\




  ((c) >= 'a' && (c) <= 'f') )




               8.防止溢出的一个方法




  #define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))




               9.返回数组元素的个数




  #define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )




10. 连接符  ##  :




##被称为连接符(concatenator),用来将两个Token连接为一个Token.




如:你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:




struct command




  {




  char * name;




  void (*function) (void);




  };




  #define COMMAND(NAME) { NAME, NAME ## _command }




  // 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:此中的#define 的宏体使用了{},因为在


结构体数组的初始化中,每个成员需加{}.




  struct command commands[] = {




  COMMAND(quit),




  COMMAND(help),




  /*……*/




  } 




























(4)  函数式宏与内联函数 inline :




1.  C++ 中 内联函数 inline  :




inline 与函数定义绑定才可以成为内联函数,


与函数声明绑定不会成为内联函数。




  类中定义的函数 自动被认为是内联函数。


  类中声明,类外定义的函数,不是内联函数,需要加上


  inline才可以定义为内联函数。




  如: 




  class A()




      {




            void c();// not a inline function;




           void d()


           { print("d() is a inline function.");


           }




       }




       如果c()函数需要时内联函数;


       则在类外实现时需要加上 inline.




       


C++使用函数式宏缺点:


       无法操作类的私有数据成员,以及容易出错。




       3. C++ 使用内联函数的好处:




        1).可以进行类型检查(参数,返回值)或类型转换,宏无法。




        2).内联函数的代码就会直接替换内联函数调用,于是省去了函数调用的开销,减少时间开销,但是会增加代码量,增加空间开销。




        3).内联函数可以访问类 成员变量,函数式宏不可以。




        4). 类内定义的函数默认为内联函数,类外的函数定义需要


        加上inline 才可以是内联函数。






4. 内联函数  应用: 




内联函数如果有循环和递归调用则不被内联。




内联函数一般是简单短小的函数。












(5) 特殊宏 :




1. _FILE_ , _LINE_




例如:用宏来跟踪一个函数的所有调用。




view plaincopy to clipboardprint?


/* g1.c:用宏来跟踪一个函数的所有调用。 */  


#include <stdio.h>   


void func(int *a,int *b){   


    int t=*a;   


    *a=*b;   


    *b=t;   


}   


#define func(x,y) \   


    (printf("func's invoking point: %s, %d\n",__FILE__,__LINE__), func((x),(y)))   


       


int main(){   


    int a[]={0,1,2,3,4,5,6,7,8};   


    printf("a[0]=%d,a[1]=%d\n",a[0],a[1]);   


    func(&a[0],&a[1]);   


    printf("a[0]=%d,a[1]=%d\n\n",a[0],a[1]);   


       


    printf("a[2]=%d,a[3]=%d\n",a[2],a[3]);   


    func(&a[2],&a[3]);   


    printf("a[2]=%d,a[3]=%d\n\n",a[2],a[3]);   


       


    printf("a[4]=%d,a[5]=%d\n",a[4],a[5]);   


    func(&a[4],&a[5]);   


    printf("a[4]=%d,a[5]=%d\n\n",a[4],a[5]);   


    return 0;   


}  




注意:




函数的定义必须出现在跟踪宏的定义之前,因为在宏体中使用了实际的函数,因此必须先看到其定义。在预处理时,预处理器发现main中的各个调用有同名的宏,并且参数匹配,因此会作宏展开,在实际的函数前插入了一个printf来跟踪这个调用的位置。




在函数内部插入printf()语句是不可以的,需要在调用该函数的前面


加上printf()语句。









--------------------------------------------------------


            六:程序设计




  (零)在做软件架构设计时,根据不同的抽象层次可分为三种不同层次的模式:架构模式(Architectural Pattern)、设计模式(Design Pattern)、代码模式(Coding Pattern)。




  






   (一) 模块划分 :




    C语言中,将一个程序依据功能划分为多个模块(C++,依据功能划分是错误的)




    (1)模块划分的要求 :


    1.模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;




    2.模块的.h文件提供其他模块调用的外部函数以及变量,并加上extern声明。


    (注意在.h文件中是对函数,以及变量等接口的声明,不是定义)




    3. 模块内使用 的函数以及变量,在.c文件中加上static 进行定义。




    4. 永远不要在.h文件中定义变量!






    (2)范例 :



1. 错误范例:




/*module1.h*/


int a = 5; /* 在模块1的.h文件中定义int a */




/*module1 .c*/


#include "module1.h" /* 在模块1中包含模块1的.h文件 */




/*module2 .c*/


#include "module1.h" /* 在模块2中包含模块1的.h文件 */




/*module3 .c*/


#include "module1.h" /* 在模块3中包含模块1的.h文件 */




原因:以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元。




2.正确范例 :




/*module1.h*/


extern int a; /* 在模块1的.h文件中声明int a */




/*module1 .c*/


#include "module1.h" /* 在模块1中包含模块1的.h文件 */


int a = 5; /* 在模块1的.c文件中定义int a */




/*module2 .c*/


#include "module1.h" /* 在模块2中包含模块1的.h文件 */




/*module3 .c*/


#include "module1.h" /* 在模块3中包含模块1的.h文件 */


  这样如果模块1、2、3操作a的话,对应的是同一片内存单元。