C-编程实践指南-全-

C 编程实践指南(全)

原文:zh.annas-archive.org/md5/81d66d8ced8382f189c3169710939893

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书关于 C 编程语言,探讨了 C 语言的所有重要元素,如字符串、数组(包括一维和二维数组)、函数、指针、文件处理、线程、进程间通信、数据库处理、高级数据结构、图和图形。由于本书采用食谱方法,读者将找到独立解决他们在制作应用程序时通常遇到的不同问题的解决方案。到本书结束时,读者将具备足够的知识来使用 C 语言的高层和底层功能,并将能够将这些知识应用于制作实时应用程序。

本书面向对象

本书面向希望用 C 编程语言制作复杂和实时应用程序的中级到高级程序员和开发者。本书对于在制作应用程序时遇到数组、指针、函数、结构、文件、数据库、进程间通信、高级数据结构、图和图形等问题的培训师、教师和软件开发人员非常有用,他们希望看到示例以找到解决问题的方法。

本书涵盖内容

第一章,与数组一起工作,涵盖了数组的一些复杂但基本操作。你将学习如何将一个元素插入到数组中,乘以两个矩阵,找到两个数组中的公共元素,以及如何找到两个集合或数组之间的差异。此外,你还将学习如何找到数组中的唯一元素,并遇到一种帮助你判断给定矩阵是否为稀疏矩阵的技术。最后,我们将探讨将两个排序数组合并为一个数组的程序。

第二章,管理字符串,教你如何操纵字符串到字符的程度。你将学习如何判断一个给定的字符串是否为回文,如何找到一个字符串中第一个重复字符的出现,以及如何计算字符串中的每个字符。你还将学习如何计算字符串中的元音和辅音,以及将句子中的元音转换为大写的程序。

第三章,探索函数,探讨了函数在将大型应用程序分解为小型、独立且可管理的模块中所扮演的主要角色。在本章中,你将学习如何编写一个函数来判断提供的参数是否为阿姆斯特朗数。你还将学习函数如何返回一个数组,并编写一个使用递归找到两个数的最大公约数(gcd)的函数。你还将学习如何编写将二进制数转换为十六进制的函数。最后,你将学习编写一个判断提供的数字是否为回文的函数。

第四章,预处理和编译,涵盖了包括执行预处理和编译、使用指令执行条件编译、应用断言进行验证、使用编译时断言提前捕获错误、应用字符串化以及如何使用标记粘贴运算符在内的多个主题。

第五章,深入指针,展示了如何使用指针从特定的内存位置访问内容。你将学习如何使用指针反转字符串,如何使用指针在数组中找到最大值,以及如何对单链表进行排序。此外,本章还解释了如何使用指针找到矩阵的转置,以及如何使用指针访问结构体。

第六章,文件处理,探讨了在存储数据以供将来使用时,文件处理为何非常重要。在本章中,你将学习如何读取文本文件并将所有句号后面的字符转换为大写。此外,你还将学习如何以相反的顺序显示随机文件的内容,以及如何计算文本文件中的元音字母数量。本章还将展示如何将给定文本文件中的一个词替换为另一个词,以及如何确保文件不被未经授权的访问。你还将学习文件是如何被加密的。

第七章,实现并发,介绍了如何实现并发以提高 CPU 操作效率。在本章中,你将学习如何使用单个线程完成任务。你还将学习如何使用多个线程执行多个任务,并检查使用互斥锁在两个线程之间共享数据的技术。此外,你将熟悉可能导致死锁的情况,以及如何避免这种死锁情况。

第八章,网络和进程间通信,专注于如何在进程之间建立通信。你将学习如何使用管道在进程之间进行通信,如何使用 FIFO 在进程之间建立通信,以及如何使用套接字编程在客户端和服务器之间建立通信。你还将学习如何使用 UDP 套接字进行进程间通信,如何使用消息队列从一个进程传递消息到另一个进程,以及两个进程如何使用共享内存进行通信。

第九章,排序和搜索,涵盖了使用二分搜索进行搜索,使用冒泡排序对数字进行排序,以及插入排序、快速排序、堆排序、选择排序、归并排序、希尔排序和基数排序的使用。

第十章,与图一起工作,检查实现栈、双向链表、循环链表、队列、循环队列以及出队函数。你还将查看递归地执行二叉搜索树的顺序遍历,然后非递归地执行二叉树的后序遍历。

第十一章,高级数据结构和算法,探讨了使用邻接矩阵和邻接表表示图,如何进行图的广度优先和深度优先遍历,以及使用普里姆算法和克鲁斯卡尔算法创建最小生成树。

第十二章,图形创意,涵盖了制作不同的图形形状,在两次鼠标点击之间画线,制作条形图,以及动画弹跳球。

第十三章,使用 MySQL 数据库,考虑了没有在数据库中存储信息,任何实时应用都是不可能的。数据库中的信息需要根据需要管理和维护。在本章中,你将学习如何显示默认 MySQL 数据库中的所有内置表。你将看到如何在 MySQL 数据库中存储信息,并在数据库表中搜索所需信息。不仅如此;你还将学习如何在数据库表中更新信息,以及当不再需要时从数据库中删除数据的流程。

第十四章,通用工具,教你如何注册一个在程序退出时被调用的函数,以及如何测量函数执行中的时钟滴答,动态内存分配,以及处理信号。

第十五章,提高代码性能,专注于使用寄存器关键字,更快地获取输入,并应用循环展开以获得更快的性能。

第十六章,低级编程,探讨了将二进制数转换为十进制,使用内联汇编语言乘除两个数,以及使用位运算符和掩码寄存器的一些位将十进制值转换为二进制。

第十七章,嵌入式软件和物联网,展示了如何在嵌入式 C 中切换微控制器的端口,增加端口的值,在 Arduino 中切换电压,从串行端口获取输入,以及如何使用 Arduino 检测和记录温度。

第十八章,在编码中应用安全性,展示了如何避免缓冲区溢出,以及如何编写安全代码,避免字符串格式化时的错误,以及访问 C 文件时的漏洞。

要充分利用这本书

您需要具备一些 C 编程的初步知识。您需要了解数组、字符串、函数、文件处理、线程和进程间通信的基本知识。此外,为了处理数据库,您还需要了解基本的 SQL 命令。

下载示例代码文件

您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本的软件解压缩或提取文件夹:

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Practical-C-Programming。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 上找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781838641108_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在图中,1000 代表变量 i 的内存地址。”

代码块设置为如下:

 for(i=0;i<2;i++)
  {
    for(j=0;j<4;j++)
    {
      matR[i][j]=0;
      for(k=0;k<3;k++)
      {
        matR[i][j]=matR[i][j]+matA[i][k]*matB[k][j];
      }
    }
  }

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

printf("How many elements are there? ");
scanf("%d", &n);

任何命令行输入或输出都按照以下方式编写:

D:\CBook>reversestring
Enter a string: manish
Reverse string is hsinam

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“只需单击“下一步”按钮即可继续。”

警告或重要注意事项看起来像这样。

小技巧和窍门看起来像这样。

章节

在本书中,您会发现一些频繁出现的标题(如何做它是如何工作的)。

要获得完成食谱的清晰说明,请按照以下方式使用这些部分:

如何做…

本节包含遵循食谱所需的步骤。

它是如何工作的…

本节详细解释了前节中遵循的步骤。

还有更多…

当本节存在时,它包含有关食谱的附加信息,以增强你对食谱的了解。

相关内容

当本节存在时,它提供了对其他有用信息的链接,以帮助了解食谱。

联系我们

欢迎读者反馈

一般反馈: 请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果你对本书的任何方面有疑问,请通过questions@packtpub.com给我们发送电子邮件。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这个错误。请访问www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果你在网上任何形式下发现了我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。

如果你有兴趣成为作者: 如果你有一个你擅长的主题,并且你感兴趣于撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

一旦你阅读并使用了这本书,为何不在你购买它的网站上留下评论呢?潜在的读者可以查看并使用你的客观意见来做出购买决定,我们 Packt 公司可以了解你对我们的产品有何看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问packtpub.com

第一章:操作数组

数组是任何编程语言的重要结构。为了将相似类型的数据放在一起,我们需要数组。数组在需要随机访问元素的应用中被大量使用。当您需要排序元素、在集合中查找所需数据或查找两个集合之间的公共或唯一数据时,数组也是一个主要的选择。数组分配了连续的内存位置,并且是排序和搜索数据集合的非常流行的结构,因为可以通过简单地指定其下标或索引位置来访问数组的任何元素。本章将涵盖包括常见数组操作的菜谱。

在本章中,我们将学习如何使用数组制作以下菜谱:

  • 在一维数组中插入一个元素

  • 乘以两个矩阵

  • 查找两个数组中的公共元素

  • 查找两个集合或数组之间的差异

  • 查找数组中的唯一元素

  • 查找矩阵是否为稀疏

  • 将两个有序数组合并为一个

让我们从第一个菜谱开始!

在数组中插入一个元素

在这个菜谱中,我们将学习如何在数组中插入一个元素。您可以定义数组的长度,也可以指定新值要插入的位置。程序将在值插入后显示数组。

如何做到这一点…

  1. 假设有一个名为 p 的数组,包含五个元素,如下所示:

图 1.1

现在,假设你想在第三个位置输入一个值,比如 99。我们将编写一个 C 程序,它将给出以下输出:

图 1.2

以下是插入数组中元素的步骤:

  1. 定义一个名为 max 的宏并将其初始化为 100 的值:
#define max 100
  1. 定义一个大小为最大元素数量的数组 p
int p[max]
  1. 当提示输入数组长度时,输入的长度将被分配给变量 n
printf("Enter length of array:");
scanf("%d",&n);
  1. 将执行一个 for 循环,提示您输入数组的元素:
for(i=0;i<=n-1;i++ )
    scanf("%d",&p[i]);
  1. 指定新值需要插入的数组中的位置:
printf("\nEnter position where to insert:");
scanf("%d",&k);
  1. 因为 C 语言中的数组是从零开始的,所以输入的位置会减去 1:
k--;
  1. 为了在指定的索引位置为新元素创建空间,所有元素都会向下移动一个位置:
for(j=n-1;j>=k;j--)
    p[j+1]=p[j];
  1. 输入将插入到空缺索引位置的新值:
printf("\nEnter the value to insert:");
scanf("%d",&p[k]);

这里是用于在数组中插入元素的 insertintoarray.c 程序:

#include<stdio.h>
#define max 100
void main()
{
    int p[max], n,i,k,j;
    printf("Enter length of array:");
    scanf("%d",&n);
    printf("Enter %d elements of array\n",n);
    for(i=0;i<=n-1;i++ )
        scanf("%d",&p[i]);
    printf("\nThe array is:\n");
    for(i = 0;i<=n-1;i++)
        printf("%d\n",p[i]);
    printf("\nEnter position where to insert:");
    scanf("%d",&k);
    k--;/*The position is always one value higher than the subscript, so it is decremented by one*/             
    for(j=n-1;j>=k;j--)
        p[j+1]=p[j];
    /* Shifting all the elements of the array one position down from the location of insertion */
    printf("\nEnter the value to insert:");
    scanf("%d",&p[k]);
    printf("\nArray after insertion of element: \n");
    for(i=0;i<=n;i++)
        printf("%d\n",p[i]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

因为我们需要指定数组的长度,所以我们首先定义一个名为 max 的宏并将其初始化为 100 的值。我之所以将 max 的值定义为 100,是因为我假设我不会在数组中输入超过 100 个值,但可以设置为所需的任何值。定义了一个大小为 max 个元素的数组 p。你将被提示指定数组的长度。让我们将数组的长度指定为 5。我们将把值 5 赋给变量 n。使用 for 循环,你将被要求输入数组的元素。

假设你输入的数组值如之前给出的 图 1.1 所示:

在前面的图中,数字 0、1、2 等被称为索引或下标,用于从数组中分配和检索值。接下来,你将被要求指定新值需要插入到数组中的位置。假设,你输入 3,它被分配给变量 k。这意味着你想要在数组的 3 号位置插入一个新值。

因为 C 语言中的数组是从 0 开始计数的,所以位置 3 表示你想要在索引位置 2 插入一个新的值,即 p[2]。因此,在 k 中输入的位置会减去 1。

为了在索引位置 p[2] 为新元素腾出空间,所有元素都将向下移动一个位置。这意味着 p[4] 位置的元素将被移动到索引位置 p[5]p[3] 位置的元素将被移动到 p[4],而 p[2] 位置的元素将被移动到 p[3],如下所示:

图 1.3

一旦目标索引位置的元素安全地复制到下一个位置,你将被要求输入新的值。假设你输入的新值为 99;该值将被插入到索引位置 p[2],如之前给出的 图 1.2 所示:

让我们使用 GCC 编译 insertintoarray.c 程序,如下所示:

D:\CBook>gcc insertintoarray.c -o insertintoarray

现在,让我们运行生成的可执行文件 insertintoarray.exe,以查看程序输出:

D:\CBook>./insertintoarray
Enter length of array:5
Enter 5 elements of array
10
20
30
40
50

The array is:
10
20
30
40
50

Enter target position to insert:3
Enter the value to insert:99
Array after insertion of element:
10
20
99
30
40
50

哇!我们已经成功地在数组中插入了一个元素。

还有更多...

如果我们想要从数组中删除一个元素怎么办?程序流程是简单的反向;换句话说,数组底部的所有元素将被复制一个位置,以替换被删除的元素。

假设数组 p 有以下五个元素 (图 1.1):

假设,我们想要从数组中删除第三个元素,换句话说,就是位于 p[2] 的元素。为此,p[3] 位置的元素将被复制到 p[2]p[4] 位置的元素将被复制到 p[3],而最后一个元素,在这里是位于 p[4] 的,将保持不变:

图 1.4

用于删除数组的 deletefromarray.c 程序如下:

#include<stdio.h>
void main()
{
    int p[100],i,n,a;
    printf("Enter the length of the array: ");
    scanf("%d",&n);
    printf("Enter %d elements of the array \n",n);
    for(i=0;i<=n-1;i++)
        scanf("%d",&p[i]);
    printf("\nThe array is:\n");\
    for(i=0;i<=n-1;i++)
        printf("%d\n",p[i]);
    printf("Enter the position/location to delete: ");
    scanf("%d",&a);
    a--;
    for(i=a;i<=n-2;i++)
    {
        p[i]=p[i+1];
        /* All values from the bottom of the array are shifted up till 
        the location of the element to be deleted */
    }
    p[n-1]=0;
    /* The vacant position created at the bottom of the array is set to 
    0 */
    printf("Array after deleting the element is\n");
    for(i=0;i<= n-2;i++)
        printf("%d\n",p[i]);
}

现在,让我们继续下一个菜谱!

乘以两个矩阵

乘以两个矩阵的前提是第一个矩阵的列数必须等于第二个矩阵的行数。

如何做到这一点…

  1. 创建两个矩阵,每个矩阵的顺序为 2 x 33 x 4

  2. 在我们编写矩阵乘法程序之前,我们需要了解矩阵乘法是如何手动执行的。为了做到这一点,让我们假设要相乘的两个矩阵具有以下元素:

图 1.5

  1. 结果矩阵的顺序将是 2 x 4,也就是说,结果矩阵将具有与第一个矩阵相同的行数和与第二个矩阵相同的列数:

图 1.6

实质上,2 x 4 顺序的结果矩阵将具有以下元素:

图 1.7

  1. 结果矩阵中第一行第一列的元素使用以下公式计算:

求和(第一个矩阵第一行的第一个元素 × 第二个矩阵第一列的第一个元素),(第一行第二个元素... × 第一列第二个元素...),(以此类推...)

例如,假设两个矩阵的元素如图 图 1.5 所示。结果矩阵的第一行第一列的元素将按以下方式计算:

图 1.8

  1. 因此,结果矩阵中第一行第一列的元素如下:

(3×6)+(9×3)+(7×5)

=18 + 27 + 35

=80

图 1.9 解释了结果矩阵中其余元素的计算方法:

图 1.9

两个矩阵相乘的 matrixmulti.c 程序如下:

#include  <stdio.h>
int main()
{
  int matA[2][3], matB[3][4], matR[2][4];
  int i,j,k;
  printf("Enter elements of the first matrix of order 2 x 3 \n");
  for(i=0;i<2;i++)
  {
    for(j=0;j<3;j++)
    {
      scanf("%d",&matA[i][j]);
    }
  }
  printf("Enter elements of the second matrix of order 3 x 4 \n");
  for(i=0;i<3;i++)
  {
    for(j=0;j<4;j++)
    {
      scanf("%d",&matB[i][j]);
    }
  }
  for(i=0;i<2;i++)
  {
    for(j=0;j<4;j++)
    {
      matR[i][j]=0;
      for(k=0;k<3;k++)
      {
        matR[i][j]=matR[i][j]+matA[i][k]*matB[k][j];
      }
    }
  }
  printf("\nFirst Matrix is \n");
  for(i=0;i<2;i++)
  {
    for(j=0;j<3;j++)
    {
      printf("%d\t",matA[i][j]);
    }
    printf("\n");
  }
  printf("\nSecond Matrix is \n");
  for(i=0;i<3;i++)
  {
    for(j=0;j<4;j++)
    {
      printf("%d\t",matB[i][j]);
    }
    printf("\n");
  }
  printf("\nMatrix multiplication is \n");
  for(i=0;i<2;i++)
  {
    for(j=0;j<4;j++)
    {
      printf("%d\t",matR[i][j]);
    }
    printf("\n");
  }
  return 0;
}

现在,让我们深入幕后,更好地理解代码。

它是如何工作的...

使用以下语句定义了两个矩阵 matAmatB,它们的顺序分别为 2 x 3 和 3 x 4:

int matA[2][3], matB[3][4]

您将被要求使用嵌套的 for 循环输入两个矩阵的元素。矩阵中的元素以行主序输入,换句话说,首先输入第一行的所有元素,然后是第二行的所有元素,依此类推。

在嵌套循环中,for ifor j,外循环 for i 代表行,内循环 for j 代表列。

在输入矩阵 matAmatB 的元素时,输入的两个矩阵中的值将被分配到二维数组的相应索引位置,如下所示:

图 1.10

实际计算矩阵乘法的嵌套循环如下:

  for(i=0;i<2;i++)
  {
    for(j=0;j<4;j++)
    {
      matR[i][j]=0;
      for(k=0;k<3;k++)
      {
        matR[i][j]=matR[i][j]+matA[i][k]*matB[k][j];
      }
    }
  }

变量 i 代表结果矩阵的行,j 代表结果矩阵的列,k 代表公共因子。这里的 公共因子 指的是第一个矩阵的列和第二个矩阵的行。

回想一下,矩阵乘法的先决条件是第一个矩阵的列数应该与第二个矩阵的行数相同。因为相应的元素在乘法后需要相加,所以元素在相加之前必须初始化为 0

以下语句初始化结果矩阵的元素:

      matR[i][j]=0;

嵌套循环中的 for k 循环有助于选择第一个矩阵的行元素,并将它们与第二个矩阵的列元素相乘:

matR[i][j]=matR[i][j]+matA[i][k]*matB[k][j];

让我们使用 GCC 编译 matrixmulti.c 程序,如下所示:

D:\CBook>gcc matrixmulti.c -o matrixmulti

让我们运行生成的可执行文件 matrixmulti.exe,以查看程序的输出:

D:\CBook\Chapters\1Arrays>./matrixmulti

Enter elements of the first matrix of order 2 x 3
3
9
7
1
5
4

Enter elements of the second matrix of order 3 x 4
6 2 8 1
3 9 4 0
5 3 1 3

First Matrix is
3 9 7 
1 5 4

Second Matrix is
6 2 8 1
3 9 4 0
5 3 1 3

Matrix multiplication is
80 108 67 24
41 59 32 13

哇!我们已经成功地将两个矩阵相乘了。

还有更多...

当你输入矩阵的元素时,你可能注意到有两种方法可以做到这一点。

  1. 第一种方法是你在输入每个元素后按 Enter
3
9
7
1
5
4

这些值将自动按照行主序分配到矩阵中,换句话说,3 将分配给 matA[0][0]9 将分配给 matA[0][1],依此类推。

  1. 在矩阵中输入元素的第二种方法是:
6 2 8 1
3 9 4 0
5 3 1 3

在这里,6 将分配给 matB[0][0]2 将分配给 matB[0][1],依此类推。

现在,让我们继续下一个菜谱!

在两个数组中找到公共元素

在两个数组中找到公共元素类似于找到两个集合的交集。让我们学习如何做到这一点。

如何做到这一点...

  1. 定义两个特定大小的数组,并将你选择的元素分配给这两个数组。假设我们创建了两个名为 pq 的数组,它们都有四个元素:

图 1.11

  1. 定义另一个数组。让我们称它为数组 r,用于存储两个数组之间的公共元素。

  2. 如果数组 p 中的一个元素存在于数组 q 中,它将被添加到数组 r 中。例如,如果数组 p 中第一个位置的元素,即 p[0],不在数组 q 中,它将被丢弃,下一个元素,即 p[1],将被选中进行比较。

  3. 如果数组 p 中的 p[0] 元素在数组 q 中任何位置找到,它将被添加到数组 r 中,如下所示:

图 1.12

  1. 这个过程会与其他数组 q 的元素重复。也就是说,p[1]q[0]q[1]q[2]q[3] 进行比较。如果 p[1] 在数组 q 中找不到,那么在直接将其插入数组 r 之前,它会与数组 r 中现有的元素进行比较,以避免重复元素。

  2. 因为数组 p 中的元素 p[1] 出现在数组 q 中,并且尚未存在于数组 r 中,所以它按照以下方式添加到数组 r 中:

图片

图 1.13

建立两个数组之间公共元素的 commoninarray.c 程序如下:

#include<stdio.h>
#define max 100

int ifexists(int z[], int u, int v)
{
    int i;
    if (u==0) return 0;
    for (i=0; i<=u;i++)
        if (z[i]==v) return (1);
    return (0);
}
void main()
{
    int p[max], q[max], r[max];
    int m,n;
    int i,j,k;
    k=0;
    printf("Enter the length of the first array:");
    scanf("%d",&m);
    printf("Enter %d elements of the first array\n",m);
    for(i=0;i<m;i++ )
        scanf("%d",&p[i]);
    printf("\nEnter the length of the second array:");
    scanf("%d",&n);
    printf("Enter %d elements of the second array\n",n);
    for(i=0;i<n;i++ )
        scanf("%d",&q[i]);
    k=0;
    for (i=0;i<m;i++)
    {
        for (j=0;j<n;j++)
        {
           if (p[i]==q[j])
           {
               if(!ifexists(r,k,p[i]))
               {
                   r[k]=p[i];
                   k++;
               }
            }
        }
    }
    if(k>0)
    {
        printf("\nThe common elements in the two arrays are:\n");
        for(i = 0;i<k;i++)
            printf("%d\n",r[i]);
    }
    else
        printf("There are no common elements in the two arrays\n");
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

定义了一个大小为 100 的宏 max。定义了一个函数 ifexists(),该函数简单地返回 true (1)false (0)。如果提供的值存在于指定的数组中,则函数返回 true,如果不存在,则返回 false

定义了两个大小为 max(换句话说,100 个元素)的数组,分别称为 pq。您将被提示指定数组 p 的长度,然后输入该数组的元素。之后,您将被要求指定数组 q 的长度,然后输入数组 q 的元素。

然后,选择数组 p 中的第一个元素 p[0],并使用 for 循环将其与数组 q 的所有元素进行比较。如果 p[0] 在数组 q 中找到,则将 p[0] 添加到结果数组 r 中。

比较完 p[0] 后,将选择数组 p 的第二个元素 p[1] 并将其与数组 q 的所有元素进行比较。这个过程会一直重复,直到数组 p 的所有元素都与数组 q 的所有元素进行比较。

如果数组 p 的任何元素在数组 q 中找到,则在将其添加到结果数组 r 之前,该元素会通过 ifexists() 函数运行以确保该元素尚未存在于数组 r 中。这是因为我们不希望在数组 r 中有重复的元素。

最后,所有在数组 r 中的元素,即两个数组的公共元素,都会显示在屏幕上。

让我们使用 GCC 按以下方式编译 commoninarray.c 程序:

D:\CBook>gcc commoninarray.c -o commoninarray

现在,让我们运行生成的可执行文件 commoninarray.exe,以查看程序输出:

D:\CBook>./commoninarray
Enter the length of the first array:5
Enter 5 elements in the first array
1
2
3
4
5

Enter the length of the second array:4
Enter 4 elements in the second array
7
8
9
0

There are no common elements in the two arrays

由于之前输入的两个数组之间没有公共元素,所以我们不能完全说我们已经真正测试了程序。让我们再次运行程序,这次我们将输入具有共同元素的数组元素。

D:\CBook>./commoninarray
Enter the length of the first array:4
Enter 4 elements in the first array
1
2
3
4

Enter the length of the second array:4
Enter 4 elements in the second array
1
4
1
2

The common elements in the two arrays are:
1
2
4

哇!我们已经成功识别出两个数组之间的公共元素。

查找两个集合或数组之间的差异

当我们谈论两个集合或数组之间的差异时,我们指的是第一个数组中不出现在第二个数组中的所有元素。本质上,第一个数组中不属于第二个数组的所有元素被称为两个集合的差异。例如,集合 pq 的差异将表示为 p – q

例如,如果数组 p 有元素 {1, 2, 3, 4},而数组 q 有元素 {2, 4, 5, 6},那么两个数组的差异 p - q 将是 {1,3}。让我们看看这是如何实现的。

如何做到这一点...

  1. 定义两个数组,例如 pq,并分配你选择的元素给这两个数组。

  2. 定义一个额外的数组,例如 r,用于存储代表两个数组之间差异的元素。

  3. 从数组 p 中取一个元素,并与数组 q 的所有元素进行比较。

  4. 如果数组 p 的元素存在于数组 q 中,则舍弃该元素,取数组 p 的下一个元素,并从步骤 3 重新开始。

  5. 如果数组 p 的元素不在数组 q 中,则将该元素添加到数组 r 中。在将该元素添加到数组 r 之前,请确保它尚未存在于数组 r 中。

  6. 重复步骤 3 到 5,直到比较完数组 p 的所有元素。

  7. 在数组 r 中显示所有元素,因为这些元素代表了数组 pq 之间的差异。

建立两个数组之间差异的 differencearray.c 程序如下:

#include<stdio.h>
#define max 100

int ifexists(int z[], int u, int v)
{
    int i;
    if (u==0) return 0;
    for (i=0; i<=u;i++)
        if (z[i]==v) return (1);
    return (0);
}

void main()
{
    int p[max], q[max], r[max];
    int m,n;
    int i,j,k;
    printf("Enter length of first array:");
    scanf("%d",&m);
    printf("Enter %d elements of first array\n",m);
    for(i=0;i<m;i++ )
        scanf("%d",&p[i]);
    printf("\nEnter length of second array:");
    scanf("%d",&n);
    printf("Enter %d elements of second array\n",n);
    for(i=0;i<n;i++ )                                                                                    scanf("%d",&q[i]);
    k=0;
    for (i=0;i<m;i++)               
    {                                
        for (j=0;j<n;j++)                                
        {
            if (p[i]==q[j])
            {                                                                                                                                    break;                                                   
            }
        }
        if(j==n)
        {
            if(!ifexists(r,k,p[i]))                                               
            {
                r[k]=p[i];
                k++;
            }
        }
    }
    printf("\nThe difference of the two array is:\n");
    for(i = 0;i<k;i++)
        printf("%d\n",r[i]);
}

现在,让我们深入了解代码,以便更好地理解它。

它是如何工作的...

我们定义了两个数组,分别称为 pq。我们不想固定这些数组的长度,因此应该定义一个名为 max 的宏,其值为 100,并将两个数组 pq 设置为 max 的大小。

此后,你将被提示指定第一个数组的大小并输入第一个数组 p 的元素。同样,你将被要求指定第二个数组 q 的长度,然后输入第二个数组的元素。

假设你已指定两个数组的长度为 4,并已输入以下元素:

图 1.14

我们需要一次从第一个数组中取一个元素,并与第二个数组的所有元素进行比较。如果数组 p 中的元素不在数组 q 中,它将被分配到我们创建的第三个数组,即数组 r

数组 r 将用于存储定义两个数组之间差异的元素。如图 图 1.15 所示,数组 p 的第一个元素,即 p[0],将与数组 q 的所有元素进行比较,即与 q[0]q[1]q[2]q[3]

因为位于 **p[0]** 的元素,即 **1**,没有出现在数组 **q** 中,所以它将被添加到数组 r 中,表示两个数组之间的第一个差异元素:

图 1.15

因为位于 **p[1]** 的元素,即 **2**,出现在数组 **q** 中,所以它被舍弃,然后取数组 **p** 中的下一个元素,即 **p[2]**,并与数组 **q** 中的所有元素进行比较。

由于位于 **p[2]** 的元素没有出现在数组 **q** 中,它将被添加到数组 r 的下一个可用位置,即 r[1](如下 图 1.16 所示):

图 1.16

继续执行此过程,直到数组 p 的所有元素都与数组 q 的所有元素进行比较。最后,我们将得到数组 r,其元素显示了我们的两个数组 pq 之间的差异。

让我们使用 GCC 编译我们的程序,differencearray.c,如下所示:

D:\CBook>gcc differencearray.c -o differencearray

现在,让我们运行生成的可执行文件,differencearray,以查看程序的输出:

D:\CBook>./differencearray
Enter length of first array:4
Enter 4 elements of first array
1
2
3
4
Enter length of second array:4
Enter 4 elements of second array
2
4
5
6
The difference of the two array is:
1
3

哇!我们已经成功找到了两个数组之间的差异。现在,让我们继续下一个菜谱!

在数组中查找唯一元素

在这个菜谱中,我们将学习如何查找数组中的唯一元素,以便数组中的重复元素只显示一次。

如何做到这一点…

  1. 定义两个大小一定的数组 pq,并将元素仅分配给数组 p。我们将数组 q 留空。

  2. 这些将分别是我们的源数组和目标数组。目标数组将包含源数组的唯一元素。

  3. 之后,源数组中的每个元素都将与目标数组中现有的元素进行比较。

  4. 如果源数组中的元素存在于目标数组中,则该元素将被丢弃,并从源数组中取出下一个元素进行比较。

  5. 如果源数组元素不在目标数组中,它将被复制到目标数组中。

  6. 假设数组 p 包含以下重复元素:

图 1.17

  1. 我们将首先将源数组 p 的第一个元素复制到目标数组 q 中,换句话说,p[0] 复制到数组 q[0],如下所示:

图 1.18

  1. 接下来,比较 p 的第二个数组元素,换句话说,p[1],与数组 q 中所有现有的元素。也就是说,p[1] 与数组 q 进行比较,以检查它是否已经存在于数组 q 中,如下所示:

图 1.19

  1. 因为 p[1] 不存在于数组 q 中,所以它被复制到 q[1],如图 1.20 所示:

图 1.20

  1. 此过程会重复进行,直到数组 p 的所有元素都与数组 q 进行比较。最后,我们将得到数组 q,它将包含数组 p 的唯一元素。

这是用于在第一个数组中查找唯一元素的 uniqueelements.c 程序:

#include<stdio.h>
#define max 100

int ifexists(int z[], int u, int v)
{
    int i;
    for (i=0; i<u;i++)
        if (z[i]==v) return (1);
    return (0);
}

void main()
{
    int p[max], q[max];
    int m;
    int i,k;
    k=0;
    printf("Enter length of the array:");
    scanf("%d",&m);
    printf("Enter %d elements of the array\n",m);
    for(i=0;i<m;i++ )
        scanf("%d",&p[i]);
    q[0]=p[0];
    k=1;
    for (i=1;i<m;i++)
    {
        if(!ifexists(q,k,p[i]))
        {
            q[k]=p[i];
            k++;
        }
    }
    printf("\nThe unique elements in the array are:\n");
    for(i = 0;i<k;i++)
        printf("%d\n",q[i]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们将定义一个大小为 100 的宏 max。定义两个大小为 max 的数组 pq。数组 p 将包含原始元素,而数组 q 将包含数组 p 的唯一元素。您将被提示输入数组的长度,然后使用 for 循环,将数组元素接受并分配给数组 p

以下语句将数组p的第一个元素赋值给空白数组q的第一个索引位置,我们将该数组命名为q

q[0]=p[0]

再次使用for循环逐个访问数组p的其余元素。首先,数组p的第一个元素,即p[0],被复制到数组qq[0]位置。

接下来,比较第二个数组p的元素p[1]与数组q中所有现有的元素。也就是说,p[1]被检查是否已经存在于数组q中。

因为数组q中只有一个元素,所以p[1]q[0]进行比较。由于p[1]不在数组q中,它被复制到q[1]

这个过程会重复进行,直到数组p中的所有元素都被检查和比较。数组p访问到的每个元素都会通过ifexists()函数来检查它们是否已经存在于数组q中。

如果数组p中的元素已经在数组q中,函数将返回1。在这种情况下,数组p中的元素将被丢弃,下一个数组元素将被选中进行比较。

如果ifexists()函数返回0,确认数组p中的元素不在数组q中,则数组p的元素将被添加到数组q的下一个可用索引/下标位置。

当检查并比较数组p的所有元素后,数组q将只包含数组p的唯一元素。

让我们使用 GCC 编译uniqueelements.c程序,如下所示:

D:\CBook>gcc uniqueelements.c -o uniqueelements

现在,让我们运行生成的可执行文件uniqueelements.exe,以查看程序的输出:

D:\CBook>./uniqueelements
Enter the length of the array:5
Enter 5 elements in the array
1
2
3
2
1

The unique elements in the array are:
1
2
3

哇!我们已经成功识别了数组中的唯一元素。现在,让我们继续下一个菜谱!

判断矩阵是否为稀疏

当一个矩阵的零值多于非零值时(非零值多时为密集矩阵),它被认为是稀疏矩阵。在这个菜谱中,我们将学习如何判断指定的矩阵是否为稀疏。

如何做到这一点…

  1. 首先,指定矩阵的阶数。然后,您将被提示输入矩阵中的元素。假设您指定了矩阵的阶数为 4 x 4。在输入矩阵元素后,它可能看起来像这样:

图片

图 1.21

  1. 一旦输入了矩阵的元素,就计算其中的零的数量。为此,初始化一个计数器为0。使用嵌套循环,扫描矩阵中的每个元素,并在找到任何零元素时,将计数器的值增加 1。

  2. 此后,使用以下公式来确定矩阵是否为稀疏。

如果计数器 > [(行数 x 列数) / 2] = 稀疏矩阵

  1. 根据前面公式的结果,屏幕上会显示以下消息之一:
The given matrix is a sparse matrix

或者

The given matrix is not a sparse matrix

用于确定矩阵是否为稀疏的sparsematrix.c程序如下:

#include <stdio.h>
#define max 100

/*A sparse matrix has more zero elements than nonzero elements */
void main ()
{
    static int arr[max][max];
    int i,j,r,c;
    int ctr=0;
    printf("How many rows and columns are in this matrix? ");
    scanf("%d %d", &r, &c);
    printf("Enter the elements in the matrix :\n");
    for(i=0;i<r;i++)
    {
        for(j=0;j<c;j++)
        {
            scanf("%d",&arr[i][j]);
            if (arr[i][j]==0)
                ++ctr;
        }
    }
    if (ctr>((r*c)/2))
        printf ("The given matrix is a sparse matrix. \n");
    else
        printf ("The given matrix is not a sparse matrix.\n");
    printf ("There are %d number of zeros in the matrix.\n",ctr);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

因为我们不想固定矩阵的大小,我们将定义一个名为 max 的宏,其值为 100。定义了一个矩阵,或称为二维数组 arr,其阶数为 max x max。你将被提示输入矩阵的阶数,你可以输入任何 100 以内的值。

假设你已经指定了矩阵的阶数为 4 x 4。你将被提示输入矩阵的元素。矩阵中输入的值将按照行主序排列。输入元素后,矩阵 arr 应该看起来像 图 1.22,如下所示:

图 1.22

创建了一个名为 ctr 的计数器,并将其初始化为 0。使用嵌套循环,检查矩阵 arr 的每个元素,如果发现任何元素是 0,则将 ctr 的值增加。之后,使用 if else 语句检查零值的数量是否多于非零值。如果零值的数量多于非零值,则将在屏幕上显示以下消息:

The given matrix is a sparse matrix

然而,如果没有满足这些条件,屏幕上将会显示以下消息:

The given matrix is not a sparse matrix

让我们使用 GCC 编译 sparsematrix.c 程序,如下所示:

D:\CBook>gcc sparsematrix.c -o sparsematrix

让我们运行生成的可执行文件,sparsematrix.exe,以查看程序的输出:

D:\CBook>./sparsematrix
How many rows and columns are in this matrix? 4 4
Enter the elements in the matrix :
0 1 0 0
5 0 0 9
0 0 3 0
2 0 4 0
The given matrix is a sparse matrix.
There are 10 zeros in the matrix.

好的。让我们再次运行程序,以查看非零值数量更多时的输出:

D:\CBook>./sparsematrix
How many rows and columns are in this matrix? 4 4
Enter the elements in the matrix:
1 0 3 4
0 0 2 9
8 6 5 1
0 7 0 4
The given matrix is not a sparse matrix.
There are 5 zeros in the matrix.

哇!我们已经成功识别了一个稀疏矩阵和一个非稀疏矩阵。

还有更多...

那么,如何找到一个单位矩阵,换句话说,判断用户输入的矩阵是否为单位矩阵呢?让我告诉你——如果一个矩阵是一个方阵,且主对角线上的所有元素都是 1,而其他所有元素都是 0,那么这个矩阵就是一个单位矩阵。一个 3 x 3 阶的单位矩阵可能如下所示:

图 1.23

在前面的图中,你可以看到矩阵的主对角线元素是 1,其余元素都是 0。主对角线元素的索引或下标位置将是 arr[0][0]arr[1][1]arr[2][2],因此按照以下步骤来检查矩阵是否是单位矩阵:

  • 检查行和列的索引位置是否相同,换句话说,如果行号是 0,列号也是 0,那么在该索引位置 [0][0],矩阵元素必须是 1。同样,如果行号是 1,列号也是 1,即在 [1][1] 索引位置,矩阵元素必须是 1

  • 验证矩阵在其他所有索引位置上的元素都是 0

如果满足上述两个条件,则该矩阵是单位矩阵,否则不是。

以下是一个 identitymatrix.c 程序,用于判断输入的矩阵是否为单位矩阵:

    #include <stdio.h>
#define max 100
/* All the elements of the principal diagonal of the  Identity matrix  are ones and rest all are zero elements  */
void main ()
{
    static int arr[max][max];
    int i,j,r,c, bool;
    printf("How many rows and columns are in this matrix ? ");
    scanf("%d %d", &r, &c);
    if (r !=c)
    {
        printf("An identity matrix is a square matrix\n");
        printf("Because this matrix is not a square matrix, so it is not an 
           identity matrix\n");
    }
    else
    {
        printf("Enter elements in the matrix :\n");
        for(i=0;i<r;i++)
        {
            for(j=0;j<c;j++)
            {
                scanf("%d",&arr[i][j]);
            }
        }
        printf("\nThe entered matrix is \n");
        for(i=0;i<r;i++)
        {
            for(j=0;j<c;j++)
            {
                printf("%d\t",arr[i][j]);
            }
            printf("\n");
        }
        bool=1;
        for(i=0;i<r;i++)
        {
            for(j=0;j<c;j++)
            {
                if(i==j)
                {
                    if(arr[i][j] !=1)
                    {
                        bool=0;
                        break;
                    }
                }
                else
                {
                    if(arr[i][j] !=0)
                    {
                        bool=0;
                        break;
                    }
                }
            }
        }
        if(bool)
            printf("\nMatrix is an identity matrix\n");                             
        else 
            printf("\nMatrix is not an identity matrix\n");                
    }
}

让我们使用 GCC 编译 identitymatrix.c 程序,如下所示:

D:\CBook>gcc identitymatrix.c -o identitymatrix

没有生成错误。这意味着程序编译完美,生成了一个可执行文件。让我们运行生成的可执行文件。首先,我们将输入一个非方阵:

D:\CBook>./identitymatrix
How many rows and columns are in this matrix ? 3 4
An identity matrix is a square matrix 
Because this matrix is not a square matrix, so it is not an identity matrix

现在,让我们再次运行程序;这次,我们将输入一个方阵

D:\CBook>./identitymatrix 
How many rows and columns are in this matrix ? 3 3 
Enter elements in the matrix : 
1 0 1 
1 1 0 
0 0 1 

The entered matrix is 
1       0       1 
1       1       0 
0       0       1 

Matrix is not an identity matrix

因为前面矩阵中的非对角线元素是 1,它不是一个单位矩阵。让我们再次运行程序:

D:\CBook>./identitymatrix 
How many rows and columns are in this matrix ? 3 3
Enter elements in the matrix :
1 0 0
0 1 0
0 0 1
The entered matrix is
1       0       0
0       1       0
0       0       1
Matrix is an identity matrix

现在,让我们继续下一个菜谱!

将两个排序数组合并成一个数组

在这个菜谱中,我们将学习如何合并两个排序数组,以便生成的合并数组也是排序的。

如何做...

  1. 假设有两个长度一定的数组 pq。两个数组的长度可以不同。它们都包含一些排序元素,如图 1.24 所示:

图片

图 1.24

  1. 从前面两个数组的排序元素中创建的合并数组将被称为数组 r。将使用三个下标或索引位置来指向三个数组中的相应元素。

  2. 下标 i 将用于指向数组 p 的索引位置。下标 j 将用于指向数组 q 的索引位置,下标 k 将用于指向数组 r 的索引位置。一开始,所有三个下标都将初始化为 0

  3. 将应用以下三个公式来获取合并的排序数组:

    1. 将比较 p[i] 中的元素与 q[j] 中的元素。如果 p[i] 小于 q[j],则将 p[i] 分配给数组 r,并增加数组 pr 的索引,以便选择数组 p 的下一个元素进行比较,如下所示:
r[k]=p[i];
i++;
k++
  1. 如果 q[j] 小于 p[i],则将 q[j] 分配给数组 r,并增加数组 qr 的索引,以便选择数组 q 的下一个元素进行比较,如下所示:
r[k]=q[j];
i++;
k++
  1. 如果 p[i] 等于 q[j],则两个元素都分配给数组 rp[i] 被添加到 r[k]ik 索引的值增加。q[j] 也被添加到 r[k]qr 数组的索引增加。请参考以下代码片段:
r[k]=p[i];
i++;
k++
r[k]=q[j];
i++;
k++
  1. 这个过程将重复进行,直到任一数组结束。如果任一数组结束,另一个数组的剩余元素将简单地追加到数组 r 中。

合并两个排序数组 mergetwosortedarrays.c 程序如下:

#include<stdio.h>
#define max 100

void main()
{
    int p[max], q[max], r[max];
    int m,n;
    int i,j,k;
    printf("Enter length of first array:");
    scanf("%d",&m);
    printf("Enter %d elements of the first array in sorted order     
    \n",m);
    for(i=0;i<m;i++)
        scanf("%d",&p[i]);
    printf("\nEnter length of second array:");
    scanf("%d",&n);
    printf("Enter %d elements of the second array in sorted 
    order\n",n);
    for(i=0;i<n;i++ )
        scanf("%d",&q[i]);
    i=j=k=0;
    while ((i<m) && (j <n))
    {
        if(p[i] < q[j])
        {
            r[k]=p[i];
            i++;
            k++;
        }
        else
        {
            if(q[j]< p[i])
            {
                r[k]=q[j];
                k++;
                j++;
            }
            else
            {
                r[k]=p[i];
                k++;
                i++;
                r[k]=q[j];
                k++;
                j++;
            }
        }
    }
    while(i<m)
    {
        r[k]=p[i];
        k++;
        i++;
    }
    while(j<n)
    {
        r[k]=q[j];
        k++;
        j++;
    }
    printf("\nThe combined sorted array is:\n");
    for(i = 0;i<k;i++)
        printf("%d\n",r[i]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

定义了一个大小为 100 的宏 max。定义了三个大小为 max 的数组,pqr。你首先将被要求输入第一个数组 p 的大小,然后输入数组 p 的排序元素。这个过程将重复用于第二个数组 q

三个索引 ijk 被定义并初始化为 0。这三个索引将分别指向三个数组 pqr 的元素。

数组 pq 的第一个元素,换句话说,p[0]q[0],将被比较,较小的值将被分配到数组 r

因为 **q[0]** 小于 **p[0]**,所以 **q[0]** 被添加到数组 **r**,并且 **q****r** 数组的索引将增加以进行下一次比较,如下所示:

图 1.25

接下来,将比较 **p[0]****q[1]**。因为 **p[0]** 小于 **q[1]**,所以 **p[0]** 的值将被分配到数组 **r****r[1]** 位置:

图 1.26

然后,将比较 **p[1]****q[1]**。因为 **q[1]** 小于 **p[1]**,所以 **q[1]** 将被分配到数组 **r**,并且 **q****r** 数组的索引将增加以进行下一次比较(参见图下所示):

图 1.27

让我们使用 GCC 编译 mergetwosortedarrays.c 程序,如下所示:

D:\CBook>gcc mergetwosortedarrays.c -o mergetwosortedarrays

现在,让我们运行生成的可执行文件 mergetwosortedarrays.exe,以查看程序的输出:

D:\CBook>./mergetwosortedarrays
Enter length of first array:4
Enter 4 elements of the first array in sorted order
4
18
56
99

Enter length of second array:5
Enter 5 elements of the second array in sorted order
1
9
80
200
220

The combined sorted array is:
1
4
9
18
56
80
99
200
220

哇!我们已经成功地将两个排序后的数组合并成了一个。

第二章:管理字符串

字符串不过是存储字符的数组。由于字符串是字符数组,它们占用的内存更少,生成的目标代码更高效,从而使程序运行更快。就像数值数组一样,字符串也是从 0 开始的,也就是说,第一个字符存储在索引位置 0。在 C 语言中,字符串通过一个空字符\0终止。

本章中的食谱将增强你对字符串的理解,并让你熟悉字符串操作。字符串在几乎所有应用程序中都扮演着重要角色。你将学习如何搜索字符串(这是一个非常常见的任务),用另一个字符串替换字符串,搜索包含特定模式的字符串,以及更多。

在本章中,你将学习如何使用字符串创建以下食谱:

  • 判断字符串是否为回文

  • 查找字符串中第一个重复字符的出现

  • 显示字符串中每个字符的计数

  • 计算字符串中的元音和辅音数量

  • 将句子中的元音转换为大写

判断字符串是否为回文

回文是一个无论正向还是反向阅读都相同的字符串。例如,单词“radar”是一个回文,因为它正向和反向读起来都一样。

如何做到这一点...

  1. 定义两个名为strrev的 80 字符字符串(假设你的字符串不会超过 79 个字符)。你的字符串可以是任何长度,但请记住,字符串的最后一个位置是固定的,用于空字符\0
char str[80],rev[80];
  1. 输入将被分配给str字符串的字符:
printf("Enter a string: ");
scanf("%s",str);
  1. 使用strlen函数计算字符串的长度并将其赋值给n变量:
n=strlen(str);
  1. 以逆序执行for循环以逆序访问str字符串中的字符,然后将它们赋值给rev字符串:
for(i=n-1;i >=0;  i--)
{
    rev[x]=str[i];
    x++;
}
rev[x]='\0';
  1. 使用strcmp比较两个字符串strrev
if(strcmp(str,rev)==0)
  1. 如果strrev相同,则该字符串是回文。

在 C 语言中,特定内置函数的功能在各自的库中指定,也称为头文件。因此,在编写 C 程序时,每当使用内置函数时,我们都需要在程序顶部使用它们各自的头文件。头文件通常具有.h扩展名。在以下程序中,我使用了一个名为strlen的内置函数,它用于查找字符串的长度。因此,我需要在程序中使用其库string.h

用于查找指定字符串是否为回文的palindrome.c程序如下:

#include<stdio.h>  
#include<string.h>
void main()
{
    char str[80],rev[80];
    int n,i,x;
    printf("Enter a string: ");
    scanf("%s",str);
    n=strlen(str);
    x=0;
    for(i=n-1;i >=0;  i--)
    {
        rev[x]=str[i];
        x++;
    }
    rev[x]='\0';
    if(strcmp(str,rev)==0)
        printf("The %s is palindrome",str);
    else
        printf("The %s is not palindrome",str);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

为了确保一个字符串是回文,我们首先需要确保原始字符串及其反转形式长度相同。

假设原始字符串是sanjay,并将其分配给字符串变量str。该字符串是一个字符数组,其中每个字符作为数组元素单独存储,字符串数组中的最后一个元素是空字符。空字符表示为\0,在 C 语言中,它总是字符串变量的最后一个元素,如下面的图所示:

图 2.1

如您所见,字符串使用零基索引,即第一个字符位于索引位置str[0],其次是str[1],依此类推。至于最后一个元素,空字符位于str[6]

使用strlen库函数,我们将计算输入字符串的长度并将其分配给n变量。通过以逆序执行for循环,str字符串的每个字符以逆序访问,即从n-10,并分配给rev字符串。

最后,在rev字符串中添加一个空字符\0,使其成为一个完整的字符串。因此,rev将包含str字符串的字符,但顺序相反:

图 2.2

接下来,我们将运行strcmp函数。如果函数返回0,则表示strrev字符串中的内容完全相同,这意味着str是回文。如果strcmp函数返回除0以外的值,则表示两个字符串不相同;因此,str不是回文。

让我们使用 GCC 编译palindrome.c程序,如下所示:

D:\CBook>gcc palindrome.c -o palindrome

现在,让我们运行生成的可执行文件palindrome.exe,以查看程序的输出:

D:\CBook>./palindrome
Enter a string: sanjay
The sanjay is not palindrome

现在,假设str被分配了另一个字符字符串sanas。为了确保str中的单词是回文,我们将再次逆序字符串中的字符顺序。

因此,我们再次计算str的长度,以逆序执行一个for循环,并将str中的每个字符访问并分配给rev。空字符\0将被分配给rev中的最后一个位置,如下所示:

图 2.3

最后,我们将再次调用strcmp函数,并传入两个字符串。

编译后,让我们用新的字符串再次运行程序:

D:\CBook>palindrome
Enter a string: sanas
The sanas is palindrome

哇!我们已经成功识别出我们的字符串是否是回文。现在,让我们继续到下一个菜谱!

在字符串中查找第一个重复字符的出现

在这个菜谱中,你将学习如何创建一个显示字符串中第一个重复字符的程序。例如,如果你输入字符串racecar,程序应该输出为:字符串 racecar 中的第一个重复字符是 c。如果输入没有重复字符的字符串,程序应显示没有字符在字符串中重复。

如何做到这一点…

  1. 定义两个字符串,分别称为str1str2。您的字符串可以是任何长度,但字符串的最后一个位置是固定的,用于空字符\0
char str1[80],str2[80];
  1. 输入要分配给str1的字符。这些字符将被分配到字符串的相应索引位置,从str1[0]开始:
printf("Enter a string: ");
scanf("%s",str1);                
  1. 使用strlen库函数计算str1的长度。在这里,str1的第一个字符被分配给str2
n=strlen(str1);
str2[0]=str1[0];
  1. 使用for循环逐个访问str1中的所有字符,并将它们传递给ifexists函数以检查该字符是否已存在于str2中。如果字符在str2中找到,这意味着它是字符串的第一个重复字符,因此它将显示在屏幕上:
for(i=1;i < n; i++)
{
    if(ifexists(str1[i], str2, x))
    {
          printf("The first repetitive character in %s is %c", str1, 
          str1[i]);
          break;
    }
}
  1. 如果str1中的字符不在str2中,则它简单地被添加到str2中:
else
{
    str2[x]=str1[i];
    x++;
}

查找字符串中第一个重复字符的repetitive.c程序如下所示::

#include<stdio.h>  
#include<string.h>
int ifexists(char u, char z[],  int v)
{
    int i;
    for (i=0; i<v;i++)
        if (z[i]==u) return (1);
    return (0);
}

void main()
{
    char str1[80],str2[80];
    int n,i,x;
    printf("Enter a string: ");
    scanf("%s",str1);
    n=strlen(str1);
    str2[0]=str1[0];
    x=1;
    for(i=1;i < n; i++)
    {
        if(ifexists(str1[i], str2, x))
        {
            printf("The first repetitive character in %s is %c", str1, 
            str1[i]);
            break;
        }
        else
        {
            str2[x]=str1[i];
            x++;
        }
    }
    if(i==n)
        printf("There is no repetitive character in the string %s", str1);
}

现在,让我们深入了解代码,以便更好地理解它。

它是如何工作的...

假设我们已经定义了一个长度为某个值的字符串str1,并输入了以下字符——racecar

字符串racecar的每个字符将被分配到str1的相应索引位置,即r将被分配到str1[0]a将被分配到str1[1],依此类推。因为 C 语言中的每个字符串都以空字符\0结束,所以str1的最后一个索引位置将包含空字符\0,如下所示:

图片

图片

使用库函数strlen计算str1的长度,并使用for循环逐个访问str1中的所有字符,除了第一个字符。第一个字符已经分配给str2,如下面的图所示:

图片

图 2.5

str1访问的每个字符都会通过ifexists函数。ifexists函数将检查提供的字符是否已存在于str2中,并相应地返回布尔值。如果提供的字符在str2中找到,函数返回1,即true。如果提供的字符在str2中未找到,函数返回0,即false

如果ifexists返回1,这意味着字符在str2中找到,因此字符串的第一个重复字符将显示在屏幕上。如果ifexists函数返回0,这意味着字符不在str2中,所以它简单地被添加到str2中。

由于第一个字符已经被分配,所以str1的第二个字符被选中并检查是否已存在于str2中。因为str1的第二个字符不在str2中,所以它被添加到后面的字符串中,如下所示:

图片

图 2.6

该过程会重复进行,直到访问完str1的所有字符。如果访问了str1的所有字符,并且没有发现它们存在于str2中,这意味着str1中的所有字符都是唯一的,没有重复。

以下图表显示了访问str1的前四个字符后的字符串str1str2。你可以看到这四个字符被添加到str2中,因为它们在str2中都不存在:

图片

图 2.7

下一个要从str1中访问的字符是c。在将其添加到str2之前,它将与str2中所有现有的字符进行比较,以确定它是否已经存在。因为c字符已经在str2中存在,所以它不会被添加到str2中,并声明为str1中的第一个重复字符,如下所示:

图片

图 2.8

让我们使用 GCC 编译repetitive.c程序,如下所示:

D:\CBook>gcc repetitive.c -o repetitive

让我们运行生成的可执行文件repetitive.exe,以查看程序的输出:

D:\CBook>./repetitive
Enter a string: education
There is no repetitive character in the string education

让我们再次运行程序:

D:\CBook>repetitive
Enter a string: racecar
The first repetitive character in racecar is c

哇!我们已经成功找到了字符串中的第一个重复字符。

现在,让我们继续下一个菜谱!

显示字符串中每个字符的计数

在这个菜谱中,你将学习如何创建一个以表格形式显示字符串中每个字符计数的程序。

如何做到这一点…

  1. 创建一个名为str的字符串。字符串的最后一个元素将是空字符,\0

  2. 定义另一个与str长度匹配的字符串chr,用于存储str中的字符:

char str[80],chr[80];

  1. 提示用户输入一个字符串。输入的字符串将被分配给str字符串:
printf("Enter a string: ");
scanf("%s",str);
  1. 使用strlen计算字符串数组str的长度:
n=strlen(str);
  1. 定义一个名为count的整数数组,用于显示字符在str中出现的次数:
int count[80];
  1. 执行chr[0]=str[0]以将str的第一个字符赋值给chr在索引位置chr[0]

  2. 被分配到chr[0]位置的字符计数通过在count[0]索引位置分配1来表示:

chr[0]=str[0];
count[0]=1;           
  1. 运行一个for循环以访问str中的每个字符:
for(i=1;i < n;  i++)
  1. 运行ifexists函数以确定str中的字符是否存在于chr字符串中。如果字符不在chr字符串中,它将被添加到chr字符串的下一个索引位置,并且相应的count数组索引位置被设置为1
if(!ifexists(str[i], chr, x, count))
{
    x++;
    chr[x]=str[i];
    count[x]=1;
}
  1. 如果字符存在于chr字符串中,ifexists函数中将相应索引位置的count数组值增加1。以下代码片段中的pq数组分别代表chrcount数组,因为chrcount数组在ifexists函数中被传递并分配给pq参数:
if (p[i]==u)
{
    q[i]++;
    return (1);
}

计算字符串中每个字符的countofeach.c程序如下所示:

#include<stdio.h>
#include<string.h>
int ifexists(char u, char p[],  int v, int q[])
{
    int i;
    for (i=0; i<=v;i++)
    {
        if (p[i]==u)
        {
            q[i]++;
            return (1);
        }
    }
    if(i>v) return (0);
}
void main()
{
    char str[80],chr[80];
    int n,i,x,count[80];
    printf("Enter a string: ");
    scanf("%s",str);
    n=strlen(str);
    chr[0]=str[0];
    count[0]=1;
    x=0;
    for(i=1;i < n;  i++)
    {
        if(!ifexists(str[i], chr, x, count))
        {            
            x++;
            chr[x]=str[i];
            count[x]=1;
        }
    }
    printf("The count of each character in the string %s is \n", str);
    for (i=0;i<=x;i++)
        printf("%c\t%d\n",chr[i],count[i]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

假设你定义的两个字符串变量,strchr,大小为80(如果你愿意,你总是可以增加字符串的大小)。

我们将把字符字符串racecar分配给str字符串。每个字符都将分配到str的相应索引位置,即r将被分配到索引位置str[0]a将被分配到str[1],依此类推。像往常一样,字符串的最后一个元素将是一个空字符,如下面的图所示:

图片

图 2.9

使用strlen函数,我们首先计算字符串的长度。然后,我们将使用字符串数组chrstr数组的每个索引位置单独存储字符。我们将从1开始执行一个for循环,直到字符串的末尾,以访问字符串中的每个字符。

我们之前定义的整数数组,即count,将表示str中字符出现的次数,这由chr数组中的索引位置表示。也就是说,如果r位于索引位置chr[0],那么count[0]将包含一个整数值(在这种情况下是 1)来表示r字符到目前为止在str字符串中出现的次数:

图片

图 2.10

以下操作之一将应用于从字符串中访问的每个字符:

  • 如果字符存在于chr数组中,则count数组中相应索引位置中的值增加 1。例如,如果字符串中的字符在chr[2]索引位置找到,那么count[2]索引位置中的值将增加 1。

  • 如果字符不在chr数组中,它将被添加到chr数组的下一个索引位置,并且当计数数组被设置为1时,相应的索引位置就会被找到。例如,如果字符achr数组中没有找到,它将被添加到chr数组的下一个可用索引位置。如果字符a被添加到chr[1]位置,那么在count[1]索引位置将分配一个值1,以指示到目前为止在chr[1]中显示的字符已经出现了一次。

for循环完成时,也就是当访问字符串中的所有字符时。chr数组将包含字符串的各个字符,而count数组将包含计数,即chr数组表示的字符在字符串中出现的次数。chrcount数组中的所有元素都会显示在屏幕上。

让我们使用 GCC 编译countofeach.c程序,如下所示:

D:\CBook>gcc countofeach.c -o countofeach

让我们运行生成的可执行文件countofeach.exe,以查看程序的输出:

D:\CBook>./countofeach
Enter a string: bintu
The count of each character in the string bintu is
b       1
i       1
n       1
t       1
u       1

让我们尝试另一个字符串来测试结果:

D:\CBook>./countofeach
Enter a string: racecar
The count of each character in the string racecar is
r       2
a       2
c       2
e       1

哇!我们已经成功显示了字符串中每个字符的计数。

现在,让我们继续到下一个菜谱!

计算句子中的元音和辅音

在这个菜谱中,你将学习如何计算输入句子中的元音和辅音数量。元音是 aeiou,其余的字母都是辅音。我们将使用 ASCII 值来识别字母及其大小写:

图片

图 2.11

空白、数字、特殊字符和符号将被简单地忽略。

如何做到这一点…

  1. 创建一个名为 str 的字符串数组来输入你的句子。像往常一样,最后一个字符将是空字符:
char str[255];
  1. 定义两个变量,ctrVctrC
int  ctrV,ctrC;
  1. 提示用户输入一个你选择的句子:
printf("Enter a sentence: ");
  1. 执行 gets 函数以接受带有单词之间空白的句子:
gets(str);
  1. ctrVctrC 初始化为 0ctrV 变量将计算句子中的元音数量,而 ctrC 变量将计算句子中的辅音数量:
ctrV=ctrC=0;
  1. 执行一个 while 循环来逐个访问句子的每个字母,直到达到句子中的空字符。

  2. 执行一个 if 块来检查字母是否为大写或小写,使用 ASCII 值。这也确认了访问的字符不是空白字符、特殊字符或符号,或数字。

  3. 完成这些后,执行一个嵌套的 if 块来检查字母是否为小写或大写元音,并等待 while 循环结束:

while(str[i]!='\0')
{
    if((str[i] >=65 && str[i]<=90) || (str[i] >=97 && str[i]<=122))
    {
        if(str[i]=='A' ||str[i]=='E' ||str[i]=='I' ||str[i]=='O' 
        ||str[i]=='U' ||str[i]=='a' ||str[i]=='e' ||str[i]=='i' 
        ||str[i]=='o'||str[i]=='u')
            ctrV++;
        else
            ctrC++;
    }
    i++;
}

用于在字符串中计算元音和辅音的 countvowelsandcons.c 程序如下:

#include <stdio.h>
void main()
{
    char str[255];
    int  ctrV,ctrC,i;
    printf("Enter a sentence: ");
    gets(str);
    ctrV=ctrC=i=0;
    while(str[i]!='\0')
    {
        if((str[i] >=65 && str[i]<=90) || (str[i] >=97 && str[i]<=122))
        {
            if(str[i]=='A' ||str[i]=='E' ||str[i]=='I' ||str[i]=='O' 
            ||str[i]=='U' ||str[i]=='a' ||str[i]=='e' ||str[i]=='i' 
            ||str[i]=='o'||str[i]=='u')
                ctrV++;
            else
                ctrC++;
        }
        i++;
    }
    printf("Number of vowels are : %d\nNumber of consonants are : 
    %d\n",ctrV,ctrC);
}

现在,让我们幕后了解代码以更好地理解它。

它是如何工作的...

我们假设你不会输入超过 255 个字符的句子,所以我们相应地定义了我们的字符串变量。当提示时,输入一个将被分配给 str 变量的句子。因为句子中可能有单词之间的空白,我们将执行 gets 函数来接受句子。

我们定义的两个变量,即 ctrVctrC,被初始化为 0。因为字符串中的最后一个字符始终是空字符,\0,所以执行一个 while 循环,它将逐个访问句子的每个字符,直到达到句子中的空字符。

检查句子中的每个访问字母,以确认它要么是大写字母,要么是小写字母。也就是说,比较它们的 ASCII 值,如果访问字符的 ASCII 值是大写或小写字母,那么它将执行嵌套的 if 块。否则,将访问句子中的下一个字符。

一旦你确保访问的字符不是空白空间,任何特殊字符或符号,或数值,那么一个if块将被执行,该块检查访问的字符是否为小写或大写元音。如果访问的字符是元音,则ctrV变量的值增加1。如果访问的字符不是元音,则确认它是辅音,因此ctrC变量的值增加1

一旦访问了句子的所有字符,即当句子的空字符被达到时,while循环终止,并在屏幕上显示存储在ctrVctrC变量中的元音和辅音的数量。

让我们使用 GCC 编译countvowelsandcons.c程序,如下所示:

D:\CBook>gcc countvowelsandcons.c -o countvowelsandcons

让我们运行生成的可执行文件countvowelsandcons.exe,以查看程序的输出:

D:\CBook>./countvowelsandcons
Enter a sentence: Today it might rain. Its a hot weather. I do like rain
Number of vowels are : 18
Number of consonants are : 23

哇!我们已经成功统计了我们句子中的所有元音和辅音。

现在,让我们继续下一个菜谱!

将句子中的元音转换为大写

在这个菜谱中,你将学习如何将句子中的所有小写元音转换为大写。句子中的其余字符,包括辅音、数字、特殊符号和特殊字符,将简单地忽略并保持原样。

通过简单地改变该字符的 ASCII 值来转换任何字母的大小写,使用以下公式:

  • 将小写字母的 ASCII 值减 32 以将其转换为大写

  • 将大写字母的 ASCII 值加 32 以将其转换为小写

以下图表显示了大小写元音的 ASCII 值:

图 2.12

大写字母的 ASCII 值低于小写字母的 ASCII 值,两者之间的差值是 32。

如何做到这一点…

  1. 创建一个名为str的字符串来输入你的句子。像往常一样,最后一个字符将是空字符:
char str[255];
  1. 输入你选择的句子:
printf("Enter a sentence: ");
  1. 执行gets函数以接受单词之间有空格的句子,并将i变量初始化为0,因为句子的每个字符将通过i访问:
gets(str);
i=0
  1. 执行一个while循环,逐个访问句子的每个字母,直到句子的空字符为止:
while(str[i]!='\0')
{
    { …
    }
}
i++;
  1. 检查每个字母以验证它是否为小写元音。如果访问的字符是小写元音,则从该元音的 ASCII 值中减去32以将其转换为大写:
if(str[i]=='a' ||str[i]=='e' ||str[i]=='i' ||str[i]=='o' ||str[i]=='u')
    str[i]=str[i]-32;
  1. 当句子中的所有字母都被访问后,只需显示整个句子。

将句子中的小写元音转换为大写的convertvowels.c程序如下:

#include <stdio.h>
void main()
{
    char str[255];
    int  i;
    printf("Enter a sentence: ");
    gets(str); 
    i=0;
    while(str[i]!='\0')
    {
        if(str[i]=='a' ||str[i]=='e' ||str[i]=='i' ||str[i]=='o' 
        ||str[i]=='u')
            str [i] = str [i] -32;
        i++;
    }
    printf("The sentence after converting vowels into uppercase 
    is:\n");
    puts(str);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

再次,我们假设你不会输入超过 255 个字符的句子。因此,我们定义我们的字符串数组 str 的大小为 255。当提示时,输入一个要分配给 str 数组的句子。因为句子中的单词之间可能有空格,所以我们不会使用 scanf,而是使用 gets 函数来接受句子。

为了访问句子中的每个字符,我们将执行一个 while 循环,该循环将一直运行,直到在句子中遇到空字符。在句子的每个字符之后,都会检查它是否是小写元音字母。如果不是小写元音字母,则忽略该字符,并选择句子中的下一个字符进行比较。

如果访问的字符是小写元音字母,则从该字符的 ASCII 值中减去 32 以将其转换为大写。记住,小写和大写字母的 ASCII 值之差是 32。也就是说,小写字母 a 的 ASCII 值是 97,而大写字母 A 的 ASCII 值是 65。因此,如果你从 97(小写字母 a 的 ASCII 值)中减去 32,新的 ASCII 值将变为 65,这是大写字母 A 的 ASCII 值。

将小写元音字母转换为大写元音字母的步骤是首先使用 if 语句在句子中找到元音字母,然后从其 ASCII 值中减去 32 以将其转换为大写。

一旦访问了字符串中的所有字符,并且句子中的所有小写元音字母都转换为大写,整个句子将使用 puts 函数显示。

让我们使用 GCC 编译 convertvowels.c 程序,如下所示:

D:\CBook>gcc convertvowels.c -o convertvowels

让我们运行生成的可执行文件 convertvowels.exe 来查看程序的输出:

D:\CBook>./convertvowels
Enter a sentence: It is very hot today. Appears as if it might rain. I like rain
The sentence after converting vowels into uppercase is:
It Is vEry hOt tOdAy. AppEArs As If It mIght rAIn. I lIkE rAIn

哇!我们已经成功地将句子中的小写元音字母转换为大写。

第三章:探索函数

无论何时你需要创建一个大型应用程序,将其划分为可管理的块,称为函数,都是一个明智的决定。函数是代表可以独立执行的任务的小模块。函数内部编写的代码可以被多次调用,这有助于避免重复的语句。

函数有助于任何应用程序的团队合作、调试和扩展。当你想要向应用程序添加更多功能时,只需向其中添加几个函数即可。在调用函数时,调用函数可能会传递某些参数,称为实际参数;这些参数随后被分配给函数的参数。参数也被称为形式参数。

以下食谱将帮助您了解如何使用函数使复杂的应用程序更容易管理和使用。通常,一个函数只能返回一个值。但在这章中,我们将学习一种从函数中返回多个值的技术。我们还将学习如何在函数中应用递归。

在本章中,我们将介绍以下关于字符串的食谱:

  • 判断一个数是否是阿姆斯特朗数

  • 返回数组的最大和最小值

  • 使用递归寻找最大公约数

  • 将二进制数转换为十六进制数

  • 判断一个数是否是回文数

由于我将在本章的食谱中使用栈结构,让我们快速介绍一下栈。

什么是栈?

栈是一种可以用数组以及链表实现的构造。它是一种桶,你输入的值将被添加到底部。你添加到栈中的下一个项目将位于之前添加的项目之上。将值添加到栈中的过程称为push操作,从栈中获取值的过程称为pop操作。可以添加或取出值的栈的位置由一个称为top的指针指向。当栈为空时,top指针的值是-1

图片

图 3.1

当执行push操作时,top的值增加1,以便它能够指向可以推入值的栈中的位置:

图片

图 3.2

现在,下一个将被推入的值将位于值 1 之上。更确切地说,top指针的值将增加1,使其值为 1,下一个值将被推入stack[1]位置,如下所示:

图片

图 3.3

因此,你可以看到栈是一种后进先出LIFO)结构;也就是说,最后推入的值位于顶部。

现在,当我们执行pop操作时,顶部的值,即值2,将首先被弹出,然后是值1的弹出。基本上,在pop操作中,由top指针指向的值被取出,然后top的值递减 1,以便它可以指向下一个要弹出的值。

现在,我们已经理解了栈,让我们从第一个菜谱开始。

查找数字是否为阿姆斯特朗数

阿姆斯特朗数是一个三位整数,它是其各个数字的立方和。这仅仅意味着如果xyz = x³+y³+z³,它就是一个阿姆斯特朗数。例如,153 是一个阿姆斯特朗数,因为1³+5³+3³ = 153

类似地,如果一个由四个数字组成的数字的各个数字的四次幂之和等于该数字,那么这个数字就是一个阿姆斯特朗数。例如,pqrs = p⁴+q⁴+r⁴+s⁴

如何做到这一点…

  1. 输入一个数字以分配给n变量:
printf("Enter a number ");
scanf("%d",&n);
  1. 调用findarmstrong函数。分配给n的值将传递给此函数:
findarmstrong(n)
  1. 在函数中,传递的参数 n 被分配给numb参数。执行一个while循环来分离numb参数中的所有数字:
while(numb >0)
  1. while循环中,对分配给numb变量的数字应用模 10(%10)运算符。模运算符将一个数字除以另一个数字并返回余数:
remainder=numb%10;
  1. 将余数推入栈:
push(remainder);
  1. 通过将numb变量除以10来移除numb变量中的最后一位数字:
numb=numb/10;
  1. 重复步骤 4 到 6,直到numb变量中的数字变为 0。此外,创建一个count计数器来计算数字中的数字数量。将计数器初始化为0,它将在while循环期间递增:
count++;
  1. 从栈中弹出所有数字并提升到给定的幂。要弹出栈中的所有数字,执行一个while循环,该循环将执行,直到top大于或等于0,即直到栈为空:
while(top >=0)
  1. while循环内部,从栈中弹出一个数字并将其提升到count的幂,其中count是所选数字中数字的数量。然后,将所有数字加到value上:
j=pop();
value=value+pow(j,count);
  1. value变量中的数字与numb变量中的数字进行比较,并编写代码以在比较的数字匹配时返回值1
if(value==numb)return 1;

如果numbvalue变量中的数字相同,返回布尔值1,这意味着该数字是一个阿姆斯特朗数。

这里是用于找出指定数字是否为阿姆斯特朗数的armstrong.c程序:

/* Finding whether the entered number is an Armstrong number */
# include <stdio.h>
# include <math.h>

#define max 10

int top=-1;
int stack[max];
void push(int);
int pop();
int findarmstrong(int );
void main()
{
   int n;
   printf("Enter a number ");
   scanf("%d",&n);
   if (findarmstrong(n))
      printf("%d is an armstrong number",n);
   else printf("%d is not an armstrong number", n);
}
int findarmstrong(int numb)
{
   int j, remainder, temp,count,value;
   temp=numb;
   count=0;
   while(numb >0)
   {
      remainder=numb%10;
      push(remainder);
      count++;
      numb=numb/10;
   }
   numb=temp;
   value=0;
   while(top >=0)
   {
      j=pop();
      value=value+pow(j,count);
   }
   if(value==numb)return 1;
   else return 0;
}
void push(int m)
{
   top++;
   stack[top]=m;
}
int pop()
{
   int j;
   if(top==-1)return(top);
   else
   {
      j=stack[top];
      top--;
      return(j);
   }
}

现在,让我们看看幕后。

它是如何工作的…

首先,我们将应用模10运算符来分离我们的数字。假设我们输入的数字是153,你可以看到153被除以10,余下的3被推入栈中:

图 3.4

栈中的值将被推入由 top 指示的索引位置。最初,top 的值是 -1。这是因为在进行 push 操作之前,top 的值增加了 1,而数组是基于零的,也就是说,数组的第一个元素放置在 0 索引位置。因此,top 的值必须初始化为 -1。如前所述,在推入之前,top 的值增加 1,即 top 的值将变为 0,余数 3 被推入 stack[0]

在栈中,top 的值增加 1 以指示栈中将要推入值的位置。

我们将再次应用模 10 运算符到 15 的商上。我们将得到的余数是 5,它将被推入栈中。同样,在推入栈之前,top 的值,原本是 0,被增加到 1。在 stack[1],余数被推入:

图 3.5

对于 1 的商,我们将再次应用模 10 运算符。但是因为 1 不能被 10 整除,所以 1 本身将被视为余数并推入栈中。top 的值将再次增加 1,1 将被推入 stack[2]

图 3.6

一旦所有数字都被分离并放置在栈中,我们将逐个弹出它们。然后,我们将每个数字提升到等于数字个数的幂。因为数字 153 由三个数字组成,每个数字都被提升到 3 的幂。

当从栈中弹出值时,由 top 指针指示的值将被弹出。top 的值是 2,因此 stack[2] 中的值,即 1,被弹出并提升到 3 的幂,如下所示:

图 3.7

弹出操作后,top 的值将减少到 1,以指示下一个要弹出的位置。接下来,stack[1] 中的值将被弹出并提升到 3 的幂。然后我们将这个值添加到之前弹出的值中:

图 3.8

弹出操作后,top 的值减少 1,现在其值为 0。因此,stack[0] 中的值被弹出并提升到 3 的幂。这个结果被添加到我们之前的计算中:

图 3.9

计算 1³ + 5³ + 3³ 后的结果是 153,这与原始数字相同。这证明了 153 是一个阿姆斯特朗数。

让我们使用 GCC 编译 armstrong.c 程序,如下所示:

D:\CBook>gcc armstrong.c -o armstrong 

让我们检查 127 是否是阿姆斯特朗数:

D:\CBook>./armstrong
Enter a number 127
127 is not an armstrong number

让我们检查 153 是否是阿姆斯特朗数:

D:\CBook>./armstrong
Enter a number 153
153 is an armstrong number

让我们检查 1634 是否是阿姆斯特朗数:

D:\CBook>./armstrong
Enter a number 1634
1634 is an armstrong number

哇!我们已经成功创建了一个函数,用于判断指定的数字是否是阿姆斯特朗数。

现在,让我们继续下一个菜谱!

在数组中返回最大和最小值

C 函数不能返回超过一个值。但如果你想让函数返回超过一个值怎么办?解决方案是将要返回的值存储在数组中,并让函数返回该数组。

在这个菜谱中,我们将创建一个函数返回两个值,即最大值和最小值,并将它们存储在另一个数组中。之后,包含最大值和最小值的数组将从函数返回。

如何做到这一点…

  1. 需要找出最大和最小值的数组的尺寸不是固定的,因此我们将定义一个名为max的宏,其大小为100
#define max 100
  1. 我们将定义一个最大大小为100个元素的arr数组:
int arr[max];
  1. 你将被提示指定数组中的元素数量;你输入的长度将被分配给n变量:
printf("How many values? ");
scanf("%d",&n);
  1. 执行一个for循环n次,以接受arr数组的n个值:
for(i=0;i<n;i++)                                
    scanf("%d",&arr[i]);
  1. 调用maxmin函数,将arr数组和它的长度n传递给它。maxmin函数将返回的数组将被分配给整数指针*p
p=maxmin(arr,n);
  1. 当你查看函数定义时,int *maxmin(int ar[], int v){ },传递给maxmin函数的arrn参数分别被分配给arv参数。在maxmin函数中,定义一个包含两个元素的mm数组:
static int mm[2];
  1. 为了与数组中的其余元素进行比较,ar数组的第一元素被存储在mm[0]mm[1]中。执行一个从1值到数组长度末尾的循环,并在循环中应用以下两个公式:
  • 我们将使用mm[0]来存储arr数组的最低值。mm[0]中的值将与数组中的其余元素进行比较。如果mm[0]中的值大于数组中的任何元素,我们将较小的元素分配给mm[0]
if(mm[0] > ar[i])
    mm[0]=ar[i];
  • 我们将使用mm[1]来存储arr数组的最高值。如果发现mm[1]中的值小于数组中的其余元素,我们将较大的数组元素分配给mm[1]
if(mm[1]< ar[i])
    mm[1]= ar[i];
  1. 执行for循环后,mm数组将包含arr数组的最大和最小值,分别位于mm[0]mm[1]。我们将返回这个mm数组到main函数,其中*p指针被设置为指向返回的数组mm
return mm;
  1. *p指针将首先指向第一个索引位置的内存地址,即mm[0]。然后,显示该内存地址的内容,即数组的最低值。之后,将*p指针的值增加 1,使其指向数组中下一个元素的内存地址,即mm[1]位置:
printf("Minimum value is %d\n",*p++);
  1. mm[1] 索引位置包含数组的最大值。最后,通过 *p 指针指向的最大值显示在屏幕上:
printf("Maximum value is %d\n",*p);

returnarray.c 程序解释了如何从一个函数中返回一个数组。基本上,该程序返回数组的最大值和最小值:

/* Find out the maximum and minimum values using a function returning an array */
# include <stdio.h>
#define max 100
int *maxmin(int ar[], int v);
void main()
{
    int  arr[max];
    int n,i, *p;
    printf("How many values? ");
    scanf("%d",&n);
    printf("Enter %d values\n", n);
    for(i=0;i<n;i++)
        scanf("%d",&arr[i]);
    p=maxmin(arr,n);
    printf("Minimum value is %d\n",*p++);
    printf("Maximum value is %d\n",*p);
}
int *maxmin(int ar[], int v)
{
    int i;
    static int mm[2];
    mm[0]=ar[0];
    mm[1]=ar[0];
    for (i=1;i<v;i++)
    {
        if(mm[0] > ar[i])
            mm[0]=ar[i];
        if(mm[1]< ar[i])
            mm[1]= ar[i];
    }
    return mm;
}

现在,让我们深入了解。

它是如何工作的...

在这个过程中,我们将使用两个数组。第一个数组将包含需要找到最大值和最小值的值。第二个数组将用于存储第一个数组的最大值和最小值。

让我们将第一个数组称为 arr 并定义它包含五个元素,其值如下:

图 3.10

让我们将第二个数组称为 mmmm 数组的第一个位置,即 mm[0],将用于存储最小值,第二个位置,即 mm[1],将用于存储 arr 数组的最大值。为了能够比较 mm 数组的元素与 arr 数组的元素,将 arr 数组的第一个元素 arr[0] 复制到 mm[0]mm[1]

图 3.11

现在,我们将比较 arr 数组的其余元素与 mm[0]mm[1]。为了保持 mm[0] 中的最小值,任何小于 mm[0] 值的元素将被分配给 mm[0]。大于 mm[0] 的值将被简单地忽略。例如,arr[1] 中的值小于 mm[0],即 8 < 30。因此,较小的值将被分配给 mm[0]

图 3.12

我们将对 mm[1] 中的元素应用反向逻辑。因为我们想要 arr 数组的最大值在 mm[1] 中,所以任何找到的比 mm[1] 值大的元素将被分配给 mm[1]。所有较小的值将被简单地忽略。

我们将继续使用 arr 数组中的下一个元素,即 arr[2]。因为 77 > 8,所以当与 mm[0] 比较时将被忽略。但 77 > 30,所以它将被分配给 mm[1]

图 3.13

我们将重复此过程,与 arr 数组的其余元素。一旦 arr 数组的所有元素都与 mm 数组的元素进行比较,我们将在 mm[0]mm[1] 中分别得到最小值和最大值:

图 3.14

让我们使用 GCC 编译 returnarray.c 程序,如下所示:

D:\CBook>gcc returnarray.c -o returnarray

这是程序的输出:

D:\CBook>./returnarray
How many values? 5
Enter 5 values
30
8
77
15
9
Minimum value is 8
Maximum value is 77

哇!我们已经成功地在数组中返回了最大值和最小值。

现在,让我们继续下一个菜谱!

使用递归查找最大公约数

在这个方法中,我们将使用递归函数来找到两个或多个整数的最大公约数GCD),也称为最大公因数。GCD 是能够整除每个整数的最大正整数。例如,8 和 12 的最大公约数是 4,9 和 18 的最大公约数是 9。

如何做到这一点…

int gcd(int x, int y)递归函数使用以下三个规则来找到两个整数 x 和 y 的最大公约数:

  • 如果 y=0,则xy的最大公约数是x

  • 如果 x mod y 为 0,则 x 和 y 的最大公约数是 y。

  • 否则,x 和 y 的最大公约数是gcd(y, (x mod y))

按照以下步骤递归地找到两个整数的最大公约数(GCD):

  1. 你将被提示输入两个整数。将输入的整数分配给两个变量,uv
printf("Enter two numbers: ");
scanf("%d %d",&x,&y);
  1. 调用gcd函数并将xy值传递给它。xy值将被分别分配给ab参数。将gcd函数返回的 GCD 值分配给g变量:
g=gcd(x,y);
  1. gcd函数中,执行a % b%(模)运算符将数字除以并返回余数:
m=a%b;
  1. 如果余数非零,则再次调用gcd函数,但这次参数将是gcd(b,a % b),即gcd(b,m),其中 m 代表模运算:
gcd(b,m);
  1. 如果这又得到一个非零余数,即如果b % m是非零的,则使用上一次执行中获得的新值重复gcd函数:
gcd(b,m);
  1. 如果b % m的结果为零,则b是提供的参数的最大公约数,并将其返回到main函数:
return(b);
  1. 返回给main函数的结果b被分配给g变量,然后显示在屏幕上:
printf("Greatest Common Divisor of %d and %d is %d",x,y,g);

gcd.c程序解释了如何通过递归函数计算两个整数的最大公约数:

#include <stdio.h>
int gcd(int p, int q);
void main()
{
    int x,y,g;
    printf("Enter two numbers: ");
    scanf("%d %d",&x,&y);
    g=gcd(x,y);
    printf("Greatest Common Divisor of %d and %d is %d",x,y,g);
}
int gcd(int a, int b)
{
    int m;
    m=a%b;
    if(m==0)
        return(b);
    else
        gcd(b,m);
}

现在,让我们看看幕后。

它是如何工作的...

假设我们想要找到两个整数1824的最大公约数。为此,我们将调用gcd(x,y)函数,在这种情况下是gcd(18,24)。因为24,即 y,不为零,所以规则 1 不适用。接下来,我们将使用规则 2 检查18%24x % y)是否等于0。因为18不能被24整除,所以18将是余数:

图 3.15

由于规则 2 的参数也没有满足,我们将使用规则 3。我们将使用gcd(b,m)参数调用gcd函数,即gcd(24,18%24)。现在,m 代表模运算。在这个阶段,我们将再次应用规则 2 并收集余数:

图 3.16

因为24%18的结果是一个非零值,我们将再次调用gcd函数,使用gcd(b, m)参数,现在gcd(18, 24%18),因为我们从上一次执行中留下了186。我们将再次应用规则 2 到这次执行。当18除以6时,余数是0

图 3.17

在这个阶段,我们最终满足了规则之一的要求,即规则 2。如果你还记得,规则 2 说的是如果 x mod y 为0,则最大公约数是 y。因为18 mod 6的结果是0,所以1824的最大公约数是6

让我们使用 GCC 编译gcd.c程序,如下所示:

D:\CBook>gcc gcd.c -o gcd

这是程序的输出:

D:\CBook>./gcd
Enter two numbers: 18 24
Greatest Common Divisor of 18 and 24 is 6
D:\CBook>./gcd
Enter two numbers: 9 27
Greatest Common Divisor of 9 and 27 is 9

哇!我们已经成功使用递归找到了最大公约数(GCD)。

现在,让我们继续下一个菜谱!

将二进制数转换为十六进制数

在这个菜谱中,我们将学习如何将二进制数转换为十六进制数。二进制数由两个位组成,0 和 1。要将二进制数转换为十六进制数,我们首先需要将二进制数转换为十进制数,然后将得到的十进制数转换为十六进制。

如何做到这一点…

  1. 输入一个二进制数并将其赋值给变量b
printf("Enter a number in binary number ");
scanf("%d",&b);
  1. 调用intodecimal函数将二进制数转换为十进制数,并将变量b作为参数传递给它。将intodecimal函数返回的十进制数赋值给变量d
d=intodecimal(b);
  1. 观察到intodecimal的定义int intodecimal(int bin) { },我们可以看到b参数被分配给intodecimal函数的bin参数。

  2. 将所有二进制位分开,并将它们乘以 2 的幂,幂的值等于它们在二进制数中的位置。将结果相加以得到十进制等效值。为了分离每个二进制位,我们需要执行一个while循环,直到二进制数大于 0:

while(bin >0)
  1. while循环中,对二进制数应用模 10 运算符并将余数推入栈中:
remainder=bin%10;
push(remainder);
  1. 执行另一个while循环以获取栈中所有二进制位的十进制数。while循环将执行,直到栈为空(即,直到top的值大于或等于 0):
while(top >=0)
  1. while循环中,弹出栈中的所有二进制位,并将每个位乘以 2 的top次幂。将结果相加以得到输入二进制数的十进制等效值:
j=pop();
deci=deci+j*pow(2,exp);
  1. 调用intohexa函数并将二进制数和十进制数传递给它以获取十六进制数:
void intohexa(int bin, int deci)
  1. intohexa函数中对十进制数应用模 16 运算符以获取其十六进制数。将得到的余数推入栈中。再次对商应用模 16 并重复此过程,直到商小于 16:
remainder=deci%16;
push(remainder);
  1. 弹出推入栈中以显示十六进制数的余数:
j=pop();

如果从栈中弹出的余数小于 10,则直接显示。否则,将其转换为以下表中提到的等效字母,并将结果字母显示出来:

十进制 十六进制
10 A
11 B
12 C
13 D
14 E
15 F
if(j<10)printf("%d",j);
else printf("%c",prnhexa(j));

binarytohexa.c程序解释了如何将二进制数转换为十六进制数:

//Converting binary to hex
# include <stdio.h>
#include  <math.h>
#define max 10
int top=-1;
int stack[max];
void push();
int pop();
char prnhexa(int);
int intodecimal(int);
void intohexa(int, int);
void main()
{
    int b,d;
    printf("Enter a number in binary number ");
    scanf("%d",&b);
    d=intodecimal(b);
    printf("The decimal of binary number %d is %d\n", b, d);
    intohexa(b,d);
}
int intodecimal(int bin)
{
    int deci, remainder,exp,j;
    while(bin >0)
    {
        remainder=bin%10;
        push(remainder);
        bin=bin/10;
    }
    deci=0;
    exp=top;
    while(top >=0)
    {
        j=pop();
        deci=deci+j*pow(2,exp);
        exp--;
    }
    return (deci);
}
void intohexa(int bin, int deci)
{
    int remainder,j;
    while(deci >0)
    {
        remainder=deci%16;
        push(remainder);
        deci=deci/16;
    }
    printf("The hexa decimal format of binary number %d is ",bin);
    while(top >=0)
    {
        j=pop();
        if(j<10)printf("%d",j);
        else printf("%c",prnhexa(j));
    }
}
void push(int m)
{
    top++;
    stack[top]=m;
}
int pop()
{
    int j;
    if(top==-1)return(top);
    j=stack[top];
    top--;
    return(j);
}
char prnhexa(int v)
{
    switch(v)
    {
        case 10: return ('A');
                 break;
        case 11: return ('B');
                 break;
        case 12: return ('C');
                 break;
        case 13: return ('D');
                 break;
        case 14: return ('E');
                 break;
        case 15: return ('F');
                 break;
    }
}

现在,让我们看看幕后。

它是如何工作的...

第一步是将二进制数转换为十进制数。为此,我们将分离所有二进制数字,并将每个数字乘以二进制数中其位置的2的次方。然后,我们将应用 mod 10运算符,以便将二进制数分离成单独的数字。每次对二进制数应用 mod 10时,其最后一位数字就会被分离并推入栈中。

假设我们需要将二进制数转换为十六进制格式的是110001。我们将对此二进制数应用 mod 10运算符。mod 运算符将除以数字并返回余数。在应用 mod 10运算符时,最后一个二进制数字——换句话说,最右边的数字将被作为余数返回(这与所有除以10的情况相同)。

操作被推入由top指针指示的栈位置。top的初始值是-1。在推入栈之前,top的值增加 1。因此,top的值增加到 0,作为余数出现的二进制数字(在这种情况下,是 1)被推入stack[0](见图 3.18),并且11000作为商返回:

图片

图 3.18

我们将再次应用 mod 10运算符到商上,以分离当前二进制数的最后一位数字。这次,mod 10运算符将返回0作为余数,1100作为商。余数再次推入栈中。如前所述,在应用push操作之前,top的值会增加。由于top的值是0,它增加到1,我们的新余数0被推入stack[1]

图片

图 3.19

我们将重复此过程,直到将二进制数的所有数字分离并推入栈中,如下所示:

图片

图 3.20

一旦完成,下一步就是逐个弹出数字,并将每个数字乘以2top次方。例如,2top次方意味着2将提升到从二进制数字被弹出位置开始的索引值。从栈中弹出的值将从top指示的位置弹出。

top的当前值是5,因此stack[5]中的元素将被弹出,并乘以25次方,如下所示:

图片

图 3.21

从栈中弹出一个值后,top 的值减少 1,以指向下一个要弹出的元素。这个过程会一直重复,直到所有数字都被弹出并乘以 2top 位置值的幂。图 3.19 展示了如何从栈中弹出所有二进制位并乘以 2top 次幂:

图 3.22

我们得到的结果是用户输入的二进制数的十进制等价数。

现在,要将十进制数转换为十六进制格式,我们将它除以 16。我们需要继续除以数字,直到商小于 16。除法的余数以 LIFO 顺序显示。如果余数小于 10,则直接显示;否则,显示其等效字母。如果你得到 10 到 15 之间的余数,可以使用前面的表格找到等效字母。

在下面的图中,你可以看到十进制数 49 被除以 16。余数以 LIFO 顺序显示,以显示十六进制,因此 31 是二进制数 110001 的十六进制表示。由于余数都小于 10,你不需要应用前面的表格:

图 3.23

让我们使用 GCC 编译 binaryintohexa.c 程序,如下所示:

D:\CBook>gcc binaryintohexa.c -o binaryintohexa

这里是程序的另一个输出:

D:\CBook>./binaryintohexa
Enter a number in binary number 110001
The decimal of binary number 110001 is 49
The hexa decimal format of binary number 110001 is 31

这里是程序的另一个输出:

D:\CBook>./binaryintohexa
Enter a number in binary number 11100
The decimal of binary number 11100 is 28
The hexa decimal format of binary number 11100 is 1C

哇!我们已经成功将二进制数转换为十六进制数。

现在,让我们继续下一个菜谱!

查找一个数字是否是回文数

回文数是指正向和反向读取时都相同的数。例如,123 不是回文数,但 737 是。要判断一个数是否是回文数,我们需要将其分解成单独的数字,并将原始数的个位转换为百位,百位转换为个位。

例如,一个 pqr 数字如果 pqr=rqp,则被称为回文 。只有当以下条件成立时,pqr 才会等于 rqp

p x 100 + q x 10 + r = r x 100 + q x 10 + p

换句话说,我们将个位上的数字乘以 10²,将其转换为百位,并将百位上的数字通过乘以 1\ 转换为个位。如果结果与原始数字匹配,则它是回文数。

如何操作...

  1. 输入一个数字以分配给 n 变量:
printf("Enter a number ");
scanf("%d",&n);
  1. 调用 findpalindrome 函数并将 n 变量中的数字作为参数传递给它:
findpalindrome(n)
  1. n 参数被分配给 findpalindrome 函数中的 numb 参数。我们需要分离数字的每一位;为此,我们将执行一个 while 循环,直到 numb 变量的值大于 0
while(numb >0)
  1. while 循环中,我们将对数字应用模 10。应用模 10 运算符后,我们将得到余数,这基本上是数字的最后一位:
remainder=numb%10;
  1. 将那个余数推入栈中:
push(remainder);
  1. 因为数字的最后一位被分离出来,我们需要从现有的数字中移除最后一位。这是通过将数字除以 10 并截断分数来完成的。while 循环将在数字被单独分成各个数字并将所有数字推入栈中时终止:
numb=numb/10;
  1. 栈顶的数字将是原数的百位,而栈底的数字将是原数的个位。回想一下,我们需要将原数的百位转换为个位,反之亦然。逐个弹出栈中的所有数字,并将每个数字乘以 10 的幂。对于第一个弹出的数字,幂将是 0。每次弹出值时,幂都会增加。在乘以相应的 10 幂之后,数字被添加到一个单独的变量中,该变量称为 value
j=pop();
value=value+j*pow(10,count);
count++;
  1. 如果 numbvalue 变量中的数字匹配,这意味着数字是一个回文数。如果数字是回文数,findpalindrome 函数将返回值 1,否则它将返回值 0
if(numb==value) return (1);
else return (0);

findpalindrome.c 程序确定输入的数字是否是回文数:

//Find out whether the entered number is a palindrome or not
# include <stdio.h>
#include <math.h>
#define max 10
int top=-1;
int stack[max];
void push();
int pop();
int findpalindrome(int);
void main()
{
    int n;
    printf("Enter a number ");
    scanf("%d",&n);   
    if(findpalindrome(n))
        printf("%d is a palindrome number",n);
    else
        printf("%d is not a palindrome number", n);
}
int findpalindrome(int numb)
{
    int j, value, remainder, temp,count;
    temp=numb;
    while(numb >0)
    {
        remainder=numb%10;
        push(remainder);
        numb=numb/10;
    }
    numb=temp;
    count=0;
    value=0;
    while(top >=0)
    {
        j=pop();
        value=value+j*pow(10,count);
        count++;
    }
    if(numb==value) return (1);
    else return (0);
}
void push(int m)
{
    top++;
    stack[top]=m;
}
int pop()
{
    int j;
    if(top==-1)return(top);
    else
    {
        j=stack[top];
        top--;
        return(j);
   }
}

现在,让我们看看幕后。

它是如何工作的...

假设我们输入的数字是 737。现在,我们想知道 737 是否是回文数。我们将从对 737 应用模 10 操作符开始。应用后,我们将收到余数 7 和商数 73。余数 7 将被推入栈中。然而,在推入栈之前,top 指针的值增加 1。top 的初始值是 -1;它增加到 0,余数 7 被推入 stack[0](见 图 3.21)。

10 操作符返回数字的最后一位作为余数。应用模 10 操作符后得到的商数是移除最后一位后的原始数字。也就是说,在将 737 应用模 10 操作符时,我们将得到的商数是 73

图 3.24

对于商数 73,我们将再次应用模 10 操作符。余数将是最后一位数字,即 3,而商数将是 7top 的值增加 1,使其变为 1,余数被推入 stack[1]。对于商数 7,我们再次应用模 10 操作符。因为 7 不能被 10 整除,所以 7 本身被返回并推入栈中。在 push 操作之前,top 的值再次增加 1,使其变为 27 的值将被推入 stack[2]

图 3.25

在将数字分解成单个数字后,我们需要逐个弹出栈中的每个数字,并将每个数字乘以10的幂次。对于栈顶的数字,幂次为0,每次弹出操作后增加 1。要从栈中弹出的数字将由栈顶指针指示。top的值为2,因此stack[2]上的数字被弹出,并乘以100次幂:

图 3.26

每次弹出操作后,top的值减少 1,幂次的值增加 1。下一个将被弹出的数字是stack[1]上的数字。也就是说,3将被弹出,并乘以101次幂。之后,top的值将减少 1,即top的值变为0,幂次的值增加 1,即幂次的值从1增加到2stack[0]上的数字将被弹出,并乘以102次幂:

图 3.27

所有乘以10的相应幂次的数字相加。因为计算结果与原始数字相同,737是一个回文数。

让我们使用 GCC 编译findpalindrome.c程序,如下所示:

D:\CBook>gcc findpalindrome.c -o findpalindrome

让我们检查123是否是一个回文数:

D:\CBook>./findpalindrome
Enter a number 123
123 is not a palindrome number

让我们检查737是否是一个回文数:

 D:\CBook>./findpalindrome
Enter a number 737
737 is a palindrome number

哇!我们已经成功确定了数字是否是回文数。

第四章:预处理和编译

有几个预处理器语句可以帮助您确定哪些源代码需要编译,哪些需要排除编译。也就是说,可以应用条件,并且只有当指定的条件为真时,所需的语句才会被编译。这些指令可以嵌套以实现更精确的分支。有大量的预处理器语句,如#if#ifdef#ifndef#else#elif#endif,可以用来将语句收集到我们希望在指定条件为真时编译的块中。

使用宏的一些优点如下:

  • 当宏的值或代码被宏名替换时,程序的执行速度会提高。因此,编译器调用或调用函数所涉及的时间被节省了。

  • 宏可以缩短程序的长度。

使用宏的主要缺点是,在程序编译之前,程序的大小会增加,因为所有宏都被它们的代码替换。在本章中,我们将学习如何使用预处理器指令应用条件编译。

我们还将学习如何通过使用断言来实现程序中的验证。断言是对程序中不同关键语句进行验证的一种检查方式。如果这些断言或表达式未验证或返回 false,则显示错误并终止程序。与通常的错误处理相比,主要区别在于断言可以在运行时禁用。

如果在#include <assert.h>指令附近定义了#define NDEBUG宏,它将禁用断言函数。

除了正常的断言外,还有一些被称为静态或编译时断言的断言,它们用于在编译时捕获错误。这些断言可以用于进行编译时验证。

此外,我们还将通过比萨店示例学习如何使用字符串化和标记粘贴运算符。

在本章中,我们将学习如何制作以下菜谱:

  • 使用指令进行条件编译

  • 应用断言进行验证

  • 使用断言确保指针不是指向NULL

  • 使用编译时断言提前捕获错误

  • 应用字符串化和标记粘贴运算符

让我们从第一个菜谱开始。

使用指令进行条件编译

在这个菜谱中,我们将学习如何应用条件编译。我们将定义某些宏,然后通过应用#if#ifdef#ifndef#else#elif#endif#undef预处理器指令,我们将指导编译器编译所需的代码。以书店为例,假设用户被要求输入书籍的价格。程序将根据代表用户购买书籍的数量或数量的Qty宏应用不同的折扣、节日优惠、折扣券和 Kindle 选项。程序还定义了其他宏,以确定不同的适用优惠。

如何操作...

按照以下步骤使用预处理器指令进行条件编译:

  1. 定义一个Qty宏并为其分配一个初始值:
#define Qty 10
  1. 用户将被提示输入书籍的价格:
printf("Enter price of a book ");
scanf("%f", &price);
  1. 使用Qty*price公式计算书籍的总数:
totalAmount=Qty*price; 
  1. 基于Qty宏,使用#if#elif#else#endif指令来确定总金额的折扣。

  2. 一旦确定了折扣百分比,就会计算折扣后的金额,并将其分配给afterDisc变量:

afterDisc=totalAmount - (totalAmount*discount)/100; 
  1. 节日折扣也是基于FestivalOffer宏计算的。也就是说,使用#ifdef#else#endif指令来确认是否已定义FestivalOffer宏,并相应地计算客户在扣除节日折扣后需要支付的金额:
#ifdef FestivalOffer
 afterFDisc=afterDisc-(totalAmount*FestivalOffer)/100;
 #else
 afterFDisc=afterDisc;
 #endif
  1. 使用#if defined指令来确认程序中是否定义了DiscountCoupon宏。相应地,用户会被告知他们是否有资格获得折扣券:
#if defined (DiscountCoupon)
 printf("You are also eligible for a discount coupon of $ %d\n", DiscountCoupon);
 #endif
  1. 预处理器指令#ifndef#endif用于确定是否已定义Kindle宏。如果Kindle宏尚未定义,则将其定义并设置其值。相应地,用户会被告知他们将有资格获得Kindle版书籍的多少个月:
#ifndef Kindle
 #define Kindle 1
 #endif
 printf("You can use the Kindle version of the book for %d 
         month(s)\n", Kindle);

使用预处理器指令进行条件编译的程序如下所示:

// condcompile.c
#include <stdio.h>
#define Qty 10
#define FestivalOffer 2
#define DiscountCoupon 5
#define Kindle 2

int main() 
{ 
    int discount;
    float price, totalAmount, afterDisc, afterFDisc; 
    printf("Enter price of a book "); 
    scanf("%f", &price); 
    #if Qty >= 10 
        discount=15; 
    #elif Qty >=5 
        discount=10; 
    #else 
        discount=5; 
    #endif 
    totalAmount=Qty*price; 
    afterDisc=totalAmount - (totalAmount*discount)/100; 
    #ifdef FestivalOffer 
        afterFDisc=afterDisc-(totalAmount*FestivalOffer)/100; 
    #else 
        afterFDisc=afterDisc; 
    #endif 
    printf("Quantity = %d, Price is $ %.2f, total amount for the 
            books is $ %.2f\n", Qty, price, totalAmount); 
    printf("Discount is %d%% and the total amount after 
            discount is $ %.2f\n", discount, afterDisc); 
    #ifdef FestivalOffer 
        printf("Festival discount is %d%%, the total amount 
                after festival discount is $ %.2f\n", 
                FestivalOffer, afterFDisc); 
    #endif 
    #if defined (DiscountCoupon) 
        printf("You are also eligible for a discount 
                coupon of $ %d\n", DiscountCoupon); 
    #endif 
    #ifndef Kindle 
        #define Kindle 1 
    #endif 
    printf("You can use the Kindle version of the book 
           for %d month(s)\n", Kindle); 
    return 0; 
}

现在,让我们深入了解幕后,以便更好地理解代码。

它是如何工作的...

定义了四个宏,分别称为QtyFestivalOfferDiscountCouponKindle,它们的值分别为10252。用户被提示输入书籍的价格。用户输入的值被分配给变量price。然后使用#if#elif#else#endif条件指令根据Qty宏的值确定应用于书籍的折扣金额。因为当前Qty宏的值为10,通过预处理器指令将折扣变量的值设置为15discount变量的值可以随时通过更改Qty宏的值来更改。通过将Qty的值乘以价格来计算书籍的总数,并将结果值分配给totalAmount变量。因为用户根据Qty值获得某种折扣,所以计算扣除折扣后的金额,并将结果值分配给afterDisc变量。

再次,由于FestivalOffer宏被定义,使用了#ifdef#else#endif预处理器指令来计算在扣除 2%节日折扣后客户需要支付的金额。我们总是可以通过注释掉#define FestivalOffer语句来取消宏的定义;在这种情况下,将不会向客户提供节日折扣。

总金额以及扣除折扣后的金额都会在屏幕上显示。如果应用了节日优惠,扣除节日优惠后的金额也会在屏幕上显示。

使用#if defined指令来确认DiscountCoupon宏是否被定义。因为目前程序中DiscountCoupon宏被定义,并赋值为5,会显示一条消息告知他们还有资格获得额外的 5 美元折扣券。如果您想避免提供任何折扣券,可以总是注释掉DiscountCoupon宏。书籍的 Kindle 版本至少需要提供给客户一个月。因为程序中定义了Kindle宏,并赋值为2,屏幕上会显示一条消息告知用户他们可以使用书籍的 Kindle 版本 2 个月。然而,如果您注释掉Kindle宏,如果程序中没有定义Kindle宏,#ifndef#endif预处理器指令将用于将Kindle宏的值设置为1。因此,如果程序中没有定义Kindle宏,会显示一条消息告知用户他们可以使用书籍的 Kindle 版本 1 个月。

程序使用 GCC 编译,如下截图所示。因为没有在编译过程中出现错误,这意味着condcompile.c程序已成功编译成.exe文件:condcompile.exe。在执行文件时,用户将被提示输入书籍的价格,并根据定义的宏,将显示总金额和折扣金额,如下截图所示:

图 4.1

接下来,保持Qty宏的值为10,并尝试注释掉以下两个宏:

#define FestivalOffer 2 
#define Kindle 2

前面的程序将显示以下输出:

图 4.2

您可以在输出中看到,由于Qty宏的值仍然是10,客户将继续获得前面截图所示的 15%折扣。此外,节日折扣根本未给予客户。因为DiscountCoupon宏仍然被定义,客户将继续获得 5 美元的折扣券,Kindle 版本的价格降低到 1 个月。

如我们之前提到的,#undef指令移除了当前宏的定义。以下代码片段使用定义的宏,并在使用后将其取消定义:

#include <stdio.h>
#define qty 10 

int main()
{
     #ifdef qty
         amount =qty * rate; 
         #undef qty
     #endif 
 return 0;
}

您可以看到qty宏被使用,并在使用后未定义。现在,让我们继续到下一个配方!

应用断言进行验证

在本配方中,我们将学习如何使用断言实现验证。程序将要求用户输入从一地飞往另一地的乘客信息。使用断言,我们可以确保输入的乘客数量是正数。如果输入的乘客数量为零或负数,程序将终止。

如何操作...

按照以下步骤使用断言创建验证检查。如果乘客数量的值为零或负数,则该配方将不允许程序运行:

  1. 用户被提示输入飞行乘客的数量:
printf("How many passengers ? ");
scanf("%d",&noOfPassengers);
  1. 定义了一个assert实例以确保乘客数量的值不应为0或负数。如果用户为乘客数量输入了0或负数值,将显示错误消息,显示行号,并且程序将终止:
assert(noOfPassengers > 0 && "Number of passengers should 
           be a positive integer"); 
  1. 如果输入的乘客数量值为正数,用户将被要求提供其他信息,例如航班从哪里出发,航班飞往哪里,以及旅行的日期:
 printf("Flight from: ");
 while((c= getchar()) != '\n' && c != EOF);
 gets(fl_from);
 printf("Flight to: ");
 gets(fl_to);
 printf("Date of journey ");
 scanf("%s", dateofJourney);
  1. 乘客输入的信息随后显示在屏幕上:
 printf("Number of passengers %d\n", noOfPassengers);
 printf("Flight from: %s\n", fl_from);
 printf("Flight to: %s\n", fl_to);
 printf("Date of journey: %s\n", dateofJourney);

使用断言实现验证检查的程序如下代码片段所示:

// assertdemoprog.c
#include <stdio.h> 
#include <assert.h> 

int main(void) 
{ 
    int c, noOfPassengers; 
    char fl_from[30], fl_to[30], dateofJourney[12]; 
    printf("How many passengers ? "); 
    scanf("%d",&noOfPassengers); 
    assert(noOfPassengers > 0 && "Number of passengers should 
                                  be a positive integer"); 
    printf("Flight from: "); 
    while((c= getchar()) != '\n' && c != EOF); 
        gets(fl_from); 
        printf("Flight to: "); 
        gets(fl_to); 
        printf("Date of journey "); 
        scanf("%s", dateofJourney); 
        printf("The information entered is:\n"); 
        printf("Number of passengers %d\n", noOfPassengers); 
        printf("Flight from: %s\n", fl_from); 
        printf("Flight to: %s\n", fl_to);
        printf("Date of journey: %s\n", dateofJourney); 
        return 0; 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

程序提示用户输入特定日期从一地飞往另一地的乘客信息。为了确保乘客数量的值不是零或负数,使用了断言。assert表达式验证分配给noOfPassengers变量的值。它检查noOfPassengers变量的值是否大于0。如果是,程序将继续执行其余的语句;否则,将文件名和行号发送到标准错误,并终止程序。

如果断言语句得到验证,即noOfPassengers分配的值大于 0,那么将要求用户输入乘客的其他详细信息,例如航班从哪里出发,航班飞往哪里,以及旅行的日期。然后,输入的信息将显示在屏幕上。

程序使用 GCC 编译,如下面的截图所示。因为没有在编译过程中出现错误,这意味着assertdemoprog.c程序已成功编译成.exe文件:assertdemoprog.exe。在执行文件时,将提示用户输入乘客数量。如果输入的乘客数量是正数,程序将完美运行,如下面的截图所示:

图片

图 4.3

在第二次执行程序时,如果noOfPassengers变量的输入值是负数或零,将显示一个错误,显示程序名称和行号,并终止程序。指定的错误信息“Number of passengers should be a positive integer”将被显示:

图片

图 4.4

哇!我们已经成功应用断言来验证我们的数据。

使用断言确保指针不是指向 NULL

让我们在断言上再做一个练习。让我们应用断言以确保指针不是指向NULL,而是指向要访问的内存地址。本质上,在这个练习中,我们将学习计算一些数字的平均值,这些数字存储在数组中,而数组元素是通过指针访问的。

如何做…

通过使用断言,遵循以下步骤以确保指针不是NULL并且指向一个内存地址:

  1. 定义一个包含要计算平均值的整数数组的数组:
int arr[]={3,9,1,6,2};
  1. 设置一个指针指向数组:
ptr=arr; 
  1. 定义一个用于计算数组元素平均值的函数。将数组的指针和数组中值的数量都传递给这个函数:
average=findaverage(ptr, count); 
  1. 在函数中,定义一个assert表达式以确保指针不是NULL。如果指针是NULL,程序将显示错误并终止:
assert(Ptr != NULL && "Pointer is not pointing to any array"); 
  1. 如果指针不是NULL,则将通过指针访问数组元素,并计算它们的平均值并在屏幕上显示:
for(i=0;i<Count;i++)
 {
   sum+=*Ptr;
   Ptr++;
 }
Average=(float)sum/Count;

实现验证以确保指针不是NULL且指向内存地址的程序如下所示:

// assertprog.c
#include <stdio.h> 
#include <assert.h> 

float findaverage(int *Ptr, int Count); 

int main() 
{ 
    int arr[]={3,9,1,6,2};
    float average; 
    int *ptr=NULL,count; 
    ptr=arr; 
    count=5; 
    average=findaverage(ptr, count); 
    printf("Average of values is %f\n", average); 
    return(0); 
} 

float findaverage(int *Ptr, int Count)
{
    int sum,i;
    float Average; 
    assert(Ptr != NULL && "Pointer is not pointing to any array"); 
    sum=0; 
    for(i=0;i<Count;i++) 
    { 
        sum+=*Ptr; 
        Ptr++; 
    } 
    Average=(float)sum/Count; 
    return(Average); 
}

现在,让我们深入了解代码以更好地理解它。

它是如何工作的...

在这个程序中,通过数组计算了几个整数的平均值。也就是说,将需要计算平均值的整数数量分配给一个数组,并使用一个整数指针来访问数组元素。定义了一个名为findaverage的函数,将整数指针和数字的数量传递给它。在函数中,使用断言确保指针不是NULL。如果指针不是NULL,则通过指针访问数组元素并执行加法。在数字加法之后,计算平均值。然后,将计算出的平均值返回到main函数,在屏幕上显示平均值。如果指针没有指向数组而是指向NULL,程序将显示错误并终止。

程序使用 GCC 编译,如下面的截图所示。因为在编译过程中没有出现错误,这意味着assertprog.c程序已成功编译成.exe文件:assertprog.exe。因为执行文件时指针指向数组,所以我们得到数组中指定的数值的平均值,如下面的截图所示:

图 4.5

接下来,注释掉以下行,其中指针指向数组:

ptr=arr;

ptr指针现在正指向NULL。因此,在运行程序时,它将显示一个错误,如下面的截图所示:

图 4.6

哇!我们已经成功使用断言确保我们的指针没有指向NULL

现在,让我们继续下一个菜谱!

使用编译时断言提前捕获错误

在这个菜谱中,我们将使用断言在编译时检测错误。本质上,我们将创建一个结构,并创建一个编译时断言,确保结构的大小是某些特定的字节。如果结构的大小不等于指定的值,程序将终止。这个约束将有助于确定存储容量,也有助于记录的简单维护,即删除和更新。

如何做到这一点...

按照以下步骤创建编译时assert表达式,以确保用户定义的结构是特定字节数:

  1. 定义一个包含几个成员的结构:
struct customers
 {
 int orderid;
 char customer_name[20];
 float amount;
 };
  1. 定义一个编译时断言,对结构体的大小施加约束。只有当断言得到验证时,即结构体的大小与 assert 表达式中提到的字节数完全相等时,程序才会编译:
static_assert(sizeof(struct customers) == 28, "The structure is consuming unexpected number of bytes"); 
  1. 在程序的主体中,你可以编写任何可执行代码。只有当 assert 表达式得到验证时,这段代码才会编译并执行:
static_assert(sizeof(struct customers) == 28, "The structure is consuming unexpected number of bytes"); 

实现编译时验证以确保结构体大小正好等于特定字节数的程序如下所示:

// compileassert.c
#include <stdio.h> 
#include <assert.h> 

struct customers 
{ 
    int orderid;
    char customer_name[20]; 
    float amount; 
}; 

static_assert(sizeof(struct customers) == 28, "The structure is consuming unexpected number of bytes"); 

int main(void) 
{ 
    printf("sizeof(int) %d\n",sizeof(int)); 
    printf("sizeof(float) %d\n",sizeof(float)); 
    printf("sizeof(char) %d\n",sizeof(char)); 
    printf("sizeof(struct customers) %d\n",sizeof(struct customers)); 
    return 0; 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

结构体通过名称 customers 定义,它包含几个成员。结构体的成员具有不同的数据类型。定义了一个编译时断言,对 customers 结构体的大小施加了 28 字节的精确约束。这意味着如果结构体的大小小于或大于 28 字节,程序将无法编译。main 函数简单地显示不同数据类型的大小,例如 intfloatchar。程序还显示了完整的 customers 结构体的大小。

程序使用 GCC 编译,如下面的截图所示。因为 customers 结构体的大小与编译时断言中指定的完全相同,所以程序编译完美,compileassert.c 程序成功编译成 .exe 文件:compileassert.exe。在执行文件后,我们得到以下输出,显示了不同数据类型和 customers 结构体的大小,如下面的截图所示:

图片

图 4.7

在修改 assert 函数中的值之后,即如果结构体的大小与编译时断言中提到的值不匹配,我们将得到以下编译错误:

图片

图 4.8

哇!我们已经成功实现了编译时断言,以便能够尽早捕捉到系统中的错误。现在,让我们继续下一个菜谱!

应用字符串化和标记粘贴运算符

字符串化或哈希符号 (#) 可以在宏定义中使用,将宏参数转换为字符串常量。你可以想象参数被双引号包围并返回。它也被称为标记连接运算符。

标记粘贴运算符 (##) 在宏定义中使用时将两个参数组合在一起。也就是说,每个 ## 运算符两侧的参数被连接成一个单独的字符串。更精确地说,它对两个参数执行字符串连接,以形成一个新字符串。

在这个菜谱中,我们将学习如何在计算中应用字符串化和标记粘贴运算符。用户被要求指定一定的披萨大小和他们的所需配料,相应地,披萨的价格将被显示。

如何做到这一点...

按照以下步骤创建一个使用stringizetoken-pasting运算符的配方:

  1. 使用token-pasting运算符定义一个名为pizzaprice的宏:
#define pizzaprice(a, b) a##b 
  1. 使用字符串化运算符定义一个名为convertIntoStr的宏:
#define convertIntoStr(str) #str 
  1. 定义一些变量,例如smallnormalmediumnormallargenormalsmallextra,它们代表不同大小和配料的披萨价格:
float smallnormal=5;
 float mediumnormal=7;
 float largenormal=10;
 float smallextra=7;
 float mediumextra=9;
 float largeextra=12;
 char pizzasize[30];
 char topping[20];
  1. 询问用户输入客户想要的披萨大小,输入的大小被分配给pizzasize变量:
printf("What size pizza you want? small/medium/large: ");
scanf("%s", pizzasize);
  1. 然后,询问用户是否希望披萨有普通奶酪或加奶酪,用户输入的选择被分配给topping变量:
printf("Normal or with extra cheese? normal/extra: ");
 scanf("%s",topping);
  1. 接下来,根据topping变量的值进行分支:
if(strcmp(topping, "normal")==0)
 {...........
}
 if(strcmp(topping, "extra")==0)
 {......................
}
  1. 此外,输入到pizzasize变量中的大小与检查比,以确定披萨大小是小型、中型还是大型,并相应地传递参数到pizzaprice宏:
if(strcmp(pizzasize, "small")==0)
 .........
 else
 if(strcmp(pizzasize, "medium")==0)
..............
else
.....................
  1. pizzaprice宏将pizzasizetopping参数连接起来,并将它们扩展为连接变量:
pizzaprice(small, extra));

在这里,small可以根据用户选择的大小替换为mediumlarge。此外,如果用户想要普通奶酪披萨,extra可以替换为normal

  1. 将连接变量的值显示为指定披萨和所需配料的定价:
printf("The prize for %s size pizza with %s toppings is $%.2f \n", pizzasize, topping, pizzaprice(small, extra));

应用stringizetoken-pasting运算符的程序如下所示:

// preconcat.c
#include <stdio.h> 
#include <string.h> 

#define pizzaprice(a, b) a##b 
#define convertIntoStr(str) #str 

int main() 
{ 
    float smallnormal=5; 
    float mediumnormal=7; 
    float largenormal=10; 
    float smallextra=7; 
    float mediumextra=9; 
    float largeextra=12; 
    char pizzasize[30]; 
    char topping[20]; 

    printf("What size pizza you want? small/medium/large: "); 
    scanf("%s", pizzasize); 
    printf("Normal or with extra cheese? normal/extra: "); 
    scanf("%s",topping); 
    if(strcmp(topping, "normal")==0) 
    { 
        if(strcmp(pizzasize, "small")==0)
            printf("The prize for %s size pizza with %s toppings is 
                    $%.2f \n", pizzasize, topping, 
                    pizzaprice(small, normal));
         else 
             if(strcmp(pizzasize, "medium")==0)
                  printf("The prize for %s size pizza with %s 
                          toppings is $%.2f \n", pizzasize, topping, 
                          pizzaprice(medium, normal)); 
              else
                  printf("The prize for %s size pizza with %s 
                          toppings is $%.2f \n", pizzasize, topping, 
                          pizzaprice(large, normal)); 
    }
    if(strcmp(topping, "extra")==0) 
    { 
        if(strcmp(pizzasize, "small")==0)
            printf("The prize for %s size pizza with %s toppings 
                    is $%.2f \n", pizzasize, topping, 
                    pizzaprice(small, extra)); 
        else 
            if(strcmp(pizzasize, "medium")==0) 
                printf("The prize for %s size pizza with %s toppings 
                        is $%.2f \n", pizzasize, topping, 
                        pizzaprice(medium, extra));
            else 
                printf("The prize for %s size pizza with %s toppings 
                        is $%.2f \n", pizzasize, topping, 
                        pizzaprice(large, extra)); 
    }
    printf(convertIntoStr(Thanks for visiting us)); 
    return 0; 
}

现在,让我们深入了解代码,以便更好地理解它。

它是如何工作的...

使用token-pasting运算符定义一个名为pizzaprice的宏。此宏将两个ab参数连接成一个字符串。此外,使用stringize运算符定义一个名为convertIntoStr的宏,该宏将str参数转换为字符串。定义了多个变量,例如smallnormalmediumnormallargenormalsmallextra。这些变量分别代表小型普通披萨的价格、中型普通披萨的价格、大型普通披萨的价格和加奶酪的小型披萨的价格。normal后缀表示这是含有常规奶酪量的披萨价格。extra后缀表示这个变量代表加奶酪披萨的价格。

提示用户输入客户订购的披萨大小。输入的大小被分配给pizzasize变量。之后,询问用户是否希望披萨有普通奶酪或加奶酪,输入的选择被分配给topping变量。

接下来,根据 topping 变量的值进行分支。如果配料是正常的,则将 pizzasize 中的字符串进行比较,以检查披萨大小是小型、中型还是大型,并相应地传递参数给 pizzaprice 宏。例如,如果用户输入小型作为披萨大小,配料为额外,则 pizzaprice 宏将使用两个参数(小型和额外)被调用。pizzaprice 宏作为一个标记粘贴操作符,将小型和额外字符串连接到 smallextra,因此 smallextra 变量的值将显示为带有额外奶酪配料的中小型披萨的价格。

pizzasizetopping 变量被合并成一个连接字符串,因此将访问相应变量的值。最后,调用 convertIntoStr 宏,该宏包含一个字符串化操作符,在账单末尾显示一个 Thanks for visiting us 字符串。

程序使用 GCC 编译,如下面的截图所示。由于编译过程中没有出现错误,preconcat.c 程序成功编译成 .exe 文件:preconcat.exe。执行该文件时,用户将被要求输入所需的披萨大小和配料,相应地,程序将显示披萨的价格,如下面的截图所示:

图 4.9

哇!我们已经成功应用了字符串化和标记粘贴操作符,并创建了自定义披萨订单。

第五章:深入探讨指针

当程序员需要以优化方式使用内存时,指针一直是他们的首选。指针使得访问任何变量、数组或数据类型的内容成为可能。你可以使用指针进行对任何内容的低级访问,并提高应用程序的整体性能。

在本章中,我们将探讨以下关于指针的食谱:

  • 使用指针反转字符串

  • 使用指针在数组中找到最大值

  • 对单链表进行排序

  • 使用指针找到矩阵的转置

  • 使用指针访问结构

在我们开始食谱之前,我想讨论一些与指针工作方式相关的事情。

什么是指针?

指针是一个包含另一个变量、数组或字符串内存地址的变量。当指针包含某个东西的地址时,它被称为指向那个东西。当指针指向某个东西时,它获得访问那个内存地址内容的权利。现在的问题是——我们为什么需要指针呢?

我们需要它们是因为它们执行以下操作:

  • 促进内存的动态分配

  • 提供一种访问数据类型的方法(除了变量名外,你还可以通过指针访问变量的内容)

  • 使得函数能够返回多个值

例如,考虑一个 i 整数变量:

int i;

当你定义一个整数变量时,内存中会为其分配两个字节。这组两个字节可以通过一个内存地址访问。变量分配的值存储在那个内存位置中,如下所示图:

图 5.1

在前面的图中,1000 代表 i 变量的内存地址。尽管在现实中,内存地址非常大,并且是十六进制格式,但为了简单起见,我使用了一个小的整数数字,100010 的值存储在 1000 的内存地址中,如下所示图:

现在,可以定义一个如下所示的 j 整数指针:

int *j;

通过以下语句,这个 j 整数指针可以指向 i 整数:

j=&i;

&(与号)符号代表地址,i 的地址将被分配给 j 指针,如下所示图所示。假设 2000 地址是 j 指针的地址,而 i 指针的地址,即 1000,存储在分配给 j 指针的内存位置中,如下所示图:

图 5.2

可以通过以下语句显示 i 整数的地址:

printf("Address of i is %d\n", &i); 
printf("Address of i is %d\n", j);

要显示 i 的内容,我们可以使用以下语句:

printf("Value of i is %d\n", i);
printf("Value of i is %d\n", *j);

在指针的情况下,&(与号)代表内存地址,*(星号)代表内存地址中的内容。

我们也可以通过以下语句定义一个指向整数指针的指针:

int **k;

这个指向k整型指针的指针可以使用以下语句指向j整型指针:

k=&j;

通过前面的语句,将j指针的地址分配给指向k整型指针的指针,如下面的图所示。假设3000k的内存地址:

图 5.3

现在,当你显示k的值时,它将显示j的地址:

printf("Address of j =%d %d \n",&j,k);

要显示ik的地址,我们需要使用*k,因为*k表示它将显示由k指向的内存地址的内容。现在,k指向j,而j中的内容是i的地址:

printf("Address of i = %d %d %d\n",&i,j,*k);

同样,要显示ik的值,必须使用**k如下所示:

printf("Value of i is %d %d %d %d \n",i,*(&i),*j,**k);

使用指针使我们能够精确地从所需的内存位置访问内容。但是,通过指针分配内存而不在任务完成后释放它可能会导致称为内存泄漏的问题。内存泄漏是一种资源泄漏。内存泄漏可能允许黑客未经授权访问内存内容,也可能阻止某些内容被访问,即使它们存在。

现在,让我们从这个章节的第一个菜谱开始。

使用指针反转字符串

在这个菜谱中,我们将学习如何使用指针反转一个字符串。最好的部分是,我们不会将字符串反转并复制到另一个字符串中,而是直接反转原始字符串本身。

如何做到这一点…

  1. 输入一个字符串并将其分配给str字符串变量,如下所示:
printf("Enter a string: ");
scanf("%s", str);
  1. 设置一个指针指向字符串,如下面的代码所示。指针将指向字符串的第一个字符的内存地址:
ptr1=str;
  1. 通过初始化一个n变量为1来找到字符串的长度。设置一个while循环,当指针到达字符串的空字符时执行,如下所示:
n=1;
while(*ptr1 !='\0')
{
  1. while循环内部,将执行以下操作:
  • 指针向前移动一个字符。

  • 变量n的值增加 1:

ptr1++;
n++;
  1. 指针将位于空字符,因此将指针向后移动一步,使其指向字符串的最后一个字符,如下所示:
ptr1--;
  1. 设置另一个指针指向字符串的开头,如下所示:
ptr2=str;
  1. 交换等于字符串长度一半的字符。为此,设置一个while循环执行n/2次,如下面的代码片段所示:
m=1;
while(m<=n/2)
  1. while循环内部,首先进行交换操作;即,我们的指针指向的字符被交换:
temp=*ptr1;
*ptr1=*ptr2;
*ptr2=temp;
  1. 在字符交换后,将第二个指针向前移动以指向其下一个字符,即字符串的第二个字符,并将第一个指针向后移动使其指向倒数第二个字符,如下所示:
ptr1--;
ptr2++;
  1. 重复此过程 n/2 次,其中 n 是字符串的长度。当while循环结束时,我们将在屏幕上显示原始字符串的反转形式:
printf("Reverse string is %s", str);

使用指针反转字符串的reversestring.c程序如下:

#include <stdio.h>
void main()
{
    char str[255], *ptr1, *ptr2, temp ;
    int n,m;
    printf("Enter a string: ");
    scanf("%s", str);
    ptr1=str;
    n=1;
    while(*ptr1 !='\0')
    {
        ptr1++;
        n++;
    }
    ptr1--;
    ptr2=str;
    m=1;
    while(m<=n/2)
    {
        temp=*ptr1;
        *ptr1=*ptr2;
        *ptr2=temp;
        ptr1--;
        ptr2++;;
        m++;
    }
    printf("Reverse string is %s", str);
}

现在,让我们看看幕后。

它是如何工作的...

我们将被提示输入一个字符串,该字符串将被分配给str变量。字符串不过是一个字符数组。假设我们输入的名字是manish,名字中的每个字符将依次分配到数组的某个位置(参见图 5.4)。我们可以看到,字符串的第一个字符,字母m,被分配到str[0]位置,接着第二个字符串字符被分配到str[1]位置,以此类推。空字符,像往常一样,位于字符串的末尾,如下面的图所示:

图 5.4

要反转字符串,我们将寻求两个指针的帮助:一个将被设置为指向字符串的第一个字符,另一个将被设置为指向字符串的最后一个字符。因此,第一个ptr1指针被设置为如下指向字符串的第一个字符:

图 5.5

字符的交换必须执行到字符串长度的一半;因此,下一步将是找到字符串的长度。在找到字符串的长度后,ptr1指针将被设置为移动到字符串的最后一个字符。

此外,另一个ptr2指针被设置为指向字符串的第一个字符m,如下面的图所示:

图 5.6

下一步是将ptr1ptr2指针所指的字符串的第一个和最后一个字符进行交换(参见图 5.7 (a))。在交换ptr1ptr2指针所指的字符后,字符串将如图图 5.7 (b)所示:

图 5.7

在交换第一个和最后一个字符之后,我们将交换字符串的第二和倒数第二个字符。为此,ptr2指针将被向前移动并设置为指向下一个字符,而ptr1指针将被向后移动并设置为指向倒数第二个字符。

你可以在下面的图**5.8 (a)中看到,ptr2ptr1指针被设置为指向as字符。一旦这样做,ptr2ptr1指针所指的字符将再次进行交换。交换as字符后,字符串将如下所示(图 5.8 (b)):

图 5.8

现在剩下的唯一任务是交换字符串中的第三个和倒数第三个字符。因此,我们将重复ptr2ptr1指针的重新定位过程。在交换字符串中的ni字符后,原始的str字符串将被反转,如下所示:

图 5.9

在应用上述步骤之后,如果我们打印**str**字符串,它将以相反的顺序出现。

让我们使用 GCC 编译reversestring.c程序,如下所示:

D:\CBook>gcc reversestring.c -o reversestring

如果你没有错误或警告,这意味着reversestring.c程序已被编译成可执行文件,称为reversestring.exe。让我们按照以下方式运行此可执行文件:

D:\CBook>./reversestring
Enter a string: manish
Reverse string is hsinam

哇!我们已经成功使用指针反转了一个字符串。现在,让我们继续下一个菜谱!

使用指针在数组中查找最大值

在这个菜谱中,将使用指针扫描数组的所有元素。

如何操作...

  1. 使用以下方式定义一个名为max的宏,大小为100
#define max 100
  1. 定义一个p整数数组,大小为max,如下所示:
int p[max]
  1. 指定数组中元素的数量如下:
printf("How many elements are there? ");
scanf("%d", &n);
  1. 按如下所示输入数组的元素:
for(i=0;i<n;i++)
    scanf("%d",&p[i]);
  1. 定义两个mxptr指针,如下所示,以指向数组的第一个元素:
mx=p;
ptr=p;
  1. mx指针将始终指向数组的最大值,而ptr指针将用于比较数组剩余的值。如果mx指针指向的值小于ptr指针指向的值,则mx指针设置为指向ptr指针指向的值。然后,ptr指针将移动到指向下一个数组元素,如下所示:
if (*mx < *ptr)
    mx=ptr;
  1. 如果mx指针指向的值大于ptr指针指向的值,则mx指针保持不变,并继续指向相同的值,而ptr指针将移动到指向下一个数组元素,以便进行以下比较:
ptr++;
  1. 此过程会重复进行,直到数组(由ptr指针指向)的所有元素都与由mx指针指向的元素进行比较。最后,mx指针将指向数组中的最大值。要显示数组的最大值,只需显示由mx指针指向的数组元素,如下所示:
printf("Largest value is %d\n", *mx);

使用指针查找数组中最大值的largestinarray.c程序如下:

#include <stdio.h>
#define max 100
void main()
{
    int p[max], i, n, *ptr, *mx;
    printf("How many elements are there? ");
    scanf("%d", &n);
    printf("Enter %d elements \n", n);
    for(i=0;i<n;i++)
        scanf("%d",&p[i]);
    mx=p;
    ptr=p;
    for(i=1;i<n;i++)
    {
        if (*mx < *ptr)
            mx=ptr;
        ptr++;
    }
    printf("Largest value is %d\n", *mx);
}

现在,让我们看看幕后。

它是如何工作的...

定义一个特定大小的数组,并在其中输入一些元素。这些将是我们要找到最大值的值。输入一些元素后,数组可能如下所示:

图 5.10

我们将使用两个指针来查找数组中的最大值。让我们将这两个指针命名为mxptr,其中mx指针将用于指向数组的最大值,而ptr指针将用于将数组的其余元素与mx指针指向的值进行比较。最初,两个指针都设置为指向数组的第一个元素p[0],如下面的图所示:

图片

图 5.11

然后,ptr指针被移动到指向数组的下一个元素,p[1]。然后,mxptr指针指向的值将被比较。这个过程会一直持续到数组中的所有元素都被比较,如下所示:

图片

图 5.12

回想一下,我们希望mx指针保持指向较大的值。由于 15 大于 3(见图 5.13),mx指针的位置将保持不变,而ptr指针将移动到指向下一个元素,p[2],如下所示:

图片

图 5.13

再次,mxptr指针指向的值,分别是 15 和 70,将被比较。现在,mx指针指向的值小于ptr指针指向的值。因此,mx指针将被设置为指向与ptr相同的数组元素,如下所示:

图片

图 5.14

数组元素的比较将继续。想法是保持mx指针指向数组中的最大元素,如下面的图所示:

图片

图 5.15

图 5.15所示,70大于20,因此mx指针将保持在p[2],而ptr指针将移动到下一个元素,p[4]。现在,ptr指针指向数组的最后一个元素。因此,程序将终止,显示mx指针指向的最后一个值,这恰好是数组中的最大值。

让我们使用 GCC 编译largestinarray.c程序,如下所示:

D:\CBook>gcc largestinarray.c -o largestinarray

如果你没有错误或警告,这意味着largestinarray.c程序已经被编译成一个可执行文件,largestinarray.exe。现在,让我们按照以下方式运行这个可执行文件:

D:\CBook>./largestinarray
How many elements are there? 5
Enter 5 elements
15
3
70
35
20
Largest value is 70
You can see that the program displays the maximum value in the array

哇!我们已经成功使用指针在数组中找到了最大值。现在,让我们继续下一个菜谱!

单链表的排序

在这个菜谱中,我们将学习如何创建一个由整数元素组成的单链表,然后我们将学习如何按升序排序这个链表。

单链表由几个通过指针连接的节点组成。单链表中的一个节点可能如下所示:

图片

图 5.16

如你所见,单链表的一个节点是由两个部分组成的结构:

  • 数据:这可以是一个或多个变量(也称为成员),可以是整数、浮点数、字符串或任何数据类型。为了使程序简单,我们将data作为一个整型变量。

  • 指针:这个指针将指向类型节点的结构。在这个程序中,我们可以将其称为next指针,尽管它可以有任何一个名字。

我们将使用冒泡排序来对链表进行排序。冒泡排序是一种顺序排序技术,通过比较相邻元素进行排序。它将第一个元素与第二个元素进行比较,第二个元素与第三个元素进行比较,依此类推。如果元素不是按预期顺序排列,则交换它们的值。例如,如果你正在按升序排序元素,并且第一个元素大于第二个元素,它们的值将被交换。同样,如果第二个元素大于第三个元素,它们的值也将被交换。

这样,你会发现,在第一次迭代的结束时,最大的值会冒泡到列表的末尾。在第二次迭代后,第二大的值将被冒泡到列表的末尾。总的来说,使用冒泡排序算法对 n 个元素进行排序需要 n-1 次迭代。

让我们了解创建和排序单链表的步骤。

如何做到这一点...

  1. 定义一个包含两个成员的节点—datanextdata成员用于存储整数值,而next成员是一个指针,用于将节点链接如下:
struct node
{
  int data;
  struct node *next;
};
  1. 指定链表中的元素数量。输入的值将被分配给n变量,如下所示:
printf("How many elements are there in the linked list ?");
scanf("%d",&n);
  1. 执行一个for循环,循环n次。在for循环内部,创建一个名为newNode的节点。当被要求时,输入一个整数值分配给newNode的数据成员,如下所示:
newNode=(struct node *)malloc(sizeof(struct node));
scanf("%d",&newNode->data);
  1. 设置两个指针,startListtemp1,以指向第一个节点。startList指针将始终指向链表的第一个节点。temp1指针将用于链接节点,如下所示:
startList = newNode;
temp1=startList;
  1. 为了连接新创建的节点,执行以下两个任务:
  • temp1的下一个成员设置为指向新创建的节点。

  • temp1指针被移动以指向新创建的节点,如下所示:

temp1->next = newNode;
temp1=newNode;
  1. for循环结束时,我们将有一个单链表,其第一个节点由startList指向,最后一个节点的下一个指针指向 NULL。这个链表已经准备好进行排序过程。设置一个for循环,从0执行到n-2,即 n-1 次迭代,如下所示:
for(i=n-2;i>=0;i--)
  1. for循环内部,为了比较值,使用两个指针,temp1temp2。最初,temp1temp2将被设置为指向链表的前两个节点,如下面的代码片段所示:
temp1=startList;
temp2=temp1->next;
  1. 在以下代码中比较temp1temp2所指向的节点:
if(temp1->data > temp2->data)
  1. 比较前两个节点后,temp1temp2指针将被设置为指向第二个和第三个节点,依此类推:
temp1=temp2;
temp2=temp2->next;
  1. 链表必须按升序排列,因此temp1的数据成员必须小于temp2的数据成员。如果temp1的数据成员大于temp2的数据成员,则使用临时变量k交换数据成员的值,如下所示:
k=temp1->data;
temp1->data=temp2->data;
temp2->data=k;
  1. 经过 n-1 次迭代比较和交换连续值后,如果一对中的第一个值大于第二个值,则链表中的所有节点将按升序排列。为了遍历链表并按升序显示值,设置一个临时的t指针指向由startList指向的节点,即链表的第一节点,如下所示:
t=startList;
  1. 一个while循环执行,直到t指针达到NULL。回想一下,最后一个节点的下一个指针被设置为 NULL,因此while循环将执行,直到遍历链表中的所有节点,如下所示:
while(t!=NULL)
  1. while循环中,将执行以下两个任务:
  • 指针t所指向的节点数据成员被显示。

  • t指针进一步移动以指向其下一个节点:

printf("%d\t",t->data);
t=t->next;

创建单链表并按升序排序的sortlinkedlist.c程序如下:

/* Sort the linked list by bubble sort */
#include<stdio.h>
#include <stdlib.h>
struct node
{
  int data;
  struct node *next;
};
void main()
{
    struct node *temp1,*temp2, *t,*newNode, *startList;
    int n,k,i,j;
    startList=NULL;
    printf("How many elements are there in the linked list ?");
    scanf("%d",&n);
    printf("Enter elements in the linked list\n");
    for(i=1;i<=n;i++)
    {
        if(startList==NULL)
        {
            newNode=(struct node *)malloc(sizeof(struct node));
            scanf("%d",&newNode->data);
            newNode->next=NULL;
            startList = newNode;
            temp1=startList;
        }
        else
        {
            newNode=(struct node *)malloc(sizeof(struct node));
            scanf("%d",&newNode->data);
            newNode->next=NULL;
            temp1->next = newNode;
            temp1=newNode;
        }
    }
    for(i=n-2;i>=0;i--)
    {
        temp1=startList;
        temp2=temp1->next;
        for(j=0;j<=i;j++)
        {
            if(temp1->data > temp2->data)
            {
                k=temp1->data;
                temp1->data=temp2->data;
                temp2->data=k;
            }
            temp1=temp2;
            temp2=temp2->next;
        }
    }
    printf("Sorted order is: \n");
    t=startList;
    while(t!=NULL)
    {
        printf("%d\t",t->data);
        t=t->next;
    }
}

现在,让我们看看幕后。

它是如何工作的...

此程序分为两部分——第一部分是创建单链表,第二部分是排序链表。

让我们从第一部分开始。

创建单链表

我们将首先创建一个名为newNode的新节点。当提示输入时,我们将输入其数据成员的值,然后设置下一个newNode指针为NULL(如图 5.17 所示)。这个下一个指针将用于与其他节点连接(正如我们很快将看到的):

图 5.17

创建第一个节点后,我们将按照以下方式使两个指针指向它:

  • startList:为了遍历单链表,我们需要一个指向列表第一个节点的指针。因此,我们将定义一个名为startList的指针,并将其设置为指向列表的第一个节点。

  • temp1:为了与下一个节点连接,我们还需要一个额外的指针。我们将把这个指针称为temp1,并将其设置为指向newNode(见图 5.18):

图 5.18

现在,我们将为链表创建另一个节点,并将其命名为 newNode。指针一次只能指向一个结构。因此,当我们创建一个新节点时,之前指向第一个节点的 newNode 指针现在将指向最近创建的节点。我们将被提示输入新节点数据成员的值,其下一个指针将被设置为 NULL

你可以在下面的图中看到,两个指针 startListtemp1 指向第一个节点,而 newNode 指针指向新创建的节点。如前所述,startList 将用于遍历链表,而 temp1 将用于连接新创建的节点,如下所示:

图 5.19

要将第一个节点与 newNode 连接,temp1 的下一个指针将被设置为指向 newNode(参见 图 5.20(a))。连接 newNode 后,temp1 指针将被进一步移动并设置为指向 newNode(参见 图 5.20(b)),以便它可以再次用于连接未来可能添加到链表中的任何新节点:

图 5.20

步骤三和四将重复应用于链表中的其余节点。最后,单链表将准备就绪,看起来可能如下所示:

图 5.21

现在我们已经创建了单链表,下一步是将链表按升序排序。

对单链表进行排序

我们将使用冒泡排序算法对链表进行排序。在冒泡排序技术中,第一个值与第二个值进行比较,第二个值与第三个值进行比较,依此类推。如果我们想按升序排序我们的列表,那么在比较值时,我们需要将较小的值保持在顶部。

因此,在比较第一个和第二个值时,如果第一个值大于第二个值,则它们的顺序将被交换。如果第一个值小于第二个值,则不会发生交换,并将继续比较第二个和第三个值。

将会有 n-1 次这样的比较迭代,这意味着如果有五个值,那么将有四次这样的比较迭代;并且每次迭代后,最后一个值将被排除在外——也就是说,当它达到目的地时不会进行比较。这里的“目的地”是指当按升序排列时必须保持值的那个位置。

第一次迭代

为了对链表进行排序,我们将使用两个指针——temp1temp2temp1 指针被设置为指向第一个节点,而 temp2 被设置为指向下一个节点,如下所示:

图 5.22

我们将按升序对链表进行排序,因此我们将较小的值保持在列表的起始位置。temp1temp2 的数据成员将被比较。因为 temp1->data 大于 temp2->data,即 temp1 的数据成员大于 temp2 的数据成员,它们的顺序将被交换(见以下图表)。在交换 temp1temp2 指向的节点数据成员之后,链表将呈现如下:

图 5.23

然后,这两个指针将进一步移动,即 temp1 指针将被设置为指向 temp2,而 temp2 指针将被设置为指向其下一个节点。我们可以在 图 5.24 (a) 中看到 temp1temp2 指针分别指向值为 3 和 7 的节点。我们还可以看到 temp1->data 小于 temp2->data,即 3 < 7。由于 temp1 的数据成员已经小于 temp2 的数据成员,因此不会发生值交换,两个指针将简单地再向前移动一步(见 图 5.24 (b))。

现在,因为 7 > 4,它们的顺序将被交换。temp1temp2 指向的数据成员的值将按以下方式交换(图 5.24 (c)):

图 5.24

之后,temp1temp2 指针将再向前移动一步,即 temp1 将指向 temp2,而 temp2 将移动到其下一个节点。我们可以在下面的 图 5.25 (a) 中看到 temp1temp2 分别指向值为 7 和 2 的节点。再次,temp1temp2 的数据成员将被比较。因为 temp1->data 大于 temp2->data,它们的顺序将被交换。图 5.25 (b) 显示了交换数据成员值后的链表:

图 5.25

这是一次迭代,你可以注意到在这次迭代之后,最大的值 7 已经被设置到我们期望的位置——链表的末尾。这也意味着在第二次迭代中,我们不需要比较最后一个节点。同样,在第二次迭代之后,第二大的值将达到或被设置到其实际位置。链表中的第二大的值是 4,因此,在第二次迭代之后,四个节点将刚好到达七个节点。如何做到?让我们看看冒泡排序的第二次迭代。

第二次迭代

我们将从比较前两个节点开始,因此 **temp1****temp2** 指针将被设置为分别指向链表的第一个和第二个节点(参见 图 5.26 (a))。将比较 **temp1****temp2** 的数据成员。如清晰可见,temp1->data 小于 temp2->data(即 1 < 7),因此它们的位置不会互换。之后,**temp1****temp2** 指针将再向前移动一步。我们可以在 图 5.26 (b) 中看到,**temp1****temp2** 指针被设置为分别指向值为 3 和 4 的节点:

图 5.26

再次,将比较 **temp1****temp2** 指针的数据成员。因为 temp1->data 小于 temp2->data,即 3 < 4,它们的位置再次不会互换,**temp1****temp2** 指针将再次向前移动一步。也就是说,**temp1** 指针将被设置为指向 **temp2**,而 **temp2** 将被设置为指向它的下一个节点。您可以在 图 5.27 (a) 中看到,temp1temp2 指针被设置为分别指向值为 4 和 2 的节点。因为 4 > 2,它们的位置将互换。在交换这些值的位置后,链表在 图 5.27 (b) 中将如下所示:

图 5.27

这是第二次迭代的结束,我们可以看到第二大值,四,按照升序被设置到我们期望的位置。因此,在每次迭代中,一个值将被设置到所需的位置。相应地,下一次迭代将需要少一次比较。

第三次和第四次迭代

在第三次迭代中,我们只需要进行以下比较:

  1. 比较第一个和第二个节点

  2. 比较第二个和第三个节点

在第三次迭代后,第三大的值,即三,将被设置在我们期望的位置,即在节点四之前。

在第四次,也是最后一次迭代中,只有第一个和第二个节点将被比较。经过第四次迭代后,链表将按升序排序如下:

图 5.28

让我们使用 GCC 来编译 sortlinkedlist.c 程序,如下所示:

D:\CBook>gcc sortlinkedlist.c -o sortlinkedlist

如果你没有收到任何错误或警告,这意味着 sortlinkedlist.c 程序已经被编译成一个可执行文件,sortlinkedlist.exe。让我们按照以下步骤运行这个可执行文件:

D:\CBook>./sortlinkedlist
How many elements are there in the linked list ?5
Enter elements in the linked list
3
1
7
4
2
Sorted order is:
1       2       3       4       7

哇!我们已经成功创建并排序了一个单链表。现在,让我们继续下一个菜谱!

使用指针寻找矩阵的转置

这个菜谱最好的部分是,我们不仅将使用指针显示矩阵的转置,而且我们还将使用指针创建矩阵本身。

矩阵的转置是一个新矩阵,其行数等于原始矩阵的列数,列数等于原始矩阵的行数。以下图表显示了2 x 3阶矩阵及其3 x 2阶转置:

图 5.29

基本上,我们可以这样说,当将矩阵的行转换为列,列转换为行时,你得到的就是它的转置。

如何操作…

  1. 定义一个 10 行 10 列的矩阵如下(如果你愿意,可以有一个更大的矩阵):
int a[10][10]
  1. 按如下输入行和列的大小:
    printf("Enter rows and columns of matrix: ");
    scanf("%d %d", &r, &c);
  1. 为保持矩阵元素,分配等于r * c数量的内存位置如下:
    ptr = (int *)malloc(r * c * sizeof(int));
  1. 按如下顺序将矩阵的元素输入到每个分配的内存中:
    for(i=0; i<r; ++i)
    {
        for(j=0; j<c; ++j)
        {
            scanf("%d", &m);
             *(ptr+ i*c + j)=m;
        }
    }
  1. 为了通过指针访问此矩阵,将ptr指针设置为指向分配的内存块的第一个内存位置,如图 5.30所示。当ptr指针设置为指向第一个内存位置时,它将自动获取第一个内存位置的地址,因此1000将被分配给ptr指针:

图 5.30

  1. 要访问这些内存位置并显示其内容,请在嵌套循环中使用*(ptr +i*c + j)公式,如以下代码片段所示:
for(i=0; i<r; ++i)
{
    for(j=0; j<c; ++j)
    {
        printf("%d\t",*(ptr +i*c + j));
    }
    printf("\n");
}
  1. 假设行的值r为二,列的值c为三。当i=0j=0时,公式将计算如下:
*(ptr +i*c + j);
*(1000+0*3+0)
*1000

它将显示内存地址1000的内容。

i=0j=1时,公式将计算如下:

*(ptr +i*c + j);
*(1000+0*3+1)
*(1000+1)
*(1002)

我们首先将*(1000+1),因为ptr指针是一个整数指针,每次我们在每个内存位置将值1加到它上面时,它将自动跳过两个字节,从而得到*(1002),并显示内存位置1002的内容。

同样,i=0j=2的值将导致*(1004);即显示内存位置1004的内容。使用此公式,i=1j=0将导致*(1006)i=1j=1将导致*(1008)i=1j=2将导致*(1010)。因此,当在嵌套循环中应用上述公式时,原始矩阵将显示如下:

图 5.31

  1. 要显示矩阵的转置,请在嵌套循环中应用以下公式:
*(ptr +j*c + i))

再次假设行的值(r=2)和列的值(c=3),以下内存位置的内容将被显示:

i j 内存地址
0 0 1000
0 1 1006
1 0 1002
1 1 1008
2 0 1004
2 1 1010

因此,应用上述公式后,以下内存地址的内容将显示为图 5.32中的以下内容。这些内存地址的内容将构成矩阵的转置:

图 5.32

让我们看看这个公式如何在程序中应用。

使用指针显示矩阵转置的transposemat.c程序如下:

#include <stdio.h>
#include <stdlib.h>
void main()
{
    int a[10][10],  r, c, i, j, *ptr,m;
    printf("Enter rows and columns of matrix: ");
    scanf("%d %d", &r, &c);
    ptr = (int *)malloc(r * c * sizeof(int));
    printf("\nEnter elements of matrix:\n");
    for(i=0; i<r; ++i)
    {
        for(j=0; j<c; ++j)
        {
            scanf("%d", &m);
             *(ptr+ i*c + j)=m;
        }
    }
    printf("\nMatrix using pointer is: \n");
    for(i=0; i<r; ++i)
    {
        for(j=0; j<c; ++j)
        {
           printf("%d\t",*(ptr +i*c + j));
        }
        printf("\n");
    }
    printf("\nTranspose of Matrix:\n");
    for(i=0; i<c; ++i)
    {
        for(j=0; j<r; ++j)
        {
             printf("%d\t",*(ptr +j*c + i));
        }
        printf("\n");
   }
}

现在,让我们看看幕后。

它是如何工作的...

每当定义一个数组时,它内部分配的内存是顺序内存。现在让我们定义一个 2 x 3 大小的矩阵,如图所示。在这种情况下,矩阵将被分配六个连续的内存位置,每个位置两个字节(参见图 5.33)。为什么是每个位置两个字节?这是因为一个整数占用两个字节。这也意味着如果我们定义一个浮点类型的矩阵,它占用四个字节,每个分配的内存位置将包含四个字节:

图 5.33

实际上,内存地址很长,并且是十六进制格式;但为了简单起见,我们将使用整数类型的内存地址,并使用易于记忆的数字,如1000,作为内存地址。在内存地址1000之后,下一个内存地址是1002(因为一个整数占用两个字节)。

现在,为了使用指针以行主序形式显示原始矩阵元素,我们需要显示内存位置的元素,100010021004以及如此等等:

图 5.34

同样,为了使用指针显示矩阵的转置,我们需要显示内存位置的元素;100010061002100810041010

图 5.35

让我们使用 GCC 编译transposemat.c程序,如下所示:

D:\CBook>gcc transposemat.c -o transposemat

如果你没有错误或警告,这意味着transposemat.c程序已经被编译成可执行文件,transposemat.exe。让我们用以下代码片段运行这个可执行文件:

D:\CBook>./transposemat
Enter rows and columns of matrix: 2 3

Enter elements of matrix:
1
2
3
4
5
6

Matrix using pointer is:
1       2       3
4       5       6

Transpose of Matrix:
1       4
2       5
3       6

哇!我们已经成功使用指针找到了矩阵的转置。现在,让我们继续下一个菜谱!

使用指针访问结构

在这个菜谱中,我们将创建一个结构来存储特定客户下单的信息。结构是一种用户定义的数据类型,可以在其中存储不同数据类型的多个成员。该结构将包含用于存储订单号、电子邮件地址和密码的成员:

struct cart
{  
    int orderno;
    char emailaddress[30];
    char password[30];
};

上述结构被命名为cart,包含三个成员——用于存储客户下单顺序号的int类型成员orderno,以及用于存储客户电子邮件地址和密码的字符串类型成员emailaddresspassword。让我们开始吧!

如何做到这一点…

  1. 定义一个名为mycartcart结构。同时,定义两个指向cart结构的指针ptrcartptrcust,如下面的代码片段所示:
struct cart mycart;
struct cart *ptrcart, *ptrcust;
  1. 输入客户的订单号、电子邮件地址和密码,这些值将通过mycart结构变量接受。如前所述,点运算符(.)将用于通过结构变量访问结构成员ordernoemailaddresspassword,如下所示:
printf("Enter order number: ");
scanf("%d",&mycart.orderno);
printf("Enter email address: ");
scanf("%s",mycart.emailaddress);
printf("Enter password: ");
scanf("%s",mycart.password);
  1. 使用ptrcart=&mycart语句将ptrcart结构指针设置为指向mycart结构。因此,ptrcart结构指针将能够通过使用箭头(->)运算符访问mycart结构的成员。通过使用ptrcart->ordernoptrcart->emailaddressptrcart->password,可以访问分配给ordernoemailaddresspassword结构成员的值并显示它们:
printf("\nDetails of the customer are as follows:\n");
printf("Order number : %d\n", ptrcart->orderno);
printf("Email address : %s\n", ptrcart->emailaddress);
printf("Password : %s\n", ptrcart->password);
  1. 我们还将通过让客户输入新的电子邮件地址和密码并接受通过指向ptrcart结构的新的详细信息来修改客户的电子邮件地址和密码。因为ptrcart指向mycart结构,新的电子邮件地址和密码将覆盖分配给mycart结构成员的现有值:
printf("\nEnter new email address: ");
scanf("%s",ptrcart->emailaddress);
printf("Enter new password: ");
scanf("%s",ptrcart->password);
/*The new modified values of orderno, emailaddress and password members are displayed using structure variable, mycart using dot operator (.).*/
printf("\nModified customer's information is:\n");
printf("Order number: %d\n", mycart.orderno);
printf("Email address: %s\n", mycart.emailaddress);
printf("Password: %s\n", mycart.password);
  1. 然后,定义一个指向*ptrcust结构的指针。使用以下malloc函数为其分配内存。sizeof函数将找出每个结构成员消耗的字节数,并返回整个结构消耗的总字节数:
ptrcust=(struct cart *)malloc(sizeof(struct cart));
  1. 输入客户的订单号、电子邮件地址和密码,所有这些值将通过指向结构的指针分配给相应的结构成员,如下所示。显然,将使用箭头运算符(->)通过指向结构的指针访问结构成员:
printf("Enter order number: ");
scanf("%d",&ptrcust->orderno);
printf("Enter email address: ");
scanf("%s",ptrcust->emailaddress);
printf("Enter password: ");
scanf("%s",ptrcust->password);
  1. 然后,通过以下方式再次通过指向ptrcust结构的指针显示用户输入的值:
printf("\nDetails of the second customer are as follows:\n");
printf("Order number : %d\n", ptrcust->orderno);
printf("Email address : %s\n", ptrcust->emailaddress);
printf("Password : %s\n", ptrcust->password);

以下pointertostruct.c程序解释了如何使用指针访问结构:

#include <stdio.h>
#include <stdlib.h>

struct cart
{
    int orderno;
    char emailaddress[30];
    char password[30];
};

void main()
{
    struct cart mycart;
    struct cart *ptrcart, *ptrcust;
    ptrcart = &mycart;
    printf("Enter order number: ");
    scanf("%d",&mycart.orderno);
    printf("Enter email address: ");
    scanf("%s",mycart.emailaddress);
    printf("Enter password: ");
    scanf("%s",mycart.password);
    printf("\nDetails of the customer are as follows:\n");
    printf("Order number : %d\n", ptrcart->orderno);
    printf("Email address : %s\n", ptrcart->emailaddress);
    printf("Password : %s\n", ptrcart->password);

    printf("\nEnter new email address: ");
    scanf("%s",ptrcart->emailaddress);
    printf("Enter new password: ");
    scanf("%s",ptrcart->password);
    printf("\nModified customer's information is:\n");
    printf("Order number: %d\n", mycart.orderno);
    printf("Email address: %s\n", mycart.emailaddress);
    printf("Password: %s\n", mycart.password);

    ptrcust=(struct cart *)malloc(sizeof(struct cart));
    printf("\nEnter information of another customer:\n");
    printf("Enter order number: ");
    scanf("%d",&ptrcust->orderno);
    printf("Enter email address: ");
    scanf("%s",ptrcust->emailaddress);
    printf("Enter password: ");
    scanf("%s",ptrcust->password);
    printf("\nDetails of the second customer are as follows:\n");
    printf("Order number : %d\n", ptrcust->orderno);
    printf("Email address : %s\n", ptrcust->emailaddress);
    printf("Password : %s\n", ptrcust->password);
}

现在,让我们看看幕后。

它是如何工作的...

当您定义一个结构类型的变量时,该变量可以以下格式访问结构成员:

structurevariable.structuremember

您可以在结构变量和结构成员之间看到一个点(.)。这个点(.)也被称为点运算符,或成员访问运算符。以下示例将使其更清晰:

struct cart mycart;
mycart.orderno

在前面的代码中,您可以看到mycart被定义为cart结构的结构变量。现在,mycart结构变量可以通过使用成员访问运算符(.)访问orderno成员。

您还可以定义一个指向结构的指针。以下语句将ptrcart定义为指向cart结构的指针。

struct cart *ptrcart;

当结构体指针指向一个结构体变量时,它可以访问该结构体变量的结构体成员。在以下语句中,指向 ptrcart 结构体的指针指向 mycart 结构体变量的地址:

ptrcart = &mycart;

现在,ptrcart 可以访问结构体成员,但将使用箭头操作符 (->) 而不是点操作符 (.)。以下语句使用指向结构体的指针访问结构体的 orderno 成员:

ptrcart->orderno

如果你不想让结构体指针指向结构体变量,那么需要为指向结构体的指针分配内存以访问结构体成员。以下语句通过为它分配内存来定义一个指向结构体的指针:

ptrcust=(struct cart *)malloc(sizeof(struct cart));

上述代码为 cart 结构体分配了与结构体大小相等的内存,将该内存类型转换为指向 cart 结构体的指针,并将分配的内存赋值给 ptrcust。换句话说,ptrcust 被定义为指向结构体的指针,它不需要指向任何结构体变量,但可以直接访问结构体成员。

让我们使用 GCC 编译 pointertostruct.c 程序,如下所示:

D:\CBook>gcc pointertostruct.c -o pointertostruct

如果你没有收到任何错误或警告,这意味着 pointertostruct.c 程序已经被编译成一个可执行文件,名为 pointertostruct.exe。让我们按照以下方式运行这个可执行文件:

D:\CBook>./pointertostruct
Enter order number: 1001
Enter email address: bmharwani@yahoo.com
Enter password: gold

Details of the customer are as follows:
Order number : 1001
Email address : bmharwani@yahoo.com
Password : gold

Enter new email address: harwanibm@gmail.com
Enter new password: diamond

Modified customer's information is:
Order number: 1001
Email address: harwanibm@gmail.com
Password: diamond

Enter information of another customer:
Enter order number: 1002
Enter email address: bintu@yahoo.com
Enter password: platinum

Details of the second customer are as follows:
Order number : 1002
Email address : bintu@yahoo.com
Password : platinum

哇!我们已经成功使用指针访问了一个结构体。

第六章:文件处理

数据存储是所有应用程序的必备功能。当我们运行程序时输入任何数据,该数据将存储为 RAM,这意味着它是临时的。当我们下次运行程序时,我们将无法获取该数据。但如果我们希望数据保留在那里,以便在需要时再次引用它,那么在这种情况下,我们必须存储数据。

基本上,我们希望我们的数据能够被存储,并且可以在需要时随时访问和重用。在 C 语言中,数据存储可以通过传统的文件处理技术以及数据库系统来完成。以下是 C 语言中可用的两种文件处理类型:

  • 顺序文件处理:数据以简单的文本格式写入,可以顺序读取和写入。要读取第 n 行,我们必须首先读取 n-1 行。

  • 随机文件处理:数据以字节形式写入,可以随机读取或写入。我们可以通过将文件指针定位在所需位置来随机读取或写入任何行。

在本章中,我们将使用文件处理方法介绍以下食谱:

  • 读取文本文件并将句号后面的所有字符转换为大写

  • 以逆序显示随机文件的内容

  • 计算文件中元音字母的数量

  • 用另一个词替换文件中的词

  • 加密文件

在我们开始编写食谱之前,让我们回顾一下我们将使用的一些函数。

文件处理中使用的函数

我将本节分为两部分。在第一部分,我们将探讨与顺序文件处理方法相关的特定函数。在第二部分,我们将探讨用于随机文件的函数。

常用于顺序文件处理的函数

以下是一些用于在顺序文件中打开、关闭、读取和写入的函数。

fopen()

fopen()函数用于打开文件进行读取、写入和其他操作。以下是它的语法:

FILE *fopen (const char *file_name, const char *mode)

在这里,file_name代表我们想要操作的文件,而mode说明了我们打开文件的目的。它可以是指以下任何一种:

  • r: 这将以读取模式打开文件并将文件指针设置为文件的第一字符。

  • w: 以写入模式打开文件。如果文件已存在,它将被覆盖。

  • a: 以追加模式打开文件。新输入的数据将被添加到文件末尾。

  • r+: 这将以读写模式打开文件。文件指针被设置为指向文件的开头。如果文件已存在,则内容不会被删除。如果文件不存在,则不会创建文件。

  • w+: 这也将文件以读写模式打开。文件指针被设置为指向文件的开头。如果文件已存在,则内容将被删除,但如果文件不存在,则将被创建。

  • a+: 这将以读取和追加新内容的方式打开文件。

fopen函数返回一个文件描述符,指向文件以执行不同的操作。

fclose()

fclose()函数用于关闭文件。以下是它的语法:

int fclose(FILE *file_pointer)

在这里,file_pointer代表指向打开文件的文件指针。

如果文件成功关闭,函数返回0值。

fgets()

fgets()函数用于从指定的文件中读取一个字符串。以下是它的语法:

char *fgets(char *string, int length, FILE *file_pointer)

此函数具有以下特性:

  • string: 这代表一个字符数组,从文件中读取的数据将被分配到这个数组中。

  • length: 这代表可以从文件中读取的最大字符数。将读取length-1个字符。从文件中读取数据将在length-1位置或在新行字符\n处停止,哪个先到就停止。

  • file_pointer: 这代表指向文件的文件指针。

fputs()

fputs()函数用于写入文件。以下是它的语法:

int fputs (const char *string, FILE *file_pointer)

在这里,string代表包含要写入文件的数据的字符数组。file_pointer短语代表指向文件的文件指针。

常用于随机文件的函数

以下函数用于在随机文件中设置文件指针到指定位置,指示文件指针当前指向的位置,以及将文件指针重置到随机文件的开始位置。

fseek()

fseek()函数用于在文件中设置文件指针到特定位置。以下是它的语法:

fseek(FILE *file_pointer, long int offset, int location);

此函数具有以下特性:

  • file_pointer: 这代表指向文件的文件指针。

  • offset: 这代表文件指针需要从由位置参数指定的位置移动的字节数。如果offset的值为正,文件指针将在文件中向前移动,如果为负,文件指针将从给定位置向后移动。

  • location: 这是定义文件指针需要移动的位置的值。也就是说,文件指针将从location参数指定的位置移动等于offset参数指定的字节数。其值可以是012,如下表所示:

含义
0 文件指针将从文件开头移动
1 文件指针将从当前位置移动
2 文件指针将从文件末尾移动

让我们看看以下示例。在这里,文件指针将从文件开头向前移动5个字节:

fseek(fp,5L,0)

在以下示例中,文件指针将从文件末尾向后移动5个字节:

fseek(fp,-5L,2)

ftell()

ftell()函数返回file_pointer当前在文件中指向的字节位置。以下是它的语法:

long int ftell(FILE *file_pointer)

在这里,file_pointer是一个指向文件的文件指针。

rewind()

rewind()函数用于将文件指针移动到指定文件的开始。以下是它的语法:

void rewind(FILE *file_pointer)

在这里,file_pointer是一个指向文件的文件指针。

在本章中,我们将学习使用两种类型的文件处理,通过制作实时应用的食谱。

读取文本文件并将所有点号后面的字符转换为大写

假设我们有一个包含一些文本的文件。我们认为文本中存在一个异常——每个点号后的第一个字符应该是大写,但实际上却是小写。在这个食谱中,我们将读取这个文本文件,并将点号(.)后面的每个小写字符转换为大写。

在这个食谱中,我假设你知道如何创建文本文件和如何读取文本文件。如果你不知道如何执行这些操作,你将在附录 A中找到这两个程序。

如何做到这一点…

  1. 使用以下代码以只读模式打开顺序文件:
    fp = fopen (argv [1],"r");
  1. 如果文件不存在或权限不足,将显示错误消息,程序将终止。使用以下代码设置:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
 }
  1. 从文件中读取一行,如下面的代码所示:
fgets(buffer, BUFFSIZE, fp);
  1. 每个字符的行都会被访问并检查是否存在点号,如下面的代码所示:
for(i=0;i<n;i++)
    if(buffer[i]=='.')

  1. 如果找到点号,则检查点号后面的字符以确认它是否为大写,如下面的代码所示:
if(buffer[i] >=97 && buffer[i] <=122)
  1. 如果点号后面的字符是小写,则从小写字符的 ASCII 值中减去32以将其转换为大写,如下面的代码所示:
buffer[i]=buffer[i]-32;
  1. 如果行还没有结束,则从步骤 4 开始的序列将重复到步骤 6;否则,更新的行将在屏幕上显示,如下面的代码所示:
puts(buffer);
  1. 使用以下代码检查是否到达了文件末尾。如果文件没有结束,则重复从步骤 3 开始的序列:
while(!feof(fp))

前面的步骤在以下图中以图示方式解释(图 6.1):

图 6.1

将文件中点号后面的小写字母转换为大写的convertcase.c程序如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFFSIZE 255

void main (int argc, char* argv[])
{
    FILE *fp;
    char buffer[BUFFSIZE];
    int i,n;

    fp = fopen (argv [1],"r");
    if (fp == NULL) {
        printf("%s file does not exist\n", argv[1]);
        exit(1);
    }
    while (!feof(fp))
    {
        fgets(buffer, BUFFSIZE, fp);
        n=strlen(buffer);
        for(i=0;i<n;i++)
        {
            if(buffer[i]=='.')
            {
                i++;
                while(buffer[i]==' ')
                {
                    i++;
                }
                if(buffer[i] >=97 && buffer[i] <=122)
                {
                    buffer[i]=buffer[i]-32;
                }
            }
        }
        puts(buffer);
    }
    fclose(fp);
}

现在,让我们看看幕后。

它是如何工作的...

以命令行参数提供的文件名打开的文件以只读模式打开,并由文件指针fp指向。这个食谱专注于读取文件并更改其大小写,因此如果文件不存在或没有读取权限,将显示错误信息,程序将终止。

将设置一个while循环,直到feof(文件末尾)到达时执行。在while循环内,将逐行读取文件并将每一行分配给名为buffer的字符串。将使用fgets()函数一次读取文件中的一行。从文件中读取一定数量的字符,直到遇到换行符\n,最多读取 254 个字符。

以下步骤将对分配给字符串缓冲区的每一行执行:

  1. 将计算缓冲区字符串的长度,并执行一个for循环以访问字符串缓冲区中的每个字符。

  2. 将检查字符串缓冲区中是否有任何句点。

  3. 如果找到了,将检查其后的字符是否为小写。然后使用 ASCII 值将小写字符转换为大写(有关对应字母的 ASCII 值的更多信息,请参阅第二章,字符串管理)。如果句点后的字符是小写,则从小写字符的 ASCII 值中减去32以将其转换为大写。记住,大写字母的 ASCII 值比对应的小写字母低32

  4. 将在屏幕上显示更新后的字符串buffer,其中句点后的字符已被转换为大写。

当读取并显示文件的所有行后,指向fp指针的文件将被关闭。

让我们使用 GCC 编译convertcase.c程序,如下所示:

D:\CBook>gcc convertcase.c -o convertcase

如果你没有错误或警告,这意味着convertcase.c程序已被编译成可执行文件convertcase.exe

假设我已经创建了一个名为textfile.txt的文件,其内容如下:

D:\CBook>type textfile.txt
I am trying to create a sequential file. it is through C programming. It is very hot today. I have a cat. do you like animals? It might rain. Thank you. Bye

上述命令是在 Windows 的命令提示符中执行的。

运行可执行文件convertcase.exe,然后将textfile.txt文件传递给它,如下面的代码所示:

D:\CBook>./convertcase textfile.txt
I am trying to create a sequential file. It is through C programming. It is very hot today. I have a cat. Do you like animals? It might rain. Thank you. Bye

你可以在前面的输出中看到,句点后的字符现在已被转换为大写。

让我们继续下一个菜谱!

以相反的顺序显示随机文件的文件内容

假设我们有一个包含一些文本行的随机文件。让我们来看看如何反转这个文件的文件内容。

如果随机文件不存在,此程序将不会给出正确的输出。请阅读附录 A了解如何创建随机文件。

如何做到这一点...

  1. 使用以下代码以只读模式打开随机文件:
fp = fopen (argv[1], "rb");
  1. 如果文件不存在或没有足够的权限,将会显示错误消息,并且程序将终止,如下面的代码所示:
if (fp == NULL) {
     perror ("An error occurred in opening the file\n");
     exit(1);
 }
  1. 要按相反顺序读取随机文件,执行一个等于文件行数的循环。循环的每次迭代都将从文件底部开始读取一行。以下公式将用于找出文件中的行数:

文件中使用的总字节数/每行的字节数

执行此操作的代码如下:

fseek(fp, 0L, SEEK_END);
n = ftell(fp);
nol=n/sizeof(struct data);
  1. 因为文件必须按相反顺序读取,所以文件指针将被定位在文件底部,如下面的代码所示:
fseek(fp, -sizeof(struct data)*i, SEEK_END); 
  1. 设置一个循环,使其执行的次数等于步骤 3 中计算的文件行数,如下面的代码所示:
for (i=1;i<=nol;i++)
  1. 在循环内部,文件指针将被定位如下:

图 6.2

  1. 要读取最后一行,文件指针将被定位在最后一行开始的字节位置,即-1 x sizeof(line)字节位置。最后一行将被读取并显示在屏幕上,如下面的代码所示:
fread(&line,sizeof(struct data),1,fp);
puts(line.str);
  1. 接下来,文件指针将被定位在倒数第二行开始的字节位置,即-2 x sizeof(line)字节位置。再次,倒数第二行将被读取并显示在屏幕上。

  2. 该过程将重复进行,直到文件中的所有行都被读取并显示在屏幕上。

读取随机文件的反向读取程序readrandominreverse.c如下:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct data{  
    char str[ 255 ];  
};

void main (int argc, char* argv[])
{
    FILE *fp;
    struct data line;
    int n,nol,i;
    fp = fopen (argv[1], "rb");
    if (fp == NULL) {
        perror ("An error occurred in opening the file\n");
        exit(1);
    }
    fseek(fp, 0L, SEEK_END); 
    n = ftell(fp);
    nol=n/sizeof(struct data);
    printf("The content of random file in reverse order is :\n");
    for (i=1;i<=nol;i++)
    {
        fseek(fp, -sizeof(struct data)*i, SEEK_END); 
        fread(&line,sizeof(struct data),1,fp);
        puts(line.str);
    }
    fclose(fp);
}

现在,让我们看看幕后。

它是如何工作的...

我们将以只读模式打开选定的文件。如果文件成功打开,它将由文件指针fp指向。接下来,我们将使用以下公式找出文件中的总行数:

文件使用的总字节数/每行使用的字节数

要知道文件使用的总字节数,文件指针将被定位在文件底部,我们将调用ftell函数。ftell函数找到文件指针的当前位置。因为文件指针在文件末尾,使用此函数将告诉我们文件使用的总字节数。要找出一行使用的字节数,我们将使用sizeof函数。我们将应用前面的公式来计算文件中的总行数;这将分配给变量nol

我们将设置一个for循环,使其执行nol次。在for循环内部,文件指针将被定位在最后一行的末尾,以便可以按相反顺序访问文件中的所有行。因此,文件指针首先被设置在文件底部的(-1 * 一行的大小)位置。一旦文件指针定位在这个位置,我们将使用fread函数读取文件的最后一行并将其分配给结构变量line。然后,line中的字符串将在屏幕上显示。

在屏幕上显示最后一行后,文件指针将设置在倒数第二行的字节位置 (-2 * 一行的大小)。我们将再次使用 fread 函数读取倒数第二行并在屏幕上显示它。

此过程将执行 for 循环执行的次数,for 循环将执行与文件中行数相同的次数。然后关闭文件。

让我们使用 GCC 编译 readrandominreverse.c 程序,如下所示:

D:\CBook>gcc readrandominreverse.c -o readrandominreverse

如果你没有错误或警告,这意味着 readrandominreverse.c 程序已经被编译成一个可执行文件,readrandominreverse.exe

假设我们有一个随机文件,random.data,包含以下文本:

This is a random file. I am checking if the code is working
perfectly well. Random file helps in fast accessing of
desired data. Also you can access any content in any order.

让我们运行可执行文件 readrandominreverse.exe,使用以下代码以逆序显示随机文件 random.data

D:\CBook>./readrandominreverse random.data
The content of random file in reverse order is :
desired data. Also you can access any content in any order.
perfectly well. Random file helps in fast accessing of
This is a random file. I am checking if the code is working

通过比较原始文件与前面的输出,你可以看到文件内容是逆序显示的。

现在,让我们继续下一个菜谱!

计算文件中元音的数量

在这个菜谱中,我们将打开一个顺序文本文件并计算它包含的元音数量(大写和小写)。

在这个菜谱中,我将假设一个顺序文件已经存在。请阅读 附录 A 了解如何创建顺序文件。

如何做到这一点...

  1. 使用以下代码以只读模式打开顺序文件:
fp = fopen (argv [1],"r");
  1. 如果文件不存在或没有足够的权限,将显示错误消息,程序将终止,如下所示:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
 }
  1. 将将用于计算文件中元音数量的计数器初始化为 0,如下所示:
count=0;
  1. 从文件中读取一行,如下所示:
fgets(buffer, BUFFSIZE, fp);
  1. 访问并检查每一行的每个字符,看是否有任何小写或大写元音,如下所示:
if(buffer[i]=='a' || buffer[i]=='e' || buffer[i]=='i' || buffer[i]=='o' || buffer[i]=='u' || buffer[i]=='A' || buffer[i]=='E' || buffer[i]=='I' || buffer[i]=='O' || buffer[i]=='U')
  1. 如果找到任何元音,计数器的值将增加 1,如下所示:
count++;
  1. 步骤 5 将重复执行,直到达到行尾。检查是否已到达文件末尾。从步骤 4 重复,直到文件末尾,如下所示:
while (!feof(fp))
  1. 通过在屏幕上打印计数变量中的值来显示文件中元音数量的计数,如下所示:
printf("The number of vowels are %d\n",count);

前面的步骤如下所示:

图片

图 6.3

计算顺序文本文件中元音数量的 countvowels.c 程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFSIZE 255

void main (int argc, char* argv[])
{
    FILE *fp;
    char buffer[BUFFSIZE];
    int n, i, count=0;
    fp = fopen (argv [1],"r");
    if (fp == NULL) {
        printf("%s file does not exist\n", argv[1]);
        exit(1);
    }
    printf("The file content is :\n");
    while (!feof(fp))
    {
        fgets(buffer, BUFFSIZE, fp);
        puts(buffer);
        n=strlen(buffer);
        for(i=0;i<n;i++)
        {
            if(buffer[i]=='a' || buffer[i]=='e' || buffer[i]=='i' || 
            buffer[i]=='o' || buffer[i]=='u' || buffer[i]=='A' || 
            buffer[i]=='E' || buffer[i]=='I' || buffer[i]=='O' || 
            buffer[i]=='U') count++;
        }         
    }
    printf("The number of vowels are %d\n",count);
    fclose(fp);
}

现在,让我们幕后看看。

它是如何工作的...

我们将以只读模式打开所选顺序文件。如果文件成功打开,它将由文件指针 fp 指向。为了计算文件中的元音数量,我们将从 0 初始化一个计数器。

我们将设置一个while循环,直到文件指针fp到达文件末尾才执行。在while循环内,将使用fgets函数读取文件中的每一行。fgets函数将从文件中读取BUFFSIZE个字符。BUFFSIZE变量的值是255,所以fgets将读取文件中的254个字符或读取字符直到遇到换行符\n,以先到者为准。

从文件中读取的行被分配给buffer字符串。为了显示文件内容以及元音的数量,buffer字符串中的内容将在屏幕上显示。将计算buffer字符串的长度,并设置一个for循环,使其执行等于字符串长度的次数。

缓冲区字符串中的每个字符都将通过for循环进行检查。如果行中出现任何小写或大写元音,则计数器变量的值将增加1。当while循环结束时,计数器变量将包含文件中存在的元音总数。最后,计数器变量中的值将在屏幕上显示。

让我们使用 GCC 按照以下方式编译countvowels.c程序:

D:\CBook>gcc countvowels.c -o countvowels

如果没有错误或警告,则表示countvowels.c程序已编译成名为countvowels.exe的可执行文件。

假设我们有一个名为textfile.txt的文本文件,其中包含一些内容。我们将运行可执行文件countvowels.exe,并将textfile.txt文件作为参数传递给它,以计算其中的元音数量,如下面的代码所示:

D:\CBook>./countvowels textfile.txt
The file content is :
I am trying to create a sequential file. it is through C programming. It is very hot today. I have a cat. do you like animals? It might rain. Thank you. bye
The number of vowels are 49

从程序输出中可以看出,程序不仅显示了元音的数量,还显示了文件的完整内容。

现在,让我们继续下一个菜谱!

在文件中将一个单词替换为另一个单词

假设你想要将文件中所有is单词替换为was。让我们看看如何做到这一点。

在这个菜谱中,我将假设已经存在一个顺序文件。请阅读附录 A了解如何创建顺序文件。

如何做到这一点…

  1. 使用以下代码以只读模式打开文件:
    fp = fopen (argv [1],"r");
  1. 如果文件不存在或没有足够的权限,将显示错误消息,程序将终止,如下面的代码所示:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
 }
  1. 使用以下代码输入要替换的单词:
printf("Enter a string to be replaced: ");
scanf("%s", str1);
  1. 使用以下代码输入将替换旧单词的新单词:
printf("Enter the new string ");
scanf("%s", str2);
  1. 使用以下代码从文件中读取一行:
fgets(line, 255, fp);
  1. 使用以下代码检查要替换的单词是否出现在行的任何位置:
if(line[i]==str1[w])
{
     oldi=i;
     while(w<ls1)
     {
         if(line[i] != str1[w])
             break;
         else
         {
             i++;
             w++;
         }
     }
}
  1. 如果单词出现在行中,则只需使用以下代码将其替换为新单词:
if(w==ls1)
{
     i=oldi;
     for (k=0;k<ls2;k++)
     {
         nline[x]=str2[k];
         x++;
     }
     i=i+ls1-1;
 }
  1. 如果单词在任何位置都没有出现,则继续到下一步。使用以下代码打印替换单词的行:
puts(nline);
  1. 使用以下代码检查是否到达了文件末尾:
while (!feof(fp))
  1. 如果尚未到达文件末尾,则转到步骤 4。使用以下代码关闭文件:
fclose(fp);

replaceword.c程序用另一个单词替换文件中的指定单词,并在屏幕上显示修改后的内容:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void main (int argc, char* argv[])
{
    FILE *fp;
    char line[255], nline[300], str1[80], str2[80];
    int i,ll, ls1,ls2, x,k, w, oldi;

    fp = fopen (argv [1],"r");
    if (fp == NULL) {
        printf("%s file does not exist\n", argv[1]);
        exit(1);
    }
    printf("Enter a string to be replaced: ");
    scanf("%s", str1);
    printf("Enter the new string ");
    scanf("%s", str2);
    ls1=strlen(str1);
    ls2=strlen(str2);
    x=0;
    while (!feof(fp))
    {
        fgets(line, 255, fp);
        ll=strlen(line);
        for(i=0;i<ll;i++)
        {
            w=0;
            if(line[i]==str1[w])
            {
                oldi=i;    
                while(w<ls1)
                {
                    if(line[i] != str1[w])
                        break;
                    else
                    {
                        i++;
                        w++;
                    }
                }
                if(w==ls1)
                {
                    i=oldi;
                    for (k=0;k<ls2;k++)
                    {
                        nline[x]=str2[k];
                        x++;
                    }
                    i=i+ls1-1;
                }
                else
                {
                    i=oldi;
                    nline[x]=line[i];
                    x++;       
                }         
            }
            else
            {
                nline[x]=line[i];
                x++;
            }
        }
        nline[x]='\0';
        puts(nline);
     }
     fclose(fp);
}

现在,让我们看看幕后。

它是如何工作的...

以只读模式打开选定的文件。如果文件成功打开,则文件指针fp将被设置为指向它。输入要替换的单词并将其分配给字符串变量str1。同样,输入将被分配给另一个字符串变量str2的新字符串。两个字符串str1str2的长度将被计算并分别分配给变量ls1ls2

设置一个while循环,直到fp指针指向的文件结束为止执行。在while循环内,将使用fgets函数从文件中读取一行。fgets函数读取文件,直到指定的最大长度或遇到换行符\n,以先到者为准。由于字符串以强制性的空字符\0结尾,因此从文件中最多读取254个字符。

从文件中读取的字符串将被分配给line变量。line字符串的长度将被计算并分配给ll变量。使用for循环,访问line变量中的每个字符,以检查它们是否与要替换的字符串的第一个字符str1[0]匹配。与要替换的字符串不匹配的line变量中的字符将被分配给另一个字符串,称为nlinenline字符串将包含所需的内容,即line变量的所有字符和新字符串。如果它在line中存在,则该字符串将被替换为新字符串,整个修改后的内容将被分配给新字符串nline

如果要替换的字符串的第一个字符与line中的任何字符匹配,则将使用while循环来匹配要替换的字符串的后续字符与line中的后续字符。如果要替换的字符串的所有字符都与line中的后续字符匹配,则将所有要替换的字符串的字符替换为新字符串,并分配给新字符串nline。这样,while循环将一次读取文件中的一行文本,搜索要替换的字符串的出现。如果找到,则将其替换为新字符串,并将修改后的文本行分配给另一个字符串nline。在修改后的字符串nline中添加空字符\0,并在屏幕上显示。最后,关闭由文件指针fp指向的文件。

在这个菜谱中,我正在替换所需的单词和另一个字符串,并在屏幕上显示更新后的内容。如果你想将更新后的内容写入另一个文件,你总是可以以写入模式打开另一个文件,并执行fputs函数来将更新后的内容写入其中。

让我们使用 GCC 编译replaceword.c程序,如下所示:

D:\CBook>gcc replaceword.c -o replaceword

如果你没有收到任何错误或警告,那么这意味着replaceword.c程序已经被编译成了一个可执行文件,名为replaceword.exe。让我们运行这个可执行文件replaceword.exe,并向它提供一个文本文件。我们将假设存在一个名为textfile.txt的文本文件,并包含以下内容:

I am trying to create a sequential file. it is through C programming. It is very hot today. I have a cat. do you like animals? It might rain. Thank you. bye

现在,让我们使用以下代码用另一个单词替换文件中的一个单词:

D:\CBook>./replaceword textfile.txt
Enter a string to be replaced: is
Enter the new string was
I am trying to create a sequential file. it was through C programming. It was very hot today. I have a cat. do you like animals? It might rain. Thank you. Bye

你可以看到在textfile.txt中所有is单词的出现都被替换成了was,并且修改后的内容显示在屏幕上。我们已经成功替换了我们选择的单词。

现在,让我们继续到下一个菜谱!

文件加密

加密意味着将内容转换为编码格式,这样未经授权的人员将无法看到或访问文件的原始内容。可以通过应用公式到内容的 ASCII 值来加密文本文件。

公式或代码可以由你选择,它可以像你想要的那样简单或复杂。例如,假设你选择将所有字母的当前 ASCII 值向前移动 15 个值。在这种情况下,如果字母是 ASCII 值为 97 的小写字母a,那么 ASCII 值向前移动 15 个值将使加密后的字母变为小写字母p,其 ASCII 值为 112(97 + 15 = 112)。

在这个菜谱中,我假设你想要加密的顺序文件已经存在。请阅读附录 A了解如何创建顺序文件。如果你想知道加密文件是如何解密的,也可以参考附录 A

如何做到这一点...

  1. 使用以下代码以只读模式打开源文件:
fp = fopen (argv [1],"r");
  1. 如果文件不存在或没有足够的权限,将会显示错误消息,并且程序将终止,如下面的代码所示:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
 }
  1. 使用以下代码以只写模式打开目标文件,即将要写入加密文本的文件:
fq = fopen (argv[2], "w");
  1. 使用以下代码从文件中读取一行,并使用以下代码访问其每个字符:
fgets(buffer, BUFFSIZE, fp);
  1. 使用以下代码,从每一行的每个字符的 ASCII 值中减去45来加密该字符:
for(i=0;i<n;i++)
    buffer[i]=buffer[i]-45;
  1. 重复步骤 5,直到行结束。一旦行中的所有字符都被加密,使用以下代码将加密行写入目标文件:
fputs(buffer,fq);
  1. 使用以下代码检查是否到达了文件末尾:
while (!feof(fp))
  1. 使用以下代码关闭两个文件:
fclose (fp);
fclose (fq);

上述步骤在以下图中展示:

图片

图 6.4

加密文件的encryptfile.c程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h> 

#define BUFFSIZE 255
void main (int argc, char* argv[])
{
    FILE *fp,*fq;
    int  i,n;
    char buffer[BUFFSIZE];

    /* Open the source file in read mode */
    fp = fopen (argv [1],"r");
    if (fp == NULL) {
        printf("%s file does not exist\n", argv[1]);
        exit(1);
    }
    /* Create the destination file.  */
    fq = fopen (argv[2], "w");
    if (fq == NULL) {
        perror ("An error occurred in creating the file\n");
        exit(1);
    }
    while (!feof(fp))
    {
        fgets(buffer, BUFFSIZE, fp);
        n=strlen(buffer);
        for(i=0;i<n;i++)
            buffer[i]=buffer[i]-45;
        fputs(buffer,fq);
    }
    fclose (fp);
    fclose (fq); 
}

现在,让我们看看幕后。

它是如何工作的...

通过命令行参数传递的第一个文件名将以只读模式打开。通过命令行参数传递的第二个文件名将以只写模式打开。如果两个文件都正确打开,那么fpfq指针分别将指向只读和只写文件。

我们将设置一个while循环,直到它达到源文件的末尾才执行。在循环内部,将使用fgets函数从源文件中读取一行。fgets函数从文件中读取指定数量的字节,或者直到遇到新行字符\n。如果文件中没有出现新行字符,那么BUFFSIZE常量将限制从文件中读取的字节数为254

从文件中读取的行被分配给buffer字符串。计算字符串buffer的长度并将其分配给变量n。然后我们将设置一个for循环,直到它达到buffer字符串的长度末尾才执行,在循环内部,将改变每个字符的 ASCII 值。

为了加密文件,我们将从每个字符的 ASCII 值中减去45,尽管我们可以应用任何我们喜欢的公式。只需确保你记住这个公式,因为我们将在解密文件时需要反转它。

在将公式应用于所有字符后,加密行将被写入目标文件。此外,为了在屏幕上显示加密版本,加密行也将显示在屏幕上。

while循环完成后,所有从源文件读取的行在加密后将被写入目标文件。最后,将关闭这两个文件。

让我们使用 GCC 编译encryptfile.c程序,如下所示:

D:\CBook>gcc encryptfile.c -o encryptfile

如果你没有收到错误或警告,这意味着encryptfile.c程序已经被编译成可执行文件,encryptfile.exe。让我们运行这个可执行文件。

在运行可执行文件之前,让我们先看看将被此程序加密的文本文件textfile.txt的内容。该文本文件的内容如下:

I am trying to create a sequential file. it is through C programming. It is very hot today. I have a cat. do you like animals? It might rain. Thank you. bye

让我们在textfile.txt上运行可执行文件encryptfile.exe,并使用以下代码将加密内容放入另一个名为encrypted.txt的文件中:

D:\CBook>./encryptfile textfile.txt encrypted.txt

textfile.txt中的普通内容被加密,加密内容被写入另一个名为encrypted.txt的文件。加密内容将如下所示:

D:\CBook>type encrypted.txt
≤4@≤GEL<A:≤GB≤6E84G8≤4≤F8DH8AG<4?≤9<?8≤<G≤<F≤G;EBH:;≤≤CEB:E4@@<A:≤≤≤G≤<F≤I8EL≤;BG≤GB74L≤;4I8≤4≤64G≤≤7B≤LBH≤?<>8≤4A<@4?F≤≤≤≤G≤@<:;G≤E4<A';4A>≤LBH≤5L8

上述命令在 Windows 的命令提示符中执行。

哇!我们已经成功加密了文件!

参见

要了解如何在顺序文件、随机文件中创建和读取内容以及解密文件,请访问此链接上的附录 Cgithub.com/PacktPublishing/Practical-C-Programming/blob/master/Appendix%20C.pdf.

第七章:实现并发

多任务处理是几乎所有操作系统的关键特性;它提高了 CPU 的效率,并以更好的方式利用资源。线程是实现多任务的最佳方式。一个进程可以包含多个线程以实现多任务。

在本章中,我们将介绍涉及线程的以下食谱:

  • 使用单个线程执行任务

  • 使用多个线程执行多个任务

  • 使用 mutex 在两个线程之间共享数据

  • 理解死锁是如何产生的

  • 避免死锁

进程和线程这两个术语可能会令人困惑,所以首先,我们要确保你理解它们。

进程和线程是什么?

每当我们运行一个程序时,当它从硬盘(或任何其他存储)加载到内存中时,它就变成了一个进程进程由处理器执行,并且为了执行它,需要一个程序计数器(PC)来跟踪下一个要执行的指令,CPU 寄存器,信号等。

线程指的是程序内可以独立执行的指令集。线程有自己的 PC 和一组寄存器,以及其他一些东西。这样,一个进程由多个线程组成。两个或多个线程可以共享它们的代码、数据和其他资源,但在线程之间共享资源时必须格外小心,因为这可能会导致歧义和死锁。操作系统还管理线程池。

线程池包含一组等待分配任务以进行并发执行的线程。使用线程池中的线程而不是实例化新线程有助于避免创建和销毁新线程造成的延迟;因此,它提高了应用程序的整体性能。

基本上,线程通过并行性提高了应用程序的效率,也就是说,通过同时运行两个或更多独立的代码集。这被称为多线程

C 语言不支持多线程,因此为了实现它,使用 POSIX 线程(Pthreads)。GCC 允许实现一个 pthread

在使用 pthread 时,定义一个 pthread_t 类型的变量来存储线程标识符。线程标识符是一个唯一的整数,即分配给系统中的线程。

你可能想知道用于创建线程的函数是哪个。pthread_create 函数被调用来创建线程。以下四个参数传递给 pthread_create 函数:

  • 线程标识符的指针,该指针由该函数设置

  • 线程的属性;通常,为此参数提供 NULL 以使用默认属性

  • 执行线程创建时要调用的函数的名称

  • 要传递给线程的参数,如果不需要传递参数给线程,则设置为 NULL

当两个或更多线程操作相同的数据时,即它们共享相同的资源,必须应用某些检查措施,以确保一次只允许一个线程操作共享资源;其他线程的访问必须被阻塞。帮助避免线程间共享资源时歧义的一种方法就是互斥。

互斥

为了避免两个或更多线程访问相同资源时的歧义,互斥实现了对共享资源的串行访问。当一个线程正在使用资源时,不允许其他线程访问相同的资源。所有其他线程在资源再次可用之前都被阻止访问相同的资源。

互斥锁基本上是与共享资源关联的锁。要读取或修改共享资源,线程必须首先获取该资源的锁。一旦线程获取了该资源的锁(或互斥锁),它就可以继续处理该资源。所有其他希望对其工作的线程都将被迫等待,直到资源解锁。当线程完成对共享资源的处理时,它将解锁互斥锁,使其他等待的线程能够获取该资源的互斥锁。除了互斥锁之外,信号量也用于进程同步。

信号量是一个用于避免两个或更多进程在并发系统中同时访问公共资源的概念。它基本上是一个变量,通过操作它只允许一个进程访问公共资源并实现进程同步。信号量使用信号机制,即分别调用waitsignal函数来通知公共资源已被获取或释放。另一方面,互斥锁(mutex)使用锁定机制——进程在操作公共资源之前必须获取mutex对象的锁。

虽然互斥锁有助于管理线程之间的共享资源,但存在一个问题。互斥锁使用顺序错误可能导致死锁。死锁发生在这样一个情况下:一个持有锁 X的线程试图获取锁 Y以完成其处理,而另一个持有锁 Y的线程试图获取锁 X以完成其执行。在这种情况下,将发生死锁,因为两个线程都将无限期地等待对方释放其锁。由于没有线程能够完成其执行,因此没有线程能够释放其锁。避免死锁的一种解决方案是让线程以特定的顺序获取锁。

以下函数用于创建和管理线程:

  • pthread_join:此函数使线程等待所有派生线程的完成。如果不使用它,线程将在完成任务后立即退出,忽略其派生线程的状态。换句话说,pthread_join会阻塞调用线程,直到指定函数中的线程终止。

  • pthread_mutex_init:此函数使用指定的属性初始化mutex对象。如果使用NULL作为属性,则使用默认的mutex属性来初始化mutex对象。当mutex初始化时,它处于未锁定状态。

  • pthread_mutex_lock:此函数锁定指定的mutex对象。如果mutex已被其他线程锁定,调用线程将被挂起,即它将被要求等待直到mutex解锁。此函数返回一个锁定状态的mutex对象。锁定mutex的线程成为其所有者,并在解锁mutex之前保持所有者状态。

  • pthread_mutex_unlock:此函数释放指定的mutex对象。调用pthread_mutex_lock函数并等待mutex解锁的线程将变为非阻塞状态并获取mutex对象,即等待的线程将能够访问和锁定mutex对象。如果没有线程等待mutex,则mutex将保持未锁定状态,没有任何所有者线程。

  • pthread_mutex_destroy:此函数销毁mutex对象并释放为其分配的资源。在调用此方法之前,mutex必须处于未锁定状态。

根据操作系统,锁可能是一个自旋锁。如果有任何线程尝试获取锁但锁不可用,自旋锁将使线程在一个循环中等待直到锁变为可用。这种锁在等待锁释放时使线程保持忙碌。它们是高效的,因为它们避免了在进程重新调度或上下文切换中消耗时间和资源。

理论就到这里。现在,让我们从一些实际例子开始!

使用单个线程执行任务

在这个菜谱中,我们将创建一个线程来执行任务。在这个任务中,我们将显示从15的序列号。这个菜谱的重点是学习如何创建线程以及如何让主线程等待直到线程完成任务完成。

如何做到这一点...

  1. 定义一个pthread_t类型的变量来存储线程标识符:
pthread_t tid;
  1. 创建一个线程并将前一步创建的标识符传递给pthread_create函数。线程使用默认属性创建。还要指定需要执行以创建线程的函数:
pthread_create(&tid, NULL, runThread, NULL);
  1. 在函数中,你将显示一条文本消息来指示线程已被创建并正在运行:
printf("Running Thread \n");
  1. 通过运行线程调用一个for循环来显示从15的数字序列:
for(i=1;i<=5;i++) printf("%d\n",i);
  1. 在主函数中调用 pthread_join 方法,使 main 方法等待直到线程完成任务:
pthread_join(tid, NULL);

创建线程并使其执行任务的 createthread.c 程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *runThread(void *arg)
{
    int i;
    printf("Running Thread \n");
    for(i=1;i<=5;i++) printf("%d\n",i);
    return NULL;
}

int main()
{
    pthread_t tid;
    printf("In main function\n");
    pthread_create(&tid, NULL, runThread, NULL);
    pthread_join(tid, NULL);
    printf("Thread over\n");
    return 0;
}

现在,让我们幕后看看。

它是如何工作的…

我们将定义一个名为 tid 的变量,其类型为 pthread_t,用于存储线程标识符。线程标识符是一个唯一的整数,即分配给系统中的线程。在创建线程之前,屏幕上会显示消息 In main function。我们将创建一个线程并将标识符 tid 传递给 pthread_create 函数。线程将以默认属性创建,并将 runThread 函数设置为执行以创建线程。

runThread 函数中,我们将显示文本消息 Running Thread 以指示线程已被创建并正在运行。我们将调用一个 for 循环,通过正在运行的线程显示从 15 的数字序列。通过调用 pthread_join 方法,我们将使 main 方法等待直到线程完成任务。在这里调用 pthread_join 是至关重要的;否则,main 方法将在等待线程完成之前退出。

让我们使用 GCC 编译 createthread.c 程序,如下所示:

D:\CBook>gcc createthread.c -o createthread

如果没有错误或警告,这意味着 createthread.c 程序已经被编译成可执行文件,名为 createthread.exe。让我们运行这个可执行文件:

图 7.1

哇!我们已经成功使用单个线程完成了一个任务。现在,让我们继续下一个菜谱!

使用多个线程执行多个任务

在这个菜谱中,你将学习如何通过并行执行两个线程来实现多任务处理。这两个线程将独立执行它们各自的任务。由于两个线程不会共享资源,因此不会出现竞争条件或歧义的情况。CPU 将随机执行任何线程,但最终,两个线程都将完成分配的任务。这两个线程将执行的任务是显示从 15 的数字序列。

如何做到这一点…

  1. 定义两个类型为 pthread_t 的变量来存储两个线程标识符:
pthread_t tid1, tid2;
  1. 调用 pthread_create 函数两次以创建两个线程,并分配我们在上一步中创建的标识符。这两个线程将以默认属性创建。指定两个线程各自需要执行的两个相应函数:
pthread_create(&tid1,NULL,runThread1,NULL);
pthread_create(&tid2,NULL,runThread2,NULL);
  1. 在第一个线程的函数中,显示一条文本消息以指示第一个线程已被创建并正在运行:
printf("Running Thread 1\n");
  1. 为了指示第一个线程的执行,在第一个函数中执行一个 for 循环以显示从 15 的数字序列。为了与第二个线程区分开来,第一个线程生成的数字序列前面会加上前缀 Thread 1
for(i=1;i<=5;i++)
    printf("Thread 1 - %d\n",i);
  1. 类似地,在第二个线程中,显示一条文本消息以告知第二个线程也被创建并正在运行:
  printf("Running Thread 2\n");
  1. 再次,在第二个函数中,执行一个 for 循环以显示从 15 的数字序列。为了区分这些数字与 thread1 生成的数字,这个数字序列将前面加上文本 Thread 2
for(i=1;i<=5;i++)
    printf("Thread 2 - %d\n",i);
  1. 两次调用 pthread_join,并将我们在第一步中创建的线程标识符传递给它。pthread_join 将使两个线程,并且 main 方法将等待直到两个线程都完成了它们的工作:
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
  1. 当两个线程都完成后,将显示一条文本消息以确认这一点:
printf("Both threads are over\n");

创建两个线程并使它们在独立资源上工作的 twothreads.c 程序如下:

#include<pthread.h>
#include<stdio.h>

void *runThread1(void *arg){
    int i;
    printf("Running Thread 1\n");
    for(i=1;i<=5;i++)
        printf("Thread 1 - %d\n",i);
}

void *runThread2(void *arg){
    int i;
    printf("Running Thread 2\n");
    for(i=1;i<=5;i++)
        printf("Thread 2 - %d\n",i);
}

int main(){
    pthread_t tid1, tid2;
    pthread_create(&tid1,NULL,runThread1,NULL);
    pthread_create(&tid2,NULL,runThread2,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("Both threads are over\n");
    return 0;
}

现在,让我们看看幕后。

它是如何工作的...

我们将定义两个类型为 pthread_t 的变量,分别命名为 tid1tid2,以存储两个线程标识符。这些线程标识符唯一地表示系统中的线程。我们将两次调用 pthread_create 函数来创建两个线程,并将它们的标识符分配给两个变量 tid1tid2,其地址传递给 pthread_create 函数。

两个线程使用默认属性创建。我们将执行 runThread1 函数来创建第一个线程,然后执行 runThread2 函数来创建第二个线程。

runThread1 函数中,我们将显示消息 Running Thread 1 以指示第一个线程已被创建并正在运行。此外,我们将通过运行线程显示从 15 的数字序列。第一个线程生成的数字序列将前面加上 Thread 1

类似地,在 runThread2 函数中,我们将显示消息 Running Thread 2 以告知第二个线程也被创建并正在运行。再次,我们将调用一个 for 循环以显示从 15 的数字序列。为了区分这些数字与 thread1 生成的数字,这些数字前面将加上文本 Thread 2

然后,我们将两次调用 pthread_join 方法,并将我们的两个线程标识符 tid1tid2 传递给它。pthread_join 被调用以使两个线程,并且 main 方法等待直到两个线程都完成了它们各自的任务。当两个线程都结束时,即当 runThread1runThread2 函数结束时,main 函数中将显示一条消息,说明 Both threads are over

让我们使用 GCC 编译 twothreads.c 程序,如下所示:

D:\CBook>gcc twothreads.c -o twothreads

如果你没有错误或警告,这意味着 twothreads.c 程序已被编译成可执行文件,twothreads.exe。让我们运行这个可执行文件:

图 7.2

您可能不会得到完全相同的输出,因为这取决于 CPU,但可以确定的是,两个线程将同时退出。

哇!我们已经成功使用多个线程完成了多个任务。现在,让我们继续下一个菜谱!

使用互斥锁在两个线程之间共享数据

独立运行两个或更多线程,其中每个线程访问其自己的资源,相当方便。然而,有时我们希望线程能够同时共享和处理相同的资源,以便我们可以更快地完成任务。共享公共资源可能会导致问题,因为一个线程可能会在另一个线程写入更新数据之前读取数据,导致模糊的情况。为了避免这种情况,使用mutex。在本菜谱中,您将学习如何在两个线程之间共享公共资源。

如何做到这一点…

  1. 定义两个pthread_t类型的变量来存储两个线程标识符。同时定义一个mutex对象:
pthread_t tid1,tid2;
pthread_mutex_t lock;
  1. 调用pthread_mutex_init方法使用默认的mutex属性初始化mutex对象:
pthread_mutex_init(&lock, NULL)
  1. 调用pthread_create函数两次以创建两个线程,并分配我们在第一步中创建的标识符。执行创建两个线程的函数:
pthread_create(&tid1, NULL, &runThread, NULL);
pthread_create(&tid2, NULL, &runThread, NULL);
  1. 在函数中,调用pthread_mutex_lock方法并将mutex对象传递给它以锁定:
pthread_mutex_lock(&lock);
  1. 调用pthread_self方法并将调用线程的 ID 分配给pthread_t类型的变量。调用pthread_equal方法并将其与变量比较以找出当前正在执行的线程。如果正在执行第一个线程,则在屏幕上显示消息First thread is running
pthread_t id = pthread_self();
if(pthread_equal(id,tid1))                                
    printf("First thread is running\n");
  1. 为了表明线程正在执行公共资源,在屏幕上显示文本消息Processing the common resource
printf("Processing the common resource\n");
  1. 调用sleep方法使第一个线程休眠5秒:
sleep(5);
  1. 经过5秒的持续时间后,在屏幕上显示消息First thread is over
printf("First thread is over\n\n");
  1. 将调用pthread_mutex_unlock函数,并将我们在第一步中创建的mutex对象传递给它以解锁:
pthread_mutex_unlock(&lock);  
  1. thread函数将由第二个线程调用。再次锁定mutex对象:
pthread_mutex_lock(&lock);

  1. 为了表明此时正在运行第二个线程,在屏幕上显示消息Second thread is running
printf("Second thread is running\n");
  1. 再次,为了表明线程正在访问公共资源,在屏幕上显示消息Processing the common resource
printf("Processing the common resource\n");
  1. 引入5秒的延迟。然后,在屏幕上显示消息second thread is over
sleep(5);
printf("Second thread is over\n\n"); 
  1. 解锁mutex对象:
pthread_mutex_unlock(&lock);  
  1. 调用pthread_join方法两次,并将线程标识符传递给它:
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
  1. 调用pthread_mutex_destroy方法以销毁mutex对象:
pthread_mutex_destroy(&lock);

创建两个线程以共享公共资源的twothreadsmutex.c程序如下:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_t tid1,tid2;
pthread_mutex_t lock;

void* runThread(void *arg)
{
    pthread_mutex_lock(&lock);
    pthread_t id = pthread_self();
    if(pthread_equal(id,tid1))
        printf("First thread is running\n");
    else
        printf("Second thread is running\n");
    printf("Processing the common resource\n");
    sleep(5);
    if(pthread_equal(id,tid1))
        printf("First thread is over\n\n");
    else
        printf("Second thread is over\n\n"); 
    pthread_mutex_unlock(&lock);  
    return NULL;
}

int main(void)
{ 
    if (pthread_mutex_init(&lock, NULL) != 0)
        printf("\n mutex init has failed\n");
    pthread_create(&tid1, NULL, &runThread, NULL);
    pthread_create(&tid2, NULL, &runThread, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);
    return 0;
}

现在,让我们看看幕后。

它是如何工作的...

我们将首先通过名称lock定义一个mutex对象。回想一下,mutex基本上是与共享资源相关联的锁。要读取或修改共享资源,线程需要首先获取该资源的锁。我们将定义两个pthread_t类型的变量,分别命名为tid1tid2,以存储两个线程标识符。

我们将调用pthread_mutex_init方法,用默认的mutex属性初始化lock对象。初始化后,lock对象处于未锁定状态。然后,我们将两次调用pthread_create函数来创建两个线程并将它们的标识符分配给两个变量tid1tid2,其地址传递给pthread_create函数。两个线程以默认属性创建。

接下来,我们将执行runThread函数以创建两个线程。在runThread函数中,我们将调用pthread_mutex_lock方法并将mutex对象lock传递给它以锁定它。现在,其余的线程(如果有)将被要求等待,直到mutex对象lock被解锁。我们将调用pthread_self方法并将调用线程的 ID 赋值给pthread_t类型的变量id。然后,我们将调用pthread_equal方法以确保如果调用线程是分配给tid1变量的标识符的线程,则屏幕上会显示消息First thread is running

接下来,屏幕上显示消息Processing the common resource。我们将调用sleep方法使第一个线程休眠5秒。经过5秒的持续时间后,屏幕上会显示消息First thread is over,以指示第一个线程已完成。然后,我们将调用pthread_mutex_unlock并将mutex对象lock传递给它以解锁它。解锁mutex对象是向其他线程发出信号,表明其他线程也可以使用公共资源。

runThread方法将由第二个线程调用,其标识符为tid2。同样,mutex对象lock被锁定,调用线程的id,即第二个线程,被分配给变量id。屏幕上显示消息Second thread is running,随后显示消息Processing the common resource

我们将引入5秒的延迟以指示第二个线程正在处理公共资源。然后,屏幕上显示消息second thread is over。此时,mutex对象lock已解锁。我们将两次调用pthread_join方法并将线程标识符tid1tid2传递给它。pthread_join被调用以使两个线程和main方法等待,直到两个线程都完成其任务。

当两个线程都完成后,我们将调用pthread_mutex_destroy方法来销毁mutex对象lock并释放为其分配的资源。

让我们使用 GCC 编译twothreadsmutex.c程序,如下所示:

D:\CBook>gcc twothreadsmutex.c -o twothreadsmutex

如果你没有错误或警告,这意味着twothreadsmutex.c程序已被编译成可执行文件,twothreadsmutex.exe。让我们运行这个可执行文件:

图 7.3

哇!我们已经成功使用mutex在两个线程之间共享数据。现在,让我们继续下一个菜谱!

理解死锁是如何产生的

锁定资源有助于得到非歧义的结果,但锁定也可能导致死锁。死锁是一种情况,其中线程已经获取了一个资源的锁,并希望获取第二个资源的锁。然而,同时,另一个线程已经获取了第二个资源的锁,但希望获取第一个资源的锁。因为第一个线程将一直等待第二个资源锁变为空闲,而第二个线程将一直等待第一个资源锁变为空闲,所以线程将无法进一步进行,应用程序将挂起(如下面的图所示):

图 7.4

在这个菜谱中,我们将使用栈。栈需要两个操作——pushpop。为了确保一次只有一个线程执行pushpop操作,我们将使用两个mutex对象——pop_mutexpush_mutex。线程需要在两个对象上获取锁才能操作栈。为了创建死锁的情况,我们将使一个线程获取一个锁,并要求它获取另一个已经被另一个线程获取的锁。

如何做到这一点...

  1. 定义一个值为10的宏,并定义一个大小相等的数组:
#define max 10
int stack[max];
  1. 定义两个mutex对象;一个将用于从栈中弹出(pop_mutex),另一个将用于将值推送到栈中(push_mutex):
pthread_mutex_t pop_mutex;
pthread_mutex_t push_mutex;
  1. 要使用stack,将top的值初始化为-1
int top=-1;
  1. 定义两个类型为pthread_t的变量来存储两个线程标识符:
pthread_t tid1,tid2;
  1. 调用pthread_create函数创建第一个线程;该线程将以默认属性创建。执行push函数以创建此线程:
pthread_create(&tid1,NULL,&push,NULL);
  1. 再次调用pthread_create函数以创建第二个线程;此线程也将以默认属性创建。执行pop函数以创建此线程:
pthread_create(&tid2,NULL,&pop,NULL);
  1. push函数中,调用pthread_mutex_lock方法并传递用于push操作的mutex对象(push_mutex)以锁定它:
pthread_mutex_lock(&push_mutex);
  1. 然后,pop操作的mutex对象(pop_mutex)将被第一个线程锁定:
pthread_mutex_lock(&pop_mutex);
  1. 用户被要求输入要推送到stack的值:
printf("Enter the value to push: ");
scanf("%d",&n);
  1. top的值递增到0。上一步输入的值被推送到stack[0]的位置:
top++;
stack[top]=n;
  1. 调用pthread_mutex_unlock并解锁用于poppop_mutex)和push操作(push_mutex)的mutex对象:
pthread_mutex_unlock(&pop_mutex);                                                       pthread_mutex_unlock(&push_mutex);  
  1. push函数的底部,显示一条文本消息,表明值已推入栈中:
printf("Value is pushed to stack \n");
  1. pop函数中,调用pthread_mutex_lock函数来锁定mutex对象pop_mutex。这将导致死锁:
pthread_mutex_lock(&pop_mutex);
  1. 再次尝试锁定push_mutex对象,尽管这是不可能的,因为它总是被第一个线程获取:
sleep(5);
pthread_mutex_lock(&push_mutex);
  1. 栈中的值,即由top指针指向的值将被弹出:
k=stack[top];
  1. 此后,top的值将减1以再次使其为-1。从栈中弹出的值将在屏幕上显示:
top--;
printf("Value popped is %d \n",k);
  1. 然后,解锁mutex对象push_mutexpop_mutex对象:
pthread_mutex_unlock(&push_mutex);     
pthread_mutex_unlock(&pop_mutex);
  1. main函数中,调用pthread_join方法并将步骤 1 中创建的线程标识符传递给它:
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);

创建两个线程并理解在获取锁时如何发生死锁的deadlockstate.c程序如下:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

#define max 10
pthread_mutex_t pop_mutex;
pthread_mutex_t push_mutex;
int stack[max];
int top=-1;

void * push(void *arg) {
    int n;
    pthread_mutex_lock(&push_mutex);
    pthread_mutex_lock(&pop_mutex);
    printf("Enter the value to push: ");
    scanf("%d",&n);
    top++;
    stack[top]=n;
    pthread_mutex_unlock(&pop_mutex);
    pthread_mutex_unlock(&push_mutex);
    printf("Value is pushed to stack \n");
}
void * pop(void *arg) {
    int k;
    pthread_mutex_lock(&pop_mutex);
    pthread_mutex_lock(&push_mutex);
    k=stack[top];
    top--;
    printf("Value popped is %d \n",k);
    pthread_mutex_unlock(&push_mutex);
    pthread_mutex_unlock(&pop_mutex);
}

int main() {
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,&push,NULL);
    pthread_create(&tid2,NULL,&pop,NULL);
    printf("Both threads are created\n");
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    return 0;
}

现在,让我们看看幕后。

它是如何工作的...

我们首先定义一个名为max的值为10的宏,以及一个大小为max的数组stack。然后,我们将定义两个名为pop_mutexpush_mutexmutex对象。为了使用stack,我们将top的值初始化为-1。我们还将定义两个类型为pthread_t的变量,分别命名为tid1tid2,以存储两个线程标识符。

我们将调用pthread_create函数来创建第一个线程,并将函数返回的标识符赋值给变量tid1。线程将以默认属性创建,我们将执行push函数来创建这个线程。

我们将再次调用pthread_create函数来创建第二个线程,并将函数返回的标识符赋值给变量tid2。这个线程也将以默认属性创建,我们将执行pop函数来创建这个线程。在屏幕上,我们将显示消息Both threads are created

push函数中,我们将调用pthread_mutex_lock方法并将mutex对象push_mutex传递给它以锁定它。现在,如果任何其他线程请求push_mutex对象,它将需要等待直到对象解锁。

然后,mutex对象pop_mutex将被第一个线程锁定。我们将被要求输入要推入栈中的值。输入的值将被分配给变量ntop的值将增加至0。我们输入的值将被推入stack[0]的位置。

接下来,我们将调用pthread_mutex_unlock并将mutex对象pop_mutex传递给它以解锁它。同时,mutex对象push_mutex也将被解锁。在push函数的底部,我们将显示消息Value is pushed to stack

pop函数中,mutex对象pop_mutex将被锁定,然后它将尝试锁定已被第一个线程锁定的push_mutex对象。栈中的值,即由指针top指向的值将被弹出。因为top的值是0,所以stack[0]位置的值将被取出并分配给变量k。之后,top的值将减1,再次变为-1。从栈中弹出的值将在屏幕上显示。然后,将mutex对象push_mutex解锁,随后解锁pop_mutex对象。

main函数中,我们将调用pthread_join方法两次,并将线程标识符tid1tid2传递给它。我们调用pthread_join方法的原因是使两个线程和main方法等待,直到两个线程都完成了它们的工作。

在这个程序中,发生了死锁,因为在push函数中,第一个线程锁定了push_mutex对象并试图获取pop_mutex对象的锁,该锁已被第二个线程在pop函数中锁定。在pop函数中,线程锁定了mutex对象pop_mutex并试图锁定已被第一个线程锁定的push_mutex对象。因此,两个线程中的任何一个都无法完成,它们将无限期地等待另一个线程释放其mutex对象。

让我们使用 GCC 编译deadlockstate.c程序,如下所示:

D:\CBook>gcc deadlockstate.c -o deadlockstate

如果你没有错误或警告,这意味着deadlockstate.c程序已编译成可执行文件,deadlockstate.exe。让我们运行这个可执行文件:

图 7.5

你现在已经看到了死锁是如何发生的。现在,让我们继续下一个菜谱!

避免死锁

如果允许线程按顺序获取锁,则可以避免死锁。假设一个线程获取了一个资源的锁,并想要获取第二个资源的锁。任何试图获取第一个锁的其他线程将被要求等待,因为它已经被第一个线程获取了。因此,第二个线程也无法获取第二个资源的锁,因为它只能按顺序获取锁。然而,我们的第一个线程将被允许获取第二个资源的锁,而无需等待。

将顺序应用于资源锁定与只允许一个线程一次获取资源相同。其他线程只能在之前的线程完成后才能获取资源。这样,我们手中就不会有死锁了。

如何做…

  1. 定义一个包含10个元素的数组:
#define max 10
int stack[max];
  1. 定义两个mutex对象——一个用于表示栈的pop操作(pop_mutex),另一个用于表示栈的push操作(push_mutex):
pthread_mutex_t pop_mutex;
pthread_mutex_t push_mutex;
  1. 要使用栈,top的值被初始化为-1
int top=-1;
  1. 定义两个类型为 pthread_t 的变量,以存储两个线程标识符:
pthread_t tid1,tid2;
  1. 调用 pthread_create 函数以创建第一个线程。该线程使用默认属性创建,并执行 push 函数以创建线程:
pthread_create(&tid1,NULL,&push,NULL);
  1. 再次调用 pthread_create 函数以创建第二个线程。该线程使用默认属性创建,并执行 pop 函数以创建此线程:
pthread_create(&tid2,NULL,&pop,NULL);
  1. 为了指示已创建了两个线程,显示消息 Both threads are created
printf("Both threads are created\n");
  1. push 函数中,调用 pthread_mutex_lock 方法并将与 push 操作相关的 mutex 对象 push_mutex 传递给它,以锁定它:
pthread_mutex_lock(&push_mutex);
  1. 2 秒的睡眠之后,第一个线程将锁定用于执行 pop 操作的 mutex 对象 pop_mutex
sleep(2);
pthread_mutex_lock(&pop_mutex);
  1. 输入要推入栈中的值:
printf("Enter the value to push: ");
scanf("%d",&n);
  1. top 的值增加至 0。将用户输入的值推入 stack[0] 位置:
top++;
stack[top]=n;
  1. 调用 pthread_mutex_unlock 并将 mutex 对象 pop_mutex 传递给它以解锁它。此外,mutex 对象 push_mutex 也将被解锁:
pthread_mutex_unlock(&pop_mutex);                                                   pthread_mutex_unlock(&push_mutex);
  1. push 函数的底部,显示消息 Value is pushed to stack
printf("Value is pushed to stack \n");
  1. pop 函数中,调用 pthread_mutex_lock 函数以锁定 mutex 对象 push_mutex
pthread_mutex_lock(&push_mutex);
  1. 5 秒的睡眠(或延迟)之后,pop 函数将尝试锁定 pop_mutex 对象。然而,由于线程正在等待 push_mutex 对象解锁,因此不会调用 pthread_mutex_lock 函数:
sleep(5);
pthread_mutex_lock(&pop_mutex);
  1. 由指针 top 指向的栈中的值被弹出。因为 top 的值为 0,所以从 stack[0] 位置取出的值:
k=stack[top];
  1. 此后,top 的值将减 1 以再次变为 -1。从栈中弹出的值将在屏幕上显示:
top--;
printf("Value popped is %d \n",k);
  1. 然后,将 mutex 对象 pop_mutex 解锁,接着是 push_mutex 对象:
pthread_mutex_unlock(&pop_mutex);
pthread_mutex_unlock(&push_mutex);
  1. main 函数中,调用 pthread_join 方法两次,并将步骤 1 中创建的线程标识符传递给它:
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);

用于创建两个线程并理解在获取锁时如何避免死锁的 avoiddeadlockst.c 程序如下:

#include <stdio.h>
#include <pthread.h>
#include<unistd.h>
#include <stdlib.h>

#define max 10
pthread_mutex_t pop_mutex;
pthread_mutex_t push_mutex;
int stack[max];
int top=-1;

void * push(void *arg) {
    int n;
    pthread_mutex_lock(&push_mutex);
    sleep(2);
    pthread_mutex_lock(&pop_mutex);
    printf("Enter the value to push: ");
    scanf("%d",&n);
    top++;
    stack[top]=n;
    pthread_mutex_unlock(&pop_mutex);
    pthread_mutex_unlock(&push_mutex);
    printf("Value is pushed to stack \n");
}

void * pop(void *arg) {
    int k;
    pthread_mutex_lock(&push_mutex);
    sleep(5);
    pthread_mutex_lock(&pop_mutex);
    k=stack[top];
    top--;
    printf("Value popped from stack is %d \n",k);
    pthread_mutex_unlock(&pop_mutex);
    pthread_mutex_unlock(&push_mutex);
}

int main() {
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,&push,NULL);
    pthread_create(&tid2,NULL,&pop,NULL);
    printf("Both threads are created\n");
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    return 0;
}

现在,让我们看看幕后发生了什么。

它是如何工作的...

我们将首先定义一个名为 max 的宏,其值为 10。然后,我们将定义一个大小为 max 的数组 stack。我们将定义两个名为 pop_mutexpush_mutexmutex 对象。

要使用栈,top 的值将被初始化为 -1。我们将定义两个类型为 pthread_t 的变量,分别命名为 tid1tid2,以存储两个线程标识符。

我们将调用 pthread_create 函数以创建第一个线程,并将函数返回的标识符分配给变量 tid1。该线程将使用默认属性创建,并执行 push 函数以创建此线程。

我们将第二次调用pthread_create函数来创建第二个线程,并将函数返回的标识符赋值给变量tid2。该线程将以默认属性创建,并执行pop函数来创建此线程。在屏幕上,我们将显示消息Both threads are created

push函数中,将调用pthread_mutex_lock方法,并将mutex对象push_mutex传递给它以锁定。现在,如果任何其他线程请求pop_mutex对象,它将需要等待直到对象被解锁。经过2秒的睡眠后,第一个线程将锁定mutex对象pop_mutex

我们将被提示输入要推送到栈中的值。输入的值将被赋值给变量ntop的值将增加至0。我们输入的值将被推送到stack[0]的位置。现在,将调用pthread_mutex_unlock,并将mutex对象pop_mutex传递给它以解锁。同时,mutex对象push_mutex也将被解锁。在push函数的底部,将显示消息Value is pushed to stack

pop函数中,它将尝试锁定mutex对象push_mutex,但由于它已被第一个线程锁定,此线程将被要求等待。经过5秒的睡眠或延迟后,它也将尝试锁定pop_mutex对象。栈中的值,即由指针top指向的值将被弹出。因为top的值为0,所以stack[0]中的值被取出并赋值给变量k

此后,top的值将减少1,使其再次变为-1。从栈中弹出的值将在屏幕上显示。然后,将解锁mutex对象pop_mutex,接着是push_mutex对象。

main函数中,将两次调用pthread_join方法,并将线程标识符tid1tid2传递给它。调用pthread_join是为了使两个线程和main方法等待,直到两个线程都完成了它们的工作。

在这里,我们避免了死锁,因为对 mutex 对象的加锁和解锁是按顺序进行的。在 push 函数中,第一个线程锁定了 push_mutex 对象,并尝试锁定 pop_mutex 对象。由于 pop_mutex 被第二个线程在 pop 函数中首先尝试锁定,然后是 pop_mutex 对象,所以 pop_mutex 保持空闲状态。由于第一个线程已经锁定了 push_mutex 对象,第二个线程被要求等待。因此,两个 mutex 对象,push_mutexpop_mutex,都处于未锁定状态,第一个线程能够轻松地锁定这两个 mutex 对象并使用公共资源。完成其任务后,第一个线程将解锁这两个 mutex 对象,使得第二个线程能够锁定这两个 mutex 对象并访问公共资源线程。

让我们使用 GCC 编译 avoiddeadlockst.c 程序,如下所示:

D:\CBook>gcc avoiddeadlockst.c -o avoiddeadlockst

如果你没有收到任何错误或警告,这意味着 avoiddeadlockst.c 程序已经被编译成一个可执行文件,名为 avoiddeadlockst.exe。让我们运行这个可执行文件:

图 7.6

哇!我们已经成功避免了死锁。

第八章:网络和进程间通信

进程各自独立运行,并在各自的地址空间中独立工作。然而,它们有时需要相互通信以传递信息。为了使进程能够合作,它们需要能够相互通信并同步它们的行为。以下是进程间发生的通信类型:

  • 同步通信:这种通信不允许进程在通信完成之前继续进行任何其他工作

  • 异步通信:在这种通信中,进程可以继续执行其他任务,因此它支持多任务处理,从而提高效率

  • 远程过程调用RPC):这是一个使用客户端服务技术进行通信的协议,客户端无法执行任何操作,也就是说,它被挂起,直到从服务器收到响应

这些通信可以是单向的或双向的。为了启用进程间任何形式的通信,以下流行的进程间通信IPC)机制被使用:管道、FIFO(命名管道)、套接字、消息队列和共享内存。管道和 FIFO 启用单向通信,而套接字、消息队列和共享内存启用双向通信。

在本章中,我们将学习如何制作以下配方,以便我们可以建立进程间的通信:

  • 使用管道进行进程间通信

  • 使用 FIFO 进行进程间通信

  • 使用套接字编程在客户端和服务器之间进行通信

  • 使用 UDP 套接字进行进程间通信

  • 使用消息队列从一个进程向另一个进程传递消息

  • 使用共享内存进行进程间通信

让我们从第一个配方开始!

使用管道进行进程间通信

在这个配方中,我们将学习如何从其写入端将数据写入管道,然后如何从其读取端读取该数据。这可以通过两种方式发生:

  • 一个进程,既从管道写入又从管道读取

  • 一个进程向管道写入,另一个进程从管道读取

在我们开始配方之前,让我们快速回顾一下在成功的进程间通信中使用的函数、结构和术语。

创建和连接进程

用于进程间通信的最常用函数和术语是pipemkfifowritereadperrorfork

pipe()

管道用于连接两个进程。一个进程的输出可以作为另一个进程的输入发送。流程是单向的,也就是说,一个进程可以写入管道,另一个进程可以从中读取。写入和读取是在主内存的一个区域进行的,这也被称为虚拟文件。管道具有先进先出FIFO)或队列结构,即先写入的将被先读取。

一个进程不应该在向管道写入内容之前尝试从管道中读取,否则它将挂起,直到有内容写入管道。

以下是它的语法:

int pipe(int arr[2]);

在这里,arr[0]是管道读取端的文件描述符,arr[1]是管道写入端的文件描述符。

函数在成功时返回0,在出错时返回-1

mkfifo()

此函数创建一个新的 FIFO 特殊文件。以下是其语法:

int mkfifo(const char *filename, mode_t permission);

在这里,filename代表文件名及其完整路径,permission代表新 FIFO 文件的权限位。默认权限是所有者、组和其他人的读写权限,即(0666)。

函数在成功完成后返回0;否则,返回-1

write()

此函数用于写入指定的文件或管道(其描述符由提供)。以下是其语法:

write(int fp, const void *buf, size_t n);

它将n个字节写入由文件指针fp指向的文件,来自缓冲区buf

read()

此函数从指定文件或管道(其描述符由方法提供)读取。以下是其语法:

read(int fp, void *buf, size_t n);

它尝试从由描述符fp指向的文件中读取多达n个字节。读取的字节随后被分配到缓冲区buf

perror()

这会显示一个错误消息,指示在调用函数或系统调用时可能发生的错误。错误消息显示到stderr,即标准错误输出流。这基本上是控制台。

以下是它的语法:

void perror ( const char * str );

显示的错误消息可以由str表示的消息作为前缀。

fork()

这用于创建一个新的进程。新创建的进程被称为子进程,它与父进程并发运行。在执行fork函数后,程序的执行继续,fork函数之后的指令由父进程和子进程同时执行。如果系统调用成功,它将返回子进程的进程 ID,并将0返回给新创建的子进程。如果子进程没有创建,函数将返回负值。

现在,让我们从第一个配方开始,使用管道在进程之间启用通信。

一个进程,既从管道写入又从管道读取

在这里,我们将学习单个进程如何进行管道的写入和读取。

如何做到这一点…

  1. 定义一个大小为2的数组,并将其作为参数传递给pipe函数。

  2. 调用write函数并通过数组的写入端写入所选字符串到管道。为第二条消息重复此过程。

  3. 调用read函数从管道中读取第一条消息。再次调用read函数以读取第二条消息。

用于写入管道并随后从管道中读取的readwritepipe.c程序如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define max 50

int main()
{
    char str[max];
    int pp[2];

    if (pipe(pp) < 0)
        exit(1);
    printf("Enter first message to write into pipe: ");
    gets(str);
    write(pp[1], str, max);
    printf("Enter second message to write into pipe: ");
    gets(str);
    write(pp[1], str, max);
    printf("Messages read from the pipe are as follows:\n");
    read(pp[0], str, max);
    printf("%s\n", str);
    read(pp[0], str, max);
    printf("%s\n", str);
    return 0;
}

让我们看看幕后。

它是如何工作的...

我们定义了一个大小为50的宏max,一个大小为max的字符串str,以及一个大小为2的数组pp。我们将调用pipe函数连接两个进程并将pp数组传递给它。索引位置pp[0]将获取管道读取端的文件描述符,而pp[1]将获取管道写入端的文件描述符。如果pipe函数没有成功执行,程序将退出。

你将被提示输入要写入管道的第一个消息。你输入的文本将被分配给字符串变量str。调用write函数,str中的字符串将被写入管道pp。重复此过程以写入第二个消息。你输入的第二个文本也将被写入管道。

显然,第二个文本将在管道中第一个文本之后写入。现在,调用read函数从管道读取。管道中首先输入的文本将被读取并分配给字符串变量str,并随后显示在屏幕上。再次调用read函数,管道中的第二个文本消息将从其读取端读取并分配给字符串变量str,然后显示在屏幕上。

让我们使用 GCC 编译readwritepipe.c程序,如下所示:

$ gcc readwritepipe.c -o readwritepipe

如果没有错误或警告,这意味着readwritepipe.c程序已被编译成可执行文件,readwritepipe.exe。让我们运行这个可执行文件:

$ ./readwritepipe
Enter the first message to write into pipe: This is the first message for the pipe
Enter the second message to write into pipe: Second message for the pipe
Messages read from the pipe are as follows:
This is the first message for the pipe
Second message for the pipe

在前面的程序中,主线程负责从管道写入和读取。但如果我们想一个进程向管道写入,另一个进程从管道读取呢?让我们看看如何实现这一点。

一个进程向管道写入,另一个进程从管道读取

在这个菜谱中,我们将使用fork系统调用创建一个子进程。然后,我们将使用子进程向管道写入,并通过父进程从管道读取,从而在两个进程之间建立通信。

如何操作…

  1. 定义一个大小为2的数组。

  2. 调用pipe函数连接两个进程,并将我们之前定义的数组传递给它。

  3. 调用fork函数创建一个新的子进程。

  4. 输入将要写入管道的消息。使用新创建的子进程调用write函数。

  5. 父进程调用read函数读取已写入管道的文本。

pipedemo.c程序通过子进程写入管道并通过父进程读取管道,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define max  50

int main()
{
    char wstr[max];
    char rstr[max];
    int pp[2];
    pid_t p;
    if(pipe(pp) < 0)
    {
        perror("pipe");
    } 
    p = fork();
    if(p >= 0)
    {
        if(p == 0)
        {
            printf ("Enter the string : ");
            gets(wstr);
            write (pp[1] , wstr , strlen(wstr));
            exit(0);
        }
        else
        {
            read (pp[0] , rstr , sizeof(rstr));
            printf("Entered message : %s\n " , rstr);
            exit(0);
        }
    }
    else
    {
        perror("fork");
        exit(2);
    }        
    return 0;
}

让我们看看幕后。

它是如何工作的...

定义一个大小为 50 的宏 max,以及两个大小为 max 的字符串变量 wstrrstrwstr 字符串将用于写入管道,而 rstr 将用于从管道读取。定义一个大小为 2 的数组 pp,它将用于存储管道的读写端文件描述符。定义一个 pid_t 数据类型的变量 p,它将用于存储进程 ID。

我们将调用 pipe 函数来连接两个进程,并将 pp 数组传递给它。索引位置 pp[0] 将获得管道的读取端文件描述符,而 pp[1] 将获得管道的写入端文件描述符。如果 pipe 函数没有成功执行,程序将退出。

然后,我们将调用 fork 函数来创建一个新的子进程。你将被提示输入要写入管道的消息。你输入的文本将被分配给字符串变量 wstr。当我们使用新创建的子进程调用 write 函数时,wstr 变量中的字符串将被写入管道 pp。之后,父进程将调用 read 函数来读取已写入管道的文本。从管道读取的文本将被分配给字符串变量 rstr,并随后显示在屏幕上。

让我们使用 GCC 编译 pipedemo.c 程序,如下所示:

$ gcc pipedemo.c -o pipedemo

如果没有错误或警告,这意味着 pipedemo.c 程序已经被编译成可执行文件,名为 pipedemo.exe。让我们运行这个可执行文件:

$ ./pipedemo
Enter the string : This is a message from the pipe
Entered message : This is a message from the pipe

哇!我们已经成功地使用管道在进程间进行了通信。现在,让我们继续下一个菜谱!

使用 FIFO 在进程间通信

在这个菜谱中,我们将学习两个进程如何使用命名管道(也称为 FIFO)进行通信。这个菜谱分为以下两个部分:

  • 展示如何将数据写入 FIFO

  • 展示如何从 FIFO 读取数据

我们在先前的菜谱中学到的函数和术语也适用于此处。

将数据写入 FIFO

正如其名所示,我们将在这个菜谱中学习如何将数据写入 FIFO。

如何做到这一点…

  1. 调用 mkfifo 函数来创建一个新的 FIFO 特殊文件。

  2. 通过调用 open 函数以只写模式打开 FIFO 特殊文件。

  3. 输入要写入 FIFO 特殊文件的文本。

  4. 关闭 FIFO 特殊文件。

写入 FIFO 的 writefifo.c 程序如下:

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fw;
    char str[255];
    mkfifo("FIFOPipe", 0666);
    fw = open("FIFOPipe", O_WRONLY);
    printf("Enter text: ");
    gets(str);
    write(fw,str, sizeof(str));
    close(fw);
    return 0;
}

让我们看看幕后发生了什么。

它是如何工作的...

假设我们定义了一个大小为 255 的字符串 str。我们将调用 mkfifo 函数来创建一个新的 FIFO 特殊文件。我们将创建一个名为 FIFOPipe 的 FIFO 特殊文件,具有所有者、组和其他人的读写权限。

我们将通过调用 open 函数以只写模式打开这个 FIFO 特殊文件。然后,我们将打开的 FIFO 特殊文件的文件描述符分配给 fw 变量。你将被提示输入将要写入文件的文字。你输入的文字将被分配给 str 变量,然后当你调用 write 函数时,这个文字将被写入特殊的 FIFO 文件。最后,关闭 FIFO 特殊文件。让我们使用 GCC 编译 writefifo.c 程序,如下所示:

$ gcc writefifo.c -o writefifo

如果你没有错误或警告,这意味着 writefifo.c 程序已编译成可执行文件,writefifo.exe。让我们运行这个可执行文件:

$ ./writefifo
Enter text: This is a named pipe demo example called FIFO

如果你的程序没有提示输入字符串,这意味着它正在等待 FIFO 的另一端打开。也就是说,你需要在第二个终端屏幕上运行下一个菜谱,从 FIFO 读取数据。请在 Cygwin 上按 Alt+F2 打开下一个终端屏幕。

现在,让我们检查这个菜谱的另一个部分。

从 FIFO 读取数据

在这个菜谱中,我们将看到如何从 FIFO 读取数据。

如何做到这一点…

  1. 通过调用 open 函数以只读模式打开 FIFO 特殊文件。

  2. 使用 read 函数从 FIFO 特殊文件读取文本。

  3. 关闭 FIFO 特殊文件。

以下是从命名管道(FIFO)读取的 readfifo.c 程序:

#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

#define BUFFSIZE 255

int main()
{
    int fr;
    char str[BUFFSIZE];
    fr = open("FIFOPipe", O_RDONLY);
    read(fr, str, BUFFSIZE);
    printf("Read from the FIFO Pipe: %s\n", str);
    close(fr);
    return 0;
}

让我们幕后看看。

它是如何工作的...

我们将首先定义一个名为 BUFFSIZE 的宏,大小为 255,以及一个大小为 BUFFSIZE 的字符串 str,即 255 个字符。我们将通过调用 open 函数以只读模式打开名为 FIFOPipe 的 FIFO 特殊文件。打开的 FIFO 特殊文件的文件描述符将被分配给 fr 变量。

使用 read 函数,将从 FIFO 特殊文件读取的文本分配给 str 字符串变量。然后,从 FIFO 特殊文件读取的文本将在屏幕上显示。最后,关闭 FIFO 特殊文件。

现在,按 Alt + F2 打开第二个终端窗口。在第二个终端窗口中,让我们使用 GCC 编译 readfifo.c 程序,如下所示:

$ gcc readfifo.c -o readfifo

如果你没有错误或警告,这意味着 readfifo.c 程序已编译成可执行文件,readfifo.exe。让我们运行这个可执行文件:

$ ./readfifo
Read from the FIFO Pipe: This is a named pipe demo example called FIFO

当你运行 readfifo.exe 文件时,你会在之前运行 writefifo.c 程序的终端屏幕上发现,会提示你输入一个字符串。当你在这个终端上输入一个字符串并按下 Enter 键时,你将得到 readfifo.c 程序的输出。

哇!我们已经成功使用 FIFO 在进程之间进行了通信。现在,让我们继续下一个菜谱!

使用套接字编程在客户端和服务器之间进行通信

在这个菜谱中,我们将学习如何将来自服务器进程的数据发送到客户端进程。这个菜谱分为以下部分:

  • 向客户端发送数据

  • 读取从服务器发送的数据

在我们开始介绍食谱之前,让我们快速回顾一下在成功的客户端-服务器通信中使用的函数、结构和术语。

客户端-服务器模型

在 IPC 中使用了不同的模型,但最流行的是客户端-服务器模型。在这个模型中,每当客户端需要某些信息时,它会连接到另一个称为服务器的进程。但在建立连接之前,客户端需要知道服务器是否已经存在,并且应该知道服务器的地址。

另一方面,服务器旨在满足客户端的需求,在建立连接之前不需要知道客户端的地址。为了建立连接,需要一个称为套接字的基本构造,并且连接的两个进程都必须建立自己的套接字。客户端和服务器需要遵循某些程序来建立它们的套接字。

在客户端建立套接字时,使用系统调用函数socket创建一个套接字。之后,使用connect函数系统调用将该套接字连接到服务器的地址,随后通过调用read函数和write函数系统调用发送和接收数据。

在服务器端建立套接字时,同样使用系统调用函数socket创建一个套接字,然后使用bind函数系统调用将该套接字绑定到一个地址。之后,调用listen函数系统调用以监听连接。最后,通过调用accept函数系统调用接受连接。

struct sockaddr_in结构体

此结构引用了用于保持地址的套接字元素。以下是该结构的内置成员:

struct sockaddr_in {
 short int sin_family;
 unsigned short int sin_port;
 struct in_addr sin_addr;
 unsigned char sin_zero[8];
};

这里,我们有以下内容:

  • sin_family:表示一个地址族。有效的选项有AF_INETAF_UNIXAF_NSAF_IMPLINK。在大多数应用中,使用的地址族是AF_INET

  • sin_port:表示 16 位服务端口号。

  • sin_addr:表示 32 位 IP 地址。

  • sin_zero:这个字段未使用,通常设置为NULL

struct in_addr包含一个成员,如下所示:


struct in_addr {
     unsigned long s_addr; 
};

在这里,s_addr用于表示网络字节顺序的地址。

socket()

此函数创建了一个通信端点。为了建立通信,每个进程需要在通信线的末端有一个套接字。此外,两个通信进程必须具有相同的套接字类型,并且它们都应该在同一个域中。以下是创建套接字的语法:

int socket(int domain, int type, int protocol);

这里,domain表示要创建套接字的通信域。基本上,指定了地址族协议族,这将用于通信。

以下列出了一些流行的地址族

  • AF_LOCAL:这用于本地通信。

  • AF_INET:这用于 IPv4 互联网协议。

  • AF_INET6:这用于 IPv6 互联网协议。

  • AF_IPX:用于使用标准 IPX(即 Internetwork Packet Exchange)套接字地址的协议。

  • AF_PACKET:用于数据包接口。

  • type:表示要创建的套接字类型。以下是一些流行的套接字类型:

  • SOCK_STREAM:流套接字使用 传输控制协议 (TCP) 作为字符的连续流进行通信。TCP 是一种可靠的、面向流的协议。因此,SOCK_STREAM 类型提供了可靠的、双向的、基于连接的字节流。

    • SOCK_DGRAM:数据报套接字使用 用户数据报协议 (UDP) 一次性读取整个消息。UDP 是一种不可靠的、无连接的、面向消息的协议。这些消息具有固定的最大长度。
  • SOCK_SEQPACKET:为数据报提供可靠的、双向的、基于连接的传输路径。

  • protocol:表示与套接字一起使用的协议。指定 0 值是为了可以使用适合请求套接字类型的默认协议。

你可以将前面列表中的 AF_ 前缀替换为 PF_ 以表示 协议族

在成功执行后,socket 函数返回一个可以用来管理套接字的文件描述符。

memset()

这用于用指定的值填充内存块。以下是其语法:

void *memset(void *ptr, int v, size_t n);

在这里,ptr 指向要填充的内存地址,v 是要填充到内存块中的值,而 n 是从指针位置开始要填充的字节数。

htons()

这用于将主机无符号短整数转换为网络字节序。

bind()

使用 socket 函数创建的套接字保持在其分配的地址族中。为了使套接字能够接收连接,需要为其分配地址。bind 函数将地址分配给指定的套接字。以下是其语法:

   int bind(int fdsock, const struct sockaddr *structaddr, socklen_t lenaddr);

在这里,fdsock 代表套接字的文件描述符,structaddr 代表包含要分配给套接字的地址的 sockaddr 结构,而 lenaddr 代表由 structaddr 指向的地址结构的大小。

listen()

它在套接字上监听连接,以便接受传入的连接请求。以下是其语法:

int listen(int sockfd, int lenque);

在这里,sockfd 代表套接字的文件描述符,而 lenque 代表给定套接字的挂起连接队列的最大长度。如果队列已满,将生成错误。

如果函数执行成功,它返回 0,否则返回 -1

accept()

它在监听套接字上接受新的连接,即从挂起的连接队列中选取的第一个连接。实际上,会创建一个新的套接字,其套接字类型协议和地址族与指定的套接字相同,并为该套接字分配一个新的文件描述符。以下是其语法:

int accept(int socket, struct sockaddr *address, socklen_t *len);

在这里,我们需要解决以下问题:

  • socket:代表等待新连接的套接字的文件描述符。这是当 socket 函数通过 bind 函数绑定到地址并成功调用 listen 函数时创建的套接字。

  • address:通过此参数返回连接套接字的地址。它是一个指向 sockaddr 结构体的指针,通过它返回连接套接字的地址。

  • len:代表提供的 sockaddr 结构体的长度。返回时,此参数包含以字节为单位返回的地址长度。

send()

这用于将指定的消息发送到另一个套接字。在调用此函数之前,套接字需要处于连接状态。以下是其语法:

       ssize_t send(int fdsock, const void *buf, size_t length, int flags);

在这里,fdsock 代表发送消息的套接字的文件描述符,buf 指向包含要发送消息的缓冲区,length 代表以字节为单位要发送的消息长度,flags 指定要发送的消息类型。通常,其值保持为 0

connect()

这在套接字上初始化一个连接。以下是其语法:

int connect(int fdsock, const struct sockaddr *addr,  socklen_t len);

在这里,fdsock 代表希望连接的套接字的文件描述符,addr 代表包含套接字地址的结构体,len 代表包含地址的结构体 addr 的大小。

recv()

这用于从连接的套接字接收消息。套接字可能处于连接模式或无连接模式。以下是其语法:

ssize_t recv(int fdsock, void *buf, size_t len, int flags);

在这里,fdsock 代表从其中获取消息的套接字的文件描述符,buf 代表存储接收到的消息的缓冲区,len 指定由 buf 参数指向的缓冲区的字节长度,flags 指定正在接收的消息类型。通常,其值保持为 0

我们现在可以开始本配方的第一部分——如何向客户端发送数据。

向客户端发送数据

在本部分的配方中,我们将学习服务器如何向客户端发送所需数据。

如何做到这一点…

  1. 定义一个 sockaddr_in 类型的变量。

  2. 调用 socket 函数创建套接字。为套接字指定的端口号是 2000

  3. 调用 bind 函数为它分配一个 IP 地址。

  4. 调用 listen 函数。

  5. 调用 accept 函数。

  6. 调用 send 函数将用户输入的消息发送到套接字。

  7. 客户端端的套接字将接收消息。

发送消息到客户端的服务器程序 serverprog.c 如下:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

int main(){
    int serverSocket, toSend;
    char str[255];
    struct sockaddr_in server_Address;
    serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    server_Address.sin_family = AF_INET;
    server_Address.sin_port = htons(2000);
    server_Address.sin_addr.s_addr = inet_addr("127.0.0.1");
    memset(server_Address.sin_zero, '\0', sizeof 
    server_Address.sin_zero); 
    bind(serverSocket, (struct sockaddr *) &server_Address, 
    sizeof(server_Address));
    if(listen(serverSocket,5)==-1)
    {
        printf("Not able to listen\n");
        return -1;
    }
    printf("Enter text to send to the client: ");
    gets(str);
    toSend = accept(serverSocket, (struct sockaddr *) NULL, NULL);
    send(toSend,str, strlen(str),0);
    return 0;
}

让我们看看幕后。

它是如何工作的...

我们将首先定义一个大小为 255 的字符串和一个类型为 sockaddr_inserver_Address 变量。这个结构引用了套接字的元素。然后,我们将调用 socket 函数以 serverSocket 的名称创建套接字。套接字是通信的端点。为套接字提供的地址族是 AF_INET,选择的套接字类型是流套接字类型,因为我们想要的通信是字符的连续流。

为套接字指定的地址族是 AF_INET,用于 IPv4 互联网协议。为套接字指定的端口号是 2000。使用 htons 函数,将短整数 2000 转换为网络字节序,然后作为端口号应用。通过调用 memset 函数,将 server_Address 结构的第四个参数 sin_zero 设置为 NULL

要使创建的 serverSocket 能够接收连接,调用 bind 函数为其分配一个地址。使用 server_Address 结构的 sin_addr 成员,将 32 位 IP 地址应用到套接字上。因为我们是在本地机器上工作,所以将本地主机地址 127.0.0.1 分配给套接字。现在,套接字可以接收连接。我们将调用 listen 函数来使 serverSocket 能够接受传入的连接请求。套接字可以有的最大挂起连接数是 5。

你将被提示输入要发送给客户端的文本。你输入的文本将被分配给 str 字符串变量。通过调用 accept 函数,我们将使 serverSocket 能够接受新的连接。

连接套接字的地址将通过 sockaddr_in 类型的结构返回。返回并准备好接受连接的套接字被命名为 toSend。我们将调用 send 函数来发送你输入的消息。客户端的套接字将接收该消息。

让我们使用 GCC 编译 serverprog.c 程序,如下所示:

$ gcc serverprog.c -o serverprog

如果你没有错误或警告,这意味着 serverprog.c 程序已编译成可执行文件,名为 serverprog.exe。让我们运行这个可执行文件:

$ ./serverprog
Enter text to send to the client: thanks and good bye

现在,让我们看看这个说明的另一个部分。

读取从服务器发送的数据

在本部分的说明中,我们将学习如何接收从服务器发送的数据并将其显示在屏幕上。

如何做到这一点...

  1. 定义一个类型为 sockaddr_i 的变量。

  2. 调用 socket 函数创建套接字。为套接字指定的端口号是 2000

  3. 调用 connect 函数来初始化与套接字的连接。

  4. 因为我们是在本地机器上工作,所以将本地主机地址 127.0.0.1 分配给套接字。

  5. 调用 recv 函数从已连接的套接字接收消息。从套接字读取的消息随后将在屏幕上显示。

读取从服务器发送的消息的客户端程序 clientprog.c 如下所示:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

int main(){
    int clientSocket;
    char str[255];
    struct sockaddr_in client_Address;
    socklen_t address_size;
    clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    client _Address.sin_family = AF_INET;
    client _Address.sin_port = htons(2000);
    client _Address.sin_addr.s_addr = inet_addr("127.0.0.1");
    memset(client _Address.sin_zero, '\0', sizeof client_Address.sin_zero); 
    address_size = sizeof server_Address;
    connect(clientSocket, (struct sockaddr *) &client_Address, address_size);
    recv(clientSocket, str, 255, 0);
    printf("Data received from server: %s", str);  
    return 0;
}

让我们深入了解幕后。

它是如何工作的...

因此,我们定义了一个大小为 255 的字符串和一个名为 client_Addresssockaddr_in 类型的变量。我们将调用 socket 函数创建一个名为 clientSocket 的套接字。

为套接字提供的地址族是 AF_INET,用于 IPv4 互联网协议,并且选择的套接字类型是流式套接字类型。指定的套接字端口号是 2000。通过使用 htons 函数,将短整数 2000 转换为网络字节序,然后作为端口号应用。

我们将通过调用 memset 函数将 client_Address 结构的第四个参数 sin_zero 设置为 NULL。我们将通过调用 connect 函数初始化 clientSocket 的连接。通过使用 client_Address 结构的 sin_addr 成员,将一个 32 位 IP 地址应用到套接字上。因为我们是在本地机器上工作,所以将本地主机地址 127.0.0.1 分配给套接字。最后,我们将调用 recv 函数从已连接的 clientSocket 接收消息。从套接字读取的消息将被分配给 str 字符串变量,然后显示在屏幕上。

现在,按 Alt + F2 打开第二个终端窗口。在这里,我们将使用 GCC 编译 clientprog.c 程序,如下所示:

$ gcc clientprog.c -o clientprog

如果你没有错误或警告,这意味着 clientprog.c 程序已编译成可执行文件,clientprog.exe。让我们运行这个可执行文件:

$ ./clientprog
Data received from server: thanks and good bye

哇!我们已经成功使用套接字编程在客户端和服务器之间进行了通信。现在,让我们继续下一个菜谱!

使用 UDP 套接字进行进程间通信

在这个菜谱中,我们将学习如何使用 UDP 套接字在客户端和服务器之间实现双向通信。这个菜谱分为以下几部分:

  • 等待客户端的消息并使用 UDP 套接字发送回复

  • 使用 UDP 套接字向服务器发送消息并从服务器接收回复

在我们开始这些菜谱之前,让我们快速回顾一下使用 UDP 套接字成功进行进程间通信所使用的函数、结构和术语。

使用 UDP 套接字进行服务器-客户端通信

在使用 UDP 进行通信的情况下,客户端不需要与服务器建立连接,只需发送一个数据报。服务器不需要接受连接;它只需等待客户端发送数据报。每个数据报都包含发送者的地址,使服务器能够根据数据报是从哪里发送的来识别客户端。

为了通信,UDP 服务器首先创建一个 UDP 套接字并将其绑定到服务器地址。然后,服务器等待来自客户端的数据报文到达。一旦到达,服务器处理数据报文并向客户端发送回复。这个过程会不断重复。

另一方面,UDP 客户端为了通信,创建一个 UDP 套接字,向服务器发送消息,并等待服务器的响应。如果客户端想要向服务器发送更多消息,它将重复此过程,否则套接字描述符将关闭。

bzero()

这将在指定的区域中放置n个零值字节。其语法如下:

void bzero(void *r, size_t n);

在这里,r是指向r的区域的地址,而n是指向由r指向的区域中放置的零值字节数。

INADDR_ANY

这是一个在不想将套接字绑定到任何特定 IP 时使用的 IP 地址。基本上,在实现通信时,我们需要将我们的套接字绑定到一个 IP 地址。当我们不知道我们机器的 IP 地址时,我们可以使用特殊的 IP 地址INADDR_ANY。它允许我们的服务器接收被任何接口针对的数据包。

sendto()

这用于在指定的套接字上发送消息。消息可以在连接模式以及无连接模式下发送。在无连接模式下,消息被发送到指定的地址。其语法如下:

ssize_t sendto(int fdsock, const void *buff, size_t len, int flags, const struct sockaddr *recv_addr, socklen_t recv_len);

在这里,我们需要解决以下问题:

  • fdsock: 指定套接字的文件描述符。

  • buff: 指向包含要发送的消息的缓冲区。

  • len: 指定消息的字节数。

  • flags: 指定正在传输的消息类型。通常,其值保持为 0。

  • recv_addr: 指向包含接收者地址的sockaddr结构体。地址的长度和格式取决于分配给套接字的地址族。

  • recv_len: 指定由recv_addr参数指向的sockaddr结构体的长度。

在成功执行的情况下,函数返回发送的字节数,否则返回-1

recvfrom()

这用于从连接模式或无连接模式的套接字接收消息。其语法如下:

ssize_t recvfrom(int fdsock, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);

在这里,我们需要解决以下问题:

  • fdsock: 表示套接字的文件描述符。

  • buffer: 表示存储消息的缓冲区。

  • length: 表示由buffer参数指向的缓冲区中的字节数。

  • flags: 表示接收到的消息类型。

  • address: 表示存储发送地址的sockaddr结构体。地址的长度和格式取决于套接字的地址族。

  • address_len: 表示由地址参数指向的sockaddr结构体的长度。

函数返回写入缓冲区(由buffer参数指向)的消息的长度。

现在,我们可以开始本食谱的第一部分:准备服务器,以便使用 UDP 套接字等待并回复来自客户端的消息。

等待来自客户端的消息并使用 UDP 套接字发送回复

在本部分的食谱中,我们将学习服务器如何等待来自客户端的消息,以及当收到来自客户端的消息时,它是如何回复客户端的。

如何做到这一点...

  1. 定义两个类型为sockaddr_in的变量。调用bzero函数初始化该结构。

  2. 调用socket函数创建一个套接字。为套接字提供的地址族是AF_INET,选择的套接字类型是数据报类型。

  3. 初始化sockaddr_in结构体的成员以配置套接字。为套接字指定的端口号是2000。使用特殊 IP 地址INADDR_ANY为套接字分配 IP 地址。

  4. 调用bind函数将地址分配给它。

  5. 调用recvfrom函数从 UDP 套接字接收消息,即从客户端机器。在从客户端机器读取的消息中添加一个空字符\0,并在屏幕上显示。输入要发送给客户端的回复。

  6. 调用sendto函数向客户端发送回复。

等待来自客户端的消息并使用 UDP 套接字发送回复的服务器程序udps.c如下所示:

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include <stdlib.h> 

int main()
{   
    char msgReceived[255];
    char msgforclient[255];
    int UDPSocket, len;
    struct sockaddr_in server_Address, client_Address;
    bzero(&server_Address, sizeof(server_Address));
    printf("Waiting for the message from the client\n");
    if ( (UDPSocket = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) { 
        perror("Socket could not be created"); 
        exit(1); 
    }      
    server_Address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_Address.sin_port = htons(2000);
    server_Address.sin_family = AF_INET; 
    if ( bind(UDPSocket, (const struct sockaddr *)&server_Address, 
    sizeof(server_Address)) < 0 ) 
    { 
        perror("Binding could not be done"); 
        exit(1); 
    } 
    len = sizeof(client_Address);
    int n = recvfrom(UDPSocket, msgReceived, sizeof(msgReceived),  0, 
    (struct sockaddr*)&client_Address,&len);
    msgReceived[n] = '\0';
    printf("Message received from the client: ");
    puts(msgReceived);
    printf("Enter the reply to be sent to the client: ");
    gets(msgforclient);
    sendto(UDPSocket, msgforclient, 255, 0, (struct 
    sockaddr*)&client_Address, sizeof(client_Address));
    printf("Reply to the client sent \n");
}

让我们深入了解。

它是如何工作的...

我们首先定义两个名为msgReceivedmsgforclient的字符串,它们的大小都是255。这两个字符串将分别用于接收来自客户端的消息和向客户端发送消息。然后,我们将定义两个类型为sockaddr_in的变量,server_Addressclient_Address。这些结构将引用套接字元素并分别存储服务器和客户端的地址。我们将调用bzero函数初始化server_Address结构,即server_Address结构体的所有成员将被填充为零。

服务器,正如预期的那样,等待来自客户端的数据报。因此,屏幕上显示以下文本消息:“等待来自客户端的消息”。我们调用socket函数创建一个名为UDPSocket的套接字。为套接字提供的地址族是AF_INET,选择的套接字类型是数据报。server_Address结构体的成员被初始化以配置套接字。

使用sin_family成员,指定给套接字的地址族是AF_INET,它用于 IPv4 互联网协议。指定给套接字的端口号是2000。使用htons函数,将短整数2000转换为网络字节序,然后作为端口号应用。然后,我们使用一个特殊的 IP 地址INADDR_ANY为套接字分配 IP 地址。使用htonl函数,将INADDR_ANY转换为网络字节序,然后作为套接字的地址应用。

为了使创建的套接字UDPSocket能够接收连接,我们将调用bind函数将地址分配给它。我们将调用recvfrom函数从 UDP 套接字接收消息,即从客户端机器。从客户端机器读取的消息被分配给msgReceived字符串,该字符串在recvfrom函数中提供。在msgReceived字符串中添加一个空字符\0,并将其显示在屏幕上。之后,你将被提示输入要发送给客户端的回复。输入的回复被分配给msgforclient。通过调用sendto函数,回复被发送到客户端。发送消息后,屏幕上显示以下消息:已向客户端发送回复

现在,让我们看看本食谱的另一个部分。

使用 UDP 套接字向服务器发送消息并接收来自服务器的回复

如其名所示,在本食谱中,我们将向您展示客户端如何通过 UDP 套接字向服务器发送消息,然后从服务器接收回复。

如何做到这一点…

  1. 执行本食谱前一部分的前三个步骤。将本地主机 IP 地址127.0.0.1分配给套接字的地址。

  2. 输入要发送给服务器的消息。调用sendto函数将消息发送到服务器。

  3. 调用recvfrom函数从服务器获取消息。从服务器接收到的消息随后显示在屏幕上。

  4. 关闭套接字的描述符。

客户端程序udpc.c用于通过 UDP 套接字向服务器发送消息并接收回复,如下所示:

#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{   
    char msgReceived[255];
    char msgforserver[255];
    int UDPSocket, n;
    struct sockaddr_in client_Address;    
    printf("Enter the message to send to the server: ");
    gets(msgforserver);
    bzero(&client_Address, sizeof(client_Address));
    client_Address.sin_addr.s_addr = inet_addr("127.0.0.1");
    client_Address.sin_port = htons(2000);
    client_Address.sin_family = AF_INET;     
    if ( (UDPSocket = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) { 
        perror("Socket could not be created"); 
        exit(1); 
    } 
    if(connect(UDPSocket, (struct sockaddr *)&client_Address, 
    sizeof(client_Address)) < 0)
    {
        printf("\n Error : Connect Failed \n");
        exit(0);
    } 
    sendto(UDPSocket, msgforserver, 255, 0, (struct sockaddr*)NULL, 
    sizeof(client_Address));
    printf("Message to the server sent. \n");
    recvfrom(UDPSocket, msgReceived, sizeof(msgReceived), 0, (struct 
    sockaddr*)NULL, NULL);
    printf("Received from the server: ");
    puts(msgReceived);
    close(UDPSocket);
}

现在,让我们看看幕后的事情。

它是如何工作的...

在本食谱的第一部分,我们已经通过msgReceivedmsgforclient这两个名称定义了两个字符串,它们的大小都是255。我们还定义了两个变量server_Addressclient_Address,它们的数据类型为sockaddr_in

现在,你将被提示输入要发送给服务器的消息。你输入的消息将被分配给msgforserver字符串。然后,我们将调用bzero函数初始化client_Address结构,即所有client_Address结构的成员都将填充零。

接下来,我们将初始化 client_Address 结构体的成员以配置套接字。使用 sin_family 成员,为套接字指定的地址族是 AF_INET,它用于 IPv4 互联网协议。为套接字指定的端口号是 2000。通过使用 htons 函数,将短整数 2000 转换为网络字节顺序,然后将其作为端口号应用。然后,我们将本地主机 IP 地址 127.0.0.1 分配给套接字。我们将对本地主机地址调用 inet_addr 函数,将包含地址的标准 IPv4 点分十进制表示法字符串转换为整数值(适合用作互联网地址),然后再将其应用于 client_Address 结构体的 sin_addr 成员。

我们将调用 socket 函数创建一个名为 UDPSocket 的套接字。为套接字提供的地址族是 AF_INET,选择的套接字类型是数据报。

接下来,我们将调用 sendto 函数将分配给 msgforserver 字符串的消息发送到服务器。同样,我们将调用 recvfrom 函数从服务器获取消息。从服务器接收到的消息分配给 msgReceived 字符串,然后显示在屏幕上。最后,关闭套接字描述符。

让我们使用 GCC 编译 udps.c 程序,如下所示:

$ gcc udps.c -o udps

如果你没有收到任何错误或警告,这意味着 udps.c 程序已编译成可执行文件,名为 udps.exe。让我们运行这个可执行文件:

$ ./udps
Waiting for the message from the client

现在,按 Alt + F2 打开第二个终端窗口。在这里,我们再次使用 GCC 编译 udpc.c 程序,如下所示:

$ gcc udpc.c -o udpc

如果你没有收到任何错误或警告,这意味着 udpc.c 程序已编译成可执行文件,名为 udpc.exe。让我们运行这个可执行文件:

$ ./udpc
Enter the message to send to the server: Will it rain today?
Message to the server sent.

服务器上的输出将给出以下输出:

Message received from the client: Will it rain today?
Enter the reply to be sent to the client: It might
Reply to the client sent

一旦从服务器发送回复,在客户端窗口,你将得到以下输出:

Received from the server: It might

要运行使用共享内存和消息队列演示 IPC 的示例,我们需要运行 Cygserver。如果你在 Linux 上运行这些程序,则可以跳过本节。让我们看看如何运行 Cygserver。

运行 Cygserver

在执行运行 Cygwin 服务器命令之前,我们需要配置 Cygserver 并将其安装为服务。为此,你需要在终端上运行 cygserver.conf 脚本。以下是运行脚本时得到的输出:

$ ./bin/cygserver-config
Generating /etc/cygserver.conf file
Warning: The following function requires administrator privileges!
Do you want to install cygserver as service? yes

The service has been installed under LocalSystem account.
To start it, call `net start cygserver' or `cygrunsrv -S cygserver'.

Further configuration options are available by editing the configuration
file /etc/cygserver.conf. Please read the inline information in that
file carefully. The best option for the start is to just leave it alone.

Basic Cygserver configuration finished. Have fun!

现在,Cygserver 将已配置并安装为服务。下一步是运行服务器。要运行 Cygserver,你需要使用以下命令:

$ net start cygserver
The CYGWIN cygserver service is starting.
The CYGWIN cygserver service was started successfully.

现在 Cygserver 正在运行,我们可以制作一个示例来演示使用共享内存和消息队列的 IPC。

使用消息队列从一个进程向另一个进程传递消息

在这个食谱中,我们将学习如何使用消息队列在两个进程之间建立通信。这个食谱分为以下几部分:

  • 将消息写入消息队列

  • 从消息队列中读取消息

在我们开始这些食谱之前,让我们快速回顾一下在成功使用共享内存和消息队列进行进程间通信时使用的函数、结构和术语。

用于共享内存和消息队列 IPC 的函数

在使用共享内存和消息队列进行 IPC 时,最常用的函数和术语是ftokshmgetshmatshmdtshmctlmsggetmsgrcvmsgsnd

ftok()

这将在提供的文件名和 ID 的基础上生成一个 IPC 键。可以提供文件及其完整路径。文件名必须引用一个现有文件。以下是其语法:

key_t ftok(const char *filename, int id);

如果提供相同的文件名(具有相同的路径)和相同的 ID,则ftok函数将生成相同的键值。在成功完成后,ftok将返回一个键,否则返回-1

shmget()

这分配一个共享内存段并返回与键关联的共享内存标识符。以下是它的语法:

int shmget(key_t key, size_t size, int shmflg);

这里,我们需要解决以下问题:

  • key: 这通常是调用ftok函数返回的值。如果您不想其他进程访问共享内存,也可以将键的值设置为IPC_PRIVATE

  • size: 表示所需共享内存段的大小。

  • shmflg: 这可以是以下任何常量:

    • IPC_CREAT: 如果没有为指定的键存在共享内存标识符,则创建一个新的段。如果没有使用此标志,则函数返回与键关联的共享内存段。

    • IPC_EXCL: 如果指定键的段已经存在,则使shmget函数失败。

在成功执行后,函数以非负整数的形式返回共享内存标识符,否则返回-1

shmat()

这用于将共享内存段附加到给定的地址空间。也就是说,通过调用shmgt函数接收到的共享内存标识符需要与进程的地址空间相关联。以下是它的语法:

void *shmat(int shidtfr, const void *addr, int flag);

这里,我们需要解决以下问题:

  • shidtfr: 表示共享内存段的内存标识符。

  • addr: 表示需要附加段地址空间。如果shmaddr是一个空指针,则段将附加到第一个可用的地址或由系统选择。

  • flag: 如果标志是SHM_RDONLY,则将其附加为只读内存;否则,它是可读可写的。

如果成功执行,则函数将附加共享内存段并返回段的起始地址,否则返回-1

shmdt()

这将分离共享内存段。以下是它的语法:

int shmdt(const void *addr);

这里,addr表示共享内存段所在的地址。

shmctl()

这用于在指定的共享内存段上执行某些控制操作。以下是它的语法:

int shmctl(int shidtr, int cmd, struct shmid_ds *buf);

在这里,我们必须处理以下问题:

  • shidtr:表示共享内存段的标识符。

  • cmd:这可以有以下任何常量:

    • IPC_STAT:这会将与由shidtr表示的共享内存段关联的shmid_ds数据结构的内容复制到由buf指向的结构中

    • IPC_SET:这会将由buf指向的结构的内容写入与由shidtr表示的内存段关联的shmid_ds数据结构

    • IPC_RMID:这将从系统中删除由shidtr指定的共享内存标识符,并销毁与其关联的共享内存段和shmid_ds数据结构

  • buf:这是指向shmid_ds结构的指针。

如果成功执行,函数返回0,否则返回-1

msgget()

这用于创建新的消息队列,以及访问与指定键相关联的现有队列。如果执行成功,该函数返回消息队列的标识符:

       int msgget(key_t key, int flag);

这里,我们必须处理以下问题:

  • key:这是一个唯一的键值,通过调用ftok函数获取。

  • flag:这可以是以下任何常量:

    • IPC_CREAT:如果消息队列不存在,则创建它,并返回新创建的消息队列的标识符。如果消息队列已经存在并且提供了相应的键值,则返回其标识符。

    • IPC_EXCL:如果同时指定了IPC_CREATIPC_EXCL,并且消息队列不存在,则创建它。然而,如果它已经存在,则函数将失败。

msgrcv()

这用于从提供标识符的指定消息队列中读取消息。以下是它的语法:

int msgrcv(int msqid, void *msgstruc, int msgsize, long typemsg, int flag);

这里,我们必须处理以下问题:

  • msqid:表示需要从中读取消息的队列的消息队列标识符。

  • msgstruc:这是用户定义的结构,其中放置读取的消息。用户定义的结构必须包含两个成员。一个是通常命名为mtype的成员,它必须是长整型,用于指定消息的类型,另一个通常称为mesg的成员,它应该是char类型,用于存储消息。

  • msgsize:表示从消息队列中读取的文本大小,以字节为单位。如果读取的消息大于msgsize,则将其截断为msgsize字节。

  • typemsg:指定需要接收的队列上的哪个消息:

    • 如果typemsg0,则接收队列上的第一个消息

    • 如果typemsg大于0,则接收第一个mtype字段等于typemsg的消息

    • 如果typemsg小于0,则接收一个mtype字段小于或等于typemsg的消息

  • flag:确定在队列中找不到所需消息时要采取的操作。如果你不想指定flag,则保持其值为0flag可以具有以下任何值:

    • IPC_NOWAIT:这使msgrcv函数在没有找到所需消息的队列时失败,也就是说,它不会使调用者等待队列上的适当消息。如果flag未设置为IPC_NOWAIT,它将使调用者等待队列上的适当消息而不是使函数失败。

    • MSG_NOERROR:这允许你接收比在msgsize参数中指定的尺寸更大的文本。它只是截断文本并接收它。如果此flag未设置,在接收较大的文本时,函数将不会接收它并使函数失败。

如果函数执行成功,则函数返回实际放置到由msgstruc指向的结构体文本字段中的字节数。在失败的情况下,函数返回-1的值。

msgsnd()

这用于向队列发送或投递消息。以下是它的语法:

 int msgsnd ( int msqid, struct msgbuf *msgstruc, int msgsize, int flag );

在这里,我们必须解决以下问题:

  • msqid:表示我们想要发送的消息的队列标识符。队列标识符通常通过调用msgget函数获取。

  • msgstruc:这是一个指向用户定义结构的指针。它是包含我们想要发送到队列的消息的mesg成员。

  • msgsize:表示消息的字节数。

  • flag:确定对消息采取的操作。如果flag值设置为IPC_NOWAIT并且消息队列已满,则消息不会被写入队列,控制权将返回到调用进程。但如果flag未设置且消息队列已满,则调用进程将挂起,直到队列中有空间可用。通常,flag的值设置为0

如果执行成功,函数返回0,否则返回-1

我们现在将开始这个菜谱的第一部分:将消息写入队列。

将消息写入消息队列

在这个菜谱的这一部分,我们将学习服务器如何将所需消息写入消息队列。

如何做到这一点...

  1. 通过调用ftok函数生成一个 IPC 键。在创建 IPC 键时提供文件名和 ID。

  2. 调用msgget函数创建一个新的消息队列。消息队列与在第 1 步中创建的 IPC 键相关联。

  3. 定义一个包含两个成员mtypemesg的结构体。将mtype成员的值设置为 1。

  4. 输入要添加到消息队列的消息。输入的字符串被分配给我们在第 3 步中定义的结构体的mesg成员。

  5. 调用msgsnd函数将输入的消息发送到消息队列。

将消息写入消息队列的messageqsend.c程序如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MSGSIZE     255

struct msgstruc {
    long mtype;
    char mesg[MSGSIZE];
};

int main()
{
    int msqid, msglen;
    key_t key;
    struct msgstruc msgbuf;
    system("touch messagefile");
    if ((key = ftok("messagefile", 'a')) == -1) {
        perror("ftok");
        exit(1);
    } 
    if ((msqid = msgget(key, 0666 | IPC_CREAT)) == -1) {
        perror("msgget");
        exit(1);
    }
    msgbuf.mtype = 1;
    printf("Enter a message to add to message queue : ");
    scanf("%s",msgbuf.mesg);
    msglen = strlen(msgbuf.mesg);
    if (msgsnd(msqid, &msgbuf, msglen, IPC_NOWAIT) < 0)
        perror("msgsnd");
    printf("The message sent is %s\n", msgbuf.mesg);
    return 0;
}

让我们看看幕后。

它是如何工作的...

我们将首先通过调用 ftok 函数生成一个 IPC 键。在创建 IPC 键时提供的文件名和 ID 分别为 messagefilea。生成的键被分配给键变量。之后,我们将调用 msgget 函数创建一个新的消息队列。该消息队列与使用 ftok 函数创建的 IPC 键相关联。

接下来,我们将定义一个名为 msgstruc 的结构体,包含两个成员,mtypemesgmtype 成员有助于确定从消息队列发送或接收的消息的序列号。mesg 成员包含要读取或写入消息队列的消息。我们将定义一个名为 msgbuf 的变量,其类型为 msgstruc 结构体。mtype 成员的值被设置为 1

您将被提示输入要添加到消息队列的消息。您输入的字符串被分配给 msgbuf 结构体的 mesg 成员。调用 msgsnd 函数将您输入的消息发送到消息队列。一旦消息写入消息队列,屏幕上会显示一条文本消息作为确认。

现在,让我们继续这个菜谱的另一个部分。

从消息队列中读取消息

在本部分菜谱中,我们将学习如何读取写入消息队列的消息并将其显示在屏幕上。

如何做到这一点…

  1. 调用 ftok 函数生成一个 IPC 键。在创建 IPC 键时提供的文件名和 ID。这些必须与在消息队列中写入消息时生成键时应用的相同。

  2. 调用 msgget 函数访问与 IPC 键关联的消息队列。与该键关联的消息队列已经包含了我们通过上一个程序写入的消息。

  3. 定义一个包含两个成员的结构体,mtypemesg

  4. 调用 msgrcv 函数从关联的消息队列中读取消息。在步骤 3 中定义的结构体被传递给此函数。

  5. 然后将读取的消息显示在屏幕上。

读取消息队列中的消息的 messageqrecv.c 程序如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#define MSGSIZE     255

struct msgstruc {
    long mtype;
    char mesg[MSGSIZE];
};

int main()
{
    int msqid;
    key_t key;
    struct msgstruc rcvbuffer;

    if ((key = ftok("messagefile", 'a')) == -1) {
        perror("ftok");
        exit(1);
    }
    if ((msqid = msgget(key, 0666)) < 0)
    {
        perror("msgget");
        exit(1);
    }
    if (msgrcv(msqid, &rcvbuffer, MSGSIZE, 1, 0) < 0)
    {
        perror("msgrcv");
        exit(1);
    }
    printf("The message received is %s\n", rcvbuffer.mesg);
    return 0;
}

让我们看看幕后发生了什么。

它是如何工作的...

首先,我们将调用 ftok 函数生成一个 IPC 键。在创建 IPC 键时提供的文件名和 ID 分别为 messagefilea。这些文件名和 ID 必须与生成消息队列中写入消息的键时应用的相同。生成的键被分配给键变量。

此后,我们将再次调用 msgget 函数来访问与 IPC 键关联的消息队列。访问的消息队列的标识符被分配给 msqid 变量。与该键关联的消息队列已经包含了我们之前程序中写入的消息。

然后,我们将通过名为 msgstruc 的结构定义两个成员,mtypemesgmtype 成员用于确定从消息队列读取的消息的序列号。mesg 成员将用于存储从消息队列读取的消息。然后我们将定义一个名为 rcvbuffer 的变量,其类型为 msgstruc 结构。我们将调用 msgrcv 函数从相关的消息队列读取消息。

消息标识符 msqid 被传递给函数,以及 rcvbuffer – 该结构体的 mesg 成员将存储读取的消息。在 msgrcv 函数成功执行后,rcvbuffermesg 成员将显示在屏幕上,包含来自消息队列的消息。

让我们使用 GCC 编译 messageqsend.c 程序,如下所示:

$ gcc messageqsend.c -o messageqsend

如果你没有收到错误或警告,这意味着 messageqsend.c 程序已编译成可执行文件,messageqsend.exe。让我们运行这个可执行文件:

$ ./messageqsend
Enter a message to add to message queue : GoodBye
The message sent is GoodBye

现在,按 Alt + F2 打开第二个终端屏幕。在这个屏幕上,你可以编译并运行从消息队列读取消息的脚本。

让我们使用 GCC 编译 messageqrecv.c 程序,如下所示:

$ gcc messageqrecv.c -o messageqrecv

如果你没有收到错误或警告,这意味着 messageqrecv.c 程序已编译成可执行文件,messageqrecv.exe。让我们运行这个可执行文件:

$ ./messageqrecv
The message received is GoodBye

哇!我们已经成功使用消息队列从一个进程传递消息到另一个进程。让我们继续下一个菜谱!

使用共享内存进行进程间通信

在这个菜谱中,我们将学习如何使用共享内存建立两个进程之间的通信。这个菜谱分为以下几部分:

  • 将消息写入共享内存

  • 从共享内存读取消息

我们将从第一个开始,也就是 将消息写入共享内存。我们在上一个菜谱中学到的函数也适用于这里。

将消息写入共享内存

在这个菜谱的这一部分,我们将学习如何将消息写入共享内存。

如何做到这一点…

  1. 通过提供一个文件名和 ID 调用 ftok 函数生成一个 IPC 键。

  2. 调用 shmget 函数分配一个与步骤 1 中生成的键关联的共享内存段。

  3. 为所需的内存段指定的尺寸是 1024。创建一个新的内存段,具有读写权限。

  4. 将共享内存段附加到系统中的第一个可用地址。

  5. 输入一个字符串,然后将其分配给共享内存段。

  6. 将附加的内存段从地址空间中分离。

将数据写入共享内存的 writememory.c 程序如下:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *str;
    int shmid;

    key_t key = ftok("sharedmem",'a');
    if ((shmid = shmget(key, 1024,0666|IPC_CREAT)) < 0) {
        perror("shmget");
        exit(1);
    }
    if ((str = shmat(shmid, NULL, 0)) == (char *) -1) {
        perror("shmat");
        exit(1);
    }
    printf("Enter the string to be written in memory : ");
    gets(str);
    printf("String written in memory: %s\n",str);
    shmdt(str);
    return 0;
}

让我们深入了解。

它是如何工作的...

通过调用 ftok 函数,我们生成一个 IPC 键,文件名为 sharedmem(你可以更改此名称)和 ID 为 a。生成的键被分配给键变量。之后,调用 shmget 函数来分配一个与使用 ftok 函数生成的提供的键相关联的共享内存段。

为所需内存段指定的尺寸是 1024。创建一个新的具有读写权限的内存段,并将共享内存标识符分配给 shmid 变量。然后,将共享内存段连接到系统中的第一个可用地址。

一旦内存段连接到地址空间,段的开头地址就被分配给 str 变量。你将被要求输入一个字符串。你输入的字符串将通过 str 变量分配给共享内存段。最后,连接的内存段从地址空间中分离。

让我们继续本配方的下一部分,从共享内存中读取消息

从共享内存中读取消息

在本部分的配方中,我们将学习如何从共享内存中读取写入的消息并在屏幕上显示。

如何操作...

  1. 调用 ftok 函数来生成一个 IPC 键。提供的文件名和 ID 应与程序中写入共享内存的内容相同。

  2. 调用 shmget 函数来分配一个共享内存段。分配的内存段指定的尺寸是 1024,并与步骤 1 中生成的 IPC 键相关联。创建具有读写权限的内存段。

  3. 将共享内存段连接到系统中的第一个可用地址。

  4. 从共享内存段读取的内容在屏幕上显示。

  5. 连接的内存段从地址空间中分离。

  6. 从系统中删除共享内存标识符,然后销毁共享内存段。

读取共享内存数据的 readmemory.c 程序如下:

#include <stdio.h> 
#include <sys/ipc.h> 
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int shmid;
    char * str;
    key_t key = ftok("sharedmem",'a');
    if ((shmid = shmget(key, 1024,0666|IPC_CREAT)) < 0) {
        perror("shmget");
        exit(1);
    }
    if ((str = shmat(shmid, NULL, 0)) == (char *) -1) {
        perror("shmat");
        exit(1);
    }
    printf("Data read from memory: %s\n",str);
    shmdt(str);                
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

让我们看看幕后。

它是如何工作的...

我们将调用 ftok 函数来生成一个 IPC 键。用于生成键的文件名和 ID 分别为 sharedmem(任何名称)和 a。生成的键被分配给 key 变量。之后,我们将调用 shmget 函数来分配一个共享内存段。分配的内存段指定的尺寸是 1024,与之前生成的 IPC 键相关联。

我们将创建一个新的具有读写权限的内存段,并将获取的共享内存标识符分配给 shmid 变量。然后,共享内存段被连接到系统中的第一个可用地址。这样做是为了我们可以通过先前的程序访问共享内存段中写入的文本。

因此,在内存段附加到地址空间之后,段的首地址被分配给 str 变量。现在,我们可以在当前程序中读取之前程序写入共享内存的内容。共享内存段的内容通过 str 字符串读取并在屏幕上显示。

之后,附加的内存段将从地址空间中分离。最后,共享内存标识符 shmid 从系统中移除,共享内存段被销毁。

让我们使用 GCC 编译 writememory.c 程序,如下所示:

$ gcc writememory.c -o writememory

如果你没有收到任何错误或警告,这意味着 writememory.c 程序已编译成可执行文件,名为 writememory.exe。现在,让我们运行这个可执行文件:

$ ./writememory
Enter the string to be written in memory : Today it might rain
String written in memory: Today it might rain

现在,按 Alt + F2 打开第二个终端窗口。在这个窗口中,让我们使用 GCC 编译 readmemory.c 程序,如下所示:

$ gcc readmemory.c -o readmemory

如果你没有收到任何错误或警告,这意味着 readmemory.c 程序已编译成可执行文件,名为 readmemory.exe。现在,让我们运行这个可执行文件:

$ ./readmemory
 Data read from memory: Today it might rain

哇!我们已经成功使用共享内存在不同进程之间进行了通信。

第九章:排序和搜索

如其名所示,搜索是定位一组元素中特定元素的过程。搜索可以大致分为以下两种类型:

  • 线性搜索:在列表中逐个元素顺序搜索以找到所需项。

  • 二分搜索:假设列表已经排序,将列表的中间值与要搜索的项进行比较,以确定需要考虑搜索项的列表哪一半。列表分割的过程会持续进行,直到找到项。

另一方面,排序是将某些元素按特定顺序排列的过程。顺序可以是升序、降序或另一个特定顺序。不仅可以对单个数字和字符串进行排序,还可以对记录进行排序。记录是根据每个记录独特的键进行排序的。这些是排序的两个主要类别:

  • 内部排序:所有要排序的元素都一起上传到主存储器

  • 外部排序:一些要排序的元素上传到主存储器,其余的保持在辅助存储器中,例如硬盘或 U 盘

为了能够进行有效的搜索,我们需要知道如何排序数据。排序是必要的,因为它使得搜索任务变得非常容易和快速。

在本章中,你将学习以下菜谱:

  • 使用二分搜索搜索项

  • 使用冒泡排序按升序排列数字

  • 使用插入排序按升序排列数字

  • 使用快速排序按升序排列数字

  • 使用堆排序按降序排列数字

让我们从第一个菜谱开始!

使用二分搜索搜索项

二分搜索使用分而治之的方法。要搜索的项与数组或文件中的中间项进行比较。这有助于确定数组的哪一半或文件可能包含要搜索的项。之后,将考虑的半个数组的中间值与要搜索的项进行比较,以确定数组的哪四分之一可能包含要搜索的项。这个过程会持续进行,直到找到要搜索的项,或者无法再对数组或文件进行分割,在这种情况下,可以理解为要搜索的项不在文件或数组中。

如何做到这一点...

考虑一个大小为len的元素数组arr。我们想要在这个数组中搜索一个数字numb。以下是使用二分搜索在arr数组中搜索numb的步骤:

  1. 初始化两个变量,lowerupper

  2. 计算数组的中间位置。

  3. 如果要搜索的值numb在位置arr[mid]找到,则显示Value found并退出(即跳转到步骤 8)。

  4. 如果你的搜索值大于数组的中间值,将搜索范围限制在数组的下半部分。因此,将数组的下限设置为数组的中间值。

  5. 如果你的搜索值小于数组的中间值,将搜索范围限制在数组的上半部分。因此,将数组的上限设置为数组的中间值。

  6. 只要upper>=lower,就重复步骤 3 到 5。

  7. 只有在找不到值的情况下,才会执行此步骤。然后显示Value not found并退出。

  8. 退出。

使用二分搜索技术在排序数组中搜索元素的程序如下:

//binarysearch.c

#include <stdio.h>

#define max 20
int binary_search(int[], int, int);

int main() {
  int len, found, numb, arr[max], i;
  printf("Enter the length of an array: ");
  scanf("%d", & len);
  printf("Enter %d values in sorted order \n", len);
  for (i = 0; i < len; i++)
    scanf("%d", & arr[i]);
  printf("Enter the value to search ");
  scanf("%d", & numb);
  found = binary_search(arr, numb, len);
  if (found == numb)
    printf("Value %d is found in the list\n", numb);
  else
    printf("Value %d is not found in the list \n", numb);
  return 0;
}

int binary_search(int arr[], int pnumb, int plen) {
  int lindex = 0, mid, uindex = plen - 1, nfound;
  while (uindex >= lindex) {
    mid = (uindex + lindex) / 2;
    if (pnumb == arr[mid]) {
      nfound = arr[mid];
      break;
    } else {
      if (pnumb > arr[mid])
        lindex = mid + 1;
      else
        uindex = mid - 1;
    }
  }
  return (nfound);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

让我们定义一个大小为 20 的宏max和一个大小为max的数组arr,即 20 个元素(你可以将max宏的值增加到任何更大的值)。接下来,我们将指定数组的长度。假设你输入的长度是 8,然后分配给len变量。当被提示时,输入指定的排序元素数量。你输入的排序元素将被分配给arr数组,如下所示:

图 9.1

然后,你将被提示输入你想要在排序数组中搜索的数字。假设你选择了 45;这个数字将被分配给numb变量。我们将调用binary_search函数,并将三个项目——包含要搜索的数字的arr数组、包含数字的numb变量以及数组的长度len——传递给函数。arrnumblen参数将分别分配给arrpnumbplen参数。

binary_search函数中,我们将初始化两个变量:lindex设置为0uindex设置为7,即等于数组的长度;这两个索引分别代表数组的下标和上标位置。因为数组是基于 0 的,所以数组的第八个元素将在索引位置 7。我们将设置一个while循环,只要uindex的值大于或等于lindex的值就执行。

要将搜索值与数组的中间值进行比较,我们首先计算中间值;将lindexuindex的值相加,然后除以 2。(0+7)/2的结果是 3。然后,将numb变量的值,即 45,与计算得出的位置arr[3]的值进行比较,即与 34(见图 9.2)进行比较:

图 9.2

因为 45 大于 34,所以我们必须继续在数组的下半部分进行搜索。然而,由于我们的列表是按升序排序的,我们现在可以集中搜索数组的下半部分。

现在,将lindex的值设置为mid+1,即等于 4。再次执行while循环,因为uindex,即 7,仍然大于lindex。我们现在将计算数组上半部分的中值:(4+7)/2 = 5。搜索值 45 将与arr[5]进行比较,即与 80 进行比较。因为 45 小于 80,我们将继续在数组的下半部分进行搜索,如下所示:

图 9.3

接下来,将uindex的值设置为mid-1,即等于 4。我们之前计算的lindex的值也是 4。由于 4 等于 4,我们将会再次执行while循环。数组的中值将被计算为(4+4)/2,即搜索值 45 将与arr[4]进行比较,而arr[4]的值是 60。

因为 45 < 60,所以uindex的值将被设置为mid-1,即等于 3。while循环将退出,因为我们的uindex (3)不再大于我们的lindex (4)binary_search函数将nfound变量返回到main函数。nfound变量包含一些垃圾值,然后在main函数中将这些值分配给found变量。在main函数中,foundnumb变量中的值将被比较。因为垃圾值不等于numb变量中的值 45,屏幕上将会显示消息Value 45 is not found in the list

假设你现在想要搜索值 15。lindexuindex的初始值将再次是 0 和 7。while循环将会执行,中值将被计算为(0+7)/2,这将得到 3。值 15 将与相应的位置arr[3]进行比较,即与 34 进行比较。值 15 小于 34,因此将考虑数组的上半部分以继续二分搜索,如图所示:

图 9.4

uindex变量的值设置为mid-1,即等于 2。因为uindex仍然大于lindex,即 2 >= 0,while循环将再次执行。再次,中值被计算为(0+2)/2,即 1。这意味着 15 将与arr[1]元素进行比较。

arr[1]位置上的值仅为 15;因此,在binary_search函数中将nfound变量设置为 15,并将nfound变量返回到main函数。在main函数中,nfound变量的值将被分配给found变量。因为foundnumb变量中的值相同,屏幕上将会显示消息Value 15 is found in the list

程序使用 GCC 编译,如下所示截图。因为没有错误出现在编译过程中,这意味着binarysearch.c程序已经成功编译成 EXE 文件,即binarysearch.exe文件。在执行可执行文件时,如果我们尝试搜索列表中不存在的值,我们会得到以下输出:

图片

图 9.5

如果我们再次运行可执行文件并输入数组中存在的数字,我们可能会得到以下输出:

图片

图 9.6

哇!我们已经成功使用二分搜索在有序数组中定位一个项目。现在让我们继续下一个菜谱!

使用冒泡排序按升序排列数字

在这个菜谱中,我们将学习如何使用冒泡排序技术按升序排列一些整数。在这个技术中,第一个元素与第二个元素进行比较,第二个元素与第三个元素进行比较,第三个元素与第四个元素进行比较,依此类推。

如何做到这一点...

考虑一个大小为len个元素的数组arr。我们想要按升序排列arr数组中的元素。以下是这样做的方法:

  1. 初始化一个变量,例如i,为len -2

  2. 重复步骤 35,直到i >=1。每次迭代后,i的值将减 1,即i=len-2len-3len-4,……,1

  3. 初始化另一个变量,j,为0

  4. 重复步骤 5j<=i。每次迭代后,j的值将增加,即j=12,……,i

  5. 如果arr[j] > arr[j+1],则交换这两个值。

  6. 退出搜索。

使用冒泡排序技术对整数数组元素进行排序的程序如下:

//bubblesort.c

#include <stdio.h>

#define max 20
int main() {
  int arr[max], temp, len, i, j;
  printf("How many values are there? ");
  scanf("%d", & len);
  printf("Enter %d values to sort\n", len);
  for (i = 0; i < len; i++)
    scanf("%d", & arr[i]);
  for (i = len - 2; i >= 1; i--) {
    for (j = 0; j <= i; j++) {
      if (arr[j] > arr[j + 1]) {
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  printf("The sorted array is:\n");
  for (i = 0; i < len; i++)
    printf("%d\n", arr[i]);
  return 0;
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们将首先定义一个值为 20 的宏max。您可以根据需要始终增加max的值。然后,我们将定义一个大小为max的数组arr,即大小为 20。您将被询问您想要排序多少个值。假设您想要排序七个元素,您输入的值将被分配给len变量。您将被提示输入要排序的值,然后这些值将被分配给arr数组。arr数组中要排序的七个值可能如下所示:

图片

图 9.7

现在,我们将运行两个嵌套的for循环:外层for循环将从len-2开始执行,即从值 5 到 1 按降序执行,内层for循环将执行从 0 到i的值。这意味着,在第一次迭代中,i的值将是 5,所以内层for j循环将执行从 0 到 5。在内层for循环中,arr的第一个值将与第二个值进行比较,第二个值与第三个值进行比较,依此类推:

图片

图 9.8

趋势是保持较低索引处的值小于较高索引处的值。如果第一个值大于第二个值,它们将交换位置;如果第一个值已经小于第二个值,那么接下来要考虑的是下一对值,即第二个和第三个值。同样,如果第二个值大于第三个值,它们也会交换位置;如果不是,那么接下来要比较的将是下一组值,即第三个和第四个值。这个过程将持续进行,直到最后一对值,即在我们的例子中是第六个和第七个值,进行比较。

整个第一次比较迭代过程如下所示:

图片

图 9.9

您可以看到,在第一次迭代后,最大的值已经冒泡到列表的底部。现在,外循环的值,即i的值将减 1,变为 4。因此,内循环中的j值将使for循环从值 0 运行到 4。这也意味着现在,第一个值将与第二个值进行比较,第二个值与第三个值进行比较,依此类推。最后,第五个值(即索引位置 4 的值)将与第六个值(即索引位置 5 的值)进行比较。索引位置 6 的最后元素不会进行比较,因为它已经处于正确的位置:

图片

图 9.10

再次,在第二次迭代后,外循环的值将减 1,变为 3。因此,内循环中的j值将使for循环从值 0 运行到 3。最后,第四个值(即索引位置 3 的值)将与第五个值进行比较。索引位置 5 和 6 的最后两个元素不会进行比较,因为它们都处于正确的位置:

图片

图 9.11

经过第三次迭代后,i的值将减 1,变为 2。因此,j的值将使for循环从值 0 运行到 2。索引位置 4、5 和 6 的最后三个元素不会进行比较,因为它们已经处于正确的位置:

图片

图 9.12

经过第四次迭代后,i的值再次减 1,变为 1。因此,内循环中的j值将使for循环从值 0 运行到 1。最后四个元素不会进行比较,因为它们已经处于最终位置:

图片

图 9.13

因此,经过五次迭代,我们已经成功将数组中的数字按升序排列。程序使用 GCC 编译,以下为编译语句:

gcc bubblesort.c -o bubblesort

由于编译过程中没有出现错误,这意味着bubblesort.c程序已成功编译成bubblesort.exe文件。在执行此文件时,它会要求我们指定要排序的数字数量。然后程序会提示我们输入要排序的数字。输入数字后,它们将以升序排列,如下面的截图所示:

图 9.14

哇!我们已经成功使用冒泡排序技术将数字按升序排列。

现在让我们继续下一个菜谱!

使用插入排序按升序排列数字

在这种排序技术中,数组的一个区域,可能是下或上部分,被认为是已排序的。从排序区域外选取一个元素,并在排序区域中搜索其适当的位置(以便在插入此元素后,该区域仍然保持排序),然后将该元素插入其中,因此得名插入排序。

如何做到的...

我们将创建一个名为InsertionSort的插入排序函数,如下所示调用,其中arr是要排序的数组,包含n个元素。

InsertionSort方法中遵循的步骤如下:

  1. 将一个变量,比如说i,初始化为1

  2. 重复步骤 2 到 5 共n-1次,即当i >= n-1时。在每次迭代后,i的值增加 1,i=1,2,3 .... n-1

  3. 初始化一个变量,j,为其值i

  4. 重复以下步骤 5 次,对于j=ij >=0。在每次迭代后,j的值减 1,即j=i, i-1, i-2, ....0

  5. 如果arr[j] < arr[j-1],则交换这两个值。

使用插入排序技术对整数数组元素进行排序的程序如下:

//insertionsort.c

#include <stdio.h>

#define max 20

int main() {
  int arr[max], i, j, temp, len;
  printf("How many numbers are there ? ");
  scanf("%d", & len);
  printf("Enter %d values to sort\n", len);
  for (i = 0; i < len; i++)
    scanf("%d", & arr[i]);
  for (i = 1; i < len; i++) {
    for (j = i; j > 0; j--) {
      if (arr[j] < arr[j - 1]) {
        temp = arr[j];
        arr[j] = arr[j - 1];
        arr[j - 1] = temp;
      }
    }
  }
  printf("\nThe ascending order of the values entered is:\n");
  for (i = 0; i < len; i++)
    printf("%d\n", arr[i]);
  return 0;
}

现在,让我们深入了解代码,以便更好地理解。

它是如何工作的...

假设我们需要排序的数字不超过 20;因此我们将定义一个大小为20的宏。你可以始终为这个宏分配任何值。接下来,我们将定义一个大小为max的整数数组arr。你将被提示输入要排序的数字数量。假设我们想要排序八个值;因此我们输入的8将被分配给变量len。然后,你将被要求输入需要排序的八个值。所以,假设我们输入了以下值,这些值被分配给arr数组:

图 9.15

在这种排序方法中,我们将借助嵌套循环,外循环i从 1 运行到 7,内循环ji的值开始运行,直到其值大于 0。因此,在嵌套循环的第一次迭代中,内循环只会执行一次,此时i的值为 1。arr[1]索引位置上的值将与arr[0]上的值进行比较。趋势是保持较低值在顶部,所以如果arr[1]上的值大于arr[0]上的值,这两个值的位将互换。因为 15 大于 9(在图 9.16的左侧),所以两个索引位置上的值将按照以下方式互换(在图 9.16的右侧):

图 9.16

在第一次迭代之后,i的值将增加至 2,内循环j将从 2 的值运行到 1,即内循环将执行两次:一次是j的值为 2 时,然后当j的值减少到 1 时。在内循环中,arr[2]上的值将与arr[1]上的值进行比较。此外,arr[1]上的值将与arr[0]上的值进行比较。如果arr[2] < arr[1],则将发生值的互换。同样,如果arr[1] < arr[0],则它们的值将互换。

arr[2]上的值为 10,小于arr[1]上的值,即 15;因此这些值将互换位置(见图 9.17)。在值互换后,我们发现arr[1]上的值大于arr[0]上的值。所以,现在不会发生互换。图 9.17显示了第二次迭代的步骤:

图 9.17

在第二次迭代之后,i的值将增加至 3,而j的值将从 3 开始运行,直到 1。因此,如果满足以下条件,将发生值的互换:

  • 如果arr[3] < arr[2]

  • 如果arr[2] < arr[1]

  • 如果arr[1] < arr[0]

您可以在图 9.18(a)中看到,arr[3],即 5,小于arr[2],即 15,因此它们的值将会互换。同样,arr[2]arr[1]以及arr[1]arr[0]的值也将互换(分别见图 9.18(b)(c))。图 9.18(d)显示了所有互换操作完成后的数组:

图 9.18

在第三次迭代之后,i的值将增加至 4,而j的值将从 4 开始运行,直到 1。如果满足以下条件,将发生值的互换:

  • 如果arr[4] < arr[3]

  • 如果arr[3] < arr[2]

  • 如果arr[2] < arr[1]

  • 如果arr[1] < arr[0]

您可以在图 9.19中看到,所有这些比较的主要趋势是将数组中的较低值移到较高值之上:

图 9.19

对于数组中的其余元素,将遵循相同的程序。

程序使用以下语句使用 GCC 编译:

gcc insertionsort.c -o insertionsort

由于编译过程中没有出现错误,这意味着insertionsort.c程序已成功编译成insertionsort.exe文件。在执行时,它将要求你指定要排序的数字数量。随后,程序将提示我们输入要排序的数字。输入数字后,它们将以升序显示,如下面的截图所示:

图 9.20

哇!我们已经成功使用插入排序将数字按升序排列。

现在让我们继续下一个菜谱!

使用快速排序按升序排列数字

快速排序是一种分而治之的算法。它根据枢轴将数组分割,其中枢轴是数组中的一个元素,以便所有小于枢轴的元素都放在枢轴之前,而所有大于枢轴的元素都放在枢轴之后。

因此,在枢轴的位置,数组被分割成两个子数组。在两个数组上重复寻找枢轴的过程。根据枢轴进一步将两个数组细分。

因此,快速排序是一个递归过程,将数组分割成子数组的递归过程会一直持续到子数组只有一个元素为止。

如何实现...

快速排序过程包括以下重要任务:

  • 寻找枢轴

  • 在枢轴位置分割数组

我们将使用两种方法:QuickSortFindingPivot

快速排序

此方法考虑一个数组或子数组。它调用方法来找到数组或子数组的枢轴,并根据枢轴分割数组或子数组。以下是其语法:

Quick Sort (arr,n)

在这里,arr是由n个元素组成的数组。

这就是使用此方法的方式:

  1. l=1u=n,其中lu分别代表数组的较低和较高索引位置。

  2. l推入stack1

  3. u推入stack2

  4. stack1stack2不为空时,重复步骤 5 至 10。

  5. 将数组stack1的较低索引位置弹出并放入变量s中,即s变为待排序数组的较低索引位置。

  6. stack2中弹出较高索引位置到变量e中,即e变量将获得数组的较高索引位置。

  7. 通过以下方式调用FindingPivot方法来找出枢轴:

pivot=FindingPivot(arr,s,e)

回想一下,枢轴点是指数组中的一个索引位置,其中小于枢轴的元素在它之前,而大于枢轴的元素在它之后。数组在枢轴点处被分割,然后对两个子数组分别递归地应用快速排序方法。

  1. 一旦确定了枢轴,将数组分为两个部分。一个数组将包含从 s(下标位置)到 pivot-1 的值,另一个数组将包含从 pivot+1e(上标位置)的元素。

  2. 对于数组的上半部分,将 s 推入 stack1,将 pivot-1 推入 stack2

  3. 对于数组的下半部分,将 pivot+1 推入 stack1,将 e 推入 stack2

FindingPivot

此方法用于找到数组或子数组的枢轴。以下是它的语法:

FindingPivot (arr,start,end)

在这里,arr 代表包含 n 个元素的数组,start 代表数组的起始索引位置,而 end 代表数组的结束索引位置。

这就是如何使用此方法:

  1. 重复 QuickSort 方法的第 2 步至第 8 步。

  2. start 变量的值存储在另一个变量中,例如 lower

  3. 从右索引位置开始,向左移动。最初,第一个元素是枢轴。趋势是将大于枢轴的元素保持在枢轴的右侧,将小于枢轴的元素保持在枢轴的左侧。

  4. 如果 lower=end,这意味着我们找到了枢轴。枢轴等于 lower 的值。将 lower 返回为枢轴元素的索引位置。

  5. 如果 arr[lower] > arr[end],则交换这两个值的顺序。现在,从左到右比较每个值与枢轴,直到我们得到一个小于枢轴值的值。

  6. arr[start] <= arr[lower]lower != start 时,重复:

start=start+1
  1. 如果 lower=start,则枢轴是 lower。将 lower 返回为枢轴元素的索引位置。

  2. 如果 arr[start] > arr[lower],则交换这两个值的顺序。

使用快速排序技术对整数数组元素进行排序的程序如下:

//quick sort.c

# include<stdio.h>
# define stacksize 10
#define arrsize 20
int top1 = -1, top2 = -1;
int stack1[stacksize];
int stack2[stacksize];
int arr[arrsize];

int quick(int, int);
void pushstk1(int);
void pushstk2(int);
int popstk1();
int popstk2();

int main() {
  int sindex, eindex, lindex, uindex, k, pivot, i, len;
  printf("How many numerical to sort? ");
  scanf("%d", & len);
  printf("Enter %d numerical:\n", len);
  for (i = 0; i <= len - 1; i++)
    scanf("%d", & arr[i]);
  lindex = 0;
  uindex = len - 1;
  pushstk1(lindex);
  pushstk2(uindex);
  while (top1 != -1) {
    sindex = popstk1();
    eindex = popstk2();
    pivot = quick(sindex, eindex);
    if (sindex < pivot - 1) {
      pushstk1(sindex);
      pushstk2(pivot - 1);
    }
    if (pivot + 1 < eindex) {
      pushstk1(pivot + 1);
      pushstk2(eindex);
    }
  }
  printf("\nAscending order using Quick Sort is:\n");
  for (i = 0; i <= len - 1; i++)
    printf("%d\n", arr[i]);
  return 0;
}

int quick(int si, int ei) {
  int li, temp;
  li = si;
  while (1) {
    while (arr[ei] >= arr[li] && li != ei)
      ei--;
    if (li == ei) return (li);
    if (arr[li] > arr[ei]) {
      temp = arr[li];
      arr[li] = arr[ei];
      arr[ei] = temp;
      li = ei;
    }
    while (arr[si] <= arr[li] && li != si)
      si++;
    if (li == si) return (li);
    if (arr[si] > arr[li]) {
      temp = arr[si];
      arr[si] = arr[li];
      arr[li] = temp;
      li = si;
    }
  }
  return 0;
}
void pushstk1(int s) {
  top1++;
  stack1[top1] = s;
}
void pushstk2(int e) {
  top2++;
  stack2[top2] = e;
}
int popstk1() {
  return (stack1[top1--]);
}
int popstk2() {
  return (stack2[top2--]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

您将被要求指定需要排序的数字数量。假设我们想要排序 8 个数字;用户输入的值 8 将被分配给 len 变量。执行一个 for 循环,使我们能够输入要排序的数字。我们输入的值将按照 图 9.21 所示分配给 arr 数组。

两个变量,lindexuindex,被初始化以表示数组所需的首个和最后一个索引,分别是 0 和 7。lindexuindex 的位置应该保留数组中的最小和最大值。lindexuindex 的值,即 0 和 7,将被推入栈中。在 pushstk1 函数中,顶部索引的值(默认为 -1)增加至 0,并将 lindex 的值赋给 stack1 数组的 [0] 索引位置。同样,在 pushstk2 函数中,top2 索引的值也增加至 0,并将 uindex 的值赋给 stack2 数组的 [0] 位置。

设置一个 while 循环,直到 top1 的值不等于 1。这意味着,直到 stack1 为空,程序将继续执行。在 while 循环内,stack1stack2 中推入的值将被弹出并分配给两个变量 sindexeindex,分别。这些变量代表我们想要使用快速排序对数组或数组的一部分进行排序的起始和结束索引位置。

stack1stack2 分别包含 0 和 7 的值,这些值被弹出并分别分配给 sindexeindex。调用 quick 函数,并将 sindexeindex 的值传递给参数。在 quick 函数中,sindexeindex 参数的值分别分配给 siei 这两个参数。

在 quick 函数内,si 的值,即 0,被分配给另一个变量 li。执行一个无限循环的 while 循环。在 while 循环内,设置另一个 while 循环,将使 ei 向左移动,即,它将使 ei 的值递减,直到 arr[ei] 位置处的元素大于 arr[li] 位置处的元素:

图片

图 9.21

因为 arr[ei] < arr[si],将它们的值进行交换(见 图 9.22(a))。在 arr[ei]arr[si] 交换值后,arr 数组将如图 图 9.22(b) 所示:

图片

图 9.22

交换值后,ei 的索引位置号,即 7,将被分配给 li。然后设置另一个 while 循环,在 arr[si] 小于 li 时执行,其中 li 代表当前的 ei 索引;在 while 循环内,si 索引指针的位置增加。也就是说,si 索引指针向右移动到 arr[si] < arr[li]

图片

图 9.23

现在,以下事情将会发生:

  • 因为 arr[si] < arr[ei](即 4 < 6),si 将向右移动一个位置到 arr[1]

  • 因为 arr[si] < arr[ei](即现在 3 < 6),si 将再次向右移动一个位置到 arr[2]

  • 因为 arr[si] < arr[ei](即现在 0 < 6),si 将再次向右移动一个位置到 arr[3]

  • 因为 arr[si]  < arr[ei](即现在 2 < 6),si 将再次向右移动一个位置到 arr[4]

  • 因为 arr[si]  > arr[ei](即现在 7 > 6),将它们的值进行交换(见 图 9.24):

图片

图 9.24

arr[ei]arr[si] 交换值后,arr[si] 的位置号,即 4,将被分配给 li。过程重复;也就是说,再次设置一个 while 循环来执行,直到 arr[ei] > arr[si]。在 while 循环内,ei 的位置递减,或者它向左移动:

图片

图 9.25

在比较arr[ei]arr[si]时,我们会发现arr[ei] > arr[si](7 > 6),因此ei将递减到值 6(参见图 9.26(a))。再次,因为arr[ei] < arr[si](1 < 6),这些索引位置的值将进行交换(参见图 9.26(b))。现在ei的位置号 6 将被分配给变量li

设置另一个 while 循环,在arr[si] < arr[ei](记住ei的位置号被分配给li)的情况下执行。在这个 while 循环中将会发生以下事情:

  • 因为arr[si] < arr[ei](即 1 < 6),si将向右移动到arr[5]

  • 因为仍然arr[si] < arr[ei](即 5 < 6),si将向右移动到arr[6]

  • 因为现在eisi的位置相同,快速函数将终止并返回数字 6 到main函数(参见图 9.26(c))。因此,数字 6 将成为arr数组的枢轴。

图片

图 9.26

执行了两个if语句,数组被分成两部分:第一部分从arr[0]arr[5],另一部分从arr[7]arr[7],即一个单独的元素。数组的两部分的第一和最后一个索引值被推入栈中。

数组的第二部分的第一和最后一个索引位置,即 7,将被推入stack1stack2。数组的第一部分的第一和最后一个索引位置,即 0 和 5,也将分别推入stack1stack2(参见图 9.27)。

图片

图 9.27

完整的快速排序技术在数组的两个半部分上应用。再次,这两个半部分将被进一步分割成两个更小的部分,然后再次在这些两个部分上应用快速排序技术,以此类推。

外层 while 循环重复执行,并将popstk1()popstk2()函数调用来弹出stack1stack2数组中的值。top1top2索引的值都是 1,所以stack1[1]stack2[1]索引位置的值被取出并分别分配给两个变量,sindexeindex。再次调用quick()函数,并将两个变量sindexeindex传递给它。在quick()函数中,sindexeindex参数的值分别被分配给siei

quick() 函数内,si 变量的值,即 0,被分配给另一个变量 li。执行一个无限循环的 while 循环。在 while 循环内,设置另一个 while 循环,将使 ei 索引位置向左移动,即它将使 ei 索引变量的值减少,直到 arr[ei] 位置的元素大于 arr[si] 位置的元素(参见 图 9.28(a))。因为 arr[ei] > arr[si],所以 ei 变量的值将减少到 4(参见 图 9.28(b))。现在,我们发现 arr[ei],即 1,小于 arr[si],即 4,所以它们的值将交换。在交换 arr[ei]arr[si] 索引位置的值后,arr 数组将如 图 9.28(c) 所示。

交换值后,ei 变量的值被分配给 li 变量,即 4 被分配给 li 变量。设置另一个 while 循环,在 arr[si] 元素小于 arr[li] 时执行,其中 li 代表当前的 si 索引;在 while 循环内,si 索引指针的值增加。以下事情将会发生:

  • 因为 arr[si],即 1,小于 arr[ei],即 4,所以 si 将增加到值为 1。

  • 因为 arr[si],即 3,小于 arr[ei],即 4,所以 si 将增加到值为 2。

  • 因为 arr[si],即 0,小于 arr[ei],即 4,所以 si 将增加到值为 3。

  • 因为 arr[si],即 2,小于 arr[ei],即 6,所以 si 将增加到值为 4。

因为 eisi 变量的值已经相同,quick() 函数将终止,将值 4 返回到 main 函数(参见 图 9.28(d)):

图 9.28

在返回 main 函数时,执行了两个 if 语句,并将数组分成两部分:第一部分从 arr[0]arr[3] 索引位置,另一部分将从 arr[5]arr[5] 索引位置,即一个单独的元素。数组的两部分起始和结束索引值被推送到栈中。数组的第二部分的起始和结束索引位置(即 5 和 5)分别推送到 stack1stack2。同样,数组的第一个部分的起始和结束索引位置(即 0 和 3)分别推送到 stack1stack2(参见 图 9.29)。

图 9.29

整个快速排序技术被应用于数组的所有分区,直到栈为空。也就是说,外层 while 循环重复,popstk1()popstk2() 函数将被调用以弹出 stack1stack2 数组中的值。再次调用 quick() 函数,并将从栈中弹出的两个变量 sindexeindex 传递给它。这个过程会一直持续到整个数组被排序。

程序使用以下语句使用 GCC 编译:

gcc quick sort.c -o quick sort

因为编译过程中没有出现错误,这意味着 quick sort.c 程序已成功编译成 quick sort.exe 文件。在执行文件时,它会要求您指定要排序的数字数量。随后,程序将提示您输入要排序的数字。输入数字后,它们将按升序排列,如下面的截图所示:

图片

图 9.30

哇!我们已经成功使用快速排序将数组中的数字排列好了。现在让我们继续下一个菜谱!

使用堆排序对数字进行降序排列

在这个菜谱中,我们将学习如何使用堆排序技术将一些整数按降序排列。

如何做到这一点...

堆排序方法被分为以下两个任务:

  1. 创建最大堆

  2. 删除最大堆

让我们从创建最大堆开始。

创建最大堆

创建最大堆的以下步骤:

  1. 用户被要求输入一个数字。该数字用于创建一个堆。用户输入的数字被分配到数组堆的索引位置 x,其中 x 的初始值为 0,并在每次插入后递增。

  2. 新插入的数字与其父节点的元素进行比较。因为我们正在使用最大堆,所以需要遵循一个规则:父节点的值应该始终大于其子节点。父节点的位置通过公式 parent=(x-1)/2 计算,其中 x 代表新节点插入的索引位置。

  3. 检查新节点的值是否大于其父节点的值。通过一个额外的变量交换 heap[parent]heap[x] 的值。

  4. 递归检查父节点的父节点的值,以查看最大堆的性质是否得到保持。

一旦堆被创建,删除最大堆的第二项任务将开始。每次从最大堆中删除一个节点时,删除的节点将被保存在另一个数组中,例如 arr,该数组将包含排序后的元素。删除最大堆的任务将重复执行,直到最大堆中存在多少个元素。

删除最大堆

三个变量 leftchildrightchildroot 的初始化如下:

leftchild=0
rightchild=0
root=1

删除最大堆的以下步骤:

  1. 根节点处的元素临时分配给 n 变量。

  2. 堆的最后一个元素放置在根节点。

  3. 如果最后一个索引位置的值为 1 或 2,即堆只剩下 1 或 2 个元素,则使用n变量返回调用者。

  4. 由于最后一个元素放置在根节点,减少堆的大小 1。

  5. 为了保持最大堆的性质,在rightchild <= last的情况下重复执行步骤 69。回想一下,最大堆的性质是父节点的值应该始终大于其子节点。

  6. 计算左子节点leftchild和右子节点rightchild的位置。

  7. 如果heap[root] > heap[leftchild] && heap[root] > heap[rightchild],则返回n并退出。

  8. 如果左子节点的值大于右子节点的值,则交换根节点和左子节点的值。根节点将下降到左子节点以检查最大堆的性质是否得到保持。

  9. 如果右子节点的值大于左子节点的值,则交换根节点和右子节点的值。根节点将下降到右子节点以检查最大堆的性质是否得到保持。

  10. 当最大堆的所有元素都处理完毕时,这意味着arr数组将包含所有已排序的元素。因此,最后一步是打印arr数组,其中包含已排序的元素。

使用堆排序技术对整数数组元素进行排序的程序如下:

//heapsort.c

# include <stdio.h>
#define max 20
int heap[max], len;

void insheap(int h);
int delsheap(int j);

int main() {
  int arr[max], numb, i, j;
  printf("How many elements to sort? ");
  scanf("%d", & len);
  printf("Enter %d values \n", len);
  for (i = 0; i < len; i++) {
    scanf("%d", & numb);
    insheap(numb);
  }
  j = len - 1;
  for (i = 0; i < len; i++) {
    arr[i] = delsheap(j);
    j--;
  }
  printf("\nThe Descending order is: \n");
  for (i = 0; i < len; i++)
    printf("%d\n", arr[i]);
  return 0;
}

void insheap(int value) {
  static int x;
  int par, cur, temp;
  if (x == 0) {
    heap[x] = value;\
    x++;
  } else {
    heap[x] = value;
    par = (x - 1) / 2;
    cur = x;
    do {
      if (heap[cur] > heap[par]) {
        temp = heap[cur];
        heap[cur] = heap[par];
        heap[par] = temp;
        cur = par;
        par = (cur - 1) / 2;
      } else break;
    } while (cur != 0);
    x++;
  }
}

int delsheap(int j) {
  int loc, n = 0, pos, lc = 0, rc = 0, temp = 0;
  loc = j;
  pos = 0;
  n = heap[pos];
  heap[pos] = heap[loc];
  if (loc == 0 || loc == 1) return (n);
  loc--;
  lc = 2 * pos + 1;
  rc = 2 * pos + 2;
  while (rc <= loc) {
    if ((heap[pos] > heap[lc] && heap[pos] > heap[rc]))
      return (n);
    else {
      if (heap[lc] > heap[rc]) {
        temp = heap[lc];
        heap[lc] = heap[pos];
        heap[pos] = temp;
        pos = lc;
      } else {
        temp = heap[rc];
        heap[rc] = heap[pos];
        heap[pos] = temp;
        pos = rc;
      }
      lc = 2 * pos + 1;
      rc = 2 * pos + 2;
    }
  }
  if (lc == loc) {
    if (heap[pos] < heap[lc]) {
      temp = heap[pos];
      heap[pos] = heap[lc];
      heap[lc] = temp;
      pos = lc;
    }
  }
  return (n);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

堆是一个完全二叉树,可以是最大堆或最小堆。最大堆具有这样的性质:任何节点的键值必须大于或等于其子节点的键值。在最小堆中,任何节点的键值必须小于或等于其子节点的值。

在这个菜谱中,我们将学习如何创建以下整数列表的最大堆:

5 2 9 3 1 4 6

在这个堆排序方法中,二叉树以数组的形式构建。在堆排序中,数组中的值一个接一个地添加,保持最大堆性质为真(即任何节点的键值应该大于或等于其子节点)。在添加数组元素时,我们使用(x-1)/2跟踪父节点的键值,其中x是要找到父节点的元素。如果插入堆中的元素大于其父节点的键值,则进行交换。例如,假设第一个键值输入是5(它被认为是根);它被存储为数组的第一个元素,即heap[0]

图 9.31

然后将 2 添加为其左子节点。第一个子节点总是添加到左边。当输入另一个值时,它被输入到heap[1]的位置。插入后,使用(x-1)/2计算其父节点位置,其中x是 1。因此,父节点是位置 0。所以,heap[1]与其父元素heap[0]进行比较。如果父元素heap[0]的关键元素大于heap[1],则继续进行;否则,交换它们的关键值。在我们的例子中,第二个元素是 2,所以不需要交换:

图 9.32

现在,我们进入第三个元素。第三个元素是 9,它被添加为节点 5 的右子节点(见图 9.33(a))。在数组中,它被存储在heap[2]的位置。再次,使用(x-1)/2计算其父元素的位置,结果再次是 0。为了保持最大堆的性质(父节点的值应该大于或等于其子节点),我们比较heap[0]heap[2]元素的关键值。因为heap[0]小于heap[2],它违反了最大堆的性质。因此,heap[0]heap[2]的关键值将进行交换,如图 9.33(b)所示:

图 9.33

然后 3 被添加为节点 2 的左子节点,如图 9.34(a)所示。在数组中,新值被插入到heap[3]的索引位置。再次,使用公式(x-1)/2计算其父元素的位置,其中x代表新值插入的索引位置,即 3。父元素的位置计算为 1。根据最大堆的性质,heap[1]必须大于或等于heap[3]。但是因为heap[1]小于heap[3],它违反了最大堆的性质。因此,heap[1]heap[3]的关键值将进行交换,如图 9.34(b)所示:

图 9.34

现在,1 被添加为节点 3 的右子节点。在数组中,新值被插入到heap[4]的索引位置。因为最大堆的性质仍然保持,不需要交换:

图 9.35

下一个值是 4,它被添加为节点 5 的左子节点。在数组中,新值被插入到heap[5]的索引位置。再次,最大堆的性质得到保持,因此不需要交换:

图 9.36

接下来,将 6 添加为节点 5 的右子节点(见图 9.37(a))。在数组中,它被插入到heap[6]的索引位置。再次,使用公式(x-1)/2计算父元素位置。父元素位置计算为 2。根据最大堆的性质,heap[2]必须大于或等于heap[6]。但是因为heap[2]小于heap[6],它违反了最大堆的性质;因此,heap[2]heap[6]的键值将被交换,如图图 9.37(b)所示:

图片

图 9.37

一旦构建了最大堆,我们就通过重复以下三个步骤进行堆排序:

  1. 移除其根元素(并将其存储在排序数组中)

  2. 将树的根元素(数组)替换为最后一个节点值,并移除最后一个节点(减少数组的大小)

  3. 重新整理键值以保持堆的性质

在下面的图 9.38(a)中,你可以看到根元素,即 9,被删除并存储在另一个名为arr的数组中。arr数组将包含排序后的元素。根元素被替换为树的最后一个元素。树的最后一个元素是 5,因此它从heap[6]索引位置移除并分配给根,即heap[0]。现在,堆的性质不再成立。因此,节点元素 5 和 6 的值被交换(见图 9.38(b)):

图片

图 9.38

现在再次重复这个过程,移除根节点的键元素,并用最后一个节点的值替换它,然后重新整理堆。也就是说,移除根节点元素 6 并将其分配给排序数组arr。然后,根节点被替换为树的最后一个元素,即 4(见图 9.39(a))。将值 4 放在根节点后,堆的性质不再成立。因此,为了保持堆的性质,将值 4 向下移动,即节点元素 4 和 5 的值被交换,如图9.39(b)所示):

图片

图 9.39

重复这些步骤以按降序对数组进行排序,如下所示:

图片

图 9.40

使用以下语句使用 GCC 编译程序:

gcc heapsort.c -o heapsort

因为编译时没有出现错误,这意味着heapsort.c程序已成功编译成heapsort.exe文件。在执行文件时,它会要求我们指定要排序的数字数量。随后,程序将提示我们输入要排序的数字。输入数字后,它们将按降序排列,如下面的屏幕截图所示:

图片

图 9.41

哇!我们已经成功使用堆排序将数字按降序排列。

参见

要了解更多如选择、归并、希尔和基数排序等排序方法,请访问此链接上的附录 Agithub.com/PacktPublishing/Practical-C-Programming/blob/master/Appendix%20A.pdf.

第十章:处理图

图表以图形格式展示信息。在图表中,某些信息被绘制出来,然后通过线条或条形连接这些绘制点。每个绘制点被称为顶点(复数形式为 vertices),连接它们的线条被称为。图表能够以易于理解的方式展示大量数据。因此,在比较大量或巨大的数据时,图表通常更受欢迎。

图可以在多个应用中使用,包括显示某种传输路线或数据包的流动。图也可以用来表示两个城市或站点之间的一种连接,其中站点可以用顶点表示,路线可以用边表示。在社交媒体上,甚至可以将朋友以图的形式连接起来,其中每个人可以表示为一个顶点,他们之间的边确保他们是朋友。同样,图可以用来表示不同的网络。

在本章中,我们将学习如何使用不同的数据结构来表示图。我们还将学习遍历图并从图中创建最小生成树。为了能够做到这一点,我们将查看以下食谱:

  • 创建有向图的邻接矩阵表示

  • 创建无向图的邻接矩阵表示

  • 创建有向图的邻接表表示

  • 执行图的广度优先遍历

  • 执行图的深度优先遍历

  • 使用普里姆算法创建最小生成树

  • 使用克鲁斯卡尔算法创建最小生成树

在我们开始食谱之前,让我们快速介绍两种主要的图类型。

图表类型

根据方向,图可以分为两种类型:有向和无向。让我们简要回顾一下这两种类型。

有向图

在有向图中,边清楚地显示了从一个顶点到另一个顶点的方向。有向图中的边通常表示为(v1,v2),这意味着边是从顶点 v1 指向顶点 v2。换句话说,(v1,v2)对表示 v1 是起始顶点,v2 是结束顶点。有向图在现实世界的应用中非常有用,并被用于万维网WWW)、谷歌的 PageRank 算法等。考虑以下有向图:

图 10.1

在这里,你可以看到顶点ab之间的边。因为边是从顶点a指向b,所以顶点a被认为是起始顶点,顶点b被认为是结束顶点。这条边可以表示为(ab)。同样,从顶点a到顶点c也存在一条边,它可以表示为(ac)。因此,我们可以说前面的图有以下顶点集:

(V) - { a,b,c,d,e}

此外,该图有以下一组边:

(E) - {(a,b), (a,c), (c,d), (c,e), (d,b), (d,e), (e,a), (e,b)}

无向图

无向图是一种在顶点之间存在边,但没有特定方向标识的图——也就是说,边的末端没有箭头。因此,我们无法知道哪个是起始顶点,哪个是结束顶点。无向图在现实世界的应用中非常广泛,例如 Facebook 和神经网络。

在无向图中,两个顶点ab之间的边意味着它们中的任何一个都可以是起始顶点或结束顶点。这样的边可以写成(a,b),即从ab,也可以写成(b,a),即从ba。以下图显示了无向图:

图片

图 10.2

因此,对于这个无向图,以下是一组顶点:

(V) - { a,b,c,d,e}

此外,该图将包含以下一组边:

(E) - {(a,b), (b,a), (a,c), (c,a), (a,e), (e,a), (b,e), (e,b), (b,d), (d,b), (c,d), (d,c), (c,e), (e,c)}

现在,让我们从这些方法开始。

创建有向图的邻接矩阵表示法

邻接矩阵是一个用于表示图的方阵。矩阵的行和列按照图中的顶点进行标记。因此,如果图顶点是12、...5,那么邻接矩阵的行和列将标记为12、...5。最初,矩阵用所有零(0)填充。然后,如果顶点ij之间存在边,则将mat[i][j]位置的 0 替换为 1(其中ij指的是顶点)。例如,如果从顶点2到顶点3存在边,那么在mat[2][3]索引位置,0 的值将被替换为1。简而言之,邻接矩阵的元素表示图中顶点对是否相邻。

考虑以下有向图:

图片

图 10.3

其邻接矩阵表示法如下:

5,5          1              2              3              4              5
-----------------------------------------------------------------------------
1            0              1              1              0              0
2            0              0              0              0              0
3            0              0              0              1              1
4            0              1              0              0              1
5            1              1              0              0              0

第一行和第一列代表顶点。如果两个顶点之间存在边,则它们各自行和列的交点处将有一个1值。它们之间没有边的情况将用0表示。邻接矩阵中非零元素的数量表示有向图中边的数量。

使用邻接矩阵表示法有以下两个缺点:

  • 这种表示法需要个元素来表示具有n个顶点的图。如果一个有向图有e条边,那么矩阵中的(n²-e)个元素将是零。因此,对于边数非常少的图,矩阵会变得非常稀疏。

  • 平行边不能由邻接矩阵表示。

在这个方法中,我们将学习如何制作有向图的邻接矩阵表示法。

如何做到这一点...

执行以下步骤以创建图的邻接矩阵表示法:

  1. 询问用户图中顶点的数量。

  2. 定义一个等于顶点数的正方形矩阵。

  3. 将矩阵的所有元素初始化为 0。

  4. 请用户输入边。对于用户输入的每条边(i,j),在mat[i][j]索引位置替换 0。

  5. 一旦所有边都输入完毕,显示邻接矩阵的所有元素。

创建图邻接矩阵表示的代码如下:

//adjmatdirect.c

#include <stdio.h>

#define max 10
int main() {
  static int edg[max][max], i, j, v1, v2, numb;
  printf("How many vertices are there? ");
  scanf("%d", & numb);
  printf("We assume that the vertices are numbered from : ");
  for (i = 1; i <= numb; i++) printf("%d ", i);
  printf("\nEnter the edges of the graph. Like 1 4 if there is an \n");
  printf("edge between vertex 1 and 4\. Enter 0 0 when over\n");
  for (i = 1; i <= numb * (numb - 1); i++) {
    /* The for loop will run for at most numb*(numb-1) times because, 
       the number of edges are at most numb*(numb-1) where numb is 
       the number of vertices */
    scanf("%d %d", & v1, & v2);
    if (v1 == 0 && v2 == 0) break;
    edg[v1][v2] = 1;
  }
  printf("\nThe adjacency matrix for the graph is \n");
  for (i = 1; i <= numb; i++) printf("\t%d", i);
  printf("\n-----------------------------------------------------\n");
  for (i = 1; i <= numb; i++) {
    printf("%d |\t", i);
    for (j = 1; j <= numb; j++) {
      printf("%d\t", edg[i][j]);
    }
    printf("\n");
  }
  return 0;
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

假设用户将在本程序中指定的有向图不会超过 10 个顶点,定义一个名为max的宏,其值为10,以及一个名为edg的二维矩阵,由最大行数和最大列数组成。然而,如果你认为用户可以指定超过 10 个顶点的图,你可以始终增加宏的大小。

为了初始化edg矩阵的所有元素为 0,将其定义为静态矩阵。之后,将提示用户指定图中顶点的数量。假设用户输入 5 来表示图中存在 5 个顶点,那么这个值将被分配给numb变量。

为了使食谱易于理解,我们假设顶点按顺序编号为 1 到 5。将提示用户指定顶点之间的边。这意味着如果顶点 1 和 3 之间存在边,则用户应输入边为 1,3。表示这些边的输入顶点随后被分配给 v1 和 v2 顶点。因为用户被要求指定图的边,并在完成时输入0 0,所以在将边分配给 v1 和 v2 顶点时,我们首先确保顶点不是 0 和 0。如果是,程序将停止请求更多边,并转到显示邻接矩阵的语句。如果边的顶点不是零,则在二维edg矩阵的[v1][v2]索引位置分配一个值,1。因此,如果顶点 1 和 2 之间存在边,则将在edg[1][2]索引位置分配值 1,替换最初那里的值 0。

当所有图的边都输入完毕后,用户将输入顶点作为0 0来表示所有边都已输入。在这种情况下,将执行嵌套的for循环,并在屏幕上显示edg矩阵的所有元素。

程序使用 GCC 编译,如下面的截图所示。因为没有错误出现在编译过程中,这意味着adjmatdirect.c程序已成功编译成adjmatdirect.exe文件。在执行文件时,将提示用户指定顶点数及其边。一旦输入了顶点和边,程序将显示图的邻接矩阵表示(请参阅下面的截图):

图 10.4

现在,让我们探索如何对无向图做同样的事情。

创建无向图的邻接矩阵表示

在这个食谱的代码中增加一条语句,就可以使用相同的程序来创建无向图的邻接矩阵表示。

如何做...

我们参考的是前面食谱中的相同图;然而,这次没有边:

图片

图 10.5

它的邻接矩阵表示如下:

5,5          1              2              3              4              5
----------------------------------------------------------------------------
1            0              1              1              0              1
2            1              0              0              1              1
3            1              0              0              1              1
4            0              1              1              0              1
5            1              1              1              1              0

有向图和无向图的程序之间唯一的区别是,在后者中,边被简单地重复。也就是说,如果 a 和 b 之间存在边,那么它被认为是两条边:一条从 a 到 b,另一条从 b 到 a。

创建无向图邻接矩阵表示的程序如下:

//adjmatundirect.c

#include <stdio.h>

#define max 10

int main() {
  static int edg[max][max], i, j, v1, v2, numb;
  printf("How many vertices are there? ");
  scanf("%d", & numb);
  printf("We assume that the vertices are numbered from : ");
  for (i = 1; i <= numb; i++) printf("%d ", i);
  printf("\nEnter the edges of the graph. Like 1 4 if there is an \n");
  printf("edge between vertex 1 and 4\. Enter 0 0 when over\n");
  for (i = 1; i <= numb * (numb - 1); i++) {
    /* The for loop will run for at most numb*(numb-1) times because, the 
       number of edges are at most numb*(numb-1) where numb is the number 
       of vertices */
    scanf("%d %d", & v1, & v2);
    if (v1 == 0 && v2 == 0) break;
    edg[v1][v2] = 1;
    edg[v2][v1] = 1;
  }
  printf("\nThe adjacency matrix for the graph is \n");
  for (i = 1; i <= numb; i++) printf("\t%d", i);
  printf("\n----------------------------------------------------------\n");
  for (i = 1; i <= numb; i++) {
    printf("%d |\t", i);
    for (j = 1; j <= numb; j++) {
      printf("%d\t", edg[i][j]);
    }
    printf("\n");
  }
  return 0;
}

它是如何工作的...

当你将前面的程序与有向图的程序进行比较时,你会注意到只增加了一条额外的语句(加粗标记):

edg[v2][v1]=1;

即,在v1v2的边的情况下,还假设了反向边,即从v2v1

程序使用 GCC 编译,如下所示截图。因为没有错误出现在编译过程中,这意味着adjmatundirect.c程序已成功编译成adjmatundirect.exe文件。正如预期的那样,运行该文件时,用户将被提示指定顶点的数量和它们的边。一旦输入了顶点的数量和边的数量,程序将显示无向图的邻接矩阵表示,如下所示截图:

图片

图 10.6

现在,让我们继续到下一个食谱!

创建有向图的邻接表表示

在邻接表表示中,使用链表来表示顶点的相邻顶点。也就是说,为每个顶点的相邻顶点创建一个单独的链表,最后,图中所有顶点都连接起来。因为使用了链表,所以这种表示图的方式在内存使用上更加优化。

考虑以下有向图:

图片

图 10.7

它的邻接表表示如下:

图片

图 10.8

你可以在前面的图中看到,顶点1的相邻顶点以链表的形式连接。因为顶点2没有相邻顶点,所以它的指针指向NULL。同样,顶点3的相邻顶点,即顶点45,以链表的形式连接到顶点3。一旦创建了整个图的顶点的所有链表,所有顶点都通过链接连接起来。

在这个食谱中,我们将学习如何创建有向图的邻接表表示。

如何实现...

按照以下步骤创建图的邻接表表示:

  1. 定义一个名为node的结构,它包含三个成员。一个成员nme用于存储图的顶点;另一个成员vrt用于连接图的顶点;最后,edg用于连接顶点的相邻顶点。

  2. 询问用户指定图中顶点的数量。

  3. 创建一个链表,其中每个节点的nme成员包含图的顶点。

  4. 使用vrt指针将表示图中顶点的所有节点相互连接。

  5. 一旦输入所有顶点,用户将被提示输入图的边。用户可以输入任意数量的边,并且为了表示所有边都已输入,用户可以输入0 0作为边。

  6. 当进入一个边,例如,b,使用一个temp1指针,并将其设置为指向顶点a

  7. 创建一个新的节点称为newNode,并将顶点名称b分配给newNodenme成员。

  8. 使用另一个指针,称为temp2,并将其设置为指向连接到顶点a的最后一个节点。一旦temp2到达顶点a的末尾,temp2节点的edg成员被设置为指向newNode,从而在ab之间建立一条边。

创建有向图的邻接表表示的程序如下:

//adjlistdirect.c

#include <stdlib.h>
#include <stdio.h>

struct node {
  char nme;
  struct node * vrt;
  struct node * edg;
};

int main() {
  int numb, i, j, noe;
  char v1, v2;
  struct node * startList, * newNode, * temp1, * temp2;
  printf("How many vertices are there ? ");
  scanf("%d", & numb);
  startList = NULL;
  printf("Enter all vertices names\n");
  for (i = 1; i <= numb; i++) {
    if (startList == NULL) {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme); /* There is a space before %c */
      startList = newNode;
      temp1 = newNode;
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
    } else {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme);
      /* There is a space before %c */
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
      temp1 - > vrt = newNode;
      temp1 = newNode;
    }
  }
  printf("Enter the edges between vertices. Enter v1 v2, if there is an edge\n");
  printf("between v1 and v2\. Enter 0 0 if over\n");
  noe = numb * (numb - 1);
  for (j = 1; j <= noe; j++) {
    scanf(" %c %c", & v1, & v2);
    /* There is a space before %c */
    if (v1 == '0' && v2 == '0') break;
    temp1 = startList;
    while (temp1 != NULL && temp1 - > nme != v1)
      temp1 = temp1 - > vrt;
    if (temp1 == NULL) {
      printf("Sorry no vertex exist by this name\n");
      break;
    }
    temp2 = temp1;
    while (temp2 - > edg != NULL) temp2 = temp2 - > edg;
    newNode = malloc(sizeof(struct node));
    newNode - > nme = v2;
    temp2 - > edg = newNode;
    newNode - > edg = NULL;
    newNode - > vrt = NULL;
  }
  printf("\nAdjacency List representation of Graph is\n");
  temp1 = startList;
  while (temp1 != NULL) {
    printf("%c\t", temp1 - > nme);
    temp2 = temp1 - > edg;
    while (temp2 != NULL) {
      printf("%c\t", temp2 - > nme);
      temp2 = temp2 - > edg;
    }
    printf("\n");
    temp1 = temp1 - > vrt;
  }
}

现在,让我们幕后了解代码,以更好地理解它。

它是如何工作的...

假设我们正在处理以下有向图:

图 10.9

该图的邻接表表示如下:

图 10.10

我们定义一个名为“node”的结构,包含以下三个成员:

  • nme:这是用于存储顶点。

  • vrt:一个指向连接图中所有顶点的指针。

  • edg:一个连接当前顶点所连接的所有顶点的指针:

图 10.11

用户被提示指定顶点的数量。假设用户输入值为 5,则 5 的值将被分配给numb变量。定义一个startList指针为NULL。整个邻接表将通过这个startList指针访问,并且它将被设置为指向图的第一个顶点。首先,用户被要求输入顶点的名称。

初始时,startList指针为NULL,因此创建了一个新节点newNode,并将用户输入的顶点名称,例如a,分配给newNodenme成员。startList指针被设置为指向newNode。为了使用newNode连接更多顶点,将temp1指针设置为指向newNode。最初,两个指针vrtedg也都设置为NULL。在for循环的第一次迭代之后,图中的节点将如下所示:

图 10.12

for循环的第二次迭代中,因为startList指针不再为NULL,将执行else块,并再次创建一个新节点,称为newNode。接下来,将顶点名称分配给newNode的命名成员。再次,将newNodevrtedg指针设置为NULL。为了将newNode连接到前面的顶点,我们将借助temp1指针。将temp1指针指向的节点的vrt指针设置为指向newNode,如下所示:

图 10.13

然后,将temp1指针设置为指向newNode,并对其余顶点重复此过程。本质上,temp1指针用于连接更多顶点。在for循环结束时,节点将如下所示连接:

图 10.14

一旦所有图的顶点都输入完毕,用户将被要求指定顶点之间的边。此外,当所有图的边都输入完毕时,用户需要输入0 0。假设用户输入a b来表示从顶点a到顶点b存在一条边。顶点分别被分配给v1v2变量。我们首先确保v1v2中的数据不是 0。如果是,这意味着所有图的边都已输入,程序将跳转到显示邻接表开始的语句。

然后,为了连接顶点ab,首先,将temp1指针设置为指向startListtemp1指针被设置为找到其nme成员等于变量v1中输入的顶点的节点,即atemp1指针已经指向顶点a。之后,你需要找到与temp1连接的最后一个节点。temp2指针用于找到由temp1指针指向的节点的最后一个连接节点。因为这是顶点a的第一个输入边,所以temp2指针指向的节点的edg成员已经是NULL。因此,创建一个新的节点称为newNode,并将变量v2中的顶点名称,即b,分配给newNodenme变量。newNodeedgvrt成员被设置为NULL,如下所示:

图 10.15

temp2edg成员设置为指向newNode,如下所示:

图 10.16

该过程会重复应用于用户输入的其余边。

程序使用 GCC 编译,如下所示。因为没有错误出现在编译过程中,这意味着adjlistdirect.c程序已成功编译成adjlistdirect.exe文件。在执行可执行文件时,用户将被提示指定顶点的数量及其边。一旦输入了顶点和边,程序将显示有向图的邻接表表示,如下所示截图:

图 10.17

现在,让我们继续下一个菜谱!

执行图的广度优先遍历

图的遍历是指按照一个良好的定义顺序访问图的每个顶点正好一次。为了确保图中的每个顶点只被访问一次,并知道哪些顶点已经被访问,最好的方法是标记它们。我们还将在此菜谱中查看如何标记顶点。

广度优先遍历倾向于创建非常短而宽的树。它通过层来操作顶点,即首先评估离起始点最近的顶点,最后评估最远的顶点。因此,它被称为树的层遍历。图的广度优先遍历在寻找两个位置(顶点)之间的最短路径(即边数最少的路径)方面非常流行。它也用于查找网页的链接页面、广播信息等。

在这个菜谱中,我们将学习如何执行图的广度优先遍历。

如何操作...

按照以下步骤执行图的广度优先遍历:

  1. 将图的第一顶点添加到队列中。任何顶点都可以作为起始顶点。

  2. 然后,重复以下步骤 38,直到队列为空。

  3. 从队列中取出顶点并将其存储在一个变量中,例如 v

  4. 将其标记为已访问(标记是为了确保这个顶点不应再次被遍历)。

  5. 显示标记的顶点。

  6. 找出顶点 v 的相邻顶点,然后对每个顶点执行 步骤 78

  7. 如果 v 的任何相邻顶点未被标记,则将其标记为已访问。

  8. 将相邻顶点添加到队列中。

  9. 退出。

图的广度优先遍历程序如下:

//breadthfirsttrav.c

#include <stdlib.h>
#include <stdio.h>

#define max 20

enum Setmarked {
  Y,
  N
};
struct node {
  char nme;
  struct node * vrt;
  struct node * edg;
  enum Setmarked marked;
};

struct node * que[max];
int rear = -1, front = -1;
void queue(struct node * paramNode);
struct node * dequeue();

int main() {
  int numb, i, j, noe;
  char v1, v2;
  struct node * startList, * newNode, * temp1, * temp2, * temp3;
  printf("How many vertices are there ?");
  scanf("%d", & numb);
  startList = NULL;
  printf("Enter all vertices names\n");
  for (i = 1; i <= numb; i++) {
    if (startList == NULL) {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme);
      /* There is a space before %c */
      startList = newNode;
      temp1 = newNode;
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
      newNode - > marked = N;
    } else {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme);
      /* There is a space before %c */
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
      newNode - > marked = N;
      temp1 - > vrt = newNode;
      temp1 = newNode;
    }
  }
  printf("Enter the edges between vertices. Enter v1 v2, if there is an edge\n");
  printf("between v1 and v2\. Enter 0 0 if over\n");
  noe = numb * (numb - 1);
  for (j = 1; j <= noe; j++) {
    scanf(" %c %c", & v1, & v2);
    /* There is a space before %c */
    if (v1 == '0' && v2 == '0') break;
    temp1 = startList;
    while (temp1 != NULL && temp1 - > nme != v1)
      temp1 = temp1 - > vrt;
    if (temp1 == NULL) {
      printf("Sorry no vertex exist by this name\n");
      break;
    }
    temp2 = temp1;
    while (temp2 - > edg != NULL) temp2 = temp2 - > edg;
    newNode = malloc(sizeof(struct node));
    newNode - > nme = v2;
    temp2 - > edg = newNode;
    newNode - > edg = NULL;
    newNode - > vrt = NULL;
  }
  printf("\nAdjacency List representation of Graph is\n");
  temp1 = startList;
  while (temp1 != NULL) {
    printf("%c\t", temp1 - > nme);
    temp2 = temp1 - > edg;
    while (temp2 != NULL) {
      printf("%c\t", temp2 - > nme);
      temp2 = temp2 - > edg;
    }
    printf("\n");
    temp1 = temp1 - > vrt;
  }
  printf("\nBreadth First traversal of the graph is \n");
  temp1 = startList;
  if (temp1 == NULL)
    printf("Sorry no vertices in the graph\n");
  else
    queue(temp1);
  while (rear != -1) {
    temp3 = dequeue();
    temp1 = startList;
    while (temp1 - > nme != temp3 - > nme) temp1 = temp1 - > vrt;
    temp3 = temp1;
    if (temp3 - > marked == N) {
      printf("%c\t", temp3 - > nme);
      temp3 - > marked = Y;
      temp2 = temp3 - > edg;
      while (temp2 != NULL) {
        queue(temp2);
        temp2 = temp2 - > edg;
      }
    }
  }
  return 0;
}

void queue(struct node * paramNode) {
  rear++;
  que[rear] = paramNode;
  if (front == -1) front = 0;
}

struct node * dequeue() {
  struct node * tempNode;
  if (front == rear) {
    tempNode = que[front];
    front = -1;
    rear = -1;
  } else {
    tempNode = que[front];
    front++;
  }
  return (tempNode);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们正在使用之前菜谱中创建的邻接表表示的有向图,创建有向图的邻接表表示

图 10.18

temp1 指针被设置为指向 startList。也就是说,temp1 正在指向具有顶点 a 的节点。如果 temp1 不是 NULL,则由 temp1 指针指向的节点被添加到队列中。当前为 -1 的尾变量被增加到 0,并将 a 节点添加到 que 节点数组中的索引位置 0。因为当前前索引位置值为 -1,所以前指针也被设置为 0,如下所示:

图 10.19

此后,调用 dequeue 函数从队列中删除一个节点。不出所料,que[0] 索引位置的节点,即 a,被返回,因为 frontrear 的值相同,所以 frontrear 索引的值被设置为 -1,以指示队列再次为空。

包含顶点 a 的节点从队列中返回,并分配给 temp3 指针。temp1 指针被设置为指向 startList 指针。temp3 节点的标记成员,即顶点 a,最初被设置为 N。节点中存储在 nme 成员中的顶点名称被显示,即顶点 a 在屏幕上显示。

在显示顶点 a 后,其标记成员被设置为 Y 以指示节点已被访问且不应再次遍历。下一步是找到顶点 a 的相邻顶点。为此,将 temp2 指针设置为指向 temp3edg 指针所指向的位置。temp3edg 指针指向顶点 b,因此 temp2 被设置为指向顶点 b。再次重复该过程。如果 temp2 不是 NULL,则 b 节点被排队,即被添加到 que[0] 索引位置。因为所有连接到顶点 a 的节点都必须排队,所以 temp2 指针被设置为指向其 edg 指针所指向的位置。节点 b(在邻接表中)的 edg 指针指向节点 c,因此节点 c 也被插入到队列中的 que[1] 索引位置,如下所示:

图 10.20

在队列中,节点 bc 存在。现在,再次调用出队函数;从队列中移除节点 b,并将 temp3 指针设置为指向它。temp1 指针最初设置为指向 startList,然后通过使用其 vrt 指针,将 temp1 指针设置为指向顶点 b。因为节点 b 的标记成员是 N,其顶点名称 b 显示在屏幕上,随后将其标记成员设置为 Y。将 temp2 指针设置为指向节点 bedg 成员所指向的位置。节点 bedg 成员指向 NULL,因此访问队列中的下一个节点,即从队列中移除节点 c 并将 temp3 指针设置为指向它。因为队列再次为空,所以将 frontrear 变量的值设置为 -1。

再次,将 temp1 指针设置为指向顶点 c,并在屏幕上显示 c 节点,即遍历它并设置其标记成员为 Y。因此,到目前为止,节点 abc 已在屏幕上显示。将连接到 cedg 成员的节点添加到队列中,即节点 d 被添加到队列的 que[0] 索引位置。此外,访问节点 dedg 指针所指向的节点,即节点 e 也被排队或换句话说,添加到 que[1] 索引位置如下:

图片

图 10.21

从队列中移除节点 d 并显示(遍历)。访问它们 edg 成员所指向的节点,如果其中任何一个被标记,则将 N 添加到队列中。整个过程重复进行,直到队列为空。在屏幕上显示顶点的顺序形成图的广度优先遍历。

程序使用 GCC 编译,如以下截图所示。因为在编译过程中没有出现错误,这意味着 breadthfirsttrav.c 程序已成功编译成 breadthfirsttrav.exe 文件。执行文件时,用户将被提示指定图中的顶点数量,然后输入顶点名称。之后,用户被要求输入图的边,并在完成后输入 0 0。输入图的边后,将显示图的邻接表表示,然后显示图的广度优先遍历,如以下截图所示:

图片

图 10.22

现在,让我们继续下一个菜谱!

执行图的深度优先遍历

在深度优先遍历(也称为深度优先搜索)中,通过取一条路径并尽可能深入地沿着该路径遍历,访问图中的所有节点。到达末端后,返回,选择另一条路径,然后重复该过程。

在这个菜谱中,我们将学习如何执行图的深度优先遍历。

如何做到这一点...

按以下步骤进行图的深度优先遍历:

  1. 将图的第一个顶点推入栈中。您可以选择图的任何顶点作为起始顶点。

  2. 然后,重复以下步骤 37,直到栈为空。

  3. 从栈中弹出顶点并按任何名称调用它,例如,v

  4. 将弹出的顶点标记为已访问。这种标记是为了确保该顶点不应再次遍历。

  5. 显示标记的顶点。

  6. 找出v顶点的邻接顶点,然后对每个顶点执行步骤 7

  7. 如果v的任何邻接顶点未标记,则将其标记为已访问并将它们推入栈中。

  8. 退出。

图的深度优先遍历程序如下:

//depthfirsttrav.c

#include <stdlib.h>
#include <stdio.h>
#define max 20

enum Setmarked {Y,N};
struct node {
  char nme;
  struct node * vrt;
  struct node * edg;
  enum Setmarked marked;
};

struct node * stack[max];
int top = -1;
void push(struct node * h);
struct node * pop();

int main() {
  int numb, i, j, noe;
  char v1, v2;
  struct node * startList, * newNode, * temp1, * temp2, * temp3;
  printf("How many vertices are there ?");
  scanf("%d", & numb);
  startList = NULL;
  printf("Enter all vertices names\n");
  for (i = 1; i <= numb; i++) {
    if (startList == NULL) {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme);
      /* There is a white space before %c */
      startList = newNode;
      temp1 = newNode;
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
      newNode - > marked = N;
    } else {
      newNode = malloc(sizeof(struct node));
      scanf(" %c", & newNode - > nme);
      /* There is a white space before %c */
      newNode - > vrt = NULL;
      newNode - > edg = NULL;
      newNode - > marked = N;
      temp1 - > vrt = newNode;
      temp1 = newNode;
    }
  }
  printf("Enter the edges between vertices. Enter v1 v2, if there is an edge\n");
  printf("between v1 and v2\. Enter 0 0 if over\n");
  noe = numb * (numb - 1);
  for (j = 1; j <= noe; j++) {
    scanf(" %c %c", & v1, & v2);
    /* There is a white space before %c */
    if (v1 == '0' && v2 == '0') break;
    temp1 = startList;
    while (temp1 != NULL && temp1 - > nme != v1)
      temp1 = temp1 - > vrt;
    if (temp1 == NULL) {
      printf("Sorry no vertex exist by this name\n");
      break;
    }
    temp2 = temp1;
    while (temp2 - > edg != NULL) temp2 = temp2 - > edg;
    newNode = malloc(sizeof(struct node));
    newNode - > nme = v2;
    temp2 - > edg = newNode;
    newNode - > edg = NULL;
    newNode - > vrt = NULL;
  }
  printf("\nAdjacency List representation of Graph is\n");
  temp1 = startList;
  while (temp1 != NULL) {
    printf("%c\t", temp1 - > nme);
    temp2 = temp1 - > edg;
    while (temp2 != NULL) {
      printf("%c\t", temp2 - > nme);
      temp2 = temp2 - > edg;
    }
    printf("\n");
    temp1 = temp1 - > vrt;
  }
  printf("\nDepth First traversal of the graph is \n");
  temp1 = startList;
  if (temp1 == NULL)
    printf("Sorry no vertices in the graph\n");
  else
    push(temp1);
  while (top >= 0) {
    temp3 = pop();
    temp1 = startList;
    while (temp1 - > nme != temp3 - > nme) temp1 = temp1 - > vrt;
    temp3 = temp1;
    if (temp3 - > marked == N) {
      printf("%c\t", temp3 - > nme);
      temp3 - > marked = Y;
      temp2 = temp3 - > edg;
      while (temp2 != NULL) {
        push(temp2);
        temp2 = temp2 - > edg;
      }
    }
  }
  return 0;
}

void push(struct node * h) {
  top++;
  stack[top] = h;
}

struct node * pop() {
  return (stack[top--]);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们使用从上一个菜谱中创建的有向图的邻接表表示

图 10.23

temp1指针设置为指向startList,即节点a,我们假设它是图的起始顶点。然后我们确保如果temp1不是NULL,则temp1指针指向的节点被推入栈中。初始值为-1 的top值增加到 0,节点a被添加到节点栈的索引位置0,如下所示:

图 10.24

此后,调用pop函数从栈中删除节点。返回stack[0]索引位置的节点,并将top的值再次减少到-1。

包含顶点a的节点被返回到temp3指针。将temp1指针设置为指向startList指针。temp3节点的标记成员,即顶点a,最初设置为N。节点中存储的顶点名称nme成员被显示,即顶点a被显示在屏幕上。显示顶点a后,其标记成员被设置为Y以指示该节点已被访问且不应再次遍历。将temp2指针设置为指向temp3edg指针指向的位置。temp3edg指针指向顶点b,因此temp2被设置为指向顶点b。再次重复该过程,即我们检查temp2是否不是NULL,然后将节点b推入栈的stack[0]索引位置。因为所有连接到顶点a的节点都必须推入栈中,所以将temp2指针设置为指向其edg指针指向的位置。节点b(在邻接表中)的edg指针指向节点c,因此节点c也被推入栈的stack[1]索引位置,如下所示:

图 10.25

在栈中,存在节点 bc。现在,再次调用 pop 函数,从栈中弹出节点 c,并将 temp3 指针设置为指向它。temp1 指针最初设置为指向 startList,然后通过使用其 vrt 指针,将 temp1 指针设置为指向顶点 c。因为节点 c 的标记成员是 N,所以其顶点名称 c 在屏幕上显示,并将其标记成员设置为 Y。因此,到目前为止,节点 ac 已在屏幕上显示。

temp2 指针设置为指向节点 cedg 成员所指向的位置。节点 cedg 成员指向节点 d,因此将节点 d 推入栈中,并访问节点 c 的下一个相邻节点。节点 c 的下一个相邻节点是节点 e,它也被按如下方式推入栈中:

图片

图 10.26

再次,从栈中弹出的最顶层节点是节点 e,将 temp3 指针设置为指向它。再次,将 temp1 指针设置为指向顶点 e,并在屏幕上显示节点 e,即进行遍历。然后,将其标记成员设置为 Y,并将连接到 eedg 成员的节点推入栈中,即节点 a 被推入栈中,随后是节点 b,如下所示:

图片

图 10.27

弹出节点 b,并将 temp3 指针设置为指向它。将 temp1 指针设置为指向节点 b。因为节点 b 的标记成员是 N,表示它尚未被遍历,所以在屏幕上显示顶点 b 并将其标记成员设置为 Y。由于顶点 b 没有相邻成员,栈中的下一个节点 a 被弹出。因为顶点 a 已经被访问,所以从栈中弹出的下一个节点是节点 d。重复此过程,显示的顶点序列被认为是图的深度遍历。

程序使用 GCC 编译,如下截图所示。因为没有在编译过程中出现错误,这意味着 depthfirsttrav.c 程序已成功编译成 depthfirsttrav.exe 文件。执行该文件时,用户将被提示指定图中顶点的数量,然后输入顶点的名称。之后,用户被要求输入图的边,完成输入后输入 0 0。输入图的边后,将显示图的邻接表表示,然后是图的深度优先遍历,如下截图所示:

图片

图 10.28

现在,让我们继续下一个菜谱!

使用 Prim 算法创建最小生成树

在这个菜谱中,我们将学习如何创建最小生成树。具有n个节点的图的 minimum spanning tree 将有n个节点。在一个加权连通图中,每个图的边被分配一个非负数,称为“边的权重”。然后,将图中的任何生成树分配一个通过将树中边的权重相加得到的总权重。一个图的 minimum spanning tree 是一个总权重尽可能小的生成树。

有多种技术可以用来为加权图创建最小生成树。其中一种方法称为 Prim 算法。

Prim 算法是贪心算法类别的一部分,其中顶点通过具有最低权重的边连接。最初选择一个任意节点作为树的根节点。在无向图中,任何节点都可以被认为是树的根节点,与其相邻的节点作为其子节点。然后,将图中的节点逐个添加到树中,直到包含图中的所有节点。在每次添加到树中的图节点都是通过最小权重的弧与树的节点相邻。最小权重的弧成为连接新节点到树的树弧。当图中的所有节点都已添加到树中时,可以说已经为图创建了一个最小生成树。

如何做...

按照以下步骤实现 Prim 算法:

  1. 从图中选择任何顶点作为最小生成树的根。它可以是一个任意顶点。

  2. 从顶点(或树中的顶点)找到到图中其他顶点的所有边。从那些顶点中,选择具有最小权重的边,并将该顶点添加到树中。

  3. 重复步骤 2,直到将图中的所有顶点添加到最小生成树中。

考虑以下加权图:

图片

图 10.29

现在,为了得到这个图的最小生成树,我们从顶点a(你可以将任何顶点视为图的起始顶点)开始连接顶点。从起始顶点开始,选择具有最低权重的最近顶点,然后重复此过程,直到所有顶点都连接起来。这样,我们得到以下最小生成树:

图片

图 10.30

上述图被称为树,因为它是无环的;它被称为生成树,因为它覆盖了每个顶点。

最小生成树中的边数是v-1,其中v是顶点的数量。

使用 Prim 算法创建最小生成树的程序如下:

//prims.c

#include <stdlib.h>
#include <stdio.h>
#define max 20 
struct node
{
 int nme;
 int wt;
 struct node *vrt;
 struct node *edg;
 };

struct node *startList;

struct lst
{
 int u,v;
 int wt;
 struct lst *next;
}lst; 

struct lst *pq=NULL;
struct lst *tr=NULL;
void addpqu(int a, int b, int w);
void maketree();
void disptree();
struct lst *delet();
int visited[max];
int n,nov=0; 

int main()
{
 int i,j,noe,w;
 int a,b;
 struct node *newNode,*temp1,*temp2;
 printf ("How many vertices are there ?");
 scanf("%d",&n);
 printf("The vertices are named\n");
 for(i=1;i<=n;i++)printf("%d\t",i);
 printf("for convenience \n");
 startList=NULL;
 for(i=1;i<=n;i++)
 {
     if (startList==NULL)
     {
         newNode =malloc(sizeof (struct node));
         newNode->nme=i;
         startList=newNode;
         temp1=newNode;
         newNode->vrt=NULL;
         newNode->edg=NULL;
     }
     else
     {
         newNode=malloc(sizeof (struct node));
         newNode->nme=i;
         newNode->vrt=NULL;
         newNode->edg=NULL;
         temp1->vrt=newNode;
         temp1=newNode;
     }
 }
 printf("Enter the edges between vertices. Enter 1 3, if there is an edge\n");
 printf("between 1 and 3\. Enter 0 0 if over\n");
 noe=n*(n-1);
 for(j=1;j<=noe;j++)
 {
     printf("Enter edge ");
     scanf("%d %d",&a,&b);
     if(a==0 && b==0)break;
     printf("Enter weight ");
     scanf("%d",&w);
     temp1=startList;
     while(temp1!=NULL && temp1->nme!=a)
     {
         temp1=temp1->vrt;
     }
     if(temp1==NULL)
     {
         printf("Sorry no vertex exist by this name\n");
         break;
     }
     temp2=temp1;
     while(temp2->edg!=NULL)temp2=temp2->edg;
     newNode=malloc(sizeof (struct node));
     newNode->nme=b;
     newNode->wt=w;
     temp2->edg=newNode;
     newNode->edg=NULL;
     newNode->vrt=NULL;
     temp1=startList;
     while(temp1!=NULL && temp1->nme!=b)
         temp1=temp1->vrt;
     if(temp1==NULL)
     {
         printf("Sorry no vertex exist by this name\n");
         break;
     }
     temp2=temp1;
     while(temp2->edg!=NULL)temp2=temp2->edg;
     newNode=malloc(sizeof (struct node));
     newNode->nme=a;
     newNode->wt=w;
     temp2->edg=newNode;
     newNode->edg=NULL;
     newNode->vrt=NULL;
}
printf ("Adjacency List representation of Graph is\n");
temp1=startList;
while (temp1!=NULL)
{
     printf ("%d\t",temp1->nme);
     temp2=temp1->edg;
     while(temp2!=NULL)
     {
         printf("%d\t",temp2->nme);
         temp2=temp2->edg;
     }
     printf("\n");
     temp1=temp1->vrt;
}
temp1=startList;
temp2=temp1->edg;
while(temp2!=NULL)
{
    addpqu(temp1->nme,temp2->nme, temp2->wt);
    temp2=temp2->edg;
}
maketree();
disptree();
return 0;
}

void addpqu(int a, int b, int w)
{
 struct lst *lstNode,*findloc1,*findloc2;
 lstNode=malloc(sizeof(struct lst));
 lstNode->u=a;
 lstNode->v=b;
 lstNode->wt=w;
 lstNode->next=NULL;
 if(pq==NULL)
 {
     pq = lstNode;
 }
 else
 {
     if(lstNode->wt < pq->wt)
     {
         lstNode->next=pq;
         pq=lstNode;
     }
     else
     {
         findloc1=pq;
         while((findloc1!=NULL) && (findloc1->wt <= lstNode->wt))
         {
             findloc2=findloc1;
             findloc1=findloc1->next;
         }
         findloc2->next=lstNode;
         lstNode->next=findloc1;
     }
  }
} 

struct lst *delet()
{
 struct lst *tempNode;
 if (pq !=NULL)
 {
     tempNode=pq;
     pq=pq->next;
     return tempNode;
 }
 else
     return NULL;
 } 

void maketree()
{
 struct lst *lstNode,*tempNode1,*tempNode2;
 struct node *x,*y;
 int i,j;
 while(nov <n)
 {
     nxt: lstNode=delet();
     for(i=1;i<=nov;i++)
     {
         if(visited[i]==lstNode->u)
         {
             for(j=1;j<=nov;j++)
                 if(visited[j]==lstNode->v) goto nxt;
         }
     }
     for(i=1;i<=nov;i++)
         if(visited[i]==lstNode->u) goto rpt;
     nov++;
     visited[nov]=lstNode->u;
     rpt: for(i=1;i<=nov;i++)
     {
         if(visited[i]==lstNode->v) goto rptt;
     }
     nov++;
     visited[nov]=lstNode->v;
     rptt: lstNode->next=NULL;
     if (tr==NULL)
     {
         tr=lstNode;
         tempNode1=tr;
     }
     else
     {
         tempNode1->next=lstNode;
         tempNode1=lstNode;
     }
     x=startList;
     while(x->nme!=lstNode->v)x=x->vrt;
     y=x->edg;
     pq=NULL;
     while(y!=NULL)
     {
         addpqu(x->nme,y->nme, y->wt);
         y=y->edg;
     }
  }
}

void disptree()
{
 struct lst *t;
 t=tr;
 printf("Minimal Spanning tree with Prims Algorithm is \n");
 while(t!=NULL)
 {
     printf("%d %d\n",t->u,t->v);
     t=t->next;
 }
} 

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

用户被提示指定顶点的数量。假设用户输入5,则5的值将被分配给变量n。为了方便,顶点将被自动命名为12345。定义了一个节点类型的startList指针,并初始设置为NULLstartList指针将指向从图中创建的邻接链表的第一节点。

定义了两个结构:一个称为node,另一个称为lstnode结构用于创建图的邻接表表示,而lst结构用于创建最小生成树。定义了两个lst类型的指针,pqtr,并定义为NULL

要创建邻接链表表示,第一步是创建一个节点链表,其中每个节点代表图中的一个顶点。因为图中包含五个顶点,所以设置了一个for循环执行五次。在for循环内,创建了一个名为newNode的节点,并将顶点号1分配给其nme成员。将startList指针设置为指向newNodenewNodevrtedg成员被设置为NULLtemp1指针也设置为指向newNodestartList指针将始终指向链表的第一节点,而temp1指针将用于连接其他节点,即其他顶点。

for循环的下一个迭代中,再次创建一个新的节点,称为newNode,并将顶点号2分配给它。newNodevrtedg成员被设置为NULL。为了与现有顶点连接,将temp1vrt成员设置为指向newNode。之后,将temp1设置为指向NewNodefor循环将执行与顶点数量相等的时间,即五个,因此创建了包含各自顶点号的五个节点。当for循环结束时,顶点将被创建,并如下所示:

图片

图 10.31

一旦创建了顶点,下一步就是询问用户顶点之间的边。具有n个顶点的图最多可以有n * (n-1)条边。因为顶点数量是五个,所以节点,即边的数量变量,被初始化为54=20*。设置了一个for循环,j,从 1 执行到 20,要求用户输入边及其相应的权重。假设用户输入的边为1 2,分别被分配给ab变量;输入的权重是1,被分配给w变量。

要创建此边,将temp1指针设置为指向startList。要创建一个(1,2)边,将temp1指针设置为指向其nme成员等于 1 的节点。目前,temp1已经指向顶点1。下一步是在顶点1的末尾添加顶点2。为了找到顶点的末尾,我们将使用另一个名为temp2的指针。首先,将temp2指针设置为指向temp1指针指向的节点。然后,使用其edg指针,将temp2设置为指向顶点1的最后一个节点。然后,创建一个新的节点,称为newNode,并将值2分配给其nme成员。将w变量中的权重1分配给newNodewt成员。将newNodeedgevrt指针设置为NULL。最后,将temp2edg成员设置为指向newNode。因此,现在顶点12已经连接在一起了。

这是一个无向图,边(1,2)也可以表示从21的边。因此,我们需要一个从顶点21的边。将temp1指针设置为指向startList。使用其vrt指针,将temp1指针移动到顶点2

temp1到达顶点2时,下一步是将temp2指针设置为指向顶点2的最后一个节点。完成此操作后,创建一个新的节点,称为newNode,并将值1分配给其nme成员。此外,将w变量中的权重分配给newNodewt成员。为了连接包含顶点21的这些节点,将temp2的边指针设置为指向newNode。将newNodeedgvrt指针设置为NULL。因此,现在顶点21也连接在一起了。

显示邻接链表

在输入所有边及其权重后,必须显示邻接表。为此,设置一个名为temp1的指针指向startList。一个while循环将执行,直到temp1指针达到NULL。因此,temp1指针最初将指向顶点1。之后,通过第二个指针temp2的帮助,访问并显示temp1指针的所有边(即顶点1)。显示顶点1的所有边后,利用vrt成员,将temp1指针设置为指向下一个顶点,即顶点2。再次,将temp2指针设置为指向顶点2,并使用其edg成员显示顶点2的所有边。对于图中的所有顶点,重复此过程。

邻接表将如下所示:

图片

图 10.32

创建最小生成树

为了构建最小生成树,我们需要按顶点边的权重顺序调整。将temp1指针设置为指向startList,即顶点1。将temp2指针设置为指向temp1edg指针所指向的位置,即顶点2

现在,直到temp2变为NULL,调用addpqu函数,并将顶点12及其权重1传递给它。在addpqu函数中,创建了一个名为lstNodelst类型结构。将顶点12及其权重1分别分配给其uvwt成员。将lstNode的下一个指针设置为NULL。另外,设置一个指针pq指向lstNode

然后,将temp2指针设置为指向其edg指针所指向的位置,即顶点3。再次调用addpqu函数,并将顶点13和权重3传递给它。在addpqu函数中,再次创建一个新的节点,称为lstNode,并将顶点13和权重3分别分配给其uvwt成员。将lstNode的下一个指针设置为NULL

因为节点必须按其权重升序排列,所以比较lstNodewt成员和前一个节点pqwt成员。lstNodewt成员是3,大于pq节点的wt成员1。因此,使用两个指针findloc1findloc2的帮助。一个指针设置为指向lstNode的权重,并与pq节点进行比较。

让我们选择一个顶点1并将其添加到最小生成树中:

图片

图 10.33

现在,从顶点1出发,有边通向顶点325,但权重最低的是通向顶点2的边。因此,顶点2也被添加到最小生成树中:

图片

图 10.34

再次,从最小生成树中的顶点12出发,我们寻找通向其他顶点的所有边。我们发现边(15)和(25)具有相同的权重,因此我们可以选择任一顶点。让我们将边(25)添加到最小生成树中:

图片

图 10.35

从最小生成树中的顶点125出发,我们寻找具有最低权重的边。边(53)具有最低的权重1,因此边(53)被添加到最小生成树中:

图片

图 10.36

现在,我们需要从现有的最小生成树中的顶点中找到通向顶点4的边。边(34)具有最低的权重,因此被添加到最小生成树中:

图片

图 10.37

编译并运行程序后,你应该得到一个类似于以下屏幕截图的输出:

图片

图 10.38

现在,让我们继续下一个菜谱!

使用克鲁斯卡尔算法创建最小生成树

在这个菜谱中,我们将学习如何使用克鲁斯卡尔算法制作最小生成树。

无向图的最小/最小生成树是由连接图中所有顶点的最低总成本的边形成的树。如果且仅当图是连通的,最小生成树才存在。如果存在任何两个顶点之间的路径,则称图是连通的。

在这里,图中的节点最初被认为是具有一个节点的n个不同的部分树。算法的每一步,两个不同的部分树通过图中的一条边连接成一个部分树。当只有一个部分树存在时(例如,经过n-1次这样的步骤后),它就是一个最小生成树。

使用最低成本的连接弧连接两个不同的树。为此,可以根据权重将弧放置在优先队列中。然后检查权重最低的弧是否连接了两个不同的树。为了确定弧(x,y)是否连接了两个不同的树,我们可以在每个节点中实现一个父字段来表示树。然后,我们可以遍历 x 和 y 的所有祖先以获得连接它们的树的根。如果两个树的根是同一个节点,并且 x 和 y 已经在同一个树中,那么弧(x,y)将被丢弃,然后检查下一个最低权重的弧。

如何做到这一点...

按照以下步骤使用克鲁斯卡尔算法创建最小生成树:

  1. 按照边的权重升序对边列表进行排序。

  2. 从边列表的顶部(权重最小的边)取出边。

  3. 从边列表中删除这条边。

  4. 使用给定的边连接两个顶点。如果通过连接顶点,在图中形成了一个环,那么就丢弃这条边。

  5. 重复先前的步骤 24,直到添加了n-1条边或边列表完整。

使用克鲁斯卡尔算法创建最小生成树的程序如下:

//kruskal.c

#include <stdlib.h>
#include <stdio.h>
#define max 20

struct node
{
 int nme;
 int wt;
 struct node *v;
 struct node *e;
}; 

typedef struct lst
{
 int u,v;
 int wt;
 struct lst *nxt;
}lst; 

lst *pq=NULL;
lst *tr=NULL;
void addpqu(int a, int b, int w);
void maketree();
void disptree();
lst *delet();
int parent[max]; 

int main()
{
 int n,i,j,noe,w;
 int a,b;
 struct node *adj,*newNode,*p,*q;
 printf ("How many vertices are there ? ");
 scanf("%d",&n);
 for(i=1;i<=n;i++)parent[i]=0;
 printf("The vertices are named\n");
 for(i=1;i<=n;i++)printf("%d\t",i);
 printf("for convenience \n");
 for(i=1;i<=n;i++)
 {
     if (i==1)
     {
         newNode =malloc(sizeof (struct node));
         newNode->nme=i;
         adj=newNode;
         p=newNode;
         newNode->v=NULL;
         newNode->e=NULL;
     }
     else
     {
         newNode=malloc(sizeof (struct node));
         newNode->nme=i;
         newNode->v=NULL;
         newNode->e=NULL;
         p->v=newNode;
         p=newNode;
     }
 }
 printf("Enter the edges between vertices. Enter 1 3, if there is an edge\n");
 printf("between 1 and 3\. Enter 0 0 if over\n");
 noe=n*(n-1);
 for(j=1;j<=noe;j++)
 {
     printf("Enter edge: ");
     scanf("%d %d",&a,&b);
     if(a==0 && b==0)break;
     printf("Enter weight: ");
     scanf("%d",&w);
     p=adj;
     while(p!=NULL && p->nme!=a)
         p=p->v;
     if(p==NULL)
     {
         printf("Sorry no vertex exist by this name\n");
         break;
     }
     q=p;
     while(q->e!=NULL)q=q->e;
     newNode=malloc(sizeof (struct node));
     newNode->nme=b;
     newNode->wt=w;
     q->e=newNode;
     newNode->e=NULL;
     newNode->v=NULL;
     addpqu(a,b,w);
 }
 printf ("Adjacency List representation of Graph is\n");
 p=adj;
 while (p!=NULL)
 {
     printf ("%d\t",p->nme);
     q=p->e;
     while(q!=NULL)
     {
         printf("%d\t",q->nme);
         q=q->e;
     }
     printf("\n");
     p=p->v;
 }
 maketree();
 disptree();
 return 0;
} 

void addpqu(int a, int b, int w)
{
 lst *newNode,*k,*h;
 newNode=(lst *)malloc(sizeof(lst));
 newNode->u=a;
 newNode->v=b;
 newNode->wt=w;
 newNode->nxt=NULL;
 if(pq==NULL)
     pq = newNode;
 else
 {
     if(newNode->wt < pq->wt)
     {
         newNode->nxt=pq;
         pq=newNode;
     }
     else
     {
         k=pq;
         while((k!=NULL) &&(k->wt <= newNode->wt))
         {
             h=k;
             k=k->nxt;
         }
         h->nxt=newNode;
         newNode->nxt=k;
     }
   }
 } 

lst *delet()
{
 lst *q;
 if (pq !=NULL)
 {
     q=pq;
     pq=pq->nxt;
     return q;
 }
 else
     return NULL;
 } 

void maketree()
{
 lst *newNode,*p;
 int x,y,r1,r2;
 newNode=delet();
 while(newNode !=NULL)
 {
     newNode->nxt=NULL;
     x=newNode->u;
     y=newNode->v;
     while(x>0)
     {
         r1=x;
         x=parent[x];
     }
     while(y>0)
     {
         r2=y;
         y=parent[y];
     }
     if(r1 !=r2)
     {
         parent[r2]=r1;
         if (tr==NULL)
         {
             tr=newNode;
             p=tr;
         }
         else
         {
             p->nxt=newNode;
             p=newNode;
         }
     }
     newNode=delet();
   }
 } 

void disptree()
{
 lst *t;
 t=tr;
 printf("Minimal Spanning tree with Kruskal Algorithm is \n");
 while(t!=NULL)
 {
     printf("%d %d\n",t->u,t->v);
     t=t->nxt;
 }
}

现在,让我们深入了解代码,以便更好地理解。

它是如何工作的...

考虑以下无向图:

图片

图 10.39

因为图有五个顶点,所以最小生成树将有四条边。克鲁斯卡尔算法的第一步是将图的边按照它们的权重升序排序:

Weight   Src    Dest
 1        1      2
 1        3      5
 2        1      5
 2        2      5
 2        3      4
 3        1      3
 3        2      4
 4        4      5

现在,我们将从先前的表中逐条取出一条边,如果它不会形成环,我们将它包含在最小生成树中。我们首先从边(12)开始。这条边中没有环;因此,它被包含在以下最小生成树中:

图片

图 10.40

表中的下一个边是(35)。这条边也不会形成环,所以它被包含在最小生成树中:

图片

图 10.41

接下来,选择边缘(15)。同样,这个边缘也没有形成循环,所以它被包含在最小生成树中:

图片

图 10.42

表格中的下一个边缘是(25),但它确实形成了一个循环,所以被舍弃了。表格中的下一个边缘是(34)。边缘(34)没有形成循环;因此,它被添加到最小生成树中:

图片

图 10.43

顶点的数量是 5,所以边的数量将是v-1,即 4,我们已经有 4 条边,所以我们的最小生成树是完整的。

在编译并运行kruskal.c程序后,我们得到一个类似于以下截图的输出:

图片

图 3.44

如您所见,我们在输出中使用了克鲁斯卡尔算法得到了邻接表表示和最小生成树。

第十一章:高级数据结构与算法

在本章中,我们将学习高级数据结构和算法。我们将学习如何使用栈、循环链表、双向链表和二叉树等结构及其遍历。

在本章中,我们将介绍以下食谱:

  • 使用单链表实现栈

  • 实现双向或双向链表

  • 实现循环链表

  • 递归地实现二叉搜索树并进行中序遍历

  • 非递归地遍历二叉树的后序

在我们深入研究食谱之前,了解我们将在本章以及本书其他食谱中使用的一些结构和相关术语对我们来说将是有帮助的。

栈是一种数据结构,其中所有插入和删除操作都在一端进行。进行插入和删除操作的一端称为栈顶tos)。栈也称为下推列表后进先出LIFO);也就是说,最后添加到栈中的项目将添加到所有较早项目的顶部,并将是第一个被取出的项目。

可以在栈上执行的操作如下:

  • Push:这是将值推入栈中。在将值推入栈之前,栈顶的值会增加以指向新位置,新值可以推入该位置。

  • Pop:这是弹出或获取栈中的值。栈顶的值或被顶指针指向的值从栈中取出。

  • Peep:这显示了栈顶的值,即栈所指向的值,而不从栈中取出该值。

双向链表(双向链表)

在双向或双向链表中,结构中使用两个指针,其中一个指针指向正向,另一个指针指向反向。这两个指针使我们能够以两种方式遍历链表,即以先进先出FIFO)顺序以及后进先出(LIFO)顺序。在单链表中,遍历只能在一个方向上进行。双向链表的节点看起来如下:

图片

如前图所示,有两个指针,nextprev(你可以给这些指针起任何你喜欢的名字)。next指针指向下一个节点,而prev指针指向其前一个节点。为了在两个方向上遍历双向链表,我们将使用另外两个称为startListendList的指针。startList指针被设置为指向第一个节点,而endList指针被设置为指向最后一个节点,以便在两个方向上遍历双向链表。

要按 FIFO 顺序遍历,我们从 startList 指针指向的节点开始遍历,借助 next 指针进一步移动。要按 LIFO 顺序遍历链表,我们从 endList 指针指向的节点开始遍历链表,然后借助 prev 指针向后移动。

由某些节点组成的双向链表可能看起来如下:

截图

注意,第一个节点的 prev 指针和最后一个节点的 next 指针被设置为 NULL。这些 NULL 值有助于终止遍历过程。

循环链表

在线性链表中,节点一个接一个地连接,除了第一个节点外,每个节点都有一个唯一的 predecessor 和 successor。最后一个节点被设置为指向 NULL 以指示链表的结束。但在循环链表的情况下,最后一个节点的下一个指针指向第一个节点,而不是指向 NULL。换句话说,循环链表没有 NULL 指针,如下面的图所示:

截图

循环链表相对于线性链表的优势在于,循环链表允许指针向反方向移动。在现实世界的应用中,循环链表被用于多个地方。例如,它可以在操作系统调度 CPU 时以轮询方式使用,它可以在歌曲播放列表中使用,也可以用于跟踪游戏中的用户。

二叉树

一棵树中所有节点都可以有两个孩子或兄弟(最多)的树称为二叉树。二叉树有以下特点:

  • 一棵树在级别 l 上最多有 2^l 个节点。

  • 如果一个二叉树在级别 l 上有 m 个节点,它在级别 l+1 上最多有 2m 个节点。

  • 一棵树包含 2d 个叶子节点,因此有 2d-1 个非叶子节点,其中 d 是它的深度。

  • 一个包含 n 个内部节点的二叉树有 (n+1) 个外部节点。

  • 一个包含 n 个节点的二叉树恰好有 n+1NULL 链接(见下面的截图):

截图

二叉搜索树

二叉搜索树是一种树,其中搜索一个元素的搜索时间是 O(log2n)(这比在二叉树中搜索一个元素的 O(n) 快)。但为了支持 O(log2n) 的搜索,我们需要向二叉树添加一个特殊属性:我们将所有值小于根节点值的节点放入其左子树,所有值大于根节点值的节点放入其右子树。

遍历树

遍历意味着访问树中的节点。遍历二叉树有三种方式:前序、中序和后序。由于遍历二叉树需要访问根节点及其左右子节点,这三种遍历方式仅在访问顺序上有所不同。使用递归方法定义的树遍历方法如下:

对于前序遍历,这些是步骤:

  1. 访问根节点

  2. 以前序遍历遍历左子树

  3. 以前序遍历遍历右子树

在前序遍历中,首先访问二叉树的根节点。

对于中序遍历,这些是步骤:

  1. 以中序遍历遍历左子树

  2. 访问根节点

  3. 以中序遍历遍历右子树

对于后序遍历,这些是步骤:

  1. 以后序遍历遍历左子树

  2. 以后序遍历遍历右子树

现在我们已经对这个章节中将要讨论的结构有了全面的介绍,我们可以开始我们的旅程。

使用单链表实现栈

在这个菜谱中,我们将学习如何实现一个具有 LIFO 结构的栈。LIFO 意味着最后添加到栈中的元素将是第一个被移除的。栈是任何编译器和操作系统中非常重要的组件。栈用于分支操作、递归以及许多其他系统级任务。栈可以使用数组以及通过链表实现。在这个菜谱中,我们将学习如何使用单链表实现栈。

如何做到这一点...

按照以下步骤使用链表实现栈:

  1. 定义一个名为node的结构。在这个结构中,除了用于存储栈内容的成员变量外,还定义了一个指针,该指针指向下一个节点。

  2. top指针初始化为NULL以指示栈当前为空。

  3. 显示菜单并询问用户是否要从栈中压入或弹出值。用户可以输入 1 表示他们想要将值压入栈中,或输入 2 表示他们想要从栈中弹出值。如果用户输入1,转到步骤 4。如果他们输入2,转到步骤 9。如果他们输入3,则表示他们想要退出程序,因此转到步骤 13

  4. 为新节点分配内存。

  5. 询问用户要压入的值并将该值分配给节点的数据成员。

  6. 调用push函数,将新节点的下一个指针设置为指向top

  7. top指针设置为指向其next指针指向的位置。

  8. 转到步骤 3以显示菜单。

  9. 检查top指针是否为NULL。如果是,则显示消息栈为空并转到步骤 3以显示菜单。如果top不是NULL,转到下一步。

  10. 设置一个临时指针temp,使其指向top指向的节点。

  11. top指针设置为指向其next指针指向的位置。

  12. temp所指向的节点作为出栈节点返回,并显示该出栈节点的数据成员。

  13. 退出程序。

使用链表实现栈的程序如下:

//stacklinkedlist.c

#include<stdio.h>

#include <stdlib.h>

struct node {
  int data;
  struct node * next;
};

void push(struct node * NewNode, struct node ** Top);
struct node * pop(struct node ** Top);

int main() {
  struct node * newNode, * top, * recNode;
  int n = 0;
  top = NULL;
  while (n != 3) {
    printf("\n1\. Pushing an element into the stack\n");
    printf("2\. Popping out an element from the stack\n");
    printf("3\. Quit\n");
    printf("Enter your choice 1/2/3:");
    scanf("%d", & n);
    switch (n) {
    case 1:
      newNode = (struct node * ) malloc(sizeof(struct node));
      printf("Enter the value to push: ");
      scanf("%d", & newNode - > data);
      push(newNode, & top);
      printf("Value %d is pushed to stack\n", newNode - > data);
      break;
    case 2:
      recNode = pop( & top);
      if (recNode == NULL) printf("Stack is empty\n");
      else
        printf("The value popped is %d\n", recNode - > data);
      break;
    }
  }
  return 0;
}
void push(struct node * NewNode, struct node ** Top) {
  NewNode - > next = * Top;
  * Top = NewNode;
}

struct node * pop(struct node ** Top) {
  struct node * temp;
  if ( * Top == NULL) return (NULL);
  else {
    temp = * Top;
    ( * Top) = ( * Top) - > next;
    return (temp);
  }
}

现在,让我们深入了解幕后,以便我们可以理解代码。

它是如何工作的...

首先,定义一个结构体,称为节点,它由两个成员组成:一个是数据,另一个是名为next的指针。因为我们希望我们的栈只存储整数值,所以结构体的数据成员被定义为整数,用于存储整数,而下一个指针用于连接其他节点。最初,top指针被设置为NULL

设置一个while循环来执行,循环内显示菜单。菜单设置为显示三个选项:1,将值推入栈中;2,从栈中弹出;3,退出。直到用户在菜单中输入 3,while循环将继续执行并继续显示菜单,提示用户输入所需的选项。如果用户输入 1 以将值推入栈中,则通过newNode创建一个新的节点。提示用户输入要推入栈中的值。假设用户输入的数据是 10。在这里,该值将被分配给newNode的数据成员,如下所示:

此后,调用push函数,并将newNodetop指针传递给它。在push函数中,将newNode的下一个指针设置为指向top指针,此时top指针为NULL,然后设置top指针指向 X,如下所示:

top指针必须始终指向最后一个插入的节点。因此,它被设置为指向newNode。完成push函数后,控制权返回到main函数,此时菜单将再次显示。

假设用户输入 1 以将另一个值推入栈中。再次,通过newNode创建一个新的节点。要求用户输入要推入的值。假设用户输入 20,则值 20 将被分配给newNode的数据成员。调用push函数,并将newNodetop指针传递给它。在这里,top指针指向之前推入的节点,如下所示:

push函数中,将newNode的下一个指针设置为指向top指针所指向的节点,如下所示:

然后,将top指针设置为指向newNode,如下所示:

执行push函数后,菜单将再次显示。假设用户想要从栈中pop出一个值。为此,他们将在菜单中输入 2。将调用pop函数,并将栈顶指针传递给它。在pop函数中,确保top指针不是NULL,因为如果是,这意味着栈已经为空;无法从空栈中弹出值。要从栈中获取值,我们将使用一个名为temp的临时指针。temp指针被设置为指向由top指针指向的节点:

图片

此后,top指针将移动到下一个节点,即其next指针所指向的节点。由temp指针指向的节点将被返回到main函数:

图片

main函数中,pop函数返回的节点被分配给recNode。首先,确认recNode不是NULL。然后,在屏幕上显示其数据成员的值。因此,20 将在屏幕上显示。

执行pop函数后,菜单将再次显示,询问用户输入所需的选项。假设用户按下 2 以从栈中弹出另一个值。再次调用pop函数。在pop函数中,我们检查top指针是否不是NULL并且它指向一个节点。因为top指针指向一个节点并且不是NULL,所以设置一个临时指针temp指向由top指针指向的节点:

图片

此后,将top指针设置为指向其next指针所指向的位置。topnext指针指向NULL,因此top指针将被设置为NULL,而由temp指针指向的节点将被返回到main函数。

main函数中,从pop函数返回的节点被分配给recNode指针。确认recNode不是指向NULL后,在屏幕上显示recNode的数据成员的值。因此,值 10 将出现在屏幕上。执行pop函数后,屏幕上再次显示菜单。

假设用户想要再次弹出栈。但在这个时候,我们知道栈是空的。当用户在菜单上按下 2 时,将调用 pop 函数。然而,由于 top 指针的值为 NULLpop 函数将返回一个 NULL 值给 main 函数。在 main 函数中,pop 函数返回的 NULL 值被分配给 recNode 指针。由于 recNode 指针被分配 NULL,屏幕上会显示一条消息“栈为空”。再次,菜单将显示,提示用户输入选择。输入 3 后,程序将终止。

程序使用 GCC 编译。因为没有错误出现在编译过程中,这意味着 stacklinkedlist.c 程序已成功编译成 stacklinkedlist.exe 文件。执行该文件后,我们得到一个菜单,提示我们从栈中压入或弹出,如下面的截图所示:

截图

当从栈中弹出时,你必须已经注意到栈是一个后进先出(LIFO)结构,最后压入的值是第一个被弹出的。

现在,让我们继续下一个菜谱!

实现双向或双向链表

在这个菜谱中,我们将学习如何创建双向链表以及如何以先进先出(FIFO)和后进先出(LIFO)的顺序遍历其元素。正如我们在本章引言中解释的那样,双向链表的节点由两个指针组成:一个指向前方,而另一个指向后方。指向前方的指针通常称为 next,用于指向下一个节点。另一个指向后方的指针通常称为 prev,用于指向前一个节点。

先进先出(FIFO)顺序的遍历意味着双向链表的元素以它们被添加到列表中的顺序显示。遍历是通过使用节点的 next 指针完成的。

后进先出(LIFO)顺序的遍历意味着元素以反向或倒序显示,并且这种遍历是通过 prev 指针完成的。

如何做这件事...

在这个双向链表中,我将使用两个指针,startListendList,其中 startList 将指向第一个节点,而 endList 将指向最后一个节点。startList 指针将帮助以先进先出(FIFO)的顺序遍历列表,而 endList 指针将帮助以后进先出(LIFO)的顺序遍历它。按照以下步骤创建双向链表并双向遍历:

  1. 定义一个名为 node 的结构体。为了存储双向链表的内容,在节点结构体中定义一个数据成员。定义两个指针,分别称为 nextprev

  2. 显示一个菜单,显示四个选项:1,创建双向链表;2,以 LIFO 顺序显示列表元素;3,以 FIFO 顺序显示元素;以及4,退出。如果用户输入 1,转到步骤 3。如果用户输入 2,转到步骤 10。如果用户输入 3,转到步骤 15。最后,如果用户输入 4,则意味着他们想要退出程序,因此转到步骤 19

  3. startList指针初始化为NULL

  4. 为新节点分配内存。

  5. 询问用户要添加到双向链表中的值。用户输入的值分配给节点的数据成员。

  6. 将节点的nextprev指针设置为NULL

  7. 如果这个节点是双向链表的第一个节点,将startList指针设置为指向新节点。如果这个节点不是第一个节点,不要干扰startList指针,让它指向它当前指向的节点。

  8. 如果这是双向链表的第一个节点,将endList指针设置为指向新节点。如果不是第一个节点,执行以下步骤:

    1. 将新节点的next指针设置为NULL

    2. 将新节点的prev指针设置为指向endList指向的节点。

    3. endListnext指针设置为指向新节点。

    4. endList设置为指向新节点。

  9. 询问用户是否需要向双向链表添加更多元素。如果用户想要添加更多,转到步骤 4;否则,通过转到步骤 2来显示菜单。

  10. 要以 LIFO 顺序显示链表,让temp指针指向endList指向的节点。

  11. 步骤 12步骤 13运行,直到temp指针达到NULL

  12. 显示temp指针指向的节点的数据成员。

  13. temp指针设置为指向其prev指针指向的位置。

  14. 双向链表的内容以 LIFO 顺序显示。现在,转到步骤 2以再次显示菜单。

  15. temp指针指向startList指针指向的节点。

  16. 如果temp指针不是NULL,显示temp指针指向的节点的数据成员。

  17. temp指向其next指针指向的节点。

  18. 如果temp已达到NULL,这意味着双向链表的所有节点都已遍历。现在,可以通过跳转到步骤 2来显示菜单。如果temp未达到NULL,则转到步骤 16以显示双向链表的其余元素。

  19. 退出程序。

实现双向或双向链表的程序如下:

//doublylinkedlist.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

struct node {
  int data;
  struct node * next, * prev;
};

struct node * startList, * endList;
void createdoubly();
void list_lifo();
void list_fifo();

int main() {
  int n = 0;
  while (n != 4) {
    printf("\n1\. Creating a doubly linked list\n");
    printf("2\. Displaying elements in L.I.F.O. order\n");
    printf("3\. Displaying elements in F.I.F.O. order\n");
    printf("4\. Quit\n");
    printf("Enter your choice 1/2/3/4: ");
    scanf("%d", & n);
    switch (n) {
    case 1:
      createdoubly();
      break;
    case 2:
      list_lifo();
      break;
    case 3:
      list_fifo();
      break;
    }
  }
  return 0;
}

void createdoubly() {
  char k[10];
  struct node * newNode;
  startList = NULL;
  strcpy(k, "yes");
  while (strcmp(k, "yes") == 0 || strcmp(k, "Yes") == 0) {
    if (startList == NULL) {
      newNode = (struct node * ) malloc(sizeof(struct node));
      printf("Enter the value to add: ");
      scanf("%d", & newNode - > data);
      newNode - > next = NULL;
      newNode - > prev = NULL;
      startList = newNode;
      endList = startList;
    } else {
      newNode = (struct node * ) malloc(sizeof(struct node));
      printf("Enter the value to add: ");
      scanf("%d", & newNode - > data);
      newNode - > next = NULL;
      newNode - > prev = endList;
      endList - > next = newNode;
      endList = newNode;
    }
    printf("Want to add more yes/no? ");
    scanf("%s", k);
  }
  printf("Doubly linked list is created\n");
}
void list_lifo() {
  struct node * temp;
  temp = endList;
  if (temp != NULL) {
    printf("The elements of the doubly linked list in L.I.F.O. order :\n");
    while (temp != NULL) {
      printf("%d\n", temp - > data);
      temp = temp - > prev;
    }
  } else
    printf("The doubly linked list is empty\n");
}

void list_fifo() {
  struct node * temp;
  temp = startList;
  printf("The elements of the doubly linked list in F.I.F.O. order: \n");
  while (temp != NULL) {
    printf("%d\n", temp - > data);
    temp = temp - > next;
  }
}

现在,让我们幕后看看,以便我们可以理解代码。

如何工作...

在实现双链表时,定义了一个结构体,称为节点,它由一个名为 data 的整数和两个指针 nextprev 组成。因为双链表可以从两端遍历——即正向或反向——所以需要两个指针。next 指针将指向它后面的节点,而 prev 指针将指向它前面的节点。

屏幕上显示了一个菜单,显示四个选项:1,用于创建双链表;2,用于以 LIFO 顺序显示双链表中的元素;3,用于以 FIFO 顺序显示元素;以及 4,用于退出程序。

假设用户输入了 1。createdoubly 函数将被调用。在这个函数中,startList 指针被设置为 NULL,一个字符串变量 k 被分配了 yes 字符串。设置了一个 while 循环,在 k 被分配了 yes 时执行。在这里,用户可以在被提示继续时输入 yes,以继续向双链表添加更多元素。startList 指针将被设置为指向双链表的第一个节点,而 endList 指针将被设置为指向最后一个节点。

添加第一个节点的过程与添加其余节点的过程不同。因此,在代码中创建了 if else 块。当创建第一个节点时 startListNULL,将执行 if 块;否则,将执行 else 块。在 if 块中,创建了一个名为 newNode 的新节点。用户被要求输入双链表的值。假设用户输入了值 10;这将分配给 newNode 的数据成员,并且 newNodenextprev 指针将被设置为 NULL

图片

startList 指针被设置为指向 newNode,而 endList 指针也被设置为指向 newNode

图片

endList 不会停留在第一个节点上;相反,它将继续前进并指向这个双链表的最后一个节点。在执行 if 块之后,用户会被询问是否需要添加更多节点。如果用户输入 yeswhile 循环将再次执行。现在,startList 不是 NULL,而是指向 newNode;因此,将执行 else 块,而不是 if 块。在 else 块中,创建了一个名为 newNode 的新节点。用户被提示输入要添加到双链表中的值。假设用户输入了值 20,这个值将被分配给 newNode 的数据成员:

图片

newNodeprev 指针被设置为指向 endList,而 newNodenext 指针被设置为 NULLendListnext 指针被设置为指向 newNode,如下所示:

图片

之后,endList指针被设置为指向newNode,但startList指针将保持指向第一个节点,如下所示:

图片

再次询问用户是否想要向双链表添加更多元素。假设用户不想向列表添加更多元素,因此他们输入的文本是no。文本no将被分配给k,因此while循环将终止。createdoubly函数结束,控制将返回到main函数。在main函数中,将显示上述四个选项的菜单。

假设用户输入 2 以按 LIFO 顺序显示双链表的元素。在这里,将调用list_lifo函数。在list_lifo函数中,使用了一个名为temp的临时指针,并将其设置为指向由endList指针所指向的最后节点:

图片

一个while循环被设置为执行,直到temp指针达到NULLtemp指针所指向的节点数据成员中的值将在屏幕上显示。在这里,屏幕上将显示一个值为 20。之后,temp指针被设置为指向其prev指针所指向的节点:

图片

再次检查temp指针的值。因为temp指针不是NULL,所以while循环将再次执行。在while循环内,temp指针所指向的节点数据成员中的值将在屏幕上显示。在这里,屏幕上将显示一个值为 10。之后,temp指针被设置为指向其prev指针所指向的节点。tempprev指针指向NULL,因此temp指针被设置为指向NULL。现在,因为temp指向NULLwhile循环将终止,list_lifo函数结束,控制返回到main函数。

main函数中,将再次显示菜单,询问用户输入所需的选项。现在,假设用户输入 3 以按 FIFO 顺序显示双链表的元素。输入 3 后,将调用list_fifo函数。在list_fifo函数中,temp指针被设置为指向由startList指针所指向的节点,如前所述。while循环被设置为执行,直到temp指针指向NULL。因为temp不是NULL,所以temp指针所指向的节点数据成员中的值将在屏幕上显示。在这里,屏幕上将显示一个值为 10。之后,temp指针被设置为指向其next指针所指向的节点,如下所示:

图片

因为 temp 指针仍然没有指向 NULL,所以 while 循环将再次执行。在 while 循环内部,显示由 temp 指针指向的节点中的数据成员的值;将显示一个值为 20 的值。再次,将 temp 指针设置为指向其下一个指针指向的节点。temp 的下一个指针指向一个 NULL 指针,因此 temp 将指向 NULL。因为 temp 指针指向 NULL,所以 while 循环将终止;因此,list_fifo 函数结束,控制返回到 main 函数。在这里,菜单再次显示,询问用户输入所需的选项。假设用户输入 4 以退出程序。输入 4 后,程序将终止。

使用 GCC 编译程序。因为没有在编译时出现错误,这意味着 doublylinkedlist.c 程序已成功编译成 doublylinkedlist.exe 文件。执行该文件时,我们得到一个菜单,要求用户输入创建双链表和遍历双链表(按 LIFO 和 FIFO 顺序)的选项。通过这样做,我们得到以下输出:

前面的截图显示了使用遍历其元素按 FIFO 和 LIFO 顺序的双向链表的好处。

实现循环链表

在这个菜谱中,我们将学习如何实现循环链表。线性链表和循环链表之间的区别在于,线性链表的最后一个节点指向 NULL,而循环链表的最后一个节点的指针指向第一个节点,因此允许指针以反向方向遍历。

如何做到这一点...

按照以下步骤实现循环链表:

  1. 定义一个名为 node 的结构体。为了在循环链表中存储数据,定义节点结构体中的一个数据成员。除了数据成员外,还定义一个指针,该指针将指向下一个节点。

  2. 一个名为 startList 的指针被初始化为 NULLstartList 指针将指定循环链表的起始位置。

  3. 显示一个菜单,并要求用户按下 1 以向循环链表中添加元素,2 以显示循环链表中的元素,以及 3 以退出程序。如果用户输入 1,则转到 步骤 4。如果他们输入 2,则转到 步骤 16。如果他们输入 3,则意味着他们想要退出程序,因此转到 步骤 23

  4. 提示用户指定他们想要添加到循环链表中的数字数量。设置一个循环,执行指定次数;也就是说,步骤 5步骤 14 将重复指定次数。

  5. 为新节点分配内存。

  6. 询问用户要添加到循环链表中的值。用户输入的值被分配给节点的数据成员。

  7. 如果 startListNULL——也就是说,如果是循环链表的第一个节点——则让 startList 指针指向一个新节点。

  8. 要使链表看起来是循环的,让 startList 的下一个指针指向 startList

  9. 如果 startList 不是 NULL——也就是说,如果不是循环链表的第一个节点——则遵循 步骤 10步骤 14

  10. temp 指针指向 startList

  11. 直到 tempnext 指针等于 startList,让 temp 指针指向其下一个指针指向的位置;也就是说,设置 temp 指针,使其指向循环链表的最后一个节点。

  12. 一旦 temp 指针达到循环链表的最后一个节点,将 temp 的下一个指针设置为指向新节点。

  13. 然后,将 temp 指针设置为指向新节点。

  14. temp 的下一个指针设置为指向 startLIst

  15. 跳转到 步骤 3 来显示菜单。

  16. 上一步确保 startList 不是 NULL。如果 startListNULL,这意味着循环链表为空。在这种情况下,会显示一条消息,通知用户循环链表为空。然后,控制跳转到 步骤 3 来显示菜单。

  17. 如果 startList 不是 NULL,则在屏幕上显示由 startList 指针指向的节点的数据成员。

  18. 设置一个临时指针 temp,使其指向 startList 的下一个指针指向的位置。

  19. 重复 步骤 20步骤 21,直到 temp 指针达到由 startList 指针指向的节点。

  20. 显示由 temp 数据成员指向的节点的内容。

  21. temp 指针设置为指向其下一个指针指向的位置。

  22. 跳转到 步骤 3 来显示菜单。

  23. 终止程序。

实现循环链表的程序如下:

//circularlinkedlist.c

#include <stdio.h>

#include <stdlib.h>

struct node {
  int data;
  struct node * next;
};

struct node * startList = NULL;

void addlist(struct node ** h);
void disp();

int main() {
  struct node * newNode;
  int n = 0, i, k;
  while (n != 3) {
    printf("\n1\. Adding elements to the circular linked list\n");
    printf("2\. Displaying elements of the circular linked list\n");
    printf("3\. Quit\n");
    printf("Enter your choice 1/2/3: ");
    scanf("%d", & n);
    switch (n) {
    case 1:
      printf("How many values are there ");
      scanf("%d", & k);
      printf("Enter %d values\n", k);
      for (i = 1; i <= k; i++) {
        newNode = (struct node * ) malloc(sizeof(struct node));
        scanf("%d", & newNode - > data);
        addlist( & newNode);
      }
      printf("Values added in Circular Linked List \n");
      break;
    case 2:
      disp();
      break;
    }
  }
  return 0;
}

void addlist(struct node ** NewNode) {
  struct node * temp;
  if (startList == NULL) {
    startList = * NewNode;
    startList - > next = startList;
  } else {
    temp = startList;
    while (temp - > next != startList)
      temp = temp - > next;
    temp - > next = * NewNode;
    temp = * NewNode;
    temp - > next = startList;
  }
}

void disp() {
  struct node * temp;
  if (startList == NULL)
    printf("The circular linked list is empty\n");
  else {
    printf("Following are the elements in circular linked list:\n");
    printf("%d\n", startList - > data);
    temp = startList - > next;
    while (temp != startList) {
      printf("%d\n", temp - > data);
      temp = temp - > next;
    }
  }
}

现在,让我们幕后看看,以便我们可以理解代码。

它是如何工作的...

定义了一个结构,称为节点,它由两个成员组成:一个整数和一个名为 next 的指针。我正在创建一个由整数组成的循环链表,这就是为什么我选择了整数成员。然而,你可以使用你想要的任何数量的成员,以及任何数据类型。

我们定义了一个名为 startList 的指针,并将其初始化为 NULLstartList 指针将用于指向循环链表的第一个节点。

屏幕上显示一个菜单,显示三个选项:1,向循环链表中添加元素;2,显示循环链表中的元素;3,退出。显然,第一步是向循环链表中添加元素。假设用户输入 1。输入 1 后,用户将被要求指定他们想在列表中输入多少个值。用户输入的限制将被分配给一个名为k的变量。假设用户想在列表中输入五个元素,将设置一个for循环运行五次。在for循环中,创建一个名为newNode的新节点。用户输入的值被分配给newNode的数据成员。假设用户输入的值是 10,它将被分配给newNode的数据成员,如下所示:

将调用addlist函数并将newNode作为参数传递给它。在addlist函数中,确认它是否是循环链表的第一个节点;也就是说,如果startListNULL,则将其设置为指向newNode

为了使其成为一个循环链表,将startList的下一个指针设置为指向startList本身:

addlist函数结束。控制权返回到主函数并继续执行for循环。在for循环中,创建一个newNode节点。用户输入的值被分配给newNode的数据成员。假设用户输入的值是 20,它将被分配给newNode的数据成员:

再次调用addlist函数并将newNode传递给它。在addlist函数中,因为startList指针不再是NULL,所以将执行else块。在else块中,设置一个临时指针temp指向startList。设置一个while循环,直到temp的下一个指针指向startList;也就是说,直到temp指针到达循环链表的最后一个节点,temp指针将不断移动,以便指向它的下一个节点。因为循环链表中只有一个节点,所以temp指针已经指向列表的最后一个节点:

一旦temp指针到达循环链表的最后一个节点,将temp的下一个指针设置为指向newNode

此后,将temp指针设置为指向newNode

最后,为了使链表看起来是循环的,将temp的下一个指针设置为指向startList

此过程将重复应用于环形链表的其余三个元素。假设输入的其他三个元素是 30、40 和 50,环形链表将如下所示:

在创建环形链表后,用户将再次看到显示菜单。假设用户想要显示环形链表的元素,他们将根据菜单选项输入一个值。输入值后,将调用disp函数。在disp函数中,确保startList指针是NULL。如果startList指针是NULL,则表示环形链表为空。在这种情况下,disp函数将在显示环形链表为空的消息后终止。如果startList指针不为空,则显示由startList指针指向的节点数据成员中的值;即屏幕上显示 10 的值。设置一个临时指针temp,使其指向由startList的下一个指针指向的节点:

设置一个while循环,直到temp指针达到由startList指针指向的节点。在while循环中,显示由temp指针指向的节点数据成员;即屏幕上显示 20 的值。之后,将temp指针设置为指向其下一个指针指向的节点。这样,while循环将继续执行并显示环形链表的所有元素。当while循环结束时,disp函数也结束。控制返回到main函数,菜单将再次显示。要退出程序,用户必须输入 3。输入 3 后,程序将终止。

使用 GCC 编译程序。因为没有错误出现在编译过程中,这意味着circularlinkedlist.c程序已成功编译成circularlinkedlist.exe文件。执行文件后,我们得到一个菜单,它不仅可以向环形链表添加元素,还可以显示它们。通过这样做,我们得到以下截图所示的输出:

哇!我们已经成功实现了环形链表。现在,让我们继续下一个菜谱!

递归地创建二叉搜索树并对其进行中序遍历

在这个菜谱中,我们将要求用户输入一些数字,并从这些数字构建一个二叉树。一旦创建了二叉树,就执行其中序遍历。这些步骤将分为两个部分:创建二叉树和中序遍历二叉树。

如何操作... – 二叉树

按照以下步骤创建二叉树:

  1. 创建一个具有以下结构的节点:用于存储树元素的data,一个指向树右子节点的right指针,以及一个指向树左子节点的left指针。

  2. 创建树的root节点。为此,为新节点分配内存空间,并将root指针设置为指向它。

  3. 提示用户输入树元素。用户输入的值被分配给root节点的数据成员。

  4. root节点的leftright指针被设置为NULL

  5. 根节点已创建。接下来,提示用户指定树中的元素数量。

  6. 重复步骤 7步骤 22,直到用户指定的元素数量。

  7. 为新节点分配内存空间,并将new指针设置为指向它。

  8. 提示用户输入树元素。用户输入的树元素被分配给新节点的数据成员。

  9. 新节点的leftright指针被设置为NULL

  10. 要将根节点连接到新节点,我们需要找到一个可以连接的位置。为此,设置temp指针,使其指向根节点。比较新节点和temp节点的数据成员中的值。

  11. 如果new ->data > temp->data,转到步骤 12;否则,转到步骤 16

  12. 如果temp的右链接是NULL——也就是说,如果temp节点的右侧没有子节点——则,新节点被添加到temp节点的右链接。

  13. 新节点被添加为temp节点的右子节点。跳转到步骤 7以添加更多树元素。

  14. 如果根的右链接不是NULL,则将temp指针设置为指向tempright指针指向的位置。

  15. 转到步骤 11进行更多比较。

  16. 如果new->data < root->data,转到步骤 17;否则,转到步骤 21

  17. 如果节点的左链接是NULL——也就是说,在temp节点的左侧没有子节点——则新节点被添加到左链接。

  18. 新节点被添加为temp节点的左子节点。跳转到步骤 7以添加更多树元素。

  19. 如果根的左链接不是NULL,则将temp指针设置为指向其left指针指向的位置。

  20. 转到步骤 11进行更多比较。

  21. 如果new->data = temp->data,这意味着新节点中的值是重复的,不能添加到树中。

  22. 转到步骤 7以添加更多树元素。

  23. 对于中序遍历,我们将遵循下一节提供的算法。inorder函数被递归调用,并将二叉树的根节点传递给此函数。

如何实现... – 树的中序遍历

因为它是递归形式,所以函数将被递归调用。函数如下:

inorder(node)

在这里,inorder是将被递归调用的函数,node是传递给它的二叉树节点。最初,节点将是二叉树的根,需要执行中序遍历。遵循以下步骤:

  1. 如果节点是NULL,则转到步骤 2;否则,返回调用函数。

  2. 使用相同的函数(即inorder函数)并传入节点的左子节点作为参数:

call inorder(node->leftchild)
  1. 显示节点中的内容:
display node->info
  1. 使用相同的函数本身(即inorder函数)并传入节点的右子节点作为参数:
   call inorder(node->rightchild)

创建二叉搜索树并在其中进行中序遍历的程序如下:

//binarysearchtree.c

#include <stdio.h>

#include <stdlib.h>

#define max 20
struct tree {
  int data;
  struct tree * right;
  struct tree * left;
};
void build(int Arr[], int Len);
struct tree * makeroot(int val);
void rightchild(struct tree * rootNode, int val);
void leftchild(struct tree * rootNode, int val);
void travino(struct tree * node);
int main() {
  int arr[max], i, len;
  printf("How many elements are there for making the binary search tree? ");
  scanf("%d", & len);
  printf("Enter %d elements in array \n", len);
  for (i = 0; i < len; i++)
    scanf("%d", & arr[i]);
  build(arr, len);
  return 0;
}

void build(int Arr[], int Len) {
  struct tree * temp, * rootNode;
  int j;
  rootNode = makeroot(Arr[0]);
  for (j = 1; j < Len; j++) {
    temp = rootNode;
    while (1) {
      if (Arr[j] < temp - > data) {
        if (temp - > left != NULL) {
          temp = temp - > left;
          continue;
        }
        leftchild(temp, Arr[j]);
      }
      if (Arr[j] > temp - > data) {
        if (temp - > right != NULL) {
          temp = temp - > right;
          continue;
        }
        rightchild(temp, Arr[j]);
      }
      break;
    }
  }
  printf("Binary Search Tree is created\n");
  printf("The inorder traversal of the tree is as follows:\n");
  travino(rootNode);
}

struct tree * makeroot(int val) {
  struct tree * rootNode;
  rootNode = (struct tree * ) malloc(sizeof(struct tree));
  rootNode - > data = val;
  rootNode - > right = NULL;
  rootNode - > left = NULL;
  return rootNode;
}

void leftchild(struct tree * rootNode, int val) {
  struct tree * newNode;
  newNode = (struct tree * ) malloc(sizeof(struct tree));
  newNode - > data = val;
  newNode - > left = NULL;
  newNode - > right = NULL;
  rootNode - > left = newNode;
}

void rightchild(struct tree * rootNode, int val) {
  struct tree * newNode;
  newNode = (struct tree * ) malloc(sizeof(struct tree));
  newNode - > data = val;
  newNode - > left = NULL;
  newNode - > right = NULL;
  rootNode - > right = newNode;
}

void travino(struct tree * node) {
  if (node != NULL) {
    travino(node - > left);
    printf("%d\t", node - > data);
    travino(node - > right);
  }
}

现在,让我们深入了解代码背后的工作原理。

工作原理... – 二叉树

我们创建了一个名为tree的结构,它包含以下成员:

  • data:用于存储整数数据的整数成员。在这里,我们假设我们的树只包含整数元素。

  • rightleft指针:这些用于分别指向左子节点和右子节点。

在内部,树将通过一个数组来维护;定义了一个大小为 20 的整数数组。为了我们的目的,让我们假设用户不会为树输入超过 20 个元素。然而,你可以始终将宏的大小增加到任何你想要的更大的数字。

提示用户指定他们想要为树输入的元素数量。假设用户想要为树输入七个元素;这里,值 7 将被分配给len变量。提示用户输入七个整数,他们输入的值将被分配到arr数组中,如下面的截图所示:

图片

调用build函数,并将包含树元素的数组arr及其长度len传递给它。在build函数中,我们需要创建一个树的根节点。为了创建树的根节点,调用makeroot函数并将数组arr的第一个元素作为参数传递给它。在makeroot函数中,创建一个名为rootNode的节点,并将第一个数组元素的值分配给其数据成员。因为此时树的根节点没有指向任何其他节点,所以根节点的左右子节点被设置为NULL

图片

makeroot 函数结束,并将 rootNode 返回给 build 函数。在 build 函数中,一个 temp 指针被设置为指向 rootNode。从索引 1 开始的所有数组元素都与 temp 节点的数据成员进行比较,即根节点。如果数组元素小于 temp 节点的数据成员,则该数组元素将被添加为根节点的左子节点。同样,如果数组元素大于 temp 节点的数据成员,它将被添加为根节点的右子节点,例如,如果第二个数组元素是 20,而根节点是 40。因为 20 小于 40,所以会检查 temp 节点的 left 指针是否为 NULL。因为 templeft 指针是 NULL,所以调用 leftchild 函数并将 20 传递给它。在 leftchild 函数中,创建了一个名为 newNode 的新节点。在这里,第二个数组元素(20)被分配给 newNode 的数据成员。newNodeleftright 指针被设置为 NULLtempleft 指针被设置为指向 newNode,如下所示:

图片

控制返回到 build 函数,其中 for 循环将选择下一个数组元素以构建树。假设下一个数组元素是 60。再次,设置一个 temp 指针指向根节点。将值 60 与根节点 40 进行比较。因为数组元素的值 60 大于根节点 40,所以检查根节点的右子节点。因为根节点的右子节点是 NULL,所以调用 rightchild 函数并将 temp 指针和数组元素 60 传递给它。在 rightchild 函数中,创建了一个名为 newNode 的新节点并将值 60 传递给它,该值被分配给其数据成员。newNodeleftright 指针被设置为 NULL。将 rootNoderight 指针设置为指向 newNode,如下所示:

图片

完成后,rightchild 函数的控制返回到 build 函数,其中 for 循环选择下一个数组元素以构建树。下一个数组元素是 80。设置一个临时指针 temp 指向根节点。将根节点 40 与要添加的新元素 80 进行比较。因为 80 大于 40,所以检查 temp 节点的右子节点。temp 节点的 right 指针不是 NULL,因此将 temp 指针设置为指向其右节点,如下所示:

图片

现在,再次检查 tempright 指针。此过程会重复进行,直到找到 tempright 指针为 NULL。60 的 right 指针是 NULL,因此调用 rightchild 函数,并将 temp、60 和新元素 80 传递给它。在 rightchild 函数中,创建了一个新节点,称为 newNode,并将值 80 分配给它。newNode 的左右指针设置为 NULL。将 tempright 指针设置为指向 newNode,如下所示:

完成对 rightchild 函数的调用后,控制权跳回到 build 函数,其中 for 循环选择下一个数组元素以构建树。使用完所有数组元素后,二叉搜索树将如下所示:

一旦创建了二叉搜索树,就调用 travino 函数对二叉树进行中序遍历,并将根节点传递给它。

它是如何工作的... – 树的中序遍历

travino 函数是一个递归函数。首先,它确保提供的节点不是 NULL。如果节点不是 NULL,则对 travino 函数进行递归调用,使用节点的左子节点。检查节点以确保它不是 NULL。如果不是,再次对 travino 函数进行递归调用,使用其左子节点。如果节点是 NULL,则在屏幕上显示节点数据成员中的值,并对 travino 函数进行递归调用,使用节点的右子节点。此过程会重复进行,直到屏幕上显示的所有节点都已访问。

中序遍历描述为 L,V,R,如下所示:

  • L 表示访问左子节点

  • V 表示访问显示其内容的节点

  • R 表示访问右子节点

在二叉树的每个节点上,应用 LVR 操作,从根节点开始。我们的二叉树已经创建,如下所示。在节点 40 上,应用了三个操作—L、V 和 R。L 表示访问其左子节点,因此我们移动到节点 40 的左子节点,但节点左边的两个操作,V 和 R,仍然需要在该节点上完成。因此,节点 40 被推入栈中,V 和 R 附着于其上:

节点 40 的左子节点是节点 20。再次,在节点 20,应用了三个操作——L、V 和 R。首先,访问 L(左子节点)。只剩下两个操作,V 和 R。因此,再次将节点 20 推入栈中,V 和 R 附加在其上。节点 20 的左子节点是节点 10。再次在这个节点上应用 L、V 和 R。由于它的左子节点是NULL,第二个操作 V 被应用;也就是说,节点被显示,或者我们可以说是被遍历。之后,我们转到它的右子节点。节点 10 的右子节点是NULL,并且由于在这个节点上已经应用了三个操作(L、V 和 R),因此它没有被推入栈中:

图片

现在,节点 20 从栈中弹出。它的两个操作,V 和 R,待执行。首先,它被访问(显示),然后我们转到它的右子节点:

图片

节点 20 的右子节点是 30。再次,在节点 30,应用了三个操作——L、V 和 R。首先,访问 L(左子节点)。由于它没有左子节点,第二个操作 V 被应用;也就是说,节点 30 被访问(显示),然后我们转到它的右子节点。它也没有右子节点,并且由于在这个节点上已经应用了三个操作(L、V 和 R),因此 30 没有被推入栈中:

图片

现在,节点 40 从栈中弹出。它的两个操作,V 和 R,待执行。首先,它被访问(显示),然后我们转到它的右子节点。节点 40 的右子节点是节点 60。在节点 60,应用了三个操作——L、V 和 R。首先,访问 L(左子节点)。V 和 R 被保留。在这里,节点 60 被推入栈中,V 和 R 附加在其上:

图片

节点 60 的左子节点是节点 50。再次在这个节点上应用 L、V 和 R。由于它的左子节点是NULL,第二个操作 V 被应用;也就是说,节点 50 被显示,或者我们可以说是被遍历。之后,我们转到它的右子节点。节点 50 的右子节点是NULL,并且由于在这个节点上已经应用了三个操作(L、V 和 R),因此它没有被推入栈中:

图片

现在,节点 60 从栈中弹出。它的两个操作,V 和 R,待执行。首先,它被访问(显示),然后我们转到它的右子节点。因此,访问过的节点将是 10、20、30、40、50 和 60。

节点 60 的右子节点是 80。再次,在节点 80,应用了三个操作——L、V 和 R。首先,访问 L(它的左子节点)。由于它没有左子节点,第二个操作 V 被应用;也就是说,节点 80 被访问(显示),然后我们转到它的右子节点。它也没有右子节点,并且由于在这个节点上已经应用了三个操作(L、V 和 R),因此 80 没有被推入栈中。

因此,树的最终中序遍历是 10、20、30、40、50、60 和 80。

程序使用 GCC 编译器以下语句编译:

D:\CAdvBook>GCC binarysearchtree.c - binarysearchtree

如以下截图所示,编译时没有出现错误。这意味着 binarysearchtree.c 程序已成功编译成名为 binarysearchtree.exe.exe 文件。让我们运行可执行文件并输入一些元素以创建二叉树并查看其中序遍历。通过这样做,我们得到以下输出:

图片

以非递归方式对二叉树进行后序遍历

在本菜谱中,我们将通过非递归函数调用以非递归方式对二叉树进行后序遍历。这将通过非递归调用函数来实现。

开始使用

要创建二叉树,请参考 递归创建二叉搜索树并对其进行中序遍历 的菜谱。我们将对在本菜谱中创建的同一二叉树执行后序遍历。

如何实现...

对于二叉树的后序遍历,我们需要在每个树节点上应用三个任务—L、R 和 V。这些任务如下:

  • L 表示访问左链接

  • R 表示访问右链接

  • V 表示访问节点

要找出 L、R 和 V 之间哪些任务待处理,哪些已执行,我们将使用两个栈:一个用于存储节点,另一个用于存储整数值 0 或 1。让我们回顾一下 0 和 1 的含义:

  • 值 0 表示 L 任务已完成,而 R 和 V 任务在该节点上待处理。

  • 值 1 表示节点上的 L 和 R 任务已完成,而 V 任务待处理。

按照以下步骤执行后序树遍历:

  1. 设置一个临时节点 temp 指向树的根节点。

  2. temp 所指向的节点推入 nodeArray,并将值 0 推入 valueArrayvalueArray 中的整数 0 表示 R 和 V 任务在该节点上待处理。

  3. 使 temp 节点指向其 left 指针所指向的节点。

  4. 如果 temp 没有指向 NULL,则转到 步骤 2

  5. 如果 temp 达到 NULL,则转到 步骤 6

  6. nodeArray 中弹出节点。

  7. valueArray 中弹出整数。

  8. 如果弹出的整数值是 1,则访问显示节点数据成员的节点。然后,转到 步骤 6

  9. 如果弹出的整数值是 0,则转到 步骤 10

  10. 将节点推入 nodeArray

  11. 将整数 1 推入 valueArray 以表示 L 和 R 操作已完成,而 V 操作待处理。

  12. 使 temp 指针指向其 right 指针所指向的位置。

  13. 如果 temp 指针没有达到 NULL,则转到 步骤 2

  14. 如果 temp 指针达到 NULL,则转到 步骤 6

创建二叉搜索树并在非递归方式遍历它的程序如下:

//postordernonrec.c

#include <stdio.h>

#include <stdlib.h>

struct tree {
  int data;
  struct tree * right;
  struct tree * left;
};

struct stackstruc {
  int valueArray[15];
  struct tree * nodeArray[15];
};

struct stackstruc stack;
int top = -1;

struct tree * makeroot(int val);
void rightchild(struct tree * rootNode, int val);
void leftchild(struct tree * rootNode, int val);
void nontravpost(struct tree * node);
void pushNode(struct tree * node, int val);
struct tree * popNode();
int popVal();

int main() {
  struct tree * temp, * rootNode;
  int val;
  printf("Enter elements of tree and 0 to quit\n");
  scanf("%d", & val);
  rootNode = makeroot(val);
  scanf("%d", & val);
  while (val != 0) {
    temp = rootNode;
    while (1) {
      if (val < temp - > data) {
        if (temp - > left != NULL) {
          temp = temp - > left;
          continue;
        }
        leftchild(temp, val);
      }
      if (val > temp - > data) {
        if (temp - > right != NULL) {
          temp = temp - > right;
          continue;
        }
        rightchild(temp, val);
      }
      break;
    }
    scanf("%d", & val);
  }
  printf("\nTraversal of tree in Postorder without using recursion: \n");
  nontravpost(rootNode);
}

struct tree * makeroot(int val) {
  struct tree * rootNode;
  rootNode = (struct tree * ) malloc(sizeof(struct tree));
  rootNode - > data = val;
  rootNode - > right = NULL;
  rootNode - > left = NULL;
  return rootNode;
}

void leftchild(struct tree * rootNode, int val) {
  struct tree * newNode;
  newNode = (struct tree * ) malloc(sizeof(struct tree));
  newNode - > data = val;
  newNode - > left = NULL;
  newNode - > right = NULL;
  rootNode - > left = newNode;
}

void rightchild(struct tree * rootNode, int val) {
  struct tree * newNode;
  newNode = (struct tree * ) malloc(sizeof(struct tree));
  newNode - > data = val;
  newNode - > left = NULL;
  newNode - > right = NULL;
  rootNode - > right = newNode;
}

void nontravpost(struct tree * node) {
  struct tree * temp;
  int val;
  temp = node;
  while (1) {
    while (temp != NULL) {
      pushNode(temp, 0);
      temp = temp - > left;
    }
    while (top >= 0) {
      temp = popNode();
      val = popVal();
      if (val == 0) {
        if (temp - > right != NULL) {
          pushNode(temp, 1);
          temp = temp - > right;
          break;
        }
      }
      printf("%d\n", temp - > data);
      continue;
    }
    if ((temp == NULL) || (top < 0)) break;
    else continue;
  }
}

void pushNode(struct tree * node, int val) {
  top++;
  stack.nodeArray[top] = node;
  stack.valueArray[top] = val;
}

struct tree * popNode() {
  return (stack.nodeArray[top]);
}

int popVal() {
  return (stack.valueArray[top--]);
}

现在,让我们深入了解代码背后的原理。

它是如何工作的...

后序遍历需要将 L、R 和 V 任务应用于二叉树的每个节点。在这里,L 表示访问左子树,R 表示访问右子树,V 表示访问显示其内容的节点。

问题是,我们如何知道已经对一个节点执行了哪些任务,以及哪些任务尚未执行?为了做到这一点,我们将使用两个数组,nodeArrayvalueArraynodeArray包含要执行任务的节点,而valueArray用于指示相应节点上留下的任务。valueArray可以有以下两个值:

  • 值 0:这表示已遍历节点的左链接,并且有两个任务待完成:遍历由其right指针指向的节点和访问节点。

  • 值 1:这表示由其right指针指向的节点已被遍历。只有访问节点的任务尚未完成。

一旦创建了二叉搜索树,就会调用nontravpost函数进行二叉树的后序遍历,并将根节点作为参数传递给函数。nontravpost函数是一个非递归函数。

设置一个临时指针temp指向根节点。设置一个while循环,直到temp不是NULL为止。在while循环中,会调用pushNode函数,并将temp指向的节点及其值 0 传递给它。

pushNode函数中,初始化为-1 的top值递增到 0,并将节点 40 和值 0 推入由top(索引位置,0)指向的nodeArrayvalueArray数组中:

图片

pushNode函数结束,控制跳转回nontravpost函数,其中temp指针被设置为指向其左侧节点。templeft指针指向节点 20,因此temp现在指向节点 20。while循环将继续执行,直到temp指针达到NULL指针。再次强调,在while循环中,会调用pushNode函数,并将节点 20 和值 0 传递给它。在pushNode函数中,top指针的值递增到 1,并将节点 20 和值 0 推入nodeArray[1]valueArray[1]数组索引位置,如下所示:

图片

此过程会重复进行,直到temp节点的左侧节点。在节点 20 的左侧是节点 10。在推入节点 10 后,nodeArrayvalueArray数组将如下所示:

图片

因为temp已经达到NULL指针,第一个while循环将终止,并且当top的值大于或等于 0 时,将执行下一个while循环。调用popNode函数,该函数返回由top索引指向的nodeArray数组中的节点。当前top索引的值为 2,因此访问索引位置nodeArray[2]处的节点,即 10,并将其返回给nontravpost函数。在nontravpost函数中,节点 10 将被分配给temp指针。接下来,调用popVal函数,该函数返回由top索引指向的valueArray数组中的值。这发生在valueArray[2]索引位置。即,popVal函数从valueArray[2]索引位置返回值 0,并将其分配给val变量。此时top的值减少到 1。因为val变量中的值是 0,nontravpost函数中执行了一个if块。该if块检查由temp指针指向的节点的右子节点是否不是NULL;如果是,则调用pushNode函数,并将由temp指向的节点,即 10 和整数值 1,作为参数传递给它。

pushNode函数中,top的值增加至 2,节点 10 和值 1 分别被推入nodeArray[2]valueArray[2]索引位置:

执行pushNode函数后,控制权跳回nontravpost函数,其中temp指针被设置为指向其right指针指向的位置。但是,因为temp的右指针是NULLwhile循环将中断,并将由temp指向的节点(即 10)的数据成员显示在屏幕上。

再次,while循环将执行,popNodepopVal函数将执行以弹出节点 20 和值 0。节点 20 将由temp指针指向。因为被弹出的值是 0,所以搜索由temp指向的节点的右指针。如果节点 20 的右指针指向节点 30,则调用pushNode函数,并将节点 20 及其值 1 推入:

接下来,将temp指针设置为指向其right指针指向的位置,即节点 30。调用pushNode函数,并将节点 30 和整数值 0 分别推入nodeArrayvalueArray数组:

此过程将重复进行,直到栈为空。

使用以下语句使用 GCC 编译程序:

D:\CAdvBook>GCC postordernonrec.c - postordernonrec

如果编译过程中没有出现错误,那么postordernonrec.c程序已成功编译成postordernonrec.exe文件。让我们运行这个文件,并输入一些新元素来构建一个二叉树,并使用非递归方法获取其后序遍历。通过这样做,我们将得到以下输出:

参见

要了解如何使用数组实现队列和循环队列,以及如何使用循环队列实现出队操作,请访问此链接中的附录 Bgithub.com/PacktPublishing/Practical-C-Programming/blob/master/Appendix%20B.pdf

第十二章:利用图形进行创意

OpenGL(代表Open Graphics Library)是一个跨平台的应用程序接口API),用于渲染二维和三维图形;它独立于操作系统工作。它提供了一些内置例程用于显示图形以及应用特殊效果、抗锯齿和不同的变换。

OpenGL 有一个名为OpenGL Utility ToolkitGLUT)的库,但它已经不支持好几年了。FreeGLUT 是一个免费的开源软件,作为其替代品。GLUT 在图形应用中非常受欢迎,因为它高度可移植且非常简单易用。它有一个大型的函数库,用于创建窗口、不同的图形形状、事件处理等。如果你电脑上没有安装 FreeGLUT,并且你的电脑上运行的是 Windows 操作系统,你可以下载freeglut 3.0.0用于 MinGW 并提取它。在 Ubuntu 上,你需要输入以下命令来安装 FreeGLUT:

sudo apt-get install freeglut3-dev

在本章中,我们将学习以下食谱:

  • 画四个图形形状

  • 画圆

  • 在两个鼠标点击之间画线

  • 根据提供的值制作条形图

  • 制作一个动画弹跳球

OpenGL 函数列表

在我们深入探讨食谱之前,让我们快速概述一下本章中我们将使用的一些 OpenGL 函数。以下是一些最常用的 OpenGL 函数:

功能 描述
glutInit 用于初始化 GLUT。
glutCreateWindow 用于创建顶层窗口。在创建窗口时,你可以提供窗口名称作为标签。
glutInitWindowSize 用于定义窗口大小。在定义窗口大小时,窗口的宽度和高度以像素为单位指定。
void glutInitWindowPosition 用于设置初始窗口位置。窗口的xy位置以像素为单位指定。
glutDisplayFunc 用于指定要执行的回调函数以在当前窗口中显示图形。为了在窗口中重新显示内容,也会执行指定的回调函数。
glutMainLoop 这是 GLUT 事件处理循环的入口点。
glClearColor 用于指定颜色缓冲区的清除值。你需要指定在清除颜色缓冲区时使用的红色、绿色、蓝色和 alpha 值。初始值都是 0。
glClear 用于将缓冲区清除到预设值。可以使用某些掩码来指定要清除的缓冲区。以下是可以使用的三个掩码。
GL_COLOR_BUFFER_BIT 这个掩码代表当前用于应用颜色的缓冲区。
GL_DEPTH_BUFFER_BIT 这个掩码代表深度缓冲区。
GL_STENCIL_BUFFER_BIT 这个掩码代表模板缓冲区。
glBegin 用于分组导致特定形状的语句。您可以通过在此分组语句内分组所需的顶点来创建不同的形状,例如点、线、三角形、矩形等。您可以通过指定以下任何模式来指定要创建的形状:GL_POINTSGL_LINESGL_LINE_STRIPGL_LINE_LOOPGL_TRIANGLESGL_TRIANGLE_STRIPGL_TRIANGLE_FANGL_QUADSGL_QUAD_STRIPGL_POLYGON
glEnd 用于结束语句组。
glColor3f 用于设置绘图时的当前颜色。可以指定红色、绿色和蓝色(按此严格顺序)的值来设置颜色。这些颜色的值介于 0 和 1 之间,其中 0 是最低强度,1 是最高强度。
glVertex 用于指定点、线和多边形顶点的坐标。此函数必须位于 glBegin/glEnd 对之间。glVertex 后可能跟有 2、3 或 4 的后缀,具体取决于定义顶点所需的坐标数量。例如,如果需要两个坐标 xy 来指定顶点,则会在 glVertex 后添加一个值为 2 的后缀,使其变为 glVertex2。同样,如果需要 3 和 4 个坐标来指定顶点,则可以分别添加 3 和 4 的后缀。此外,还可以添加一个后缀,如 sifd,如果顶点坐标分别是 shortintfloatdouble 数据类型。例如,可以使用 glVertex2f() 来指定具有 xy 坐标的顶点,坐标值将是 float 数据类型。
glLineWidth 用于指定要绘制的线的宽度。线的宽度可以用像素来指定。默认宽度为 1。
glPointSize 用于指定光栅化点的直径。默认直径为 1。
glFlush 命令有时会根据资源利用率和网络状况进行缓冲。glFlush 函数清空所有缓冲区,并确保命令尽可能早地执行。
glutSwapBuffers 此函数用于交换前缓冲区与后缓冲区。前缓冲区显示屏幕上的图像或帧,后缓冲区是图像(或帧)尚未渲染的地方。一旦图像或帧在后缓冲区中渲染,则此函数交换前后缓冲区,显示现在已在后缓冲区中准备好的图像。
glutReshapeFunc 用于指定当前窗口的 reshape 回调函数。在以下情况下自动调用该函数:当窗口被调整大小、在窗口第一次显示回调之前以及窗口创建之后。
glViewport 用于设置视口,即我们希望渲染图像出现的窗口附近区域。该函数接收四个参数。前两个参数表示视口矩形的左下角,以像素为单位。第三个和第四个参数表示视口的宽度和高度。视口的宽度和高度通常设置为与窗口尺寸相等或更小。
glMatrixMode 用于指定当前矩阵是哪个。顶点是基于矩阵的当前状态进行渲染的,因此必须选择一个矩阵以满足我们的需求。以下有两个主要选项。
GL_MODELVIEW 这是默认的矩阵选项。当用户想要执行平移、旋转等类似操作时使用此选项。
GL_PROJECTION 当用户想要执行平行投影、透视投影等操作时使用此选项。
glLoadIdentity 用于用单位矩阵替换当前矩阵。
gluOrtho2D 用于设置二维正交视图区域。该函数接收四个参数。前两个坐标表示左右垂直裁剪平面。最后两个指定底部和顶部的水平裁剪平面坐标。
glutMouseFunc 用于设置当前窗口的鼠标回调函数。也就是说,每当鼠标按钮被按下或释放时,每个动作都会调用鼠标回调函数。在回调函数中,以下三个参数会自动传递。
button 它代表三个按钮中的任何一个,GLUT_LEFT_BUTTONGLUT_MIDDLE_BUTTONGLUT_RIGHT_BUTTON,具体取决于哪个鼠标按钮被按下。
state 状态可以是 GLUT_UPGLUT_DOWN,具体取决于回调是因为鼠标释放还是鼠标按下而触发的。
xy 表示鼠标按钮状态改变时窗口的相对坐标。
glutIdleFunc 用于设置全局空闲回调,主要用于执行后台处理任务。即使没有事件发生,空闲回调也会持续调用。将 NULL 参数发送到该函数以禁用空闲回调的生成。

您需要初始化X 窗口系统X11)以进行图形处理。X11 提供了一个 GUI 环境,即它允许显示窗口和图形,并提供了一个与鼠标和键盘交互的环境。启动 X11 的命令是 xinit 命令。

绘制四个图形形状

在本教程中,我们将学习绘制四种不同的图形:正方形、三角形、点和线。

如何操作...

制作不同图形形状的步骤如下:

  1. 初始化 GLUT,定义窗口大小,创建窗口,并设置窗口位置。

  2. 定义将在窗口显示后自动调用的回调函数。

  3. 要绘制正方形,首先,定义它的颜色。

  4. 通过定义其四个顶点并将它们包含在glBeginglEnd语句中,使用GL_QUADS关键字绘制一个正方形。

  5. 要绘制线条,设置线条的宽度和颜色。

  6. 使用GL_LINES关键字在glBeginglEnd之间将一对顶点分组来绘制一条线。

  7. 要绘制点,设置点的大小为 3 px,并设置它们的颜色。

  8. 顶点是点必须显示的位置。将它们分组为glBeginglEnd语句对,并使用GL_POINTS关键字。

  9. 要绘制三角形,将三个顶点分组在glBeginglEnd语句中,并使用GL_TRIANGLES关键字。

  10. 调用glFlush函数来清空所有缓冲的语句,并快速绘制形状。

绘制前面四个形状的程序如下:

//opengldrawshapes.c

#include <GL/glut.h>

void drawshapes() {
  glClearColor(0.0 f, 0.0 f, 0.0 f, 1.0 f);
  /* Making background color black as first 
   All the 3 arguments R, G, B are 0.0 */
  glClear(GL_COLOR_BUFFER_BIT);
  glBegin(GL_QUADS);
  glColor3f(0.0 f, 0.0 f, 1.0 f);
  /* Making picture color blue (in RGB mode), as third argument is 1\. */
  glVertex2f(0.0 f, 0.0 f);
  glVertex2f(0.0 f, .75 f);
  glVertex2f(-.75 f, .75 f);
  glVertex2f(-.75 f, 0.0 f);
  glEnd();
  glLineWidth(2.0);
  glColor3f(1.0, 0.0, 0.0);
  glBegin(GL_LINES);
  glVertex2f(-0.5, -0.5);
  glVertex2f(0.5, -0.5);
  glEnd();
  glColor3f(1.0, 0.0, 0.0);
  glPointSize(3.0);
  /* Width of point size is set to 3 pixel */
  glBegin(GL_POINTS);
  glVertex2f(-.25 f, -0.25 f);
  glVertex2f(0.25 f, -0.25 f);
  glEnd();
  glBegin(GL_TRIANGLES);
  glColor3f(0, 1, 0);
  glVertex2f(0, 0);
  glVertex2f(.5, .5);
  glVertex2f(1, 0);
  glEnd();
  glFlush();
}

int main(int argc, char ** argv) {
  glutInit( & argc, argv);
  glutCreateWindow("Drawing some shapes");
  /* Giving title to the window */
  glutInitWindowSize(1500, 1500);
  /* Defining the window size that is width and height of window */
  glutInitWindowPosition(0, 0);
  glutDisplayFunc(drawshapes);
  glutMainLoop();
  return 0;
}

现在,让我们幕后了解代码以更好地理解它。

它是如何工作的...

第一步,正如预期的那样,是初始化 GLUT,然后创建一个顶层窗口,为窗口提供的标签是绘制一些形状。然而,你可以给它任何标签。窗口的宽度定义为 1,500 px,高度为 1,500 px。窗口的初始位置设置为 0,0,即x=0y=0的坐标。drawshapes回调函数被调用来在窗口中显示不同的形状。

drawshapes函数中,清除颜色缓冲区的值,然后清除缓冲区以预设值。

我们要绘制的第一个形状是一个正方形,因此绘制正方形的语句组被包含在glBeginglEnd语句中。GL_QUADS关键字与glBegin语句一起提供,因为四边形指的是由 4 个顶点组成的任何形状。调用glColor3f函数创建一个填充蓝色的正方形。提供四组顶点来形成一个正方形。一个顶点由xy坐标组成。

接下来,我们将绘制线条。调用glLineWidth函数来指定要绘制的线条宽度为 2 px。调用glColor3f函数使线条以红色显示。使用GL_LINES关键字在glBeginglEnd之间将两个顶点分组来绘制一条线。

接下来,我们将绘制两个点。为了使点清晰可见,点的大小设置为 3 px,点将被绘制的颜色设置为红色(或任何颜色,除了黑色)。我们想要显示点的两个顶点在将它们分组为glBeginglEnd语句对之后提供。使用GL_POINTS关键字与glBegin语句一起提供来绘制点。

最后,我们通过将三个三角形顶点组合到 glBeginglEnd 语句中来绘制三角形。将 GL_TRIANGLES 关键字与 glBegin 一起提供,以指示组中指定的顶点是为了绘制三角形。调用 glColor3f 确保三角形将被填充为绿色。

最后,调用 glFlush 函数以清空所有缓冲语句并快速执行它们以显示所需的形状。

要编译程序,我们需要打开命令提示符,并将目录更改为程序保存的文件夹。然后,在命令提示符中执行 xinit 命令以启动 X 服务器(X11)。

一旦 X 服务器启动,给出以下命令来编译程序。记住,在编译程序时,程序必须与 -lGL -lGLU -lglut 链接。

语法如下:

gcc filename.c -lGL -lGLU -lglut 

这里,filename.c 是文件名。

我们将使用以下命令来编译我们的程序:

gcc opengldrawshapes.c -lGL -lGLU -lglut -lm -o opengldrawshapes

如果没有错误出现,这意味着 opengldrawshapes.c 程序已成功编译成可执行文件:opengldrawshapes.exe。此文件使用以下命令执行:

$./opengldrawshapes

我们将得到如下截图所示的输出:

图片

图 12.1

哇!我们已经成功绘制了四种不同的图形形状:一个正方形、一个三角形、一些点和一条线。现在让我们继续到下一个菜谱!

绘制圆

绘制圆的过程与其他图形形状完全不同,因此它有自己的专用菜谱。它需要一个 for 循环来在 0 到 360 度绘制小点或线条。所以,让我们学习如何绘制圆。

如何操作...

绘制圆的步骤如下:

  1. 初始化 GLUT,定义顶级窗口的大小,并创建它。同时,设置窗口的初始位置以显示我们的圆。

  2. 定义一个在创建窗口后自动调用的回调函数。

  3. 在回调函数中,清除颜色缓冲区,并设置显示圆的颜色。

  4. 绘制圆的语句被包含在 glBeginglEnd 函数对中,并带有 GL_LINE_LOOP 关键字。

  5. 使用 for 循环从 0 到 360 绘制小线条,以形成圆形的形状。

绘制圆的程序如下:

//opengldrawshapes2.c

#include <GL/glut.h> 
#include<math.h> 
#define pi 3.142857 

void drawshapes() { 
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f); 
    glClear(GL_COLOR_BUFFER_BIT);  
    glColor3f(0.0f, 1.0f, 0.0f); 
    glBegin(GL_LINE_LOOP); 
        for (int i=0; i <360; i++) 
        { 
            float angle = i*pi/180; 
            glVertex2f(cos(angle)*0.5,sin(angle)*0.5);                                              }
    glEnd();
    glFlush(); 
} 

int main(int argc, char** argv) {
    glutInit(&argc, argv);    
    glutCreateWindow("Drawing some shapes"); 
    glutInitWindowSize(1500, 1500);  
    glutInitWindowPosition(0, 0);
    glutDisplayFunc(drawshapes); 
    glutMainLoop();     
    return 0; 
}

现在,让我们深入了解代码以更好地理解它。

它是如何工作的...

GLUT 已初始化,并创建了一个带有标签 Drawing some shapes 的顶级窗口。窗口的大小定义为 1,500 像素宽和 1,500 像素高。窗口的初始位置设置为 0, 0,即 x=0y=0 坐标位置。drawshapes 回调函数被调用来在窗口中绘制圆。

drawshapes 函数中,清除颜色缓冲区的值,然后清除缓冲区到预设值。调用 glColor3f 函数来设置我们想要绘制的圆的颜色。我已经将颜色设置为绿色来绘制圆,但你可以选择任何颜色。一组用于绘制圆的语句被收集在 glBeginglEnd 函数之间。glBegin 函数被提供给 GL_LINE_LOOP 关键字,表示我们将要绘制的圆将由几条小线组成。

glBeginglEnd 函数内,使用了一个从 0 到 360 的 for 循环来执行;也就是说,将在 0 到 360 度的每个度数处绘制一条非常小的线,以给出圆形的形状。首先将度数转换为弧度,然后在顶点位置绘制线条,cos(角度) * 半径,sin(角度) 半径*.当在每度处绘制这样的小线时,它将在屏幕上呈现出圆形的外观。

要编译程序,启动 X 服务器,并给出以下命令来编译程序:

gcc opengldrawshapes2.c -lGL -lGLU -lglut -lm -o opengldrawshapes2

如果没有出现错误,这意味着 opengldrawshapes2.c 程序已成功编译成可执行文件:opengldrawshapes2.exe。此文件使用以下命令执行:

$./opengldrawshapes2

我们将得到以下截图所示的输出:

图 12.2

哇!我们已经成功学习了如何绘制圆。现在让我们继续下一个菜谱!

在两个鼠标点击之间绘制线条

在这个菜谱中,我们将学习如何在两个鼠标点击之间绘制线条。鼠标点击被认为是按下鼠标按钮并释放它的过程。你可以在一对鼠标按下和释放事件之间绘制任意数量的线条。

如何做到这一点...

以下是在两个鼠标点击之间绘制线条的步骤:

  1. 初始化 GLUT,定义顶级窗口的大小,并显示窗口。

  2. 定义一个 drawLine 回调函数,当发生任何鼠标点击事件时绘制线条。

  3. drawLine 函数中,指定了清除缓冲区的清除值。

  4. 调用 glutSwapBuffers() 函数以交换前后缓冲区,显示后缓冲区中渲染的任何帧,并准备显示。

  5. 调用 glutReshapeFunc 函数来指定当窗口形状改变时将自动调用的重绘线条的回调函数。

  6. 由于线条的顶点是基于矩阵的当前状态渲染的,因此设置了一个矩阵作为当前矩阵,用于视图和建模变换。

  7. 还设置了一个二维正交视图区域。

  8. 设置了一个名为 mouseEvents 的鼠标回调函数。每当鼠标按钮被按下或释放时,都会调用回调。

  9. 根据鼠标按钮按下和释放的坐标,将调用 drawLine 函数来绘制两个坐标之间的线条。

以下是在两个鼠标点击之间绘制线条的程序:

//openglmouseclick.c

#include <GL/glut.h> 

int noOfClicks = 0; 
int coord[2][2]; 
int leftPressed = 0; 

void drawLine(void) 
{ 
    glClearColor(0.0, 0.0, 0.0, 1.0);  
    glClear(GL_COLOR_BUFFER_BIT); 
    glBegin(GL_LINES); 
        for(int i=0; i<noOfClicks; i++) {
            glVertex2f(coord[i][0],coord[i][1]); 
        } 
    glEnd(); 
    glutSwapBuffers(); 
}  

void projection(int width, int height) 
{ 
    glViewport(0, 0, width, height); 
    glMatrixMode(GL_PROJECTION); 
    glLoadIdentity(); 
    gluOrtho2D(0, width, height, 0); 
    glMatrixMode(GL_MODELVIEW); 
} 

void mouseEvents(int button, int state, int x, int y) 
{ 
    switch (button) {
        case GLUT_LEFT_BUTTON:
            if (state == GLUT_DOWN) {
                leftPressed = 1;
            }
            if (state == GLUT_UP) { 
                if(leftPressed) {                                                                                                                        coord[noOfClicks][0]=x;                                                                                                     coord[noOfClicks][1]=y;                                                                                                     noOfClicks++;                                                                                                     leftPressed = 0;                                                                                 }                                                                                 glutIdleFunc(NULL);                                                
            }  
            break;                                 
        default:                                                 
            break;                 
    } 
    drawLine(); 
} 

int main(int argc, char **argv) 
{ 
    glutInit(&argc, argv); 
    glutInitWindowSize(1000, 1000); 
    glutCreateWindow("Displaying lines between two mouse clicks"); 
    glutDisplayFunc(drawLine); 
    glutReshapeFunc(projection); 
    glutMouseFunc(mouseEvents); 
    glutMainLoop(); 
    return 0;            
}

现在,让我们幕后了解代码,以便更好地理解。

它是如何工作的...

GLUT 被初始化,并创建了一个带有标签Displaying lines between two mouse clicks的顶层窗口。窗口大小被指定为 1,000 像素宽和 1,000 像素高。如果发生了任何鼠标点击事件,将调用 drawLine 回调函数来绘制线条。

drawLine 函数中,指定了清除缓冲区的清除值。此外,缓冲区被清除到预设值,以便可以应用颜色。由于尚未发生鼠标点击,全局变量 noOfClicks 的值为 0,因此目前不会绘制任何线条。

glutSwapBuffers() 函数被调用以交换前后缓冲区,以便显示在后台缓冲区中渲染并准备好显示的任何帧。由于尚未进行鼠标点击,此函数将不会产生任何效果。

然后,调用 glutReshapeFunc 函数来指定当前窗口的 reshape 回调函数。每当窗口被调整大小时,将自动调用回调函数 projection,在窗口的第一个显示回调之前和窗口创建之后。在投影回调中,设置一个视口来定义我们想要绘制线条的邻近区域。之后,设置一个矩阵作为当前矩阵,用于视图和建模变换。此外,基于矩阵的当前状态渲染顶点,因此相应地选择矩阵。

此外,还设置了一个二维正交视区。将鼠标回调函数命名为 mouseEvents,因此每当鼠标按钮被按下或释放时,mouseEvents 回调函数将自动被调用。在回调函数中,传递有关哪个鼠标按钮被按下以及鼠标按钮是被按下还是被释放的信息。同时,鼠标动作发生的 xy 坐标也被传递给 mouseEvents 回调函数。

mouseEvents 函数中,首先检查是否按下了左鼠标按钮。如果是,则拾取鼠标按钮释放的位置,以及该位置的 xy 坐标,并将它们分配给 coord 数组。基本上,必须先按下鼠标按钮然后释放,才能存储坐标值。当观察到两次鼠标点击和释放时,将调用 drawLine 函数来绘制两个坐标之间的线条。

要编译程序,启动 X 服务器,并给出以下命令来编译程序:

gcc openglmouseclick.c -lGL -lGLU -lglut -lm -o openglmouseclick

如果没有出现错误,这意味着 openglmouseclick.c 程序已成功编译成可执行文件:openglmouseclick.exe。此文件使用以下命令执行:

$./openglmouseclick

我们将得到以下截图所示的输出:

图 12.3

一旦实现了这个功能,你可以绘制任意多的线条。

现在让我们继续下一个教程!

制作条形图

在本教程中,我们将学习如何绘制条形图。假设我们有一家公司在过去三年中利润增长百分比的统计数据。我们将将该利润增长百分比分配给一个数组,然后基于数组中的值,在屏幕上绘制一个包含三个条形的条形图。

如何实现...

使用数组中定义的值绘制条形图的以下步骤:

  1. 初始化 GLUT,定义顶级窗口的大小,设置其初始显示位置,并在屏幕上显示窗口。

  2. 定义一个回调函数,该函数在创建用于绘制条形图的窗口后自动调用。

  3. 在回调函数中定义了一个数组,该数组定义了条形图的高度。条形图的宽度固定为 2 像素。

  4. 设置了一个二维正交视图区域,即设置了水平和垂直裁剪平面的坐标。

  5. 为了显示水平和垂直的 xy 轴,将两条线的顶点分组在 glBeginglEnd 对配中,使用 GL_LINES 关键字。

  6. 为了显示三个条形,设置了一个 for 循环以执行三次。为了显示并排的条形,计算下一个条形的 x 轴。每个条形的高度基于第 3 步中定义的数组计算。

  7. 条形图使用与 glBeginglEnd 对配的 GL_POLYGON 关键字分组形成的四个顶点来显示。

基于数组中的值绘制条形图的程序如下:

//opengldrawbar.c

#include <GL/glut.h> 

void display(){ 
    float x,y,width, result[] = {10.0, 15.0, 5.0};
    int i, barCount = 3;
    x=1.0; 
    y = 0.0; 
    width = 2.0; 
    glColor3f(1.0, 0.0, 0.0); 
    glClearColor(1.0, 1.0, 1.0, 1.0); 
    gluOrtho2D(-5, 20, -5, 20); 
    glBegin(GL_LINES);
        glVertex2f(-30, 0.0);       
        glVertex2f(30, 0.0); 
        glVertex2f(0.0, -30);
        glVertex2f(0.0, 30); 
    glEnd(); 
    for(i=0; i<barCount; i++){ 
        x = (i * width) + i + 1; 
        glBegin(GL_POLYGON); 
            glVertex2f(x, y); 
            glVertex2f(x, y+result[i]); 
            glVertex2f(x+width, y+result[i]); 
            glVertex2f(x+width, y); 
        glEnd(); 
    } 
    glFlush(); 
} 

int main(int argc, char *argv[]){ 
    glutInit(&argc, argv); 
    glutInitWindowPosition(0, 0); 
    glutInitWindowSize(500, 500); 
    glutCreateWindow("Drawing Bar Chart"); 
    glutDisplayFunc(display); 
    glutMainLoop(); 
    return 0; 
}

现在,让我们深入了解代码以更好地理解。

它是如何工作的...

GLUT 已初始化,并创建了一个带有标签Displaying Bar Chart的顶级窗口。窗口的初始位置设置为 0,0,即 x=0y=0 坐标位置。窗口的宽度指定为 500 像素,高度也为 500 像素。display 回调函数被调用以绘制条形图。

在显示回调中,初始化一个包含三个值的结果数组。基本上,结果数组中的值代表公司过去三年利润百分比的增长。假设公司 2019 年、2018 年和 2017 年的利润百分比增长分别为 10%、15% 和 5%。我们希望与这些数据对应的三个条形图位于 x 轴上,因此将 y 坐标设置为 0。为了使第一个条形图在一段距离后出现,将 x 坐标值设置为 1。每个条形图的宽度设置为 2。条形图的颜色设置为红色。

设置一个二维正交视图区域,即设置水平和垂直裁剪平面的坐标。在绘制条形图之前,必须绘制水平和垂直的 xy 轴,因此将两条线的顶点组合在 glBeginglEnd 对中,使用 GL_LINES 关键字。

在绘制 xy 轴之后,设置一个 for 循环执行三次,因为我们需要绘制三个条形图。在 for 循环中,条形图被赋予固定的宽度 2 px,并且在每个条形图之后,计算下一个条形图的 x 轴。此外,条形图的高度——即 y 坐标——是基于每个结果数组中提到的利润百分比计算的。条形图使用 glBeginglEnd 对中的四个顶点以及 GL_POLYGON 关键字显示。

要编译程序,请启动 X 服务器并输入以下命令来编译程序:

gcc opengldrawbar.c -lGL -lGLU -lglut -lm -o opengldrawbar

如果没有出现错误,这意味着 opengldrawbar.c 程序已成功编译成可执行文件:opengldrawbar.exe。此文件使用以下命令执行:

$./opengldrawbar

我们将得到以下截图所示的输出:

图片

图 12.4

哇!我们已经成功使用数组中输入的数据创建了一个条形图。现在让我们继续下一个菜谱!

制作一个动画弹跳球

在这个菜谱中,我们将学习如何创建一个弹跳球的动画。球将看起来像是落在地板上然后弹回。为了使球看起来像是落在地板上,球被显示在特定的 x, y 坐标上;在绘制球之后,它从当前位置清除并重新绘制在其原始位置的下方。这种快速连续绘制球、清除它并在较低的 y 坐标位置重新绘制的操作将使球看起来像是落在地面上。可以使用相反的过程来显示球弹回。

如何实现...

制作一个小型弹跳球动画的步骤如下:

  1. GLUT 已初始化,顶层窗口被定义为特定大小,其位置被设置,最后创建了顶层窗口。

  2. 调用回调函数以显示弹跳球。

  3. 在回调函数中,清除颜色缓冲区,并将弹跳球的颜色设置为绿色。

  4. glPointSize 设置为 1 px,因为圆将通过小点或点来绘制。

  5. GL_PROJECTION 被设置为当前矩阵,以便启用平行和透视投影。同时,设置了一个二维正交视域区域。

  6. 要制作动画的下落部分,在某个 x, y 坐标处画一个球。画完那个球后,清除屏幕,并在更低的位置(在更低的 y 坐标处)重新绘制球。

  7. 前一个步骤会快速连续重复,以产生下落球体的效果。

  8. 要使球体弹回,绘制球体,然后清除屏幕,并在高于 地面 位置的更高 y 坐标处重新绘制球体。

制作动画弹跳球的程序如下:

//ballanim.c

#include<stdio.h> 
#include<GL/glut.h> 
#include<math.h> 
#define pi 3.142857 

void animball (void) 
{ 
    int x,y; 
    glClearColor(0.0, 0.0, 0.0, 1.0); 
    glColor3f(0.0, 1.0, 0.0); 
    glPointSize(1.0); 
    glMatrixMode(GL_PROJECTION); 
    glLoadIdentity(); 
    gluOrtho2D(-350, 350, -350, 350); 
    for (float j = 0; j < 1000; j += 0.01) 
    { 
        glClear(GL_COLOR_BUFFER_BIT); 
        glBegin(GL_POINTS);      
            for (int i=0; i <360; i++) 
            { 
                x = 100 * cos(i); 
                y = 100 * sin(i); 
                /* If 100 is radius of circle, then circle is defined as 
                x=100*cos(i) and y=100*sin(i) */
                glVertex2i(x / 2 - 1 * cos(j), y / 2 - 150* sin(j));
            }
        glEnd(); 
        glFlush(); 
    } 
} 

int main (int argc, char** argv) 
{ 
    glutInit(&argc, argv); 
    glutCreateWindow("Animating a ball"); 
    glutInitWindowSize(1000, 1000); 
    glutInitWindowPosition(0, 0); 
    glutDisplayFunc(animball); 
    glutMainLoop(); 
}

现在,让我们深入幕后,更好地理解代码。

它是如何工作的...

GLUT 被初始化,并创建了一个带有标签 Animating a ball 的顶层窗口。窗口的初始位置设置为 0,0,即 x=0y=0 坐标位置。窗口大小指定为 1,000 px 宽和 1,000 px 高。调用回调函数 animball 来显示弹跳球。

animball 回调函数中,清除颜色缓冲区的值。绘制弹跳球的颜色设置为绿色。因为球体将使用小点或点绘制,所以 glPointSize 设置为 1 px。

GL_PROJECTION 被设置为当前矩阵,以便启用平行和透视投影。同时,设置了一个二维正交视域区域,定义了左右垂直裁剪平面以及底部和顶部水平裁剪平面。

要显示弹跳球,我们首先让球体落在地板上,然后弹回。为了制作下落球体,我们在某个 x, y 坐标处画一个球。画完那个球后,我们清除屏幕,并在原始坐标下方(即降低 y 坐标)重新绘制球。通过重复快速地以逐渐降低的 y 坐标清除和重新绘制球,球体看起来就像在下落。我们将执行相反的操作来使球体弹起。也就是说,球体被绘制,屏幕被清除,并在逐渐更高的 y 坐标处重新绘制球体。假设球体的半径为 100 px(但可以是任何半径)。

要编译程序,启动 X 服务器,并给出以下命令来编译程序:

gcc ballanim.c -lGL -lGLU -lglut -lm -o ballanim

如果没有出现错误,这意味着 ballanim.c 程序已成功编译成可执行文件:ballanim.exe。此文件使用以下命令执行:

$./ballanim

我们将得到以下截图所示的输出:

图 12.5

哇!我们已经成功创建了一个动画弹跳球。

第十三章:使用 MySQL 数据库

MySQL 是近年来最受欢迎的数据库管理系统之一。众所周知,数据库用于存储将来需要使用的数据。数据库中的数据可以通过加密来保护,并且可以建立索引以实现更快的访问。当数据量过高时,数据库管理系统比传统的顺序和随机文件处理系统更受欢迎。在数据库中存储数据是任何应用程序中的一项重要任务。

本章的重点是了解如何在数据库表中管理表行。在本章中,你将学习以下食谱:

  • 显示默认 MySQL 数据库中的所有内置表

  • 将信息存储到 MySQL 数据库中

  • 在数据库中搜索所需信息

  • 更新数据库中的信息

  • 使用 C 语言从数据库中删除数据

在我们进入食谱之前,我们将回顾 MySQL 中最常用的函数。同时,确保你在实现本章中的食谱之前阅读 附录 B附录 C 以安装 Cygwin 和 MySQL 服务器。

MySQL 中的函数

在 C 编程中访问和使用 MySQL 数据库时,我们将不得不使用几个函数。让我们来看看它们。

mysql_init()

这将初始化一个可以用于 mysql_real_connect() 方法的 MYSQL 对象。以下是它的语法:

MYSQL *mysql_init(MYSQL *object)

如果传递的对象参数是 NULL,则函数初始化并返回一个新对象;否则,提供的对象将被初始化,并返回对象的地址。

mysql_real_connect()

这将在指定的主机上运行的 MySQL 数据库引擎上建立连接。以下是它的语法:

MYSQL *mysql_real_connect(MYSQL *mysqlObject, const char *hostName, const char *userid, const char *password, const char *dbase, unsigned int port, const char *socket, unsigned long flag)

这里:

  • mysqlObject 表示现有 MYSQL 对象的地址。

  • hostName 是提供主机名或 IP 地址的地方。要连接到本地主机,可以提供 NULL 或字符串 localhost

  • userid 表示有效的 MySQL 登录 ID。

  • password 表示用户的密码。

  • dbase 表示需要建立连接的数据库名称。

  • port 是指定值 0 或提供 TCP/IP 连接的端口号的地方。

  • socket 是指定 NULL 或提供套接字或命名管道的地方。

  • flag 可以用来启用某些功能,例如处理过期的密码和在客户端/服务器协议中应用压缩,但其值通常保持为 0

如果建立了连接,则函数返回 MYSQL 连接句柄;否则,它返回 NULL

mysql_query()

此函数执行提供的 SQL 查询。以下是它的语法:

int mysql_query(MYSQL *mysqlObject, const char *sqlstmt)

这里:

  • mysqlObject 表示 MYSQL 对象

  • sqlstmt 表示包含要执行的 SQL 语句的空终止字符串

如果 SQL 语句执行成功,则函数返回 0;否则,它返回一个非零值。

mysql_use_result()

在成功执行 SQL 语句后,此方法用于保存结果集。这意味着结果集被检索并返回。以下是其语法:

MYSQL_RES *mysql_use_result(MYSQL *mysqlObject)

在此,mysqlObject 代表连接处理程序。

如果没有发生错误,该函数返回一个 MYSQL_RES 结果结构。在发生任何错误的情况下,该函数返回 NULL

mysql_fetch_row()

此函数从结果集中获取下一行。如果没有更多行在结果集中检索或发生错误,则函数返回 NULL。以下是其语法:

MYSQL_ROW mysql_fetch_row(MYSQL_RES *resultset)

在这里,resultset 参数是从中获取下一行数据的集合。您可以通过使用下标 row[0]row[1] 等来访问行的列中的值,其中 row[0] 表示第一列中的数据,row[1] 表示第二列中的数据,依此类推。

mysql_num_fields()

这返回值的数量;即提供的行中的列。以下是其语法:

unsigned int mysql_num_fields(MYSQL_ROW row)

在这里,参数行代表从 resultset 访问的单独行。

mysql_free_result()

这释放了分配给结果集的内存。以下是其语法:

void mysql_free_result(MYSQL_RES *resultset)

在这里,resultset 代表我们想要释放内存的集合。

mysql_close()

此函数关闭先前打开的 MySQL 连接。以下是其语法:

void mysql_close(MYSQL *mysqlObject)

它释放由 mysqlObject 参数表示的连接处理程序。该函数不返回任何值。

这涵盖了我们需要了解的用于在食谱中使用 MySQL 数据库的函数。从第二个食谱开始,我们将在一个数据库表中工作。所以,让我们开始创建一个名为 ecommerce 的数据库和其中的表。

创建 MySQL 数据库和表

打开 Cygwin 终端并使用以下命令打开 MySQL 命令行。通过此命令,我们希望通过用户 ID root 打开 MySQL,并尝试连接到运行在本地的 MySQL 服务器(127.0.0.1):

$ mysql -u root -p -h 127.0.0.1 
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 5.7.14-log MySQL Community Server (GPL)
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 
MySQL [(none)]>                                                    

出现的前一个 MySQL 提示确认了 useridpassword 已正确输入,并且您已成功连接到正在运行的 MySQL 服务器。现在,我们可以继续运行 SQL 命令。

创建数据库

create database 语句创建具有指定名称的数据库。以下是其语法:

Create database database_name;

在这里,database_name 是要创建的新数据库的名称。

让我们创建一个名为 ecommerce 的数据库来存储我们的食谱:

MySQL [(none)]> create database ecommerce; 
Query OK, 1 row affected (0.01 sec)                                            

为了确认我们的 ecommerce 数据库已成功创建,我们将使用 show databases 语句查看 MySQL 服务器上现有的数据库列表:

MySQL [(none)]> show databases; 
+--------------------+
| Database           | 
+--------------------+
| information_schema | 
| ecommerce          | 
| mysql              | 
| performance_schema | 
| sakila             |
| sys                | 
| world              |
+--------------------+
8 rows in set (0.00 sec)                         

在前面的数据库列表中,我们可以看到名称 ecommerce,这证实了我们的数据库已成功创建。现在,我们将应用 use 语句来访问 ecommerce 数据库,如下所示:

MySQL [(none)]> use ecommerce;
Database changed        

现在,ecommerce数据库正在使用中,因此我们将给出的任何 SQL 命令都仅应用于ecommerce数据库。接下来,我们需要在我们的ecommerce数据库中创建一个表。用于创建数据库表的命令是Create table。让我们接下来讨论它。

创建表

这将创建一个具有指定名称的数据库表。以下是其语法:

CREATE TABLE table_name (column_name column_type,column_name column_type,.....);

在这里:

  • table_name代表我们想要创建的表的名称。

  • column_name代表我们希望在表中出现的列名。

  • column_type代表列的数据类型。根据我们想要存储在列中的数据类型,column_type可以是intvarchardatetext等等。

create table语句创建了一个具有三个列的users表:email_addresspasswordaddress_of_delivery。假设这个表将包含已在线下订单的用户的信息,我们将存储他们的电子邮件地址、密码以及订单需要送达的位置:

MySQL [ecommerce]> create table users(email_address varchar(30), password varchar(30), address_of_delivery text);
Query OK, 0 rows affected (0.38 sec)                                           

为了确认表已成功创建,我们将使用show tables命令显示当前打开数据库中现有表列表,如下所示:

MySQL [ecommerce]> show tables;
+---------------------+ 
| Tables_in_ecommerce | 
+---------------------+ 
| users               | 
+---------------------+ 
1 row in set (0.00 sec)         

show tables命令的输出显示了users表,从而确认表确实已成功创建。为了查看表结构(即其列名、列类型和列宽度),我们将使用describe语句。以下语句显示了users表的结构:

MySQL [ecommerce]> describe users;
+---------------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------+-------------+------+-----+---------+-------+
| email_address | varchar(30) | YES | | NULL | |
| password | varchar(30) | YES | | NULL | |
| address_of_delivery | text | YES | | NULL | |
+---------------------+-------------+------+-----+---------+-------+
3 rows in set (0.04 sec)  

因此,现在我们已经了解了与数据库交互的一些基本命令,我们可以开始本章的第一个教程。

显示默认 mysql 数据库中的所有内置表

MySQL 服务器在安装时附带了一些默认数据库。其中之一是mysql。在本教程中,我们将学习如何显示mysql数据库中所有可用的表名。

如何操作...

  1. 创建一个 MySQL 对象:
mysql_init(NULL);
  1. 建立与指定主机上运行的 MySQL 服务器的连接。同时连接到所需的数据库:
mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)
  1. 创建一个包含show tables的执行 SQL 语句:
mysql_query(conn, "show tables")
  1. 将执行 SQL 查询的结果(即mysql数据库的表信息)保存到resultset中:
res = mysql_use_result(conn);
  1. resultset中逐行获取数据,并在while循环中仅显示该行的表名:
while ((row = mysql_fetch_row(res)) != NULL)
     printf("%s \n", row[0]);
  1. 释放分配给resultset的内存:
mysql_free_result(res);
  1. 关闭打开的连接处理器:
mysql_close(conn);

显示内置mysql数据库中所有表的mysql1.c程序如下:

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
     MYSQL *conn;
     MYSQL_RES *res;
     MYSQL_ROW row;
     char *server = "127.0.0.1";
     char *user = "root";
     char *password = "Bintu2018$";
     char *database = "mysql";
     conn = mysql_init(NULL);
     if (!mysql_real_connect(conn, server,
         user, password, database, 0, NULL, 0)) {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(1);
    }
    if (mysql_query(conn, "show tables")) {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(1);
    }
    res = mysql_use_result(conn);
    printf("MySQL Tables in mysql database:\n");
    while ((row = mysql_fetch_row(res)) != NULL)
        printf("%s \n", row[0]);
    mysql_free_result(res);
    mysql_close(conn);
}

现在,让我们深入幕后,更好地理解代码。

它是如何工作的...

我们将首先与 MySQL 服务器建立连接,为此,我们需要调用mysql_real_connect函数。但是,我们必须将一个MYSQL对象传递给mysql_real_connect函数,并且必须调用mysql_init函数来创建MYSQL对象。因此,首先调用mysql_init函数来初始化一个名为connMYSQL对象。

然后,我们将MYSQL对象conn以及有效的用户 ID、密码和主机详情一起提供给mysql_real_connect函数。mysql_real_connect函数将建立与指定主机上运行的 MySQL 服务器的连接。除此之外,该函数还将链接到提供的mysql数据库,并将conn声明为连接处理器。这意味着conn将在整个程序中用于执行对指定 MySQL 服务器和mysql数据库的任何操作。

如果在建立与 MySQL 数据库引擎的连接过程中发生任何错误,程序将在显示错误消息后终止。如果成功建立了与 MySQL 数据库引擎的连接,将调用mysql_query函数,并将 SQL 语句show tables和连接处理器conn提供给它。mysql_query函数将执行提供的 SQL 语句。为了保存mysql数据库的结果表信息,将调用mysql_use_result函数。从mysql_use_result函数接收到的表信息将被分配给resultset res

接下来,我们将在一个while循环中调用mysql_fetch_row函数,每次从resultset res中提取一行;也就是说,每次从resultset中提取一个表详情,并分配给数组row。数组row将包含一次一个表的完整信息。存储在row[0]索引中的表名将在屏幕上显示。随着while循环的每次迭代,下一块表信息将从resultset res中提取出来,并分配给数组row。因此,mysql数据库中的所有表名都将显示在屏幕上。

然后,我们将调用mysql_free_result函数来释放分配给resultset res的内存,最后,我们将调用mysql_close函数来关闭打开的连接处理器conn

让我们使用 GCC 编译mysql1.c程序,如下所示:

$ gcc mysql1.c -o mysql1 -I/usr/local/include/mysql -L/usr/local/lib/mysql -lmysqlclient          

如果你没有收到任何错误或警告,这意味着mysql1.c程序已编译成可执行文件,mysql1.exe。让我们运行这个可执行文件:

$ ./mysql1 
MySQL Tables in mysql database:                                                                         columns_priv                                                                      db 
engine_cost                                                                       event
func
general_log
gtid_executed
help_category
help_keyword 
help_relation 
help_topic
innodb_index_stats
innodb_table_stats
ndb_binlog_index
plugin
proc
procs_priv
proxies_priv
server_cost
servers
slave_master_info
slave_relay_log_info
slave_worker_info
slow_log
tables_priv
time_zone
time_zone_leap_second
time_zone_name
time_zone_transition 
time_zone_transition_type 
user 

哇!正如你所见,输出显示了mysql数据库中内置表列表。现在,让我们继续到下一个菜谱!

在 MySQL 数据库中存储信息

在本食谱中,我们将学习如何将新行插入到users表中。回想一下,在本章开头,我们创建了一个名为ecommerce的数据库,并在该数据库中创建了一个名为users的表,该表具有以下列:

email_address varchar(30)
password varchar(30) 
address_of_delivery text  

我们现在将向此users表中插入行。

如何做到这一点…

  1. 初始化一个 MYSQL 对象:
conn = mysql_init(NULL);
  1. 建立与运行在本地主机的 MySQL 服务器的连接。同时,连接到您想要工作的数据库:
mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)
  1. 输入您要将新行插入到ecommerce数据库中users表中的信息,这将包括新用户的电子邮件地址、密码和送货地址:
printf("Enter email address: ");
scanf("%s", emailaddress);
printf("Enter password: ");
scanf("%s", upassword);
printf("Enter address of delivery: ");
getchar();
gets(deliveryaddress);
  1. 准备一个包含此信息的 SQL INSERT语句;即新用户的电子邮件地址、密码和送货地址:
strcpy(sqlquery,"INSERT INTO users(email_address, password, address_of_delivery)VALUES (\'");
strcat(sqlquery,emailaddress);
strcat(sqlquery,"\', \'");
strcat(sqlquery,upassword);
strcat(sqlquery,"\', \'");
strcat(sqlquery,deliveryaddress);
strcat(sqlquery,"\')");
  1. 执行 SQL INSERT语句以将新行插入到ecommerce数据库中的users表中:
mysql_query(conn, sqlquery)
  1. 关闭连接处理程序:
mysql_close(conn);

在以下代码中显示了用于将行插入 MySQL 数据库表的adduser.c程序:

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
    MYSQL *conn;
    char *server = "127.0.0.1";
    char *user = "root";
    char *password = "Bintu2018$";
    char *database = "ecommerce";
    char emailaddress[30], 
    upassword[30],deliveryaddress[255],sqlquery[255];
    conn = mysql_init(NULL);
    if (!mysql_real_connect(conn, server, user, password, database, 0, 
    NULL, 0)) {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(1);
    }
    printf("Enter email address: ");
    scanf("%s", emailaddress);
    printf("Enter password: ");
    scanf("%s", upassword);
    printf("Enter address of delivery: ");
    getchar();
    gets(deliveryaddress);
    strcpy(sqlquery,"INSERT INTO users(email_address, password, 
    address_of_delivery)VALUES (\'");
    strcat(sqlquery,emailaddress);
    strcat(sqlquery,"\', \'");
    strcat(sqlquery,upassword);
    strcat(sqlquery,"\', \'");
    strcat(sqlquery,deliveryaddress);
    strcat(sqlquery,"\')");
    if (mysql_query(conn, sqlquery) != 0)               
    { 
        fprintf(stderr, "Row could not be inserted into users
    table\n");
        exit(1);
    } 
    printf("Row is inserted successfully in users table\n");
    mysql_close(conn);
}

现在,让我们深入了解代码以更好地理解它。

它是如何工作的...

我们首先调用mysql_init函数,通过名称conn初始化一个MYSQL对象。初始化后的MYSQL对象conn随后被用于调用mysql_real_connect函数,同时提供有效的用户 ID 和密码,这将建立与运行在本地主机的 MySQL 服务器的连接。此外,该函数还将链接到我们的ecommerce数据库。

如果在建立与 MySQL 数据库引擎的连接时发生任何错误,将显示错误消息,程序将终止。如果成功建立与 MySQL 数据库引擎的连接,则conn将作为程序其余部分的连接处理程序。

您将被提示输入要将新行插入到ecommerce数据库中users表中的信息。您将被提示输入新行信息:电子邮件地址、密码和送货地址。我们将创建一个包含此信息(电子邮件地址、密码和送货地址)的 SQL INSERT语句,该语句应由用户输入。之后,我们将调用mysql_query函数,并将 MySQL 对象conn和 SQL INSERT语句传递给它以执行 SQL 语句并将新行插入到users表中。

如果在执行mysql_query函数时发生任何错误,屏幕上将显示错误消息,程序将终止。如果新行成功插入到users表中,屏幕上将显示消息Row is inserted successfully in users table。最后,我们将调用mysql_close函数,并将连接处理程序conn传递给它以关闭连接处理程序。

让我们打开 Cygwin 终端。我们需要两个终端窗口;在一个窗口中,我们将运行 SQL 命令,在另一个窗口中,我们将编译和运行 C 语言。通过按 Alt+F2 打开另一个终端窗口。在第一个终端窗口中,使用以下命令调用 MySQL 命令行:

$ mysql -u root -p -h 127.0.0.1
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g. 
Your MySQL connection id is 27 
Server version: 5.7.14-log MySQL Community Server (GPL) 
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 

要使用我们的 ecommerce 数据库,我们需要将其设置为当前数据库。因此,使用以下命令打开 ecommerce 数据库:

MySQL [(none)]> use ecommerce;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A 
Database changed          

现在,ecommerce 是我们的当前数据库;也就是说,我们将执行的任何 SQL 命令都只应用于 ecommerce 数据库。让我们使用以下 SQL SELECT 命令来查看 users 数据库表中的现有行:

MySQL [ecommerce]> select * from users;
Empty set (0.00 sec)  

给定的输出确认 users 表目前为空。要编译 C 程序,切换到第二个终端窗口。让我们使用 GCC 编译 adduser.c 程序,如下所示:

$ gcc adduser.c -o adduser -I/usr/local/include/mysql -L/usr/local/lib/mysql -lmysqlclient        

如果没有错误或警告,这意味着 adduser.c 程序已编译成可执行文件 adduser.exe。让我们运行这个可执行文件:

$./adduser 
Enter email address: bmharwani@yahoo.com 
Enter password: gold 
Enter address of delivery: 11 Hill View Street, New York, USA
Row is inserted successfully in users table 

给定的 C 程序输出确认新行已成功添加到 users 数据库表中。要确认这一点,切换到打开 MySQL 命令行的终端窗口,并使用以下命令:

MySQL [ecommerce]> select * from users;
+---------------------+----------+------------------------------------+
| email_address       | password | address_of_delivery                |
+---------------------+----------+------------------------------------+
| bmharwani@yahoo.com | gold     | 11 Hill View Street, New York, USA | 
+---------------------+----------+------------------------------------+ 
1 row in set (0.00 sec)   

Voila!给定的输出确认通过 C 语言输入的新行已成功插入到 users 数据库表中。

现在,让我们继续下一个菜谱!

在数据库中搜索所需信息

在这个菜谱中,我们将学习如何在数据库表中搜索信息。再次强调,我们假设已经存在一个包含三个列的 users 表,分别是 email_addresspasswordaddress_of_delivery(请参阅本章的 创建 MySQL 数据库和表 部分,其中我们创建了一个 ecommerce 数据库并在其中创建了一个 users 表)。输入电子邮件地址后,菜谱将搜索整个 users 数据库表,如果找到与提供的电子邮件地址匹配的行,则将在屏幕上显示该用户的密码和送货地址。

如何做到这一点...

  1. 初始化一个 MYSQL 对象:
mysql_init(NULL);
  1. 建立与指定主机上运行的 MySQL 服务器的连接。同时,建立与 ecommerce 数据库的连接:
mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)
  1. 输入您要搜索详情的用户电子邮件地址:
printf("Enter email address to search: ");
scanf("%s", emailaddress);
  1. 创建一个 SQL SELECT 语句,搜索 users 表中与用户输入的电子邮件地址匹配的行:
strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
strcat(sqlquery,emailaddress);
strcat(sqlquery,"\'");
  1. 执行 SQL SELECT 语句。如果 SQL 查询未执行或发生错误,则终止程序:
if (mysql_query(conn, sqlquery) != 0)                                 
{                                                                                                                                fprintf(stderr, "No row found in the users table with this email     address\n");                                                             
    exit(1);                                                                                     }  
  1. 如果 SQL 查询执行成功,则与指定电子邮件地址匹配的行将被检索并分配给 resultset
resultset = mysql_use_result(conn);
  1. 使用 while 循环逐行从 resultset 中提取并分配给数组 row
while ((row = mysql_fetch_row(resultset)) != NULL)
  1. 通过显示子脚本来显示整行信息 row[0]row[1]row[2],分别:
printf("Email Address: %s \n", row[0]);
printf("Password: %s \n", row[1]);
printf("Address of delivery: %s \n", row[2]);
  1. 分配给resultset的内存将被释放:
mysql_free_result(resultset);
  1. 打开的连接处理器被关闭:
mysql_close(conn);

在以下代码中展示了用于在 MySQL 数据库表中搜索特定行的searchuser.c程序:

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
    MYSQL *conn;
    MYSQL_RES *resultset;
    MYSQL_ROW row;
    char *server = "127.0.0.1";
    char *user = "root";
    char *password = "Bintu2018$";
    char *database = "ecommerce";
    char emailaddress[30], sqlquery[255];
    conn = mysql_init(NULL);
    if (!mysql_real_connect(conn, server, user, password, database, 0, 
    NULL, 0)) {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(1);
    }
    printf("Enter email address to search: ");
    scanf("%s", emailaddress);
    strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
    strcat(sqlquery,emailaddress);
    strcat(sqlquery,"\'");
    if (mysql_query(conn, sqlquery) != 0)                 
    {                  
        fprintf(stderr, "No row found in the users table with this 
    email address\n");                  
        exit(1);                                                                     
    }  
    printf("The details of the user with this email address are as 
    follows:\n");
    resultset = mysql_use_result(conn);
    while ((row = mysql_fetch_row(resultset)) != NULL)
    {
        printf("Email Address: %s \n", row[0]);
        printf("Password: %s \n", row[1]);
        printf("Address of delivery: %s \n", row[2]);
    }
    mysql_free_result(resultset);
    mysql_close(conn);
}

现在,让我们幕后了解一下代码,以便更好地理解它。

它是如何工作的...

我们将首先调用mysql_init函数,通过名称conn初始化一个MYSQL对象。之后,我们将调用mysql_real_connect函数,并将有效的用户 ID、密码和主机详情传递给该函数。mysql_real_connect函数将连接到在指定主机上运行的 MySQL 服务器,并将连接到提供的数据库ecommerceMYSQL对象conn将作为程序其余部分的连接处理器。无论何时需要连接到 MySQL 服务器和ecommerce数据库,引用conn就足够了。

如果在建立与 MySQL 数据库引擎或ecommerce数据库的连接时发生任何错误,将显示错误消息,程序将终止。如果成功建立与 MySQL 数据库引擎的连接,你将被提示输入你想要搜索的用户详情的电子邮件地址。

我们将创建一个 SQL SELECT语句,该语句将搜索与用户输入的电子邮件地址匹配的users表中的行。然后,我们将调用mysql_query函数,并将创建的 SQL SELECT语句及其连接处理器conn传递给它。如果 SQL 查询没有执行或发生某些错误,程序将在显示错误消息后终止。如果查询成功,则通过调用mysql_use_result函数检索满足条件的结果行(即与提供的电子邮件地址匹配的行),并将它们分配给结果集resultset

然后,我们将在一个while循环中调用mysql_fetch_row函数,每次从resultset中提取一行;也就是说,resultset中的第一行将被访问并分配给数组row

回想一下,users表包含以下列:

  • email_address varchar(30)

  • password varchar(30)

  • address_of_delivery text

因此,数组row将包含访问行的完整信息,其中索引row[0]将包含email_address列的数据,row[1]将包含密码列的数据,row[2]将包含address_of_delivery列的数据。通过分别显示索引row[0]row[1]row[2],将显示整行的信息。

最后,我们将调用mysql_free_result函数来释放分配给resultset的内存。然后,我们将调用mysql_close函数来关闭打开的连接处理器conn

让我们打开 Cygwin 终端。我们需要两个终端窗口;在一个窗口中,我们将运行 SQL 命令,在另一个窗口中,我们将编译和运行 C 语言程序。通过按 Alt+F2 打开另一个终端窗口。在第一个终端窗口中,使用以下命令调用 MySQL 命令行:

$ mysql -u root -p -h 127.0.0.1 
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g. 
Your MySQL connection id is 27 
Server version: 5.7.14-log MySQL Community Server (GPL) 
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 

要与我们的 ecommerce 数据库一起工作,我们需要将其设置为当前数据库。因此,使用以下命令打开 ecommerce 数据库:

MySQL [(none)]> use ecommerce; 
Reading table information for completion of table and column names 
You can turn off this feature to get a quicker startup with -A 
Database changed           

现在,ecommerce 是我们的当前数据库;也就是说,我们将执行的任何 SQL 命令都只应用于 ecommerce 数据库。让我们使用以下 SQL SELECT 命令来查看 users 数据库表中的现有行:

MySQL [ecommerce]> select * from users; 
+---------------------+----------+------------------------------------+ 
| email_address       | password | address_of_delivery  |
+---------------------+----------+------------------------------------+
| bmharwani@yahoo.com | gold     | 11 Hill View Street, New York, USA

| harwanibm@gmail.com | diamond  | House No. xyz, Pqr Apartments, Uvw Lane, Mumbai, Maharashtra                        |                                                                                 | bintu@gmail.com     | platinum | abc Sea View, Ocean Lane, Opposite Mt. Everest, London, UKg 
+---------------------+----------+------------------------------------+
3 rows in set (0.00 sec)     

给定的输出显示 users 表中有三行。

要编译 C 程序,切换到第二个终端窗口。让我们使用 GCC 编译 searchuser.c 程序,如下所示:

$ gcc searchuser.c -o searchuser -I/usr/local/include/mysql -L/usr/local/lib/mysql -lmysqlclient         

如果没有错误或警告,这意味着 searchuser.c 程序已编译成可执行文件,名为 searchuser.exe。让我们运行这个可执行文件:

$ ./searchuser 
Enter email address to search: bmharwani@yahoo.com 
The details of the user with this email address are as follows: 
Email Address:bmharwani@yahoo.com
Password: gold 
Address of delivery: 11 Hill View Street, New York, USA 

哇!我们可以看到,带有电子邮件地址 bmharwani@yahoo.com 的用户完整信息显示在屏幕上。

现在,让我们继续下一个菜谱!

更新数据库中的信息

在这个菜谱中,我们将学习如何在数据库表中更新信息。我们假设已经存在一个 users 数据库表,包含三个列——email_addresspasswordaddress_of_delivery(请参阅本章开头,我们学习了如何创建数据库和其中的表)。输入电子邮件地址后,将显示用户的全部当前信息(即他们的密码和送货地址)。之后,用户将被提示输入新的密码和送货地址。这些新信息将更新到表中的当前信息。

如何做到这一点…

  1. 初始化一个 MYSQL 对象:
mysql_init(NULL);
  1. 建立与指定主机上运行的 MySQL 服务器之间的连接。同时,生成一个连接处理器。如果建立与 MySQL 服务器引擎或 ecommerce 数据库的连接时发生错误,程序将终止:
 if (!mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)) 
 {
      fprintf(stderr, "%s\n", mysql_error(conn));
      exit(1);
 }
  1. 输入需要更新信息的用户的电子邮件地址:
printf("Enter email address of the user to update: ");
scanf("%s", emailaddress);
  1. 创建一个 SQL SELECT 语句,该语句将搜索 users 表中与用户输入的电子邮件地址匹配的行:
 strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
 strcat(sqlquery,emailaddress);
 strcat(sqlquery,"\'");
  1. 执行 SQL SELECT 语句。如果 SQL 查询未成功执行或发生其他错误,程序将终止:
if (mysql_query(conn, sqlquery) != 0) 
{ 
     fprintf(stderr, "No row found in the users table with this          email address\n"); 
     exit(1); 
 }  
  1. 如果 SQL 查询成功执行,则将匹配提供的电子邮件地址的行检索并分配给 resultset
 resultset = mysql_store_result(conn);
  1. 检查 resultset 中是否至少有一行:
if(mysql_num_rows(resultset) >0)
  1. 如果 resultset 中没有行,则显示消息,指出在 users 表中没有找到指定电子邮件地址的行,并退出程序:
printf("No user found with this email address\n");
  1. 如果resultset中存在任何行,则访问它并将其分配给数组行:
row = mysql_fetch_row(resultset)
  1. 用户信息(即分配给子脚标row[0]row[1]row[2]的电子邮件地址、密码和送货地址)将在屏幕上显示:
printf("Email Address: %s \n", row[0]);
printf("Password: %s \n", row[1]);
printf("Address of delivery: %s \n", row[2]);
  1. 释放分配给resultset的内存:
mysql_free_result(resultset);
  1. 输入用户的新更新信息;即新的密码和新的送货地址:
printf("Enter new password: ");
scanf("%s", upassword);
printf("Enter new address of delivery: ");
getchar();
gets(deliveryaddress);
  1. 准备了一个包含新输入的密码和送货地址信息的 SQL UPDATE语句:
strcpy(sqlquery,"UPDATE users set password=\'");
strcat(sqlquery,upassword);
strcat(sqlquery,"\', address_of_delivery=\'");
strcat(sqlquery,deliveryaddress);
strcat(sqlquery,"\' where email_address like \'");
strcat(sqlquery,emailaddress);
strcat(sqlquery,"\'");
  1. 执行 SQL UPDATE语句。如果在执行 SQL UPDATE查询时发生任何错误,程序将终止:
if (mysql_query(conn, sqlquery) != 0)                 
{                                                                                                                                                  fprintf(stderr, "The desired row in users table could not be 
    updated\n");  
    exit(1);
 }  
  1. 如果 SQL UPDATE语句执行成功,将在屏幕上显示一条消息,告知用户信息已成功更新:
printf("The information of user is updated successfully in users table\n");
  1. 关闭打开的连接句柄:
mysql_close(conn);

更新 MySQL 数据库表特定行的updateuser.c程序如下所示:

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
    MYSQL *conn;
    MYSQL_RES *resultset;
    MYSQL_ROW row;
    char *server = "127.0.0.1";
    char *user = "root";
    char *password = "Bintu2018$";
    char *database = "ecommerce";
    char emailaddress[30], sqlquery[255],             
    upassword[30],deliveryaddress[255];
    conn = mysql_init(NULL);
    if (!mysql_real_connect(conn, server, user, password, database, 0,     NULL, 0)) {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(1);
    }
    printf("Enter email address of the user to update: ");
    scanf("%s", emailaddress);
    strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
    strcat(sqlquery,emailaddress);
    strcat(sqlquery,"\'");
    if (mysql_query(conn, sqlquery) != 0)                 
    {                                                                                 
        fprintf(stderr, "No row found in the users table with this 
        email address\n");                                                                                                     
        exit(1);                                                                     
    }  
    resultset = mysql_store_result(conn);
    if(mysql_num_rows(resultset) >0)
    {
        printf("The details of the user with this email address are as 
        follows:\n");
        while ((row = mysql_fetch_row(resultset)) != NULL)
        {
            printf("Email Address: %s \n", row[0]);
            printf("Password: %s \n", row[1]);
            printf("Address of delivery: %s \n", row[2]);
        }
        mysql_free_result(resultset);
        printf("Enter new password: ");
        scanf("%s", upassword);
        printf("Enter new address of delivery: ");
        getchar();
        gets(deliveryaddress);
        strcpy(sqlquery,"UPDATE users set password=\'");
        strcat(sqlquery,upassword);
        strcat(sqlquery,"\', address_of_delivery=\'");
        strcat(sqlquery,deliveryaddress);
        strcat(sqlquery,"\' where email_address like \'");
        strcat(sqlquery,emailaddress);
        strcat(sqlquery,"\'");
        if (mysql_query(conn, sqlquery) != 0)                 
        {                                                                                                                                                         
            fprintf(stderr, "The desired row in users table could not 
            be updated\n");                                                             
            exit(1);                                                                     
        }  
        printf("The information of user is updated successfully in 
        users table\n");
    }
    else
        printf("No user found with this email address\n");
    mysql_close(conn);
}

现在,让我们深入了解代码背后的原理。

它是如何工作的...

在这个程序中,我们首先要求用户输入他们想要更新的电子邮件地址。然后,我们在users表中搜索是否存在具有匹配电子邮件地址的行。如果我们找到了它,我们显示用户的当前信息;即当前的电子邮件地址、密码和送货地址。之后,我们要求用户输入新的密码和新的送货地址。新的密码和送货地址将替换旧的密码和送货地址,从而更新users表。

我们将首先调用mysql_init函数,通过名称conn初始化一个MYSQL对象。然后,我们将MYSQL对象conn传递给mysql_real_connect函数,以建立与在指定主机上运行的 MySQL 服务器的连接。还将向mysql_real_connect函数传递其他几个参数,包括有效的用户 ID、密码、主机详情以及我们想要工作的数据库。mysql_real_connect函数将建立与在指定主机上运行的 MySQL 服务器的连接,并将MYSQL对象conn声明为连接句柄。这意味着conn可以在任何使用它的地方连接到MySQL服务器和ecommerce数据库。

如果在建立与 MySQL 服务器引擎或ecommerce数据库的连接时发生错误,程序将在显示错误消息后终止。如果成功建立与 MySQL 数据库引擎的连接,您将被提示输入您想要更新的用户记录的电子邮件地址。

正如我们之前提到的,我们首先将显示当前用户的信息。因此,我们将创建一个 SQL SELECT语句,并将在users表中搜索与用户输入的电子邮件地址匹配的行。然后,我们将调用mysql_query函数,并将创建的 SQL SELECT语句及其连接处理程序conn传递给它。

如果 SQL 查询没有成功执行或发生其他错误,程序将在显示错误消息后终止。如果查询成功执行,则通过调用mysql_use_result函数检索的结果行(即与提供的电子邮件地址匹配的行)将被分配给resultset

然后,我们将调用mysql_num_rows函数以确保resultset中至少有一行。如果resultset中没有行,这意味着在users表中没有找到与给定电子邮件地址匹配的行。在这种情况下,程序将在通知在users表中没有找到给定电子邮件地址的行后终止。如果resultset中甚至有一行,我们将对resultset调用mysql_fetch_row函数,这将从一个resultset中提取一行并将其分配给数组行。

users表包含以下三个列:

  • email_address varchar(30)

  • password varchar(30)

  • address_of_delivery text

数组行将包含访问行的信息,其中子索引row[0]row[1]row[2]将分别包含email_addresspasswordaddress_of_delivery列的数据。通过显示分配给上述子索引的信息来显示当前用户的信息。然后,我们将调用mysql_free_result函数来释放分配给resultset的内存。

在此阶段,将要求用户输入新的密码和新的送货地址。我们将准备一个包含新输入的密码和送货地址信息的 SQL UPDATE语句。将调用mysql_query函数,并将 SQL UPDATE语句及其连接处理程序conn传递给它。

如果在执行 SQL UPDATE查询时发生任何错误,将再次显示错误消息,并终止程序。如果 SQL UPDATE语句成功执行,将显示一条消息,告知用户信息已成功更新。最后,我们将调用mysql_close函数来关闭打开的连接处理程序conn

让我们打开 Cygwin 终端。我们需要两个终端窗口;在一个窗口中运行 SQL 命令,在另一个窗口中编译和运行 C。通过按Alt+F2打开另一个终端窗口。在第一个终端窗口中,使用以下命令调用 MySQL 命令行:

$ mysql -u root -p -h 127.0.0.1 
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g. 
Your MySQL connection id is 27 
Server version: 5.7.14-log MySQL Community Server (GPL) 
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 

要与我们的 ecommerce 数据库一起工作,我们需要将其设置为当前数据库。因此,使用以下命令打开 ecommerce 数据库:

MySQL [(none)]> use ecommerce; 
Reading table information for completion of table and column names 
You can turn off this feature to get a quicker startup with -A 
Database changed            

现在,ecommerce 是我们的当前数据库;也就是说,我们将执行的任何 SQL 命令都只应用于 ecommerce 数据库。让我们使用以下 SQL SELECT 命令来查看 users 数据库表中的现有行:

MySQL [ecommerce]> select * from users;
+---------------------+----------+------------------------------------+
| email_address       | password | address_of_delivery|
+---------------------+----------+------------------------------------+
| bmharwani@yahoo.com | gold     | 11 Hill View Street, New York, USA|
| harwanibm@gmail.com | diamond  | House No. xyz, Pqr Apartments, Uvw Lane, Mumbai, Maharashtra|
| bintu@gmail.com     | platinum | abc Sea View, Ocean Lane, Opposite Mt. Everest, London, UKg
+---------------------+----------+------------------------------------+
3 rows in set (0.00 sec)      

从前面的输出中我们可以看到,users 表中有三行。要编译 C 程序,切换到第二个终端窗口。让我们使用 GCC 编译 updateuser.c 程序,如下所示:

$ gcc updateuser.c -o updateuser -I/usr/local/include/mysql -L/usr/local/lib/mysql -lmysqlclient           

如果没有错误或警告,这意味着 updateuser.c 程序已编译成可执行文件 updateuser.exe。让我们运行这个可执行文件:

$ ./updateuser 
Enter email address of the user to update: harwanibintu@gmail.com 
No user found with this email address                     

让我们再次运行程序并输入一个已存在的电子邮件地址:

$ ./updateuser 
Enter email address of the user to update: bmharwani@yahoo.com 
The details of the user with this email address are as follows: 
Email Address: bmharwani@yahoo.com 
Password: gold 
Address of delivery: 11 Hill View Street, New York, USA 
Enter new password: coffee 
Enter new address of delivery: 444, Sky Valley, Toronto, Canada 
The information of user is updated successfully in users table                 

因此,我们已经更新了电子邮件地址为 bmharwani@yahoo.com 的用户的行。为了确认该行已在 users 数据库表中更新,切换到运行 MySQL 命令行的终端窗口,并执行以下 SQL SELECT 命令:

MySQL [ecommerce]> MySQL [ecommerce]> select * from users;
+---------------------+----------+------------------------------------+ 
| email_address       | password | address_of_delivery|
+---------------------+----------+------------------------------------+ 
| bmharwani@yahoo.com | coffee   | 444, Sky Valley, Toronto, Canada 
| 
| harwanibm@gmail.com | diamond  | House No. xyz, Pqr Apartments, Uvw Lane, Mumbai, Maharashtra 
|
| bintu@gmail.com     | platinum | abc Sea View, Ocean Lane, Opposite Mt. Everest, London, UKg
+---------------------+----------+------------------------------------+

!我们可以看到,电子邮件地址为 bmharwani@yahoo.comusers 表的行已被更新,并显示了新的信息。

现在,让我们继续下一个教程!

使用 C 从数据库中删除数据

在本教程中,我们将学习如何从数据库表中删除信息。我们假设已经存在一个包含三个列的 users 表,分别是 email_addresspasswordaddress_of_delivery(请参阅本章开头,我们在这里创建了一个 ecommerce 数据库和其中的 users 表)。您将被提示输入要删除行的用户的电子邮件地址。输入电子邮件地址后,将显示该用户的所有信息。之后,您将再次被要求确认是否要删除显示的行。确认后,该行将从表中永久删除。

如何操作...

  1. 初始化一个 MYSQL 对象:
mysql_init(NULL);
  1. 建立与指定主机上运行的 MySQL 服务器连接。同时,生成一个连接处理程序。如果在建立与 MySQL 服务器引擎的连接过程中发生任何错误,程序将终止:
  if (!mysql_real_connect(conn, server, user, password, database, 0, 
    NULL, 0)) {
      fprintf(stderr, "%s\n", mysql_error(conn));
      exit(1);
  }
  1. 如果成功建立了与 MySQL 数据库引擎的连接,您将被提示输入要删除记录的用户的电子邮件地址:
 printf("Enter email address of the user to delete: ");
 scanf("%s", emailaddress);
  1. 创建一个 SQL SELECT 语句,该语句将搜索与用户输入的电子邮件地址匹配的 users 表中的行:
 strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
 strcat(sqlquery,emailaddress);
 strcat(sqlquery,"\'");
  1. 执行 SQL SELECT 语句。如果 SQL 查询执行不成功,程序将在显示错误信息后终止:
 if (mysql_query(conn, sqlquery) != 0)                 
 {                                                                                                                                   
    fprintf(stderr, "No row found in the users table with this email 
    address\n");                                                                                                     
    exit(1);                                                                     
 }  
  1. 如果查询执行成功,则将检索与提供的电子邮件地址匹配的结果行(如果有的话),并将其分配给 resultset
resultset = mysql_store_result(conn);
  1. 调用 mysql_num_rows 函数以确保 resultset 中至少有一行:
if(mysql_num_rows(resultset) >0)
  1. 如果 resultset 中没有行,这意味着在 users 表中没有找到与给定电子邮件地址匹配的行;因此,程序将终止:
printf("No user found with this email address\n");
  1. 如果结果集中有任何行,则该行将从 resultset 中提取出来,并将分配给数组行:
row = mysql_fetch_row(resultset)
  1. 通过显示数组行中的相应子脚本来显示用户信息:
printf("Email Address: %s \n", row[0]);
printf("Password: %s \n", row[1]);
printf("Address of delivery: %s \n", row[2]);
  1. 分配给 resultset 的内存被释放:
mysql_free_result(resultset);The user is asked whether he/she really want to delete the shown record.
printf("Are you sure you want to delete this record yes/no: ");
scanf("%s", k);
  1. 如果用户输入 yes,将创建一个 SQL DELETE 语句,该语句将从 users 表中删除与指定电子邮件地址匹配的行:
if(strcmp(k,"yes")==0)
{
    strcpy(sqlquery, "Delete from users where email_address like 
    \'");
    strcat(sqlquery,emailaddress);
    strcat(sqlquery,"\'");
  1. 执行 SQL DELETE 语句。如果在执行 SQL DELETE 查询时发生任何错误,程序将终止:
if (mysql_query(conn, sqlquery) != 0)                 
{                                                                                   
    fprintf(stderr, "The user account could not be deleted\n");                                                             
    exit(1);                                                                     
}
  1. 如果 SQL DELETE 语句执行成功,将显示一条消息,告知指定电子邮件地址的用户账户已成功删除:
printf("The user with the given email address is successfully deleted from the users table\n");
  1. 打开的连接处理程序被关闭:
mysql_close(conn);

用于从 MySQL 数据库表中删除特定行的 deleteuser.c 程序如下所示:

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() {
MYSQL *conn;
MYSQL_RES *resultset;
MYSQL_ROW row;
char *server = "127.0.0.1";
char *user = "root";
char *password = "Bintu2018$";
char *database = "ecommerce";
char emailaddress[30], sqlquery[255],k[10];
conn = mysql_init(NULL);
if (!mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)) {
    fprintf(stderr, "%s\n", mysql_error(conn));
    exit(1);
}
printf("Enter email address of the user to delete: ");
scanf("%s", emailaddress);
strcpy(sqlquery,"SELECT * FROM users where email_address like \'");
strcat(sqlquery,emailaddress);
strcat(sqlquery,"\'");
if (mysql_query(conn, sqlquery) != 0)                 
{                                                                          
    fprintf(stderr, "No row found in the users table with this email 
    address\n");                                                             
    exit(1);                                                                      
}  
resultset = mysql_store_result(conn);
if(mysql_num_rows(resultset) >0)
{
    printf("The details of the user with this email address are as 
    follows:\n");
    while ((row = mysql_fetch_row(resultset)) != NULL)
    {
        printf("Email Address: %s \n", row[0]);
        printf("Password: %s \n", row[1]);
        printf("Address of delivery: %s \n", row[2]);
    }
    mysql_free_result(resultset);
    printf("Are you sure you want to delete this record yes/no: ");
    scanf("%s", k);
    if(strcmp(k,"yes")==0)
    {
        strcpy(sqlquery, "Delete from users where email_address like 
        \'");
        strcat(sqlquery,emailaddress);
        strcat(sqlquery,"\'");
        if (mysql_query(conn, sqlquery) != 0)                 
        {                                                                                 
            fprintf(stderr, "The user account could not be deleted\n");                                                             
            exit(1);                                                                      
        }  
        printf("The user with the given email address is successfully 
        deleted from the users table\n");
    }
}
else
    printf("No user found with this email address\n");
    mysql_close(conn);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们将首先调用 mysql_init 函数,通过名称 conn 初始化一个 MYSQL 对象。然后,我们将 MYSQL 对象 conn 传递给 mysql_real_connect 函数,该函数用于建立与在指定主机上运行的 MySQL 服务器的连接。还将向 mysql_real_connect 函数传递其他几个参数,包括有效的用户 ID、密码、主机详细信息以及我们想要工作的数据库。mysql_real_connect 函数将建立与在指定主机上运行的 MySQL 服务器的连接,并将一个 MYSQL 对象 conn 声明为连接处理程序。这意味着 conn 可以在任何使用它的地方连接到 MySQL 服务器和 commerce 数据库。

如果在连接到 MySQL 服务器引擎或 ecommerce 数据库时发生错误,程序将在显示错误消息后终止。如果成功连接到 MySQL 数据库引擎,系统将提示您输入要删除记录的用户电子邮件地址。

我们首先将显示用户的信息,然后将从用户那里获取是否真的想要删除该行的许可。因此,我们将创建一个 SQL SELECT 语句,该语句将搜索与用户输入的电子邮件地址匹配的 users 表中的行。然后,我们将调用 mysql_query 函数,并将创建的 SQL SELECT 语句及其连接处理程序 conn 传递给它。

如果 SQL 查询没有成功执行或发生其他错误,程序将在显示错误消息后终止。如果查询执行成功,则通过调用 mysql_use_result 函数检索到的结果行(即与提供的电子邮件地址匹配的行)将被分配给 resultset

我们将调用 mysql_num_rows 函数以确保 resultset 中至少有一行。如果没有行在 resultset 中,这意味着在 users 表中没有找到与给定电子邮件地址匹配的行。在这种情况下,程序将在告知在 users 表中没有找到给定电子邮件地址的行后终止。如果 resultset 中甚至有一行,我们将对 resultset 调用 mysql_fetch_row 函数,这将从一个 resultset 中提取一行并将其分配给数组行。

users 表包含以下三个列:

  • email_address varchar(30)

  • password varchar(30)

  • address_of_delivery text

数组行将包含访问行的信息,其中子索引 row[0]row[1]row[2] 分别包含 email_addresspasswordaddress_of_delivery 列的数据。当前用户信息将通过显示分配给子索引 row[0]row[1]row[2] 的当前电子邮件地址、密码和送货地址来显示。然后,我们将调用 mysql_free_result 函数来释放分配给 resultset 的内存。

在此阶段,将要求用户确认他们是否真的想要删除显示的记录。用户应输入全部小写的 yes 来删除记录。如果用户输入 yes,将创建一个 SQL DELETE 语句,该语句将删除与指定电子邮件地址匹配的 users 表中的行。将调用 mysql_query 函数,并将 SQL DELETE 语句及其连接处理器 conn 传递给它。

如果在执行 SQL DELETE 查询时发生任何错误,将再次显示错误消息,并且程序将终止。如果 SQL DELETE 语句执行成功,将显示一条消息,告知指定邮件地址的用户账户已成功删除。最后,我们将调用 mysql_close 函数来关闭已打开的连接处理器 conn

让我们打开 Cygwin 终端。我们需要两个终端窗口;在一个窗口中,我们将运行 MySQL 命令,在另一个窗口中,我们将编译和运行 C 语言。通过按 Alt+F2 打开另一个终端窗口。在第一个终端窗口中,通过以下命令调用 MySQL 命令行:

$ mysql -u root -p -h 127.0.0.1 
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g. 
Your MySQL connection id is 27 
Server version: 5.7.14-log MySQL Community Server (GPL) 
Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 

要与我们的 ecommerce 数据库一起工作,我们需要将其设置为当前数据库。因此,使用以下命令打开 ecommerce 数据库:

MySQL [(none)]> use ecommerce; 
Reading table information for completion of table and column names 
You can turn off this feature to get a quicker startup with -A 
Database changed            

现在,ecommerce 是我们的当前数据库;也就是说,我们将执行的任何 SQL 命令都只应用于 ecommerce 数据库。让我们使用以下 SQL SELECT 命令来查看 users 数据库表中的现有行:

MySQL [ecommerce]> select * from users;
+---------------------+----------+------------------------------------+
| email_address | password | address_of_delivery | 
+---------------------+----------+------------------------------------+
| bmharwani@yahoo.com | coffee | 444, Sky Valley, Toronto, Canada 
|
| harwanibm@gmail.com | diamond | House No. xyz, Pqr Apartments, Uvw Lane, Mumbai, Maharashtra | 
| bintu@gmail.com | platinum | abc Sea View, Ocean Lane, Opposite Mt. Everest, London, UKg
+---------------------+----------+------------------------------------+
3 rows in set (0.00 sec)

从前面的输出中,我们可以看到 users 表中有三行。要编译 C 程序,切换到第二个终端窗口。让我们使用 GCC 编译 deleteuser.c 程序,如下所示:

$ gcc deleteuser.c -o deleteuser -I/usr/local/include/mysql -L/usr/local/lib/mysql -lmysqlclient

如果你没有收到任何错误或警告,这意味着 deleteuser.c 程序已编译成可执行文件,名为 deleteuser.exe。让我们运行这个可执行文件:

$ ./deleteuser
Enter email address of the user to delete: harwanibintu@gmail.com 
No user found with this email address                

现在,让我们使用有效的电子邮件地址再次运行程序:

$ ./deleteuser 
Enter email address of the user to delete: bmharwani@yahoo.com 
The details of the user with this email address are as follows:
Email Address: bmharwani@yahoo.com
Password: coffee
Address of delivery: 444, Sky Valley, Toronto, Canada
Are you sure you want to delete this record yes/no: yes 
The user with the given email address is successfully deleted from the users table

因此,具有电子邮件地址 bmharwani@yahoo.com 的用户行将从 users 表中删除。为了确认该行已从 users 数据库表中删除,切换到运行 MySQL 命令行的终端窗口,并执行以下 SQL SELECT 命令:

 MySQL [ecommerce]> select * from users;
+---------------------+----------+------------------------------------+
| email_address       | password | address_of_delivery 
| 
+---------------------+----------+------------------------------------+
| harwanibm@gmail.com | diamond  | House No. xyz, Pqr Apartments, Uvw Lane, Mumbai, Maharashtra 
| 
| bintu@gmail.com     | platinum | abc Sea View, Ocean Lane, Opposite Mt. Everest, London, UKg 
+---------------------+----------+------------------------------------+

Voila!我们可以看到现在 users 表中只剩下两行,这证实了一行已从 users 表中删除。

第十四章:通用工具

在本章中,我们将学习在执行不同任务时使用的不同函数。我们将学习如何注册在程序终止时自动执行的函数。我们将学习关于测量特定任务执行所需的时钟滴答和 CPU 秒数的函数。我们还将学习如何在运行时分配内存,并在任务完成后释放它。最后,我们将学习如何处理不同的信号。

在本章中,我们将深入以下食谱:

  • 注册在程序退出时调用的函数

  • 测量函数执行所需的时钟滴答和 CPU 秒数

  • 执行动态内存分配

  • 处理信号

然而,在我们继续之前,对动态内存分配和一些相关函数的简要介绍是必要的。

动态内存分配

正如其名所示,动态内存分配是在运行时分配内存的概念。与预定的静态内存分配不同,动态内存分配可以根据需要随时预订。静态分配的内存大小不能增加或减少,而动态分配的内存块的大小可以根据您的需求增加或减少。此外,当处理完成后,动态分配的内存可以释放,以便其他应用程序可以使用。以下小节描述了动态内存分配所需的几个函数。

malloc()

此函数动态分配内存,即在运行时。分配给定大小的内存块(以字节为单位),并返回指向该块的指针。以下是它的语法:

pointer = (data_type*) malloc(size_in_bytes)

此函数不会初始化分配的内存,因为该内存块最初包含一些垃圾值。

calloc()

此函数分配多个内存块,并返回指向该内存块的指针。以下是它的语法:

pointer=(data_type*) calloc( size_t num_of_blocks, size_t size_of_block )

此函数将分配的内存块初始化为零。

realloc()

正如其名所示,此函数用于重新分配或调整分配的内存大小。内存重新分配不会导致现有数据的丢失。以下是它的语法:

pointer= realloc(void *pointer, size_t new_blocksize);

在这里,pointer是指向现有分配内存块的指针。new_blocksize表示以字节为单位的新块大小,可以比现有分配块大小小或大。

free()

当分配的内存块中的任务或作业完成时,该内存块需要被释放,以便其他应用程序可以使用。为了释放动态分配的内存,使用free函数。以下是它的语法:

free(pointer);

在这里,pointer代表指向分配内存的指针。

让我们现在开始我们的第一个食谱!

注册在程序退出时调用的函数

我们的第一道菜谱将是注册一个在程序正常终止时自动执行的函数。为此菜谱,我们将使用atexit()函数。

atexit函数被设置为指向一个函数;这个函数在程序终止时将自动无参数调用。如果一个程序中定义了多个atexit函数,那么这些函数将以后进先出LIFO)的顺序调用,即atexit函数最后指向的函数将首先执行,然后是倒数第二个,依此类推。

atexit函数接受一个单一强制参数:程序终止时要调用的函数的指针。此外,如果函数注册成功,即要调用的函数成功指向,则函数返回0。如果没有注册,则函数返回非零值。

在这个菜谱中,我们将动态分配一些内存以接受用户输入的字符串。输入的字符串将在屏幕上显示,当程序终止时,注册的函数将自动执行,从而释放动态分配的内存。

如何做到这一点…

按照以下步骤创建一个注册程序正常终止时自动执行的函数的菜谱:

  1. 使用atexit函数注册一个函数。

  2. 动态分配一些内存,并允许该内存通过指针指向。

  3. 请求用户输入一个字符串,并将该字符串分配给动态分配的内存块。

  4. 在屏幕上显示输入的字符串。

  5. 当程序终止时,通过atexit函数注册的函数将自动调用。

  6. 注册的函数简单地释放动态分配的内存,以便其他应用程序可以使用。

注册程序在程序终止时自动执行的函数的程序如下(atexistprog1.c):

#include <stdio.h> 
#include <stdlib.h> 

char *str; 
void freeup() 
{ 
    free(str); 
    printf( "Allocated memory is freed  \n"); 
} 

int main() 
{ 
    int retvalue; 
    retvalue = atexit(freeup); 
    if (retvalue != 0) { 
        printf("Registration of function for atexit () function 
          failed\n"); 
        exit(1); 
    } 
    str = malloc( 20 * sizeof(char) ); 
    if( str== NULL ) 
    { 
        printf("Some error occurred in allocating memory\n"); 
        exit(1); 
    } 
    printf("Enter a string "); 
    scanf("%s", str); 
    printf("The string entered is %s\n", str); 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

使用atexit函数注册一个名为freeup的函数,确保如果程序正常终止,则freeup函数将被调用。检查atexit函数返回的值,确保它为零。如果atexit函数返回的值是非零值,则表示函数未成功注册,程序将在显示错误消息后终止。

如果函数注册成功,则动态分配 20 字节,并将分配的内存块赋值给字符指针 str。如果 str 指针为 NULL,则表示内存块分配过程中发生了错误。如果确认 str 指针不为 NULL 并指向一个内存块,用户将被要求输入一个字符串。用户输入的字符串将被赋值给由 str 指针指向的内存块。然后,用户输入的字符串将在屏幕上显示,最后程序终止。然而,在程序终止之前,使用 atexit 函数注册的 freeup 函数被调用。freeup 函数释放了分配给 str 指针的内存,并显示一条消息:“已分配的内存已释放”。

程序使用 GCC 编译,如下面的截图所示。因为在编译过程中没有出现错误,所以 atexistprog1.c 程序已成功编译成 .exe 文件:atexistprog1.exe。执行此文件时,用户将被提示输入一个字符串,该字符串将被分配给动态分配的内存。在程序终止时,使用 atexit 注册的函数被执行,释放了动态分配的内存,如下面的截图中的文本消息所确认:

图 14.1

Voilà! 我们已经成功注册了一个在程序退出时调用的函数。

还有更多...

如果通过 atexit 函数注册了多个函数,那么这些函数将按照后进先出(LIFO)的顺序执行。为了理解这一点,让我们修改前面的 atexistprog1.c 程序,通过 atexit 函数注册两个函数。我们将修改后的程序保存为 atexistprog2.c,如下所示(atexistprog2.c):

#include <stdio.h> 
#include <stdlib.h> 

char *str; 
void freeup1() 
{
    free(str); 
    printf( "Allocated memory is freed  \n"); 
} 

void freeup2()
{ 
    printf( "The size of dynamic memory can be increased and decreased  \n"); 
} 

int main() 
{
    int retvalue; 
    retvalue = atexit(freeup1); 
    if (retvalue != 0) { 
        printf("Registration of function freeup1() for atexit () 
          function failed\n"); 
        exit(1); 
    }
    retvalue = atexit(freeup2); 
    if (retvalue != 0) { 
        printf("Registration of function freeup2() for atexit () 
          function failed\n");
        exit(1); 
    }
    str = malloc( 20 * sizeof(char));
    if( str== NULL ) 
    { 
        printf("Some error occurred in allocating memory\n"); 
        exit(1); 
    } 
    printf("Enter a string "); 
    scanf("%s", str); 
    printf("The string entered is %s\n", str); 
}

编译并执行程序后,我们得到以下输出:

图 14.2

这个输出确认了最后注册的函数 freeup2 首先执行,然后是第一个注册的函数 freeup1

现在,让我们继续下一个菜谱!

测量函数执行所需的时钟滴答和 CPU 秒数

在这个菜谱中,我们将学习如何找出函数执行所需的时钟滴答和 CPU 秒数。我们将创建一个包含函数的程序。这个函数将简单地运行一个嵌套循环,我们将找出运行它所需的时间。为此,我们将使用 clock() 函数。

clock()函数返回程序消耗的处理器时间。本质上,这个时间取决于操作系统在分配资源给进程时使用的技巧。更精确地说,该函数返回从程序被调用时开始经过的时钟滴答数。该函数不需要任何参数,并返回运行某些语句所需的处理器时间,或者在出现任何故障时返回-1

函数返回的时间以每秒CLOCKS_PER_SECCLOCKS_PER_SEC来衡量,其中CLOCKS_PER_SEC根据操作系统而变化,其值大约为 1,000,000。因此,为了找到 CPU 使用的秒数,需要将函数返回的时钟滴答数除以CLOCKS_PER_SEC

clock()函数返回的值是clock_t数据类型。clock_t数据类型用于表示处理器时间。

如何操作...

按以下步骤查找运行函数所需的时钟滴答数和 CPU 秒数:

  1. 定义两个clock_t数据类型的变量来保存处理器时间。

  2. 调用clock()函数以确定从程序被调用时开始经过的时钟滴答数。时钟滴答数保存在其中一个变量中。

  3. 调用一个需要确定其处理时间的函数。

  4. 再次调用clock()函数,并将返回的时钟滴答数保存到另一个变量中。

  5. 从两个变量中的时钟滴答数中减去,以确定执行函数所需的时钟滴答数。

  6. 将前一步返回的时钟滴答数除以CLOCKS_PER_SEC,以确定函数使用的秒数。

    1. 屏幕上显示函数所需的时钟滴答数和 CPU 秒数。

知道执行函数所需的时钟滴答数和 CPU 秒数的程序如下(timecalc.c):

#include <time.h> 
#include <stdio.h> 

void somefunction() 
{ 
    for (int i=0; i<32000; i++) 
    { 
        for (int j=0; j<32000; j++) ; 
    } 
} 

int main() 
{ 
    clock_t clocktickstart, clocktickend; 
    double timeconsumed; 
    clocktickstart = clock();  
    somefunction(); 
    clocktickend = clock(); 
    timeconsumed = (double)(clocktickend - clocktickstart) / 
      CLOCKS_PER_SEC; 
    printf("Number of clocks ticks required in running the function is 
      : %.3f\n",  (double)(clocktickend - clocktickstart)); 
    printf("Time taken by program is : %.2f sec\n", timeconsumed); 
    return 0; 
}

现在,让我们深入了解幕后,以便更好地理解代码。

工作原理...

定义两个clock_t数据类型的变量,clocktickstartclocktickend,因为它们将用于表示处理器时间。这个程序的主要思想是确定函数执行过程中消耗的时间。

调用clock函数是为了知道从程序被调用以来经过的时钟滴答数。将返回的时钟滴答数赋值给clocktickstart变量。然后,调用一个somefunction()函数,该函数包含一个嵌套的for循环。使用嵌套循环的目的是让 CPU 在执行这些循环上投入一些时间。somefunction函数执行完毕后,再次调用clock()函数,并将从程序调用以来经过的时钟滴答数赋值给clocktickend变量。clocktickendclocktickstart变量之间的差值将给出执行somefunction函数所使用的时钟滴答数。然后,将时钟滴答数除以CLOCKS_PER_SEC以区分执行函数所使用的 CPU 秒数。最后,将执行somefunction函数所使用的时钟滴答数和它所使用的 CPU 秒数显示在屏幕上。

程序使用 GCC 编译,如下所示。由于编译过程中没有出现错误,timecalc.c程序成功编译成.exe文件:timecalc.exe。执行此文件后,屏幕上会显示程序中特定函数执行所需的时钟滴答数和 CPU 秒数,如下所示:

图片

图 14.3

现在,让我们继续下一个配方!

执行动态内存分配

在本配方中,我们将学习如何动态分配一些内存。我们还将学习如何增加内存块的数量,如何减少已分配内存块的数量,以及如何释放内存。

如何操作...

我们将询问用户需要分配多少内存块,并将动态分配相应数量的内存块。然后,将要求用户将这些内存块分配整数值。之后,将询问用户还需要分配多少额外的内存块。同样,将询问用户需要减少多少内存块。以下是通过增加和减少内存块动态分配内存的步骤:

  1. 用户被要求输入一个整数值,然后通过调用calloc函数动态分配相应数量的内存块。每个分配的内存块将能够存储整数数据类型的数值。

  2. 然后将要求用户在动态分配的内存块中输入值。

  3. 显示分配给内存块的整数值。

  4. 询问用户还需要添加多少内存块。

  5. 调用realloc函数来增加已分配内存块的数量。

  6. 要求用户在新增的内存块中输入整数值。

  7. 显示分配给内存块的整数值。

  8. 询问用户需要多少可用的内存块。

  9. 再次调用realloc函数以减少分配的内存块数量。

  10. 显示现有内存块中可用的整数值。

  11. 释放所有内存块,以便其他应用程序可以使用。

显示动态内存分配优势的程序,即如何在运行时分配内存,以及如何增加或减少其大小并释放它,如下所示(dynamicmem.c):

#include <stdio.h> 
#include <stdlib.h> 

int main() 
{ 
    int* ptr; 
    int m,n, i; 

    printf("How many elements are there? "); 
    scanf("%d", &n); 
    ptr = (int*)calloc(n, sizeof(int));
    if (ptr == NULL) { 
        printf("Memory could not be allocated.\n"); 
        exit(0); 
    } 
    printf("Enter %d elements \n", n); 
    for (i = 0; i < n; ++i) 
        scanf("%d",&ptr[i]); 
    printf("\nThe elements entered are: \n"); 
    for (i = 0; i < n; ++i) 
        printf("%d\n", ptr[i]); 
    printf("\nHow many elements you want to add more? "); 
    scanf("%d",&m); 
    ptr = realloc(ptr, (m+n) * sizeof(int)); 
    printf("Enter values for %d elements\n",m); 
    for (i = n; i < (m+n); ++i) 
        scanf("%d",&ptr[i]); 
    printf("\nThe complete set of elements now are: \n"); 
    for (i = 0; i < (m+n); ++i) 
        printf("%d\n", ptr[i]); 
    printf("\nHow many elements you want to keep ? "); 
    scanf("%d", &m); 
    ptr = realloc(ptr, (m) * sizeof(int)); 
    printf("\nThe new set of elements now are: \n"); 
    for (i = 0; i < m; ++i) 
        printf("%d\n", ptr[i]); 
    free(ptr);   
    return 0; 
}

现在,让我们深入了解代码背后的原理。

它是如何工作的...

将用户指定的元素数量分配给变量n。假设用户输入的值是4,然后将其分配给变量n。使用calloc函数,动态分配了4个内存块,每个内存块的大小等于int数据类型消耗的大小。换句话说,动态分配了一个可以存储四个整数值的内存块,并将指针ptr设置为指向它。如果ptr指针是NULL,则这意味着内存无法分配,程序将在显示错误消息后终止。

如果内存分配成功,将要求用户输入四个整数值。输入的值将被分配给由指针ptr指向的各个内存块。然后,在屏幕上显示用户输入的整数值。随后将询问用户是否想要添加更多元素。假设用户想要将两个额外的内存块添加到现有的已分配内存块中,用户输入的2值将被分配给变量m

使用realloc函数,内存块的数量从四个增加到六个,其中每个内存块能够存储一个整数。将要求用户输入两个新添加的内存块中的整数值。为了表示内存块的大小已从四个增加到六个,将显示分配给六个内存块的所有六个整数。之后,将询问用户想要保留多少个内存块中的六个内存块。假设用户输入的值是3;即用户想要保留前三个内存块中的整数,并丢弃其余的。

用户输入的3值将被分配给变量m。再次调用realloc函数,将内存块的数量从六个减少到三个。最后,在屏幕上显示三个内存块中的整数。

程序使用 GCC 编译,如下面的截图所示。由于编译过程中没有出现错误,dynamicmem.c 程序已成功编译成 .exe 文件:dynamicmem.exe。执行此文件后,用户会被提示定义他们想要动态分配多少内存块。之后,用户会被问及他们想要多少额外的内存块。用户还会被问及是否希望从总数中保留一些内存块以保持活跃,从而减少分配的内存块数量,最后,所有内存块都会被释放。所有这些操作如下所示:

图片

图 14.4

现在,让我们继续下一个食谱!

处理信号

在这个食谱中,我们将学习信号处理。我们将学习如何自动引发信号,用户如何通过操作引发信号,以及信号如何被引导到特定的信号处理函数。信号处理需要在信号发生时采取必要的行动。这些行动可能包括忽略信号、终止进程、阻塞或挂起进程、恢复进程等等。

在我们深入到食谱之前,让我们先快速了解一下信号。

信号

信号是通过软件生成的指示器,用于停止程序的常规执行,并通过 CPU 的一个分支执行一些特定任务。信号可以由进程生成,或者当用户按下 Ctrl + C 时生成。当执行操作时出现错误或发生某些错误时,信号充当进程和操作系统之间的通信媒介。信号由操作系统引发,并转发给进程以采取必要的行动。本质上,执行相应的信号处理程序作为纠正措施。

以下是一些你应该了解的重要信号:

  • SIGABRT信号终止):此信号报告程序的异常终止。在出现关键错误的情况下会引发此信号;例如,如果断言失败或无法分配内存,或者任何类似的内存堆错误。

  • SIGFPE信号浮点异常):此信号报告算术错误。任何算术错误,包括溢出或除以零,都包含在此信号中。

  • SIGILL信号非法指令):此信号报告非法指令。当程序尝试执行数据或可执行文件损坏时,会引发此类信号。换句话说,当程序尝试执行非可执行指令时,会引发此信号。

  • SIGINT信号中断):这是由用户通过按下 Ctrl + C 生成的程序中断信号。

  • SIGSEGV信号段违规):当程序尝试写入只读内存或没有写权限的块时,会触发此信号。当程序尝试读取或写入分配给它范围之外的内存时,也会触发此信号。

  • SIGTERM信号终止):这是一个发送给进程以终止它的终止信号。

要在程序中处理信号,使用 signal() 函数。让我们快速了解一下信号函数。

signal()

信号函数将信号的发生指向以下任一信号处理函数:

  • SIG_IGN:这将导致信号被忽略。

  • SIG_DFL:这将导致调用与触发信号关联的默认动作。

  • user_defined_function:这将导致在信号被触发时调用用户定义的函数。

现在,让我们开始编写步骤。

如何做到这一点...

将信号关联到信号处理函数、自动触发信号以及当用户触发信号时执行所需动作的步骤如下:

  1. 将一个信号中断 (SIGINT) 关联到一个函数。这个函数将作为信号处理函数。

  2. 在信号处理函数中编写一些代码。

  3. main 函数中,创建一个执行 5 次的 while 循环。你可以让 while 循环运行任意次数。while 循环被设置为在延迟 1 秒后显示一条文本消息。

  4. 策略是在 5 秒后自动触发信号。因此,在经过 5 次迭代后,while 循环结束,中断信号被自动触发。

  5. 执行相关的信号处理函数。

  6. 在信号处理函数中执行代码后,在 main 函数中,再次将信号中断 (SIGINT) 关联到另一个信号处理函数。

  7. 设置一个无限 while 循环执行,每次延迟 1 秒后显示一条文本消息。

  8. 如果用户按下 Ctrl + C,则信号中断被触发,并调用相关的信号处理函数。

  9. 在信号处理函数中,我们将信号中断与其默认动作关联。

  10. 因此,如果用户再次按下 Ctrl + C,即如果信号中断再次被触发,将执行默认动作:程序将终止。

显示信号如何自动触发、用户如何触发信号以及如何处理的程序如下(signalhandling.c):

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sighandler1(int signum) {
    printf("Ctrl + C is auto pressed \n"); 
} 

void sighandler2(int signum) { 
    printf("You have pressed Ctrl+c\n"); 
    printf("Press Ctrl+c again to exit\n"); 
    (void) signal(SIGINT, SIG_DFL); 
} 

int main () { 
    int x=1; 
    signal(SIGINT, sighandler1); 
    while(x<=5) { 
        printf("Signal will be raised automatically after 5 
          seconds\n");
        x++; 
        sleep(1); 
    }
    raise(SIGINT); 
    signal(SIGINT, sighandler2);
    while(1) {
        printf("Infinite loop, press Ctrl+C to raise signal\n"); 
        sleep(1); 
    } 
    return(0); 
}

现在,让我们幕后了解代码,以便更好地理解。

它是如何工作的...

使用 signal 函数,将信号中断 (SIGINT) 与名为 signalhandler1 的函数关联。也就是说,如果中断是自动生成的或是由用户生成的,sighandler1 函数将被调用。计数器 x 被初始化为 1,并设置一个 while 循环执行,直到计数器 x 的值大于 5。在 while 循环中,显示以下文本:“信号将在 5 秒后自动提升”。此外,在 while 循环中,计数器 x 的值增加 1。在 while 循环中插入 1 秒的延迟。简而言之,while 循环将在每次间隔 1 秒后显示一条文本消息。在显示文本消息 5 次后,调用 raise 函数提升 SIGINT 信号。

当提升 SIGINT 信号时,将调用 signalhandler1 函数。signalhandler1 函数除了显示一条文本消息:“Ctrl+C 已自动按下”外,不做任何操作。执行 signalhandler1 函数后,控制权恢复执行 main 函数中的语句。再次调用 signal 函数并将 SIGINT 信号与 sighandler2 函数关联。再次设置一个 while 循环执行;然而,这次循环将无限运行。在 while 循环中,显示一条文本消息:“无限循环,按 Ctrl+C 生成信号”。在显示文本消息后,插入 1 秒的延迟;也就是说,在 1 秒的间隔后,文本消息将无限期地显示。如果用户按下 Ctrl + C,将提升信号中断并调用 sighandler2 函数。

sighandler2 函数中,一行显示一条文本消息,“您已按下 Ctrl+C”,下一行显示“再次按 Ctrl+C 退出”。之后,调用 signal 函数将 SIGINT 信号设置为默认操作。SIGINT 中断的默认操作是终止并退出程序。这意味着如果用户再次按下 Ctrl + C,程序将终止。

该程序使用 GCC 进行编译,如下所示截图。由于编译过程中没有出现错误,signalhandling.c 程序已成功编译成 .exe 文件:signalhandling.exe。执行此文件后,通过第一个 while 循环在屏幕上显示五条文本消息。之后,信号自动提升,第一个信号处理器的文本消息出现在屏幕上。然后,无限 while 循环的文本消息出现。最后,当用户生成信号中断时,第二个信号处理器的输出出现。所有动作如下所示:

图 14.5

Voilà!我们已经成功处理了信号。

第十五章:提高代码性能

在本章中,我们将学习如何加快任何 C 程序的执行速度。我们将学习如何在 CPU 寄存器中保存频繁使用的内容,以及如何更快地从用户那里获取输入。我们还将学习如何在 C 程序中应用循环展开。

以下是本章我们将要处理的菜谱:

  • 在 C 代码中使用register关键字以获得更好的效率

  • 在 C 中更快地获取输入

  • 应用循环展开以获得更快的速度

让我们从第一个菜谱开始。

在 C 代码中使用register关键字以获得更好的效率

使用寄存器时的访问时间比从任何内存变量中访问内容低得多。因此,为了利用这一点,任何程序中频繁使用的内容都保存在寄存器中。使用register关键字来指示需要保存在这些寄存器中的内容。

在这个菜谱中,我们将找出指定距离租车所需的费用。租车费用不仅取决于距离,还取决于车型,即汽车是否有空调AC)。

如何操作...

使用寄存器变量查找指定距离和指定车型汽车总租金的步骤如下:

  1. 用户被要求输入计划旅程的距离。

  2. 用户被要求指定汽车的类型,即汽车是否应该有空调。

  3. 定义了两个寄存器变量,分别表示空调和非空调汽车的每公里租金。

  4. 定义了一个额外的寄存器变量,用于表示服务税百分比。

  5. 根据用户选择的车型,距离值乘以相应的寄存器变量以找出总金额。

  6. 服务税被计算并加到总金额上。服务税百分比是从相应的寄存器变量中获取的。

  7. 汽车总租金显示在屏幕上。

使用寄存器变量计算指定车型和行程长度的汽车总租金的程序如下:

//tourvehicle.c

#include <stdio.h> 
#include <string.h> 

int main() { 
    int distance;
    char car_type[20]; 
    register int Acperkm,Nonacperkm,servicetax; 
    float carRent, totalrent; 

    printf("How many kilometers? "); 
    scanf("%d", &distance); 
    printf("AC car or non AC ac/non? "); 
    scanf("%s", car_type); 
    Acperkm=3; 
    Nonacperkm=2; 
    servicetax=1; 
    if(strcmp(car_type, "ac")==0) 
        carRent=distance*Acperkm; 
    else 
        carRent=distance*Nonacperkm; 
    totalrent=carRent + (carRent*servicetax/100); 
    printf("The total rent for the car will be $ %.2f\n",totalrent); 
    return 0; 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

用户被要求指定需要租用多少公里。用户输入的值被分配给distance变量。之后,用户被要求指定他们想要租用的车型:空调车或非空调车。用户输入的选项被分配给car_type变量。定义了三个寄存器变量,分别命名为AcperkmNonacperkmservicetax

因为寄存器变量更靠近 CPU,与从内存变量访问内容相比,它们的访问时间非常低,所以寄存器变量用于那些在计算中频繁需要的值。三个寄存器变量AcperkmNonacperkmservicetax分别初始化为 3、2 和 1,以表示 AC 车的费用是每公里 3 美元,非 AC 车的费用是每公里 2 美元。服务税假定为总金额的 1%。

通过字符串比较来确定用户指定的车型。如果选定的车型是 AC 车,distance变量和Acperkm寄存器变量的值将被相乘。

同样,如果选定的车型是非 AC 车,distanceNonacperkm变量的值将被相乘。乘法的结果将被分配给carRent变量。然后,在这个总额上加上 1%的服务税,以计算出总租金。然后,在屏幕上显示指定距离和车型下汽车的租金总额。

程序使用 GCC 编译,如下面的截图所示。因为没有在编译时出现错误,这意味着tourvehicle.c程序已成功编译成 EXE 文件,即tourvehicle.exe。在执行文件时,用户将被提示输入租车所需的公里数。用户还将被要求指定所需的车型。然后程序显示汽车的租金总额,如截图所示:

图片

图 15.1

哇!我们已经成功使用寄存器变量来加速 C 语言中的处理。现在让我们继续下一个菜谱!

在 C 语言中更快地获取输入

在这个菜谱中,我们将学习如何从用户那里更快地获取输入。我们将要求用户输入一个数字,输入的数字将在屏幕上显示。为此,我们将使用getchar_unlocked()函数。

getchar_unlocked()函数与getchar()函数的工作方式类似,不同之处在于它不是线程安全的。因此,它忽略了某些输入约束,所以比getchar()快得多。它用于在仅使用单个线程处理输入和其他流的情况下获取长输入数据。

如何做到这一点...

使用快速输入方法从用户那里获取数字的步骤如下:

  1. 用户被要求输入一个数字。

  2. 用户将要输入的数字将通过getchar_unlocked()函数接受。该函数一次只接受一个数字。

  3. 用户输入的值首先会被检查以确保它仅是一个数字。如果不是,用户将被要求重新输入值。

  4. 如果用户输入的值是一个数字,它的 ASCII 值被保存在变量中。这是因为getchar_unlocked()将输入值的 ASCII 值赋给变量。

  5. 从输入值的 ASCII 码中减去 48,将其转换为用户实际输入的数字。

  6. 如果输入的数字是用户输入的第一个数字,那么它将被简单地赋给另一个变量。但如果它不是第一个数字,那么变量中现有的数字乘以 10,然后将新数字加到变量上。

  7. 步骤 27会重复进行,直到用户按下Enter键,对用户输入的每一位数字进行操作。

  8. 变量中的数字是用户实际输入的数字,因此显示在屏幕上。

使用快速输入技术输入数字的程序如下:

//fastinp.c

#include <stdio.h> 

int getdata() { 
    char cdigit = getchar_unlocked(); 
    int cnumb = 0; 
    while(cdigit<'0' || cdigit>'9') cdigit = getchar_unlocked(); 
    while(cdigit >='0' && cdigit <='9') { 
        cnumb = 10 * cnumb + cdigit - 48; 
        cdigit = getchar_unlocked(); 
    } 
    return cnumb; 
} 

int main() 
{ 
    int numb; 
    printf("Enter a number "); 
    numb=getdata(); 
    printf("The number entered is %d\n",numb); 
    return 0; 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

用户被要求输入一个数字。调用用户定义的getdata()函数,并将函数返回的值赋给numb变量,然后该变量被显示在屏幕上。getdata函数会不断请求输入数字的每一位,并在按下Enter键时返回该数字。

假设用户想要输入20。在getdata函数中,调用getchar_unlocked()函数。因此,在输入第一个数字 2(数字 20 的一部分)时,它将被赋给cdigit变量,该变量是字符数据类型。2 的 ASCII 值是 50,所以实际上将 50 赋给cdigit变量。

在继续之前,我们确保用户输入的值是一个数字,而不是字符或其他符号。如果用户输入的不是数字,则再次调用getchar_unlocked()函数,要求用户输入有效的数字。如果输入的值是数字,则从其 ASCII 值中减去 48 以将其转换为实际值。这是因为 2 的 ASCII 值是 50;从 50 中减去 48,结果是 2,这是用户实际输入的数字。2 的值被赋给cnumb变量。

由于数字 20 的下一位是 0,因此调用getchar_unlocked()函数,并将值 0 赋给cdigit变量。再次检查用户输入的值是否为数字,而不是其他。0 的 ASCII 值是 48。从 0 的 ASCII 值中减去 48,使其值变为 0。cnumb变量中的当前值是 2,然后将其乘以 10,并将cdigit的值加到结果中。这次计算的结果将是 20,并将其赋给cnumb变量。cnumb变量中的值返回到主函数以显示。

简而言之,无论用户输入的是哪个数字,其 ASCII 值都会被分配给变量,并从该数字的 ASCII 值中减去数值 48,以将其转换为用户实际输入的数字。

程序使用 GCC 编译,如下面的截图所示。因为没有错误出现在编译过程中,这意味着fastinp.c程序已经成功编译成 EXE 文件,fastinp.exe。在执行文件时,用户被提示输入一个数字。该数字使用快速输入技术接受。在输入所有数字后,当用户按下Enter键时,输入的数字将显示在屏幕上,如下面的截图所示:

图 15.2

Voilà!我们已经成功配置了 C 语言中数字的快速输入。现在让我们继续下一个菜谱!

应用循环展开以获得更快的速度

在这个菜谱中,我们将学习如何使用循环展开技术打印用户输入限制的从 1 到限制的数字序列之和。循环展开意味着减少或从程序中移除循环以减少运行循环时的开销。基本上,为了运行一个循环,操作系统必须管理两个开销——第一个开销是维护循环计数,第二个开销是进行条件分支。循环展开有助于避免这两个开销。让我们看看它是如何做到的。

如何做到这一点...

使用循环展开技术求前n个数字序列之和的步骤如下:

  1. 将用于存储序列数加和的sum变量初始化为 0。

  2. 用户被要求输入一个限制,即希望求和的数字序列的上限。用户输入的值被分配给limit变量。

  3. 我们需要找到一个在 9 到 1 之间的数字,它能完美地整除limit变量中的值。为了找到这个数字,我们设置一个从 9 到 1 的for循环。

  4. for循环中,limit变量中的值被除以for循环变量。

  5. 如果limit变量中的数字可以被for循环变量整除,则for循环将中断。

  6. 如果limit变量中的数字不能被for循环变量整除,则循环将使用减少的值执行下一次迭代,即使用值 8。重复这些步骤,直到limit变量中的值能被for循环变量完美整除。

  7. 一旦我们得到了可以整除 limit 的整数,我们就将for循环的数量减少那个整数,即,将for循环的增量设置为那个整数值。

  8. for循环中,使用了一个while循环,该循环将数字序列添加到sum变量中。

  9. 最后,将sum变量中数字序列的加和显示在屏幕上。

使用循环展开技术打印数字序列和的程序如下:

//loopunrolling.c

#include <stdio.h> 

int main() { 
    int sum,i,limit,rem,quot,incr,x, count; 
    sum = 0; 
    printf("Enter limit "); 
    scanf("%d", &limit); 
    for(i=9;i>=1;i--) 
    { 
        rem=limit % i; 
        if (rem==0) break; 
    } 
    incr=i; 
    count=0; 
    for(i=1;i<=limit; i+=incr) 
    { 
        x=0; 
        while(x<incr) 
        { 
            sum += i+x; 
            x++; 
        } 
        count++; 
    }
    printf("The sum of first %d sequence numbers is %d\n",limit, sum);
    printf("The loop executed for %d number of times\n",count);
    return 0; 
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

这个程序找出用户输入的上限值以内的序列数字之和。程序会要求用户输入上限值,用户输入的值被分配给 limit 变量。为了加和序列数字,我们将使用一个 for 循环。为了进行循环展开或减少循环的迭代次数,我们找到可以整除上限的整数。也就是说,我们将 limit 变量中的值除以从 9 到 1 的整数。一旦我们得到可以整除上限的整数,我们就减少 for 循环的次数。

假设用户输入了一个上限值 40,并将其分配给 limit 变量。设置一个 for 循环从 9 到 1 运行,并且从 9 到 1 的每个值都将用来尝试除以 limit 变量中的值。在任何除法中,如果余数为 0,则 for 循环将中断;否则,将执行下一个迭代,并使用减少的值。目前,limit 变量中的值是 40,第一次迭代的 i 值是 9。40 除以 9 的余数是一个非零值,所以 for 循环的下一个迭代将从下一个减少的值开始,即 8。

因为,当 40 除以 8 时,你得到一个余数为 0,for 循环将中断,控制将跳转到 for 循环后的下一个语句。那时的 i 值是 8,所以 8 的值被分配给 incr 变量。也就是说,for 循环将以 8 的增量增加。这也意味着我们通过减少 for 循环的迭代次数 8 倍来应用循环展开。换句话说,for 循环将被设置为从 1 运行到上限 40,每次迭代后增加 8。

在第一次迭代中,i 的值为 1。用于计算数字序列加和的 sum 变量被初始化为 0。i 的值被加到 sum 变量上。正如之前所说的,for 循环的下一个迭代将 i 的值增加 8。所以,在 for 循环内部,使用了一个 while 循环。在 while 循环内部,使用了一个变量 x,它从 0 执行到 incr 变量的值(即,直到 8 的值)。换句话说,while 循环将把从 1 到 8 的数字序列加到 sum 变量中。

一旦计算并分配了数字序列前八个值的总和到sum变量,for循环的下一个迭代将从i的值增加至 9 开始。再次在for循环内,while循环将执行以计算从 9 到 16 的数字序列的总和。同样,for循环的下一个迭代将i的值增加到 17。这个过程会一直持续到for循环完成。简而言之,for循环被展开到incr变量所赋的值。最后,数字序列的总和在屏幕上显示。

程序使用 GCC 编译,如下面的截图所示。因为没有错误出现在编译过程中,这意味着loopunrolling.c程序已经成功编译成 EXE 文件,名为loopunrolling.exe。在执行文件时,用户将被提示输入想要计算数字序列总和的上限。程序不仅会打印数字序列的总和,还会打印出计算总和所需的循环迭代次数,如下面的截图所示:

截图

图 15.3

Voilà! 我们已经成功执行了循环展开以生成更快的结果。

第十六章:低级编程

有时,为了获得精确的结果并克服编程语言的限制,您需要控制 CPU 寄存器的内容在位级别。在这种情况下,您可以利用以下两个东西:位运算符和汇编语言编程。

在本章中,我们将学习如何执行以下操作,以便在 C 中进行底层编程:

  • 使用位运算符将二进制数转换为十进制

  • 使用位运算符将十进制转换为二进制

  • 使用位掩码将十进制数转换为二进制

  • 使用 C 中的内联汇编语言进行乘法

  • 使用 C 中的汇编代码进行除法

位运算符简介

我们将任何数字输入任何变量时,都是内部以二进制位的形式存储。为了执行位级操作,C 提供了以下位运算符。

&(二进制与)

如果两个操作数都是 1,则结果为二进制 1。如果任一位是 0,那么&操作的结果为 0。

假设操作数 A 的值为 1010,操作数 B 的值为 0111,那么 A&B 的结果如下:

A 1010
B 0111
A&B 0010

|(二进制或)

如果任一操作数为 1,则结果为二进制 1。如果两个位都是 0,那么|操作的结果为 0。

假设操作数 A 的值为 1010,操作数 B 的值为 0111,那么 A|B 的结果如下:

A 1010
B 0111
A B

^(二进制异或)

如果任一操作数为 1 但不是两者都为 1,则结果为二进制 1。如果两个位都是 0 或都是 1,则^操作的结果为 0。

假设操作数 A 的值为 1010,操作数 B 的值为 0111,那么 A^B 的结果如下:

A 1010
B 0111
A^B 1111

~(二进制补码)

这将反转操作数的二进制位。也就是说,二进制位 1 将转换为 0,反之亦然。假设操作数 A 的值为 1010,那么~A 的结果如下:

A 1010
~A 0101

<<(二进制左移)

这将操作数的二进制位向左移动指定的位数,并在最低有效位之后创建的空位用 0 填充。

假设操作数 A 的值为 00001010,那么将 A 左移 2 位(A<<2)将得到以下结果:

A 00001010
A<<2 00101000

在每次左移时,操作数的值乘以 2 的幂。也就是说,如果操作数左移 2 位,那么它乘以 2 x 2,即 4。

>>(二进制右移)

这个操作将操作数的二进制位向右移动指定的位数,并在最高有效位之后创建的空位用 0 填充。

假设操作数 A 的值为 00001010,那么将 A 右移 2 位(A>>2)将得到以下结果:

A 00001010
A>>2 00000010

你可以看到,在右移时,最低有效位被丢弃。在每次右移时,操作数的值被除以 2 的幂。也就是说,如果操作数向右移动 2 位,这意味着它被除以 2 x 2,即 4。

让我们继续通过制作一些实际的工作食谱来获取一些实际知识。第一个食谱是下一个。

使用位运算符将二进制数转换为十进制数

在这个过程中,你将学习如何将二进制数转换为十进制数。

如何操作...

要将二进制数转换为十进制数,执行以下步骤:

  1. 输入一个二进制数。

  2. 将模 10 (% 10) 操作符应用于二进制数的二进制数字,以隔离二进制数的最后一位。

  3. 将在 步骤 2 中隔离的二进制数字左移,乘以 2 的幂。

  4. 将前一次乘法的结果加到将要存储结果的变量中,即十进制数。我们可以称这个变量为 dec

  5. 二进制数的最后一位被截断。

  6. 重复 步骤 2步骤 4,直到二进制数字的所有位都处理完毕。

  7. dec 变量中显示十进制数。

将二进制数转换为十进制数的程序如下:

binintodec.c
#include <stdio.h>
void main()
{
    int num,bin,temp,dec=0,topower=0;

    printf("Enter the binary number: ");
    scanf("%d",&bin);
    temp=bin;
    while(bin >0)
    {
        num=bin %10;
        num=num<<topower;
        dec=dec+num;
        topower++;
        bin=bin/10;
    }    
    printf("The decimal of %d is %d\n",temp,dec);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

你将被提示输入一个二进制数。你输入的数字将被分配给 bin 变量。二进制数临时分配给一个名为 temp 的变量。执行一个 while 循环,直到 bin 变量中的二进制数变为 0。

假设输入到 bin 变量的二进制数是 1101。然后,我们将对 bin 变量中的二进制数字应用 mod (%) 操作符,以隔离其最后一位。实际上,% 操作符除以指定的数字并返回余数。也就是说,当 1 1 0 1 应用 % 10 时,它将返回 1,然后分配给 num 变量。

topower 变量初始化为 0topower 变量的目的是将数字左移,即乘以 2 的幂。num 变量中的二进制数字 1 被添加到另一个名为 dec 的变量中。topower 变量的值增加至 1bin 变量中的二进制数 1 1 0 1 通过除以 10 并去除小数部分被截断为 1 1 0

再次,整个过程重复进行。通过应用 %10 操作符,bin 变量中的最后一个数字被隔离;也就是说,0 将从 1 1 0 中隔离出来并分配给 num 变量。二进制数字 0 向左移动 1 位,变成 0 0。因此,0 的值被加到 dec 变量的值上;也就是说,dec 变量的值保持为 1topower 的值增加至 2。通过除以 10bin 变量中二进制数字 1 1 0 的最后一个数字被移除;因此,bin 变量中的二进制数字将变为 1 1

再次,将 1 1 应用 %10;余数将是 1,它将被分配给 num 变量。二进制位 1 向左移动了 2 位,变成了 1 0 0。二进制值 1 0 0 代表 4,然后将其加到 dec 变量中的值上。dec 变量中的值原本是 1,加上 4 后,dec 变量中的总和将变为 5。再次,topower 变量中的值将增加,使其值变为 3bin 变量中二进制位 (1 1) 的最后一位将被除以 10 截断。因此,bin 变量中的数字将变为 1

再次,%10 被应用于 bin 变量中的二进制位 1。因此,1 将被分配给 num 变量。num 变量中的二进制位 1 向左移动了 3 位,变成了 1 0 0 0。二进制值 1 0 0 0 代表 8,然后将其加到 dec 变量中的值上。dec 变量当前的值是 5。加上 8 后,dec 变量中的值将变为 13topower 变量的值增加至 4bin 变量中的二进制值 1 除以 10,变成了 0while 循环将终止,dec 变量中的十进制值 13 将显示在屏幕上。

整个过程可以如下表示:

图 16.1

让我们使用 GCC 编译 binintodec.c 程序如下:

D:\CBook>gcc binintodec.c -o binintodec

如果你没有错误或警告,这意味着 binintodec.c 程序已编译成可执行文件,binintodec.exe。让我们按照以下方式运行这个可执行文件:

D:\CBook>binintodec
Enter the binary number: 1101
The decimal of 1101 is 13

哇!我们已经成功使用位运算符将二进制数转换为十进制数。现在,让我们继续下一个菜谱!

使用位运算符将十进制转换为二进制

在这个菜谱中,我们将学习如何通过使用位运算符将十进制数转换为二进制数。位运算符在数字的二进制位上操作,使我们能够进行精确的操纵,以满足我们的需求。

如何做到这一点…

要通过使用位运算符将十进制数转换为二进制数,执行以下步骤:

  1. 输入一个十进制数。这个数字以二进制位的形式内部存储。

  2. 通过在输入的十进制数和值 1 之间应用逻辑与操作来隔离十进制数的最低有效位。

  3. 步骤 2 得到的最低有效位被存储在数组中。

  4. 将十进制数的二进制位向右移动 1 位。向右移动时,第二位最低有效位将变成最低有效位。

  5. 重复 步骤 24,直到将十进制数的所有二进制位放入数组中。

  6. 分配给数组的二进制位是输入的十进制数的二进制版本。将二进制位显示在数组中以获得结果。

使用位运算符将十进制数转换为二进制数的程序如下:

convertintobin.c
#include <stdio.h>
void main()
{
    int num,i,x,temp;
    int p[10];
    printf("Enter Decimal Number : ");
    scanf("%d",&num);
    temp=num;
    x=0;
    while(num > 0)
    {
        if((num & 1) == 0 )
        {
            p[x]=0;                                                            x++;
        }
        else
        {
            p[x]=1;
            x++;
        }
        num = num >> 1;
    }
    printf("Binary of %d is ",temp);
    for(i=x-1;i>=0;i--)printf("%d",p[i]);
}

现在,让我们看看幕后。

它是如何工作的...

你将被提示输入一个十进制数。你输入的数字将被分配给num变量。num变量中输入的值暂时分配给另一个变量temp。设置一个while循环,直到num的值变为0时执行。应用逻辑 AND 操作以隔离数字的每个二进制位。例如,如果变量num中输入的值是13,那么,在内部,它将以以下二进制格式存储:

图片

图 16.2

现在,通过应用 AND 操作来隔离最低有效位。也就是说,13的二进制数字与1进行 AND 操作如下。AND 操作意味着在13的二进制数字和1上应用 AND 操作:

图片

图 16.3

对二进制数字11进行 AND 操作的结果是1。如果任一二进制数字是0,那么 AND 操作的结果将是0。因此,num1的 AND 操作的结果将是1,然后将被存储到数组p的索引位置0,如图下所示:

图片

图 16.4

之后,将num变量中的数字右移 1 位。在右移时,最低有效位1将被移除,并在最高有效位添加一个0。再次,num变量中的新二进制数字集与1进行 AND 操作,即,在num变量中的新二进制数字集和1之间应用 AND 操作。num1的 AND 操作的结果将是0,然后将其分配给数组p的索引位置1;即,0将被分配到p[1]位置,如图所示:

图片

图 16.5

再次,将num变量的数字右移 1 位。再次,最低有效位,0,将被移除,并在最高有效位添加一个0。再次,num变量中的新二进制数字集与1进行 AND 操作,如图图 16.6(a)所示。num变量与1进行 AND 操作的结果将是1,然后将其分配给数组p的索引位置2。之后,再次将num变量中的数字右移 1 位。num变量中的最高有效位1将被移除,并在最高有效位位置添加一个0位。num中的二进制数字再次与1进行 AND 操作。这里的 AND 操作意味着在num的二进制数字和1之间应用 AND 操作。AND 操作的结果将是1,然后将被分配给数组p的索引位置p[3]图 16.6(b)):

图片

图 16.6 (a) 和 (b)

现在,分配给数组p的二进制位是分配给变量num的数字的二进制转换。只需以相反的顺序显示数组p中的二进制位即可得到结果。因此,1 1 0 113的二进制转换。

让我们使用 GCC 编译convertintobin.c程序,如下所示:

D:\CBook>gcc convertintobin.c -o convertintobin

如果没有错误或警告,这意味着convertintobin.c程序已编译成可执行文件convertintobin.exe。让我们按照以下方式运行这个可执行文件:

D:\CBook>convertintobin
Enter Decimal Number : 13
Binary of 13 is 1101

哇!我们已经成功使用位运算符将十进制数转换为二进制数。现在,让我们继续下一个菜谱!

使用位掩码将十进制数转换为二进制数

在这个菜谱中,我们将学习如何通过掩码寄存器的一些位将十进制数转换为二进制数。掩码意味着隔离或分离出所需的二进制位。掩码隐藏了不需要的二进制位,只使所需的二进制位可见。

如何做到这一点...

要使用位掩码将十进制数转换为二进制数,请执行以下步骤:

  1. 输入一个十进制值。输入的十进制数以二进制位的形式内部存储。

  2. 将一个由一个数字1后跟 31 个零组成的数字赋值给一个名为mask的变量。

  3. 逐个掩码十进制数的一位二进制数,从最高位开始。在输入的十进制数的二进制数和mask变量的二进制数之间应用 AND 操作。

  4. mask变量中的二进制位右移1位,使其变为0 1后跟 30 个零。

  5. 重复此过程。在输入的十进制数和mask变量之间应用 AND 操作,并将结果二进制位显示在屏幕上。当mask变量中的值变为0时,重复此过程。

将十进制数转换为二进制数的位掩码程序如下:

decintobin.c
#include <stdio.h>
void main()
{
    int i, totbits;
    unsigned mask,num;
    printf("Enter decimal value: ");
    scanf("%d", &num);
    totbits=32;
    mask = 1 << (totbits - 1);
    for(i = 0; i < totbits; i++)
    {
        if((num & mask) == 0 )
            printf("0");
        else
            printf("1");
        mask >>= 1;
    }
}

现在,让我们看看幕后。

它是如何工作的...

您将被提示输入一个十进制值。您输入的十进制值将被分配给num变量。让我们假设您输入了一个值为13。此值将以以下形式作为二进制位的形式在num变量中内部存储:

图 16.7

我们将设置totbits变量为32位,因为 C 语言中的int数据类型由32位组成,我们必须掩码num变量中的每个位以显示其二进制版本。我们将定义一个mask变量并将其赋值为1。为了使mask变量中的1值显示为10000...00,即1后跟 31 个零,我们将1左移31位,如下所示:

图 16.8

现在,我们将执行一个 for 循环 32 次,以屏蔽或隔离 num 变量中的每个位并显示它。在 for 循环中,我们将对 nummask 变量应用 AND 操作。因此,这两个变量的每个二进制位都将进行 AND 操作。我们知道,在 AND 操作中,只有当两个位都是 1 时,输出才是 1。如果任一位是 0,AND 操作将返回 0

图 16.9 显示了在 nummask 变量上应用 AND 操作:

图 16.9

因此,返回值是 0。之后,我们将 mask 变量的二进制位向右移动 1 位,使其变成 0 1 后面跟着 30 个零。再次,当在 nummask 变量之间应用 AND 操作时,结果将是 0(参见图),然后显示在屏幕上。所以,到目前为止,屏幕上显示的是 0 0

图 16.10

再次,我们将 mask 变量的二进制位向右移动 1 位,使其变成 0 0 1 后面跟着 29 个零。再次,当在 nummask 变量之间应用 AND 操作时,结果将是 0,然后显示在屏幕上。

下一个 25 位的过程和输出将相同;也就是说,屏幕上会有 28 个零。之后,当我们对 mask 变量的二进制位应用另一个右移操作时,它将变成 1 0 0 0。在 nummask 变量之间应用 AND 操作后,我们将得到一个输出 1,然后显示在屏幕上。所以,到目前为止,屏幕上有 28 个零后面跟着 1 位:

图 16.11

随着我们不断重复这个程序,我们将得到 图 16.12 中显示的输出。屏幕上会有 28 个零,后面跟着 1 1 位 (图 16.12 (a))。再次重复后,屏幕上会有 28 个零,后面跟着 1 1 0 位 (图 16.12 (b))。在 for 循环的最终执行中,分配给 num 变量的数字的最终二进制版本将是 28 个零,后面跟着 1 1 0 1 (图 16.12 (c)):

图 16.12 (a), (b), 和 (c)

让我们使用 GCC 按以下步骤编译 decintobin.c 程序:

D:\CBook>gcc decintobin.c -o decintobin

如果你没有错误或警告,这意味着 decintobin.c 程序已编译成可执行文件,decintobin.exe。让我们按照以下步骤运行这个可执行文件:

D:\CBook>decintobin
Enter decimal value: 13
00000000000000000000000000001101

哇!我们已经成功使用位屏蔽将十进制数字转换为二进制。

汇编语言编程简介

x86 处理器有八个 32 位通用寄存器。其中一些通用寄存器的名称是 EAX、EBX、ECX 和 EDX。这些寄存器可以在子部分中使用。例如,EAX 的最低 2 个字节可以用作 16 位寄存器 AX。再次,AX 的最低字节可以用作 8 位寄存器 AL,而 AX 的最高字节可以用作 8 位寄存器 AH。同样,BX 寄存器可以用作 BH 和 BL 寄存器,依此类推。

我们将在本章中编写内联汇编代码,因为这段代码在代码生成期间很容易与 C 代码集成。因此,编译器会对 C 和汇编代码进行优化,以生成高效的对象代码。

使用内联汇编代码的语法如下:

asm [volatile] (
asm statements
: output statements
: input statements
);

asm语句被引号包围,输出和输入以`"约束"(名称)对的形式呈现,由逗号分隔。约束可以是以下任何一种:

约束 用法
g 编译器将决定用于变量的寄存器
r 将数据加载到任何可用的寄存器
a 将数据加载到eax寄存器
b 将数据加载到ebx寄存器
c 将数据加载到ecx寄存器
d 将数据加载到edx寄存器
f 将数据加载到浮点寄存器
D 将数据加载到edi寄存器
S 将数据加载到esi寄存器

输出和输入通过数字引用。

使用 C 中的内联汇编语言乘以两个数字

在这个菜谱中,我们将学习如何使用 C 中的内联汇编语言来乘以两个数字。通过使用内联汇编代码,我们可以更好地控制 CPU 寄存器,在位级别上操作它们的值,并利用 C 的优势。

如何做到这一点...

要使用 C 中的内联汇编语言乘以两个数字,请执行以下步骤:

  1. 将要乘的两个值加载到eaxebx寄存器

  2. eaxebx寄存器的内容相乘,并将结果存储在eax寄存器中

  3. 在屏幕上显示eax寄存器的内容

使用内联汇编代码乘以两个数字的程序如下:

#include <stdio.h>
#include <stdint.h>
int main(int argc, char **argv)
{
    int32_t var1=10, var2=20, multi = 0;
    asm volatile ("imull %%ebx,%%eax;"
        : "=a" (multi)          
        : "a" (var1), "b" (var2) 
    );
    printf("Multiplication = %d\n", multi);
    return 0;
}

现在,让我们看看幕后。

它是如何工作的...

让我们将我们想要乘的两个数字分配给两个整数变量,var1var2。然后,我们将var1变量的内容加载到eax寄存器,将var2变量的内容加载到ebx寄存器。我们将乘以eaxebx寄存器的内容,并将结果存储在eax寄存器中。

eax寄存器的内容被分配给multi变量。这个变量中包含两个变量的乘积,其内容将在屏幕上显示。

让我们使用 GCC 编译multiasm.c程序,如下所示:

D:\CBook>gcc multiasm.c -o multiasm

如果你没有错误或警告,这意味着multiasm.c程序已编译成可执行文件,multiasm.exe。让我们按照以下步骤运行这个可执行文件:

D:\CBook>multiasm
Multiplication = 200

哇!我们已经成功使用 C 语言中的内联汇编语言乘以两个数。现在,让我们继续下一个菜谱!

使用 C 语言中的汇编代码进行两个数的除法

在这个菜谱中,我们将学习如何使用 C 语言中的内联汇编语言除以两个数。汇编语言为我们提供了更好的对 CPU 寄存器的控制,因此我们必须手动将除数和被除数放入各自的寄存器中。此外,在除法之后,商和余数将自动保存在各自的寄存器中。

如何做到这一点…

要使用 C 语言中的汇编代码除以两个数,请执行以下步骤:

  1. 将被除数加载到eax寄存器中。

  2. 将除数加载到ebx寄存器中。

  3. edx寄存器初始化为零。

  4. 执行divl汇编语句,将eax寄存器的内容除以ebx寄存器的内容。通过这种方式进行除法,商将被分配给任何可用的寄存器,余数将被分配给ebx寄存器。

  5. 从可用寄存器中检索商,从ebx寄存器中检索余数,并在屏幕上显示。

使用内联汇编代码除以两个数字的程序如下:

asmdivide.c
#include <stdio.h>
void main() {
    int var1=19,var2=4, var3=0, remainder, quotient;
    asm("divl %%ebx;"
        "movl %%edx, %0"
        : "=b" (remainder) , "=r" (quotient)
        : "a" (var1), "b" (var2), "d" (var3) 
    );
    printf ("On dividing %d by %d, you get %d quotient and %d remainder\n", var1, var2, quotient, remainder);
}

现在,让我们看看幕后。

它是如何工作的...

让我们将要除以的两个数分配给两个变量var1var2。将被除数分配给var1,除数分配给var2。之后,我们将从var1变量中将被除数加载到eax寄存器中,并将除数从var2变量中加载到ebx寄存器中。

edx寄存器必须初始化为零。为此,我们将一个var3变量初始化为零。从var3,零值被加载到edx寄存器中。然后,我们将执行divl汇编语句,将eax寄存器的内容除以ebx寄存器的内容。通过这种方式进行除法,商将被分配给任何可用的寄存器,余数将被分配给ebx寄存器。

从可用寄存器中将商加载到名为quotient的变量中,将ebx寄存器中的余数加载到另一个名为remainder的变量中。最后,在屏幕上显示商和余数的值。

让我们使用 GCC 按照以下步骤编译asmdivide.c程序:

D:\CBook>gcc asmdivide.c -o asmdivide

如果你没有错误或警告,这意味着asmdivide.c程序已编译成可执行文件,asmdivide.exe。让我们按照以下步骤运行这个可执行文件:

D:\CBook>asmdivide
On dividing 19 by 4, you get 4 quotient and 3 remainder

哇!我们已经成功使用 C 语言中的汇编代码除以两个数。

第十七章:嵌入式软件和物联网

当涉及到更高效、更精确地执行特定任务时,嵌入式系统更受欢迎。它们作为独立组件工作,并且可以组合成更大的设备。互联网是一个庞大且无穷无尽的信息来源;因此,物联网(IoT)在使嵌入式设备更智能、以便远程管理和控制方面发挥着重要作用。

在本章中,我们将深入探讨以下与嵌入式软件和物联网相关的食谱:

  • 在嵌入式 C 中切换微控制器的端口(闪烁 LED)

  • 在嵌入式 C 中增加端口的值

  • 使用 Arduino 切换输出引脚的电压(闪烁 LED)

  • 使用 Arduino 从串行端口获取输入

  • 使用 LM35 传感器通过 Arduino 感知温度

技术要求

对于 C 语言嵌入式编程,我们将使用 Keil MDK,它为广泛的基于 ARM Cortex-M 的微控制器设备提供了一个软件开发环境。MDK 提供了非常易于使用的µVision IDE、Arm C/C++编译器和其他库。您可以从以下 URL 下载 Keil MDK:www.keil.com/download/。让我们看看以下步骤:

  1. 下载以下三个可执行文件。您可能找不到完全相同的文件名,但它们将大致相似:

    • mdk526.exe:这为 ARM 设备提供了一个开发环境。

    • c251v560.exe:这为所有 80251 设备提供了开发工具。

    • c51v959.exe:这为所有 8051 设备提供了开发工具。

  2. 逐个双击这些可执行文件,并按照设置对话框安装这三个 Keil 产品。

在成功安装这些产品后,您将在桌面上找到一个名为 Keil uVision5 的图标。该图标代表集成开发环境(IDE),它使我们能够编写、编辑、调试和编译程序。编译器将源代码转换为 HEX 文件,然后可以将其烧录到目标芯片上。

为了与 Arduino 一起工作,您必须购买 Arduino 板并从www.arduino.cc/en/main/software下载 Arduino IDE。

在编写本章时,可用的最新 Arduino IDE 版本是 1.8.8。下载的可执行文件将是arduino-1.8.8-windows.exe。将 Arduino 板连接到您的 PC,并简单地双击可执行文件以安装 Arduino IDE。安装成功后,您将在桌面上找到 Arduino IDE 图标。

嵌入式系统简介

嵌入式系统是由计算机硬件和软件的组合,旨在在更大的设备中执行特定功能。在工业、汽车、医疗程序、家用电器和移动设备中使用的重型设备都使用了嵌入式系统。大多数嵌入式系统使用 RISC 家族微控制器,例如 PIC 16F84、Atmel 8051 或 Motorola 68HC11。可以将多个输入和输出设备连接到嵌入式系统的微控制器上,例如液晶显示屏、键盘、打印机、传感器。这些设备可以控制多个其他设备,如风扇、电机、灯泡、洗衣机、烤箱、空调控制器、汽车、打印机等等。

要编程微控制器以执行特定任务,需要通过连接到插槽将微控制器与 PC 连接。可以使用汇编程序或嵌入式 C 来编写和烧录程序到微控制器上。程序可以存储在微控制器的 EPROM(缩写为 Erasable Programmable Read-Only Memory)。它是一种内部、只读内存,当暴露在紫外光源下时可以编程和擦除。我们将使用 Keil 等软件用嵌入式 C 开发嵌入式系统应用程序。

物联网(IoT)简介

物联网(IoT)是一个由硬件和软件系统或设备组成的架构,这些设备通过 WiFi、以太网等多种方式连接到互联网。当 Web API 和其他协议结合使用时,提供了一个环境,允许智能嵌入式设备连接到互联网。因此,它使我们能够从远程地区访问数据,并通过互联网控制或触发各种设备上的某些操作。换句话说,物联网是一个相互关联的嵌入式计算设备系统,这些设备具有通过网络传输数据并采取必要行动的能力。Arduino 被认为是嵌入式物联网的最佳起点。为了使 Arduino 能够作为物联网设备工作,需要 Android 和以太网盾。

让我们快速了解一下 Arduino。

Arduino 简介

Arduino 是一个包括 Atmel 微控制器系列和标准硬件的架构。Arduino 的引脚图如下:

图片

Arduino 包含 14 个数字引脚,可以与 5V 操作:

  • 引脚 0RXD)和 1TXD)是用于传输 TTL 串行数据的串行引脚。

  • 引脚 23 是外部中断引脚,用于激活中断。

  • 引脚 35691011 用于提供 PWM 输出。

  • 引脚 10111213SPI 引脚(缩写为 Serial Peripheral Interface)。命名为 SSMOSIMISOSCK,这些引脚用于 SPI 通信。

  • 引脚 13 是 LED 引脚。当向该引脚提供高数字值时,LED 会发光。

  • 模拟引脚 45 分别称为 SDASCL,用于 TWI(代表 Two-Wire Interface)的通信。

  • AREF(代表 Analog Reference)引脚用于连接到外部电源的某些参考电压。

  • RESET(或 RST)引脚用于重置微控制器。

就软件而言,Arduino 随附一个 IDE,我们可以使用它来编写和编辑应用程序,甚至可以将其上传以执行特定任务。此 IDE 包括对 C 和 C++ 编程语言的编程支持,并包含几个库,使软件开发者的工作变得相当容易。此外,IDE 提供通信窗口,以便将数据输入板以及获取输出。

Arduino 板提供端口以连接 LCD、继电器等设备到其输出引脚,并提供输入引脚以从传感器、继电器等设备输入信息。Arduino 板可以通过 USB 或连接 9V 电池供电。

在使用 Arduino 编程时,我们将使用以下函数:

  • Serial.begin() 用于在建立 Arduino 板与 PC 之间的通信时设置数据速率。为了通过串行数据传输与计算机通信,我们首先需要设置每秒的比特率(波特率)。我们可以使用任何波特率,例如 300、600、1200、2400、4800、9600、14400、19200、28800、38400、57600 或 115200。

  • Serial.println() 用于以人类可读的格式向串行端口显示消息。它在串行监视器上显示消息,后跟换行符。您需要按 Ctrl + Shift + M 打开串行监视器。

  • Serial.available() 检查从串行端口读取的字节数据是否可用。本质上,要从串行端口读取的数据存储在串行接收缓冲区中,此方法检查数据是否已到达此缓冲区。此方法返回可读取的字节数:

  • Serial.read() 读取传入的串行数据并返回可用的第一个字节。如果没有可读数据,该方法返回 -1

  • analogRead() 从指定的模拟引脚读取值。Arduino 板包含一个多通道、10 位模数转换器。因此,它将输入电压映射到 0 和工作电压(5V 或 3.3V)之间的整数值,介于 0 和 1023 之间。

例如,如果您使用的是 5V Arduino,并且传感器连接到其模拟引脚,那么以下公式用于将 10 位模拟读数转换为温度:

Voltage at pin in milli volts = Reading from ADC * 5000/1024

此公式将 ADC 的 0-1023 数字转换为 0-5000 mV。

如果您使用的是 3.3V Arduino,那么以下公式用于将模拟读数转换为温度:

Voltage at pin in milli volts = Reading from ADC * 3300/1024

此公式将 ADC 的 0-1023 数字转换为 0-3300 mV。

为了将前一个公式中检索到的毫伏数转换为温度,使用以下公式:

Centigrade temperature = Analog voltage in mV / 10

这就结束了我们对嵌入式系统和物联网的介绍。现在,我们将回顾完成本章菜谱所需的软件和硬件。之后,我们将开始第一个菜谱。

在嵌入式 C 中切换微控制器的端口(闪烁 LED)

在这个菜谱中,我们将学习如何向连接到 LED 的特定端口发送高电平和低电平信号,并使 LED 闪烁。这个练习背后的想法是学习如何控制连接到微控制器特定端口的设备。

如何做到这一点...

要在嵌入式 C 中切换微控制器的端口,请执行以下步骤:

  1. 我们将使用 Keil 执行这个菜谱;双击 Keil uVision5 图标以激活 IDE。

  2. 通过点击“项目 | 新 uVision 项目”选项创建一个新项目。

  3. 当提示时,指定项目名称以及你想要创建新项目的文件夹。

  4. 给新项目命名为LedBlinkProject,然后点击“保存”按钮。

  5. 将打开设备选择窗口,并提示你选择一个设备。

  6. 从设备组合框中选择“Legacy Device Database [no RTE]”选项。你将在左下角的窗格中看到设备列表(参看以下截图)。

  7. 点击 Microchip 节点以展开它并显示其中的设备列表。

  8. 因为我们想要编程 Atmel 微控制器,从 Microchip 节点中选择 AT89C51 设备。所选设备的描述将出现在右侧的描述窗格中。点击“确定”以继续:

  1. 你将被询问是否要将STARTUP.A51文件复制到项目文件夹中(参看以下截图)。启动文件将用于运行项目,因此点击“是”按钮添加文件并继续:

  1. IDE 将显示如下。您可以在 IDE 中看到三个窗口:项目工作区、编辑窗口和输出窗口。此外,您还可以看到在项目空间下创建的 Target1 节点:

  1. 添加一个 C 文件。在 Target1 节点下的 Source Group1 上右键单击,然后点击“向组‘Source Group 1’添加新项”选项。从列表框中选择“C 文件 (.c)”选项。指定文件名为blinkingLed(或任何其他名称),然后点击“添加”按钮(参看以下截图):

  1. blinkingLed.c文件已添加到 Source Group 1。在编辑窗口中输入以下代码:
#include<reg52.h>   

sbit LED = P1⁰;          
void Delay(int);
void main (void)
{
    while(1)               
    {
        LED = 0;           
        Delay(500);
        LED = 1;           
        Delay(500);
    }
}

void Delay(int n)
{
    int i,j;
    for(i=0;i<n;i++)
    {
        for(j=0;j<100;j++);
    }
}
  1. 在输入代码后,点击工具栏中的保存图标以保存blinkingLed.c文件。

现在,让我们幕后了解代码,以便更好地理解。

它是如何工作的...

我们将定义一个名为LEDsbit类型变量。sbit类型定义了一个特殊功能寄存器SFR)内的位。我们将设置LED变量来表示端口P1的 0 位。然后,我们将定义Delay函数的原型,该函数接受一个整型参数但不返回任何内容。在main函数中,我们将执行一个无限循环中的while循环。在while循环中,我们将LED变量设置为0,即向端口P1的 0 位发送低信号。

此后,我们将通过两个嵌套循环引入延迟。延迟后,我们将LED变量的值设置为1,即向端口P1的 0 位发送高信号。如果 LED 连接到端口1的 0 位,LED 将发光,经过一些延迟后熄灭。再次经过一些延迟后,LED 将再次发光;因此,我们得到了一个闪烁的 LED。

F7键或点击构建按钮以开始编译代码。如果没有错误,您可以继续下一步。您可以选择生成 HEX 文件以注入到所需的硬件中,或者可以使用模拟技术来查看程序是否给出了预期的输出。为了生成 HEX 文件,右键单击“Target1”节点并选择“为‘Target 1’选项”。我们将得到一个显示不同选项的对话框。点击输出选项卡,并勾选创建 HEX 文件框(参见图表)。此外,点击设备选项卡以确认所选设备是 AT89C51。然后,点击确定按钮:

图 5.5

Keil 的内置调试选项可用于代码模拟。为此,请点击调试|开始/停止调试会话;或者,您可以按Ctrl + F5作为快捷键,或者点击工具栏中的开始/停止调试会话图标(它以 d 的形式出现)。Keil 工具的免费版本有一个条件,即运行代码的大小不应超过 2 KB。您将看到一个对话框,指示运行代码的上限为 2 KB:

图 5.6

点击确定按钮继续。现在,项目工作区窗口显示了大多数 SFR 以及 GPRs,即 r0 到 r7,如下所示:

图 5.7

从工具栏中点击运行图标或按F5键。要查看端口的输出,请转到“外围设备”|“选择 I/O 端口”|“端口 1”。您将在“端口 1”上看到闪烁的 LED,如下面的截图所示。您可以看到,在“端口 1”的bit0中有一个低信号,在同一个位上有一个高信号:

图 5.8

Voilà!我们已经成功使用微控制器端口创建了一个闪烁的 LED。现在,让我们继续下一个菜谱!

在嵌入式 C 中递增端口的值

在本教程中,我们将学习如何在微控制器的特定端口上显示从 0 到 255 的值,并使值像计数器一样递增。

如何操作...

在嵌入式 C 中递增端口的值,请执行以下步骤:

  1. 启动 Keil uVision5 IDE。

  2. 通过点击“新建项目 | 新建 uVision 项目”选项来创建一个新项目。

  3. 当提示时,指定项目名称和文件夹位置。让我们将新项目命名为CounterApp;点击“保存”。

  4. 将打开设备选择窗口,并提示你选择一个设备。从设备组合框中选择“Legacy Device Database [no RTE]”。

  5. 你将在左下角的窗格中看到设备列表。点击 Microchip 节点以展开它并显示其中的设备列表。

  6. 从 Microchip 节点中选择 AT89C51 设备。所选设备的描述将出现在右侧的描述面板中。点击“确定”以继续操作。

  7. 系统会询问你是否要将STARTUP.A51文件复制到项目文件夹中。点击“是”以添加文件并继续操作。

  8. IDE 将打开并显示三个窗口:左侧的“项目工作区”,右侧的“编辑窗口”,以及底部的“输出窗口”。你将在项目空间下看到创建的 Target1。

  9. 通过在 Target1 节点下的源组 1 上右键单击并点击“添加新项到组‘源组 1’”来添加一个 C 文件。

  10. 从列表框中选择“C 文件 (.c)”选项。指定文件名为showcounter,然后点击“添加”。

  11. showcounter.c文件将被添加到源组 1。在编辑窗口中输入以下代码:

#include<stdio.h>
#include<reg52.h>
void delay(void);        
void main()
{
    unsigned char i; 
    i=0x00;                   
    while(++i)
    {
         P3=i;                                  
         delay();            
    }             
}

void delay(void)
{
    int j;
    int i;
    for(i=0;i<1000;i++)
    {
        for(j=0;j<10000;j++)
        {
        }
    }
}
  1. 输入代码后,点击工具栏中的保存图标以保存showcounter.c文件。

现在,让我们深入了解这些步骤。

它是如何工作的...

我们将定义一个无符号字符类型的变量i;无符号字符类型的变量可以存储 256 位,而有符号字符类型的变量只能存储 128 位。i变量被分配了十六进制值0。我们将设置一个无限循环的while循环。在while循环的每次迭代中,i变量的值将递增1。在while循环内部,我们将i变量的值,即0,分配给端口P3

我们将通过使用两个嵌套的for循环来引入延迟。然后,我们再次执行while循环,递增i变量的值,使其变为1。再次,我们将i变量的值分配给端口P3以进行显示。然后,我们再次引入一些延迟,并再次执行while循环。这个过程将无限循环;因此,端口P3将重复显示从0255的计数器。

F7或点击构建按钮来编译代码。如果没有错误,您可以继续到下一步;否则,首先调试代码。为了通过仿真查看代码的输出,我们将使用 Keil 的内置仿真调试选项。为此,点击调试 | 开始/停止调试会话。或者,您可以按Ctrl + F5作为快捷键,或者点击工具栏中的开始/停止调试会话图标(它以 D 的形式出现)。

在 Keil 的免费版本中,有一个条件是运行代码的大小不应超过 2 KB,因此您将得到一个对话框,指示运行代码的上限为 2 KB。点击确定继续。项目工作区窗口显示了大多数 SFR 以及从 r0 到 r7 的 GPR。从工具栏中点击运行图标或按F5键。您可以在外围设备 | 选择 I/O 端口 | 端口 3 下看到端口的输出。您将看到端口的位显示从0255的计数器。

在以下屏幕截图,(a)显示了设置为5值的位。5 的二进制值是 101,因此,相应地,第一和第三位被设置为高电平信号,其余位被设置为低电平信号。同样,(b)显示了显示计数器值为 10 的位。端口的位将被设置为从 0 到 255 的值:

图 5.9

哇!我们已经成功使用嵌入式 C 创建了一个计数器。现在,让我们继续下一个菜谱!

使用 Arduino 在输出引脚上切换电压(闪烁 LED)

在这个菜谱中,我们将学习如何制作一个连接到 Arduino 板输出引脚的 LED 闪烁。

如何操作...

要使连接到 Arduino 板输出引脚的 LED 闪烁,请执行以下步骤:

  1. 打开 Arduino IDE。Arduino 会打开一个显示默认内容的文件,如下所示:
void setup() {
// put your setup code here, to run once:
}

void loop() {
// put your main code here, to run repeatedly:

}
  1. 将 Arduino 板连接到电脑。

  2. 从工具菜单中选择端口,确认是否显示 COM3(Arduino/Genuino Uno)或您连接到电脑的 Arduino 板。此外,确认工具菜单中的板选项是否指示连接到电脑的 Arduino 板。在我的情况下,板选项将显示 Arduino/Genuino Uno。

  3. 记住,LED 有极性;因此,只有当它们正确连接时才会发光。长腿是正极,应该连接到 Arduino 板上的数字引脚。我使用 Arduino 板的第 13 个引脚作为输出,所以将 LED 的长腿连接到 Arduino 板的第 13 个引脚(参见图 5.10)。然后,将 LED 的短腿(负极)连接到 Arduino 板的 GND。

  4. 在编辑器窗口中输入以下程序:

int Led = 13;

void setup() {
pinMode(Led, OUTPUT);
}

void loop() {
digitalWrite(Led, HIGH);
                 delay(1000);
                digitalWrite(Led, LOW);
                delay(1000);
}
  1. 通过点击文件 | 另存为保存应用程序。在提示时指定应用程序名称。选择所需的文件夹位置并指定应用程序名称。让我们将应用程序命名为ArduinoLedBlink。将创建一个具有指定应用程序名称(ArduinoLedBlink)的文件夹,并在ArduinoLedBlink文件夹内创建一个名为ArduinoLedBlink.ino的应用程序。

  2. 通过点击工具栏中的上传图标将应用程序上传到 Arduino。

现在,让我们深入了解这些步骤。

它是如何工作的...

我们将首先定义一个Led变量并将其设置为表示 Arduino 的 13 号引脚。通过调用pinMode函数,Led变量将被指示为输出引脚,即它将连接到输出设备以进行显示或执行某些任务。

loop函数中,我们将调用digitalWrite方法向变量Led发送一个高电平信号。这样做,连接到输出引脚 13 的 LED 将打开。之后,我们将引入 1,000 毫秒的时间延迟。

再次,我们将调用digitalWrite方法并发送一个低电平信号,这次发送到变量Led。结果,连接到输出引脚 13 的 LED 将关闭。同样,我们将引入 1,000 毫秒的延迟。loop函数中的命令将无限执行,使连接的 LED 保持闪烁。

将程序上传到 Arduino 后,连接到 Arduino 13 号引脚的 LED 将开始闪烁,如下所示:

图 5.10

哇!我们已经成功地在 Arduino 板上的一个输出引脚上切换电压,使 LED 闪烁。

现在,让我们继续下一个菜谱!

使用 Arduino 从串行端口获取输入

在这个菜谱中,我们将把一个 LED 连接到 Arduino 板,并提示用户按下01。用户可以通过串行端口输入一个值。如果用户输入的值是0,它将关闭 LED;如果输入的值是1,它将使 LED 发光。

如何做到这一点…

要使用 Arduino 从串行端口获取输入,请执行以下步骤:

  1. 调用 Arduino IDE。Arduino 将以显示其默认内容的文件打开,如下所示:
void setup() {
// put your setup code here, to run once:

}

void loop() {
// put your main code here, to run repeatedly:

}
  1. 将 Arduino 板连接到您的电脑。

  2. 从工具菜单中选择端口,并确认它是否显示 COM3(Arduino/Genuino Uno)或您连接到电脑的 Arduino 板。此外,确认工具菜单中的板选项是否指示连接到电脑的 Arduino 板。在我的情况下,板选项将显示 Arduino/Genuino Uno。

  3. 我们将把 LED 连接到 Arduino 的输出引脚 13。由于 LED 有极性并且需要正确连接,我们将把 LED 的长腿(也称为正极引脚)连接到 Arduino 板上的第 13 个数字引脚。此外,我们将把 LED 的短腿(也称为负极引脚)连接到 Arduino 板上的 GND。

  4. 在编辑器窗口中输入以下程序:

int Led = 13;
void setup() {
                pinMode(Led,OUTPUT);
                Serial.begin(9600);
                Serial.println("Enter 0 to switch Off LED and 1 to 
                  switch it On");
}

void loop() {
if(Serial.available())
                {
                                int input=Serial.read();
                                input=input-48;
                                if(input==0)
                                {
                                                Serial.println("LED 
                                                  is OFF");
                                                digitalWrite(Led,LOW);
                                 }
                                else if(input==1)
                                 {
                                                Serial.println("LED 
                                                  is ON");
                                                digitalWrite(Led,HIGH);
                                }
                                else
                                {

Serial.println("Enter 0 to switch Off
 LED and 1 to switch it On");
                                 }
                }
}
  1. 通过点击文件 | 另存为选项保存应用程序。在提示时指定应用程序名称。让我们将应用程序命名为ArduinoTakinginput。将创建一个具有指定应用程序名称的文件夹,并在ArduinoLedBlink文件夹中创建一个名为ArduinoTakinginput.ino的应用程序文件。

  2. 通过点击工具栏中的上传图标将应用程序上传到 Arduino(参考以下截图):

图片

现在,让我们深入了解背后的步骤。

它是如何工作的...

我们将定义一个Led变量并将其设置为表示 Arduino 的 13 号引脚。通过调用pinMode函数,Led变量被声明为输出引脚,因此我们将使用它来连接到输出设备以执行所需操作。在本应用中,输出设备将是一个 LED。

由于我们希望我们的 PC 通过串行通信与 Arduino 通信,我们需要以每秒比特数来设置数据速率。因此,我们将调用Serial.begin函数将串行数据传输速度设置为 9,600 比特每秒(然而,可以是任何波特率)。之后,我们将在串行端口上以人类可读的格式显示一条消息,通知用户按下0可以关闭连接的 LED,按下1可以打开 LED。

在 Arduino 的loop函数中,我们将调用Serial.available函数来检查串行端口是否有可读数据。也就是说,将检查串行接收缓冲区以查看是否有数据或字节可供读取。只有当用户按下任何键时,数据才会出现在串行接收缓冲区中。这也意味着在用户按下任何键之前不会出现任何输出。当用户按下任何键时,该字节将进入串行接收缓冲区,Serial.available函数将返回布尔值true。因此,if块将执行。

if块中,我们将调用Serial.read函数从串行端口读取串行数据。从串行端口读取的数据或字节将被分配给变量input。读取的字节始终是 ASCII 格式。用户应按下01;它们的 ASCII 值分别是 48 和 49。因此,如果用户按下0,其 ASCII 值 48 将被分配给变量input。如果用户按下1,其 ASCII 值 49 将被分配给变量input

为了得到用户输入的实际数值,我们从变量输入中减去 48 的值。如果用户按下 0,将执行一个指定的 if 块。在该 if 块内,我们将调用 Serial.println 函数来显示消息 LED is OFF 以通知用户。我们还将调用 digitalWrite 方法向连接 LED 的输出引脚 13 发送低电平信号。因此,如果 LED 正在发光,它将被关闭。

如果用户按下 1,则将执行另一个 if 块;在这种情况下,我们将调用 Serial.println 函数来显示消息 LED 是开启的。我们还将调用 digitalWrite 函数向输出引脚 13 发送高电平信号,使 LED 发光。如果用户没有按下 01,我们将显示一条消息,要求他们只按下 01

将程序上传到 Arduino 后,我们可以按 Ctrl + Shift + M 打开串行监视器。在串行监视器中,我们将得到以下消息:输入 0 以关闭 LED 并输入 1 以开启它(参见图 5.11 中的第一个对话框)。在串行监视器中按下 0 后,我们将得到消息 LED is OFF,并且,你将再次被提示输入 01(参见图 5.11 中的第二个对话框)。除了串行监视器中的消息外,连接到 Arduino 板 13^(th) 引脚的 LED 也会关闭(如果它之前是发光的)。按下 1 时,串行监视器中将显示消息 LED is ON。此外,将出现一条消息提示我们输入 01(参见图 5.11 中的第三个对话框)。此外,连接到 Arduino 板的 LED 将发光:

图 5.11

Voilà!我们已经成功使用 Arduino 通过串行端口输入使 LED 开关。

现在,让我们继续下一个菜谱!

使用 LM35 传感器通过 Arduino 感知温度

在这个菜谱中,我们将学习如何使用连接到 Arduino 板的 LM35 传感器来感知温度,并将温度以摄氏度和华氏度显示。

准备就绪…

对于这个菜谱,我们需要以下三个组件:一个面包板、Arduino Uno R3 和一个 LM35 传感器。

LM35 是一个温度传感器,其输出电压与摄氏温度成线性比例。它不需要任何外部校准或调整即可提供准确的温度。它有三个端子,VsVoutGround,如下所示:

图 5.12

我们将按照以下方式将 LM35 传感器连接到 Arduino 板:

  1. 将 +Vs 端口连接到 Arduino 板上的 +5v。

  2. 将 Vout 端口连接到 Arduino 板上的 Analog0 或 A0。

  3. 将 GND 端口与 Arduino 上的 GND 端口连接。

以下图表使这一点更清晰:

图 5.13

现在 LM35 传感器已连接到 Arduino 板,让我们执行以下步骤。

如何做到这一点...

要使用连接到 Arduino 板的 LM35 传感器检测温度,请执行以下步骤:

  1. 调用 Arduino IDE。Arduino 将以显示其默认内容的文件打开,如下所示:
void setup() {
// put your setup code here, to run once:
}

void loop() {
// put your main code here, to run repeatedly:

}
  1. 将 Arduino 板连接到 PC。

  2. 在工具菜单中,选择端口并确认是否显示 COM3(Arduino/Genuino Uno)或您连接到 PC 的任何 Arduino 板。此外,确认工具菜单中的板选项是否指示连接到您的 PC 的 Arduino 板。在我的情况下,板选项将显示 Arduino/Genuino Uno。

  3. 将以下程序输入到编辑器窗口中:

float voltage;
int tempPin = 0;

void setup() {
   Serial.begin(9600);
}

void loop() {
   voltage = analogRead(tempPin);
   float tempInCelsius = voltage * 0.48828125;
   float tempinFahrenheit = (tempInCelsius*9)/5 + 32;
   Serial.print("Temperature in Celsius is: ");
   Serial.print(tempInCelsius);
   Serial.print("*C");
   Serial.println();
   Serial.print("Temperature in Fahrenheit is: ");
   Serial.print(tempinFahrenheit);
   Serial.print("*F");
   Serial.println();
   delay(1000);
}
  1. 通过点击 File | Save As 选项保存应用程序。在提示时指定应用程序名称。让我们将其命名为SensorApp。将创建一个名为SensorApp的文件夹,在该文件夹中,将创建一个名为SensorApp.ino的应用程序文件。

  2. 通过点击工具栏中的上传图标将应用程序上传到 Arduino。

它是如何工作的...

我们定义一个名为voltage的浮点变量和一个名为tempPin的整型变量;我们将后者设置为表示 Arduino 的 0 号引脚。为了使我们的 PC 通过串行通信与 Arduino 通信,我们需要以每秒比特数来设置数据速率。因此,我们将调用Serial.begin函数将串行数据传输速度设置为 9,600 比特每秒(然而,可以是任何波特率)。

loop函数中,我们将调用analogRead函数从指定的模拟引脚读取值,0。回想一下,Arduino 板包含一个多通道、10 位模拟到数字转换器,它将 0 到工作电压(5V 或 3.3V)之间的输入电压映射到 0 到 1023 之间的整数值。从模拟引脚 0 读取的值被分配给voltage变量。

我们使用 5V Arduino 板,并且 LM35 传感器已经连接到其模拟引脚。我们将使用以下公式将 10 位模拟读数转换为温度:

Voltage at pin in milliVolts = Reading from ADC * 5000/1024

此公式将 ADC 中的数字 0-1023 转换为 0-5000 mV。要将此公式检索到的毫伏数转换为温度,我们将使用另一个公式:

Centigrade temperature = Analog voltage in mV / 10

可以将前面提到的两个公式重写如下:

Centigrade temperature = Reading from ADC * 0.48828125;

使用此公式,将读取到电压变量的值转换为摄氏度温度,并分配给tempInCelsius变量。要将摄氏度(°C)温度转换为华氏度(°F),使用以下公式:

F=(C*9)/5+32

使用此公式,将tempInCelsius变量中找到的摄氏度温度转换为华氏度,并分配给tempinFahrenheit变量。

通过调用,将摄氏度(°C)和华氏度(°F)的温度显示到串行端口。可以通过打开串行监视器来查看温度读数。按Ctrl + Shift + M打开串行监视器并显示温度。您还可以按下您的拇指上的 LM35 传感器来观察温度的升降。

我们将通过调用delay函数在每次温度显示之间引入 1,000 毫秒的延迟。也就是说,应用程序将以 1,000 毫秒的延迟无限期地显示摄氏度和华氏度的温度。

在将程序上传到 Arduino 后,我们可以按Ctrl + Shift + M打开串行监视器。在串行监视器中,我们将得到摄氏度和华氏度的温度读数。您将每隔 1,000 毫秒连续获得温度读数:

图片

图 5.14

在下面的照片中,您可以看到 LM35 传感器连接到 Arduino 板上。您可以按下您的拇指上的 LM35 传感器来观察温度读数的上升:

图片

图 5.15

哇!我们已成功使用 Arduino 和 LM35 传感器创建了一个温度传感器。

第十八章:在编码中应用安全性

在编码过程中,有时你可能使用不检查或约束用户输入数据的函数。用户可能输入错误的数据或内容,可能比接收变量的容量大。在这种情况下,可能会发生缓冲区溢出或段错误。因此,程序将给出错误输出。

在本章中,我们将使用以下方法来查看我们如何使程序中的数据输入无错误:

  • 避免从键盘读取字符串时的缓冲区溢出

  • 在复制字符串时编写安全代码

  • 避免字符串格式化时的错误

  • 避免在 C 中访问文件时的漏洞

缓冲区溢出

在 C 编程中最常见的漏洞是缓冲区溢出。缓冲区,正如其名所示,代表程序在 RAM 中使用的临时内存存储区域。通常,程序中使用的所有变量都被分配临时缓冲区存储,以保持分配给它们的值。一些函数在将较大的值(大于分配的缓冲区)赋给变量时,不限制缓冲区内的数据,导致缓冲区溢出。溢出的数据会破坏或覆盖其他缓冲区中的数据。

这些缓冲区溢出可能被黑客或恶意用户用来损坏文件或数据,或提取敏感信息。也就是说,攻击者可能会输入导致缓冲区溢出的输入。

在为数组赋值时,没有边界检查,代码可能工作,无论访问的内存是否属于你的程序。在大多数情况下,这会导致段错误,覆盖另一个内存区域中的数据。

在这个程序中,我们将反复使用一些术语和函数。让我们快速概述一下它们。

gets()

gets() 函数从标准输入设备读取字符,并将它们分配到指定的字符串中。读取字符在遇到换行符时停止。此函数不检查缓冲区长度,总是导致漏洞。以下是它的语法:

char *gets ( char *str);

在这里,str 代表指向字符串(字符数组)的指针,读入的字符将被分配到该字符串中。

在成功执行的情况下,该函数返回 str,如果发生任何错误则返回 NULL

fgets()

fgets() 函数用于从指定的源读取字符串,其中源可以是任何文件、键盘或其他输入设备。以下是它的语法:

char *fgets(char *str, int numb, FILE *src);

从指定的源 src 读取 numb 个字节,并将它们分配到由 str 指向的字符串中。该函数要么读取 numb-1 个字节,要么直到遇到换行符 (\n) 或文件结束,以先到者为准。

该函数还会将空字符 (\0) 添加到读取的字符串中,以终止字符串。如果执行成功,函数返回指向 str 的指针;如果发生错误,则返回 NULL

fpurge(stdin)

fpurge(stdin) 函数用于清除或清空流的输入缓冲区。有时,在为一些变量提供数据后,数据(可能是空格或换行符的形式)会留在缓冲区中,没有被清除。在这种情况下,使用此函数。如果执行成功,函数返回零,否则返回 EOF。

这里是它的语法:

fpurge(stdin)

sprintf()

sprintf() 函数用于将格式化文本分配到字符串中。以下是它的语法:

int sprintf(char *str, const char *format, ...)

在这里,str 是一个指向字符串的指针,格式化字符串需要被分配到这个字符串中,而 formatprintf 语句中的用法相同,可以使用不同的格式化标签,如 %d%s 来格式化内容。

snprintf()

snprintf() 函数格式化给定内容并将其分配到指定的字符串。只有指定数量的字节将被分配到目标字符串。以下是它的语法:

int snprintf(char *str, size_t numb, const char *format, ...);

下面是对前面代码的分解:

  • *str:表示指向字符串的指针,格式化内容将被分配到该字符串。

  • numb:表示可以分配到字符串的最大字节数。

  • format:与 printf 语句类似,可以使用多个格式化标签,如 %d%s 来格式化内容。

注意:snprintf 自动将空字符追加到格式化字符串中。

strcpy()

strcpy() 函数用于从一个字符串复制内容到另一个字符串。以下是它的语法:

char* strcpy(char* dest, const char* src);

在这里,src 代表源字符串的指针,内容需要从该字符串复制,而 dest 代表目标字符串的指针,内容需要复制到该字符串。

strncpy( )

strncpy() 函数用于从字符串复制指定数量的字节到另一个字符串。以下是它的语法:

char * strncpy ( char * dest, const char *src, size_t numb);

下面是对前面代码的分解:

  • dest:表示复制到目标字符串的指针

  • src:表示从源字符串复制字节的指针

  • numb:表示从源字符串复制到目标字符串的字节数

如果 numb 参数的值大于源字符串的长度,目标字符串将以空字节填充。如果目标字符串的长度小于 numb 参数,则字符串将被截断以等于目标字符串的长度。

让我们现在开始我们的安全编码之旅,从第一个菜谱开始。

理解缓冲区溢出是如何发生的

在这个菜谱中,我们将学习如何从用户那里获取输入,并看到导致缓冲区溢出并产生模糊输出的情况。我们还将学习避免缓冲区溢出的方法。

基本上,我们将创建一个包含两个成员的结构体,并在其中一个成员中故意输入超过其容量的文本,从而导致缓冲区溢出。这将导致结构体另一个成员的内容被覆盖。

如何做到这一点...

这里是创建会导致缓冲区溢出的程序的步骤:

  1. 定义一个包含nameorderid两个成员的结构体。

  2. 定义两个类型为步骤 1中定义的结构体的变量。在其中一个结构体变量中,我们将故意输入大量数据以生成缓冲区溢出。

  3. 提示用户为第一个结构体输入orderid成员的值。

  4. 在调用gets函数之前,调用fpurge函数以清空输入流缓冲区。

  5. 调用gets函数为第一个结构体的name成员输入数据。输入比name成员长度更大的文本。

  6. 重复步骤 35以输入第二个结构体的orderidname成员的数据。这次,请输入在name成员容量内的数据。

  7. 显示分配给第一个结构体的orderidname成员的数据。在第一个结构体中将会发生缓冲区溢出,您在显示orderid值时将得到一个模糊的输出。

  8. 显示分配给第二个结构体的orderidname成员的数据。在这个结构体中没有发生缓冲区溢出,您将得到与两个成员输入完全相同的数据。

以下程序将获取两个结构体的名称和订单号值。在结构体的一个成员中,我们将输入超过其容量的数据以生成缓冲区溢出:

//getsproblem.c

#include <stdio.h>

struct users {
  char name[10];
  int orderid;
};
int main(void) {
  struct users user1, user2;
  printf("Enter order number ");
  scanf("%d", & user1.orderid);
  fpurge(stdin);
  printf("Enter first user name ");
  gets(user1.name);
  printf("Enter order number ");
  scanf("%d", & user2.orderid);
  fpurge(stdin);
  printf("Enter second user name ");
  gets(user2.name);
  printf("Information of first user - Name %s, Order number %d\n", 
   user1.name, user1.orderid);
  printf("Information of second user - Name %s, Order number %d\n", 
   user2.name, user2.orderid);
}

现在,让我们深入了解代码,以更好地理解其工作原理。

它是如何工作的...

程序将要求输入两对名称和订单号。在第一对中,我们将通过输入比变量大小更长的文本来故意生成缓冲区溢出,而对于第二对,我们将输入指定范围内的数据。因此,第一个用户(对)的信息将显示不正确,即数据不会与输入的完全相同,而第二个用户的信息将正确显示。

因此,我们将定义一个名为users的结构体,其中包含两个字段或成员,分别称为nameorderid,其中name被定义为大小为 10 字节的字符串,orderid被定义为 2 字节的 int 变量。然后,我们将定义两个users结构体类型的变量user1user2;这意味着user1user2变量都将各自获得一个nameorderid成员。

您将被提示两次输入用户名和订单号。输入的第一组名称和订单号将被分配给user1,第二组分配给user2。然后,两个用户的输入信息将在屏幕上显示。

让我们使用 GCC 编译getsproblem.c程序。如果你没有错误或警告,这意味着getsproblem.c程序已编译成可执行文件:getsproblem.exe。让我们运行这个文件:

图 18.1

我们可以在前面的输出中看到,由于第一个结构体中name成员造成的缓冲区溢出,orderid成员的值101被覆盖。因此,我们得到第一个结构体的orderid的垃圾值。第二个结构体的输出是正确的,因为为其成员输入的值在其容量范围内。

为了避免在输入数据时发生溢出,我们只需将gets函数替换为fgets函数。使用fgets函数,我们可以指定允许在指定字符串中的最大字符数。超出部分的文本将被截断,并且不会分配给指定的字符串。

学习如何避免缓冲区溢出

在前面的步骤中,我们定义了两个结构体变量,因为我们想展示,如果输入的数据大小大于接收变量字段允许的大小,将导致模糊的输出;如果输入的数据在接收变量的容量范围内,将生成正确的输出。

在下面的步骤中,我们不需要两个结构体变量,因为我们将会使用fgets函数来解决问题。这个函数永远不会导致缓冲区溢出。

如何做到这一点...

下面是使用fgets函数避免缓冲区溢出的步骤:

  1. 定义一个包含两个成员的结构体,nameorderid

  2. 定义一个类型为步骤 1中定义的结构体的变量。

  3. 提示用户为结构体的orderid成员输入一个值。

  4. 在调用fgets函数之前,调用fpurge函数清空输入流缓冲区。

  5. 调用fgets函数为结构体的name成员输入数据。为了限制分配给name成员的文本的大小,通过调用sizeof函数计算其长度,并将该字符串长度提供给fgets函数。

  6. 如果字符串中还没有空字符,则添加一个空字符以终止字符串。

  7. 显示分配给结构体成员orderidname的数据,以验证没有缓冲区溢出。

以下程序定义了一个包含两个成员的结构体,并解释了如何在通过键盘输入数据时避免缓冲区溢出:

//getssolved.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct users {
  char name[10];
  int orderid;
};

int main(void) {
  struct users user1;
  int n;
  printf("Enter order number ");
  scanf("%d", & user1.orderid);
  fpurge(stdin);
  printf("Enter user name ");
  fgets(user1.name, sizeof(user1.name), stdin);
  n = strlen(user1.name) - 1;
  if (user1.name[n] == '\n')
    user1.name[n] = '\0';
  printf("Information of the user is - Name %s, Order number %d\n", 
   user1.name, user1.orderid);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

在程序中,fgets 函数从标准输入设备获取输入,从输入设备读取的最大字符数将等于 user1 结构体中 name 变量允许的字节数。因此,即使用户输入了较长的字符串,也只会从输入中选取指定的字节数;也就是说,只会选取输入中的前 10 个字符并分配给 user1 结构体的 name 成员。

fgets 函数会将空字符 (\0) 追加到字符串中,前提是输入的字符数比函数中指定的最大长度少一个。但对于比指定长度大的字符串,我们需要在字符串的末尾插入空字符。为此,我们需要检查换行符是否作为字符串的最后一个字符存在。如果是,那么我们将字符串中的换行符替换为空字符以终止字符串。

让我们使用 GCC 编译 getssolved.c 程序。如果你没有错误或警告,这意味着 getssolved.c 程序已编译成可执行文件:getssolved.exe。让我们运行这个文件:

图片

图 18.2

我们可以在前面的输出中看到,分配给结构体成员 name 的较长的文本被截断,按照成员的大小进行截断,因此不会发生缓冲区溢出。

理解在复制字符串时如何发生漏洞

在这个菜谱中,我们将看到在复制字符串时可能发生的漏洞。我们还将看到如何避免这个漏洞。我们首先定义一个由两个成员组成的结构体。在一个成员中,我们将复制一个比其容量大的文本,这将导致覆盖另一个成员的内容。

在下一个菜谱中,我们将学习如何避免这个问题。

如何做...

这里是理解在复制字符串时如何发生漏洞的步骤:

  1. 定义一个包含两个成员的结构体,nameorderid

  2. 定义一个由步骤 1 中定义的结构体类型定义的变量。

  3. 将任何整数值分配给结构体的 orderid 成员。

  4. 调用 strcpy 函数将文本分配给结构体的 name 成员。为了生成缓冲区溢出,向其分配较长的文本。

  5. 显示分配给结构体成员 orderidname 的数据,以确认是否生成了模糊的输出,这验证了发生了缓冲区溢出。

显示在复制字符串时出现漏洞的程序如下:

//strcpyproblem.c

#include <stdio.h>
#include <string.h>

struct users {
  char name[10];
  int orderid;
};

int main(void) {
  struct users user1;
  char userid[] = "administrator";
  user1.orderid = 101;
  strcpy(user1.name, userid);
  printf("Information of the user - Name %s, Order number %d\n", 
   user1.name, user1.orderid);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

为了输入客户的名称和订单号,定义一个名为 users 的结构体,包含两个成员,nameorderidname 成员是一个长度为 10 个字节的字符数组或字符串,而 orderid 成员是一个由 2 个字节组成的 int 数据类型的变量。

定义一个名为user1的变量,其类型为users结构;因此,user1结构将获得两个成员,nameorderid。将整数值 101 分配给user1结构中orderid成员。同时,将字符串administrator分配给user1name成员。由于字符串administrator的大小超过了name成员的大小,将发生缓冲区溢出,覆盖下一个内存位置的内存,即orderid成员的内存。因此,在显示用户信息时,尽管name成员中的数据可能显示正确,但orderid成员的内容将显示错误,因为其内容已被覆盖。

让我们使用 GCC 编译strcpyproblem.c程序。如果你没有错误或警告,这意味着strcpyproblem.c程序已编译成可执行文件:strcpyproblem.exe。让我们运行这个文件:

图片

图 18.3

在前面的输出中,你可以看到,由于name成员被分配了一个比其大小大的字符串,这导致它覆盖了另一个成员orderid的内容。name成员的内容与用户输入的相同,而orderid的内容显示错误。

学习如何在复制字符串时编写安全的代码

为了避免使用strcpy函数时发生的缓冲区溢出,只需将strcpy函数替换为strncpy函数。strncpy将只复制指定数量的字节到目标字符串,因此在这个函数中不会发生缓冲区溢出。让我们看看它是如何完成的。

如何做到这一点...

在复制字符串时编写安全代码的步骤如下:

  1. 定义一个包含两个成员nameorderid的结构。

  2. 定义一个类型为步骤 1中定义的结构类型的变量。

  3. 将任何整数值分配给结构的orderid成员。

  4. 确定结构的name成员的长度,以找到它可以容纳的最大字符数。

  5. 调用strncpy函数将文本复制到结构的name成员。同时,也将name成员的长度传递给strncpy函数,以便在文本大于name成员容量时截断文本。

  6. 如果字符串中还没有空字符,请添加一个空字符以终止它。

  7. 显示分配给结构的orderidname成员的数据,以验证没有发生缓冲区溢出,并且显示的数据与输入的数据相同。

以下是一个足够安全的字符串复制程序:

//strcpysolved.c

#include <stdio.h>
#include <string.h>

struct users {
  char name[10];
  int orderid;
};

int main(void) {
  int strsize;
  struct users user1;
  char userid[] = "administrator";
  user1.orderid = 101;
  strsize = sizeof(user1.name);
  strncpy(user1.name, userid, strsize);
  if (user1.name[strsize - 1] != '\0')
    user1.name[strsize - 1] = '\0';
  printf("Information of the user - Name %s, Order number %d\n", 
   user1.name, user1.orderid);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

在这个菜谱中,一切与上一个菜谱相同。不同之处在于我们调用了strncpy函数。当调用此函数时,只有从administrator文本中分配的strsize个字节被分配给user1结构体的name成员。因为strsize包含name成员的最大长度,所以在这种情况下不会发生缓冲区溢出。

最后,我们检查空字符\0是否存在于name成员的最后一个字符。如果没有,则在末尾添加空字符以终止字符串。在显示用户信息时,我们可以看到,由于name成员的长度为 10,只有文本administrator的前 9 个字符被分配给name成员,后面跟着一个空字符。因此,orderid成员的值也将正确显示,与输入的完全相同。

让我们使用 GCC 编译strcpysolved.c程序。如果你没有错误或警告,这意味着strcpysolved.c程序已编译成可执行文件:strcpysolved.exe。让我们运行这个文件:

图片

图 18.4

你可以在前面的输出中看到,无论为两个成员输入什么值,我们都会得到完全相同的输出。

理解字符串格式化时出现的错误

在这个菜谱中,我们将了解在格式化字符串时可能会发生什么样的错误。我们还将看到如何避免这种错误。我们将定义一个包含两个成员的结构,并将格式化字符串分配给其中一个成员。让我们看看我们会遇到什么错误。

在下一个菜谱中,我们将看到如何避免这种情况。

如何做...

这里是制作一个由于字符串格式化而出现错误的程序的步骤:

  1. 定义一个包含两个成员nameorderid的结构。

  2. 定义一个在步骤 1中定义的结构类型的变量。

  3. 将任何整数值分配给结构体的orderid成员。

  4. 调用sprintf函数将格式化文本分配给结构的name成员。为了生成缓冲区溢出,请分配一个较长的文本。

  5. 显示分配给结构体成员orderidname的数据,以确认是否生成了模糊的输出,以验证是否发生了缓冲区溢出。

以下是一个由于应用字符串格式化而产生错误输出的程序:

//sprintfproblem.c

#include <stdio.h>

struct users {
  char name[10];
  int orderid;
};

int main(void) {
  struct users user1;
  user1.orderid = 101;
  sprintf(user1.name, "%s", "bintuharwani");
  printf("Information of the user - Name %s, Order number 
   %d\n", user1.name, user1.orderid);
}

现在,让我们深入了解代码,以更好地理解它。

它是如何工作的...

我们想输入关于客户名称和他们下订单的信息。因此,我们定义了一个名为users的结构体,包含两个成员,nameorderid,其中name成员定义为长度为 10 字节的字符数组,而orderid成员定义为 2 字节的 int 数据类型。定义了一个users结构体类型的变量user1,因此user1结构体会获得两个成员,nameorderid。将值101的整数赋值给user1结构体的orderid成员。

使用sprintf函数,将字符串bintuharwani赋值给user1结构体的name成员。由于bintuharwani字符串的大小超过了name成员,因此将发生缓冲区溢出,覆盖下一个内存位置的内存,即orderid成员的内存。因此,在显示用户信息时,名称将正确显示,但你将得到orderid成员的不同或模糊的值。

让我们使用 GCC 编译sprintfproblem.c程序。如果没有错误或警告,这意味着sprintfproblem.c程序已编译成可执行文件:sprintfproblem.exe。让我们运行这个文件:

图 18.5

在输出中,你可以看到订单号显示错误;也就是说,不是显示分配的值101,而是显示值0。这是因为将bintuharwani字符串赋值给name成员时,由于字符串的大小超过了name成员的容量,导致缓冲区溢出,覆盖了orderid成员的值。

学习如何在格式化字符串时避免错误

在这个菜谱中,我们将使用snprintf函数。snprintf函数将格式化文本赋值给name成员,但会限制分配给它的字符串的大小。sprintfsnprintf函数之间的区别在于,sprintf无论目标字符串的容量如何,都会简单地将完整的格式化文本赋值给目标字符串,而snprintf允许我们指定可以分配给目标字符串的文本的最大长度。因此,不会发生缓冲区溢出,因为只将指定的文本大小分配给目标字符串。

如何做到这一点...

下面是创建一个由于字符串格式化而出现错误的程序的步骤:

  1. 定义一个包含两个成员的结构体,nameorderid

  2. 定义一个由步骤 1 中定义的结构体类型变量。

  3. 将任何整数值赋给结构体的orderid成员。

  4. 调用snprintf函数将格式化文本赋值给结构体的name成员。同时将name成员的长度传递给snprintf函数,以便在文本大于name成员的容量时截断文本。

  5. 显示结构体orderidname成员分配的数据,以验证不会发生缓冲区溢出,并且显示的数据与输入的数据相同。

以下程序展示了如何避免与字符串格式化相关的错误:

//sprintfsolved.c

#include <stdio.h>

struct users {
  char name[10];
  int orderid;
};

int main(void) {
  struct users user1;
  user1.orderid = 101;
  snprintf(user1.name, sizeof(user1.name), "%s", "bintuharwani");
  printf("Information of the user - Name %s, Order number 
   %d\n", user1.name, user1.orderid);
}

现在,让我们深入了解幕后,更好地理解代码。

它是如何工作的...

为了限制分配给user1结构体name成员的内容大小,我们将使用snprintf函数。你可以看到,通过snprintf函数,只有文本bintuharwani的前 10 个字符被分配给name成员。因为name成员的长度是 10,它能够存储 10 个字符,因此不会发生缓冲区溢出,分配给orderid成员的值将保持完整和未受干扰。在显示orderidname成员的值时,它们的值都将正确显示。

让我们使用 GCC 编译sprintfsolved.c程序。如果你没有错误或警告,这意味着sprintfsolved.c程序已编译成可执行文件:sprintfsolved.exe。让我们运行这个文件:

图片

图 18.6

在前面的输出中,我们可以看到分配给name成员的额外格式化文本被截断,因此正确显示在屏幕上的nameorderid成员的输出。

理解在 C 语言访问文件时如何出现漏洞

假设你编写了一个程序来创建一个名为file1.txt的文本文件。在这样的程序中,恶意用户或黑客可能会在你要创建的文件中添加一些软链接到某些重要或敏感文件。结果,这会导致重要文件的覆盖。

如何做到这一点...

我们将首先假设一个名为file2.txt的重要文件已经存在于你的计算机上,并包含一些敏感信息。以下是恶意用户或黑客可以在你的程序中使用的步骤来创建一个覆盖file2.txt的文件:

  1. 定义一个文件指针。

  2. 黑客可能会创建一个软链接,并将一个敏感文件附加到我们想要创建的文件上。

  3. 打开我们想要写入内容的文件。但在现实中,附加到我们的文件上的敏感文件将以只写模式打开。

  4. 提示用户输入要写入文件的文本行。

  5. 将用户输入的行写入文件。

  6. 重复步骤 4步骤 5,直到用户输入stop

  7. 关闭由文件指针fp指向的文件。

以下是一个恶意用户可以用来将某些重要文件链接到你要创建的文件,从而覆盖和破坏系统上该重要文件的程序:

//fileproblem.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFSIZE 255

void main(int argc, char * argv[]) {
  FILE * fp;
  char str[BUFFSIZE];
  if (symlink("file2.txt", "file1.txt") != 0) {
    perror("symlink() error");
    unlink("file2.txt");
    exit(1);
  } else {
    fp = fopen("file1.txt", "w");
    if (fp == NULL) {
      perror("An error occurred in creating the file\n");
      exit(1);
    }
    printf("Enter content for the file\n");
    gets(str);
    while (strcmp(str, "stop") != 0) {
      fputs(str, fp);
      gets(str);
    }
  }
  fclose(fp);
}

现在,让我们深入了解幕后,更好地理解代码。

它是如何工作的...

文件指针由名称 fp 定义。在这个阶段,黑客或恶意用户可能会调用 symlink 函数创建一个名为 file1.txt 的软链接指向名为 file2.txt 的文件。在这个程序中,file2.txt 可以是密码文件或其他恶意用户可能想要覆盖或破坏的敏感文件。

因为程序是用来创建新文件的,程序调用 fopen 函数以写入模式打开 file1.txt,打开的文件将由文件指针 fp 指向。但由于 file1.txtfile2.txt 是链接的,实际上会打开 file2.txt,并以写入模式指向,由文件指针 fp 指向。如果无法以写入模式打开文件或发生其他错误,程序将终止。

用户被提示输入文件的文本行。用户输入的行被分配给 str 字符串。调用 fputs 函数将分配给 str 字符串的内容写入由文件指针 fp 指向的文件。因此,敏感文件将被覆盖。用户可以输入任意多的文本行,并在完成后输入 stop。因此,设置了一个 while 循环来执行,将不断从用户那里获取文本行并将它们写入文件,直到输入 stop。最后,关闭由文件指针 fp 指向的文件。

让我们使用 GCC 编译 fileproblem.c 程序,如下面的截图所示。如果你没有错误或警告,这意味着 fileproblem.c 程序已编译成可执行文件:fileproblem.exe。让我们运行这个文件:

图片

图 18.7

前面的文本不会进入期望的文件 file1.txt,而是会覆盖敏感文件 file2.txt,如果有的话,删除其早期内容。如果我们查看 file2.txt 文件的内容,我们将看到本应写入 file1.txt 的内容:

图片

图 18.8

现在,让我们重写程序以消除文件漏洞。

学习如何在编写 C 语言文件时避免漏洞

在这个菜谱中,我们将特别注意:我们将解除所有(如果有)到我们要创建的文件的链接。我们还将确保如果文件已存在,我们的程序不会覆盖任何文件。

如何做到这一点...

编写程序以避免在 C 语言中创建文件时的漏洞的步骤如下:

  1. 定义一个文件指针。

  2. 黑客可能会创建一个软链接并将敏感文件附加到我们想要创建的文件上。

  3. 从你想要写入的文件中删除链接。

  4. 使用检查文件是否存在的标志打开文件。如果文件存在,它应该被覆盖。

  5. 将文件描述符与文件流关联。

  6. 提示用户输入要写入文件的文本行。

  7. 将用户输入的行写入文件。

  8. 重复 步骤 5步骤 6,直到用户输入 stop

  9. 关闭由文件指针 fp 指向的文件。

以下是在创建文本文件时移除漏洞的程序:

//filesolved.c

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFSIZE 255

void main(int argc, char * argv[]) {
  int ifp;
  FILE * fp;
  char str[BUFFSIZE];
  if (symlink("file2.txt", "file1.txt") != 0) {
    perror("symlink() error");
    unlink("file2.txt");
    exit(1);
  } else {
    unlink("file1.txt");
    ifp = open("file1.txt", O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (ifp == -1) {
      perror("An error occurred in creating the file\n");
      exit(1);
    }
    fp = fdopen(ifp, "w");
    if (fp == NULL) {
      perror("Could not be linked to the stream\n");
      exit(1);
    }
    printf("Enter content for the file\n");
    gets(str);
    while (strcmp(str, "stop") != 0) {
      fputs(str, fp);
      gets(str);
    }
  }
  fclose(fp);
}

现在,让我们深入了解代码背后的情况。

它是如何工作的...

你可以在程序中看到定义了一个名为 fp 的文件指针。我们预计黑客或恶意用户可能已创建一个名为 file1.txt 的软链接到现有文件 file2.txtfile2.txt 是一个敏感文件,我们不希望它被覆盖或破坏。为了使程序无任何漏洞,调用 unlink() 函数来删除对 file1.txt 的任何链接。这将避免覆盖可能与 file1.txt 链接的任何敏感文件。

此外,使用的是 open 函数来打开文件,而不是传统的 fopen 函数。open 函数以只写模式打开 file1.txt 文件,并带有 O_CREATO_EXCL 标志,如果文件已存在,则 open 函数将失败。这将确保不会意外覆盖任何与 file1.txt 链接的现有敏感文件。open 函数将返回一个打开文件的文件描述符,该描述符将被分配给 ifp 变量。

要与文件一起工作,我们需要一个文件流。因此,调用 fdopen 函数将文件流与通过 open 函数生成的 ifp 文件描述符关联。fdopen 函数返回一个指向文件流的指针,该指针被分配给文件指针 fp。此外,在 fdopen 函数中使用 w 模式,因为尽管它以写入模式打开文件,但它永远不会导致文件截断。这使得程序更加安全,避免了意外删除任何文件。

此后,程序与之前的程序相同。它要求用户输入某些行,然后这些行被写入 file1.txt。最后,关闭由文件指针 fp 指向的文件。

让我们使用 GCC 编译 filesolved.c 程序,如下所示截图所示。如果你没有错误或警告,这意味着 filesolved.c 程序已编译成可执行文件:filesolved.exe。让我们运行这个文件:

图 18.9

我们可以验证在运行程序时输入的内容是否已进入 file1.txt。为此,我们将打开 file1.txt 来查看其内容如下:

图 18.10

我们可以看到用户输入的内容已进入 file1.txt

file2.txt 的内容如下所示:

图 18.11

posted @ 2025-10-02 09:35  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报