《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 个变量类型,分别是FILE,size_t和 fpos_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 语言的过程是漫长且枯燥的,一定要有一个非常强大的心理状态。
翁凯老师说:在计算机里头没有任何黑魔法!
学计算机一定要有一个非常强大的心理状态,计算机的所有东西都是人做出来的,别人能想的出来,我也一定能想得出来,在计算机的世界里没有任何的黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全搞明白的。
尾记:如何学习一门编程语言?
我的编程母语是易语言(一款中文编程语言),我的编程启蒙导师是觅风。
觅风老师在他的 《易语言视频教程》中说过:
” 兴趣是最好的老师。”
如果你对编程感兴趣,那么这条路及时很艰难,你也会有无穷无尽的力量。
其次就是多动手,代码眼看千遍不如手敲一遍,多动手去实操,毕竟:
” 纸上得来终觉浅,绝知此事要躬行。”
然后就是多看源码,看看高手是怎么写代码的,大有裨益。
最后,遇到问题要深入了解,要具备对编程知识的求知欲,对问题要有钻研精神。
|
[ 笔者 ] 王亦优 | 雷向明 [ 更新 ] 2023.6.29(初稿)| 2023.7.1(审稿结束)❌ [ 勘误 ] /* 暂无 */ [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
参考文献:
|