VisualStudio的最佳设置
新建项目
点击显示所有文件,右键新建src文件夹用来放源码
修改解决方案的属性
输出目录改为: $(SolutionDir)bin$(Platform)$(Configuration)
中间目录改为: $(SolutionDir)intermediates\bin$(Platform)$(Configuration)
通过两个类的定义来快速学习与回顾C++的知识点
包含的知识点有:
防卫式声明
成员初始化列表
this关键字
符号重载
友元函数
内联函数
拷贝赋值与拷贝构造
complex.h(没有传指针的类)
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 #ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ class complex ; complex& __doapl (complex* ths, const complex& r); complex& __doami (complex* ths, const complex& r); complex& __doaml (complex* ths, const complex& r); class complex { public : complex (double r = 0 , double i = 0 ): re (r), im (i) { } complex& operator += (const complex&); complex& operator -= (const complex&); complex& operator *= (const complex&); complex& operator /= (const complex&); double real () const { return re; } double imag () const { return im; } private : double re, im; friend complex& __doapl (complex *, const complex&); friend complex& __doami (complex *, const complex&); friend complex& __doaml (complex *, const complex&); }; inline complex&__doapl (complex* ths, const complex& r) { ths->re += r.re; ths->im += r.im; return *ths; } inline complex&complex::operator += (const complex& r) { return __doapl (this , r); } inline complex&__doami (complex* ths, const complex& r) { ths->re -= r.re; ths->im -= r.im; return *ths; } inline complex&complex::operator -= (const complex& r) { return __doami (this , r); } inline complex&__doaml (complex* ths, const complex& r) { double f = ths->re * r.re - ths->im * r.im; ths->im = ths->re * r.im + ths->im * r.re; ths->re = f; return *ths; } inline complex&complex::operator *= (const complex& r) { return __doaml (this , r); } inline double imag (const complex& x) { return x.imag (); } inline double real (const complex& x) { return x.real (); } inline complexoperator + (const complex& x, const complex& y){ return complex (real (x) + real (y), imag (x) + imag (y)); } inline complexoperator + (const complex& x, double y){ return complex (real (x) + y, imag (x)); } inline complexoperator + (double x, const complex& y){ return complex (x + real (y), imag (y)); } #include <cmath> inline complexpolar (double r, double t) { return complex (r * cos (t), r * sin (t)); } inline complexconj (const complex& x) { return complex (real (x), -imag (x)); } inline double norm (const complex& x) { return real (x) * real (x) + imag (x) * imag (x); } #endif
string.h(传指针的类)
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #ifndef __MYSTRING__ #define __MYSTRING__ class String { public : String (const char * cstr=0 ); String (const String& str); String& operator =(const String& str); ~String (); char * get_c_str () const { return m_data; } private : char * m_data; }; #include <cstring> inline String::String (const char * cstr) { if (cstr) { m_data = new char [strlen (cstr)+1 ]; strcpy (m_data, cstr); } else { m_data = new char [1 ]; *m_data = '\0' ; } } inline String::~String () { delete [] m_data; } inline String& String::operator =(const String& str) { if (this == &str) return *this ; delete [] m_data; m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); return *this ; } inline String::String (const String& str) { m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); } #include <iostream> using namespace std;ostream& operator <<(ostream& os, const String& str) { os << str.get_c_str (); return os; } #endif
防卫式声明
在头文件中添加防卫式声明后可以防止程序多次包含这个头文件。
这个防卫式声明的作用跟#program once作用差不多,目前个人喜欢用#program once。
模板
类模板
假如我们现在定义一个名字叫做complex的类,然后它是有实部和虚部的,因此它就需要定义两个变量来充当它的实部和虚部。但是这个实部和虚部的类型可能是int也可能是float等等,这是不一定的。
像这种需要定义变量意义是一样的,但是类型不一定的时候,就可以借助模板了。
向下方这些代码一样,我们可以以T来代替类型然后当使用时就可以通过"<>"来指定类型了。
举例一: complex<double> c1(2.5,1.5);与 complex<int> c2(2,6)
1 2 3 4 5 6 7 8 9 10 template <typename T> class complex { public : complex (T r=0 , T i=0 ) : re (r), im (i) { } private : T re, im; };
举例二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <string> template <typename T,int N>class Array { private : T m_array[N]; public : int GetSize () { return N; } }; int main () { Array<std::string, 5 > array1; Array<int , 50 > array2; std::cout << array1. GetSize () << std::endl; std::cout << array2. GetSize () << std::endl; }
函数模板
函数模板跟类模板的区别是函数模板在使用时不强制 需要通过<>来指定类型是什么,编译器会自动的根据调用函数时传进来的参数类型来进行使用.
只有当调用这个函数时,这个函数才会被实际创建.
例如这里定义一个函数min,调用时直接 c = min(a,b)即可,编译器会根据a和b的类型自动替代模板T
1 2 3 4 5 6 template <typename T>inline const T& min (const T& a, const T& b) { return b < a ? b : a; }
inline(内联)函数
在class的本体里面进行定义的函数会自动成为inline函数的候选人 ,之所以是候选人是因为编译器会根据函数的内容的复杂程度来将函数变成inline函数,如果太复杂就无法变成inline函数。
在类外面进行定义的函数可以通过添加inline关键字来让编辑器尽量 将函数变成inline函数
inline函数的好处是比较快。
构造函数
构造函数的作用是当类的对象被创建出来时会自动的进行调用
构造函数的名字需要跟类的名字一样。
构造函数的特殊语法:initialization list(初值列,初始列)
构造函数可以后面可以加:来写赋值的操作,例如下图中将形参r和形参i赋予类的自定义变量re和im
重载
重载的意思是: 允许创建多个名称相同的函数,但是函数所需要的参数需要不同,这样就可以在调用的时候根据传入的参数的个数与类型的不同来调用不同的对应函数
操作符重载
操作符是+,-,*,+=,-=,==,!=这些符号.
操作符也可以被重载,只需要在定义与声明的操作符前面加上operator即可
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;struct Vector2 { float x; float 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; } }; ostream& operator <<(ostream& stream, const Vector2& vector) { stream << "(" << vector.x << "," << vector.y << ")" ; return stream; } int main () { Vector2 a (1.0f , 2.0f ) ; Vector2 b (3.0f , 4.0f ) ; cout << "a==" << a << endl; cout << "b==" << b << endl; cout << "a+b==" << a + b << endl; cout << "a*b==" << a * b << endl; cout << "a==b:" << (a == b) << endl; cin.get (); }
const
const可以用来针对函数也可以用来针对变量.
const用在类的成员函数上表示这个函数不会修改对象(*this)内部的数据成员.
const用在对象上表示这个对象不会被进行修改.
基础类型不需要添加const,因为他们是等效的
例如const int 与 int是等效的.
基础类型有int,char,bool,float,void这些.
const针对指针的不同写法的区别:
const放到号前面代表,指针指向的内容是不能改变的
const放到 号后面代表,指针的指向是不能改变的(不能够给ptr一个其他的地址),但是指针指向的内容(ptr)是可以改变的
const int ptr = int const* ptr != int* const ptr
如果一个函数不针对参数进行修改一定要记得加上const .
原因是因为如果一个对象被定义为const,那么这个对象就不能够使用没有被const定义的函数.
const用在函数上: 函数类型 函数名() const {}
const用在变量上: const 类名 对象名();
mutable
mutable有两种不同的用途,其中之一是与const一起使用,另一种是用在lambda表达式中,或同时覆盖这两种情况.
mutable绝大部分情况下是用在类中使用的,不会使用在lambda上面
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 #include <iostream> class Enitity { private : std::string m_Name = "TestName" ; mutable int m_Debug_num = 0 ; public : const std::string& Get_Name () const { m_Debug_num++; std::cout << m_Debug_num << std::endl; return m_Name; } }; int main () { const Enitity e; for (int i = 0 ; i < 5 ; i++) { std::cout << e.Get_Name () << std::endl; } std::cin.get (); }
friend友元函数
类中被定义为private的成员只能被类的函数所访问,但是通过友元函数打开封装的大门.
友元函数可以在类中进行声明,声明以后这个被声明为友元函数的函数就可以访问类的private里的参数了.
友元函数只需要在类中声明即可,友元函数的定义不需要在类中定义.
相同class的各个object互为友元 ,也就是说相同类的obj1和obj2可以互相访问对方的私有成员.
static 静态
可以把函数和变量设定为静态的,静态的函数和变量是属于类的,无论创建多少个对象,这个设定为静态的函数和变量都只有一份.
静态变量
静态变量只有一份,属于类,不同的对象都是使用的这一份.如果不是静态变量,那么创建多少个对象就会有多少个对应的变量
静态函数
静态函数不像其他的类的函数一样有this指针,静态函数是没有this指针的,因此静态函数的定义中是不能够访问类中的其他变量的,静态函数只能够访问静态变量.
静态函数可以通过类名来调用
继承与多态与虚函数表
继承
继承代表了 is a 关系
子类的范围大小是要比父类要大的
构造是由内而外的,先执行父类的构造函数,再执行子类的构造函数.
析构是由外而内的,先执行子类的析构函数,再执行父类的析构函数.
父类的析构函数必须是virtual ,否则会出现undefined behavior
用下图来表示, Base这个类是父类,Derived这个类是子类.
多态与虚函数表与虚析构函数
多态是面向对象编程中的一种特性,它允许同一个接口调用的不同实现。这种特性使得一个基类指针或引用可以指向派生类的对象,并且可以调用派生类的重载方法,而不需要知道具体的派生类。这种机制主要有两种类型:
1.编译时多态(静态多态性):通过函数重载和模板实现。
2.运行时多态(动态多态性):通过虚函数(virtual functions)和继承实现。
在C++中,运行时多态通过虚函数来实现。当基类的一个方法被声明为虚函数,派生类可以重写(override)这个方法。当通过基类指针或引用调用这个方法时,会执行指向对象的实际类型的版本。
函数表是由编译器实现的一种机制,用来支持C++的运行时多态性。它是一个指针表,其中包含类的虚函数的地址。每个拥有虚函数的类都有一个虚函数表。具体来说:
虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,表中包含该类的所有虚函数的地址。
虚指针(vptr):每个对象包含一个指向该类的虚函数表的指针。当对象创建时,这个指针被初始化指向相应的虚函数表。
当通过基类指针或引用调用虚函数时,程序会通过对象的虚指针找到对应的虚函数表,然后从表中找到实际要调用的函数地址,并进行调用。
代码举例
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 28 29 30 31 32 33 34 35 36 37 38 #include <iostream> class Parent { public : Parent () { std::cout << "进行父类构造函数" << std::endl; } virtual ~Parent () { std::cout << "进行父类析构函数" << std::endl; } virtual void Print () { std::cout << "执行父类的Print函数" << std::endl; } }; class Child : public Parent{ private : int * m_Array; public : void Print () override { std::cout << "执行子类的Print函数" << std::endl; } Child () { m_Array = new int [5 ]; std::cout << "创建内存并进行子类构造函数" << std::endl; } ~Child () { delete [] m_Array; std::cout << "删除创建的内存并进行子类析构函数" << std::endl; } }; int main () { Parent* parent = new Parent (); parent->Print (); delete parent; std::cout << "--------------------" << std::endl; Child* child = new Child (); child->Print (); delete child; std::cout << "--------------------" << std::endl; Parent* poly = new Child (); poly->Print (); delete poly; std::cin.get (); }
复合
复合代表了 has a 关系
一个类中可以包含其他类,然后可以使用包含的其他类的东西,这种情况叫做复合
包含其他类的这个大类可以称之为container(容器),其所包含的类称之为component(组件)
构造是由内而外的,先执行component的默认 构造函数,再执行container的构造函数
析构是由外而内的,先执行container的析构函数,再执行component的析构函数
用下图来表示,菱形是实心的黑色
意思是Container这个类包含了Component这个类
委托(又称代理) delegation
委托跟复合的区别是,它包含的是其他类的指针,
用下图来表示,菱形是空心的 ,空心可以这样解释:一个拥有其他类,但是因为是指针,不是实实在在的,所以是空心.
virtual&pure virtual 虚函数和纯虚函数
虚函数的概念:父类的函数声明前加上virtual,即可将父类的这个函数设定为虚函数,设定为虚函数以后,这个函数就能够被父类的子类重新定义,并且父类本身要有对于这个函数的定义.
纯虚函数的概念: 父类的函数声明前加上virtual,然后后面加上"=0",即可将父类的这个函数设定为纯虚函数,纯虚函数必须被父类的子类重新定义,并且父类本身一般没有进行这个函数的定义.
转换函数
如图中所示红框黄底的代码就是转换函数,有了这个转换函数后,就可以在进行运算时编译器能够自动地寻找合适的类型来进行运算.
转换函数是没有返回类型的
三元操作符
三元操作符可以实现一行代码就能实现if else功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> int main () { int a = 5 ; int b = 4 ; std::string c = a > b ? "a大于b" : "a小于b" ; std::cout << c << std::endl; std::cin.get (); }
new和delete
new与delete
new是用于在堆上动态分配内存的关键字,它会返回一个指向分配的内存的指针
delete用于释放由new分配的动态内存
当使用new时要配合使用delete,当使用new[]时要配合使用delete[]
语法1:
type* ptr = new type
delete ptr
语法2 分配指定大小:
type* arrayPtr = new type[size]
delete[] arrayPtr
堆与栈
栈与堆,它们是我们可以存储数据的地方,它们的工作原理非常不同,但本质上它们做的事情是一样的.
栈:
在函数调用时,函数的局部变量和参数会被存储在栈中.当函数执行完成后,这些数据会被自动释放.
栈的内容分配速度更快,但是大小有限.
栈是由编译器自动管理的,在栈上分配内存就是一条cpu指令
栈分配的内存地址是连续的.
堆:
堆是一种动态分配的内存池,是由程序员负责管理的.
堆分配的内存地址不是连续的.
在堆上分配的内存需要手动释放,否则会导致内存泄漏.
智能指针
智能指针的概念
已知,在堆上分配内存需要手动使用delete来删除内存.而智能指针的作用是为了实现分配内存和释放内存这一过程自动化的一种方式.
智能指针本质上是一个原始指针的包装.
当创建一个智能指针,它会调用new来分配内存,然后基于智能指针的使用方式,这些内存会在某一时刻被自动释放.
智能指针的类型
智能指针有很多种类型.分别是unique_ptr,shared_ptr,weak_ptr.
智能指针中最简单的是unique_ptr,unique_ptr是作用域指针,当代码执行超过作用域时,它会被销毁并调用delete来释放内存.
unique_ptr这种类型的智能指针不能够进行拷贝,因为如果两个智能指针同时指向一个内存,当释放内存后,其中一个智能指针会指向一个已经被删除的内存.
shared_ptr多了一个控制块来存储引用计数,因此shared_ptr能够进行拷贝,可以多个shared_ptr指向同一个对象,当引用计数为0时,内存将进行释放(调用析构函数).
weak_ptr负责与shared_ptr一起使用,
为了便于理解unique_ptr,所做的举例:
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 28 29 30 31 #include <iostream> class Entity {public : Entity () { std::cout << "创建了一个Entity对象" << std::endl; } ~Entity () { std::cout << "释放了Entity对象的空间" << std::endl; } }; class ScopedPtr {private : Entity* m_ptr; public : ScopedPtr (Entity* ptr) : m_ptr (ptr) {}; ~ScopedPtr () { delete m_ptr; } }; int main () { { ScopedPtr e1 (new Entity()) ; ScopedPtr e2 = new Entity (); } std::cin.get (); }
实际上使用智能指针时的举例以及不同智能指针种类的对比:
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 28 29 #include <iostream> #include <memory> class Entity {public : Entity () { std::cout << "创建了一个Entity对象" << std::endl; } ~Entity () { std::cout << "释放了Entity对象的空间" << std::endl; } }; int main () { { std::weak_ptr<Entity> weakPtr; { std::unique_ptr<Entity> uniquePtr = std::make_unique <Entity>(); std::shared_ptr<Entity> sharedPtr = std::make_shared <Entity>(); weakPtr = sharedPtr; } if (weakPtr.expired ()) { std::cout << "weakPtr 指向的对象已被销毁" << std::endl; } } std::cin.get (); }
动态数组
动态数组std::vector是标准库的一部分,要想使用它,需要#include <vector>
std::vector可以根据需要自动调整其大小。这意味着你可以在运行时添加或删除元素,而不需要手动管理内存。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <iostream> #include <string> #include <vector> struct Vertex { float x, y, z; Vertex (float x, float y, float z) :x (x), y (y), z (z) {}; }; std::ostream& operator <<(std::ostream& stream, const Vertex& vertex) { return stream << vertex.x << "," << vertex.y << "," << vertex.z << std::endl; } void Function (const std::vector<Vertex>& vertices) { } int main () { std::vector<Vertex> vertices; vertices.reserve (3 ); vertices.push_back (Vertex ( 1 ,2 ,3 )); vertices.push_back (Vertex ( 2 ,3 ,4 )); vertices.push_back (Vertex ( 3 ,4 ,5 )); vertices.emplace_back (1 , 2 , 3 ); vertices.emplace_back (2 , 3 , 4 ); vertices.emplace_back (3 , 4 , 5 ); for (int i = 0 ; i < vertices.size (); i++) { std::cout << vertices[i]; } vertices.erase (vertices.begin () + 1 ); for (Vertex& i : vertices) { std::cout << i; } std::cin.get (); }
C++中使用库
预编译二进制文件(pre-compiled binaries)是已经编译好的可执行文件或库,它们可以直接在目标系统上运行或使用,而无需进行源码编译。使用预编译二进制文件的主要优点是节省了用户自己编译源码所需的时间和资源。
#include <>:用于标准库和第三方库头文件
#include “”:用于项目内部头文件
静态链接是在编译时将库的代码复制到生成的可执行文件中,从而生成一个独立的、包含所有代码的可执行文件。
动态链接是在程序运行时将库加载到内存中。可执行文件只包含对库的引用,而不包含库的实际代码。
静态链接
以glfw静态链接举例:
前往glfw网站下载预编译二进制文件https://www.glfw.org/download.html
将其中的include文件夹和lib文件夹放到项目中.
类似这种,在HelloWorld项目文件夹中新建一个Dependencies文件夹,里面再创建一个glfw文件夹,然后将glfw的include文件夹和lib文件夹放到这里面.
修改项目配置中附加包含目录
修改链接器对应的附加库目录
添加glfw3.lib;到附加依赖项
使用以下代码编译测试
1 2 3 4 5 6 7 8 9 10 #include <iostream> #include <GLFW/glfw3.h> int main () { int a = glfwInit (); std::cout << a << std::endl; std::cin.get (); }
动态链接
在glfw静态链接的基础上举例:
将链接器对应的附加库目录的glfw3.lib修改为flfw3dll.lib
将glfw3.dll文件放到可执行文件exe的旁边,不然会因为动态链接找不到对应的dll文件.
创建与使用库
在解决方案处右键新建项目
在库项目处右键属性,修改配置类型为静态库
在主项目处右键属性,修改附加包含目录,使其可以include库中的头文件
通过引用的方式实现自动的静态链接
宏,macro
C++中的宏(Macros)是一种预处理指令,使用#define关键字来定义。在编译之前,预处理器会扫描代码中的宏定义,并用宏的具体内容替换掉代码中的宏调用。这一过程在编译器实际编译代码之前进行。宏可以用于定义常量、简化复杂的表达式、创建内联函数等。
编译器中定义宏
无参数宏与有参数宏与多行宏
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 #include <iostream> #include <string> #if MY_DEBUG == 1 #define LOG(x) std::cout << x << std::endl #define ADD int add(int x,int y)\ {\ return x+y;\ } #elif defined(MY_RELEASE) #define LOG(x) #endif ADD int main () { LOG ("Hello" ); LOG (add (1 , 2 )); std::cin.get (); }
auto关键字
auto 关键字是C++11引入的一种类型推导机制,用于让编译器根据初始化表达式自动推导变量的类型。
auto适合用在迭代器上,用于避免繁琐的类型声明.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <unordered_map> int main () { std::unordered_map<std::string, int > myUnorderedMap1 = { {"apple" , 1 }, {"banana" , 2 }, {"cherry" , 3 } }; for (std::unordered_map<std::string, int >::iterator it = myUnorderedMap1. begin (); it != myUnorderedMap1. end (); ++it) { std::cout << it->first << ": " << it->second << std::endl; } std::unordered_map<std::string, int > myUnorderedMap2 = { {"auto_apple" , 1 }, {"auto_banana" , 2 }, {"auto_cherry" , 3 } }; for (auto it = myUnorderedMap2. begin (); it != myUnorderedMap2. end (); ++it) { std::cout << it->first << ": " << it->second << std::endl; } std::cin.get (); return 0 ; }
Array
std::array 是 C++ 标准库中的一个容器类,定义在 <array> 头文件中。它提供了与内置数组类似的功能,但具有更多的标准库容器的特性和接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <array> int main () { std::array<int , 5> arr = { 1 , 2 , 3 , 4 , 5 }; for (const auto & elem : arr) { std::cout << elem << " " ; } std::cout << std::endl; for (size_t i = 0 ; i < arr.size (); ++i) { std::cout << arr[i] << " " ; } std::cout << std::endl; return 0 ; }
函数指针与lambda表达式
函数指针是一个指向函数的指针,它允许你通过指针调用函数,而不仅仅是通过函数名直接调用。函数指针在C和C++中非常有用,因为它们使得编写更灵活和模块化的代码成为可能。例如,可以将函数指针作为参数传递给另一个函数,或者在运行时决定调用哪个函数。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> #include <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 , 3 , 4 , 2 }; ForEach (values, [](int value) { std::cout << "Value: " << value << std::endl; }); std::cin.get (); return 0 ; }
线程
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 28 29 30 31 32 #include <iostream> #include <thread> static bool is_thread_working = true ;void ThreadFunc (const std::string name) { using namespace std::literals::chrono_literals; std::cout << "这个子线程的id是:" << std::this_thread::get_id () << std::endl; while (is_thread_working) { std::cout << "Thread name : " << name << "Working... " << std::endl; std::this_thread::sleep_for (1 s); } std::cout << "不再循环输出Working..." << std::endl; } int main () { std::cout << "这个主线程的id是:" << std::this_thread::get_id () << std::endl; void (*ThreadFuncPtr)(std::string); ThreadFuncPtr = ThreadFunc; std::cout << "开始执行子线程id" << std::endl; std::thread worker (ThreadFunc,"Test" ) ; std::cin.get (); is_thread_working = false ; worker.join (); std::cout << "线程结束了" << std::endl; std::cin.get (); return 0 ; }
定义计时器
如下代码所示,通过chrono来定义Timer结构体,放到自定义函数中配合构造函数和析构函数来计算函数的执行时间.
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 28 29 30 31 32 33 34 35 36 37 38 39 #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 (); duration = std::chrono::duration<float >::zero (); } ~Timer () { end = std::chrono::high_resolution_clock::now (); duration = end - start; float ms = duration.count () * 1000.0f ; std::cout << "执行时间为:" << ms << "毫秒" << std::endl; } }; void TestFunc () { Timer timer; for (int i = 1 ; i < 100 ; i++) { std::cout << "Hello World!" << std::endl; } } int main () { TestFunc (); std::cin.get (); }
排序
下面介绍了三种使用std::sort对std::vector进行排序的过程
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 28 #include <iostream> #include <vector> #include <algorithm> #include <functional> int main () { std::vector<int > values = {3 ,5 ,1 ,4 ,2 }; 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 (); }
隐式转换和explicit
隐式转换是在没有显式指示的情况下,由编译器自动进行的类型转换。
explicit关键字用于防止构造函数或转换运算符进行隐式转换,仅允许显式转换。这在防止意外的类型转换错误方面非常有用。
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 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> using namespace std;class Entity { private : string m_name; int m_age; public : explicit Entity (const string& name) : m_name(name), m_age(-1 ) { }; explicit Entity (int age) : m_name("Unknown" ), m_age(age) { }; const void PrintEntity () const { cout << m_name << endl; cout << m_age << endl; } }; void printEntity (const Entity& e) { e.PrintEntity (); } int main () { Entity e1 = Entity ("TestName" ); Entity e2 = Entity (23 ); printEntity (e1); printEntity (e2); cin.get (); }
类型双关
类型双关(type punning)是指在编程中通过不同的类型来访问同一块内存。换句话说,就是使用一种类型的变量或指针来访问另一种类型的数据。这通常通过类型转换和指针运算来实现。
在这个例子中,我们将一个 float 类型的变量 x 的内存解释为 int 类型。这就是类型双关,因为我们用 int 类型的指针来访问原本是 float 类型的数据。
1 2 3 4 5 6 7 8 9 10 #include <iostream> int main () { float x = 3.14 ; int y = *(int *)&x; std::cout << "Float value: " << x << std::endl; std::cout << "Interpreted as int: " << y << std::endl; return 0 ; }
类型转换
C++的转换操作符有四种,分别是static_cast,reinterpret_cast,dynamic_cast,const_cast
C语言风格的类型转换: float a = 3.5f; int b = (int)a;
使用C++的转换操作符相比使用C语言风格的类型转换的好处是:通过转换操作符可以使用搜索功能快速定位到进行了类型转换的代码区域,同时它也帮助减少了强制转换时发生的意外错误。
static_cast: 常用的转换操作符,它会做一些其他的编译时检查来确定转换是否真的可能。
reinterpret_cast: 把内存重新解释成别的东西,类似于类型双关。
const_cast: 用于移除或添加变量的const
dynamic_cast: 用于基类和派生类之间的转换,转换失败时返回nullptr
使用方法:
float f = 3.14;int i = static_cast(f);
其他类型转换同static_cast.
union(联合体)
在C++中,union(联合体)是一种特殊的数据结构,它允许你在同一个内存位置存储不同类型的变量。也就是说,union中的所有成员共享同一块内存,因此一个union实例在任何时候只能存储其中一个成员的数据。
如果想要给同一个类型取两个不同的名字时,它非常有用.
举例: 这里是把Vector4可以看成由两个Vector2组成的类型.
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 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> struct Vector2 { float x, y; }; struct Vector4 { union { struct { float x, y, z, w; }; struct { Vector2 a, b; }; }; }; void PrintVector2 (const Vector2& vector) { std::cout << vector.x << "," << vector.y << std::endl; } int main () { Vector4 vector = { 1.0f ,2.0f ,3.0f ,4.0f }; PrintVector2 (vector.a); PrintVector2 (vector.b); std::cout << "--------------------------" << std::endl; vector.z = 500.0f ; PrintVector2 (vector.a); PrintVector2 (vector.b); std::cin.get (); }
预编译头文件
C++ 的预编译头文件(Precompiled Headers,简称 PCH)是一种优化编译过程的技术。通过将项目中常用且不经常变动的头文件预先编译成一个二进制文件,下次编译时可以直接使用这个二进制文件,而不需要重新编译这些头文件,从而显著减少编译时间。
如何使用预编译头文件:
应用到整个项目文件中:
应用到cpp文件中:
在要使用预编译头文件的cpp文件的属性设置中设置使用预编译头,并且将要预编译的头文件填上去
基准测试
在C++中,基准测试(benchmarking)是指评估和比较不同实现方式或算法性能的过程。基准测试通常用于衡量代码片段或整个程序在特定条件下的性能表现,如执行时间、内存使用等。其主要目的是找出代码中可能存在的性能瓶颈或改进空间。
举例:
通过定义计时器timer来计算代码运行消耗时间,测试make shared和new shared和make unique谁更快。
最终测试结果是make unique最快。
需要注意的是:
在测试中VisualStudio的设置中release和debug的编译方式是不一样的。
Debug 配置适合于开发和调试阶段,因为它提供了详细的调试信息,并且程序行为更接近于源码,便于发现和修复问题。
Release 配置适合于程序发布阶段,因为它生成的代码经过优化,执行效率更高,适合最终用户使用。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <iostream> #include <memory> #include <chrono> #include <array> class Timer { public : Timer () { m_StartTimepoint = std::chrono::high_resolution_clock::now (); } ~Timer () { Stop (); } void Stop () { auto endTimepoint = std::chrono::high_resolution_clock::now (); auto start = std::chrono::time_point_cast <std::chrono::microseconds>(m_StartTimepoint).time_since_epoch ().count (); auto end = std::chrono::time_point_cast <std::chrono::microseconds>(endTimepoint).time_since_epoch ().count (); auto duration = end - start; double ms = duration * 0.001 ; std::cout << duration << "us(" << ms << "ms)\n" ; } private : std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint; }; int main () { struct Vector2 { float x, y; }; { std::array<std::shared_ptr<Vector2>, 1000 > sharedPtrs; std::cout << "Make Shared" << std::endl; Timer timer; for (int i = 0 ; i < sharedPtrs.size (); i++) sharedPtrs[i] = std::make_shared <Vector2>(); } { std::array<std::shared_ptr<Vector2>, 1000 > sharedPtrs; std::cout << "New Shared" << std::endl; Timer timer; for (int i = 0 ; i < sharedPtrs.size (); i++) sharedPtrs[i] = std::shared_ptr <Vector2>(new Vector2 ()); } { std::array<std::unique_ptr<Vector2>, 1000 > uniquePtrs; std::cout << "Make Unique" << std::endl; Timer timer; for (int i = 0 ; i < uniquePtrs.size (); i++) uniquePtrs[i] = std::make_unique <Vector2>(); } __debugbreak(); }
Optional的使用
在C++中,std::optional 是一个模板类,用于表示一个可能包含值的对象,或者不包含值(即为空)。它可以用来表达那些可以有“缺失”状态的情况。std::optional 是C++17标准引入的。
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 28 29 #include <iostream> #include <optional> #include <string> #include <sstream> #include <fstream> std::optional<std::string> ReadFileAsString (const std::string& filepath) { std::ifstream stream (filepath) ; std::string result; if (stream) { std::stringstream buffer; buffer << stream.rdbuf (); result = buffer.str (); stream.close (); return result; } return {}; } int main () { std::string filepath = "data.txt" ; std::optional<std::string> data = ReadFileAsString (filepath); std::string result = data.value_or ("没有读取到文件" ); std::cout << result << std::endl; std::cin.get (); }
单一变量存放多种类型的数据
std::variant 是 C++17 引入的一个标准库类型,它允许在一个变量中存储多个不同类型之一。换句话说,std::variant 是一个类型安全的联合体(union),可以在编译时确定可以存储的类型集合,并在运行时存储其中的一个类型的值。
主要特点:
1.类型安全:std::variant 确保了访问变量时的类型安全,不会出现传统 C++ 中联合体(union)中未定义行为的问题。你可以使用 std::get 和 std::visit 访问和操作 variant 中的值,这些方法都提供了类型检查。
2.可以存储多个类型之一:std::variant 可以存储多个可能的类型,但在任何时候只能存储其中的一个类型。例如,std::variant<int, std::string> 可以存储一个 int 或者一个 std::string,但不能同时存储两者。
类型索引:std::variant 内部跟踪当前存储的类型的索引,可以使用 index() 方法获取当前存储类型在模板参数中的索引。
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 #include <iostream> #include <variant> int main () { std::variant<int , std::string> data1; std::variant<int , std::string> data2; data1 = "StringName" ; data2 = 1 ; if (auto value = std::get_if <std::string>(&data1)) { std::string& v = *value; std::cout << "data1的值是: " << v << " data1 的类型是string" << std::endl; if (auto value2 = std::get_if <int >(&data2)) { std::cout << "data2的值是: " << *value2 << " data2 的类型是int" << std::endl; } } else { std::cout << "data 不是string类型" << std::endl; } std::cout << "data1的类型序号为:" << data1. index () << " data2的类型序号为: " << data2. index (); std::cin.get (); }
std::async与互斥锁
std::async 是 C++11 中引入的一个函数模板,用于启动异步任务。它可以在后台线程中异步执行一个函数,并返回一个 std::future 对象,通过该对象可以获取异步操作的结果。
互斥锁:如果异步任务访问了共享数据,并且至少一个线程修改了这些数据,那么你需要使用锁来保护这些数据,防止出现数据竞争的情况。数据竞争可能导致未定义行为,包括程序崩溃或产生错误的结果。
std::launch 标志:控制异步任务的执行策略,std::launch::async:强制创建新线程执行任务。std::launch::deferred:任务延迟执行,直到调用 get() 或 wait(),这时任务才在调用线程中执行。
std::async 返回一个 std::future 对象,通过它可以:
获取结果:调用 future.get() 会返回异步任务的结果。
等待完成:调用 future.wait() 可以阻塞线程,直到异步任务完成。
std::mutex 是 C++11 标准库中提供的一种互斥锁(mutex),用于保护共享资源,使得同一时刻只有一个线程可以访问该资源。通过使用 std::mutex,可以防止多个线程同时修改共享数据,从而避免数据竞争。
std::lock_guard 是一个 RAII 风格的锁管理器,用来自动管理锁的获取和释放。当创建 lock 对象时,它会自动锁定 mtx,并在 lock 对象离开作用域时(即 for 循环的每次迭代结束后)自动释放锁。
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 #include <iostream> #include <future> #include <mutex> std::mutex mtx; int shared_value = 0 ;void increment_shared_value (int num) { for (int i = 0 ; i < num; ++i) { std::lock_guard<std::mutex> lock (mtx) ; ++shared_value; } } int main () { std::future<void > f1 = std::async (std::launch::async, increment_shared_value, 10000 ); std::future<void > f2 = std::async (std::launch::async, increment_shared_value, 10000 ); f1. get (); f2. get (); std::cout << "Final shared value: " << shared_value << std::endl; return 0 ; }
std::string_view(C++17支持的更快的string)
std::string_view 是 C++17 中引入的一种轻量级的、不可拥有的字符串视图类型。它提供了一种不需要拷贝或分配内存即可操作字符串的方式,非常适合用于只读字符串操作。
C++的单例模式(设计模式)
当我们想要拥有应用于某种全局数据集的功能,且我们只是想要重复使用时,单例是非常有用的.
例子:渲染器,渲染器通常是一个非常全局的东西,我们通常不会有一个渲染器的多个实例,我们有一个渲染器,我们向它提交所有这些渲染命令,本质上就是调用全局函数.
单例的行为就像命名空间,单例类可以像命名空间一样工作.
C++中的单例只是一种组织一堆全局变量和静态函数的方式.
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 28 29 30 31 #include <iostream> class Random { public : Random (const Random&) = delete ; static Random& Get () { static Random instance; return instance; } static float Float () { return Get ().IFloat (); } private : Random () {}; const float IFloat () const { return m_RandomGenerator; } float m_RandomGenerator = 0.5f ; }; int main () { float random_float = Random::Float (); std::cout << random_float << std::endl; std::cin.get (); }