序言:
C语言起始于1972年,到今年(2023年)已经50个年头了,如今在各大排行榜上也占据前3的位置,经久不衰,值得我们好好研究。
许多人觉得C语言太简单/C语言指针很难,这两种想法,终究原因是只学习“语言”的属性,不关心各种计算机语言其实是为了解决人类的问题而发明的,而且是真正要跑在计算机上的,
我们不能只谈论语言本身,而忽视了实际运行时的环境,C语言深入的学习还需要了解计算机操作系统、计算机组成原理、计算机体系结构等相关知识。注意:这里只是需要用到相关知识。
C语言的发明其实用来开发Unix操作系统的,所以值得我们去学习,并将上述计算机理论原理相关知识融入其中。(如:CPU和内存到底是什么?如何存/取出一个数据?)
基于上述,我写下了如下的C语言深度理解。
(希望大家点点关注!!!若有什么错误之处,可以直接指出,望各位大佬指教。)
写于 2023年7月26日
文章目录
C语言至少需要知道的C89/C90中32个关键字
关键字的分类
C语言一共多少个关键字呢?一般的书上,都是32个(包括本书),但是这个都是 C90(C89) 的标准。其实 C99 后又新增 了5个关键字(在C99标准中加⼊了 inline
、 restrict
、 _Bool
、 _Comploex
、 _Imaginary
等关键字)。不过,目前主流的编译器,对 C99 支持的并不好,我们后面默认情况,使用 C90 ,即,认为32个。
实际上目前的C语言标准(C23) (本人认为目前了解C11之后的即可)
下面是C语言关键字参考链接:
-
C语言程序设计:现代方法(第2版•修订版) (K.N.King) / C Primer Plus (第六版 )(C11)
下面我们来看具体32个关键字的作用
关键字 | 解释 |
---|---|
auto | 声明自动变量 |
short | 声明短整型变量或函数 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数 |
float | 声明单精度浮点型变量或函数 |
double | 声明双精度浮点型变量或函数 |
char | 声明字符型变量或函数 |
struct | 声明结构体变量或函数 |
union | 声明共用(联合)数据类型 |
enum | 声明枚举类型 |
typedef | 用以给数据类型取别名 |
const | 声明只读变量(注:C语言中,const类型限定符声明的是变量,不是常量) |
unsigned | 声明无符号类型变量或函数 |
signed | 声明有符号类型变量或函数 |
extern | 声明变量是在其他文件正声明 |
register | 声明寄存器变量 |
static | 声明静态变量 |
volatile | 说明变量在程序执行中可被隐含地改变 |
void | 声明函数无返回值或无参数,声明无类型指针 |
if | 条件语句 |
else | 条件语句否定分支(与 if 连用) |
switch | 用于开关语句 |
case | 开关语句分支 |
for | 一种循环语句 |
do | 循环语句的循环体 |
while | 循环语句的循环条件 |
goto | 无条件跳转语句 |
continue | 结束当前循环,开始下一轮循环 |
break | 跳出当前循环 |
default | 开关语句中的“其他”分支 |
return | 子程序返回语句(可以带参数,也可不带参数)循环条件 |
sizeof | 计算数据类型长度 |
第一个C程序 - 补充内容
// Visual Studio 2019(下面简称:VC2019)中建立项目/vscode远程ssh + linux(Ubuntu 22.04 LTS)创建C源文件
// 编写第一个C程序"hello world"
#include <stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
//1.运行程序的方式,当然可以用VC直接启动(按Ctrl + F5就可以直接运行)
//2.当然,也可以在VC项目中,找到代码生成的二进制可执行程序,双击即可。(Linux平台下通过找到生成的可执行程序,通过命令行运行)
//3.所以:我们的角色是写代码,编译器的角色是把文本代码变成二进制可执行程序。
//4.双击?不就是windows下启动程序的做法吗?
//5.那么启动程序的本质是什么呢?将程序数据,加载到内存中,让计算机运行!
//6.那么为什么要加载到内存中呢?
下面我们回答这6个问题:
- VC 2019 其中是以项目为单位来运行C语言程序的,其中集成了快捷功能(运行、调试)。若在Linux平台下运行C语言程序,则要创建如test.c的源文件,通过gcc/clang等编译器来生成可执行程序(也可以通过装插件来实现类似VC 2019的快捷功能)。
-
下面是VC 2019的演示:
VC 2019中可以通过快捷键来运行,前面已经概述过。
其实通过快捷键运行后会生成可执行程序,他在我们的
C:\Users\Administrator\Desktop\demo\test_7_26\Debug
目录下,会生成test_7_26.exe
可执行程序。我们双击它就可以运行程序。我们会发现屏幕一闪而过,原因是程序运行完就自动关闭了,若想看到运行时的效果,要在头文件加入#include <windows.h>
windows.h系统头文件,作用是为了停屏,然后在return 0
前一行加一句system("pause");
pause是为了停屏。代码如下:
#include <stdio.h> #include <windows.h> //windows.h系统头文件,作用是是为了停屏 int main() { printf("hello world!\n"); system("pause"); //pause是停屏 return 0; }
我们通过快捷键运行后,发现屏幕会多显示一行文字:
请按任意键继续. . .
成功起到停屏作用。
接下来,我们查看
C:\Users\Administrator\Desktop\demo\test_7_26\Debug
目录下,生成test_7_26.exe
可执行程序。
我们双击运行它,得到如下结果:
双击
test_7_26.exe
可执行程序。说明这个生成的可执行程序就和我们双击QQ/网易云音乐等程序一样,没什么区别。
下面是Linux平台下的演示:
接下来我们来看一下Linux平台下的执行流程:首先我们先查看gcc版本,通过
gcc -v
命令查看,我们得到以下结果root@ser836639051336:~# gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.3.0-1ubuntu1~22.04.1' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-aYxV0E/gcc-11-11.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-aYxV0E/gcc-11-11.3.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04.1)
查看最后一条信息得到使用的gcc版本:
gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04.1)
我们使用的平台是
Target: x86_64-linux-gnu
。插入一个题外话:市面上大多数是 X86-64 体系架构的Linux服务器,当然也有arm架构的服务器。我们通过 gcc编译命令
gcc test.c - o test
生成test
的可执行程序。Linux中通过./
+可执行程序文件名来运行程序。得到结果如下:下面是执行的相关命令:
root@ser836639051336:~/test_7_26# gcc test.c -o test root@ser836639051336:~/test_7_26# ls test test.c root@ser836639051336:~/test_7_26# ./test hello world! root@ser836639051336:~/test_7_26#
我们发现成功打印了hello word!,说明和在Windows平台下运行的结果一致。(看到了一点点的C语言跨平台性,其实这里使用的是不同的编译器,他们支持的C语言标准可能不同,而且都有自己编译器扩展的C语言标准,当然,大部分情况下,相同的代码都是能跨平台编译的)
-
作为编程者的我们,我们的目的是编写各种计算机语言的代码,解决相关问题,我们编写代码是文本的代码,是人能通过字面意思直接识别的,而生成的二进制程序我们用文本编辑器打开是乱码的,我们的编程人员很难看懂这段二进制序列,因为每台机器所支持的指令集和架构是不同的(最早开始编程的时候,是在纸带上打小孔,来标识0和1的序列来编写程序,可想而知,这种编程效率是有多么低,这里还出现了Bug这个词的来源,Bug的英文翻译是小虫子,而Bug的来是,在早期的计算机是由很多大的真空管组成的,会产生大量的光和热,引来一只小虫子钻进了真空管,导致整个机器死机了,后来被人们将计算机错误称为Bug)上面的历史故事也许你听起来很有趣,其实还有很多计算机的故事,只是我们在学习知识的时候,可能忽视背后的故事。
下面我们来看一下二进制可执行程序是什么,我们这里以Linux平台为例子,通过vscode自带的编辑器的功能打开test
这个程序
我们发现有相关提示,我们点击仍然打开,选择图示的编辑器 Hex Editor
(通过vscode 插件市场搜索这个名字就可以安装)
我们打开后发现其中有往屏幕(终端)hello world!
这段二进制代码,看起来很奇怪?其实计算机显示到屏幕的文字是相应的字符。
我们目前这么理解就可以了,后面我还要谈论字符/字符集的概念。
-
在windows下双击启动程序的做法,我们看起来很简单,其实其中涉及不少知识。首先,我们补充一些操作系统相关的知识:
程序是很多二进制序列的集合,我们的程序是放在硬盘上的,硬盘就是我们经常说的HDD(机械硬盘)/SSD(固态硬盘),当然现在的笔记本/台式机基本上都是固态硬盘,因为固态硬盘读写速度特别快,有的比机械硬盘快十几~一百倍,如果你去各大网购平台搜索固态硬盘,你会发现有PClE 4.0 /PCLE 3.0/STAT的m.2固态硬盘,比如PCLE 4.0的固态硬盘读写速度可以到达7000MB/s,看起来很快吧,而普通机械硬盘读写速度就100-200MB/s,但是机械硬盘虽然读写速度看起来很慢,他里面存储的数据时间是最长的,而固态硬盘是有读写寿命的,你可以理解为固态硬盘经常读写,到了一定时间,会发生掉速/掉盘等事情,当然,只要你购买的是大品牌的固态硬盘,在规定的保修时间(可能有5年)和 规定的读写容量范围内,会提供返厂/换新服务,这里不得不说,最近的固态硬盘和白菜一样便宜,这其中最大的功劳就是–国产之光
长江存储
发售的致态系列固态硬盘,让各大外国厂商都不敢随便因为失火,停电的原因来涨价。说了这么多,我们回到主题,在在windows下双击启动程序,操作系统会将存放在硬盘上的程序加载到内存中,变成我们所说的进程。 -
启动程序的本质就是操作系统将程序加载到内存中,变成进程,然后在计算机上运行。
程序为什么要加载到内存,这里可能有许多疑惑,因为内存比硬盘快很多,这里说很多可能没有具体概念,目前主流的内存是DDR4和 DDR5 (DDR3只有很老的机器还在用,本人有台2010年的机器,其中的内存条就是DDR3的),除次之外,还要服务器专用ECC纠错内存,比如 DDR5-6200规格的速度可以超过100GB/s的度,DDR4 3600的内存读写速度有50GB/s,注意这里的单位是GB/s,比我们的硬盘读写速度快了很多,所以我们要程序加载到内存。其实这样的解释有点只知表象了,要深刻理解这个问题还要了解计算机组成原理中的冯诺依曼结构。
-
现代计算机也是基于冯诺依曼结构设计的,只有宏观上明白了计算机是由什么部件构成的,才知道为什么有内存?冯诺依曼结构如下:由五个部分组成(运算器、控制器、存储器、输入设备、输出设备)
这里的输入设备就是我们经常提到的鼠标,键盘,硬盘,存储器就是内存,输出设备就是我们的显示器。可能看这个图还是不理解,我们举个真实的计算机的例 子(摘取CSAPP):
通过上面的例子,我们可以看一下周围的计算机机箱,里面的各个硬件也对应图上所描述的部件,这里我们不解释其中每个具体作用,只了解大概就行,整个计
算计执行屏幕打印hello world!
这个程序的流程大概是这样的(摘取自哈工大csapp课件)
- 要回答为什么程序要加载到内存,则要回答上面提到的缓存是什么??这里的内存就是磁盘的缓存,这里我讲一下原因:因为前面提到过硬盘和内存(主存)的速度相差很大,CPU直接从硬盘读取速度太 慢了,所以我们设计了内存作为缓存,这里有个问题,我们的内存通常只有8GB/16GB/32GB/64GB,而我们的硬盘有1-2TB,如何保证,计算机cpu每次取到的数据(二进制序列)都是内存中有的呢/或者我们cpu将要访问的数据大部分在内存中,这里要提到一个概念了:
局部性原理
,即程序具有访问局部区域里的数据和代码的趋势。通过让内存里存放可能经常访问的数据,大部分的操作都在比硬盘快的内存中完成。我们在后面还要提到存储器的层次结构,使用这种缓存计算机,可以在成本和速度方面做一个折中,让我们现在人人都买的起电脑。总的来说,硬盘上的程序先加载进入内存,变成进程,由CPU向内存读取数据来运行程序。
定义与声明 - 补充内容
什么是变量(是什么)
变量是在内存中开辟特定大小的空间,用来保存数据。
这里我们提到了一个关键字:内存,我们在前面讲述了内存是什么,但是这里所指的内存是虚拟内存,不是真实的物理内存。(这个知识点是操作系统相关的,这里不进行深刻讲解)
如何定义变量(怎么用)
int x = 10;
char c = 'a';
double d = 3.14;
类型 变量名 = 默认值
为什么要定义变量(为什么)
理解:
计算机是为了解决人计算能力不足的问题而诞生的。即,计算机是为了进行计算的。
而计算,就需要数据。
而要计算,任何一个时刻,不是所有的数据都要立马被计算。
如同:要吃饭,不是所有的饭菜都要立马被你吃掉。饭要一口一口吃,那么你还没有吃到的饭菜,就需要暂时放在盘子里。 这里的盘子,就如同变量,饭菜如同变量 里面的数据。
换句话说,为何需要变量?因为有数据需要暂时被保存起来,等待后续处理。
那么,为什么吃饭要盘子?我想吃一口菜了,直接去锅里找不行吗?当然行,但是效率低。 因为我们吃饭的地方,和做饭的地方,是比较"远"的。
变量定义的本质
我们现在已知:
-
程序运行,需要加载到内存中 ,变成进程。
-
程序计算,需要使用变量 那么,定义变量的本质:在内存中开辟一块空间,用来保存数据。(为何一定是内存:因为定义变量,也是程序逻辑的一部 分,程序已经被加载到内存)
变量声明的本质
我们目前只需要区分定义
和声明
,即可,当我们学到extern
的这个关键字,在细谈。
定义
:开辟一个内存空间(只能有一次)声明
:告知编译器我有这个变量(通告)(声明可以多次)
结论:声明就是广而告之