bug誕生記 隱蔽的指標偏移計算導致的資料錯亂

2021-08-28 13:18:24 字數 3949 閱讀 3455

class base 

int get_v_b() const

private:

int _v_b;

};class derived :

public base

int get_v_d() const

private:

int _v_d;

};

base是derived的基類,前者擁有成員變數_v_b,後者擁有前者的_v_b和自己定義的_v_d。

我們分別構建乙個base和derived物件陣列

base * build_base_list(size_t count) 

for (size_t i = 0; i < count; i++)

return b_list;

}derived * build_derived_list(size_t count)

for (size_t i = 0; i < count; i++)

return d_list;

}

我們再提供乙個方法,用於遍歷陣列中物件的_v_b(base基類定義的成員變數)。

void print_v_b(base *b_list, size_t b_list_count) 

for (size_t i = 0; i < b_list_count; i++)

}

然後我們對構建的兩個陣列分別呼叫print_v_b,以期望列印出各自的v_b。

const size_t count = 8;

std::unique_ptr > base_list(

build_base_list(count),

(base* p)

);std::cout << "base_list:" << std::endl;

print_v_b(base_list.get(), count);

std::unique_ptr > derived_list(

build_derived_list(count),

(derived* p)

);std::cout << "derived_list:" << std::endl;

print_v_b(derived_list.get(), count);

理論上,我們將看到兩組相同的結果。因為base_list和derived_list中每個元素的_v_b是其在陣列中的下標。然而結果是

base_list:

_v_b(0):0

_v_b(1):1

_v_b(2):2

_v_b(3):3

_v_b(4):4

_v_b(5):5

_v_b(6):6

_v_b(7):7

derived_list:

_v_b(0):0

_v_b(1):0

_v_b(2):1

_v_b(3):1

_v_b(4):2

_v_b(5):2

_v_b(6):3

_v_b(7):3

很明顯,derived_list陣列輸出的元素資訊不正確。

derived_list陣列中的每個元素都是base子類derived的物件。理論上,對derived物件,通過基類base的方法訪問,是可以獲得正確資料的。那問題出在**?我們還要回到print_v_b方法中

void print_v_b(base *b_list, size_t b_list_count) 

for (size_t i = 0; i < b_list_count; i++)

}

我們看到第7行是通過陣列下標的形式獲取每個元素的。在c語言中,如果乙個陣列通過下標訪問元素,其獲取的元素實際位址是head+index*sizeof(struct)。

我們分別看乙個int型和long long型陣列通過下標獲取元素的取址值

const size_t count = 8;

int integer_list[count];

std::cout << "head:" << integer_list << " sizeof(int):" << sizeof(int) << std::endl;

for (size_t i = 0; i < count; i++)

long long longlong_list[count];

std::cout << "head:" << integer_list << " sizeof(int):" << sizeof(long long) << std::endl;

for (size_t i = 0; i < count; i++)

可以看到,雖然每次下標只是自增1,但是位址實際增加了每個元素的大小。

head:0x7fffffffe900 sizeof(int):4

integer_list[0] address:0x7fffffffe900

integer_list[1] address:0x7fffffffe904

integer_list[2] address:0x7fffffffe908

integer_list[3] address:0x7fffffffe90c

integer_list[4] address:0x7fffffffe910

integer_list[5] address:0x7fffffffe914

integer_list[6] address:0x7fffffffe918

integer_list[7] address:0x7fffffffe91c

head:0x7fffffffe900 sizeof(int):8

longlong_list[0] address:0x7fffffffe9a0

longlong_list[1] address:0x7fffffffe9a8

longlong_list[2] address:0x7fffffffe9b0

longlong_list[3] address:0x7fffffffe9b8

longlong_list[4] address:0x7fffffffe9c0

longlong_list[5] address:0x7fffffffe9c8

longlong_list[6] address:0x7fffffffe9d0

longlong_list[7] address:0x7fffffffe9d8

在print_v_b陣列中,它預設認為陣列中每個元素大小是base物件的大小。然而derived_list陣列中每個元素的是derived物件大小。derived模擬base類多乙個元素_v_d,從而大小從base物件的4位元組變成了8位元組。這樣第7行中,每次下標移動實際只是移動了4位元組,於是每個奇數次移動均移動到derived物件的_v_d前,每個偶數次移動均移動到derived物件的_v_b前。這就出現了上面的資料錯亂的問題。

陣列是c的遺產。為了相容c,c++保留了很多c語言的印記,於是導致自身呈現出一些不清晰的表達。比如下面如下三種寫法

void print_t(t *t)

void print_t(t t)

void print_t(t & t)

第3種寫法,我們可以知道t是個物件。

第2種寫法,我們可以知道t表達了乙個陣列。

第1中寫法,則可以表達出t可以是乙個陣列,可以是乙個物件。那麼到底它是個組數還是物件?我們沒法從語法上得知。

像本例中,使用者很有可能會把print_v_b的第一元素當成乙個物件指標(當然第二個引數透露出其應該是乙個陣列,但是假如沒有第二個引數呢?),那麼他怎麼也不會想到,對derived_list呼叫print_v_b會出錯。

這從乙個側面可以說明,對於可以靈活表達的c++語言,我們需要採用一些易於理解的方式去設計api。

寫給那個茶水妹的《乘風破浪》誕生記

去年秋冬,one實驗室作者生活在 乘風破浪 劇組中間,見證了拍攝過程,本文講述的就是電影誕生的故事。這也是茶水妹和她的夥伴們,包括導演和主演們的故事。正是三百多個年輕人像候鳥一樣來到亭林鎮附近,帶著他們年輕的人生,發生了一場年輕的化學反應,才造就了 乘風破浪 2016年,上海,初秋,穿過城市的風帶著...

菜鳥類庫誕生記一 值型別的擴充套件

自從上次一篇部落格已經有兩個月了,因為一些事耽擱了 出發前說說我的個人感想 到上海之後參加了很多面試,不管從技術上還是專案經驗都遭遇到了一些打擊,所以決心打造屬於自己的框架作品。雖然我還是乙個菜鳥,不過我相信只要堅持就會實現我的目標。今天的內容會很簡單,只是想在部落格上做乙個簡單的記錄。此擴充套件方...

菜鳥類庫誕生記一 值型別的擴充套件

自從上次一篇部落格已經有兩個月了,因為一些事耽擱了 出發前說說我的個人感想 到上海之後參加了很多面試,不管從技術上還是專案經驗都遭遇到了一些打擊,所以決心打造屬於自己的框架作品。雖然我還是乙個菜鳥,不過我相信只要堅持就會實現我的目標。今天的內容會很簡單,只是想在部落格上做乙個簡單的記錄。此擴充套件方...