×
Featured image of post 换个姿势学C语言第6章

换个姿势学C语言第6章

换个姿势学C语言 第6章 创建自己的函数库

0. 说明

《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

Snipaste_2024-03-10_14-51-10.png

这是一本非常不错的书!

6. 创建自己的函数库

渐渐地,读者已经发现在程序设计中实现某种功能的代码经常被重复使用。不建议在写程序时经常复制和粘贴那些代码,这会使程序变得臃肿和复杂,可以将这些功能设计成独立的函数,在需要使用时可以随时调用它们。

除此之外,还可以建立自己的函数库,将函数代码编译成库文件代他人或自己使用。

本章涉及:

  • 函数库的类型
  • 自定义处理字符串的函数
  • 自定义处理键盘输入的函数
  • 在Visual Studio 2020中创建静态库

本章将实现一些字符串的函数:

  • 计算字符串长度— strLength;
  • 在字符串是查找特定字符的位置— indexOfChar;
  • 转换字符串中大写字母为小写字母—toLowerCase;
  • 转换字符串大小写字母为大写字母—toUpperCase;
  • 复制字符串—strCopy。

还要实现几个用于提高键盘输入数据效率的函数:

  • 输入整数型—inputInteger;
  • 输入字符—inputChar;
  • 输入字符串—inputStr。

这些函数可以用于一下的外汇牌价看板的开发。

6.1 什么是函数库

  • 函数库是以重复利用程序为目的的且经过编译的函数的二进制代码 。
  • 函数库分为动态链接库和静态链接库两种。在生成可执行文件时,这两种库文件的处理方式是不同的。
6.1.1 静态链接库

静态链接库是最简单的处理方式。如果在程序中引用一个静态函数库,编译时程序会先被编译成一个目标文件(二进制代码),然后链接器会将这个目标文件和静态函数库中的二进制代码一起合并成一个可执行文件,这个可执行文件就可以脱离函数库而独立运行。

前面使用的外汇牌价接口库就是一个静态链接库,它的二进制代码会和调用它的代码一起生成一个可执行文件。

⚠️ 警告
静态函数库的缺点:当有很多个程序都引用同一个库时,每个程序的可执行文件都要包含一份同样的函数代码,这样就会浪费内存和磁盘空间;如果这个函数库需要升级,则所有使用该静态函数库的程序都要重新编译和链接!!
6.1.2 动态链接库

动态链接库克服了静态链接库的缺点。动态函数库文件独立地存在于某些系统目录下,在编译程序时动态函数库中的代码不被包含在可执行文件中,程序在使用到库函数时再从内存或磁盘的指定位置寻找动态函数库的代码。如在windows操作系统中,部分动态链接库被存放于C:\Windows\System32目录下。

动态链接库有两个好处,首先是可以节约内存和磁盘空间,其次是在函数库升级时不需要重新编译、链接使用该库的程序。

凡事总有例外,有时某些软件的安装包中并不包含运行时所需的动态链接库文件,而运行它的计算机上恰好也没有该动态链接库文件,这时候就会报错!!!

6.2 自定义字符串处理函数

对字符串进行处理是程序员的基本功之一。尽管包括C语言在内的高级编程语言都提供了处理字符串的库函数,但是为了锻炼自己的编程思维,加深对现有知识的掌握,在本节中我们还是要编写一些函数来处理字符串。

要创建字符串处理的函数库,可以分成两步:

  • 第1步:创建字符串处理的函数。
  • 第2步:将第1步中的函数代码编译成函数库。

我们分步处理,先来创建一些有用的函数。

6.2.1 计算字符串长度

计算字符串长度就是计算字符串中字符的个数( 不包含字符串终止符"\0"), 是程序中经常要进行的基础操作之一,因此考虑将其写成自定义函数。

在C语言的字符串中并没有一个地方存储着每个字符串的长度,不也能用sizeof运算符获取字符串的长度。

  • sizeof是运算符,用于计算变量 / 数据类型占用的内存字节数,它计算的是整个存储区域的大小,包括字符串末尾的\0(字符串结束符),甚至如果是字符数组,还会包含数组中未赋值的部分。

要计算字符串长度,就得从头开始一个一个“数”字符,直到碰到字符串终止符"\0"为止,计算机很擅长做重复的事情,计算机不会累,而且速度还很快,所以就一个一个数!

要学习以下编程思维!!!

和以前一样,在开始进行函数设计之前考虑下面的问题。

  1. 函数的功能是什么?

    计算字符串中“\0”之前的字符个数。

  2. 函数的名字是什么?

    strLength,意思是“字符串长度”,在标准库函数中strlen函数可实现同样的功能。因此这里使用strLength作为自定义函数的名字以避免发生冲突。

  3. 函数需要传入哪些参数?

    传入字符串的首地址,参数类型是字符指针(char*),名为str,但由于函数中不允许修改字符串的值,所以参数类型应为const char*

  4. 函数是否需要返回值?返回何种类型的值?

    当然需要,返回整型值,也就是字符串的长度。字符串长度的最小值是0,不可能是负值,因此可以使用unsigned int型(无符号整型)。

确定了这些后,函数的原型就确定了!!!

1
unsigned int strLength(const char* str);
6.2.1.1 测试程序

即使现在还不知道strLength的代码该如何实现,但根据函数的原型,我们可以先确定代码的基本结构,测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// L06_01_COMMON_TOOLS.cpp :
// 通用工具库
//
#include <stdio.h>

unsigned int strLength(const char* str)
{
    // todo: 此函数还未完成
    return 0;
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'l','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    unsigned int length = strLength(string);
    printf("字符数组string的长度是:%d\n", length);

    return 0;
}

此时也可以运行程序,但结果是不对的:

1772979513414.png

6.2.1.2 实现strLength函数

以下是实现的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
//
#include <stdio.h>

// 计算字符串长度
unsigned int strLength(const char* str)
{
    // 定义存储字符串长度的变量
    unsigned int length = 0;
    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        printf("当前字符%c的地址为:%p\n", *str, str);
        // 长度值增1
        length++;
        // 指针变量str的值+1,表示指向下一个元素
        str++;
        // 最后打印出计算出的总长度
        printf("length:%d\n", length);
    }
    return length;
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'l','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    unsigned int length = strLength(string);
    printf("字符数组string的长度是:%d\n", length);

    return 0;
}

运行程序:

Snipaste_2026-03-08_22-43-19.png

可以看到能正常获取字符串长度。

注意,此函数仅适用于 英文字符(ASCII):在 C 语言中通常以单字节存储(char类型),\0也是单字节,你的函数通过逐个字节判断\0来计数,对英文完全适用。不适用中文字符。

优化后的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
//
#include <stdio.h>

// 计算字符串长度
unsigned int strLength(const char* str)
{
    // 定义存储字符串长度的变量
    unsigned int length = 0;

    if (str == NULL) { // 增加空指针判断,避免崩溃
        return 0;
    }
    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        printf("当前字符%c的地址为:%p\n", *str, str);
        // 长度值增1
        length++;
        // 指针变量str的值+1,表示指向下一个元素
        str++;
        // 最后打印出计算出的总长度
        printf("length:%u\n", length);
    }
    return length;
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'l','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    unsigned int length = strLength(string);
    printf("字符数组string的长度是:%d\n", length);

    // 测试空指针
    const char* str = NULL;
    printf("总长度:%u\n", strLength(str));

    return 0;
}
6.2.2 在字符串是查找特定字符的位置

查找字符串是是否包含指定的字符是经常要进行的工作,例如用户输入Email地址,就需要判断其中是否存在字母@.点字符。

查找的结果有两种:

  • 找到了这个字符,此时应该返回字符的位置(第几个,从0开始计数),以便 程序进行进一步的处理。
  • 没有找到,此时可以返回负数,以区别于找到的情况。

用与前文同样的方法考虑这个函数的设计:

  1. 函数的功能是什么?

在一个字符串中查找特定字符的出现位置(从0开始计数),未找到返回-1。

  1. 函数的名字是什么?

indexOfChar,意思是“字符的索引”。

  1. 函数需要传入哪些参数?

需要传入字符串的首地址,参数类型是字符指针(char*),名为str,但由于函数中不允许修改字符串的值,所以参数类型应为const char*。另外,还需要传入需要查找的字符,类型为char

  1. 函数是否需要返回值?返回何种类型的值?

当然需要,返回有符号整型值,使用int型(有符号整型)。

确定了这些后,函数的原型就确定了!!!

1
int indexOfChar(const char* str, char ch);

此时参考上一节的方法来实现该函数并写测试程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
//
#include <stdio.h>

// 在字符串中查找特定字符的位置
int indexOfChar(const char* str, char ch)
{
    // 定义存储索引的位置
    int index = 0;

    if (str == NULL) { // 增加空指针判断,避免崩溃
        return -1;
    }
    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *str, str);
        // 判断是否相等
        if (*str == ch)
        {
            printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 匹配\n", str, index, *str, ch);
            return index;
        }

        printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 不匹配\n", str, index, *str, ch);
        // 指针变量str的值+1,表示指向下一个元素
        str++;
        index++;
    }
    return -1;
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'l','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    char ch = 'e';
    int index = indexOfChar(string, ch);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch, string, index);

    char ch1 = 'l';
    int index1 = indexOfChar(string, ch1);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch1, string, index1);

    char ch2 = 'A';
    int index2 = indexOfChar(string, ch2);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch2, string, index2);

    return 0;
}

此时,运行代码,输出如下:

Snipaste_2026-03-15_20-02-15.png

此时,能正常在字符串中查找到特定字符,输出结果是对的。但输出的原始字符串在不停的变。原因:

  • str++ 会改变指针指向的位置

  • 一旦移动,原来的开头就访问不到了

  • 遍历字符串一定要用临时指针,不要动原始指针!

因此,对代码进行代码,优化后代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
//
#include <stdio.h>

// 在字符串中查找特定字符的位置
int indexOfChar(const char* str, char ch)
{
    // 定义存储索引的位置
    int index = 0;

    if (str == NULL) { // 增加空指针判断,避免崩溃
        return -1;
    }

    // 遍历字符串一定要用临时指针,不要动原始指针!
    // 用临时指针遍历,不修改原始 str
    const char* p = str;

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*p != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否相等
        if (*p == ch)
        {
            printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 匹配\n", str, index, *p, ch);
            return index;
        }

        printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 不匹配\n", str, index, *p, ch);
        // 临时指针变量p的值+1,表示指向下一个元素
        p++;
        index++;
    }
    return -1;
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'l','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    char ch = 'e';
    int index = indexOfChar(string, ch);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch, string, index);

    char ch1 = 'l';
    int index1 = indexOfChar(string, ch1);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch1, string, index1);

    char ch2 = 'A';
    int index2 = indexOfChar(string, ch2);
    printf("字符%c在字符串%s的位置是:%d\n\n", ch2, string, index2);

    return 0;
}

此时运行代码,输出结果如下:

Snipaste_2026-03-15_20-13-09.png

6.2.3 转换字符串中的大写字母为小写字母

在一个字符串中,可能同时存在大写、小写字母和其他字母,在程序设计中经常要对其中的大小写字母相互转换。绝大多数的编程语言都提供大小写字母转换的函数,如Python里面字符串的.upper()方法和.lower()方法就可以实现字符串大小写转换。

我们先进行大写字母转换成小写字母的原型设计,再完成测试程序。

用与前文同样的方法考虑这个函数的设计:

  1. 函数的功能是什么?

将现在字符串中的大写字母转换成小写字母。

  1. 函数的名字是什么?

toLowerCase,意思是“转换为小写”。

  1. 函数需要传入哪些参数?

需要传入字符串的首地址,参数类型是字符指针(char*),名为str,之所以参数类型不是const char*,是因为我们要通过传入的字符指针改变其指向位置的值(原地转换)。

  1. 函数是否需要返回值?返回何种类型的值?

不需要,因为函数已经处理了字符数组中的字符。

确定了这些后,函数的原型就确定了!!!

1
void toLowerCase(char* str);

在第五章5.3节总结参考换个姿势学C语言 第5章 5.3节 中已经知道了字母在ASCII中的值:

  • a-z是97-122,而A-Z是65-90。

因些,我们们可以像之前的函数一样,一个个的读取字符串中的字符,然后进行判断就行了!

然后,我的代码就成这样了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

// 将字符串中大写字母转换成小写字符
void toLowerCase(char* str)
{
    if (str == NULL) { // 增加空指针判断,避免崩溃
        return;
    }
    // 由于需要对原始字符串进行修改,此处不使用临时指针

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否是大写字母
        if (*str >= 'A' && *str <= 'Z')
        {
            *str += 'a' - 'A';
        }

        // 指针变量str的值+1,表示指向下一个元素
        str++;
    }
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'L','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    toLowerCase(string);
    printf("字符数组string中存储的字符串是:%s\n", string);

    // 测试空指针异常
    toLowerCase(NULL);

    char str[] = "ABCDefgh";
    printf("字符串str转换前是:%s\n", str);
    toLowerCase(str);
    printf("字符串str转换后是:%s\n", str);

    return 0;
}

运行结果:

Snipaste_2026-03-24_22-10-53.png

相应的,将小写转换成大写也比较简单了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

// 将字符串中小写字母转换成大写字符
void toUpperCase(char* str)
{
    if (str == NULL) { // 增加空指针判断,避免崩溃
        return;
    }
    // 由于需要对原始字符串进行修改,此处不使用临时指针

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否是小写字母
        if (*str >= 'a' && *str <= 'z')
        {
            *str -= 'a' - 'A';
        }

        // 指针变量str的值+1,表示指向下一个元素
        str++;
    }
}

int main()
{
    // 定义一个字符串
    char  string[10] = { 'H', 'e', 'l', 'L','o', '\0' };

    printf("字符数组string的首地址为:%p\n", string);
    printf("字符数组string中存储的字符串是:%s\n", string);
    toUpperCase(string);
    printf("字符数组string中存储的字符串是:%s\n", string);

    // 测试空指针异常
    toUpperCase(NULL);

    char str[] = "ABCDefgh";
    printf("字符串str转换前是:%s\n", str);
    toUpperCase(str);
    printf("字符串str转换后是:%s\n", str);

    return 0;
}

这个时候,运行程序:

Snipaste_2026-03-24_22-20-48.png

字符串正常都变成大写了!!!

6.2.4 复制字符串

将一个内存区域内的字符串复制到另外一个区域,也是程序中经常需要进行的操作。

如有两个数组:

1
2
char  str_a[10] = { 'H', 'e', 'l', 'l','o', '\0' };
char  str_b[10];

字符数组str_a的前6个元素已经被初始化,后4个没有;字符数组str_b完全没有被初始化。

我们的任务是把str_a所指向的内存区域的字符串复制到str_b所指向的内存区域。

用与前文同样的方法考虑这个函数的设计:

  1. 函数的功能是什么?

复制字符串,把一个内存位置的字符串(包括字符串终止符)复制到另一个内存位置上去。

  1. 函数的名字是什么?

strCopy,意思是“字符串复制”。

  1. 函数需要传入哪些参数?

需要传入源字符串的首地址,参数类型是字符指针(const char*),名为source; 还需要传入目标字符串的首地址,参数类型是字符指针(char*), 名为destination

  1. 函数是否需要返回值?返回何种类型的值?

不需要,暂时不需要返回值。

确定了这些后,函数的原型就确定了!!!

1
void strCopy(char* destination, const char* source);

然后我来实现这个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>

// 将源字符串复制到目标字符串中
void strCopy(char* destination, const char* source)
{
    if (source == NULL || destination == NULL) { // 增加空指针判断,避免崩溃
        return;
    }

    // 遍历字符串一定要用临时指针,不要动原始指针!
    // 用临时指针遍历,不修改原始 source
    const char* p = source;

    // 判断指针p指向的值是否为字符串结束符'\0'
    while (*p != '\0')
    {
        // 直接将当前字符复制到目录字符对应位置
        *destination = *p;

        // 指针变量p的值+1,表示指向下一个元素
        p++;
        destination++;
    }

    // 最后补字符串结束符
    *destination = '\0';
}

int main()
{
    // 定义一个字符串
    char  str_a[10] = { 'H', 'e', 'l', 'L','o', '\0' };
    char  str_b[10];

    printf("字符数组str_a的首地址为:%p\n", str_a);
    printf("字符数组str_a中存储的字符串是:%s\n", str_a);
    strCopy(str_b, str_a);
    printf("字符数组str_b中存储的字符串是:%s\n", str_b);

    // 测试空指针异常
    strCopy(NULL, str_a);

    char source[] = "ABCDefgh";
    printf("字符串source的内容是:%s\n", source);
    char dest[10];
    // 未初始化的字符串不要去打印,会出现奇怪的 烫 字
    // printf("字符串dest复制前是:%s\n", dest);
    strCopy(dest, source);
    printf("字符串dest  复制后是:%s\n", dest);

    return 0;
}

然后运行程序:

Snipaste_2026-03-24_23-37-43.png

可以看到,正常将源字符串复制到目录字符串上面了。

6.2.5 自定义字符串函数的其他需求
  • 让函数支持链式表达式。这种就希望函数有返回值。以上面的strCopy函数为例,当return destination时,就可以返回目标字符串的首地址。

我们来测试一下:

对程序进行一下优化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>

// 将源字符串复制到目标字符串中
char* strCopy(char* destination, const char* source)
{
    char* returnValue = destination;
    if (source == NULL || destination == NULL) { // 增加空指针判断,避免崩溃
        return NULL;
    }

    // 遍历字符串一定要用临时指针,不要动原始指针!
    // 用临时指针遍历,不修改原始 source
    const char* p = source;

    // 判断指针p指向的值是否为字符串结束符'\0'
    while (*p != '\0')
    {
        // 直接将当前字符复制到目录字符对应位置
        *destination = *p;

        // 指针变量p的值+1,表示指向下一个元素
        p++;
        destination++;
    }

    // 最后补字符串结束符
    *destination = '\0';
    return returnValue;
}

// 将字符串中小写字母转换成大写字符
char* toUpperCase(char* str)
{
    char* returnValue = str;
    if (str == NULL) { // 增加空指针判断,避免崩溃
        return NULL;
    }
    // 由于需要对原始字符串进行修改,此处不使用临时指针

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否是小写字母
        if (*str >= 'a' && *str <= 'z')
        {
            *str -= 'a' - 'A';
        }

        // 指针变量str的值+1,表示指向下一个元素
        str++;
    }
    return returnValue;
}

int main()
{
    // 定义一个字符串
    char  str_a[10] = { 'H', 'e', 'l', 'L','o', '\0' };
    char  str_b[10];

    printf("字符数组str_a的首地址为:%p\n", str_a);
    printf("字符数组str_a中存储的字符串是:%s\n", str_a);
    toUpperCase(strCopy(str_b, str_a));
    printf("字符数组str_b中存储的字符串是:%s\n", str_b);

    // 测试空指针异常
    strCopy(NULL, str_a);

    char source[] = "ABCDefgh";
    printf("字符串source的内容是:%s\n", source);
    char dest[10];
    toUpperCase(strCopy(dest, source));
    printf("字符串dest  复制转换后是:%s\n", dest);

    return 0;
}

Snipaste_2026-03-28_20-23-11.png

可以看到toUpperCase(strCopy(str_b, str_a));toUpperCase(strCopy(dest, source));实现了字符串的复制,并将复制后的字符串变成大写字母。

  • 使用断方检查非法参数值。函数的作者和调用者可能不是一个人,即使是同一个人写的程序在调用函数时也可能会犯错。作为函数的作者,我们要防范函数的调用者传入不恰当的参数。比如画圆的时候,用户传入了一个负值作为圆的半径,这个时候用负数画圆就会有问题。
  • C语言中可以使用断言(assert),程序运行到断言处如果断言不成立,则立即终止程序运行,而不会执行后面的语句。这种机制可以帮助程序员快速发现可能存在问题的地方,而不是等到一个错误导致了另一个错误时再来追查。

以上面的代码为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
//
#include <stdio.h>
#include <assert.h>

// 将源字符串复制到目标字符串中
char* strCopy(char* destination, const char* source)
{
    char* returnValue = destination;
    assert(source != NULL && destination != NULL);
    if (source == NULL || destination == NULL) { // 增加空指针判断,避免崩溃
        return NULL;
    }

    // 遍历字符串一定要用临时指针,不要动原始指针!
    // 用临时指针遍历,不修改原始 source
    const char* p = source;

    // 判断指针p指向的值是否为字符串结束符'\0'
    while (*p != '\0')
    {
        // 直接将当前字符复制到目录字符对应位置
        *destination = *p;

        // 指针变量p的值+1,表示指向下一个元素
        p++;
        destination++;
    }

    // 最后补字符串结束符
    *destination = '\0';
    return returnValue;
}

// 将字符串中小写字母转换成大写字符
char* toUpperCase(char* str)
{
    char* returnValue = str;
    assert(str != NULL);
    if (str == NULL) { // 增加空指针判断,避免崩溃
        return NULL;
    }
    // 由于需要对原始字符串进行修改,此处不使用临时指针

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否是小写字母
        if (*str >= 'a' && *str <= 'z')
        {
            *str -= 'a' - 'A';
        }

        // 指针变量str的值+1,表示指向下一个元素
        str++;
    }
    return returnValue;
}

int main()
{
    // 定义一个字符串
    char  str_a[10] = { 'H', 'e', 'l', 'L','o', '\0' };
    char  str_b[10];

    printf("字符数组str_a的首地址为:%p\n", str_a);
    printf("字符数组str_a中存储的字符串是:%s\n", str_a);
    toUpperCase(strCopy(str_b, str_a));
    printf("字符数组str_b中存储的字符串是:%s\n", str_b);

    // 测试空指针异常
    strCopy(NULL, str_a);

    char source[] = "ABCDefgh";
    printf("字符串source的内容是:%s\n", source);
    char dest[10];
    toUpperCase(strCopy(dest, source));
    printf("字符串dest  复制转换后是:%s\n", dest);

    return 0;
}

11行加入了个断言判断assert(source != NULL && destination != NULL);,然后执行程序:

Snipaste_2026-03-28_21-16-01.png

前面的运行正常了,在运行测试空指针异常时代码时,就直接调用abort(),并且输出中file D:\BC101\Examples\L06\L06_01_COMMON_TOOLS\L06_01_COMMON_TOOLS.cpp, line 11 暴露源码路径、行号,不安全。

  • abort()函数 属于 C 标准库,头文件是#include <stdlib.h>
6.2.6 字符串处理的库函数
  • 我们自己编写字符串处理函数的目的主要是为了加深对字符串处理的理解和练习,C语言的标准库也包含一些常用的字符串处理函数。在日常开发中可以使用这些库函数,但首先应包含头文件string.h
1
#include <string.h>

其中,strlen函数与我们定义的strLength函数是一致的,strchrindexOfChar函数功能接近,strcpy函数和strCopy函数功能接近。

没必要记住所有的库函数,在需要使用时再去查询它们的使用方法即可。

6.3 处理键盘输入

由于外汇牌价看板程序所需的数据来自网络,因此此前只介绍了少数几个键盘输入函数,但是处理键盘输入也是程序设计中的常规任务。

之前我们使用getchar函数来获取单个字符输入,使用scanf函数获取从键盘输入的字符串,scanf也可以用于输入数值。

换个姿势学C语言 第5章 获取完整的牌价数据 5.3节 字符和字符串 中曾使用过scanf来获取用户输入数据。

  • scanf函数运行时,如果键盘缓冲区中没有包含换行符的内容,会停下来等待用户输入;在用户输入若干个字符并按下回车键后,开始进行处理;
  • scanf函数将键盘缓冲区中的内容复制到数组中,但也不是全部复制—–当scanf函数遇到空格或换行符时将不再复制,没有复制完的字符仍然留在键盘缓冲区中;
  • scanf函数还做了一件事:自动在用户输入的最后一个字符后加上字符串终止符\n并一起送入到数组,这一点很重要!!!
  • 默认情况下,从键盘缓冲区取字符串时遇到空格就“停止搬运”是C语言标准中对scanf函数的规定。
6.3.1 使用scanf函数输入数值

scanf函数的原型如下:

1
int scanf(const char* format,...)

参数const char* format表示参数format是一个字符型指针,前面的限定符const表示scanf参数内部不能通过该指针修改内容。const char*一般表示一个字符串常量。

参数format称为“格式控制字符串”,scanf函数将按照格式控制字符串中指定的格式尝试匹配键盘缓冲区中的内容。...表示这里可以是一个或多个参数,如何让函数支持多个参数暂且不论,此处知道scanf函数后可以有多个参数即可,从scanf的第2个参数开始都必须是内存地址,用于明确scanf函数在获得数据后将数据送到何处。

scanf函数的返回值,由于scanf函数有输入失败的可能(例如要求输入整数,而用户却偏偏输入字母a),因此scanf函数会将成功输入的数据项的个数作为返回值返回。

以下是使用scanf函数的简单例子:

1
2
int x = 0;
int count = scanf("%d", &x);

第1行代码声明了一个整形变量x并赋值为0,第2行代码调用scanf函数,括号中是它的参数,可以注意到它的第1个参数是字符串常量"%d",表示此处预期输入一个十进制整型值(d是demical,十进制的意思),第2个参数是&x&运算符的作用是取出变量的地址,scanf函数会把取得的十进制数值送入这个地址。

  • scanf至少需要2个参数,第1个参数format是格式控制字符串,用于说明输入数组的格式,格式控制字符串中最重要的是“转换说明”,是格式控制字符串是最重要的内容,通常由%开始,其后加上特定的字符表示不同的数据类型。

以上是scanf中格式控制符的说明:

序号字符输入的数据类型
1%d输入十进制整数(decimal)
2%s输入字符串(string)
3%c输入单个字符 (character)
4%u输入无符号十进制整数(unsigned)
5%i输入十进制,八进制(0开头),十六进制(0x开头)整数
6%o小写O输入八进制整数(octal)
7%x输入十六进制整数(hexadecimal)
8%e输入浮点数

使用scanf函数输入数值的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// L06_05_SCANF_NUMBER.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main()
{
    int a = 0;
    printf("请输入一个整数:\n");
    int ret = scanf("%d", &a);
    printf("%d\n", ret);

    clearInputBuffer();
    printf("你输入的数值是:%d\n", a);

    float f = 0;
    printf("请输入一个浮点数:\n");
    int ret1 = scanf("%f", &f);
    printf("%d\n", ret1);
    printf("你输入的浮点数是:%f\n", f);

    return 0;
}

不正确输入整数和浮点数时的输出示例:

Snipaste_2026-04-02_20-23-40.png

正确输入整数和浮点数时的输出示例:

Snipaste_2026-04-02_20-27-27.png

可以看到,使用scanf读取整数或浮点数时,正常输入时,scanf返回数字1;未读取到时返回值是0。

使用scanf函数输入字符串的示例。

先看会溢出的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// L06_06_SCANF_STRING.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main()
{
    char str[10];
    printf("请输入字符(最多10个):");
    int num = scanf("%s", str);
    printf("num:%d\n", num);
    if (num != 1) {
        printf("你未输入,请重试\n");
        return 1;
    }
    // 判断用户是否输入超长
    // 方法:看缓冲区里还有没有剩下的字符
    int ch = getchar();
    if (ch != '\n' && ch != EOF) {
        printf("错误:输入超长!\n");
        clearInputBuffer();
        return 2;
    }
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

这个时候运行程序,如果输入abcdefghijkl,此时输入了12个字符,超过了str允许的最大长度限制。

char str[10]; 只能存 9 个有效字符 + 1 个结束符

Snipaste_2026-04-03_22-38-31.png

改进:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// L06_06_SCANF_STRING.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main()
{
    char str[10];
    printf("请输入字符(最多10个):");
    // 使用 %9s 限制用户最多只能输入9个字符
    int num = scanf("%9s", str);
    printf("num:%d\n", num);
    if (num != 1) {
        printf("你未输入,请重试\n");
        return 1;
    }
    // 判断用户是否输入超长
    // 方法:看缓冲区里还有没有剩下的字符
    int ch = getchar();
    if (ch != '\n' && ch != EOF) {
        printf("读取到其他字符:%c\n", ch);
        printf("错误:输入超长!\n");
        clearInputBuffer();
        return 2;
    }
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

优化后,再输入字符abcdefghijkl,此时输出如下:

Snipaste_2026-04-03_22-44-17.png

测试不输入任何字符的情况,直接按CTRL+Z后,然后按回车键,输出如下:

Snipaste_2026-04-03_22-45-32.png

再测试正常输入9个字符的情况:

Snipaste_2026-04-03_22-46-29.png

  • %s 绝对不安全,必须写成 %9s 这种带长度的格式 !!!

如果仅在Windows平台使用,推荐使用更安全的scanf_s函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// L06_06_SCANF_STRING.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main()
{
    char str[10];
    printf("str的长度:%d\n", sizeof(str));
    printf("请输入字符(最多10个):");
    //// 使用 %9s 限制用户最多只能输入9个字符
    //int num = scanf("%9s", str);
    int num = scanf_s("%s", str, sizeof(str));
    printf("num:%d\n", num);
    if (num != 1) {
        printf("你未输入或输入超长,请重试\n");
        return 1;
    }
   
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

此时输入超长字符也不会溢出。 用 scanf_s 会直接拦截超长并返回 0

  • scanf_s函数这样设计体现了一个重要的原则:一个操作要么全部成功,要么全部失败。

输出示例:

Snipaste_2026-04-03_22-59-37.png

如果是需要跨平台执行,则还是推荐使用 %9s 这种带长度的格式

使用scanf函数输入多项数据,请看以下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// L06_07_SCANF_MULTI_VARIABLES.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

int main()
{
    char a;
    char b;
    char c;
    printf("请输入三个字符,中间用英文逗号,分隔开\n");
    scanf("%c,%c,%c", &a, &b, &c);
    printf("你输入的三个字符是:\n%c\n%c\n%c\n", a, b, c);

    return 0;
}

运行程序:

Snipaste_2026-04-04_16-25-12.png

由于我故意在输入a字母前输入了空格,这个时候程序就先读了空格,然后空格后面又没有,逗号,所以后面的字符b和字符c都没有匹配到,打印出了两个问题。

如果严格按字符后面加,逗号输入则可以正常显示:

Snipaste_2026-04-04_16-45-10.png

为了让用户不严格输入,也能正常接收,可以在%c前后都加空格:

  • scanf 里的空格 = 匹配任意空白(空格、换行、Tab)

  • 想让输入不严格、不挑剔,就在 %c, 前后都加空格

优化版本1,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// L06_07_SCANF_MULTI_VARIABLES.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

int main()
{
    char a;
    char b;
    char c;
    printf("请输入三个字符,中间用英文逗号,分隔开\n");
    // scanf("%c,%c,%c", &a, &b, &c);
    // 优化,支持换行、空格、Tab
    scanf(" %c , %c , %c", &a, &b, &c);
    printf("你输入的三个字符是:\n%c\n%c\n%c\n", a, b, c);

    return 0;
}

输出如下:

Snipaste_2026-04-04_16-48-35.png

可以看到, 在输入字符a前面是输入了多个空格,然后a后面有个空格,然后再是,逗号,再输入了一个Tab键,b后面又接了2个空格,再是一个,逗号,然后是输入了回车(也就是一个换行符),最后再输入的c

scanf正常读取到三个字母,然后正常输出了!!!

这个时候虽然输入字符没问题,但是还是存在隐藏bug。如果后面还有一个scanf读取字符,会直接读取键盘缓冲区的回车!!

请看以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// L06_07_SCANF_MULTI_VARIABLES.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

int main()
{
    char a;
    char b;
    char c;
    printf("请输入三个字符,中间用英文逗号,分隔开\n");
    // scanf("%c,%c,%c", &a, &b, &c);
    // 优化,支持换行、空格、Tab
    scanf(" %c , %c , %c", &a, &b, &c);
    printf("你输入的三个字符是:\n%c\n%c\n%c\n", a, b, c);
    char d;
    scanf("%c", &d);
    printf("%c\n", d);

    return 0;
}

运行程序:

Snipaste_2026-04-04_17-05-45.png

在输入a,b,c按回车后,第一个scanfa,b,c字符读取到到,此时键盘缓冲区还存在一个回车!!

%c 不会跳过任何字符,直接读取缓冲区里的第一个东西 → \n

此时,变量 d 读到的不是你新输入的字符而是上一次输入残留的回车符!

还没来得及输入程序就自动输出了回车符(现象就是 输出一个空行。 ),这就复现了这个隐藏bug了。

因此, 每次 scanf 后清空缓冲区 = 最稳健的编程习惯

优化版本2,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// L06_07_SCANF_MULTI_VARIABLES.cpp
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
#include <stdio.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main()
{
    char a;
    char b;
    char c;
    printf("请输入三个字符,中间用英文逗号,分隔开\n");
    // scanf("%c,%c,%c", &a, &b, &c);
    // 优化,支持换行、空格、Tab
    scanf(" %c , %c , %c", &a, &b, &c);
    clearInputBuffer(); // 每次读完都清空缓冲区
    printf("你输入的三个字符是:\n%c\n%c\n%c\n", a, b, c);

    char d;
    printf("再输入一个字符\n");
    scanf("%c", &d);
    clearInputBuffer(); // 每次读完都清空缓冲区
    printf("你输入的字符是:\n%c\n", d);

    return 0;
}

此时再运行程序:

Snipaste_2026-04-04_17-14-07.png

没有出现异常读回车符的现象了!!

通过以上测试,后续任何时候,只要scanf读取用户输入,就马上用clearInputBuffer();清空键盘缓冲区。

  • 让用户在一行中输入多项数据不是一个很好的主意,这会降低用户界面的友好性,用户也容易发生错误!
6.3.2 自定义数据输入函数

scanf函数是用于获取键盘输入的标准库函数,其使用方法也非常多,这一方面给我们带来了很大的灵活性,另一方面也会增加编程的复杂度。同时,在实际编程时要从用户处得到输入,往往还有其他事要处理,包括:

  • 在输入之前给用户提示;
  • 清空键盘缓冲区;
  • 检查用户的输入是否有效。

这些都是scanf不能单独完成的。接下来我们将自行定义几个函数来实现上面的功能。

下面我们来实现三个自定义函数:

  • 输入整数。
  • 输入字符。
  • 输入字符串。
6.3.2.1 输入整数

和以前一样,在开始进行函数设计之前考虑下面的问题。

  1. 函数的功能是什么?

显示提示信息,清空键盘缓冲区并要求用户输入一个整型值;当用户输入了不符合要求的数据时,要求用户重新输入,直到用户输入了符合要求的数据。

  1. 函数的名字是什么?

inputInteger,意思是“输入整数”。

  1. 函数需要传入哪些参数?

既然要求输入之前给出提示,则提示的内容(字符串)应该通过参数传递给函数,这个参数应该是一个指向字符串的指针,且不允许修改字符串的值,参数名可以是prompt,参数类型应为const char*,所以第一个参数是const char* prompt

除此之外,还要限制允许输入的最大值(max)和最小值(min),这两个值需要通过参数传入,类型是整型。

  1. 函数是否需要返回值?返回何种类型的值?

当然需要,返回用户输入的整型值。

确定了这些后,函数的原型就确定了!!!

1
int inputInteger(const char* prompt, int min, int max);

下面我们来实现这个函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
#include <string.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

// 输入整数
int inputInteger(const char* prompt, int min, int max)
{
    if (prompt == NULL) { // 增加空指针判断,避免崩溃
        printf("提示语不能为空\n");
        return -1;
    }

    if (min > max) {
        printf("最小值不能大于最大值\n");
        return -1;
    }

    int inputFlag = 0;
    int inputValue = 0;
    do {
        // 读取用户输入的整数
        printf("%s", prompt);
        // 使用 " %d" 跳过前面所有空白(空格、回车、Tab)
        inputFlag = scanf(" %d", &inputValue);

        if (inputFlag == 1) {
            // 【关键】检查后面是不是还有非法字符(. 或 字母)
            int ch = getchar();
            if (ch != '\n' && ch != EOF) {
                // 还有内容 → 不是纯整数!
                inputFlag = 0; // 强制标记为输入失败
                // 清空缓冲区
                clearInputBuffer();
            }
        }
        else {
            // 清空缓冲区
            clearInputBuffer();
        }

        // 用户输入错误时进行提示,提升用户体验
        if (inputFlag != 1) {
            printf("==> 输入无效,请输入整数!\n");
        }
        else if (inputValue < min || inputValue > max) {
            printf("==> 输入超出范围,请输入 %d ~ %d 之间的整数!\n", min, max);
        }
    } while (inputFlag != 1 || inputValue < min || inputValue > max);
    return inputValue;
}

int main()
{
    int value = inputInteger("请输入一个整数(1-10之间)", 1, 10);
    printf("用户输入的值为:%d\n", value);

    return 0;
}

此时霆代码:

Snipaste_2026-04-10_22-51-17.png

可以看到,程序能正常运行,并且处理常见异常!

现在代码完美支持所有正常输入:

  • ✅ 输入 5 → 正常通过
  • ✅ 输入 5(空格 + 5)→ 正常通过
  • ✅ 输入 10 → 正常通过
  • ❌ 输入 123 → 正确提示:超出范围
  • ❌ 输入 1.23 → 正确提示:输入无效
  • ❌ 输入 abc → 正确提示:输入无效
  • ❌ 输入 12a3 → 正确提示:输入无效
  • ✅ 回车、空格乱输 → 不卡死

所有日常使用场景全部正常!

这个版本已经很完美了,但实际运行可以发现prompt这个参数可以不需要用户输入,由函数自己生成,可以避免用户输入的提示词与实际的数值范围不一致。

6.3.2.2 输入单个字符

与输入整数值不同的是,在输入整型值时可以简单地限定最大值和最小值,而输入字符时我们可能需要限定哪些字符是有效的,哪些字符是无效的!

例如,在咨询用户“是否要继续?”时,只允许用户输入Y、y、N、n中的一个字符,其他任何输入都被视为无效输入,这样的条件如何传入函数呢?比较便捷的方式是,把允许输入的多个字符以字符串的形式传入函数内部,再检查用户输入的字符是否包含在这个字符串中。

和以前一样,在开始进行函数设计之前考虑下面的问题。

  1. 函数的功能是什么?

显示提示信息,清空键盘缓冲区并要求用户输入一个字符;当用户输入了不符合要求的字符时,要求用户重新输入,直到用户输入了符合要求的数据。

  1. 函数的名字是什么?

inputChar,意思是“输入字符”,是输入单个字符,不是字符串。

  1. 函数需要传入哪些参数?

既然要求输入之前给出提示,则提示的内容(字符串)应该通过参数传递给函数,这个参数应该是一个指向字符串的指针,且不允许修改字符串的值,参数名可以是prompt,参数类型应为const char*,所以第一个参数是const char* prompt

除此之外,还要将允许用户输入的字符(多个)以字符串的形式传入,它同样也是一个字符指针,参数名可以是validChars,参数类型应为const char*,所以第一个参数是const char* validChars

  1. 函数是否需要返回值?返回何种类型的值?

当然需要,返回用户输入的字符。

确定了这些后,函数的原型就确定了!!!

1
char inputChar(const char* prompt, const char* validChars);

下面我们来实现这个函数。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
// L06_01_COMMON_TOOLS.cpp
// 通用工具库
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
#include <string.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

// 在字符串中查找特定字符的位置
int indexOfChar(const char* str, char ch)
{
    // 定义存储索引的位置
    int index = 0;

    if (str == NULL) { // 增加空指针判断,避免崩溃
        return -1;
    }

    // 遍历字符串一定要用临时指针,不要动原始指针!
    // 用临时指针遍历,不修改原始 str
    const char* p = str;

    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*p != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *p, p);
        // 判断是否相等
        if (*p == ch)
        {
            // printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 匹配\n", str, index, *p, ch);
            return index;
        }

        // printf("在字符串%s当前索引是 %d,对应字符是:%c ,与目前字符 %c 不匹配\n", str, index, *p, ch);
        // 临时指针变量p的值+1,表示指向下一个元素
        p++;
        index++;
    }
    return -1;
}

// 输入单个字符
char inputChar(const char* prompt, const char* validChars)
{
    if (prompt == NULL || validChars == NULL) { // 增加空指针判断,避免崩溃
        printf("提示语 或 允许输入的字符 不能为空\n");
        return '\0';
    }

    int inputFlag = 0;
    char inputValue = 0;
    do {
        // 读取用户输入字符
        printf("%s", prompt);
        // 使用 " %c" 跳过前面所有空白(空格、回车、Tab)
        inputFlag = scanf(" %c", &inputValue);

        if (inputFlag == 1) {
            // 【关键】检查后面是不是还有非法字符
            int ch = getchar();
            if (ch != '\n' && ch != EOF) {
                // 还有内容 → 不是单字符!
                printf("读取到多余字符:%c\n", ch);
                inputFlag = 0; // 强制标记为输入失败
                // 清空缓冲区
                clearInputBuffer();
            }
        }
        else {
            // 清空缓冲区
            clearInputBuffer();
        }

        // 用户输入错误时进行提示,提升用户体验
        if (inputFlag != 1) {
            printf("==> 输入无效,请输入【%s】中的一个字符!\n", validChars);
        }
        else if (indexOfChar(validChars, inputValue) < 0) {
            printf("==> 输入无效,你输入的是 %c, 请输入【%s】中的一个字符!\n", inputValue, validChars);
            inputFlag = 0; // 强制标记为输入失败
        }
    } while (inputFlag != 1);
    return inputValue;
}

int main()
{
    char value = inputChar("是否要继续?(Y/N): ", "YyNn");
    printf("用户输入的值为:%c\n", value);

    return 0;
}

运行代码:

Snipaste_2026-04-11_18-06-23.png

6.3.2.3 输入字符串

输入字符串函数,提示用户输入字符串,需要注意的是,inputIntegerinputChar函数都将用户的输入作为函数的返回值,而inputString函数有些特殊,用户输入的字符串需要保存在一块内存空间中去。此时有两种选择:

  • inputString函数内部动态分配内存用于存储字符串,输入完成后返回内存地址;
  • 要求inputString函数的调用者传入已分配好的内存空间首地址,将用户的输入传入其中。

一定不要选择第1种方案,内存分配的原则是“谁分配,谁释放”,采用第1种方案时有很大概率调用者会忘记释放这块内存,最终导致内存泄露。

要求inputString函数的调用者事先准备好存储字符串的内存空间(字符串数组或动态分配的内存),并且将首地址作为参数传入,这就像去食堂打饭时自带饭盒一样!!

和以前一样,在开始进行函数设计之前考虑下面的问题。

  1. 函数的功能是什么?

显示提示信息,清空键盘缓冲区并要求用户输入一个字符串;限制用户输入字符串的最小长度和最大长度,当用户输入了不符合要求的字符时,要求用户重新输入,直到用户输入了符合要求的数据。

  1. 函数的名字是什么?

inputString,意思是“输入字符串”。

  1. 函数需要传入哪些参数?

首先需要传入调用者事先准备好的,用于存储字符串的内存区域首地址,它应该是一个字符指针,在函数内部我们将使用这个指针将字符串送过去。参数名可以是destination,参数类型应为char*,所以第一个参数是char* destination

既然要求输入之前给出提示,则提示的内容(字符串)应该通过参数传递给函数,这个参数应该是一个指向字符串的指针,且不允许修改字符串的值,参数名可以是prompt,参数类型应为const char*,所以第二个参数是const char* prompt

除此之外,还要限制允许输入字符串的最大长度(maxLength)和最小长度(minLength),这两个值需要通过参数传入,类型是整型。

  1. 函数是否需要返回值?返回何种类型的值?

当然需要,返回用户传入的内存地址destination

确定了这些后,函数的原型就确定了!!!

1
char inputString(char* destination, const char* prompt, int minLength, int maxLength);

下面我们来实现这个函数。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
// 通用工具库
// 忽略scanf不安全告警
#define _CRT_SECURE_NO_WARNINGS
// 屏蔽scanf返回值被忽略警告
#pragma warning(disable: 6031)
//
#include <stdio.h>
#include <string.h>

// 正确的清空缓冲区函数
void clearInputBuffer()
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

// 计算字符串长度
unsigned int strLength(const char* str)
{
    // 定义存储字符串长度的变量
    unsigned int length = 0;

    if (str == NULL) { // 增加空指针判断,避免崩溃
        return 0;
    }
    // 判断指针str指向的值是否为字符串结束符'\0'
    while (*str != '\0')
    {
        // 打印当前字符和内存地址
        // printf("当前字符%c的地址为:%p\n", *str, str);
        // 长度值增1
        length++;
        // 指针变量str的值+1,表示指向下一个元素
        str++;
        // 最后打印出计算出的总长度
        // printf("length:%u\n", length);
    }
    return length;
}

// 输入字符串
char* inputString(char* destination, const char* prompt, int minLength, int maxLength)
{
    // 定义允许的最小长度和最大长度,调用者定义的最小最大长度不能超过这个区间
    const int MIN = 1;
    const int MAX = 1024;
    if (prompt == NULL || destination == NULL) { // 增加空指针判断,避免崩溃
        printf("提示语 或 目标字符串首地址 不能为空\n");
        return NULL;
    }

    if (minLength < MIN) {
        printf("最小长度值不能小于 %d \n", MIN);
        return NULL;
    }

    if (maxLength > MAX) {
        printf("最大长度值不能大于 %d \n", MAX);
        return NULL;
    }

    if (minLength > maxLength) {
        printf("最小长度不能大于最大长度\n");
        return NULL;
    }

    // 输入标记,0为输入异常,1为输入正常
    int inputFlag = 0;

    do {
        // 读取用户输入字符
        printf("%s", prompt);
        // 使用 " %1024[^\n]" 跳过前面所有空白(空格、回车、Tab),并且宽度最多1024个字符
        // %1024s 表示读取不包含空格的字符
        // [^\n] 意思:除了换行符,所有字符都读
        inputFlag = scanf(" %1024[^\n]", destination);
        if (inputFlag != 1) {
            // 读取失败清空缓冲区
            clearInputBuffer();
        }

        int inputLen = strLength(destination);
        if (inputLen < minLength || inputLen > maxLength) {
            inputFlag = 0; // 强制标记为输入失败
            printf("您输入的字符串长度不满足要求 [%d, %d],当前长度: %d\n", minLength, maxLength, inputLen);
        }
    } while (inputFlag != 1);
    return destination;
}

int main()
{
    char dest[50];
    char* returnValue = inputString(dest, "请输入长度在[10, 30]区间的字符串: ", 10, 30);
    if (returnValue != NULL) {
        printf("用户输入的值为:%s\n", dest);
    }

    return 0;
}

运行输出示例:

Snipaste_2026-04-11_22-36-38.png

豆包对我的代码进行了检查:

  1. 安全
  • %1024[^\n] 限制最大长度,绝对不会溢出
  • 空指针判断完整,不会崩溃
  • 长度范围校验严格,拒绝非法参数
  1. 健壮
  • 读取失败自动清空缓冲区
  • 支持带空格输入
  • 长度不满足自动重新输入
  • 任何异常都不会卡死
  1. 规范
  • MIN/MAX 使用 const 定义,非常标准
  • 注释清晰
  • 无多余调试代码
  • 风格统一、干净
  1. 内存安全
  • 栈数组 dest[50] 自动释放
  • 无动态内存,无泄漏
  • 指针安全,无野指针
  1. 使用体验
  • 提示友好
  • 错误提示明确
  • 输入流畅
  • 支持空格、长度区间控制