表示字符串的几种方式
// strings1.c
#include <stdio.h>
#define MSG "I am a symbolic string constant."
#define MAXLENGTH 81
int main(void)
{
char words[MAXLENGTH] = "I am a string in an array.";
const char * pt1 = "Something is pointing at me.";
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
/*
Here are some strings:
I am an old-fashioned symbolic string constant.
I am a string in an array.
Something is pointing at me.
I am a spring in an array.
*/
和printf()函数一样,puts()函数也属于stdio.h系列的输入/输出函数。
但是,与printf()不同的是,puts()函数只显示字符串,而且自动在显示的字符串末尾加上换行符。
数组和指针
字符数组名和其他数组名一样,是该数组首元素的地址。
因此,假设有下面的初始化:
char car[10] = "Tata";
那么,以下表达式都为真:
car == &car[0]、
*car == 'T'、
*(car+1) == car[1] == 'a'。
还可以使用指针表示法创建字符串。
例如,程序清单11.1中使用了下面 的声明:
const char * pt1 = "Something is pointing at me.";
该声明和下面的声明几乎相同:
const char ar1[] = "Something is pointing at me.";
以上两个声明表明,pt1和ar1都是该字符串的地址。
在这两种情况下, 带双引号的字符串本身决定了预留给字符串的存储空间。
尽管如此,这两种形式并不完全相同。
初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
初始化字符数组来储存字符串和初始化指针来指向字符串有何区别 (“指向字符串”的意思是指向字符串的首字符)?
例如,假设有下面两个声明:
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
两者主要的区别是:
数组名heart是常量,而指针名head是变量。
那么, 实际使用有什么区别?
首先,两者都可以使用数组表示法:
for (i = 0; i < 6; i++)
putchar(heart[i]);
putchar('\n');
for (i = 0; i < 6; i++)
putchar(head[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(*(head + i));
putchar('\n');
输出如下:
I love
I love
但是,只有指针表示法可以进行递增操作:
while (*(head) != '\0') /* 在字符串末尾处停止*/
putchar(*(head++)); /* 打印字符,指针指向下一个位置 */
这段代码的输出如下:
I love Millie!
假设想让head和heart统一,可以这样做:
head = heart; /* head现在指向数组heart */
这使得head指针指向heart数组的首元素。
但是,不能这样做:
heart = head; /* 非法构造,不能这样写 */
这类似于x = 3;和3 = x;的情况。赋值运算符的左侧必须是变量(或概括 地说是可修改的左值),如*pt_int。
顺带一提,head = heart;不会导致head指向的字符串消失,这样做只是改变了储存在head中的地址。
除非已经保存 了"I love Millie!"的地址,否则当head指向别处时,就无法再访问该字符串。
另外,还可以改变heart数组中元素的信息:
heart[7]= 'M';或者*(heart + 7) = 'M';
数组的元素是变量(除非数组被声明为const),但是数组名不是变量。
我们来看一下未使用const限定符的指针初始化:
char * word = "frame";
是否能使用该指针修改这个字符串?
word[1] = 'l'; // 是否允许?
编译器可能允许这样做,但是对当前的C标准而言,这样的行为是未定义的。
例如,这样的语句可能导致内存访问错误。原因前面提到过,编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量。
例如,下 面的语句都引用字符串"Klingon"的一个内存位置:
char * p1 = "Klingon";
p1[0] = 'F'; // ok?
printf("Klingon");
printf(": Beware the %ss!\n", "Klingon");
也就是说,编译器可以用相同的地址替换每个"Klingon"实例。
如果编译器使用这种单次副本表示法,并允许p1[0]修改'F',那将影响所有使用该字 符串的代码。所以以上语句打印字符串字面量"Klingon"时实际上显示的 是"Flingon":
Flingon: Beware the Flingons!
实际上在过去,一些编译器由于这方面的原因,其行为难以捉摸,而另一些编译器则导致程序异常中断。
因此,建议在把指针初始化为字符串字面量时使用const限定符:
const char * pl = "Klingon"; // 推荐用法
然而,把非const数组初始化为字符串字面量却不会导致类似的问题。
因为数组获得的是原始字符串的副本。
总之,如果不修改字符串,不要用指针指向字符串字面量。
字符串输入
要做的第 1 件事是分配空间,以储存稍后读入的字符串。
前面提到过, 这意味着必须要为字符串分配足够的空间。
不要指望计算机在读取字符串时顺便计算它的长度,然后再分配空间(计算机不会这样做,除非你编写一个处理这些任务的函数)。
最简单的方法是,在声明时显式指明数组的大小:
char name[81];
现在name是一个已分配块(81字节)的地址。
还有一种方法是使用C库函数来分配内存,第12章将详细介绍。
为字符串分配内存后,便可读入字符串。
C 库提供了许多读取字符串的函数:scanf()、gets()和fgets()。
gets()
在读取字符串时,scanf()和转换说明%s只能读取一个单词。
可是在程序中经常要读取一整行输入,而不仅仅是一个单词。
许多年前,gets()函数就用于处理这种情况。
gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,
并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。
它经常和 puts()函数配对使用,该函数用于显示字符串,并在末尾添加换行符。
gets()函数只知道数组的开始处,并不知道数组中有多少个元素。
如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。
目前该函数已被摒弃。
fgets()
过去通常用fgets()来代替gets(),fgets()函数稍微复杂些,在处理输入方面与gets()略有不同。
C11标准新增的gets_s()函数也可代替gets()。
该函数与 gets()函数更接近,而且可以替换现有代码中的gets()。
但是,它是stdio.h输 入/输出函数系列中的可选扩展,所以支持C11的编译器也不一定支持它。
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。
该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。
fgets()和 gets()的区别如下:
fgets()函数的第2个参数指明了读入字符的最大数量。
如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
fgets()函数的第3个参数指明要读入的文件。
如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中。
因为 fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与 fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。
fputs()函数的第2个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。
gets_s()函数
C11新增的gets_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。
gets_s()与fgets()的区别如下。
gets_s()只从标准输入中读取数据,所以不需要第3个参数。
如果gets_s()读到换行符,会丢弃它而不是储存它。
如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。
首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。
接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
第2个特性说明,只要输入行未超过最大字符数,gets_s()和gets()几乎一 样,完全可以用gets_s()替换gets()。
第3个特性说明,要使用这个函数还需要进一步学习。
我们来比较一下 gets()、fgets()和 gets_s()的适用性。
如果目标存储区装得下输入行,3 个函数都没问题。但是fgets()会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符。
如果输入行太长会怎样?使用gets()不安全,它会擦写现有数据,存在安全隐患。
gets_s()函数很安全,但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。
另外,如果打算让程序继续运行, gets_s()会丢弃该输入行的其余字符,无论你是否需要。
由此可见,当输入太长,超过数组可容纳的字符数时,fgets()函数最容易使用,而且可以选择不同的处理方式。
如果要让程序继续使用输入行中超出的字符,可以参考程序清单11.8中的处理方法。
如果想丢弃输入行的超出字符,可以参考程序清 单11.9中的处理方法。
所以,当输入与预期不符时,gets_s()完全没有fgets()函数方便、灵活。
也许这也是gets_s()只作为C库的可选扩展的原因之一。
鉴于此,fgets()通常是处理类似情况的最佳选择。
字符串函数
strlen()
统计字符串的长度。
strcat()
用于拼接字符串;
函数接受两个字符串作为参数,将第二个字符串的备份附加在第一个字符串末尾;
并把拼接后形成的新字符串作为第1个字符串,第2个字符串不变。
strcat()函数的类型是char *(即,指向char的指针)。
strcat()函数返回第1个参数,即拼接第2个字符串后的第1个字符串的地址。
strncat()
strcat()函数无法检查第1个数组是否能容纳第2个字符串。
如果分配给第1个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。
当然,可以像程序清单11.15那样,用strlen()查看第1个数组的长度。
注意, 要给拼接后的字符串长度加1才够空间存放末尾的空字符。
或者,用 strncat(),该函数的第3个参数指定了最大添加字符数。
例如,strncat(bugs, addon, 13)将把 addon字符串的内容附加给bugs,在加到第13个字符或遇到空字符时停止。
因此,算上空字符(无论哪种情况都要添加空字符),bugs数组应该足够大,以容纳原始字符串(不包含空字符)、添加原始字符串在后面的13个字符和末尾的空字符。
strcmp()
该函数要比较的是字符串的内容,不是字符串的地址。
例如:
要比较两个字符串
str1 = hello;
str2 = hallo;
不能写成 if(hello != hallo),因为这样比较的是地址,永远不相等;
strcmp(str1,str2)才是在比较内容;
如果两个字符串相同则返回0。
strncmp()
strcmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。
而strncmp()函数在比较两个字符串时,可以 比较到字符不同的地方,也可以只比较第3个参数指定的字符数。
例如,要查找以"astro"开头的字符串,可以限定函数只查找这5个字符。
strcpy()/strncpy()
前面提到过,如果pts1和pts2都是指向字符串的指针,
那么下面语句拷贝的是字符串的地址而不是字符串本身:
pts2 = pts1;
如果希望拷贝整个字符串,要使用strcpy()函数。
strcpy()和 strcat()都有同样的问题,它们都不能检查目标空间是否能容纳源字符串的副本。
拷贝字符串用 strncpy()更安全,该函数的第 3 个参数指明可拷贝的最大字符数。
sprintf()
sprintf()函数声明在stdio.h中,而不是在string.h中。
该函数和printf()类似,但是它是把数据写入字符串,而不是打印在显示器上。
因此,该函数可以把多个元素组合成一个字符串。
sprintf()的第1个参数是目标字符串的地址。
其余参数和printf()相同,即格式字符串和待写入项的列表。
例如:
sprintf(formal, "%s, %-19s: $%6.2f\n", last, first, prize);
注意, sprintf()的用法和printf()相同,只不过sprintf()把组合后的字符串储存在数组formal中而不是显示在屏幕上。
编程练习
参考
太难惹 向大佬学习
1
设计并测试一个函数,从输入中获取下n个字符(包括空白、制表符、换行符),
把结果储存在一个数组里,它的地址被传递作为一个参数。
#include <stdio.h>
#define LEN 10
char * getnchar(char * str, int n);
int main(void)
{
char input[LEN];
char *check;
check = getnchar(input, LEN - 1);
if (check == NULL)
puts("Input failed.");
else
puts(input);
puts("Done.\n");
return 0;
}
char * getnchar(char * str, int n)
{
int i;
int ch;
for (i = 0; i < n; 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个字符后停止,或在读到第1个空白、制表符或换行符时停止,哪个先遇到哪个停止。
不能只使用scanf()。
#include <stdio.h>
#include <ctype.h>
#define LEN 10
int get_char(char* ar, char* end);
int main(void)
{
char arr[LEN];
char* p;
int l;
printf("Please enter:\n");
l = get_char(arr, arr + LEN);
printf("\ncharacter array:\n");
for (p = arr; p < arr + l; p++)
putchar(*p);
putchar('\n');
return 0;
}
int get_char(char* ar, char* end)
{
int l = 0;
for (; ar < end; ar++, l++)
{
*ar = getchar();
if (isspace(*ar))
break;
}
return l;
}
3
设计并测试一个函数,从一行输入中把一个单词读入一个数组中,并丢弃输入行中的其余字符。
该函数应该跳过第1个非空白字符前面的所有空白。
将一个单词定义为没有空白、制表符或换行符的字符序列。
#include <stdio.h>
#include <ctype.h>
#define LEN 20
int get_char(char* ar);
int main(void)
{
char arr[LEN];
char* p;
int l;
printf("Please enter:\n");
l = get_char(arr);
printf("\nWord:\n");
for (p = arr; p < arr + l; p++)
putchar(*p);
putchar('\n');
return 0;
}
int get_char(char* ar)
{
int l = 0;
_Bool inword = 0;
while (*ar = getchar())
{
if (isspace(*ar) && !inword)
continue;
else if (!isspace(*ar) && !inword)
inword = 1;
else if (isspace(*ar) && inword)
break;
l++;
ar++;
}
return l;
}
4
设计并测试一个函数,它类似编程练习3的描述,
只不过它接受第2个参数指明可读取的最大字符数。
#include <stdio.h>
#include <ctype.h>
#define LEN 10
int get_char(char* ar,int n);
int main(void)
{
char arr[LEN];
char* p;
int l;
printf("Please enter:\n");
l = get_char(arr,LEN);
printf("\nWord:\n");
for (p = arr; p < arr + l; p++)
putchar(*p);
putchar('\n');
return 0;
}
int get_char(char* ar,int n)
{
char* p = ar;
int l = 0;
_Bool inword = 0;
while (p < ar + n)
{
*p = getchar();
if (isspace(*ar) && !inword)
continue;
else if (!isspace(*ar) && !inword)
inword = 1;
else if (isspace(*ar) && inword)
break;
l++;
p++;
}
return l;
}
/*
Please enter:
helloworldhi go go
Word:
helloworld
*/
5
设计并测试一个函数,搜索第1个函数形参指定的字符串,在其中查找第2个函数形参指定的字符首次出现的位置。
如果成功,该函数返指向该字符的指针,
如果在字符串中未找到指定字符,则返回空指针(该函数的功能与 strchr()函数相同)。
在一个完整的程序中测试该函数,使用一个循环给函数提供输入值。
#include<stdio.h>
#define LENGTH 20
char* s_gets(char* st, int n);
char* search(char* st, char ch);
int main(void)
{
char string[LENGTH];
char tar;
char* p_tar;
printf("Please enter some text.(less than %d character)\n", LENGTH);
if (s_gets(string, LENGTH))
{
printf("Your String: ");
puts(string);
printf("\nPlease enter the target character.(empty line to quit)\n");
while ((tar = getchar()) != '\n')
{
p_tar = search(string, tar);
if (p_tar)
printf("The rest of string: \"%s\"", p_tar);
else
printf("No such character.");
printf("\nEnter another character.(empty line to quit)\n");
while (getchar() != '\n')
continue;
}
}
else
printf("error\n");
printf("bye\n");
return 0;
}
char* s_gets(char* st, int n)
{
char* ret_val;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (*st != '\n' && *st != '\0')
st++;
if (*st == '\n')
* st = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
char* search(char* st, char ch)
{
char* target;
while (*st != '\0' && *st != ch)
st++;
if (*st == ch)
target = st;
else
target = NULL;
return target;
}
/*
Please enter some text.(less than 20 character)
please give me a dream
Your String: please give me a dr
Please enter the target character.(empty line to quit)
i
The rest of string: "ive me a dr"
Enter another character.(empty line to quit)
h
No such character.
Enter another character.(empty line to quit)
bye
*/
6
编写一个名为is_within()的函数,接受一个字符和一个指向字符串的指针作为两个函数形参。
如果指定字符在字符串中,该函数返回一个非零值(即为真)。
否则,返回0(即为假)。
在一个完整的程序中测试该函数,使用一个循环给函数提供输入值。
#include<stdio.h>
#define LENGTH 20
char* s_gets(char* st, int n);
int is_within(char ch, char* ar);
int main(void)
{
char string[LENGTH];
char target;
printf("Please enter some text.(less than %d character)\n", LENGTH);
if (s_gets(string, LENGTH))
{
printf("Your input: ");
puts(string);
printf("\nPlease enter target character.\n");
while ((target = getchar()) != '\n')
{
if (is_within(target, string))
printf("Character in this string.\n");
else
printf("Not found.\n");
printf("Enter another character.\n");
while (getchar() != '\n')
continue;
}
}
else
printf("error\n");
printf("bye\n");
return 0;
}
char* s_gets(char* st, int n)
{
char* ret_val;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (*st != '\n' && *st != '\0')
st++;
if (*st == '\n')
* st = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
int is_within(char ch, char* ar)
{
while (*ar != '\0')
{
if (*ar == ch)
return 1;
else
ar++;
}
return 0;
}
/*
Please enter some text.(less than 20 character)
please give me a dream
Your input: please give me a dr
Please enter target character.
m
Character in this string.
Enter another character.
h
Not found.
Enter another character.
bye
*/
7
strncpy(s1, s2, n)函数把s2中的n个字符拷贝至s1中,截断s2,或者有必要的话在末尾添加空字符。
如果s2的长度是n或多于n,目标字符串不能以空字符结尾。
该函数返回s1。
自己编写一个这样的函数,名为mystrncpy()。
在一个完整的程序中测试该函数,使用一个循环给函数提供输入值。
8
编写一个名为string_in()的函数,接受两个指向字符串的指针作为参数。
如果第2个字符串中包含第1个字符串,该函数将返回第1个字符串开始的地址。
例如,string_in(“hats”, “at”)将返回hats中a的地址。
否则,该函数返回空指针。
在一个完整的程序中测试该函数,使用一个循环给函数提供输入值。
9
编写一个函数,把字符串中的内容用其反序字符串代替。
在一个完整的程序中测试该函数,使用一个循环给函数提供输入值。
10
编写一个函数接受一个字符串作为参数,并删除字符串中的空格。
在一个程序中测试该函数,使用循环读取输入行,直到用户输入一行空行。
该程序应该应用该函数只每个输入的字符串,并显示处理后的字符串。
11
编写一个函数,读入10个字符串或者读到EOF时停止。
该程序为用户提供一个有5个选项的菜单:
打印源字符串列表、以ASCII中的顺序打印字符串、按长度递增顺序打印字符串、按字符串中第1个单词的长度打印字符串、退出。
菜单可以循环显示,除非用户选择退出选项。
当然,该程序要能真正完成菜单中各选项的功能。
12
编写一个程序,读取输入,直至读到 EOF,
报告读入的单词数、大写字母数、小写字母数、标点符号数和数字字符数。
使用ctype.h头文件中的函数。
#include<stdio.h>
#include<ctype.h>
#define LENGTH 50
int main(void)
{
char ch;
_Bool inword = 0;
int words = 0;
int upper = 0;
int lower = 0;
int digit = 0;
int punct = 0;
printf("Please enter some text(end with EOF).\n");
while ((ch = getchar()) != EOF)
{
if (isdigit(ch))
digit++;
else if (ispunct(ch))
punct++;
else if (islower(ch))
lower++;
else if (isupper(ch))
upper++;
if (isalpha(ch) && !inword)
{
words++;
inword = 1;
}
else if (!isalpha(ch) && inword)
inword = 0;
}
printf("\nNumber of words: %d\n", words);
printf("Uppercase letters: %d\n", upper);
printf("Lowercase letters: %d\n", lower);
printf("Punctuation number: %d\n", punct);
printf("Number of digits: %d\n", digit);
return 0;
}
/*
Please enter some text(end with EOF).
Do you have 100 dollars?
No.
^Z
Number of words: 5
Uppercase letters: 2
Lowercase letters: 16
Punctuation number: 2
Number of digits: 3
*/
13
编写一个程序,反序显示命令行参数的单词。
例如,命令行参数是 see you later,该程序应打印later you see。
14
编写一个通过命令行运行的程序计算幂。
第1个命令行参数是double 类型的数,作为幂的底数,第2个参数是整数,作为幂的指数。
15
使用字符分类函数实现atoi()函数。
如果输入的字符串不是纯数字, 该函数返回0。
#include<stdio.h>
#include<ctype.h>
#include<string.h>
#define LENGTH 10
int atoi(char* st);
int pow_i(int base, int n);
int main(void)
{
char integer[LENGTH];
int value;
printf("Please enter an integer.\n");
scanf_s("%s", integer, 10);
value = atoi(integer);
printf("\nInteger:\n%d\n", value);
return 0;
}
int atoi(char* st)
{
int value = 0;
int len;
int i = 0;
len = strlen(st);
while (*st != '\0')
{
if (!isdigit(*st))
{
value=0;
break;
}
else
{
value += (*st - '0') * pow_i(10, len - 1 - i);
}
st++;
i++;
}
return value;
}
int pow_i(int base, int n)
{
int pow = 1;
for (; n > 0; n--)
pow *= base;
return pow;
}
16
编写一个程序读取输入,直至读到文件结尾,然后把字符串打印出 来。
该程序识别和实现下面的命令行参数:
-p 按原样打印
-u 把输入全部转换成大写
-l 把输入全部转换成小写
如果没有命令行参数,则让程序像是使用了-p参数那样运行。