默认的 Ubuntu 软件源包含了一个软件包组,名称为 “build-essential”,它包含了 GNU 编辑器集合,GNU 调试器,和其他编译软件所必需的开发库和工具。
想要安装开发工具软件包,以 拥有 sudo 权限用户身份或者 root 身份运行下面的命令:
sudo apt update sudo apt install build-essential
这个命令将会安装一系列软件包,包括gcc
,g++
,和make
。
你可能还想安装关于如何使用 GNU/Linux开发的手册。
sudo apt-get install manpages-dev
通过运行下面的命令,打印 GCC 版本,来验证 GCC 编译器是否被成功地安装。
gcc --version
在 Ubuntu 20.04 软件源中 GCC 的默认可用版本号为9.3.0
:
gcc (Ubuntu 9.3.0-10ubuntu2) 9.3.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
就这些。GCC 已经在你的 Ubuntu 系统上安装好了,你可以开始使用它了。
使用 GCC 编译一个基本的 C 或者 C++ 程序非常简单。打开你的文本编辑器,并且创建下面的文件:
nano hello.c #includeint main() { printf ("Hello World!\n"); return 0; }
保存文件,并且将它编译成可执行文件,运行:
gcc hello.c -o hello
这将在你运行命令的同一个目录下创建一个二进制文件,名称为"hello”。
运行这个hell0
程序:
./hello
程序应该打印:
Hello World!
这一节提供一些指令,关于如何在 Ubuntu 20.04 上安装和使用多个版本的 GCC。更新的 GCC 编译器包含一些新函数以及优化改进。
在写作本文的时候,Ubuntu 源仓库包含几个 GCC 版本,从7.x.x
到10.x.x
。在写作的同时,最新的版本是10.1.0
。
在下面的例子中,我们将会安装最新的三个版本 GCC 和 G++:
输入下面的命令,安装想要的 GCC 和 G++ :
sudo apt install gcc-8 g++-8 gcc-9 g++-9 gcc-10 g++-10
下面的命令配置每一个版本,并且设置了优先级。默认的版本是拥有最高优先级的那个,在我们的场景中是gcc-10
。
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90 --slave /usr/bin/g++ g++ /usr/bin/g++-9 --slave /usr/bin/gcov gcov /usr/bin/gcov-9 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 80 --slave /usr/bin/g++ g++ /usr/bin/g++-8 --slave /usr/bin/gcov gcov /usr/bin/gcov-8
以后,如果你想修改默认的版本,使用update-alternatives
命令:
sudo update-alternatives --config gcc
输出:
There are 3 choices for the alternative gcc (providing /usr/bin/gcc). Selection Path Priority Status ------------------------------------------------------------ * 0 /usr/bin/gcc-10 100 auto mode 1 /usr/bin/gcc-10 100 manual mode 2 /usr/bin/gcc-8 80 manual mode 3 /usr/bin/gcc-9 90 manual mode Pressto keep the current choice[*], or type selection number:
你将会被展示一系列已经安装在你的 Ubuntu 系统上的 GCC 版本。输入你想设置为默认的 GCC 版本,并且按回车Enter
。
这个命令将会创建符号链接到指定版本的 GCC 和 G++。
gdb:
要使用GDB(GNU调试器)调试C程序,你需要按照以下步骤进行操作:
首先,确保你的系统上已经安装了GDB。GDB是GNU Binutils套件的一部分,你可以通过包管理器安装它。例如,在Ubuntu上,你可以使用以下命令安装GDB:
arduino复制代码 sudo apt-get install gdb
编译你的程序时,使用-g
选项启用调试信息。这将告诉编译器在生成的可执行文件中包含额外的调试信息,以便GDB使用。例如,你可以使用以下命令编译你的程序:
复制代码 gcc -g -o myprogram myprogram.c
运行GDB并加载你的程序。你可以使用以下命令启动GDB并加载你的程序:
复制代码 gdb myprogram
在GDB中,你可以使用各种命令来设置断点、运行代码、查看变量值等。以下是一些常用的GDB命令:
break
(或简写为b
):设置断点。例如,break main
将在main
函数处设置断点。
run
(或简写为r
):开始运行程序,直到遇到断点或程序结束。
step
(或简写为s
):逐行执行代码,进入函数调用。
next
(或简写为n
):逐行执行代码,但不进入函数调用。
continue
(或简写为c
):继续执行程序,直到遇到下一个断点或程序结束。
print
(或简写为p
):打印变量的值。例如,print x
将显示变量x
的值。
使用上述命令进行调试。你可以设置断点,使用step
、next
和continue
命令执行代码,并使用print
命令查看变量的值。
当你完成调试时,可以使用quit
命令退出GDB。
makefile make cmake
# Makefile main : main.c gcc main.c -o main
通配符:
# Makefile main : $(wildcard *.c) gcc $(wildcard *.c) -o main
变量:
# Makefile SRCS := $(wildcard *.c) main : $(SRCS) gcc $(SRCS) -o main
# Makefile INCS := -I./func SRCS := $(wildcard *.c) main : $(SRCS) gcc $(INCS) $(SRCS) -o main
在C语言中,变量名不是地址。变量名是用来标识内存地址的符号,它表示变量在计算机内存中的位置。当定义一个变量时,系统会为该变量分配一个内存地址,并且可以使用变量名来访问该变量的值。
在C语言中,指针是一种特殊的变量,它存储的是其他变量的内存地址,而不是值本身。通过指针,我们可以间接地访问和修改其指向的内存区域的值。
指针的声明和定义如下:
数据类型 *指针变量名;
其中,数据类型可以是任何有效的C语言数据类型,如int、char、float等。指针变量名是你为指针变量选择的名称。
下面是一个完整的例子,演示了如何声明、定义和使用指针变量:
#includeint main() { int num = 10; int *ptr; // 声明指针变量ptr ptr = # // 将num的地址赋值给ptr printf("num的值为:%d\n", num); printf("num的地址为:%p\n", &num); printf("ptr指向的值为:%d\n", *ptr); printf("ptr的地址为:%p\n", ptr); return 0; }
在上面的例子中,我们声明了一个整型变量num
并初始化为10。然后声明了一个指向整型的指针变量ptr
。通过将&num
赋值给ptr
,我们将num
的地址存储在了ptr
中。使用*ptr
可以访问ptr
所指向的内存区域的值,即num
的值。通过&num
可以得到num
的地址。程序输出了num
的值、num
的地址、ptr
指向的值以及ptr
的地址。
数据交互:
可以使用指针来交换两个变量的值,这是一个非常常见的使用指针的例子。以下是一个使用 C 语言实现的示例:
首先,定义一个交换函数,它接收两个整数的指针:
#includevoid swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; }
然后,你可以在主函数中这样使用这个函数:
int main() { int x = 5; int y = 10; printf("Before swap: x = %d, y = %d\n", x, y); swap(&x, &y); printf("After swap: x = %d, y = %d\n", x, y); return 0; }
在这个例子中,swap
函数通过接收两个指针来交换两个整数的值。当我们调用swap(&x, &y)
时,我们传递的是x
和y
的地址,所以函数能够直接影响到这两个变量的值。
ps:
在C语言中,函数的参数传递是值传递。这意味着当你传递一个变量到函数中时,函数会创建一个新的副本,而不是直接引用原始变量。因此,在函数内部对参数的任何修改都不会影响原始变量的值。
下面是一个简单的示例来说明这个概念:
#includevoid addOne(int num) { num = num + 1; printf("num inside the function: %d\n", num); } int main() { int num = 0; addOne(num); printf("num in main: %d\n", num); return 0; }
在这个例子中,addOne
函数接收一个整数参数num
,然后对它加一。然而,这种修改不会影响到main
函数中的num
变量。输出将是:
makefile复制代码 num inside the function: 1 num in main: 0
这表明尽管在函数内部num
的值被改变了,但这种改变并没有影响到函数外部的原始变量。这就是因为在C语言中,函数参数是通过值传递的。
如果你希望在函数中修改一个变量的值,并影响到原始变量,你需要使用指针。通过指针,你可以直接引用和修改内存中的原始值,而不是传递一个副本。
在C语言中,数组和指针之间有一个非常紧密的关系。数组的名称可以被看作是一个指向数组第一个元素的指针。同样,一个指向某个特定类型的指针也可以被看作是一个指向该类型的数组。这种关系可以在下面的示例代码中看到:
#includeint main() { // 定义一个包含5个整数的数组 int array[5] = {1, 2, 3, 4, 5}; // 定义一个指向整数的指针 int *ptr; // 将ptr指向array的第一个元素 ptr = array; // 使用指针访问数组元素 for(int i = 0; i < 5; i++) { printf("array[%d] = %d\n", i, *(ptr + i)); } // 修改数组中的元素值通过指针 *(ptr + 2) = 20; // 打印修改后的数组 printf("Modified array: "); for(int i = 0; i < 5; i++) { printf("%d ", array[i]); } printf("\n"); return 0; }
在这个例子中,我们首先定义了一个包含5个整数的数组array
,然后定义了一个指向整数的指针ptr
。我们将ptr
指向array
的第一个元素,然后使用一个循环通过指针访问数组的每个元素。我们也可以使用指针修改数组中的元素值,如示例中我们将第三个元素值修改为20。
includeint main(void) { int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} }; int (*pz)[2]; pz = zippo; printf(" pz = %p, pz + 1 = %p\n", pz, pz + 1); printf("pz[0] = %p, pz[0] + 1 = %p\n", pz[0], pz[0] + 1); printf(" *pz = %p, *pz + 1 = %p\n", *pz, *pz + 1); printf("pz[0][0] = %d\n", pz[0][0]); printf(" *pz[0] = %d\n", *pz[0]); printf(" **pz = %d\n", **pz); printf(" pz[2][1] = %d\n", pz[2][1]); printf("*(*(pz+2) + 1) = %d\n", *(*(pz+2) + 1)); return 0; }
/* zippo1.c -- zippo info */
include
int main(void)
{ int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} }; printf(" zippo = %p, zippo + 1 = %p\n", zippo, zippo + 1); printf("zippo[0] = %p, zippo[0] + 1 = %p\n", zippo[0], zippo[0] + 1); printf(" *zippo = %p, *zippo + 1 = %p\n", *zippo, *zippo + 1); printf("zippo[0][0] = %d\n", zippo[0][0]); printf(" *zippo[0] = %d\n", *zippo[0]); printf(" **zippo = %d\n", **zippo); printf(" zippo[2][1] = %d\n", zippo[2][1]); printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo+2) + 1)); return 0;
}
函数指针是指向函数的指针变量。它可以用来存储函数的地址,并在需要时调用该函数。下面是一个简单的例子,演示了如何定义和使用函数指针。
#include// 定义一个函数,用于计算两个整数的和 int add(int x, int y) { return x + y; } int main() { // 定义一个函数指针,指向add函数 int (*fp)(int, int) = add; // 定义一个整数数组 int arr[] = {1, 2, 3, 4, 5}; // 使用函数指针调用add函数,计算数组元素的和 int sum = 0; for (int i = 0; i < 5; i++) { sum = fp(sum, arr[i]); } // 输出计算结果 printf("Sum of array elements: %d\n", sum); return 0; }
在这个例子中,我们定义了一个函数add
,用于计算两个整数的和。然后,我们定义了一个函数指针fp
,指向add
函数。在main
函数中,我们使用fp
来调用add
函数,计算一个整数数组的元素之和,并输出结果。
当然,以下是函数指针在C语言中的三种常见应用场景的例子:
回调函数(Callback Functions)
在C语言中,函数指针最常见的用途是实现回调函数。以下是一个简单的例子:
#include// 定义一个函数指针类型 typedef void (*callback_t)(int); // 定义一个函数,这个函数接受一个整数和一个回调函数 void demo_function(int x, callback_t callback) { printf("The value of x is: %d\n", x); // 调用回调函数 callback(x); } // 定义一个回调函数,用于输出一个整数的平方 void square(int x) { printf("The square of %d is: %d\n", x, x * x); } int main() { // 调用demo_function函数,并传入回调函数square demo_function(5, square); return 0; }
函数参数传递(Function Parameters Passing)
函数指针也可以作为参数传递给其他函数,以实现更灵活的功能。以下是一个例子:
#include// 定义一个函数,这个函数接受一个整数和一个函数指针 void apply_func(int x, void (*func)(int)) { func(x); } // 定义一个函数,用于输出一个整数的平方 void square(int x) { printf("The square of %d is: %d\n", x, x * x); } int main() { // 调用apply_func函数,并传入函数square作为参数 apply_func(5, square); return 0; }
函数表(Function Tables)
函数指针还可以用于实现函数表,以便根据运行时的决策来调用不同的函数。以下是一个例子:
#include// 定义一个函数指针类型,用于指向处理函数的指针数组的函数指针类型 typedef void (*operation_t)(int); // 定义几个处理函数 void print_square(int x) { printf("%d\n", x * x); } void print_cube(int x) { printf("%d\n", x * x * x); } void print_quartic(int x) { printf("%d\n", x * x * x * x); } // 定义一个包含这三个函数的函数指针数组(即函数表) operation_t operations[] = { print_square, print_cube, print_quartic }; int main() { // 通过函数表调用不同的函数 for (int i = 0; i < 3; i++) { operations[i](i + 1); // 分别计算并打印1的平方、立方和四次方,2的平方、立方和四次方,以及3的平方、立方和四次方。 } return 0; }
...
(三个点),通常作为函数参数列表的最后一个参数。可变参数的应用场景是在函数需要处理可变数量或类型的参数时,例如函数需要接受任意数量的整数、字符串或其他数据类型,或者需要接受不同数量的参数进行不同的操作。
下面是一个简单的示例,演示了如何使用可变参数实现一个函数,该函数接受任意数量的整数并计算它们的和:
#include#include int sum(int count, ...) { va_list args; // 定义一个va_list类型的变量,用于存储可变参数的列表 int sum = 0; // 初始化一个sum变量用于计算总和 va_start(args, count); // 初始化args变量,将其指向第一个可变参数 // 遍历可变参数列表,计算它们的总和 for (int i = 0; i < count; i++) { int num = va_arg(args, int); // 依次获取每个整数参数的值 sum += num; } va_end(args); // 清理va_list变量 return sum; } int main() { int a = 1, b = 2, c = 3; printf("Sum: %d\n", sum(3, a, b, c)); // 输出:Sum: 6 return 0; }
在上面的示例中,sum
函数接受一个整数count
表示可变参数的数量,然后使用一个va_list
类型的变量args
来存储可变参数的列表。通过调用va_start
宏初始化args
变量,然后使用va_arg
宏依次获取每个整数参数的值,并计算它们的总和。最后,调用va_end
宏清理args
变量。
在C语言中,fopen
函数用于打开文件,并返回一个文件指针。如果打开文件成功,它会返回一个指向文件的指针,该指针随后可用于进行其他的输入和输出操作。如果打开文件失败,fopen
则会返回NULL。
fopen
函数的原型如下:
FILE *fopen(const char *path, const char *mode);
path
:这是一个字符串,表示要打开的文件的路径或文件名。
mode
:这也是一个字符串,表示打开文件的模式。下面是一些常见的模式:
r
:以只读方式打开文件。文件必须存在。
w
:以只写方式打开文件。如果文件存在,内容会被清空。如果文件不存在,会尝试创建一个新文件。
a
:以追加方式打开文件。如果文件存在,写操作将在文件的末尾进行。如果文件不存在,会尝试创建一个新文件。
r+
:以读/写方式打开文件。文件必须存在。
w+
:以读/写方式打开文件。如果文件存在,内容会被清空。如果文件不存在,会尝试创建一个新文件。
a+
:以读/追加方式打开文件。如果文件存在,写操作将在文件的末尾进行。如果文件不存在,会尝试创建一个新文件。
在C语言中,fread()函数用于从文件中读取数据。它是一个非常强大的工具,因为它可以读取任意类型的数据,无论是字符、整数、浮点数,还是自定义的数据结构。
fread()
fread()函数的原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明:
ptr
:指向用于存储数据的内存块的指针。
size
:要读取的每个元素的大小,以字节为单位。
nmemb
:要读取的元素的数量。
stream
:指向FILE对象的指针,该对象指定了一个输入流。
fread()函数会从stream
指向的文件中读取nmemb
个元素,每个元素的大小为size
字节,并将这些数据存储在由ptr
指向的内存块中。函数返回成功读取的元素数量。如果返回值小于nmemb
,则可能表示发生了错误或者到达了文件的末尾。
例如,以下代码将从文件中读取一个整数数组:
#includeint main() { FILE *file; int numbers[10]; size_t i, n; file = fopen("numbers.txt", "r"); if (file == NULL) { printf("Cannot open file\n"); return 1; } n = fread(numbers, sizeof(int), 10, file); for (i = 0; i < n; i++) { printf("%d ", numbers[i]); } printf("\n"); fclose(file); return 0; }
在这个例子中,我们打开名为"numbers.txt"的文件,并使用fread()函数从文件中读取10个整数。然后,我们遍历这些整数并打印出来。注意,我们使用了sizeof(int)作为fread()的第二个参数,这是因为我们要读取的是整数,所以我们需要知道每个整数在内存中占用的字节数。
线性表是一种常见的数据结构,它可以使用数组或者链表来实现。以下是使用数组来实现线性表的示例代码:
#include#include #define MAXSIZE 100 // 线性表的最大长度 typedef struct { int data[MAXSIZE]; // 存储数据元素的数组 int length; // 线性表的当前长度 } SqList; // 初始化线性表 void InitList(SqList *L) { L->length = 0; } // 插入元素 int ListInsert(SqList *L, int i, int e) { if (i < 1 || i > L->length + 1 || L->length >= MAXSIZE) { return 0; } for (int j = L->length; j >= i; j--) { L->data[j] = L->data[j - 1]; } L->data[i - 1] = e; L->length++; return 1; } // 删除元素 int ListDelete(SqList *L, int i) { if (i < 1 || i > L->length) { return 0; } for (int j = i; j < L->length; j++) { L->data[j - 1] = L->data[j]; } L->length--; return 1; } // 查找元素 int GetElem(SqList L, int i) { if (i < 1 || i > L.length) { return -1; } return L.data[i - 1]; } // 输出线性表中的元素 void PrintList(SqList L) { for (int i = 0; i < L.length; i++) { printf("%d ", L.data[i]); } printf("\n"); } int main() { SqList L; InitList(&L); // 初始化线性表 ListInsert(&L, 1, 10); // 在第1个位置插入元素10 ListInsert(&L, 2, 20); // 在第2个位置插入元素20 ListInsert(&L, 3, 30); // 在第3个位置插入元素30 PrintList(L); // 输出线性表中的元素 ListDelete(&L, 2); // 删除第2个位置的元素 PrintList(L); // 输出线性表中的元素 printf("%d\n", GetElem(L, 2)); // 查找第2个位置的元素并输出 return 0; }
以上代码实现了线性表的基本操作,包括初始化、插入、删除、查找和输出等。在使用时,可以根据具体的需求来调用这些函数。
**用链式存储实现线性表,并实现了插入、删除、查找和遍历操作。
以下是一个简单的C语言程序,它使用链式存储实现线性表,并实现了插入、删除、查找和遍历操作。
#include#include // 定义线性表结点的结构体 typedef struct node { int data; struct node *next; } Node; // 创建新结点 Node* create_node(int data) { Node *new_node = (Node*)malloc(sizeof(Node)); new_node->data = data; new_node->next = NULL; return new_node; } // 初始化线性表 void init_list(Node **head) { *head = NULL; } // 插入元素 void insert_element(Node **head, int data, int position) { Node *new_node = create_node(data); if (*head == NULL || position == 1) { new_node->next = *head; *head = new_node; } else { Node *current_node = *head; int i; for (i = 1; i < position - 1; i++) { if (current_node == NULL) { printf("线性表长度不足,不能插入元素!\n"); return; } current_node = current_node->next; } if (current_node == NULL) { printf("线性表长度不足,不能插入元素!\n"); return; } new_node->next = current_node->next; current_node->next = new_node; } } // 删除元素 void delete_element(Node **head, int position) { if (*head == NULL) { printf("线性表为空,不能删除元素!\n"); return; } if (position == 1) { Node *temp = *head; *head = (*head)->next; free(temp); } else { Node *current_node = *head; int i; for (i = 1; i < position - 1; i++) { if (current_node == NULL) { printf("线性表长度不足,不能删除元素!\n"); return; } current_node = current_node->next; } if (current_node == NULL || current_node->next == NULL) { printf("线性表长度不足,不能删除元素!\n"); return; } Node *temp = current_node->next; current_node->next = current_node->next->next; free(temp); } } // 查找元素 int find_element(Node *head, int data) { Node *current_node = head; int i = 1; while (current_node != NULL) { if (current_node->data == data) { return i; } current_node = current_node->next; i++; } return -1; // 没有找到元素返回-1 } // 遍历线性表 void traverse_list(Node *head) { Node *current_node = head; while (current_node != NULL) { printf("%d ", current_node->data); current_node = current_node->next; } printf("\n"); // 输出换行符以保持格式整齐 }`
这段代码是用C语言定义的一个链表节点。链表是一种常见的数据结构,由一系列的节点组成,每个节点包含两部分:数据和指向下一个节点的指针。
具体地,这段代码定义了一个名为'Node'的结构体,用于表示链表的节点。每个'Node'对象都有一个名为'data'的整型字段,用于存储数据,和一个名为'next'的指向下一个'Node'对象的指针。
这里是一个使用这种链表节点的简单例子:
#include#include // 定义链表节点 typedef struct node { int data; struct node *next; } Node; // 创建新节点 Node* createNode(int data) { Node* newNode = (Node*)malloc(sizeof(Node)); if(newNode == NULL) { printf("Error creating a new node.\n"); exit(0); } newNode->data = data; newNode->next = NULL; return newNode; } // 在链表末尾添加新节点 void appendNode(Node** head, int data) { Node* newNode = createNode(data); if(*head == NULL) { *head = newNode; return; } Node* lastNode = *head; while(lastNode->next != NULL) { lastNode = lastNode->next; } lastNode->next = newNode; } // 打印链表内容 void printList(Node* head) { while(head != NULL) { printf("%d ", head->data); head = head->next; } printf("\n"); } int main() { Node* head = NULL; // 初始化空链表 appendNode(&head, 1); // 添加节点1 appendNode(&head, 2); // 添加节点2 appendNode(&head, 3); // 添加节点3 printList(head); // 打印链表:1 2 3 return 0; }
这个例子中,我们首先定义了一个链表节点类型'Node',然后创建了一个名为'createNode'的函数,用于创建新的链表节点。我们还定义了一个名为'appendNode'的函数,用于在链表的末尾添加新的节点。最后,我们定义了一个名为'printList'的函数,用于打印链表的所有元素。在'main'函数中,我们创建了一个空链表,然后添加了三个节点,并打印了链表的所有元素。
#include#include #define MAX_SIZE 100 typedef struct { int data[MAX_SIZE]; int front; int rear; } Queue; void initialize(Queue* queue) { queue->front = -1; queue->rear = -1; } int isEmpty(Queue* queue) { return queue->front == -1; } int isFull(Queue* queue) { return (queue->rear + 1) % MAX_SIZE == queue->front; } void enqueue(Queue* queue, int item) { if (isFull(queue)) { printf("Queue is full. Cannot enqueue element.\n"); return; } if (isEmpty(queue)) { queue->front = 0; queue->rear = 0; } else { queue->rear = (queue->rear + 1) % MAX_SIZE; } queue->data[queue->rear] = item; } int dequeue(Queue* queue) { if (isEmpty(queue)) { printf("Queue is empty. Cannot dequeue element.\n"); return -1; } int item = queue->data[queue->front]; if (queue->front == queue->rear) { queue->front = -1; queue->rear = -1; } else { queue->front = (queue->front + 1) % MAX_SIZE; } return item; } int getFront(Queue* queue) { if (isEmpty(queue)) { printf("Queue is empty.\n"); return -1; } return queue->data[queue->front]; } int main() { // 创建并初始化队列 Queue queue; initialize(&queue); // 将元素入队列 enqueue(&queue, 10); enqueue(&queue, 20); enqueue(&queue, 30); // 获取并输出队首元素 printf("Front element: %d\n", getFront(&queue)); // 出队列并输出 while (!isEmpty(&queue)) { int item = dequeue(&queue); printf("Dequeued element: %d\n", item); } return 0; }
#include#include #define MAX_SIZE 100 typedef struct { int data[MAX_SIZE]; int top; } Stack; void initialize(Stack* stack) { stack->top = -1; } int isEmpty(Stack* stack) { return stack->top == -1; } int isFull(Stack* stack) { return stack->top == MAX_SIZE - 1; } void push(Stack* stack, int item) { if (isFull(stack)) { printf("Stack is full. Cannot push element.\n"); return; } stack->data[++stack->top] = item; } int pop(Stack* stack) { if (isEmpty(stack)) { printf("Stack is empty. Cannot pop element.\n"); return -1; } return stack->data[stack->top--]; } int getTop(Stack* stack) { if (isEmpty(stack)) { printf("Stack is empty.\n"); return -1; } return stack->data[stack->top]; } int main() { // 创建并初始化堆栈 Stack stack; initialize(&stack); // 将元素压入堆栈 push(&stack, 10); push(&stack, 20); push(&stack, 30); // 获取并输出堆栈顶部元素 printf("Top element: %d\n", getTop(&stack)); // 弹出堆栈顶部元素并输出 while (!isEmpty(&stack)) { int item = pop(&stack); printf("Popped element: %d\n", item); } return 0; }
#includeusing namespace std; #define MAXSIZE 100 #include typedef struct BiTnode{ string data; struct BiTnode *lchild,*rchild; }BiTnode,*BiTree; //创建二叉树 void creatbitree(BiTree &T){ char ch; cin>>ch; if(ch=='#') T=NULL; else{ T=new BiTnode; T->data=ch; creatbitree(T->lchild); creatbitree(T->rchild); } } //复制树 void Copy(BiTree T,BiTree &newT){ if(T==NULL){ newT=NULL; return; } else{ newT=new BiTnode; newT->data=T->data; Copy(T->lchild,newT->lchild); Copy(T->rchild,newT->rchild); } } //中序遍历树 void inordertraverse(BiTree T){ if(T){ inordertraverse(T->lchild); cout< data< rchild); } } //preortrave void pretrave(BiTree T){ if(T){ cout< data<<" "; pretrave(T->lchild); pretrave(T->rchild); } } //latrave void latrave(BiTree T){ if(T){ latrave(T->lchild); latrave(T->rchild); cout< data<<" "; } } //深度计算 前序遍历 int Depth(BiTree T){ int m,n; if(T==NULL)return 0; else{ m=Depth(T->lchild); n=Depth(T->rchild); return (m>n?m:n); } } //结点计算 int NodeCount(BiTree T){ if(T==NULL)return 0; else{ return NodeCount(T->lchild)+NodeCount(T->rchild)+1; } } int main(){ BiTree T; creatbitree(T); cout<<"计算深度: "< c++
alt+insert 快捷键文件操作
•实现与linux命令who相同功能的程序,提示在头文件utmp.h中有一个结构体struct utmp,保存了登入用户的信息,实现的程序至少显示用户名及所登入的终端
以下是一个用 C 语言实现与 Linux 命令
who
相同功能的程序示例,它使用了utmp.h
头文件中的struct utmp
结构体来获取登录用户的信息,并显示用户名及所登录的终端:#include#include int main() { struct utmp *ut; FILE *fp = fopen("/var/run/utmp", "r"); if (fp == NULL) { perror("Error opening utmp"); return 1; } while ((ut = getutent()) != NULL) { if (ut->ut_type == USER_PROCESS) { printf("%-8s %s\n", ut->ut_user, ut->ut_line); } } fclose(fp); return 0; } 该程序通过打开
/var/run/utmp
文件,并使用getutent()
函数循环读取utmp
结构体。当ut_type
字段的值为USER_PROCESS
(表示用户进程)时,打印出ut_user
字段(用户名)和ut_line
字段(终端名称)。请注意,在不同的系统上,
utmp
数据库的位置可能会有所不同。因此,你可能需要根据实际情况修改程序中的数据库路径。此外,还需要包含适当的头文件并链接
libutmp
库。在编译程序时,可以使用以下命令:Copy Codegcc program.c -o program -lutmplink
在C语言中,使用
link
和unlink
函数可以修改一个文件的名字。#includeint link(const char *oldpath, const char *newpath); int unlink(const char *pathname);
link
函数创建一个新的链接,将oldpath
文件名链接到newpath
文件名。它会使newpath
成为oldpath
的另一个别名。如果newpath
已经存在,则无法创建链接。
unlink
函数删除一个文件,即通过文件名pathname
删除文件的链接。如果该文件有多个链接,只有在最后一个链接被删除时,文件才会真正被删除。以下是一个例子,展示如何使用
link
和unlink
修改文件名:#include#include int main() { const char *oldpath = "oldfile.txt"; const char *newpath = "newfile.txt"; // 使用 link 函数创建链接 if (link(oldpath, newpath) == 0) { printf("Link created successfully.\n"); // 使用 unlink 函数删除旧链接 if (unlink(oldpath) == 0) { printf("Old link removed successfully.\n"); } else { perror("Error removing old link"); return 1; } } else { perror("Error creating link"); return 1; } return 0; } 在上述示例中,首先使用
link
函数创建一个新链接,然后使用unlink
函数删除旧链接。请注意,示例中的错误处理是简化的,并没有处理所有可能的错误情况。在实际使用中,需要根据返回值进行适当的错误处理。目录流
在C语言中,
telldir
函数用于获取目录流的当前位置(位置指针)。
telldir
函数的原型如下:long telldir(DIR *dirp);它接受一个
DIR
类型的指针作为参数,表示目录流。函数返回一个long
类型的值,代表目录流的当前位置。
telldir
函数常与其他目录操作函数一起使用,比如seekdir
和rewinddir
函数。这些函数用于在目录流中定位和重新设置位置指针。以下是一个简单的示例,演示了如何使用
telldir
函数获取目录流的当前位置:#include#include #include int main() { DIR *dir; struct dirent *entry; long position; dir = opendir("."); if (dir == NULL) { printf("无法打开目录\n"); return 1; } // 获取目录流的当前位置 position = telldir(dir); printf("当前位置:%ld\n", position); // 读取目录中的文件和子目录 printf("目录中的文件和子目录:\n"); while ((entry = readdir(dir)) != NULL) { printf("%s\n", entry->d_name); } // 再次获取目录流的当前位置 position = telldir(dir); printf("当前位置:%ld\n", position); closedir(dir); return 0; } 在上面的代码中,首先使用
opendir()
函数打开当前目录。然后使用telldir()
函数获取目录流的当前位置,并打印出来。接着使用readdir()
函数读取目录中的每个条目,并打印出文件名。最后再次调用telldir()
函数获取目录流的当前位置,也就是遍历完成后的位置。注意,每次调用
readdir()
函数都会自动更新位置指针,所以可以通过telldir()
函数在需要的时候获取位置信息。这就是
telldir
函数在C语言中的作用。希望能够帮助你理解它的用途。如果还有其他问题,请随时提问。以下是一个基于C语言的简单目录浏览器工具的示例代码,可在Linux下使用。该程序使用ncurses库实现了基本的用户界面和目录浏览功能。
#include#include #include #include #include #include #include #include // 定义最大路径长度 #define MAX_PATH_LEN 1024 // 定义最大文件名长度 #define MAX_FILENAME_LEN 256 // 定义最大文件列表长度 #define MAX_FILE_COUNT 100 // 存储文件列表的结构体 struct FileList { char filenames[MAX_FILE_COUNT][MAX_FILENAME_LEN]; int count; }; // 获取指定目录中的文件列表 void getFileList(const char *dirPath, struct FileList *fileList) { DIR *dir; struct dirent *entry; struct stat fileStat; char filePath[MAX_PATH_LEN]; dir = opendir(dirPath); if (dir == NULL) { return; } fileList->count = 0; while ((entry = readdir(dir)) != NULL && fileList->count < MAX_FILE_COUNT) { snprintf(filePath, sizeof(filePath), "%s/%s", dirPath, entry->d_name); if (stat(filePath, &fileStat) < 0) { continue; } strcpy(fileList->filenames[fileList->count], entry->d_name); fileList->count++; } closedir(dir); // 对文件列表按字母顺序进行排序 for (int i = 0; i < fileList->count - 1; i++) { for (int j = i + 1; j < fileList->count; j++) { if (strcmp(fileList->filenames[i], fileList->filenames[j]) > 0) { char temp[MAX_FILENAME_LEN]; strcpy(temp, fileList->filenames[i]); strcpy(fileList->filenames[i], fileList->filenames[j]); strcpy(fileList->filenames[j], temp); } } } } // 显示文件列表 void displayFileList(struct FileList *fileList, int selected) { clear(); for (int i = 0; i < fileList->count; i++) { if (i == selected) { attron(A_REVERSE); } printw("%s\n", fileList->filenames[i]); if (i == selected) { attroff(A_REVERSE); } } refresh(); } int main() { char currentPath[MAX_PATH_LEN] = "/"; struct FileList fileList; int selected = 0; initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); while (1) { getFileList(currentPath, &fileList); displayFileList(&fileList, selected); int ch = getch(); switch (ch) { case 'q': endwin(); return 0; case KEY_UP: if (selected > 0) { selected--; } break; case KEY_DOWN: if (selected < fileList.count - 1) { selected++; } break; case '\n': if (fileList.filenames[selected][0] == '.') { if (strcmp(fileList.filenames[selected], "..") == 0) { // 上一级目录 char *lastSlash = strrchr(currentPath, '/'); if (lastSlash != NULL) { *lastSlash = '\0'; } } } else { // 进入子目录 strcat(currentPath, "/"); strcat(currentPath, fileList.filenames[selected]); } selected = 0; break; } } } 这个程序使用了ncurses库来创建终端界面。它首先初始化ncurses环境,然后进入一个循环,在循环中获取当前目录下的文件列表,并在屏幕上显示出来。用户可以使用方向键来选择文件,按Enter键进入所选的目录,按q键退出程序。
注意,这只是一个简单的示例代码,可能还有一些改进的空间,例如添加错误处理、更复杂的用户界面等。你可以根据自己的需求和兴趣进行进一步扩展和定制。希望对你有所帮助!如果还有其他问题,请随时提问。
getcwd
是C语言中的一个函数,用于获取当前工作目录的路径。函数原型如下:
char *getcwd(char *buf, size_t size);参数:
buf
:指向一个字符数组的指针,用于存储当前工作目录的路径。可以为NULL,此时函数会自动分配内存来存储路径。
size
:buf
指向的字符数组的大小。返回值:
若成功,返回指向存储路径的指针(与
buf
相同),否则返回NULL。使用示例:
#include#include int main() { char cwd[1024]; if (getcwd(cwd, sizeof(cwd)) != NULL) { printf("当前工作目录:%s\n", cwd); } else { perror("getcwd() 错误"); return 1; } return 0; } 在上述示例中,
getcwd
函数被用来获取当前工作目录的路径,并将该路径打印出来。请注意,cwd
数组的大小应该足够大,以确保能够容纳路径字符串。如果路径超过了数组大小,可能会导致截断或缓冲区溢出错误。希望这个例子能够帮助你理解
getcwd
函数的用法。如有需要,请随时提问。fflush
fflush
是C语言中的一个函数,用于刷新流(文件缓冲区)。函数原型如下:
int fflush(FILE *stream);参数:
stream
:指向一个流的指针。返回值:
若成功,返回0;若失败,返回非零值。
fflush
函数的作用取决于传入的流参数:
如果流参数为
NULL
,那么fflush
会刷新所有打开的输出流。如果流参数是一个指向文件的指针,那么
fflush
会刷新该文件的输出缓冲区。如果流参数是一个指向输入流,
fflush
没有任何作用。常见的使用情况有:
在需要确保输出被写入文件的时候,可以使用
fflush
来刷新输出缓冲区,以确保数据被写入磁盘,而不仅仅停留在缓冲区中。在从输入流读取数据之前,可以使用
fflush
清空输入缓冲区,以防止之前的输入对后续操作产生干扰。以下是一个简单的示例程序,演示了
fflush
的用法:#includeint main() { FILE *file; char buffer[100]; file = fopen("example.txt", "w"); if (file == NULL) { printf("无法打开文件\n"); return 1; } fprintf(file, "Hello, World!"); fflush(file); // 刷新输出缓冲区,确保数据被写入文件 fclose(file); return 0; } 在上述示例中,程序打开一个名为"example.txt"的文件,向该文件写入字符串,然后使用
fflush
函数刷新输出缓冲区,以确保数据被写入文件。最后关闭文件。希望能解答你的问题。如有更多疑问,请随时提问。
fseek
fseek
是C语言中的一个函数,用于设置文件指针的位置。函数原型如下:
int fseek(FILE *stream, long offset, int whence);参数:
stream
:指向一个被操作的流的指针。
offset
:要移动的字节偏移量。 whence:设置起始点的位置,可以是以下值之一:
SEEK_SET
:从文件开头开始计算偏移量。
SEEK_CUR
:从当前文件位置计算偏移量。
SEEK_END
:从文件末尾开始计算偏移量。返回值:
若成功,返回0;若出错,返回非零值。
fseek
函数可用于在文件中进行随机访问,将文件指针定位到指定的位置。通过设置偏移量和起始点,可以移动文件指针到特定的位置。以下是一个示例程序,演示了
fseek
的用法:#includeint main() { FILE *file; char buffer[100]; file = fopen("example.txt", "r"); if (file == NULL) { printf("无法打开文件\n"); return 1; } fseek(file, 6, SEEK_SET); // 将文件指针移动到第7个字节处 fgets(buffer, sizeof(buffer), file); // 读取从第7个字节开始的内容 printf("内容:%s\n", buffer); fclose(file); return 0; } 在上述示例中,程序打开一个名为"example.txt"的文件,并使用
fseek
函数将文件指针移动到第7个字节处。然后使用fgets
函数从该位置开始读取文件的内容,并将其打印出来。最后关闭文件。希望能回答你的问题。如有更多疑问,请随时提问。
lseek
和
fseek
lseek
和fseek
是用于设置文件位置的函数,但在使用上有一些区别:
跨平台性:
lseek
是Linux系统下的系统调用,而fseek
是C标准库中的函数。因此,lseek
在Linux以及类Unix系统中可用,而fseek
在大多数C编译器和操作系统中都可以使用。参数类型:
lseek
的偏移量参数使用的是off_t
类型,而fseek
的偏移量参数使用的是long
类型。这意味着lseek
可以处理比long
更大范围的文件大小。操作对象:
lseek
通过文件描述符(file descriptor)来操作文件位置,而fseek
通过文件指针(file pointer)来操作文件位置。文件描述符是一个非负整数,用于唯一标识打开的文件,而文件指针是一个指向FILE
结构的指针,由C标准库提供。返回值:
lseek
的返回值是新的文件偏移量,表示成功移动后的位置。而fseek
的返回值通常用于检测是否发生了错误,成功移动后的位置需要通过其他方式获取。综上所述,
lseek
主要用于底层的文件操作,适合在Linux系统下进行文件定位;而fseek
是C标准库提供的函数,适合在跨平台的C程序中进行文件定位。希望解答了你的问题,如果还有其他疑问,请随时提出。
fgetc
、getc
和getchar
fgetc
、getc
和getchar
都是C语言中用于从文件或标准输入流(stdin)中读取一个字符的函数,它们之间的区别如下:
参数:
fgetc
函数需要一个参数,即指定要读取的文件指针。
getc
函数也需要一个参数,即指定要读取的文件指针。
getchar
函数没有参数,它直接从标准输入流(stdin)中读取字符。返回值:
fgetc
和getc
函数在成功读取一个字符后,返回对应的字符值(作为unsigned char
类型);如果到达文件末尾或出错,返回特殊值EOF
(表示End-of-File)。
getchar
函数与getc
函数类似,但它的返回值类型是int
,可以存储读取的字符值或特殊值EOF
。使用场景:
fgetc
函数主要用于从指定的文件中逐个读取字符。
getc
函数可从任何已打开的文件中读取字符,也可以从标准输入流中读取字符。
getchar
函数主要用于从标准输入流中读取字符。宏定义:
getc
函数通常是一个宏定义,其实现类似于fgetc
函数。
getchar
函数通常也是一个宏定义,其实现类似于getc(stdin)
。示例代码:
#includeint main() { int ch; FILE *file = fopen("example.txt", "r"); if (file == NULL) { printf("无法打开文件\n"); return 1; } // 使用fgetc读取文件中的字符 while ((ch = fgetc(file)) != EOF) { printf("%c ", ch); } printf("\n"); fclose(file); // 使用getchar从标准输入中读取字符 printf("请输入一行文字:\n"); while ((ch = getchar()) != '\n') { printf("%c ", ch); } printf("\n"); return 0; } 在上述示例中,首先使用
fopen
函数打开一个文件,然后使用fgetc
函数逐个读取文件中的字符。接下来,使用getchar
函数从标准输入中读取一行文字,直到遇到换行符\n
为止。希望能够解答你的问题,如果还有其他疑问,请随时提出。
fgets
`读取一行字符。
fgets
和
gets`是C语言中用于从标准输入流(stdin)或文件中读取一行字符串的函数,它们之间的区别如下:
参数:
fgets
函数需要三个参数,分别是用于存储读取的字符串的字符数组、最大读取长度和要读取的文件指针。
gets
函数只需要一个参数,即用于存储读取的字符串的字符数组。安全性:
fgets
函数相对较安全,因为它可以指定最大读取长度,防止字符串溢出。它会读取最多指定长度减1的字符,并在最后添加一个空字符\0
作为字符串的结束符。
gets
函数不安全,如果读取的字符数超过了目标数组的长度,则会导致缓冲区溢出,可能引发程序的崩溃或安全漏洞。处理换行符:
fgets
函数会将读取的字符串中包含的换行符(\n)也保存在目标数组中。
gets
函数会将读取的字符串中的换行符替换为空字符(\0),不会保存在目标数组中。返回值:
fgets
函数在成功读取一行字符串后,返回指向目标数组的指针;如果到达文件末尾或发生错误,返回空指针。
gets
函数没有返回值。示例代码:
#includeint main() { char str1[10]; char str2[10]; printf("请输入字符串(fgets):"); fgets(str1, sizeof(str1), stdin); printf("读取的字符串为:%s", str1); printf("请输入字符串(gets):"); gets(str2); printf("读取的字符串为:%s", str2); return 0; } 在上述示例中,我们通过
fgets
和gets
函数分别从标准输入中读取一行字符串。使用fgets
时,需要指定最大读取长度为目标数组的大小。而在使用gets
时,仅需提供目标数组即可。需要注意的是,由于
gets
函数存在安全性问题,自C11标准起已被废弃,不建议使用。应该优先使用更安全的fgets
函数。
printf
、fprintf
和sprintf
printf
、fprintf
和sprintf
是C语言中用于输出格式化字符串的函数,它们之间的区别如下:
输出位置:
printf
函数将格式化字符串输出到标准输出流(stdout),即控制台。
fprintf
函数将格式化字符串输出到指定的文件流中,即可以输出到文件。
sprintf
函数将格式化字符串输出到一个字符数组中,可以将格式化结果存储在字符串变量中。参数:
printf
函数的第一个参数是格式化字符串,后续参数为格式化字符串中的占位符所对应的实际值。
fprintf
函数的第一个参数是要输出的文件指针,第二个参数是格式化字符串,后续参数同样是占位符对应的实际值。
sprintf
函数的第一个参数是一个字符数组,用于存储格式化结果,后续参数同样是格式化字符串中的占位符对应的实际值。返回值:
printf
函数和fprintf
函数没有返回值。
sprintf
函数返回一个整数,表示写入字符数组的字符数(不包括末尾的空字符\0
),如果发生错误,则返回负值。使用场景:
printf
函数主要用于打印输出到控制台。
fprintf
函数主要用于将输出结果写入文件。
sprintf
函数主要用于将格式化结果保存到字符串变量中,以便后续使用。示例代码:
#includeint main() { int num = 123; char str[20]; // 使用printf将格式化字符串输出到控制台 printf("数字:%d\n", num); FILE *file = fopen("example.txt", "w"); if (file == NULL) { printf("无法打开文件\n"); return 1; } // 使用fprintf将格式化字符串输出到文件 fprintf(file, "数字:%d\n", num); fclose(file); // 使用sprintf将格式化结果保存到字符串变量中 sprintf(str, "数字:%d\n", num); printf("字符串:%s", str); return 0; } 在上述示例中,我们演示了使用
printf
、fprintf
和sprintf
函数的示例。通过printf
函数,我们将格式化字符串直接输出到控制台;通过fprintf
函数,我们将格式化字符串输出到文件;通过sprintf
函数,我们将格式化结果存储在一个字符数组中,并通过printf
函数再次输出。setvbuf
setvbuf
函数用于设置文件流的缓冲类型和大小。它可以管理文件IO的缓冲机制,使得IO操作更高效。函数原型如下:
int setvbuf(FILE *stream, char *buffer, int mode, size_t size);参数解释:
stream
:要设置缓冲区的文件流指针。
buffer
:自定义的缓冲区,如果为NULL,则由setvbuf
函数自动分配缓冲区。 mode:设置缓冲类型的模式,可以取以下三个值:
_IONBF
:无缓冲,不使用缓冲区。
_IOLBF
:行缓冲,每次输出操作都会刷新缓冲区,并且在遇到换行符时也会刷新缓冲区。
_IOFBF
:全缓冲,当缓冲区满或遇到刷新操作时才会刷新缓冲区。
size
:缓冲区的大小,当buffer
为NULL时指定实际分配的缓冲区大小。
setvbuf
函数的作用包括:
设置输入/输出流的缓冲机制:可以选择无缓冲、行缓冲或全缓冲模式。
提高IO性能:使用适当的缓冲机制可以减少频繁的系统调用,从而提高IO的效率。
自定义缓冲区:可以提供自己的缓冲区,避免
setvbuf
函数内部自动分配缓冲区。自定义缓冲区可以是数组或者动态分配的内存。示例代码:
#includeint main() { FILE *file = fopen("example.txt", "w"); if (file == NULL) { printf("无法打开文件\n"); return 1; } char buffer[1024]; // 设置文件流为全缓冲模式,并指定自定义缓冲区 setvbuf(file, buffer, _IOFBF, sizeof(buffer)); fprintf(file, "这是一条测试数据\n"); fclose(file); return 0; } 在上述示例中,我们使用
setvbuf
函数将文件流设置为全缓冲模式,并指定自定义缓冲区为buffer
数组。这样,每次写入数据时,会先将数据写入缓冲区,直到缓冲区满或遇到刷新操作才会将数据真正写入文件。这样可以减少系统调用的次数,提高IO性能。希望能够解答你的问题,如果还有其他疑问,请随时提出。
信号signal
是的,
signal
函数的作用是注册信号处理函数。它用于设置指定信号的处理方式,告诉操作系统在接收到该信号时应该执行哪个函数。
signal
函数原型如下:void (*signal(int sig, void (*func)(int)))(int);其中,
sig
参数表示要注册的信号编号,func
参数是一个指向信号处理函数的指针,它接受一个整型参数(表示信号编号)并返回空类型(void
)。例如,可以使用以下代码将信号SIGINT(中断信号)的处理方式设置为一个名为
handler
的自定义信号处理函数:#include#include void handler(int sig) { printf("Received SIGINT\n"); //signal(SIGINT, SIG_DFL); } int main() { signal(SIGINT, handler); // 无限循环 while (1) { // 程序主逻辑 } return 0; } 这样,在接收到SIGINT信号(例如按下Ctrl+C)时,将会调用
handler
函数来处理该信号。
sigaction
sigaction
函数用于注册和处理信号,并提供了更多的控制选项和信息。下面是对sigaction
函数的说明以及一个使用例子:Code#includeint sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:要注册或处理的信号编号。
act
:指向struct sigaction
结构体的指针,用于设置信号处理函数和其他选项。
oldact
:指向struct sigaction
结构体的指针,在调用函数后用于获取之前的信号处理函数和选项。
struct sigaction
结构体包含以下字段:Codestruct sigaction { void (*sa_handler)(int); // 信号处理函数指针 void (*sa_sigaction)(int, siginfo_t *, void *); // 带有附加信息的信号处理函数指针 sigset_t sa_mask; // 用于屏蔽其他信号的信号集合 int sa_flags; // 一些标志,如 SA_RESTART、SA_NOCLDSTOP 等 void (*sa_restorer)(void); // 恢复原始处理函数的函数指针(已弃用) };下面是一个使用
sigaction
函数注册和处理SIGINT
信号的例子:#include#include void handleSignal(int signum) { printf("Received signal: %d\n", signum); // 其他操作或清理代码 // ... } int main() { struct sigaction sa; sa.sa_handler = handleSignal; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); return 1; } // 正常的程序逻辑 while (1) { // ... } return 0; } 在上述例子中,首先定义了一个名为
handleSignal
的信号处理函数,用于处理接收到的SIGINT
信号。在这个简单的例子中,它只是打印出接收到的信号编号。在
main
函数中,创建了一个struct sigaction
结构体,并将sa_handler
字段指定为handleSignal
函数。然后,通过sigemptyset
函数清空了sa_mask
字段,即不屏蔽任何其他信号。最后,设置sa_flags
字段为 0,表示默认行为。最后,使用
sigaction
函数将SIGINT
信号的处理函数注册为handleSignal
函数。如果注册失败,会输出错误信息。接下来,程序会进入一个无限循环,可以执行正常的程序逻辑。当接收到
SIGINT
信号时,会调用注册的handleSignal
函数进行处理。需要注意的是,此处仅示范了如何注册和处理
SIGINT
信号,你可以根据需要注册和处理其他信号。还可以利用struct sigaction
结构体的其他字段来设置更多的选项和功能,如设置屏蔽信号集合、使用带有附加信息的信号处理函数等。详细的功能和选项,请参考相关文档或手册。sigprocmask
sigprocmask
函数用于修改当前进程的信号屏蔽字,即设置要阻塞的信号集合。下面是sigprocmask
函数的说明:#includeint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:指定了如何修改信号屏蔽字的方式,有三个可能的取值:
SIG_BLOCK
:将set
指定的信号集合添加到当前的信号屏蔽字中。
SIG_UNBLOCK
:从当前的信号屏蔽字中移除set
指定的信号集合。
SIG_SETMASK
:将当前的信号屏蔽字设置为set
指定的信号集合。
set
:指向sigset_t
类型的指针,表示要修改的信号集合。
oldset
:如果非空,则在函数调用后用于获取之前的信号屏蔽字。下面是一个使用
sigprocmask
函数的例子:#include#include void handleSignal(int signum) { printf("Received signal: %d\n", signum); } int main() { sigset_t mask, oldmask; // 清空信号集合 sigemptyset(&mask); // 添加要阻塞的信号 sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); // 设置信号屏蔽字 if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) { perror("sigprocmask"); return 1; } // 注册信号处理函数 signal(SIGINT, handleSignal); signal(SIGTERM, handleSignal); // 正常的程序逻辑 while (1) { // ... } return 0; } 在上述例子中,首先创建了一个
sigset_t
类型的变量mask
,并使用sigemptyset
函数将其初始化为空的信号集合。然后使用sigaddset
函数将SIGINT
和SIGTERM
信号添加到信号集合中。接下来,使用
sigprocmask
函数将mask
指定的信号集合添加到当前进程的信号屏蔽字中。这样,当SIGINT
或SIGTERM
信号被触发时,它们将被阻塞。然后,使用
signal
函数注册了两个信号处理函数,分别用于处理SIGINT
和SIGTERM
信号。这些信号处理函数在收到对应的信号时会打印相应的信息。最后,程序进入一个无限循环,执行正常的程序逻辑。当收到
SIGINT
或SIGTERM
信号时,对应的信号处理函数会被调用。需要注意的是,在实际的使用中,需要根据具体需求选择要阻塞或解除阻塞的信号,并合理地设置信号屏蔽字,以保证程序的正确性和可靠性。
什么是进程?
cat test.c | wc -l
•为了合理的管理各种各样的进程,操作系统就会在内核空间的某个位置存放进程的属性信息,这就是PCB。
•PCB除了保存PID外还保持进程状态等其他信息。
grep
grep
是一个在 Linux 或类 Unix 系统上使用的强大的文本搜索工具。
grep
命令的基本语法如下:grep [选项] 搜索模式 [文件名]其中,
搜索模式
是要搜索的文本模式或正则表达式。文件名
是要在其中进行搜索的文件名。
grep
命令会在指定的文件中搜索包含匹配搜索模式的行,并将其显示在终端上。它可以用于查找特定单词、字符串或符合特定模式的行。一些常见的
grep
选项包括:
-i
:忽略大小写进行匹配。
-v
:只显示不匹配搜索模式的行。
-r
:递归地在目录及其子目录中搜索。
-l
:只显示包含匹配搜索模式的文件名,而不显示具体匹配行。
-n
:显示匹配行及其行号。以下是一些示例用法:
在文件中搜索特定单词:
shellCopy Codegrep "keyword" file.txt
忽略大小写,递归地在目录及其子目录中搜索特定字符串:
shellCopy Codegrep -i -r "pattern" directory/
显示包含匹配模式的文件名:
shellCopy Codegrep -l "pattern" file1.txt file2.txt
grep
命令非常灵活,并且可以与其他命令组合使用,以实现更复杂的搜索和处理操作。ps:在 C 语言中,
_t
是一种命名约定,表示该类型是一个特定的类型。它用于标识命名空间中的类型,有时也表示该类型是一个抽象数据类型(Abstract Data Type, ADT)。在这种情况下,
pid_t
表示一个进程标识符(Process IDentifier),即进程的唯一标识符。pid_t
是一个整数类型的别名,通常被定义为typedef
如下:typedef int pid_t;使用
pid_t
类型时,可以存储和处理进程的标识符,例如通过系统调用获取当前进程的 ID 或获取其他进程的 ID。注意,
_t
并不是 C 语言的关键字,而是一种命名约定,用于表示特定的类型。在标准库中,许多类型都遵循这种命名约定,以便更好地区分它们。父子进程:
#include#include #include int main() { pid_t pid; // 创建子进程 pid = fork(); if (pid < 0) { // 创建子进程失败 fprintf(stderr, "Fork failed.\n"); return 1; } else if (pid == 0) { // 子进程 printf("Hello from child process!\n"); } else { // 父进程 printf("Hello from parent process!\n"); } return 0; } vfork 慎用:
includeinclude int main() { pid_t pid; // 创建子进程 pid = vfork(); if (pid < 0) { // 创建子进程失败 fprintf(stderr, "vfork failed.\n"); return 1; } else if (pid == 0) { // 子进程 printf("Hello from child process!\n"); // 在子进程中执行其他操作 // ... _exit(0); // 子进程退出 } else { // 父进程 printf("Hello from parent process!\n"); // 在父进程中执行其他操作 // ... // 父进程等待子进程结束 // ... } return 0; } wait:
在 Linux 中,wait() 是一个系统调用函数,用于父进程等待其子进程结束并获取子进程的退出状态。它的原型如下:
pid_t wait(int *status);wait() 函数的作用是暂停当前进程的执行,直到一个子进程结束。它会阻塞当前进程,直到有一个子进程退出或被信号中断。
wait() 函数具体的使用方法如下:
在父进程中调用 wait(NULL),父进程会一直阻塞,直到任意一个子进程退出。wait() 函数返回退出子进程的进程ID(PID),如果不关心子进程的退出状态,可以将 status 参数设为 NULL。可以使用 WIFEXITED(status) 宏来判断子进程是否正常退出。如果子进程正常退出,WIFEXITED(status) 返回真。如果子进程正常退出,可以使用 WEXITSTATUS(status) 宏获取子进程的退出状态。以下是一个简单的示例代码:
#include#include #include #include #include int main() { pid_t pid; int status; pid = fork(); if (pid < 0) { fprintf(stderr, "fork failed.\n"); return 1; } else if (pid == 0) { // 子进程 printf("Hello from child process!\n"); _exit(42); // 子进程退出,并返回退出状态为 42 } else { // 父进程 printf("Waiting for child process to exit...\n"); wait(&status); if (WIFEXITED(status)) { printf("Child process exited with status: %d\n", WEXITSTATUS(status)); } } return 0; } 在上面的示例中,父进程调用 wait() 来等待子进程退出,并使用 WEXITSTATUS() 宏来获取子进程的退出状态。在子进程中,我们使用 _exit() 来退出,并指定退出状态为 42。
需要注意的是,wait() 函数只能等待第一个终止的子进程,如果希望等待指定的子进程,可以通过指定子进程的 PID 来调用 waitpid() 函数。此外,还可以使用 WIFSIGNALED() 宏来判断子进程是否是被信号终止的。详细的用法和相关宏的说明可以参考相关的系统文档或手册页。
WEXITSTATUS(status)
WEXITSTATUS(status) 是一个宏,用于从 wait() 系统调用返回的状态值中提取子进程的退出状态。
在调用 wait() 函数后,通过 status 参数可以获取子进程的退出状态。这个状态值是一个16位整数,其中包含了子进程退出时传递给 _exit() 或 exit() 函数的参数。
WEXITSTATUS(status) 宏的作用是提取子进程的退出状态,并将其从系统返回的状态值中解压缩出来。它会对 status 进行位操作,提取出低 8 位(即一个字节)的状态值。
例如,如果子进程使用 _exit(42) 或 exit(42) 退出,并通过 wait() 在父进程中获取到状态值 status,则可以使用 WEXITSTATUS(status) 宏来获取退出状态为 42。
下面是一个示例代码:
includeinclude include include include int main() { pid_t pid; int status; pid = fork(); if (pid < 0) { fprintf(stderr, "fork failed.\n"); return 1; } else if (pid == 0) { // 子进程 printf("Hello from child process!\n"); _exit(42); // 子进程退出,并返回退出状态为 42 } else { // 父进程 printf("Waiting for child process to exit...\n"); wait(&status); if (WIFEXITED(status)) { printf("Child process exited with status: %d\n", WEXITSTATUS(status)); } } return 0; } 在上面的示例中,父进程使用 WEXITSTATUS(status) 宏来提取子进程的退出状态,并将其打印输出。结果应该显示 "Child process exited with status: 42"。
exec()
和
fork()应用场景
exec()
和fork()
是 Linux 中常用的进程编程函数,它们在不同的场景下有不同的应用。
fork()
函数用于创建一个新的子进程,该子进程是父进程的副本,包括代码、数据和打开的文件等。子进程从fork()
调用的点开始执行,并且拥有自己的进程 ID。fork()
的返回值有以下几种情况:
在父进程中,
fork()
返回新创建的子进程的进程 ID。在子进程中,
fork()
返回 0。如果
fork()
失败,返回一个负数。常见的
fork()
的应用场景包括:
并发服务器:通过
fork()
创建多个子进程,每个子进程负责处理一个客户端请求,实现并发处理。进程池:使用
fork()
创建一组子进程,放入进程池中,当需要处理任务时,直接从进程池中获取一个空闲进程。父子进程通信:通过
fork()
创建子进程后,可以使用进程间通信机制(如管道、共享内存、消息队列等)进行通信。守护进程:使用
fork()
创建子进程,并使子进程脱离控制终端,成为一个后台运行的守护进程。而
exec()
函数则用于在当前进程中执行一个新的程序,它会将当前进程的地址空间替换为新程序的地址空间,并开始执行新程序的代码。exec()
函数有多个变体,如execvp()
、execlp()
等,用于执行不同类型的程序。
exec()
的常见应用场景包括:
启动其他程序:通过
exec()
执行其他程序,例如启动 shell 命令、运行脚本等。进程替换:在子进程中使用
exec()
来替换当前进程的映像,实现程序的动态加载和更新。程序过滤器:将当前进程作为过滤器,通过
exec()
调用其他程序来处理输入数据。总结起来,
fork()
用于创建子进程,而exec()
用于执行新的程序。fork()
创建的子进程是父进程的副本,可以在子进程中使用exec()
来执行其他程序。这样,fork()
和exec()
结合使用,可以实现进程间的并发处理、进程池、进程通信以及动态加载新程序等功能。exec()
在 Linux 中,exec() 是一个系统调用,用于在当前进程中执行一个新的程序。它会将当前进程的地址空间替换为新程序的地址空间,并开始执行新程序的代码。exec() 函数一般与 fork() 函数一起使用,通过创建子进程并在子进程中调用 exec() 来执行新程序。下面是一个使用 exec() 的简单示例代码:
includeinclude int main() { pid_t pid; pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程执行新程序 char *args[] = {"ls", "-l", NULL}; execvp("ls", args); // 如果 execvp 执行成功,以下代码将不会执行 perror("execvp"); return 1; } else { // 父进程等待子进程执行完毕 wait(NULL); printf("Child process finished.\n"); } return 0; }
execl()
、execlp()
、execle()
、execv()
和execvp()
是exec()
函数族的成员,它们用于在当前进程中执行一个新的程序。这些函数之间的主要区别在于参数传递方式和搜索可执行文件的路径。
execl()
只能接受可变参数列表,需要在函数调用时指定每个参数。
需要明确指定待执行程序的完整路径。
适用于参数数量固定的情况。
execlp()
只能接受可变参数列表,需要在函数调用时指定每个参数。
不需要明确指定待执行程序的完整路径,会在系统 PATH 环境变量定义的路径中搜索可执行文件。
适用于参数数量固定的情况。
execle()
需要明确指定待执行程序的完整路径。
可以接受可变参数列表和环境变量列表。
适用于需要自定义环境变量的情况。
execv()
可以接受一个参数数组来传递命令行参数。
需要明确指定待执行程序的完整路径。
适用于参数数量不固定的情况。
execvp()
可以接受一个参数数组来传递命令行参数。
不需要明确指定待执行程序的完整路径,会在系统 PATH 环境变量定义的路径中搜索可执行文件。
适用于参数数量不固定的情况。
一般来说,如果命令行参数数量是固定的,可以使用
execl()
或execlp()
;如果需要自定义环境变量,可以使用execle()
;如果命令行参数数量不固定,可以使用execv()
或execvp()
。当需要执行的程序完整路径已知时,建议使用execl()
、execle()
和execv()
,否则建议使用execlp()
和execvp()
。需要注意的是,在调用这些函数时,需要确保当前进程的资源已经处理完成,因为这些函数会替换调用它们的进程映像并启动新的程序。此外,这些函数失败时会返回 -1,并设置 errno 错误码,需要根据 errno 的值来判断错误的类型。
execl()
,execlp()
,execle()
,execv()
,execvp()
是exec()
函数族的不同成员,用于在当前进程中执行一个新的程序。它们之间的区别主要在于参数的传递方式和搜索可执行文件的路径。下面是对它们的详细说明和示例:
execl()
函数原型:
int execl(const char *path, const char *arg0, ..., const char *argn, (char *)NULL)
参数说明:第一个参数
path
是待执行的程序路径,后面的参数是传递给新程序的命令行参数,以NULL
结束。功能说明:将当前进程的映像替换为指定的程序,并执行该程序。
示例代码:
#includeint main() { execl("/bin/ls", "ls", "-l", NULL); return 0; } 上述示例使用
execl()
执行了/bin/ls
程序,并传入-l
参数。
execlp()
函数原型:
int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)NULL)
参数说明:第一个参数
file
是待执行的程序名,可以是一个可执行文件的路径,也可以是系统的可执行文件,后面的参数是传递给新程序的命令行参数,以NULL
结束。功能说明:根据给定的程序名,在系统的可执行文件路径中搜索该程序,并执行。
示例代码:
#includeint main() { execlp("ls", "ls", "-l", NULL); return 0; } 上述示例使用
execlp()
执行了系统中的ls
程序,并传入-l
参数。
execle()
函数原型:
int execle(const char *path, const char *arg0, ..., const char *argn, char *const envp[])
参数说明:
path
是待执行的程序路径,arg0
到argn
是传递给新程序的命令行参数,以NULL
结束,envp[]
是新程序的环境变量数组,以NULL
结束。功能说明:将当前进程的映像替换为指定的程序,并执行该程序,可以指定新程序的环境变量。
示例代码:
#includeint main() { char *env[] = {"USER=binjie", "HOME=/home/binjie", NULL}; execle("/bin/ls", "ls", "-l", NULL, env); return 0; } 上述示例使用
execle()
执行了/bin/ls
程序,并传入-l
参数,并指定了新程序的环境变量。
execv()
函数原型:
int execv(const char *path, char *const argv[])
参数说明:
path
是待执行的程序路径,argv[]
是传递给新程序的命令行参数数组,以NULL
结束。功能说明:将当前进程的映像替换为指定的程序,并执行该程序。
示例代码:
#includeint main() { char *args[] = {"ls", "-l", NULL}; execv("/bin/ls", args); return 0; } 上述示例使用
execv()
执行了/bin/ls
程序,并传入-l
参数。
execvp()
函数原型:
int execvp(const char *file, char *const argv[])
参数说明:
file
是待执行的程序名,可以是一个可执行文件的路径,也可以是系统的可执行文件,argv[]
是传递给新程序的命令行参数数组,以NULL
结束。功能说明:根据给定的程序名,在系统的可执行文件路径中搜索该程序,并执行。
示例代码:
#includeint main() { char *args[] = {"ls", "-l", NULL}; execvp("ls", args); return 0; } 上述示例使用
execvp()
执行了系统中的ls
程序,并传入-l
参数。需要注意的是,这些函数在执行成功时不会返回,只有在执行失败时才会返回,并返回 -1。如果发生错误,可以使用
perror()
函数来打印相应的错误信息。此外,这些函数可以是可变参数形式,也可以通过传入参数数组来实现。选择合适的函数取决于传参的方便性和需求。共用文件
# include# include # include # include # include void fatal(const char *msg); int printpos(const char *string, int filedes); int main() { int fd; // 文件描述符 pid_t pid; // 进程ID char buf[10]; // 缓冲区 if ((fd = open("test.c", O_RDONLY)) == -1) fatal("open failed"); // 打开名为 "filedata" 的文件,返回文件描述符 read(fd, buf, 10); // 从文件中读取 10 字节数据保存到缓冲区 buf 中 printpos("Before fork", fd); // 打印当前文件位置 switch(pid = fork()) { case -1: fatal("fork failed"); // 复制进程失败 break; case 0: // 子进程代码段 printpos("Child before read", fd); // 打印当前文件位置 read(fd, buf, 10); // 从文件中读取 10 字节数据保存到缓冲区 buf 中 printpos("Child after read", fd); // 打印当前文件位置 break; default: // 父进程代码段 wait((int*)0); // 等待子进程结束 printpos("Parent after wait", fd); // 打印当前文件位置 } close(fd); // 关闭文件描述符,释放资源 return 0; } void fatal(const char *msg) { printf("ERROR: %s\n", msg); exit(1); } int printpos(const char *string, int filedes) { off_t pos; if ((pos = lseek(filedes, 0, SEEK_CUR)) == -1) fatal("lseek failed"); // 获取当前文件位置 printf("%s:%ld\n", string, pos); return 0; }
main()
函数调用return
、exit
和_exit
都可以用来终止程序的执行,但它们之间有一些区别。
return
:在main()
函数中使用return
语句可以正常退出程序。当main()
函数执行完毕并返回一个整数值时,这个整数值将作为程序的退出状态码(或称为返回码)被返回给操作系统。通常情况下,返回值为 0 表示程序执行成功,非零值表示程序执行失败或出现错误。此外,return
只能用于退出main()
函数,不会直接影响到其他子进程或线程。
exit
:exit
是一个库函数,用于终止整个程序的执行。当调用exit(code)
时,程序会立即终止,并且返回码code
将被返回给操作系统。与return
不同的是,exit
不仅仅用于退出main()
函数,它可以在任何地方调用,并且可以在多线程环境中正确地终止所有线程。此外,
exit
还会执行一系列清理工作,包括调用注册的atexit
函数,关闭所有打开的文件流,并刷新缓冲区等。因此,exit
提供了一种正常退出程序并进行善后处理的方式。
_exit
:_exit
是一个系统调用函数,用于立即终止当前进程的执行,不会进行任何清理工作。它不会刷新缓冲区、关闭文件流或调用atexit
注册的函数。_exit
的使用相对较少,通常用于在出现严重错误时强制终止程序执行,而不进行任何善后处理。总结:
return
用于从main()
函数中正常退出,返回状态码给操作系统。
exit
用于在任何地方终止程序的执行,并进行一系列清理工作。
_exit
用于立即终止当前进程的执行,不进行任何清理工作。在一般情况下,推荐使用
exit
来终止整个程序的执行,以确保进行必要的资源释放和清理工作。
mknod
命令
mknod
命令用于在 Linux 系统上创建设备文件节点。设备文件节点用于表示设备文件,如字符设备或块设备。命令语法为:
mknod filename [type] [major] [minor]参数说明:
filename
:指定要创建的设备文件节点的名称。
type
:可选参数,指定要创建的设备文件的类型。可以是p
(FIFO 管道),c
(字符设备)或b
(块设备)。默认为c
(字符设备)。
major
:可选参数,指定设备文件的主设备号。对于字符设备和块设备,主设备号用于标识设备类型。
minor
:可选参数,指定设备文件的次设备号。对于字符设备和块设备,次设备号用于区分同一类型的不同设备。请注意,创建设备文件节点需要超级用户权限(root 权限)。以下是一些示例用法:
创建一个命名管道(FIFO):
mknod mypipe p创建一个字符设备文件:
mknod mychardev c 250 0创建一个块设备文件:
mknod myblockdev b 8 0这些示例命令将在当前目录下创建相应的设备文件节点。
需要注意的是,现代的 Linux 系统通常会自动管理设备文件节点的创建和删除,因此在大多数情况下并不需要手动使用
mknod
命令来创建设备文件节点。只有在特殊情况下或特定需求时,才需要手动创建设备文件节点。请谨慎使用
mknod
命令,并确保理解其用途和参数的含义,以避免对系统造成意外影响。$ cat < /tmp/my_fifo &
在命令行中,
&
符号表示将进程置于后台运行。具体地说,当在执行命令时,在命令的末尾使用&
符号可以使该命令在后台运行,不占用当前终端的控制权。cat < /tmp/my_fifo &
表示将cat
命令以后台进程的形式运行,并从/tmp/my_fifo
文件中读取内容。该命令将启动cat
进程,该进程将读取 FIFO 文件的内容并将其输出到终端。同时,你可以继续在终端中执行其他命令,而不需要等待cat
命令的完成。open(FIFO_NAME, O_RDONLY ,0);
在
open()
函数中,最后一个参数是一个整数,通常称为mode
或permission
。在打开FIFO文件时,该参数指定了文件的访问权限。在给定的参数中,0代表不设置额外的权限标志。它指示
open()
函数按照默认的文件权限来打开FIFO文件。在Unix/Linux系统中,默认的FIFO文件权限取决于当前进程的 umask 值。umask 是一个掩码,用于屏蔽创建文件时的权限位,因此创建的文件将不会具备这些权限。通过设置不同的umask值,可以控制默认权限。
在使用参数0的情况下,
open(FIFO_NAME, O_RDONLY, 0)
将以默认权限打开FIFO文件,并且权限将根据当前进程的umask值而定。这意味着FIFO文件的实际权限可能与传递给open()
的权限参数有所不同。如果要显式地指定权限标志,应该使用合适的八进制值来替代0。例如,要指定所有者具有读权限,可以使用0400;要指定所有者、所属组和其他人都具有读和写权限,可以使用0666。
总结来说,参数0表示
open()
函数以默认权限打开FIFO文件,而具体的权限取决于当前进程的umask值。信号量的原理
以下是一个完整的C语言代码示例,演示了Linux中信号量的原理和使用:
#include#include #include #include #include #define NUM_THREADS 4 int counter = 0; sem_t semaphore; void* thread_func(void* arg) { int i, thread_id = *(int*)arg; for (i = 0; i < 1000000; ++i) { // 等待信号量 sem_wait(&semaphore); // 修改共享资源 ++counter; // 释放信号量 sem_post(&semaphore); } printf("Thread %d finished. Counter = %d\n", thread_id, counter); pthread_exit(NULL); } int main() { int i, thread_ids[NUM_THREADS]; pthread_t threads[NUM_THREADS]; // 初始化信号量 sem_init(&semaphore, 0, 1); // 创建线程 for (i = 0; i < NUM_THREADS; ++i) { thread_ids[i] = i + 1; pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]); } // 等待线程结束 for (i = 0; i < NUM_THREADS; ++i) { pthread_join(threads[i], NULL); } // 销毁信号量 sem_destroy(&semaphore); printf("Final counter value: %d\n", counter); return 0; } 在上述代码中,首先定义了全局变量
counter
作为共享资源,表示计数器的值。然后定义了信号量semaphore
。在线程函数
thread_func
中,每个线程会执行1000000次的循环进行计数器的递增操作。在每次计数操作之前,线程会调用sem_wait()
来等待信号量。如果信号量的值大于0,表示有可用资源,线程将成功获取信号量,并将信号量的值减1,表示资源被占用。如果当前没有可用资源(信号量的值为0),线程将被阻塞,直到其他线程释放资源。在计数操作完成后,线程将调用
sem_post()
来释放信号量。这会将信号量的值加1,表示资源被释放,其他等待获取信号量的线程将有机会继续执行。在主函数
main()
中,首先初始化信号量semaphore
,设置初值为1。然后创建了四个线程,并在每个线程中执行thread_func
。接着等待所有线程结束,并销毁信号量。最后,输出最终的计数器的值。
通过使用信号量,我们实现了对共享资源的互斥访问,确保了多个线程对计数器的操作是安全和同步的。
ps:
对于
sem_init()
函数,第二个参数并不是用于标识信号量是否在进程间共享的。实际上,第二个参数是用于指定信号量的作用范围,即进程内部线程之间共享还是仅在线程内部使用。具体说明如下:int sem_init(sem_t *sem, int pshared, unsigned int value);
sem
:指向要初始化的信号量变量的指针。 pshared:指定信号量的作用范围,即是否在进程间共享。
如果
pshared
为0,则该信号量只能在调用sem_init()
的进程内的线程间共享。如果
pshared
非零(通常是1),则该信号量可在多个进程间共享,用于进程间的同步和通信。
value
:指定信号量的初始值。在代码示例中,
sem_init(&semaphore, 0, 1)
将信号量semaphore
初始化为一个非进程间共享的信号量,只能在调用sem_init()
的进程内的线程间共享。初始值为1,表示有一个可用的资源。
semget
和semop
是Linux系统提供的用于进程间同步和互斥的信号量操作函数。下面是一个完整的例子来说明如何使用这两个函数:
#include#include #include #include #include #include // 定义信号量的数量 #define NUM_SEMAPHORES 1 int main() { int semid; struct sembuf sb; // 生成一个唯一的key key_t key = ftok(".", 'S'); // 创建信号量集合,其中包含NUM_SEMAPHORES个信号量 semid = semget(key, NUM_SEMAPHORES, IPC_CREAT | 0666); if (semid == -1) { perror("Failed to create semaphore"); exit(1); } // 初始化信号量的值为1(可用) unsigned short sem_values[NUM_SEMAPHORES] = {1}; semctl(semid, 0, SETALL, sem_values); // 准备要操作的信号量 sb.sem_num = 0; // 操作第一个信号量(下标为0) sb.sem_op = -1; // 执行P操作(将信号量的值减1) sb.sem_flg = 0; printf("Before P operation\n"); // 执行P操作 if (semop(semid, &sb, 1) == -1) { perror("Failed to perform P operation"); exit(1); } printf("After P operation\n"); sleep(5); // 模拟某个操作 // 准备要操作的信号量 sb.sem_num = 0; // 操作第一个信号量(下标为0) sb.sem_op = 1; // 执行V操作(将信号量的值加1) sb.sem_flg = 0; printf("Before V operation\n"); // 执行V操作 if (semop(semid, &sb, 1) == -1) { perror("Failed to perform V operation"); exit(1); } printf("After V operation\n"); // 删除信号量集合 if (semctl(semid, 0, IPC_RMID) == -1) { perror("Failed to remove semaphore"); exit(1); } return 0; } 在上面的例子中,首先调用
ftok
函数生成一个唯一的key。然后使用semget
创建一个包含一个信号量的信号量集合。接着使用semctl
初始化信号量的值为1,表示该信号量可用。然后,我们通过设置
struct sembuf
结构体的字段来准备要执行的P操作。在本例中,我们将操作第一个信号量(下标为0),将其值减1。然后调用semop
执行P操作。紧接着,我们模拟某个操作,这里使用
sleep
函数来暂停程序一段时间。接下来,我们再次准备要执行的V操作,将信号量的值加1。然后调用
semop
执行V操作。最后,我们使用
semctl
函数删除信号量集合。以上就是使用
semget
和semop
进行信号量操作的完整例子。在实际应用中,可以根据需要设置多个信号量和进行复杂的同步操作。使用
semop
和semget
的完整例子代码:#include#include #include #include #include #define KEY 1234 union semun { int val; struct semid_ds *buf; unsigned short *array; }; int main() { int semid, status; struct sembuf sb; union semun arg; // 创建信号量 semid = semget(KEY, 1, IPC_CREAT | 0666); if (semid == -1) { perror("semget"); exit(1); } // 初始化信号量为1 arg.val = 1; status = semctl(semid, 0, SETVAL, arg); if (status == -1) { perror("semctl"); exit(1); } // 对信号量进行 P 操作 sb.sem_num = 0; // 信号量编号 sb.sem_op = -1; // P 操作 sb.sem_flg = 0; // 操作标志 status = semop(semid, &sb, 1); if (status == -1) { perror("semop"); exit(1); } printf("进程获得了信号量\n"); // 对信号量进行 V 操作 sb.sem_num = 0; // 信号量编号 sb.sem_op = 1; // V 操作 sb.sem_flg = 0; // 操作标志 status = semop(semid, &sb, 1); if (status == -1) { perror("semop"); exit(1); } printf("进程释放了信号量\n"); // 删除信号量 status = semctl(semid, 0, IPC_RMID, arg); if (status == -1) { perror("semctl"); exit(1); } return 0; } 这个例子展示了如何使用
semget
创建一个信号量,使用semctl
初始化信号量的值,使用semop
进行P(等待)操作和V(发出)操作,以及使用semctl
删除信号量。在以上代码中,我们创建了一个信号量集合,并将其初始化为1。然后,我们获取该信号量,并对它进行P操作(等待)。接着,我们释放信号量,并最终删除该信号量。需要注意的是,上述代码只是一个简单的示例,实际使用时可能还需要添加错误处理、进程同步等额外的逻辑。
semop
函数用于对信号量进行操作,包括等待(P操作)和发出(V操作)。下面是一个完整的示例代码,展示了如何使用
semop
对信号量进行操作:#include#include #include #include #include #define KEY 1234 union semun { int val; struct semid_ds *buf; unsigned short *array; }; void semaphore_P(int semid) { struct sembuf sb; sb.sem_num = 0; // 信号量编号 sb.sem_op = -1; // P 操作 sb.sem_flg = 0; // 操作标志 if (semop(semid, &sb, 1) == -1) { perror("semop - P"); exit(1); } } void semaphore_V(int semid) { struct sembuf sb; sb.sem_num = 0; // 信号量编号 sb.sem_op = 1; // V 操作 sb.sem_flg = 0; // 操作标志 if (semop(semid, &sb, 1) == -1) { perror("semop - V"); exit(1); } } int main() { int semid, status; union semun arg; // 创建信号量 semid = semget(KEY, 1, IPC_CREAT | 0666); if (semid == -1) { perror("semget"); exit(1); } // 初始化信号量为1 arg.val = 1; status = semctl(semid, 0, SETVAL, arg); if (status == -1) { perror("semctl"); exit(1); } // 对信号量进行 P 操作 semaphore_P(semid); printf("进程获得了信号量\n"); // 对信号量进行 V 操作 semaphore_V(semid); printf("进程释放了信号量\n"); // 删除信号量 status = semctl(semid, 0, IPC_RMID, arg); if (status == -1) { perror("semctl"); exit(1); } return 0; } 在这个示例中,我们使用了两个自定义的函数
semaphore_P
和semaphore_V
来封装了对信号量的P操作和V操作。首先,我们创建了一个信号量集合,并将其初始化为1。然后,我们调用semaphore_P
函数对信号量进行P操作,表示进程正在等待(申请)信号量。接下来,我们调用semaphore_V
函数对信号量进行V操作,表示进程释放了信号量。最后,我们删除信号量。需要注意的是,这个示例并没有考虑多进程同步的问题,仅仅展示了对信号量进行基本操作的方式。在实际使用时,可能需要更复杂的逻辑和同步机制来确保进程之间的正确协作。
Linux消息队列使用,包括创建、发送和接收消息:
#include#include #include #include #include #include #define KEY 1234 struct message { long mtype; char mtext[80]; }; int main() { int msqid, status; struct message msg; key_t key; // 创建消息队列 key = ftok(".", 'a'); if (key == -1) { perror("ftok"); exit(1); } msqid = msgget(key, IPC_CREAT | 0666); if (msqid == -1) { perror("msgget"); exit(1); } // 发送消息 msg.mtype = 1; strcpy(msg.mtext, "hello, world"); status = msgsnd(msqid, &msg, sizeof(msg.mtext), 0); if (status == -1) { perror("msgsnd"); exit(1); } printf("sent message: %s\n", msg.mtext); // 接收消息 status = msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0); if (status == -1) { perror("msgrcv"); exit(1); } printf("received message: %s\n", msg.mtext); // 删除消息队列 status = msgctl(msqid, IPC_RMID, NULL); if (status == -1) { perror("msgctl"); exit(1); } return 0; } 在这个示例中,我们首先使用
ftok
函数生成一个唯一的键值,用于创建消息队列。然后,我们使用msgget
函数创建一个消息队列,并使用msgsnd
函数向该队列发送一条消息。接着,我们使用msgrcv
函数从该队列接收一条消息。最后,我们使用msgctl
函数删除该消息队列。需要注意的是,在以上示例中,我们设置了消息类型为1。在实际应用中,可能需要根据需求设置不同的消息类型。此外,在生产环境的应用程序中,可能需要支持多个进程(线程)同时对消息队列进行操作,因此需要引入进程同步机制,例如使用信号量或互斥锁。