22
2020
08

《c陷阱与缺陷》笔记----第二篇

继续更新,最近下雨太多,导致很少看视频,夏天不是热就是雨多,很难学习的下啊(其实就是为了骗自己,都是借口

3-7求值顺序陷阱

#include<stdio.h>

int main(){

	int a=10;
	int b = 1;
        int v=a || b++;
	int v=a && b++;
	int v = a>b?a++:b++;
	return 0;

} 
//这三个都属于短路求值,意思简单的就是说当a || b++的a是真的时候,那就不用理b++了,其他的类似
if(y != 0 && x/y >tolerance);
运算符&&和运算符||对于保证检查操作按照正确的顺序执行至关重要;在x/y中的y不能为0,所以当y等于0时,就不用计算
x/y >tolerance,是不是很秒,嘻嘻

总结来说就是(书本上的原话):c语言中只有四个运算符(&&,||,?:和,)存在规定的求值顺序,

运算符&&和运算符||首先对左侧操作数求值,只在需要时才对右侧操作数求值。运算符?:有三个操作数:

在a?b:c中,操作数a首先被求值,根据a的值再求操作数b或c的值。而逗号运算符,首先对左侧操作数求值,

然后该值被“丢弃”,再对右侧操作数求值

3-8运算符与或非陷阱

emmm,还是做下笔记吧

c语言中两类逻辑运算符,某些时候可以互换:按位运算符&,|和~,以及逻辑运算符&&,

||和!;如果互换之后程序看上去还能“正常”工作,但是实际上这只是巧合所致

#include<stdio.h>
int main (){
	int a=10;//1010
	int b=12;//1100
	int v;
	v=a&&b;
	printf("v=%d\n",v);//1
	v=a&b;
	printf("v=%d\n",v);//1000,即8 
	v=a||b;
	printf("v=%d\n",v);//1 
	v=a|b;
	printf("v=%d\n",v);//1110 即14 
	v=!a;
	printf("v=%d\n",v);//0 
	v=~a;
	printf("v=%d\n",v);//-11 ,就是取反 0101,因为是int类型 32位的,所以取反后应该是1111 1111 1111 0101 
	return 0;
}

只要x和y的取值都限制在0或1,那么x&y与x&&y总是得出相同的结果

反正总的意思就是要区分两类逻辑运算符,两类逻辑运算符不能划等号,虽然有时候巧合结果是相同的,那也是巧合,我们要正确使用

3-9整数溢出陷阱

先了解什么是溢出

#include<stdio.h>

int main(){
	int a=2147483647;//int类型最大的值 
	int b=1;
	a +=b;
	printf("a=%d\n",a) ;//输出是-2147483648,因为a加一后就溢出了 ,这就是溢出 
	return 0;
}

咋么改才变成不溢出呢

#include<stdio.h>

int main(){
	unsigned int a=2147483647;
	int b=1;
	a +=b;
	printf("a=%u\n",a) ;//输出是2147483648,注意无符号整型输出要用%u 
	return 0;
}
因为有符号整型的31位才是有效的,最高位是符号位,而无符号整型的32位都是有效
#include<stdio.h>
#define INT_MAX 2147483647
int main(){
	int a =2147483647;
	int b=1;
	if((unsigned int)a+(unsigned int)b > INT_MAX){//这样可以检测溢出,因为直接a+b>INT_MAX不一定能检测出来,看下面解释
		printf("OK!\n");
	}
	return 0;
} 
在某些机器上,加法运算将设置一个内部寄存器为四种状态之一:正,负,零和溢出,当加法操作发生溢出时,这个内部
寄存器的状态是溢出而不是负,那么if的语句的检查就会失败(对于a+b>INT_MAX)
void main(){
    unsigned int a=4294967295;
    int b=1;
    a+=b;
    printf("a=%u\n",a);//输出0,这里也是溢出的
}

3-10main函数返回值陷阱

·函数main与其他任何函数一样,如果没有显式声明返回类型,那么函数返回类型就默认为是整型

·一个返回值为整数的函数如果返回失败,实际上是隐含地返回了某个“垃圾整数”。只要改数值不被用到,就无关紧要

·大多数c语言实现都通过函数main的返回值来告知操作系统该函数的执行是成功还是失败,典型的处理方案是,fanhuiz

为0代表程序执行成功,返回值非0则表示程序执行失败

4-1连接器的陷阱

什么是连接器

c语言中的一个重要思想就是分别编译,即若干个源程序可以在不同的时候单独进行编译,然后在恰当的时候

整合到一起。但是,连接器一般是与c编译器分离的,他不可能了解c语言的诸多细节,那连接器是咋么把c语言整合在一起的呢:尽管连接器并不理解c语言,然而它却能够理解机器语言和内存布局。编译器的责任是把c从、源程序“翻译”成对连接器有意义的形式,这样连接器就能够“读懂”c源程序了。典型的连接器把由编译器或汇编器生成的若干个目标模块(即.o或.obj),整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。


· IDE 集成开发环境:包含编译器,链接器,执行器,调试器

如何生成.exe文件:预处理(展开头文件,宏定义的展开),编译(检查语法错误,并生成汇编文件),汇编(生成目标文件.o,.obj),链接(多个文件链接起来形参.exe)

4-2声明与定义陷阱

声明不会开辟空间,而定义是需要系统开辟空间

#include<stdio.h>
int x;//系统会默认初始值为0
void main(){
    int a;//初始值是一个随机数
}
void main(){
    {
      extern int a;//这里说明外部引入的a的作用域只有这么大,所以下一句并不能输出a
    }
    printf("%d",a);
}

4-3命名冲突与static修饰符陷阱

如果一个函数仅仅被同一个源文件中的其他函数调用,我们就应该声明该函数为static

4-5检查外部类型陷阱

在其他文件里面定义:char filename[]="xiaoxiao";
在主文件里面声明:extern char* filename;//如果是这样声明就出错
在主文件里面声明:extern char filename[];//如果是这样声明就是对的
//因为尽管在某些上下文环境中,数组与指针非常类似,但他们是不一样的,就像上面这个例子,而数组形参的时候,指针和数组都可以

5-1返回整数的getchar函数陷阱

#include<stdio.h>
int main(){
        char ch;//因为getchar返回值是整型而不是字符型,所以这里应该改成int ch;
	while((ch=getchar())!=EOF){
		putchar(ch);
	}
	return 0;
}
·在Windows系统中,用户可以通过Ctrl+Z来表示EOF,以结束文本流的输入。
·在前面有字符输入且不为换行符时,要连着输入两次Ctrl+D,这时第二次输入的Ctrl+D起到文件结束符的功能
·当输入了若干字符(不能包含换行符)之后,直接输入Ctrl+D,此时的Ctrl+D并不是文件结束符,而只是相当于换行符的
 功能,即结束当前的输入。
·EOF的作用也可以总结为:当终端有字符输入时,Ctrl+D产生的EOF相当于结束本行的输入,将引起getchar()新一轮的
输入;当终端没有字符输入或者可以说当getchar()读取新的一次输入时,输入Ctrl+D,此时产生的EOF相当于文件结束
符,程序将结束getchar()的执行。
·了解个东西:此时用户输入的字符存储在缓冲区中,但没有按下Enter键,那么程序即使执行到putchar(),也不会将
这个字符打印出来,因为压根就没有刷新缓冲区(笑),当用户按下Enter键之后,计算机刷新缓冲区,将这个缓冲区
的内容打印出来。

5-2更新顺序文件陷阱

· 文本文件只能放字符

· 如果一个

·如果一个记录需要被重新写入文件,不但要用到fwrite和fread,还要用到fseek来正确定位文件指针fp

·需要了解一个知识点就是一开始文件指针是指向第一个字符,并且文件指针会自动下移到下一个字符

#include<stdio.h>
int main(){
	int value;
	FILE *fp=NULL;
	int i;
	fp=fopen("test.txt","rb+");
	if(fp==NULL){
		printf("Open file fail!\n");
		return 0;
	}
	while(fread(&value,sizeof(int),1,fp)==1){
		value=i++;
		fseek(fp,-sizeof(int),SEEK_CUR);
		fwrite(&value,sizeof(int),1,fp);
		fseek(fp,sizeof(int),SEEK_CUR);
	}
	fclose(fp);
	return 0;
}

5-3缓冲输出与内存分配陷阱

偶买噶发现个惊人的发现 

#include<stdio.h>

int main(){
	int ch;
        char buf[BUFSIZ];// BUFSIZ是系统定义的,大小为512 
	int i;
	setbuf(stdout,buf);//stdout是标准输出先缓存在buf中 
	for(i=0;i<2;i++){
		scanf("%d",&ch);
	
	}
        printf("%d",ch)	;
   	for(i=0;i<2;i++){
	scanf("%d",&ch);
	
	}
	 printf("%d",ch)	;
	return 0;
} 
//这个程序是错的,你运行会发现最后缓存输出是错误的
因为在main函数结束之前,并且缓存还没被输出,buf就被注销了,所以缓存输出是乱码
(原文):在main函数结束之后,作为程序交回控制给操作系统之前c运行时库所必须进行的清理工作的一部分。
但是,在此之前buf字符数组已经被释放!

修改的办法就是:第一:增长数组生命周期,变成全局变量或者静态变量,第二:动态分配内存给缓存

因为动态分配在程序中并不主动释放分配的缓冲区

#include<stdio.h>
//char buf[BUFSIZ];第二种
int main(){
	int ch;
   static char buf[BUFSIZ];// BUFSIZ是系统定义的,大小为512 第一种
	int i;
	setbuf(stdout,buf);//stdout是标准输出先缓存在buf中
	//setbuf(stdout,(char *)malloc(BUFSIZE)); 第三种
	for(i=0;i<2;i++){
		scanf("%d",&ch);
	
	}
    printf("%d",ch)	;
   	for(i=0;i<2;i++){
	scanf("%d",&ch);
	
	}
	 printf("%d",ch)	;
	return 0;
}

5-4使用errno检测错误陷阱

当在错误发生的时候,才用errno来检测,这样就能避免出错

#include<stdio.h>
#include<errno.h>
int main(){
	FILE *fp=NULL;
	fp=fopen("AA.txt","r");//这个文件不存在
	printf("errno=%d\n",errno);//打印2
	fp=fopen("test.txt","r");//这个文件存在
	printf("errno=%d\n",errno);//这个应该本意想打0的,但是打印了2,和我们想的不一样
	
	return 0;
}
//下面改进:当在错误发生的时候,才用errno来检测,这样就能避免出错
#include<stdio.h>
#include<errno.h>
int main(){
	FILE *fp=NULL;
	fp=fopen("AA.txt","r");
	if(NULL==fp){
	printf("errno=%d\n",errno);
	}
	
	fp=fopen("test.txt","r");
	if(NULL==fp){
	printf("errno=%d\n",errno);//出错了才打印
	}
	return 0;
}

6-1宏定义中的空格陷阱

#include<stdio.h>
int fun(int x){
return x-1;
}

#define f (x) ((x)-1)//正确的是这样的f(x) ((x)-1),是没有空格的,有空格就报错了

int main(){

int result1,result2;
result1=fun(10);
result2=f(10);
return 0;
}
#include<stdio.h>
int fun(int x){
return x-1;
}
#define f(x) ((x)-1)
int main(){
int result1,result2;
result1=fun(10);
result2=f  (10);//这样是对的,程序正确
return 0;
}

6-2宏函数陷阱

因为宏是替换后再进行运算作用,所以在定义宏的时候记得加括号,参数要加括号,整体的结果也要用括号给补上,例如

#define add(a,b)  ((a)+(b))

但是在一些情况下即使做到了上面说的,还是出现bug,例子如下

#include<stdio.h>
#define max(a,b) ((a)>(b)?(a):(b))
int main(){
	int x[3]={2,3,1};
	int biggest=x[0];
	int i=1;
	while(i<3){
		biggest=max(biggest,x[i++]);
	}//宏替换之后 ((biggest)>(x[i++])?(biggest):(x[i++]))会发现i++计算了两次,第一次是 在(biggest)>(x[i++])比较的时候,
	//第二次是x[i++],所以会导致出现错误 
	 
	printf("biggest is= %d\n",biggest);//输出答案是1 
	return 0;
}
//解决这个bug的方法是:方法一:在使用宏的时候不要用++或者--方法二:把max宏变成max函数就不会出现上面的错误了
//因为变成函数的时候,是把参数的值传过去,不会导致i++运算两次

6-3

来认识一下一个新的概念:断言,看个例子就知道是说什么了

#include<stdio.h>
#include<assert.h>
int main(){
	int x=1;
	assert(x>10);//程序会终止并且会告诉你Assertion failed!,Program: F:\c++\1.exe,File: F:\c++\未命名1.cpp, Line 5,Expression: x>10
	return 0;
} 
//自己运行下就知道了

6-4宏类型定义陷阱 

#include<stdio.h>
#include<stdlib.h>
#define XIAO char*  //这样定义的时候 :char* p1,p2;因为只是文本替代 所以*会和p1结合形成 char *p1,p2;
                    //导致了两者类型不一样 
typedef char* XIAO;//这样定义的时候 : char* p1,p2;因为是类型的重新命名,所以把char*是看成整体的,所以
                    //结果是 (char*) p1,p2;两个的类型都一样 
int main(){
	char ch='A';
	XIAO p1,p2;
	p1=&ch;
	p2=&ch;
	return 0;
}

7-3整数大小的陷阱

#include<stdio.h>
#include<stdlib.h>
int main(){
	printf("%d\n",sizeof(char)) ;
	printf("%d\n",sizeof(short)) ;
	printf("%d\n",sizeof(int)) ;
	printf("%d\n",sizeof(long)) ;
	printf("%d\n",sizeof(float)) ;
	printf("%d\n",sizeof(double)) ;//系统不一样结果就不一样
		return 0;
}

7-4字符符号陷阱

#include<stdio.h>
#include<stdlib.h>
int main(){
	char ch=-10;//1000 1010
	int a=(unsigned char)ch;
	printf("%d",a) ;//结果是246.而不是138,在计算机负数是以补码形式出现,所以要补码转原码即原码取反加1
		return 0;
}

需要注意一点是:(原文)如果c是一个字符变量,使用(unsigned)c就可以得到与c等价的无符号整数。这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果

正确的方式是使用语句(unsigned char)c,因为一个unsigned char类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接进行转换

总结:就是想表示的类型要写完整

7-5移位运算符陷阱

向右移,正数用0补充,负数用1补充

#include<stdio.h>
#include<stdlib.h>
int main(){
	int a=-3;
	int v=a>>1;
	printf("%d",v); 
		return 0;
}

移位的速度要比除数的速度快的多

7-7除法运算截断陷阱

q=a/b;

r=a%b;

当b>0时,我们希望保证r>=0且r<b。例如,如果余数用于哈希表的索引,确保它是一个有效的索引值很重要,总的意思就是在哈希表索引的时候保证余数为正数

7-8随机数大小的陷阱

· 伪随机数的意思就是:每次随机的数字都一样

#include<stdio.h>
#include<stdlib.h>
int main(){
	int i;
	
    for(i=0;i<10;i++){
    	printf("%d ",rand());//每次运行的数都一样,看起来随机但是并没有次次不同
	}

		return 0;
}

如何产生随机数请看:https://blog.qiquanji.com/post/10440.html

7-11可移植性问题的一个例子

又发现一个神奇的东西

为了避免字符集中的数字不是顺序排列的,解决办法是使用一张代表数字的字符表。因为一个字符串常量可以用来

表示一个字符数组,所以在数组名出现的地方都可以用字符串常量来替换。

#include <stdio.h>
#include <stdlib.h>
#include <time.h> 
void print(char c)
{
	printf("%c",c);
}
void printnum(long n,void (*p)(char)){
	if(n<0){
		(*p)('-');
		n=-n;    //这里其实还有个隐含的bug,会溢出,因为基于2的补码的计算机一般允许表示的负数取值范围要大于正数的取值范围
	}
	if(n>=10){
		printnum(n/10,p);
	}
//	(*p)((n%10)+'0');
	(*p)("0123456789"[n%10]);//和上面那一句是同样的效果
}
int main()
{
    long value=-85014;
    printnum(value,print);
    return 0;
}

改进如下

#include <stdio.h>
#include <stdlib.h>
#include <time.h> 
void print(char c)
{
	printf("%c",c);
}
void printneg(long n,void(*p)(char)){
    if(n<=-10){
    printneg(n/10,p);
    }
    (*p)("0123456789"[-(n%10)]);//其实这里还有bug,n为负数的时候,n%10完全可能是一个正数
}
void printnum(long n,void (*p)(char)){
	if(n<0){
		(*p)('-');
	printneg(n,p);
	}else{
		printneg(-n,p);
	}
}
int main()
{
    long value=-2147483648;  //-2147483648~2147483647
    printnum(value,print);
    return 0;
}

改进

#include <stdio.h>
#include <stdlib.h>
#include <time.h> 
void print(char c)
{
	printf("%c",c);
}
void printneg(long n,void(*p)(char)){
    long q;
    int r;
    q=n/10;
    r=n%10;
    if(r>0){
    r-=10;
    q++;   //这里没有太看明白,嘻嘻尬笑
    }
    if(n<=-10)
     printneg(q,p);
      (*p)("0123456789"[-r]);
}
void printnum(long n,void (*p)(char)){
	if(n<0){
		(*p)('-');
	printneg(n,p);
	}else{
		printneg(-n,p);
	}
}
int main()
{
    long value=-2147483648;  //-2147483648~2147483647
    printnum(value,print);
    return 0;
}

看完《c陷阱与缺陷》,开心

gzh

微信扫码关注

更新实时通知

« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。