動態數組索引越界問題

1、在C++中,可以採用幾種不同的方法創建一個某種類型T的對象的數組。3種常用的方法如下:

#define N 10 //數組的長度N在編譯時已知
  T static_array[10];

  int n = 20; //數組的長度n是在運行時計算的
  T* dynamic_array = new T[n];

  std::vector<T> vector_array; //數組的長度可以在運行時進行修改

當然,我們仍然可以使用calloc()和malloc()函數,並且這樣的程序仍然能夠通過編譯並順利運行。但是,混合C和C++代碼並不是良好的編程思路,除非由於依賴遺留的C函數庫的原因而必須這樣做。不管我們用什麼方法分配數組,都可以用一個無符號整數作爲索引訪問數組中的元素。

const T& element_of_static_array = static_array[index];
const T& element_of_dynamic_array = dynamic_array[index];
const T& element_of_vector_array = vector_array[index];

如果我們提供一個大於或等於數組長度的索引值,會發生什麼情況呢?以上的代碼都會安靜的返回垃圾數據。如果我們決定在賦值符的左邊使用[]操作符,情況就會變得更糟。

some_array[index] = x;
取決於運氣,這個操作可能會重寫其他某個不相關的變量,一個其他數組的元素甚至是一條程序指令。在最後一種情況下,程序很可能會崩潰。每種錯誤都向惡意入侵者提供機會接管程序併產生不良後果。但是,std::vector提供了一個at(index)函數,它通過拋出一個out_of_range異常執行邊界檢查。它的問題在於如果我們想執行這種安全檢查,必須在訪問數組元素的每個地方都嚴格地使用at()函數。顯然,這種做法會降低代碼的效率。因此在完成了測試之後,我們可能想用速度更快的[]操作符在每處對它進行替換。但是,這種替換需要對代碼進行大量的修改,工作量很大,並且在此之後還要對代碼進行重新測試,因爲在這個乏味的過程中,很可能會不小心輸錯數據。

因此較之使用at()函數,使用以下的方法。儘管動態數組使[]操作符完全超出自己的控制,但STL的vector容器把它實現爲一個C++函數,我們可以根據自己的缺陷捕捉目標對它進行改寫,例如,重新定義[]操作符:

T& operator [](size_type index)
{
	SCPP_TEST_ASSERT(index < std::vector<T>::size(),
		"Index "<<index<<" must be less than "
		<<std::vector<T>::size());
	return std::vector<T>::operator[](index);
}

const T& operator [](size_type index) const
{
	SCPP_TEST_ASSERT(index < std::vector<T>::size(),
		"Index "<<index<<" must be less than "
		<<std::vector<T>::size());
	return std::vector<T>::operator[](index);
}

使用上面定義的文件中的代碼,舉例如下:

#include <iostream>
#include "scpp_vector.hpp"

using namespace std;

int main()
{
	scpp::vector<int> vect;
	for(int i=0; i<3; i++)
		vect.push_back(i);

	cout<<"My vector = "<<vect<<endl;

	for(int i=0; i<=vect.size(); i++)
		cout<<"Value of vector at "<<i<<" is "<<vect[i]<<endl;

	return 0;
}
首先,我們並沒有採用std::vector<int>或簡單的vector<int>這樣的寫法,而是採用了scpp::vector<int>的形式。這是爲了把我們的vector與STL的vector區分開來。通過使用scpp::vector,我們把標準實現替換爲我們自己的安全實現。scpp::vector還提供了一個<<操作符,因此只要vector不要太大,並且類型T定義了<<操作符,就可以用這個操作符打印vector。

在第二個for循環中,我們沒有使用i<vect.size()的寫法,而是寫成了i<=vect.size()。這是一個極爲常見的編程錯誤,我們只是爲了觀察當索引越界時會出現什麼情況,其實,程序的結果輸出爲:

My vector = 0 1 2
     Value of vector at 0 is 0
     Value of vector at 1 Value of vector at 2 is 2
     Index 3 must be less than 3 in file scpp_vector.hpp
只要SCPP_TEST_ASSERT_ON符號已被定義,這個安全檢查就會起作用,並可以方便的根據需要在編譯時打開或關閉這個檢查。這種方法的問題是vector的[]操作符在循環內部的使用極爲頻繁,因此這種安全檢查會被大量使用,因爲會顯著地降低程序的執行速度,就像使用at()函數一樣。如果覺得這種方法不適合自己的程序,可以定義一個新宏,例如SCPP_TEST_ASSERT_INDEX_OUT_OF_BOUNDS,它的工作方式與SCPP_TEST_ASSERT完全相同,但只在scpp::vector::operator[]內部使用。SCPP_TEST_ASSERT_OUT_OF_BOUNDS與SCPP_TEST_ASSERT的區別應該是它的打開或關閉與SCPP_TEST_ASSERT宏無關,因此如果確信代碼沒有缺陷,就可以使它處於未激活狀態,同時又讓其他的安全檢查繼續有效。

除了允許捕捉索引越界錯誤之外,vector模板較之靜態分配的數組和動態分配的數組還有一個優點,即它的長度可以根據需要增長(只要內存沒有耗盡)。但是,這個優點是要付出代價的。如果一個vector預先並未聲明需要多少內存,它就會分配一些默認內存(稱爲它的“容量”)。當這個vector的實際大小達到了這個容量時,它將分配一塊更大的內存,把舊數據複製到這塊新的內存區域,並用它替換舊的那塊內存。因此,隨着時間的推移,向vector模板添加一個新元素可能突然變得非常緩慢。因此,如果預先知道需要多少元素,應該像靜態數組和動態數組一樣,在構造函數中告訴vector需要多少內存:

scpp::vector<int> vect(n);
這樣就創建了一個具有指定元素數量的vector,其實我們還可以寫成如下:

scpp::vector<int> vect(n, 0);
它還把所有的元素初始化爲一個指定的值。

另一種替代方法是創建一個0個元素的vector,但是指定它所需要的容量。

scpp::vector<int> vect;
vect.reserve(n);

這個例子和前一個例子的區別在於,這個例子中的vector是空的(即vector.size()的返回值是0),但是當我們開始向它添加元素時,在它的長度達到n之前,不會出現導致速度降低的容量增長現象。




發佈了36 篇原創文章 · 獲贊 15 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章