Cherno
TheCherno原视频
防御式声明防御在同一个cpp里重复引用.h。1
2
3
4
5
6
...
//另一种简单常用的
多个相同的定义linker报错
在头文件里定义了函数体并被多个cpp重复引用:
- 对.h里的文件的函数体前增加static(仅自己cpp使用)或inline(将函数体直接套入主函数)
- 把函数体放到一个cpp文件中, .h头文件里只声明
无论什么类型的指针本质都是存储内存地址的整数
指针类型只是在读取或修改数据时告诉编译器数据的类型(占多大的内存)
new底层其实是call了malloc,malloc是memory allocation的简写,它负责分配内存,delete则调用了free(),区别是new和delete不仅管理内存,还会调用constructor和destructor,另外它们都是operator,可以重载。
new【】和delete【】其实另两个operator,它们做的事情稍微有点不一样,你调用new【】的时候,必须要指定一个size,但调用delete【】的时候,并没有指定size,这是因为new【】不仅分配了所需要的内存,还会多分配一个额外的空间,来存储这个size,所以char* buffer = new char[8];
,它所做的是分配这样一块内存【8, 0, 0, 0, 0, 0, 0, 0, 0】,连续的,但是多一块在最前面,但是return给你的是跳过那块内存的地址,比如malloc返回的是0x1,但new【】给你返回的是0x1+2(我记得它分配的是一个word(一般是short)的大小,具体大小需要看系统),然后在delete[] buffer;
的时候,它会往前推一个word,因为它知道前面一个word肯定是size,从而拿到size,进而delete所有
memset(buffer,0,8)
将buffer所指的首地址后八个字节都填0
int& ref = a;
引用即别名:引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。引用定义的同时必须赋值 并且赋值后不能改变引用对象
函数传值和传引用(只要在函数定义的形参前加&) 引用能做的指针都能做
C++中struct和class的用法作用相同 不过struct默认成员都为public class默认为private(struct中也可定义方法)
类或结构体外的static表示只对它声明所在的cpp文件中“可见” extern表示要在别的cpp文件中寻找定义
类或结构体内的static共享空间,可看作在类或结构体的命名空间中 Class::element
静态成员函数中不能调用非静态成员 静态成员函数没有 this 指针
静态成员变量使用前必须先初始化(如 int MyClass::m_nNumber = 0;),否则会在 linker 时出错
virtual会创建虚表,虚表中类似存储函数指针,不同类初始化时,保存的函数指针不同,因此不同实例的基类指针,访问虚函数时,其获取的函数指针不同,完成重载
virtual std::string GetName() = 0;
纯虚函数 包含pure vertual function的类我们无法实例化 一定需要子类提供所有纯虚函数的实现子类才能实例化 (接口)
父类中的protected的作用是让子类可以访问父类中的private成员
函数中要返回数组(数组在函数中创建的)需要用new来分配除非传入的是数组的地址参数 int arr[5];
储存在栈中 int* arr = new int[5];
储存在堆中(不能用sizeof求大小)
字符串字面量 只读常量
const Entity&
相当于常量指针常量,指向的内容和本身都不能修改 常对象只能调用常成员函数,而不能调用其他的成员函数 所以调用的函数要加constint GetX const(){}
表示不能修改实际的类成员
mutable 对于const的常函数中需要更改类的成员数据时(Debug) 定义时加上mutable int m_DebugCount;
就可在常函数里修改m_DebugCount
Lambda表达式中可用(?)
尽量使用成员初始化列表 初始化const成员变量只能用初始化列表实现,因为不能赋值
构造函数前加explicit则不能用隐式转换
- 智能指针
智能指针本质就是一个类模板 对象会自动调用析构函数去释放该指针所指向的空间 不需要new和delete
unique_ptr 不能进行拷贝和赋值std::unique_ptr<Entity> entity = std::make_unique<Entity>();
shared_ptr 允许多个智能指针可以指向同一块资源 维护着一份引用计数 计数为0则释放weak_ptr 通常情况下需要跟 shared_ptr 一起使用 构造和析构时引用计数不增加也不会减少 专门用来解决两个类中shared_ptr相互引用而导致的内存泄露问题1
2
3
4
5
6
7{
std::shared_ptr<Entity> entity1;
{
std::shared_ptr<Entity> entity2 = std::make_shared<Entity>();
entity1 = entity2;
}
}
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,深拷贝和浅拷贝的区别是在对象状态中包含其它对象的引用的时候,当拷贝一个对象时,如果需要拷贝这个对象引用的对象,则是深拷贝,否则是浅拷贝。未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,对有引用的对象浅拷贝会释放同一块内存两次,导致程序崩溃。1
2
3
4
5
6
7
8
9
10//深拷贝构造函数
String(const String& other)
:m_Size(other.m_Size)
{
m_String = new char[m_Size+1];
memcpy(m_String,other.m_String,m_Size+1);
}
~String(){
delete[] m_string;
}
总是通过const引用来传递对象
Vector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Vertex {
public:
float m_X, m_Y, m_Z;
Vertex(float x, float y, float z) :m_X(x), m_Y(y), m_Z(z) {}
Vertex(const Vertex& vertex) :m_X(vertex.m_X), m_Y(vertex.m_Y), m_Z(vertex.m_Z) {
std::cout << "copy" << std::endl;
}
};
int main() {
std::vector<Vertex> vertices;
vertices.push_back(Vertex(1, 2, 3));
vertices.push_back(Vertex(4, 5, 6));
vertices.push_back(Vertex(7, 8, 9));
}
//得到六次输出 说明有六次复制分别为 在main的栈中构造临时变量vertex然后调用拷贝复制到vector的栈中1+1+1 以及 vector每次输入后扩展空间要把原来的数据复制到新的内存空间中1+2
优化:1
2
3
4
5
6
7
8int main() {
std::vector<Vertex> vertices;
vertices.reserve(3);
vertices.emplace_back(1, 2, 3);
vertices.emplace_back(4, 5, 6);
vertices.emplace_back(7, 8, 9);
}
//无输出reserve提前预定好vector的空间不用扩展 emplace_back则是传递参数化列表直接在vector中构建vertex
库文件
include中的.h文件 lib文件夹中的.lib .dll dll.lib
静态链接
gcc进行链接时会把静态库.lib中的代码打包到可执行文件中,编译时加载,所以在发布程序的时候无须提供静态库,移植方便,运行快
属性->C++->常规->附加包含目录 填include文件夹路径$(SolutionDir)Dependencies\include
即可在程序中include<.h>
属性->链接器->常规->附加库目录 填lib文件夹路径$(SolutionDir)Dependencies\lib
属性->链接器->输入->附加依赖项中填.lib文件名 即可引入库文件
动态链接
gcc进行链接时,动态库的代码不会被打包到可执行程序中,运行时加载,所以在发布程序的时候需要提供动态库
include同上
链接器输入中附加依赖项改为dll.lib(指向文件) 同时将dll文件放在和exe文件同一目录当中即可同一个Solution中自定义库 对project的属性调成lib include路径改为自己创建的.h的路径 对主项目添加引用选择库项目即可(静态)
模板函数只有在被调用时才会被创建
栈分配比堆分配更好 要很大的存储空间或者要持续很长作用域的才用堆分配
函数指针typedef void(*Function)(int);
Function func = Helloworld//函数名;
每一个函数名都是一个函数指针,函数名后加()就是调用该函数
lambda 更多看为一个变量而不是一个函数 有函数指针的地方可以用 auto lambda = [](//参数){//函数主体};
[外层变量传入](参数变量传入) -> 返回类型 { 函数体 } //(-> 返回变量可以不写,因为很容易从return推断)
例子:[](int value){ std::cout << "value : " << value << std::endl;}
[]用于捕获 要用到函数体外定义的变量时 [=]所有变量按值传递 [&]所有变量按引用传递 [a]a按值传递 [&a]a变量按引用传递
非捕获lambda可以隐式转换为函数指针,而有捕获lambda不可以。所以非捕获lambda可以作为原始函数指针void(*func)(int)
的参数 捕获的参数要为const std::function<void(int)>& func
计时
1
2
3
4
5
6
7using namespace std::literals::chrono_literals;//可以使用s,h,min等时间后缀
auto start = std::chrono::high_resolution_clock::now();//记录时间
std::this_thread::sleep_for(1s);//停1秒
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> duration = end - start;
std::cout << duration.count() << "s" << std::endl;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Timer {//计时类 将类放在作用域中Timer timer;即可
std::chrono::time_point<std::chrono::steady_clock> start, end;
std::chrono::duration<float> duration;
Timer() {
start = std::chrono::high_resolution_clock::now();
}
~Timer() {
end = std::chrono::high_resolution_clock::now();
duration = end - start;
//startTime = std::chrono::time_point_cast<std::chrono::microseconds>(start).time_since_epoch().count();显式类型转换 不损失精度 以微秒输出
float ms = duration.count() * 1000.0f;
std::cout << ms << " ms" << std::endl;
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Timer {
public:
Timer(const char* name) : m_Name(name), m_Stopped(false) {
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}
void Stop() {
auto endTimepoint = std::chrono::high_resolution_clock::now();
long long start = std::chrono::time_point_cast<std::chrono::milliseconds>(m_StartTimepoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::milliseconds>(endTimepoint).time_since_epoch().count();
std::cout << m_Name << ":" << (end - start) << "ms\n";
m_Stopped = true;
}
~Timer() {
if (!m_Stopped)
Stop();
}
private:
const char* m_Name;
std::chrono::time_point<std::chrono::steady_clock> m_StartTimepoint;
bool m_Stopped;
};
类型双关 用不同的类型去解释同一段内存 *(float*)&x//x为int
union里的成员会共享内存,分配的大小是按最大成员的sizeof, union中要是有两个结构体,改变其中一个另外一个里面对应的也会改变. 如果是这两个成员是结构体struct{ int a,b}和int k,如果k=2;对应a=2,b不变 union用不同的结构表示同样的数据很好用
虚析构函数
对派生类的会先调用基类构造函数再调用派生类构造函数 析构时先调用派生类析构函数再调用基类析构函数
而多态时(用基类指针来引用派生类对象)则只会调用基类的析构函数而不会调用派生类的析构函数造成内存泄漏
此时要用虚析构函数告诉编译器还有派生类的析构函数需要调用 基类析构函数前加virtual (不是覆写析构函数)
当需要写一个有子类的类时一定要用虚析构函数定义基类的虚析构并不是增加,而是:基类中只要定义了虚析构(且只能在基类中定义虚析构,子类析构才是虚析构,如果在二级子类中定义虚析构,编译器不认,且virtual失效)
在编译器角度来讲,那么由此基类派生出的所有子类地析构均为对基类的虚析构的重写,当多态发生时,用父类引用,引用子类实例时,此时的虚指针保存的子类虚表的地址,该函数指针数组中的第一元素永远留给虚析构函数指针。所以当delete 父类引用时,即第一个调用子类虚表中的子类重写的虚析构函数此为第一阶段。
然后进入第二阶段:(二阶段纯为内存释放而触发的逐级析构与虚析构无关)而当子类发生析构时,子类内存开始释放,因内存包涵关系,触发父类析构执行,层层向上递进,至到子类所包涵的所有内存释放完成。编译器保证在继承体系中,构造由内到外(内指基类),析构有外到内,子类析构函数结束时,会沿着继承链往上析构
C++的强制类型转换
cast 分为 static_cast dynamic_cast reinterpret_cast const_cast 相比c方便检查
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换,不能用于指针类型的强制转换static_cast<double>(n)//n为int型
reinterpret_cast 用于进行各种不同类型的指针之间强制转换 类型双关
const_cast 仅用于进行去除 const 属性的转换
dynamic_cast 不检查转换安全性,仅运行时检查,如果不能转换,返回null 用于检查一个对象是否为给定类型 存储rtti(运行时内存信息)
条件与操作断点
预编译头文件
C++ 编译器是单独、分别对每个cpp文件进行预编译(也就是对#include,#define 等进行文本替换),生成编译单元。编译单元是一个自包含文件,C++编译器对编译单元进行编译。考虑,头文件A.h被多个cpp文件(比如A1.cpp,A2.cpp)包含,每个cpp文件都要进行单独编译,其中的A.h部分就会被多次重复的编译,造成效率低。
把A.h以及类似A.h这样的头文件,包含到stdafx.h中(当然也可以是其他文件),在stdafx.cpp中包含stdafx.h,打开stdafx.cpp文件的属性对话框,将属性对话框中的”预编译头”设置为 “/Yc”即创建预编译头。对于原先包含A.h的cpp文件,删除#include “A.h”,改成包含stdafx.h,同时打开这些cpp文件(A1.cpp,A2.cpp)的属性对话框(也可打开整个项目的属性),将属性对话框中的 “预编译头” 设置为 “/Yu”即使用预编译头。这样的话,下次编译A1.cpp,A2.cpp的时候,对于A.h头文件中的那部分,就不需要编译了,节省时间。
工程对预先编译的代码进行编译,会生成一个pch文件(precompiled header),该文件包含了编译的结果。注意,可以对任何代码生成到pch中,但是生成pch是个很耗时的操作,因此,只对那些稳定的代码(比如标准模板库)创建预编译头文件。结构化绑定 C++17
结构化绑定允许声明多个变量,可以使用数组,结构体 ,pair等中的元素来初始化
返回多个值可以用元组来实现替代用结构体了1
2
3
4
5
6std::tuple<std::string, int> CreatPerson() {
return { "Cherno",19 };
}
int main() {
auto[name, age] = CreatPerson();
}处理OPTIONAL数据 C++17
#include <optional>
std::optional<type> function(param){statement; return type;}//type返回值
auto result = function(param);
result.has_value()或直接通过if (result)判断数据是否存在
通过result.value()获取数据,或type& res = *result;
result.value_or(xxx)其中xxx作为默认值,如果存在数据返回数据,不存在返回xxx
使用场景—目标值可能存在也可能不存在,比如读取文件并返回内容,可能读取成功有数据,读取成功无数据,读取不成功。单一变量存放多种类型的数据variant C++17
#include <variant>
std::variant<type1, type2> data;
data = type1(xxx)
data.index()
打印第几个类型可以看出是什么类型
std::get<type>(data)
auto value = std::get_if(type)(&data)
如果为type则返回指向其地址的指针否则返回空指针
类似于union,type1与type2表示存储的数据类型。类型安全 所占空间为存储的数据的总和
存储任意类型的数据any 不用确定具体类型的variant多线程并发基础 参考
把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行
#include<thread>
std::thread th1(proc1);
创建了一个名为th1的线程,并且线程th1开始执行
当线程启动后,一定要在和线程相关联的std::thread对象销毁前,对线程运用join()或者detach()方法。作用为等待调用线程运行结束后当前线程再继续运行.join()与detach()都是std::thread类的成员函数,是两种线程阻塞方法,两者的区别是是否等待子线程执行结束。都只能调用一次
互斥量(锁)
共享数据同一时间只能给一个人用 解决数据共享过程中可能存在的访问冲突 开始使用时lock则其他线程只能等待lock 使用完后unlock
死锁 多个线程争夺共享资源导致每个线程都不能取得自己所需的全部资源,程序无法向下执行
在互斥量锁定到互斥量解锁之间的代码叫做临界区 需要互斥访问共享资源的那段代码称为临界区
互斥锁 互斥量mutex就是互斥锁,加锁的资源支持互斥访问
读写锁
shared_mutex读写锁把对共享资源的访问者划分成读者和写者,多个读线程能同时读取共享资源,但只有一个写线程能同时读取共享资源1
2
3
4
5
6
7
8
9
10
11
12
13
14shared_mutex s_m;
std::string book;
void read()
{
s_m.lock_shared();
cout << book;
s_m.unlock_shared();
}
void write()
{
s_m.lock();
book = "new context";
s_m.unlock();
}互斥锁流程
#include<mutex>
std::mutex m;//实例化m对象,不要理解为定义变量
进入临界区之前对互斥量加锁m.lock();,退出临界区时对互斥量解锁m.unlock()
用std::lock_guard<mutex> g1(m);
lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m 可替换lock与unlock 类似智能指针 退出作用域时析构即自动unlock
异步线程
#include<future>
std::async是函数模板,用来启动一个异步任务,返回一个std::future类模板对象,future对象起到了占位的作用 就是说该变量现在无值,但将来会有值 调用std::future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给std::future,即通过FutureObject.get()获取函数返回值 之后才不再阻塞1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using namespace std;
double t1(const double a, const double b)
{
double c = a + b;
Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
return c;
}
int main()
{
double a = 2.3;
double b = 6.7;
future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
return 0;
}std::shard_future 也是占位 但std::future的get()成员函数是转移数据所有权;std::shared_future的get()成员函数是复制数据 future对象的get()只能调用一次 无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。 std::shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。
线程池
程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态 接收到任务后,任务被挂在任务队列,线程池选择一个空闲线程来执行此任务。任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。
std::thread 创建线程时需要传递函数名和函数的参数一并传入作为参数
传入参数为基本数据类型(int,char,string等)时,会拷贝一份给创建的线程
传入参数为指针时,会浅拷贝一份给创建的线程,(只会拷贝对象的指针)
传入的参数为类对象时,会拷贝一份给创建的线程。会调用类对象的拷贝构造函数
传入的参数为引用时,必须用ref(实参)
(而不是&)传递给形参,否则编译不通过,此时不存在“拷贝”行为。引用只是变量的别名,该对象始终只有一份单例
C++中的单例只是一种组织全局变量和静态函数的方式 静态函数不一定对这些变量有作用 不能在外部被实例化(构造函数在private中) 和命名空间作用类似1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Singleton{
public:
Singleton(const Singleton&) = delete;//防止复制
static Singleton& Get() {
static Singleton instance;
return instance;
//return s_Instance;
}
int Function() { return m_Num; }
//static int Function(){return Get().IFunc();}这样可直接调用Singleton::Function()而不用加Get()
private:
Singleton() {}
//static Singleton s_Instance;
//int IFunc(){return m_Num;}
int m_Num = 0;
};
//Singleton Singleton::s_Instance;
在release模式 c++标准库中的std::string在小于等于15个字符的时候不会导致堆分配 只分配一小块基于栈的缓冲区
跟踪内存分配1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct AllocationMetrics {
uint32_t TotalAllocated = 0;
uint32_t TotalFreed = 0;
uint32_t CurrentUsage() { return TotalAllocated - TotalFreed; }
};
static AllocationMetrics s_AllocationMetrics;
void* operator new(size_t size) {//关键就在重写new和delete 简单的直接在重写的函数中打印size 还可打断点看call stack
s_AllocationMetrics.TotalAllocated += size;
return malloc(size);
}
void operator delete(void* memory, size_t size) {
s_AllocationMetrics.TotalFreed += size;
free(memory);
}
static void PrintMemoryUsage() {//在main中调用即可查看共用了多少内存
std::cout << "Memory Usage: " << s_AllocationMetrics.CurrentUsage() << " bytes\n";
}
移动语义 参考
左值时有具体存储空间的变量 右值是临时值 左值引用std::string& str
只能接受左值 加上constconst std::string& str
则也能接受右值 右值引用std::string&& str
只能接受右值
左值有地址和值,可以出现在赋值运算符左边或者右边 右值是数值字符串那些 也可以出现在赋值的左边,右值也有地址,只不过访问不到 如a+1这种右值只是将结果存储在一个临时对象中赋值完则析构
左值对应于一个实实在在的内存位置,右值只是临时的对象,它的内存对程序来说只能读不能写移动语义
很多时候我们只是单纯创建一些右值,然后赋给某个对象用作构造函数。这时候会出现的情况是,我们首先需要在main函数里创建这个右值对象,然后复制给这个对象相应的成员变量。如果我们可以直接把这个右值变量移动到这个成员变量而不需要做一个额外的复制行为,程序性能就这样提高了。
移动构造函数
在赋值时不调用被复制的类的赋值构造函数,而是直接移动
将复制其它类的类的原有的构造函数的参数改为右值引用1
2
3
4// Entity(const String& name)
// : m_Name(name) {}
Entity(String&& name)
: m_Name(name) {}但仍调用了被复制的类的赋值构造函数,实际上接受右值的函数在参数传进来后其右值属性就退化了,所以给m_Name的参数仍然是左值,还是会调用复制构造函数。解决的办法是将name转型
1
2
3
4
5Entity(String&& name)
:m_Name((String&&)name) {}
//或直接用库函数
Entity(String&& name)
:m_Name(std::move(name)) {}移动赋值运算符
将一个已经存在的对象移动给另一个已经存在的对象 移动赋值运算符重载1
2
3
4
5
6
7
8
9
10
11
12
13
14String& operator=(String&& other) {
printf("Moved\n");
if (this != &other) {//移动赋值相当于把别的对象的资源都偷走,不需要移动到自己
delete[] m_Data;
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Data = nullptr;//原来自己的资源一定要释放,否则指向自己原来内容内存的指针没了造成内存泄露
other.m_Size = 0;
}
return *this;
}
//使用时 str1 = std::move(str2);