Cherno C++系列笔记22——P64~P65 多维数组、std::sort排序

1.P64 多维数组

参考:视频 笔记

1.1.二维数组的内存分配和初始化

1.1.1.内存分配

多维数组实际上就是数组的数组,是数组的集合,可以用指针处理数组。如下代码:

#include<iostream>

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

注意

  1. 上面的代码中,二维数组分配的时候是new int*,而不是new int。因为int** a2d的实际意义就是这是一个指向int*类型的指针,也就是这是一个指向int型的指针的指针,所以在new的时候也要new int*
  2. 另外注意上面的语句仅仅是进行了内存分配,并没有进行变量的初始化。所以对于内存分配来说,每个int*都是一个指针,结果也是4字节,所以int** a2d也是得到了200个字的空间,用来存储这些指针。究竟存储的这些指针指向哪些内存地址(也就是一维数组的地址)还没有进行赋值,所以二维数组还需要进行初始化。

1.1.2.初始化

我们可以遍历并设置每个指针指向一个数组,就有了一个包含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();
}

1.2.二维数组的内存释放

因为是堆分配,所以需要遍历它们然后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();
}

1.3.三维数组

以上就是二维数组的内存分配和初始化过程,如果是三维数组 ,那么使用也是类似的。如下所示。

#include<iostream>

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

	for (int i = 0;i < 50;i++)
	{
		a3d[i] = new int*[50]; //指针数组
		// 问题:感觉上面这句话应该写成  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();
}

问题

a3d[i] = new int*[50]; //指针数组
// 问题:感觉上面这句话应该写成  a3d[i] = new int**[50];  ?

1.4.尽量使用一维数组代替多维数组提高内存访问效率

1.4.1.缓存命中和缓存不命中

上面使用多维数组的方式,对编程来说比较方便使用,但是内存分配效率并不高。

以二维数组为例,每次分配其中一维数组的指针的时候,都是new int[50],这样就导致每次分配的一维数组的内存地址都是随机的,因为堆上的内存是由空闲链表进行随机分配的。这样可能两个一维数组离得很近,也可能离得很远,所以这样分配内存就会造成内存碎片的问题。但是一维数组在内存中就是连续分配的。

内存是否连续对访问效率有什么影响呢?像二维数组那样,访问数组的下一行的时候,就需要调到另一个地址访问,这样会导致cache miss(缓存不命中)的问题,这样我们就必须从RAM中读取数据,而不是从缓存cache中读取数据,这样就会浪费时间。但是如果是一维数组这样内存连续分配的,那么造成cache miss的可能性就小得多。实际上,编程优化内存的时候,其中可以优化的一件事情就是优化内存访问。如果把内存分配到一起,那么在定位数据的时候,就会有更多的cache hits(缓存命中)以及更少的cache miss(缓存不命中)

1.4.2.用一维数组代替二维数组

如下面的程序,根据二维数据的角标手动计算一维数组的角标,变成在一维数组中的访问。
在这里插入图片描述

另外B站翻译视频的UP主下面的留言:结论与Cherno的完全相反,二维数组比一维在debug与release下,均快1倍,如果在二维数组方式下,加上一句delete【】,再快将近5倍。

先把问题抛在这,以后再看看

#include<iostream>
#include<chrono>

struct Rgb
{
	int r;
	int g;
	int b;
};

#define M 8000
#define N 5000

void draw()
{
	Timer timer("draw");
	Rgb* a = new Rgb[M * N];
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < N; j++)
		{
			a[i + j * M] = {1,2,3};
		}
	}
	//delete [] a;
}

void draw2()
{
	Timer timer("draw");
	Rgb** a = new Rgb*[M];
	for (int i = 0; i < M; i++)
	{
		a[i] = new Rgb[N];
		for (int j = 0; j < N; j++)
		{
			a[i][j]  = { 1,2,3 };
		}
		//delete [] a[i];   //这一句很神奇,加上后在release模式下,速度快5倍
	}
	//delete [] a;
}

int main()
{
	draw();
	draw2();
}

实际上它上面访问一维数组的方式有问题:

  • 应该是你draw里面赋值的时候有问题。你这个两层循环内层是j,但j又是列指标,所以相当于本来完全连续的赋值变成每次赋值都要跑隔M的地方才能赋所以会变得很慢。
  • 这里分配必然是慢的,因为是间隔分配,了解内存分配都知道越分散性能越差。Cherno说的快应该是读取的时候,读取的时候因为少了间接性(多层指针指向),读取性能要比多维高很多,修改性能应该也高很多。 另这里不应该用 [i + j * M]而是应该用[j + i * N]这样性能也会好很多,因为这是连续分配。
  • release模式会优化代码,不一定会执行全部。另外按升序遍历,索引应该是i*N+j,因为j走一遍,i才加1。连续的内存才能容易cache hit

2.P65 std::sort排序

参考:视频 笔记

关于排序算法,可以自己写,比如冒泡排序等。也可以使用C++标准库中的std::sort函数来进行排序,调用这个函数需要#include <algorithm>

std::sort是C++内置的排序函数,可以为任何类型的迭代器执行排序。我们需要给它提供一个开始迭代器和结束迭代器,迭代器内的所有东西都会基于我们提供的某个谓词进行排序,这个谓词实际就是制定了排序的规则。

这里的谓词有以下几种,一般来说使用lambda表达式的方式更加常见。

  • 可以不传入任何谓词,则默认对int数据是从小到大排序;还可以使用std内置的谓词函数,比如使用std::greater,会按照从大到小顺序排列,但是需要添加头文件functional
#include<iostream>
#include<vector>
#include<algorithm>    // std::sort
#include<functional>   // std::greater<int>


int main()
{
	std::vector<int>  values = {3, 5, 1, 4, 2};          
	
	// 若想对vector做升序排列,传入起始的values.begin()和结束的values.end(),可以不提供任何谓词,对于整数它会默认按升序排列。
	std::sort(values.begin(), values.end());
	
  //使用std内置函数
	std::sort(values.begin(), values.end(),std::greater<int>()); 

	for (int value : values)
	{
		std::cout << value << std::endl;
	}
	std::cin.get();
}
  • 可以通过传递一个函数让它按照某种方式进行排序,既可以是创建的结构体函数,也可以是lambda

关于lambda表达式作为谓词的使用方法是:[](int a,int b){ return true/false},即传入两个参数ab,这两个参数就是在排序的时候依次从迭代器中拿到的相邻的前后两个数据。若想要传入的第一个参数a最后排在前面则返回true,若想要传入的第二个参数b排在前面则返回false。比如如果返回return a<b,那么如果a真的小于b的话,他就会被排在前面,否则就会被排在后面,所以这就是升序排序。

再比如想把values = {3, 5, 1, 4, 2}; 中的元素1排到最后,因此lambda表达式的写法应该是:拿到了相邻两个元素ab,他们如果都不是1,那么就进行升序排序;若a1,则返回false把它移到后面去;如果b1,则返回truea仍然排在前面。

#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;
	});
	
	// 把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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值