League Director is a tool for staging and recording videos from League of Legends replays.
]]>奇瑞万达需要实现快速选择、对比、修改端子功能端子查询功能,做一下项目的总结。
删除端子实体时需要一个识别标记,我选择加入扩展数据作为标识符:
appName
const CString appName = L"SelectXDataApp"; |
acutBuildList()
创建链表时前两个默认为 APP 类型和 APP 名称,后面也是以 type-value
的形式两两创建链表数据。
现在的业务场景需要在弹出模态对话框之后再返回 CAD 选择实体(也就是端子),再重新显示对话框。
最开始我的处理办法是将模态对话框改为非模态对话框,但在对话框的生命周期控制上很难把控,问题比较多,所以目光又重新回到如何使用模态对话框处理这类场景。
在模态对话框中实现用户和AutoCAD 的交互操作 这篇文章提到使用 BeginEditorCommand()
这个方法去从模态对话框切换到 CAD 应用程序,下面是 官方文档 的描述:
Call this method to indicate an AutoCAd interactive command is starting.
还贴心的给了一个使用示例:
BeginEditorCommand(); |
BeginEditorCommand
函数用于将控制权(焦点)交给 CAD,一般用于开始一个交CompleteEditorCommand
函数用于从一个在 CAD 中完成的交互命令返回到应用CancelEditorCommand
函数用于从一个在 CAD 中被取消的交互命令返回到应用程这三个函数组合使用,能够在模态对话框中实现用户和 CAD 的交互操作。
ads_point point{ 0 }; |
奇瑞万达方面需要在选择所有端子后所有都高亮显示,并且能够显示出各自的夹点。
一开始不知道高显这个效果该如何实现,尝试直接调用实体的 highlight()
方法高亮。这种方式没有夹点,在缩放后也没有虚化边框那样明显的视觉效果,客户方面不接受这样的高显只能另寻他法。后面在网上查阅到 ARX亮显问题,里面提到了使用 acedSSSetFirst
这个方法。
由于之前从来没用过这个方法,本着严谨的态度去 官方文档 上又查了一下如何使用。
int acedSSSetFirst( |
pset
:Set of entities to be added to the pickfirst selection set and on which grips will be displayedunused
:IgnoredThis function sets which objects are selected and gripped.
The parameters have the following data type definition:
typedef long ads_name[2];
The selection set of objects specified by the
gset
argument are gripped, and the selection set of objects specified bypset
are both gripped and selected. If any objects are common to both selection sets,acedSSSetFirst()
grips and selects the selection set specified bypset
only (it does not grip thegset
set).If
gset
isNULL
andpset
is specified,acedSSSetFirst()
grips and selectspset
. Ifgset
andpset
areNULL
,acedSSSetFirst()
turns off any existing grips and selections.You are responsible for creating a valid selection set. For example, you may need to verify that a background paper space viewport (DXF group code 69) is not included in the selection set. You may also need to ensure that selected objects belong to the current layout.
Note The
addCommand()
optional flagsACRX_CMD_USEPICKSET
andACRX_CMD_REDRAW
must be used in order foracedSSSetFirst()
to work.Do not call
acedSSSetFirst()
when AutoCAD is in the middle of executing a command.
照着人家给的示例代码照猫画虎,也算达到效果了:
ads_name ssName, ssTemp; |
注意启动命令要设置为 ACRX_CMD_REDRAW | ACRX_CMD_USEPICKSET
,acedSSSetFirst
可以控制加点或者选择的显示,但要注意注册命令的参数。
acedRegCmds->addCommand(_T("xxxxxx"), _T("xxx"), _T("xxx"), ACRX_CMD_TRANSPARENT | ACRX_CMD_USEPICKSET | ACRX_CMD_REDRAW, function); |
奇瑞万达业务上需要下拉框有记忆功能(即下拉框顺序需要以 最近最少使用 的原则去缓存),在 LeetCode 中也有类似的题目(146. LRU 缓存)。
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量 capacity
初始化 LRU 缓存int get(int key)
如果关键字 key
存在于缓存中,则返回关键字的值,否则返回 -1
。void put(int key, int value)
如果关键字 key
已经存在,则变更其数据值 value
;如果不存在,则向缓存中插入该组 key-value
。如果插入操作导致关键字数量超过 capacity
,则应该 逐出 最久未使用的关键字。函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入 |
提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 10^5
2 * 10^5
次 get
和 put
class LRUCache { |
思路:
put
),或者访问过(get
),就算新鲜,就需要 splice
到链表头pop_back()
,链表节点越往后,越陈旧class LRUCache { |
代码要领:
map
中保存的是 <key, 链表节点的指针>
,这样查找的时候就不用需要去遍历链表了,使用 unordered_map
就能很快找到链表节点指针std::list::size()
方法,在 c++ 里,这个方法可能不是 O(1)
的。记录博主个人使用 OBS Studio 直播的心得体会。
截取一段维基百科的概述:
OBS 是一个用于录制和进行网络直播的自由开源软件包。OBS 使用 C 和 C++ 语言编写,提供实时源和设备捕获、场景组成、编码、录制和广播。数据传输主要通过实时消息协议 RTMP 完成,可以发送到任何支持RTMP的软件,包括 YouTube、Twitch、Instagram 和 Facebook 等流媒体网站。
OBS 相比各大平台自己的直播软件功能更强大,也能在几乎所有平台上直播,很多全职主播的直播画面都是通过 OBS 精心调配的
对于我们使用者来讲,需要着重关注它的 开源 特性:
万事开头难,OBS 有四种不同的下载途径。
从官方网站下载软件虽然很简单,但是我认为这是现代人类上网冲浪的基本修养,请不要在从莫名其妙还带着色情赌博诈骗广告的网站上下载东西了!
进入 OBS官网,选择自己的平台下载即可,这里我也是直接给出下载链接:
Windows MacOS (Intel) macOS (Apple Silicon) Linux前段时间 OBS 登陆了 steam,没听错,你甚至可以在 steam 上下载 OBS Studio:
在 steam 上直接搜索 OBS Studio ,进入页面点击获取即可:
作为开源软件怎么能没有大名鼎鼎的 github 呢?github 搜索 OBS Studio 进入 Release 界面,选择自己需要的平台和版本下载最原汁原味的软件安装包:
需要注意的是如果没有魔法,下载速度应该比较慢,如果只是普通使用者不推荐这种下载方式。
既然 OBS 是开源软件,但没有魔法下载又很慢,那为什么不问问神奇的 清华大学开源软件镜像站 呢?
搜索 OBS 选择并下载:
ダウンロードせずに使える「OBS用デジタル時計」をオンライン上に設置しました。
htmlの書き換えが不要な場合は以下のURLをOBSで読み込んでご利用ください。
ダウンロード版はこちら
- Go to
/lol
- Add your League Of Legends account. You must change your profile icon with the one provided by LoboBot.
/lol
- If all goes well, you will see a message saying that your account has been added successfully.
- Click on the account and you will see a both queues (Ranked Solo and Ranked Flex). Select the queue you want to show.
单/双排位
和 灵活组排
的战绩,选择你想展示的战绩
- Click on the desired box, customize it and finally click on the Create Box button.
- Use the link to add the box to your stream.
Create Box
- Once you have created the box, you will see a link to add the box to your stream.
- Copy the link and create a Browser Source in OBS sources.
- Paste the link in the URL field. Keep default settings and click on OK.
后面两种方式我自己没有尝试过,应该都是通过插入浏览器源的方式,故不做翻译了。
- Go to lobobot.com/spotify
- Click on the Connect Spotify button.
- You will be redirected to Spotify to login and authorize LoboBot to access your account.
- Once you have authorized LoboBot, you will be redirected back.
- In options, you can customize the box and finally click on the Save Options button.
- Use the link to add the box to your stream.
- Once you have created the box, you will see a link to add the box to your stream.
- Copy the link and create a Browser Source in Streamlabs Desktop sources.
- Paste the link in the URL field. Keep default settings and click on OK.
部分笔记本使用了混合输出技术,需要把 OBS 设置为核显输出才能正确捕获内容。
]]>R3nzskin 这个项目我关注了小半年,也算是见证了这个开源项目的兴衰。深夜有感,决定写一篇博文来记录一下我的所见所闻,也希望能给其他开源项目和开发者一些启发和经验。
我本人是游戏《英雄联盟》(League of Legends) 的狂热玩家,从大学开始就很喜欢玩这个游戏,也从中收获了很多快乐。
然而秉持着 “差生文具多” 的原则,我在腾讯代理的国服和拳头公司的直营服都为英雄买了很多皮肤,但我并没有足够的经济能力去购买所有我想要和喜欢的皮肤,恰好听闻现在有许多 “换肤” 软件可以使用(老玩家的话可能知道以前的多玩盒子),便踏上了使用换肤软件的道路。
最开始我找到的是大名鼎鼎的 LOLskin:
坦诚来说,它的使用并不方便,但是有如此海量的皮肤选择对于我而言已经足够了,更何况这个换肤软件本身就是免费的,我的钱包也终于能松一口气。
然而 LOLSkin 有一个问题:每次游戏版本更新它都必须重新从官网去下载,国服玩家还必须在更新后去等官网的 .1
版本,还是比较繁琐的。作为懒人代表,我把目光放到了淘宝,寄希望于消费获取比较稳定不需要每个游戏版本都要重新折腾的换肤软件。
淘宝网上这类换肤盒子软件眼花缭乱,我尝试了两个买量比较多的店铺,使用上还是比较复杂。共性上两款软件都需要通过远程服务器验证后才可以使用,付费模式就是购买体验卡包月、季、年这样,换肤模式仍是游戏开始前提前手动选择好需要的皮肤。 这种模式我也不是很喜欢,所以还是换回了 LOLskin 使用,同时也逛逛相关的帖子看看有什么比较好的换肤软件。
转机在有一天某位吧友提到了 R3nzskin 换肤效果不错,我就去搜索了一下:
居然还是开源项目,作为程序员那必须上手体验一番了。
C++ 大师侯捷在《Effective C++》中曾经提到,许多新技术得以推广应用,除了发明者的辛劳付出,也离不开许多布道者用通俗易懂的方式去教授讲解。
本着开源共享的精神,我找到了 R3nzskin 的贴吧,发了一篇手把手教如何使用 R3nzskin 的教学帖:
截止到写这篇博文为止,这个教学贴拥有 21w 阅读量、658 条回帖以及 227 个点赞,可以肯定的是这个帖子帮助了很多和我一样的人,并且让这个开源项目能够被更多人所熟知。
不仅如此,事实上我也花了一个下午去做了详细的使用教学视频投稿到 bilibili,然而不到半个小时审核就把我的视频下架了,理由是破坏计算机安全:
不过吧主制作了类似的视频用以教学,只是我的视频第一次下架的理由是涉及宣传和广告还是令人啼笑皆非,一个开源软件又哪里有广告呢?
随着许多朋友的共同努力,R3 在换肤已经小有名气,我有时间也会解答许多朋友遇到的常见问题例如缺失 dll、版本不匹配等等。有人在吧里收集皮肤 bug 提 issue,也有开发直接提交 pr 帮助完善这个项目,我自己也抽时间看了一些项目的源码,但逆向这块技术栈确实不太了解而且工作比较忙,所以后面就没再研究了。
项目中也加入了国人开发者 rainzee 支持国服版本,总之似乎一切都在向好的方向去发展。
与此同时淘宝店家自然不能放过这种赚钱的机会,他们把 R3 免费开源的软件套壳成之前那种需要服务器远程验证付费的样子去倒卖给那些无知的人,吧里也出现了一些倒狗。
R3 很快被腾讯盯上了,也可以理解,毕竟断人财路如杀人父母,皮肤可以说占这个游戏 95%+ 的收入都不为过,在国服换肤自然不可能放过(使用 LOLSkin 也有一定概率受到处罚)。 很多人开始抱怨不能在国服使用,但也只是埋怨腾讯,而且恰逢当时拳头直营的台服刚开服,所以许多人跑去外服接着用,也没什么大问题。
大洪水源自 R3 项目原作者需要服兵役,可能未来不会频繁更新项目。
许多朋友感到惋惜,这么好用的软件以后没有人维护,我也给作者发了一封邮件去交流这个项目关于技术上的一些问题,看能不能接手下来继续维护,但是作者一直没有回复我,只好作罢。
一部分开发者开始自救,在 R3 的基础上创建仓库开始自己维护这个项目,其中不乏上面的淘宝店家混入其中推广自己套壳的付费换肤。
压死骆驼的最后一根稻草是拳头的一次更新(v13.5
),这次更新让之前用旧版本的 R3 玩家在角色死亡后换肤失效:
无数质疑涌向了 R3,倒狗们宣传着 “自己开发 稳定维护” 的套壳 R3,许多人转而投向这些倒狗的怀抱,同时控诉着 R3 不作为。
R3 后续还是更新了,但是存在使用时严重掉帧的问题,不能像之前一样正常使用。
R3 的国服开发者 rainzee 加入了 R3 贴吧,决定把自己改过的不掉帧稳定的修改版本分享给大家,但出于对套壳奸商的警惕决定也采用服务器发放许可的方式,虽然许可获取是免费的,但这恰恰成为了许多人攻击的源头。
他们认为 rainzee 就是没赚到钱所以不维护 R3 项目的后续,将开源分享的精神理念和和心怀感恩体谅的情感抛之脑后,对参与 R3 项目的开发者当作倒狗肆意谩骂,更有甚者对着那些主动无偿维护、自愿汉化的贡献者谩骂侮辱,这可能是我自参与开源社区以来见到的最荒谬的事情了。
他们赢了,他们也输了。
rainzee 发了声明,选择离开了这里,又一位理想主义者离我们而去,也让我想起之前看到的 开源人宣言:
开源人宣言 - Open Source Fans Manifesto
我们是一群开源的爱好者与信仰者,我们相信:开源代表着一种向善的力量!作为一场席卷全球的世界性运动,20 多年来的历史证明,开源不仅仅能够孕育最新的技术、创造更好的软件,更能够帮助这个世界变得更好。
开源精神
剖析开源的内涵,理解开源的精神,能够让我们理解,为何开源能够让世界变得更好。在我们看来,开源的精神体现在以下一些方面:
分享(Sharing)
当一个软件工程师写出一个不错的软件,他不会敝帚自珍,不会故步自封。他乐于分享,是因为他相信:这个软件可能会对别人也有帮助,更会有人帮助他,一起做出更好的软件。西谚有云:赠人玫瑰,手留余香。我们都相信:乐于分享是一切善举的开端。
开放(openness)
在很多方面,开放都非常重要。不仅仅是开放源代码,更包括公开透明的社区。这样的社区能够吸引更多的朋友加入。也能够帮助新来者,理解并认同社区规则。还能够促进监督以提升社区运行的程序正义。开放还包括欢迎一切的可能性,开源是世界的,也欢迎来自世界任何一个角落的使用者、参与者和贡献者。中国谚语有云:海纳百川,有容乃大。我们都相信:公开透明是一切良好协作的基石。
平等(Equality)
我们欢迎任何人的任何贡献,我们以统一的标准平等地评审每一次代码或文档提交,我们评审的仅仅是代码或文档本身的质量与价值,而不是以贡献者的学历、年龄、种族、性别或职位等标准来判断。人皆生而平等,所以我们都相信:对于平等的追求是社区健康的保障。
协作(Collaboration)
开源社区的协作,正是从接纳点滴贡献开始的,一个开放的社区,崇尚开放式的协作。这样的协作,不会在整个群体达成所有共识之后再开始,而是欢迎来自每一个人的一点一滴的改进。中国古语有云:不积小流,无以成江海。我们都相信:开放式协作,逐步凝聚共识是社区繁荣的秘诀。
创造美好世界(Build a better world)
每一位投身开源的朋友,都或多或少是理想主义者。我们都相信:这个并不完美的世界,理应变得更好。我们都相信:通过自己掌握的技术,借助开源的方法,能够把这个世界变得更好。我们更加相信:开源的精神内涵,应该被推广到更多的领域。因为:创造更加美好的世界,是开源的终极追求。
行动倡议
开源社区的朋友都相信从我做起的力量,因此,我们发出如下行动倡议:
推而广之(Advocate widely)
我们应该更加努力的向大众传播开源的理念与精神,让更多的人接受开源的理念,成为开源的同道中人。我们还应该在开源软件、开源硬件之外的领域,推广开源的实践,不仅仅是开放源代码,还应该开放数据、开放知识、开放一切可以帮助这个世界变得更好的知识与经验,让更多的行业、更多的群体,都接纳开源,成为开放式协作的受益者。
互帮互助(Help each other)
我们应该帮助更多的开源项目,不断发展成长。帮助各个开源社区,把社区的力量团结起来,共同协作。我们还应该防止开源的含义被滥用或曲解。我们要阻止割裂,反对人为设置的障碍,反对任何附加歧视条款的“伪开源”,确保开源始终是一项惠及全球的事业。
立即行动(Just do it)
每一个人都可以参与开源,而不是只有大咖才能做到。我们可以从翻译或撰写文档,纠正拼写做起,为代码除错,审核代码,提交代码,志愿支持开源活动,我们还可以布道演讲,吸引更多的朋友加入。
我不知道这样的环境之后还会有多少开源项目会因此毁掉,我只知道拥抱开源共享的理念和心怀感恩体谅的情感目前还为时尚早。
]]>深入 C++ 的诸多设计细节,了解实际场景的最佳实践,以面向对象的方式重新认识 C++。
Item 1: View C++ as a federation of languages
最初,C++ 只是 C 语言加上一些面向对象的特性,所以 C++ 的原名是 “C with Classes”。 现在的 C++ 已经逐渐成熟,成为一门 多范式的程序设计语言(multiparadigm programming language)。同时支持过程式、面向对象、函数式、泛型编程,以及元编程。
C++ 的灵活使得它在很多问题上并没有统一的规则,而是取决于具体的程序设计范式和当前架构的设计意图。这样的情况下,我们最好把 C++ 看做是一系列的编程语言,而非一种特定的编程语言。
C++ 有四种主要的子语言:
C
:C++ 是基于 C 设计的,你可以只使用 C++ 中 C 的那部分语法。此时你会发现你的程序反映的完全是C的特征:没有模板、没有异常、没有重载。 Object-Oriented C++
:面向对象程序设计也是 C++ 的设计初衷:构造与析构、封装与继承、多态、动态绑定的虚函数。 Template C++
:这是 C++ 的泛型编程部分,多数程序员很少涉及,但模板在很多情况下仍然很方便。另外 模板元编程(template metaprogramming)也是一个新兴的程序设计范式,虽然有点非主流。 STL
:这是一个特殊的模板库,它的容器、迭代器和算法优雅地结合在一起,只是在使用时你需要遵循它的程序设计惯例。当然你也可以基于其他想法来构建模板库。总之 C++ 并非单一的一门语言,它有很多不同的规则集。因而C++可以被视为四种主要子语言的集合,每个子语言都有自己的程序设计惯例。
C++ 程序设计的惯例并非一成不变,而是取决于你使用C++语言的哪一部分。例如, 在基于C语言的程序设计中,基本类型传参时传值比传引用更有效率。 然而当你接触Object-Oriented C++时会发现,传常量指针是更好的选择。 但是你如果又碰到了STL,其中的迭代器和函数对象都是基于C语言的指针而设计的, 这时又回到了原来的规则:传值比传引用更好。
define
Item 2: Prefer consts, enums, and inlines to #defines
尽量使用常量、枚举和内联函数,代替 #define
。我们知道 #define
定义的宏会在编译时进行替换,属于模块化程序设计的概念。 宏是全局的,面向对象程序设计中破坏了封装。因此在 C++ 中尽量避免它!
接着我们具体来看 #define
造成的问题。
众所周知,由于预处理器会直接替换的原因,宏定义最好用括号括起来。#define函数将会产生出乎意料的结果:
|
i
自加次数将取决于 j
的大小,然而调用者并不知情。宏的行为不易理解,本质上是因为宏并非 C++ 语言的一部分,它只是源代码的预处理手段。
宏替换发生在编译时,语法检查之前。因此相关的编译错误中不会出现宏名称,我们不知道是哪个宏出了问题。例如:
|
如果 alice
未定义,PERSON=bob;
便会出错:use of undeclared identifier ‘alice’。 然而我们可能不知道 alice
是什么东西,PERSON
才是我们定义的“变量”。
宏替换是在预处理过程中进行的,原则上讲编译器不知道宏的概念。然而,在现代的编译器中(例如Apple LLVM version 6.0), 编译器会记录宏替换信息,在编译错误中给出宏的名称:
test.cpp:8:5: error: use of undeclared identifier 'alice' |
于是,Meyers 提到的这个问题已经不存在了。然而作者的本意在于:尽量使用编译器,而不是预处理器。 因为 #define
并不是 C++ 语言的一部分。
enum
比 const
更好用既然 #define
不能封装在一个类中,我们可以用 static const
来定义一个常量,并把它的作用于局限在当前类:
class C{ |
通常 C++ 要求所有的声明都给出定义,然而数值类型(char
, int
, long
)的静态常量可以只给声明。这里的 NUM
就是一个例子。 然而,如果你想取 NUM
的地址,则会得到编译错误:
Undefined symbols for architecture x86_64: |
因此如果你要取地址,那么就给出它的定义:
class C{ |
因为声明 NUM
时已经给定了初始值,定义时不允许再次给初始值。 如果使用 enum
,事情会简单很多:
class C{ |
Item 3: Use const whenever possible
尽量使用常量。不需多说,这是 防卫型(defensive)程序设计的原则, 尽量使用常量限定符,从而防止客户错误地使用你的代码。
总结一下各种指针的声明方式吧:
char greeting[] = "Hello"; |
const
出现在 *
左边则被指向的对象是常量,出现在 *
右边则指针本身是常量。 然而对于常量对象,有人把 const
放在类型左边,有人把 const
放在 *
左边,都是可以的:
void f1(const Widget *pw); // f1 takes a pointer to a constant Widget object |
STL 的 iterator
也是类似的,如果你希望指针本身是常量,可以声明 const iterator
; 如果你希望指针指向的对象是常量,请使用 const_iterator
:
std::vector<int> vec; |
返回值声明为常量可以防止你的代码被错误地使用,例如实数相加的方法:
const Rational operator*(const Rational& lhs, const Rational& rhs); |
当用户错误地使用 =
时:
Rational a, b, c; |
编译器便会给出错误:不可赋值给常量。
声明常量成员函数是为了确定哪些方法可以通过常量对象来访问,另外一方面让接口更加易懂: 很容易知道哪些方法会改变对象,哪些不会。
成员方法添加常量限定符属于函数重载。常量对象只能调用常量方法, 非常量对象优先调用非常量方法,如不存在会调用同名常量方法。 常量成员函数也可以在类声明外定义,但声明和定义都需要指定 const
关键字。 例如:
class TextBlock { |
比特常量(bitwise constness):如果一个方法不改变对象的任何非静态变量,那么该方法是常量方法。 比特常量是 C++ 定义常量的方式,然而一个满足比特常量的方法,却不见得表现得像个常量, 尤其是数据成员是指针时:
class TextBlock{ |
因为 char* text
并未发生改变,所以编译器认为我们的操作都是合法的。 然而我们定义了一个常量对象 tb,只调用它的常量方法,却能够修改tb的数据。 对数据的操作甚至可以放在 operator[]()
方法里面。
这一点不合理之处引发了 逻辑常量(logical constness)的讨论:常量方法可以修改数据成员, 只要客户检测不到变化就可以。可是常量方法修改数据成员 C++ 编译器不会同意的!这时我们需要 mutable
限定符:
class CTextBlock { |
通常我们需要定义成对的常量和普通方法,只是返回值的修改权限不同。 当然我们不希望重新编写方法的逻辑。最先想到的方法是常量方法调用普通方法,然而这是 C++ 语法不允许的。 于是我们只能用普通方法调用常量方法,并做相应的类型转换:
const char& operator[](size_t pos) const{ |
*this
的类型是 TextBlock
,先把它强制隐式转换为 const TextBlock
,这样我们才能调用那个常量方法operator[](size_t) const
,得到的返回值类型为 const char&
const
属性,得到类型为 char&
的返回值维拓标准接口的开发也基本完成了,这也是我首次用 C# 完成定制项目。之前 Unity 学的很多东西都忘掉了,再捡起来用还是有些吃力,所以写了这篇博客总结一下。
老生常谈的问题了,这里构造 Paper
的时候 item[]
如果没有这些索引又会抛出异常,但自己写的时候经常注意不到:
+if (item.Length < 2) |
之前对于需要合并的路径我都是这么通过对 string
直接进行加减来操作的:
string FilePath1 = @"C:\Users\MFGYF-WXY\source\repos"; |
但这种方式很业余,这种情况正确的处理是用 Path.Combine()
方法将两个路径进行合并:
string FilePath1 = @"C:\Users\MFGYF-WXY\source\repos"; |
<bindingRedirect/>
在完成部分接口把程序打包成exe时重新校对了一下 NewtonJson 的版本,之前在 Nuget 上下载的是 13 版本,但是 GStarCAD 目录下的版本是 12。将版本调整之后 exe 无法正常使用:
未经处理的异常:SystemI0.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Json,Version=13.0.0.0 Culture=neutral Pub1icKeyToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040)--->SystemIO.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Ison,Version=12.0.0.0,Culture=neutral, PublicKe yToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040) |
再次检查 Nuget 版本已经调整过来了,但是编译之后的 exe 还是会报错 NewtonJson 版本错误。其实只需要调整一下 app.config
文件:
|
问题在于 <bindingRedirect/>
这个标签,微软官方API文档 这里有比较详细的解释。
将一个程序集版本重定向到另一个版本。
<bindingRedirect |
oldVersion
:指定最初请求的程序集的版本。 程序集版本号的格式为 major.minor.build.revision
。 该版本号的每个部分的有效值介于 0 和 65535 之间。newVersion
:指定要用来取代最初请求的版本的程序集版本(格式为:n.n.n.n
) ,此值可以指定 oldVersion 之前的版本。官方还给出了一个示例演示如何将一个程序集版本重定向到另一个版本:
<configuration> |
所以把这个标签删掉,程序就可以正常运行了,这是一个知识盲点记录下来。
.dat
文件数据所需要读取的 PaperSet.dat
长相如下,包含了国标图幅的各个数据尺寸:
Name B L a c e k BD LD |
观察数据结构,每行数据均以若干空格分隔图幅名称、长度、宽度等等数据,所以我们需要逐行去读取,先简单声明一个图幅信息类包含图幅名称和长宽:
/// <summary> |
之后通过 StreamReader
逐行读取(首行不读取)即可:
List<Paper> result = new List<Paper>(); |
String.Split
方法可以参考微软的这篇 如何在 C# 中使用 String.Split 分隔字符串, StringSplitOptions.RemoveEmptyEntrie
s 参数来排除返回数组中的任何空字符串。要对返回的集合进行更复杂的处理,可使用 LINQ 来处理结果序列。
获取 CAD 应用程序,首先明确我们的思路:
try |
这里捕获用到了 System.Runtime.InteropServices
中的 Marshal.GetActiveObject()
方法。
注意 app.Visible
可以让后台的应用程序前置显示,在忘记关闭 Quit()
时会出现多个后台运行程序,在项目的前期测试造成了不少麻烦。
该命令会传入一个图纸路径 filePath
,关闭该指定路径的图纸。
关闭图纸的业务场景相对简单,只需要判断两点:
filePath
相对应的图纸最后对找到的图纸进行关闭操作即可。
// 图纸数量小于等于 0 则未打开任何图纸 |
需要注意的是这里需要用 doc.FullName
而非 doc.Name
,以规避出现诸如 C:\test.dwg
和 D:\test.dwg
在 Name
中均为 test.dwg
的末端图纸路径重名的情况。
// 若当前已有打开图纸 |
// 图纸数量小于等于 0 则未打开任何图纸 |
if (appController.CadApp.Documents.Count <= 0) |
这个项目一个比较关键的过程就是去解析图纸数据并转换为 Json 格式,前面 PLM 接口负责解析数据,那么该如何将这些数据转换为 Json 呢?
查阅官网的 Custom JsonConverter,给出了一个比较完整的示例:
public class KeysJsonConverter : JsonConverter |
Employee employee = new Employee |
在 C 语言中的全局变量和静态变量都是会自动初始化为 0,堆和栈中的局部变量不会初始化而拥有不可预测的值。 C++ 保证了所有对象与对象成员都会初始化,但其中基本数据类型的初始化还得依赖于构造函数。 下文来详细探讨 C 风格的”默认初始化”行为,以及 C++ 中成员变量的初始化规则。
很多人至今不知道 C++ 中如何正确地初始化一个变量,我们首先来解决语法的问题。 C语言中在声明时用 =
即可完成初始化操作。但我们偏向于使用 C++ 风格(本文中均指面向对象程序设计风格)来初始化内置类型:
// C 风格 |
在 C 语言中 int a;
表示声明了整型 a
但未初始化,而 C++ 中的对象总是会被初始化的,无论是否写了圆括号或者是否写了参数列表,例如:
int basic_var; // 未初始化:应用"默认初始化"机制 |
定义基本数据类型变量(单个值、数组)的同时可以指定初始值,如果未指定 C++ 会去执行默认初始化(default-initialization)。 那么什么是”默认初始化”呢?
栈中的变量(函数体中的自动变量)和堆中的变量(动态内存)会保有不确定的值;
全局变量和静态变量(包括局部静态变量)会初始化为零。
C++11: If no initializer is specified for an object, the object is default-initialized; if no initialization is performed, an object with automatic or dynamic storage duration has indeterminate value. Note: Objects with static or thread storage duration are zero-initialized, see 3.6.2.
所以函数体中的变量定义是这样的规则:
int i; // 不确定值 |
未初始化的和初始化为零的静态/全局变量编译器是同样对待的,把它们存储在进程的BSS段(这是全零的一段内存空间)中。所以它们会被”默认初始化”为零。
来看例子:
int g_var; |
输出:
0 // 全局变量 |
动态内存中的变量在上述代码中没有给出,它们和局部变量(自动变量)具有相同的”默认初始化”行为。
成员变量分为成员对象和内置类型成员,其中成员对象总是会被初始化的。而我们要做的就是在构造函数中初始化其中的内置类型成员。 还是先来看看内置类型的成员的”默认初始化”行为:
class A{ |
输出:
0 2407223 0 |
可见内置类型的成员变量的”默认初始化”行为取决于所在对象的存储类型,而存储类型对应的默认初始化规则是不变的。 所以为了避免不确定的初值,通常会在构造函数中初始化所有内置类型的成员。Effective C++: Item 4一文讨论了如何正确地在构造函数中初始化数据成员。 这里就不展开了,直接给出一个正确的初始化写法:
class A{ |
再来探讨一下当对象聚合发生时成员变量的”默认初始化”行为,同样还是只关注于基本数据类型的成员。
class A{ |
输出:
0 0 |
规则还是是一样的,默认初始化行为取决于它所属对象的存储类型。 封闭类(Enclosing)中成员对象的内置类型成员变量的”默认初始化”行为取决于当前封闭类对象的存储类型,而存储类型对应的默认初始化规则仍然是不变的。
]]>中车大同的旧图纸转换功能的定制开发告一段落,这段时间和周工学到了很多有用的知识,特此记录这个项目学到的编程技巧和项目开发经验。
在这个项目中我有很多时候对数组越界问题并不敏感,导致在一些情况下程序直接崩溃了,下面举一些具体的例子:
bool parseDTTitleBar(const CString & filePath) |
本例中我去实例化一个对象,但使用的方式是直接选取 vec
元素没有增加数量判断。倘若 vec
只有 6 个元素但代码中却取到了 vec[9]
就会导致崩溃。
前向声明是我编程时忽略掉的一个细节,在之前学校里写的代码只要编译能过去就不考虑这些问题了,但工作中需要注意效率。
#pragma once |
#include
所做的就是将整个代码复制过来,而这里我们并不关心 AbstractDetailsCreator
具体如何是什么,只需要知道有这样一个类型存在即可,这时就可以用前置声明而非引入整个头文件。
从代码编写的优雅程度来讲这样也会让阅读代码更加容易,不会因为引入过多无关头文件而一头雾水。前置声明最大的好处在于避免编译膨胀,一个优秀的 CPP 代码应该只包含它的最小代码集合。
自己经常忘记如何读取实体的 XData,特此记录:
AcDbObjectPointer<AcDbText> text(textId); |
获取链表后根据所需类型选择相应 get
函数即可。
大同希望把我们产品的图框插入点由内框左下点改为外框左下点,需要以组为单位做一下平移操作。思路上还是比较清晰的:
AcDbDictionaryPointer dict(acdbCurDwg()->groupDictionaryId(), kForRead); |
附加栏缩放方式由变换矩阵改为修改实体的缩放因子:
AcDbObjectId idEnt; |
.ini
文件函数业务场景需要读取下面的 .def
配置文件:
[Info] |
.ini
文件结构我之前遇到的大部分配置文件的类型都是 .xml
或者 .josn
(现在网络端也是这两者使用比较多,属于通用的配置文件类型了),面对 .ini
文件还是比较陌生。 这里引用简书一位博主的 ini文件格式和读取:
ini 就是英文 “initialization” 的头三个字母的缩写,当然 INI file 的后缀名也不一定是 .ini
,也可以是 .cfg
,.conf
或者是 .txt
。
ini 文件的格式很简单,最基本的三个要素是:parameters
,sections
和 comments
。
ini 所包含的最基本的”元素”就是 parameter
,每一个 parameter
都有一个 name
和一个 value
,如下所示:
name = value |
所有的 parameters
都是以 sections
为单位结合在一起的。所有的 section
名称都是独占一行,并且 sections
名字都被方括号包围着([ section's name ]
)。
在 section
声明后的所有 parameters
都是属于该 section
。对于一个 section
没有明显的结束标志符,一个 section
的开始就是上一个 section
的结束,或者是 end of the file。 section
如下所示:
[section] |
在 ini 文件中注释语句是以分号 ;
开始的。所有的所有的注释语句不管多长都是独占一行直到结束的。在分号和行结束符之间的所有内容都是被忽略的。
项目的工具类中有之前写好的读取 .ini
配置文件函数:
...... |
这些函数的实现则是依赖于底层 Windows 提供的 API 接口。
而在具体实践中,由于业务场景中 .ini
文件的 parameters
的 value
值过长,导致读取的时候出现遗漏的情况(下面以读取 [DTTitleBarText]
为例):
std::map<CString, CString> resultMap; |
得到的 resultMap
则是缺失了一部分:
[DTTitleBarText] |
直觉告诉我可能是缓冲区大小的问题,所以还是查一下微软官方文档看一下实现比较好,可能需要自己去重新实现一下读取函数。
GetPrivateProfileSection function
DWORD GetPrivateProfileSection( |
[in] lpAppName
The name of the section in the initialization file.
[out] lpReturnedString
A pointer to a buffer that receives the key name and value pairs associated with the named section. The buffer is filled with one or more null-terminated strings; the last string is followed by a second null character.
[in] nSize
The size of the buffer pointed to by the lpReturnedString parameter, in characters.
The maximum profile section size is 32,767 characters.
[in] lpFileName
The name of the initialization file. If this parameter does not contain a full path to the file, the system searches for the file in the Windows directory.
这里比较关键的地方就是这个 nSize
,可以看到它是通过一个缓冲区大小的参数设定读取的缓冲区大小,最大可以设置为 32,767 字节。经过周工排查 IM_GetPrivateProfileSectionMap
的缓冲区为 2k 左右,所以需要我们重新编写函数读取。
那么首先我们先重新设定最大的缓冲区大小(也就是刚才的 32,767):
TCHAR buffer[32767] = { 0 }; |
之后则需要用一个我之前几乎没有用过的 string_view
。C++17 中我们可以使用 std::string_view
来获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,而只是拥有一个字符串的查看功能。std::string_view
比 std::string
的性能要高很多,因为每个 std::string
都独自拥有一份字符串的拷贝,而 std::string_view
只是记录了自己对应的字符串的指针和偏移位置,当我们在只是查看字符串的函数中可以直接使用 std::string_view
来代替。
读取出来的 buffer
字符串则是以下面形式排列:
name1 = value1 \0 name2 = value2 \0 name3 = value3 \0 ... nameN = valueN \0\0 |
这里我的算法是用左右双指针寻找 =
和 \0
,以此类推直至读取到 profileSize
:
size_t left = 0; |
左指针归零,右指针先找到 =
。此时观察可以发现左右指针已经可以把 name1
读取出来了:
name1 = value1 \0 name2 = value2 \0 name3 = value3 \0 ... |
接着左指针移到右指针(也就是 =
所在位置)后面一位,右指针找到 \0
。此时观察可以发现左右指针又可以把 value1
读取出来了:
name1 = value1 \0 name2 = value2 \0 name3 = value3 \0 ... |
依此类推,最终完整实现如下:
std::map<CString, CString> Utility::readPrivateProfile(CString appName, CString filePath) |
在移植的过程中发现序号实体一直没办法添加进去,返回的 eWasOpenForWrite
找了好久,也尝试使用升降读写权限也失败了。后经周工点拨才发现问题所在:
-shared_ptr<XuHaoEntity> pMechXH(new XuHaoEntity(m_MechXHDInfo)); |
由于智能指针的广泛使用,所以我对内存管理这一块并不够敏感。张帆的那本书曾经提到 ObjectARX 是如何管理内存的:
在操作图形数据库的各种对象时,必须遵守 AutoCAD 的打开和关闭对象的协议。该协议确保当对象被访问时在物理内存中,而未被访问时可以被分页存储在磁盘中。创建和打开数据库的对象之后,必须在不用的时候关闭它。
给初学 ObiectARX 的人两个建议。
- 不要忘记各种数据库对象的关闭:在打开或创建数据库对象之后,必须尽可能早地关闭它。在初学者所犯的错误中,未及时关闭对象的错误至少占一半!
- 不要使用
delete pLine
的语句:对 C++ 比较熟悉的读者,习惯于配对使用new
和delete
运算符,这在 C++ 编程中是一个良好的编程习惯。但是在ObjectARX 的编程中,当编程者使用appendAcDbEntity
函数将对象添加到图形数据库之后,就需要由图形数据库来操作该对象。
这里的 shared_ptr<TH_XuHaoEntity>
就是问题关键。当我使用智能指针的时候将 XuHaoEntity
同时托管给 AutoCAD 和系统,这就导致在该段代码的作用域结束时会自动销毁该序号实体,而此时它已经被添加到数据库对象当中!内存泄漏的问题也就此诞生了,倘若此时用 Debug 工具测试会直接崩溃,因为该实体根本无法访问(毕竟已经在内存中被删除了)。
这个问题正好对应了建议二:不要使用 delete pLine
的语句。
天喻的旧图纸中转换时明细表部分需使用文字样式 Standard
的字体、大字体和宽度因子,但创建明细表时字体使用的是 HC_TEXTSTYLE
。虽然新建的图纸已经将两者调整为一致,但对于旧图纸而言仍不同步,需要将 Standard
的各项设置到 HC_TEXTSTYLE
:
AcDbTextStyleTablePointer pTextStyleTable(acdbHostApplicationServices()->workingDatabase(), AcDb::kForRead); |
用的是笨办法挨个设置,暂时没找到类似整个拷贝的方式去处理文字样式表。
大同方提供的 .shx
字体没办法正常显示 瞭
字,并且强烈要求增加该字的插入功能。
时间有限,讨论了一下决定先将 瞭
字做成 liao.dwg
文件,然后通过 LISP 代码交互并插入:
(defun liao() |
项目进度后期需要交付图纸转换的源码,但不会将我们所有项目的源码交付给大同方,所以需要将其余无关项目以头文件和库文件的形式交付。
先前我做交付时会一个一个去编译每个依赖项目,看缺少哪个头文件再加进去,效率很低。后面周工教了如何使用 find
命令去操作文件:
find <file_path> -type f ! -name "*.h" -delete |
该命令会将所有非 .h
头文件删除,非常方便。
最近一段时间,AI 技术的进步则让代码补全有了更上一层楼的机会。接下来,我们为大家介绍的 Github Copilot 就是这样一款基于 AI 的代码补全工具。
我们编程时写出的代码,在未编译前通常以纯文本格式存在。因此,实际上我们能使用任何文本编辑器来编写代码,包括系统自带的记事本。但是,好的工具能够让我们事半功倍。面对复杂的工作任务,我们需要 IDE(集成开发环境)这样的生产力工具。IDE 本质上就是更高级的文本编辑器,集成了许多人性化功能来提升效率,比如:自动补全变量,提示可能会用到的函数列表,语法高亮,显示语法错误等等。
IDE 本身也在不断进化。我的第一门使用 IDE 的编程语言是 Java,使用的是 Eclipse,当时自动补全功能还比较简陋,局限于符号的提示与选单,我也没有对 IDE 究竟有多强大建立起概念。后来,开始学习开发框架后,我慢慢接触到 JetBrains 出品的 IDEA。IDEA 的提示更智能,例如:可以在数组后输入「.for」自动构成 foreach 循环,也可以使用快捷键自动生成 Getter/Setter、构造函数、重载函数等等。毫无疑问,JetBrains 系列产品为编码工作带来了更高的效率,提供了更加全面、智能的补全功能。
Github Copilot 是 GitHub 和 OpenAI 合作开发的人工智能工具,可以在编辑代码时帮助你自动生成可能会需要的代码。
GitHub Copilot 能够提取代码上下文,给出整行代码或整个函数的补全建议。它可以帮助我们完成下列任务:
我们先介绍一下 GPT-3。GPT-3(Generative Pre-trained Transformer 3)是一个用于处理自然语言的 AI 模型,由 OpenAI 训练开发。GPT-3 通过阅读几乎一切人类可阅读的内容来进行训练,理论上,它能够完成一切通过语言完成的工作,而且完成效果还非常接近人类。已经有实验证明 GPT-3 可用于撰写文章、回答问题、编写代码生成应用程序、设计表格、开发游戏、将文字描述便携为成型的网页等等。
而 OpenAI Codex 则是基于 GPT-3 开发的一款针对编程所设计的 AI 模型。Codex 从公共代码仓库学习人类编写的代码,其代码来源包括 Github 上的公共代码仓库。官网原文如下:
OpenAI Codex is a descendant of GPT-3; its training data contains both natural language and billions of lines of source code from publicly available sources, including code in public GitHub repositories. (OpenAI Codex 是 GPT-3 的衍生项目;它的训练数据包括自然语言和数以亿计来自公开可用来源的源代码,其中包括 Github 公开仓库的代码。)
最后,GitHub Copilot 则是使用了 Codex 进行研发的一款商业产品。Github 将算法进行包装,做成了插件和网页,进行应用分发。现在 GitHub Copilot 支持在 Visual Studio Code、Visual Studio、JetBrains Rider 上通过插件形式集成进 IDE,以便我们使用。
要想使用 GitHub Copilot,首先需要注册一个 Github 账号。有了帐号后,按下面的步骤可以找到并启用 GitHub Copilot:
找到 GitHub Copilot 设置页面:在边栏的「代码、规划和自动化」部分,单击「GitHub Copilot」。
启用 GitHub Copilot:在 GitHub Copilot 设置页面上,单击「启用 GitHub Copilot」。
设置完成后,IDE 提示可以使用「Tab」来自动补全代码,使用「⌥ + ]」或者「⌥ + [」来选择其他候选的补全选项。
在编写代码的过程中,Github Copilot 会自动提示可能的补全方案,此时按下「Tab」即可完成补全。
有时,AI 并不会一次给出完整的提示代码,例如,图示的代码就并非一次性生成的,而是逐行自动补全,最终生成了一个可以实际使用的函数(甚至包括注释)。下图的例子在 Unity3D 中绘制了一条射线用于检测前方是否有物品,只有第一行注释是我写下的。
下面的例子很有趣:当我尝试把乐谱的音高编写成数组时,Github Copilot 也给出了他所理解的音乐:
在这样的例子中,对重复的流行乐片段,Github Copilot 有时可以给出不错的答案。比如预先输入《卡农》的重复性模进片段,Github Copilot 往往可以完全正确地补全乐谱。可见,在面临重复性较高的功能开发,或是使用一些常用的算法时,依靠 AI 补全是一个称得上非常可靠的选择。不过,如果需求非常复杂,大部分情况下,它并不能独立地给出完美的解决方案。GitHub 团队在对一组 Python 函数进行基准性测试后发现尝试十次后,大约 57% 情况下可以给出正确的答案。部分情况下,Github Copilot 也会给出无法通过编译的代码。
使用 Github Copilot 很久后,Reddit 大佬 Colin Eberhardt 指出了几点不足:
许多人指出 Github Copilot 会使用有版权的代码作为提示内容(参见 Jacob Crume 的文章“GitHub Copilot is Now Available for All and Not Everyone Likes It”)。少数派作者 100gle 在《GitHub Copilot:革命未竟,未来可期》中更是举出了很多例子。最为出名的莫过于,如果你在编辑器中输入 Fast inverse square root
,便会得到一段代码,它和当年《雷神之锤》使用的算法完全一致。
现代开源软件多使用 GPL (GNU General Public License)协议,这个协议要求你也将代码开源,且使用 GPL 协议。而通过 Github Copilot 补全时我们并不确定这段代码的作者为它指定的协议。开源许可证的主要作用是对软件的使用、复制、修改和再发布等进行限制。而显然使用 AI 补全显然会破坏这一点。
可以预见的是,同当今各种 AI 作画工具面对的种种争议一样,Github Copilot 也一定会因为版权问题,难以被大型企业所用,至少短期内如此。
Github Copilot 或许并不能承载类似“AI 即将取代程序员”的想象,但在当下,它无疑是程序员的好帮手。作为辅助,它提供的补全并没有智能到让完全不会编程的用户完成开发,但也并不只是简单的提示工具。合理运用 Github Copilot 能够为开发者的学习成长带来很大帮助。
与此同时,它不可避免地存在一些缺陷,代码的版权问题也限制了它商业化的应用前景。不够熟练的程序员可能也会对它失望——就像它名字中的 Copilot 一样,Github Copilot 更接近优秀的副驾驶角色,但工作总归还是需要一位优秀的主驾驶领导。
最好的旅行靴已经送到我们手中,走出什么样的路还需要开发者自己去定夺。
]]>Jetbrains 全家桶开发还是比较顺手的,但工作之后学生认证到期了,所以需要重新激活,特此写一篇博文记录自己使用 Jetbrains 产品的各种激活方式。
需要提前声明,Jetbrains 也提供了社区版供学生和初学者使用,本文仅作激活操作记录,使用激活的软件请勿用作商业用途,如有条件请务必支持正版购买许可证。
Github 的学生认证可以通过上传学校信息的方式获取正规免费的许可,在 Unity Student Plan 申请指南 这篇文章有详细操作。
如果是在读学生,完全可以用这种方式激活 Jetbrains 相关产品。
不推荐这种激活方式。
之前我使用这种方式激活,因为之前有一些相关文件未删除,会导致整个软件闪退无法使用,在 Mac 上折腾了很长一段时间。
先打开这个网站:https://search.censys.io/
然后搜索框输入:
services.http.response.headers.location: account.jetbrains.com/fls-auth |
选择第一个搜索结果,右击进去:
将网址到 Jetbrains,选择许可证服务器 /License server
,粘贴刚刚复制的网址 http://134.53.225.196
,激活。
大功告成,顺带一提,这个好像是迈阿密大学的服务器……
]]>GStarCAD 笔试和机试总结。笔试题主要考核 C++、代码阅读理解、基本几何运算、英文阅读能力,机试题主要考察 VC++、MFC 的实际编程操作能力。
笔试题主要考核 C++、代码阅读理解、基本几何运算、英文阅读能力。
下列代码有问题、或有可改进之处吗?如有,请直接修改,并写明原因.
class CBase |
答:int GetDouble() const {m_nVal *= 3; return m_nVal;}
若需要对成员变量进行赋值需删除 const
。
请写出 main
函数的输出结果,并写明理由.
class CBase |
Send:102 |
阅读理解类声明代码,并使用之实现功能。
class AcGePoint2d |
class AcGePoint2d |
基本几何运算。
pt1
,pt2
,如何计算从起点 pt1
到终点 pt2
的向量 v
?V = pt2 - pt1 |
v1{1.0,0.0,0.0}
,v2{0.0,1.0,0.0}
,v3 = v1 - v2
,则v3 =?{1.0,-1.0,0.0} |
{0.0,0.0,1.0} |
v1·v2
等于 0 ,意味着两向量是什么关系?垂直 |
翻译英文资料。
An ObjectARXapplication is a dynamic link library (DLL) that shares the address space of AutoCAD and makes direct function calls to AutoCAD. You can add new classes to the ObjectARXprogram environment and export them for use by other programs.
一个 ObjectARX 应用是一个的动态链接库(DLL),它共享 AutoCAD 地址空间,并直接调用函数操作 AutoCAD。你可以在 ObjectARX 程序环境中新增新的类,并将其导出给其他程序使用。
CDialog::DoModal()
Call this member function to invoke the modal dialog box and return the dialog-box result when done. This member function handles all interaction with the user while the dialog box is active.
CDialog::DoModal()
,使用这一成员函数可调出模态对话框,并且当其使用完成后可返回对话框的结果。当对话框激活时,这一成员函数处理所有与用户的交互。
AutoCADstores the values for its operating environment in system variables. Each system variable has an associated type: integer, real, point, or text string. You can examine any system variable and change any writable system variable directly on the command line by entering the system variable name. Many system variables are also accessible through dialog box options.
AutoCAD 保存与操作环境相关的值于系统变量中。每个系统变量有一个相关类型:整形,实型,点或字符串。你可以检测任何系统变量,并通过在命令行输入系统变量名称直接改变系统变量。许多系统变量也可以通过对话框选项设置。
机试题主要考察 VC++、MFC 的实际编程操作能力。
CEdit
的派生类 CMyEdit
。在 CMyEdit
类中定义成员函数 void SetIndex(int index)
,调用本函数后,编辑框内显示 index
数值。CMyEdit
、CButton
、CComboBox
、CEdit
,分别对齐对话框4个角。CArray<CWnd*> m_arrCtrl
,并在对话框初始化时将上述4控件的对象指针按逆时针顺序保存到 m_arrCtrl
数组(第1个为左上角控件)。m_arrCtrl
中控件指针也同步切换位置(第1个始终是左上角控件)。每次切换控件位置后,需从指针数组中找到其中唯一的 CMyEdit
控件,并调用它的 SetIndex(index)
成员函数,index
为 CMyEdit
对象在 m_arrCtrl
数组中的新索引(0-3)。DoDataExchange
和 OnInitDialog
函数外,对话框其它函数中只能使用 m_arrCtrl
成员变量,不能使用其它成员变量(也不能使用全局变量和静态变量)。LISTBOX
。LISTBOX
刷新显示为本学校或本班级的所有学生姓名。Google 经常会发布一些开源项目, 意味着会接受来自其他代码贡献者的代码。但是如果代码贡献者的编程风格与 Google 的不一致, 会给代码阅读者和其他代码提交者造成不小的困扰。Google 因此发布了这份自己的编程风格指南, 使所有提交代码的人都能获知 Google 的编程风格。
C++ 是 Google 大部分开源项目的主要编程语言. 正如每个 C++ 程序员都知道的, C++ 有很多强大的特性, 但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug, 难以阅读和维护.
本指南的目的是通过详细阐述 C++ 注意事项来驾驭其复杂性. 这些规则在保证代码易于管理的同时, 也能高效使用 C++ 的语言特性.
风格, 亦被称作可读性, 也就是指导 C++ 编程的约定. 使用术语 “风格” 有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单.
使代码易于管理的方法之一是加强代码一致性. 让任何程序员都可以快速读懂你的代码这点非常重要. 保持统一编程风格并遵守约定意味着可以很容易根据 “模式匹配” 规则来推断各种标识符的含义. 创建通用, 必需的习惯用语和模式可以使代码更容易理解. 在一些情况下可能有充分的理由改变某些编程风格, 但我们还是应该遵循一致性原则,尽量不这么做.
本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用.
Google 主导的开源项目均符合本指南的规定.
注意: 本指南并非 C++ 教程, 我们假定读者已经对 C++ 非常熟悉.
通常每一个 .cc
文件都有一个对应的 .h
文件. 也有一些常见例外, 如单元测试代码和只包含 main()
函数的 .cc
文件.
正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以
.h
结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以.inc
结尾。不允许分离出-inl.h
头文件的做法.
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 #define
保护,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols.
不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 .inc
文件扩展名。
如果 .h
文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cc
文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h
文件里(译者注:过去该规范曾提倡把定义放到 -inl.h
里过)。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 .cc
文件里。
所有头文件都应该有
#define
保护来防止头文件被多重包含, 命名格式当是:<PROJECT>_<PATH>_<FILE>_H_
.
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo
中的头文件 foo/src/bar/baz.h
可按如下方式保护:
|
尽可能地避免使用前置声明。使用
#include
包含需要的头文件即可。
所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.
#include
会迫使编译器展开更多的文件,处理更多的输入。#include
使代码因为头文件中无关的改动而被重新编译多次。std::
的 symbol 时,其行为未定义。#include
。极端情况下,用前置声明代替 #include
甚至都会暗暗地改变代码的含义:// b.h: |
如果 #include
被 B
和 D
的前置声明替代, test()
就会调用 f(void*)
.
include
冗长。只有当函数只有 10 行甚至更少时才将其定义为内联函数.
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch
语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch
语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
#include
的路径及顺序使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 .
(当前目录) 或 ..
(上级目录). 例如, google-awesome-project/src/base/logging.h
应该按如下方式包含:
又如, dir/foo.cc
或 dir/foo_test.cc
的主要作用是实现或测试 dir2/foo2.h
的功能, foo.cc
中包含头文件的次序如下:
dir2/foo2.h
(优先位置, 详情如下)- C 系统文件
- C++ 系统文件
- 其他库的
.h
文件- 本项目内
.h
文件
这种优先的顺序排序保证当 dir2/foo2.h
遗漏某些必要的库时, dir/foo.cc
或 dir/foo_test.cc
的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。
dir/foo.cc
和 dir2/foo2.h
通常位于同一目录下 (如 base/basictypes_unittest.cc
和 base/basictypes.h
), 但也可以放在不同目录下.
按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。比如您要用到 bar.h
中的某个符号, 哪怕您所包含的 foo.h
已经包含了 bar.h
, 也照样得包含 bar.h
, 除非 foo.h
有明确说明它会自动向您提供 bar.h
中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc
只包含 foo.h
就够了,不用再管后者所包含的其它内容。
举例来说, google-awesome-project/src/foo/internal/fooserver.cc
的包含次序如下:
例外:
有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
-inl.h
可提高代码可读性 (一般用不到吧:D);.
和 ..
虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译” (对应源文件处 :D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.#includes
来插入文本,且其文件扩展名 .inc
看上去也很科学。-inl.h
用法。.cc
文件里。这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则。#include
中插入空行以分割相关头文件, C 库, C++ 库, 其他库的 .h
和本项目内的 .h
是个好习惯。鼓励在 .cc
文件内使用匿名命名空间或 static
声明. 使用具名的命名空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。
命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 命名空间在这基础上又封装了一层.
举例来说, 两个不同项目的全局作用域都有一个类 Foo
, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同命名空间中, project1::Foo
和 project2::Foo
作为不同符号自然不会冲突.
内联命名空间会自动把内部的标识符放到外层作用域,比如:
namespace X { |
X::Y::foo()
与 X::foo()
彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。
命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。
内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).
根据下文将要提到的策略合理使用命名空间.
// .h 文件
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cc 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
...
}
} // namespace mynamespace更复杂的
.cc
文件包含更多, 更复杂的细节, 比如 gflags 或 using 声明。
DEFINE_FLAG(bool, someflag, false, "dummy flag");
namespace a {
...code for a... // 左对齐
} // namespace a
std
内声明任何东西, 包括标准库的类前置声明. 在 std
命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件.
// 禁止 —— 污染命名空间
using namespace foo;
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
在
.cc
文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static
。但是不要在.h
文件中这么做。
所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为 static
拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
推荐、鼓励在 .cc
中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在 .h
中使用。
匿名命名空间的声明和具名的格式相同,在最后注释上 namespace
:
namespace { |
使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免污染全局作用域.
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 2.1. 命名空间。举例而言,对于头文件 myproject/foo_bar.h
, 应当使用
namespace myproject { |
而非
namespace myproject { |
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内.
如果你必须定义非成员函数, 又只是在 .cc
文件中使用它, 可使用匿名 2.1. 命名空间 或 static
链接关键字 (如 static int Foo() {...}
) 限定其作用域.
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v 一开始就初始化
属于 if
, while
和 for
语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.
// 低效的实现 |
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次 |
禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。
禁止使用类的静态储存周期变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过 constexpr
变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如 getenv()
或 getpid()
)不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
Xris 译注:
同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。
同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 main()
返回还是对 exit()
的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。
改善以上析构问题的办法之一是用 quick_exit()
来代替 exit()
并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 atexit()
所绑定的任何 handlers. 如果您想在执行 quick_exit()
来中断时执行某 handler(比如刷新 log),您可以把它绑定到 _at_quick_exit()
. 如果您想在 exit()
和 quick_exit()
都用上该 handler, 都绑定上去。
综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector
(使用 C 数组替代) 和 string
(使用 const char[]
)。
如果您确实需要一个 class
类型的静态或全局变量,可以考虑在 main()
函数或 pthread_once()
内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
Yang.Y 译注:
上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.
cc
中的匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用using
关键字污染命名空间;public
;class
类型 (含 STL 容器), 避免不明确行为导致的 bug.我们倾向于按值返回, 否则按引用返回。 避免返回指针, 除非它可以为空.
C++ 函数由返回值提供天然的输出, 有时也通过输出参数(或输入/输出参数)提供. 我们倾向于使用返回值而不是输出参数: 它们提高了可读性, 并且通常提供相同或更好的性能.
C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之. 非可选输入参数通常是值参或 const
引用, 非可选输出参数或输入/输出参数通常应该是引用 (不能为空). 对于可选的参数, 通常使用 std::optional
来表示可选的按值输入, 使用 const
指针来表示可选的其他输入. 使用非常量指针来表示可选输出和可选输入/输出参数.
避免定义需要 const
引用参数去超出生命周期的函数, 因为 const
引用参数与临时变量绑定. 相反, 要找到某种方法来消除生命周期要求 (例如, 通过复制参数), 或者通过 const
指针传递它并记录生命周期和非空要求.
在排序函数参数时, 将所有输入参数放在所有输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前.
这并非一个硬性规定. 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂. 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通.
我们倾向于编写简短, 凝练的函数.
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug. 使函数尽量简短, 以便于他人阅读和修改代码.
在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数.
所有按引用传递的参数必须加上 const
.
在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int* pval)
. 在 C++ 中, 函数还可以声明为引用参数: int foo(int &val)
.
定义引用参数可以防止出现 (*pval)++
这样丑陋的代码. 引用参数对于拷贝构造函数这样的应用也是必需的. 同时也更明确地不接受空指针.
容易引起误解, 因为引用在语法上是值变量却拥有指针的语义.
函数参数列表中, 所有引用参数都必须是 const
:
void Foo(const string &in, string *out); |
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const
引用, 输出参数为指针. 输入参数可以是 const
指针, 但决不能是非 const
的引用参数, 除非特殊要求, 比如 swap()
.
有时候, 在输入形参中用 const T*
指针比 const T&
更明智. 比如:
总而言之, 大多时候输入形参往往是 const T&
. 若用 const T*
则说明输入另有处理. 所以若要使用 const T*
, 则应给出相应的理由, 否则会使得读者感到迷惑.
若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数.
你可以编写一个参数类型为 const string&
的函数, 然后用另一个参数类型为 const char*
的函数对其进行重载:
class MyClass { |
通过重载参数不同的同名函数, 可以令代码更加直观. 模板化代码需要重载, 这同时也能为使用者带来便利.
如果函数单靠不同的参数类型而重载 (acgtyrant 注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何. 另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑.
如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString()
和 AppendInt()
等, 而不是一口气重载多个 Append()
. 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector
以便使用者可以用列表初始化指定参数.
只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与函数重载遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下.
有些函数一般情况下使用默认参数, 但有时需要又使用非默认的参数. 缺省参数为这样的情形提供了便利, 使程序员不需要为了极少的例外情况编写大量的函数. 和函数重载相比, 缺省参数的语法更简洁明了, 减少了大量的样板代码, 也更好地区别了 “必要参数” 和 “可选参数”.
缺省参数实际上是函数重载语义的另一种实现方式, 因此所有不应当使用函数重载的理由也都适用于缺省参数.
虚函数调用的缺省参数取决于目标对象的静态类型, 此时无法保证给定函数的所有重载声明的都是同样的缺省参数.
缺省参数是在每个调用点都要进行重新求值的, 这会造成生成的代码迅速膨胀. 作为读者, 一般来说也更希望缺省的参数在声明时就已经被固定了, 而不是在每次调用时都可能会有不同的取值.
缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致. 而函数重载不会导致这样的问题.
对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作. 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用. (例如, 不要写像 void f(int n = counter++);
这样的代码.)
在其他情况下, 如果缺省参数对可读性的提升远远超过了以上提及的缺点的话, 可以使用缺省参数. 如果仍有疑惑, 就使用函数重载.
只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
C++ 现在允许两种不同的函数声明方式. 以往的写法是将返回类型置于函数名之前. 例如:
int foo(int x); |
C++11 引入了这一新的形式. 现在可以在函数名前使用 auto
关键字, 在参数列表之后后置返回类型. 例如:
auto foo(int x) -> int; |
后置返回类型为函数作用域. 对于像 int
这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别.
后置返回类型是显式地指定Lambda 表达式的返回值的唯一方式. 某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现. 即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了.
有时在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时. 例如:
template <class T, class U> auto add(T t, U u) -> decltype(t + u); |
对比下面的例子:
template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u); |
后置返回类型相对来说是非常新的语法, 而且在 C 和 Java 中都没有相似的写法, 因此可能对读者来说比较陌生.
在已有的代码中有大量的函数声明, 你不可能把它们都用新的语法重写一遍. 因此实际的做法只能是使用旧的语法或者新旧混用. 在这种情况下, 只使用一种版本是相对来说更规整的形式.
在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法. 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样复杂的模板代码.
Google 用了很多自己实现的技巧 / 工具使 C++ 代码更加健壮, 我们使用 C++ 的方式可能和你在其它地方见到的有所不同.
动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.
所有权是一种登记/管理动态内存和其它资源的技术. 动态分配对象的所有主是一个对象或函数, 后者负责确保当前者无用时就自动销毁前者. 所有权有时可以共享, 此时就由最后一个所有主来负责销毁它. 甚至也可以不用共享, 在代码中直接把所有权传递给其它对象.
智能指针是一个通过重载 *
和 ->
运算符以表现得如指针一样的类. 智能指针类型被用来自动化所有权的登记工作, 来确保执行销毁义务到位. std::unique_ptr 是 C++11 新推出的一种智能指针类型, 用来表示动态分配出的对象的独一无二的所有权; 当 std::unique_ptr
离开作用域时, 对象就会被销毁. std::unique_ptr
不能被复制, 但可以把它移动(move)给新所有主. std::shared_ptr 同样表示动态分配对象的所有权, 但可以被共享, 也可以被复制; 对象的所有权由所有复制者共同拥有, 最后一个复制者被销毁时, 对象也会随着被销毁.
<span class="pre">std::unique_ptr</span>
的所有权传递原理是 C++11 的 move 语法, 后者毕竟是刚刚推出的, 容易迷惑程序员.如果必须使用动态分配, 那么更倾向于将所有权保持在分配者手中. 如果其他地方要使用这个对象, 最好传递它的拷贝, 或者传递一个不用改变所有权的指针或引用. 倾向于使用 std::unique_ptr
来明确所有权传递, 例如:
std::unique_ptr<Foo> FooFactory(); |
如果没有很好的理由, 则不要使用共享所有权. 这里的理由可以是为了避免开销昂贵的拷贝操作, 但是只有当性能提升非常明显, 并且操作的对象是不可变的(比如说 std::shared_ptr<const Foo>
)时候, 才能这么做. 如果确实要使用共享所有权, 建议于使用 std::shared_ptr
.
不要使用 std::auto_ptr
, 使用 std::unique_ptr
代替它.
使用 cpplint.py
检查风格错误.
cpplint.py
是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 在行尾加 // NOLINT
, 或在上一行加 // NOLINTNEXTLINE
, 可以忽略报错.
某些项目会指导你如何使用他们的项目工具运行 cpplint.py
. 如果你参与的项目没有提供, 你可以单独下载 cpplint.py.
scoped_ptr
和 auto_ptr
已过时. 现在是 shared_ptr
和 uniqued_ptr
的天下了.最重要的一致性规则是命名管理. 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏, 等等, 甚至. 我们大脑中的模式匹配引擎非常依赖这些命名规则.
命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以无论你认为它们是否重要, 规则总归是规则.
函数命名, 变量命名, 文件命名要有描述性; 少用缩写.
尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.
int price_count_reader; // 无缩写 |
int n; // 毫无意义. |
注意, 一些特定的广为人知的缩写是允许的, 例如用 i
表示迭代变量和用 T
表示模板参数.
模板参数的命名应当遵循对应的分类: 类型模板参数应当遵循类型命名的规则, 而非类型模板应当遵循变量命名的规则.
文件名要全部小写, 可以包含下划线 (_
) 或连字符 (-
), 依照项目的约定. 如果没有约定, 那么 “_
” 更好.
可接受的文件命名示例:
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc
// _unittest
和 _regtest
已弃用.C++ 文件要以 .cc
结尾, 头文件以 .h
结尾. 专门插入文本的文件则以 .inc
结尾, 参见头文件自足.
不要使用已经存在于 /usr/include
下的文件名 (Yang.Y 注: 即编译器搜索系统头文件的路径), 如 db.h
.
通常应尽量让文件名更加明确. http_server_logs.h
就比 logs.h
要好. 定义类时文件名一般成对出现, 如 foo_bar.h
和 foo_bar.cc
, 对应于类 FooBar
.
内联函数必须放在 .h
文件中. 如果内联函数比较短, 就直接放在 .h
中.
前面说明的编程习惯基本都是强制性的. 但所有优秀的规则都允许例外, 这里就是探讨这些特例.
对于现有不符合既定编程风格的代码可以网开一面.
当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定. 如果不放心, 可以与代码原作者或现在的负责人员商讨. 记住, 一致性 也包括原有的一致性.
Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南.
如果你习惯使用 Windows 编码风格, 这儿有必要重申一下某些你可能会忘记的指南:
iNum
). 使用 Google 命名约定, 包括对源文件使用 .cc
扩展名.DWORD
, HANDLE
等等. 在调用 Windows API 时这是完全可以接受甚至鼓励的. 即使如此, 还是尽量使用原有的 C++ 类型, 例如使用 const TCHAR*
而不是 LPCTSTR
.#pragma once
; 而应该使用 Google 的头文件保护规则. 头文件保护的路径应该相对于项目根目录 (Yang.Y 注: 如 #ifndef SRC_DIR_BAR_H_
, 参考#define 保护一节).#pragma
和 __declspec
. 使用 __declspec(dllimport)
和 __declspec(dllexport)
是允许的, 但必须通过宏来使用, 比如 DLLIMPORT
和 DLLEXPORT
, 这样其他人在分享使用这些代码时可以很容易地禁用这些扩展.然而, 在 Windows 上仍然有一些我们偶尔需要违反的规则:
_ATL_NO_EXCEPTIONS
以禁用异常. 你需要研究一下是否能够禁用 STL 的异常, 如果无法禁用, 可以启用编译器异常. (注意这只是为了编译 STL, 自己的代码里仍然不应当包含异常处理).StdAfx.h
或 precompile.h
的文件. 为了使代码方便与其他项目共享, 请避免显式包含此文件 (除了在 precompile.cc
中), 使用 /FI
编译器选项以自动包含该文件.resource.h
且只包含宏, 这一文件不需要遵守本风格指南.运用常识和判断力, 并且保持一致.
编辑代码时, 花点时间看看项目中的其它代码, 并熟悉其风格. 如果其它代码中 if
语句使用空格, 那么你也要使用. 如果其中的注释用星号 (*) 围成一个盒子状, 那么你同样要这么做.
风格指南的重点在于提供一个通用的编程规范, 这样大家可以把精力集中在实现内容而不是表现形式上. 我们展示的是一个总体的的风格规范, 但局部风格也很重要, 如果你在一个文件中新加的代码和原有代码风格相去甚远, 这就破坏了文件本身的整体美观, 也让打乱读者在阅读代码时的节奏, 所以要尽量避免.
好了, 关于编码风格写的够多了; 代码本身才更有趣. 尽情享受吧!
]]>本课程旨在围绕各类数据结构的设计与实现,揭示其中的规律原理与方法技巧;同时针对算法设计及其性能分析,使学生了解并掌握主要的套路与手段。本文将讲解数据结构向量及查找和排序。
向量属于最最基本的线性结构,我们笼统称之为线性序列。
本章我们将围绕这种数据结构展示和讨论两方面问题:
首先我们要辨析抽象数据类型和数据结构:
更形象一点,我们可以将数据结构比喻成某种产品比如汽车。作为用户 Application 而言,他只关心这种产品的外在特性能够提供的功能;而实现者 Implementation 则需要对这些功能以及特性具体如何落实负责。
所谓向量,实际上是 C++ 等高级编程语言中数组这种数据组织形式的一个推广和泛化。
在这些高级程序设计语言中所谓的数组实际上就是一段连续的内存空间,它被均匀地划分为若干个单元,而每一个单元都会与一个编号彼此回应,并且可以直接访问。
而向量可以被认为是数组的抽象与泛化,它同样是由一组抽象的元素按照刚才的线性次序封装而成。不同的是原来通过下标 i
的访问方式变成了秩 rank。
另外向量中元素的类型得到了拓展,不限于是某一种特定的基本类型,它的所有操作、管理维护更加简化,可以通过统一的接口来完成。
可以通过这些操作接口对向量做各种操作,同时也只能通过这些操作接口对向量进行操作。
|
整个 Vector 被封装起来,来自各种用户 application 的操作接口 interface 提供在外面,相当于一个 Vector 结构的使用说明书。
/* 构造函数 */ |
template <typename T> |
复制操作将 _elem
空间扩展为原来的二倍,然后将区间元素依次复制。
现在我们用 _size
表示实际规模,_capacity
表示总容量。
T* _elem; // 数据 |
这里的问题是 _capacity
一旦确定按照目前的方案它就将一成不变,而这样一种策略显然存在明显的不足。这种不足体现在两个方面:
_elem[]
不足以存放所有元素,尽管此时系统仍有足够的空间_elem[]
中的元素寥寥无几,装填因子 = _size/_capacity << 50%我们需要从静态管理策略改编为动态管理策略,模仿蝉的做法在即将发生上溢时适当地扩大内部数组容量。
向量的生命周期:
template <typename T> |
得益于向量的封装,尽管扩容之后数据区的物理地址有所改变,却不致出现野指针。
每当发现当前的内部数组即将发生上溢,我们并不是对它进行容量的加倍而只是在原来的容量的基础上追加一个固定的数额:
T* oldElem = _elem; |
对于这种策略而言,每经过 I 次插入操作它都需要进行一次扩容,每次分摊成本为 O(n)。
T* oldElem = _elem; |
每次的分摊成本为 O(1) 常数时间。
倍增策略通过在空间的效率上做了一个适当的牺牲换取在时间方面的巨大收益。
首先讨论向量元素的访问。表面上看这并不是什么问题,因为在向量 ADT 中已经定义了两个标准的接口 V.get(r)
和 V.put(r, e)
。通过它们我们已经可以自如地来写或者是读向量中特定的元素,但这两种接口在形式上还不是那么简洁直观。
我们期望数组那种直接地访问方式:A[r]
,为此需要重载下标操作符 []
:
template <typename T> |
再来考察向量的插入算法,如何讲某一个特定的元素插入到向量的特定位置。
因为原有向量所有元素都是紧邻排列的,所以为了能够插入新的元素我们需要将对应位置之后的所有元素称作它的后继,进行一个整体的右移操作。
template <typename T> |
template <typename T> |
template <typename T> |
无序向量只支持判等操作,有序向量还需要支持其中的元素相互比较大小。
template <typename T> |
从 hi
出发逆向逐一取出向量中的各个元素,与目标元素进行比对。如果不相等,就忽略它并且考察它的前驱,整个工作会遍历向量中的所有元素。
向量的唯一化需要把其中重复的元素都剔除掉,只保留一个拷贝。
template <typename T> |
template <typename T> |
template <typename T> |
与起泡排序算法的理解相同,有序/无序序列中,任意/总有一对相邻元素顺序/逆序。
因此,相邻逆序对的数目,可用以度量向量的逆序程度。
template <typename T> |
本节我们将探索计算几何的核心问题:凸包问题。计算几何领域几乎所有的问题都可以“归约”为凸包问题,因此学习凸包问题对整个计算几何体系至关重要。
我们计算几何的第一站就是凸包问题,它在计算几何中处于核心位置,这个核心体现在几乎所有的问题从理论上讲都可以归结为凸包问题。
接下来我们通过一个具体的动手实验领会一下凸包到底是什么。
为此你需要找到一张桌子或是屏幕,假想在这个桌子上钉上一系列的钉子,然后用皮筋将其撑到足够大以至于它能将桌面上的所有钉子都包含进去。
接下来的事情非常的轻松,你只要松手就行。那么随着啪的一声,你将会看到这幅图景:
刚才的皮筋就会变成这样一段一段蓝色的线段,它们首尾相连构成了一个紧绷的包围圈。这个蓝色的橡皮筋在在现在这样的一个图景状态就是我们所说的凸包,我们可以看到所谓的凸包是由这上面若干个钉子来决定的,虽然其中有一些钉子并不发挥作用,我们大致可以感觉到因为它们呆在内部。
那么,这之间的玄机又是什么呢?
为了更好地理解什么是凸包,我们再来看一个应用的例子。
艺术家经常要通过混合得到某种他想要又不是从工厂直接生产出来的颜料。我们知道一般来说每种颜料都可以分成是红绿蓝三个分量的数值指标,每种组合对应的大致都是一种颜料。
我们不妨为了简便起见只考虑红的以及绿的两个分量,所以这样的话每一种颜料也就是它所对应的颜色都可以用红的和绿的这样两个数字,或者说它们在整体的成份中所占的百分比来对应。
C = (R, G) |
比如说某种颜料 X 它所对应的红的分量可能是 10%,而绿的分量是 35%;另一种颜料比如叫 Y,那么它所对应的这两个分量一个是 16% 一个是 20%。 现在的问题来了,用这两种颜料能否兑出我们所希望的某些颜料呢?
X = (10%, 35%) Y = (16%, 20%) |
我们来看一下,当颜料混合在一块的时候它们的变化是多端的,有很多很多种组合,每几种颜料它们按照不同的分量、按照不同的比重勾兑在一块所得到的颜色其实都会不同。当然,艺术家有他的勾兑的方法,包括他的灵感,那么如果从数学的角度,从算法的角度来考虑,这其中应该用什么样的指导的方法呢?
那么从数学上来看我们一般来说都可以认为有一个目标的颜色,比如说这里的 U,这种颜色比如说特定的来说他希望红的占的比重是 12%,而绿的比重是 30%。
U = (12%, 30%) |
对于这样的一种目标的颜料我们应该用刚才的 X、Y,这两种来自于工厂的原始颜料用什么样的比例来对它们进行混合和勾兑呢?
好,我想你已经知道这个答案了。没错 我们应该用两份的 X 和一份的 Y 勾兑起来,就可以得到 U 了。
你不妨去做个简单的验算,两份的 10% 再加上一份*的 16% 合在一块再除以 3,正好是 12%;而两份的 35%,再加上一份的 20% 也同样的除以 3 恰好也是 30%,所以用 2 比 1 的比例是这个问题的一个解。
好,如果说我们为此花费这些时间还是值得的话,我们还是希望得到一个方法,否则的话我们会很困惑,因为如果你没有掌握这背后的、统一的方法的话,那么如果下一次换一种颜色比如说这里的 V 它要求的是 13% 和 22%,那你可能又要花费一些时间了。
V = (13%, 22%) |
那么首先一个问题是这种颜料能不能勾兑出来。并不是像我们这里所说的那样,每两种颜色给定了以后你都能勾兑出所有的颜色。其实在这个时候我们或许需要第三种颜色,比如这里我们也许从厂房里可以拿到第三种颜色 Z,它的对应的比重是 7% 和 15%。
Z = (07%, 15%) |
好了,这个时候用这三种颜色是否能把它勾兑出来呢?
好,现在我来揭晓答案。正确的比例应该是一份的 X,三份的 Y 再加上刚才我们新添的第三种颜色 Z 一份 1 比 3 比 1。你可以按照刚才同样的方法去推算一下 验算一下,我想答案应该是它。
那么所有这里讨论的事情其实都是颜色,或者准确地讲是颜料之间的那种勾兑混合。这个东西和我们这里讨论的计算几何有什么关系呢?其实它们之间有着非常深刻的联系。
既然谈到几何,那么少不了就要谈到它最最基础的一个概念叫做空间,欧氏空间。
在这里我们将欧氏空间对应于颜色,我们称之为颜色空间,具体来讲我们要将每一种颜色都对应成是这个空间中的一个点。无论这种颜色或者颜料是来自于生产厂家直接供应的那种基础性的颜料,还是艺术家为了创作的需要必须重新勾兑出来的新的颜色。总而言之每一种颜色都对应这个空间中的一个点。
当然这里因为我们讨论的都是正数,那可以认为它基本上都限于第一个象限,这不是主要的问题。那么现在的问题是在于我们固然可以按照这种方法将我们刚才的三种颜料也就是 X、Y、Z 按照横轴也就是刚才比如红色的分量数值以及纵轴,也就是刚才说的绿色的分量的数值对应地画出一个一个的点,三种颜料,分别是三种点。
我们刚才看到过,在我们只有 X 和 Y 两种颜料的时候如果我们要勾兑出 U,那个比重是 2 比 1。其实这件事情倒过来,我们在给出了固定的 X 和 Y 之后我可以将我们目标的那个 U 也在这个屏幕上画出来,如果你画出来的话你就会发现其实非常地巧,我们可以验证一下它们三者是所谓共线的。
如果是这种情况,那么我们认为 U 肯定是能被勾兑出来的,而且它的勾兑比例可以从几何上一目了然的能解释。
你可以再去计算一下,我会告诉你其实 U 到 X 的距离相对更短,U 到 Y 的距离相对更长,而二者的距离之比其实是 1 比 2,而我们刚才勾兑的比例是反过来的 2 比 1。
其实这就是一个规律,也就是说如果我要勾兑的一种颜色恰好是位于这两个顶点的那条连接的线段上,而且它们的距离存在一个比的话,那么这种颜色就必然能够被勾兑出来。而且勾兑的方法就蕴含在刚才的那个比例中,只要把刚才那个距离比 1 比 2 颠倒过来变成 2 比 1,它就必然能得到这种颜色。
你可以作为一个极端的例子去想一下,整个的是如果要勾兑 Y 和勾兑 X 本身的时候另一个分量是 0 是同样的道理。
好,那么刚才我们也可以解释为什么 V 这种颜色必须要借助第三种颜色才能够勾兑出来。因为你大致可以看出来因为 V 并没有位于 X 和 Y 所确定的这条线段上跑偏了,在这种情况下我们说必然要借助 Z,而之所以要借助 Z 或者说准确地讲按照我们刚才那个比例必须是 1 比 3 比 1 也蕴含在这个图中,原理是一样的。
如果在这种情况下我们要做的事情就是要首先确认 V 这个颜料所对应的那个点是不是落在 X、Y、Z 所定义的这个三角形的内部,如果是它就一定能勾兑出来;如果不是,至少它是不能勾兑出来的。
好,如果它能勾兑出来,具体的勾兑的比例是多少呢?在这个图中也给出来了,为此我们只需要去量一下 V 到这三个点的距离,然后找一下它们的比。我们在这里会发现它们的比恰好是 3 比 3 比 1,所以倒过来在这里我们勾兑的比例自然也就是这个最短的最近的这个点对应的那个颜色要取的更多,反其道而行之它要取三份;而到更远的那两个点所对应的颜色所取的比例要更少,完全可以用这个来度量
当然以上的这些结论你还需要在课后再做仔细的推导和严格的验证,在这里你不妨把这个结论记下来:也就是说如果有一种颜料能够被两种已知的颜料勾兑出来,它必然位于二者之间的那条连线上;如果是对于三种颜料的情况,那么某种目标的颜色能够被勾兑出来当且仅当在颜色空间中它位于这三个点所对应的那个三角形的内部,而勾兑的比例是与他们的距离成反比的。
我们虽然不是很喜欢数学,但是不得不还要用一些简单的数学把刚才我们所看到的那个结论严格地表述出来。
也就是说我们如果给定的是平面二维空间中的一系列的点的话,那么这些点所对应的颜料能构造出哪些新的颜料出来呢?我们会发现其实每一种新的颜料从几何来讲,对应于原来那些颜料的某一个调和方案。
那么在这里有一些勾兑方案专门地称之为凸的勾兑方案,或者叫作凸组合 Convex Combination。具体而言,如果是一个凸组合需要有哪些条件呢?
我们说大致有两个主要的条件:
在我们最开始给定的这些点中,哪些是最终对凸包有贡献的被皮筋绷住的,哪些是没有实质作用的,这种性质可以归纳为所谓的极性。
沿着刚才的那个思路,我们观察结论可以表述为这样的一幅图。我们看到在刚才的所有那些钉子中凡事被最终的皮筋绷住的钉子,暂时没有实质作用的这些钉子我们都用青色来表示,有什么本质不同呢?
if there exists a line L through p |
数学上的观察告诉我们,所谓有用的点都有一个共同的特点:经过它们我们总能找到一条直线使得所有的点都落在这条直线的同一侧。
在排序算法中有一个非常有意思的算法:起泡排序 Bubblesort。我们这里的算法设计和它是非常类似的:
如何甄别极点和非极点呢?
我们需要回忆颜料勾兑的例子,一种颜料能够被其他几种颜料勾兑出来当且仅当它落在某一个三角形的内部。反过来像极点这样不能被其他颜料勾兑出来的颜色它就不可能被包含于任何三角形的内部,这样的话我们又往前转化了一步,将我们的甄别任务转化为某一个点是否会被包含于另外的三个点所确定的三角形内部。
根据刚才的分析,所谓凸包问题可以归结为一系列的判断:任何的一个点是否会落在其他的三个点所对应的三角形内部被它们包围,我们称这个为 In-Triangle Test。
基于 In-Triangle Test,我们就可以将非极点们一个一个地找出来并且将它们排除在我们的视野之外。
首先做初始化,要像无罪推论一样将所有的点都设定为极点。接着枚举出所有可能的三角形,对于每个三角形我们还要去考察除它们之外的每一个点 s;一旦我们发现 s 的确是落在当前这个三角形内部,我们就可以立即断定它不是一个极点,从而将它排除在外。
Make all points of S as EXTREME |
void extremePoint(Point S[], int n) { |
我们给出的第一个基于极点的凸包算法虽然效率低下,但是它的意义还是很重要的,它会引出 To-Left Test,后面这个测试几乎是贯穿于我们计算几何这个课程的始终。
每当我们给定了一个点以及一个三角形后,如何来判定这个点是否落在这个三角形的内部?
依然是大事化小小事化了,我们将刚才这个 In-Triangle Test 转化为三次 To-Left Test。也就是说一个点如果确实落在某一个三角形的内部的话,那么相对于这个三角形的三条边所做的 To-Left Test 都会统一的返回 true。
所谓 To-Left Test,就是说这个点相对于有向线段而言位于左侧还是右侧。这里的敏锐观察可以归结为一个点如果落在三角形内部,它的充要条件当且仅当它相对于这三条直线的 To-Left Test 都是 true,它同时位于这三条直线的左侧。
那么现在问题转变为如何判断一个点在线段的左侧/右侧?
bool ToLeft(Point p, Point q, Point s) |
延续极点的思路推广到边,引入所谓的极边。
极边的候选者其实就是来自于任何两个相邻极点的连边,凡是对最终的凸包有贡献的那些边都称之为极边;凡是那些对凸包没有贡献的就不是极边,或者叫作非极边,non-extreme Edge。
就像我们定义极点一样,如果有一条这样的连边确实是极边的话,那么所有的点都会同时落在它的同侧,相应的另一侧就必然是空的。更具体来讲,以逆时针次序凸包边界每一条边都有这样一个特性:所有的点都恰好落在它的左侧,它们的右侧都是空的。
这样我们算法中的实质问题就自然地转化和具体化为如何来甄别任何两个点之间的那条连边是否为极边的问题。
Let EE = null |
按照极边的思路,我们可以将伪代码细化为这样一段真实的代码:
void markEE(Point S[], int n) { |
接下来我们将从一个典型的算法思想减而治之 Decrease and Conquer 进一步改进。
一个经典的应该能回忆起来的算法就是插入排序 Insertionsort。插入排序整个思路可以归纳为将整个待排序序列存成线性结构,接下来在任何时候都将它分为排序和未排序两部分,在未排序部分随机找出一个(一般是两者分界的那个元素),通过一次查找在 sorted 子序列中找到这个元素对应的恰当插入位置。
同理,我们也可以应用于极边算法。
递进式的核心技术是 In-Convex-Polygon Test,也就是判别多边形内部或者外部的问题。
我们要判断一个新引入的点是否是当前的极点,其实本质上就是判断当前这个点是否落在此前的凸包的外面或者是里面的位置关系。
要将刚才那种直觉转化成数学上的判断:每次我们递增式新引入的这个点如果是当前的 extreme point 的话,那么充要条件其实就是看它是否落在当前这个凸包的外面:如果落在外面那它就是下一个 extreme point;否则不是。
如果凸多边形确实是给定的,而且在此后要反复多次地做这类的查询的话,你是可以对这个多边形做一个预处理(本质上是排序)。
我们可以大致以一个点作为基础,在其余的 n - 1 个点中可以找到一个居中的连接起来确定一条有向线段。接下来又是我们刚才的惯用的 To-Left Test,经过这样一次常数成本的操作,我们确实可以判断出来这个未知的点到底是落在左边或者是右边,无论是哪边我们都可以将搜索的范围有效地收缩为原先的一半。
如此往复,我们每一次经过常数时间的成本都可以将这个问题的范围有效地降解为此前的一半,如此下去最终总会到达平凡的情况–trivial case:In-Triangle Test。
但是这个算法却不可行,最重要的是凸包并不是一成不变的,这种情况下我们的预处理是没有效力的。
与插入排序类似,sorted 部分本身就是动态的,即便可以使用二分查找,线性存储所带来的插入成本在最坏情况也会将这种优化无效化。
回到凸包,对于这种情况朴素的方法反而是最好的。我们可以沿着给定的凸多边形边界做习惯性的 CCW 逆时针旋转遍历,可以发现内部的点一定是在左手一侧的;反之如果我们在任何一段发现某一个点在右侧,那么可以立即断定它并非落在内部。
其实我们还有一个任务要完成,解决如何将新引入的这个点附着或者是增加到原先的凸包上去,要使之成为一个完整的可以继续使用的结构。
凸包切线又被称为 Support Line。
只需要花费两次 To-Left Test,就可以明确确定一个顶点到底是来自 ts(L + R) 还是 st(R + L)。
在介绍 GW 算法之前为了更好地理解它的算法思路,不妨温习一下之前我们很熟悉的选择排序。
与刚才的插入排序非常对称,在这里我们的 sorted 和 unsorted 部分是前后颠倒了,这个颠倒实际上是有本质区别的。
我们需要从 unsorted 部分中去找出一个最大的元素,接着将它进行一次交换挪到刚才 sorted 那个部分的首部。悄然之间,sorted 部分就向前迈进了一步。
那么这样一个算法思路从宏观的策略来讲我们可以概括为:每次我们都是维护一个局部的解,然后在尚未处理的部分中要去找到一个与当前的这个局部解紧密相关联的一个元素。没错,凸包就可以这么来做。
我们如果反思一下在 Extreme Edge 那个算法中为什么会需要多达 n^3 的时间,就会发现根本的原因在于我们实际上考察的对象是遍布所有可能的那些边,这些边的总数会多达 n^2,每个又需要 n 时间鉴别。那么有什么改进的诀窍呢?
刚才的 selectionsort 就给了我们提示,也就是说我们或许能够将下一个的查找范围缩小到一个足够小的范围。
Jarvis 观察注意到一些结论:
该图可以说明如何在当前已有的这些极边基础上沿着下一个端点拓展出新的极边:
当前节点称作 k
,它的前驱我们称之为 i
,下一个极边则是 s
。根据刚才 Jarvis 的判断,这个 s
必然来自于其他尚未处理的那些点中的一员。
而 s
之所以可以脱颖而出,其资本在于它是所有这些拐角中的最小者。也许有同学已经跃跃欲试准备用三角函数和反三角函数操作了,但其实有一种基本的技术就可以解决我们的问题。
一个技术细节问题,也就是我们刚才说到的起点和第一条极边应该如何来找呢?
作为第一个点,它至少是极点。在这里针对于我们目前的算法需求,可以对问题进一步简化,也就是找到沿着 y 轴负方向最低的位置。这个点也就是所谓的 Lowest Point,在没有退化的情况下必然是 extreme point,所以我们可以以它为起点。
如果出现多个最低点的退化情况,则优先选择最左侧的点,也称为 Lowest-Then-Leftmost point。
void Jarvis(Point S[], int n) { |
初始化所有点都被视为非极点,接下来找到刚才所说的 Lowest-Then-Leftmost point 并且把它作为我们的第一个点 k
进入下面一个迭代循环。
每一个点当它进入这个循环的时候必为极点,第一个点如此,后面的点也一样。接下来我们则要找 s
是逐渐优化最终找到的极点,任何时候我们都未必知道它就是,需要遍历所有候选 t
。
当 t
通过 To-Left 测试时什么都不处理,s
依然为候选者;反过来 To-Left 测试失败意味着出现在右侧,需要更迭 s
为 t
。
int LTL(Point S[], int n) { |
在前面几节里我们围绕凸包的计算问题给了一系列的算法,从最开始的 n^4 极点算法一直到后面 n^3 极边的算法,再到 Jarvis march 以及 Incremental n^2,我们在沿着一条不断递减的路线在降低这个算法的复杂度。
但是如果计算模型是固定的话,必然有一个我们所说的 Low Bound 的概念:下界,也就是复杂度再低也不会低于某一个极限。
三国中曹操的儿子曹冲有个很著名的故事:曹冲称象。
我们需要度量一个东西的难度,曹冲是要称出一头象的重量,他去找中间参照物石头,通过石头的重量估算出象的重量,而 Reduction 关系就是曹冲的船和水。
那么为什么这个问题可以像曹冲称象一样能够间接通过 A 问题的难度就得到 B 问题的难度呢?
对于 A 问题的任何一个输入,我们都可以曲径通幽式的先把它转化为 B 问题的输入,接下来调用 B 问题的任意算法得到输出,再转化为 A 的输出。
如果 A 问题确实存在某一个下界,而且这个下界是严格大于 n 的,那么我们说 B 问题的所有算法都不可能低于这个复杂度下界。
首先要把我们未知的那个问题(也就是那头象)摆在右边,这里我们考虑二维的凸包 2-dimensional convex hull 这个问题。
而石头则是 Sorting。也许初看这个问题可能会很迷茫,排序这个问题和凸包这个问题一个是纯粹的抽象计算问题,一个是具体的几何计算问题,二者之间怎么会有联系呢?
排序问题的输入可以理解为在数轴或者平面上 x 轴一系列的点,在图中我们只取了四个点。为了转换为凸包问题我们需要辅助线,以抛物线作为标尺将每一个点做提升变换,将 n 个数字转化为平面上的 n 个点。
来自抛物线上有线个点的凸包都具有这样的一个特性:最左侧的那个点和最右侧的那个点会在上面连上一条纵跨的一条单调直线。
这样我们就完成了 Reduction 的第二步:将凸包问题转化为排序问题。输入是无序的,输出是有序的,这正是排序算法的要求。
(注:这里有一个疑惑就是如果是正五边形,那么这个左右边界又该如何去界定呢?边界的连线并不单调。)
所以排序算法的下界是 nlogn,那么凸包问题也是如此,成为 Convex Hull 的下界。
那么我们来看一个下界意义上讲最优的算法:Graham Scan。
Graham Scan 首先要做的一件事情是一个预处理,一个排序。这个 presorting 其实就是要找到某一个特定的点,并且将其余所有的点按照这个点所对应的极坐标按极角来做一个排序。
那么具体的这样第一个点应该找谁呢?
其实任何一个极点理论上都是可以的,同样为了简化算法的解释和实现,我们不妨依然采用前面所讲过的 Lowest-then-Leftmost point 为 1 号点。
接下来会有与 1 号成角度最小的 2 号点,这里不妨假设 1、2 号点为同一高度,并且没有三点共线的情况,接着按照 (1, 2) 极轴的夹角从小到大命名其他点。
Graham Scan 算法的数据结构也很简单,只需要两个栈 T 和 S。初始化时依次将 1、2 入栈 S 中,其他 n-2 个点自顶到底存入 T 栈。
而排序可以选用任意排序,只是对象变成了点,而比较器变为 To-Left Test。
这个扫描过程中要关注三个东西:S 栈栈顶以及次栈顶、T 栈栈顶,我们可以用 S[0]
、S[1]
、T[0]
表示。
while(!T.empty()) { |
9 号点被包含在了某一个三角形(1-8-10)的内部,它应该被排除掉。
根据欧拉公式,平面图中所有边的数量包括面数加在一起依然和顶点数目保持同阶,边数不会超过顶点数的三倍。
if: S.size()++; T.size()--; // 1 - 2 |
归并排序作为引子引出我们的算法。
Divide-And-Conquer 要求我们接近均匀切分 divide,接着我们把这些结果合并起来成为有序序列,变成最终结果。
凸包问题也是如此,把输入的点集分成大小规模接近的子集分别求出它们的凸包。问题实质就变成了我有两个凸包子集之后如何将它们合并得到更大的凸包。
找到一个公共核使得这两个待合并的子凸包能够同时关于这个点是角度有序的。
二路归并采用环形次序,然后 Graham Scan 即可。
我们预选的那个来自第一个子凸包的 centroid point 不幸落在第二个子凸包的外面,在这种情况下我们应当如何完成二者的归并呢?
不妨做一个假设,待合并的两个子凸包或者说它们对应的点集是沿着某个方向是可分割的,彼此独立。如果这样我们的合并任务就会变得更加简明、简单。
为了保证这一点,我们引入一个预处理:按 x 轴排序。
我们可以在最初构造一个子凸包的时候记下 leftmost 和 rightmost 各是哪两个顶点,剩下几乎不用花时间:把此前计算结果延续下来即可,而分摊到每一次合并常数时间就够了。
计算几何的重要之处在于它是多门技术与学科的基础,例如图形学、CAD、GIS、路径规划等。这些技术的背后原理往往是基于计算几何的本质上。所以该门课程的学习对养成计算几何理论的总体认识很有帮助,这种认识将为学习者日后的研究工作提供几何的视角。
- Awareness of Computational Geometry theory that will help students incorporate Computational Geometry into their future research
- Comprehensive understanding on fundamental paradigms/strategies for solving geometric problems, incremental construction, plane sweeping
- Essential geometric structures and algorithms such as polygon decompositions, Voronoi diagrams, Delaunay triangulations
本课程的教学目标有三:
Computational Geometry requires some skills of algorithm design and analysis as well as programming, but you don’t need to be an expert before learning this course. Actually, C/C++ programming experience and some basic knowledge of common data structures will be enough. To make sure whether you are qualified for learning this course, check the list below:
- C/C++ programming: variable, function, struct, class;
- Algorithm design and analysis: complexity, amortized analysis, recursion, divide and conquer, linked list, binary search tree, priority queue.
计算几何这门课对数据结构和算法基础和编程基础有一定的要求,但这并不意味着你需要精通所有相关课程。实际上,你只需掌握一些常见数据结构,拥有一定的算法分析能力,以及C/C++语言编程的基本技巧。为确认自己是否适宜选修这门课程,不妨对照以下清单做一清点:
这门课已经开设 18 年之久,虽然国外诸多著名高校都开设了这门课程,但国内做计算几何方面的学校和机构屈指可数。
说到计算几何,我们要做一个名词辨析。
如果你第一次听到 Computational Geometry,首先注意到的肯定是几何,脑海中浮现的是曲线、曲面诸如此类。事实上我国数学家苏步青八十年代就曾出版过一本《计算几何》的书。 此计算几何非彼计算几何,这门课更加强调的是计算。现代计算几何人们公认诞生于 1978 年 Shamos 那篇著名的博士论文,所以这门学科到现在也不过区区四十年的发展历史。
当然计算几何之所以很重要,就是因为它是很多学科尤其是技术学科的基础,包括典型的图形学、CAD、GIS、路径规划等等……最后都会回到计算几何这些基本的问题。
在学习之前如果一言以蔽之概括一下的话,计算几何就是就是”算法设计与分析”的几何版,它所讨论的对象、问题的表面形式都是几何的,它求解这些问题的方法、策略高到上面的方法论其实也都是几何的。尽管从这个方面讲计算几何只是算法设计与分析的一个分支,但是正因为它融入了很多古典的一些离散几何学、组合几何学等等精华的结论和方法,所以它不仅仅是一个几何和计算两个问题的物理反应,而是很深入的化学反应。
计算几何强调本质的东西就是要形象。
没有人喜欢复杂深奥的东西,所以这门课如果在学习过程中没办法很好理解推导和公式,不必拘泥于复杂深奥的泥潭,暂时放下它,将注意力放在图形和具体表现上。
]]>简单介绍一些基本算法,包括:搜索、贪心、二分查找与三分查找、序列分治以及排序。
深度优先搜索(Depth-First Search)优先遍历一个后继结点的子树内所有结点
广度优先搜索(Breadth-First Search)先遍历所有后继结点,再遍历后继结点的后继
深度优先搜索 DFS
广度优先搜索 BFS
果树剪枝是为了让树长得更好看,结出的水果质量更高
搜索树也可以剪枝,让搜索效率更高;注意不要把最优解给剪枝掉了
可行性剪枝
最优性剪枝
此外还有其它剪枝思路,例如在双人游戏中有 Alpha-beta 剪枝等,在这里不详细展开
在贪墨成风的反乌托邦世界中,四处都是生化改造植入体。动画剧聚焦一个在街头长大的鲁莽天才少年努力想成为边缘行者:拿钱办事的法外之徒。
近些年里,游戏改编的影视作品越来越多,每一部都会宣称自己制作如何精良,但它们要么如《龙之血》《双城之战》那样摒弃了游戏玩法、着重于挖掘背景故事,要么如《神秘海域》或者《光环》,大幅改造甚至看不起原作剧情直接另起炉灶,没有任何一个能像《边缘行者》这样忠实地遵从原作的框架、同时还能讲好一个故事。它甚至还弥补了《2077》至今未能实现的缺憾:我们终于看到了单分子线在大杀四方的同时也能实现骇入,也终于看到了 NCART,其实是能坐人的。
《边缘行者》播出以来在各个评分网站上都收获了不错的口碑,这不仅证明了《2077》确实有着优秀的基础框架,可惜潜力没有被充分发挥出来;同时也是打了那些自大的好莱坞编剧们的脸:老老实实照着游戏内容拍,远比你们一拍脑门搞出来的那套东西更能讨好观众。
当然,出色的作画、讨喜的人设,还有以上说的种种,固然能够大幅提升玩家们的观感;但真正能够打动观众的,还是赛博朋克的内核。
本剧的主角,大卫·马丁内斯,在故事刚开始的时候,还是一个不谙世事的学生。他的母亲葛洛丽亚在市政部门工作,薪水微薄,日常工作是清理横死街头的赛博疯子和帮派分子。借着职务之便,能够接触到这些死人身上拆下来的义体,她便通过把义体倒卖给边缘行者们来赚取外快。而她这样辛辛苦苦、不惜违法地赚钱,目的就是供养自己的儿子在荒坂学院念书。
荒坂学院是荒坂公司附属的精英学术机构,费用高昂,但学员能够成功毕业,就有机会进入荒坂公司工作,再之后,就有机会一步一步爬到高层——这在葛洛丽亚看来,是普通人唯一能够改变命运的手段。
而对大卫来说,自己和学院里其他那些少爷终究不是一路人。尽管成绩优异,但连备用制服都买不起的贫寒家境让他处处遭到排挤。平时,他只能在黑超梦带来的感官刺激里麻醉自己,同时靠帮黑市的义体医生推销这些超梦来赚些零花钱。
本来,日子像这样平平常常地过去,也许大卫最终会成为荒坂公司的一颗螺丝钉,在无止境的工作和加班中被消磨殆尽;又或许时运眷顾,他真的会在企业里步步高升,最后出人头地呢。
但按部就班的生活因为一场车祸戛然而止。赶来救援的创伤小队把没有保险的母子二人留在原地等死,超级摩天楼里简陋医院的廉价急救套餐终于还是没能救回葛洛丽亚。
大卫把母亲火化——这是最便宜的丧葬方案——抱着母亲的骨灰回到了因为租金逾期未交而把他拒之门外的家。
他甚至没有哭泣。
在夜之城,死亡会让人麻木。
可他在母亲的遗物中发现了一件义体,他在黑超梦中见过它。斯安威斯坦,军用级义体,能够触发缓时。发动时,周围的一切仿佛静止,只有使用者能够移动自如。
到了2077年,斯安威斯坦已经发展到可以人手一件的程度,但在剧中故事发生时,装备这件义体还是一个禁忌。不只是因为它专供军用科技内部使用,外部难以获取;更是因为,普通人使用它,十有八九会发疯。
大卫没管这么多,他甚至在不知道什么是免疫抑制剂的情况下,去找那个相熟的义体大夫安装了斯安威斯坦。随后他直奔学院,在全班同学面前,把之前羞辱了自己和自己的母亲的田中痛打了一番。
在这之后又是无尽的空虚。他漫无目的地行走在那些曾经走过无数遍的道路上,不知道该做些什么。
而在这时他遇到了生命中的光。
Lucy,我偶尔,只是很偶尔的时候,会问自己,如果我没有见过你,我到现在的人生会不会不一样?没有成为边缘行者的我,没有遇到爱情的我,没有结实这么多同伴的我。我也许会给田中道歉、回到荒坂学院、成为义体实验对象,也许,有那么一丝的可能性,我能够进入公司的高层,能够实现妈妈的愿望。那样的什么都不知道的我,会不会也很快乐呢?
但是再给我一次机会,我仍然不可能做出其他的选择。在那趟列车之前,在妈妈遇到车祸之前,在田中把我揍得体无完肤之前,我就见过你了。也许没有真的见过你,也许只是在梦里见过你。但那一头银发,是我黑暗中的光,我早已见过一次又一次,就算是在梦里我也不会认错。
我的人生早已注定了。我注定会认识你。
而这是我遇到过的,最最最幸运的事。
他之前就遇到过好几次,一头璀璨耀眼的银发,但总是转瞬即逝,以至于他会以为是幻觉。不过,这次是在轻轨上,她无法再那么轻易地消失了。
女孩名叫露西。他看到她在偷取别人的芯片,她发现了他窥伺的目光,冲突、解释、握手言和。他提出帮忙,三七分成。之后是一番奇遇,她把他邀请到家中,分享了自己隐秘的梦想——离开夜之城的牢笼,去月球生活。他们在超梦里登月,在虚拟的低重力下跳跃、欢笑。然后美梦醒来,一伙壮汉把大卫拉回现实。他们是赛博朋克,即是边缘行者。斯安威斯坦本是那伙人中的头领曼恩向葛洛丽亚订购的,如今后者杳无音信,露西按图索骥找到了大卫,现在他们要拿回自己的东西。大卫坚定地要为他们工作来偿还债务,思忖良久,曼恩答应了下来。
就这样大卫加入了这个小团体,认识了浑身装满义体的大块头曼恩 Maine、曼恩强壮的女友多利欧 Dorio、有着一双灵活手臂的技术狂皮拉 Pilar、皮拉的萝莉妹妹瑞贝卡 Rebecca、沉默寡言的黑客专家琦薇 Kiwi、以及老练可靠的司机法尔科 Falco。
大卫在这里找到了家的感觉、和同伴们打成一片,也在一次次任务中逐渐成长为了优秀的边缘行者。他向 Lucy 吐露了自己的感情,答应要带着她去月球。Lucy 吻了过去,两颗心贴在了一起。
如果到此为止,不过是一系列热血番中常见的展开。主角团中有人死去、有人离开,但主角总是借着光环无法倒下。可赛博朋克的世界不是童话故事。一次任务中,大哥曼恩终于无法控制住自己日渐被义体所侵蚀的神经系统,失手攻击 Kiwi,打乱了行动计划,Lucy 作为备用黑客迫不得已加入任务;后面又因为失神造成了 Dorio 的死去。面对着 NCPD 和创伤小队的双重围堵,曼恩知道自己大限已至。面对前来试图营救自己的大卫,他只是淡然地说了一句:“这就是我的终点了。”
随后,便用烈火将自己和爱人焚尽。
他们都说,夜之城的传奇都在坟墓里。
这大概是真的吧。那么,曼恩大哥也算是一个传奇了吧。
但如果有选择的话,我宁愿不做那个传奇。毕竟,以前你们好多人和我说,说我老是为别人的梦想而活;而现在我也有自己的梦想了呢。我的梦想,就是我之前承诺过的,帮你实现你的梦想。你那时说你的梦想是去月球,我从来没有忘记过。月球的单程票是25万欧,当然,如果想要在那里生活,应该还需要更多的钱吧。如果完成了这最后一份差事,大概就足够了。如果能拿到赏金,如果能和你一起去月球,是不是也挺不错的呢?
此去经年,大卫成了小团体的新领袖,在圈子里的声望也越来越显赫。Rebecca 在和大卫搭档的过程中对他暗生情愫,可这份心思又怎么能够挑明呢?露西和他一起住进了漂亮的大公寓里,但不再参与组织的工作。当时,她在任务目标的大脑中发现,对方想要拿大卫作为荒坂的新产品“义体金刚”的实验对象。为了保护大卫,她删除了相关信息,没告诉任何人;在这几年中,她名义上拒绝参与团队工作,实际上却是在追杀任何了解实验计划的荒坂员工。
Lucy 的讳莫如深在大卫眼中看来是逐渐的疏远,可他自己又何尝没有改变呢?
当初,为了跑步时能够追上 Lucy,他给自己装上了斯安威斯坦外的第一个义体——一对人工肺。后来,曼恩嘱托他,为自己多装几个义体,变强,活下来。于是几年下来,大卫也变成了一个钢铁大块头,一个机械部分多于肉体的义体改造狂。他用的免疫抑制剂,药效也越来越猛、剂量也越来越多。他会时不时地抑制不住自己手臂的抖动,一如曼恩最后的那些日子。
明眼人都看得出来,大卫离赛博精神病不远了。
在《2077》里,也许是因为 Relix 芯片的特殊性,又或许仅仅是因为 CDPR 偷懒没有做出来,V 就算把自己浑身上下改装个遍,也感觉不到义体的副作用。可对普通人来说,你的身上不属于自己的部件越多,你的神经系统和肉体对它们的排异反应就会越大,最终,你的大脑会成为机械的奴隶,这就是赛博精神病。成为赛博疯子就是每个没在这之前就挂掉的义体改造狂最终的结局,而在终点等待着他们的,就是疯控小队。
大卫相信自己有某种天赋。这天赋从他还是十几岁的孩子、刚刚装上斯安威斯坦就能熟练掌控、随心所欲地运用就能看出端倪。要知道,就算是V,发动斯安威斯坦的效果都需要60秒游戏内时间的冷却。这天赋让他能装上一个又一个的义体,而不良反应比起其他人来说又是少之又少。这天赋让他觉得自己是“独特“的,让他觉得他能在夜之城里混出个名堂,让他觉得,带着露西去月球生活,也是有可能的。
所以说就算 Lucy 和 Rebecca 都劝说他,不要再改装自己了,拆卸下一些义体吧,他还是固执地为自己安装更多的功能模块。
在他看来,这是能让他赚到足够去月球的钱的,唯一的道路。
机会来了。一个大单子。拦截荒坂的一辆运输车,取到货,数百万欧,足够团队里每个人过上逍遥日子。当然,这自始至终都是诱饵,目的是让大卫穿上“义体金刚“、与军用科技斗个两败俱伤、最后由荒坂公司自己回收其中的实战数据。另一边,Lucy之前的行迹败露,又遭到Kiwi的出卖,被已成为荒坂哈巴狗的中间人法拉第扭送往荒坂。
计划如公司所料般进行,大卫一伙被军用科技包围,法拉第用合成的 Lucy 声音哄骗大卫穿上义体金刚,Kiwi跳反,偷袭法尔科后扬长而去。此后,按计划,大卫会发疯、与军用科技同归于尽——可 Lucy 在最后关头挣脱,向大卫发出了警告。这义体是把你赛博精神病的最后一根稻草,就算你没有疯,超量的免疫抑制剂也会让你的理智滑向边缘之外。求求你,千万别装。
可大卫有什么选择呢?穿了这义体,就不能陪你去月球;但不穿这义体,就不能救下你。这看似是两种选择,但对大卫来说,可能性只有一个。
义体安装完成,Rebecca 帮他注入了一大瓶抑制剂。他启动机体,反重力装置和磁场发生装置风卷残云般摧毁了军用科技的包围圈。和荒坂料想中的不同,大卫还保持着清醒。下一步,他们向荒坂塔开去。
可是我也许一开始就知道这是不可能的吧。大概从我安装上斯安威斯坦那天起,我就从它前主人的超梦中预见到了自己的结局。曼恩大哥那时对我说,那就是他的终点了。我当时不甘心,我当时觉得也许我再努力一点就能救下他了。但现在我知道了,当一个边缘行者的终点到来时,他会明白的。正如这就是我的终点了。
我没能救下妈妈,没能救下曼恩大哥,没能救下瑞贝卡,但我终于救下了你。
在月球好好生活吧。去感受地球六分之一的重力。去感受太阳的温度。
只是对不起,我们不能一起去了。
穿过荒坂和军用科技的重重围堵,一行人终于来到了最后的目的地。在荒坂塔前,大卫注射了最后一管免疫抑制剂,这其实就是他的死亡宣告:就算他最后战胜了重锤,也无法活着离开。更何况我们都心知肚明,他不可能打赢。
相比于 V 的轰轰烈烈,大卫的荒坂塔之旅,结束得既迅速又潦草。早在突围时大卫就开始在疯狂和清醒的边缘游走,越接近公司广场时更是越发难以稳定智识。恍惚中他登上了荒坂塔的顶端,在某种意义上完成了母亲的梦想。随后他冲进大楼,在这里遇到了亚当·重锤,一个他以为并非真实的人物。一个几乎只有大脑是原装的机械怪物。一个全无人性的梦魇。斯安威斯坦对重锤来说不过是初级的植入物,面对他,大卫毫无胜算。
在此时的大卫身上我仿佛看到了自己。曾经自命不凡地以为自己是独特的那个,在现实日复一日的捶打下逐渐动摇了信心,开始怀疑自己,最后终于在某一刻发现,自己的“独特”在别人眼里可能只是个笑话。看到重锤,正是让大卫明白,自己的“独特”、“对于义体的天生钝感”,在这种公司培育出来的怪物面前,根本不值一提。
于是他释然了。就像当初的曼恩那样,大卫也明白了自己的结局。他选择了他能做到的最好的事:给法尔科和露西争取时间,让他们带着钱离开。让露西能够实现去月球的梦想。至于他自己呢?
从第一集开始大卫因为没有钱所以只能眼睁睁地看着本来有救的母亲去世,到最后站在荒坂大厦的顶端往下纵身一跃,他确实给垄断这个世界的大企业造成了一点小小的麻烦,但是归根结底,他都始终无法像传统的 TRIGGER 主角们那样用自己的意志去决定自己的命运。
恰巧相反,男主大卫从一开始接受移植手术到最后组装金刚机甲其实都是在接受一种看似自由选择的命运操弄,这种无论如何努力却依旧还是在既有框架体系之中的绝望和无力感,个人认为是对于 TRIGGER 传统的【钻破体系障碍】的逆反,但同时也是对于赛博朋克这一题材的绝佳诠释。
在赛博朋克的世界里,一切的传统价值都被解构掉了,就连【相信】这个词也不能够被相信了,只有赤裸裸的能够被量化的金钱、身体机能改造或者成瘾品才能作为生存的意义,以至于大卫实际上只能够为了别人而活,为别人的梦想而活,他自己根本找不到自己为什么要活着的原因。
赛博朋克这个概念本来也就是作为一种现代化狂飙突进到极点之后的反乌托邦,因此大卫的迷茫其实也有其一定的现实意义。
有批评者认为本作剧情不佳,觉得情节转折推进生硬、大卫行事动机薄弱,觉得露西明明可以和大卫解释清楚,觉得大卫明明可以拆下义体,觉得两个人明明可以靠着攒来的钱远走高飞,又何必走到最后那一步呢?
可我们别忘了,这里是夜之城,在这里,公司就是不坏的王权。
哪怕是当年强尼·银手和摩根·黑手把两颗战术核弹塞进了荒坂塔,把它夷为了平地,荒坂也能够在原地重新建造一座更气派的大楼。哪怕是后来无所不能的、最后成了城市之王的V,也不过是杀了几个西海岸的董事会成员、暂时阻止了荒坂三郎借尸还魂,荒坂在日本的根基并没有动摇、何况三郎的意识在别的分部可能也有备份。哪怕是荒坂就此一蹶不振,军用科技、康陶、夜氏集团也会立刻把它的份额瓜分殆尽。一切都不会有任何改变。
而大卫呢?大卫后来租住的公寓看上去相当豪华,但如果不接任务,他可能会连抑制剂都供养不起,更别提攒钱了;他的团队已经算是圈子里的顶尖队伍,在中间人法拉第眼中也不过是一批耗材;后者还幻想着一步登天进入公司,但在真正的公司人眼中也只是个逐利的小丑。在夜之城,哪怕你混成了来生的传奇,在公司眼里也是随时可以碾碎的蛆虫。大名鼎鼎、天赋异禀的大卫·马丁内斯,甚至都打不过亚当·重锤这条荒坂豢养的看门狗,更遑论撼动公司的一根汗毛。
在边缘行者们眼中,大卫最终迎来了一个壮烈的牺牲,一个传奇式的结局。
而在公司眼中,整个事件自始至终也没有惊动任何一个荒坂家族的成员,甚至可能董事会都对此漠不关心。
只是疯控小队又在公司广场上处决了一个赛博疯子,夜之城普普通通的一天而已。
对于在这样一个世界里的底层民众来说,只有梦想,只有那一点点对于未来的希望,才能支撑着人活下去。
人们总是对大卫说,你不要为了别人的梦想而活,但大卫根本不知道该梦想什么。在城市的边缘徘徊了那么久,他早已丧失里梦想的能力,哪怕最后对女主说出,“我的梦想就是完成你的梦想”,也依然没有跳出为别人梦想而活的桎梏。这其实也揭示了他注定的悲剧结局,因为直到最后,他也没有学会该怎样为自己而活。
就算是那些有梦想的人,又能梦想到多远的地方呢?母亲的梦想是迎合,是儿子有朝一日能出人头地;曼恩的梦想是苟且,是靠不断变强的身体和同伴的支持走下去;露西的梦想是逃避,是逃往一个能够远离荒坂的触手的地方。而就连这些卑微的梦想,也会被公司一个接一个地毁灭。就算没有遇到车祸,葛洛丽亚的身体也会被不断累积的账单、债务和日夜的操劳压垮;就算任务没有出现差错,曼恩也会因为义体对神经系统的侵蚀而一步一步滑向彻底疯狂的深渊;而如果没有大卫,就算露西特意租了一间可以看到发射场的公寓,那一艘又一艘腾空而起的飞船里,也永远不会有她的身影;就算是现在,露西成功地来到了月球上,她又能躲得过荒坂的清算吗?
潘多拉因为好奇打开了众神留下的盒子,所有丑恶的东西一齐向人间四散飞去。在最后一刻她终于关上了盒子,留下了希望。有人说这是众神最后的怜悯,就算周围一片黑暗,希望仍存。
也有人说这是众神最大的恶意,因为每一个希望背后,总有绝望随行。
L’enfer est pavé de bonnes intentions.
哦,还有爱情,这最后一点慰藉,最后一点美好的东西。
可夜之城的爱情,也不过是风中的烛火,轻轻一吹,就熄灭了。
对未来的担忧往往会被人们以鲜明而极端的方式所表达出来。比如阿道司·赫胥黎的《美丽新世界》和乔治·奥威尔的《1984》等经典反乌托邦的末日预言,或者赫伯特·乔治·威尔斯的电影《先河》呈现出的未来世界完美或近乎完美的愿景。
而兴起于20世纪80年代由“控制论”和“朋克”两个概念组合而成的“赛博朋克”,正诞生于社会大变革下人们对未来的担忧的时代。于是,一场基于赛博朋克概念的文学运动逐渐蔓延,其所传达的精神文化通过各种形式的媒体传播,一种包罗万象、不断增长的亚文化随之流行。
赛博朋克展现了一种信息高度发达的未来人类社会图景,这种社会表面充满和平,内在却充斥着难以控制的阶级矛盾、资源紧缺等弊病。物质文明泛滥并高于精神文明,致使人类精神在高度发达的技术社会难以实现真正自由,从而具有明显的反乌托邦特性和悲观主义色彩。
从1984至今,科技迅速发展,新技术层出不穷,就在我们的世界随着现实时间的推进而更新的同时,赛博朋克下构建近未来世界的元素也大大增加。
尽管赛博朋克不是现实生活的完全映射,其狂想的架构更是塑造了许多个陌生的世界,以至于需要一定的接受度和反应时间。但赛博朋克作为一种基于时代环境的自我反思,揭示出了其中反映的数字时代的认知、认知局限与认知方式的转变,也持续地发人深省,供给科技伦理更多善意。
二十世纪60年代,是一个社会大变革的年代。二战的滚滚硝烟与第三次科技革命的爆发,导致了这个黑暗压抑又有一丝光明前景的时代,未来近在眼前,历史还未走远。
一方面,曾经自由民主的国家无法抑制失业率上升或通货膨胀,国家干预也无法解决诸如种族主义或个人对意义和秩序的渴望等社会问题。超级大国利用游击队和傀儡政权作为他们争夺世界霸权的筹码。越来越多的经济学家和未来学家开始怀疑,冷战最终不过是日渐式微的西方世界的杂耍表演。
第三世界的主要国家正在崛起。日本比欧洲和美国更娴熟地玩着资本主义的游戏,中国和东南亚“七虎”在不受西方自由主义影响的情况下开始了自己的致富之路。而西方则无法与他们日益提高的生产效率和越来越多的劳动力相抗衡。
世界环境也在走向地狱,生物学家雷切尔·卡森早就在《寂静的春天》一书中对使用DDT和其他杀虫剂存在的危害发出了第一次警告,而这仅仅是个开始。事实证明,有毒废物造成的危害比任何人想象的都要多,公众的担忧似乎也无法阻止农药进入空气、土地和水中。
工厂和城市的有毒排放物不断地进入环境之中,持续的气候变化也迫在眉睫。1979年,世界气象协会(WMA)警告称,全球变冷已经持续了几十年,冰川期很可能即将来临。
另一方面,20世纪后期,控制论、信息论、计算机/网络、生物遗传工程等飞速地发展。尤其是80年代中期后,虚拟现实技术、人工智能技术,计算机图形学、仿真技术、多媒体技术、人工智能技术、计算机网络技术、并行处理技术和多传感技术的发展,人类生活水平前所未有地提高了。
现代性许诺了美好的前景和理想,诸如平等、自由和理性。人们在希望和绝望之间摇摆不定。终于,这种矛盾产生了科幻艺术创作的参考设定——赛博朋克。
事实上,赛博朋克所具备的元素在20世纪初的科幻小说中就可见端倪。在视觉文本出现以前,科幻小说是科幻领域的主要的表现形式。在整个十九世纪中,科幻创作经历了草创期以及从古典到现代的转型,工业革命引发了人类文明史上科技前所未有的大发展,这为作家们提供了用之不竭的创作激情。
进入二十世纪后,科幻领域开始出现变化,科幻电影、绘画、连环漫画、广播剧以及电视作品先后出现。梅里埃的《月球旅行记》成为了科幻电影的发端,也揭开了小说改编成电影的序幕。
1982年,世界上第一部赛博朋克电影《电子世界争霸战》在美国上映,《漫长的明天》将科幻小说和黑色电影相融合,《银翼杀手》则展现了一个雨后华丽的未来都市。
而真正开启了赛博朋克流派的发展则是1984年布鲁斯·贝斯克的《赛博朋克》和威廉·吉布森的《神经漫游者》问世。事实上,无论从哪方面来评价,《赛博朋克》和《神经漫游者》都是赛博朋克流派的权威之作。
《神经漫游者》的展望中,未来的两部分泾渭分明。一边是肮脏、充满犯罪的物质世界,一边是明亮的网络空间;一边是大街上为了生存抗争的人们,一边是绕地球环行的贵族努力找办法填补他们人为延长的寿命;一边是来自我们世界的老旧残迹——在故事早期,凯斯买了“一把50年前南美版瓦尔特PPK手枪的越南仿制品”——另一边则是能够让人们用新的肢体、眼睛和皮肤来强化身体的尖端科技,只要他们买得起。
于是,借助流行文化、科幻小说、戏剧和电影,这些基于既定事实又承载着超越想象力的故事,以《神经漫游者》为代表的赛博朋克作品从多个侧面描绘了一个关于未来的模糊信仰。
它既包含着对技术的依赖和恐惧、对未来浪漫而悲观的想象,又掺杂了身处技术爆炸时期的后人类对世界与自我的颠覆性认知。而这些杂陈的情绪以一种哲学化的方式被植入赛博空间的意象中,使它本身作为一个通信科学发展的产物,承载了更加值得深思的文化隐喻。
20世纪80年代明确了赛博朋克作为一种风格的界限,一并开启了赛博朋克流派作品的创作。
同时,在计算机领域突飞猛进的发展下,到了赛博朋克出现的八十年代,信息技术、生物工程、基因技术、网络、黑客等名词逐渐进入公众领域。人机联网,人工智能,虚拟空间等开始在现实生活中逐步实现。而在赛博朋克文学和电影诞生之初,赛博朋克就将这些先进技术与很多现实问题联系在一起。
其中,帕特·卡蒂甘的《合成人(1991)》构筑了一个由复杂的人机合作所掌控的世界,关注大脑改造技术的心理暗示;鲁迪·鲁克的Ware系列则延续了《神经漫游者》里有自我意识的人工智能这一思路,并得出了逻辑上的结论,即在此基础上产生的机械生命体是如何在其后代中进化的。
K.W.基特曾以《极度恐怖》而闻名,他推出的《玻璃锤》,则是一部结合了《硬线》风格的寓言故事——诺斯替主义邪教的超速者和走私者以及他们救赎世界的理念误入歧途的图景。
格雷格·贝尔则在《血音乐》一书中创造了一个复杂的未来,人类会被因基因改而拥有自我意识的细菌所破坏和改造。赛博朋克主题出现在他后来的一些作品中,尤其是以1990年的《天使女王》为开端的系列,书中的故事发生在洛杉矶,在那里纳米技术带来了根本性的变化。
布鲁斯·斯特林的作品,比如《网络岛》,对黑客这种亚文化开始特别关注。同时,斯特林是赛博朋克舞台上的一个标志,他编辑的《镜影:赛博朋克选集》是一本重要的短故事合集,包括吉布森、卡蒂甘和鲁克的作品。在这本书的前言中,斯特林写道:
“有些中心主题在赛博朋克中反复出现,比如身体入侵,包括假肢、植入电路、整容手术和基因突变。更重要的主题是心灵入侵:人脑-电脑交互,人工智能,神经化学——这都是从根本上重新定义了人性本质和自我本质的技术。”
于是,第一波浪潮中的赛博朋客作家继续他们的多元化发展,赛博朋克的思想和意象向四面八方扩散。赛博朋克的成功展示了一种思想在实现实体表达之前所具有的力量。正如乔治·奥威尔在《1984》中的构思已经成为了政治话语的一部分。因此,赛博朋克的存在也同时影响着现实世界中计算机和其他领域的发展。
然而,这并不意味着赛博朋克的发展就是一帆风顺的。事实上,在赛博朋克小说上发生的事情,同样发生在流行文化任何一个分支里的成功新事物上。布鲁斯·贝斯克说,“它从一个意料之外的、崭新的原创事物变成一股短暂的新潮,一个可重复的商业公式和一种老套的修辞。”
《神经漫游者》的主题变成了某种清单。疏离的独行者在镜影中做着毒品生意或飞快地入侵电脑,这样的故事很快成为标准内容。然而类似故事太多了,一些90年代最重要的赛博朋克故事,将这种公式推至具有讽刺意味的极端,使得赛博朋克终于在90年代走向了退潮。
尽管看起来赛博朋克走向了消逝,但奇异的是,随着千禧年的结束,赛博朋克迎来了它最重要的时刻。它的影响力向外扩展,朝着许多不同方向突变,最终进入了主流文化。
究其根本,是因为赛博朋克本身的吸引力远不止于表面的皮革、铬合金和霓虹灯。风格显然很重要,但是赛博朋克更为重要的内核是:人们可以通过自我的表达充分说明所处的文化。
早期的赛博朋克作家们和他们的同龄人担心的很多事情都没有发生。冷战确实结束了,但不是通过核战争的形式。苏联解体了,即时它会因错位的怀旧情绪而复苏,但苏联式的共产主义对任何极端狂热分子来说都不再是未来的潮流。日本十年前陷入的经济困境依然深重,看不到真正复苏的希望。
上世纪70年代的许多大型企业要么倒闭,要么被其他企业吞并。冰川纪似乎不太可能在短时间内再次降临,人口这颗滴答作响的巨大炸弹正在缓慢而稳步地解除武装。
当然,新的恐惧总会取代旧的恐惧,全球变暖在许多人的脑海萦绕不去。曾经被认为已经解决的传染病问题又回来了,抗生素的滥用与自然进化相结合,制造出了越来越危险的微生物。
人们所担心的不再是苏联霸权,而是宗教狂热和恐怖主义。计算机化无时无刻不在给工作和娱乐的新领域带来革命,但也有代价,包括失业、数字鸿沟的扩大。精通技术的人和不具备使用高科技工具进行工作的能力的人之间的鸿沟,以及传统社区形成和维护方式的崩溃,网络互动无法(现在,也许永远)完全取代传统的社区。
社会构架偏向全球化,各个地域文化通过各种形式交融。人工智能发达,有强大的系统通过各种手段统治着所有人的生活。
在这样的背景下,赛博朋克再一次迸发出了新生的力量。当下大多数赛博朋克作品,都在二元对立下重新定义了“人”:机器人也可以为自己赋予人格,并成为新本体。《攻壳机动队》中,反抗政府过度化发展科技的群体被政府视为可弃之物,他们游走于城市边缘游行示威,最后却被政府抓走做义体人实验。生物组织通过无数次实验后,第一个真正意义上的义体人素子出现。素子竭力寻找自己的真实身份,自我觉醒让她重获新生。
《银翼杀手2049》中,复制人K的工作任务是追杀老式型号复制人。影片中,人类作为复制人的创造者,主宰复制人的生与死。
在游戏方面,《杀出重围》为CDPR创作《赛博朋克2077》奠定了基础。小岛秀夫在十年之前创作了《掠夺者》,也吸取了神经控制论和人工智能等元素,并将之运用在《合金装备》,获得了极大的成功。、
人类对世界的关注具有周期性。思想和风格会重新流行起来,故事也会不断重复。如果处理得当,旧的观念可以被打磨成新的、引人注目的东西,使人们对最原始的恐惧和希望产生强烈的共鸣。我们生活在赛博空间的临界点上,科学与人文问题和以往一样重要。
赛博朋克是我们这一代的流派。它是在计算机的体积和成本都非常巨大的时候被构想出来的,并预示了一个由微型处理器和超导体组成的世界。它赋予了黑色主题新的风格和复杂性,预示着对克隆和人类灭绝的恐惧,而这些正是今天社会关注的热点问题。
或许,这也是赛博朋克经久不衰的原因。赛博朋克作为一种具有思辨精神的基于美学的哲学,带有强烈的悲观主义色彩,却为浸淫在华丽的网络空间中逐渐模糊现实与虚幻的人类提供了一个自我审视的机会,以创造一个反乌托邦的未来世界的方式来警醒人们:任何一种进步都存在弊端,赛博朋克提出的问题都是人类在未来即将遇到且无法回避的。
]]>GAMES101现代图形学入门是由闫令琪老师教授。本次作业我们会通过 de Casteljau 算法来绘制由 4 个控制点表示的 Bézier 曲线。
Bézier 曲线是一种用于计算机图形学的参数曲线。在本次作业中,你需要实现 de Casteljau 算法来绘制由 4 个控制点表示的 Bézier 曲线 (当你正确实现该算法时,你可以支持绘制由更多点来控制的 Bézier 曲线)。
你需要修改的函数在提供的 main.cpp 文件中。
OpenCV::Mat
对象作为输入,没有返回值。它会使 t
在 0 到 1 的范围内进行迭代,并在每次迭代中使 t
增加一个微小值。对于每个需要计算的 t
,将调用另一个函数 recursive_bezier
,然后该函数将返回在 Bézier 曲线上 t
处的点。最后,将返回的点绘制在 OpenCV::Mat
对象上。t
作为输入, 实现 de Casteljau 算法来返回 Bézier 曲线上对应点的坐标。De Casteljau 算法说明如下:
使用 [0,1] 中的多个不同的 t 来执行上述算法,你就能得到相应的 Bézier 曲线。
在本次作业中,你会在一个新的代码框架上编写,它比以前的代码框架小很多。和之前作业相似的是,你可以选择在自己电脑的系统或者虚拟机上完成作业。 请下载项目的框架代码,并使用以下命令像以前一样构建项目:
mkdir build |
之后,你可以通过使用以下命令运行给定代码 ./BezierCurve
。运行时,程序将打开一个黑色窗口。现在,你可以点击屏幕选择点来控制 Bézier 曲线。程 序将等待你在窗口中选择 4 个控制点,然后它将根据你选择的控制点来自动绘制 Bézier 曲线。代码框架中提供的实现通过使用多项式方程来计算 Bézier 曲线并绘制为红色。两张控制点对应的 Bézier 曲线如下所示:
在确保代码框架一切正常后,就可以开始完成你自己的实现了。注释掉 main
函数中 while
循环内调用 naive_bezier
函数的行,并取消对 bezier
函数的注释。要求你的实现将 Bézier 曲线绘制为绿色。
如果要确保实现正确,请同时调用 naive_bezier
和 bezier
函数,如果实现正确,则两者均应写入大致相同的像素,因此该曲线将表现为黄色。如果是这样,你可以确保实现正确。
你也可以尝试修改代码并使用不同数量的控制点,来查看不同的 Bézier 曲线。
评分:
提交:
|
$$
b^2_0(t) = (1 - t)^2b_0 + 2t(1 - t)b_1 + t^2b_2
$$
bezier()
函数则调用 recursive_bezier()
算法并将线段颜色设置为绿:
void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) |
由于给定的框架代码有四个控制点,所以我们可以向课程中那样依次推演:
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) |
另一种递归方式则采用分而治之的策略,将问题不断分化:
cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points, float t) |
简单介绍 Vector 向量、List 链表、Queue 队列、Stack 栈、Priority_Queue 优先队列的原 理,以及 C++ STL 中这些数据结构的使用,以及笔试和面试的一些应用场景。
容器(Containers)是用于保存一系列对象的对象。
例如,std::vector
分类:
你也可以设计自己的容器,只要它满足通用的标准和接又
*注:在使用 C++98 标准编译时,需要在两个 > 中间添加空格。
为了保证通用性,标准库中还提供了一些库函数
容器的 begin() 和 end() 方法可以获得首尾迭代器
另外 cbegin() 和 cend() 方法可以返回首尾的常量迭代器(类似于常量指针)
课后:查询反向迭代器的相关资料,解释 rbegin(), rend(); crbegin(), crend() 的用法。
C++ 标准在设计的过程中,就有意地让这些标准容器共享接又,从而发挥模板多态的特性。
例如,常见的构造函数:
容量相关的方法:
Reference: Bjarne Stroustrup. The C++ Programming Language, 4th edition. §31.3
一些基本操作:
int queue[N], head, tail; 其中元素存放在 [head, tail) 区间。
基本操作
if (stack2.empty()) { |
Node* insert(Node* pos, int value) { |
Node* insert(Node *pos, int value) { |
void erase(Node *pos) { |
void erase2(Node *pos) { |
Reference: std::priority_queue - cppreference.com