C Primer Plus(第五版)11

第 11 章 字符串和字符串函数


在本章中你将学习下列内容;

· 函数: gets(), puts(),
strcat(), strncat(),
strcmp(), strncmp(),
strcpy(), strncpy(),
sprintf(), strchr(),

· 创建和使用字符串。

· 利用 C 库里的字符串和字符串函数创建你自己的字符串函数。

· 使用命令行参数。


字符串是 C 里面最有用,最重要的数据类型之一。虽然你一直在使用字符串,要学的东西仍然很多。 C 库提供了众多的函数用来读写字符串,复制字符串,比较字符串,纵使字符串,查找字符串等等。本章将通过这些内容增强你的编程技能。

 

11.1 字符串表示和字符串 I/O


当然,最基本的你已经知道了:字符串(character string)是以空字符(\0)结尾的 char 数组。因此,你所学的数组和指针知识就可以用到字符串上。但是由于字符串的使用非常广泛,C 提供了很多专为字符串设计的函数。本章将讨论字符串的特性,声明和初始化方法,如何在程序中输入输出字符串,以及字符串的操作。


程序清单 11.1 给出了一个程序,其中说明了建立,读入和输出字符串的几种方式。该程序使用了两个新的函数:gets()和 puts(),其中 gets()读入字符串, puts()输出字符串(你可能已经注意到了它们和 getchar(),putchar()系列函数的相似性)。程序的其他部分在你看起来会比较熟悉。

 

程序清单 11.1 strings.c 程序
-----------------------------------------------------------------------
/* strings.c -- 使用字符串和用户交互 */
#include <stdio.h>
#define MSG "You must have many talents, Tell me some " // 一个符号字符串常量
#define LTM 5
#define LINELEN 81 // 最大字符串长度+1
int main (void)
{
char name[LINELEN];
char talents[LINELEN];
int i;
const char m1[40] = "Limit yourself to one line's worth .";
//初始化一个大小已确定的 char 数组
const char m2[] = " If you can't think of anything, fake it ";
// 让编译器计算数组大小
const char *m3 = "\nEnough about me - what's your name ?";
// 初始化一个指针
const char *mytal[LIM] = {"Adding numbers swiftly","multiplying accurately",
"Stashing data", "Following instructions to the letter ",
"Understanding the C language" };
// 初始化一个字符串指针的数组

printf ("Hi ! I'm clyde the Coputer" "I have many talents .\n");
printf ("Let me tell you some of them \n");
puts("What were they? Ah, yes,here's a partial list "):
for (i = 0; i < LIM; i++)
puts(myal[i]); // 打印计算功能的列表
puts(m3);
gets(name);
printf ("Well, %s,%s \n",name,MSG);
printf ("%s \n %s\n",m1,m2);
gets(talents);
puts ("Let's see if I've got thar list :");
puts (talents);
printf (" Thanks for the informaation, %s \n",name);
return 0;
}

下面是一个运行示例,可以看到程序的功能:

Hi ! I'm clyde the CoputerI have many talents .
Let me tell you some of them
What were they? Ah, yes,here's a partial list
Adding numbers swiftly
multiplying accurately
Stashing data
Following instructions to the letter
Understanding the C language

Enough about me - what's your name ?
Nlgel Barntwit // 交互输入语句
Well, Nlgel Barntwit,You must have many talents, Tell me some
Limit yourself to one line's worth .
If you can't think of anything, fake it
Fencing,yodeling,malingering,cheese tasging,and sighing. // 交互输入语句
Let's see if I've got thar list :
Fencing,yodeling,malingering,cheese tasging,and sighing.
Thanks for the informaation, Nlgel Barntwit


与其逐行解释程序清单 11.1 ,不如采用一种更为行之有效的方法。首先,我们看一下在程序中定义字符串的几个种方法;然后你将了解把字符串读入程序中所涉及的操作;最后,你将学会如何输出字符串。

11.1.1 在程序中定义字符串


阅读程序清单 11.1 时你可能已经注意到,定义字符串的方法很多。基本的办法是使用字符串常量,char 数组, char 指针和字符串数组。程序应确保有存储字符串的地方,这一点我们稍后也会讨论到。

------------------------------------------------------------------------------------
一· 字符串常量(字符串文字)

字符串常量 (string constant),又称为字符串文字(string literal),是指位于一对双引号中的任何字符。双引号里的字符加上编译器自动提供的结束标志 \0 字符,作为一个字符串被存储在内存里。程序中使用了几个这样的字符串常量,大多数是用作函数 printf() 和 puts()的参数。注意,还可以用 #define 来定义字符串常量。


如果字符串文字中间没有间隔或者间隔的是空格符,ANSI C 会将其串联起来。例如,

char greeting[50] = "Hello,and" "how are" "you" "today!";

char greeting[50] = "Helo, and how are you today!";
是相等的。


/* 如果想在字符串中使用双引号,可以在双引号前加一个反斜线符号,如下所示: */

printf ("\"Run,spot,run!\"exclaimed Dick \n");

它的输出如下:

"Run,Spot,run!" exclaimed Dick.


字符串常量属于静态存储(static storage)类。静态存储是指如果在一个函数中使用字符串常量,即使是多次调用了这个函数,该字符串在程序的整个运行过程中只存储一份。整个引号中的内容作为指向该字符串存储位置的指针。这一点与把数组名作为指向数组存储位置的指针类似。如果事实确实如此,程序清单 11.2 中的程序的输出会是什么?

可以把字符串看作是数组名
程序清单 11.2 quotes.c 程序
-----------------------------------------------------------------------
/* quotes.c -- 把字符串看作指针 */
#include <stdio.h>
int main (void)
{
printf ("%s, %p, %c \n","We", "are", *"space farers");
return 0;
}

%s 格式将输出字符串 We。%p 格式产生一个地址。因此如果“are”是个地址,那么 %p 应该输出字符串中第一个字符的地址(ANSI 之前的实现可能用 %u 或 %lu 而不用 %p)。最后,
*"space farers" 应该产生所指向的地址中的值,即字符串"space farers"中的第一个字符。真的是这样吗? 下面是输出结果:

We, 01185790, s

 

 

------------------------------------------------------------------------------------

二 · 字符串数组及其初始化

定义一个字符串数组时,你必须让编译器知道它需要多大空间。一个办法就是指定一个足够大的数组来容纳字符串,下面的声明用指定字符串中的字符初始化数组 m1:

const char m1[40] = "Limit yourself to one line's worth ";

const 表明这个字符串不可以改变。

这种初始化和下面所示的标准数组初始化相比是很简短的;

const char m1[] = {'L','i','m','i','t',' ','y','o','u','r','s','e','l',
'f',' ','t','o',' ','o','n','e',' ',
'l','i','n','e','\','s',' ','w','o','r',
't','h','.','\0'};

注意标志结束的空字符。如果没有它,得到的就只是一个字符数组而不是一个字符串。


指定数组大小时,一定要确保数组元素数比字符串长度至少多 1 (多出来的 1 个元素用于容纳空字符)。末被使用的元素均自动初始化为 0 。这里的 0 是 char 形式的空字符,而不是数字字符 0 ,请参见图 11.1 。


-----------------------------------------------------------
图 11.1 数组初始化
多余的元素被初始化为 \0
| |
--------------------------------------------------
| n | i | c | e | | c | a | t | . | \0 | \0 | \0 |
--------------------------------------------------
char pets[12] = "nice cat.";

-----------------------------------------------------------

通常,让编译器决定数组大小是很方便的。回忆一下,在进行初始化声明时如果省略了数组大小,则该大小由编译器决定。

const char m2[] = "If you can't think of anything,fake it .";

初始化字符数组是体现由编译器决定数组大小的优点的又一个例子。这是因为字符串处理函数一般不需要知道数组的大小,因为它们能够简单地通过查找空字符来确定字符串结束。


请注意程序必须为数组 name 明确分配大小:

#define LINELEN 81 // 最大字符串长度 + 1
...
char name[LINELEN];

由于直到程序运行时才能读取 name 的内容,所以除非你说明,编译器无法预先知道需要为你预留多大空间。当前没有字符串常量可以让编译器计算字符数,因此我们假定 80 个字符足以容纳用户的名字。声明一个数组时,数组的大小必须为整形常量,而不能是运行时得到的变量值。编译时大小被锁定到程序中(事实上,在 C99 中可以使用变长数组,但仍然无法预先知道数组大小应为多大)。

int n =8;
char cakes[2 + 5]; /* 合法,数组大小是一个常量表达式 */
char crumbe[n]; /* 在 C99 之前是无效的,在 C99 之后是一个变长数组(VLA) */

和任何数组名一样,字符数组名也是数组首元素的地址。因此,下面的式子对于数组 m1 成立:

m1 == &m1[0], *m1 == 'L', and *(m1+1) == m1[1] == 'i' /* 对应下面的语句 */
(const char m1[40] = "Limit yourself to one line's worth ";)


的确,可以使用指针符号建立字符串。例如,程序清单 11.1 中使用了下面的声明:

const char *m3[] = "\nEnough about me - what's your name? ";

这个声明和下列声明的作用几乎相同:

char m3[] = "\nEnough about me - what's your name? ";


上面两个都声明 m3 是一个指向给定字符串的指针。在两种情况下,都是被引用的字符串本身决定了为字符串预留的存储空间大小。尽管如此,这两种形式并不完全相同。

 

 

------------------------------------------------------------------------------------

三 · 数组与指针

那么,数组和指针形式的不同是什么呢? 数组形式(m3[])在计算机内存中被分配一个有 38 个元素的数组(其中每个元素对应一个字符,还有一个的元素对应结束的空字符'\0')。每个元素都被初始化为相应的字符。


通常,被引用的字符串被称为位于静态存储区。但是在程序开始运行后才为数组分配存储空间。这时候,把被引用的字符串复制到数组中(第 12 章“存储类,链接和内存管理”会更详细地讲述内存管理)。

此后,编译器会把数组名 m3 看作是数组首元素的地址 &m3[0] 的同义词。这里重要的一点是,在数组形式中 m3 是个地址常量。你不能更改 m3 ,因为这意味着更改数组存储的位置(地址)。可以使用运算符 m3+1 来标识数组里的下一个元素,但是不允许使用 ++m3 。增量运算符只能用在变量名前,而不能用在常量前。


指针形式(*m3) 也在静态存储区为字符串预留 38 个元素的空间。此外,一旦程序开始执行,还要为指针变量 m3 另外预留一个存储位置,以在该指针变量中存储字符串的地址。这个变量初始时指向字符串的第一个字符,但是它的值是可以改变的。因此,可以对它使用增量运算符。例如,++m3 将指向第二个字符 E 。


总之,数组初始化是从静态存储区把一个字符串复制给数组,而指针初始化只是复制字符串的地址。

这些区别重要吗?通常并不重要,但是这要取决于做什么。下面的讨论中将用一些例子说明。


--------------------------------------------------------------------------------------


四· 数组和指针的差别


我们研究一下初始化一个存放字符串的字符数组和初始化一个指向字符串的指针这两者的不同(指向字符串其实是指向字符串的第一个字符)。例如,考虑下面两个声明:

char heart[] = "I love tillie!.";
char *hear = "I love Millie!.";

主要的差别在于数组名 heart 是个常量,而指针 head 则是个变量。实际使用中又有什么不同呢?

首先,两都都可以使用数组符号:

for ( i = 0; i < 6; i++)
putchar (heart[i]);
putchar('\n');

for ( i = 0; i < 6; i++)
putchar (hear[i]);
putchar('\n');

以下是输出:

I love
I love


其次,两者都可以使用指针加法:

for (i = 0; i < 6; i++)
putchar(*(heart+i));
putchar('\n');
for (i = 0; i < 6; i++)
putchar(*(hear+i));
putchar('\n');

输出仍然如下:

I love
I love


但是只有指针可以使用增量运算符;

while (*(hear) != '\0') /* 在字符串的结尾处停止 */
putchar(*(hrar++)); /* 打印字符,并向前推进指针 */

产生的输出下:

I love Millie!

假定希望 hear 与 heart 相同,可以这样做:

hear = herat; /* 现在 hear 指向数组 heart */

这就使得 hear 指针指向 heart 数组的首元素。但是,不能这样做:

heart = hear; /* 非法语句 */

这种情况类似于 x = 3; 和 3 = x; 。赋值语句的左边必须是一个变量或者更一般地说是一个左值(lvalue),比如 *p_int 。顺便提一下,hear = heart; 不会使 Millie 字符串消失,它只是改变了 hear 中存储的地址。但是除非已在别处保存了“I love Millie!”的地址,否则当 hear 指向另一个地址时就没有办法访问这个字符串了。


可以改变 heart 中的信息,方法是访问单个的数组元素:

heart[7] = 'M';

或者:

*(heart + 7) = 'M';

数组的元素是变量(除非是声明数组时带有关键字 const),但是数组名不是变量。

让我们回到对指针初始化的讨论:

char *word = "frame";

可以用指针改变这个字符串吗?

word[1] = 'l'; // 是否允许?

你的编译器可能会允许上面的情况,但按照当前的 C 标准,编译器不应该允许这样做。这种语句可能会导致内存访问错误。原因在于编译器可能选择内存中的同一个单个的拷贝,来表示所有相同的字符串文字。例如,下面的语句都指向字符串“KLingon”的同一个单独的内存位置。

char *p1 = "KLingon";
p1[0] = 'F'; // ok?
printf ("KLingon");
printf (":Beware the %dd \n","Klingon");

这就是说,编译器可以用相同的地址来替代每一个“Klingon”的实例。如果编译器使用这种单个拷贝表示法并且允许把 p1[0] 改为‘F’的话,那将会影响到所有对这个字符串的使用。于是,打印字符串文字“Klingon”的语句实际将会显示“Flingon”:

Flingon: Beware the Flingons !


实际上,有些个编译器确实是按这种容易混淆的方式工作,而其他的一些则会产生程序异常中断。因此,建议的做法是初始化一个指向字符串文件的指针时使用 const 修饰符:


const char *p1 = "Klingon"; // 推荐用法

用一个字符串文字来初始化一个非 const 的数组,则不会导致此类问题,因为数组从最初的字符串得到一个拷贝。

 


-----------------------------------------------------------------------------------------


五· 字符串数组


有一个字符串数组是很方便的。这样就可以使用下标来访问多个不同的字符串。程序清单 11.1 就使用了下面这个例子:

const char *mytal[LIM] = {"Adding numbers swiftly","multiplying accurately",
"Stashing data", "Following instructions to the letter ",
"Understanding the C language" };


让我们研究一下上面的声明。因为 LIM 是 5,所以 mytal 是一个由 5 个指向 char 的指针组成的数组。也就是说,mytal 是个一维数组,而且数组里的每一个元素都是一个 char 类型值的地址。第一个指针是 mytal[0],它指向第一个字符串的第一个字符。第二个指针是 mytal[1],它指向第二个字符串的开始。一般地,每一个指针指向相应字符串的第一个字符:

*mytal[0] == 'A', *myatl[1] == 'M', *myatl[2] == 'S',


依此类推。mytal 数组实际上并不存放字符串,它只是存放字符串的地址(字符串存在程序用来存放常量的那部分内存中)。可以把 mytal[0]看作表示第一个字符串,*mytal[0]表示第一个字符串的引一个字符。由于数组符号和指针之间的关系,也可以用 mytal[0][0]表示第一个字符串的第一个字符,尽管 mytal 并没有被定义成二维数组。


字符串数组的初始化遵循数组初始化的规则。花括号里那部分的形式如下:

{ {...}, {...}, {...}, {...} };


省略号代表我们懒得键入的内容。关键之处是第一对双引号对应着一对花括号,用于初始化第一个字符串指针。第二对双引号初始化第二个指针,等等。相邻字符串要用逗号隔开。



另一个方法就是建立一个二维数组:

char mytal_2[LIN][LINLIM];

在这里 mytal_2 是一个 5 个元素的数组,每一个元素本身又是一个 81 个 char 的数组。在这种情况下,字符串本身也被存储在数组里。两者差别之一就是第二种方法选择建立了一个所有行的长度都相同的矩形(rectangular)数组。也就是说,每一个字符中都用 81 个元素来存入。而指针数组建立的是一个不规则的(ragged)的数组,每一行的长度由初始化字符串决定:

char *mytal[LIM];

这个不规则数组不浪费任何存储空间。图 11.2 示意了这两种类型的数组(实际上,mytal 数组元素指向的字符串不必在内存中连续存放,但该图确实示意了存储需求的不同)。


另外一个区别就是 mytal 和 mytal_2 的类型不同;mytal 是一个指向 char 的指针的数组,而 mytal_2 是一个 char 数组的数组。一句话, mytal 存放 5 个地址,而 mytal_2 存放 5 个完整的字符数组。

 

----------------------------------------------------------------------------------------


1


11.1.2 指针和字符串


可能你已经注意到在对字符串的讨论中会不时地用到指针。绝大多数的 C 字符串操作事实上使用的都是指针。例如,考虑一下程序清单 11.3 所示的用于起到指示作用的程序。


程序清单 11.3 p_and_s.c 程序
----------------------------------------------------------------------
/* p_and_s.c --- 指针和字符串 */
#include <stdio.h>
int main (void)
{
char *mesg = "Don't be a fool!";
copy *copy;

char = mesg;
printf ("%s\n",copy);
printf ("mesg = %s; &mesg = %p; value = %p \n",mesg,&mesg,mesg);
printf ("copy = %s; &copy = %p; value = %p \n",copy,&copy,copy);
return 0;
}

看一下这个程序,你会以为它复制了字符串 "Don't be a fool!", 而且乍一看结果似乎也验证了你的猜测。

Don't be a fool!
mesg = Don't be a fool!; &mesg = 0013FB70; value = 013257A0
copy = Don't be a fool!; &copy = 0013FB64; value = 013257A0

但是再仔细研究一下 函数 prinf()的输出。首先, mesg 和 copy 以字符串形式 (%s)输出。这里没有奇怪的事情发生,两个字符串都是 "Don't be a fool!"。


每一行的下一项是指定指针的地址。 mesg 和 copy 这两个指针分别放在位置 0013FB70 和
0013FB64 。


现在注意一下最后一项,即 value 。它是指定指针的值。指针的值是该指针中存放的地址,可以看到 mesg 指向位置 013257A0 ,copy 也是如此。因此,字符串本身没有被复制。语句 copy = mesg;
因此,字符串本向没有被复制。语句 copy = mesg ;所做的事情就是产生指向同一个字符串的第二个指针。


为什么 如此谨慎行事?为什么不干脆复制整个字符串?好了,问一下自己哪一各路方法更有效率?复制一个地址还是复制 50 个单元的元素?通常,只有地址才是程序执行所需要的。如果确实需要复制字符串,可以使用函数 strcpy()或 strncpy(),这两个函数会在本章稍后讨论。


我们已经讨论了如何在程序中定义字符串,现在来看一看如何从键盘输入字符串。

11.2 字符串输入


如果想把一个字符串读到程序中,必须首先预留存储字符串的空间,然后使用输入函数来获取这个字符串。

-------------------------------------------------------------------
11.2.1 创建存储空间

要做的第一件事是建立一个空间以存放读入的字符串。正如前面提过的,这意味着需要分配足够大的存储区来存放希望读入的字符串。不要指望计算机读的时候会先计算字符串的长度,然后为字符串分配空间。计算机是不会这么做的(除非你写了一个函数命令它这么做)。例如,假定你尝试写了下面的语句:

char *name;
scanf ("%s",name);

这可能会通过编译器,但是在读入 name 的时候, name 会覆盖程序中的数据和代码,并可能导致程序异常终止。这是因为 scanf()把信息复制到由参数给定的地址中,而在这种情况下,参数是个末初始化的指针;name 可能指向任何地方。绝大多数程序员认为这很搞笑,但仅限于这出现在别人的程序中是。


最简单的方法就是声明中明确指出数组大小:

char name[81];

现在 name 是一个已分配的 81 字节存储块的地址。另外一个方法就是使用 C 库里分配存储空间的函数,这一点会在第 12 章讨论。


为字符串预留空间后,就可以读取字符串了。C 库提供了三个读取字符串的函数; scanf(),
gets()和 fgets()。我们先讨论最常用的 gets()。

--------------------------------------------------------------------------

11.2.2 gets()函数

gets()(代表 get string) 函数对于交互式程序非常方便。它从系统的标准输入设备(通常是键盘)获得一个字符串。因为字符串没有预定的长度,所以 gets()需要知道输入何时结束。解决办法是读字符串直到遇到一个换行字符(\n),按回车键可以产生这个字符。它读取行符之前(不包括换行符)的所有字符,在这些字符后添加一个空字符(\0),然后把这个字符串交给调用它的程序。它将读取换行符并将其丢弃,这样下一次读取会在新的一行开始。程序清单 11.4 给出了一个使用 gets()简单例子。


程序清单 11.4 name1.c 程序
-------------------------------------------
/* name1.c -- 读取一个名字 */
#include <stdio.h>
#define MAX 81
int main (void)
{
char name[MAX]; // 分配空间
printf ("Hi,what's your name ?\n");
gets(name); // 把字符串放进 name 数组中
printf ("Nice name ,%s \n",name);
return 0;
}

下面是一个运行示例:

Hi,what's your name ?
The mysterious Davina D'Lsma
Nice name ,The mysterious Davina D'Lsma


程序清单 11.4 接受并存储最多 80 个字符(包括空格)的任何名字(记住为数组里的 \0 预留空间)。注意到希望 gets()改变调用函数中的某个变量(name),也就是说应当使用一个地址作为参数;当然,数组名正是一个地址。

gets()函数的使用可以比前面的例子更为复杂,请参见程序清单 11.5.


程序清单 11.5 name2.c 程序
-----------------------------------------------
/* name2.c -- 读取一个名字 */
#include <stdio.h>
#define MAX 81
int main (void)
{
char name[MAX];
char *ptr;

printf ("Hi, what's your name ?\n");
ptr = gets(name);
printf ("%s ? Ah! %s \n",name,ptr);
return 0;
}

下面是一个交互的例子;

Hi, what's your name ?
Wellington Snackworthy
Wellington Snackworthy ? Ah! Wellington Snackworthy!

gets()函数通过两种方式获得输入;

它使用一个地址把字符串赋予 name 。

gets()的代码使用 return 关键字返回字符串的地址,程序把这个地址分配给 ptr。注意到 ptr
是一个 char 指针,这意味着 gets()必须返回一个指向 char 的指针值。


ANSI C 要求 stdio.h 头文件包括 gets()的函数原型。你不需要亲自声明这个函数,只须记住包含这个头文件即可。但是一些 C 的旧版本要求你提供 gets()的函数声明。


gets()函数的构造如下:
char *gets (char *s)
{
...
return (s);
}

这个函数头说明 gets()返回一个指向 char 的指针。请注意 gets()返回的指针与传递给它的是同一个指针。输入字符串只有一个备份,它放在作为函数参数传递过来的地址中,因此程序清单 11.5 中的 ptr 最后也指向 name 数组。 gets()函数实际的构造更复杂一点,因为它有两个可能的返回值。如果一切顺利,它返回的是读入字符串的地址,正如我们上面所说。如果出错或如果 gets()遇到文件结尾,它就返回一个空(或 0 )地址。这个空地址被称为空指针,并用 stdio.h 里定义的常量 NULL 来表示。因此 gets()中还加入了一些错误检测,这使它可以很方便地以如下形式使用:

while (gsts(name) != NULL)

这样的指令使你既可以检查是否到了文件结尾,又可以读取一个值。如果遇到了文件结尾,name 中什么也不会读入。这种一举两得的方法就比 getchar()函数所采用的方法简洁得多,getchar()只返回一个值而没有参数:

while ((ch = getchar()) != EOF);

附带提一下,不要混淆空指针和空字符。空指针是一个地址,而空字符是一个 char 类型的数据对象,其值为 0 。数值上两者都可以用 0 表示,但是它们的概念不同:NULL 是一个指针,而 0 是一个 char 类型的常量。


---------------------------------------------------------------------------------


11.2.3 fgets() 函数

gets()的一个不足是它不检查预留存储区是否能够容纳实际输入的数据。多出来的字符简单地溢出到相邻的内存区。fgets()函数改进了这个问题,它让你指定是大读入字符数。由于 fgets(0是为文件 I/O 而设计的,在处理键盘输入时就不如 gets()那么方便。fgets()和 gets()有三方面不同:

1·它需要第二个参数来说明最大读入字符数。如果这个参数值为 n ,fgets()就会读取最多 n-1 个
字符或者读完一个换行符为止,由这二者中最先满足的那个来结束输入。

如果 fgets()读取到换行符,就会把它存到字符串里,而不是像 gets()那样丢弃它。

3·它还需要第三个参数说明来读哪一个文件。从键盘上读数据时,可以使用 stdin(代表 standard
input)作为该参数,这个标识符在 stdio.h 中定义。


程序清单 11.6 name3.c 程序
--------------------------------------------
/* name3.c -- 使用 fgets()读取一个名字 */
#include <stdio.h>
#define MAX 81
int main (void)
{
char name[MAX];
char *ptr;

printf ("Hi, what's your name? \n");
ptr = fgets(name,MAX,stdin);
pritnf ("%s? Ah! %s !\n",name,ptr);
return 0;
}

下面是一个输出示例,它显示了 fgets() 的一个不足之处:

Hi, what's your name?
Jon Dough
Jon Dough
? Ah! Jon Dough
!

问题在于 fgets()把换行符存储到字符串里,这样每次显示字符串时就会显示换行符。本章后面“其他字符串函数”小节的结尾将会介绍如何用 strchr()来定位和删除换行符。


由于 gets()不检查目标数组是否能够容纳输入,所以很不安全。的确,几年前就有人注意到一些 UNIX 操作系统代码使用 gets(),于是他们利用这个弱点,用很长的输入覆盖操作系统的代码,从而发明了在 UNIX 网络上传播的“蠕虫(worm)”病毒。那些系统代码后来被不使用 gets()的代码所代替。因此对于重要的编程,应该使用 fgets()而不是 gets(),但本书使用了更随便的做法。


-------------------------------------------------------------------------


11.2.4 scanf() 函数


前面你已经使用了带有 %s 格式的 scanf()函数来读入一个字符串。scanf()和 gets()主要的差别在于它们如何决定字符串何时结束。scanf()更基于获取单词(get word)而不是获取字符串
(get string);而 gets()函数,正如你所看到的,会读取所有的字符,直到遇到第一个换行符为止。scanf()使用两种方法决定输入结束。无论哪种方法,字符串都是以遇到第一个非空白字符开始。如果使用 %s 格式,字符串读到(但不包括)下一个空白字符(比如空格,制表符或换行符)。如果指定了字符宽度,比如 %10s, scanf()就会读入 10 个字符或直到遇到第一个空白字符,由二者中最先满足的那一个终止输入(请参见图 11.3 )


回忆一下,scanf()函数返回一个整数值,这个值是成功读取的项目数;或者当遇到文件结束时返回一个EOF。


程序清单 11.7 举例说明了指定字段宽度时 scanf()的工作情况

程序清单 11.7 scan_str.c 程序
---------------------------------------------------
/* scan_str.c -- 使用 scnaf() */
#include <stdio.h>
int main (void)
{
char name1[11],name2[11];
int count;

printf ("Please enter 2 names .\n");
count = scanf ("%5s %10s",name1,name2);
printf ("I read the %d names %s and %s \n",count,name1,name2);
return 0;
}

下面是三次运行结果
Please enter 2 names .
Jesse Jukes
I read the 2 names Jesse and Jukes


Please enter 2 names .
Liza Applebottham
I read the 2 names Liza and Applebotth


Please enter 2 names .
Portensia Callowit
I read the 2 names Porte and nsia


在第一个例子中,两个名字都小于允许的大小。在第二个例子中,由于使用了 %10s 格式, Applebottham 只有前 10 个字符被读取。在第三个例子中, Portensia 的后 4 个字母被读到 name2 中,这是因为第二次调用 scnaf()时,它在第一个调用结束的地方继续开始读输入数据。在这个例子中,仍从单词 Portensia 中间开始读取。


根据所需输入的特点,用 gets()从键盘读取文本可能要更好,因为它更容易被使用,更快而且更简洁。scanf()主要用于以某种标准形式输入的混合类型数据的读取和转换。例如,如果每一行输入行都包括一种工具的名称,库存数量和单价,你就可以使用 gets();否则你必须在函数中自己处理输入错误的检测。如果希望一次只输入一个单词,最好使用 scanf()。


下面我们讨论字符串的显示方法。


11.3 字符串输出


现在让我们把注意力从字符串的输入转移到字符串。这里再次要用到库函数。C 有三个用于输出字符串的标准库函数: puts(), fputs()和 printf()。


---------------------------------------------------------------

11.3.1 puts()函数

puts()函数的使用很简单,只需要给出字符串参数的地址。程序清单 11.8 列出了输出字符串的多种方式


程序清单 11.18 put_out.c 程序
-----------------------------------------------
/* put_out.c -- 使用 puts() */
#include <stdio.h>
#define DEF "I am a #define string."
int main (void)
{
char str1[80] = "An array was initialized to me ";
const char *str2 = "A pointer was initalized to me";

puts ("I'm an argument to puts() ");
puts (DEF);
puts (str1);
puts (str2);
puts (&str1[5]);
puts (str2+4);
return 0;
}

输出如下:

I'm an argument to puts()
I am a #define string.
An array was initialized to me
A pointer was initalized to me
ray was initialized to me
inter was initalized to me

注意,每一个字符串都单行显示,与 printf()不同, puts()显示字符串时自动在其后添加一个换行符。

这个例子让人想起了双引号中的字符是字符串常量,并被看作地址。同样,字符数组字符串的名字也被看作是地址。表达式 &str1[5] 是 数组 str1 的第 6 个元素的地址。这个元素包含 字符‘r’,它也正是 puts()输出字符串的起点。与之类似,str2+4 指向包含 ‘i’(“pointer”中的‘i’)那个内存单元。puts()如何知道何时停止?遇到空字符时它就会停下来,所以应该确保有空字符存在。不要模仿程序清单 11.9 中的程序!


程序清单 11.9 nono.c 程序
-------------------------------------------------
/* nono.c -- 不要效仿这个程序 */
#include <stdio.h>
int main (void)
{
char side_a[] = 'SIDE A';
hcar dont[] = {'W','O','W','!'};
char side_b = 'SIDE B';

puts(dont); /* dont 不是一个字符串 */

return 0;
}

dont 缺少一个表示结束的空字符,因此它不是一个字符串,这样 puts()就不知道应该到哪里停止。它只是一直输出内存中 dont 后面的字符,直到发现一个空字符。为了使这个空字符不太遥远,程序把 dont 存储在两个真正的字符串之间。下面是一个运行示例:

WOW! SIDE A

这里用到的特定的编译器在内存中把 side_a 数组存储在 dont 数组之后。因此, puts()函数继续执行直到遇到了 side_a 中的空字符。运行该程序时编译器在内存中存储数据方式不同,得到的结果也不同。如果程序漏掉了 side_a 和 side_b 怎么办呢? 通常内存中有很多空字符,如果幸运的话, puts()可能很快发现一个,但这是很不可靠的。


-----------------------------------------------------------------------------------------


11.3.2 fgets() 函数


fputs()函数是 puts()面向文件版本。两者之间的主要区别是:

1·fputs()需要第二个参数来说明要写的文件。可以使用 stdout(代表 standard output)作为参
数来进行输出显示,stdout 在 stdio.h 中定义。

与 puts()不同,fputs()并不为输出自动添加换行符。


注意, fputs()丢掉输入里的换行符,但是 puts()为输出添加换行符。另一方面,fgets()存储输入中的换行符,而 fputs()也不为输出添加换行符。假定写一个循环,读取一行并把它回显在下一行,可以这么写:

char line[81];
while (gets(line))
puts(line);

回忆一下,如果遇到文件结尾,gets()就返回空指针。空指针的值为 0 (也即假),这样就结束了循环。或者也可以这么做:

char line[81];
while (fgets(line,81,stdin))
fputs(line,stdout);

在第一个循环,line 数组中的字符串被显示在单独的一行上,这是由于 puts()为它添加了一个换行符。第二个循环,line 数组中的字符串同样被显示在单独的一行上,这是由于 fgets()存储了一个换行符。注意,如果把 fgets()输入和 puts()输出结合使用,每个字符串后就会显示两个换行符。关键在于 puts()是为和 gets()一起使用而设计的,而 fputs()是为和 fgets()一起使用而设计的


-------------------------------------------------------------------------------------


11.3.3 printf()函数


在第 4 章“字符串和格式化输入/输出”中我们详细讨论了 printf(),如同 puts()一样,printf()需要一个字符串地址作为参数。printf()函数使用起来没有 puts()那么方便,但是它可以格式化多种数据类型,因而更通用。


它们的区别之一就是 printf()并不自动在新行上输出每一个字符串。相反,你必须指明需要另起一行的地方。因此:

printf("%s \n", string);

与下面的语句效果一样:

puts(string);


正如你所见,第一种形式需要键入更多代码,此外计算机的执行时间也更长(但你觉察不到)。不过,printf()使在一行上输出多个字符串变得更为简单。例如,下面的语句把 Well, 用户名和一个用 #define 定义的字符串统统显示在一行上:


printf ("Well, %s, %s \n",name,MSG);


11.4 自定义字符串输入/输出函数


不一定要使用标准 C 库的函数进行输入和输出。如果不具备或者不喜欢它们,你可以自行定义,在
getchar()和 putchar()的基础上建立自己的函数。假定你希望有一个类似 puts()但并不自动添加换行符的函数。 程序清单 11.10 给出了一种方法。


程序清单 11.10 put1.c 程序
-----------------------------------------------------
/* put1.c -- 不添加换行符打印一个字符串 */
#include <stdio.h>
void put1 (const char *string) /* 不会改变这个字符串 */
{
while (*string != '\0')
putchar(*string++);
}


char 指针 string 最初指向被调参数的第一个元素。由于这个函数并不改变字符串,因此使用了 const 修饰符。这一元素的内容输出以后,指针递增并指向下一个元素。这个过程一直继续下去,直到指针指向一个包含空字符的元素。记住,++ 比 * 的优先级高,这意味着 putchar(*string++)输出 string 指向的值,然后再 string 的本身,而不是增加 string 指向的字符。


可以把 put1.c 看作自定义字符串处理函数的范例。每个字符串都有一个空字符标志其结束,因此不必向函数传递字符串的大小。相反,函数依次处理每个字符直到遇到空字符。


用数组符号写这个函数会比较长;

int i = 0;
while (string[i] != '\0')
putchar (string[i]);


其中用到了一个作为索引的额外变量。

很多 C 程序员会在 while 循环中使用下面的判断条件;

while (*string)

当 string 指向空字符时,*string 的值为 0 ,这将结束循环。这个方法需要的键入自然比前面的方法要少。如果你还不熟悉 C 的惯例,这一优点就不是很明显。上面的语句被广泛使用,C 程序员应该熟悉它。


----------------------------------------------------------------------

PS : 说明

为什么程序清单 11.10 用 const char *string 而不用 const char string[]作为形式参数?
从技术上来说,二者等价,因此它们都有效。用方括号符号的一个用户是提醒用户这个函数处理的是数组。但在使用字符串时,实际的参数可以是数组名,引起来的字符串,或被声明为 char *类型的变量。使用 const char *string 可以提醒你实际的参数不一定是一个数组。


-----------------------------------------------------------------------


假定你希望有一个类似 puts()的函数,并且这个函数还可以给出输出的字符个数。如程序清单 11.11 所示,添加这一功能很简单。


程序清单 11.11 put2.c 程序
---------------------------------------------------------
/* put2.c --- 打印一个字符串,并统计其中的字符个数 */
#include <stdio.h>
int put2 (const char *string)
{
int const = 0;
while (*string)
{
putchar(*string++);
count++;
}
putchar('\n'); /* 换行符不统计在内 */
return (count);
}

下面的函数调用输出字符串 pizza:

put1("pizza");

下面的函数调用还返回一个字符计数值,并把该值赋给 num。 在本例中这个值为 5 。

num = put2 ("pizza");

程序清单 11.12 给出一个使用 put1() 和 put2()的驱动程序,其中还使用了嵌套函数调用。



程序清单 11.12 put_put.c 程序
--------------------------------------------------------------
// put_put.c --- 用户定义的输出函数
#include "stdafx.h"
void put1 (const char *);
int put2 (const char *);

int main (void)
{
put1("If I'd as much money");
put2(" as I could spend, ");
printf ("I count %d characters \n",
put2("I never would cry old chairs to mend "));
return 0;
}

void put1 (const char *string)
{
while (*string) /* 等同于 *string != '\0' */
putchar(*string++);
}

int put2 (const char *string)
{
int count = 0;
while (*string)
{
putchar(*string++);
count++;
}
putchar('\n');
return (count);
}
/* 注: 书中的这个清单的代码有问题,如果按书上的代码,不可能是这种结果
会多一个空行 */
------------------------------------------------------------------------

嗯,我们使用 printf()输出 put2()的值。但是在计算 put2()值的过程中,计算机必须先执行这个函数,这样就输出了其中的字符串。下面是输出结果:

If I'd as much money as I could spend,
I never would cry old chairs to mend
I count 37 characters

 

11.5 字符串函数


C 库提供了许多处理字符串的函数;ANSI C 用头文件 string.h 给出这些函数原型。下面是一些最有用和最常用的函数: strlen(), strcar(), strncat(), strcmp(), strncmp(), strcpy(),strncpy() 。此外我们也将研究一下头文件 stdio.h 支持的 sprintf()函数。要查看 string.h 中的函数系列的完整列表,请参见参考资料 5 “添加了 cqq 的标准 ANSI C 库”部分。


--------------------------------------------------------------------------------

11.5.1 strlen()函数

我们已经知道,用 strlen()函数可以得到字符串的长度。下面的函数中用到了 strlen()函数,这是一个可以缩短字符串长度的函数:

/* test_fit.c */

void fit (char *string, unsigned int size)
{
if (strlen(string) > size)
*(string + size) = '\0';
}

这个函数确实要改变字符串,因此在函数头中声明形式参量 string 时没有使用 const 修饰符。

在程序清单 11.13 的程序中测试一下 fit()函数。注意,代码中用到了 C 的字符串文本串联功能。


程序清单 11.13 test.c 程序
----------------------------------------------------------------------
/* test.c -- 试用缩短字符串的函数 */
#include <stdio.h>
#include <string.h> /* 该头文件中包含字符串函数的原型 */
void fit (char *, unsigned int);

int main (void)
{
char mesg[] = "Hold on to your hats,hackers. ";

puts (mesg);
fit (mesg,7);
puts (mesg);
puts ("Let's look at some of the string. ");
puts (mesg + 8); /* mgsg 数组的第 8 个元素开始 */
return 0;
}

void fit(char *string, unsigned int size)
{
if (strlen(string) > size)
*(string + size) = '\0'; /* 在这个指针位置放置一个结束字符 */
}


输出如下:

Hold on to your hats,hackers.
Hold on
Let's look at some of the string.
to your hats,hackers.


fit()函数在数组的第 8 个元素中放置了 ‘\0’字符来代替原有的空格字符。puts()函数输出时停在第一个空字符处,忽略数组的其他元素。然而,数组的其他元素仍然存在,如下面的函数调用的输出结果所示:

puts (mesg + 8 );

表达式 mesg + 8 是 mesg[8] 即‘t‘字符的地址。因此 puts()显示这个字符并且继续输出直到遇到原字符串中的空字符。图 11.4 (一个短字符串)给出了程序的执行过程(这句话是从人们对艾伯特 爱因斯坦的评价中变化而来,但是这句话看上去更像是他的哲学思想的代表,而不仅仅是引用)。


ANSI 的 string.h 文件中包含了 C 字符串函数系列的原型,因此这个示例程序要包含这个文件。


--------------------------------------------------------------------------------------


11.5.2 strcar()函数


strcar()(代表 string concatenation) 函数接受两个字符串参数。它将第二个字符串的一份拷贝添加到第一个字符串的结尾,从而使第一个字符串成为一个新的组合字符串,第二个字符串并没有改变。strcar()函数是 char *(指向 char 的指针)类型。这个函数返回它的第一个参数的值,即其后添加了第二个字符串的那个字符串中第一个字符的地址。


程序清单 11.14 举例说明了 strcar()的功能。

程序清单 11.14 str_car.c 程序
----------------------------------------------------------
/* str_car.c --- 连接两个字符串 */
#include <stdio.h>
#include <string.h> /* 声明 strcat()函数 */
#define SIZE 80
int main (void)
{
char flower[SIZE];
char addon[] = "s smell like old shoes.";

puts ("What is your favorite flower? ");
gets (flower);
strcat (flower, addon);
puts (flower);
puts (addon);

return 0;
}

输出如下:

What is your favorite flower?
Rose
Roses smell like old shoes.
s smell like old shoes.


----------------------------------------------------------------------------------------


11.5.3 strncat()函数


strcat()函数并不检查第一个数组是否能够容纳第二个字符串。如果没有为第一个数组分配足够大的空间,多出来的字符溢出到相邻存储单元时就会出现问题。当然,可以像程序清单 11.15 那样,为第一个数组分配足够大的空间后再使用 strlen()函数。


注意,应该给组合串的长度加 1 以用来存入空字符。你也可以使用 strncar()函数,这个函数需要另一个参数来指明最多允许添加的字符的数目。例如, strncar(bugs,addon,13)函数把 addon 字符串中的内容添加到 bugs 上,直到加到 13 个字符或遇到空字符为止,由二者中先符合的那一个来终止添加过程。


因此,把空字符算在内(两种情况下都要添加空字符),bugs 数组应该足够大,以存放原始字符串(不包括空字符),增加的最多 13 个字符和结束的空字符。程序清单 11.15 使用这一知识计算 available 变量值,这个值被用作最多允许添加的字符数


程序清单 11.15 join_chk.c 程序
----------------------------------------------------------------
/* join_chk.c --- 连接两个字符串,并检查第一个字符串的大小 */
#include <stdio.h>
#include <string.h>
#define SIZE 30
#define BUGSIZE 13
int main (void)
{
char flower[SIZE];
char addon[] = "s smell like old shoes .";
char bug[BUGSIZE];
int available;

puts ("What is your faveorite flower?");
gets (flower);
if ((strlen(addon) + strlen(flower) + 1) <= SIZE); // 使用 strlen 来计算长度
strcat(flower,addon);
puts(flower);
puts ("What is your favorite bug ?");
gets (bug);
available = BUGSIZE - strlen(bug) - 1; /* 设置 strncat 显示个数的参数 */
strncat(bug,addon,available);
puts(bug);

return 0;
}

下面是一个运行示例:

What is your faveorite flower?
Rose
Roses smell like old shoes .
What is your favorite bug ?
Aphid
Aphids smell


------------------------------------------------------------------------------------------


11.5.4 strcmp()函数

假定你希望把用户的响应和一个已有的字符串进行比较,如程序清单 11.16 所示。

程序清单 11.16 nogo.c 程序
-----------------------------------------------------------
/* nogo.c --- 这个程序能满足要求吗? */
#include <stdio.h>
#define ANSWER "Grant"
int main (void)
{
char try1[40];

puts ("Who is buried in Grant's tomb?");
gets (try1);
while (try1 != ANSWER)
{
puts ("No that's wrong. Try again.");
gets (try1);
}
puts ("That's right !");
return 0;

}

尽管这个程序看起来不错,但却不能正确工作。ANSWER 和 try1 实际上是指针,因此比较式 try != ANSWER 并不检查这两个字符是否一样,而是检查这两个字符串的地址是否一样。由于 ANSWER 和 try1 被存放在不同的位置,所以这两个地址永远也不会一样,用户永远被告知他或她是 “wrong”。这种程序总让人泄气。


我们需要的是一个可以比较字符串内容(content)而不是字符串地址(address)的函数。你可以自行设计一个,但并不需要这样做,因为 strcmp()(代表 string comparison) 函数就可以实现这个功能。这个函数对字符串的操作就像关系运算符对数字的操作一样。特别地,如果两个字符串参数相同,它就返回 0 。改进后的程序如程序清单 11.17 所示。


程序清单 11.17 compare.c 程序
----------------------------------------------------------
/* compare.c -- 这个程序可以满足要求 */
#include <stdio.h>
#include <string.h> /* 声明 strcmp()函数 */
#define ANSWER "Grant"
#define MAX 40
int main (void)
{
char try1[MAX];

puts ("Who is buried in Grant's tomb?");
gets (try1);
while (strcmp(try1, ANSWER) != 0)
{
puts ("No, that's wrong, try again.");
gets (try1);
}
puts ("That's right!");
return 0;
}

-----------------------------------
ps : 说明

由于任何非零值都为真,大多熟练的 C 程序员会把 while 语句简单地写为:

while (strcmp(try1,ANSWER))

---------------------------------

strcmp()函数的一个优点是它比较的是字符串,而不是数组。尽管数组 try1 占用 40 个内存单元,而字符串 “Grant”只占用 6 个内存单元(一个用来存放空字符),但是函数在比较时只看 try1 的第一个空字符之前的部分。因此, strcmp()可以用来比较存放在不同大小数组里的字符串。


如果用户回答“GRANT”或者“grant”或“Ulysses S.Grant”会怎么样呢?用户会被告知他或她是错的。要编出一个更友好的程序,你必须先考虑到所有可能正确的答案。这里需要一些技巧。例如可以使用 #define 把答案定义为 “GRANT”,然后编写一个函数,把所有输入转换成大写字母。这样就解决了大小写的问题,但还是有其他需要考虑的形式。这一点我们留给读者自己练习。


strcmp() 的返回值

如果字符串不相同, strcmp()返回什么值呢? 程序清单 11.18 给出了一个例子。


程序清单 11.18 cmpback.c 程序
-----------------------------------------------------------
/* compback.c --- strcmp() 的返回值 */
#include <stdio.h>
#include <string.h>
int main (void)
{
printf ("strcmp (\"A\", \"A\") is ");
printf ("%d \n",strcmp ("A","A"));

printf ("strcmp (\"A\",\"B\") is ");
printf ("%d \n",strcmp ("A","B"));

printf ("strcmp (\"B\",\"A\") is ");
printf ("%d \n",strcmp ("B","A"));

printf ("strcmp (\"C\",\"A\") is ");
printf ("%d \n",strcmp ("C","A"));

printf ("strcmp (\"Z\",\"a\") is ");
printf ("%d \n",strcmp ("Z","a"));

printf ("strcmp (\"apples\",\"apple\") is ");
printf ("%d \n",strcmp ("apples","apple"));

return 0;
}

下面是一个系统上的输出结果:

strcmp ("A", "A") is 0
strcmp ("A","B") is -1
strcmp ("B","A") is 1
strcmp ("C","A") is 1
strcmp ("Z","a") is -1
strcmp ("apples","apple") is 1


比较“A”和它本身,返回值是 0 。比较 “A”和“B”的返回值是 -1 。两者交换,再进行比较,返回值是 1 。这些结果说明如果第一个字符串在字母表中的顺序先于第二个字符串,则 strcmp()函数返回的是负数;相反,返回的就是正数。因此,比较“C”和“A”得到的是 1 。其他系统可能返回的是 2 ,即二者的 ASCII 编码值之差。ANSI 标准规定,如果第一个字符串在字母表中的顺序先于第二个字符串,strcmp()返回一个负数;如果两个字符串相同,它返回 0 ;如果第一个字符串在字母表中的顺序落后于第二个字符串,它返回一个正数。而确切的数值是依赖于不同的 C 实现的。例如,这里给出另一种实现下的输出结果,它在两个字符比较间返回不同的结果:

strcmp ("A", "A") is 0
strcmp ("A","B") is -1
strcmp ("B","A") is 1
strcmp ("C","A") is 2
strcmp ("Z","a") is -7
strcmp ("apples","apple") is 115

如果两个字符串中初始的字符相同会怎么样呢?一般来说,strcmp()函数一直往后查找,直到找到第一对不一致的字符。然后它就返回相应的值。例如,在上一个例子中,“apples”和“apple”只有最后一个字符(第一个字符串中最后的那个‘s’)不同。匹配要进行到 "apple" 的第 6 个字符, 即空 (ASCII 中的 0). 由于空字符在 ASCII 中排在第一个, 字符 s 在它后面, 因此函数返回一个正数.


上面的比较表明 strcmp() 比较所有的字符, 而不仅仅是字母; 因此我们不应称比较是按字母表顺序, 而应该称 strcmp() 是按机器编码顺序 (collating sequence) 进行比较的. 这意味着字符的比较是根据它们的数字表示法, 一般是 ASCII 值. 在 ASCII 中, 大写字母先于小写字母. 因此, strcmp("Z","a") 是负数.


通常我们不会在意返回的确切值, 只想知道结果为 0 还是非 0 (也就是说看看它们是否匹配); 或者我们是把字符串按字母表顺序排序, 希望知道比较结果是正数, 负数还是 0 .

-------------------------------------------------

PS : 说明

strcmp() 函数用于比较字符串, 而不是字符. 因此可以使用诸如 "apples" 和 "A" 之类的参数; 但是不能使用字符参数, 如 'A' .考虑到 char 类型是整数类型, 因此使用关系运算符来对字符进行比较. 假定 word 是一个存储在 char 数组里的字符串, ch 是一个 char 变量. 那么下面的语句是合法的:

if (strcmp (word, "quit") == 0) //使用 strcmp() 进行字符串比较
puts ("Bye!");
if (ch == 'q') // 使用 == 进行字符比较
puts ("Bye!");

但是, 不能使用 ch 或 'q' 作为 strcmp() 的参数.

----------------------------------------------------------------

程序清单 11.19 使用 strcmp() 函数判断一个程序是否应该停止读取输入.


程序清单 11.19 quit_chk.c 程序
-------------------------------------------------
/* quit_chk.c -- 某程序的开始 */
#include <stdio.h>
#include <string.h>
#define SIZE 81
#define LIM 100
#define STOP "quit"
int main (void)
{
char input[LIM][SIZE];
int ct = 0;

printf("Enter up to %d lines (tpye quit to quit ) : \n",LIM);
while (ct < LIM && gets(input[ct]) != NULL && strcmp(input[ct],STOP) != 0 )
{
ct++;
}
printf ("%d strings enetred \n ",ct);
return 0;
}

当程序遇到一个 EOF 字符 (此时 gets() 返回空) 时, 或者你输入单词 quit 时, 或者达到 LIM 的限制时, 程序就会退出对输入的读取.


顺便提一下, 有时候输入一个空行来终止输入更方便, 也就是说, 在一个新行中不输入任何字符就下 Enter 键或 Return 键. 要这样做, 你可以对 while 循环的控制语句做如下的修改:

while (ct < LIM && gets(input[ct] != NULL && input[ct][0] != '\0')

此处, input[ct] 是刚输入的字符串, input[ct][0] 是该字符串的第一个字符. 如果用户输入一个空行, gets() 就把空字符放在第一个元素处, 因此如下表达式是用来检测空输入行的:

input[ct][0] != '\0'


-----------------------------------------------------------------

11.5.5 strncmp() 变种


strcmp() 函数比较字符串时, 一直比较到找到不同的相应字符, 搜索可能要进行的字符串结尾处. 而 strncmp() 函数比较字符串时, 可以比较到字符串不同处, 也可以比较完由第三个参数指定的字符数. 例如, 如果想搜索以 "astro" 开头的字符串, 你可以限定搜索前 5 个字符. 程序清单 11.20 示例了这个函数的使用.


程序清单 11.20 starsrch.c 程序
--------------------------------------------------
/* starsrch.c -- 使用 strncmp() 函数 */
#include <stdio.h>
#include <string.h>
#define LISTSIZE 5
int main (void)
{
char *list[LISTSIZE] = { "astronomy", "astounding",
"astrophysics", "ostracize",
"asterism" };
int count = 0;
int i;

for ( i = 0; i < LISTSIZE; i++)
if (strncmp (list[i], "astro", 5) == 0)
{
printf (" Found ; %s \n",list[i]);
count++;
}
printf ("The list contained %d words beginning with astro. \n",count);
return 0;
}

输出如下:

Found ; astronomy
Found ; astrophysics
The list contained 2 words beginning with astro.


-------------------------------------------------------------------------


11.5.6 strcpy() 和 strncpy() 函数

我们已经提到过, 如果 pts1 和 pts2 都是指向字符串的指针, 则下面的表达式只复制字符串的地址而不是字符串本身:

pts2 = pts1;

假定你确实希望复制字符串, 那么可以使用 strcpy() 函数. 程序清单 11.21 要求用户输入以 q 开头的单词. 程序把输入复制到一个临时的数组里, 如果第一个字母是 q ,程序就使用 strcpy() 函数把字符串从临时数组复制到永久的目的地. strcpy() 函数在字符串运算中的作用等价于赋值运算符.


程序清单 11.21 copy1.c 程序
---------------------------------------------------------
/* copy1.c -- strcopy() 示例程序 */
#include <stdio.h>
#include <string.h> /* 声明 strcpy() 函数 */
#define SIZE 40
#define LIM 5

int main (void)
{
char qwords[LIM][SIZE];
char temp[SIZE];
int i = 0;

printf ("Enter %d words beginning with q: \n",LIM);
while ( i < LIM && gets(temp))
{
if (temp[0] != 'q')
printf ("%s doesn't begin with q! \n",temp);
else
{
strcpy(qwords[i], temp);
i++;
}
}
puts ("here are the words accepted : ");
for ( i = 0; i < LIM; i++)
puts(qwords[i]);
return 0;
}

下面是一个运行示例:
Enter 5 words beginning with q:
quackery
quasar
quilt
quotint
no more
no more doesn't begin with q!
quiz
here are the words accepted :
quackery
quasar
quilt
quotint
quiz


请注意只有当输入的单词通过了 q 判断, 计数值 i 才会增加. 还要注意程序使用了一个基于字符的判断:

if (temp[0] != 'q')

这相当于, temp 数组的第一个字符是否不为 q ? 还可以使用一个基于字符串的判断:

if (strncmp (temp, "q", 1) != 0)

这相当于,字符串 temp 和字符串 "q" 的第一个元素是否不同?

注意, 第二个参数 temp 指向的字符串被复制到第一个参数 qword[i] 指向的数组中. 复制的那份字符串被称为目标 (target) 字符串, 最初的字符串被称为源 (source) 字符串. 如果注意到它和赋值语句的顺序一样, 目标字符串在左边, 就容易记住参数的顺序.


char target[20];
int x;
x = 50; /* 数值的赋值 */
strcpy (target, "Hi ho!"); /* 字符串的赋值 */
tatget = "So long"; /* 语法错误 */

确保目标数组对复制源字符串来说有足够大的空间就是你的责任了. 看看下面语句有什么问题:

char *str;
strcpy (str, "The C of Tranquility"); /* 存在一个问题 */

函数将把字符串 "The C of tranquility" 复制到 str 指定的地址中, 但是 str 没有初始化, 因此这个字符串可能被复制到任何地方!


总之, strcpy() 接受两个字符串指针参数. 指向最初字符串的第二个指针可能是一个已声明的指针, 数组名或字符串常量. 指向复制字符串的第一个指针应指向空间大到足够容纳该字符串的数据对象,
比如一个数组. 记住, 声明一个数组将为数据分配存储空间; 而声明一个指针只为一个地址分配存储空间.

 

--------------------------------------------------------------

一 . strcpy() 的高级属性

strcpy()函数还有另外两个有用的属性. 首先, 它是 char *类型, 它返回的是第一个参数的值, 即一个字符的地址; 其次, 第一个参数不需要指向数组的开始, 这样就可以只复制数组的一部分. 程序清单 11.22 举例说明了这两种属性的使用.


程序清单 11.22 copy2.c 程序
-----------------------------------------------
/* copy2.c --- strcpy() 示例程序 */
#include <stdio.h>
#include <string.h> /*声明 string() 函数 */
#define WORDS "beast"
#define SIZE 40

int main (void)
{
char *orig = WORDS;
char copy[SIZE] = "Be the best that you can be";
char *ps;

puts (orig);
puts (copy);
ps = strcpy (copy + 7, orig);
puts (copy);
puts (ps);

return 0;
}

输出如下:

beast
Be the best that you can be
Be the beast
beast

注意, strcpy() 从源字符串复制空字符. 在这个例子中, 空字符覆盖了 that 中的第一个 t ,这样新的字符串就以 beast 结尾 ( 请参见图 11.5) . 还要注意, ps 指向 copy 的第 8 个元素 (索引为 7), 这是因为第一个参数是 copy+7. 因此, puts(ps) 从这个地方开始输出字符串.


----------------------------------------------------------------------------


二 . 较为谨慎的选择: strncpy()

strcpy() 和 gets() 函数同样都有一个问题, 那就是都不检查目标字符串是否容纳得下源字符串. 复制字符串使用 strncpy() 比较安全. 它需要第三个参数来指明最大可复制的字符数. 程序清单 11.23 用 strncpy() 代替了程序清单 11.21 中的 strcpy(). 为了说明源字符串太大会产生的问题, 它使用了一个相当小的目标字符串 ( 7 个元素, 6 个字符 ).


程序清单 11.23 copy3.c 程序
----------------------------------------------
/* copy3.c -- strncpy() 示例程序 */
#include <stdio.h>
#include <string.h> /* 声明 strncpy()函数*/
#define SIZE 40
#define TARGSIZE 7
#define LIM 5

int main (void)
{
char qwords[LIM][TARGSIZE];
char temp[SIZE];
int i = 0;

printf ("Enter %d words beginning with q: \n", LIM);
while ( i < LIM && gets(temp))
{
if (temp[0] != 'q')
printf ("%s doesn't begin with q !\n", temp);
else
{
strncpy(qwords[i], temp, TARGSIZE -1);
qwords[i][TARGSIZE -1] = '\0';
i++;
}
}
puts ("Here are the words accepted :");
for ( i = 0; i < LIM; i++)
puts (qwords[i]);

return 0;
}

下面是一个运行示例:

Enter 5 words beginning with q:
quack
quadratic
quisling
quota
quagga
Here are the words accepted :
quack
quadra
quisli
quota
quagga


函数调用 strncpy(target,source,n) 从 source 把 n 个字符 (或空字符之前的字符, 由二者中最满足的那个决定何时终止) 复制到 target. 因此, 如果源字符串的字符数比 n 小, 整个字符串都被复制过来, 包括空字符. 函数复制的字符数绝不会超过 n , 因此如果源字符串还没结束就达到了限制, 就不会添加空字符. 结果, 最终的字符串可能有也可能没有空字符. 出于这个原因, 程序设置的 n 比目标数组的大小在少 1 . 这样就可以把空字符放到数组中的最后一个元素里.


strncpy ( qwords[i], temp, TARGSIZE - 1);
qwords[i][TARGSIZE - 1] = '\0';

这就确保你已经存储了一个字符串.如果源字符串确实可以容纳得下, 和它一复制的空字符就标志着字符串的真正结束. 如果源字符串在目标数组中容纳不下, 这个最后的空字符就标志着字符串的结束.

 

---------------------------------------------------------------------------------------

11.5.7 sprintf()函数

sprintf() 函数是在 stdio.h 而不是在 string.h 里声明的. 它的作用和 printf() 一样, 但是它写到字符串里而不是写到输出显示. 因此, 它提供了把几个元素组合成一个字符串的一种途径. sprintf() 的第一个参数是目标字符串的地址, 其余的参数和 printf() 一样: 一个转换说明字符串, 接着是要写的项目的列表.


程序清单 11.24 使用 sprintf() 把三个项目 (两个字符串和一个数字) 组合成一个单一的字符串. 注意, 使用 sprintf() 和使用 printf() 的方法一样, 只是结果字符串被存放在数组 formal 中, 而不是被显示在屏幕上 .

 

程序清单 11.24 format.c 程序
----------------------------------------------------
/* format.c -- 格式化一个字符串 */
#include <stdio.h>
#define MAX 20

int main (void)
{
char first[MAX];
char last[MAX];
char formal[2 * MAX +10];
double prize;

puts (" Enter your first name :");
gets (first);
puts (" Enter your last name: ");
gets (last);
puts (" Enter your prize money: ");
scanf ("%lf", &prize);
sprintf (formal, "%s, %-19s; $%6.2f \n",last,first,prize);
puts (formal);

return 0;
}

下面是一个运行示例:

Enter your first name :
Teddy
Enter your last name:
Behr
Enter your prize money:
2000
Behr, Teddy ; $2000.00


sprintf() 命令获取输入, 并把输入格式化为标准形式后存放在字符串 formal 中.

 

 

 

 


11.5.8 其他字符串函数


ANSI C 库有 20 多个处理字符串的函数, 下面的列表总结了其中最常用的一些;
--------------------------------------------------------------------------

1 char *strcpy (char *s1, const char *s2);

该函数把 s2 指向的字符串 (包括空字符) 复制到 s1 指向的位置, 返回值是 s1 .

--------------------------------------------------------------------------

2. char *strncpy (char *s1, const char *s2, size_t n);

该函数把 s2 指向的字符串复制到 s1 指向的位置, 复制的字符数不超过 n 个. 返回值是 s1
空字符后的字符不被复制. 如果源字符串的字符数少于 n 个, 在目标字符串中就以空字符填充. 如果源字符串的字符数大于或等于 n 个, 空字符就不被复制. 返回值是 s1 .

--------------------------------------------------------------------------

3 char *strcar (char *s1, const char *s2);

s2 指向的字符串被复制到 s1 指向字符串的结尾. 复制过来的 s2 所指字符串的第一个字符覆盖了
s1 所指字符串结尾的空字符. 返回值是 s1 .

---------------------------------------------------------------------------

4. char *strncat (char *s1, const char *s2, size_t n);

s2 字符串中只有前 n 个字符被追加到 s1 字符串, 复制过来的 s2 字符串的第一个字符覆盖了 s1
字符串结尾的空字符. s2 字符串中的空字符及其后的任何字符都不会被复制, 并且追加一个空字符到所得结果后面. 返回值是 s1 .

-----------------------------------------------------------------------------

5. int strcmp (const char *s1, const char *s2);

如果 s1 字符串在机器编码顺序中落后于 s2 字符串, 函数的返回值是一个正数; 如果两个字符串相同, 返回值是 0 ; 如果第一个字符串在机器编码顺序中先于第二个字符串, 返回值是一个负数.

-------------------------------------------------------------------------------

6. int strncmp (const char *s1 const char *s2 size_t n);

该函数的作用和 strcmp() 一样, 只是比较 n 个字符后或者遇见第一个空字符会停止比较, 由二者中最先满足的那一个条件终止比较过程.

--------------------------------------------------------------------------------

7. char *strchr (const char *s, int c );

该函数返回一个指向字符串 s 中存放字符 c 的第一个位置的指针 (标志结束的空字符是字符串的一部分, 因此也可以搜索到它 ). 如果没找到该字符, 函数就返回空指针.

--------------------------------------------------------------------------------

8. char *strpbrk (const char *s1, const char *s2 );

该函数返回一个指针, 指向 s1 中存放 s2 字符串中的任何字符的第一个位置. 如果没找到任何字符, 函数就返回空指针.


--------------------------------------------------------------------------------

9. char *strrchr (const char *s, int c );

该函数返回一个指针, 指向字符串 s 中字符 c 最后一次出现的地方 (标志结束的空字符是字符串的一部分, 因此也可以搜索到它). 如果没找到该字符, 函数就返回空指针.


---------------------------------------------------------------------------------

11. char *strstr (const char *s1, const char *s2 );

该函数返回一个指针, 指向 s1 字符串中第一次出现 s2 字符串的地方. 如果在 s1 中没找到
s2 字符串, 函数就返回空指针.

----------------------------------------------------------------------------------

12. size_t strlen (const char *s);

该函数返回 s 字符串中的字符个数, 其中不包括标志结束的空字符.

------------------------------------------------------------------------------------

注意, 这些原型使用关键字 const 来指出哪个字符串是函数不能改动的. 例如, 考虑下面这个原型:

char *strcpy (char *s1 , const char *s2);

这意味着 s2 指向一个不可改变的字符串, 至少 strcpy() 函数不会改变它, 但是 s1 指向的字符串却可以改变. 这是因为, s1 是需要改变的目标的字符串, 而 s2 是不应当有改变的源字符串.


第 5 章 "运算符, 表达式和语句" 中已经讨论过, size_t 类型是 sizeof 运算符返回的任何类型.C 规定 sizeof 运算符返回一个整数类型, 但是没有指定是哪种整数类型. 因此 size_t 在一个系统上可以是 unsigned int 类型; 在另一个系统上, 又可以是 unsigned long 类型. srting.h 文件为你的特定系统定义了 size_t ,或者你可以参考其他有该定义的头文件.


前面已经提到过, 参考资料 5 中列出了 string.h 系列中所有的函数. 除了 ANSI 标准要求的那些, 很多 C 实现还提供了其他一些函数. 应该查看你的 C 实现的文档以了解可以使用哪些函数.


让我们看一下这些函数其中一个的简单使用. 前面你已学习了 fgets() 函数. 在读取一行输入时, 这个函数把换行符存储到目标字符串中. 可以使用 strchr() 函数来用一个空字符代替这个换行符.
首先, 使用 strchr() 找到换行符 (如果有的话). 如果找到了, 函数就返回这个换行符的地址, 于是就可以在该地址放一个空字符:

char line[80];
char *find;

fgets (line, 80, stdin);
find = strchr (line, '\n'); // 查找换行符
if (find) // 如果该地址不为 NULL,
*find = '\0'; // 就把一个空字符放在这里


如果 strchr() 没有找到换行符, 说明 fgets() 在行末结束时就达到了大小限制. 你可经给 if 加个 else 来处理这种情况.


接下来, 我们看一个处理字符串的完整程序.

 

 

11.6 字符串例子: 字符串排序


我们来解决一个把字符串按字母表顺序排序的实际问题. 准备花名册, 建立索引以及很多其他情况下都会用到字符串的排序. 这个程序的一个主要工具就是 strcmp(), 因为可以使用这个函数来决定两个字符串的顺序. 一般的做法是读一个字符串数组, 对它们进行排序并输出. 先前, 我们给出了一个读字符串的方案, 我们就按那个方案开始该程序. 输出字符串不会有什么问题. 程序使用的标准排序算法, 后面会进行解释. 我们在其中采用了一个技巧, 看看你能否弄明白它. 程序清单 11.25 给出了程序.


程序清单 11.25 sort_str.c 程序
--------------------------------------------------------------------
/* sort-str.c --- 读进一些字符串并对它们排序 */
#include <stdio.h>
#include <string.h>
#define SIZE 81 /* 字符串长度限制,包括\0 */
#define LIM 20 /* 最多读取的行数 */
#define HALT " " /* 用空字符终止输入 */
void stsrt (char *strings[], int num); /* 字符串排序函数 */

int main (void)
{
char input[LIM][SIZE]; /* 存储输入的数组 */
char *ptstr[LIM]; /* 指针变量的数组 */
int ct = 0; /* 输入计数 */
int k; /* 输出计数 */

printf ("Input up to %d lines, and I will sotr them.\n",LIM);
printf ("To stop, press the Enter key at a line's start.\n");
while (ct < LIM && gets(input[ct]) != NULL && input[ct][0] != '\0')
{
ptstr[ct] = input[ct]; /* 令指针指向输入字符串 */
ct++;
}
stsrt (ptstr, ct); /* 调用字符串排序函数 */
puts ("\nHere's the sorted list : \n");
for (k = 0; k < ct; k++)
puts (ptstr[k]); /* 排序后的指针 */

return 0;

}

/* 字符串 - 指针 - 排序函数 */
void stsrt (char *strings[], int num)
{
char *temp;
int top, seek;

for (top = 0; top < num-1; top++)
for (seek = top+1; seek < num; seek++)
if (strcmp(strings[top],strings[seek]) > 0)
{
temp = strings[top];
strings[top] = strings[seek];
strings[seek] = temp;
}
}

对于程序清单 11.22, 我们用一首咿咿呀呀的儿歌来测试

Input up to 20 lines, and I will sotr them.
To stop, press the Enter key at a line's start.
0 that I was where I would be.
Then would I be where I am not;
But where I am Imust be,
And where I woulb be I can not.


Here's the sorted list :

0 that I was where I would be.
And where I woulb be I can not.
But where I am Imust be,
Then would I be where I am not;


嗯, 看起来这首儿歌经过排序后似乎没有什么变化. 生活

------------------------------------------------------------------

11.6.1 排序指针而不是字符串


程序的技巧部分在于它并不是重新安排字符串本身, 而仅仅重新安排指向字符串的指针. 让我们解释一下. 起初, ptrst[0] 被赋值为 iput[0], 等等.


这就是说指针 ptrst[i] 指向数组 input[i] 的第一个字符. 每个 input[i] 都是一个含 81 个元素的数组, 而 每个 ptrst[i] 都是一个变量. 排序过程重新安排 ptrts, 而不改变 input. 例如,


如果用 input[1] 在字母表中先于 input[0], 程序就交换 ptrsts, 使 ptrst[0] 指向 input[1] 的开始, 使 ptrst[1] 指向 input[0] 的开始.


这要比使用 strcpy() 来交换两个 input 字符串的内容简单多了. 图 11.6 是这个过程的另一种表示. 这种方法的优点还在于保留了原始的字符串顺序.


----------------------------------------------------------------


11.6.2 选择排序算法


我们使用了选择排序 (selection sort) 算法来进行指针排序. 其思想是使用一个 for 循环把每个元素轮流与第一个元素比较. 如果被比较元素在顺序上先于当前第一个元素, 程序就交换这二者. 程序执行到循环结束时, 第一个元素包含的指针指向在机器编码顺序中排在第一个的字符串. 然后外部的 for 循环重复这个过程, 这次是以 input 的第二个元素作为开始元素. 内部循环完成时, ptrst 的第二个元素包含的指针就指向顺序排第二的字符串. 这个过程一直继续下去, 直到所有的元素都已经排好序.


现在再仔细看一下选择排序. 下面是用伪代码形式表示的纲要:


for n = first to n = next - to - last element,
find largest remaining number and place it in the nth element

流程是这样的: 首先以 n = 0 开始. 扫锚整个数组, 找出最大的数, 把它和第一个元素交换位置. 然后设 n = 1, 扫描数组第一个元素以外的其他元素, 找出剩余数中的最大数, 并把它和第二元素交换.
继续这个过程, 直到倒数第二个元素为止. 现在只剩下两个元素, 比较它们并把较大的一个放在倒数第二个位置上. 最小的元素就放在最后一个位置上.


这看起来像是一个 for 循环可以完成的任务, 但我们还必须更详细地描述这个"查找和放置" 的过程.
选择剩余最大值的一个办法就是比较剩余数组的第一和第二个元素. 如果第二个元素大, 就交换这两个数据. 现在比较第一个和第三个元素. 如果第三个大, 就交换这两个数据. 每一次交换都把大的元素移到上面. 继续这种方法, 直到比较第一个和最后一个元素. 完成以后, 最大的数就在剩余数组的第一个元素中. 此时第一个元素已经排好了序, 但是数组中的其他元素还很混乱. 下面是该过程的伪代码:


for n - second element to last element,
compare nth element with first element; if nth is greater, swap values


这个过程看起来也像是一个 for 循环可以完成的任务, 它会被嵌套在第一个 for 循环中. 外部循环表明要填充哪一个数组元素, 内循环找出该数组元素中要放置的值. 把这两部分的伪代码结合在一起, 并翻译成 C , 我们就得到了程序清单 11.25 中的函数. 顺便提一下, C 库包含一个更高级的排序函数 qsotr(), 它使用一个指向函数的来进行排序比较. 第 16 章 "C 预处理器和 C 库" 给出了这一应用的例子.


 

 

11.7 ctype.h 字符函数和字符串

第 7 章 " C 控制语句: 分支和跳转" 介绍了 ctype.h 系列字符相关的函数. 这些函数不能被应用于整个字符串, 但是可以被应用于字符串中的个别字符. 例如, 程序清单 11.26 定义了一个函数, 它把 toupper() 函数应用于一个字符串中的每个字符, 这样就可以把整个字符串转换为大写. 此外, 程序还定义了一个使用 isputct() 函数计算一个字符串中的标点字符个数和的函数.

程序清单 11.26 mod_str.c 程序
---------------------------------------------------------------------
/* mod_str.c -- 修改一个字符串 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define LIMIT 80
void ToUpper (char *);
int PunctCount (const char *);

int main (void)
{
char line[LIMIT];

puts ("Please enter a line");
gets (line);
ToUpper (line);
puts (line);
printf ("That line has %d punctuation characters. \n",PunctCount(line));

return 0;
}

void ToUpper (char *str)
{
while (*str)
{
*str = toupper (*str);
str++;
}
}

int PunctCount (const char *str)
{
int ct = 0;
while (*str)
{
if (ispunct (*str))
ct++;
str++;
}
return ct;
}


循环 while (*str) 处理 str 指向的字符串中的每个字符, 直到遇见空字符. 当遇到空字符时, *str 的值变为 0 (空字符的编码值), 即为假, 则循环结束. 下面是一个运行示例:

Please enter a line
Me? You talkin' to me? get outta here!
ME? YOU TALKIN' TO ME? GET OUTTA HERE!
That line has 4 punctuation characters.


ToUpper() 函数把 toupper() 应用于字符串中的每个字符 (由于 C 区分大小写, 所以这是两个不同的函数名). 正如 ANSI C 所定义的, toupper() 函数只改变小写字符. 然而, C 的一些很旧的版本不进行自动检查, 因此旧的代码通常会这样做:

if (islower (*str)) /* ANSI C 之前的做法 -- 转换之前先检查 */
*str = toupper (*str);

顺便提一下, ctype.h 函数通常被作为宏 (macro) 来实现. 这些 C 预处理器指令的作用很像函数, 但是有一些重要差别. 在第 16 章 " C 预处理器和 C 库" 中我们会介绍宏.


接下来, 我们讨论 main() 的圆括号里的 void

 

11.8 命令行参数

现代的图形界面出现之前是命令行界面. DOS 和 UNIX 就是例子. 命令行 (command line) 是一一个命令行环境下, 用户输入的用于运行程序的行. 假定有一个程序在名为 fuss 的文件中, 那么在 UNIX 下运行该程序的命令行如下:

$ fuss

或者在 windows 命令行模式下, 如 windows XP 命令提示符:

C > fuss

命令行参数 (command-line argument) 是同一行中的附加项. 如下例:

% fuss -r Ginger

一个 C 程序可以读取这些附加项为自己所用 (请参见图 11.7).

C 程序通过使用 main() 参数读取这些项目. 程序清单 11.27 给出了一个典型例子.


程序清单 11.27 repeat.c 程序
---------------------------------------------------------------
/* repeat.c -- 带有参数的 main() 函数 */
#include <stdio.h>
int _t main(int argc, _TCHAR* argv[])
{
int count;

printf ("the command line has %d arguments: \n", argc-1);
for (count = 1; count < argc; count++)
printf ("%d; %s \n",count,argv[count]);
printf ("\n");

return 0;
}

把这个程序编译为可执行文件 repeat; 下面是从命令行运行该程序的结果:

C:\>repeat resistance is futile
the command line has 3 arguments:
1; Resistance
2; is
3; futile

可以看出为什么该程序被称为 repeat, 但是你可能想知道它是怎么工作的. 现在我们解释一下.

C 编译器允许 main() 没有参数, 或者有两个参数 (有些实现允许更多的参数, 但这将是对标准的扩展). 有两个参数时, 第一个参数是命令行中的字符串数. 按照惯例 (但不是必须的), 这个 int 参数被称为 argc (代表 argument count ). 系统使用空格判断一个字符串结束, 另一个字符串开始. 因此, repeat 例子中包括命令名在内有 4 个字符串, fuss 例子有 3 个. 第二个参数是一个指向字符串的指针数组. 命令行中的每个字符被存储在内存中, 并且分配一个指针指向它. 按照惯例, 这个指针数组被称为 argv (代表 argument value). 如果可以 (有些操作系统不允许这样做), 把程序本身的名字赋值给 argv[0]. 接着, 把随后的第一个字符串赋给 argv[1] ,等等. 对于我们的例子, 有表 11.1 所示的关系:


表 11.1 main() 的第二个参数
--------------------------------------------------------------------------------
argv[0] 指向 repeat(对于大多数系统)
---------------------------------------------------------------------------------
argv[1] 指向 resistance
---------------------------------------------------------------------------------
argv[2] 指向 is
---------------------------------------------------------------------------------
argv[3] 指向 rutile
---------------------------------------------------------------------------------

程序清单 11.27 中的程序使用一个 for 循环来依次输出每个字符串. 回忆一下, printf() 的 %s 说明符需要提供字符串的地址作为参数. 每个元素, argv[0], argv[1] 等等, 正是一个这样的地址.


该形式和有形式的其他函数一样. 很多程序员使用不同的方式声明 argv:

int main (int argc, char **argv)

这种对 argv 的声明和 char *argv[]等价. 它意味着 argv 是一个指向 "指向字符的指针" 的指针.

示例程序中那种形式的效果也一样. 它有一个包含几个元素的数组. 数组名是指向第一个元素的指针,
因此 argv 指向 argv[0], 而 argv[0] 是一个指向字符的指针. 因此, 即使在原始的定义中, argv 仍是一个指向 "指向字符的指针" 的指针. 两种形式都可以用, 但我们认为第一种形式更清楚地表明 argv 代表一系列字符串.


顺便提一下, 很多环境 (包括 UNIX 和 DOS) 允许使用引号把多个单词集中在一个参数里. 例如:

repeat "I am hungry" now

这个命令会把字符串 " I am hungry" 分配给 argv[1], 把字符串 "now" 分配给 argv[2] .

 


---------------------------------------------------------------------------------

11.8.1 集成环境下的命令行参数


集成的 Windows 环境, 比如 Metrowerks CodeWarrior, Microsoft Visual C++ 和 Borland C/C++, 都不使用命令行运行程序. 然而, 有些环境有菜单选择, 可以让你指定命令行参数. 其他情况下, 你可以在 IDE 中编译程序, 然后打开 MS-DOS 窗口用命令行模式运行程序.


11.9 把字符串转换为数字


数字既能以字符串形式也能以数字形式存储. 以字符串形式存储数字就是存储数字字符. 例如, 数字 213 能以数字 '2' '1' '3' '\0' 的形式存储在一个字符串数组中. 以数字形式存储 213 意味着把它存储为一个 int 数值.


对于数字运算 (比如加法运算和比较运算) C 要求数字形式. 但是在屏幕上显示数字却要求字符串形式, 这是因为屏幕显示的是字符. printf() 和 sprintf() 函数通过 %d 或其他说明符把数字形式转换为字符串形式或者相反. C 还有一些函数专门用于把字符串形式转换为数字形式.


例如, 假定你想编写一个使用数字命令行参数的程序. 很不巧的是, 命令行参数是以字符串形式被读取的. 因此, 要想使用数字值, 就必须先把字符串转换为数字, 如果数字是整数, 那就可以使用 atoi() (代表 alphanumeric to integer ) 函数. atoi()函数以字符串为参数, 返回相应的整数值. 程序清单 11.28 就是个例子.


程序清单 11.28 hello.c 程序
------------------------------------------------------------
/* hello.c -- 把命令行参数转换为数字 */

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

int main (int argc, char *argv[])
{
int i, times;

if ((argc < 2) || (times = atoi(argv[1])) < 1)
printf ("Usage: %s positive - number \n",argv[0]);
else
for (i = 0; i < times; i++)
puts ("Hello,good looking !");

return 0;
}

下面是一个运行实例:

% hello 3
Hello, good looking!
Hello, good looking!
Hello, good looking!

VC6.0直接输出了printf ("Usage: %s positive - number \n",argv[0]);

% 是 UNIX 提示符. 命令行参数 3 以字符串 "3\0" 的形式存放. atoi() 函数把这个字符串转换为整数 3, 然后把 2 赋给 times. 这样就决定了 for 循环执行的次数.


如果运行程序时没有提供命令行参数, argc < 2 判断就会中断程序并给出一个提示信息. 如果 times 为 0 或为负, 也会发生同样的情况. C 的逻辑运算符的运算顺序规则确保: 如果 argc < 2 ,就不再运算 atoi(argv[1]),


如果字符串只是以一个整数作为开头, atoi() 函数仍然可以工作. 在这种情况下, atoi() 函数在遇到非整数部分之前一直转换字符. 例如, atoi("42regular") 返回整数 42. 如果命令行类似 hello what , 又会出现什么情况呢? 在我们使用的 C 实现上, 如果参数不能识别为数字, atoi()函数返回一个 0 值. 但 ANSI 标准规定, 上述情况下的行为是末定义的. 稍后我们将简要介绍 strtol()函数可以提供更可靠的错误检测.


我们包含了 stdlib.h 头文件, 这是因为在 ANSI C 中这个文件包含了 atoi() 函数的声明. 此外, 这个头文件还包含了 atof() 和 atol() 函数的声明. atof() 函数把一个字符串转换为 double 类型的值, atol() 函数则把字符串转换为 long 类型的值. 他们的作用和 atoi() 类似, 因此分别为 double 和 long 类型.


ANSI C 提供了这些函数的更复杂版本: strtol(), strtoul() 和 strtod(), 其中 strtol()函数把一个字符串转换为 long 型值, strtoul()函数把一个字符串转换为 unsigned long 型值, strtod()把一个字符串转换为 double型值. 这些函数的复杂性在于它们还可以识别并报告字符串中非数字部分的第一个字符. strtol() 和 strtoul() 函数还允许你指定数字的基数.


long strtol (const char *nptr, char **endptr, int base);

在这里, nptr 是一个指向你希望转换的字符串的指针, endptr 是指向标志输入数字的结束字符的指针地址,
base 是数字的基数. 程序邓小平是 11.29 中的例子更清楚地表明了这些.

 

程序清单 11.29 strcnvt.c 程序
-------------------------------------------------------------------
/* strcnvt.c --- 尝试使用 strtol()函数 */
#include <stdio.h>
#include <stdlib.h>

int main ()
{
char number[30];
char *end;
long value;

puts ("Enter a number (emppty line to quit) ;");
while (gets(number) && number[0] != '\0')
{
value = strtol (number, &end ,10);
printf ("value : %ld, stopped at %s(%d)\n",value,end,*end);
value = strtol (number, &end, 16); /* 基于 16 */
printf ("value : %ld, stopped at %s(%d)\n",value,end,*end);
puts ("Next number:");
}
puts ("Bye \n");
return 0;
}


下面是一些输出示例:

Enter a number (emppty line to quit) ;
10
value : 10, stopped at (0)
value : 16, stopped at (0)
Next number:
10atom
value : 10, stopped at atom(97)
value : 266, stopped at tom(116)
Next number:

Bye

首先请注意: 如果基数是 10 ,字符串"10"就被转换为 10; 如果基数是 16, 字符串"10"就被转换为 16.

还要注意, 如果 end 指向一个字符, 那么 *end 就是一个字符. 因此, 第一次转换在遇到空字符时结束, 这样 end 就指向空字符. 如果输出 end, 会显示一个空字符串, 如果 %d 格式输出 *end ,就会显示空字符的 ASCII 码.


对于输入的第二个字符串 (以 10 为基数进行解释), end 是 'a' 字符的地址. 因此, 输出 end 显示的是字符串 "atom", 输出 *end 显示的则是 'a'字符的 ASCII 码. 但是, 如果基变为 16, 'a' 字符就会识别为一个有效的十六进制数字, 函数会把十六进制数 10a 转换为十进制的 266 .


strtol() 函数最多可以有三十六进制, 使用一直到 'z' 的字母作为数字. strtoul() 函数也一样, 但它转换的是无符号值. strtod() 函数只按照十进制进行转换, 因此它只使用两个参数.


很多实现中都用 itoa() 和 ftoa() 函数把整数和浮点数转换为字符串. 但是, 这两个函数并不是 ANSI C 库里的函数; 如果要求兼容性更好, 可以使用 sprintf(0 函数来完成这些功能.

 

 

11.10 关键概念


很多程序都需要处理文件数据. 一个程序可能会要求你输入姓名, 公司列表, 地址, 某种蕨类植物的名称, 乐曲名称等等, 毕竟我们是用语言和这个世界打交道, 使用文本的例子多得不计其数. 字符串就是 C 程序处理它们的方式.


C 的字符串, 无论是用字符数组还是指针或字符串文字定义的, 都是以包含字符编码的一系列字节形式存放, 并以空字符为结束标志. C 通过提供一个函数库对字符串进行操作, 搜索和分析来实现字符串的广泛用途. 特别地, 一定要记住在比较字符串时, 应该用 strcmp() 函数而不是用关系运算符; 应该用 strcpy() 或 strncpy() 函数, 而不是用赋值运算符来把字符串赋值给字符数组.

 


11.11 总结


C 字符串是一串以空字符 '\0' 结束的 char 类型值. 字符串可以存放在字符数组中, 也可以用字符串常量表示. 在字符串常量中, 字符 (除了空字符) 是被包含在双引号中的. 编译器为它加上空字符. 因此, 存储 "joy" 时有 4 个客人: j o y 和 \0 . strlen() 函数没得的字符串长度不包括空字符.


字符串常量, 又叫做字符串文字, 可以用来初始化字符数组. 数组大小至少应该比字符串长度大 1, 这样才能存放空字符. 字符串常量还可以用来初始化指向 char 的指针.


函数利用指向字符串第一个字符的指针来标识它所作用的字符串. 通常, 相应的实际参数可以是数组名, 指针变量或引号中的字符串. 这些情况下, 传递的都是第一个字符的地址. 一般来说, 并不需要传递字符串的长度, 因为可以根据标志结束的空字符来确定字符串的结束.


gets() 和 puts() 函数分别读取一行输入和进行一行输出. 这两个函数都是 stdio.h 系列里的函数.


C 库里有许多处理字符的函数. 在 ANSI C 中, 这些函数都是在 string.h 文件中声明的. C 库里还有一些处理字符的函数, 它们是在 ctype.h 文件里声明的.


你可以通过给 main() 提供两个形式合适的变量来使程序获得命令行参数. 第一个参数通常被称为 argc, 是一个整数数, 其值是命令行的单词个数. 第二个参数通常被称为 argv , 是一个指针, 指向一个 char 指针数组.
每个指向 char 的指针指向一个命令行参数字符串: argv[0] 指向命令名, argv[1] 指向第一个命令行参数, 等等


atoi() atol() 和 atof() 函数分别把数字的字符口中表示转换为 int long 和 double 形式. strtol()
strtoul() 和 strtod() 函数分别把数字的字符口中表示转换为 long unsigned long 和 double 形式

11.12 复习题

----------------------------------------------------------------
1. 下面这个字符串的声明错在哪里?

int main (void)
{
char name[] = {'F','e','s','s'};
...
}

答: 如果想得到一个字符串, 就应该在初始化中包括一个 '\0' .当然, 另一种语法可以自动添加空字符:
char name[] = "Fess";

---------------------------------------------------------------

2. 下面这个程序会打印出什么?

#include <stdio.h>
int main (void)
{
char note[] = "See you at the snack bar .";
char *ptr;

ptr = note;
puts (ptr);
puts (++ptr);
note[7] = '\0';
puts (note);
puts (++ptr);
return 0;
}
答:
See you at the snack bar .
ee you at the snack bar.
See you
e you

---------------------------------------------------------------------------

3. 下面这个程序会打印出什么?

#include <stdio.h>
#include <string.h>
int main (void)
{
char food[] = "Yummy";
char *ptr;

ptr = food + strlen(food);
while (--ptr) >= food)
puts (ptr);
return 0;
}

答:
y
my
mmy
ummy
Yummy

---------------------------------------------------------------------------

4. 下面这个程序会打印出什么?

#include <stdio.h>
#include <string.h>
int main (void)
{
char goldwyn[40] = "art of it all";
char samuel[40] = " I read p";
char *quote = "the way through";

strcat (goldwyn, quote);
strcar (samuel, goldwyn);
puts (samuel);
return 0;
}

答: I read part of it allthe way through

--------------------------------------------------------------------------------

5. 这个练习涉及到了字符串, 循环, 指针和指针增量的使用. 首先, 假设已经定义了下面的函数:

#include <stdio.h>
char *pr (char *str)
{
char *pc;

pc = str;
while (*pc)
putchar (*pc++);
do {
putchar (*--pc);
} while (pc - str);
return (pc)
}

考虑下面的函数调用;

x = pr("Ho Ho Ho !");

a. 会打印出什么? Ho Ho Ho !!oH oH oH

b. x 是什么类型? 指向 char 的指针类型 char *x

c. x 值等于是多少? 第一个 H 的地址, 即 pr[0] ;

d. 表达式 *--pc 是什么意思? 它和 --*pc 有什么不同?

/* *--pc 把指针减 1 并使用那里的值. --*pc 取出 pc 指向的值然后把那个值减 1 (例如把 H 变为 G) */

e. 如果用 *pc-- 代替 *--pc, 会打印出什么? Ho Ho Ho!! oH oH o

/* 在 ! 和 ! 之间有一个空字符, 但是它不产生任何打印效果. */

f. 两个 while 表达式有什么判断功能?

/* while(*pc) 检查 pc 是否指向一个空字符 (也就是说字符串的结尾). 这个表达式使用指针所指向位置的值.

while (pc-str) 检查 pc 是否与 str 指向同一个地址 (字符串的开始). 这个表达式使用指针本身的值. */

g. 如果 pr() 函数的参数是一个空字符串, 会有什么结果?

/* 在第一个 while 循环之后, pc 指向空字符, 进入第二个循环后令它指向空字符之前的存储区, 也就是说 str 指向的位置之前的位置, 把那个字节解释为一个字符进行打印. 然后指针再退回前面的字节处. 永远都不会满足终止条件 (pc == str), 所以这个过程会一直继续下去. */

h. 怎样调用函数 pr()才能实现所示的功能?

必须在调用程序中对 pr() 进行声明: char *pr (char *);


-----------------------------------------------------------------------------------

6. 假定有下列声明:

char sign = '$'; sign 的存储需要多少字节? '$' 呢? "$" 呢?

答: 字符变量占用一个字节, 所以 sign 占用一个字节. 但是字符常量是被存储在一个 int 中的, 也就是说 '$'通常会使用 2 个或 4 个字节; 但是实际上只使用 int 的一个字节来存储 '$' 的编码. 字符串 "$" 使用两个字节, 一个用来保存 '$' 另一个字节用来保存 '\0' .

------------------------------------------------------------------------------------

7. 下面的程序会打印出什么?

#include <stdio.h>
#include <string.h>
#define M1 "How are ya, sweetie ?";
char M2[40] = "Beat the clock.";
char *M3 = "chat";
int main (void)
{
char words[80];
printf (M1);
puts (M1);
puts (M2);
puts (M2 + 1);
strcpy (words,M2);
strcat (words, " Win a toy '");
puts (words);
words[4] = '\0';
puts (words);
while (*M3);
puts (M3++);
puts (--M3);
puts (--M3);
M3 = M1;
puts (M3);
return 0;
}

答:
How are ya, sweetie ?How are ya, sweetie ?
Beat the clock.
eat the clock.
Beat the clock. Win a toy '
Beat
chat
hat
at
t
t
at
How are ya, sweetie ?

------------------------------------------------------------------------------

8. 下面程序会打印出什么?

#include <stdio.h>
int main (void)
{
char strl[] = "gawsie";
char str2[] = "bletonism";
char *ps;
int i = 0;

for (ps = str1; *ps != '\0'; ps++){
if (*ps == 'a' || *ps == 'e')
putchar (*pc);
else
(*ps)--;
putchar(*ps);
}
putchar ('\n');
while ( str2[i] != '\0'){
printf ("%c", i % 3 ? str2[i] : '*');
++i;
}
return 0;
}

答: faavrhee
*le*on*sm

-----------------------------------------------------------------------

9. strlen() 函数需要一个指向字符串的指针作为参数, 并返回字符串的长度. 自己编写这个函数.


答:

int strlen (const char *s)
{
int ct;

while ( *s++ != '\0')
ct++;
return (ct);
}

----------------------------------------------------------------------------------

10. 设计一个函数. 其参数为一个字符串指针, 并且返回一个指针, 该指针指向字符串中所指位置后(包括该位置) 的处一个空格字符. 如果找不到空格字符,就返回指针.


答:

下面是第一种方案
#include <stdio.h> // 提供 NULL 的定义
char *strblk (char *string)
{
while (*string != ' ' && *string != '\0')
string++l // 在第一个空格或空字符处停止
if (*string == '\0')
return NULl; // 返回空指针
else
return string;
}

下面是第二种方案, 它防止函数修改字符串, 但是允许使用返回值来改变字符串. 表达式 (char *)string
被称为 " 使用类型指派取消 const".

#include <stdio.h>
char *strblk (const char *string)
{
while (*string != ' ' && *string != '\0')
string++;
if (*string == '\0')
return NULL;
else
return (char *)string;


-------------------------------------------------------------------------

11. 用 ctype.h 中的函数重写程序清单 11.17 中的程序, 使得不管用户选择的大与还是小写, 程序都可以识别正确答案.

答:

#include "stdafx.h"
#define ANSWER "GRANT"
#define MAX 40
void ToUpper (char *str);
int main (void)
{
char try1[MAX];
puts ("Who is buried in Grant's tomb?");
gets (try1);
ToUpper(try1);
while (strcmp(try1, ANSWER) != 0 )
{
puts ("No, that's wrong, try again.");
gets (try1);
ToUpper(try1);
}
puts ("That's right!");
return 0;
}

void ToUpper (char *str)
{
while (*str != '\0')
{
*str = toupper(*str);
str++;
}
}

 


 

 

 

 


 

 

11.13 编程练习

----------------------------------------------------------------------------------------------------

1. 设计并测试一个函数, 可以从输入读取 n 个字符 (包括空格, 制表符和换行符), 把结果存储在一个数组中, 这个数组的地址通过参数来传递.

解:
#include "stdafx.h"
#define SIZE 10
char *teststr (char *str, int n);
int main (void)
{
char input[SIZE];
char *ch;

ch = teststr (input,SIZE - 1);
if ( ch == NULL)
puts ("Input failed");
else
puts (input);
puts ("Bey !");

return 0;
}

char *teststr (char *str, int n)
{
int i;
char ch;

for (i = 0; i < SIZE; i++)
{
ch = getchar();
if ( ch != EOF)
str[i] = ch;
else
break;
}
if ( ch == EOF)
return NULL;
else
{
str[i] = '\0';
return str;
}
}


注: 这是官方源码... 头晕晕的 不知道是对字符串数组迷糊还是天气原因 都提不起精神...

------------------------------------------------------------------------------------------------

2. 修改并测试练习 1 中的函数, 使得可以在 n 个字符后, 或第一个空格, 制表符, 换行符后 停止读取输入, 由上述情况中最先被满足的那个终止读取 ( 不能用 scanf() 函数).

解:

#include "stdafx.h"
#define SIZE 10
char *teststr (char *str, int n);
int main (void)
{
char input[SIZE];
char *ch;

ch = teststr (input,SIZE - 1);
if ( ch == NULL)
puts ("Input failed");
else
puts (input);
puts ("Bey !");

return 0;
}

char *teststr (char *str, int n)
{
int i;
char ch;

for (i = 0; i < SIZE; i++)
{
ch = getchar();
if ( ch != EOF || !isspace(ch))
str[i] = ch;
else
break;
}
if ( ch == EOF)
return NULL;
else
{
str[i] = '\0';
return str;
}
}

---------------------------------------------------------------------------------
3. 设计并测试一个函数, 其功能是读取输入行里的第一个单词到数组, 并丢掉该行中其他的字符. 一个单词的定义是一串字符, 其中不含空格, 制表符和换行符.

解:
#include "stdafx.h"
#define SIZE 100 // 设置数组元素常量
char *getword (char *str); // 函数定义
int main (void)
{
char input[SIZE]; // 定义存放数组

puts("Plsae ipnut words");
while (getword(input) != NULL) // 如果调用函数结果不为空 即不为 0
puts(input); // 输出字符数组内容
puts("Bey. \n");
return 0;
}

char *getword (char *str)
{
char ch;
int i;

while (( ch = getchar()) != EOF && !isspace(ch)) // 如果 ch 不为 EOF 和 不为其他字符
*str++ = ch; // 将字符放进字符数组里后指针移动到下一个位置
*str = '\0'; // 跳出输入后 在数组指针指向的最后一个位置 放一个空字符 表示结束
if (ch == EOF) // 如果 ch 等于 EOF 返回为空
return NULL;
else
{
while (ch != '\n') // 如果 ch 不等于换行
ch = getchar();
return str;
}
}

-------------------------------------------------------------------------------------------

4. 设计并测试一个函数, 其功能是搜索由函数的第一个参数指定的字符串,在其中查找由函数的第二个参数指定的字符的第一次出现的位置. 如果找到, 返回指向这个字符的指针; 如果没有找到, 返回空字符 (这种方式和 strchr() 函数功能一样). 在一个使用循环语句为这个函数提供输入的完整程序中进行测试 .

解:
#include "stdafx.h"
#define LEN 80
int charlook (const char *str, char ch);
int main (void)
{
char input[LEN];
char ch;
int found;

puts ("Enter a string : ");
while (gets(input) && input[0] != '\0') // 循环输入
{
puts ("Enter a charcter :");
ch = getchar();
while (getchar() != '\n') // 如果获取不等于换行 如果等换行跳出循环
continue; // 继续执行语句
found = charlook(input,ch); // 函数返回赋给 found
if (found == 0) // 如果返回为 0
printf ("%c not found in string .\n",ch);
else
printf ("%d found in string %s \n",found+1, input); // found+1 是因为数组元素[0]开始
puts ("next string :"); // 重新执行循环
}
puts ("Bey \n");
return 0;
}

int charlook(const char *str, char ch)
{
int count = 0;
while (*str != ch && *str != '\0') //如果数组元素指针不等于 ch 或 不是空字符
{

str++; // 指向下一个数组元素
count++; // 如果不满足循环条件 计数++
}
if (*str == ch) // 如果数组元素有元素等于 ch
return count; // 返回数组元素的序列
else
return NULL; // 否则返回为空
}

注: 有点悲剧, 弄了好头天, 要就是 main 正常而 函数 不正常, 最终还是参照了官方源码 第 5 题 修改的
主要还是经验, 就是应用的问题, 看来以后要加注释了, 之前的代码不加注释一看就很明白了, 这个阶段的
代码 看着有点晕, 不过实际上还是经验的问题. 如果是类似的题目, 基本就能解决了.


------------------------------------------------------------------------------------


5. 编写一个函数 is_within(). 它接受两个参数, 一个是字符, 另一个是字符串指针. 其功能是如果在字符串中,就返回一个非 0 值 (真); 如果字符不在字符串中, 就返回 0 值(假). 在一个使用循环语句为这个函数提供输入的完整程序中进行测试


解:

#include "stdafx.h"
#define LEN 80
int is_within (const char *str, char ch);
int main (void)
{
char input[LEN];
char ch;
int found;
puts ("Enter a string : ");

while (gets(input) && input[0] != '\0')
{
puts ("Enter a character : ");
ch = getchar();
while (getchar() != '\n')
continue;
found = is_within(input,ch);
if (found == 0)
printf ("%c not found in string . \n",ch);
else
printf ("%c found in string %s \n",ch, input);
puts ("Next string :");
}
puts (" Bey \n");
return 0;
}

int is_within(const char *str, char ch)
{
while (*str != ch && *str != '\0')
str++;
return *str;
}

注: 还是不行啊, 这是官方源码, 偶认为是练习熟悉度的问题.. 以后会好的
------------------------------------------------------------------------------------------

6. strncpy(s1,s2,n) 函数从 s2 复制 n 个字符给 s1, 并在必要时截断 s2 或为其填充额外的空字符. 如果 s2 的长度等于或大于 n . 目标字符串就没有标志结束的空字符. 函数返回 s1 . 自己编写这个函数, 并在一个使用循环语句为这个函数提供输入的完整程序中进行测试.


解:

#include "stdafx.h"
#define LEN 80
char *mystrcpy (char *str, char *str1, int count);
int main (void)
{
char input[LEN];
char aim[LEN];
char *p;
int i;

puts ("Enter first string : ");
while (gets(input) && input[0] != '\0') // 输入字符串 1 循环 无输入退出
{
puts ("Enter secondly string :");
{
while (gets(aim) && aim[0] != '\0') // 输入字符串 2 循环 无输入退出
{
puts ("Enter a Len");
scanf("%d", &i);
p = mystrcpy(input,aim,i); // 调用函数 并把返回值赋给 指针 p
printf ("reult %s \n",p); //显示结果
puts ("Next first string :"); // 重新开始循环
}
}
}
puts("Bey \n");
return 0;
}

char *mystrcpy(char *str, char *str1, int count )
{
char *temp = str; // 声明一个指针指向 传递的数组
int len = strlen(str1); // 计算 str1 长度并赋给 len

if (len < count) // 如果 str1 长度小于 count
return str; // 返回 str
else
{
while (*temp != '\0') // 如果指针不是指向数组末尾, 指针++ 直到指向
temp++;
for (int i = 0; i < count; count--) // 利用计数变量增减 将 str1元素 放进 str 元素
*temp++ = *str1++;

if (*temp != '\0') // 如果 str 数组末尾无 \0 结尾;
*++temp = '\0'; // 放进一个 \0 给数组
return str;
}
}

注: 虽然 函数部分有参照在网上找的.. 但主程序的循环部分 是偶迄今用的最好的循环了...也算是一个案例了
.....继续努力
-------------------------------------------------------------------------------------------

7. 编写一个函数 string_in (), 它接受两个字符串指针参数. 如果第二个字符串被包含在第一个字符串中, 函数就返回被包含的字符串开始的地址. 例如, string_in ("hats", "at") 返回 hats 中 a 的地址, 否则, 函数返回空指针. 在一个使用循环语句为这个函数提供输入的完整程序中进行测试.

解:

#include "stdafx.h"
#define LEN 80
char *string_in (char *str, const char *str1);
int main (void)
{
char input[LEN];
char look[LEN];
char *p;

puts ("Enter a string :");
while (gets(input) && input[0] != '\0')
{
puts ("Enter lookup string");
{
while (gets(look) && look[0] != '\0')
{
p = string_in (input,look);
if (p != NULL)
printf ("%s in string %p \n",look,p);
else
puts ("Error \n");
puts ("Next Enter a string : ");
}
}
}
}

char *string_in(char *str, const char *str1)
{
int len = strlen(str1);
int num;
int found = 1;

num = strlen(str) + 1 - len;
if (num > 0)
while ((found = strncmp (str,str1,len)) && num--)
str++;
if (found)
return NULL;

else
return (char*) str;

}


注: 暂时这样吧 偶就觉得一点都不理想, 但网上搜索的答案和官方的答案, 更离题的很.... 以后再重做吧


--------------------------------------------------------------------------------------------------


8. 编写一个函数, 其功能是使输入字符串反序. 在一个使用循环语句为这个函数提供输入的完整程序中进行测试

解:

#include "stdafx.h"
#define LEN 80
void *antitone (const char *str);
int main (void)
{
char input[LEN];
char *p;

puts ("Enetr a string :");
while (gets(input) && input[0] != '\0' )
{
antitone(input);
puts ("Next Enetr a string :");
}
puts ("bey \n");

return 0;
}

void *antitone(const char *str)
{
int len = strlen(str); // 计算数组长度
char temp[LEN]; // 建立暂时数组

for(int i = 0; i < len -1; i++) // 使用 for 把 str指向数组末尾
str++;
// 这里关键是 第一个 for len-1 , 而第二个 for 是 len
for (int i = 0; i < len ; ++i) // 使用for 将str 从末尾指回首元素并赋给给暂时数组
temp[i] = *str--;

temp[len] = '\0'; // 将暂时数组的末尾加上 结束字符 以免它指向其他内存位置 显示乱码
puts(temp);
return NULL;
}


注: 这里取巧了一下, 直接在函数里实现了, 因为调试了好久都实现不了传正确的值去给主程序的变量

 

---------------------------------------------------------------------------------------------

9. 编写一个程序. 其参数为一个字符串, 函数删除字符串中的空格. 在一个可以循环读取的程序中进行测试, 直到用户输入空行. 对于任何输入字符串, 函数都应该适用并可以显示结果 .

解:

#include "stdafx.h"
#define LEN 80
char *delete_blank (char *str);
int main (void)
{
char input[LEN];
char *p;

puts("Enter a stirng (EOF to blank quit) :");
while (gets(input) && input[0] != '\0')
{
delete_blank(input);
puts("Next Enetr a string (EOF to blank quit) :");
}
puts("Bey \n");
return 0;
}

char *delete_blank(char *str)
{
int len = strlen(str);
char temp[LEN];
char *p = temp;

while (*str)
{
if (*str != ' ')
*p++ = *str++;
else
str++;
}
if (*p != '\0')
*p = '\0';
puts(temp);
return NULL;
}


注: 还是在传递回主程序上解决不了.. 慢慢来吧
--------------------------------------------------------------------------------------

10. 编写一个程序, 读取输入, 直到读入了 10 个字符串 或遇到 EOF, 由二者中最先被满足的那个终止读取过程. 这处程序可以为用户提供一个有 5 个选项的菜单: 输出初始字符串列表, 按 ASCII 顺序输出字符串, 按长度递增顺序输出字符串, 按字符串第一个单词的长度输出字符串和退出. 菜单可以循环, 直到用户输入请求. 当然, 程序要能真正完成菜单中的各项功能.

#include "stdafx.h"
#define ROW 10
#define COL 100
char *ptstr[ROW]; /* 定义一个全局变量 */

int store (char ar[][COL]); // 读取字符串循环 函数
int menu (void);
void show_arr (char *ptr[COL], int ro);
/* 使用 (*ptr) 是为了可以不用 for 循环就能显示数组内容 */
void asciistr (char *ar[], int num);
void arrlen (char *ar[],int row);
void onewodr (char *ar[],int row);


int main (void)
{
char str[ROW][COL];
int strrows = store(str); // 获取数组元素的个数
int num;

begin: // goto 跳转起点
num = menu();
while (num != 5){
switch(num) {
case 1:
puts("输出初始字符串列表");
show_arr(ptstr,strrows);
goto begin;
break;
case 2:
puts ("按 ASCII 顺序输出字符串 ");
asciistr(ptstr,strrows);
show_arr(ptstr,strrows);
goto begin;
break;

case 3:
puts ("按长度递增顺序输出字符串");
arrlen(ptstr,strrows);
show_arr(ptstr,strrows);
goto begin;
break;

case 4:
puts ("按字符串第一个单词的长度输出字符串");
onewodr(ptstr,strrows);
show_arr(ptstr,strrows);
goto begin;
break;

default:
goto begin;
break;

}
}

return 0;
}

/* show_arr 函数: 显示数组内容 */
void show_arr(char *ptr[COL], int ro)
{
int r = 0;

while (r < ro) { // 如果 r < 传递的过来的 ro
puts (*ptr++); // 显示指针指向数组
r++;
}
}

/* store 函数: 读取用户输入并保存到数组中 */
int store(char ar[][COL]) // 获取字符串函数
{
int ct = 0;

printf ("请输入字符串, 还需输入 %d 个 \n", ROW);
while (ct < ROW && gets(ar[ct]) != NULL && ar[ct][0] != '\0'){ /* 用 gets 代替 getchar */
ptstr[ct] = ar[ct]; /* 令指针指向输入字符串 */
ct++;
}
return ct;
}

/* menu 函数: 提供菜单让用户选择 */
int menu(void)
{
int code,status;

puts ("\n-----------------------------------------");
puts ("| 请选择输出方式: |");
puts ("| 1. 输出初始字符串列表 |");
puts ("| 2. 按 ASCII 顺序输出字符串 |");
puts ("| 3. 按长度递增顺序输出字符串 |");
puts ("| 4. 按字符串第一个单词的长度输出字符串 |");
puts ("| 5. 退出 |");
puts ("-----------------------------------------");
while ((status = scanf("%d",&code)) != 1 || (code < 1 || code >5)) {
if (status != 1) // 如果 输入不为 1
scanf ("%*s"); // %*s 代表不赋值给 code ;
puts ("输出错误, 请重新输出");
}
return code;
}

/* asciistr 函数: 使用 asscii 来为数组排序 */
void asciistr(char *ar[], int num)
{
char *temp; /* 声明一个做为交换的指针 */
int i,j;

for (i = 0; i < num-1; i++)
for (j = i+1; j < num; j++)
if (strcmp(ar[i],ar[j]) > 0){ /* 使用 strcmp 函数来比较 */
temp = ar[i]; /* 将数组元素比较后进行交换 */
ar[i] = ar[j];
ar[j] = temp;
}
}

/* arrlen 函数: 按字符串长度进行排序 */
void arrlen (char *ar[], int row)
{
char *temp; /* 声明一个做为交换的指针 */
int i,j;

for (i = 0; i < row-1; i++)
for (j = i+1; j < row; j++)
if (strlen(ar[i]) > strlen(ar[j])){ /* 用 strlen 来比较字符串长度 */
temp = ar[i]; /* 将数组元素比较后进行交换 */
ar[i] = ar[j];
ar[j] = temp;
}
}

/* onewodr 函数: 按第一个单词的长度进行排序 */
int getwodrlen (char *cp); // 声明用来计算第一个单词长度的函数
void onewodr(char *ar[],int row)
{

int i,j;
char *temp; // 声明一个做为交换的指针

for (i= 0; i < row-1; i++)
for (j = i+1; j < row; j++)
if ((getwodrlen(ar[i])) > (getwodrlen(ar[j]))){
temp = ar[i]; /* 将数组元素比较后进行交换 */
ar[i] = ar[j];
ar[j] = temp;
}
}

/* stewodrlen 函数: onewodr 的附属函数, 来用比较第一个单词的长度 */
int getwodrlen (char *cp)
{
int i = 0;

while (cp[i] != ' ' && cp[i] != '\n' && cp[i] != '\0' )
i++;
return i;
}


注: 这道题写了好久好久.... 总算能完成了.. 指针和数组确实是 C 最难掌握的东东 还要努力啊


----------------------------------------------------------------------------------------------


11. 编写一个程序, 功能是读取输入, 直到遇到 EOF, 并报告单词数, 大写字母数, 小写字母数, 标点符号数 和数字数字字符. 使用 ctype.h 系列的函数.


解:

#include "stdafx.h"
#define IN 1 // 在单词内
#define OUT 0 // 在单词外
int main (void)
{
/* 声明需要计数的各类的变量 */
int words_co = 0;
int max_edh_co = 0;
int mix_edh_co = 0;
int punct_co = 0;
int num_co = 0;
int c;
int state = OUT;

puts ("请输入任意内容, 程序将统计输入各各类的次数");
while ((c = getchar()) != EOF){
if (isdigit(c))
num_co++;
if (ispunct(c))
punct_co++;
if (islower(c))
mix_edh_co++;
if (isupper(c))
max_edh_co++;
if (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT){
state = IN;
words_co++;
}
}
printf ("你输入的单词数 %d , 大写字母 %d , 小字字母 %d, 标号符号 %d, 数字 %d.",
words_co, max_edh_co, mix_edh_co, punct_co, num_co);

return 0;
}

 

注: .... 这道题也太简单了....

-----------------------------------------------------------------------------------------------

12. 编写一个程序, 按照相反的单词顺序显示命令行参数. 即, 如果命令行参数是 see you later, 程序的显示应该为 later you see

解:

#include <stdio.h>
int main(int argc, char *argv[])
{
int count,i;

printf ("the command line has %d arguments: \n", argc-1);
for (count = 1; count < argc; count++);
for (i = count; i > 0; i--)
printf ("%4s",argv[i]);
printf ("\n");

return 0;
}

注: 需要已经答了出来..但对命令行参数确实还是迷糊中

--------------------------------------------------------------------------

13. 编写一个计算乘幂的基于命令行的程序. 第一个命令行参数为 double 类型数, 作为幂的底数; 第二个参数为整数, 作为幂的指数.

解:

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

int main (int argc, char *argv[])
{
double num ,exp;

if (argc != 3)
printf ("Usage: %s number exponent \n", argv[0]); // error 提示
else{
num = atof (argv[1]); // 接受输入的第一个数并转换为浮点类型再赋值给 num
exp = atof (argv[2]); // 同上
printf ("%f to the %f power = %g \n",num, exp, pow(nom,exp));
}
return 0;
}


-----------------------------------------------------------------------------------------

14. 使用字符分类函数实现 atoi() 函数.

解:

/* 题目说明白点不好么 字符分类函数就是 ctype.h 所在的库函数 */

#include "stdafx.h"
#define LEN 30
int atoi_s (char s[]);
int main (void)
{
char input[LEN];
int n = 0;

puts ("请输入字符串, 程序将转换为相应的整数数 ( # to qiut) : \n");
gets(input);
atoi_s(input);

return 0;
}

int atoi_s (char s[])
{
int i, n;
n = 0;
for ( i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 *n + (s[i] - '0');
return n;
}

注: 但确实不明白这个函数的用途... 不好测试 就随随便便吧

---------------------------------------------------------------------------------------

15. 编写一个程序, 其功能是读取输入, 直到遇到文件结尾, 并把文件显示出来. 要求程序可以识别并执行下面的命令行参数:

-p 按照原样显示输入
-u 把输入全部为大写
-l 把输入全部转换为小写


解:

/* Programming Exercise 11-15 */
#include <stdio.h>
#include <ctype.h>
/* #include <console.h> */ /* Macintosh adjustment */

int main(int argc, char *argv[])
{
char mode = 'p';
int ok = 1;
int ch;

/*argc = ccommand(&argv); */ /* Macintosh adjustment */

if (argc > 2)
{
printf("Usage: %s [-p | -u | -l]\n", argv[0]);
ok = 0; /* skip processing input */
}
else if (argc == 2)
{
if (argv[1][0] != '-')
{
printf("Usage: %s [-p | -u | -l]\n", argv[0]);
ok = 0;
}
else
switch(argv[1][1])
{
case 'p' :
case 'u' :
case 'l' : mode = argv[1][1];
break;
default : printf("%s is an invalid flag; ", argv[1]);
printf("using default flag (-p).\n");
}
}

if (ok)
while ((ch = getchar() ) != EOF)
{
switch(mode)
{
case 'p' : putchar(ch);
break;
case 'u' : putchar(toupper(ch));
break;
case 'l' : putchar(tolower(ch));
}
}

return 0;
}

注: 此为官方源码.....

----------------------------------------------------------------------------------------------

你可能感兴趣的:(C Primer Plus(第五版)11)