首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
260 阅读
2
nvim番外之将配置的插件管理器更新为lazy
140 阅读
3
2018总结与2019规划
137 阅读
4
从零开始配置 vim(15)——状态栏配置
134 阅读
5
PDF标准详解(五)——图形状态
110 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
emacs
VimScript
linux
elisp
文本编辑器
Java
投资理财
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
378
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
378
篇与
的结果
2026-04-26
wsl 迁移
之前我使用Linux的方式主要是在wsl 中。WSL 中的Linux系统默认放在了C盘,随着使用频率的增加,里面会充斥各种环境、代码、以及其他的内容,最终导致占用磁盘飙升。因为最近我发现我C盘已经飘红了,所以我需要将其中Linux系统从C盘移动到其他盘,目前我就D盘还有大量的空间所以这里我将它移动到了D盘。迁移在移动之前我们先要确保系统处于关闭状态。这里使用如下命令确认它们的状态wsl --list --verbose这条命令会输出当前系统中所有的Linux子系统的名称和对应的状态,我们需要保证对应的系统状态为 Stopped如果处于运行状态,我们可以使用wsl --terminate Arch其中最后的 Arch 就是上一步看到的系统名称停止之后,我们可以使用命令wsl --export Arch D:\WSL_Storage\Arch.tar后面两个参数分别是前面显示的系统名称,和需要备份保存的位置之后我们需要删除原先位于C盘的子系统,使用命令wsl --unregister Arch最后我们从之前备份的位置导入并生成一个新的子系统wsl --import Arch D:\WSL_Storage\Arch D:\WSL_Storage\Arch.tar这条第一个参数Arch代表的是新系统的名称。第二个参数表示系统内容存放的目录。第三个参数是从哪里导入系统数据。再次打开wsl的子系统我们会发现它采用的是root账户登录我们需要默认采用之前配置的普通用户。我们可以使用命令Arch.exe config --default-user archArch.exe 是之前子系统的名称,后面是设置该系统默认的登录用户,这里我仍然使用之前的用户名进入系统之后,我发现终端上之前配置的一些Unicode emoji 符号失效了显示乱码。这是因为我们相当于重新建立了一个子系统,在Windows Terminal 中为这个新系统配置的字体是默认的字体,我们需要在Windows Termianl中为这个新系统选择一个Unicode 字体。之后我们会看到一切恢复如初了
2026年04月26日
22 阅读
0 评论
0 点赞
2026-04-19
股票投资学习记录(三)——快速筛选公司
价值投资有一个非常重要的概念,就是能力圈,投资那些你能看懂的公司。投资之前需要详细的分析,但是A股有5000多家上市公司,要是每一家公司都详细的分析,可能大部分的都被浪费在了垃圾公司里面,A股大量是平庸的公司,值得投资的好公司和特别垃圾的公司占少数。我们在平庸公司中投入大量时间分析、投资,结果可能不如投资少数值得投资的优秀公司。所以在了解公司之前需要快速过滤垃圾和平庸的公司,只留下少数值得投入精力研究和分析的公司。我们可以通过基本的财务数据筛选值得后续关注的公司。公司的经营活动围绕着如何赚钱、如何赚更多的钱、以及如何搜集更多的资源赚钱。这就对应着公司的营运、投资和筹资三大活动。财务报表可以反应这三种活动具体的效果。财务指标上可以分为:成长类指标、资产指标和效率类的指标成长类指标成长类指标:主要有营业收入、主营业务现金流、净利润;它们主要衡量企业的赚钱能力。营业收入营业收入是指公司做生意赚的钱,一般在利润表中可以找到。做生意也好、投资也好肯定想要投资那种业务越做越大,赚的钱越来越多的公司,对应的也就是营业收入越来越高的公司。我们可以拉长时间到5年或者10年,观察营业数据是否稳定增长,有没有大幅下跌。企业的营业收入是比较好调节的,有时候发现企业营业收入大幅度上涨,觉得它是好公司,但是它的上涨可能是造假,营业收入不等于真实收到的钱,只要签了合同就算营业收入。具体这个收入的增长是不是真的可以从现金流量表中的销售商品、提供劳务收到的现金这一个科目的增长趋势看出来。当然这一个科目也有造假的可能,但是比只盯一个指标要保险的多。我们可以将营业收入和主营业务的现金流叠加到一个图表中查看二者趋势是否一致。有的时候发现主营业务的现金流要大于营业收入,这主要是公司在整个生态中处于优势地位可以先收钱后发货。相对应的主营业务现金流小于营业收入的一般是处于弱势,需要先发货后收尾款,这种企业虽然也能增长但是一旦行业出现变故容易收不回尾款造成财务危机。当然也有二者几乎相等的企业,这种一般是面向个人消费者的公司,个人消费者购买产品一手交钱一手交货。总结一下这部分,选股的一个标准应该是长期来看营业收入有增长并且主营业务的现金流不小于营业收入。利润增长同样的,看利润也是要看几年的利润增长。一般来讲营业收入与利润应该是同步增长的,如果营业收入是增长的但是利润没有什么起色,可能是公司财务造假又或者是成本上来了,收入无法转化为利润,这个时候就应该留一个心眼了。资产类的指标我之前不知道在哪看的一个定义,企业的资产是能带来实际收益的东西。大致意思就是能给企业赚钱的就是资产,所以我们分析企业的资产质量就能知道企业未来能不能继续赚钱。也能与之前的营业收入相对照。在快速排查企业的时候我们只需要关注几个指标就行:现金、短期借款、应付账款。现金在财报中对应的是现金和短期金融资产。但是现金太少有些急于用钱的场合就拿不出钱来,比如员工的工资和福利保障、给供应商结尾款、还欠款的利息。账上的现金太多了也不好,多的钱一来会贬值,而来无法产生效益可能降低企业未来的增长。但看某一年的现金是无法体现出价值的。我们仍然需要看多年的现金情况,企业应该将现金控制在一个合理的范围。我们可以看公司的现金能否覆盖公司的短期负债。可以用现金和现金等价物 与 短期借款、应付票据、应付账款、预收账款、合同负债、其他应付账款、一年内到期的非流动负债简单对比一下,看看规模是否可控。并不是说现金需要大于它们的和,因为有时候后面经营持续产生现金流,可以用后面的钱来还债。但是要保证它们一直处于可控范围,如果现金没有增加但是短期负债大量增加就需要小心了。预售账款、合同负债属于收钱但是没有发货,这种不需要真正的给钱,只要后续发货了,债务就能消除。所以我们真正要看的就是短期借款、应付票据这几项需要真正给钱的。效率类的指标效率类的指标目前处于新手阶段的我主要关注ROE,也就是净资产收益率,这也是我在介绍巴菲特投资的相关书籍中学到的,芒格也说长期来看投资公司的收益约等于公司的净资产收益率。它的公式为:净利润/净资产;衡量企业用股东的钱赚钱的效率。我在之前的一些书籍之中,推荐看ROE在10%以上的公司,那么我也采用这个标准来作为我筛选公司的指标。总结最后我给出我目前采用的筛选公司的具体指标:最好被纳入沪深300等宽基指数之中(我不认为我有能力找到被各路股评人和专家都看走眼的公司,所以最好的办法就是在大家公认的优等生中找)连续3年净资产收益率不小于10%连续3年公司扣非净利润为正、净利润为正营业现金流 / 净利润 > 1连续3年公司自由现金流为正流动资产与流动负债的比值大于70%累计分红派现金额大于累计融资金额连续分红5年以上,且后3年分红金额不能大幅减少股息支付率大于30%小于70%我比较看重公司的分红,对我这样的新手来说,我不认为我能找到被大家普遍看衰的低估公司。我也不认为我能预判市场能准确找到未来盈利能力大幅增加的公司。我个人的定位是找到那些财务稳健能持续分红的好公司,如果能叠加营业收入和每股分红都缓慢增长的企业就不错了。
2026年04月19日
21 阅读
0 评论
0 点赞
2026-04-19
大秦铁路(601006)初步分析
基本信息公司名称: 大秦铁路股份有限公司所处行业:交通运输 — 铁路公路核心业务:以铁路货物运输(主要为煤炭重载运输)为核心,兼顾少量客运业务,辅助向国内其他铁路运输企业提供运输相关服务(如线路使用、机车牵引等)特点:核心资产为大秦线(我国重载煤炭运输核心干线),聚焦“西煤东运”核心任务,主要将山西、陕西等中西部地区的煤炭运往东部沿海港口及工业基地商业模式靠什么赚钱: 核心盈利来源是铁路货物运输收入(其中煤炭运输收入占比超90%),依托大秦线重载运输能力,承接中西部煤炭产区到东部港口、工业基地的运输业务;其次是少量客运收入、线路使用费、机车牵引服务等辅助收入。公司实际控制人为中国铁路太原局集团,依托铁路系统资源,聚焦“西煤东运”核心场景。护城河是什么:拥有并运营我国运能最大、效率最高、技术最先进的煤炭重载运输线路——大秦线,具备绝对路网垄断性(无替代线路连接中西部煤炭核心产区与东部核心港口);承载国家“西煤东运”战略,具备政策壁垒;重载运输技术成熟,形成难以复制的技术壁垒;同时依托铁路系统资源,具备稳定的货源和运力保障未来5到10年会消失吗:煤炭作为我国基础能源,未来5-10年仍是电力、钢铁、化工等行业的核心原料,“西煤东运”的核心需求不会消失;大秦线作为“西煤东运”的核心通道,无替代方案,业务稳定性极强。此外,公司核心资产(大秦线)已建成,后续主要投入为线路维护成本,资本开支较低,现金流充裕,具备持续分红能力,业务无消失风险。分红历史连续分红多少年不间断连续18年分红不间断近5年分红是否稳定近5年每股分红金额有下降趋势最近12个月股息率大概是多少: 4.19% 左右分红率(分红/净利润) : 57.31% 左右盈利稳定性5 年净利润: 近5年净利润呈波动下降趋势5-10年营业额: 前5年稳定重有增长,最近5年,营业额略有下降特点: 主营业务(煤炭运输)增长弹性较低,短期受煤炭运输量、运价影响较大,存在一定增长瓶颈;但盈利稳定性较强,不会出现大幅亏损,利润增长依赖运输量提升、运价优化及成本控制。现金流经营活动现金流是否稳健经营活动的现金流相比前几年大幅下降,自由现金流持续减少财务安全资产负债率:14.02%, 与之前相比得到优化有息负债高吗: 有息负债率约为3%左右,偿债压力不大短期应付刚性债务大概为:61亿左右,账上现金足够还债。短期内没有偿债压力是否有大风险: 财务稳健,不会有大的风险。背靠国家,主营业务目前稳定。赚钱能力ROE:3.82%,赚钱能力一般估值与安全边界股息率: 处于合理范围,也符合我个人的大于4%的买入标准PB: 10 年历史分位约 2.18%,处于低估区间PE:10 年历史分位约 92.58%,处于高估核心风险公司收入90%以上来自煤炭运输,业务结构极度单一,抗风险能力弱;若煤炭运输量出现下滑,将直接导致主营收入、净利润大幅下降能源转型与环保风险:全球低碳经济推进,光伏、风电等新能源替代煤炭的速度加快,叠加国内环保政策收紧,长期将导致煤炭需求下降,进而减少煤炭运输量,主营收入可能持续下滑。运价管控风险:铁路运输运价受国家政策严格管控,公司无法自主根据市场需求调整运价,盈利增长只能依赖运输量提升和成本控制,增长弹性极低。运输量波动风险:煤炭运输量受煤炭行业周期、上下游供需(如电厂库存、钢铁行业需求)影响较大,若出现煤炭供过于求或需求萎缩,将导致运输量波动,影响盈利稳定性。结论商业模式:商业模式简单清晰,聚焦“西煤东运”核心场景,依托大秦线的垄断优势和国家战略支撑,业务稳定性极强;但业务结构单一,增长弹性较低。财务状况:财务结构极其稳健,资产负债率低、有息负债少,短期无偿债压力,经营活动现金流充裕,具备持续分红能力,无重大财务风险。分红:当前股息率(4.19%)处于合理区间,符合个人“大于4%”的买入标准,且公司分红连续性强、分红率稳定,具备高分红吸引力。估值:综合PB低估(10年分位2.18%)、股息率达标、PE伪高估的特点,当前估值处于低估区间,具备一定的安全边际。主营业务收入持续减少,背靠铁路,而铁路运输运力有限,运输业务有天花板。叠加未来能源转型与低碳经济下,日子不会好过。业务量有点守不住的样子,不纳入我的股票池
2026年04月19日
14 阅读
0 评论
0 点赞
2026-04-11
长江电力(600900) 初步分析
本篇是我尝试初步分析公司的第二篇,本周我打算尝试分析长江电力。基本信息公司名称: 中国长江电力股份有限公司所处行业:电力——水利发电核心业务:水利发电特点:背靠长江,发电几乎不需要成本,唯一的成本可能就是电机的维修与更新商业模式靠什么挣钱:背靠长江,靠长江水发电,将电力卖给国家电网,靠卖电挣钱护城河是什么: 独有的巨型水电站资源壁垒,长期购电协议 + 优先上网长期购电协议 + 优先上网,极低的边际发电成本未来5到10年分红会消失吗:不会,长江水不会枯竭,发电量常年稳定分红历史连续多少年分红不间断: 分红连续23年不间断近5年分红是否稳定:分红稳定并逐年增长最近12个月股息率大概是多少:股息率大概为3.51%左右分红率 (分红/净利润):71%盈利稳定性5 年净利润: 近几年下滑5-10 年营业额: 营业额一直大幅增加特点: 经营相对稳定有一定增长,但是成本攀升导致利润不增反降现金流经营活动现金流是否稳健:经营活动的现金流同比忽高忽低,借款攀升财务安全资产负债率: 60.79% 对于水电行业来说,属于正常水平有息负债高吗: 52%短期应付债务对财务有影响吗: 短期债务600多亿,而经营现金流只有500多亿,叠加账上现金的60多亿还债够呛是否有大的风险:负债一直增加,叠加营业成本增加。而营收主要靠发电,未来不改变,经营压力会增大赚钱能力ROE:13.01%估值与安全边界股息率: 当前处于偏高水平PB: 处于正常偏低的水平PE: 低估核心风险电价固定的情况下,营收增长完全靠发电量增长,主要靠长江水量,如果运营成本不降低,叠加未来长江枯水期,运营压力会增大结论商业模式: 背靠长江,靠天吃饭,特许经营,商业模式优秀财务状况: 财务费用高,拖累利润,但偿债风险可控分红情况:分红长期稳定估值:处于正常水平当前股息率小于4%,不具备投资价值。曾今发生过管理层希望违背当初的分红承诺,被投资人骂之后又改了回来的事情,我认为这是缺乏诚信,侵犯股东权益的恶劣事件。叠加25年长江电力拿着股东的钱投资葛洲坝航运扩能工程,很难想象它能在投资之后有足够的钱分红。因此我不打算将它纳入我的股票池。
2026年04月11日
17 阅读
0 评论
0 点赞
2026-04-11
读《方舟》
书中的故事发生在一个封闭的底下,一群人在一个名叫裕哉的小伙的带领下来到了一处位于地下的建筑。由于中途迷路,导致到达该处时时间来到傍晚,回去已经不可能了,众人决定在这处废弃建筑中先暂住一晚。在天黑之后又发现名为野崎的一家三口因为采蘑菇迷路找到了这里。就这样一行一共10人暂时在建筑中安顿下来。在第二天清晨疑似发生了地震,出口被一块巨石挡住,而另外一个紧急出口需要从被水淹没的地下三层穿过,也就是说目前能有希望出去的出口只剩下被巨石拦住的那个出入口。众人也发现可以启动牵引机器将巨石拉到地下但是启动机器的人将被困在狭小的空间内无法逃生,也就意味着需要牺牲自己成全其他人。更糟糕的是众人发现水位比刚来时涨的更快了,经过计算差不多7天之后将淹没整个建筑。就在此时众人发现裕哉不知道被什么人勒死了,此时众人决定先找出凶手由凶手来拉动牵引机器以便大家逃生。本书的故事脉络有点类似于传统的暴风雪山庄模式,一行人与凶手共同被困在封闭的区间,众人必须在时限之前找出凶手。书中的推理中规中矩,因为也没有什么类似不在场证明杀人、或者密室杀人的神秘谜题,但是本书最后的反转确实来的巧妙。书中大量的篇幅用来描述人物的心理,特别是对于是否该由凶手牺牲自己成全他人的探讨。本身我不喜欢在推理小说中看到别人讲大道理,我认为推理小说专注于推理解密就好了,至于社会问题、人性的探讨留给其他小说就足够了。我耐着性子看完书中长段关于人性的讨论,最终在最后一节出现反转时我突然意识到这本书算是将人性的探讨与最终的反转很好的结合了,某种程度上来说从角色说出的话已经反应了最后的反转只是我没注意到而已。从这点来说比起单纯的讨论人性、讨论社会的其他推理小说,本书做到了逻辑自洽,少了这部分的讨论在结尾反转时总少了点什么。
2026年04月11日
11 阅读
0 评论
0 点赞
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日
33 阅读
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日
16 阅读
0 评论
0 点赞
2026-03-29
股票投资学习笔记(一)——前言
我最早萌生投资的想法,源于《软技能:代码之外的生存指南》一书。书中着重提醒程序员,不应只埋头于代码世界,更要关注代码之外的生活本质,其中特别提到,程序员应当主动学习一些投资相关知识,为自己的生活多一份保障。作为一名程序员,我的收入相较于其他不少职业略高一些,工作多年下来,手上也渐渐积累了一笔闲钱。而如今银行存款利率仅在1%左右,单纯将钱存入银行,早已无法实现资产的保值,继续这样下去显然不合时宜。再加上近期张雪峰老师心源性猝死的事件,让我深刻意识到,人生无常,与其一味奔波忙碌,不如尽早为健康和未来打算,思考如何实现提前退休,从容享受往后的人生。FIRE运动中有一个核心观点:只要资产总额的4%能够覆盖日常开支,就意味着实现了财务自由。在阅读了一些理财书籍后,我对“下金蛋的鹅”这个概念深深着迷——这正是我一直追寻的状态:即便不工作,也能有持续的收入来源。我由此萌生了一个想法:如果能投资股息率稳定在4%的股票,长期持有并获取股息,只要股息收入能够覆盖我的日常生活开支,那不就实现财务自由了吗?但这个想法背后,还藏着两个亟待解决的问题:如何筛选出股息率稳定在4%的股票?买入并长期持有后,股息能否保持稳定,甚至逐年上涨?为了找到这些问题的答案,我决定正式开启自己的股票投资学习之路。我学习新鲜事物时,习惯遵循“是什么、为什么、怎么做”的逻辑框架,放到股票投资上,就是要弄清楚:股票是什么、为什么要投资股票、以及如何投资股票。其中,前两个问题相对容易解答,通过维基百科、AI工具就能获取基础答案。接下来,我结合网络上的公开信息和自己的思考,试着把这两个问题讲清楚。为什么要投资股票我想从自己小时候的经历说起。我出生于上世纪90年代初,那时候物质条件相对匮乏,我一周的零花钱只有5毛钱,每天的早饭钱也不过1块钱——当时一碗普通的面条只要1块,小孩子吃的小份半碗面,5毛钱就能买到。每年过年,我收到的压岁钱加起来也不到200块,父母总会把这些钱收起来,告诉我要存着给我上大学用。可等我真正走进大学校园,再翻开当初的存折时,发现连本带利也只有4000多块,刚好够一年的学费。也就是说,我辛辛苦苦存了十几年的“大学储备金”,到最后连一个学期的学费都不够。后来我才了解到“通货膨胀”这个概念,原来钱是会贬值的,把钱单纯存在银行,它的实际价值只会慢慢缩水,最终被通胀悄悄吞噬。另一个让我深受触动的,是短视频里看到的一个故事:两位博主采访一位卖鸭腿饭的大叔,问他为什么鸭腿饭卖得这么便宜。大叔笑着回答,他有两栋房子用来收租,卖鸭腿饭只是自己的业余爱好,不为赚钱,只为图个自在。这个故事让我明白,人只有拥有稳定的被动现金流,不用为了生存而被迫上班,才能真正追随自己的内心,去做喜欢的事,实现自我价值的提升。这两个故事,其实已经给出了“为什么要学习投资”的答案:钱存在银行会不断贬值,不进行合理投资,自己辛辛苦苦赚来的血汗钱,最终会被通货膨胀一点点侵蚀;投资能为我们创造稳定的被动现金流,而只有拥有能覆盖日常生活的被动现金流,才能真正摆脱生存的束缚,实现人生自由。市面上的投资品种有很多,为什么我偏偏选择股票?核心原因很简单:我个人的投资目标不是大富大贵,而是希望能长期获取公司分红,一辈子有稳定的被动收入。从这一点来看,股票对我而言,无疑是最契合需求的资产。当然,我也清楚股票投资的风险:股票价格有涨有跌,很可能出现“你想拿它的分红,它却想吞你的本金”的情况;而且,我们无法预知一家公司未来是否能持续经营,分红是否能保持稳定甚至上涨。而我相信,通过系统学习股票投资的相关知识,就能学会识别这类优质公司,规避潜在风险——这正是我坚持学习投资的意义所在。股票是什么了解了投资股票的意义,接下来我们就来弄清楚:股票到底是什么。股票的历史,可以追溯到17世纪的荷兰。当时正处于地理大发现时期,西欧的船只可以绕过好望角,前往亚洲开展贸易,从东南亚带回香料、茶叶等商品,通过贸易差价赚取丰厚利润。但在那个年代,航海技术并不发达,船只在航行中遭遇大风浪,导致船只损坏、货物丢失的情况屡见不鲜。尽管如此,大家都清楚,长期、多次利用大船开展远洋贸易,必然是有利可图的。可问题在于,当时很少有人能拿出足够的钱,独自购买大量船只、支撑起整个贸易项目。于是,一种全新的模式应运而生:大家共同出资,成立贸易公司,由公司统一购买船只、开展贸易,而出资的人,就成为了公司最早的股东。不过,远洋贸易的周期很长,往往一次航行就要耗费数月甚至数年,期间总有一些股东因为急需用钱,想要将手中的股份出售;同时,也有不少人看到贸易的利润,想要加入其中、购买股份。就这样,在阿姆斯特丹的一座桥上,形成了最早的股票交易场所,普通人也能参与其中,买卖股份。这段历史,恰恰道出了股票投资的本质:股票是公司所有权的凭证,我们投资股票,本质上就是投资股票所代表的那家公司,成为公司的股东,与公司共同承担风险、分享收益。这些认知,将构成我后续股票投资的核心理念。至于“如何投资股票”,我深知这不是一蹴而就的事情,而是一场需要耐心和坚持的长期学习之旅,后续我也会慢慢梳理、逐步分享自己的学习心得。
2026年03月29日
34 阅读
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日
12 阅读
0 评论
0 点赞
2026-03-22
读《李飞飞自传》
这本书的标题是:我看见的世界。书中描绘了作者在不同时期追随着目标的过程:幼年时向往那些物理界的先贤,希望在物理学上有所建树本科阶段了解了视觉相关的知识,希望研究视觉与智能,因此考虑研究人工智能中的图像识别博士毕业之后了解到人类视觉进化之后一直没有什么大的变化,人类如今能识别世间万物纯粹是周遭环境训练的结果,因此希望贯彻这一理念,通过WordNet 中各种词句的关联关系创造了训练人工智能的ImageNet视觉识别飞速发展之后,考虑到人工智能在应用过程中的问题,又有了新的目标,研究人工智能的伦理问题。作者这一生都在追求她梦寐以求的北极星,但是她的家庭和美国的伙伴们给了她莫大的支持。父亲放弃国内中产的身份,之身前往美国为了给孩子和家庭一个更好的未来。在美国的学业中,早期不熟悉英语给飞飞带来了莫大的痛苦,在完成每门功课时,相当于要同时完成一项英语作业。但是在学业中遇到了一个好老师萨贝利先生。二人是亦师亦友的关系,萨贝利在飞飞家庭经济拮据时赞助他们开了一家自己的洗衣店。在飞飞多次因为家庭经济问题想要放弃学术研究,进入职场时,是母亲一遍遍的引导她思考“你自己真正想做的是什么”,并且告诉她“家庭是支持你追寻梦想的,不需要你过多的为家庭考虑”。可以说没有亲朋好友的支持,没有父母的支持,飞飞不可能走到这一步。本书没有太多学术性的东西,大量的是作者追求目标中自己的个人思考,特别是她确定研究方向的起始点。我们可以从中找到一个完整的逻辑链条,现在看起来复杂的事物往往它的出发点都是容易理解的。书中我学到了不少东西:多读书:我看到很多伟大人物的传记,几乎所有的人童年都有一个特点,大量的阅读。跨学科思维:飞飞在人类了解人类视觉的过程中受到启发,认为人类几百年来视觉没有迎来大的进化,人类的视觉如此智能的关键来源是整个大自然对人类视觉的训练。从不同的学科中提取分析解决问题的思路。珍惜家庭和亲友关系:他们是心灵的支柱,是人生遇到暴风雨时的避风港一致性原理:遇到复杂事物,从事物最基础的原理出发,往往能得到解决。复杂的事物通常都具有简单的原点科学技术的发展需要与时代结合,过于超前的研究可能在当代掀不起浪花,但是可能对后世的影响极大。像书中提到的神经网络技术,早期因为算力不足的问题,无法产生影响,但是当出现GPU,算力问题解决的时候,神经网络技术在此迅速占据主流下面是书中我喜欢的一些句子的摘录:学术研究,领先一步是先进,领先两步是先驱,领先三步是先烈个体的尊严是至高无上的—这是任何数据集都无法解释、任何算法都无法优化的变量
2026年03月22日
25 阅读
0 评论
0 点赞
1
2
3
4
...
38