前言
上一章我们完整掌握了文件操作与标准IO,学会了数据的持久化存储。这一章我们把C语言标准库中最常用、工程开发中最依赖的工具函数集中梳理一遍——包括字符串处理、内存操作、数学计算、错误提示与时间相关函数。 很多同学写代码时总在“重复造轮子”:手动写字符串长度计算、自己实现内存拷贝、处理错误时只打印“出错了”却不知道具体原因,本质都是对标准库函数不熟悉。标准库是C语言自带的“官方工具包”,经过严格测试、效率高、跨平台性好,熟练使用能大幅减少代码量、降低bug率,也是从“新手代码”到“工程代码”的关键一步。 本章只聚焦日常开发真正高频使用的核心函数,不讲偏门用法;每个函数都会讲清“功能+用法+安全坑点+工程规范写法”,搭配可直接编译运行的示例,确保你学完就能用。
目录
- 一、本章学习目标
- 二、核心知识点
- 2.1 字符串处理函数:文本操作的核心(string.h)
- 2.1.1 基础函数:strlen/strcpy/strncpy
- 2.1.2 拼接与比较:strcat/strncat/strcmp/strncmp
- 2.1.3 查找与分割:strstr/strchr/strtok
- 2.2 内存操作函数:通用数据处理(string.h)
- 2.2.1 内存初始化:memset
- 2.2.2 内存拷贝:memcpy/memmove
- 2.2.3 内存比较:memcmp
- 2.3 数学函数:数值计算(math.h)
- 2.4 错误处理函数:让程序更健壮(stdio.h/errno.h)
- 2.5 时间函数:时间戳与格式化(time.h)
- 2.1 字符串处理函数:文本操作的核心(string.h)
- 三、Dev-C++保姆式实操
- 3.1 实操1:安全字符串处理全流程
- 3.2 实操2:内存函数处理结构体/数组
- 3.3 实操3:错误处理与数学函数结合
- 3.4 实操4:获取并格式化系统时间
- 3.5 保姆式排错指南
- 四、踩坑记录
- 五、课后小练习
- 六、小结
一、本章学习目标
二、核心知识点
2.1 字符串处理函数:文本操作的核心(#include <string.h>)
C语言没有原生的“字符串类型”,所有字符串本质是以\\0(空字符,ASCII码0)结尾的字符数组。字符串函数的核心逻辑都是围绕“识别\\0、操作字符数组”展开,是日常开发中使用频率最高的函数群。
2.1.1 基础函数:strlen/strcpy/strncpy
1. strlen:获取字符串有效长度
size_t strlen(const char *s);
- 功能:计算字符串s的有效长度(只统计\\0之前的字符,不包含\\0本身);
- 返回值:size_t类型(无符号整数),这是新手最容易踩坑的点;
- 示例:char str[] = "hello";
printf("strlen: %zu\\n", strlen(str)); // 输出5(有效字符数)
printf("sizeof: %zu\\n", sizeof(str)); // 输出6(包含\\0) - 坑点:不要用strlen的返回值做减法比较。比如strlen(a) – strlen(b),如果a比b短,无符号数相减会得到一个极大的正数,而非负数:char a[] = "hi";
char b[] = "hello";
if (strlen(a) – strlen(b) < 0) { // 错误:结果是无符号数,永远为正
printf("a更短\\n");
}
// 正确写法:先转成有符号数
if ((int)strlen(a) – (int)strlen(b) < 0) {
printf("a更短\\n");
}
2. strcpy:字符串拷贝(不安全)
char *strcpy(char *dest, const char *src);
- 功能:把src指向的字符串(包含\\0)完整拷贝到dest指向的内存;
- 风险:完全不检查dest的内存空间大小,如果src长度超过dest,会直接导致内存越界,破坏其他变量/数据,甚至让程序崩溃;
- 示例(错误):char dest[3]; // 只有3字节空间
char src[] = "hello"; // 6字节(含\\0)
strcpy(dest, src); // 内存越界,程序大概率崩溃
3. strncpy:有限长度字符串拷贝(工程推荐)
char *strncpy(char *dest, const char *src, size_t n);
- 功能:最多拷贝n个字符从src到dest,避免无脑越界;
- 关键规则:
- 如果src长度 < n:拷贝完src后,会补\\0直到凑够n个字符;
- 如果src长度 >= n:只拷贝前n个字符,不会自动补\\0;
- 工程规范写法(必记):char dest[32];
char src[] = "这是一个很长的字符串,可能超过dest长度";
// 1. 最多拷贝sizeof(dest)-1个字符(留1个位置给\\0)
strncpy(dest, src, sizeof(dest)–1);
// 2. 强制补\\0,避免src长度>=n时没有结束符导致乱码
dest[sizeof(dest)–1] = '\\0';
2.1.2 拼接与比较:strcat/strncat/strcmp/strncmp
1. strcat/strncat:字符串拼接
char *strcat(char *dest, const char *src); // 不安全
char *strncat(char *dest, const char *src, size_t n); // 安全
- 功能:把src拼接到dest的末尾(覆盖dest原有的\\0);
- strcat风险:不检查dest剩余空间,容易越界;
- strncat优势:只拼接前n个字符,且自动在末尾补\\0(无需手动补);
- 示例:char dest[32] = "姓名:";
char src[] = "张三 年龄:20";
strncat(dest, src, sizeof(dest)–strlen(dest)–1); // 计算剩余空间
printf("%s\\n", dest); // 输出:姓名:张三 年龄:20
2. strcmp/strncmp:字符串比较
int strcmp(const char *a, const char *b); // 比较整个字符串
int strncmp(const char *a, const char *b, size_t n); // 只比较前n个字符
- 核心规则:按ASCII码逐字符比较,直到遇到不同字符或\\0;
- 返回值:
- 0:两段字符串(前n个字符)相等;
- < 0:a的第一个不同字符ASCII码 < b;
-
0:a的第一个不同字符ASCII码 > b;
- 新手致命坑:绝对不要用==比较字符串!==比较的是字符数组的地址,而非内容:char a[] = "hello";
char b[] = "hello";
if (a == b) { // 错误:a和b是不同数组,地址不同
printf("相等\\n");
}
if (strcmp(a, b) == 0) { // 正确:比较字符串内容
printf("相等\\n");
}
2.1.3 查找与分割:strstr/strchr/strtok
1. strstr:子串查找
char *strstr(const char *haystack, const char *needle);
- 功能:在haystack中查找needle第一次出现的位置(区分大小写);
- 返回值:找到则返回子串起始地址,没找到返回NULL;
- 示例:char str[] = "hello world, hello C";
char *pos = strstr(str, "world");
if (pos != NULL) {
printf("找到子串:%s\\n", pos); // 输出:world, hello C
}
2. strchr:字符查找
char *strchr(const char *s, int c);
- 功能:在s中查找字符c第一次出现的位置;
- 示例:查找字符串中的第一个逗号:char str[] = "张三,20,90.5";
char *comma = strchr(str, ',');
if (comma != NULL) {
*comma = '\\0'; // 截断字符串
printf("姓名:%s\\n", str); // 输出:姓名:张三
}
3. strtok:字符串分割(按分隔符拆分)
char *strtok(char *str, const char *delim);
- 核心规则:
- 第一次调用:str传入要分割的字符串,函数会找到第一个分隔符,替换为\\0,返回第一个子串地址;
- 后续调用:str传入NULL,函数会从上次分割的位置继续找下一个子串;
- 分割完成(无分隔符):返回NULL;
- 示例(拆分CSV格式字符串):char str[] = "张三,20,90.5"; // 必须是可修改的字符数组,不能是字符串常量
char *delim = ",";
// 第一次分割
char *name = strtok(str, delim);
// 后续分割
char *age = strtok(NULL, delim);
char *score = strtok(NULL, delim);printf("姓名:%s,年龄:%s,分数:%s\\n", name, age, score);
2.2 内存操作函数:通用数据处理(#include <string.h>)
字符串函数只认\\0,无法处理“不含\\0的二进制数据”(如结构体、int数组、图片),此时必须用内存操作函数——它们以“字节”为单位操作数据,不关心内容是什么。
2.2.1 内存初始化:memset
void *memset(void *s, int c, size_t n);
- 功能:把s指向的内存起始位置的n个字节,逐字节设置为c(c会被转为unsigned char);
- 最常用场景:结构体/数组清零(工程开发中几乎必用);
- 示例:// 1. 数组清零
int arr[10];
memset(arr, 0, sizeof(arr)); // 把arr的10*4=40字节全部设为0// 2. 结构体清零
typedef struct {
char name[20];
int id;
double score;
} Student;
Student stu;
memset(&stu, 0, sizeof(stu)); // 所有成员初始化为0 - 坑点:不要用memset给非字符类型数组设“非0值”!比如想把int数组设为1:int arr[2];
memset(arr, 1, sizeof(arr)); // 错误:每个字节设为1,而非每个int设为1
// arr[0]实际值:0x01010101 = 16843009,而非1
2.2.2 内存拷贝:memcpy/memmove
1. memcpy:基础内存拷贝
void *memcpy(void *dest, const void *src, size_t n);
- 功能:从src拷贝n个字节到dest;
- 限制:不能处理内存重叠的情况(如dest和src指向的内存有交集),否则拷贝结果会错乱;
- 示例(拷贝结构体数组):Student stus1[3] = {{"张三", 1, 90}, {"李四", 2, 88}};
Student stus2[3];
// 拷贝2个Student结构体(每个占20+4+8=32字节)
memcpy(stus2, stus1, 2 * sizeof(Student));
2. memmove:安全内存拷贝(支持重叠)
void *memmove(void *dest, const void *src, size_t n);
- 核心优势:内部做了内存重叠判断,即使dest和src重叠,也能正确拷贝;
- 工程建议:不确定内存是否重叠时,一律用memmove,性能损失可以忽略,安全性大幅提升;
- 示例(内存重叠拷贝):int arr[] = {1,2,3,4,5};
// 把arr[1]开始的3个元素(2,3,4)拷贝到arr[0]位置,内存重叠
memmove(arr, arr+1, 3*sizeof(int));
// 结果:arr = {2,3,4,4,5}(如果用memcpy会出错)
2.2.3 内存比较:memcmp
int memcmp(const void *a, const void *b, size_t n);
- 功能:逐字节比较a和b的前n个字节;
- 返回值:和strcmp规则一致(0=相等,<0=a<b,>0=a>b);
- 适用场景:比较结构体、二进制数据是否完全一致;
- 示例:Student stu1 = {"张三", 1, 90};
Student stu2 = {"张三", 1, 90};
if (memcmp(&stu1, &stu2, sizeof(Student)) == 0) {
printf("两个结构体内容一致\\n");
}
2.3 数学函数:数值计算(#include <math.h>)
数学函数默认以double类型为输入/输出,使用时注意类型匹配,避免精度丢失。
常用数学函数表
| sqrt(x) | 计算平方根(x≥0) | sqrt(16.0) → 4.0 |
| fabs(x) | 浮点数绝对值 | fabs(-3.14) → 3.14 |
| abs(x) | 整数绝对值(头文件stdlib.h) | abs(-5) → 5 |
| pow(x, y) | x的y次方 | pow(2, 3) → 8.0 |
| ceil(x) | 向上取整 | ceil(3.2) → 4.0 |
| floor(x) | 向下取整 | floor(3.9) → 3.0 |
| round(x) | 四舍五入 | round(3.5) → 4.0 |
| sin(x)/cos(x) | 正弦/余弦(x为弧度) | sin(M_PI/2) → 1.0 |
注意事项
int num = 10000000000000001;
double s = sqrt(num);
if (s == floor(s)) {
printf("是完全平方数\\n"); // 错误判断
}
int res = sqrt(a); // 错误:sqrt返回4.0,转为int是4(看似正确,但不规范)
double res = sqrt(a); // 正确
2.4 错误处理函数:让程序更健壮(#include <stdio.h>/#include <errno.h>)
开发中经常遇到“文件打不开”“内存申请失败”“系统调用出错”等情况,只打印“出错了”毫无意义——需要知道具体错在哪里,这就是错误处理函数的核心作用。
1. errno:错误码全局变量
- errno是<errno.h>中定义的全局变量,系统调用/库函数失败时,会把对应的错误码存入errno;
- 每个错误码对应一个具体的错误原因(如文件不存在、权限不足、内存不足);
- 关键规则:只有函数明确返回“失败”(如fopen返回NULL、malloc返回NULL)时,才检查errno——成功时errno的值是未定义的(可能保留上次的错误码)。
2. perror:打印错误原因(最常用)
void perror(const char *msg);
- 功能:先打印自定义提示msg,再自动拼接冒号、空格,最后打印errno对应的系统错误描述;
- 示例(文件打开失败处理):FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
perror("fopen failed"); // 输出:fopen failed: No such file or directory
return 1;
}
3. strerror:获取错误描述字符串
char *strerror(int errnum);
- 功能:根据错误码errnum,返回对应的错误描述字符串;
- 示例:#include <errno.h>
#include <string.h>FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
printf("打开文件失败:%s\\n", strerror(errno));
return 1;
}
2.5 时间函数:时间戳与格式化(#include <time.h>)
时间函数用于获取系统时间、记录日志时间戳、实现程序计时等,是工程开发中常用的辅助功能。
1. 核心类型与函数
| time_t | 时间戳类型(本质是long,存储从1970-01-01 00:00:00到现在的秒数) |
| struct tm | 分解时间结构体,包含年、月、日、时、分、秒等字段 |
| time(&t) | 获取当前系统时间戳,存入t;传入NULL直接返回时间戳 |
| localtime(&t) | 把时间戳t转为本地时间的struct tm结构体 |
| difftime(t2, t1) | 计算t2 – t1的秒数(返回double) |
2. struct tm结构体字段
struct tm {
int tm_sec; // 秒(0~59)
int tm_min; // 分(0~59)
int tm_hour; // 时(0~23)
int tm_mday; // 日(1~31)
int tm_mon; // 月(0~11,0=1月)
int tm_year; // 年(从1900开始算,如2024=124)
int tm_wday; // 星期(0~6,0=周日)
int tm_yday; // 年内天数(0~365)
int tm_isdst; // 夏令时标记
};
3. 基础用法示例
// 1. 获取当前时间戳
time_t now_ts = time(NULL);
printf("当前时间戳:%ld\\n", now_ts);
// 2. 转为本地时间
struct tm *now_tm = localtime(&now_ts);
// 注意:tm_year要+1900,tm_mon要+1
printf("当前时间:%04d-%02d-%02d %02d:%02d:%02d\\n",
now_tm->tm_year + 1900,
now_tm->tm_mon + 1,
now_tm->tm_mday,
now_tm->tm_hour,
now_tm->tm_min,
now_tm->tm_sec);
// 3. 程序计时(计算函数执行耗时)
time_t start = time(NULL);
// 模拟耗时操作
for (int i = 0; i < 100000000; i++);
time_t end = time(NULL);
printf("耗时:%.0f 秒\\n", difftime(end, start));
坑点
- localtime返回的是静态内存区的地址,多次调用会覆盖之前的结果:time_t t1 = time(NULL) – 86400; // 昨天
time_t t2 = time(NULL); // 今天
struct tm *tm1 = localtime(&t1);
struct tm *tm2 = localtime(&t2); // 覆盖tm1的内容
// 此时tm1和tm2都指向今天的时间 - 解决方法:拷贝struct tm的内容到自定义变量:struct tm tm1, tm2;
time_t t1 = time(NULL) – 86400;
time_t t2 = time(NULL);
tm1 = *localtime(&t1); // 拷贝内容
tm2 = *localtime(&t2); // 拷贝内容
三、Dev-C++保姆式实操
3.1 实操1:安全字符串处理全流程
实现字符串的安全拷贝、拼接、查找、分割,Dev-C++可直接编译运行:
#include <stdio.h>
#include <string.h>
int main() {
// 1. 安全字符串拷贝
char dest[64];
char src[] = "C语言标准库字符串处理示例,这是一段较长的文本";
// 安全拷贝:留1字节给\\0
strncpy(dest, src, sizeof(dest)–1);
dest[sizeof(dest)–1] = '\\0';
printf("1. 安全拷贝结果:%s\\n", dest);
// 2. 字符串拼接
char append[] = " | 拼接的内容";
strncat(dest, append, sizeof(dest)–strlen(dest)–1);
printf("2. 拼接结果:%s\\n", dest);
// 3. 子串查找
char *sub = strstr(dest, "标准库");
if (sub != NULL) {
printf("3. 找到子串:%s\\n", sub);
}
// 4. 字符串分割
char csv_str[] = "张三,男,20,90.5";
char *token = strtok(csv_str, ",");
int i = 1;
printf("4. 分割CSV结果:\\n");
while (token != NULL) {
printf(" 字段%d:%s\\n", i++, token);
token = strtok(NULL, ",");
}
getchar();
return 0;
}
运行结果
1. 安全拷贝结果:C语言标准库字符串处理示例,这是一段较长的文本
2. 拼接结果:C语言标准库字符串处理示例,这是一段较长的文本 | 拼接的内容
3. 找到子串:标准库字符串处理示例,这是一段较长的文本 | 拼接的内容
4. 分割CSV结果:
字段1:张三
字段2:男
字段3:20
字段4:90.5
3.2 实操2:内存函数处理结构体/数组
用memset/memcpy/memmove处理结构体和数组,Dev-C++可直接编译运行:
#include <stdio.h>
#include <string.h>
// 定义学生结构体
typedef struct {
char name[20];
int id;
double score;
} Student;
int main() {
// 1. 结构体清零与赋值
Student stu;
memset(&stu, 0, sizeof(stu)); // 清零
strncpy(stu.name, "李四", sizeof(stu.name)–1);
stu.id = 202402;
stu.score = 88.5;
printf("1. 初始化后的结构体:\\n");
printf(" 姓名:%s,学号:%d,分数:%.1lf\\n", stu.name, stu.id, stu.score);
// 2. 结构体拷贝
Student stu_copy;
memcpy(&stu_copy, &stu, sizeof(Student));
printf("2. 拷贝后的结构体:\\n");
printf(" 姓名:%s,学号:%d,分数:%.1lf\\n", stu_copy.name, stu_copy.id, stu_copy.score);
// 3. 数组内存重叠拷贝
int arr[] = {1,2,3,4,5,6};
printf("3. 原数组:");
for (int i = 0; i < 6; i++) {
printf("%d ", arr[i]);
}
// 把arr[2]开始的3个元素(3,4,5)拷贝到arr[0]位置(内存重叠)
memmove(arr, arr+2, 3*sizeof(int));
printf("\\n 内存重叠拷贝后:");
for (int i = 0; i < 6; i++) {
printf("%d ", arr[i]);
}
getchar();
return 0;
}
运行结果
1. 初始化后的结构体:
姓名:李四,学号:202402,分数:88.5
2. 拷贝后的结构体:
姓名:李四,学号:202402,分数:88.5
3. 原数组:1 2 3 4 5 6
内存重叠拷贝后:3 4 5 4 5 6
3.3 实操3:错误处理与数学函数结合
实现“计算圆面积”,并处理输入错误、数学函数调用错误,Dev-C++可直接编译运行:
#include <stdio.h>
#include <math.h>
#include <errno.h>
#include <string.h>
#define PI 3.1415926
int main() {
double r;
printf("请输入圆的半径:");
// 检查输入是否有效
if (scanf("%lf", &r) != 1) {
perror("scanf failed");
return 1;
}
// 检查半径是否非负
if (r < 0) {
errno = EINVAL; // 手动设置无效参数错误码
printf("计算面积失败:%s\\n", strerror(errno));
return 1;
}
// 计算面积
double area = PI * pow(r, 2);
printf("半径为%.2lf的圆面积:%.2lf\\n", r, area);
getchar();
getchar();
return 0;
}
运行结果1(正常输入)
请输入圆的半径:2
半径为2.00的圆面积:12.57
运行结果2(输入非数字)
请输入圆的半径:abc
scanf failed: Invalid argument
运行结果3(输入负数)
请输入圆的半径:-3
计算面积失败:Invalid argument
3.4 实操4:获取并格式化系统时间
获取当前时间、计算程序耗时,Dev-C++可直接编译运行:
#include <stdio.h>
#include <time.h>
#include <unistd.h> // 用于sleep函数(Dev-C++需勾选“兼容POSIX”)
int main() {
// 1. 获取当前时间并格式化
time_t now_ts = time(NULL);
struct tm now_tm = *localtime(&now_ts); // 拷贝内容,避免被覆盖
printf("1. 当前系统时间:\\n");
printf(" 时间戳:%ld\\n", now_ts);
printf(" 格式化:%04d-%02d-%02d %02d:%02d:%02d\\n",
now_tm.tm_year + 1900,
now_tm.tm_mon + 1,
now_tm.tm_mday,
now_tm.tm_hour,
now_tm.tm_min,
now_tm.tm_sec);
// 2. 程序计时(模拟耗时操作)
time_t start = time(NULL);
printf("\\n2. 开始执行耗时操作…\\n");
sleep(2); // 休眠2秒
time_t end = time(NULL);
printf(" 耗时:%.0f 秒\\n", difftime(end, start));
getchar();
return 0;
}
运行结果
1. 当前系统时间:
时间戳:1741567890
格式化:2025-03-09 15:31:30
2. 开始执行耗时操作…
耗时:2 秒
3.5 保姆式排错指南
| 字符串打印出现乱码 | strncpy拷贝后未手动补\\0,或字符串无结束符 | 拷贝后强制设置dest[sizeof(dest)-1] = ‘\\0’ |
| 用==比较字符串永远不相等 | ==比较的是地址,而非内容 | 改用strcmp函数比较字符串内容 |
| memset给int数组设1后,数值不是1 | memset逐字节赋值,int占4字节,每个字节设1导致数值错误 | 非字符数组清零用memset,设非0值手动循环赋值 |
| 数学函数编译报错“undefined reference” | 未链接数学库 | Dev-C++中:项目→项目属性→参数→链接器,添加“-lm” |
| localtime多次调用后结果相同 | localtime返回静态区地址,被后续调用覆盖 | 拷贝struct tm的内容到自定义变量 |
| perror打印的错误信息不对 | 函数成功时检查了errno,读取到旧的错误码 | 只在函数明确失败后(返回NULL/-1)检查errno |
| strtok分割字符串崩溃 | 传入了字符串常量(不可修改),strtok会修改原字符串 | 确保传入可修改的字符数组,而非const char* |
四、踩坑记录
五、课后小练习
六、小结
本章我们把C语言标准库中最核心、最实用的函数全部梳理完毕:字符串函数解决文本处理问题,内存函数解决通用数据的初始化/拷贝/比较,数学函数提供数值计算能力,错误处理函数让程序更健壮,时间函数让程序能和系统时间交互。 标准库的设计思想是“通用、高效、安全”,熟练使用这些函数,能让你摆脱“重复造轮子”的低效状态,写出更简洁、更稳定的代码。需要注意的是,每个函数都有其适用场景和坑点——比如strcpy的越界风险、memset的字节赋值规则、localtime的静态内存问题,这些都是工程开发中容易出bug的地方,一定要牢记规范用法。 下一章我们将进入内存管理与指针进阶总结,把前面学过的指针、数组、动态内存、结构体、函数指针串联成完整的内存模型,帮你建立真正的C语言底层思维,彻底搞懂“内存到底是怎么回事”。




