作者LoganChien (简子翔)
看板b97902HW
标题[计程] 阵列简介 II (来连载吧,继续害人...
时间Wed Oct 8 00:43:44 2008
前言
上一篇文章我们简单地介绍了阵列的基本概念,我们可以把阵列想像
成一个表格,每一个表格可以有若干个储存格,每一个储存格有一个
索引(Index),可以存一个元素(Element)。
在这一篇我要来介绍阵列的进阶用法。在这一篇文章我会提到
字串
(String)。
在 C 语言中,如果我们要输出、读入一串文字,我们都会用到字串。
即使是像 Hello World 这一种简单的程式我们也已经用到字串了,
所以字串的重要性可见一斑。我们在这一篇文章会讨论这一个问题。
printf("Hello world!\n");
预备知识
1. 字元型别有二种,一种是
char,另一种是 wchar_t。前者主要用
於 ASCII、BIG5 环境,後者主要用於 Unicode 环境。不过因为
一些因素,本文将只讨论 char 的使用。
2.
字元字面常数(Character Literal Constant),一般而言我们会
用数字来代表字元,不过,因为并不是所有的环境都使用相同的
数字来表示同一个字元(例如:
ASCII v.s. EBCDIC),所以为了可
移植性(Portability),我们会用
'A' 来代表「用以表示 A 这一
个字元的数字」,其余类推。
3.
脱逸(Escape),因为有些字元如果想用字元字面常数来表示,就
会破坏 C 语言的语法规则,所以我们不能直接用 '<字元>' 来表
示,而又有不少是很常见的字元,所以我们有其他的表示法。例
如
换行字元,我们就用 \n 来代替;单引号字元,我们就用 \'
来代替。又因为 \ 被拿来当作脱逸符号,所以我们就用 \\ 来
代替反斜线。部分替换表如下:
┌───┬───┬───┬──────────┐
│
字元 │
脱逸 │
ASCII │
说明 │
├───┼───┼───┼──────────┤
│
Null │
\0 │
0x00 │
空字元(字串终止字元)│
├───┼───┼───┼──────────┤
│Tab │\t │0x09 │ │
├───┼───┼───┼──────────┤
│换行 │\n │0x0A │ │
├───┼───┼───┼──────────┤
│" │\" │0x22 │在字串内的双引号 │
├───┼───┼───┼──────────┤
│' │\' │0x27 │'\'' 单引号的代号 │
├───┼───┼───┼──────────┤
│\ │\\ │0x5C │脱逸脱逸字元 │
└───┴───┴───┴──────────┘
字串
在 C 语言中,
字串的定义为:一个以字元型别为元素型别的阵列,
而且它必需以 空字元(\0) 结尾。另外,还有一种东西也可以算是字
串,那就是
字串字面(String Literal)。所谓的
字串字面就是指以
双引号夹起来的东西,我们下面会再提到。
┌────────────────────────────┐
│一段故事:如果你有 Windows 程式设计经验的话,你一定有看 │
│过「匈牙利命名法」。一种会在变数名称前面加上型别资讯的命│
│名方法。在匈牙利命名法中,有一个规则就是字串变数的名字会│
│加上 sz,例如 szName 之类的。其中,sz 指的就是 Zero-ter-│
│minated String 的意思。微软早期的工程师就是用这一种方法 │
│来让错误的程式码容易看得出来。不过,要不要用匈牙利命名法│
│呢?我想...,微软的程式品质...,嗯,总之师其意可,师其法│
│则万万不可。(因为有其他的缺点) │
└────────────────────────────┘
根据上面的定义,我们知道,下面 array1 是字串,而 array2 和
array3 不是,为什麽?
char array1[] = {'A', 'B', 'C', 'D', '\0'};
char array2[] = {'A', 'B', 'C', 'D', '\n'};
int array3[] = {'A', 'B', 'C', 'D', '\0'};
很显然地,array1 满足我们字串的定义,它是一个以 char 为元素
型别的阵列而且以空字元(\0)结尾。不过,
array2 就不是,因为它
不是以空字元(\0)结尾;而 array3 也不是,因为它的元素型别是
int 并不是字元型别。如果我们对 array2 做以下修改,我们也可以
称它为字串,因为有以空字元(\0)结尾,即便 \0 不是在阵列的最後
一个位置,也仍然是字串。
array2[2] = '\0'; /* After this assign operation,
array2 is a string. */
可是如果每一次要用字串的时候,我们都需要用上面 array1 的方式
来宣告,那写 C 语言程式的时候根本是一场梦魇。所以有一种叫作
字串字面的东西就被发明了。向上面的 array1 可以简写成像
array4 这样:
char array4[] = "ABCD";
我们可以直接用一对双引号括住若干个字元,它就会变成字串。当然
也因为如此,
你的字串中如果有 ",你就必需用脱逸字元来处理。另
外,如果你很细心的话,你会发现,array4 没有用 \0 来终止字串,
你想必会质疑:为何它可以是字串?
事实上,
字串字面会自动的帮我们在字串的後方加上一个空字元,所
以 "ABCD" 是 等价 {'A', 'B', 'C', 'D', '\0'} 的。也因为如此,
如果你写出下面的程式码,编译器会向你抱怨:阵列过小,因为事实
上 "ABCD" 有五个字元。
char array5[4] = "ABCD"; /* ERROR! */
当然也因为 字串字面(String Literal) 会自动加上 \0,所以和字
元字面常数是不同的东西。
简单来说 "A" 不等价 'A'。
字串函式
C 语言的标准函式库里面有若干个有用的函式,可以帮你简化字串处
理的繁锁工作。它的功能涵盖输入输出、字串比较、字串串接、等字
串处理工作。本文将其中一部分简述如下,不过限於篇幅,有些函式
被我省略掉了,如要更详细的资讯,大家可以自行去寻找「C Stand-
ard Libray」的相关资料。
#include <stdio.h>
stdio.h 这一个标头档,专门放输入输出的函式原型。所以如果要输
入抑或输出,你都必需引入 stdio.h 标头档。
scanf("%s", ...);
首先,字串不会总是写死在程式里,有时候我们也必需要让使用者
输入字串。在 C 语言中,我们可以用 scanf 来输入字串。用法和
我们读入数字的方法很像,不同点是我们用的不是 %d 而是
%s。
另外,我们不用使用取址(Address-of)运算子,
直接把阵列名称传
入就好了(为什麽会这样呢?我们有机会说到指标再谈)。例如:
char input[16];
scanf("%s", input);
这样我们就可以读入一个字串。假如我输入 xyz 之後按下 Enter,
input 的前四个元素就会是 'x', 'y', 'z', '\0'。也就是说:
input[0] == 'x';
input[1] == 'y';
input[2] == 'z';
input[3] == '\0';
scanf 会一直读入字元,写到阵列中,直到遇到「空白、换行、
Tab」等字元,它才会在阵列加上 \0,然後停止。不过,细心一点
的读者很快就会发现问题:我输入多少字,不论阵列够不够大,
scanf 都会照单全收!进而产生缓充区溢位(Buffer Overflow)!
这是很多程式会有安全漏洞的原因。要怎麽避免呢?你可以
在 s
的前面在上数字,表示字串(不含 \0)最多可以多长。例如:
char input[16];
scanf("%15s", input);
就表示:最多可以输入 15 个字元,加上 1 个 \0 做为字串结尾。
如此一来就可以确保不会溢位。
printf("%s", ...);
当然我们常常会有输出字串的需求,我们要怎麽输出字串呢?当然
直接用 printf(字串) 也是一个方法。不过,他却不一定是一个好
方法。举例来说:如果字串是由使用者输入的,你本来不预期使用
者会输入 % 字元,可是使用者输入的话会发生什麽事呢?请自行
想像!XD
所以正确的方法是什麽呢?
标准答案是 printf("%s", 字串);
printf("%s", 字串);
使用 %s 来告诉 printf 函式:我们要印出一个字串,请把字串中
除了 \0 之外的所有字元都印到萤幕上。例如:
char str[] = "String!";
printf("%s", str);
printf("%s", "This is my string!");
fgets(str, n, stdin);
有时候,我们
想要读入的字串含有空白,我们希望以
换行字元为分
界怎麽办呢?这时候 fgets 就可以上场了,
fgets 可以帮我们读
入字元,直到遇上换行字元。第一个引数是字串,第二个引数是你
希望最多读入多少字元(含 \0),第三个引数大家先打 stdin 就可
以了。
char str[16];
fgets(str, 16, stdin);
以上会读入最多 15 个字元加上一个 \0。会以换行为分界。如果
输入的字元少於 15 个字元,在 \0 之前会有一个 \n。如果你不
想要他,你可以用下面的方式来处理。
if (strchr(str, '\n'))
{
*strchr(str, '\n') = '\0';
}
#include <string.h>
string.h 这一个标头档顾名思义,就是要用来处理字串用的。不过
除了处理字串的函式之外,还有一些是处理记忆体的,或者正确的说
是处理一段记忆体空间。
值得注意的是:
以 str 开头的函式,在引数的部分会假定输入的字
串都是以真得字串,如果你传入的只是一个字元阵列,而且不以 \0
结尾,则你的程式执行起来会如何,没有人会知道。所以写程式的时
候要小心!
strlen(str);
这一个函式以一个字串为引数,然後它会
回传这一个字串的字数
(STRing LENgth)。所谓的字数,指得是「非 \0 的字数」,也就
是在碰到 \0 之前一共有多少个字。例如:
char str1[] = "ABCD";
char str2[] = {'q', 'w', 'e', 'r', 't', 'y', '\0'};
size_t strlen1 = strlen(str1); /* == 4 */
size_t strlen2 = strlen(str2); /* == 6 */
strcmp(stra, strb);
这一个函式是用来比较二个字串的大小(STRing CoMPare)。如果二
个字串全等,则传会 0。如果二个字串之间第一个不相同的字元的
索引是 i,则回传值会和 stra[i] - strb[i] 同号。也就是说在
ASCII 环境下,
strcmp("A", "a") < 0
strcmp("Ab", "AB") > 0
strcmp("A", "A") == 0
strcpy(dest, src);
这一个函式的用途是把一个字串复制到另一个字元阵列(STRing
CoPY)。第一个引数是目标阵列,第二个引数是来源字串。这一个
函式会把 src 所有「在 \0 之前包含 \0 的字元」复制到 dest。
char receive[1024];
char source[] = "ABCDEFG";
strcpy(receive, source);
printf("%s", receive); /* We get "ABCDEFG" as output */
不过必需注意,
如果 dest 的大小,不足以容纳 src,则你的程式
极可能发生执行阶段错误。这也是不少安全漏洞的来源,所以写程
式的时候请千万小心。
char buffer[1];
char source2[] = "This Is Going To Overflow!!!! HA! HA! HA!";
strcpy(buffer, source2); /* 错误!缓冲区溢位 */
strncpy(dest, src, n);
这一个函式也是要用来复制字串。不过和 strcpy 不同的是,它最
多只会复制 n 个字元。如果 strlen(src) < n,\0 会被复制,如
果 strlen(src) >= n,则 \0 就不会被复制。所以 strncpy 复制
出来的东西不一定是字串。
char buffer1[1024], buffer2[1024];
char buffer3[1024], buffer4[1024];
strncpy(buffer1, "ABCDE", 4); /* buffer1 不是字串 */
strncpy(buffer2, "ABCDE", 5); /* buffer2 也不是字串 */
strncpy(buffer3, "ABCDE", 6); /* buffer3 是字串 */
strncpy(buffer4, "ABCDE", 7); /* buffer4 是字串 */
不过如果你手动补上 \0,buffer1、buffer2 也可以是字串。
buffer1[4] = '\0';
buffer2[5] = '\0';
我们会有:
strcmp(buffer1, "ABCD") == 0 /* 要补上 \0 才能满足条件 */
strcmp(buffer2, "ABCDE") == 0 /* 要补上 \0 才能满足条件 */
strcmp(buffer3, "ABCDE") == 0
strcmp(buffer4, "ABCDE") == 0
strcat(dest, src);
这一个函式的用途是把 src 字串接到 dest 字串之後(STRing
conCATenate)。strcat 会先找到 dest 字串中的 \0 然後,从这
一个字元开始复制 src 的字元直到 src 结束为止。当然,src 的
\0 会被复制,所以执行完之後 dest 仍然会是一个字串。例如:
char dest[1024] = "abc";
strcat(dest, "ABCDEFG");
printf("%s", dest); /* 输出 "abcABCDEFG" */
strcpy(dest, "12345");
strcat(dest, "HIJKL");
printf("%s", dest); /* 输出 "12345HIJKL" */
不过和 strcpy 一样,
strcat 不会在乎 dest 是否有足够的空间,
所以也会有溢位问题,你必需确定 dest 可以再加上 strlen(src)
个字元才能用这一个函式。请小心使用,我就不再赘述。
strncat(dest, src, n);
这一个函式和 strcat 一样是用来串接字串;和 strcat 不一样的
是有串接字数限制。和 strncpy 一样有字数限制,不同的是 \0
字元永远会被复制(事实上你可以想像成原本 dest 的 \0 被搬到
後面)。所以 dest 只要可以再加上 n 个字元就不会溢位。例如:
char str1[10] = "ABCDE";
strncat(str1, "abcdefg", 4); /* OK! */
printf("%s", str1); /* 输出 "ABCDEabcd" */
memcpy(dest, src, n);
这一个函式可以
把记忆体中的特定区块复制 n bytes 到另一处。
和 strcpy 不同,memcpy 会把从 src 算起一共 n bytes 搬到
dest 的位置。我们直接看范例:
char str1[6];
char str2[] = "ABCDE";
memcpy(str1, str2, 6);
printf("%s", str1); /* 输出 "ABCDE" */
char str3[] = "ABCDE";
char str4[] = "abcde";
memcpy(str3, str4, 2);
printf("%s", str3); /* 输出 "abCDE" */
有了基本概念之後,我们来看看变化型。我们在前一章有说到阵列
不可以 assign,我们要复制,只能一个元素一个元素地复制。不
过这样实在太麻烦了,有没有简单的方法?是有的。
int array1[] = {56, 58, 59};
int array2[3];
memcpy(array2, array1, sizeof(int) * 3);
array2[0] == 56, array2[1] == 58, array2[2] == 59
稍微讲解一下,
sizeof(int) 是指一个 int 要多少个 byte,我们
有空再谈。之後 memcpy 会帮我们复制 sizeof(int) * 3 个
bytes,所以
array2 的那一段记忆体空间会和 array1 的一样。
最後我们就会有和 array1 一样的 array2。
提醒一下: [dest, dest + n), [src, src + n) 最好不要重叠,
不然结果不被定义(EDIT: ckclark学长说,可以改用 memmove 函
式)。
memset(dest, ch, n);
这一个函式可以
从 dest 这一个 byte 开始,把 n 个 bytes 全部
设为 ch 这一个字元。例如:
char str[] = "ABCDEF";
memset(str, 'a', 3);
printf("%s", str); /* 输出 "aaaDEF" */
当然,只有这一种功能当然不够看。
memset 最强大的功能就是可
以初始化阵列。不过我要先声明,这
不是一个 Portable (可移植)
的方法。
int array1[3];
memset(array1, 0, sizeof(int) * 3); /* 所有 array1 的元素是 0 */
int array2[3];
memset(array2, 0xFF, sizeof(int) * 3); /* 所有 array2 的元素是 -1 */
unsigned int array3[3];
memset(array3, 0xFF, sizeof(unsigned int) * 3);
/* 所有 array3 的元素是 UINT_MAX */
不过这一个方法,不是一个 Portable 的方法,它依赖平台的特性。
例如:如果有一个平台一个 9-bits 等於 1byte,array2 的元素
就不一定会是都是 -1。虽然,这一个方法很快速、简洁、好用,
不过,切记:跨平台的时候不要忘了!
备注:(在 C99, 也许 C89 也对) 如果要把阵列初始化为 0,大可
不用这种方法,还有更简单的方法,有空我们再谈。
结语
结语要说啥?我不知道....。打那麽多字,手好酸。总之,字串很重
要,看不懂的人,欢迎来信(MSN 的帐号,GMAIL结尾)。下集预
告:再看看.....(阵列III?)。
PS. 这就是 钢弹 吗?我可没有自信,第一次碰钢弹就可以重写 OS,
并干掉 Rusty.....。众人嘘:还不快滚去救生舱!
PS2. 你的推文是我再写的原动力,你的嘘文是我自 d 的原动力。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.112.241.166
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 00:44)
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 00:44)
1F:推 ming1053:用gets就好了吧? 10/08 00:47
2F:推 ming1053:再推一个 这篇好文 10/08 00:51
3F:→ LoganChien:其实不说 gets 是故意的,连 strcpy, strcat 我都有一 10/08 00:54
4F:→ LoganChien:点犹豫,原因是... 不是很安全。 10/08 00:55
5F:推 iForests:彩色好文推 10/08 01:26
6F:推 drazi:科科 等到脱离scanf printf的时候 才会知道这些的珍贵... 10/08 02:03
7F:推 drazi:你要知道windows程式设计...要让东西出现在萤幕上 10/08 02:03
8F:推 drazi:跟读取Input是多困难的事情= = 噢还有...... 10/08 02:04
9F:推 drazi:匈牙利命名法有它存在的价值及必要......绝对有...... 10/08 02:04
10F:推 haoto:伤心TA推 目前AC的全都给我用array.....明明就不用...T_T 10/08 02:08
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 02:19)
11F:→ LoganChien:感谢 ckclark 助教/学长 的勘误。(204.) 10/08 02:22
12F:推 benck:太认真了 推一个 10/08 02:33
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 07:39)
13F:推 anfranion:不是很安全是指会不会用而造成的错误吗@@ 10/08 08:11
14F:→ anfranion:gets()比fgets基础很多,有f都是涉及开档读档了 10/08 08:12
15F:→ anfranion:虽然好像也是有人这样用啦囧 10/08 08:12
16F:推 sa072686:好文推XD 真是太详细了! 10/08 09:53
17F:推 godgunman:推好文XD 10/08 12:31
18F:→ hrs113355:fgets比gets安全... 10/08 12:52
19F:→ LoganChien:To drazi: 我觉得匈牙利命名法在那一个 type check 不 10/08 15:55
20F:→ LoganChien:是很好的年代,确实有它的好处。不过,我觉得现在的编 10/08 15:56
21F:→ LoganChien:译器都可以正确地检查型别,使用匈牙利命名法的理由有 10/08 15:57
22F:→ LoganChien:很大一部分被消灭了。除了像文中所提到的 sz 我觉得我 10/08 15:58
23F:→ LoganChien:不会想要用这一个命名法。因为太繁锁了,而且有一些现 10/08 16:00
24F:→ LoganChien:在来说不太必要(例如: lp 的 l 是指 long pointer)。 10/08 16:01
25F:→ LoganChien:除非要 Windows Programming 我现在都不会推荐别人用 10/08 16:02
26F:→ LoganChien:匈牙利命名法。不过话说回来,匈牙利命名法也已经是十 10/08 16:03
27F:→ LoganChien:多年的产物,以现今的角度看可能有失公允。 10/08 16:04
28F:→ LoganChien:To anfranion: 不是很安全指得是「你没有办法控制输入 10/08 16:06
29F:→ LoganChien:的大小」。如果你用 gets,然後,有心人士故意输入一个 10/08 16:07
30F:→ LoganChien:很长的字串,想要不溢位也难。如果溢位的是一般的字元 10/08 16:08
31F:→ LoganChien:还好,如果是精心设计,还可以执行你输入的字串(程式) 10/08 16:09
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 16:16)
32F:推 drazi:你说到重点了XD windows programming XD 10/08 18:05
33F:推 drazi:好啦我必须说有windows programming经验的就知道~ 没有的就. 10/08 18:05
34F:推 drazi:.....(摊手~) 10/08 18:05
36F:→ jimmycool:说得还满有道理的,可以参考看看:D 10/08 19:03
37F:推 drazi:大推XD 10/08 19:22
38F:推 dennis2030:楼主好文我顶 10/09 00:53
39F:→ LoganChien:To jimmycool: 那一篇我也看过,真得写得不错,不过他 10/09 00:59
40F:→ LoganChien:好像比较着重在型别之外加上抽象意义,而非把型别直接 10/09 01:04
41F:→ LoganChien:写上去。我觉得那一篇真得值得推荐! 10/09 01:05
※ 编辑: LoganChien 来自: 140.112.241.166 (10/09 01:06)
42F:推 anfranion:噢嗯原来是指这个方向的实作 楼主真强大XD 10/09 08:16
※ 编辑: LoganChien 来自: 140.112.241.166 (10/10 07:53)