C语言常用字符串函数
文章目录
一、基础知识
为了更好的理解后面提到的一些字符串处理函数,我们先补充一些基础知识。假如你已经掌握,可以直接跳过,进入第二部分。
1. 常量指针与指针常量
先来看一个例子:
int a = 100; const int * p = &a; //常量指针,指向的值不可更改,但指向的地址可以更改
int const * p = &a; //与上式等价
int * const p = &a; //指针常量,指向的地址不可以更改,但指向的值可更改
那么该怎么区分这两种形式呢?从左往右看,跳过类型,看const修饰哪个字符。如果是*, 说明指针指向的值不能改变,如果是指针变量,说明指针的指向不能改变,指针的值不能修改。这个原则你可以通俗理解成 “就近原则”。
我们利用这个规则对上面提到的例子进行分析:
int a = 100;
const (int) * p = &a;
(int) const * p = &a;
//将类型int隐去,则可以看到这两种形式,const修饰的都是*p,即指针变量p所指 向的值不能被改变,但p的值可以改变。
(int) * const p = &a;
//将类型int隐去,const修饰指针变量p,p存储的是什么,是变量a的地址。p的值 不能改变,但是p指向的变量的值可以改变。
2. 字符串的存储区
C语言中字符串一般存储在两个地方:数据常量区和栈区。
我们还是先来看一段代码说明:
//存储在栈区的字符串,存放与字符数组str1中,我们可以通过str1指针修改其值,但是str1本身是一个常来常量我们不能对其进行修改。类似于我们前面定义char *const str1;
char str1[] = "hello world";
str1[1] = 'm';
char str4[] = "hello world";
//存储在常量区的字符串,即该指针执行我们不可以通过str2指针修改其值。类似与前面提到的定义:const char *str2或者char const *str2;
char *str2 = "hello world";
char *str3 = "hello world";
//指针变量str2和str3指向同一块数据常量区,所以他们的地址是一样的。而str1,str4是两个不同的自动给变量,存储在栈区,所以他们的值(地址)应该是不同的。
printf("str2 = %p\n",str2);
printf("str3 = %p\n",str3);
printf("str1 = %p\n",str1);
printf("str4 = %p\n",str4);
运行结果如下:
上面两行代码中,我们将str2与str3指向的内存地址分别打印出来,发现他们的值是一样的,为什么呢,这是因为常量区内存的值是只读的,我们即便声明两个不同的变量,只要他们的值是相同的,那么两个变量指向的就是同一块内存区域。
这里值得注意的是,在c++中,字符串指针与c语言中稍有区别,c++中直接将字符串指针做了增强处理,因为c++中规定字符串指针必须用const修饰,例如在c++中这样定义,编译器会直接报错:
char* str = "hello world"; //直接报错
const char * str = "hello world"; //正确
而在实际开发过程中,我们使用字符串一般使用数组形式,不太建议使用指针字符串形式,也即:
char str[] = "hello world"; //建议使用
char* str = "hello world"; //不建议使用
3. 字符串长度
我们还从一个小例子来看:
char str1[] = "hello";
char* str2 = "hello";
printf(" sizeof(str1) = %ld\n", sizeof(str1)); //输出结果为 6
printf(" sizeof(str2) = %ld\n", sizeof(str2)); //输出结果为 4 或者 8 (由编译器决定,32位的机器中,该值为4,64位的机器中,该值为8)
运行结果为:
这里的str2我们定义的是一个字符串指针,它指向一个常量区的字符串,而sizeof操作符操作这个指针的时候,实际上计算的是这个指针所占用的存储空间的大小。因为我本地的机器为64位,所以这里的sizeof(str2)值为8,即每个指针变量需要8个字节来存储(机器位数,其实也就代表着机器的寻址能力)。
下面我们来分析为什么str1的长度为6,我们明明只存进去5个字符。不应该是 5 才对吗?这是因为在声明一个字符串的时候,系统会自动为你的结尾添加上一个以 ‘\0’ 为结尾的结束字符,内存模型如下:
所以我们以后再遇到字符串的时候,一定要再心里给’\0’留个位置,这很重要。
那么我们怎么计算str2字符串的长度呢?别急,接着往下看。
二、字符串处理函数
1. 字符串长度计算
所需头文件
#include <string.h>
函数原型
size_t strlen(const char *s);
功能
计算字符串的长度
参数
s:传入的字符串指针
返回值
字符串长度
strlen()比较简单,但有以下几点需要说明一下:
- strlen 计算字符串也是以’\0’为结束的。strlen() 函数会不断判断当前字符是否为 ‘\0’,如果是的话,立马结束。
- strlen计算的长度是不含’\0’的,其返回的长度只包含’\0’以前的字符个数。这点注意和sizeof()区分,sizeof计算的字符数组的长度是包含’\0’的。
下面来看一个例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[] = "hello";
char* str2 = "hello";
char str3[] = {'h','e','\0', 'l','l','0'};
char str4[] = {'h', 'e', 'l','l','o', '\0'};
printf("strlen(str1) = %ld\n", strlen(str1));
printf("sizeof(str1) = %ld\n", sizeof(str1));
printf("strlen(str2) = %ld\n", strlen(str2));
printf("strlen(str3) = %ld\n", strlen(str3));
printf("sizeof(str3) = %ld\n", sizeof(str3));
printf("strlen(str4) = %ld\n", strlen(str4));
printf("sizeof(str4) = %ld\n", sizeof(str4));
return 0;
}
运行结果如下:
可以看到,strlen()计算字符串长度时,遇到’\0’就结束,且其计算的字符串长度时不含’\0’的。
2. 字符串复制
所需头文件
#include <string.h>
函数原型
char *strcpy(char *dest, const char *src);
功能
把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
参数
dest:目的字符串首地址
src:源字符首地
返回值
成功:返回dest字符串的首地址
失败:NULL
先来看一个小例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[] = "hello\0world";
char* str2 = "hello";
char str3[] = {'h','e','\0', 'l','l','0'};
char str[3][16];
//strcpy(&str[0][0],str1);
//strcpy(&str[1][0],str2);
//strcpy(&str[2][0],str3);
strcpy(str[0],str1);
strcpy(str[1],str2);
strcpy(str[2],str3);
printf("str[0] = %s\n",str[0]);
printf("str[1] = %s\n",str[1]);
printf("str[2] = %s\n",str[2]);
return 0;
}
输出结果如下:
其实现的内存模型如下:
对于strcpy()我们有以下几点需要注意:
1. strcpy()会将src的’\0’也复制到dest的末尾。
2. 由于是逐个拷贝,意味着哪怕在字符串的中间遇到了 ‘\0’ 字符,也会结束拷贝。
3. 必须保证 dest 所指向的内存空间足够大,否则可能会造成缓冲溢出的错误。如数组访问越界,踩内存等。
4. 由于本身strcpy函数是一个非安全函数,所以我们使用的时候一定要注意。要想成为一个C语言高手,对于这些细节必须要特别注意。
所需头文件
#include <string.h>
函数原型
char *strncpy(char *dest, const char *src);
功能
把src指向字符串的前n个字符复制到dest所指向的空间中,拷贝是否提前结束看指定的长度是否包含'\0'。
参数
dest:目的字符串首地址
src:源字符首地
n:指定需要拷贝字符串个数
返回值
成功:返回dest字符串的首地址
失败:NULL
这个函数与strcpy()类似,我们先来看一个例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[] = "hello\0world";
char* str2 = "hello";
char str3[] = {'h','e','\0', 'l','l','o'};
char str[3][16];
strncpy(str[0],str1,12);
strncpy(str[1],str2,4);
strncpy(str[2],str3,sizeof(str[2]));
printf("str[0] = %s\n",str[0]);
printf("str[1] = %s\n",str[1]);
printf("str[2] = %s\n",str[2]);
printf("\n");
return 0;
}
输出如下:
对于strncpy()函数,和strcpy()用法基本一样,但是有以下几点需要注意以下:
1. strncpy遇到’\0’或到达指定长度就停止拷贝,如果src长度小于n,则拷贝到src的最后一位即’\0’为止
2. 如果src的前n个字节里没有’\0’则dest将不会以’\0’结尾。
3. strncpy()相对与strcpy()更安全点儿,即我们可以指定拷贝长度。
3. 字符串拼接
所需头文件
#include <string.h>
函数原型
char *strcat(char *dest, const char *src);
功能
将src字符串连接到dest的尾部,‘\0’也会追加过去
参数
dest:目的字符串首地址
src:源字符首地
返回值
成功:返回dest字符串的首地址
失败:NULL
strcat是一个字符串拼接函数,src的’\0’也会被追加过去。下面来看一个小例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[32] = "hello";
char str2[] = "world";
char *tmp_str = strcat(str1,str2);
printf("tmp_str = %s\n",tmp_str);
return 0;
}
输出结果为:
同样的dest要有足够的存储空间来接收,否则会报错。
所需头文件
#include <string.h>
函数原型
char *strncat(char *dest, const char *src,size_t n);
功能
将src字符串前n个字符连接到dest的尾部,‘\0’也会追加过去
参数
dest:目的字符串首地址
src:源字符首地
n:指定需要追加字符串个数
返回值
成功:返回dest字符串的首地址
失败:NULL
strncat与strcat用法和功能基本类似,不同的是strncat可以指定连接src字节数。
同样的,我们来看一个小例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[16] = "hello";
char str2[] = "world";
char *tmp_str = strncat(str1,str2,4);
printf("tmp_str = %s\n",tmp_str);
return 0;
}
运行结果为:
对于strcat()比较简单,但是有以下几点需要注意一下:
1. strcat()返回dest的首地址,即连接后的字符串。
2. dest要有足够的缓存空间,否则会报错,造成缓冲溢出。
3. strcat()会覆盖dest原字符串末尾的’\0’,并且会将src末尾的’\0’也连接到新字符串的末尾。
4. 字符串比较函数
所需头文件
#include <string.h>
函数原型
int strcmp(char *s1, const char *s2);
功能
比较字符串s1和s2,自左向右逐个按照ASCII码值进行比较,直到出现不同的字符或遇’\0’为止。
参数
s1:字符串1的首地址
s2:字符串2的首地址
返回值
如果返回值 < 0,则表示 s1 小于 s2。
如果返回值 > 0,则表示 s1 大于 s2。
如果返回值 = 0,则表示 s1 等于 s2
需要注意的是strcmp()只能比较字符串,其他形式参数不能比较。
int main(void)
{
char sr1[] = "English";
char sr2[] = "ENGLISH";
char sr3[] = "english";
char sr4[] = "English";
//strcmp()只能比较字符串, 其他形式的参数不能比较
printf("strcmp(str1, str2) : %d\n", strcmp(sr1, sr2));
printf("strcmp(str1, str3) : %d\n", strcmp(sr1, sr3));
printf("strcmp(str1, str4) : %d\n", strcmp(sr1, sr4));
printf("strcmp(&sr1[2], \"glish\"):%d\n", strcmp(&sr1[2], "glish"));
return 0;
}
运行结果如下:
strcmp()会逐个字符的比较s1和s2中的每个字符,如果全部相同,则返回0。如果出现不相同的字符,则比较不相同的两个字符,然后返回字符串s1中对应的字符串的ASCLL码值减去s2中字符串中对应的字符的ASCLL码值。
所需头文件
#include <string.h>
函数原型
int strncmp(char *s1, const char *schar *s2, size_t n);
功能
比较字符串s1和s2,自左向右逐个按照ASCLL码值比较s1和s2的前n个字符,直到出现不同的字符或遇’\0’为止。
参数
s1:字符串1的首地址
s2:字符串2的首地址
n:指定比较的字符个数
返回值
如果返回值 < 0,则表示 s1 小于 s2。
如果返回值 > 0,则表示 s1 大于 s2。
如果返回值 = 0,则表示 s1 等于 s2
strncmp和strcmp用法基本一致。先看个简单的小例子,看下怎么用。
#include <stdio.h>
#include <string.h>
int main(void)
{
char sr1[] = "English";
char sr2[] = "EngLISH";
//strncmp()也只能比较字符串, 其他形式的参数不能比较
printf("strncmp(str1, str2,3) : %d\n", strncmp(sr1, sr2,3));
printf("strncmp(str1, str2,5) : %d\n", strncmp(sr1, sr2,5));
printf("strncmp(str2, str1,5) : %d\n", strncmp(sr2, sr1,5));
return 0;
}
运行结果如下:
strcmp()和strncmp()两个接口的用法也比较简单,一般是用来比较两个字符串是否相等。用法细节前面都有表述,这里不做赘述。
5. 格式化字符
所需头文件
#include <stdio.h>
函数原型
int sprintf(char *str, const char *format, ...);
功能
根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符 '\0' 为止。
参数
str:字符串首地址
format:字符串格式,用法和printf()一样
...:表示该函数是变参的
返回值
成功:实际格式化的字符个数
失败: - 1
话不多说,先看例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char dst[100] = {0x0};
int a = 10;
char src[] = "hello";
int len = sprintf(dst,"a=%d,\nsrc=%s", a, src);
printf("%s\n", dst);
printf("len = %d\n", len);
return 0;
}
输出结果为:
sprintf()用于格式化输出字符串,返回实际格式化的字符的个数,我们这里看本来应该返回14的,可实际返回的是15,因为它将’\0’计算进去了。即它实际返回的值为转换的字符个数加上’\0’字符。
sprintf()函数是的参数是可变的,即属于可变参函数接口。
sprintf()使用的还是比较多的,比如我们打造日志系统的时候,需要将相关信息格式存储。
所需头文件
#include <stdio.h>
函数原型
int sscanf(const char *str, const char *format, ...);
功能
从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数
str:指定的字符串首地址
format:字符串格式,用法和scanf()一样
...:表示该函数是变参的
返回值
成功:参数数目,成功转换的值的个数
失败: - 1
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "a=10, b=20";
int a;
int b;
sscanf(src, "a=%d, b=%d", &a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
输出如下:
sscanf与scanf类似,都是用于输入的,只是后者以屏幕(stdin)为输入源,前者以固定字符串为输入源。sscanf()的用法也较为广泛,即从指定的字符串中解析出我们想要的格式化的数据。
6. 字符串查找
所需头文件
#include <string.h>
函数原型
char *strchr(const char *s, char c);
功能
在字符串s中查找字母c出现的位置
参数
s:字符串首地址
c:匹配字母(字符)
返回值
成功:返回第一次出现的c地址(注意是地址,不是字符数组索引)
失败:NULL
我们来看一个例子,加深理解:
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "ddda123abcd";
char *p = strchr(src, 'a');
printf("p = %s\n", p);
return 0;
}
输出如下:
注意,这里strchr返回第一次出现字符c的指针。
所需头文件
#include <string.h>
函数原型
char *strstr(const char *haystack, const char *needle);
功能
在字符串haystack中查找字符串needle出现的位置
参数
haystack:源字符串首地址
needle:匹配字符串首地址
返回值
成功:返回第一次出现的needle地址
失败:NULL
strstr()函数是字符串中寻找指定的字符串。来看一个例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "ddda123abcd";
char *p = strstr(src, "123");
printf("p = %s\n", p);
return 0;
}
输出为:
strstr()同strchr()不同的是,它查找的是字符串。其他的比较简单,这里不再赘述。
7. 字符串分割
所需头文件
#include <string.h>
函数原型
char *strtok(char *str, const char *delim);
功能
将字符串分割成一个个片段, 当strtok()在参数str的字符串中发现参数delim中包含的分割 字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0,该函数会破坏原有字符串。首次调用strtok()时候需要传入需要源字符串的地址,再次调用时,把str设为NULL,会继续向后进行分隔,直到字符串末尾。
参数
str:指向欲分割的字符串
delim:分割字符串中包含的所有字符
返回值
成功:分割后字符串首地址
失败或查到到末尾时返回NULL
查找不到delim中的字符时,返回当前strtok的字符串指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
有一点需要注意的是,strtok()对字符串进行分割时会破坏源字符串的内容。
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "ddda123abcd";
char *p = strtok(src, "123");
printf("p = %s\n", p);
return 0;
}
输出为:
strtok()会将源字符串中出现指定字符串中任一字符的地方替换为’\0’,然后返回。
为了验证上述说法,我们改变需要查找的字符串的顺序。
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "ddda123abcd";
char *p = strtok(src, "321");
printf("p = %s\n", p);
return 0;
}
运行结果为:
需要注意的是,使用该函数进行字符串分割时,会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。
第一次分割之后,原字符串str是分割完成之后的第一个字符串,剩余的字符串存储在一个静态变量中,因此多线程同时访问该静态变量时,则会出现错误。
一般不建议使用,如果要保持原字符串的完整,可以使用strchr和sscanf的组合等。
在网络编程中我们有时会用strtok()来分隔IP地址,下面给出一个应用的例子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char src[] = "192.168.1.24";
int a[4],i;
char *p = strtok(src, ".");
for(i = 0;i < sizeof(a) && p != NULL;i++)
{
printf("p = %s\n", p);
sscanf(p,"%d",&a[i]);
printf("a[%d] = %d\n",i,a[i]);
p = strtok(NULL,"."); //注意,这里再次调用的时候传入的NULL
}
return 0;
}
运行结果为:
三、总结
日常开发中常用到的一些字符串处理函数已经整理如上了,要成为C语言高手,这些基础内容需要我们用心去掌握。
这里总结的只是一些常用的字符串处理函数,后续也会陆续补充一些进来,以飨读者,敬请关注。