前言:C++学习笔记,主要供自己参考。本文非系统性的知识点总结,只是整理的之前的笔记,内容比较杂。(有些并不是C++的知识点)
主要参考资料:《C++ Primer Plus》、YouTube TheCherno
<本文中的术语解释>
常量:1、1.5f、'a'、"abc"等。(const修饰的变量称为const变量)
1. g++命令
吐槽:用VS等IDE就不需要手动输入编译命令了……
(1) 执行CPP程序
在终端中依次使用如下命令:
cd "路径"
g++ Test.cpp -o Test
start Test.exe
例如,我想要将code目录下的Main文件与Add文件编程成一个可执行程序(Windows操作系统)。则使用以下代码即可:
cd ".\code"
g++ Main.cpp Add.cpp -o Test
start Test.exe
(2) g++(gcc)的编译过程
gcc的编译过程分为四个过程(但主要是编译和连接)
预编译(预处理) | Preprocessing | 处理头文件和宏定义等 |
编译 | Compilation | 将预处理后的源代码转换成汇编码(生成汇编文件) |
汇编 | Assemble | 将汇编码转换成机器码(生成二进制文件) |
链接 | Linking | 将多个二进制文件关联(生成可执行文件) |
-E | 执行预处理 |
-S | 执行预处理、编译 |
-c | 执行预处理、编译、汇编 |
-o | 执行预处理、编译、汇编、链接 |
2. C语言风格字符串
// 复制字符串 s2 到字符串 s1
strcpy(s1, s2)
// 连接字符串 s2 到字符串 s1 的末尾
strcat(s1, s2)
// 返回字符串 s1 的长度
strlen(s1)
// 如果 s1 和 s2 是相同的,则返回 0
// 如果 s1 < s2 则返回值小于 0
// 如果 s1 > s2 则返回值大于 0
strcmp(s1, s2)
// 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置
strchr(s1, ch)
// 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置
strstr(s1, s2)
3. sizeof()
字节大小(Byte) | 32位操作系统 | 64位操作系统 |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int / float | 4 | 4 |
指针 / long | 4 | 8 |
double / long long | 8 | 8 |
4. RUP && UML
吐槽:学不懂,不知道有啥用。
(1) RUP
<1> RUP的三大特点
迭代模型、用例驱动、以架构为中心
<2> RUP中的软件生命周期
初始、细化、构造、交付四个阶段
<3> RUP要求
设计师必须对可扩展性、安全性、可维护性、可延拓性、可重用性、运行速度提出可行方
<4> RUP中有9个核心工作流
6个过程工作流:商业建模、需求、分析和设计、实现、测试、部署
3个支持工作流:配置和变更管理、项目管理、环境
<5> RUP十大要素
开发前景、达成计划、标识和减小风险、分配和跟踪任务、检查商业理由、设计组件构架、构建和测试、验证和评价结果、管理控制变化、提供用户支持
<6> RUP的本质
a、RUP是风险驱动的、基于Use Case技术的、以架构为中心的、迭代的、可配置的软件开发流程。
b、我们可以针对RUP所规定出的流程,进行客户化定制,定制出适合自己组织的实用的软件流程。
因此RUP是一个流程定义平台,是一个流程框架。
(2) UML
四层结构:原介质模型层、元模型层、模型层、用户模型层。
常见视图(由图组成):用例视图、逻辑视图、并发视图、组件视图、开发视图。
用例图:从用户角度描述系统功能。
类图:描述系统中类的静态结构。
对象图:系统中的多个对象在某一时刻的状态。
状态图:是描述状态到状态控制流,常用于动态特性建模
活动图:描述了业务实现用例的工作流程
顺序图:对象之间的动态合作关系,强调对象发送消息的顺序,同时显示对象之间的交互
协作图:描述对象之间的协助关系
构件图:一种特殊的UML图来描述系统的静态实现视图
部署图:定义系统中软硬件的物理体系结构
包图:对构成系统的模型元素进行分组整理的图
组合结构图:表示类或者构建内部结构的图
交互概览图:用活动图来表示多个交互之间的控制关系的图
① 用例图侧重描述用户需求
② 类图侧重描述系统具体实现
③ 类图描述的是系统的结构
④ 序列图描述的是系统的行为
⑤ 构件图描述系统的模块结构,抽象层次较高
⑥ 类图是描述具体模块的结构,抽象层次一般
⑦ 对象图描述了具体的模块实现,抽象层次较低
5. new && delete
吐槽:游戏引擎好像都有GC机制,可能不需要手动delete吧。
- new 和delete都是内建的操作符,语言本身所固定了,无法重新定制,想要定制new和delete的行为,徒劳无功的行为。
- 动态分配失败,则返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败。
- 指针删除与堆空间释放。删除一个指针p(delete p;)实际意思是删除了p所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身(指针p本身并没有撤销,它自己仍然存在,该指针所占内存空间并未释放),释放堆空间后,p成了空指针。
- 内存泄漏(memory leak)和重复释放。new与delete 是配对使用的, delete只能释放堆空间。如果new返回的指针值丢失,则所分配的堆空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,所以必须妥善保存new返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放堆内存空间。
- 动态分配的变量或对象的生命期。我们也称堆空间为自由空间(free store),但必须记住释放该对象所占堆空间,并只能释放一次,在函数内建立,而在函数外释放,往往会出错。
- 要访问new所开辟的结构体空间,无法直接通过变量名进行,只能通过赋值的指针进行访问。
用new和delete可以动态开辟和撤销地址空间。在编程序时,若用完一个变量(一般是暂时存储的数据),下次需要再用,但却又想省去重新初始化的功夫,可以在每次开始使用时开辟一个空间,在用完后撤销它。
6. 正则表达式
吐槽:应该是C++的正则表达式……看着就头大,感觉基本上都是在验证表单信息的时候用到。
匹配对象 | 正则表达式 |
---|---|
QQ号码 | [1-9]\\d{4,} |
电子邮件(Email) | \\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)* |
中国大陆手机号码 | 1\\d{10} |
中国大陆邮政编码 | [1-9]\\d{5} |
中国大陆身份证号(15位或18位) | \\d{15}(\\d\\d[0-9xX])? |
密码(由数字/大写字母/小写字母/标点符号组成,四种都必有,8位以上) | (?=^.{8,}$)(?=.*\\d)(?=.*\\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\\n).*$ |
时间(小时:分钟, 24小时制) | ((1|0?)[0-9]|2[0-3]):([0-5][0-9]) |
符号 | 意义 |
---|---|
^ | 匹配行的开头 |
$ | 匹配行的结尾 |
. | 匹配任意单个字符 |
[…] | 匹配[]中的任意一个字符 |
(…) | 设定分组 |
\ | 转义字符 |
\d | 匹配数字[0-9] |
\D | \d 取反 |
\w | 匹配字母[a-z],数字,下划线 |
\W | \w 取反 |
\s | 匹配空格 |
\S | \s 取反 |
+ | 前面的元素重复1次或多次 |
* | 前面的元素重复任意次 |
? | 前面的元素重复0次或1次 |
{n} | 前面的元素重复n次 |
{n,} | 前面的元素重复至少n次 |
{n,m} | 前面的元素重复至少n次,至多m次 |
| | 逻辑或 |
7. 键盘默认键码值
吐槽:在方块游戏合集中用到过。反正四个箭头是由两个char组成的键码,其他都是一个char。键码值自己试一试就知道了,没必要查表(键码值可以改的)。
按键 | 键码 | 按键 | 键码 |
---|---|---|---|
左箭头 | 224 75 | 空格 | 32 |
右箭头 | 224 77 | A | 65 |
上箭头 | 224 72 | B | 66 |
下箭头 | 224 80 | Z | 90 |
8. 大小端模式
0x123456在内存中的存储方式
——大端模式
低地址 → 高地址
0x12 | 0x34 | 0x56
——小端模式
低地址 → 高地址
0x56 | 0x34 | 0x12
小端模式强制转换类型时不需要调整字节内容,直接截取低字节即可
大端模式由于符号位为第一个字节,很方便判断正负
判断大小端的方法:
1、通过强制类型转换截断
bool IsBigEndian()
{
short a = 0x1234;
char b = *(char*)&a;
if (0x12 == b) return true;
return false;
}
2、利用联合体共享内存的特性,截取低地址部分
bool IsBigEndian()
{
union
{
short a;
char b;
}num{0x1234};
if (0x12 == num.b) return true;
return false;
}
9. 指针(包括:引用、数组)
(1) 指针
a、指针的本质
首先,我很讨厌指针,晦涩难懂但又博大进深,但是我写的指针代码很容易出错,以至于在我眼里指针过于神秘莫测。
这里内容很多……后面补上。
b、空指针
指针值为NULL(C++中NULL就是0)。
c、野指针
指向“未知”地址的指针(例如,指针声明后未初始化,或指针指向的变量已经在内存中释放)。C++中BUG的主要来源之一,这就是为什么我建议能用数组就不要用指针的原因,你用指针去操作数组,不小心下标越界了那不就成了野指针吗?数组下标越界的错误本质上就是野指针的错误。
char* Func()
{
char str[] = "abc";
return str;
}
int main()
{
// char_p声明后未初始化, 为野指针
char* char_p;
// 初始化char_p, 但char_p指向的是已经释放内存的变量的地址, 所以仍为野指针
char_p = Func();
// 为char_p赋值NULL后, char_p为空指针,但不是野指针
char_p = NULL;
}
d、空类型指针(万能指针)
不能取值,不知道有什么用。
void* void_p = NULL;
e、指针常量 && 常量指针 && 指向常量的指针常量
这三个词容易引搞混都是翻译的问题,我都不理解为什么这样叫。
我更喜欢称指针常量为const指针,称常量指针为指向const类型的指针。
那么指向常量的指针常量就是指向const类型的const指针。
char str[] = "abc";
// 声明指向const类型的指针cchar_p
char* const cchar_p = str;
// 声明const指针char_cp
const char* char_cp = str;
//char const* char_cp = str;
// 声明指向const类型的const指针cchar_cp
const char* const cchar_cp = str;
//char const* const cchar_cp = str;
f、指向指针的指针 && 指针数组 && 数组指针
同样的,数组指针就是指向数组的指针,这几个我基本上不会用,还是那样能不用指针就不用指针。
char str[] = "abc";
char char_p = str;
// 声明指向char指针的指针char_pp
char** char_pp = &char_p;
// 声明指向char数组的数组指针char_ap
char(*char_ap)[4] = &str;
// 声明char指针的数组char_pa
char* char_pa[1] = {char_p};
g、深拷贝 && 浅拷贝
深拷贝就是创建当前变量的副本,而浅拷贝就是创建指向当前变量地址的指针。显然,变量与指针谁小谁就更快。一般情况下浅拷贝更快,但有些情况不能用浅拷贝。
大学不是很喜欢收表吗,类比普通docx文件与多人文档(假设文档网址存在docx文件里面)。显然都要先下载docx文件,普通docx文件不会对他人产生影响,且填完还要传回去;多人文档默认权限下你还能去该别人填的东西,填完也不用传回去。
char c1 = 'a';
char& c2 = c1; // 浅拷贝
char c3 = c1; // 深拷贝,比上面的浅拷贝更快
如下,抛开输出语句不谈,就已经深拷贝了7次。若能将深拷贝全部优化为浅拷贝,则能少申请与拷贝28个字节的内容,理论速度也将变为原先的2/3。
struct Vector3 { float x, y, z; };
// 分别为a、b赋值, 深拷贝2次
const Vector3 add(Vector3 a, Vector3 b)
{
// 系统自动声明1个匿名变量并初始化, 深拷贝1次
return { a.x + b.x,a.y + b.y,a.z + b.y };
}
int main()
{
// 系统自动声明2个匿名变量并初始化, 深拷贝2次
// 系统自动声明1个匿名变量并初始化(接收add函数的返回值),深拷贝1次
// 匿名变量给sum赋值,深拷贝1次
Vector3 sum = add({ 0,0,0 }, { 5,5,5 });
//std::cout << "(" << sum.x << "," << sum.y << "," << sum.z << ")\n";
}
然而,实际上我们只能将个3个深拷贝优化为浅拷贝,可惜咯。构造时初始化的深拷贝没办法优化。返回const &的例子可以参考const。
struct Vector3 { float x, y, z; };
// 不要写const Vector3& add(const Vector3& a,const Vector3& b)
// 会造成野指针错误,虽然是const野指针
const Vector3 add(const Vector3& a,const Vector3& b)
{
// 系统自动声明1个匿名变量并初始化, 深拷贝1次
return { a.x + b.x,a.y + b.y,a.z + b.y };
}
int main()
{
// 系统自动声明2个匿名变量并初始化, 深拷贝2次
// 系统自动声明1个匿名变量并初始化(接收add函数的返回值),深拷贝1次
const Vector3& sum = add({ 0,0,0 }, { 5,5,5 });
//std::cout << "(" << sum.x << "," << sum.y << "," << sum.z << ")\n";
}
总之,看变量与指针的大小,若变量小于指针则尽量用深拷贝,否则尽量用浅拷贝。
(2) 数组 && 引用:封装的const指针
a、数组
数组就是对const指针的一种封装,所以数组使用更加方便。
因为数组被封装过,所以在声明时系统会自动为其初始化。然而数组初始化实际上指对数组每个元素对应的内存地址的数据赋初值,我认为广为流传的数组初始化这一说法存在歧义。感觉数组项初始化或数组元素初始化意义更加准确。
// 用数组
int nums1[4];
//nums1 = { 2,1,3 }; // error
// 用指针
int num;
int * const nums2 = #
如下,两种写法效果等价,数组可以在声明时为所有元素赋初值,而指针很显然只能为第一个元素赋初值,因此数组更加高效。
数组还保存了大小信息,当下标越界的时候编译器就会鬼叫,用因此数组更加安全。建议指定数组大小,除非很难数。
// 用数组
int nums1[4] = { 2,1,3 };
// 用指针
int num = 2;
int * const nums2 = #
*(nums2 + 1) = 1;
*(nums2 + 2) = 3;
*(nums2 + 3) = 0;
如下,数组可以用字符串常量初始化,所有的指针都无法用常量初始化。"abc\0cef"实际上应该就是{'a', 'b', 'c'},具体之后总结字符串再细说。
char str[] = "abc"; // "abc"为const char[]类型
//char* str = "abc"; // error
std::cout << (str == "abc") << '\n';
b、引用
不难发现,引用比指针更简洁。其实引用就是对const指针的一种封装,而指针常量被const修饰,所以引用必须在声明时初始化。
int& b_r = a;
int* const b_p = &a;
std::cout << (&b_r == b_p) << '\n';
// b_r等价于 *b_p
// (不要写int& const b_r = a;这种玩意)
引用与const组合使用,参见const。
(4) 总结
引用应该在声明时就初始化。
指针指向的变量释放内存前应该将该指针设为NULL。
只要是指针常量都建议用引用与数组代替,以提高代码可读性与安全性。
10. 共用体(union)
用同一内存段存放几种不同类型的数据(不是同时存放几种,每一瞬时只能存放其中的一种)。
11. 自加(++)、自减(--)的前置与后置运算
前置运算不需要直接计算,而后置运算需要保存副本,并在语句结束后再计算。所以性能上前置运算符优于后置运算符,不过现在的编译器一般会自行优化。
//前置运算
void operator++()
{
++Pages;
}
//后置运算
void operator++(int)
{
++Pages;
}
12. 多态
何为多态,我认为就是“同一个东西在不同的时间或不同的空间下表现出不同的状态”。
那么C++实现多态的途径有很多,如:继承、模板、重载、类型转换、甚至是分支语句。
一般谈到C++的多态都是指父类与子类之间的多态,即通过继承实现的多态。早绑定实现静态多态(隐藏),晚绑定实现动态多态(覆盖、重写)。一般而言,都是直接重写而非隐藏(为了父类指针指向的子类对象可以调用子类方法)。
13. 模板
模板一般声明在函数或类的前面。
// typename关键字可替换为class关键字, T为模板类型的名称, 一般大写
template<typename T>
void Swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
14. 运行时类型识别(RTTI)
RTTI的作用:假设有一个类层次结构,其中的类都是从一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。
- 如果可能的话,
dynamic_cast
运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0(空指针); typeid
运算符返回一个指出对象的类型的值;type_info
结构存储了有关特定类型的信息。
<1> typeid操作符与type_info类
使用时需要包含头文件#include<typeinfo.h>
- typeid()获取的指针是基类类型,不是子类类型或是派生类型;
- typeid()获取的引用是子类型引用。
- 指针指向的类型在typeid()看来是派生类而不是基类;
- 而用一个引用的地址时产生的是基类而不是派生类。
- typeid(指针) = 指针静态类型
- typeid(*指针) = 实际类型
- typeid(引用) = 实际类型
<2> RTTI映射语法
- dynamic_cast(安全类型的向下映射)
- const_cast(常量和变量的映射)
- ststic_cast(为了行为良好或较好使用的映射)
- reinterpret_cast(将某一类型映射回原有类型)
15. 套接字
嵌套字的作用:在WinSock中通过套接字来实现网络通信和管理。
嵌套字包括:原始套接字、流式套接字和数据包套接字。
- Winsocket嵌套字常用函数
- 套接字阻塞模式:阻塞套接字(默认)、非阻塞套接字。
- 面向连接流在通讯前要先建立连接。
16. 标准模板库(STL)
<1> 容器
底层数组:array(静态数组)、vector(动态数组)、deque(双端队列)。
底层链表:forward_list(单向链表)、list(双向链表)。
底层红黑树:set(有序集合)、map(有序映射)。
底层哈希表:unordered_set(无序集合)、unordered_map(无序映射)。
数组 | 链表 | 红黑树 | 哈希表 | |
---|---|---|---|---|
访问第N个元素 | O(1) | O(n) | \ | \ |
查找 | O(n) | O(n) | O(logn) | O(1) |
插入 / 删除 | O(n) | O(1) | O(logn) | O(1) |
哈希表的操作效率不稳定,占用的内存更多。
deque比较特殊,相较于vector而言,在头部增删元素更快,访问元素时稍微慢点,占用内存可能更大(释放不使用的元素,可以让该容器变小)。
array 类(C++ 标准库) | Microsoft Learn
效率高是选用 forward_list 而弃用 list 容器最主要的原因,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。
<2> 容器适配器
stack栈(先进后出)、queue队列(先进先出)、priority_queue优先队列(类比小顶堆、大顶堆)
<3> 算法
使用时需要包含头文件#include <algorithm>
主要用sort()排序函数。
<4> 迭代器
容器所包含的迭代器决定了容器的函数。
17. 代码考虑
- 代码不出错(能跑, 且能实现想要的效果)
- 可读性良好(避免晦涩难懂的代码, 酌情使用下划线和数字命名)
- 较为严格的可见度(而非全都public)
- 可扩展性良好
- 性能良好
18. 浮点数运算的误差
加法会让浮点数变得略大,减法会让浮点数变得略小,故用浮点数比大小的时候一定要小心。
#include <iostream>
#include <iomanip>
int main()
{
std::cout << std::boolalpha << std::fixed << std::setprecision(63);
double alpha = 0.0;
while (alpha < 1.0)
alpha += 0.05;
std::cout << alpha << '\n';
std::cout << (alpha > 0.95) << '\n';
alpha = 1.0;
while (alpha > 0.0)
alpha -= 0.05;
std::cout << alpha << '\n';
std::cout << (alpha < 0.1) << '\n';
}
19. const
(1) 用法
a、修饰变量
const声明后无法赋值,故const变量必须在声明时初始化。可以用非const变量对其初始化(不然第一个const变量怎么诞生?)。
int a = 213;
const int b = a;
//b = 666; // error
b、修饰成员函数
修饰成员函数就是约束该成员函数不会修改成员变量。因为类的实例的const &只能调用被const修饰的非static成员函数,所以所有不需要修改成员变量的非static成员函数都应该用const修饰。
在示例代码中,Print()成员函数若不用const修饰则enemyPosition无法对其进行调用。
(2) const &
在示例代码用Vector3重载的 - 操作符的函数中,通过传递const &类型节省了时间与空间(浅拷贝)。同理,Enemy类中的GetPosition()函数返回const &类型以及enemyPosition使用const &都是一样的目的。
(3) 示例代码
class Vector3
{
public:
float x, y, z;
Vector3() :x(0), y(0), z(0) {}
Vector3(float x, float y, float z) :x(x), y(y), z(z) {}
const Vector3 operator-(const Vector3& oldPosition) const
{
return Vector3(x - oldPosition.x, y - oldPosition.y, z - oldPosition.z);
}
void Print() const
{
std::cout << "(" << x << "," << y << "," << z << ")\n";
}
};
class Enemy
{
Vector3 position;
float speed;
public:
Enemy() :position(Vector3()), speed(1) {}
void MoveUp()
{
position.z += speed;
}
const Vector3& GetPosition()
{
return position;
}
};
int main()
{
Enemy enemy({ 3,3,3 }, 8);
const Vector3& enemyPosition = enemy.GetPosition();
Vector3 oldPosition = enemyPosition;
enemyPosition.Print();
enemy.MoveUp();
enemy.MoveUp();
enemyPosition.Print();
(enemyPosition - oldPosition).Print();
}
(3) 其它
const变量无法隐式转换为非const变量。
// 问题:C++中开辟内存与释放内存的方式(假设需要开辟空间存cnt个int类型的变量)
// C语言风格:
int* nums = (int*)malloc(sizeof(int) * cnt);
free(nums);
// C++风格
int* nums = new int[cnt]();
delete[] nums;
20. 打印一段代码块执行的时间
class Timer
{
private:
std::chrono::time_point<std::chrono::steady_clock> start, end;
public:
Timer()
:start(chrono::steady_clock::now())
{
}
~Timer()
{
end = chrono::steady_clock::now();
std::chrono::duration<float, std::milli> time = end - start;
std::cout << setiosflags(ios::fixed) << setprecision(0) << time.count() << "ms\n";
}
};
21. 输入处理C++每次读取一行作为字符串
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
vector<string> strs;
string str;
while (getline(cin, str))
strs.emplace_back(str);
for (const string &str : strs)
cout << str << '\n';
}
21. C++总结
- 凡是不用修改的值都应该使用const
- 变量尽量在声明时就初始化
=========================================================================
(不知不觉已经写了好长了,不过也越来越混乱了,之后有空拆成几篇小的总结吧……)