12
2020
08

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

1.在c语言中,符号之间的空白(包括空格符,制表符或换行符)将被忽略

if(x>big) big=x;

还可以写出:

if(x>big) 
    big=x;

2.编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号

如:y=x/*p      /*p指向除数*/;

被编译器理解成

y=x        /*p      /*p指向除数*/;//后面一整块变成了注释,原意只是后面一小块成为注释

3.字符与字符串

用单引号引起的一个字符实际上代表一个整数

用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为零的字符'\0'初始化

4.某些c编译器对函数参数并不进行类型检查,特别是对printf函数的参数

如:printf('\n');

则会在程序运行的时候产生难以预料的错误,而不会给出编译器诊断信息  ps:我试了cfree和DEV-C++他们都会编译不通过,所以现在的编译器一般能够检测到在函数调用时混用单引号和双引号的情形

5,整型数(一般为16位或32位)的存储空间可以容纳多个字符(一般为8位),因此有的c编译器允许在一个字符常量(以及字符串常量)中包括多个字符。

#include<stdio.h>
int main(){
    int a;
    a='yes';
    printf("%d",a);
    return 0;
}


所以这个编译是成功的

注释嵌套

例子:

/*/*/0*/**/1

如果编译器允许嵌套注释,则上式将被解释为

/*   /*   /0   */   *   */   1,值为1

如果编译器不允许嵌套注释,则上式将被解释为

/*   /    */    0*    /**/     1,值为0*1=0


但是c语言定义并不允许嵌套注释,因此一个完整遵守c语言标准的编译器就别无其他选择了

理解函数声明

先了解一个知识点:c变量的声明都由两部分组成,类型以及一组类似表达式的声明符。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。

float *pf;//这个声明的含义是*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针

float *g(),(*h)();//*g(),(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()):g是一个函数,该函数的返回值类型为指向浮点数的指针。h所指向函数的返回值为浮点类型

2.一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可
eg:float (*h)();---->(float (*)())

void show(void);
int (*fun)(int);--->去掉变量名和分号就变成int (*)(int)
把show强制转换为fun类型---》(int (*)(int))show
#include<stdio.h>
int max(int a,int b){

return a>b?a:b;
}

int (*pfun)(int,int);

void main(){
printf("max value=%d\n",max(1,2));

pfun=&max;//标准写法
printf("max value=%d\n",(*pfun)(10,20));

pfun=max;//也是对的
printf("max value=%d\n",pfun(10,20));

}

3.如果一个函数不需要返回值(即返回值为void),我们经常在函数声明时省略了返回值类型,但是此时对编译器而言会隐含地将函数返回值类型视作int类型

4,c语言允许初始化列表中出现多余的逗号

eg:    int a[]={1,2,3,4,5,6}; 
   因为int a[]={
          1,2,3,
          4,5,6,
};
#include <stdio.h>
int Max(int a,int b){
return a>b?a:b;
}
typedef int(*HANDLER)(int,int);
HANDLER Fun(int a,int b,HANDLER pfun){
printf("Max value=%d\n",pfun(a,b));
return pfun;
}

void main(){
Fun(10,20,Max);
}

2.2运算符优先级:不用背那些优先级,在用的时候根据自己的意思加括号就行

#include<stdio.h>
void main(){
  char hi=3;   //0000 0011
  char low=4; //0000 0100
  char r;
//  r=hi<<4+low;这样是错误的,因为优选级问题达不到自己想要的结果,下面是解决办法
r=(hi<<4)+low;//第一种办法,按自己的想法加括号
r=hi<<4|low;//第二种办法,就是把加号变成|,因为hi移位之后,后四位都是0,所以|和+是一个意思

}

2.3结束标记分号陷阱

用分号的时候要注意

#include<stdio.h>
void main(){
int a=10;
int big=100;
if(a>big);//此处加了个分号就得到错误的结果
a=big;
printf("a=%d\n",a);
}
#include <stdio.h>
struct logrec{
	int date;
	int time;
	int code;
};//这里如果少了一个分号,那就意味着main函数的返回值是一个结构体+void,但是这种类型不存在(类比:unsigned int a;两个类型组合)
func();//如果结构体那里忘记写分号,那就意味着func()函数的返回值是结构体类型               
void main(){
	logrec log;
	int ar[]={1,2,3};
	int n=sizeof(ar)/sizeof(int);
	if(n<3)
	return   //这个地方不加分号就意味着return log.date=ar[0];,但是这个程序是void没有返回值的,所以就会报错
	log.date=ar[0];
	log.time=ar[1];
	log.code=ar[2];
	
}
还有一个就是for循环的分号的注意,这里就不举例子了


2.4switch case语句陷阱,case后面跟的只能是整型或者表达式(不能是字符串,浮点型)

switch(color){
case 1:printf("red");
break;//记得case和break搭配,但是特殊情况可以不用,灵活运用
}


switch(){ //当1,2,3,4的结果都是一样的时候,可以像下面这样简单的写
case 1:
case 2:
case 3:
case 4: 
       printf("xx");
       break;
case 5:
case 6:
    printf("xxx");       
    break;
}

2.6悬挂else陷阱

else始终与同一对括号内最近的未匹配的if结合

3-1指针和数组

· 数组名代表整个数组空间,而不是数组的首地址,但是数值名的值和首元素的地址的值是一样的,所以两者是等价的

· 对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针

void main(){
    int ar[10]={0};
    printf("%d\n",sizeof(ar));//答案是40,因为ar代表的是整个数组空间,而不是一个地址
   
    printf("%p\n",&ar[0]);
    printf("%p\n",ar);   //这三者打印出来的值都一样,但是代表的意思都不同
     printf("%p\n",&ar);
      
    int *p=&ar[0];
    int *p1=ar;
    int *p2=&ar;//这个是错的,因为&ar代表的意思是数组指针,所以应该像下面这样写
    int (*p2)[10]=&ar;//这个才是对的
}

· ar[0]=10这种写法经常见,但是写成0[ar]=10这样既然也是对的,为什么呢

因为
ar[0]=10;
//会被解释成*(ar+0)=10 因为加法的交换律所以也等价于*(0+ar)=10 也就是0[ar]
//即*(ar+0)=10 ==> *(0+ar)=10 ==> 0[ar],所以上面那种写法是对的
void main(){
int br[10];
int *p=br;//br的值和首元素地址的值相同,首元素是单个整型的地址
int ar[3][4]={0};
int (*p1)[4]=ar;//ar的值也和首元素地址的值相同,但是二维数组的首元素是一个数组,所以要用数组指针
}
void main(){
int ar[3][4];
ar[1][1]=10;
*(*(ar+1)+1)=100;
}

3-2非数组的指针陷阱

void main(){
    char *r;
    char *s="hello";
    char *t="world";
    
    strcpy(r,s);//r hello
    strcat(r,t);//helloword
    printf("r=%s\n",r);
    
}
//虽然编译通过,但是运行时出现错误,因为r没有被分配指向某个可用空间,r这个变量虽然被分配了空间
但是指针r指向的空间并没有被分配

进行改进,如下

void main(){
   char r[200];
    char *s="hello";
    char *t="world";
    
    strcpy(r,s);//r hello
    strcat(r,t);//helloword
    printf("r=%s\n",r);
}
//这样是可以通过,并且r也被分配了空间,但是当字符串很大的时候,数组空间就容易出现内存泄漏的问题
所以我们最好就是需要多少空间就分配多少空间,使用malloc函数动态分配内存

进行改进,如下

void main(){
    char *s="hello";
    char *t="world";
    char *r=(char *)malloc(strlen(s)+strlen(t));
    
    strcpy(r,s);//r hello
    strcat(r,t);//helloword
    printf("r=%s\n",r);

}
//这样是存在错误的,只是巧合运行成功,因为字符串一般都是\0结尾,那么r应该申请11个空间才能存放那些数据,既然空间不够,为啥程序还运行成功呢
因为可能刚刚巧合申请的r空间后面还有个空闲空间给你存放\0,当那个空间不是空间的时候,程序就出现错误了

进行改进,如下

void main(){
    char *s="hello";
    char *t="world";
    char *r=(char *)malloc(strlen(s)+strlen(t)+1);//把\0需要的空间补上
    if(NULL==r){
    printf("out of memory!\n");
    exit(1);
    }
    strcpy(r,s);//r hello
    strcat(r,t);//helloword
    printf("r=%s\n",r);
    free(r);//手动释放,要不然会造成内存泄漏
}

另个例子

struct Student{
char *name;
int age;
char sex[3];
};
 
void main(){
    Student s;
    s.name=(char *)malloc(strlen("小小")+1);
    if(NULL==s.name){
    exit(1);
    }
    strcpy(s.name,"小小");
    s.age=22;
    strcpy(s.sex,"女");
    free(s.name);
}

3-3作为参数的数组声明陷阱

在c语言中,我们没有办法可以将一个数组作为函数参数直接传递。如果我们使用数组名作为参数,那么数组名会立刻被转换为指向该数组第一个元素的指针

1.第一个陷阱

void main(){
int ar[10]={1,2,3,4};
int br[10];
br=ar;//这里是错的,ar只是个整型的值来着,不能相互赋值
}

2.第二个陷阱

void main(){
    int ar[10]={1,2,3,4};
    printf("%p\n",ar);//打印出来的是个地址的值
    char br[]="hello";
    printf("%s\n",br);
    printf("%s\n",&br[0]);//这两个都打印的是字符串,因为br和&br[0]的值都等于首地址的值
    }
//为啥整型的数组打印数组名就是地址,而字符型的数组打印出来的却是值呢 
因为 整型的数组没有结束符,不知道哪里才结束,固然打印不出来,所以打印了地址的值,而字符串有结束符,so能打印出值来

3.第三个陷阱

//也可以这样写void fun(char *ar)
              void fun(char ar[]){
            printf("at fun size =%d\n",sizeof(ar));//4,因为ar是地址来着
                }
//void main(int argc,char **argv)  两个是相同的              
//void main(int argc,char *argv[])//argc表示个数,argv表示值
void main(){
char br[]="hello world";
printf("length=%d\n",strlen(br));//打印的是多效长度,11
printf("size=%d\n",sizeof(br)); //打印真实长度,12
fun(br);//传递的是地址,不能把整个数组传过去,因为这样时间空间都不允许
}

3-4避免举隅法陷阱

void main(){
    char *p="hello";
    printf("%s\n",p);
    p[0]='H';//这个是错误的
}
//首先要知道,char *p="hello"系统会分配两个空间,第一个是给p变量分配个空间,第二个是分配常量区给“hello”,
,那么把h的地址放到p里面,这样p就能访问hello了,因为hello是存放在常量区,所以不能被修改
下面这个程序一样不能被修改
void fun(char *str){
str[0]='H';
}
void main(){
    char *p="hello";
    printf("%s\n",p);
   fun(p);
}
//一样是错误的,同样的错误

下面这样就对了

void fun(char *str){
str[0]='H';
}
void main(){
   char p[]="hello";
   // char *p="hello";
    printf("%s\n",p);
   fun(p);
}
这个为啥就是正确的呢,因为内存分配的空间不一样,数组分配的数组是在栈区,在栈区的内容可以被修改


void main(){
char *p="hello";
char *q=p;
}
//这里要注意的地方是,q不是指向p,而是p的内容赋值给q,这个例子中,p的值就是字符串hello的首地址,所以就是
q的值也是字符串hello的首地址,在这里的q变量,系统也会分配空间给q,总之就是q和p都是指向字符串hello的首地址
void main(){

char *s=(char *)malloc(sizeof(char)*10);
char *t=s;
free(t);
//free(s);
}
//因为s和t指向的都是同一个空间,所以只要其中一个释放就行,因为释放的都是同一个空间

想看”C/C++编译的程序占用的内存的划分---分别属于什么区”就点击下面的链接了解

https://www.1949la.com/post/10422.html

3-5空指针并非空字符串

编译器保证由0转换而来的指针不等于任何有效的指针,无论直接用常数0,还是用符号NULL,效果都是相同的

#define NULL    0
//其实在我们编译器中,null的定义就是0,所以int *p=NULL和int *p=0是一样的
int *p=NULL;
int *p=0;//这两个是一样的
//p并不是代表指向地址为0位置的空间,而是不指向任何空间

当常数0被转换为指针使用时,这个指针绝对不能被解除引用

void main(){
    int *p=NULL;
    printf("%d\n",*p);//错误的
}
//因为p是null,说明p并没有指向哪里,那它哪里来的解引用
void main(){
    int a=10;
    char *p=NULL;
    printf("%s\n",p);//打印出来的是(null)或者什么都不打印
}
打印出(null)并不代表是空字符串,和空字符串是两回事,看下面的解释
void main(){
    char *p=NULL;//说明p是无指向的
    char *q="";//这个是空字符串,说明里面还有个\0,也说明了指针q是有指向的
    printf("plen=%d\n",strlen(p));//这句是错误的,因为p没有指向任何地方
     printf("qlen=%d\n",strlen(p));//答案是0
}

3-6边界计算与不对称边界陷阱

void main(){
    int i;
    int ar[10];
    for(i=0;i<10;i++){  //建议用不对称边界[ ),这样用上界-下界就可以等于真实的容量,而不用去考虑加1那些
        ar[i]=0;
        printf("ar[%d]=%d\n",i,ar[i]);
    }
}
#define N 10
char buffer[N+1];
char *bufptr;

void flushbuffer(){
    printf("%s",buffer);
    bufptr =buffer;
}

void bufwrite(char *p,int n){
bufptr =buffer;
while(n-- >=0){
if(bufptr == &buffer[N]){
flushbuffer();
*bufptr++ = *p++;//这里就很好的体现了不对称边界的好处
}
}

void main(){
    char *str="xiaoxiaoxiaoxiao";
    bufwrite(str,strlen(str));
    flushbuffer();
    printf("\n");
}


《c陷阱与缺陷》第二篇:https://www.1949la.com/post/10429.html

« 上一篇 下一篇 »

评论列表:

1.xialibing  2020-08-12 23:13:53 回复该评论
文章太长了 你再开一篇 哈哈
2.xialibing  2020-05-25 00:47:47 回复该评论
这是沙
2.xialibing  2020-06-28 23:37:59 回复该评论
这本书挺不错的,我也看过一点
3.访客  2019-10-22 04:49:22 回复该评论
挑战学习

发表评论:

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