C++是如何工作的
1.预处理
#include :寻找一个指定的文件,将该文件的所有内容拷贝到当前文件中,插入位置即为#include的位置
2.编译
目标平台:x86即windows 32平台,以此类推
解决方案配置:debug:没有优化,好处是可以方便调试代码;release:最大程度地优化
单个文件编译:ctrl+F7
error list:就是个垃圾,会省略很多信息,只能了解一个大概
output窗口:能够提供完整的报错信息
3.链接
链接作用:把所有地obj文件合并成一个exe文件
链接器工作:解析它必须链接的函数符号
4.函数声明(注意与函数定义的区别)
在当前文件使用在其他文件中定义的函数,会引发编译报错,此时必须声明这个函数,编译器会无条件信任在某处确实有这么一个函数。函数声明默认自带extern关键字。
如:void Log(const char* message);
C++编译器
作用:代码文本文件(.c) -> 目标文件(.obj)
报错:未找到函数定义
编译单元(Translation Unit)
在C++中,没有文件的概念(文件没有任何意义)。文件只是为编译器提供源码的一种方式。你只需要告诉编译器这是什么类型的文件以及编译器如何看待它就行了。若创建的文件的扩展名是.cpp,则编译器就会将其视为一个C++文件,类似的,如果创建一个扩展名为.c或.h,那么编译器就会将其视为一个C文件或头文件。
以上均为默认规定,你也可以修改这些规定。
预处理
#include:直接将包含的文件拷贝到当前位置。
#define:直接替换单词。如#define INTEGER int的作用是,搜索所有INTEGER字符并使用int替换它们。
#if:根据if的条件来使得一段代码有效/无效。如:
使有效:
#if 1 int Multiply(int a, int b){ int result = a * b; return result; } #endif
使无效:
#if 0 int Multiply(int a, int b){ int result = a * b; return result; } #endif
正式编译
.obj文件内容是一串二进制代码。
带编译优化和不带编译优化的代码量差别很大。
C++链接器
目的:找到每个符号和函数的位置并将它们链接在一起。
注意:链接既可发生在不同文件之间,也可发生在同一个C++文件中,因为主函数也需要调用其他函数(若该函数已声明但未定义,则链接报错;若该函数未声明也未定义,则编译报错),链接器需要找到主函数的位置。
编译和链接是两个不同的阶段,==若只编译,可以没有主函数==。
报错:未解析的外部符号
如:若Log函数未定义,则下面代码会链接报错
//下面代码会链接报错 #include <iostream> void Log(const char* message); int Multiply(int a, int b) { Log("Multiply"); return a * b; } int main() { std::cout << Multiply(5, 8) << std::endl; std::cin.get(); }
若将Log的调用注释,则不会报错(因为从不调用Log函数,所以链接器不必链接)
//下面代码不会报错 #include <iostream> void Log(const char* message); int Multiply(int a, int b) { //Log("Multiply"); return a * b; } int main() { std::cout << Multiply(5, 8) << std::endl; std::cin.get(); }
若将调用Multiply函数的地方注释掉,则依然会报错(因为虽然当前文件没有用到Multiply,可能其他文件会用到,所以链接器需要链接它)
//下面代码会链接报错 #include <iostream> void Log(const char* message); int Multiply(int a, int b) { Log("Multiply"); return a * b; } int main() { //std::cout << Multiply(5, 8) << std::endl; std::cin.get(); }
若能告诉编译器Multiply函数只在当前文件使用,则可以不连接该函数。可以使用==static关键字==。
//下面代码不会报错 #include <iostream> void Log(const char* message); static int Multiply(int a, int b) { Log("Multiply"); return a * b; } int main() { //std::cout << Multiply(5, 8) << std::endl; std::cin.get(); }
还有一种情况,就是声明函数的返回值类型或者参数类型个数与定义的不一致,也会链接报错。或者是重复定义了该函数,产生了二义性,编译器不知道链接哪个(通常出现在#include文件中出现了函数定义,导致引用的时候重复定义)。
C++头文件
目的:一个只有函数声明而没有函数定义的存储声明的地方。
如果不想每次使用一个函数就声明,那么就可以用#include来自动完成复制和黏贴的操作。
#pragma once 告诉编译器只包含一次
重复包含出错的情况:
void InitLog(); void Log(const char* message); struct Player{};
两种方法避免重复包含:
#pragma once void InitLog(); void Log(const char* message); struct Player{};
#ifndef _LOG_H #define _LOG_H void InitLog(); void Log(const char* message); struct Player{}; #endif
C++调试代码(VISUAL STUDIO)
调试代码的重要两个点:==保存断点==和==查看内存==
必须在debug模式下进行调试,因为release模式会修改代码,可能导致断点永远不命中。
正好在断点的那行代码未执行但即将执行。
==黄色箭头指向的那行代码未执行但即将执行==。
步进:逐行调试,进入当前函数。
步过:执行当前函数,直接到下一行。
步出:执行到当前函数末尾并跳出。
如果想快速跳出多重循环或者复杂的函数,可以在循环外的下面的代码中再设置断点,然后按F5。
C++指针
对指针来说,类型毫无意义,它只是内存的一个地址,一个整数。地址的位数取决于当前运行平台(64位或32位)。
void*指针表示当前不关心这个指针指向的实际的数据类型是什么。
如void* ptr = nullptr;
当需要取内容(*ptr=10
)时,就必须要指定类型。
动态分配8个字节内存到堆空间:
char* buffer = new char[8]; memset(buffer, 0, 8); delete[] buffer; std::cin.get();
C++引用
指针和引用的区别:
-
引用必须引用一个已有的变量,而指针可以初始化为空。
-
一旦定义了一个引用,就不能改变。当需要改变时,可以使用指针来实现。
-
引用必须初始化,也就是必须引用一个东西。
int& ref;
是语法错误的。
int&
可以看作是一个类型。
int a = 5; int* b = &a; int& ref = a;
引用ref并不实际存在,它只存在于源代码中,如果编译这段代码,你不会创建两个变量,而只会得到a一个变量。
引用的常见使用场景:
由于函数的调用只传送副本,调用结束后就会销毁,所以要想实际改变变量的值,必须传递变量地址,并取内容。
void Increment(int* value) { (*value)++; } int main() { int a = 5; Increment(&a); }
如果使用引用,就非常方便。
void Increment(int& value) { value++; } int main() { int a = 5; Increment(a); }
C++类
类的出现只是为了方便程序员组织和整理代码,让他们看上去更加整洁。类不是必须的,所用需要用类来编写的程序也一定能用不支持类的语言来编写(如C语言)。
C++类和结构体的区别
从技术上讲,它们几乎一样,只是在可见性方面有差别。
技术上的唯一差别:==类默认成员均为私有,而结构体默认成员为公有==。
在编程习惯上:如果只是想表示数据的结构,一般使用结构体。如果想表示一个充满功能的类,就像游戏世界或玩家一样,或者其他可能具有继承性的东西,所有这些系统,一般会使用类。
C++中的Static关键字
在翻译单元的范畴内,static相当于“在类中声明一个私有变量”,顾没有其他翻译单元会看到这个变量,链接器不会在全局范围内看到它。
-
两个不同的翻译单元之间不能有同名的全局变量,否则会链接报错。
例如,一个文件里有int variable = 5;
,另一个文件里有int variable = 10
,且均为全局变量,则会链接报错。
-
一种解决方法是,在其中一个文件中改为
extern int variable;
表示引用外部的variable变量。即:extern int variable;
和int variable = 5;
-
还有一种方法是,在其中一个文件中改为
static int variable = 5;
表示variable在该翻译单元私有,其他翻译单元不可见。即:static int variable = 5;
和int variable = 10;
-
函数和变量的情况完全相同
鉴于全局变量的弊端,建议尽可能地使用static将变量限制在翻译单元之内,除非你确实想让他们跨翻译单元链接。
C++静态成员
C++静态成员共享内存,所有实例引用同一块内存区域。
struct Entity { int x, y; void Print() { std::cout << x <<", "<<y<<std::endl; } }; int main() { Entity e; e.x = 2; e.y = 3; Entity e1 = {5, 8}; e.Print(); e1.Print(); }
如果把x,y定义为static,那么初始化程序(Entity e1 = {5, 8};
)就会失败。因为x,y不再是类成员。正确代码:
struct Entity { static int x, y; void Print() { std::cout << x <<", "<<y<<std::endl; } }; int Entity::x; int Entity::y; int main() { Entity e; e.x = 2; //与Entity::x = 2等价 e.y = 3; //与Entity::y = 3等价 Entity e1; e1.x = 5; //与Entity::x = 5等价 e1.y = 8; //与Entity::y = 8等价 e.Print(); e1.Print(); }
打印结果是两个5,8。
静态方法:
静态方法不能访问非静态成员。因为静态方法没有类实例(作为参数)。
类背后的工作原理:
==在类中编写的每一个非静态方法总是会获得一个当前类的一个实例作为参数==。
实际上不存在类,他们只是带有某种隐藏参数的函数
struct Entity { static int x, y; static void Print() { std::cout << x <<", "<<y<<std::endl; } }; int Entity::x; int Entity::y; int main() { Entity e; e.x = 2; //与Entity::x = 2等价 e.y = 3; //与Entity::y = 3等价 Entity e1; e1.x = 5; //与Entity::x = 5等价 e1.y = 8; //与Entity::y = 8等价 e.Print(); //与Entity::Print()等价 e1.Print(); }
非静态方法等价于:
void Print(Entity e) { std::cout << e.x <<", "<<e.y<<std::endl; }
C++枚举类
本质:一种命名值的方式,当你想要使用==整数==来表示==某些状态或某些值==,但是你想给他们一个名称,以便你的代码更具有可读性。
先看下面代码:
int A = 0; int B = 1; int C = 2; int main() { int value = B; if(value == B){ // Do something here } }
这段代码的问题是,ABC并没有分组,他们只是整数,我们希望value只取ABC三者之一。
enum Example { A,B,C //默认情况A=0,B=1,C=2,以此类推 }; int main() { Example value = B; //value只能是ABC中的一种 if(value == 1){ // 可以和整数比较 // Do something here } }
enum Example : unsigned char //默认是int,也可自定义 { A = 0, B = 5, C = 8 //自定义值 }; int main() { Example value = B; //value只能是ABC中的一种 if(value == 1){ // 可以和整数比较 // Do something here } }
C++构造函数
构造函数是一种特殊类型的方法,每次实例化一个对象时都会运行它。
用途:每当构造一个实例时,都希望初始化这个类的成员。(否则这些成员就是内存中的随机值),这与java自动置0不一样,C++必须手动初始化成员。
当然不用构造函数,用一个init()函数也可实现,但是这样每次创建一个实例的时候都要调用init()方法,这无疑增加了代码量。
构造函数要求:
-
没有返回值
-
函数名与类名一致
class Entity { public: float x, y; Entity(){ x = 0.0f; y = 0.0f; } void Print(){ std::cout << x << "," << y << std::endl; } }; int main() { Entity e; e.Print(); }
构造函数也可重载:
class Entity { public: float x, y; Entity(){ x = 0.0f; y = 0.0f; } Entity(float X, float Y){ x = X; y = Y; } void Print(){ std::cout << x << "," << y << std::endl; } }; int main() { Entity e(10.0f, 5.0f); e.Print(); }
C++析构函数
析构函数在对象被销毁时调用,它同时适用于堆栈分配和堆分配。
-
如果你使用new来创建对象,那么当你使用delete的时候就会调用析构函数
-
如果你只有基于堆栈的对象,那么当可用范围和对象被删除时,析构函数将被调用。
class Entity { public: float x, y; Entity(){ x = 0.0f; y = 0.0f; std::cout << "Created Entity!" << std::endl; } ~Entity(){ std::cout << "Destroyed Entity!" << std::endl; } void Print(){ std::cout << x << "," << y << std::endl; } }; void Function() { Entity e; e.Print(); } int main() { std::cin.get(); }
C++继承
目的:避免代码重复,把多个类的共同部分放入一个基类中。
class Entity { public: float X, Y; void Move(float xa, float ya) { X += xa; Y += ya; } }; class Player : public Entity { public: const char* Name; void Print() { std::cout << Name << std::endl; } };
同时具有Entity和Player两种类型。Player是Entity的超集;
Entity的大小是8,而Player的大小是12;
C++虚函数(实现多态)
虚函数允许子类覆写方法。
class Entity { public: std::string GetName(){return "Entity";} }; class Player : public Entity { private: std::string m_Name; public: Player(const std::string& name) : m_Name(name) {} std::string GetName() { return m_Name;} }; void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; } int main() { Entity* e = new Entity(); PrintName(e); Player* p = new Player("Cherno"); PrintName(p); }
上面代码会打印两个Entity(未实现多态,因为父类中的方法是一个普通方法)。如果想让编译器知道第二次调用子类中覆盖的方法,则需要将父类中的方法设置为虚函数(virtual)。
虚函数底层实现方式:设置一个V表,在运行时查表,根据指针类型来判断调用哪个子类中的方法。
子类中的方法可以加一个override关键字来表示它要覆写父类方法。
class Entity { public: virtual std::string GetName(){return "Entity";} }; class Player : public Entity { private: std::string m_Name; public: Player(const std::string& name) : m_Name(name) {} std::string GetName() override { return m_Name;} //帮助查错的 }; void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; } int main() { Entity* e = new Entity(); PrintName(e); Player* p = new Player("Cherno"); PrintName(p); }
虚函数性能缺陷:
-
需要引入额外的内存空间来存储V表
-
需要花额外的时间查找表来找到匹配的函数
C++接口(纯虚函数)
纯虚函数:允许在基类中定义一个没有函数体(=0)的虚函数,然后==强制==让子类实现它们。具有纯虚函数的类不能实例化。
C++中没有接口这个概念(interface关键字),他是用纯虚函数类来实现接口的功能,一切都是类。
class Printable { public: virtual std::string GetClassName() = 0; }; class Entity : public Printable { public: virtual std::string GetName(){return "Entity";} std::string GetClassName() override {return "Entity";} }; class Player : public Entity { private: std::string m_Name; public: Player(const std::string& name) : m_Name(name) {} std::string GetName() override { return m_Name;} //帮助查错的 std::string GetClassName() override {return "Player";} }; void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; } //Print函数不关心这个类具体是什么,但是它肯定实现了GetClassName方法 void Print(Printable* obj) { std::cout << obj->GetClassName() << std::endl; } int main() { Entity* e = new Entity(); //PrintName(e); Player* p = new Player("Cherno"); //PrintName(p); Print(e); //打印:Entity Print(p); //打印:Cherno }
纯虚函数应用场景:
-
你想保证一个类拥有某个方法,那么你可以将它传递给一个相当通用的方法。
C++可见性
可见性:谁可以看见它,谁可以调用它或使用它。
可见性对程序的运行没有任何影响,对程序性能没有影响。它是纯粹在语言中存在的东西,帮你编写更好的代码或帮助你组织代码。
C++只有三种可见关键字:private,protected,public
-
private:只有该类本身或者友元函数有权限直接访问这些变量或调用这些函数。
-
protected:子类也可访问这些变量或调用这些函数。
在同一个类中的public函数也可调用本类的private变量。可见性是对别人看的。
目标:
通过设置可见性,我们可以确保人们不会调用他们不应该调用的其他代码。并且可能会破坏某些东西。就像游戏开挂一样,直接输入x,y坐标就能传送,这是不允许的。
C++数组(Array)
常规数组:(建立在栈上,函数调用结束后自动删除)
int example[5]; int* ptr = example; //下面三条语句等价 example[2] = 5; *(ptr + 2) = 5; *(int*)((char*)ptr + 8) = 5;
动态数组:(建立在堆上,需要手动删除或整个程序运行终止)
int* another = new int[5]; //another用法与example完全相同,只是他们的生命周期不同 delete[] another;
但是sizeof(example)=20,而sizeof(another)=4,也就是堆分配不知道数组的长度。
C++String
保证字符串内容不被改变:const char* name = "Cherno";
const char* name = "Cherno"; name[2] = 'a'; //这里会报错,因为不可修改name的内容
字符串内容可以改变:char* name = "Cherno";
char* name = "Cherno"; name[2] = 'a'; //不会报错,可以修改
在C++中,单引号代表一个字符,双引号代表字符串(字符指针)
STL中的string:
#include <iostream> #include <string> int main() { //正确用法 std::string name = "Cherno"; std::cout << name << std::endl; }
#include <iostream> #include <string> int main() { std::string name = "Cherno" + "hello!"; //报错 std::cout << name << std::endl; }
#include <iostream> #include <string> int main() { //正确 std::string name = "Cherno"; name += "hello!"; std::cout << name << std::endl; }
这是因为+没有重载两个字符数组,但是重载了string和字符数组(字符指针)。
当字符串作为一个参数传递时,经常使用指针传递,因为这样可以减少内存复制的空间,同时为了保证原字符串不被影响(不被修改),可以加上const。
#include <iostream> #include <string> void PrintString(const std::string& string) { std::cout << string << std::endl; } int main() { //正确 std::string name = "Cherno"; name += "hello!"; PrintString(name); }
C++的const
const可以说是假关键字,他在生成代码的时候意义不大,很像==类和结构体中的可见性==。为了保持代码简洁。
它就像是一种承诺,承诺永痕不变,当然你也可以打破这个承诺。
const的前后位置问题:
const int* a = new int;//不能改变a指向的内容,但a本身可以改变 //int const* a = new int;与上同含义 int* const a = new int;//a本身不可以改变,但可以改变a指向的内容 const int* const a = new int;//a本身和a指向的内容都不可改变
const存在于类方法中,代表:==不会修改类的信息(即类的成员变量)==(除了使用mutable修饰的变量),常用于修饰GET方法,是只读的。
class Entity { private: int m_X, m_Y; public: int GetX() const { return m_X; } };
如果你的类方法不想修改或不应该修改类,应该在后面加const,以防止别人调用const Entity& e的时候报错。因为不加const的方法不能保证是只读的。
class Entity { private: int m_X, m_Y; public: int GetX() const { return m_X; } }; void PrintEntity(const Entity& e) { //如果GetX()方法没有加const,下面代码会报错 std::cout << e.GetX() << std::endl; }
C++的Mutable
-
用在被const修饰的类方法中,修改成员变量(即特许)
-
lamda表达式
1的情况主要用于调试程序:
class Entity { private: int m_X, m_Y; mutable int m_DebugCount = 0; public: int GetX() const { m_DebugCount++; return m_X; } }; void PrintEntity(const Entity& e) { std::cout << e.GetX() << std::endl; }
2:lamda表达式
auto f = [=]() mutable { x++; std::cout << x << std::endl; };
C++初始化列表
一般的构造函数:
class Entity { private: std::string m_Name; public: Entity() { m_Name = "Unknow"; } Entity(const std::string& name) { m_Name = name; } const std::string& GetName() const { return m_Name; } }; int main() { Entity e0; std::cout << e0.GetName() << std::endl; Entity e1("Cherno"); std::cout << e1.GetName() << std::endl; }
加了初始化列表后:
==列表的初始化顺序必须与声明成员变量的顺序一致。==
class Entity { private: int m_Score; std::string m_Name; public: Entity() : m_Score(0), m_Name("Unknown") { } Entity(const std::string& name) : m_Score(0), m_Name(name) { } const std::string& GetName() const { return m_Name; } }; int main() { Entity e0; std::cout << e0.GetName() << std::endl; Entity e1("Cherno"); std::cout << e1.GetName() << std::endl; }
C++如何创建/实例化对象
创建对象有两种方式:堆栈(stack),堆(heap)
stack:
有一个自动的生命周期,这个生命周期通常是由它们声明的范围控制的,一旦该变量超出范围,内存就空闲了。因为当该范围结束时,堆栈会弹出。栈空间一般都很小。
示例:
//stack Entity e; //调用默认构造函数 Entity e("Cherno"); //调用自定义构造函数
heap:一个非常大而且神秘的地方。如果你在heap上创建了一个对象,它就会一直在那里,直到你决定不再需要它并将其释放。在heap上创建对象时,使用new关键字,并返回一个指针。
//heap Entity* entity = new Entity("Cherno"); //必须手动释放 delete entity;
在C++中提供了两种不同的分配方式,但是Java只能在堆上创建对象。
==什么时候使用heap?==
-
创建的对象很大很大,以至于超过了栈空间。
-
想显式地控制生命周期。
若不满足上述两点,建议还是在stack上创建对象,因为它更快,自动。
C++的new关键字
new:在堆内存(heap)上申请一块内存空间,空间大小取决于类型,由操作系统查找空闲块表,然后返回一个内存指针,该指针指向连续的内存块。
int* a = new int; int* b = new int[50]; // 200 bytes Entity* e = new Entity[50]; //只是分配了足够的内存,但是没有真正分配对象 Entity* e1 = new Entity(); //既分配了内存又调用了构造函数 Entity* e2 = (Entity*)malloc(sizeof(Entity));//只分配了内存(不推荐) delete[] b; delete[] e; delete e1; //delete也会调用析构函数
注:malloc返回值类型是void*
C++隐式/显式转换
class Entity { private: std::string m_Name; int m_Age; public: Entity(int age) : m_Name("Unknown"), m_Age(age){} Entity(const std::string& name) : m_Name(name), m_Age(-1){} }; int main() { //隐式类型转换,会自动调用构造函数 Entity a = "Cherno"; Entity b = 22; }
显式类型转换:
class Entity { private: std::string m_Name; int m_Age; public: explicit Entity(int age) : m_Name("Unknown"), m_Age(age){} explicit Entity(const std::string& name) : m_Name(name), m_Age(-1){} }; int main() { //下面做法错误 //Entity a = "Cherno"; //Entity b = 22; //下面做法正确 Entity b(22); Entity b = (Entity)22; Entity b = Entity(22); }
C++运算符和运算符重载
运算符:一种用于代替函数功能的符号。
如果在类中重载运算符,则默认运算符左边是这个类。
如果在全局重载运算符,则要把两边的参数都写上。
struct Vector2 { float x, y; Vector2(float x, float y) : x(x), y(y), {} Vector2 Add(const Vector2& other) const { return Vector2(x + other.x, y + other.y); } Vector2 operator+(const Vector2& other) const { return Add(other); } Vector2 Multiply(const Vector2& other) const { return Vector2(x * other.x, y * other.y); } Vector2 operator*(const Vector2& other) const { return Multiply(other); } bool operator==(const Vector2& other) const { return x == other.x && y == other.y; } bool operator!=(const Vector2& other) const { return !(*this == other); } }; std::ostream& operator<<(std::ostream& stream, const Vector2& other) { stream << other.x << ", " << other.y; return stream; } int main() { Vector2 position(4.0f, 4.0f); Vector2 speed(0.5f, 1.5f); Vector2 powerup(1.1f, 1.1f); //普通调用(Java只能这么做) Vector2 result1 = position.Add(speed.Multiply(powerup)); //运算符重载 Vector2 result2 = position + speed * powerup; if(result1 == result2){} // <<重载 std::cout << result1 << std::endl; }
C++的this
==this只能通过成员方法来访问,指向当前对象实例的指针==。
void PrintEntity(const Entity& e); class Entity { public: int x, y; Entity(int x, int y) { this->x = x; this->y = y; PrintEntity(*this); } }; void PrintEntity(const Entity& e) { //Print }
C++作用域
class ScopedPtr { private: Entity* m_Ptr; public: ScopedPtr(Entity* ptr) : m_Ptr(ptr) { } ~ScopedPtr() { delete m_Ptr; } }; int main() { { //隐式转换,e会随着超出作用域而自动销毁stack上的Entity实例 ScopedPtr e = new Entity(); } }
C++智能指针(Smart Pointer)
必须加头文件#include <memory>
智能指针:当你调用new时,你不必调用delete。甚至都不必调用new。本质上是一个原始指针的包装器。当你创建智能指针并make它时,他将调用new并为你分配内存,然后根据你使用的是哪种智能指针,它会在某个时候自动释放。
智能指针种类:
-
唯一指针(Unique Pointer):
调用make_unique<T>()来生成实例,不能被复制,超出范围自动销毁(delete)。
范围指针(Scope Pointer),当该指针超过范围时,他将被销毁(即调用delete)。不能被复制,因为一旦被复制,当原指针自动销毁时,复制的指针就会指向一块已经被释放的内存。推荐首先使用。
class Entity { public: Entity() { std::cout << "Created Entity!" << std::endl; } ~Entity() { std::cout << "Destroyed Entity!" << std::endl; } void Print() {} }; int main() { { //必须显式转换 //使用make的好处是如果构造函数出现异常,不会有悬挂的空指针 std::unique_ptr<Entity> entity = std::make_unique<Entity>(); //因为不可复制,所以下面代码会报错 std::unique_ptr<Entity> e0 = entity; entity->Print(); } //到这里Entity实例会自动销毁 }
-
共享指针(Shared Pointer):
调用make_shared<T>()来生成实例
使用引用计数的方法跟踪对指针有多少引用,一旦引用计数达到零,它就会被删除。
class Entity { public: Entity() { std::cout << "Created Entity!" << std::endl; } ~Entity() { std::cout << "Destroyed Entity!" << std::endl; } void Print() {} }; int main() { { std::shared_ptr<Entity> e0; { std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>(); e0 = sharedEntity; }//到这里sharedEntity销毁,但是Entity实例没有销毁 } //到这里引用计数为0,Entity实例销毁 }
弱指针(weak_ptr):当将一个共享指针赋给一个弱指针时,不会增加引用计数。
{ std::weak_ptr<Entity> e0; { std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>(); e0 = sharedEntity; }//到这里Entity的实例就销毁了,因为弱指针不增加引用次数 }
C++复制和复制构造函数
不必要的复制会浪费性能。
struct Vector2 { float x, y; }; int main() { //不使用指针,全新副本 Vector2 a = {2, 3}; Vector2 b = a; //b是一个全新的副本 //b的值改变与a无关 b.x = 5; //使用指针,两个指针指向相同的内存地址 Vector2* a = new Vector2(); Vector2* b = a; //b和a两个指针指向相同的内存地址 b->x = 2; //b修改的同时,a也会被修改 }
浅拷贝(调用默认构造函数):单纯地复制成员变量的值。(默认复制构造函数就是如此)
class String { private: char* m_Buffer; unsigned int m_Size; public: String(const char* string) { m_Size = strlen(string); m_Buffer = new char[m_Size + 1]; memcpy(m_Buffer, string, m_Size + 1); m_Buffer[m_Size] = 0; } ~String() { delete[] m_Buffer; } //重载[]操作符用于随机访问m_Buffer的值 char& operator[](unsigned int index) { return m_Buffer[index]; } //重载<<操作符用于打印 friend std::ostream& operator<<(std::ostream& stream, const String& string); }; std::ostream& operator<<(std::ostream& stream, const String& string) { stream << string.m_Buffer; return stream; } int main() { String string = "Cherno"; String second = string; //浅拷贝,只是单纯地复制成员变量的值 //second是string的一个副本 //所以它们的成员char* m_Buffer指向同一块内存 second[2] = 'a'; //结果两个都改了 std::cout << string << std::endl; }//最后程序会崩溃,因为已经被释放的内存再次被释放
深拷贝(调用自定义的复制构造函数):
-
拷贝得到的副本拥有全新的,不同于原有的自定义的成员变量。
-
需要自定义复制构造函数。
复制构造函数:
-
也是属于构造函数的一类
-
只不过参数是const 类& other
-
如果设置成String(const String& other) = delete,则表示该实例不能被复制
class String { private: char* m_Buffer; unsigned int m_Size; public: String(const char* string) { m_Size = strlen(string); m_Buffer = new char[m_Size + 1]; memcpy(m_Buffer, string, m_Size + 1); m_Buffer[m_Size] = 0; } //复制构造函数 String(const String& other) : m_Size(other.m_Size) { m_Buffer = new char[m_Size + 1]; memcpy(m_Buffer, other.m_Buffer, m_Size + 1); } ~String() { delete[] m_Buffer; } char& operator[](unsigned int index) { return m_Buffer[index]; } friend std::ostream& operator<<(std::ostream& stream, const String& string); }; std::ostream& operator<<(std::ostream& stream, const String& string) { stream << string.m_Buffer; return stream; } int main() { String string = "Cherno"; String second = string; //使用复制构造函数 //string和second实例的m_Buffer成员有不同的地址 second[2] = 'a'; //只会改第二个,第一个不受影响 std::cout << string << std::endl; }//最后程序不会崩溃
记住:我们总是用常量引用(const 类&)来调用函数,否则会引起不必要的拷贝(深或浅)。
C++动态数组(std::vector)
STL:Standard Template Library。本质上是一个充满容器的库。容器提供模板,其中的数据类型是由用户指定的。
动态数组的底层机制:当分配的大小超过capacity个时,它会在内存中创建一个更大的数组,将所有内容复制到那里,然后删除旧数组。capacity的大小默认是1,然后每当添加一个元素时,capacity加一。
必须加头文件#include <vector>
vector<类型> a;其中“类型”可以时基本数据类型。(与java不同)
使用对象指针vector<TreeNode*>,还是对象本身vector<TreeNode>视不同情况而定。一般情况用对象本身,各个成员变量之间连续存放。如果数组经常需要调整大小,则用指针,以减少复制的性能损失。
vector<Vertex> a(3)
表示分配3个内存并构造3个对象,并且需要构造函数
vector.reserve(3)
表示只分配3个内存,而不构造对象,并且不需要构造函数
vector优化:
struct Vertex { float x, y, z; Vertex(float x, float y, float z) : x(x), y(y), z(z) { } }; 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)); //复制三次(主函数+扩容) } //这段代码会复制六次
优化后:
struct Vertex { float x, y, z; Vertex(float x, float y, float z) : x(x), y(y), z(z) { } }; int main() { std::vector<Vertex> vertices; //告诉总容量 //注意与resize不同,不会构造对象 vertices.reserve(3); //直接在向量中添加对象,而不是在主函数构造对象后再复制副本传入 vertices.emplace_back(1, 2, 3); vertices.emplace_back(4, 5, 6); vertices.emplace_back(7, 8, 9); } //这段代码复制0次
C++静态/动态链接库
-
静态链接允许进行更多的优化,因为编译器和链接器可以看到更加完整的程序
-
动态链接在运行时才发生,当启动可执行文件时,动态链接库才会被加载。所以它并不是可执行文件的一部分
C++实现(不同类型)多返回值(tuple or pair)
tuple: 可以包含任意多个不同类型的变量
include <utility> include <functional> //定义tuple std::tuple<std::string, std::string, int> //获取tuple中的值 std::string vs = std::get<0>(sources) //构造tuple(不一定对) std::make_pair(vs, fs, 1);
pair:只可以包含两个不同类型的变量
底层其实就是结构体,结构体还更好,因为可以按照变量名索引。
C++模板
什么是模板:
让编译器根据一组规则为你编写代码。
可以避免多个不同类型的函数重载。
typename:类型形参,与class等价(但是不推荐用class)
#include <iostream> #include <string> template<typename T> //下面代码只有在调用时才会生成具体的 void Print(T value) { std::cout << value << std::endl; } // int main() { //以下代码可以正常运行,编译时根据传入参数类型生成对应的函数,再调用 Print(5); Print("Hello"); Print(5.5f); Print<int>(5); }
模板在实际调用之前其实并不存在。只有在调用的时候才会生成实际的代码并传给编译器。模板用于指导编译器生成真正的函数代码。
STL的实质:用模板创建一个类。
int N:非模板形参,在模板内部是一个常量值,传入时必须是一个常量表达式
在创建数组时很有用。
template<int N> class Array { private: //由于模板是在编译时创建的 //而数组需要在编译时确定大小,就很完美 int m_Array[N]; public: int GetSize() const { return N; } }; int main() { Array<5> array; std::cout << array.GetSize() << std::endl; }
如果把数组的类型也改为T:
template<typename T, int N> class Array { private: T m_Array[N]; public: int GetSize() const { return N; } }; int main() { Array<int, 5> array; std::cout << array.GetSize() << std::endl; }
C++宏定义
#include <iostream> #include <string> #define WAIT std::cin.get() int main() { WAIT; }
#include <iostream> #include <string> #define LOG(x) std::cout << x << std::endl int main() { LOG("Hello"); std::cin.get(); }
//如果在debug模式下 #ifdef PR_DEBUG #define LOG(x) std::cout << x << std::endl; #else #define LOG(x) #endif
多行续行符\
:
#define MAIN int main() \ {\ std::cin.get();\ } MAIN
C++auto关键字
使用auto关键字时,会自动推断变量的类型。(类似var)
int a = 5; auto b = a;//b类型是int auto x = 5;//x类型是int auto y = "Cherno"; //y的类型是指针
auto使用场景:
std::vector<std::string> strings; strings.push_back("Apple"); strings.push_back("Orange"); for(std::vector<std::string>::iterator it = strings.begin(); it != strings.end(); it++) { std::cout << *it << std::endl; } //可以改造成 for(auto it = strings.begin(); it != strings.end(); it++) { std::cout << *it << std::endl; }
变量类型太长。
class Device {}; class DeviceManager { private: std::unordered_map<std::string, std::vector<Device*>> m_Devices; public: const std::unordered_map<std::string, std::vector<Device*>>& GetDevices() const { return m_Device; } } int main() { DeviceManager dm; const auto& devices = dm.GetDevices(); }
C++函数指针
函数指针的作用就是将一个函数赋值给一个变量。
void HelloWorld() { std::cout << "Hello World!" << std::endl; } int main() { auto function = HelloWorld; //会打印Hello World! function(); function(); }
function的实际类型是void(*function)();
下面代码也可正常运行
void(*cherno)() = HelloWorld; cherno(); //或者 typedef void(*HelloWorldFunction)(); HelloWorldFunction function = HelloWorld; function();
若函数参数是int:
void HelloWorld(int a) { std::cout << "Hello World!" << a << std::endl; } int main() { typedef void(*HelloWorldFunction)(int); HelloWorldFunction function = HelloWorld; function(8); }
使用场景:当需要传送某个函数是可以用到。例如遍历一个vector,并对其中的每一个元素作用于函数上。
void PrintValue(int value) { std::cout << "Value: " << value << std::endl; } void ForEach(const std::vector<int>& values, void(*func)(int)) { for(int value : values) func(value); } int main() { std::vector<int> values = { 1, 5, 4, 2, 3}; ForEach(values, PrintValues); }
也可用lamda表达式:
void ForEach(const std::vector<int>& values, void(*func)(int)) { for(int value : values) func(value); } int main() { std::vector<int> values = { 1, 5, 4, 2, 3}; ForEach(values, [](int value) { std::cout << "Value: " << value << std::endl; }); }
C++lamda表达式
lamda表达式可以看做是一个匿名函数或者一次性函数。
什么时候使用lamda:每当有函数指针的地方,就可以在C++中使用lamda。
[]用于捕获lamda表达式外的变量。
-
[=]表示值传递所有变量
-
[&]表示引用传递所有变量
-
[&a]表示传递a的引用(只针对单独的a)
-
使用mutable可以修改 值传递的变量
void ForEach(const std::vector<int>& values, void(*func)(int)) { for(int value : values) func(value); } int main() { std::vector<int> values = { 1, 5, 4, 2, 3}; int a = 5; //值传递a ForEach(values, [=](int value) { std::cout << "Value: " << value << a << std::endl; }); //寻找第一个值大于3的迭代器 std::vector<int> values = {1, 5, 4, 2, 3}; auto it = std::find_if(values.begin(), values.end(), [](int value){ return value > 3; }) std::cout << *it << std::endl; }
C++的namespace
目的:避免不同库中的类似函数的==命名冲突==。
注意:千万不要在头文件中使用using namespace std,谁都不知道他会被包含在哪里
namespace apple { void print(const char* text) { std::cout << text << std::endl; } } namespace orange { void print(const char* text) { std::string temp = text; std::reverse(temp.begin(), temp.end()); std::cout << temp << std::endl; } } using namespace apple; using namespace orange; int main(){ //会冲突报错 print("Hello"); }
C++迭代器(iterators)
-
iterator:普通迭代器,从前向后遍历
-
reverse_iterator:反向迭代器,从后向前遍历
-
const_iterator:只读,遍历过程不能改变原有数值
-
values.end():最后一个元素后面的元素,实际不存在
#include<iostream> #include<vector> int main() { std::vector<int> values = {1, 2, 3, 4, 5}; //第一种遍历方式 for(int i = 0; i < values.size(); i++) { std::cout << values[i] << std::endl; } //第二种遍历方式 for(int value : values) { std::cout << value << std::endl; } //第三种遍历方式 for(std::vector<int>::iterator it = values.begin(); it != values.end(); it++) { std::cout << *it << std::endl; } }
迭代器对不能随机存取的数据结构很有用,比如树形结构,无序集合,无序映射等。
#include<iostream> #include<vector> #include<unordered_map> int main() { using ScoreMap = std::unordered_map<std::string, int>; ScoreMap map; std::unordered_map<std::string, int> map; map["Cherno"] = 5; map["C++"] = 2; for(ScoreMap::const_iterator it = map.begin(); it != map.end(); it++) { atuo& key = it->first; auto& value = it->second; std::cout << key << "=" << value << std::endl; } //或 for(auto kv : map) { //kv是std::pair类型 auto& key = kv.first; auto& value = kv.second; std::cout << key << "=" << value << std::endl; } }
迭代器类中只有一个私有成员,那就是==指向对象的指针==。其他方法都是重载了指针的运算符。
C++ Threads
join:同步操作,等待线程执行完毕,主线程再继续向下执行
#include <iostream> #include <thread> static bool s_Finished = false; void DoWork() { using namespace std::literals::chrono_literals; //可以用1s std::cout << "Started thread id = " << std::this_thread::get_id() << std::endl; //打印当前thread id while (!s_Finished) { std::cout << "Working..\n"; std::this_thread::sleep_for(1s); } } int main() { std::thread worker(DoWork); std::cin.get(); s_Finished = true; worker.join(); //等待worker thread完成 std::cout << "Finished." << std::endl; std::cout << "Started thread id = " << std::this_thread::get_id() << std::endl; std::cin.get(); }
C++ Timing
关于时间的API:<chrono>
#include <iostream> #include <chrono> #include <thread> struct 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; float ms = duration.count() * 1000.0f; std::cout << "Timer took " << ms << "ms" << std::endl; } }; void Function() { Timer timer; for(int i = 0; i < 100; i++) std::cout << "Hello" << std::endl; } int main() { Function(); std::cin.get(); }
C++ Sort
使用头文件:<algorithm>
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> values = {3, 5, 1, 4, 2}; //升序排列,并使1作为最后一个元素 std::sort(values.begin(), values.end(), [](int a, int b) { if (a == 1) return false; if (b == 1) return true; return a < b; }); for(int value : values) std::cout << value << std::endl; std::cin.get(); }