C 陷阱与缺陷 总结_legend

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的话,对应的是同一片内存单元。






  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值