C 00_C语言基础概述

一、通俗地理解编程语言

1.1 理解编程语言

程序员使用特有的 “语言” 来控制计算机,让计算机为我们做事情,这样的语言就叫做 编程语言(Programming Language)。 编程语言有很多种类,常用的有 C、C++、Java、C#、Python、PHP、JavaScript、Go、Objective-C、Swift、汇编语言等,每种编程语言都有自己擅长的方面。

编程语言 主要用途
C/C++ C++ 是在C语言的基础上发展起来的,C++ 包含了C语言的所有内容,C语言是C++的一个部分,它们往往混合在一起使用,所以统称为 C/C++。C/C++主要用于PC软件开发、Linux开发、游戏开发、单片机和嵌入式系统。
Java Java 是一门通用型的语言,可以用于网站后台开发、Android 开发、PC软件开发,近年来又涉足了大数据领域(归功于 Hadoop 框架的流行)。
C# C# 是微软开发的用来对抗 Java 的一门语言,实现机制和 Java 类似,不过 C# 显然失败了,目前主要用于 Windows 平台的软件开发,以及少量的网站后台开发。
Python Python 也是一门通用型的语言,主要用于系统运维、网站后台开发、数据分析、人工智能、云计算等领域,近年来势头强劲,增长非常快。
PHP PHP 是一门专用型的语言,主要用来开发网站后台程序。
JavaScript JavaScript 最初只能用于网站前端开发,而且是前端开发的唯一语言,没有可替代性。近年来由于 Node.js 的流行,JavaScript 在网站后台开发中也占有了一席之地,并且在迅速增长。
Go语言 Go语言是 2009 年由 Google 发布的一款编程语言,成长非常迅速,在国内外已经有大量的应用。Go 语言主要用于服务器端的编程,对 C/C++、Java 都形成了不小的挑战。
Objective-C Swift Objective-C 和 Swift 都只能用于苹果产品的开发,包括 Mac、MacBook、iPhone、iPad、iWatch 等。
汇编语言 汇编语言是计算机发展初期的一门语言,它的执行效率非常高,但是开发效率非常低,所以在常见的应用程序开发中不会使用汇编语言,只有在对效率和实时性要求极高的关键模块才会考虑汇编语言,例如操作系统内核、驱动、仪器仪表、工业控制等。

编程语言 是用来控制计算机的一系列指令(Instruction),它有固定的 词汇(关键字)格式(语法)(不同编程语言的关键字和语法不尽相同或完全不一样)组成,使用时必须遵守相关语法,否则就会出错,达不到我们的目的。

1.2 编程语言分类

编程主要分为3个大类:

  • 机器语言(抽像层次最低的由0、1序列所表示的机器码)
  • 汇编语言(抽象层次较高的对应机器硬件的cpu指令集,英文缩的助记符号代码)
  • 高级语言(抽象层次更高的便于记忆和表示的英文代码)

计算机底层只能识别 01 形式的 机器码,所以高级语言所编写的代码,都要以某种方式被转换成机器码。将代码转换为机器码的方式有 编译解释 两种。因此,按程序代码转换为机器码的方式不同,将高级编程语言分为 编译型编程语言解释型编程语言混合型编程语言 三种。

编译型语言 就是先从源程序转换成机器指令,然后再由机器运行,而 解释型语言 就是逐条读取,逐条执行。

编译型语言:是在应用源程序执行之前,就将程序源代码 “翻译” 成目标代码(机器语言),因此其目标程序可以脱离其语言环境独立执行,使用比较方便、效率较高。但应用程序一旦需要修改,必须先修改源代码,再重新编译生成新的可执行文件才能执行,只有可执行文件而没有源代码,修改很不方便。现在大多数的编程语言都是编译型的编程语言。编译程序(编译器)将源程序翻译成目标程序后保存在另一个文件中,该目标程序可脱离编译程序和编译环境直接在计算机上多次运行。大多数软件产品都是以目标程序形式发行给用户的,不仅便于直接运行,同时又使它人难于盗用其中的技术,C、C++、Golang、Fortran、Visual Foxpro、Pascal、Delphi、Ada都是编译实现的。

解释型语言:在实现中,翻译器并不产生目标机器代码,而是产生易于执行的中间代码,这种中间代码与机器代码是不同的,中间代码的解释是由软件支持的,不能直接使用硬件,软件解释器通常会导致执行效率较低。用解释型语言编写的程序是由另一个可以理解中间代码的解释程序(解析器)执行的。与编译程序不同的是,解释程序的任务是逐一将源程序的语句解释成可执行的机器指令,不需要将源程序翻译成目标代码后再执行。解释程序的优点是当语句出现语法错误时,可以立即引起程序员注意,而程序员在程序开发期间就能进行校正。对于解释型语言,需要一个专门的解释器解释执行程序代码,每条程序语句只有在执行才被翻译。这种解释型语言每执行一次就翻译一次,因而效率低下。一般地,动态语言都是解释型的,如Python、Tcl、Perl、Ruby、VBScript、 JavaScript等。

混合型语言:Java很特殊,Java程序也需要编译,但是没有直接编译为机器语言,而是编译为字节码,然后在Java虚拟机上用解释的方式执行字节码。Python 也可采用了类似Java的编译模式,先将Python程序编译成Python字节码,然后由一个专门的Python字节码解释器负责解释执行字节码。Java虚拟机对字节码的执行相当于模拟一个cpu,而Ruby1.8(在虚拟机还未出现前)是通过解释成语法树执行。

1.3 高级编程语言发展简史

  • 1954年,John Backus发明了世界上第一种计算机高级语言Fortran,为之后出现的各类高级编程语言奠定了基础。

  • 1969年,AT&T公司的 Bell实验室,Ken Thompson 以BCPL语言为基础,设计出简单且接近硬件的B语言(取BCPL的首字母),并且它用B语言写了第一个Unix操作系统。

  • 1972年,Bell实验室的 Dennis Ritchie 和 Ken Thompson 共同发明了 C 语言,并使用 C 重写Unix。1973年初,C语言的主体完成。Dennis Ritchie 和 Ken Thompson 使用 C语言 完全重写了UNIX。

  • 1977年,Dennis Ritchie 发表了不依赖于具体机器系统的C语言编译文本《可移植的C语言编译程序》。

  • 1979年,Bjame Stroustrup到了Bell实验室,开始从事将C改良为带类的C(C with Classes)的工作,1983年该语言被正式命名为C++,主要意图是表明C++是C的增强版。

  • 1982年,很多有识之士和美国国家标准协会(ANSI)为了使C语言健康地发展下去,决定成立C标准委员会(ANSI),建立C语言的标准。委员会由硬件厂商、编译器及其它软件工具生产商、软件设计师、顾问、学术界人士、C语言作者和应用程序员组成。

  • 1985年发布了第一个C++版本。第一个版本的C++,因其面向对象的思想使得编程变得简单,并且又保持了C语言的运行效率,在推出的一段时间内,得到了快速的发展,占据了编程语言界的半壁江山。

  • 1989年,ANSI发布了第一个完整的C语言标准——ANSI X3.159-1989,简称 C89,不过人们也习惯称其为 ANSI C。C89在1990年被国际标准化组织(International Standard Organization,ISO)一字不改地采纳,ISO官方给予的名称为:ISO/IEC 9899,所以ISO/IEC9899:1990也通常被简称为 C90

  • 从1985年到1998年,C++从最初的C with Classes新增了很多其它的特性,比如异常处理、模板、标准模板库(STL)、运行时异常处理(RTTI)与名字空间(Namespace)等。

  • 1998年,C++标准委员会统筹C++的所有特性,发布了第一个C++国际标准 C++98

  • 1999年,在做了一些必要的修正和完善后,ISO发布了新的C语言标准,命名为ISO/IEC 9899:1999,简称 C99

  • 2011年12月8日,ISO又正式发布了新的标准,称为ISO/IEC9899:2011,简称为 C11

二、C语言(C Language)

2.1 学习C语言编程

C语言(C Language)是一种编译型的高级编程语言,学习C语言,主要是学习它的关键字、语法及编译原理。 下面是一个C语言的完整例子,它会让计算机在屏幕上显示 Hello, C语言.

1
2
3
4
5
#include <stdio.h>
int main(){
    puts("Hello, C语言.");
    return 0;
}

这些具有特定含义的词汇、语句,按照特定的格式组织在一起,就构成了C源代码(Source Code),也称C源码 或 C代码(Code)。

在开发软件的过程中,我们需要将编写好的代码(Code)保存到一个文件中(以便进行编译或重复使用),这种用来保存代码的文件就叫做源文件(Source File)

每种编程语言的源文件都有特定的后缀,以方便被编译器或解释器识别,被程序员理解。源文件后缀大都根据编程语言本身的名字来命名,例如:

  • C语言源文件的后缀是 .c
  • C++语言(C Plus Plus)源文件的后缀是 .cpp
  • Java 源文件的后缀是 .java
  • Python 源文件的后缀是 .py
  • JavaScript 源文件后置是 .js
  • Golang 源文件后置是 .go

源文件的后缀仅仅是为了表明该文件中保存的是某种语言的代码(例如.c文件中保存的是C语言代码),这样程序员更加容易区分,编译器也更加容易识别,它并不会导致该文件的内部格式发生改变。

C语言规定了源代码中每个词汇、语句的含义,也规定了它们该如何组织在一起,这就是语法(Syntax)。

学习C语言,一是学习它的词法语法,二来是学习内存、编译和链接、弄清编程语言的内在机理。

C语言仅支持面向过程编程

2.2 C语言标准库

标准C语言(ANSI C)共定义了15 个头文件,称为 C标准库,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平。

  • 合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h>
  • 熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h>
  • 优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>

2.3 C语言的三套标准:C89、C99 和 C11

  • C89标准:1983 年美国国家标准局(American National Standards Institute,简称 ANSI)成立了一个委员会,专门来制定C语言标准。1989 年C语言标准被批准,被称为 ANSI X3.159-1989 “Programming Language C”。 这个版本的C语言标准通常被称为 ANSI C,又由于这个版本是 89 年完成制定的,因此也被称为 C89

  • C99标准:在 ANSI C 标准确立之后,C语言的规范在很长一段时间内都没有大的变动。1995 年C程序设计语言工作组对C语言进行了一些修改,增加了新的关键字,编写了新的库,取消了原有的限制,并于 1999 年形成新的标准—— ISO/IEC 9899:1999 标准,通常被成为 C99

Tips: C99对C89的改变

  • 增加restrict指针
  • inline(内联)关键字
  • 新增数据类型 _Bool 
  • 对数组的增强,可变长数组,数组声明中的类型修饰符等
  • 单行注释
  • 分散代码与声明
  • 预处理程序的修改
    • 变元列表
    • _Pragma运算符
    • 内部编译指令
    • 新增的内部宏
  • for语句内的变量声明  
  • 复合赋值
  • 柔性数组结构成员
  • 指定的初始化符
  • printf()和scanf()函数系列的增强
  • C99新增的库
  • __func__预定义标识符
  • 其它特性的改动
    • 放宽的转换限制
    • 不再支持隐含式的int规则
    • 删除了隐含式函数声明
    • 对返回值的约束
    • 扩展的整数类型
    • 对整数类型提升规则的改进
  • C11 标准C11 标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C语言标准委员会于 2011 年底正式发布,支持此标准的主流C语言编译器有 GCC、LLVM/Clang、Intel C++ Compile 等。

Tips: C11相比C99的变化

  • 对齐处理操作符 alignof,函数 aligned_alloc(),以及 头文件 <stdalign.h>。
  • _Noreturn 函数标记,类似于 gcc 的 attribute((noreturn))。
  • _Generic 关键词,有点儿类似于 gcc 的 typeof。
  • 静态断言( static assertions),_Static_assert(),在解释 #if 和 #error 之后被处理。
  • 删除了 gets() 函数,C99中已经将此函数被标记为过时,推荐新的替代函数 gets_s()。
  • 新的 fopen() 模式,(“…x”)。类似 POSIX 中的 O_CREAT|O_EXCL,在文件锁中比较常用。
  • 匿名结构体/联合体。
  • 多线程支持,包括:_Thread_local,头文件 <threads.h>,里面包含线程的创建和管理函数(比如 thrd_create(),thrd_exit()),mutex (比如 mtx_lock(),mtx_unlock())等等。
  • _Atomic类型修饰符和 头文件 <stdatomic.h>。
  • 带边界检查(Bounds-checking)的函数接口,定义了新的安全的函数,例如 fopen_s(),strcat_s() 等等。
  • 改进的 Unicode 支持,新的头文件 <uchar.h> 等。
  • 新增 quick_exit() 函数,作为第三种终止程序的方式,当 exit() 失败时可以做最少的清理工作(deinitializition)。
  • 创建复数的宏, CMPLX()。
  • 更多浮点数处理的宏 。
  • struct timespec 成为 time.h 的一部分,以及宏 TIME_UTC,函数 timespec_get()。

三、进制详解:二进制、八进制和十六进制

3.1 进制

进制 也就是 进位计数制,是人为定义的带进位的计数方法(有不带进位的计数方法,比如原始的结绳计数法,唱票时常用的“正”字计数法,以及类似的tally mark计数)。对于任何一种进制 —— X进制,就表示每一位上的数运算时都是逢 X 进一位。 十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,以此类推,x 进制就是逢 x 进一位。

Tips:X进制 在进行加法运算时逢 X 进一(满X进一),进行减法运算时借一当X,这就是 X进制,这种进制也就包含X个数字,基数为X。

  • 十进制(Decimalism): 十进制(Decimalism)有 0~9 共10个数字,基数为10,在加减法运算中,逢十进一,借一当十。

  • 二进制(Binary): 二进制(Binary) 用 0、1 两个数字来表示的数值,基数为2,在加减法运算中,逢二进一,借一当二。在计算机内部,数据都是以二进制的形式存储的,二进制是学习编程必须掌握的基础。

  • 八进制(Octal): 除了二进制,C语言还会使用到八进制。八进制有 0 ~ 7 共8个数字,基数为8,加法运算时逢八进一,减法运算时借一当八。

  • 十六进制(Hexadecimal) 除了二进制和八进制,十六进制也经常使用,甚至比八进制还要频繁。十六进制中,用A来表示10,B表示11,C表示12,D表示13,E表示14,F表示15,因此有 0~F 共16个数字,基数为16,加法运算时逢16进1,减法运算时借1当16。

注: 十六进制中的字母不区分大小写,ABCDEF 也可以写作 abcdef。

3.2 进制转换

3.2.1 二进制、八进制、十六进制转换为十进制

二进制、八进制和十六进制向十进制转换都非常容易,就是“按权相加”。所谓“权”,也即“位权”。 假设当前数字是 N 进制,那么:

  • 对于整数部分,从右往左看,第 i 位的位权等于 Ni-1
  • 对于小数部分,恰好相反,要从左往右看,第 j 位的位权为 N-j

各种进制转换成十进制示例

  • 二进制:

    1001 = $1×2^3 + 0×2^2 + 0×2^1 + 1×2^0$ = 8 + 0 + 0 + 1 = 9(十进制)

  • 二进制:

    101.1001 = $1×2^2 + 0×2^1 + 1×2^0$ + 1×2-1 + 0×2-2 + 0×2-3 + 1×2-4 = 4 + 0 + 1 + 0.5 + 0 + 0 + 0.0625 = 5.5625(十进制)

  • 八进制:

    302 = $3×8^2 + 0×8^1 + 2×8^0$ = 192 + 0 + 2 = 194(十进制)

  • 八进制:

    302.46 = $3×8^2 + 0×8^1 + 2×8^0$ + 4×8-1 + 6×8-2 = 192 + 0 + 2 + 0.5 + 0.09375= 194.59375(十进制)

  • 十六进制:

    EA7 = $14×16^2 + 10×16^1 + 7×16^0$ = 3751(十进制)

3.2.2 十进制转换为N进制(如:二进制、八进制、十六进制)

将十进制转换为其它进制时比较复杂,整数部分和小数部分的算法不一样,下面我们分别讲解。

  1. 整数部分 —— 十进制整数转换为 N 进制整数采用 除 N 取余,逆序排列 法,具体做法是:
  • 将 N 作为除数,用十进制整数除以 N,可以得到一个商和余数;
  • 保留余数,用商继续除以 N,又得到一个新的商和余数;
  • 仍然保留余数,用商继续除以 N,还会得到一个新的商和余数;
  • ……
  • 如此反复进行,每次都保留余数,用商接着除以 N,直到商为 0 时为止。
  • 把先得到的余数作为 N 进制数的低位数字,后得到的余数作为 N 进制数的高位数字,依次排列起来,就得到了 N 进制数字。
  1. 小数部分 —— 十进制小数转换成 N 进制小数采用 乘 N 取整,顺序排列 法,具体做法是:
  • 用 N 乘以十进制小数,可以得到一个积,这个积包含了整数部分和小数部分;
  • 将积的整数部分取出,再用 N 乘以余下的小数部分,又得到一个新的积;
  • 再将积的整数部分取出,继续用 N 乘以余下的小数部分;
  • ……
  • 如此反复进行,每次都取出整数部分,用 N 接着乘以小数部分,直到积中的小数部分为 0,或者达到所要求的精度为止。
  • 把取出的整数部分按顺序排列起来,先取出的整数作为 N 进制小数的高位数字,后取出的整数作为低位数字,这样就得到了 N 进制小数。

注意: 十进制小数转换成其它进制小数时,结果有可能是一个无限位的小数。请看下面的例子:

十进制 0.51 对应的二进制为 0.100000101000111101011100001010001111010111…,是一个循环小数; 十进制 0.72 对应的二进制为 0.1011100001010001111010111000010100011110…,是一个循环小数; 十进制 0.625 对应的二进制为 0.101,是一个有限小数。

3.2.3 二进制与八进制、十六进制的转换

其实,任何进制之间的转换都可以使用上面讲到的方法,只不过有时比较麻烦,所以一般针对不同的进制采取不同的方法。将二进制转换为八进制和十六进制时就有非常简洁的方法,反之亦然。

  1. 二进制整数和八进制整数之间的转换
  • 二进制整数转换为八进制整数时,每三位二进制数字转换为一位八进制数字,运算的顺序是从低位向高位依次进行,高位不足三位用零补齐。
  • 八进制整数转换为二进制整数时,思路是相反的,每一位八进制数字转换为三位二进制数字(不足三位从左边用零补齐),运算的顺序也是从低位向高位依次进行。
  1. 二进制整数和十六进制整数之间的转换
  • 二进制整数转换为十六进制整数时,每四位二进制数字转换为一位十六进制数字,运算的顺序是从低位向高位依次进行,高位不足四位用零补齐。
  • 十六进制整数转换为二进制整数时,思路是相反的,每一位十六进制数字转换为四位二进制数字(不足四位从左边用零补齐),运算的顺序也是从低位向高位依次进行。

在C语言编程中,二进制、八进制、十六进制之间几乎不会涉及小数的转换,所以这里我们只讲整数的转换,大家学以致用足以。

本节前面两部分(3.2.1、3.2.2小节)讲到的转换方法是通用的,任何进制之间的转换都可以采用,只是有时比较麻烦而已。(3.2.3小节)二进制和八进制、十六进制之间的转换有非常简洁的方法,所以没有采用前面(3.2.1、3.2.2小节)的方法。

3.2.4 二进制是计算机处理数据的基础

  1. 计算机要处理的信息是多种多样的,如数字、文字、符号、图形、音频、视频等,这些信息在人们的眼里是不同的。但对于计算机来说,它们在内存中都是一样的,都是以二进制的形式来表示。

  2. 这些二进制的,实际上就是电路元器件;电路的电压会变化,低电压表示 0,高电压表示 1。

四、数据在内存中的存储(二进制形式存储)

4.1 内存的原理

内存条 是一个非常精密的大规模集成电路,它包含了上亿或更多个电子元器件,它们很小,达到了纳米级别。人们通过电路来控制每个元器件的通断电从而产生高低两个不同电平(电压),用低电平(0V)表示0、高电平(2-5V)表示1,这样一个元器件有了2种状态(0 或者 1)以此表示二级制的 0、1。把多个元器件组合在一起就会得到很多种($2^n$种)不同的0、1的组合。例如,8个元器件有 $2^8$=256 种不同的组合,16个元器件有 $2^{16}$ =65536 种不同的组合。虽然一个元器件只能表示2个数值,但是多个结合起来就可以表示很多数值了。

我们可以给每一种组合赋予特定的含义,例如,可以分别用 1101000、00011100、11111111、00000000、01010101、10101010 来表示 C、语、言、中、文、网 这几个字,那么结合起来 1101000 00011100 11111111 00000000 01010101 10101010 就表示 “C语言中文网”。

4.2 内存的组织形式及内存大小的换算方式

在计算机系统中,一般情况下不会一个一个的使用内存的元器件,而是将8个元器件看做一个单位,即使表示很小的数,例如 1,也需要8个,也就是 00000001。

1个元器件称为1比特(Bit)或1位,8个元器件组合在一起(一般是连续的)称为1字节(Byte),那么16个元器件就是2Byte,32个就是4Byte,以此类推:

  • 8×1024个元器件就是1024Byte,简写为1KB;
  • 8×1024×1024个元器件就是1024KB,简写为1MB;
  • 8×1024×1024×1024个元器件就是1024MB,简写为1GB。

现在,我们知道1GB的内存有多少个元器件了吧 ($8 * 2^{30}$个)。我们通常所说的文件大小是多少 KB、多少 MB,就是这个意思。

计算机存储单位换算:

  • 1Byte = 8Bit (1个字节 等于 8个比特)
  • 1KB = 1024Byte = $2^{10}$ Byte = 8 * $2^{10}$Bit
  • 1MB = 1024KB = $2^{20}$ Byte
  • 1GB = 1024MB = $2^{30}$ Byte
  • 1TB = 1024GB = $2^{40}$ Byte
  • 1PB = 1024TB = $2^{50}$ Byte
  • 1EB = 1024PB = $2^{60}$ Byte

我们平时使用计算机时,通常只会设计到 KB、MB、GB、TB 这几个单位,PB 和 EB 这两个高级单位一般在大数据处理过程中才会用到。

在内存中没有abc这样的字符,也没有gif、jpg这样的图片,只有 01 两个数字,计算机也只认识 01。所以,计算机使用二进制,而不是我们熟悉的十进制,写入内存中的数据,都会被转换成 01 的组合。

五、程序载入内存运行起来的过程

5.1 程序(软件)加载运行

程序(软件)是安装(保存)在计算机硬盘(外存)上的一类可以被加载到计算机内存中由CPU读取并执行的指令集(以文件形式存储)及相关数据文件的集合。

双击软件图标 或 在命令行输入程序命令和参数并回车后,操作系统就会找到(根据图标文件或命令行的环境变量)要运行这个软件(程序)在硬盘中的存储路径,并将程序数据(安装的软件本质上就是很多数据的集合)读取(复制)到内存中运行起来。

扩展:

内存的读写速度比硬盘快很多 对于读写速度,内存 > 固态硬盘 > 机械硬盘。 机械硬盘是靠电机带动盘片转动来读写数据的,而内存条通过电路来读写数据,电机的转速肯定没有电的传输速度(几乎是光速)快。 虽然固态硬盘也是通过电路来读写数据,但是因为与内存的控制方式不一样,速度也不及内存。

所以,所有的程序都是先从硬盘上将程序数据复制到内存,才能让CPU来处理(运行),这个过程就叫作 载入内存(Load into Memory),完成这个过程需要一个特殊的程序(软件)叫做 加载器(Loader)

CPU直接与内存打交道,它会读取内存中的数据进行处理,并将结果保存到内存。如果需要保存到硬盘,才会将内存中的数据复制到硬盘。

CPU直接从内存中读取数据,处理完成后将结果再写入内存(CPU、内存、硬盘和主板的关系)。 alt 属性文本

5.2 虚拟内存(Virtual Memory)

计算机的内存(内存条)容量是比较有限的存储资源,如果我们运行的程序较多,占用的空间就会超过内存(内存条)容量。例如计算机的内存容量为2G,却运行着10个程序,这10个程序共占用3G的空间,也就意味着需要从硬盘复制 3G 的数据到内存,这显然是不够的。

操作系统(Operating System,简称 OS)为我们解决了这个问题:当程序运行需要的空间大于内存容量时,会将内存中暂时不用的数据再写回硬盘;需要这些数据时再从硬盘中读取,并将另外一部分不用的数据写入硬盘。这样,硬盘中就会有一部分空间用来存放内存中暂时不用的数据,这一部分空间就叫做 虚拟内存(Virtual Memory)

硬盘的读写速度比内存慢很多,反复交换数据会消耗很多时间,所以如果你的内存太小,会严重影响计算机的运行速度,甚至会出现“卡死”现象,即使CPU强劲,也不会有大的改观。

六、字符编码集

6.1 计算机编码简介

计算机是以二进制的形式来存储数据的,它只认识 01 两个数字,我们在屏幕上看到的文字,在存储之前都被转换成了二进制(01 序列),在显示时也要根据二进制找到对应的字符。

可想而知,特定的文字必然对应着固定的二进制,否则在转换时将发生混乱。那么,怎样将文字与二进制对应起来呢?这就需要有一套规范,计算机公司和软件开发者都必须遵守,这样的一套规范就称为字符集(Character Set)或者字符编码(Character Encoding)。

严格来说,字符集和字符编码不是一个概念,字符集定义了文字和二进制的对应关系,为字符分配了一个唯一的编号,而字符编码规定了如何将文字的编号存储到计算机中。

可以将字符集理解成一个很大的表格,它列出了所有字符和二进制的对应关系,计算机显示文字或者存储文字,就是一个查表的过程。

在计算机逐步发展的过程中,先后出现了几十种甚至上百种字符集,有些还在使用,有些已经淹没在了历史的长河中,本节我们要讲解的是一种专门针对英文的字符集——ASCII编码。

6.2 拉丁字母简介

在正式介绍 ASCII 编码之前,我们先来说说什么是拉丁字母。估计也有不少读者和我一样,对于拉丁字母、英文字母和汉语拼音中的字母的关系不是很清楚。

拉丁字母也叫罗马字母,它源自希腊字母,是当今世界上使用最广的字母系统。基本的拉丁字母就是我们经常见到的 ABCD 等26个英文字母。

拉丁字母阿拉伯字母斯拉夫字母(西里尔字母) 被称为世界三大字母体系。 拉丁字母原先是欧洲人使用的,后来由于欧洲殖民主义,导致这套字母体系在全球范围内开始流行,美洲、非洲、澳洲、亚洲都没有逃过西方文化的影响。中国也是,我们现在使用的拼音其实就是拉丁字母,是不折不扣的舶来品。

后来,很多国家对 26 个基本的拉丁字母进行了扩展,以适应本地的语言文化。最常见的扩展方式就是加上变音符号,例如汉语拼音中的ü,就是在u的基础上加上两个小点演化而来;再如,á、à 就是在a的上面标上音调。

总起来说:

  • 基本拉丁字母就是 26 个英文字母;
  • 扩展拉丁字母就是在基本的 26 个英文字母的基础上添加变音符号、横线、斜线等演化而来,每个国家都不一样。

6.3 ASCII 编码

ASCII码一览表,ASCII码对照表: https://c.biancheng.net/c/ascii/

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节(总共有256个数)就可以存储(表示),它等同于国际标准 ISO/IEC 646。

计算机是美国人发明的,它们首先要考虑的问题是,如何将二进制和英文字母(也就是拉丁文)对应起来。

扩展: ASCII 编码是美国人给自己设计的,它们并没有考虑欧洲那些扩展的拉丁字母,也没有考虑汉字、韩语和日语等。 计算机也是美国人发明的,起初使用的就是 ASCII 码,只能显示英文字符。各个国家为了让本国公民也能正常使用计算机,开始效仿 ASCII 开发自己的字符编码,例如 ISO/IEC 8859(欧洲字符集)、shift_Jis(日语字符集)、GBK(中文字符集)等。

当时,各个厂家或者公司都有自己的做法,编码规则并不统一,这给不同计算机之间的数据交换带来不小的麻烦。但是相对来说,能够得到普遍认可的有 IBM 发明的 EBCDIC 和此处要谈的 ASCII。

ASCII 的标准版本于 1967 年第一次发布,最后一次更新则是在 1986 年,迄今为止共收录了 128 个字符,用一个字节(Byte)中较低的 7 个比特位(Bit)足以表示($2^7$ = 128)全部字符(所以还会空闲下一个比特位置为0),其中包含了 95 个可显示字符 和 33 个控制字符(具有某些特殊功能但是无法显示的字符)。

ASCII 编码包含:

  • 52个基本的拉丁字母(英文大小写字母 A ~ Za ~ z
  • 10个阿拉伯数字(也就是 1 2 3 4 5 6 7 8 9 0
  • 标点符号(, . ! 等)
  • 特殊符号(@ # $ % ^ & 等)
  • 具有控制功能的字符:第 0 ~ 31 个字符(开头的 32 个字符)以及第 127 个字符(最后一个字符)都是不可见的(无法显示),但是它们都具有一些特殊功能,所以称为控制字符( Control Character)或者功能码(Function Code)

ASCII 编码 中,大写字母、小写字母和阿拉伯数字都是连续分布的,这给程序设计带来了很大的方便。例如要判断一个字符是否是大写字母,就可以判断该字符的 ASCII 编码值是否在 65~90 的范围内。

Tips: 标准ASCII码中文对照表 https://c.biancheng.net/c/ascii

EBCDIC 编码正好相反,它的英文字母不是连续排列的,中间出现了多次断续,给编程带来了一些困难。现在连 IBM 自己也不使用 EBCDIC 了,转而使用更加优秀的 ASCII。

ASCII 编码已经成了计算机的通用标准,没有人再使用 EBCDIC 编码了,它已经消失在历史的长河中了。

业界也将 ASCII 编码分成两部分:

全角和半角输入法的区别

全角和半角的区别主要在于除汉字以外的其它字符,比如标点符号、英文字母、阿拉伯数字等,全角字符和半角字符所占用的位置的大小不同。

在计算机屏幕上,一个汉字要占两个英文字符的位置,人们把一个英文字符所占的位置称为 “半角”,相对地把一个汉字所占的位置称为 “全角”

标点符号、英文字母、阿拉伯数字等这些字符不同于汉字,在半角状态它们被作为英文字符处理,而在全角状态作为中文字符处理,。

七、C语言关键字分类

7.1 关键字分类

在C语言里共有32个关键字,可以将其分为4类:数据类型、控制语句、存储类型、其它关键字

  • 数据类型关键字(12个):char、int、short、long、signed、unsigned、float、double、enum、struct、union、void
  • 控制语句关键字(12个):if、else、switch、case、default、for、while、do、continue、break、 goto、return
  • 存储类型关键字(4个):auto、static、register、extern。
  • 其它关键字(4个):const、sizeof、typedef、volatile

7.2 关键字简介

7.2.1 数据类型关键字(12个)

1、char:声明字符型变量 或 函数返回值,占一个字节,也就是 8 个二进制位:

  • char 表示有符号的类型,表示的范围是 -128 ~ 127;
  • unsigned char 表示无符号的类型,表示的范围是 0 ~ 255;

2、int: 声明整型变量 或 函数返回值

int 数据类型占内存的位数与操作系统的位数及编译器有关,一般情况下在当前主流的编译器中 int 类型无论在 32 位或 64 位系统中都是 4 个字节。

3、short:声明短整型变量 或 函数返回值

short 是占两个字节。short 在C语言中是定义整型变量家族的一种,short i ;表示定义一个短整型的变量 i 。

依据程序编译器的不同short定义的字节数不同。标准定义short短整型变量不得低于16位,即两个字节。编译器头文件夹里面的limits.h定义了short能表示的大小:SHRT_MIN ~ SHRT_MAX。在32位平台下如windows(32位)中short一般为16位。

4、long:声明长整型变量 或 函数返回值

C语言中 long 是4个字节,是一种数据类型,有两种表现形式:有符号和无符号。

  • 在有符号中,long 的表示数的范围为:-2147483648 ~ 2147483647
  • 在无符号中,long 的表示数的范围为:0 ~ 4294967295

5、signed:声明有符号类型变量 或 函数返回值

signed 是默认的,表示这个变量是有符号的,可以存储整数和负数。

signed 存储符号是有代价的,代价就是存储空间中的一个比特位专门用来存储符号,这一位不能表示数值。一般来说,同类型的 signed 能够存储的数的绝对值大小要小于 undigned 。

6、unsigned:声明无符号类型变量 或 函数返回值

每一种整型都有 无符号( unsigned )有符号( signed ) 两种类型( float 和 double 总是带符号的)

在默认情况下声明的整型变量都是有符号的类型( char 有点特别),如果需声明无符号类型的话就需要在类型前加上 unsigned 。

无符号版本和有符号版本的区别就是无符号类型能保存 2 倍于有符号类型的数据,比如 16 位系统中一个 int 能存储的数据的范围为 -32768~32767 ,而 unsigned 能存储的数据范围则是 0~65535。

7、float:声明浮点型变量 或 函数返回值

float 是C语言的基本数据类型中的一种,表示单精度浮点数,占 4 个字节。

8、double:声明双精度变量 或 函数返回值

double 占的字节:16 位编译器下,double 占 8 个字节;32 位编译器下,double 占 8 个字节;64 位编译器下,double 占 8 个字节。

9、enum:声明枚举类型

enum 是计算机编程语言中的一种数据类型。在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些数量声明为整型,字符型或其它类型显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

10、struct:声明结构体变量 或 函数返回值

在C语言中,可以使用结构体( Struct )来存放一组不同类型的数据。

结构体的定义形式为:

1
2
3
struct 结构体名{
    结构体所包含的变量或数组
;

结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员( Member )变量。

11、union:声明共用体(联合)数据类型

C语言中的 union 是联合体,就是一个多个变量的结构同时使用一块内存区域,区域的取值大小为该结构中长度最大的变量的值。

12、void:声明函数无返回值或无参数,声明无类型指针(基本上就这三个作用)

void 被翻译为"无类型",相应的 void * 为"无类型指针" (可以指向任意具体类型的指针(内存地址))。常用在程序编写中对定义函数的参数类型、返回值、函数中指针类型进行声明。

7.2.2 控制语句关键字(12个)

  • 条件语句

13、if: 条件语句

if (表达式) {语句;} 用于单分支选择结构; 如含有交叉关系,使用并列的if语句;

14、else:条件语句否定分支(与 if 连用)

if (表达式) {语句;} else {语句;}

在C语言中 else 是与 if 一起使用的一个关键字,表示如果满足if条件则不执行 else ,否则执行else 。

  • 开关语句

15、switch: 用于开关语句

switch 语句也是一种分支语句,常常用于多分支的情况。

16、case:开关语句分支

case 常量表达式只是起语句标号作用,并不是该处进行条件判断。在执行 switch 语句时,根据 switch 的表达式,找到与之匹配的 case 语句,就从此 case 子句执行下去,不在进行判断,直到碰到 break 或 函数返回值结束为止。

c语言中 case 是和 switch 一起使用的,构成 switch—case 语句,进行判断选择,case 是用来表示选择结构的。

17、default:开关语句中的“其它”分支

default 的作用就是switch语句里所有的 case 都不成立时所要执行的语句。

default 关键字用来标记switch语句中的默认分支。

  • 循环语句

18、for:循环语句

for 是C语言中的一个关键字,主要用来控制循环语句的执行

19、while:循环语句的循环条件

while 语句创建了一个循环,重复执行直到测试表达式为假或0。

while 语句是一种入口条件循环,也就是说,在执行多次循环之前已决定是否执行循环。因此,循环有可能不被执行。 循环体可以是简单语句,也可以是复合语句。

20、do:循环语句的循环体

C语言中 do 是执行某代码块的意思,do 关键字不能单独使用,通常用在 do…while 循环中。

在C语言中,do…while 循环是在循环的尾部检查它的条件,do…while 循环与 while 循环类似,但是 do…while 循环不管循环控制条件是真是假都要确保至少执行一次循环。

21、break:跳出当前循环

C 语言中 break 语句有以下两种用法:

  • 终止循环: 该语句结束(终止)循环且程序流将继续执行循环语句块的下一条语句。当 break 语句出现在一个循环内时,循环会立即结束;
  • 终止switch语句的case它可用于终止 switch 语句中的一个 case 。

Tips: 如果使用的是嵌套循环(即一个循环内嵌套另一个循环, 或多层循环),break 语句会停止执行当前层的循环,然后开始执行该块之后的下一行代码。

22、continue:结束当前循环,开始下一轮循环

continue 跳过本次循环余下的语句,进入(继续)下一次循环。而 break 是直接跳出(结束)循环。

比如 for 循环,遇到 contimue 生效后,直接重新执行 for 的表达式,也就是本循环中 continue 下面的语句就不执行,跳过循环中的一次。 contimue 其作用为结束本次循环。即跳过循环体中下面尚未执行的语句,对于 while 循环,继续求解循环条件。而对于 for 循环程序流程接着求解 for 语句头中的第三个部分 expression 表达式。

continue 语句只结束本次循环,而不终止整个循环的执行。而 break 语句则是结束整个循环过程,不再判断执行循环的条件是否成立。

  • 返回语句

23、return:子程序返回语句(可以带参数,也看不带参数)

return 表示把程序流程从被调函数转向主调函数并把表达式的值带回主调函数,实现函数值的返回,返回时可附带一个返回值,由 return 后面的参数指定。

  • 跳转语句

24、goto :无条件跳转语句

goto 语句可以使程序在没有任何条件的情况下跳转到指定的位置,所以 goto 语句又被称为是无条件跳转语句。

使用 goto 语句只能 goto 到同一函数内,而不能从一个函数里 goto 到另外一个函数里

不能从一段复杂的执行状态中的位置 goto 到另外一个位置,比如,从多重嵌套的循环判断中跳出去就是不允许的。

应该避免向两个方向跳转。这样最容易导致"面条代码"(死循环)。

7.2.3 存储类型关键字(4个)

25、auto:声明自动变量 一般不使用

26、extern:声明变量是在其它文件正声明(也可以看做是引用变量)

extern 用在变量 或 函数返回值的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

27、register:声明寄存器变量

用 register 声明的变量是寄存器变量,是存放在CPU的寄存器里的。而我们平时声明的变量是存放在内存中的。虽说内存的速度已经很快了,不过跟寄存器比起来还是差得远。

寄存器变量和普通变量比起来速度上的差异很大,毕竟CPU的速度远远大于内存的速度。寄存器有这么两个特点,一个是运算速度快,一个是不能取地址。

28、static:声明静态变量 或 函数

static关键字可以用在以下情况下:

  • 静态全局变量

当使用static关键字声明全局变量时,称为静态全局变量。它在程序的顶部声明,并且其可见性在整个程序中。

  • 静态局部变量

当使用static关键字声明局部变量时,称为静态局部变量。静态局部变量的内存在整个程序中有效,但是变量的可见范围与自动局部变量相同。然而,当函数在第一次调用时修改了静态局部变量的值,那么在下一次函数调用时,这个修改后的值也将可用。

  • 静态函数

C语言中的静态函数是只能在定义它的文件内部访问的函数。它具有有限的作用域,不能被其他源文件访问。静态函数在函数声明中使用static关键字在返回类型之前声明。

1
2
3
4
static 返回类型 函数名(参数)
{
// 函数体
}

此处 static 的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。

Tips: 静态变量和全局变量的区别

  • 作用域:全局变量在整个程序中都可以访问,包括其它源文件,而静态变量仅限于定义它们的源文件,不能被其它源文件访问。
  • 可访问性:全局变量可以在程序外部访问,而静态变量不能在定义它们的文件之外访问。
  • 内存分配:全局变量在程序开始时分配内存,并在程序结束时销毁,而静态变量根据不同的作用域有不同的分配内存方式(时间),并在函数调用之间保持其值不变。

Tips: 静态局部变量和静态全局变量的区别

  • 作用域:静态局部变量与自动局部变量具有相同的作用域,限于定义它们的代码块内部,而静态全局变量具有文件作用域。
  • 内存分配:静态局部变量在函数调用时分配内存,并在函数调用之间保持其值不变,而静态全局变量在程序开始时分配内存,并在整个程序执行期间存在。
  • 可访问性:静态局部变量不能在定义它们的函数之外访问,而静态全局变量可以在同一文件的其它函数中访问。

Tips: 静态变量的存储方式与全局变量一样,都是静态存储方式。静态变量属于静态存储方式,属于静态存储方式的变量却不一定就是静态变量。

Tips:

  • 存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
  • 在静态数据区,内存中所有的字节默认值都是初始化(并且只在程序启动时初始化一次)为 0x00

7.2.4 其它关键字(4个)

29、const:声明只读变量

const 是 constant 的缩写,意思是“恒定不变的”!它是定义只读变量的关键字,或者说 const 是定义常变量的关键字。

30、sizeof:计算数据类型长度

sizeof 的作用就是返回一个对象或者类型所占的内存字节数。返回值类型为 size_t ,在头文件stddef.h 中定义

31、typedef:用以给数据类型取别名

在C语言中,除系统定义的标准类型和用户自定义的结构体、共用体等类型之外,还可以使用类型说明语句 typedef 定义新的类型来代替已有的类型。

32、volatile:说明变量在程序执行中可被隐含地改变

volatile 是一个类型修饰符(type specifier),就像我们熟悉的 const 一样,它是被设计用来修饰被不同线程访问和修改的变量; volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

7.3.复杂用法关键字详解

7.3.1 sizeof 的魔法

sizeof 用于计算数据类型 和 变量 占用的内存字节长度

sizeof 的作用就是返回一个 变量 或 类型 所占的内存字节数。返回值类型为 size_t ,在头文件stddef.h 中定义

sizeof 操作对象 示例 sizeof操作结果
类型名 sizeof(int) int类型变量所占内存空间字节数,32位、64位机器其值通常都为 4
变量名 int i; sizeof(i); int类型变量i所占内存空间字节数, 与 sizeof(int) 相等, 32位、64位机器其值通常都为 4
类型指针名 sizeof(int*) int * 类型变量所占内存空间字节数,一个机器字长, 32位机器其值是 4, 64位机器其值是 8
指针类型变量名 int *pr = NULL; sizeof(pr); int * 类型指针变量 pr 变量所占内存空间字节数,一个机器字长,32位机器其值是 4, 64位机器其值是 8
数组名 int arr[10]; sizeof(arr); 数组占用的内存字节总数,数组长度 10 乘以数组元素类型(int)的字节数, 32位、64位机器其值通常都为 10 * 4 = 40
函数名 sizeof(printf) 1

Tips:

  • 指针变量保存的是内存地址, 不管指针变量指向任何类型数据变量(基础类型变量、数组、结构体变量等) 或 函数,其占用的内存空间都是一个机器字长;
  • 结构体类型和结构体类型变量 可能存在内存对齐的情况,这会导致sizeof 计算的占用内存空间大小超过所有成员类型长度总和;

7.3.2 static 的妙用

static 关键字用于修饰的 函数变量,具有限制 函数/全局变 量作用域 和 局部变量生命周期 两个功能:

  • static 关键字修饰的 函数全局变量 只在当前文件可见,不再具有 全局可见性;
  • static 关键字修饰的 局部变量 可见性不变,但变量的生命周期变为与全局变量相同(static 局部变量在 data段的 分配内存空间);
  • static 关键字修饰的变量 默认情况下都初始化 0 值;

Tips: 所有未加 static 关键字修饰的 函数全局变量 都具有 全局可见性,其它的源文件也能访问。

1) 限制 函数/全局变 量作用域(隐藏)

利用 static 修饰的 函数全局变量 只在当前文件可见这一特性,可以在不同的文件中定义 同名函数 和 同名变量,而不必担心命名冲突。static 用作 函数 和 全局变量 的前缀修饰符, 起到了隐藏的作用。 static 修饰的局部变量(函数内的变量)随着所在函数的被调用而 显现 、随着所在函数的退出而 隐藏

2)保持局部变量的持久性

利用 static 修饰的局部变量(函数内的变量)在程序运行结束之前始终存在(static 变量在data 段分配内存空间), 变量随着所在函数的被调用而 显现 、随着所在函数的退出而 隐藏

Tips:隐藏static 变量的作用域只是C语言语法上的规定,通过指针(定义static 变量的 函数内部 指向static变量 ,或函数返回 static 变量地址)在函数外同样可以修改访问(获取或修改)static 变量, 这和const 常量类似。

3)变量默认初始化为 0

利用 static 修饰的变量(所有变量)在没有明确进行初始化时 编译器 默认将变量初始化为 0 值。

Licensed under CC BY-NC-SA 4.0
最后更新于 2023-03-03 22:32 CST