C++杂记

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++中的单例模式只是一种组织一堆全局变量和静态函数方式,这些静态函数有时可能对这些变量起作用,有时也可能不对这些变量起作用,最后这些组织在一起,本质上是在一个单一的名称空间下。

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值