🔗 《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()
* 0x07 main 函数执行前做的事
* 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 <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}
你可以手动将代码打到源文件中,也可以直接点击右上角 "复制" 按钮,将代码粘贴到源文件中。
下面我们准备运行这段代码,点击顶部菜单栏 调试 (D) → 开始执行(不调试) :
当然你也可以使用快捷键运行,输入 Ctrl + F5 运行代码:
我们也推荐大家使用!因为作为键盘侠,使用快捷键会很 cool !
运行后就会弹出一个窗口 —— Microsoft Visual Stidio 调试控制台:
我们可以看到,我们的 Hello, World! 在控制台上出现了,这是因为我们调用了打印函数 printf :
#include <stdio.h>
int main()
{
printf("Hello, World!\n"); 👈 打印 Hello,World
return 0;
}
(如果是零基础读者,看不明白这段代码,这都没有关系,我们会深入浅出地慢慢讲解)
只会敲 Hello World 的我瑟瑟发抖……
至此,我们的 Hello World 就敲完了,我们也成功的将这段话输出到了我们的显示器上。
下面我们会慢慢地、深入浅出地讲解刚才的 Hello World 代码:
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}
等讲解完这些必要的知识点后,我们再回来看这段代码,就会非常地清楚 ~
Ⅱ. 头文件(Header)
- 围绕着 HelloWorld 继续推进,理解第一行代码我们需要先知道什么是头文件,以及我们引入的 stdio 库,这里我们会详细的讲解各种头文件的知识点,包含了头文件保护、声明定义分离、#include 展开等内容。
0x00 引入:什么是头文件?
下面是 HelloWorld 示例程序的第一行代码:
#include <stdio.h> // 引入头文件 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 <stdio.h>
我们可以看到 #include 后面使用了两个尖括号 < > 包住了 stdio.h。
尖括号中引入的头文件基本都是 C 语言中各种各样的 库函数,刚才学习的 stdio 就是其中之一。
这行代码中,我们通过 #include <> 去引入了标准输入输出库 stdio。
头文件的引入除了尖括号 < > ,还有双引号 " " 形式的引入:
🔍 两种引用方式的区别:头文件用 < > 引入和用 " " 引入的区别
首先,< > 和 " " 包含头文件的本质区别是查找策略区别。
① 尖括号 < > 的查找策略:直接去标准路径下去查找,用来引用标准库的头文件。
#include <文件> // 编译器将从标准库目录开始搜索
② 双引号 " " 的查找策略:先在源文件所在的目录下查找。如果该头文件未找到,则在库函数的头文件目录下查找。如果仍然找不到,则提示编译错误。用来引用非标准库的头文件。
#include "文件" // 编译器将从用户的工作目录开始搜索
❓ 思考:那可不可以用双引号 " " 包含库文件?
#include "stdio.h"
当然,库文件也是可以使用 " " 包含的,但是不建议库文件用 " " 形式引入!
#include <stdio.h> 是库文件,你用引号形式引入 #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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cstdio>
#include <memory>
#include <cstdlib>
#include <random>
#include <cstdio>
#include <stdio.h> ← 引入两次了
#include <utility>
#include <cstring>
#include <vector>
#include <functional>
...
这就是典型的重复引用错误,此时编译器会处理两次头文件的内容,从而产生错误。
为了防止这种情况的发生,经典的做法是将文件的整个内容都放到条件编译语句中。
具体做法是使用预处理指令 #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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
// 前置声明
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 <cstdio>
#include <memory>
#include <cstdlib>
#include <random>
#include <cstdio>
#include <utility>
#include <cstring>
#include <vector>
#include <iostream>
#include <functional>
...
* 0x08 整理:stdio 定义的库变量、库宏和库函数
📃 专栏阅读贴士
当标题前出现 *(星号)时,则说明该部分知识点是面向对 C 语言已经有一定基础的读者阅读的,讲解时难免会穿插不属于本章范围的内容。如果你是一名初学者,可以选择跳过。
对 stdio 有过一些了解的读者可能会知道 stdio.h 定义了 3 个变量类型,以及一些宏和各种函数。
对于库中的变量、宏和函数,我们一般称之为 库变量、库宏 和 库函数。
① 库变量 (Library Variable):
头文件在 stdio.h 中定义了 3 个变量类型,分别是 FILE,size_t 和 fpos_t 类型。
FILE
size_t
fpos_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. _IOFBF
8. _IOLBF
9. _IONBF
拓展了带有特定值的整型常量表达式,适用于 setvbuf 的第三个参数。
10. SEEK_CUR
11. SEEK_END
12. SEEK_SET
在 fseek 函数中使用,用于在一个文件中定位不同的位置。
13. TMP_MAX
tmpnam 函数可生成的独特文件名的最大数量。
14. stderr
15. stdin
16. stdout
FILE 类型的指针,标准错误、标准输出和标准输出流。
③ 库函数 (Libary Funciton):共有 42 种函数,我们会在后续章节将一些常用的函数进行介绍。
Ⅲ. main 函数(Main Function)
- 介绍完头文件后,下面我们将讲解 C 程序中最为重要的 main 函数,掌握 main 函数的特点与特性,探讨 main 函数的各种写法形式,并对这些写法进行说明讲解。
0x00 引入:继续观察 HelloWorld 示例
刚才我们讲解了 #include <stdio.h>,下面我们来看第三行代码,int main()
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}
首先,int 表示整型,这里表示 main 函数的返回值为整型。(下一章我们会讲解数据类型)
* [注] int 是英文单词 integer 的缩写,意思是 "整数"。
而 int 后面跟着的 main 就表示了 main 函数,我们先了解下什么是 函数 (function) :
这里的函数可不是数学中的函数,在 C 语言中,函数是一种可重复使用的代码块,用于执行特定的任务。它是程序的基本组成部分,用于模块化和组织代码,使程序更易于理解、调试和维护。
0x01 什么是 main 函数?
📚 概念:main 函数,又称主函数,每个 C 程序都必须有一个 main 函数。
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
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,岂不美哉?
* 0x07 main 函数执行前做的事
待更新……
* 0x08 可以调用 main 函数吗?
❓ 思考:用户可不可以调用 main 函数?
这个问题乍一看会让人觉得,不可以吧……main 函数怎么可以让用户调呢:
很多人都没有想过这个问题,但事实上是可以的,main 函数是可以递归的。
可以是可以,但是调用会一直输出,直至栈溢出(触发 StackOverflow )。
VS 下触发警告:warning C4717: “main”: 如递归所有控件路径,函数将导致运行时堆栈溢出。
💬 代码演示:调用 main()
#include <stdio.h>
int main()
{
printf("柠檬叶子C\n");
main();
return 0;
}
🚩 运行结果如下:
Ⅳ. 深入浅出:Hello,World!
- 最后,我们讲讲 Hello World 示例程序中剩下来的部分知识点,引出函数体的概念,简单介绍一下 printf 函数,谈论语句结束的标志(分号),最后讲解 return 语句。这些知识点讲完后,我们串联前面讲的内容,就能很清楚地理解 Hello World 了。
0x00 引入:再看 Hello,World!
" 通过这小小的 Hello,World 来展开我们的教学,岂不美哉?"
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
我们刚才讲解了头文件和 main 函数,
现在大家应该能理解 #include <stdio.h> 和 int main() 的含义了。
下面我们来细说一下 main() 后面接的花括号 { },所以我们先要介绍一下函数体的概念。
再介绍一下 printf 函数,以及 return 语句,我们的 Hello World 示例程序就可以 "浅出" 了。
0x01 函数体的概念(花括号匹配原则)
📚 概念:定义一个函数功能的所有代码组成的整体,称为 函数体 (Function Body) 。
函数体是用花括号括起来的若干语句,他们完成了一个函数的具体功能。
(函数声明与函数体组成函数定义,我们会在函数章节详细讲解)
💭 举个例子:比如下面的 main 函数下面跟着一对花括号,花括号内的内容就是函数体了。
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
经过本章的学习后,你也应该会有一个大概的认识了。
我们通过这 Hello World 来展开了我们的教学专栏,介绍了头文件、主函数和一些基础概念。
这样的开篇方式我们构思了很久很久,希望能得到大家的喜爱!
C 语言之父 丹尼斯里奇 的曾言:
" C 诡异离奇,缺陷重重,却获得了巨大的成功。"
C 语言无疑是改变人类历史的最伟大发明之一……
在计算机蓬勃发展的今天,我们是站在了巨人的肩膀上,得以让我们看得更高,行的更远……
学习 C 语言的过程是漫长且枯燥的,一定要有一个非常强大的心理状态。
👑 翁凯老师说:在计算机里头没有任何黑魔法!
学计算机一定要有一个非常强大的心理状态,计算机的所有东西都是人做出来的,别人能想的出来,我也一定能想得出来,在计算机的世界里没有任何的黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全搞明白的。
尾记:如何学习一门编程语言?
我的编程母语是易语言(一款中文编程语言),我的编程启蒙导师是觅风。
觅风老师在他的 《易语言视频教程》中说过:
" 兴趣是最好的老师。"
如果你对编程感兴趣,那么这条路及时很艰难,你也会有无穷无尽的力量。
其次就是多动手,代码眼看千遍不如手敲一遍,多动手去实操,毕竟:
" 纸上得来终觉浅,绝知此事要躬行。"
然后就是多看源码,看看高手是怎么写代码的,大有裨益。
最后,遇到问题要深入了解,要具备对编程知识的求知欲,对问题要有钻研精神。
|
📌 [ 笔者 ] 王亦优 | 雷向明
📃 [ 更新 ] 2023.6.29(初稿)| 2023.7.1(审稿结束)
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
📜 参考文献:
|