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