C-编程秘籍-全-

C 编程秘籍(全)

原文:zh.annas-archive.org/md5/cadef7b542b6ecf6dcaf657ca283d399

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在本书中,我们将探讨 C 语言的所有重要元素,如字符串、数组、一维和二维数组、函数、指针、文件处理、线程、进程间通信和数据库处理。通过烹饪书的方法,你将找到解决你在制作应用程序时通常会遇到的不同问题的解决方案。到本书结束时,你将拥有足够的知识来使用 C 语言的低级和高级功能,并能够将它们应用于制作实时应用程序。

这本书面向的对象

本书旨在帮助基础到中级程序员和开发者,他们希望在 C 语言中制作复杂和实时应用程序。本书对那些在制作数组、指针、函数、结构、文件、数据库和进程间通信时遇到难题的训练师、教师和软件开发者非常有用,他们希望通过查看运行示例找到解决问题的方法。

本书涵盖的内容

第一章,与数组一起工作,解释了如何使用数组执行一些复杂但必要的操作。你将学习如何在数组中插入一个元素,如何乘以两个矩阵,找到两个数组中的公共元素,以及找到两个集合或数组之间的差异。此外,你还将学习如何找到数组中的唯一元素,如何判断一个矩阵是否是稀疏矩阵,以及如何将两个排序数组合并成一个数组。

第二章,字符串管理,涵盖了在字符级别操作字符串。你将学习如何判断给定的字符串是否是回文,如何找到字符串中的第一个重复字符,以及如何计算字符串中的每个字符。你还将学习如何计算字符串中的元音和辅音数量,以及将句子中的元音转换为大写的步骤。

第三章,探索函数,涵盖了函数,这在将大型应用程序分解为小型、独立、可管理的模块中起着重要作用。在本章中,你将学习如何创建一个函数,该函数可以判断提供的参数是否是阿姆斯特朗数。你还将学习函数如何返回一个数组,以及我们如何创建一个使用递归找到两个数的最大公约数(GCD)的函数。你还将学习如何创建一个将二进制数转换为十六进制的函数,以及如何创建一个确定提供的数字是否是回文的函数。

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

第五章,文件处理,解释了文件处理对于存储数据以备后用的重要性。在本章中,你将学习如何读取文本文件并将所有句号之后的字符转换为大写。你还将学习如何以相反的顺序显示随机文件的内容以及如何计算文件中的元音字母数量。本章还将解释如何用另一个词替换文件中的词,以及如何保护文件免受未经授权的访问。你还将了解文件是如何被加密的。

第六章,实现并发,涵盖了并发,这是为了提高 CPU 的效率而实现的。在本章中,你将学习如何使用单个线程完成任务。此外,你还将学习如何使用多个线程执行多个任务,以及使用互斥锁在两个线程之间共享数据的技巧。除此之外,你还将学习如何识别可能导致死锁的情况以及如何避免它。

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

第八章,使用 MySQL 数据库,解释了没有在数据库中存储信息,任何实时应用都是不可能的。数据库中的信息需要被管理。在本章中,你将学习如何显示默认 MySQL 数据库中的所有内置表。你将学习如何在 MySQL 数据库中存储信息以及如何在数据库表中搜索信息。你还将学习如何更新数据库表中的信息,以及当不再需要时如何从数据库中删除数据。

附录 A 逐步解释了如何创建顺序文件和随机文件。第五章 文件处理 中的大多数食谱都是关于从文件中读取内容,并且这些食谱假设文件已经存在。本章解释了如何创建顺序文件并在其中输入一些文本。你还将学习如何从顺序文件中读取内容。除此之外,你还将学习如何创建随机文件并在其中输入一些内容,以及如何从随机文件中读取内容并在屏幕上显示它。最后,你还将学习如何解密加密文件的内容。

附录 B 解释了如何安装 Cygwin。

附录 C 解释了如何安装 MySQL Community Server。

为了充分利用本书

你必须对 C 编程有一些初步的了解。如果你对数组、字符串、函数、文件处理、线程和进程间通信有一些先前的基本知识,这将很有益处。

此外,你必须对基本的 SQL 命令有所了解,以便处理数据库。

下载示例代码文件

您可以从您的 www.packt.com 账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packt.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/C-Programming-Cookbook。我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 上找到。查看它们吧!

下载彩色图像

我们还提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789617450_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/submit-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 语言中的数组是基于 0 的,所以您输入的位置会减去 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 语言中的数组是零基的,位置 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[0] 在数组 q 的任何位置找到,它将被添加到数组 r 中,如下所示:

图 1.12

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

  2. 因为在 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 循环,将 p[0] 与数组 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,它们分别指向三个数组pqr的元素。

比较数组pq的第一个元素,即p[0]q[0],并将较小的值分配到数组r中。

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

图 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中,并且qr数组的索引将增加以进行下一次比较(参见图下所示):

图 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

Voilà!我们已经成功将两个排序后的数组合并为一个。

第二章:管理字符串

字符串不过是存储字符的数组。由于字符串是字符数组,它们占用的内存较少,导致代码更高效,使程序运行更快。就像数值数组一样,字符串也是零基的,也就是说,第一个字符存储在索引位置 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,程序应该输出“The first repetitive character in the string racecar is c.”。如果输入没有重复字符的字符串,程序应显示“No character is repeated in the string”。

如何做到这一点…

  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,如下所示:

图 2.4

使用库函数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中:

图片

图片

下一个要从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 函数,我们首先计算字符串的长度。然后,我们将使用字符串数组 chr 在每个索引位置单独存储 str 数组中的字符。我们将从 1 开始执行一个循环,直到字符串的末尾,以访问字符串中的每个字符。

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

图 2.10

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

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

  • 如果字符不在 chr 数组中,它将被添加到 chr 数组的下一个索引位置,并且当计数数组设置为 1 时找到相应的索引位置。例如,如果字符 achr 数组中没有找到,它将被添加到下一个可用的索引位置。如果字符 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以将其转换为大写

  • 32加到一个大写字符的 ASCII 值上以将其转换为小写

以下图表显示了大小写元音的 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

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

第三章:探索函数

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

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

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

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

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

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

  • 使用递归查找最大公约数(GCD)

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

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

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

什么是栈?

栈是一种可以用数组以及链表实现的抽象数据类型。它类似于一个桶,您输入的值将被添加到桶底。您接下来添加到栈中的下一个项将位于之前添加的项之上。将值添加到栈中的过程称为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. 需要找出最大和最小值的数组的大小不是固定的,因此我们将定义一个大小为 100 的宏 max
#define max 100
  1. 我们将定义一个最大大小的 arr 数组,即 100 个元素:
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 的 GCD 是 4,9 和 18 的 GCD 是 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))

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

  1. 你将被提示输入两个整数。将输入的整数赋给两个变量,uv
printf("Enter two numbers: ");
scanf("%d %d",&x,&y);
  1. 调用 gcd 函数并将 xy 的值传递给它。xy 的值将分别赋给 ab 参数。将 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%24 (x % y) 是否等于 0。因为 18 不能被 24 整除,所以 18 将是余数:

图 3.15

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

图 3.16

因为 24%18 的结果是非零值,所以我们将再次使用 gcd(b, m) 参数调用 gcd 函数,现在是 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

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

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

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

在这个菜谱中,我们将学习如何将二进制数转换为十六进制数。二进制数由两个位组成,即 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循环中,对二进制数应用 mod 10 运算符并将余数推送到栈中:
remainder=bin%10;
push(remainder);
  1. 执行另一个while循环,从栈中获取所有二进制位的十进制数。while循环将执行,直到栈为空(即,直到top的值大于或等于0):
while(top >=0)
  1. while循环中,弹出栈中的所有二进制位,并将每个位乘以2top次幂。将结果相加以获取输入二进制数的十进制等效值:
j=pop();
deci=deci+j*pow(2,exp);
  1. 调用intohexa函数,并将二进制数和十进制数传递给它以获取十六进制数:
void intohexa(int bin, int deci)
  1. intohexa函数中对十进制数应用 mod 16运算符以获取其十六进制数。将得到的余数推送到栈中。再次对商应用 mod 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的幂。然后,我们将应用模10运算符以将二进制数分离成单独的数字。每次对二进制数应用模10时,其最后一个数字都会被分离并推入栈中。

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

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

图 3.18

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

图 3.19

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

图 3.20

完成此操作后,下一步是逐个弹出数字,并将每个数字乘以2top次方。例如,2top次方意味着2将提升到从弹出二进制位的位置的索引值。从栈中弹出的值是从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

所有乘以相应幂的数字然后相加。因为计算结果与原始数字匹配,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

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

第二部分:指针和文件

在本节中,我们将更详细地探讨指针的使用以及我们在 C 语言中使用指针的有效实现。我们还将查看各种配方,以更好地理解和处理文件及其内容。

本节将涵盖以下章节:

  • 第四章,深入指针

  • 第五章,文件处理

第四章:深入了解指针

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

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

  • 使用指针反转字符串

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

  • 对单链表进行排序

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

  • 使用指针访问结构

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

什么是指针?

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

我们需要它们,因为它们做以下事情:

  • 促进内存的动态分配

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

  • 使函数能够返回多个值

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

int i;

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

图 4.1

在前面的图中,1000代表变量i的内存地址。尽管实际上内存地址很大,并且是十六进制格式,但为了简单起见,我选择了一个小的整数数字1000。值10存储在内存地址1000中。

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

int *j;

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

j=&i;

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

图 4.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 的内存地址:

图 4.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,名字中的每个字符将依次分配到数组的某个位置(参见 图 4.4)。我们可以看到,字符串的第一个字符,字母 m,被分配到 str[0] 位置,第二个字符串字符被分配到 str[1] 位置,依此类推。空字符,像往常一样,位于字符串的末尾,如下面的图所示:

图 4.4

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

图 4.5

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

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

图 4.6

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

图 4.7

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

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

图 4.8

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

图 4.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);
}

现在,让我们看看幕后。

它是如何工作的…

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

图 4.10

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

图 4.11

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

图 4.12

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

图 4.13

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

图 4.14

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

图 4.15

图 4.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

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

对单链表进行排序

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

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

图 4.16

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

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

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

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

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

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

如何操作...

  1. 定义一个包含两个成员——datanext的节点。数据成员用于存储整数值,而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。这个链表已经准备好进行排序过程。设置一个从0n-2for循环,即 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(如图 4.17 所示)。这个下一个指针将用于与其他节点连接(正如我们很快将看到的):

图 4.17

在创建第一个节点后,我们将执行以下两个指针指向它,如下所示:

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

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

图 4.18

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

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

图 4.19

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

图 4.20

第三个和第四个步骤将重复应用于链表中的其余节点。最后,单链表将准备就绪,看起来可能像这样:

图 4.21

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

对单链表进行排序

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

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

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

第一次迭代

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

图 4.22

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

图片

图 4.23

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

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

图片

图 4.24

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

图片

图 4.25

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

第二次迭代

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

图 4.26

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

图 4.27

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

第三次和第四次迭代

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

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

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

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

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

图 4.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

图 4.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指针设置为指向分配的内存块的第一个内存位置,如图 4.30 所示。当ptr指针被设置为指向第一个内存位置时,它将自动获取第一个内存位置的地址,因此1000将被分配给ptr指针:

图 4.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)。所以,当上述公式在嵌套循环中应用时,原始矩阵将如下显示:

图 4.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

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

图片

图 4.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 大小的矩阵,如图所示。在这种情况下,矩阵将被分配六个连续的内存位置,每个位置两个字节(见图 4.33)。为什么每个位置是两个字节?这是因为一个整数占用两个字节。这也意味着如果我们定义一个占用四个字节的浮点型矩阵,每个分配的内存位置将包含四个字节:

图片

图 4.33

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

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

图片

图 4.34

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

图片

图 4.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))

以下图表(图 5.1)中直观地解释了前面的步骤:

图 5.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. 在循环中,文件指针将被定位如下:

图 5.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 * size of one line)位置。一旦文件指针定位在这个位置,我们将使用fread函数读取文件的最后一行并将其分配给结构变量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);

前面的步骤如下面的图所示:

图 5.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);

以下图显示了前面的步骤:

图 5.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 的命令提示符中执行。

哇!我们已经成功加密了文件!

第六章:第三节:并发、网络和数据库

在本节中,我们将学习如何在线程中实现并发以减少内存使用并加快 CPU 速度。然后我们将学习如何建立进程间的通信并在服务器和客户端之间创建网络。最后,我们将学习如何使用数据库来存储和管理我们的数据。

本节将涵盖以下章节:

  • 第六章,实现并发

  • 第七章,网络和进程间通信

  • 第八章,使用 MySQL 数据库

第七章:实现并发

多任务处理是几乎所有操作系统的关键特性;它提高了 CPU 的效率,并以更好的方式利用资源。线程是实现多任务处理的最佳方式。一个进程可以包含多个线程以实现多任务处理。

在本章中,我们将介绍以下涉及线程的食谱:

  • 使用单个线程执行任务

  • 使用多个线程执行多个任务

  • 使用 mutex 在两个线程之间共享数据

  • 理解死锁是如何产生的

  • 避免死锁

进程和线程这两个术语可能会令人困惑,所以首先,我们要确保你理解它们。

进程和线程是什么?

每当我们运行一个程序时,当它从硬盘(或任何其他存储)加载到内存中时,它就变成了一个进程进程由处理器执行,并且为了执行它,它需要一个程序计数器(PC)来跟踪下一个要执行的指令,CPU 寄存器,信号等。

线程指的是程序中的一组可以独立执行的指令。线程有自己的 PC 和一组寄存器,以及其他一些东西。这样,一个进程由多个线程组成。两个或多个线程可以共享它们的代码、数据和其他资源,但在线程之间共享资源时必须格外小心,因为这可能会导致歧义和死锁。操作系统还管理线程池。

线程池包含一组等待分配任务以进行并发执行的线程。使用线程池中的线程而不是实例化新线程有助于避免创建和销毁新线程造成的延迟;因此,它提高了应用程序的整体性能。

基本上,线程通过并行性提高了应用程序的效率,即通过同时运行两个或更多独立代码集。这被称为多线程

C 语言不支持多线程,因此为了实现它,使用 POSIX 线程(Pthreads)。GCC 允许实现一个 pthread

当使用 pthread 时,定义一个 pthread_t 类型的变量来存储线程标识符。线程标识符是一个唯一的整数,即分配给系统中的线程。

你可能想知道用于创建线程的函数是哪个。pthread_create 函数被调用以创建线程。以下四个参数传递给 pthread_create 函数:

  • 指向线程标识符的指针,该指针由该函数设置

  • 线程的属性;通常,为此参数提供 NULL 以使用默认属性

  • 用于创建线程时要执行的函数的名称

  • 要传递给线程的参数,如果不需要传递参数,则设置为 NULL

当两个或多个线程操作相同的数据时,即它们共享相同的资源,必须应用某些检查措施,以确保一次只允许一个线程操作共享资源;其他线程的访问必须被阻塞。帮助避免线程间共享资源时歧义的一种方法就是互斥。

互斥

为了避免两个或多个线程访问相同资源时的歧义,互斥实现了对共享资源的序列化访问。当一个线程正在使用资源时,不允许其他线程访问相同的资源。所有其他线程都会被阻塞,直到资源再次空闲。

互斥锁基本上是与共享资源相关联的锁。要读取或修改共享资源,线程必须首先获取该资源的锁。一旦线程获取了该资源的锁(或互斥锁),它就可以继续处理该资源。所有其他希望处理该资源的线程都将被迫等待,直到资源解锁。当线程完成对共享资源的处理时,它会解锁互斥锁,使其他等待的线程能够获取该资源的互斥锁。除了互斥锁之外,信号量也用于进程同步。

信号量是一个用于避免在并发系统中两个或多个进程访问公共资源的概念。它基本上是一个变量,通过操作它只允许一个进程访问公共资源并实现进程同步。信号量使用信号机制,即分别调用waitsignal函数来通知公共资源已被获取或释放。另一方面,互斥锁使用锁定机制——进程在操作公共资源之前必须获取互斥锁对象的锁。

虽然互斥锁有助于管理线程之间的共享资源,但存在一个问题。互斥锁的错误顺序应用可能导致死锁。死锁发生在这样一个情况下:一个持有锁 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。让我们运行这个可执行文件:

哇!我们已经成功使用单个线程完成了一个任务。现在,让我们继续下一个菜谱!

使用多个线程执行多个任务

在这个菜谱中,你将学习如何通过并行执行两个线程来执行多任务。这两个线程将独立执行它们自己的任务。由于两个线程不会共享资源,因此不会出现竞争条件或歧义的情况。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;
}

现在,让我们看看幕后。

它是如何工作的...

我们将定义两个名为 tid1tid2pthread_t 类型的变量,以存储两个线程标识符。这些线程标识符唯一地代表系统中的线程。我们将调用 pthread_create 函数两次以创建两个线程,并将它们的标识符分配给两个变量 tid1tid2,其地址被传递给 pthread_create 函数。

两个线程使用默认属性创建。我们将执行 runThread1 函数以创建第一个线程,然后执行 runThread2 函数以创建第二个线程。

runThread1 函数中,我们将显示消息 Running Thread 1 以指示第一个线程已被创建并正在运行。此外,我们将调用一个 for 循环,通过运行中的线程显示从 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。让我们运行这个可执行文件:

您可能不会得到完全相同的输出,因为这取决于 CPU,但可以肯定的是,两个线程将同时退出。

哇!我们已经成功地使用多个线程完成了多个任务。现在,让我们继续下一个菜谱!

使用 mutex 在两个线程之间共享数据

独立运行两个或更多线程,其中每个线程访问其自己的资源,相当方便。然而,有时我们希望线程能够同时共享和处理相同的资源,以便我们可以更快地完成任务。共享公共资源可能会导致问题,因为一个线程可能会在另一个线程写入更新数据之前读取数据,导致模糊的情况。为了避免这种情况,使用 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,以存储两个线程标识符。

我们将调用初始化lock对象的pthread_mutex_init方法,该对象具有默认的mutex属性。初始化后,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。让我们运行这个可执行文件:

哇!我们已经成功使用mutex在两个线程之间共享数据。现在,让我们继续下一个菜谱!

理解死锁是如何产生的

锁定资源有助于得到非歧义的结果,但锁定也可能导致死锁。死锁是一种情况,其中线程已经获取了一个资源的锁并想要获取第二个资源的锁。然而,同时,另一个线程已经获取了第二个资源的锁,但想要第一个资源的锁。因为第一个线程将一直等待第二个资源锁变为空闲,而第二个线程将一直等待第一个资源锁变为空闲,所以线程将无法进一步进行,应用程序将挂起(如下面的图所示):

在这个菜谱中,我们将使用栈。栈需要两个操作——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操作的mutex对象(push_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。让我们运行这个可执行文件:

图片

你现在已经看到了死锁是如何发生的。现在,让我们继续到下一个菜谱!

避免死锁

如果允许线程按顺序获取锁,则可以避免死锁。假设一个线程获取了一个资源的锁,并想要获取第二个资源的锁。任何尝试获取第一个锁的其他线程将被要求等待,因为它已经被第一个线程获取了。因此,第二个线程也无法获取第二个资源的锁,因为它只能按顺序获取锁。然而,我们的第一个线程将被允许获取第二个资源的锁,而无需等待。

将一个序列应用于资源锁定与只允许一次只有一个线程获取资源是相同的。其他线程只有在之前的线程完成后才能获取资源。这样,我们手中就不会有死锁了。

如何做到这一点…

  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 秒后,mutex 对象,即打算调用 pop 操作 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函数中的第二个线程首先尝试锁定push_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。现在我们来运行这个可执行文件:

哇!我们已经成功避免了死锁。

第八章:网络和进程间通信

进程各自独立运行并在各自的地址空间中工作。然而,它们有时需要相互通信以传递信息。为了使进程能够协作,它们需要能够相互通信并同步它们的行为。以下是进程间发生的通信类型:

  • 同步通信:这种通信不允许进程在通信完成前继续进行任何其他工作

  • 异步通信:在这种通信中,进程可以继续执行其他任务,因此它支持多任务处理,并导致更高的效率

  • 远程过程调用RPC):这是一个使用客户端服务技术进行通信的协议,其中客户端无法执行任何操作,也就是说,它被挂起,直到从服务器收到响应

这些通信可以是单向的或双向的。为了在进程之间启用任何形式的通信,以下常用的进程间通信IPC)机制被使用:管道、FIFOs(命名管道)、套接字、消息队列和共享内存。管道和 FIFOs 允许单向通信,而套接字、消息队列和共享内存则允许双向通信。

在本章中,我们将学习如何制作以下食谱,以便我们可以在进程之间建立通信:

  • 使用管道在进程间通信

  • 使用 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 函数并将选定的字符串写入管道的 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,以及一个名为str的字符串,其大小也是BUFFSIZE,即 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代表给定套接字的挂起连接队列的最大长度。如果队列已满,将生成错误。

如果函数成功,它返回零,否则返回-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 转换为网络字节序,然后作为端口号应用。server_Address 结构的第四个参数 sin_zero 通过调用 memset 函数设置为 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结构的长度。

函数返回写入缓冲区的消息长度,该缓冲区由缓冲区参数指向。

现在,我们可以开始这个配方的第一部分:使用 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结构体的所有成员都将填充零。

服务器如预期的那样等待来自客户端的数据报。因此,屏幕上显示以下文本消息:“等待来自客户端的消息”。我们通过调用名为UDPSocketsocket函数创建套接字。为套接字提供的地址族是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函数,将回复发送给客户端。发送消息后,屏幕上显示以下消息:Reply to the client sent

现在,让我们看看本食谱的另一个部分。

使用 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。

使用消息队列从一个进程向另一个进程传递消息

在这个食谱中,我们将学习如何使用消息队列在两个进程之间建立通信。这个食谱分为以下几部分:

  • 将消息写入消息队列

  • 从消息队列中读取消息

在我们开始这些食谱之前,让我们快速回顾一下在成功使用共享内存和消息队列进行进程间通信时使用的函数、结构和术语。

在使用共享内存和消息队列进行进程间通信中使用的函数

在使用共享内存和消息队列进行进程间通信中最常用的函数和术语是 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函数成功执行后,rcvbuffer中的mesg成员将显示在屏幕上,包含来自消息队列的消息。

让我们使用 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函数,我们使用文件名sharedmem(你可以更改此名称)和 ID 为a生成一个 IPC 密钥。生成的密钥被分配给键变量。之后,调用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

我们将创建一个新的具有读写权限的内存段,并将获取的共享内存标识符分配给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

哇!我们已经成功使用共享内存在不同进程之间进行了通信。

第九章:使用 MySQL 数据库

MySQL 是近年来最受欢迎的数据库管理系统之一。众所周知,数据库用于存储将来需要使用的数据。数据库中的数据可以通过加密来保护,并且可以建立索引以实现更快的访问。当数据量太大时,数据库管理系统比传统的顺序和随机文件处理系统更受欢迎。在任何应用程序中,将数据存储在数据库中都是一个非常重要的任务。

本章的重点是理解如何在数据库表中管理表行。在本章中,你将学习以下菜谱:

  • 显示默认 MySQL 数据库中的所有内置表

  • 将信息存储到 MySQL 数据库中

  • 在数据库中搜索所需信息

  • 更新数据库中的信息

  • 使用 C 语言从数据库中删除数据

在我们继续到菜谱之前,我们将回顾 MySQL 中最常用的函数。同时,确保你在实施本章中的菜谱之前阅读附录 B附录 C以安装 Cygwin 和 MySQL 服务器。

MySQL 中的函数

在 C 编程语言中访问和使用 MySQL 数据库时,我们不得不使用几个函数。让我们来看看它们。

mysql_init()

这初始化了一个MYSQL对象,该对象可以在mysql_real_connect()方法中使用。以下是它的语法:

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 数据库的函数。从第二个食谱开始,我们将在一个数据库表中工作。所以,让我们开始创建一个数据库和其中的表。

创建 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. while循环中逐行从resultset中获取,并仅显示该行中的表名:
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[0]索引中的表名将在屏幕上显示。随着while循环的每次迭代,将从resultset res中提取下一块表信息并分配给数组行。因此,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 

Voila!正如你所见,输出显示了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)   

!给定的输出确认,通过 C 语言输入的新行已成功插入到 users 数据库表中。

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

在数据库中搜索所需信息

在这个菜谱中,我们将学习如何在数据库表中搜索信息。同样,我们假设已经存在一个包含三个列 email_addresspasswordaddress_of_deliveryusers 表(请参阅本章的 创建 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 函数,并将 MYSQL 对象 conn 及有效的用户 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
+---------------------+----------+------------------------------------+

Voila!我们可以看到,具有电子邮件地址 bmharwani@yahoo.comusers 表的行已经被更新,并显示了新的信息。

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

使用 C 语言从数据库中删除数据

在这个菜谱中,我们将学习如何从数据库表中删除信息。我们假设一个包含三个列 email_addresspasswordaddress_of_deliveryusers 表已经存在(请参阅本章开头,我们在其中创建了一个 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 表中删除。

附录 A

在本书的这一部分,我们将探讨一些超出第三章“探索函数”范围的其他菜谱:

  • 创建一个顺序文件并将一些文本输入到它里面

  • 从顺序文件读取内容并在屏幕上显示

  • 创建一个随机文件并将一些数据输入到它里面

  • 从随机文件读取内容并在屏幕上显示

  • 解密加密文件的内容

创建一个顺序文件并将一些数据输入到它里面

在这个菜谱中,我们将创建一个顺序文件,用户可以输入任意数量的行到它里面。要创建的文件名将通过命令行参数传递。你可以输入任意多的行到文件中,完成时,你必须输入 stop,然后按 Enter 键。

如何操作…

  1. 以只写模式打开一个顺序文件,并用文件指针指向它:
fp = fopen (argv[1], "w");
  1. 当提示时输入文件内容:
printf("Enter content for the file\n");
gets(str);
  1. 当你完成输入文件内容时,请输入 stop
while(strcmp(str, "stop") !=0)
  1. 如果你输入的字符串不是 stop,则该字符串将被写入文件:
fputs(str,fp);
  1. 关闭文件指针以释放分配给文件的所有资源:
fclose(fp);

创建顺序文件的 createtextfile.c 程序如下:

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

void main (int argc, char* argv[])
{
   char str[255];
   FILE *fp;

  fp = fopen (argv[1], "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 定义一个文件指针。我们将以只写模式打开通过命令行参数提供的顺序文件,并将 fp 文件指针设置为指向它。如果文件无法以只写模式打开,可能是因为权限不足或磁盘空间限制。将显示错误消息,程序将终止。

如果文件以只写模式成功打开,你将被提示输入文件内容。你输入的所有文本都将分配给 str 字符串变量,然后写入文件。完成输入文件内容后,你应该输入 stop。最后,我们将关闭文件指针。

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

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

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

D:\CBook>createtextfile textfile.txt
Enter content for the file
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
stop

哇!我们已经成功创建了一个顺序文件并在其中输入了数据。

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

从顺序文件读取内容并在屏幕上显示

在这个菜谱中,我们假设一个顺序文件已经存在,因此我们将读取该文件的内容并在屏幕上显示。我们想要读取内容的文件名将通过命令行参数提供。

如何操作...

  1. 以只读模式打开顺序文件并将 fp 文件指针设置为指向它:
fp = fopen (argv [1],"r");
  1. 如果文件无法以只读模式打开,程序将终止:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
}
  1. 设置一个while循环,直到文件末尾:
while (!feof(fp))
  1. while循环中,一次读取一行文件并显示在屏幕上:
fgets(buffer, BUFFSIZE, fp);
puts(buffer);
  1. 关闭文件指针以释放分配给文件的所有资源:
fclose(fp);

读取顺序文件的readtextfile.c程序如下:

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

#define BUFFSIZE 255

void main (int argc, char* argv[])
{
   FILE *fp;
   char buffer[BUFFSIZE];

  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);
    puts(buffer);
  }
  fclose(fp);
}

现在,让我们深入了解代码背后的情况。

它是如何工作的…

我们将定义一个名为fp的文件指针和一个名为buffer的字符串,大小为 255。我们将以只读模式打开通过命令行参数提供的顺序文件,并将fp文件指针设置为指向它。如果因为文件不存在或权限不足而无法以只读模式打开文件,将显示错误消息,程序将终止。

如果文件以只读模式成功打开,设置一个while循环,直到文件末尾。使用fgets函数逐行从文件中读取;从文件中读取的行被分配给buffer字符串。然后,在屏幕上显示buffer字符串中的内容。当到达文件末尾时,while循环将终止,文件指针被关闭以释放分配给文件的所有资源。

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

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

如果你没有错误或警告,这意味着readtextfile.c程序已经被编译成一个可执行文件,readtextfile.exe。假设我们想要读取内容的文件是 textfile.txt,让我们运行可执行文件readtextfile.exe

D:\CBook>readtextfile 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

哇!我们已经成功从我们的顺序文件中读取内容并在屏幕上显示。

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

创建一个随机文件并将一些数据输入其中

在这个菜谱中,我们将创建一个随机文件,并将一些文本行输入其中。随机文件是有结构的,随机文件中的内容是通过结构写入的。使用结构创建文件的好处是我们可以直接计算任何结构的定位,并且可以随机访问文件中的任何内容。要创建的文件名通过命令行参数传递。

如何做到这一点…

创建随机文件并在其中输入几行文本的步骤如下。你可以输入任意数量的行;完成时,只需按stop,然后按Enter键:

  1. 定义一个由字符串成员组成的结构:
struct data{
    char str[ 255 ];
};
  1. 以只写模式打开一个随机文件,并用文件指针指向它:
fp = fopen (argv[1], "wb");
  1. 如果文件无法以只写模式打开,程序将终止:
if (fp == NULL) {
    perror ("An error occurred in creating the file\n");
    exit(1);
}
  1. 在提示时输入文件内容并将其存储到结构成员中:
printf("Enter file content:\n");
gets(line.str);
  1. 如果输入的文本不是stop,则包含该文本的结构将被写入文件:
while(strcmp(line.str, "stop") !=0){
    fwrite( &line, sizeof(struct data), 1, fp );
  1. 重复步骤 4 和 5,直到你输入stop

  2. 当你输入stop时,指向文件的文件指针被关闭以释放分配给文件的资源:

fclose(fp);

创建随机文件的createrandomfile.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;
    fp = fopen (argv[1], "wb");
    if (fp == NULL) {
        perror ("An error occurred in creating the file\n");            
        exit(1);
    }
    printf("Enter file content:\n");
    gets(line.str);
    while(strcmp(line.str, "stop") !=0){
        fwrite( &line, sizeof(struct data), 1, fp );
        gets(line.str);
    }
    fclose(fp);
}

现在,让我们幕后了解一下代码。

它是如何工作的…

让我们从定义一个名为data的结构体开始,它包含一个名为str的成员,这是一个大小为 255 的字符串变量。然后我们将定义一个名为fp的文件指针,接着是一个类型为data结构体的变量line,这样行就变成了一个包含名为str的成员的结构体。我们将以只写模式打开一个名为通过命令行参数提供的随机文件,并将fp文件指针设置为指向它。如果由于任何原因无法以只写模式打开文件,将显示错误消息,程序将终止。

你将被提示输入文件内容。你输入的文本将被分配给行结构体的str成员。因为你应该输入stop来表示你已完成文件中的数据输入,所以你输入的文本将与stop字符串进行比较。如果输入的文本不是stop,它将被写入由fp文件指针指向的文件。

因为它是随机文件,所以文本通过结构行写入文件。fwrite函数将等于结构行大小的字节数写入由fp指针指向的文件在其当前位置。行结构中的str成员中的文本被写入文件。当用户输入的文本是stop时,指向文件的文件指针fp被关闭。

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

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

如果你没有错误或警告,这意味着createrandomfile.c程序已经被编译成一个可执行文件,createrandomfile.exe。假设我们想要创建一个名为random.data的随机文件,让我们运行可执行文件createrandomfile.exe

D:\CBook>createrandomfile random.data
Enter file content:
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.
stop

哇!我们已经成功创建了一个随机文件并在其中输入了一些数据。

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

从随机文件读取内容并在屏幕上显示

在这个菜谱中,我们将读取随机文件的内容,并将其显示在屏幕上。因为随机文件中的内容由记录组成,其中记录的大小已经知道,所以可以从随机文件中随机选择任何记录;因此,这种类型的文件被称为随机文件。要从随机文件中访问第n条记录,我们不需要先读取前n-1 条记录,就像在顺序文件中做的那样。我们可以计算该记录的位置,并直接访问它。要读取的文件名通过命令行参数传递。

如何做到这一点…

  1. 定义一个包含字符串成员的结构体:
struct data{
    char str[ 255 ];
};
  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. 使用for循环一次读取文件中的一个记录:
for (i=1;i<=nol;i++)
fread(&line,sizeof(struct data),1,fp);
  1. 从随机文件读取的内容是通过步骤 1 中定义的结构体获取的。通过显示分配给结构体成员的文件内容来显示文件内容:
puts(line.str);
  1. for循环结束时,达到文件末尾。关闭文件指针以释放分配给文件的资源:
fclose(fp);

读取随机文件内容的readrandomfile.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);
    rewind(fp);
    printf("The content in file is :\n");
    for (i=1;i<=nol;i++)
    {
        fread(&line,sizeof(struct data),1,fp);
        puts(line.str);
    }
    fclose(fp);
}

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

它是如何工作的...

我们将定义一个名为data的结构体,它包含一个名为str的成员,该成员是一个大小为 255 的字符串变量。然后,定义一个名为fp的文件指针和一个类型为数据结构line的变量,这样行就变成了一个具有名为str的成员的结构体。我们将以只读模式打开一个随机文件,其名称通过命令行参数提供,并将fp文件指针设置为指向它。如果由于任何原因无法以只读模式打开文件,将显示错误消息,并终止程序。如果引用了任何不存在的文件,或者文件没有足够的权限,则可能发生文件错误。如果文件成功以只读模式打开,下一步是找到文件中的记录总数。为此,应用以下公式:

文件中的总字节数/每条记录的大小

要找到文件中的总字节数,首先通过调用fseek函数将文件指针移动到文件末尾。然后,使用ftell函数检索文件消耗的总字节数。然后,我们将文件中的总字节数除以每条记录的大小,以确定文件中的总记录数。

现在,我们已经准备好一次读取文件中的一个记录,为此,我们将文件指针移动到文件的开头。我们将设置一个for循环,使其执行次数与文件中的记录数相同。在for循环内部,我们将调用fread函数一次读取文件中的一个记录。从文件中读取的文本被分配给行结构体的str成员。行结构体中str成员的内容将在屏幕上显示。当for循环结束时,由fp文件指针指向的文件将被关闭,以释放分配给文件的资源。

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

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

如果没有错误或警告,这意味着readrandomfile.c程序已被编译成可执行文件,readrandomfile.exe。假设我们想要创建一个名为random.data的随机文件,让我们运行可执行文件,readrandomfile.exe

D:\CBook>readrandomfile random.data
The content in file is :
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.

哇!我们已经成功从随机文件中读取内容并在屏幕上显示它。

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

解密加密文件的内容

在这个菜谱中,我们将读取一个加密文件。我们将解密其内容,并将解密后的内容写入另一个顺序文件。两个文件名,加密文件和我们将保存解密版本的文件,都是通过命令行参数提供的。

如何操作...

在这个程序中使用了两个文件。一个是只读模式打开的,另一个是只写模式打开的。读取并解密一个文件的内容,并将解密内容存储在另一个文件中。以下是将现有加密文件解密并保存到另一个文件的步骤:

  1. 以只读和只写模式打开两个文件:
fp = fopen (argv [1],"r");
fq = fopen (argv[2], "w");
  1. 如果任一文件无法以相应模式打开,程序将在显示错误消息后终止:
if (fp == NULL) {
    printf("%s file does not exist\n", argv[1]);
    exit(1);
}
if (fq == NULL) {
    perror ("An error occurred in creating the file\n");
    exit(1);
}
  1. 设置一个while循环来执行。它将逐行从要读取的文件中读取:
while (!feof(fp))
fgets(buffer, BUFFSIZE, fp);
  1. 从文件中读取的行的长度被计算:
n=strlen(buffer);
  1. 设置一个for循环来执行。这将逐个访问行的所有字符:
for(i=0;i<n;i++)
  1. 45的值加到每个字符的 ASCII 值上以解密它。我假设每个字符的 ASCII 值减去45以加密文件:
buffer[i]=buffer[i]+45;
  1. 解密后的行将被写入第二个文件:
fputs(buffer,fq);
  1. while循环完成后(读取,即正在解密的文件),两个文件指针都将被关闭以释放分配给它们的资源:
fclose (fp);
fclose (fq);

用于解密加密文件的decryptfile.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];
    fp = fopen (argv [1],"r");
    if (fp == NULL) {
        printf("%s file does not exist\n", argv[1]);
        exit(1);
    }
    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。我们将以只读模式打开通过命令行参数提供的第一个文件,并将第二个文件以只写模式打开。如果无法以只读模式和只写模式分别打开文件,将显示错误消息,程序将终止。以只读模式打开的文件由fp文件指针指向,以只写模式打开的文件由fq文件指针指向。

我们将设置一个while循环来执行,该循环将逐行读取由fp指针指向的文件的每一行。while循环将继续执行,直到到达由fp指向的文件的末尾。

while 循环中,读取一行并将其分配给 buffer 字符串变量。计算行的长度。然后设置一个 for 循环,执行到行的末尾;也就是说,访问行的每个字符。我们将把值 45 添加到每个字符的 ASCII 值以加密它。之后,我们将解密后的行写入第二个文件。该文件由 fq 文件指针指向。当读取内容的文件完成时,关闭两个文件指针以释放分配给两个文件的资源。

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

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

假设加密文件命名为 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 的命令提示符中执行的。

如果在编译文件时没有错误或警告,这意味着 decryptfile.c 程序已经被编译成一个可执行文件,名为 decryptfile.exe。假设已经存在一个名为 encrypted.txt 的加密文件,并且你想将其解密到另一个文件中,名为 originalfile.txt。因此,让我们运行可执行文件 decryptfile.exe 来解密 encrypted.txt 文件:

D:\CBook>decryptfile encrypted.txt originalfile.txt

让我们查看 orignalfile.txt 的内容,看看它是否包含文件的解密版本:

D:\CBook>type originalfile.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

哇!你可以看到 originalfile.txt 包含了解密后的文件。

附录 B

我在这本书中使用 Cygwin 来编译和运行我的 C 程序。Cygwin 是一个优秀的工具,它提供了一个在 Windows 操作系统上编译和运行 UNIX 或 Linux 应用程序的环境。在本书的这一部分,我们将学习如何安装 Cygwin。

安装 Cygwin

要下载 Cygwin,请访问www.cygwin.com/。您将获得两个设置文件,setup-x86_64.exe用于 64 位安装,setup-x86.exe用于 32 位安装。根据您机器上安装的 Windows 操作系统版本,您可以下载 64 位或 32 位设置文件。因为我电脑上安装了 64 位的 Windows 10,所以我下载了setup-x86_64.exe文件。执行以下步骤:

  1. 要开始 Cygwin 安装,只需双击下载的setup-x86_64.exe文件。然后您将看到一个对话框,显示 Cygwin 设置程序的小介绍。只需点击“下一步”按钮继续:

截图

  1. 您将看到一个对话框,要求您从以下三个选项中选择您想要的安装类型:
  • 从互联网安装:此选项将从网络下载 Cygwin 文件并安装它们。下载的文件将保存在本地硬盘上以供将来使用。

  • 不安装下载:此选项将下载整个 Cygwin 程序,但不会安装它(但您可以在将来任何时候从下载的文件中安装它)。

  • 从本地目录安装:此选项将从之前下载的设置文件中安装 Cygwin。

因为我们想要从互联网下载和安装文件,所以让我们选择第一个选项,从互联网安装,并点击“下一步”按钮,如下面的截图所示:

截图

  1. 您将被提示指定您想要安装 Cygwin 的驱动器和目录。我想在 D:驱动器上安装 Cygwin,在名为cygwin64的文件夹中(如下面的截图所示)。您可以指定任何您想要的驱动器或文件夹。您还需要指定您是否希望 Cygwin 仅供您使用或供本系统所有用户使用。因为我们希望 Cygwin 供系统所有用户使用,所以我们将选择第一个选项,选择“所有用户”(这是默认选项),然后点击“下一步”按钮,如下面的截图所示:

截图

  1. 您将被提示指定要存储安装文件的驱动器和目录(如下面的截图所示)。指定所需的驱动器和目录后,点击“下一步”按钮继续:

截图

  1. 您将被要求指定您如何连接到互联网——即您是否直接连接到互联网或通过代理服务器连接。如果您通过代理连接,您需要指定代理主机的详细信息及其端口号。因为我直接连接到互联网,所以我将选择“直接连接”选项(如下面的截图所示),然后点击“下一步”按钮:

图片

  1. 您将看到可以下载和安装 Cygwin 的网站列表,如下面的截图所示。从列表中选择任何选项,然后点击“下一步”按钮:

图片

  1. 您将看到一个要安装的软件包类别的列表(如下面的截图所示)。除了默认选择的软件包外,我们还需要从数据库软件包类别安装一些文件。我们将需要数据库软件包,因为我们将在本书中使用 C 程序访问 MySQL 数据库和表。您可以在数据库类别节点上点击加号符号来展开它:

图片

  1. 您将在数据库类别节点下获取可下载的软件包列表。选择以 mysql 为前缀的软件包,例如 mysql 客户端应用、mysql 基准测试、mysql 通用、mysql 服务器,如下面的截图所示。选择所需的软件包后,点击“下一步”按钮下载和安装 Cygwin:

图片

  1. 然后将从网络上下载 Cygwin 文件并在您的机器上安装。一旦 Cygwin 成功安装,您将看到一个确认对话框。点击“完成”按钮关闭对话框:

图片

现在您已经准备好使用 Cygwin 来实施本书中的食谱了。

附录 C

在本书的这一部分,我们将快速回顾如何安装 MySQL 服务器。这是实现第八章使用 MySQL 数据库中食谱所必需的。

安装 MySQL 服务器

您需要从dev.mysql.com/downloads/下载 MySQL 社区服务器。截至编写时,可用的最新 MySQL 服务器版本是 8.0.15。将被下载的安装文件命名为 mysql-installer-community-8.0.15.0.msi

  1. 简单双击文件以启动安装。第一个出现的屏幕是许可协议。阅读提到的不同条款和条件。如果您同意这些条款,请点击“我接受许可条款”复选框,然后点击“下一步”按钮。

  2. 下一个对话框将提示您选择设置类型。以下选项将被显示:

  • 开发者默认:它将安装 MySQL 服务器以及其他用于开发应用程序的工具。

  • 仅服务器:它将只安装 MySQL 服务器。

  • 仅客户端:它将安装 MySQL 应用程序和连接器,使客户端机器能够访问 MySQL 数据库表。

  • 完整版:它将安装所有可用的 MySQL 产品。

  • 自定义:系统将提示您选择要安装的 MySQL 产品。

因为我们想开发访问 MySQL 数据库表的程序,我们将选择“开发者默认”选项并点击“下一步”按钮,如下截图所示:

图片

  1. 您将被提示指定 MySQL 服务器文件需要安装的目录。同时,您还需要指定数据目录。对话框默认也会显示这些目录。您可以继续使用默认目录来安装 MySQL 服务器和数据文件,然后点击“下一步”按钮继续。

  2. 简单接受对话框中的默认值并点击“下一步”按钮继续安装过程。您将看到一个对话框,指示将在您的机器上安装的产品。点击“执行”按钮下载并安装显示的产品。

  3. 当产品下载并安装后,您将看到一个对话框,显示您机器上安装的产品列表,如下截图所示。点击“下一步”按钮继续:

图片

  1. 下一个对话框将显示您需要在您的机器上配置以下内容:
  • MySQL 服务器

  • MySQL 路由器

  • 样本和示例

点击“下一步”按钮配置这三个项目,如下截图所示:

图片

  1. 您将被询问是否要将 MySQL 服务器配置为独立服务器或作为沙盒 InnoDB 集群设置。在沙盒 InnoDB 集群设置中,您可以轻松配置和管理至少三个格式为 InnoDB 集群的 MySQL 服务器实例。每个 MySQL 服务器都有在 InnoDB 集群内复制数据的能力。集群足够智能,能够在任何服务器实例发生故障时自动重新配置。但因为我们想要一个单独的独立服务器实例,所以请选择“独立 MySQL 服务器”选项,然后点击“下一步”按钮继续,如图所示:

图片

  1. 您将被提示选择服务器配置类型。您需要选择适合您需求和可用资源的配置类型。以下将显示三个选项:
  • 开发计算机:在此配置类型中,您的机器可以运行 MySQL 服务器以及其他服务器和不同的开发框架。该机器将被用于开发可以访问 MySQL 数据库和表的应用程序。MySQL 服务器将被配置为使用最少的内存。

  • 服务器:在此配置类型中,除了 MySQL 服务器外,还可能运行其他服务器,包括 Web 服务器。MySQL 服务器将被配置为使用中等数量的内存。

  • 专用:在此设置类型中,该机器将仅用于运行 MySQL 服务器。因为没有其他应用程序或服务器将在此机器上运行,所以 MySQL 服务器将被配置为使用大部分可用内存。

因为我们将使用此机器进行开发,也就是说,我们将在此机器上运行 MySQL 服务器以及其他服务器和应用程序,所以我们将选择默认的配置类型,即开发计算机。

  1. 对话框还询问您希望您的应用程序如何连接到 MySQL 服务器。默认情况下,已选择 TCP/IP 网络和端口 3306。此外,默认情况下还选中了一个复选框,该复选框将打开 Windows 防火墙端口以进行网络访问。除此之外,您还将获得两个其他选项,即命名管道和共享内存,用于连接到 MySQL 服务器。

如果您想通过命名管道连接到 MySQL 服务器,则需要启用并定义管道名称。同样,如果您想使用共享内存连接到 MySQL 服务器,则需要启用并定义内存名称。

我们将保留默认的连接选项 TCP/IP,以及端口 3306。点击“下一步”按钮继续,如图所示:

图片

  1. 下一个对话框将提示您选择您想要用于连接到 MySQL 服务器的身份验证方法。以下将显示两个选项:
  • 使用强密码加密进行身份验证(推荐)

  • 使用传统身份验证方法(保留 MySQL 5.x 兼容性)

对于任何无法更新以使用最新的 MySQL 8.0 连接器和驱动程序的应用程序,请选择“使用传统身份验证方法”选项。因此,保留默认的第一个选项“用于身份验证的强密码加密”,然后点击“下一步”按钮继续,如图所示:

图片

  1. 在下一个对话框中,您将被提示输入一个“Root 账户密码”。输入一个包含大小写字母、数字和符号的强密码。在输入当前 Root 密码后,点击其右侧的“检查”按钮以确认输入的密码是否强大。如果您在点击“检查”按钮时得到勾选,这意味着输入的密码完全没问题。此外,请记住密码,因为您将来需要输入它:

图片

  1. 在下一个对话框中,您将被要求将 MySQL 服务器配置为 Windows 服务。如果 MySQL 服务器被配置为 Windows 服务,它将在 Windows 启动和停止时自动启动和停止。默认情况下,将根据您使用的 MySQL 版本显示一个 Windows 服务名称。此外,一个复选框“在系统启动时启动 MySQL 服务器”将被默认选中。我们将坚持默认值。此外,您将被询问是否希望以标准系统账户或自定义用户运行 Windows 服务。标准系统账户将默认自动选中。因为我们希望 MySQL 服务器以标准系统账户的形式作为 Windows 服务运行,所以我们将保留默认选项选中并点击“下一步”按钮继续,如图所示:

图片

  1. 下一个屏幕将询问是否应用您选择的配置设置。点击对话框底部的“执行”按钮以应用我们选择的配置设置。在应用配置步骤后,您将得到以下对话框通知我们 MySQL 服务器已成功配置。点击“完成”按钮继续:

图片

  1. 您将被告知现在需要配置 MySQL 路由器,您将被要求选择“下一步”按钮以继续配置 MySQL 路由器。点击“下一步”按钮以显示 MySQL 路由器配置对话框。路由器用于将大量数据库流量路由到正在运行的 MySQL 服务器,以便高效使用资源,快速且高效地访问数据库表。

  2. 因为我们使用的是独立的 MySQL 服务器安装,所以我们不需要配置 MySQL 路由器,因此,点击“完成”按钮关闭路由器配置对话框。

  3. 下一个对话框将询问您是否想要为 MySQL 服务器配置样本和示例。样本模式可以直接在我们的应用程序中使用,示例有助于我们更好地理解 MySQL。因此,点击“下一步”按钮来配置样本和示例。您将看到一个对话框,提示您选择需要在哪个服务器上创建样本模式和数据的(请参阅以下截图)。因为我们已经安装了一个独立的 MySQL 服务器,所以只会显示一个 MySQL 服务器实例,并且默认情况下也会被选中。

在对话框的底部,您将被提示输入 root 密码以在创建样本和示例之前确认身份验证。输入 root 密码后,点击“检查”按钮以确认身份验证。如果您看到一个带有消息“所有连接均成功”的对勾,这意味着您已正确输入了 root 密码,您可以继续点击“下一步”按钮开始创建样本和示例的流程,如以下截图所示:

截图

  1. 下一个对话框将提示您点击“执行”按钮来运行创建样本模式和相关数据的脚本。点击“执行”按钮开始流程。当脚本执行并且样本和示例在运行的 MySQL 服务器上成功创建和配置后,您将看到一个对话框,如以下截图所示。点击“完成”按钮继续:

截图

  1. 下一个对话框确认配置 MySQL 服务器、MySQL 路由器和样本及示例的任务已完成。因此,点击“下一步”按钮继续。

  2. 最后一个对话框确认 MySQL 服务器及其相关产品的安装已完成(请参阅以下截图)。默认情况下,两个复选框“设置后启动 MySQL Workbench”和“设置后启动 MySQL Shell”将被自动选中。如果您希望在设置完成后启动它们,可以保留这两个复选框选中,或者如果您希望在需要时再调用它们,可以取消选中任一复选框。点击“完成”按钮以完成安装:

截图

现在,您已经准备好使用 MySQL 来实现第八章 使用 MySQL 数据库中的食谱了。

posted @ 2025-10-02 09:35  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报