有人說指針是C語言的靈魂,也有人說沒學好指針就等於不會C語言。
雖然在現代C++中一般都是推薦盡量避免使用原生的raw指針,而是以smart pointer 和reference替代之。但是無論怎樣,對於C/C++來說,指針始終是個繞不過去的坎。究其原因,是因為C/C++都是支持面向底層操作的語言,而面向底層操作就得能操縱內存,這個時候就需要指針了。為什麼呢?個人覺得指針實際上就是對機器語言/ASM中的通過虛擬地址操作內存的這一行為的一種抽象。
例如
movl %eax, (%edx)
將寄存器eax中的值寫入內存地址為寄存器edx的值的內存中。如果把edx看做一個指針的話,也就相當於
*p_edx = value_eax
關於指針,有著許多技巧和用途,後文主要談談關於C++中指針比較操作的一些容易踏入的坑。
先來看看這段代碼
1 class BaseA 2 { 3 public: 4 int a; 5 }; 6 7 class BaseB 8 { 9 public: 10 double b; 11 }; 12 13 class Derived : public BaseA, public BaseB 14 { 15 }; 16 17 int main(int argc, char const *argv[]) 18 { 19 Derived derivd; 20 Derived* pd = &derivd; 21 BaseB* pb = &derivd; 22 printf("pb = %p\n", pb); 23 printf("pd = %p\n", pd); 24 if (pb == pd) 25 { 26 printf("pb == pd\n"); 27 } 28 else 29 { 30 printf("pb != pd\n"); 31 } 32 }
輸出的結果是:
pb = 0028FEE0
pd = 0028FED8
pb == pd
可以看到指針pb和pd值並不一樣,但是編譯器卻認為他們相等,為什麼呢?
1. 當2個指針的靜態類型以及所指對象的類型都屬於同一個繼承層次結構中,並且其中一個指針類型是所指對象的靜態類型的時候,指針的比較,實際上比較的是兩個指針是否指向同一個對象。
若2個指針指向同一個對象,被編譯器決議為相等。編譯器在比較的時候加上適當的offset值,例如上面的情況,相當於在比較的時候編譯器做了這樣的改動:
if((pb - sizeof(int) == pd)
offset是由C++對象的內存模型決定的,具體的就不再本文討論的范疇了。
若2個指針指向不同的對象,就被決議為不相等,並且比較的是指針保存的地址的值的大小。
1 int main(int argc, char const *argv[]) 2 { 3 Derived derived1; 4 Derived derived2; 5 Derived* pd = &derived1; 6 BaseB* pb = &derived2; 7 printf("%p\n", pd); 8 printf("%p\n", pb); 9 if (pd < pb) 10 { 11 printf("pd < pb\n"); 12 } 13 else if (pd == pb) 14 { 15 printf("pd == pb\n"); 16 } 17 else 18 { 19 printf("pd > pb\n"); 20 } 21 }
得到的結果為:
0028FED8
0028FED0
pd > pb
2. 當2個指針的靜態類型不屬於同一個繼承層次結構中,但是2個指針都指向同一個對象的時候,該比較是違法行為,編譯器會報編譯期錯誤
1 int main(int argc, char const *argv[]) 2 { 3 Derived derivd; 4 Derived* pd = &derivd; 5 int* pb = reinterpret_cast<int*>(&derivd); 6 printf("pb = %p\n", pb); 7 printf("pd = %p\n", pd); 8 if (pb == pd) 9 { 10 printf("pb == pd\n"); 11 } 12 else 13 { 14 printf("pb != pd\n"); 15 } 16 }
編譯器報錯為:
error: comparison between distinct pointer types 'int*' and 'Derived*' lacks a cast [-fpermissive]
if (pb == pd)
3. 當2個指針的靜態類型以及所指對象類型都屬於同一個繼承層次結構,但是2個指針的靜態類型都不是所指對象的類型時,該比較是違法行為,編譯器會報編譯期錯誤:
1 int main(int argc, char const *argv[]) 2 { 3 Derived derivd; 4 BaseB* pb = &derivd; 5 BaseA* pa = &derivd; 6 printf("pb = %p\n", pb); 7 printf("pd = %p\n", pa); 8 if (pb == pa) 9 { 10 printf("pb == pa\n"); 11 } 12 else 13 { 14 printf("pb != pa\n"); 15 } 16 }
編譯器報錯為:
error: comparison between distinct pointer types 'BaseB*' and 'BaseA*' lacks a cast
if (pb == pa)
另外一些其他的行為,例如2個指針的類型不同但同屬於一個繼承層次,然後通過強制類型轉換讓他們倆都指向一個不屬於該繼承層次的對象,這樣的行為都是為未定義行為,也許編譯器不會報編譯期錯誤,但結果是未定義的,可能是任何結果。
可能有人會說,什麼時候指針比較的是他們所保存的地址的值呢呢?
答案是當2個指針的靜態類型相同的時候:
1 int main(int argc, char const *argv[]) 2 { 3 Derived derived1; 4 Derived derived2; 5 Derived* p1 = &derived1; 6 Derived* p2 = &derived2; 7 if (p1 < p2) 8 { 9 printf("p1 < p2\n"); 10 } 11 else if (p1 == p2) 12 { 13 printf("p1 == p2\n"); 14 } 15 else 16 { 17 printf("p1 > p2\n"); 18 } 19 }
結果為:p1 > p2
boost::shared_ptr/std::shared_ptr中有一個owner_before成員函數,原型為
template <class U> bool owner_before (const shared_ptr<U>& x) const; template <class U> bool owner_before (const weak_ptr<U>& x) const;
當該shared_ptr和x的類型同屬一個繼承層次時,不管他們類型是否相同,他們兩都被決議為“相等”。當他們的類型不屬於同一繼承層次時,比較的為他們所管理指針的地址值的大小。
1 int main(int argc, char const *argv[]) 2 { 3 boost::shared_ptr<Derived> pd(new Derived); 4 boost::shared_ptr<BaseB> pb(pd); 5 printf("%p %p\n", pd.get(), pb.get()); 6 printf("%d %d\n", pd < pb, pb < pd); // 0 0 7 printf("%d %d\n", pd.owner_before(pb), pb.owner_before(pd)); // 0 0 8 boost::shared_ptr<void> p0(pd), p1(pb); 9 printf("%p %p\n", p0.get(), p1.get()); 10 printf("%d %d\n", p0.get() < p1.get(), p1.get() < p0.get()); // 1 0 11 printf("%d %d\n", p0.owner_before(p1), p1.owner_before(p0)); // 0 0 12 }
為什麼shared_ptr會提供這樣的成員函數呢?
因為一個智能指針有可能指向了另一個智能指針指向對象中的某一部分,但又要保證這兩個智能指針銷毀時,只對那個被指的對象完整地析構一次,而不是兩個指針分別析構一次。
在這種情況下,指針就可以分為兩種,一種是 stored pointer 它是指針本身的類型所表示的對象(可能是一個大對象中的一部分);另一種是 owned pointer 指向內存中的實際完整對象(這一個對象可能被許多智能指針指向了它裡面的不同部分,但最終只析構一次)。owner-based order 就是指後一種情況,如果內存中只有一個對象,然後被許多 shared pointer 指向了其中不同的部分,那麼這些指針本身的地址肯定是不同的,也就是operator<()可以比較它們,並且它們都不是對象的 owner,它們銷毀時不會析構對象。但它們都指向了一個對象,在owner-based order 意義下它們是相等的。
cpluscplus中是這樣解釋的:
Returns whether the object is considered to go before x following a strict weak owner-based order.
Unlike the operator< overload, this ordering takes into consideration the shared_ptr's owned pointer, and not the stored pointer in such a way that two of these objects are considered equivalent (i.e., this function returns false no matter the order of the operands) if they both share ownership, or they are both empty, even if their stored pointer value are different.
The stored pointer (i.e., the pointer the shared_ptr object dereferences to) may not be the owned pointer (i.e., the pointer deleted on object destruction) if the shared_ptr object is an alias (alias-constructed objects and their copies).
This function is called by owner_less to determine its result.
cppreference中是的解釋:
Checks whether this
shared_ptr
precedesother
in implementation defined owner-based (as opposed to value-based) order. The order is such that two smart pointers compare equivalent only if they are both empty or if they both own the same object, even if the values of the pointers obtained by get() are different (e.g. because they point at different subobjects within the same object)This ordering is used to make shared and weak pointers usable as keys in associative containers, typically through std::owner_less.
(完)