《数据结构与算法之美》笔记 05 | 数组下标为何是从0开始?

数组下标为何是从0开始?

本节开头,作者用标题这个问题引入,让读者带着问题开始学习。这里直接给出答案,对于C语言来讲,数组使用下标索引时,是通过偏移地址去操作。
先来说下数组随机访问的实现方式,对于数组
a[i]_addr=base_addr + i * data_type_size
举个简单例子如下

int a[3]={0}
a[1]=1

如果下标从0开始,那么a_i_addr = a_0_addr+ik_byte, a_head_addr表示第一个元素地址,k_byte表示每个元素占用的字节数,a_i_addr表示第i+1个元素地址。
如果下标从1开始,那么a_i_addr = a_1_addr+(i-1)
k_byte, a_head_addr表示第一个元素地址,k_byte表示每个元素占用的字节数,a_i_addr表示第i个元素地址。
可以看出,每次通过下标去访问某个数组元素的时候,下标从1开始比从0开始要多出一次减法运算,对与基本数据类型来说,执行效率就低了。
而如C++、Java和Java Script语言也依然使用从0开始,除了相同的考虑外,应该还考虑到了降低C语言编程人员使用其他语言的学习成本。

如何实现随机访问?

数组是一种线性表数据结构,用一组连续的内存空间,来存储一组具有相同类型的数据。
1、线性表
线性表就是数据排成像一条线一样的结构,每个线性表上的数据最多只有前和后两个方向,除了数组,线性表结构还有链接、队列、栈。
而与之对立的概念就是非线性表,比如对,二叉树、图等,数据之间并不仅有前后关系。
2、连续的内存空间和相同类型的数据
因为这两个限制,数组有了看成“杀手锏”的特性:“随机访问”。弊端:插入和删除数据变得复杂,为了保证数据的连续性,需要做大量的数据搬移工作。

低效的“插入”和“删除”

如果是普通的数组插入和删除,为了保证数据的连续性,进行了大量的数据搬移,效率很低。但在某些特殊场景下,不追求数据的联系性,可以用更高效率的方法来实现插入和删除。
对于数组a[10], 如果已有5个元素,a,b,c,d,e。如果需要在a[3]位置插入 x,我们只需要将a[2]=x, a[5]=c,这就避免了其他数据的搬移。
对于删除,则是多次删除操作先记录,等到数组没有多余空间存储数据时,我们再触发执行一次真正的删除操作,这也就大大减少了删除操作带来的数据搬移。先标记再清除,也就是JVM标记清除垃圾回收算法的核心思想(没接触过jvm,以后有机会详细了解下)。

警惕数组访问越界

数组越界在C语言中是一种未决行为,并没有规定数组访问越界时编译器该如何处理。C语言把数组越界检查的工作交给了程序员,而java本身会做越界检查。

int main(){
    int i =0;
    int arr[3]={0};
    for(;i<=3;i++){
    arr[i]=0;
    printf("hello world\n");
    }
}

王老师拿以上程序来演示的效果是,"hello world"循环打印不停止,这是因为arr数组和i在存储时,因为要保持内存对齐,i正好存储在了arr的后面,紧跟在数组最后一个元素之后,这就造成数组越界访问了i,被赋值为0后,不断循环。
在本节文章评论中有这么一段话:

函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,由高向低增长。变量i和arr在相邻地址,且i比arr的地址大,所以arr越界正好访问到。当然,前提是i和arr元素同类型,否则那段代码仍是味觉行为。

又有一位评论如下:

例子中死循环问题跟编译器分配内存和字节对齐相关。数组3个元素,加上一个变量i,4个整数刚好能满足8字节对齐,所以i的地址恰好跟在a[2]后面,导致死循环。如果数组本身是4个元素,则这里不会出现死循环……因为编译器64位操作系统下默认会进行8字节对齐,变量i的地址,就不紧跟着数组后面了。

这两位评论的都是高手。自己动手跑了一下程序,出现如下结果:

$ ./a.out
hello world
hello world
hello world
hello world
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

查了下,是gcc的保护机制,具体可查博文stack smashing detected,程序段错误

posted @ 2020-08-14 17:45  飞吧小鸡  阅读(400)  评论(0编辑  收藏  举报