《C语言编程魔法书:基于C11标准》——第一篇 预备知识篇 第1章 C魔法概览1.1 例说编程语言

简介:

本节书摘来自华章计算机《C语言编程魔法书:基于C11标准》一书中的第1章,第1.1节,作者: 陈轶 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

第一篇 预备知识篇

第1章 C魔法概览

本章内容主要对C编程语言(以下简称C语言)进行大体介绍,包括它的历史以及C语言标准的演化进程。然后介绍一下C语言编程思想,当前主流C语言编译器以及GNU语法扩展。最后简单介绍一下从用C语言编写程序到编译、构建一个可执行程序的大致过程。
计算机编程语言从对计算机硬件底层的抽象程度进行分类,可分为:机器语言、汇编语言以及高级语言。下面由底层到高层分别介绍这几种类别的编程语言。

1.1 例说编程语言

1)机器语言是直接通过十六进制数表示当前处理器架构的机器指令码。指令码包含了当前指令的功能(比如算术逻辑运算、移位、分支、中断、I/O等)、寄存器、立即数等多种元素。每种处理器架构所对应的机器码的字节长度也各不相同,有些是固定长度的(比如ARM、MIPS等架构),有些是可变长度的(比如x86架构)。
2)汇编语言(Assembly Language)通过简单的指令助记符(memonics)来表示对应机器指令的功能、寄存器编号、立即数(immediates)等元素。汇编语言是对机器指令的简单抽象,通过汇编器(assembler)可以将汇编语句翻译成对应的机器指令码。
3)高级语言的表达形式更为抽象且贴近我们日常的语言表述。而且,高级语言比起汇编语言往往更具有表达力,且拥有更加丰富的语法特性,以便将程序进行结构化和模块化。比如,高级语言具有自定义变量标识符、自定义数据结构、分支与循环、更形象自然的表达式等。高级语言一般通过编译器(compiler)可直接将表达式翻译为对应的机器指令码;也可以将高级语言先翻译为中间语言(类似于汇编,但可能比汇编适用范围更广、更利于跨平台的字节码),最后将中间语言翻译为最终的机器指令码。
当然,有些书中还介绍了第四代语言,它基于高级语言,比高级语言更抽象,只需要一些简单的描述语句就能让计算机做比较复杂的工作。比如SQL(结构化查询语言,用于数据库查询)算是一种第四代语言。
下面,为了能让大家对这三种层次的编程语言有一个感性的认识,这里将列举ARMv8架构处理器下的机器语言、汇编语言,加上它们相应的C语言。读者如果手头有Xcode,并且有包含Apple A7或更高版本处理器的iOS设备的话,可以直接编译运行,并能看到最终效果。
下面首先列出一个文件名为my_sub.s的汇编源文件,其中包含了机器语言和汇编语言。见代码清单1-1:

代码清单1-1 机器语言与汇编语言
.text
.align 4

#ifdef __arm64__

.globl _my_sub_machine
.globl _my_sub_assembly

// 用机器语言实现减法操作
_my_sub_machine:

    .long 0x4b010000

    .long 0xd65f03c0

// 用汇编语言实现减法操作
_my_sub_assembly:

    sub w0, w0, w1

    ret

#endif

在代码清单1-1中,_my_sub_machine程序片段中的两条.long语句即为机器指令。这两条机器指令正好与_my_sub_assembly中的两条汇编指令相对应。也就是说,“0x4b010000”这串32位的十六进制代码意思就是“sub w0, w0, w1”,表示将寄存器w0与寄存器w1的值进行相减,然后将结果写回w0寄存器中。而“0xd65f03c0”指令码对应于“ret”(更确切地说是ret x30),表示返回当前过程(procedure)。在汇编语言中,一般会使用过程或者例程(routine)来表示一个可执行的程序片段。在C语言中一般都用函数(function)表示。我们在这里能够明显看到,汇编语言采用指令助记符的方式比写机器指令码要直观得多,而且也不容易出错。“sub”指令的功能从助记符上就能知道是“减法”功能;而w0、w1也明确指明了使用的寄存器是w0和w1。这些在“0x4b010000”这种机器指令码上都无法直观地表现出来。
代码清单1-2列出C语言是如何表达一个减法操作的。

代码清单1-2 减法操作对应的C语言
static int my_sub_c(int a, int b)
{
    return a - b;
}

代码清单1-2所列出的C语言代码与代码清单1-1中的机器指令码和汇编语言完全对应,意思一目了然——将参数变量a的值与参数变量b的值进行相减,然后将结果返回。从这里我们就能看到机器语言、汇编语言以及以C语言为代表的高级语言之间在表达力上的差距了。高级语言的目的就是为了给程序员提供更良好的编程工具,更简洁、更富有表达力的语言,使得我们程序员能提升生产力,并且能构思出更多精彩炫酷的应用,而不是把太多的精力都投入在如何让计算机执行的细节上。
代码清单1-3能让我们在主函数或其他函数中测试上述已经编写好的函数。

代码清单1-3 展示减法操作的结果
#ifdef __arm64__

extern int my_sub_machine(int a, int b);
extern int my_sub_assembly(int a, int b);

int result_machine = my_sub_machine(10, 2);
int result_assembly = my_sub_assembly(5, 3);
int result_c = my_sub_c(6, 2);

printf("Three results: %d, %d, %d\n", result_machine, result_assembly, result_c);

#endif

执行了上述代码之后,我们最后能在控制台看到输出结果:“Three results: 8, 2, 4”。可见,上述三种不同的编程语言,计算功能是完全一致的,都是对两个输入参数做减法操作,然后返回差值。然而就可读性、可理解性以及编程便利性而言,显然C语言比起其他两者要强得多。而可读性最差的无疑就是机器指令码了。
1.C语言的类别与产生
对于高级语言来说,从表达上又可分为命令式编程语言(imperative programming language)和陈述型编程语言(declarative programming language)。命令式语言主要包括过程式(procedural)、结构化(structured)以及面向对象(object-oriented)的编程语言;陈述型编程语言主要包括函数式(functional)以及逻辑型(logical)编程语言。而C语言则属于结构化的命令式编程语言。不过现在很多命令式编程语言也包含了一些函数式编程语言的特征。在本书中,后面第18章中谈到的Blocks语法就是一个很典型的函数式编程语言的语法。
C语言最初由Dennis Ritchie于1969年到1973年在AT&T贝尔实验室里开发出来,主要用于重新实现Unix操作系统。此时,C语言又被称为K&R C。其中,K表示Kernighan的首字母,而R则是Ritchie的首字母。K&R C语言与后来标准化的C语言有很大差异。比如,如果函数返回类型为int,则int可省:int my_function() { },也可以写成my_function(){ }。编译器不会有任何警告,更不会报错。另外,还有现在看来比较奇葩的函数定义,像我们现在定义这么一个函数——void my_function(int a, char p){ },如果是用K&R C语法定义的话要写成:void my_function(a, p) int a; char p; { }。K&R的C语法中,定义一个函数时,其形参列表先列出形参的标识符,然后在函数声明的后面紧跟着对形参标识符的完整声明,最后是函数体。这在现行标准中已经被逐步废弃使用了。另外,当时的第一本C语言专业书《The C Programming Language》也并非一个正式的编程语言规范,但被用了许多年。
2.C90标准
由于C语言被各大公司所使用(包括当时处于鼎盛时期的IBM PC),因此到了1989年,C语言由美国国家标准协会(ANSI)进行了标准化,此时C语言又被称为ANSI C。而仅过一年,ANSI C就被国际标准化组织ISO给采纳了。此时,C语言在ISO中有了一个官方名称——ISO/IEC 9899:1990。其中,9899是C语言在ISO标准中的代号,像C++在ISO标准中的代号是14882。而冒号后面的1990表示当前修订好的版本是在1990年发布的。对于ISO/IEC 9899:1990的俗称或简称,有些地方称为C89,有些地方称为C90,或者C89/90。不管怎么称呼,它们都指代这个最初的C语言国际标准。这个版本的C语言标准作为K&R C的一个超集(即K&R C是此标准C的一个子集),把后来引入的许多非官方特性也一起整合了进去。其中包括了从C++借鉴的函数原型(Function Prototypes),指向void的指针,对国际字符集以及本地语言环境的支持。在此标准中,尽管已经将函数定义的方式改为现在我们常用的那种方式,不过K&R的语法形式仍然兼容。
3.C99标准
在随后的几年里,C语言的标准化委员会又不断地对C语言进行改进,到了1999年,正式发布了ISO/IEC 9899:1999,简称为C99标准。C99标准引入了许多特性,包括内联函数(inline functions)、可变长度的数组、灵活的数组成员(用于结构体)、复合字面量、指定成员的初始化器、对IEEE754浮点数的改进、支持不定参数个数的宏定义,在数据类型上还增加了long long int以及复数类型。毫不夸张地说,即便到目前为止,很少有C语言编译器是完整支持C99的。像主流的GCC以及Clang编译器都能支持高达90%以上,而微软的Visual Studio 2015中的C编译器只能支持到70%左右。
4.C11标准
2007年,C语言标准委员会又重新开始修订C语言,到了2011年正式发布了ISO/IEC 9899:2011,简称为C11标准。C11标准新引入的特征尽管没C99相对C90引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制(generic selection)、对多线程的支持、静态断言、原子操作以及对Unicode的支持。本书将主要针对C11标准为大家详细讲解C编程语言
笔者近两年也是在不断地了解C语言标准委员会的最新动态,其中看到有人提出想为C语言添加面向对象的特性,包括增加类、继承、多态等已被C++语言所广泛使用的语法特性,但是最终被委员会驳回了。因为这些复杂的语法特性并不符合C语言的设计理念以及设计哲学,况且C++已经有了这些特性,C语言无需再对它们进行支持。笔者将在第19章给大家谈谈C语言设计理念与发展方向。

相关文章
|
2月前
|
Unix 编译器 Shell
[oeasy]python0033_先有操作系统还是先有编程语言_c语言是怎么来的
本文回顾了计算机语言与操作系统的起源,探讨了早期 Unix 操作系统及其与 C 语言的相互促进发展。Unix 最初用汇编语言编写,运行在 PDP-7 上,后来 Thompson 和 Ritchie 开发了 C 语言及编译器,使 Unix 重写并成功编译。1974 年 Ritchie 发表论文,Unix 开始被学术界关注,并逐渐普及。伯克利分校也在此过程中发挥了重要作用,推动了 Unix 和 C 语言的广泛传播。
60 9
[oeasy]python0033_先有操作系统还是先有编程语言_c语言是怎么来的
|
1月前
|
NoSQL C语言 索引
十二个C语言新手编程时常犯的错误及解决方式
C语言初学者常遇错误包括语法错误、未初始化变量、数组越界、指针错误、函数声明与定义不匹配、忘记包含头文件、格式化字符串错误、忘记返回值、内存泄漏、逻辑错误、字符串未正确终止及递归无退出条件。解决方法涉及仔细检查代码、初始化变量、确保索引有效、正确使用指针与格式化字符串、包含必要头文件、使用调试工具跟踪逻辑、避免内存泄漏及确保递归有基准情况。利用调试器、编写注释及查阅资料也有助于提高编程效率。避免这些错误可使代码更稳定、高效。
162 12
|
2月前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
2月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
2月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
2月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
27天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
31 3
|
18天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
32 10