【C绿竹拔节(二)】 C语言函数划分解说

建议:开发环境: 非([IDE: Dev Cpp][std: -std=c11])、([IDE: Clion]、[C Library: C11])、Linux
摘要:
   在C语言中,函数可以根据其用途和行为进行多种分类。这些分类包括回调函数、异步函数、同步函数、阻塞函数、非阻塞函数、静态函数、内联函数、递归函数、变参函数、纯函数和高阶函数。每种类型的函数在不同的编程场景中都有其独特的用途和优势。回调函数常用于事件驱动和异步编程,异步函数用于处理I/O操作和网络请求,同步函数会阻塞调用线程直到操作完成,阻塞函数会暂停调用线程直到某个条件满足,非阻塞函数用于提高程序的并发性和响应性。静态函数用于实现模块内部的私有功能,内联函数减少函数调用开销,递归函数用于解决可以分解为更小子问题的问题,变参函数可以接受可变数量参数,纯函数没有副作用,高阶函数可以接受函数作为参数或返回函数。了解这些函数类型及其应用场景,有助于编写更高效、更可维护的C语言代码。
提供:
1. Clion(软件: ‌CLion是一款由JetBrains公司开发的跨平台集成开发环境(IDE),专门用于C和C++语言的开发‌。)【付费】「AL」〖✿〗


1️⃣ C语言中的函数

小蛙: 啊,C语言中的函数有什么好讲的呀,我都用过好多不同类型的函数了,先生这有什么好讲的呢。

先知: 小友先别急,让为夫细细道来,先讲一下模块化程序设计。


1️⃣⏺️1️⃣模块化程序设计

mome: 1969年,Wirth提出采用“自顶向下逐步求精、分而治之”的原则进行大型程序的设计,运用科学抽象的方法,将系统按功能进行分解,一步一步按层次把大功能分解为小功能,从上到下,每一层不断将功能细化,到最后一层都是功能单一、简单易实现的模块。这就是我们所说的“模块化程序设计”思想,模块化程序设计不仅使程序更容易理解,也更容易调试和维护。

描述: 将程序分割成模块的一系列好处:
1.抽象性: 抽象让一个团队的多个程序员共同开发一个程序更容易,团队成员可以在更大的程度上相互独立地工作,我们知道模块会做什么,单数不需要知道这些功能的实现细节,可以不必为了修改部分程序而了解整个程序是如何工作的。
2.可复用性: 任何一个提供服务的模块都有可能在其他程序中复用。
3.可维护性: 将程序模块化以后,程序中的错误通常只影响一个模块实现,因而更容易找到并修正错误。
4.高内聚性: 模块中的元素彼此紧密相关。高内聚可以使模块易于使用,程序更容易理解。
5.低耦合性: 模块之间应该尽可能地相对独立。低耦合可以使程序便于修改,并方便模块使用。

小蛙: 听起来模块化编程还真可以借鉴一二,这和函数有什么联系呀,难不成是。。。模块对应相应的函数,额。。。

小夏: 函数是C语言中模块化程序设计的核心概念,既可以把每个函数看做一个模块,也可以将若干个相关的函数合并成一个模块。模块的接口就是头文件,头文件中包含了可以被其他文件调用的函数的原型,源文件包含该模块中的函数的定义。C语言中的函数库就是一些模块的集合,库中的每个头文件都是一个模块的接口,例如在每个程序中都用到的是包含输入/输出函数的模块的接口。

mome: 从使用者的角度来看,C语言中的函数分为两类,一类是有系统提供的标准库函数,如标准输入输出函数(scanf、printf、getchar()、putchar()等)、数学计算函数(sin、cos、fabs、sqrt等)、数据格式转换函数(stoi、stof、sscanf、sprintf等)、字符串处理函数(strlen、strcpy、strcmp等)和文件存取函数(fread、ferite、fopen等),这类函数无需用户自己定义,也不必在程序中进行类型说明,只需要在程序开头包含该函数原型声明所在的头文件,直接调用即可。另一类是用户在自己得程序中根据需要编写的函数。C程序的编写过程就是将用户编写的函数与C标准库函数组合在一起使用。
    一个C程序可以由一个或多个源程序组成,一个源程序又可以由一个或多个函数组成。函数通过函数的调用语句而被执行。函数调用语句需要指定被调用的函数名,提供被掉函数执行任务是所需要的信息。至于被调用函数是如何执行任务的,调用函数并不需要知道,也无需知道。这样就通过函数实现了信息隐藏,这些执行细节隐藏得越好,程序的软件工程质量就越高。

⚠️注: 由于篇幅有限,本文主要介绍函数分类,然程序设计原则和软件工程不做过多探讨,有兴趣的道友可参考相关文献。


1️⃣⏺️2️⃣函数的基本概念

大灰: 在C语言中,函数是一段完成特定功能的代码,可以在程序中多次调用。函数的定义包括函数名、返回类型、参数列表和函数体。
mome: 函数是一个完成特定工作的独立程序模块,每个函数本质上是一个自带声明和语句的小程序。程序中一旦调用了某个函数,该函数就会完成一些特定的工作,然后返回到调用它的地方。

函数的一般形式:

返回值类型 函数名([形式参数列表])	/* 函数头 */
{								/* 函数体 */
    // [声明变量]
    // [函数实现过程]
    return [表达式];
}

【示例1.2-1】计算一个正数的绝对值

// Method 1
int abs01(int num) {
	return num < 0? -num: num;
}
// Method 2
int abs02(int num){
    /* Integers in computers are usually stored in binary form, and the storage method is usually a complement */
    /* In the computer, whether addition or subtraction, are directly used in the form of complement. */
    int mask = num >> 31;   // 4Bytes * 8bits - 1bit
    return (num + mask) ^ mask;
}

【演示1.2-1】计算一个正数的绝对值

小蛙: 这看起来,使用DevCpp比CLion测试模块方便多了。
大灰: 小蛙你这样看着确实没错,但是你没封装对应的模块呀,而且CLion写模块测试也确实方便呀,是你不想使用gcc命令工具吧,且看下图,你就知道CLion的强大之处啦。

【演示1.2-2】计算一个正数的绝对值,使用Clion的高效之处。

小蛙: 喔X,这都可以,看来我真的是落伍啦,放着Dev Cpp不放手还真以为没其他好的IDE呢。

⚠️注: 由于篇幅有限,本文主要介绍函数分类,然函数的详细定义和运行机制不做过多讲解,有兴趣的道友可参考相关文献。



2️⃣ C语言中的函数分类

小蛙: 啊,C语言中的函数有什么好讲的呀,我都用过好多不同类型的函数了,先生这有什么好讲的呢。

先知: 小友先别急,让为夫细细道来,先讲一下模块化程序设计。


2️⃣⏺️1️⃣函数一般分类方式

小夏: C语言中的函数可以根据多种方式进行划分,以下是一些常见的划分方式及其解释说明:

1.按定义来源划分:
  • 库函数: 由C语言标准库提供,用户无需定义,只需包含相应的头文件即可直接调用。例如 printf、scanf、strcpy 等。这些函数经过优化和测试,功能丰富,如IO函数、字符串操作函数、数学函数等。
  • 自定义函数:由程序员根据具体需求自行定义和实现的函数。用于完成特定的功能,提高代码的复用性和可维护性。
  • 系统调用:由操作系统实现的函数,用于执行底层的系统操作,如文件操作、进程控制等。在C语言中,系统调用通常通过库函数来封装和调用。
2.按函数的作用范围划分:
  • 全局函数:在程序的任何地方都可以调用的函数。通常用于实现通用的功能,如输入输出、数学计算等。
  • 静态函数:使用 static 关键字声明,仅在声明它的源文件中可见。有助于封装和模块化代码,减少命名冲突。
3.按函数的调用方式划分:
  • 递归函数:函数在执行过程中调用自身。递归函数通常用于解决可以分解为相同子问题的问题,如计算阶乘、斐波那契数列等。递归函数需要有明确的终止条件,以避免无限递归。
  • 内联函数:使用 inline 关键字声明,建议编译器在调用点直接插入函数体,以减少函数调用的开销。内联函数适用于小的、频繁调用的函数,但编译器有权决定是否真正内联。
4.按函数的特殊用途划分:
  • 变量参数函数: 可以接受可变数量的参数。例如 printf 函数,它可以根据格式字符串接受不同数量的参数。变量参数函数通常使用 头文件中的宏来处理可变参数。
  • 回调函数: 作为参数传递给其他函数,并在适当的时候被调用。回调函数在事件处理、异步编程等场景中非常有用。
5.按函数的实现方式划分:
  • 普通函数: 常规的函数实现,没有特殊的修饰或调用方式。
  • 中断服务函数: 用于处理中断的函数,通常在嵌入式系统中使用。中断服务函数需要满足特定的语法和调用约定,以确保在中断发生时能够正确处理。
6.按函数的存储位置划分:
  • 静态存储函数: 函数的代码存储在静态存储区,函数的生命周期与程序的生命周期相同。
  • 动态存储函数: 函数的代码存储在动态存储区,通常用于动态加载和卸载的模块。在C语言中,动态存储函数通常通过动态链接库(DLL)实现。
7.按函数的返回类型划分:
  • 返回基本类型函数: 返回值为基本数据类型,如 int、float、char 等。例如 int add(int a, int b) 返回两个整数的和。
  • 返回指针函数:返回值为指针类型,用于返回动态分配的内存地址或指向特定数据结构的指针。例如char* strdup(const char* s);返回一个字符串的副本。
  • 返回结构体或联合体函数:返回值为结构体或联合体类型,用于返回复杂的数据结构。例如struct Point getPoint(int x, int y);返回一个点的坐标。
8.按函数的参数类型划分:
  • 参数为基本类型函数: 参数为基本数据类型,如 int、float、char 等。例如 void printInt(int n) 打印一个整数。
  • 参数为指针函数: 参数为指针类型,用于传递地址,实现对变量的修改或传递大块数据。例如 void swap(int* a, int* b) 交换两个整数的值。
  • 参数为数组函数: 参数为数组类型,通常通过指针传递数组的首地址。例如 void printArray(int arr[], int size) 打印一个整数数组。
  • 参数为结构体或联合体函数:参数为结构体或联合体类型,用于传递复杂的数据结构。例如 void printPoint(struct Point p) 打印一个点的坐标。

2️⃣⏺️2️⃣函数一般性划分

小蛙: 咿呀,这不是凑字数吗?还不同的分类和划分。
大灰: 小蛙,你别老是插嘴,等先生讲了在咕噜。

小夏: 以下是C语言中函数按含义和用途进行划分:

函数划分01 标准 含义 用途
按函数的调用约定划分 标准调用约定函数 使用标准的调用约定,如 cdecl。这是默认的调用约定,调用者负责清理栈。
快速调用约定函数 使用 fastcall 调用约定,参数通过寄存器传递,减少栈操作,提高调用效率。
按函数的作用域划分 静态函数 仅在定义它们的源文件内可见的函数,用于实现模块内部的私有功能。比如在一个源文件中定义的用于内部数据处理的辅助函数,通过静态修饰后,其他源文件无法直接调用,保证了模块内部的封装性。
非静态函数 在多个源文件中可见的函数,用于实现模块间共享的功能。例如在多个源文件中都需要使用的数学计算函数,定义为非静态函数后,可以在各个源文件中通过外部声明后进行调用。
按函数的内存管理划分 动态内存分配函数 用于动态分配和释放内存的函数,如 malloc 用于分配内存,free 用于释放内存。
静态内存分配函数 使用静态存储区分配内存的函数,通常用于全局变量和静态变量。
函数划分02 标准 含义 用途
按函数的调用上下文划分 主函数 程序的入口点,每个C程序必须有一个 main 函数。
辅助函数 用于辅助主函数或其他函数完成特定任务的函数。例如 swap 用于交换两个变量的值。
按函数的纯度划分 纯函数 没有副作用,返回值仅依赖于输入参数。例如 int add(int a, int b)。
非纯函数 有副作用,返回值可能依赖于外部状态或修改外部状态。例如 printf 会修改标准输出。
按函数的按调用方式划分 回调函数 通过函数指针传递的函数,常用于事件驱动和异步编程。例如在图形界面编程中,为按钮点击事件设置的回调函数,当按钮被点击时,会调用该函数来处理相应的事件。
按编译优化方式划分 内联函数 在编译时被插入到每个调用点的函数,减少函数调用开销。
函数划分03 标准 含义 用途
按函数的线程局部存储划分 线程局部存储函数 使用 __thread 关键字声明,每个线程有自己的独立存储空间。例如 __thread int thread_local_var。
非线程局部存储函数 没有使用 __thread 关键字,所有线程共享同一存储空间。例如全局变量 int global_var。
按函数的线程安全性划分 线程安全函数 在多线程环境中可以安全调用的函数,不会导致数据竞争或资源冲突。例如 strncpy 是线程安全的字符串复制函数。
非线程安全函数 在多线程环境中调用可能导致数据竞争或资源冲突的函数。例如 strcat 在多线程环境中可能不安全。
按函数的可重入性划分 可重入函数 可以被多个线程或中断安全调用的函数,不会导致数据竞争或资源冲突。例如 strncpy 是可重入的字符串复制函数。
非可重入函数 在多线程或中断环境中调用可能导致数据竞争或资源冲突的函数。例如 strtok 是非可重入的字符串分割函数。
按函数的执行特定划分 异步函数 不会阻塞调用线程的函数,常用于处理I/O操作和网络请求。比如在进行网络数据接收时,使用异步函数可以在数据未准备好时,调用线程不会被阻塞,从而可以继续执行其他任务,待数据准备好后再处理。
同步函数 会阻塞调用线程直到操作完成的函数,大多数标准库函数都是同步的。像标准库中的文件读写函数fread、fwrite等,在读写操作完成前,调用它们的线程会被阻塞。
按函数的执行特定划分 阻塞函数 会暂停调用线程直到某个条件满足的函数,如accept。在网络编程中,accept函数用于接受客户端的连接请求,当没有客户端连接时,调用它的服务器线程会阻塞,直到有客户端连接请求到来。
非阻塞函数 不会暂停调用线程的函数,常用于提高程序的并发性和响应性。例如在使用非阻塞的socket编程时,发送和接收数据的函数不会阻塞线程,即使数据未准备好,线程也可以继续执行其他操作,从而提高程序的并发性能。
函数划分04 标准 含义 用途
按函数调用自身情况划分 递归函数 直接或间接调用自身的函数,用于解决可以分解为更小子问题的问题。
按参数数量特性划分 变参函数 可以接受可变数量参数的函数,如printf、scanf。
按函数作为参数划分 高阶函数 可以接受函数作为参数或返回函数的函数,常用于函数式编程。例如在函数式编程中,一个高阶函数可以接受一个处理数据的函数作为参数,然后对一组数据进行批量处理,或者返回一个新的函数用于后续的数据处理操作。

先知: 下面将介绍在全局和局部作用域、静态和动态内存分配、单线程或多线程、同步或异步、阻塞和非阻塞等等环境下,C函数是如何设计的。



3️⃣ C语言中的函数划分讲解01

3️⃣⏺️1️⃣按函数的调用约定划分

1. 标准调用约定函数(Standard Calling Convention Function)

英文全称及简写: C Declaration(简写为__cdecl)

定义: 这是C语言默认的调用约定。在__cdecl调用约定中,函数参数从右向左依次入栈,调用者负责清理栈空间。如果函数有返回值,返回值通常存放在EAX寄存器中。
特点:
优: 这种调用约定的优点是可以支持变长参数的函数,如printf函数。因为调用者知道参数的数量和类型,所以由调用者负责清理栈。
缺: 不过,这也导致使用__cdecl方式编译的程序比使用其他方式编译的程序要大,因为它需要在每次函数调用后插入清理栈的代码。

【示例3.1-1】求两个数的最大公因数

#include

/* Recursion of the division method */
int gcd01(int x, int y){
    if(x < 0 || y < 0)  return -1;
    return y == 0? x:gcd01(y, x % y);
}

/* Iteration of the iterative division method */
int __cdecl gcd02(int x, int y){		// The default is __cdecl, recommend not to write
    if(x < 0 || y < 0)  return -1;
    int t = y;
    while (y != 0){
        t = x % y;
        x = y;
        y = t;
    }
    return x;
}

int main(){
	int num1 = 15, num2 = 6;
    printf("gcd01(%d,%d) = %d\n", num1, num2, gcd01(num1, num2));
    printf("gcd02(%d,%d) = %d\n", num1, num2, gcd02(num1, num2));
	return 0;
}

解说: 在这个示例中,gcd01和gcd02函数的功能一致,只是实现方式不一样可能影响性能,二者探讨其一即可,调用 gcd01 函数时,先将参数 6 压入栈,再将参数 15 压入栈。调用完 gcd01 函数后,由 main 函数负责清理栈。

适用场景:
1.支持变长参数的函数: 如printf、scanf等标准库函数。这些函数需要支持不定数量的参数,因此必须使用__cdecl调用约定。
2.普通函数调用: 由于其灵活性,也常用于普通的函数调用,尤其是在参数数量固定且不多的情况下。
3.动态链接库(DLL): 在Windows平台上,__cdecl调用约定常用于动态链接库(DLL)的导出函数,以确保与C语言编写的调用者兼容。
比较: 使用和不使用__cdecl约束二者的联系和区别
同:
异:

2. 快速调用约定函数

英文全称及简写: Fast Call(简写为__fastcall)

定义: __fastcall调用约定尝试将参数放在寄存器中,而不是堆栈中,从而使函数调用更快。通常,前两个参数(通常是整数或指针)通过寄存器(如ECX和EDX)传递,其余参数从右向左依次入栈。被调用者负责清理栈。
特点:
优: 这种调用约定的优点是可以减少栈操作,提高函数调用的效率,特别适用于参数较少且调用频繁的函数。
缺: 不过,由于寄存器数量有限,对于参数较多的函数,其优势会逐渐减弱。此外,__fastcall调用约定在不同的编译器和平台上可能有不同的实现细节。

【示例3.1-2】两整数求和

#include

int __fastcall add(int x, int y){
    return x + y;
}

int main(){
    int num1 = -10, num2 = 10;
    printf("%d + %d = %d", num1, num2, add(num1, num2));
    return 0;
}

解说: 在这个示例中,参数 x 和 y 可能会通过寄存器传递,具体取决于编译器的实现。调用完add函数后,由add函数自身负责清理栈空间。

适用场景:
1.高性能计算: 适用于对性能要求较高的场景,尤其是那些需要频繁调用小函数的情况,如数学运算库中的基本运算函数、图形渲染中的顶点处理函数等。
2.嵌入式系统:在嵌入式系统中,资源有限,减少栈操作可以提高系统的整体性能。
3.实时系统: 在实时系统中,快速的函数调用可以减少延迟,提高系统的响应速度。
比较: 使用和不使用__cdecl约束二者的联系和区别
同: 1.ffd
异:

3. 标准子程序调用约定(STDCALL)

英文全称及简写: Standard Call(简写为__stdcall)

定义:__stdcall调用约定中,参数从右向左依次入栈,被调用者负责清理栈空间。返回值通常存放在EAX寄存器中。
特点:
优: 这种调用约定的优点是可以减少调用者的代码量,因为被调用者负责清理栈。
缺: 不过,它不支持变长参数的函数。__stdcall调用约定常用于Windows API函数。

【示例3.1-2】两整数求和

#include 
#include 

int __stdcall add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(1, 2);
    printf("Result: %d\n", result);
    return 0;
}

解说: 在这个示例中,调用add函数时,先将参数2入栈,再将参数1入栈。调用完add函数后,由add函数自身负责清理栈空间。

适用场景:
1.Windows API: 大多数Windows API函数使用__stdcall调用约定,因此在调用这些函数时,必须使用相同的调用约定。
2.COM组件: COM组件中的方法调用也通常使用__stdcall调用约定。
比较: 使用和不使用__cdecl约束二者的联系和区别
同:
异:

4. 成员函数调用约定(THISCALL)

英文全称及简写: This Call(简写为__thiscall)

定义:__thiscall调用约定主要用于C++类的成员函数。通常,this指针通过寄存器(如ECX)传递,其他参数从右向左依次入栈。被调用者负责清理栈空间。
特点:
优: 这种调用约定的优点是可以高效地传递this指针,减少栈操作。
缺: 不过,__thiscall调用约定在不同的编译器和平台上可能有不同的实现细节。

【示例3.1-4】类中两整数求和

#include 

class MyClass {
public:
    int __thiscall add(int a, int b) {
        return a + b;
    }
};

int main() {
    MyClass obj;
    int result = obj.add(1, 2);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

解说: 在这个示例中,调用add方法时,this指针通过寄存器传递,参数2和1依次入栈。调用完add方法后,由add方法自身负责清理栈空间。

适用场景:
1.C++类的成员函数: __thiscall调用约定主要用于C++类的成员函数,确保this指针的高效传递。
2.COM组件中的方法调用: 在COM组件中,成员方法调用也通常使用__thiscall调用约定。
比较: 使用和不使用__cdecl约束二者的联系和区别
同:
异:
✨总结:
1. __cdecl: 适用于变长参数的函数和普通函数调用,支持动态链接库(DLL)。
2. __fastcall: 适用于高性能计算、嵌入式系统和实时系统,减少栈操作,提高调用效率。
3. __stdcall: 适用于Windows API和COM组件,减少调用者的代码量。
4. __thiscall: 适用于C++类的成员函数和COM组件中的方法调用,高效传递this指针。

大灰: 选择合适的调用约定可以提高程序的性能和兼容性。在实际开发中,根据具体的应用场景和需求选择最合适的调用约定。

3️⃣⏺️2️⃣按函数的作用域划分

1. 静态函数(Static Function)

英文全称及简写: Static Function(简写为static)

定义: 静态函数是指在定义时使用static关键字修饰的函数。静态函数仅在定义它们的源文件内可见,即它们的作用域被限制在定义它们的文件内部。
特点:
优: ① 静态函数的主要作用是实现模块内部的私有功能。通过将函数定义为静态,可以防止其他源文件直接调用该函数,从而保证模块内部的封装性和代码的组织性。
② 这有助于减少命名冲突,并且可以隐藏模块内部的实现细节。
缺:

【示例3.2-1】 静态函数作用范围
Project Tree:

workplace
	|--- static
			|--- file01.c
			|--- file02.c

FileName: file01.c

#include 

static void helper_function() {
    printf("This is a helper function.\n");
}

void public_function() {
    helper_function();
    printf("This is a public function.\n");
}

FileName: file02.c

#include 

void public_function();  		// 外部声明
static void helper_function();

int main() {
    public_function();  	// 可以调用 public_function
    // helper_function();	// 错误:helper_function 在 file2.c 中不可见
    return 0;
}

编译和运行步骤
(1)使用GCC编译器

gcc file1.c file2.c -o static_example.exe

(2)运行静态函数示例

.\static_example.exe

输出:

This is a helper function.
This is a public function.

解说: 在这个示例中,helper_function是一个静态函数,仅在file1.c中可见。public_function是一个非静态函数,可以在其他源文件中通过外部声明后调用。尝试在file2.c中直接调用helper_function会导致编译错误。

适用场景:
1.模块内部的辅助函数: 用于实现模块内部的私有功能,如数据处理、辅助计算等。
2.减少命名冲突: 用于实现模块内部的私有功能,如数据处理、辅助计算等。
3.封装实现细节: 隐藏模块内部的实现细节,只暴露必要的公共接口,提高代码的可维护性和可读性。

2. 非静态函数(Non-Static Function)

英文全称及简写: Non-Static Function(通常不使用特定简写)

定义: 非静态函数是指没有使用static关键字修饰的函数。非静态函数在多个源文件中可见,即它们的作用域是全局的。
特点:
优: ① 非静态函数的主要作用是实现模块间共享的功能。通过将函数定义为非静态,可以在多个源文件中通过外部声明后调用该函数。
② 这有助于实现代码的复用和模块间的协作。
缺:

【示例3.2-1】 非静态函数作用范围

Project Tree:

workplace
	|--- non-static
			|--- math_utils.c
			|--- main.c

FileName: math_utils.c

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

FileName: main.c

#include 

int add(int a, int b);  // 外部声明
int subtract(int a, int b);  // 外部声明

int main() {
    int sum = add(5, 3);
    int difference = subtract(5, 3);
    printf("Sum: %d, Difference: %d\n", sum, difference);
    return 0;
}

编译和运行步骤
(1)使用GCC编译器

gcc math_utils.c main.c -o non_static_example.exe

(2)运行静态函数示例

.\non_static_example.exe

输出:

Sum: 8, Difference: 2

解说: 在这个示例中,add和subtract是非静态函数,可以在main.c中通过外部声明后调用。这些函数在多个源文件中共享,实现了代码的复用。

适用场景:
1.公共库函数: 如数学计算函数、字符串处理函数等,这些函数在多个源文件中都需要使用。
2.模块间协作: 在大型项目中,多个模块需要共享一些功能,将这些功能定义为非静态函数可以方便模块间的协作。
3.工具函数: 提供一些通用的工具函数,如日志记录、错误处理等,这些函数在多个源文件中都需要调用。
比较: 静态函数和非静态函数二者的联系和区别
同:
异:
✨总结:
1. 静态函数: 适用于模块内部的私有功能,减少命名冲突,封装实现细节。。
2. 非静态函数: 适用于模块间共享的功能,实现代码复用和模块间的协作。

大灰: 选择合适的函数作用域可以提高代码的组织性、可维护性和可读性。在实际开发中,根据具体的应用场景和需求选择最合适的函数作用域。


3️⃣⏺️3️⃣按函数的内存管理划分

1. 动态内存分配函数(Dynamic Memory Allocation Functions)

英文全称及简写: Dynamic Memory Allocation Functions(简写为DMAF)

定义: 动态内存分配函数用于在程序运行时动态地分配和释放内存。这些函数通常从堆(Heap)中分配内存,分配的内存大小在运行时确定。常见的动态内存分配函数包括malloc、calloc、realloc和free。
具体含义:
1. calloc: 分配指定大小的内存块,返回指向该内存块的指针。分配的内存初始化为未定义值。
2. malloc: 分配指定数量的元素,每个元素大小为指定值,并将分配的内存初始化为0。
3. realloc: 重新分配已分配的内存块,可以增大或缩小内存块的大小。
4. free: 释放之前分配的内存块,使其可以被其他部分的程序重新使用。
介绍: 中用到的动态分配函数声明部分
#define _CRT_ALLOCATION_DEFINED
  void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
  void __cdecl free(void *_Memory);
  void *__cdecl malloc(size_t _Size);
  void *__cdecl realloc(void *_Memory,size_t _NewSize);
  _CRTIMP void *__cdecl _recalloc(void *_Memory,size_t _Count,size_t _Size);

【示例3.3-1】动态分配函数使用案例·

#include 
#include 

int main() {
    int *array = (int *)malloc(5 * sizeof(int));	/* 使用 malloc 分配内存 */
    if (array == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }
    
    for (int i = 0; i < 5; i++)	/* 初始化分配的内存 */
        array[i] = i * i;
    

    for (int i = 0; i < 5; i++)	/* 打印分配的内存内容 */
        printf("%d ", array[i]);

    printf("\n");

    /* 使用 realloc 重新分配内存 */
    int *new_array = (int *)realloc(array, 10 * sizeof(int));
    if (new_array == NULL) {
        free(array);  /* 释放原内存 */
        fprintf(stderr, "Memory reallocation failed\n");
        return 1;
    }
    array = new_array;

    // 初始化新分配的内存
    for (int i = 5; i < 10; i++)
        array[i] = i * i;

    // 打印新分配的内存内容
    for (int i = 0; i < 10; i++)
        printf("%d ", array[i]);
    
    printf("\n");

    // 释放分配的内存
    free(array);
    array = NULL;	// 避免野指针

    return 0;
}

输出:

0 1 4 9 16
0 1 4 9 16 25 36 49 64 81

解说: 在这个示例中,首先使用malloc分配了5个整数的内存,并初始化这些内存。然后使用realloc将内存重新分配为10个整数的大小,并初始化新分配的内存。最后,使用free释放分配的内存。

适用场景:
1.数据结构: 如链表、树、图等动态数据结构,需要在运行时动态分配和释放内存。
2.动态数组: 当数组的大小在编译时无法确定,需要在运行时动态分配内存。
3.内存池: 在高性能应用中,可以使用动态内存分配函数实现内存池,提高内存分配和释放的效率。
4.资源管理: 在资源管理器中,动态分配和释放内存可以灵活地管理资源。
5.处理不确定大小的数据: 当处理的数据大小在程序运行时才能确定时,比如从文件中读取数据,但不知道文件内容的大小,就可以使用malloc动态分配足够大的内存来存储这些数据。
6.多线程环境下的内存管理: 在多线程程序中,每个线程可能需要独立的内存空间来存储自己的数据。使用malloc可以在堆上为每个线程动态分配内存,避免线程之间对内存的冲突。

⚠️注: 当释放内存后,这块内存就可以被系统回收,供其他程序或后续的内存分配请求使用。需要注意的是,释放内存后,指针本身仍然存在,但指向的内存已经不再属于程序,为了避免野指针(指向已经释放的内存的指针)问题,一般会将指针设置为NULL。

2. 静态内存分配函数(Static Memory Allocation Functions)

英文全称及简写: Static Memory Allocation Functions(简写为SMAF)

定义: 静态内存分配函数使用静态存储区分配内存,通常用于全局变量和静态变量。这些变量在程序的整个运行期间都存在,内存分配在编译时确定,且在程序启动时自动分配,程序结束时自动释放。
具体含义:
1. 全局变量: 在所有函数外部定义的变量,作用域为整个源文件或多个源文件(通过外部声明)。
2. 静态变量: 在函数内部定义的静态变量,作用域为定义它的函数,但生命周期为整个程序运行期间。

【示例3.3-2】静态内存分配函数使用案例·

#include 

// 全局变量
int global_var = 10;

// 静态变量
static int static_var = 20;

void print_vars() {
    int local_var = 30;  // 函数内部的局部变量
    static int static_local_var = 40;  // 函数内部的静态变量
    printf("Global Var: %d\n", global_var++);
    printf("Static Var: %d\n", static_var++);
    printf("Local Var: %d\n", local_var++);
    printf("Static Local Var: %d\n", static_local_var++);
}

int main() {
    print_vars();
    print_vars();  // 静态局部变量的值会保留
    return 0;
}

输出:

Global Var: 10
Static Var: 20
Local Var: 30
Static Local Var: 40
Global Var: 11
Static Var: 21
Local Var: 30
Static Local Var: 41

解说: 在这个示例中,global_var是一个全局变量,static_var是一个静态变量,local_var是一个函数内部的局部变量,static_local_var是一个函数内部的静态变量。全局变量和静态变量在程序启动时自动分配内存,程序结束时自动释放内存。函数内部的静态变量在第一次调用函数时初始化,后续调用时保留其值。

适用场景:
1.全局配置: 全局变量可以用于存储程序的全局配置信息,如日志级别、配置文件路径等。
2.模块内部状态: 静态变量可以用于存储模块内部的状态信息,如计数器、缓存等。
3.单例模式: 在实现单例模式时,可以使用静态变量来确保只有一个实例存在。
4.函数内部状态: 函数内部的静态变量可以用于保留函数调用之间的状态,如递归调用中的计数器。
5.资源管理: 在管理有限资源(如文件句柄、网络连接等)时,静态变量可以用来记录资源的使用状态。例如在一个文件操作函数中,使用静态变量来记录当前打开的文件数量,当文件数量达到上限时,可以拒绝后续的文件打开请求,防止资源耗尽。
比较: 动态内存分配函数和静态内存分配函数二者的联系和区别
同:
异:
✨总结:
1. 动态内存分配函数: 适用于需要在运行时动态分配和释放内存的场景,如动态数据结构、动态数组、内存池和资源管理。
2. 静态内存分配函数: 适用于需要在程序整个运行期间保持内存分配的场景,如全局配置、模块内部状态、单例模式和函数内部状态。

大灰: 选择合适的内存管理方式可以提高程序的灵活性和效率。在实际开发中,根据具体的应用场景和需求选择最合适的内存管理方式。



5️⃣ C语言中的函数划分讲解03

5️⃣⏺️1️⃣按函数的执行特性划分

1. 阻塞函数(Blocking Functions)

英文全称及简写: Blocking Functions(简写为BF)

定义: 阻塞函数是指在调用过程中会暂停执行直到某一条件成立或事件发生的一类函数。这些函数在多线程编程尤其是在需要并发控制的场合中扮演着重要角色。
具体含义:
1. 暂停执行: 当函数被调用时,它会暂停执行直到满足特定的条件。
2. 线程同步: 阻塞函数常用于线程同步,确保多个线程按预定顺序执行。
3. 性能影响: 频繁使用阻塞函数可能会降低程序的性能,因为它们会导致线程在等待条件满足期间浪费CPU资源。

【示例5.1-1】阻塞函数案例

#include 
#include 
#include 
#include 

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);

    printf("Waiting for connection...\n");
    int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    if (new_socket < 0) {
        perror("Accept failed");
        return -1;
    }
    printf("Connection accepted\n");

    close(new_socket);
    close(server_fd);
    return 0;
}

解说: 在这个示例中,accept函数是一个阻塞函数,用于接受客户端的连接请求。当没有客户端连接时,调用它的服务器线程会阻塞,直到有客户端连接请求到来。

适用场景:
1.网络编程: 如服务器端接受客户端连接请求,accept函数会阻塞直到有客户端连接。
2.文件I/O操作: 如fread和fwrite,在读写操作完成前,调用它们的线程会被阻塞。
3.线程同步: 使用互斥锁(pthread_mutex_lock)和条件变量(pthread_cond_wait)等同步机制时,线程会在等待条件满足时被阻塞。

2. 非阻塞函数(Non-Blocking Functions)

英文全称及简写: Non-Blocking Functions(简写为NBF)

定义: 非阻塞函数是指不会暂停调用线程的函数。这些函数通常用于提高程序的并发性和响应性,允许调用线程在等待操作完成时继续执行其他任务。
具体含义:
1. 非阻塞: 调用非阻塞函数后,调用线程不会被阻塞,可以继续执行其他任务。
2. 立即返回: 即使操作未完成,函数也会立即返回,通常返回一个状态码表示操作是否成功或是否需要再次尝试。
3. 提高效率: 特别适用于I/O密集型任务,如网络请求、文件读写等,可以提高程序的并发性和效率。

【示例5.1-2】非阻塞函数案例

#include 
#include 
#include 
#include 
#include 

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        return -1;
    }
    flags |= O_NONBLOCK;
    if (fcntl(fd, F_SETFL, flags) == -1) {
        return -1;
    }
    return 0;
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(server_fd);  // 设置为非阻塞模式

    struct sockaddr_in address;
    int addrlen = sizeof(address);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);

    printf("Waiting for connection...\n");
    int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    if (new_socket < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("No connection available, continuing with other tasks...\n");
        } else {
            perror("Accept failed");
            return -1;
        }
    } else {
        printf("Connection accepted\n");
    }

    close(server_fd);
    return 0;
}

解说: 在这个示例中,set_nonblocking函数将套接字设置为非阻塞模式。调用accept函数时,如果没有客户端连接,函数会立即返回-1,并设置errno为EAGAIN或EWOULDBLOCK,表示没有连接可用。调用线程可以继续执行其他任务,而不是被阻塞。

适用场景:
1.网络编程: 如非阻塞的socket编程,发送和接收数据的函数不会阻塞线程,即使数据未准备好,线程也可以继续执行其他操作。
2.用户界面: 在图形用户界面中,非阻塞处理可以防止界面冻结,提高用户体验。
3.服务器应用: 在处理多个客户端请求时,非阻塞处理可以提高服务器的吞吐量。
比较: 阻塞和非阻塞函数二者的联系和区别
同:
异:
✨总结:
1. 阻塞函数: 阻塞函数
2. 非阻塞函数: 适用于需要提高并发性和响应性的场景,如网络编程、用户界面等。非阻塞函数不会暂停调用线程,调用线程可以继续执行其他任务。

大灰: 选择合适的函数类型可以提高程序的正确性和性能。在实际开发中,根据具体的应用场景和需求选择最合适的函数类型。如果任务是I/O密集型的,优先考虑使用非阻塞函数;如果任务是计算密集型的或需要按顺序执行,阻塞函数可能更合适。


5️⃣⏺️2️⃣按函数的执行特性划分

1. 异步函数(Asynchronous Functions)

英文全称及简写: Asynchronous Functions(简写为Async Functions)

定义: 异步函数是指不会阻塞调用线程的函数。这些函数通常用于处理I/O操作和网络请求,允许调用线程在等待操作完成时继续执行其他任务。
具体含义:
1. 非阻塞: 调用异步函数后,调用线程不会被阻塞,可以继续执行其他任务。
2. 回调机制: 通常使用回调函数或事件通知机制来处理操作完成后的结果。
3. 提高效率: 特别适用于I/O密集型任务,如网络请求、文件读写等,可以提高程序的并发性和效率。

【示例5.2-1】异步函数案例

Project Tree:

workplace
	|--- async
			|--- example.txt
			|--- main.c

FileName: example.txt

Hello LuminKu!

FileName: main.c

#include 
#include 
#include 

void* async_read_file(void* filename) {
    FILE* file = fopen((const char*)filename, "r");
    if (file == NULL) {
        perror("Failed to open file");
        return NULL;
    }
    char buffer[1024];
    size_t bytes_read = fread(buffer, 1, sizeof(buffer), file);
    fclose(file);
    printf("Read %zu bytes from file: %s\n", bytes_read, buffer);
    return NULL;
}

int main() {
    pthread_t thread;
    const char* filename = "example.txt";

    // 创建线程异步读取文件
    pthread_create(&thread, NULL, async_read_file, (void*)filename);

    // 主线程继续执行其他任务
    printf("Main thread is doing other work...\n");
    sleep(2);  // 模拟其他工作

    // 等待异步读取完成
    pthread_join(thread, NULL);

    return 0;
}

解说: 在这个示例中,async_read_file函数是一个异步函数,它在一个单独的线程中读取文件内容。主线程在调用pthread_create后不会被阻塞,可以继续执行其他任务。当文件读取完成后,async_read_file函数会打印读取的内容。

输出:

Main thread is doing other work...
Read 14 bytes from file: Hello LuminKu!

--------------------------------
Process exited after 2.073 seconds with return value 0
请按任意键继续. . .
适用场景:
1.I/O密集型任务: 如网络请求、文件读写等,可以提高程序的并发性和效率。
2.用户界面: 在图形用户界面中,异步处理可以防止界面冻结,提高用户体验。
3.服务器应用: 在处理多个客户端请求时,异步处理可以提高服务器的吞吐量。

2. 同步函数(Synchronous Functions)

英文全称及简写: Synchronous Functions(简写为Sync Functions)

定义: 同步函数是指会阻塞调用线程直到操作完成的函数。大多数标准库函数都是同步的,调用这些函数时,调用线程会一直等待,直到操作完成。
具体含义:
1. 阻塞: 调用同步函数后,调用线程会被阻塞,直到操作完成。
2. 简单直接: 适用于简单的、不需要并发处理的场景,代码逻辑简单直接。
3. 性能瓶颈:在I/O密集型任务中,可能会导致性能瓶颈,因为调用线程在等待I/O操作完成时无法执行其他任务。

【示例5.2-2】同步函数案例

#include 

int sync_read_file(const char* filename) {
    FILE* file = fopen(filename, "r");
    if (file == NULL) {
        perror("Failed to open file");
        return -1;
    }
    char buffer[1024];
    size_t bytes_read = fread(buffer, 1, sizeof(buffer), file);
    fclose(file);
    printf("Read %zu bytes from file: %s\n", bytes_read, buffer);
    return 0;
}

int main() {
    const char* filename = "example.txt";

    // 同步读取文件
    if (sync_read_file(filename) == -1) {
        return 1;
    }

    // 主线程在读取文件期间被阻塞,无法执行其他任务
    printf("Main thread is doing other work...\n");

    return 0;
}

解说: 在这个示例中,sync_read_file函数是一个同步函数,它在主线程中读取文件内容。调用sync_read_file函数时,主线程会被阻塞,直到文件读取操作完成。文件读取完成后,主线程才会继续执行后续的代码。

输出:

Read 14 bytes from file: Hello LuminKu!
Main thread is doing other work...

--------------------------------
Process exited after 0.06205 seconds with return value 0
请按任意键继续. . .
适用场景:
1.简单任务: 适用于简单的、不需要并发处理的场景,代码逻辑简单直接。
2.顺序执行: 在需要按顺序执行任务的场景中,同步函数可以确保任务的顺序性。
3.调试:适用于简单的、不需要并发处理的场景,代码逻辑简单直接。
比较: 异步和同步函数二者的联系和区别
同:
异:
✨总结:
1. 异步函数: 适用于I/O密集型任务,如网络请求、文件读写等,可以提高程序的并发性和效率。异步函数不会阻塞调用线程,调用线程可以继续执行其他任务。
2. 同步函数: 适用于简单的、不需要并发处理的场景,代码逻辑简单直接。同步函数会阻塞调用线程,直到操作完成。 选择合适的函数类型可以提高程序的正确性和性能。在实际开发中,根据具体的应用场景和需求选择最合适的函数类型。

大灰: 选择合适的函数类型可以提高程序的正确性和性能。在实际开发中,根据具体的应用场景和需求选择最合适的函数类型。如果任务是I/O密集型的,优先考虑使用异步函数;如果任务是计算密集型的或需要按顺序执行,同步函数可能更合适。


5️⃣⏺️3️⃣按函数的线程安全性划分

1. 线程安全函数(Thread-Safe Functions)

英文全称及简写: Thread-Safe Functions(简写为TSF)

定义: 线程安全函数是指在多线程环境中可以安全调用的函数,不会导致数据竞争或资源冲突。这些函数通常通过内部的同步机制(如互斥锁)来确保在多个线程同时调用时,函数的行为是正确的。
具体含义:
1. 数据竞争: 多个线程同时访问和修改共享数据,导致数据不一致。
2. 资源冲突: 多个线程同时访问和修改共享资源,导致资源状态不一致。
3. 同步机制: 使用互斥锁(Mutex)、信号量(Semaphore)等同步原语来保护共享数据和资源,确保同一时间只有一个线程可以访问。

小夏: 线程安全函数通常会采用一些机制来避免多线程环境下的问题。例如,使用互斥锁(mutex)来保护共享资源,确保同一时刻只有一个线程可以访问该资源;或者使用原子操作来保证操作的不可分割性,使得多个线程对同一个变量的操作不会出现中间状态。线程安全函数的设计使得它们可以在并发编程中被安全地使用,不会因为线程之间的干扰而导致程序出错。

【示例5.3-1】多线程中的安全函数

#include 
#include     // 包含POSIX线程库,用于多线程编程。

int shared_data = 0;    // 定义一个全局变量shared_data,初始化为0。这个变量将被多个线程共享和修改。
/* 定义一个互斥锁mutex,并初始化为静态分配的默认互斥锁。这个互斥锁用于保护对shared_data变量的访问,确保同一时间只有一个线程可以修改它。 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

/*
	这是每个线程将执行的函数。它接受一个void*类型的参数(在这个例子中未使用),并返回一个void*类型的结果(这里返回NULL)。
    在函数内部,有一个循环,循环100次。每次循环中,它首先通过pthread_mutex_lock(&mutex);对互斥锁进行加锁,确保当前线程是唯一一个可以访问shared_data的线程。
    然后,它增加shared_data的值,并打印出来。
    最后,通过pthread_mutex_unlock(&mutex);解锁互斥锁,允许其他线程访问shared_data。
*/
void* thread_function(void* arg) {
    for (int i = 0; i < 100; i++) {
        pthread_mutex_lock(&mutex);  // 加锁
        shared_data++;
        printf("Shared Data: %d\n", shared_data);
        pthread_mutex_unlock(&mutex);  // 解锁
    }
    return NULL;
}

int main() {
    pthread_t threads[10];  // 定义一个pthread_t类型的数组threads,用于存储10个线程的标识符。
    // 创建10个线程。每个线程都执行thread_function函数。
    for (int i = 0; i < 10; i++) {
        pthread_create(&threads[i], NULL, thread_function, NULL);
    }

    // 接着,通过另一个循环,使用pthread_join(threads[i], NULL);等待所有线程完成。
    // pthread_join函数确保主线程(即执行main函数的线程)会等待指定的线程结束。在这个例子中,主线程会等待所有10个线程都执行完毕。
    for (int i = 0; i < 10; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_mutex_destroy(&mutex);  // 销毁互斥锁。这是一个清理步骤,确保所有使用完的资源都被正确释放。
    return 0;
}

【演示5.3-1】多线程下加锁和不加锁的区别

解说: 在这个示例中,shared_data是一个全局变量,多个线程通过thread_function函数同时访问和修改它。为了确保线程安全,使用了pthread_mutex_lock和pthread_mutex_unlock函数来加锁和解锁,防止多个线程同时修改shared_data。

适用场景:
1.多线程程序: 在多线程环境中,需要访问和修改共享数据或资源的函数。
2.并发编程: 在并发编程中,需要确保多个线程同时调用函数时不会导致数据竞争或资源冲突。
3.服务器应用: 在服务器应用中,多个请求可能同时访问和修改共享资源,需要使用线程安全函数来确保数据一致性。
4.多线程库函数: 很多标准库函数都是线程安全的,如pthread_mutex_lock、pthread_cond_wait等pthread库中的函数。这些函数用于线程同步和通信,它们通过内置的机制(如互斥锁的加锁和解锁操作)来保证线程安全。在开发多线程程序时,使用这些线程安全的库函数可以方便地实现线程之间的协调工作。
5.网络编程中的数据处理: 在网络服务器程序中,当多个线程同时处理客户端请求时,涉及到对共享数据(如客户端连接信息、会话数据等)的操作。使用线程安全函数来处理这些数据可以避免数据混乱。例如,使用线程安全的哈希表操作函数来存储和检索客户端会话信息,确保在高并发情况下数据的一致性和准确性。
6.资源管理: 在多线程程序中管理资源(如文件句柄、数据库连接等)时,线程安全函数也很重要。例如,有一个线程安全的函数用于获取和释放数据库连接,它通过内部的同步机制(如使用互斥锁来保护连接池)来保证在多线程环境下数据库连接的正确分配和回收。

【示例5.3-2】以strdup函数为例,这是一个线程安全的字符串复制函数(在一些系统库中是线程安全的实现)。

/* 以strdup函数为例,这是一个线程安全的字符串复制函数(在一些系统库中是线程安全的实现)。 */
#include  // 引入标准输入输出库
#include  // 引入标准库,包含动态内存分配函数
#include  // 引入字符串处理库
#include  // 引入POSIX线程库

// 定义一个安全的字符串复制函数,返回复制后的字符串指针
char* safe_strdup(const char* str) {
    if (str == NULL) { // 如果输入字符串为NULL,则返回NULL
        return NULL;
    }
    size_t len = strlen(str) + 1; // 计算字符串长度(包括结束符'\0')
    char* copy = (char*)malloc(len); // 动态分配内存来存储复制的字符串
    if (copy != NULL) { // 如果内存分配成功
        memcpy(copy, str, len); // 使用memcpy函数复制字符串,包括结束符'\0'
    }
    return copy; // 返回复制后的字符串指针
}

// 定义线程函数
void* thread_func(void* arg) {
    const char* original_str = "Hello, Thread-Safe World!"; // 定义一个原始字符串
    char* copied_str = safe_strdup(original_str); // 使用safe_strdup函数复制字符串
    printf("线程 %ld 复制的字符串: %s\n", pthread_self(), copied_str); // 打印当前线程ID和复制的字符串
    free(copied_str); // 释放复制字符串所占用的内存
    return NULL; // 线程函数返回NULL
}

int main() {
    pthread_t threads[5]; // 定义一个线程数组,用于存储线程ID
    for (int i = 0; i < 5; i++) { // 循环创建5个线程
        pthread_create(&threads[i], NULL, thread_func, NULL); // 创建线程,线程函数为thread_func,参数为NULL
    }

    for (int i = 0; i < 5; i++) { // 循环等待5个线程结束
        pthread_join(threads[i], NULL); // 等待线程结束,第二个参数为NULL,表示不获取线程的返回值
    }

    return 0; // 程序正常结束
}

解说: 在这个例子中,safe_strdup函数是一个线程安全的字符串复制函数。它首先计算输入字符串的长度,然后使用malloc分配一块足够大的内存来存储复制的字符串,接着使用memcpy将字符串内容复制到新分配的内存中。在多线程环境中,每个线程调用safe_strdup函数时,都会独立地进行内存分配和字符串复制操作,不会相互干扰。因为malloc和memcpy等函数在很多系统库中都是线程安全的实现,所以safe_strdup函数也是线程安全的。

输出:

线程 1 复制的字符串: Hello, Thread-Safe World!
线程 2 复制的字符串: Hello, Thread-Safe World!
线程 3 复制的字符串: Hello, Thread-Safe World!
线程 4 复制的字符串: Hello, Thread-Safe World!
线程 5 复制的字符串: Hello, Thread-Safe World!

2. 非线程安全函数(Non-Thread-Safe Functions)

英文全称及简写: Non-Thread-Safe Functions(简写为NTSF)

定义: 非线程安全函数是指在多线程环境中调用可能导致数据竞争或资源冲突的函数。这些函数通常没有内部的同步机制,多个线程同时调用时,可能会导致数据不一致或资源状态不一致。
具体含义:
1. 数据竞争: 多个线程同时访问和修改共享数据,导致数据不一致。
2. 资源冲突: 多个线程同时访问和修改共享资源,导致资源状态不一致。
3. 无同步机制: 没有使用互斥锁、信号量等同步原语来保护共享数据和资源。

小夏: 非线程安全函数没有采取措施来防止多线程环境下的问题。它们可能访问共享资源而没有加锁保护,或者进行的操作不是原子的,容易被其他线程中断。当多个线程同时调用这些函数时,可能会出现一个线程修改了共享数据,而另一个线程同时也在读取或修改这个数据,导致数据不一致或其他不可预料的问题。

【示例5.3-3】多线程中的非安全函数

#include 
#include 

// 全局变量
int shared_data = 0;

// 线程名称数组(模拟)
const char* thread_names[] = {"Thread 0", "Thread 1", "Thread 2", "Thread 3", "Thread 4", "Thread 5", "Thread 6", "Thread 7", "Thread 8", "Thread 9"};

void* thread_function(void* arg) {
    int thread_index = *(int*)arg;  // 从参数中获取线程索引
    for (int i = 0; i < 100; i++) {
        shared_data++;  // 未加锁,这里会有数据竞争问题
        printf("%s <----> Shared Data: %-4d <----> i:%d\n", thread_names[thread_index], shared_data, i);
    }
    return NULL;
}

int main() {
    pthread_t threads[10];
    int thread_indices[10];  // 用于存储线程索引并传递给线程函数

    for (int i = 0; i < 10; i++) {
        thread_indices[i] = i;  // 设置线程索引
        pthread_create(&threads[i], NULL, thread_function, &thread_indices[i]);
    }

    for (int i = 0; i < 10; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

解说: 在这个示例中,shared_data是一个全局变量,多个线程通过thread_function函数同时访问和修改它。由于没有使用互斥锁,多个线程同时修改shared_data,导致数据竞争,最终的shared_data值可能小于预期的1000。

【示例5.3-4】以strtok函数为例,这是一个非线程安全的字符串分割函数。

#include 
#include 
#include 

void* thread_func(void* arg) {
    char str[] = "Hello,Non-Thread-Safe,World!";
    char* token = strtok(str, ",");
    while (token != NULL) {
        printf("线程 %ld 分割的字符串:%s\n", pthread_self(), token);
        token = strtok(NULL, ",");
    }
    return NULL;
}

int main() {
    pthread_t threads[2];
    pthread_create(&threads[0], NULL, thread_func, NULL);
    pthread_create(&threads[1], NULL, thread_func, NULL);

    pthread_join(threads[0], NULL);
    pthread_join(threads[1], NULL);

    return 0;
}

解说: 在这个例子中,strtok函数用于分割字符串。它使用一个静态缓冲区来存储分割后的字符串和一些内部状态信息。当多个线程同时调用strtok函数时,它们会相互干扰。因为strtok函数内部的静态缓冲区是共享的,一个线程的调用可能会覆盖另一个线程的分割状态和结果,导致输出混乱,无法正确地分割字符串。这就是非线程安全函数在多线程环境下的典型问题。

适用场景:
1.单线程程序: 在单线程环境中,可以安全使用非线程安全函数,非线程安全函数可以正常使用。因为不存在多个线程同时访问和修改共享数据的问题。例如,在一个简单的命令行工具程序中,只有一个执行流程,使用strtok等非线程安全函数不会出现问题。因为没有其他线程会同时访问这些函数操作的数据。
2.局部变量: 在多或单线程环境中,如果函数只使用局部变量,且不访问共享数据或资源,可以使用非线程安全函数。例如,一个函数内部使用strtok来分割一个仅在该函数内部定义的字符串,并且这个函数不会被多个线程同时调用,那么在这个局部范围内使用strtok是安全的。
3.性能优化: 在某些高性能要求的场景中,如果可以确保不会出现数据竞争或资源冲突,可以使用非线程安全函数来提高性能。非线程安全函数通常比线程安全函数执行效率更高,因为它们没有加锁等同步开销。在一些对性能要求极高,并且可以确保不会出现多线程冲突的场景下,可以考虑使用非线程安全函数。但是这种情况需要非常谨慎地控制程序的执行流程,确保不会引入线程安全问题。
✨总结:
1. 线程安全函数: 适用于多线程环境,需要访问和修改共享数据或资源的函数。通过内部的同步机制确保数据一致性和资源状态一致性。
2. 非线程安全函数: 适用于单线程环境或局部变量的函数。在多线程环境中使用时,需要外部同步机制来确保数据一致性和资源状态一致性。

大灰: 选择合适的函数类型可以提高程序的正确性和性能。在实际开发中,根据具体的应用场景和需求选择最合适的函数类型。

小蛙: 哇,这么高级的用法,现在才知道,这么多年的函数白用啦,看来会用还是让我太狭隘了,真的会那是要弄懂这些本质的原理啊。先生大义呀,怪我一叶障目,曲解了先生的用意。


原文: 无意义的热闹,远不如有意义的孤独。
        时不时的欢愉,不如销声匿迹的沉淀。
⌛悠然: ...


LuminKu looks forward to seeing you again

你可能感兴趣的:(C,c语言)