??xml version="1.0" encoding="utf-8" standalone="yes"?>
王咏刚,2003q?2?br>
写作本文的初h惛_大家分n垃圾攉Q?Garbage Collection Q技术简单而有的发展双Ӏ动W之前,我站在窗边,望了(jin)望正在小区里装运垃圾的清zR。和生活中环卫工Z清运垃圾的工作相|软g开发里的垃圾收集其实就是一U自动打扫和清除内存垃圾的技术,它可以有效防范动态内存分配中可能发生的两个危险:(x)因内存垃圾过多而引发的内存耗尽Q这和生zd圑֠塞排污管道的危险q没有什么本质的不同Q,以及(qing)不恰当的内存释放所造成的内存非法引用(q类g我们在生zM买到?jin)一瓶已l过期三q的牛奶Q?
据历史学家们介绍Q四千多q前的古埃及(qing)人已l在城市(jng)里徏设了(jin)完善的排污和垃圾清运设施Q一千多q前的中国h更是修筑?jin)当时世界上保洁能力最强的都市(jng) ——长安。今天,当我们在软g开发中体验自动垃圾攉的便捷与舒适时Q我们至应当知道,q种拒绝杂ؕ、追求整z的“垃圾攉”_其实是hc自古以来就已经具备?jin)的?
-------------------------------------------------------------------------------------------------------
国内的程序员大多是在 Java 语言中第一ơ感受到垃圾攉技术的巨大力的,许多Z因此?Java 和垃圾收集看成了(jin)密不可分的整体。但事实上,垃圾攉技术早?Java 语言问世?30 多年已l发展和成熟h?jin)?Java 语言所做的不过是把q项奇的技术带C(jin)q大E序员n边而已?
如果一定要为垃圾收集技术找一个孪生兄弟,那么Q?Lisp 语言才是当之无愧的h选?1960 q前后诞生于 MIT ?Lisp 语言是第一U高度依赖于动态内存分配技术的语言Q?Lisp 中几乎所有数据都?#8220;?#8221;的Ş式出玎ͼ?#8220;?#8221;所占用的空间则是在堆中动态分配得到的?Lisp 语言先天具有的动态内存管理特性要?Lisp 语言的设计者必解军_中每一个内存块的自动释N题(否则Q?Lisp E序员就必然被程序中不计其数?free ?delete 语句Ҏ(gu)Q,q直接导致了(jin)垃圾攉技术的诞生和发展——说句题外话Q上大学Ӟ一位老师曑֑诉我们, Lisp 是对C软g开发技术A(ch)献最大的语言。我当时对这一说法不以为然Q布满了(jin)圆括P看上dq宫一L(fng) Lisp 语言怎么能比 C 语言?Pascal 语言更伟大呢Q不q现在,当我知道垃圾攉技术、数据结构技术、h工智能技术、ƈ行处理技术、虚拟机技术、元数据技术以?qing)程序员们耳熟能详的许多技术都h?Lisp 语言Ӟ我特别想向那位老师当面道歉Qƈ收回我当时的q稚x?
知道?Lisp 语言与垃圾收集的密切关系Q我们就不难理解Qؓ(f)什么垃圾收集技术的两位先驱?J. McCarthy ?M. L. Minsky 同时也是 Lisp 语言发展史上的重要h物了(jin)?J. McCarthy ?Lisp 之父Q他在发?Lisp 语言的同时也W一ơ完整地描述?jin)垃圾收集的法和实现方式?M. L. Minsky 则在发展 Lisp 语言的过E中成ؓ(f)?jin)今天好几种L垃圾攉法的奠Zh——和当时不少技术大师的l历怼Q?J. McCarthy ?M. L. Minsky 在许多不同的技术领域里都取得了(jin)令h艳M的成。也许,?1960 q代那个软g开发史上的拓荒时代里,思维敏捷、意志坚定的研究者更Ҏ(gu)成ؓ(f)无所不能的西部硬汉吧?
在了(jin)解垃圾收集算法的h之前Q有必要先回一下内存分配的主要方式。我们知道,大多C的语言或运行环境都支持三种最基本的内存分配方式,它们分别是:(x)
一、静(rn)态分配( Static Allocation Q:(x)?rn)态变量和全局变量的分配Ş式。我们可以把?rn)态分配的内存看成是家里的耐用家具。通常Q它们无需释放和回Ӟ因ؓ(f)没h?x)天天把大衣柜当作垃圾扔到窗外?
二、自动分配( Automatic Allocation Q:(x)在栈中ؓ(f)局部变量分配内存的Ҏ(gu)。栈中的内存可以随着代码块退出时的出栈操作被自动释放。这cM于到家中串门的访客,天色一晚就要各回各Ӟ除了(jin)个别不识时务者以外,我们一般没必要把客人捆在垃圾袋里扫地出门?
三、动态分配( Dynamic Allocation Q:(x)在堆中动态分配内存空间以存储数据的方式。堆中的内存块好像我们日怋用的巾U,用过?jin)就得扔到垃圄里,否则屋内׃?x)满地D。像我这L(fng)懒h做梦都想有一台家用机器h跟在w边打扫卫生。在软g开发中Q如果你懒得释放内存Q那么你也需要一台类似的机器人——这其实是一个由特定法实现的垃圾收集器?
也就是说Q下面提到的所有垃圾收集算法都是在E序q行q程中收集ƈ清理废旧“巾U?#8221;的算法,它们的操作对象既不是?rn)态变量,也不是局部变量,而是堆中所有已分配内存块?
-------------------------------------------------------------------------------------------------------
1960 q以前,Z胎中?Lisp 语言设计垃圾攉机制ӞW一个想到的法是引用计数算法。拿巾U的例子来说Q这U算法的原理大致可以描述为:(x)
午餐ӞZ(jin)把脑子里H然跛_来的设计灉|C来,我从巾U袋中抽Z张餐巄Q打在上面dpȝ架构的蓝图。按?#8220;巾U怋用规U之引用计数?#8221;的要求,d之前Q我必须先在巾U的一角写上计数?1 Q以表示我在使用q张巾U。这Ӟ如果你也想看看我ȝ蓝图Q那你就要把巾U怸的计数值加 1 Q将它改?2 Q这表明目前?2 个h在同时用这张餐巄Q当?dng)我是不?x)允许你用q张巾U来擦E涕的Q。你看完后,必须把计数值减 1 Q表明你对该巾U的使用已经l束。同P当我餐巄上的内容全部誊写到笔记本上之后,我也?x)自觉地把餐巄上的计数值减 1 。此Ӟ不出意外的话Q这张餐巄上的计数值应当是 0 Q它?x)被垃圾攉器——假N是一个专门负责打扫卫生的机器人——捡h扔到垃圾里Q因为垃圾收集器的惟一使命是扑ֈ所有计数gؓ(f) 0 的餐巄q清理它们?
引用计数法的优点和~陷同样明显。这一法在执行垃圾收集Q务时速度较快Q但法对程序中每一ơ内存分配和指针操作提出?jin)额外的要求Q增加或减少内存块的引用计数Q。更重要的是Q引用计数算法无法正释攑@环引用的内存块,Ҏ(gu)Q?D. Hillis 有一D风而精辟的Q?
一天,一个学生走?Moon 面前_(d)(x)“我知道如何设计一个更好的垃圾攉器了(jin)。我们必记录指向每个结点的指针数目?#8221; Moon 耐心(j)地给q位学生讲了(jin)下面q个故事Q?#8220;一天,一个学生走?Moon 面前_(d)(x)‘我知道如何设计一个更好的垃圾攉器了(jin)……’”
D. Hillis 的故事和我们时候常说的“从前有山,׃有个庙,庙里有个老和?#8221;的故事有异曲同工之妙。这说明Q单是用引用计数算法还不以解军_圾收集中的所有问题。正因ؓ(f)如此Q引用计数算法也常常被研I者们排除在狭义的垃圾攉法之外。当?dng)作?f)一U最单、最直观的解x案,引用计数法本nh其不可替代的优越性?1980 q代前后Q?D. P. Friedman Q?D. S. Wise Q?H. G. Baker {h对引用计数算法进行了(jin)数次改进Q这些改q得引用计数算法及(qing)其变U(如gq计数算法等Q在单的环境下,或是在一些综合了(jin)多种法的现代垃圾收集系l中仍然可以一展n手?
-------------------------------------------------------------------------------------------------------
W一U实用和完善的垃圾收集算法是 J. McCarthy {h?1960 q提出ƈ成功地应用于 Lisp 语言的标讎ͼ清除法。仍以餐巄ZQ标讎ͼ清除法的执行过E是q样的:(x)
午餐q程中,厅里的所有h都根据自q需要取用餐巄。当垃圾攉机器人想攉废旧巾U的时候,它会(x)让所有用的人先停下来,然后Q依ơ询问餐厅里的每一个hQ?#8220;你正在用巾U吗Q你用的是哪一张餐巄Q?#8221;机器人根据每个h的回{将Z正在使用的餐巄M记号。询问过E结束后Q机器h在餐厅里L所有散落在桌上且没有记号的餐巄Q这些显焉是用q的废旧巾U)(j)Q把它们l统扔到垃圾里?
正如其名U所暗示的那P标记Q清除算法的执行q程分ؓ(f)“标记”?#8220;清除”两大阶段。这U分步执行的思\奠定?jin)现代垃圾收集算法的思想基础。与引用计数法不同的是Q标讎ͼ清除法不需要运行环境监每一ơ内存分配和指针操作Q而只要在“标记”阶段中跟t每一个指针变量的指向——用cM思\实现的垃圾收集器也常被后人统UCؓ(f)跟踪攉器( Tracing Collector Q?
伴随着 Lisp 语言的成功,标记Q清除算法也在大多数早期?Lisp q行环境中大攑ּ彩。尽最初版本的标记Q清除算法在今天看来q存在效率不高(标记和清除是两个相当耗时的过E)(j){诸多缺P但在后面的讨ZQ我们可以看刎ͼ几乎所有现代垃圾收集算法都是标讎ͼ清除思想的gl,仅此一点, J. McCarthy {h在垃圾收集技术方面的贡献׃毫不亚于他们?Lisp 语言上的成就?jin)?
-------------------------------------------------------------------------------------------------------
Z(jin)解决标记Q清除算法在垃圾攉效率斚w的缺P M. L. Minsky ?1963 q发表了(jin)著名的论?#8220;一U用双存储区的 Lisp 语言垃圾攉器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage Q?#8221;?M. L. Minsky 在该论文中描q的法被h们称为复制算法,它也?M. L. Minsky 本h成功地引入到?Lisp 语言的一个实现版本中?
复制法别出?j)裁地将堆空间一分ؓ(f)二,q用简单的复制操作来完成垃圾收集工作,q个思\相当有趣。借用巾U的比喻Q我们可以这L(fng)?M. L. Minsky 的复制算法:(x)
厅被垃圾收集机器h分成南区和北Z个大完全相同的部分。午时Q所有h都先在南区用(因ؓ(f)I间有限Q用h数自然也减一半)(j)Q用时可以随意使用巾U。当垃圾攉机器为有必要回收废旧巾U时Q它?x)要求所有用者以最快的速度从南{Ud北区Q同旉w携带自己正在用的巾U。等所有h都{Ud北区之后Q垃圾收集机器h只要单地把南Z所有散落的巾U扔q垃圄q完成d?jin)。下一ơ垃圾收集的工作q程也大致类|惟一的不同只是h们的转移方向变成?jin)从北区到南区。如此@环往复,每次垃圾攉都只需单地转移Q也是复制Q一ơ,垃圾攉速度无与伦比——当?dng)对于用餐者往q奔波于南北两区之间的辛劻I垃圾攉机器人是决不?x)流露出丝毫怜?zhn)的?
M. L. Minsky 的发明绝对算得上一U奇思妙惟뀂分区、复制的思\不仅大幅提高?jin)垃圾收集的效率Q而且也将原本J纷复杂的内存分配算法变得前所未有地简明和DQ既然每ơ内存回攉是对整个半区的回Ӟ内存分配时也׃用考虑内存片{复杂情况,只要Ud堆顶指针Q按序分配内存可以了(jin)Q,q简直是个奇q!不过QQ何奇q的出现都有一定的代h(hun)Q在垃圾攉技术中Q复制算法提高效率的代h(hun)是h为地可用内存羃?yu)?jin)一半。实话实_(d)q个代h(hun)未免也太高了(jin)一些?
无论优缺点如何,复制法在实践中都获得了(jin)可以与标讎ͼ清除法相比拟的成功。除?M. L. Minsky 本h?Lisp 语言中的工作以外Q从 1960 q代末到 1970 q代初, R. R. Fenichel ?J. C. Yochelson {h也相l在 Lisp 语言的不同实C对复制算法进行了(jin)改进Q?S. Arnborg 更是成功地将复制法应用C(jin) Simula 语言中?
xQ垃圾收集技术的三大传统法——引用计数算法、标讎ͼ清除法和复制算法——都已在 1960 q前后相l问世,三种法各有所长,也都存在致命的缺陗从 1960 q代后期开始,研究者的主要_֊逐渐转向对这三种传统法q行改进或整合,以扬镉K短,适应E序设计语言和运行环境对垃圾攉的效率和实时性所提出的更高要求?
-------------------------------------------------------------------------------------------------------
?1970 q代开始,随着U学研究和应用实늚不断深入Qh们逐渐意识刎ͼ一个理想的垃圾攉器不应在q行时导致应用程序的暂停Q不应额外占用大量的内存I间?CPU 资源Q而三U传l的垃圾攉法都无法满些要求。h们必L出更新的法或思\Q以解决实践中碰到的诸多N。当Ӟ研究者的努力目标包括Q?
W一Q提高垃圾收集的效率。用标讎ͼ清除法的垃圾收集器在工作时要消耗相当多?CPU 资源。早期的 Lisp q行环境攉内存垃圾的时间竟占到?jin)系l总运行时间的 40% Q——垃圾收集效率的低下直接造就?Lisp 语言在执行速度斚w的坏名声Q直C天,许多条g反射似地误以为所?Lisp E序都奇慢无比?
W二Q减垃圾收集时的内存占用。这一问题主要出现在复制算法中。尽复制算法在效率上获得了(jin)质的H破Q但牺牲一半内存空间的代h(hun)仍然是巨大的。在计算机发展的早期Q在内存h?KB 计算的日子里Q浪费客L(fng)一半内存空间简直就是在变相敲诈或拦路打劫?
W三Q寻扑֮时的垃圾攉法。无论执行效率如何,三种传统的垃圾收集算法在执行垃圾攉d旉必须打断E序的当前工作。这U因垃圾攉而造成的g时是许多E序Q特别是执行关键d的程序没有办法容忍的。如何对传统法q行改进Q以便实CU在后台(zhn)?zhn)执行Q不影响——或臛_看上M影响——当前进E的实时垃圾攉器,q显然是一件更h战性的工作?
研究者们探寻未知领域的决?j)和研究工作的进展速度同样令h惊奇Q在 1970 q代?1980 q代的短短十几年中,一大批在实用系l中表现优异的新法和新思\脱颖而出。正是因为有?jin)这些日成熟的垃圾攉法Q今天的我们才能?Java ?.NET 提供的运行环境中随心(j)所Ʋ地分配内存块,而不必担?j)空间释放时的风险?
-------------------------------------------------------------------------------------------------------
标记Q整理算法是标记Q清除算法和复制法的有机结合。把标记Q清除算法在内存占用上的优点和复制算法在执行效率上的牚wl合hQ这是所有h都希望看到的l果。不q,两种垃圾攉法的整合ƈ不像 1 ?1 {于 2 那样单,我们必须引入一些全新的思\?1970 q前后, G. L. Steele Q?C. J. Cheney ?D. S. Wise {研I者陆l找C(jin)正确的方向,标记Q整理算法的轮廓也逐渐清晰?jin)v来:(x)
在我们熟(zhn)的厅里,q一ơ,垃圾攉机器Z再把厅分成两个南北区域?jin)。需要执行垃圾收集Q务时Q机器h先执行标讎ͼ清除法的第一个步骤,为所有用中的餐巄d标记Q然后,机器人命令所有就者带上有标记的餐巄向餐厅的南面集中Q同时把没有标记的废旧餐巄扔向厅北面。这样一来,机器人只消站在餐厅北面,怀抱垃圄Q迎接扑面而来的废旧餐巄p?jin)?
实验表明Q标讎ͼ整理法的M执行效率高于标记Q清除算法,又不像复制算法那样需要牺牲一半的存储I间Q这昄是一U非常理想的l果。在许多C的垃圾收集器中,Z都用了(jin)标记Q整理算法或其改q版本?
-------------------------------------------------------------------------------------------------------
对实时垃圾收集算法的研究直接D?jin)增量收集算法的诞生?
最初,Z关于实时垃圾攉的想法是q样的:(x)Z(jin)q行实时的垃圾收集,可以设计一个多q程的运行环境,比如用一个进E执行垃圾收集工作,另一个进E执行程序代码。这样一来,垃圾攉工作看上d仿佛(jng)是在后台(zhn)?zhn)完成的,不?x)打断E序代码的运行?
在收集餐巄的例子中Q这一思\可以被理解ؓ(f)Q垃圾收集机器h在h们用的同时L废弃的餐巄q将它们扔到垃圾里。这个看似简单的思\?x)在设计和实现时(l)Cq程间冲H的N。比如说Q如果垃圾收集进E包括标记和清除两个工作阶段Q那么,垃圾攉器在W一阶段中辛辛苦苦标记出的结果很可能被另一个进E中的内存操作代码修改得面目全非Q以至于W二阶段的工作没有办法开展?
M. L. Minsky ?D. E. Knuth 对实时垃圾收集过E中的技术难点进行了(jin)早期的研IӞ G. L. Steele ?1975 q发表了(jin)题ؓ(f)“多进E整理的垃圾攉Q?Multiprocessing compactifying garbage collection Q?#8221;的论文,描述?jin)一U被后hUCؓ(f)“ Minsky-Knuth-Steele 法”的实时垃圾收集算法?E. W. Dijkstra Q?L. Lamport Q?R. R. Fenichel ?J. C. Yochelson {h也相l在此领域做Z(jin)各自的A(ch)献?1978 q_(d) H. G. Baker 发表?#8220;串行计算Z的实时表处理技术( List Processing in Real Time on a Serial Computer Q?#8221;一文,pȝ阐述?jin)多q程环境下用于垃圾收集的增量攉法?
增量攉法的基仍是传统的标讎ͼ清除和复制算法。增量收集算法通过对进E间冲突的妥善处理,允许垃圾攉q程以分阶段的方式完成标记、清理或复制工作。详l分析各U增量收集算法的内部机理是一件相当繁琐的事情Q在q里Q读者们需要了(jin)解的仅仅是:(x) H. G. Baker {h的努力已l将实时垃圾攉的梦惛_成了(jin)现实Q我们再也不用ؓ(f)垃圾攉打断E序的运行而烦(ch)g(jin)?
-------------------------------------------------------------------------------------------------------
和大多数软g开发技术一Pl计学原理总能在技术发展的q程中v到强力催化剂的作用?1980 q前后,善于在研I中使用l计分析知识的技术h员发玎ͼ大多数内存块的生存周期都比较短,垃圾攉器应当把更多的精力放在检查和清理新分配的内存块上。这个发现对于垃圾收集技术的价值可以用巾U的例子概括如下Q?
如果垃圾攉机器够聪明,事先摸清?jin)餐厅里每个人在用餐时用餐巄的?fn)惯——比如有些h喜欢在用前后各用掉一张餐巄Q有的h喜欢自始至终攥着一张餐巄不放Q有的h则每打一个喷嚏就用去一张餐巄——机器h可以制定出更完善的巾U回收计划,qL在h们刚扔掉巾U没多久把垃圾捡走。这U基于统计学原理的做法当然可以让厅的整z度成倍提高?
D. E. Knuth Q?T. Knight Q?G. Sussman ?R. Stallman {h对内存垃圄分类处理做了(jin)最早的研究?1983 q_(d) H. Lieberman ?C. Hewitt 发表?jin)题?#8220;Z对象寿命的一U实时垃圾收集器Q?A real-time garbage collector based on the lifetimes of objects Q?#8221;的论文。这著名的论文标志着分代攉法的正式诞生。此后,?H. G. Baker Q?R. L. Hudson Q?J. E. B. Moss {h的共同努力下Q分代收集算法逐渐成ؓ(f)?jin)垃圾收集领域里的主技术?
分代攉法通常堆中的内存块按寿命分ؓ(f)两类Q年老的和年ȝ。垃圾收集器使用不同的收集算法或攉{略Q分别处理这两类内存块,q特别地把主要工作时间花在处理年ȝ内存块上。分代收集算法垃圾攉器在有限的资源条件下Q可以更为有效地工作——这U效率上的提高在今天?Java 虚拟Z得到?jin)最好的证明?
-------------------------------------------------------------------------------------------------------
Lisp 是垃圾收集技术的W一个受益者,但显然不是最后一个。在 Lisp 语言之后Q许许多多传l的、现代的、后C的语a已经把垃圾收集技术拉入了(jin)自己的怀抱。随便D几个例子吧:(x)诞生?1964 q的 Simula 语言Q?1969 q的 Smalltalk 语言Q?1970 q的 Prolog 语言Q?1973 q的 ML 语言Q?1975 q的 Scheme 语言Q?1983 q的 Modula-3 语言Q?1986 q的 Eiffel 语言Q?1987 q的 Haskell 语言……它们都先后用了(jin)自动垃圾攉技术。当?dng)每一U语a使用的垃圾收集算法可能不相同,大多数语a和运行环境甚臛_时用了(jin)多种垃圾攉法。但无论怎样Q这些实例都说明Q垃圾收集技术从诞生的那一天v׃是一U曲高和寡的“学院z?#8221;技术?
对于我们熟?zhn)?C ?C++ 语言Q垃圾收集技术一样可以发挥巨大的功效。正如我们在学校中就已经知道的那P C ?C++ 语言本nq没有提供垃圾收集机Ӟ但这q不妨碍我们在程序中使用h垃圾攉功能的函数库或类库。例如,早在 1988 q_(d) H. J. Boehm ?A. J. Demers 成功地实现?jin)一U用保守垃圾收集算法( Conservative GC Algorithmic Q的函数库(参见 http://www.hpl.hp.com/personal/Hans_Boehm/gc Q。我们可以在 C 语言?C++ 语言中用该函数库完成自动垃圾收集功能,必要Ӟ甚至q可以让传统?C/C++ 代码与用自动垃圾收集功能的 C/C++ 代码在一个程序里协同工作?
1995 q诞生的 Java 语言在一夜之间将垃圾攉技术变成了(jin)软g开发领域里最为流行的技术之一。从某种角度_(d)我们很难分清I竟?Java 从垃圾收集中受益Q还是垃圾收集技术本w?Java 的普?qing)而扬名。值得注意的是Q不同版本的 Java 虚拟Z用的垃圾攉机制q不完全相同Q?Java 虚拟机其实也l过?jin)一个从单到复杂的发展过E。在 Java 虚拟机的 1.4.1 版中Qh们可以体验到的垃圾收集算法就包括分代攉、复制收集、增量收集、标讎ͼ整理、ƈ行复Ӟ Parallel Copying Q、ƈ行清除( Parallel Scavenging Q、ƈ发( Concurrent Q收集等许多U, Java E序q行速度的不断提升在很大E度上应该归功于垃圾攉技术的发展与完善?
管历史上已l有许多包含垃圾攉技术的应用q_和操作系l出玎ͼ?Microsoft .NET 却是W一U真正实用化的、包含了(jin)垃圾攉机制的通用语言q行环境。事实上Q?.NET q_上的所有语aQ包?C# ?Visual Basic .NET ?Visual C++ .NET ?J# {等Q都可以通过几乎完全相同的方式?.NET q_提供的垃圾收集机制。我们似乎可以断aQ?.NET 是垃圾收集技术在应用领域里的一ơ重大变革,它垃圾攉技术从一U单U的技术变成了(jin)应用环境乃至操作pȝ中的一U内在文化。这U变革对未来软g开发技术的影响力也许要q远过 .NET q_本n的商业h(hun)倹{?
-------------------------------------------------------------------------------------------------------
今天Q致力于垃圾攉技术研I的Z仍在不懈努力Q他们的研究方向包括分布式系l的垃圾攉、复杂事务环境下的垃圾收集、数据库{特定系l的垃圾攉{等?
但在E序员中_(d)仍有不少人对垃圾攉技术不屑一,他们宁愿怿自己逐行~写?free ?delete 命o(h)Q也不愿把垃圾收集的重Q交给那些在他们看来既蠢又W的垃圾攉器?
我个为,垃圾攉技术的普及(qing)是大势所,q就像生zM(x)来好一h庸置疑。今天的E序员也怼(x)因ؓ(f)垃圾攉器要占用一定的 CPU 资源而对其望而却步,但二十多q前的程序员q曾因ؓ(f)高语言速度太慢而坚持用机器语言写程序呢Q在g速度日新月异的今天,我们是要吝惜那一点儿旉损耗而踟w不前,q是该坚定不Ud站在代码和运行环境的净化剂——垃圾收集的一边呢Q?
本文内容来自|络Q但不知道原创和出处
如果?/span>
A
?/span>
B
?/span>
C
?/span>
D……X
?/span>
Y
?/span>
Z
q?/span>
26
个英文字母,分别{于癑ֈ?/span>
1
?/span>
2
?/span>
3
?/span>
4…?4
?/span>
25
?/span>
26
q?/span>
26
个数|那么我们p得出如下有趣的结论:(x)
努力工作Q?/span>
H+A+R+D+W+O+R+K=8+1+18+4+23+15+18+11=98%
知识Q?/span>
K+N+O+W+L+E+D+G+E=11+14+15+23+12+5+4+7+5=96%
爱情Q?/span>
L+O+V+E=12+5+22+5=54%
好运Q?/span>
L+U+C+K=12+21+3+11=47%
q些我们通常非常看重的东襉K不是最完满的,虽然它们非常重要。那么,I竟什么能使得生活变得圆满Q?/span>
是金钱吗Q?/span>
M+O+N+E+Y=13+15+14+5+25=72%
是领导力吗?
L+E+A+D+E+R+S+H+I+P=12+5+1+4+5+18+19+9+16=89%
是性吗Q?/span>
S+E+X=19+24+5=48%
那么Q什么能使生zd?/span>
100%
得圆满呢Q?/span>
?j)态!
A+T+T+I+T+U+D+E=1+20+20+9+20+21+4+5=100%
正是我们对待工作、生zd态度能够使我们的生活辑ֈ
100%
的圆满!
Ma Jia nan
vector<string> svec;
string s( "MaJianan" ); |
insert()第二个参数Q要被插入的|(j)插入到第一个参敎ͼ指向容器中某个位|的iteratorQ指向的位置的前面?
更ؓ(f)随机的插入操作可以如下实玎ͼ(x)
string s1( "yuanlaishini" );
iter = find( slist.begin(), slist.end(), s1 ); |
find()q回被查扑օ素在容器中的位置Q如果查扑֤败,q回end()iterator.
//slist.push_back( value ){h(hun)于 ?
slist.insert( slist.end(), value );
2Q?在某个位|插入指定数量的元素.
例如Q在vector的开始处插入10个MaJiananQ?
vector<string> svec; svec.insert( svec.begin(), 10, mjn ); |
3Q向容器插入一D范围内的元素:(x)
string sa[3] = { "MaJianan", "yuanlaishini", "blog.sina.com.cn"};
//插入数组中的全部元素
//插入数组中的部分元素 |
4Q通过一对iterator来标记带插入值的范围Q可以是另一个vector
例一Q?br /> //插入svec中含有的元素Q从svec2中间开?br /> svec2.insert( svec2.begin() + svec2.size()/2, svec.begin(), svec.end() );
例二Q?br /> //把sevc中的元素插入到slist中sValue的前?br /> list<string> slist; |
?序容器操作之删除(eraseQ?/strong>
1Q?删除单个元素
list<string> slist; |
2Q?删除有一对iterator标记的一D范围内的元?
//删除所有元?br />slist.erase( slist.begin(), slist.end() );
//删除部分元素
例二Q?br />list<string>::iterator first, last; |
3Q?与push_back()相对应,pop_back()删除容器的末օ素?
?序容器操作之赋?=)和对?swap)
//slist1含有8个元?br />//slist2含有16个元?br />1)如果
slist1 = slist2;
slist1拥有与被拯容器相同的元素数?--16.slist2?6个元素,没有变化?br /> slist1中原来的8个元素被删除(调用string的析构函?
2)如果 slist1.swap( slist2 );
slist1现在?6个元素,而slist2函数slist1中原有的8个元素的拯
如果两个容器长度不同Q则重置容器的长度?
本文中,我将针对q些看似怪异的最?jng)_践阐q我的观点,q简q我对实施这些最?jng)_늚一些思考?/font>
一、计划游?/font>
能够把计划叫?#8220;游戏”是需要一定勇气的。在传统的Y件开发方法学中,计划是D重的一环,在制订计划前需要仔l的估算Q在计划实施的过E中Q还要不停的跟踪、修正,直至整个目完成?/font>
而在XP中,计划g要轻松许多。这q不是因划本w变得草率和无关痛痒Q而是得益于XP的小版本发布思维。Y件的一个版本是如此短小单,以至于对它进行完整的评估、预、跟t和修正要容易许多。事实上QXP中采用的计划方式Q业务h员和开发h员共同参与,各司其职Q在大多数现代Y件企业中早已采用Q但是由于项目过于庞大,很难在开始阶D制订出完善的计划。而在XP中,Z只需要针对一个一两个月的项目进行跟t和理Q无形中降低?jin)计划的风险?/font>
二、隐?/font>
在XP中,Zl常?x)用隐L代替传统开发过E中的体pȝ构设计。从指导开发的角度来说Q隐M乎不够精,Ҏ(gu)让h误解。但是,对于hcM背景的同一个项目组中的开发h员来_(d)隐喻则更便于理解和交。很难想象两个程序员面对着一张庞大的体系l构图时能够真正有效的沟通,而隐d好的解决?jin)这个问题?/font>
三、简单设?/font>
不知道从什么时候开始,开发h员习(fn)惯了(jin)为明天而设计。一个开发h员设计了(jin)一个复杂的cȝ承结构,只是Z(jin)提高E序的所谓灵zL。没人知道这样g|q不是Y件的每一个部分都需要扩展。但是,对于传统的Y件开发h员来_(d)q么做又是迫不得巌Ӏ如果没有预先做好准备,在变化来临时׃(x)措手不及(qing)Q付出沉重的代h(hun)?/font>
但是在XP中,版本发行的Ҏ(gu)使得变化q不那么可怕,而重构的q泛采用Q得代码L可以在需要时变得更加灉|。此外,׃你的代码L?x)被别h审查Q代码集体所有权和结对编E)(j)Q因此也可以避免q于q求单而忽视了(jin)重要的细节?/font>
四、测试优?/font>
没有代码要测试程序有什么用Q这是测试优先最Ҏ(gu)让h误解的地斏V测试优先能够让开发h员更清楚的认识到Q程序将?x)如何被使用。通过对不同的试用例的思考,开发h员也能够更清晰的认识到程序的功能外g。而更多的其他的开发h员,则通过试用例可以获得一份精的使用手册Q在q䆾使用手册中,描述?jin)作者考虑到的所有输入和输出l果Q这样不仅便于h们了(jin)解程序,更增加了(jin)发现E序错误的机?x)(~失的测试用例往往体现Z者忽视的某些使用情况Q?/font>
五、结对编E?/font>
两个E序员坐在一P能够提高开发效率吗Q程序员N不是一高傲的猫,?fn)惯于离?ch)居,把头抬得高高吗?
事实q如此。在一个正的、合理的、能够实现的大目标下Q程序员们不仅能够和q_处,更可以相互合作,创造出优秀的、高质量的程序。沟通一直是软g目理中的一个重要议题,而结对编E提供了(jin)一个十分有效的沟通渠道。此外,l对~程也更Ҏ(gu)让新入团体。在几个高E序员的指引下,他会(x)更容易找出程序的脉络Q把握程序的思想。较之正规的培训Q这U方式更L也更有效。对于团队中的所有程序员来说Q结对编E都是一个了(jin)解其他h设计思想的机?x),通过l对~程Q能够更好的实现代码集体所有权Q也能够降低因ؓ(f)人员?gu)动造成的风险?/font>
l对~程最大的好处在于Q能够极大的减少E序中潜在变化的可能性。两个h通过交流互相交换自己对程序的不同理解Q更Ҏ(gu)扑ևE序中可能出现的变化或错误,从而ɽE序更加可靠和健壮?/font>
六、持l集?/font>
集成一直是最费力的工作之一Q本来工作的好好的代码,攑֜一起就不能q{Q更p糕的是成百上千条不知所云的错误码,没有人知道这些错误码来自何处。这是每个项目几乎都?x)遇到的最困难的阶D,E序员们必须集合在一P阅数量巨大的接口定义文Ӟ反复查看代码Q同时还要不断的做出承诺?/font>
持箋集成正是解决上述问题的方法。通过多次、小增量的集成,我们L能够以最快的速度定位错误出现的位|(因ؓ(f)增加的代码很)(j)Q结合大量测试用例,我们也可以确保每一个集成版本都可能的可靠?/font>
此外Q持l集成几乎可以在M旉向我们提供一个可以工作的版本Q我们可以将q个版本用于内部讨论和测试、客户展C、客h试、小版本发布{等Q这使得我们不需要花费太多的旉对现有的E序修修补补Q以生成一个demo?/font>
上文单叙qC(jin)XP中常?x)引起争议的六个最?jng)_늚优点。下面本文将l合实际谈谈实施XP中需要注意的一些问题?/font>
一、适用性问?/font>
XP理论在提出时Q明的说明QXP是适用于中型团队在需求不明确或者迅速变化的情况下进行Y件开发的轻量U方法。这意味着QXPq不适用于所有情c(din)在准备实施XP前,你也?dng)R要仔l评估项目的具体情况Q以军_是否真的需要采用XP?/font>
二、最?jng)_践间的关?/font>
XP的一个特Ҏ(gu)Q它所推崇的最?jng)_践几乎L和其它实践关联紧密,在实施一Ҏ(gu)?jng)_跉|Q如果不同时实施其它实践Q往往难以辑ֈ最初的目的。因此,在实施XPӞ需要仔l研I各实践间的关联,以确定最佳的实施Ҏ(gu)?/font>
三、宽杄环境
XP是一U追求自然的工作Ҏ(gu)。它所倡导的是Q程序员们以最自然开发的方式完成他们的工作。对于习(fn)惯了(jin)传统开发方法严格管理制度的理人员来说Q这往往是很难接受的。于是就出现?jin),虽然最高决{h军_实施XPQ但理层却无法Q或不愿Q给开发h员提供宽杄环境。在一个古板僵化的Ҏ(gu)里,开发h员不?x)真正的回复自然Q他们会(x)装作正在实践XPQ但事实上,他们依然在老\上行赎ͼ可以见到很多q样的例子,比如一些虚张声势的试用例{等Q?/font>
四、忍受变?/font>
XP对于传统软g目理思想的冲击,可能?x)很多理人员感到不舒服。也许XP一l实施,׃(x)l项目组带来d覆地的变化。如果这L(fng)变化让你感到恐惧Q那么请暂时忍耐,你不能肯定这U变化不好,除非你亲眼看到。到那时再决定也不迟?/font>
五、慢慢来
实施XP的过E不能操之过急。最好的Ҏ(gu)是,在部分项目组中先行实施,实施时也不需要同时实施所有实践(但要注意各个实践间的兌问题Q。有的时候你?x)发玎ͼ在实施?jin)部分实践后,其它的实践也成ؓ(f)水到渠成的事情。当l过仔细评估Q确信XP在项目组中确实有效后Q再逐步在企业范围内推广Q必要的时候,需要采取自愿的原则Q由目l的成员军_是否需要实施XP?/font>
以上x我在软g工程q程评中以?qing)^时工作、学?fn)中对XP的一些认识?/font>
来源Q?/font> http://blog.csdn.net/leasun/archive/2006/08/15/1067508.aspx
关于 sizeof() 的一些思?/font>
马嘉?br />
关键词:(x)
sizeof
q是|上的一个帖子,最初来自那里已l记不得?jin),不过我觉得很不错?/font>
我对原文做了(jin)一些修改,q添加了(jin)一些内宏V如果有什么错误的地方Q请大家指正Q谢谢~~
--- majianan 2005-12-19
0.关键?/font> QsizeofQ字节对齐,cd大小
前向声明Q?/font>
sizeofQ一个其貌不扬的家伙Q引无数菜鸟竟折?
虾我当初也没少犯迷p,U着“辛苦我一个,q福千万人”的伟大思想,我决定将其尽可能详细的ȝ一下?
但当我ȝ的时候才发现Q这个问题既可以单,又可以复杂。所以本文有的地方ƈ不适合初学者,甚至都没有必要大作文章。但如果你想“知其然Q更知其所以然”的话,那么q篇文章对你或许有所帮助?br />
菜鸟我对C++的掌握尚未深入,其中不乏错误Q欢q各位指正啊
1. 定义Q?br /> sizeof是何方神圣?
sizeof ?C/C++ 中的一个操作符QoperatorQ是也。简单说其作用就是返回一个对象或者类型所占的内存字节数?br />
MSDN上的解释为:(x)
The sizeof keyword gives the amount of storage, in bytes, associated with a variable or a type (including aggregate types).This keyword returns a value of type size_t.
其返回值类型ؓ(f)size_tQ在头文件stddef.h中定义。这是一个依赖于~译pȝ的|一般定义ؓ(f)
typedef unsigned int size_t;
世上~译器林林L,但作Z个规范,它们都会(x)保证char、signed char和unsigned char的sizeofgؓ(f)1Q毕竟char是我们编E能用的最数据类型?br />
2. 语法Q?/font>
sizeof有三U语法Ş式,如下Q?br /> 1) sizeof( object ); // sizeof( 对象 );
2) sizeof( type_name ); // sizeof( cd );
3) sizeof object; // sizeof 对象;
所以,
int i;
sizeof( i ); // ok
sizeof i; // ok
sizeof( int ); // ok
sizeof int; // error
既然写法2可以用写?代替Qؓ(f)求Ş式统一以及(qing)减少我们大脑的负担,W?U写法,忘掉它吧Q?
实际上,sizeof计算对象的大也是{换成对对象类型的计算。也是_(d)同种cd的不同对象其sizeof值都是一致的?
q里Q对象可以进一步g伸至表达式,即sizeof可以对一个表辑ּ求倹{编译器Ҏ(gu)表达式的最l结果类型来定大小Q一般不?x)对表达式进行计?
例如Q?
sizeof( 2 ); // 2的类型ؓ(f)intQ所以等价于 sizeof( int );
sizeof( 2 + 3.14 ); // 3.14的类型ؓ(f)doubleQ?也会(x)被提升成doublecdQ所以等价于 sizeof( double );
sizeof也可以对一个函数调用求|?font color="#0000ff">l果是函数返回类型的大小Q函数ƈ不会(x)被调用?/font>我们来看一个完整的例子Q?
*********************************************************
char foo()
{
printf("foo() has been called.\n");
return 'a';
}
int main()
{
size_t sz = sizeof( foo() ); // foo() 的返回值类型ؓ(f)charQ所以sz = sizeof(char)Q但函数foo()q不?x)被调?br /> printf("sizeof( foo() ) = %d\n", sz);
}
*********************************************************
C99标准规定Q函数、不能确定类型的表达式以?qing)位域(bit-fieldQ成员不能被计算sizeof|即下面这些写法都是错误的Q?
sizeof( foo ); // error
void foo2() { }
sizeof( foo2() ); // error
struct S
{
unsigned int f1 : 1;
unsigned int f2 : 5;
unsigned int f3 : 12;
};
sizeof( S.f1 ); // error
3. sizeof的常量?/font>
sizeof的计发生在~译时刻Q所以它可以被当作常量表辑ּ使用。如Q?
char ary[ sizeof( int ) * 10 ]; // ok
最新的C99标准规定sizeof也可以在q行时刻q行计算。如下面的程序在Dev-C++中可以正执行:(x)
int n;
n = 10; // n动态赋?br />char ary[n]; // C99也支持数l的动态定?br />printf("%d\n", sizeof(ary)); // ok. 输出10
但在没有完全实现C99标准的编译器中就行不通了(jin)Q上面的代码在VC6中就通不q编译。所以我?font color="#0000ff">最好还是认为sizeof是在~译期执行的Q这样不?x)带来错误,让程序的可移植性强些?
4. 基本数据cd的sizeof
q里的基本数据类型指short、int、long、float、doubleq样的简单内|数据类型。由于它们都是和pȝ相关的,所以在不同的系l下取值可能不同。这务必引v我们的注意,量不要在这斚wl自q序的UL造成ȝ(ch)?
一般的Q在32位编译环境中Qsizeof(int)的取gؓ(f)4?
5. 指针变量的sizeof
学过数据l构的你应该知道指针是一个很重要的概念,它记录了(jin)另一个对象的地址?font color="#0000ff">既然是来存放地址的,那么它当然等于计机内部地址ȝ的宽度?/font>所以在32位计机中,一个指针变量的q回值必定是4Q注意结果是以字节ؓ(f)单位Q。可以预计,在将来的64位系l中指针变量的sizeofl果??
*********************************************************
char* pc = "abc";
int* pi;
string* ps;
char** ppc = &pc;
void (*pf)(); // 函数指针
sizeof( pc ); // l果?
sizeof( pi ); // l果?
sizeof( ps ); // l果?
sizeof( ppc );// l果?
sizeof( pf ); // l果?
*********************************************************
指针变量的sizeofg指针所指的对象没有M关系Q正是由于所有的指针变量所占内存大相{,所以MFC消息处理函数使用两个参数WPARAM、LPARAMp传递各U复杂的消息l构Q用指向结构体的指针)(j)?
6. 数组的sizeof
数组的sizeof值等于数l所占用的内存字节数Q如Q?
char a1[] = "abc";
int a2[3];
sizeof( a1 ); // l果?Q字W?末尾q存在一个NULLl止W?br />sizeof( a2 ); // l果?*4=12Q依赖于intQ?
一些朋友刚开始时把sizeof当作?jin)求数组元素的个敎ͼ现在Q你应该知道q是不对的。那么应该怎么求数l元素的个数呢?
EasyQ通常有下面两U写法:(x)
int c1 = sizeof( a1 ) / sizeof( char ); // 总长?单个元素的长?br />int c2 = sizeof( a1 ) / sizeof( a1[0]); // 总长?W一个元素的长度
写到q里Q提一问,下面的c3Qc4值应该是多少呢?
*********************************************************
void foo3(char a3[3])
{
int c3 = sizeof( a3 ); // c3 ==
}
void foo4(char a4[])
{
int c4 = sizeof( a4 ); // c4 ==
}
*********************************************************
也许当你试图回答c4的值时已经意识到c3{错?jin),是的Qc3!=3?
q里函数参数a3已不再是数组cdQ而是蜕变成指针?/font>相当于char* a3Qؓ(f)什么仔l想惛_不难明白?
我们调用函数foo1ӞE序?x)在栈上分配一个大ؓ(f)3的数l吗Q不?x)?
数组是“传址”的Q调用者只需实参的地址传递过去,所以a3自然为指针类型(char*Q,c3的g׃ؓ(f)4?
7.string的sizeof
一个string的大与它所指向的字W串的长度无?/font>?br />
*********************************************************
string st1("blog.sina.com.cn");
string st2("majianan");
string st3;
string *ps = &st1;
cout << "st1: " << sizeof(st1) << endl;
cout << "st2: " << sizeof(st2) << endl;
cout << "st3: " << sizeof(st3) << endl;
cout << "ps: " << sizeof(ps) << endl;
cout << "*ps: " << sizeof(*ps) << endl;
*********************************************************
输出l果为:(x)
st1Q?28
st2Q?28
st3Q?28
psQ?4
*psQ?28
*********************************************************
对于不同的STLQStringcȝl构定义?x)有所不同
所以不同的工具Q例如VC++Q和.NETQ结果会(x)有所不同Q?br />在VC++6.0中(我的机器Q结果是16
?NET2003中结果是28
但是对于同一个编译器Q那么它的结果都是一定的
8.引用的sizeof
sizeof操作W应用在引用cd上的时候,q回的是包含被引用对象所需的内存长度(卌引用对象的大)(j)
*********************************************************
cout << "short:\t" << sizeof(short) << endl;
cout << "short*:\t" << sizeof(short*) << endl;
cout << "short&:\t" << sizeof(short&) << endl;
cout << "short[4]:\t" << sizeof(short[4]) << endl;
cout << "int&:\t" << sizeof(int&) << endl;
*********************************************************
输出l果为:(x)
shortQ?2
short*Q?4
short&Q?2
short[4]Q?8
int&Q?4
9. l构体的sizeof
q是初学者问得最多的一个问题,所以这里有必要多费点笔墨。让我们先看一个结构体Q?
struct S1
{
char c;
int i;
};
问sizeof(s1){于多少Q?
聪明的你开始思考了(jin)Qchar?个字节,int?个字节,那么加v来就应该??
是这样吗Q?
你在你机器上试过?jin)吗Q?
也许你是对的Q但很可能你是错的!
VC6中按默认讄得到的结果ؓ(f)8?br />
WhyQؓ(f)什么受伤的L我?
请不要沮丧,我们来好好琢一下sizeof的定?—?sizeof的结果等于对象或者类型所占的内存字节数。好吧,那就让我们来看看S1的内存分配情况:(x)
S1 s1 = { 'a', 0xFFFFFFFF };
定义上面的变量后Q加上断点,q行E序Q观察s1所在的内存Q你发现?jin)什么?
以我的VC6.0ZQs1的地址?x0012FF78Q其数据内容如下Q?
0012FF78: 61 CC CC CC FF FF FF FF
发现?jin)什么?怎么中间Ҏ(gu)?个字节的CCQ?
看看MSDN上的说明Q?
When applied to a structure type or variable, sizeof returns the actual size, which may include padding bytes inserted for alignment.
原来如此Q这是传说中的字节寚w啊!一个重要的话题出现?jin)?
Z么需要字节对齐?
计算机组成原理教导我们,q样有助于加快计机的取数速度Q否则就得多花指令周期了(jin)?
为此Q编译器默认?x)对l构体进行处理(实际上其它地方的数据变量也是如此Q,让宽度ؓ(f)2的基本数据类型(short{)(j)都位于能?整除的地址上,让宽度ؓ(f)4的基本数据类型(int{)(j)都位于能?整除的地址上?/font>以此cLQ这P两个C间就可能需要加入填充字节,所以整个结构体的sizeof值就增长?jin)?
让我们交换一下S1中char与int的位|:(x)
struct S2
{
int i;
char c;
};
看看sizeof(S2)的结果ؓ(f)多少Q怎么q是8?
再看看内存,原来成员c后面仍然?个填充字节?
q又是ؓ(f)什么啊Q别着急,下面ȝ规律?
字节寚w的细节和~译器实现相养I但一般而言Q满三个准则:(x)
1) l构体变量的首地址能够被其最宽基本类型成员的大小所整除Q?br /> 2) l构体每个成员相对于l构体首地址的偏U量QoffsetQ都是成员大的整数倍,如有需要编译器?x)在成员之间加上填充字节Qinternal addingQ;
3) l构体的dؓ(f)l构体最宽基本类型成员大的整数倍,如有需要编译器?x)在最末一个成员之后加上填充字节(trailing paddingQ?br />
对于上面的准则,有几炚w要说明:(x)
1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏U量?jin)呢Q?
因ؓ(f)有了(jin)W?点存在,所以我们就可以只考虑成员的偏U量Q这h考v来简单。想想ؓ(f)什么?
l构体某个成员相对于l构体首地址的偏U量可以通过宏offsetof()来获得,q个宏也在stddef.h中定义,如下Q?
#define offsetof(s,m) (size_t)&(((s *)0)->m)
例如Q想要获得S2中c的偏U量Q方法ؓ(f)
size_t pos = offsetof(S2, c);// pos{于4
2) 基本cd是指前面提到的像char、short、int、float、doubleq样的内|数据类型。这里所说的“数据宽度”就是指其sizeof的大。由于结构体的成员可以是复合cdQ比如另外一个结构体Q所以在L最宽基本类型成员时Q应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合cd作ؓ(f)整体看待?
q里叙述h有点拗口Q思考v来也有点挠头Q还是让我们看看例子吧(具体数g以VC6ZQ以后不再说明)(j)Q?
struct S3
{
char c1;
S1 s;
char c2;
};
S1的最宽简单成员的cd为intQS3在考虑最宽简单类型成员时是将S1“打散”看的,所以S3的最宽简单类型ؓ(f)int。这P通过S3定义的变量,其存储空间首地址需要被4整除Q整个sizeof(S3)的g应该?整除?
c1的偏U量?Qs的偏U量呢?q时s是一个整体,它作为结构体变量也满_面三个准则,所以其大小?Q偏U量?Qc1与s之间侉K?个填充字节,而c2与s之间׃需要了(jin)Q所以c2的偏U量?2Q算上c2的大ؓ(f)13Q?3是不能被4整除的,q样末尾q得补上3个填充字节。最后得到sizeof(S3)的gؓ(f)16?
通过上面的叙qͼ我们可以得到一个公式:(x)
l构体的大小{于最后一个成员的偏移量加上其大小再加上末填充字节数目Q?/strong>卻I(x)
sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( trailing padding )
辛辛苦苦,q舒服日?
舒舒服服,q辛苦日?