在日常写程序中有一些功能函数是可以重复使用的,在c语言的标准库里面也有对应的功能函数,但是那些功能函数有会有小问题然后我就整理了一下对应功能的安全函数的使用。其中前四个函数可以编译成一个动态库,然后在项目工程中只需要包含对应的头文件然后就可以调用了。
1:strcmp_iot()
在c语言标准库中用于比较两个字符串是否相等提供的函数为:strcmp()函数,还有它的衍生函数strncmp()等。
strcmp()函数的函数原型为:
#include <string.h>
int strcmp(const char *s1, const char *s2);
其核心原理是按照ASCII值逐字比较两个字符串,然后根据返回值确定连个字符串是否相等。当返回值为0表示两个字符串相等,<0表示S1小于S2,>0表示S1大于S2。
隐藏风险:
1:当传递的参数为NULL空指针的时候使用这个函数会直接出现段错误进而程序崩溃。
2:判断字符的结束的标志位为'\0',如果传递过来的不是以'\0'结尾,可能会出现越界的风险。
3:strcmp
本身不写入内存,但如果比较的是一个被溢出破坏的字符串,结果不可靠。
4:区分大小写。
虽然strncmp函数比较与strcmp函数具有一定的优化和安全性,但是在实际使用中也会受到'\0'的影响,所以在这里给出一个全新的安全比较函数strcmp_iot();
函数原型:
#include <stddef.h> // for size_t
#include <string.h> // for strlen
/*** 安全比较两个字符串是否相等* * @param string1 第一个字符串指针* @param string2 第二个字符串指针* @return 0 表示两个字符串相同;1 表示不同或任一为 NULL*/
int strcmp_iot(const char *string1, const char *string2)
{// 检查空指针if (string1 == NULL || string2 == NULL) {return 1; // 认为不同}size_t len1 = strlen(string1);size_t len2 = strlen(string2);// 长度不同,直接返回不同if (len1 != len2) {return 1;}// 恒定时间比较,防止时序攻击int result = 0;for (size_t i = 0; i < len1; i++) {result |= string1[i] ^ string2[i]; // 异或:相同为0,不同为非0}// 如果 result 为 0,说明所有字符都相同return result != 0;
}
这个函数的优点:
1:加上了空指针防护
2:采用长度检查
3:恒定时间比较,防时序攻击
4:可以在IOT设备的设备密钥、Token、验证码、身份验证、比对数据中广泛使用,更安全。
2:strcpy_iot()
在传统的c语言标准库里面提供的复制函数分为两类函数:1.字符串复制函数,2:内存复制函数。
函数 | 是否检查长度 | 是否自动补 \0 | 适用场景 |
---|---|---|---|
strcpy | ❌ 否 | ✅ 是 | 已知目标足够大(不推荐) |
strncpy | ✅ 是 | ❌ 否(需手动) | 安全字符串复制 |
memcpy | ✅ 是 | ❌ 否 | 任意内存块复制 |
memmove | ✅ 是 | ❌ 否 | 支持内存重叠的复制 |
在程序员的实践中有句话叫永远不要使用strcpy函数,其他的几个函数想要实现安全的复制函数最优解就是组合使用,我给到一个封装好的安全的复制函数。
函数原型:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>/*** @brief 安全复制字符串到目标缓冲区* * @param dest 目标缓冲区* @param dest_size 目标缓冲区总大小(字节)* @param src 源字符串* @return int 0 = 成功, -1 = 失败(如空指针或缓冲区太小)*/
int strcpy_iot(char *dest, size_t dest_size, const char *src)
{// 检查空指针if (dest == NULL || src == NULL) {return -1;}// 检查缓冲区大小if (dest_size == 0) {return -1;}// 使用 snprintf 进行安全复制// 返回值是“如果不截断应写入的字符数”int ret = snprintf(dest, dest_size, "%s", src);// 如果返回值 >= dest_size,说明被截断了if (ret < 0) {// 编码错误dest[0] = '\0';return -1;}if ((size_t)ret >= dest_size) {// 被截断,可以视为失败,或仅警告// 这里我们清空并返回失败(严格模式)dest[0] = '\0';return -1;}return 0; // 成功
}
优点:
- ✅ 防空指针
- ✅ 防缓冲区溢出
- ✅ 自动补
\0
- ✅ 返回成功/失败状态
- ✅ 使用
snprintf
(推荐的安全方式) - ✅ 支持跨平台
3:strstr_iot()
strstr是C 标准库函数中用于字符串查找函数,用于在一个字符串中查找另一个子字符串的首次出现位置。其中还有一些衍生的函数:
函数 | 用途 |
---|---|
strstr(h, n) | 查找子字符串 |
strchr(s, c) | 查找单个字符 |
strrchr(s, c) | 查找字符最后一次出现 |
strcasestr(h, n) | 忽略大小写的 strstr (Linux) |
直接使用strstr函数的话可能出现的问题有:
1:传递的参数为空指针而导致的段错误。
2:区分大小写,容易漏匹配
3:返回的是指针,容易误用或越界访问
4:无法处理重叠或二进制数据中的非字符串
基于上面可能会出现的问题给出两个分装函数用于解决上面的问题:
函数原型:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>/*** @brief 安全查找子字符串(区分大小写)* * @param haystack 主字符串(被搜索的)* @param needle 子字符串(要找的)* @return char* 匹配位置指针,未找到返回 NULL*/
const char* strstr_iot(const char *haystack, const char *needle)
{// ✅ 空指针检查if (haystack == NULL) {return NULL;}if (needle == NULL) {return NULL;}// ✅ 空字符串处理:strstr 标准行为是返回 haystackif (needle[0] == '\0') {return haystack;}// ✅ 调用标准库return strstr(haystack, needle);
}
忽略大小写的版本(跨平台)
/*** @brief 安全忽略大小写的字符串查找(跨平台)* * @param haystack 主字符串* @param needle 子字符串* @return const char* 匹配位置,未找到返回 NULL*/
const char* strcasestr_iot(const char *haystack, const char *needle)
{if (haystack == NULL || needle == NULL) {return NULL;}if (needle[0] == '\0') {return haystack;}size_t needle_len = strlen(needle);for (const char *p = haystack; *p != '\0'; p++) {const char *h = p;const char *n = needle;while (*h && *n && (tolower(*h) == tolower(*n))) {h++;n++;}if (*n == '\0') {return p; // 找到了}}return NULL; // 未找到
}
4:atoi_iot()
在c语言中将字符串转换为整数的标准函数,虽然操作简单但是存在巨大的安全隐患,我在初期写程序中就被这个摆了一道,也是没有注意其中出现的风险。
缺点:
1:atoi函数的转喊成功与失败的依据是函数的返回值,但是在使用中如果转换的数据是字符0,那就会混淆转换结果。
int b = atoi("0"); // → 0 ✅
int c = atoi("xyz"); // → 0 ❌ 但你怎么知道是失败了?
2:函数使用的是int类型,当超出int范围会出现未定义现象。
3:如果传入的数据是空指针或者空字符串就直接触发段错误从而程序崩溃。
因此根据上面出现的问题,也给出相应的替代函数strtol
(字符串转 long)是 atoi
的安全替换函数,我在此基础之上再进行封装。
函数原型:
#include <stdlib.h>
#include <errno.h>
#include <limits.h>/*** @brief 安全字符串转整数* * @param str 输入字符串* @param result 输出的整数值* @return int 0 = 成功, -1 = 失败*/
int atoi_iot(const char *str, int *result)
{// 检查空指针if (str == NULL || result == NULL) {return -1;}// 跳过前导空白(可选)while (*str == ' ' || *str == '\t') str++;// 空字符串if (*str == '\0') {return -1;}char *endptr;errno = 0;long val = strtol(str, &endptr, 10);// 检查是否完全没有转换if (endptr == str) {return -1; // 无有效数字}// 检查是否转换了整个字符串while (*endptr == ' ' || *endptr == '\t') endptr++;if (*endptr != '\0') {return -1; // 后面有非法字符}// 检查溢出if (errno == ERANGE || val < INT_MIN || val > INT_MAX) {return -1;}*result = (int)val;return 0;
}
最后分享一个linux系统的主函数参数解析函数
5:getopt()
在我前期的写程序中对于主函数传参的处理就是简单的调用,后面设计到传递的参数过多就显得很麻烦了,然后我就发现了原来还有一个主函数传参的高级用法getopt()。
函数原型:
int getopt(int argc, char *const argv[], const char *optstring);
参数 含义
argc 命令行参数个数(main 函数传入)
argv 命令行参数数组(main 函数传入)
optstring 定义合法选项的字符串
optstring 的格式
每个字符代表一个有效的命令行选项。
如果某个选项需要参数(比如 -d /dev/lirc0 中的 /dev/lirc0),就在字符后加一个冒号 :。
示例:
"d:f:h"
表示:
-d 后面必须跟一个参数(如设备路径)
-f 后面必须跟一个参数(如配置文件路径)
-h 是一个开关型选项,不需要参数
程序举例:
#include <unistd.h>
#include <stdio.h>int main(int argc, char *argv[])
{int opt;while ((opt = getopt(argc, argv, "i:o:v")) != -1) {switch (opt) {case 'i':printf("输入文件: %s\n", optarg);break;case 'o':printf("输出文件: %s\n", optarg);break;case 'v':printf("启用详细模式\n");break;default:fprintf(stderr, "用法: %s -i input -o output [-v]\n", argv[0]);return 1;}}return 0;
}
主函数运行传递的参数为:
./app -i input.txt -o output.txt -v