函数

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参数列表

img

作用:在程序运行的时候传入参数

#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;
}

img


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;
}

img

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,...);

img

通过参数栈来实现参数传递, 在函数内部通过下面这些类型和宏来实现参数获取:

#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 中
  1. strlen计算的字符串必须以\0为结尾

  2. strlen不会检查空指针,如果传入空指针回导致程序崩溃或未定义行为。

  3. strcmp如果两个字符串有一个为空字符串"",strcmp会认为非空字符串大于空字符串。

  4. strcmp同样需要避免空指针。

⭐注意:

在 C 语言中,char *p = "hello"; strcpy(p, "world");报错是因为字符串字面量(如 "hello")在大多数编译器中是存储在只读内存区域的。

问题分析:

  1. char *p = "hello";
    这里,p 是一个指向字符的指针,指向字符串字面量 "hello"。在大多数 C 编译器中,字符串字面量会被存储在只读的内存区域,因此你不能通过 p 来修改它。
  2. strcpy(p, "world");
    你试图将 "world" 字符串复制到 p 指向的内存区域,即原本存储 "hello" 的位置。但这个内存区域是只读的,因此会触发一个 段错误(segmentation fault),这就是“核心已转储”错误。

错误原因:

  • 只读内存区域:C 中的字符串字面量通常存储在只读内存区域,因此尝试修改它们会导致程序崩溃。
  • 未分配可写内存p 指向的是一个只读字符串,而不是你可以自由修改的内存区域。

解决方法:

  1. 使用字符数组
    如果你希望修改字符串,可以将 p 定义为字符数组,而不是指向字符串字面量的指针。

    char p[6] = "hello";  // 使用字符数组
    strcpy(p, "world");   // 可以修改 p 的内容
    

    这样,p 就是一个可写的数组,包含 "hello",并且可以使用 strcpy 来修改其内容。

  2. 动态分配内存
    如果你需要使用指针,也可以通过 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
  1. 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"
  1. strstr
  • 空字符串:如果 needle 是空字符串(""),strstr 会返回 haystack 的起始位置,即字符串本身的指针。
  • 避免空指针:在使用 strstr 前,确保 haystackneedle 都有效。如果传入 NULL 指针,程序会崩溃或产生未定义行为。
  1. 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) 会继续处理相同的字符串,直到没有更多标记为止。

注意事项:

  1. 破坏输入字符串

    • strtok 会修改传入的字符串,它用 \0 替换分隔符。因此,原始字符串会被破坏,不能再使用原来的字符串内容。如果需要保留原始字符串,可以在使用 strtok 之前对其进行备份。

    例如:

    char str[] = "Hello, world!";
    char str_copy[20];
    strcpy(str_copy, str);  // 备份原始字符串
    
  2. 线程不安全

    • 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;
    }
    
  3. 连续的分隔符

    • 如果输入字符串中包含连续的分隔符,strtok 会认为它们是多个分隔符,而不会返回空的标记。例如,在 "Hello,,world" 中,strtok 会跳过两个逗号并返回 "Hello""world",不会返回空字符串。
  4. 分隔符的使用

    • delim 参数指定的分隔符字符是单个字符集,即可以是多个字符组成的字符串。只要是该字符集中的任意字符,就会被视作分隔符。
  5. 结束标记

    • strtok 在处理完所有标记后,返回 NULL。所以,调用 strtok 后需要检查返回值是否为 NULL 来结束循环。
  6. 处理空字符串

    • 如果传入空字符串 ""strtok 会立即返回 NULL,表示没有更多的标记。
  7. 首次调用和后续调用

    • 在首次调用时传入待分解的字符串 str,后续的调用都需要传入 NULL 来继续处理同一字符串。

3.1.4 字符串打包和解析

sprintfsscanf 是 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"
注意事项:
  1. 缓冲区溢出sprintf 不会检查目标字符数组的大小,因此可能会发生缓冲区溢出。为了防止这种情况,可以使用 snprintf,它允许指定最大写入字符数。
  2. 返回值的使用sprintf 的返回值是成功写入的字符数。如果目标字符串不够大,可能会发生溢出或数据丢失,因此需要确保目标数组足够大,或使用更安全的函数如 snprintf
  3. 性能考虑:由于 sprintf 不进行边界检查,可能导致程序崩溃。因此,现代 C 编程中更推荐使用 snprintf 以确保安全。

2. sscanf - 从字符串中读取格式化数据

功能:

sscanf 函数从字符串中读取数据并将其解析到变量中,类似于 scanf,但是 sscanf 是从字符串而不是标准输入中读取数据。

原型:
int sscanf(const char *str, const char *format, ...);
  • 参数
    • str:要解析的输入字符串。
    • format:格式控制字符串,定义了如何解析数据。
    • ...:要存储解析数据的变量。
  • 返回值
    • 返回成功解析的字段数。如果没有成功解析任何字段,返回 0;如果发生错误,返回 EOF
示例:
#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 用来指定数据的类型和顺序。
  • 解析成功后,变量 numpiword 分别存储解析的结果。
注意事项:
  1. 格式匹配sscanf 会根据格式字符串解析数据,如果格式不匹配,可能会导致解析错误或不完全的结果。因此,使用时要确保输入字符串与格式字符串匹配。
  2. 返回值sscanf 返回成功解析的字段数。如果返回值小于预期的字段数,表示有错误或不匹配。在使用 sscanf 时,应该检查返回值来确保解析成功。
  3. 未成功读取的字段:如果某个字段没有被成功读取,相关变量的值不会改变。如果有不匹配的格式,sscanf 会停止解析,因此需要检查输入数据和格式是否一致。
  4. 安全性问题:像 sprintfsscanf 也可能面临缓冲区溢出的风险,特别是在读取字符串时,确保为字符数组提供足够的空间。

3.sscanf格式说明符

sscanf 是一个常用的函数,用于从字符串中按指定格式提取数据。它的格式说明符与 scanf 非常相似,用来定义输入数据的解析方式。

1. 整数类型
  • %d:读取一个有符号十进制整数。
    • 例如:"123" 被解析为 123
  • %i:读取一个整数,支持十进制、八进制(以 0 开头)和十六进制(以 0x0X 开头)。
    • 例如:"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 用法示例

  1. 读取整数

    int x;
    sscanf("123", "%d", &x);  // 读取整数 123
    printf("%d\n", x);  // 输出:123
    
  2. 读取字符串

    char str[20];
    sscanf("hello world", "%s", str);  // 读取字符串 "hello"
    printf("%s\n", str);  // 输出:hello
    
  3. 读取多个值

    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
    
  4. 读取自定义格式的字符串

    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
    
  5. 读取带空格的字符串

    char sentence[100];
    sscanf("This is a sentence.", "%[^\n]", sentence);  // 读取整行,直到遇到换行符
    printf("Sentence: %s\n", sentence);
    // 输出:Sentence: This is a sentence.
    
注意事项
  1. 缓冲区溢出
    使用 %s%[set]%[^set] 时,要小心缓冲区溢出问题。可以限制读取的字符数,例如:

    char str[10];
    sscanf("hello", "%9s", str);  // 最多读取 9 个字符,加上结尾的 '\0'
    
  2. 返回值
    sscanf 返回成功读取的项数,如果读取失败,返回值为 0。如果遇到格式错误,返回值为负数。可以用它来判断解析是否成功:

    int result = sscanf("25", "%d", &age);
    if (result == 1) {
        printf("Read successfully\n");
    } else {
        printf("Error reading\n");
    }
    
  3. 输入格式问题
    sscanf 对输入字符串的格式要求严格,输入必须完全匹配格式字符串,否则可能无法成功解析。格式字符串中无法容忍额外的字符或不同的顺序。

  4. 不支持宽字符集
    sscanf 默认只支持单字节字符,如果要解析宽字符(如中文),可能需要使用 swscanf 或其他相关函数。

4.比较 sprintfsscanf

功能 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 返回的微秒部分对很多应用场景非常有用,特别是在高精度时间计算或者性能分析时。
posted @ 2025-05-08 10:02  cyt2730  阅读(67)  评论(0)    收藏  举报