C++学习笔记(8)

四十二、命名空间

有两个print函数它们有不同的函数签名,一个参数是string,一个参数是const char。若转变为两者都接受const char*,不使用命名空间可能会出现问题。

#include<iostream>
#include<string>

//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;
	}
//}

int main()
{
	print("Hello");
	std::cin.get();
}
//按crtl+F7编译会报错,它告诉我们这个print函数参数是const char*的,已经有了函数体。我们有两个相同名字的符号,会产生编译或者链接错误。
  • 命名空间的主要目的是避免命名冲突,类本身也是一种命名空间。

若apple有多个函数比如print_again,但只想单独引入print函数,可以使用using apple::print。

#include<iostream>
#include<string>

namespace apple {

	void print(const char* text)
	{
		std::cout << text << std::endl;
	}

	void print_again()
	{

	}
}

namespace orange {

	void print(const char* text)
	{
		std::string temp = text;
		std::reverse(temp.begin(), temp.end());
		std::cout << temp << std::endl;
	}
}

int main()
{
	using apple::print;

	print("Hello");

	apple::print_again();

	std::cin.get();
}

也可以给命名空间取别名;

int main()
{
	namespace a = apple; //a只存在于main函数的作用域下
	a::print("Hello");
	std::cin.get();
}

四十三、线程

使用线程类的所有东西需要thread头文件。

等待线程完成它的工作的方法是输入worker.join()。join的目的是在主线程上等待工作线程完成所有的执行后,再继续执行主线程,这意味着cin.get()代码不会运行。

#include<iostream>
#include<thread>

void DoWork()
{
}

int main()
{
	std::thread worker(DoWork); //工作线程
	worker.join();
	std::cin.get();
}

如下代码,按F5运行会一直打印Working…,再按回车会暂停打印,再按回车程序会结束。

#include<iostream>
#include<thread>

static bool s_Finished = false;

void DoWork()
{
	while (!s_Finished)
	{
		std::cout << "Working...\n";//等同于<<std::endl;
	}
}

int main()
{
	std::thread worker(DoWork); 
	std::cin.get();   //暂停打印
	s_Finished = true;

	worker.join();
	std::cin.get();  //程序结束
}

若让线程每秒打印一次,需要引入命名空间std::literals::chrono_literals;

#include<iostream>
#include<thread>

static bool s_Finished = false;

void DoWork()
{
	using namespace std::literals::chrono_literals;
	//打印id
	std::cout << "Started thread id =" << std::this_thread::get_id() << std::endl;
	while (!s_Finished)
	{
		std::cout << "Working...\n";
		std::this_thread::sleep_for(1s); //暂停一秒
		//this_thread可以用来给当前线程下达命令,也可以打印这个线程的id。
	}

}

int main()
{
	std::thread worker(DoWork); 
	std::cin.get();
	s_Finished = true;
	worker.join();
	//打印id
	std::count<<"Started thread id = "<<std::this_thread::get_id()<<std::endl;
	
	std::cin.get();
}

四十四、计时

C++11后有chrono,它是C++库的一部分,不需要去使用操作系统库。 但是在这之前如果想要一个非常精确的计时器,那么需要使用操作系统库。例如Windows中有QueryPerformanceCounter,事实上如果想要更多地控制计时,控制CPU的计时能力,那么你可能会使用平台特定的库。

#include <iostream>
#include <chrono>
#include <thread>

int main()
{
	using namespace std::literals::chrono_literals;

	auto start = std::chrono::high_resolution_clock::now(); // 当前时间
	std::this_thread::sleep_for(1s);
	auto end = std::chrono::high_resolution_clock::now();

	std::chrono::duration<float> duration = end - start;
	std::cout << duration.count() << "s " << std::endl;

	std::cin.get();
}

std::endl因为某些原因非常慢,处于优化换成\n,并且包装一个结构体:

#include <iostream>
#include <chrono>
#include <thread>

struct Timer
{
	std::chrono::time_point<std::chrono::steady_clock> start, end;
	std::chrono::duration<float> duration = end - start;

	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!\n";//优化
	}
}

int main()
{
	Function();
	std::cin.get();
}

四十五、多维数组

多维数组实际上是数组的集合,可以用指针处理数组。如下代码从堆分配简单的数组。array[0]是一个int,a2d[0]是int指针。

#include<iostream>

int main()
{
	int* array = new int[50]; //分配200个字节内存
	
	//指向指针集合的指针
	int** a2d = new int* [50]; // 分配50个int*,用空间来存储50个int指针。
	
	std::cin.get();
}

我们可以遍历并设置每个指针指向一个数组,就有了一个包含50个数组的内存位置的数组,这就是二维数组。

#include<iostream>

int main()
{
	int** a2d = new int* [50]; 
	for (int i = 0; i < 50;i++)
		a2d[i] = new int[50]; //数组中的每个位置都存在a2d数组中
	std::cin.get();
}

三维数组:

#include<iostream>

int main()
{
	int*** a3d = new int**[50];

	for (int i = 0;i < 50;i++)
	{
		a3d[i] = new int*[50]; //指针数组
		{
			for (int j = 0;j < 50;j++)
			{
				int** ptr = a3d[i];
				ptr[j] = new int[50]; 设置每个指针等于一个数组,int是实际类型分配
			   //或者 a3d[i][j] = new int[50]; 
			}
		}
	}
	std::cin.get();
}

因为是堆分配,所以需要遍历它们然后delete所有的数组,不能一次delete就搞定。若只删除一次会造成内存泄漏,50个数组的数据无法被访问,因为删除了保存这些指针的数组。

#include<iostream>

int main()
{
	int** a2d = new int* [50];
	for (int i = 0; i < 50;i++)
		a2d[i] = new int[50]; 

	for (int i = 0; i < 50;i++)
		delete[] a2d[i];
	
	delete[] a2d;  //若只删除一次
		              
	std::cin.get();
}

四十六、排序

std::sort是C++内置的排序函数,可以为任何类型的迭代器执行排序。

我们需要给它提供一个开始迭代器和结束迭代器,迭代器内的所有东西都会被排序,基于我们提供的某个谓词排序,需要头文件algorithm。若想对vector做升序排列,传入起始的values.begin()和结束的values.end(),可以不提供任何谓词,对于整数它会默认按升序排列。

#include<iostream>
#include<vector>
#include<algorithm>//sort在此头文件中

int main()
{
	std::vector<int>  values = {3, 5, 1, 4, 2};          
	
	std::sort(values.begin(), values.end());

	for (int value : values)
			std::cout << value << std::endl;
	
	std::cin.get();
}

我们可以通过传递一个函数让它按照某种方式进行排序,即可以是创建的结构体函数,也可以是lambda。或者使用内置函数,添加头文件functional,使用std::greater,会按照从大到小顺序排列

#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(),std::greater<int>()); //使用内置函数

	for (int value : values)
			std::cout << value << std::endl;

	std::cin.get();
}

可以设一个普通的lambda:[ ](int a,int b);给它提供一个函数,若传入的第一个参数排在前面则返回true,a < b则按升序排列。

#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)
	{
			return a < b;
	});

	for (int value : values)
	std::cout << value << std::endl;

	std::cin.get();
}

我们想把1排到最后,若a是第一个值,则把它移到后面去,我们返回false,不希望它在b前。如果b==1,我们希望a在前面,要返回true。

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
	std::vector<int> values = { 3, 5, 1, 4, 2 };
	std::sort(values.begin(), values.end(), [](int a, int b)
	{
		// 返回true意味着a排在前面

		// 1 排在末尾
		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();
}

四十七、类型双关

类型双关(type punning)只是一个花哨的术语,用来在C++中绕过类型系统

C++是一个强类型语言,也就是说我们有一个类型系统。而像JavaScript就没有变量类型的概念。

然而,C++的这种类型系统并不像在其他语言中那样强制,比如Java,它们的类型很难绕开,包括C#也是,你虽然也可以绕开类型系统,但要做更多的工作。在C++中虽然类型是由编译器强制执行的,但你可以直接访问内存。

把一个int型的内存,换成double去解释,当然这样做很糟糕,因为添加了四字节不属于原本自己的内存,只是作为演示。

#include <iostream>

int main()
{
	int a = 50;
	//取地址,换成对应类型的指针,再解引用
	double value = *(double*)&a;
	std::cout << value << std::endl;

	std::cin.get();
}

而如果只是想针对int的这四个字节,就可以用引用,而不是拷贝成一个新的变量:

#include <iostream>

int main()
{
	int a = 50;
	double& value = *(double*)&a;//value是引用的a
	std::cout << value << std::endl;

	std::cin.get();
}

四十八、联合体

联合体有点像类类型,或者结构体类型。只不过它一次只能占用一个成员的内存。若我们有一个结构体声明了4个浮点数,则有16个字节在这个结构体中。若用联合体声明声明4个浮点数,联合体的大小仍然是4个字节。

可以像使用结构体或者类一样使用它们,也可以给它添加静态函数或者普通函数、方法等待。然而你不能使用虚方法,还有其他一些限制。但通常人们用联合体来做的事情,是和类型双关紧密相关的。通常union是匿名使用的,但是匿名union不能含有成员函数

#include <iostream>

int main()
{
	struct Union
	{
		union
		{
			float a;
			int b;
		};
	};

	Union u;
	u.a = 2.0f;
	std::cout << u.a << ", " << u.b << std::endl;

	std::cin.get();
}
#include<iostream>

struct Vector2
{
	float x, y;
};

struct Vector4
{
	float x, y, z, w;
	
	Vector2& GetA()//联合
	{
		return *(Vector2*)&x;
	}
};

void PrintVector2(const Vector2& vector)
{
	std::cout << vector.x << "," << vector.y << std::endl;
}
int main()
{
	std::cin.get();
}

Vector4实际上是2个Vector2,所以构造一个Vector2,给它一些参数,返回整个对象,但是这会创建一个全新的对象,所以可以返回一个指向Vector2的引用,引用的x是一个类型双关的版本,我们要做的就是把x的内存地址转换成Vector2,然后解引用。

另一种方法是使用union,这是一个匿名union,因为union只有一个成员,所以x,y,z,w会占用相同的空间,我们把它包装在一个匿名结构体中。现在这个结构体是union的一个成员,这是一个16字节的结构体。我们创建一个Vector4,可以正常访问vector.x并把它设为2.0f。

#include<iostream>

struct Vector2
{
	float x, y;
};

struct Vector4
{
	union//匿名union
	{
		struct//匿名结构体
		{
			float x, y, z, w;
		};
	};
};

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 };
	vector.x = 2.0f;
	std::cin.get();
}

若继续创建一个结构体,向union添加了第二个成员,它和第一个元素占据相同的空间,它有两个Vector2组成。

struct Vector2
{
	float x, y;
};

struct Vector4
{
	union
	{
		struct 
		{
			float x, y, z, w;
		};

		struct
		{
			Vector2 a, b;
		};
	};
};

现在有几种访问Vector4内数据的方法,可以用xywz或者ab,a和xy的内存是一样的,b和zw的内存一样。如下代码:


int main()
{
	Vector4 vector = { 1.0f,2.0f,3.0f,4.0f };
	PrintVector2(vector.a);
	PrintVector2(vector.b);
	vector.z = 500.0f;
	std::cout << "------------------" << std::endl;
	PrintVector2(vector.a);
	PrintVector2(vector.b);
	vector.x = 2.0f;
	std::cin.get();
}

在这里插入图片描述
虽然没有碰过vector里b的部分,设置的是z=500,但它们占用了相同的内存。

四十九、虚析构函数

只要你允许一个类拥有子类,就一定要把析构函数写成虚函数,否则没人能安全地扩展这个类。

如果用基类指针来引用派生类对象,那么基类的析构函数必须是 virtual 的,否则 C++ 只会调用基类的析构函数,不会调用派生类的析构函数。

#include <iostream>

class Base
{
public:
	Base() { std::cout << "Base Constructor\n"; }
	virtual ~Base() { std::cout << "Base Destructor\n"; }//基类的析构函数必须是 virtual (虚)的
};

class Derived : public Base//Base的子类
{
public:
	Derived() { m_Array = new int[5]; std::cout << "Derived Constructor\n"; }
	~Derived() { delete[] m_Array; std::cout << "Derived Destructor\n"; }
private:
	int* m_Array;
};

int main()
{
	Base* base = new Base();
	delete base;
	std::cout << "------------------------\n";
	Derived* derived = new Derived();
	delete derived;
	std::cout << "------------------------\n";
	Base* poly = new Derived();
	delete poly;

	std::cin.get();
}

在这里插入图片描述

五十、类型转换

这里的类型转换是指在C++可用的类型系统中进行的类型转换;类型转换分为隐式和显示。

//隐式转换
int a = 5;
double value = a;

double value=5.25;
int a=value;

//显示转换
double value=5.25;
int a=(int)value;

double value=5.25;
double a=(int)value+5.3;//a=10.3,会截value掉小数的0.25
//double a=(int)(value+5.3);//a=10

//还可以用C++的方式cast,这里使用static_cast(静态类型转换)。
double s=static_cast<int>(value)+5.3;

C++风格,四种主要的cast:
static_cast、reinterpret_cast、dynamic_cast、const_cast共四种主要的cast。(它们可能会做其他的事情,但是实际的结果也只是一个成果的类型转换而已

  • 静态类型转换的情况下(static_cast),它们还会做一些其他的编译时检查,看看这种转换是否真的可能;
  • reinterpret_cast则如单词reinterpret的重新解释意思一样,联系类型双关;
  • const_cast,移除或者添加变量的const限定。

搞这么多cast的好处是,除了可能收到的那些编译时检查以外,还可以在代码库中搜索它们。

对于dynamic_cast,示例:

#include <iostream>

class Base
{
public:
	Base() { }
	virtual ~Base() { }
};

class Derived : public Base
{
public:
	Derived() { }
	~Derived() { }
};

class AnotherClass : public Base
{
public:
	AnotherClass() { }
	~AnotherClass() { }
};

int main()
{
	Derived* derived = new Derived();

	Base* base = derived;

	std::cin.get();
}

我们看到,有Derived和AnotherClass同时继承Base,通过Base* base = derived;我们有了base指针,那么有了这个指针后,我们怎样判定这个指针是一个Derived的实例呢还是一个AnotherClass类的实例呢?

这时候用dynamic_cast,它做的就不仅是问这个问题,而且还会尝试去做转换,如果失败还会做一些事情。

#include <iostream>

class Base
{
public:
	Base() { }
	virtual ~Base() { }
};

class Derived : public Base
{
public:
	Derived() { }
	~Derived() { }
};

class AnotherClass : public Base
{
public:
	AnotherClass() { }
	~AnotherClass() { }
};

int main()
{
	Derived* derived = new Derived();

	Base* base = derived;

	AnotherClass* ac = dynamic_cast<AnotherClass*>(base);
	if (ac)
	{
		std::cout << "This is the AnotherClass" << std::endl;
	}
	else
	{
		std::cout << "This is the Derived" << std::endl;
	}

	std::cin.get();
}

比如这里,我们知道base其实是一个Derived指针,因此这里dynamic_cast<AnotherClass*>(base)会失败,得到的ac就会是nullptr,因此会打印This is the Derived。

所以dynamic_cast是一个很好的方法,来查看转换是否成功。它与运行时类型信息RTTI(runtime type information)紧密联系。它会做运行时检查。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值