wenmo8 发布的文章

前言

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

图片

可以看到,C 语言是一种底层语言,是一种系统层级的语言,操作系统就是使用 C 语言来编写的,比如 Windows、Linux、UNIX 。如果说其他语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。

C 语言特性

那么,既然 C 语言这么重要,它有什么值得我们去学的地方呢?我们不应该只因为它重要而去学,我们更在意的是学完我们能学会什么,能让我们获得什么。

C 语言的设计

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

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

C 语言具有高效性

C 语言是一门高效性语言,它被设计用来充分发挥计算机的优势,因此 C 语言程序运行速度很快,C 语言能够合理了使用内存来获得最大的运行速度

C 语言具有可移植性

C 语言是一门具有可移植性的语言,这就意味着,对于在一台计算机上编写的 C 语言程序可以在另一台计算机上轻松地运行,从而极大的减少了程序移植的工作量。

C 语言特点

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

下面让我们通过一个简单的示例来说明一下 C 语言

入门级 C 语言程序

下面我们来看一个很简单的 C 语言程序,我觉得工具无所谓大家用着顺手就行。

第一个 C 语言程序

#include <stdio.h>

int main(int argc, const char * argv[]) {
    printf("Hello, World!\n");
  
   printf("my Name is cxuan \n")
    
    printf("number = %d \n", number);
    
    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) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、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 的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。

<errno.h>

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

<float.h>

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

<limits.h>

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

<locale.h>

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

<math.h>

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

<setjmp.h>

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

<signal.h>

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

<stdarg.h>

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

<stddef.h>

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

<stdlib.h>

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

<string.h>

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

<time.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 语言最基本的变量与常量。

变量和常量

变量和常量是程序处理的两种基本对象。

有些数据类型在程序使用之前就已经被设定好了,在整个过程中没有变化(这段话描述不准确,但是为了通俗易懂,暂且这么描述),这种数据被称为常量(constant)。另外一种数据类型在程序执行期间可能会发生改变,这种数据类型被称为 变量(variable)。例如 int number 就是一个变量,而3.1415 就是一个常量,因为 int number 一旦声明出来,你可以对其任意赋值,而 3.1415 一旦声明出来,就不会再改变。

变量名

有必要在聊数据类型之前先说一说变量名的概念。变量名是由字母和数字组成的序列,第一个字符必须是字母。在变量名的命名过程中,下划线 _ 被看作字母,下划线一般用于名称较长的变量名,这样能够提高程序的可读性。变量名通常不会以下划线来开头。在 C 中,大小写是有区别的,也就是说,a 和 A 完全是两个不同的变量。一般变量名使用小写字母,符号常量(#define 定义的)全都使用大写。选择变量名的时候,尽量能够从字面上描述出变量的用途,切忌起这种 abc 毫无意义的变量。

还需要注意一般局部变量都会使用较短的变量名,外部变量使用较长的名字。

数据类型

在了解数据类型之前,我们需要先了解一下这些概念 位、字节和字

位、字节和字都是对计算机存储单元的描述。在计算机世界中,最小的单元是 位(bit),一个位就表示一个 0 或 1,一般当你的小伙伴问你的电脑是 xxx 位,常见的有 32 位或者 64 位,这里的位就指的是比特,比特就是 bit 的中文名称,所以这里的 32 位或者 64 位指的就是 32 bit 或者 64 bit。字节是基本的存储单元,基本存储单元说的是在计算机中都是按照字节来存储的,一个字节等于 8 位,即 1 byte = 8 bit。字是自然存储单位,在现代计算机中,一个字等于 2 字节。

C 语言的数据类型有很多,下面我们就来依次介绍一下。

整型

C 语言中的整型用 int 来表示,可以是正整数、负整数或零。在不同位数的计算机中其取值范围也不同。不过在 32 位和 64 位计算机中,int 的取值范围是都是 2^32 ,也就是 -2147483648 ~ +2147483647,无符号类型的取值范围是 0 ~ 4294967295。

整型以二进制整数存储,分为有符号数和无符号数两种形态,有符号数可以存储正整数、负整数和零;无符号只能存储正整数和零。

可以使用 printf 打印出来 int 类型的值,如下代码所示。

#include <stdio.h> 
int main(){
 int a = -5;
 printf("%d\n",a);
 
 unsigned int b = 6;
 printf("%d\n",b);
 
}

C 语言还提供 3 个附属关键字修饰整数类型,即 short、long 和 unsigned

  • short int 类型(或者简写为 short)占用的存储空间可能比 int 类型少,适合用于数值较小的场景。
  • long int 或者 long 占用的存储空间可能比 int 类型多,适合用于数值较大的场景。
  • long long int 或者 long long(C99 加入)占用的存储空间比 long 多,适用于数值更大的场合,至少占用 64 位,与 int 类似,long long 也是有符号类型。
  • unsigned int 或 unsigned 只用于非负值的场景,这种类型的取值范围有所不同,比如 16 位的 unsigned int 表示的范围是 0 ~ 65535 ,而不是 -32768 ~ 32767。
  • 在 C90 标准中,添加了 unsigned long int 或者 unsigned long 和 unsigned short int 或 unsigned short 类型,在 C99 中又添加了 unsigned long long int 或者 unsigned long long 。
  • 在任何有符号类型前面加 signed ,可强调使用有符号类型的意图。比如 short、short int、signed short、signed short int 都表示一种类型。

比如上面这些描述可以用下面这些代码来声明:

long int lia;
long la;
long long lla;
short int sib;
short sb;
unsigned int uic;
unsigned uc;
unsigned long uld;
unsigned short usd;

这里需要注意一点,unsigned 定义的变量,按照 printf 格式化输出时,是能够显示负值的,为什么呢?不是 unsigned 修饰的值不能是负值啊,那是因为 unsigned 修饰的变量,在计算时会有用,输出没什么影响,这也是 cxuan 刚开始学习的时候踩的坑。

我们学过 Java 的同学刚开始都对这些定义觉得莫名其妙,为什么一个 C 语言要对数据类型有这么多定义?C 语言真麻烦,我不学了!

千万不要有这种想法,如果有这种想法的同学,你一定是被 JVM 保护的像个孩子!我必须从现在开始纠正你的这个想法,因为 Java 有 JVM 的保护,很多特性都做了优化,而 C 就像个没有伞的孩子,它必须自己和这个世界打交道!

上面在说 short int 和 long int 的时候,都加了一个可能,怎么,难道 short int 和 long int 和 int 还不一样吗?

这里就是 C 语言数据类型一个独特的风格。

为什么说可能,这是由于 C 语言为了适配不同的机器来设定的语法规则,在早起的计算机上,int 类型和 short 类型都占 16 位,long 类型占 32 位,在后来的计算机中,都采用了 16 位存储 short 类型,32 位存储 int 类型和 long 类型,现在,计算机普遍使用 64 位 CPU,为了存储 64 位整数,才引入了 long long 类型。所以,一般现在个人计算机上常见的设置是 long long 占用 64 位,long 占用 32 位,short 占用 16 位,int 占用 16 位或者 32 位。

char 类型

char 类型一般用于存储字符,表示方法如下

char a = 'x';
char b = 'y';

char 被称为字符类型,只能用单引号 '' 来表示,而不能用双引号 “” 来表示,这和字符串的表示形式相反。

char 虽然表示字符,但是 char 实际上存储的是整数而不是字符,计算机一般使用 ASCII 来处理字符,标准 ASCII 码的范围是 0 - 127 ,只需 7 位二进制数表示即可。C 语言中规定 char 占用 1 字节。

其实整型和字符型是相通的,他们在内存中的存储本质是相通的,编译器发现 char ,就会自动转换为整数存储,相反的,如果给 int 类型赋值英文字符,也会转换成整数存储,如下代码

#include <stdio.h>

int main(){
 char a = 'x';
 int b;
 b = 'y';
 
 printf("%d\n%d\n",a,b);
}

输出

120

121

所以,int 和 char 只是存储的范围不同,整型可以是 2 字节,4 字节,8 字节,而字符型只占 1 字节。

有些 C 编译器把 char 实现为有符号类型,这意味着 char 可表示的范围是 -128 ~ 127,而有些编译器把 char 实现为无符号类型,这种情况下 char 可表示的范围是 0 - 255。signed char 表示的是有符号类型,unsigned char 表示的是无符号类型。

_Bool 类型

_Bool 类型是 C99 新增的数据类型,用于表示布尔值。也就是逻辑值 true 和 false。在 C99 之前,都是用 int 中的 1 和 0 来表示。所以 _Bool 在某种程度上也是一种数据类型。表示 0 和 1 的话,用 1 bit(位)表示就够了。

float、double 和 long double

整型对于大多数软件开发项目而言就已经够用了。然而,在金融领域和数学领域还经常使用浮点数。C 语言中的浮点数有 float、double 和 long double 类型。浮点数类型能够表示包括小数在内更大范围的数。浮点数能表示小数,而且表示范围比较大。浮点数的表示类似于科学技术法。下面是一些科学记数法示例:

数字科学记数法指数记数法
10000000001 * 10^91.0e9
4560004.56 * 10^54.56e5
372.853.7285 * 10 ^ 23.7285e2
0.00252.5 * 10 ^ -32.5e-3

C 规定 float 类型必须至少能表示 6 位有效数字,而且取值范围至少是 10^-37 ~ 10^+37。通常情况下,系统存储一个浮点数要占用 32 位。

C 提供的另一种浮点类型是 double(双精度类型)。一般来说,double 占用的是 64 位而不是 32 位。

C 提供的第三种类型是 long double ,用于满足比 double 类型更高的精度要求。不过,C 只保证了 long double 类型至少与 double 类型相同。

浮点数的声明方式和整型类似,下面是一些浮点数的声明方式。

#include <stdio.h>

int main(){

 float aboat = 2100.0;
 double abet = 2.14e9;
 long double dip = 5.32e-5;
 
 printf("%f\n", aboat);
 printf("%e\n", abet);
 printf("%Lf\n", dip);
 
}

printf() 函数使用 %f 转换说明打印十进制计数法的 float 和 double 类型浮点数,用 %e 打印指数记数法的浮点数。打印 long double 类型要使用 %Lf 转换说明。

关于浮点数,还需要注意其上溢下溢的问题。

上溢指的是是指由于数字过大,超过当前类型所能表示的范围,如下所示

float toobig = 3.4E38 * 100.0f;
printf("%e\n",toobig);

输出的内容是 inf,这表示 toobig 的结果超过了其定义的范围,C 语言就会给 toobig 赋一个表示无穷大的特定值,而且 printf 显示值为 inf 或者 infinity 。

下溢:是指由于数值太小,低于当前类型所能表示的最小的值,计算机就只好把尾数位向右移,空出第一个二进制位,但是与此同时,却损失了原来末尾有效位上面的数字,这种情况就叫做下溢。比如下面这段代码

float toosmall = 0.1234e-38/10;
printf("%e\n", toosmall);

复数和虚数类型

许多科学和工程计算都需要用到复数和虚数,C99 标准支持复数类型和虚数类型,C 语言中有 3 种复数类型:float _Complex、double _Complex 和 long double  _Complex

C 语言提供的 3 种虚数类型:float _Imaginary、 double _Imaginary 和 long double _Imaginary

如果包含 complex.h 头文件的话,便可使用 complex 替换 _Complex,用 imaginary 替代 _Imaginary。

其他类型

除了上述我们介绍过的类型之外,C 语言中还有其他类型,比如数组、指针、结构和联合,虽然 C 语言没有字符串类型,但是 C 语言却能够很好的处理字符串。

常量

在很多情况下我们需要常量,在整个程序的执行过程中,其值不会发生改变,比如一天有 24 个小时,最大缓冲区的大小,滑动窗口的最大值等。这些固定的值,即称为常量,又可以叫做字面量

常量也分为很多种,整型常量,浮点型常量,字符常量,字符串常量,下面我们分别来介绍

整数常量

整数常量可以表示为十进制、八进制或十六进制。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。

330         /* 合法的 */
315u        /* 合法的 */
0xFeeL      /* 合法的 */
048         /* 非法的:8 进制不能定义 8 */

浮点型常量

浮点型常量由整数部分、小数点、小数部分和指数部分组成。你可以使用小数形式或者指数形式来表示浮点常量。

当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

3.14159       /* 合法的 */
314159E-5L    /* 合法的 */
510E          /* 非法的:不完整的指数 */
210f          /* 非法的:没有小数或指数 */

字符常量

C 语言中的字符常量使用单引号(即撇号)括起来的一个字符。如‘a’,‘x’,'D',‘?’,‘$’ 等都是字符常量。注意,‘a’ 和 ‘A’ 是不同的字符常量。

除了以上形式的字符常量外,C 还允许用一种特殊形式的字符常量,就是以一个 “\” 开头的字符序列。例如,前面已经遇到过的,在 printf 函数中的‘\n’,它代表一个换行符。这是一种控制字符,在屏幕上是不能显示的。

常用的以 “\” 开头的特殊字符有

图片

表中列出的字符称为“转义字符”,意思是将反斜杠(\)后面的字符转换成另外的意义。如 ‘\n’ 中的 “n” 不代表字母 n 而作为“换行”符。

表中最后第 2 行是用ASCII码(八进制数)表示一个字符,例如 ‘\101’ 代表 ASCII 码(十进制数)为 65 的字符 “A”。‘\012’(十进制 ASCII 码为 10)代表换行。

需要注意的是 ‘\0’ 或 ‘\000’ 代表 ASCII 码为 0 的控制字符,它用在字符串中。

字符串常量

字符串常量通常用 "" 进行表示。字符串就是一系列字符的集合。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

常量定义

C 语言中,有两种定义常量的方式。

  1. 使用 #define 预处理器进行预处理
  2. 使用 const 关键字进行处理

下面是使用 #define 预处理器进行常量定义的代码。

#include <stdio.h>

#define LENGTH 5
#define WIDTH 10

int main(){
 
 int area = LENGTH * WIDTH;
 
 printf("area = %d\n", area);
 
}

同样的,我们也可以使用 const 关键字来定义常量,如下代码所示

#include <stdio.h>

int main(){
 
 const int LENGTH = 10;
 const int WIDTH = 5;
 
 int area;
 area = LENGTH * WIDTH;
 
 printf("area = %d\n", area);
 
}

那么这两种常量定义方式有什么不同呢?

编译器处理方式不同

使用 #define 预处理器是在预处理阶段进行的,而 const 修饰的常量是在编译阶段进行。

类型定义和检查不同

使用 #define 不用声明数据类型,而且不用类型检查,仅仅是定义;而使用 const 需要声明具体的数据类型,在编译阶段会进行类型检查。


前言

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

图片

可以看到,C 语言是一种底层语言,是一种系统层级的语言,操作系统就是使用 C 语言来编写的,比如 Windows、Linux、UNIX 。如果说其他语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。

C 语言特性

那么,既然 C 语言这么重要,它有什么值得我们去学的地方呢?我们不应该只因为它重要而去学,我们更在意的是学完我们能学会什么,能让我们获得什么。

C 语言的设计

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

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

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) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、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 语言的编译过程和执行过程等,在这之后我们又加深讲解了一下入门例子程序的组成特征。

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


这篇文章是 C 语言系列第三篇,之前两篇见

哦!这该死的 C 语言!

C 语言基础,来喽!

下面我们来介绍一下 C 语言中一个非常重要的概念 - 函数 (function)。首先就要先给函数下一个定义,函数就是完成特定任务的独立代码单元,这也就是说,一个函数肯定是要为了完成某种功能的,比如一个函数它能够执行加法运算,比如一个函数能交换两个数的值,还有一些函数可能只是为了打印某些东西等等。

函数也可以把很多大的任务拆分成一个个小的任务,通过设计每个小的任务来完成一个大的功能。一个设计优良的函数能够把程序中不需要了解的细节隐藏起来,从而使整个程序结构更加清晰,降低程序的修改难度。

C 语言程序由许多小的函数组成,一个程序会被保存在多个源文件中,每个文件可以单独编译,并可以与库中已编译过的函数一起加载。

下面我们通过一个例子来讨论一下函数是如何创建并使用的。

函数创建以及使用

函数的创建和使用会分为三个步骤:

  • 函数原型 ( function type ):这个是创建函数定义,也叫函数声明,能够表明一个文件中有哪些函数。
  • 函数调用 ( function call ):调用函数的位置,函数被定义出来肯定是要使用它的,在哪里使用的这个函数就被称为函数调用。
  • 函数定义 ( function definition ):这个就是函数的具体要干的什么事儿,也就是函数的具体逻辑是什么。

这么一看,函数和变量简直一模一样了,函数需要原型、调用和定义,而变量也需要这些,只不过变量还可以把原型和定义一起表示。

#include <stdio.h>int num; // 变量原型 int sum(int,int); // 函数原型 int main(void){ num = 12; // 变量定义  int num2 = 11; // 函数原型 + 函数定义   int all = sum(num,num2); // 变量使用 ,函数使用  printf("all = %d",all);  return 0;} // 函数定义 int sum(int a,int b){  return a + b;}

上面这段代码很好的列举了变量的定义以及函数的定义。

我们首先定义了一个 num 变量,这个就是变量的原型,然后在 main 函数中使用这个变量,就是变量的定义和使用,当然变量也可以直接使用原型 + 定义的方式( 上面的 num2 ),sum 函数演示了函数的原型、定义和使用。这里注意一点,main 函数比较特殊,它是所有方法的入口,而且 main 函数无需定义原型就能直接使用。

上面这段代码被一起保存在一个文件中,当然你也可以把它们保存在不同的文件中,只不过把它们放在同一个文件中我们在演示的时候比较方便,还有一点就是能够一起进行编译,这两个函数也可以定义在不同的文件中,分别进行编译,这样的好处是使程序更加易于维护,代码读起来更加顺畅,事实上项目中也是采用的单独编译的方式。当然你也可以把所有的功能都写在 main 函数中,只不过这样不易于维护,也不符合项目开发标准。

一个完整的函数定义形同如下:

返回值类型 函数名(参数列表){    函数体(函数的具体功能)}

注意我们上面说的只是一个完整的函数定义,而不是每个函数必须都要有返回值类型、参数列表、函数体,只有函数名是必须的(这个肯定好理解)。

当然也有函数定义出来什么都没有做,这就相当于是一个空函数,C 语言默认是允许空函数出现的,比如下面函数就是一个空函数。

sort(){}

sort 函数不执行任何操作也不返回任何值,这种函数可以在程序开发期间用于保留位置,留待以后再填充代码

程序其实就是一些变量和函数的集合,函数之间的通信可以通过函数参数、返回值来进行,函数通过传递参数,进行一系列的逻辑计算后,把返回值返回回去,以此达到函数交流、通信的目的。

对于函数来说,我们需要了解的两个关键点是参数列表返回值

函数参数

对于上面的 sum 函数来说,它的函数参数有两个,分别是 int 类型的 a 和 b,像这种在函数定义的括号中的变量被称为函数参数,这两个变量 a 和 b 也叫做形式参数,简称形参。

和定义在函数中的变量一样,形式参数也是局部变量,这些都属于函数私有的,作用域范围都是从进入函数开始起作用到函数执行完成后作用结束。

当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型,函数原型中你可以使用下面方式定义。

int sum(int a,int b); // 函数原型

也可以省略具体的变量名称,使用下面这种方式进行定义。

int sum(int,int); // 函数原型

在函数原型中没有定义变量,只是声明了两个 int 类型的参数。

除了形参之外,还有一个叫做实际参数 ( 实参 ) 的概念,就对应于上面代码中的 sum(num,num2),因为在调用 sum 的时候是知道 num 和 num2 的具体值的,像这种在调用函数中对参数进行传值的参数被称为实参。

简单点来说就是 形式参数是被调用函数中的变量,实际参数是调用函数赋给被调函数的具体值。实际参数可以是常量、变量,或甚至是更复杂的表达式。

被调函数不知道也不关心传入的数值是来自常量、变量还是一般表达式。实参在把值传递给函数的时候,其实是把值拷贝给被调函数的形式参数,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据。

如下代码所示

#include <stdio.h>int num; // 变量原型 void sum(int,int); // 函数原型 int main(void){ num = 12; // 变量定义  int num2 = 11; // 函数原型 + 函数定义   sum(num,num2); // 变量使用 ,函数使用  printf("num = %d, num2 = %d",num,num2);  return 0;} // 函数定义 void sum(int a,int b){  int sumAll =  a + b; printf("sumAll = %d\n",sumAll); }

从输出结果可以看出,只要把值传递给 sum 后,不论 sum 函数内部进行何种处理,都不会影响 main 函数中 num 和 num2 的值。

函数返回值

我们上面说过函数之间的通信可以通过函数参数、返回值来进行。函数参数的传递方向是由函数调用者 -> 被调函数,而函数返回值的方向是和参数传递的方向相反,也就是被调函数 -> 函数调用者。

图片

当然并不是所有的函数都需要返回值,而且 return 语句后面也不一定需要表达式,当 return 语句后面没有表达式时,函数不会向调用者返回值。返回值会通过

return 表达式

进行返回,这个返回值的表达式类型和函数定义的返回值类型是一致的。

我们还用上面的 sum 函数来举例子

int sum(int a,int b){  return a + b;}

可以看到,sum 函数的表达式返回了 a + b,这其实就是一个表达式。而我们可以看到上面的 int main 方法,它的返回值是 0 ,这就是返回了一个常量。

return 后面可以不返回任何值,只是单独写一个 return 也是允许的,不过这种方式相当于没有返回任何值,所以它的函数类型可以定义为 void ,如下代码所示:

// 函数定义 void sum(int a,int b){  int sumAll =  a + b;  printf("sumAll = %d\n",sumAll);  return ; }

使用 return 语句的另外一个作用是终止函数的执行,强制把控制返回给调用函数,如下代码所示:

// 函数定义 int sum(int a,int b){   int sumAll =  a + b;  printf("sumAll = %d\n",sumAll);    if(a + b > 0){   return sumAll;  }else{    return 0;  }   }

如果 a + b 的值大于 0 的话,会直接返回 a + b 的和,否则为 0 。这个 if 的控制流程就是强制把结果返回给函数调用者。如果在 if 控制流程后面添加代码的话,那么这段代码不会执行,但是编译却没有给出警告。

图片

在 Java 编辑器中,如果最后一行代码出现在 return 强制返回后面的话,编译器会给出警告或者错误提示这行代码不会被执行。

函数类型

这里需要再强调一下函数类型,定义函数的时候需要声明函数的类型,带返回值的函数类型与返回值类型相同,没有返回值的函数应该将其定义为 void 类型。在老版本的 C 编译器中,如果你没有声明函数类型,编译器会默认把函数当做 int 类型来处理,不过这都是早期的事儿了,现在 C 标准不再支持默认函数为 int 类型这种情况。

在编写函数的时候,你就需要考虑好函数的具体功能是什么,也就是这个函数做了哪些事情,需不需要返回值,如果需要返回值的话,它的返回类型是什么。

函数声明

如果大家学过 Java ,可能对 C 这种先声明再定义的方式很不习惯,为什么函数在定义前需要再单独声明一下呢?我直接定义函数不声明行吗?答案肯定是不行的。

这个先声明再使用一直是 C 语言的标准,标准没有为什么,这就是一个标准,但是这个标准却是一个历史遗留问题。

上世纪 70 年代,大部分计算机内存很小,处理速度也比较差,所以导致代码的运行>时间很长,效率很差,这时候进行我们就需要考虑内存占用和编译时间的问题。因为 C 语言开发的比较早,而且 C 又是和硬件直接打交道的,所以提前声明一下函数能够提前分配内存空间,提升效率。说白了还是效率问题。

还有为什么 C 语言不选择采用预编译一下呢?

参考自 https://www.zhihu.com/question/20567689

首先,C语言出现的很早,那时候编译器也是一个很复杂的东西,当时计算机的内存、外存都很小,编译器做的太大也是一个麻烦的事情,所以事先声明就成为一种规范,保留下来,目的是为了让编译器更简单,虽然这一切已经很过时了。

其次,预编译的成本很高,与脚本语言、解释语言不同,C语言项目的规模可以很大,比如操作系统一级的C语言工程,其源文件有几万个,涉及全局符号几十万个,这样规模的项目预编译一次的负担是很高的,如果是整个项目完全扫描一遍,遍历所有全局符号,再进行真正的编译,估计很多码农都会疯了,等待时间会特别长。

再次,C语言是一种静态链接的语言,如果一个项目被设计成只编译,不链接的方式,比如有些库就会被设计成这样,有些合作开发的项目里,组员之间有时候也只提供obj文件,那么某些全局符号可能就不包含在现有的代码里,那么预搜索就一定找不到某些符号,那么该怎么办?如果不提供声明,这个代码就没办法编译了。

基于以上几点考虑,所以C语言才设计成这样,对于开发者而言,不算友好,但也不算很糟糕,甚至在某些方面是有好处的。

对于一个函数来说,它的最终目的就是通过一系列的逻辑处理获得我们想要的结果,逻辑处理离不开各种程序控制语句,比如说 While 、for、do while 等,下面我们就要来讨论一下这些程序控制语句。

程序控制语句

在有些时候的某些程序可能会重复做一件事情,就应该让计算机做这些重复性的工作,这才是我们需要计算机的意义。毕竟,需要重复计算是使用计算机的主要原因。

C 语言中有很多用于重复计算的方法,我们下面先来介绍其中的一种 --- while 循环

while 循环

下面我们通过一段代码来看一下 while 循环的使用。

#include <stdio.h>int main(){ int i = 1; while (i <= 10) {  printf("%d\n", i);  i++; } return 0;}

这段代码首先声明了一个 i 变量,然后使用了 while 循环来判断 i 的值,当 i 的值 <= 10 的时候,就会执行 while 中的循环逻辑,否则即 i > 10 就会直接跳过循环,不会输出任何结果就直接返回 0 。

如果 i 的值在 10 以内,就会循环打印出来 i 的值。这就是 while 循环的作用。

用通俗易懂的语句来描述 while 循环:当某个判断条件为 true 的时候,循环执行 while 中的代码块。

流程图如下:

图片

在 while 循环中的一个关键点就是进入 while 循环的判断,上面代码就是判断 i <= 10 ,这个表达式是关系运算符的一种。

while循环经常依赖测试表达式作比较,这样的表达式被称为关系表达式,出现在关系表达式中间的运算符叫做关系运算符,下表是我们经常使用到的关系运算符。


运算符说明
<小于
<=小于或等于
==等于
>=大于或等于
>大于
!=不等于


这些运算符会不单单会出现在 while 循环中,实际上任何逻辑控制语句都会使用到这几种运算符。

这里需要说明一点,不能用关系运算符来比较字符串,比如 ch != '@' 。

虽然关系运算符可以用来比较浮点数,但是要注意:比较浮点数时,尽量只使用 < 和 > 。因为浮点数的舍入误差会导致在逻辑上应该相等的两数却不相等。例如,3乘以1/3的积是1.0。如果用把1/3表示成小数点后面6位数字,乘积则是 .999999,不等于 1。

for 循环

for 循环一个非常明显的特征就是把三个行为组合在一处,也就是初始化、判断、更新,如下代码所示。

#include <stdio.h>int main(){ for(int i = 1;i <= 10;i++){  printf("%d\n", i); } return 0;}

可以看到,上面代码中 for 循环分别做了三件事情,每个表达式用 ; 进行分隔。

  • int i = 0 相当于是对 i 进行初始化操作;
  • i <= 10 相当于对 i 进行一个逻辑判断,逻辑判断是判断是否进行下一次循环的关键。
  • i++ 相当于是更新 i 的值。

for 循环的一般形式定义如下:

for(表达式1;表达式2;表达式3){    语句;}

这里要注意的是,表达式 1 只在循环开始时执行一次,而表达式 3 是循环结束后再执行。表达式 2 可以省略,省略后默认值为 1,则判断为真,for 循环就会成为一个死循环。

for 循环的流程图如下

图片

do while 循环

一般来说,循环的方式可以分为两种:入口循环和出口循环,什么意思呢?入口循环是先进行循环,再执行每次循环要做的事情,比如上面的 while 循环、for 循环,他们都是先进行判断是否需要进行下一次循环,如果需要的话,才会打印出 i 的值,这就是入口循环。

而出口循环则是要先执行代码,再判断是否要进行下一次循环,即在循环的每次迭代之后检查测试条件,这保证了至少执行循环体中的内容一次,典型的出口循环就是 do ... while。

我们把上面的代码进行修改:

#include <stdio.h>int main(){ int i = 1; do{  printf("%d\n", i);  i++; }while(i <= 10);  return 0;}

从输出结果可以看到,do while 循环在执行完循环体后才执行测试条件,所以 do ... while 循环至少执行循环体一次,而 for 循环和 while 循环在执行循环体之前先执行测试条件,do ... while 的一般形式如下

do 代码while ( 表达式 );

do ... while 循环的流程图如下

图片

到现在为止, C 语言中的程序控制语句我们都了解了,那么该如何进行选择呢?

实际上上面我们已经稍微讨论了一下如何选择的问题了。

while 循环和 for 循环很类似,这两类循环都是先进行一次循环条件的判断,然后再执行具体的循环体操作,只要一次循环条件不满足则一次都不会执行;而 do ... while 循环会至少先进行一次循环,然后才会执行循环判断。

一般来说,使用 for 循环的场景比较多,因为 for 循环形式更加简洁,而且在 for 循环中,变量和判断以及更新的作用域都在循环体内,不会有其他外部代码来修改这些变量,更可控,在 while 和 do ... while 循环中,变量的更新不可控,而且代码也没有 for 循环可读性强。

break 和 continue

break 和 continue 相当于是循环体内领导者的这样一个角色,有了这两个角色存在,循环体内的代码会根据这两个关键字来判断是中断循环还是执行下一次循环。

C 语言中的 break 有两种用法:

  • 一种用法是用在循环体中,当 break 出现在循环体中时,会中断这个循环。
  • 一种用法是用在 switch 语句中,用作中断这个 switch 语句的 case 条件。

break 用于中断循环:如下代码所示

#include <stdio.h>int main(void){ for(int i = 1;i <= 10;i++){  if(i == 5){   break;  }  printf("i 的值 = %d\n",i); }  return 0;}

输出的结果是 i 的值 = 1 - 4, 当 i == 5 时,会进入到 if 判断中,if 判断会直接触发 break,break 用于跳出当前循环,当前是 for 循环,所以 break 会直接跳到 for 循环外面,也就是直接 return ,不会再打印 i 的值。

图片

continue 关键字用于跳过当前循环,执行下一次循环,它和 break 很相似但是有着本质的区别,break 是跳出循环,continue 是执行下一次循环,我们同样拿这个代码来说明,只需要把上面的 break 改成 continue 即可。

#include <stdio.h>int main(void){ for(int i = 1;i <= 10;i++){  if(i == 5){   continue;  }  printf("i 的值 = %d\n",i); }  return 0;}

(这段代码的输出结果会输出出 i = 5 以外的值)

从输出结果可以看出,只有 i = 5 的值没有输出,这也就是说,当代码执行到 i == 5 的时候,会进行 continue 继续执行当前循环,从而跳过这次循环后面的代码,如下图所示。

图片

总结

这篇文章我主要和你聊了聊 C 语言中的函数,函数定义、函数返回值、参数以及程序控制流程中的三类循环的特点以及选型,最后又介绍了一下 break 和 continue 的作用。


Minio是一个开源的分布式文件存储系统,它基于 Golang 编写,虽然轻量,却拥有着不错的高性能,可以将图片、视频、音乐、pdf这些文件存储到多个主机,可以存储到多个Linux,或者多个Windows,或者多个Mac,Minio中存储最大文件可以达到5TB任何类型的文件都是支持的,主要应用在微服务系统中.


图片

一、准备机器


获取最新更新以及文章用到的软件包,请移步点击:查看更新

1、准备四台机器,(minio集群最少四台)。





192.168.223.131 minio-1192.168.223.128 minio-2192.168.223.129 minio-3192.168.223.130 minio-4

2、编辑hosts文件,将以上内容添加到hosts中


vim /etc/hosts

图片

部署(所有机器均执行)

以下的操作都需要在四台机器上执行

3、创建挂载磁盘路径


mkdir -p /data/minio_data/

4、挂载磁盘路径到文件系统

注意:需要将新建的目录挂在到对应的磁盘下,磁盘不挂载好,集群启动会报错,还需要注意的是挂载的文件系统至少要1G不然无法初始化导致集群报错

文件系统 容量 已用 可用 已用% 挂载点












[root@minio-1 minio]# df -h文件系统                                容量  已用  可用 已用% 挂载点devtmpfs                                470M     0  470M    0% /devtmpfs                                   487M     0  487M    0% /dev/shmtmpfs                                   487M  8.4M  478M    2% /runtmpfs                                   487M     0  487M    0% /sys/fs/cgroup/dev/mapper/centos_hadoop--master-root   47G   12G   36G   25% //dev/sda1                              1014M  240M  775M   24% /boottmpfs                                    98M     0   98M    0% /run/user/0tmpfs                                    98M   12K   98M    1% /run/user/42————————————————

5、将上面挂载磁盘路径挂载到相应的文件系统上


mount /dev/sda1 /data/minio_data/

6、查看挂载信息

图片

 7、创建minio目录


cd   /data/minio_data/

  8、下载安装包



wget http://dl.minio.org.cn/server/minio/release/linux-amd64/miniowget https://dl.min.io/client/mc/release/linux-amd64/mc

  9、赋执行权限(根据情况,这里赋全部权限)


chmod +x minio mc

  10、创建启动脚本,编辑run.sh文件


mkdir /data/minio_data && cd /data/minio_data

内容如下:







cat > run.sh <<EOF#!/bin/bashexport MINIO_ACCESS_KEY=minioexport MINIO_SECRET_KEY=Leo825#20210423/usr/local/bin/minio server --address=192.168.81.235:9000 http://192.168.81.235/data/minio_data/data1  http://192.168.81.236/data/minio_data/data1  http://192.168.81.237/data/minio_data/data1 http://192.168.81.234/data/minio_data/data1EOF

11、赋执行权限(根据情况,这里赋全部权限)


chmod 777 /data/minio_data/run.sh

12、创建启动服务,创建minio.service启动脚本


vim /usr/lib/systemd/system/minio.service


内容如下:



















[Unit]Description=Minio serviceDocumentation=https://docs.minio.io/

[Service]#安装包路径WorkingDirectory=/data/minio_data#启动命令路径ExecStart=/data/minio_data/run.sh

Restart=on-failureRestartSec=5

[Install]WantedBy=multi-user.target

复制代码

13、启动测试(所有机器执行)


复制代码

重新加载服务的配置文件


systemctl daemon-reload


启动minio服务


systemctl start minio


查看minio状态












systemctl status minio[root@minio-2 ~]# systemctl status minio● minio.service - Minio serviceLoaded: loaded (/usr/lib/systemd/system/minio.service; disabled; vendor preset: disabled)Active: active (running) since 日 2021-01-31 17:22:54 CST; 17s agoDocs: https://docs.minio.io/Main PID: 2036 (run.sh)Tasks: 8CGroup: /system.slice/minio.service├─2036 /bin/bash /data/minio_data/run.sh└─2039 /data/minio_data server http://192.168.223.232/data/minio_data/data1 http://192.168.223.233/data/minio_data/data1

关闭minio服务


systemctl stop minio

复制代码

14、访问地址

集群中的任何一台机器都可以访问:





http://192.168.223.132:9000/http://192.168.223.133:9000/http://192.168.223.134:9000/http://192.168.223.135:9000/

15、创建测试桶

图片

16、上传测试

图片

17、主机上可以查看到上传的文件


图片


二、nginx配置文件服务器访问

1、执行命令




mc  alias set minio http://192.168.223.132:9000/ minio Leo825#20210423 --api S3v4开启匿名访问mc policy set public minio/sy01

2、web页面开启匿名访问


图片

图片


 3、http访问,sy01是桶名称,方便浏览器访问。




























































upstream minio-server{  server 192.168.6.124:9000 weight=25 max_fails=2 fail_timeout=30s;  server 192.168.6.125:9000 weight=25 max_fails=2 fail_timeout=30s;  server 192.168.6.126:9000 weight=25 max_fails=2 fail_timeout=30s;  server 192.168.6.128:9000 weight=25 max_fails=2 fail_timeout=30s;}

server {  listen 8888;  server_name 192.168.6.120;

  #To allow special characters in headers  ignore_invalid_headers off;  # Allow any size file to be uploaded.  # Set to a value such as 1000m; to restrict file size to a specific value  client_max_body_size 0;  # To disable buffering  proxy_buffering off;

  location /sy01/ {     proxy_set_header X-Real-IP $remote_addr;     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;     proxy_set_header X-Forwarded-Proto $scheme;     proxy_set_header Host $http_host;

     proxy_connect_timeout 300;     # Default is HTTP/1, keepalive is only enabled in HTTP/1.1     proxy_http_version 1.1;     proxy_set_header Connection "";     chunked_transfer_encoding off;

     proxy_pass http://minio-server;   }

   location / {     proxy_set_header X-Real-IP $remote_addr;     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;     proxy_set_header X-Forwarded-Proto $scheme;     proxy_set_header Host $http_host;

     proxy_connect_timeout 300;     # Default is HTTP/1, keepalive is only enabled in HTTP/1.1     proxy_http_version 1.1;     proxy_set_header Connection "";     chunked_transfer_encoding off;

     proxy_pass http://minio-server; # If you are using docker-compose this would be the hostname i.e. minio     # Health Check endpoint might go here. See https://www.nginx.com/resources/wiki/modules/healthcheck/     # /minio/health/live;   }}

  4、https访问,minio是负载minio服务,sy01是桶名称,方便浏览器访问。
















































upstream minio-server{       server 192.168.6.124:9000 weight=25 max_fails=2 fail_timeout=30s;       server 192.168.6.125:9000 weight=25 max_fails=2 fail_timeout=30s;       server 192.168.6.126:9000 weight=25 max_fails=2 fail_timeout=30s;       server 192.168.6.128:9000 weight=25 max_fails=2 fail_timeout=30s;}

server {    listen 443 ssl;    server_name  192.168.6.120;

    ssl_certificate /etc/nginx/ssl/192.168.6.120.crt;    ssl_certificate_key /etc/nginx/ssl/192.168.6.120.key;

    location /sy01/ {         proxy_set_header X-Real-IP $remote_addr;         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;         proxy_set_header X-Forwarded-Proto $scheme;         proxy_set_header Host $http_host;

         proxy_connect_timeout 300;         proxy_http_version 1.1;         proxy_set_header Connection "";         chunked_transfer_encoding off;         proxy_pass http://minio-server;  }

  location /minio {     proxy_set_header X-Real-IP $remote_addr;     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;     proxy_set_header X-Forwarded-Proto $scheme;     proxy_set_header Host $http_host;

     proxy_connect_timeout 300;     proxy_http_version 1.1;     proxy_set_header Connection "";     chunked_transfer_encoding off;     proxy_pass http://minio-server;  }  }

或者换成tcp代理也可以实现,放在http层外,如果服务器在内网,访问在外网,中间经过了代理,或者隧道之内的,必须要要tcp,否则会http头部hred验证会失败。













upstream minio-server {        server 192.168.178.40:9000     max_fails=3 fail_timeout=30s;        server 192.168.178.41:9000     max_fails=3 fail_timeout=30s;        server 192.168.178.42:9000     max_fails=3 fail_timeout=30s;        server 192.168.178.43:9000     max_fails=3 fail_timeout=30s;}server {        listen 9000;        proxy_connect_timeout 2s;        proxy_timeout 900s;        proxy_pass minio-server;}


企业级架构ELKF集群


图片

服务器

A:10.10.1.3

B:10.10.1.4

C:10.10.1.5

客户端

D:10.10.1.6

下面将用ABC代表服务器了

ABC修改计算机名称




A服务器:hostnamectl set-hostname node1B服务器:hostnamectl set-hostname node2C服务器:hostnamectl set-hostname node3


ADCD更新源并安装java




curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repocurl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repoyum install lrzsz vim java-1.8.0-openjdk java-1.8.0-openjdk-devel-y   java –version -y


ABC都切换到 


cd /usr/local/src/


吧文件下面文件都拷贝到上面的这个目录







apache-zookeeper-3.6.0-bin.tar.gzelasticsearch-7.13.1-x86_64.rpmfilebeat-7.13.1-x86_64.rpmkafka_2.12-2.5.0.tgzkibana-7.13.1-x86_64.rpmlogstash-7.13.1-x86_64.rpm


图片


ABC都要进行更新源,和安装java




curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repocurl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repoyum install lrzsz vim java-1.8.0-openjdk java-1.8.0-openjdk-devel-y   java –version -y


ABC安装ES集群服务,进行连接



cd /usr/local/src/yum localinstall elasticsearch-7.13.1-x86_64.rpm -y


 JVM的内存限制更改,根据实际环境调整,更具服务器内存修改,如果服务器8G,这里可以改为6G,给服务器预留1-2G即可



vi /etc/elasticsearch/jvm.options -Xms4g 改为 -Xmx2g





ES集群实战注意

  集群交互是使用证书交互

  搭建集群前需要先创建证书文件

ES集群交互CA证书创建  A服务器上


/usr/share/elasticsearch/bin/elasticsearch-certutil ca           #一直回车


查看证书位置


ll -rht /usr/share/elasticsearch/elastic-stack-ca.p12


创建证书


/usr/share/elasticsearch/bin/elasticsearch-certutil cert --ca /usr/share/elasticsearch/elastic-stack-ca.p12           #一直回车


查看证书



ll -rht /usr/share/elasticsearch/elastic-certificates.p12cp /usr/share/elasticsearch/elastic-certificates.p12 /etc/elasticsearch/elastic-certificates.p12


证书要求权限修改,不然集群搭建失败



chmod 600 /etc/elasticsearch/elastic-certificates.p12chown elasticsearch:elasticsearch /etc/elasticsearch/elastic-certificates.p12


吧加密ca证书拷贝到BC服务器



scp /etc/elasticsearch/elastic-certificates.p12 10.10.1.4:/etc/elasticsearch/scp /etc/elasticsearch/elastic-certificates.p12 10.10.1.5:/etc/elasticsearch/


图片


ABC服务器对比MD5值是否一致


md5sum /etc/elasticsearch/elastic-certificates.p12


图片


B服务器和C服务器都要执行



chmod 600 /etc/elasticsearch/elastic-certificates.p12chown elasticsearch:elasticsearch /etc/elasticsearch/elastic-certificates.p12


ADC服务器都要进行修改


















vi /etc/elasticsearch/elasticsearch.ymlcluster.name: maixiaolunnode.name: node1   #服务器A就改为nede1,服务器B改为node2,服务器C改为node3node.master: truenode.data: truepath.data: /var/lib/elasticsearchpath.logs: /var/log/elasticsearchnetwork.host: 0.0.0.0http.port: 9200discovery.seed_hosts: ["10.10.1.3", "10.10.1.4", "10.10.1.5"]cluster.initial_master_nodes: ["10.10.1.3", "10.10.1.4", "10.10.1.5"]xpack.security.enabled: truexpack.monitoring.enabled: truexpack.security.transport.ssl.enabled: truexpack.security.transport.ssl.verification_mode: certificatexpack.security.transport.ssl.keystore.path: /etc/elasticsearch/elastic-certificates.p12xpack.security.transport.ssl.truststore.path: /etc/elasticsearch/elastic-certificates.p12


ABC防火墙要放通端口









firewall-cmd --zone=public --add-port=9300/tcp --permanentfirewall-cmd --zone=public --add-port=9300/udp --permanentfirewall-cmd --zone=public --add-port=9200/tcp --permanentfirewall-cmd --zone=public --add-port=9200/udp --permanentfirewall-cmd --zone=public --add-port=5601/tcp --permanentfirewall-cmd --zone=public --add-port=5601/udp --permanentfirewall-cmd --zone=public --add-port=80/tcp --permanentfirewall-cmd --zone=public --add-port=3000/tcp --permanent

重新载入


firewall-cmd --reload


图片


图片


图片


ABC服务器顺序启动ES服务



systemctl enable elasticsearchsystemctl restart elasticsearch


继续看第一台服务器日志


图片


出现valid,表示成功了

接下来是查看3台集群中间的互动



yum -y install net-tools  安装 netstatnetstat -anp |grep 10.0.0.20:9300   查看集群之间的交互


检查9300端口连接情况


图片


确认集群中所有es的日志正常再设置密码maixiaolun..123

在主服务器(服务器A,想把那个当主服务器就在那个服务器上运行)上运行

ES设置密码/usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive


图片


ES设置随机密码/usr/share/elasticsearch/bin/elasticsearch-setup-passwords auto

验证集群是否成功,标记为*的为master节点。网页访问或者curl访问

http://10.10.1.3:9200    账号:elastic   密码:刚刚设置的

http://10.10.1.3:9200/_cat/nodes?v    //查看节点信息  带*的就是主的


图片


http://xxx:9200/_cat/indices?v

命令方式查看 输入账号密码

curl -u elastic:maixiaolun..123 http://10.10.1.3:9200/_cat/nodes?v


图片


B服务器安装Kibana (因为是前端,蹦了就蹦了,不要紧,所有不需要每个地方都搭建来)

B服务器安装



cd /usr/local/src/yum localinstall kibana-7.13.1-x86_64.rpm -y


图片


Kibana配置连接ES集群








vi /etc/kibana/kibana.ymlserver.port: 5601server.host: "0.0.0.0"elasticsearch.hosts: ["http://10.10.1.3:9200", "http://10.10.1.4:9200", "http://10.10.1.5:9200"]elasticsearch.username: "elastic"elasticsearch.password: "maixiaolun..123"logging.dest: /tmp/kibana.log



B服务器Kibana的启动和访问



systemctl enable kibanasystemctl restart kibana



Kibana监控开启


netstat -tulnp


图片


http://10.10.1.3:5601

账号:elastic

密码:maixiaolun..123


BC服务器安装logstash日志分析 (就是蹦了一台也不要紧,此做法为高可用做法,安装一台的话也是可以的)



cd /usr/local/src/yum localinstall logstash-7.13.1-x86_64.rpm -y


图片


内存配置jvm.options 1g,根据自己实际情况改


vi /etc/logstash/jvm.options

配置logstash配置文件



































vi /etc/logstash/conf.d/logstash.conf#监听5044传来的日志input {   beats {   host => '0.0.0.0'   port => 5044 }}#这里是正则表达式,分析日志filter {grok {match => {"message" => '%{IP:remote_addr} - (%{WORD:remote_user}|-) [%{HTTPDATE:time_local}] "%{WORD:method} %{NOTSPACE:request} HTTP/%{NUMBER}" %{NUMBER:status} %{NUMBER:body_bytes_sent} %{QS} %{QS:http_user_agent}'}remove_field => ["message"]}date {match => ["time_local", "dd/MMM/yyyy:HH:mm:ss Z"]target => "@timestamp"}}

#发送给所有ES服务器output {elasticsearch {hosts => ["http://10.10.1.3:9200", "http://10.10.1.4:9200", "http://10.10.1.5:9200"]user => "elastic"password => "maixiaolun..123"index => "mxlnginx-%{+YYYY.MM.dd}"}}


启动Logstash,配置重载:kill -1 pid



systemctl restart logstashsystemctl enable logstash


等待启动,查看日志

/var/log/logstash   日志生成的地方


tail -f logstash-plain.log


图片


ZK集群部署

所有服务器都要进行安装







cd /usr/local/src/tar -zxvf apache-zookeeper-3.6.0-bin.tar.gzmv apache-zookeeper-3.6.0-bin /usr/local/zookeepermkdir -pv /usr/local/zookeeper/datacd /usr/local/zookeeper/confcp zoo_sample.cfg zoo.cfg

  zk集群至少需要三台机器

集群配置zoo.cfg 












vi zoo.cfgtickTime=2000initLimit=10syncLimit=5dataDir=/usr/local/zookeeper/dataclientPort=2181autopurge.snapRetainCount=3autopurge.purgeInterval=1server.1=10.10.1.3:2888:3888server.2=10.10.1.4:2888:3888server.3=10.10.1.5:2888:3888


更改zk集群的id


vi /usr/local/zookeeper/data/myid

第一台就填写1

第二台就填写2

第三台就填写3


图片


图片

分别为1 2 3

systemctl管理











vi /usr/lib/systemd/system/zookeeper.service[Unit]Description=zookeeperAfter=network.target[Service]Type=forkingExecStart=/usr/local/zookeeper/bin/zkServer.sh startUser=root[Install]WantedBy=multi-user.target


启动zk



systemctl enable zookeepersystemctl restart zookeeper


启动zk集群查看状态




cd /usr/local/zookeeper/bin/./zkServer.sh start./zkServer.sh status


图片

报错的话,检查防火墙。

正确显示为下


图片

Kafka集群部署

下载地址:http://kafka.apache.org/downloads

安装



cd /usr/local/src/tar -zxvf kafka_2.12-2.5.0.tgz -C /usr/local/

  Jvm内存修改/usr/local/kafka_2.12-2.5.0/bin/kafka-server-start.sh,根据实际情况修

改,每台服务器都要改


图片


修改Kafka配置server.properties



cd /usr/local/kafka_2.12-2.5.0/config/vi server.properties


图片













broker.id=0listeners=PLAINTEXT://xxx:9092 log.retention.hours=1  #根据实际情况修改  日志保留时间zookeeper.connect=xxx:2181,xxx:2181,xxx:2181zookeeper.connection.timeout.ms=18000 改为 zookeeper.connection.timeout.ms=60000拷贝到其他服务器上scp server.properties 10.0.0.21:/usr/local/kafka_2.12-2.5.0/config/然后在新的服务器上vi /usr/local/kafka_2.12-2.5.0/config/server.properties改listeners=PLAINTEXT://xxx:9092 对应当前服务器的ipbroker.id=0 改为broker.id=1

如果启动的时候报如下错误,是因为链接超时,吧60000改为 1200000也就是2分钟


图片


Kafka使用systemctl管理











vi /usr/lib/systemd/system/kafka.service[Unit]Description=kafkaAfter=network.target[Service]Type=simpleExecStart=/usr/local/kafka_2.12-2.5.0/bin/kafka-server-start.sh /usr/local/kafka_2.12-2.5.0/config/server.propertiesUser=root[Install]WantedBy=multi-user.target

Kafka启动



systemctl enable kafka systemctl restart kafka


查看kafka日志



cd /usr/local/kafka_2.12-2.5.0/logstail -f server.log


图片


创建topic,创建成功说明kafka集群搭建成功

--replication-factor 3 代表3台服务器



cd /usr/local/kafka_2.12-2.5.0/bin/./kafka-topics.sh --create --zookeeper 10.10.1.4:2181 --replication-factor 3 --partitions 1 --to


图片


logstash获取kafka数据 


vi /etc/logstash/conf.d/logstash.conf


#获取kafka的日志






















































input { kafka {   bootstrap_servers => "10.10.1.3:9092,10.10.1.4:9092,10.10.1.5:9092"   topics => ["mxlkafka"]   group_id => "mxlgroup"   codec => "json" }}





filter { mutate {   remove_field => ["agent","ecs","log","input","[host][os]"] }}



#正则表达式分析日志filter {grok {match => {"message" => '%{IP:remote_addr} - (%{WORD:remote_user}|-) [%{HTTPDATE:time_local}] "%{WORD:method} %{NOTSPACE:request} HTTP/%{NUMBER}" %{NUMBER:status} %{NUMBER:body_bytes_sent} %{QS} %{QS:http_user_agent}'}remove_field => ["message"]}date {match => ["time_local", "dd/MMM/yyyy:HH:mm:ss Z"]target => "@timestamp"}}



#吧分析过后的日志发送给ESoutput {elasticsearch {hosts => ["http://10.10.1.3:9200", "http://10.10.1.4:9200", "http://10.10.1.5:9200"]user => "elastic"password => "maixiaolun..123"index => "mxlnginx-%{+YYYY.MM.dd}"     #这里代表日志的名字,不通服务器可以写不通的名字}}

重载



ps auxfww | grep logstashkill -1 804


图片


看日志

cd /var/log/logstash/

tail -f logstash-plain.log


图片


测试环境是否正常

效果同ELFK,但架构做了优化

Kafka查看队列信息,验证新架构是否生效




cd /usr/local/kafka_2.12-2.5.0/bin/./kafka-consumer-groups.sh --bootstrap-server 10.10.1.4:9092 --list./kafka-consumer-groups.sh --bootstrap-server 10.10.1.4:9092 --group mxlgroup -describe


  LOG-END-OFFSET不断增大,LAG不堆积说明架构生效


图片


LOG-END-OFFSET 一直增加就代表kafka在消费

LAG 一直在堆积

Logstash扩展

  配置保持一致,启动即可




客户端D进行安装nginx进行测试数据是否传入服务器集群

安装nginx做测试,如果有其他服务可以直接测试



yum install nginx -ysystemctl start nginx


安装filebeat,用于发送日志到服务器



cd /usr/local/src/yum localinstall filebeat-7.13.1-x86_64.rpm -y




配置filebeat进行监控nginx



















vi /etc/filebeat/filebeat.ymlfilebeat.inputs:- type: log  tail_files: true  backoff: "1s"  paths:     - /var/log/nginx/access.log

processors:- drop_fields:   fields: ["agent","ecs","log","input"]

output:  kafka:    hosts: ["10.10.1.3:9092", "10.10.1.4:9092", "10.10.1.5:9092"]    topic: mxlkafka   #这里代表日志的名字,不通服务器可以写不通的名字


启动filebeat



systemctl start filebeatsystemctl enable filebeat


登入B服务器网页

http://10.10.1.4:5601

账号:elastic

密码:maixiaolun..123


打开Stack Management


图片



添加日志数据 Index Patterns


图片


添加


图片



输入刚刚设置的名字 mxlnginx* 点击下一步

图片


选择@timestamp  下一步


图片


这时候就能看到添加的内容了



图片



访问客户端地址nginx页面 10.10.1.6  多刷新几次


图片

就可以去看数据了


图片



看到日志已经上来了



图片


读者可在公众号后台回复关键字 ELKF集群7搭建 获取二进制安装包