構(gòu)體排序?qū)崙?zhàn):重載、函數(shù)與Lambda三種方式詳解)
1. 項(xiàng)目概述為什么結(jié)構(gòu)體排序是編程中的“家常便飯”在C的實(shí)際開發(fā)里尤其是處理業(yè)務(wù)數(shù)據(jù)時(shí)我們很少只對(duì)單一的基本類型比如一個(gè)整數(shù)、一個(gè)字符串進(jìn)行排序。更多時(shí)候我們面對(duì)的是一個(gè)個(gè)“數(shù)據(jù)包”比如一個(gè)學(xué)生的信息學(xué)號(hào)、姓名、成績(jī)、一本書的信息ISBN、書名、價(jià)格、庫存或者一條交易記錄時(shí)間、金額、類型。這些數(shù)據(jù)包在C里最自然的載體就是結(jié)構(gòu)體struct。于是一個(gè)高頻需求就出現(xiàn)了如何對(duì)這一堆結(jié)構(gòu)體對(duì)象按照我們指定的某個(gè)或某幾個(gè)字段的規(guī)則進(jìn)行排序“結(jié)構(gòu)體排序的三種方式”這個(gè)標(biāo)題直指的就是解決這個(gè)問題的三種經(jīng)典且核心的實(shí)現(xiàn)路徑。這絕不是紙上談兵的理論而是每天都會(huì)在代碼中上演的實(shí)戰(zhàn)操作。想象一下你要在控制臺(tái)顯示一個(gè)學(xué)生成績(jī)榜需要按總分從高到低排或者在一個(gè)游戲里需要根據(jù)玩家的等級(jí)和最近登錄時(shí)間生成一個(gè)活躍度榜單。這些場(chǎng)景的背后都是結(jié)構(gòu)體排序在發(fā)揮作用。掌握這三種方式意味著你拿到了處理自定義數(shù)據(jù)排序的“萬能鑰匙”。它們各有各的適用場(chǎng)景和優(yōu)劣理解其背后的思想不僅能讓你在寫排序代碼時(shí)游刃有余更能加深你對(duì)C語言特性如運(yùn)算符重載、函數(shù)對(duì)象、Lambda表達(dá)式的理解。接下來我們就拋開教科書式的說教直接進(jìn)入實(shí)戰(zhàn)拆解這三種方式究竟怎么用以及為什么在某些場(chǎng)合下你必須用其中某一種。2. 核心思路拆解三種方式的本質(zhì)與選型考量在深入代碼之前我們得先搞清楚這三種方式分別是什么以及它們解決問題的核心思路有何不同。這就像你要出門得先知道是開車、騎車還是走路每種方式適合不同的距離和路況。2.1 方式一重載小于號(hào) (operator)這是最“C”的一種方式它賦予了你的自定義結(jié)構(gòu)體一種天生的、默認(rèn)的比較能力。其核心思想是定義結(jié)構(gòu)體對(duì)象之間何為“小于”。一旦你重載了運(yùn)算符那么不僅std::sort可以直接使用其他所有依賴比較的STL容器和算法如std::set,std::map,std::priority_queue也都能自動(dòng)識(shí)別并使用這個(gè)規(guī)則。為什么選擇它語義自然如果你設(shè)計(jì)的結(jié)構(gòu)體有一個(gè)明確的、最主要的排序標(biāo)準(zhǔn)比如Student按score排序重載使得a b這樣的表達(dá)式變得直觀且合理。兼容性廣一次定義處處可用。非常適合作為結(jié)構(gòu)體的默認(rèn)排序規(guī)則。缺點(diǎn)一個(gè)結(jié)構(gòu)體只能有一個(gè)全局的operator重載。如果你需要針對(duì)同一結(jié)構(gòu)體類型在不同的場(chǎng)景下按不同規(guī)則排序比如一會(huì)按成績(jī)一會(huì)按學(xué)號(hào)這種方式就力不從心了。2.2 方式二定義獨(dú)立的比較函數(shù) (cmp)這是一種非常靈活且傳統(tǒng)的C風(fēng)格做法。核心思想是不改變結(jié)構(gòu)體本身而是定義一個(gè)外部的、獨(dú)立的函數(shù)專門用來告訴排序算法兩個(gè)對(duì)象的大小關(guān)系。這個(gè)函數(shù)接收兩個(gè)常量結(jié)構(gòu)體引用作為參數(shù)返回一個(gè)布爾值。為什么選擇它靈活性高你可以定義無數(shù)個(gè)cmp函數(shù)比如cmpByScore,cmpById,cmpByScoreThenById。需要哪種排序就把對(duì)應(yīng)的函數(shù)指針傳給std::sort。職責(zé)分離比較邏輯與結(jié)構(gòu)體定義分離保持了結(jié)構(gòu)體的純潔性。特別是當(dāng)這個(gè)結(jié)構(gòu)體來自第三方庫你無法修改其源代碼時(shí)這是唯一的選擇。缺點(diǎn)函數(shù)是全局或命名空間內(nèi)的如果比較邏輯很復(fù)雜或者需要捕獲外部變量寫起來會(huì)有點(diǎn)麻煩并且可能污染命名空間。2.3 方式三使用Lambda表達(dá)式這是C11之后最受推崇的現(xiàn)代寫法可以說是為std::sort這類算法“量身定做”的。核心思想是在調(diào)用排序函數(shù)的地方就地、匿名地定義一個(gè)臨時(shí)的比較邏輯。它融合了獨(dú)立函數(shù)的靈活性和函數(shù)對(duì)象的封裝能力。為什么選擇它極致便捷代碼高度內(nèi)聚你可以在調(diào)用sort的那一行直接看到排序規(guī)則是什么無需跳轉(zhuǎn)到其他地方查找函數(shù)定義。強(qiáng)大的捕獲能力Lambda可以方便地捕獲其所在作用域中的變量比如一個(gè)用于比較的閾值、一個(gè)權(quán)重系數(shù)這是普通cmp函數(shù)難以優(yōu)雅實(shí)現(xiàn)的?,F(xiàn)代C的標(biāo)配寫法簡(jiǎn)潔明了是現(xiàn)代C代碼的典型特征。選型決策速查表場(chǎng)景推薦方式理由結(jié)構(gòu)體有唯一、明確的默認(rèn)排序規(guī)則重載operator語義正確一勞永逸被STL廣泛支持。需要多種排序規(guī)則或無法修改結(jié)構(gòu)體源碼獨(dú)立cmp函數(shù)或Lambda表達(dá)式靈活。Lambda通常更簡(jiǎn)潔。排序規(guī)則簡(jiǎn)單且只在一處使用Lambda表達(dá)式代碼內(nèi)聚無需額外命名和定義。排序規(guī)則需要依賴外部變量或狀態(tài)Lambda表達(dá)式利用捕獲列表實(shí)現(xiàn)簡(jiǎn)單直觀。需要將比較規(guī)則作為參數(shù)傳遞或存儲(chǔ)函數(shù)對(duì)象或Lambda它們都是可調(diào)用對(duì)象比函數(shù)指針更通用。注意在實(shí)際項(xiàng)目中Lambda表達(dá)式因其無與倫比的便利性已成為處理此類臨時(shí)比較邏輯的絕對(duì)主流。重載用于定義默認(rèn)序而獨(dú)立的cmp函數(shù)則在需要與C接口兼容或邏輯極其復(fù)雜時(shí)使用。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)理解了三種方式是什么我們來看看在實(shí)現(xiàn)它們時(shí)有哪些必須注意的“魔鬼細(xì)節(jié)”。這些細(xì)節(jié)直接關(guān)系到代碼的正確性、效率和可維護(hù)性。3.1 重載小于號(hào)常量性與嚴(yán)格弱序當(dāng)你重載運(yùn)算符時(shí)最佳實(shí)踐是將其定義為常量成員函數(shù)。這是因?yàn)楸容^操作不應(yīng)該改變對(duì)象的狀態(tài)。struct Student { int id; std::string name; double score; // 正確做法常量成員函數(shù) bool operator(const Student other) const { return score other.score; // 按分?jǐn)?shù)升序 } };那個(gè)const關(guān)鍵字確保了在比較this對(duì)象和other對(duì)象時(shí)它們的內(nèi)容都不會(huì)被修改。更關(guān)鍵的一點(diǎn)是你必須確保你的比較邏輯滿足嚴(yán)格弱序。這是所有STL比較操作的基礎(chǔ)要求簡(jiǎn)單來說需要滿足非自反性comp(a, a)必須為false。非對(duì)稱性如果comp(a, b)為true則comp(b, a)必須為false。可傳遞性如果comp(a, b)為true且comp(b, c)為true則comp(a, c)必須為true。等價(jià)的可傳遞性如果!comp(a, b) !comp(b, a)即a和b等價(jià)并且!comp(b, c) !comp(c, b)那么必須有!comp(a, c) !comp(c, a)。對(duì)于簡(jiǎn)單的數(shù)值比較這自然滿足。但當(dāng)你實(shí)現(xiàn)多級(jí)排序時(shí)要特別小心。例如先按分?jǐn)?shù)降序分?jǐn)?shù)相同按學(xué)號(hào)升序bool operator(const Student other) const { if (score ! other.score) { return score other.score; // 注意這里是 表示分?jǐn)?shù)高的“小于”分?jǐn)?shù)低的這違反了直覺但可以實(shí)現(xiàn)降序。 } return id other.id; }上面這個(gè)實(shí)現(xiàn)雖然功能上能實(shí)現(xiàn)“分?jǐn)?shù)降序id升序”但用operator來實(shí)現(xiàn)降序語義非常別扭容易導(dǎo)致誤解。因此重載通常僅用于定義最自然的、升序的默認(rèn)規(guī)則。復(fù)雜的、特別是包含降序的規(guī)則建議使用Lambda或cmp函數(shù)這樣意圖更清晰。3.2 獨(dú)立比較函數(shù)參數(shù)傳遞與性能cmp函數(shù)的參數(shù)應(yīng)該使用const引用。傳值Student a, Student b在結(jié)構(gòu)體較大時(shí)會(huì)產(chǎn)生不必要的拷貝開銷。傳const引用既避免了拷貝又保證了函數(shù)不會(huì)修改原始對(duì)象。// 好的做法 bool cmpByScore(const Student a, const Student b) { return a.score b.score; } // 避免的做法性能差 bool cmpByScoreBad(Student a, Student b) { return a.score b.score; }當(dāng)需要多級(jí)排序時(shí)cmp函數(shù)的邏輯需要清晰排列條件。例如先按班級(jí)升序再按分?jǐn)?shù)降序bool cmpByClassThenScoreDesc(const Student a, const Student b) { if (a.class ! b.class) { return a.class b.class; // 第一級(jí)班級(jí)升序 } // 班級(jí)相同比較分?jǐn)?shù) return a.score b.score; // 第二級(jí)分?jǐn)?shù)降序 }這種if-return的鏈?zhǔn)浇Y(jié)構(gòu)是實(shí)現(xiàn)多級(jí)排序的標(biāo)準(zhǔn)模式邏輯清晰易于擴(kuò)展。3.3 Lambda表達(dá)式捕獲方式與泛型LambdaLambda表達(dá)式的強(qiáng)大之處在于其捕獲列表[]。你需要根據(jù)需求決定捕獲方式[]不捕獲任何外部變量。[var]按值捕獲變量var。[var]按引用捕獲變量var。[]按值捕獲所有外部變量謹(jǐn)慎使用可能造成不必要的拷貝或懸空引用。[]按引用捕獲所有外部變量更需謹(jǐn)慎容易引發(fā)生命周期問題。[this]捕獲當(dāng)前類對(duì)象的this指針。一個(gè)常見陷阱如果你在Lambda體內(nèi)使用了外部變量但沒有在捕獲列表中聲明編譯器會(huì)報(bào)錯(cuò)。反之如果捕獲了不需要的變量可能會(huì)引入隱蔽的bug。對(duì)于C14及以上你可以使用泛型Lambda讓編譯器自動(dòng)推導(dǎo)參數(shù)類型這在編寫模板代碼或參數(shù)類型復(fù)雜時(shí)非常有用// C14 泛型Lambda auto genericComparator [](const auto a, const auto b) { return a.score b.score; }; // 可以用于排序 Student也可以用于排序任何有 score 成員的結(jié)構(gòu)體實(shí)操心得對(duì)于簡(jiǎn)單的、局部的排序盡量使用Lambda。在捕獲列表里遵循“最小權(quán)限原則”只捕獲真正需要的變量并且優(yōu)先考慮按值捕獲 ([var])除非你明確需要修改外部變量或該變量很大按引用捕獲 ([var]) 更高效。避免使用默認(rèn)的[]或[]它們會(huì)讓代碼的依賴關(guān)系變得不清晰。4. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)下面我們用一個(gè)完整的例子來演示三種方式的具體實(shí)現(xiàn)。假設(shè)我們有一個(gè)Student結(jié)構(gòu)體需要對(duì)其進(jìn)行多種方式的排序。4.1 定義公共數(shù)據(jù)結(jié)構(gòu)與測(cè)試數(shù)據(jù)首先定義我們的結(jié)構(gòu)體和一些測(cè)試數(shù)據(jù)。#include iostream #include string #include vector #include algorithm // for std::sort struct Student { int id; std::string name; double score; int classId; // 為了方便打印重載 運(yùn)算符 friend std::ostream operator(std::ostream os, const Student s) { os ID: s.id , Name: s.name , Score: s.score , Class: s.classId; return os; } }; int main() { std::vectorStudent students { {101, Alice, 88.5, 1}, {102, Bob, 92.0, 2}, {103, Charlie, 88.5, 1}, {104, David, 76.0, 2}, {105, Eve, 95.5, 1} }; // 后續(xù)的排序演示都將基于這個(gè) students 向量 // ... return 0; }4.2 方式一實(shí)操重載 operator我們?cè)诮Y(jié)構(gòu)體內(nèi)部定義默認(rèn)的排序規(guī)則比如按id升序。struct Student { int id; std::string name; double score; int classId; // 重載小于號(hào)定義默認(rèn)按id升序 bool operator(const Student other) const { return id other.id; } // ... 其他成員和友元函數(shù) }; // 在 main 函數(shù)中使用 std::cout \n--- 排序方式1: 重載operator (按id升序) ---\n; std::vectorStudent students1 students; // 拷貝一份數(shù)據(jù) std::sort(students1.begin(), students1.end()); // 直接使用std::sort無需額外參數(shù) for (const auto s : students1) { std::cout s std::endl; }運(yùn)行后學(xué)生將按學(xué)號(hào)101, 102, 103...的順序排列。注意std::sort的默認(rèn)行為就是使用operator進(jìn)行升序排序。如果你想用這個(gè)規(guī)則降序排可以使用std::sort的重載版本配合std::greater()std::sort(students1.begin(), students1.end(), std::greaterStudent()); // 這將使用 operator但我們的結(jié)構(gòu)體沒有重載 所以會(huì)編譯錯(cuò)誤。 // 正確做法是為降序定義另一個(gè)規(guī)則或者使用方式二/三。4.3 方式二實(shí)操定義獨(dú)立cmp函數(shù)我們?cè)谌只蛎臻g內(nèi)定義幾個(gè)不同的比較函數(shù)。// 獨(dú)立比較函數(shù)1按分?jǐn)?shù)升序 bool cmpByScoreAsc(const Student a, const Student b) { return a.score b.score; } // 獨(dú)立比較函數(shù)2按班級(jí)升序同班級(jí)按分?jǐn)?shù)降序 bool cmpByClassAscThenScoreDesc(const Student a, const Student b) { if (a.classId ! b.classId) { return a.classId b.classId; } return a.score b.score; // 注意這里是 實(shí)現(xiàn)降序 } // 在 main 函數(shù)中使用 std::cout \n--- 排序方式2: 獨(dú)立cmp函數(shù) (按分?jǐn)?shù)升序) ---\n; std::vectorStudent students2 students; std::sort(students2.begin(), students2.end(), cmpByScoreAsc); for (const auto s : students2) { std::cout s std::endl; } std::cout \n--- 排序方式2: 獨(dú)立cmp函數(shù) (按班級(jí)升序同班分?jǐn)?shù)降序) ---\n; std::vectorStudent students3 students; std::sort(students3.begin(), students3.end(), cmpByClassAscThenScoreDesc); for (const auto s : students3) { std::cout s std::endl; }這里的關(guān)鍵是將函數(shù)名cmpByScoreAsc作為第三個(gè)參數(shù)傳遞給std::sort。函數(shù)名在需要時(shí)會(huì)自動(dòng)退化為函數(shù)指針。4.4 方式三實(shí)操使用Lambda表達(dá)式這是最靈活的方式我們直接在std::sort調(diào)用處寫規(guī)則。// 在 main 函數(shù)中使用 std::cout \n--- 排序方式3: Lambda表達(dá)式 (按姓名字典序升序) ---\n; std::vectorStudent students4 students; std::sort(students4.begin(), students4.end(), [](const Student a, const Student b) { return a.name b.name; // 直接使用string的運(yùn)算符 }); for (const auto s : students4) { std::cout s std::endl; } // 更復(fù)雜的例子按班級(jí)降序同班級(jí)按分?jǐn)?shù)升序且只排序分?jǐn)?shù)大于80的學(xué)生演示捕獲 std::cout \n--- 排序方式3: Lambda表達(dá)式 (復(fù)雜規(guī)則帶捕獲) ---\n; std::vectorStudent students5 students; double scoreThreshold 80.0; std::sort(students5.begin(), students5.end(), [scoreThreshold](const Student a, const Student b) { // 假設(shè)我們想將低于閾值的學(xué)生排到最后但內(nèi)部仍按規(guī)則比較 // 注意這個(gè)比較函數(shù)必須滿足嚴(yán)格弱序。以下邏輯在a,b一個(gè)高于閾值一個(gè)低于閾值時(shí)會(huì)破壞等價(jià)傳遞性僅作演示。 // 更健壯的做法是先用 partition 分開再排序。 if (a.score scoreThreshold b.score scoreThreshold) return false; if (a.score scoreThreshold b.score scoreThreshold) return true; // 都在閾值以上或以下按主要規(guī)則比較 if (a.classId ! b.classId) { return a.classId b.classId; // 班級(jí)降序 } return a.score b.score; // 分?jǐn)?shù)升序 }); for (const auto s : students5) { std::cout s std::endl; }重要提示上面第二個(gè)Lambda例子中混合了“過濾”根據(jù)閾值和“排序”的邏輯這很容易破壞嚴(yán)格弱序?qū)е挛炊x行為實(shí)際運(yùn)行可能看似正常但在某些輸入或STL實(shí)現(xiàn)下會(huì)崩潰。這是一個(gè)典型的錯(cuò)誤示范。正確的做法是分兩步先用std::partition將滿足條件和不滿足條件的元素分開再對(duì)滿足條件的部分進(jìn)行排序。這里只是為了展示Lambda可以捕獲外部變量 (scoreThreshold)。5. 常見問題與排查技巧實(shí)錄在實(shí)際使用中你肯定會(huì)遇到一些坑。下面是我總結(jié)的幾個(gè)典型問題和解決方法。5.1 編譯錯(cuò)誤“invalid comparator”這是最常見的問題根本原因就是你的比較函數(shù)或Lambda沒有滿足嚴(yán)格弱序。特別是當(dāng)比較規(guī)則中包含相等性判斷和降序邏輯時(shí)。錯(cuò)誤示例// 試圖實(shí)現(xiàn)降序但寫法錯(cuò)誤 bool badComparator(const Student a, const Student b) { return a.score b.score; // 錯(cuò)誤當(dāng)a.score b.score時(shí)返回true違反了非自反性。 }當(dāng)a和b是同一個(gè)對(duì)象或者兩個(gè)分?jǐn)?shù)相等的不同對(duì)象時(shí)badComparator(a, a)返回true這違反了“非自反性”。std::sort內(nèi)部可能會(huì)陷入無限循環(huán)或直接崩潰。正確寫法// 實(shí)現(xiàn)降序的正確寫法 bool correctComparatorDesc(const Student a, const Student b) { return a.score b.score; // 使用 而不是 }排查技巧當(dāng)遇到invalid comparator或程序在sort時(shí)崩潰首先檢查你的比較邏輯。確保對(duì)于任何a和bcomp(a,b)和comp(b,a)不會(huì)同時(shí)為true。多級(jí)排序時(shí)仔細(xì)檢查你的if-return鏈?zhǔn)欠窀采w了所有情況且邏輯正確。5.2 性能問題結(jié)構(gòu)體過大導(dǎo)致拷貝開銷如果你的結(jié)構(gòu)體非常大例如包含很長(zhǎng)的字符串或數(shù)組成員在比較函數(shù)中按值傳遞參數(shù)會(huì)帶來巨大的性能損失。struct BigData { char data[1024]; int key; }; bool slowCmp(BigData a, BigData b) { return a.key b.key; } // 糟糕每次比較拷貝2KB bool fastCmp(const BigData a, const BigData b) { return a.key b.key; } // 優(yōu)秀只傳引用排查技巧養(yǎng)成習(xí)慣比較函數(shù)的參數(shù)一律使用const T。5.3 Lambda捕獲引用導(dǎo)致懸空引用這是一個(gè)隱蔽的Bug。如果你在Lambda中按引用捕獲了一個(gè)局部變量而這個(gè)Lambda被存儲(chǔ)起來例如賦值給一個(gè)std::function并在局部變量銷毀后被調(diào)用就會(huì)訪問已釋放的內(nèi)存。std::functionbool(const Student, const Student) getComparator() { int threshold 90; // 危險(xiǎn)按引用捕獲了局部變量 threshold auto lambda [threshold](const Student a, const Student b) { return a.score * (a.score threshold) b.score * (b.score threshold); // 假設(shè)的邏輯 }; return lambda; // lambda被返回但threshold即將被銷毀 } // 后續(xù)調(diào)用返回的lambda會(huì)導(dǎo)致未定義行為解決方法如果Lambda的生命周期可能超過被捕獲的局部變量對(duì)于基本類型或小對(duì)象使用按值捕獲 ([threshold])。對(duì)于必須共享的大對(duì)象確保其生命周期覆蓋Lambda的整個(gè)使用期或者使用std::shared_ptr來管理。5.4 多級(jí)排序的優(yōu)先級(jí)順序?qū)懛丛趯懚嗉?jí)排序的if-return鏈時(shí)很容易把優(yōu)先級(jí)的順序搞反。記住最先判斷的條件是最高優(yōu)先級(jí)的排序鍵。// 目標(biāo)先按班級(jí)升序再按分?jǐn)?shù)降序 bool wrongOrder(const Student a, const Student b) { // 錯(cuò)誤先判斷了分?jǐn)?shù)意味著分?jǐn)?shù)是第一優(yōu)先級(jí) if (a.score ! b.score) { return a.score b.score; } // 分?jǐn)?shù)相同才看班級(jí) return a.classId b.classId; } // 這個(gè)函數(shù)實(shí)現(xiàn)的是“先按分?jǐn)?shù)降序再按班級(jí)升序”與目標(biāo)不符。排查技巧在寫多級(jí)排序時(shí)用注釋明確寫出每一級(jí)的規(guī)則并從上到下檢查優(yōu)先級(jí)。5.5 使用std::sort處理降序的簡(jiǎn)便寫法除了自己寫return a b這樣的比較邏輯對(duì)于基本類型或已重載了比較運(yùn)算符的類型可以使用標(biāo)準(zhǔn)庫提供的函數(shù)對(duì)象讓代碼更清晰。std::vectorint vec {5, 2, 8, 1}; // 升序 std::sort(vec.begin(), vec.end()); // 默認(rèn)使用 std::sort(vec.begin(), vec.end(), std::lessint()); // 等價(jià) // 降序 std::sort(vec.begin(), vec.end(), std::greaterint()); // 清晰對(duì)于自定義結(jié)構(gòu)體如果你已經(jīng)重載了operator想按降序排可以這樣// 假設(shè)Student已重載了 operator (按id升序) std::sort(students.begin(), students.end(), [](const Student a, const Student b) { return b a; }); // 通過調(diào)換參數(shù)實(shí)現(xiàn)降序 // 或者定義一個(gè)通用的反向比較器 auto reverseCompare [](const auto a, const auto b) { return b a; }; std::sort(students.begin(), students.end(), reverseCompare);掌握這三種結(jié)構(gòu)體排序方式并理解其背后的原理和陷阱你在處理C中的自定義數(shù)據(jù)排序時(shí)將再無阻礙。核心原則是默認(rèn)規(guī)則用重載靈活多變用Lambda兼容傳統(tǒng)用函數(shù)。在實(shí)際編碼中多思考一下你的排序需求屬于哪種場(chǎng)景選擇最清晰、最安全的方式來實(shí)現(xiàn)你的代碼質(zhì)量會(huì)立刻提升一個(gè)檔次。