搞竞赛的时候学的 C 语言,这门语言对我来说应该算是很亲切了。
然而竞赛涉及到的语言特征毕竟有限,较为偏向理论,与工作时需要用到的相关语言特征肯定有很大不同。遂萌生了想从零开始系统的学习一下 C 语言和面向对象编程理念的想法,那么就从 coursera 上的这门课开始吧!
第三次数学危机到图灵机
哥德尔提出了不完全性定理:
任何一个形式系统,只要包括了简单的初等数论描述,而且是自洽的,它必定包含某些系统内所允许的方法既不能证明真也不能证伪的命题。
不完全性定理使数学家们追求完美形式化数学的梦想破碎了。
然而,数学家们决定继续研究可计算问题与不可计算问题的分界。可计算问题可以简化为如下的命题:设函数 f 定义域 D,值域 R,若 存在一种算法 对 D 中任意给出的 x 都能计算出相应的 f(x),则称该函数是可计算的。
所以,我们只需建立一个数学模型,然后证明,凡是这个计算模型能够完成的任务,就是 可计算的任务。
图灵提出的图灵机 (Turing machine),就是这样一种解决问题的模型,亦是现在计算机的前身。
计算机为什么能计算?
计算机是由各种电子元件与电路组成的,没有人脑的复杂结构,其是如何实现“计算”这一功能的呢?
关于计算机中数的表示
以图灵机为例,很容易发现:字母表中的符号越多,读入移动次数就越少,但是程序数量就越多;反之亦然。
字母表中的符号的最优数量最中确定为 2,这也就是为什么计算机中采用二进制表示体系的原因。
关于计算机中数的计算 (以加法为例)
布尔提出的布尔代数使得加法可以转变为一系列逻辑运算,而这些简单的逻辑运算可以用电路实现。
半加器可以实现不进位的二进制加法,半加器串联构成的全加器可以实现考虑进位二进制加法
因此:
- 参与运算的数可以转变为二进制数
- 二进制数的运算可以运用基本的布尔运算实现
- 基本的布尔运算都可以由电路实现
这就使得由电路与电子元件构成的计算机能够进行 “计算”
计算机发展史
- 现代计算机:真空管 => 晶体管 => 集成电路 => 大规模集成电路
- 摩尔定律 (Moore's Law):集成电路上可容纳的晶体管数目,约每隔两年便会增加一倍;CPU性能与成本比每18个月翻一番:摩尔定律还能坚持多久?
摩尔定律下的计算危机:晶体管大小限制;电泄露问题;散热问题
突破摩尔定律:全新的计算模式与计算理念 (量子计算机,生物计算机) - 量子计算机:量子 => 同一时间可以表示多种状态,大大提高计算效率
实现量子计算机的困境:与外界环境隔离以保持良好相干性 VS. 与外界环境耦合以控制演化并读出结果
计算机是如何计算的?、
冯诺依曼机 (Von Neumann machine)
- 早期理念:对于不同的计算问题,重新改编电路的排布
- 冯诺依曼的否定:应当采用 某种命令 来控制计算机以改变计算机的功能;这种 命令 不是临时输入的,而是 可存储的 => 存储程序式计算机/冯诺依曼式计算机 (EDVAK的问世)
- 冯诺依曼机:控制器,运算器,存储器,输入输出设备以总线相连接
现代计算机:控制器,运算器与存储器中的高速缓存区被集成在 CPU 中;存储器还有内存,外存 (硬盘,光驱) 部分;键鼠,显示器 => 输入输出设备
存储器
- 存储空间单位:1B = 8bit.. B => KB => MB => GB => TB (8个位 Bit 组成一个字节 Byte :字节是程序能够控制的最小内存单位)
- 构成(工作速度/价格排序):寄存器 \(>\) 高速缓冲区(CASHE) \(>\) 内存(临时存储,断电丢失) \(>\) 外存
CPU 由缓存到内存读取数据,若在内存中读取到,则将该数据所在数据块加载到缓存中,提高读取效率
CPU 对数据的访问具有 局部性 :时间局部(短时间内对同一个内存地址的多次访问), 空间局部(使用的信息相邻) - 静态 RAM 的六管基本存储单元:保存 0/1
- 地址与数据单元:32 位的 CPU 最多只能配备 4G 内存 => 32 位地址最多有 \(2^{32} B = 4G\)
CPU 程序执行
- CPU 电路可以进行计算,如何识别,执行程序? => 指令集
- 指令最终表示为二进制码,包含指令码与操作数
- 程序语言(人类编写) => 机器语言 => 二进制码形式指令
变量的定义
将内存感性地想成一个长长的 “火车轨道”;其中,每个格子有着 1 Byte = 8 bits 的储存空间,且每一格都有一个用十六进制字符串表示的地址。
定义一个变量 max:
- 选取几个连续的存储单元作为一个存储空间
- 将该存储空间命名为 max
- 将定义变量的初始值放入该存储空间中
- 记录 变量名 与 该片存储空间的起始地址 的 对应关系
整形变量
int
\((32 bit)\),short
\((16 bit)\),long long
\((64 bit)\) => 占用的字节数不同 => 在内存中占用的存储单元大小不同 => 表达的范围不同
为什么
unsigned
所能表示正数的范围通常是signed
范围的两倍?
signed X type 与 unsigned X type 占内存相同,但是 符号占 1 bit (正 0 负 1)。因此 signed : \(-2^{n-1}\) ~ \(2^{n-1}\), unsigned : \(0\) - \(2^n\)
- 负数的存储:符号位为 1, 剩余位置为绝对值的原码取反后加 1 => 这被称为 负数补码
- 打印一个数的二进制表示:打印一个数的十六进制表示 (hex):打印一个数的八进制表示 (oct) : 打印一个数的十进制表示 (dec)
cout << hex << a << endl;
cout << oct << a << endl;
cout << dec << a << endl;
INT_MAX
=>0x7fffffff
(f => 4 个 bit 全为 1) => \(2147483647\) => \(01111...111\)
INT_MIN
=>0x7fffffff + 1
=> -\(2147483648\) (\(-0\) => \(-2147483648\) => \(10000...000\), bc:当最高位为 1 其他位为 0 时:最高位 1 既表示数字位又表示符号位)
浮点型变量
float
\((32 bit)\),double
\((64 bit)\),long double
\((64 bit)\)- 浮点型数的存储:
以float
为例:符号位 1bit,指数位 8bit 包含 1bit 符号位,底数位 23 bit。以科学计数法形式存储
字符型变量
char
一个字符型占一个字节 (8 bit),\(e.g. @ => ASCII:64 => 01000000\)
最多可以表示 256 种字符,即 ASCII码集- 由于储存模式相同,可以与
int
型进行运算 - 关于转义字符
布尔型变量
bool
一个布尔型占一个字节 (8 bit) , \(00000000 or 00000001\)
为什么不用一个 bit? => 计算机所能控制的最小内存单位即为 1 byte
- 值的特性:非 0 即 1, 非 0 即 true
常量
- 常量标识符用于确定常量的类型:整型/浮点型常量后缀
const
关键字:为某个常量 “取名”
赋值运算
- 赋值:自动强制类型转换
- 长数赋值短数:截取短数长度的各位赋入
短数赋值长数:值不变
unsigned 型赋值为 signed 型:数字位 1 变为 符号兼数字位 1 - 赋值语句表达式:
x = e
注意,这个语句本身也是有值的!值即为 e。也就是 x = e evaluate 为 e (估计可以完成很多炫技操作)
如:
a = (b = 4) + (c = 6);
// a = 10, b = 4, c = 6
算术运算
- 算术运算符:
+
,-
,*
,/
,%
- 表达式运算法则,先乘除模后加减,括号优先,从左至右结合
- 计算不同类型数据的计算时的类型转换:
char /short * int -> int
,double * any -> double
,int * unsigned -> unsigned
- 自增自减运算:
i++
返回i
++i
返回i+1
关系运算符
- 关系运算符:
>
,<
,>=
,<=
,==
,!=
- 优先级:高优先级:
>
,<
,>=
,<=
低优先级:==
,!=
- 算术运算 > 关系运算 > 赋值运算
求取表达式的值:剪刀法,优先级从低到高来剪,再分别求值
逻辑运算符
- 逻辑运算符:
&&
,||
,!
- 优先级:非 > 与 > 或
- 逻辑非高于一切!
逗号运算符(woc,这原来是运算符)
,
:用于连接多个表达式,创造 逗号表达式x = e1, e2, e3, ..., en
整个逗号表达式的值为 en,即x = en
- 逗号运算符优先级:最低
例:
x = (x * 3, 16) // x = 16
x = x * 3, 16 // x = x * 3
- 条件运算符:
e1 ? e2 : e3
位运算
- 位运算:按位与,按位或,按位异或,左移,右移
- 复习一下之前用过的小 trick:用异或实现两数交换
运用的性质:a ^ b ^ b = a
a ^= b ^= a ^= b; // swap(a, b)
程序的控制成分
- 理论上可以证明:任何具有单入口单出口的程序都可以用三种基本结构表达:顺序结构,分支结构,循环结构
- 分支结构:
if-else
结构switch-case
结构:case 与 switch 中表达式相匹配。default
:其他所有情况
注意每个 case 相当于一个入口,所以如想要每个 case 控制一条语句的话加break
- 循环结构:
for
语句while
语句,do-while
语句goto
语句 (尽量避免使用!)
i = 0;
loop:
++i
goto loop
数组
- 一种数组初始化方式
a[MAX_N] = {0}
- 二维数组初始化:
a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
- 数组编程训练题:
有一道题有点意思:循环移动一个数组,如数组 \([1,2,3,4,5]\) 循环移动 \(2\) 位变为 \([4,5,1,2,3]\)
在不创造新数组,并且完全得到转换后的数组的情况下,以 \(O(3n)\) 的时间复杂度解决
方法是:将数组 a 从中间断开,两段分别进行翻转,再将整个数组翻转即可,很巧妙
\([1,2,3,4,5]\) -> \([1,2,3]\), \([4,5]\) -> \([3,2,1]\), \([5,4]\) -> \([3,2,1,5,4]\) -> \([4,5,1,2,3]\)
字符串与字符数组 (原来这是两个概念)
- 字符串都是以 '\0'为结尾的 :所有以 '\0' 为结尾的字符数组都是字符串
char x[5] = "China"; // 1
x[5] = "china" // 2
char x[5] = {'C', 'h', 'i', 'n', 'a'}; // 3
ccout << x << endl; // 4
第一句是合法的,将 x 字符数组初始化为字符串 China
第二句不合法,字符数组只有在定义时可以如此初始化。想要重新赋值只能一个一个改
第三句合法,但是请注意第三句和第一句是不等价的,第三句中的字符数组长度为 5,"China",而第一句中的字符数组为 6,"China\0"
第四句合法:可以用 cout + 字符数组名直接输出字符数组,输出至 '\0' 为止。为确保不会输出乱码,请确保字符数组以 '\0' 结尾
- 利用
cin
读入一个字符:输入缓冲区与缓冲区指针
只接受合适 type 的数据,以空格和回车区分数据
从输入流中读取数据:
while (cin >> c) {...}
// ^Z 终止
- 利用 ``cin.get()" 读入一个字符:将空格与回车也当作普通字符读入
while (cin.get(c)) { ... } // = while ((c = cin.get()) != EOF)
// ^Z 终止
- 利用
getchar()
读入一个字符:不跳过任何字符(包括 ^Z)
while (c = getchar()) { ... }
- 利用
cin
输入一个字符串:以空格与回车区分不同字符串
char s[10];
while (cin >> s) { ... } // abc\ndef\nghi => abc def ghi
// 以 ^Z 终止输入
- 利用
cin.get()
读入一个字符串:三个参数
char s[20];
cin.get(s, 10, 'o'); // we are good friends. => we are g
// 字符串名,从缓冲区中读取的字符数,终止字符(不填该参数默认为 '\n')
- 利用
cin.getline()
读入一个字符串:仍然是三个参数,与cin.get()
相同
cin.get() 与 cin.getline() 的区别:
使用cin.get()
后,缓冲区指针停留在终止字符之前;
而使用cin.getline()
后,缓冲区指针停留在终止字符之后
因此cin.getline()
更为常用:当终止符为\n
时,可以连续读入
e.g. 设终止符为 'o'
We are good friends => We are g|ood friends // cin.get()
We are good friends => We are go|od friends // cin.getline()
- 使用
cin.getline()
的常见误区
cin >> n;
for (int i = 1; i <= n; ++i) cin.getline(s, 10);
实际运行可以发现,我们只能输入 \(n-1\) 行字符串
这是因为在用 cin
读取完 n 后,缓冲区指针停在了换行符 \n
之前,因此有一个getline()
的作用仅仅是跳过了该换行符,而并未进行读取
修改方法
cin >> n;
cin.get() // or getchar() 吃掉这一换行符
for (int i = 1; i <= n; ++i) cin.getline(s, 10);