基础语法
# 01.基础语法
# 目录介绍
# 1.1 C语言标准
# 1.1.1 标准简史
- 1972年C语⾔在⻉尔实验室诞⽣. 丹尼斯·⾥奇 参考B语⾔开发.
- 1970-80年代,C语⾔被⼴泛应⽤,产⽣很多不同的C语⾔版本. 程序可移植性⽐较差.
- 1983年,美国国家标准委员会(ANSI) 成⽴⼀个⼩组来制定C语⾔的标准. C语⾔⽀持哪些语法、⽀持哪些功能 等等.
- 1989年,通过了C语⾔的第⼀个标准. C89标准.
- 1990年,国际标准化组织(ISO) 和 国际电⼯委员会(IEC) 将 C89标准当做国际的C语⾔标准. C90标准. C89和 C90指的是同⼀个标准
- 1994年 ISO和 IEC 对 C89标准进⾏修订. C94标准. 由于并没有增加新的语法特性,还是叫做 C89或者C90.
- 1995年 ISO和IEC再次做了修正,C95 标准.
- 1999年 ISO 和 IEC 发布了C语⾔新标准. C语⾔第⼆个标准. 在该标准中,新增许多实⽤的C语⾔语法特性. 增 加新的关键字、可变⻓数组等等. C99标准
- 2007年,重新修订了C语⾔.
- 2011年, 发布新的版本。新增了⼀些语法,泛型、国际化⽀持. ⽬前为⽌最新版本是 C11.
# 1.1.2 标准的影响
- 可将C语⾔的标准理解为C语⾔说明书。但其并没有强制性约束⼒。
- 如果编译器不⽀持标准,我们即使使⽤标准中的语法仍然会报错。
- 编译器版本也会影响程序。因此,编写程序之前要确定编译器版本。
# 1.1.4 优缺点
优点:1.学习成本低;2.运⾏速度快;3.功能强⼤。
缺点:1.代码实现周期⻓;2.可移植性差;3.对经验要求⾼;4.对平台库依赖多
# 1.1.5 C语⾔特点
C 语言能够长盛不衰、广泛应用,主要原因是它有一些鲜明的特点。
1)低级语言:直接操作硬件、管理内存、跟操作系统对话,这使得它是一种非常接近底层的语言,也就是低级语言,非常适合写需要跟硬件交互、有极高性能要求的程序。
2)可移植性:C 语言的原始设计目的,是将 Unix 系统移植到其他计算机架构。这使得它从一开始就非常注重可移植性,C 程序可以相对简单地移植到各种硬件架构和操作系统。
3)简单性:C 语言的语法相对简单,语法规则不算太多,也几乎没有语法糖。一般来说,如果两个语法可以完成几乎相同的事情,C 语言就只会提供一种,这样大大减少了语言的复杂性。
而且,C 语言的语法都是基础语法,不提供高级的数据结构,比如 C 语言没有“类”class),复杂的数据结构都需要自己构造。
4)灵活性 :C 语言对程序员的限制很少。它假设程序员知道自己在干嘛,不会限制你做各种危险的操作,你干什么都可以,后果也由自己负责。
C 语言的哲学是“信任程序员,不要妨碍他们做事”。比如,它让程序员自己管理内存,不提供内存自动清理功能。另外,也不提供类型检查、数组的负索引检查、指针位置的检查等保护措施。
表面上看,这似乎很危险,但是对于高级程序员来说,却有了更大的编程自由。不过,这也使得 C 语言的 debug 不太容易。
5)总结:上面这些特点,使得 C 语言可以写出性能非常强、完全发挥硬件潜力的程序,而且 C 语言的编译器实现难度相对较低。但是另一方面,C 语言代码容易出错,一般程序员不容易写好。
此外,当代很多流行语言都是以 C 语言为基础,比如 C++、Java、C#、JavaScript 等等。学好 C 语言有助于对这些语言加深理解。
# 1.1.6 C语言版本
历史上,C 语言有过多个版本。
1)K&R C :1978年,C 语言的发明者丹尼斯·里奇Dennis Ritchie)和布莱恩·柯林汉Brian Kernighan)合写了一本著名的教材《C 编程语言》The C programming language)。由于 C 语言还没有成文的语法标准,这本书就成了公认标准,以两位作者的姓氏首字母作为版本简称“K&R C”。
2)ANSI C又称 C89 或 C90)C 语言的原始版本非常简单,对很多情况的描述非常模糊,加上 C 语法依然在快速发展,要求将 C 语言标准化的呼声越来越高。
3)C95:1995年,美国国家标准协会对1989年的那个标准,进行了补充,加入多字节字符和宽字符的支持。这个版本称为 C95。
4)C99:C 语言标准的第一次大型修订,发生在1999年,增加了许多语言特性,比如双斜杠//)的注释语法。这个版本称为 C99,是目前最流行的 C 版本。
5)C11:2011年,标准化组织再一次对 C 语言进行修订,增加了 Unicode 和多线程的支持。这个版本称为 C11。
6)C17:C11 标准在2017年进行了修补,但发布是在2018年。新版本只是解决了 C11 的一些缺陷,没有引入任何新功能。这个版本称为 C17。
7)C2x:标准化组织正在讨论 C 语言的下一个版本,据说可能会在2023年通过,到时就会称为 C23。
# 1.1.5 应用领域
- 服务器。
- 操作系统。
- 上层应⽤。 MFC、QT
- 嵌⼊式
- ⼈⼯智能、硬件驱动。
- 中间件。
- ⽹络攻防、数据安全。
# 1.1.7 综合案例与思考
综合案例:查看当前编译器支持的C标准版本
#include <stdio.h>
int main() {
// __STDC_VERSION__ 宏定义了当前支持的C标准版本
#ifdef __STDC_VERSION__
printf("C Standard Version: %ld\n", __STDC_VERSION__);
#if __STDC_VERSION__ >= 201112L
printf("支持 C11 或更高版本\n");
#elif __STDC_VERSION__ >= 199901L
printf("支持 C99\n");
#else
printf("支持 C89/C90\n");
#endif
#else
printf("编译器可能仅支持 C89\n");
#endif
printf("编译日期: %s\n", __DATE__);
printf("编译时间: %s\n", __TIME__);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
原理说明:__STDC_VERSION__ 是一个预定义宏,由编译器自动设置。C99 标准将其定义为 199901L,C11 标准定义为 201112L,C17 标准定义为 201710L。通过检测这个宏的值,可以在编译时判断编译器支持的标准版本。
思考题:
- C语言为什么至今仍然是系统级编程的首选语言?相比现代语言(如Rust、Go),C语言有什么不可替代的优势?
- 为什么说C语言是"信任程序员"的语言?这种设计哲学带来了哪些好处和风险?
- C89 和 C99 之间最重要的区别是什么?在实际项目中选择哪个标准更合适?
# 1.2 开发工具和环境
# 1.2.1 编译器
编译器是将C语言源代码转换为可执行程序的工具。以下是常用的C语言编译器:
GCC (GNU Compiler Collection):最流行的开源编译器,支持多种平台(Linux、Windows、macOS)。编译命令:gcc -o program program.c
Clang:LLVM 项目的一部分,以快速编译和清晰的错误信息著称。 编译命令:clang -o program program.c
MSVC (Microsoft Visual C++):Windows 平台上的官方编译器,集成在 Visual Studio 中。适合 Windows 应用程序开发。
# 1.2.2 集成开发环境
IDE 提供了代码编辑、编译、调试等一体化功能,适合大型项目开发。
Visual Studio (Windows)。功能强大,支持 C/C++ 开发,适合 Windows 平台。下载地址:Visual Studio (opens new window)
Code::Blocks。跨平台的开源 IDE,支持多种编译器(GCC、Clang 等)。下载地址:Code::Blocks (opens new window)
Eclipse CDT。基于 Eclipse 的 C/C++ 开发工具,适合大型项目。下载地址:Eclipse CDT (opens new window)
CLion。JetBrains 推出的跨平台 C/C++ IDE,功能强大,支持智能代码补全和调试。下载地址:CLion (opens new window)
# 1.2.3 构建工具
构建工具用于自动化编译和链接过程。
Make。经典的构建工具,通过 Makefile 定义编译规则。使用方法:make
CMake。跨平台的构建工具,生成 Makefile 或其他构建文件。使用方法:cmake . && make
# 1.2.4 综合案例与思考
综合案例:使用GCC编译器从源码到可执行文件的完整流程
# 1. 预处理:展开宏、头文件,生成 .i 文件
$ gcc -E hello.c -o hello.i
# 2. 编译:将预处理后的代码转为汇编代码,生成 .s 文件
$ gcc -S hello.i -o hello.s
# 3. 汇编:将汇编代码转为目标文件(机器码),生成 .o 文件
$ gcc -c hello.s -o hello.o
# 4. 链接:将目标文件与库文件链接,生成可执行文件
$ gcc hello.o -o hello
# 也可以一步完成所有步骤
$ gcc -o hello hello.c
2
3
4
5
6
7
8
9
10
11
12
13
14
原理说明:C程序的编译过程分为四个阶段——预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。预处理阶段处理所有以 # 开头的指令;编译阶段将C代码翻译为汇编语言;汇编阶段将汇编语言翻译为机器码;链接阶段将目标文件和标准库函数合并为最终的可执行文件。
思考题:
- 预处理、编译、汇编、链接这四个阶段各自做了什么?为什么要分成这么多步骤?
gcc -E生成的.i文件中,#include <stdio.h>会被替换成什么内容?- 静态链接和动态链接有什么区别?各有什么优缺点?
# 1.3 第一个程序
# 1.3.1 Hello案例
C 语言的源代码文件,通常以后缀名.c结尾。下面是一个简单的 C 程序hello.c。它就是一个普通的文本文件,任何文本编译器都能用来写。
#include <stdio.h> // 头文件,包含标准输入输出函数
int main() { // 主函数,程序入口
printf("Hello, World!\n"); // 输出语句
return 0; // 返回0,表示程序正常结束
}
2
3
4
5
# 1.3.2 代码逐行分析
- ‘#include' : 引⼊头⽂件专⽤关键字。
- <> : ⽤来包裹 库头⽂件名
- stdio.h : 使⽤的头⽂件。因为程序中使⽤了 printf() 函数。就必须使⽤该头⽂件。
std:标准:standard i: input 输⼊。 o: output 输出。
- int :main 函数返回值为整型。
- main: 整个程序的⼊⼝函数。 任何.c 程序,有且只有⼀个 main 函数。
- printf(); C语⾔向屏幕输出字符使⽤的函数。
- printf(“helloworld\n”);向屏幕输出一段内容
- return 0;return 返回。 C程序要求,main 函数要有返回值。借助 return 实现返回。0:成功!因为 int ,返回整数。
注意事项
- 程序中使⽤的所有的字符,全部是 “英⽂半⻆” 字符。
- 程序中,严格区分⼤⼩写。
- “;” 代表⼀⾏结束。不能使⽤ 中⽂ “;”,必须是英⽂。
# 1.3.3 编译源文件
假设你已经安装好了 GCC 编译器,可以打开命令行,执行下面的命令。
$ gcc hello.c
上面命令使用gcc编译器,将源文件hello.c编译成二进制代码。注意,$是命令行提示符,你真正需要输入的是$后面的部分。
运行这个命令以后,默认会在当前目录下生成一个编译产物文件a.out(assembler output 的缩写,Windows 平台为a.exe)。执行该文件,就会在屏幕上输出Hello World。
$ ./a.out
Hello World
2
GCC 的-o参数(output 的缩写)可以指定编译产物的文件名。
$ gcc -o hello hello.c
上面命令的-o hello指定,编译产物的文件名为hello(取代默认的a.out)。编译后就会生成一个名叫hello的可执行文件,相当于为a.out指定了名称。执行该文件,也会得到同样的结果。
$ ./hello
Hello World
2
GCC 的-std=参数(standard 的缩写)还可以指定按照哪个 C 语言的标准进行编译。
$ gcc -std=c99 hello.c
上面命令指定按照 C99 标准进行编译。注意,-std后面需要用=连接参数,而不是像上面的-o一样用空格,并且=前后也不能有多余的空格。
# 1.3.4 综合案例与思考
综合案例:编写一个多文件程序并编译
创建两个文件 main.c 和 math_utils.c:
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
// math_utils.c
#include "math_utils.h"
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// main.c
#include <stdio.h>
#include "math_utils.h"
int main() {
int x = 10, y = 20;
printf("加法: %d + %d = %d\n", x, y, add(x, y));
printf("乘法: %d * %d = %d\n", x, y, multiply(x, y));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 编译多文件程序
$ gcc -o program main.c math_utils.c
$ ./program
2
3
原理说明:在实际开发中,程序通常由多个源文件组成。每个 .c 文件会被独立编译为 .o 目标文件,然后通过链接器合并在一起。头文件 .h 用于声明函数原型,#ifndef 头文件保护防止重复包含。main() 函数是程序的唯一入口点,操作系统加载程序后首先调用它。
思考题:
- 为什么
main()函数的返回值是int而不是void?返回值 0 代表什么含义? - 如果忘记
#include <stdio.h>,程序还能编译通过吗?为什么? - 多文件编程中,头文件保护(
#ifndef)有什么作用?如果不加会怎样?
# 1.4 基本语法介绍
# 1.4.1 语句
C 语言的代码由一行行语句(statement)组成。语句就是程序执行的一个操作命令。C 语言规定,语句必须使用分号结尾,除非有明确规定可以不写分号。
int x = 1; //一个变量声明语句,声明整数变量x,并且将值设为1。
多个语句可以写在一行。下面示例是两个语句写在一行。所以,语句之间的换行符并不是必需的,只是为了方便阅读代码。
int x; x = 1;
# 1.4.2 语句块
C 语言允许多个语句使用一对大括号{},组成一个块,也称为复合语句(compounded statement)。在语法上,语句块可以视为多个语句组成的一个复合语句。
{
int x;
x = 1;
}
2
3
4
# 1.4.3 空格
C 语言里面的空格,主要用来帮助编译器区分语法单位。如果语法单位不用空格就能区分,空格就不是必须的,只是为了增加代码的可读性。
int x = 1;
// 等同于
int x=1;
2
3
空格还用来表示缩进。多层级的代码有没有缩进,其实对于编译器来说并没有差别,没有缩进的代码也是完全可以运行的。强调代码缩进,只是为了增强代码可读性,便于区分代码块。
# 1.4.4 注释
注释是对代码的说明,编译器会忽略注释,也就是说,注释对实际代码没有影响。
C 语言的注释有两种表示方法。第一种方法是将注释放在/.../之间,内部可以分行。
/* 注释 */
/*
这是一行注释
*/
2
3
4
5
这种注释可以插在行内。
int open(char* s /* file name */, int mode);
上面示例中,/* file name /用来对函数参数进行说明,跟在它后面的代码依然会有效执行。这种注释一定不能忘记写结束符号/,否则很容易导致错误。
# 1.4.5 标准库头文件
程序需要用到的功能,不一定需要自己编写,C 语言可能已经自带了。程序员只要去调用这些自带的功能,就省得自己编写代码了。举例来说,printf()这个函数就是 C 语言自带的,只要去调用它,就能实现在屏幕上输出内容。
C 语言自带的所有这些功能,统称为“标准库”(standard library),因为它们是写入标准的,到底包括哪些功能,应该怎么使用的,都是规定好的,这样才能保证代码的规范和可移植。
不同的功能定义在不同的文件里面,这些文件统称为“头文件”(header file)。如果系统自带某一个功能,就一定还会自带描述这个功能的头文件,比如printf()的头文件就是系统自带的stdio.h。头文件的后缀通常是.h。
如果要使用某个功能,就必须先加载对应的头文件,加载使用的是#include命令。这就是为什么使用printf()之前,必须先加载stdio.h的原因。
#include <stdio.h>
注意,加载头文件的#include语句不需要分号结尾。
# 1.4.6 综合案例与思考
综合案例:展示C语言基本语法要素
#include <stdio.h> // 标准库头文件
/*
* 这是一个演示C语言基本语法要素的程序
* 包含:语句、语句块、注释、头文件的使用
*/
int main() {
// 语句:每条语句以分号结尾
int radius = 5;
// 语句块:用大括号包围的多条语句
{
double pi = 3.14159;
double area = pi * radius * radius;
printf("圆的半径: %d\n", radius);
printf("圆的面积: %.2f\n", area); // %.2f 保留两位小数
}
// 多个语句可以写在一行(但不推荐)
int a = 10; int b = 20; int sum = a + b;
printf("求和: %d + %d = %d\n", a, b, sum);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
原理说明:C语言的语句是程序执行的基本单位,编译器通过分号 ; 来识别语句的结束。语句块 {} 定义了一个新的作用域,块内声明的变量在块外不可见。#include 是预处理指令,在编译前由预处理器处理,它把头文件的全部内容复制插入到当前位置。
思考题:
- 如果在语句块
{}内部定义了一个变量,在块外能否访问该变量?为什么? - C语言中
/* */和//两种注释有什么区别?哪种注释可以嵌套使用? - 为什么
#include语句不需要分号结尾?它和普通的C语句有什么本质区别?
# 1.5 修饰符和标识符
# 1.5.1 修饰符
修饰符(Modifiers):
- 修饰符是用来修改基本数据类型的含义的关键字,通常用于声明变量或函数的类型。
- 一些常见的修饰符包括:
const(常量修饰符)、volatile(易失修饰符)、static(静态修饰符)、extern(外部修饰符)等。 - 修饰符可以改变变量的存储方式、作用域、生命周期等特性。
#include <stdio.h>
int main() {
const int MAX_VALUE = 100; // 使用const修饰符声明常量
static int count = 0; // 使用static修饰符声明静态变量
printf("MAX_VALUE: %d\n", MAX_VALUE);
printf("count: %d\n", count);
return 0;
}
2
3
4
5
6
7
8
9
在上面的示例中,const修饰符用于声明常量MAX_VALUE,static修饰符用于声明静态变量count。这些修饰符可以帮助我们更好地控制变量的特性和行为。
# 1.5.2 标识符
标识符(Identifiers):
- 标识符是用来标识变量、函数、数组等程序实体的名称。
- 在C语言中,标识符必须以字母(大小写均可)或下划线开头,后面可以跟字母、数字或下划线组成。
- 标识符区分大小写,因此
MyVar和myvar是不同的标识符。 - C语言中有一些保留的关键字(如
int、char、if等),不能用作标识符。
# 1.5.3 常见关键字
关键字:在C语言中被赋予了特定含义的英文单词,一共有32个关键字。 但是不需要背,后面会挨个讲解,现在只要只有关键字的两个特点即可:
- 关键字全部小写
- 在特定的编译器中,关键字是高亮显示的
在C语言中,关键字是具有特殊含义的保留字,用于表示语言的基本结构和功能。以下是一些C语言中常见的关键字:
基本数据类型关键字,一共有9个:
关键字 描述 int定义整数类型(通常为 4 字节)。 char定义字符类型(通常为 1 字节)。 float定义单精度浮点数类型(通常为 4 字节)。 double定义双精度浮点数类型(通常为 8 字节)。 void表示无类型,通常用于函数返回值或指针类型。 short定义短整数类型(通常为 2 字节)。 long定义长整数类型(通常为 4 或 8 字节)。 signed表示有符号类型(默认)。 unsigned表示无符号类型(只能表示非负数)。 控制语句关键字:
关键字 描述 if条件判断语句。 else与 if配合使用,表示条件不成立时的分支。switch多分支选择语句。 case与 switch配合使用,表示一个分支条件。default与 switch配合使用,表示默认分支。for循环语句,用于重复执行代码。 while循环语句,当条件为真时重复执行代码。 do与 while配合使用,先执行代码再判断条件。break跳出循环或 switch语句。continue跳过当前循环的剩余部分,进入下一次循环。 goto无条件跳转到指定标签(不推荐使用)。 函数关键字:
关键字 描述 main程序入口函数 return从函数中返回值并结束函数执行。 void无返回值 存储类关键字:
关键字 描述 auto自动变量(默认存储类别,通常省略)。 static静态变量或函数,表示作用域或生命周期延长。 extern声明外部变量或函数,表示定义在其他文件中。 register建议编译器将变量存储在寄存器中(现代编译器通常忽略)。 const常量 volatile易失变量 指针关键字:
关键字 描述 sizeof返回变量或类型的大小 enum定义枚举类型 struct定义结构体 typedef定义类型别名 union定义共用体
在 C 语言中,关键字(Keywords)是语言本身定义的保留字,具有特定的含义和用途,不能用作变量名、函数名或其他标识符。
# 1.5.4 综合案例与思考
综合案例:修饰符和标识符的综合使用
#include <stdio.h>
// extern声明:表示该变量在其他文件中定义
// extern int global_count;
// static全局变量:只在当前文件可见
static int file_counter = 0;
// const常量:值不可修改
const double GRAVITY = 9.8;
void demonstrate_modifiers() {
// auto变量(默认,通常省略):函数结束后销毁
auto int local_var = 100;
// static局部变量:函数结束后值保留
static int call_count = 0;
call_count++;
// volatile变量:告诉编译器不要优化,每次都从内存读取
volatile int sensor_value = 0;
// register变量:建议编译器放入寄存器(现代编译器通常自动优化)
register int loop_var;
printf("第 %d 次调用,local_var = %d\n", call_count, local_var);
}
int main() {
// 合法标识符
int myVariable = 10;
int _count = 20;
int student_age = 18;
// 非法标识符(取消注释会报错)
// int 2name = 0; // 不能以数字开头
// int my-var = 0; // 不能包含连字符
// int int = 0; // 不能使用关键字
demonstrate_modifiers(); // 输出: 第 1 次调用
demonstrate_modifiers(); // 输出: 第 2 次调用(static变量保留)
demonstrate_modifiers(); // 输出: 第 3 次调用
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
原理说明:修饰符改变了变量的存储方式和生命周期。static 局部变量存储在全局/静态区而非栈上,所以函数返回后值不会丢失。const 修饰的变量被放入只读内存区域,任何修改操作在编译时就会被拦截。volatile 告诉编译器该变量可能被外部因素(如硬件中断)修改,不要进行优化缓存。
思考题:
static修饰局部变量和修饰全局变量时,含义有什么不同?const int *p和int * const p有什么区别?各自限制了什么?- 为什么C语言中标识符不能以数字开头?这是出于什么设计考虑?