- 1、顺序程序设计举例
- include
- 2、数据的表现形式及其运算
- 3、常量和变量
- 4、整型与补码
- 5、字符型数据
- include
- 8、表达式与语句
- include
- include
- include
- 附录 integer overflow
1、顺序程序设计举例
例 3.1 温度转换
今有人使用温度计测量出华氏温度为 64°F,求转化为摄氏温度 17.8°C。
- 解题思路:算法就是温度转换关系:
- 流程图:
int main() { float f, c; printf(“Please input Fahrenheit: “); scanf(“%f”, &f); c = 5 * (f - 32) / 9; printf(“Celsius:%f\n”, c); return 0; }
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/temperatureConvert.c -o ./bin/temperatureConvert<br />b12@PC:~/chapter3$ ./bin/temperatureConvert<br />Please input Fahrenheit: 64<br />Celsius:17.777779
:::
<a name="VxZH2"></a>
## 例 3.1 计算存款利息
有 1000 元,想存一年。有 3 种方式可选:(1)活期,年利率为 `r1` ;(2)一年期定期,年利率为 `r2` ;存两次半年定期,年利率为 `r3` 。分别计算出每中存款方式最后所得到的本息和。
- 解题思路:本息和计算公式就是本金加你投入本金在规定时间内获得的收益。若存款为,利率为,规定时间后,本息和为 。
- 存一年活期本息和:
- 存一年定期本息和:
- 存半年定期,再存半年本息和:。(注意这里是半年,银行都是以年作为时间期限,因此除 2)
- 流程图:

- C语言
```c
#include <stdio.h>
int main() {
float p0, r1, r2, r3, p1, p2, p3;
printf("Please input your deposits: ");
scanf("%f", &p0);
printf("Please input the interest rate of r1, r2, r3:");
scanf("%f %f %f", &r1, &r2, &r3);
p1 = p0 * (1 + r1);
p2 = p0 * (1 + r2);
p3 = p0 * (1 + r3 / 2) * (1 + r3 / 2);
printf("p1=%f,p2=%.2f,p3=%f\n", p1, p2, p3);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/interests.c -o ./bin/interest
b12@PC:~/chapter3$ ./bin/interest
Please input your deposits: 1000
Please input the interest rate of r1, r2, r3: 0.0036 0.0225 0.0198
p1=1003.599976,p2=1022.50,p3=1019.897949
:::
综上,所有的浮点数定义最后跟书本的结果不太一致??为何出现这样的情况?假定这是银行算钱的,失之毫厘,差之千里啊。这是因为浮点数浮动问题,具体可见 IEEE 754浮点数标准详解。但是日常生活中,我们大多精确到分就可以了,比如上面对 p2=%.2f
格式化输出保留两位小数,即角和分就足够了。
2、数据的表现形式及其运算
综上对于每个变量我们都要定义它的类型,上述定义的 f
和 c
是单精度(float)型。的涉及小数的都要把它定义为浮点型数据。并且也看到计算机算的不准,有误差。
那么为什么C语言要求在定义所有变量的时候都需要指定变量的类型呢?要定义这些数据类型呢??难道不可以直接像 python 语言一样,写个「标识符」不就完事了?
(1)数学中,数值是不分类型的(小数,整数,复数不算??),数值的运算时绝对准确的,比如循环小数 。数小是一门研究抽象问题的学科,数和数的运算都是抽象的。
(2)计算机中,数据是存放在存储单元中的,它是具体存在的。而且,存储单元是由有限的字节构成,每一个存储单元中存放的数据的范围是有限的,不可能存放“无穷大”的数,也不能存放循环小数。(Python 也只能做到理论无穷大)。用计算机进行的计算不是抽象的理论值的计算,而是用工程的方法实现的计算,在很多情况下只能得到近似的结果(尤其是在浮点数表示有限的情况下更为明显,有兴趣可以看下 计算机是如何运算的)。
「数据类型」:对数据分配存储单元的安排,包括存储单元的长度(占多少字节)以及数据的存储形式。不同的类型分配不同的长度和存储形式。(P42,真题考过)
- 单个对象构成一个类型有如下两种:
- 算术类型:基本类型(浮点和整型)和枚举类型变量都是数值。
- 纯量类型:算术类型和指针类型总称,因为其变量的值是以数字来表示的(我觉得这个与上面有点相反,其目的就是表示变量内值是数字都可进行运算,是为何指针可以
**p++**
的原因)
- 多个对象构成组合类型:
- 组合类型(aggregate type):数组类型(要求所有子元素类型一致)和结构体类型(成员类型随意,大小按最长字节进行对齐)。
- 因为组合类型不是单独个体,其内的“子元素”可能是数值类型,必须取最内的成员/元素才能进行算术运算。如果存在嵌套,如二维数组,必须取到最内元素才可以进行运算。
- 共用体类型:不是组合类型,因为在同一时间内只有一个成员具有值。(它是孤儿,多面鬼)
- 函数类型:描述一个函数的接口,包括函数返回值的数据类型和参数类型的类型。 :::tips C语言甩锅时间到:
- 不同类型的数据在内存中占用的存储单元长度是不同的,例如 gcc 对
char
类型分配 1 个字节,为int
型分配 4 个字节,存储不同类型数据的方法也是不同的。(什么是字长?P43) - C 标准没有具体规定各种类型数据所占用存储单元的长度,这是由编译系统自行决定的。C 标准只要求
sizeof
是测量类型或变量长度的运算符(注意:千万不要与函数搞混淆,它就是运算符,只是与函数调用形式相似,还有很多坑,真题考过)- 由于不同编译系统决定数据储存字节长度不同(书中举例:A系统把
int
分配 4 字节,而B系统分配 2 字节,当整数 5000 在A编译系统完美运行,在B编译系统下出现离奇结果?),因此代码从你的机器跑过其他机器上就有可能「溢出」,具体可见下面视频。 ::: 使用sizeof
运算符查看常见类型所占用的字节大小程序如下:
编译运行: :::success b12@PC:~/chapter3$ gcc -Wall ./src/datatype.c -o ./bin/datatype#include <stdio.h>
#define N 2
int main() {
// integer
printf("char:%ld\n", sizeof(char));
printf("short:%ld\n", sizeof(short));
printf("int:%ld\n", sizeof(int));
printf("long int:%ld\n", sizeof(long int));
printf("long long int:%ld\n", sizeof(long long int));
// float,double
printf("float:%ld\n", sizeof(float));
printf("double:%ld\n", sizeof(double));
// array
printf("array with %d elements:%ld\n", N, sizeof(int [N])); // notice
// pointer
printf("pointer:%ld\n", sizeof(int *)); // notice
return 0;
}
b12@PC:~/chapter3$ ./bin/datatype
char:1
short:2
int:4
long int:8
long long int:8
float:4
double:8
array with 2 elements:8
pointer:8
struct type:8
union type:4 ::: 这里重点说明下派生类型,比较特殊:
- 指针类型:一般都被某些视频带着指针类型差不多都是 4 个字节,可是 gcc 9.3.0 分配是 8 个字节。
- 数组类型:是由基本类型组合而来的,因此必须指定有多少个才可以,比如上述假定
N=2
即数组中含有两个整型,那么由sizeof(int [N])
和sizeof(int)
可知该整型数组就是多个int
的容器。 - 结构体:结构体就是自定义不同类型的混合在一起。但是问题就是上面,理论上结构体是其内成员的大小值的累加和,即此处该为
4 + 1
才对!为何变为 8 ?由 8 推导 肯定是4 + 4
,那么问题就是出在char
如何变为 4 而不是 1 呢?这有关字节对齐概念,最长对齐原则。(具体将在结构体内说明) - 联合体:同结构体不同,使用是覆盖技术,即其类型大小是由字节最长的决定。(具体将在联合体内说明)
另外在上述结构体和联合体的组合类型中,尚未对其内元素/成员进行字节运算,因其需要定义变量,才可以访问其内的成员。具体将会在自定义数据类型章节说明。同时对函数类型进行字节运算意义不大。
3、常量和变量
:::tips 个人认为:书本编排应该把本章放在第二章,先了解什么是数据类型和变量的定义才进行算法讲解。因为使用计算机解决问题,肯定需要知道计算机一些常识,掌握C语言基本语法后能运行成功才能谈算法。即先解决如何让计算机能帮我们弄出答案先,再去想其他的办法,甚至更好的算法。 :::
3.1 常量
常量:在程序运行过程中,其值不会被改变的量称为常量。
字面常量:是没有名字的不变量。从字面形式上即可识别的常量称为“字面常量”或“直接常量”。(也就是你一眼可以望穿的,不是代号 007 那个意思)
- 整型常量:
- 十进制:如
1000
,12345
等。默认为int
型,如果想long
就加上后缀L/l
,如1000L
,12345l
,1000LL
,而12345Ll
是不可以的! - 八进制/十六进制:无英文符号是没有二进制,比如
101
不是二进制的 5 ,而是八进制的 65 (二进制 111 000 111);含有0x
或者0X
的是十六进制整型常量。
- 十进制:如
- 实型常量:
- 十进制小数形式:由数字和小数点组成。如
123.345
,0.0
等。默认为double
类型,如果想要为float
再在数字结尾加上f/F
,如123.345F
,0.0f
- 指数形式:如
12.34e3
(代表)。由于在计算机输入输出时,无法表示上角或下角,故规定以字母
e
或E
代表以 10 为底的指数。但是 e 或 E 之前必须要有数字,不能单独存在。且 e 或 E 后面必须跟着整数(12.45e2.3是非法的)。(真题考过,注意其不要求科学计数法中的)
- 十进制小数形式:由数字和小数点组成。如
字符常量:
- 普通字符:用单撇号包括起来的一个字符,如 ‘a’,’A’等,字符常量储存在计算机储存单元中时,并不是储存字符(如a 和 A)本身,而是以其代码(一般采用 ASCII 代码)存储的。如 ‘a’ 的 ASCII 码是 97,因此在计算机内部就是 97 储存的。
- 转义字符:C 语言还允许用一种特殊形式的字符常量,就是以字符
\
开头的字符序列。 | \‘ | 单引号(‘) | 具有此八进制码的字符 | | —- | —- | —- | | \“ | 双引号(“) | 输出此字符 | | \? | 问号(?) | 输出此字符 | | \\ | 反斜杠(\) | 输出此字符 | | \a | 警告(alert) | 产生声音或视觉信号 | | \b | 退格(backspace) | 将当前位置后退一个字符 | | \f | 换页(form feed) | 将当前位置移到下一页开头 | | \n | 换行 | 将当前位置移到下一行的开头 | | \r | 回车(carriage return) | 将当前位置移动到本行开头 | | \t | 水平制表符 | 将当前位置移动到下一个tab位置 | | \v | 垂直制表符 | 将当前位置移到下一个垂直制表对齐点 | | \o,\oo,\ooo(o代表8进制数字) | 与该8进制码对应的ASCII字符 | 与该8进制码对应的字符 | | \xhh… | 与该16进制码对应的ASCII字符 | 与该16进制码对应的字符 |
字符串常量:用双引号把若干个字符括起来,字符串常量是双引号中的全部字符(但不包括双引号本身)。单引号只能包含一个字符,双引号内可以包含一个或多个字符。
- 符号常量:用
#define
指令,指定用一个符号名称代表一个常量。如#define PI 3.14
。在对程序进行编译前,预处理器先对PI
进行处理,把所有PI
全部置换为3.1416
。这种用一个符号名代表一个常量的,称为符号常量。在预编译后,符号常量已全部变成字面常量。优点如下:- 含义清楚:见名知义。应该尽量使用“见名知意”的变量名和符号常量。
- 在需要修改程序中多处使用到的同一个常量时,能做到“一改全改”。
注意:要区分符号常量和变量,不要把符号常量误认为变量。符号常量不占内存,只是一个临时符号,在预编译后这个符号就不存在了,故不能对符号常量进行重新赋值(记住文本替换就完事了,同时因此也导致优先级问题)。为与变量名相区别,习惯上符号常量用大写表示。
3.2 标识符
标识符(identifier):在计算机高级语言中,用来对变量、符号常量名、函数、数组、类型等命名的有效字符序列。(真题多次考过)
C语言规定标识符只能由字母、数字和下划线3中字符组成,且第一个字符必须为字母或下划线。(一般而言,变量用小写字母表示,常量以及结构体等使用大写字母以区分。
3.3 变量
变量代表一个有名字的、具有特定属性的一个储存单元。它是用来存放数据,也就是存放变量的值。在程序运行期间,它的值可以改变。(只能是同类型的值的改变,不能改变为其他类型的执行,即使发生隐式类型转换,它的只都不回改变的)
- 变量必须先定义后使用。在定义时指定该变量的名字和类型。一个变量应该有名字,以便被引用。
- 变量名实际上是以一个名字代表的一个储存地址。在对程序编译连接时由编译系统给每个变量名分配一个对应的内存地址。
- 对变量进行赋值就是对相应内存地址进行写入数据。从变量中取值,实际上是通过变量名找到相应的内存地址,从该存储单元中读取数据。
int a = 3:变量名为a,变量值为3,其中储存空间内储存的值3.
变量定义方式:这是决定这个变量存储形式、类型、属性等,具体参见函数部分螺旋规则!
变量定义位置:一般在函数的开头的部分定义变量,也可以在函数外定义变量(即全局变量、全局变量等)。C99 中允许在函数中的复合语句(用一堆花括号包起来的局部变量)中定义的变量。
3.4 常变量
const int a = 3;
表示a被定义为一个整型变量,指定其值为3,而且在变量存在期间其值不能改变(可以通过指针改变的)。
(1)常变量与常量的区别:常变量具有变量的基本属性:有类型,占存储单元,只是不允许改变。可以说的是,常变量就是有名字的,而常量是没有名字的,常变量可以在运行过程中引用,而常量不能引用。
(2)符号常量PI
和常变量:#define Pi 3.14;const float pi=3.14;
二者的性质不同:定义符号常量是使用预编译指令#define
,它使用符号常量代替一个字符串,在预编译时仅仅是替换,在预编译后,符号常量就不存在了(word的查找替换),对符号常量的名字是不分配储存单元的(唯一区别,也是优势,不占内存)。而常变量要占储存空间,有变量值,只是该值不能改变。
从使用角度看,常变量具有符号常量的优点,而且使用方便。有了常变量以后可以少用符号常量。对上述所有常量和变量进行小结:
#include <stdio.h>
#define symbol 999u // unsigned int
int main() {
// symbolic literal
printf("symbolic literal:%u\n\n", symbol); // notice
// 1.integer
printf("int:%d\n", 10000);
printf("long:%ld\n", 12345L);
printf("long long:%lld\n", 1234523ll);
printf("Octal integer:%d\n", 101);
printf("Hexadecimal integer:%d\n\n", 0X10f);
// 2.real number
printf("float:%f\n", 3.60f);
printf("double:%lf\n", 5.20);
printf("exponential form1:%e\n", 12.34e3);
printf("exponential form2:%E\n\n", 1e6);
// Scientific notation:automatic change the form bewteen %f and %E
printf("Scientific notation:%g\n\n", 16.6E8);
// 3.literal and escape char
printf("character:%c\n", 97); // ASCII:a
printf("backslash:%c\n%c", '\\', '\n'); // notice
// 4.string
printf("A string end with backslash:%s\n\n", "C programme\\"); // notice
// 5.variant
int x = 6666666;
printf("integer form of x:%i\n", x);
printf("Octal form of x:%o\n", x); // o/O
printf("Hexadecimal form of x:%X\n", x); // x/X
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/variant.c -o ./bin/variant
b12@PC:~/chapter3$ ./bin/variant
symbolic literal:999
int:10000
long:12345
long long:1234523
Octal integer:101
Hexadecimal integer:271
float:3.600000
double:5.200000
exponential form1:1.234000e+04
exponential form2:1.000000E+06
Scientific notation:1.66e+09
character:a
backslash:\
A string end with backslash:C programme\
integer form of x:6666666
Octal form of x:31334652
Hexadecimal form of x:65B9AA
:::
4、整型与补码
4.1 基本类型(int型)
编译系统分配给int数据类型2或4个字节(具体由编译系统决定)。在储存单元中的储存方式是:用整数的补码(complement)形式存放。
- 正数:二进制形式。
- 负数:先求出负数的补码。求负数的补码的方法是:现将此数的绝对值写成二进制形式,然后对其后面所有的二进制位按位取反,再加1. | 5的补码 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 1 0 1 | | —- | —- | —- | | 5的原码 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 1 0 1 | | 按位取反 | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 0 1 0 | | 再加1(-5的补码) | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 0 1 1 |
:::tips 观看完以上视频,有一个特殊点就是为何负数的最小值不是全部都是 1?因为 -0 无任何意义就可用来多表示一个数字。但是为何用这个多余的来表示负数最小值呢?这就与补码的发明形式有关!使得正负数“读起来”就是刚好对应权。 ::: 假设 gcc 为 int 型分配4个字节
正数 | 01111111 11111111 11111111 11111111 | |
---|---|---|
负数 | 10000000 00000000 00000000 00000000 |
4.2 短整型(short int)
类型名为short
或者short int
,一般分配2字节。如果给整型变量分配2字节,则存储单元中能够存放的最大值为0111 1111 1111 1111
,第一位位 0 代表正数,后面15位全为1,此数值是(215-1=32767).
最小值为1000 0000 0000 0000。
正数 | 01111111 11111111 | |
---|---|---|
负数 | 10000000 00000000 |
4.3 长整型(long int)
类型名为long int
或long
。一个long int
型变量的值范围跟 int
差不多,gcc 一般分配 8 个字节长度。
正数 | 01111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 | |
---|---|---|
负数 | 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 |
4.4 双长整型(long long int)
类型名为long long int
或者long long
,一般分配8个字节。C99 中新增的。
:::warning
注意:C标准没有具体规定各种数据类型所占储存单元长度,这是由各编译系统决定的。
C标准只要求long型长度不短于short,short不短于int. sizeof(short)<=sizeof(int)<=sizeof(long)<=sizeof(long long)
由于数据范围的有限性,因此在考虑定义变量类型的时候需要注意到范围,不要溢出。具体范围见下表。
:::
| int(基本类型) | 2 | -32768~32767(-215~215-1) | [signed] int |
| —- | —- | —- | —- |
| | 4 | -214783648~2147483647(-231~231-1) | |
| unsigned int(无符号基本整型) | 2 | 0~65535(216-1) | unsigned int |
| | 4 | 0~429467295(232-1) | |
| short(短整型) | 2 | -32768~32767(-215~215-1) | [signed] short [int] |
| unsigned short(无符号短整型) | 2 | 0~65535(216-1) | unsigned short [int] |
| long(长整型) | 4 | -214783648~2147483647(-231~231-1) | [signed] long [int] |
| unsigned long(无符号长整型) | 4 | 0~429467295(232-1) | unsigned long [int] |
| long long(双长型) | 8 | -9223372036854775808~9223372036854775807(-263~263-1) | [signed] long long [int] |
| unsigned long long(无符号双长型) | 8 | 0~18446744073709551615(0~264-1) | unsigned long long [int] |
4.5 signed 无符号属性修饰
signed
:无符号的区别就是最高位符号位不存在,只有正数,因此不需要看最高位,所以最大值就是全部1
说明:
(1)只有整型(包括字符型)数据可以加signed和unsigned修饰符且放在最前面,实型数据不可以加。
(2)对无符号整型数据用“%u”格式输出。%u表示用无符号十进制的格式输出。
unsigned short int price = 50; // 定义price为无符号短整型变量
printf("%u\n", price); // 指定用无符号十进制数的格式输出
错误:讲一个变量定义为无符号整型后,不应该向他赋值负数,否则会得到错误的结果。
unsigned short price = -1; // 错误赋值负数
printf("%d\n", price); // 输出65535
原因如下:系统对 -1 先转换成补码形式,就是全部二进位都是1),然后把它存入变量price中。由于price是无符号短整型变量,其左面第一位不代表符号,按 %d
格式输出(应该用 %u
输出),就是 65535 。
1的原码 | 0000 0000 0000 0001 |
---|---|
-1补码 | 取反:1111 1111 1111 1110 + 1 后全为 1 |
因此输出就是无符号短整型的最大值。
5、字符型数据
由于字符是按其代码(整数)形式储存,因此 C99 把字符型数据作为整数类型的一种。
5.1 字符与字符代码
字符与字符代码不是任意写一个字符,程序都能识别。(但是C语言本身对这些字符极扩展不是很好,头文件中 wchar_t
类型定义可以使用宽字节表示字符,虽然没有像 unicode 那样的编码规范,但是也不错了)
常见的字符的ASCII码需要记住: | 数字字符0-9 | 48-57 | | —- | —- | | 大写字母A-Z | 65-90 | | 小写字母a-z | 97-120 | | 空格字符 | 32 | | 换行符\n | 10 |
空格符:空格、水平制表符(tab)、垂直制表符、换行、换页(form feed)。
- 不能显示的字符:空(null)字符(以\0’表示)、警告(以’\a’表示)、退格(以\b’表示)、回车(以’\r’表示)等。(ctype.h 有
int isgraph(int ch)
库函数查看是否可以打印) :::tips 注意:字符1’和整数1是不同的概念。字符’1’只是代表一个形状为’1’的符号,在需要时按原样输出,在内存中以ASCII码形式存储,占1个字节;而整数1是以整数存储方式(二进制补码方式)存储的,占2个或4个字节。(一般printf输出的时候数字的输出基本上都是经过转换为字符进行的,当然这个不需要我们管) :::5.2 字符变量
字符变量:用类型符 char(character)定义字符变量。同变量一样,处于全局范围一般默认初始化为ASCII码的 0 ,即空白(屏幕上输出看不到)。如char c = '?';
定义 c 为字符型变量并使初值为字符'?'
。?
的ASCII代码是 63,系统把整数 63 赋给变量 c。
c 是字符变量,实质上是一个字节的整型变量,由于它常用来存放字符,所以称为字符变量。可以把 0~127 之间的整数赋给一个字符变量。在输出字符变量的值时,可以选择以十进制整数形式输出,或以字符形式输出。如:
#include <stdio.h>
int main() {
char c = '?';
printf("character:%c integer:%d\n", c, c);
int b = 98;
printf("character:%c integer:%d\n", b, b);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/character.c -o ./bin/character
b12@PC:~/chapter3$ ./bin/character
character:? integer:63
character:b integer:98
:::
5.3 字符型 unsigned 修饰
前面介绍了整型变量可以用signed
和unsigned
修饰符表示符号属性。字符类型也属于整型,也可以用signed
和unsigned
修饰符。
整型 | 字节数 | 取值范围 |
---|---|---|
signed char:有符号字符型 | 1 | |
unsigned char:无符号字符型 | 1 |
127个基本字符用 7 个二进制位存储,如果系统只提供127个字符,那么就将char型变量的第1个二进制位设置为0,用后面7位存放 127 个字符的代码。在这种情况下,系统提供的 char 类型相当于signed char。
但是在实际应用中,往往觉得127个字符不够用,希望能多提供一些可用的字符。根据此需要,有的系统提供了扩展的字符集。把可用的字符由127个扩展为255个,即扩大了一倍。
怎么解决这个问题呢? 就是把本来不用的第一位用起来。把 char 变量改为 unsigned char,即第一位并不固定设为 0,而是把 8 位都用来存放字符代码。这样,可以存放28-1即255个字符代码。
#include <stdio.h>
int main() {
int a = 127;
printf("character:%c integer:%d\n", a, a);
int b = 128;
printf("character:%c integer:%d\n", b, b);
return 0;
}
- 在使用有符号字符型变量时,允许存储的值为-128~127,但字符的代码不可能为负值,所以在存储字符时实际上只用到0~127这一部分,其第1位都是0.
- C 语言中的字符类型其本质还是整数,在内存中是以整数来储存的,只不过是用来装字符而已。因此在进行赋值和格式化输出的时候,它们大多是是等价的,没有什么隐式类型转换和报错等信息。但是溢出和字符集大小问题又是让人很烦。具体将在课后习题探索。
```c
include
int main() { for (int i = 128; i < 255; i++) { printf(“%d -> :%c\t”, i, i); } return 0; }
在 Ubuntu 打印 [https://askubuntu.com/questions/529114/why-cant-i-use-conio-h-or-ncurses-h](https://askubuntu.com/questions/529114/why-cant-i-use-conio-h-or-ncurses-h)<br />为何不可以在 Ubuntu 打印字符集问题!<br />[https://stackoverflow.com/questions/5781447/showing-characters-in-extended-ascii-code-ubuntu](https://stackoverflow.com/questions/5781447/showing-characters-in-extended-ascii-code-ubuntu)
<a name="zgL8q"></a>
## 5.4 字符集
到目前为止,都是在输出英文的字符,我大中文怎么在编程书里面就这么少呢???为啥作者说要汉化的编译器才可以支持中文?
- [字符集与编码问题](https://blog.csdn.net/a10929/article/details/78235793)
- [gcc编译器对宽字符的识别](https://www.cnblogs.com/guobbs/p/3654317.html)
<a name="KvLkK"></a>
# 6、浮点型数据
**浮点型数据**是用来表示具有小数点的实数的。为什么在C中把实数称为浮点数呢?
- 在C语言中,实数是以指数形式存放在存储单元中的。一个实数表示为指数可以有不止一种形式,如 3.14159 可以表示为 等,它们代表同一个值。
- 可以看到:小数点的位置是可以在 314159 几个数字之间、之前或之后(加0)浮动的,只要在小数点位置浮动的同时改变指数的值,就可以保证它的值不会改变。
- 由于小数点位置可以浮动,所以实数的指数形式称为浮点数。
浮点数类型包括float(单精度浮点型)、double(双精度浮点型)、long double(长双精度浮点型)。
- [浮点数表示](https://blog.csdn.net/shuzfan/article/details/53814424)(难懂)
- [浮点数二进制表示](https://blog.csdn.net/fwb330198372/article/details/70238982)(浅显易懂的例子)
:::tips
计算机中存储小数,即实数的两种方式是
1. 定点数存储法:整数+小数点:这样做的缺点就是在有限空间内无法存储更大的数。例如整数很大很大或者小数很长,究竟该如何选呢?这就是定点数存储法的缺点。
1. IEEE 指数形式存储:很像科学计数法(请注意这与补码一样是人为创造出来的刚好为计算机服务的),小数点位置由指数确定,因此可以存很大,但是具体精确度就受到阶码和尾数的制约。
:::
<a name="lv1TD"></a>
## 6.1 float(单精度浮点型)
比如 3.14159 这个浮点数,使用 float 来表示:
1. 分别求出整数和小数部分的二进制表示形式。
2. 将其二进制形式进行 **格式化** 处理,计算偏移量和填写尾数。如下表格所示。
| 数符 | 阶码 | 尾数 |
| --- | --- | --- |
| | | |
在 4 个字节(32位)中,究竟用多少位来表示小数部分,多少位来表示指数部分,**C标准并无具体规定**,由各编译器决定,例如 gcc 默认是 1 位表示符号,8 位表示阶码,23 位表示尾数。
- 尾数(小数)所占字节越多精度越高。
- 阶码(指数)所占字节越多范围越大。
<a name="SK8Yz"></a>
## 6.2 double(双精度)
为了扩大能表示的数值范围,用8个字节存储一个double型数据,可以得到 15 位有效数字,数值范围为。**为了提高运算精度,在C语言中进行浮点数的算术运算时,将float型数据都自动转换为double型,然后进行运算。**
<a name="lfOlP"></a>
## 6.3 long double(长双精度)
不同的编译系统对 long double型的处理方法不同,Turbo C 对long double型分配16个字节。而Visual C++则对long double型和 double 型一样处理,分配 8 个字节。请读者在使用不同的编译系统时注意其差别。
| 类型 | 字节数 | 有效数字 | 数值范围(绝对值) |
| --- | --- | --- | --- |
| float | 4 | 6 | <br /> |
| double | 8 | 15 | <br /> |
| long double | 8 | 15 | <br /> |
| | 16 | 19 | <br /> |
:::warning
浮点型数据判等问题:<br />典型例子就是判断根是否存在:,因为输入的数据可能存在浮点型,因此通过判断  就**天生**存在浮点误差导致原本应该是只有一个根,但是算出两个??<br />但是此时我们有不能进行要求输入的 `a, b, c` 是整型,也不可以对 向下或者取整运算,因此此时就用到计算机浮点误差 来认为“大致相等”。
:::
<a name="yLdhP"></a>
# 7、运算符
<a name="7a5qp"></a>
## 7.1 运算符、优先级与结合性
全部运算符:
| 优先级 | 运算符 | 含义 | 要求运算对象个数 | 结合方向 |
| --- | --- | --- | --- | --- |
| 1 | ( ) | 圆括号;函数调用 | | → |
| | [ ] | 下标运算符 | | |
| | -> | 指向结构体成员 | | |
| | . | 结构体成员 | | |
| 2 | ! | 逻辑非 | 1(单目运算符) | ← |
| | ~ | 按位取反 | | |
| | ++ | 自增:**后缀大于前缀** | | |
| | -- | 自减:**后缀大于前缀** | | |
| | - | 负号运算符 | | |
| | (类型) | 强制类型转换 | | |
| | * | 指针取地址内容 | | |
| | & | 取地址符 | | |
| | **sizeof** | **字节运算符** | | |
| 3 | * | 乘法 | 2(双目运算符) | → |
| | / | 除法 | | |
| | % | (整数)取余 | | |
| 4 | + | 加法 | 2(双目运算符) | → |
| | - | 减法 | | |
| 5 | << | 位运算:左移 | 2(双目运算符) | → |
| | >> | 位运算:右移 | | |
| 6 | < <= > >= | 关系运算符 | 2(双目运算符) | → |
| 7 | == != | 判断等于,不等于 | 2(双目运算符) | → |
| 8 | & | 位运算:按位与 | 2(双目运算符) | → |
| 9 | ^ | 位运算:异或 | 2(双目运算符) | → |
| 10 | | | 位运算:按位或 | 2(双目运算符) | → |
| 11 | && | 逻辑与:**短路寻假** | 2(双目运算符) | → |
| 12 | || | 逻辑或:**短路寻真** | 2(双目运算符) | → |
| 13 | cond ? : true : false | 条件运算符 | 3(三目运算符) | ← |
| 14 | = += -= *= /= %= >>= <<= &= ^= |= | [复合]赋值:lvalue | 2(双目运算符) | → |
| 15 | , | 逗号:顺序求值返回最后一个 | | → |
- **优先级**:同数学运算一致,先乘除后加减,因此根据运算符优先级确定执行顺序。
- **结合性**:如果在一个运算对象两侧的运算符的**优先级别相同**,则**按规定的“结合方向”处理**。
- 如 `a-b+c` `+-` 运算符优先级一致,算术运算符的结合方向都是“自左至右”,即先左后右,因此 b 先与减号结合,执行 `a-b` 的运算,然后再执行加 c 的运算。
- “自左至右的结合方向”又称“左结合性”,即运算对象先与左面的运算符结合。以后可以看到有些运算符的结合方向为“自右至左”,即右结合性(赋值运算符:若有 `a=b=c`,按从右到左顺序,先把变量 c 的值赋给变量 b,然后把变量 b 的值赋给变量 a)。
:::tips
- 键盘无  号,只能使用正斜杠 `/` 替换,需要和转义符,反斜杠: `\` 区分!整数除法是向下取整,如 。但是负数向哪里取整,由编译器决定!
- `%` 要求操作数都是整数,否则使用 `<math.h>` 头文件内的 `fmod(float a, float b)` 实现。
- 除 `%` 操作符之外,其余操作数可以是任意算术类型。
不必死记,只要知道算术运算符是自左至右(左结合性),赋值运算符是自右至左(右结合性),其他复杂的遇到时查一下即可。
拓展:用栈实现运算符的优先级!
:::
<a name="Sugdi"></a>
## 7.2 不同类型数据间混合运算
首先本例讨论范围是 **算术类型 **之间**不同类型数据数据间混合运算,**其他非算术类型如数组等组合类型于函数类型是不存在运算!。(**注:指针类型也纯量,也可参与混合运算,但是其只存在于整型之间的混合运算,其原理是加上地址偏移 **** **)<br />在程序中经常会遇到不同类型的数据进行运算,如 `5*4.5`。如果一个运算符两侧的数据类型不同,则先自动进行类型转换,使二者成为同一种类型,然后进行运算。整型、实型、字符型数据间可以进行混合运算。规律为:
- **float 和 double 混合**:`+、-、*、/` 运算的两个数中有一个数为 float 或 double 型,结果是 double 型,因为系统将所有 float 型数据都先转换为 double 型,然后进行运算。
- **“整型”之间混合**:此处“整型”是 **枚举类型(Enum)的枚举元素** 、 **字符型(char) **和 **布尔型(bool) **之间的转换!
- **枚举元素与整型**:首先枚举元素必须**先声明枚举类型并列出枚举元素**,再定义**枚举变量**去使用 **枚举元素**,其中枚举元素实际上就是**递增的整型**!
- **字符型与整型**:就是把字符的 ASCII 代码与整型数据进行运算。如:`12+'A'`,由于字符 A 的 ASCII 代码是 65,相当于`12+65`,等于 77 。
- **布尔型与整型**:在 C99 之前是没有布尔类型,真(1)假(0)表示,因此即使出现 `true` 和 `false` ,本质上任然是整型,因此可以进行运算。[C 语言的布尔类型(true 与 false)](https://www.runoob.com/w3cnote/c-bool-true-false.html)
- **int 和实型混合**:,向高精度看齐!
- 如果字符型数据与 float 数据进行运算,则将字符的 ASCII 代码转换为 float 型数据,然后进行运算。
- 如果 int 型与 float 或 double 型数据进行运算,先把 int 型和 float 型数据转换为 double 型,然后进行运算,结果是 double 型。
以上的转换是编译系统自动完成的,用户不必过问。**隐式类型转换,可能存在精度丢失。**各大编译器对**浮点型常量都以双精度处理。(唯一一次达成共识,因此存在浮点型常量就意味着存在 double,全部向 double 看齐!)**
```c
#include <stdio.h>
#include <stdbool.h>
enum Weekday {Sun=7, Mon=1, Tue, Wed, Thu, Fri, Sat}; // enumerate
int main() {
// 1.float and double
float f = 0.2; // 0.2f maybe more precise
double d = 0.1;
printf("%f\n", f + 0.5f); // float
printf("%lf\n", f + d); // double
// 2.int, Enum, char, bool
int i = 9;
enum Weekday day = Thu;
printf("Mon:%d, day:%d\n", Mon, day);
printf("Mon + i:%d, day + i:%d\n", Mon + i, day + i);
char a = 'a';
printf("a:%c, a:%d\n", a, a);
printf("char: a + i = %c, int: a + i = %d\n", a + i, a + i);
printf("a + day:%c, int: a + day = %d\n", a + day, a + day);
bool t = true;
printf("true:%d, false:%d\n", true, false);
printf("true + i:%d, false + i:%d\n", true + i, false + i);
printf("true + a:%d, false + a:%d\n", true + a, false + a);
printf("true + day:%d, false + day:%d\n", true + day, false + day);
// 3.int between float
printf("float + i + a + t + day = %f\n", f + i + a + t + day);
printf("double + i + a + t + day = %lf\n", d + i + a + t + day);
return 0;
}
编译运行:(由于 gcc 不会在意 float 被赋值一个浮点型常量而发出警告,因此 float f = 0.1
没有警告。
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/typeCal.c -o ./bin/typeCal
b12@PC:~/chapter3$ ./bin/typeCal
0.700000
0.300000
Mon:1, day:4
Mon + i:10, day + i:13
a:a, a:97
char: a + i = j, int: a + i = 106
a + day:e, int: a + day = 101
true:1, false:0
true + i:10, false + i:9
true + a:98, false + a:97
true + day:5, false + day:4
float + i + a + t + day = 111.199997
double + i + a + t + day = 111.100000
:::
书本案例:假设 ,求
的结果。
编译时,从左至右扫描,运算次序如下:
- 进行
10 + 'a'
的运算,’a’ 的值是整数 97,运算结果为 107 。 - 由于“”比“+”优先级高,先进行 `i f` 的运算。先将 i 与 f 都转成 double 型,运算结果为 7.5,double型。
- 整数 107 与
i * f
的积相加。先将整数 107 转换成双精度数,相加结果为 114.5,double型。 - 进行
d / 3
的运算,先将 3 转换成 double 型,d / 3
结果为 2.5,double型。(正是因为两操作数类型不同决定/
的特性,整数取整,实数“数学除法”) - 将
10 + 'a' + i * f
的结果 114.5 与d / 3
的商 2.5 相减,结果为 112.0,double型。
上述程序编译运行:注意输出可以使用 %f
输出 double 类型结果。因为 %f
和 %lf
可对 double 混用,但是输入只能 %lf
输入!
#include <stdio.h>
#define i 3
#define f 2.5f
#define a 'a'
#define d 7.5
int main() {
printf("10 + %d + %c + %d * %f - %lf / 3 = %f\n", // output format:%f instead of %lf
i, a, i, f, d, 10 + i + a + i * f - d / 3);
return 0;
}
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/typeTest.c -o ./bin/typeTest
b12@PC:~/chapter3$ ./bin/typeTest
10 + 3 + a + 3 * 2.500000 - 7.500000 / 3 = 115.000000
:::
7.3 强制与隐式类型转换
- 隐式类型转换:编译系统不经你同意进行的转换。包括算术转换,函数参数传参,定义变量类型与右值不同发生的改变。
- 可减轻负担,不需要思考太多转换。如上不同算数类型运算之间发生了隐式类型转换。
int a = 2.5;
- 带来不确定的精度误差。这种看编译器是否会提醒你精度问题,否则很容易带来不可预测的 bug。
- 可减轻负担,不需要思考太多转换。如上不同算数类型运算之间发生了隐式类型转换。
- 强制类型转换格式:
(类型名)(表达式)
- 其中表达式两侧括号不是必须,但是强烈建议添加上!
(int)(x + y)
:将 x + y 转换为 int 型。- 如果写成
(int)x + y
:此时就是先将计算(int)x
表达式的值,返回 x 的整型值,然后再与 y 相加得到整个表达式的结果。
- 误区:以为会对原操作数性质发生改变,书中绝大多数都是用“转换”一词描述其行为,但是个人认为最好的方式是“生成指定类型”。因而实际上原操作数类型是不会发生改变!
(double)a
:将 a 转换为 double 型。(float)(5 % 3)
:将5 % 3
表达式的值为整型,生成一个 float 型拷贝。
- 其中表达式两侧括号不是必须,但是强烈建议添加上!
强制类型转换到底用在何处?平常生活中的平均分成绩保留 2 位小数强制类型转换能做到吗?目前还不可以,只能在输出时按格式输出!主要用途:
%
运算一定要求两操作数必须是整型!!因此可以使用强制类型转换。- 动态内存强制类型转换!(用的最多)。
- 推荐文章:C 强制类型转换
将上述例子进行测试:
#include <stdio.h>
int main() {
// implicitly transform double into int
double d = 5.656;
int a = d - 2; // 3.656 ?
printf("double literal:%f\n", 5.555);
printf("a = %d, d = %lf\n", a, d);
// explicitly transform double into int
int c = (int)d;
printf("c %% a = %d\n", c % a); // notice:%%
printf("c = %d, d = %lf\n", c, d);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/TypeTransform.c -o ./bin/TypeTransform
b12@PC:~/chapter3$ ./bin/TypeTransform
double literal:5.555000
a = 3, d = 5.656000
c % a = 2
c = 5, d = 5.656000
:::
从结果上看到,double 类型的变量不管在隐式类型还是显式类型转换过程中都没有发生改变过。因为这两个过程都是产生新的类型,即开辟新的内存而不是在原来的基础上进行的。
:::tips
如果非要进行覆盖怎么办?既然你数据都定义了,想让它销毁再造,好像目前还无法主动让它消灭?那还不如新生一个类型,占用的一点空间不足为惜。
因为大多数强制类型都是要保留预先的值,而对于静态语言来说想要强制类型改变原来的数据类型。臣妾表示做不到!而动态语言类型,每秒都在改变变量的类型,为啥它能做到的呢??
:::
7.4 自增++、自减—运算符
这两个运算符最容易出错,因为它要求一个操作数,且必须是左值!!因此根据操作数与运算符的位置共有两种组合。其中运算符位于最后的称为后缀自增/减(先用操作数的值,用完再进行自增/减一,即使用原操作数);运算符位于最前的称为后缀自增/减(先对操作数的值进行自增/减一并返回增加后的操作数,即使用增减 1 的操作数)。
- 设
,其结果如下 | 操作 | 表达式返回结果 | i 最终值 | | —- | —- | —- | | i++ | 2 | 3 | | i— | 2 | 1 | | ++i | 3 | 3 | | —i | 1 | 1 |
注意:书上说它们优先级是一致的,结合顺序是从右往左,但是实际上 后缀表达式一般优先于前缀,还有很多人说后缀表达式的自增/减操作是在执行完该语句后再进行增/减,是错误的!!不存在这种说法!
#include <stdio.h>
int i = 2;
void test(int x) {
printf("test function:x = %d\n", x);
printf("test function:i = %d\n", i);
}
int main() {
test(i++);
printf("i = %d\n", i);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/suffixAdd.c -o ./bin/suffixAdd
b12@PC:~/chapter3$ ./bin/suffixAdd
test function:x = 2
test function:i = 3
i = 3
:::
不要学书上 **i+++j**
,整一堆混在一起运算,此时这个运算顺序跟编译器有关,但是一般如果你中间使用空格分隔,编译器知道你的意思。但是也非常不建议这种语义不清晰的代码。在目前阶段,可读性大于一切!!
8、表达式与语句
由运算符(operator)和运算数(operand)组成的式子就是表达式。表达式一定返回一个右值,因此可以用此表达式去给变量进行赋值操作或者继续参与其他表达式运算。
最简单的表达式就是 a + b
其计算两变量之和,然后得到和的值并返回。复杂度就是嵌套,其中 a
可是另外一个表达式, b
亦可嵌套多层表达式等等。
因而由规定的运算符优先级决定了执行表达式的顺序,因此会优先执行某个表达式,将其返回值继续作为下一个表达式的操作数(运算数)继续运算。
一个函数包含声明部分和执行部分,执行部分是由语句组成的,语句的作用是向计算机系统发出操作指令,要求执行相应的操作。一个C语句经过编译后产生若干条机器指令。声明部分不是语句,它不产生机器指令,只是对有关数据的声明。
C 语言共有以下 5 种语句:
- 控制语句
控制语句用于完成一定的控制功能。C语言只有9种控制语句,它们的形式是:下面 9 种语句表示形式中的 () 表示括号中是一个“判别条件”…”表示内嵌的语句
if(condition)…else… | 条件语句 |
---|---|
for(condition)… | 循环语句 |
while (condition)… | 循环语句 |
do…while (condition) | 循环语句 |
continue | 结束本次循环语句 |
break | 中止执行switch或循环语句 |
switch | 多分支选择语句 |
return | 从函数返回语句 |
goto | 转向语句,在结构化程序中基本不用goto语句 |
- 函数调用语句
函数调用语句由一个函数调用加一个分号构成,例如:printf("This is a C statement.");
其中 printf("This is a C statement.")
是一个函数调用,加一个分号成为一个语句。
- 表达式语句:表达式+;
表达式语句由一个表达式加一个分号构成,最典型的是由赋值表达式构成一个赋值语句。例如: a=3
是一个赋值表达式,而 a=3;
是一个赋值语句。一个语句必须在最后有一个分号,分号是语句中不可缺少的组成部分,而不是两个语句间的分隔符号。
- 空语句 ;
此语句只有一个分号,它什么也不做。作用:用于 for(stat1; stat2; stat3)
中选择省略语句一和语句二;还可以省略全部构成死循环。
- 复合语句 {}
可以用 {}
把一些语句和声明括起来成为复合语句(又称语句块)。
{
float pi = 3.14159, r = 2.5, area; // 定义变量
area = pi * r * r;
printf("area=%f", area);
}
- 可以在复合语句中包含声明部分(如上面的第2行)
- C99 允许将声明部分放在复合语句中的任何位置,例如
for(int i = 0; i < 5; i++)
。 - 复合语句常用在
if
语句或循环中,此时程序需要连续执行一组语句。 - 注意:复合语句内定义变量的作用域与生命周期问题!
8.1 赋值运算符
赋值符号“=”就是赋值运算符,它的作用是将一个数据赋给一个变量。
- 赋值运算符左侧应该是一个可修改值的“左值”(left value,简写为 lvalue)。左值是它可以出现在赋值运算符的左侧,它的值是可以改变的。
- 并不是任何形式的数据都可以作为左值的,左值应当为存储空间并可以被赋值。
- 变量可以作为左值,而算术表达式
a+b
就不能作为左值,常量也不能作为左值,因为常量不能被赋值。 - 如
a=3
的作用是执行一次赋值操作(或称赋值运算)。把常量(右值)3 赋给变量(左值) a。
能出现在赋值运算符右侧的表达式称为“右值”(right value,简写为rvalue)。
先求赋值运算符右侧的“表达式”的值,即
3 * 5
的结果是 15.- 然后将值
15
赋给赋值运算符左侧的变量。
既然是一个表达式,就应该有一个值,表达式的值等于赋值后左侧变量的值。赋值表达式 a=3*5
,对表达式求解后,变量 a
的值和表达式(整坨返回)的值都是15。其应用如下:赋值表达式中的“表达式”又可以是一个赋值表达式。
#include <stdio.h>
int main() {
int a, b, c;
a = b = c = 5;
printf("a:%d\tb:%d\t%d\n", a, b, c);
a = 5 + (c = 6);
printf("a:%d\tb:%d\t%d\n", a, b, c);
a = (b = 10) / (c = 2);
printf("a:%d\tb:%d\t%d\n", a, b, c);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/assignment.c -o ./bin/assignment
b12@PC:~/chapter3$ ./bin/assignment
a:5 b:5 5
a:11 b:5 6
a:5 b:10 2
:::
虽然看上去将赋值 =
变为运算符,然后构成表达式可以让代码“更短”,但是可读性不好,甚至容易出错!!个人是不推荐的!其最大的作用就是用于判断,当某个条件成立我们就给变量赋值,否则舍弃它。比如用于文件打开指针的赋值;简化某个判断耗费很大时候“偷懒”用!
比如如下程序用于计算输入的 x
的立方是否大于 N= 1000
,要求不管大于还是小于,给出 的结果,再给出
与
N = 1000
的大小关系。
#include <stdio.h>
#define N 1000
int main() {
int x;
printf("Please input a number: ");
scanf("%d", &x);
// x = x * x * x;
if ((x = x * x * x) > N) {
printf("x ^ 3 is %d > %d\n", x, N);
} else if (x == N) {
printf("x ^ 3 is %d = %d\n", x, N);
} else { // Error
printf("x ^ 3 is %d < %d\n", x, N);
}
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ vi ./src/assignExpression.c
b12@PC:~/chapter3$ gcc -Wall ./src/assignExpression.c -o ./bin/assignExpression
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 11
x ^ 3 is 1331 > 1000
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 10
x ^ 3 is 1000 = 1000
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 9
x ^ 3 is 729 < 1000
:::
其完全可以使用一个语句单独去替换,比如输入后就就行立方计算。本例只是用于举例,可能有点牵强,具体还是在文件 IO 操作指出。缺点有如下:
- 这种情况下,很容易将
()
漏掉而造成>
比=
大,例如if (x = x * x * x > N)
,其优先级是x * x * x
然后与N
进行比较,将比较表达式的结果(0/1)赋值给x
,最后赋值表达式x=0/1
返回赋值结果给if
语句当作条件判断决定其内语句的执行与否。因此不管怎样,x
最终的值要么是 0,要么是 1。完全超过预计,更别说if
条件判断错误!
将程序的 if
改为 if (x = x * x * x > N)
编译运行将会产生警告:结果与分析相吻合!
:::warning
b12@PC:~/chapter3$ vi ./src/assignExpression.c
b12@PC:~/chapter3$ gcc -Wall ./src/assignExpression.c -o ./bin/assignExpression
./src/assignExpression.c: In function ‘main’:
./src/assignExpression.c:8:6: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
8 | if (x = x x x > N) {
| ^
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 10
x ^ 3 is 0 < 1000
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 11
x ^ 3 is 1 > 1000
b12@PC:~/chapter3$ ./bin/assignExpression
Please input a number: 9
x ^ 3 is 0 < 1000
:::
8.4 复合赋值运算符
在赋值符 **=**
之前加上其他运算符,可以构成复合的运算符。如果在“=”前加一个“+”运算符就成了复合运算符“+=”。通过上面对赋值表达式的分析,赋值运算符有双重功能,计算和赋值。而复合赋值运算符就是完全体现这两点,且自带右操作数优先级!
表达式 | 括号优先级 | 展开 |
---|---|---|
a += 3 | - | a = a +3 |
a *= 3 + 2 | a *= (3 + 2) | a = a * (3 + 2) |
a %= 3 + 2 | a %= (3 + 2) | a = a % (3 + 2) |
a -= 3 | - | a = a - 3 |
a++ | a +=1 | a = a + 1 |
a— | a -= 1 | a = a - 1 |
- 赋值运算符顺序:先计算右侧表达式的值(因为
=
优先级比其它低且右结合性),然后将表达式的“右值”通过 = 赋值运算符送到变量 a 地址内的空间去,并最后返回赋值结果!(最后返回结果不一定是右侧表达式的值,可能发生溢出) - 复合赋值运算符默认 右侧表达式 有“隐形的”的括号,即展开后先计算右侧表达式的值。
- 自增/减 操作符是复合赋值运算符,其等价于上面的
a += 1
,即默认右侧表达式的值就是 1 。
8.5 赋值语句
在 C 程序中,最常用的语句是:赋值语句和输入输出语句。其中最基本用得最多的是赋值语句。C语句分类中,并没有看到赋值语句,实际上 C 语言的赋值语句是在赋值表达式的末尾加一个分号构成的。
其他一些高级语言(如 Python 等)有赋值语句,而无“赋值表达式”这一概念。这是 C 语言的一个特点,使之应用灵活方便。
在一个表达式中可以包含另一个表达式。赋值表达式既然是表达式,那么它就可以出现在其他表达式之中。例如:if((a = b) > 0) max = a;
- if 后面的括号内应该是一个“条件”,例如可以是
if(a > 0)max = a;
现在,在 a 的位置上换上一个赋值表达式a = b
先进行赋值运算(将 b 的值赋给 a ),然后判断 a 是否大于 0,如大于 0,执行
max = a
。 :::warning 注意,在 if 语句中的a = b
不是赋值语句,而是赋值表达式。if((a = b;)> 0) max = a;
:a = b;
是赋值语句,该语句结束后没有任何返回值(指令执行完毕)
结论:在 if 的条件中可以包含赋值表达式,但不能包含赋值语句。
用途:C 语言把赋值语句和赋值表达式区别开来,增加了表达式的种类,使表达式的应用几乎“无孔不入”,能实现其他语言中难以实现的功能。
:::
8.6 变量赋初值
前面说变量必须先定义再使用:先定义一个变量,然后可用赋值运算符对变量赋值;也可以在定义变量时对变量赋以初值(定义式声明)。
int a = 3; |
int a; a = 3; |
---|---|
float f = 3.56; |
float f; f = 3.56; |
char c = 'a'; |
char c; c = 'a'; |
- 部分初始化:
**int a, b, c = 5**
:指定 a,b,c 为整型变量,但只对 c 初始化,c 的初值为 5,其他值是随机的! - 多变量同时赋初值:
int a = 3, b = 4, c = 5;
:表示 a,b,c的初值都是 3。- 不能写成
int a = b = c = 3;
。为什么不可以?? - 这里的
**,**
是分隔作用(数组元素)还是运算符作用??我认为是分隔作用,不然后面的作为表达式的值向左返回,那么最后来个int val
? ,
和=
优先级坑:int a = 3, 2 + 5;
、int (a = 3), 2 + 5;
和int a = (3 - 5), 2 + 5;
结果如何?
- 不能写成
链式赋值:
int a, b, c; a = b = c = 5;
:将变量a, b, c
全部赋值为 5。因为赋值运算符的是右结合性,让遇到两个甚至多个=
,其顺序是先看最后边,因此执行c = 5
返回赋值结果5
,以此类推最后a = 5;
语句结束(该条指令完毕,不存在返回值说法) :::info 为什么要给变量赋值?不赋值会怎么样?一般变量初始化不是在编译阶段完成的(只有在静态存储变量和外部变量的初始化是在编译阶段完成的),而是在程序运行时执行本函数时赋予初值的,相当于执行一个赋值语句。
- 上面涉及变量生命周期问题,具体在函数章节指出。其主要内容就是是否在运行程序之前,全局变量值就是已知,它在整个程序运行周期都存在!
- 变量也不一定要求必须定义后赋初值,可以选择先不赋值,后面用到再赋值。
int main() { printf(“global_int:%d\tglobal_char:%c\tglobal_float:%f\n”, global_int, global_char, global_float);
int local_a, local_b, local_c;
printf("Uninitialization\tlocal_a:%d\tlocal_b:%d\tlocal_c:%d\n",
local_a, local_b, local_c);
// 1.test the priority of comma and equal-sign
local_a = 3, local_b = 4, local_c = 5;
printf("Priority test\tlocal_a:%d\tlocal_b:%d\tlocal_c:%d\n",
local_a, local_b, local_c);
local_a = (3, local_b = (4, local_c = 5));
printf("Priority test\tlocal_a:%d\tlocal_b:%d\tlocal_c:%d\n",
local_a, local_b, local_c);
// 2.Chain assignment
local_a = local_b = local_c = 996;
printf("Chain assignment\tlocal_a:%d\tlocal_b:%d\tlocal_c:%d\n",
local_a, local_b, local_c);
// 3.Variable initialization
int a, b, c = 5;
printf("Variable initialization\ta:%d\tb:%d\tc:%d\n",
a, b, c);
int d = 3, e = 4, f = 5;
printf("Variable initialization\td:%d\te:%d\tf:%d\n",
d, e, f);
// 4.Define but not use
double unused_var;
return 0;
}
编译运行:
:::warning
b12@PC:~/chapter3$ vi ./src/varInit.c<br />b12@PC:~/chapter3$ gcc -Wall ./src/varInit.c -o ./bin/varInit<br />./src/varInit.c: In function ‘main’:
./src/varInit.c:30:9: warning: unused variable ‘unused_var’ [-Wunused-variable]<br /> 30 | double unused_var;<br /> | ^~~~~~~~~~<br />./src/varInit.c:11:2: warning: ‘local_a’ is used uninitialized in this function [-Wuninitialized]<br /> 11 | printf("Uninitialization\tlocal_a:%d\tlocal_b:%d\tlocal_c:%d\n",<br /> | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br /> 12 | local_a, local_b, local_c);<br /> | ~~~~~~~~~~~~~~~~~~~~~~~~~~<br />./src/varInit.c:11:2: warning: ‘local_b’ is used uninitialized in this function [-Wuninitialized]<br />./src/varInit.c:11:2: warning: ‘local_c’ is used uninitialized in this function [-Wuninitialized]<br />./src/varInit.c:24:2: warning: ‘a’ is used uninitialized in this function [-Wuninitialized]<br /> 24 | printf("Variable initialization\ta:%d\tb:%d\tc:%d\n",<br /> | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br /> 25 | a, b, c);<br /> | ~~~~~~~~<br />./src/varInit.c:24:2: warning: ‘b’ is used uninitialized in this function [-Wuninitialized]
b12@PC:~/chapter3$ ./bin/varInit<br />global_int:0 global_char:A global_float:0.000000<br />Uninitialization local_a:32664 local_b:0 local_c:0<br />Priority test local_a:3 local_b:4 local_c:5<br />Priority test local_a:5 local_b:5 local_c:5<br />Chain assignment local_a:996 local_b:996 local_c:996<br />Variable initialization a:32664 b:-581926976 c:5<br />Variable initialization d:3 e:4 f:5
:::
程序分析:
- 共有 3 处警告,
1. `unused variable ‘unused_var’` :double 变量没使用。
1. `‘local_a/b/c’ is used uninitialized in this function`:表示在函数内使用未初始化的变量。
1. `‘a/b’ is used uninitialized in this function`:同上分析,只初始化 `c` 。
- 结果分析:
- **全局变量 **`**global**`** **:即使未赋初值也不警告,赋初值的就打印初值。
- **局部变量 **`**local**`** **:**未赋初值警告且值随机**,赋初值的就打印初值。
- `**,**`** 与 **`**=**`** 优先级测试**: `=` 优先级比 `,` 高,因此两次赋值结果不一致。通常用于一行写无数条装B语句。
- **链式赋值**:赋值表达式返回结果,因此最终 `local_a, local_b, local_c` 的值都是 996 。
<a name="tCIC0"></a>
## 8.7 赋值过程中的类型转换
:::danger
这是 C 程序最烦的,不会告诉你哪里错了。企图妄想编译器给你警告,那是不可能的。编译器不会给你说出一切,只是稍微提醒下你。但是编译器也是人开发的,如现在有 `Overflow` 和内存检测机制。一般出毛病的就是在使用指针和数组等溢出情况。懂得这部分内容,以后看到报错内容就知道内存错误或数据溢出!其中内存错误大多是越界或者野指针。
:::
1. 如果赋值运算符两侧的类型一致,则直接进行赋值。如 `i = 234;` 即设已定义i为整型变量此时直接将整数234存入变量i的存储单元中。
1. 如果赋值运算符两侧的类型不一致,但都是基本类型时,在赋值时要进行类型转换。类型转换是由系统自动进行的,转换的规则是:
1. :先对浮点数取整,即舍弃小数部分,然后赋予整型变量。如 
1. :数值不变,但以浮点数形式存储到变量中。;。(小数点??注意后缀 `f` 为区别 `float/double` 而添加)
1. :,只取  位有效数字,存储到 float 型变量的 `4` 个字节中。**应注意双精度数值的大小不能超出 float 型变量的数值范围。而 ****,**数值不变,在内存中以 `8` 个字节存储,**有效位数**扩展到 15 位。
- :指数为 100,超过了 float 数据的最大范围,`f` 无法容纳如此大的数,就出现错误,无法输出正确的信息。
4. :字符的 ASCII 代码赋给整型变量。`i = 'A';`:已定义 `i` 为整型变量由于 `'A'` 字符的ASCII 代码为 65,因此赋值后i的值为 65。
4. **占字节多的整型数据赋给占字节少的整型变量或字符变量:若超出低字节变量存储范围则发生溢出!**如把占 4 个字节的 int 型数据赋给占 2 个字节的 short 变量或占 1 个字节的 char 变量,只将其低字节原封不动地送到被赋值的变量(即发生“截断”)。
- `int i = 289; char c = 'a'; c = i;`:c 的值为 33,如果用 `"%c"` 输出 c,将得到字符“!”(其 ASCII 码为 33,如下图 3.15 所示)
- `int a = 32767; short b = a + 1;`:按理论上应得到 32768,但输出的结果却是 -32768,原因s 是对短整型数据分配 2 个字节,最大能表示 32767,无法表示 32768,如下图 3.16(a) 表示 int 型变量用 4 个字节存储 32767 的情况,加 1 以后,两个低字节的第 1 位为 1 发生进位,后 15 位为 0,把它传送到 short 变量 b 中,见图3.16(b)。由于整型变量的**最高位代表符号**,第 1 位是 1,代表此数是**负数**,它就是 `-32768` 的补码形式。
将上述编码:
```c
#include <stdio.h>
int main() {
float f = 3.56f;
double d = 123.456789e100;
int i = 23;
char c = 'z';
// 1.float/double -> int
int i1 = f;
printf("float -> int\ti1 = %d\n", i1);
i1 = d; // overflow
printf("double -> int\ti1 = %d\n", i1);
// 2.int -> float/double
float f1 = i;
printf("int -> float\tf1 = %f\n", f1);
double d1 = i;
printf("int -> double\td1 = %lf\n", d1);
// 3.float <-> double
float f2 = d; // overflow
printf("Overflow:double -> float\tf2 = %f\n", f2);
double d2 = f;
printf("float -> double\td2 = %f\n", d2);
// 4.char <-> int
int i2 = c;
printf("char -> int\ti2 = %d\n", i2);
char c1 = i;
printf("int -> char\tc1 = %c\n", c1);
char c2 = 289; // truncation
printf("Truncation:int -> char\tc2 = %d, c2 = %c\n", c2, c2);
// 5.int -> short
int i3 = 32767;
short s = i3 + 1; // overflow
printf("Overflow:int -> short\ti3 = %d, s = %d\n", i3, s);
return 0;
}
编译运行:
:::warning
b12@PC:~/chapter3$ gcc -Wall ./src/assignChange.c -o ./bin/assignChange
./src/assignChange.c: In function ‘main’:
./src/assignChange.c:32:12: warning: overflow in conversion from ‘int’ to ‘char’ changes value from ‘289’ to ‘33’ [-Woverflow]
32 | char c2 = 289; // overflow
| ^~~
b12@PC:~/chapter3$ ./bin/assignChange
float -> int i1 = 3
double -> int i1 = -2147483648
int -> float f1 = 23.000000
int -> double d1 = 23.000000
Overflow:double -> float f2 = inf
float -> double d2 = 3.560000
char -> int i2 = 122
int -> char c1 =
Truncation:int -> char c2 = 33, c2 = !
Overflow:int -> short i3 = 32767, s = -32768
:::
程序分析:
- warning:很明显只给出
int -> char
的溢出警告,但是double -> float
却没有。并且输出是。
char c1 = i
:因为 ASCII 为 23 是一个因此输出这个。
Truncation:int -> char
:发生 8 字节截断,编译器没有发出警告,输出字符!
。Overflow:int -> short
:如分析所示,因为最高位是符号位截取后就是负数的补码形式。 :::tips 如何避免细节坑:
- 避免把占字节多的整型数据向占字节少的整型变量赋值,因为赋值后数值可能发生失真。如果一定要进行这种赋值,应当保证赋值后数值不会发生变化,即所赋的值在变量的允许数值范围内。如果把上面的 a 值改为12345,就不会失真。
- 知道整型数据之间的赋值,按存储单元中的存储形式直接传送。实型数据之间以及整型与实型之间的赋值,是先转换(类型)后赋值。
- 在不同类型数据之间赋值时,常常会出现数据的失真,而且这不属于语法错误,编译系统并不提示出错。
:::
例 3.4 求三角形面积
解题思路:假设给定的三个边符合构成三角形的条件:任意两边之和大于第三边。解此题的关键是要找到求三角形面积的公式。从数学知识已知求三角形面积的公式为,其中
编写程序:根据上面的公式编写程序如下: ```cinclude
include
int main() { double a, b, c, s, area; printf(“Please input the triangle edges: “); scanf(“%lf %lf %lf”, &a, &b, &c); if (a + b <= c) { printf(“Error\n”); return -1; } if (a + c <= b) { printf(“Error\n”); return -1; } if (b + c <= a) { printf(“Error\n”); return -1; } s = (a + b + c) / 2; area = sqrt(s (s - a) (s - b) * (s - c)); printf(“a=%f\tb=%lf\tc=%f\n”, a, b, c); printf(“area=%f\n”, area); return 0; }
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/triangle.c -o ./bin/triangle -lm<br />b12@PC:~/chapter3$ ./bin/triangle<br />Please input the triangle edges: 1 2 1<br />Error
b12@PC:~/chapter3$ ./bin/triangle<br />Please input the triangle edges: 3.67 5.43 6.21<br />a=3.670000 b=5.430000 c=6.210000<br />area=9.903431
:::
:::tips
说明:
1. 三角形判断存在关系:任意两边之和大于第三边
1. 当不满足条件 1,那么没有必要继续算, `return -1` 是 main 函数异常退出给 OS 的返回值。这里用于结束,类似可以用 `<stdlib.h>` 中的 `exit(0);` 实现程序的终止;或者增加 `flag` 进行判断。
1. `gcc` 数学库需要加上 `-lm` 参数。
:::
<a name="jXI3B"></a>
# 9. 数据的输入输出
C 语言函数库中有一批标准输入输出函数,它是以标准的输入输出设备(一般为终端设备)为输入输出对象的。其中有 `putchar` (输出字符)、`getchar`(输入字符)、`printf`(格式输出)、`scanf`(格式输入)、`puts`(输出字符串)和 `gets`(输入字符串)。
<a name="hmTgs"></a>
## 9.1 例 3.5 求方程式  的根。
- **解题思路**:这里需要考虑两种情况,但是作者题意是。要是输入的 `a = 0` 程序因为除数为 0 爆炸!暂且认为是一元二次方程。求根公式步骤如下:
1. 有两个不等的实根。
1. 有两个相等实根。
1. 没有根(复数根)
- **算法**:
- 先判断
- ,方程有两个相等的根,即为 
- ,方程有两个不相等的根,即为 
- ,方程有两个不相等的复根,实部 ,虚部 ,即为 。(不要求掌握!有库函数对应求解负数根,知道就好)
```c
#include <stdio.h>
#include <math.h>
int main() {
double a, b, c;
printf("Please input three numbers: ");
scanf("%lf %lf %lf", &a, &b, &c);
// 1.whether Quadratic equation with one variable
if (a <= 1e-5) {
printf("invalid input! a = %7.2f\n", a);
return -1;
}
double delta = b * b - 4 * a * c, x1, x2;
double sqrt_delta = sqrt(fabs(delta)); // negative or positive
if (0 > delta) {
double p = -b / 2 * a;
double q = sqrt_delta / 2 * a;
printf("x1: %7.2f + %7.2fi, x2: %7.2f - %7.2fi\n", p, q, p, q);
} else if (delta - 0.0 <= 1e-5) {
x1 = x2 = -b / 2 * a;
printf("x1 = x2: %7.2f\n", x1);
} else {
x1 = (-b + sqrt_delta) / 2 * a;
x2 = (-b - sqrt_delta) / 2 * a;
printf("x1: %7.2f, x2: %7.2f\n", x1, x2);
}
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/equationRoot.c -o ./bin/equationRoot -lm
b12@PC:~/chapter3$ ./bin/equationRoot
Please input three numbers: 1 3 2
x1: -1.00, x2: -2.00
b12@PC:~/chapter3$ ./bin/equationRoot
Please input three numbers: 0 1 6
invalid input! a = 0.00
b12@PC:~/chapter3$ ./bin/equationRoot
Please input three numbers: 1 2 2
x1: -1.00 + 1.00i, x2: -1.00 - 1.00i
b12@PC:~/chapter3$ ./bin/equationRoot
Please input three numbers: 1 7 12
x1: -3.00, x2: -4.00
:::
程序说明:
:::info
- 用 scanf 函数输入a,b,c 的值,请注意在 scanf 函数中括号内变量 a,b,c 的前面,要用地址符&,即
&a,&b,&c
。&a
表示变量a在内存中的地址。该 scanf 函数表示从终端输入的 3 个数据分别送到地址为的&a,&b,&c
存储单元,也就是赋给变量 a,b,c。 - 输入
double
型必须双撇号内用**%lf**
格式声明,表示输入的是双精度型实数。输出可以**%lf**
或者**%f**
混用。 - 在 printf 函数中,不是简单地用
%f
格式声明,而是在格式符f的前面加了“7.2”,表示在输出 x1 和 x2 时,指定数据占 7 列,其中小数占 2 列。这样做的好处是:- 可以根据实际需要来输出小数的位数,因为并不是任何时候都需要 6 位小数的,例如价格只须 2 位小数即可(第3位按四舍五入处理)。
- 如果输出多个数据,各占一行,而用同一个格式声明(
%7.2f
),即使输出的数据整数部分值不同,但输出时上下行必然按小数点对齐,使输出数据整齐美观。
- 本例先判断是否是一元二次方程,然后求 3 种情况的方程根。由于是实数,
与
0
的大小很难判定。因为浮点数是浮动存放的,这里判断技巧是先判断小于 0 ,然后判断等于 0,最后就是大于 0。浮点数判断等于尤其是需要注意,只要满足小于一个很小的范围即可,即一般取或者
,其中
. :::
9.2 头文件与标准库函数
头文件:
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。 来自菜鸟教程
**#include**
指令形式:
- 标准方式:将头文件用尖括号括起来
#include <stdio.h>
的方式,是编译系统从存放 C 编译系统的子目录中去找所要包含的文件(如 stdio.h)。- 如果用
#include
指令是为了使用系统库函数,因而要包含系统提供的相应头文件,这时以用标准方式为宜,以提高效率。
- 如果用
双撇号括起来:
#include"stdio.h"
,在编译时,编译系统先在用户的当前目录(一般是用户存放源程序文件的子目录)中寻找要包含的文件,若找不到再按标准方式查找,还找不到就报错!。- 如果用户想包含的头文件不是系统提供的相应头文件,而是用户自己编写的文件(这种文件一般都存放在用户当前目录中),这时应当用双撇号形式,否则会找不到所需的文件。
- 如果该头文件不在当前目录中,可以在双撇号中写出文件路径(如
#include"path/filel.h"
),以便系统能从中找到所需的文件。 :::tips
应养成习惯,只要在本程序文件中使用标准输入输出库函数,一律加上
#include <stdio.h>
指令。- 考试不会涉及到头文件的知识,更不会让写项目。顶多一个文件写很多函数。 :::
库函数:
C 提供的标准函数以库的形式在 C 的编译系统中提供,它们不是 C 语言文本中的组成部分。不把输入输出作为 C 语句的目的是使 C 语言编译系统简单精练,因为将语句翻译成二进制的指令是在编译阶段完成的,没有输入输出语句就可以避免在编译阶段处理与硬件有关的问题,可以使编译系统简化,而且通用性强,可移植性好,在各种型号的计算机和不同的编译环境下都能适用,便于在各种计算机上实现。
各种 C 编译系统提供的系统函数库是各软件公司编制的,包括全部标准函数,已对它们进行了编译,成为目标文件(.obj文件)。
- 它们在程序连接阶段与由源程序经编译而得到的目标文件(.obj文件)相连接,生成一个可执行的目标程序(.exe文件)。
- 如果在源程序中有 printf 函数,在编译时并不把它翻译成目标指令,而是在连接阶段与系统函数库相连接后,在执行阶段中调用函数库中的 printf 函数。
- 不同的编译系统所提供的函数不完全相同。如通用的函数(如 printf 和 scanf )。
9.3 有关数据输入输出的概念
每一个 C 程序都包含输入输出。因为要进行运算,就必须给出数据,而运算的结果当然需要输出,以便人们应用。没有输出的程序是没有意义的。输入输出是程序中最基本的操作之一。
:::tips
从之前所有的程序,都看到后面运算结果是要输出的,但是数据是否有输入呢?比如一个程序中根本没有调用 scanf
函数是否就证明没有输入呢?其实不能以这样看,因为没有 scanf
就意味着你不能在程序运行的时候跟它打交道,让它改变其他的数据继续运算。这时指定执行的是你编写程序的数据而已。
:::
- 所谓输入输出是以计算机主机为主体而言的。
- 从计算机向输出设备(如显示器、打印机等)输出数据称为输出,从输入设备(如键盘、光盘、扫描仪等)向计算机输入数据称为输入。
C 语言本身不提供输入输出语句,输入和输出操作是由 C 标准函数库中的函数来实现的。
- 在 C 标准函数库中提供了一些输入输出函数,例如
printf
函数和scanf
函数。 - 不要误认为它们是 C 语言提供的“输入输出语句”。(记住 C 语言甩锅给编译器,不同编译器有不同库函数和实现方式)
- 在 C 标准函数库中提供了一些输入输出函数,例如
在程序文件开头用预处理指令
#include
把有关头文件
放在本程序中。- 如果程序调用标准输入输出函数,就必须在本程序的开头用
#include <stdio.h>
指令把stdio.h
头文件包含到程序中。#include
指令放在程序的开头,所以把 stdio.h 称为“头文件”(header file),文件后缀为“.h”。 - 在
stdio.h
头文件中存放了调用标准输入输出函数时所需要的信息,包括与标准 I/O 库有关的变量定义和宏定义以及对函数的声明。 - 在对程序进行编译预处理时,系统会把在该头文件中存放的内容调出来,取代本行的
#include
指令。 - 调用不同的库函数,应当把不同的头文件包含进来(如数学库
<math.h>
等)。
- 如果程序调用标准输入输出函数,就必须在本程序的开头用
9.4 [getchar](https://www.runoob.com/cprogramming/c-function-getchar.html)
(字符输入)、[putchar](https://www.runoob.com/cprogramming/c-function-putchar.html)
(字符输出)
C 函数库提供一些专门用于输入和输出字符的函数。注意是一个字符,即一个单词,而不是句子。
- 从计算机向显示器输出一个字符,可以调用系统函数库中的
**putchar**
函数(字符输出函数)。- 函数原型:
int putchar(int char)
,**char**
是要被写入的字符。该字符以其对应的 int 值进行传递。 putchar
是 put character(给字符)的缩写。作用是输出字符变量 c 的值,显然输出的是一个字符。putchar
函数只能输出一个字符。如果想输出多个字符就要用多个putchar
函数。
- 函数原型:
例 3.8 输出 BOY 3 个字符
#include <stdio.h>
#define NewLine 10
int main() {
char a = 66, b = 'O', c = 'Y';
putchar(a); // output a
putchar(b); // output b
putchar(c); // output c
putchar(NewLine); // output 10
putchar('\142'); // literal b
putchar('\157'); // literal o
putchar('\171'); // literal y
putchar('\n'); // output newline
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/putChar.c -o ./bin/putChar
b12@PC:~/chapter3$ ./bin/putChar
BOY
boy
:::
程序说明:
:::info
putchar(c)
中的 c 可以是字符常量、整型常量、字符变量或整型变量(其值在字符的 ASCII 码范围内)。- 可以用
putchar
函数输出转义字符,例如:putchar('\142');
:输出八进制的 142 是 98,即b
字符。putchar('\n')
:括号中的\n
是转义字符代表换行符,输出换行。 :::
- 向计算机输入一个字符,可以调用系统函数库中的
**getchar**
函数(字符输入函数)。- 函数原型:
int getchar(void)
,函数没有参数要求,返回值就是字符的 ASCII 码,赋值给变量即可。 getchar
是 get character(取得字符)的缩写。作用是从计算机终端(一般是键盘)输入一个字符,即计算机获得一个字符。getchar
函数只能接收一个字符。如果想输入多个字符就要用多个getchar
函数。
- 函数原型:
例 3.9 从键盘输入 BOY 3 个字符,然后把它们输出到屏幕。
解题思路:用 3 个 getchar
函数先后从键盘向计算机输入 BOY 3 个字符,然后用 putchar
函数输出。
#include <stdio.h>
int main() {
char a, b, c;
a = getchar(); // input a
b = getchar(); // input b
c = getchar(); // input c
putchar(a);
putchar(b);
putchar(c);
putchar('\n');
return 0;
}
编译运行:(一行输满 3 个字符再按 Enter 键)
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/getChar1.c -o ./bin/getChar
b12@PC:~/chapter3$ ./bin/getChar
BoY
BoY
:::
每输入一个字符后马上按 Enter 键,会得到什么结果?运行情况如下所示:
:::warning
b12@PC:~/chapter3$ ./bin/getChar
B
O
B
O
:::
程序说明:
:::info
- 在用键盘输入信息时,并不是在键盘上敲一个字符,该字符就立即送到计算机中去的。
- 这些字符先暂存在键盘的缓冲器中,只有按了 Enter 键才把这些字符一起输入到计算机中,然后按先后顺序分别赋给相应的变量。
- 输入字符 B 后马上按Enter,再输入字符 O,按Enter。立即会分两行输出 B 和 O
- 第 1 行输入的不是一个字符B,而是两个字符:B 和换行符,其中字符 B 赋给了变量 a,换行符赋给了变量 b
- 第 2 行接着输入两个字符:O 和换行符,其中字符 O 赋给了变量 c,换行符没有送入任何变量
- 在用
putchar
函数输出变量a,b,c的值时,就输出了字符B,然后输出换行,再输出字符 O,然后执行putchar('\n');
:::
拓展延申1:用 **getchar**
函数得到的字符可以赋给一个字符变量或整型变量,也可不赋给变量,而作为表达式利用它的值作为 **putchar**
函数参数而输出。
#include <stdio.h>
int main() {
char a, b, c;
putchar(getchar()); // input a
putchar(getchar()); // input b
putchar(getchar()); // input c
putchar('\n');
return 0;
}
编译运行:(一行输满 3 个字符再按 Enter 键)
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/getChar2.c -o ./bin/getChar2
b12@PC:~/chapter3$ ./bin/getChar2
BoY
BoY
:::
例 3.10 大写字母转小写字母
解题思路:只要知道 ASCII 码上大小写字母相差是 32 即 即可。
#include <stdio.h>
int main() {
char lower, upper;
upper = getchar(); // input upper
lower = upper + 32; // plus 32
putchar(lower);
putchar('-');
putchar('>');
putchar(upper);
putchar('\n');
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/upper2lower.c -o ./bin/upper2lower
b12@PC:~/chapter3$ ./bin/upper2lower
Q
q->Q
:::
拓展延申2:【位运算】大小写统一化(**tolower**
,**toupper**
)、大小写翻转(**swapcase**
)。
#include <stdio.h>
#include <ctype.h> // tolower,toupper
int main() {
char ch = getchar(); // input char
// 1.toupper
putchar(ch & 0x5f); // 101 1111
putchar(' ');
putchar(toupper(ch));
putchar('\n');
// 2.tolower
putchar(ch | 0x20); // 10 0000
putchar(' ');
putchar(tolower(ch));
putchar('\n');
// 3.swapcase
putchar(ch ^ 0x20); // 10 0000
putchar(' ');
if ('a' <= ch && ch <= 'z') {
putchar(ch - 32);
} else {
putchar(ch + 32);
}
putchar('\n');
return 0;
}
编译运行:
:::success
b12@PC:~/chapter3$ gcc -Wall ./src/swapcase.c -o ./bin/swapcase
b12@PC:~/chapter3$ ./bin/swapcase
Q
Q Q
q q
q q
b12@PC:~/chapter3$ ./bin/swapcase
r
R R
r r
R R
:::
9.5 gets
(字符串输入)、puts
(字符串输出)
9.5 格式化输入(scanf
)输出(printf
)函数
在 C 程序中用来实现输出和输入的主要是 printf 函数和 scanf 函数(默认从终端读取与输出)。
两者函数原型: :::info int printf(const char format, …)
int scanf(const char format, …) :::函数参数:
format
是“格式控制字符串”:是用双撇号括起来的字符串,简称格式字符串。它包括两个信息:- 格式声明:格式声明由
“%”
和格式字符组成,如%d, %f
等。作用是将输出的数据转换为指定的格式后输出/入。格式声明总是由“%”字符开始的。 - 普通字符:即需要在输出时原样输出的字符。例如上面
printf
函数中双撇号内的逗号、空格和换行符,也可以包括其他字符。
- 格式声明:格式声明由
- 输出/入表列:程序需要输出的一些数据,可以是常量、变量或表达式。
scanf
函数要求传入参数为指针。
format 标签属性是 %[flags][width][.precision][length]specifier 菜鸟教程:C 库函数 - printf() 菜鸟教程:C 库函数 - scanf()
格式字符 | 意义 |
---|---|
d/i | 以十进制形式输出带符号整数(正数不输出符号,i 不常用) |
u | 以十进制形式输出无符号整数 |
lu/llu | 32/64位无符号整数 |
f | 以小数形式输出单、双精度(lf,llf )实数 |
e,E | 以指数形式输出单、双精度实数 |
g,G | 以%f 或 %e 中较短的输出宽度输出单、双精度实数 |
o | 以八进制形式输出无符号整数(不输出前缀0 ) |
x,X | 以十六进制形式输出无符号整数(不输出前缀0x ) |
c | 输出单个字符 |
s | 输出字符串 |
p | 输出指针地址 |
flags(标识) | 描述 |
---|---|
- | 在给定的字段宽度内左对齐,默认是右对齐 |
+ | 强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认只有负数前面会显示一个 - 号 |
# | 与 o、x 或 X 说明符一起使用:非零值前面会分别显示 0、0x 或 0X。 与 e、E 和 f 一起使用:会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认如果后边没有数字,不会显示显示小数点。 与 g 或 G 一起使用:结果与使用 e 或 E 时相同,但是尾部的零不会被移除。 |
空格 | 如果没有写入任何符号,则在该值前面插入一个空格。 |
0 | 在指定填充 padding 的数字左边放置零(0),而不是空格。 |
width(宽度) | 描述 |
---|---|
(number) | 要输出的字符的最小长度 如果输出的值短于该数,结果会用空格填充。 如果输出的值长于该数,结果不会被截断。 |
* | 宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。典型案例:指定保留多少位小数! |
.precision(精度) | 描述 |
---|---|
.number | 对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。 如果写入的值短于该数,结果会用前导零来填充。 如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。 对于 e、E 和 f 说明符:要在小数点后输出的小数位数。 对于 g 和 G 说明符:要输出的最大有效位数。 对于 s: 要输出的最大字符数(默认所有字符都会被输出,直到遇到末尾的空字符)。 当未指定任何精度时,默认为 1;如果指定时不带有一个显式值,则假定为 0。 |
.* | 精度在 format 字符串中未指定,但会作为附加整数值参数放置于要被格式化的参数之前。 |
length(长度) | 描述 |
---|---|
h | 参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。 |
l | 参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。 |
L | 参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。 |
附录 integer overflow
整型溢出就是超过数据类型的所能容纳的最大限度,例如 int
类型范围为 ,超过这个范围就无法表示,并且不会报错!因为这个 BUG 非常致命!至于为什么数据类型要规定范围,这涉及到计算机组成原理。
integer security.pdf
整型溢出检测:参考 C: avoiding overflows when working with big numbers
gcc 编译器有一个 -ftrapv
参数,配合使用 SIGABRT
可以当作回调参数告诉你程序检测出整型溢出问题。
/* compile with gcc -ftrapv <filename> */
#include <signal.h>
#include <stdio.h>
#include <limits.h>
void signalHandler(int sig) {
printf("Overflow detected\n");
}
int main() {
signal(SIGABRT, &signalHandler);
int largeInt = INT_MAX;
int normalInt = 42;
int overflowInt = largeInt + normalInt; /* should cause overflow */
/* if compiling with -ftrapv, we shouldn't get here */
printf("overflowInt:%d", overflowInt);
return 0;
}
编译运行:
:::success
b12@PC:~/chapter0$ gcc -Wall -ftrapv ./src/test.c -o ./bin/test
b12@PC:~/chapter0$ ./bin/test
Overflow detected
Aborted (core dumped)
:::
使用 gcc
编译器自带函数检测:见文章 Detecting signed overflow in C/C++