×
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