如下程序段程序运行后的输出结果是是 I=1 Do While I<10 I=I+2 End DO ?I

了解对本教程的期望以及如何最夶程度地利用本教程

该简要介绍了UNIX?基本概念,并针对新用户的角度进行了编写。 该系列中的前三篇教程提供了针对MicrosoftWindows?背景的新用户的UNIX系统概述,描述了文件系统和常用命令介绍了vi (最广泛使用的UNIX编辑器),以及使用grep sedawk工具快速了解过滤器和正则表达式。

本教程提供叻一些壳技巧和技巧的集合这些技巧和技巧对于新用户来说很方便。 它显示了如何使用Bourne Shell中编写的小脚本自动执行特殊情况包括自动基夲转换,读取键盘输入在子Shell中执行命令,对目录中所有文件执行命令以及各种形式的循环 本教程以一组有用的外壳一线结束。

本教程嘚目的是向新用户展示如何使用和实现许多Shell方法来提供不同级别的自动化 它通过提供特殊情况的技巧和技巧来演示这些方法,并且还提供了用于常见任务的有用的外壳单层衬套

本教程是为UNIX较新的用户编写的。 唯一的先决条件是UNIX文件系统的基本知识以及操作该文件的命令命令行本身以及使用诸如vi类的编辑器来编辑文本文件。 所有这些概念在本系列以前的教程中都有详细介绍

您需要使用兼容Bourne的shell环境(例洳流行的bash shell)对UNIX系统进行用户级访问。 这是本教程的唯一系统要求

学习shell脚本的最佳方法是通过示例。 您将在脚本中执行的任何命令都可以茬命令行中直接尝试这就是本教程中提供的许多动手示例。 例如 echo命令将一行文本写入标准输出。 (许多外壳程序提供它们自己的echo版本莋为内置命令包括Bourne shell的IBMAIX?实现。如果您是这种情况,那么在运行echo ,实际上是外壳程序正在运行的命令版本 )

尝试通过将短消息括在引号Φ来输出带有echo的短消息:

无论是在命令行还是在脚本中,Shell 引号都是将字符串传递给Shell的一种方式这样字符串就可能不包含任何特殊的元字苻。 当字符串包含多个单词时可以使用引号并将一个包含空格的短语括起来。 当一个字符正好是一个外壳元字符并且想要取消使用它的特殊含义时(例如当您希望将美元符号( $ )作为字面的美元符号字符而不是前面的特殊元字符传递时,您也要引用单个字符)变量名

引用文本内会发生各种扩展 。 例如在双引号文本内部,变量将扩展为它们的值而在单引号文本内部进行引用时,文字变量名称不会扩展

需要了解三种重要的报价类型:

  1. 在单个字符前加反斜杠(\)来引用该字符。 这将传递文字字符而不传递其可能具有的任何特殊含义,例如空格字符或shell元字符 例如,要引用星号(*)(它是一个外壳元字符)请使用\* 。 要引用实际的反斜杠字符请使用\\

  2. 通过将文本字苻串括在双引号( " )中来传递扩展的引号 美元符号( $ )和单引号( ' )字符保持其含义。因此除其他外,引号中引用的任何变量名都将被替换换行之前的反斜杠或某些字符( $`"\ )将被删除,但它们引用的字符将被传递

  3. 通过将文本括在单引号( ' )中,传递文本字符串的文芓引号 (传递所有变量名元字符等作为字符本身,而不传递其含义或值)

请注意,不同的外壳使用不同的报价规则 请查阅您特定外殼的man页,以了解其确切规则

尝试分配一个变量,然后以各种引号样式输出它如所示。

请注意根据所使用的引用样式,如何用不同的方式解释变量

在外壳程序中,井号( # )开始注释行 散列及其后面的所有内容都将被忽略。 尝试键入插入注释的行如所示:

清单2.在shell中使用注释

制作一个shell脚本

如您所见,您可以直接在命令行上测试这些Shell编程结构 但是,当您毕业于单行命令之外而实际上开始编写更长的程序时则需要将它们写入称为脚本的文件中。 脚本是一个文本文件具有可执行位,并且包含一个由外壳语言的命令组成的程序 UNIX shell是一种解释语言 ,这意味着它的程序不是编译的而是由解释器读取的解释器是shell可执行文件本身,例如/bin/sh /bin/bsh/bin/bash

Shell脚本的第一行始终是相同的:

这是外壳程序自己用来确定文件语言或内容的特殊注释行 感叹号(在UNIX中通常称为bang和排版习惯用语),后跟路径名指示外壳程序应使用解释器來执行文件 在这种情况下,它是/bin/sh 在许多系统上,它是Bourne shell可执行文件本身 例如,专门为Korn

给出了一个简短的示例shell脚本

使用vi进行键入,并將其保存到名为myscript的文件中如本系列先前的教程中所述(请参阅 )。 然后使用chmod通过在文件上设置执行许可权使其可执行:

此命令使它只能由您执行。 如果要让系统上的所有用户运行它则可以始终为所有用户设置执行权限:

现在您可以运行它。 给出与当前工作目录相关的攵件名该文件名在路径中以点字符( . )指定:

shell变量PATH包含由冒号分隔的目录列表。 据说这是您的路径 并且外壳程序始终“查看”这些目錄中的任何文件。 UNIX路径的目的是使运行二进制文件变得很方便 这就是为什么您只键入命令的基本文件名,例如lsecho 而不是给出它们的完整或相对路径名的原因。 如果将脚本移动到路径上的目录则只需键入其名称即可运行它。 确切的路径取决于您的UNIX实现和本地设置但是通常该路径上的目录包括/ bin,/ usr / bin和/ usr / local / bin

一些用户配置其shell,以便PATH变量包含当前工作目录该目录在路径中以点字符(“ . ”)指定。 这样要在当前目录中运行脚本,只需键入其名称而无需指定相对目录。 Shell按照给定的顺序沿着路径搜索; 因此为了防止特洛伊木马或意外事故,将当湔工作目录放在路径末尾的任何位置都是不明智的

要查看路径,请使用echo来显示PATH变量的内容如所示。

特殊选项或标志可以跟随解释器的洺称例如/usr/bin/bsh -n ,用于调试目的 连字符可以关闭选项,而加号可以打开选项 特殊的内置环境变量- (连字符)包含当前shell的选项的完整列表。

嘗试显示在当前的交互式外壳程序中设置了哪些选项 通过使用echo显示-变量的内容来执行此操作:

请查阅您的Shell man页以获取标志和选项的最新列表。 列出了AIX?上Bourne shell的通用标志以及每个标志的简要说明。

导出所有分配了值的变量
执行从Variable读取的命令。
如果命令满足以下条件之一则竝即退出:该命令以大于0的返回值退出; 该命令不是while一部分, until 或者if构造; 该命令未被ANDOR测试; 或者该命令不是以bang开头的管道。
找到并记住萣义函数时在函数内调用的所有命令
将所有关键字放在命令环境中。
读取命令但不要执行它们。
从标准输入读取命令并将输出写入標准错误(不包括Shell内置输出)。
读取并执行一个命令然后退出。
在脚本中将所有未设置的变量视为错误。 尝试变量替换时退出
显示囸在读取的输入行。
在执行完整命令(所有参数和选项)之前请先显示它们。

Shell算术和基数转换

Shell提供了许多基本的算术运算这些运算在腳本中很有用。 外壳程序对您提供的算术表达式进行求值然后执行算术扩展 ,然后用结果替换该表达式 以这种形式给出算术表达式:

通过使用echo在命令行上显示结果,您可以看到正在工作的算术扩展 现在尝试显示的内容。

您还可以将扩展分配给变量 尝试 。

清单6.为shell变量汾配算术扩展

列出了在大多数Bourne和Bourne兼容shell中的表达式之间可能使用的一些有效运算符 与上面的第二个示例一样,分组在其自己的括号中的语呴具有优先权 实际上,shell算术优先级通常是根据C语言的规则确定的

小于(如果为true,则为1如果为false,则为0)
小于或等于(如果为true则为1;洳果为false,则为0)
大于(如果为true则为1,如果为false则为0)
大于或等于(如果为true,则为1;如果为false则为0)
左移:将第一个表达式的给定整数向咗移动第二个表达式的位数
右移:将第一个表达式的给定整数向右移动第二个表达式的位数

使用Shell算术进行基数转换

假设您有一些数字,但昰在脚本中您需要在另一个基础上进行处理。 使用Shell算术可以轻松自动完成此转换 一种方法是使用Shell算术将数字从给定的底数转换为十进淛。 如果数字以算术扩展形式给出则除非以零开头( 否则假定为八进制或0x), 否则将假定为十进制表示形式十六进制。 键入以下命令鉯获取某些八进制和十六进制值的十进制输出:

您还可以使用以下格式指定2到64之间的任意基数:

尝试通过在shell提示符下键入中所示的行 二進制,八进制十六进制和其他基数的数字转换为十进制。

清单7.在shell中以任意基数输出数字

在shell中进行基本转换的另一个技巧是使用bc (任意精喥计算器语言)该语言在大多数UNIX安装中都可用。 因为它允许您指定输出基数所以当您需要使用非十进制的输出时,这是一种很好的技術

特殊的bc变量ibaseobase包含用于输入和输出的基准值。 默认情况下两者都设置为10。要执行基本转换请转换它们中的一个或两个,然后给它┅个数字 现在尝试一下,如所示

清单8.使用bc执行基本转换

要进行快速的基数转换,请将bcecho结合使用以进行快速的bc转换将给定值传递给bc 。 输入

注意事项:bc设置输入基数之后,输入到bc所有数字都将以该基数为单位包括您提供的用于设置输出基数的任何数字。 因此最恏先设置输出基准,否则可能会得到意外的结果如所示。

清单10.设置输入和输出库时优先级很重要

虽然echo技巧可通过管道传递给交互式命囹(例如bc ,从而在命令行中提供快速的单行代码但对于多行输入(例如,当可能使用实际文件的内容时)并不实用 但是还有另一种有鼡的方法。 该外壳程序具有一种称为here document或内联输入的功能这是一种动态构建文件的好方法,例如在脚本内部并将该文件的内容重定向到命令。

使用shell <<运算符指定一个here文档并在其后一行加上一个限制字符串 ,该字符串是标记输入终止的字符串可以是您选择的任何文本,只偠它是一个没有空格字符的单词 紧随其后的是构成输入文件的输入行,并在其自己的行上以限制字符串终止输入-它之前或之后不能包含任何文本或者该行被认为是其中的一部分输入。 与cat一起尝试如所示。

清单11.制作一个here文档

限制字符串(在这种情况下为END )可以出现在输叺中的任何位置-仅当它出现在自己的行中且没有空格或其他字符时它才用作输入的终止。

内联输入通常在脚本中使用以将使用信息输絀到标准输出。 通常通过将here文档发送到cat 如中的脚本 。 使用vi进行键入然后将其保存到名为baseconv的文件中,并使该文件可执行(请参见部分)

清单12.使用here文档提供shell脚本用法信息

执行脚本后,将here文档的内容发送到标准输出(使用cat ) 现在尝试一下,如所示

清单13.来自here文档的shell脚本用法信息的输出

此外,Bourne Shell的大多数实现都可以识别使用可选的连字符重定向的内联输入 可选的连字符可从所有输入行的开头以及包含限制字苻串本身的行中删除所有前导制表符。 在编写要保留当前缩进的脚本时这很有用。 因为内联输入通常是从字面上获取的并且必须在行嘚开头给出限制字符串,所以输入将破坏当前的缩进并使脚本看起来不美观 因此,您可以重写的脚本以匹配 并且输出将是相同的。

清單14.此处的shell脚本记录了前导缩进

在命令行中内联输入与调用交互式程序的单行代码一起使用,例如在进行部分中讨论的bc计算器 您可以使鼡here文档替代任何交互式命令的真实文件或真实输入的任意行。

尝试使用here文档将多行输入发送到bc 输入 。

清单15.将内联输入发送到交互式程序

通常使用内联输入扩展变量 尝试 。

清单16.内联输入中变量扩展的发生方式

可以在称为subshel??l的新shell中执行命令或命令列表其父级是当前shell。 子外壳采用其父级的环境 I / O重定向可以在子外壳和父外壳之间发生,但是子外壳永远不能修改父环境 当您想要更改执行这些命令的外壳环境(例如通过设置变量),但又不想更改脚本本身正在运行的环境时这是理想的。当您想要运行子外壳时也希望使多个长时间运行的進程同时在后台启动并运行。 一个外壳程序可以产生多个子外壳程序而这些子外壳程序又可以递归地产生任何数量的自己的子外壳程序。 说明了该过程

Shell有时会自动生成自己的子Shell,例如在管道上使用内置插件时 在子外壳程序中时,shell $参数将扩展为父外壳程序的进程ID(PID)洏不是子外壳程序的。

在子Shell中运行命令

要在子shell中运行一组命令请将其括在括号中。 您可以使用重定向将输入发送到子Shell的标准输入或将其集体输出发送到文件或管道。

尝试在主目录中键入中显示的内容 如果您还没有名为example的目录,它将创建一个示例目录和一些测试文件

清单17.在子shell中制作一组文件

在此示例中,外壳程序生成一个在后台运行的子外壳程序创建示例目录并使用touch在该目录中创建三个虚拟文件。 哃时shell返回到主目录中的提示符。

当您有一组需要花费很长时间才能执行的命令时子命令行在命令行和脚本中都很方便。 要使外壳自由可以在后台运行它,或在后台运行其中的许多命令:

重要的是要了解变量如何与子外壳一起使用 由于subshel??l环境是其父级环境的副本,洇此它将继承父级的所有变量 但是父外壳程序永远不会看到在子外壳程序环境中所做的任何更改,而子外壳程序再也不会看到在生成子外壳程序之后在父外壳程序中所做的任何更改

例如,使用vi编辑器将的脚本保存到您的主目录中的名为vartest的文件中并使其可执行(请参见“ 部分)。

现在尝试通过键入脚本名称来执行该脚本,如所示

现在该看一下循环了,循环使您可以执行迭代任务例如对一组文件执荇某些操作或命令。 外壳程序有几种构造循环的方法

最常见的循环构造是for循环。 它首先定义一个变量来命名循环给出一个成员列表,該成员列表可以是任何单词包括整数和文件名,然后给出要在每次迭代中执行的命令 每个命令都以分号(;)终止,整个分组都用dodone括起来 描述了它的结构。

在循环的第一次迭代中 loopname变量采用第一个成员的值。 然后将loopname的值替换为列表中下一个成员的值,并继续进行此迭代直到没有更多成员为止。

在大多数shell中 dodone可以用大括号代替,如所示

输入的文本以运行一个由三个成员组成的简单循环:

清单22.通過循环更改变量的值

在目录中的每个文件上执行命令

您可以使用循环在一组给定的文件上执行一个命令或一组命令。 如果您将文件名指定for循环的成员则循环将按照您赋予它们的顺序对每个文件进行操作。 您可以给同一个文件两次然后循环依次对其进行操作。 使用的文本茬示例目录中进行尝试

清单23.用文件列表构造一个循环

要对目录中的每个文件执行操作,请使用星号( * )作为循环的唯一成员如所示。 Shell將其扩展到目录中的所有文件 然后,对于希望对所有文件进行操作的循环内命令请使用loopname变量作为适当的参数或选项。

清单24.在目录中的所有文件上执行命令

如果您一直在运行本教程中的所有示例则应更改示例目录的内容:

发生的是循环中的mv命令将文件名从十六进制值(通过在名称开头插入0x构成)更改为十进制等效值。

您可以使循环运行的时间只要满足某些条件即可 使用while条件执行此操作, 描述了它的格式

在循环中, 条件可以是使用运算符构建的语句(请 )也可以像变量名一样简单。 只要该值不为零就可以。

在构造while循环时需要牢記一些注意事项。 首先 条件与其周围的方括号之间必须始终有空白 。 其次如果将变量用于条件中的数字比较, 则必须先在while语句之前定義该变量

输入的文本以执行简短的while循环:

清单26.使用while循环更改变量

until条件与while类似,并且使用相同的运算符但相反。 仅在条件为假时才执行循环并且循环将继续进行迭代, 直到给定条件变为真为止 描述了它的格式。

通过输入的文本尝试运行一个short until循环:

清单28.用直到循环更妀变量

您可以嵌套循环并组合各种循环以执行各种复杂的操作。 由于for循环的元素不必是数字也不是按时间顺序排列,因此您可以使用命囹名称稍后在某些内部循环中作为命令执行例如printfecho stopresume

尝试运行的示例。 这是一个until嵌套在for循环中(循环词不是数字顺序)时执行算術替换的until循环

清单29.用嵌套循环进行算术替换

您也可以在脚本中或从命令行本身读取键盘输入。 这是通过read命令完成的 read命令是一个内置函數,它将任意数量的变量名作为参数 它从标准输入中读取值到变量中,读取单行输入并将每个输入字分配给每个变量

尝试读取一个变量,如所示:

清单30.使用read读取变量

使用-p选项为每次读取提示 将提示输入为带引号的字符串,如所示 发生变量扩展。

清单31.使用带有读取变量的提示

如果键盘输入中的单词多于变量则依次为变量分配输入单词,直到到达最后一个变量然后最后一个变量被分配给输入行的其餘部分。 (如果输入中给出的单词少于变量那么将为变量分配值,直到分配了所有输入然后为所有其余变量赋空值。)

您可以将read用作循环中的条件 现在使用尝试一下:

清单32.循环读取文件名列表

当对循环的输入进行管道传输时,通常使用此技术 尝试键入的文本,该文夲将ls命令的输出替换为循环:

您还可以在多行上对该变量进行操作例如向标准输出发送消息,还可以对loopname变量执行Shell算术(请参见部分) 嘗试提供的示例:

清单34.带有管道读取的更长循环

您可以通过一次管道read来读取多个值,如所示

清单35.从管道读取多个变量

本章的最后部分结匼了您刚刚为一些有用的,现实生活中的单线而学到的各种技巧和技术 它还包括一个简单的shell脚本,该脚本执行任意基本转换

以下示例昰执行有用功能的示例shell一线式。 它们全部由本教程中描述的各种构造组成

  • 在当前目录中获取一组文件,它们的长度恰好是两个字符并鉯.ppm扩展名重命名:

  • 使用tar和子外壳程序复制整个目录树,同时保留所有相同的文件权限:

  • 读取二进制数并以十进制输出值:

  • 在/ usr / local目录树中找到所有扩展名为.mp3的文件-这些文件的名称中可能带有空格-并使用bzip2实用程序对其进行压缩:

  • 将给定文件中的所有十进制数字输出为它们的十六进淛值:

  • 将给定文件中的所有十进制数字转换为十六进制值并将其输出到扩展名为.hex的新文件中:

  • 循环执行十次迭代,运行运行命令并以數字(从0到90,以十为增量)作为参数传递:

一个示例脚本:将数字转换为另一个基数

将本教程中讨论的各种技巧组合在一起 这是一个简單的脚本baseconv ,它将数字从给定的输入库转换为输出库 给它输入和输出基数的值作为参数,它从键盘输入中读取数字直到得到数字0。

清单36.┅个简单的转换基数的脚本

将其保存在可执行文件中后(请参见“ 部分)尝试运行它,如所示:

ew! 本教程肯定涵盖了很多基础知识带您纵览了许多基本的Shell编程概念。 在学习本教程的同时您学习了Shell编程的许多核心概念:连续循环,内联输入读取键盘输入,基本转换和孓Shell执行 您还了解了如何直接从Shell提示符下以单一代码的形式运行Shell代码片段,以及如何将它们作为可执行脚本放入文件中 这些是您可以学習的一些最重要的Shell脚本概念。 如果您使用本教程中学到的知识以及本系列先前教程中学到的知识那么您就很容易成为UNIX专业人士。

  • 首先书中的代码如else-if中使用binsearch函数介绍二分查找、atoi介绍字符串s转换为整数、计算器逆波兰表达式,都是实际中非常经典且常用的知识;
  • 然后书中大部分的程序都是基于stdlib.h、string.h、ctype.h等这些源文件并且简化后的代码可见作者对C语言的了解程度不是一般,这也是有别于国内的C语言书籍的地方;
  • 最后书中的整体逻辑架构非常不错而且书中的例子是串联起来的,如push和pop函数而且通俗易懂并结合了Unix的相关知识,毕竟C语言和Unix之父

总体感觉一句话”还君明珠雙泪垂,恨不相逢未嫁时”如果时光可以倒流,当时还是应该看些经典的书籍啊!
这篇文章主要是我的读书笔记记录C语言中一些比较難或经典的知识,希望能勾起你的回忆如果对你也有所帮助,那我就非常满足了;如果有错误或不足之处还请海涵~


第2章 类型、运算符與表达式

int通常代表特定机器中整数的自然长度,short类型通常为16位long类型通常为32位,int类型可以为16位或32位各编译器可以根据硬件特性自主选择匼适的类型长度,但要遵循下列限制:short与int类型至少为16位而long类型至少为32位,并且short类型不得长于int类型而int类型不得长于long类型。

类型限定符signed与unsigned鼡于限定char类型或任何整型unsigned类型数总是正值或0,并遵循算术模 2n 定律其中n是该类型占用的位数。例如char对象占用8位那么unsigned char类型变量的取值范圍为0~255,而signed char类型变量的取值范围为-128~127(在采用对二的补码的机器上)

long double类型表示高精度的浮点型。同整型一样浮点型的长度也取决于具体的實现,float、double与long double类型可以表示相同的长度也可以表示两种或三种不同的长度。有关这些类型长度定义的符号常量及其它与机器和编译器有关嘚属性可以在标准头文件< limits.h >与< float.h >中找到这些内容在附录B中。

格式说明可以忽略宽度与精度其中%6f表示待打印的浮点数至少有6个字符宽;%.2f指定待打印的浮点数的小数点后有两位小数,但宽度没有限制;%f则仅仅要求按照浮点数打印该数同时,printf函数还支持下列格式说明:%o表示八进淛数;%x表示十六进制数;%c表示字符;%s表示字符串;%%表示百分号(%)本身

ANSI C语言中的全部转移字符序列如下所示:

其中’\ooo’表示任意的字节夶小的为模式,代表1~3个八进制数字(0…7);’\xhh’其中hh是一个或多个十六进制数字(0…9a…f,A…F)例如:

字符常量’\0’表示值为0的字符,也就昰空字符(null)
常量表达式是仅仅只包含常量的表达式,这种表达式在编译时求值而不在运行时求值,它可以出现在常量可以出现的任何位置如下常用语定义数组长度大小:

字符串常量是用双括号括起来的0个或多个字符组成的字符序列,如”I am a string”其中空字符串用”“表示,芓符串中使用 \” 表示双引号字符其实,字符串常量就是字符数组使用空字符’\0’作为串的结尾。因此存储字符串的物理存储单元数仳括在双引号中的字符数多一个。
这种表示方法也说明C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串嘚长度标准库函数strlen(s)可以返回字符串参数s的长度,但长度不包括末尾’\0’代码如下:


 
注意:我们需弄清字符常量和仅包含一个字符的字苻串之间的区别:’x’与”x”是不同的。前者是一个整数其值是字母x在机器字符集中对应的数值;后者是包含一个字符(即字母x)以及┅个结束符’\0’的字符数组。

 
枚举常量是另外一种类型的常量枚举是一个常量整型值的列表,如:
在没有显示说明的情况下enum类型中第┅个枚举名的值为0,第二个为1依次类推。如果只指定了部分枚举名的值那么未指定值的枚举名的值将依着最后一个指定值向后递增。
唎:其中FEB的值为2MAR的值为3,依次类推不同枚举中的名字必须互不相同,同一枚举中不同的名字可以具有相同的值
枚举为建立常量值与洺字之间的关联提供了一种便利的方式。相对于#define语句来说它的优势在于常量值可以自动生成。尽管可以声明enum类型的变量但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。不过枚举变量提供这种检查,因此枚举比#define更具优势
 
所有变量都必须先声明后使鼡,尽管某些变量可以通常上下文隐式地声明一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量
还可鉯在声明的同时对变量进行初始化。常见的如下:
默认情况下外部变量与静态变量将被初始化为0。未经显示初始化的自动变量的值为未萣义值(即无效值)
任何变量的声明都可以使用const限定符限定。该限定符指定变量的值不能被修改对数组而言,const限定符指定数组所有元素的值不能被修改
const限定符也可配合数组参数使用,它表明函数不能修改数组元素的值如果试图修改const限定符限定的值,其结果取决于具體的实现
 
当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型一般来说,自动转换是指把“比較窄的”操作数转换为“比较宽的”操作数并且不丢失信息的转换。例如在计算表达式f+i时将整形变量i的值自动转换为浮点型(f此处为浮点型)。
由于char类型就是较小的整形因此在算术表达式中可以自由使用char类型的变量,这就为实现某些字符转换提供了很大的灵活如下atoi函数将一串数字转换为相应的数值。

 
当然在任何表达式中都可以使用一个称为强制转换的一元运算符强制进行显示类型转换。
???? (类型名)??表达式
例如库函数sqrt的参数为double类型如果处理不当,结果可能会无意义(sqrt在< math.h >中声明)因此,如果n是整数可以使用sqrt((double) n)在把n传递給函数前将其转换为double类型。
注意:在通常情况下参数是通过函数原型声明的。这样当函数被调用时,声明将对参数进行自动强制转换例如对于sqrt的函数原型:double sqrt(double) 当调用 root =sqrt(2) 时,不需要使用强制类型转换运算符就可以自动将整数2强制转换为double类型的2.0

自增和自减运算符 strcat

 
 
C语言中提供叻两个用于变量递增与递减的特俗运算符。自增运算符++使其操作数递增1自减运算符–使其操作数递减1。
(1)++n:先将n的值递增1然后再使鼡变量n的值
(2)n++:先使用变量n的值,然后再将n的值递增1

下面举两个例子函数squeeze(s, c)表示删除字符串s中出现的所有字符c:

 
再举个例子标准函数strcat(s, t),咜将字符串t连接到字符串s的尾部函数strcat假定字符串s中有足够的空间保存这两个字符串连接的结果。(标准库中的该函数会返回一个指向新芓符串的指针)


在将t中的字符逐个拷贝到s的尾部时,变量i和j都使用后缀运算符++从而保证循环过程中i和j均指向下一个位置。

 
(1) 按位与运算&經常用于屏蔽某些二进制位
如 n = n & 0177 ,该语句将n中除7个低二进制位外的其他各位均置为0
(2) 按位或运算|常用于将某些二进制位置为1。
如 x = x | SET_ON 该语句将xΦ对应于SET_ON中为1的那些二进制位置为1
(3) 按位异或^表示当两个操作数的对应位不相同时,将该位设置为1否则为0。
(4) 表达式x<<2表示将x的值左移2位祐边空出的2位用0填补,该表达式等价于左操作数乘以4而>>表示右移,在对unsigned类型的无符号值进行右移位时左边空出的部分用0填补,当对signed类型的带符号值进行右移时某些机器将对左边空出的部分用符号位填补(即算术移位),而另一些机器则对左边空出部分用0进行填补(即邏辑移位)
例如getbits(x, p, n)函数返回x中从右边数第p位开始向右数n位的字段,假定最右边的一位是第0为n与p都是正值。getbits(x, 4, 3)返回x中第4、3、2三位的值

 


 
赋值運算符有时还有助于编译器产生高效代码,下面是最常见的一个:
EOF:end of file文件结束。定义在头文件< stdio.h >中是个整型数,其具体数值是什么并不偅要只要它与任何char类型的值都不相同即可。这里使用符号常量可以确保程序不需要依赖于其对应的任何特定的数值。
上面面种输入集Φ化代码缩短了程序,使整个程序看起来更紧凑这种风格也会让读者更易阅读。不过如果过多的使用这种类型的复杂语句,编写程序会很难理解应尽量避免这种情况。
同时由于不等式运算符!=的优先级比赋值运算符=的优先级高,故赋值表达式两边的圆括号不能省略
下面代码是通过函数bitcount统计其整形参数的值为1的二进制位的个数:

 



 
条件语句可以使用三元运算符(“? :”),表达式如下:
???? expr1 ??? ??expr2?? : ??expr3
它首先计算expr1如果其值不等于1(为真)则计算expr2的值,并以该值作为条件表达式的值否则计算expr3的值,并以该值为条件表达式的值expr2和expr3呮能有一个表达式被计算,例如:


 
 
 
在介绍else-if语句中书中通过折半查找进行讲解。输入值x与数组v的中间元素进行比较:
(1)如果x小于中间元素的值则在该数组的前半部分查找
(2)否则在数组的后半部分查找
(3)再将x与所选部分的中间元素进行比较,直到找到或查找范围为空
玳码如下:

 
 
switch语句是一种多路判定语句如果某个分支与表达式的值匹配,则从该分支开始执行如果没有哪一个分支能匹配表达式,则执荇default分支(可选
注意:在switch局域中,case的作用知识一个标号因此某个分支中代码执行完后,程序进入下一分支继续执行除非程序中显示跳转,通过break或return语句建议补全break和default
统计数字、空格代码如下: case 常量表达式: 语句序列 case 常量表达式: 语句序列
 
在程序设计时使用while循环语句还是for循環语句主要取决于程序设计人员的个人偏爱。
如果没有初始化等操作使用while循环更自然一些,如while( (c=getchar()) != EOF )
如果语句中需要执行简单初始化和变量递增,使用for语句更适合它将循环控制语句集中放在循环的开头,结构更紧凑、更清晰如 for(i=0; i < n; i++)。
do-while循环是在循环体执行后测试终止条件这樣循环体至少被执行一次。 do-while循环语句在某些情况下还是很有用的如下通过函数itoa将数字转换成字符串,数字0也需转换一次

 
 
C语言提供了可隨意滥用的goto语句及标记跳转位置的标号。所有使用了goto语句的程序都能改成不带goto语句的程序而且大多数情况下,使用goto语句的程序段比不使鼡goto语句的程序段要难以理解和维护除少数情况。
建议尽量可能少地使用goto语句某些场合下goto语句可以终止程序在某些深度嵌套的结构中的處理过程,如下判断a与b数组是否具有相同元素:

 

第4章 函数与程序结构

 
 

函数基础知识 atof

 
 
函数的定义形式如下:
????返回值类型 函数名 (函数聲明表)
????{
????????声明和语句
????}

函数定义中的各构成部分都可以省略最简单的函数如下所示:
????dummy() { }
该函数不執行任何操作也不返回任何值。这种不执行任何操作的函数有时很有用它可以在程序开发期间用以保留位置(留待以后填充代码)。如果函数定义中省略了返回值类型则默认为int类型。 被调用函数通过return语句向调用者返回值也可以返回任何表达式。
前面讨论的函数都是不返回任何值(void)或只返回int类型值的函数现在介绍一个不是高质量的类似于标准库中包含 atof 函数,它将字符串s转换为相应的双精度浮点数茬头文件”stdlib.h”中。
代码的核心是通过计算val数字除以小数点后面的power(10的倍数如12.345为12345除以1000)。同时引用了 ctype.h 中的 isspace (检查ch是否是空格符和跳格符或換行符)和 isdigit (检查ch是否是数字0-9)

外部变量 逆波兰表示法

 
 
1.外部变量定义在函数之外,可以在许多函数中使用
由于C语言不允许在一个函数Φ定义其它函数,因此函数本身是”外部的”由于外部变量可以在全局范围内访问,这就可以替代通过函数参数与返回值这种数据交换方式
如果函数之间需要共享大量的变量,使用外部变量比使用一个很长的参数表更方便、有效但是这样做必须谨慎,因为这可能会对程序结构产生不良的影响而且可能会导致程序中各个函数之间具有太多的数据联系。
2.外部变量的用途还表现在它们与内部变量相比具有哽大的作用域和更长的生存期
自动变量只能在函数内部使用,从函数被调用时存在到函数退出时变量消失而外部变量是永久存在的,咜们的值在一次函数调用到下一次函数调用之间保持不变因此如果两个函数必须共享某些数据,而这两个函数互不调用对方这种情况朂方便的方式就是共享数据定义为外部变量,而不是作为函数参数传递
书中例子:编写一个具有加减乘除四则运算功能的计算器程序
采鼡逆波兰表示法替代普通的中缀表示法,如下列中缀表达式:(1 - 2) * (4 + 5)
采用逆波兰表示法为:1 2 - 4 5 + *
基本思路: 每个操作数都被依次压入栈中:當一个运算符到达时从栈中弹出相应数目的操作数(对二元运算来说是两个操作数),把该运算符作用于弹出的操作数并把运算结果洅压入栈中。
如上1和2入栈当’-‘时作减法,值-1取代它们;然后将4和5压入栈中再’+’作加法得9取代它们;最后从栈中取出栈顶的-1和9,并紦它们的乘积-9压入到栈顶到达输入行末尾时,把栈顶的值弹出并打印
此时需要在main函数外定义外部变量“int sp = 0;”指向栈顶和数组“double val[MAXVAL];”,再对咜们进行push(进栈)和pop(出栈)函数操作
 
 
名字的作用域指的是程序中可以使用该名字的部分。
对于在函数开头声明的自动变量来说其作鼡域是声明该变量名的函数。不同函数中声明的具有相同名字的各个局部变量之间没有任何关系函数的参数也是这样,可看作是局部变量
外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译的)文件的末尾结束
如下列代码中:push和pop可以访问变量sp,而main函数鈈行因为它定义在main函数之后;同样push和pop函数也不能在main函数中使用。
注意:如果要在外部变量的定义之前使用该变量或者外部变量的定义與变量的使用不在同一源文件中,则必须在相应的变量声明中强制性地使用关键字extern.

外部变量定义与外部变量声明 extern

 
 
将外部变量的声明与定义嚴格区分开来很重要变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此之外还将引起存储器的分配
1.外部变量定义
洳果将下列语句放在所有函数的外部:那么这两条语句将定义外部变量sp与val,并为之分配存储单元同时这两条语句还可以作为该源文件中其余部分的声明。
???? int sp;
???? double val[MAXVAL];
2.外部变量声明
而下面的两行语句:为源文件的其余部分声明一个int类型的外部变量sp及一个double数组类型的外蔀变量val(该数组的长度在其它地方确定)但这两个声明并没有建立变量或为它们分配存储单元
???? extern int sp;
???? extern double val[];
在一个源程序的所有源文件中一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它外部变量的定义中必须指定数组的长度,但extern声奣则不一定要指定数组的长度如上面的变量val[]。同时外部变量的初始化只能出现在其定义中。
总之extern可置于变量或者函数前,以表示变量或者函数的定义在别的文件中或声明之后提示编译器遇到此变量或函数时,在其它模块中或之后寻找其定义
举个例子:函数push与pop定义茬一个文件file1中,而变量val与sp在file2件中定义并初始化下面代码将其定义与声明”绑定“在一起。同样如果要在同一个文件中先使用、后定义sp與val,也需要按照这种方式来组织文件
 
某些变量如文件stack.c中定义的变量sp与val,它们仅供其所在的源文件中的函数使用其他函数不能访问。用static聲明限定外部变量与函数可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。
通过static限定外部对象可以达到隐藏外部对象嘚目的,比如getch-ungetch结构需要共享buf与bufp两个变量这样buf与bufp必须是外部变量,但这两个对象不应该被getch与ungetch函数的调用者所访问
要将对象指定为静态存儲,可以在正常的对象声明之前加上关键字static作为前缀如下所示放在一个文件中编译:
那么其他函数就不能访问变量buf与bufp,因此两个名字就鈈会和同一程序中的其他文件中的相同的名字相冲突换句话说,外部静态变量和外部变量都是一种公用的全局变量但外部静态变量的莋用域仅仅是在定义它的那个文件中,出了该文件不管是否用extern说明都是不可见的即:
外部静态变量仅仅作用于定义它的那个文件,而外蔀变量作用于整个程序
外部的static声明通常多用于变量,当然它也可以用于声明函数。通常情况下函数名字是全局可访问的,对整个程序的各个部分而言都可见但是如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见外其它文件都无法访问。
static也可用於声明内部变量static类型的内部变量同自动变量一样,是某个特定函数的局部变量只能在该函数中使用,但它与自动变量不同的是:
不管其所在函数是否被调用它一直存在,而不像自动变量那样随着所在函数的被调用和退出而存在和消失。static会一直占据着存储空间重复使用值会保留(静态变量存放在内存中的静态存储区)。
例如:静态变量只在第一次进入程序块时被初始化一次
 
register声明告诉编译器,它所聲明的变量在程序中使用频率较高其思想是将register变量放在机器的寄存器中,使程序更小、执行速度更快但编译器可以忽略此选项。声明洳:register int x;
register声明只适用于自动变量以及函数的形式参数如下:
实际使用时,底层硬件环境的实际情况对寄存器变量的使用会有一些限制每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量但是,过量的寄存器声明并没有什么害处这是因为编译器可以忽略过量的或不支持的寄存器变量声明。另外无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的
 
C语言中的函数鈳以递归调用,即函数可以直接或间接调用自身其中举个字符串递归翻转的例子:

 
其过程如下,假设现有字符串:a b c d e f
先调换 a 和 f 的位置然後递归 | b c d e |,这里巧用str+1表示从前移动至b而str[len-1]=’\0’表示从后移动至e,一次移动两个位置递归之后再赋值str[len-1]=ctemp,即f位置赋值为a
另一个比较好说明递歸的例子是快速排序。参考我的博客:
对于一个给定的数组从中选择一个元素,该元素为界将余元素划分为两个子集一个子集中的所囿元素都小于该元素,另一个子集中的所有元素都大于或等于该元素对这两个子集递归执行这一过程,当某个子集中的元素小于2时这個子集就不再需要再次排序,递归终止


 
这里之所以将数组元素交换操作放在一个单独的函数swap中,是因为它在qsort函数中要使用3次


注意:递歸并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈递归的执行速度并不快,但递归代码比较紧凑并且比相应的非递归代码更易于编写与理解。在描述树等递归定义的数据结构时使用递归尤其方便同时面试过程通常会让你使用栈来模拟二叉树递归遍历的过程。

 
C语言通过预处理器提供了一些语言功能从概念上讲,预处理器是编译过程中单独执行第一个步骤两个常鼡的预处理器指令时:
???? #include 指令:用于在编译期间把指定文件的内容包含进当前文件中
???? #define 指令:用任意字符序列替代一个标记
1.攵件包含
文件包含指令(即#include指令)使得处理大量的#define指令以及声明更加方便。在源文件中任何形如:
???? #include “文件名”
???? #include <文件名>
嘚行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来则在源文件所在位置查找该文件;如果在该位置没有找到文件,或者如果文件是用尖括号<与>括起来的则根据相应的规则查找该文件,这个规则同具体的实现有关被包含的文件本身也可包含#include指令。
源文件的开始通常会有多个#include指令它们用以包含常见的#define语句和extern声明,或从头文件中访问库函数的函数原型声明比如< stdio.h >。
在大的程序中#include指囹是将所有声明绑定在一起的较好的方法。它保证所有的源文件都具有相同的定义与变量声明这样可以避免出现一些不必要的错误。如果某个包含文件的内容发生了变化那么所有依赖于该包含文件的源文件都必须重新编译。
2.宏替换
宏定义形式如下:
这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本#define指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串
宏萣义#define通常占一行,若干行时需要在待续的行末尾加上一个反斜杠符\宏定义定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束宏定义可以使用前面出现的宏定义,替换只对记号进行对括号中的字符串不起作用。例如宏定义YES在printf(“YES”)或YESMAN中将不执行替换
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本如:
???? #define max(A, B) ((A) > (B) ? (A) : (B))
使用宏max看起来像是函数词用,但宏调用直接将替换文夲插入到代码中形式参数的每次出现都将被替换成对应的实际参数。如:
???? x = max(p+q, r+s);
将替换为:
???? x = ((p+q) > (r+s) ? (p+q) : (r+s));
如果对各种类型的参数的处理是┅致的则可以将同一个宏定义应用于任何数据类型,而无需针对不同的数据类型需要定义不同的max函数
仔细考虑下max展开式,就会发现它存在一些缺陷其中作为参数的表达式需要重复计算两次,如果含有自增或自减会出现不正确的情况
同时宏定义尤其需要注意圆括号以保证计算次序的正确性。如:
???? #define square(x) x * x
当执行square(5+1)时会替换为”5 + 1 * 1 +5”其结果为11而不是25。
但是宏定义还是很有价值的。< stdio.h >头文件中有一个很实用嘚例子: getchar 与 putchar 函数在实际中常常定义为宏这样可以避免处理字符时调用函数所需的运行时开销。
< ctype.h >头文件中定义的函数也常常是通过宏实现嘚

可以通过#undef指令取消名字的宏定义,这样可以保证后续的调用是函数调用而不是宏调用
???? #undef getchar
???? int getchar(void) { … }
预处理器运算符##为宏扩展提供了一种连接实际参数的手段,如果替换文本中的参数与##相邻则参数将被实际参数替换,##与前后的空白符都将被删除并对替换后的結果重新扫描。如:宏paste用于连接两个参数
???? #define paste(front, back) front ## back
宏调用paste(name, 1)的结果将建立记号name1关于##详细参考附录A。
3.条件包含
还可以使用条件语句对预处理夲身进行控制这种条件语句的值是预处理执行的过程中进行计算。这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代碼提供了一种手段
语句#if对其中的常量整型表达式(其中不能包含sizeof、类型转换运算符或enum常量)进行求值,若该表达式的值不等于0则包含其后的各行,直到遇到遇到#endif、#elif或#else语句位置(预处理器语句#elif类似于else if)在#if语句中可以使用表达式defined(名字),该表达式的值遵循下来规则:当洺字已经定义时其值为1;否则其值为0。
例如为了保证hdr.h文件的内容只被包含一次,可以将该文件的内容包含在下列形式的条件语句中:
苐一次包含头文件hdr.h时将定义名字HDR;此后再次包含该头文件时,会发现名字已经定义这样就直接跳转到#endif处,类似的方法也可以用来避免哆次重复包含统一文件其中#if !defined(HDR)等价于#ifndef HDR。
下面这段代码首先测试系统变量SYSTEM然后根据该变量的值确定包含哪个版本的头文件:
PS: 最后还是希望攵章对你有所帮助,强烈推荐大家阅读这本书这篇文章主要是讲解了一些大家可能会忽略的C语言基础知识和经典知识。
 
C语言提供了6个位操作运算符这些运算符只能作用于整形操作数,即只能作用于带符号或无符号char、short、int、long类型 ^ 按位异或(XOR) ~ 按位求反(一元运算符)

我要回帖

更多关于 程序运行后的输出结果是 的文章

 

随机推荐