P 1-7
x86 32位 操作系统 x64 64位操作系统
声明 declaration 定义 definition
注意 link error,
compiling 编译 linking 链接 preprocess 预处理 translation unity 编译单元
任何常量在编译时会被直接算得出, 如代码中5*2, 编译时会直接显示10
编译和链接是两个不同的阶段, 报的错误也是独立的, C开头的错误是编译阶段, LNK开头的错误是链接阶段
在链接阶段, 如果链接的函数不是一模一样, 比如返回值或者传参数量不同,都会引发链接错误,因为实际上还是找不到这个函数
inline 修饰的函数, 是把函数的身体拿过来
P 8-10
int 大约-21亿到21亿 正负2的31次方 还有一个符号位 无符号数, unsigned int
定义float时, float a = 5.5; 鼠标放到5.5,提示是个double, 为了区分, 加大小写f或者F, float a = 5.5f;
bool 0是false, 除了0都是true 本来1或者0只需要1 bit空间,但是在计算机寻址时,只能以bytes为单位, 1bytes=8bit,所以实际上, 是8个bool占用一个bytes, 实际空间没有变化, 仍是1个字节bytes
sizeof 看各个数据类型占用字节
主函数 main()或者指定的主函数, 可以不用返回值,会自动返回0 debug模式会提示其他函数没有返回值, 但是release模式,不会提示,
创建头文件, 自动添加的 #pragama onece , 是一种header guard (头文件保护符), 意思是只include 这个文件一次, 即,只是防止include 到一个翻译单元,即一个单独的cpp文件.
不要认为自己聪明,年轻人, 当头文件过多, 头文件里面会有重复include, 最后再融合, 会出现include不止两次!! 当把#pragama onece 删除,就会报错,重读导入. 因此, #pragama onece 是必须存在的, 它会意识到如果重复导入, 将不会再导入 , 同时,大神还教了另一种 判断重复导入的, #ifndef ..... #endif 在这个里面, 在实际的声明前加入 #define 值, 用那个if判断常量是否存在,如果存在,将不会执行下面的
例:
#ifndef _LOG_H
#define _LOG_H
void Log(const char* message);
void InitLog();
struct Player {};
#endif
这个#ifndef了解即可,老代码,现在都用#progama once <>表示在绝对路径里的include里搜索, ""表示相对路径,即本文件目录搜索,但实际上,""表示所有路径,相对绝对都行, <>是一定是绝对路径的include文件里搜索, "iostream"也是可以的
.h一般是c的扩展库, c++的扩展库一般没有扩展名,这也是一个区分手段, 当然,除了自己写的.h
P 11- 20
调试两大部分: 端点和看内存
端点会出现 locals,autos,watch1. autos和locals一般只展示局部变量或者比较重要的变量, watch1 用来监控变量,自己输入
&变量值,会得到内存位置
constant folding 常量折叠 其实就是 直接回出结果的那种, 2*3 替换为6 2 == 2 替换为ture
c++空指针 是nullptr 是关键字, 避免了c中 NULL 和 0的歧义
if ... else if .... else .. 只会执行一个, else if 实际上就是 else( if )
vs中,头文件,资源文件,源文件都是虚拟的文件夹形式,实际上并没有文件夹,只是筛选器, 当点击展示全部文件(show all files)时,就会显示真实的文件结构,此时再新建文件夹,会真实的存在(new filters 和 new folder),folder是真是的文件夹
void* ptr = 0; 就是无地址,nullptr的意思
小端大端存储, 小端:低位在低地址存储
int* double* void*等,定义的指针,是定义, &var是取var的地址 *ptr是解引用,可以访问ptr内存地址的实际值,也可以对其修改
使用new可以申请内存,使用memset,可以设置申请的内存里的值全部改成特定的 memset(buffer, 0, 8)把buffer这个起始的地址开始,取8个地址,全部设置为0 最后,使用delete[] buffer;删除申请的buffer的堆内存
双层乃至三层指针 char** 第二个*是本指针, 第一个是指向的指针, 注意后面必须是&取地址,即使是数组, buffer是char*,但也要&buffer,取地址 注意是小端,所以是颠倒的
引用 int& 等, 也叫起别名, 不是新建的变量,不占用空间,还是原来的那个 更多的用于函数中变量的传参,可以降低占用空间 能用引用就用引用,代码更简洁 注意引用一次后, 不能再引用,在引用编程了重新赋值
只能指针可以重新指向新的 不加*,就是内存地址,再等于新的即可
class class默认private, struct默认public 这是c++中,class和struct的唯一区别 都可以用函数哦 c中不行
注意class定义,没有(),不是函数, 最后};要有分号
#define a b 是把 a 全换成b
P 21-24
静态 static 两种意思: 在类或结构体外部:该函数链接只在本翻译单元内(cpp),其余cpp不可见 在类或结构体内部: 表示所有的类实例公用一个静态量,只占一个内存,且属于类本身,不能被实例化
如果需要的全局变量在另一个翻译单元(不能是静态变量)里,则需要extern关键字进行修饰
注意结构体中,静态变量和静态方法是怎么调用的,是用命名空间的格式,struct名 :: 静态变量或者静态方法
并且静态变量必须先定义,否则无法调用
结构体中,静态方法无法访问非静态变量,因为非静态的方法默认会有本实例的指针,但是静态方法没有,所以如果不传入本实例的指针,就无法找到非静态变量
局部作用域使用静态变量,提到了生命周期
静态局部变量!!local static 生命周期是整个程序期间
p23写的代码不太懂,单例模式?
静态相关的知识不少,注意再看!这节好帅,跟着写一遍
枚举enums 增加代码可读性 其实就是一个整数 这个比较新,可以多看看了解了解
在类中的的枚举,最好用类去访问,类名::枚举值 不要用类实例 类实例.枚举值 这会混淆,因为levelError看起来像是log对象的成员,而实际上它是Log类的枚举成员。 即 第二种是错误的,虽然某些编译器会用
P 25-31
构造函数 constructors 在每次实例化时运行 只有在实例化时运行,若只是调用类静态方法,则不会运行 一般用于类中变量自动初始化! 直接在类中构建一个同类名函数,没有返回值,可传参,也可不传 传参时, 则创建时,需要输入要初始化的值
可以使用私有化,防止使用构造函数, 或者是在公共下,使用 构造函数=delete; 不使用构造函数
C++ 默认使用类时, 使用构造函数, 即使没有构造函数,也有这个设定 当类中只有静态方法,且不想要构造函数时, 删除构造函数, 会不能实例化
析构函数 destructor 在销毁对象时运行,与构造函数对比 适用于栈和堆分配的内存 在构造函数名前加~即可, 内容自己定义
析构函数或者构造函数, 基本都是自动调用, 尤其析构函数,尽量不要手动调用
继承 inhreitance 定义方式 class 派生类名 : public 父类
虚函数 virtual functions 是实现多态的一种机制 新知识:构造函数初始化列表 为什么要用虚函数, 当我们把派生类实例指向父类类型实例,但我们仍希望是使用派生类,而不是使用父类, 就需要使用虚函数了
虚函数引入了动态分配, 通过虚表(vtable)实现,虚表是包含类中所有虚函数映射的列表 在父类中,将需要在派生类中重写的方法前加入 virture 关键字, 这样,就可以提示类, 这是个虚函数, 需要动态的找实际用的 c++1新标准给重写的虚函数加入了 override 关键字标记,显示的指示了重写的基类中的虚函数 override可以不加,不影响 推荐加,若函数名写错,会报错 虚函数会需要额外的内存存储虚表, 调用时需要遍历查找
纯虚函数,在基类中 只声明,未定义 强制子类去定义 一般称之为接口 让虚函数=0,就是纯虚函数
可见性(visibility) 三个修饰词 private protected public 提到了友元,即其他的可以访问类的私有成员 私有成员,子类是无法访问的,只有自己和友元能访问 protected 这个类和所有派生类可以访问 public是可见性最大的,任何都可以访问
数组 访问越界, debug模式会报错, release模式不会, 引文这是修改了别的内存 所谓数组的索引,就是在第一个内存地址偏移两个字节,
对于数组int example[5]; example[2]=5; 与 int* ptr = example;*(ptr+2)=5 一样 2会自动计算2*4,两个字节,
注意堆栈, 当要返回新创建的数组, 使用new在堆里创建,否则 函数结束,栈里会自定清空那
类里面用堆new创建数组,创建实例后调用,会是指针p->p->实际数组 这样跳来跳去会影响性能 间接内存引用 int* s=new int[5] 这是new方式创建数组
在c++11中,有一个 内置的std::array的数组数据结构,有边界检查,记录数组大小
注意:原生的new创建数组无法获得实际大小,在栈上可以使用sizeof来获得, new不行, 并且,即使是栈中数组,在传递时,会退化为指针,sizeof还是无法获得实际大小
维护数组的大小,这对程序员很重要 原始数组,先提前定义常量,作为数组大小
在一个就是使用std::array<int,5> = 数组名 需要头文件<array>
P 32-33
c++ 11及以后,认为char* 类型的字符串不安全, 必须使用const char* , 且该方式不能随意更改
c++ 有个类叫string , 还有个模板类basic_string, string是将basic_string的char做为模板类的实例 这叫做 模板特化(template Specialization),没听过
std::string 是c++风格, 这个定义的,引号里实际上是char数组 const char[]类型,不能相加 去追加
'' 一般是单个字符, "" 是字符串
char* 的字符串,基本都是常量,不允许更改, 类比 109行
using namespace std::string_literals 14标准的追加字符串 "HZH"s + " hello!" 加个s即可
字符串支持一个变量是多行的 用R"(字符串 换行 字符串)"或者多个"字符串\n"
P 34-36
const 类似于类和结构体的可见性 只是一种承诺,表示不会改,但是可以有方法去改
第一种const 关键在与 * 和 const的位置,有人会写不同的写法 指针常量:int* const p 常量指针:const int* p=int const* p *在const前,代表指针指向不能变 *在const后,代表指向的实际内容不能变 这个要理解,可以组合p,const p p是指针,指针指向就不能变了,是指针常量
第二种const 类和结构体中 const写在类方法外面,只能在类中用 意思是这个类方法不能修改任何成员变量,只读 但是mutable关键字可以改变 mutable int var; var在const方法里可以修改
int* m_X,m_Y; Y将不是指针 必须是int* m_X,* m_Y; 才两个都是指针
const Enity&不能改变e,这个实际上是在改变实际内容,引用本身就是内容 和指针对比
记住,如果没有修改类, 请把方法标记为const 也要记住 mutable关键字 可修改的const量
mutable 关键字 两种用法 (1)结合类中const方法 (2)在lambda表达式用,或者两个结合
一般,只有类实例是const时, 第二种const使用才有效 否则,没有const也能调用,而且不会报错
lambda表达式 先浅理解一下
构造函数初始化列表 member initializer lists 注意,必须按顺序写 区别: 在使用另一类(有默认和传值两种初始化)作为成员函数, 可以避免初始化两次成员类实例, 其实就是直接覆盖了 按原来的, 是复制两份类实例,初始化两次
P 37 - 45
三元运算符 ternary operators 表达式? 结果1 :结果2 在定义时直接可以用, 比一般的少了构造中间变量,是一种优化
三元运算符可以嵌套 表达式1? 表达式2 ? 结果1 : 结果2 : 结果3 == ( (表达式1 && 表达式3 )? (表达式2 ? 结果1 ): 结果2) : 结果3 运算符优先级够了,加不加括号都行
最好不要嵌套使用, 一个就行了
创建对象 即使是空的类,也至少会占用一个字节的内存
堆,栈,全局区,代码区
栈对象有自动的生命周期 由其声明的地方的作用域决定 堆对象一直存在,直到我们释放
using 可以用于命名空间声明 : using namespace std 还可以用来别名 using String = std::string
堆 ,用new关键字 有时候不一定是因为作用域不同而用堆,不用栈, 也可能是栈空间不够,只有1-2M
从性能方面,堆上性能低,速度慢,且必须手动释放 delete 变量;
对于指针对象e 可以e->函数 或者 (*e).函数
new关键字, 返回的是指向内存的指针 必须用指针接收 Etity* e = new Etity("hzh")
选择堆 (1)对象太大 (2)显示控制生命周期
不用delete 会造成内存泄漏 所以出现了智能指针, 后续再讲
new
有一个空闲列表,存储着空闲的内存地址 调用new关键字,会调用底层的c函数 malloc 其返回void指针
new Entity 差不多是 (Entity*)malloc(sizeof(Entity)) 强转类型 但是c++最好用new, 这个不好 malloc与free搭配, new与delete
new int[] 数组 应该 delete[] 指针
plentment new 允许在已经分配的内存上,构建对象 int* b = new int[50]; Entity* e = new(b) Entity();
隐藏构造函数,隐藏转换,explicit(中文,显示的,明确的)关键字 不常用
explicit 禁用隐式转换 就必须显示的调用 构造函数, 不能直接等于数字了 应用场景--可能之一 不让数字和向量比较
操作符和操作符重载
C++ 提供了一套丰富的操作符,包括但不限于算术操作符、比较操作符、逻辑操作符、位操作符以及赋值操作符。此外,C++ 还支持操作符重载,这意味着你可以为自定义类型定义操作符的行为。 逗号,括号也可称为操作符
operator+ 或者operator* 等等
运算符重载,返回的类型各不相同, 要注意, 不是所有的重载都是返回对象
算术运算符=,-,*,/,% 返回对象
比较运算符 ==,!=,<,>,<=, >= 返回bool
赋值运算符 (=, +=, -=, *=, /=, %=) 返回对象的引用 对象&
流插入和提取运算符 (<<, >>) 对于 ostream 或 istream 类的运算符,通常返回对流的引用 std::ostream&
递增和递减运算符 (++, --) 前缀返回引用 后缀返回对象
解引用运算符 (*, ->) *返回对象引用 ->返回对象指针
具体都怎么用,怎么写, 用的时候看看示例代码
this 关键字
在函数内部的this, 指向该函数所属的当前对象实例的指针.
对象生命周期
对于作用域是全局的函数, 不要返回在栈区的局部变量的指针或引用, 函数用完就销毁, 每一次调用都这样 必须用堆区
作用域指针, 不能复制,唯一的 自己写了一个
智能指针,共享指针以及 weak_ptr
作用域指针 Scoped Pointer 使用C++标准库std::unique_ptr 是实现作用域指针概念的智能指针 必须显示调用, unique_ptr定义的就是explicit, 禁用隐式转换
需要头文件<memory>
共享指针 shared_ptr引入了 引用计数, 可以跟踪指针有多少个引用,计数为0会释放
weak_ptr结合shared_ptr使用,只声明,不计数
c++赋值 和 "复制构造函数"" 这节干货多
除了引用,赋值时基本都是 拷贝复制, 注意细节,看代码, strlen不计算\0, 会少一个结束符,导致打印出错
浅拷贝:复制内存地址过去,本质是共享一块内存地址,跟引用差不多
深拷贝: 新的内存地址,不共享一块
在对象里, 想要深拷贝,实际上构建一个"复制构造函数"就行, 会再次初始化
一般最好使用const引用传递, 不要复制
P46 - P49
P46 箭头运算符 对于指针对象,原本需要解引用调用成员, 很繁琐, 箭头运算符改进了这点,很方便
90% 就是普通使用即可
还可以进行 -> 重载 用自己写的 (作用域指针)智能指针 举个例子
箭头运算符还可以获取内存中某个成员变量的偏移量 这个有点叼 用到空指针
空指针实际上是 nullptr(即地址 0),所以访问成员 b 实际上等于从地址 0 开始计算成员 b 的偏移量。
P47 动态数组
模板 template 标准模板库: STL 动态数组模板: std::vector<T> T是要存储的数据类型
Vector模板与数学中的向量无联系, 实际上是一个集合, 数组
自带的标准模板库性能可能慢, 许多公司会自己写自己的标准模板库, 提高性能
std::vector<T> push_back({..}) 添加数据
注意: 大多数情况用引用, 不然会复制整个数组
范围 for 循环 C++11 中引入的特性 用于任何支持迭代器的容器或序列。包括标准模板库(STL)中的几乎所有容器,以及任何自定义类型,只要它们提供了适当的迭代器支持。
for (Vertex& v : vertices) 类似这样的语法
P48 优化vector
原始的vector容量不够, 会重新分配大小,整体复制, 删除原来的
注意 是怎么工作的 为什么在原始的vector里添加,会复制 每再添加, 就会扩容,在新的地址,所以要复制 ,所以添加3次就复制了6次
P49-53
P49 库 ------- 展示了在vs里手动添加 库的过程
最好 将依赖库的源代码 编译为静态或动态库
本节内容, 以二进制文件进行链接, 而不是获取世纪以来哭的源代码并自己编译
对于二进制文件位数的选择, 与操作系统位数无关,而是和要做的应用程序位数有关, x86 32位 x64 64位
库通常包含两部分 includes 和 library 目录和库目录 有动态库和静态库两种
静态库 会被放到可执行文件中, 在exe文件中 或者其他OS里的可执行文件
动态链接库是在运行时被连接的
有一个loadLibrary函数 可以在winapi使用
还可以在应用程序结束时加载dll文件
在链接库时, 头文件包含 引号和尖号的区别 引号:先在相对于这个文件的目录找,找不到再去编译器的include目录找
链接 需要在vs里设置包含的库目录和链接器的链接文件
P50 动态库
静态链接会有更多的优化空间, 因为可以看到更多
静态库是一种在编译阶段直接与应用程序链接的库,其代码在编译时就被拷贝到生成的可执行文件中 在Linux上是以.a结尾,而在Windows上以.lib结尾。
动态库是一种在运行时由操作系统加载的库,程序在启动或执行期间才会链接到库。在Linux上以.so(Shared Object)结尾,而在Windows上以.dll(Dynamic-Link Library)结尾。
.dll 和 dll.lib 必须是一起的, 不能分开
P51 创建和使用库
学习命名空间怎么在头文件声明,怎么在源文件定义
使用vs自动化链接,可以避免改名后找不到
P52 处理多返回值 以及什么是tuple,pair
(1) 使用结构体, 写返回的类型即可, 可以随便加
(2)使用引用, 可以实际修改, 直接修改即可, 会反映到实际的参数上 很妙这里, 他是在调用函数之前,定义参数, 然后引用传入, 这样就避免了在调用的函数外复制
(3)使用指针, 可以传入空指针, 不如引用安全,直观
(4)返回数组,
(5)使用std::array std::array 是一个固定大小的容器,其功能和普通的数组类似,但提供了类似于标准容器(如 std::vector)的接口。std::array 是 C++11 引入的,由 <array> 头文件提供 使用方法 : std::array<int, 5> arr = {1, 2, 3, 4, 5}; <>里指定数据类型和容量大小 可以和动态数组 vector 区别学习
(6)使用std::vector 动态数组 这个在堆区 array在栈区
(7)元组 tuple 可以返回任意数量的值, 可以返回不同类型 std::tuple<返回的各种类型> --- return std::make_tuple<a,b,c...>
(8)std::pair<各种类型> --- return std::make_pair< ... > 对值,这两个值可以是同类型或不同类型。它在 <utility> 头文件中定义,广泛用于那些需要将两个值关联起来的场合
pair和tuple可以合着用, 方法挺多
虽然乱, 但是还是推荐 使用结构体 更易读
P53 模板 template
模板函数不是真的函数,只有在调用时才会创建
模板中的 typename 和 class 可以互用, 不影响
模板, 像是编译器在编译时编程,
好多公司禁用模板 模板不能使用太过, 会容易看不懂
P54-P58
P54 堆和栈的内存比较 heap and stack
内存实际都在 ram里
堆栈不同之处在于 如何分配内存
这个栈区内存分布,编译器不同,方向也不同, VS2022栈区内存是从低地址向高地址分布的 而cherno讲课的版本是从高到低
这些不是连续的,因为中间加了安全卫士(safety guards),确保不会溢出. 新添加的栈变量, 直接移动指针,就可以到达新的地址, 很快
栈离开作用域就会销毁, 所谓销毁, 实际就是栈指针移动到了最前面, 重新分配 栈的更像是一条cpu指令
堆 new和delete new会调用malloc函数, memory allocate(分配)缩写 是一种调用底层函数 有一个free list, 告知空闲的内存 堆得要做的很多
可以看看汇编代码, 很直观的看到分配的区别
P56 宏
宏和模板在时间上, 模板更晚一些
有好多骚操作, 提前定义(在vs设置里设置那种) 替换函数(注意转义换行 \ ) 加不加分号 等等, 有的不太懂
gpt: (1)符号常量宏 π等 (2)函数式宏 (3)条件编译 #ifdef #ifndef #endif (4)多行宏 \
#ifdef 如果定义 #ifndef 如果未定义 #if--#elif--#else #endif 结束之前三种编译, 条件编译必须有
宏不进行类型检查 有潜在缺点
P57 auto 关键字
自动会识别类型 不要滥用
在一些简单的地方,不要用auto 在复杂的代码集, 可以看情况,不得不使用auto 比如一些过长的名称
P58 静态数组 std::array
推荐使用,有边界检查, 不牺牲性能
有size(), 可以知道大小 但是存储时不占用大小,因为不存储, 这是个调用的函数,
array在栈中, vector在堆中
array有边界检查, 过了边界会过不去 原始的数组,没有边界检查,虽然编译报错, 但是那段越界的代码是执行了的 而array会阻止
P59 函数指针 来自于c语言
函数指针 与 auto 的结合 之所以使用auto,就是因为返回的类型太奇怪了
指针类型,会默认知道这是要执行 指针存储的地址的函数, 而取地址是直接的执行 不要想太多
函数作为传参时, 就需要函数指针, 返回的类型写法也要会 void(*函数别名)(传参类型)
P59 - P 68
P59 lambda函数 匿名函数 一次性函数,可以视为变量
lambda函数, 不是原始函数指针, 所以传参时, 函数指针的类型要变化
无捕获的 lambda 可以隐式转换为函数指针。
捕获外部变量的 lambda 不能直接转换为函数指针,但可以使用 std::function 传递 头文件functional
多看看 cppreferrence.com 里面的参考手册, 有很多 需要看手册才会只知道
std::find_if 常常与 lambda 表达式一起使用,因为 lambda 可以直接在调用时内联定义谓词,代码更加简洁。 用于查找第一个满足条件的元素。
P60 为什么不用 using namespace std;
如果自己写了一个 std里的一个类,类名又一样, 会出现歧义问题, 最严重的是都导入命名空间了, 都只写类名, 会编译错误
绝对不要在头文件使用 可以适当的在 if 函数等作用域里用
P61 命名空间
可以起别名, 可以只using ..::.. 命名空间里特定的某个函数
还可以嵌套
P62 线程
thread(线程)用于实现并发编程,允许程序同时执行多个任务。C++11 标准引入了 std::thread 类,它提供了创建和管理线程的方式,使得多线程编程变得更加简洁和安全。
std::this_thread 等等
P63 计时
<chrono> 是一个用于处理时间相关任务的头文件。它提供了一套强大的时间库,用来管理时间点、时间间隔、时钟等功能。<chrono> 被引入自 C++11,主要用于解决 C++ 中时间管理的复杂性问题。
这个chrono 学起来的缺点就是 函数名太长了, 很难记住
P64 多维数组
他这里讲的是动态多维数组 需要指针, 指针嵌套
静态数组很简单
动态多维数组很抽象,很难理解
动态多维数组速度不行, 不如一维的 delete也很麻烦, 容易内存泄漏, 因为删除了指针, 而没有删除里面的值
用一维的存储二维的,速度快, 还好理解 强烈推荐
P65 内置排序 std::sort
实际用起来, 挺多细节的
P66 双关
"类型双关"(Type punning)指的是一种技术,其中利用不同的数据类型来解释相同的内存内容,这通常是为了绕过类型系统的限制,实现底层内存的操作或优化。类型双关在C++中并不是推荐的做法,因为它可能会引起未定义行为,但在一些特定情况下,它仍然被用于实现特定的功能或优化。
使用不同的方法, 访问内存,
在C++中,类型双关是一种技术,它让你能以一种数据类型的视角来查看和操作本来属于另一种数据类型的数据。这种技术通常用于底层编程,比如在需要直接处理硬件或进行性能优化的场合。
不知道怎么理解
对于同一块内存, 可以使用不同的解释看他的值 不是特别懂 但是在性能优化和嵌入式用的多
稍微懂了点, 一个整型的内存 有整数的表示(本身的值) 有IEEE标准的浮点型表示(将原本的值对应的二进制数,用浮点数的计算和读取方式得出) 还有ascii码 表示
这种更多的是 用到了地址 ,在地址上用类型双关
P67 联合体(共用体) union
一个成员,只占用一个内存 空间
&a 虽然叫做 取a的地址 但实际上,他就是一个指针 从表达式 int* ptr = &a 就可以看出, &a本身就是指针 这样,类型双关里的就能说通了
P68 虚析构函数
首先回忆 设么是虚函数, 派生类可能重写父类的方法, 这个方法用虚函数标明--看前面的即可
总结: 如果允许有子类, 父类不许使用虚析构函数, 防止内存泄漏
P69-P74
P69 类型转换
分为 c 语言风格的强制类型转换 c++风格的强制类型转换 static_cast<类型> 值
还有 reinterpret_cast(重新解释) dynamic_cast const_cast 这四种是主要的强转 cast
只是加了语法, 相比c,基本没有变化 cast便于查找强转的位置
同时, 使用这个, 便于 自动检查 类型转换, 看是否兼容
static_cast<type>(expression) 最常用的类型转换,用于不同类型的对象之间的转换,比如将 float 转换为 int。它在编译时检查类型安全,不允许不兼容的转换。
dynamic_cast<type>(expression) 用于处理多态性,安全地将基类指针或引用转换为派生类指针或引用,并在运行时检查是否有效。只适用于含有虚函数的类
const_cast<type>(expression) 用于修改类型的 const 或 volatile 属性,常用于去除对象的常量性。
reinterpret_cast<type>(expression) 提供低级别的重新解释转换,在不同类型之间进行转换,例如将指针类型转换为足够大的整数类型。这种转换风险较高,应谨慎使用。
新手 用 c++ 类型转换 高手用c类型转换 新搜可以学到甚多东西
P70 条件与操作断点 (conditions and actions)
利用vs 的断点功能, 右键有更详细的选项 有条件和操作断点, 可以打印自定义的内容, 这个条件和操作不是语句, 而是断点里的选项
P71 现代C++中的安全(没看, 比较水)
省流助手,用于生产环境使用智能指针,用于学习和了解工作积累,使用原始指针,当然,如果你需要定制的话,也可以使用自己写的智能指针
P72 预编译头文件
大型项目 新手可能还不知道怎么用
可以抓取头文件, 并转换为编译器可以使用的格式,而不必一遍遍读取头文件
一般的头文件,每当修改, 都要重新从头开始编译, 每次都要复制一堆头文件代码, 很耗费时间
预编译头文件 是接受一堆要接受的头文件, 只编译一次, 以二进制文件存储, 对编译器来说, 比单纯的文本处理快很多
不要将频繁更改的头文件放到 预编译头文件里
大型不需要修改的头文件可以使用预编译, 但是如果头文件 里包含了多种大型的, 将减少可读性, 不知道需要哪些文件
对于频繁使用的不需要修改的,比如stl , string,vector等, 非常适合
vs 默认是 stdafx.h 文件, 可以修改
注意: 预编译头文件 需要对对项目设置进行修改, 预编译的 .cpp设置为创建编译头 所在项目 设置为使用预编译, 使用的文件就是 stdafx.h 或者自己改名
电脑性能可能太吊了, 看不出区别
P73 dynamic_cast -- 保护类型不要乱转换
dynamic_cast<type>(expression) 用于处理多态性,安全地将基类指针或引用 转换为派生类指针或引用,并在运行时检查是否有效。只适用于含有虚函数的类
c++安全机制提供的
专门用于 沿继承层次结构进行的强转
貌似需要 虚函数 才能用
这个c++的转换, 实际上就是控制 不要转换成 不是原来类型的量 如果不一样, 就不能转换, 秩转换一样的, 就是为了控制必须是原类型,
如果C的风格, 将会导致类型错乱
转换不成功, 会自动给NULL
之所以会知道类型, 是因为存储了 RTTI(runtime type information) 增加了存储开销, 增加检查 时间
RTTI在 不同的编译器, 一般可以关掉 但是关掉会警告 会抛出异常
P74 基准测试 -- 最好release模式看, 有参考意义,会去除冗余代码
vs 代码中输入 双下划线 _ _debugbreak(); 会自动断点 头文件 <memory>
release模式, 这个断点没用
这节看看时间是怎么用代码编写的
同时测试了 在共享指针中, make_shared比new 更快 也就是make_shared会比共享指针原本的构造函数(new 是调用原始构造函数)快
而unique指针,的make_unique更快 因为不需要共享,不需要计数
P75-P 90
P75 结构化绑定 c++17 特性 也算是多返回值的一种新方法吧
元组细节: 函数返回是元组时, 要用{}括起来, ()不行, 转换不了元组
元组, 需要用 std::get<0-n>(元组变量名) 取值.
或者用 std::tie(变量名1,2) = 函数() 可以自己对应取值
结构化绑定 auto[变量1,2] = 函数() 直接可以, 也不需要提前定义变量. vs 默认 c++ 14 右键项目, 改一下默认即可
P76 处理optional数据 c++ 17 std::optional
处理 有些数据可能存在也可能不存在 比如 文件夹可能无法访问,或者是空的
为了 看到是否是空
"optional"(可选类型)用于处理可能存在也可能不存在的值。每种编程语言都有其实现此概念的方式。
std::optional 是在 C++17 引入的功能,用于安全地管理可能存在或不存在的值。它可以包含一个值或没有值,提供了比使用指针或特殊返回值来指示数据缺失的更安全的选择
P77 单一变量存放多个类型的数据 std::variant
std::variant 允许列出所有可能出现的类型,并定义值 依赖于最后一次赋值
std::variant<类型1,2,3> 变量名
std::get<类型>(变量名) 是一种访问方式 但是没有先验检查类型
使用get_if()函数, 可以结合if语句, 提前检查要类型, get_if<类型>(&variant变量)
除了底层处理, 像桌面等 使用variant更好
std::get_if 是一个模板函数,用于安全地访问 std::variant 或 std::tuple 类型的数据。它尤其用于 std::variant,因为 std::variant 可以持有多种不同类型的值,而 std::get_if 提供了一种类型安全的方式来查询和获取存储在 variant 中的值,如果当前的 variant 实例中存储的类型与请求的类型匹配,则返回一个指向该值的指针;如果不匹配,则返回 nullptr。
缺点: 依赖于最后一次赋值, 也就是说, 虽然能给不同的值, 但每次调用以最近的一次赋值为准, 不能自动的获得别的类型的值
P78 存储任意类型的值 std::any
std::any 类型是一个功能强大的工具,用于存储任何类型的单一值。与 std::variant 不同,std::any 不要求在编译时知道可能存储的类型集。这使得 std::any 更加灵活,但也有一定的性能和类型安全的代价。
std::any_cast 可以尝试提取 std::any 中的值,如果类型不匹配,则会抛出 std::bad_any_cast 异常。
同样依赖于最后一次赋值
需要小的存储空间 variant和any 区别不大(看源码, 可能32字节), 但是大的 any 需要动态分配内存
有点无用? variant 相当于是 any 的 类型安全版本
P79 让c++更快 多线程 看看别的教程
std::ansyc 这节例子 用的游戏引擎 future 头文件
这里面, 出现了 引用有时候会有问题, 换成指针好了, 有一些bug
P80 让字符串更快 c++ 17 std::string_view 实际上是一个指针, 移动指针即可
在 C 和 C++ 中,size_t 是一个数据类型,通常用来表示大小和计数。这是一个无符号整数类型,通常被用来表示数组的大小、字符串的长度或内存块的大小等。其在不同平台上的大小可能有所不同,但设计上是足够大,能够表示进程可能使用的最大内存量。
uint32_t:这是一个无符号 32 位整数类型,定义在 <cstdint>(或在 C 语言中为 <stdint.h>)。无符号意味着它不能表示负数,32位意味着它能存储从 0 到 4294967295 的值。
在 C++ 中,字符串字面量本质上是 const char[] 类型,但它可以自动转换为 std::string 对象。这里的关键在于自动类型转换(或构造)生成的 std::string 对象是一个临时对象。
非常量引用 (std::string&) 而不是常量引用 (const std::string&),代码会编译失败。原因是非常量引用不能绑定到临时对象。编译器不允许这样做,因为非常量引用意味着函数内部可能会修改传入的对象,而绑定一个临时对象到可能会被修改的引用是不安全的——一旦函数执行完毕,修改的内容就会丢失,因为临时对象会被销毁。
字符串创建, 需要分配内存, 取其中的一部分还要分配内存 效率很低
string的 c_str() 返回一个char*类型, 即首地址
P81 可视化基准测试 使用 谷歌浏览器的 tracing 测试
代码有点难, 先不看了, 知道有这个东西先
P82 单例模式 设计模式之一 先暂停, 这个之后再看
单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。单例模式在许多场景中都非常有用,比如在需要控制某些共享资源的访问,或者当对象的创建和销毁成本较高时。
单例有点像名称空间
P91-100