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个字符拼接到目标字符串末尾,并自动添加\0。 | char* 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码差值,许多实现只返回-1,0,1。 |
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,剩余部分补\0。 | char* 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(分隔符集合) | 指向下一个标记的指针;如果没有更多标记,返回 NULL。 | 1. 会修改原字符串,用\0替换分隔符。2. 非线程安全,因为内部使用静态缓冲区。 3. 首次调用传入待分割字符串,后续调用需传入 NULL。 |
memcpy() | 从源内存地址拷贝指定字节数的数据到目标地址。 | void* dest, const void* src, size_t n(字节数) | 目标内存地址(dest)。 | 不处理内存重叠。如果源和目标内存区域重叠,行为是未定义的。通常用于拷贝不重叠的数据块,效率高。 |
memmove() | 从源内存地址拷贝指定字节数的数据到目标地址。 | void* dest, const void* src, size_t n(字节数) | 目标内存地址(dest)。 | 能正确处理内存重叠。即使src和dest区域重叠,也能保证拷贝结果正确。通常比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():相等返回0;ptr1<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标准规定此种行为的结果是未定义的。