总目录 > 1 语言基础 > 1.1 C 语言基础
前言
这篇文章对 C 语言里一些最基础的编程知识进行简要介绍。因为对 C / C++ 大部分入门内容都轻车熟路了,有些地方可能就不恰当地略过了。
更新日志
20200713 - 分支与循环部分施工完毕。
20200716 - 所有内容全部施工完毕。
20200901 - 原 1.2 C++ 入门基础知识 现更改为 1.1 C 语言基础,仅介绍 C 语言中存在的基础知识。
20200903 - 部分内容移动至 1.2 C 语言数据类型,介绍变量,常量,数组,结构体与函数等内容。
子目录列表
1、环境与编译
2、头文件与主函数
3、注释
4、输入与输出
5、格式与缩进
6、预处理器
7、运算符
8、分支与循环
9、文件操作
1.1 C 语言基础
1、环境与编译
① 概念
众所周知,C / C++ 是一种计算机语言,是开发者和计算机的沟通方式,是两者之间的桥梁。同样众所周知的是,计算机本身并不能识别这些由英文单词和数字组成的话,而只能理解由 0 和 1 构成的二进制数串。那么,要将 C / C++ 翻译成二进制数,则需要一位翻译员进行翻译,在计算机领域里,翻译的过程被称作 —— 编译(compile),而担任翻译员的,则是编译器。
② IDE
③ 编译器
Windows 下需要自行下载,当然 IDE 可能直接集成了编译器,或者提供了下载渠道。
macOS 在终端执行:xcode-select --install
Linux 在终端执行:sudo apt update && sudo apt install g++
④ 命令行执行
如果不想被 IDE 的框架受限,或者想显得更高端的话,可以在命令行直接进行编译,格式为(C++):
g++ *.cpp -o *,* 表示要编译的文件名,-o 用来指定输出文件名(C 格式类似,此处不介绍)
除此之外,还有许多可以添加的编译选项,列举几个常用的:
-O / -O1 优化生成代码;-O2 进一步优化;-O3 再进一步优化;
-w 关闭所有警告提醒;-Wall 显示所有警告提醒;
-lm 链接数学库(这个一般竞赛编译都会加上,但其实不加上也能正常运行)
2、头文件与主函数
先演示一个最经典的 C 语言示例代码。
#include <cstdio> int main() { printf("Hello, world!"); // output 'Hello, world!' return 0; }
① 头文件
其中,第一行(L1)为头文件格式:
#include <头文件名>
头文件的作用是什么?一般,头文件包含了各类功能函数或者数据接口声明。任何函数都不是与生俱来的,而是保存在 C / C++ 里内置的各种 .h 文件中,如果不事先声明,编译器并不能识别这是什么。
比如,L4 的 printf 函数是来自内置的 <cstdio> 中,故需要在最开始表明,接下来的代码需要用到这里面的函数,那么编译器在执行主程序之前就会先进行预处理。
一般而言,只需要写上你需要的头文件即可,都有时候可能出现遗漏,而编译器并不会在所有这类情况下报错,所以你可以选择:
① 提前写好常用的头文件作为模板,每次编写时直接复制上去;
② 多检查几次;
③ 使用万能头文件(仅适用于 C++):
#include <bits/stdc++.h>
它包含了 C++ 绝大多数头文件。但是请注意,首先 C 语言不能使用,MSVC 编译器不支持;其次并不是所有情况下都允许使用,多加留意考试/比赛规则;最后,头文件声明越多,对编译时间影响越大,尽管一般不至于导致 TLE。
头文件也可以自己编写,比如自行定义了 “bebe.h”,并在其中定义了一些函数,那么在声明头文件时可以这样写:
#include "bebe.h"
注意,自定义头文件使用 " " 而不是 < >。
一般使用不到,尤其竞赛就不可能用到了,所以暂时不过多介绍。
声明头文件本身是一种预处理语句。关于预处理,下面会有介绍。
② 主函数 main
L3 定义了一个主函数:int main()。任何 .c / .cpp 文件有且仅有一个主函数,且为整型(int)函数。注意,请不要使用各类古老的教材教辅上写的 void main(),实际上这种写法早已被废弃,是不会被编译通过的。
L6 有一句 “return 0”,表示主函数的返回值为 0,作为主函数的结束标识,尽管可以不写,但还是建议写上,这是规范,某些情况下不写会出现错误。
主函数本身也是一个函数。关于函数,将会在 施工中 介绍。
3、注释
上面的示例代码中的 L5 中,后面有个 “//” 符号,表示注释,对程序编译无任何影响,一般用于让某段代码暂时不执行,或者解释程序,便于日后维护或者供他人阅读。
两种注释的方法:
① 行内注释,以 “//” 开头,后面所有内容全部为注释;
举例:如上示代码。
② 注释块,以 “/*” 开头,以 “*/” 结尾,可以跨行,中间内容全部为注释。
举例:
/* --------------------- Hello, world! --------------------- */
4、输入与输出
在 C 语言中,使用 scanf / printf 函数分别进行数据的输入与输出,一般格式为:
scanf("A", B);
printf("A", B);
"A" 内填写字符或者格式说明符,B 填写变量。
scanf("%d", &a); // 表示输入一个变量 a scanf("%d %d", &a, &b); // 表示输入两个变量 a, b printf("1"); // 表示输出一个数 1,等同于 printf("%d", 1); printf("%d %d", a, b); // 表示输出两个变量 a, b,中间会换行
格式看起来很复杂(确实比 C++ 的 cin / cout 流输入输出函数要复杂许多),进行一些解释:
> %d 是什么?
格式说明符的一种。使用 scanf / printf 时,需要先声明该变量的格式,下面是常用的格式说明符:
%d - 十进制有符号整型变量,对应 int 类型等;
%c - 字符变量,对应 char 类型;
%f - 单精度浮点型变量,对应 float 类型;
%lf - 双精度浮点型变量,对应 double 类型;
%s - 字符串(字符数组)变量,对应 char[](字符数组);
%lld - 长整型变量(Windows 下使用 %I64d),对应 long long 类型。
在 '%' 与字母之间添加一个数字,表示设置输出域宽(即输出数据占用的字符个数)。这个数字小于数本身位数没有影响;大于本身位数将自行填充空格。
举例:
printf("%3d", 5); // 将输出 “ 5”
在 '%' 与字母之间添加一个 '.' 和一个数字,表示设置浮点数精度(即小数保留位数),仅适用于浮点型变量,即 %f, %lf。
举例:
printf("%.2lf", 2.077); // 将输出 “2.08”
> 是什么?
转义字符的一种。使用 scanf / printf 时,用于表示一些不能直接输入的字符,下面是常用的转义字符:
- 换行符; - 制表符;
" - 表示 ";\ - 表示 (因为这两个字符可能出现歧义)
> scanf 中的 & 是什么?
& 表示取址运算符,表示变量在内存中的地址。具体请参见:1.4.3 指针与引用 中的 指针的声明 部分。
5、格式与缩进
C / C++ 对书写格式和缩进并无任何规定,理论上只要语法正确,想怎么写怎么写。但语言是什么?是人类进行沟通表达和传递信息的工具,不要满足于计算机能理解你的行为,所以尽可能地保持代码的格式统一和结构清晰,能形成自己的代码风格更好。当年在 cj 搞 OI 那么久,早期风格变化大,后来慢慢定型了,基本上是师承 bebe 的,过了若干年再次看到他的代码的时候发现还是那么像(
对于格式,大多是空格位置、换行位置、大括号位置等方面有一定差异。
对于缩进,一般有四格缩进和两格缩进的区别。
见仁见智。
以前我是不怎么打空格的,后来看 bebe 和 zed 的代码,逐渐被同化,成了一个空格王;同时比较喜欢压行,用逗号,还有各种能简短代码的方式。
粘贴一份 KMP 算法(请参见:施工中)的代码体现一下代码风格。。(C++ 代码)
1 int main() { 2 cin >> a + 1 >> b + 1; 3 la = strlen(a + 1), lb = strlen(b + 1); 4 fail[0] = -1; 5 for (int i = 1, x = -1; i <= lb; i++) { 6 while (x >= 0 && b[x + 1] != b[i]) x = fail[x]; 7 fail[i] = ++x; 8 } 9 for (int i = 1, x = 0; i <= la; i++) { 10 while (x >= 0 && b[x + 1] != a[i]) x = fail[x]; 11 if (++x == lb) cout << i - lb + 1, exit(0); 12 } 13 cout << "N/A"; 14 return 0; 15 }
以前我更不喜欢换行,现在可能好点了(
6、预处理器
预处理器是在真正的编译开始之前由编译器调用的独立程序。C 语言预处理器中提供了一些预处理命令,如 #define, #else, #elif, #endif, #error, #if, #ifdef, #ifndef, #include, #pragma, #undef 等,下面选取其中一些常用预处理介绍。
① #include 头文件声明
一般格式为 #include <> 或 include "",前者适用于 C 内置的头文件,后者适用于自定义头文件,所有内置的头文件均可以在 C 语言编译器目录下找到。
一般情况下,头文件声明均书写在每个文件的起始位置。
② #define 宏定义
> #define
#define 用于定义一个标识符常量或带参的宏,本质上是一种文本替换,格式为:
#define A B
可以使程序中所有独立出现的 A 全部视作 B,使用起来非常灵活,比如:
#define N 2020 printf("%d", N); // 将输出 2020
还可以带参数,比如:
#define printf("%d", &a) PRINT(a) PRINT(5); // 将输出 5
也正是因为使用很灵活,有时容易出现一些不可预见的问题,比如:
#define sum(A, B) A + B int c = 2 * sum(3, 4);
看起来是先求 3 + 4 = 7 再 7 * 2 = 14,但实际上会被理解为:
int c = 2 * 3 + 4;
故最后结果为 10 而非 14。
所以建议一般只在进行简单的替换时才使用 #define,比如:
int a[20000], b[20000], c[20000], d[20000];
输出这么多次 20000 又麻烦又不美观,这时可以:
#define MAXN 20000
> #undef
#undef 用于删除由 #define 定义的宏,比如:
#define N
那么,N 就不再会被替换。
> typedef
提到 #define 就不得不提 typedef。它并不属于预处理语句,只是和 #define 功能很相近,从其全称 typedefine 便可知。
typedef 用来为一个已有的数据类型定义一个别名,格式为:
typedef 原类型名 新类型名;
可以用于原类型名太长而需要简化的情况,比如:
typedef long long ll;
可以用于掩饰复合类型,比如:
typedef int arr[100]; arr a;
这样可以更便捷地定义 int 类型的数组。
还可以用于隐藏指针语法。
相比 #define 宏定义,typedef 功能较弱,但是更为规范,会进行正确性检查。
③ #ifdef, #endif 条件编译
一般格式为:
#ifdef 标识符
语句组 1
[#else
语句组 2]
#endif
[] 中内容表示可以缺省。这段预处理表示如果已经用 #define 定义了某标识符,就编译语句组 1,否则编译语句组 2(如果没有 #else 则没有否则部分)。
平时相当常用的条件编译是一个有意思的例子。在前面我们提到,对于 long long 这个数据类型,使用 scanf / printf 输入输出时,其格式说明符在 Windows 和 Linux 系统下是不同的,分别是 %I64d 和 %lld。因为一般个人计算机都是使用 Windows,但绝大多数评测系统都是 Linux 平台上搭建,所以在自己的电脑上测试过之后上交之前,又需要把说明符进行一些修改,麻烦而容易出错。这时候,我们使用如下条件编译:
#ifdef WIN32 #define lld "%I64d" #else #define lld "%lld" #endif
问题完美解决。WIN32 / Linux 是内置的标识符,分别表示当前操作系统为 Windows 和 Linux。
7、运算符
① 算术运算符
+ 正/加法;- 负/减法;* 乘法;/ 除法;% 取模。
② 位运算符
~ 非;& 与;| 或;^ 异或;<< 左移;>> 右移。
关于位运算更详细的介绍,请参见:6.1 位运算与进位制
③ 赋值运算符
= 赋值,前面还可以加上算术运算符或位运算符形成复合赋值运算符,比如:
i += 2,等价于 i = i + 2;
i %= j,等价于 i = i & j。
④ 自加自减符
++ 自加:i++ 等价于 i = i + 1;
-- 自减:i-- 等价于 i = i - 1;
更多使用方法和意义请参见:<施工中>
⑤ 比较运算符
< 小于;<= 小于等于;> 大于;>= 大于等于;== 等于;!= 不等于。
注意,== 表示等于,而 = 表示赋值,初学者常见问题之一。
⑥ 逻辑运算符
&& 逻辑与;|| 逻辑或;! 逻辑非。
注意和前面的位运算符区分开来:位运算符是返回与 / 或 /... 操作的值,而逻辑运算符只返回其值是否为 0,为 0 时返回 0,其余情况就返回 1,用于各类判断而非运算。
⑦ 分号与逗号
; 分号:表示一个表达式的结束;
, 逗号:用于将若干个表达式分隔开,有时候等价于分号,但实际有区别,这里不赘述。
⑧ 成员访问运算符
[ ] 下标符;. 对象成员;& 取地址 / 引用类型符;* 寻址 / 解引用符;-> 指针成员;
> 关于优先级
所有运算符都有运算的先后顺序,就像我们平常计算的时候知道乘除优先于加减,下面给出 C++ 大部分运算符的优先级总表:
8、分支与循环
① 分支
> if 和 if - else 语句
if (条件) { 主体1; } else { 主体2; }
众多主流语言中必学的第一条语句 —— if 语句,基本判断语句。最基本结构为 if (...) ...,和英文一样,表示“如果……则……”;还可以在语句后加上 else ...,表示“如果……则……否则……”。
if - else 语句还可以嵌套使用,举个例子:
if (a == 1) b = 10; else if (a == 2) b = 100; else if (a == 3) b = 1000; else b = 0;
> switch 语句
switch (选择句) { case 标签1: 主体1; break; case 标签2: 主体2; break; ... default: 主体n; }
其含义为:当选择句的返回值为标签 1 时,执行主体 1 语句;为标签 2 时,执行主体 2 语句,等等;如果不等于任何标签,则执行 default 中的主体 n 语句。
标签本身没有数量限制。
其中,选择句必须是整数类型表达式,标签必须是整型常量,所以相比之下局限性比 if - else 大很多。
举个例子:
switch (a) { case 1: b = 10; break; case 2: b = 100; break; case 3: b = 1000; break; }
这段代码和上面的 if-else 代码是等价的。
注意每一个 case 后都写上了 break,从 switch 的功能来看是必要的,但是如果不加,编译是没有问题的,只是会出现不一一对应执行的情况,比如:
在第二个 case 中不加 break,且选择句返回值为标签 2,则将会执行主体 2, 3, ... 的语句,直到出现 break。
举个例子:
switch (a) { case 1: b = 10; case 2: b = 100; case 3: b = 1000; break; } return 0;
那么,其实 a 不管是等于 1 / 2 / 3,最后 b 的值均为 1000。
② 循环
顾名思义,许多时候我们要反复做一件相同的事情,比如我们需要求出 2 ^ 20,写二十次 * 2 显然是不合理的,这时候需要使用循环语句。
> for 语句
for (初始化; 判断条件; 更新) { 循环体; }
最基本的循环语句,使用最广泛,功能最强大。就以上述求 2 ^ 20 为例,使用 for 循环可以这样写:
ans = 1; for (int i = 1; i <= 20; i++) { ans = ans * 2; }
初始化定义一个变量 i 并赋值为 1,其作用为计数器,当 i <= 20,即计数器计数到 20 之前,执行花括号内包含的循环体语句 ans = ans * 2;再执行更新语句 i++ 以进行累加。故 for 循环语句的执行顺序为:初始化语句 -> 判断条件 -> 循环体语句 -> 更新语句。
上述是 for 循环最基本的使用方式,而其实其灵活性极高。初始化、判断条件、更新和循环体四大部分,理论上都可以进行省略 —— 省略初始化语句,即不进行初始化;省略判断条件,即判断条件永远为真,一般会在循环体内在一定条件下通过某种方式跳出循环,如果没有,则会进入死循环;省略更新语句,即不进行更新,一般在循环体内包含了更新内容,否则也会进入死循环;省略循环体没有意义,但同样可以编译。
C 语言中不允许在初始化语句里定义变量,所以需要在循环语句前就进行定义。
> while 语句
while (判断条件) { 循环体; }
不能初始化的循环语句(初始化只能在循环前完成)。如果将上述 if 的结构套用到 while 中,则相当于:
初始化; while (判断条件) { 循环体; 更新; }
不同的是,判断条件不能省略。如果需要永远为真,则需要填入 true(或 1)。
> do - while 语句
do { 循环体; } while (判断条件);
和 while 语句类似,唯一区别是 do - while 语句先执行循环体再进行判断。
三种循环一般情况下都是互通的,根据实际需要和个人习惯进行选择即可。
> break 和 continue
循环语句中很关键的两个语句。有时我们的中断循环的判断条件并非那么明朗而需审时度势,break 和 continue 能够满足我们的需求。
break 的作用是立即退出当前循环。比如我们需要 1 + 2 + ... 累加到刚好大于 100,则可以:
for (int i = 1; ; i++) { ans += i; if (ans > 100) break; }
continue 的作用是跳过本轮循环,直接准备下一轮循环。对于 for 循环是执行更新语句,对于 whlie 循环则直接判断。比如我们需要 1 + 3 + 5 + ... + 21,可以这样写:
for (int i = 1; i <= 21; i++) { if (i % 2 == 0) continue; ans += i; }
9、文件操作
不涉及文件操作的时候,程序的输入输出都是直接在终端中进行的,引入文件操作后,可以使我们的程序从指定文件中读入数据,再将数据输出到指定文件。
① freopen 函数
一般格式:
freopen(文件名, "模式", 流);
部分模式列表:
r - 只读;w - 只写;r+ - 读写,文件必须存在;w+ - 读写,会自动新建;
a - 只写,不存在会自动新建,存在会直接将数据附加在文件末尾,保留 EOF 符;
a+ - 同 a,但不保留 EOF 符。
比如:
freopen("data.in", "r", stdin); freopen("data.out", "w", stdout)
关闭文件(可以不写):
fclose(stdin);
fclose(stdout);
② fopen 函数
格式和 freopen 类似:
fopen(文件名, "模式");
区别在需要定义文件指针,比如:
FILE *in, *out; in = fopen("data.in", "r"); out = fopen("data.out", "w");
与此同时,输入输出的函数需要进行修改,比如:
fscanf(in, "%d", &a); fprintf(out, "%d", a);
相对比较麻烦,平时用的不太多。
③ fstream 函数
C++ 特有,名为 “文件输入输出流”,格式为:
fstream 流名(文件名);
比如:
fstream file("data.txt");
其中,fstream 可以更改为 ifstream 或 ofstream,表示只支持读 / 写。
中间的读写函数从 cin / cout 改成 file 即可。
关闭文件:
流名.close();