C/C++道经第1卷 - 第1阶 - 新手村(一)
传送门:CB1-1-新手村(一)
传送门:CB1-1-新手村(二)
传送门:CB1-1-新手村(三)
心法:本章使用 C 项目进行练习
练习项目结构如下:
|_ v1-1-basic-rookie
S01. 环境搭建
心法:C 语言源于贝尔实验室,由丹尼斯·里奇基于肯·汤普森的 B 语言开发,两人用它重写了 UNIX 系统,随系统发展,C 语言也不断完善。
C 语言需要通过编译器将源代码编译为目标文件,而现代 IDE 一般都集成了 C 语言的编译器,所以无需单独安装,常用的现代 IDE 如下:
Codeblocks:
- 轻量级 IDE,安装包小,占用资源少,支持插件拓展。
- 界面简洁,清晰,易上手,且支持个性化定制。
- 适合初学者和低配电脑。
Visual Studio:
- 功能强大,支持多种语言,支持代码提示,支持断点调试。
- 资源占用比较高,界面稍显复杂,支持高度定制化。
- 适合大型企业级项目和 Windows 平台开发。
CLion:
- 专注于 C/C++ 开发,支持代码提示,支持CMake 构建和热部署。
- 界面简洁,采用符合现代开发习惯的交互设计,性能平衡性较好。
- 适合专业 C/C++ 开发者和基于 CMake 的项目。
E01. 安装CLion工具
武技:在 Windows 平台上下载安装并配置 CLion 集成开发工具。
- 下载CLion2024.3 后,傻瓜式安装即可,注意路径中不要有中文或特殊符号。
- 自行购买或激活 CLion,后续可通过 Help → About 查看激活情况。
- 使用管理员方式启动 @/bin/CLion64.exe 程序。
1. 禁用自动更新
武技:禁用 CLion 自动更新。
进入 File → Settings → Appearance & Behavior → System Settings → Updates
界面:
- 取勾 Check IDE updates for … 选项。
- 取勾 Check for plugin updates 选项。
- 取勾 Show What’s New in the editor after an IDE update 选项。
2. 设置字体大小
武技:设置调整 CLion 字体大小的快捷键。
进入 File → Keymap
界面,然后展开 Editor Actions 栏,在右上角搜索栏中输入 font 关键字:
- 选择 Increase Font Size,设置快捷键为 Ctrl + Wheel up,即 Ctrl 加鼠标上滑轮。
- 选择 Decrease Font Size,设置快捷键为 Ctrl + Wheel down,即 Ctrl 加鼠标下滑轮。
- 选择 Reset Font Size,设置快捷键为 Ctrl + Middle-Click,即 Ctrl 加鼠标中键。
3. 设置字符编码
武技:设置 CLion 字符编码(需要再次设置其他项目)。
进入 File → Settings → Editor → File Encodings
界面:
- 修改 Global Encoding 为 UTF-8 编码。
- 修改 Project Encoding 为 UTF-8 编码。
- 修改 Default encoding for properties files 为 UTF-8 编码。
4. 解决中文乱码
武技:在 CLion 里,run.processes.with.pty 这个选项涉及到运行程序时是否使用伪终端(Pseudo-Terminal,PTY),不使用 PTY 时可以解决控制台的中文乱码问题。
使用快捷键 shift + ctrl + alt + / 打开 CLion 配置面板,然后选择 Registry 进入 CLion 注册表,然后取勾 run.processes.with.pty 项即可:
5. 修改调试窗口
心法:调整 CLion 为在外部控制台运行的模式,可以支持更多特定的终端命令,兼容性和稳定性更高,用户的交互体验也会更好。
- 运行 HelloWorld 程序,然后在 CLion 内置的控制台处找到三个竖点的图片,选择 Modify Run Configuration… 打开页面:
- 勾选 Run in external console 配置:
- 临时设置控制台编码为 UTF8,并使用 system(“pause”) 代码暂停住命令窗口以防止闪退:
在 C 源代码中,需要引入 #include <stdlib.h>
以启用 system(“”) 函数:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// 临时设置控制台编码为UTF-8
system("chcp 65001 > nul");
// 输出语句
printf("Hello, world!");
// 暂停(要引入 #include <stdlib.h> 以启用 system() 函数)
system("pause");
return 0;
}
在 C++ 源代码中,需要引入 #include <iostream>
以启用 system(“”) 函数:
#include <iostream>
int main() {
// 临时设置控制台编码为UTF-8
system("chcp 65001 > nul");
// 输出语句
std::cout << "Hello, World!" << std::endl;
// 暂停(要引入 #include <iostream> 以启用 system() 函数)
system("pause");
return 0;
}
E02. 开发C项目
1. 创建C项目
武技:使用 CLion 创建 C 项目 v1-1-basic-rookie
点击 New Project ,然后选择 C Executable 以创建 C 语言项目:
- 在 location 栏填写项目地址,建议多指定一层目录,目录不存在会自动创建。
- 在 Language standard 栏选择 C11 以配置 C 语言版本。
- 点击 Create 完成项目创建。
2. 开发主源文件
心法:在 C 语言里,main.c 通常是程序的主源文件,main 函数是程序的入口点。
include命令:在 C 和 C++ 标准中,使用 #include xxx
命令来引入程序所需的头文件,比如主文件中引入的 stdio.h 头文件,就是系统提供的标准输入输出库。
主函数返回值:在 C 和 C++ 标准中,主函数通常要求返回 int 整数,如果主函数中没有编写 return 语句,则隐式返回 0,表示程序正常结束,其他非零值表示程序异常结束。
主函数参数:若省略参数中的 void 表示该方法允许接收任意数量和类型的命令行参数,而使用 void 表示不接收任何参数,明确性更高,可读性更高。
心法:在主文件中开发 HelloWorld 代码。
- 开发 HelloWorld 代码:
// 引入IO头文件
#include <stdio.h>
/**
* 主函数(参数 void 可以省略)
* @return 主函数返回值固定为 int
*/
int main(void) {
// 输出语句
printf("Hello, C!");
// 主函数的返回值可以省略
return 0;
}
- 编译 HelloWorld 代码:CLion 在运行代码之前,会自动帮我们进行编译工作,无需手动操作。
- 运行 HelloWorld 代码:点击主方法左侧的播放按钮,使用 Debug 的方式运行该方法,在控制台查看结果。
3. 用户文件
心法:在 C 语言中,用户能够自主创建自定义的源文件与头文件(一般都会成对儿创建),不过这些自行创建的文件无法直接运行,仍需要在主源文件里对其进行调用,经过编译、链接等一系列操作后,程序才能顺利执行。
头文件(.h):主要用于存放声明信息,如宏定义,变量,函数,结构体,类等,方便多个源文件共享这些声明,当需要修改某个函数的声明时,只需修改头文件即可,类似于 Java 中的接口。
源文件(.c):主要用于存放头文件声明对应的实现,类似于 Java 中的实现类。
武技:测试用户文件
- 开发头文件:
// test/Test.h
// Created by JoeZhou on 2025/3/3.
//
#ifndef V1_1_BASIC_ROOKIE_TEST_H
#define V1_1_BASIC_ROOKIE_TEST_H
#include <stdio.h>
/** 函数声明 */
void test();
#endif //V1_1_BASIC_ROOKIE_TEST_H
- 开发源文件:
// test/Test.c
// Created by JoeZhou on 2025/3/3.
//
#include "Test.h"
/** 函数实现 */
void test() {
printf("test");
}
- 测试头文件:
// main.c
#include <stdio.h>
#include "test/Test.h"
int main() {
test();
}
E03. 开发CPP项目
1. 创建CPP项目
武技:使用 CLion 创建 C++ 项目 v1-1-basic-rookie
点击 New Project ,然后选择 C++ Executable 以创建 C++ 语言项目:
- 在 location 栏填写项目地址,建议多指定一层目录,目录不存在会自动创建。
- 在 Language standard 栏选择 C++20 以配置 C++ 语言版本。
- 点击 Create 完成项目创建。
2. 主源文件
心法:在 C 语言里,main.cpp 通常是程序的主源文件,main 函数是程序的入口点。
include命令:在 C 和 C++ 标准中,使用 #include xxx
命令来引入程序所需的头文件,比如主文件中引入的 iostream 头文件,就是系统提供的标准输入输出库,对比 C 语言的不同,C++ 在引入头文件时省略了 .h 后缀。
主函数返回值:在 C 和 C++ 标准中,主函数通常要求返回 int 整数,如果主函数中没有编写 return 语句,则隐式返回 0,表示程序正常结束,其他非零值表示程序异常结束。
心法:在主文件中开发 HelloWorld 代码
- 开发 HelloWorld 代码:
// main.cpp
# 引入IO头文件
#include <iostream>
/**
* 主函数
* @return 主函数返回值固定为 int
*/
int main() {
// 输出语句
std::cout << "Hello, World!" << std::endl;
// 主函数的返回值可以省略
return 0;
}
- 编译 HelloWorld 代码:CLion 在运行代码之前,会自动帮我们进行编译工作,无需手动操作。
- 运行 HelloWorld 代码:点击主方法左侧的播放按钮,使用 Debug 的方式运行该方法,在控制台查看结果。
3. 用户文件
心法:在 C++ 语言中,用户能够自主创建自定义的源文件与头文件(一般都会成对儿创建),不过这些自行创建的文件无法直接运行,仍需要在主源文件里对其进行调用,经过编译、链接等一系列操作后,程序才能顺利执行。
头文件(.hpp):主要用于存放声明信息,如宏定义,变量,函数,结构体,类等,方便多个源文件共享这些声明,当需要修改某个函数的声明时,只需修改头文件即可,类似于 Java 中的接口。
源文件(.cpp):主要用于存放头文件声明对应的实现,类似于 Java 中的实现类。
武技:测试用户文件
// test/Test.hpp
// Created by JoeZhou on 2025/3/3.
//
#ifndef V1_1_BASIC_ROOKIE_TEST_HPP
#define V1_1_BASIC_ROOKIE_TEST_HPP
#include <iostream>
/** 函数声明 */
void test();
#endif //V1_1_BASIC_ROOKIE_TEST_HPP
// test/Test.cpp
// Created by JoeZhou on 2025/3/3.
//
#include "Test.hpp"
/** 函数实现 */
void test() {
std::cout << "Hello, World!" << std::endl;
}
// main.cpp
#include <iostream>
#include "test/Test.hpp"
int main() {
test();
return 0;
}
4. 命名空间
心法:在大型项目中,不同的库或者不同开发者编写的代码可能会使用相同的标识符(如变量名、函数名、类名等),这就会导致命名冲突,而 C++ 中提供了命名空间,专门用于解决命名冲突问题。
命名空间允许程序员将代码组织成不同的逻辑单元,每个单元有自己独立的命名空间,在不同命名空间中可以使用相同的名称而不会产生冲突。
定义命名空间:使用 namespace 关键字来定义一个命名空间,命名空间内可以包含变量、函数、类等各种元素:
- 不要在命名空间中对 全局变量或函数(全局常量和内联函数除外) 直接进行初始化,否则当多个源文件同时引入该头文件时,会出现重复定义的错误,可以对全局变量或函数进行 extern 修饰(函数可以省略),表示在外部文件,如对应的源文件中再进行初始化即可。
访问命名空间:通过 命名空间名::元素名 的方式来访问命名空间中的元素。
using 声明:使用 using 声明可以在局部作用域内引入命名空间中的某个特定元素,之后可以直接使用该元素而无需再加上命名空间前缀。
// test/NameSpace.h
// Created by JoeZhou on 2025/3/3.
//
#ifndef V1_1_BASIC_ROOKIE_NAMESPACE_H
#define V1_1_BASIC_ROOKIE_NAMESPACE_H
#include <iostream>
// 定义一个名为 ns1 的命名空间
namespace ns1 {
// 全局变量声明
extern int a;
// 函数声明
extern void test();
}
// 定义一个名为 ns2 的命名空间
namespace ns2 {
// 全局变量声明
extern int a;
// 函数声明
extern void test();
}
#endif //V1_1_BASIC_ROOKIE_NAMESPACE_H
// test/NameSpace.cpp
// Created by JoeZhou on 2025/3/3.
//
#include "NameSpace.h"
int ns1::a = 1;
int ns2::a = 2;
void ns1::test() {
std::cout << "ns1::test().a = " << a << std::endl;
}
void ns2::test() {
std::cout << "ns2::test().a = " << a << std::endl;
}
// main.cpp
#include <iostream>
#include "test/NameSpace.h"
int main() {
ns1::test();
ns2::test();
return 0;
}
E04. 可执行文件
心法:C/C++ 语言需要通过编译器将源代码编译为目标文件,C/C++ 源代码成功编译后,会在项目的 cmake-build-debug 目录中生成一个和项目同名的 exe 文件,该文件就是 Windows 平台中的可执行文件。
武技:测试可执行文件
可将 .exe
文件拖到 CMD 运行,或双击(但双击后会立即关闭,不建议)。
# 查看当前终端编码:936表示GBK编码,65001表示UTF-8编码
chcp
# 将终端临时切换为UTF-8编码
chcp 65001
# 运行可执行文件
v1_1_basic_rookie.exe
S02. 基础入门
E01. 预处理机制
心法:程序运行前,C 预处理器会根据预处理指令,将指定内容进行替换。
1. include 指令
心法:预处理器遇到 include 指令时,会将指定文件内容插入当前文件,替换该指令,比如主页面自带了一行
#include <stdio.h>
代码,用于将系统库中的 stdio.h 头文件引入进来并替换。
指令格式:
// 用于包含标准库头文件
// 编译器会在系统指定的标准库路径中查找这些文件
#include <文件>
// 用于包含用户自定义的头文件
// 编译器首先会在当前源文件所在的目录中查找该文件
// 如果找不到,再到系统指定的其他路径中查找
#include "文件"
武技:测试 include 指令
- 开发 C/C++ Header File 头文件,里面自带的内容可以全部删除:
// init/HelloWorld.h
void testHelloWorld();
- 开发 C/C++ Source File 源文件,建议和头文件同名,方便对应:
// init/HelloWorld.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include "HelloWorld.h"
void testHelloWorld() {
printf("Hello World!\n");
}
- 在 CMakeLists.txt 文件中确认加入了自定义的 C 源文件(没加入自己补),头文件可以不用加入:
注意:通过右键 new 出来的源文件会自动加入到 CMakeLists.txt 的 add_executable()
中,而通过复制粘贴出来的源文件不会,需要手动加入到 CMakeLists.txt 的 add_executable()
中:
- 在主页面中引入指定的头文件,然后运行查看结果:
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include "00_init/HelloWorld.h"
int main() {
testHelloWorld();
}
2. define 指令
心法:预处理器遇到 define 指令时,会将参数批量替换到文本中,该操作通常被称为 宏 或 定义宏 ,该替换操作发生在程序运行之前,属于纯文本替换(字符串中的文本不会被替换)。
指令格式:
// 注意定义宏的时候末尾不能加分号结束
#define 宏名 被替换的内容
武技:测试 define 指令
// init/Define.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
// 将文件中全部 PI 单词替换为 3.14
#define PI 3.14
// 将文件中全部 myDouble 单词替换为 double
#define myDouble double
// 将文件中全部 mySquare(r) 单词替换为 r * r,其中 r 是一个变量,括号为了保证优先级
#define mySquare(r) ((r) * (r))
// 将文件中全部 myLog(msg) 单词替换为 printf("日志:%s\n", #msg)
// msg 是一个变量
// #msg 会将 str 转换为字符串,例如:#123 会转换为 "123",#abc 会转换为 "abc"
#define myLog(msg) printf("日志:%s\n", #msg)
// 将文件中全部 myVar(n) 单词替换为 x##n,其中 n 是一个变量,## 会使用参数进行拼接
// 例如:myVar(1) 会替换为 a1,myVar(2) 会替换为 a2
#define myVar(n) a##n
void testDefine() {
printf("PI = %.2f\n", PI);
myDouble radius = 2.0;
printf("半径 = %.2lf\n", radius);
double area = PI * mySquare(radius);
printf("面积 = %.2lf\n", area);
myLog("调用结束");
int myVar(1) = 10;
printf("a1 = %d\n", a1);
}
3. undef指令
心法:undef 指令用于取消之前使用 define 定义的宏,当编译器遇到 undef 指令后,后续代码将不再识别该宏。
在 C 语言中,宏是定义常量和简单函数的常用手段,因此 undef 指令 的使用相对较为常见。例如,在一些大型项目中,可能会根据不同的编译选项定义不同的宏,当某个宏的作用范围结束后,就可以使用 undef 指令取消其定义,避免对后续代码产生影响。
指令格式:
// 注意取消宏的时候末尾不能加分号结束
#undef 宏名
示例如下:
// init/Define.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#define myInt int
#undef myInt
void testUnDef() {
// myInt a = 10;
// printf("a = %d\n", a);
}
4. 预定义宏
心法:C 语言预定义宏由编译器预设,提供编译环境、代码位置和日期等信息。
预定义宏名称 | 含义 |
---|---|
__STDC_VERSION__ | 编译器支持的 C 标准版本 例如 199901L 表示支持 C99 标准,201112L 表示支持 C11 标准 |
__func__ | 当前函数的名称 |
__DATE__ | 当前的日期,格式为类似 Jun 27 2023 的字符串 |
__TIME__ | 当前的时间,格式为类似 10:23:12 的字符串 |
__FILE__ | 当前源代码文件的名称(含路径)的字符串 |
__LINE__ | 当前所处的行号是多少就替换为多少,整数 |
武技:测试预定义宏
// init/Define.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
void sysDefine() {
// 201112L 表示 C11 标准
printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__);
printf("__func__ = %s\n", __func__);
printf("__FILE__ = %s\n", __FILE__);
printf("__FILE_NAME__ = %s\n", __FILE_NAME__);
printf("__LINE__ = %d\n", __LINE__);
printf("__DATE__ = %s\n", __DATE__);
printf("__TIME__ = %s\n", __TIME__);
}
5. 条件编译
心法:条件编译包含 ifdef, else, endif 等编译指令,且支持嵌套。
重复包含问题:在大型项目中,一个源文件可能会多次包含同一个头文件,或者多个源文件都会包含同一个头文件,若头文件里有全局的变量、函数声明、结构体定义等内容,重复包含头文件就可能导致编译错误,像重复定义之类的问题。
头文件卫士:为了避免重复包含问题,C 语言通常会使用预处理器指令来创建头文件保护(也叫头文件卫士,Header Guards),使用方式如下:
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件内容,比如函数声明、结构体定义等
#endif // HEADER_FILE_NAME_H
指令格式:
示例一:如果 a 已经被 define 定义,则执行内容 A,否则执行内容 B:
#ifdef a 内容A
#else 内容B
#endif
示例二:如果 a 没有被 define 定义,则执行内容 A,否则执行内容 B:
#ifndef a 内容A
#else 内容B
#endif
示例三:如果 a 大于 60,则执行内容 A,如果 a 小于 60,则执行内容 B,如果 a 等于 60 ,则执行内容 C,均不满足时执行内容 D:
#if a > 60 内容A
#elif a < 60 内容B
#elif a == 60 内容C
#else 内容D
#endif
武技:测试常用的条件编译指令
// init/Define.c
// Created by 周航宇 on 2025/2/26.
//
#define monkey "SWK"
#ifdef monkey
#define monkeyResult "monkey被定义了"
#else
#define monkeyResult "monkey没被定义"
#endif
void testIfDef() {
printf("%s", monkeyResult);
}
#define myScore 59
#if myScore > 90
#define myScoreResult "A"
#elif myScore > 80
#define myScoreResult "B"
#elif myScore > 70
#define myScoreResult "C"
#elif myScore > 60
#define myScoreResult "D"
#else
#define myScoreResult "E"
#endif
void testIfElifElse() {
printf("%s", myScoreResult);
}
E02. 程序运行原理
心法:C 语言运行一共分为预处理,编译,汇编,链接,加载 5 个步骤。
预处理器:负责处理以 # 开头的预处理指令,比如文本替换,条件编译和删除注释等工作,实现了代码模块化(头文件),常量定义和条件编译,提升了系统的维护性和移植性。
编译器:负责将预处理后的源代码转为目标代码或中间汇编代码,检查语法并优化代码,编译器将高级语言转为机器码,让程序员用抽象语言编程,无需直接用机器语言。
汇编器:汇编器负责将汇编语言转为二进制指令,并生成一个二进制目标文件,汇编器连接了高级语言与机器语言,将汇编代码转为计算机可执行的机器指令。
链接器:负责将这个二进制目标文件与系统库等进行链接,最终生成平台可执行文件:
- 链接器用于将多个目标文件以及库文件组合成一个可执行文件。
- 链接器使得大型程序可以被分解为多个模块进行开发,便于开发和管理。
加载器:负责将可执行文件载入内存,分配空间,读取代码和数据,设好程序计数器,从入口点开始执行,加载器将程序从磁盘加载到内存,确保其在计算机中正确运行。
1. GCC
心法:GCC 是一个强大的编译器集合,主要用于将高级编程语言(如 C、C++ 等)编译成机器语言。
GCC 原名 GNU C Compiler,只能处理 C 语言,现名 GNU Compiler Collection,拓展支持了 Fortran,Objective -C、Java、Go,汇编等多种语言。
武技:使用 GCC 编译器工具手动编译源代码
- 把 CLion 自带的 GCC 工具的 bin 目录配置到环境变量 path 中,然后重启 CLion 使其生效:
# 配置到 path 中,然后重启 CLion
D:\clion\CLion-2024.3.win\bin\mingw\bin
- 使用 cmd 命令行手动编译源文件:
# 切换到项目目录
cd D:\workspace\c\v1-1-basic-rookie
# 预处理,得到 .i 源文件
gcc -E main.c -o main.i
# 编译,得到 .s 汇编文件
gcc -S main.i -o main.s
# 转换,得到 .o 二进制文件
gcc -c main.s -o main.o
# 链接,得到 .exe 可执行文件,.exe 后缀可以省略
gcc main.o -o main.exe
# 以上四步可以简化为1步:多个 .c 源文件直接空格隔开即可
gcc main.c -o main
# 加载,直接运行可执行文件
./main.exe
2. Make
心法:Make 是 1977 年推出的一个构建工具,用于通过 Makefile 文件中定义的依赖关系来自动化构建项目。
GCC 擅长编译源文件,但不擅长管理项目构建和依赖,而 Make 通过依赖管理和增量构建,高效处理大型项目的构建,特别适合多源文件和复杂依赖关系。
Makefile 编写格式如下:
- 注意:Makefile 中的命令必须以 tab 键开头,不能使用空格。
- 注意:Makefile 中的命令必须以换行符结尾,不能使用分号。
# targets:最终构建的目标,名称自定义,如 HelloWorld.exe
# depends:构建依赖文件,多个依赖文件用空格分割,如 hello.o world.o
# command:构建命令,如 `gcc hello.o world.o -o HelloWorld.exe`
targets: depends
command
targets: depends
command
...
武技:用 Make 工具编译 main.c 和 00_init/HelloWorld.c,并合并生成 main.exe 可执行文件。
- 在根目录下编写 Makefile 文件:
# 根据 main.o 和 helloWorld.o 构建 main.exe
main.exe: main.o HelloWorld.o
gcc main.o 00_init/HelloWorld.o -o main.exe
# 根据 main.c 构建 main.o
main.o: main.c
gcc -c main.c -o main.o
# 根据 00_init/HelloWorld.c 构建 00_init/HelloWorld.o
HelloWorld.o: 00_init/HelloWorld.c
gcc -c 00_init/HelloWorld.c -o 00_init/HelloWorld.o
- 在控制台编译:
- 如果没修改源文件,则本次编译不会做任何事情,提示 “xxx is up to date” 信息。
- 如果修改了源文件,则本次编译都会重新构建目标文件。
# 编译
mingw32-make.exe
# 运行
main.exe
3. CMake
心法:CMake 是一个跨平台的构建工具,用于通过 CMakeLists.txt 文件中定义的依赖关系来自动化构建项目,并且可以根据这些信息为不同的平台生成相应的构建文件。
CLion 默认使用 CMake 构建工具,当我们创建一个项目后,CLion 会自动为我们配置 CMake,而具体的配置都是写在 CMakeList.txt 文件中的。
CMakeList.txt 内容如下:
# 指定当前CMake的最低版本,程序低于此版本时无法构建
cmake_minimum_required(VERSION 3.30)
# 项目名和语言,名称随意,语言默认为C
project(v1_1_basic_rookie C)
# 设定环境变量,如设定C11标准
set(CMAKE_C_STANDARD 11)
# 添加可执行文件,参数包括一个可执行文件名,以及 N 个需要编译的源文件(头文件可以不写)
add_executable(v1_1_basic_rookie main.c 00_init/HelloWorld.c)
武技:测试 CMake 工具
- 把 CLion 自带的 CMake 工具的 bin 目录(如 D:\clion\CLion-2024.3.win\bin\cmake\win\x64\bin)配置到环境变量 path 中。
- 重启 CLion 使其生效。
- 使用 CMake 工具生成 MakeFile 文件:
# 切换到项目根目录
cd D:\workspace\c\v1-1-basic-rookie
# -S 源文件目录 -B 构建目录 -G 生成器
cmake -S . -B cmake-test -G "MinGW Makefiles"
# 切换到构建目录中
cd cmake-test
# 执行 make 编译
mingw32-make.exe
# 运行
v1_1_basic_rookie.exe
E03. 代码注释
心法:注释是对代码的一种文字说明,提高可读性,不参与编译过程,尽量少写废话并规避行尾注释。
单行注释 //:写在方法函数内,独占 1 行,C99 及以上版本支持。
多行注释 /* 内容 */:写在方法函数内,独占 1 或 N 行,C98 及以上版本支持。
文档注释 /** 内容 */ :一般写在方法函数外,独占 1 或 N 行,额外支持 @param,@return 等注解。
TODO 注释:任何注释中都可以使用 TODO 前缀来标记一个待做事项,后续可通过 IDEA 的 TODO 窗口来查看。
武技:测试注释格式
// basic/Comment.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
void testSingleLine(){
// 我是单行注释
printf("单行注释测试!\n");
}
void testMultiLine(){
/* 我是多行注释 */
printf("多行注释测试!\n");
}
/**
* 两个数的求和函数
* @param a 第一操作数
* @param b 第二操作数
* @return 两个操作数的和
*/
int testDoc(int a, int b){
return a + b;
}
E04. 输出语句
心法:输出语句用于将指定的内容显示在控制台上,为了提高输出效率,减少对屏幕的频繁访问,输出语句会使用缓冲机制,这意味着输出的数据不会立即显示在屏幕上,而是先存储在缓冲区中,当缓冲区满或者遇到手动刷新缓冲区的代码时,缓冲区中的数据才会被输出到屏幕上。
相关 API 方法 | 描述 |
---|---|
fflush(stdout) | 手动刷新缓冲区,此时缓冲区中的数据会立刻输出到屏幕上 |
sleep(n) | 程序睡眠 n 秒,需要引入 unistd.h 头文件(Unix 标准系统调用头文件) |
1. printf
心法:格式化输出格式为 printf(“模板”, 不定长参数),需要导入 <stdio.h> 头文件。
格式化输出不换行且不允许空输出:
- 如果输出的变量值的字节长度小于占位符类型的长度,则会在二进制位高位补零。
- 如果输出的变量值的字节长度大于占位符类型的长度,则会在二进制位高位截取。
常用的模板占位符如下:
模板占位符 | 描述 | 示例 |
---|---|---|
%hd | 十进制且有符号的 short 型整数 | short a = 10 |
%d | 十进制且有符号的 int 型整数 | int a = 10 |
%ld | 十进制且有符号的 long 型整数 | long a = 10 |
%lld | 十进制且有符号的 long long 型整数 | long long a = 10 |
%hu | 十进制且无符号的 short 型整数 | unsinged short a = 10 |
%u | 十进制且无符号的 int 型整数 | unsinged int a = 10 |
%lu | 十进制且无符号的 long 型整数 | unsinged long a = 10 |
%llu | 十进制且无符号的 long long 型整数 | unsinged long long a = 10 |
%ho | 八进制的 short 型整数 | short a = 0123 |
%o | 八进制的 int 型整数 | int a = 0123 |
%lo | 八进制的 long 型整数 | long a = 0123 |
%llo | 八进制的 long long 型整数 | long long a = 0123 |
%hx | 十六进制的 short 型整数,小写 x 符号 | short a = 0x123 |
%x | 十六进制的 int 型整数,小写 x 符号 | int a = 0x123 |
%lx | 十六进制的 long 型整数,小写 x 符号 | long a = 0x123 |
%llx | 十六进制的 long long 型整数,小写 x 符号 | long long a = 0x123 |
%hX | 十六进制的 short 型整数,大写 X 符号 | short a = 0X123 |
%X | 十六进制的 int 型整数,大写 X 符号 | int a = 0X123 |
%lX | 十六进制的 long 型整数,大写 X 符号 | long a = 0X123 |
%llX | 十六进制的 long long 型整数,大写 X 符号 | long long a = 0X123 |
%f | float 型浮点数,%.nf 表示保留 n 位小数,默认 n=6 | float f = 3.15F |
%lf | double 型浮点数,%.nf 表示保留 n 位小数,默认 n=6 | double f = 3.15 |
%c | 单一字符,仅支持存储 ASCII 码表中的字符,不支持存储中文 | char a = 'A' |
%s | 字符串 | char a[] = "hello" |
%% | 百分号 | |
%p | 指针 | int arr[] = {1} |
ASCII 表 - 控制字符:ASCII 表中,0~31 表示控制字符,一般无法直接显示,详情如下:
十进制 | 十六进制 | 二进制 | 字符/缩写 | 解释 |
---|---|---|---|---|
0 | 0x00 | 00000000 | NUL (NULL) | 空字符 |
1 | 0x01 | 00000001 | SOH (Start Of Headling) | 标题开始 |
2 | 0x02 | 00000010 | STX (Start Of Text) | 正文开始 |
3 | 0x03 | 00000011 | ETX (End Of Text) | 正文结束 |
4 | 0x04 | 00000100 | EOT (End Of Transmission) | 传输结束 |
5 | 0x05 | 00000101 | ENQ (Enquiry) | 请求 |
6 | 0x06 | 00000110 | ACK (Acknowledge) | 回应/响应/收到通知 |
7 | 0x07 | 00000111 | BEL (Bell) | 响铃 |
8 | 0x08 | 00001000 | BS (Backspace) | 退格 |
9 | 0x09 | 00001001 | HT (Horizontal Tab) | 水平制表符 |
10 | 0x0A | 00001010 | LF/NL(Line Feed/New Line) | 换行键 |
11 | 0x0B | 00001011 | VT (Vertical Tab) | 垂直制表符 |
12 | 0x0C | 00001100 | FF/NP (Form Feed/New Page) | 换页键 |
13 | 0x0D | 00001101 | CR (Carriage Return) | 回车键 |
14 | 0x0E | 00001110 | SO (Shift Out) | 不用切换 |
15 | 0x0F | 00001111 | SI (Shift In) | 启用切换 |
16 | 0x10 | 00010000 | DLE (Data Link Escape) | 数据链路转义 |
17 | 0x11 | 00010001 | DC1/XON (Device Control 1/Transmission On) | 设备控制1/传输开始 |
18 | 0x12 | 00010010 | DC2 (Device Control 2) | 设备控制2 |
19 | 0x13 | 00010011 | DC3/XOFF (Device Control 3/Transmission Off) | 设备控制3/传输中断 |
20 | 0x14 | 00010100 | DC4 (Device Control 4) | 设备控制4 |
21 | 0x15 | 00010101 | NAK (Negative Acknowledge) | 无响应/非正常响应/拒绝接收 |
22 | 0x16 | 00010110 | SYN (Synchronous Idle) | 同步空闲 |
23 | 0x17 | 00010111 | ETB (End of Transmission Block) | 传输块结束/块传输终止 |
24 | 0x18 | 00011000 | CAN (Cancel) | 取消 |
25 | 0x19 | 00011001 | EM (End of Medium) | 已到介质末端/介质存储已满/介质中断 |
26 | 0x1A | 00011010 | SUB (Substitute) | 替补/替换 |
27 | 0x1B | 00011011 | ESC (Escape) | 逃离/取消 |
28 | 0x1C | 00011100 | FS (File Separator) | 文件分割符 |
29 | 0x1D | 00011101 | GS (Group Separator) | 组分隔符/分组符 |
30 | 0x1E | 00011110 | RS (Record Separator) | 记录分离符 |
31 | 0x1F | 00011111 | US (Unit Separator) | 单元分隔符 |
ASCII 表 - 键盘字符:ASCII 表中,32~126 表示键盘字符,能在键盘上直接找到,详情如下:
十进制 | 十六进制 | 二进制 | 字符/缩写 | 解释 |
---|---|---|---|---|
32 | 0x20 | 00100000 | (space) | 空格 |
33 | 0x21 | 00100001 | ! | 感叹号 |
34 | 0x22 | 00100010 | " | 双引号 |
35 | 0x23 | 00100011 | # | 井号 |
36 | 0x24 | 00100100 | $ | 美元符号 |
37 | 0x25 | 00100101 | % | 百分号 |
38 | 0x26 | 00100110 | & | 和号 |
39 | 0x27 | 00100111 | ’ | 单引号 |
40 | 0x28 | 00101000 | ( | 左括号 |
41 | 0x29 | 00101001 | ) | 右括号 |
42 | 0x2A | 00101010 | * | 星号 |
43 | 0x2B | 00101011 | + | 加号 |
44 | 0x2C | 00101100 | , | 逗号 |
45 | 0x2D | 00101101 | - | 减号 |
46 | 0x2E | 00101110 | . | 句号 |
47 | 0x2F | 00101111 | / | 斜杠 |
48 | 0x30 | 00110000 | 0 | 数字0 |
49 | 0x31 | 00110001 | 1 | 数字1 |
50 | 0x32 | 00110010 | 2 | 数字2 |
51 | 0x33 | 00110011 | 3 | 数字3 |
52 | 0x34 | 00110100 | 4 | 数字4 |
53 | 0x35 | 00110101 | 5 | 数字5 |
54 | 0x36 | 00110110 | 6 | 数字6 |
55 | 0x37 | 00110111 | 7 | 数字7 |
56 | 0x38 | 00111000 | 8 | 数字8 |
57 | 0x39 | 00111001 | 9 | 数字9 |
58 | 0x3A | 00111010 | : | 冒号 |
59 | 0x3B | 00111011 | ; | 分号 |
60 | 0x3C | 00111100 | < | 小于号 |
61 | 0x3D | 00111101 | = | 等号 |
62 | 0x3E | 00111110 | > | 大于号 |
63 | 0x3F | 00111111 | ? | 问号 |
64 | 0x40 | 01000000 | @ | 艾特符号 |
65 | 0x41 | 01000001 | A | 大写字母A |
66 | 0x42 | 01000010 | B | 大写字母B |
67 | 0x43 | 01000011 | C | 大写字母C |
68 | 0x44 | 01000100 | D | 大写字母D |
69 | 0x45 | 01000101 | E | 大写字母E |
70 | 0x46 | 01000110 | F | 大写字母F |
71 | 0x47 | 01000111 | G | 大写字母G |
72 | 0x48 | 01001000 | H | 大写字母H |
73 | 0x49 | 01001001 | I | 大写字母I |
74 | 0x4A | 01001010 | J | 大写字母J |
75 | 0x4B | 01001011 | K | 大写字母K |
76 | 0x4C | 01001100 | L | 大写字母L |
77 | 0x4D | 01001101 | M | 大写字母M |
78 | 0x4E | 01001110 | N | 大写字母N |
79 | 0x4F | 01001111 | O | 大写字母O |
80 | 0x50 | 01010000 | P | 大写字母P |
81 | 0x51 | 01010001 | Q | 大写字母Q |
82 | 0x52 | 01010010 | R | 大写字母R |
83 | 0x53 | 01010011 | S | 大写字母S |
84 | 0x54 | 01010100 | T | 大写字母T |
85 | 0x55 | 01010101 | U | 大写字母U |
86 | 0x56 | 01010110 | V | 大写字母V |
87 | 0x57 | 01010111 | W | 大写字母W |
88 | 0x58 | 01011000 | X | 大写字母X |
89 | 0x59 | 01011001 | Y | 大写字母Y |
90 | 0x5A | 01011010 | Z | 大写字母Z |
91 | 0x5B | 01011011 | { | 左花括号 |
92 | 0x5C | 01011100 | \ | 反斜杠 |
93 | 0x5D | 01011101 | } | 右花括号 |
94 | 0x5E | 01011110 | ^ | 脱字符 |
95 | 0x5F | 01011111 | _ | 下划线 |
96 | 0x60 | 01100000 | ` | 重音符 |
97 | 0x61 | 01100001 | a | 小写字母a |
98 | 0x62 | 01100010 | b | 小写字母b |
99 | 0x63 | 01100011 | c | 小写字母c |
100 | 0x64 | 01100100 | d | 小写字母d |
101 | 0x65 | 01100101 | e | 小写字母e |
102 | 0x66 | 01100110 | f | 小写字母f |
103 | 0x67 | 01100111 | g | 小写字母g |
104 | 0x68 | 01101000 | h | 小写字母h |
105 | 0x69 | 01101001 | i | 小写字母i |
106 | 0x6A | 01101010 | j | 小写字母j |
107 | 0x6B | 01101011 | k | 小写字母k |
108 | 0x6C | 01101100 | l | 小写字母l |
109 | 0x6D | 01101101 | m | 小写字母m |
110 | 0x6E | 01101110 | n | 小写字母n |
111 | 0x6F | 01101111 | o | 小写字母o |
112 | 0x70 | 01110000 | p | 小写字母p |
113 | 0x71 | 01110001 | q | 小写字母q |
114 | 0x72 | 01110010 | r | 小写字母r |
115 | 0x73 | 01110011 | s | 小写字母s |
116 | 0x74 | 01110100 | t | 小写字母t |
117 | 0x75 | 01110101 | u | 小写字母u |
118 | 0x76 | 01110110 | v | 小写字母v |
119 | 0x77 | 01110111 | w | 小写字母w |
120 | 0x78 | 01111000 | x | 小写字母x |
121 | 0x79 | 01111001 | y | 小写字母y |
122 | 0x7A | 01111010 | z | 小写字母z |
123 | 0x7B | 01111011 | { | 左花括号 |
124 | 0x7C | 01111100 | | | 竖线 |
125 | 0x7D | 01111101 | } | 右花括号 |
126 | 0x7E | 01111110 | ~ | 波浪号 |
127 | 0x7F | 01111111 | DEL | 删除 |
武技:测试格式化输出
// basic/Print.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <unistd.h>
void testFlush() {
printf("Hello World\n");
sleep(3);
printf("Hello World with flush\n");
fflush(stdout);
sleep(3);
}
void testPrintf() {
printf("十进制数:%hd, %d, %ld, %lld\n",
(short)1, (int)1, (long)1L, (long long)1LL);
printf("十进制数(无符号):%hu, %u, %lu, %llu\n",
(short)2, (int)2, (long)2L, (long long)2LL);
printf("八进制数:%ho, %o, %lo, %llo\n",
(short)01, (int)01, (long)01L, (long long)01LL);
printf("十六进制数(小写):%hx, %x, %lx, %llx\n",
(short)0xa, (int)0xa, (long)0xAL, (long long)0xALL);
printf("十六进制数(大写):%hX, %X, %lX, %llX\n", 0xa, 0xa, 0xAL, 0xALL);
printf("浮点数:%f, %.2f\n",
(double)3.14, (float)3.14F);
printf("字符:%c, %d\n", (char)'A', (char)'A');
printf("字符串:%s\n", "Hello World");
printf("百分号:%%\n");
int arr[] = {1};
printf("指针:%p\n", arr);
// 如果输出的变量值的字节长度小于占位符类型的长度,则会在二进制位高位补零。
// 原值:10000000 00000000 00000000 00000001
// 反码:11111111 11111111 11111111 11111110
// 补码:11111111 11111111 11111111 11111111
// 补零:00000000 00000000 00000000 00000000 11111111 11111111 11111111 11111111
// 真值:4294967295
printf("%lld\n", -1);
// 如果输出的变量值的字节长度大于占位符类型的长度,则会在二进制位高位截取。
// 原值:00000000 00000000 00000000 00000000 11111111 11111111 11111111 11111111
// 补码:00000000 00000000 00000000 00000000
// 截取:11111111 11111111 11111111 11111111
// 反码:10000000 00000000 00000000 00000000
// 补码:10000000 00000000 00000000 00000001
// 真值:-1
printf("%d\n", 4294967295LL);
}
2. puts
心法:C 语言支持使用 put() 将字符串输出到控制台,但只能输出字符串,并自动添加换行符,没有 printf 灵活,且 puts() 也会使用缓冲区。
puts() vs printf():
- puts() 的功能相对单一,只能输出字符串,并且会自动添加换行符。
- printf() 更加灵活,可以输出不同类型的数据,并可以通过格式说明符进行格式化输出。
武技:测试 puts 函数
// basic/Print.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <unistd.h>
void testPuts(){
// puts(123); // error: 类型不匹配
// 打印字符串到控制台
puts("Hello World");
puts("Hello World");
// 刷新缓冲区
fflush(stdout);
// 睡眠3秒,用于测试缓冲区
sleep(3);
}
3. 特殊字符
心法:特殊字符指的是在输出语句中具有自己独特含义的字符
特殊字符 | 英文 | 中文 | 功能 |
---|---|---|---|
\n | newLine | 换行符 | 立刻将光标置于下一行的首位 |
\t | table | 制表符 | 对前一个字符串补充 1-4 个空格,使其字节数为 4 的整数倍 一个英文字母或符号占 1 字节,一个UTF8字符占 2 字节 |
\0 | 空字符 | 一个空字符 NULL | |
\ | escape | 转义符 | 废除紧随其后的一个字符的功能,使其变成一个纯文本 |
武技:测试输出特殊字符
// basic/Print.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
void testSpecialCharacters(){
// 换行符: 立刻将光标置于下一行的首位
printf("世\n界\n");
/*
* 制表符: 对前一个字符串补充1-4个空格,使其字节数为4的整数倍
* eq: 一个UTF8字符 "亚" 占2字节,`\t` 会补充2个空格到4字节
* eq: "亚y" 占3字节,`\t` 会补充1个空格到4字节
* eq: "亚ya" 占4字节,`\t` 会补充4个空格到8字节(因为至少补充1个空格)
*/
printf("亚\t洲\n");
printf("亚y\t洲\n");
printf("亚ya\t洲\n");
// 转义符: "\" 可以抵消紧随其后的一个符号的功能
printf("\\n是换行符\n");
printf("\\r是回行符\n");
printf("\\t是制表符\n");
printf("\\是转义字符\n");
printf("\"\"是双引号\n");
}
E05. 输入语句
心法:输入语句用于将控制台上输入的内容输入到指定的变量中。
1. scanf
心法:当使用 printf 输出内容后,数据会先存入缓冲区,而非立即显示在控制台。
若在 printf 后紧接着使用 scanf,有可能会因为缓冲区没有被刷新,导致 printf 的内容没有输出,此时推荐在 printf 和 scanf 之间使用 fflush(stdout);
手动刷新缓冲区。
武技:测试 scanf 函数
// basic/Print.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
void testScanf(){
// 打印提示信息
printf("请输入一个整数和一个浮点数(空格或回车分割):");
// 手动刷新 stdout 缓冲区
fflush(stdout);
// 使用基本变量接收输入,则必须对接收变量使用 & 进行取地址
// 因为基本类型变量是复制传参,scanf 对基本类型变量的任何操作都无法作用在原变量上。
int a;
double b;
scanf("%d", &a);
scanf("%lf", &b);
printf("a = %d, b = %lf\n", a, b);
// 打印提示信息
printf("请输入一个字符串:");
// 手动刷新 stdout 缓冲区
fflush(stdout);
char c[10];
// 使用数组接收输入,则直接使用数组名称即可
scanf("%s", c);
printf("c = %s\n", c);
}
2. gets
心法:C 语言支持使用 gets() 从控制台读取一行字符串,直至遇到换行符为止,并存入字符数组中。
gets() vs scanf():
- gets 会把末尾的换行符丢弃,并且在字符串末尾添加 \0 作为字符串结束标志。
- scanf 是一个格式化输入函数,能够依据指定的格式字符串读取不同类型的数据,像整数、浮点数、字符串等。
武技:测试 gets 函数
// basic/Print.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
void testGets() {
printf("请输入一个字符串:");
// 手动刷新 stdout 缓冲区
fflush(stdout);
char str[10];
// 接收控制台输入,然后将结果丢给str数组中
gets(str);
printf("接收到的字符串为:%s\n", str);
}
C/C++道经第1卷 - 第1阶 - 新手村(一)
传送门:CB1-1-新手村(一)
传送门:CB1-1-新手村(二)
传送门:CB1-1-新手村(三)