【C语言程序设计】第3篇:C程序基本结构分析
1 引言:从Hello World开始
学习任何编程语言,通常都是从"Hello, World!"程序开始的。下面是一个最简单的C语言程序:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
这个程序虽然只有几行代码,却包含了C语言程序的所有核心构成要素:
预处理指令(#include <stdio.h>)
函数(main函数)
函数调用(printf)
语句(printf("Hello, World!\n");和return 0;)
代码块(由{ }括起来的部分)
本章将逐一剖析这些要素。
2 预处理指令
2.1 预处理指令的基本概念
在上一章中,我们已经了解到C程序的编译过程包含预处理阶段。预处理指令就是在这一阶段被处理的特殊命令,它们以井号#开头,通常放在程序的开头。
预处理指令不是C语言本身的组成部分,而是指导预处理器如何对源代码进行加工的指令。它们在编译之前被处理,处理结果才进入后续的编译阶段。
2.2 #include指令
#include是C语言中最常用的预处理指令,它的作用是将指定文件的内容插入到当前文件的#include指令所在位置。
2.2.1 两种包含形式
#include指令有两种使用形式:
形式一:尖括号形式
#include <文件名>
形式二:双引号形式
#include "文件名"
这两种形式的区别在于搜索头文件的路径不同:
| 形式 | 搜索路径 | 典型用途 |
|---|---|---|
#include <文件名> |
在系统指定的标准头文件目录中搜索 | 包含标准库头文件(如stdio.h) |
#include "文件名" |
先在当前源文件所在目录搜索,找不到再到系统目录搜索 | 包含用户自定义的头文件 |
2.2.2 头文件的作用
头文件(通常以.h为扩展名)主要包含以下内容:
函数声明:告诉编译器某个函数的存在及其参数类型、返回类型
宏定义:使用#define定义的常量或宏
类型定义:如struct、union、enum等自定义类型的定义
全局变量声明:使用extern声明的外部变量
以stdio.h为例,它包含了标准输入输出库函数的声明,如printf、scanf、getchar、putchar等。
2.2.3 头文件包含的实质
假设有一个头文件myheader.h内容如下:
/* myheader.h */
#define PI 3.14159
int add(int a, int b);
当我们在源文件中写:
#include "myheader.h"
预处理后,#include "myheader.h"这行会被替换为myheader.h的全部内容,相当于:
/* myheader.h的内容开始 */
#define PI 3.14159
int add(int a, int b);
/* myheader.h的内容结束 */
2.2.4 避免重复包含
当头文件被多次包含时,可能会导致重复定义错误。常用的解决方法是使用条件编译指令:
c
/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H
#define PI 3.14159
int add(int a, int b);
#endif
这种模式被称为包含守卫(Include Guard),它确保头文件的内容只被处理一次。
2.3 其他常用预处理指令
除了#include,C语言还有其他重要的预处理指令,下表简要列举:
| 指令 | 用途 | 示例 |
|---|---|---|
#define |
定义宏或常量 | #define MAX 100 |
#undef |
取消宏定义 | #undef MAX |
#if |
条件编译 | #if MAX > 50 |
#ifdef |
如果宏已定义 | #ifdef DEBUG |
#ifndef |
如果宏未定义 | #ifndef HEADER_H |
#else |
与#if配合 |
#else |
#elif |
否则如果 | #elif MAX == 100 |
#endif |
结束条件编译 | #endif |
#error |
输出错误信息 | #error "Version mismatch" |
#pragma |
编译器特定指令 | #pragma once |
这些指令将在后续章节中详细介绍。
3 main函数的本质
3.1 main函数的定义形式
main函数是C程序的入口点——程序从main函数开始执行。ANSI标准规定了两种主要的main函数定义形式:
形式一:无参数形式
int main(void)
{
/* 函数体 */
return 0;
}
形式二:带参数形式
int main(int argc, char *argv[])
{
/* 函数体 */
return 0;
}
其中:
argc(argument count):命令行参数的个数
argv(argument vector):指向命令行参数字符串的指针数组
3.2 main函数的返回类型
main函数的返回类型是int,表示它返回一个整数值给操作系统。
3.2.1 返回值的作用
main函数的返回值用于向操作系统报告程序的执行状态:
返回0:通常表示程序成功执行
返回非0:通常表示程序执行出错,具体数值可表示不同错误类型
在批处理脚本或Shell脚本中,可以通过检查程序的返回值来判断执行结果。
3.2.2 返回值的使用示例
#include <stdio.h>
int main(void)
{
int num;
printf("请输入一个正数:");
scanf("%d", &num);
if (num <= 0) {
printf("错误:输入的不是正数\n");
return 1; /* 返回1表示错误 */
}
printf("您输入的是:%d\n", num);
return 0; /* 返回0表示成功 */
}
在Linux/Unix系统中,可以通过echo $?查看上一个程序的返回值:
bash
$ ./a.out
请输入一个正数:5
您输入的是:5
$ echo $?
0
$ ./a.out
请输入一个正数:-3
错误:输入的不是正数
$ echo $?
1
3.3 main函数的参数
3.3.1 参数的含义
带参数的main函数允许程序接收命令行传递的参数:
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("参数个数:%d\n", argc);
for (i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
3.3.2 参数传递示例
假设将上述程序编译为test,在命令行执行:
$ ./test hello world 123
输出:
text
参数个数:4
argv[0] = ./test
argv[1] = hello
argv[2] = world
argv[3] = 123
重要说明:
argv[0]通常是程序本身的名称
argv[argc]是一个空指针(NULL)
参数之间以空白字符(空格、制表符等)分隔
如果参数本身包含空格,需要用引号括起来
3.4 main函数的调用者
一个常见的问题是:main函数由谁调用?
答案是:main函数由操作系统调用。当程序被执行时,操作系统负责加载程序到内存,然后调用main函数,程序开始运行。当main函数返回时,控制权交还给操作系统。
3.5 标准允许的其他main形式
虽然标准规定了上述两种形式,但某些编译器可能允许其他变体:
/* 省略返回类型(不推荐) */
main() /* 默认返回int */
{
/* ... */
}
/* void返回类型(不符合标准) */
void main() /* 某些系统允许,但不推荐 */
{
/* ... */
}
为了代码的可移植性,始终使用标准规定的形式:int main(void)或int main(int argc, char *argv[])。
4 标准输入输出库函数
4.1 标准库函数的概念
C语言本身只提供了核心语言特性(如数据类型、运算符、控制结构等),而输入输出操作并不属于语言的核心部分。C语言通过标准库(Standard Library)提供了丰富的函数,用于执行各种常见任务。
标准库函数是一组预先编写好的函数,它们被编译成目标代码,存放在库文件中。当我们使用#include指令包含相应的头文件后,就可以在程序中调用这些函数。
4.2 标准输入输出库
标准输入输出库(Standard Input Output Library)是C语言最常用的库,由头文件stdio.h声明。它提供了与输入输出相关的函数、宏和类型定义。
4.2.1 标准输入输出的概念
在C程序中,有三个预定义的标准流(Standard Streams):
| 流名称 | 设备 | 默认关联 | 用途 |
|---|---|---|---|
stdin |
标准输入 | 键盘 | 从键盘读取数据 |
stdout |
标准输出 | 屏幕 | 向屏幕输出数据 |
stderr |
标准错误 | 屏幕 | 向屏幕输出错误信息 |
这些流在程序启动时自动打开,在程序结束时自动关闭。
4.3 printf函数详解
printf(print formatted)是C语言中最常用的输出函数,用于将格式化的数据输出到stdout。
4.3.1 函数原型
int printf(const char *format, ...);
参数:
format:格式控制字符串,指定输出的格式
...:可变参数列表,表示要输出的数据
返回值:
成功:返回输出的字符数
失败:返回负数
4.3.2 格式控制字符串
格式控制字符串包含两种成分:
普通字符:原样输出
格式转换说明符:以%开头,用于指定如何输出对应的参数
常用的格式转换说明符:
| 说明符 | 含义 | 示例 |
|---|---|---|
%d |
输出十进制整数 | printf("%d", 123); |
%i |
输出十进制整数(与%d同) |
printf("%i", 123); |
%u |
输出无符号十进制整数 | printf("%u", 123u); |
%o |
输出八进制整数 | printf("%o", 123); |
%x |
输出十六进制整数(小写) | printf("%x", 123); |
%X |
输出十六进制整数(大写) | printf("%X", 123); |
%f |
输出浮点数(小数形式) | printf("%f", 3.14159); |
%e |
输出浮点数(科学计数法,e小写) | printf("%e", 3.14159); |
%E |
输出浮点数(科学计数法,E大写) | printf("%E", 3.14159); |
%g |
自动选择%f或%e中较短的 |
printf("%g", 3.14159); |
%c |
输出单个字符 | printf("%c", 'A'); |
%s |
输出字符串 | printf("%s", "Hello"); |
%p |
输出指针地址 | printf("%p", &a); |
%% |
输出百分号%本身 |
printf("%%"); |
4.3.3 格式修饰符
在%和转换字符之间可以插入格式修饰符,控制输出的格式:
| 修饰符 | 含义 | 示例 |
|---|---|---|
- |
左对齐 | %-10d |
+ |
显示正负号 | %+d |
| 空格 | 正数显示空格,负数显示负号 | % d |
0 |
用0填充宽度 | %05d |
| 数字 | 最小字段宽度 | %10d |
.数字 |
精度(浮点数小数点后位数,字符串最大长度) | %.2f, %.5s |
h |
短整数(short) | %hd |
l |
长整数(long) | %ld |
ll |
长长整数(long long) | %lld |
4.3.4 printf使用示例
#include <stdio.h>
int main(void)
{
int i = 123;
float f = 3.14159;
char c = 'A';
char str[] = "Hello";
/* 基本用法 */
printf("整数:%d\n", i);
printf("浮点数:%f\n", f);
printf("字符:%c\n", c);
printf("字符串:%s\n", str);
/* 宽度和精度控制 */
printf("宽度10:%10d\n", i); /* 右对齐,宽度10 */
printf("左对齐:%-10d结束\n", i); /* 左对齐,宽度10 */
printf("精度.2:%.2f\n", f); /* 保留两位小数 */
printf("补零:%05d\n", i); /* 宽度5,不足补零 */
/* 多种格式输出同一个数 */
int n = 255;
printf("十进制:%d\n", n);
printf("八进制:%o\n", n);
printf("十六进制:%x\n", n);
printf("十六进制大写:%X\n", n);
return 0;
}
输出:
整数:123
浮点数:3.141590
字符:A
字符串:Hello
宽度10: 123
左对齐:123 结束
精度.2:3.14
补零:00123
十进制:255
八进制:377
十六进制:ff
十六进制大写:FF
4.3.5 转义字符
在字符串中,反斜杠\开始一个转义序列,表示特殊字符:
| 转义序列 | 含义 |
|---|---|
\n |
换行 |
\t |
水平制表符(Tab) |
\b |
退格 |
\r |
回车 |
\f |
换页 |
\a |
响铃 |
\\ |
反斜杠本身 |
\' |
单引号 |
\" |
双引号 |
\0 |
空字符(字符串结束标志) |
4.4 scanf函数详解
scanf(scan formatted)是C语言中用于从stdin读取格式化输入的函数。
4.4.1 函数原型
int scanf(const char *format, ...);
参数:
format:格式控制字符串,指定输入的格式
...:可变参数列表,必须是变量的地址(指针)
返回值:
成功:返回成功匹配并赋值的输入项个数
失败或遇到文件结束:返回EOF
4.4.2 与printf的重要区别
scanf的参数必须是变量的地址,而不是变量本身:
int num;
scanf("%d", &num); /* 正确:&num是num的地址 */
scanf("%d", num); /* 错误:num是变量的值,不是地址 */
4.4.3 scanf使用示例
#include <stdio.h>
int main(void)
{
int i;
float f;
char c;
char str[100]; /* 字符数组,用于存储字符串 */
/* 读取整数 */
printf("请输入一个整数:");
scanf("%d", &i);
printf("您输入了:%d\n", i);
/* 读取浮点数 */
printf("请输入一个浮点数:");
scanf("%f", &f);
printf("您输入了:%f\n", f);
/* 读取字符 */
printf("请输入一个字符:");
scanf(" %c", &c); /* 注意%c前的空格,用于消耗上一输入的换行符 */
printf("您输入了:%c\n", c);
/* 读取字符串(不用加&,数组名本身就是地址) */
printf("请输入一个字符串:");
scanf("%s", str); /* 字符串输入遇到空白字符停止 */
printf("您输入了:%s\n", str);
return 0;
}
4.4.4 scanf的常见问题与注意事项
1. 缓冲区中的换行符问题
#include <stdio.h>
int main(void)
{
int i;
char c;
printf("请输入一个整数:");
scanf("%d", &i);
printf("请输入一个字符:");
scanf("%c", &c); /* 问题:会读取上一行输入末尾的换行符 */
printf("整数:%d,字符:%c\n", i, c);
return 0;
}
输入:
text
123
A
输出:
text
请输入一个整数:123
请输入一个字符:整数:123,字符:
字符c读取的是换行符,而不是'A'。
解决方法:在%c前加空格,告诉scanf跳过空白字符:
scanf(" %c", &c); /* 空格会跳过所有空白字符(空格、制表符、换行符) */
2. 输入格式必须严格匹配
c
scanf(" %c", &c); /* 空格会跳过所有空白字符(空格、制表符、换行符) */
如果用户输入123 456(没有逗号),则只有a被赋值,b保持原值(未定义)。
3. 字符串输入的限制
%s在遇到空白字符(空格、制表符、换行符)时会停止读取。如果要读取包含空格的整行,应使用fgets函数。
4. 检查输入是否成功
利用scanf的返回值可以检查输入是否成功:
int num;
if (scanf("%d", &num) != 1) {
printf("输入错误,请输入整数\n");
/* 处理错误 */
}
4.5 字符输入输出函数
除了格式化的printf和scanf,stdio.h还提供了字符级的输入输出函数。
4.5.1 getchar和putchar
getchar:从stdin读取一个字符
int getchar(void);
返回值:读取的字符(作为int返回),出错或文件结束时返回EOF
注意:返回类型是int而不是char,这是为了能够返回EOF(通常是-1)
putchar:向stdout输出一个字符
int putchar(int c);
参数:要输出的字符
返回值:成功返回输出的字符,失败返回EOF
使用示例:
#include <stdio.h>
int main(void)
{
int ch; /* 注意:用int类型接收getchar的返回值 */
printf("请输入一些字符(输入Ctrl+D结束):\n");
while ((ch = getchar()) != EOF) {
putchar(ch); /* 回显输入的字符 */
}
printf("\n程序结束\n");
return 0;
}
这个程序会逐个读取字符并输出,直到遇到文件结束标志(Linux:Ctrl+D,Windows:Ctrl+Z)。
4.5.2 gets和puts
gets:从stdin读取一行字符串(危险!不要使用)
char *gets(char *str);
gets函数在C11标准中已被正式移除,因为它无法限制读取的字符数,容易导致缓冲区溢出。
puts:向stdout输出字符串,并自动添加换行符
int puts(const char *str);
使用示例:
#include <stdio.h>
int main(void)
{
char str[100];
printf("请输入一行文字:");
fgets(str, sizeof(str), stdin); /* 安全的替代:fgets */
printf("您输入的是:");
puts(str); /* puts会自动添加换行符 */
return 0;
}
5 语句与代码块
5.1 语句的概念
语句(Statement)是C程序中的可执行单元。每条语句都完成某种操作,并以分号;结尾。
C语言中的语句可以分为以下几类:
5.1.1 表达式语句
任何表达式后跟分号就构成表达式语句:
x = y + z; /* 赋值表达式语句 */
i++; /* 自增表达式语句 */
printf("Hi"); /* 函数调用表达式语句 */
3 + 5; /* 合法但无意义的语句,计算结果被丢弃 */
5.1.2 控制语句
控制语句控制程序的执行流程,包括:
条件判断语句:if、switch
循环执行语句:for、while、do-while
转向语句:break、continue、goto、return
这些将在后续章节详细讲解。
5.1.3 空语句
只有一个分号的语句称为空语句:
; /* 空语句,什么也不做 */
空语句在某些语法要求有语句但无需操作的地方有用,例如无限循环:
while (1)
; /* 空循环体 */
5.2 代码块的概念
代码块(Block),也称为复合语句(Compound Statement),是由一对花括号{ }括起来的零条或多条语句的集合。
5.2.1 代码块的基本形式
{
语句1;
语句2;
/* ... */
语句n;
}
5.2.2 代码块的语法作用
在语法上,代码块被视为一条语句,因此可以出现在任何单条语句可以出现的位置。例如,在if语句中:
if (条件) {
语句1;
语句2;
语句3;
}
如果不使用代码块,if后面只能跟一条语句:
if (条件)
语句1; /* if只控制这一条语句 */
语句2; /* 这条语句不在if控制范围内,总会执行 */
5.2.3 代码块与作用域
代码块还定义了变量的作用域(Scope)。在代码块内部声明的变量,其作用域仅限于该代码块内部:
#include <stdio.h>
int main(void)
{
int a = 10; /* 外层变量 */
{
int b = 20; /* 内层变量,作用域仅限于这个代码块 */
printf("块内:a = %d, b = %d\n", a, b);
}
printf("块外:a = %d\n", a);
/* printf("块外:b = %d\n", b); */ /* 错误:b在此处未定义 */
return 0;
}
代码块可以嵌套,内层代码块可以访问外层代码块的变量,但外层不能访问内层的变量。
5.2.4 代码块与函数体
函数的函数体本身就是一个代码块:
int max(int x, int y)
{ /* 代码块开始 */
int result;
if (x > y)
result = x;
else
result = y;
return result;
} /* 代码块结束 */
5.3 语句与代码块的关系总结
| 特性 | 简单语句 | 代码块 |
|---|---|---|
| 构成 | 表达式 + 分号 | 花括号 { } 括起的语句序列 |
| 结尾 | 必须有分号 | 花括号后不用分号 |
| 是否引入作用域 | 否 | 是 |
| 包含关系 | 不能包含其他语句 | 可以包含多个语句和其他代码块 |
6 综合案例分析
让我们综合本章所学,分析一个稍微复杂的程序:
/* 文件名:calculator.c */
#include <stdio.h> /* 预处理指令:包含标准输入输出库 */
int main(void) /* 主函数,程序入口 */
{ /* 代码块开始 */
int num1, num2; /* 变量声明 */
int sum; /* 变量声明 */
char operator; /* 运算符 */
int result; /* 计算结果 */
/* 输入部分 */
printf("简单计算器\n");
printf("请输入第一个整数:");
scanf("%d", &num1); /* 输入语句 */
printf("请输入运算符(+、-、*、/):");
scanf(" %c", &operator); /* 注意空格 */
printf("请输入第二个整数:");
scanf("%d", &num2);
/* 计算部分 - 使用if语句控制流程 */
if (operator == '+') {
result = num1 + num2; /* 表达式语句 */
}
else if (operator == '-') {
result = num1 - num2;
}
else if (operator == '*') {
result = num1 * num2;
}
else if (operator == '/') {
if (num2 != 0) { /* 嵌套的if语句 */
result = num1 / num2;
} else {
printf("错误:除数不能为0\n");
return 1; /* 返回错误代码 */
}
}
else {
printf("错误:不支持的运算符\n");
return 1; /* 返回错误代码 */
}
/* 输出部分 */
printf("%d %c %d = %d\n", num1, operator, num2, result);
return 0; /* 返回0表示成功 */
} /* 代码块结束 */
这个程序包含了本章讨论的所有要素:
预处理指令:#include <stdio.h>
main函数:程序入口,带返回值
变量声明:int num1, num2;等
输入输出语句:printf和scanf函数调用
控制语句:if-else条件判断
表达式语句:result = num1 + num2;等
代码块:函数体、if语句体都用{}括起
return语句:返回状态值
7 常见错误与注意事项
7.1 忘记包含必要的头文件
/* 错误示例 */
int main(void)
{
printf("Hello\n"); /* 错误:printf未声明 */
return 0;
}
解决方法:在文件开头添加#include <stdio.h>
7.2 main函数格式错误
/* 错误示例 */
void main() /* 不符合标准 */
{
/* ... */
}
/* 正确示例 */
int main(void) /* 符合标准 */
{
/* ... */
return 0;
}
7.3 忘记添加分号
#include <stdio.h>
int main(void)
{
int a = 10 /* 错误:缺少分号 */
printf("%d\n", a);
return 0;
}
7.4 代码块匹配错误
#include <stdio.h>
int main(void)
{
if (1) {
printf("条件成立\n");
/* 缺少右花括号 */
return 0;
}
7.5 scanf缺少取地址符
#include <stdio.h>
int main(void)
{
int num;
scanf("%d", num); /* 错误:应该是 &num */
printf("%d\n", num);
return 0;
}
7.6 格式说明符与数据类型不匹配
#include <stdio.h>
int main(void)
{
int num = 100;
printf("%f\n", num); /* 错误:%f用于float/double,但传入int */
return 0;
}
8 本章小结
本章系统分析了C程序的基本结构,主要包括以下内容:
1. 预处理指令
#include用于包含头文件,有尖括号(系统头文件)和双引号(用户头文件)两种形式
预处理指令在编译前被处理,不属于C语言本身的语法
2. main函数
程序的入口点,由操作系统调用
标准形式:int main(void)和int main(int argc, char *argv[])
返回值向操作系统报告执行状态(0表示成功)
3. 标准输入输出库函数
printf:格式化输出,支持丰富的格式控制符和修饰符
scanf:格式化输入,注意参数必须为变量地址
getchar/putchar:字符级输入输出
转义字符:表示特殊字符(如\n换行)
4. 语句与代码块
语句:以分号结尾的可执行单元,包括表达式语句、控制语句、空语句
代码块:花括号括起的语句集合,在语法上视为一条语句,并引入变量作用域
理解这些基本构成要素,是阅读和编写C程序的基础。在后续的章节中,我们将在此基础上深入学习数据类型、运算符、控制结构等更复杂的内容。
更多推荐

所有评论(0)