字符串操作函数

来源:互联网 发布:被覆盖的监控数据恢复 编辑:程序博客网 时间:2024/05/18 00:07

 #include <string.h>

 

1、初始化字符串

void *memset (void *s, int c, size_t n);

返回值:s指向哪,返回的指针就指向哪

memset 函数把 s所指的内存地址开始 的 n个字节 都填充为 c的值。 通常c 的值为0把一块内存区清零

定义char buf[10];,如果它是全局变量或静态变量,则自动初始化为0(位于.bss段),

如果它是函数的局部变量,则初始值不确定,可以用memset(buf,0,10)清零,----长度一般都在函数参数的最后。

由malloc分配的内存初始值也是不确定的,也可以用memset清零。

 

***一个程序本质上都是由bss段、data段、text段三个组成的。

BSS段:一般存放程序中未初始化的全局变量。一般操作系统自动把其值置零。 

数据段data段:用来存放程序中 已初始化的全局变量的一块内存区域。

代码段:存放程序执行代码的一块内存区域

 

  read/write segment                两者的区别在于:

       (.data, .bss)  -----------------全局的未初始化变量存在于.bss段中,具体体现为一个占位符;

 |________________|              全局的已初始化变量存于.data段中;  .bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化

 

  read-only segment 

   (.init, .text, .rodata)----------------常量!常量区,char *name = "abcdef"; 字符串abcdef保存在常量区   

 |________________|

 

 .rodata =>segment for constant data 常量。

 

----在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol 以符号开始的块segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

  比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

  text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。

 

1) 静态内存分配是在编译时完成的,不需要占用CPU资源;动态分配内存是在运行时完成的,动态内存的分配与释放需要占用CPU资源;2) 静态内存分配是在栈上分配的,动态内存是堆上分配的;3) 动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要;4) 静态分配内存需要在编译前确定内存块的大小,而动态分配内存不需要编译前确定内存大小,根据运行时环境确定需要的内存块大小,按照需要分配内存即可。可以这么说,静态内存分配是按计划分配,而动态内存分配是按需分配。5) 静态分配内存是把内存的控制权交给了编译器,而动态内存是把内存的控制权交给了程序员;综上所述,静态分配内存适合于编译时就已经可以确定需要占用内存多少的情况,而在编译时不能确定内存需求量时可使用动态分配内存;但静态分配内存的运行效率要比动态分配内存的效率要高,因为动态内存分配与释放需要额外的开销;动态内存管理水平严重依赖于程序员的水平,如果处理不当容易造成内存泄漏。

 

bss段和data段的区别:

一个程序本质上都是由 bss段、data段、text段三个组成的。这样的概念,不知道最初来源于哪里的规定,但 在当前的计算机程序设计中是很重要的一个基本概念。而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的 问题。

    在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

    比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。
    在《Programming ground up》里对.bss的解释为:There is another section called the .bss. This section is like the data section, except that it doesn’t take up space in the executable.
    text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。

http://blog.csdn.net/bobocheng1231/archive/2008/02/23/2115289.aspx

 

【例一】

用cl编译两个小程序如下:(cl是MS的。)

程序1:

int ar[30000];

void main()
{
    ......
}


程序2:

int ar[300000] = {1, 2, 3, 4, 5, 6 };

void main()
{
    ......
}


发现程序2编译之后所得的.exe文件比程序1的要大得多。当下甚为不解,于是手工编译了一下,并使用了/FAs编译选项来查看了一下其各自的.asm,发现在程序1.asm中ar的定义如下:

_BSS SEGMENT
     ?ar@@3PAHA DD 0493e0H DUP (?)    ; ar
_BSS ENDS


而在程序2.asm中,ar被定义为:

_DATA SEGMENT
     ?ar@@3PAHA DD 01H     ; ar
                DD 02H
                DD 03H
                ORG $+1199988
_DATA ENDS


区别很明显,一个位于.bss段,而另一个位于.data段,两者的区别在于:全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。

【例二】

编译如下程序(test.cpp):
#include <stdio.h>

#define LEN 1002000

int inbss[LEN];

float fA;

int indata[LEN]={1,2,3,4,5,6,7,8,9};

double dbB = 100.0;

const int cst = 100;

int main(void)
{
    int run[100] = {1,2,3,4,5,6,7,8,9};
    for(int i=0; i<LEN; ++i)
        printf("%d ", inbss[i]);
    return 0;
}

命令:cl /FA test.cpp 回车 (/FA:产生汇编代码)
产生的汇编代码(test.asm):
    TITLE    test.cpp
    .386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT    SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT    ENDS
_DATA    SEGMENT DWORD USE32 PUBLIC 'DATA'
_DATA    ENDS
CONST    SEGMENT DWORD USE32 PUBLIC 'CONST'
CONST    ENDS
_BSS    SEGMENT DWORD USE32 PUBLIC 'BSS'
_BSS    ENDS
_TLS    SEGMENT DWORD USE32 PUBLIC 'TLS'
_TLS    ENDS
FLAT    GROUP _DATA, CONST, _BSS
    ASSUME    CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC    ?inbss@@3PAHA                    ; inbss
PUBLIC    ?fA@@3MA                    ; fA
PUBLIC    ?indata@@3PAHA                    ; indata
PUBLIC    ?dbB@@3NA                    ; dbB
_BSS    SEGMENT
?inbss@@3PAHA DD 0f4a10H DUP (?)            ; inbss
?fA@@3MA DD    01H DUP (?)                ; fA
_BSS    ENDS
_DATA    SEGMENT
?indata@@3PAHA DD 01H                    ; indata
    DD    02H
    DD    03H
    DD    04H
    DD    05H
    DD    06H
    DD    07H
    DD    08H
    DD    09H
    ORG $+4007964
?dbB@@3NA DQ    04059000000000000r        ; 100    ; dbB
_DATA    ENDS
PUBLIC    _main
EXTRN    _printf:NEAR
_DATA    SEGMENT
$SG537    DB    '%d ', 00H
_DATA    ENDS
_TEXT    SEGMENT
_run$ = -400
_i$ = -404
_main    PROC NEAR
; File test.cpp
; Line 13
    push    ebp
    mov    ebp, esp
    sub    esp, 404                ; 00000194H
    push    edi
; Line 14
    mov    DWORD PTR _run$[ebp], 1
    mov    DWORD PTR _run$[ebp+4], 2
    mov    DWORD PTR _run$[ebp+8], 3
    mov    DWORD PTR _run$[ebp+12], 4
    mov    DWORD PTR _run$[ebp+16], 5
    mov    DWORD PTR _run$[ebp+20], 6
    mov    DWORD PTR _run$[ebp+24], 7
    mov    DWORD PTR _run$[ebp+28], 8
    mov    DWORD PTR _run$[ebp+32], 9
    mov    ecx, 91                    ; 0000005bH
    xor    eax, eax
    lea    edi, DWORD PTR _run$[ebp+36]
    rep stosd
; Line 15
    mov    DWORD PTR _i$[ebp], 0
    jmp    SHORT $L534
$L535:
    mov    eax, DWORD PTR _i$[ebp]
    add    eax, 1
    mov    DWORD PTR _i$[ebp], eax
$L534:
    cmp    DWORD PTR _i$[ebp], 1002000        ; 000f4a10H
    jge    SHORT $L536
; Line 16
    mov    ecx, DWORD PTR _i$[ebp]
    mov    edx, DWORD PTR ?inbss@@3PAHA[ecx*4]
    push    edx
    push    OFFSET FLAT:$SG537
    call    _printf
    add    esp, 8
    jmp    SHORT $L535
$L536:
; Line 17
    xor    eax, eax
; Line 18
    pop    edi
    mov    esp, ebp
    pop    ebp
    ret    0
_main    ENDP
_TEXT    ENDS
END
----------------------------------------
通过汇编文件可以看到,数组inbss和indata位于不同的段(inbss位于bss段,而indata位于data段
若把test.cpp中的indata数组拿掉,查看生成的exe文件的大小,可以发现,indata拿掉之后exe文件的大小小了很多。而若拿掉的是inbss数组,exe文件大小跟没拿掉时相差无几。

说明了:
bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。

数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段后面。当这个内存区进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。

 

 

2、取字符串的长度

#include <string.h>size_t strlen(const char *s);返回值:字符串的长度
strlen函数返回s所指的字符串的长度。该函数从s所指的第一个字符开始找'\0'字符,一旦找到就返回,返回的长度不包括'\0'字符在内。例如定义char buf[] = "hello";,则strlen(buf)的值是5,但要注意,如果定义char buf[5] = "hello";,则调用strlen(buf)是危险的,会造成数组访问越界。
1.3. 拷贝字符串
在第 1 节 “本章的预备知识”中介绍了strcpy和strncpy函数,拷贝以'\0'结尾的字符串,strncpy还带一个参数指定最多拷贝多少个字节,此外,strncpy并不保证缓冲区以'\0'结尾。现在介绍memcpy和memmove函数。
#include <string.h>void *memcpy(void *dest, const void *src, size_t n);void *memmove(void *dest, const void *src, size_t n);返回值:dest指向哪,返回的指针就指向哪
memcpy函数从src所指的内存地址拷贝n个字节到dest所指的内存地址,和strncpy不同,memcpy并不是遇到'\0'就结束,而是一定会拷贝完n个字节。这里的命名规律是,以str开头的函数处理以'\0'结尾的字符串,而以mem开头的函数则不关心'\0'字符,或者说这些函数并
不把参数当字符串看待,因此参数的指针类型是void *而非char *。
memmove也是从src所指的内存地址拷贝n个字节到dest所指的内存地址,虽然叫move但其实也是拷贝而非移动。但是和memcpy有一点不同,memcpy的两个参数src和dest所指的内存区间如果重叠则无法保证正确拷贝,而memmove却可以正确拷贝。假设定义了一个数组char buf[20] ="hello world\n";,如果想把其中的字符串往后移动一个字节(变成"hhello world\n"),调用memcpy(buf + 1, buf, 13)是无法保证正确拷贝的:
例 25.1. 错误的memcpy调用
#include <stdio.h>#include <string.h>int main(void){ char buf[20] = "hello world\n"; memcpy(buf + 1, buf, 13); printf(buf); return 0;}
在我的机器上运行的结果是hhhllooworrd。如果把代码中的memcpy改成memmove则可以保证正确拷贝。memmove可以这样实现:
void *memmove(void *dest, const void *src, size_t n){ char temp[n]; int i; char *d = dest; const char *s = src; for (i = 0; i < n; i++) temp[i] = s[i]; for (i = 0; i < n; i++) d[i] = temp[i]; return dest;}
借助于一个临时缓冲区temp,即使src和dest所指的内存区间有重叠也能正确拷贝。思考一下,如果不借助于临时缓冲区能不能正确处理重叠内存区间的拷贝?
用memcpy如果得到的结果是hhhhhhhhhhhhhh倒不奇怪,可为什么会得到hhhllooworrd这个奇怪的结果呢?根据这个结果猜测的一种可能的实现是:
void *memcpy(void *dest, const void *src, size_t n){ char *d = dest; const char *s = src; int *di; const int *si; int r = n % 4; while (r--) *d++ = *s++; di = (int *)d; si = (const int *)s; n /= 4; while (n--) *di++ = *si++; return dest;
}
在32位的x86平台上,每次拷贝1个字节需要一条指令,每次拷贝4个字节也只需要一条指令,memcpy函数的实现尽可能4个字节4个字节地拷贝,因而得到上述结果。
C99的
restrict关键字
我们来看一个跟memcpy/memmove类似的问题。下面的函数将两个数组中对应的元素相加,结果保存在第三个数组中。
void vector_add(float *x, float *y, float *result){ int i; for (i = 0; i < 64; ++i) result[i] = x[i] + y[i]; }
如果这个函数要在多处理器的计算机上执行,编译器可以做这样的优化:把这一个循环拆成两个循环,一个处理器计算i值从0到31的循环,另一个处理器计算i值从32到63的循环,这样两个处理器可以同时工作,使计算时间缩短一半。但是这样的编译优化能保证得出正确结果吗?假如result和x所指的内存区间是重叠的,result[0]其实是x[1],result[i]其实是x[i+1],这两个处理器就不能各干各的事情了,因为第二个处理器的工作依赖于第一个处理器的最终计算结果,这种情况下编译优化的结果是错的。这样看来编译器是不敢随便做优化了,那么多处理器提供的并行性就无法利用,岂不可惜?为此,C99引入restrict关键字,如果程序员把上面的函数声明为void vector_add(float*restrict x, float *restrict y, float *restrict result),就是告诉编译器可以放心地对这个函数做优化,程序员自己会保证这些指针所指的内存区间互不重叠。
由于restrict是C99引入的新关键字,目前Linux的Man Page还没有更新,所以都没有restrict关键字,本书的函数原型都取自Man Page,所以也都没有restrict关键字。但在C99标准中库函数的原型都在必要的地方加了restrict关键字,在C99中memcpy的原型是void *memcpy(void * restrict s1,const void * restrict s2, size_t n);,就是告诉调用者,这个函数的实现可能会做些优化,编译器也可能会做些优化,传进来的指针不允许指向重叠的内存区间,否则结果可能是错的,而memmove的原型是void *memmove(void *s1, const void *s2,size_t n);,没有restrict关键字,说明传给这个函数的指针允许指向重叠的内存区间。在restrict关键字出现之前都是用自然语言描述哪些函数的参数不允许指向重叠的内存区间,例如在C89标准的库函数一章开头提到,本章描述的所有函数,除非特别说明,都不应该接收两个指针参数指向重叠的内存区间,例如调用sprintf时传进来的格式化字符串和结果字符串的首地址相同,诸如此类的调用都是非法的。本书也遵循这一惯例,除非像memmove这样特别说明之外,都表示“不允许”。
关于restrict关键字更详细的解释可以参考[
BeganFORTRAN]。
字符串的拷贝也可以用strdup(3)函数,这个函数不属于C标准库,是POSIX标准中定义的,POSIX标准定义了UNIX系统的各种接口,包含C标准库的所有函数和很多其它的系统函数,在第 2 节 “C标准I/O库函数与Unbuffered I/O函数”将详细介绍POSIX标准。
#include <string.h>char *strdup(const char *s);返回值:指向新分配的字符串
这个函数调用malloc动态分配内存,把字符串s拷贝到新分配的内存中然后返回。用这个函数省去了事先为新字符串分配内存的麻烦,但是用完之后要记得调用free释放新字符串的内存。
1.4. 连接字符串
#include <string.h>char *strcat(char *dest, const char *src);char *strncat(char *dest, const char *src, size_t n);返回值:dest指向哪,返回的指针就指向哪
strcat把src所指的字符串连接到dest所指的字符串后面,例如:
char d[10] = "foo";char s[10] = "bar";strcat(d, s);printf("%s %s\n", d, s);
调用strcat函数后,缓冲区s的内容没变,缓冲区d中保存着字符串"foobar",注意原来"foo"后面的'\0'被连接上来的字符串"bar"覆盖掉了,"bar"后面的'\0'仍保留。
strcat和strcpy有同样的问题,调用者必须确保dest缓冲区足够大,否则会导致缓冲区溢出错误。strncat函数通过参数n指定一个长度,就可以避免缓冲区溢出错误。注意这个参数n的含义和strncpy的参数n不同,它并不是缓冲区dest的长度,而是表示最多从src缓冲区中取n个字符(不包括结尾的'\0')连接到dest后面。如果src中前n个字符没有出现'\0',则取前n个字符再加一个'\0'连接到dest后面,所以strncat总是保证dest缓冲区以'\0'结尾,这一点又和strncpy不同,strncpy并不保证dest缓冲区以'\0'结尾。所以,提供给strncat函数的dest缓冲区的大小至少应该是strlen(dest)+n+1个字节,才能保证不溢出。
1.5. 比较字符串
#include <string.h>int memcmp(const void *s1, const void *s2, size_t n);int strcmp(const char *s1, const char *s2);int strncmp(const char *s1, const char *s2, size_t n);返回值:负值表示s1小于s2,0表示s1等于s2,正值表示s1大于s2
memcmp从前到后逐个比较缓冲区s1和s2的前n个字节(不管里面有没有'\0'),如果s1和s2的前n个字节全都一样就返回0,如果遇到不一样的字节,s1的字节比s2小就返回负值,s1的字节比s2大就返回正值。
strcmp把s1和s2当字符串比较,在其中一个字符串中遇到'\0'时结束,按照上面的比较准则,"ABC"比"abc"小,"ABCD"比"ABC"大,"123A9"比"123B2"小。
strncmp的比较结束条件是:要么在其中一个字符串中遇到'\0'结束(类似于strcmp),要么比较完n个字符结束(类似于memcmp)。例如,strncmp("ABCD", "ABC", 3)的返回值是0,strncmp("ABCD", "ABC", 4)的返回值是正值。
#include <strings.h>int strcasecmp(const char *s1, const char *s2);int strncasecmp(const char *s1, const char *s2, size_t n);返回值:负值表示s1小于s2,0表示s1等于s2,正值表示s1大于s2
这两个函数和strcmp/strncmp类似,但在比较过程中忽略大小写,大写字母A和小写字母a认为是相等的。这两个函数不属于C标准库,是POSIX标准中定义的。
1.6. 搜索字符串
#include <string.h>char *strchr(const char *s, int c);char *strrchr(const char *s, int c);返回值:如果找到字符c,返回字符串s中指向字符c的指针,如果找不到就返回NULL
strchr在字符串s中从前到后查找字符c,找到字符c第一次出现的位置时就返回,返回值指向这个位置,如果找不到字符c就返回NULL。strrchr和strchr类似,但是从右向左找字符c,找到字符c第一次出现的位置就返回,函数名中间多了一个字母r可以理解为Right-to-left。
#include <string.h>char *strstr(const char *haystack, const char *needle);返回值:如果找到子串,返回值指向子串的开头,如果找不到就返回NULL
strstr在一个长字符串中从前到后找一个子串(Substring),找到子串第一次出现的位置就返回,返回值指向子串的开头,如果找不到就返回NULL。这两个参数名很形象,在干草堆haystack中找一根针needle,按中文的说法叫大海捞针,显然haystack是长字符串,needle是要找的子串。
搜索子串有一个显而易见的算法,可以用两层的循环,外层循环把haystack中的每一个字符的位置依次假定为子串的开头,内层循环从这个位置开始逐个比较haystack和needle的每个字符是否相同。想想这个算法最多需要做多少次比较?其实有比这个算法高效得多的算法,有兴趣的读者可以参考[
算法导论]。
1.7. 分割字符串
很多文件格式或协议格式中会规定一些分隔符或者叫界定符(Delimiter),例如/etc/passwd文件中保存着系统的帐号信息:
$ cat /etc/passwdroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/bin/shbin:x:2:2:bin:/bin:/bin/sh......
每条记录占一行,也就是说记录之间的分隔符是换行符,每条记录又由若干个字段组成,这些字段包括用户名、密码、用户id、组id、个人信息、主目录、登录Shell,字段之间的分隔符是:号。解析这样的字符串需要根据分隔符把字符串分割成几段,C标准库提供的strtok函数可以很方便地完成分割字符串的操作。tok是Token的缩写,分割出来的每一段字符串称为一
个Token。
#include <string.h>char *strtok(char *str, const char *delim);char *strtok_r(char *str, const char *delim, char **saveptr);返回值:返回指向下一个Token的指针,如果没有下一个Token了就返回NULL
参数str是待分割的字符串,delim是分隔符,可以指定一个或多个分隔符,strtok遇到其中任何一个分隔符就会分割字符串。看下面的例子。
例 25.2. strtok
#include <stdio.h>#include <string.h>int main(void){ char str[] = "root:x::0:root:/root:/bin/bash:"; char *token; token = strtok(str, ":"); printf("%s\n", token); while ( (token = strtok(NULL, ":")) != NULL) printf("%s\n", token); return 0;}
$ ./a.out rootx0root/root/bin/bash
从"root:x::0:root:/root:/bin/bash:"这个例子可以看出,如果在字符串开头或结尾出现分隔符会被忽略,如果字符串中连续出现两个分隔符就认为是一个分隔符,而不会认为两个分隔符中间有一个空字符串的Token。第一次调用时把字符串传给strtok,以后每次调用时第一个参数只要传NULL就可以了,strtok函数自己会记住上次处理到字符串的什么位置(显然这是通过strtok函数中的一个静态指针变量记住的)。
用gdb跟踪这个程序,会发现str字符串被strtok不断修改,每次调用strtok把str中的一个分隔符改成'\0',分割出一个小字符串,并返回这个小字符串的首地址。
(gdb) startBreakpoint 1 at 0x8048415: file main.c, line 5.Starting program: /home/djkings/a.out main () at main.c:55 {(gdb) n6 char str[] = "root:x::0:root:/root:/bin/bash:";(gdb) 9 token = strtok(str, ":");(gdb) display str1: str = "root:x::0:root:/root:/bin/bash:"(gdb) n10 printf("%s\n", token);1: str = "root\000x::0:root:/root:/bin/bash:"
(gdb) root11 while ( (token = strtok(NULL, ":")) != NULL)1: str = "root\000x::0:root:/root:/bin/bash:"(gdb) 12 printf("%s\n", token);1: str = "root\000x\000:0:root:/root:/bin/bash:"(gdb) x11 while ( (token = strtok(NULL, ":")) != NULL)1: str = "root\000x\000:0:root:/root:/bin/bash:"
刚才提到在strtok函数中应该有一个静态指针变量记住上次处理到字符串中的什么位置,所以不需要每次调用时都把字符串中的当前处理位置传给strtok,但是在函数中使用静态变量是不好的,以后会讲到这样的函数是不可重入的。strtok_r函数则不存在这个问题,它的内部没有静态变量,调用者需要自己分配一个指针变量来维护字符串中的当前处理位置,每次调用时把这个指针变量的地址传给strtok_r的第三个参数,告诉strtok_r从哪里开始处理,strtok_r返回时再把新的处理位置写回到这个指针变量中(这是一个Value-result参数)。strtok_r末尾的r就表示可重入(Reentrant),这个函数不属于C标准库,是在POSIX标准中定义的。关于strtok_r的用法Man Page上有一个很好的例子:
例 25.3. strtok_r
#include <stdio.h>#include <stdlib.h>#include <string.h>int main(int argc, char *argv[]){ char *str1, *str2, *token, *subtoken; char *saveptr1, *saveptr2; int j; if (argc != 4) { fprintf(stderr, "Usage: %s string delim subdelim\n", argv[0]); exit(EXIT_FAILURE); } for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) { token = strtok_r(str1, argv[2], &saveptr1); if (token == NULL) break; printf("%d: %s\n", j, token); for (str2 = token; ; str2 = NULL) { subtoken = strtok_r(str2, argv[3], &saveptr2); if (subtoken == NULL) break; printf(" --> %s\n", subtoken); } } exit(EXIT_SUCCESS);}
$ ./a.out 'a/bbb///cc;xxx:yyy:' ':;' '/'1: a/bbb///cc --> a --> bbb
--> cc2: xxx --> xxx3: yyy --> yyy
a/bbb///cc;xxx:yyy:这个字符串有两级分隔符,一级分隔符是:号或;号,把这个字符串分割成a/bbb///cc、xxx、yyy三个子串,二级分隔符是/,只有第一个子串中有二级分隔符,它被进一步分割成a、bbb、cc三个子串。由于strtok_r不使用静态变量,而是要求调用者自己保存字符串的当前处理位置,所以这个例子可以在按一级分隔符分割整个字符串的过程中穿插着用二级分隔符分割其中的每个子串。建议读者用gdb的display命令跟踪argv[1]、saveptr1和saveptr2,以理解strtok_r函数的工作方式。
Man Page的BUGS部分指出了用strtok和strtok_r函数需要注意的问题:这两个函数要改写字符串以达到分割的效果这两个函数不能用于常量字符串,因为试图改写.rodata段会产生段错误在做了分割之后,字符串中的分隔符就被'\0'覆盖了strtok函数使用了静态变量,它不是线程安全的,必要时应该用可重入的strtok_r函数,以后再详细介绍“可重入”和“线程安全”这两个概念
习题
1、出于练习的目的,strtok和strtok_r函数非常值得自己动手实现一遍,在这个过程中不仅可以更深刻地理解这两个函数的工作原理,也为以后理解“可重入”和“线程安全”这两个重要概念打下基础。上一页上一级 下一页

25

C
标准库起始页