首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
241 阅读
2
nvim番外之将配置的插件管理器更新为lazy
133 阅读
3
2018总结与2019规划
133 阅读
4
从零开始配置 vim(15)——状态栏配置
122 阅读
5
PDF标准详解(五)——图形状态
103 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
VimScript
读书笔记
linux
elisp
文本编辑器
Java
反汇编
OLEDB
数据库编程
投资理财
Masimaro
累计撰写
363
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
363
篇与
的结果
2026-04-05
股票投资学习记录(二)——公司的初步了解
股票的本质是上市公司的股权,买股票就是买公司,在学习价值投资的过程中一定要牢记这句话。公司的英文是 corporation,它与单词 cooperate (合作、协作),有点关系。从英文的角度上来讲,我觉得可以讲公司看作一个由拥有相同目标的人组织起来的合作组织,在这个组织中每个人都各司其职,共同完成一个宏伟的目标。公司的注册如果我们个人要创业,第一步就是注册一家公司,这就表明,你的公司将被国家保护、由政府提供信用背书、并且公司被纳入税务体系、接受国家的监督。除了上述理由,注册公司还有几个好处:首先公司正规化,在招聘人员、与其他公司进行合作由官方提供法律保证,看着比个人要靠谱一些,便于进行业务推进另外的一个好处是,公司的财产和公司法人的财产是分开的,公司如果经营不善,可以申请公司破产清算,不会影响公司法人的个人财产。注册公司之后很重要的一步就是缴纳注册资本。我们国家原来是采用的实缴制,需要在公司成立初的2年内往公司账上汇入初始注册股本资金。后面为了支持大家创业,特别是白手起家的人群,从2014年之后开始实施认缴制,可以在注册之时不再验资。但是在疫情之后发现很多认缴制注册的公司因为资本不足倒闭了,产生了很多经济纠纷,从2024年起又改为实缴制,不过这个缴纳初始资金的期限被延长到5年,一般来说如果企业经营正常,能正常盈利,这个钱5年时间是可以赚到的。注册成功之后,我们可以在国家企业信用信息公示系统中查到公司的信息,包括公司业务、法人、股权结构;不过这个网站估计被各互联网公司的爬虫爬烂了,导致它特别卡。我们可以去一些查询企业信息的APP中查找。公司的业务公司所有行为和部门岗位都是围绕着公司的业务来运行的。公司注册之初就会决定具体的业务模式。最基本的,有的公司提供产品比如:小米、百度、茅台等等。有的公司是提供服务的,例如提供会计服务、法律服务、广告服务、或者作为程序员我比较熟悉的外包公司。一般来说做产品的公司比较标准化,一次生产可以大量重复,例如手机、汽车;只会在固定周期内推出新型号,然后按照之前的设计批量生产大量同型号的产品,容易形成流水线,业务容易扩张。随着业务扩张生产成本会逐渐降低。而服务型公司比较需要更专业的人才、更个性化的服务。每多一个业务都需要专门的人处理,例如广告公司每多接一单生意,需要专门的人来对接客户,实施方案。本质上讲服务也是一种产品。不管业务是什么,公司最本质的活动就是生产产品然后把产品卖出去。公司为了卖产品会形成不同的部门,一般来讲,公司的业务线分为这么几个:产品:产品就是把产品做出来,他们需要调研用户需求、根据需求生产设计产品、并负责产品的迭代升级市场:市场一般负责推销,做品牌、定位、口碑、公关、内容、活动营销:营销与市场比较相似都是负责卖产品的,但是他们负责怎么卖、按什么价格卖;相当于他们指定销售战略方针; 特别是提供专业产品和服务的公司,由营销根据目标公司的特点执行对应的销售计划。销售:真正跑客户、签合同、催回款,可以说他们是直接与客户部门打交道交付:卖出去之后如何把产品落地、包括安装部署、培训、售后:负责后续的客服、维修、退换货、解决投诉等等整个流程可能是这样的:产品设计生产产品 --> 市场进行宣传让客户知道有这种产品 --> 营销设计如何卖这种产品 --> 销售跑客户与客户签约 --> 交付进行产品落地培训验收 --> 售后进行维修服务根据这些具体的业务线,我们可以分为不同的部门:产品部、市场部、营销部(一般的小公司可能将二者合并统称为市场营销部)、销售部、交付部门以及售后部。另外,公司产品多了,产品线复杂之后,可能需要一个独立的项目部负责管理产品的研发进度、调度研发资源、对接其他部门;随着公司业务扩张,公司相关职员也越来越多,人员的管理、招聘、晋升、去留等等需要管理,此时就可以成立一个人力资源部,也就是所谓的HR。其次人员多了,琐事也就多了,例如需要更换办公设备、调度办公室资源、安排一些福利活动等等,就需要专门的行政部门。公司业务做到了,人多了各种费用也渐渐多了起来,例如研发、推广产品需要资金、卖出的产品需要催尾款、需要支付采购的费用、需要支付员工的薪酬、需要按时缴纳相关税费、财务就需要专门的人员来处理,此时就需要一个财务部门。根据公司的类型不同,各个职能部门也存在一些差异。对产品型公司来说:产品是重中之重,决定公司的存亡;市场和营销次之,交付、售后等几乎是顺手的事情;而对于服务型公司来说:交付最强,交付了之后各个部分才开始有活干,此时才会轮到产品提供解决/设计方案、项目部跟进保证按期交付,后续才能完成一单生意并收到客户支付的费用。从投资者的角度看,公司除了可以分为服务型公司和产品型公司,还可以根据它业务所在行业进行细分,根据投资难易程度可以分为:适合普通人投资的:食品饮料、消费电子、汽车、家具家电具备相关行业的专业技能才能投资的行业: 银行、保险、医药、医疗、电力、制造业不适合普通投资者的:煤炭、石油、化工、环保、矿物其实适合普通人投资的一般是我们能接触到产品的面向大众的公司。例如我们每个人都能买到手机、可乐、汽车、空调、电视等等,我们能直观的感受到产品的差异和优势,从直觉上就能判断一家公司是否好坏。并且面向大众的都是巨头,例如美股的七姐妹(苹果的手机、微软的操作系统和office、英伟达的显卡、特斯拉的汽车、Google的搜索引擎、安卓和Youtube)。国内的茅台、腾讯等等这些都是面向大众的。我们本身就离不开他们的产品,直觉上就能知道他们是好公司。到此就对公司运营有了一个基础的认知。公司股票在我们眼中也不再是一个虚无缥缈的概念。公司的股票代表着活生生的公司,代表着千千万万为公司努力工作的各部门的员工。公司股权变化了解了公司运作相关的内容,我们还需要知道公司是如何一步一步从由原始股东独享发展到我们普通大众可以购买它的股票并享受它发展的红利。公司起点是注册时缴纳的公司注册资金。如果与朋友一起开公司,一般公司的股权比例是 7:3。这样创始人有绝对的话语权,后续融资时也方便对创始人的股权进行稀释。这个时候注册的都是有限公司,还不能称之为股票只能称为股权。公司在发展过程中上市之前一般会经历这么几轮融资:种子轮、天使轮、A轮、B轮、C轮、Pre-IPO 轮。成立公司的第一步就是设计出公司提供的产品和服务,如果在这个阶段要融资,被称为种子轮。这一轮的融资一般在50万到100万左右,可以向投资人转让10%~15%的股权。这一步的融资目标是做出产品使其具备交付给客户的水平。接着产品做出来之后,需要拿着产品去谈客户、验证产品是否能产生商业价值并且能产生盈利。这个阶段比上一个做产品的阶段要难并且需要的资金也比较大,这一阶段的融资称为天使轮,这一轮的融资需求大概在200万~500万。天使轮之后,公司已经有了几款产品,商业模式经过市场验证确实能赚到钱。此时就需要组建营销和市场团队,需要大量的对外推广。这阶段的融资被称为A轮融资,融资需求一般在 500万~1000万。这一轮的融资的目的是快速扩张抢占市场。这一轮融资之后扩张顺利就可以在本地将业务铺开或者往其他邻近城市尝试展开业务。A轮之后,如果进展顺利,公司已经初具规模,可以进行下一轮融资也可以不进行融资,靠着自身的现金流就可以活下来。但是如果想继续快速扩张市场规模,从单个城市扩张到全省、全国。首先要建立专业复杂的团队、需要根据不同需求继续打磨产品并成立专业的售后部门、需要在各地开分公司进行本地的扩张,并且全国的竞争更加激励,此时就需要进行下一轮融资,这一轮被称为B轮融资。B轮融资的规模一般在千万以上。前期进行种子轮、天使轮、A轮融资的一般是风投。从B轮开始投资的主力由风投转化为各种投资机构,此时就需要向投资人出售股权进行融资。B轮阶段公司需要从有限责任公司转化为股份有限公司。从此时开始公司的所有权由股权转化为股份。这个阶段需要聘请专业的审计机构审核公司的资产并根据资产转化为对应的股份,股份的数量一般与公司的净资产进行对应,例如公司此时的净资产是1亿,公司的股份数量就是1亿股,每股净资产价值1元。B 轮融资之后,企业继续做大做强,如果还需要大笔资金就会考虑上市了。另外公司上市的原因可能不光是需要资金有可能有下列需求:通过上市提升企业知名度提高监管、提高监管水平实现早期股东股东退出,这种一般是在公司干了很多年通过股权激励拿到原始股权的人再融资,上市之后可以直接按照二级市场定价出售股份当公司筹备上市就需要进行C轮融资,C轮融资的主要目的是提升公司业绩规模达到交易所上市要求,并且优化股权结构,融资的规模在亿以上。当执行完C轮融资之后,企业一般能具备上市的条件了,但是有些企业为了稳妥会再进行一轮Pre-IPO融资。这一轮主要是继续优化股权结构、引入明星投资机构拉高估值。融资规模一般也在亿以上。这些准备妥当之后就可以准备上市了,上市本质上也是进行一轮融资,只不过此时融资不再局限于机构投资,普通的投资者也可以出资购买股份。上市的流程如下:聘请专业的中介结构帮忙,聘请券商帮忙准备材料、聘请律师事务所和会计机构进行财务和法律事物的审查在中介的帮助下准备上市材料并对企业高管进行上市培训、制作申报材料(招股说明书)提交IPO申请证监会审核和注册发行与上市:接受普通投资者的报价,提出最高的10%的报价,并进行加权平均计算形成一个发行价之后投资者就可以进行打新股,在发行时按发行价购买并且在上市挂牌之后就可以开始正常在二级市场进行交易。公司的融资到上市就是一个不断通过出售股权换取公司发展资金的过程。假设公司初始有3个合伙人,由创始人占比70%、两个合伙人分别占20%和10%。在经历天使轮融资时,由创始人出售10%的股权换200万的资金,此时公司的估值就是 200 / 10% = 2000(万)。此时创始人手上的股权变为60%、两个合伙人占比20%、10%、天使轮的股东占比10%。在进行A轮融资时,就不再是创始人单独出让股权而是所有股东等比例稀释,A轮一般也是出售10%的股权,按照比例创始人出10%的6成,由60%变为54%,两个合伙人变为 18%、9%、天使轮老股东变为9%、A轮新股东占比10%。在进行B轮融资前,需要进行股份制改革。假设经过会计事务所的审计,确定公司净资产为1亿,那么此时创始人拥有54%=5400万股;两个合伙人分别拥有 18%= 1800万股、9%=900万股、天使轮老股东拥有 9%= 900万股、A轮股东拥有10%=1000万股。在B轮融资时假设也是出让10%的股权,融资2亿,公司此时的估值就是20亿。团队的股东也是等比例稀释,这一轮融资之后,创始人剩余股份为 创始人拥有 48.6% = 4860万股,合伙人分别占比 16.2% = 1620万股、8.1% = 810万、天使轮老股东占比 8.1% = 810万、A轮股东占比 9% = 900万股、B轮股东占比10% = 1000万股,此时每股价值为公司估值20亿除以总股本1亿股等于20元。为了激励核心员工,创始人从他拥有的股份中拿出60万股作为股权激励。此时创始人股份变为4800万股。此时就到了C轮融资,依旧稀释10%,并且仍然按照等比例进行稀释,假设此时融资金额为5亿,公司的估值为50亿,每股的价值上升到50元。创始人拥有 4800万 * 0.9 = 4320万股,两个合伙人分别拥有 1458万股和 729万股,天使轮的老股东拥有729万股,A轮股东拥有 810万股,B轮股东拥有 900万股,C轮股东拥有 1000万股,团队核心成员拥有 54万股。此时创始人再次拿出20万股作为股权激励,创始人就剩下4300万股,团队的核心成员拥有 74万股假设上市时,公司利润为5亿,按20倍PE计算,此时公司的估值大概为100亿,公司股本为1亿股,每股价值为100元。对于一个准备上市的公司来说,当前的股票价值还是太高了,不利于股票流动。一般市场喜欢价格在20元以下的股票。为了降低每股股价,可以进行扩股。扩股的算法就是将原有的股本数量乘以10,团队每个人的股数增加10倍,每股价值变为原来的十分之一,有100元降低为10元。在IPO阶段,交易所对上市公司有最低公开发行比例要求,要求最低公开发行量不低于总股本的25%,公司打算在原有10亿股份数量的基础上增加5亿股,此时股本数为 15亿股,按照之前计算的100亿估值,每股的发行价大概为 6.67元左右。此时公司中各个成员股份为,创始人拥有股份数为 43000万股,占比 43000/ 150000 = 28.66%,两个创始人占比分别为:14580 / 150000 = 9.72% 和 7290 / 150000 ≈ 4.86%,天使轮的老股东占比为: 7290 / 150000 = 4.86%,A轮投资人占比:8100 / 150000 = 5.4%, B轮占比 = 9000 / 13000 = 6%,C轮占比 = 10000 / 150000 = 6.67%,核心员工占比 740 / 150000 = 0.49%,新发行股票占总股本的 33%。此时创始人团队的总股本占比为 43.24% 这个占比在公司上市之后不超过50%,是非常危险的。在上市之后,挂牌交易之前的打新阶段,这多出来的5亿股其中70%被机构申购了。而交易所为了防止上市挂牌交易之后大股东套现,规定在公司上市之后一段时间内原始股东才能进行坚持,也就是说15亿股真正能给散户交易的只剩 1.5 亿股,只占公司总股份的10%,在大家都看好公司的前提下,购买需求旺盛但是初始阶段流通的股本少,因此在正式可以自由交易的初期股价容易暴涨。因此打新股是A股市场稍有的留给投资者的一个小福利。公司上市之后的股本变化公司上市之后股本还有可能出现变化,一般是定向增发、股东配售、可转债、以及公司自己回购注销。定向增发是与之前几轮融资类似,公司有新的发展机会需要大量融资,此时可以增发股票,但是只能向机构增发并且增发时每股价值不低于基准价的80%。作为持有公司股票的小散来说具体是利好还是利空,暂时无法确定,如果融资之后公司业绩大增,每股净资产升高可能是利好,要是投资失败不仅面临股份稀释还面临每股净资产下降。股东配售与定向增发类似,也是会稀释股权的。唯一区别在于,这次是面向普通投资者的,也就是需要我们这些小散户掏钱购买这些增发的股票。可转债顾名思义就是企业发行的一种债券,债券到期后持有者可以兑换成股票。可转债是需要申购的,一般100元一张,每个投资者可以买10张。后续按照一个约定价格兑换成公司股票,因为一般发行可转债的企业不太想还钱所以会定一个比较低的价格引导投资者将手上的债券换成股票。也会增加股本,持有股份被稀释公司可以在二级市场上回购自己的股票,很多散户一看到公司回购就觉得是利好,疯狂涌入。具体分析的话,我们要看公司回购股票的用途,如果用于注销,公司总股本会降低,每股的价值会提升,我们手上的股票就会更值钱。如果是为了给高管发股权激励,这就是用公司的利润,也就是股东的钱给高管发工资。如果是在股价底部回购,会短暂的引起二级市场上股价的上升,如果是在股价明显高估的情况下回购,这就是配合公司高管和大股东出货,并且最后还用低于市场的价格奖励给公司的高管,这是最恶劣的行为。这种与股东利益不一致的管理者所在的公司普通投资者还是远离他们比较好。公司还可以进行高送转,也即是对持有公司股份的股东赠送股票。这种情况下股东持有的股份数量增加,但是相对价值占比不变。企业进行赠股行为主要是为了提升二级市场上股票的数量,降低每股的价格,减少购买门槛、增加流动性。
2026年04月05日
12 阅读
0 评论
0 点赞
2026-04-04
沪宁高速(600377)初步分析
最近我想学习股票投资的基本理论,其中有一项就是分析公司。根据豆包的建议我可以每周初步分析一家公司,决定后续是否继续对其进行跟踪和深入研究。作为新手,为了稳妥我当前主要精力用于关注财务稳健能持续高分红的公司。 根据我当前关注的重点,我使用豆包辅助生成了相关分析内容如下:基本信息公司名称: 江苏沪宁高速公路股份有限公司所处行业:铁路公路——高速公路,属于公共事业,弱周期核心业务:江苏省境内高速公路的建设、维护、运营、以及配套设施的经营特点:背靠公路,建设都是一次性的,但是可以持续在上面经营收费商业模式靠什么赚钱: 收高速费、少量靠路上一些配套设施的收费:例如服务区售卖商品,提供饮食、住宿护城河是什么:政府特许经营权+区域垄断,沪宁高速是长三角核心通道,重新建一条成本极高、审批极难,属于“不可替代”的护城河未来5到10年会消失吗:不会,未来我国公路运输仍然会占有较大比重;并且江苏、上海都是相对发达城市,人口比较集中,车流量可观分红历史连续分红多少年不间断连续25年分红不间断近5年分红是否稳定近五年股息基本维持在一个稳定水平,没有大涨也没有大跌最近12个月股息率大概是多少: 4.2% 左右分红率(分红/净利润) : 53% 左右盈利稳定性5 年净利润: 近5年基本稳定在45亿左右5-10年营业额: 最近10年,营业额稳步向上,或者略有下降特点: 盈利稳定,小幅波动,不暴涨暴跌现金流经营活动现金流是否稳健经营活动的现金流稳定在0亿左右,相当稳健,且持续大于净利润财务安全资产负债率:40%, 处于合理期间有息负债高吗: 不高,不会出现变卖资产还债的情况短期应付刚性债务大概为:41.46亿,虽然大于账上的货币资金和交易型金融的13.87亿,但是每年的经营现金流有60亿,可以覆盖这个债务是否有大风险: 财务稳健,不会有大的风险。背靠国家,背靠繁忙的高速;赚钱能力ROE:11.48%,赚钱能力稳定估值与安全边界股息率: 历史分位约 35%, 处于合理区间,也符合我个人的大于4%的买入标准PB: 10 年历史分位约 45%,处于合理区间PE:10 年历史分位约 40%,处于合理偏低区间核心风险收入将近80%来自于高速收费,政策如果降低高速费,将会对收入产生影响经济下行,车辆可能会减少,高铁飞机越来越便利,会抢夺一些公路运输的生意结论商业模式:商业模式简单,背靠公路吃公路财务状况:财务状况稳健分红:分红当前处于合理,满足我自己定义的大于4%估值:估值处于合理状态可以放入待观察的股票池中或者小额买入
2026年04月04日
8 阅读
0 评论
0 点赞
2026-03-29
股票投资学习笔记(一)——前言
我最早萌生投资的想法,源于《软技能:代码之外的生存指南》一书。书中着重提醒程序员,不应只埋头于代码世界,更要关注代码之外的生活本质,其中特别提到,程序员应当主动学习一些投资相关知识,为自己的生活多一份保障。作为一名程序员,我的收入相较于其他不少职业略高一些,工作多年下来,手上也渐渐积累了一笔闲钱。而如今银行存款利率仅在1%左右,单纯将钱存入银行,早已无法实现资产的保值,继续这样下去显然不合时宜。再加上近期张雪峰老师心源性猝死的事件,让我深刻意识到,人生无常,与其一味奔波忙碌,不如尽早为健康和未来打算,思考如何实现提前退休,从容享受往后的人生。FIRE运动中有一个核心观点:只要资产总额的4%能够覆盖日常开支,就意味着实现了财务自由。在阅读了一些理财书籍后,我对“下金蛋的鹅”这个概念深深着迷——这正是我一直追寻的状态:即便不工作,也能有持续的收入来源。我由此萌生了一个想法:如果能投资股息率稳定在4%的股票,长期持有并获取股息,只要股息收入能够覆盖我的日常生活开支,那不就实现财务自由了吗?但这个想法背后,还藏着两个亟待解决的问题:如何筛选出股息率稳定在4%的股票?买入并长期持有后,股息能否保持稳定,甚至逐年上涨?为了找到这些问题的答案,我决定正式开启自己的股票投资学习之路。我学习新鲜事物时,习惯遵循“是什么、为什么、怎么做”的逻辑框架,放到股票投资上,就是要弄清楚:股票是什么、为什么要投资股票、以及如何投资股票。其中,前两个问题相对容易解答,通过维基百科、AI工具就能获取基础答案。接下来,我结合网络上的公开信息和自己的思考,试着把这两个问题讲清楚。为什么要投资股票我想从自己小时候的经历说起。我出生于上世纪90年代初,那时候物质条件相对匮乏,我一周的零花钱只有5毛钱,每天的早饭钱也不过1块钱——当时一碗普通的面条只要1块,小孩子吃的小份半碗面,5毛钱就能买到。每年过年,我收到的压岁钱加起来也不到200块,父母总会把这些钱收起来,告诉我要存着给我上大学用。可等我真正走进大学校园,再翻开当初的存折时,发现连本带利也只有4000多块,刚好够一年的学费。也就是说,我辛辛苦苦存了十几年的“大学储备金”,到最后连一个学期的学费都不够。后来我才了解到“通货膨胀”这个概念,原来钱是会贬值的,把钱单纯存在银行,它的实际价值只会慢慢缩水,最终被通胀悄悄吞噬。另一个让我深受触动的,是短视频里看到的一个故事:两位博主采访一位卖鸭腿饭的大叔,问他为什么鸭腿饭卖得这么便宜。大叔笑着回答,他有两栋房子用来收租,卖鸭腿饭只是自己的业余爱好,不为赚钱,只为图个自在。这个故事让我明白,人只有拥有稳定的被动现金流,不用为了生存而被迫上班,才能真正追随自己的内心,去做喜欢的事,实现自我价值的提升。这两个故事,其实已经给出了“为什么要学习投资”的答案:钱存在银行会不断贬值,不进行合理投资,自己辛辛苦苦赚来的血汗钱,最终会被通货膨胀一点点侵蚀;投资能为我们创造稳定的被动现金流,而只有拥有能覆盖日常生活的被动现金流,才能真正摆脱生存的束缚,实现人生自由。市面上的投资品种有很多,为什么我偏偏选择股票?核心原因很简单:我个人的投资目标不是大富大贵,而是希望能长期获取公司分红,一辈子有稳定的被动收入。从这一点来看,股票对我而言,无疑是最契合需求的资产。当然,我也清楚股票投资的风险:股票价格有涨有跌,很可能出现“你想拿它的分红,它却想吞你的本金”的情况;而且,我们无法预知一家公司未来是否能持续经营,分红是否能保持稳定甚至上涨。而我相信,通过系统学习股票投资的相关知识,就能学会识别这类优质公司,规避潜在风险——这正是我坚持学习投资的意义所在。股票是什么了解了投资股票的意义,接下来我们就来弄清楚:股票到底是什么。股票的历史,可以追溯到17世纪的荷兰。当时正处于地理大发现时期,西欧的船只可以绕过好望角,前往亚洲开展贸易,从东南亚带回香料、茶叶等商品,通过贸易差价赚取丰厚利润。但在那个年代,航海技术并不发达,船只在航行中遭遇大风浪,导致船只损坏、货物丢失的情况屡见不鲜。尽管如此,大家都清楚,长期、多次利用大船开展远洋贸易,必然是有利可图的。可问题在于,当时很少有人能拿出足够的钱,独自购买大量船只、支撑起整个贸易项目。于是,一种全新的模式应运而生:大家共同出资,成立贸易公司,由公司统一购买船只、开展贸易,而出资的人,就成为了公司最早的股东。不过,远洋贸易的周期很长,往往一次航行就要耗费数月甚至数年,期间总有一些股东因为急需用钱,想要将手中的股份出售;同时,也有不少人看到贸易的利润,想要加入其中、购买股份。就这样,在阿姆斯特丹的一座桥上,形成了最早的股票交易场所,普通人也能参与其中,买卖股份。这段历史,恰恰道出了股票投资的本质:股票是公司所有权的凭证,我们投资股票,本质上就是投资股票所代表的那家公司,成为公司的股东,与公司共同承担风险、分享收益。这些认知,将构成我后续股票投资的核心理念。至于“如何投资股票”,我深知这不是一蹴而就的事情,而是一场需要耐心和坚持的长期学习之旅,后续我也会慢慢梳理、逐步分享自己的学习心得。
2026年03月29日
19 阅读
0 评论
0 点赞
2026-03-28
std::move 并没有移动任何事物:深入探讨值类型
本文翻译自 std::move doesn't move anything: A deep dive into Value Categories问题:当“优化”让程序变慢时让我们从一个让经验丰富的开发者都感到困惑的问题开始。你写了一段看似完全合理的C++ 代码struct HeavyObject { std::string data; HeavyObject(HeavyObject&& other) : data(std::move(other.data)) {} HeavyObject(const HeavyObject& other) : data(other.data) {} HeavyObject(const char* s) : data(s) {} }; std::vector<HeavyObject> createData() { std::vector<HeavyObject> data; // ... populate data ... return data; } void processData() { auto result = createData(); }这段代码可以工作。它可以通过编译,也可以正常运行。但是根据上述实现的代码,它可能会执行成千上万次耗时的拷贝操作而不是你实现的轻便的移动操作让我们来看看具体发生了什么:当你的 std::vector 当前容量无法容纳新增元素时会重新分配一块大的内存。然后将旧的元素移动到新的内存中。但是这里有一个关键点,如果你的移动构造函数没有使用 noexcept 关键字标记,编译器就不会调用移动构造,反而会退回到采用普通构造函数拷贝每一个元素为什么呢?因为 std::vector 需要维持所谓的 "强异常保证"。这是一种比较文雅的说法,它指的是:如果在重分配的过程中发生错误,原始的vector数据完全不受影响。如果在调用拷贝构造,在拷贝到新内存时发生错误,原始的vector仍然是完整的,如果在调用移动构造过程中发生错误,某些元素因为之前调用移动构造导致了原始的vector数据不完整。所以标准库采取了保守的策略:如果你的移动构造函数可能抛出异常(因为你没有将移动构造标记为 noexcept), 容器就会使用拷贝代替移动。你认为的“优化”并没有发生。而这里就变得有趣了:std::move 并不会神奇的解决这个问题,甚至如果使用不当,它会使事情变的更糟糕。让我向你展示为什么会这样。机制:什么是真正的 std::move一个可能会让你感到惊讶的事实:std::move 并不会移动任何事物。当你调用 std::move 时,不会有任何字节的内容发生移动。它是C++ 标准库中最具有误导性命名的函数之一。那它实际上做了什么呢?让我们看看标准库中一种真正的实现(这份实现来自 libstdc++,但是其他的标准库实现也差不多):template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }如果你看到这个,并想到“这就是一个强制转换!”。你这么想是对的,这就是它的全部。std::move 接收你传递给它的任何参数,去掉所有的引用限定符(通过std::remove_reference)然后添加一个 &&让它变成右值引用,然后对该类型执行 static_cast,这就是整个函数。让我用更简单的话来说:std::move 就像在你的对象上贴了一个标签,“我处理完了,你可以拿走它的东西了”。真正的拿走动作发生在之后,由看到这个标签的其他代码执行。具体来说,这个标签(右值引用类型)告诉编译器选择调用移动构造而不是拷贝构造。理解值类别:一切的基础现在,为了真正理解为什么 std::move 是这样的行为,我们需要深入了解C++ 的值类别的概念。这不仅可以帮助我们理解 std::move ,还是理解一般移动语义的方法。在现代C++种,每一个表达式都有一个值类型,它决定了表达式可以被如何使用。可以将这些类别理解为回答关于某个表达式的两个问题:它有身份吗(我能取它的地址吗)?我能从它那里移动过吗(我能从它内部窃取资源吗)基于这些问题,C++ 将表达式分为几个类型,我们将重点关注其中的三类:接下来让我来详细拆解这些概念:lvalue(左值 left value):这是我们最熟悉的一类。左值是指任何具有名称(标识符)且在内存中占据特定位置的对象,可以通过 & 获取它的内存地址,当你写下如下代码:int x = 5; std::string name = "Alice";x 和 name 都是左值,它们有名称,也有地址。它们的生命周期超过了当前的表达式,你可以将它们理解为“拥有固定地址的对象”。prvalue(纯右值 pure right value): 这一类代表着那些不具有持久身份标识的临时值。它们通常在表达式求值期间被创建,并且会被立即使用。例如:42; 5 + 3; std::string("hello"); Add(x, y) ;// 译者添加:函数的返回值也是典型的纯右值这些值无法通过名称或者内存地址获取,它们仅仅存在于创造它们的表达式求值期间。它们就像"转瞬即逝的过客"。xvalue(将亡值 expiring value): 它是比较特殊的一类,它是由 std::move 创建。xvalue 仍然有一个标识(可以引用它,它自身也有一个名称),但是我们将它视为即将销毁的对象。这就好比在说“这个对象名义上还存在,但是我已经不需要它了。所以请把它作为临时值处理”当你写下 std::move(name) 时,并没有移动 name 。我们将 name 变量从 左值转化为了一个将亡值,你只是改变了编译器对 name 变量的解释,真实的 name 变量依然安然无恙,并没有发生移动或者销毁。真正发生的事情是,编译器现在将该表达式视为“该对象即将消亡,你可以随意窃取其内部资源”。这就是为什么我说 std::move 仅仅是一个类型转化。它改变的是表达式的值类型,而不是对象本身。它通过从一种值类型转化为另一种值类型(既转换为将亡值)来实现这一点。资源的实际移动发生在后续阶段,即当该将亡值调用移动构造或者移动赋值运算符时。你可以想象成这样,std::move 只是在对象上挂了一块 "免费清仓,随便拿" 的牌子。 资源的实际转出发生在那些具备移动能力的函数读取该标签并据此执行移动操作的时候。直觉方案的陷阱:三个降低性能的常见错误现在我们明白了 std::move 真正做了什么,接下来我们来讨论人们如何因为误用导致性能不升反降。错误1:误用 std::move(local_var) 导致编译器无法采用优化这可能是 std::move 最常见的误用情况std::string createString() { std::string result = "expensive data"; // 针对这个对象做一些事情 return std::move(result); // 千万记住不要这么做! }你可能会这么想:“嘿,因为要返回一个本地局部变量,所以我应该使用 std::move 来避免拷贝!” 但是实际上,这么做会让事情变得更糟糕。让我们来看看为什么。现代C++ 编译器有一种称之为 NRVO(Name Return Value Optimization) 的优化。它是这么工作的:当你返回一个局部变量,编译器被允许直接在接受返回值的内存位置直接构造对象。这就意味着这里根本不需要拷贝或者移动。这个对象被就地构造,换句话说,编译器将不会做这些事:在函数栈帧种创建一个 result 对象将 result 对象移动到用户指定的位置在函数栈帧种销毁 result 对象取而代之的是,编译器只会做这件事:直接在用户指定位置构建 result 对象这不仅省去了移动操作,还免除了局部对象的析构。它比移动更优雅,因为这是从“一次操作”变为“0次操作”。但是这里有一个前提,NRVO 有其适用规则。为了让这种优化能够正常运行,需要通过名称返回对象。当你写 "std::move(result);" 时,不再是通过名称返回 result 对象,实际上你返回的是一个通过 std::move 转化的将亡值。所以编译器认为,“我无法执行 NRVO 优化操作, 因为它并没有仅仅通过名称进行值返回”。现在你只能被动执行一次移动操作,你将一次0开销的操作“优化”成了1次移动操作,这被称作反向优化。“请先等等”,你可能会说,“移动操作不是很快的嘛”。是的,移动操作比拷贝操作更快。但是0操作比1次操作更快。对于那些移动操作并不简单的类型(例如具备短字符串优化,或者其他复杂逻辑的字符),你可能反而强迫编译器执行了额外的操作,而这些开销本可以完全避免的。为了修复这个问题,你需要通过名称返回对象std::string createString() { std::string result = "expensive data"; // 针对这个对象做一些事情 return result; // 正确,这里将采用 NRVO,如果调用 std::move 则无法执行优化 }编译器将会尽一切可能执行 NRVO 操作,如果它不能执行(可能是因为复杂的控制流程),它将会把返回值转化为右值,然后执行移动操作,你不需要做任何多余的事情。规则:切记不要在 return 语句中对局部变量使用 std::move ,编译器比你想得更聪明。错误2: const T obj; std::move(obj) 暗中的拷贝这拷贝更为隐蔽,而且同样会损害性能void process() { const std::vector<int> data = getData(); consume(std::move(data)); // 注意:这里实际发生的是拷贝操作,而非移动操作 }让我们来看看为什么这是一个错误。当你使用了 const ,就相当于告诉了编译器"该对象处于不可变状态" 。但是移动的核心操作就是改变对象状态,即从一个对象中夺取资源并将其转移给另一个对象。执行移动操作后的对象状态必然会发生变化(通常来说,它会变为空或者 null)所以如果尝试对const 对象执行移动操作时会发生什么呢?让我们看看编译器是如何思考的:std::move(data) 返回一个 const std::vector<int>&& 类型(const 类型的右值引用)移动构造的函数原型为 vector(vector&&) (传入的是非 const 的右值引用)const T&& 类型无法转化为 T&& (无法通过普通的类型转化消除const)但是 const T&& 可以转化为 const T& (拷贝构造函数参数类型)所以编译器会调用拷贝构造取而代之你写的代码看上去会执行移动操作,但是编译器会默默回退到使用拷贝操作,因为它不能合法的从 const 对象中移动资源。你希望避免的那些耗时的拷贝操作仍然会发生。这是最危险的bug之一,这里没有警告,也没有错误。你的代码可以正常的编译运行,但是它并没有按照你的预期执行。规则:永远不要在 const 对象上使用 std::move , 如果某个变量是const 类型,你不应该从它那里移动资源,移动意味着修改源对象,而 const 意味着不能修改错误3:使用移动后的对象,这就像在玩火这是第三种最常见的错误:std::string name = "Alice"; std::string movedName = std::move(name); std::cout << name << std::endl; // 这里会发生什么?C++ 标准规定,从标准库对象中移动后,该对象处于"有效但是未定义的状态",让我来解释这个隐晦短语的意思:所谓“有效”,是指该对象依然满足其类不变式(译者注:Class Invariants,即对象内部的数据结构始终保持合法且可读的状态)。对于 std::string 而言,内部指针没有悬空其大小与其容量一致你可以安全的调用它的析构函数你可以调用没有前置条件的函数未定义意味着,你不知道它的值是什么。也许 name 的值是空的,也许它仍然包含着字符串 Alice。也许它包含着其他完全不同的值,标准中没有描述,每个编译器的实现各有不同。以下是你可以安全的对已移动的对象执行的操作:销毁它(它会正确的执行销毁操作)赋值给它 (name = "Bob")调用没有先决条件的函数 (name.empty(), name.clear())以下是你不该做的事:读取它的值 (std::cout << name)调用带有先决条件的方法 (name[0] 或者 name.back() 假设字符串不为空)对其状态做出任何假设事实上,大部分标准库在实现时确实会将已移动对象置为可预测状态(std::string 通常为空, std::vector 通常为空),但是这并非要求,依赖于此的代码是不可移植的,同时也是未定义行为。我使用的思维模型:将一个被移动的对象视为被销毁的对象,技术上它仍然存在(所以你可以对其进行赋值),但是它的值已经被销毁。这就像一个抹除心智的人,身体还在,但是构建“他”的一切都消失了。规则:在对象上调用 std::move 后,除了给它赋予新值和销毁它之外,不要再使用该对象。将其视为已销毁。正确实现移动操作既然我们已经讨论了不该做什么,现在让我们谈谈如何正确的实现它。如果你正在实现一个资源管理器(内存、文件、句柄、网络联结)的类,你需要实现移动语义。并且有一个成熟的模型可以正确的实现这一点。五法则在现代C++ 中,如果你需要实现其中一个,那么你通常需要实现全部5个:析构函数拷贝构造函数拷贝赋值运算符重载移动构造函数移动赋值运算符重载这被称之为 "无法则" (在移动语义出现以前,它通常被称之为"三法则")。让我给你展示一个完整、正确的实现,然后我们会逐个分析每个部分class Resource { private: int* data; size_t size; public: // 构造函数 Resource(size_t n) : data(new int[n]), size(n) { std::cout << "Constructing Resource with " << n << " elements\n"; } // 析构函数 ~Resource() { std::cout << "Destroying Resource\n"; delete[] data; } // 拷贝构造,进行深拷贝 Resource(const Resource& other) : data(new int[other.size]), size(other.size) { std::cout << "Copy constructing Resource\n"; std::copy(other.data, other.data + size, data); } // 拷贝赋值运算符重载,进行深拷贝 Resource& operator=(const Resource& other) { std::cout << "Copy assigning Resource\n"; if (this != &other) { // 防止自赋值 // 先分配一段新的内存. int* new_data = new int[other.size]; std::copy(other.data, other.data + other.size, new_data); delete[] data; // 更新状态 data = new_data; size = other.size; } return *this; } // 移动构造,转移所有权 Resource(Resource&& other) noexcept : data(std::exchange(other.data, nullptr)), size(std::exchange(other.size, 0)) { std::cout << "Move constructing Resource\n"; } // 移动赋值运算符重载: 转移所有权 Resource& operator=(Resource&& other) noexcept { std::cout << "Move assigning Resource\n"; if (this != &other) { // 防止自赋值 delete[] data; data = std::exchange(other.data, nullptr); size = std::exchange(other.size, 0); } return *this; } };让我逐一讲解这些内容,因为每一个都有其特定的用途:构造和析构函数很简单:构造函数分配资源,析构函数释放资源。这是基本的RAII(Resource Acquisition Is Initialization)。资源的生命周期与对象的生命周期绑定。拷贝构造和拷贝赋值运算符完全符合你的预期,它们完全创造了一个完全独立的资源副本,如果某个资源对象拥有一块内存,复制它将创建一个新的资源对象,该对象拥有一个内容相同但完全不同的内存块。复制后两个对象互不影响。移动构造和移动赋值操作是关心的重点。它们不创造新的资源,而是窃取原始资源,让我们聚焦于移动构造函数Resource(Resource&& other) noexcept : data(std::exchange(other.data, nullptr)), size(std::exchange(other.size, 0)) { std::cout << "Move constructing Resource\n"; }理解 std::exchange: 干净的移动方式注意,我们这里使用了 std::exchange ,它是 <utility> 中的一个工具函数,它在一次操作中完成两件事:它返回第一个参数的当前值它将第一个参数值设置为第二个的值所以 std::exchange(other.data, nullptr) 表示:获取 other.data (资源指针) 的当前值将 other.data 设置为 nullptr ,表示 other 不再拥有该资源返回原始指针这对于实现移动操作非常完美,这正是我们进行移动操作时所必要的操作:从 other 处接管资源将 other 置于一个有效的空状态(这样它的析构函数就不会释放我们刚刚接管的资源)我们可以不使用 std::exchange 来实现这个操作:Resource(Resource&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; other.size = 0; }这是 std::exchange 更简洁,更能说明当前发生的事,这是现代 C++ 中实现移动的惯用法。移动操作完成后,内存本身没有变化,只是指针交换了位置,对象B获得了内存的所有权,而对象A则处于安全的空状态。noexcept 至关重要现在我们来谈谈为什么两个移动操作都被标记为 noexcept 。这个关键字不是可选的,它对性能至关重要。还记得之前提到过的 std::vector 在重新分配时不会使用你提供的移动构造函数,除非它是 noexcept。原因如下:当 std::vector 增长时它需要维持强异常保证,这意味着在分配过程中出现异常,原始向量必须保持完全不变,让我们来分析这一个场景:情景1:使用拷贝构造函数创建新的内存块将元素1拷贝到新内存块中 (可能抛出异常)如果它抛出异常,删除新创建的内存,原始vector 不受影响拷贝元素2到新内存中 (可能抛出异常)如果它抛出异常,在新内存块中删除之前拷贝的元素1,删除之前创建的新内存,原始的vector不受影响继续下一步无论合适抛出异常,我们都可以清理新创建的内存,而原始的vector保持原样情景2:使用移动构造时可能抛出异常创建新的内存块将元素1移动到新内存块中 (可能抛出异常)如果它抛出异常,元素1现在处于未定义状态将元素2移动到新内存块中 (可能抛出异常)如果此时抛出异常,元素1已经被移动了(也可能损坏),元素2现在也被损坏我们无法恢复到原来的状态如果移动操作可能抛出异常,那么如果在重新分配过程中发生异常,我们无法保证原始vector仍然有效。所以 std::vector 做了一个取舍,如果你的移动构造函数可以抛出异常 (没有打上 noexcept 标记) , 它使用拷贝构造来代替移动操作,保证发生异常时原始数据有效实际影响:如果你忘记了在移动构造函数后面加上 noexcept , 每次你的 vector 增长时它都采用赋值来代替移动操作,对于一个包含复杂对象的 vector 这可能意味着数百万次不必要的的内存分配。规则:始终,始终,始终标记移动构造函数和移动赋值运算符 noexcept ,除非你有不标记的例外理由(而实际上你几乎从不这样做)std::move 与 std::forward,两种用于不同任务的工具现在你已经理解了 std::move ,现在让我们认识一下它的表兄弟 std::forward ,它们都是处理值类型转换的函数,但是它们服务于不同的场景std::move 是无条件的,它总是将其参数转化为右值引用,无论你传入什么内容template<typename T> void process(T&& arg) { // arg 是一个左值 (它在函数里面有名称!) consume(std::move(arg)); // 总是将值转化为一个右值引用, 用来消耗 }尽管 arg 包含了一个 && , 但是在函数中,它有了名称,它就是一个左值。这是规则:如果有名称它就是左值。所以在 process 函数内部,它是一个左值,然后我们通过 std::move 将其转化为一个右值以供消耗。std::forward 是条件性的。它保留其参数的值类别。这在模板中用于完美转发:template<typename T> void wrapper(T&& arg) { // 如果 arg 原来是一个左值,将它转化为左值 // 如果 arg 原来是一个右值, 将它转化为右值 process(std::forward<T>(arg)); }关键区别在与 std::forward 记住 arg 被传递给 wrapper 时它是一个什么值,并保持这种状态。如果你用左值 x 调用 wrapper(x), std::forward<T>(arg) 会产生左值,如果你用右值调用 wrapper(std::move(x)),std::forward<T>(arg) 会产生右值以下是何时使用何种情况:当你确定需要从某物中移动,并且确定后续不再用它时使用 std::move仅在模板函数中使用转发引用 (T&& , 其中 T 是模板参数),当你想传递参数时,同时保留它们是左值还是右值在 99% 的普通代码中,你会使用 std::move 。std::forward 主要用于库作者和框架代码,这些代码视图完美封装其他函数。现代C++ 环境,移动语义的演变移动语义在C++ 11中引入,但在后续标准中仍在不断发展。让我们看看影响现代C++编写方式的关键发展C++14 constexpr 移动在C++11中 std::move 仅是运行时动作。 C++ 14 通过将 std::move 标记为 constexpr 改变了这一点这看似是个小细节,毕竟 std::move 只是一个转换。但它是实现复杂的、重度依赖移动语义的逻辑(如排序或交换)完全在编译时运行的关键第一步。C++17 强制拷贝消除在C++17 之前,消除拷贝(包括返回值优化 ROV 和命名返回值优化 NROV)是一种允许的优化,但不是必须的。编译器可以执行它,也可以不执行。这在C++17中发生了变化。当你返回一个纯右值时(一个纯粹的临时值),编译器必须直接在调用者的位置构造该对象std::string create() { return std::string("hello"); // C++17 以后保证不会移动或者复制 }对象直接在调用者的内存中构造,始终如此,这不是一个可能发生的优化,而是标准要求必须这样做。这与 NRVO (命名返回值优化)不同,后者仍是可选的。当你返回一个有名称的局部变量时:std::string create() { std::string result = "hello"; return result; // NRVO 仍然是可选的,但是编译器通常会执行优化 }编译器允许直接在调用者的内存中构建 result 但是并非必须这样做。实际上现代编译器可以可靠的执行这种优化,但是标准并没有从技术上保证这一点。要点:在C++17 以及更高的版本中,返回临时对象是保证高效的,不要再尝试使用 std::move 来优化。C++ 20 编译时移动堆内存事情开始变得复杂了,虽然自C++14 起,std::move 就是 constexpr 但由于无法在编译时分配内存,你实际上无法用它来操作标准容器。C++ 20 引入了 constexpr 动态内存分配,这意味着 std::vector 和 std::string 现在可以在 constexpr 中使用并且可以被移动constexpr int sum_data() { std::vector<int> data = {1, 2, 3}; std::vector<int> moved_data = std::move(data); // C++ 20 中合法 int sum = 0; for(int i : moved_data) sum += i; return sum; } // 这个 vector 的创建、移动、以及销毁都发生在编译期 constexpr int result = sum_data();这使得更复杂的编译期编程能够成为可能。你现在可以编写移动对象的 constexpr 函数,并且整个计算都是在编译期进行。C++23 Move-Only 函数包装器C++ 23 中引入 std::move_only_function 它类似于 std::function ,但可以持有不可拷贝的类型// 在 C++23 之前, 这段代码无法工作,因为std::function 要求必须可拷贝: // std::function<void()> func = [ptr = std::make_unique<int>(42)]() { // std::cout << *ptr; // }; // Error: unique_ptr is not copyable! // C++23 的解决方案: std::move_only_function<void()> func = [ptr = std::make_unique<int>(42)]() { std::cout << *ptr; }; // 可以工作 func 可以被移动而不需要可以拷贝这对于需要拥有独占资源的回调和处理器特别有用未来,平凡迁移(仍在开发中)围绕平凡迁移正在进行一项标准化工作,尽管它尚未标准化,但是仍然值得了解。它的基本思想是:对于许多类型,移动一个对象等同于仅仅复制其字节并忘记原始对象(译者注:其实就是支持使用memcpy 直接进行内存拷贝)考虑当 std::vector<std::string> 需要增长时,当前是怎么做的:对于每个字符串调用移动构造函数(复制指针,将旧指针制空)对于旧内存中的每个字符串调用其析构函数(检查指针是否为空,不执行实际操作)那有很多函数调用,但从概念上讲,我们只需要:memcpy 整个字符串块移动到新内存中忘掉旧内存(不需要析构函数,它们都是空的)这被称为“平凡重定位”,类型可以通过简单的字节复制进行重定位有两个提案正在竞争这个特性:P1144 : (由 Arhtur O'Dwyer 撰写), 与 Qt、Folly、BDE 等库的实现方式一致P2786 : (由 Giuseppe D’Angelo 等人提出),在 Hagenberg 2025 会议中被合并到工作草案中,但仍然存在争议。争议源于语义和接口的差异,尽管实现者对 P2786 的语义与现有实践不符表示担忧,但是它仍被合并,许多主要库的维护者更倾向于 P1144 的设计这对你有什么关系?如果当平凡重定位被标准化时,对于合适的类型,例如 std::vector 重分配这样的操作可能会变的快得多,对于大型容器可能会快几个数量级,但是关于如何主动采用这一特征,以及它能提供哪些具体的行为保证,目前仍在讨论和制定中。基准测试,正确处理带来的性能影响让我们通过一些具体的数字来理解性能影响。我在 GCC 13.3.0 的x86_64 机器上运行了基准测试,优化级别为 -O3,测量了一个包含 10000 个自定义对象的 vector 的操作:安全操作的代价:移动与复制包含 10000 个自定义对象的 vector 的性能比较操作时间加速比注释深度复制7.82ms1倍基础操作,分配内存并执行复制操作移动(正确)1.08ms7倍即时交换指针在const类型上移动7.50ms1倍陷阱:如果移动构造传入的参数是const 类型,会默默退回到深度拷贝测试2,返回值迷思一个常见的优化是将返回值包装在 std::move 中,这有用吗操作耗时结果return x; (NRVO)0.83ms最快(零拷贝构造)return std::move(x);0.82ms等价 (误差范围内)它们的结果是相同的测试3,重新分配的成本类型实现时间性能损耗大类型(带有 noexcept)1.63ms正常水平错误类型(未带有noexcept)16.42ms慢10倍结果慢了10倍测试代码让我们对上述这些数字做一个更详细的说明:移动与复制:对于这个场景,正确实现的移动操作大约比复制操作快7倍,这不是笔误,就是7倍,复制操作需要分配并复制10000个对象,而移动操作只用交换几个指针。NRVO 与移动:现代编译器足够智能(如GCC 15) 可以直接在调用者的栈帧中构造返回值(命名返回值优化),在这里添加 std::move 并不能使代码更快,至多它什么都不做,最坏的情况是它阻止编译器进行最块的优化。const 错误:从const 对象移动的性能与复制完全相同,因为这就是它的作用,编译器会默默的选择拷贝构造函数,并且你不会得到任何的警告。这就是性能分析为何如此重要,看起来最优化的代码可能在做完全不同的事用实际例子来说明:如果你正在构建一个在各个阶段之间移动数据的数据处理管道,如果移动语义使用不当,一个毫秒级的操作可能会变成7毫秒的操作。在规模上,这可能是每秒处理1000个请求和处理140个请求的差别理解 std::move 的心智模型让我们把这些整合到你可以编写代码的思维模型中把 std::move 想象成对编译器的承诺,“我已经用完这个对象了,你可以安全的掠夺它的资源”。这不是一个移动的指令,而是一个移动的许可。实际的移动发生在这个被许可的对象执行移动构造或者移动赋值运算时。当你写 std::move(x) 时,你正在改变编译器对 x 这个值的理解。你正在将它从左值(有具体的名称,有内存地址)转换为右值(即将过期,且可以被掠夺),变量 x 本身并没有去哪里,它的内容没有被移动,它们仍然在内存中,你只是改变了它在类型系统中的类别实际的移动操作即资源转移,发生在那个xvalue 表达式被用来构造或者给另一个对象赋值时,这时移动构造或者移动赋值运算符重载会被执行,指针会交换,所有权会转移,源对象会被置空。你的实用检查清单不要在返回值上使用 std::move ,编译器会尽可能进行 RVO 和 NRVO,否则会自动执行移动操作。添加 std::move 会阻止编译器执行优化始终标记移动构造和移动赋值运算符重载函数的 noexcept 。没有这个标记,标准容器在重新分配内存时不会使用你的移动操作。它们会进行赋值从而牺牲性能永远不要在 const 对象上调用 std::move :编译器会默默执行拷贝而不是移动,编译器也不会警告你,如果某个对象是const的,它不能被移动在移动实现中使用 std::exchange,这是最干净,最惯用的移动实现方法。它使所有权转移变的明确而且显而易见不要使用已移动过的对象,除了赋新值或者销毁之外,将它们视为已经死亡的对象。即使它们在技术上仍然存在,它们的值已经被转移消失了仅将 std::forward 用于模板,在普通代码中使用 std::move,仅在实现模板函数中的完美转发时使用 std::forward实际案例,构造一个支持移动操作的容器让我们通过一个现实的例子来整合所有内容,我们将构建一个正确实现移动语义的动态数组类,并解释其中的每个步骤#include <iostream> #include <algorithm> #include <utility> #include <stdexcept> template<typename T> class DynamicArray { private: T* data_; size_t size_; size_t capacity_; // 在需要时用来实现增长的工具类 void reserve_more() { size_t new_capacity = capacity_ == 0 ? 1 : capacity_ * 2; T* new_data = new T[new_capacity]; // 采用移动操作来将元素移动到新内存空间 for (size_t i = 0; i < size_; ++i) { new_data[i] = std::move(data_[i]); } delete[] data_; data_ = new_data; capacity_ = new_capacity; } public: // 构造函数 DynamicArray() : data_(nullptr), size_(0), capacity_(0) { std::cout << "Default constructor\n"; } // 析构函数 ~DynamicArray() { std::cout << "Destructor (size=" << size_ << ")\n"; delete[] data_; } // 拷贝构造,深拷贝 DynamicArray(const DynamicArray& other) : data_(new T[other.capacity_]), size_(other.size_), capacity_(other.capacity_) { std::cout << "Copy constructor (copying " << size_ << " elements)\n"; std::copy(other.data_, other.data_ + size_, data_); } // 拷贝赋值 DynamicArray& operator=(const DynamicArray& other) { std::cout << "Copy assignment (copying " << other.size_ << " elements)\n"; if (this != &other) { // 创建一个新的数据内存 (强异常保证) T* new_data = new T[other.capacity_]; std::copy(other.data_, other.data_ + other.size_, new_data); // 只有在前面的操作成功之后才执行后面的 delete[] data_; data_ = new_data; size_ = other.size_; capacity_ = other.capacity_; } return *this; } // 移动构造,只交换所有权 DynamicArray(DynamicArray&& other) noexcept : data_(std::exchange(other.data_, nullptr)), size_(std::exchange(other.size_, 0)), capacity_(std::exchange(other.capacity_, 0)) { std::cout << "Move constructor (transferred " << size_ << " elements)\n"; } // 移动赋值 DynamicArray& operator=(DynamicArray&& other) noexcept { std::cout << "Move assignment (transferred " << other.size_ << " elements)\n"; if (this != &other) { // 删除原来的数据内存 delete[] data_; // 将当前所有权转移给另外的对象 data_ = std::exchange(other.data_, nullptr); size_ = std::exchange(other.size_, 0); capacity_ = std::exchange(other.capacity_, 0); } return *this; } // 添加元素 void push_back(const T& value) { if (size_ == capacity_) { reserve_more(); } data_[size_++] = value; } // 添加元素 (移动版本) void push_back(T&& value) { if (size_ == capacity_) { reserve_more(); } data_[size_++] = std::move(value); } // 访问 T& operator[](size_t index) { if (index >= size_) throw std::out_of_range("Index out of range"); return data_[index]; } const T& operator[](size_t index) const { if (index >= size_) throw std::out_of_range("Index out of range"); return data_[index]; } size_t size() const { return size_; } size_t capacity() const { return capacity_; } };现在让我们使用这个类,看看具体会发生什么int main() { std::cout << "=== Creating array1 ===\n"; DynamicArray<std::string> array1; array1.push_back("Hello"); array1.push_back("World"); std::cout << "\n=== Copy construction (array2) ===\n"; DynamicArray<std::string> array2 = array1; // Calls copy constructor std::cout << "\n=== Move construction (array3) ===\n"; DynamicArray<std::string> array3 = std::move(array1); // Calls move constructor // array1 is now in moved-from state - don't use it! std::cout << "\n=== Creating array4 for assignment ===\n"; DynamicArray<std::string> array4; std::cout << "\n=== Copy assignment ===\n"; array4 = array2; // Calls copy assignment std::cout << "\n=== Move assignment ===\n"; array4 = std::move(array3); // Calls move assignment // array3 is now in moved-from state std::cout << "\n=== Function returning by value ===\n"; auto make_array = []() { DynamicArray<std::string> temp; temp.push_back("Temporary"); return temp; // RVO or automatic move }; DynamicArray<std::string> array5 = make_array(); std::cout << "\n=== Destructors will be called ===\n"; return 0; }让我们逐步了解每一步发生的情况第一步:创建 array1Default constructor默认构造,数组以空指针、零大小、零容量开始第二步:拷贝构造Copy constructor (copying 2 elements)当我们写 DynamicArray<std::string> array2 = array1 我们明确想要一个拷贝操作,拷贝构造函数分配新的内存并且将每个元素拷贝到新内存中,array1 和 array2 现在拥有各自独立的资源第三步:移动构造Move constructor (transferred 2 elements)这就有趣了, std::move 将 array1 由左值转化为 xvalue(将亡值),移动构造函数看到这一点,不进行内存分配和对象拷贝,而是:获取array1 数据的指针获取array1 的大小和容量将array1 的指针置空,并且将大小和容量设置为0没有内存分配,没有对象拷贝,只是指针交换,现在 array3 拥有array1曾今拥有的资源。而array1则处于一个有效的空状态。第四步:赋值拷贝Copy assignment (copying 2 elements)array4 已经存在(它通过默认构造函数创建),所以这里使用赋值而不是构造。拷贝赋值会分配新的内存,赋值数据然后交换,注意我们在删除旧数据之前分配新内存并且分配新数据。这提供了强异常保证,如果分配失败,array4 内容保持不变第五步:移动赋值Move assignment (transferred 2 elements)类似于构造函数,但是我们需要首先清理 array4 的内部资源(如果有),然后在接管array3 的内容,同样没有内存分配,没有拷贝,只有指针交换第六步,函数返回Default constructor Move constructor (transferred 1 elements) // 可能让我们来讨论为什么,最后的输出是可能出现如果你以高级别优化策略来编译这段代码(比如 GCC/Clang 中的 -O3 或者 MSVC 中的 /O2) 你可能完全看不到 Move constructor (transferred 1 elements) 被打印出来,只会看到 Default constructor 这是由于 NRVO (命名返回值优化) 编译器足够智能,能够意识到 lambda 表达式中的 temp 和 外部额array5 实际上是一个对象,它会完全略过移动操作,直接在最终目的地构建对象然而如果你在调试模式下(为了方便调试而关闭优化),或者函数逻辑过于复杂,编译器无法分析,NRVO 可能不会发生在这种情况下,C++ 保证了下一个最佳的选项,移动操作。编译器隐式地将返回的对象视为右值,它看到 return temp,但是将其视为 return std::move(temp)要点:这是一个两全其美的情况,执行 NRVO (零成本),。最坏的情况,低成本的移动操作。你在这里永远不用付出深度拷贝的代价注意:如果你想验证在 NRVO 禁用的情况下是否会执行移动操作,尝试在 GCC/Clang 上使用 -fno-elide-constructors 编译选项,你会看到移动构造函数理解被打印出来代码链接github 上的示例代码这个示例教会我们什么五法则的实践:我们实现了所有5个特殊函数。如果我们只实现其中一个可能会出现意想不到的情况。例如,如果我们实现了析构但是没有实现拷贝构造,编译器生成的拷贝函数会进行浅拷贝,我们就会得到双重删除的错误。关于移动操作的 noexcept:我们注意到,两个移动函数都添加了 noexcept,这是至关重要的。如果没有它,如果有人将我们的 DynamicArray 放到 std::vector 中,vector在重新分配时不会使用我们的移动操作使用已移动对象:在 std::move(array1) 和 std::move(array3) 之后,这些对象处于已移动状态,它们仍然存在(但是未被销毁),但是资源已经被转移。它们的析构函数仍然会运行,但是它们正在销毁空对象(删除 nullptr 是安全的)强异常保证:注意在拷贝赋值运算符中,我们在删除旧内存之前分配新内存并拷贝数据到新内存。这样在分配或者拷贝时发生异常,我们的对象保持不变。这是异常安全C++的常见模式重载用于右值: 注意我们有两个版本的 push_back。 一个接受 const T& 用于接受左值,一个接受 T&& 用于接受右值,当你使用临时对象调用 push_back 时,会调用右值重载,我们可以将临时对象移动到数组中。当你用变量名调用它时,会选择调用左值重载,进行拷贝操作。性能陷阱:移动语义合适出错让我们来看看即使你认为正确使用了移动语义时也可能出现的微妙性能问题陷阱1:小字符串优化使得移动操作并不是轻微开销现代C++ 使用小字符串优化(SSO) 来处理字符串 std::string 。长度小于特定大小(通常为 15~23个字符) 字符串直接存储在字符串对象上而不是堆上std::string small = "Hi"; // 存储在字符串对象上,不分配堆内存 std::string large = "This is a much longer string that definitely needs heap allocation"; std::string moved_small = std::move(small); // 拷贝字符串中的内存 std::string moved_large = std::move(large); // 交换指针当你对小字符串执行移动操作时,你实际上是在小的字符串上执行拷贝操作。这仍然比堆分配要快,但是它不像移动一个大字符串那样快,移动过的小字符串仍然是有效的(通常是空)移动操作的开销并非总是微小的,对于栈上的小对象,移动操作本质上就是复制,这仍然是可接受的,这是设计如此,但是理解实际发生的情况非常重要陷阱2:在循环中忘记移动std::vector<std::string> source = getLargeStrings(); std::vector<std::string> dest; for (const auto& s : source) { // const 引用无法移动 dest.push_back(s); // 总是执行拷贝操作 }这里的const 阻止了移动,即使你想从 source 处移动,也无法做到,因为const 引用无法绑定到右值对象。正确的版本for (auto& s : source) { // 非 const 引用 dest.push_back(std::move(s)); // 现在可以执行移动 }但是循环结束后 source 仍然存在,并且仍然包含字符串,但是它们都处于已移动状态(通常是空的)。如果你确实不再使用它,这是没问题的,如果你后续打算使用,这就是一个错误。若要彻底转移源资源的所有权,更优的方案如下:std::vector<std::string> dest = std::move(source); // 直接移动整个vector // 现在 `source` 是空的,`dest` 拥有这些字符串的所有权这会移动整个 vector(只是几个指针交换),而不是单个字符串,效率更高陷阱3:多层移动void process(std::string s) { // 采用值传递 consume(s); // 拷贝而非移动 } std::string data = "important"; process(std::move(data)); // 移动到了 process 函数, 但是 process 函数中,执行了拷贝操作移动操作能高效的将资源转移到函数内部,但是随后我们又将它复制到 consume 如果我们希望移动操作能传递void process(std::string s) { consume(std::move(s)); // 现在我们将它移动到 consume }教训:移动操作不会自动传递,每次函数调用都是一次新的机会,可以选择移动或者复制。当你处理一个变量时,对 std::move 要明确。陷阱4:返回语句中的意外复制:std::pair<std::string, std::string> getData() { std::string a = "first"; std::string b = "second"; return {a, b}; // 执行的是拷贝而不是移动 }花括号初始化列表 {a, b} 创建了一个临时对象,将 a 和 b 复制到其中,然后该临时对象会通过移动语义或 直接消除拷贝(Copy-elision) 的方式,传递给返回值。。我们为两个不必要的临时值付出了拷贝的代价。更好的方式std::pair<std::string, std::string> getData() { std::string a = "first"; std::string b = "second"; return {std::move(a), std::move(b)}; // 实现我们将 }这是少数几个在返回值中使用 std::move 正确的情形之一,因为我们不是在移动返回值本身,而是在将元素移动到我们正在构建的用于返回的对象中。继承中的移动语义当你有继承时,移动语义需要小心处理class Base { std::string base_data_; public: Base(Base&& other) noexcept : base_data_(std::move(other.base_data_)) {} }; class Derived : public Base { std::string derived_data_; public: Derived(Derived&& other) noexcept : Base(std::move(other)), // 必须明确指定移动 derived_data_(std::move(other.derived_data_)) {} };注意 Derived 移动构造函数中的 Base(std::move(other)),尽管 other 是一个右值引用,作为表达式使用时,它是一个左值(它有名字),如果没有 std::move 我们将会调用 Base 的拷贝构造函数,而不是移动构造关键点:在派生类的移动构造函数中,other 是一个左值,即使它的类型是 Derived&&。这是"命名右值引用是左值"的规则,一开始让很多人感到疑惑最终的思考:移动语义的哲学让我为你揭示移动语义背后的深层见解。在C++11 以前,C++存在一个根本性的问题,你必须在效率和安全性之间做出抉择。效率:通过指针传递,手动管理所有权,存在内存泄漏和悬垂指针的风险安全性:在所有地方使用深拷贝,接受性能代价移动语义为我们提供了第三种选择,安全高效的转移所有权,它让我们能够编写即像深度拷贝一样安全(编译器跟踪所有内容,无需手动管理)又像指针一样快(无需深度复制,只需交换指针)的代码关键在于许多对象处于行将就木的状态,它们即将被摧毁,或者我们已经不再使用它们。移动语义让我们有机会从这些本来要销毁的对象中收回对应的值,我们不是让它们的资源随着它们一起销毁,而是将这些资源转移给将会继续使用它们的对象。std::move 是个转移的明确标记。这是你在告诉编译器:“我已经用完这个对象,它已经死了,把它的器官拿去交给其他需要的人。” 这就是心智模型比语法更重要,移动语义不仅仅是一个性能技巧,它们是我们对C++中对象生命周期和资源所有权思考方式的根本性转变,理解它们不经能让你在编写C++时更快,还能让你在任何语言中更好的进行资源管理推理。进一步阅读的资源官方文档:带有示例的完整参考关于左值、右值、xvalue 的详细解释编译器何时以及如何消除移动和拷贝完整规范标准提案:P1144:对象重定位 - Arthur O’Dwyer 的非平凡可重定位提案P2786: C++26 的平凡可重定位性 - 被合并到工作草案中的替代方法文章和演讲:-理解何时不应使用 std::move(Red Hat 开发者博客)- 聚焦于常见错误书籍:《Effective Modern C++》作者:Scott Meyers - 第 23-25 条详细介绍了移动语义《C++ Move Semantics - The Complete Guide》作者:Nicolai Josuttis - 整本书都致力于这个主题记住:移动语义是一种工具,而非目标。目标是编写正确、可维护且高效的代码。移动语义有助于实现这一目标,但只有在恰当使用时才能做到。有时你需要的是拷贝。有时编译器会完全消除该操作。懂得何时以及如何使用移动——以及何时不该使用——正是区分优秀 C++代码与卓越 C++代码的关键。
2026年03月28日
11 阅读
0 评论
0 点赞
2026-03-22
读《李飞飞自传》
这本书的标题是:我看见的世界。书中描绘了作者在不同时期追随着目标的过程:幼年时向往那些物理界的先贤,希望在物理学上有所建树本科阶段了解了视觉相关的知识,希望研究视觉与智能,因此考虑研究人工智能中的图像识别博士毕业之后了解到人类视觉进化之后一直没有什么大的变化,人类如今能识别世间万物纯粹是周遭环境训练的结果,因此希望贯彻这一理念,通过WordNet 中各种词句的关联关系创造了训练人工智能的ImageNet视觉识别飞速发展之后,考虑到人工智能在应用过程中的问题,又有了新的目标,研究人工智能的伦理问题。作者这一生都在追求她梦寐以求的北极星,但是她的家庭和美国的伙伴们给了她莫大的支持。父亲放弃国内中产的身份,之身前往美国为了给孩子和家庭一个更好的未来。在美国的学业中,早期不熟悉英语给飞飞带来了莫大的痛苦,在完成每门功课时,相当于要同时完成一项英语作业。但是在学业中遇到了一个好老师萨贝利先生。二人是亦师亦友的关系,萨贝利在飞飞家庭经济拮据时赞助他们开了一家自己的洗衣店。在飞飞多次因为家庭经济问题想要放弃学术研究,进入职场时,是母亲一遍遍的引导她思考“你自己真正想做的是什么”,并且告诉她“家庭是支持你追寻梦想的,不需要你过多的为家庭考虑”。可以说没有亲朋好友的支持,没有父母的支持,飞飞不可能走到这一步。本书没有太多学术性的东西,大量的是作者追求目标中自己的个人思考,特别是她确定研究方向的起始点。我们可以从中找到一个完整的逻辑链条,现在看起来复杂的事物往往它的出发点都是容易理解的。书中我学到了不少东西:多读书:我看到很多伟大人物的传记,几乎所有的人童年都有一个特点,大量的阅读。跨学科思维:飞飞在人类了解人类视觉的过程中受到启发,认为人类几百年来视觉没有迎来大的进化,人类的视觉如此智能的关键来源是整个大自然对人类视觉的训练。从不同的学科中提取分析解决问题的思路。珍惜家庭和亲友关系:他们是心灵的支柱,是人生遇到暴风雨时的避风港一致性原理:遇到复杂事物,从事物最基础的原理出发,往往能得到解决。复杂的事物通常都具有简单的原点科学技术的发展需要与时代结合,过于超前的研究可能在当代掀不起浪花,但是可能对后世的影响极大。像书中提到的神经网络技术,早期因为算力不足的问题,无法产生影响,但是当出现GPU,算力问题解决的时候,神经网络技术在此迅速占据主流下面是书中我喜欢的一些句子的摘录:学术研究,领先一步是先进,领先两步是先驱,领先三步是先烈个体的尊严是至高无上的—这是任何数据集都无法解释、任何算法都无法优化的变量
2026年03月22日
19 阅读
0 评论
0 点赞
2026-03-15
读《枪炮、病菌与钢铁》
这本书在互联网上的名气非常的大。如果作为读书博主,推荐书单中没有这本书,似乎就不配作为读书博主。我在好多读书博主的推荐书单中都看到这本书,我也很早就将它加入了自己的书单,直到最近才有将它翻开阅读。书中以一个作者的土著朋友的问题开始“为什么是欧洲的白人殖民了我们,而不是我们殖民欧洲?”。很多科学家都想以此来证明欧洲人的种族优势大于非洲土著,但是作者通过一些研究得到完全的不同的结论。作者的结论十分简单,这一切都是地理因素。首先欧亚大陆有明显的四季变化,有适合进行驯化的动植物。所以它们优先从狩猎采集发展出了农业。而同样处于温带气候的美洲为什么没有发展出农业呢?作者给出的答案是,美洲大陆没有合适的进行驯化的植物,也没有能够进行驯化的动物,而动物在农业生产中至关重要。在农业种牲畜可以提供劳动力,可以犁地、帮忙运输物资等等。而美洲大陆缺乏能提供劳动力的牲畜,农业规模无法扩大。农业的产生能供养更多的人口,可以养活不进行农业生产的人口,例如各类专家、政治家、职业军人。农业生产会养活更多的人口,形成复杂的人口社会结构,有利于文明的发展。考古学证明大型水利工程都是在形成大的联邦或者国家之后才有能力组织人力进行修建。人口的聚集有利于发展技术,技术的进步能提高生产力,从而存进农业的发展,人口会进一步增加,形成正向的循环。而发明家、专家这些人促进了技术的进步,他们发展出了冶金、航海、文字、火药、轮子等等技术,从而进一步推动文明的发展。农业驯养的马匹给职业军队提供了极强的战斗力,而航海技术为远洋殖民提供了技术支持。而农业社会人畜长时间待在一起,而农业社会人口密度大,长时间定居,这些都有利于人类和牲畜之间互相感染疾病,有利于微生物的进化。在漫长的人类、动物共存的情况下,都进化出了对于疾病抵抗能力。而美洲和非洲土著世代过着狩猎采集的生活,不与动物混在一起,隔一段就迁移,与疾病接触的机会少,缺乏对疾病的抵抗力,在遇到欧洲殖民者时,殖民者身上的致命病菌给他们带来了灭顶之灾,对他们的伤害不亚于武力。似乎到此逻辑链完整了。因为亚欧大陆适合发展农业-->亚欧大陆发展出了农业-->农业能养活各种人口-->不进行粮食生产的人口多了各种发明创造-->定居让人们有了固定财产-->人口密度大,固定居所,病菌传播-->进化出抵抗力但是作者又提到几个问题,既然农业生产这么多优势,为何其他地区的人们不采用?各个地区都有自己的技术优势,为何亚欧大陆形成了压倒性的优势?首先作者提出,农业社会并不一定比狩猎采集社会有优势,只是基于地理环境的无奈选择,这与《人类简史》中的观点一致。非洲大陆作为人类的起源,野生动物在长时间与人类共存中进化出了怕人这一技能。因此非洲大陆的大型野生动物没有灭据,而其他大陆包括亚欧大陆,大型哺乳动物大量灭据,人类可供捕猎的动物资源减少了。另外就是上面提到的亚欧大陆可供驯化的野生动物数量比其他大陆的都多,牲畜给农业提供了劳动力,有利于农业的发展。另外亚欧大陆是东西走向,维度适合农作物生长,也没有天然的屏障。农作物和农业技术可以很方便的进行传播,亚欧大陆上各族人民技术交流频繁,一个地区发展出来的技术很容易传播到另一个技术,从语言学上可以知道很多民族的语言多多少少都受到了其他名族的影响。周边的狩猎采集部落要么看到农业社会的优势主动转变,要么被征服,驱赶,被动的接受改变。这些可以在短时间内完成,而美洲和非洲因为地理环境的影响,他们看不到远在亚欧大陆的人类社会,也无法被早期亚欧大陆上的农业社会所知晓。造成二者初次见面时巨大的差距整本书的结论十分清楚,亚欧大陆的技术优势并不是人种优势,而是他们恰好位于亚欧大陆。如果将各个人种在各个大陆的位置调换,结果也不会发生变化,仍然是亚欧大陆的技术发展领先欧洲。书中我得到一些启示:任何技术的发展我们都无法清晰的遇见它的未来,就像农业基于狩猎采集,二者是在特定环境下的产物,谁也无法预料当初选择农业生产会对人类文明产生几千年的影响。人类现代文明某种程度上也是基于环境的产物,很多我们认为古来如此的习俗和规矩也是基于环境的最优解,环境变了,规矩也得变环境与生产力是共同作用的,生产力的发展使社会环境产生变化,而这个变化反过来有会影响生产力的发展技术的进步需要与外界的交流碰撞,闭门造车要么会导致技术的退步,要么会跟不上时代的潮流。几万年前因为海洋、高山挡住人类文明的交流导致各个地区发展出不同程度的社会样貌,过去非洲和美洲被殖民并不是人种和文化的落后,而是因为地理环境的限制,限制他们看到外面世界的变化。而现代社会我们有了飞机货轮等交通工具、以及互联网这个交流的平台,这些技术的发展抹平了地理和距离的限制。但是人心中似乎慢慢筑起一道不可逾越的屏障,这个屏障正在阻碍不同地区的贸易、技术等的发展。以此警戒我个人能抛弃偏见,客观的看待社会的发展。
2026年03月15日
10 阅读
0 评论
0 点赞
2026-02-27
读《失去的三百年》
学过历史都知道,在明清时期政府采取闭关锁国的政策,最终导致中国错过了西方的工业革命,造成了近代的落后。这本书告诉我们,虽然政府采取闭关锁国,但是那个时候的中国人并不像想象中那么封闭对外界的事物一无所知,甚至清政府也不像想象中那么完全封闭,这本书从地理大发现开始介绍了鸦片战争过去的300年间中国是如何同外部世界打交道的。开放与封闭的历史明朝时期从朱元璋开始就采取闭关锁国的政策,那个时候外国人来华贸易的主要方式就是参与中国制定的朝贡体系,外国商人与使臣一起带上贸易货物来进行上供,在此期间明朝皇帝会表现出对外国人的宽厚和仁慈,也为了表现天朝上国的示例会赏赐数倍于贡品价值的财物。因为巨大的利益可图,前来朝贡了外国人络绎不绝,为了减轻财政压力,政府不断要求外国人减少朝贡次数,同时要求减少使臣的随行人员。虽然明朝的贸易制度有些另外国人不解,但是当时与中国贸易仍然就巨大的利益可以赚取,因此一些制度上的不便并没有减少贸易的热情。同时因为明朝重视农业而歧视商业,并且对农业征收重税导致许多农名放弃农业转而开始经商。外国人的热情不减,民间商人团体不断扩大,最终明朝政府逐渐开放了一些港口作为通商口岸。在明朝时期因为不重视商业,无法理解海洋的重要性,丢失了澳门并且在葡萄牙人占领澳门后并没有很大的反应。随着明朝军事实力的下降以及对中央政府对地方的控制逐渐的力不从心,民间开始发展起来的与外国的贸易,同时政府因为缺乏资金,为了收取关税补充国库,政府开启了几个港口开始对外贸易。在此期间发生了明朝的士大夫随着与传教士的深交发现西方科技高明和领先之处,同时也见识到了西方火器的厉害,一方面士大夫们掀起了一股学习西方的风气,政府为了战争需要也开始聘请西方传教士制作火炮并训练火炮手。同时民间也发起了翻译西方著作的运动。明末时期中国因为政府控制力度不够罕见了形成了开放的社会风气。在此期间出现了大量涌向东南亚的华人,也因为需要对日贸易形成了新的海上势力,早期是一些海盗团伙,随着海盗团伙的合作和吞并,最终形成了一支庞大的海上势力,势力的领袖就是郑芝龙。清朝顺治时期,郑成功利用父亲郑芝龙留下的班子成功收复台湾。利用与澳门和海外的贸易建立了一个海上贸易帝国。但是这个过程随着康熙收复台湾而结束了。康熙皇帝可以说是清朝皇帝中最开放,也是西学最渊博的皇帝,在位期间他对西方的科学技术十分痴迷,甚至聘请传教士来为自己讲课,计算历法,解决工程问题。他明白西方的强大,为了统治的稳定,他允许甚至鼓励他的皇子皇孙学习西方科技,但是他禁止百姓接触传教士,学习西学。康熙为了解决台湾问题重新颁布了禁海令,在台湾问题解决之后重新开启了4个港口,开始了4口通商的时代。雍正皇帝是一个保守的皇帝,从雍正朝开始,清朝的皇帝不再热衷于学习西学,同时禁止传教士传教,开始了对传教士的迫害。清朝的官员与明朝不同,明朝虽然也是闭关锁国,但是底下官员的自主能动性高,可以自行搞学习西方的运动。清朝的官员经过文字狱的洗礼已经变成只会逢迎皇帝的奴才。皇帝搞闭关锁国底下自然跟着搞,甚至层层加码。此时在中国已经形成了两个并行却不相交的世界:商人和海盗能够看到海外的巨变,但他们无法将看到的一切传达给精英群体。而精英群体已经彻底忘记了西方的知识。到乾隆时期,4口通商已经变成一口通商。广州一家独大并且形成了特色的十三行制度。在此期间清政府两头盘剥,对外向洋人商船收取各种苛捐杂税,对内向十三行收取各种钱财,包括但不限于皇帝过寿要求官商捐款,救灾要求捐款。在此层层盘剥下洋人与十三行都不好过。洋人认为是清朝下面的官员贪得无厌,想要上京告御状,但是皇帝压根就不重视于洋人的贸易。皇帝表面上答应了会处理此事,背地里严查在此期间与洋人有过交流的人员特别是给洋人引路写状子的人。19世纪,英国人占据了对华贸易的主要地位。但是此时十三行已经濒临破产,而国内在极度封闭的情况下形成了自给自足的小农经济。英国想要贸易却无门路,同时国内对英国发生了贸易顺差,大量的白银流入中国。英国人此时动起了歪心思,决定用鸦片挽回这一形式。随着鸦片的输入,清政府开始了不同程度的戒烟运动,以林则徐的虎门销烟最为著名。英国要求清政府承认鸦片贸易合法,同时要求开辟新的通商口岸,并且英国以“保护贸易”为名,派遣军舰封锁广州、武力威胁。清政府认为此举是蛮夷挑衅,清政府认为西方需要与中国贸易才能活下去但是中国地大物博不需要依靠西方贸易,因此为了惩罚蛮夷决定断绝与英国的贸易。双方的矛盾越来越深,最终导致了1840年的鸦片战争。总结封建集权制度的核心任务是维持内部的稳定,并以皇帝为中心来格式化社会上的一切,一切都是皇帝意志的体现。皇帝管制信息的行为最终伤害最大的就是皇帝本人,因为他完全被自己制造的预设立场封闭了。皇帝不喜欢外界变化的社会,不喜欢西方的新思潮,底下的官员自然不会向皇帝报告西方是如何先进,技术是如何又发生了翻天覆地的变化。从历史上看,在地理大发现以及远洋贸易中,中国并不是封闭且独立与远洋贸易之外的。中国从某种程度上是参与了贸易的,甚至明末掀起了大翻译运动学习西方科技和文化。郑成功的海上贸易帝国差点可能就成功了。但中国古代历史上一个困扰了两千年的问题是:不管一个时代采取了多少改革和开放的措施,但到最后,集权主义所产生的稳定需求,最后都会导致权力重归闭塞,将之前的所有成果尽数推翻。中国人并不比西方差,而且早期也有开眼看世界的机会,但是因为封建集权制度陷入封闭错失了一系列的机会。
2026年02月27日
15 阅读
0 评论
0 点赞
2026-02-01
Emacs折腾日记(三十六)——打造个人笔记系统
在前面我介绍了如何使用 org mode 来实践 gtd 的理念。其实org mode 和其他工具的结合可以打造一个强大的个人笔记系统嵌入 plantuml作为程序员,对 uml 自然不会陌生,虽然时至今日可能有些显老,但是对我来说它仍然是我不可或缺的工具。而 plantuml 是一种将文本转化为图片的工具。我们可以在 plantuml 入门 找到对应的安装步骤。对于archlinux 来说,我需要首先保证java 安装sudo pacman -S jdk21-openjdk根据 plantuml 的官方介绍,只需要jdk8就可以运行,但是我比较喜欢安装最新的版本。plantuml 中某些图需要依赖 graphviz 来生成,所以这里需要再安装一个 graphvizsudo pacman -S graphviz完成了这些基础组件的安装,下面我们就可以下载 程序这个 jar 包放在哪里都可以,既然是Emacs配合,那么我打算将它放置到 ~/.emacs.d/lib 中作为功能的依赖库我们可以使用官网的测试用例来测试一下是否正常@startuml Alice -> Bob: test @enduml将上述文本保存为 test.txt。然后执行 plantuml.jar -jar test.txt。默认在当前目录中生成同名的png图片。如果显示正常,那么我们就可以进行Emacs的改造工作了根据 官方 的文档,针对Emacs,它提供了名为 plantuml-mode 的扩展插件。我们可以通过以下简单的配置来进行org和plantuml 的联动(use-package plantuml-mode :ensure t :mode ("\\.puml\\'" "\\.plantuml\\'") :config (setq plantuml-default-exec-mode 'jar) (setq plantuml-jar-path (expand-file-name "~/.emacs.d/lib/plantuml.jar")) ;; 设置plantuml jar包的位置 ;; 让org代码块能识别plantuml语法 (add-to-list 'org-src-lang-modes '("plantuml" . "plantuml")) (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))) (setq org-plantuml-jar-path plantuml-jar-path) )我们还是可以用官方给的示例来看看具体的效果#+begin_src plantuml :file demo.png @startuml Alice -> Bob: test @enduml #+end_src这里必须通过 :file 来指定生成图片的链接这里可以为 plantuml 做一个代码片段以便快速进行进入画图的流程。具体细节就不再深入介绍了。各位读者有兴趣可以自行探索。另外关于画图的一些其他技巧和配置,可以参考 面向产品经理的Emacs教程:15. 在Org mode里用纯文本画图构建笔记系统我个人习惯使用双链笔记,简单来说它就像wiki一样随意插入链接,各种知识结构是一个网状的。传统的笔记是树状结构(这里主要是指马克飞象那样的笔记软件对笔记的组织形式),某一条笔记输入某个单元,而这个单元又属于某个父级单元中,就像一本书一样。但我们在学习的过程中,很难在一开始就把知识整理成体系,而是先零散学习,之后随着知识面的增加逐渐形成体系。另外有些跨学科的知识可能会在多个地方被提及到,就像芒格说的跨学科思维。我们无法将某条知识仅仅归于一个大类里面。而当前双链笔记它是没有层级的,它是一个网络结构,任何知识都可以随意引用其他知识。更符合我们的认知习惯,学到新知识了先记下来,未来知识成体系了可以通过链接随意将它放置到任何体系下。在Emacs中可以使用 org-roam 插件来实现(use-package org-roam :ensure t :after org :init (setq org-oram-v2-ack t) :config (org-roam-setup) :custom (org-roam-directory "~/org/roam/") :bind (("C-c n f" . org-roam-node-find) (:map (("C-c n i" . org-roam-node-insert) ("C-c n o" . org-id-get-create) ("C-c n t" . org-roam-tag-add) ("C-c n a" . org-roam-alias-add) ("C-c n l" . org-roam-buffer-toggle)))))在 org-roam 中,一个文件就是一个note,我们可以通过 org-roam-node-find 来打开或者新建一个节点。新建的文件会被保存到我们定义的 org-roam-directory 目录中。在我们编写笔记的时候如果需要关联另一个笔记,可以通过 org-roam-node-insert 在随意位置插入对另一个文件的引用。当我们对知识有了一定的理解之后可以通过 org-roam-tag-add 来添加一些标签方便我们日后查找。另外有些时候我们组织某个知识点时,它下面有一些小的知识点,我将它们作为当前文件中的一个子标题,日后如果希望能链接到这个子标题,我们可以在子标题上使用 org-id-get-create 来创建。请记住在 org-roam中无法直接链接标题和子标题,它实际链接的是一个id,我们在创建新的知识点时使用 org-roam-node-find 本身就完成了创建id的过程。另外我们可以通过 org-roam-ui 来将笔记的节点进行可视化(use-package org-roam-ui :vc (:url "https://github.com/org-roam/org-roam-ui" :rev :newest) :after org-roam :config (setq org-roam-ui-sync-theme t org-roam-ui-follow t org-roam-ui-update-on-save t org-roam-ui-open-on-start t))在Emacs 29及以上版本内置了通过github下载的功能,mepla 本身没有提供org-roam-ui 包,所以这里我使用内置的从GitHub下载的功能。在安装好之后可以通过 org-roam-mode 来开启笔记节点的可视化。它会创建一个web服务并打开浏览器访问 http://127.0.0.1:35901/具体的细节可以查看它的官方文档org-roam-ui因为我的笔记暂时都记录在 obsidian 中,还没有迁移过来,暂时不贴我的截图了。总结到此为止对我来说Emacs已经可以成为日常使用的代码编辑器、笔记管理、日程管理软件了。所以我的折腾就暂时告一段落了。但是这并不意味着这个系列的完结。后续如果当前的配置有问题或者我看到好的点子,又或者自己有什么想法实践之后觉得不错的也会更新到这个系列中。但是这个系列不会像现在这样大规模的更新了。我个人对Emacs的了解并不深入,当前的配置也仅仅是一个可用的状态。但是在编写此系列中仍然受到许多读者的喜爱,在这里感谢各位读者的支持与鼓励。在前面我的博客出现错误或者我有疑惑时也有比我强的读者给出意见,指出我的问题,在这里对他们进行感谢。终于从对Emacs的一知半解到拥有了自己的一套配置,虽然不完美甚至显得幼稚,但是在这个折腾的过程中我收获许多,下一阶段我想实践一下 懒猫说的认真读一读 Elisp reference manual 加深自己的理解。最后列举一下我在这个系列中参考的一些教程专业Emacs入门面向产品经理的Emacs教程21天学会Emacs还有其他一些我引用了但是忘记了具体链接的博客或者教程。
2026年02月01日
21 阅读
0 评论
0 点赞
2026-01-29
lazygit 规范提交记录
背景随着项目的进程,我们经常面临一个问题:发现之前的代码有bug,但是我不知道当初为什么这么写,如果改了会影响哪些?会不会把原来改好的bug又改出来了。我们可以通过 git 的提交记录来查看当初为什么改的。但是 git 提交记录的增长,一个文件提交记录可能有成千上万,要是从头到尾找一遍不知道要找到什么时候。更糟糕的是,好不容易找到了结果提交记录就一条 update at 2026/01/29。这种情况着实令人抓狂。要防止这种情况,我们可以从两个方面着手:要求整个团队规范git 的提交记录在IDE中能快速找到每行代码对应的提交记录规范提交记录git 原版的提交信息模板提交记录我们可以采取国际通用的 Conventional Commits (约定式提交)。它的格式如下:<类型>(影响范围): 一句话总结 <空行> [正文:详细解释为什么这么做,解决了什么痛点] <空行> [脚注:关联的任务单号 ID]正文部分我希望用 Why、How 这两个关键词,也就是为什么要改,如何改。git本身支持自定义 commit 信息的格式,我们可以将一个模板添加到 ~/.gitmessage。然后通过命令git config --global commit.template ~/.gitmessage来指定使用定义的模板,这里我定义的模板如下:<type>(scope): <subject> # --- 为什么修改 (Why) --- # 描述导致问题的现象,或为什么要增加这个功能 # --- 解决方案 (How) --- # 简述核心算法或处理逻辑 # --- 关联单号 --- # Fixes: #这里的 <type> 可以是修改的类型,这个部分是必须的,我一般喜欢定义这么几种类型bugfix (修改bug)feature (添加新功能)doc (更新文档或者注释)forspell (拼写修改)scope 代表的是影响范围,可以根据项目情况灵活的定义,例如在一个前后端分离的项目中,可以定义范围为UI、数据传输、权限等等模块最后的 subject 就是一句话总结,例如"修改普通用户可以访问其他用户隐私文件的bug"后面我可以通过 git commit 来触发模板,后续通过git 默认的编辑器(一般是vi 或者 nano)。lazygit 的配置lazygit 本身也支持自定义配置,它主要通过 config.yml 文件配置,默认的配置文件位置如下:Windows: %LOCALAPPDATA%\lazygit\config.ymlMacOs: ~/Library/Application Support/lazygit/config.ymlLinux: ~/.config/lazygit/config.yml我们可以通过一个命令快捷键触发一个规范化提交的功能。用户自定义命令的模板可以在这里找到。它以 customCommands 作为根节点。后面接 key,command 和 prompts。各个部分的含义如下:key: 用来触发命令的快捷键command: 真实触发的命令prompts: 触发时的行为prompts 是另一个根节点,用于定义详细的行为。它的子元素如下:type: 输入项的类型,有 menu 表示下拉列表框;input代表输入框;menuFromCommand根据用户提供的外部shell命令来生成一个下拉列表框title: 输入框的标题,提示我们这个框用来输入什么信息key: 在command中,需要填入一些数据,我们暂时利用占位符来表示,key代表的是某个具体占位符,需要与占位符对应如果我们的类型是 menu 的话,还需要利用 options 标签来表示具体的选项。最终我的配置如下:customCommands: - key: 'X' command: "git commit -m '{{.Form.Type}}{{.Form.Scope}}: {{.Form.Subject}}' -m 'Why: {{.Form.Why}}' -m 'How: {{.Form.How}}' -m '用例文档或者jira单: {{.Form.TestCase}}'" context: 'files' description: '规范化提交 (Gitmoji + Scope)' prompts: - type: 'menu' title: '选择提交类型 (Type)' key: 'Type' options: - name: '✨ feat (新功能)' value: '✨' - name: '🐛 fix (修复Bug)' value: '🐛' - name: '🚀 更新流水线或者部署脚本' value: '🚀' - name: '📝 docs (文档修改)' value: '📝' - name: '⚡ perf (性能优化)' value: '⚡' - name: '🎨 style (格式/美化)' value: '🎨' - name: '🍎 修复苹果系统上的问题' value: '🍎' - name: '🐧 修复linux 系统上的问题' value: '🐧' - name: '🏁 修复Windows上的问题' value: '🏁' - name: '🤖 修复安卓上的问题' value: '🤖' - name: '⬆️ 升级依赖' value: '️⬆️' - name: '⬇️ 降低依赖' value: '⬇️' - name: '♻️ 代码重构' value: '♻️' - name: '➕ 添加依赖' value: '➕' - name: '➖ 删除依赖' value: '➖' - name: '⏪ 代码回滚' value: '⏪' - name: '🔀 代码合并' value: '🔀' - name: '👽 因外部API改动而更新代码' value: '👽' - type: 'menu' title: '选择影响范围 (Scope)' key: 'Scope' options: - name: 'layout' value: '(layout)' - name: 'render' value: '(render)' - name: 'data' value: '(data)' - name: 'none (无特定范围)' value: '' - type: 'input' title: '简短总结 (Subject)' key: 'Subject' - type: 'input' title: '为什么修改 (Why)' key: 'Why' - type: 'input' title: '具体做法 (How)' key: 'How' - type: 'input' title: '用例文档或者jira单 (TestCase)' key: 'TestCase'在command 中利用git命令来生成一条记录详细提交信息的内容。{{}} 中包裹的都是占位符, .Form.Type 表示这部分内容来自用户后续提交的表单项 Type 中的内容。后续在 prompts 中某一个key的名称需要为 Type 以便进行对应上述提交的内容我仍然采用 Conventional Commits 的格式,首先 type 部分我采用 gitmoji 中规定的符号来表示提交的类型。影响范围我根据我当前的项目模块暂时定了 layout、render、data 等范围。正文部分我提供了三项,即 Why、How、TestCase表示为什么这么改,可以描述一下bug现象,产生的原因。How 表示如何修改的,可以简短的描述一下算法或者具体修改项。最后加上一个用例或者bug管理系统中的单子,因为我公司采用的是jira,所以这里我可以关联上jira单号IDE 中查看提交记录因为我在公司中主要采用 Visual Studio 和 Visual Studio Code,所以这里主要介绍它们上面可以使用的插件,至于我钟爱的NeoVim 和 Emacs,我还没来得及研究,暂时不介绍它们的配置了Visual Studio 中可以使用 Git Line Blame 插件。Visual Studio Code 上可以使用 GitLens 它们的作用都是显示光标所在行对应的提交记录。它们的效果各位读者可以自行到插件官方文档中找到截图。我们在上面记录了测试用例或者bug 单子的另一个好处时可以根据测试用例和bug单快速查找与之相关的提交记录。可以使用下列命令git log --grep jira-111实际上它就是一个 grep 过滤,如果使用管道加 grep ,它只会找到对应的输出无法关联到具体的提交记录,但是通过git log 提供的grep它会显示匹配上的具体的提交记录到此我觉得已经可以解决我个人的问题了,不知道上述内容对各位读者是否有用。各位读者如果有更好的想法可以在评论区留言,欢迎读者给我介绍新的解决思路
2026年01月29日
15 阅读
0 评论
0 点赞
2026-01-25
Emacs 折腾日记(三十五)——归档
在前几篇文章中,我们经历了 GTD 流程中的收集想法、制定计划、以及执行和记录计划的过程,现在我们继续后续的流程,也就是最后的回顾和归档。当日回顾在我个人实践 GTD 的流程中,前一晚会做这些事情:回顾一下今天完成哪些内容哪些内容未完成的原因是什么?时间利用的效率不够?有其他优先级更高的任务占用了时间?任务划分的颗粒度不够细?今天时间利用的效率如何明天计划要做哪些事针对颗粒度的不够细的问题,我们可以考虑一下将事情分解成几个子任务,子任务又可以再分子任务。也就是加几个子列表的时期。如何查看时间的利用效率呢?我们可以统计各个任务的耗时。如果我们严格按照org-pomodoro 插件的方式来记录时间消耗的话,后续在一天结束时可以利用emacs中的报表功能来统计时间的消耗情况在org-agenda 视图的 Agenda 中,有一个名为 org-agenda-clockreport-mode 的命令可以展示当天的耗时情况。但是默认显示的内容比较简单,我们需要对其进行简单的改造,这里主要通过变量 org-agenda-clockreport-parameter-plist 来完成,我设置的相关代码如下: (org-agenda-clockreport-parameter-plist '(:link t ; 让任务名称可点击,快速跳转到原文 :maxlevel 5 ; 显示到第5级任务(数字可调,越大显示越深) :fileskip0 t ; 跳过耗时0的文件,让报告更简洁 :compact nil ; 设为 nil 以显示完整树状结构,而非紧凑模式 :narrow 80))之后我们可以在 Agenda 视图中按下R 或者直接调用 org-agenda-clockreport-mode 来显示任务耗时。这里因为我测试机器上数据不够,暂时无法显示出好的效果,就不给出图片了。各位读者可以自行实验。归档针对已完成的任务我们需要对它进行归档,将它们从事先定义的位置移动到另一个位置进行保存。每月或者每季度可以根据归档内容做一个总结。首先我们需要定义将归档的内容放到哪个文件中,可以通过 org-archive-location 来实现。(org-archive-location "~/org/archive/%s_archive::")上述代码可以将条目归档到原文件同级目录下以日期命名的归档文件中。我只需要在对应任务条目下执行 org-archive-subtree。但是针对我个人的需求来说,它有两个问题它会无条件的将我光标所在的任务和它的子任务进行归档,不管它是否有未完成的子任务或者它本身是否完成它需要在对应的org 文件中进行,对于任务分散到多个org文件中的场景无法一次性完成归档任务对应多个任务分散在多个文件的情况,我们可以使用 org-agenda 中的搜索功能找到所有的状态为DONE的任务。但是它会显示一些子任务,如果不仔细区分很有可能在主任务未完成的情况下将主任务进行了归档。目前我没有找到合适的方法来过滤这种情况。所以只能根据实际情况来辨别了。在操作上,我们可以通过在 org-agenda 命令面板上输入s 来搜索所有状态为 DONE的任务。然后在显示的任务中寻找需要归档的任务,最后将光标放置到具体任务上按下 $ 即可完成归档操作总结在本篇其实我想做的事情还是有的,但是能力有限现在没有找到有效的办法,我认为在归档方面需要做的改进主要有两个:提供简单的方法可以一键显示需要归档的任务,这个视图只显示主任务,其下的子任务不应该显示回顾当日任务时应该只显示任务名称,我不太关心它来自于哪个文件的哪个分支下哪位读者有相应的解决方案可以给我留言,或者在评论区给出。或者有更好的思路也可以留言评论。最后感谢各位读者的阅读。
2026年01月25日
22 阅读
0 评论
0 点赞
1
2
3
...
37