C++杂记
1.静态 static
c++类外的static 使该变量只允许在该文件中被看到
在编译器链接时,不会读取到该变量
例如:test.cpp文件中
static int s_Value = 5;//s_Value 这个变量只会在test.cpp文件中被看到
2.enum
例:
enum Example:变量类型(只能为整数)
{
A,B,C //如果没有规定数值,默认A为从0开始,B为1 逐渐递加
};
3.构造函数
class Log{
private:
log(){}//使构造函数无法被访问,导致对象无法被创建
public:
Log()=delete;//删除构造函数,使类无法创建对象
};
4.虚函数
类A是类B的基类,如果想在类B中重写类A中的某个方法去做其他事情,须在类A中使用virtual将该方法标记为虚函数
在派生类中,在重写的函数后面加上override(非必须,但是可以增加代码可读性,预防bug)
额外的性能开销(比较小,基本上可以忽略)
虚函数需要额外的内存来存储虚表(v表),是我们可以使用正确的函数,而且基类中需要一个成员指针,指向v表
每当我们调用虚函数时,需要遍历这个表,来确定要映射到哪个函数
5.纯虚函数(接口?还需探索,暂时没明白)
6.可见性
public:公开可见
private:只有该类可以访问
protect:只有该类以及该类的子类可以访问
7.数组
如果想返回函数中创建的数组,需要在函数中使用new关键字创建该数组
8.字符串
字符串就是字符数组,数组是一组元素的集合
c语言定义字符串的方式 ------const char* arr=“array”;
当读到0或‘\0‘时,停止读取字符串或字符数组 占一个字符空间
字符串复制速度很慢 传递字符串时,尽量使用引用传递
const char* example = R"line1
line2
line3";//字符串之前加上R会忽略字符串中的转义字符 如"\0"
字符串字面量永远保存在内存的只读区域
char *p = “hello”; // p是一个指针,直接指向常量区,修改p【0】就是修改常量区的内容,这是不允许的。
char p [ ] = “hello”; // 编译器在栈上创建一个字符串p,把"hello"从常量区复制到p,修改p【0】就相当于修改数组元素一样,是可以的。 ------具体参考内存分布
9.const
第一种用法:
const int* a = new int;//说明不能更改a指向内存中的内容 可以改变指针指向的地址
int* const a =new int;//说明不能更改a指针指向的地址 可以更改指针指向内存中的内容
int const * 的作用和const int* 是一样的
重点在于const 在 * 之前还是之后
第二种用法:
const int* const a =new int;//说明既不能更改指针的指向,也不能更改指向内存的内容
第三种用法:
在方法名之后加上const,这个方法不会修改任何实际的类
mutable int var;//mutable使变量可修改,在const修饰的函数内仍然适用
int GetX( ) const{
var=2;//这里是对的
return m_X;
}
int* m_X,m_Y;//此时m_X是int类型指针,而m_Y是int整型\
在有常量引用或类似的情况下,只能调用const修饰的函数,否则报错
void Print(const Entity& e){
std::cout<< e.GetX() <<std::endl;
}
10.成员初始化列表
class Entity
{
private:
std::string m_Name;
public:
Entity( ) : m_Name("Unknown"){ }
Entity(const std::string& name ) : m_Name(name){ }
};
按照定义的先后顺序来初始化
如果不使用成员初始化列表会浪费性能 不使用列表,在初始化时,会创建两个对象,然后扔掉一个
11.三元操作符
std::string rank = s_Level > 10 ? “Master” : “Beginner”;
12.new关键字
Entity* e = new Entity();
使用了构造函数,并且返回在堆上创建的对象的内存地址
Entity* ent = (Entity*)malloc(sizeof(Entity));//c用法,只申请了空间,没有调用构造函数
13.隐式转换与explicit关键词
c++编译器允许代码执行一次隐式转换
explicit 会禁止隐式转换 可用于数学库之类的东西
如果想要构造这个对象,则必须显式调用使用了explicit的构造函数
14.运算符及其重载
// << 重载
std::ostream& operator<<(std::ostream& stream,const Entity& oth) {
stream << oth.m_Name <<", " << oth.m_Age;
return stream;
}
###
15.this关键字
this 是指向当前对象的指针
16.作用域指针unique_ptr
unique_ptr是作用域指针,不能够复制
超出作用域时,它会被销毁,然后调用delete
建议使用:
std::unique_ptr entity = std::make_unique();
不建议使用:
std::unique_ptr e0(new Entity());
不直接调用new 的原因是因为异常安全
17.shared_ptr
建议使用:
std::shared_ptr e = std::make_shared();
shared_ptr需要分配另一块内存,叫做控制块,用来存储引用计数
引用计数:可以跟踪指针有多少引用,一旦引用计数达到0,它就被删除了
例:当创建了一个shared_ptr之后,再创建另一个shared_ptr来复制它,此时的引用计数器是2,当第一个shared_ptr被释放时,引用计数器减少1,当另一个shared_ptr释放时,此时引用计数器为0,内存被释放
当将一个shared_ptr赋值给另一个shared_ptr,引用计数器会增加引用计数,但是当把一个shared_ptr赋值给另一个weak_ptr时,引用计数器不会增加引用计数
18.weak_ptr 弱指针
19.拷贝构造函数
拷贝构造函数是一个构造函数,当复制第二个字符串时,它会被调用
当你试图创建一个新的变量并给它分配另一个变量时,这个变量和正在创建的变量又相同的类型,复制这个变量,这就是所谓的拷贝构造函数
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);
}
void *memcpy(void *destin, void *source, unsigned n);
作用是:以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。
函数有三个参数,第一个是目标地址,第二个是源地址,第三个是数据长度。
使用memcpy函数时,需要注意:
1.数据长度(第三个参数)的单位是字节(1byte = 8bit)。
2.注意该函数有一个返回值,类型是void*,是一个指向destin的指针。
注意:总是通过const引用传递对象,最好不要复制
20.vector
动态数组是连续的内存空间
当vector的空间不够时,vector需要分配新的内存,至少足够容纳这些想要加入的新元素,当前vector的内容,从内存中的旧位置复制到内存中的新位置,然后删除旧位置的内存
当我们尝试push_back一个元素时,如果容量用完,则会调整大小,重新分配,在这个过程中,会复制元素对象等等,这是将代码拖慢的原因之一
vector vertices;
优化方法:
设置vector对象的容量,reserve 确保有足够的内存 vertices.reserve(3); 设置容量为3 与调整大小(resize)不同
emplace_back( 参数 ); 在实际的vector内存中,使用以下参数构造一个相应的对象
21.静态链接(看不懂,还需探索)
注:cherno p49~p51
22.tuple以及pair(返回多返回值)
当需要返回两个或两个以上类型不同的返回值时,可以使用struct结构体
例:
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
static std::ShaderProgramSource ParseShader(const std::string& filepath)
{
......
return { vs , fs };
}
相当于一个pair,由两个string组成 所有东西都是在栈上创建的
23.template模板
只有当模板被调用时才会被创建,因为它只是一个模板,而不是实际的代码
只有当它基于模板的使用情况,发送到编译器,进行编译之后才会具体化为真正的代码
函数:
template<typename T>
void print(T value)
{
std::cout << value << std::endl;
}
类:
template<typename T,int N>
class Array
{
private:
T m_Array[N];
public:
int GetSize() const {
return N;
}
};
24.栈和堆的内存的比较
在应用程序启动后,操作系统要做的是将整个程序加载到内存并分配一大堆物理RAM,以便实际的应用程序可以运行。
栈和堆是RAM中实际存在的两个区域,栈通常是一个预定义大小的内存区域,通常约为2兆字节左右,堆也是一个预定义了默认值的区域,但是它可以生长,并随着应用程序的进行而改变。
重要的是,这两个内存区域的实际位置(物理位置)在RAM中是完全一样的
25.宏
#define MAIN int main()
{\
std::cin.get();\ ’ \ '是转义字符
}
一个宏写在多行上
#if 0
/*
代码块
*/
#endif 此时代码块可重叠,处于禁用状态
26.auto关键字
可以在类型很大的情况下使用auto 其他情况下尽量避免使用auto,因为会让大多数代码都更难读
例如使用迭代器时可以使用auto关键字,或是使用using或typedef取别名
for (std::vector<std::string>::iterator it = strings.begin(); it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
//使用auto
for (auto it = strings.begin(); it != strings.end(); it++)
{
std::cout << *it << std::endl;
}
//使用using
using DeviceMap = std::unordered_map<std::string, std::vector<Device*>>;
//使用typedef
typedef std::unordered_map<std::string, std::vector<Device*>> DeviceMap;
DeviceManager dm;
const std::unordered_map<std::string, std::vector<Device*>>& devices = dm.GetDevices();
const DeviceMap& devices2 = dm.GetDevices();
27.静态数组(std::array)
不增长的数组,当创建这个数组时,定义这个数组有多大,也就是有多少元素以及都是些什么类型的元素,不能改变这个数组的大小
std::array<int,5> data;
data[2] = 3;
28.函数指针
函数指针是将一个函数赋值给一个变量的方法
把函数赋值给变量,还可以将函数作为参数传递给其他函数
void HelloWorld(int value) {
std::cout << "Hello World value : " << value << std::endl;
}
//void(*function1)(int) = HelloWorld;//实际函数指针
//function1(6);
typedef void(*HelloWorldFunction)(int);//重命名这个类型为HelloWorldFunction
HelloWorldFunction function = HelloWorld;//使用这个类型定义变量function
function(8);
函数指针例子:
void PrintValue(int value) {
std::cout << value << std::endl;
}
void ForEach(std::vector<int>& values, void(*func)(int)) { //函数指针定义 void(*func)(int)
for (int value : values) {
func(value);
}
}
std::vector values = { 3,2,5,1,4 };
ForEach(values, PrintValue);
29.lambda
[ ] ( ) { }
auto it = std::find_if(values.begin(), values.end(), [](int value) {return value > 3; });//返回第一个value大于3的迭代器it
std::cout << *it << std::endl;
30.避免使用using namespace std
蛇形命名法:字母小写,单词之间用下划线隔开
帕斯卡命名法:每个单词首字母大写,中间不能有空格或下划线等
驼峰命名法:和帕斯卡的区别是,驼峰首字母小写
31.名称空间 namespace
::是名称空间的操作符,当要使用到某个名称空间时,只需要写上::,就会进入到这个命名空间,然后允许调用这个名称空间中的东西。同样适用于静态函数或者类中的方法,类等等
类本身就是名称空间
使用命名空间原因:
如果我们正在创建一个代码库,或者我们有一个项目,我们希望把它放在命名空间后面,这样就不会有任何命名冲突,可以自由的创建想要的函数
namespace apple {
void print(const char* strings)
{
std::cout << "apple" << std::endl;
}
}
using namespace apple;//在相对的作用域中引用apple命名空间
using apple::print;//只引入apple中的print
namespace a = apple;//给apple取别名
a::print("Hello");
嵌套命名空间
namespace apple {
namespace function {
void print(const char* strings)
{
std::cout << "apple" << std::endl;
}
}
}
using namespace apple::function;
using apple::function::print;
namespace a = apple::function;
a::print("Hello");
不要随意在头文件中使用命名空间
32.线程thread
33.C++计时
通常情况下使用chrono库就足够了
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 << ms << "ms " << std::endl;
}
};
void Function()
{
Timer time;//利用结构体生存周期计算函数运行所需的时间
for (int i = 0; i < 100; i++)
{
std::cout << "Hello\n";
}
}
34.多维数组
一维数组
int* a1d = new int;//a1d存储的是一个指针,指向分配内存空间的地址
二维数组
int** a2d = new int*[50];//a2d存储的是一个指向一个指针空间的指针 这个指针空间里存储的是指向分配空间的指针
for(int i=0;i<50;i++)
{
a2d[i]=new int[50];
}
三维数组
int*** a3d=new int**[50];//a3d存储的是一个指向(指向(指向存储分配内存空间的指针)的指针)的指针
for(int i=0;i<50;i++){
a3d[i]=new int*[50];
for(int j=0;j<50;j++){
a3d[i][j]=new int[50];
}
}
在堆上分配内存需要释放
应该先释放数组中存储的内容,再释放存储指针的数组
如果先释放了存储指针的数组,会导致无法访问到存储内容的数组,从而导致内存泄漏。
for(int i=0;i<50;i++){
delete[] a2d[i];
}
delete[] a2d;
int* array = new int[5*5];//一维数组
for(int y =0;u<5;y++){
for(int x=0;x<5;x++){
array[x+y*5] = 2;//每当y加1,就向下挪一行 这样可以像二维数组一样访问
}
}
35.排序 std::sort
c++内置的排序函数
当没有给定条件是,默认按照升序排序
可以使用内置的函数或lambda函数等等
std::vector<int> values = { 3, 5, 1, 4, 2 };
std::sort(values.begin(),values.end(),std::greater<int>());//从大到小
利用lambda函数排序
std::sort(values.begin(),values.end(),[](int a,int b){
return a < b;//如果a小于b的话,它会排到列表的前面 即从小到大排序
});
sort中的比较函数返回的是bool值,true或者false
如果传入的第一个参数排在前面的话,返回true,否则返回false
36.类型双关
int a=5;
int& ref = a; //相当于给a取了个别名为ref
如果不想创建新的变量,只是想把int当作double来访问,只需要在double后面加一个&
int a = 5;
double& value = *(double*)&a;//这里是引用而不是拷贝成了一个新的变量
如果struct是空的,那么它至少有一个字节
如果要把拥有的这段内存当作不同类型的内存对待,只需要将该类型作为指针,然后将其转换为另一个指针,如果有必要还可以对他进行解引用
37.联合体union
每次占用一个成员的内存
在结构体中声明4个浮点数,一个浮点数为4字节,那么这个结构体占4*4字节,在联合体中声明4个浮点数,联合体的大小仍然是4字节
union通常是匿名使用的,但是匿名union不能含有成员函数 通常用来作类型双关
struct vector4 {
union
{
struct {
float x, y, z, w;
};
struct
{
vector2 a, b;//这里的a相当于上面的x,y b相当于上面的z,w 因为他们占用了相同的内存
};
};
};
void PrintVector(const vector2& vector) {
std::cout << vector.x << " , " << vector.y << std::endl;
}
vector4 vector = { 1.0f,2.0f,3.0f,4.0f };
PrintVector(vector.a);
PrintVector(vector.b);
vector.z = 300.0f;
PrintVector(vector.a);
PrintVector(vector.b);
38.虚析构函数
虚析构函数不是覆写析构函数,而是加上一个析构函数
Base* poly = new Derived(); 会造成内存泄漏,调用了Derived的构造函数,却没有调用Derived析构函数
delete poly;
如何解决:将基类的析构函数标记为虚函数
virtual ~Base() {std::cout<<“Base Destructor\n”;}//这意味着这个类可能被拓展为子类,可能还有一个析构函数也需要被调用 这个告诉人们,你需要调用派生析构函数,如果它存在的话
如果把基类析构函数改为虚函数,它实际上会调用两个析构函数,它会先调用派生类析构函数,然后在层次结构中向上,调用基类析构函数
在写一个要扩展的类或者子类时,只要允许一个类拥有子类,百分之百的需要声明析构函数是虚函数。否则不能安全的扩展这个类,当根据类的基类类型来处理该类时,类的析构函数永远不会被调用
39.类型转换
reinterpret_cast 当不想转换任何东西,只是想把那种指针解释成别的东西的时候,想把现有的内存解释为另一种类型时 主要用于类型双关
const_cast 用来添加或者移除const修饰符
dynamic_cast 会做运行时的检查
static_cast 在静态类型转换的情况下,它们还会做一些其他的编译时检查,看看这种转换是否真的可能
40.条件断点和操作断点(VS操作)
条件断点是在断点处的约束触发条件,并且可以设置断点忽略次数,条件断点在多线程上也能使用,可以线程ID用来分离线程(只在指定的线程中断点)
操作断点是允许我们采取某种动作,一般是在碰到断点时打印一些东西到控制台
41.预编译头文件
接收一堆你告诉它要接收的头文件,它只编译一次,它以二进制格式存储,这对编译器来说比单纯的文本处理要快得多。本质上还是头文件,它包括一堆其他头文件,不要将频繁更改的文件放入到预编译头文件中
预编译头文件可以加速编译时间,也使实际编写代码更加方便
42.dynamic_cast
dynamic_cast像是一个函数,它不像编译时进行的类型转换,而是在运行时计算
dynamic_cast做了额外的工作,会带来一个小的性能成本
专门用于沿继承层次结构进行的强制类型转换
用处:如果我有一个类,它是另一个类的子类,我想转换为基类型,或者从基类型转换为派生类型,可以使用dynamic_cast
如果强制转换是有效的,那么会返回想要的指针的值,如果是无效的,说明不是声称给定的类型,会返回NULL
dynamic_cast 存储了运行时类型信息(RTTI) RTTI存储了所有类型的运行时类型信息
43.基准测试
方法很多,用自己喜欢的方法
例子:运用计时器进行性能测试
#include <iostream>
#include <chrono>
#include <array>
class Timer
{
public:
Timer() {
m_StartTimePoint = std::chrono::high_resolution_clock::now();
}
void stop() {
m_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>(m_EndTimePoint).time_since_epoch().count();
auto duration = end - start;
double ms = duration * 0.001;
std::cout << duration << "us(" << ms << " ms)" << std::endl;
}
~Timer() {
stop();
}
private:
std::chrono::time_point< std::chrono::high_resolution_clock> m_StartTimePoint;
std::chrono::time_point< std::chrono::high_resolution_clock> m_EndTimePoint;
};
int main()
{
struct vector2
{
};
std::cout << "make share " << std::endl;
{
std::array<std::shared_ptr<vector2>, 1000> sharedPtr;
Timer timer;
for (int i = 0; i < sharedPtr.size(); i++)
sharedPtr[i] = std::make_shared<vector2>();
}
std::cout << "new share " << std::endl;
{
std::array<std::shared_ptr<vector2>, 1000> sharedPtr;
Timer timer;
for (int i = 0; i < sharedPtr.size(); i++)
sharedPtr[i] = std::shared_ptr<vector2>(new vector2());
}
std::cout << "make unique " << std::endl;
{
std::array<std::unique_ptr<vector2>, 1000> UniquePtr;
Timer timer;
for (int i = 0; i < UniquePtr.size(); i++)
UniquePtr[i] = std::make_unique<vector2>();
}
std::cin.get();
}
44.结构化绑定(仅针对c++17以及之后的版本)
结构化绑定是一个新特性,让我们更好的处理多返回值
例子:
std::tuple<std::string, int> createPerson()
{
return { "Cherno",24 };
}
int main()
{
auto[name, age] = createPerson();
std::cout << name << " " << age << std::endl;
}
在这个作用域中,name和age都可以访问到
45.如何处理optional数据(c++17以及之后的版本)
例子:
std::optional<std::string> ReadFileAsString(const std::string& filepath)
{
std::ifstream stream(filepath);
if (stream) {
std::string result;
stream.close();
return result;
}
return {};
}
int main()
{
std::optional<std::string> data = ReadFileAsString("data.txt");
if (data.has_value()) {//如果data.has_value是true,意味着可选的已经被设置
std::cout << "File read successfully" << std::endl;
}
else {
std::cout << "File not be open" << std::endl;
}
std::string res = data.value_or("Not Present");//如果数据确实存在于std::optional中,它将返回给我们那个字符串,否则返回我们传入的任何值
std::cout << res << std::endl;
}
46.多线程std::asnyc
线程函数的参数按值移动或复制。如果引用参数需要传递给线程函数,它必须被包装(例如使用std :: ref或std :: cref)
如果std::async没有返回值,那么在一次for循环之后,临时对象会被析构,而析构函数中需要等待线程结束,所以就和顺序执行一样,一个个的等下去
如果将返回值赋值给外部变量,那么生存期就在for循环之外,那么对象不会被析构,也就不需要等待线程结束。
47.如何让c++字符串更快
在堆上分配不是最好的方法,可以避免的情况下应该避免,因为它会降低程序的速度,std::string和它的很多函数都喜欢分配
让字符串更快的方法是完全不使用std::string
追踪内存分配的方法:重载new操作符
void* operator new(size_t size){
return malloc(size);
}
std::string_view (c++17新类) 本质上只是一个指向现有内存的指针,换句话说,就是一个const char* 指针,指向其他人拥有的现有字符串,再加上一个大小(size)
void printName(std::string_view name){
std::cout<<name<<std::endl;
}
std::string name = "Yan Chernikov";
std::string_view firstName(name.c_str(),3);
std::string_view lastName(name.c_str()+4,9);
void printName(const std::string& name){//即使是引用,这里也会分配一次内存 使用了string分配了内存
std::cout<<name<<std::endl;
}
48.单例模式
当我们想要拥有应用于某种全局数据集的功能,且我们只是想要重复使用时,单例是非常有用的
C++中的单例模式只是一种组织一堆全局变量和静态函数方式,这些静态函数有时可能对这些变量起作用,有时也可能不对这些变量起作用,最后这些组织在一起,本质上是在一个单一的名称空间下。