数组你的命运在何方


 

目录:

    越界溢出的数组

    指针眼中的数组

    力求完美的数组

    总结——我心中的数组

    参考文献

 

题记——记得小时候去外婆家,总喜欢踏着那铺满青石的小路,一边欣赏着风景一边默数着自己的脚步。数的久了惊奇的发现,每次去外婆家的步数都有着惊人的相似,也许至此我们就与“同一”产生了不解的情缘。这种情结伴随着我们成长,教室中整齐划一的桌椅,同样的升学压力,同样是在青葱岁月中经历着寒窗苦读…

 

    对于学习计算机的人们来说,数组的概念似乎一点也不陌生。数组将同一类型的数据集合起来,并且密排分配在一段内存空间上供我们使用。数组概念的引入大大方便了我们对内存数据的操作,但传统数组的“丢”大小以及“丢”类型的问题(数组溢出、数组弱化为指针等)使得直接使用数组会产生一些错误。针对这些问题我们急需要一种可以在运行时限定边界,并且可以安全访问存储空间的数组。

 

一、越界溢出的数组

    数组越界是计算机初学者在编程时常犯的错误,对于一般人来说,说到数组越界的第一反应是:

 

int  a[13];            //定义一个元素大小为int,元素个数为13的数组

cout << a[13] << endl;   //输出数组的第14个元素,造成数组溢出

 

    这一反应是很令人奇怪的,为什么说到数组越界首先会想到a[13]而不会想到a[-1]呢?原因似乎来源于我们对生活经验的一种感知:当我们看到放有一杯满水杯子的桌子上出现大滩水渍时,首先会反应是水溢出而不会想到是杯底漏了。数组一旦越界我们就会由于获得一些意外数据而导致程序错误,那么既然如此编译器为什么允许这样的代码通过编译并且最终获得执行,例如:

 

int a[13] = {0};

cout<< a[13] << endl << a[-1] << endl;  //编译通过,可以执行

 

    事实上,对于普通的软件程序员来说,“a[-1]”这种形式应该被避免,因为在一些计算机的体系架构中,数组经常被分配在物理地址的边界,所以从内存分配的角度来说a[-1]、a[-2]…是没有任何意义的应该被避免;然而“a[13]”这种形式在许多算法中都有着重要的地位,例如在字符数组之间的拷贝:

 

template<class In, class Out>    

void copy(In from, In too_far, Out to)

{

          while(from !=too_far)

         {

                *to = *from;

                ++to;

                ++from;

         }

}

 

char c1[100];

char c2[200];

 

void fun()

{

          copy(&c1[0],&c1[100], &c2[0]);  //越界元素c1[100]

}

 

    既然“a[-1]”这种形式毫无意义,那么我们为什么要保留它呢?事实上,对于熟悉操作系统编程的人来说:在操作系统中,引导程序的设计是实现操作系统功能的重要组成部分之一,而操作系统的引导程序讲究的是“短小精悍”,这就要求引导程序在内存上的使用要计算到字节。所以,很有可能我们申请了一段内存顺序存放int i与int a[13]那么这样a[-1]事实上就是在访问整型变量i这样既方便又可以提高代码的执行效率。

   

二、指针眼中的数组

    当指针遇到数组,当看到数组那条长长的、笔直的“身躯”时,指针笑了,发出这般的豪言壮语:给我一个起点我可以走遍你的每一个角落。但事实真是如此么?然而当我们把数组名弱化为指针的时候,也许就要承担这种隐式转换所带来的后果,例如:

 

void fun(int i)

{

       Circle c[i];

       Shape *p = c;     //数组的弱化,派生类到基类的隐式转换

       c[7].draw();      //没有进行下标检查,可能导致溢出

       p[7].draw();      //错误的偏移量(灾难性的)

}

 

    在此我们假定Shape类为Circle类的基类,并且sizeof(Shape) < sizeof(Circle),这样当进行隐式转换的数组弱化时,导致p的指针类型发生变化,进而导致p[7]的偏移发生“错位”(与c[7]相比较),这样的错误是具有毁灭性的。即使我们不采用隐式转换也不能保证c[7]不会溢出。

    对于一维数组来说的确存在着这样与那样的问题,那么多维数组呢、是不是也存在这些问题呢?我们还是以指针为例:事实上对于多维数组,比如说二维数组int a[3][3]是可以用两个指针来进行索引的。一个指针索引数组“横向”每一个int型数据,另一个指针索引数组“纵向”每3个int型数组,我们姑且称横向的指针为“短指针”、纵向的为“长指针”,如图1所示:


 

图1

 

    也就是说一个二维数组可以通过两个一维指针来索引,那么当我们要索引a[2][2]的值时就有两种方案可以采用:一种是通过短指针索引、另一种是通过长指针来索引,两者都可以索引到a[2][2]的值只不过通过长指针索引速度要快一些。在此我们是否可以这样理解:在某些情况下,在达到相同目的的情形中长指针可以达到“快速收敛”的效果。此处对于维数的理解我引用一个例子如图2所示:我们对于多维空间的理解可以采用类比的方式,比如我们看待四维空间可以反向通过我们观察二维事物来获得。假设有一个二维人在拍“球”,在我们看来“球”就是平面上的一个圆,当我们往二维“球”中仍一个石子的时候在二维人看来是不可理解的,我的“球”没有坏怎么会无缘无故多一个石子呢。事实上就是因为我们比他要多一个维度,所以我们认为是理所当然的事情在他看来是不可理解的。


 

图2

 

    再回到数组我们是不是可以这样理解:对于一维数组所遇到的问题当我们提高维数,也就是说在多维数组中就可以解决了呢。事实上由图1的情形我们可以得知,多维数组只是在语言形式上模拟多维,而在语言实质上仍然可以看做是一维数组(无论是几维数组都是在同一块内存条上寻址,要想实质不同至少要使得实际物理存储介质的“存储形式”发生变化?),也就是说在一维数组中遇到的问题在多维数组中也会遇到(至此以下论述均以一维数组为例)。

 

三、力求完美的数组

    通过以上论述我们得知数组溢出在某些情况下有着其不可替代的作用,但是对于一般程序员来说数组溢出带来的麻烦要远远大于它的功用(研究操作系统构建的程序员毕竟少数,大部分仍面向应用软件的开发),所以我们急需要确定一种可以在运行时限定边界,并且可以安全访问存储空间的数组。针对这一问题在今年的芝加哥会议上给出了几种不同的解决方案。

 

1、  Dynarray

    这一方案由Lawrence Crowl与Matt Austern提出,他们定义了一种新的数组机制:数组的元素个数在数组构建时就已经被限定了。他们称之为“动态数组”(dynamic array,Dynarray)并希望将其写入到C++的标准库中,这样Dynarray在语法上可以仿照std::array与std::vector、在语义上可以仿照内置类型的数组(bulit-in array)这样动态数组中元素的类型就只是std::dynarray而不是标准数组中的那么多元素类型(double、char、int…)。

    依照Dynarray的声明我们的代码可能写成如下的形式:

 

dynarray <int>a(10);

f(a);

 

void f(dynarray<int>& a)

{

       a.~dynarray<int>();

       new(a) dynarray<int>(50);

}

 

    针对以上代码先不论它的书写复杂度,就从Dynarray使用容器这一点就是值得商榷的。“容器者,盛物也”,也就要求盛物的容器不仅要有可用性还要有适用性,比如说:用锅做饭用碗吃饭,如果说你非要用锅吃饭也不能说不行但你要用碗来做饭就显得有些困难了。也就是说采用容器实现的Dynarray是很难达到通用性的。其次将Dynarray作为标准库的一员就要求编译器要支持这一“关键性的优化”,这对编译器是一个很高的要求。

 

2、  Explicit Arrays

    数组可以弱化为指针的原因在于c到p可以进行隐式转换(Circle c[i]、Shape *p)采用“存储类别标识符(storage class specifier)”explicit可以禁止隐式转换避免这一问题,例如:

     

 void fun(int i)

 {

          Explicit Circle c[i];

          Shape*p = c;          //错误:explicit数组不允许弱化为指针

          Shape*q = &c[0]       //正确:指针之间的转化

                    

           p[7].draw();            //错误:p本身就不能被c转换

           q[7].draw();            //正确:但仍会产生偏移量的错误

}

   

    所以说explicit禁用隐式转换的想法是好的,但是如果不限制指针转化后的“下标”使用权,仍会出现偏移量错误的问题。此外每次定义数组都要带上explicit这一前缀显得过于“碍眼”。再者从通用性与兼容性的角度考虑,explicit数组应该可以和非explicit数组相互转化,然而:

 

const int i1 = 13;

             

void  fun(int i2)

{

       int a1[i1];

       int a2[i2];

       int *p = a1;

       p = a2;

      

       explicit int a3[i1];

       explicit int a4[i2];

       array_ref<int> q = a3;       // array_ref为explicit数组指针的类型

                                                    //选择array_ref原因为可以“见文知意”

       q = a4;

 

       p = q;                //错误,a1与a2为不同类型

       q = p;                //错误

}

 

3、Array Constructors

    在讨论会上J.Daniel Garcia建议:可以在“数组类”的构造函数中对ARB的大小进行限定(ARB:Array of Run-Time Bounds)。也就是说:

     

template <class T>

class bs_array

{

       bs_array(int n) : v[n], sz{n} {}

       …

       T& operator[](int i);

       const T& operator(int i)const;

       T& at(int i);                             //范围检查

       const T& at(int i) const;       //范围检查

 

       T* begin();

       const T* begin() const;

       T* end();

       const T* end() const;  

 

private:

       T v[];         //也可以写成T[] v;避免与empty array bound冲突

       int sz;

}

 

 

void fun(int n)

{

       bs_array as{n};                                     //栈上分配元素

       bs_array *p = new bs_array{n};         //堆上分配元素

       static bs_array sas {n};                       //静态数据区上分配元素

}

 

    Array Constructors的这种写法似乎非常适合我们的习惯,针对数组的存储区域Daveed建议将构造函数进行改造:

     

Struct MyArray

{

       …

       MyArray(int n) double storage[n];   //指明存放元素的存储区

       …

}

 

    这种做法虽然很直观,但是当一个类中含有多个数组时就会显得过于繁琐。再者将元素分配到何种存储区这件事交给编译器处理要比增加一种语法机制要简单的多。

 

四、总结——我心中的数组

    纵观数组的发展从传统的限长数组到VLA(C`s Variable-Length Arrays)、ARB(Array of Run-Time Bounds)再到std::array,最后到时至今日的讨论,数组每一步的发展都源于我们有一颗“追求完美”的心。承载着如此众多期待的数组又将何去何从?正如本文所述:提到“溢出”我们为什么想不到a[-1]、a[-2]呢?也许我们从小就习惯于从1开始向下数数(很少有老师教孩子倒着数),我们已经变得“先入为主”了。对于多维数组也许我们就约定俗成的认为低维的错误会出现在高维上(幸好在数组中这是对的)。

    所以说与其改变数组不如先从改变我们的习惯入手,比如说在访问数组时采用array.at(index)的方式而避免直接访问数组下标(除非你很有把握)。这样通过数组与我们的共同努力也许会发挥出数组的最大功用。说到这你也许会疑惑这似乎并不是你我所追求的答案,真相究竟是怎样的呢。也许每个人的心中都有一个真相,在我看来当某一天数组可以做到“同中求异”时(根据实际情况动态添加与它本身不同类型的数据),也许它就上升到了一个新的层次。

 

 

参考文献:

《The C++ Progamming Language》 Bjarne Stroustrup

《Alternatives for Array Extensions》 Bjarne Stroustrup

《Linux内核设计的艺术》 新设计团队


 

    

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值