-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.json
1 lines (1 loc) · 36.8 KB
/
search.json
1
[{"title":"2023总结","path":"/2023/12/31/2023总结/","content":"这几天给自己做个年终总结的想法越来越强烈,可惜荒废了多年的文笔,加上多年理工科文化的熏陶和自己有意避及写点东西的心情(我始终认为写作是需要特定环境下的特定心情的),却不知从何写起。江郎才尽的我早已忘记各类名人名言,或许10年前的我写这篇总结是会以诗、词结尾,但现在面对屏幕的我只能汗颜。同时觉得各大app的年终报告流水账似的总是从1月到12月记录让人看了厌烦。 虽说是年底,但我身上的任务却一点也没减少(苦笑),也许是出于仪式感,我索性把事情扔下,找个时间总结下23年的各种事务。 “生活不可能像你想象的那么好,也不会像你想象的那么糟”,2023年对我而言如果用一个词来形容,我觉得是充实。直白一点,就是累。 1. 学术 科研中95%的时间是令人沮丧的。 与研一上的兴奋、轻松不同,23年上半年的西安本部学习生涯一直给我紧张、矛盾的氛围。线下答辩似的组会与对未来的迷茫贯穿了我上半年的生活。依稀记得当时每天最快乐的是11点从图书馆里出来到校门口买小吃。小吃摊主和城管斗智斗勇,见到巡逻车立马流利地收摊,留下我们一群人站在路口风中凌乱。 于我而言,那段时间的科研生活是枯燥、乏味的,早已记不清多少次实验失败,心态爆炸,期间甚至一度怀疑自己能不能毕业。考研时从图书馆出来觉得学习一天非常充实,而读研后晚上背着电脑回寝室的路上多了些空虚与不知所措。 “千呼万唤始出来”,我在下半年开始前完成了第一篇小论文。赶稿的日子就不叙述了,依旧**。只是在提交后如释重负,感慨了小导与自己的差距: 当然,本部的生活也并不全是焦虑、迷茫,科研之外的生活也同样值得回味。和室友及小姐姐们一起打篮球、吃各种对我来说稀奇古怪的餐厅、和本科兄弟室内乱逛,令我印象最深刻的是傍晚骑着车打卡西北大学、陕西师范大学、西北政法大学,互相吐槽遇到的奇葩事。 那是一段很充实的日子,唯一的遗憾可能就是没有记录下我学生时代最后一节课。恍惚记得那是一个平常的上午,我只想赶紧回到食堂吃午饭,丝毫没有意识到我的学生生涯在某种程度上结束了。 下半年的杭州科研生活则明显轻松了许多,让我有更多的时间去放空自己,去准备工作方面的专业书籍以及追寻自己喜欢的事,并好好地热爱生活。我和同门经常开玩笑,“还是杭州组会形式爽”。 23年的学术科研分为两个明显割裂的阶段,上半年痛并快乐着与下半年略显宽松自由。我想,上半年可以看作大多数人的高三生活,那是一份宝贵的回忆,但不愿意再经历一次。我更喜欢下半年,可以有更多的机会斡旋学术科研与生活,有空余的时间做自己的想做的事,虽然我的哥们和两个室友都因为各种原因与我暂时分别了。 站在今天这个节点,我仍十分感激西电这个平台带给我更广阔的视野以及我导耐心地领着我一步步探索学术道路。还记得第一次协助审稿的惊讶,第一次坐飞机参与学术会议的兴奋,第一次在网站上投稿的新奇。这些独特经历足以让我可以有底气的说,“即使我没有做的很好,但我也知道学术是怎样一回事儿了”。如果非要找一个意义,那么这句话就是西安科研生活的意义。当我踏上回杭的火车时,对博士师兄们深厚的科研功底依旧敬佩。 “考研容易读研难”。 2. 编程技术 半年的本部生活让我明白自己并不适合读博,那段时间的矛盾痛苦在于我确认自己不适合继续深造后,繁重的科研压力与找工作的冲突使我显得十分拧巴。在通过各种途径收集就业信息、形式后,我开始了就业方面的准备。难以两全,半年的时间我仅看完了编程语言入门圣经的一半多一些,力扣只做了不到10道题。 我的23年编程方面的东西基本都是下半年完成的:搭建个人网站、看完3本专业书籍及视频、力扣完成40题等等。编程项目也开始了,不过23年最后一个月的规划出了些问题,断断续续的看了不到3个视频。老实说,这个进度可能要找实习来不及了(再次苦笑): 后半年时间完成这些着实令人汗颜。实验室开放杂乱的环境可能让我始终无法静下心来去做一些事情,在这一年的最后两个月,我搬到了图书馆旁边的小自习室,我把它称之为“阁楼”,它有冰箱、微波炉和中央空调,虽然我一次都没用过哈哈。我喜欢中隐隐于市的感觉,它比较偏僻,但也有几位常客。 3. 个人生活 相遇是人世间的常态,离散也是。 人总是在不断地得到与失去,尽管我知道悲欢离合是常态,但我依旧很讨厌旧人的离去。是三年舍友生活只能一年结束,好不容易摸清每个人秉性,四个人却再也没办法在夜里一块侃山;是两年和师姐的同门生涯相处地却不到一年;也是因轨迹不同和本科同学无可避免地渐行渐远。 所幸,今年也见了不少旧友,在西安和本科好哥们一起去大唐不夜城、爬华山、滑雪,在杭州和小班同学登宝石山、打卡钱塘江。还有和本科组员的匆匆碰面。“人面不知何处去,桃花依旧笑春风”。 这一年去的城市还有无锡和香港。无锡给我的印象是典型的南方城市,园林式的建筑和高绿植化的街道似一婉转优雅的女生,静静地坐在那里。香港则更加包容,处处带有人文关怀,带有电铃的红绿灯和随处可见的残疾人通道真的体现了**“以人为本” **的理念。就是吃的略微不惯和走在中环高楼大厦处稍显压抑。 除此之外,23年读完了4本课外书,写下了一段书评。读书,可以使我暂且逃离繁杂的科研现实世界,沉浸在另一片天地。“莫向外求”: 还有一些零零散散的生活记录,比如杭州亚运会灯光秀、云栖大会… 明天就是2024年了,我相信时间具有连续性,23年的最后一秒与24年的第一秒并没有什么不同。况且人生不如意事,十之八九。但这些并不妨碍人们对新的一年的殷切期望,我仍然很感激这一年经历的一切,尽管我依然迷茫,尽管生活依旧不那么阳光。 2023年12月27日,我在b站许下了2024年新年愿望: (虽然字挺丑…) 当然,还有一些期望我没有放到上面,我觉得那不是靠许愿实现的。 但愿人长久,千里共婵娟。 哦对,我们学校这里也贴上了“新桃”: 跨年就不在朋友圈更新啦,祝所有看到这篇文章的朋友们新年快乐!","tags":["生活"],"categories":["生活","个人总结"]},{"title":"C/C++后端开发岗位技能","path":"/2023/11/30/C-C-后端开发岗位技能/","content":"经过搜集网上相关C++后端开发资料,整理了开发工程师所需的相关技能。 开发工程师需要具备两种技能:硬技能和软技能。 硬技能包括各种计算机基础知识及必备的开发能力,可以通过技术年限不断获得。硬技能决定了能否成为一名合格的开发工程师。 软技能包括跨部门沟通能力,独立组织项目能力,此技能难以通过年限获得大量提升,它决定了工程师的上限。 加油,祝你好运!","tags":["C++"],"categories":["编程","后台开发路线"]},{"title":"以独立语句初始化shared_ptr","path":"/2023/11/21/以独立语句初始化shared-ptr/","content":"在智能指针shared_ptr的应用中,我们提到了可以利用智能指针 shared_ptr 以避免资源泄漏。尽管如此,在 C++中,某些情况下使用 shared_ptr 依然会泄漏资源,这可能与我们的初衷相违背。考虑下面的一种情况: 假设我们有个函数用来表示程序优先执行的等级,另一个函数用来在某动态分配所得的 Widget 上进行某些带有优先权的处理。 12int priority();void processWidget(std::shared_ptr<Widget> pw, int priority); 现考虑调用 processWidget : 1processWidget(new Widget, priority()); 但直接调用上面代码无法通过编译,这是因为上述代码暗含了从new Widget到shared_ptr的隐式转换,即: 1std::shared_ptr<Widget> pw = new Widget; shared_ptr 的构造函数可以需要一个原始指针,但该构造函数是一个 explicit 的构造函数,不能进行隐式转换,可见下图 shared_ptr 的源代码: 因此可以为了通过编译,可以写成这样: 1processWidget(std::shared_ptr<Widget>(new Widget), priority()); 这样似乎很完美,我们利用了 shared_ptr 来管理资源,但不幸的是,上述调用仍可能泄漏资源。 processWidget 函数的第二实参只是一个单纯的对 priority 函数的调用,但是第一实参 std::shared_ptr<Widget>(new Widget) 由两部分组成: 执行 “new Widget” 表达式 调用 shared_ptr 构造函数 于是在调用 precessWidget 之前,编译器必须做一下三件事: 调用 priority 执行 “new Widget” 调用 shared_ptr 构造函数 C++编译器以什么样的次序完成这些事情呢?弹性很大。这与Java、C# 不同,这两种语言总是以固定次序完成函数参数的初始化。可以确定的是,new Widget一定执行于 shared_ptr 构造函数之前,但对 priority 的调用可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行它,最终获得这样的操作序列: 执行 “new Widget” 调用 priority 调用 shared_ptr 构造函数 考虑这样一种情况,如果 priority 的调用导致异常,会发生什么?在此情况下,“new Widget”返回的指针将会遗失,因为它尚未被放入 shared_ptr 内,后者是我们期待用来避免资源泄漏的武器。是的,在对 processWidget 的调用过程中可能引发了资源泄漏,因为在资源被创建(“new Widget”)和资源被转换为资源管理对象(shared_ptr 初始化)的两个时间点之间,可能发生异常干扰。 避免发生这类问题很简单:使用分离语句,分别写出 (1)创建Widget,将它置入一个智能指针内,(2)将这个智能指针传给 processWidget: 12std::shared_ptr<Widget> pw(new Widget); // 在单独语句内以指针指针存储 newed 所得对象processWidget(pw, priority()); // 这样调用绝不会导致资源泄漏 编译器对于“跨越语句的各项操作”没有重新排列的自由(只有在一个语句内它才有自由度)。在上述修订后的代码内,“new Widget”表达式以及“对shared_ptr构造函数的调用“这两个动作,和”对priority的调用“是分隔开的,位于不同语句内,所以编译器不能在它们之间选择任意执行的次序。","tags":["C++"],"categories":["编程","C++"]},{"title":"CPU眼里的汇编语言","path":"/2023/10/30/CPU眼里的汇编语言/","content":"打开 Compiler Explorer, 写一个简单的自加函数: 代码分析 该函数涉及到三个CPU寄存器:eax/rax、rbp 、rsp: eax/rax[1]:一般用来存放数值,类似于C/C++里面的普通变量; rbp 、rsp:一般用来存放内存地址,类似于C/C++里面的指针变量。用来管理、读写内存的栈区(stack)。 初始状态寄存器的值均为 0x100。 首先看函数对应的汇编指令:push。 push对应了两个微操作: 将寄存器 rbp 的值,存放在“栈顶”寄存器 rsp 指向的内存下方(rsp 所指内存的Value值为 rbp 的地址)。 随着“栈顶”向低生长,也就是让 rsp 寄存器的值减 4。 随后是简单的 mov 指令,把寄存器 rsp 的值赋给 rbp,如下图所示: 接着是一个较为复杂的 mov 指令[2]。但通过参考源代码,我们很容易猜出它是要把数值 1,写入到变量 a 所在的内存。 mov 指令和数值 1 都很容易找到,但变量 a 的内存地址就颇为复杂。不过关键字 PTR 提示我们:这是一个指针操作,这也恰好对应了 rbp 本身就是类似于指针变量的寄存器。 所以,这行汇编代码对应C/C++代码类似于: 1*(rbp - 4) = 1; 变量 a 的内存地址等于寄存器 rbp 的值减4;而中括号[ ]就相当于指针变量的解引用*操作;DWORD是指针类型,表明数值 1 将占用 4 个字节的长度。 这里说个题外话:对比 C/C++ 和汇编语言可以看出,C/C++语言是最接近底层的高级语言。 接着进行自加运算。 带 PTR 和 [ ] 的 add 指令也有两个微操作: 首先,用指针的*操作获得变量 a 的值,并与 2 做加法运算; 把加法运算得到的结果,通过指针的*写入到变量 a 所在的内存。 随后的 mov 指令与上文的相似,即将变量 a 的值从内存中读出来,写入到寄存器 eax 里面。 这里对应的C/C++ 语言类似于: 1eax = *(rbp - 4) 即使是普通变量操作,背后的实现原理也跟“指针”脱不了干系。因为在 CPU 眼里,万物皆有地址,万物皆可指针。 最后,就是 push 的反向操作 pop。 它也对应了两个微操作: 把寄存器 rsp 指向的“栈顶”内存的值 0x100,写入到寄存器 rbp。 随着“栈顶”的升高, rsp 寄存器的值也随之加 4。 总结 再宏伟、壮观的房子,真正施工的时候,不过是在重复搭钢筋、倒水泥;再搭钢筋、再倒水泥的过程。CPU 也是如此,我们不过是把数据,在寄存器和内存之间搬来搬去。 通过上文的代码分析我们发现,为了做 1 次简单的 +2 运算,居然产生了(至少)5 次内存读写,内存读写占比高达 83%。据有些机构统计指出,CPU 的内存读写,占据了 CPU 90%的工作负荷。 寄存器 eax 和寄存器 rax 的区别:寄存器 eax 是 32 位的 x86 CPU的寄存器,如今的 x86 CPU多是 64 位的,其对应的寄存器是 rax,eax 只是 rax 的低 32 位而已。 ↩︎ 汇编语言中,mov 指令用于写入。 ↩︎","tags":["CPU"],"categories":["编程","汇编"]},{"title":"智能指针shared_ptr的应用","path":"/2023/10/28/智能指针shared-ptr的应用/","content":"与Java不同的是,C++程序员需要手动管理众多资源,典型的就是内存回收。通过使用智能指针 shared_ptr 可以几乎消除内存泄漏的资源管理问题。这里我们分别以内存中的堆区和栈区为例,介绍 shared_ptr 在资源管理方面的优势。 堆区(heap-based) 假设有一个投资 class 的基类Investment,其余各种各样的投资类型继承它: 1class Investment {...} // “投资类型”继承体系中的基类 现有一个工厂(factory)函数[1]供应我们某一个特定的 Investment 对象: 1Investment* createInvestment(); // 返回指针,指向Investment继承体系内的动态分配对象 为了避免泄露内存和其他资源,将 factory 函数返回的每一个对象适当地 delete 掉很重要: 1234Investment* pInv = createInvestment(); // 调用 factory 函数 ... // 使用它delete pInv; // 释放它,避免内存泄漏 现考虑有个 f 函数履行了这个责任: 123456void f(){ Investment* pInv = createInvestment(); // 调用 factory 函数 ... delete pInv; // 释放pInv所指的对象,避免内存泄漏} 目前看起来似乎没什么问题,但在某些情况下,f 可能无法履行删除 pInv 所指的对象的责任。如: “ … ”区域内有一个过早的 return 语句,导致控制流不会触及到 delete 语句 “ … ”区域内的语句抛出异常 上面代码可能在初期版本没什么问题,但是随着时间渐渐过去可能会被修改、维护,如果有些人添加 return 语句或者 f 的区域有可能调用一个“过去从未抛出异常,却在被 ‘改善后’ 开始出现异常“时,那么这就背离了函数的资源管理策略的初衷。 为了确保 createInvestment 返回资源总是被释放,我们需要确保当控制流离开 f,该对象的析构函数会自动释放这些资源。这就引出了智能指针—shared_ptr。下面示范如何用 shared_ptr 以避免 f 函数潜在的资源泄漏的可能性: 12345void f(){ std::shared_ptr<Investment> pInv(createInvestment()); // 调用 factory 函数 ... // 一如既往使用 pInv} // 经由 shared_ptr 析构函数自动删除 pInv 当 shared_ptr 被销毁(例如一个局部的 shared_ptr 离开其作用域)时,计数器该 shared_ptr 关联的计数器会递减。一旦计数器变为0,它就会自动释放自己所管理的对象。 在改进后的代码中,不论控制流如何离开区块,如过早 return 或抛出异常,一旦对象 pInv 被销毁(离开 f 作用域),其析构函数会被自动调用,于是资源被释放。(在某些情况下,即使使用 shared_ptr 仍可能出现资源泄漏的情况,详见以独立语句初始化shared_ptr) 需要补充的是,shared_pte 在其析构函数内做 delete 而不是 delete[ ]动作,这就意味着在动态分配而得到的数组身上使用shared_ptr是个糟糕的行为,尽管它能通过编译: 12std:: shared_pte<std::string> sps(new std::string[10]); // 很糟糕!会用上错误的 delete 形式std:: shared_pte<int> spi(new int[1024]); // 同样问题 不必惊讶为什么C++工程师没有解决这个问题,即没有特别针对”C++动态分配数组“而设计的类型shared_ptr这样的东西,这是因为STL容器里的 vector 和 string 几乎可以取代动态数组,它们两个有自己的析构函数,可以避免这个问题。 栈区(stack-based)## 我们可以使用 shared_ptr 来建立自己的资源管理类。 假设我们有一个 class Lock 以及它的两个成员函数 lock、unlock: 12void lock(Mutex* pm); // 锁定pm所指的互斥器void unlock(Mutex* pm); // 将互斥器解除锁定 为确保不会忘记将一个被锁住的Mutex解锁,可以通过建立一个 class Lock 来管理机锁: 123456789class Lock{ public: explict Lock(Mutex* pm) : mutexPtr(pm){ lock(mutexPtr); } // 获得资源 ~Lock() { unlock(mutexPtr); } // 释放资源 private: Mutex *mutexPtr;} 用户对 Lock 的用法类似于 shared_ptr 的方式: 123456Mutex m; // 定义你需要的互斥器...{ // 建立一个区块用来定义 critical section Lock m1(&m); // 锁定互斥器 ... // 执行 critical section 内的操作} // 执行析构函数 目前看起来没什么问题,但是如果 Lock 对象被复制: 12Lock ml1(&m); // 锁定mLock ml2(ml1); // 复制ml1到ml2上,会出现问题 当我们将 ml1 复制到 ml2 上时,我们希望保留 ml2的资源 mutexPtr,但如果 ml1 离开区块,其析构函数执行,那么经过复制后的 ml2 里的资源也会被释放,这可能与我们的初衷不符。我们希望保留资源,直到它的最后一个使用者(某对象)被销毁,而中间其余对象被销毁不受影响。为此,我们可以引入 shared_ptr。 如果简单地将 Mutex* 改为 shared_ptr<Mutex>,那么其析构函数行为是”当引用计数为0时删除所指之物“,而不是执行函数 unlock 。幸运的是,shared_ptr 允许指定所谓的”删除器“,即一个函数或函数对象,当引用计数为0时被调用。下面是更新后的代码: 12345678class Lock{ public: explict Lock(Mutex* pm) : mutexPtr(pm, unlock){ // 以某个 Mutex 初始化 shared_ptr 并以 unlock 函数作为删除器 lock(mutexPtr.get()); } // 获得资源 private: std::shared_ptr<Mutex> mutexPtr; // 使用 shared_ptr 代替 raw pointer} 注意,本例中的 Lock class 不再声明析构函数,因为没有必要。class的析构函数(无论是编译器生成的还是用户自定的)会自动调用其 non-static成员变量(本例为 mutexPtr)的析构函数。而 mutexPtr 的析构函数会在互斥器引用计数为0时自动调用 shared_ptr 的删除器(本例为 unlock)。 内存主要分区 最后简要介绍下内存主要的分区。 堆区(heap):用来储存动态分配的对象,即由 malloc/new 分配的内存块。这是在程序运行时分配的对象,当其不再使用时,我们的代码必须显示地销毁它们。 栈区(stack):用来保存定义在函数内的非 static 对象。 静态内存:用来保存局部 static 对象[2]、类 static 数据成员以及定义在任何函数之外的变量(全局变量)。 分配在栈区或者静态内存中的对象由编译器自动创建和销毁。 factory(工厂)函数屏蔽了实现细节,返回一个base class指针,指向新生成的derived class对象。 ↩︎ 函数内的 static 对象是局部 static 对象(local static 对象,因为它们对函数而言是 local 的),其他 static 对象称为全局 static 对象。 ↩︎","tags":["C++"],"categories":["编程","C++"]},{"title":"尽可能使用const","path":"/2023/10/17/const/","content":"const for pointer const 约束指针分为两种:常量指针(const pointer)[1]和指向常量的指针(pointer to const)[2]。 12345char greeting[] = "Hello";char *p = greeting; //non-const pointer, non-const dataconst char *p = greeting; //non-const pointer, const datachar* const p = greeting; //const pointer, non-const dataconst char* const p = greeting; //const pointer, const data 对于pointer to const,关键字const可以放在类型之前,也可以放在类型之后、星号*之前。即: 1const int *p = &i; 等价于 1int const *p = &i; const for STL iterator STL迭代器的作用就像 T*指针。对应常量指针(const pointer)和指向常量的指针(pointer to const),STL迭代器也有不同的声明方式: 类似于T* const指针。在最前面声明const即可。 1234vector<int> vec(10, 5);const vector<int>::iterator iter = vec.begin(); // iter类似于 T* const *iter = 9; // 正确,可以改变iter所指内容的值iter++; // 错误!iter本身是const的 类似于const T*指针。需要用const_iterator。 1234vector<int> vec(10, 5);vector<int>::const_iterator iter1 = vec.begin(); // iter1类似于 const T**iter1 = 99; // 错误!*iter1是constiter1++; // 正确,可以改变iter1自身的值 const for function return value 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,又不至于放弃安全性和高效性。考虑下面一个例子,定义有理数Rational类的operator*: 12class Rational { ... };const Rational operator* (const Rational& lhs, const Rational& rhs); 这里返回一个const对象是为了避免出现错误造成的意外情况: 123Rational a, b, c;...if (a * b = c) ... // 其实是想做一个比较动作 如果a和b都是内置类型,这样的代码直截了当就是不合法。将operator*的返回值声明为const可以防止出现上面的错误。这样做与内置类型恰好兼容。 顶层const ↩︎ 底层const ↩︎","tags":["C++"],"categories":["编程","C++"]},{"title":"const char*、const char[]与string","path":"/2023/09/28/C-中的const-char-、const-char-与string/","content":"以下代码是基于gcc的vscode编译器编写的。首先导入运行示例的必要库,并指定标准命名空间std。 123#include <iostream>#include <string> using namespace std; 1. const char* 与 const char[ ] 定义一个char型的指针p,指向字符串“now”;定义一个字符串数组p1,值为“now”。两者都属于const类型。 12const char *p = "now";const char p1[4] = "now"; const char * 和 const char[ ] 都可以用于处理 C 风格的字符串。 const char* 主要出现在早期旧版本的代码中。这个指针本身是可变的(即可以改变它所指向的位置),但通过这个指针不能修改所指向数组的内容。它比较灵活(可以重新指向其他位置),在某些情况下更为方便; const char[ ]更接近于 “准确” 的类型,因为它更明确地表示这是一个字符数组。但它没有const char* 灵活。 数组有一个特性,在很多用到数组名字的地方,编译器都会自动地将其转换成一个指向该数组首元素的指针。所以const char p1[4] = “now” 中p1也是一个指针。 1234cout << "p: " << p << endl; // p: nowcout << "p1: " << p1 << endl; // p1: nowcout << "*p: " << *p << endl; // *p: ncout << "*p1: " << *p1 << endl; // *p1: n 但是,当我们打印指针p和p1时并没有输出地址值,输出的反而是它们指向的字符串数组。 这是因为cout有一个特殊的重载,当它看到一个 const char[ ] 或 const char * 类型,它会假定这是一个以空字符(即 ‘\\0’)结尾的 C 风格字符串,并逐字符输出,直到遇到空字符为止。 后面两行就容易理解了,p 和 p1都是指向字符串数组首元素的指针,解引用后代表首元素。 2.其他类型数组 与 string 定义一个string对象s,它的内容是字符串字面值 “now”。 为了说明其余类型的数组和const char类型的不同,我们这里定义一个int型数组,它有四个元素:0, 1, 2, 3。由上文,int i[4] = {0,1,2,3}中 i 也是一个指针,指向该数组的首元素0。 12345string s = "now";int i[4] = {0, 1, 2, 3};cout << "s: " << s << endl; // s: nowcout << "i: " << i << endl; // i: 0x61fdd0cout << "*i: " << *i << endl; // *i: 0 s为string的一个对象,与int i[4]中指针 i 代表含义不同,直接打印其值为初始化的"now"。 我们打印指针 i 时输出即为首元素0的地址,第一部分中特殊的重载并不适用。当解引用 i 时打印0。 string对象可以使用下标运算符([ ])访问string对象中的单个字符,这是string类所定义的。 与string类似,对于数组而言,只要指针指向的是数组里的元素,就可以执行下标运算。 123cout << "s[0]: " << s[0] << endl; // s[0]: ncout << "p[1]: " << p[1] << endl; // p[1]: ocout << "i[2]: " << i[2] << endl; // i[2]: w 我们可以分别用s[0]、p[1]、i[2]得到 s 中第0个字符,p 中第1个字符,i 中第2个字符(计数从0算起)。 对于数组而言,p[1]等价于*(p + 1),i[2]等价于 *(i + 2),其中p、i都是指向首元素的指针。 由于p所指向的数组是const的,所以直接对其某一个元素重新赋值会报错。而这对于非const的string类而言并没有限制: 123s[2] = 'a';cout << "s: " << s << endl; // p[1] = 'a'; // error: assignment of read-only location '*(p + 1)'/ 3.补充","tags":["C++"],"categories":["编程","C++"]},{"title":"用十年时间自学编程","path":"/2023/09/28/用十年时间自学编程/","content":"在网上找到了一篇关于编程心得交流分享的文章《Teach Yourself Programming in Ten Years》,原文链接放到这里。下面是中文翻译版: Teach Yourself Programming in Ten Years Peter Norvig 为什么每个人都这么急切? 走进任何一家书店,你都会看到《24小时自学Java》这样的书,以及无数教你几天或几小时内学会C、SQL、Ruby、算法等等。在亚马逊高级搜索中,搜索[title: teach, yourself, hours, since: 2000],找到了512本这样的书。在前十名中,九本是编程书籍,另一本是关于记账的。用“learn”替换“teach yourself”或用“days”替换“hours”得到的结果也相似。 这说明两种情况,要么大家都急着学编程,要么编程真的比其他技能容易得多。Felleisen等人在他们的书《如何设计程序》中也提到了这种现象,说:“写糟糕的代码很简单,21天就能学会,哪怕你是个门外汉。”还有一部Abtruse Goose的漫画也做了类似的描述。 那么,《24小时自学C++》这样的标题到底意味着什么呢? 自学:在24小时里,你根本没办法写出几个有深度的程序,更别说从中吸取经验教训了。你也没机会与资深程序员合作,体验真正的C++编程是怎样的。简单说,你学不到多少。这本书只能教你一些皮毛,而不是真正的深入理解。就像亚历山大·教皇说的,浅尝辄止是很危险的(a little learning is a dangerous thing)。 C++:你可能在24小时内学会一些C++的基础语法(如果你之前已经学过其他编程语言的话),但你真的了解C++吗?如果你只是一个初学者,你可能只能用C++的语法写出初级的程序,但你真的理解C++的精髓吗?Alan Perlis曾说:“一个不改变你编程思维的语言,是没必要学的(A language that doesn’t affect the way you think about programming, is not worth knowing)。”或许,你只是为了完成某个特定任务,需要学一点C++(或者是JavaScript或Processing)。但那样,你其实并没有真正学会编程,只是学会了如何完成那个任务。 24小时:显然,这时间远远不够,下一部分会进一步说明。 十年自学编程 研究者(如Bloom (1985)、Bryan & Harter (1899)、Hayes (1989)、Simmon & Chase (1973))已经证明,在许多领域,如下棋、音乐创作、电报操作、绘画、钢琴演奏、游泳、网球,以及神经心理学和拓扑学的研究中,要达到专家水平大约需要十年时间。关键是有目的的实践:不仅仅是重复做,而是给自己设定一个略超出当前能力的任务,尝试去做,做的时候和做完后分析自己的表现,并纠正错误。然后再重复。再重复。似乎没有真正的捷径:即使是4岁就展现出音乐天赋的莫扎特,也花了13年时间才开始创作世界级的音乐。披头士乐队似乎在1964年凭借一系列的热门歌曲和在Ed Sullivan秀上的表现一炮而红。但他们从1957年开始就在利物浦和汉堡的小俱乐部演出,尽管他们很早就受到大众的喜爱,但他们的第一个广受好评的作品,Sgt. Peppers,是在1967年发布的。 Malcolm Gladwell 推广了这个观点,尽管他更注重的是10,000小时,而不是10年。Henri Cartier-Bresson (1908-2004)有另一个标准:“你的前10,000张照片是最差的。”(他没想到在数字相机的时代,有些人一周内就能达到这个数字。)真正的专业水平可能需要一生的时间:Samuel Johnson (1709-1784)说:“任何领域的卓越都只能通过一生的努力来获得;它的价值不菲。”乔叟(1340-1400)抱怨说:“生命短暂,技艺学习之路漫长。”希波克拉底(公元前400年左右)因“ars longa, vita brevis”而著称,这是更长的引文“Ars longa, vita brevis”而著称,这是更长的引文“Ars longa, vita brevis, occasio praeceps, experimentum periculosum, iudicium difficile”的一部分,英文意为“生命短暂,技艺长久,机会稍纵即逝,实验充满风险,判断困难。”当然,没有一个固定的数字可以作为最终答案:认为所有技能(如编程、下棋、打跳棋和演奏音乐)都需要完全相同的时间来掌握,或者所有人都需要完全相同的时间,似乎都不合理。正如K. Anders Ericsson教授所说:“在大多数领域,即使是最有天赋的人也需要相当长的时间才能达到最高水平。10,000小时这个数字只是给你一个感觉,我们说的是每周10到20小时,连一些人认为最有天赋的人也需要这么长时间才能达到最高水平。” 你想成为一名程序员吗? 以下是我为成功编程提供的建议: 对编程产生兴趣,并因为它有趣而去做。确保它始终足够有趣,这样你会愿意投入你的十年或10,000小时。 以下是我为成功编程提供的建议: 对编程产生兴趣,并因为它有趣而去做。兴趣是任何长期承诺的重要驱动力,编程也不例外。如果你对编程感到兴趣,你会更愿意投入时间和努力去学习和提高。编程本身有很多有趣和挑战性的方面,从解决复杂问题,到实现创新的算法,再到构建引人入胜的应用程序。 动手编程。最好的学习方式是实践中学习。更专业地说,“个体在某一领域的最大性能水平并不是随着经验的增长而自动获得的,但即使是经验丰富的个体,也可以通过有意识地努力提高来提高性能水平。”以及“最有效的学习需要一个为特定个体设定的具有适当难度的明确定义任务、有益的反馈以及重复和纠正错误的机会。”《实践中的认知:日常生活中的心智、数学和文化》这本书是这种观点的一个有趣的参考。 与其他程序员交流;阅读其他人的程序。这比任何书籍或培训课程都更重要。 如果你愿意,可以在大学度过四年(或在研究生院度过更长时间)。这将使你有机会获得一些需要资格证书的工作,并使你对这个领域有更深入的了解,但如果你不喜欢学校,你可以(只要有一些决心)自己或在工作中获得类似的经验。无论如何,仅仅通过书本学习是不够的。“计算机科学教育不能使任何人成为专家程序员,正如学习画笔和颜料不能使任何人成为专家画家一样。”《新黑客词典》的作者Eric Raymond如是说。我雇佣过的最好的程序员之一只有高中学历;他生产了很多优秀的软件,有自己的新闻组,并通过股票期权赚了足够的钱买下自己的夜总会。 与其他程序员一起参与项目。在某些项目中成为最好的程序员;在其他一些项目中成为最差的。当你是最好的时候,你可以测试你领导项目的能力,并用你的动力激励他人。当你是最差的时候,你会学到高手们都做些什么,以及他们不喜欢做什么(因为他们让你帮他们做那些事)。 至少学习六种编程语言。包括一种强调类抽象的语言(如Java或C++),一种强调函数抽象的语言(如Lisp或ML或Haskell),一种支持语法抽象的语言(如Lisp),一种支持声明式规范的语言(如Prolog或C++模板),以及一种强调并行性的语言(如Clojure或Go)。 记住,“计算机科学”中有“计算机”这个词。了解你的计算机执行一条指令、从内存中取出一个字(有或没有缓存未命中)、从磁盘连续读取字、以及在磁盘上寻找新位置所需的时间。(答案在这里) 参与一项语言标准化工作。可以是ANSI C++委员会,也可以是决定你们本地编码风格是否采用2个或4个空格缩进。无论如何,你都可以了解其他人对语言的喜好,他们对此有多深的感情,甚至可能了解他们为什么有这样的感情。 明智地尽快退出语言标准化工作。 考虑到这些,仅仅通过书本学习能走多远还是一个疑问。在我第一个孩子出生之前,我读了所有的育儿书,但仍然感觉自己像个毫无经验的新手。30个月后,当我第二个孩子即将出生时,我是否回到书本上进行复习?没有。相反,我依赖于我的亲身经验,这比专家写的数千页书籍对我来说更有用、更令人放心。 Fred Brooks在他的文章《No Silver Bullets》(没有银弹)中,为找到优秀的软件设计师提出了一个三部分的计划: 尽早系统地识别顶级设计师。 指派一位职业导师负责潜在人才的发展,并仔细维护其职业档案。 为成长中的设计师提供互动和激励彼此的机会。 这意味着有些人已经具备成为优秀设计师的必要品质;任务是适当地引导他们。Alan Perlis更简洁地说:“每个人都可以被教会雕塑:但米开朗基罗则需要被教如何不这样做。对于伟大的程序员也是如此。”Perlis的意思是,伟大的人有一种超越他们培训的内在品质。但这种品质从哪里来?是与生俱来的吗?还是他们通过勤奋发展出来的?正如《美食总动员》中的虚构厨师Auguste Gusteau所说:“人人都可以做饭,但只有无畏的人才能成为伟大的厨师。”我更认为这是愿意为刻意练习投入生活的大部分时间。但也许“无畏”是一种总结这种状态的方式。或者,正如Gusteau的评论家Anton Ego所说:“并不是每个人都可以成为伟大的艺术家,但伟大的艺术家可以来自任何地方。” 所以,放心去买那本Java/Ruby/Javascript/PHP的书;你可能会从中受益。但你的生活或你作为程序员的真正专业技能不会在24小时或21天内发生改变。如何努力在24个月内不断进步呢?好吧,现在你开始了。","tags":["编程建议"],"categories":["编程","编程建议"]}]