一、在 Ubuntu 20.04 上安装 GCC
默认的 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 系统上安装好了,你可以开始使用它了。
二、编译一个 Hello World 实例
使用 GCC 编译一个基本的 C 或者 C++ 程序非常简单。打开你的文本编辑器,并且创建下面的文件:
nano hello.c #include <stdio.h> int main() { printf ("Hello World!\n"); return 0; }
保存文件,并且将它编译成可执行文件,运行:
gcc hello.c -o hello
这将在你运行命令的同一个目录下创建一个二进制文件,名称为"hello”。
运行这个hell0
程序:
./hello
程序应该打印:
Hello World!
三、安装多个 GCC 版本
这一节提供一些指令,关于如何在 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 Press <enter> to 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并执行make
# Makefile main : main.c gcc main.c -o main
(makefile 三要素)
通配符:
# Makefile main : $(wildcard *.c) gcc $(wildcard *.c) -o main
变量:
# Makefile SRCS := $(wildcard *.c) main : $(SRCS) gcc $(SRCS) -o main
ps:
变量分为系统变量、自定义变量、自动化变量
自定义变量:
= 是延迟赋值
:= 是立即赋值
?= 是空赋值
+= 是追加赋值
自动化变量:
$< 第一个依赖文件
$^ 全部依赖文件
$@ 目标
模式匹配
% 匹配任意多个字符
* 通配符
默认规则
.o文件默认.使用.c文件进行编译
条件分支1
ifeq(var1, var2)
...
else
..
endif
----------------------------------------------------------
ifeq(<arg1>, <arg2>)
语句1
else ifeq(<arg3>, <arg4>)
语句2
else
语句3
endif
条件分支2
ifneq(<arg1>, <arg2>)
语句1
else
语句2
endif
cc
命令时通过 -I
选项指定头文件所在路径
# Makefile INCS := -I./func SRCS := $(wildcard *.c) main : $(SRCS) gcc $(INCS) $(SRCS) -o mainma
make file 文件中的:.PHONY
Phony Targets
A phony target is one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request. There are two reasons to use a phony target: to avoid a conflict with a file of the same name, and to improve performance.
指针
在C语言中,变量名不是地址。变量名是用来标识内存地址的符号,它表示变量在计算机内存中的位置。当定义一个变量时,系统会为该变量分配一个内存地址,并且可以使用变量名来访问该变量的值。
在C语言中,指针是一种特殊的变量,它存储的是其他变量的内存地址,而不是值本身。通过指针,我们可以间接地访问和修改其指向的内存区域的值。
指针的声明和定义如下:
数据类型 *指针变量名;
其中,数据类型可以是任何有效的C语言数据类型,如int、char、float等。指针变量名是你为指针变量选择的名称。
下面是一个完整的例子,演示了如何声明、定义和使用指针变量:
#include <stdio.h>
int 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 语言实现的示例:
首先,定义一个交换函数,它接收两个整数的指针:
#include <stdio.h> void 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语言中,函数的参数传递是值传递。这意味着当你传递一个变量到函数中时,函数会创建一个新的副本,而不是直接引用原始变量。因此,在函数内部对参数的任何修改都不会影响原始变量的值。
下面是一个简单的示例来说明这个概念:
#include <stdio.h>
void 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
变量。输出将是:
num inside the function: 1 num in main: 0
这表明尽管在函数内部num
的值被改变了,但这种改变并没有影响到函数外部的原始变量。这就是因为在C语言中,函数参数是通过值传递的。
如果你希望在函数中修改一个变量的值,并影响到原始变量,你需要使用指针。通过指针,你可以直接引用和修改内存中的原始值,而不是传递一个副本。
数组与指针
在C语言中,数组和指针之间有一个非常紧密的关系。数组的名称可以被看作是一个指向数组第一个元素的指针。同样,一个指向某个特定类型的指针也可以被看作是一个指向该类型的数组。这种关系可以在下面的示例代码中看到:
#include <stdio.h>
int 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。
二维数组的指针:
include <stdio.h>
int 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 <stdio.h>
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 <stdio.h>
// 定义一个函数,用于计算两个整数的和
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语言中,函数指针是指向函数的指针变量。它通常用于回调函数、函数参数传递和函数表等场景。分别给出这三个场景下应用的例子
当然,以下是函数指针在C语言中的三种常见应用场景的例子:
-
回调函数(Callback Functions)
在C语言中,函数指针最常见的用途是实现回调函数。以下是一个简单的例子:
#include <stdio.h>
// 定义一个函数指针类型
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 <stdio.h>
// 定义一个函数,这个函数接受一个整数和一个函数指针
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 <stdio.h>
// 定义一个函数指针类型,用于指向处理函数的指针数组的函数指针类型
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;
}
在C语言中,函数的可变参数(Variable Arguments)是一种特殊的函数参数类型,它允许函数接受可变数量的参数。这种参数类型被表示为...
(三个点),通常作为函数参数列表的最后一个参数。
可变参数的应用场景是在函数需要处理可变数量或类型的参数时,例如函数需要接受任意数量的整数、字符串或其他数据类型,或者需要接受不同数量的参数进行不同的操作。
下面是一个简单的示例,演示了如何使用可变参数实现一个函数,该函数接受任意数量的整数并计算它们的和:
#include <stdio.h>
#include <stdarg.h>
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
,则可能表示发生了错误或者到达了文件的末尾。
例如,以下代码将从文件中读取一个整数数组:
#include <stdio.h>
int 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 <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
#include <stdlib.h>
// 定义线性表结点的结构体
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 <stdio.h>
#include <stdlib.h>
// 定义链表节点
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 <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
#include <stdlib.h>
#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;
}
树
#include<iostream>
using namespace std;
#define MAXSIZE 100
#include<string>
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<<T->data<<endl;
inordertraverse(T->rchild);
}
}
//preortrave
void pretrave(BiTree T){
if(T){
cout<<T->data<<" ";
pretrave(T->lchild);
pretrave(T->rchild);
}
}
//latrave
void latrave(BiTree T){
if(T){
latrave(T->lchild);
latrave(T->rchild);
cout<<T->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<<"计算深度: "<<Depth(T)<<endl;
BiTree T1;
cout<<"复制树T1:"<<endl;
Copy(T,T1);
cout<<"前序遍历"<<endl;
pretrave(T);
cout<<"中序遍历树"<<endl;
inordertraverse(T);
cout<<"后序遍历"<<endl;
latrave(T);
cout<<"结点计算:"<<NodeCount(T)<<endl;
system("pause");
return 0;
}
c++
alt+insert 快捷键
文件操作
•实现与linux命令who相同功能的程序,提示在头文件utmp.h中有一个结构体struct utmp,保存了登入用户的信息,实现的程序至少显示用户名及所登入的终端
以下是一个用 C 语言实现与 Linux 命令 who
相同功能的程序示例,它使用了 utmp.h
头文件中的 struct utmp
结构体来获取登录用户的信息,并显示用户名及所登录的终端:
#include <stdio.h>
#include <utmp.h>
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 -lutmp
link
在C语言中,使用 link
和 unlink
函数可以修改一个文件的名字。
#include <unistd.h> int link(const char *oldpath, const char *newpath); int unlink(const char *pathname);
-
link
函数创建一个新的链接,将oldpath
文件名链接到newpath
文件名。它会使newpath
成为oldpath
的另一个别名。如果newpath
已经存在,则无法创建链接。 -
unlink
函数删除一个文件,即通过文件名pathname
删除文件的链接。如果该文件有多个链接,只有在最后一个链接被删除时,文件才会真正被删除。
以下是一个例子,展示如何使用 link
和 unlink
修改文件名:
#include <unistd.h>
#include <stdio.h>
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 <stdio.h>
#include <stdlib.h>
#include <dirent.h>
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 <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <ncurses.h>
// 定义最大路径长度
#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 <stdio.h>
#include <unistd.h>
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
的用法:
#include <stdio.h>
int 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
的用法:
#include <stdio.h>
int 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)
。
-
示例代码:
#include <stdio.h>
int 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
函数没有返回值。
-
示例代码:
#include <stdio.h>
int 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
函数主要用于将格式化结果保存到字符串变量中,以便后续使用。
-
示例代码:
#include <stdio.h>
int 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
函数内部自动分配缓冲区。自定义缓冲区可以是数组或者动态分配的内存。
示例代码:
#include <stdio.h>
int 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 <stdio.h>
#include <signal.h>
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#include <signal.h> int 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 <stdio.h>
#include <signal.h>
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
函数的说明:
#include <signal.h> int 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 <stdio.h>
#include <signal.h>
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 <stdio.h>
#include <sys/types.h>
#include <unistd.h>
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 慎用:
include <stdio.h>
include <unistd.h>
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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
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。
下面是一个示例代码:
include <stdio.h>
include <stdlib.h>
include <sys/types.h>
include <sys/wait.h>
include <unistd.h>
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() 的简单示例代码:
include <stdio.h>
include <unistd.h>
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
结束。 -
功能说明:将当前进程的映像替换为指定的程序,并执行该程序。
-
示例代码:
#include <unistd.h> int 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
结束。 -
功能说明:根据给定的程序名,在系统的可执行文件路径中搜索该程序,并执行。
-
示例代码:
#include <unistd.h> int 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
结束。 -
功能说明:将当前进程的映像替换为指定的程序,并执行该程序,可以指定新程序的环境变量。
-
示例代码:
#include <unistd.h> int 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
结束。 -
功能说明:将当前进程的映像替换为指定的程序,并执行该程序。
-
示例代码:
#include <unistd.h> int 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
结束。 -
功能说明:根据给定的程序名,在系统的可执行文件路径中搜索该程序,并执行。
-
示例代码:
#include <unistd.h> int main() { char *args[] = {"ls", "-l", NULL}; execvp("ls", args); return 0; }
-
上述示例使用
execvp()
执行了系统中的ls
程序,并传入-l
参数。
-
需要注意的是,这些函数在执行成功时不会返回,只有在执行失败时才会返回,并返回 -1。如果发生错误,可以使用 perror()
函数来打印相应的错误信息。此外,这些函数可以是可变参数形式,也可以通过传入参数数组来实现。选择合适的函数取决于传参的方便性和需求。
共用文件
# include <stdio.h>
# include <unistd.h>
# include <fcntl.h>
# include <sys/wait.h>
# include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
#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;
}
在上述代码中,首先定义了全局变量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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
// 定义信号量的数量
#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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#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。在实际应用中,可能需要根据需求设置不同的消息类型。此外,在生产环境的应用程序中,可能需要支持多个进程(线程)同时对消息队列进行操作,因此需要引入进程同步机制,例如使用信号量或互斥锁。
socket
C语言中定义了一个名为sockaddr_in的结构体
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* internet address family */
in_port_t sin_port; /*port number */
struct in_addr sin_addr; /* holds the IP address */
unsigned char sin_zero[8] /*filling */
};
具体解释如下:
-
#include <netinet/in.h>
:这是一个头文件的引入指令,它包含了定义网络通信相关的数据类型和函数的声明。 -
struct sockaddr_in
:这行代码定义了一个名为sockaddr_in的结构体。结构体是一种复合数据类型,用于存储和组织多个不同类型的数据成员。 -
sa_family_t sin_family
:这是结构体中的第一个数据成员,用于表示套接字地址的协议族。在这里,它被声明为sa_family_t类型,表示协议族的整数值。 -
in_port_t sin_port
:这是结构体中的第二个数据成员,用于表示端口号。在网络通信中,端口号是用于标识进程或服务的数字。 -
struct in_addr sin_addr
:这是结构体中的第三个数据成员,它是一个嵌套的结构体类型in_addr。它用于存储IP地址。 -
unsigned char sin_zero[8]
:这是结构体中的第四个数据成员,它是一个长度为8的无符号字符数组。在早期版本的结构体定义中,这个字段被用作填充字段,以确保整个结构体的大小为固定的大小。
总结起来,这段代码定义了一个用于表示IPv4套接字地址的结构体,其中包括协议族、端口号、IP地址和填充字段等信息。它在网络编程中常用于设置和操作套接字地址。
主机字节序(Host Byte Order)和网络字节序(Network Byte Order)
主机字节序(Host Byte Order)和网络字节序(Network Byte Order)是在计算机网络中用于表示数据的字节序列的两种不同规范。
主机字节序是指计算机系统中处理器使用的字节顺序。在大多数现代计算机系统中,常用的主机字节序有两种:大端字节序(Big Endian)和小端字节序(Little Endian)。
- 大端字节序:数据的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。这种字节序类似于人类书写的方式,先写高位再写低位。
- 小端字节序:数据的低位字节存储在内存的低地址处,高位字节存储在内存的高地址处。这种字节序则是反过来的,先写低位再写高位。
网络字节序是一种统一规定的字节序,用于在计算机网络中传输数据。它采用的是大端字节序(Big Endian)。网络字节序的定义是为了确保不同计算机体系结构的设备在进行网络通信时能够正确地解释接收到的二进制数据。
在网络通信中,发送方和接收方需要对数据进行字节序的转换以确保数据能够正确地解析和处理。通常使用以下函数进行主机字节序和网络字节序之间的转换:
htons()
:用于将16位(2字节)整数从主机字节序转换为网络字节序。htonl()
:用于将32位(4字节)整数从主机字节序转换为网络字节序。ntohs()
:用于将16位(2字节)整数从网络字节序转换为主机字节序。ntohl()
:用于将32位(4字节)整数从网络字节序转换为主机字节序。
这些函数在网络编程中被广泛使用,以确保不同主机之间的数据传输和解析的一致性和正确性。
网络字节序表示的 IPv4 地址 给出一个这样的例子
Pv4 地址通常以点分十进制表示法(dotted-decimal notation)的形式呈现,例如 192.168.0.1
。在计算机网络中,IP 地址通常以网络字节序(big-endian byte order)表示,其中高位字节先传输。因此,在使用网络字节序表示 IPv4 地址时,需要通过一定的转换过程将其转换为整数形式。
例如,假设我们要将 IPv4 地址 192.168.0.1
转换为网络字节序表示,可以使用以下代码:
include <stdio.h> #include <arpa/inet.h> int main() { struct in_addr addr; addr.s_addr = inet_addr("192.168.0.1"); printf("转换后的IPv4地址:%u\n", ntohl(addr.s_addr)); return 0; }
在上述代码中,我们使用了 inet_addr
函数将点分十进制字符串 192.168.0.1
转换为无符号整数形式,并将结果存储在 addr.s_addr
中。然后,我们使用 ntohl
函数将 addr.s_addr
中的整数值从网络字节序转换为主机字节序(即本机字节序)。在大多数情况下,主机字节序通常是小端字节序,但在本例中,由于我们使用的是 ntohl
函数,因此该值将被转换为大端字节序。最后,我们使用 printf
函数将转换后的 IPv4 地址打印为无符号整数形式。
在本例中,打印结果将是:
换后的IPv4地址:16885952
即将 192.168.0.1
转换为网络字节序表示后的无符号整数。
listen()
函数用于将一个已经创建的套接字标记为被动套接字(passive socket),并开始监听连接请求。它将套接字的状态设置为监听状态,以便接受传入的连接。
函数原型如下:
int listen(int sockfd, int queue_size);
-
sockfd
参数是之前使用socket()
函数创建的套接字文件描述符。 -
queue_size
参数指定了连接请求队列的最大长度。当有多个连接请求同时到达时,如果当前连接请求队列已满,则新的连接请求会被拒绝。因此,queue_size
应该根据实际情况进行合理设置。
listen()
函数成功执行时,返回值为 0。如果出现错误,返回值为 -1,并设置相应的错误码。
以下是一个使用 listen()
函数的简单例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 将套接字绑定到指定地址和端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 开始监听连接请求
if (listen(sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("服务器已经开始监听连接请求...\n");
// 其他操作...
return 0;
}
在上述例子中,首先使用 socket()
函数创建了一个套接字,并将其绑定到了本地的 IP 地址和端口。然后,使用 listen()
函数将套接字设为监听状态,设置连接请求队列的最大长度为 5。最后,服务器进入监听状态,可以接受传入的连接请求。
在实际应用中,需要在 listen()
之后编写相应的代码来处理接受的连接请求,例如使用 accept()
函数接受连接,并创建新的套接字与客户端进行通信。这只是 listen()
的基本示例,具体使用方式和后续操作需要根据具体的网络编程需求进行进一步开发。
ps:
INADDR_ANY
是一个特殊的常量,表示服务器绑定的 IP 地址。具体来说,它是用于指定服务器套接字可以接受来自任意网络接口(包括所有已分配的 IP 地址)的连接请求。
INADDR_ANY
的值是 0,在头文件 <netinet/in.h>
中定义为 (in_addr_t)0x00000000
。当将 INADDR_ANY
分配给服务器套接字的地址字段时,系统将选择适当的 IP 地址和网络接口。
服务器通常会有多个网络接口,可以分别对应不同的 IP 地址。如果将特定的 IP 地址分配给服务器套接字的地址字段,那么该套接字只能接受来自分配的 IP 地址对应网络接口的连接请求。而使用 INADDR_ANY
时,服务器套接字可接受来自任何接口的连接请求,这在某些场景下非常有用。
accept()
accept()
函数用于接受传入的连接请求,并创建一个新的套接字与客户端进行通信。它从服务器监听队列中选择一个等待的连接请求进行处理。
函数原型如下:
int accept(int sockfd, struct sockaddr *address, socklen_t *addrlen);
-
sockfd
参数是之前使用socket()
函数创建的套接字文件描述符,该套接字必须处于监听状态。 -
address
参数是一个指向struct sockaddr
类型的指针,用于存储接受连接的客户端的地址信息。 -
addrlen
参数是一个指向socklen_t
类型的指针,指定了address
结构体的大小。
accept()
函数成功执行时,返回值为一个新的套接字文件描述符,这个套接字用于与客户端进行通信。如果出现错误,返回值为 -1,并设置相应的错误码。
以下是一个使用 accept()
函数的简单例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd, new_sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 将套接字绑定到指定地址和端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 开始监听连接请求
if (listen(sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("服务器已经开始监听连接请求...\n");
// 接受连接
addr_len = sizeof(client_addr);
new_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (new_sockfd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("与客户端建立连接\n");
// 其他操作...
return 0;
}
在上述例子中,在 listen()
后使用 accept()
函数等待接受连接请求。当有客户端连接时,accept()
函数返回一个新的套接字文件描述符 new_sockfd
,用于与客户端进行通信。可以从 new_sockfd
中读取和写入数据来进行通信。
需要注意的是,accept()
函数是一个阻塞调用,当没有客户端连接时,程序会一直阻塞在这里,直到有新的连接请求到达为止。如果希望非阻塞地接受连接请求,可以使用非阻塞套接字或者设置超时选项(例如使用 select()
或 poll()
函数)。
在实际应用中,通常需要在 accept()
返回后编写相应的代码来处理客户端的请求,包括读取数据、发送响应等操作。这只是 accept()
的基本示例,具体使用方式和后续操作需要根据具体的网络编程需求进行进一步开发。
connect()
函数用于通过指定的套接字连接到远程服务器。它发送一个连接请求并等待服务器的响应,如果连接成功建立,则该套接字可以用来与服务器进行通信。
函数原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
参数是通过socket()
函数创建的套接字文件描述符。 -
addr
参数是指向要连接的地址结构体的指针。 -
addrlen
参数是地址结构体的大小。
connect()
函数成功执行时,返回值为 0。如果出现错误,返回值为 -1,并设置相应的错误码。
以下是一个使用 connect()
函数的简单例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8080);
// 连接服务器
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect");
exit(EXIT_FAILURE);
}
printf("与服务器建立连接\n");
// 其他操作...
return 0;
}
在上述例子中,connect()
函数被调用以连接到指定的服务器地址和端口。如果连接成功建立,程序将打印输出 "与服务器建立连接"。
需要注意的是,在实际应用中,通常需要在 connect()
返回后编写相应的代码来进行发送请求并接收响应等操作。这只是 connect()
的基本示例,具体使用方式和后续操作需要根据具体的网络编程需求进行进一步开发。
•面向非连接的发送与接受函数
在 Linux 中,面向非连接的发送与接收函数是指用于 UDP 协议的数据传输的函数。UDP 是一种无连接的传输协议,它不需要在发送数据之前建立连接,也不维护连接状态。
Linux 提供了两个主要的面向非连接的发送与接受函数:sendto()
和 recvfrom()
。
-
sendto()
函数用于向指定目标地址发送一个 UDP 数据报文。 -
recvfrom()
函数用于接收一个 UDP 数据报文并存储在指定缓冲区中。
这两个函数可以在无需事先建立连接的情况下,直接通过指定的目标地址和端口进行数据的发送和接收。与面向连接的发送和接收函数(如 send()
和 recv()
)不同,面向非连接的发送与接收函数不会在通信的过程中建立、维护和关闭连接状态。因此,在使用 UDP 协议进行数据传输时,可以直接使用面向非连接的发送与接收函数来发送和接收数据。
需要注意的是,由于 UDP 是一种不可靠的传输协议,它不保证数据的顺序和完整性,也没有内置的错误处理机制。因此,在应用层面,需要对数据进行适当的校验和处理,以确保数据的完整性和正确性。
sendto()
和 recvfrom()
是基于 UDP 协议进行网络传输时常用的两个函数,它们可以用于发送和接收 UDP 数据报文。
sendto()
函数用于向指定目标地址发送一个 UDP 数据报文。函数原型如下:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
-
sockfd
参数是通过socket()
函数创建的套接字文件描述符。 -
buf
参数是指向要发送数据的缓冲区的指针。 -
len
参数是要发送的数据长度。 -
flags
参数是可选标志,通常设为 0。 -
dest_addr
参数是指向目标地址的结构体的指针。 -
addrlen
参数是目标地址的大小。
sendto()
函数成功执行时,返回值为发送的数据字节数。如果出现错误,返回值为 -1,并设置相应的错误码。
以下是一个使用 sendto()
函数的简单例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char message[] = "Hello, Server!";
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8080);
// 发送数据
if (sendto(sockfd, message, sizeof(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("sendto");
exit(EXIT_FAILURE);
}
printf("已发送数据: %s\n", message);
// 其他操作...
return 0;
}
在上述例子中,sendto()
函数被调用以向指定地址发送 "Hello, Server!" 字符串。如果发送成功,程序将打印输出 "已发送数据: Hello, Server!"。
socket(AF_INET, SOCK_DGRAM, 0)
函数用于创建一个基于 UDP 协议的套接字。其中 SOCK_DGRAM
是指该套接字的类型,表示这是一个面向无连接的套接字。
UDP 是一种无连接的传输协议,它不需要在数据传输前先建立连接,也不需要维护连接的状态,因此使用 UDP 协议进行数据传输时,数据包之间是相互独立的,每个数据包具有自己的目标地址和端口,发送和接收操作都是面向无连接的。
与 SOCK_STREAM
类型的套接字(比如 TCP 套接字)相比,SOCK_DGRAM
类型的套接字轻量级、速度快,但是可靠性较低,不能保证数据的可靠性和完整性,因此适用于对数据传输速度要求较高,但数据可靠性要求不高的场景,比如音视频传输等。
需要注意的是,使用 SOCK_DGRAM
类型的套接字进行数据传输时,需要使用 sendto()
和 recvfrom()
等面向无连接的发送和接收函数,来发送和接收独立的数据包。
recvfrom()
函数用于接收一个 UDP 数据报文并存储在指定缓冲区中。函数原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
-
sockfd
参数是通过socket()
函数创建的套接字文件描述符。 -
buf
参数是接收数据的缓冲区的指针。 -
len
参数是缓冲区的大小。 -
flags
参数是可选标志,通常设为 0。 -
src_addr
参数是用于存储源地址的结构体指针。 -
addrlen
参数是源地址的大小。
recvfrom()
函数成功执行时,返回值为接收到的数据字节数。如果出现错误,返回值为 -1,并设置相应的错误码。
以下是一个使用 recvfrom()
函数的简单例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
char message[1024];
socklen_t addr_len;
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 将套接字绑定到指定地址和端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
printf("服务器已经开始监听数据...\n");
// 接收数据
addr_len = sizeof(client_addr);
if (recvfrom(sockfd, message, sizeof(message), 0, (struct sockaddr*)&client_addr, &addr_len) == -1) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
printf("收到来自 %s:%d 的数据: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), message);
// 其他操作...
return 0;
}
在上述例子中,在 bind()
后使用 recvfrom()
函数等待接收数据报文。当有客户端发送数据时,recvfrom()
函数返回,并将接收到的数据保存在 message
缓冲区中。程序将打印输出收到的数据和其来源地址。
需要注意的是,在实际应用中,通常需要编写相应的代码来进行数据的处理、解析等操作。这只是 sendto()
和 recvfrom()
的基本示例,具体使用方式和后续操作需要根据具体的网络编程需求进行进一步开发。
面向连接的发送与接受函数
c++杂记
应该将C++视为一个语言联邦!
它由四种编程模式构成:
C ——面向过程编程模式
Object-Based (C++)——基于对象编程模式
Object-Oriented (C++)——面向对象编程模式
Template (C++)——泛型化编程模式
C++语言的运算符 :: 称作作用域限定运算符
C++语言的“::” 还可用于限定数据成员或成员函数所属的类以及名空间,这时把运算符:: 称作作用域分辨运算符或作用域分辨符。
千万不要delete非动态分配的内存,行为将不确定。
int i = 10;
int *p = &i;
delete p;
诸位可以验证一下。
大纲:
Vs code 是个纯编辑器
、演示项目效果
2、Vs、vscode 安装
3、编译器
gcc编译程序的过程
gcc编译程序主要经过四个过程:
Gcc c c gcc-->c g++ cpp
预处理实际上是将头文件、宏进行展开。编译阶段,gcc调用不同语言的编译器,例如c语言调用编译器ccl。gcc实际上是个工具链,在编译程序的过程中调用不同的工具。汇编阶段,gcc调用汇编器进行汇编。链接过程会将程序所需要的目标文件进行链接成可执行文件。汇编器生成的是可重定位的目标文件,学过操作系统,我们知道,在源程序中地址是从0开始的,这是一个相对地址,而程序真正在内存中运行时的地址肯定不是从0开始的,而且在编写源代码的时候也不能知道程序的绝对地址,所以重定位能够将源代码的代码、变量等定位为内存具体地址。下面以一张图来表示这个过程,注意过程中文件的后缀变化,编译选项和这些后缀有关。
c/c++编译器安装:
编译器用mingw,可以从官网或者我们提供的百度网盘下载
下载内容解压到自己选的文件夹下:
我的是
Ps: 你在之前可以把 mingw64 解压或移动到不同于此的位置,但请务必确认这时的地址栏中显示的路径里不包含任何中文或空格!一个中文字都不能有!否则请把 mingw64 文件夹挪到一个纯英文、无空格、最好也比较短的路径中。这件事非常重要!中文路径很容易导致编译器或调试器不能被正确识别或运行
配置环境变量:
根据前一步解压实际路径配置。
测试:
编辑器种类:
Vscode/vs/clion…
Vs code c/c++开发环境配置
6.可选择安装vs
Vscode 安装:
1.下载vscode: (Documentation for Visual Studio Code) for Windows.
2. run the installer.
3. By default, VS Code is installed under `C:\Users\{Username}\AppData\Local\Programs\Microsoft VS ..
根据自己电脑实际情况选择安装路径,
安装:Vscode插件
至少安装上图中标注的五个插件
插件安装方法参考文档:
如何给VsCode(Visual Studio Code) 安装插件?保姆级教程来啦_vscode 手动安装扩展-CSDN博客
也可参考:
Get Started with C++ and MinGW-w64 in Visual Studio Code
vs安装比较简单,直接从官网下载安装即可
注意问题:
c++ 插件退回一个版本 不用最新的版本;
中文问题:
Task 当中:
"-fexec-charset=GBK", // 处理mingw中文编码问题
"-finput-charset=UTF-8",// 处理mingw中文编码问题
Vscode+Vc++环境配置:
配置 vs code c++ 有四个文件 lanuch.json 、tasks.json、c_cpp_properties.json 、setting.json ,作用为:
在 Visual Studio Code (VS Code) 中,C++ 的相关配置通常涉及四个主要的 JSON 文件:launch.json、tasks.json、c_cpp_properties.json 和 settings.json。这些文件分别用于不同的目的:
这四个文件通常位于你的工作区根目录下的 .vscode 文件夹中。如果你还没有这个文件夹或这些文件,你可以手动创建它们,或者通过 VS Code 的界面来生成它们(例如,通过“运行和调试”视图来生成 launch.json 和 tasks.json
<<tasks.json>>
<<settings.json>>
<<launch.json>>
<<c_cpp_properties.json>>
sqlite安装:
本机安装sqlite:
把下载好文件解压到自己选定定文件夹中:
其中sqllite3.lib 是我们用vs 下lib 命令生成的。可参考:
win10下使用VS2022搭建sqlite3环境_visual studio 2022 sqlite-CSDN博客
项目中sqlite的配置参考:
vscode安装sqlite插件:
Sqlite 命令行:
Practical SQLite Commands That You Don't Want To Miss
Sqlite 可视化 :
Vs code: 插件
Sqlite 是什么:
c++ 如何使用sqlite:
Summary
The following two objects and eight methods comprise the essential elements of the SQLite interface:
使用步骤:(描述…)
1 获取数据库对象
2 stmt 对象
3 sql
sqlite3_prepare_v2 --->stmt
Bind 参数
step
3 打开连接
Close(连接的)
4 prepare step 执行程序
Convenience Wrappers Around Core Routines
The sqlite3_exec() interface is a convenience wrapper that carries out all four of the above steps with a single function call. A callback function passed into sqlite3_exec() is used to process each row of the result set. The sqlite3_get_table() is another convenience wrapper that does all four of the above steps. The sqlite3_get_table() interface differs from sqlite3_exec() in that it stores the results of queries in heap memory rather than invoking a callback.
It is important to realize that neither sqlite3_exec() nor sqlite3_get_table() do anything that cannot be accomplished using the core routines. In fact, these wrappers are implemented purely in terms of the core routines.
Sqlite3_exec()完整例子:
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
int main(int argc, char* argv[])
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char *sql;
/* Open database */
rc = sqlite3_open("test.db", &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
exit(0);
}else{
fprintf(stdout, "Opened database successfully\n");
}
/* Create SQL statement */
sql = "CREATE TABLE COMPANY(" \
"ID INT PRIMARY KEY NOT NULL," \
"NAME TEXT NOT NULL," \
"AGE INT NOT NULL," \
"ADDRESS CHAR(50)," \
"SALARY REAL );";
/* Execute SQL statement */
rc = sqlite3_exec(db, sql, callback, 0, &zErrMsg);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Table created successfully\n");
}
sqlite3_close(db);
return 0;
}
sqlite3_exec() 是 SQLite3 数据库库中的一个函数,它允许你执行一个或多个由分号 (;) 分隔的 SQL 语句。这个函数特别有用,因为它允许你执行多个语句,并返回一个错误码,而不是为每一个单独的语句都返回一个错误码。
这是 sqlite3_exec() 函数的签名:
intsqlite3_exec( |
sqlite3*, /* Database handle */ |
constchar*sql, /* SQL statement, UTF-8 encoded */ |
int(*callback)(void*,int,char**,char**), /* Callback function */ |
void*, /* 1st argument to callback */ |
char**errmsg /* Error msg written here */ |
); |
参数说明:
返回值:
这个函数通常用于执行不需要查询结果的 SQL 语句,例如 INSERT、UPDATE、DELETE 和 CREATE 语句。如果你需要执行一个查询并处理结果,通常使用 sqlite3_prepare_v2() 和 sqlite3_step() 函数更为合适。
在 sqlite3_exec() 函数的参数列表中,int (*callback)(void*,int,char**,char**) 是一个函数指针的类型声明,它定义了一个回调函数,该函数将在 sqlite3_exec() 执行的每个 SQL 语句之后被调用。让我们详细解析这个回调函数的类型声明:
int(*callback)(void*, int, char**, char**)
下面是一个简单的示例,展示了如何使用这个回调函数:
#include<stdio.h> |
#include<sqlite3.h> |
/* 回调函数 */ |
staticintcallback_function(void*data, intargc, char**argv, char**azColName){ |
inti; |
for(i = 0; i < argc; i++) { |
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); |
} |
printf("\n"); |
return0; |
} |
intmain(){ |
sqlite3 *db; |
char*errMsg = 0; |
intrc; |
rc = sqlite3_open("test.db", &db); |
if(rc) { |
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); |
return(0); |
} |
/* 执行 SQL 查询,并使用回调函数 */ |
rc = sqlite3_exec(db, "SELECT * FROM some_table;", callback_function, 0, &errMsg); |
if(rc != SQLITE_OK) { |
fprintf(stderr, "SQL error: %s\n", errMsg); |
sqlite3_free(errMsg); |
} else{ |
printf("Operation done successfully\n"); |
} |
sqlite3_close(db); |
return0; |
} |
在这个示例中,callback_function 是一个简单的回调函数,它打印出查询结果的每一行和每一列的值。sqlite3_exec() 在执行 SELECT 语句后,会为结果集中的每一行调用这个回调函数。
- void*: 这是第一个参数,通常是一个指向用户数据的指针。在 sqlite3_exec() 的上下文中,这个指针是你在调用 sqlite3_exec() 时通过 void *data 参数传递的值。这个参数允许你将一些上下文信息传递给回调函数,比如你可能有一个结构体,其中包含了执行 SQL 语句所需的所有信息,你可以将这个结构体的指针传递给回调函数。
- int: 这是第二个参数,它表示 SQL 语句的结果状态码。这个值通常是 SQLITE_DONE(表示成功执行了一个查询或修改语句),或者是其他表示错误或特殊状态的代码。
- char**: 这是第三个参数,它是一个指向指针的指针,这些指针指向查询结果的列名。对于不返回结果的 SQL 语句(如 INSERT、UPDATE、DELETE),这个参数通常是 NULL。
- char**: 这是第四个参数,它是一个指向指针的指针,这些指针指向查询结果的行数据。对于不返回结果的 SQL 语句,这个参数也是 NULL。对于返回结果的 SQL 语句(如 SELECT),你可以通过这个参数来访问查询结果的每一行数据。
- int: 这是回调函数的返回类型。通常,对于 SQLite 的回调函数,返回 SQLITE_OK 表示成功,返回其他值则表示错误。
- (*callback): 这是一个函数指针的名称,名为 callback。* 表示这是一个指针,指向一个函数。
- (void*, int, char**, char**): 这是回调函数的参数列表,它描述了回调函数期望接收的参数类型:
- 如果执行成功,返回 SQLITE_OK(0)。
- 如果出现错误,返回一个非零的错误码。
- *void data: 这是传递给回调函数的第一个参数。
- **char errmsg: 这是一个指向字符指针的指针。如果执行 SQL 语句时出现错误,这个指针会被设置为一个描述错误的字符串。如果执行成功,它将是 NULL。
- sqlite_callback: 一个可选的回调函数,它在每个 SQL 语句执行后被调用。这个函数有四个参数:传递给 sqlite3_exec() 的用户数据,SQL 语句的结果类型(例如 SQLITE_DONE、SQLITE_ROW 等),一个指向查询结果字符串的指针(对于 SQLITE_ROW 结果类型),以及一个指向错误消息的指针。
- *const char sql: 要执行的 SQL 语句。可以是一个或多个由分号分隔的语句。
- sqlite3*: SQLite 数据库的连接句柄。
- sqlite3 → The database connection object. Created by sqlite3_open() and destroyed by sqlite3_close().
- sqlite3_stmt → The prepared statement object. Created by sqlite3_prepare() and destroyed by sqlite3_finalize().
- sqlite3_open() → Open a connection to a new or existing SQLite database. The constructor for sqlite3.
- sqlite3_prepare() → Compile SQL text into byte-code that will do the work of querying or updating the database. The constructor for sqlite3_stmt.
- sqlite3_bind() → Store application data into parameters of the original SQL.
- sqlite3_step() → Advance an sqlite3_stmt to the next result row or to completion.
- sqlite3_column() → Column values in the current result row for an sqlite3_stmt.
- sqlite3_finalize() → Destructor for sqlite3_stmt.
- sqlite3_close() → Destructor for sqlite3.
- sqlite3_exec() → A wrapper function that does sqlite3_prepare(), sqlite3_step(), sqlite3_column(), and sqlite3_finalize() for a string of one or more SQL statements.
- ps:https://www.sqlite.org/cintro.html
- 独立第三方工具
- 集成到vscode 第三方工具
- 第三方工具:
- 请访问 SQLite 下载页面,从 Windows 区下载预编译的二进制文件。
- 创建文件夹 d:\sqlite3,并在此文件夹下解压上面两个压缩文件,将得到 sqlite3.def、sqlite3.dll 和 sqlite3.exe 文件。
- 添加 d:\sqlite3 到 PATH 环境变量,最后在命令提示符下,使用 sqlite3 命令,将显示如下结果。
- 作用:这是 VS Code 的全局或工作区设置文件。它可以配置许多与 VS Code 相关的选项,包括编辑器的外观、行为、插件的配置等。
- 使用场景:你可以在这个文件中配置 C++ 相关的设置,例如更改默认的编译器、启用/禁用某些特性、设置代码格式化规则等。
- settings.json:
- 作用:这个文件用于配置 C++ 的编译器和 IntelliSense(代码智能感知功能)的属性。它定义了编译器路径、包含路径、宏定义、框架路径等。
- 使用场景:当你在编写 C++ 代码时,IntelliSense 功能(如自动完成、错误检查等)会根据 c_cpp_properties.json 中的配置来工作。
- c_cpp_properties.json:
- 作用:这个文件用于配置构建任务,即如何编译你的 C++ 代码。它可以定义编译器的路径、编译选项、输出目录等。
- 使用场景:当你按下 Ctrl+Shift+B(或 Cmd+Shift+B 在 macOS 上)来构建你的项目时,VS Code 会根据 tasks.json 中的配置来执行编译任务。
- tasks.json:
- 作用:这个文件用于配置调试器。它定义了如何启动程序,包括程序入口、调试器类型、环境变量、命令行参数等。
- 使用场景:当你想要调试你的 C++ 程序时,你需要在这个文件中配置调试器的相关设置。
- launch.json:
- Vscode 安装
- Vscode 插件安装
- c/c++编译器安装
- sqlite数据库安装
- vscode安装sqlite插件
- 预处理(Pre-Processing)
- 编译 (Compiling)
- 汇编 (Assembling)
- 链接 (Linking)
- Vs 平台
- 常用编辑器
- Vscode+c++ 开发环境配置
- Sqlite 介绍+安装
- sqlite集成(分为vs & vscode)
- Sqlite crud
- Vs/vscode 安装
- 演示项目
- c/c++编译
-
面向对象三大特征:
- 封装
- 安全性
- 继承
- 减少代码重复
- 多态
- 扩展、灵活性
- 类型适应
- 子类对象可以作为基类对象使用
-
Shape s;
Cirlce c;
s=c;
- 基类指针可以指向子类对象
-
Shape *p
p=&c
- 基类的引用可以引用子类对象
- 函数覆盖(override)
-
方法签名完全一样
如果方法签名不一样那是“重写”
- 多态
- 静态多态
- 动态多态
-
特征是:指针与引用、继承(inheritance)、覆盖(override)
- 虚函数
- 动态绑定的函数
// testPoly2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
class CShape {
protected:
int x;
int y;
int width;
public:
CShape() {}
~CShape() {}
virtual void draw() {
std::cout << "CShape draw method!\n";
}
};
class CRectlange :public CShape {
public:
virtual void draw() {
std::cout << "CRectlange draw method!\n";
}
};
class CCircle :public CShape {
public:
virtual void draw() { //override
std::cout << "CCircle draw method!\n";
}
};
// 通用的绘制图形方法
void commonDraw(CShape& shape) {
//todo set Pen color line style width....
shape.draw();
//todo do something
}
int main()
{
CShape* p_s;
CRectlange r;
CCircle c;
p_s = &r;
commonDraw(r);
commonDraw(c);
std::cout << "Hello World!\n";
}
多态的概念:
polymorphism:
the quality or state of being able to assume different form
- 同样的消息被不同类型对象接受时导致不同行为,消息即指对类的成员函数的调用,不同行为指不同实现,也就是说调用了不同的函数
-
静态多态:
ps: 静态多态往往通过函数重载和模板(泛型编程)来实现,具体可见下面代码:
#include <iostream>
using namespace std;
//两个函数构成重载
int add(int a, int b)
{
cout<<"in add_int_int()"<<endl;
return a + b;
}
double add(double a, double b)
{
cout<<"in add_double_doube()"<<endl;
return a + b;
}
//函数模板(泛型编程)
template <typename T>
T add(T a, T b)
{
cout<<"in func tempalte"<<endl;
return a + b;
}
int main()
{
cout<<add(1,1)<<endl; //调用int add(int a, int b)
cout<<add(1.1,1.1)<<endl; //调用double add(double a, double b)
cout<<add<char>('A',' ')<<endl; //调用模板函数,输出小写字母a
}
虚函数背后:
虚函数指针+虚函数表
Void *vptr virtual table point
纯虚函数:
纯虚函数是在基类中声明的,没有定义,且派生类必须实现的虚函数。纯虚函数允许派生类为其提供一个实现。
纯虚函数的声明形式如下:
Virtual void function_name()= 0;
其中,virtual 表示这是一个虚函数,function_name 是函数名,() 中的参数是函数的参数列表,= 0 表示这是一个纯虚函数。
如果一个基类中包含了纯虚函数,那么这个基类就是抽象类,不能被实例化。只有实现了纯虚函数的派生类才能被实例化。如果派生类没有实现纯虚函数,那么这个派生类也是抽象类,不能被实例化。
虚析构:
通过父类指针,释放子类空间。
构造顺序: 父类-->成员--> 子类
析构 :