C 犯错总结

limits.h

要检查范围的时候,使用以下宏,很好用的,增强程序移植性,和减少bug。 要是传参时,入口函数都能有个范围判断的assert或什么的,感觉还是可以省不少事。

        CHAR_BIT char的位数(bit)

  CHAR_MAX char的十进制整数最大值
  CHAR_MIN char的十进制整数最小值
  MB_LEN_MAX 多字节字符的最大字节(byte)数
  INT_MAX int的十进制最大值
  INT_MIN int的十进制最小值
  LONG_MAX long的十进制最大值
  LONG_MIN long的十进制最小值
  SCHAR_MAX signedchar的十进制整数最大值
  SCHAR_MIN signedchar的十进制整数最小值
  SHRT_MIN short的十进制最小值
  SHRT_MAX short的十进制最大值
  UCHAR_MAX unsignedchar的十进制整数最大值
  UINT_MAX unsignedint的十进制最大值
  ULONG_MAX unsignedlongint的十进制最大值
  USHRT_MAX unsignedshortint的十进制最大值


枚举

1、枚举平日里使用的稍微少一点,很有用,只是自己使用的少,以至于弄得不怎么熟练。

enum BOOL {

TRUE = 1,

FALSE = !TRUE

};

首先要注意的是上面声明实际就是enum BOOL {TRUE = -1,FALSE = !TRUE };一个语句。所以里面隔开是用逗号,结尾要用分号。这里是声明一个名为BOOL的枚举类型

enum BOOL m_value, m_value2。是定义两个BOOL枚举类型变量,m_value m_value2。这两个变量的值只能是enum BOOL上面定义出来的TRUE 和FALSE。

当然类型名字不起也行,enum {TRUE = 1 , FALSE = !TRUE} m_value, m_value2;意思与上面是一样的。

如果这样定义出了BOOL类型,想要在函数返回值等地方使用的时候,直接写BOOL func (Type argument)是不行的。因为BOOL是枚举类型,要写为

enum BOOL func (Type argument)。觉得麻烦的话用 typedef enum BOOL BOOL; 分号结尾。语句可以定义出BOOL类型,这样的话之后用BOOL做函数返回值类型就可以了。


1、#define TRUE 1 ;

即#define后面加个 ' ; ' 。。。。这个低级错误很疼。因为如果这个宏使用的多的话(我这写个TRUE,但只是值常量,不是只针对TRUE出问题)报错的时候是漫天遍野都是错,而且不会写#define那行有问题。唉。。是低级错误,但是我犯了很多很多次了。有时候错还好找,有时候就开始浪费时间。。觉得还是要记下来。给自己提醒。

优先级

优先级问题虽然都说不记得就写(),但是我时常懒得写(),不是自己多牛,是就是手懒,有时甚至缩进我都懒得按空格。。。。。于是出了很多问题

2、!= 优先级刚好比 & 大一

有一次我写这么个语句:if ( bit & MASK != 0 ) 即看看某一个位,是否被置位。那时我就懒得写括号,因为习惯了 if ( a + b != 0 )等等语句的书写,就想当然认为这里肯定也差不多,&的优先级肯定大于!=。这种想当然根深蒂固的让我派错的时候根本就没去看这句话,一直在别处找错。浪费时间,浪费精力。。。。

用的不熟的操作符,使用之前一定要查查优先级表或者使用(),别想当然。查表或者按括号会节约很多时间。

3、断言宏assert

标准库里assert.h中的assert断言是个表达式,而非语句。其实现是用 ? : 三目运算符实现的。所以其能够在表达式能出现的地方运行。比如,胡写的,只是说明一下:

while(assert(aaabbb == cccddd))。很多时候会自己编写断言,那时候就要注意你自己写的断言是语句还是表达式,比如你是否用了if ...else。那就是语句了。用? :那就是表达式。

另外,常使用assert,那知道怎么关闭它吗?就是怎么发布release版本。在编译时加入-DNDEBUG即可。man assert可以看看。


语句与表达式

1、char *Rotate( char *str )
{
        assert(str != NULL);
    
        char *lpFinal = str;
        while( *str != '\0' ){
                while ( *str == '#' )
                        ++str;
                *lpFinal++ = *str++;
        }   
            
        *lpFinal = '\0';

        return lpFinal;
}

我写了一个函数,具体功能不用管,就是传一个字符串,形式如aaa###lll####ooo####PP。即字符串中间夹杂了很多#,函数目标是将#去除,并且在去除后的字符串末尾加上'\0',以将其做成真正的字符串,并且返回其最后指向'\0'位置的指针。

然后我写了这么个语句:key-- = Rotate(lpStart);

报错说“lvalue required as left operand of assignment”,即表达式需要个左值来作为左操作数。

这个问题我感觉比较好的地方在于,能让我感觉到左值与右值的不同。左值就像是一个内存块,是用来存东西的,而右值是这个内存块里存储的内容。

key = Rotate(lpStart);中,key为左值,表示这个标识符为key的内存块的地址,有确定的存储位置。现在要将Rotate函数返回值放入这个内存块。

key-- 是将标识符key 下存储的值,放入一个临时变量,然后将key下值减一,再返回临时变量的值。这里返回的是个值,是地址的值,不是真正的存储位置。

反正记着,-- ++ 返回的是右值,* 返回左值。

2、表达式和语句的区别

随便写个宏如下:

#define func(x)                         \
{                                       \
        if (x > 0){                     \
                x = 1;                  \
        }                               \
        else {                          \
                x = 2;                  \
        }                               \
}

完全胡乱写的,只为说明一下表达式和语句区别。再写的宏:

#define func1(x) x = x > 0 ? 1: 2
如果我在某个函数中写:

if (func(x)){

    then do something

}
必然会报错,因为这里写func的位置是只能写表达式的,而func宏是个复合语句,是语句。如果把上面函数中的if内func改为func1(x),那就能顺利执行了。因为func1宏是语句。也可以看到他们的一个小标志,就是末尾的分号。一个有分号,一个没有。

这个有什么用呢?我自己也说不上,就感觉在写宏的时候要小心,容易出bug。写成语句的宏,别用在该用表达式的地方,写成表达式的宏,别用在该用语句的地方。

比如assert。标准库中的assert是?:操作符实现的表达式,它可以放在if 里面等等地方,而有时候,你自己可能因为各种需要,要自己写一个ASSERT(大写哦),比如你并不想让程序出错后停止,想让它打印出错,然后继续执行什么的。自己写的ASSERT如果写为语句,那么使用的时候要注意场合。


数组、指针

1、数组作为形参时,自动转换为指针。

void function( int array[])
{
       sizeof(array);
       ...........
}

void function( int *array)
{
         sizeof(array);
         .....
}

两种声明方式完全一样,这里的完全是指在编译器级别都是一样的。数组array【】自动转换为指针,并且这里sizeof操作符返回的值都是4,表示的是指针的大小。如果对一个数组做sizeof,比如

int array[10];

sizeof(array);

返回的sizeof操作符值是40,是整个数组的大小。

那么这样声明呢:

void function( int array[10])
{
       sizeof(array);
       .......................
}
这样声明形参它是不是就不转换为指针了呢。不是的。这里array返回值还是4,表示对于这个函数来说,数组名array还是自动转换为了指针。那个10据我自己测试,没有任何效力,编译器不给任何warnning。gcc -Wall显示:statement with no effect。那个10就是给写函数的人自己看的,他自己认为自己写的这个函数用的数组大小就是10,你传个array[20],从10以后的元素我不使用,传个array[5],那我就越界的访问5以后的元素。
所以这样写没有意义。

2、形参为指向const变量的指针,在函数内赋值给一个新变量。

在写字符串相关函数时,比如char *my_strcpy( char *dest, const char *src)一般指导下都要将原字符串声明为const。但有时候,函数中可能需要将src指向的位置复制一个副本。比如,我想对整个字符串进行操作,然后操作到末尾后,还要回来,再循环一遍字符串做别的操作。这时可能会写:

char *lpStart = src;

至少我时常这么干。此时要将lpStart也声明为const char *才比较好,不然会失去const约束力,而且也不严谨。

3、if内判断语句的先后顺序

看到书上一道题:

if ( array[ which ] == 5 && which <SIZE )  .................

if ( which < SIZE && array[ which] == 5) ...............

上述两个语句哪个比较好呢?array就是个数组,SIZE那么大。用which循环数组。

我在看到这个题之前,我从来都是乱写的,顺序的问题不管。后来发现,第二个要好,短路求值,先去判断是否越界,再解引用看当前指向的元素是否为5.要先判断元素值的话,万一array[which]指向个不该指的位置,解引用会出问题。

4、int array[10][20];

...

i = array[3 , 4];这个表达式什么时候可能是正确的呢?考虑一下再往后看。。。当i 声明为 i * tni 时候。。。

5、函数返回指针,不能返回成局部变量的指针了。

char * get_string(void)

{

char string[] = "hello";

return string;

}

char *get_string(void)

{

char *string = "hello";

return string

}

两函数长得很像,一个返回的指针能够直接输出hello,另一个不行。虽然你认真一下会知道咋回事,但是写程序的时候这种问题还是会出现,脑袋稍微跑毛就从手底下溜出来了。但是咱不怕错,关键在于写完以后,检查的时候一定要想起来。这也是我写这总结的目的。


结构与联合

1、union {

                  int a;

                  float b;

                  char c[4];


} x = {19};

union的初始化,只能初始化第一个成员,这里写个3.14或者'c'什么的都会自动转化为int去初始化x。小心。

2、struct S{

int data;

char * str;

};

假如现在新建struct,然后要初始化或说是赋值。可以struct S aStruct;新建,然后aStruct.data = 2; aStruct.str = "aaaaa";(这里是个坑)。

这样赋值肯定是可以的,但是不方便,要连续赋值或说初始化时,用 aStruct = {.data = 2, .str = "aaaaa"};,方便很多。但是这个需要编译器支持。不过现在看得似乎都支持。

3、typedef struct {

int x;

int y;

}Point;

Point x, y;

x.x = 1;

x.y  = 2;

x = y;

这么直接赋值是可以的。不只是C++(C++基本将struct当class,调用默认拷贝函数)。


链接

1、强弱符号问题

假如有两个文件,一个叫fileNo1.c,另一个叫fileNo2.c,然后里面都有个全局变量叫做g_universData;

fileNo1.c这样定义:int g_universData = 1;

fileNo2.c这样定义:int g_universData = 2;

此时,因为两个同名的都是全局变脸,且都初始化了,所以都是强类型,都在.data段中。链接两个文件时会报命名冲突,不过链接。

如果fileNo2.c改成这样:int g_universData;

不进行初始化,那么fileNo2.c中的这个变量就是弱类型了,虽然会出现重名,但是可以过链接,不报任何问题。在编译、汇编fileNo2.c为可重定向目标文件后,链接为可执行目标文件前fileNo2.c中的g_universData属于 COMMON块,不在.bss段中,与fileNo1.c链接时,其会检查flieNo1.c中有没有同名的g_universData,且还要看其是不是强类型,是强类型,以强类型为准,放入.data段,从此可执行目标文件中,g_universData这个符号只对应fileNo1.c中的符号,fileNo2.c中的被忽略。如果fileNo1.c中的不是强类型,是个弱类型。也就是说其在fileNo1.c中也是个COMMON块,此时就要比较fileNo1.c 和fileNo2.c两个文件中,这个同名为g_universData的两个COMMON块谁更大,谁大,以谁为准,放入.bss段(因为弱符号未初始化)。

神奇的是,如果fileNo1.c中的g_universData初始化过,即为强类型。且fileNo2.c中的同名g_universData未初始化,但它这样写:double g_universData;

此时fileNo2.c汇编完变成可重定向目标文件后,其COMMON块大小为8(只对g_universData来说),比强类型fileNo1.c中的大。此时fileNo1.c fileNo2.c链接后,会舍弃fileNo1.c中的这个强类型的g_universData,而使用double的g_universData放入.bss。这个估计和编译器还是有关系的。会报warnning。

注意,函数的声明为static的局部变量会直接放入.data或者.bss不会和别的文件重名变量出冲突。其在.data或.bss中不直接就是变量的名字。而是会假如一些函数相关信息,像C++中文件名修饰那样,对符号进行重新修饰后,放入相应段,以做区分。


库函数

1、realloc函数。之前我完全没有想到这是个设计的如此不好的函数。看下面语句:

pbBuf = (int *)realloc(pbBuf, sizeNew);
if (pbBuf != NULL){
        then we can use the pbBuf
}

简单明了,但是里面有bug。仔细看看。

如果pbBuf指向了一块内存,且仅有这一个指针指向这块内存,那么会出现什么?

当realloc失败的时候,返回NULL,扔给pbBuf。从此,pbBUF之前指向的那块内存再没人能访问到。内存泄漏了。非常容易出错的地方。现在我对自己的总结就是,永远不要小看一个  Bug。当你嘲笑一个bug简单,觉得怎么会有人这么蠢,犯这么低级的错误的时候,一定要制止自己这样想。真的是指不定哪天,一个奇怪的,你如此瞧不起的bug,就会出现在你写的code里,mocking you just like you did。一定小心小心再小心。没有错误能称得上低级,都是高级错误,都很重要。

realloc函数讨厌的地方在于,它管的太宽了,给他传NULL也行(作用与malloc相同),sizeNew那里传0也行(作用又相当于free了),两个完全相反的功能它都干了。只有两个参数同时为NULL 和0时,结果才未定义,也就是说,这一个函数干了malloc free还有它自己,三个功能,甚至要细分的话还能找到更多。。。。所以提醒一下我自己,写函数的时候一定要KISS当先,不要写大函数,管好自己最重要。

参看《编程精粹(writing solid code)》英文版 p91



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值