3

C语言中的“字符串”

 1 year ago
source link: https://yuxinli1.github.io/C%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E2%80%9C%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9D/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

不像一些语言中存在字符串的变量类型,C语言中,字符串就是char类型的数组。在C语言中,处理字符串也有可以调用的库,即string.h,它是标准库中的一员。如果想要复制、比较、连接字符串,那么string.h就要上场了!

以下是本文涉及到的函数:



string.h
strcmp()
strncmp()
strcpy()
strncpy()
strcat()
strncat()
strlen()
strchr()
strstr()
strcspn()
strspn()


string.h
memcpy()
memmove()
memset()
memcmp()
memchr()

字符串的初始化

字符串有两种初始化方式,一种是通过定义字符数组初始化,一种是以字符串常量的方式初始化。通过字符数组的方式如下:

char str1[] = "Hello World!" // 这种方式编译器会自动计算字符串的长度并置为数组长度,在这里是13(包括\0)
char str2[20] = "Hello World!"; // 给定数组长度的方式

可以分别查看一下两个字符串和字符数组的大小:

printf("%ld %ld %ld %ld\n", sizeof(str1), strlen(str1), sizeof(str2), strlen(str2));
// output: 13 12 20 12

另外还可以通过定义字符串常量的方式定义字符串,如:

char *str = "Hello World!";
02.png

我们知道,存储器中有不同的区段,用这种方法定义的字符串会存储在常量段,指针str指向常量段存储的字符串,而常量区段是只读的不可更改的,因此我们不能通过像修改字符串数组中某个字符那样的方法来修改字符串常量:

str1[1] = "o"; // 正确
str[1] = "o"; // 错误,编译时会给出一个警告但是能够变已成功,运行时会发生错误

通过数组的方式定义char str1[] = "Hello World!";,常量段会存储一个"Hello World!",当代码运行到该处时,程序会在栈内开辟一块空间,并将常量段中的"Hello World!"复制到栈中,因此通过数组定义的字符串可以修改。

所以,为了使代码更加清晰,如果要使用指针的方式定义初始化字符串,我们通常需要加一个const关键字:

const char *str = "Hello World!";

字符串数组

字符串数组有两种创建方式:通过创建字符串数组的数组,即二维字符数组的方式;通过使用字符串指针的数组。

数组的数组

使用“数组的数组”创建初始化的方法如下:

char tracks[][80] = {
"I left my heart in Harvard Med School",
"Newark, Newark - a wonderful town",
"Dancing with a Dork",
"From here to maternity",
"The girl from Iwo Jima",
};

编译器可以识别到有五个字符串,因此我们可以不指定行,也就是第一个框中的数字。但是,我们必须告诉编译器每行最大的字符数,在这里是不超过79个字符,即80。

通过这种方式定义,每个字符串都是一个数组,所以我们创建了一个二维数组。

指针的数组

除此之外,我们也可以通过创建指针数组的方式创建并初始化字符串:

char *tracks[] = {
"I left my heart in Harvard Med School",
"Newark, Newark - a wonderful town",
"Dancing with a Dork",
"From here to maternity",
"The girl from Iwo Jima",
};

与字符串的初始化一样,通过“指针的数组”创建字符串数组,不像通过创建二维数组的方式创建字符串数组,每一个行元素都是一个字符数组,这种方式创建的数组中的元素均是字符串指针,因此同样无法修改其中的内容,原理同上。

strcmp(): 字符串比较

/* Compare S1 and S2.  */
int strcmp (const char *__s1, const char *__s2);
/* Compare N characters of S1 and S2. */
int strncmp (const char *__s1, const char *__s2, size_t __n);
// output: 0 -1 1
printf("%d %d %d\n", strcmp("abc", "abc"), strcmp("abc", "afc"), strcmp("aic", "abc"));
// output: 0 -14
printf("%d %d\n", strncmp("aabc", "aagc", 2), strncmp("aabc", "aapc", 3));

字符串比较没有什么特殊的,根据字典序,如果完全相同返回0;如果s1小于s2返回负值;否则返回正值。

strcpy(): 字符串复制

/* Copy SRC to DEST.  */
char *strcpy (char *__restrict __dest, const char *__restrict __src);
/* Copy no more than N characters of SRC to DEST. */
char *strncpy (char *__restrict __dest,
const char *__restrict __src, size_t __n);

既然要复制字符串,一定会涉及到存储区的更新,因此传入的指针变量一定时需要指向可写存储区的,而不能是只读存储区,如常量段。

对于字符串复制,可能会存在内存泄漏。如:

char sht[] = "ECNUer";
char lng[] = "East China Normal University";
printf("sizeof(sht)=%ld strlen(sht)=%ld\n", sizeof(sht), strlen(sht));
printf("sht=%s\n", sht);
printf("lng=%s\n", lng);
strcpy(sht, lng);
printf("sizeof(sht)=%ld strlen(sht)=%ld\n", sizeof(sht), strlen(sht));
printf("sht=%s\n", sht);
printf("lng=%s\n", lng);

// output:
// sizeof(sht)=7 strlen(sht)=6
// sht=ECNUer
// lng=East China Normal University
// sizeof(sht)=7 strlen(sht)=28
// sht=East China Normal University
// lng=ina Normal University

sizeof()是在编译时就计算好的,也就是说两个printf()中的sizeof()在编译时就已经被替换为了sht定义时的长度,也就是7

所以,如果想要复制字符串的话,最好使用strncpy()函数,控制要复制的字节数量,避免内存泄漏。

strcat(): 连接字符串

/* Append SRC onto DEST.  */
extern char *strcat (char *__restrict __dest, const char *__restrict __src);
/* Append no more than N characters from SRC onto DEST. */
extern char *strncat (char *__restrict __dest, const char *__restrict __src, size_t __n);

与字符串复制函数一样,strcat()也存在内存泄漏问题,如:

char sht[10] = "ECNU";
char lng[30] = "East China Normal University";
printf("sizeof(sht)=%ld strlen(sht)=%ld\n", sizeof(sht), strlen(sht));
printf("sht=%s\n", sht);
printf("lng=%s\n", lng);
strcat(sht, lng);
printf("sizeof(sht)=%ld strlen(sht)=%ld\n", sizeof(sht), strlen(sht));
printf("sht=%s\n", sht);
printf("lng=%s\n", lng);

// output:
// sizeof(sht)=10 strlen(sht)=4
// sht=ECNU
// lng=East China Normal University
// sizeof(sht)=10 strlen(sht)=32
// sht=ECNUEast China Normal University
// lng=hina Normal University

所以,同样还是建议,如果想要连接字符串的话,使用strncat()

strlen(): 字符串长度

/* Return the length of S.  */
size_t strlen (const char *__s);

返回以\0结尾的字符串的长度,长度值不包括\0

如果一不小心把字符串末尾的\0更改了,那么输出结果就不为人知了,例如:

char str1[] = "Hello World!";
str1[12] = 'A';
printf("%ld\n", strlen(str1)); // 输出什么就不为人知了,但肯定不会是12

需要注意的是,strlen()输出的是字节数,这对于ASCII码字符串来说当然没有问题,但是如果是中文呢?

char sss[] = "我喜欢你!";
printf("%ld\n", strlen(sss)); // output: 15 (一个中文字符需要3个字节编码)

strstr(): 在字符串中查找子字符串

/* Find the first occurrence of NEEDLE in HAYSTACK.  */
char *strstr (const char *__haystack, const char *__needle);
printf("%s\n", strstr("abcdefghigklmn", "def")); // output: defghigklmn

strstr()在第一个字符串中查找第二个字符串,如果存在,则返回找到的第二个字符串在存储期中的位置。如果找不到字符串,则返回0,我们可以用此判断一个字符串是否在另一个字符串中:

if(strstr("dysfunctional", "fun"))
printf("我在dysfunctional中发现了fun!");
else:
printf("我没有发现fun");

strchr(): 在字符串中查找字符

/* Find the first occurrence of C in S.  */
char *strchr (const char *__s, int __c);
/* Find the last occurrence of C in S. */
char *strrchr (const char *__s, int __c);

尽管这里接收的参数是一个int值,但是strchr()在实现中将int强制类型转换为了char,代码如下:

// https://codebrowser.dev/linux/linux/lib/string.c.html#:~:text=char%20*strchr(const%20char%20*s%2C%20int%20c)

char *strchr(const char *s, int c)
{
for (; *s != (char)c; ++s)
if (*s == '\0')
return NULL;
return (char *)s;
}

在使用过程中需要注意这一点,c还是要传入一个char能接收的。否则:

char str[] = "喜欢我吗?";
char *target1 = "我"; // 我:E68891=\346\210\221 欢:E6ACA2=\346\254\242
int target2 = 0xE68891;
printf("%s\n", strchr(str, *target1)); // output:欢我吗?
printf("%s\n", strchr(str, target2)); // strchr返回字符串指向"\221吗?"
// 使用int的话,应该是实现中的强制类型转换导致取末位字节也就是\221作为target,
// 而是用字符指针的话,它会把指向的字符串的第一个字节也就是E6=\346作为target,
// 所以出现了以上两种结果。

strcspn() & strspn()

/* Return the length of the initial segment of S which
consists entirely of characters not in REJECT. */
size_t strcspn (const char *__s, const char *__reject);
/* Return the length of the initial segment of S which
consists entirely of characters in ACCEPT. */
size_t strspn (const char *__s, const char *__accept);
  • strcspn 检索字符串__s开头有几个字符不在字符集__reject
  • strspn 检索字符串__s开头有几个字符存在字符集__accept

这两个函数是通过strchr实现的,因此上述问题需要注意一下。举两个🌰吧:

char str[] = "East China Normal University";
char reject[] = "ori";
char accept[] = "sai tE";
printf("strcspn: %ld\n", strcspn(str, reject)); // strcspn: 7("East Ch")
printf("strspn: %ld\n", strspn(str, accept));// strspn: 5("East ")

内存相关函数

/* Copy N bytes of SRC to DEST.  */
void *memcpy (void *__restrict __dest, const void *__restrict __src, size_t __n);

/* Copy N bytes of SRC to DEST, guaranteeing
correct behavior for overlapping strings. */
void *memmove (void *__dest, const void *__src, size_t __n);

/* Set N bytes of S to C. */
void *memset (void *__s, int __c, size_t __n);

/* Compare N bytes of S1 and S2. */
int memcmp (const void *__s1, const void *__s2, size_t __n);

/* Search N bytes of S for C. */
void *memchr (const void *__s, int __c, size_t __n);

为什么string.h中会有内存相关的函数?这是历史原因造成的,最开始并不是在string.h而是在memory.h中,后来标准化的话的时候考虑到大多数人都会include string.h,所以把这组函数放到了这里。

memcpy() v.s. strcpy()

  • 复制内容不同。strcpy()只能复制字符串,而memcpy()可以复制任何内容。
  • 复制方法不同。strcpy()会一直复制直到遇到\0,而memcpy()需要指定复制的字节数。

当然,使用memcpy()也需要注意内存泄漏的问题,如果__n超过了__dest的长度,同样会内存泄漏。

memmove()

memmove()的功能与memcpy()类似,但是如果是在重叠区域上复制,memmove()会是更好的选择。例如有一个长为30的数组,我们想要将10...29的部分复制到5...24,此时如果使用memcpy()可能会得到我们不想要的结果,而memmove()对于在重叠区域上复制做了一定的检查。memmove()首先会复制一份要复制的内容,然后再复制到目标区域上。因此,尽管memmove()的速度要比memcpy()慢一些,但却进行了安全性控制。当然,在某些系统上,memcpy()memmove()的效果是一样的。

memset()

memset()用于将某一块内存空间设置为某个字符(这里同样是接收的int型参数,复制时或许也是用低八位设置内存区域?),常用来将某一块内存区域清空,更多的是用于结构体上。例如:

struct Grade
{
char Name[20];
char Course[20];
double grade;
};

myGrade.Name = {0};
myGrade.Course = {0};
myGrade.grade = 0.0;

如果使用memset()的话:

memset(&myGrade, 0, sizeof(struct Grade));

memcmp() & memchr()

有点累了,这两个不写了。memcmp()没什么需要多说的,memchr()还是需要注意传参即可。

最后,在使用这些函数的时候,我们时刻要记住的是,这些函数均是以字节为单位的,因此对于非ASCII码的内容,需要注意到这一点。除此之外,某些函数可能会存在内存泄漏,如strcpystrcmp等,我们在使用的时候需要多加注意!

References:

strspn() In C

strcspn()函数

C语言库函数:strcspn和strspn的区别

为什么string.h头文件里面有内存相关函数?

string.h文件中strcpy、memcpy和memset之间的区别

What is difference between memmove and memcpy (memmove vs memcpy)?..pdf

string.c source code [linux/lib/string.c] - Codebrowser


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK