C语言概述

读懂C程序的基本结构——main函数/头文件/printf/变量声明/函数调用,手写并运行第一个多文件C程序

学习目标

  • 能逐行解释一个最小 C 程序的每一行在干什么,以及缺了任何一行会怎样
  • 能声明变量、选对类型、用 printf%d/%f/%s 正确输出值——写出完整可编译运行的程序
  • 能把一个单文件程序拆成两个 .c 文件,写出完整的编译命令并成功生成可执行文件——能回答:gcc -c 和直接 gcc a.c -o a 的区别是什么?多文件编译的链接阶段干了什么?

你写的代码,是怎么变成电脑能跑的程序的?

上一章你看到了整条工厂流水线的全貌:四道工位把 .c 加工成可执行文件。这一章,我们走进流水线的第一道工序——你敲的是什么?

你敲的第一段 C 代码长什么样?它由几个「零件」搭起来,每个零件有它自己的任务。缺了哪个零件,要么编译器报错不让你过,要么跑出来的结果不对。

这一章就带你逐行拆解一个最小的 C 程序——顺便亲手写出第一个多文件程序。不会写代码、看不懂别人写的代码,你就永远是站在流水线外面看热闹的人。

最小 C 程序的结构解剖

把你看到的第一个 C 程序拿出来,逐段标清楚:头文件是干什么的、main 函数什么角色、花括号管哪块、printf 怎么输出、return 给谁看。

一个完整的 C 程序,长这样

先看一眼完整的:

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

下面用一张图把这段程序的每一个关键点标出来。每一处气泡都是后面会展开讲的知识点——带着这张图往下读。

hello.c1#include <stdio.h>3int main(void) {4 printf("Hello, World!\n");5 return 0;6}#include:把 stdio.h 内容粘贴到这里main 函数:程序从这里开始执行printf:输出字符串;\n 是换行return 0:程序正常结束最小 C 程序:include 引入能力、main 定义入口、printf 输出文字、return 返回状态
一个最简 C 程序的四个关键部分:头文件声明能力、main 函数指定入口、 printf 把文字送到控制台、return 返回执行状态给操作系统。

现在逐行解释:

  1. <Term def="预处理包含指令,使指定标准头提供的声明和宏在当前翻译单元后续位置可见。程序依赖的是头文件接口,不应假设实现只是原样文本粘贴。">#include &lt;stdio.h&gt;</Term>——让标准输入输出接口在当前翻译单元可见。stdio.h 声明 printf 等函数;使用接口的源文件应直接包含对应头。

  2. <Term def="每个 C 程序的入口函数。操作系统加载你的可执行文件后,第一件事就是跳进 main 函数开始执行。int 是返回值类型,void 表示这个函数不接受命令行参数。">int main(void)</Term>——程序入口。int 是返回值类型(交一个整数给操作系统),void 表示 main 不接受命令行参数。操作系统运行你的程序时,第一件事就是钻进来找 main

  3. { } 花括号——把函数体裹住。左花括号 { 表示「main 函数从这里开始」,右花括号 } 表示「main 函数到此结束」。中间的所有语句是 main 函数要干的活。

  4. <Term def="C 标准库的输出函数。把格式字符串里的普通文字原样输出,把 % 打头的转换说明替换成后面参数的值,然后送到标准输出(通常是屏幕)。">printf()</Term>——C 语言里最常用的输出函数。括号里是一段字符串,双引号裹住。\n 是个特殊的「转义序列」,代表换行——让光标跳到下一行开头。

  5. <Term def="函数结束时的语句,把 return 后面整数交给操作系统。0 表示「正常结束」,非零通常表示出了某种错误。">return 0;</Term>——返回语句。0 是一个整数值,交给操作系统,告诉它「程序一切正常,跑完了」。

变量与赋值:程序怎么「记住」东西

程序需要记住数据——用户的年龄、圆周率、一个名字。这些数据住在对象中,你通过 <Term def="由声明引入的具名对象;类型决定允许的操作、表示范围、对齐和存储需求,具体字节数可能随 C 实现和目标平台变化。">变量</Term> 来访问它们。

在 C 语言里使用变量要分两步:先「声明」,再「赋值」。

int num;        /* 声明:告诉编译器 num 是 int 类型 */
num = 10;       /* 赋值:把 10 存进 num 那块内存 */

也可以一步到位——声明的同时给一个初始值,这叫 <Term def="在声明变量的同时就给它一个初始值。int a = 5; 让 a 从出生就是 5,避免读到不确定的垃圾值。">初始化</Term>

int num = 10;       /* 声明 + 初始化:一步到位 */
float pi = 3.14f;
char grade = 'A';

未显式初始化的自动对象通常具有不确定值;在不满足标准例外的情况下读取这种值可能产生未定义行为。让变量在首次使用前取得有效值,并尽量缩短“已声明但尚未赋值”的区间。

下面这张图画了三个变量在内存里的样子:

栈内存 (stack)numint4 字节)= 10chchar1 字节)= 'A'pifloat4 字节)= 3.14每个变量在栈上占用一段连续的内存空间。int 和 float 通常占 4 字节,char 占 1 字节。
C 程序运行时,局部变量分配在栈(stack)上。不同类型占用的字节数不同: int 4 字节、float 4 字节、char 1 字节。变量名只是这块内存的"标签"。

每一种类型占用的字节数不同、能存的东西不同。int 存整数,float 存小数,char 存单个字符。选错类型就像拿称米的秤去称书——能秤,但结果可能不是你想要的。

printf():往屏幕上印东西

printf 的核心是一个「模板 + 填空」的机制。双引号里的文字大部分原样输出,但遇到 % 打头的地方——那是「转换说明」——就用后面参数的值来替换。

printf("num=%d, pi=%.2f, name=%s\\n", 10, 3.14, "Tom");格式段含义实际参数最终输出"num="普通文字:原样输出到屏幕num=%d转换说明:把 int 参数转成十进制整数字符串1010", pi="普通文字:原样输出 , pi=%.2f转换说明:把 float 参数转成小数,保留 2 位3.143.14", name="普通文字:原样输出 , name=%s转换说明:把 char* 字符串参数填入"Tom"Tom\n转义序列:输出一个换行符↲ 换行最终控制台输出:num=10, pi=3.14, name=Tom(\n 使光标换到下一行)
printf 的格式字符串就像填空题的"模板":普通文字原样输出, 百分号开头的转换说明(%d、%f、%s)被替换成后面参数的值。

三个最常用的转换说明:

转换说明替换类型示例输出
%dint 整数printf("%d", 42)42
%ffloat/double 浮点printf("%f", 3.14)3.140000
%schar* 字符串printf("%s", "Hi")Hi

除了 % 转换说明,还有一类叫 <Term def="反斜杠 \ 开头的特殊字符组合,在输出中代表一个不可见的控制字符——比如 \n 换行、\t 制表符、\ 打出反斜杠本身。">转义序列</Term>\n 不是把 \n 两个字印出来,而是告诉 printf:「这里换一行」。

printf("Line1\nLine2\n");
// 输出:
// Line1
// Line2

注释:写给人类看的标记

C 语言有两种注释方式。在翻译早期,注释会被替换为空白;它们用于源码说明,不形成运行时语句。

/* 这是多行注释——可以跨行,裹住的部分全部被忽略 */
 
int x = 10;  // 这是单行注释——从 // 到本行末尾全部被忽略

写注释的三条建议:① 写为什么这样做而不是代码干了什么(代码本身已经说了干什么)。② 变量名起好就是最好的注释——int daysPerYear = 365 不需要再注释「这是每年的天数」。③ 过时的注释比没注释更危险——把注释当代码的一部分维护。

动手:把两个 .c 文件编译成一个可执行文件

真实项目里,你不会把所有代码塞进一个大文件——你得拆成多个 .c,每个负责一块功能,最后拼起来。下面用 Stepper 一步步看这个「拆开→各编各的→拼起来」的过程。

猜一猜:编译 main.c 时,如果它调用了 helper.c 里的函数——但 main.c 没有 #include "helper.c"——编译器会报错吗?切到第①步找答案。

分步1 / 3

① 编译 main.c:只翻译自己,不关心别人

main.c主程序源码1gcc -cmain.o目标文件(零件)helper.c辅助函数源码2gcc -chelper.o目标文件(零件)3链接器 (linker)拼零件,解符号,生成可执行文件app可执行文件 ✓① 编译 main.c:gcc -c main.c -o main.o,产出函数的机器码,但函数调用还是"空的"
第①步:把 main.c 单独编译成 main.o。此时 main 里对 helper 函数的调用还是「未解决」的符号,链接时才填地址。

main.c 单独编译成 main.o

gcc -c main.c -o main.o

-c 表示完成当前翻译单元的预处理、编译和汇编,但不链接。如果 main.c 调用 greet(),调用点之前必须有兼容声明;C99 起隐式函数声明违反约束,符合标准的实现必须给出诊断,现代工具链常直接按错误处理。声明正确但最终没有任何定义时,才在链接阶段报告未定义引用。

代码逐段拆解:手写你的第一个多文件 C 程序

下面从头开始,手写一个完整的多文件程序。这段程序没有在虚讲——每一行都能在你的电脑上跑。 拆成两块:main.c 负责程序入口和主流程,helper.c 负责提供一个辅助函数。

主程序:main.c

#include <stdio.h>
 
void greet(void);    /* 函数声明——告诉编译器「后面有 greet 这么个函数」 */
 
int main(void)
{
    int count = 3;
    float price = 9.99f;
 
    printf("count = %d\n", count);
    printf("price = %.2f\n", price);
 
    greet();         /* 调用另一个文件里定义的函数 */
 
    return 0;
}

逐行解释:

  • #include <stdio.h>:让 printf 可用。
  • void greet(void);函数声明(原型)。告诉编译器「存在一个名叫 greet 的函数,它没有返回值(void)、不需要参数(void)」。你在本章就要开始理解「声明」和「定义」是两件事——声明只是承诺有这个东西,定义才是真把它写出来。这里的声明告诉编译器不要因为没有 greet 的定义就报错——链接的时候会找到它。
  • int count = 3;:声明 + 初始化一个 int 变量。
  • float price = 9.99;:声明 + 初始化一个 float 变量。float 和 int 在内存里存法完全不同——float 用 IEEE 754 浮点数格式,小数点可以漂移。
  • printf("count = %d\n", count);%d 换成 count 的值(3),输出 count = 3 然后换行。
  • printf("price = %.2f\n", price);%.2f 把 price 保留 2 位小数输出——price = 9.99
  • greet();:调用另一个文件里的函数。链接器会去 helper.o 里找 greet 的实现。
  • return 0;:程序正常结束。

辅助函数:helper.c

#include <stdio.h>
 
void greet(void)
{
    printf("Hello from helper.c!\n");
}

这个文件很简单——只定义了一个函数 greet。注意两点:① 它自己也 #include <stdio.h>(每个 .c 文件独立编译,不共享别人的 include)。② 函数名后面是 { } 里写具体干的事(定义),而不是分号结尾(声明才用分号)。声明只承诺名字和类型,定义才把具体做的事写出来。

编译并跑起来

# 方法一:一步到位(推荐日常用)
gcc main.c helper.c -o app
./app
 
# 方法二:分步来(理解流程用)
gcc -c main.c -o main.o
gcc -c helper.c -o helper.o
gcc main.o helper.o -o app
./app

输出:

count = 3
price = 9.99
Hello from helper.c!

注释怎么写

注释是写给六个月后的你(或同事)看的。好的注释解释意图,不描述代码:

/* 把华氏度转成摄氏度:公式 C = 5.0 / 9.0 * (F - 32) */
float f_to_c(float f) {
    return 5.0 / 9.0 * (f - 32);
}
 
int x = 0;   // 计数器——累计读入的行数

容易踩的坑

小结

  • 一个最小 C 程序 = #include <stdio.h> + int main(void) { ... } + printf 语句 + return 0;
  • printf 用格式字符串——%d→int、%f→float、%s→字符串,\n 换行,普通文字原样输出
  • 变量先声明(定类型和名字),再初始化(给初值)。用之前必须已经有值——垃圾值不可预期
  • ///* */ 两种注释在预处理阶段被删掉,只给人看,不进二进制
  • 多文件编译:gcc -c 只编译不链接(出 .o),最后 gcc *.o -o app 链接拼接。undefined reference 出在链接阶段

练习

问题 1(改代码型)count 输出成 003,把 price 输出成带正负号的两位小数。查 printf 转换说明并运行验证。

问题 2(独立实现型) 自己从零建一个两文件项目:

  • 文件一 calc.c——定义两个函数:int add(int a, int b) 返回 a+b,int multiply(int a, int b) 返回 a*b。
  • 文件二 main.c——main 函数里调用 add(7, 8)multiply(4, 5),分别用 printf 输出结果。输出格式如 7 + 8 = 154 * 5 = 20
  • 编译成 calculator 程序并运行。
  • 额外挑战:把 addmultiply 的声明抽到一个头文件 calc.h 里,然后在 main.c#include "calc.h"。体会一下头文件怎么让小项目变干净。

问题 3(问答型) 如果用 /* */ 临时屏蔽一段已含块注释的代码,会发生什么?给出更稳妥的临时排除方法。

名词解释

本章出现的专业名词,用大白话再讲一遍。

#include <stdio.h>

让标准输入输出头提供的声明和宏在当前翻译单元后续位置可见的包含指令。使用 printfscanf 等接口的源文件应直接包含它。

int main(void)

每个 C 程序必须有的入口函数。操作系统启动你的程序后,第一个就执行 main 里面的代码。int 表示函数结束时交回一个整数(0=正常),void 表示 main 不接受命令行参数。详见本章「最小 C 程序的结构解剖」一节。

printf()

C 标准库提供的输出函数。你把要输出的文字写在一对双引号里,普通文字照原样输出,%d(整数)、%f(小数)、%s(字符串)这类转换说明被替换成后面参数的值。是你和程序对话的「嘴」。详见本章「printf():往屏幕上印东西」一节。

return 0;

函数体最后一行,把 return 后面的整数(0)交回给操作系统。0 表示「程序正常结束了,没出问题」。用非零值(如 return 1)可以表示程序遇到了某种错误退出。详见本章「最小 C 程序的结构解剖」一节。

变量

由声明引入的具名对象。类型决定操作、表示范围、对齐和存储需求,具体大小可能随实现和目标平台变化。

初始化

在对象开始生命周期时为它建立初始值。自动对象若没有初始化通常具有不确定值,读取前必须先沿所有控制路径赋予有效值。

转义序列

反斜杠 \ 开头的特殊字符组合,代表一个看不见的控制字符。\n=换行,\t=制表符(tab),\\=反斜杠自己,\"=双引号——当 printf 碰到它们,不输出字面上的 \n,而是执行对应的操作。详见本章「printf()」一节。

资料与写作方式声明

本章以C Primer Plus(第6版)权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…