嵌入式就是在已有的硬件平台上移植了操作系统,降低软硬件之间的耦合度,移植性高;使开发者无需考虑硬件结构参与项目,通过操作系统提供的API就可以完成大部分工作,提高了产品的开发效率,提高了用户体验率。
嵌入式操作系统?
就是一种用途广泛的系统软件;负责系统全部的软硬件资源分配,任务管理,控制协调各个进程;使软硬件之间的耦合度降低,使其依赖于一个不变的体系;将软硬件分离开来,推动项目进度,提高产品的开发效率。
(1)可移植性强的,但不一定开源;
(2)强实时性
(3)统一的接口
(4)操作方便
(5)轻量级
(6)可定制
运行时语言 :编译出来的可执行文件是直接可以被识别执行的,不需要第三方插入的。
解释语言:运行时解释给第三方,第三方再去做操作。(shell脚本、java、python)
静态语言 :编译时已经固定好了,不需要做太多变化。(fpga)
C语言是一种广泛使用的编程语言,具有许多特点和优势。以下是C语言的主要特点:
尽管C语言具有许多优势,但也有一些不足之处,如没有内置的面向对象编程特性,相对较弱的动态内存管理等。然而,由于其简洁、高效和可移植性,C语言仍然是许多编程领域的首选语言之一。
最早的C语言标准有ANSI/C89/C90,由美国国家标准协会(ANSI)于1989年和1990年发布。它定义了C语言的基本语法和功能,并成为后续标准的基础。这个标准通常被称为"ANSI C"或"C89/C90"。
国际标准化组织(ISO)发布的C99标准,C99标准增加了一些新的功能,如变长数组、内联函数、bool类型和复数支持等。
C11: C11是C语言的最新标准,于2011年发布。C11标准在C99的基础上进行了扩展,增加了一些新的特性,如泛型选择、多线程支持和_Atomic关键字等。
编译器是一种将高级语言(如C、C++等)代码转换为目标代码(通常是机器码)的软件工具。它将程序员编写的源代码转换成计算机可以执行的指令,从而使得程序能够在计算机上运行。
“GCC”(GNU Compiler Collection)是一个开源的编译器集合,由GNU计划开发。GCC最初是为C语言编写的,但后来扩展到支持多种编程语言,包括C++、Objective-C、Fortran、Ada等。GCC是一个功能强大且广泛使用的编译器,支持多种平台,如Linux、macOS、Windows等。
"Clang"是LLVM项目的一部分,作为LLVM的编译器前端,用于C、C++、Objective-C和Objective-C++编程语言。Clang的目标是提供更快的编译速度、更好的错误信息和更严格的代码检查。它也成为了流行的C/C++编译器之一。
除了GCC和Clang,还有其他编译器,如Microsoft Visual C++编译器(Windows平台)、Intel C++编译器、Oracle Solaris Studio编译器等。每个编译器都有其独特的特点和优势,选择合适的编译器取决于项目的需求、目标平台和开发人员的偏好。
PS:Visual Studio(VS)是一个集成开发环境(IDE),不是一个单独的编译器。 它包含了多种编程语言的编译器和其他开发工具。
C语言具有多种数据类型,可以分为基本数据类型和复合数据类型。
基本数据类型:
复合数据类型:
指针类型:
指针是一种特殊的数据类型,用于存储变量的内存地址。可以用指针来间接访问变量的值。
C语言的数据类型可以根据不同编译器和平台有所不同,但上述是C语言中的常见数据类型。在使用这些数据类型时,开发人员需要根据项目需求和变量的特性来选择适当的数据类型。
补码(Two’s complement)在计算机中有着重要的作用,特别是在处理带符号整数时。补码是一种表示有符号整数的方法,它解决了使用原码或反码表示带符号整数时出现的一些问题。
作为计算机中表示带符号整数的标准方法,补码有以下几个重要的作用:
表示负数: 补码能够简洁有效地表示负数。在补码表示中,最高位(最左边的位)为符号位,为1表示负数,为0表示非负数(包括0)。这使得计算机可以处理负数和非负数的整数运算。
无需额外操作: 在补码表示中,负数的表示方式和正数基本一致,无需对符号位进行特殊处理。这简化了计算机硬件的设计和运算逻辑,使得计算机可以使用相同的算术运算器来处理正数和负数。
运算方便: 补码可以简化整数的加法和减法运算。在补码中,负数的加法变成了减法操作,即两个数的补码相加后,如果产生了进位,可以将进位丢弃,最后结果就是两个数的差的补码。
表示范围: 使用n位补码,可以表示-2^(n-1) 到 2^(n-1) - 1的整数范围。例如,使用8位补码可以表示整数范围为-128到127。
总体来说,补码在计算机中提供了一种有效的表示带符号整数的方法,并简化了带符号整数的运算和处理。它是计算机硬件和软件中表示和操作带符号整数的基础。
sizeof()
和strlen()
是C语言中两个不同的函数,它们分别用于获取变量或数据的大小和计算字符串的长度。它们的区别如下:
sizeof():
sizeof()
是一个编译时运算符,而不是函数。它用于获取指定数据类型或变量所占用的字节数。它在编译时执行,返回一个常量值,不需要执行时计算。可以用于任何数据类型、变量、结构体等。
示例:
int num = 10;
size_t size = sizeof(num); // size将是整数类型的字节数,通常是4
strlen():
strlen()
是一个函数,用于计算C风格字符串的长度,即字符串中的字符数(不包括空字符’\0’)。它在运行时执行,需要遍历整个字符串才能确定长度。
示例:
char str[] = "Hello, World!";
size_t length = strlen(str); // length将是字符串的长度,不包括空字符,为12
请注意,sizeof()
返回的是数据类型或变量占用的字节数,而strlen()
返回的是字符串的实际长度。在使用这两个操作时,需要注意它们的不同用途和返回结果类型。而且,strlen()
要求字符串必须以空字符’\0’结尾,否则可能导致不正确的结果。在处理字符串时,务必注意字符串的结尾字符。
C语言的内存空间布局通常包括以下几个部分:
malloc()
、calloc()
等函数从堆中分配内存。程序员需要手动管理堆中的内存,确保在不需要时及时释放。关于Linux操作系统的内存空间,它涉及更广泛的概念:
Linux采用虚拟地址空间,将不同的内存区域隔离开来,每个进程都有自己的用户空间,进程间不会互相干扰。用户空间和内核空间是相互独立的。
在Linux中,用户空间和内核空间是相互独立的,它们各自占用不同的虚拟内存地址范围。具体的大小可以根据不同的系统架构和配置而有所不同。
通常情况下,Linux的虚拟内存空间被划分如下:
需要注意的是,这些大小可以通过内核的配置选项进行修改,具体的数值可能会因Linux版本、系统架构和配置而有所不同。因此,在实际应用中,建议使用操作系统提供的宏或函数(如PAGE_SIZE
、getrlimit
等)来动态获取用户空间和内核空间的大小,而不是依赖于固定的数值。
区别:
局部变量:
全局变量:
静态局部变量:
静态局部变量是在函数内部声明的一种特殊类型的变量。与普通的局部变量不同,静态局部变量在函数执行结束后并不会被销毁,其值在函数调用之间保持不变。在C语言中,使用static
关键字可以将局部变量声明为静态局部变量。
静态局部变量的主要特点和用途如下:
全局变量和局部变量的比较:
在C语言中,static
关键字具有不同的作用,它可以用于不同的上下文中,包括局部变量、全局变量、函数和函数参数。以下是static
关键字的主要作用:
静态局部变量: 当static
关键字用于局部变量时,它将变量声明为静态局部变量。静态局部变量的生命周期超出其所属函数的执行范围,它在第一次执行时初始化,并在后续的函数调用中保持其值。
示例:
void function() {
static int count = 0; // 这是一个静态局部变量
count++;
}
静态全局变量: 当static
关键字用于全局变量时,它将变量声明为静态全局变量。静态全局变量具有文件作用域,只在当前文件中可见,不会与其他文件中同名的全局变量产生冲突。
示例:
static int globalVar; // 这是一个静态全局变量
void function() {
globalVar = 20; // 可以在当前文件的任何函数中使用和修改 globalVar
}
静态函数: 当static
关键字用于函数时,它将函数声明为静态函数。静态函数具有文件作用域,只在当前文件中可见,不会被其他文件调用。
示例:
static void helperFunction() {
// 这是一个静态函数,只在当前文件内可见
}
void main() {
helperFunction(); // 可以调用静态函数
}
静态函数参数: 在函数声明时使用static
关键字,可以将函数参数声明为静态参数。静态函数参数在函数调用之间保持其值不变,类似于静态局部变量的特性。
示例:
void function(static int param) {
// 这是一个带有静态参数的函数
}
extern
关键字在C语言中有两种主要作用:
声明外部变量: 在一个文件中使用extern
关键字来声明一个在另一个文件中定义的全局变量。这样做是为了告诉编译器,该变量是在其他地方定义的,编译器在当前文件中只是对它的声明,而不会为它分配内存空间。
示例:
// File1.c
int globalVar = 10;
// File2.c
extern int globalVar; // 声明在另一个文件中定义的全局变量
在上述示例中,File2.c
中的extern int globalVar;
声明了globalVar
是在File1.c
文件中定义的全局变量。
声明外部函数: 在一个文件中使用extern
关键字来声明一个在另一个文件中定义的函数。这样做是为了告诉编译器,在当前文件中只是对函数的声明,函数的定义在其他文件中。
示例:
// File1.c
int add(int a, int b) {
return a + b;
}
// File2.c
extern int add(int a, int b); // 声明在另一个文件中定义的函数
在上述示例中,File2.c
中的extern int add(int a, int b);
声明了add
函数是在File1.c
文件中定义的。
需要注意的是,extern
关键字只是进行变量或函数的声明,并不会为其分配内存或定义函数体。它是为了告诉编译器,在其他地方已经定义了这个变量或函数,当前文件只是对它的声明。这样,在编译时,编译器就知道这些标识符在其他文件中定义,并能正确地链接它们。
register
是C语言中的一个关键字,用于给编译器提供一个提示,建议将指定的变量存储在寄存器中,以便在频繁访问的情况下提高变量的访问速度。然而,需要注意的是,register
关键字仅仅是一个建议,编译器是否真正将变量存储在寄存器中取决于编译器的实现和优化策略。在现代编译器中,由于编译器已经具有强大的优化能力,通常不再需要显式使用register
关键字。
C++中已经弃用。
const
关键字在C语言中用于创建常量、修饰指针、修饰函数参数、修饰结构体成员等,能够提高程序的安全性和可维护性。合理使用const
关键字有助于编写更加健壮和清晰的代码。
1. 常量定义:
const
用于定义常量,常量一旦被定义,其值在程序执行过程中不能被修改。常量可以用于表示不变的数值、固定的参数等。
const int MAX_VALUE = 100; // 定义一个常量 MAX_VALUE,并初始化为100
const float PI = 3.14159; // 定义一个常量 PI,并初始化为3.14159
2. 常量指针:
const
用于修饰指针,表示指针所指向的值是常量,不能通过该指针修改所指向的值。
int num = 10;
const int* ptr = # // 定义一个指向常量整数的指针
*ptr = 20; // 错误!不能通过ptr修改num的值
3. 指针常量:
const
用于修饰指针,表示指针本身是常量,不能修改指针所指向的地址。
int num = 10;
int* const ptr = # // 定义一个指针常量,不可修改ptr的值
*ptr = 20; // 可以通过ptr修改num的值
4. 常量指针常量:
const
同时用于修饰指针和指针指向的值,表示指针本身是常量,指针指向的值也是常量,即指针和指针指向的值都不能修改。
const int num = 10;
const int* const ptr = # // 定义一个常量指针常量
*ptr = 20; // 错误!不能通过ptr修改num的值
ptr = &anotherNum; // 错误!不能修改ptr的值
测试
#include
int main() {
int num = 10;
int anotherNum = 100;
// 指针常量(Pointer to Constant)
const int* ptrToConst = # // 定义一个指向常量整数的指针常量
printf("Pointer to Constant:\n");
printf("*ptrToConst: %d\n", *ptrToConst); // 输出:*ptrToConst: 10
//*ptrToConst = 20; // 错误!不能通过ptrToConst修改num的值
ptrToConst = &anotherNum; // 正确!可以修改指针指向的地址
printf("*ptrToConst after reassignment: %d\n", *ptrToConst); // 输出:*ptrToConst after reassignment: 100
printf("\n");
// 常量指针(Constant Pointer)
int* const constPtr = # // 定义一个常量指针
printf("Constant Pointer:\n");
printf("*constPtr: %d\n", *constPtr); // 输出:*constPtr: 10
*constPtr = 20; // 可以通过constPtr修改num的值
printf("num after modification: %d\n", num); // 输出:num after modification: 20
//constPtr = &anotherNum; // 错误!不能修改常量指针的地址
printf("\n");
// 常量指针常量(Constant Pointer to Constant)
const int* const constPtrToConst = # // 定义一个常量指针常量
printf("Constant Pointer to Constant:\n");
printf("*constPtrToConst: %d\n", *constPtrToConst); // 输出:*constPtrToConst: 20
//*constPtrToConst = 30; // 错误!不能通过constPtrToConst修改num的值
//constPtrToConst = &anotherNum; // 错误!不能修改常量指针的地址
printf("\n");
return 0;
}
5. 函数参数中的const:
const
用于修饰函数的参数,表示函数内部不会修改该参数的值。这样可以增加函数的安全性和可读性。
void printValue(const int value) {
printf("Value: %d\n", value);
}
int num = 100;
printValue(num); // 可以传递变量作为参数
printValue(42); // 也可以传递常量作为参数
6. const与数组:
const
用于定义常量数组,这样数组的元素值在程序执行过程中不能被修改。
const int SIZE = 5;
const int arr[SIZE] = {1, 2, 3, 4, 5}; // 定义一个常量数组
arr[0] = 10; // 错误!不能修改数组元素的值
7. const与函数返回值:
const
用于修饰函数的返回值,表示函数返回的值是常量。
const int getConstantValue() {
return 100; // 函数返回一个常量值
}
int num = getConstantValue();
num = 200; // 错误!函数返回值是常量,不能修改
8. const与结构体:
const
用于修饰结构体,表示结构体内的成员值是常量,不能修改。
struct Point {
const int x;
const int y;
};
struct Point p1 = {1, 2};
p1.x = 10; // 错误!不能修改结构体成员的值
注意事项:
const
关键字可以用于任何基本数据类型和自定义数据类型(结构体、枚举等)。const
定义的常量必须在声明时初始化,且初始化后不能再修改。const
定义的常量在编译时即被确定,存储在符号表中,不占用存储空间。extern
声明,同时可以使用const
关键字修饰来防止被修改。// File1.c
const int MAX_VALUE = 100;
// File2.c
extern const int MAX_VALUE; // 声明在另一个文件中定义的全局常量
当使用typedef
定义函数指针类型和函数指针数组类型时,可以将较为复杂的类型声明简化为更直观的类型别名。下面将给出一个详细的例子来说明这两个用法。
1. 使用typedef
定义函数指针类型:
#include
// 原始的函数指针声明
int (*ptrToFunction)(int, int);
// 使用typedef定义函数指针类型
typedef int (*FunctionPointer)(int, int);
// 定义一个函数,用于加法操作
int add(int a, int b) {
return a + b;
}
int main() {
// 使用原始函数指针声明方式
ptrToFunction = add;
int result1 = (*ptrToFunction)(5, 3);
printf("Result 1: %d\n", result1);
// 使用typedef定义的函数指针类型
FunctionPointer myFunctionPtr = add;
int result2 = myFunctionPtr(10, 20);
printf("Result 2: %d\n", result2);
return 0;
}
在上述代码中,我们先使用原始的函数指针声明方式int (*ptrToFunction)(int, int);
,然后使用typedef
定义了一个名为FunctionPointer
的函数指针类型。在main
函数中,我们分别使用这两种方式声明了函数指针变量,并将其指向了add
函数。然后通过两种方式调用函数指针并得到了不同的计算结果。
2. 使用typedef
定义函数指针数组类型:
#include
// 原始的函数指针数组声明
int (*ptrArray[3])(int, int);
// 使用typedef定义函数指针数组类型
typedef int (*FunctionPointer)(int, int);
typedef FunctionPointer FunctionPointerArray[3];
// 定义一些函数,用于加法、减法和乘法操作
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
// 使用原始函数指针数组声明方式
ptrArray[0] = add;
ptrArray[1] = subtract;
ptrArray[2] = multiply;
// 使用typedef定义的函数指针数组类型
FunctionPointerArray myFunctionPtrArray = { add, subtract, multiply };
// 通过循环调用函数指针数组中的函数
int a = 10, b = 5;
for (int i = 0; i < 3; i++) {
int result = ptrArray[i](a, b);
printf("Result %d: %d\n", i + 1, result);
}
printf("\n");
// 通过循环调用typedef定义的函数指针数组中的函数
for (int i = 0; i < 3; i++) {
int result = myFunctionPtrArray[i](a, b);
printf("Result %d: %d\n", i + 1, result);
}
return 0;
}
在上述代码中,我们先使用原始的函数指针数组声明方式int (*ptrArray[3])(int, int);
,然后使用typedef
定义了一个名为FunctionPointer
的函数指针类型和一个名为FunctionPointerArray
的函数指针数组类型。在main
函数中,我们分别使用这两种方式声明了函数指针数组,并将其指向了add
、subtract
和multiply
三个不同的函数。然后通过循环调用函数指针数组中的函数,得到了不同的计算结果。
通过这两个例子,我们可以看到,使用typedef
定义函数指针类型和函数指针数组类型可以使代码更加清晰和简洁,增加了代码的可读性和可维护性。
break
和continue
是两个用于控制流程的关键字,它们在循环语句中的作用有所不同。
1. break:
break
关键字用于立即终止循环(for
、while
、do-while
)的执行,并跳出循环体。当程序执行到break
语句时,循环内剩余的代码将不再执行,而是直接跳出整个循环,继续执行循环之后的代码。
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break; // 当i等于5时,立即终止循环
}
printf("%d ", i);
}
// 输出结果:1 2 3 4
在上述代码中,当i
等于5时,break
语句被执行,导致循环立即终止,因此只输出了1、2、3、4四个数字。
2. continue:
continue
关键字用于跳过循环中余下的代码,直接进入下一次循环的判断条件。当程序执行到continue
语句时,循环体中剩余的代码将被跳过,但循环条件会重新进行判断,从而决定是否进行下一次循环迭代。
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 当i等于3时,跳过循环内的剩余代码,进行下一次循环迭代
}
printf("%d ", i);
}
// 输出结果:1 2 4 5
在上述代码中,当i
等于3时,continue
语句被执行,导致循环内剩余的代码(即printf("%d ", i);
)被跳过,直接进入下一次循环的判断条件。因此,输出结果中没有数字3。
总结:
break
用于立即终止循环,跳出循环体,执行循环之后的代码。continue
用于跳过循环内余下的代码,直接进行下一次循环迭代。在C语言中,可以使用异或运算和四则运算来交换两个变量的值。下面分别介绍这两种方法的实现:
1. 使用异或运算交换两个变量的值:
异或运算有一个重要的性质:对于任意整数a和b,有a ^ b ^ b = a,即连续两次异或同一个值会得到原来的值。基于这个性质,可以使用异或运算来交换两个变量的值。
使用异或运算的方法相比使用四则运算的方法更加巧妙,因为它不需要借助额外的临时变量,从而节省了内存空间。
#include
void swapUsingXOR(int* a, int* b) {
if (a != b) { // 确保a和b不是同一个地址
*a = *a ^ *b;
*b = *a ^ *b;
*a = *a ^ *b;
}
}
int main() {
int x = 10, y = 20;
printf("Before swap: x = %d, y = %d\n", x, y);
swapUsingXOR(&x, &y);
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
在上述代码中,swapUsingXOR
函数使用异或运算交换两个变量的值。首先,通过*a = *a ^ *b;
将a和b的值异或,并将结果保存到a中;然后,通过*b = *a ^ *b;
将a和b的值异或,并将结果保存到b中;最后,通过*a = *a ^ *b;
将a和b的值异或,并将结果保存到a中,此时a的值就变成了原来b的值,b的值就变成了原来a的值,完成了变量的交换。
2. 使用四则运算交换两个变量的值:
除了使用异或运算,还可以使用四则运算来交换两个变量的值。这种方法需要借助一个临时变量。
#include
void swapUsingArithmetic(int* a, int* b) {
if (a != b) { // 确保a和b不是同一个地址
int temp = *a;
*a = *b;
*b = temp;
}
}
int main() {
int x = 10, y = 20;
printf("Before swap: x = %d, y = %d\n", x, y);
swapUsingArithmetic(&x, &y);
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
在上述代码中,swapUsingArithmetic
函数使用四则运算交换两个变量的值。首先,通过int temp = *a;
将a的值保存到一个临时变量temp中;然后,通过*a = *b;
将b的值赋给a;最后,通过*b = temp;
将临时变量temp的值赋给b,此时a的值就变成了原来b的值,b的值就变成了原来a的值,完成了变量的交换。
位运算在计算机底层编程、嵌入式开发、算法设计等方面发挥着重要作用。位运算主要是直接操控二进制数时使用 ,主要目的是节约内存,使程序速度更快,还有就是在对内存要求苛刻的地方使用。
指针是用来存储其他变量的内存地址的变量。指针的出现使得程序可以间接地访问和操作内存中的数据,极大地增强了程序的灵活性和效率。
什么是指针?
指针是一个存储其他变量地址的变量。它包含了一个内存地址,指向另一个变量在内存中的存储位置。通过指针,可以间接地访问和修改这个地址所指向的变量的值。指针的类型必须与它所指向的变量类型相匹配。
指针的作用?
指针的运算?
指针可以进行一些基本的运算,包括:
指针变量的大小为什么是固定大小?
指针变量的大小在不同的架构和操作系统中是固定的,通常是与系统的寻址能力相关的。在32位系统中,指针的大小通常是4字节(32位),而在64位系统中,指针的大小通常是8字节(64位)。这是因为指针必须能够存储一个完整的内存地址,而内存地址的大小由系统的寻址能力决定。
为什么会有不同类型的指针?
不同类型的指针是为了处理不同类型的数据而存在的。指针的类型必须与它所指向的变量类型相匹配,这样才能正确地访问和操作内存中的数据。例如,指向整数的指针必须是int*
类型,指向字符的指针必须是char*
类型,指向结构体的指针必须是相应结构体类型的指针等。不同类型的指针可以用于处理各种数据类型,使得程序更加灵活和通用。同时,不同类型的指针也要求开发人员在使用时要注意类型匹配,避免出现潜在的错误。
万能指针(void*
)是C语言中一种特殊类型的指针,它可以指向任意类型的数据。由于其灵活性,它被称为"万能指针"。
万能指针可以接受任何指针变量的值,函数的返回值和形参使用万能指针可以提高通配性。使用时要注意不能通过*来获取指向空间。
编程时需要注意以下事项:
1. 无类型指针: void*
是一种无类型指针,它没有指向具体类型的信息。因此,在使用万能指针时,必须明确指定其指向的数据类型。
2. 需要显式转换: 由于void*
是无类型指针,无法直接进行指针运算和解引用。在使用之前,必须将其转换为特定类型的指针。转换时要确保指向的数据类型与实际数据类型匹配,否则可能导致未定义的行为。
3. 内存泄漏风险: 由于void*
丧失了类型信息,容易导致内存泄漏。如果使用void*
进行内存分配(例如,使用malloc
返回void*
),在释放内存时需要注意转换回正确的类型。
4. 潜在错误: 由于万能指针无法提供类型检查,如果误用或不正确地转换指针类型,可能导致程序运行时出现意想不到的错误,如数据错误、段错误等。
5. 慎用万能指针: 虽然万能指针在某些场景下非常有用,但在一般情况下,应该避免频繁使用它。最好使用类型安全的指针,这样可以在编译时进行类型检查,减少错误的发生。
6. 使用场景: 万能指针通常用于处理未知类型的数据或在不同类型之间进行通用的数据传递。在使用万能指针时,最好有充分的理由,并且需要仔细检查类型转换的正确性。
示例:
#include
int main() {
int num = 42;
double pi = 3.14159;
void* ptr;
// ptr指向int类型的数据
ptr = #
int* intPtr = (int*)ptr; // 转换为int指针
printf("Value of num: %d\n", *intPtr);
// ptr指向double类型的数据
ptr = π
double* doublePtr = (double*)ptr; // 转换为double指针
printf("Value of pi: %lf\n", *doublePtr);
return 0;
}
在上述示例中,我们使用void*
指针分别指向int类型和double类型的数据,并在使用前显式转换为相应的指针类型。这个例子展示了void*
指针的基本用法。然而,需要特别注意在实际应用中避免滥用万能指针,以确保代码的类型安全性和稳定性。
什么是野指针?
野指针是指指向无效内存地址的指针。这种指针指向的内存地址可能是未初始化的、已释放的或者超出了其作用域的,因此访问这个指针指向的内存会导致未定义的行为。野指针通常是由于不正确地使用指针引起的,是一种常见的编程错误。
如何避免野指针?
NULL
或者一个有效的内存地址。避免在声明时不给指针赋值,以免产生未知的初始值。NULL
或者释放指向的内存,避免指针悬空。malloc
、calloc
等)时,要确保适时释放内存,并避免产生内存泄漏。如何检测野指针?
检测野指针是一项重要的防御性编程措施,以及确保程序的安全性和稳定性的手段。可以通过以下方法检测野指针:
*ptr
)之前,先判断指针是否为NULL。如果指针为NULL,说明它是一个野指针。当谈及字符串和内存复制函数时,C语言提供了几个不同的函数,它们有不同的特点和用途。以下是 memcpy
、strcpy
、strncpy
和 memmove
函数的整理和区别:
1. memcpy
:
void *memcpy(void *dest, const void *src, size_t n);
2. strcpy
:
char *strcpy(char *dest, const char *src);
3. strncpy
:
char *strncpy(char *dest, const char *src, size_t n);
\0
填充。不会自动添加字符串结束符。4. memmove
:
void *memmove(void *dest, const void *src, size_t n);
memcpy
类似,也用于内存复制,但可以处理源和目标内存块有重叠的情况。使用这些函数时需要注意以下事项:
strcpy
、strncpy
和memcpy
时,要确保目标数组有足够的空间来存储复制的内容,避免缓冲区溢出问题。strncpy
时,需要特别注意源字符串的长度和目标数组的大小,确保目标数组有足够的空间来存储复制的内容。手动添加字符串结束符是一个常见的做法。memmove
函数而不是memcpy
函数。特殊字符串处理函数在C语言中用于对字符串进行特定的操作和处理。以下是一些常见的特殊字符串处理函数及它们的作用:
1. strtok
:
char *strtok(char *str, const char *delim);
str
,后续调用传入NULL
。delim
是一个包含分隔符的字符串,用于指定分隔符。函数会返回一个指向下一个子字符串的指针,当返回NULL
时表示没有更多的子字符串。2. strcspn
:
size_t strcspn(const char *str, const char *reject);
strcspn
会从str
中查找连续不包含在reject
中的字符的长度,并返回这个长度。3. strspn
:
size_t strspn(const char *str, const char *accept);
strspn
会从str
中查找连续包含在accept
中的字符的长度,并返回这个长度。4. strstr
:
char *strstr(const char *haystack, const char *needle);
strstr
会在haystack
字符串中查找needle
字符串的第一次出现,并返回指向这个位置的指针。5. strchr
和strrchr
:
char *strchr(const char *str, int c);
和 char *strrchr(const char *str, int c);
strchr
查找字符c
在str
中的第一次出现,并返回指向这个位置的指针;strrchr
查找字符c
在str
中的最后一次出现,并返回指向这个位置的指针。6. strcmp
和strncmp
:
int strcmp(const char *str1, const char *str2);
和 int strncmp(const char *str1, const char *str2, size_t n);
strcmp
用于比较两个字符串是否相等,返回值为0表示相等;strncmp
比较两个字符串的前n个字符是否相等,返回值为0表示相等。7. strlen
:
\0
。size_t strlen(const char *str);
strlen
返回str
字符串的字符数,不包括字符串结束符\0
。一维数组名的作用:
arr++;
是非法的,因为数组名 arr
是指针常量,不能被赋值或修改。一维数组的地址取值的意义:
int arr[5]
,&arr[0]
、&arr[1]
等表示数组中不同元素的地址。int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("Array address: %p\n", &arr); // 表示整个一维数组 arr 的地址
printf("Array address: %p\n", arr); // 数组的首元素地址
printf("Element 0 address: %p\n", &arr[0]); // 数组第一个元素的地址
printf("Element 1 address: %p\n", &arr[1]); // 数组第二个元素的地址
return 0;
}
二维数组名的作用:
int (*)[n]
,其中 n
是数组的第二维大小(列数)。因此,二维数组名代表整个二维数组的起始地址。二维数组地址取值的意义:
arr
可以获取整个二维数组 arr
的地址,即数组的首元素地址。这在函数参数传递、动态内存分配等场景中非常有用。具体来说:
示例:
#include
int main() {
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
printf("Array address: %p\n", &arr); // 整个二维数组的地址
printf("Array address: %p\n", arr); // 二维数组的首元素地址
printf("Array address: %p\n", &arr[0]); // 二维数组的首元素地址
printf("Address of first row: %p\n", arr[0]); // 第一行的地址
return 0;
}
arr
表示整个二维数组的首元素地址:
arr
是一个指向包含 4 个元素的一维数组的指针。在指针运算中,arr + 1
会指向二维数组的下一行,即第二行的首元素地址。
&arr[0]
表示二维数组中的第一行的地址:
&arr[0]
是一个指向包含 4 个元素的一维数组的指针。在指针运算中,&arr[0] + 1
会指向二维数组中的下一行,即第二行的首元素地址。
总结:
arr
是指向二维数组中第一行的指针,表示整个二维数组的首元素地址。&arr[0]
是指向二维数组中第一行的地址,它也是一个指向包含 4 个元素的一维数组的指针。数组指针变量是指向数组的指针,它可以指向一维数组、二维数组或更高维度的数组。数组指针变量的类型取决于它所指向的数组的类型和维度。使用数组指针可以方便地操作数组元素,而无需知道数组的具体维度和大小。
数组指针变量的作用:
使用场景:
示例:
#include
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int *ptr1 = arr1; // 数组指针指向一维数组
int (*ptr2)[3] = arr2; // 数组指针指向二维数组的一维数组
printArray(ptr1, 5); // 通过数组指针传递一维数组
for (int i = 0; i < 3; i++) {
printArray(ptr2[i], 3); // 通过数组指针传递二维数组的每个一维数组
}
return 0;
}
输出:
1 2 3 4 5
1 2 3
4 5 6
7 8 9
在上面的示例中,ptr1
是指向一维数组 arr1
的数组指针变量,ptr2
是指向二维数组 arr2
的数组指针变量。通过这些数组指针变量,我们可以方便地传递数组给函数并遍历数组的元素。
动态数组:
动态数组是在程序运行时根据需要动态分配内存空间的数组。它的大小可以在运行时确定,并且可以根据实际需求进行动态调整。在 C 语言中,动态数组通常通过使用 malloc
、calloc
或 realloc
函数来分配内存空间,并通过 free
函数来释放内存空间。
零长数组:
零长数组是指数组的长度为0的数组。在 C 语言中,零长数组通常用于占位或作为某些数据结构的末尾标志。虽然零长数组本身不占用内存空间,但它通常用于占位,以便在需要时在同一块内存中动态分配实际的数组长度。
应用场景:
数组是一种具有固定大小、连续存储、相同数据类型和快速访问特性的数据结构。由于其固定大小和连续存储的特点,数组适用于随机访问和有序数据。然而,数组的大小是固定的,无法在运行时动态调整,这可能限制了其灵活性。在需要动态大小的情况下,可以使用动态数组或其他数据结构来替代数组。
在 C 语言中,数组名在某些情况下会被隐式地转换为指针,因此需要注意一些与数组和指针相关的细节和注意事项。下面是数组做指针时需要注意的几点:
数组名是指针常量: 数组名在大多数情况下会被隐式地转换为指向数组首元素的指针,但是数组名本身是一个常量指针,它存储了数组的地址,且不可更改。例如,对于 int arr[5]
,arr
表示指向数组 arr
的第一个元素的指针,但不能修改 arr
的值。
数组名作为函数参数: 当数组作为函数的参数传递时,数组名会退化为指向数组首元素的指针。在函数内部,形参看起来像一个指针变量,并不是数组。因此,函数内部无法获取数组的大小。通常,我们需要通过额外的参数传递数组大小。在函数内部使用 sizeof 运算符来获取数组的大小时,返回的是指针的大小,而不是整个数组的大小
示例:
void printArray(int arr[], int size) {
// 在函数内部,arr 类似于指针 int* arr
// 无法获取数组的大小,需要额外的参数 size
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 1 2 3 4 5
return 0;
}
sizeof 运算符与数组名: 在 sizeof
运算符中,数组名会被解释为数组,返回整个数组的字节大小。例如,sizeof(arr)
返回整个数组 arr
的字节大小,而不是指针的大小。
数组名与指针运算: 数组名可以进行指针运算,如 arr + 1
将得到数组中下一个元素的地址。但要注意,数组名不能被赋值或修改,因为它是一个常量指针。
示例:
int arr[] = {1, 2, 3, 4, 5};
int* ptr = arr; // 数组名被隐式转换为指向数组首元素的指针
// 数组名可以进行指针运算
printf("%d\n", *(ptr + 1)); // 输出 2
// 但是数组名本身不能被赋值或修改
// arr = ptr; // 错误:不能修改数组名 arr
总结:在使用数组做指针时,需要注意数组名是指针常量,无法修改;在函数参数传递中,数组名会退化为指针,无法在函数内部获取数组大小;在 sizeof
运算符中,数组名表示整个数组的字节大小;数组名可以进行指针运算,但本身不能被赋值或修改。理解这些细节有助于避免数组操作中的错误和误解。
指针数组和数组指针:
data_type *array_name[size];
data_type (*ptr_name)[size];
使用注意事项:
作为参数传递如何定义形参:
void functionName(data_type *array_name[], int size)
{
// Function body
}
void functionName(data_type (*ptr_name)[size], int rows)
{
// Function body
}
示例:
#include
void printIntArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void print2DArray(int (*arr)[3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printIntArray(arr1, 5); // 传递指针数组
print2DArray(arr2, 3); // 传递数组指针
return 0;
}
传值和传地址是在函数调用过程中传递参数的两种方式。它们有不同的特点和适用场景,选择哪种方式取决于需求和性能考虑。
传值(Pass by Value):
传地址(Pass by Address,也称为传引用):
选择使用传值还是传地址:
传出参数是指在函数调用中,通过函数的形参将数据从函数内部传递给函数外部,从而实现函数对外部数据的修改。传出参数的作用主要在于让函数能够返回多个值或修改外部数据。
在函数中,我们可以使用传出参数来将函数内部的计算结果传递给函数外部,而不仅仅是通过返回值来返回一个结果。这样可以简化函数的返回类型,避免使用复杂的数据结构或多个返回值。
传出参数的作用如下:
需要注意的是,在使用传出参数时,应确保传入的指针不为空,并在函数内部正确处理指针的边界情况,以避免出现指针为空或野指针引起的问题。
示例:
#include
// 传出参数实现返回多个值
void calculateSumAndProduct(int a, int b, int *sum, int *product) {
*sum = a + b;
*product = a * b;
}
// 传出参数实现修改外部数据
void incrementNumber(int *num) {
*num = *num + 1;
}
int main() {
int x = 5, y = 10;
int sum, product;
calculateSumAndProduct(x, y, &sum, &product);
printf("Sum: %d, Product: %d\n", sum, product);
int number = 10;
printf("Before increment: %d\n", number);
incrementNumber(&number);
printf("After increment: %d\n", number);
return 0;
}
输出:
Sum: 15, Product: 50
Before increment: 10
After increment: 11
在上述示例中,calculateSumAndProduct
函数使用传出参数 sum
和 product
实现返回多个值;incrementNumber
函数使用传出参数 num
实现修改外部数据。这样可以避免使用全局变量,并提高函数的灵活性。
在 C 语言中,函数本身只能返回一个值。如果需要让函数返回多个值,可以通过以下几种方式实现:
在 C 语言中,主函数是程序的入口点,它具有以下形式:
int main(int argc, char *argv[])
主函数的形参代表的意义:
int argc
:表示命令行参数的数量(Argument Count)。argc
是一个整数,用于记录命令行参数的个数。至少为 1,因为第一个参数始终是程序的名称。char *argv[]
或 char **argv
:表示命令行参数的字符串数组(Argument Vector)。argv
是一个指向指针的指针,每个指针指向一个命令行参数的字符串。主函数运行前要进行预处理、编译、汇编、链接操作,链接结束后会将启动代码链接进去。主函数被启动代码调用,而启动代码是由编译器添加到程序中的,也是程序和操作系统的桥梁,事实上,int main()描述的是main()是操作系统之间的接口。
事实证明main函数只是一个程序的入口,也相当于一个普通的函数,也能被自身调用,也能被其他函数调用。这和一般的函数之间互相调用的概念是一样的。不过需要注意的是,main函数不管是自身的调用还是被其他函数调用,都要设置函数终止的条件,这个递归函数有点相似,不然就会陷入死循环。
restrict
是c99新增的关键字,restrict指针是限定指针指向不同内存空间的,是一个限定符,是编译器用来优化的。使用者可以通过restrict限定符提示调用者某些指针是指向不同空间的,相同空间会被优化成不同空间。
使用时要注意编译器并不能完全优化不同的空间,谨慎使用restrict。
在使用 restrict
关键字时,需要遵循以下几个注意事项:
restrict
关键字用于告知编译器,通过这个指针访问的内存区域不会与其他指针重叠。因此,使用 restrict
关键字时要确保指针指向的内存区域没有被其他指针别名访问。restrict
修饰的指针,否则可能导致别名规则被破坏。restrict
关键字进行别名检查,它假设程序员已经正确使用了 restrict
关键字。示例:
#include
void addArrays(int n, int * restrict a, int * restrict b, int * restrict result) {
for (int i = 0; i < n; i++) {
result[i] = a[i] + b[i];
}
}
int main() {
int a[] = {1, 2, 3, 4, 5};
int b[] = {5, 4, 3, 2, 1};
int result[5];
addArrays(5, a, b, result);
for (int i = 0; i < 5; i++) {
printf("%d ", result[i]);
}
printf("\n");
return 0;
}
35.操作可变参数的宏有哪些?操作流程
操作可变参数的宏,可以根据传入的不定个数的参数进行不同的操作。C 语言提供了一些预定义的宏来操作可变参数,主要有以下几个:
va_start
: 该宏用于初始化可变参数列表。它接受两个参数,第一个是 va_list
类型的变量,用于保存参数列表信息;第二个是最后一个固定参数的名称。va_arg
: 该宏用于获取可变参数列表中的参数值。它接受两个参数,第一个是 va_list
类型的变量;第二个是要获取的参数的类型。每次调用 va_arg
,它会返回一个指定类型的参数,并将指针移动到下一个参数。va_end
: 该宏用于结束对可变参数的操作。它接受一个 va_list
类型的变量,用于释放参数列表相关的资源。下面是一个使用这些宏的简单例子:
#include
#include
// 可变参数宏,计算多个整数的平均值
double average(int num, ...)
{
va_list args;
va_start(args, num);
int sum = 0;
for (int i = 0; i < num; i++)
{
sum += va_arg(args, int);
}
va_end(args);
return (double)sum / num;
}
int main()
{
double result1 = average(3, 10, 20, 30);
printf("Average 1: %.2f\n", result1);//Average 1: 20.00
double result2 = average(5, 5, 10, 15, 20, 25);
printf("Average 2: %.2f\n", result2);//Average 2: 15.00
return 0;
}
操作流程:
va_list
类型的变量,用于保存参数列表信息。va_start
宏初始化 va_list
变量,传入最后一个固定参数的名称。va_arg
宏获取可变参数列表中的参数值,每次调用 va_arg
可以取得一个指定类型的参数,并将指针移动到下一个参数。va_end
宏结束对可变参数的操作,释放参数列表相关的资源。return 0;
语句的作用是在 C 语言中用于表示程序执行成功,并向调用该程序的进程返回一个退出状态码。在 C 语言中,主函数 main
的返回类型为 int
,并且规定主函数必须以 return
语句结束,返回一个整数值作为程序的退出状态码。通常情况下,返回 0 表示程序执行成功,返回其他非零值表示程序执行出现异常或错误。
return
和 exit(1)
都可以用于终止程序的执行,但它们有以下区别:
return
用于从函数中返回一个值,并结束该函数的执行。exit(1)
用于立即终止整个程序的执行,不会执行后续的代码。return
语句用于从函数中返回一个值,并将控制权返回给调用该函数的代码处,继续执行后续代码。exit(1)
函数用于立即终止程序的执行,并返回操作系统,不会继续执行任何后续代码。return
语句可以返回一个整数值作为退出状态码,表示程序的执行结果。通常情况下,返回 0 表示程序执行成功,返回其他非零值表示程序执行出现异常或错误。exit(1)
函数的参数表示程序的退出状态码,一般来说,非零的状态码表示程序执行出现错误,而 0 表示程序执行成功。NULL
),在调用函数指针之前,最好检查它是否为空。示例:
#include
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*operation)(int, int); // 声明一个函数指针变量
operation = add; // 将函数 add 的地址赋值给函数指针
printf("Result of add: %d\n", operation(5, 3)); // 输出 8
operation = subtract; // 将函数 subtract 的地址赋值给函数指针
printf("Result of subtract: %d\n", operation(5, 3)); // 输出 2
return 0;
}
示例:
#include
#include
int* createIntArray(int size) {
int* arr = (int*)malloc(size * sizeof(int));
return arr;
}
int main() {
int* array = createIntArray(5);
for (int i = 0; i < 5; i++) {
array[i] = i * 2;
printf("%d ", array[i]); // 输出 0 2 4 6 8
}
free(array); // 释放动态分配的内存
return 0;
}
在上述示例中,createIntArray
是一个指针函数,它返回一个指向动态分配整型数组的指针。在 main
函数中,我们调用 createIntArray
函数来获取一个动态分配的整型数组,并在使用完后释放了这段内存。
总结:
函数指针数组是一个数组,其元素是函数指针。函数指针数组的定义和初始化方式如下:
定义和初始化:
#include
// 假设有两个函数原型:
int add(int a, int b);
int subtract(int a, int b);
// 定义函数指针数组,数组元素为指向函数的指针
int (*operation[])(int, int) = {add, subtract};
上述代码中,我们定义了一个名为 operation
的函数指针数组,该数组的元素是指向函数的指针。在这个例子中,我们假设有两个函数 add
和 subtract
,它们都接受两个整型参数并返回一个整型结果。然后,我们将这两个函数的地址分别存储在 operation
数组中,使得 operation[0]
指向 add
函数,operation[1]
指向 subtract
函数。
作用:
函数指针数组的主要作用是实现函数映射或函数调度表。通过函数指针数组,我们可以根据索引或其他条件选择性地调用不同的函数。这样可以提高代码的灵活性和可扩展性。
使用函数指针数组的典型场景包括:
示例:
#include
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 定义函数指针数组,包含两个函数指针
int (*operation[])(int, int) = {add, subtract};
int choice;
int a = 10, b = 5;
printf("Enter 0 for add, 1 for subtract: ");
scanf("%d", &choice);
// 根据用户输入的选择调用相应的函数
int result = operation[choice](a, b);
printf("Result: %d\n", result);
return 0;
}
在上述示例中,根据用户输入的选择,我们使用函数指针数组 operation
调用了不同的函数(add
或 subtract
)。这样,在运行时可以根据用户的选择执行不同的函数操作,实现了函数调度表的功能。
总结:
在 C 语言中,数组名在某些情况下会被隐式地转换为指针,因此需要注意一些与数组和指针相关的细节和注意事项。下面是数组做指针时需要注意的几点:
数组名是指针常量: 数组名在大多数情况下会被隐式地转换为指向数组首元素的指针,但是数组名本身是一个常量指针,它存储了数组的地址,且不可更改。例如,对于 int arr[5]
,arr
表示指向数组 arr
的第一个元素的指针,但不能修改 arr
的值。
数组名作为函数参数: 当数组作为函数的参数传递时,数组名会退化为指向数组首元素的指针。在函数内部,形参看起来像一个指针变量,并不是数组。因此,函数内部无法获取数组的大小。通常,我们需要通过额外的参数传递数组大小。
示例:
void printArray(int arr[], int size) {
// 在函数内部,arr 类似于指针 int* arr
// 无法获取数组的大小,需要额外的参数 size
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 1 2 3 4 5
return 0;
}
sizeof 运算符与数组名: 在 sizeof
运算符中,数组名会被解释为数组,返回整个数组的字节大小。例如,sizeof(arr)
返回整个数组 arr
的字节大小,而不是指针的大小。
数组名与指针运算: 数组名可以进行指针运算,如 arr + 1
将得到数组中下一个元素的地址。但要注意,数组名不能被赋值或修改,因为它是一个常量指针。
示例:
int arr[] = {1, 2, 3, 4, 5};
int* ptr = arr; // 数组名被隐式转换为指向数组首元素的指针
// 数组名可以进行指针运算
printf("%d\n", *(ptr + 1)); // 输出 2
// 但是数组名本身不能被赋值或修改
// arr = ptr; // 错误:不能修改数组名 arr
总结:在使用数组做指针时,需要注意数组名是指针常量,无法修改;在函数参数传递中,数组名会退化为指针,无法在函数内部获取数组大小;在 sizeof
运算符中,数组名表示整个数组的字节大小;数组名可以进行指针运算,但本身不能被赋值或修改。理解这些细节有助于避免数组操作中的错误和误解。
内存管理的根本目的是解决运行时的错误,特别是由于内存资源有限而导致的内存相关问题。确保内存的高效合理使用可以提高系统的性能和稳定性。
内存管理方式主要分为用户管理和系统管理(垃圾回收机制)两种:
C语言的分配内存方式分为栈上的静态分配和堆上的动态分配。
静态分配一般通过数组和alloca函数,栈上分配内存需要注意栈空间的大小是有限的。
动态分配一般是通过指针,堆上分配容易照成内存泄漏。
以下函数通常用于在堆上分配内存,可以通过指针来访问这些内存。
void* malloc(size_t size);
malloc
分配的内存在使用之前需要手动进行初始化,否则可能会包含随机值。void* calloc(size_t num_elements, size_t element_size);
void* realloc(void* ptr, size_t new_size);
ptr
是空指针,则 realloc
的行为等同于 malloc(new_size)
。void free(void* ptr);
malloc
、calloc
或 realloc
分配的内存块。释放内存后,指向该内存块的指针不再有效,应该避免继续使用该指针。通过这些动态内存分配函数,可以根据实际需要在程序运行时动态地管理内存,从而提高程序的灵活性和资源利用率。同时,需要特别注意在使用这些函数时,正确地管理内存的分配和释放,避免内存泄漏和悬挂指针等问题。
内存池是一种内存管理技术,其作用是在程序运行时预先分配一定数量的内存块,并将这些内存块组织成池(或缓冲区)。程序在需要内存时,可以直接从内存池中获取已经分配好的内存块,而不是每次都使用动态内存分配函数(如 malloc
或 calloc
)来申请新的内存,从而减少内存分配的开销和碎片化,并提高内存分配和释放的效率。
在一些实际的实现中,malloc
函数为了提高内存分配和释放的效率,通常会按照一定的策略进行内存管理。这可能导致实际分配的内存块大小比用户请求的大小要大(会多分配40-60个字节),以便在内存块中维护一些额外的信息,如内存块的大小、状态等。这种额外的内存空间用于管理和跟踪内存块,以便在释放内存时能够准确地找到并回收内存。
这样做的好处是避免了频繁地进行小内存块的分配和释放,从而减少了内存分配的开销,并降低了内存碎片。一旦用户使用完毕后,内存管理系统可以将这些内存块归还到内存池中,供后续请求使用,从而减少了对操作系统内存管理的频繁调用,提高了内存分配和释放的效率。
内存池的确是一个常用的技术手段,旨在减少 malloc
函数的调用次数,避免频繁的动态内存分配,从而降低内存管理的开销,提高程序的性能和稳定性。通过合理设计内存池的大小和管理策略,可以在一定程度上避免空间的浪费,减少了内存分配的时间和开销,特别适用于需要频繁分配和释放固定大小内存块的场景。
将源文件(如 C/C++ 文件)编译成可执行文件的过程涉及多个步骤。以下是通常的步骤:
#
开头的指令),如宏定义、头文件包含等,并将其替换成相应的内容。预处理后的文件称为预处理文件。最终,经过上述步骤,源文件就会被编译成一个可执行文件,可以在相应的操作系统上运行。在不同的编程语言和编译器中,可能会有一些细微的差异,但基本的编译过程通常都遵循上述的步骤。
宏定义是 C 和 C++ 编程语言中的一种预处理指令,它允许程序员在代码中定义一些简单的文本替换规则。宏定义的作用是在编译过程中将指定的文本片段替换为预定义的内容。宏定义通常用于定义常量、函数或代码块的缩写,以及在编译时进行简单的代码替换。
宏定义的主要作用有以下几点:
定义常量: 宏定义可以用于定义常量,在代码中使用宏名称时会被替换为其对应的值。这样可以提高代码的可读性和可维护性,同时便于修改常量的值。
示例:
#define MAX_SIZE 100
int arr[MAX_SIZE];
代码块缩写: 宏定义可以用于定义代码块的缩写,将一组代码片段替换为一个宏名称。这样可以简化代码,使其更加简洁易读。
示例:
#define SQUARE(x) ((x) * (x))
int result = SQUARE(5); // 将会被替换为 int result = ((5) * (5));
条件编译: 宏定义可以用于条件编译,在预处理阶段根据条件判断是否包含某段代码。这在处理不同平台或配置下的代码时非常有用。
示例:
#define DEBUG
#ifdef DEBUG
// 在调试模式下执行的代码
#endif
函数宏: 宏定义还可以用于定义函数宏,即将宏定义替换为一段代码块。函数宏在使用时类似于函数调用,但是避免了函数调用的开销。
示例:
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int min_value = MIN(x, y);
宏函数是用编译时间换取内存空间,它的优点是省去了函数调用返回一系列操作,省去了形参的内存空间,提高了程序的运行效率,缺点就是傻瓜式替换,不安全,复杂的功能无能为力。
自定义函数是用内存空间换取运行时间,它的优点是在编译时能够及时检测出语法的问题,可以实现复杂的功能。缺点是函数运行返回释放需要一定的运行时间,对于形参还会去开辟内存空间,运行效率较慢。
总的来说,宏主要用于定义常量和缩写代码块,其定义是简单的文本替换,不做参数 检查;而 typedef 主要用于给数据类型起别名,提高代码的可读性和可维护性,具有类型安全性。
inline
是 C 和 C++ 中的一个关键字,用于对编译器提出函数内联的建议。函数内联是一种编译器优化技术,它通过将函数调用处直接替换为函数体,以减少函数调用的开销,从而提高程序的执行效率。inline
关键字可以向编译器建议将指定的函数进行内联展开。
inline
关键字的作用和优势主要有以下几点:
inline
关键字可以将函数的实际代码插入到函数调用处,减少了函数调用的开销。inline
关键字特别适用于短小的函数,这些函数的调用开销相对较大,将它们进行内联展开可以显著提高程序的执行效率。inline
关键字实际上是一种空间换时间的优化策略。将函数内联展开会增加代码的体积,但可以减少函数调用的开销,从而在一定程度上提高程序的运行速度。需要注意的是,inline
关键字只是向编译器提出内联展开的建议,编译器是否真正对函数进行内联展开取决于编译器的优化策略和实际情况。通常,编译器会根据函数的大小、复杂度和调用频率等因素来决定是否进行内联展开。在使用 inline
关键字时,应该注意以下几点:
inline
关键字,过度使用内联可能会导致代码体积增大,反而降低了缓存命中率,从而降低了性能。inline
关键字可能不会生效,编译器可能会忽略内联展开的建议。总的来说,inline
关键字是一种优化技术,通过在一定程度上增加代码体积来减少函数调用开销,从而提高程序的执行效率。但在使用时需要根据实际情况进行合理的优化,并避免滥用。
container_of
宏的作用是通过结构体内某个成员变量的地址和该变量名,以及结构体类型,找到该主结构体的地址。函数在定义时,会给形参开辟空间,如果传整个结构体会导致空间过大,如果使用container_of宏传递某个成员变量的地址,就可以节约形参的开辟空间。
container_of
宏的定义一般如下:
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
其中,ptr
是结构体成员的指针,type
是包含该成员的结构体类型,member
是结构体成员的名称。
C语言中常见的内置宏一般用于调试程序,常见的内置宏有:
LINE:打印所在的行号
func:打印函数名
TIME:打印编译时刻的时间
DATE:打印编译时时刻的日期
FILE:打印文件名
头文件重复包含导致重复定义问题可以通过使用预处理器指令来解决,常用的解决方法有以下几种:
#ifndef HEADER_NAME_H
#define HEADER_NAME_H
// 头文件内容
#endif // HEADER_NAME_H
HEADER_NAME_H
是一个自定义的宏名称,可以根据头文件的名称自行命名。在第一次包含头文件时,HEADER_NAME_H
会被定义,防止头文件内容重复包含。在后续的包含中,由于 HEADER_NAME_H
已经被定义,预处理器会跳过头文件的内容,避免了重复定义问题。
#pragma once
指令,它也可以用来避免头文件的重复包含。使用 #pragma once
指令的头文件会在第一次被包含时,标记为“已包含”,之后的包含会被忽略。#pragma once
// 头文件内容
需要注意的是,#pragma once
并非 C/C++ 标准规范,虽然大多数编译器都支持该指令,但并不是所有编译器都支持。为了确保代码的可移植性,通常推荐使用头文件保护的方法。
通过以上方法,可以有效避免头文件重复包含导致的重复定义问题,提高代码的可维护性和可移植性。在编写头文件时,建议养成良好的头文件保护习惯,以确保头文件在包含时能够正确地工作。
#include
:
#include
形式时,编译器不会在用户指定的搜索路径中查找头文件,只会在系统标准库路径中查找。#include "filename"
:
在 GCC 编译器中,可以使用 -I
选项来指定头文件搜索路径。例如,假设有一个名为 libfoo
的第三方库,其头文件位于 /path/to/libfoo/include
目录下,那么可以使用以下命令来编译包含该库的源文件:
gcc -o main.o -c main.c -I/path/to/libfoo/include
这样编译器在包含头文件时就会在指定的路径下查找。如果头文件被包含在多个源文件中,可以在编译命令中的每个源文件都加上 -I
选项,也可以将路径添加到 C_INCLUDE_PATH
或 CPLUS_INCLUDE_PATH
环境变量中。
总结:#include
用于标准库或系统提供的头文件,搜索路径为系统标准库路径;#include "filename"
用于用户自定义的头文件或第三方库的头文件,搜索路径可以通过编译器的选项或环境变量来指定。
结构体变量的大小是按照字节对齐的方式进行相加,是所有成员变量大小和字节长度的和。满足成员变量中最大数据类型的整数倍。
共用体变量是共用同一内存空间,大小由字节最长的成员变量和最大成员变量的数据类型决定,满足字节对齐方式,也满足成员变量中最大数据类型的整数倍。
因为共用体内所有成员公用一段内存,容易造成数据覆盖。用于函数传参时,传递不同类型的值,但只能存储一个值,多个值会产生覆盖。
使用结构体时要注意结构体是按照字节对齐的方式进行数据的存储,容易造成内存空洞,尽量将同类型的成员放在一起。
当成员变量中存在指针变量的时候,一定要注意浅拷贝问题,赋值符号会进行隐式浅拷贝,此时我们要自定义深拷贝函数解决浅拷贝的逻辑问题。
当结构体中包含指针变量时,需要特别小心浅拷贝问题。默认情况下,结构体的赋值符号进行的是浅拷贝,即只是简单地复制指针的值,而不会复制指针指向的数据。这样可能会导致多个结构体指针指向同一块内存,一旦其中一个指针修改了内存内容,会影响其他指针所指向的数据。为了解决浅拷贝问题,需要自定义深拷贝函数,确保在复制结构体时,也复制指针指向的数据,而不是仅仅复制指针本身。
示例:
#include
#include
#include
struct Person {
char *name;
int age;
};
// 自定义深拷贝函数
void deepCopyPerson(struct Person *dest, const struct Person *src) {
dest->name = strdup(src->name); // 使用 strdup 进行字符串的深拷贝
dest->age = src->age;
}
int main() {
struct Person p1;
p1.name = "Alice";
p1.age = 30;
struct Person p2;
deepCopyPerson(&p2, &p1);
// 修改 p2 的姓名,不会影响 p1
p2.name = "Bob";
printf("p1: %s, %d\n", p1.name, p1.age);
printf("p2: %s, %d\n", p2.name, p2.age);
// 释放内存
free(p2.name);
return 0;
}
使用联合(union
)可以判断机器的大小端(Endian)。大小端是指在多字节数据类型(如整数、浮点数)在内存中的存储方式。常见的大小端类型有大端序(Big-Endian)和小端序(Little-Endian)。
以下是使用联合来判断机器的大小端的方法:
#include
// 定义一个联合类型
union EndianChecker {
int value;
char bytes[sizeof(int)];
};
int main() {
union EndianChecker checker;
checker.value = 1;
// 如果小端序,bytes[0] 存放的是 1
// 如果大端序,bytes[3] 存放的是 1
if (checker.bytes[0] == 1) {
printf("Little-Endian\n");
} else {
printf("Big-Endian\n");
}
return 0;
}
请注意,这种方法是一种常见的用于判断大小端的方法,但在实际应用中,最好使用系统提供的相关函数或宏来获取机器的大小端,以确保代码的可移植性和准确性。例如,在 C 语言中,可以使用 htonl
和 ntohl
函数来转换网络字节序和主机字节序,从而获取当前机器的大小端信息。在 C++ 中,可以使用 std::endian
命名空间下的相关宏来获取大小端信息。
枚举实际上就是一系列整数宏定义。与结构体和共用体不同,枚举不存在枚举变量。
枚举元素是常数,因此枚举元素又称为枚举常量。因为是常量,所以不能对枚举元素进行赋值。
在实际问题中,有些变量的取值被限定在一个有限的范围内,此时就可以使用枚举类型。
文件指针就是FILE类型指针,指向文件类型的结构,结构里包含该文件的各种属性,可以对它指向的文件进行各种操作。可以随机访问文件,可以把I/O操作抽象为文件操作。
feof是站在当前文件读取位置向后看,看是否有字符,有字符返回0。feof一般用于判断一个文件是否为空。
对于一个空文件来说,当程序打开时,他的光标会停在文件的开头,此时使用feof遍历文件,如果返回值不为0,则文件为空。
fgets
函数:char* fgets(char* str, int num, FILE* stream);
函数原型说明:
str
:指向一个字符数组,用于存储读取的字符串(包括换行符 \n
)。num
:指定要读取的字符数(包括换行符 \n
),通常应该将数组大小减去 1,以确保在字符串末尾添加 null 终止符。stream
:指向一个文件流,表示要从该文件流中读取数据。使用注意事项:
fgets
函数读取数据时,会将换行符 \n
一并读取,并存储在字符串中,因此读取的字符串可能包含换行符。fgets
成功读取数据,会将字符串的地址作为返回值,如果到达文件末尾或读取失败,返回值为 NULL。fgets
读取数据后,字符串最后会被自动添加 null 终止符,因此需要确保数组大小足够容纳读取的字符串和终止符。fputs
函数:int fputs(const char* str, FILE* stream);
函数原型说明:
str
:指向一个以 null 终止的字符串,表示要写入到文件中的数据。stream
:指向一个文件流,表示要写入数据的文件流。使用注意事项:
fputs
函数会将给定的字符串写入到文件中,但不会添加换行符 \n
。fgetc
函数:int fgetc(FILE* stream);
函数原型说明:
stream
:指向一个文件流,表示要从该文件流中读取字符。使用注意事项:
fgetc
函数从文件流中读取一个字符,并以整数形式返回其 ASCII 码值。fputc
函数:int fputc(int character, FILE* stream);
函数原型说明:
character
:表示要写入到文件的字符,以整数形式传递,通常使用字符的 ASCII 码值。stream
:指向一个文件流,表示要写入数据的文件流。使用注意事项:
fputc
函数将一个字符写入到文件中。注意事项:
以下是 fopen
、fclose
、fread
、fwrite
和 fseek
函数的函数原型以及使用注意事项:
fopen
函数:FILE* fopen(const char* filename, const char* mode);
函数原型说明:
filename
:要打开的文件的路径和名称。mode
:打开文件的模式,如 “r” 表示只读,“w” 表示写入,“a” 表示追加,“rb” 表示以二进制方式只读,等等。具体模式可以组合使用。使用注意事项:
fclose
函数:int fclose(FILE* stream);
函数原型说明:
stream
:指向要关闭的文件流的指针。使用注意事项:
fread
函数:size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
函数原型说明:
ptr
:指向用于存储读取数据的缓冲区的指针。size
:每个数据项的字节大小。nmemb
:要读取的数据项的个数。stream
:指向要从中读取数据的文件流。使用注意事项:
fread
函数用于从文件中读取二进制数据。ptr
缓冲区足够大,能够容纳读取的数据。fread
返回实际成功读取的数据项个数,应该与 nmemb
相等,否则可能意味着到达文件末尾或读取错误。fwrite
函数:size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
函数原型说明:
ptr
:指向要写入文件的数据缓冲区的指针。size
:每个数据项的字节大小。nmemb
:要写入的数据项的个数。stream
:指向要写入数据的文件流。使用注意事项:
fwrite
函数用于向文件中写入二进制数据。ptr
缓冲区中包含正确的数据,且缓冲区大小能够容纳要写入的数据。fwrite
返回实际成功写入的数据项个数,应该与 nmemb
相等,否则可能意味着写入错误。fseek
函数:int fseek(FILE* stream, long offset, int whence);
函数原型说明:
stream
:指向要定位的文件流。offset
:相对于 whence
的偏移量,可以是正数或负数。whence
:指定起始位置,可以是 SEEK_SET
(文件开头)、SEEK_CUR
(当前位置)或 SEEK_END
(文件末尾)。使用注意事项:
fseek
函数用于定位文件指针的位置,以便实现文件的随机访问。fseek
函数时,应该检查返回值,确保定位操作是否成功。注意:以上函数都是 C 和 C++ 标准库中的文件操作函数。在使用这些函数时,需要注意对文件的打开和关闭操作,以及进行错误处理,以保证文件操作的正确性和稳定性。同时,为了防止数据截断或数据溢出,应该合理设置读取和写入缓冲区的大小,并确保缓冲区足够容纳读取或写入的数据。
以下是 fprintf
和 fscanf
函数的函数原型以及使用注意事项:
fprintf
函数:int fprintf(FILE* stream, const char* format, ...);
函数原型说明:
stream
:指向要写入数据的文件流。format
:格式化字符串,用于指定要写入文件的数据格式。...
:可变参数,根据 format
字符串中的格式化指示符,用于指定要写入文件的数据。使用注意事项:
fprintf
函数用于向文件中写入格式化的数据。format
字符串中的格式化指示符应与可变参数列表中的数据类型相匹配,否则可能导致输出错误或内存访问错误。stream
文件流指针有效,且文件已经打开,并具有写入权限。fprintf
返回成功写入的字符数,如果发生写入错误,返回负值。示例用法:
#include
int main() {
FILE* file = fopen("example.txt", "w");
if (file == NULL) {
printf("Failed to open the file.\n");
return 1;
}
int num1 = 10;
double num2 = 3.14;
char str[] = "Hello, World!";
// 向文件中写入格式化的数据
fprintf(file, "Integer: %d\n", num1);
fprintf(file, "Double: %lf\n", num2);
fprintf(file, "String: %s\n", str);
fclose(file);
return 0;
}
fscanf
函数:int fscanf(FILE* stream, const char* format, ...);
函数原型说明:
stream
:指向要读取数据的文件流。format
:格式化字符串,用于指定要读取的数据格式。...
:可变参数,根据 format
字符串中的格式化指示符,用于指定读取的数据存储位置。使用注意事项:
fscanf
函数用于从文件中读取格式化的数据。format
字符串中的格式化指示符应与可变参数列表中的数据类型相匹配,否则可能导致读取错误或内存访问错误。stream
文件流指针有效,且文件已经打开,并具有读取权限。fscanf
返回成功读取的项数,如果发生读取错误或到达文件末尾,返回 EOF。示例用法:
#include
int main() {
FILE* file = fopen("example.txt", "r");
if (file == NULL) {
printf("Failed to open the file.\n");
return 1;
}
int num1;
double num2;
char str[100];
// 从文件中读取格式化的数据
fscanf(file, "Integer: %d\n", &num1);
fscanf(file, "Double: %lf\n", &num2);
fscanf(file, "String: %s\n", str);
printf("Integer: %d\n", num1);
printf("Double: %lf\n", num2);
printf("String: %s\n", str);
fclose(file);
return 0;
}
在使用 fprintf
和 fscanf
函数时,要特别注意格式化字符串中的格式化指示符与可变参数列表的数据类型匹配,以及文件流指针的有效性和文件的权限。同时,在读取数据时,应该确保文件中的数据格式与读取的格式化字符串匹配,以避免读取错误。
使用 fflush
主要有两个作用:
printf
)向屏幕输出信息时,这些信息会先被写入到输出缓冲区,然后根据一定的条件(比如缓冲区满、换行符 \n
出现等)进行实际的输出。如果想要立即将缓冲区的内容输出到屏幕上,可以在适当的位置使用 fflush(stdout)
,这样可以实时查看输出结果,而不需要等到程序执行结束。scanf
)从用户获取输入时,输入数据会先被写入输入缓冲区,然后根据一定的条件(比如用户按下 Enter 键)进行实际的读取。如果输入缓冲区中还有数据未读取,而程序又需要等待用户输入时,可以使用 fflush(stdin)
来清空输入缓冲区,防止遗留的数据干扰后续的输入操作。注意事项:
fflush
函数的使用应该谨慎,一般来说,在标准输出和标准输入中使用 fflush
是合理的。但是在文件 I/O 操作中,fflush
可能会导致性能下降,因为频繁的刷新缓冲区会引起多次磁盘写入,降低文件操作效率。因此,在文件 I/O 操作中,通常不需要手动调用 fflush
,因为当文件关闭时,缓冲区会被自动刷新。如果确实需要强制刷新文件缓冲区,可以使用 fflush
函数。示例用法:
#include
int main() {
int num;
printf("Enter a number: ");
scanf("%d", &num);
// 清空输入缓冲区,防止遗留的换行符影响后续输入
fflush(stdin);
printf("You entered: %d\n", num);
printf("Flushing output buffer...\n");
// 刷新输出缓冲区,立即显示输出结果
fflush(stdout);
return 0;
}
在 C 语言中,除了常规的文件指针 FILE*
,还有一些特殊的文件指针用于特定的文件操作或位置控制。这些特殊文件指针包括:
stdin
:标准输入文件指针,通常用于从键盘读取输入。它是一个预定义的文件指针,指向标准输入流(stdin)。stdout
:标准输出文件指针,通常用于向屏幕输出信息。它是一个预定义的文件指针,指向标准输出流(stdout)。stderr
:标准错误文件指针,通常用于向屏幕输出错误信息。它是一个预定义的文件指针,指向标准错误流(stderr)。系统调用和库函数是两种不同的方式来访问操作系统功能或执行特定任务的方法。它们的区别在于如何与操作系统进行交互和完成任务。
系统调用的特点:
库函数的特点:
总结:
使用系统调用时,程序直接请求操作系统执行底层任务,需要切换特权级别和涉及较多的安全检查,开销较大,但能够访问更底层的功能。而使用库函数时,程序调用封装好的高级抽象接口,运行在用户空间,开销较小,但功能通常更有限,且不能直接访问底层硬件或特权级功能。
在实际编程中,通常建议优先使用库函数,除非需要特定的底层功能或操作系统接口,才考虑使用系统调用。库函数使得程序开发更简单,易于维护,并且通常具有良好的跨平台兼容性。
选择使用系统调用API或库函数取决于需求和应用场景。下面提供了一些指导原则来帮助做出正确的选择:
使用系统调用API的情况:
使用库函数的情况:
综合考虑以上因素,通常优先选择使用库函数,除非需要直接访问底层或特定的操作系统功能,或者对性能要求非常高,这时才考虑使用系统调用API。对于大部分应用程序,使用库函数可以提供更好的开发体验和更高的可移植性。
优化读写效率是提高程序性能的重要方面。下面列举了一些常见的优化方式来提高读写效率:
在优化读写效率时,需要根据具体的应用场景和系统环境进行针对性的优化。使用性能分析工具可以帮助发现瓶颈,并针对性地进行优化。同时,要注意在优化过程中不要牺牲代码的可维护性和可读性。
存储映射(Memory-mapped I/O)是一种将文件的内容直接映射到内存地址空间的技术。它允许将文件数据看作是内存中的一部分,从而实现对文件的读写操作,而无需使用传统的read和write函数。在许多操作系统中,这项技术由mmap函数提供支持。
mmap函数接口(在C语言中)通常如下:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
addr
: 指定映射的起始地址,通常设为0,让操作系统自动选择映射的地址。length
: 指定映射的长度,一般为文件的大小。prot
: 指定内存保护模式,可以是以下值的组合:
flags
: 指定映射选项,可以是以下值的组合:
fd
: 文件描述符,指定要映射的文件。offset
: 文件的偏移量,指定映射从文件的哪个位置开始。使用注意事项:
使用存储映射可以提高文件读写性能和简化代码逻辑,但在使用时需要谨慎,并遵循上述注意事项,以确保正确且安全地使用该功能。
常见的文件标志位是在打开文件时用来指定文件的打开模式和行为。在C语言中,文件标志位是通过在open()
函数或相关函数中指定的参数来设置的。不同的操作系统可能有略微不同的标志位,下面列举了一些常见的文件标志位以及它们的功能:
O_RDONLY
: 只读模式。打开文件以供读取,文件必须存在,不能修改。O_WRONLY
: 只写模式。打开文件以供写入,如果文件不存在,则创建一个新文件;如果文件已存在,则截断文件为零长度。O_RDWR
: 读写模式。打开文件以供读取和写入,如果文件不存在,则创建一个新文件。O_CREAT
: 如果文件不存在,则创建一个新文件。需要与O_WRONLY
或O_RDWR
标志位一起使用。O_TRUNC
: 如果文件已存在,在打开文件时截断文件为零长度。需要与O_WRONLY
或O_RDWR
标志位一起使用。O_APPEND
: 以追加模式打开文件,每次写入会将数据追加到文件末尾。O_EXCL
: 与O_CREAT
一起使用,用于确保创建新文件时,如果文件已存在,则返回错误。O_NONBLOCK
(或O_NDELAY
): 非阻塞模式。在读取或写入时,如果没有数据或无法立即写入,则不会阻塞进程,而是立即返回。O_SYNC
: 同步写入模式。每次写入都会直接传输到磁盘,保证数据持久化。O_DIRECTORY
: 用于打开目录而非文件。O_CLOEXEC
: 在进程执行exec
函数时,会关闭这个文件描述符。这些文件标志位可以根据需要进行组合,以满足不同的打开文件需求。在使用文件标志位时,需要根据具体场景和要求来选择适当的标志位组合,以实现正确的文件操作和行为。
阻塞I/O(Blocking I/O)和非阻塞I/O(Non-blocking I/O)是两种不同的I/O操作方式,它们的主要区别在于进程或线程在执行I/O操作时的行为和状态。
主要区别:
注意:
IO多路复用(IO Multiplexing)是一种高效的I/O操作模型,用于在单个线程或进程中同时监视多个I/O事件,并在有事件发生时进行响应。它的作用在于提高程序的并发性和响应性,使得一个线程或进程能够同时处理多个I/O操作而无需阻塞。
IO多路复用的实现方式主要有以下几种:
IO多路复用适用于需要同时监视多个文件描述符,并且希望在有事件发生时立即得到通知并进行相应处理的场景。常见的应用包括网络服务器,特别是在高并发环境下,使用IO多路复用能有效地提高服务器的性能和响应能力,避免多线程或多进程中可能出现的资源竞争和线程切换开销。
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
#include
#include
#include
int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);
IO多路复用的每种方式(select、poll、epoll、kqueue等)都有各自的优点和缺点。下面分别列出它们的特点:
总体而言,epoll和kqueue在性能和功能上都优于select和poll。如果在Linux系统上开发,可以优先考虑使用epoll,而在BSD或macOS系统上开发,可以优先考虑使用kqueue。如果需要跨平台兼容,可以根据具体情况选择select或poll,但需要注意它们在大规模并发环境下可能带来的性能问题。
#include
int fcntl(int fd, int cmd, ... /* arg */);
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
#include
int dup(int oldfd);
#include
int dup2(int oldfd, int newfd);
1、进程是操作系统最小的资源分配单位,相对程序来讲,程是一个独立运行的程序在计算机中的一次执行过程。进程是动态的,程序是静态的;
2、每个进程都拥有自己的地址空间、代码、数据、文件描述符等资源,并且相互之间是独立的、隔离的。一个进程消亡不会影响其他进程;所以进程实现多任务是安全的;但是多进程的开销比较大;
3、多进程每个进程都有三种状态:进程可以处于运行、就绪、阻塞等状态。运行状态表示进程正在CPU上执行指令,就绪状态表示进程已准备好执行,但还没有得到CPU执行时间,阻塞状态表示进程在等待某些事件发生。
4、进程调度的策略:根据进程的三态,通过时间片轮转(实时性差)来实现进程的调度。抢占式:(可以在一个时间片内再次获取CPU):高优先级优先、短进程优先、先来先服务,非抢占式:不可以在一个时间片内再次获取CPU。
5、每个进程都有自己的ID号,叫做进程的pid;
6、操作系统在系统启动时会自动创建出三个进程:
0:负责引导系统启动,也会创建一个1号进程init进程
1:负责初始化硬件,回收资源
2:负责资源的分配,系统的调度
fork
和vfork
是两个创建新进程的系统调用,在Unix-like系统中常见。它们的主要区别在于创建子进程的方式和父子进程之间的行为。
fork()
:pid_t fork(void);
fork
函数用于创建一个新的子进程,子进程是父进程的副本,从fork
调用之后的代码开始执行。子进程拥有与父进程几乎完全相同的地址空间和资源,包括代码、数据、堆栈等。fork
函数会被调用一次,但返回两次,一次在父进程中返回子进程的PID(进程ID),一次在子进程中返回0。因此,通过返回值可以在父进程和子进程中区分执行的代码。fork
创建子进程时,子进程继承了父进程的地址空间和资源。父子进程共享同一文件表项,但文件表项中的文件偏移量是各自独立的,因此它们在共享文件时需要注意文件指针的位置。vfork()
:pid_t vfork(void);
vfork
函数用于创建一个新的子进程,子进程是父进程的副本,从vfork
调用之后的代码开始执行。与fork
不同的是,vfork
保证在子进程中先执行,并且子进程共享父进程的地址空间,不会为子进程创建新的页表,这使得vfork
的性能通常比fork
更高。vfork
保证在子进程中先执行,而子进程与父进程共享地址空间,因此子进程在调用exec
系列函数或者退出后应该立即调用_exit
函数来避免在共享地址空间中造成意外的修改。vfork
通常用于创建临时子进程,它在父进程调用exec
函数前临时运行一些代码。总结:
fork
创建的子进程拥有与父进程几乎相同的地址空间和资源,适用于一般的进程复制需求。vfork
创建的子进程与父进程共享地址空间,适用于创建临时子进程并在子进程中执行一些代码的场景,通常与exec
函数结合使用。进程间通信(Inter-Process Communication,IPC)是多个进程之间进行数据交换和通信的机制。在多进程编程中,进程间通信是实现进程间协作的重要方式。常见的进程间通信方式包括:
int pipe(int pipefd[2]);
pipe
函数用于创建一个无名管道,它是一个单向通信通道,用于在有亲缘关系的进程之间进行通信。管道是半双工的,数据只能在一个方向上流动。int mkfifo(const char *pathname, mode_t mode);
mkfifo
函数用于创建一个命名管道,它可以在没有亲缘关系的进程之间进行通信。命名管道是一种特殊的文件类型,可以通过文件名进行访问。int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_init
函数用于初始化一个信号量,用于进程间同步和互斥。int msgget(key_t key, int msgflg);
msgget
函数用于创建一个消息队列或获取一个已经存在的消息队列。int shmget(key_t key, size_t size, int shmflg);
shmget
函数用于创建一个共享内存区域或获取一个已经存在的共享内存区域。int socket(int domain, int type, int protocol);
socket
函数用于创建一个套接字,用于在不同主机上的进程之间进行通信,支持网络通信。首先进程是资源分配的最小单位,线程是任务调度的最小单位;进程比线程更健壮,每个进程拥有独立的地址空间,一个进程的崩溃不会影响其他进程、而线程之间共享地址空间,一个线程的崩溃可能影响其他线程或者整个程序;由于线程的调度必须通过频繁加锁来保持同步,线程的性能比进程低;但线程之间切换比进程之间切换开销少,成本低;
进程:
线程:
综合应用场景:
在实际应用中,可能会根据实际需求综合考虑进程和线程的特点,选择合适的方式来实现多任务并发。
线程的同步是指在多个线程并发执行的情况下,通过一些机制来协调线程之间的执行顺序和共享资源的访问,以避免竞争条件和数据不一致等问题。常见的线程同步方式包括:
线程池的作用:
主要作用:避免创建过多的线程时引发的内存溢出问题,因为创建线程还是比较耗内存的。
为什么使用线程池?
何时使用线程池?
何时不适宜使用线程池?
线程池的实现步骤:
TCP服务器创建过程:
socket()
int socket(int domain, int type, int protocol);
domain
:地址族,通常为AF_INET(IPv4)。type
:套接字类型,通常为SOCK_STREAM(TCP流式套接字)。protocol
:协议,通常为0(由给定的domain和type自动选择合适的协议)。bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:套接字描述符。addr
:指向要绑定的地址结构的指针,通常是一个struct sockaddr_in
类型的指针。addrlen
:地址结构的长度。listen()
int listen(int sockfd, int backlog);
sockfd
:套接字描述符。backlog
:等待队列的最大长度,即允许等待连接的最大客户端数量。accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
:套接字描述符。addr
:用于存储客户端地址的结构体指针。addrlen
:指向存储addr结构体长度的变量的指针。send()
和 recv()
等函数发送和接收数据。close()
int close(int sockfd);
TCP客户端创建过程:
connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:套接字描述符。addr
:指向服务器地址结构的指针,通常是一个struct sockaddr_in
类型的指针。addrlen
:地址结构的长度。send()
和 recv()
等函数发送和接收数据。listen()
函数用于将一个套接字(socket)标记为被动(监听)套接字,以便它可以接受传入的连接请求。在TCP服务器程序中,listen()
函数的作用是开始监听指定的套接字,使其可以接受客户端的连接请求。
函数原型:
int listen(int sockfd, int backlog);
参数:
sockfd
:要监听的套接字描述符。backlog
:等待队列的最大长度,即允许等待连接的最大客户端数量。功能:
listen()
函数用于将指定的套接字转换为被动套接字,使其可以开始监听连接请求。accept()
函数来接受传入的连接请求。返回值:
需要注意的是,一旦调用 listen()
函数后,套接字将变为被动状态,只能用于接受连接请求,而不能直接用于数据的发送和接收。接下来,通常会使用 accept()
函数来接受客户端的连接请求,获取新的套接字用于与客户端通信。
将套接字(Socket)文件描述符转为被动描述符后才能监听连接,涉及到网络通信的基本原理和TCP协议的工作机制。
TCP协议是一种面向连接的协议,它在通信双方之间建立起一条可靠的、全双工的数据传输通道。在建立TCP连接时,通常有一个主动方(客户端)和一个被动方(服务器端)。这种主动方和被动方的区分在套接字的角色上有所体现:
将套接字文件描述符转为被动描述符后,主要是为了准备服务器端接受连接请求的环境。这涉及到以下原因:
总之,将套接字文件描述符转为被动描述符后,服务器端可以进入监听状态,等待客户端的连接请求,并在连接请求到来时准备好进行握手和建立连接,从而实现可靠的网络通信。
在网络编程中,无法绑定的错误通常指的是在使用 bind()
函数时出现了错误。bind()
函数用于将套接字(Socket)与一个特定的IP地址和端口号绑定,以便于在该地址和端口上监听连接请求。如果出现无法绑定的错误,可能是以下几种原因导致的:
关于 setsockopt()
函数,你提到了两个使用场景:
SO_REUSEADDR
选项,可以在端口释放后立即重用端口,避免等待 TIME_WAIT 的时间。SO_REUSEADDR
选项。accept()函数接受连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accept ()内部将产生用于数据I/O的套接字,并返回器文件描述符。需要强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。
从处于 ESTABLISHED状态下的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
在网络编程中,以及在处理文件和数据流的情况下,有两组常用的函数用于数据的读写和传输:recv
和 send
,以及 read
和 write
。这些函数在不同的上下文中用于不同的目的,但本质上都是用于在套接字(sockets)或文件中进行数据传输。
recv
和 send
:
recv
用于从套接字接收数据。它等待并接受数据,然后将数据存储到指定的缓冲区中。send
用于将数据从缓冲区发送到套接字。它负责将数据发送给连接的远程主机。read
和 write
:
read
用于从文件或数据流中读取数据。它会将数据读取到指定的缓冲区中。write
用于将数据从缓冲区写入文件或数据流。它将缓冲区中的数据写入到目标文件或流中。在网络编程中,创建和使用 UDP(User Datagram Protocol)套接字涉及一系列 API 调用。以下是使用典型的套接字 API(例如,C 语言的套接字库)来创建 UDP 套接字的一般步骤:
和
。socket()
函数来创建套接字。指定套接字的地址族(AF_INET 或 AF_INET6)和套接字类型(SOCK_DGRAM 用于 UDP 套接字)。int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Use any available network interface
server_addr.sin_port = htons(PORT); // Define the port number
bind()
函数将套接字与指定的地址和端口绑定。if (bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Binding failed");
exit(EXIT_FAILURE);
}
recvfrom()
函数接收数据,使用 sendto()
函数发送数据。这两个函数用于 UDP 数据的接收和发送。char buffer[MAX_BUFFER_SIZE];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t bytes_received = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &addr_len);
if (bytes_received == -1) {
perror("Receive failed");
exit(EXIT_FAILURE);
}
// Process and handle received data...
ssize_t bytes_sent = sendto(socket_fd, buffer, bytes_received, 0, (struct sockaddr *)&client_addr, addr_len);
if (bytes_sent == -1) {
perror("Send failed");
exit(EXIT_FAILURE);
}
close()
函数关闭套接字。close(socket_fd);
这些步骤展示了使用 C 语言的套接字库创建 UDP 套接字的基本过程。具体的细节可能因编程语言、操作系统和网络库而有所不同。不同编程语言和库可能会有相应的 API 调用,但总体的创建过程基本类似。
在网络编程中,创建和使用 TCP(Transmission Control Protocol)套接字涉及一系列 API 调用。以下是使用典型的套接字 API(例如,C 语言的套接字库)来创建 TCP 套接字的一般步骤:
和
。socket()
函数来创建套接字。指定套接字的地址族(AF_INET 或 AF_INET6)和套接字类型(SOCK_STREAM 用于 TCP 套接字)。int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Use any available network interface
server_addr.sin_port = htons(PORT); // Define the port number
bind()
函数将套接字与指定的地址和端口绑定。if (bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Binding failed");
exit(EXIT_FAILURE);
}
listen()
函数开始监听传入的连接请求。参数指定了允许排队的连接请求的最大数量。if (listen(socket_fd, BACKLOG) == -1) {
perror("Listening failed");
exit(EXIT_FAILURE);
}
accept()
函数接受传入的连接请求,创建一个新的套接字用于与客户端进行通信。struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_socket_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_socket_fd == -1) {
perror("Accepting failed");
exit(EXIT_FAILURE);
}
recv()
函数接收数据,使用 send()
函数发送数据。char buffer[MAX_BUFFER_SIZE];
ssize_t bytes_received = recv(client_socket_fd, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("Receive failed");
exit(EXIT_FAILURE);
}
// Process and handle received data...
ssize_t bytes_sent = send(client_socket_fd, buffer, bytes_received, 0);
if (bytes_sent == -1) {
perror("Send failed");
exit(EXIT_FAILURE);
}
close()
函数关闭套接字。close(client_socket_fd);
close(socket_fd);
服务器模型分为两种:循环服务器,并发服务器
循环服务器:循环服务器在同一个时刻只能相应一个客户端请求
并发服务器:并发服务器在同一个时刻可以相应多个客户端的请求
TCP服务器默认是循环服务器,因为TCP服务器有两个读阻塞函数accept和recv,这两个函数默认无法独立执行,所以无法实现并发
UDP默认是并发服务器,因为udp只有一个读阻塞函数recvfrom
如何实现TCP并发服务器?
方法1:使用多线程实现并发服务器
缺点:
线程的个数受限(硬件资源(内存),操作系统资源(pid号)) --解决方法:线程池
当客户端不与服务器交互时,此时资源消耗;(所有创建的读线程不断切换)–解决方法:IO多路复用
优点:
高性能(最优)
方法2:使用多进程实现并发服务器
缺点:
1、开销大(创建开销、通信开销、切换开销),
2、进程的个数受限(硬件资源(内存),操作系统资源(pid号) )–解决方法:进程池
3、当客户端不与服务器交互时,此时资源消耗;(所有创建的读进程不断切换)–解决方法:IO多路复用
优点:
高并发(最优)
close()
和 shutdown()
都是用于关闭套接字(socket)连接的函数,但它们在行为和用法上有一些区别。
1. close():
close()
用于关闭套接字连接。它会关闭套接字的读写功能,并释放与该套接字相关的资源。close()
后,不能再对该套接字进行任何读写操作。close()
并不会立即销毁套接字,而是将套接字的引用计数减一。只有当没有引用时,套接字才会被真正关闭和销毁。2. shutdown():
shutdown()
用于关闭套接字的一部分功能,即可以关闭读功能、写功能或同时关闭读写功能。SHUT_RD
(关闭读功能)、SHUT_WR
(关闭写功能)或 SHUT_RDWR
(同时关闭读写功能)。shutdown()
后,套接字的某些功能将被关闭,但套接字本身并不会被销毁。可以继续使用未关闭的功能,但已关闭的功能将无法使用。总结:
close()
函数。shutdown()
函数,并根据需要指定相应的参数。数据存储的方式:文件VS 数据库
文件缺点:1、无格式(操作复杂、查找性能低)2、安全性
数据库(DataBase,简记为DB)就是一个有结构的、集成的、可共亨的统管理的数据集合。它不仅包括数据本身,而且包括相关数据之间的联系。数据库技术主要研究如何存储、使用和管理数据;
数据库的特点:
可视化,数据保存有格式,便于查找,操作方便
不需要配置,不需要安装和管理
提供了简单和易于使用的API
可跨平台使用
有哪些数据库课作为嵌入式数据库?
目前有许多数据库产品,如Oracle、SQL Server、DB2、MySQL 、Access,SQLite3等产品各以自己特有的功能,在数据库市场上占有一席之地。
这些函数是 SQLite C/C++ 接口中常用的函数,用于在应用程序中操作 SQLite 数据库。以下是对这些函数的简要介绍:
filename
参数是数据库文件的名称。ppDb
参数是一个指向 SQLite 数据库指针的指针,用于接收数据库连接句柄。pDb
参数是要关闭的数据库连接句柄。db
参数是数据库连接句柄。sql
参数是要执行的 SQL 查询语句。callback
参数是一个回调函数,用于处理查询结果(可以为 NULL
,不需要处理结果时可省略)。data
参数是传递给回调函数的用户数据。errmsg
参数是一个指向错误消息的指针,用于接收执行中的错误信息。db
参数是数据库连接句柄。sql
参数是要执行的 SQL 查询语句。result
参数是一个指向结果表格的指针,将存储查询结果。nrow
参数用于接收表格的行数。ncolumn
参数用于接收表格的列数。errmsg
参数是一个指向错误消息的指针,用于接收执行中的错误信息。sqlite3_get_table()
函数分配的结果表格内存。result
参数是要释放的表格指针。这些函数是 SQLite C/C++ 接口中的一部分,用于方便地在应用程序中进行数据库操作。它们提供了打开数据库、执行查询、获取查询结果等功能。使用这些函数可以进行数据库的增、删、改、查等操作,使得应用程序能够与 SQLite 数据库进行交互。
互斥锁,顾名思义,当一个线程加互斥锁成功后,其他准备加锁的线程会加锁失败,实现同一时间内只有一个线程在占用CPU,避免多线程占用共享资源而出现错误,其他线程加锁失败后会有用户态转到内核态,内核会将线程转为睡眠状态,加锁成功的线程将锁释放掉之后内核会将其他处于睡眠状态的线程唤醒;
自旋锁,区别于互斥锁,只在用户态进行加锁和解锁的操作,不会进行进程间上下文切换,减少了进程间的开销,没有加锁的进程会处于忙等待状态,不会进入睡眠状态。