1. 什么是C库函数
C语言的库函数并不是C语言本身的一部分,它是由 编译程序 根据一般用户的需要,编制并提供用户使用的一组程序。C的库函数极大地方便了用户,同时也补充了C语言本身的不足。在编写C语言程 序时,使用库函数,既可以提高程序的运行效率,又可以提高编程的质量。所有的C编译器都会实现C库函数,其中著名的C库有GNU发布的glibc。2.为什么要造轮子写C库函数
在C语言开发中,我们直接包含相关库函数头文件(如string.h)即可使用字符串处理相关C库函数。但C语言不像其他面向对象语言,在安全处理等做了相关优化处理,如调用字符串C库函数就有很多限制,但C语言基于效率考虑,将这些留给调用者处理。如果没有考虑这些,写的C代码就很容易存在bug。
从零实现部分C库函数主要有要两点好处:熟悉常用的C库函数使用
增强自己C基本功
3. 开始造轮子
下面是一些常见的字符串处理库函数,对于字符串处理函数处理的都为字符串,所谓字符串即为以‘\0’结尾的ASCII字符串。我们将以实现如下函数抛转引玉:
序号 | 函数原型 |
1 | size_t strlen( char *str ) 函数返回字符串str 的长度 |
2 |
char *strcpy( char *to,const char *from ) 复制字符串from 中的字符到字符串to,包括空值结束符 |
3 |
将字符串from 中至多count个字符复制到字符串to中。如果字符 串from 的长度小于count,其余部分用'\0'填补 |
1)my_strlen()
//my_strlen()库函数实现size_t my_strlen(const char* str) { size_t len = 0; while (*str++ != '\0') { len++; } return len;}//my_strlen()库函数测试用例bool test1(const char *msg) { if (strlen(msg) == my_strlen(msg)) { std::cout << "单元测试1:测试通过!," << "消息长度:" << my_strlen(msg) << "。" << std::endl; return true; } else { std::cout << "单元测试1:测试失败!" << std::endl; return false; }}
2)my_strcpy()
从my_strcpy()库函数实现我们可以发现,调用strcpy()是需注意两个问题:
1.确保from指向的源字符串以'\0'为结尾的字符串,
2. 确保to指向的空间足够大,至少比源字符串长度大,不然就会出现内存溢出问题
//my_strcpy()库函数实现char* my_strcpy(char* to, const char* from){ char* pDes = to; //从这我们可以看出,字符拷贝是以源字符结尾做为判断的, //因此该库函数调用时应注意两点: // 1.确保from指向的时以'\0'为结尾的字符串, // 2. 确保to指向的空间足够大,不然就会出现内存溢出问题 while (*from != '\0') { *pDes++ = *from++; } *pDes = '\0'; //记得一定要补字符串尾, return to;}//库函数测试用例//拷贝功能测试bool my_strcpy_test1(const char* from){ bool ret = false; size_t msg_size = my_strlen(from) + 1; char* pBuf1 = (char *)malloc(strlen(from) + 1); if (pBuf1 == NULL ) { return false; } memset(pBuf1, 0, msg_size); my_strcpy(pBuf1, from); if (!strcmp(pBuf1, from)) { std::cout << "单元测试1:测试通过!," << "消息内容:" << pBuf1 << ":"<< std::endl; ret = true; } else { std::cout << "单元测试1:测试失败!" << std::endl; ret = false; } free(pBuf1); return ret;}
3)my_strncpy()
从strcpy()的实现我们知道,这个函数存在一定安全风险,如果有恶意使用这个函数往我们的内存中拷贝一个很大的数据内容,而我们未做限制,就会导致我们的内存溢出,从而使得恶意攻击者可以修改我们的内存内容。 这就引出另外一个字符拷贝函数strncpy(),这个函数限制了最大的拷贝字节数。//my_strncpy代码char* my_strncpy(char* to, const char* from, size_t count){ size_t cnt = 0; size_t index = 0; char* pDes = to; while (*from != '\0' && cnt < count) //从这可以看出一个问题,如果count小于源字符串长度,我们拷贝的字符串就不是真正的字符串。 { *pDes++ = *from++; cnt++; } for (index = cnt; index < count; index++) //源字符串不够count长度, 剩余部分补‘\0' { *pDes++ = '\0'; } return to;}
1. 测试单元1:拷贝长度小于源字符串长度
//单元测试1:拷贝长度小于源字符串长度bool my_strncpy_test1(void){ bool ret = false; const char* msg = "hello,strncpy()"; char pBuf[100]; memset(pBuf, 0xff, 100); my_strncpy(pBuf, msg, 7); std::cout << "strncpy()拷贝内容:" << pBuf << std::endl; if (!memcmp(msg, pBuf, 7)) { std::cout << "单元测试1(拷贝长度小于源字符串长度):测试通过!," << ":" << std::endl; ret = true; } else { std::cout << "单元测试1(拷贝长度小于源字符串长度):测试失败!," << std::endl; ret = false; } return ret;}
测试结果:
拷贝长度小于源字符串长度我们看到测试单元测试通过,但我们打印出的拷贝内容出现了乱码,这是因为源字符串比要拷贝的字节数多,因此没有拷贝'\0'结束符,所以打印了多余的内容(见内存内容)
1. 测试单元2:拷贝长度大于源字符串长度
//源字符串长度小于于countbool my_strncpy_test2(void){ bool ret = false; const char* msg = "hello"; char pBuf[100]; //memset(pBuf, 0xff, 100); memset(pBuf, 0xff, 100); //my_strncpy(pBuf, msg, 7); my_strncpy(pBuf, msg, 7); std::cout << "strncpy()拷贝内容:" << pBuf << std::endl; if (!memcmp(msg, pBuf, 7)) { std::cout << "单元测试2(拷贝长度小于源字符串长度):测试通过!," << ":" << std::endl; ret = true; } else { std::cout << "单元测试2(拷贝长度小于源字符串长度):测试失败!," << std::endl; ret = false; } return ret;}
内存图
测试结果:拷贝长度小于源字符串长度
我们看到测试单元测试通过,打印也正常 (见内存内容) 4) 安全函数如果你是用visual studio进行编码测试,使用以上函数编译器会告警,并提示你有其他的安全函数可调用(如,strcpy_s, strncpy_s)。
以我们实现的strcpy()来考虑,这个函数还有什么问题呢?
对输出的参数指针未做NULL判断处理。
目的内存可能不足,造成溢出
4. 造轮子总结
从以上我们的代码实现,我们可以看到每个函数使用要注意的事项,以及相关替代函数又避免了那些东西,这些对提高我们的C基础是十分有效的。
C库函数包含巨大的函数功能,我们无需全部实现,也不可能全部实现。
我们写C库造轮子的目的是熟悉常用的C库函数并且夯实C语言基础,以下是本人建议需要造轮子的函数。
字符串相关库函数
strcpy(), strncpy(), strcat(),strncat(),isdigit(),strstr()等
内存处理相关函数
5. C语言流行趋势
6. 总结
对于嵌入式软件开发而言,C语言是目前唯一的选择。但C语言的应用远超与此,C实现了linux操作系统,C实现强大的编译器gcc等,目前C仍然是最流行的编程语言之一。对应希望从事嵌入式软件开发的人,自己造轮子实现C库函数,对应夯实C语言基础,学好C语言这一强大武器十分必要。