目录
4.1 前导程序(略)
4.2 字符串简介
C语言中没有专门用于储存字符串的变量类型,字符串都被储存在char类型的数组中,数组是由连续的存储单元组成,字符串的字符储存在相邻的存储单元中,每个单元存储一个字符。
1、字符串常见陷阱
1)\0
结束符可以不写,但是必须给它预留空间
#include<stdio.h>
#pragma warning (disable:4996)
int main()
{
char i[3] = "123";
printf("%s", i);
}
//输出结果:123烫烫虁虐乴
从输出看,很明显发生了溢出,这说明C风格的字符串必须遵守:字符串储存数要始终大于储存字符量至少1个字符(字节),否则会发生溢出,原因是因为字符串需要存储一个字节的\0
字符来表明字符串结束。
2)手动加入的\0
也会被识别并且作为结尾
#include<stdio.h>
#include<string.h>
#pragma warning (disable:4996)
int main()
{
char a[40] = "1\0ABC";
printf("%s,%c",a,a[2]);
}
//输出结果为1,A
虽然在准备字符串时,给定了"1\0ABC"
,但是由于加入了\0
字符串结束识别符,字符串的实际长度只有1,即只将1存储了进去。
但是后面存储的字符是依旧存在的,但是并不算做字符串范围内,这里使用了a[2]
强行越界读取,编译器报告了一个警告,但是依旧读取出了结果。
3)%s
在scanf中无法存储空格
#include<stdio.h>
#pragma warning (disable:4996)
int main()
{
char i[40];
scanf("%s",i);
printf("%s", i);
}
//输入:fasdaw a7221
//输出结果:fasdaw
在scanf()
读取到空格时字符串发生了截断,只把fasdaw读取了,后面的a7221并没有读取。这是因为scanf的读取规则是:遇到第一个空白(空格、制表符或者换行符)时不再读取输入。这是因为 scanf()
默认使用空白字符作为分隔符来区分不同的输入项。如果你想同时读取 fasdaw
和 a7221
,你可以使用 %[^\n]
格式说明符,它告诉 scanf()
读取直到遇到换行符为止的所有字符;
char str[100];
scanf("%[^\n]", str);
另外,为了安全起见,最好总是使用 scanf() 的宽度限定符来避免缓冲区溢出:
scanf("%99s", str); // 假设 str 的大小是 100
这样也有坏处或者说是限制:
-
截断输入:如果你的输入字符串长度超过了99个字符,那么
%99s
只会读取前99个字符,剩余的字符会被忽略。这可能会导致数据丢失,特别是当你需要处理长字符串时。 -
换行符问题:使用
%s
格式说明符时,scanf()
会在遇到空白字符(如空格、制表符或换行符)时停止读取。这意味着如果输入中包含空白字符,scanf()
将不会读取整个行。而使用%99s
并不会改变这一行为,它只是限制了最大读取字符数。 -
不处理特殊字符:
%s
和%99s
都不会处理特殊字符,如引号或逗号。如果你的输入数据中包含这些字符,它们可能会被错误地解释或导致读取失败。 -
不灵活:使用
%99s
意味着你每次都需要根据你的字符串数组的大小来调整格式字符串。如果你的程序需要处理不同大小的字符串,这可能会变得笨拙。 -
可能的混淆:对于不熟悉
scanf()
的程序员来说,%99s
可能会造成混淆,因为它看起来像是在指定一个特定的数字,而不是一个限制。 -
效率问题:在某些情况下,如果输入的字符串长度远小于99,那么使用
%99s
可能会稍微降低效率,因为scanf()
需要检查更多的字符来确定何时停止读取。
4.2.1 char类型数组和null字符
C语言没有专门存储字符串的变量类型,字符串存储在char类型数组中。
在数组末尾有一个空字符(null character) \0 标记字符串结束。
4.2.2 使用字符串
char name[40];
scanf("%s", name);
printf("Hello, %s\n", name);
tip(只有一个字符的字符串和字符不同): ‘x' 和"x“ 是不同的,单引号的是字符;双引号的是字符串,实际上是两个字符x和\0组成的。
‘x’ 是一个字符(char基本类型) “x” 是一个字符串 (本质上:char数组)
4.2.3 strlen()函数
strlen()与
sizeof()的区别
strlen()
是用于计算字符串实际长度的,它会以\0
为结束符来计算到底有几个实际字符存入了该字符串。(如果没有遇到 \0 导致该字符串没有结束符。字符串前后的存储空间并不是空的,只不过并不在字符串的存储范围内,他们通常是一些垃圾数据。此时strlen()
在超过字符串限制后会继续往后读取垃圾数据,直到它遇到\0
才会结束读取,所以出现了字符量远大于字符串容量的情况。)
sizeof()
是用来计算字符串容量的,无论存储了几个字符进去,他只会显示该字符串的最大容量。
4.3 常量和C预处理器
定义常量
#define NAME value
//注意 后面没;
例如:define PI 3.1415
这里涉及到了宏替换的定义规则
4.3.1 const限定符
const关键字 限定一个变量为只读,
const int MONTHS = 12;
PS:在C语言里,const类型限定符声明的是变量,而不是常量
const 和 #define的区别
#define在编译的预处理阶段起作用,只是进行字符串的替换,并没有类型检查等操作,在程序运行时,程序每使用一次#define定义的值就会开辟一块内存,容易浪费内存。程序中使用#define可以使程序简单明了,通俗易懂。并且代码的维护相对容易,在修改定义的值时只需修改定义处即可。
const常量有数据类型,在运行时会对其进行类型安全检查,const定义的只读变量在程序运行过程中只备份一次,比较节省空间。
4.3.2 明示常量
C头文件limits.h和float.h 分别提供了与整数类型和浮点数类型大小限制相关的信息。
4.4 printf()和scanf()
输出 和 输入函数,简称I/O函数。
宽度与精度
1.基础说明
printf与scanf在输入输出时可以在占位符上添加修饰符,其中使用最多的就是宽度与精度。
%d加入宽度后:%数字d
%d加入精度后:%.数字d,请注意scanf是不可以描述精度的,scanf中只允许使用宽度。
printf中一个占位符可以同时存在宽度与精度:%数字.数字d
scanf()输入原理
scanf的输入和赋值是两部分组成,输入时通过一个输入池进行管理。运行到scanf位置时,输入池打开时,可以向里面输入任何字符都会被保存在输入池中(包括enter键的换行符),识别到换行符时关闭输入池,scanf按顺序从输入池中取出相应的字符进行赋值。
如果在第一次打开输入池时,输入的值满足了后续赋值所需要的数据个数,同样运行到该scanf位置上时,会直接从输入池中取出所需的值进行赋值,而不再开放输入池。
但是有char的情况,这种却可以运行的。scanf并没有将池子里多出的[enter]赋值给b,而是在遇到第二个scanf时重新开启了输入池,在输入结束后,将检测到的数字赋值给b。
原因有两个:
1.scanf自身有一定的输入池检测能力,它能清楚被赋值的数到底需要什么类型的值,如果类型不匹配,则会选择一种方式输出:输出乱码或者输出换行符之后的东西。
2.接收值有char变量时,降低了scanf对于字符类型的灵敏度,这时候换行符将不再会被检测为无关数据,而是会以可能存在的输入值保存在输入池中,所以在最初的例子中,scanf实际在输入池中识别到的字符是3和换行符两个,满足了后续输入,所以第二个输入池不再开放,将换行符赋给了c。
搬用的【速过C primer plus】第四章_c primer plus 第四章溢出-CSDN博客
printf()函数
在使用printf()函数打印数据时指令要和待打印的类型相匹配,例如在打印整形数据时,不能使用字符的转换字符,C语言有标准的转换说明要求,如下表所示:
scanf()函数
scanf()和printf()类似,也是用格式字符串和参数列表。两个函数主要区别在参数列表,printf()函数使用变量、常量和表达式,而scanf()函数使用只指向变量的指针。这里有两条规则:
如果scanf()读取基本变量类型的值,在变量名前加&;
如果scanf()把字符串读入字符数组中,不需要使用&。
对于scanf()函数中的转换说明具体含义如下表所示:
4.4.1 printf()函数
printf()的格式是 printf(格式字符串,待打印项1,待打印项2,...);
例子:printf("My age is %d, my weight is %g kg", 18,66.5);
Tip:printf()函数也有返回值,它返回打印的字符数,如果输出错误,则返回负数。
输出长字符串的3种方式