• <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>

            loop_in_codes

            低調做技術__歡迎移步我的獨立博客 codemaro.com 微博 kevinlynx

            #

            使用Clang實現C語言編程規范檢查

            概述

            Clang是LLVM編譯器工具集的前端部分,也就是涵蓋詞法分析、語法語義分析的部分。而LLVM是Apple在Mac OS上用于替代GCC工具集的編譯器軟件集合。Clang支持類C語言的語言,例如C、C++、Objective C。Clang的與眾不同在于其模塊化的設計,使其不僅實現編譯器前端部分,并且包裝成庫的形式提供給上層應用。使用Clang可以做諸如語法高亮、語法檢查、編程規范檢查方面的工作,當然也可以作為你自己的編譯器前端。

            編程規范一般包含編碼格式和語義規范兩部分。編碼格式用于約定代碼的排版、符號命名等;而語義規范則用于約定諸如類型匹配、表達式復雜度等,例如不允許對常數做邏輯運算、檢查變量使用前是否被賦值等。本文描述的主要是基于語義方面的檢查,其經驗來自于最近做的一個檢查工具,該工具實現了超過130條的規范。這份規范部分規則來自于MISRA C

            編程模式

            編譯器前端部分主要是輸出代碼對應的抽象語法樹(AST)。Clang提供給上層的接口也主要是圍繞語法樹來做操作。通過google一些Clang的資料,你可能會如我當初一樣對該如何正確地使用Clang心存疑惑。我最后使用的方式是基于RecursiveASTVisitor。這是一種類似回調的使用機制,通過提供特定語法樹節點的接口,Clang在遍歷語法樹的時候,在遇到該節點時,就會調用到上層代碼。不能說這是最好的方式,但起碼它可以工作?;赗ecursiveASTVisitor使用Clang,程序主體框架大致為:

            // 編寫你感興趣的語法樹節點訪問接口,例如該例子中提供了函數調用語句和goto語句的節點訪問接口
            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                bool VisitCallExpr(CallExpr *expr);
            
                bool VisitGotoStmt(GotoStmt *stmt);
                ...
            };
            
            class MyASTConsumer : public ASTConsumer {
            public:
                virtual bool HandleTopLevelDecl(DeclGroupRef DR) {
                    for (DeclGroupRef::iterator b = DR.begin(), e = DR.end(); b != e; ++b) {
                        Visitor.TraverseDecl(*b);
                    }
                    return true;
                } 
                
            private:
                MyASTVisitor Visitor;
            };
            
            int main(int argc, char **argv) {
                CompilerInstance inst;
                Rewriter writer;
                inst.createFileManager();
                inst.createSourceManager(inst.getFileManager());
                inst.createPreprocessor();
                inst.createASTContext();
                writer.setSourceMgr(inst.getSourceManager(), inst.getLangOpts());
                ... // 其他初始化CompilerInstance的代碼
              
                const FileEntry *fileIn = fileMgr.getFile(argv[1]);
                sourceMgr.createMainFileID(fileIn);
                inst.getDiagnosticClient().BeginSourceFile(inst.getLangOpts(), &inst.getPreprocessor());
                MyASTConsumer consumer(writer);
                ParseAST(inst.getPreprocessor(), &consumer, inst.getASTContext());
                inst.getDiagnosticClient().EndSourceFile();
                return 0;
            }
            

            以上代碼中,ParseAST為Clang開始分析代碼的主入口,其中提供了一個ASTConsumer。每次分析到一個頂層定義時(Top level decl)就會回調MyASTConsumer::HandleTopLevelDecl,該函數的實現里調用MyASTVisitor開始遞歸訪問該節點。這里的decl實際上包含定義。

            這里使用Clang的方式來源于Basic source-to-source transformation with Clang。

            語法樹

            Clang中視所有代碼單元為語句(statement),Clang中使用類Stmt來代表statement。Clang構造出來的語法樹,其節點類型就是Stmt。針對不同類型的語句,Clang有對應的Stmt子類,例如GotoStmt。Clang中的表達式也被視為語句,Clang使用Expr類來表示表達式,而Expr本身就派生于Stmt。

            每個語法樹節點都會有一個子節點列表,在Clang中一般可以使用如下語句遍歷一個節點的子節點:

            for (Stmt::child_iterator it = stmt->child_begin(); it != stmt->child_end(); ++it) {
                Stmt *child = *it;
            }
            

            但遺憾的是,無法從一個語法樹節點獲取其父節點,這將給我們的規范檢測工具的實現帶來一些麻煩。

            TraverseXXXStmt

            在自己實現的Visitor中(例如MyASTVisitor),除了可以提供VisitXXXStmt系列接口去訪問某類型的語法樹節點外,還可以提供TraverseXXXStmt系列接口。Traverse系列的接口包裝對應的Visit接口,即他們的關系大致為:

            bool TraverseGotoStmt(GotoStmt *s) {
                VisitGotoStmt(s);
                return true;
            }
            

            例如對于GotoStmt節點而言,Clang會先調用TraverseGotoStmt,在TraverseGotoStmt的實現中才會調用VisitGotoStmt。利用Traverse和Visit之間的調用關系,我們可以解決一些因為不能訪問某節點父節點而出現的問題。例如,我們需要限制逗號表達式的使用,在任何地方一旦檢測到逗號表達式的出現,都給予警告,除非這個逗號表達式出現在for語句中,例如:

            a = (a = 1, b = 2); /* 違反規范,非法 */
            for (a = 1, b = 2; a < 2; ++a) /* 合法 */
            

            逗號表達式對應的訪問接口為VisitBinComma,所以我們只需要提供該接口的實現即可:

            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                ...
                bool VisitBinComma(BinaryOperator *stmt) {
                    /* 報告錯誤 */
                    return true;
                }
                ...
            };
            

            (注:BinaryOperator用于表示二目運算表達式,例如a + b,逗號表達式也是二目表達式)

            但在循環中出現的逗號表達式也會調用到VisitBinComma。為了有效區分該逗號表達式是否出現在for語句中,我們可以期望獲取該逗號表達式的父節點,并檢查該父節點是否為for語句。但Clang并沒有提供這樣的能力,我想很大一部分原因在于臆測語法樹(抽象語法樹)節點的組織結構(父節點、兄弟節點)本身就不是一個確定的事。

            這里的解決辦法是通過提供TraverseForStmt,以在進入for語句前得到一個標識:

            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                ...
                // 這個函數的實現可以參考RecursiveASTVisitor的默認實現,我們唯一要做的就是在for語句的頭那設定一個標志m_inForLine
                bool TraverseForStmt(ForStmt *s) {
                    if (!WalkUpFromForStmt(s))
                        return false;
                    m_inForLine = true;
                    for (Stmt::child_range range = s->children(); range; ++range) {
                        if (*range == s->getBody())
                            m_inForLine = false;
                        TraverseStmt(*range);
                    }
                    return true;
                }
            
                bool VisitBinComma(BinaryOperator *stmt) {
                    if (!m_inForLine) {
                        /* 報告錯誤 */
                    }
                    return true;
                }
                ...
            };
            

            (注:嚴格來說,我們必須檢查逗號表達式是出現在for語句的頭中,而不包括for語句循環體)

            類型信息

            對于表達式(Expr)而言,都有一個類型信息。Clang直接用于表示類型的類是QualType,實際上這個類只是一個接口包裝。這些類型信息可以用于很多類型相關的編程規范檢查。例如不允許定義超過2級的指針(例如int ***p):

            bool MyASTVisitor::VisitVarDecl(VarDecl *decl) { // 當發現變量定義時該接口被調用
                QualType t = decl->getType(); // 取得該變量的類型
                int pdepth = 0;
                // check pointer level
                for ( ; t->isPointerType(); t = t->getPointeeType()) { // 如果是指針類型就獲取其指向類型(PointeeType)
                    ++pdepth;
                }
                if (pdepth >= 3)
                    /* 報告錯誤 */
            }
            

            可以直接調用Expr::getType接口,用于獲取指定表達式最終的類型,基于此我們可以檢查復雜表達式中的類型轉換,例如:

            float f = 2.0f;
            double d = 1.0;
            f = d * f; /* 檢查此表達式 */
            

            對以上表達式的檢查有很多方法,你可以實現MyASTVisitor::VisitBinaryOperator(只要是二目運算符都會調用),或者MyASTVisitor::VisitBinAssign(賦值運算=調用)。無論哪種方式,我們都可以提供一個遞歸檢查兩個表達式類型是否相同的接口:

            bool HasDiffType(BinaryOperator *stmt) {
                Expr *lhs = stmt->getLHS()->IgnoreImpCasts(); // 忽略隱式轉換
                Expr *rhs = stmt->getRHS()->IgnoreImpCasts();
                if (lhs->getType() == rhs->getType())) {
                    if (isa<BinaryOperator>(lhs) && HasDiffType(cast<BinaryOperator>(lhs)))
                        return true;
                    if (isa<BinaryOperator>(rhs) && HasDiffType(cast<BinaryOperator>(rhs)))
                        return true;
                    return false;
                }
                return true;
            }
            

            (注:此函數只是簡單實現,未考慮類型修飾符之類的問題)

            該函數獲得二目運算表達式的兩個子表達式,然后遞歸檢測這兩個表達式的類型是否相同。

            Expr類提供了更多方便的類型相關的接口,例如判定該表達式是否為常數,是否是布爾表達式,甚至在某些情況下可以直接計算得到值。例如我們可以檢查明顯的死循環:

            while (1) { }
            

            可以使用:

            ASTContext &context = inst.GetASTContext();
            bool result;
            // 假設stmt為WhileStmt
            if (stmt->getCond()->EvaluateAsBooleanCondition(result, context)) {
                if (result) 
                    /* 死循環 */
            

            符號表

            符號表這個概念比較廣義,這里我僅指的是用于保存類型和變量信息的表。Clang中沒有顯示的符號表數據結構,但每一個定義都有一個DeclContext,DeclContext用于描述一個定義的上下文環境。有一個特殊的DeclContext被稱為translation unit decl,其實也就是全局環境。利用這個translation unit decl,我們可以獲取一些全局符號,例如全局變量、全局類型:

            // 獲取全局作用域里指定名字的符號列表
            DeclContext::lookup_result GetGlobalDecl(const std::string &name) {
                ASTContext &context = CompilerInst::getSingleton().GetASTContext();
                DeclContext *tcxt = context.getTranslationUnitDecl();
                IdentifierInfo &id = context.Idents.get(name);
                return tcxt->lookup(DeclarationName(&id));
            }
            
            // 可以根據GetGlobalDecl的返回結果,檢查該列表里是否有特定的定義,例如函數定義、類型定義等
            bool HasSpecDecl(DeclContext::lookup_result ret, Decl::Kind kind) {
                for (size_t i = 0; i < ret.size(); ++i) {
                    NamedDecl *decl = ret[i];
                    if (decl->getKind() == kind) {
                        return true;
                    }
                }
                return false;
            }
            

            有了以上兩個函數,我們要檢測全局作用域里是否有名為”var”的變量定義,就可以:

            HasSpecDecl(GetGlobalDecl("var"), Decl::Var);
            

            每一個Decl都有對應的DeclContext,要檢查相同作用域是否包含相同名字的符號,其處理方式和全局的方式有點不一樣:

            // 檢查在ctx中是否有與decl同名的符號定義
            bool HasSymbolInContext(const NamedDecl *decl, const DeclContext *ctx) {
                for (DeclContext::decl_iterator it = ctx->decls_begin(); it != ctx->decls_end(); ++it) {
                    Decl *d = *it;
                    if (d != decl && isa<NamedDecl>(d) && 
                        cast<NamedDecl>(d)->getNameAsString() == decl->getNameAsString())
                        return true;
                }
                return false;
            }
            
            bool HasSymbolInContext(const NamedDecl *decl) {
                return HasSymbolInContext(decl, decl->getDeclContext());
            }
            

            可以看出,這里檢查相同作用域的方式是遍歷上下文環境中的所有符號,但對于全局作用域卻是直接查找。對于DeclContext的詳細信息我也不甚明了,只能算湊合使用。實際上,這里使用“作用域”一詞并不準確,在C語言中的作用域概念,和這里的context概念在Clang中并非等同。

            如果要檢查嵌套作用域里不能定義相同名字的變量,例如:

            int var;
            {
                int var;
            }
            

            通過Clang現有的API是無法實現的。因為Clang給上層的語法樹結構中,并不包含作用域信息(在Clang的實現中,用于語義分析的類Sema實際上有作用域的處理)。當然,為了實現這個檢測,我們可以手動構建作用域信息(通過TraverseCompoundStmt)。

            宏的處理屬于預處理階段,并不涵蓋在語法分析階段,所以通過Clang的語法樹相關接口是無法處理的。跟宏相關的接口,都是通過Clang的Preprocessor相關接口。Clang為此提供了相應的處理機制,上層需要往Preprocessor對象中添加回調對象,例如:

            class MyPPCallback : public PPCallbacks {
            public:
                // 處理#include
                virtual void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
                    StringRef FileName, bool IsAngled, CharSourceRange FilenameRange,
                    const FileEntry *File, StringRef SearchPath, StringRef RelativePath, const Module *Imported) {
                }
            
                // 處理#define
                virtual void MacroDefined(const Token &MacroNameTok, const MacroInfo *MI) {
                }
            
                virtual void MacroUndefined(const Token &MacroNameTok, const MacroInfo *MI) {
                } 
            }
            
            inst.getPreprocessor().addPPCallbacks(new MyPPCallback());
            

            即,通過實現PPCallbacks中對應的接口,就可以獲得處理宏的通知。

            Clang使用MacroInfo去表示一個宏。MacroInfo將宏體以一堆token來保存,例如我們要檢測宏體中使用###的情況,則只能遍歷這些tokens:

            // 分別記錄#和##在宏體中使用的數量
            int hash = 0, hashhash = 0;
            for (MacroInfo::tokens_iterator it = MI->tokens_begin(); it != MI->tokens_end(); ++it) {
                const Token &token = *it;
                hash += (token.getKind() == tok::hash ? 1 : 0);
                hashhash += (token.getKind() == tok::hashhash ? 1 : 0);
            }
            

            其他

            在我們所支持的編程規范中,有些規范是難以支持的,因此我使用了一些蹩腳的方式來實現。

            手工解析

            在針對函數的參數定義方面,我們支持的規范要求不能定義參數為空的函數,如果該函數沒有參數,則必須以void顯示標識,例如:

            int func(); /* 非法 */
            int func(void); /* 合法 */
            

            對于Clang而言,函數定義(或聲明)使用的是FunctionDecl,而Clang記錄的信息僅包括該函數是否有參數,參數個數是多少,并不記錄當其參數個數為0時是否使用void來聲明(記錄下來沒多大意義)。解決這個問題的辦法,可以通過SourceLocation獲取到對應源代碼中的文本內容,然后對此文本內容做手工分析即可。

            (注:SourceLocation是Clang中用于表示源代碼位置的類,包括行號和列號,所有Stmt都會包含此信息)

            通過SourceLocation獲取對應源碼的內容:

            std::pair<FileID, unsigned> locInfo = SM.getDecomposedLoc(loc);
            bool invalidTemp = false;
            llvm::StringRef file = SM.getBufferData(locInfo.first, &invalidTemp);
            if (invalidTemp)
                return false;
            // tokenBegin即為loc對應源碼內容的起始點
            const char *tokenBegin = file.data() + locInfo.second;
            

            要手工分析這些內容實際上還是有點繁雜,為此我們可以直接使用Clang中詞法分析相關的組件來完成這件事:

            Lexer *lexer = new Lexer(SM.getLocForStartOfFile(locInfo.first), opts, file.begin(), tokenBegin, file.end());
            Token tok;
            lexer->Lex(tok); // 取得第一個tok,反復調用可以獲取一段token流
            

            Diagnostic

            Clang中用Diagnostic來進行編譯錯誤的提示。每一個編譯錯誤(警告、建議等)都會有一段文字描述,這些文字描述為了支持多國語言,使用了一種ID的表示方法??傊?,對于一個特定的編譯錯誤提示而言,其diagnostic ID是固定的。

            在我們的規范中,有些規范檢測的代碼在Clang中會直接編譯出錯,例如函數調用傳遞的參數個數不等于函數定義時的形參個數。當Clang編譯出錯時,其語法樹實際上是不完善的。解決此問題的最簡單辦法,就是通過diagnostic實現。也就是說,我是通過將我們的特定規范映射到特定的diagnostic,當發生這個特定的編譯錯誤時,就可以認定該規范實際上被檢測到。對于簡單的情況而言,這樣的手段還算奏效。

            // `TextDiagnosticPrinter`可以將錯誤信息打印在控制臺上,為了調試方便我從它派生而來
            class MyDiagnosticConsumer : public TextDiagnosticPrinter {
            public:
                // 當一個錯誤發生時,會調用此函數,我會在這個函數里通過Info.getID()取得Diagnostic ID,然后對應地取出規范ID
                virtual void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
                    const Diagnostic &Info) {
                    TextDiagnosticPrinter::HandleDiagnostic(DiagLevel, Info);
                    // 例如檢查三字母詞(trigraph)的使用
                    if (Info.getID() == 816)
                        /* 報告使用了三字母詞 */
                }
            };
            
            // 初始化時需傳入自己定義的diagnostic
            inst.createDiagnostics(0, NULL, new MyDiagnosticConsumer(&inst.getDiagnosticOpts()));
            

            該例子代碼演示了對三字母詞(wiki trigraph)使用限制的規范檢測。

            全文完。

            posted @ 2013-02-12 21:53 Kevin Lynx 閱讀(10215) | 評論 (0)編輯 收藏

            C++陷阱:構造函數中的多態

            C++中主要是通過給函數加上virtual關鍵字來實現多態。多態可用于改變一個接口的實現,也算是一種嵌入應用層代碼到底層的實現手段。就算你用不到C++那些復雜的技術,多態肯定會被用到。

            但加上virtual不一定能保證多態成功:

            #include <stdio.h>
            
            class Base {
            public:
                Base() {
                    Init();
                }
            
                virtual ~Base() {
                    Release();
                }
            
                virtual void Init() {
                    printf("Base::Init\n");
                }
            
                virtual void Release() {
                    printf("Base::Release\n");
                }
            };
            
            class Derived : public Base {
            public:
                virtual void Init() {
                    printf("Derived::Init\n");
                }
            
                virtual void Release() {
                    printf("Derived:Release\n");
                }
            };
            
            int main()
            {
                Base *obj = new Derived();
                delete obj;
                return 0;
            }
            

            當在構造函數,包括析構函數中調用virtual函數時,預想中的多態是無法完成的,以上代碼輸出結果為:

            Base::Init
            Base::Release
            

            從語言設計角度來看,我個人是不接受這種行為的。我覺得對一門語言而言,幾乎所有特性都應該是一致的,不應該或盡量少地出現這種“例外“。如果我構造一個對象,讓它以不同的方式被構造,這和改變它的某個行為有什么區別?(從這句話來看,似乎還真有區別)

            當然,從語言實現來看,這樣的運行結果又似乎是必然的。因為,基類的構造是早于派生類的(作為其一部分),只有當構造完派生類后,其用于支持多態的虛表才會被正確構造。也就是說,在基類中調用虛函數時,既然虛表都為正確構造,自然調用的不會是派生類的虛函數了。析構函數按照析構的順序來看,也會面臨同樣的情況。

            posted @ 2012-09-17 16:30 Kevin Lynx 閱讀(3230) | 評論 (0)編輯 收藏

            C++陷阱:virtual析構函數

            有一天有個同事在通過vld調試一個內存泄漏問題,折騰了很久然后找到我。我瞥了一眼他的代碼,發現問題和我曾經遇到的一模一樣:

            class Base {
            public:
                ~Base();
            };
            
            class Derived : public Base {
            privated:
                std::vector<int> m_data;    
            }; Base *obj = new Derived(); delete obj;

            當然,實際代碼比這個復雜得多(這也是導致從發現問題到找到問題耗費大量時間的原因)。vld在報內存泄漏時,當然報的位置是new的地方。這個同事檢查了這個對象的整個生命周期,確定他正確地釋放了這個對象。

            問題的關鍵就在于:Base類的析構函數不是virtual。因為不是virtual,所以在對一個Base類型的指針進行delete時,就不會調用到派生類Derived的析構函數。而派生類里的析構函數會用于析構其內部的子對象,也就是這里的m_data。這樣,就造成了內存泄漏。

            這其實是一個很低級的失誤。但毫不客氣地說C++中有很多這種少個關鍵字或者代碼位置不對就會造成另一個結果的例子。事實上,針對這些悲劇也有很多書提出一些準則來讓大家去無腦遵守。例如針對這個例子,我就記得曾有書說,只要你覺得你的類會被繼承,那么最好給析構函數加上virtual。

            posted @ 2012-09-13 17:31 Kevin Lynx 閱讀(4537) | 評論 (8)編輯 收藏

            C/c++中幾種操作位的方法

            參考How do you set, clear and toggle a single bit in C?

            c/c++中對二進制位的操作包括設置某位為1、清除某位(置為0)、開關某位(toggling a bit)、檢查某位是否為1等。這些操作較為常見并且可以作為其他位運算的基礎接口,以下羅列幾種方法:

            傳統方法

            • 設置某位為1
            number |= 1 << x; // 設置第x位為1
            
            • 清除某位
            number &= ~(1 << x); // 置第x位為0
            
            • 開關某位
            number ^= 1 << x;
            
            • 檢查某位
            if (number & (1 << x))
            

            相應地我們可以將其封裝起來,簡便的方法是使用宏來封裝:

            #define BIT_SET(a,b) ((a) |= (1<<(b)))
            #define BIT_CLEAR(a,b) ((a) &= ~(1<<(b)))
            #define BIT_FLIP(a,b) ((a) ^= (1<<(b)))
            #define BIT_CHECK(a,b) ((a) & (1<<(b)))
            

            使用位結構操作

            這個使用起來簡單很多:

            struct bits {
                unsigned int a:1;
                unsigned int b:1;
                unsigned int c:1;
            };
            
            struct bits mybits;
            
            // set/clear a bit
            mybits.b = 1;
            mybits.c = 0;
            
            // toggle a bit
            mybits.a = !mybits.a;
            mybits.b = ~mybits.b;
            mybits.c ^= 1;
            
            // check a bit
            if (mybits.c)
            

            使用STL的std::bitset

            這個方法其實類似于使用位結構,只不過STL包裝了這個結構定義,當然還提供了很多便捷的接口:

            std::bitset<5> bits;
            bits[0] = true;
            bits[1] = false;
            bits.set(2);
            bits.flip(3);
            bits.reset(2);
            

            posted @ 2012-09-04 20:29 Kevin Lynx 閱讀(4591) | 評論 (2)編輯 收藏

            C/c++中的-->運算符

            參考What is the name of this operator: “–>”?

            c/c++中以下代碼是合法的:

            #include <stdio.h>
            int main()
            {
                 int x = 10;
                 while( x --> 0 ) // x goes to 0
                 {
                    printf("%d ", x);
                 }
            }
            

            -->是一個合法的操作符,我打賭自認c/c++熟手的你們都不知道這個操作符。有人稱它為goes to操作符,x-->0表示x向0趨近。

            其實我在忽悠你們。 并且我相信有很多人對此把戲相當熟悉。沒錯,-->只是兩個操作符恰好遇在了一起,他們是自減運算符--和大于比較運算符>

            while (x-- > 0)
                ...
            

            類似的把戲還有:

            while (x -- \
            \
            \
            \
            > 0) printf("%d ", x);

            posted @ 2012-09-03 15:30 Kevin Lynx 閱讀(2946) | 評論 (3)編輯 收藏

            為什么處理排序的數組要比非排序的快?

            參考Why is processing a sorted array faster than an unsorted array?

            問題

            看以下代碼:

            #include <algorithm>
            #include <ctime>
            #include <iostream>
            
            int main()
            {
                // generate data
                const unsigned arraySize = 32768;
                int data[arraySize];
            
                for (unsigned c = 0; c < arraySize; ++c)
                    data[c] = std::rand() % 256;
            
            
                // !!! with this, the next loop runs faster
                std::sort(data, data + arraySize);
            
            
                // test
                clock_t start = clock();
                long long sum = 0;
            
                for (unsigned i = 0; i < 100000; ++i)
                {
                    // primary loop
                    for (unsigned c = 0; c < arraySize; ++c)
                    {
                        if (data[c] >= 128)
                            sum += data[c];
                    }
                }
            
                double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
            
                std::cout << elapsedTime << std::endl;
                std::cout << "sum = " << sum << std::endl;
            }
            

            問題就在于,去掉std::sort那一行,以上代碼將運行更長的時間。在我的機器上未去掉std::sort耗時8.99s,去掉后耗時24.78s。編譯器使用的是gcc4.4.3。事實上,以上代碼跟編譯器沒有關系,甚至跟語言沒有關系。那這是為什么呢?

            這跟處理這個數組的邏輯有非常大的關系。如以上代碼所示,這個循環里有個條件判斷。條件判斷被編譯成二進制代碼后,就是一個跳轉指令,類似:

            具體為什么會不同,這涉及到計算機CPU執行指令時的行為。

            CPU的流水線指令執行

            想象現在有一堆指令等待CPU去執行,那么CPU是如何執行的呢?具體的細節可以找一本計算機組成原理的書來看。CPU執行一堆指令時,并不是單純地一條一條取出來執行,而是按照一種流水線的方式,在CPU真正執行一條指令前,這條指令就像工廠里流水線生產的產品一樣,已經被經過一些處理。簡單來說,一條指令可能經過這些過程:取指(Fetch)、解碼(Decode)、執行(Execute)、放回(Write-back)。

            假設現在有指令序列ABCDEFG。當CPU正在執行(execute)指令A時,CPU的其他處理單元(CPU是由若干部件構成的)其實已經預先處理到了指令A后面的指令,例如B可能已經被解碼,C已經被取指。這就是流水線執行,這可以保證CPU高效地執行指令。

            Branch Prediction

            如上所說,CPU在執行一堆順序執行的指令時,因為對于執行指令的部件來說,其基本不需要等待,因為諸如取指、解碼這些過程早就被做了。但是,當CPU面臨非順序執行的指令序列時,例如之前提到的跳轉指令,情況會怎樣呢?

            取指、解碼這些CPU單元并不知道程序流程會跳轉,只有當CPU執行到跳轉指令本身時,才知道該不該跳轉。所以,取指解碼這些單元就會繼續取跳轉指令之后的指令。當CPU執行到跳轉指令時,如果真的發生了跳轉,那么之前的預處理(取指、解碼)就白做了。這個時候,CPU得從跳轉目標處臨時取指、解碼,然后才開始執行,這意味著:CPU停了若干個時鐘周期!

            這其實是個問題,如果CPU的設計放任這個問題,那么其速度就很難提升起來。為此,人們發明了一種技術,稱為branch prediction,也就是分支預測。分支預測的作用,就是預測某個跳轉指令是否會跳轉。而CPU就根據自己的預測到目標地址取指令。這樣,即可從一定程度提高運行速度。當然,分支預測在實現上有很多方法。

            簡單的預測可以直接使用之前的實際執行結果。例如某個跳轉指令某一次產生了跳轉,那么下一次執行該指令時,CPU就直接從跳轉目標地址處取指,而不是該跳轉指令的下一條指令。

            答案

            了解了以上信息后,文章開頭提出的問題就可以解釋了。這個代碼中有一個循環,這個循環里有一個條件判斷。每一次CPU執行這個條件判斷時,CPU都可能跳轉到循環開始處的指令,即不執行if后的指令。使用分支預測技術,當處理已經排序的數組時,在若干次data[c]>=128都不成立時(或第一次不成立時,取決于分支預測的實現),CPU預測這個分支是始終會跳轉到循環開始的指令時,這個時候CPU將保持有效的執行,不需要重新等待到新的地址取指;同樣,當data[c]>=128條件成立若干次后,CPU也可以預測這個分支是不必跳轉的,那么這個時候CPU也可以保持高效執行。

            相反,如果是無序的數組,CPU的分支預測在很大程度上都無法預測成功,基本就是50%的預測成功概率,這將消耗大量的時間,因為CPU很多時間都會等待取指單元重新取指。

            本文完。最后感嘆下stackoverflow上這個帖子里那個老外回答問題的專業性,我要是樓主早就感動得涕淚橫飛了。感謝每一個傳播知識的人。

            參考資料

            1. http://blog.sina.com.cn/s/blog_6c673e570100zfmo.html
            2. http://www.cnblogs.com/dongliqian/archive/2012/04/05/2433847.html
            3. http://en.wikipedia.org/wiki/Branch_predictor

            posted @ 2012-08-30 17:43 Kevin Lynx 閱讀(3074) | 評論 (3)編輯 收藏

            MMO聊天服務器設計

            MMO中的聊天服務主要功能就是做客戶端之間的聊天內容轉發。但是聊天的形式有很多,例如私聊、同場景聊、隊伍內聊、工會內聊、全服務器聊、甚至臨 時組建房間聊。這些邏輯功能其實都是可以做在邏輯服務器上的,最多改改世界服務器,但是這樣完成功能的話,不免將聊天本身的邏輯與游戲邏輯關聯起來。我們 希望做得更上一層,將聊天服務本身脫離開來。但是獨立聊天服務還不夠,因為就算獨立出來了,也有可能在實現上與具體的游戲邏輯相關聯。所以,我們做了進一 步的抽象,想實現一個更為通用的聊天服務器。

            設計實現

            實體設計

            聊天這個過程,我們將其抽象為實體(entity)與實體間的對話。這個實體概念其實很寬泛。任何可接收聊天消息的都算做實體,例如單個玩家、一個 場景、一個隊伍、一個房間、一個工會、甚至整個服務器。這個思想其實就是支持整個聊天服務器設計的最根本思想。最開始,我將聊天服務器分為個體和組兩個概 念,其實這個抽象程度都太低,并且會導致實現上的復雜。相反,將整個系統完全使用實體這個概念來組裝,就簡單很多。當然,實體是有很多種類的,在處理接收 聊天消息這個動作時,其處理方式就不同。例如單個玩家實體僅做消息的發送,場景實體則是將消息發給場景內的所有玩家,隊伍實體就是將消息發給隊伍內的所有 玩家。從這一點來看,我們的實體種類其實并不多,因為場景、隊伍這些,都是組實體(group entity)。用C++來描述:

            class Entity {
            public:
                
            // send text to this entity
                virtual bool Send(Entity *sender, const std::string &text) = 0;

            protected:
                GUID m_id;
                
            int m_type;
            };

            class SockEntity : pubilc Entity {
            public:
                
            virtual bool Send(Entity *sender, const std::string &text) {
                    
            // find the map socket and send text to the socket
                    long socket = FindSocket(this);
                    Message msg(MSG_CS2E_SENDTEXT);
                    msg.Add(sender
            ->ID());
                    msg.Add(text);
                    msg.SendToSocket(socket);
                    
            return true;
                }
            };

            class GroupEntity : public Entity {
            public:
                
            virtual bool Send(Entity *sender, const std::string &text) {
                    
            for (std::list<Entity*>::const_iterator it = m_mems.begin(); it != m_mems.end(); ++it) {
                        (
            *it)->Send(sender, text);
                    }
                    
            return true;
                }
            private:
                std::list
            <Entity*> m_mems;
            };

            
            

            SockEntity用于表示物理上聊天服務器的客戶端,例如游戲客戶端。

            網絡拓撲

            實際上,除了轉發聊天內容外(Entity::Send),實體還有很多其他行為,例如最起碼的,創建組實體,往組實體里添加成員等。在設計上,組 實體的創建由邏輯服務器或者其他服務器來完成,目前游戲客戶端是沒有創建組實體的權限的(實現上我們還為實體添加了權限驗證機制)。在網絡拓撲上,聊天服 務器始終是作為服務器角色,而它的客戶端則包括游戲客戶端、邏輯服務器、甚至其他服務器,這樣聊天服務器在提供了固定的協議后,它就是完全獨立的,不依賴 任何其他組件:

                         CS
                      
            /  |  \
                     
            /   |   \
                    
            /    |    \
                   GC   GC   GS
            
            

            (CS: Chat Server, GC: Game Client, GS: Game Server)

            基于此,我們擴充了Entity的類體系:

            class ClientEntity : public SockEntity {

            private:
                GUID m_gsEntity; 
            // 標示該客戶端實體位于哪個邏輯服務器實體上
            };

            class GSEntity : public SockEntity {
            };

            消息協議

            聊天服務器的核心實現,其實就是針對以上實體做操作。因此,聊天服務器的消息協議方面,也主要是針對這些實體的操作,包括:

            • 創建

              實體的創建很簡單,不同的實體其創建所需的參數都不一樣。例如客戶端實體創建時需要傳入一個邏輯服務器實體的ID,組實體的創建可以攜帶組成員實體列表。 為了處理權限和安全問題,在具體實現上,邏輯服務器實體的創建是由聊天服務器本地的配置決定,即聊天服務器啟動則根據配置創建好邏輯服務器實體;客戶端實 體是當角色進入邏輯服務器后,由服務器創建,客戶端無法創建實體。

            • 刪除

              實體的刪除為了處理方便,約定刪除請求必須由實體的創建者發起。因為從邏輯上將,某個模塊如果可以創建一個實體,那么其必然知道什么時候該刪除這個實體。

            • 修改

              修改指的是修改實體內部實現的一些屬性,例如組實體修改其組成員。這個操作是非常重要的。對于SockEntity而 言,修改意味著修改其連接狀態,例如當邏輯服務器在聊天服務器上創建了客戶端實體后,實際上此時客戶端并沒有在網絡方面連接聊天服務器,此時這個Entity實 際上是不可用的,因為它無法用于發送消息。這個時候我們標志該實體的狀態為非連接狀態。當客戶端主動連接上聊天服務器后,客戶端就主動發起修改自己對應的 客戶端實體請求,該請求將自己的狀態修改為連接狀態。當客戶端關閉時,聊天服務器網絡層接收到連接斷開通知,該通知肯定是早于邏輯服務器發來的刪除實體通 知的,此時將該客戶端實體狀態修改為斷開狀態,并在接收到邏輯服務器刪除實體通知時將其真正刪除。這里展示的這種狀態修改策略,實際上在整個系統中是非常 重要的。它用于指導網絡連接和上層邏輯之間的關系,因為整個聊天系統中,各個進程的狀態是不可預料的(隨時可能宕掉),當某個進程尤其是邏輯服務器宕掉 后,聊天服務器是得不到任何正常邏輯通知的,它只能得到網絡連接的通知。

            總結

            整個系統實現下來,實際上是非常簡單的,代碼量也很少。當然還有很多細節問題,例如聊天信息中攜帶物品信息,這涉及到異步預處理聊天內容,這里就不 方便細說了。

            原文地址: http://codemacro.com/2012/08/29/mmo-chat-server/
            written by Kevin Lynx  posted at http://codemacro.com

            posted @ 2012-08-29 11:37 Kevin Lynx 閱讀(3588) | 評論 (2)編輯 收藏

            使用memcmp比較兩個變量結果一定嗎?

            參考Is using memcmp on array of int strictly conforming?

            以下代碼一定會輸出ok嗎?

            #include <stdio.h>
            #include <string.h>
            
            struct S { int array[2]; };
            
            int main () {
                struct S a = { { 1, 2 } };
                struct S b;
                b = a;
                if (memcmp(b.array, a.array, sizeof(b.array)) == 0) {
                    puts("ok");
                }
                return 0;
            }
            

            我在vs2005以及gcc4.4.3上做了測試,都輸出了ok。但這并不意味這個代碼會永遠輸出ok。問題主要集中于這里使用了賦值語句來復制值,但卻使用了memcmp這個基于內存數據比較的函數來比較值。

            c語言中的賦值運算符(=)被定義為基于值的復制,而不是基于內存內容的復制。

            C99 section 6.5.16.1 p2: In simple assignment (=), the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.

            這個其實很好理解,尤其在不同類型的數字類型間復制時,例如:

            float a = 1.1;
            int b = a;
            

            因為浮點數和整形數的內存布局不一樣,所以肯定是基于值的一種復制。另外,按照語言標準的思路來看,內存布局這種東西一般都屬于實現相關的,所以語言標準是不會依賴實現去定義語言的。

            上面的定理同樣用于復雜數據類型,例如結構體。我們都知道結構體每個成員之間可能會有字節補齊,而使用賦值運算符來復制時,會不會復制這些補齊字節的內容,是語言標準未規定的。這意味著使用memcmp比較兩個通過賦值運算符復制的兩個結構體時,其結果是未定的。

            但是上面的代碼例子中,比較的其實是兩個int數組。這也無法確認結果嗎?這個問題最終集中于,難道int也會有不確定的補齊字節數據?

            C99 6.2.6.2 integer types For signed integer types, the bits of the object representation shall be divided into three groups: value bits, padding bits, and the sign bit. […] The values of any padding bits are unspecified.

            這話其實我也不太懂。一個有符號整數int,其內也有補齊二進制位(bits)?

            但無論如何,這個例子都不算嚴謹的代碼。人們的建議是使用memcpy來復制這種數據,因為memcpy和memcmp都是基于內存內容來工作的。

            posted @ 2012-08-17 14:07 Kevin Lynx 閱讀(3955) | 評論 (2)編輯 收藏

            讓wxListCtrl支持子item編輯

            我使用的wxLua版本信息為wxLua 2.8.7.0 built with wxWidgets 2.8.8,也就是LuaForWindows_v5.1.4-40.exe這個安裝包里自帶的wxLua。我不知道其他wxWidgets版本里wxListCtrl怎樣,但我使用的版本里wxListCtrl是不支持編輯里面的子item的。在我使用的report模式下,子item也就是特定某一行一列的item。

            google了一下,發現悲劇地需要自己實現,主要就是自己顯示一個wxTextCtrl:

            --
            -- file: wxListCtrlTextEdit.lua
            -- author: Kevin Lynx
            -- date: 08.06.2012
            --
            local EditList = {}
            
            -- get the column by an abs point
            function EditList:getColumn(x)
                local cols = self.listctrl:GetColumnCount()
                local cx = 0
                for i = 0, cols - 1 do
                    local w = self.listctrl:GetColumnWidth(i)
                    if x <= cx + w then return i end
                    cx = cx + w
                end
                return -1
            end
            
            -- when a mouse down, show a text edit control 
            function EditList:onLeftDown(evt)
                if self.editor:IsShown() then
                    self:closeEditor()
                end
                local p = evt:GetPoint()
                local row = evt:GetIndex()
                local col = self:getColumn(p.x)
                local rect = wx.wxListCtrlEx.GetSubItemRect(self.listctrl, row, col)
                rect:SetHeight(rect:GetHeight() + 5) -- adjust
                self.editor:SetSize(rect)
                self.editor:Show()
                self.editor:SetValue(wx.wxListCtrlEx.GetItemText(self.listctrl, row, col))
                self.editor:SetFocus()
                self.col = col
                self.row = row
            end
            
            function EditList:closeEditor()
                if not self.editor:IsShown() then return end
                self.editor:Hide()
                self.listctrl:SetItem(self.row, self.col, self.editor:GetValue())
            end
            
            function EditList:initialize()
                self.editor = wx.wxTextCtrl(self.listctrl, wx.wxID_ANY, "", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxTE_PROCESS_ENTER + wx.wxTE_RICH2)
                self.editor:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, function () self:closeEditor() end)
                -- not work actually
                self.editor:Connect(wx.wxEVT_COMMAND_KILL_FOCUS, function () self:closeEditor() end)
                self.editor:Hide()
            end
            
            function wx.wxListCtrlTextEdit(listctrl)
                local o = {
                    listctrl = listctrl,
                    editor = nil,
                }
                local editlist = newObject(o, EditList)
                editlist:initialize()
                listctrl:Connect(wx.wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, function (evt) editlist:onLeftDown(evt) end)
                listctrl:Connect(wx.wxEVT_COMMAND_LIST_ITEM_FOCUSED, function () editlist:closeEditor() end)
                return listctrl
            end
            

            其原理就是獲取到當前鼠標點擊所在的子item位置,然后在此位置顯示一個wxEditCtrl即可。以上代碼需要依賴我之前寫的Lua里實現簡單的類-對象中的代碼,同時依賴以下針對wxListCtrl的擴展接口:

            --
            -- file: wxListCtrlExtend.lua
            -- author: Kevin Lynx
            -- date: 08.07.2012
            -- brief: extend some util functions to wx.wxListCtrl
            -- 
            wx.wxListCtrlEx = {}
            
            function wx.wxListCtrlEx.GetSubItemRect(listctrl, item, col)
                local rect = wx.wxRect()
                listctrl:GetItemRect(item, rect)
                local x = 0
                local w = 0
                for i = 0, col do
                    w = listctrl:GetColumnWidth(i)
                    x = x + w
                end
                return wx.wxRect(x - w, rect:GetY(), w, rect:GetHeight())
            end
            
            function wx.wxListCtrlEx.GetItemText(listctrl, item, col)
                local info = wx.wxListItem()
                info:SetId(item)
                info:SetColumn(col)
                info:SetMask(wx.wxLIST_MASK_TEXT)
                listctrl:GetItem(info)
                return info:GetText()
            end
            

            在我看到的wxWidgets官方文檔里,其實wxListCtrl已經有GetSubItemRect接口,并且在另一些示例代碼里,也看到了GetItemText接口,但是,我使用的版本里沒有,所以只好自己寫?;谝陨?,要使用這個可以支持編輯子item的wxListCtrl,可以:

            list = wx.wxListCtrlTextEdit(wx.wxListCtrl(dialog, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxLC_REPORT))
            

            也就是通過wx.wxListCtrlTextEdit這個函數做下處理,這個函數返回的是本身的wxListCtrl。當然更好的方式是使用繼承之類的方式,開發一種新的控件,但在Lua中,針對usedata類型的擴展貌似只能這樣了。

            最好吐槽下,這個控件擴展其實很惡心。本來我打算當編輯控件失去焦點后就隱藏它,但是往編輯控件上注冊KILL_FOCUS事件始終不起作用;我又打算弄個ESC鍵盤事件去手動取消,但顯然wxTextCtrl是不支持鍵盤事件的。好吧,湊合用了。

            posted @ 2012-08-07 17:09 Kevin Lynx 閱讀(2962) | 評論 (0)編輯 收藏

            像寫函數式語言代碼一樣寫C++


            忘記最早接觸函數式編程語言是什么時候了,也忘記接觸的第一門函數式語言是哪一門。斷斷續續接觸過好幾種函數式語言(當然都算不純的,ruby/lisp不算純吧),這些語言的思想在潛移默化中多多少少對我有所影響。

            我是個C++程序員,我不知道我平時寫的都是些什么代碼。最讓人印象深刻就是我會經常寫遍歷STL容器的代碼,是經常,這樣的遍歷你可能也不陌生:

            for (ListType::iterator it = con.begin(); it != con.end(); ++it) {
                something
            }

            或者針對std::map/set等的查找:

            Table::iterator it = table.find(key);
            if (it == table.end())
                
            do-something
            do-something

            多虧STL接口的一致性,這讓我們寫出了很多“一致性“代碼。慢慢地我覺得惡心,不禁想起函數式編程語言中,對于這種需求一般都會提供類似的接口:

            con.map(function (it) if (it->some-filed == some-value) return something end)
            # 或者
            con.each 
            do |it| if it.some-filed == some-value then return something end end
            # 或者
            (con.map (lambda (it) (
            if ((= it.some-filed some-value)) (return something))))

            (好吧,lisp我又忘了)總之,這種針對容器的遍歷操作,都會成為一種內置接口,并且通過lambda來讓用戶直接編寫處理代碼,少去寫循環的冗余。然后,我寫了類似下面的一組宏(隨手敲的不保證能運行):

            #define IT_N __it

            #define TRAVERSE_MAP(type, map, exps) \
                
            for (type::iterator IT_N = map.begin(); IT_N != map.end(); ++IT_N) { \
                    exps; \
                }
            #define I_KEY (IT_N->first)
            #define I_VALUE (IT_N->second)

            #define TRAVERSE_LIST(type, list, exps) \
                
            for (type::iterator IT_N = list.begin(); IT_N != list.end(); ++IT_N) { \
                    exps; \
                }
            #define L_VALUE (*IT_N)

            #define FIND_MAP_ITEM(type, map, key, fexps, texps) \
                
            do { \
                    type::iterator IT_N 
            = map.find(key); \
                    
            if (IT_N == map.end()) { \
                        fexps; \
                    } 
            else { \
                        texps; \
                    } \
                } 
            while(0)

            #define VAL_N __val
            #define FIND_LIST_ITEM_IF(type, list, cmp, fexps, texps) \
                
            do { \
                    
            struct Comp { \
                        
            bool operator() (const type::value_type &VAL_N) const { \
                            
            return cmp; \
                        } \
                    }; \
                    type::iterator IT_N 
            = std::find_if(list.begin(), list.end(), Comp()); \
                    
            if (IT_N != list.end()) { \
                        texps; \
                    } 
            else { \
                        fexps; \
                    } \
                } 
            while(0)

            #define NULL_EXP ;

            當然,以上接口都還包含一些const版本,用于const容器的使用。使用的時候(截取的項目中的使用例子):

            TRAVERSE_MAP(TimerTable, m_timers, 
                    I_VALUE.obj
            ->OnTimerCancel(I_KEY, I_VALUE.arg);
                    TIMER_CANCEL(I_VALUE.id)); 

            TRAVERSE_LIST(AreaList, areas,
                    ids.push_back(L_VALUE
            ->ID()));

            FIND_MAP_ITEM(PropertyTable, m_properties, name,
                    LogWarn(
            "set a non-existed property %s", name.c_str()); return NIL_VALUE,
                    
            if (val.Type() != I_VALUE.type()) {
                        
            return NIL_VALUE; 
                    } 
            else {
                        GValue old 
            = I_VALUE;
                        I_VALUE 
            = val; 
                        
            return old;
                    });

            多虧了C/C++宏對一切內容的可容納性,可以讓我往宏參數里塞進像if這種復合語句,甚至多條語句(例如最后一個例子)。這些宏我使用了一段時間,開始覺得挺爽,很多函數的實現里,我再也不用寫那些重復的代碼了。但是后來我發覺這些代碼越來越惡心了。最大的弊端在于不可調試,我只能將斷點下到更深的代碼層;然后就是看起來特不直觀,連作者自己都看得覺得不直觀了,可想而知那些連函數式編程語言都不知道是什么的C++程序員看到這些代碼會是什么心情(可以想象哥已經被詛咒了多少次)。

            函數式語言讓人寫出更短的代碼,這一點也對我有影響,例如我最近又寫下了一些邪惡代碼:

            // split a string into several sub strings by a split character i.e:
            // "a;b;c;" => "a", "b", "c"
            // "a;b;c" => "a", "b", "c"
            std::vector<std::string> SplitString(const std::string &str, char split) {
                std::vector
            <std::string> ret;
                size_t last 
            = 0;
                
            for (size_t pos = str.find(split); pos != std::string::npos; last = pos + 1, pos = str.find(split, last)) {
                    ret.push_back(str.substr(last, pos 
            - last));
                }
                
            return last < str.length() ? ret.push_back(str.substr(last)) : 0, ret;
            }

            惡心的就是最后那條return語句,因為我需要處理”a;b;c”這種c后面沒加分隔符的情況,但我并不愿意為了這個需求再寫一個會占超過一行的if語句。因為,我太喜歡ruby里的if了:


            do-something if exp

            也就是ruby里允許這種只有一行if的代碼將if放在其后并作為一條語句。我的不愿意其實是有理由的,在c/c++中有太多只有一行條件體的if語句,對這些語句參合進編程風格/可讀性進來后,就不得不讓你寫出不安的代碼,例如:

            if (something) return something; // 某些編程風格里不允許這樣做,因為它不方便調試

            if (something) 
                
            return something; // 某些風格里又有大括號的統一要求

            if (something) {
                
            return something; // 就算符合風格了,但這一條語句就得多個大括號
            }

            if (something) 
            {
                
            return something; // 某些風格里這大括號就更奢侈了
            }

            這個return除了乍看上去有點糾結外,其實也不算什么大問題,但是那個問號表達式返回的0實在沒有任何意義,而正是沒有意義才會讓它誤導人。本來我是可以寫成:

            return last < str.length() && ret.push_back(str.substr(last)), ret;

            這樣利用條件表達式的短路運算,代碼也清晰多了。但是,std::vector::push_back是一個沒有返回值的函數,所以。

            全文完。

            posted @ 2012-07-31 09:43 Kevin Lynx 閱讀(3059) | 評論 (3)編輯 收藏

            僅列出標題
            共12頁: 1 2 3 4 5 6 7 8 9 Last 
            久久久国产打桩机| 国产美女久久久| 久久九色综合九色99伊人| 久久有码中文字幕| 伊人久久久AV老熟妇色| 中文字幕亚洲综合久久| 久久人人爽人人人人片av| 久久亚洲国产精品123区| 久久精品国产精品亚洲艾草网美妙| 精品综合久久久久久97超人| 久久精品国产99国产精品澳门| 99久久人妻无码精品系列| 久久伊人亚洲AV无码网站| 亚洲а∨天堂久久精品| 国内精品人妻无码久久久影院 | 久久婷婷午色综合夜啪| 久久亚洲欧美国产精品| 人人狠狠综合久久亚洲高清| 色8激情欧美成人久久综合电| 2021国产精品午夜久久| 国产日韩久久久精品影院首页 | 久久亚洲日韩精品一区二区三区| 无码AV波多野结衣久久| 亚洲人成网站999久久久综合| 欧美日韩中文字幕久久伊人| 色婷婷久久综合中文久久蜜桃av| 久久久老熟女一区二区三区| 一本久久a久久精品vr综合| 无码人妻精品一区二区三区久久久| 99久久久国产精品免费无卡顿| 青青青国产成人久久111网站| 久久精品国产72国产精福利| 97久久国产露脸精品国产| 高清免费久久午夜精品| 久久婷婷五月综合97色一本一本 | 久久性精品| 亚洲va久久久噜噜噜久久狠狠| www.久久热| 欧美日韩精品久久久久| 久久综合久久综合久久综合| 一本久久综合亚洲鲁鲁五月天亚洲欧美一区二区 |