Cherno C++学习笔记 P31 C++数组

这里我们来讲一下C++的数组,可能在我们学习的C++教程当中,很少有这么晚了才讲到数组的,但是这也是我很喜欢Cherno这个C++教程的原因之一,就是他并不急于把一些其他教程里面很早就介绍出来的概念马上就端出来,而是前面做好了足够的基础铺垫之后才会进行深入的介绍。

想要学好数组 ,首先一定要搞懂指针,这其实也是国内C++教育没搞明白的地方,总是先介绍数组后介绍指针,然后又搞出数组指针和指针数组这种绕口令,神烦。但是我们后面应该知道,指针是使用数组的基本方式。

数组是什么?数组是一堆变量组成的集合,且通常都是相同的类型。比如我们在很多场合下,都需要表示大量的相同类型变量,如果我们一个一个来定义然后初始化,就显得非常的愚蠢。所以我们需要一个包含着很多个variable的variable,那么数组就随之而来了。

定义一个数组的方式通常是类型名+数组名称[数组长度],那么接下来我们举一个例子。

cpp 复制代码
#include<iostream>

int main() {
	int example[5];
	example[0] = 2;
	example[3] = 5;
	std::cout << example << std::endl;
	std::cin.get();
}

这样我们就做到了定义了一个数组。这是一个有五个int变量的array,且我们已经给这个数组分配了足够多的内存。

需要注意两个点,第一,我们会发现数组的序号是从0开始的,这个如果稍微有一些编程经验应该都会习惯了,大部分语言都是从0开始计数的;第二,如果我们打印了example,我们会发现打印出来是这样一个东西:

cpp 复制代码
#include<iostream>

int main() {
	int example[5];
	for (int i = 0; i < 5; i++) {
		example[i] = 2;
	}
	std::cin.get();
}

可以看到,打印出来居然是一个地址!说明我们数组名本身其实是一个指针变量。

那么如果我们在使用数组序号时不小心使用了超过数组长度的序号会发生什么?那么通常来说,debug下编译器是会给我们报错的,但是如果是release模式下,其实是不会告诉我们发生了这样的错误,这样会导致我们可能无意识的修改了其他内存,其他变量,从而带来程序问题。

数组通常会和for loop循环配合使用,因为我们希望能够把数组每个位置都填上一些值。

cpp 复制代码
#include<iostream>

int main() {
	int example[5];
	for (int i = 0; i < 5; i++) {
		example[i] = 2;
	}
	std::cin.get();
}

如代码所示,这里有一个小细节,一般在循环判断时,我们倾向于使用 < 而不是 <=,因为使用 < 可以让我们少做一次判断,节省时间。

那么我们进入调试模式来看看内存里面发生了什么。

可以看到,在这个数组存储的位置上,我们有连续五个占有4个字节的int变量2,这也就意味着数组在存储数据的时候是进行的连续存储,而且看起来好像把一整个20字节的内存分成了5份,但是实际上并没有,只是看起来是这样。这也就表明了,我们实际上在使用这个序号的时候做的是什么事情,我们做的其实是地址的移动 。序号表明指针应该向后移动多少,但是具体移动多少个byte还和这个数组的类型有关系,具体关系就是移动的字节数 = 序号*类型大小。在我们这个例子当中,因为是int类型的数组,所以类型大小是4字节,那么我们在赋值的时候,就是分别移动0、4、8、12、16个字节。

因为我们知道了数组的序号实际上是移动的字节数,那么我们其实可以用一些更野的方法来实现。首先就是我们直接使用指针,如下所示:

cpp 复制代码
int* ptr = example;
*(ptr + 1) = 5;

需要注意的一点是,因为我们ptr指针类型是int,所以当ptr加一的时候,实际上会向后移动四个字节。那么我们再进入内存里面看看发生了什么:

可以看到,正如我们所期待的,这样修改了第二个元素的值,从2改为了5。

那么如果我们玩的更花一点,因为我们知道,指针只是一串数而已,与类型无关,那么我们如果将指针类型修改掉,其实也是可以做到找到对应位置并对数组进行操作的。

cpp 复制代码
char* ptr2 = (char*)example;
*((int*)(ptr2 + 8)) = 6;

注意我们在第一行,进行了指针类型强制转换;在第二行,分别进行了指针的移动、指针类型强制转换与逆向引用。注意因为char类型只占一个字节,所以我们需要+8来保证它修改了我们数组当中的第三个元素。我们继续看一下内存:

很好,我们看到确实修改成功了第三个元素的值。但是需要注意的是,一般我们没人会这么写代码= =但是这样的例子确实很容易能加深对C++指针的理解。

除了这种创建数组的方式之外,我们还有使用new关键字在堆上创建数组的方法。

cpp 复制代码
int* example = new int[5];
delete[] example;

这样我们同样创建了一个有五个int变量的数组,但是注意一点,使用new关键字一定要在使用完毕之后配合delete关键字释放这部分内存,不然的话会发生内存泄漏。

那么我们为什么要使用new关键字的动态内存分配?因为生存周期。如果我们写一个函数,这个函数返回一个数组,那么一定要用new关键字创建这个数组,不然的话在离开函数的时候,内部的所有局部变量都会失效,我们就找不到我们返回的这个数组了。

除了这个之外,两种创建数组的方法还有一个区别,在于是否会间接寻址。我们举一个例子来看看什么叫做间接寻址:

cpp 复制代码
#include<iostream>

class Entity {
public:
	int example[5];
public:
	Entity() {
		for (int i = 0; i < 5; i++) {
			example[i] = 2;
		}
	}
};

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

在下面这段代码中,我们在一个类当中定义了一个数组成员,然后我们进入到内存当中输入&e,也就是我们实例化的类的地址,然后看看能找到什么:

可以看到,这个地址直接就是这个数组本身的值,很好,接下来我们看看如果我们在类当中使用new关键字进行动态分配内存,再输入&e会找到什么:

cpp 复制代码
class Entity {
public:
	int* example = new int[5];
public:
	Entity() {
		for (int i = 0; i < 5; i++) {
			example[i] = 2;
		}
	}
};

我们会发现,这里居然储存的是一个地址,那我们再把这个地址输入进去查找一下:

这下我们可以看到了,原来我们的数组储存在这里了。也就是说,在类的地址,其实储存的是这个数组的指针,我们再找一层,才能找到这个数组的值。这个就叫做间接寻址,这样多次在内存中跳转会降低我们程序的运行速度,影响我们的性能。

最后我们来讨论一下数组的大小,这也是一个比较尴尬的地方,在C++当中,我们无法直接获取到数组有多长,也就是里面有多少个元素。如果我们使用sizeof函数,返回的是这个数组占了多少个字节。

cpp 复制代码
int example[5];
std::cout << sizeof(example) << std::endl;
std::cin.get();

这样的话我们会看到控制台窗口打印了20,也就是这个数组占了20个字节。如果想要打印出这个数组具体有多长,还需要除以这个数组类型的长度:

cpp 复制代码
std::cout << sizeof(example)/sizeof(int) << std::endl;

这样我们会看到输出了5。

如果是new建立在堆上面的数组,我们甚至无法用sizeof来判断它的字节数:

cpp 复制代码
int* example = new int[5];
std::cout << sizeof(example)/sizeof(int) << std::endl;

可以看到,输出结果变成了2,因为我们其实是在用int指针所占的字节数除以了int的字节数,无论这个数组多长,我们最后得到的结果都会是2。

但是实际上,我们可以意识到一个事情,就是delete的时候,编译器是需要知道到底要释放多少内存的,也就是编译器有一个判断方式,来判断这个数组到底占了多少内存。那么我们能不能使用编译器相同的方式来判断呢?其实是可以的,但是不同的编译器在处理这个问题的时候是不同的,所以如果我们真的这样做,那么换了编译器之后我们还得修改代码,不然会出现错误。所以这个方法理论上虽然可以,但是并不推荐。所以在C++当中,我们只能自己维护自己的数组长度。

还有一点需要注意的是,如果我们在栈上新建一个数组,那么在编译之前,就需要知道到底要用多少内存,也就是数组有多长,如下所示,第一种建立数组的方式就会报错,但是第二种在堆上建立的就可以正常编译通过。

cpp 复制代码
int a = 5;
int example[a];
int* example2 = new int[a];

所以如果想要在栈上建立数组,要不然我们直接用数字规定它的长度,要不然我们用一个加了constant关键字的变量。

cpp 复制代码
const int a = 5;
int example[a];

那么我们有没有办法,可以避免C++原始数组的这么多问题,来更加简易的建立数组,获取长度,且能够进行边界检查防止溢出呢?有的,那就是自带的std array,如下所示:

cpp 复制代码
#include<iostream>
#include<array>


int main() {
	std::array<int, 5> example;
	std::cout << example.size() << std::endl;
	std::cin.get();
}

这样我们可以直接获得array的大小,也会对我们的数组进行边界检查。当然这需要一定的开销。对于希望安全性为主的同学,可以使用这个自带的array类型,但是使用C++不就是为了风险和收益并存么?

相关推荐
Genevieve_xiao13 分钟前
【数模学习笔记】插值算法和拟合算法
笔记·学习·算法·数学建模
南宫生13 分钟前
力扣-数据结构-19【算法学习day.90】
java·数据结构·学习·算法·leetcode
网络安全成叔1 小时前
【入门级】计算机网络学习
学习·计算机网络·web安全·计算机·网络安全·编程
梳子烟YAN2 小时前
UML系列之Rational Rose笔记一:用例图
笔记·uml
wxszz104 小时前
25.1.10学习笔记(算法(滑动窗口))
笔记·学习
智驾4 小时前
SOLID原则学习,接口隔离原则
c++·接口隔离原则·solid
不是只有你能在乱世中成为大家的救世主5 小时前
学习第六十四行
linux·c语言·开发语言·经验分享·学习
JoneMaster6 小时前
[读书日志]从零开始学习Chisel 第十一篇:Scala的类型参数化(敏捷硬件开发语言Chisel与数字系统设计)
开发语言·学习·scala
power-辰南6 小时前
人工智能学习路线全链路解析
人工智能·学习·机器学习
Growthofnotes6 小时前
C++—14、C++ 中的指针最基础的原理
开发语言·c++