哦!这该死的 C 语言!

点击蓝色“程序员cxuan ”关注我哟程序员

加个“星标”,欢迎来撩web


这是程序员cxuan的第 37 期分享


前言

C 语言是一门抽象的面向过程的语言,C 语言普遍应用于底层开发,C 语言在计算机体系中占据着不可替代的做用,能够说 C 语言是编程的基础,也就是说,无论你学习任何语言,都应该把 C 语言放在首先要学的位置上。下面这张图更好的说明 C 语言的重要性面试

能够看到,C 语言是一种底层语言,是一种系统层级的语言,操做系统就是使用 C 语言来编写的,好比 Windows、Linux、UNIX 。若是说其余语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。shell

C 语言特性

那么,既然 C 语言这么重要,它有什么值得咱们去学的地方呢?咱们不该该只由于它重要而去学,咱们更在乎的是学完咱们能学会什么,能让咱们得到什么。编程

C 语言的设计

C 语言是 1972 年,由贝尔实验室的丹尼斯·里奇(Dennis Ritch)肯·汤普逊(Ken Thompson)在开发 UNIX 操做系统时设计了C语言。C 语言是一门流行的语言,它把计算机科学理论和工程实践理论完美的融合在一块儿,使用户可以完成模块化的编程和设计。数组

计算机科学理论:简称 CS、是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。xcode

C 语言具备高效性

C 语言是一门高效性语言,它被设计用来充分发挥计算机的优点,所以 C 语言程序运行速度很快,C 语言可以合理了使用内存来得到最大的运行速度缓存

C 语言具备可移植性

C 语言是一门具备可移植性的语言,这就意味着,对于在一台计算机上编写的 C 语言程序能够在另外一台计算机上轻松地运行,从而极大的减小了程序移植的工做量。安全

C 语言特色

  • C 语言是一门简洁的语言,由于 C 语言设计更加靠近底层,所以不须要众多 Java 、C# 等高级语言才有的特性,程序的编写要求不是很严格。
  • C 语言具备结构化控制语句,C 语言是一门结构化的语言,它提供的控制语句具备结构化特征,如 for 循环、if⋯ else 判断语句和 switch 语句等。
  • C 语言具备丰富的数据类型,不只包含有传统的 字符型、整型、浮点型、数组类型等数据类型,还具备其余编程语言所不具有的数据类型,好比指针。
  • C 语言可以直接对内存地址进行读写,所以能够实现汇编语言的主要功能,并可直接操做硬件。
  • C 语言速度快,生成的目标代码执行效率高。

下面让咱们经过一个简单的示例来讲明一下 C 语言微信

入门级 C 语言程序

下面咱们来看一个很简单的 C 语言程序,我是 mac 电脑,因此我使用的是 xcode 进行开发,我以为工具无所谓你们用着顺手就行。

第一个 C 语言程序

#include <stdio.h>

int main(int argc, const char * argv[]) {
    printf("Hello, World!\n");
    
    printf("My Name is cxuan \n");
    
    return 0;
}

你可能不知道这段代码是什么意思,不过别着急,咱们先运行一下看看结果。

这段程序输出了 Hello,World! 和 My Name is cxuan,最后一行是程序的执行结果,表示这段程序是否有错误。下面咱们解释一下各行代码的含义。

首先,第一行的 #include <stdio.h>, 这行代码包含另外一个文件,这一行告诉编译器把 stdio.h 的内容包含在当前程序中。stdio.h 是 C 编译器软件包的标准部分,它可以提供键盘输入和显示器输出。

什么是 C 标准软件包?C 是由 Dennis M 在1972年开发的通用,过程性,命令式计算机编程语言。C标准库是一组 C 语言内置函数,常量和头文件,例如<stdio.h>,<stdlib.h>,<math.h>等。此库将用做 C 程序员的参考手册。

咱们后面会介绍 stdio.h ,如今你知道它是什么就好。

在 stdio.h 下面一行代码就是 main 函数。

C 程序可以包含一个或多个函数,函数是 C 语言的根本,就和方法是 Java 的基本构成同样。main() 表示一个函数名,int 表示的是 main 函数返回一个整数。void 代表 main() 不带任何参数。这些咱们后面也会详细说明,只须要记住 int 和 void 是标准 ANSI C 定义 main() 的一部分(若是使用 ANSI C 以前的编译器,请忽略 void)。

而后是 /*一个简单的 C 语言程序*/ 表示的是注释,注释使用 /**/ 来表示,注释的内容在两个符号之间。这些符号可以提升程序的可读性。

注意:注释只是为了帮助程序员理解代码的含义,编译器会忽略注释

下面就是 { ,这是左花括号,它表示的是函数体的开始,而最后的右花括号 } 表示函数体的结束。{ } 中间是书写代码的地方,也叫作代码块。

int number 表示的是将会使用一个名为 number 的变量,并且 number 是 int 整数类型。

number = 11 表示的是把值 11 赋值给 number 的变量。

printf(Hello,world!\n); 表示调用一个函数,这个语句使用 printf() 函数,在屏幕上显示 Hello,world , printf() 函数是 C 标准库函数中的一种,它可以把程序运行的结果输出到显示器上。而代码 \n 表示的是 换行,也就是另起一行,把光标移到下一行。

而后接下来的一行 printf() 和上面一行是同样的,咱们就很少说了。最后一行 printf() 有点意思,你会发现有一个 %d 的语法,它的意思表示的是使用整形输出字符串。

代码块的最后一行是 return 0,它能够当作是 main 函数的结束,最后一行是代码块 } ,它表示的是程序的结束。

好了,咱们如今写完了第一个 C 语言程序,有没有对 C 有了更深的认识呢?确定没有。。。这才哪到哪,继续学习吧。

如今,咱们能够概括为 C 语言程序的几个组成要素,以下图所示

C 语言执行流程

C 语言程序成为高级语言的缘由是它可以读取并理解人们的思想。然而,为了可以在系统中运行 hello.c 程序,则各个 C 语句必须由其余程序转换为一系列低级机器语言指令。这些指令被打包做为可执行对象程序,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。

在 UNIX 系统中,从源文件到对象文件的转换是由编译器执行完成的。

gcc -o hello hello.c

gcc 编译器驱动从源文件读取 hello.c ,并把它翻译成一个可执行文件 hello。这个翻译过程可用以下图来表示

这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:预处理器、编译器、汇编器、链接器,下面咱们逐个击破。

  • 预处理阶段(Preprocessing phase),预处理器会根据开始的 # 字符,修改源 C 程序。#include <stdio.h> 命令就会告诉预处理器去读系统头文件 stdio.h 中的内容,并把它插入到程序做为文本。而后就获得了另一个 C 程序hello.i,这个程序一般是以 .i为结尾。

  • 而后是 编译阶段(Compilation phase),编译器会把文本文件 hello.i 翻译成文本hello.s,它包括一段汇编语言程序(assembly-language program)

  • 编译完成以后是汇编阶段(Assembly phase),这一步,汇编器 as会把 hello.s 翻译成机器指令,把这些指令打包成可重定位的二进制程序(relocatable object program)放在 hello.c 文件中。它包含的 17 个字节是函数 main 的指令编码,若是咱们在文本编辑器中打开 hello.o 将会看到一堆乱码。

  • 最后一个是连接阶段(Linking phase),咱们的 hello 程序会调用 printf 函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫作 printf.o文件中,它是一个单独的预编译好的目标文件,而这个文件必需要和咱们的 hello.o 进行连接,链接器(ld) 会处理这个合并操做。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。

你须要理解编译系统作了什么

对于上面这种简单的 hello 程序来讲,咱们能够依赖编译系统(compilation system)来提供一个正确和有效的机器代码。然而,对于咱们上面讲的程序员来讲,编译器有几大特征你须要知道

  • 优化程序性能(Optimizing program performance),现代编译器是一种高效的用来生成良好代码的工具。对于程序员来讲,你无需为了编写高质量的代码而去理解编译器内部作了什么工做。然而,为了编写出高效的 C 语言程序,咱们须要了解一些基本的机器码以及编译器将不一样的 C 语句转化为机器代码的过程。
  • 理解连接时出现的错误(Understanding link-time errors),在咱们的经验中,一些很是复杂的错误大可能是由连接阶段引发的,特别是当你想要构建大型软件项目时。
  • 避免安全漏洞(Avoiding security holes),近些年来, 缓冲区溢出(buffer overflow vulnerabilities)是形成网络和 Internet 服务的罪魁祸首,因此咱们有必要去规避这种问题。

系统硬件组成

为了理解 hello 程序在运行时发生了什么,咱们须要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,咱们来对其进行解释

  • 总线(Buses):在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。一般总线被设计成传送定长的字节块,也就是  字(word)。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。如今大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。

  • I/O 设备(I/O Devices):Input/Output 设备是系统和外部世界的链接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。

    每一个I/O 设备链接 I/O 总线都被称为控制器(controller) 或者是 适配器(Adapter)。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备自己或者系统的主印制板电路(一般称做主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。不管组织形式如何,它们的最终目的都是彼此交换信息。

  • 主存(Main Memory),主存是一个临时存储设备,而不是永久性存储,磁盘是 永久性存储 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory) 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它惟一的地址编号,从 0 开始。通常来讲,组成程序的每条机器指令都由不一样数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。好比,在 Linux 的 x86-64 机器上,short 类型的数据须要 2 个字节,int 和 float 须要 4 个字节,而 long 和 double 须要 8 个字节。

  • 处理器(Processor)CPU(central processing unit)  或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任什么时候刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。

    从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操做。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操做,而后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(好比 jmp 指令就不会顺序读取)

    下面是 CPU 可能执行简单操做的几个步骤

  • 加载(Load):从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容

  • 存储(Store):将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容

  • 操做(Operate):把两个寄存器的内容复制到 ALU(Arithmetic logic unit)。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。

算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。

  • 跳转(jump):从指令中抽取一个字,把这个字复制到 程序计数器(PC) 中,覆盖原来的值

剖析 hello 程序的执行过程

前面咱们简单的介绍了一下计算机的硬件的组成和操做,如今咱们正式介绍运行示例程序时发生了什么,咱们会从宏观的角度进行描述,不会涉及到全部的技术细节

刚开始时,shell 程序执行它的指令,等待用户键入一个命令。当咱们在键盘上输入了 ./hello 这几个字符时,shell 程序将字符逐一读入寄存器,再把它放到内存中,以下图所示

当咱们在键盘上敲击回车键的时候,shell 程序就知道咱们已经结束了命令的输入。而后 shell 执行一系列指令来加载可执行的 hello 文件,这些指令将目标文件中的代码和数据从磁盘复制到主存。

利用 DMA(Direct Memory Access) 技术能够直接将磁盘中的数据复制到内存中,以下

一旦目标文件中 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 hello,world\n 字符串中的字节从主存复制到寄存器文件,再从寄存器中复制到显示设备,最终显示在屏幕上。以下所示

高速缓存是关键

上面咱们介绍完了一个 hello 程序的执行过程,系统花费了大量时间把信息从一个地方搬运到另一个地方。hello 程序的机器指令最初存储在磁盘上。当程序加载后,它们会拷贝到主存中。当 CPU 开始运行时,指令又从内存复制到 CPU 中。一样的,字符串数据 hello,world \n 最初也是在磁盘上,它被复制到内存中,而后再到显示器设备输出。从程序员的角度来看,这种复制大部分是开销,这减慢了程序的工做效率。所以,对于系统设计来讲,最主要的一个工做是让程序运行的愈来愈快。

因为物理定律,较大的存储设备要比较小的存储设备慢。而因为寄存器和内存的处理效率在愈来愈大,因此针对这种差别,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory, 简称为 cache 高速缓存),做为暂时的集结区域,存放近期可能会须要的信息。以下图所示

图中咱们标出了高速缓存的位置,位于高速缓存中的 L1高速缓存容量能够达到数万字节,访问速度几乎和访问寄存器文件同样快。容量更大的 L2 高速缓存经过一条特殊的总线连接 CPU,虽然 L2 缓存比 L1 缓存慢 5 倍,可是仍比内存要快 5 - 10 倍。L1 和 L2 是使用一种静态随机访问存储器(SRAM) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L一、L2 和 L3。系统能够得到一个很大的存储器,同时访问速度也更快,缘由是利用了高速缓存的 局部性原理。

Again:入门程序细节

如今,咱们来探讨一下入门级程序的细节,由浅入深的来了解一下 C 语言的特性。

#include<stdio.h>

咱们上面说到,#include<stdio.h> 是程序编译以前要处理的内容,称为编译预处理命令。

预处理命令是在编译以前进行处理。预处理程序通常以 # 号开头。

全部的 C 编译器软件包都提供 stdio.h 文件。该文件包含了给编译器使用的输入和输出函数,好比 println() 信息。该文件名的含义是标准输入/输出 头文件。一般,在 C 程序顶部的信息集合被称为 头文件(header)

C 的第一个标准是由 ANSI 发布的。虽然这份文档后来被国际标准化组织(ISO)采纳而且 ISO 发布的修订版也被 ANSI 采纳了,但名称 ANSI C(而不是 ISO C) 仍被普遍使用。一些软件开发者使用ISO C,还有一些使用 Standard C

C 标准库

除了 <sdtio.h> 外,C 标准库还包括下面这些头文件

<assert.h>

提供了一个名为 assert 的关键字,它用于验证程序做出的假设,并在假设为假输出诊断消息。

<ctype.h>

C 标准库的 ctype.h 头文件提供了一些函数,能够用于测试和映射字符。

这些字符接受 int 做为参数,它的值必须是 EOF 或者是一个无符号字符

EOF是一个计算机术语,为 End Of File 的缩写,在操做系统中表示资料源无更多的资料可读取。资料源一般称为档案或串流。一般在文本的最后存在此字符表示资料结束。

C 标准库的 errno.h 头文件定义了整数变量 errno,它是经过系统调用设置的,这些库函数代表了什么发生了错误。

C 标准库的 float.h 头文件包含了一组与浮点值相关的依赖于平台的常量。

limits.h 头文件决定了各类变量类型的各类属性。定义在该头文件中的宏限制了各类变量类型(好比 char、int 和 long)的值。

locale.h 头文件定义了特定地域的设置,好比日期格式和货币符号

math.h 头文件定义了各类数学函数和一个宏。在这个库中全部可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。

setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则。

signal.h 头文件定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不一样信号。

stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。

stddef .h 头文件定义了各类变量类型和宏。这些定义中的大部分也出如今其它头文件中。

stdlib .h 头文件定义了四个变量类型、一些宏和各类通用工具函数。

string .h 头文件定义了一个变量类型、一个宏和各类操做字符数组的函数。

time.h 头文件定义了四个变量类型、两个宏和各类操做日期和时间的函数。

main() 函数

main 函数听起来像是调皮捣蛋的孩子故意给方法名起一个 主要的 方法,来告诉他人他才是这个世界的中心。但事实却不是这样,而 main() 方法确实是世界的中心。

C 语言程序必定从 main() 函数开始执行,除了 main() 函数外,你能够随意命名其余函数。一般,main 后面的 () 中表示一些传入信息,咱们上面的那个例子中没有传递信息,由于圆括号中的输入是 void 。

除了上面那种写法外,还有两种 main 方法的表示方式,一种是 void main(){} ,一种是 int main(int argc, char* argv[]) {}

  • void main() 声明了一个带有不肯定参数的构造方法
  • int main(int argc, char* argv[]) {} 其中的 argc 是一个非负值,表示从运行程序的环境传递到程序的参数数量。它是指向 argc + 1 指针数组的第一个元素的指针,其中最后一个为null,而前一个(若是有的话)指向表示从主机环境传递给程序的参数的字符串。若是argv [0]不是空指针(或者等效地,若是argc> 0),则指向表示程序名称的字符串,若是在主机环境中没法使用程序名称,则该字符串为空。

注释

在程序中,使用 /**/ 的表示注释,注释对于程序来讲没有什么实际用处,可是对程序员来讲却很是有用,它可以帮助咱们理解程序,也可以让他人看懂你写的程序,咱们在开发工做中,都很是反感不写注释的人,因而可知注释很是重要。

C 语言注释的好处是,它能够放在任意地方,甚至代码在同一行也不要紧。较长的注释能够多行表示,咱们使用 /**/ 表示多行注释,而 // 只表示的是单行注释。下面是几种注释的表示形式

// 这是一个单行注释

/* 多行注释用一行表示 */

/*
  多行注释用多行表示
    多行注释用多行表示
      多行注释用多行表示
        多行注释用多行表示

*/

函数体

在头文件、main 方法后面的就是函数体(注释通常不算),函数体就是函数的执行体,是你编写大量代码的地方。

变量声明

在咱们入门级的代码中,咱们声明了一个名为 number 的变量,它的类型是 int,这行代码叫作 声明,声明是 C 语言最重要的特性之一。这个声明完成了两件事情:定义了一个名为 number 的变量,定义 number 的具体类型。

int 是 C 语言的一个 关键字(keyword),表示一种基本的 C 语言数据类型。关键字是用于语言定义的。不能使用关键字做为变量进行定义。

示例中的 number 是一个 标识符(identifier),也就是一个变量、函数或者其余实体的名称。

变量赋值

在入门例子程序中,咱们声明了一个 number 变量,并为其赋值为 11,赋值是 C 语言的基本操做之一。这行代码的意思就是把值 1 赋给变量 number。在执行 int number 时,编译器会在计算机内存中为变量 number 预留空间,而后在执行这行赋值表达式语句时,把值存储在以前预留的位置。能够给 number 赋不一样的值,这就是 number 之因此被称为 变量(variable) 的缘由。

printf 函数

在入门例子程序中,有三行 printf(),这是  C 语言的标准函数。圆括号中的内容是从 main 函数传递给 printf 函数的。参数分为两种:实际参数(actual argument) 和 形式参数(formal parameters)。咱们上面提到的 printf 函数括号中的内容,都是实参。

return 语句

在入门例子程序中,return 语句是最后一条语句。int main(void) 中的 int 代表 main() 函数应返回一个整数。有返回值的 C 函数要有 return 语句,没有返回值的程序也建议你们保留 return 关键字,这是一种好的习惯或者说统一的编码风格。

分号

在 C 语言中,每一行的结尾都要用 ; 进行结束,它表示一个语句的结束,若是忘记或者忽略分号会被编译器提示错误。

关键字

下面是 C 语言中的关键字,C 语言的关键字一共有 32 个,根据其做用不一样进行划分

数据类型关键字

数据类型的关键字主要有 12 个,分别是

  • char: 声明字符型变量或函数
  • double: 声明双精度变量或函数
  • float: 声明浮点型变量或函数
  • int : 声明整型变量或函数
  • long: 声明长整型变量或函数
  • short : 声明短整型变量或函数
  • signed : 声明有符号类型变量或函数
  • _Bool:  声明布尔类型
  • _Complex :声明复数
  • _Imaginary: 声明虚数
  • unsigned : 声明无符号类型变量或函数
  • void : 声明函数无返回值或无参数,声明无类型指针

控制语句关键字

控制语句循环的关键字也有 12 个,分别是

循环语句

  • for : for 循环,使用的最多
  • do :循环语句的前提条件循环体
  • while:循环语句的循环条件
  • break : 跳出当前循环
  • continue:结束当前循环,开始下一轮循环

条件语句

  • if:条件语句的判断条件
  • else : 条件语句的否认分支,与 if 连用
  • goto: 无条件跳转语句

开关语句

  • switch: 用于开关语句
  • case:开关语句的另一种分支
  • default : 开关语句中的其余分支

返回语句

retur:子程序返回语句(能够带参数,也看不带参数)

存储类型关键字

  • auto : 声明自动变量 通常不使用
  • extern : 声明变量是在其余文件正声明(也能够看作是引用变量)
  • register : 声明寄存器变量
  • static: 声明静态变量

其余关键字

  • const: 声明只读变量
  • sizeof : 计算数据类型长度
  • typedef: 用以给数据类型取别名
  • volatile : 说明变量在程序执行中可被隐含地改变


后记

这篇文章咱们先介绍了 C 语言的特性,C 语言为何这么火,C 语言的重要性,以后咱们以一道 C 语言的入门程序讲起,咱们讲了 C 语言的基本构成要素,C 语言在硬件上是如何运行的,C 语言的编译过程和执行过程等,在这以后咱们又加深讲解了一下入门例子程序的组成特征。

若是你以为这篇文章不错的的话,欢迎小伙伴们四连走起:点赞、在看、留言、分享。你的四连是我更文的动力。


往期精选

我工做三年了,该懂并发了(干货)

个人文章是最棒的狗粮

今天怼个人面试官居然和我在同一个群里...

2w字 + 40张图带你参透并发编程!

完了,这个硬件成精了,它居然绕过了 CPU......



本文分享自微信公众号 - 程序员cxuan(cxuangoodjob)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索