函数
1.函数定义
1.1 函数元素
函数名命名规则:与变量名命名规则一致
参数列表:可以有0/1/n个参数
返回类型:可以返回1/0(void)个数据
1.2 实参与形参
实参:在函数调用的传入的参数(实际参数),用来初始化形参数
形参:函数声明或函数定义时的参数,在函数被调用的时候才会分配空间(局部变量)
传参方式:
1.传值---在函数内存使用的是局部变量与外部无关
2.传地址(传指针)---在函数内部可以同地址取访问或修改外部值
void show(int a, int b) //这里的a和b就是形参
{// int a = a, int b=b
printf("[a=%d, b=%d]\n", a,b);
}
int main(void)
{
int a=100;
int b=200;
show(a, b); //这里的a和b就是实参
}
1.3 主函数main函数参数
主函数参数格式:
int main(int argc, char *argv[]) //argc为参数个数, argv参数列表
作用:在程序运行的时候传入参数
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("运行时参数个数:%d\n", argc);
printf("连接服务器:%s\n", argv[1]);
printf("端口号:%s\n", argv[2]);
return 0;
}
2.特殊函数
2.1 static修饰函数静态函数
背景:普通函数都是跨文件可见的,即在文件 a.c中定义的函数可以在 b.c 中使用。
静态函数:只能在定义的文件内可见的函数,称为静态函数(私有函数)。
例子:
model.c
#include <stdio.h>
//只在本文中使用---避免与其他文件里面的函数重名冲突
static int calc(int a,int b)
{
return atb;
}
//跨文件使用
void show(int a, int b)
{
printf("%d\n",calc(a,b));
}
main.c
#include <stdio.h>
int main(void)
{
int a=10,b=20;
//int c= calc(a,b); //会报错
show(a,b);
return 0;
}
编译:
gcc -o main main.c model.c //两个文件要一起编译
2.2 递归函数
如果一个函数内部,包含了对自身的调用,则该函数称为递归函数.
要求:
1)只有能被表达为递归的问题,才能用递归函数解决。
2)递归函数必须有一个可直接退出的条件,否则会进入无限递归。
3)递归函数包含两个过程,一个逐渐递进的过程,和一个逐渐回归的过程
例子:用递归函数实现依次输出 n 个自然数
void fun(int n) //5
{
if(n<=0) return;
fun(n-1);
printf("%d ", n);
}
int main(void)
{
fun(5);
return 0;
}
2.2.1 作业
使用递归函数计算一个整数的幂,即 base 的 exponent 次幂。例如,2 的 3 次幂为2*2*2=8。要求在 main 函数中输入 base 和 exponent,调用递归函数计算结果并输出。
⭐注意:要考虑幂为0和负数的情况.
#include <stdio.h>
// 递归计算 base 的 exponent 次幂
double power(double base, int exponent) {
if (exponent == 0) {
return 1; // 任何数的0次幂为1
} else if (exponent < 0) {
return 1 / power(base, -exponent); // 如果指数是负数,则返回 1 / base 的正指数次幂
} else {
return base * power(base, exponent - 1); // 递归调用,逐渐减少 exponent
}
}
int main() {
double base;
int exponent;
// 输入 base 和 exponent
printf("Enter the base: ");
scanf("%lf", &base);
printf("Enter the exponent: ");
scanf("%d", &exponent);
// 调用递归函数并输出结果
double result = power(base, exponent);
printf("%.2lf raised to the power of %d is: %.2lf\n", base, exponent, result);
return 0;
}
2.3 回调函数
⭐把一个函数指针作为另外一个函数的参数 (把一个函数传递给另外一个函数)
| 名称 | 说明 |
|---|---|
| 指针函数 | 本身是一个函数, 函数的返回值类型是指针 |
| 函数指针 | 本身是一个指针, 指针类型是函数类型(指针指向一类函数) |
2.3.1. 回调函数例子
2.3.1.1. 定义一个函数
int add(int a, int b) //函数名称就是函数入口地址
{
return a+b;
}
2.3.1.2. 定义指针指向上面函数
int (*Fptr)(int , int ) = add; //&add 在c语言中&一般省略
int c = add(1,2);
int d = Fptr(1,2); //等价add调用
2.3.2. ⭐通过函数指针,跳转到指定的地址上执行
typedef void (*Ftype)(void) ; //定义函数指针类型Ftype
void (*fptr)(void) = (Ftype)0xff00ff00;
//等价Ftype fptr = (Ftype)0xff00ff00;
fptr();//就会跳转到0xff00ff00地址上运行
作业
要对绝对地址0x100000赋值,我们可以用(unsigned int *)0x100000 =1234; 那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
typedef void (*funptr)(void);
funptr f = (funptr)0x100000;
f();
2.3.3. 应用:
2.3.4.1. 函数指针数组:
本身是一个数组, 数组中存储的是指针, 指针类型是函数
例子:
#include <stdio.h>
int add(int a, int b){return a+b;}
int sub(int a, int b){return a-b;}
int dev(int a, int b){return a/b;}
int mul(int a, int b){return a*b;}
int main(void){
//定义函数指针数组来存储这些函数
int (*farr[4])(int , int) = {add, sub, dev, mul};
for(int i=0; i<4; i++){
printf("%d\n", farr[i](1, 2));
}
}
2.3.4.2. 函数指针作为函数参数——回调函数
设计第一个员工一天工作流程
void work( void(*constom)(void) ){
printf("1.起床\n");
printf("2.洗漱\n");
printf("3.吃早餐\n");
constom();
printf("5.睡觉\n");
}
void jack_work(){
printf("搬砖\n");
}
void rose_work() {
printf("运输\n");
}
int main(void){
work(jack_work);
work(rose_work);
}
2.4 变参函数
int printf(const char *format,...);
//例子:printf("%d,%c",a, b);
int scanf(const char *format,...);
通过参数栈来实现参数传递, 在函数内部通过下面这些类型和宏来实现参数获取:
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
设计一个变参函数:实现输出n个整数的和
#include <stdio.h>
#include <stdarg.h>
// 变参函数:计算多个整数的和
int sum(int num, ...) {
va_list args; // 声明一个 va_list 类型的变量,用于访问可变参数
int total = 0;
va_start(args, num); // 初始化 args,num 是固定参数
for (int i = 0; i < num; i++) {
total += va_arg(args, int); // 获取下一个参数,指定类型为 int
}
va_end(args); // 清理 va_list 变量
return total;
}
int main() {
int result = sum(4, 1, 2, 3, 4); // 求 1 + 2 + 3 + 4
printf("Sum: %d\n", result);
return 0;
}
⭐在gcc编译c语言程序如果函数没有声明会自动声明(并且统一把函数返回值改为int)
3.字符串函数
字符串双引号引用的连续字符
常量: "hello world" ----(存储在数据段)
变量: char name[]={'j','a','c','k','\0',};
printf输出字符串: printf("%s", name); //从name第一个字符开始输出, 直到遇到\0结束
3.1 字符串处理函数(C语言库函数)
头文件#include <string.h>
常用字符串处理函数有:
| 函数 | 功能 | 英文名解释 |
|---|---|---|
| size_t strlen(const char *str) | 计算字符串长度,不包含\0 | string length |
| int strcmp(const char *str1, const char *str2) | 字符串比较: 返回值0表示相等,非0表示不等 | string compare |
| char * strcpy(char *dest, const char *src) | 字符串拷贝: 把src拷贝到dest。⭐dest是要改变的。返回值是dest,即目标字符串的地址。 | string copy |
| char * strcat(char *dest, const char *src) | 字符串拼接: src被追加到dest的末尾(会覆盖dest的末尾\0),dest需要足够大。返回dest。 | string concatenate |
| char * strstr(const char *hatstack, const char *needle) | 字符串查找: haystack是要搜索的目标字符串,needle是要查找的子字符串。 找到就返回指向haystack中第一个匹配位置的指针。找不到就返回NULL。 | string substring search |
| void bzero(void *s, size_t len) | 字符串清除: s是指向要设置为0的内存区域的指针,len是要清零的字节数。 | byte zero |
| strtok | 字符串分割 | string tokenizer |
| atoi,atof | 字符串转整型,浮点型 | ASCII to Integer,ASCII to Float |
| sprintf | 字符串打包函数 | string print |
| sscanf | 字符串解包函数 | string scan with format |
3.1.1 字符串长度计算, 比较, 拷贝
例子:
//size_t strlen(const char *str);
const char *str = "Hello";
size_t len = strlen(str); // len = 5
//int strcmp(const char *str1, const char *str2);
const char *str1 = "apple";
const char *str2 = "banana";
int result = strcmp(str1, str2); // 返回负数,因为 "apple" 小于 "banana"
//char *strcpy(char *dest, const char *src);
char dest[20];
const char *src = "Hello";
strcpy(dest, src); // 将 "Hello" 复制到 dest 中
-
strlen计算的字符串必须以\0为结尾
-
strlen不会检查空指针,如果传入空指针回导致程序崩溃或未定义行为。
-
strcmp如果两个字符串有一个为空字符串
"",strcmp会认为非空字符串大于空字符串。 -
strcmp同样需要避免空指针。
⭐注意:
在 C 语言中,char *p = "hello"; strcpy(p, "world");报错是因为字符串字面量(如 "hello")在大多数编译器中是存储在只读内存区域的。
问题分析:
- char *p = "hello";:
这里,p是一个指向字符的指针,指向字符串字面量"hello"。在大多数 C 编译器中,字符串字面量会被存储在只读的内存区域,因此你不能通过p来修改它。 - strcpy(p, "world");:
你试图将"world"字符串复制到p指向的内存区域,即原本存储"hello"的位置。但这个内存区域是只读的,因此会触发一个 段错误(segmentation fault),这就是“核心已转储”错误。
错误原因:
- 只读内存区域:C 中的字符串字面量通常存储在只读内存区域,因此尝试修改它们会导致程序崩溃。
- 未分配可写内存:
p指向的是一个只读字符串,而不是你可以自由修改的内存区域。
解决方法:
-
使用字符数组:
如果你希望修改字符串,可以将p定义为字符数组,而不是指向字符串字面量的指针。char p[6] = "hello"; // 使用字符数组 strcpy(p, "world"); // 可以修改 p 的内容这样,
p就是一个可写的数组,包含"hello",并且可以使用strcpy来修改其内容。 -
动态分配内存:
如果你需要使用指针,也可以通过malloc动态分配一块足够的内存来存储字符串:char *p = malloc(6 * sizeof(char)); // 为 "hello" 分配空间 strcpy(p, "hello"); strcpy(p, "world"); // 现在可以安全修改 p 指向的内存注意:如果你使用
malloc,记得在适当的时候使用free(p)来释放内存,避免内存泄漏。
总结:
- 字符串字面量通常存储在只读内存中,不能通过指针修改它们。
- 如果你希望修改字符串内容,应该使用字符数组或动态分配内存。
3.1.2 字符串拼接,查找, 清除
例子:
//char *strcat(char *dest, const char *src);
char dest[20] = "Hello, ";
char src[] = "World!";
strcat(dest, src); // 结果:dest = "Hello, World!"
//char *strstr(const char *haystack, const char *needle);
char str[] = "Hello, World!";
char *result = strstr(str, "World"); // 返回指向 "World" 的指针
//void bzero(void *s, size_t len);
char buffer[10];
bzero(buffer, 10); // 将 buffer 数组的 10 个字节设置为 0
- strcat
- 目标字符串必须足够大:
strcat函数不会检查目标字符串的空间大小,若目标字符串的缓冲区不足以容纳源字符串的内容,将发生缓冲区溢出,导致未定义行为。- 为避免这种情况,建议使用
strncat,它允许你指定最大追加的字符数,从而减少溢出的风险。
- 为避免这种情况,建议使用
- 目标字符串必须以 \0 结尾:
strcat假设目标字符串是以\0结尾的。如果目标字符串没有正确的结尾,可能会导致未定义行为。
//char *strncat(char *dest, const char *src, size_t n);
//n为指定要追加的最多字符数,
//如果 src 的长度小于 n,则会将整个 src 字符串追加到 dest 后面。
//如果 src 长度大于 n,只会追加前 n 个字符。
char dest[20] = "Hello, ";
char src[] = "World! How are you?";
// 将 src 中最多 6 个字符追加到 dest
strncat(dest, src, 6);
printf("%s\n", dest); // 输出 "Hello, World"
- strstr
- 空字符串:如果
needle是空字符串(""),strstr会返回haystack的起始位置,即字符串本身的指针。 - 避免空指针:在使用
strstr前,确保haystack和needle都有效。如果传入NULL指针,程序会崩溃或产生未定义行为。
- bzero
-
非标准函数:
bzero是一个 POSIX 函数,并不是标准 C 函数。它在某些平台上已被弃用,推荐使用memset来替代://void *memset(void *s, int c, size_t n); memset(buffer, 0, 10); // 将 buffer 数组的 10 个字节设置为 0 int arr[5] = {1, 2, 3, 4, 5}; memset(arr, 0, sizeof(arr));// 将数组 arr 的所有字节设置为 0 -
避免空指针:确保传入的指针
s有效。若传入NULL指针,bzero将导致未定义行为。
3.1.3 字符串分割
strtok只能分割数组,不能分割字符串常量
char *p = "jack,rose,tom" ;//⭐不能用strtok分割
char str[]="jack,rose,tom" ;//可以用strtok分割
strtok 是 C 标准库中的一个字符串处理函数,用于将一个字符串分解为一系列的标记(tokens)。它通过指定的分隔符将字符串切分成多个部分,每次调用 strtok 返回一个标记,直到没有更多标记为止。
strtok 是一个破坏性函数,即它会修改输入的字符串,将分隔符替换为 \0,使得原始字符串在分解后变成多个以 \0 结尾的子字符串。
用法:
char *strtok(char *str, const char *delim);
- 参数:
str:要分解的字符串。如果这是⭐第一次调用strtok,需要传入字符串;⭐之后的调用应传入NULL,这样strtok会继续处理同一字符串。delim:用于分隔字符串的字符集,通常是一个字符串,包含所有分隔符字符(例如空格、逗号、句号等)。
- 返回值:
- 如果找到分隔符并成功提取一个标记,
strtok返回指向该标记的指针。 - 如果没有更多的标记可供提取,返回
NULL。
- 如果找到分隔符并成功提取一个标记,
例子:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, world! How are you?";
char *delim = " ,!?"; // ⭐使用空格、逗号、问号和感叹号作为分隔符
// 获取第一个标记
char *token = strtok(str, delim);// *token="Hello";
// 遍历并打印所有标记
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, delim); // 获取下一个标记
}
return 0;
}
输出:
Hello
world
How
are
you
说明:
- 第一次调用
strtok(str, delim)会返回字符串str中第一个标记(即Hello)。 - 之后,调用
strtok(NULL, delim)会继续处理相同的字符串,直到没有更多标记为止。
注意事项:
-
破坏输入字符串:
strtok会修改传入的字符串,它用\0替换分隔符。因此,原始字符串会被破坏,不能再使用原来的字符串内容。如果需要保留原始字符串,可以在使用strtok之前对其进行备份。
例如:
char str[] = "Hello, world!"; char str_copy[20]; strcpy(str_copy, str); // 备份原始字符串 -
线程不安全:
strtok不是线程安全的,因为它会使用一个静态变量来保存字符串的当前位置。如果多个线程调用strtok处理不同的字符串,可能会发生竞态条件。为了避免这种问题,可以使用strtok_r,它是线程安全的版本。
char *strtok_r(char *str, const char *delim, char **saveptr);#include <stdio.h> #include <string.h> int main() { char str[] = "Hello, world! How are you?"; char *delim = " ,!?"; // 使用空格、逗号、问号和感叹号作为分隔符 char *token; char *saveptr; // ⭐用于保存分解的位置 // 获取第一个标记⭐ token = strtok_r(str, delim, &saveptr); // 遍历并打印所有标记 while (token != NULL) { printf("%s\n", token); token = strtok_r(NULL, delim, &saveptr); // ⭐获取下一个标记 } return 0; } -
连续的分隔符:
- 如果输入字符串中包含连续的分隔符,
strtok会认为它们是多个分隔符,而不会返回空的标记。例如,在"Hello,,world"中,strtok会跳过两个逗号并返回"Hello"和"world",不会返回空字符串。
- 如果输入字符串中包含连续的分隔符,
-
分隔符的使用:
delim参数指定的分隔符字符是单个字符集,即可以是多个字符组成的字符串。只要是该字符集中的任意字符,就会被视作分隔符。
-
结束标记:
strtok在处理完所有标记后,返回NULL。所以,调用strtok后需要检查返回值是否为NULL来结束循环。
-
处理空字符串:
- 如果传入空字符串
"",strtok会立即返回NULL,表示没有更多的标记。
- 如果传入空字符串
-
首次调用和后续调用:
- 在首次调用时传入待分解的字符串
str,后续的调用都需要传入NULL来继续处理同一字符串。
- 在首次调用时传入待分解的字符串
3.1.4 字符串打包和解析
sprintf 和 sscanf 是 C 标准库中用于字符串格式化和解析的两个函数。它们分别用于将数据格式化为字符串并将字符串解析为数据。
1. sprintf - 格式化字符串并输出
功能:
sprintf 函数将格式化的输出写入字符串,而不是输出到控制台。它将数据(如整数、浮点数、字符等)转换为特定格式的字符串,并将结果存储在指定的字符数组中。
原型:
int sprintf(char *str, const char *format, ...);
- 参数:
str:指向字符数组的指针,用于存储格式化后的字符串。format:格式控制字符串,类似于printf中的格式字符串,定义了输出内容的格式。...:可变参数列表,指定要格式化的数据。
- 返回值:
- 返回写入的字符数,不包括终止符
\0。如果发生错误,返回一个负值。
- 返回写入的字符数,不包括终止符
示例:
#include <stdio.h>
int main() {
int num = 42;
float pi = 3.14159;
char str[50];
// 使用 sprintf 将数据格式化为字符串
sprintf(str, "Number: %d, Pi: %.2f", num, pi);
// 输出格式化后的字符串
printf("%s\n", str); // 输出: Number: 42, Pi: 3.14
return 0;
}
说明:
sprintf(str, "Number: %d, Pi: %.2f", num, pi);将整数num和浮点数pi格式化为字符串,并存储在str中。- 格式字符串
%d和%.2f表示整数和浮点数的格式。 - 最后,
str中的内容为"Number: 42, Pi: 3.14"。
注意事项:
- 缓冲区溢出:
sprintf不会检查目标字符数组的大小,因此可能会发生缓冲区溢出。为了防止这种情况,可以使用snprintf,它允许指定最大写入字符数。 - 返回值的使用:
sprintf的返回值是成功写入的字符数。如果目标字符串不够大,可能会发生溢出或数据丢失,因此需要确保目标数组足够大,或使用更安全的函数如snprintf。 - 性能考虑:由于
sprintf不进行边界检查,可能导致程序崩溃。因此,现代 C 编程中更推荐使用snprintf以确保安全。
2. sscanf - 从字符串中读取格式化数据
功能:
sscanf 函数从字符串中读取数据并将其解析到变量中,类似于 scanf,但是 sscanf 是从字符串而不是标准输入中读取数据。
原型:
int sscanf(const char *str, const char *format, ...);
- 参数:
str:要解析的输入字符串。format:格式控制字符串,定义了如何解析数据。...:要存储解析数据的变量。
- 返回值:
- 返回成功解析的字段数。如果没有成功解析任何字段,返回 0;如果发生错误,返回
EOF。
- 返回成功解析的字段数。如果没有成功解析任何字段,返回 0;如果发生错误,返回
示例:
#include <stdio.h>
int main() {
char str[] = "123 3.14 hello";
int num;
float pi;
char word[20];
// 使用 sscanf 从字符串中读取数据
sscanf(str, "%d %f %s", &num, &pi, word);
// 输出解析的数据
printf("Integer: %d, Float: %.2f, String: %s\n", num, pi, word);
// 输出: Integer: 123, Float: 3.14, String: hello
return 0;
}
说明:
sscanf(str, "%d %f %s", &num, &pi, word);从字符串str中解析出一个整数、一个浮点数和一个字符串。- 格式字符串
%d、%f和%s用来指定数据的类型和顺序。 - 解析成功后,变量
num、pi和word分别存储解析的结果。
注意事项:
- 格式匹配:
sscanf会根据格式字符串解析数据,如果格式不匹配,可能会导致解析错误或不完全的结果。因此,使用时要确保输入字符串与格式字符串匹配。 - 返回值:
sscanf返回成功解析的字段数。如果返回值小于预期的字段数,表示有错误或不匹配。在使用sscanf时,应该检查返回值来确保解析成功。 - 未成功读取的字段:如果某个字段没有被成功读取,相关变量的值不会改变。如果有不匹配的格式,
sscanf会停止解析,因此需要检查输入数据和格式是否一致。 - 安全性问题:像
sprintf,sscanf也可能面临缓冲区溢出的风险,特别是在读取字符串时,确保为字符数组提供足够的空间。
3.sscanf格式说明符
sscanf 是一个常用的函数,用于从字符串中按指定格式提取数据。它的格式说明符与 scanf 非常相似,用来定义输入数据的解析方式。
1. 整数类型
- %d:读取一个有符号十进制整数。
- 例如:
"123"被解析为123。
- 例如:
- %i:读取一个整数,支持十进制、八进制(以
0开头)和十六进制(以0x或0X开头)。- 例如:
"012"会被解析为10(八进制),"0x1F"会被解析为31(十六进制)。
- 例如:
- %u:读取一个无符号十进制整数。
- 例如:
"123"被解析为123。
- 例如:
- %o:读取一个八进制整数。
- 例如:
"012"被解析为10(八进制)。
- 例如:
- %x:读取一个十六进制整数。
- 例如:
"1A"被解析为26(十六进制)。
- 例如:
- %X:读取一个十六进制整数(大写字母)。
- 例如:
"1A"被解析为26(十六进制)。
- 例如:
2. 浮点数类型
- %f:读取一个浮点数(单精度)。例如:
"3.14"被解析为3.14。 - %lf:读取一个双精度浮点数。实际上,在
sscanf中,%f和%lf都可以用于读取浮点数(但%lf是为兼容scanf的标准写法)。 - %e、%E:读取科学计数法表示的浮点数,
%e输出小写e,%E输出大写E。例如:"1.23e3"被解析为1230。 - %g、%G:根据输入的数值自动选择
%e或%f格式。对于较小的数字,%g使用%f格式,对于较大的数字,使用%e格式。
3. 字符和字符串类型
- %c:读取一个字符。每次调用时,它会从输入字符串中读取一个字符。例如:
"A"被解析为'A'。 - %s:读取一个字符串,直到遇到空白字符(空格、换行符、制表符等)。读取的字符串不包括空白字符。注意:
%s不会读取空格后的部分。- 例如:
"hello world"会读取"hello"。
- 例如:
- %[set]:读取一组字符,直到遇到不在指定集合中的字符为止。
set是一个字符集,可以包含普通字符、范围符号(如0-9表示所有数字)等。- 例如:
%[0-9]会读取所有的数字直到遇到非数字字符。 - 例如:
%[a-zA-Z]会读取所有字母字符直到遇到非字母字符。
- 例如:
- ⭐%[^set]:读取所有不在指定集合中的字符。直到遇到集合中的字符为止。
set中的字符是不能读取的字符。- 例如:
%[^,]会读取所有非逗号(,)的字符直到遇到逗号为止。
- 例如:
4. 空白字符处理
- %n:不读取任何字符,但会将读取的字符数存入指定的整数变量中。
- 例如:在输入
"abc123"时,%n会将3存入变量,因为它已经读取了三个字符。
- 例如:在输入
5. 指针类型
- %p:读取一个指针类型(
void*)。它将解析十六进制的地址值。- 例如:
"0x7fffb9a1d3b0"被解析为一个地址。
- 例如:
常见的 sscanf 用法示例
-
读取整数:
int x; sscanf("123", "%d", &x); // 读取整数 123 printf("%d\n", x); // 输出:123 -
读取字符串:
char str[20]; sscanf("hello world", "%s", str); // 读取字符串 "hello" printf("%s\n", str); // 输出:hello -
读取多个值:
int age; float height; char name[50]; sscanf("25 5.9 John", "%d %f %s", &age, &height, name); printf("Age: %d, Height: %.2f, Name: %s\n", age, height, name); // 输出:Age: 25, Height: 5.90, Name: John -
⭐读取自定义格式的字符串:
char name[50]; int age; sscanf("name:jack,age:30", "name:%[^,],age:%d", name, &age);//⭐ printf("Name: %s, Age: %d\n", name, age); // 输出:Name: jack, Age: 30 -
读取带空格的字符串:
char sentence[100]; sscanf("This is a sentence.", "%[^\n]", sentence); // 读取整行,直到遇到换行符 printf("Sentence: %s\n", sentence); // 输出:Sentence: This is a sentence.
注意事项
-
缓冲区溢出:
使用%s、%[set]或%[^set]时,要小心缓冲区溢出问题。可以限制读取的字符数,例如:char str[10]; sscanf("hello", "%9s", str); // 最多读取 9 个字符,加上结尾的 '\0' -
返回值:
sscanf返回成功读取的项数,如果读取失败,返回值为0。如果遇到格式错误,返回值为负数。可以用它来判断解析是否成功:int result = sscanf("25", "%d", &age); if (result == 1) { printf("Read successfully\n"); } else { printf("Error reading\n"); } -
输入格式问题:
sscanf对输入字符串的格式要求严格,输入必须完全匹配格式字符串,否则可能无法成功解析。格式字符串中无法容忍额外的字符或不同的顺序。 -
不支持宽字符集:
sscanf默认只支持单字节字符,如果要解析宽字符(如中文),可能需要使用swscanf或其他相关函数。
4.比较 sprintf 和 sscanf:
| 功能 | sprintf |
sscanf |
|---|---|---|
| 作用 | 格式化数据并写入字符串 | 从字符串中解析数据 |
| 主要用法 | 将数据转换为字符串(写入输出) | 从输入字符串中提取数据(读取输入) |
| 输出目标 | 写入指定的字符数组 | 读取并存储到指定的变量中 |
| 返回值 | 成功写入的字符数 | 成功解析的字段数 |
| 常见问题 | 缓冲区溢出风险(未限制字符数) | 格式不匹配时解析失败,字段解析不完全时返回较少字段 |
练习:
1.假设存在一种简单的文本协议,消息格式为 [类型]:[长度]:[内容],例如 MSG:10:HelloWorld。编写一个函数,接收这样的协议字符串,提取出其中的消息类型、消息长度和消息内容,并分别返回对应的字符串(消息长度需转换为整数)。不允许使用sscanf,只能通过字符串处理函数(如 strtok、strlen 等)实现。
示例输入:"DATA:8:SampleMsg"
示例输出:
消息类型:DATA
消息长度:8
消息内容:SampleMsg
char data[]="DATA:8:SampleMsg";
char *name[]={"消息类型","消息长度","消息内容"};//⭐
int i=0;
char type[32]={0};
int len=0;
char msg[128]={0};
char *p = strtok(data, ":")
strcpy(type, p);
printf("%s:%s\n",name[i++],p );
while(1){
p = strtok(NULL, ":");
if(p == NULL) break;
if(i==1){
len = atoi(p);
}
if(i==2){
strcpy(msg, p);
}
printf("%s:%s\n",name[i++], p);
}
printf("%s:%d:%s\n", type, len, msg);
2.假设一个通信协议格式如下:
协议格式:[版本号]-[消息类型]-[发送方ID]-[接收方ID]:[内容长度]:[内容]
数据样例:
版本号:V1
消息类型:PING
发送方 ID:123
接收方 ID:456
内容长度:10
内容:SGVsbG8sIF
用sprintf打包输出
sprintf(buffer, "%s-%s-%d-%d:%d:%s","V1","PING",123, 456, 10,"SGVsbG8sIF");
3.2 时间函数(了解)
头文件#include<time.h>你提到的几个时间相关的函数和结构体主要是用于获取当前的时间、时间转换以及处理时间的操作。下面是这些知识点的整合与详细介绍:
1. time 函数
time_t time(time_t *tloc);
time 函数用于获取当前的时间,并返回一个 time_t 类型的值。time_t 是一个长整型数值,表示从 1970-01-01 00:00:00 UTC (协调世界时) 到当前时刻的秒数。这个值被称为 "UNIX 时间戳"。
- 参数:
time_t *tloc是一个指针,指向存储当前时间的变量。如果该指针为NULL,则只返回当前时间,不会写入任何值。 - 返回值:当前的时间(以秒为单位的 UNIX 时间戳)。
2. localtime 函数
struct tm *localtime(const time_t *timep);
localtime 函数将 time_t 类型的时间戳转换为当地时间(本地时区时间)。它返回一个指向 struct tm 的指针,其中包含了当前时间的详细组成部分(年、月、日、时、分、秒等)。
- 参数:
time_t *timep是指向时间戳的指针。 - 返回值:指向
struct tm结构体的指针。struct tm包含了具体的时间信息,包括年、月、日、小时、分钟、秒等。
struct tm 结构体的成员说明:
tm_sec:秒数(0-60)。通常是 0-59,但在闰秒情况下可能是 60。tm_min:分钟数(0-59)。tm_hour:小时数(0-23)。tm_mday:日期(1-31)。tm_mon:月份(0-11),0 代表 1 月,11 代表 12 月。tm_year:自 1900 年以来的年数。比如 2025 年,tm_year为 125。tm_wday:星期几(0-6),0 表示星期天。tm_yday:一年中的天数(0-365),0 表示 1 月 1 日。tm_isdst:夏令时标志(0 表示不使用夏令时,1 表示使用夏令时,负值表示无法知道是否使用夏令时)。
3. gettimeofday 函数
int gettimeofday(struct timeval *tv, struct timezone *tz);
gettimeofday 函数用于获取当前的时间。它返回自 "1970-01-01 00:00:00 UTC" 到当前时刻的秒数和微秒数,并存储在 tv 结构体中。同时,它还可以提供时区信息(虽然这个参数通常在现代应用中不常用)。
- 参数:
struct timeval *tv:这是一个指向struct timeval结构体的指针,用于存储当前的时间。struct timeval包含了秒(tv_sec)和微秒(tv_usec)。struct timezone *tz:指向struct timezone结构体的指针,存储时区信息(通常现在不使用这个参数)。
- 返回值:如果成功,返回 0;如果出错,返回 -1,并设置
errno。
struct timeval 结构体的成员说明:
tv_sec:自 1970 年以来的秒数。tv_usec:微秒数(0-999,999),表示当前秒的微秒部分。
4. 示例代码:将 time_t 转换为年月日时分秒格式
#include <stdio.h>
#include <time.h>
int main() {
time_t rawtime;
struct tm *timeinfo;
// 获取当前时间
time(&rawtime);
// 将时间转换为本地时间
timeinfo = localtime(&rawtime);
// 输出年月日时分秒
printf("当前时间:%d-%02d-%02d %02d:%02d:%02d\n",
timeinfo->tm_year + 1900,
timeinfo->tm_mon + 1,
timeinfo->tm_mday,
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
return 0;
}
5. 注意事项
localtime返回的是一个指向静态内存的指针,因此每次调用localtime都会覆盖之前返回的值。为了避免这种问题,可以使用localtime_r(线程安全版本)。gettimeofday返回的微秒部分对很多应用场景非常有用,特别是在高精度时间计算或者性能分析时。
本文来自博客园,作者:cyt2730,转载请注明原文链接:https://www.cnblogs.com/cytwjyy/p/18865713

浙公网安备 33010602011771号