《C语言趣味教程》 猛戳订阅!!!

——热门专栏《维生素C语言》的重制版 ——

  • 写在前面:这是一套C 语言趣味教学专栏,目前正在火热连载中,欢迎猛戳订阅!本专栏保证篇篇精品,继续保持本人一贯的幽默式写作风格,当然,在有趣的同时也同样会保证文章的质量,旨在能够产出”有趣的干货” !

    本系列教程不管是零基础还是有基础的读者都可以阅读,可以先看看目录!标题前带星号 (*) 的部分不建议初学者阅读,因为内容难免会超出当前章节的知识点,面向的是对 C 语言有一定基础或已经学过一遍的读者,初学者可自行选择跳过带星号的标题内容,等到后期再回过头来学习。

    值得一提的是,本专栏强烈建议使用网页端阅读!享受极度舒适的排版!你也可以展开目录,看看有没有你感兴趣的部分!本章是本教程的开篇之作,篇幅较长且内容极为丰富 (全文近两万字),是本团队 (只有我一个人的团队) 呕心沥血耗时一周打磨出来的内容,希望需要学C 语言的朋友可以耐下心来读一读。最后,可以订阅一下专栏防止找不到。

” 有趣的写作风格,还有特制的表情包,而且还干货满满!太下饭了!”

—— 沃兹基硕德

本章目录:

Ⅰ.你好, 世界!(Hello World)

0x00 引入:HelloWorld 的由来

0x01 代码编辑器的选用(推荐 VS2022)

0x02 创建新项目

0x03敲下这 “跨越历史” 的 Hello World!

Ⅱ. 头文件(Header)

0x00引入:什么是头文件?

0x01标准输入输出库 stdio

0x02 引入头文件的方式

* 0x03 养成声明与定义分离的习惯

* 0x04头文件保护(防止头文件重复包含)

* 0x05 条件引用技术

* 0x06实践错误记录:#include 展开的问题

* 0x07技巧:学会定义 _GLOBAL_H 来管理头文件

* 0x08 整理:stdio 定义的库变量、库宏和库函数

Ⅲ. main 函数(Main Function)

0x00 引入:继续观察 HelloWorld 示例

0x01 什么是 main 函数?

0x02常规写法: int main() 和 int main(void)

* 0x03带参写法:int main(int argc, char *argv[])

* 0x04颇有争议的写法:void main()

* 0x05存在于 C89 的写法:main()

* 0x06错误写法:mian()

* 0x07main 函数执行前做的事

* 0x08 可以调用 main 函数吗?

Ⅳ. 深入浅出:Hello,World!

0x00 引入:再看 Hello,World!

0x01 函数体的概念(花括号匹配原则)

0x02简单介绍一下 printf 函数

0x03 分号,语句结束的标志!

0x04 返回语句 return

* 0x05 关于return 后面带括号的写法

0x06 深入浅出:Hello,World!

尾记:如何学习一门编程语言?


Ⅰ.你好, 世界!(Hello World)

  • 本章是首个章节,将通过计算机最经典的示例程序 Hello World 来展开我们的教程,考虑到 C 语言历史大家应该早已屡见不鲜,所以这里我们选择介绍 Hello World 的历史和由来。然后带着大家创建项目并敲下这最经典的代码。

0x00 引入:HelloWorld 的由来

“所有的伟大,都源于一个勇敢的开始!”

❓ 思考:什么是 Hello World ?它又是怎么来的?

Hello World 是一种常见的计算机程序,通常作为一个新编程语言或平台的入门示例。

​它的起源可以追溯到 1974 年,由计算机科学家布莱恩·柯林汉创造。

没错,就是那个80 岁还在咔咔咔写代码的巨佬,贝尔实验室的 神仙

​在C 语言第一本教材《C程序设计语言》中,

他使用了 Hello World 作为一个简单的示例,来介绍 C 语言的基本语法。

他与C 语言之父 ——丹尼斯里奇共同合作了撰写了这本书,K&R就是两人名字的缩写。

​同时,他还是开发 Unix 的主要贡献者,Unix 就是由柯林汉命名的!

“全员恶人?什么恶人?全员神仙!”

​可以这么说,当你把 Hello World 这几个字成功打印到屏幕上时,

你的内心体验到的不仅仅是一种成功的喜悦,更重要的是,你正在亲身经历一个跨越历史的时刻!

” 编程生涯,由此开始!”

Hello World 究竟从何而来?

当 Forbes India 杂志采访柯林汉时,他本人对自己这段传奇故事中一些记忆已经有点儿模糊了。当被问及为什么选择 “Hello, World! “时,他回答道:”我只记得,我好像看过一幅漫画,讲述一枚鸡蛋和一只小鸡的故事,在那副漫画中,小鸡说了一句 Hello World ” ——

0x01 代码编辑器的选用(推荐 VS2022)

​刚才我们了解了 Hello World 的故事,敲之前还需要做一些 “必要” 的准备!正所谓:

” 工欲善其事,必先利其器!”

我们先来对 “代码编辑器” 做一个简单的了解,我们这里指的 “编辑器” 是 集成开发环境 (IDE)

集成开发环境(即 IDE)是一种软件应用程序,提供了一个集成的开发环境,包括代码编辑器、编译器、调试器和其他开发工具,用于简化和加速软件开发的过程。IDE 通常用于软件开发,尤其是针对特定编程语言的开发,例如 Java、Python、C++ 等。IDE 的主要优点是提供了一个集成的工作流程,使得开发人员能够更加高效地编写、测试和调试代码。IDE通常具有自动代码完成、语法高亮、代码调试、版本控制等功能,可以大大提高开发效率和代码质量。常见的 IDE 包括 Visual Studio、Eclipse、和 IntelliJ IDEA 等。

​下面我先打开我的编辑器,我的代码编辑器是:

​大人!时代变了!怎么还有人在用 VC6.0啊!不会吧?

哈哈哈哈哈,怎么会!我用的可是支持脑机接口编程的VS2077

开个玩笑,其实是 VS2022!本系列博客 Windows 系统下一律选用 Visual Studio 2022

我们也是非常推荐大家使用 VS2022 的,臣以为 VS2022 真乃 宇宙最强编辑器 也!

” 强烈建议安装,用过都说好,下载链接我贴到下面了。”

VS 官网:Visual Studio 2022 IDE – 适用于软件开发人员的编程工具

学习阶段我们使用免费的 Community2022 版本 (社区版) 就完全够用了,大家可以自行下载。

关于为什么选用 VS


关于 Visual Studio 的安装,网上有很多很不错的教学视频,简易大家跟着视频去安装。安装过程还是没有什么难度的,喝杯咖啡的功夫就可以搞定了!和安装 Java 环境比真的容易太多,Java 还要手动配置环境,设置电脑环境变量等一系列杂七杂八的工作…… 而 Visual Studio非常人性化地一条龙服务,几乎帮你包揽了这一切!(还有个编辑器叫CLion,J家的开包即用的编辑器,但本专栏还是选用了VS)

此外,微软还有一个叫 Vscode 的神器,这个环境的准备就复杂得多,我们也不简易新手一上来就用 Vscode(虽然我当年系统性学C的时候就直接捣鼓了Vscode 环境),安装环境的过程是非常劝退人的,所以也不建议。值得一提的是,Vscode 属于轻量化的编辑器,并不是IDE!

当然有些学校可能会要求大家使用 Dev C++,那个也是个不错的编辑器,但是还是建议大家去使用Visual Studio,因为后续章节中会附带一些基础调试的教学,是基于Visual Studio 的,本专栏在 Window 下也都是采用Visual Studio 来给大家做教学演示的,所以为了更好地贴合本专栏进行系统性地学习,尽量选用Visual Studio!(ps:再多说一句:如果你学校推荐的编译器是 VC6.0 + 和用到包浆的 PPT,额…… )

* 当然,为了更方便大家系统性地学习,后期我会更新一篇 VS 的安装教程的。

​下面,我们将认为读者已经安装好了 VS2022,并带着大家手把手敲出 Hello,World!

注意:所以为了更好地贴合本专栏进行系统性地学习,尽量选用Visual Studio

0x02 创建新项目

首先,找到桌面快捷方式,双击打开Visual Studio 2022

(简约而不失优雅的加载页面…)

进入下方页面后,点击右下角的创建新项目 (N)

然后点击空项目

随后到了配置新项目,这里创建项目名称,演示阶段名字自行取,之后点击 创建 (C)

创建完新项目后,我们找到 解决方案资源管理器,右键 “源文件”,点击 “添加新建项” :

​名称我们就取为 test.c,然后点 添加(A),我们的工程就会在指定的位置很好地创建出来:

​这里的 .c 文件 和 .h 以及 VS 默认的 .cpp 后缀是什么?我们来简单介绍一下:

简而言之,.c文件用于 C 语言编程,.cpp文件用于 C++ 编程:

  • .c 是 C源代码文件的扩展名。
  • .cpp 是 C++ 源代码文件的扩展名。
  • .h 是头文件 (header file) ,包含在 C 或 C++ 程序中,用于声明函数、变量和数据结构等,以便在程序的其他部分中使用。

(这些我们在后期讲解 编译链接 时都会详细讲解,这里只需要 .c 是 C 语言源文件的后缀即可)

​创建完毕后,我们就可以在 test.c下打代码了:

​至此,我们要做的准备工作基本就完成了,下面我们就要开始敲 Hello,World 了!

0x03敲下这 “跨越历史” 的 Hello World!

​如果你是一个初学者,我们很能理解你迫切的想要敲出 Hello,World 的心情!

我们现在就拉出本教程的第一个代码 —— Hello,World,来让大家 一睹芳容 !

” 手动播放劲爆的新宝岛 bgm,敲下这令人激动的第一个代码!”

代码演示:C 语言经典示例程序 —— Hello,World

#include int main(){printf("Hello, World!\n");return 0;}

你可以手动将代码打到源文件中,也可以直接点击右上角 “复制” 按钮,将代码粘贴到源文件中。

下面我们准备运行这段代码,点击顶部菜单栏 调试 (D) → 开始执行(不调试)

​当然你也可以使用快捷键运行,输入Ctrl + F5运行代码:

我们也推荐大家使用!因为作为键盘侠,使用快捷键会很 cool !

运行后就会弹出一个窗口 ——Microsoft Visual Stidio调试控制台:

我们可以看到,我们的 Hello, World! 在控制台上出现了,这是因为我们调用了打印函数 printf

#include int main(){printf("Hello, World!\n"); 打印 Hello,Worldreturn 0;}

(如果是零基础读者,看不明白这段代码,这都没有关系,我们会深入浅出地慢慢讲解)

​只会敲 Hello World 的我瑟瑟发抖……

至此,我们的 Hello World 就敲完了,我们也成功的将这段话输出到了我们的显示器上。

​下面我们会慢慢地、深入浅出地讲解刚才的 Hello World 代码:

#include int main(){printf("Hello, World!\n");return 0;}

​等讲解完这些必要的知识点后,我们再回来看这段代码,就会非常地清楚 ~

Ⅱ. 头文件(Header)

  • 围绕着 HelloWorld 继续推进,理解第一行代码我们需要先知道什么是头文件,以及我们引入的 stdio 库,这里我们会详细的讲解各种头文件的知识点,包含了头文件保护、声明定义分离、#include 展开等内容。

0x00引入:什么是头文件?

​下面是 HelloWorld 示例程序的第一行代码:

#include // 引入头文件 stdio 库

* 注:两个斜杠 (//) 后面的内容属于「注释」,我们会在下一章进行讲解!

这一行代码的意思是 “引入头文件 stdio.h 库”,我们先来简单介绍一下什么是 头文件(header)。

概念:头文件,[英] header,[繁] 標頭檔

后缀为 .h 的文件被称为 “头文件” ,因为头文件的英文是 header,所以缩写为 .h

包含了 C 语言函数声明和宏定义,被多个源文件中引用共享。

头文件分为两种,一种是 “程序员自己编写的头文件”,另一种是 “编译器自带的头文件” 。

关于头文件的一些细节


在 C 语言中,头文件是一个文件。头文件通常是源代码的形式,由编译器在处理另一个源文件时自动包含进来。程序员通过编译器指令将头文件包含进其他源文件的开始。

一个头文件包含类、子程序、变量和其他标识符的前置声明,需要在一个以上源文件中生命的标识符可以放在一个头文件中,并在需要的地方包含该头文件。头文件作为一种包含功能函数、数据接口声明的载体文件,主要用于保存程序的声明。

早期的编程语言如 Basic、Fortran 并没有头文件的概念。

我们使用 #include 来引用头文件,这是一个 C 预处理指令,对于预处理的知识我们后期会讲解。

#include 引入头文件

我们的 Hello World 示例代码中,就是使用了 #include 来引用头文件的,引用的是 stdio.h

​这是什么?下面,我们就来介绍一下这个 stdio.h

0x01标准输入输出库 stdio

stdio.h 是编译器自带的头文件,其中 .h 我们刚才说过,是扩展名,被称之为头文件。

stdio 全称 standard input & output,即标准输入输出,我们称之为 C 标准库,直接看图记忆:

因为我们的 HelloWorld 示例程序需要用到 printf 这个函数,所以我们需要引入 stdio.h

​之所以叫做 stdio 是因为standard input & output,而它表示了这库中最经典的两个函数:

  • printf() :标准输入函数(input)
  • scanf() :标准输出函数(output)

(对于这两个函数,我们后续会详细讲解)

了解:stdio.h 的文件说明

/**stdio.h*This file has no copyright assigned and is placed in the Public Domain.*This file is a part of the mingw-runtime package.*Now arranty is given;refer to the file DISCLAIMER within the package.**Definitions of types and proto types of functions for *standard inputand output.**NOTE:The file manipulation functions provided by Microsoft seem to*work with either slash(/) or backslash(\) as the directory separator.**/

0x02 引入头文件的方式

​我们前面讲了,我们使用 #include 来引用头文件,这个 # 是预处理指令。

预处理指令 # 可用于宏定义、文件包含 和 条件编译。

刚才我们介绍了 stdio.h 库,现在我们再来观察HelloWorld 示例程序的第一行代码:

#include 

​我们可以看到 #include 后面使用了两个尖括号 包住了 stdio.h

尖括号中引入的头文件基本都是 C 语言中各种各样的库函数,刚才学习的 stdio 就是其中之一。

这行代码中,我们通过 #include 去引入了标准输入输出库 stdio。

头文件的引入除了尖括号 ,还有双引号 ” ” 形式的引入:

两种引用方式的区别:头文件用 引入和用 ” “ 引入的区别

​首先,” “ 包含头文件的本质区别是查找策略区别。

尖括号 的查找策略:直接去标准路径下去查找,用来引用标准库的头文件。

#include  // 编译器将从标准库目录开始搜索

双引号 ” “ 的查找策略:先在源文件所在的目录下查找。如果该头文件未找到,则在库函数的头文件目录下查找。如果仍然找不到,则提示编译错误。用来引用非标准库的头文件。

#include "文件" // 编译器将从用户的工作目录开始搜索

❓ 思考:那可不可以用双引号 ” “ 包含库文件?

#include "stdio.h"

​当然,库文件也是可以使用 ” “ 包含的,但是不建议库文件用 ” ” 形式引入!

#include 是库文件,你用引号形式引入 #include “stdio.h” 也是可以的,

但是这样查找的效率就会大打折扣,因为会先从源文件所在目录下去找一遍,然后再去找。

我们既然明确知道自己是要使用库文件了,我们自然就直接使用尖括号才是正常的。

​而且这样也不容易区分是库文件还是本地文件。

(看到尖括号引入就知道是库文件,看到引号引入的就知道是本地文件,还是很香的)

① Linux环境 标准头文件的路径:

/usr/include

② VS环境 标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

* 0x03 养成声明与定义分离的习惯

​要做到声明与定义相分离,头文件中应当只存放声明,而不存放定义。

为了养成模块化编程的好习惯,我们尽量把代码分开写,声明放到头文件中,定义放到源文件中。

VS 下,在解决方案资源管理器中:

  • 在 “头文件” 文件夹中右键取文件名以 .h 结尾即可创建头文件。
  • 在 “源文件” 文件夹中右键取文件名以 .c 结尾即可创建源文件。

C++ 语法中,类的成员函数可以在声明的同时被定义,并自动成为内联函数。这带来了书写上的便利,但也造成了风格的不一致,弊大于利。建议将成员函数的定义与声明分离开来。

* 0x04头文件保护(防止头文件重复包含)

“使用头文件保护技术,能够有效提升代码的一致性和可维护性……”

当需要引入多个头文件时,头文件引用条目达到十几条以上时,难免会出现重复引用的失误。

​比如下面这个代码,仔细观察你会发现不小心引入了两次 stdio:

#include #include #include #include #include #include #include #include #include  ← 引入两次了#include #include #include #include ...

这就是典型的重复引用错误,此时编译器会处理两次头文件的内容,从而产生错误。

为了防止这种情况的发生,经典的做法是将文件的整个内容都放到条件编译语句中。

具体做法是使用预处理指令 #ifndef#define#endif 来避免重复包含:

#ifndef STDIO_H#define STDIO_H// stdio.h 的内容#endif

这里我们使用了包装器 #ifndef,当再次引用头文件时条件为假,因为 STDIO_H 已经定义。

如此一来,预处理器会检查 STDIO_H 是否以及定义,如果没有定义就会进入 #ifndef 块内;

如果已经定义,此时预处理器会跳过文件的整个类容,编译器就会将其忽略不计。

​该技术我们称之为 头文件保护 (header guards) ,可以有效确保头文件只被包含一次。

⚡ 如果嫌麻烦,还有一种非常简单的方法:

#pragma once // 让头文件即使被包含多次,也只包含一份

(具体的讲解我们会在后面的预处理章节进行更加深刻细致的讲解)

因此,为了预防头文件被重复引入,建议使用 ifndef / define /endif 结构产生与处理块。

* 0x05 条件引用技术

​条件引用是一种根据特定条件来选择性包含或排除代码的技术。

如果需要从多个不同的头文件选择一个引用到程序中,此时就需要用”有条件引用” 来达成目的。

举个例子,现在我们需要指定不同的操作系统上使用的配置参数:

#if SYSTEM_1 # include "system_1.h"#elif SYSTEM_2 # include "system_2.h"#elif SYSTEM_3 ...#endif

当头文件较多时,此这么做显然不妥,我们可以预处理器使用宏来定义头文件的名称。

这种方法就是 有条件引用,不用头文件的名称作为 #include 的直接参数,

而只需要用宏名称代替即可,因为 SYS_H 会拓展,预处理器会查找 sys_1.h

 #define SYS_H "sys_1.h" ... #include SYS_H

(SYSTEM_H 可通过 -D 选项被 Makefile 定义)

* 0x06实践错误记录:#include 展开的问题

#include 的展开是有特点的,下面我将通过一个实践中碰到的例子,来讲解展开的特点。

举个例子:下面我们手动定义了一颗二叉树(Queue.c)

​#define _CRT_SECURE_NO_WARNINGS 1#include "Queue.h"typedef char BTDataType;typedef struct BinaryTreeNode {struct BinaryTreeNode* left;struct BinaryTreeNode* right;BTDataType data;} BTNode;//#include "Queue.h"解决方案?/* 创建新节点 */BTNode* BuyNode(BTDataType x) {BTNode* new_node = (BTNode*)malloc(sizeof(BTNode));if (new_node == NULL) {printf("malloc failed!\n");exit(-1);}new_node->data = x;new_node->left = new_node->right = NULL;return new_node;}/* 手动创建二叉树 */BTNode* CreateBinaryTree() {BTNode* nodeA = BuyNode('A');BTNode* nodeB = BuyNode('B');BTNode* nodeC = BuyNode('C');BTNode* nodeD = BuyNode('D');BTNode* nodeE = BuyNode('E');BTNode* nodeF = BuyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left = nodeD;nodeC->left = nodeE;nodeC->right = nodeF;return nodeA;}

我们可以看到,我们 #include 引入了 Queue.h 文件,由于是我们的数据类型是 BTNode

​所以我们需要修改一下 Queue.h 中的 QueueDataType

#include #include #include #include typedef BTNode* QueueDataType;typedef struct QueueNode {struct QueueNode* next;QueueDataType data;} QueueNode;typedef struct Queue {QueueNode* pHead;QueueNode* pTail;} Queue;void QueueInit(Queue* pQ);//队列初始化void QueueDestroy(Queue* pQ); //销毁队列bool QueueIfEmpty(Queue* pQ); //判断队列是否为空void QueuePush(Queue* pQ, QueueDataType x); //入队void QueuePop(Queue* pQ); //出队QueueDataType QueueFront(Queue* pQ);//返回队头数据QueueDataType QueueBack(Queue* pQ); //返回队尾数据int QueueSize(Queue* pQ); //求队列大小

​此时令人瞠目结舌的问题就出现了,我们运行代码就会产生报错:

​我们来冷静分析一下这个报错,分析完你会发现分析了个寂寞……

报错说又缺少 ” { ” 又缺少 ” ) “的,这明显是胡说八道。

我们的编译器在这里就显得没有这么智能了,报的错都开始胡言乱语了。

❓ 思考:这里产生问题的原因是什么呢?(提示:想想编译器的原则)

编译器原则:编译器认识 int,是因为 int 是一个内置类型。但是 BTNode* 编译器并不认识,

就需要 “往上面” 去找这个类型。这里显然往上找,是找不到它的定义的,所以编译器会报错。

如果你要用这个类型,你就需要先定义这个类型。

test.c 文件中 #include “Queue.h” ,相当于把这里的代码拷贝过去了。

此时,由于 BTNode* 会在上面展开,导致找不到 BTNode*

❓ 思考:我把 #include 移到 定义类型的代码 的后面,可以解决问题吗?

可以!遗憾的是只能解决这里 typedef BTNode* 的问题,还有 Queue.c 里的问题……

​那我们该怎么做,才能彻底解决呢?

解决方案:使用前置声明。这样就不会带来问题了,满足了先声明后使用。

代码演示:使用 “前置声明” 修改后的 Queue.h

#include #include #include #include // 前置声明struct BinaryTreeNode;typedef struct BinaryTreeNode* QueueDataType;typedef struct QueueNode {struct QueueNode* next;QueueDataType data;} QueueNode;typedef struct Queue {QueueNode* pHead;QueueNode* pTail;} Queue;void QueueInit(Queue* pQ);//队列初始化void QueueDestroy(Queue* pQ); //销毁队列bool QueueIfEmpty(Queue* pQ); //判断队列是否为空void QueuePush(Queue* pQ, QueueDataType x); //入队void QueuePop(Queue* pQ); //出队QueueDataType QueueFront(Queue* pQ);//返回队头数据QueueDataType QueueBack(Queue* pQ); //返回队尾数据int QueueSize(Queue* pQ); //求队列大小

此时问题就解决了,头文件展开后不会引发报错问题。

* 0x07技巧:学会定义 _GLOBAL_H 来管理头文件

“有效避免头文件过多,乱七八糟,不方便管理的问题。”

​如果一个工程中有大量的 .h 文件 和 .c 文件,此时难免会很乱。

此时我们可以选择用一个 global.h 的头文件来把所有.h 文件 “打包” 起来,

在除 global.h 文件外的头文件中包含此文件,这样我们就可以有条不紊地管理好所有的头文件了。

#ifndef _GLOBAL_H#define _GLOBAL_H#include #include #include #include #include #include #include #include #include #include ...

* 0x08 整理:stdio 定义的库变量、库宏和库函数

专栏阅读贴士

当标题前出现 *(星号)时,则说明该部分知识点是面向对 C 语言已经有一定基础的读者阅读的,讲解时难免会穿插不属于本章范围的内容。如果你是一名初学者,可以选择跳过。

对 stdio 有过一些了解的读者可能会知道 stdio.h 定义了 3 个变量类型,以及一些宏和各种函数。

对于库中的变量、宏和函数,我们一般称之为 库变量库宏库函数

库变量 (Library Variable):

头文件在 stdio.h 中定义了 3 个变量类型,分别是FILEsize_tfpos_t 类型。

FILEsize_tfpos_t
  • FILE 类型:存储文件流信息的对象类型(文件章节我们会详细讲解)。
  • size_t 类型:无符号整数类型,为关键字sizeof 的返回结果。
  • fpos_t 类型:存储文件中任何位置的对象类型。

库宏(Libary Micro):以下是 stdio.h 中定义的宏,一共有 16 种,我们先简单介绍一下。

1. NULL

NULL:空指针常量的值。

2. BUFSIZ

BUFSIZ:该宏为整数,代表了 setbuf 函数使用的缓冲区大小。

3. EOF

EOF:文件结束标志 EndOfFile,表示文件结束的负整数,非常常见!后续我们学习 getchar 函数时会详细讲解。

4. FOPEN_MAX

FOPEN_MAX:该宏为整数,代表了系统可以同时打开的文件数量。

5. FILENAME_MAX

FILENAME_MAX:该宏为整数,代表了字符数组可以存储的文件名最大长度,如果事先没有任何限制,则该值为推荐的最大值。

6. L_tmpnam

L_tmpnam:该宏为整数,代表了字符数组可以存储的由 tmpnam 函数创建的临时文件名的最大长度。

7. _IOFBF8. _IOLBF9. _IONBF

拓展了带有特定值的整型常量表达式,适用于 setvbuf 的第三个参数。

10. SEEK_CUR11. SEEK_END12. SEEK_SET

fseek 函数中使用,用于在一个文件中定位不同的位置。

13. TMP_MAX

tmpnam 函数可生成的独特文件名的最大数量。

14. stderr15. stdin16. stdout

FILE 类型的指针,标准错误、标准输出和标准输出流。

库函数 (Libary Funciton):共有 42 种函数,我们会在后续章节将一些常用的函数进行介绍。

Ⅲ. main 函数(Main Function)

  • 介绍完头文件后,下面我们将讲解 C 程序中最为重要的 main 函数,掌握 main 函数的特点与特性,探讨 main 函数的各种写法形式,并对这些写法进行说明讲解。

0x00 引入:继续观察 HelloWorld 示例

​刚才我们讲解了 #include ,下面我们来看第三行代码,int main()

#include int main(){printf("Hello, World!\n");return 0;}

首先,int 表示整型,这里表示 main 函数的返回值为整型。(下一章我们会讲解数据类型)

*[注]int 是英文单词integer 的缩写,意思是 “整数”。

int后面跟着的 main 就表示了 main 函数,我们先了解下什么是 函数 (function) :

这里的函数可不是数学中的函数,在 C 语言中,函数是一种可重复使用的代码块,用于执行特定的任务。它是程序的基本组成部分,用于模块化和组织代码,使程序更易于理解、调试和维护。

0x01 什么是 main 函数?

概念:main 函数,又称主函数,每个 C 程序都必须有一个 main 函数。

#include int main()程序从这里执行{return 0;}

主函数是程序执行的起点,你的程序会从这里开始执行,每次执行程序都从 main 函数开始。

一个工程中可以有多个 .c 文件,但多个 .c 文件中只能有一个 main 函数,main 函数具有唯一性。

“一个程序不能没有主函数,就像山东不能没有曹县!”

铁律:main函数是程序的入口,主函数有且仅有一个!

一个 C 语言程序的执行,从 main 函数开始,到 main 函数结束。 如何理解呢?

你可以把 main 函数想象成入口,一切都从这里开始执行,最后肯定也是要从这出去的。

​main 函数的返回类型取决于 main 标识符前是什么数据类型,比如这里是 int:

int main() {return 0;}

那么该主函数就存在一个整型类型的返回值,我们需要使用 return去返回(我们后续再说)。

在我们的演示代码 HelloWorld 中,我们可以看到,main 后面跟了一个括号,

括号中定义了函数的参数类型,而我们演示代码中,括号中是什么都没有的。

​我们可以在括号里面加上一个 void,就像这样:

int main(void)

void 也是数据类型,表示无类型,演示代码中为了演示简单化,我们把括号中的 void 省略掉了。

这里 在括号中加上 void 表示函数不接受任何参数, 后面接上的花括号,就是 函数体 了:

#include int main(void)// 主函数的返回类型是 int,主函数 void 不接受任何参数{printf("Hello, World!\n");return 0; // 由于主函数要求返回整型,这里 return 0}

C 语言规定,在一个源程序中,main 函数的位置可以任意。

​下面我先列举一些大家常见的主函数形式,然后我们再慢慢评价。

约定俗成型(目前主流的写法):

int main()int main(void)

什么都不返回型(颇有争议):

void main()void main(void)

带参型(带参形式,标准的 main 函数,带两参数):

int main(int argc, char **argv)int main (int argc, char *argv[], char *envp[])

超级简单型(存在于 C89 标准中的写法,没有返回值,没有入参):

main()// Brian W. Kernighan 和 Dennis M. Ritchie 的经典巨著 // The C programming Language 用的就是 main()。

直接报错型(相信 90% 的程序员都犯过这个错误):

int mian()

0x02常规写法: int main() 和 int main(void)

” 约定俗成型,这应该是现在最普遍的写法了。”

​我们平时写 main 函数的时 main 函数参数一般都不写,即括号里什么都不填。

或者填上一个 void 表示主函数不接受任何参数:

int main()int main(void)

注意:值得注意的是,int main() 和 int main(void) 这两种写法是存在区别的!

  • int main():参数列表没有被 void 限制死,可以传入参数,程序需要返回值。
  • int main(void):不能传入参数,如果输入参数就会报错,程序必须要有返回值。

这两种写法中,更加建议加 void 的写法,因为是 C89/C99/C11 标准文档中提供的写法之一:

int main(void) { /* ... */ }

* 0x03带参写法:int main(int argc, char *argv[])

” VS下输入 main 然后回车后就能自动补全的标准写法 “

​该写法也是 C89/C99/C11 标准文档中提供的标准写法,也是很常见的一种写法:

int main(int argc, char *argv[]) { /* ... */ }

参数列表中,argc 是一个整数参数,表示传递给程序的命令行参数的数量。

argv是一个指向字符指针数组的指针,用于存储命令行参数的字符串。

  • argv[0]通常是程序的名称或路径。
  • argv[1]argv[argc-1]是传递给程序的其他命令行参数。

代码演示:int main(int argc, char *argv[])

#include int main(int argc, char *argv[]) {printf("命令行参数的数量:%d\n", argc);printf("第一个参数(程序名称):%s\n", argv[0]);for (int i = 1; i < argc; i++) {printf("参数 %d: %s\n", i, argv[i]);}return 0;}

* 0x04颇有争议的写法:void main()

​首先抛出一个事实 —— C/C++ 中从来没有定义过 void main()

C++ 之父本贾尼在他的主页上的 FAQ 中明确地写着 ——

The definition void main() {/*…*/} is not and never has been C++, nor has it even been C.

(void main从来就不存在于 C++ 或者 C)

void main 这种写法究竟规不规范?这个广为大家所知的void main 究竟是怎么回事?

void main()void main(void)

​光从语法角度上来看,C 语言里是可以编译运行 void main 的。

因为 C 中的函数只关注函数名,而关注其返回类型,在找入口函数时仅仅在找 “main” 而已。

从汇编角度来看,返回值即函数返回时寄存器 的值,因此 中总是有值的。

和变量一样,不主动赋值给的是随机数,因此void main 即使返回类型是 void,也是有返回值的。

很多人说这种写法都是 “谭C” 带的,事实上并非如此,因为早在上世纪就已然出现了这种写法。

“还有甩锅给谭C的,更是无稽之谈,难道微软和 Borland 的工程师也是跟谭C学的?”

—— 电脑博物馆站长

​当然,一些编译器void main 是可通过编译的,比如 VC6.0

有人说 void main 其实挺合理的,编写单片机的话你 return 给值给谁?

​此时出现了一个非常炸裂的回答:你可以不收,我不能不给。

哈哈哈哈,看来写代码也讲人情世故!return 什么无所谓,我就是要个态度 ~

最后,对 void main 感兴趣的朋友可以看此帖:

链接:C 语言的「void main」是怎么一代代传下来的? – 知乎

* 0x05存在于 C89 的写法:main()

​C89 标准中支持这种只丢一个 main() 的写法:

main()

C语言中是调用者负责清栈任务,因此 main 函数不写返回参数依然是可以的。

你看到有人直接扔一个main() 但是不报错,事实上也只是编辑器支持而已。

* 0x06错误写法:mian()

” 因为真的很有趣,所以我不得不提一提。”

​相信很多人都犯过这个错误,不小心把 main 打成了mian……

int mian(void)

相信很多程序员在职业生涯中都犯过这个错误,看了半天没看出问题到底出在哪里:

编译器只会去找 main,你不小心写成 mian 编译器会说没找到 main 函数的。

总结:我们只需老老实实地遵循 C 语言标准文档的两种写法,就行了。

int main(void) { /* ... */ }int main(int argc, char *argv[]) { /* ... */ }

​可以保障代码的通用性和可移植性,没有 bug,岂不美哉?

* 0x07main 函数执行前做的事

待更新……

* 0x08 可以调用 main 函数吗?

❓ 思考:用户可不可以调用 main 函数?

这个问题乍一看会让人觉得,不可以吧……main 函数怎么可以让用户调呢:

很多人都没有想过这个问题,但事实上是可以的,main 函数是可以递归的。

可以是可以,但是调用会一直输出,直至栈溢出(触发 StackOverflow )。

VS 下触发警告:warning C4717: “main”: 如递归所有控件路径,函数将导致运行时堆栈溢出。

代码演示:调用 main()

#include int main(){printf("柠檬叶子C\n");main();return 0;}

运行结果如下:

Ⅳ. 深入浅出:Hello,World!

  • 最后,我们讲讲Hello World 示例程序中剩下来的部分知识点,引出函数体的概念,简单介绍一下 printf 函数,谈论语句结束的标志(分号),最后讲解 return 语句。这些知识点讲完后,我们串联前面讲的内容,就能很清楚地理解 Hello World 了。

0x00 引入:再看 Hello,World!

” 通过这小小的 Hello,World 来展开我们的教学,岂不美哉?”

#include int main(void){printf("Hello, World!\n");return 0;}

​我们刚才讲解了头文件和 main 函数,

现在大家应该能理解 #include int main() 的含义了。

下面我们来细说一下 main() 后面接的花括号 { },所以我们先要介绍一下函数体的概念。

再介绍一下 printf 函数,以及 return 语句,我们的 Hello World 示例程序就可以 “浅出” 了。

0x01 函数体的概念(花括号匹配原则)

概念:定义一个函数功能的所有代码组成的整体,称为 函数体 (Function Body) 。

函数体是用花括号括起来的若干语句,他们完成了一个函数的具体功能。

(函数声明与函数体组成函数定义,我们会在函数章节详细讲解)

举个例子:比如下面的 main 函数下面跟着一对花括号,花括号内的内容就是函数体了。

#include int main(void){printf("Hello, World!\n");return 0;}

​一对花括号{ } 由 左花括号 { 和 右花括号 } 组成,左花括号匹配右花括号。

比如这里的 main 函数,其 函数体包括左花括号和与之匹配的右花括号之间的任何内容

0x02简单介绍一下 printf 函数

printf("Hello, World!\n");

printf 函数,该函数是stdio.h 中的库函数,因此在使用前需要使用 #include 引入 stdio.h

这里我们就简单介绍一下,printf 后面跟着 () ,括号中的内容用双引号括出,

双引号内的内容可以输出到 控制台,所以我们的 Hello,World 示例程序运行后控制台会显示:

所以,你可以把它叫做 “打印函数”,通过 printf 函数将 Hello,World 打印到了控制台上:

printf("需要打印到控制台的内容");

❓ 这里的 \n 为什么没有被打印出来?

因为 \n 是转义字符,用于换行,关于转义字符的知识我们会放到下一章进行系统性讲解。

0x03 分号,语句结束的标志!

​在我们日常生活中,”句号” 表示一段话的结束。

而在 C 语言中,语句的结束用 ;表示,分号是语句结束的标志,表示一条语句的结束。

​可以看到,我们的 printf() 函数后面就有一个 分号;

printf("Hello,World\n");加分号return 0; 加分号

当一条语句结束时,我们需要手动给它加上分号,表明该语句结束。

不加分号会报错,人难免是会忘的,如果忘记加了编译器会提示:

随着敲代码越来越熟练,忘记加分号的情况会越来越少的,就像你写作文不会忘记加句号一样。

遗漏分号会引发 C2143 号报错:error C2143: 语法错误: 缺少“;”

当然了,也不能在不该加分号的地方加分号!

如果你在 main() 后面直接加分号,那就表示语句直接结束了,后面的 { } 就会让编译器一脸懵……

所以,分号是初学者最容易犯错的地方,多用了会错少用了也会错。

但是不用担心,在后续学习过程中慢慢就会掌握了。(比如 do…while 语句 while 结尾要加分号)

0x04 返回语句 return

我们前面说了,因为我们的 main 函数的返回值类型定义的是 int,所以我们需要 return 一个值:

#include int main(void){printf("Hello, World!\n");return 0;}

概念:在C 语言中,return 用于表示函数的返回值,用来将函数的结果返回给调用它的程序。

return 返回值;

因此,这里我们 return 一个 0,表示程序正常退出。

* 0x05 关于return 后面带括号的写法

值得一提的是,return 后面 “不带括号” 和”带括号” 的写法,都是正确的。

return 表达式;✅ 常规写法return (表达式);✅ 也是可以的

但是为了简单起见,一般我们不去写,当然写了也不会错,这取决于个人的编程习惯。

比如下面这两行实际效果是一样的:

return 10 + 20;return (10 + 20);

我见过一些老的微软大佬写的代码里,return 是这么写的:

return( 0); // 括号直接顶着return

*举个例子:库函数 src/strlen.c

size_t __cdecl strlen (const char * str){const char *eos = str; while( *eos++ ) ; return( eos - str - 1 ); 表达式}

0x06 深入浅出:Hello,World!

现在,我们已经解释完所有 Hello,World 的细节了,并且把其中的知识点都讲的差不多了。

​现在我们再回到最初的起点,再次感受那个跨越历史的时刻……

#include int main(void){printf("Hello, World!\n");return 0;}

经过本章的学习后,你也应该会有一个大概的认识了。

我们通过这 Hello World 来展开了我们的教学专栏,介绍了头文件、主函数和一些基础概念。

​这样的开篇方式我们构思了很久很久,希望能得到大家的喜爱!

C 语言之父 丹尼斯里奇的曾言:

” C 诡异离奇,缺陷重重,却获得了巨大的成功。”

C 语言无疑是改变人类历史的最伟大发明之一……

在计算机蓬勃发展的今天,我们是站在了巨人的肩膀上,得以让我们看得更高,行的更远……

学习 C 语言的过程是漫长且枯燥的,一定要有一个非常强大的心理状态。

翁凯老师说:在计算机里头没有任何黑魔法!

学计算机一定要有一个非常强大的心理状态,计算机的所有东西都是人做出来的,别人能想的出来,我也一定能想得出来,在计算机的世界里没有任何的黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全搞明白的。

尾记:如何学习一门编程语言?

我的编程母语是易语言(一款中文编程语言),我的编程启蒙导师是觅风。

觅风老师在他的 《易语言视频教程》中说过:

” 兴趣是最好的老师。”

如果你对编程感兴趣,那么这条路及时很艰难,你也会有无穷无尽的力量。

其次就是多动手,代码眼看千遍不如手敲一遍,多动手去实操,毕竟:

” 纸上得来终觉浅,绝知此事要躬行。”

然后就是多看源码,看看高手是怎么写代码的,大有裨益。

最后,遇到问题要深入了解,要具备对编程知识的求知欲,对问题要有钻研精神。

  • 网制焦,实不严,没毕业,零经验,面八家,十offer,三万二,外包也,两万七,自研也,明日有,多面试。说寒冬,年年有,计算机,月过万,现不学,没机会,趁红利,狠赚笔。
 [ 笔者 ] 王亦优 | 雷向明 [ 更新 ] 2023.6.29(初稿)| 2023.7.1(审稿结束)❌ [ 勘误 ] /* 暂无 */ [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

参考文献:

  • – C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.
  • – Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
  • – 百度百科[EB/OL]. []. https://baike.baidu.com/.
  • – 维基百科[EB/OL]. []. https://zh.wikipedia.org/wiki/Wikipedia
  • – R. Neapolitan, Foundations of Algorithms (5th ed.), Jones & Bartlett, 2015.
  • – B. 比特科技. C/C++[EB/OL]. 2021[2021.8.31]
  • – 林锐博士. 《高质量C/C++编程指南》[M]. 1.0. 电子工业, 2001.7.24.
  • – 陈正冲. 《C语言深度解剖》[M]. 第三版. 北京航空航天大学出版社, 2019.
  • – 侯捷. 《STL源码剖析》[M]. 华中科技大学出版社, 2002.
  • – T. Cormen《算法导论》(第三版),麻省理工学院出版社,2009年。
  • – T. Roughgarden, Algorithms Illuminated, Part 1~3, Soundlikeyourself Publishing, 2018.
  • – J. Kleinberg&E. Tardos, Algorithm Design, Addison Wesley, 2005.
  • – R. Sedgewick&K. Wayne,《算法》(第四版),Addison-Wesley,2011
  • – S. Dasgupta,《算法》,McGraw-Hill教育出版社,2006。
  • – S. Baase&A. Van Gelder, Computer Algorithms: 设计与分析简介》,Addison Wesley,2000。
  • – E. Horowitz,《C语言中的数据结构基础》,计算机科学出版社,1993
  • – S. Skiena, The Algorithm Design Manual (2nd ed.), Springer, 2008.
  • – A. Aho, J. Hopcroft, and J. Ullman, Design and Analysis of Algorithms, Addison-Wesley, 1974.
  • – M. Weiss, Data Structure and Algorithm Analysis in C (2nd ed.), Pearson, 1997.
  • -A. Levitin, Introduction to the Design and Analysis of Algorithms, Addison Wesley, 2003. – A. Aho, J.
  • – E. Horowitz, S. Sahni and S. Rajasekaran, Computer Algorithms/C++, Computer Science Press, 1997.
  • – R. Sedgewick, Algorithms in C: 第1-4部分(第三版),Addison-Wesley,1998
  • – R. Sedgewick,《C语言中的算法》。第5部分(第3版),Addison-Wesley,2002