这里讨论C语言标准库中各类常用函数,以及它们的高危情况。
1、atoi 函数
这个函数是转换输入字符串转换为整型数。
对于该函数的实现需要考虑以下几个方面:
- 输入字符串为NULL;
- 输入的字符包含前导的空格;
- 输入开始是否包含符号‘+’、‘-’;
- 输入的字符是否合法(对于十进制‘0’~‘9’为合法的输入);
- 计算出的数值为 long int,足够判断溢出;
- 数据溢出的处理(上溢出时,返回最大正数;下溢出时,返回最大负数);
上面的实现比较棘手的就是数据溢出的处理:这里我们用计算出的数值与最大值(最小值的无符号型)/10 进行比较,小于自然不会溢出,由于负数的最大值是-2147483648,最大值是2147483647,个位数不是9,所以还需考虑等于的情况下,个位数的比较。
将计算出的数值与最大值(最小值的无符号型)/10 比较而不是计算出数值*10 与最大值比较,是因为计算出的数值*10 有可能本身就溢出了。比如输入字符串为”314748364“,计算出的数值为314748364,然后其*10,必然会溢出出错,所以只能进行最大值 /10 操作。代码如下:
#define MAX_UINT ((unsigned)~0)
#define MAX_INT ((int)(MAX_UINT >> 1))
#define MIN_INT ((int)~MAX_INT)
int atoi(const char *str)
{
assert(str != NULL);
long int res = 0;
bool minus = false;
int c;
while (*str == ' ')//跳过开头空格
++str;
/*正负判断*/
if (*str == '+' || *str == '-')
{
if (*str == '-')
minus = true;
++str;
}
/*只针对数字*/
while (*str >= '0' && *str <= '9')
{
c = *str - '0';
/*正数溢出判断,溢出则返回相应上限值*/
if (!minus && (res > MAX_INT / 10 || (res == MAX_INT / 10 && c > MAX_INT % 10)))
{
res = MAX_INT;
break;
}
/*负数溢出判断,这里的比较转换为无符号,大于则溢出*/
else if (minus && (res > (unsigned)MIN_INT / 10
|| (res == (unsigned)MIN_INT / 10 && c > (unsigned)MIN_INT % 10)))
{
res = MIN_INT;
break;
}
res = res * 10 + c;
++str;
}
return minus ? -res : res;
}
2、strcpy 函数和 memcpy 函数
strcpy 函数可以复制以null 为退出字符的存储器区块到另一个存储器区块内,只用于字符串的复制,字符串在存储器内以连续的字节区块组成,strcpy 可以有效复制两个配置在存储器以指针回传的字符串(也就是字符指针或字符串指针)。
函数原型如下:
#include <string.h>
char * strcpy(char * dst, const char * src);
/*把src的内容复制到dst,然后目的字符串dst指针*/
先下面看看微软的写法:
char * __cdecl strcpy(char * dst, const char * src)
{
char * cp = dst;
while (*cp++ = *src++)
; /* Copy src over dst */
return(dst);
}
上面这个写法为了提高性能,减去了那些安全检查,其余漏洞后面讨论。
除去安全性检查,strcpy 还不允许 src 与 dst 两内存块有重叠。只要有重叠势必会写入修改src 只读区域,这是不允许的,另外有重叠区域,当dst 在高地址时,复制过来的可能就是dst 前面部分的字符了。鉴于上面分析,我们写出下面实现代码:
char * strcpy(char * dst, const char * src)
{
char *ret = dst;
int count = 0;
assert((dst != NULL) && (src != NULL));//检查指针的有效性
count = strlen(src);
assert((src + count < dst) || (dst + count < src));//检查内存是否存在重叠区域,此处非最优方法
while (*ret++ = *src++)
;
return dst;
}
上面的程序最后返回 char* 类型,是为了使函数能够支持链式表达式,增加了函数的“附加值”。
实际上上面对于地址重叠还有一个更好的解决方法,那就是判断地址是哪部分重叠,如果dst 地址位于 src 前面,按照正常的赋值操作是没问题的,如果dst地址位于src后面,那么则从src尾部开始复制,这样可以解决地址重叠问题。代码就不贴出来了,可自行画一个示意图,一目了然。
另外值得注意的是:上面那个函数一样,这是strcpy 的硬伤,就是必须为目标字串分配足够的空间,如果目标字串的长度小于源字串的长度,那么在复制操作的时候会出现缓存溢出。在拷贝字符串的时候没有越界检查,这使得 strcpy 成为一个高危函数。
从strcpy 函数的参数就可以看出,strcpy 只能复制字符串,也不需要指定复制长度(strncpy 需要指定长度)
下面顺带看看memcpy 函数
void * memcpy(void * dst, const void * src, size_t count)
{
void *ret = dst;
assert((dst != NULL) && (src != NULL));
while (count--)
{
*(char*)dst = *(char*)src; //强制转换为char*,因为char占一个字节
dst = (char *)dst + 1; //这样,地址增加一个字节位移,可以全部复制
src = (char *)src + 1;
}
return ret;
}
memcpy 接受void* 类型的形参,这使得memcpy 函数可以复制任意内容。strcpy 拷贝是遇到‘\0’ 就停止,而memcpy 并不是遇到‘\0’ 就结束,而是一定拷贝 count 个字符。一般而言,在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
3、strcat 函数
函数原型:
char * strcat(char * dst, const char * src)
功能是把 src 所指字符串添加到 dst 结尾处(覆盖dst 结尾处的'\0')并添加'\0',最终返回指向 dst 的指针。
char * strcat(char * dst, const char * src)
{
assert(dst != NULL && src != NULL);
int count = strlen(src);
assert((src + count < dst) || (dst + count < src)); /*检查是否重叠*/
char * cp = dst;
while (*cp) /*先判断,再指针增加*/
cp++; /* dst末位置 */
while (*cp++ = *src++); /* 拷贝 */
return(dst);
}
src 和 dst 所指的内存区域不可以重叠且 dst 必须保证有足够的空间来容纳 src 的字符串,否则会出错。C 语言标准库中strcat 函数同 strcpy 函数一样,没有保证dst 有足够的空间容纳操作后的字符串,也使得strcat 成为一个高危函数。
4、strcmp 函数
该函数用于比较两个字符串
int strcmp(const char * src, const char * dst)
{
assert((src != NULL) && (dst != NULL));
int ret = 0;
/*两个字符串自左向右逐个比较(ASCII值),直到出现不同字符或dst遇'\0'为止*/
/*如果前面字符相同,dst的'\0'最后会参与比较*/
while (!(ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst)
++src, ++dst;
/*不同返回值对应不同比较结果*/
if (ret < 0)
ret = -1;
else if (ret > 0)
ret = 1;
return(ret);
}