字符串
C使用原始的字符数组储存字符串,字符串就是在字符数组结尾加上’\0’表示结束,这种原始的方式的优缺点和数组相同,优点是性能好且结构透明,缺点是编码效率低,且因为字符长度不等于数组长度,总要考虑结尾的’\0’,还要随时观察数组的长度进一步增加了复杂性。在C中声明一个字符串需要保证数组长度足够,例如:
char c[20]=”Hello World”; //等效于char c[20]={“Hello World”};
字符数组长度为20,字符串长度为11,实际使用12个元素,如果字符串今后不再修改,则多余的空间就被浪费掉了,要想不浪费又不想数数,我们常常使用下面方法:
char c[]=”Hello World”;
注意字符串必须进行初始化,即便是一个空字符串也是如此,例如:
char a[20]=””;
看似空字符串什么都没有,其实在初始化时编译器将数组所有的元素都设置为0,因此第一个字符为\0,如果不进行初始化则数组内容是未知的,对字符串a的任何操作都可能产生意外的结果。在堆上声明字符串同样需要初始化:
#include <stdio.h>
#include <string.h>
#include<stdlib.h>
int main()
{
char *str=malloc(5);
memset(str,0,5);
printf("%d\n",strlen(str));
return(0);
}
在堆上声明字符串的优点是数组长度可以修改,如果字符串不再使用可以释放占用的空间,不同的是堆上声明需要调用memset()将所有字符都设置为0。如果仅使用str[0]=’\0’来执行初始化,对于某些编译器的字符串处理函数可能无法正常工作,因为这些函数认为所有元素都为’\0’,这就导致了隐藏的bug,必须将所有字符设置为0。由于字符串是原始数组,这表示字符串数组名也是常量,不能对字符串常量再次赋值,例如str=”abc”是不允许的。虽然可以通过指针变相解决这个问题,但这是另外一种形式的字符串,我们把这部分放到指针章节去解决。对于数组来说,因为数组长度固定,我们的代码除了要保证正确性还要兼顾性能,这显然不是一件容易的事情,好在C语言的标准库给我们提供了处理字符串的通用方法,下面我们来一起看看。
常用字符串函数
strlen(str) 返回字符串str的长度,这个长度不包含结尾字符’\0’
strcat(str1,str2) 连接str1和str2,将结果写到str1中,要保证str1足够大
strncat(str1,str2,n) 连接str1和str2,取n个字符,将结果写到str1中,要保证str1足够大
strcpy(str1,str2) 将str2复制到str1中,要保证str1的空间能够容纳复制的内容。
strncpy(str1,str2,len) 将str2中len个字符复制到str1中,要保证str1的长度足够容纳len个字符.
strcmp(str1,str2) 按照ascii码比较两个字符串,相等返回0,str1>str2返回正数,str1<str2返回负数。
strncmp(str1,str2,n); 只比较str1和str2前n个字符。
当str1的长度不够容纳str2时,连接和复制函数会导致str1溢出从而产生异常,这种溢出比gets()溢出强,因为这个错误是程序员的疏忽导致的,而gets()溢出会被黑客用于攻击。
下面函数用于搜索字符串:
strchr(str,c) 如果字符串str包含字符c,返回str中第一个找到的字符指针,如果未找到返回NULL
strrchr(str,c) 如果字符串str包含字符c,返回str中最后一个找到的字符指针,如果未找到返回NULL
strstr(str1,str2) 如果字符串str1包含str2,返回str1中第一个找到的字符指针,如果未找到返回NULL
strpbrk(str1,str2) 如果str1包含str2中的任意字符,返回第一个找到字符的指针,如果未找到返回NULL
strspn(str1,str2) 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标
strcspn(str1,str2) 检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符
strchr()和strrchr()用于正向和反向搜索字符,strstr()和strpbrk()用于测试两个字符串包含和交集。strspn()和strcspn()比较两个相似的字符串不同位置。
上面这几个是最常用的字符串处理函数,由于这些方法不是c语言的标准,需要使用#include <string.h>导入字符串库,不同编译器提供的方法可能会有不同,但最基本的几个方法都是相同的,例如对于strncpy()和strncat(),当复制或连接的字符个数超限时,有的编译器会自动处理,有的编译器会产生内存溢出。另外从字符串处理函数中的参数可以看出,它们通过形参来修改原有的字符串而不是返回新的字符串。
大小写转换
string.h提供了两个字符串大小写转换函数:
strlwr() 把字符串转换为小写
strupr() 把字符串转换为大写
ctype.h提供了字符大小写转换函数toupper()和tolower(),对于不支持strlwr()和strupr()的编译器需要遍历字符串更换大小写。例如:
#include <stdio.h>
#include<string.h>
#include<ctype.h>
int main()
{
char str[20] = “AbcDefG”;
char* p = str;
do *p = toupper(p); while (++p);
puts(str);
return(0);
}
字符匹配
当我们需要验证字符串内容时就涉及到匹配,C语言不可能有正则表达式这样智能的匹配方式,只提供了简单的字符验证函数,这些函数包含在ctype库中,需要导入ctype.h,如下:
isalnum() 字母数字(字母或数字)
isalpha () 字母
isblank() 标准的空白字符(空格、水平制表符或换行符)或任何其他本地化指定为空白的字符
iscntrl () 控制字符,如Ctrl+B
isdigit () 数字
isgraph () 除空格之外的任意可打印字符
islower () 小写字母
isupper () 大写字母
isprint () 可打印字符
ispunct () 标点符号(除宦格或字母数字字符以外的任何可打印字符
isspace () 空白字符(空格、换行符、换页符、回车符、垂直制表符、水平制表符或其他本地化定义的字符)
isxdigit() 十六进制数字符
其中isalpha()和isdigit()最常用,用于区分字母和数字,isalnum()和isspace()用于判断内容是否空白。
字符串串联
如果将字符串写为"abc"“efg”“ghi”,会被编译器自动串联为"abcefgghi",平时我们不会这么写,但是当使用宏时会出现这种情况,具体请参考预编译章节。
分解字符串
strtok()函数可以将字符串按照指定的分隔符分解成子字符串,格式为:
char *strtok(char *str, const char *delim)
每次调用strtok()都会返回一个分割的子字符串,如果找不到返回NULL,例如:
#include <string.h>
#include <stdio.h>
int main() {
char str[80] = “This is - www.runoob.com - website”;
const char s[2] = “-”;
char* token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while (token != NULL)
{
printf("%s\n", token);
token = strtok(NULL, s);
}
return(0);
}
如果要继续查找,可以将第一个参数设置为NULL,搜索指针将从当前位置查找。注意strtok()会修改原字符串,将分隔符改为’\0’。
数值和字符串之间的转化
数值和字符串之间的转化分为两种情况,一种是字符串以数字开头,例如"100"或"100abc",另一种情况是字符串以非数字开头,例如"abc123"或&123",我们先来看看第一种情况。
数字开头的字符串
对于以数值开头的字符串,无需解析内容,可以直接使用stdlib库提供了的一系列函数,如下:
atoi() 把字符串转换为整数
atol()/strtol()/strtoll() 把字符串转换为长整数
strtoul()/strtoull() 把字符串转换为无符号长整形
atof() 把字符串转换为单精度浮点
strtod() 把字符串转换为双精度浮点
itoa() 把整数转换为字符串
ltoa()/lltoa() 把长整数转换为字符串
ultoa()/ulltoa() 把无符号长整形转换为字符串
gcvt()/ecvt()/fcvt() 将浮点转换为字符串
strtol()与atoi()都只能转化数字开头的字符串,区别是strtol()能告诉你转化是否成功,它还支持进制,格式如下:
long strtol(const char*s, char** endptr, int base)
s为需要转化的字符串,endptr为指针的指针,记录转化结果。如果字符串开头为字母,strtol()和atol()一样返回0,但是可以根据endptr判断是否转化成功,如果转化成功,*endptr指向s,如果不成功指向第一个不匹配的地址。例如:
#include <stdio.h>
#include<string.h>
#include<ctype.h>
#include<stdlib.h>
int main()
{
char* end=NULL;
int res_help = strtol(“help”, &end, 10);
if (!*end)
printf("Converted successfully\n");
else
printf("Conversion error, non-convertible part: %s", end);
return 0;
}
如果换做atol(),将无法区别"hello"和"0",strtol()起码能告诉你转换失败,另外参数base表示进制,strtol()可以将字符串内容作为八进制、十六进制解析。如果你在VS中编写这段代码会遇到编译错误,VS将itoa()这些函数视作不安全函数,要通过编译需要在项目属性栏中加入宏_CRT_NONSTDC_NO_DEPRECATE和_CRT_SECURE_NO_WARNINGS,如图:
非数值开头的字符串
对于含有杂质的字符串,如果知道字符串的结构,可以通过printf()和scanf()两个函数输出和解析字符串,但这两个函数只能用于和控制台交互,如果不通过控制台输入输出可以使用sprintf()和sscanf(),它们都包含于stdio库中。
sprintf()
这个方法可以将数据以格式化方式写入字符串而不是输出到控制台,它的第一个参数是目标字符串,有了这个函数,连接字符串又多了一种方法,更棒的是可以同时连接n个混合数值的数据,例如:
#include <stdio.h>
int main()
{
char* str1 = “first”;
char* str2 = “second”;
char str[20] = “”;
sprintf(str, "%s %d %s %d", str1, 1, str2, 2);
puts(str);
return(0);
}
sscanf()
这个方法将字符串按照格式化信息解析并赋值给其它变量,规则和scanf()相同,默认情况下只能转化数字开头的字符串,但格式控制符能力强大,还支持正则表达式,这就使得sscanf()也能解析字母开头的字符串。当字符串以字母开头时如果知道个数可以根据个数取出,例如:
#include <stdio.h>
int main()
{
char str[10] = “abc123”;
char str1[10] = “”;
int n = 0;
sscanf(str, “%3s%d”, str1, &n);
printf("%s,%d",str1,n);
return 0;
}
这段代码先用%3s将前面3个字符解析出来,然后将数值123解析出来赋值给n。如果不知道是否以字母开头,亦不知道字母个数可以使用正则表达式忽略开头字母,如下:
#include <stdio.h>
#include<ctype.h>
int main()
{
char str[20] = “ABC123ghi”;
int n = 0;
int result = 0;
result=sscanf(str,"%d",&n);
if(result==0) result=sscanf(str, "%[^0-9]%d", &n);
printf("%d\n",n);
return 0;
}
sscanf()返回成功赋值的变量个数,如果字符串以数字开头sscanf()返回1,赋值成功,如果以非字符开头sscanf()返回0,此时用%[0-9]%d替代%s再次解析,正则表达式忽略字符串开头的所有非数字字符,遇到数字停止匹配,即抛弃了字符串前面的"abcEFG"后剩下"123ghi"与"%d"匹配,n被赋值为123。为什么不能直接用%*[0-9]%d匹配字母开头的字符串?因为一开始丢弃失败会导致sscanf()直接退出不再匹配后面的格式符。关于格式符、正则表达式的详细内容请参阅标准输入输出章节,sscanf()和sprintf()以格式化方式读写字符串,scanf()和printf()以格式化方式与控制台交互,fscanf()和fprintf()以格式化方式读写文件。