不论是 GUI 还是命令行,计算机程序的处理的内容总是数据。在编程语言中,我们可以通过变量的概念来存取数据。
对 JavaScript 这样灵活的脚本语言,编写形如 var x = 123
和 var y = 'abc'
的变量赋值代码是非常简单而直观的。但这也带来了潜在的问题:脚本语言用户在定义并使用变量时,往往只关注变量名,而忽略了变量的类型。
类型有多重要呢?类型系统可以成为划分编程语言的主要维度之一。字符串、整数、浮点数、布尔值、集合、字典……类型的背后是编程语言的种种设计决策,它直接关系到一门语言所写出代码的:
- 安全性 - 完善的静态类型检查能避开空指针等暗坑。
- 抽象能力 - 抽象能力强的语言通常能够支持更复杂的类型。
- 可维护性 - 动态类型一时爽,重构代码火葬场。
- 运行效率 - 能够静态推导出的类型更利于性能优化。
正是因为类型系统如此重要,因而在跑通 Hello World 之后,我们希望首先从类型系统出发来重新了解 C。
我们经常能够听见这样的说法:“JavaScript 中函数是一等公民。”那么一等公民有何定义,这样的划分在 C 语言中又是如何体现的呢?
可以宽泛地认为,一等公民相当于一门编程语言中这样的实体:
- 可以被存入变量。
- 可以作为函数参数传递。
- 可以作为函数返回值返回。
- 可以在运行时创建,无需编译期静态声明。
- 可以匿名存在。
面向对象语言中的对象类型满足上面的每一条规则,因而我们可以认为这些语言中对象是一等公民。类似地,JavaScript 的函数也满足这些规则,故而有 JavaScript 中函数一等公民的说法。而在 C 中,四种基本的运算类型 char、int、float 和 double 都属于一等公民,至于剩下的布尔值、数组、指针、结构体和函数呢?它们都有着各自的局限:
- C 没有原生的布尔值类型,TRUE 和 FALSE 不过是 0 和 1 的语法糖。
- C 不支持对数组重新赋值,只能通过指针间接操作。
- C 没有匿名指针。
- C 的结构体、枚举和联合类型不能够动态创建,只能静态指定。
- C 的函数不能像
new Function()
那样动态创建。
现在我们已经知道了 C 有哪些类型,其中又有哪些是一等公民。但要根据类型来快速了解一门编程语言,其维度显然不止这一点。下面我们不妨从动态类型和静态类型的区别,来看看 C 与 JavaScript 的异同。
在 JavaScript 这样的脚本语言中,我们通常习惯只提供变量名来声明变量:
let x = 123;
let y = 'Hello World'
let z = [1, 2, 3]
而在 C 里,我们需要同时为变量指定变量名和类型:
int x = 42;
float y = 1.5;
这种区别的背后,实际上涉及一门编程语言是动态类型语言还是静态类型语言,这对其使用体验会产生很大的影响:
- 静态语言 - 变量类型在编译期指定,类型问题会造成编译失败。它的开发效率相对较低,但运行时更不容易出现类型错误。
- 动态语言 - 变量类型在运行时才能判断。它的开发效率相对较高,但更有可能出现运行时的类型错误。
很显然,C 属于静态语言,而 JavaScript 属于动态语言。静态语言能够避免什么错误呢?譬如这样的代码:
let fn // undefined
fn()
将任意的变量当做函数调用,这样的语法在 JavaScript 中都是合法的。但正如上面所看到的,被当做函数调用的变量,其值完全有可能是一个未定义的 undefined
,这所带来的报错相信前端同学一定不会陌生:
TypeError: undefined is not a function
那么,C 这样的静态语言就能通过编译期检查来杜绝运行时的类型错误了吗?这也是不准确的。在这方面,编程语言的强类型和弱类型之分是另一个重要的影响因素。
静态类型和动态类型的区别可以说泾渭分明,但强类型和弱类型则没有一个非常明确的边界。记得化学中熵的概念吗?熵没有单位,但可以比较。类似地,编程语言没有绝对的强类型和弱类型,只有相对的强弱之分。
在类型的强弱方面,有两个非常普遍的错误观点:
- Python 是脚本语言,所以它是弱类型的。
- C 是静态语言,所以它是强类型的。
我们可以用一行代码的实验,来分辨出 Python、JavaScript 和 C 中类型的强弱:
- 终端运行 Python,输入
1 + '1'
查看结果。 - 终端运行 Node,输入
1 + '1'
查看结果。 - 编译 implicit-conversion.c 并运行,查看 C 中
1 + '1'
的结果。
为什么同样是 1 + '1'
,在 JavaScript 中得到 '11'
,在 C 中得到 50,而在 Python 中会报错呢?
一门语言的类型越强,则意味着它越不容忍隐式类型转换;反之类型越弱,则越倾向于容忍隐式类型转换。作为例子,我们可以解析这三门语言在上面这个场景(将整数和字符串两种类型相加)下的处理策略:
- Python 没有隐式转换,解释器认为这是一个类型错误。作为替代,你可以选择
str(1) + 1
或1 + int('1')
。 - JavaScript 选择将数字隐式转换成字符串,然后执行字符串的相加操作,相当于
'1' + '1'
。 - C 的设计是先将 char 类型隐式转换为整数,再执行相加操作。C 的 char 类型是基于 ASCII 码表示的,这个编码中每个字符使用 0~256 中的一个数字表示,
'1'
对应十进制的 49,故而我们得到了 49 + 1 = 50 的整数。
所以,C 和 JavaScript 其实都属于弱类型语言,而常常被认为非常灵活的 Python 却是一门正经的强类型语言。对 C 来说,在后面的篇幅中我们会介绍的指针,甚至可以任意指定其所指的类型,这也是 C 中最灵活和最不容易掌握的特性了。
对于上文中提及的几种 C 基本数据类型,可以在 basic-types.c 中查看它们的定义和使用方式,注意 printf
中用于打印不同类型变量的标识符哦。
在明白了变量的类型后,在 C 中定义并使用它们相信不会是一件难事。但如何处理程序逻辑的复杂度呢?接下来让我们看看 C 中的控制流语句吧。