token
token(令牌):程序的基本元素,一个程序由各种令牌组成,它是程序构成源代码的最小的、不可分割的单元。它可以是关键字、标识符、常量、字符串文字、运算符或者特殊符号。在语法分析过程中,编译器会将源代码分解成各种token,并进行进一步的处理。以下是C语言中的一些常见token类型:
- 关键字(Keywords):如
if、else、for、while等,用于表示语言的关键结构和功能。 - 标识符(Identifiers):用来表示变量、函数、类型等用户定义的名称,例如变量名、函数名等。
- 常量(Constants):包括整数常量、浮点数常量、字符常量和字符串常量等。
- 字符串文字(String literals):由双引号括起来的字符序列,用于表示字符串值。
- 运算符(Operators):如
+、-、*、/等,用于进行各种数学和逻辑运算。 - 特殊符号(Special symbols):如括号
()、方括号[]、花括号{}、逗号,等,用于控制程序结构和分隔符号。
编译器在词法分析阶段会将源代码分解成这些token,并在接下来的语法分析阶段使用它们来构建语法树以进行进一步的编译处理。
如下面语句由下面几个令牌组成:
printf("Hello World");- printf 2.( 3.“Hello World” 4.) 5.;
标识符 声明 定义
inta;//声明(引用式声明)并定义(定义式声明)一个名为a的整形变量int是数据类型关键字,用于声明并定义标识符a(注意:声明和定义是两个不同的概念,这点在多文件程序中会更加充分体现出来。一般在单文件程序中同时完成了声明和定义两个步骤)。
基类型:即基本数据类型,有short、int、long、long long、char、float、double、long double。可加signed、unsigned等前缀修饰符。
指针数据类型:变量指针(声明方法:基类型*) 常量指针 指针常量 数组指针 函数指针 结构体指针…
复合数据类型:数组、enum、struct、union等。
a是一个标识符,标识符一般是变量名、函数名或其他实体的名称。在这种情况下,标识符可以用来指定特定对象的内容。标识符遵循变量的命名规则。在此处,标识符是C程序指定硬件内存中的对象的方式。指定对象的表达式被称为左值。C语言规定编译器至少需要识别局部标识符的前63个字符和外部标识符的前31个字符。
标识符的命名应做到见名知义,同时在软件工程中应采用“驼峰命名法”。
声明可分为定义式声明和引用式声明。一般地,采用声明代指引用式声明,定义代指定义式声明。
通过声明(引用式声明),编译器在符号表(类似于哈希表/C++中的map/Python中的字典数据类型,添加一对一的映射关系)中创建一个表项,用于记录标识符a是一个整形变量;通过定义(定义式声明),编译器根据变量所属作用域不同,在对应内存区域内申请一块关键字大小的内存(全局作用域:全局/静态存储区;块作用域:栈区),C语言将这一块内存称为对象(C语言是面向过程的语言,仅此处提到了对象的概念),并将标识符与申请的内存通过符号表进行绑定,之后通过标识符即可访问该块内存。对象可以存储一个值或多个值。
变量:标识符所代表的量是可变的,最简单的变量是标量变量,如整形变量、浮点型变量、字符型变量等,这些存储单个值的变量叫标量变量。可存储多个值的变量叫矢量变量,如数组、枚举(enum)、结构体(struct)、联合体(union)等。
与之相对,标识符所代表的量是恒常不变的,就是常量。
类型大小
查看数据类型或变量所占的内存大小:sizeof运算符。
当sizeof后为数据类型关键字时必须加圆括号,当为标识符时则圆括号可选。为确保跨平台的兼容性,应对数据类型采取sizeof,而并非对大小硬编码(如int可能和short相同,也有可能和long相同)。
可以在<limits.h>和<float.h>中查看整型和浮点型的大小限制相关符号常量。
格式化转换符
格式化转换符出现在格式化字符串中,由以下几部分构成:
格式化转换说明符(即%,用于在格式化字符串中区分字面字符和格式化转换符)+修饰符+数据类型转换符
注意I/O函数根据从左到右的顺序处理,如果出现用错格式化转换符会影响字节的读取,从而影响之后的变量I/O。
例如,不能将float/double以%d形式输出,这是未定义行为。
printf默认浮点数的精度为6位。
数字进制的基本概念
- 数码:数据中的每一位数字
- 位码:数码在此数中的位置,从0开始,右侧为低位,左侧为高位
- 基数:每一位数码最多能由多少个数表示(即为进制)。
- 位权:基数的位码次幂,任何数都可被位权展开
不同进制数的区分方法:
- 将数据用括号括起来,并在右下角用进制加以区分。
- 对于特殊的进制,可在数据后用对应进制英文缩写。
D:十进制 B:二进制 O:八进制 H:十六进制
整形变量
| 类型 | 数据范围 | 存储大小 | 备注 | 格式化转换符 |
|---|---|---|---|---|
| char(字符型) | [0,28−1)[0,2^{8-1})[0,28−1) | 1B | 缺省时具体是signed char或unsigned char取决于编译器 | %c (%d打印ASCII码) |
| signed char (有符号字符型) | (−28−1,28−1)(-2^{8-1},2^{8-1})(−28−1,28−1) | 1B | ||
| unsigned char (无符号字符型) | [0,28)[0,2^{8})[0,28) | 1B | ||
| short (short int,有符号短整形) | (−216−1,216−1)(-2^{16-1},2^{16-1})(−216−1,216−1) | 2B | C标准中未规定具体范围,仅规定占用存储空间**short**<=int即可 具体实现取决于编译器 | %hd |
| unsigned short (unsigned short,无符号短整型) | [0,216)[0,2^{16})[0,216) | 2B | %hu | |
| int(有符号整形) | (−232−1,232−1)(-2^{32-1},2^{32-1})(−232−1,232−1) | 4B | C标准中未规定具体范围,仅规定占用存储空间short<=int<=long即可 一般而言int要么与short相同,要么与long相同。具体实现取决于编译器 | %d |
| unsigned int (无符号整形) | [0,232)[0,2^{32})[0,232) | 4B | %u | |
| long (long int,有符号长整形) | (−232−1,232−1)(-2^{32-1},2^{32-1})(−232−1,232−1) | 4B | C标准中未规定具体范围,仅规定占用存储空间int<=long<=long long即可 具体实现取决于编译器 | %ld |
| unsigned long (unsigned long int,无符号长整形) | [0,232)[0,2^{32})[0,232) | 4B | %lu | |
| long long (long long int,有符号长长整形) | (−264−1,264−1)(-2^{64-1},2^{64-1})(−264−1,264−1) | 8B | C标准中未规定具体范围,仅规定占用存储空间long<=**long long**即可 具体实现取决于编译器 | %lld |
| unsigned long long (unsigned long long int,无符号长长整形) | [0,264)[0,2^{64})[0,264) | 8B | %llu |
- char类型在C语言中属于整形范围,采用ASCII码存储。
- 整形在计算机中存储采用二进制的方式。
- 数据范围溢出是未定义行为,一般会从最小范围重新开始一轮循环。
- 通过在整形字面常量后增加后缀可将一个较小数据类型当做一个较大数据类型的数据来处理,叫宽整形字面常量。如
U、L、LU、LL、LLU。 - 枚举类型的变量也当做整形处理。
浮点型(IEEE754)
| 类型 | 存储大小 | IEEE754存储 | 存储范围 | 指数偏移TTT | 精度 | 格式化转换符 |
|---|---|---|---|---|---|---|
| float(单精度浮点型,短浮点型) | 4B | 1符号位+8指数位+23尾数位(含1隐藏位) | −2128-2^{128}−2128-21282^{128}2128(−1038-10^{38}−1038-103810^{38}1038) | +127 | C标准并未规定精度范围,仅要求float<=double即可 一般认为是6~7位十进制有效数字 | %f |
| double(双精度浮点型,长浮点型) | 8B | 1符号位+11指数位+52尾数位 | −21024-2^{1024}−21024-210242^{1024}21024(−10308-10^{308}−10308-1030810^{308}10308) | +1023 | C标准并未规定精度范围,仅要求float<=double<=long double即可 一般认为是15~16位十进制有效数字 | %lf |
| long double(80bit) | 16B | 1符号位+15指数位+64尾数位 | 取决于实现 | +16383 | C标准并未规定精度范围,仅要求double<=long double即可 一般认为是19~20位十进制有效数字 | %Lf |
浮点数:指的是小数点可进行偏移浮动。与之相对的为定点数。
根据小数进制转换原理,仅当形如整数2n\dfrac{整数}{2^n}2n整数的十进制小数才能被转换为有限位的二进制小数。
浮点型在计算机中存储遵循IEEE754标准:X=S×M×rE+TX=S\times M\times r^{E+T}X=S×M×rE+T,其中rrr称为进制,由符号位(S)、指数位(阶码,E)、尾数位(M)组成。
符号位:表示该小数正负性;
尾数位:即小数位,其位数代表能可靠存储†^\dagger†的有效数字(精度)。计算机系统将尾数进行归一化处理,保证尾数最高位为111。由于多数十进制小数转换为二进制时即为无限小数,但计算机无法存储无限位。IEEE754规定,超出尾数位的部分,需根据进行修约,进行舍入存入(银行家舍入)。因此存入计算机时,超出精度的部分就已不精确。因此不能用浮点数代替整数存入大范围数。
†\dagger†可靠存储:精度不是有多少位是精确的,而是浮点数x^\hat xx^与真实值xxx的偏差永远不会超过某个极小值ϵ\epsilonϵ,即∣x−x^∣∣x∣≤ϵ\frac{|x-\hat x|}{|x|}\le \epsilon∣x∣∣x−x^∣≤ϵ。
类型 尾数位数(含隐藏位) ϵ\epsilonϵ 十进制精度 float 24 bit ≈2−242^{-24}2−24 ≈ 7 位 double 53 bit ≈2−532^{-53}2−53 ≈ 15~16 位 long double (80-bit) 64 bit ≈2−642^{-64}2−64 ≈ 19 位 long double (quad 128-bit) 113 bit ≈2−1132^{-113}2−113 ≈ 34 位 当尾数位不够时,通常采用 GRS 位法进行舍入。设保留kkk位尾数,则 G 代表第k+1k+1k+1位,R 代表第k+2k+2k+2位,S 代表第k+3k+3k+3位后的所有位的逻辑或 (有1则1)。
银行家舍入规则:
- 若 G=0,舍弃
- 若 G=1 且 (R=1 或 S=1),进位
- 若 G=1 且 R=0 且 S=0:
- 若第kkk位 =1,进位
- 若第kkk位 =0,舍弃
也就意味着,计算机将十进制浮点数以二进制存入时,就已经产生了偏差。如经典的
print(0.1+0.2==0.3)为 False 的问题。而使用 printf 打印浮点数(默认精度为6)时,需先将二进制浮点数转换为十进制浮点数(常见的算法包括Dragon4算法),这就又产生了一次偏差,导致十进制浮点数偏大或偏小;然后再根据十进制修约规则:四舍六入五凑偶,按照规定精度打印出小数。
四舍六入五凑偶:设要保留kkk位精度
- 若第k+1k+1k+1位≤4\le4≤4,舍弃
- 若第k+1k+1k+1位≥6\ge6≥6,进位
- 若第k+1k+1k+1位=5=5=5,则看第kkk位——奇进偶舍
- 若第kkk位为偶数,舍弃
- 若第kkk位为奇数,进位
四舍六入五凑偶的提出,本质上是要解决传统四舍五入带来的偏见问题。在四舍五入中,当<4<4<4则舍,当≥5\ge 5≥5则进,设存在0.0,0.1,…,0.8,0.90.0,0.1,\dots,0.8,0.90.0,0.1,…,0.8,0.9,则根据四舍五入,舍去的误差值为1.01.01.0,进位的误差值为1.51.51.5。因此对于大量数据,四舍五入会使结果偏高。四舍六入五凑偶,本质上是在0.50.50.5时,等概率舍弃与进位。
指数位:必须为一个无符号型整数。IEEE754 规定,不以补码的形式存入指数位,因此需设置一个偏移量TTT,将指数位偏移表示。阶码=真值+偏移值。
数据范围溢出:上溢:给变量赋一个无穷大的特定值,并在printf时输出INF(infinity)。
下溢:数据太小以至于无法被计算机存储,NaN(Not A Number)。下溢一般指代的是浮点数下溢。(整型的下溢一般为将负数赋给无符号数)浮点数四则运算问题:
- 大数吃小数:精度丢失。浮点数做加减法必须先进行指数对齐,若较大的浮点数加上一个极小的浮点数,会造成小浮点数直接超出精度。
- 下溢:两个极接近的浮点数相减,会造成有效位数骤降,甚至发生下溢。
- 放大精度误差:浮点数做加减运算时受精度限制,误差为<10−精度<10^{-精度}<10−精度。一旦做乘法运算,则该误差将被放大,进而影响printf的四舍六入五凑偶。此时应对被放大误差的浮点数加上一个大于当前误差的eps。
在 C 语言中,字面型浮点数常量默认被视为
double型。科学计数法在计算机中的表示:指数计数法(e计数法)。e即为
*10^。如2.4e−3=2.4×10−32.4e-3=2.4\times 10^{-3}2.4e−3=2.4×10−3等。通过e指数法表示默认为双精度浮点型字面常量,使用后缀
f或L可覆盖为float或Long double型,加L后缀叫宽浮点型字面常量。使用格式化转换符%e转换成e指数表示法
布尔型(<stdbool.h>)
false(0)——假
true(非0)——真
逻辑表达式返回值为布尔型
常量
命名常量:通过const限定符进行修饰的标识符,用于限定其为只读。如
const int MAX=10;中原本是整形变量的MAX,经过const限定符修饰后就变成了命名常量。const修饰符能限定的东西有很多,比如变量、数组、指针、形参、函数返回值等。字面常量(明示常量)(编译时常量):在程序中直接使用的常量值,在编译时就已确定,如上例中的10即为字面常量
字面常量可分类为:整形常量 浮点型常量 字符型常量 字符串型常量 布尔型常量
字符型常量分别以字符型常量说明符(‘,即单引号)作为开始和结束,
字符串型常量以字符串型常量说明符(“,即双引号)作为开始和结束,编译器自动在字符串常量末尾处加上\0(字符数组用此种字符串型常量的方式进行初始化才能成为字符串)。字符串型常量属于静态存储类别,若在函数中使用字符串型常量(在整个程序的生命周期内都一直存在),即使函数被调用了多次,该字符串也只会被存储一次。字符串型常量用双引号括起来的内容被视作指向该字符串存储位置的指针,即可用%p打印其地址,也可用解引用符。宽字符/字符串型常量:在C语言中用来表示Unicode字符的常量,而不仅仅是ASCII字符。在C语言中,是由L前缀和一个字符、字符串或转义序列组成的,如
L'x'或L"Hello"。在C++中,也可以使用u、U或者u8前缀。说明符只是对编译器的提示作用,而并非常量本身的一部分。
符号常量:明示常量通过宏定义成为符号常量 例如
#define PI 3.14中,PI是符号常量,3.14是明示常量。宏定义是预处理器在编译时进行文本替换的。枚举常量
类型限定符
所有类型限定符都具有幂等性,即在定义或声明中多次使用同一个限定符,只保留一个,多余的将会被忽略。
- const(只读类型限定符):具有恒常性,应用于命名常量 指针常量 常量指针 常量数组 常量结构体 函数形参 函数返回值…
- restrict(限制类型限定符):只能用于指针,表明该指针是访问数据对象唯一且初始的方式
- volatile(易变类型限定符):用于硬件开发和多线程并发程序,可连同const使用
- _Atomic(原子类型限定符):用于多线程并发程序
类型限定符和存储类别说明符static在函数形参的初始方括号中,如:
voidfun(int*consta);//也可写作voidfun(inta[const]);static:1.表示静态存储变量的作用域和链接属性2.告知编译器如何使用形参(此处为此作用)。
形参表中不能使用static的静态存储类别说明符的作用,但可以使用告知编译器如何使用形参的作用。如void fun(int a[static 10]);表明,函数的形参为指向数组的基址,且该数组至少有10个元素。
复合字面量
可用于创建数组和结构内容的复合字面量
一维数组
(int[2]){10,20};符合字面量数组也可以省略数组大小,如:
(int[]){10,20};注:复合字面量是匿名的,因此必须在创建后立即使用,如创建后通过赋值表达式给指针。
int *p=(int []){10,20}; cout<<*p<<" "<<p[1]<<endl;输出结果:
10 20多维数组
以二维数组为例:
int(*pt2)[4]=(int[2][4]){{1,2,3,4},{5,6,7,8}};
基本格式化I/O函数(printf和scanf)
printf("格式化转换字符串",可变参数列表);scanf("格式化转换字符串",可变参数地址列表);注意,可变参数列表指的是参数的数量可变,而不是参数的内容可变。
printf和scanf中传入的可变参数表可以是变量也可以是字面常量。如:
intx=100;printf("%d",x);printf("%d",100);上述两种方案是等效的
Q&A:为什么scanf的可变参数列表是输入项地址表?
scanf(“格式化字符串”,可变参数(输入项地址表));
scanf将格式化字符串内容进行转换。有格式化转换符的将字符型(或字符串型)转换为对应格式。
由于C语言函数间传递默认是按值进行传递的,传递的只是数值的拷贝(自动存储类型),在函数结束之后该拷贝值将会被自动释放,无法起到修改的目的。若想让函数修改某个变量,必须采用按址传递的方式(传递的是该变量的地址),通过传递地址,scanf可以直接在内存中找到对应变量的位置,并将输入的值存储到那个位置,才能起到真正修改的目的。因此scanf函数在设计时考虑了这一点,因此scanf的可变参数为输入项地址表。若忘记加上取地址符(间接寻址符)(&),由于scanf不会检查传入可变参数的内容是否是合法,仍然将传入的内容当做正确的地址处理,由于与变量先前的值相等值的地址是不可预测的(可能是其他程序申请的内存,或为操作系统核心部分内存),会造成地址的非法访问(访问了未经申请的内存),这样操作会触发操作系统内存保护机制,被操作系统认为是具有危害性的行为,操作系统会强行终止程序以确保系统正常运行(Runtime Error)。
scanf返回值:若读入正确 则返回值为读入项的个数
若读入类型与格式化转换符不匹配且无法进行隐式转换(自动升级),则该项会返回0,并终止该scanf之后继续读入。返回值也是出错之前的项的个数。无法读取的输入留在输入缓冲区中,等待下个输入函数尝试进行读入。因此下次读入时必须手动清除缓冲区中该项内容(getchar()或fflush(stdin))。
若读取失败(到文件末尾或设备损毁),则返回-1(EOFEOFEOF)
在 C 语言中,%s格式说明符在scanf函数中有一种特殊的行为,它会将输入的字符序列存储到指定的字符数组中。而其他的格式说明符(如%d、%f等)则要求提供变量的地址,因为它们需要修改变量的值。
这个差异源于 C 语言中变量的类型和如何在内存中存储的方式。在 C 语言中,数组名本身就是数组首元素的地址。所以当你传递一个字符数组的名字给%s格式说明符时,实际上是传递了数组首元素的地址,这是符合%s格式说明符的要求的。
而对于其他的数据类型,比如整数、浮点数等,它们是直接存储在内存中的值,而不是像字符串一样存储在连续的内存块中。因此,当你需要从输入中读取这些数据并修改相应的变量时,你必须传递变量的地址,以便scanf函数可以将读取到的值存储到正确的内存位置中。
举例来说:
intnum;scanf("%d",&num);// 需要取地址,因为需要修改 num 的值在这个例子中,%d格式说明符期望一个整数变量的地址作为参数,因此需要使用&取地址符来获取变量num的地址,以便scanf函数能够修改num的值。
综上所述,差异主要是因为%s格式说明符与其他格式说明符在处理方式上的不同,以及变量类型在内存中的存储方式所导致的。