C10 字符串和字符串函数

1. 表示字符串和字符串I/O

字符串是以空字符(\0)结尾的char类型数组。

1.1 在程序中定义字符串

1.1.1 字符串字面量(字符串常量)

用双引号括起来的内容称为字符串字面量string literal),也叫作字符串常量string constant)。双引号中的字符,编译器会自动在末尾添加\0

ANSI C标准开始,如果字符串字面量之间没有间隔,或用空白字符分隔,C会将其视为串联起来的字符串字面量。

char ca1[20] = "Hello"" World";
// 与下面等价
char ca2[20] = "Hello World";

字符串属于静态存储类别static storage class)。如果在函数中使用字符串字面量,该字符串只会被储存一次,在整个程序的生命期内存在。即使函数被调用多次。

用双引号括起来的内容被视为指向该字符串储存位置的指针。类似数组名是指向数组首元素的指针。

#include <stdio.h>
int main() {
    printf("%s %p %c\n", "Caldm", "CALDM", "caldm");
    return 0;
}
//输出结果
//Caldm 00007FF75CBC9C2C c

1.1.2 字符串数组和初始化

用指定的字符串初始化数组:

const char s[20] = "Hello World";

对比标准的数组初始化会简单许多。

const char s[20] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};

必须添加末尾的\0,否则则就不是一个字符串,而是一个字符数组。

对于char类型的数组,所有未被使用的元素会被设置为'0'(注意是char类型的0)。

const char s[] = "All in the game, all in game.";

让编译器确定初始化字符数组的大小很合理,也很方便,处理字符的函数大部分都不知道数组的长度,而是通过查找末尾的空字符来确定字符串的末尾。

1.1.3 数组和指针

当你用指针指向字符串字面量时,并尝试修改时。

#include <stdio.h>
int main() {
    char* p1 = "Klingon";
    p1[0] = 'F';
    printf("Klingon");
    printf(": Beware the %ss!\n", "Klingon");
    return 0;
}
//实际输出
//Flingon: Beware the Flingons!

因为编译器使用同一个副本来表示相同的字符串字面量,所以当修改字面量时,其他使用相同字符串字面量的地方也会同步修改。

⚠️注意:当修改字符串时,不要用指针指向字符串字面量。建议把指针初始化为字符串字面量时,使用const限定符。

可以指向字符串变量,因为字符串变量中存储的是字符串字面量的副本。

1.1.4 字符串数组

指向字符串的指针数组和char类型数组的数组。

#include <stdio.h>
int main() {
    const char* arrPt[4] = {
        "雏稚心高向云巅,惜是穷翅软爪尖。",
        "滚滚长江东逝水",
        "君不见,大河之水天上来",
        "他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”",
    };
    const char arrStr[4][100] = {
        {"雏稚心高向云巅,惜是穷翅软爪尖。"},
        {"滚滚长江东逝水"},
        {"君不见,大河之水天上来"},
        {"他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”"},
    };
    for (int i = 0; i < 4; i++) {
        printf("arrPt:  %s\n", arrPt[i]);
        printf("arrStr: %s\n", arrStr[i]);
    }
    printf("sizeof(arrPt): %d, sizeof(s2): %d\n", (int)sizeof(arrPt), (int)sizeof(arrStr));
    return 0;
}
//输出结果
//arrPt:  雏稚心高向云巅,惜是穷翅软爪尖。
//arrStr: 雏稚心高向云巅,惜是穷翅软爪尖。
//arrPt:  滚滚长江东逝水
//arrStr: 滚滚长江东逝水
//arrPt:  君不见,大河之水天上来
//arrStr: 君不见,大河之水天上来
//arrPt:  他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”
//arrStr: 他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”
//sizeof(arrPt): 32, sizeof(arrStr): 400

两者在打印效果上相同,区别在于:

  • arrPt存储的是五个指针,并且这些指针指向字符串字面量,arrStr存储的是五个字符串数组。
  • arrPt的每个字符串的长度是不同的,而arrStr存储的每个数组的长度是相同的,且长度至少为最长字符串的长度。从末尾的打印中可以看出arrPt的大小是32,arrStr的大小是400。
  • arrStr初始化时,编译器先在静态存储区中创建字符串字面量,然后再将其赋给arrStr,因此arrStr在速度上比arrPt稍慢。
  • arrStr中的内容可以更改,arrPt中的内容不可以更改。

如果要用数组存储一系列用于显示的字符串,优先使用指针数组。若需要更改内容,则使用二维字符数组。

2. 字符串输入

想把字符串读入程序,需要先预留储存空间,然后通过输入函数获取。

2.1 分配空间

给字符串分配空间时,必须指定字符串数组的长度。

2.2 gets()函数

gets()函数会读取整行输入,遇到换行符时停止,随后丢弃换行符,在末尾添加\0使其成为字符串。

它常与puts()函数配对使用,该函数用于显示字符串并在末尾添加换行符。

2.2.1 gets()函数的问题

gets()函数并不知道输入字符的多少,当输入字符大于预留的储存空间时,会导致缓冲区溢出buffer overflow)。如果多余的字符只是占用未分配的内存空间, 那么还没有问题,若是擦除了程序中其他数据,则会导致程序异常或中止。

从C99开始,不再建议使用gets()函数,C11标准正式废除了gets()函数。

2.3 gets()的替代品

C11标准之前,常用fgets()代替gets()。C11标准之后,用gets_s()代替gets()

2.3.1 fgets()函数和fputs()函数

  • fgets()函数的第二个参数指明读入字符的最大数量,如果参数值为n,则只会读入n-1个字符,或者遇到第一个换行符为止。
  • fgets()函数读到第一个换行符时会将其保存,这与gets()不同
  • fgets()函数第三个参数指明读入的文件,如果从键盘读入,则以stdin(该标识符定义在stdio.h中)为参数。

fgets()常与fputs()配对使用,fputs()函数不会在字符串末尾添加换行,它的第二个参数指明写入的文件,如果要显示在屏幕上,则用stdout作为参数。

2.3.2 gets_s()函数

  • gets_s()函数只从标准输入中读取数据。
  • gets_s()函数遇到换行符时会丢弃它。
  • gets_s()函数读到最大字符数都没有读到换行符时,首先会把目标数组中的首字符设置为空字符,读取并丢弃剩余输入直至换行符或文件末尾,然后返回空指针(NULL),最后调用依赖实现的处理函数,这可能会中止或退出程序。

2.4 总结

gets()fgets()gets_s()
换行符丢弃保留丢弃
输入用参数指定输入(stdin指定从键盘)只能读取标准输入
最大字符限制用参数指定,最大读取n-1个字符用参数指定,最大读取n-1个字符
输入过长时缓冲区溢出添加空字符,丢弃剩余输入,调用依赖实现函数的“处理函数”,可能会中止或退出程序

3. 字符串输出

C标准库有3个用于打印字符串的函数:puts()fputs()printf()

3.1 puts()函数

puts()函数接收一个字符串地址作为参数,打印时会在末尾添加换行符。

3.2 fputs()函数

fputs()函数是puts()函数针对文件定制的版本。

  • fputs()函数接收第2个参数指明写入的文件,如果打印在显示器上,则使用定义在stdio.h中的stdout作为参数。
  • fputs()函数不会在末尾添加换行符。

3.3 printf()函数

printf()函数同样接收一个字符串地址作为参数,不会在末尾添加换行符,并且它的效率也更低,但是printf()函数能格式化不同的数据类型,在打印多个字符串时更加简单。

4. 字符串函数

C提供了多个处理字符串的函数,ANSI C把这些函数放在string.h头文件中。

4.1 strlen()函数

strlen()函数用于统计字符串的长度。空字符不会计算在字符串长度内。

4.2 strcat()函数

strcat()函数接受两个字符串作为参数,该函数会把第2个字符串拼接在第1个字符串后边,并把拼接后的字符串作为第1个字符串,第2个字符串不变。

#include <stdio.h>
#include <string.h>

#pragma warning(disable : 4996)

int main() {
    char s1[20] = "123";
    char s2[10] = "456";
    printf("s1: %-10s s2: %-10s\n", s1, s2);
    strcat(s1, s2);
    printf("s1: %-10s s2: %-10s\n", s1, s2);
    return 0;
}

//实际输出
//s1: 123        s2: 456
//s1: 123456     s2: 456

⚠️注意strcat()函数无法检查第1个数组能否容纳第2个字符串。如果不能,怎会造成溢出。

4.3 strncat()函数

strncat()函数与strcat()函数类似,不过strncat()函数的第3个参数指定了最大添加字符数。

#include <stdio.h>
#include <string.h>

#pragma warning(disable : 4996) // 忽略这行代码

int main() {
    char s1[10] = "123";
    char s2[10] = "456789";
    printf("s1: %-10s s2: %-10s\n", s1, s2);
    strncat(s1, s2, 3);
    printf("s1: %-10s s2: %-10s\n", s1, s2);
    return 0;
}
//实际输出
//s1: 123        s2: 456789
//s1: 123456     s2: 456789

4.4 strcmp()函数

若直接比较两个字符串,如s1 == s2,那么实际比较的并不是字符串,而是两个字符串首字符的地址,所以这个条件表达式的结果永远为假值。

使用C标准库中的strcmp()函数可以比较两个字符串的内容,内容相同返回0,否则返回非0值。

strcpm(string1, string2);

4.4.1 strcmp()函数的返回值

当两个字符串内容不同时,strcmp()函数返回非0值,准确地说,返回的是两个值的ASCII码之差。

⚠️注意:C并没有规定strcmp()一定返回两个值的ASCII码之差,大部分的实现都只返回-1、0、1。

#include <stdio.h>
#include <string.h>

int main() {
    printf("A D: %d\n", strcmp("A", "D"));
    printf("A A: %d\n", strcmp("A", "A"));
    printf("D A: %d\n", strcmp("D", "A"));
    return 0;
}
//这里返回的就不是ASCII码之差,只是-1、0、1
//A D: -1
//A A: 0
//D A: 1

strcpm()函数会比较两个字符串的每一对字符,直到发现第一对不同的字符为止。

4.5 strncpm()

strncpm()函数与strcpm()类似,但可以添加第3个参数,表示只对比到第n个字符。

#include <stdio.h>
#include <string.h>

int main() {
    printf("123456 123123: %d\n", strncmp("123456", "123123", 3));
    printf("123456 123123: %d\n", strncmp("123456", "123123", 4));
    return 0;
}
//实际输出
//123456 123123: 0
//123456 123123: 1

从例子中可以看出,第一次strncmp()函数只比较到第3个字符,因此没有发现两个字符串的不同,而第二次strncmp()函数比较到第4个字符,因此发现两个字符串的不同。

4.6 strcpy()strncpy()函数

strcpy()函数用于拷贝字符串。该函数接收两个字符串作为参数,返回第一个字符串的地址。

#include <stdio.h>
#include <string.h>

#pragma warning(disable : 4996)

int main() {
    char str1[10] = "123";
    char str2[10] = "456789";
    printf("str1: %10s, str2: %10s\n", str1, str2);
    strcpy(str1, str2);
    printf("str1: %10s, str2: %10s\n", str1, str2);
    return 0;
}
//实际输出
//str1:        123, str2:     456789
//str1:     456789, str2:     456789

strcpy()的第一个参数不必指向字符串的开始,这样可以指定想拷贝的部分。

strcpy(&str1[1], str2);执行后,str1的内容为"1456789",从这里可以看到索引0的位置并没有拷贝的字符串覆盖。

strcat()的问题类似,strcpy()并没有检查第1个字符是否能容纳第2个字符,strncpy()更安全,它的第3个参数指定可拷贝的最大字符数。

#include <stdio.h>
#include <string.h>

int main() {
    char str1[10] = "123";
    char str2[10] = "456789";
    printf("str1: %10s, str2: %10s\n", str1, str2);
    strncpy(str1, str2, 4);
    printf("str1: %10s, str2: %10s\n", str1, str2);
    return 0;
}
//输出结果
//str1:        123, str2:     456789
//str1:       4567, str2:     456789

例子中的strncpy()的第3个参数为4,所以拷贝的结果只有str2的前4个。

同样的,strncpy()的第1个参数也不一定是字符串的开始。

4.7 sprintf()函数

sprintf()函数声明在stdio.h头文件中,这与其他字符串函数不同。sprintf()函数与printf()函数类似,它是把数据写入字符串,而不是打印在屏幕上。

sprintf()函数接收3个参数,第1个参数是字符串地址,其余两个参数与使用printf()时填写的参数一致。

#include <stdio.h>
#include <string.h>

int main() {
    char str[30] = "This is a number: %d\n";
    printf("%s", str);
	sprintf(str, "This is a number: %d\n", 42);
	printf("%s", str);
    return 0;
}
//数据类型
//This is a number: %d
//This is a number: 42

⚠️注意

  • 若想对写入的数据进行格式化,修改str中的转换说明是没用的,需要对sprintf()函数中的第2个参数进行修改。
  • sprintf()不会检查写入数据后,数组能否容纳字符串,当写入数据后字符串长度超过数组时,同样会溢出,导致程序异常或中止。

4.8 总结

函数名功能描述参数说明返回值主要特点与注意事项
strlen()计算字符串长度(不包含空字符\0)。const char* str字符串的字符数(size_t类型)。仅统计\0之前的字符。
strcat()将源字符串拼接到目标字符串末尾char* dest, const char* src目标字符串的起始地址(char*)。不安全。不检查目标数组空间,可能导致缓冲区溢出。目标字符串必须可修改且有足够空间。
strncat()将源字符串的前n个字符拼接到目标字符串末尾,并自动添加\0char* dest, const char* src, size_t n目标字符串的起始地址(char*)。相对安全。通过n限制最大添加字符数,但需确保目标数组有(原长度 + n + 1)的空间。
strcmp()比较两个字符串的内容是否完全相同。const char* str1, const char* str2相等则返回0
str1<str2返回负整数
str1>str2返回正整数
按字符的ASCII码逐对比较,直到遇到不同或\0。C标准不保证返回具体ASCII码差值,许多实现只返回-101
strncmp()比较两个字符串的前n个字符是否相同。const char* str1, const char* str2, size_t n比较结果,规则同strcmp()只比较到指定长度n或遇到\0。适用于比较字符串前缀。
strcpy()将源字符串拷贝到目标地址(覆盖目标)。char* dest, const char* src目标字符串的起始地址(char*)。不安全。不检查目标数组空间,可能导致溢出。目标地址不一定是数组开头(可指定偏移量)。
strncpy()将源字符串的前n个字符拷贝到目标地址。若未到n就遇到\0,剩余部分补\0char* dest, const char* src, size_t n目标字符串的起始地址(char*)。相对安全。通过n限制最大拷贝数,但不会自动在末尾添加\0(除非src长度小于n),使用时需谨慎。目标地址可指定偏移。
sprintf()格式化的数据写入字符串(而非屏幕)。char* str, const char* format, ...成功时返回写入的字符总数(int)。声明在stdio.h。功能强大,但不安全,不检查目标数组大小,极易导致溢出。建议使用更安全的sprintf_s()(如可用)。

4.9 其他字符串函数

函数名功能描述参数说明返回值主要特点与注意事项
strchr()在字符串中从左至右查找第一次出现的指定字符。const char* str, int c(要查找的字符)指向该字符的指针;如果未找到,则返回 NULL查找包含终止空字符 \0在内的字符。常用于检查字符串中是否存在某个字符或分割字符串。
strrchr()在字符串中从右至左(反向)查找最后一次出现的指定字符。const char* str, int c指向该字符最后出现位置的指针;如果未找到,返回 NULL常用于获取文件路径中的文件名(查找最后一个/\`)或文件扩展名(查找最后一个.`)。
strstr()在字符串中查找子字符串的第一次出现位置。const char* haystack(被查找字符串), const char* needle(要查找的子串)指向找到的子串首字符的指针;如果未找到,返回 NULL子串查找的核心函数。如果needle为空字符串,通常返回haystack的起始地址。
strspn()计算字符串起始部分连续包含指定字符集中字符的最大长度const char* str1, const char* str2(接受的字符集合)连续匹配字符的长度size_t类型)。功能是“扫描并计数匹配的字符”。常用于跳过前缀中的特定字符(如空白符)。
strcspn()计算字符串起始部分连续不包含指定字符集中任何字符的最大长度const char* str1, const char* str2(拒绝的字符集合)连续不匹配字符的长度size_t类型)。功能是“扫描并计数不匹配的字符”。是strspn()的互补函数,常用于查找第一个分隔符出现的位置。
strpbrk()在字符串中查找任何一个属于指定字符集的字符的第一次出现位置。const char* str1, const char* str2(要搜索的字符集合)指向找到的字符的指针;如果未找到,返回 NULL功能是“查找来自集合的任何一个字符”。常用于查找第一个分隔符(如空格、逗号等)。
strtok()根据一组分隔符将字符串分割成一系列标记(tokens)。char* str(待分割字符串,首次调用传入), const char* delim(分隔符集合)指向下一个标记的指针;如果没有更多标记,返回 NULL1. 会修改原字符串,用\0替换分隔符。
2. 非线程安全,因为内部使用静态缓冲区。
3. 首次调用传入待分割字符串,后续调用需传入NULL
memcpy()从源内存地址拷贝指定字节数的数据到目标地址。void* dest, const void* src, size_t n(字节数)目标内存地址(dest)。不处理内存重叠。如果源和目标内存区域重叠,行为是未定义的。通常用于拷贝不重叠的数据块,效率高。
memmove()从源内存地址拷贝指定字节数的数据到目标地址。void* dest, const void* src, size_t n(字节数)目标内存地址(dest)。能正确处理内存重叠。即使srcdest区域重叠,也能保证拷贝结果正确。通常比memcpy稍慢,但更安全。
memset()将内存区域的每个字节设置为指定的值。void* ptr, int value, size_t n(字节数)内存区域起始地址(ptr)。常用于内存初始化(如数组清零 memset(arr, 0, sizeof(arr)))或为字符串数组填充特定字符。
memcmp()比较两个内存区域的前n个字节const void* ptr1, const void* ptr2, size_t n(字节数)strcmp():相等返回0ptr1<ptr2返回负整数ptr1>ptr2返回正整数字节逐位比较,可用于比较任意数据类型(结构体、数组等),不仅限于字符串。

5. 命令行参数

C编译器允许main()函数没有参数或有2个参数。当main()函数有两个参数是,第1个参数为命令行中的字符串数量,第2个参数是字符串数组。

#include <stdio.h>

int main(int argc, char *argv[]) {
	for (int i = 1; i < argc; i++) {
		printf("argv[%d]: %s\n", i, argv[i]);
	}
	return 0;
}
E:\Documents\_005projects\C练习项目\day10\x64\Debug>day10.exe arg1 arg2 arg3
argv[0]: day10.exe
argv[1]: arg1
argv[2]: arg2
argv[3]: arg3
C:\Users\admin>E:\Documents\_005projects\C练习项目\day10\x64\Debug\day10.exe arg1 arg2 arg3
argv[0]: E:\Documents\_005projects\C练习项目\day10\x64\Debug\day10.exe
argv[1]: arg1
argv[2]: arg2
argv[3]: arg3

⚠️注意:有些系统会把程序本身名称赋给argv[0],但有些系统不会。

6. 将数字字符转换为数字

使用定义在stdlib.h头文件中的atoi()函数可以将数字字符串转换为数字。

#include <stdio.h>
#include <stdlib.h>

int main() {
	printf("%d\n", atoi("12"));
	printf("%.2f\n", atoi("12.13"));
	printf("%d\n", atoi("12abc"));
	printf("%d\n", atoi("abc12"));
	printf("%d\n", atoi("Hello"));
	return 0;
}
//实际输出
//12
//0.00
//12
//0
//0

atoi()函数只能处理整数开头的字符串,对于不能处理的情况,C标准规定此种行为的结果是未定义的。