【Markdown】笔记,大部分有价值的点
这篇书评可能有关键情节透露
避免低级错误
---------------
1. `if(flight == 063)`。这里程序员的本意是对63 号航班进行测试,但因为前面多了一个0 使063 成了八进制数。结果变成对51 号航班进行测试。
2. `If(pb != NULL & pb != 0xff)`。这里不小心把&&键入为&,结果即使pb 等于NULL 还会执行*pb != 0xff。
3. `quot = numer/*pdenom`。这里无意间多了个*号结果使`/*`被解释为注释的开始。
4. `word = bHigh<<8 + bLow`。由于出现了运算优先级错误,该语句被解释成了:`word = bHigh << (8+bLow)`
5. C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX 定义在limit.h中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX 替换成0:
```
……
#if UINT_MAX > 65535u
……
#endif
```
低级错误 详细例子
---------------
1. 这样一个代码:`strCopy = memcpy(malloc(length), str, length);`
该语句在多数情况下都会工作得很好,除非malloc 的调用产生失败。当malloc 失败时,
就会给memcpy 返回一个NULL 指针。由于memcpy 处理不了NULL 指针,所以出现了错误。如
果你很走运,在交付之前这个错误导致程序的瘫痪,从而暴露出来。但是如果你不走运,没
有及时地发现这个错误,那某位顾客就一定会“走运”了。
编译程序查不出这种或其他类似的错误。同样,编译程序也查不出算法的错误,无法验
证程序员所作的假定。或者更一般地,编译程序也查不出所传递的参数是否有效。
寻找这种错误非常艰苦,只有技术非常高的程序员或者测试者才能将它们根除并且不会
引起其他的问题。
然而假如你知道应该怎样去做的话,自动寻找这种错误就变得很容易了。
让我们直接进入memcpy,看看怎样才能查出上面的错误。最初的解决办法是使memcpy
对NULL 指针进行检查,如果指针为NULL,就给出一条错误信息,并中止memcpy 的执行。
下面是这种解法对应的程序。
```
/* memcpy ─── 拷贝不重叠的内存块 */
void memcpy(void* pvTo, void* pvFrom, size_t size)
{
void* pbTo = (byte*)pvTo;
void* pbFrom = (byte*)pvFrom;
if(pvTo == NULL | | pvFrom == NULL)
{
fprintf(stderr, “Bad args in memcpy\n”);
abort();
}
while(size-->0)
*pbTo++ == *pbFrom++;
return(pvTo);
}
```
只要调用时错用了NULL 指针,这个函数就会查出来。所存在的唯一问题是其中的测试
代码使整个函数的大小增加了一倍,并且降低了该函数的执行速度。如果说这是“越治病越
糟”,确实有理,因为它一点不实用。要解决这个问题需要利用C 的预处理程序。
如果保存两个版本怎么样?一个整洁快速用于程序的交付;另一个臃肿缓慢件(因为包
括了额外的检查),用于调试。这样就得同时维护同一程序的两个版本,并利用C 的预处理
程序有条件地包含或不包含相应的检查部分。
```
#ifdef DEBUG
void _Assert(char* , unsigned); /* 原型 */
#define ASSERT(f) \
if(f) \
NULL; \
else \
_Assert(__FILE__ , __LINE__)
#else
#define ASSERT(f) NULL
#endif
```
2. 如果读者停下来读读 ANSI C 中memcpy 函数的定义,就会看到其最后一行说:“如果在
存储空间相互重叠的对象之间进行了拷贝,其结果无定义
`/* 内存块重叠吗?如果重叠,就使用memmove */`
`ASSERT(pbTo>=pbFrom+size || pbFrom>=pbTo+size);`
3. 消除所做的隐式假定
最近,Microsoft 的一些小组渐渐发现他们不得不对其代码进行重新的考察和整理,因
为相当多的代码中充满了“+2”而不是“+sizeof(int)”、与0xFFFF 而不是UINT_MAX 进行
无符号数的比较、在数据结构中使用的是int 而不是真正想用的16 位数据类型这一类问题。
你也许会认为这是因为这些程序员太懒散,但他们却不会同意这一看法。事实上,他们
认为有很好的理由说明他们可以安全地使用“+2”这种形式,即相应的C 编译程序是由
Microsoft 自己编写的。这一点给程序员造成了安全的假象,正如几年前一位程序员所说:
“编译程序组从来没有做使我们所有程序垮掉的改变”。
但这位程序员错了。
为了在Intel 80386 和更新的处理器上生成更快更小的程序,编译程序组改变了int
的大小(以及其他一些方面)。虽然编译程序组并不想使公司内部的代码垮掉,但是保持在
市场上的竞争地位显然更重要。毕竟,这是那些自己做了错误假定的Microsoft 程序员的过
错。
4. 为某一数据结构分配内存时, 忘了初始化, 或当维护人员扩展该数据结构时, 忘了为新增的域编写相应的初始化代码.
不管怎样,我们还是不希望所分配内存块的内容无定义,因为这样会使错误难以再现。
那么如果只有当所分配内存块中的无用信息碰巧是某个特定值时才出错,会产生什么样的结
果呢?这就会在大部分的时间内发现不了错误,而程序却会由于不明显的原因不断地失败、
我们可以想象一下,如果每个错误都是在某个特定的时刻才发生,要排除程序中的所有错误
会多难。要是这样,程序(和测试人员)非发疯不可。暴露错误的关键是消除错误发生的随
机性。
确实,如何做到这一点要取决于具体的子系统及其所涉及到的随机特性。但对于malloc
来说,通过对其所分配的内存块进行填充,就可以消除其随机性。当然,这种填充只应该用
在程序的调试版本中。这样既可以解决问题,又不影响程序的发行代码。然而必须记住,我
们不希望隐瞒错误,所以用来填充内存块的值应该离奇得看起来象是无用的信息,但又应该
能够使错误暴露。
```
#define bGarbage 0xA3
flag fNewMemory(void** ppv, size_t size)
{
byte** ppb = (byte**)ppv;
ASSERT(ppv!=NULL && size!=0);
*ppb = (byte*)malloc(size);
#ifdef DEBUG
{
if( *ppb != NULL )
memset(*ppb, bGarbage, size);
}
#endif
return(*ppb != NULL);
}
```
5. 如果释放内存之后,内存指针未清除呢?
```
void FreeMemory(void* pv)
{
ASSERT(pv != NULL);
#ifdef DEBUG
{
memset(pv, bGarbage, sizeofBlock(pv) );
}
#endif
free(pv);
}
```
6. 你可能有用过realloc?
realloc 改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新
块和老块长度的最小长度之间得到保留。
?? 如果该内存块的新长度小于老长度,realloc 释放该块尾部不再想要的内存空间,
返回的pv 不变。
?? 如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址处,
该块的原有内容被拷贝到新的位置。返回的指针指向扩大后的内存块,并且该块扩
大部分的内容未经初始化。
?? 如果满足不了扩大内存块的请求,realloc 返回NULL,当缩小内存块时,realloc
总会成功。
?? 如果 pv 为NULL,那么realloc 的作用相当于调用malloc(size),并返回指向新分
配内存块的指针,或者在该请求无法满足时返回NULL。
?? 如果 pv 不是NULL,但新的块长为零,那么realloc 的作用相当于调用free(pv)
并且总是返回NULL。
?? 如果 pv 为NULL 且当前的内存块长为零,结果无定义
7.dummy header 链表
8. memchr是否存在问题
```
void* memchr( void *pv, unsigned char ch, size_t size )
{
unsigned char *pch = ( unsigned char * )pv;
unsigned char *pchEnd = pch + size;
while( pch < pchEnd )
{
if( *pch == ch )
return ( pch );
pch ++ ;
}
return( NULL );
}
```
注意:
```
pchEnd = pch + size;
while( pch < pchEnd )
```
正确做法
```
void *memchr( void *pv, unsigned char ch, size_t size )
{
unsigned char *pch = ( unsigned char * )pv;
while( size -- > 0 )
{
if( *pch == ch )
return( pch );
pch ++;
}
return( NULL );
}
```
9. 它们有什么不同?
char chGetNext(void)
{
int ch; /* ch“必须”是int类型 */
ch = getchar();
return(chRemapChar(ch));
}
char chGetNext(void)
{
return( chRemapChar(getchar()) );
}
---------------
1. `if(flight == 063)`。这里程序员的本意是对63 号航班进行测试,但因为前面多了一个0 使063 成了八进制数。结果变成对51 号航班进行测试。
2. `If(pb != NULL & pb != 0xff)`。这里不小心把&&键入为&,结果即使pb 等于NULL 还会执行*pb != 0xff。
3. `quot = numer/*pdenom`。这里无意间多了个*号结果使`/*`被解释为注释的开始。
4. `word = bHigh<<8 + bLow`。由于出现了运算优先级错误,该语句被解释成了:`word = bHigh << (8+bLow)`
5. C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX 定义在limit.h中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX 替换成0:
```
……
#if UINT_MAX > 65535u
……
#endif
```
低级错误 详细例子
---------------
1. 这样一个代码:`strCopy = memcpy(malloc(length), str, length);`
该语句在多数情况下都会工作得很好,除非malloc 的调用产生失败。当malloc 失败时,
就会给memcpy 返回一个NULL 指针。由于memcpy 处理不了NULL 指针,所以出现了错误。如
果你很走运,在交付之前这个错误导致程序的瘫痪,从而暴露出来。但是如果你不走运,没
有及时地发现这个错误,那某位顾客就一定会“走运”了。
编译程序查不出这种或其他类似的错误。同样,编译程序也查不出算法的错误,无法验
证程序员所作的假定。或者更一般地,编译程序也查不出所传递的参数是否有效。
寻找这种错误非常艰苦,只有技术非常高的程序员或者测试者才能将它们根除并且不会
引起其他的问题。
然而假如你知道应该怎样去做的话,自动寻找这种错误就变得很容易了。
让我们直接进入memcpy,看看怎样才能查出上面的错误。最初的解决办法是使memcpy
对NULL 指针进行检查,如果指针为NULL,就给出一条错误信息,并中止memcpy 的执行。
下面是这种解法对应的程序。
```
/* memcpy ─── 拷贝不重叠的内存块 */
void memcpy(void* pvTo, void* pvFrom, size_t size)
{
void* pbTo = (byte*)pvTo;
void* pbFrom = (byte*)pvFrom;
if(pvTo == NULL | | pvFrom == NULL)
{
fprintf(stderr, “Bad args in memcpy\n”);
abort();
}
while(size-->0)
*pbTo++ == *pbFrom++;
return(pvTo);
}
```
只要调用时错用了NULL 指针,这个函数就会查出来。所存在的唯一问题是其中的测试
代码使整个函数的大小增加了一倍,并且降低了该函数的执行速度。如果说这是“越治病越
糟”,确实有理,因为它一点不实用。要解决这个问题需要利用C 的预处理程序。
如果保存两个版本怎么样?一个整洁快速用于程序的交付;另一个臃肿缓慢件(因为包
括了额外的检查),用于调试。这样就得同时维护同一程序的两个版本,并利用C 的预处理
程序有条件地包含或不包含相应的检查部分。
```
#ifdef DEBUG
void _Assert(char* , unsigned); /* 原型 */
#define ASSERT(f) \
if(f) \
NULL; \
else \
_Assert(__FILE__ , __LINE__)
#else
#define ASSERT(f) NULL
#endif
```
2. 如果读者停下来读读 ANSI C 中memcpy 函数的定义,就会看到其最后一行说:“如果在
存储空间相互重叠的对象之间进行了拷贝,其结果无定义
`/* 内存块重叠吗?如果重叠,就使用memmove */`
`ASSERT(pbTo>=pbFrom+size || pbFrom>=pbTo+size);`
3. 消除所做的隐式假定
最近,Microsoft 的一些小组渐渐发现他们不得不对其代码进行重新的考察和整理,因
为相当多的代码中充满了“+2”而不是“+sizeof(int)”、与0xFFFF 而不是UINT_MAX 进行
无符号数的比较、在数据结构中使用的是int 而不是真正想用的16 位数据类型这一类问题。
你也许会认为这是因为这些程序员太懒散,但他们却不会同意这一看法。事实上,他们
认为有很好的理由说明他们可以安全地使用“+2”这种形式,即相应的C 编译程序是由
Microsoft 自己编写的。这一点给程序员造成了安全的假象,正如几年前一位程序员所说:
“编译程序组从来没有做使我们所有程序垮掉的改变”。
但这位程序员错了。
为了在Intel 80386 和更新的处理器上生成更快更小的程序,编译程序组改变了int
的大小(以及其他一些方面)。虽然编译程序组并不想使公司内部的代码垮掉,但是保持在
市场上的竞争地位显然更重要。毕竟,这是那些自己做了错误假定的Microsoft 程序员的过
错。
4. 为某一数据结构分配内存时, 忘了初始化, 或当维护人员扩展该数据结构时, 忘了为新增的域编写相应的初始化代码.
不管怎样,我们还是不希望所分配内存块的内容无定义,因为这样会使错误难以再现。
那么如果只有当所分配内存块中的无用信息碰巧是某个特定值时才出错,会产生什么样的结
果呢?这就会在大部分的时间内发现不了错误,而程序却会由于不明显的原因不断地失败、
我们可以想象一下,如果每个错误都是在某个特定的时刻才发生,要排除程序中的所有错误
会多难。要是这样,程序(和测试人员)非发疯不可。暴露错误的关键是消除错误发生的随
机性。
确实,如何做到这一点要取决于具体的子系统及其所涉及到的随机特性。但对于malloc
来说,通过对其所分配的内存块进行填充,就可以消除其随机性。当然,这种填充只应该用
在程序的调试版本中。这样既可以解决问题,又不影响程序的发行代码。然而必须记住,我
们不希望隐瞒错误,所以用来填充内存块的值应该离奇得看起来象是无用的信息,但又应该
能够使错误暴露。
```
#define bGarbage 0xA3
flag fNewMemory(void** ppv, size_t size)
{
byte** ppb = (byte**)ppv;
ASSERT(ppv!=NULL && size!=0);
*ppb = (byte*)malloc(size);
#ifdef DEBUG
{
if( *ppb != NULL )
memset(*ppb, bGarbage, size);
}
#endif
return(*ppb != NULL);
}
```
5. 如果释放内存之后,内存指针未清除呢?
```
void FreeMemory(void* pv)
{
ASSERT(pv != NULL);
#ifdef DEBUG
{
memset(pv, bGarbage, sizeofBlock(pv) );
}
#endif
free(pv);
}
```
6. 你可能有用过realloc?
realloc 改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新
块和老块长度的最小长度之间得到保留。
?? 如果该内存块的新长度小于老长度,realloc 释放该块尾部不再想要的内存空间,
返回的pv 不变。
?? 如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址处,
该块的原有内容被拷贝到新的位置。返回的指针指向扩大后的内存块,并且该块扩
大部分的内容未经初始化。
?? 如果满足不了扩大内存块的请求,realloc 返回NULL,当缩小内存块时,realloc
总会成功。
?? 如果 pv 为NULL,那么realloc 的作用相当于调用malloc(size),并返回指向新分
配内存块的指针,或者在该请求无法满足时返回NULL。
?? 如果 pv 不是NULL,但新的块长为零,那么realloc 的作用相当于调用free(pv)
并且总是返回NULL。
?? 如果 pv 为NULL 且当前的内存块长为零,结果无定义
7.dummy header 链表
8. memchr是否存在问题
```
void* memchr( void *pv, unsigned char ch, size_t size )
{
unsigned char *pch = ( unsigned char * )pv;
unsigned char *pchEnd = pch + size;
while( pch < pchEnd )
{
if( *pch == ch )
return ( pch );
pch ++ ;
}
return( NULL );
}
```
注意:
```
pchEnd = pch + size;
while( pch < pchEnd )
```
正确做法
```
void *memchr( void *pv, unsigned char ch, size_t size )
{
unsigned char *pch = ( unsigned char * )pv;
while( size -- > 0 )
{
if( *pch == ch )
return( pch );
pch ++;
}
return( NULL );
}
```
9. 它们有什么不同?
char chGetNext(void)
{
int ch; /* ch“必须”是int类型 */
ch = getchar();
return(chRemapChar(ch));
}
char chGetNext(void)
{
return( chRemapChar(getchar()) );
}