bug诞生记——隐蔽的指针偏移计算导致的数据错乱
? ? ? ? C++語言為了兼容C語言,做了很多設計方面的考量。但是有些兼容設計產生了不清晰的認識。本文就將討論一個因為認知不清晰而導致的bug。(轉載請指明出于breaksoftware的csdn博客)
class Base {
public:Base() = default;void set_v_b(int v_b) {_v_b = v_b;}int get_v_b() const {return _v_b;}
private:int _v_b;
};class Derived :public Base
{
public:Derived() = default;void set_v_d(int v_d) {_v_d = v_d;}int get_v_d() const {return _v_d;}
private:int _v_d;
};
? ? ? ? Base是Derived的基類,前者擁有成員變量_v_b,后者擁有前者的_v_b和自己定義的_v_d。
? ? ? ? 我們分別構建一個Base和Derived對象數組
Base * build_base_list(size_t count) {Base *b_list = new (std::nothrow) Base[count];if (!b_list) {return nullptr;}for (size_t i = 0; i < count; i++) {b_list[i].set_v_b(static_cast<int>(i));}return b_list;
}Derived * build_derived_list(size_t count) {Derived *d_list = new (std::nothrow) Derived[count];if (!d_list) {return nullptr;}for (size_t i = 0; i < count; i++) {d_list[i].set_v_b(static_cast<int>(i));d_list[i].set_v_d(static_cast<int>(i));}return d_list;
}
? ? ? ? 我們再提供一個方法,用于遍歷數組中對象的_v_b(Base基類定義的成員變量)。
void print_v_b(Base *b_list, size_t b_list_count) {if (!b_list) {return;}for (size_t i = 0; i < b_list_count; i++) {std::cout << "_v_b(" << i << "):" << b_list[i].get_v_b() << std::endl;}
}
? ? ? ? 然后我們對構建的兩個數組分別調用print_v_b,以期望打印出各自的v_b。
const size_t count = 8;std::unique_ptr <Base, std::function<void(Base*)>> base_list(build_base_list(count),[](Base* p) {delete [] p;});std::cout << "base_list:" << std::endl;print_v_b(base_list.get(), count);std::unique_ptr <Derived, std::function<void(Derived*)>> derived_list(build_derived_list(count),[](Derived* p) {delete [] 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) {if (!b_list) {return;}for (size_t i = 0; i < b_list_count; i++) {std::cout << "_v_b(" << i << "):" << b_list[i].get_v_b() << std::endl;}
}
? ? ? ? 我們看到第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++) {std::cout << "integer_list[" << i << "] address:" << &integer_list[i] << std::endl;}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++) {std::cout << "longlong_list[" << i << "] address:" << &longlong_list[i] << std::endl;}
? ? ? ? 可以看到,雖然每次下標只是自增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。
總結
以上是生活随笔為你收集整理的bug诞生记——隐蔽的指针偏移计算导致的数据错乱的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一套使用注入和Hook技术托管入口函数的
- 下一篇: C++拾趣——STL容器的插入、删除、遍