前言
自我介绍
2015年开始学习和使用C++
课程形式
- b站直播(一对一辅导):哔哩哔哩直播
- 将录像剪辑为课程上传b站:C++入门-直播录像(未剪辑)
- 编写配套的CSDN博客
- 实践代码:chudonghao/cpp-tutorial
课程思路
- 思想为纲,尝试理解C/C++,而不是学习C/C++
- 兴趣为先,尽量做有趣的事
- 实践为主,从实践中学习
入门内容大概
- 编译过程:源代码 编译器 构建工具 IDE main
- 从思想出发
- 数据结构与算法
- C++如何表示数据结构
- 基本数据类型
- 内存 变量 结构体
- C++如果表示算法
- 表达式
- 函数
- 返回值
- 名称
- 参数
- 函数体
- 运算符
- 运算符重载
- C++如何组织语言
- 预处理
- 头文件引入
- 宏
- 声明与定义
- 命名空间
- 指针与引用
- 函数的值传递
- 其它基础知识
- 基本输入输出
课程记录
0. 工作与课程安排
C/C++工作方向:
- 机器视觉
- 嵌入式
- Qt 军工(外包) 医疗影像
- 大厂(后端) 大厂(外包)
- 音视频
- 游戏
- …
从发展上看,工作稳定性从高到低:
- 大厂(后端)、游戏、音视频
- 医疗影像、机器视觉、嵌入式
- Qt、外包
课程时间安排:
1-3个月(40-120小时)
0. 开发环境
安装操作系统
Ununtu官网教程:Install Ubuntu desktop | Ubuntu
参考Ununtu文档安装即可。
如果你用虚拟机,则大致过程如下:
- 下载Ubuntu镜像(iso文件)
- 下载并安装VirtualBox/vmware
- 通过VirtualBox创建虚拟机,大致配置:
- 显存调最大
- 内存调当前系统的一半
- CPU调当前系统的一半
- 立即创建虚拟磁盘
- 启动虚拟机并选择下载的iso文件启动
- 按照指引安装Ubuntu;大致过程为:选择语言为中文、选择清除整个磁盘(准备的空磁盘)并安装、时区选择上海、输入计算机名、输入用户名和密码
- 安装成功后重启虚拟机即可
如果你现在使用的是Windows系统,目标是安装双系统,则大致的过程如下(不使用虚拟机,后面有虚拟机大致安装过程):
- 准备一个U盘
- 准备一个空的硬盘并安装
- 下载Ubuntu镜像(iso文件)
- 下载U盘启动器制作工具UltraISO或者balenaEtcher
- 制作U盘启动器
如果是使用UltraISO,则:- 首先打开ISO文件,然后点击菜单栏《启动》->《写入磁盘镜像》
- 选择要安装的U盘
- 点击写入
- 重启电脑
- 按F2/DELETE按键进入BISO,找到启动相关的内容,将U盘作为第一启动项,保存并推出
- 按照指引安装Ubuntu;大致过程为:选择语言为中文、选择清除整个磁盘(选择准备的硬盘)并安装、时区选择上海、输入计算机名、输入用户名和密码
- 安装成功后重启电脑即可
自己熟悉和使用一段时间Ubuntu,刚开始可能不太适应。
适应之后会喜欢Linux操作系统,并且会觉得Windows系统是真的差劲。
常用的操作:
windows
键预览窗口或打开应用ctrl+alt+t
打开控制台
常用的应用:
gedit
文本编辑器gnome-terminal
控制台(输入和执行命令)firefox
浏览器
常用的命令:
cd
切换工作目录
安装编译器-g++
通过控制台执行如下指令(过程中需要输入密码,输入密码时不会显示字符,莫慌):
sudo apt install g++
也可安装编译器clang:
sudo apt install clang
安装IDE-CLion
- 通过控制台安装依赖:
sudo apt install libfuse2
- 下载JetBrains Toolbox App,解压后启动
- 通过JetBrains Toolbox App安装CLion
- 启动CLion
激活CLion可以通过JetBrains免费教育许可证或JetBrains在线商店购买许可证进行。
CLion已经内置CMake和gdb
0. 编译过程、编译器、构建工具、IDE
开发环境准备完毕后,我们要清楚开发过程及各个程序的作用和关系。
各个软件的定位
- CLion IDE
- cmake 构建工具
- gdb/lldb 调试器
- g++/clang 编译器
不同软件的作用
编译器(g++/clang) 用于完成编译过程:
- 预处理 main.cpp(源代码) -> main.cpp.p(预处理结果)
- 编译 main.cpp.p(预处理结果) -> main.cpp.s(汇编代码)
- 汇编 main.cpp.s(汇编代码) -> main.cpp.o(机器码)
- 链接 main.cpp.o(机器码)+系统的库(已经编译好的机器码) -> main(可执行程序)
构建工具(cmake) 用于:
- 组织你的项目/源码
- 创建编译指令
- 最终通过编译器生成可执行程序
IDE(CLion/VS/vscode/qtcreator)(集成开发环境) 用于组织开发环境,完成对下列工具的调用:
- 编辑器(编辑源代码)
- 代码补全工具(帮助编辑源代码)
- 构建工具
- 编译器
- 调试器
1. 课程思路
- 思想为纲,尝试理解C/C++,而不是学习C/C++
- 兴趣为先,尽量做有趣的事
- 实践为主,从实践中学习
比如:
思想1:
数据结构与算法:对现实世界的抽象/对现实世界进行建模
面向过程:1 2 3 4,就是描述一个过程
面向对象:一个过程伴随一个状态
思想2:
C++是一门语言!语言!
不论是汉语、英语、C++,语言是用来表达思想的!它是用来描述人的想法的!
我们要把C++当成语言来学,就像学习英语一样!
有些人可能会把C++当作工具来学习和使用,这完全忽视了语言的含义!
2. 数据结构与算法
2.1 基本数据类型与结构体
基础知识:
计算机的基本结构:
- CPU
- 总线
- 内存
计算机内存如何表示数据:
- 二进制 0 1
- 8个0/1组成一个字节(后续我们用格子来描述)
- 一个格子能代表256个数字(256种状态)
计算机如何表示算法:
- 首先程序是存在内存里的,用数字表示的行为或指令
- 用特定数字代表特定行为,比如
- 加减乘除
- 内存拷贝
- 若干指令组合成为算法
程序:
指令操控数据
C++基本数据类型:
- bool
- char/unsigned char
- short/unsigned short
- int/unsigned int
- long/unsigned long
- long long/unsigned long long
- float
- double
数组
常量:
无法直接修改的值
C风格字符串:
以0结尾
数组
结构体
基础知识:
CPU如何存取内存(格子):
32位64位的含义:
- 连接CPU与内存的总线宽度
- CPU一次性取几个0/1(取几个格子的数据)
- 32位表示CPU一次取4个格子
- 64位表示CPU一次取8个格子
物理内存的排布:
如果是64位,则8个格子为一行内存使用优化:
不要让CPU分两次取一个基本数据类型
结构体的内存结构
结构体的成员访问
2.2 函数、表达式和逻辑语句
函数定义:
<返回值> <函数名>(<若干参数>) {
<若干语句>
return <值>;
}
函数调用
<函数名>(<参数>)
表达式
运算符:
- 可以被视作函数调用
- 优先级
- 结合性
a + b * c;
operator+(a, operator*(b, c));
逻辑语句:
判断和跳转
条件语句(if/switch):只判断
返回/跳转/继续/中断(return/goto/continue/break):只跳转
循环语句(for/while):判断和跳转的结合
int times_eating = 3;
const char *food_1 = "rice";
const char *food_2 = "noodles";
const char *food_3 = "apple";
for (int number_eating = 0; number_eating < times_eating; /*continue*/ ++number_eating) {
if (number_eating == 0) {
std::cout << number_eating + 1 << " " << food_1 << std::endl;
} else {
//
}
continue;
// break;
if (number_eating == 1) {
std::cout << number_eating + 1 << " " << food_2 << std::endl;
}
if (number_eating == 2) {
std::cout << number_eating + 1 << " " << food_3 << std::endl;
}
}// break;
std::cout << "switch: " << std::endl;
{
int number_eating = 0;
for (; number_eating < times_eating;) {
switch (number_eating) {
case 0:
std::cout << number_eating + 1 << " " << food_1 << std::endl;
break;
case 1:
std::cout << number_eating + 1 << " " << food_2 << std::endl;
break;
case 2:
std::cout << number_eating + 1 << " " << food_3 << std::endl;
break;
default:
break;
}
++number_eating;
}
}
3. 数据结构和函数进阶
3.1 常见数据结构
- 数组
- 链表
- 栈
- 队列
- 集合
- 映射
函数调用–栈–递归
3.2 指针与引用
基础知识:
内存的结构
指针:
- 获取指针
- 使用指针
引用:
- 语法糖 – 和指针等价
3.3 函数调用的底层原理
4. 实践-俄罗斯方块
基础知识:
- 应用程序都是基本输入输出系统
控制台程序:
- 打字机
- 显示器
4.1 源代码结构
顶层之可以包含如下内容:
- 预处理指令,例如:
- 头文件引入
- 宏定义
- 定义类型
- 定义变量
- 定义函数
4.2 俄罗斯方块之移动骨牌
文件CMakeLists.txt
的内容(构建工具的代码):
# 设置cmake参数(最小版本)
cmake_minimum_required(VERSION 3.20)
# 声明工程
project(Tetris)
# 设置C++标准c++17
set(CMAKE_CXX_STANDARD 17)
# 创建可执行程序Tetris
add_executable(Tetris main.cpp)
# 指定Tetris链接ncurses库
# 该库可能需要安装: sudo apt install libncurses-dev
target_link_libraries(Tetris PRIVATE ncursesw)
文件main.cpp
的内容:
// 引入std::setlocal函数
#include <locale>
// 引入std::clamp函数
#include <algorithm>
// 引入ncurses库,包括类型和函数:
// * WINDOW - 窗口类型
// * newwin - 创建窗口
// * mvwprintw - 在窗口上显示内容
// * wrefresh - 刷新窗口显示
// * getch - 获取用户输入
// * napms - 等待若干时间
// * ...
#include <ncurses.h>
// 四格骨牌
struct Tetro {
};
// 格子
struct Cell {
// bool 0/1 false/true 1字节
bool has_tetro;//< 有没有骨牌
};
// 棋盘
struct Board {
// 20 行 10 列
Cell cells[20][10];
};
Board board;
WINDOW *board_window;
void CreateWindow() {
//height, width, starty, startx
board_window = newwin(20, 10, 0, 0);
}
void RenderBoard() {
// 遍历行
for (int row = 0; row < 20; ++row) {
// 遍历每个格子
for (int col = 0; col < 10; ++col) {
// 看格子里有没有骨牌,有则绘制*,无则绘制空白
Cell &cell = board.cells[row][col];
if (cell.has_tetro) {
mvwprintw(board_window, row, col, "*");
} else {
mvwprintw(board_window, row, col, " ");
}
}
}
// 刷新界面
wrefresh(board_window);
}
int main() {
// 初始化界面系统ncurses
setlocale(LC_ALL, "");
initscr();
cbreak();
noecho();
curs_set(0);
scrollok(stdscr, TRUE);
nodelay(stdscr, TRUE);
// 创建窗口
CreateWindow();
// 记录骨牌当前的位置
int x = 0;
int y = 0;
// 循环处理用户的输入
for (;;) {
// 等待一毫秒(节约CPU)
napms(1);
// 删除之前的骨牌
board.cells[y][x].has_tetro = false;
// 获取用户输入
char input = getch();
// 处理用户输入
// w a s d 表示上下左右
switch (input) {
case 'w':
y = y - 1;
break;
case 'a':
x = x - 1;
break;
case 's':
y = y + 1;
break;
case 'd':
x = x + 1;
break;
}
// 防止越界
x = std::clamp(x, 0, 9);
y = std::clamp(y, 0, 19);
// 创建新的骨牌
board.cells[y][x].has_tetro = true;
// 绘制棋盘
RenderBoard();
}
// 退出程序
return 0;
}
5.1 声明、定义、命名空间-初步
源代码(编译单元)顶层:
- 定义(全局)变量
- 定义函数
- 定义类型
- 声明(全局)变量
- 声明函数
- 声明类型
函数体:
- 定义局部变量
- 定义结构体
命名空间的创建:
namespace <name>{
}
命名空间的嵌套:
namespace <name>{
namespace <name2>{
}
}
别名:
namespace <alias> = <name>;
可以简化使用
using namespace <ns>;
using <symbol>;
- 作用域
符号名解决冲突:
4. ::
代表全局
5. 明确指定命名空间
adl
5.2 类
-
struct vs class
-
private vs public
-
成员变量
-
成员函数
-
外部访问成员变量
-
外部调用成员函数
-
内部访问成员变量
-
内部访问成员函数
-
静态成员变量
-
静态成员函数
-
外部访问静态成员变量
-
外部调用静态成员函数
-
内部访问静态成员变量
-
内部访问静态成员函数
-
成员函数的实现原理
-
声明类型
-
定义类型
-
定义成员变量
-
声明静态成员变量(全局变量)
-
声明成员函数
-
声明静态成员函数(全局函数)
-
定义静态成员变量(全局变量)
-
定义成员函数
-
定义静态成员函数(全局函数)
工程实践的经验总结:数据与过程的绑定
取“类”与“面向对象”
const-常量
const的第二种语法-修饰this指针
volatile
mutable
RAII 有始有终!!
一个对象必然包含两个部分:创建和销毁
类-构造函数-析构函数
// 默认构造
// 带参数构造函数
// 复制构造
// 移动构造
// 析构函数
隐式创建的构造函数-析构函数
隐式调用的构造函数-析构函数
// 初始化列表
模板
宏
模板——相对高级的宏替换
模板的重要作用:替换——代码复用
模板参数推导
类模板
形参:可以是类型或整数(包括枚举)
函数模板
成员函数模板
模板特化
偏特化
全特化
模板实例化
创建一个实实在在的类型定义或函数定义
STL
容器
迭代器
说法1:遍历需要一个工具
说法2:遍历过程中发生变化的对象
new - delete
-
malloc 动态分配一段内存
-
free 释放一段动态内存(malloc分配的)
-
new = malloc + 构造函数
-
delete = 析构函数 + free
new 的用法
// 创建一个基本数据类型
int *p_int = new int;
// 创建一个基本数据类型的数组
int *p_int_array = new int[100];
int **pp_int = new int*;
*pp_int = new int;
class Snake{
public:
Snake();
Snake(int x,int y);
int x;
int y;
};
// 创建一个对象
Snake *p_snake = new Snake{}; // new Snake
// Snake *p_snake2 = new Snake{1, 2};
// 创建一对象数组
Snake *p_shake_array = new Snake[100]{};
delete 的用法
// 销毁一个基本数据类型
delete p_int;
// 销毁一个基本数据类型的数组
delete []p_int_array;
delete *pp_int;
delete pp_int;
// 销毁一个对象
delete p_snake;
// 销毁对象数组
delete []p_shake_array;
函数指针
获取
使用
封装、继承、多态
封装:把一组数据或着过程实现,交给外部使用,外部不用关心内部如何实现
继承: IS-A
覆盖:语法上覆盖父类的函数
通过命名空间调用特定成员函数
从内存角度理解继承
多继承
成员/继承 访问说明符:
private protected public
对于类外访问类成员而言:
访问说明符链路上必须全是public
对于类内访问父类成员而言:
访问说明符链路上不能包含private
virtual
重写
相同的过程,不同的行为
继承中构造函数和析构函数的行为
函数重载
运算符重载