• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            陳碩的Blog

            C++ 工程實(shí)踐(8):值語義

            陳碩 (giantchen_AT_gmail)
            http://blog.csdn.net/Solstice  http://weibo.com/giantchen
            陳碩關(guān)于 C++ 工程實(shí)踐的系列文章: http://blog.csdn.net/Solstice/category/802325.aspx
            排版正常的版本: http://www.cnblogs.com/Solstice/category/287661.html
            陳碩博客文章合集下載: http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx
            本作品采用“Creative Commons 署名-非商業(yè)性使用-禁止演繹 3.0 Unported 許可協(xié)議(cc by-nc-nd)”進(jìn)行許可。http://creativecommons.org/licenses/by-nc-nd/3.0/

            本文是前一篇《C++ 工程實(shí)踐(7):iostream 的用途與局限》的后續(xù),在這篇文章的“iostream 與標(biāo)準(zhǔn)庫其他組件的交互”一節(jié),我簡單地提到iostream的對象和C++標(biāo)準(zhǔn)庫中的其他對象(主要是容器和string)具有不同的語義,主要體現(xiàn)在iostream不能拷貝或賦值。今天全面談一談我對這個(gè)問題的理解。

            本文的“對象”定義較為寬泛,a region of memory that has a type,在這個(gè)定義下,int、double、bool 變量都是對象。

            什么是值語義

            值語義(value sematics)指的是對象的拷貝與原對象無關(guān),就像拷貝 int 一樣。C++ 的內(nèi)置類型(bool/int/double/char)都是值語義,標(biāo)準(zhǔn)庫里的 complex<> 、pair<>、vector<>、map<>、string 等等類型也都是值語意,拷貝之后就與原對象脫離關(guān)系。Java 語言的 primitive types 也是值語義。

            與值語義對應(yīng)的是“對象語義/object sematics”,或者叫做引用語義(reference sematics),由于“引用”一詞在 C++ 里有特殊含義,所以我在本文中使用“對象語義”這個(gè)術(shù)語。對象語義指的是面向?qū)ο笠饬x下的對象,對象拷貝是禁止的。例如 muduo 里的 Thread 是對象語義,拷貝 Thread 是無意義的,也是被禁止的:因?yàn)?Thread 代表線程,拷貝一個(gè) Thread 對象并不能讓系統(tǒng)增加一個(gè)一模一樣的線程。

            同樣的道理,拷貝一個(gè) Employee 對象是沒有意義的,一個(gè)雇員不會(huì)變成兩個(gè)雇員,他也不會(huì)領(lǐng)兩份薪水。拷貝 TcpConnection 對象也沒有意義,系統(tǒng)里邊只有一個(gè) TCP 連接,拷貝 TcpConnection  對象不會(huì)讓我們擁有兩個(gè)連接。Printer 也是不能拷貝的,系統(tǒng)只連接了一個(gè)打印機(jī),拷貝 Printer 并不能憑空增加打印機(jī)。凡此總總,面向?qū)ο笠饬x下的“對象”是 non-copyable。

            Java 里邊的 class 對象都是對象語義/引用語義。ArrayList<Integer> a = new ArrayList<Integer>(); ArrayList<Integer> b = a; 那么 a 和 b 指向的是同一個(gè)ArrayList 對象,修改 a 同時(shí)也會(huì)影響 b。

            值語義與 immutable 無關(guān)。Java 有 value object 一說,按(PoEAA 486)的定義,它實(shí)際上是 immutable object,例如 String、Integer、BigInteger、joda.time.DateTime 等等(因?yàn)?Java 沒有辦法實(shí)現(xiàn)真正的值語義 class,只好用 immutable object 來模擬)。盡管 immutable object 有其自身的用處,但不是本文的主題。muduo 中的 Date、Timestamp 也都是 immutable 的。

            C++中的值語義對象也可以是 mutable,比如 complex<>、pair<>、vector<>、map<>、string 都是可以修改的。muduo 的 InetAddress 和 Buffer 都具有值語義,它們都是可以修改的。

            值語義的對象不一定是 POD,例如 string 就不是 POD,但它是值語義的。

            值語義的對象不一定小,例如 vector<int> 的元素可多可少,但它始終是值語義的。當(dāng)然,很多值語義的對象都是小的,例如complex<>、muduo::Date、muduo::Timestamp。

            值語義與生命期

            值語義的一個(gè)巨大好處是生命期管理很簡單,就跟 int 一樣——你不需要操心 int 的生命期。值語義的對象要么是 stack object,或者直接作為其他 object 的成員,因此我們不用擔(dān)心它的生命期(一個(gè)函數(shù)使用自己stack上的對象,一個(gè)成員函數(shù)使用自己的數(shù)據(jù)成員對象)。相反,對象語義的 object 由于不能拷貝,我們只能通過指針或引用來使用它。

            一旦使用指針和引用來操作對象,那么就要擔(dān)心所指的對象是否已被釋放,這一度是 C++ 程序 bug 的一大來源。此外,由于 C++ 只能通過指針或引用來獲得多態(tài)性,那么在C++里從事基于繼承和多態(tài)的面向?qū)ο缶幊逃衅浔举|(zhì)的困難——資源管理。

            考慮一個(gè)簡單的對象建模——家長與子女:a Parent has a Child, a Child knows his/her Parent。在 Java 里邊很好寫,不用擔(dān)心內(nèi)存泄漏,也不用擔(dān)心空懸指針:

            public class Parent
            {
                private Child myChild;
            }
                
            public class Child
            {
                private Parent myParent;
            }

            只要正確初始化 myChild 和 myParent,那么 Java 程序員就不用擔(dān)心出現(xiàn)訪問錯(cuò)誤。一個(gè) handle 是否有效,只需要判斷其是否 non null。

            在 C++ 里邊就要為資源管理費(fèi)一番腦筋:Parent 和 Child 都代表的是真人,肯定是不能拷貝的,因此具有對象語義。Parent 是直接持有 Child 嗎?抑或 Parent 和 Child 通過指針互指?Child 的生命期由 Parent 控制嗎?如果還有 ParentClub 和 School 兩個(gè) class,分別代表家長俱樂部和學(xué)校:ParentClub has many Parent(s),School has many Child(ren),那么如何保證它們始終持有有效的 Parent 對象和 Child 對象?何時(shí)才能安全地釋放 Parent 和 Child ?

            直接但是易錯(cuò)的寫法:

            class Child;
            
            class Parent : boost::noncopyable
            {
             private:
              Child* myChild;
            };
            
            class Child : boost::noncopyable
            {
             private:
              Parent* myParent;
            };
            

            如果直接使用指針作為成員,那么如何確保指針的有效性?如何防止出現(xiàn)空懸指針?Child 和 Parent 由誰負(fù)責(zé)釋放?在釋放某個(gè) Parent 對象的時(shí)候,如何確保程序中沒有指向它的指針?在釋放某個(gè) Child 對象的時(shí)候,如何確保程序中沒有指向它的指針?

            這一系列問題一度是C++面向?qū)ο缶幊填^疼的問題,不過現(xiàn)在有了 smart pointer,我們可以借助 smart pointer 把對象語義轉(zhuǎn)換為值語義,從而輕松解決對象生命期:讓 Parent 持有 Child 的 smart pointer,同時(shí)讓 Child 持有 Parent 的 smart pointer,這樣始終引用對方的時(shí)候就不用擔(dān)心出現(xiàn)空懸指針。當(dāng)然,其中一個(gè) smart pointer 應(yīng)該是 weak reference,否則會(huì)出現(xiàn)循環(huán)引用,導(dǎo)致內(nèi)存泄漏。到底哪一個(gè)是 weak reference,則取決于具體應(yīng)用場景。

            如果 Parent 擁有 Child,Child 的生命期由其 Parent 控制,Child 的生命期小于 Parent,那么代碼就比較簡單:

            class Parent;
            class Child : boost::noncopyable
            {
             public:
              explicit Child(Parent* myParent_)
                : myParent(myParent_)
              {
              }
            
             private:
              Parent* myParent;
            };
            
            class Parent : boost::noncopyable
            {
             public:
              Parent()
                : myChild(new Child(this))
              {
              }
            
             private:
              boost::scoped_ptr<Child> myChild;
            };

            在上面這個(gè)設(shè)計(jì)中,Child 的指針不能泄露給外界,否則仍然有可能出現(xiàn)空懸指針。

            如果 Parent 與 Child 的生命期相互獨(dú)立,就要麻煩一些:

            class Parent;
            typedef boost::shared_ptr<Parent> ParentPtr;
            
            class Child : boost::noncopyable
            {
             public:
              explicit Child(const ParentPtr& myParent_)
                : myParent(myParent_)
              {
              }
            
             private:
              boost::weak_ptr<Parent> myParent;
            };
            typedef boost::shared_ptr<Child> ChildPtr;
            
            
            class Parent : public boost::enable_shared_from_this<Parent>,
                           private boost::noncopyable
            {
             public:
              Parent()
              {
              }
            
              void addChild()
              {
                myChild.reset(new Child(shared_from_this()));
              }
            
             private:
              ChildPtr myChild;
            };
            
            int main()
            {
              ParentPtr p(new Parent);
              p->addChild();
            }
            

            上面這個(gè) shared_ptr+weak_ptr 的做法似乎有點(diǎn)小題大做。

            考慮一個(gè)稍微復(fù)雜一點(diǎn)的對象模型:a Child has parents: mom and dad; a Parent has one or more Child(ren); a Parent knows his/her spouser. 這個(gè)對象模型用 Java 表述一點(diǎn)都不復(fù)雜,垃圾收集會(huì)幫我們搞定對象生命期。

            public class Parent
            {
                private Parent mySpouser;
                private ArrayList<Child> myChildren;
            }
            
            public class Child
            {
                private Parent myMom;
                private Parent myDad;
            }

            如果用 C++ 來實(shí)現(xiàn),如何才能避免出現(xiàn)空懸指針,同時(shí)避免出現(xiàn)內(nèi)存泄漏呢?借助 shared_ptr 把裸指針轉(zhuǎn)換為值語義,我們就不用擔(dān)心這兩個(gè)問題了:

            class Parent;
            typedef boost::shared_ptr<Parent> ParentPtr;
            
            class Child : boost::noncopyable
            {
             public:
              explicit Child(const ParentPtr& myMom_,
                             const ParentPtr& myDad_)
                : myMom(myMom_),
                  myDad(myDad_)
              {
              }
            
             private:
              boost::weak_ptr<Parent> myMom;
              boost::weak_ptr<Parent> myDad;
            };
            typedef boost::shared_ptr<Child> ChildPtr;
            
            class Parent : boost::noncopyable
            {
             public:
              Parent()
              {
              }
            
              void setSpouser(const ParentPtr& spouser)
              {
                mySpouser = spouser;
              }
            
              void addChild(const ChildPtr& child)
              {
                myChildren.push_back(child);
              }
            
             private:
              boost::weak_ptr<Parent> mySpouser;
              std::vector<ChildPtr> myChildren;
            };
            
            int main()
            {
              ParentPtr mom(new Parent);
              ParentPtr dad(new Parent);
              mom->setSpouser(dad);
              dad->setSpouser(mom);
              {
                ChildPtr child(new Child(mom, dad));
                mom->addChild(child);
                dad->addChild(child);
              }
              {
                ChildPtr child(new Child(mom, dad));
                mom->addChild(child);
                dad->addChild(child);
              }
            }
            

            如果不使用 smart pointer,用 C++ 做面向?qū)ο缶幊虒?huì)困難重重。

            值語義與標(biāo)準(zhǔn)庫

            C++ 要求凡是能放入標(biāo)準(zhǔn)容器的類型必須具有值語義。準(zhǔn)確地說:type 必須是 SGIAssignable concept 的 model。但是,由 于C++ 編譯器會(huì)為 class 默認(rèn)提供 copy constructor 和 assignment operator,因此除非明確禁止,否則 class 總是可以作為標(biāo)準(zhǔn)庫的元素類型——盡管程序可以編譯通過,但是隱藏了資源管理方面的 bug。

            因此,在寫一個(gè) class 的時(shí)候,先讓它繼承 boost::noncopyable,幾乎總是正確的。

            在現(xiàn)代 C++ 中,一般不需要自己編寫 copy constructor 或 assignment operator,因?yàn)橹灰總€(gè)數(shù)據(jù)成員都具有值語義的話,編譯器自動(dòng)生成的 member-wise copying&assigning 就能正常工作;如果以 smart ptr 為成員來持有其他對象,那么就能自動(dòng)啟用或禁用 copying&assigning。例外:編寫 HashMap 這類底層庫時(shí)還是需要自己實(shí)現(xiàn) copy control。

            值語義與C++語言

            C++ 的 class 本質(zhì)上是值語義的,這才會(huì)出現(xiàn) object slicing 這種語言獨(dú)有的問題,也才會(huì)需要程序員注意 pass-by-value 和 pass-by-const-reference 的取舍。在其他面向?qū)ο缶幊陶Z言中,這都不需要費(fèi)腦筋。

            值語義是C++語言的三大約束之一,C++ 的設(shè)計(jì)初衷是讓用戶定義的類型(class)能像內(nèi)置類型(int)一樣工作,具有同等的地位。為此C++做了以下設(shè)計(jì)(妥協(xié)):

            • class 的 layout 與 C struct 一樣,沒有額外的開銷。定義一個(gè)“只包含一個(gè) int 成員的 class ”的對象開銷和定義一個(gè) int 一樣。
            • 甚至 class data member 都默認(rèn)是 uninitialized,因?yàn)楹瘮?shù)局部的 int 是 uninitialized。
            • class 可以在 stack 上創(chuàng)建,也可以在 heap 上創(chuàng)建。因?yàn)?int 可以是 stack variable。
            • class 的數(shù)組就是一個(gè)個(gè) class 對象挨著,沒有額外的 indirection。因?yàn)?int 數(shù)組就是這樣。
            • 編譯器會(huì)為 class 默認(rèn)生成 copy constructor 和 assignment operator。其他語言沒有 copy constructor 一說,也不允許重載 assignment operator。C++ 的對象默認(rèn)是可以拷貝的,這是一個(gè)尷尬的特性。
            • 當(dāng) class type 傳入函數(shù)時(shí),默認(rèn)是 make a copy (除非參數(shù)聲明為 reference)。因?yàn)榘?int 傳入函數(shù)時(shí)是 make a copy。
            • 當(dāng)函數(shù)返回一個(gè) class type 時(shí),只能通過 make a copy(C++ 不得不定義 RVO 來解決性能問題)。因?yàn)楹瘮?shù)返回 int 時(shí)是 make a copy。
            • 以 class type 為成員時(shí),數(shù)據(jù)成員是嵌入的。例如 pair<complex<double>, size_t> 的 layout 就是 complex<double> 挨著 size_t。

            這些設(shè)計(jì)帶來了性能上的好處,原因是 memory locality。比方說我們在 C++ 里定義 complex<double> class,array of complex<double>, vector<complex<double> >,它們的 layout 分別是:(re 和 im 分別是復(fù)數(shù)的實(shí)部和虛部。)

            value1

            而如果我們在 Java 里干同樣的事情,layout 大不一樣,memory locality 也差很多:

            value2

            Java 里邊每個(gè) object 都有 header,至少有兩個(gè) word 的開銷。對比 Java 和 C++,可見 C++ 的對象模型要緊湊得多。

            待續(xù)

            下一篇文章我會(huì)談與值語義緊密相關(guān)的數(shù)據(jù)抽象(data abstraction),解釋為什么它是與面向?qū)ο蟛⒘械囊环N編程范式,為什么支持面向?qū)ο蟮木幊陶Z言不一定支持?jǐn)?shù)據(jù)抽象。C++在最初的時(shí)候是以 data abstraction 為賣點(diǎn),不過隨著時(shí)間的流逝,現(xiàn)在似乎很多人只知 Object-Oriented,不知 data abstraction 了。C++ 的強(qiáng)大之處在于“抽象”不以性能損失為代價(jià),下一篇文章我們將看到具體例子。

            posted on 2011-08-16 21:13 陳碩 閱讀(2807) 評論(4)  編輯 收藏 引用

            評論

            # re: C++ 工程實(shí)踐(8):值語義 2011-08-17 08:51 nk_ysg

            學(xué)習(xí)下,lz這么久才更新。  回復(fù)  更多評論   

            # re: C++ 工程實(shí)踐(8):值語義 2011-08-17 12:17 江浸月

            樓主大牛啊,坐等樓主更新新文章。  回復(fù)  更多評論   

            # re: C++ 工程實(shí)踐(8):值語義[未登錄] 2011-08-24 12:11 Chipset

            shared_ptr, weak_ptr是廢物,除了降低速度和吃掉更多內(nèi)存,我沒有見到什么好處。C++的析構(gòu)函數(shù)可以以很小的代價(jià)避免很多資源管理方面的問題,但是為了遷就太多不熟悉C++的人不得不納入標(biāo)準(zhǔn)。

            就算為了尊重人權(quán)允許用垃圾收集,引用計(jì)數(shù)設(shè)計(jì)的垃圾收集跟保守的標(biāo)記清理似乎沒有發(fā)現(xiàn)什么優(yōu)勢,看看Boehm吧。

            把C++當(dāng)Java用可能是最傻的事情,可惜太多程序員正在這么干,專門喜歡垃圾用法。

            OO在歐美的上世紀(jì)七十年代就被看成雞肋,可惜亞太地區(qū)尤其中國很多程序員拿過來當(dāng)寶貝,加上腐朽的科技支持力度,導(dǎo)致了太多的悲哀,IT技術(shù)水平跟歐美距離越來越大,而且加速拉大...  回復(fù)  更多評論   

            # re: C++ 工程實(shí)踐(8):值語義 2011-08-31 08:40 Neuron Teckid

            看標(biāo)題還以為陳老師會(huì)談新標(biāo)準(zhǔn)中的 left value 和 rvalue reference. 不過有些東西還是很相通的, 比如對象復(fù)制構(gòu)造在 cpp 中被嚴(yán)重濫用了, 大部分對象不應(yīng)當(dāng)有 "復(fù)制" 這樣的操作, 更多的是 "轉(zhuǎn)移", 我認(rèn)為 c++0x 的 rvalue reference 正是為了修正這個(gè) cpp 一直以來的設(shè)計(jì)漏洞產(chǎn)生的. 包括后面標(biāo)準(zhǔn)庫中 vector 對成員的要求也從 assignable and copyable 變成了 (assignable and copyable) or movable.

            至于內(nèi)存管理, 這個(gè)比較虛, 如果真有非常離奇復(fù)雜的對象引用關(guān)系, 是不是考慮該修改設(shè)計(jì)本身了, 反正我個(gè)人經(jīng)歷接觸的對象模型多以樹形為主, 很少有 parent / child 這樣互搞的 :D  回復(fù)  更多評論   

            <2012年7月>
            24252627282930
            1234567
            891011121314
            15161718192021
            22232425262728
            2930311234

            導(dǎo)航

            統(tǒng)計(jì)

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            99久久精品午夜一区二区| 亚洲欧洲久久av| 久久精品国产精品亚洲毛片| 亚洲精品tv久久久久| 久久青青草视频| 色综合久久无码中文字幕| 亚洲va国产va天堂va久久| 久久精品国产亚洲AV嫖农村妇女| 久久久久人妻一区精品性色av| 久久A级毛片免费观看| 91精品国产色综合久久| 成人久久综合网| 精品久久久久一区二区三区| 国产精品美女久久久网AV| 国产巨作麻豆欧美亚洲综合久久| 久久电影网| 久久精品一本到99热免费| 久久青青草原精品国产| 国产精品嫩草影院久久| 2020国产成人久久精品| 欧美精品久久久久久久自慰| 亚洲国产成人久久综合碰碰动漫3d| 丰满少妇人妻久久久久久4| 香蕉久久夜色精品国产2020| 久久久女人与动物群交毛片 | 亚洲综合日韩久久成人AV| 亚洲香蕉网久久综合影视| 99久久精品国产高清一区二区 | 亚洲欧美国产日韩综合久久| 色偷偷偷久久伊人大杳蕉| 久久久久人妻一区精品果冻| 综合久久国产九一剧情麻豆| 久久精品国产黑森林| 久久久久人妻一区精品性色av| 久久久久亚洲AV成人网| 嫩草伊人久久精品少妇AV| 久久久久亚洲av毛片大| 91亚洲国产成人久久精品| 久久永久免费人妻精品下载| 伊人久久国产免费观看视频| 国产巨作麻豆欧美亚洲综合久久|