├── LICENSE ├── README.md ├── c++ ├── C++11 新特性.md ├── C++八股.md ├── C++学习方法与学习路线.md ├── C++总结.md ├── C++标准与历史 │ ├── C++ 知识图谱.png │ ├── C++的发展历史、简介及应用.md │ └── c++发展史 │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ └── C++发展史.md ├── STL │ ├── STL copy.md │ ├── STL.md │ └── 哈希容器 │ │ ├── unordered_map哈希映射.md │ │ ├── unordered_multimap哈希多重集合.md │ │ ├── unordered_multiset多重哈希映射.md │ │ ├── unordered_set哈希映射.md │ │ └── 哈希容器.md ├── images │ ├── C++内存分区.png │ ├── Hash表的时间复杂度为什么是O(1).png │ ├── const和static.png │ ├── future函数.png │ ├── new_delete对比malloc_free.png │ ├── vector的底层原理.png │ ├── 什么不能被继承.png │ ├── 动态链接.jpeg │ ├── 单继承时的虚函数(重写基类虚函数).png │ ├── 单继承时的虚函数.png │ ├── 原始基类的虚函数表.png │ ├── 多重继承.png │ ├── 多重继承时的虚函数.png │ ├── 必须使用友元的情况.png │ ├── 移动语义1.jpg │ ├── 移动语义2.jpg │ ├── 类对象的内存结构.png │ ├── 继承权限.png │ ├── 菱形继承.png │ ├── 虚函数的执行过程.png │ ├── 虚基类指针数组.png │ ├── 虚继承在C++标准库中的实际应用.png │ ├── 进程的内存空间分布.png │ ├── 静态对象初始化.png │ └── 静态链接.png ├── 关键字与限定符 │ ├── #define.md │ ├── =default.md │ ├── =delete.md │ ├── const.md │ ├── delete.md │ ├── explicit.md │ ├── extern.md │ ├── final.md │ ├── free.md │ ├── include.md │ ├── malloc.md │ ├── new.md │ ├── noexcept.md │ ├── override.md │ ├── realloc.md │ ├── static.md │ ├── typedef.md │ ├── using.md │ └── volatile.md ├── 基本语法 │ ├── 位运算.md │ └── 函数.md ├── 数据类型 │ ├── auto 类型推导.md │ ├── decltype 类型推导.md │ ├── images │ │ └── 类C的内存分布和虚表.png │ ├── initializer_list.md │ ├── lambda 表达式.md │ ├── 函数对象-仿函数.md │ ├── 函数指针.md │ ├── 列表初始化.md │ ├── 引用.md │ ├── 强制类型转换.md │ ├── 指针.md │ ├── 指针和引用.md │ ├── 数据类型.md │ ├── 枚举.md │ └── 结构体.md ├── 最佳实践与编码规范 │ ├── 代码风格指南 │ │ ├── Google │ │ │ ├── Google.md │ │ │ ├── classes.md │ │ │ ├── comments.md │ │ │ ├── contents.md │ │ │ ├── end.md │ │ │ ├── exceptions.md │ │ │ ├── formatting.md │ │ │ ├── functions.md │ │ │ ├── headers.md │ │ │ ├── images │ │ │ │ └── 一张图总结 Google C++编程规范.png │ │ │ ├── index.md │ │ │ ├── magic.md │ │ │ ├── naming.md │ │ │ ├── others.md │ │ │ └── scoping.md │ │ └── 代码风格指南.md │ ├── 命名规则 │ │ ├── images │ │ │ └── 命名-刷新7剑客.png │ │ ├── 命名原则.md │ │ ├── 希腊字母.md │ │ └── 文件夹和文件命名参考.md │ └── 注释规范 │ │ └── 注释.md ├── 模板与泛型编程 │ ├── addressof.md │ ├── 模板.md │ └── 泛型编程.md ├── 编译内存相关 │ ├── C++ 程序编译过程.md │ ├── C++内存模型.md │ ├── images │ │ ├── C++的内存分布模型.png │ │ ├── Linux下的C++内存模型.png │ │ ├── shared_ptr 析构并释放共享资源weak_ptr的变化.png │ │ ├── shared_ptr例子.png │ │ ├── weak_ptr对象内存结构.png │ │ ├── 创建shared_ptr.png │ │ ├── 在linux下size命令可以查看一个可执行二进制文件基本情况.png │ │ ├── 复制shared_ptr.png │ │ ├── 栈帧.png │ │ ├── 编译过程.png │ │ ├── 虚拟内存与物理内存的联系.png │ │ └── 隐藏.png │ ├── 内存对齐.md │ ├── 内存泄露.md │ ├── 函数调用.md │ ├── 变量.md │ ├── 智能指针.md │ ├── 深拷贝与浅拷贝.md │ ├── 生命周期和作用域.md │ └── 虚拟内存.md ├── 语言对比 │ ├── C 和 C++ 的区别.md │ └── Python 和 C++ 的区别.md ├── 语言特性相关 │ ├── images │ │ └── 类列表初始化.png │ ├── move函数与移动语义.md │ ├── 名称空间.md │ ├── 完美转发.md │ ├── 左值和右值.md │ ├── 正则表达式.md │ └── 范围 for 语句.md └── 面向对象 │ ├── MyClass.cpp │ ├── friend友元.md │ ├── this指针.md │ ├── 匿名对象.md │ ├── 委派构造函数.md │ ├── 嵌套类-内部类.md │ ├── 类与对象.md │ ├── 虚函数与纯虚函数.md │ ├── 虚表与虚表指针,虚基类表与虚基类表指针.md │ ├── 重载、重写、隐藏.md │ └── 面向对象.md └── tools ├── cmake ├── bibilicmake教程 │ └── 001工欲善其事必先利其器.md ├── cmake demo.rar ├── 但丁总结 │ ├── cmake.md │ ├── image │ │ ├── 01cmake编译流程.png │ │ └── cmake │ │ │ └── 1683940342147.png │ └── 单文件项目 │ │ └── CMakeLists.txt └── 简单cmake教程 │ ├── 01cmake交互式配置界面.webp │ ├── CMakeLists.txt │ ├── cmake.md │ ├── cmake教程.md │ ├── cmake示例.md │ └── 编译项目.png └── git └── git教程.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 淡定但丁 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CppNotes 2 | 3 | C++笔记总结 C++八股文 (一文在手,天下我有) 4 | 5 | ## 目录 6 | 7 | ### c++ 8 | 9 | [C++总结](./c++/C++总结.md) 10 | 11 | [c++八股](./c++/C++八股.md) 12 | 13 | [c++新特性](./c++/C++11%20新特性.md) 14 | 15 | [C++学习方法与学习路线](./c++/C++学习方法与学习路线.md) 16 | 17 | ### 其他工具 18 | 19 | [git](./tools/git/git教程.md) 20 | 21 | ## 版权声明 22 | 23 | 本文由[淡定但丁](https://github.com/newcleanbird)编辑,有任何问题欢迎访问github提交PR。 24 | -------------------------------------------------------------------------------- /c++/C++11 新特性.md: -------------------------------------------------------------------------------- 1 | # C++11新特性 2 | 3 | 在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称;不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯的把两个标准合称为C++98/03标准;从C++0x到C++11,C++标准十年磨一剑,第二个真正意义上的标准姗姗来迟; 相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言;相比较而言,C++11能更好地用于系统开放和库开发,语法更加泛化和简单化,更加稳定和安全,不仅功能强大,而且能提升程序员的开发效率; 4 | 5 | ## C++11 新特性 6 | 7 | 语句: 8 | [范围 for 语句](./语言特性相关/范围%20for%20语句.md) 9 | [using](./关键字与限定符/using.md) 10 | 11 | 类型推导: 12 | [auto 20类型推导](./数据类型/auto%20类型推导.md) 13 | [decltype 20类型推导](./数据类型/decltype%20类型推导.md) 14 | 15 | 重载和重写: 16 | [final](./关键字与限定符/final.md) 17 | [override](./关键字与限定符/override.md) 18 | 19 | 默认函数控制: 20 | [=delete](./关键字与限定符/=delete.md) 21 | [=default](./关键字与限定符/=default.md) 22 | 23 | 类与对象: 24 | [委派构造函数](./面向对象/委派构造函数.md) 25 | 26 | 移动语义与完美转发: 27 | [move函数与移动语义](./语言特性相关/move函数与移动语义.md) 28 | [完美转发](./语言特性相关/完美转发.md) 29 | 30 | 模板和泛型编程: 31 | [lambda 表达式](./数据类型/lambda%20表达式.md) 32 | [initializer_list](./数据类型/initializer_list.md) 33 | [函数对象-仿函数](./数据类型/函数对象-仿函数.md) 34 | -------------------------------------------------------------------------------- /c++/C++学习方法与学习路线.md: -------------------------------------------------------------------------------- 1 | # 浅谈C++学习方法与学习路线 2 | 3 | ## 学习路线 4 | 5 | 对于初学者,从零开始学习C++,建议遵循以下逐步深入的学习路线: 6 | 7 | 1. 基础语法 8 | - 开始前,安装一个合适的IDE,如Visual Studio, CLion或Code::Blocks,确保 9 | 环境搭建正确。 10 | - 学习变量、数据类型、运算符、控制结构(if语句、循环)、函数等基本概 11 | 念。 12 | - 动手实践,通过编写简单的程序,如计算器、温度转换器等,巩固基础语法。 13 | 2. 面向对象编程(OOP) 14 | - 理解类与对象的概念,学习封装、继承和多态三大特性。 15 | - 编写一些小型的面向对象程序,如学生信息系统,加深对OOP的理解。 16 | - 学习异常处理、命名空间、模板等进阶特性。 17 | 3. 标准模板库(STL) 18 | - 掌握容器(如vector, list, map)和算法(如sort, find)的使用。 19 | - 通过实际项目,如数据排序、搜索算法等,熟练运用STL。 20 | 4. 数据结构与算法 21 | - 学习常见数据结构,如数组、链表、栈、队列、树、图等。 22 | - 实现和优化经典算法,如排序、查找、递归等。 23 | 5. 进阶主题 24 | - 学习智能指针、RAII资源管理、模板元编程等高级主题。 25 | - 探索C++11及以后版本的新特性,如lambda表达式、右值引用等。 26 | 6. 实战项目 27 | - 参与或发起一个C++项目,如游戏开发、Web服务器、桌面应用等。 28 | - 推荐先写一个线程池练练手。 29 | - 通过实践,将理论知识转化为实际技能。 30 | 31 | ## 学习方法论 32 | 33 | 1. 持之以恒 34 | - 设定每日/每周学习计划,坚持每天至少投入一定时间学习。 35 | - 利用碎片时间,如通勤路上听C++相关的播客或观看视频教程。 36 | 2. 带着问题学习 37 | - 遇到不懂的问题,先尝试自己解决,然后查阅资料或向社区求助。(有请群友 38 | 老哥闪亮登场) 39 | - 将问题及其解决方案记录下来,建立个人知识库。(善用github) 40 | 3. 实践为主 41 | - 编程是一项实践性很强的技能,多写代码,多调试,才能真正掌握。 42 | - 尝试重构旧代码,理解不同解决方案的优缺点。 43 | 4. 参与社区 44 | - 加入C++相关的论坛、社群,参与讨论,获取最新资讯。(加入本 45 | 群:820878690,你已经遥遥领先!!!) 46 | - 为他人答疑和解决他人问题是回顾知识,加深理解的极好方法。不要认为帮助他人是浪费时间,实际上这即是帮助他人的善举,也是与他人交流提高自己水平的优秀途径。 47 | - 阅读其他人的代码,理解不同的编程风格和思路。 48 | 5. 定期复习 49 | - 定期回顾已学知识点,避免遗忘。 50 | - 通过编写笔记、制作思维导图等方式,加深记忆。 51 | 6. 阅读优秀源码 52 | - 阅读开源项目代码,理解实际开发中的最佳实践。 53 | - 推荐阅读STL源码,但需要一定的C++基础。 54 | - 尝试理解一些经典的C++项目,如Boost库、Qt框架等。 55 | 56 | ## 书籍阅读推荐 57 | 58 | ### C入门到进阶四件套 59 | 60 | - C primer plus 四件套: 61 | - 《C Primer Plus(第六版)》: 62 | - 特点:细节讲解清楚,练习题丰富,适合初学者建立信心,并逐步加深对C语言的理解。 63 | - 适用读者:对初学者非常友好,特别是希望能够通过实践加深理解的读者。 64 | - 《C和指针》: 65 | - 特点:深度和高度比国内教材更高,尤其适合有一定基础的读者深入理解C语言中的指针和数组等概念 66 | - 适用读者:有C语言基础,希望提高理解水平的读者。(建议四件套按顺序阅读) 67 | - 《C专家编程》: 68 | - 特点:这本书提供了许多C语言的高级技巧和专业知识,涵盖了从编译器行为到内存泄漏等多个方面的内容。书中的问题、例子和解决方案对于希望深入了解C语言的读者来说非常有价值。 69 | - 适用读者:适合已经掌握C语言基础知识并希望进一步提升编程技能的中高级程序员。 70 | - 《C陷阱与缺陷》 71 | - 特点:该书以问答形式讨论了C程序设计中的常见错误及编程陷阱,并介绍了一些常用C库函数的隐含错误。书中提供了大量的示例代码,帮助读者识别和避免这些常见的错误和陷阱。 72 | - 适用读者:此书适合那些已经能够编写基本的C程序,但希望提高代码质量和避免常见错误的读者。 73 | 74 | ### C++入门级 75 | 76 | - 《C++ Primer Plus》(C++ Primer Plus) by Stephen Prata 77 | - 这本书对零基础的读者非常友好,它详细地介绍了C++的基础知识,提供了大量的实例和练习,帮助读者循序渐进地学习。 78 | - 适合在读完C primer plus四件套后搭配学习,体会C和C++的区别与联系。 79 | - 《C++ Primer》 by Lippman, Lajoie, and Moo 80 | - 虽然这本书的深度可能更适合有一定编程经验的读者,但它仍然是C++学习者的重要资源,因为它覆盖了C++的各个方面,包括标准库。 81 | - 仅凭名字感觉本书要比《C++ Primer Plus》要低一个level,但其实本书阅读难度和知识点均多于《C++ Primer Plus》,非常适合在先学完《C++ Primer Plus》后学习,温故而知新,你的C++基础必然会再上一层楼。 82 | - 《A Tour of C++》 by Bjarne Stroustrup 83 | - C++的创造者Bjarne Stroustrup所著,这本书适合那些已经学过C++或者很久没接触C++,需要快速回顾的读者。 84 | - 《Accelerated C++》 by Andrew Koenig and Barbara Moo 85 | - 这本书以一种快速的方式介绍C++,适合希望迅速上手并开始编写代码的读者 86 | - 《Essential C++》 by Stanley B. Lippman 87 | - 对于有其他编程语言经验的读者,这本书提供了一个快速学习C++的途径,深入介绍了C++的核心概念。 88 | 89 | ### C++进阶级 90 | 91 | Effective三部曲: 92 | 93 | - 《Effective C++》 by Scott Meyers 94 | - 这本书包含了55条规则,指导你如何写出高效、健壮的C++代码。它强调了C++的最佳实践和陷阱避免。 95 | - 最初出版于1995年,主要围绕C++98标准。因此不少条款提到的问题已经在C++更新中得以解决或有更好的解决办法,但依然是非常值得一学的经典进阶教材。 96 | - 《More Effective C++》 by Scott Meyers 97 | - 进一步深化了C++编程的主题,提供了35项额外的规则和建议,涵盖了一些更高级的话题,如继承、多态、模板和异常处理等。 98 | - 作为《Effective C++》的后续,出版于1996年,面向C++98标准。 99 | - 《Effective Modern C++》 by Scott Meyers 100 | - 这本书专注于现代C++,即C++11和C++14带来的新特性,如自动类型推导、移动语义、Lambda表达式、右值引用等。它提供了90项规则和建议,指导读者如何有效地利用这些新特性,以编写更高效、更简洁的代码。 101 | - 首次出版于2014年,紧跟C++11和C++14标准。 102 | 103 | 其他进阶书: 104 | 105 | - 《深度探索C++对象模型》 106 | - 特点:详细讨论了C++对象模型的细节,包括内存结构、构造函数、析构函数等,适合那些希望理解C++底层原理的高级程序员。 107 | - 适用读者:熟练掌握C++并希望了解其内部机制的高级开发者。 108 | - 《C++标准程序库》 by Nicolai M. Josuttis 109 | - 这本书深入探讨了C++标准库,包括容器、迭代器、算法和泛型编程,是每个C++程序员的必备参考书。 110 | - 《Exceptional C++》和《More Exceptional C++》 by Herb Sutter 111 | - 这两本书专注于C++异常处理的最佳实践和深入讨论,以及如何避免常见错误。 112 | - 《Modern C++ Design》 by Andrei Alexandrescu 113 | - 探索了C++的现代设计模式和泛型编程技术,适合希望深入理解现代C++编程的读者。 114 | - 《C++ Templates: The Complete Guide》 by David Vandevoorde, Nicolai M. Josuttis, and Douglas Gregor 115 | - 深入介绍模板元编程,这是C++的一个强大特性,允许在编译时生成代码。 116 | - 《The C++ Programming Language》 by Bjarne Stroustrup 117 | - C++的创造者Bjarne Stroustrup的权威著作,涵盖了语言的各个方面,包括最新的C++标准。 118 | 119 | ### 推荐入门阅读顺序 120 | 121 | - 时间充裕,打牢基础: 122 | 1. 建议先学C语言,优先读《C Primer Plus(第六版)》,如果学有余力或者对C比较感兴趣,建议C primer plus 四件套都读。 123 | 2. 再学《C++ primer plus》,作为与上述书籍相似名的C++版本,表述和知识结构一脉相承,非常值得作为C++第一本书籍学习。 124 | 3. 再学《C++ Primer》,很像《C++ primer plus》的知识点重构与拓展版。但需注意,本书的部分表达和C++ primer并不相同,需要同时知道两者翻译。 125 | 4. 《Effective C++》 和 《More Effective C++》以及《Effective Modern C++》,了解C++的最佳实践,体会C++不同版本的区别。 126 | 5. 《深度探索C++对象模型》:有一定阅读难度,但其所表述的关于C++对象模型和虚函数、虚函数表等内容是大厂面试最喜欢问的问题。 127 | 128 | - 时间不充裕,快速上手,准备招聘: 129 | 1. 啃完一本《C++ primer plus》,再辅以总结好的八股足矣。 130 | 131 | ## 总结 132 | 133 | C++是一个难学易用的语言! 134 | 135 | C++的难学,不仅在其广博的语法,以及语法背后的语义,以及语义背后的深 层思维, 136 | 以及深层思维背后的对象模型; C++ 的难学还在千它提供了四种不同而又 相辅相成的 137 | 编程范型 (programming paradigms) : procedural-based(过程化编程): ), object-based(基于对象编程), object-oriented(面向对象编程,OOP), generics(泛型编程)。 138 | 139 | 世上没有白吃的午餐!又要有效率,又要有弹性,又要前瞻塑远,又要回溯相 容,又要 140 | 治大国,又要烹小鲜,学习起来当然就不可能太简单。在庞大复杂的机制 下,万千使用 141 | 者前仆后继的动力是:一旦学成,妙用无穷。 142 | 143 | C++自1979年诞生于AT&T贝尔实验室,距今已有40余年,历经诸多版本的更新,加入 144 | 了许多复杂的特性。因此不同的C++版本,不同的开发环境与设计模式下,最佳实践并 145 | 不相同,需要大量的编程经验。并且只学习C++本身你能做的事情非常有限,你还需要 146 | 关注特定领域,学习使用相关开源库去解决具体的问题。 147 | 148 | 记住,编程学习是一个长期的过程,保持耐心和毅力,你将逐渐掌握这门强大的编程语 149 | 言 150 | -------------------------------------------------------------------------------- /c++/C++总结.md: -------------------------------------------------------------------------------- 1 | # C++八股 2 | 3 | ## 数据类型 4 | 5 | [数据类型](./数据类型/数据类型.md) 6 | 7 | 类型: 8 | [枚举](./数据类型/枚举.md) 9 | [指针](./数据类型/指针.md) 10 | [指针和引用](./数据类型/指针和引用.md) 11 | [函数指针](./数据类型/函数指针.md) 12 | [函数对象/仿函数](./数据类型/函数对象-仿函数.md) 13 | [lambda 表达式](./数据类型/lambda%20表达式.md) 14 | [initializer_list](./数据类型/initializer_list.md) 15 | 16 | 初始化: 17 | [列表初始化](./数据类型/列表初始化.md) 18 | 19 | 类型推导: 20 | [auto 类型推导](./数据类型/auto%20类型推导.md) 21 | [decltype 类型推导](./数据类型/decltype%20类型推导.md) 22 | 23 | 强制类型转换: 24 | [强制类型转换](./数据类型/强制类型转换.md) 25 | 26 | ## 基本语法 27 | 28 | [函数](./基本语法/函数.md) 29 | [位运算](./基本语法/位运算.md) 30 | 31 | ## 编译内存相关 32 | 33 | [程序编译过程](./编译内存相关/C++%20程序编译过程.md) 34 | [C++内存模型](./编译内存相关/C++内存模型.md) 35 | [变量](./编译内存相关/变量.md) 36 | [生命周期和作用域](./编译内存相关/生命周期和作用域.md) 37 | [内存对齐](./编译内存相关/内存对齐.md) 38 | [内存泄露](./编译内存相关/内存泄露.md) 39 | [智能指针](./编译内存相关/智能指针.md) 40 | [深拷贝与浅拷贝](./编译内存相关/深拷贝与浅拷贝.md) 41 | [虚拟内存](./编译内存相关/虚拟内存.md) 42 | [函数调用](./编译内存相关/函数调用.md) 43 | 44 | ## 语言对比 45 | 46 | [C 和 C++ 的区别](./语言对比/C%20和%20C++%20的区别.md) 47 | [Python 和 C++ 的区别](./语言对比/Python%20和%20C++%20的区别.md) 48 | 49 | ## 面向对象 50 | 51 | [面向对象](./面向对象/面向对象.md) 52 | [重载、重写、隐藏](./面向对象/重载、重写、隐藏.md) 53 | [类与对象](./面向对象/类与对象.md) 54 | [this指针](./面向对象/this指针.md) 55 | [虚函数与纯虚函数](./面向对象/虚函数与纯虚函数.md) 56 | [虚表与虚表指针,虚基类表与虚基类表指针](./面向对象/虚表与虚表指针,虚基类表与虚基类表指针.md) 57 | [匿名对象](./面向对象/匿名对象.md) 58 | [委派构造函数](./面向对象/委派构造函数.md) 59 | 60 | ## 关键字与限定符 61 | 62 | 变量修饰符: 63 | [const](./关键字与限定符/const.md) 64 | [static](./关键字与限定符/static.md) 65 | [extern](./关键字与限定符/extern.md) 66 | [volatile](./关键字与限定符/volatile.md) 67 | 68 | 函数修饰符: 69 | [explicit](./关键字与限定符/explicit.md) 70 | [noexcept](./关键字与限定符/noexcept.md) 71 | [final](./关键字与限定符/final.md) 72 | [override](./关键字与限定符/override.md) 73 | [=delete](./关键字与限定符/=delete.md) 74 | [=default](./关键字与限定符/=default.md) 75 | 76 | 内存分配关键字: 77 | [new](./关键字与限定符/new.md) 78 | [delete](./关键字与限定符/delete.md) 79 | [malloc](./关键字与限定符/malloc.md) 80 | [realloc](./关键字与限定符/realloc.md) 81 | [free](./关键字与限定符/free.md) 82 | 83 | 预编译命令: 84 | [include](./关键字与限定符/include.md) 85 | 86 | 定义类型别名或符号替换: 87 | [typedef](./关键字与限定符/typedef.md) 88 | [#define.md](./关键字与限定符/#define.md) 89 | [using](./关键字与限定符/using.md) 90 | 91 | ## 语言特性相关 92 | 93 | 语句: 94 | [范围 for 语句](./语言特性相关/范围%20for%20语句.md) 95 | 96 | 左值右值与移动语义: 97 | [左值右值](./语言特性相关/左值和右值.md) 98 | [std::move()函数与移动语义](./语言特性相关/move函数与移动语义.md) 99 | [完美转发](./语言特性相关/完美转发.md) 100 | 101 | 常用工具: 102 | [正则表达式](./语言特性相关/正则表达式.md) 103 | 104 | ## 模板与泛型编程 105 | 106 | [模板](./模板与泛型编程/模板.md) 107 | [泛型编程](./模板与泛型编程/泛型编程.md) 108 | [addressof](./模板与泛型编程/addressof.md) 109 | 110 | ## STL 111 | 112 | [STL](./STL/STL.md) 113 | 114 | ## 最佳实践与编码规范 115 | 116 | [代码风格指南](./最佳实践与编码规范/代码风格指南/代码风格指南.md): 117 | 118 | - [Google](./最佳实践与编码规范/代码风格指南/Google/Google.md) 119 | 120 | [命名规则](./最佳实践与编码规范/命名规则/命名原则.md) 121 | 122 | ## C++标准与历史 123 | 124 | [C++的发展历史、简介及应用](./C++标准与历史/C++的发展历史、简介及应用.md) 125 | -------------------------------------------------------------------------------- /c++/C++标准与历史/C++ 知识图谱.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/C++标准与历史/C++ 知识图谱.png -------------------------------------------------------------------------------- /c++/C++标准与历史/C++的发展历史、简介及应用.md: -------------------------------------------------------------------------------- 1 | # C++的发展历史、简介及应用 2 | 3 | ## 一、C++的来历及诞生背景 4 | 5 | * C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。 6 | 7 | * C++的发展史可以追溯到1979年,当时Bjarne Stroustrup在贝尔实验室工作时开始设计一种名为“C with Classes”的语言。这种语言是在C语言的基础上加入了面向对象编程的特性,以便更好地应对复杂的软件开发需求。 8 | 9 | * 1983年,Stroustrup正式将这种语言命名为C++,并在同年发表了C++的第一篇论文。此后,C++经历了多个版本的更新和改进。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。 10 | 11 | * C++是一种编程语言,它是在C语言的基础上发展而来的。 12 | 13 | * C++的起源:C++最初被称为“带类的C”,是在C语言的基础上添加了面向对象编程的特性。C++的名称中的“++”是从C语言中的“++”运算符中取得的,表示C++是C语言的一个增强版本。 14 | 15 | * C++的发展:C++在1983年正式命名为C++,并在之后的几十年中不断发展壮大。C++通过引入虚函数、函数重载、引用、const关键字以及//注释符号等功能,使得程序员能够更加方便地进行面向对象编程和模块化开发。 16 | 17 | * C++与C的比较:C++与C语言有很多相似之处,但也有一些重要的区别。C++相比C语言更加强大和灵活,它支持面向对象编程、模板元编程和异常处理等高级特性。C++还提供了更多的标准库和工具,使得开发更加高效和便捷。 18 | 19 | ## 二、C++的每个版本时间 20 | 1. C with Classes:类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符 重载等 21 | 2. C++1.0:添加虚函数概念,函数和运算符重载,引用、常量等 22 | 3. C++2.0:更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 23 | 4. C++3.0:进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 24 | 5. C++98:C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,引入了许多重要的特性,如命名空间、模板、异常处理等。以模板方式重写C++标准库,引入了STL(标准模板库)。 25 | 6. C++03:C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 26 | 7. C++05:C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名 C++0x,即:计划在本世纪第一个10年的某个时间发布 27 | 8. C++11标准:C++11标准于2011年发布,也被称为C++0x标准。这个标准引入了许多新特性,如自动类型推导、Lambda表达式、右值引用、智能指针等,使得C++语言更加现代化和强大。 28 | 9. C++14标准:C++14标准于2014年发布。这个标准主要是对C++11标准的一些修订和改进,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表 达式,auto的返回值类型推导,二进制字面常量等。 29 | 10. C++17标准:C++17标准于2017年发布。增加了19个新特性,如结构化绑定、折叠表达式、并行算法、static_assert()的文本信息可选、Fold表达式用于可变的模板、if和switch语句中的初始化器等,进一步提升了C++的功能和性能。 30 | 11. C++20标准:C++20标准于2020年发布。这个标准被认为是自C++11以来最大的发行版,引入了许多新特性,如模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)、范围for循环等重大特性概念,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等,进一步简化了代码编写和维护。 31 | 12. C++23标准:C++23相较于C++20是渐进式的小修小补,继续推动了模块化编程的进程;通过引入如if consteval等特性,加强了编译时检查;:C++23在标准库方面做了不少工作,比如对std::span、std::string_view等的改进,以及对并发和线程库的增强,这些都使得标准库更加丰富和强大,更好地支持现代编程实践。 32 | 33 | ## 三、C++的详细应用领域 34 | 35 | * 游戏开发:C++在游戏开发中非常常见,因为它提供了高性能和底层控制的能力。例如,著名的游戏引擎Unity就是使用C++开发的。 36 | 37 | * 嵌入式系统:C++在嵌入式系统开发中也非常流行,因为它可以提供高效的代码和对硬件的底层访问。例如,汽车控制系统、智能家居设备等都可以使用C++进行开发。 38 | 39 | * 图形图像处理:C++在图形图像处理领域也有广泛的应用。例如,OpenCV是一个流行的计算机视觉库,它使用C++编写,提供了各种图像处理和计算机视觉算法。 40 | 41 | * 金融领域:C++在金融领域中也被广泛使用,因为它可以提供高性能和低延迟的计算能力。例如,高频交易系统和金融风险管理系统通常使用C++进行开发。 42 | 43 | * 操作系统开发:C++在操作系统开发中也扮演着重要的角色。例如,Linux操作系统的内核就是使用C++编写的。 44 | 45 | * 数据库系统:C++在数据库系统的开发中也有应用。例如,MySQL数据库管理系统的核心部分就是使用C++编写的。 46 | 47 | * 网络编程:C++在网络编程中也非常常见,因为它可以提供高性能和底层网络访问的能力。例如,Web服务器和网络游戏服务器通常使用C++进行开发。 -------------------------------------------------------------------------------- /c++/C++标准与历史/c++发展史/01.md: -------------------------------------------------------------------------------- 1 | # 1. 前言 2 | 3 | 最初,我设计 C++ 是为了回答这样的一个问题:如何直接操作硬件,同时又支持高效、高级的抽象?C++ 在 1980 年代仅仅是一个基于 C 和 Simula 语言功能的组合,在当时的计算机上作为系统编程的相对简单的解决方案,经过多年的发展,已经成长为一个远比当年更复杂和有效的工具,应用极其广泛。它保持了如下两方面的关注: 4 | 5 | - **语言构件到硬件设施的直接映射** 6 | - **零开销抽象** 7 | 8 | 这种组合是 C++ 区别于大多数语言的决定性特征。“零开销”是这样解释的 [Stroustrup 1994]: 9 | 10 | - 你不用的东西,你就不需要付出代价(“没有四处散落的赘肉”)。 11 | - 你使用的东西,你手工写代码也不会更好。 12 | 13 | 抽象在代码中体现为函数、类、模板、概念和别名。 14 | 15 | C++ 是一种活的语言,因此它会不断变化以应对新出现的挑战和演变中的使用风格。2006 年至 2020 年期间的这些挑战和变化是本文的重点。当然,一门语言本身不会改变;是人们改变了它。所以这也是参与 C++ 演化的人们的故事,他们识别出面临的挑战,诠释解决方案的局限,组织他们的工作成果,并解决他们之间必然出现的分歧。当我呈现一种语言或标准库特性时,其背景是 C++ 的一般发展和当时参与者的关切。对于在早期被接受的许多特性,我们现在从大量的工业使用中获得了后见之明。 16 | 17 | C++ 主要是一种工业语言,一种构建系统的工具。对于用户来说,C++ 不仅仅是一种由规范定义的语言;它是由许多部分组成的工具集的一部分: 18 | 19 | - 语言 20 | - 标准库 21 | - 许多的其他库 22 | - 庞大的——常常是旧的——代码库 23 | - 工具(包括其他语言) 24 | - 教学和培训 25 | - 社区支持 26 | 27 | 只要有可能,只要合适,我就会考虑这些组成部分之间的相互作用。 28 | 29 | 有一种流传广泛的谬见,就是程序员希望他们的语言是简单的。当你不得不学习一门新的语言、不得不设计一门编程课程、或是在学术论文中描述一门语言时,追求简单显然是实情。对于这样的用途,让语言干净地体现一些明确的原则是一个明显的优势,也是理想情况。当开发人员的焦点从学习转移到交付和维护重要的应用程序时,他们的需求从简单转移到全面的支持、稳定性(兼容性)和熟悉度。人们总是混淆熟悉度和简单,如果可以选择的话,他们更倾向于熟悉度而不是简单。 30 | 31 | 看待 C++ 的一种方式是,把它看成几十年来三种相互矛盾的要求的结果: 32 | 33 | - **让语言更简单!** 34 | - **立即添加这两个必要特性!!** 35 | - **不要搞砸我的(任何)代码!!!** 36 | 37 | 我添加了感叹号,因为这些观点的表达常常带着不小的情绪。 38 | 39 | 我想让简单的事情简单做,并确保复杂的事情并非不可能,也不会没有必要地难。前者对于不是语言律师的开发者来说是必不可少的;后者对于基础性代码的实现者是必要的。稳定是所有意图持续运行几十年的系统的基本属性,然而一种活的语言必须适应不断变化的世界。 40 | 41 | C++ 有一些总体构想。我阐述了一些(如《C++ 语言的设计和演化》(*The Design and Evolution of C++*)[Stroustrup 1994]([§2](02.md#2-背景c-的-19792006))、设计原则([§9.1](09.md#91-设计原则)),以及 C++ 模型([§11.1](11.md#111-c-模型)))并试图让语言在演化时遵循它们。然而,C++ 的开发由 ISO 标准委员会控制,它主要关注的是长长的新特性列表,以及对实际细节的关心。这是社区里最能表达和最有影响力的人所坚持的东西,仅仅基于哲学或理论观点就否认他们的关切和意见的话,恐怕就失之鲁莽了。 42 | 43 | ## 1.1 年表 44 | 45 | 为了给出一个快速的概述,这里有一个粗略的年表。如果你不熟悉 C++,很多术语、构件、库都会晦涩难懂;大多数在以前的 HOPL 论文 [Stroustrup 1993, 2007] 或本文中有详细解释。 46 | 47 | - 1979 年:工作始于“带类的 C”,它变成了 C++;拥有了第一个非研究性的用户; 48 | - 语言:`class`、构造函数/析构函数、`public`/`private`、简单继承、函数参数类型检查 49 | - 库:`task`(协程和仿真支持)、用宏参数化的 `vector` 50 | - 1985 年:C++ 的首次商业发行;TC++PL1 [Stroustrup 1985b] 51 | - 语言:`virtual` 函数、运算符重载、引用、常量 52 | - 库:`complex` 算法,流输入输出 53 | - 1989–91 年:ANSI 和 ISO 标准化开始;TC++PL2 [Stroustrup 1991] 54 | - 语言:抽象类、多重继承、异常、模板 55 | - 库:输入输出流(但没有 `task`) 56 | - 1998 年:C++98、第一个 ISO C++ 标准 [Koenig1998]、TC++PL3 [Stroustrup 1997] 57 | - 语言:`namespace`、具名类型转换[^1]、`bool`、`dynamic_cast` 58 | - 库:STL(容器和算法)、`string`、`bitset` 59 | - 2011 年:C++11 [Becker 2011],TC++PL4 [Stroustrup 2013] 60 | - 语言:内存模型、`auto`、范围 `for`、`constexpr`、lambda 表达式、用户定义字面量…… 61 | - 库:`thread` 和锁、`future`、`unique_ptr`、`shared_ptr`、`array`、时间和时钟、随机数、无序容器(哈希表)…… 62 | - 2014 年:C++14 [du Toit 2014] 63 | - 语言:泛型 lambda 表达式、`constexpr` 函数中的局部变量、数字分隔符…… 64 | - 库:用户定义字面量…… 65 | - 2017 年:C++17 [Smith 2017] 66 | - 语言:结构化绑定、变量模板、模板参数的构造函数推导…… 67 | - 库:文件系统、`scoped_lock`、`shared_mutex`(读写锁)、`any`、`variant`、`optional`、`string_view`、并行算法…… 68 | - 2020 年:C++20 [Smith 2020] 69 | - 语言:`concept`、`module`、协程、三路比较、改进对编译期计算的支持…… 70 | - 库:概念、范围、日期和时区、`span`、格式、改进的并发和并行支持…… 71 | 72 | 请注意,早年 C++ 的库是很匮乏的。事实上,当时还是存在大量各种各样的库(包括图形用户界面库),但很少被广泛使用,并且很多库是专有软件。这是在开源开发普及之前的事。这造成了 C++ 社区没有一个重要的共享基础库。在我的 HOPL2 论文 [Stroustrup 1993] 的回顾中,我认为那是早期 C++ 最糟糕的错误。 73 | 74 | 任务库 [Stroustrup 1985a,c] 是一个基于协程的库,支持事件驱动的仿真(例如随机数生成),与替代方案相比是非常高效的,甚至可以运行在很小的计算机上。例如,我在 256KB 的内存中运行了 700 个任务的仿真。任务库在 C++ 早期非常重要,是贝尔实验室和其他地方许多重要应用的基础。然而,它有点丑陋,并且不容易移植到 Sun 的 SPARC 体系结构,因此大多数 1989 年以后的实现都不支持它。2020 年,协程才刚刚回归([§9.3.2](09.md#932-协程))。 75 | 76 | 总的来说,C++ 的特性不断增多。ISO 委员会也废除了一些特性,对语言进行了稍许清理,但是考虑到 C++ 的大量使用(数十亿行代码),重要的特性是永远不会被移除的。稳定性也是 C++ 的关键特性。要解决跟语言不断增长的规模和复杂性相关的问题,办法之一是通过编码指南([§10.6](10.md#106-编码指南))。 77 | 78 | ## 1.2 概述 79 | 80 | 这篇论文是按照 ISO 标准发布的大致时间顺序组织的。 81 | 82 | - [§1](#1-前言):前言 83 | - [§2](02.md#2-背景c-的-19792006):背景:C++ 的 1979–2006 84 | - [§3](03.md#3-c-标准委员会):C++ 标准委员会 85 | - [§4](04.md#4-c11感觉像是门新语言):C++11:感觉像是门新语言 86 | - [§5](05.md#5-c14完成-c11):C++14:完成 C++11 87 | - [§6](06.md#6-概念):概念 88 | - [§7](07.md#7-错误处理):错误处理 89 | - [§8](08.md#8-c17大海迷航):C++17:大海迷航 90 | - [§9](09.md#9-c20方向之争):C++20:方向之争 91 | - [§10](10.md#10-2020-年的-c):2020 年的 C++ 92 | - [§11](11.md#11-回顾):回顾 93 | 94 | 如果一个主题跨越了一段较长的时间,比如“概念”和标准化流程,我会把它放在一个地方,让内容优先于时间顺序。 95 | 96 | 这篇论文特别长,真是一篇专题论文了。但是从 2006 年到 2020 年,C++ 经历了两次主要修订:C++11 和 C++20;而论文的早期读者们也都要求获得更多的信息。结果就是论文的页数几乎翻倍。即使以目前的篇幅,读者也会发现某些重要的主题没有得到充分的展现,如并发和标准库。 97 | 98 | [^1]: 译注:即新的、非 C 风格的类型转换 99 | -------------------------------------------------------------------------------- /c++/C++标准与历史/c++发展史/05.md: -------------------------------------------------------------------------------- 1 | # 5. C++14:完成 C++11 2 | 3 | 依据大版本和小版本交替发布的计划,C++14 [du Toit 2014] 的目标是“完成 C++11”([§3.2](03.md#32-组织));也就是说,接受 2009 年特性冻结后的好的想法,纠正最初大规模使用 C++11 标准时发现的问题。对这个有限目标而言,C++14 是成功的。 4 | 5 | 重要的是,它表明 WG21 可以按时交付标准。反过来,这也使得实现者能够按时交付。在 2014 年年底之前,三个主要的 C++ 实现者(Clang、GCC 和微软)提供了大多数人认为完整的特性。尽管并没有完美地符合标准,但人们基本上可以对所有的特性和特性组合进行实验。要能编译“用到所有高级特性”的库,还需要延后一些时间(对微软而言要到 2018 年),但对于大多数用户而言,对标准的符合程度足以满足实际使用。标准工作和实现工作已经紧密联系在一起。这给社区带来了很大的不同。 6 | 7 | C++14 特性集可以概括为: 8 | 9 | - 二进制字面量,例如 `0b1001000011110011` 10 | - [§5.1](#51-数字分隔符):数字分隔符——为了可读性,例如 `0b1001'0000'1111'0011` 11 | - [§5.2](#52-变量模板):变量模板——参数化的常量和变量 12 | - [§5.3](#53-函数返回类型推导):函数返回类型推导 13 | - [§5.4](#54-泛型-lambda-表达式):泛型 lambda 表达式 14 | - [§5.5](#55-constexpr-函数中的局部变量):`constexpr` 函数中的局部变量 15 | - 移动捕获——例如 `[p = move(ptr)] {/* ... */};` 将值移入 lambda 表达式 16 | - 按类型访问元组,例如 `x = get(t);` 17 | - 标准库中的用户定义字面量,例如:`10i`,`"Hello"s`,`10s`,`3ms`,`55us`,`17ns` 18 | 19 | 这些特性中的大多数都面临着两个问题:“很好,什么使你花了这么长的时间?”以及“谁需要这个?”我的印象是,每个新特性都有着重要的需求作为动机——即使该需求不是通用的。在 `constexpr` 函数中添加局部变量和泛型 lambda 表达式大大改善了人们的代码。 20 | 21 | 重要的是,从 C++11 升级到 C++14 是相对无痛的,没有 ABI 破坏。经历过从 C++98 到 C++11 这一大而困难的升级的人感到了惊喜:他们升级可以比预想还快,花费的精力也更少。 22 | 23 | ## 5.1 数字分隔符 24 | 25 | 奇怪的是,数字分隔符引起了最激烈的争论。Lawrence Crowl 反复提出了各种选项的分析 [Crowl 2013]。包括我在内的许多人都主张使用下划线作为分隔符(和好几种其他语言一样)。例如: 26 | 27 | ```cpp 28 | auto a = 1_234_567; // 1234567 29 | ``` 30 | 31 | 不幸的是,人们正在使用下划线作为用户定义字面量后缀的一部分: 32 | 33 | ```cpp 34 | auto a = 1_234_567_s; // 1234567 秒 35 | ``` 36 | 37 | 这可能会引起歧义。例如,最后一个下划线是多余的分隔符还是后缀的开始?令我惊讶的是,这种潜在的歧义使下划线对很多人来说变得难以接受。其中一个原因是,为了免得程序员遇到意想不到的结果,库小组为标准库保留了不以下划线开头的后缀。经过长时间的讨论,包括全体委员会(约 100 人)的辩论,我们一致同意使用单引号: 38 | 39 | ```cpp 40 | auto a = 1'234'567; // 1234567(整数) 41 | auto b = 1'234'567s; // 1234567 秒 42 | ``` 43 | 44 | 尽管有严厉的警告指出使用单引号会破坏无数的工具,但实际效果似乎不错。单引号由 David Vandevoorde 提出 [Crowl et al. 2013]。他指出,在一些国家,特别是在瑞士的金融写法中,单引号被当作分隔符来使用。 45 | 46 | 我的另一个建议,使用空白字符,则一直没有得到认同: 47 | 48 | ```cpp 49 | int a = 1 234 567; // 1234567 50 | int b = 1 234 567 s; // 1234567 秒 51 | ``` 52 | 53 | 许多人认为这个建议是一个与在愚人节发表的老文章 [Stroustrup 1998] 有关的笑话。而实际上,它反映了一个旧规则,即相邻字符串会被连接在一起,因而 `"abc" "def"` 表示 `"abcdef"`。 54 | 55 | ## 5.2 变量模板 56 | 57 | 2012 年,Gabriel Dos Reis 提议扩展模板机制,在模板类、函数和别名 [Dos Reis 2012] 之外加入模板变量。例如: 58 | 59 | ```cpp 60 | template 61 | constexpr T pi = T(3.1415926535897932385); 62 | 63 | template 64 | T circular_area(T r) 65 | { 66 | return pi * r * r; 67 | } 68 | ``` 69 | 70 | 起初,我觉得这是一种平淡无奇的语言技术上的泛化,没有特别重要的意义。然而,为指定各种精度的常数而采取的变通办法由来已久,而且充斥着令人不安的变通和妥协。经过这种简单的语言泛化,代码可以大幅简化。特别是,变量模板作为定义概念的主要方式应运而生([§6.3.6](06.md#636-改进))。例如: 71 | 72 | ```cpp 73 | // 表达式: 74 | template 75 | concept SignedIntegral = Signed && Integral; 76 | ``` 77 | 78 | C++20 标准库提供了一组定义为变量模板的数学常数,最常见的情况是定义为 `constexpr` [Minkovsky and McFarlane 2019]。例如: 79 | 80 | ```cpp 81 | template constexpr T pi_v = unspecified; 82 | constexpr double pi = pi_v; 83 | ``` 84 | 85 | ## 5.3 函数返回类型推导 86 | 87 | C++11 引入了从 lambda 表达式的 return 语句来推导其返回类型的特性。C++14 将该特性扩展到了函数: 88 | 89 | ```cpp 90 | template 91 | auto size(const T& a) { return a.size(); } 92 | ``` 93 | 94 | 这种写法上的便利对于泛型代码中的小函数来说非常重要。但用户必须很小心,此类函数不能提供稳定的接口,因为它的类型现在取决于它的实现,而且在编译到使用这个函数的代码时,函数实现必须是可见的。 95 | 96 | ## 5.4 泛型 lambda 表达式 97 | 98 | lambda 表达式是函数对象([§4.3.1](04.md#431-lambda-表达式)),因此它们显然可以是模板。有关泛型(多态)lambda 表达式的问题在 C++11 的工作中已经进行了广泛讨论,但当时被认为还没有完全准备好([§4.3.1](04.md#431-lambda-表达式))。 99 | 100 | 2012 年,Faisal Vali、Herb Sutter 和 Dave Abrahams 提议了泛型 lambda 表达式 [Vali et al. 2012]。提议的写法只是从语法中省略了类型: 101 | 102 | ```cpp 103 | auto get_size = [](& m){ return m.size(); }; 104 | ``` 105 | 106 | 委员会中的许多人(包括我)都强烈反对,指出该语法太过特别,且不能推广到受约束的泛型 lambda 表达式中。因此,写法更改为使用 `auto` 作为标记,指明有类型需要推导: 107 | 108 | ```cpp 109 | auto get_size = [](auto& m){ return m.size(); }; 110 | ``` 111 | 112 | 这使泛型 lambda 表达式与早在 2002 年就提出的概念提案和泛型函数建议 [Stroustrup 2003; Stroustrup and Dos Reis 2003a,b] 保持一致。 113 | 114 | 这种将 lambda 表达式语法与语言其他部分所用的语法相结合的方向与一些人的努力背道而驰,这些人希望为泛型 lambda 表达式提供一种独特(超简洁)的语法,类似于其他语言 [Vali et al. 2012]: 115 | 116 | ``` 117 | C# 3.0 (2007): x => x * x; 118 | Java 1.8 (~2013): x -> x * x; 119 | D 2.0 (~2009): (x) { return x * x; }; 120 | ``` 121 | 122 | 我认为,使用 `auto` 而且没有为 lambda 表达式引入特殊的不与函数共享的写法是正确的。此外,我认为在 C++14 中引入泛型 lambda 表达式,而没有引入概念,则是个错误;这样一来,对受约束和不受约束的 lambda 表达式参数和函数参数的规则和写法就没有一起考虑。由此产生的语言技术上的不规则(最终)在 C++20 中得到了补救([§6.4](06.md#64-c20-概念))。但是,我们现在有一代程序员习惯于使用不受约束的泛型 lambda 表达式并为此感到自豪,而克服这一点将花费大量时间。 123 | 124 | 从这里简短的讨论来看,似乎委员会流程对写法/语法给予了特大号的重视。可能是这样,但是语法并非无足轻重。语法是程序员的用户界面,与语法有关的争论通常反映了语义上的分歧,或者反映了对某一特性的预期用途。写法应反映基础的语义,而语法通常偏向于对某种用法(而非其他用法)有利。例如,一个完全通用和啰嗦的写法有利于希望表达细微差别的专家,而一个为表达简单情况而优化的写法,则有利于新手和普通用户。我通常站在后者这边,并且常常赞成两者同时都提供([§4.2](04.md#42-c11简化使用))。 125 | 126 | ## 5.5 `constexpr` 函数中的局部变量 127 | 128 | 到 2012 年,人们不再害怕 `constexpr` 函数,并开始要求放松对其实现的限制。实际上有些人希望能够在 `constexpr` 函数中执行任何操作。但是,无论是使用者还是编译器实现者都还没有为此做好准备。 129 | 130 | 经过讨论,Richard Smith(谷歌)提出了一套相对适度的放松措施 [Smith 2013]。特别是,允许使用局部变量和 `for` 循环。例如: 131 | 132 | ```cpp 133 | constexpr int min(std::initializer_list xs) 134 | { 135 | int low = std::numeric_limits::max(); 136 | for (int x : xs) 137 | if (x < low) 138 | low = x; 139 | return low; 140 | } 141 | 142 | constexpr int m = min({1,3,2,4}); 143 | ``` 144 | 145 | 给定一个常量表达式作为参数,这个 `min()` 函数可以在编译时进行求值。本地的变量(此处为 `low` 和 `x`)仅在编译器中存在。计算不能对调用者的环境产生副作用。Gabriel Dos Reis 和 Bjarne Stroustrup 在原始的(学术)`constexpr` 论文中指出了这种可能性 [Dos Reis and Stroustrup 2010]。 146 | 147 | 这种放松简化了许多 `constexpr` 函数并使许多 C++ 程序员感到高兴。以前在编译时只能对算法的纯函数表达式进行求值,他们对此感到不满。特别是,他们希望使用循环来避免递归。就更长期来看,这释放出了要在 C++17 和 C++20([§9.3.3](09.md#933-编译期计算支持))中进一步放松限制的需求。为了说明潜在的编译期求值的能力,我已经指出 `constexpr thread` 也是可能的,尽管我并不急于对此进行提案。 148 | -------------------------------------------------------------------------------- /c++/C++标准与历史/c++发展史/C++发展史.md: -------------------------------------------------------------------------------- 1 | # 在纷繁多变的世界里茁壮成长:C++ 2006–2020 2 | 3 | 这是 C++ 之父 Bjarne Stroustrup 的 [HOPL4 论文](https://www.stroustrup.com/hopl20main-p5-p-bfc9cd4--final.pdf)的中文版。 4 | 5 | [HOPL](https://dl.acm.org/conference/hopl) 是 History of Programming Languages(编程语言历史)的缩写,是 [ACM](https://www.acm.org/)(Association of Computing Machines,国际计算机协会)旗下的一个会议,约每十五年举办一次。Bjarne 的这篇论文是他为 2021 年 [HOPL IV 会议](https://hopl4.sigplan.org/)准备的论文,也是他的第三篇 HOPL 论文。在这三篇前后间隔近三十年的论文里,Bjarne 记录了 C++ 的完整历史,从 1979 年到 2020 年。这篇 HOPL4 论文尤其重要,因为它涵盖了 C++98 之后的所有 C++ 版本,从 C++11 直到 C++20。如果你对更早期的历史也感兴趣的话,则可以参考[他的其他 HOPL 论文](https://www.stroustrup.com/papers.html),及他在 1994 年出版的《C++ 语言的设计和演化》(*The Design and Evolution of C++*)。 6 | 7 | 鉴于这篇论文对于 C++ 从业者的重要性,[全球 C++ 及系统软件技术大会](http://cpp-summit.org/)的主办方 [Boolan](http://boolan.com/) 组织了一群译者,把这篇重要论文翻译成了中文,让 C++ 开发人员对 C++ 的设计原则和历史有一个系统的了解。下面是论文的完整摘要: 8 | 9 | > 到 2006 年时,C++ 已经在业界广泛使用了 20 年。它既包含了自 1970 年代初引入 C 语言以来一直没有改变的部分,又包含了在二十一世纪初仍很新颖的特性。从 2006 年到 2020 年,C++ 开发者人数从约 300 万增长到了约 450 万。在这段时期里,有新的编程模型涌现出来,有硬件架构的演变,有新的应用领域变得至关重要,也有好些语言在争夺主导地位,背后有雄厚的资金支持和专业的营销。C++——一种没有真正商业支持的、老得多的语言——是如何在这些挑战面前继续茁壮成长的? 10 | > 11 | > 本文重点关注 ISO C++ 标准在 2011 年、2014 年、2017 年和 2020 年的修订版中的重大变化。标准库在篇幅上约占 C++20 标准的四分之三,但本文的主要重点仍是语言特性和它们所支持的编程技术。 12 | > 13 | > 本文包含了长长的特性清单,其中记录了 C++ 的成长。我会对重要的技术要点进行讨论,并用简短的代码片段加以说明。此外,本文还展示了一些失败的提案,以及导致其失败的讨论。它提供了一个视角,如何看待这些年来令人眼花缭乱的事实和特性。我的重点是塑造语言的想法、人和流程。 14 | > 15 | > 讨论主题包括各种方向上的努力,包括:通过演进式变化保留 C++ 的本质,简化 C++ 的使用,改善对泛型编程的支持,更好地支持编译期编程,扩展对并发和并行编程的支持,以及保持对几十年前的代码的稳定支持。 16 | > 17 | > ISO C++ 标准是通过一个共识流程演化而来的。无可避免,在方向、设计理念和原则方面,不同的提案间存在竞争和(通常是礼貌性的)冲突。委员会现在比以往任何时候都更大、更活跃,每年有多达 250 人参加三次为期一周的会议,还有更多的人以电子方式参加。我们试图(并不总是成功)减轻各种不良影响,包括“委员会设计”、官僚主义,以及对各种语言时尚的过度热衷。 18 | > 19 | > 具体的语言技术话题包括内存模型、并发并行、编译期计算、移动语义、异常、lambda 表达式和模块。要设计一种机制来指定模板对其参数的要求,既足够灵活和精确,又不会增加运行期开销,实践证明这很困难。设计“概念”来做到这一点的反复尝试可以追溯到 1980 年代,并触及到 C++ 和泛型编程的许多关键设计问题。 20 | > 21 | > 文中的描述基于个人对关键事件和设计决策的参与,并以 ISO C++ 标准委员会档案中的数千篇论文和数百份会议记录作为支持。 22 | 23 | 下面是论文的一级目录: 24 | 25 | 1. [前言](01.md) 26 | 2. [背景:C++ 的 1979–2006](02.md) 27 | 3. [C++ 标准委员会](03.md) 28 | 4. [C++11:感觉像是门新语言](04.md) 29 | 5. [C++14:完成 C++11](05.md) 30 | 6. [概念](06.md) 31 | 7. [错误处理](07.md) 32 | 8. [C++17:大海迷航](08.md) 33 | 9. [C++20:方向之争](09.md) 34 | 10. [2020 年的 C++](10.md) 35 | 11. [回顾](11.md) 36 | 37 | --- 38 | 39 | 参加论文翻译工作的译者有(按拼音序): 40 | 41 | - 陈常筠 42 | - 高辉 43 | - 何荣华 44 | - 何一娜 45 | - 侯晨 46 | - 侯金亭 47 | - 彭亚 48 | - 王奎 49 | - 王绍新 50 | - 吴咏炜 51 | - 徐宁 52 | - 杨文波 53 | - 于波 54 | - 余水清 55 | - 翟华明 56 | - 章爱国 57 | - 张云潮 58 | 59 | 论文翻译的校对和体例统一工作由吴咏炜、杨文波、张云潮完成。最后的发布由吴咏炜完成。 60 | 61 | 我们翻译的是论文的正文部分,英文原文超过 140 页。最后的参考文献部分,由于翻译的意义不大,没有译出。不过,这也带来了一个小小的负面后果:虽然我们对论文内部的交叉引用可以用脚本生成链接,但对指向参考文献的链接就完全无能为力了。所以,如果想要阅读参考文献的话,只能请你到[英文原文](https://www.stroustrup.com/hopl20main-p5-p-bfc9cd4--final.pdf)结尾的 References 部分自行查找了。 62 | 63 | 翻译过程中我们发现了一些原文中的小问题,并在译文中进行了修正或标注(绝大部分已经经过 Bjarne 老爷子确认)。当然,在翻译过程中引入翻译错误或其他技术问题,恐怕也在所难免——不过,跟 ACM 上发表论文不同,这个网页仍然是可以修正的。所以,如果你,亲爱的读者,发现问题的话,请不吝提交 pull request,我们会尽快检查并进行修正。 64 | 65 | ![第1章](./01.md) 66 | ![第2章](./02.md) 67 | ![第3章](./03.md) 68 | ![第4章](./04.md) 69 | ![第5章](./05.md) 70 | ![第6章](./06.md) 71 | ![第7章](./07.md) 72 | ![第8章](./08.md) 73 | ![第9章](./09.md) 74 | ![第10章](./10.md) 75 | ![第11章](./11.md) -------------------------------------------------------------------------------- /c++/STL/STL copy.md: -------------------------------------------------------------------------------- 1 | # STL(标准模板库) 2 | 3 | STL主要分为分为三类: 4 | 1.algorithm(算法)——对数据进行处理(解决问题) 步骤的有限集合 5 | 2.container(容器)——用来管理一组数据元素 6 | 3.Iterator (迭代器)——可遍历STL容器内全部或部分元素”的对象 7 | 8 | ## 特性 9 | 10 | 1:数据结构与算法的分离。基于泛型编程思想,STL中算法并不依赖于数据结构进行实现。换句话说,STL中的一个算法可以操作几种不同的容器数据。 11 | 例如,sort()可作用于动态数组、栈和队列,甚至是链表。实际上,这主要是通过迭代器实现的。 12 | 2:STL并非面向对象(OOP)。STL中的数据结构与算法皆不具有明显的继承关系,各部分之间相互独立。 13 | “采用非面向对象的方法在逻辑上降低了各部分之间的耦合关系,以提升各自的独立性、弹性、交互操作性”。 14 | 这符合泛型编程设计原理。不过,STL也采纳了一些封装的思想,令使用者不必知晓其底层实现也能应用自如。 15 | 16 | ## STL的组件 17 | 18 | STL由6个组件组成:容器、算法、迭代器、仿函数、容器适配器、空间配置器; 19 | 容器:各种数据结构,如vector、list、deque、set、map。以模板类的方式提供。 20 | 算法:对容器中的数据执行操作的模板函数,如sort()、find()。在 std 命名空间中定义。 21 | 迭代器:一种访问容器的方法,可理解为指针。算法对容器的操作要借助迭代器才能实现。 22 | 仿函数:一种类,可以像使用函数一样使用它,可理解为更高级的运算符重载。 23 | 容器适配器:一种接口类,封装了一些基本容器,如stack、queue等。 24 | 空间配置器:自动配置与管理内存空间。使用STL时无需手动管理内存。 25 | 26 | STL组件的交互关系:容器通过空间配置器取得数据存储空间,算法通过迭代器操作容器中的内容,仿函数可以协助算法,适配器可以修饰仿函数。 27 | 28 | 说明: 29 | 1.容器和算法通过迭代器可以进行无缝地连接。 30 | 在STL中几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。 31 | 2.STL 最早源于惠普实验室,早于C++存在,但是C++引入STL概念后,STL就成为C++的一部分,因为它被内建在你的编译器之内,不需要另行安装。 32 | 3.STL被组织为下面的13个头文件: 33 | 。 34 | 35 | ## 分类 36 | 37 | map, set, multimap, and multiset 38 | 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 39 | 插入: O(logN) 40 | 查看: O(logN) 41 | 删除: O(logN) 42 | 43 | hash_map, hash_set, hash_multimap, and hash_multiset 44 | 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 45 | 插入:O(1),最坏情况O(N)。 46 | 查看:O(1),最坏情况O(N)。 47 | 删除:O(1),最坏情况O(N)。 48 | 49 | 记住,如果你采用合适的哈希函数,你可能永远不会看到最坏情况。但是记住这一点是有必要的。 50 | -------------------------------------------------------------------------------- /c++/STL/STL.md: -------------------------------------------------------------------------------- 1 | # STL(标准模板库): 2 | STL主要分为分为三类: 3 | 1. algorithm(算法)——对数据进行处理(解决问题) 步骤的有限集合 4 | 2. container(容器)——用来管理一组数据元素 5 | 3. Iterator (迭代器)——可遍历STL容器内全部或部分元素”的对象 6 | 7 | ## 特性: 8 | 1. 数据结构与算法的分离。基于泛型编程思想,STL中算法并不依赖于数据结构进行实现。换句话说,STL中的一个算法可以操作几种不同的容器数据。 9 | 例如,sort()可作用于动态数组、栈和队列,甚至是链表。实际上,这主要是通过迭代器实现的。 10 | 2. STL并非面向对象(OOP)。STL中的数据结构与算法皆不具有明显的继承关系,各部分之间相互独立。 11 | “采用非面向对象的方法在逻辑上降低了各部分之间的耦合关系,以提升各自的独立性、弹性、交互操作性”。 12 | 这符合泛型编程设计原理。不过,STL也采纳了一些封装的思想,令使用者不必知晓其底层实现也能应用自如。 13 | 14 | ## STL的组件: 15 | STL由6个组件组成:容器、算法、迭代器、仿函数、容器适配器、空间配置器; 16 | 17 | - 容器:各种数据结构,如vector、list、deque、set、map。以模板类的方式提供。 18 | - 算法:对容器中的数据执行操作的模板函数,如sort()、find()。在 std 命名空间中定义。 19 | - 迭代器:一种访问容器的方法,可理解为指针。算法对容器的操作要借助迭代器才能实现。 20 | - 仿函数:一种类,可以像使用函数一样使用它,可理解为更高级的运算符重载。 21 | - 容器适配器:一种接口类,封装了一些基本容器,如stack、queue等。 22 | - 空间配置器:自动配置与管理内存空间。使用STL时无需手动管理内存。 23 | 24 | STL组件的交互关系:容器通过空间配置器取得数据存储空间,算法通过迭代器操作容器中的内容,仿函数可以协助算法,适配器可以修饰仿函数。 25 | 26 | 说明: 27 | 1. 容器和算法通过迭代器可以进行无缝地连接。 28 | 在STL中几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。 29 | 2. STL 最早源于惠普实验室,早于C++存在,但是C++引入STL概念后,STL就成为C++的一部分,因为它被内建在你的编译器之内,不需要另行安装。 30 | 3. STL被组织为下面的13个头文件: 31 | 。 32 | 33 | 34 | ## 分类 35 | map, set, multimap, and multiset 36 | 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 37 | 插入: O(logN) 38 | 查看: O(logN) 39 | 删除: O(logN) 40 | 41 | hash_map, hash_set, hash_multimap, and hash_multiset 42 | 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 43 | 插入:O(1),最坏情况O(N)。 44 | 查看:O(1),最坏情况O(N)。 45 | 删除:O(1),最坏情况O(N)。 46 | 47 | 记住,如果你采用合适的哈希函数,你可能永远不会看到最坏情况。但是记住这一点是有必要的。 -------------------------------------------------------------------------------- /c++/STL/哈希容器/unordered_multimap哈希多重集合.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/STL/哈希容器/unordered_multimap哈希多重集合.md -------------------------------------------------------------------------------- /c++/STL/哈希容器/unordered_multiset多重哈希映射.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/STL/哈希容器/unordered_multiset多重哈希映射.md -------------------------------------------------------------------------------- /c++/STL/哈希容器/unordered_set哈希映射.md: -------------------------------------------------------------------------------- 1 | # unordered_set哈希映射 2 | 3 | ## 定义 4 | 5 | unordered_set: 6 | 7 | - 无序 set 容器,即 unordered_set 容器和 set 容器很像,唯一的区别就在于 set 容- 器会自行对存储的数据进行排序,而 unordered_set 容器不会。 8 | 9 | ## unordered_set的特性 10 | 11 | - 不再以键值对的形式存储数据,而是直接存储数据的值 12 | - 容器内部存储的各个元素的值都互不相等,且不能被修改; 13 | - 不会对内部存储的数据进行排序 14 | -------------------------------------------------------------------------------- /c++/STL/哈希容器/哈希容器.md: -------------------------------------------------------------------------------- 1 | # 哈希容器 2 | 3 | STL(Standard Template Library,标准模板库)中的哈希容器是C++中用于高效存储和检索数据的数据结构,特别是当需要快速插入、删除和查找元素时。在C++11之后,STL引入了unordered系列的哈希容器,包括`unordered_map`、`unordered_set`、`unordered_multimap`和`unordered_multiset`。这些容器底层使用`哈希表`实现,它不会对键值对进行排序,元素的位置由哈希函数决定。相比于传统的红黑树实现的关联式容器(如map和set),在平均情况下提供常数时间复杂度的查找、插入和删除操作。 4 | 5 | ## 分类 6 | 7 | [unordered_map (哈希映射)](./unordered_map哈希映射.cpp) 8 | [unordered_multimap (哈希多重集合)](./unordered_multimap哈希多重集合.cpp) 9 | [unordered_set (哈希映射)](./unordered_set哈希映射.cpp) 10 | [unordered_multise (多重哈希映射)](./unordered_multiset多重哈希映射.cpp) 11 | 12 | ### unordered_map 13 | 14 | - 用途:存储键值对(key-value pairs),其中键(key)必须唯一。适合快速查找、插入和删除键值对。 15 | - 特点:底层使用哈希表实现,平均时间复杂度为O(1),但最坏情况下可能退化为O(n)。 16 | - 操作:插入、删除、查找操作高效,支持直接通过键访问值(如m[key])。 17 | - 冲突解决:当多个键哈希到同一槽位时,采用开放寻址法或链地址法解决冲突。 18 | 19 | ### unordered_set 20 | 21 | 用途:存储唯一元素,无序。适用于需要快速判断元素是否存在的情况。 22 | 特点:底层同样使用哈希表,保证元素的唯一性,查找、插入和删除操作高效。 23 | 操作:插入、删除、查找元素,不支持通过索引访问,因为它是无序集合。 24 | 25 | ### unordered_multimap 26 | 27 | 用途:与unordered_map相似,但允许键或元素重复。 28 | 特点:unordered_multimap允许一个键对应多个值。 29 | 操作:除了基本的插入、删除、查找外,还能够处理具有相同键或值的多个条目。 30 | 31 | ### unordered_multiset 32 | 33 | 用途:与unordered_set相似,但允许键或元素重复。 34 | 特点:unordered_multiset允许存储多个相同的元素。 35 | 操作:除了基本的插入、删除、查找外,还能够处理具有相同键或值的多个条目。 36 | -------------------------------------------------------------------------------- /c++/images/C++内存分区.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/C++内存分区.png -------------------------------------------------------------------------------- /c++/images/Hash表的时间复杂度为什么是O(1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/Hash表的时间复杂度为什么是O(1).png -------------------------------------------------------------------------------- /c++/images/const和static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/const和static.png -------------------------------------------------------------------------------- /c++/images/future函数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/future函数.png -------------------------------------------------------------------------------- /c++/images/new_delete对比malloc_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/new_delete对比malloc_free.png -------------------------------------------------------------------------------- /c++/images/vector的底层原理.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/vector的底层原理.png -------------------------------------------------------------------------------- /c++/images/什么不能被继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/什么不能被继承.png -------------------------------------------------------------------------------- /c++/images/动态链接.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/动态链接.jpeg -------------------------------------------------------------------------------- /c++/images/单继承时的虚函数(重写基类虚函数).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/单继承时的虚函数(重写基类虚函数).png -------------------------------------------------------------------------------- /c++/images/单继承时的虚函数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/单继承时的虚函数.png -------------------------------------------------------------------------------- /c++/images/原始基类的虚函数表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/原始基类的虚函数表.png -------------------------------------------------------------------------------- /c++/images/多重继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/多重继承.png -------------------------------------------------------------------------------- /c++/images/多重继承时的虚函数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/多重继承时的虚函数.png -------------------------------------------------------------------------------- /c++/images/必须使用友元的情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/必须使用友元的情况.png -------------------------------------------------------------------------------- /c++/images/移动语义1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/移动语义1.jpg -------------------------------------------------------------------------------- /c++/images/移动语义2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/移动语义2.jpg -------------------------------------------------------------------------------- /c++/images/类对象的内存结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/类对象的内存结构.png -------------------------------------------------------------------------------- /c++/images/继承权限.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/继承权限.png -------------------------------------------------------------------------------- /c++/images/菱形继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/菱形继承.png -------------------------------------------------------------------------------- /c++/images/虚函数的执行过程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/虚函数的执行过程.png -------------------------------------------------------------------------------- /c++/images/虚基类指针数组.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/虚基类指针数组.png -------------------------------------------------------------------------------- /c++/images/虚继承在C++标准库中的实际应用.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/虚继承在C++标准库中的实际应用.png -------------------------------------------------------------------------------- /c++/images/进程的内存空间分布.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/进程的内存空间分布.png -------------------------------------------------------------------------------- /c++/images/静态对象初始化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/静态对象初始化.png -------------------------------------------------------------------------------- /c++/images/静态链接.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/images/静态链接.png -------------------------------------------------------------------------------- /c++/关键字与限定符/#define.md: -------------------------------------------------------------------------------- 1 | # #define 2 | 3 | `#define`是C/C++预处理器指令之一,用于定义宏。宏是一种文本替换机制,在编译之前,预处理器会将源代码中的宏定义替换为对应的文本。#define不仅可以用来定义常量,还可以用于简单的文本替换和条件编译控制。 4 | 5 | ## 基本语法 6 | 7 | ```cpp 8 | #define MACRO_NAME replacement_text 9 | ``` 10 | 11 | 其中,`MACRO_NAME`是宏的名称,而`replacement_text`是在编译前将被宏名称替换掉的文本内容。 12 | 13 | ## 定义常量 14 | 15 | 尽管C++11以后推荐使用`const`或`constexpr`来定义常量,但#define依然常见于旧代码或某些特定场景: 16 | 17 | ```cpp 18 | #define PI 3.14159 19 | ``` 20 | 21 | 在源代码中,每次出现PI都会被替换为`3.14159`。 22 | 23 | ## 文本替换 24 | 25 | `#define`可以用于简单的文本替换,但需要注意的是,它不做语法检查或类型安全检查: 26 | 27 | ```cpp 28 | #define SQUARE(x) (x * x) 29 | ``` 30 | 31 | 调用`SQUARE(5)`会被替换为`(5 * 5)`,但如果有副作用的表达式作为参数传递,可能导致未预期的结果。 32 | 33 | ## 参数化宏 34 | 35 | 宏可以带有参数,形式上像函数调用,但实际上是在预编译阶段进行文本替换: 36 | 37 | ```cpp 38 | #define ADD(a, b) ((a) + (b)) 39 | ``` 40 | 41 | 注意使用括号来避免运算符优先级引起的问题。 42 | 43 | ## 条件编译 44 | 45 | `#define`也可以用于条件编译,控制代码是否被编译器编译: 46 | 47 | ```cpp 48 | #define DEBUG 49 | 50 | #ifdef DEBUG 51 | std::cout << "Debug mode is on." << std::endl; 52 | #endif 53 | ``` 54 | 55 | 如果`DEBUG`被定义,相应的代码块将被编译;否则,这部分代码会被忽略。 56 | 57 | ## 优缺点 58 | 59 | 1. 优点: 60 | - 提供了一种简单的文本替换机制,可用于定义常量、简化复杂的表达式或控制编译流程。 61 | - 可以提高代码的可读性和维护性,尤其是对于配置开关和版本控制。 62 | 63 | 2. 缺点: 64 | - 缺乏类型安全,宏展开不经过编译器类型检查,可能导致错误难以发现。 65 | - 宏可能引入副作用,特别是带参数的宏,不当使用可能导致未预期行为。 66 | - 在调试时,宏的使用可能使追踪问题变得困难,因为它们在源代码中不可见。 67 | 68 | ## 总结 69 | 70 | `#define`是一个强大的预处理功能,虽然在现代C++编程中,许多场景下推荐使用`const`、`constexpr`、`枚举类`或`模板`等替代方案,但了解并掌握`#define`的使用仍然对阅读遗留代码、优化编译选项或进行特定的代码控制非常有用。使用时需谨慎,避免不必要的复杂度和潜在的错误。 71 | -------------------------------------------------------------------------------- /c++/关键字与限定符/=default.md: -------------------------------------------------------------------------------- 1 | # =defaule 2 | 3 | 自C++11起,default在类的特殊成员函数声明后紧跟=, 如= default,用于告诉编译器按照默认行为生成该函数的定义。这对于那些用户没有提供具体实现,但又希望保持其默认行为的成员函数特别有用。这四个主要的特殊成员函数分别是: 4 | 5 | - 默认构造函数 6 | - 拷贝构造函数 7 | - 拷贝赋值运算符(operator=) 8 | - 析构函数 9 | 10 | 使用= default的好处在于,它允许程序员显式请求默认实现,同时保持代码的清晰度,并允许未来的修改。如果程序员重载了某个特殊成员函数,编译器通常不会自动生成默认版本,这时使用= default就能恢复默认行为。 11 | 12 | ## 基本概念 13 | 14 | 当在类的特殊成员函数声明后加上=default,你实际上是告诉编译器:“尽管我在这里声明了这个函数,但我并不打算提供具体的实现细节,请你自动为我生成。”这样做有几个目的: 15 | 16 | 1. 明确性:它使得代码的意图更加清晰。阅读代码的人一眼就能看出这个函数是故意使用默认行为的,而不是因为程序员忘记了实现。 17 | 2. 控制性:即使类的成员或基类发生了变化(可能会影响到默认行为),=default的成员函数仍然会被重新生成,确保行为正确无误。 18 | 3. 可维护性:如果未来需要对这些默认行为进行自定义实现,只需移除=default并提供具体的实现即可,无需修改调用这些函数的地方。 19 | 4. 性能:编译器生成的默认实现往往能优化得更好,尤其是在涉及资源管理的场景下,如RAII(Resource Acquisition Is Initialization)对象的复制控制。 20 | 21 | ## 应用场景 22 | 23 | - 默认构造函数:=default 可以用于显式请求一个无参数的构造函数,即使类中有其他构造函数。 24 | 25 | ```cpp 26 | class MyClass { 27 | public: 28 | MyClass() = default; // 显示声明默认构造函数 29 | // ... 30 | }; 31 | ``` 32 | 33 | - 拷贝控制成员:包括拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。当类中包含指针或资源管理时,合理使用=default或=delete可以遵循资源管理的五法则(RAII)。 34 | 35 | ```cpp 36 | class MyClass { 37 | std::string data; 38 | public: 39 | MyClass(const MyClass&) = default; // 拷贝构造函数 40 | MyClass& operator=(const MyClass&) = default; // 拷贝赋值运算符 41 | // ... 42 | }; 43 | ``` 44 | 45 | - 析构函数:=default也可以用于析构函数,确保即使类的成员发生变化,也能正确清理资源。 46 | 47 | ```cpp 48 | class MyClass { 49 | std::unique_ptr ptr; 50 | public: 51 | ~MyClass() = default; // 显示声明默认析构函数 52 | // ... 53 | }; 54 | ``` 55 | 56 | ## 注意事项 57 | 58 | - 如果类中有自定义的构造函数、拷贝构造函数等,编译器通常不会自动生成默认版本,此时需要显式=default来请求。 59 | 60 | - 在某些情况下,如果类的设计导致默认行为不适用(例如含有不可拷贝的成员变量),则应该=delete这些操作,以避免编译错误或运行时错误。 61 | 62 | - =default必须直接跟在声明后面,不能在类外单独使用(除非它是内联定义的一部分)。 63 | -------------------------------------------------------------------------------- /c++/关键字与限定符/=delete.md: -------------------------------------------------------------------------------- 1 | # 禁止继承或重写 = delete 2 | 3 | 从C++11开始,delete还可以用于类的定义中,以禁止特定的构造函数、析构函数、复制构造函数、赋值运算符或者普通成员函数的实例化或继承。这通常称为“删除定义”或“删除函数”。 4 | 5 | ## 功能 6 | 7 | ### 禁止特定成员函数 8 | 9 | ``= delete``可以直接用于类的成员函数声明之后,表明该函数被明确禁止定义或调用。这通常用于: 10 | 11 | - 删除拷贝构造函数和赋值操作符:防止类的实例被复制。 12 | - 禁用不希望使用的构造函数或其他成员函数:确保类只能通过特定方式使用或初始化。 13 | 14 | 语法示例 15 | 16 | ```cpp 17 | class Immutable { 18 | public: 19 | Immutable(const std::string& data) : mData(data) {} // 允许的构造函数 20 | Immutable(const Immutable&) = delete; // 禁止拷贝构造 21 | Immutable& operator=(const Immutable&) = delete; // 禁止赋值运算符 22 | 23 | private: 24 | std::string mData; 25 | }; 26 | ``` 27 | 28 | ### 阻止类的继承 29 | 30 | 要阻止一个类被其他类继承,可以在类声明的开始处使用= delete,但这不是直接通过= delete语法完成的,而是通过将类声明为final来实现。这是因为= delete主要用于成员函数,而阻止继承是类级别的特性。 31 | 32 | 语法示例 33 | 34 | ```cpp 35 | class FinalClass final { 36 | // 类定义... 37 | }; 38 | ``` 39 | 40 | ### 删除编译器自动生成的函数 41 | 42 | C++编译器会自动为类生成一些特殊成员函数,如默认构造函数、拷贝构造函数、赋值运算符和析构函数,如果未显式定义。= delete可以用来指示编译器不要生成这些默认的函数。 43 | 44 | 例子: 45 | 46 | ```cpp 47 | class NonDefaultConstructible { 48 | public: 49 | NonDefaultConstructible(int value) : mValue(value) {} // 自定义构造函数 50 | NonDefaultConstructible() = delete; // 禁止默认构造函数 51 | 52 | private: 53 | int mValue; 54 | }; 55 | ``` 56 | 57 | ## 优点 58 | 59 | - 增加安全性:通过禁止不安全或不希望的操作,减少错误和资源管理问题。 60 | - 设计清晰:明确表达类的设计意图,增强代码的可读性和维护性。 61 | - 编译时检查:= delete的使用会在编译时产生错误,而不是运行时,有助于早期发现问题。 62 | 63 | ## 注意事项 64 | 65 | - 当使用= delete时,务必确保该操作确实不应在程序中发生,因为一旦定义,任何尝试使用该函数的代码都将无法编译通过。 66 | - 对于禁止继承,使用final关键字而不是= delete。 67 | - 即使函数被= delete,它仍然需要声明,这意味着它可以被用作模板参数或友元函数的重载解析的一部分。 68 | -------------------------------------------------------------------------------- /c++/关键字与限定符/delete.md: -------------------------------------------------------------------------------- 1 | # delete 2 | 3 | 在C++中,delete关键字有两个主要用途,但它们分别位于不同的上下文中,一个是用于动态内存管理,另一个则是用于禁止某些类成员的继承或实例化。 4 | 5 | ## 动态内存管理 - delete表达式 6 | 7 | 在C++中,delete关键字是一个操作符,是用来释放之前通过new操作符动态分配的内存的。它确保了对象的析构函数被调用(如果有的话),并且释放了与对象关联的内存。delete有两种主要的使用形式,分别对应于单个对象和对象数组的释放。 8 | 9 | ### 释放单个对象 10 | 11 | 当你要释放一个通过new创建的单个对象时,使用以下形式的delete: 12 | 13 | ```cpp 14 | delete pointer; 15 | ``` 16 | 17 | 这里的pointer是一个之前通过new分配的内存地址的指针。执行delete后,对象的析构函数(如果存在)会被调用,以执行必要的清理工作,随后这块内存被标记为可用,可以被后续的内存分配重用。需要注意的是,删除指针后,指针本身并不会被自动置为nullptr,因此建议手动将其置为nullptr以避免悬挂指针的问题: 18 | 19 | ```cpp 20 | delete pointer; 21 | pointer = nullptr; // 避免悬挂指针 22 | ``` 23 | 24 | #### 单个对象的释放原型 25 | 26 | ```cpp 27 | void operator delete(void* ptr) noexcept; 28 | ``` 29 | 30 | 这个原型表示标准的delete操作,用于释放单个对象的内存。它接受一个指向之前由new分配的内存的指针作为参数,并释放与之关联的内存。noexcept表明此操作不应该抛出异常。如果定义了自己的析构函数且没有显式调用delete释放内存,编译器会自动插入对operator delete的调用。 31 | 32 | ### 释放对象数组 33 | 34 | 如果你通过new[]分配了一个对象数组,那么需要使用带有方括号的delete[]来释放: 35 | 36 | ```cpp 37 | delete[] arrayPointer; 38 | ``` 39 | 40 | 使用delete[]时,数组中的每个对象都会调用自己的析构函数(如果有)。与单个对象的释放一样,释放后应该将指针置为nullptr: 41 | 42 | ```cpp 43 | delete[] arrayPointer; 44 | arrayPointer = nullptr; 45 | ``` 46 | 47 | #### 数组的释放原型 48 | 49 | ```cpp 50 | void operator delete[](void* ptr) noexcept; 51 | ``` 52 | 53 | 这个原型用于释放由new[]分配的数组内存。它同样接受一个指向数组首元素的指针,并负责释放整个数组的内存空间。和单对象的delete一样,它也不应该抛出异常。 54 | 55 | ### 重载 56 | 57 | operator delete和operator delete[]都可以被用户重载,以实现自定义的内存管理策略。重载时,你可以定义额外的参数,比如一个用于追踪内存分配的大小参数,来帮助管理内存。 58 | 59 | ### C++11 带有对齐参数的operator new和operator delete 60 | 61 | 在C++中,为了支持对齐要求较高的类型(例如某些SIMD向量或硬件加速器要求的数据结构),C++11引入了带有对齐参数的operator new和operator delete。这些重载版本允许程序员指定内存分配的对齐方式,以确保数据正确对齐,从而提升程序的性能或兼容性。以下是这些操作符的原型和使用方式: 62 | 63 | #### 带对齐参数的operator new 64 | 65 | ```cpp 66 | void* operator new(std::size_t size, std::align_val_t alignment); 67 | ``` 68 | 69 | 这里的size参数表示要分配的内存大小,而alignment参数是一个std::align_val_t类型的值,用于指定所需的对齐边界。std::align_val_t是一个能够表示对齐值的类型,通常通过std::align_val_t{N}的形式来构造,其中N是2的幂,代表对齐的字节数。 70 | 71 | #### 带对齐参数的operator delete 72 | 73 | ```cpp 74 | void operator delete(void* ptr, std::align_val_t alignment) noexcept; 75 | ``` 76 | 77 | 此版本的operator delete用于释放由带有对齐参数的operator new分配的内存。它同样接受一个指针和对齐值作为参数,以确保正确地释放内存。 78 | 79 | #### 使用示例 80 | 81 | 尽管直接调用这些带有对齐参数的版本并不常见(通常由编译器根据类型自动处理),但在某些自定义内存管理或特定性能优化的场景下,可能会直接使用它们。例如,如果你有一个需要特定对齐的类型,可以这样分配内存: 82 | 83 | ```cpp 84 | struct AlignedData { 85 | alignas(64) int data[100]; 86 | }; 87 | 88 | AlignedData* ptr = static_cast(::operator new(sizeof(AlignedData), std::align_val_t{64})); 89 | ``` 90 | 91 | 释放时,也要使用对应的带有对齐参数的delete: 92 | 93 | ```cpp 94 | ::operator delete(ptr, std::align_val_t{64}); 95 | ``` 96 | 97 | #### 注意事项 98 | 99 | 确保对齐值是一个有效的对齐值,通常是2的幂。 100 | 当重载这些操作符时, 101 | 102 | ### delete总结与注意事项 103 | 104 | - 未初始化的指针:不要对未初始化或已经释放过的指针调用delete。 105 | - 重复释放:重复调用delete或delete[]释放同一块内存会导致未定义行为,可能引起程序崩溃。 106 | - 类型匹配:使用new分配内存时指定的类型必须与delete释放时的类型相匹配,包括数组和非数组类型的区别。 107 | - 自定义内存管理:可以通过重载全局的operator new和operator delete来自定义内存分配和释放的行为。如果你使用了自定义的operator new,也应该提供对应的operator delete版本,以保持内存管理的一致性。 108 | [new](../关键字与限定符/new.md) 109 | - 智能指针:现代C++推荐使用智能指针(如std::unique_ptr和std::shared_ptr)来自动管理动态内存,从而减少手动调用delete的需要。 110 | - 在C++11之后,还可以使用带有对齐参数的operator new和operator delete,以支持对齐要求较高的类型。 111 | 112 | ## 禁止继承或重写 = delete 113 | 114 | [=delete](../关键字与限定符/=delete.md) 115 | -------------------------------------------------------------------------------- /c++/关键字与限定符/explicit.md: -------------------------------------------------------------------------------- 1 | # explicit 2 | 3 | 在C++中,explicit关键字是一个用于限制构造函数或类型转换运算符的特殊限定符,它的主要目的是控制编译器如何执行类型转换,从而增强代码的清晰度、减少类型转换错误,并提升程序的健壮性。 4 | 5 | ## 目的 6 | 7 | explicit关键字的主要目的是禁止编译器执行从一个类型到另一个类型的隐式类型转换,尤其是在使用构造函数或类型转换运算符时。这有助于避免非预期的类型转换,这些转换可能导致逻辑错误或不符合程序员初衷的行为。 8 | 9 | ## 隐式转换 10 | 11 | 常见场景: 12 | 13 | 1. 数值类型提升:在涉及运算的不同数值类型中,较小的类型通常会被自动转换为较大的类型,以避免数据丢失。例如,在C++中,计算 int + double 时,int 类型的值会被提升为 double 类型。 14 | 15 | 2. 算术转换:进行算术运算时,不同类型会转换为同一类型。例如,整数与浮点数运算时,整数会被转换为浮点数。 16 | 17 | 3. 指针与布尔值转换:在很多语言中,空指针(如C++中的nullptr)在布尔上下文中被视为false,非空指针被视为true。 18 | 19 | 4. 数组与指针:在C/C++中,数组名在大多数情况下会被自动转换为指向其首元素的指针。 20 | 21 | 5. 类对象与基类对象:面向对象编程中,派生类对象可隐式转换为其基类类型的对象或指针/引用。 22 | 23 | 6. 类型构造与函数重载解析:编译器可能会根据函数参数或操作符重载的需要,隐式转换实参类型。 24 | 25 | ### C++11 隐式转换 26 | 27 | C++11确实引入了对构造函数的改进,允许在某些条件下进行更灵活的隐式类型转换。具体来说,如果一个构造函数除了第一个参数之外的所有参数都有默认值,那么这个构造函数可以用于隐式类型转换。这意味着,即使构造函数有多个参数,只要从第一个之后的参数都可以通过默认值“省略”,这样的构造函数就可以在需要的场合被编译器用来执行隐式类型转换。 28 | 29 | ```cpp 30 | class MyClass { 31 | public: 32 | MyClass(int a, std::string s = "") : a_(a), s_(s) {} // 第二个参数有默认值 33 | 34 | // ... 其他成员 35 | private: 36 | int a_; 37 | std::string s_; 38 | }; 39 | ``` 40 | 41 | 在这个例子中,MyClass 的构造函数接受两个参数,其中第二个参数 s 有默认值。因此,这个构造函数可以用于从 int 类型到 MyClass 类型的隐式转换,例如: 42 | 43 | ```cpp 44 | MyClass obj = 42; // 隐式转换,使用默认的字符串"" 45 | ``` 46 | 47 | 然而,值得注意的是,过度使用这种隐式转换可能导致代码的可读性和可维护性降低,因为它可能使类型转换发生得不那么明显。因此,在设计类时,应该谨慎考虑是否需要这样的隐式转换,并且C++提供了 explicit 关键字来显式禁止这种行为,确保构造函数只能显式调用。 48 | 49 | ## 防止构造函数的隐式转换 50 | 51 | 当一个类的构造函数只有一个参数时,这个构造函数可以被隐式调用来进行类型转换。例如,如果你有一个接受int参数的构造函数,编译器可能会在需要该类实例的地方自动将一个int值转换成该类的对象。通过将这样的构造函数声明为explicit,你可以阻止这种隐式转换,要求必须通过直接初始化或显式类型转换来创建对象。 52 | 53 | ```cpp 54 | class Widget { 55 | public: 56 | explicit Widget(int size) { 57 | // 构造函数实现 58 | } 59 | }; 60 | 61 | void doSomething(Widget w) { 62 | // 函数实现 63 | } 64 | 65 | // 示例使用 66 | Widget w(10); // 直接初始化是允许的 67 | // doSomething(10); // 错误:不能隐式转换int到Widget 68 | doSomething(Widget(10)); // 正确:显式转换 69 | ``` 70 | 71 | 在这个例子中,Widget的构造函数被标记为explicit,因此不能隐式地从int转换为Widget。 72 | 73 | ## 防止转换运算符的隐式转换 74 | 75 | 类可以通过定义**类型转换运算符**(如operator int())来允许对象被隐式转换为另一种类型。标记这样的转换运算符为explicit,可以防止这种隐式转换,要求转换必须通过显式类型cast(如(int)myObject)来执行。 76 | 77 | ```cpp 78 | class Boolean { 79 | bool value; 80 | public: 81 | Boolean(bool value) : value(value) {} 82 | explicit operator bool() const { 83 | return value; 84 | } 85 | }; 86 | 87 | // 示例使用 88 | Boolean b(true); 89 | bool val = b; // 错误:不能隐式转换Boolean到bool 90 | bool val2 = static_cast(b); // 正确:显式转换 91 | ``` 92 | 93 | 在这个例子中,Boolean类的转换运算符被标记为explicit,阻止了其隐式转换到bool。 94 | 95 | ## 使用场景 96 | 97 | - 提高代码可读性:通过阻止隐式转换,explicit关键字强制程序员显式地表明转换的意图,提高代码的可读性和可维护性。 98 | - 避免意外的行为:隐式转换有时会导致意料之外的行为,使用explicit可以避免这类问题。 99 | 100 | ```cpp 101 | class Fraction { 102 | int numerator, denominator; 103 | public: 104 | explicit Fraction(int n, int d = 1) : numerator(n), denominator(d) {} 105 | // ... 106 | }; 107 | 108 | void processFraction(Fraction f) { 109 | // 函数实现 110 | } 111 | 112 | // 示例使用 113 | Fraction f = Fraction(3, 4); // 正确:显式构造 114 | ``` 115 | 116 | 在这个例子中,Fraction的构造函数被标记为explicit,避免了int到Fraction的隐式转换,使得转换意图更加明确。 117 | 118 | 总之,explicit关键字是C++中一个非常有用的特性,它能有效地避免因隐式类型转换导致的错误和不可预期的行为,提高代码的安全性和可维护性。 119 | -------------------------------------------------------------------------------- /c++/关键字与限定符/final.md: -------------------------------------------------------------------------------- 1 | # final 2 | 3 | 在C++中,final关键字是一个用于限制继承和覆盖的修饰符,它体现了设计者对类结构的意图,增加了代码的健壮性和可读性。final主要有以下两种用法: 4 | 5 | ## 1. 类的最终声明(Final Classes) 6 | 7 | 当一个类被声明为``final``时,它不能被其他类继承。这表明该类的设计已经完整,不需要也不应该有子类对其进行修改或扩展。这对于确保类的行为不会被意外改变非常有用,特别是在实现一些核心功能或库类时。 8 | 9 | 语法: 10 | 11 | ```cpp 12 | class FinalClass final { 13 | // 类定义... 14 | }; 15 | ``` 16 | 17 | ## 2. 方法的最终实现(Final Methods) 18 | 19 | 当类中的某个成员函数被标记为final时,该方法在其所有派生类中都不能被重写(override)。这意味着该方法提供了最终的实现版本,任何试图覆盖它的尝试都会导致编译错误。这对于强制执行基类中方法的行为至关重要,确保某些关键逻辑或算法不被子类修改。 20 | 21 | 语法: 22 | 23 | ```cpp 24 | class BaseClass { 25 | public: 26 | virtual void someMethod() final { 27 | // 最终实现... 28 | } 29 | }; 30 | 31 | class DerivedClass : public BaseClass { 32 | // 编译错误:试图覆盖final方法 33 | void someMethod() override { 34 | // ... 35 | } 36 | }; 37 | ``` 38 | 39 | ## 为什么使用final? 40 | 41 | - 设计约束:通过明确指出某些类或方法不应该被继承或覆盖,final帮助设计者传达其对软件架构的意图,增强代码的可读性和可维护性。 42 | 43 | - 性能优化:编译器在处理final方法时,可以做出一些优化假设,因为它们知道这些方法不会在运行时被其他方法覆盖,这有时可以带来性能上的微小提升。 44 | 45 | - 确保稳定性:在库开发或框架设计中,使用final可以保护核心组件不受用户代码的意外修改,从而保证系统的稳定性和安全性。 46 | 47 | ## 注意事项 48 | 49 | - 使用final关键字是一种设计决策,应当谨慎考虑。过度使用可能会限制代码的灵活性和未来的扩展能力。 50 | 51 | - final不能用于非虚函数,因为非虚函数默认就是不可覆盖的。 52 | 53 | - C++11标准开始引入了final关键字,确保你的编译器支持这一特性。 54 | -------------------------------------------------------------------------------- /c++/关键字与限定符/free.md: -------------------------------------------------------------------------------- 1 | # free 2 | 3 | free函数是C语言中的一个内存管理函数,用于释放之前通过malloc、calloc或realloc分配的动态内存。尽管在C++中推荐使用new和delete来管理动态内存,但free函数仍可用于与C代码交互或在某些特定情况下释放由malloc等分配的内存。 4 | 5 | ## 函数原型 6 | 7 | ```c 8 | void free(void* ptr); 9 | ``` 10 | 11 | 参数说明: 12 | 13 | - void* ptr:一个之前由malloc、calloc或realloc成功分配的内存地址的指针。如果传递的指针为NULL,free函数不做任何操作。 14 | 15 | 功能描述: 16 | 17 | - free函数将指定的内存块标记为未使用,使其可供后续的内存分配请求重新使用。请注意,调用free并不意味着立即擦除该内存区域的内容,只是将其标记为可用状态。实际数据可能依然存在,直到同一块内存被后续的分配覆盖。 18 | 19 | ## 使用示例 20 | 21 | ```cpp 22 | #include 23 | 24 | int main() { 25 | int *ptr = (int*)malloc(sizeof(int)); // 分配内存 26 | if (ptr != NULL) { 27 | *ptr = 42; // 使用内存 28 | free(ptr); // 释放内存 29 | } 30 | return 0; 31 | } 32 | ``` 33 | 34 | ## 注意事项 35 | 36 | - 重复释放:对同一块内存多次调用free是未定义行为,可能导致程序崩溃或内存损坏。 37 | 38 | - 野指针:释放内存后,原指针变为悬空指针(野指针),继续使用该指针会导致未定义行为。建议在释放内存后立即将指针设置为NULL,以避免误用。 39 | 40 | - 不释放内存:忘记释放不再使用的动态内存会导致内存泄漏,随着时间推移,可能耗尽程序可用内存。 41 | 42 | - 与new/delete的对比:在C++中,应优先使用new来分配内存,并使用delete(对于单个对象)或delete[](对于数组)来释放内存,这些操作与构造函数和析构函数的调用相结合,提供了更好的类型安全和资源管理。 43 | 44 | ## 现代C++实践 45 | 46 | 在现代C++编程实践中,推荐使用**智能指针**(如std::unique_ptr、std::shared_ptr)来自动管理内存,它们会在超出作用域时自动调用相应的删除器(默认为delete),从而避免了手动调用free或delete的需求,提高了代码的安全性和简洁性。 47 | -------------------------------------------------------------------------------- /c++/关键字与限定符/include.md: -------------------------------------------------------------------------------- 1 | # include 2 | 3 | 在C++中,#include 是一个预处理指令,用于在编译前将一个源代码文件(通常称为头文件或标头文件)的内容插入到当前的源代码文件中。这一机制使得程序员能够重用代码、定义和声明,以及共享类型定义、函数原型、类结构等。 4 | 5 | ## 基本语法 6 | 7 | `#include` 指令的基本形式有两种: 8 | 9 | 1. 使用尖括号:通常用于包含标准库头文件。 10 | 11 | ```cpp 12 | #include <头文件名> 13 | ``` 14 | 15 | 例如,包含标准输入输出流库: 16 | 17 | ```cpp 18 | #include 19 | ``` 20 | 21 | 2. 使用双引号:通常用于包含用户自定义的头文件或项目内的头文件。 22 | 23 | ```cpp 24 | #include "头文件名" 25 | ``` 26 | 27 | 例如,包含项目内的自定义头文件“myheader.h”: 28 | 29 | ```cpp 30 | #include "myheader.h" 31 | ``` 32 | 33 | ## 目的 34 | 35 | - 代码重用:通过将常用的函数声明、类定义、宏定义等放在头文件中,可以被多个源文件共享,减少重复编写相同的代码。 36 | - 模块化编程:将程序分解成逻辑上独立的部分,每个部分的接口通过头文件定义,实现细节隐藏在相应的源文件中。 37 | - 类型安全和提前检查:在编译阶段就可以发现类型不匹配、未声明的标识符等问题,因为所有的声明和定义都会通过包含的头文件被检查。 38 | 39 | ## 搜索路径 40 | 41 | 当使用尖括号`< >`时,编译器会在标准库路径中搜索指定的头文件。 42 | 当使用双引号`" "`时,编译器首先在当前源文件所在的目录中查找头文件,如果找不到,则会按照编译器的配置(通常是项目的包含路径设置)去其他指定的目录中搜索。 43 | 44 | ## 预处理器的工作方式 45 | 46 | `#include` 指令是在编译过程的预处理阶段被执行的,这意味着实际的文件包含操作发生在编译之前,源代码文件在被编译器看到之前就已经包含了所有通过`#include`引入的内容。 47 | 48 | ## 避免重复包含 49 | 50 | 为了避免头文件被多次包含导致的错误(如函数或类的多重定义),通常会在头文件顶部使用预处理器宏来防止重复包含,这种做法称为“头文件守卫”(Header Guard)。 51 | 52 | ```cpp 53 | #ifndef HEADER_FILE_NAME_H 54 | #define HEADER_FILE_NAME_H 55 | 56 | // 这里放置头文件的内容 57 | 58 | #endif // HEADER_FILE_NAME_H 59 | ``` 60 | 61 | 其中 `HEADER_FILE_NAME_H` 应该被替换为一个与头文件相关的唯一标识符,确保在不同的头文件中不会冲突。 62 | 63 | ## 总结 64 | 65 | `#include` 指令是C++编程中组织和管理代码的关键元素,它促进了代码的模块化、重用性和类型安全性。正确理解和使用 #`include` 及头文件守卫是每位C++开发者必备的技能。 66 | 67 | ### nclude " " 和 <> 的区别 68 | 69 | include<文件名> 和 #include"文件名" 的区别: 70 | 71 | - 查找文件的位置:include<文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include"文件名" 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。 72 | - 使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include"文件名" 73 | -------------------------------------------------------------------------------- /c++/关键字与限定符/malloc.md: -------------------------------------------------------------------------------- 1 | # malloc 2 | 3 | malloc是一个来自C语言的函数,用于动态分配内存,尽管在C++中不鼓励直接使用它(更推荐使用new操作符),但为了兼容性和某些特定情况,C++仍然保留了这个函数。 4 | 5 | ## 作用 6 | 7 | 在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的开头位置。 8 | 9 | ## 使用malloc 10 | 11 | ### 头文件 12 | 13 | ```cpp 14 | #include 或#include 15 | ``` 16 | 17 | ### 函数原型 18 | 19 | ```cpp 20 | void* malloc(size_t size); 21 | ``` 22 | 23 | 参数说明 24 | size_t size:要分配的内存大小,以字节为单位。size_t是一个无符号整数类型,通常足够大以表示程序可能需要分配的任何内存块的大小。 25 | 26 | ### 返回值 27 | 28 | - 如果分配成功,malloc返回一个指向分配到的内存起始地址的指针。 29 | - 如果分配失败(例如,没有足够的可用内存),malloc返回NULL。 30 | 31 | 在以前malloc返回的是char型指针,新的ANSIC标准规定,该函数返回为void型指针。 32 | void*表示未确定类型1的指针。C,C++规定,void* 类型可以通过类型转换强制转换为任何其它类型的指针,因此必要时要进行强制类型转换。 33 | 34 | ```cpp 35 | char *str = (char *) malloc(15);//定义一个指向char(空间大小为15个字节)的指针 36 | ``` 37 | 38 | 如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。 39 | 40 | ### 释放内存 41 | 42 | 当内存不再使用时,应使用free()函数将内存块释放。 43 | 注:   44 | 使用malloc分配的内存空间在虚拟地址空间上是连续的,即外部使用起来是连续的,可以对指针进行运算而且结果正确。但是转换到物理内存空间上有可能是不连续的,因为有可能相邻的两个字节是在不同的物理分页上; 45 | 46 | ### 使用示例 47 | 48 | ```cpp 49 | #include // 或#include ,取决于编译器环境 50 | 51 | int main() { 52 | int *ptr = (int*)malloc(sizeof(int)); // 申请一个int大小的内存 53 | if (ptr == NULL) { 54 | std::cout << "Memory allocation failed\n"; 55 | return 1; 56 | } 57 | 58 | *ptr = 42; // 在分配的内存中存储一个值 59 | std::cout << "Value: " << *ptr << "\n"; 60 | 61 | free(ptr); // 释放内存 62 | return 0; 63 | } 64 | ``` 65 | 66 | ## 注意事项 67 | 68 | - 类型安全:与C++的new操作符不同,malloc不执行构造函数,也不对返回的指针类型进行检查。因此,使用malloc后通常需要进行类型转换,并且程序员需确保正确管理内存和类型安全。 69 | 70 | - 内存初始化:malloc分配的内存内容是未定义的,可能包含之前分配在此处的数据残余。如果需要初始化为特定值,需要手动设置。 71 | 72 | - 释放内存:使用malloc分配的内存必须用free函数释放。忘记释放会导致内存泄漏。 73 | 74 | - 异常安全:C++中的new操作符可以与异常处理机制结合,提供更好的错误处理。而直接使用malloc时,需要手动检查分配是否成功并处理错误。 75 | 76 | - 现代C++实践:虽然了解malloc及其工作原理对于理解底层内存管理有帮助,但在现代C++编程中,更推荐使用智能指针(如std::unique_ptr、std::shared_ptr)和容器类来自动管理内存,以及使用new和delete操作符进行显式内存管理,以提高代码的安全性和可维护性。 77 | -------------------------------------------------------------------------------- /c++/关键字与限定符/noexcept.md: -------------------------------------------------------------------------------- 1 | # noexcept 2 | 3 | noexcept是C++11引入的关键字,用于指定一个函数承诺不抛出任何异常。这个关键字对于性能优化、接口设计和异常处理策略有着重要意义。以下是关于noexcept的详细讲解: 4 | 5 | ## 基本用法 6 | 7 | 1. 声明函数不抛出异常: 8 | 当你在函数声明或定义中使用noexcept关键字,你是在告诉编译器该函数不会抛出任何异常。基本语法如下: 9 | 10 | ```cpp 11 | void myFunction() noexcept; 12 | ``` 13 | 14 | 或者在类的成员函数声明中: 15 | 16 | ```cpp 17 | class MyClass { 18 | public: 19 | void myMethod() noexcept; 20 | }; 21 | ``` 22 | 23 | 2. 基于条件的noexcept: 24 | 你还可以根据一个常量表达式的值决定是否抛出异常。如果该表达式求值为true,则函数承诺不抛出异常;如果为false,则函数可以抛出异常。 25 | 26 | ```cpp 27 | void mayThrowOrNot(bool condition) noexcept(condition); 28 | ``` 29 | 30 | ## 优点 31 | 32 | 1. 性能提升: 33 | 编译器知道一个函数不抛出异常,可以执行以下优化: 34 | - 省略异常处理的开销,比如无需展开调用栈以维护异常处理信息。 35 | - 在某些情况下,允许编译器进行更为激进的内联和优化,因为不需要考虑异常路径。 36 | 37 | 2. 接口清晰: 38 | noexcept提供了关于函数行为的额外信息,帮助库使用者了解哪些函数可以安全地在可能不支持异常的环境中使用,如实时系统或嵌入式系统。 39 | 40 | 3. 移动语义和完美转发: 41 | 对于STL容器和智能指针等需要高效资源管理的组件,noexcept对于启用高效的移动构造函数和移动赋值至关重要,因为它允许编译器在必要时安全地选择移动而非复制。 42 | 43 | ## 限制与注意事项 44 | 45 | 1. 继承和虚函数: 46 | 如果基类的虚函数被声明为noexcept,派生类覆盖该函数时也必须保持noexcept。反之,如果基类函数允许抛出异常,派生类的对应函数可以声明为noexcept或不声明。 47 | 48 | 2. const和引用限定符: 49 | 在成员函数中,noexcept应当紧跟在const和引用限定符之后,但在final、override或纯虚函数=0之前。 50 | 51 | 3. 模板与SFINAE: 52 | noexcept特性可以用于SFINAE(Substitution Failure Is Not An Error),即模板特化失败不会被视为错误,允许编译器在模板推导时排除不满足条件的候选函数。 53 | 54 | 4. 运算符: 55 | C++还提供了一个noexcept运算符,它在编译时评估一个表达式是否可能抛出异常。例如,noexcept(expr)返回一个布尔值,指示expr是否可以无异常地执行。 56 | 57 | ## 总结 58 | 59 | noexcept是现代C++编程中一个重要的特性,它不仅能够帮助开发者编写更高效、更明确的代码,而且对于库设计者来说,是实现高效资源管理、优化性能和确保接口兼容性的关键工具。合理使用noexcept能够显著提升程序的可靠性和执行效率。 60 | -------------------------------------------------------------------------------- /c++/关键字与限定符/override.md: -------------------------------------------------------------------------------- 1 | # override 2 | 3 | 在C++中,override关键字是一个继承控制修饰符,它用于显式地指示一个成员函数旨在覆盖(或重写)基类中的虚函数。override的引入增强了代码的清晰度和类型安全,帮助开发者避免因函数签名不匹配导致的无意覆盖错误。 4 | 5 | ## 目的 6 | 7 | - 明确意图:通过在派生类的成员函数声明后加上override,编译器能够验证该函数是否确实覆盖了基类中的虚函数。这使得代码意图更加清晰,易于理解和维护。 8 | 9 | - 编译时检查:如果函数签名与基类中的虚函数不完全匹配(包括函数名称、参数类型、数量和顺序,以及const/volatile限定符),编译器将生成错误,防止意外的函数定义。 10 | 11 | ## 使用场景 12 | 13 | 虚函数覆盖:当派生类想要提供基类虚函数的不同实现时,可以在派生类的函数声明后添加override关键字。 14 | 15 | ## 语法 16 | 17 | ```cpp 18 | class Base { 19 | public: 20 | virtual void someFunction() const; 21 | }; 22 | 23 | class Derived : public Base { 24 | public: 25 | void someFunction() const override; // 明确表示此函数覆盖了Base类中的someFunction() 26 | }; 27 | ``` 28 | 29 | ## 注意事项 30 | 31 | - 必须匹配:使用override的函数必须精确匹配基类中的虚函数签名,除了函数返回类型可以不同(C++14开始允许协变返回类型)。 32 | 33 | - 非虚函数:override不能用于非虚函数,因为非虚函数不能被覆盖。 34 | 35 | - 默认情况下非final:即使基类中的函数标记为final,在派生类中错误地使用override也不会被编译器捕获,因为final阻止了覆盖,编译器根本不会检查override标签。 36 | 37 | - C++11引入:override关键字是在C++11标准中引入的,因此确保你的编译器支持C++11或更高版本。 38 | 39 | ## 示例 40 | 41 | ```cpp 42 | #include 43 | 44 | class Animal { 45 | public: 46 | virtual void speak() const { 47 | std::cout << "Some generic animal sound." << std::endl; 48 | } 49 | }; 50 | 51 | class Dog : public Animal { 52 | public: 53 | void speak() const override { // 正确覆盖Animal的speak函数 54 | std::cout << "Woof!" << std::endl; 55 | } 56 | }; 57 | 58 | int main() { 59 | Dog dog; 60 | dog.speak(); // 输出 "Woof!" 61 | return 0; 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /c++/关键字与限定符/realloc.md: -------------------------------------------------------------------------------- 1 | # realloc 2 | 3 | realloc是C语言中的一个内存管理函数,用于调整之前通过malloc、calloc或realloc分配的内存块的大小。虽然C++提供了更高级的内存管理机制,如智能指针和容器类,但realloc在某些特定场景下,尤其是在与C代码交互或者进行低级别内存管理时,仍然是一个可用的工具。 4 | 5 | ## 函数原型 6 | 7 | ```cpp 8 | c: 9 | extern void *realloc(void *mem_address, unsigned int newsize) 10 | c++: 11 | void* realloc(void* ptr, size_t size); 12 | ``` 13 | 14 | 参数说明: 15 | 16 | - void* ptr:之前通过malloc、calloc或realloc分配的内存块的指针。如果ptr为NULL,则realloc的行为等同于malloc(size)。 17 | - size_t size:新的内存块大小(以字节为单位)。如果size为0且ptr非NULL,则会释放内存并返回NULL。如果size大于原内存块的大小,则尝试扩大内存块;如果size小于原内存块的大小,则可能会缩小内存块,但不会返回多余的内存给系统。 18 | 19 | ### 头文件 20 | 21 | `#include ` 或`#include ` 22 | 23 | ### 作用 24 | 25 | c: 26 | 先判断当前的指针是否有足够的连续空间,如果有,改变mem_address指向的地址,并且将返回mem_address;如果空间不够,先按照newsize指定的大小分配另一块新空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域(注意:原来指针是自动释放2,不需要使用free),同时返回新分配的内存区域的首地址。 27 | 新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;如果新的大小小于原内存大小,可能会导致数据丢失。 28 | 举例: 29 | 30 | ```cpp 31 | int *p=(int *)malloc(sizeof(int));//定义一个指向int的指针,指针名为p 32 | int *base=(int *)realloc(p,10);//重新将p指向的地址扩大范围为10个字节,用新指针base指向扩大后的地址,同时自动释放原指针p; 33 | ``` 34 | 35 | c++: 36 | realloc尝试重新分配内存块的大小。如果现有内存块后面有足够的空间满足新大小要求,它可能会直接调整大小而不移动数据。如果当前内存块无法调整到所需大小(比如因为内存碎片问题),realloc可能会分配一个新的内存块,将原内存块的内容复制到新位置,然后释放旧内存块。这表示调用realloc时,传入的指针可能会改变。 37 | 38 | ### 返回值 39 | 40 | - 成功时,返回调整后内存块的新地址,可能与原地址相同也可能不同。 41 | - 失败时,返回NULL,原内存块保持不变,不会释放。因此,建议在调用realloc前先保存原指针,以便在失败时恢复。 42 | 43 | ### 示例 44 | 45 | ```cpp 46 | #include 47 | #include 48 | 49 | int main() { 50 | int *ptr = (int*)malloc(sizeof(int)); // 初始分配 51 | if (ptr == NULL) { 52 | std::cerr << "Memory allocation failed.\n"; 53 | return 1; 54 | } 55 | 56 | *ptr = 42; // 写入数据 57 | 58 | int *newPtr = (int*)realloc(ptr, sizeof(int) * 6); // 扩展内存大小 59 | if (newPtr == NULL) { // 检查是否成功 60 | free(ptr); // 如果失败,释放原始内存 61 | std::cerr << "Memory reallocation failed.\n"; 62 | return 1; 63 | } else { 64 | ptr = newPtr; // 更新指针 65 | } 66 | 67 | for(int i = 1; i <= 5; ++i) { 68 | ptr[i] = i * 10; // 使用扩展后的内存 69 | } 70 | 71 | // 使用完毕,释放内存 72 | free(ptr); 73 | return 0; 74 | } 75 | ``` 76 | 77 | ## 注意事项 78 | 79 | - 数据移动:调用realloc可能导致数据在内存中的位置发生变化,因此在调用后需要检查返回的指针是否与原指针相同。 80 | 81 | - 内存泄漏风险:如果realloc失败,原始内存不会被释放,需要在失败路径上手动处理释放。 82 | 83 | - 初始化:与malloc一样,realloc扩展或新分配的内存内容是未定义的,可能需要初始化。 84 | 85 | - C++实践:虽然realloc在某些场景下可用,但C++推荐使用容器(如std::vector)或智能指针(如std::unique_ptr配合自定义删除器)来更安全、高效地管理动态内存。这些机制能自动处理内存的扩展和收缩,并且与C++的异常安全机制兼容。 86 | 87 | ### malloc与realloc搭配使用,用法也有很多相似之处 88 | 89 | - realloc失败的时候,返回NULL。 90 | - realloc失败的时候,原来的内存不改变,不会释放也不会移动。 91 | - 假如原来的内存后面还有足够多剩余内存的话,realloc的内存=原来的内存+剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被自动 free 掉,realloc返回新内存的地址。 92 | - 如果size为0,效果等同于 free ()。这里需要注意的是只对 指针 本身进行释放,例如对二维指针**a,对a调用realloc时只会释放一维,使用时谨防 内存泄露 。 93 | - 传递给realloc的 指针 必须是先前通过 malloc (), calloc (), 或realloc()分配的。 94 | - 传递给realloc的 指针 可以为空,等同于 malloc 。 95 | -------------------------------------------------------------------------------- /c++/关键字与限定符/typedef.md: -------------------------------------------------------------------------------- 1 | # typedef 2 | 3 | C++中的`typedef`关键字用于为已存在的数据类型创建一个新的名字,也被称为类型别名。它并不创造新的数据类型,只是提供了一个额外的、可能更易读或更有意义的名字来引用原始类型。`typedef`的使用增强了代码的可读性和可维护性,尤其是在处理复杂类型声明时。 4 | 5 | ## 基本语法 6 | 7 | ```cpp 8 | typedef existing_type new_type_name; 9 | ``` 10 | 11 | - existing_type 是已存在的数据类型,可以是基本类型、结构体、联合体、指针类型、引用类型等。 12 | - new_type_name 是你为existing_type提供的新名称,即类型别名。 13 | 14 | ## 作用 15 | 16 | 1. 定义一种类型的别名 17 | - 注意typedef并不是简单的宏替换 18 | 19 | 2. 定义struct结构体别名 20 | 21 | 3. 用typedef来定义与平台无关的类型。 22 | 23 | 4. 为复杂的声明定义一个简单的别名 24 | 25 | ## typedef和#define的区别 26 | 27 | typedef是一种类型别名,而#define只是宏定义。二者并不总是可以互换的。 28 | 29 | ```cpp 30 | typedef char *pStr1; 31 | #define pStr2 char *; 32 | pStr1 s1, s2; 33 | pStr2 s3, s4; 34 | ``` 35 | 36 | 其中s1, s2, s3是char*类型,而s4是char类型。 37 | 38 | ## 例子 39 | 40 | 1. 基本类型别名 41 | 42 | ```cpp 43 | typedef int Integer; // 创建Integer作为int的别名 44 | Integer num = 10; // 现在可以使用Integer代替int 45 | ``` 46 | 47 | 2. 复杂类型简化 48 | 49 | ```cpp 50 | typedef int (*FuncPtr)(int, double); // 定义一个函数指针类型 51 | FuncPtr myFunction; // myFunction是一个指向接受int和double参数并返回int的函数的指针 52 | ``` 53 | 54 | 3. 结构体和类的别名 55 | 56 | ```cpp 57 | struct Point { 58 | int x, y; 59 | }; 60 | 61 | typedef Point Coord; // Coord作为Point的别名 62 | Coord p; // 等同于Point p; 63 | ``` 64 | 65 | ## C C++的使用差异 66 | 67 | 1. C使用typedef struct 可以定义结构体别名,以后忽略struct,直接使用别名。 68 | 69 | 2. C++即使没有使用typedef,你也可以在声明结构体变量时省略struct关键字直接使用结构体名称。这一特性是从C++的早期版本就开始支持的,旨在提高代码的可读性和简化语法。 70 | 71 | ## C++11的using关键字 72 | 73 | 从C++11开始,using关键字提供了另一种创建类型别名的方法,其语法更接近于变量声明,有时被认为更直观: 74 | 75 | ```cpp 76 | using Integer = int; 77 | using Coord = Point; 78 | ``` 79 | 80 | ## 优点 81 | 82 | 可读性:通过给复杂类型命名,使得代码更易于理解。 83 | 维护性:更改类型时,只需在一个地方修改,提高了代码的维护效率。 84 | 移植性:可以用来隐藏平台相关的类型,提升代码的跨平台能力。 85 | 86 | ## 注意事项 87 | 88 | - typedef不改变类型的基本性质,只是提供了另一种访问方式。 89 | - 使用typedef时,建议选择有意义的名称,以提高代码的自解释性。 90 | - typedef定义是全局的,但可以限制在命名空间或类内以减少污染全局命名空间。 91 | 92 | ## 有趣的代码 93 | 94 | ```cpp 95 | typedef char* pStr; 96 | const char* p1 = "hello"; 97 | const pStr p2 = "hello"; 98 | p1++;//正常 99 | p2++;//报错 100 | ``` 101 | 102 | - p1和p2都是常量指针,意思是指针指向的内容不能修改,而指针是可以修改的。 103 | 104 | 那为什么p1++正常,而p2++报错呢? 105 | 对于p1++,当我们对指针 `p1` 使用 `p1`++ 操作时,这意味着指针 `p1` 的值(即内存地址)将增加。由于 `p1` 是一个字符指针(char *),`p1++` 将使指针 `p1` 移动到下一个字符的地址。因为常量指针不能修改指向的对象的值,但指针本身的值是可变的。 106 | 而`p2`是我们定义的别名,而不是系统固有类型,编译器在编译时,会认为`p2`是常量,不可修改, 107 | 所以`p2++`会报错。 108 | -------------------------------------------------------------------------------- /c++/关键字与限定符/using.md: -------------------------------------------------------------------------------- 1 | # using 2 | 3 | 在C++中,`using`关键字有多种用途,它不仅用于类型别名(类似于C语言中的`typedef`),还涉及到命名空间、作用域解析以及继承中的特定用法。 4 | 5 | ## 类型别名(Type Aliases) 6 | 7 | 从`C++11`开始,`using`提供了一种更清晰、更接近于声明语法的方式来定义类型别名,相比typedef,它更易于阅读和理解。 8 | 9 | ```cpp 10 | using Integer = int; 11 | using Coordinate = Point3D; 12 | ``` 13 | 14 | 上面的代码分别定义了`Integer`作为`int`的别名,以及`Coordinate`作为`Point3D`的别名。 15 | 16 | ## 命名空间导入(Namespace Import) 17 | 18 | using namespace指令允许你将整个命名空间或者特定的命名空间成员引入当前作用域,从而避免了每次使用该命名空间成员时都需加上命名空间前缀。 19 | 20 | ```cpp 21 | using namespace std; // 导入std命名空间的所有内容,不推荐在全局作用域使用 22 | using std::cout; // 只导入std命名空间中的cout 23 | ``` 24 | 25 | ## 命名空间别名(Namespace Alias) 26 | 27 | 与类型别名类似,也可以为命名空间定义别名,这对于长命名空间路径特别有用。 28 | 29 | ```cpp 30 | namespace very_long_namespace_name { 31 | class MyClass {}; 32 | } 33 | 34 | using VLN = very_long_namespace_name; 35 | VLN::MyClass obj; 36 | ``` 37 | 38 | ## 作用域解析(Scope Resolution) 39 | 40 | 在类的继承和实现中,`using`声明可以用来指定基类的成员函数在派生类中的可见性,解决名称冲突或实现接口。 41 | 42 | ```cpp 43 | class Base { 44 | public: 45 | virtual void print() { cout << "Base" << endl; } 46 | }; 47 | 48 | class Derived : public Base { 49 | public: 50 | void print() override { cout << "Derived" << endl; } 51 | using Base::print; // 显式地将Base的print函数引入到Derived的作用域 52 | }; 53 | ``` 54 | 55 | ## 引入模板 56 | 57 | using也可以用于引入模板,特别是在类模板特化或偏特化时。 58 | 59 | ```cpp 60 | template 61 | class SmartPointer {}; 62 | 63 | // 引入模板别名 64 | template 65 | using MySmartPointer = SmartPointer; 66 | 67 | MySmartPointer ptr; 68 | ``` 69 | 70 | ## 总结 71 | 72 | `using`关键字在C++中是一个多功能的工具,它不仅简化了类型别名的定义,还在命名空间管理、作用域解析、类继承和模板使用等方面提供了便利。合理使用`using`能够显著提高代码的可读性和可维护性,但同时也需要注意避免过度使用导致的命名冲突或代码可读性下降。 73 | 74 | ## 区分 typedef #define using 75 | 76 | 1. typedef 77 | - 用途:主要用于为现有类型定义一个新的名字,即类型别名。它定义的是类型,因此具有类型检查的优势。 78 | - 语法:typedef existing_type new_type_name; 79 | - 类型安全:是类型安全的,因为编译器会检查类型匹配。 80 | - 作用域:遵循常规的变量作用域规则,可以在函数、类、命名空间或全局范围内定义。 81 | - 特性:不支持函数类型的定义,但C++11引入了typedef的增强版——using,可以用来定义函数类型别名。 82 | 83 | 2. #define 84 | - 用途:预处理器指令,用于简单的文本替换,可以定义常量、宏函数等。 85 | - 语法:#define MACRO_NAME replacement_text 86 | - 类型安全:不具备类型检查,容易引发错误,尤其是在复杂表达式和宏中。 87 | - 作用域:全局作用域,预处理器会在编译前进行替换,不受函数或文件作用域限制。 88 | - 特性:可以用于条件编译、字符串化、参数化宏等高级功能,但使用不当可能导致难以调试的问题。 89 | 90 | 3. using 91 | - 用途:C++11引入的新特性,既可以定义类型别名(类似于typedef,但语法更清晰),也可以导入命名空间成员,甚至定义模板别名。 92 | - 类型别名语法:using new_type_name = existing_type; 93 | - 命名空间导入:using namespace some_namespace; 或 using some_namespace::function_name; 94 | - 类型安全:类型安全,支持函数类型、类模板等更复杂的类型定义。 95 | - 作用域:与typedef类似,遵循正常的变量作用域规则。 96 | - 特性:提供更现代、更清晰的语法,尤其在处理模板和函数类型时更加灵活。 97 | 98 | 4. 小結 99 | - 类型定义:对于类型定义,using和typedef是类型安全的,而#define仅做文本替换,不保证类型安全。 100 | - 作用域:typedef和using遵循作用域规则,而#define具有全局作用域。 101 | - 灵活性和安全性:using提供了最现代和灵活的语法,同时保持类型安全;typedef较传统,主要用于类型别名;#define则因缺乏类型安全和作用域限制,应谨慎使用,尤其是在现代C++编程中。 102 | -------------------------------------------------------------------------------- /c++/关键字与限定符/volatile.md: -------------------------------------------------------------------------------- 1 | # volatile 2 | 3 | volatile 是C++和其他一些编程语言(如C)中的一个关键字,它用于修饰变量,以指示编译器该变量的值可能会在没有程序显式修改的情况下改变,例如由硬件、并发线程、信号处理器或其他不可预知的外部事件。 4 | 5 | ## 主要作用和特性 6 | 7 | 1. 禁止编译器优化 8 | - 避免值缓存:编译器通常会对代码进行优化,包括可能将变量的值缓存在寄存器中以提高访问速度。但若一个变量被声明为 volatile,编译器就必须每次都从内存中读取该变量的值,而不是使用可能已经过时的寄存器副本,确保读取的是最新值。 9 | - 防止指令重排序:在多线程或有中断的环境中,编译器和处理器可能会为了提高性能而对指令进行重排序。使用 volatile 可以防止这种重排序,确保相关代码按顺序执行。 10 | 2. 支持并发和多线程 11 | - 多线程共享数据:在多线程程序中,共享的变量应标记为 volatile 以确保所有线程看到的变量值是最新的,尤其是在没有锁或其他同步机制保护时。但需要注意,volatile 本身并不能保证原子性,对于复合操作(如++)仍需额外同步措施。 12 | - 信号处理:在处理异步信号时,与信号处理函数共享的变量通常需要声明为 volatile,以确保信号处理时能正确反映变量的最新状态。 13 | - **注意**:volatile不能保证多线程原子性,对于多线程的作用不大。 14 | 3. 与硬件交互 15 | - 硬件寄存器:访问硬件寄存器的软件通常使用 volatile 标记这些变量,因为硬件可能在任何时候改变寄存器的值。 16 | - 中断服务例程:在中断服务例程(ISR)中访问的变量也应声明为 volatile,确保中断发生时能够正确读取或更新这些变量。 17 | 4. 使用注意事项 18 | - 不等同于线程安全:虽然 volatile 能够保证变量的可见性,但它并不提供原子性保证,因此在多线程环境下对变量进行读写操作时,还需要使用互斥锁等同步工具来保证操作的原子性。 19 | - 不适用于所有并发控制:volatile 适合于简单读取硬件状态或标志位的场景,但对于复杂的数据结构操作或逻辑控制流控制,并不能替代传统的并发控制机制。 20 | - 使用成本:频繁访问的 volatile 变量可能会降低程序的性能,因为它阻止了编译器的一些优化。 21 | 总的来说,volatile 是一个强大的工具,用于处理程序中不受程序控制的变量变更情况,但在使用时需要仔细考虑其适用场景,并且明白它并不能替代所有的并发控制机制。 22 | 23 | 24 | -------------------------------------------------------------------------------- /c++/基本语法/函数.md: -------------------------------------------------------------------------------- 1 | # C++函数 2 | 3 | 函数是一组一起执行一个任务的语句。与C程序类似,每个 C++ 程序都至少有一个函数,即主函数 main(),通过函数,还可以把一个复杂任务分解成为若干个易于解决的小任务,充分体现结构化程序设计由粗到精,逐步细化的设计思想,即将任务合理划分为功能相对简单的若干子任务,分别进行设计调试,并通过某种机制将其连接成完整的程序,可以提高程序设计的效率。 4 | 5 | ## 函数基础 6 | 7 | ### 函数定义的基本结构 8 | 9 | 函数定义包括以下几个部分: 10 | 11 | * 返回类型:函数完成后返回给调用者的值的类型。如果没有返回值,使用void关键字。 12 | * 函数名:标识函数的名称,遵循C++的标识符命名规则。 13 | * 参数列表:函数可以接受零个或多个参数,参数之间用逗号分隔。每个参数包括类型和名称。如果没有参数,使用空的圆括号()。 14 | * 函数体:花括号{}包围的一段代码,实现了函数的具体功能。 15 | 16 | 语法示例: 17 | 18 | ```cpp 19 | 返回类型 函数名(参数类型 参数名, ...) { 20 | // 函数体 21 | // ... 22 | [return 返回值;] // 如果函数不是void类型,通常需要返回一个值 23 | } 24 | ``` 25 | 26 | 无参数无返回值的例子: 27 | 28 | ```cpp 29 | void greet() { 30 | std::cout << "Hello, World!" << std::endl; 31 | } 32 | ``` 33 | 34 | 有参数有返回值的例子: 35 | 36 | ```cpp 37 | int add(int a, int b) { 38 | return a + b; 39 | } 40 | ``` 41 | 42 | ### 参数传递方式 43 | 44 | C++支持两种参数传递方式: 45 | 46 | * 值传递:函数接收参数的一份副本,对副本的操作不会影响原变量。这是默认的方式。 47 | * 引用传递:通过引用或指针传递参数,允许函数直接修改实参的值。引用传递在参数前加&,指针传递则传递变量的地址。 48 | 49 | ### 返回类型 50 | 51 | * 基本类型:如int、double、char等。 52 | * 用户自定义类型:包括结构体、类的对象、指针、引用等。 53 | * void:表示函数不返回任何值。 54 | 55 | ### 函数命名规则 56 | 57 | * 遵循C++标识符命名规范,即只能由字母、数字和下划线组成,不能以数字开头。 58 | * 采用驼峰式命名或下划线分隔单词,以增强可读性,如calculateTotal或calculate_total。 59 | 60 | ### 函数重载 61 | 62 | C++支持函数重载,即在同一作用域内可以有多个同名函数,只要它们的参数列表不同(类型、数量或顺序不同)或具有不同的模板参数列表。 63 | 64 | ### 内联函数 65 | 66 | 使用inline关键字声明的函数,提示编译器尝试将函数体直接插入每次调用该函数的地方,以减少函数调用的开销,但是否真正内联取决于编译器。 67 | 68 | ### 递归函数 69 | 70 | 函数可以调用自身,称为递归函数。使用递归时要确保有一个明确的结束条件,防止无限循环。 71 | 72 | ## 函数 函数指针 仿函数 Lambda表达式 73 | 74 | * **所有能用函数指针的地方都可以直接使用函数本身,还可以使用仿函数,也也可以使用lambda** 75 | 76 | 1. 直接使用函数: 77 | 在C++11及以后的版本中,由于函数重载解析和类型推导机制,很多时候可以直接使用函数名称而不需要显式转换为函数指针。编译器会自动处理类型匹配问题。 78 | 79 | ```cpp 80 | template 81 | void functionToCall(Func func); 82 | 83 | // 直接传递函数 84 | functionToCall(myFunction); 85 | ``` 86 | 87 | 2. 函数指针: 88 | 函数指针是最基础的形式,它指向一个函数的地址,使得可以通过指针间接调用函数。如果一个函数的签名与要求的回调函数类型匹配,可以直接传递该函数的地址。 89 | 90 | ```cpp 91 | void functionToCall(void (*funcPtr)(int)); 92 | void myFunction(int x) { /*...*/ } 93 | 94 | // 调用时 95 | functionToCall(myFunction); 96 | ``` 97 | 98 | 3. 仿函数(Function Objects,也称为Functors): 99 | 仿函数是重载了()操作符的类的实例,可以像普通函数那样被调用。它们比函数指针更灵活,因为它们可以携带状态。 100 | 101 | ```cpp 102 | struct MyFunctor { 103 | void operator()(int x) { /*...*/ } 104 | }; 105 | 106 | // 使用仿函数 107 | MyFunctor functor; 108 | functionToCall(functor); 109 | ``` 110 | 111 | 4. Lambda表达式: 112 | Lambda表达式是C++11引入的匿名函数对象,可以在代码中直接定义并创建临时的仿函数对象。它们非常适合用于定义简短的、即时的函数逻辑,并且可以捕获上下文中的变量。 113 | 114 | ```cpp 115 | // 使用lambda表达式 116 | functionToCall([](int x) { /*...*/ }); 117 | ``` 118 | 119 | ### 函数(Function) 120 | 121 | * 定义:最基础的代码单元,用于执行特定任务。它有名字、返回类型、参数列表和函数体。 122 | * 特点:直接调用,可被重载,易于理解和调试。 123 | * 使用场景:当任务相对独立,且可能在多处被调用时。 124 | 125 | ### 函数指针(Function Pointer) 126 | 127 | * 定义:一个指向函数入口地址的指针变量,可以用来存储或传递函数地址。 128 | * 特点:允许将函数作为参数传递给其他函数,或者从函数中返回一个函数地址,增加了灵活性。 129 | * 语法:返回类型 (*指针变量名)(参数类型列表); 130 | * 使用场景:当需要动态决定调用哪个函数时,或在回调函数、排序算法中作为比较函数等。 131 | 132 | ### 3. 仿函数(Function Object,Functor) 133 | 134 | * 定义:一个重载了()操作符的类的对象,使得这个对象可以像函数一样被调用。 135 | * 特点:除了执行功能外,还可以携带状态,比普通函数更灵活。 136 | * 使用场景:在STL算法中作为策略传递,比如自定义的比较操作。 137 | 138 | ### 4. Lambda表达式(Lambda Expression) 139 | 140 | * 定义:匿名函数,可以在代码中直接定义和使用,常用于创建短小的、临时的功能性代码片段。 141 | * 特点: 142 | * 可以捕获周围作用域的变量(值捕获、引用捕获)。 143 | * 直接编写在调用位置,减少代码分散。 144 | * 支持自动类型推导,简洁明了。 145 | * 语法:`[捕获列表] (参数列表) -> 返回类型 { 函数体 };` 146 | * 使用场景:配合算法、在并发编程中定义任务、事件处理等,任何需要短小、即时定义函数的地方。\ 147 | 148 | ### 对比总结 149 | 150 | * 可读性:函数 > Lambda表达式 > 仿函数 > 函数指针(通常情况下,因为函数直接命名,而Lambda和仿函数可能隐藏逻辑,函数指针最不易理解) 151 | * 灵活性:Lambda表达式 & 仿函数 > 函数指针 > 函数(Lambda和仿函数可以携带状态,函数指针可以动态改变调用目标,而普通函数较为固定) 152 | * 复杂度:函数指针 > 仿函数 > Lambda表达式 > 函数(函数指针使用较为繁琐,Lambda和仿函数提供了高级特性,普通函数最简单直接) 153 | -------------------------------------------------------------------------------- /c++/数据类型/auto 类型推导.md: -------------------------------------------------------------------------------- 1 | # auto 类型推导 2 | auto 关键字:自动类型推导,编译器会在``编译期间``通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。 3 | 4 | 在C++中,auto关键字用于类型推导,这意味着编译器会自动根据初始值的类型来决定变量的类型。自从C++11起,auto成为了类型声明的关键特性之一,极大地提高了代码的可读性和灵活性。 5 | 6 | ## auto用法 7 | ### 基本用法 8 | ```cpp 9 | auto x = 42; // x 的类型为 int 10 | auto y = 3.14; // y 的类型为 double 11 | ``` 12 | auto让编译器根据初始化表达式的类型来推断变量的类型。 13 | 14 | ### 复杂类型推导 15 | ```cpp 16 | auto ptr = &x; // ptr 的类型为 int* 17 | auto& ref = y; // ref 的类型为 double& 18 | auto&& rr = ref; // rr 的类型为 double& 19 | ``` 20 | 对于指针、引用和右值引用,auto也能准确推导出正确的类型。特别是使用引用时,auto会保持引用的性质,不论是左值引用还是右值引用。 21 | 22 | ### 模板和泛型编程 23 | ```cpp 24 | template 25 | void printType(T param) { 26 | auto val = param; // val 的类型与 param 相同 27 | // ... 28 | } 29 | ``` 30 | 在模板函数中,auto可以帮助简化类型声明,使得函数更加通用。 31 | 32 | ### 初始化列表与类型推导 33 | ```cpp 34 | std::vector vec = {1, 2, 3}; 35 | auto list = {1, 2.0, 'a'}; // list 的类型为 std::initializer_list 36 | ``` 37 | 当使用初始化列表初始化auto变量时,推导规则可能会根据列表的内容推导出std::initializer_list类型。 38 | 39 | ### lambda表达式 40 | ```cpp 41 | auto lambda = [](int x) -> int { return x + 1; }; 42 | ``` 43 | auto同样适用于lambda表达式,允许编译器推导出合适的函数对象类型 44 | 45 | ### 注意事项 46 | auto声明的变量必须在声明时初始化,因为编译器需要初始值来推导类型。 47 | 当涉及到复杂类型或模板时,auto可以与decltype一起使用来更精确地控制类型推导过程。 48 | 在C++14中,auto还可以用于返回类型推导,简化了函数和lambda表达式的编写。 49 | 50 | ## auto 类型推导的原理 51 | auto类型推导的原理主要是基于编译时的静态分析,它利用编译器的类型检查能力来确定变量、返回值或表达式的类型。下面是其工作流程的几个关键步骤: 52 | 53 | 1. 分析初始化表达式:auto关键字用于声明一个变量时,编译器首先会分析紧随其后的初始化表达式。这个表达式可以是一个字面量、变量、函数调用、复杂表达式等。 54 | 55 | 2. 推导类型:编译器基于初始化表达式的结果类型来进行类型推导。例如,如果初始化表达式是一个整数值(如42),那么auto推导出的类型就是int。对于更复杂的表达式,编译器会考虑表达式求值过程中的类型转换规则。 56 | 57 | 3. 应用类型修饰符:如果在auto声明中还包含了类型修饰符,如const、volatile或引用符号&、右值引用符号&&,这些修饰符会影响最终推导出的类型。例如,const auto& ref = someValue;会推导出一个指向someValue类型const引用。 58 | 59 | 4. 模板和泛型编程中的类型推导:在模板函数或类中使用auto时,类型推导过程会结合模板参数推导和函数参数的实际类型来进行。例如,auto func(auto param)中的auto会根据调用时的实际参数类型进行推导。 60 | 61 | 5. 初始化列表特殊情况:当使用初始化列表(花括号{})初始化auto变量时,C++11规定编译器会推导出std::initializer_list类型,除非有上下文暗示了不同的类型。 62 | 63 | 6. 编译器生成类型:一旦类型被推导出来,编译器实际上会用这个确切的类型替换掉auto关键字,这意味着auto声明的变量仍然是静态类型化的,只是类型被隐式地确定。 64 | 65 | 7. 限制与注意事项:auto不能单独使用而没有初始化表达式,因为类型推导依赖于这个表达式。此外,auto的推导不会考虑变量的后续赋值操作,它只基于初始化表达式。 66 | 67 | - 通过这一系列的编译时分析和推导过程,auto提供了在不牺牲类型安全的前提下,编写更加简洁、灵活的代码的能力。 68 | 69 | -------------------------------------------------------------------------------- /c++/数据类型/decltype 类型推导.md: -------------------------------------------------------------------------------- 1 | # decltype 类型推导 2 | decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。 3 | 如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。 4 | 5 | ## decltype 和 auto 6 | ```cpp 7 | auto var = val1 + val2; 8 | decltype(val1 + val2) var1 = 0; 9 | ``` 10 | - auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。 11 | 12 | - auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。 13 | 14 | ## 基本原理 15 | 1. 编译时推导:decltype的类型推导发生在编译时期,它不会引起运行时开销,并且不会实际计算表达式的值,仅分析表达式来确定其类型。 16 | 17 | 2. 表达式分析:decltype接受一个表达式作为参数,然后返回该表达式的类型。这使得decltype能够推导出更广泛类型的表达式,包括那些不能直接用于初始化变量的表达式。 18 | 19 | ## 推导规则 20 | 1. 变量或成员访问:如果decltype的参数是一个未加括号的变量名或类成员访问表达式,那么decltype(exp)的结果就是去掉顶层const和volatile的exp的类型。例如,如果x是一个int类型的变量,则decltype(x)是int。 21 | 22 | 2. 函数调用:如果exp是一个函数调用表达式,那么decltype(exp)的结果就是该函数的返回类型。 23 | 24 | 3. 左值与右值:对于其他类型的表达式,如果exp是一个左值(可寻址的表达式,如变量名),则decltype(exp)的结果是该左值的引用类型(例如,对于变量x,decltype((x))给出的是int&)。如果exp是一个右值,则结果是其非引用类型。 25 | 26 | 4. 括号的影响:当表达式被括号包围时,即使原表达式是左值,也会被当作右值处理,除非表达式自身就是左值引用。例如,decltype((expr))通常会返回expr类型的右值引用,除非expr已经是一个左值引用。 27 | 28 | ## 应用场景 29 | - 复杂类型表达式的类型获取:在需要显式指定复杂类型或模板参数时,decltype可以帮助直接从表达式推导类型,避免手动书写类型。 30 | 31 | - 返回类型后置语法:在C++11中,结合auto关键字,decltype可以用来实现函数返回类型的延迟声明,即所谓的返回类型后置语法(trailing return type)。这对于模板函数尤其有用,当函数返回类型依赖于模板参数时。 32 | 33 | 总之,decltype是一种强大的类型推导工具,它在理解和运用现代C++编程中扮演着重要角色,尤其是在涉及复杂类型推导和泛型编程的场景下。 -------------------------------------------------------------------------------- /c++/数据类型/images/类C的内存分布和虚表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/数据类型/images/类C的内存分布和虚表.png -------------------------------------------------------------------------------- /c++/数据类型/initializer_list.md: -------------------------------------------------------------------------------- 1 | # initializer_list 2 | initializer_list是一个标准库类型,是一个代表数组的轻量级包装器,用于存储同一类型的常量对象的列表。主要用于方便地初始化聚合类型(如数组、向量)和在构造函数中接受不定数量的同类型参数。支持作为函数参数或类成员初始化列表使用,用于传递一个初始化元素列表。 3 | 4 | ## 定义与包含头文件 5 | 6 | initializer_list 是一个模板类,定义在头文件中。其声明如下: 7 | ```cpp 8 | template 9 | class initializer_list { 10 | public: 11 | const E* begin() const noexcept; 12 | const E* end() const noexcept; 13 | size_t size() const noexcept; 14 | 15 | private: 16 | const E* _M_array; 17 | size_t _M_len; 18 | }; 19 | ``` 20 | 这里,E 是元素的类型。initializer_list 实例包含两个私有成员:一个指向元素数组的指针 _M_array 和一个表示元素数量的大小 _M_len。提供了三个公有成员函数:begin() 返回指向第一个元素的指针,end() 返回指向最后一个元素之后位置的指针,size() 返回元素的数量。 21 | 22 | ## 特点 23 | - 不可变性:initializer_list中的元素是常量,一旦创建就不能修改。 24 | - 轻量级:它并不是一个完整的容器,而是一个简单的双向指针(指向元素的开始和结束)加上元素数量,没有分配额外的内存来存储元素。 25 | - 临时性:它通常用于临时目的,比如构造函数的参数传递,一旦构造函数完成,initializer_list 对象通常就会被销毁。 26 | - 隐式构造:当使用花括号 {} 初始化一个能够接受initializer_list 的对象时,编译器会自动创建一个initializer_list 对象。 27 | - 兼容性:它可以和容器的构造函数以及接受范围的算法很好地协同工作,提高了代码的简洁性和可读性。 28 | 29 | ## 用法: 30 | 31 | 1. std::initializer_list 类型对象是一个访问 const T 类型对象数组的轻量代理对象。 32 | 2. std::initializer_list 对象在这些时候自动构造: 33 | - 用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个std::initializer_list 参数 34 | - 以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数 35 | - 绑定花括号初始化器列表到 auto ,包括在范围 for 循环中 36 | 3. initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。 37 | 4. 底层数组不保证在原始 initializer_list 对象的生存期结束后继续存在。 std::initializer_list 的存储是未指定的(即它可以是自动、临时或静态只读内存,依赖场合)。 (C++14 前) 38 | 5. 底层数组是 const T[N] 类型的临时数组,其中每个元素都从原始初始化器列表的对应元素复制初始化(除非窄化转换非法)。底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期,恰如绑定引用到临时量(有例外,例如对于初始化非静态类成员)。底层数组可以分配在只读内存。 (C++14 起) 39 | 6. 若声明了 std::initializer_list 的显式或偏特化则程序为病式。 40 | 41 | ### 成员类型: 42 | 43 | | 成员类型 | 定义 | 44 | | --------------- | ---------- | 45 | | value_type | T | 46 | | reference | const T& | 47 | | const_reference | const T& | 48 | | size_type | std_size_t | 49 | | iterator | const T* | 50 | | const_iterator | const T* | 51 | 52 | ### 成员函数 53 | 54 | - 构造函数 55 | - initializer_list() noexcept; 56 | - constexpr initializer_list() noexcept; 57 | 58 | ```cpp 59 | #include 60 | #include 61 | int main(){ 62 | std::initializer_list empty_list; 63 | std::cout << "empty_list.size(): " << empty_list.size() << '\n'; 64 | 65 | // 用列表初始化创建初始化器列表 66 | std::initializer_list digits {1, 2, 3, 4, 5}; 67 | std::cout << "digits.size(): " << digits.size() << '\n'; 68 | 69 | // auto 的特殊规则表示‘ fraction ’拥有类型 70 | // type std::initializer_list 71 | auto fractions = {3.14159, 2.71828}; 72 | std::cout << "fractions.size(): " << fractions.size() << '\n'; 73 | } 74 | ``` 75 | 76 | - size :返回initializer_list中元素数目 77 | - begin:返回指向首元素的指针 78 | - end:返回指向末尾元素后一位置的指针 79 | 80 | ### 非成员函数 81 | 82 | | 成员类型 | 定义 | 83 | | ----------------------------------------- | ------------------------- | 84 | | std::begin(std::initializer_list) (C++11) | 特化 std::begin | 85 | | std::end(std::initializer_list)(C++11) | 定义于头文件 特化std::end | 86 | | rbegin(std::initializer_list) (C++14) | 特化std::rbegin | 87 | | rend(std::initializer_list)(C++14) | 特化std::rend | 88 | 89 | ```cpp 90 | #include 91 | #include 92 | #include 93 | 94 | template 95 | struct S { 96 | std::vector v; 97 | S(std::initializer_list l) : v(l) { 98 | std::cout << "constructed with a " << l.size() << "-element list\n"; 99 | } 100 | void append(std::initializer_list l) { 101 | v.insert(v.end(), l.begin(), l.end()); 102 | } 103 | std::pair c_arr() const { 104 | return {&v[0], v.size()}; // 在 return 语句中复制列表初始化 105 | // 这不使用 std::initializer_list 106 | } 107 | }; 108 | 109 | template 110 | void templated_fn(T) {} 111 | 112 | int main(){ 113 | S s = {1, 2, 3, 4, 5}; // 复制初始化 114 | s.append({6, 7, 8}); // 函数调用中的列表初始化 115 | 116 | std::cout << "The vector size is now " << s.c_arr().second << " ints:\n"; 117 | 118 | for (auto n : s.v) 119 | std::cout << n << ' '; 120 | std::cout << '\n'; 121 | std::cout << "Range-for over brace-init-list: \n"; 122 | 123 | for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作 124 | std::cout << x << ' '; 125 | std::cout << '\n'; 126 | 127 | auto al = {10, 11, 12}; // auto 的特殊规则 128 | 129 | std::cout << "The list bound to auto has size() = " << al.size() << '\n'; 130 | 131 | // templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式, 132 | // 它无类型,故 T 无法推导 133 | templated_fn>({1, 2, 3}); // OK 134 | templated_fn>({1, 2, 3}); // 也 OK 135 | } 136 | ``` 137 | 138 | ## 使用场景 139 | 1. 构造函数参数:当定义类时,可以通过接受initializer_list 参数来允许列表初始化。 140 | ```cpp 141 | class MyClass { 142 | public: 143 | MyClass(std::initializer_list list) : data(list) {} 144 | private: 145 | std::vector data; 146 | }; 147 | 148 | MyClass obj {1, 2, 3, 4}; // 使用initializer_list进行初始化 149 | ``` 150 | 151 | 2. 函数参数:可以接受不定数量的同类型参数,使函数调用更加灵活。 152 | ```cpp 153 | void printIntegers(std::initializer_list list) { 154 | for (int i : list) { 155 | std::cout << i << " "; 156 | } 157 | } 158 | 159 | printIntegers({1, 2, 3}); // 输出: 1 2 3 160 | ``` 161 | 162 | 3. 配合其他容器:可以直接用于初始化标准库中的容器,如std::vector、std::array等。 163 | ```cpp 164 | std::vector vec = {1, 2, 3, 4, 5}; 165 | ``` 166 | 167 | ## 示例 168 | 1. 初始化容器 169 | ```cpp 170 | std::vector vec = {1, 2, 3, 4, 5}; // 使用initializer_list初始化vector 171 | ``` 172 | 173 | 2. 函数参数 174 | ```cpp 175 | void printElements(std::initializer_list elems) { 176 | for (auto elem : elems) { 177 | std::cout << elem << " "; 178 | } 179 | } 180 | 181 | printElements({10, 20, 30}); // 调用函数时使用initializer_list 182 | ``` 183 | 184 | ## 优缺点 185 | - 优点: 186 | - 简洁性:提供了一种简洁的语法来初始化数据结构,提高了代码的清晰度和可读性。 187 | 简化了构造函数和函数的参数传递过程。 188 | - 灵活性:可以在编译时确定元素数量,适用于多种数据类型的初始化。支持泛型编程,增加了代码的灵活性。 189 | 190 | - 缺点: 191 | - 不可变性:由于元素是常量,初始化后无法修改,这限制了它的应用场景。 192 | - 性能考量:对于大型数据集,复制整个列表可能会成为性能瓶颈,尤其是在频繁调用接受initializer_list的函数时。 193 | 194 | ## 注意事项 195 | - 当initializer_list作为函数参数时,它会被拷贝给函数,即使在函数内部不修改列表,拷贝也可能涉及大量数据。 196 | - 对于大型数据初始化,考虑直接使用迭代器或区间等其他方式,以避免不必要的拷贝。 -------------------------------------------------------------------------------- /c++/数据类型/lambda 表达式.md: -------------------------------------------------------------------------------- 1 | # lambda 表达式 2 | lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。 3 | 4 | ## lambda匿名函数的定义: 5 | ```cpp 6 | [capture list] (parameter list) -> return type 7 | { 8 | function body; 9 | }; 10 | ``` 11 | 其中: 12 | - capture-list(捕获列表): 定义了lambda函数体中可以访问的外部变量的方式。可以是按值捕获(=, 默认)、按引用捕获(&)、或明确指定哪些变量按值或按引用捕获([x, &y])。 13 | - parameters(参数列表): 和普通函数一样,定义了lambda接收的参数。 14 | - return-type(返回类型): 可选,自C++14起可以直接省略,编译器会根据函数体自动推导返回类型。如果显式指定,需使用->后跟类型。 15 | - function-body(函数体): 包含了lambda表达式执行的具体代码。 16 | 17 | ## 示例 18 | 1. 简单Lambda(无参数,无返回值) 19 | ```cpp 20 | []{ std::cout << "Hello, World!\n"; }() 21 | ``` 22 | 23 | 2. 带参数Lambda(有一个整型参数,无返回值) 24 | ```cpp 25 | [](int x){ std::cout << "The value is: " << x << '\n'; }(42); 26 | ``` 27 | 28 | 3. 有返回类型Lambda 29 | ```cpp 30 | auto add = [](int a, int b) -> int { return a + b; }; 31 | std::cout << add(2, 3) << '\n'; // 输出5 32 | ``` 33 | 34 | 4. 捕获外部变量(按值捕获x,按引用捕获y) 35 | ```cpp 36 | int x = 10, y = 20; 37 | auto lambda = [=, &y]{ return x + y++; }; 38 | std::cout << lambda() << '\n'; // 输出30 39 | std::cout << lambda() << '\n'; // 输出31,因为y是按引用捕获的 40 | ``` 41 | 42 | ## sort使用lambda自定义排序 43 | 44 | ```cpp 45 | #include 46 | #include 47 | using namespace std; 48 | 49 | int main(){ 50 | int arr[4] = {4, 2, 3, 1}; 51 | //对 a 数组中的元素进行升序排序 52 | sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } ); 53 | for(int n : arr){ 54 | cout << n << " "; 55 | } 56 | return 0; 57 | } 58 | ``` 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /c++/数据类型/函数对象-仿函数.md: -------------------------------------------------------------------------------- 1 | # 函数对象 2 | 函数对象(Function Object),也被称为仿函数(Functor),是C++中一种重要的编程概念,它允许用户自定义类型像函数那样被调用。函数对象可以是普通的类对象,但通过重载其operator(),使其成为一个可调用的对象。这种设计不仅灵活,还能利用类的特性(如数据成员、继承、多态)来增强功能,使得函数对象成为函数式编程和泛型编程中的强大工具。 3 | 4 | ## 基本概念 5 | 1. 重载operator():函数对象的核心在于重载了调用运算符operator(),这使得对象能够像函数那样被调用。例如: 6 | ```Cpp 7 | class Adder { 8 | public: 9 | int operator()(int a, int b) const { 10 | return a + b; 11 | } 12 | }; 13 | ``` 14 | 上面的Adder类就是一个简单的函数对象,通过Adder对象可以调用operator()来执行加法操作。 15 | 16 | 2. 数据成员:与普通函数不同,函数对象可以拥有状态,即数据成员。这意味着函数对象的行为可以根据其内部状态而变化。 17 | 18 | 3. 作为参数传递:由于函数对象是对象,因此可以作为参数传递给其他函数或存储在容器中,这在算法定制和策略模式中尤为有用。 19 | 20 | ### 简单函数对象/仿函数 21 | ```cpp 22 | #include 23 | 24 | class Square Functor { 25 | public: 26 | int operator()(int x) const { 27 | return x * x; 28 | } 29 | }; 30 | 31 | int main() { 32 | Square Functor square; 33 | std::cout << "5 squared is: " << square(5) << std::endl; // 输出: 5 squared is: 25 34 | return 0; 35 | } 36 | ``` 37 | 38 | ## 仿函数的特点 39 | 1. 类结构:仿函数是一个类,它通过重载函数调用操作符operator()来模拟函数的行为。 40 | 2. 携带状态:与普通函数不同,仿函数可以是带有成员变量的类实例,因此它们能够保存状态信息,这是普通函数所不具备的能力。 41 | 3. 重用性和多态性:由于仿函数是类的实例,因此它们可以作为参数传递给其他函数,存储在容器中,或作为算法的一部分,这为代码的复用和多态提供了便利。 42 | 4. 泛型编程:在C++ STL(标准模板库)中,仿函数常用于算法中,作为算法行为的自定义策略,如std::sort可以通过仿函数来指定排序规则。 43 | 44 | 45 | ## 优点 46 | - 灵活性:通过封装状态和行为,函数对象可以提供更灵活的解决方案。 47 | - 类型安全:相比于函数指针,函数对象提供更好的类型安全,尤其是在使用模板时。 48 | - 多态性:利用继承和虚函数,可以实现函数对象的多态行为。 49 | - 可重用性:函数对象作为类实现,便于复用和扩展。 50 | 51 | ## C++ STL中的仿函数 52 | C++标准库中有许多预定义的仿函数类,位于````头文件中,比如``std::plus, std::minus, std::bind``, 以及C++11引入的``lambda``表达式,它们也可以视为一种匿名的仿函数。 53 | 54 | ## 实例应用 55 | 1. 标准库算法:C++标准库中的许多算法,如std::sort、std::find_if等,都接受函数对象作为参数,用于定制行为。 56 | ```cpp 57 | std::vector vec = {1, 2, 3, 4, 5}; 58 | std::sort(vec.begin(), vec.end(), std::greater()); // 使用std::greater作为比较函数对象 59 | ``` 60 | 61 | 2. Lambda表达式:C++11引入了Lambda表达式,它是一种更简洁的创建匿名函数对象的方式。Lambda表达式可以直接在代码中定义,并且可以捕获外部变量。 62 | ```cpp 63 | auto addFive = [](int x) { return x + 5; }; // Lambda表达式定义的函数对象 64 | std::cout << addFive(10) << std::endl; // 输出15 65 | ``` 66 | 67 | 3. 策略模式:在策略模式中,函数对象可以作为策略接口,实现算法或策略的可插拔。 68 | 69 | ## 注意事项 70 | - 性能:虽然函数对象提供了额外的功能,但在某些情况下,直接使用内联函数或模板元编程可能提供更好的性能。 71 | - 复杂性:过度使用函数对象可能会增加代码的理解难度,特别是当函数对象的状态和行为变得复杂时。 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /c++/数据类型/函数指针.md: -------------------------------------------------------------------------------- 1 | # 函数指针 2 | 函数指针是C++中一个重要的概念,它允许你将函数像普通变量那样存储和传递。函数指针可以用来指向特定的函数,使得在运行时能够动态地选择和调用不同的函数,这种特性在编写灵活和可扩展的代码时非常有用。 3 | 4 | ## 定义函数指针 5 | 函数指针的定义包括函数的返回类型、指针声明符 (*)、指针变量名以及函数参数列表。基本格式如下: 6 | ```cpp 7 | 返回类型 (*指针变量名)(参数类型列表); 8 | ``` 9 | 10 | 例如,指向一个无参数返回整数的函数的指针定义如下: 11 | ```cpp 12 | int (*ptrToFunction)(); 13 | ``` 14 | 15 | ## 初始化函数指针 16 | 你可以将一个函数的地址赋给函数指针变量,就像给普通指针变量赋值一样。但要注意,赋值时只写函数名(不加括号和参数): 17 | ```cpp 18 | int someFunction() { return 42; } 19 | 20 | int (*ptrToFunction)() = someFunction; // 正确的赋值 21 | ``` 22 | 23 | 对于有参数的函数,赋值时同样不加括号和参数: 24 | ```cpp 25 | int add(int a, int b) { return a + b; } 26 | 27 | int (*addPtr)(int, int) = add; // 正确的赋值 28 | ``` 29 | 30 | ## 通过函数指针调用函数 31 | 一旦函数指针被初始化,你就可以像通过函数名一样通过指针来调用函数: 32 | ```cpp 33 | int result = ptrToFunction(); // 调用someFunction 34 | int sum = addPtr(1, 2); // 调用add函数 35 | ``` 36 | 37 | ## 函数指针作为参数 38 | 函数指针的一个重要用途是作为其他函数的参数,这样可以实现更灵活的回调机制或策略模式: 39 | ```cpp 40 | void applyOperation(int (*operation)(int, int), int a, int b) { 41 | int result = operation(a, b); 42 | // 处理结果... 43 | } 44 | 45 | // 现在可以传入任何符合签名的函数 46 | applyOperation(add, 10, 20); // 使用add函数 47 | ``` 48 | 49 | ## 函数指针数组和指针 50 | 你还可以定义指向函数的数组或指针,这样就可以在运行时选择调用不同的函数: 51 | ```cpp 52 | int (*functions[])(int) = {add, subtract, multiply}; // 假设存在subtract和multiply函数 53 | int result = functions[1](5, 3); // 调用subtract函数 54 | ``` 55 | 56 | ## Lambda表达式与函数指针 57 | C++11引入了lambda表达式,虽然lambda表达式自身不能直接赋值给函数指针,但你可以使用函数对象或者std::function来达到类似的目的。 58 | 59 | ## 注意事项 60 | - 类成员函数的函数指针需要额外的处理,因为成员函数有一个隐含的this参数。你需要定义一个指向成员函数的指针时,需要使用特殊的语法,并且在调用时提供一个指向对象的指针或引用来绑定this。 61 | 62 | - 函数指针的类型必须与所指向的函数类型完全匹配,包括返回类型和参数类型。 63 | 64 | 函数指针是C++中一种强大的工具,它在事件驱动编程、策略模式、回调函数等场景中有着广泛的应用。 65 | 66 | ## 区分函数指针 和 指针函数 67 | 纯粹是翻译的问题,导致的二义性 68 | 69 | 函数指针:指针指向函数的首地址,通过函数指针访问函数 70 | 71 | 指针函数;返回值是函数的指针 -------------------------------------------------------------------------------- /c++/数据类型/列表初始化.md: -------------------------------------------------------------------------------- 1 | # 列表初始化 list_initialization 2 | 3 | 列表初始化是一种 C++11 中引入的新特性,可用来初始化各种类型的对象,包括数组、结构体、类以及容器等。具体来说,列表初始化使用花括号 {} 将一组值括起来,用逗号分隔各个元素,形成一个初始化列表; 4 | 5 | ## { }初始化 6 | 7 | 在C++98中,标准允许使用{}对数组元素进行统一的列表初始化设定;但对一些自定义类型不可这样使用,无法编译,导致每次定义vector时,都需要先定义vector,在循环对其赋初始值,非常不方便; 8 | 9 | ```cpp 10 | int arr[] = {1, 2, 3, 4, 5}; 11 | int arr[5] = {0}; 12 | //c++98中不可这样使用,需循环赋值 13 | vector v = {1, 2, 3, 4, 5}; 14 | ``` 15 | 16 | C++11扩大了用大括号{}括起来的列表(初始化)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加; 17 | 18 | 编译器会对大括号中的元素进行类型检查,不符合要求即报错; 19 | 20 | - 元素类型与被初始化对象类型匹配; 21 | - 对自定义类型大括号内的元素个数与被初始化对象构造函数参数个数匹配; 22 | 23 | ```cpp 24 | //内置类型 25 | int x = 10; 26 | int x = {10}; 27 | int x{10}; 28 | 29 | int x = 1+2; 30 | int x = {1+2}; 31 | int x{1+2}; 32 | 33 | //数组 34 | int arr[5] = {1,2,3,4,5}; 35 | int arr[] = {1,2,3,4,5}; 36 | int arr[]{1,2,3,4,5}; 37 | 38 | //动态数组,C++98不支持 39 | int* parr = new int[5]{1,2,3,4,5}; 40 | int* parr = new int[]{1,2,3,4,5}; 41 | 42 | //标准容器 43 | vector v1{ 1, 2, 3, 4, 5 }; 44 | vector v2={ 1, 2, 3, 4, 5 }; 45 | map m{{1,1},{2,2},{3,3},{4,4}}; 46 | ``` 47 | 48 | ## 类列表初始化 49 | 50 | - 编译器尝试匹配构造函数,该构造函数参数列表与大括号内元素个数一致; 51 | 52 | ```cpp 53 | class Point 54 | { 55 | public: 56 | Point(int x=0,int y=0) 57 | :_x(x) 58 | ,_y(y) 59 | {} 60 | private: 61 | int _x; 62 | int _y; 63 | }; 64 | 65 | int main() 66 | { 67 | //以下等价 68 | Point p(1, 2); 69 | Point p1 = { 1, 2 }; 70 | Point p2{ 1, 2 }; 71 | } 72 | ``` 73 | 74 | ### C++列表初始化顺序 75 | 76 | - 成员变量声明顺序:无论在构造函数初始化列表中成员的书写顺序如何,成员变量总是按照它们在类定义中声明的顺序进行初始化。这意味着**成员变量的初始化顺序仅与其在类中的声明顺序有关,而非初始化列表中的顺序。** 77 | 78 | ```cpp 79 | class MyClass { 80 | private: 81 | int n1; 82 | int n2; 83 | public: 84 | MyClass(int value) : n2(value), n1(n2 * 2) {} // 注意初始化列表中n2在n1之前 85 | }; 86 | ``` 87 | 88 | ### 列表初始化只能用于构造函数 89 | 90 | 在构造函数中,使用冒号后跟成员变量名和初始化值的方式,即构造函数初始化列表,来直接初始化成员变量。这是初始化成员变量的推荐做法。 91 | 92 | 对于**没有默认构造函数的类类型成员**,或者希**望直接控制初始化过程**(比如调用特定的构造函数),**必须在初始化列表中进行初始化**。 93 | 94 | ### 常量和引用成员 95 | 96 | 常量成员和引用成员必须在初始化列表中初始化,因为它们不能被赋值,只能在创建时初始化。 97 | 98 | ### 注意 99 | 100 | 1. 避免重复初始化:每个成员变量在初始化列表中只能出现一次。重复初始化会导致编译错误或未定义行为。 101 | 2. 性能考量:直接使用初始化列表而非构造函数体内赋值可以提高效率,尤其是对于非POD(Plain Old Data)类型,因为它直接调用对应的构造函数,避免了默认构造后再赋值的额外开销。 102 | 3. 统一初始化风格:在C++11之后,统一初始化语法(使用大括号 {})可以用于大多数类型的初始化,包括基本类型、聚合类型、容器等,这有助于保持代码的一致性和清晰度。 103 | 4. 默认初始化与值初始化:不显式提供初始化值时,某些类型(如内置类型)可能默认初始化为未定义值,而使用= {}(值初始化)可以确保内置类型被零初始化。 104 | 5. 继承与基类初始化:在派生类的构造函数初始化列表中,必须首先初始化基类,然后按照声明顺序初始化派生类的成员变量。 105 | 6. 列表初始化与窄化转换:列表初始化(尤其是使用大括号 {})不会进行窄化转换(即不会自动将一个范围较大的类型转换为范围较小的类型,除非显式强制转换),这有助于避免意外的数据丢失。 106 | 7. C++11以后的改进:C++11引入了统一初始化语法,使得列表初始化更加通用和强大,能够应用于更多场景,包括直接列表初始化数组和类对象,以及利用auto关键字进行类型推导时的初始化。 107 | 108 | ## 类模板列表初始化 109 | 110 | - 要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数; 111 | - initialier_list是系统自定义的类模板,该类模板中主要有三个方法,begin()、end()迭代器,以及获取区间中元素个数的方法size(); 112 | 113 | ```cpp 114 | //库定义 115 | template 116 | class initializer_list 117 | { 118 | public: 119 | ... 120 | constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {} 121 | constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept 122 | : _First(_First_arg), _Last(_Last_arg) {} 123 | _NODISCARD constexpr const _Elem* begin() const noexcept {return _First;} 124 | _NODISCARD constexpr const _Elem* end() const noexcept {return _Last;} 125 | _NODISCARD constexpr size_t size() const noexcept {return static_cast(_Last - _First);} 126 | private: 127 | const _Elem* _First; 128 | const _Elem* _Last; 129 | }; 130 | ``` 131 | 132 | ### 库模板初始化列表实现 133 | 134 | - 调用对应的初始化列表构造函数; 135 | - 然后将初始化列表值循环赋值对象; 136 | 137 | ```cpp 138 | //vector库模板,初始化列表构造函数 139 | _CONSTEXPR20_CONTAINER vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc()) 140 | : _Mypair(_One_then_variadic_args_t{}, _Al) 141 | { 142 | auto&& _Alproxy = _GET_PROXY_ALLOCATOR(_Alty, _Getal()); 143 | _Container_proxy_ptr<_Alty> _Proxy(_Alproxy, _Mypair._Myval2); 144 | //初始化列表赋值 145 | _Range_construct_or_tidy(_Ilist.begin(), _Ilist.end(), random_access_iterator_tag{}); 146 | _Proxy._Release(); 147 | } 148 | 149 | template 150 | _CONSTEXPR20_CONTAINER void _Range_construct_or_tidy(_Iter _First, _Iter _Last, input_iterator_tag) { 151 | _Tidy_guard _Guard{ this }; 152 | for (; _First != _Last; ++_First) { 153 | emplace_back(*_First); // performance note: emplace_back()'s strong guarantee is unnecessary here 154 | } 155 | _Guard._Target = nullptr; 156 | } 157 | ``` 158 | 159 | ### 模拟模板初始化列表实现 160 | 161 | ```cpp 162 | 163 | #include 164 | template 165 | class MyVector 166 | { 167 | public: 168 | MyVector(std::initializer_list lt) 169 | :_capacity(lt.size()) 170 | ,_size(0) 171 | { 172 | _array = new T[_capacity]; 173 | for (auto e : lt) 174 | _array[_size++] = e; 175 | } 176 | 177 | MyVector& operator=(std::initializer_list lt) 178 | { 179 | delete[] _array; 180 | size_t i = 0; 181 | for (auto e : lt) 182 | _array[i++] = e; 183 | return *this; 184 | } 185 | private: 186 | T* _array; 187 | size_t _capacity; 188 | size_t _size; 189 | }; 190 | 191 | int main() 192 | { 193 | //以下三种形式等价 194 | MyVector v1({ 1,2,3 }); 195 | MyVector v2 = { 1,2,3 }; 196 | MyVector v3{ 1,2,3 }; 197 | //调用operator= 198 | v1 = { 3,2,1 }; 199 | return 0; 200 | } 201 | ``` 202 | -------------------------------------------------------------------------------- /c++/数据类型/引用.md: -------------------------------------------------------------------------------- 1 | # 引用 2 | 3 | C++中的引用是对另一个变量的别名,它允许你以另一个名字来引用同一个变量。引用在C++中是非常强大的特性,用于多种用途,包括函数参数传递、返回值优化、以及提供更直观和安全的内存访问方式。 4 | 5 | ## 引用的声明与初始化 6 | 7 | 引用必须在声明时被初始化,并且一旦初始化后,就不能改变引用的关联对象。声明引用的基本语法是: 8 | 9 | ```cpp 10 | type &reference_name = variable; 11 | 12 | 类型& 引用变量名(对象名) = 引用实体; 13 | ``` 14 | 15 | 这里,`type`是引用类型,`reference_name`是引用的名称,`variable`是要引用的变量。例如: 16 | 17 | ```cpp 18 | int x = 10; 19 | int &ref = x; // ref 是 x 的引用 20 | ``` 21 | 22 | ## 引用的特性 23 | 24 | 💦 引用在定义时必须初始化 25 | 26 | 💦 一个变量可以有多个引用 27 | 28 | 💦 一个引用可以绑定其他引用 29 | 30 | 💦 引用一旦绑定一个对象,再不能绑定其他对象 31 | 32 | 💦 引用可以绑定任何类型(对象),如变量、指针、对象。 33 | 34 | ## 引用的底层实现 35 | 36 | - 引用的底层实现是指针常量(常指针),即绑定对象不可改变的指针。 37 | - 引用本身自带const顶层修饰 38 | 39 | 编译器层面对于引用更多地表现为一个`重定向操作`。 40 | 41 | ## 常引用 42 | 43 | const引用,就是无法使用该别名修改原对象的值。 44 | 45 | - 对比引用本质的理解,常引用的本质就是,指向常量的常量指针,即既不能修改绑定对象的值,也不能重新再绑定新对象的指针。 46 | - const修饰引用,是底层修饰 47 | 48 | 使用场景: 49 | 50 | 1. 给常量取别名`const int& c = 20;` 51 | - 临时变量具有常性 52 | 2. 对权限控制的用处 53 | 1. 如若函数写出普通的引用,那么很多参数可能会传不过来,如临时变量。 54 | 55 | ## 引用与指针的区别 56 | 57 | 尽管引用和指针在某些方面表现相似,但它们之间有根本的区别: 58 | 59 | 1. 概念:引用概念上定义一个变量的别名,指针存储一个变量地址 60 | 2. 初始化:引用必须在定义时初始化,而指针可以在任何时候被初始化。 61 | 3. 空值:引用必须连接到合法的存储位置,不能为NULL或未定义;指针可以指向NULL。 62 | 4. 操作符:对引用的操作如同直接操作变量本身,不需要解引用操作(如*);而指针需要解引用才能访问目标变量。 63 | 5. 修改:一旦定义,引用就不能被重新绑定到另一个变量;指针可以改变指向。 64 | 6. 内存占用:引用本身不占用额外的内存空间,它只是目标变量的别名;指针本身占用内存空间(通常为4或8字节)。 65 | 7. `++`:引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 66 | 8. 有多级指针,但是没有多级引用 67 | 9. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 68 | 10. 引用比指针使用起来相对更安全 69 | 11. const引用:C++中可以有const引用,它不允许修改引用的对象,但可以引用const或非const对象;指针也有const指针,可以指向const或非const对象,但const指针自身是否可变是另外一回事。 70 | 71 | ## 引用的用途 72 | 73 | - 函数参数:使用引用作为函数参数可以避免复制大对象的成本,并且允许函数直接修改外部变量。 74 | - 交换两数:传值无法交换,使用引用和指针都可以实现 75 | - 单链表的头节点修改,以往需要二级指针,现在可以使用引用实现。 76 | - 函数返回值:返回引用可以避免复制,特别是当返回容器或大对象时非常有用,但需谨慎以避免返回局部变量的引用导致的未定义行为。 77 | - 传值返回:当需要将函数中的临时变量返回时,无论这个变量是在栈区、堆区或者静态区开辟空间,都会通过一个临时变量去充当返回值【小一点的话可能是寄存器eax,大一点可能是在上一层栈帧开好的】然后再返回给外界的值做接受。 78 | - 传引用返回(优化):对于像`静态变量`、`全局变量`等这些出了作用域不会销毁的对象,就可以使用【传引用返回】 79 | - 别名:为复杂的表达式或对象提供简洁的访问方式。 80 | - 迭代器:C++标准库中的迭代器常使用引用,使得可以像直接操作容器元素那样操作迭代器指向的元素。 81 | 82 | ## 注意事项 83 | 84 | - const与非const引用:非const引用只能绑定到同类型的非const对象,而const引用可以绑定到const或非const对象,甚至可以绑定到临时对象,这有助于实现诸如const char&的字符串字面量传递。 85 | - 避免悬空引用:确保引用的生命周期不长于它所引用的对象,否则会导致未定义行为。 86 | -------------------------------------------------------------------------------- /c++/数据类型/指针和引用.md: -------------------------------------------------------------------------------- 1 | # 指针和引用 2 | 3 | [指针](./指针.md) 4 | 5 | [引用](./引用.md) 6 | 7 | ## 指针和引用的区别 8 | 9 | - 1.本质:指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名 10 | - 2.多级指针:指针可以有多级,引用只有一级 11 | - 3.是否可空:指针可以为空,引用不能为NULL且在定义时必须初始化 12 | - 4.是否可变:指针在初始化后可以改变指向,而引用在初始化之后不可再改变 13 | - 5.sizeof():sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小 14 | - 6.占用空间:引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(指针大小是由当前CPU运行模式的寻址位数决定,而不是字长)。 15 | - 7.传值:当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。 16 | - 8.初始化和声明:引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。 17 | 18 | ## 在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢? 19 | 20 | - 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的 21 | - 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小 22 | - 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式 23 | 24 | ## 左值、右值、左值引用、右值引用 25 | 26 | 1. 左值:既能够出现在等号左边,也能出现在等号右边的变量。即左值是可寻址的变量,有持久性,能被修改的变量 27 | 2. 右值:只能出现在等号右边的变量。即右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。右值无法被修改 28 | 3. 左值引用:引用一个对象; 29 | 4. 右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。 30 | 31 | [左值和右值](../语言特性相关/左值和右值.md) 32 | 33 | ### (1)移动语义 34 | 35 | 右值引用的作用之一:移动语义(std::move),用来减少临时资源的开辟 36 | 37 | [移动语义](../语言特性相关/move函数与移动语义.md) 38 | 39 | ```c++ 40 | class Person { 41 | public: 42 | //默认构造函数 43 | Person() { 44 | m_Age = 0; 45 | m_Height = nullptr; 46 | } 47 | 48 | //拷贝构造函数 49 | Person(const Person& p) { 50 | m_Age = p.m_Age; 51 | m_Height = new int(*p.m_Height); //深拷贝,防止浅拷贝 52 | //cout << "拷贝构造函数申请的地址为:" << m_Height << endl; 53 | cout << "调用了拷贝构造函数" << endl; 54 | } 55 | 56 | //移动构造函数 57 | Person(Person&& p) noexcept { 58 | m_Age = p.m_Age; 59 | m_Height = p.m_Height; 60 | p.m_Height = nullptr; 61 | cout << "调用了移动构造函数" << endl; 62 | } 63 | 64 | //赋值运算符 65 | Person& operator=(Person& p) { 66 | m_Age = p.m_Age; 67 | m_Height = new int(*p.m_Height); 68 | //cout << "赋值运算符申请的地址为:" << m_Height << endl; 69 | return *this; 70 | } 71 | //移动赋值运算符 72 | Person& operator=(Person&& p) noexcept { 73 | //先自我检测,释放自身资源 74 | if (this != &p) { 75 | delete m_Height; 76 | } 77 | 78 | m_Age = p.m_Age; //接管p的资源 79 | m_Height = p.m_Height; 80 | 81 | p.m_Height = nullptr; //将p的资源置空 82 | } 83 | //有参构造函数 84 | Person(int age, int height) { 85 | m_Age = age; 86 | m_Height = new int(height); 87 | //cout << "有参构造函数申请的地址为:" << m_Height << endl; 88 | cout << "调用了有参构造函数" << endl; 89 | } 90 | //析构函数 91 | ~Person() { 92 | if (m_Height != nullptr) { 93 | delete m_Height; 94 | m_Height = nullptr; 95 | } 96 | } 97 | int m_Age; 98 | int* m_Height; 99 | }; 100 | 101 | int main(){ 102 | //传统的左值引用 103 | int a = 10; 104 | int& b = a; // 定义一个左值引用变量 105 | b = 20; // 通过左值引用修改引用内存的值 106 | 107 | //下面一行代码无法通过编译,因为等号右边的数无法取地址 108 | int &var = 10; 109 | 110 | //上面一行代码可以改成如下的常引用,理由上面已经说过 111 | const int& var = 10; 112 | 113 | //但改成常引用就无法修改var的值了,因此需要使用右值引用来解决问题 114 | //下面这行代码就能编译通过 115 | int&& var = 10; 116 | 117 | //并且右值引用也能改变值 118 | var = 1; 119 | 120 | vector vec; 121 | Person p1(20, 160); 122 | 123 | vec.push_back(p1); //p1会在传入参数时调用赋值构造函数被拷贝一次,之后马上销毁 124 | vec.push_back(std::move(p1)); //p1会被转换成右值,于是会调用移动构造函数,不会调用拷贝构造, 125 | //提升效率 126 | } 127 | ``` 128 | 129 | ### (2)完美转发 130 | 131 | [完美转发](完美转发.md) 132 | 133 | 右值引用的作用之二:完美转发 134 | 完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。 135 | 看下面这段代码 136 | 137 | ```c++ 138 | template 139 | void f(T&& x){ 140 | cout << ++x; 141 | } 142 | f(2); // 3 143 | ``` 144 | 145 | 函数f接受一个右值x,但是f(2)这条语句将2传入时,可以对2进行自增。这说明右值引用本身时左值,即x时左值。那么在传参时就会出现问题。看下面这段代码 146 | 147 | ```c++ 148 | template 149 | void g(T&& x) { 150 | cout << "右值" << endl; 151 | } 152 | 153 | template 154 | void g(T& x) { 155 | cout << "左值" << endl; 156 | } 157 | 158 | template 159 | void f(T&& x) { 160 | cout << "f右值引用" << endl; 161 | g(x); 162 | } 163 | template 164 | void f(T& x) { 165 | cout << "f左值引用" << endl; 166 | g(x); 167 | } 168 | ``` 169 | 170 | 一共有三个函数,f函数调用了g函数,g函数的作用是如果传入的参数是左值就输出左值,如果传入的参数是右值就输出右值。但是由于g函数的参数是通过f函数传入的(即x先通过外部的f函数传入,再由f函数传给g),经过第一次传参时x已经变为左值了(可以和第一个例子比对着看),所以g(x)永远只会输出左值(即永远只会和第二个函数模板匹配),这明显与我们想要的结果不匹配。那么我们可以这么写: 171 | 172 | ```c++ 173 | template 174 | void f(T&& x) { 175 | cout << "f右值引用" << endl; 176 | g(std::forward(x)); 177 | } 178 | template 179 | void f(T& x) { 180 | cout << "f左值引用" << endl; 181 | g(std::forward(x)); 182 | } 183 | ``` 184 | 185 | 符合我们的预期。std::forward函数保持了 x 的引用类型。 186 | 那么再结合移动语义想象一下这样的一个例子:你需要通过函数传入的参数进行一个赋值(或者拷贝操作),但是如果不保持参数的性质,虽然你为了减小开销,外面传入的是一个右值,但是一进入函数就会变成左值。举个例子: 187 | 回到之前的移动语义的例子中来,之前的代码不变,我们增加一个函数 188 | 189 | ```c++ 190 | void fun(Person&& p) { 191 | Person p2(p); 192 | } 193 | int main(int argc, char* argv[]) 194 | { 195 | Person p1(20, 160); 196 | fun(std::move(p1)); 197 | } 198 | ``` 199 | 200 | 我们在主函数中构造了P1对象,fun()函数本义是利用移动构造函数来减小复制次数,但是可以看到尽管我们在main函数中使用了右值,但是fun函数里任然使用了拷贝构造函数。我们做如下修改: 201 | 202 | ``` 203 | Person p2(std::forward(p)); 204 | ``` 205 | 206 | ### (3)new delete与malloc free 207 | 208 | 相同点:都能从堆上申请、释放空间 209 | 区别: 210 | 211 | 1. new与delete属于运算符,而malloc是函数 212 | 2. malloc只负责开辟空间,new不仅仅有malloc的功能,可以进行数据的初始化 213 | 3. malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常 214 | 4. free只会释放空间,而delete会先调用类的析构函数,在释放空间。 215 | 216 | [new](../关键字与限定符/new.md) 217 | [delete](../关键字与限定符/delete.md) 218 | [malloc](../关键字与限定符/malloc.md) 219 | [realloc](../关键字与限定符/realloc.md) 220 | [free](../关键字与限定符/free.md) -------------------------------------------------------------------------------- /c++/数据类型/枚举.md: -------------------------------------------------------------------------------- 1 | # 枚举类型: 2 | 3 | ## 枚举类型的定义 4 | 5 | **枚举类型**(enumeration)是C++中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。 6 | 如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。所谓"枚举"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内。 7 | 8 | ## 枚举格式 9 | 1. 定义格式一: 10 | ``enum <类型名> {<枚举常量表>};`` 11 | ```cpp 12 | enum color_set1 {RED, BLUE, WHITE, BLACK}; // 定义枚举类型color_set1 13 | enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 定义枚举类型week 14 | ``` 15 | 16 | 2. 定义格式二: 17 | ```cpp 18 | enum 枚举名 19 | { 20 | 标识符[=整型常数], 21 | 标识符[=整型常数], 22 | ... 23 | 标识符[=整型常数] 24 | }枚举变量; 25 | ``` 26 | 如果枚举没有初始化, 即省掉"=整型常数"时, 则从第一个标识符开始。默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。也可以给名称赋予一个特殊的值,只需要添加一个初始值即可。默认情况下,每个名称都会比它前面一个名称大 1。 27 | 28 | 29 | ## 枚举声明 30 | 1. 枚举类型定义与变量声明分开 31 | ```cpp 32 | enum Suit { Diamonds, Hearts, Clubs, Spades }; // 定义 33 | enum Suit a; // 声明 34 | enum Suit b,c; // 声明 35 | ``` 36 | 变量``a,b,c``的类型都定义为枚举类型``enum Suit``。 37 | 38 | 2. 枚举类型定义与变量声明同时进行 39 | ```cpp 40 | enum Suit { Diamonds, Hearts, Clubs, Spades }a,b,c; 41 | //此处类型名可以省略,如以下的声明也是可以的。 42 | enum { Diamonds, Hearts, Clubs, Spades }a,b,c; 43 | ``` 44 | 45 | 3. 用typedef先将枚举类型定义为别名,再利用别名进行变量的声明 46 | 有以下几种方式: 47 | ```cpp 48 | typedef enum Suit { Diamonds, Hearts, Clubs, Spades }Suit; 49 | enum Suit a; 50 | enum Suit b,c; 51 | ``` 52 | ```cpp 53 | typedef enum{ Diamonds, Hearts, Clubs, Spades }Suit; 54 | enum Suit a; 55 | enum Suit b,c; 56 | ``` 57 | ```cpp 58 | typedef enum Suit { Diamonds, Hearts, Clubs, Spades }; 59 | enum Suit a; 60 | enum Suit b,c; 61 | ``` 62 | >注意: 同一程序中不能定义同类型名的枚举类型;不同枚举类型的枚举元素不能同名。 63 | 枚举变量的值只能取枚举常量表中所列的值,就是整型数的一个子集。 枚举变量占用内存的大小与整型数相同。 64 | 枚举型可以隐式的转换为int型,int型不能隐式的转换为枚举型。 65 | 枚举变量只能参与赋值和关系运算以及输出操作,参与运算时用其本身的整数值。 66 | 67 | ## 使用枚举类型的变量 68 | ### 对枚举型的变量赋值。 69 | 实例将枚举类型的赋值与基本数据类型的赋值进行了对比: 70 | 71 | 1. 方法一:先声明变量,再对变量赋值 72 | ```cpp 73 | #include 74 | 75 | /* 定义枚举类型 */ 76 | enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; 77 | 78 | int main() 79 | { 80 | /* 使用基本数据类型声明变量,然后对变量赋值 */ 81 | int x, y, z; 82 | 83 | x = 10; 84 | y = 20; 85 | z = 30; 86 | 87 | /* 使用枚举类型声明变量,再对枚举型变量赋值 */ 88 | enum DAY yesterday, today, tomorrow; 89 | 90 | yesterday = MON; 91 | today = TUE; 92 | tomorrow = WED; 93 | 94 | printf("%d %d %d \n", yesterday, today, tomorrow); 95 | } 96 | ``` 97 | 98 | 2. 方法二:声明变量的同时赋初值 99 | ```cpp 100 | #include 101 | 102 | /* 定义枚举类型 */ 103 | enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; 104 | 105 | itn main() 106 | { 107 | /* 使用基本数据类型声明变量同时对变量赋初值 */ 108 | int x=10, y=20, z=30; 109 | 110 | /* 使用枚举类型声明变量同时对枚举型变量赋初值 */ 111 | enum DAY yesterday = MON, today = TUE,tomorrow = WED; 112 | 113 | printf("%d %d %d \n", yesterday, today, tomorrow); 114 | } 115 | ``` 116 | 117 | 3. 方法三:定义类型的同时声明变量,然后对变量赋值。 118 | ```cpp 119 | #include 120 | 121 | /* 定义枚举类型,同时声明该类型的三个变量,它们都为全局变量 */ 122 | enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN } yesterday, today, tomorrow; 123 | 124 | /* 定义三个具有基本数据类型的变量,它们都为全局变量 */ 125 | int x, y, z; 126 | 127 | int main() 128 | { 129 | /* 对基本数据类型的变量赋值 */ 130 | x = 10; y = 20; z = 30; 131 | 132 | /* 对枚举型的变量赋值 */ 133 | yesterday = MON; 134 | today = TUE; 135 | tomorrow = WED; 136 | 137 | printf("%d %d %d \n", x, y, z); //输出:10 20 30 138 | printf("%d %d %d \n", yesterday, today, tomorrow); //输出:1 2 3 139 | } 140 | ``` 141 | 4. 方法四:类型定义,变量声明,赋初值同时进行。 142 | ```cpp 143 | #include 144 | 145 | /* 定义枚举类型,同时声明该类型的三个变量,并赋初值。它们都为全局变量 */ 146 | enum DAY 147 | { 148 | MON=1, 149 | TUE, 150 | WED, 151 | THU, 152 | FRI, 153 | SAT, 154 | SUN 155 | } 156 | yesterday = MON, today = TUE, tomorrow = WED; 157 | 158 | /* 定义三个具有基本数据类型的变量,并赋初值。它们都为全局变量 */ 159 | int x = 10, y = 20, z = 30; 160 | 161 | int main() 162 | { 163 | printf("%d %d %d \n", x, y, z); //输出:10 20 30 164 | printf("%d %d %d \n", yesterday, today, tomorrow); //输出:1 2 3 165 | } 166 | ``` 167 | 168 | ### 对枚举型的变量赋整数值时,需要进行类型转换 169 | ```cpp 170 | #include 171 | 172 | enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; 173 | 174 | int main() 175 | { 176 | enum DAY yesterday, today, tomorrow; 177 | 178 | yesterday = TUE; 179 | today = (enum DAY) (yesterday + 1); //类型转换 180 | tomorrow = (enum DAY) 30; //类型转换 181 | //tomorrow = 3; //错误 182 | 183 | printf("%d %d %d \n", yesterday, today, tomorrow); //输出:2 3 30 184 | } 185 | ``` 186 | 187 | ### 举例:使用枚举型变量 188 | ```cpp 189 | #include 190 | 191 | enum 192 | { 193 | BELL = '\a', 194 | BACKSPACE = '\b', 195 | HTAB = '\t', 196 | RETURN = '\r', 197 | NEWLINE = '\n', 198 | VTAB = '\v', 199 | SPACE = ' ' 200 | }; 201 | 202 | enum BOOLEAN { FALSE = 0, TRUE } match_flag; 203 | 204 | int main() 205 | { 206 | int index = 0; 207 | int count_of_letter = 0; 208 | int count_of_space = 0; 209 | 210 | char str[] = "I'm Ely efod"; 211 | 212 | match_flag = FALSE; 213 | 214 | for(; str[index] != '\0'; index++) 215 | if( SPACE != str[index] ) 216 | count_of_letter++; 217 | else 218 | { 219 | match_flag = (enum BOOLEAN) 1; 220 | count_of_space++; 221 | } 222 | 223 | printf("%s %d times %c", match_flag ? "match" : "not match", count_of_space, NEWLINE); 224 | printf("count of letters: %d %c%c", count_of_letter, NEWLINE, RETURN); 225 | } 226 | ``` 227 | ```cpp 228 | 输出: 229 | match 2 times 230 | count of letters: 10 231 | Press any key to continue 232 | ``` 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /c++/数据类型/结构体.md: -------------------------------------------------------------------------------- 1 | # 结构体 2 | C++中的结构体(struct)是一种复合数据类型,它允许你将不同数据类型的多个成员(或称为字段)组合在一起,形成一个单一的实体。结构体在概念上类似于类(class),但默认情况下结构体内的成员是公有的(public),而类的成员默认为私有的(private)。 3 | 4 | ## 结构体定义 5 | 结构体定义的基本语法如下: 6 | ```cpp 7 | struct 结构体名称 { 8 | 数据类型 成员1; 9 | 数据类型 成员2; 10 | // ... 11 | 数据类型 成员n; 12 | }; 13 | ``` 14 | 15 | - 结构体名称:为结构体指定一个名称,用于标识这个类型。 16 | - 成员:结构体可以包含任意类型的数据成员,如基本数据类型(int, char, float等)、指针、甚至其他结构体或类的对象。 17 | 18 | ## 创建结构体变量 19 | 定义结构体之后,你可以创建该结构体类型的变量。有几种方式可以做到这一点: 20 | 1. 直接声明变量: 21 | ```cpp 22 | struct 结构体名称 变量名; 23 | ``` 24 | 25 | 2. 定义结构体时顺便创建变量: 26 | ```cpp 27 | struct 结构体名称 { 28 | // 成员定义 29 | } 变量名1, 变量名2; 30 | ``` 31 | 32 | 3. 初始化结构体变量: 33 | ```cpp 34 | struct 结构体名称 变量名 = { 成员1的初始值, 成员2的初始值, ... }; 35 | ``` 36 | 37 | ## 访问结构体成员 38 | 结构体成员可以通过点操作符(.)来访问: 39 | ```cpp 40 | 结构体变量名.成员名; 41 | ``` 42 | 43 | 例如: 44 | ```cpp 45 | struct Student { 46 | int age; 47 | std::string name; 48 | }; 49 | 50 | Student stu; 51 | stu.age = 20; 52 | stu.name = "Alice"; 53 | ``` 54 | 55 | ## 结构体的声明位置 56 | - 外部声明:将结构体的声明放在所有函数之外,这样定义的结构体可以在整个文件中被使用。 57 | - 内部声明:在某个函数内声明结构体,这样的结构体只能在该函数内部使用 58 | 59 | ## 结构体和类的区别 60 | 尽管结构体和类在语法上非常相似,但它们之间存在一些关键差异: 61 | - 默认访问权限不同:结构体默认成员访问权限是public,而类是private。 62 | - 语义区别:结构体倾向于用于简单的数据聚合,而类更常用于实现面向对象编程中的封装、继承和多态特性。 63 | 64 | ## 结构体作为参数和返回值 65 | 结构体可以作为函数的参数和返回值,这使得它们成为组织和传递复杂数据的理想选择。 66 | 67 | ## *6.10 如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等? 68 | 需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。 69 | 70 | 利用运算符重载来实现结构体对象的比较: 71 | ```cpp 72 | #include 73 | 74 | using namespace std; 75 | 76 | struct A 77 | { 78 | char c; 79 | int val; 80 | A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {} 81 | 82 | friend bool operator==(const A &tmp1, const A &tmp2); // 友元运算符重载函数 83 | }; 84 | 85 | bool operator==(const A &tmp1, const A &tmp2) 86 | { 87 | return (tmp1.c == tmp2.c && tmp1.val == tmp2.val); 88 | } 89 | 90 | int main() 91 | { 92 | A ex1('a', 90), ex2('b', 80); 93 | if (ex1 == ex2) 94 | cout << "ex1 == ex2" << endl; 95 | else 96 | cout << "ex1 != ex2" << endl; // 输出 97 | return 0; 98 | } 99 | 100 | ``` 101 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/Google.md: -------------------------------------------------------------------------------- 1 | # Google C++风格指南 中文版 2 | 3 | 本项目转载自[zh-google-styleguide](https://github.com/zh-google-styleguide)。 4 | 本人使用Markdown重新编辑,内容尽量保持一致,如果存在一些扩展和修改,我会特别标记。 5 | 6 | ## 目录 7 | 8 | [扉页](./index.md) 9 | [头文件](./headers.md) 10 | [作用域](./scoping.md) 11 | [类](./classes.md) 12 | [函数](./functions.md) 13 | [来自 Google 的奇技](./magic.md) 14 | [其他 C++ 特性](./others.md) 15 | [命名约定](./naming.md) 16 | [注释](./comments.md) 17 | [格式](./formatting.md) 18 | [规则特例](./exceptions.md) 19 | [结束语](./end.md) 20 | 21 | ## 一张图总结 Google C++编程规范(Google C++ Style Guide) 22 | 23 | ![一张图总结 Google C++编程规范](./images/一张图总结%20Google%20C++编程规范.png) 24 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/contents.md: -------------------------------------------------------------------------------- 1 | .. 请确保至少包含基本的 `toctree` 指令. 2 | 3 | .. _cpp_contents: 4 | 5 | C++ 风格指南 - 内容目录 6 | ======================================== 7 | 8 | .. contents:: 9 | :backlinks: none 10 | 11 | .. toctree:: 12 | 13 | index 14 | headers 15 | scoping 16 | classes 17 | functions 18 | magic 19 | others 20 | naming 21 | comments 22 | formatting 23 | exceptions 24 | end 25 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/end.md: -------------------------------------------------------------------------------- 1 | # 11. 结束语 2 | 3 | 运用常识和判断力, 并且 *保持一致*. 4 | 5 | 编辑代码时, 花点时间看看项目中的其它代码, 并熟悉其风格. 如果其它代码中 ``if`` 语句使用空格, 那么你也要使用. 如果其中的注释用星号 (*) 围成一个盒子状, 那么你同样要这么做. 6 | 7 | 风格指南的重点在于提供一个通用的编程规范, 这样大家可以把精力集中在实现内容而不是表现形式上. 我们展示的是一个总体的的风格规范, 但局部风格也很重要, 如果你在一个文件中新加的代码和原有代码风格相去甚远, 这就破坏了文件本身的整体美观, 也让打乱读者在阅读代码时的节奏, 所以要尽量避免. 8 | 9 | 好了, 关于编码风格写的够多了; 代码本身才更有趣. 尽情享受吧! 10 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/exceptions.md: -------------------------------------------------------------------------------- 1 | # 10. 规则特例 2 | 3 | 前面说明的编程习惯基本都是强制性的. 但所有优秀的规则都允许例外, 这里就是探讨这些特例. 4 | 5 | ## 10.1. 现有不合规范的代码 6 | 7 | **总述** 8 | 9 | 对于现有不符合既定编程风格的代码可以网开一面. 10 | 11 | **说明** 12 | 13 | 当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定. 如果不放心, 可以与代码原作者或现在的负责人员商讨. 记住, *一致性* 也包括原有的一致性. 14 | 15 | .. _windows-code: 16 | 17 | ## 10.2. Windows 代码 18 | 19 | **总述** 20 | 21 | Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南. 22 | 23 | **说明** 24 | 25 | 如果你习惯使用 Windows 编码风格, 这儿有必要重申一下某些你可能会忘记的指南: 26 | 27 | - 不要使用匈牙利命名法 (比如把整型变量命名成 ``iNum``). 使用 Google 命名约定, 包括对源文件使用 ``.cc`` 扩展名. 28 | 29 | - Windows 定义了很多原生类型的同义词 (YuleFox 注: 这一点, 我也很反感), 如 ``DWORD``, ``HANDLE`` 等等. 在调用 Windows API 时这是完全可以接受甚至鼓励的. 即使如此, 还是尽量使用原有的 C++ 类型, 例如使用 ``const TCHAR *`` 而不是 ``LPCTSTR``. 30 | 31 | - 使用 Microsoft Visual C++ 进行编译时, 将警告级别设置为 3 或更高, 并将所有警告(warnings)当作错误(errors)处理. 32 | 33 | - 不要使用 ``#pragma once``; 而应该使用 Google 的头文件保护规则. 头文件保护的路径应该相对于项目根目录 (Yang.Y 注: 如 ``#ifndef SRC_DIR_BAR_H_``, 参考 :ref:`#define 保护 ` 一节). 34 | 35 | - 除非万不得已, 不要使用任何非标准的扩展, 如 ``#pragma`` 和 ``__declspec``. 使用 ``__declspec(dllimport)`` 和 ``__declspec(dllexport)`` 是允许的, 但必须通过宏来使用, 比如 ``DLLIMPORT`` 和 ``DLLEXPORT``, 这样其他人在分享使用这些代码时可以很容易地禁用这些扩展. 36 | 37 | 然而, 在 Windows 上仍然有一些我们偶尔需要违反的规则: 38 | 39 | - 通常我们 :ref:`禁止使用多重继承 `, 但在使用 COM 和 ATL/WTL 类时可以使用多重继承. 为了实现 COM 或 ATL/WTL 类/接口, 你可能不得不使用多重实现继承. 40 | 41 | - 虽然代码中不应该使用异常, 但是在 ATL 和部分 STL(包括 Visual C++ 的 STL) 中异常被广泛使用. 使用 ATL 时, 应定义 ``_ATL_NO_EXCEPTIONS`` 以禁用异常. 你需要研究一下是否能够禁用 STL 的异常, 如果无法禁用, 可以启用编译器异常. (注意这只是为了编译 STL, 自己的代码里仍然不应当包含异常处理). 42 | 43 | - 通常为了利用头文件预编译, 每个每个源文件的开头都会包含一个名为 ``StdAfx.h`` 或 ``precompile.h`` 的文件. 为了使代码方便与其他项目共享, 请避免显式包含此文件 (除了在 ``precompile.cc`` 中), 使用 ``/FI`` 编译器选项以自动包含该文件. 44 | 45 | - 资源头文件通常命名为 ``resource.h`` 且只包含宏, 这一文件不需要遵守本风格指南. 46 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/functions.md: -------------------------------------------------------------------------------- 1 | # 4. 函数 2 | 3 | ## 4.1. 输入和输出 4 | 5 | ### 总述 6 | 7 | 我们倾向于按值返回, 否则按引用返回。 避免返回指针, 除非它可以为空. 8 | 9 | ### 说明 10 | 11 | C++ 函数由返回值提供天然的输出, 有时也通过输出参数(或输入/输出参数)提供. 我们倾向于使用返回值而不是输出参数: 它们提高了可读性, 并且通常提供相同或更好的性能. 12 | 13 | C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之. 非可选输入参数通常是值参或 ``const`` 引用, 非可选输出参数或输入/输出参数通常应该是引用 (不能为空). 对于可选的参数, 通常使用 ``std::optional`` 来表示可选的按值输入, 使用 ``const`` 指针来表示可选的其他输入. 使用非常量指针来表示可选输出和可选输入/输出参数. 14 | 15 | 避免定义需要 ``const`` 引用参数去超出生命周期的函数, 因为 ``const`` 引用参数与临时变量绑定. 相反, 要找到某种方法来消除生命周期要求 (例如, 通过复制参数), 或者通过 ``const`` 指针传递它并记录生命周期和非空要求. 16 | 17 | 在排序函数参数时, 将所有输入参数放在所有输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前. 18 | 19 | 这并非一个硬性规定. 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂. 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通. 20 | 21 | ## 4.2. 编写简短函数 22 | 23 | ### 总述 24 | 25 | 我们倾向于编写简短, 凝练的函数. 26 | 27 | ### 说明 28 | 29 | 我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割. 30 | 31 | 即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug. 使函数尽量简短, 以便于他人阅读和修改代码. 32 | 33 | 在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数. 34 | 35 | ## 4.3. 函数重载 36 | 37 | ### 总述* 38 | 39 | 若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数. 40 | 41 | ### 定义 42 | 43 | 你可以编写一个参数类型为 ``const string&`` 的函数, 然后用另一个参数类型为 ``const char*`` 的函数对其进行重载: 44 | 45 | ```cpp 46 | 47 | class MyClass { 48 | public: 49 | void Analyze(const string &text); 50 | void Analyze(const char *text, size_t textlen); 51 | }; 52 | ``` 53 | 54 | ### 优点 55 | 56 | 通过重载参数不同的同名函数, 可以令代码更加直观. 模板化代码需要重载, 这同时也能为使用者带来便利. 57 | 58 | ### 缺点 59 | 60 | 如果函数单靠不同的参数类型而重载 (acgtyrant 注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何. 另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑. 61 | 62 | ### 结论 63 | 64 | 如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 ``AppendString()`` 和 ``AppendInt()`` 等, 而不是一口气重载多个 ``Append()``. 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 ``std::vector`` 以便使用者可以用 :ref:`列表初始化 ` 指定参数. 65 | 66 | ## 4.4. 缺省参数 67 | 68 | ### 总述 69 | 70 | 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 :ref:`函数重载 ` 遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下. 71 | 72 | ### 优点 73 | 74 | 有些函数一般情况下使用默认参数, 但有时需要又使用非默认的参数. 缺省参数为这样的情形提供了便利, 使程序员不需要为了极少的例外情况编写大量的函数. 和函数重载相比, 缺省参数的语法更简洁明了, 减少了大量的样板代码, 也更好地区别了 "必要参数" 和 "可选参数". 75 | 76 | ### 缺点 77 | 78 | 缺省参数实际上是函数重载语义的另一种实现方式, 因此所有 :ref:`不应当使用函数重载的理由 ` 也都适用于缺省参数. 79 | 80 | 虚函数调用的缺省参数取决于目标对象的静态类型, 此时无法保证给定函数的所有重载声明的都是同样的缺省参数. 81 | 82 | 缺省参数是在每个调用点都要进行重新求值的, 这会造成生成的代码迅速膨胀. 作为读者, 一般来说也更希望缺省的参数在声明时就已经被固定了, 而不是在每次调用时都可能会有不同的取值. 83 | 84 | 缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致. 而函数重载不会导致这样的问题. 85 | 86 | ### 结论 87 | 88 | 对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作. 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用. (例如, 不要写像 ``void f(int n = counter++);`` 这样的代码.) 89 | 90 | 在其他情况下, 如果缺省参数对可读性的提升远远超过了以上提及的缺点的话, 可以使用缺省参数. 如果仍有疑惑, 就使用函数重载. 91 | 92 | ## 4.5. 函数返回类型后置语法 93 | 94 | ### 总述 95 | 96 | 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法. 97 | 98 | ### 定义 99 | 100 | C++ 现在允许两种不同的函数声明方式. 以往的写法是将返回类型置于函数名之前. 例如: 101 | 102 | ```cpp 103 | int foo(int x); 104 | ``` 105 | 106 | C++11 引入了这一新的形式. 现在可以在函数名前使用 ``auto`` 关键字, 在参数列表之后后置返回类型. 例如: 107 | 108 | ```cpp 109 | auto foo(int x) -> int; 110 | ``` 111 | 112 | 后置返回类型为函数作用域. 对于像 ``int`` 这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别. 113 | 114 | ### 优点 115 | 116 | 后置返回类型是显式地指定 :ref:`Lambda 表达式 ` 的返回值的唯一方式. 某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现. 即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了. 117 | 118 | 有时在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时. 例如: 119 | 120 | ```cpp 121 | template auto add(T t, U u) -> decltype(t + u); 122 | ``` 123 | 124 | 对比下面的例子: 125 | 126 | ```cpp 127 | template decltype(declval() + declval()) add(T t, U u); 128 | ``` 129 | 130 | ### 缺点 131 | 132 | 后置返回类型相对来说是非常新的语法, 而且在 C 和 Java 中都没有相似的写法, 因此可能对读者来说比较陌生. 133 | 134 | 在已有的代码中有大量的函数声明, 你不可能把它们都用新的语法重写一遍. 因此实际的做法只能是使用旧的语法或者新旧混用. 在这种情况下, 只使用一种版本是相对来说更规整的形式. 135 | 136 | ### 结论 137 | 138 | 在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法. 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样 :ref:`复杂的模板代码 `. 139 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/images/一张图总结 Google C++编程规范.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/最佳实践与编码规范/代码风格指南/Google/images/一张图总结 Google C++编程规范.png -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/index.md: -------------------------------------------------------------------------------- 1 | # 扉页 2 | 3 | 版本: 4 | 5 | * 2024/02/18 6 | 7 | 原作者: 8 | 9 | * Benjy Weinberger 10 | * Craig Silverstein 11 | * Gregory Eitzmann 12 | * Mark Mentovai 13 | * Tashana Landray 14 | 15 | 翻译: 16 | 17 | * YuleFox 18 | * Yang.Y 19 | * acgtyrant 20 | * lilinsanity 21 | * 楼宇 22 | 23 | 项目主页 24 | 25 | * `Google Style Guide `_ 26 | * `Google 开源项目风格指南 - 中文版 `_ 27 | 28 | ## 译者前言 29 | 30 | Google 经常会发布一些开源项目, 意味着会接受来自其他代码贡献者的代码. 但是如果代码贡献者的编程风格与 Google 的不一致, 会给代码阅读者和其他代码提交者造成不小的困扰. Google 因此发布了这份自己的编程风格指南, 使所有提交代码的人都能获知 Google 的编程风格. 31 | 32 | * 翻译初衷: 33 | 34 | * 规则的作用就是避免混乱. 但规则本身一定要权威, 有说服力, 并且是理性的. 我们所见过的大部分编程规范, 其内容或不够严谨, 或阐述过于简单, 或带有一定的武断性. 35 | 36 | * Google 保持其一贯的严谨精神, 5 万汉字的指南涉及广泛, 论证严密. 我们翻译该系列指南的主因也正是其严谨性. 严谨意味着指南的价值不仅仅局限于它罗列出的规范, 更具参考意义的是它为了列出规范而做的谨慎权衡过程. 37 | 38 | * 指南不仅列出你要怎么做, 还告诉你为什么要这么做, 哪些情况下可以不这么做, 以及如何权衡其利弊. 其他团队未必要完全遵照指南亦步亦趋, 如前面所说, 这份指南是 Google 根据自身实际情况打造的, 适用于其主导的开源项目. 其他团队可以参照该指南, 或从中汲取灵感, 建立适合自身实际情况的规范. 39 | 40 | * 我们在翻译的过程中, 收获颇多. 希望本系列指南中文版对你同样能有所帮助. 41 | 42 | * 我们翻译时也是尽力保持严谨, 但水平所限, bug 在所难免. 有任何意见或建议, 可与我们取得联系. 43 | 44 | * 中文版和英文版一样, 使用 ``Artistic License/GPL`` 开源许可. 45 | 46 | * 中文版修订历史: 47 | 48 | * 2015-08 : 热心的清华大学同学 @lilinsanity 完善了「类」章节以及其它一些小章节。至此,对 Google CPP Style Guide 4.45 的翻译正式竣工。 49 | 50 | * 2015-07 4.45 : acgtyrant 为了学习 C++ 的规范,顺便重新翻译了本 C++ 风格指南,特别是 C++11 的全新内容。排版大幅度优化,翻译措辞更地道,添加了新译者笔记。Google 总部 C++ 工程师 innocentim, 清华大学不愿意透露姓名的唐马儒先生,大阪大学大学院情报科学研究科计算机科学专攻博士 farseerfc 和其它 Arch Linux 中文社区众帮了译者不少忙,谢谢他们。因为 C++ Primer 尚未完全入门,暂时没有翻译「类」章节和其它一些小章节。 51 | 52 | * 2009-06 3.133 : YuleFox 的 1.0 版已经相当完善, 但原版在近一年的时间里, 其规范也发生了一些变化. 53 | 54 | * Yang.Y 与 YuleFox 一拍即合, 以项目的形式来延续中文版 : `Google 开源项目风格指南 - 中文版项目 `_. 55 | 56 | * 主要变化是同步到 3.133 最新英文版本, 做部分勘误和改善可读性方面的修改, 并改进排版效果. Yang.Y 重新翻修, YuleFox 做后续评审. 57 | 58 | * 2008-07 1.0 : 出自 `YuleFox 的 Blog `_, 很多地方摘录的也是该版本. 59 | 60 | 以下是正文. 61 | 62 | ## 背景 63 | 64 | C++ 是谷歌的开源项目所采用的主要编程语言之一. C++ 程序员都知道, 该语言有很多强大的特性 (feature), 但强大之处也伴随着复杂性, 让代码容易出错, 难以阅读、维护. 65 | 66 | 本指南的目标是详述 C++ 的注意事项来控制复杂性. 这些规则会在保持代码易于管理的同时, 不影响程序员高效地使用 C++ 的语言特性. 67 | 68 | 风格 (style, 亦称作可读性 (readability)) 是用于管理 C++ 代码的惯例. "风格" 这一术语略有不准确, 因为这些惯例并非仅仅囊括代码格式. 69 | 70 | 谷歌主导的大部分开源项目遵守本指南的要求. 71 | 72 | 注意: 本指南并非 C++ 教程, 我们假定读者已经非常熟悉 C++. 73 | 74 | ## 本指南的目标 75 | 76 | 为什么编写这份文档? 77 | 78 | 我们认为该指南应该实现以下核心目标. 这些目标是每条规则背后的基本 **依据** . 我们希望公开这些想法, 作为讨论的基础, 让广大社区了解每条规则和特定决策背后的来由. 在理解规则所服务的目标以后, 所有人都应该清楚某条规则在哪些情况下可以忽略 (有些规则可以忽略), 以及改变规则时需要提供怎样的论据和替代品. 79 | 80 | 我们认为风格指南当前的目标如下: 81 | 82 | * 风格规则应该有影响力 83 | 84 | * 一条风格规则应该具备足够大的好处, 以至于值得所有工程师铭记. 所谓好处是相对于当前代码库的状态而言的, 所以即使某一习惯十分恶劣, 如果人们很少使用, 那么禁止这一习惯的好处依然很小. 这样可以解释为什么我们没有写下某些规则. 例如, ``goto`` 语句违背了许多原则, 但是现在已经很少出现, 所以风格指南不会讨论它. 85 | 86 | * 为读者优化, 而非为作者优化 87 | 88 | * 我们的代码库 (以及其中的每个组件) 应该会存在很长时间. 因此, 我们读代码的时间比写代码的时间更长. 我们明确地选择优化平均水平的软件工程师阅读、维护和调试代码的体验, 而非编写代码的舒适度. "为读者留下线索" 是这一理念的一个方面. 如果代码中有特殊的情况 (例如指针所有权转移), 在此处给读者留下的文字提示很有价值 (在代码中使用 ``std::unique_ptr`` 就明确地表达了所有权转移). 89 | 90 | * 和现有代码保持一致 91 | 92 | * 我们让代码库的风格保持整体一致, 就能聚焦在其他 (更有价值的) 问题上. 一致性也会帮助自动化: 那些格式化代码或者调整 ``#include`` 顺序的工具, 只能在你的代码符合预期时才能正常工作. 很多时候, 那些用于 "保持一致" 的规则本质上就是 "任选其一并停止内耗"; 在这些问题上, 争论的成本超过了提供自由度的价值. 不过, 一致性原则也有局限性. 在没有清晰的技术性论据和长远方向时, 这才是很好的打破平局的方式. 这一原则适合局部使用 (一个文件内, 或者一组关联性强的接口). 不应该为了一致性而采用旧风格, 忽视新风格的好处. 应该考虑到代码库会随时间推移而过渡到新风格. 93 | 94 | * 恰当时与广大 C++ 社区保持一致 95 | 96 | * 与其他组织保持一致性是有价值的, 这和我们保持内部一致性的原因一样. 如果 C++ 标准中的特性解决了某个问题, 或者某一范式被广泛采用, 这就是采纳它们的依据. 不过, 有时标准的特性和范式有缺陷, 或者在设计上没有考虑我们代码库的需求. 此时 (正如下文所描述的) 应该限制或者禁止这些标准特性. 有时, 相较于 C++ 标准库, 我们偏向于自研库或某些第三方库. 一般这是因为我们所选择的库具有优越性, 或者迁移到标准库的价值不值得那些工作量. 97 | 98 | * 避免使用奇特或危险的语法结构 99 | 100 | * 有些 C++ 的特性比表面上更加奇特或危险. 风格指南中的一些限制就是为了防止掉入这些陷阱. 你需要达到很高的标准才能豁免这些限制, 因为忽略这些规则就很可能直接引起程序错误. 101 | 102 | * 避免使用那些正常水平的 C++ 程序员认为棘手或难以维护的语法结构 103 | 104 | * 有些 C++ 特性会给代码带来复杂性, 因此通常不适合使用. 在用途广泛的代码中, 我们可以接受更巧妙的语法结构. 这是因为复杂的实现方式所带来的收益会被众多使用者放大, 而且在编写新代码时, 也不需要重新解读这些复杂的语法. 如有疑问, 可以请求项目主管豁免这些规则. 这对我们的代码库至关重要, 因为代码负责人和团队成员会变化: 即使所有现在修改这段代码的人都理解代码, 几年后人们就不一定还能理解了. 105 | 106 | * 需要注意我们的规模 107 | 108 | * 我们有上亿行代码和成千上万的工程师, 因此一位工程师的失误或者投机取巧的行为会成为很多人的负担. 举例来说, 一定要避免污染全局命名空间 (global namespace): 如果所有人都往全局命名空间里塞东西, 就很难避免上亿行代码之间的符号冲突 (name collision), 也难以修复冲突. 109 | 110 | * 在必要时为优化让路 111 | 112 | * 即使性能优化的手段会和此文档的其他理念冲突, 有时这些手段也是必要且恰当的. 113 | 114 | 此文档的目的是提供最大程度的指导和合理限制. 和往常一样, 你应该追随常理和正常审美. 这里我们特指整个谷歌 C++ 社区所建立的规范, 而不是你个人或者所在团队的偏好. 应该对巧妙或奇特的语法结构保持怀疑和犹豫的态度: 并不是 "法无禁止即可为". 运用你的判断力. 如有疑惑, 请不要犹豫, 随时向项目主管咨询意见. 115 | 116 | ## C++ 版本 117 | 118 | 目前代码的目标版本是 C++20, 所以不应该使用 C++23 的特性. 本指南的 C++ 目标版本会随时间 (激进地) 升级. 119 | 120 | 禁止使用非标准扩展. 121 | 122 | 在使用 C++17 和 C++20 的特性之前, 需要权衡其他环境的可移植性. 123 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/Google/magic.md: -------------------------------------------------------------------------------- 1 | # 5. 来自 Google 的奇技 2 | 3 | Google 用了很多自己实现的技巧 / 工具使 C++ 代码更加健壮, 我们使用 C++ 的方式可能和你在其它地方见到的有所不同. 4 | 5 | ## 5.1. 所有权与智能指针 6 | 7 | ### 总述 8 | 9 | 动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权. 10 | 11 | ### 定义 12 | 13 | 所有权是一种登记/管理动态内存和其它资源的技术. 动态分配对象的所有主是一个对象或函数, 后者负责确保当前者无用时就自动销毁前者. 所有权有时可以共享, 此时就由最后一个所有主来负责销毁它. 甚至也可以不用共享, 在代码中直接把所有权传递给其它对象. 14 | 15 | 智能指针是一个通过重载 ``*`` 和 ``->`` 运算符以表现得如指针一样的类. 智能指针类型被用来自动化所有权的登记工作, 来确保执行销毁义务到位. `std::unique_ptr ` 是 C++11 新推出的一种智能指针类型, 用来表示动态分配出的对象的独一无二的所有权; 当 ``std::unique_ptr`` 离开作用域时, 对象就会被销毁. ``std::unique_ptr`` 不能被复制, 但可以把它移动(move)给新所有主. `std::shared_ptr `_ 同样表示动态分配对象的所有权, 但可以被共享, 也可以被复制; 对象的所有权由所有复制者共同拥有, 最后一个复制者被销毁时, 对象也会随着被销毁. 16 | 17 | ### 优点 18 | 19 | * 如果没有清晰、逻辑条理的所有权安排, 不可能管理好动态分配的内存. 20 | 21 | * 传递对象的所有权, 开销比复制来得小, 如果可以复制的话. 22 | 23 | * 传递所有权也比"借用"指针或引用来得简单, 毕竟它大大省去了两个用户一起协调对象生命周期的工作. 24 | 25 | * 如果所有权逻辑条理, 有文档且不紊乱的话, 可读性会有很大提升. 26 | 27 | * 可以不用手动完成所有权的登记工作, 大大简化了代码, 也免去了一大波错误之恼. 28 | 29 | * 对于 const 对象来说, 智能指针简单易用, 也比深度复制高效. 30 | 31 | ### 缺点 32 | 33 | * 不得不用指针(不管是智能的还是原生的)来表示和传递所有权. 指针语义可要比值语义复杂得许多了, 特别是在 API 里:这时不光要操心所有权, 还要顾及别名, 生命周期, 可变性以及其它大大小小的问题. 34 | 35 | * 其实值语义的开销经常被高估, 所以所有权传递带来的性能提升不一定能弥补可读性和复杂度的损失. 36 | 37 | * 如果 API 依赖所有权的传递, 就会害得客户端不得不用单一的内存管理模型. 38 | 39 | * 如果使用智能指针, 那么资源释放发生的位置就会变得不那么明显. 40 | 41 | * ``std::unique_ptr`` 的所有权传递原理是 C++11 的 move 语法, 后者毕竟是刚刚推出的, 容易迷惑程序员. 42 | 43 | * 如果原本的所有权设计已经够完善了, 那么若要引入所有权共享机制, 可能不得不重构整个系统. 44 | 45 | * 所有权共享机制的登记工作在运行时进行, 开销可能相当大. 46 | 47 | * 某些极端情况下 (例如循环引用), 所有权被共享的对象永远不会被销毁. 48 | 49 | * 智能指针并不能够完全代替原生指针. 50 | 51 | ### 结论 52 | 53 | 如果必须使用动态分配, 那么更倾向于将所有权保持在分配者手中. 如果其他地方要使用这个对象, 最好传递它的拷贝, 或者传递一个不用改变所有权的指针或引用. 倾向于使用 ``std::unique_ptr`` 来明确所有权传递, 例如: 54 | 55 | ```cpp 56 | std::unique_ptr FooFactory(); 57 | void FooConsumer(std::unique_ptr ptr); 58 | ``` 59 | 60 | 如果没有很好的理由, 则不要使用共享所有权. 这里的理由可以是为了避免开销昂贵的拷贝操作, 但是只有当性能提升非常明显, 并且操作的对象是不可变的(比如说 ``std::shared_ptr`` )时候, 才能这么做. 如果确实要使用共享所有权, 建议于使用 ``std::shared_ptr`` . 61 | 62 | 不要使用 ``std::auto_ptr``, 使用 ``std::unique_ptr`` 代替它. 63 | 64 | ## 5.2. Cpplint 65 | 66 | ### 总述 67 | 68 | 使用 ``cpplint.py`` 检查风格错误. 69 | 70 | ### 说明 71 | 72 | ``cpplint.py`` 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 在行尾加 ``// NOLINT``, 或在上一行加 ``// NOLINTNEXTLINE``, 可以忽略报错. 73 | 74 | 某些项目会指导你如何使用他们的项目工具运行 ``cpplint.py``. 如果你参与的项目没有提供, 你可以单独下载 `cpplint.py `_. 75 | 76 | ## 译者(acgtyrant)笔记 77 | 78 | * 把智能指针当成对象来看待的话, 就很好领会它与所指对象之间的关系了. 79 | * 原来 Rust 的 Ownership 思想是受到了 C++ 智能指针的很大启发啊. 80 | * ``scoped_ptr`` 和 ``auto_ptr`` 已过时. 现在是 ``shared_ptr`` 和 ``uniqued_ptr`` 的天下了. 81 | * 按本文来说, 似乎除了智能指针, 还有其它所有权机制, 值得留意. 82 | * Arch Linux 用户注意了, AUR 有对 cpplint 打包. 83 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/代码风格指南/代码风格指南.md: -------------------------------------------------------------------------------- 1 | # 代码风格指南 2 | 3 | ## Google 4 | 5 | github大佬维护的项目: 6 | [Google C++风格指南 中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents.html) 7 | 8 | [Google C++风格指南 英文版](https://google.github.io/styleguide/cppguide.html) 9 | 10 | [Google 本人转载版本](./Google/Google.md) 11 | -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/命名规则/images/命名-刷新7剑客.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/最佳实践与编码规范/命名规则/images/命名-刷新7剑客.png -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/命名规则/希腊字母.md: -------------------------------------------------------------------------------- 1 | # 希腊字母 2 | 希腊字母是希腊语的书写系统,也是数学、科学、工程学等领域中广泛使用的符号。希腊字母表由24个主要字母组成,包括7个元音和17个辅音。 3 | 4 | ## 速览 5 | α 阿尔法, β 贝塔, γ 伽玛,δ 德尔塔, ε 伊普西隆, ζ 泽塔, η 伊塔, θ 西塔, ι 约塔, κ 卡帕, λ 兰姆达,μ 米欧 ,ν 纽, ξ 克西, ο 欧米克隆, π 派, ρ 柔 ,σ 西格玛, τ 陶 ,υ 玉普西隆, φ 弗爱, χ 凯, ψ 普赛 6 | 7 | ## 希腊字母表及其对应的英文名称和常见的用途示例: 8 | 序号 大写 小写 国际音标 中文读音 意义 9 | 10 | 1 Α α a:lf 阿尔法 角度;系数 11 | 12 | 2 Β β bet 贝塔 磁通系数;角度;系数 13 | 14 | 3 Γ γ ga:m 伽马 电导系数(小写) 15 | 16 | 4 Δ δ delt 德尔塔 变动;密度;屈光度 17 | 18 | 5 Ε ε ep`silon 伊普西龙 对数之基数 19 | 20 | 6 Ζ ζ zat 截塔 系数;方位角;阻抗;相对粘度;原子序数 21 | 22 | 7 Η η eit 艾塔 磁滞系数;效率(小写) 23 | 24 | 8 Θ θ θit 西塔 温度;相位角 25 | 26 | 9 Ι ι aiot 约塔 微小,一点儿 27 | 28 | 10 Κ κ kap 卡帕 介质常数 29 | 30 | 11 Λ λ lambd 兰布达 波长(小写);体积 31 | 32 | 12 Μ μ mju 缪 磁导系数微(千分之一)放大因数(小写) 33 | 34 | 13 Ν ν nju 纽 磁阻系数 35 | 36 | 14 Ξ ξ ksi 克西 37 | 38 | 15 Ο ο omik`ron 奥密克戎 39 | 40 | 16 Π π pai 派 圆周率 41 | 42 | 17 Ρ ρ rou 肉 密度/电阻系数(小写) 43 | 44 | 18 Σ σ sigma 西格马 总和(大写),表面密度;跨导(小写) 45 | 46 | 19 Τ τ tau 套 时间常数 47 | 48 | 20 Υ υ jup`silon 宇普西龙 位移 49 | 50 | 21 Φ φ fai 佛爱 磁通;角 51 | 52 | 22 Χ χ phai 西 53 | 54 | 23 Ψ ψ psai 普西 角速;介质电通量(静电力线);角 55 | 56 | 24 Ω ω o`miga 欧米伽 欧姆(大写);角速(小写);角 -------------------------------------------------------------------------------- /c++/最佳实践与编码规范/注释规范/注释.md: -------------------------------------------------------------------------------- 1 | # 注释 2 | 注释是编译器忽略但对于程序员非常有用的文本。 注释通常用于批注代码以供将来参考。 在C/C++中,使用注释有三种方法。 3 | 4 | ## 一、cpp的注释类型 5 | ### 1.单行注释 6 | - 注释以 ``//`` 开始,直到行末为止。 7 | 8 | ### 2.多行注释 9 | - 注释以 ``/*`` 开始,以 ``*/`` 终止 10 | 11 | ### 3.预处理形式注释 12 | - 使用 ``#if 0`` … ``#endif`` 来实现注释,可以实现嵌套: 13 | ```cpp 14 | #if 0 15 | 16 | code 17 | 18 | #endif 19 | ``` 20 | >你可以把 #if 0 改成 #if 1 来执行 code 的代码。 21 | 这种形式对程序调试也可以帮助,测试时使用 #if 1 来执行测试代码,发布后使用 #if 0 来屏蔽测试代码。 22 | #if 后可以是任意的条件语句 23 | 24 | ## 二、注释风格 25 | ### 总述 26 | 一般使用 // 或 /* */,只要统一就好。 27 | 28 | ### 说明 29 | // 或/* */都可以,但//更 常用,要在如何注释及注释风格上确保统一。 30 | 31 | ## 三、具体注释说明 32 | ### 1.文件注释 33 | #### 总述 34 | 在每一个文件开头加入版权、作者、时间等描述。 35 | 文件注释描述了该文件的内容,如果一个文件只声明,或实现,或测试了一个对象,并且这个对象已经在它的声明处进行了详细的注释,那么就没必要再加上文件注释,除此之外的其他文件都需要文件注释。 36 | 37 | #### 说明 38 | 法律公告和作者信息:每个文件都应该包含许可证引用. 为项目选择合适的许可证版本(比如, Apache 2.0, BSD, LGPL, GPL)。 39 | 如果你对原始作者的文件做了重大修改,请考虑删除原作者信息。 40 | 41 | #### 文件内容 42 | 如果一个 .h 文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 43 | 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中. 44 | 不要在 .h 和 .cc 之间复制注释, 这样的注释偏离了注释的实际意义。 45 | 46 | ### 2.函数注释 47 | #### 总述 48 | 函数声明处的注释描述函数功能; 定义处的注释描述函数实现。 49 | 50 | #### 说明 51 | 函数声明: 52 | 基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数)。 53 | 54 | #### 函数定义 55 | 如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释。比如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要。 56 | 不要 从 .h 文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上。 57 | 58 | ### 3.变量注释 59 | #### 总述 60 | 通常变量名本身足以很好说明变量用途, 某些情况下, 也需要额外的注释说明。 61 | 62 | #### 说明 63 | 根据不同场景、不同修饰符,变量可以分为很多种类,总的来说变量分为全局变量、局部变量。 64 | 一般来说局部变量仅限于局部范围,其含义相对简单容易理解,只需要简单注释即可。 65 | 全局变量一般作用于多个文件,或者整个工程,因此,其含义相对更复杂,所以在注释的时候,最好描述清楚其具体含义,就是尽量全面描述。 66 | (提示:全局变量尽量少用) 67 | 68 | 69 | ## 四、常见编程软件注释 70 | ```cpp 71 | /** 72 | * @brief @param @return @author @date @version 是代码书写的一种规范 73 | * @brief :简介,简单介绍函数作用 74 | * @param :介绍函数参数 75 | * @return:函数返回类型说明 76 | * @exception NSException 可能抛出的异常. 77 | * @author zhangsan: 作者 78 | * @date 2011-07-27 22:30:00 :时间 79 | * @version 1.0 :版本 80 | * @property :属性介绍 81 | */ 82 | ``` -------------------------------------------------------------------------------- /c++/模板与泛型编程/addressof.md: -------------------------------------------------------------------------------- 1 | # addressof 2 | 3 | ## std::addressof简介 4 | 获取对象的实际地址,尤其是取地址符被重载时想获取真实地址使用。 5 | std::addressof的作用是获取一个对象的实际地址,即使这个对象的&操作符已被重载。它接受一个参数,该参数为要获得地址的那个对象的引用。下面通过一个极其简单的例子了解一下std::addressof的使用方法 6 | ```cpp 7 | #include 8 | #include 9 | #include 10 | 11 | class Test 12 | { 13 | public: 14 | int* operator&() 15 | { 16 | return &b; 17 | } 18 | 19 | int* a_addr() 20 | { 21 | return &a; 22 | } 23 | 24 | int* b_addr() 25 | { 26 | return &b; 27 | } 28 | 29 | private: 30 | int a; 31 | int b; 32 | }; 33 | 34 | int main(int argc, char* argv[]) 35 | { 36 | Test t; 37 | std::cout << "&t.a:" << t.a_addr() << std::endl; 38 | std::cout << "&t.b:" << t.b_addr() << std::endl; 39 | std::cout << "&t:" << &t << std::endl; 40 | std::cout << "addressof(t):" << std::addressof(t) << std::endl; 41 | } 42 | ``` 43 | 上面的代码输出结果为: 44 | ```cpp 45 | &t.a:0x7ffefcb48eb0 46 | &t.b:0x7ffefcb48eb4 47 | &t:0x7ffefcb48eb4 48 | addressof(t):0x7ffefcb48eb0 49 | ``` 50 | 在这里正常来说使用&t应该取到的是t.a的地址值才对,但是由于我们重载了&运算符,所以这里取到了t.b的地址值,但是如果使用了std::addressof,就可以取到正确的值了。 51 | 52 | ## std::addressof源码解析 53 | std::addressof位于libstdc++-v3\include\bits\move.h中 54 | ```cpp 55 | template 56 | inline _Tp* __addressof(_Tp& __r) _GLIBCXX_NOEXCEPT 57 | { 58 | return reinterpret_cast<_Tp*>(&const_cast(reinterpret_cast(__r))); 59 | } 60 | 61 | template 62 | inline _Tp* addressof(_Tp& __r) noexcept 63 | { return std::__addressof(__r); } 64 | ``` 65 | 从代码中可以看出``std::addressof``里面调用了``std::__addressof``,所以真正起作用的是``std::__addressof``。``__addressof``的处理过程可以分为以下四步: 66 | 67 | 1. 将``__r``由类型``_Tp&``强制转换为``const volatile char&``,这样做有两个作用:一是防止后面使用``&``操作符获取地址时触发原类型(即``_Tp``)的重载操作(``operator&``),就像上面那个例子那样;二是``reinterpret_cast``操作符总是可以合法的在原类型的基础上加``const``或``volatile``, 但是如果_Tp原来就带有``const``或``volatile``的话, 通过``reinterpret_cast``去掉是不允许的, 因此需要加上``const volatile``来避免编译器报错, 也就是此时不用再管``_Tp``是否本来就带有``const``或``volatile``属性了。 68 | 2. 将前面转换得到的结果强制转换为``char&``类型,此时如果转换成其它类型有可能会触发强制地址对齐的操作,这样的话真实地址就有可能会被改变了,最终造成程序错误。需要注意的是这个转换过程使用的是const_cast,可以顺便将前面留下的const和volatile属性给去掉了。 69 | 3. 使用``&``符号将前面的结果的地址给取出来(此时已经不会触发重载了) 70 | 4. 最后一步使用``reinterpret_cast``将前面获取到的地址转换回``_Tp*``类型,并且此时也会保留``_Tp``的``const``或``volatile``属性(如果有的话) 71 | 72 | ## 总结 73 | 本文通过一个简单的例子和源码介绍了C++11新引入的模板函数std::addressof,内容虽然比较简单,但是考虑到std::addressof在标准库中使用频率是比较高的,所以我们还是应该掌握其原理和用法。 74 | 最后需要注意的一点就是``std::addressof``并不保证转换的正确性和合理性,这个是需要程序员自己把控的,虽然标准库使用``std::addressof``的频率比较高,但是我们平时在编程中还是得谨慎一点使用它。 75 | 76 | 77 | -------------------------------------------------------------------------------- /c++/模板与泛型编程/模板.md: -------------------------------------------------------------------------------- 1 | # 模板 2 | ## 什么是模板?如何实现? 3 | **模板**:创建类或者函数的蓝图或者公式,分为函数模板和类模板。 4 | **实现方式**:模板定义以关键字 `template` 开始,后跟一个模板参数列表。 5 | 6 | - 模板参数列表不能为空; 7 | - 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。 8 | ```cpp 9 | template 10 | ``` 11 | 12 | **函数模板**:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。 13 | - 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。 14 | - 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。 15 | ```cpp 16 | #include 17 | using namespace std; 18 | 19 | template 20 | T add_fun(const T & tmp1, const T & tmp2){ 21 | return tmp1 + tmp2; 22 | } 23 | 24 | int main(){ 25 | int var1, var2; 26 | cin >> var1 >> var2; 27 | cout << add_fun(var1, var2); 28 | 29 | double var3, var4; 30 | cin >> var3 >> var4; 31 | cout << add_fun(var3, var4); 32 | return 0; 33 | } 34 | ``` 35 | 36 | **类模板**:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。 37 | ```cpp 38 | #include 39 | using namespace std; 40 | 41 | template 42 | class Complex{ 43 | public: 44 | //构造函数 45 | Complex(T a, T b) 46 | { 47 | this->a = a; 48 | this->b = b; 49 | } 50 | 51 | //运算符重载 52 | Complex operator+(Complex &c){ 53 | Complex tmp(this->a + c.a, this->b + c.b); 54 | cout << tmp.a << " " << tmp.b << endl; 55 | return tmp; 56 | } 57 | 58 | private: 59 | T a; 60 | T b; 61 | }; 62 | 63 | int main(){ 64 | Complex a(10, 20); 65 | Complex b(20, 30); 66 | Complex c = a + b; 67 | 68 | return 0; 69 | } 70 | ``` 71 | 72 | ## 函数模板和类模板的区别? 73 | - 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。 74 | - 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。 75 | - 默认参数:类模板在模板参数列表中可以有默认参数。 76 | - 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。 77 | - 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。 78 | 79 | 函数模板调用方式举例: 80 | ```cpp 81 | #include 82 | using namespace std; 83 | 84 | template 85 | T add_fun(const T & tmp1, const T & tmp2){ 86 | return tmp1 + tmp2; 87 | } 88 | 89 | int main(){ 90 | int var1, var2; 91 | cin >> var1 >> var2; 92 | cout << add_fun(var1, var2); // 显式调用 93 | 94 | double var3, var4; 95 | cin >> var3 >> var4; 96 | cout << add_fun(var3, var4); // 隐式调用 97 | return 0; 98 | } 99 | ``` 100 | 101 | ## 什么是可变参数模板? 102 | 可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。 103 | - 模板参数包:表示零个或多个模板参数; 104 | - 函数参数包:表示零个或多个函数参数。 105 | 106 | 用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符。 107 | ```cpp 108 | template // Args 是模板参数包 109 | void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包 110 | ``` 111 | 112 | ```cpp 113 | #include 114 | using namespace std; 115 | 116 | template 117 | void print_fun(const T &t){ 118 | cout << t << endl; // 最后一个元素 119 | } 120 | 121 | template 122 | void print_fun(const T &t, const Args &...args){ 123 | cout << t << " "; 124 | print_fun(args...); 125 | } 126 | 127 | int main(){ 128 | print_fun("Hello", "wolrd", "!"); 129 | return 0; 130 | } 131 | /*运行结果: 132 | Hello wolrd ! 133 | */ 134 | ``` 135 | 说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。 136 | 137 | ## 什么是模板特化?为什么特化? 138 | 模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。 139 | 140 | 模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化 141 | - 函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。 142 | - 类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。 143 | 144 | 特化分为全特化和偏特化: 145 | - 全特化:模板中的模板参数全部特例化。 146 | - 偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。 147 | 148 | 说明:要区分下函数重载与函数模板特化 149 | 定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。 150 | ```cpp 151 | #include 152 | #include 153 | using namespace std; 154 | //函数模板 155 | template 156 | bool compare(T t1, T t2){ 157 | cout << "通用版本:"; 158 | return t1 == t2; 159 | } 160 | 161 | template <> //函数模板特化 162 | bool compare(char *t1, char *t2){ 163 | cout << "特化版本:"; 164 | return strcmp(t1, t2) == 0; 165 | }在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。 166 | 167 | int main(int argc, char *argv[]){ 168 | char arr1[] = "hello"; 169 | char arr2[] = "abc"; 170 | cout << compare(123, 123) << endl; 171 | cout << compare(arr1, arr2) << endl; 172 | 173 | return 0; 174 | } 175 | /* 176 | 运行结果: 177 | 通用版本:1 178 | 特化版本:0 179 | ``` 180 | 181 | -------------------------------------------------------------------------------- /c++/模板与泛型编程/泛型编程.md: -------------------------------------------------------------------------------- 1 | # 泛型编程 2 | ## 泛型编程如何实现? 3 | 泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。 4 | 5 | 泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。 6 | 7 | - 容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。 8 | - 迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。 9 | - 模板:可参考本章节中的模板相关问题。 -------------------------------------------------------------------------------- /c++/编译内存相关/C++ 程序编译过程.md: -------------------------------------------------------------------------------- 1 | # C++ 程序编译过程 2 | 3 | 编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。 4 | 5 | ![编译过程](images/编译过程.png) 6 | 7 | ## 编译预处理 8 | 9 | 编译预处理:处理以 # 开头的指令,产生 .i 文件; 10 | 主要的处理操作如下: 11 | 12 | - 对全部的#define进行宏展开。 13 | - 处理全部的条件编译指令,比方#if、#ifdef、#elif、#else、#endif; 14 | - 处理 #include 指令,这个过程是递归的,也就是说被包括的文件可能还包括其它文件; 15 | - 删除全部的注释 // 和 /**/ 16 | - 加入行号和文件标识 17 | - 保留全部的 #pragma 编译器指令 18 | 19 | ps:经过预处理后的 .i 文件不包括任何宏定义,由于全部的宏已经被展开。而且包括的文件也已经被插入到 .i 文件里。 20 | 21 | ## 编译、优化 22 | 23 | 编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码; 24 | 25 | - 词法分析:将源代码的字符序列分割成一系列的记号。 26 | - 语法分析:对记号进行语法分析,产生语法树。 27 | - 语义分析:判断表达式是否有意义。 28 | - 代码优化: 29 | - 目标代码生成:生成汇编代码。 30 | - 目标代码优化: 31 | 32 | 编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后的.s是ASCII码文件。 33 | 34 | ## 汇编 35 | 36 | 汇编:将汇编代码 .s 翻译成机器指令的 .o 或.obj 目标文件; 37 | 38 | - 汇编过程调用汇编器AS来完成,是用于将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。 39 | - 汇编后的.o文件是纯二进制文件。 40 | 41 | ## 链接 42 | 43 | 链接:产生 .out 或 .exe 可运行文件 44 | 45 | 汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。 46 | 47 | 详细来说,链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows的.exe文件或Linux的.out文件)等。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向。 48 | 49 | 最基本的链接叫做静态链接,就是将每个模块的源代码文件编译、汇编成目标文件(Linux:.o 文件;Windows:.obj文件),然后将目标文件和库一起链接形成最后的可执行文件(.exe或.out等)。库其实就是一组目标文件的包,就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。 50 | 51 | 链接分为两种:静态链接 动态链接 52 | 53 | ### 静态链接 54 | 55 | 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。 56 | 57 | 把目标程序运行时需要调用的函数代码直接链接到了生成的可执行文件中,程序在运行的时候不需要其他额外的库文件,且就算你去静态库把程序执行需要的库删掉也不会影响程序的运行,因为所需要的所有东西已经被链接到了链接阶段生成的可执行文件中。 58 | 59 | Windows下以.lib为后缀,Linux下以.a为后缀。 60 | 61 | ### 动态链接 62 | 63 | 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。 64 | 65 | 动态 “动” 在了程序在执行阶段需要去寻找相应的函数代码,即在程序运行时才会将程序安装模块链接在一起 66 | 67 | 具体来说,动态链接就是把调⽤的函数所在⽂件模块(DLL )和调⽤函数在⽂件中的位置等信息链接进目标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码,因此需要相应 DLL ⽂件的⽀持 。(Windows) 68 | 69 | 包含函数重定位信息的文件,在Windows下以.dll为后缀,Linux下以.so为后缀。 70 | 71 | ### 二者的区别 72 | 73 | 静态链接是将各个模块的obj和库链接成**一个完整的可执行程序**; 74 | 动态链接是程序在运行的时候寻找动态库的函数符号(重定位),即**DLL不必被包含在最终的exe文件中**; 75 | 76 | 1. 链接使用工具不同: 77 | 78 | - 静态链接由称为“链接器”的工具完成; 79 | - 动态链接由操作系统在程序运行时完成链接; 80 | 81 | 2. 库包含限制: 82 | 83 | - 静态链接库中不能再包含其他的动态链接库或者静态库; 84 | - 动态链接库中还可以再包含其他的动态或静态链接库。 85 | 86 | 3. 运行速度: 87 | 88 | - 静态链接运行速度快(因为执行过程中不用重定位),可独立运行 89 | - 动态链接运行速度慢、不可独立运行 90 | 91 | 4. 二者的优缺点: 92 | 93 | - 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。 94 | - 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。 95 | -------------------------------------------------------------------------------- /c++/编译内存相关/C++内存模型.md: -------------------------------------------------------------------------------- 1 | # C++内存模型 2 | 3 | ## 内存区域与功能 4 | 5 | ![C++的内存分布模型](images/C++的内存分布模型.png) 6 | 7 | 从高地址到低地址,一个程序由 内核空间、栈区、堆区、BSS段、数据段(data)、代码区组成。 8 | 常说的C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。 9 | 10 | 可执行程序在运行时会多出两个区域:栈和堆 11 | 12 | - 栈(stack):存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。栈从高地址向低地址增长。是一块连续的空间。栈一般分配几M大小的内存。 13 | - 函数的参数、函数体内声明的变量都会存储在栈中,栈的特点是由运行时系统自动分配与释放,另栈分配空间是向高地址向低地址扩张。 14 | - 函数的参数、函数体内声明的变量都会存储在栈中,栈的特点是由运行时系统自动分配与释放,另栈分配空间是向高地址向低地址扩张。 15 | - 栈:由程序员管理,需要手动 new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中堆是两回事,分配方式倒是类似于链表。 16 | - 局部const、局部变量、函数参数等 17 | 18 | - 堆(heap):动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。堆从低地址向高地址增长。一般可以分配几个G大小的内存。 19 | 20 | - 共享区:在堆栈之间存在,(文件映射区)。 21 | - 动态库加载区:动态链接库(DLLs或.so文件)在加载时占用的内存空间。根据程序运行时链接的动态库而定,可以共享内存映射文件 22 | 23 | - 数据段(Data Segment) (即全局/静态存储区)(包含.BSS 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,程序中未初始化的全局变量和静态变量存放在.BSS 段中,已初始化的全局变量和静态变量存放在 .data 段中。在C++中,这种区分已经不那么明显了,所有的全局变量和静态变量(无论是否初始化)都被视为初始化,并且存储在同一区域。未初始化的全局或静态变量会被默认初始化为零值(对于基本类型)或默认构造(对于类类型)。 24 | - 未初始化数据段 (.bss):存储未初始化的全局变量和静态变量,或者初始化为0(或等效零值)的变量。在程序开始执行时由操作系统清零,不需要在磁盘上占用实际的空间来存储零值。 25 | - 全局数据段 (.data):存储初始化过的全局变量和静态变量的值,以及那些在定义时赋予了非零初始值的静态数据。在程序开始执行时被初始化,是可读写的。 26 | - 只读数据段 (.rodata 或 .rdata):存储常量数据,如字符串字面量、const修饰的全局变量和静态变量,以及枚举类型的值。程序执行期间不可修改,程序运行结束自动释放。 27 | 28 | - 代码区(.text 或 Text Segment):存放程序执行代码的一块内存区域。只读,不允许修改,但可以执行。编译后的二进制文件存放在这里。代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段) 29 | - 共享性:即在内存里面只有一份此程序代码,而无论谁都可以使用并运行它. 好处是当被频繁使用时,节约更多空间 30 | - 只读性:即不允许被修改, 31 | 32 | - 线程局部存储区 (Thread Local Storage, TLS):TLS是为了解决多线程环境下数据隔离问题而设计的,每个线程拥有自己独立的TLS区域,其中存储的数据仅对该线程可见,其他线程无法访问。TLS区域通常位于进程的虚拟地址空间内,但其确切位置取决于操作系统和实现细节。是一个额外的、独立管理的区域。TLS可以视作是每个线程独有的“数据段”,其大小和内容随线程的生命周期动态变化。 33 | 34 | ![Linux下的内存模型](images/Linux下的C++内存模型.png) 35 | 在linux下size命令可以查看一个可执行二进制文件基本情况: 36 | ![在linux下size命令可以查看一个可执行二进制文件基本情况](images/在linux下size命令可以查看一个可执行二进制文件基本情况.png) 37 | 38 | ### 初始化为0的全局变量在bss还是data 39 | 40 | BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。 41 | 42 | ## 栈和堆的区别 43 | 44 | - 申请方式: 45 | - 栈由系统自动分配 46 | - 堆是程序员申请开辟; 47 | - 申请效率: 48 | - 栈是有系统自动分配,申请效率高,但程序员无法控制; 49 | - 堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。 50 | - 申请大小限制不同。 51 | - 栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改。 52 | - 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。 53 | - 申请后系统响应: 54 | - 分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出; 55 | - 申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。 56 | - 空间大小: 57 | - 栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置)。 58 | - 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大。 59 | - 存储的内容不同。 60 | - 栈在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 61 | - 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。 62 | - 碎片问题 63 | - 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 64 | - 对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别) 65 | - 生长方向 66 | - 堆向上,向高地址方向增长。 67 | - 栈向下,向低地址方向增长。 68 | - 管理方式 69 | - 堆中资源由程序员控制,容易产生内存泄漏(memory leak) 70 | - 栈资源由编译器自动管理,无需手工控制 71 | - 分配方式 72 | - 堆都是动态分配(没有静态分配的堆) 73 | - 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 74 | - 分配效率 75 | - 堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。 76 | - 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令。 77 | 78 | >形象的比喻 79 | 栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 80 | 堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 81 | 82 | ### 怎么判断数据分配在栈上还是堆上 83 | 84 | - 局部变量分配在栈上; 85 | - 通过malloc和new申请的空间是在堆上。 86 | 87 | ### 你觉得堆快一点还是栈快一点? 88 | 89 | 毫无疑问是栈快一点。 90 | 91 | - 因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。 92 | 93 | - 而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。 94 | 95 | ## 程序启动的过程 96 | 97 | 操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。 98 | 99 | 加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。 100 | 101 | 加载器针对该程序的每一个动态链接库调用LoadLibrary (1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。 (2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。 (3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3 (4)调用该动态链接库的初始化函数 102 | 103 | 初始化应用程序的全局变量,对于全局对象自动调用构造函数。 104 | 105 | 进入应用程序入口点函数开始执行。 106 | 107 | ## STL分配的内存 108 | 109 | ### vector 110 | 111 | C++的std::vector本身作为一个对象,其在内存中的位置取决于它的声明方式: 112 | 113 | 1. 当你这样声明一个`vector:std::vector myVector;`,这个vector对象myVector本身是创建在栈上的,但是它所管理的元素(即你放入vector中的那些int值)是存储在堆上的。这是因为vector需要能够动态地调整其大小,而栈上的内存是静态分配的,无法满足这种动态变化的需求。因此,当向vector添加元素时,vector会自动在堆上分配或重新分配内存来容纳这些元素。 114 | 115 | 2. 如果你使用指针来创建vector,如:`std::vector* myVector = new std::vector;`,这时不仅vector内部管理的元素在堆上,连vector对象本身也在堆上,因为你使用了new操作符显式地在堆上分配了vector对象的内存。 116 | 117 | 总结来说,std::vector容器本身作为对象可以存储在栈或堆上,但其包含的元素总是存储在堆上,以支持容量的动态调整。 118 | 119 | ### STL 120 | 121 | STL(Standard Template Library)容器根据其设计目的和特性,在内存分配上有所不同,但普遍遵循以下原则: 122 | 123 | 1. 动态内存分配容器:大多数STL容器,如std::vector、std::list、std::map、std::unordered_map等,它们的元素是存储在堆上的。这是因为这些容器需要能够在运行时动态地调整大小,而堆内存是唯一能够提供这种灵活性的内存区域。容器本身(即管理元素的数据结构)如果是局部变量,则存储在栈上,但它们所持有的数据则位于堆中。 124 | 125 | 2. 静态内存分配容器:像std::array这样的容器,它是一个静态大小的数组,其元素和容器本身(因为它不是一个动态容器)都存储在栈上,或者如果它是类或结构体的成员,则存储在相应的内存区域。std::array不涉及堆分配,其大小在编译时就必须确定。 126 | 127 | 3. 容器适配器:如std::stack、std::queue和std::priority_queue等,它们不直接管理内存,而是基于其他容器(默认情况下是std::deque或std::vector)实现。因此,它们的内存分配特性跟随底层使用的容器。例如,基于std::vector的std::stack会将元素存储在堆上。 128 | 129 | 4. 内存分配器:STL容器使用一个名为Allocator的模板参数来控制内存的分配和释放,默认情况下使用的是std::allocator,它从自由存储区(即堆)分配内存。尽管如此,用户可以通过自定义Allocator来改变内存的来源,比如从栈上分配(尽管这在实践中很少见且具有挑战性,因为栈的大小限制和缺乏动态性)。 130 | 131 | 总的来说,STL容器大部分涉及动态内存分配的元素都存储在堆上,以适应元素数量的变化,而容器的管理结构如果作为局部变量,则位于栈上。 132 | -------------------------------------------------------------------------------- /c++/编译内存相关/images/C++的内存分布模型.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/C++的内存分布模型.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/Linux下的C++内存模型.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/Linux下的C++内存模型.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/shared_ptr 析构并释放共享资源weak_ptr的变化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/shared_ptr 析构并释放共享资源weak_ptr的变化.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/shared_ptr例子.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/shared_ptr例子.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/weak_ptr对象内存结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/weak_ptr对象内存结构.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/创建shared_ptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/创建shared_ptr.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/在linux下size命令可以查看一个可执行二进制文件基本情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/在linux下size命令可以查看一个可执行二进制文件基本情况.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/复制shared_ptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/复制shared_ptr.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/栈帧.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/栈帧.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/编译过程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/编译过程.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/虚拟内存与物理内存的联系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/虚拟内存与物理内存的联系.png -------------------------------------------------------------------------------- /c++/编译内存相关/images/隐藏.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/编译内存相关/images/隐藏.png -------------------------------------------------------------------------------- /c++/编译内存相关/内存对齐.md: -------------------------------------------------------------------------------- 1 | # 内存对齐 2 | 3 | ## 什么是内存对齐 4 | 5 | 内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 6 | 7 | ## 内存对齐的原则 8 | 9 | 1. 第一个成员在结构体首地址处,对齐到0处 10 | 2. 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除; 11 | 3. 结构体每个成员相对于结构体首地址的偏移量 (offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding); 12 | 4. 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。 13 | 14 | ## 进行内存对齐的原因:(主要是硬件设备方面的问题) 15 | 16 | 1. 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常; 17 | 2. 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作; 18 | 3. 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间; 19 | 4. 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap); 20 | 5. 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。 21 | 22 | - 内存对齐本质是:空间换时间。 23 | 24 | ## 内存对齐的优点 25 | 26 | 1. 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常; 27 | 2. 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。 28 | 29 | ## 修改编译器默认对齐数 30 | 31 | ```cpp 32 | #pragma pack(n) 33 | ``` 34 | 35 | - n 是一个整数,有效值通常为1、2、4、8、16,表示对齐的字节数。这意味着结构体成员将按照不超过n字节的边界对齐。 36 | 37 | ```cpp 38 | #include 39 | 40 | #pragma pack(push) // 保存当前的对齐设置 41 | #pragma pack(1) // 设置对齐为1字节 42 | 43 | struct MyStruct1 { 44 | char a; 45 | int b; 46 | double c; 47 | }; 48 | 49 | #pragma pack(pop) // 恢复之前的对齐设置 50 | 51 | struct MyStruct2 { 52 | char a; 53 | int b; 54 | double c; 55 | }; 56 | 57 | 58 | int main() { 59 | std::cout << "Alignment of MyStruct1: " << alignof(MyStruct1) << " bytes\n"; // 获取内存对齐大小 60 | std::cout << "Alignment of MyStruct2: " << alignof(MyStruct2) << " bytes\n"; 61 | 62 | std::cout << "Size of MyStruct: " << sizeof(MyStruct1) << " bytes\n"; 63 | std::cout << "Size of MyStruct: " << sizeof(MyStruct2) << " bytes\n"; 64 | 65 | return 0; 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /c++/编译内存相关/内存泄露.md: -------------------------------------------------------------------------------- 1 | # 内存泄露 2 | 3 | ## 什么是内存泄漏 4 | 5 | 内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。简单地说就是申请了一块内存空间,使用完毕后没有释放掉。 6 | 7 | - 并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。 8 | - 常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。 9 | - 使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete释放内存,否则这块内存就会造成内存泄漏。 10 | 指针重新赋值 11 | 12 | ## 常见的内存泄露场景 13 | 14 | (1)new和malloc申请资源使用后,没有用delete和free释放; 15 | (2)子类继承父类时,父类析构函数不是虚函数。 16 | (3)Windows句柄资源使用后没有释放。 17 | 18 | 举例: 19 | 20 | ```cpp 21 | char *p = (char *)malloc(10); 22 | char *p1 = (char *)malloc(10); 23 | p = np; 24 | ``` 25 | 26 | 开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。 27 | 28 | ## 怎么防止内存泄漏?内存泄漏检测工具的原理? 29 | 30 | 防止内存泄漏的方法: 31 | 32 | 1. 良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。 33 | 2. 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况) 34 | 3. 智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。 35 | 4. 将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。 36 | 5. 一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。 37 | - 使用VLD工具(Visual Leak Detector) 38 | 39 | ### VS下内存泄漏的检测方法(CRT) 40 | 41 | 检查方法: 42 | 在main函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出(不要定制调试),查看输出 43 | 在debug模式下以F5运行: 44 | 45 | ```cpp 46 | #define CRTDBG_MAP_ALLOC 47 | #include 48 | #include 49 | //在入口函数中包含 _CrtDumpMemoryLeaks(); 50 | //即可检测到内存泄露 51 | 52 | //以如下测试函数为例: 53 | int main(){ 54 | char* pChars = new char[10]; 55 | CrtDumpMemoryLeaks(); 56 | return 0; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /c++/编译内存相关/函数调用.md: -------------------------------------------------------------------------------- 1 | # 函数调用 2 | 3 | ## 函数栈帧(Stack Frame) 4 | 5 | 在大多数现代计算机体系结构中,函数调用的实现依赖于栈(stack)。每次函数调用时,都会在栈上创建一个新的栈帧(stack frame),用于存储函数的局部变量、函数参数、返回地址等信息。 6 | 7 | 栈帧的布局通常包括: 8 | 9 | - 函数参数:调用者传递给函数的参数按一定规则(通常是从右到左)压入栈中。 10 | - 返回地址:调用函数后应继续执行的下一条指令的地址。在函数返回前,控制权转移回此地址。 11 | - 局部变量:函数内部声明的变量存储在栈上。 12 | - 保存的寄存器:某些情况下,为了保护调用者环境,会将当前函数使用到的一些寄存器的值保存在栈上。 13 | 14 | ## 调用约定(Calling Convention) 15 | 16 | 不同的编程语言和编译器可能会有不同的调用约定,规定了函数调用时参数如何传递(寄存器或栈)、哪一方负责清理栈(调用者还是被调用者)、返回值如何传递等。常见的调用约定有cdecl、stdcall、fastcall等。 17 | 18 | ## 编译器的角色 19 | 20 | - 名字修饰(Name Mangling):由于C++支持函数重载,编译器需要一种方式来区分具有相同名字但参数类型或数量不同的函数。这通过名字修饰(也称作名称重整)实现,为每个函数生成一个唯一的内部名称,该名称包含了函数名、参数类型等信息。 21 | 22 | - 虚函数表(VTable):对于C++中的虚函数,编译器会在类的实例中添加一个指向虚函数表的指针。虚函数表是一个函数指针数组,包含了类及其基类中所有虚函数的地址。这允许在运行时动态决定调用哪个函数版本,支持多态性。 23 | 24 | - 模板实例化:对于模板函数,编译器在遇到具体类型实例化时,会生成具体的函数代码。这意味着每个不同的类型参数组合都会导致编译器生成独立的函数代码。 25 | 26 | ## 寄存器的角色 27 | 28 | - EIP/EIP(Instruction Pointer):在x86架构中,EIP寄存器指向当前正在执行的指令地址。函数调用时,调用函数的下一条指令地址(即返回地址)会被压栈,EIP被设置为被调用函数的入口地址。 29 | 30 | - ESP/EBP(Stack Pointer and Base Pointer):ESP指向栈顶,EBP指向栈帧的底部。函数调用时,EBP通常保存当前栈帧的顶部地址,然后ESP减去一定的空间为新栈帧腾出位置。 31 | 32 | ## 参数传递 33 | 34 | C++支持多种参数传递方式,包括值传递、引用传递、指针传递等。编译器根据函数定义和调用约定决定如何实际执行这些传递: 35 | 36 | - 值传递:实际参数的副本被压入栈或存储在寄存器中传递给函数。 37 | - 引用传递:传递实际参数的引用(即内存地址),允许函数直接修改原数据。 38 | - 指针传递:与引用传递相似,但更底层,提供了对裸内存地址的操作。 39 | 40 | ## 返回值处理 41 | 42 | - 基本类型和指针:直接通过寄存器(如EAX/RAX在x86架构中)或栈返回。 43 | - 复杂类型:对于较大的对象或结构体,通常会返回一个指向已分配内存(堆或栈)的指针或引用,而不是直接复制整个对象。 44 | 45 | ## 函数重载与解析 46 | 47 | C++编译器在编译时期通过类型检查和名字修饰来解析函数调用,确定调用哪一个重载版本。这涉及到类型匹配、类型转换规则以及模板特化。 48 | 49 | ## 异常处理 50 | 51 | 虽然不是直接与函数调用机制相关,但C++的异常处理机制会影响函数调用的流程。当函数抛出异常时,编译器会生成额外的代码来维护一个未处理异常的调用链,直到找到适当的catch块或导致程序终止。 52 | 53 | ## 函数调用过程 54 | 55 | 1. 参数传递:根据调用约定,参数被压入栈或存入寄存器。 56 | 2. 压入返回地址:调用指令执行前,当前指令的下一条指令地址被压入栈中。 57 | 3. 栈帧建立:调整ESP,为局部变量等预留空间,并可能保存EBP。 58 | 4. 执行函数体:控制权转移到被调用函数的入口地址。 59 | 5. 返回值处理:函数通过指定的机制(如EAX寄存器在x86架构中)返回值。 60 | 6. 栈帧销毁:恢复EBP(如果有保存的话),调整ESP回收栈空间。 61 | 7. 返回调用点:从栈顶弹出返回地址到EIP,控制权回到调用者。 62 | 63 | ## main函数调用 64 | 65 | ### 在main执行之前和之后执行的代码可能是什么? 66 | 67 | 1. main函数执行之前,主要就是初始化系统相关资源: 68 | 69 | - 设置栈指针 70 | - 初始化静态static变量和global全局变量,即.data段的内容 71 | - 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容 72 | - 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码 73 | - 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数 74 | - `__attribute__((constructor))` 75 | 76 | 2. main函数执行之后: 77 | - 全局对象的析构函数会在main函数之后执行; 78 | - 可以用 atexit 注册一个函数,它会在main 之后执行; atexit()函数注册了一个在正常程序终止时调用的函数。 79 | - `__attribute__((destructor))` 80 | -------------------------------------------------------------------------------- /c++/编译内存相关/变量.md: -------------------------------------------------------------------------------- 1 | # 变量 2 | 3 | ## 从作用域区分变量的种类 4 | 5 | - 全局变量和局部变量 6 | 7 | ### 全局变量 8 | 9 | #### 全局变量定义 10 | 11 | - 在所有函数外部定义的变量(通常是在程序的头部),称为全局变量。 12 | 13 | #### 全局变量作用域 14 | 15 | - 全局作用域: 16 | - 本文件内:从"定义变量的位置"开始到本"源文件"结束。 17 | - 所有文件内:使用extern等关键字声明全局作用域(只需要在一个源文件中定义,就可以作用于所有的源文件) 18 | - 全局变量可以被所有定义在全局变量之后的函数访问。 19 | - 声明在前,定义在后就可以。因此最好先声明,再在最下面定义实现。 20 | 21 | #### 全局变量生命周期 22 | 23 | - 从程序运行期一直存在,从程序开始到程序结束。 24 | - 在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。 25 | 26 | #### 全局变量使用注意 27 | 28 | - 屏蔽: 29 | - 在同一个源文件中,如果全局变量和局部变量同名,则在局部变量的作用范围内,全局变量被屏蔽,即不起作用。 30 | - 只能定义一次: 31 | - 如果再两个文件中都定义了相同名字的全局变量,则连接错误:变量重定义。 32 | - 使用不宜过多: 33 | - 全局变量不宜过多,应限制使用,因为过多会使各个含有全局变量的函数在执行时可能改变其值,程序容易出错。而能修改全局变量的语句又很多,这使调试程序变得困难。 34 | 35 | #### 全局变量全局变量重定义问题 36 | 37 | 1. 原因 38 | 39 | - 外部链接性 40 | 首先明确一点全局变量是具备 外部链接性的,何为外部连接性呢?外部链接性的变量通常简称为外部变量,作用域为整个文件。可以在文件中位于外部变量定义的后面的任意函数中使用它,因此也称为全局变量。 41 | 42 | 2. 头文件的作用 43 | 当.c或者.cpp引用头文件的时候,无非是将头文件的内容拷贝入本.c或者.cpp。当我们在头文件中定义一个全局变量,而又在多个.c或者.cpp文件中引用它,就会引发全局变量重定义。话不多说看图:本图中B.cpp和C.cpp都引用了a.h等价于在B.cpp和C.cpp中都声明了全局变量。而全局变量的作用域是整个文件,故而出现了全局变量重定义的问题。 44 | 45 | 3. 解决办法: 46 | 1. 定义和声明 47 | 1. 变量定义: 用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。 48 | 2. 变量声明: 用于向程序表明变量的类型和名字。 49 | 3. 注意 50 | - 变量在使用前就要被定义或者声明。 51 | - 在一个程序中,变量只能定义一次,却可以声明多次。 52 | - 定义分配存储空间,而声明不会。 53 | - C++程序通常由许多文件组成,为了让多个文件访问相同的变量,C++区分了声明和定义。 54 | 2. 解决经验 55 | 1. 永远不要在.h文件中定义变量,这样容易导致重复定义错误。 56 | 2. 尽量使用static关键字把变量定义限制于该源文件作用域,除非变量被设计成全局的。 57 | 3. 可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。 58 | 4. 头文件(.h)中 是对于该模块接口的声明,接口包括该模块提供给其它模块调用的外部函数及外部全局变量,对这些变量和函数都需在.h中文件中冠以extern关键字声明。 59 | 60 | 3. 使用static解决全局变量初始化带来的重复定义问题。 61 | 1. 全局变量范围只能限定在文件里,不会外联到整个模块和项目中。 62 | 2. 变量只在源文件中有效。 63 | - 解决函数重定义:引入 inline关键字修饰函数 64 | 65 | ### 局部变量 66 | 67 | #### 局部变量定义 68 | 69 | 在“函数内部”或一个“代码块内部”声明的变量,称为局部变量。 70 | 71 | - 使用 { } 括起来的一段代码可称为代码块。 72 | 73 | #### 局部变量生命周期 74 | 75 | 程序运行处的==局部作用域==。(块作用域,函数作用域,类作用域,程序全局作用域) 76 | 77 | - 局部变量只在函数调用时存在,一旦调用结束,局部变量就被销毁。 78 | 79 | #### 局部变量作用域 80 | 81 | 只在当前函数内部和代码段内部。 82 | 83 | - 函数的形参也是局部变量。 84 | - 主函数无法使用其他函数中定义的局部变量。同理其他函数也无法使用主函数中的变量 85 | - 不同的函数可使用同名的局部变量,它们互不干扰。 86 | 87 | ### 静态全局变量:static int = 1 88 | 89 | #### 静态全局变量定义 90 | 91 | 在所有函数外部使用"static"定义:static int i = 0; 92 | 93 | - static关键字,const关键字(注意C/C++意义不同) 94 | 95 | 先来解释一下C++中const全局变量的作用域: 96 | 97 | 1. C++中的const修饰的全局常量具有跟static相同的特性(有条件的,const放在只读静态存储区),即它们只能作用于本编译模块中,可是const可以与extern连用来声明该常量可以作用于其他编译模块中. 98 | 2. C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中.而在C中,const是一个不能被改变的普通变量。 99 | 100 | #### 静态全局变量生命周期 101 | 102 | 程序运行期一直存在,从程序开始到程序结束; 103 | 104 | - static没有改变生命周期。 105 | 106 | #### 静态全局变量作用域 107 | 108 | 文件作用域(只在被定义的文件中可见:static的一个作用就是隐藏) 109 | 110 | - 使用static改变了作用域,只能在被定义文件中可见。 111 | 112 | #### 静态全局变量内存分布 113 | 114 | - 全局(静态存储区)。 115 | 116 | #### 静态全局变量注意点 117 | 118 | - 只被定义一次。 119 | 120 | ### 静态局部变量 121 | 122 | #### 静态局部变量定义 123 | 124 | 局部作用域中用static定义。 125 | 126 | #### 静态局部变量生命周期 127 | 128 | 在静态存储区,整个程序运行期间一直存在。 129 | 130 | #### 静态局部变量作用域 131 | 132 | 局部作用域(只在局部作用域可见,超过其作用域便无法被引用) 133 | 134 | #### 静态局部变量内存分布 135 | 136 | 全局(静态存储区)。 137 | 138 | #### 静态局部变量注意点 139 | 140 | 只能被初始化一次,多线程中需要加锁保护。 141 | 142 | ### 静态成员变量 143 | 144 | #### 静态成员变量定义 145 | 146 | static修饰的成员变量 147 | 148 | - 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。 当通过对象名访问时,对于不同的对象,访问的是同一份内存。 149 | 150 | #### 静态成员变量初始化 151 | 152 | - 类外:类型 类名::静态成员变量名 = 赋值; 153 | 154 | #### 静态成员变量访问:(三种访问方式) 155 | 156 | 1. 类名::静态成员变量 157 | 2. 对象名.静态成员变量 158 | 3. 对象指针->静态成员变量 159 | 160 | #### 静态成员变量生命周期 161 | 162 | 和静态变量一样,程序运行期一直存在; 163 | 164 | #### 静态成员变量作用域 165 | 166 | 文件作用域 167 | 168 | #### 静态成员变量内存分布 169 | 170 | 全局(静态存储区) 171 | 172 | ### 类的成员函数里定义的静态变量 173 | 174 | #### 类的成员函数里定义的静态变量定义 175 | 176 | 类的成员函数中定义static变量。 177 | 178 | #### 类的成员函数里定义的静态变量初始化 179 | 180 | 未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化) 181 | 182 | #### 类的成员函数里定义的静态变量特点 183 | 184 | 和类的静态变量没有差别,所有的成员函数都共享这个静态变量 185 | 186 | #### 类的成员函数里定义的静态变量生命周期 187 | 188 | 在静态存储区,整个程序运行期间一直存在。 189 | 190 | #### 类的成员函数里定义的静态变量作用域 191 | 192 | 作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变; 193 | 194 | #### 类的成员函数里定义的静态变量内存分布 195 | 196 | 全局(静态存储区) 197 | 198 | ## 变量的区别 199 | 200 | ### 全局变量、局部变量、静态全局变量、静态局部变量的区别 201 | 202 | - 全局变量就是定义在函数外的变量。 203 | - 局部变量就是函数内定义的变量。 204 | - 静态变量就是加了static的变量。 例如:static int value = 1 205 | 206 | ### 各自存储的位置 207 | 208 | - 全局变量,存储在常量区(静态存储区)。 209 | - 静态变量,存储在常量区(静态存储区)。 210 | - 局部变量, 存储在栈区。 211 | 212 | 注意: 因为静态变量都在静态存储区(常量区),所以下次调用函数的时候还是能取到原来的值。 213 | 214 | ### 各自初始化的值 215 | 216 | - 局部变量, 存储在栈区。局部变量一般是不初始化的, 217 | - 局部变量, 存储在栈区。全局变量和静态变量,都是初始化为0的,有一个初始值。 218 | - 局部变量, 存储在栈区。如果是类变量,会调用默认构造函数初始化。 219 | 220 | ### 从作用域看 221 | 222 | C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。 223 | 224 | - 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。会一直存在到程序结束。 225 | - 静态全局变量:全局作用域+文件作用域,所以无法在其他文件中使用。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。 226 | - 局部变量:具有局部作用域。比如函数的参数,函数内的局部变量等等;它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被销毁,其所占用的内存也被收回。 227 | - 静态局部变量:具有局部作用域。它只被初始化一次, 直到程序结束。自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。 228 | 229 | ### 从分配内存空间看 230 | 231 | - 静态存储区:全局变量,静态局部变量,静态全局变量。 232 | - 栈:局部变量。 233 | 234 | ### 各自的应用场景 235 | 236 | - 局部变量就是我们经常用的,进入函数,逐个构造,最后统一销毁。 237 | - 全局变量主要是用来给不同的文件之间进行通信。 238 | - 静态变量:只在本文件中使用,局部静态变量在函数内起作用,可以作为一个计数器。 239 | 240 | ```cpp 241 | void func(){ 242 | static int count; 243 | count ++; 244 | } 245 | int main(int argc, char** argv){ 246 | for(int i = 0; i < 10; i++) 247 | func(); 248 | } 249 | ``` 250 | 251 | ### 说说静态变量在代码执行的什么阶段进行初始化? 252 | 253 | ```cpp 254 | static int value //静态变量初始化语句 255 | ``` 256 | 257 | 对于C语言: 静态变量和全局变量均在编译期进行初始化,即初始化发生在任何代码执行之前。 258 | 对于C++: 静态变量和全局变量仅当首次被使用的时候才进行初始化。 259 | 260 | 助记: 如果你使用过C/C++你会发现,C语言要求在程序的最开头声明全部的变量,而C++则可以随时使用随时声明;这个规律是不是和答案类似呢? 261 | 262 | ### 全局变量定义在头文件中有什么问题? 263 | 264 | 如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。 265 | -------------------------------------------------------------------------------- /c++/编译内存相关/深拷贝与浅拷贝.md: -------------------------------------------------------------------------------- 1 | # 深拷贝与浅拷贝 2 | 3 | - c++默认的拷贝构造函数是浅拷贝 4 | 浅拷贝就是对象的数据成员之间的简单赋值,如你设计了一个类而没有提供它的复制构造函数,当用该类的一个对象去给另一个对象赋值时所执行的过程就是浅拷贝。当数据成员中没有指针时,浅拷贝是可行的;但**当数据成员中有指针时,如果采用简单的浅拷贝**,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。 5 | 6 | - 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,而不是一个简单的赋值过程,从而也就解决了指针悬挂的问题。 7 | -------------------------------------------------------------------------------- /c++/编译内存相关/生命周期和作用域.md: -------------------------------------------------------------------------------- 1 | # 生命周期和作用域 2 | 3 | ## 变量的存储类别 4 | 5 | - auto 自动变量(动态存储方式) 6 | - register 寄存器变量(动态存储方式) 7 | - static 静态变量(静态存储方式) 8 | - extern   外部变量(静态存储方式) 9 | 10 | ## 生命周期(存储类型) 11 | 12 | - 生命周期指数据在内存中保留的时间,也可称为存储持续性。 13 | - 变量从定义(分配空间)到销毁(空间被收回)的时间段,称为变量的生命周期。 14 | 15 | 1. 自动存储:如函数定义时声明的变量就属于自动存储类别。生命周期较短,仅在函数被调用到函数执行结束后其内存就会被释放。 16 | 2. 静态存储:在函数定义外声明的变量(全局变量)、使用关键字static声明的变量都为静态存储类别。它们在整个程序运行过程中都存在。 17 | 3. 线程存储:在并发、并行环境中,如果变量使用关键字 thread_local声明,则生命周期和所依附的线程生命周期同步。 18 | 4. 动态存储:使用``new``运算符声明的变量,其存储块一般在堆中,如果开发者不显示释放``delete``会一直存在,直到程序结束。 19 | 20 | ### 自动存储 21 | 22 | 函数体内声明的变量属于自动存储类别。变量在函被调用时生命开始(分配空间),函数执行完毕后,变量的生命结束(回收空间)。此类型的变量的特点: 23 | 24 | - 局部的。 25 | - 没有共享性。 26 | 27 | #### 共享性 28 | 29 | 共享性指变量中的数据是否能让其它的代码可见、可用。 30 | 局部变量的局部的含义可以理解为不共享,作用域范围只供自己使用。 31 | 32 | ```c++ 33 | #include 34 | void test(){ 35 | int tmp=10; 36 | } 37 | int main(int argc, char** argv) { 38 | int tmp=20; 39 | test(); 40 | return 0; 41 | } 42 | 在函数 test中声明的 tmp变量只有在 test函数被调用时才会分配空间,当函数调用结束后自动释放。 43 | 同时 main中 tmp变量也局部变量。虽然 test和 main函数中有同名的 tmp变量,两者是互不可见的,或者说两者存在于 2 个不同的时空中。 44 | ``` 45 | 46 | #### 为什么会互不可见? 47 | 48 | - 原因可用函数的底层调用机制解释: 49 | - C++调用函数时,会在栈中为函数分配一个区域用来存储此函数有关的数据,称这个区域叫栈帧。 50 | - 每一个函数所分配到的栈帧是隔离的,且按先调用先分配的栈原则。 51 | ![栈帧](./images/栈帧.png) 52 | 53 | #### 变量间的隐藏性 54 | 55 | ```c++ 56 | #include 57 | using namespace std; 58 | int main(int argc, char** argv) { 59 | int temp=20; 60 | { 61 | int temp=10; 62 | cout<<"代码块中输出:"<当执行流从高级别的作用域进入低级别作用域后,如果有同名变量,则会隐藏高级别变量的可见性。 83 | 当再次从低级别作用域返回高级别作用域后,高级别作用域中的同名变量会变得可见。 84 | 85 | 2. 在同一个作用域内是不能有同名变量的,如下代码,会报错。 86 | 87 | ```c++ 88 | int main(int argc, char** argv) { 89 | //函数体内这一范围内不能出现同名变量 90 | int guoKe; 91 | int guoKe; 92 | return 0; 93 | } 94 | ``` 95 | 96 | ```c++ 97 | int main(int argc, char** argv) { 98 | { 99 | //同一代码块中不能出现同名变量 100 | int guoKe; 101 | int guoKe; 102 | } 103 | return 0; 104 | } 105 | ``` 106 | 107 | 3. 在C++ 中有 2 个与自动存储变量相关的关键字: 108 | 109 | - auto: auto关键字在C++ 11以前的版本和 C语言中,用来显示指定变量为自动存储。 C++ 11中表示自动类型推断。 110 | - register:此关键字由C语言引入,如果有 register关键字的变量声明为寄存器变量,目的是为加快数据的访问速度。而在C++ 11中的语义是显示指定此变量为自动存储,和以前的 auto 功能相同。 111 | 112 | ### 静态存储 113 | 114 | C++对内存进行管理划分时,除了分有栈和堆之外,还分有==全局\静态区域==(还有常量区域、自由存储区域),具有静态存储类别的变量被存储在此区域。 115 | 116 | 静态存储变量的特点: 117 | 118 | - 生命周期长。其生命周期从变量声明开始,可以直到程序结束。 119 | - 生命周期长,并不意味着谁都可以看得见它,谁都可以使用它。其作用域有外部可见、内部可见、局部可见 3 种情形。 120 | 121 | #### 外部可见 122 | 123 | 外部可见作用域,可认为在整个程序中可用。此类型变量为广义上的全局变量。 124 | >一个有一定规模的程序往往会有多个源代码文件。 125 | 126 | ```c++ 127 | #include 128 | int guoKe; 129 | using namespace std; 130 | int main(int argc, char** argv) { 131 | cout<本文件可使用的范围指从变量声明位置开始一直到文件结束的任一位置都能使用。外部文件可使用指在另一个文件中也可以使用。 139 | 140 | 如果要在文件的外部使用,需要使用 extern变量说明符。 141 | >如果在整个程序运行期间,需要一个在整个程序中大家都能访问到的全局可用的变量时,则可以使用外部可见的存储方案。 142 | 143 | #### 内部可见 144 | 145 | 在文件内当使用 static关键字声明的变量其作用域为本文件可见,也就是内部可见。变量只能在声明的文件内使用,不能在外部文件中使用,也是广义上的全局变量。 146 | 147 | ```c++ 148 | static int chengxuyuan = 996; 149 | ``` 150 | 151 | #### 局部可见 152 | 153 | 在函数体内使用``static``声明的变量, 如下声明语句,则认为变量的作用域是局部可见,变量只能在声明它的函数体内使用。也是广义上的局部变量。 154 | 155 | ```c++ 156 | #include 157 | using namespace std; 158 | void test(){ 159 | //静态局部变量 160 | static int temp=20; 161 | temp++; 162 | cout<变量的声明位置也决定了变量在内存中的存储位置,如函数体内声明的局部变量一般会存储在栈中,如类中声明的变量存储在堆中,文件中声明的全局变量存储在全局\静态存储区。 194 | 195 | #### C++细分 196 | 197 | C++可细分为: 198 | 199 | - 全局作用域:全局变量 200 | - 局部作用域:局部变量、静态局部变量 201 | - 语句作用域 202 | - 类作用域 203 | - 命名空间作用域 204 | - 文件作用域 205 | 206 | #### 常量的作用域 207 | 208 | const修饰的变量称为常量 209 | 210 | - c++中,const常量作用域是local(内部链接)(只能在本文件中可用) 211 | - const static 共同修饰的变量,拥有外部链接性(可以在其他文件使用) 212 | -------------------------------------------------------------------------------- /c++/编译内存相关/虚拟内存.md: -------------------------------------------------------------------------------- 1 | # 虚拟内存 2 | 3 | ## 物理内存与虚拟内存 4 | 5 | 物理内存: 6 | 7 | - 物理内存实际上是CPU中能直接寻址的地址线条数。由于物理内存是有限的,例如32位平台下,寻址的大小是4G,并且是固定的。内存很快就会被分配完,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的。 8 | 9 | - 虚拟内存: 10 | 在进程创建的时候,系统都会给每个进程分配4G的内存空间,这其实是虚拟内存空间。进程得到的这4G虚拟内存,进程自身以为是一段连续的空间,而实际上,通常被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,需要的时候进行数据交换。 11 | 12 | 关于虚拟内存与物理内存的联系,下面这张图可以帮助我们巩固。 13 | ![虚拟内存与物理内存的联系](images/虚拟内存与物理内存的联系.png) 14 | 15 | ## 虚拟内存机理 16 | 17 | 1. 地址映射:虚拟内存系统使用页表或段表等数据结构,将进程看到的虚拟地址(逻辑地址)映射到实际的物理地址。每个进程拥有独立的地址空间,这增加了系统的安全性和稳定性。 18 | 19 | 2. 分页与分段:虚拟内存通常采用分页或分段(或两者的结合)来管理内存。分页系统将内存分为固定大小的块(页面),而分段则是基于逻辑模块划分内存。页面或段不在使用时可以被换出到硬盘上,腾出物理内存供其他数据使用。 20 | 21 | 3. 页面置换算法:当物理内存不足以存放新的数据时,系统需要选择某些页面从内存中移除(换出)到硬盘上,这称为页面置换。常见的页面置换算法有最近最少使用(LRU)、先进先出(FIFO)等。 22 | 23 | 4. 请求调页:当程序尝试访问一个尚未加载到物理内存中的页面时,会发生缺页中断,操作系统会负责将所需页面从硬盘读入内存,这个过程称为请求调页。 24 | 25 | ## 虚拟内存是如何工作的? 26 | 27 | - 当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。 28 | 29 | - 另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。 30 | 31 | - 可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的) 32 | 33 | ## 虚拟内存优点 34 | 35 | 1. 扩大内存容量:通过虚拟地址空间,应用程序可以使用超过物理内存大小的地址空间,解决了物理内存有限的问题。 36 | 37 | 2. 内存保护:每个进程看到的是独立的地址空间,操作系统可以限制进程只能访问自己分配的地址区域,提高了系统的安全性。 38 | 39 | 3. 内存共享:多个进程可以映射到相同的物理内存页面,便于实现共享库的使用和内存的有效利用。 40 | 41 | 4. 减少碎片:通过动态的内存分配和回收,虚拟内存有助于减少内存碎片,提高内存利用率。 42 | 43 | 5. 提高程序兼容性:程序员可以编写程序时不必考虑实际物理内存的限制,降低了程序的编写难度。 44 | 45 | 6. 提升系统稳定性:即使某个进程崩溃或异常,也不会影响到其他进程的地址空间,增强了系统的稳定性和可靠性。 46 | 47 | 然而,虚拟内存的使用也带来了一些潜在缺点,如增加的I/O开销(频繁的硬盘读写)可能导致系统响应变慢,以及在物理内存和虚拟内存间频繁交换数据可能导致的性能下降。因此,合理配置虚拟内存大小和优化页面置换策略对系统性能至关重要。 48 | 49 | ## 利用虚拟内存机制的优点? 50 | 51 | - 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系 52 | 53 | - 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存 54 | 55 | - 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存 56 | -------------------------------------------------------------------------------- /c++/语言对比/C 和 C++ 的区别.md: -------------------------------------------------------------------------------- 1 | # C 和 C++ 的区别 2 | ## 面向对象和面向过程 3 | - 面向过程的思路:面向过程编程就是分析出解决问题的步骤,然后把这些步骤一步一步的实现,使用的时候一个一个的依次调用就可以了。 4 | - 面向对象的思路:面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。 5 | 6 | 举个例子:(玩五子棋) 7 | 1. 用面向过程的思想来考虑就是:开始游戏,白子先走,绘制画面,判断输赢,轮到黑子,绘制画面,判断输赢,重复前面的过程,输出最终结果。 8 | 2. 用面向对象的思想来考虑就是:黑白双方(两者的行为是一样的)、棋盘系统(负责绘制画面)、规定系统(规定输赢、犯规等)、输出系统(输出赢家)。 9 | 面向对象就是高度实物抽象化(功能划分)、面向过程就是自顶向下的编程(步骤划分) 10 | 11 | ### 面向过程语言: 12 | - 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 13 | - 缺点:没有面向对象易维护、易复用、易扩展 14 | 15 | ### 面向对象语言: 16 | - 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 17 | - 缺点:性能比面向过程低 18 | 19 | ## 区别和联系: 20 | ### 动态内存管理 21 | - C和C++一个典型的区别就在动态内存管理上了,C语言通过malloc和free来进行堆内存的分配和释放,而C++是通过new和delete来管理堆内存的; 22 | 23 | ### 强制类型转换 24 | - 强制类型转换上也不一样,C的强制类型转换使用()小括号里面加类型进行类型强转的,而C++有四种自己的类型强转方式,分别是const_cast,static_cast,reinterpret_cast和dynamic_cast; 25 | 26 | ### 输入输出 27 | - C和C++的输入输出方式也不一样,printf/scanf,和C++的cout/cin的对别,前面一组是C的库函数,后面是ostream和istream类型的对象。 28 | 29 | ### 命名空间 30 | - C++还支持namespace名字空间,可以让用户自己定义新的名字空间作用域出来,避免全局的名字冲突问题。 31 | 32 | ### 应用领域 33 | - 应用领域:C 语言主要用于嵌入式领域,驱动开发等与硬件直接打交道的领域,C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域。 34 | 35 | ### 增加更多关键字 36 | - C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。 37 | 38 | ### 更多新特性 39 | - C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。 40 | 41 | ## 易混淆的点 42 | 43 | ### 函数参数 44 | C语言本身没有直接支持按引用传递参数的方式,如C++中的引用(&)。**在C语言中,所有的函数参数传递都是值传递**,也就是说,函数接收的是参数的副本。但是,可以通过传递指针(或指针的指针)来模拟按引用传递的效果,从而在函数内部修改外部变量的值。 45 | 46 | 当你将一个指针作为函数参数时,虽然传递给函数的是指针的副本,但这个副本指向的地址与原指针相同。因此,通过这个指针,你可以在函数内部修改外部变量的值,达到类似“按引用传递”的效果。 47 | 48 | 例如,在前面讨论的insertNode函数中,使用Node** head就是利用指针的指针来实现对head指针本身的修改,模拟了按引用传递的功能。虽然传递的是指针的副本,但实际上达到了修改外部head指针指向的目的。 49 | 50 | ### typedef 51 | C语言的typedef对结构体进行重命名,可以实现不写struct。C++原生支持省略struct 52 | 53 | -------------------------------------------------------------------------------- /c++/语言对比/Python 和 C++ 的区别.md: -------------------------------------------------------------------------------- 1 | # Python 和 C++ 的区别 2 | - 语言自身:Python 为脚本语言,解释执行,不需要经过编译;C++ 是一种需要编译后才能运行的语言,在特定的机器上编译后运行。 3 | 4 | - 运行效率:C++ 运行效率高,安全稳定。原因:Python 代码和 C++ 最终都会变成 CPU指令来跑,但一般情况下,比如反转和合并两个字符串,Python 最终转换出来的 CPU 指令会比 C++ 多很多。首先,Python中涉及的内容比 C++ 多,经过了更多层,Python 中甚至连数字都是 object ;其次,Python 是解释执行的,和物理机CPU 之间多了解释器这层,而 C++ 是编译执行的,直接就是机器码,编译的时候编译器又可以进行一些优化。 5 | 6 | - 开发效率:Python 开发效率高。原因:Python 一两句代码就能实现的功能,C++ 往往需要更多的代码才能实现。 7 | 8 | - 书写格式和语法不同:Python 的语法格式不同于其 C++ 定义声明才能使用,而且极其灵活,完全面向更上层的开发者。 -------------------------------------------------------------------------------- /c++/语言特性相关/images/类列表初始化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/c++/语言特性相关/images/类列表初始化.png -------------------------------------------------------------------------------- /c++/语言特性相关/move函数与移动语义.md: -------------------------------------------------------------------------------- 1 | # 移动语义 与 move函数 2 | 3 | ## 移动语义 4 | 移动语义(Move Semantics)是C++11引入的一项重要特性,旨在优化资源管理,特别是对于那些拥有动态分配内存或大型数据结构的对象。移动语义通过所谓的“移动构造函数”(move constructor)和“移动赋值运算符”(move assignment operator)来避免不必要的深拷贝,从而提高程序的性能。下面是移动语义的详细解释: 5 | 6 | ### 基本概念 7 | - 右值引用:右值引用(rvalue reference)是移动语义的基础,它由两个与符号(&&)表示,如 T&&。右值引用可以绑定到临时对象或即将被销毁的对象(称为右值),这使得可以安全地“偷取”或“移动”这些对象的资源,而无需复制。 8 | [左值和右值](../语言特性相关/左值和右值.md) 9 | 10 | - 移动构造函数:当一个临时对象或将要被丢弃的对象被用于创建新对象时,移动构造函数会被调用。它将资源(如内存指针)从临时对象直接转移到新对象,同时将临时对象置为有效的但无害的状态(例如,清空指针)。移动构造函数的声明形式通常为 ClassName(ClassName&& other)。 11 | [移动构造函数](../面向对象/类与对象.md/#移动构造函数) 12 | 13 | - 移动赋值运算符:当一个对象通过赋值操作符 = 接收一个右值时,移动赋值运算符被调用,执行类似移动构造函数的操作,将资源从右值对象移动到左侧对象,并清理右侧对象。移动赋值运算符的声明形式为 ClassName& operator=(ClassName&& other)。 14 | [移动赋值运算符](../面向对象/类与对象.md/#移动赋值运算符) 15 | 16 | ### 为什么需要移动语义 17 | - 性能优化:对于含有动态分配内存或大量数据的类(如 std::vector、std::string),传统的深拷贝(复制构造函数和赋值运算符)可能非常昂贵。移动语义通过转移资源而非复制,大大减少了这类操作的成本。 18 | 19 | - 资源管理:移动语义有助于避免资源泄露和不必要的资源复制,提高程序的内存效率和响应速度。 20 | 21 | ### 如何使用移动语义 22 | - 自动应用:C++编译器在适当的情况下会自动选择移动构造函数或移动赋值运算符,例如,当临时对象被用作函数参数或返回值时。 23 | 24 | - 手动实现:开发者可以显式地定义自己的移动构造函数和移动赋值运算符,以优化特定类的移动行为。这通常涉及到交换资源指针和清空源对象的资源。 25 | 26 | ### 注意事项 27 | - 资源所有权:移动操作后,源对象(被移动的对象)应处于有效的但无害的状态,通常意味着它不再拥有任何资源,任何对它的后续使用都应该是安全的,尽管可能没有实际意义。 28 | 29 | - 与拷贝语义的区分:移动语义并不总是优于拷贝语义,二者有各自的应用场景。例如,当对象需要保持其状态的副本时,应使用拷贝而非移动。 30 | 31 | - 避免误用:误用 std::move 可能导致意料之外的结果,特别是当对象被多次移动或源对象在移动后被继续使用时。 32 | 33 | ## move函数 34 | ### 功能和用途 35 | 1. 资源转移而非复制:在C++中,当对象被复制时,通常会执行深拷贝,这可能导致性能开销,特别是对于大型对象或包含动态分配内存的对象。通过使用移动语义,可以避免这种开销,将资源的所有权直接从一个对象转移到另一个对象,而不是复制资源。 36 | 37 | 2. 转换左值为右值引用:std::move 的核心作用是将一个左值表达式转换为一个右值引用类型。这使得原本只能绑定到临时对象(右值)的移动构造函数或移动赋值运算符能够被调用,从而实现资源的转移。 38 | 39 | 3. 使用时机:当确定一个对象不再需要其内部资源时(例如,对象即将被销毁或其资源将被替换),可以使用 std::move 将资源转移给另一个对象,以避免不必要的复制。 40 | 41 | ### 标准库 move() 函数 42 | std::move是C++标准库中的一个 utility 函数,位于头文件中。它的主要作用是将一个左值(即便是一个非临时对象)转换为右值引用,从而暗示编译器这个对象可以被“移动”而不是被复制。这在处理大型对象或者资源拥有对象时特别有用,因为它允许实现更高效的资源转移,避免不必要的深拷贝操作。 43 | 44 | #### 例子 45 | ```cpp 46 | std::string str1("Hello, World!"); 47 | std::string str2 = std::move(str1); // 使用move,str1的资源可能会被转移到str2,str1可能被置为空或处于未定义状态 48 | ``` 49 | 在这个例子中,str1的内容没有被复制到str2,而是str1内部的资源(如内存缓冲区)被转移给了str2,这样可以更快并且避免了额外的内存分配。请注意,调用std::move后,源对象(在这里是str1)的状态通常是未指定的(除非特别处理),因此通常认为它已经被“移动出去”,不应再被使用。 50 | 51 | std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。 52 | ```cpp 53 | #include 54 | using namespace std; 55 | 56 | void fun1(int& tmp) 57 | { 58 | cout << "fun1(int& tmp):" << tmp << endl; 59 | } 60 | 61 | void fun2(int&& tmp) 62 | { 63 | cout << "fun2(int&& tmp)" << tmp << endl; 64 | } 65 | 66 | int main() 67 | { 68 | int var = 11; 69 | // fun1(12); // error:非常量引用值必须为左值 70 | fun1(var); 71 | // fun2(var);// error:无法将右值绑定到左值 72 | fun2(move(var)); // 将左值转换为右值 73 | fun2(1); // 正常调用右值 74 | } 75 | ``` 76 | 77 | ### std::move() 函数的实现原理 78 | ``std::move``的声明如下: 79 | ```cpp 80 | template< class T > 81 | typename std::remove_reference::type&& move(T&& t) noexcept; 82 | ``` 83 | 它接受一个universal reference(万能引用)作为参数,然后转换为对应类型的右值引用返回。实际上,std::move并不直接移动任何数据,而是创建了一个右值引用,这个引用可以被移动构造函数或移动赋值运算符用来执行实际的移动操作。 84 | 85 | 说明:引用折叠原理 86 | - 右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。 87 | - 左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。 88 | 89 | 小结:通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。 90 | ### remove_reference 具体实现: 91 | ```cpp 92 | //原始的,最通用的版本 93 | template struct remove_reference{ 94 | typedef T type; //定义 T 的类型别名为 type 95 | }; 96 | 97 | //部分版本特例化,将用于左值引用和右值引用 98 | template struct remove_reference //左值引用 99 | { typedef T type; } 100 | 101 | template struct remove_reference //右值引用 102 | { typedef T type; } 103 | 104 | //举例如下,下列定义的a、b、c三个变量都是int类型 105 | int i; 106 | remove_refrence::type a; //使用原版本, 107 | remove_refrence::type b; //左值引用特例版本 108 | remove_refrence::type b; //右值引用特例版本 109 | ``` 110 | 111 | 举例: 112 | ```cpp 113 | int var = 10; 114 | 115 | 转化过程: 116 | 1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&) 117 | 118 | 2. 此时:T 的类型为 int&,typename remove_reference::type 为 int,这里使用 remove_reference 的左值引用的特例化版本 119 | 120 | 3. 通过 static_cast 将 int& 强制转换为 int&& 121 | 122 | 整个std::move被实例化如下 123 | string&& move(int& t) 124 | { 125 | return static_cast(t); 126 | } 127 | ``` 128 | 129 | #### 总结std::move() 实现原理: 130 | - 利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变; 131 | - 然后通过 remove_refrence 移除引用,得到具体的类型 T; 132 | - 最后通过 static_cast<> 进行强制类型转换,返回 T&& 右值引用。 133 | 134 | ### 注意事项 135 | - 对象状态:使用 std::move 后,原对象的状态可能变得不可预测,通常认为是“被移动后的”或“无效的”,不应再被访问或使用。 136 | - 不保证移动操作:虽然 std::move 促使使用移动构造函数或移动赋值,但实际上是否发生移动取决于接收对象的实现。如果对象没有定义合适的移动构造函数或移动赋值运算符,编译器可能退回到拷贝操作。 137 | - 不改变实参:std::move 不改变实参本身,它仅仅改变了实参的类型,使得实参可以被当作右值使用。 138 | 139 | 140 | -------------------------------------------------------------------------------- /c++/语言特性相关/名称空间.md: -------------------------------------------------------------------------------- 1 | # 名称空间 2 | 3 | c++中名称空间是变量、函数、结构、枚举、类、类和结构的成员。名称相互可能冲突,不同类库也可能冲突。 4 | c++提供了名称空间工具,更好的控制名称的作用域。 5 | 6 | ## 传统的c++ 名称空间 7 | (1)声明区域:可以在其中进行声明的区域。 8 | 在函数外面声明全局变量,对于这种变量声明区域为其所在的文件。 9 | 在函数内声明的变量,声明区域为所在的代码块。 10 | (2)潜在作用域 11 | 变量的潜在作用域从声明点到声明区域的结尾,潜在作用域比声明区域要小。 12 | 变量对程序可见的范围被称为作用域,潜在作用域要大于作用域,例如局部变量会隐藏全局变量。 13 | (3)c++关于全局变量和局部变量的规则定义了i中名称空间层次。 14 | 每个区域都可以声明名称,这些名称独立于其他空间区域中中声明的名称,相互不冲突。 15 | 例如函数外是一个名称区域,每个函数内是一个独立的名称区域。 16 | 17 | ## 新的名称空间特性 18 | c++新增特性:通过定义一种新的声明区域来创建命名的名称空间,提供一个声明区域的名称。 19 | 一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。 20 | 例如使用namespace创建两个名称空间:Jack和Jill 21 | namespacee Jack{ 22 | double pail; 23 | void fetch(); 24 | int pal; 25 | struct Well {...}; 26 | } 27 | namespace Jil{ 28 | double bucket(double n) {...} 29 | double fetch; 30 | int pal; 31 | struct Hill {...}; 32 | } 33 | 名称空间可以是全局的,也可以位于另一个名称空间中但不能位于代码块中。默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。 34 | 除了用户定义的名称空间外,还存在另一个名称空间----全局名称空间(global namespace)/它对应于文件级声明区域,因此全局变量现在被描述为位于全局名称空间中。 35 | 通过作用域解析运算符::,来访问名称空间中的名称。 36 | Jack::pail = 12.34; 37 | jill::Hill mole; 38 | Jack::fetch(); 39 | 40 | 1. using声明 41 | using声明由被限定的名称和关键字using组成: 42 | using Jill:fetch; 43 | 44 | 2. using编译指令 45 | ``using namespace Jack;`` 46 | 在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用: 47 | ```cpp 48 | #include 49 | using namespace std; 50 | 在函数中使用using编译指令,将使该名称在函数中可用: 51 | int main(){ 52 | using namespace jack; 53 | } 54 | ``` 55 | 56 | 3. 区别 57 | using声明使一个名称可用。 58 | using编译指令是所有名称可用。区别于using声明,更像是大量使用作用域解析运算符。 59 | 如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名。 60 | 一般来说,using声明比using编译指令更安全,如果与局部名称发生冲突,则编译器会警告。 61 | 62 | 4. 其他特性 63 | (1)名称空间嵌套: 64 | ```cpp 65 | namespace elements 66 | { 67 | namespace fire 68 | { 69 | int flame; 70 | ... 71 | } 72 | float water; 73 | } 74 | flame指的是element::fire::flame 75 | using namespace element::fire; 76 | ``` 77 | 78 | (2)可以在名称空间中使用using编译指令和using声明 79 | ```cpp 80 | namespace myth 81 | { 82 | using Jill::fetch; 83 | using namespace element; 84 | using std::cout; 85 | using std::cin; 86 | } 87 | ``` 88 | 89 | (3)创建别名: 90 | ```cpp 91 | namespace my_favorite_things {...}; 92 | namespace mvft = my_favorite_things; 93 | ``` 94 | 可以用来简化对嵌套名称空间的使用: 95 | ```cpp 96 | namespace MEF = myth::elements::fire; 97 | using MEF::flame; 98 | ``` 99 | 100 | (4)未命名的名称空间: 101 | ```cpp 102 | namespace 103 | { 104 | int ice; 105 | int bandycoot; 106 | } 107 | ``` 108 | 这提供了链接性为内部的静态变量的替代品。 109 | 110 | ## 名称空间及其前途 111 | 1. 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。 112 | 2. 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。 113 | 3. 如果开发了一个函数库或类库,将其放在一个名称空间中。C++提倡将标准函数库放在std中。 114 | 4. 仅将编译指令using作为一种将旧代码转化为使用空间的权宜之计。 115 | 5. 不要在头文件中使用using编译指令,这样掩盖了要让哪些名称可用。包含头文件的顺序可能影响程序的行为。 116 | 如果非要用编译指令using namespace,则在预处理器编译指令#include后再使用. 117 | 6. 导入名称时,首先使用作用域解析运算符或using声明的方法。 118 | 7. 对于using声明,首选将其作用域设置为局部而不是全局。 119 | -------------------------------------------------------------------------------- /c++/语言特性相关/完美转发.md: -------------------------------------------------------------------------------- 1 | # 完美转发 2 | C++完美转发(Perfect Forwarding)是一种技术,它允许在函数模板中将参数原封不动地传递给另一个函数,保持参数的类型(包括左值和右值属性)、以及是否为引用等特性不变。这对于编写高度灵活和高效的泛型代码尤为重要,特别是在实现委托构造函数、转发函数参数到其他构造函数或函数调用时。完美转发主要依赖于两个C++11特性:std::forward函数和 universal references(通用引用,通常写作T&&,其中T是一个模板参数)。 3 | 4 | ## 1. Universal References(通用引用) 5 | 通用引用是C++中的一种特殊类型的引用,其声明形式为T&&,这里的T是一个模板参数。在某些上下文中,这样的引用可以绑定到任何类型的对象,无论是左值还是右值,并且能够保留原对象的左值/右值属性。当一个universal reference绑定到一个左值时,它实际上表现为一个左值引用(T&);绑定到右值时,则表现为一个右值引用(T&&)。这是完美转发的基础。 6 | 7 | ## 2. std::forward的作用 8 | std::forward函数位于头文件中,它的作用是根据传递给它的对象的类型(左值或右值),决定是否加上&引用符号,从而保持对象的原始属性。其原型为: 9 | ```cpp 10 | template 11 | T&& forward(typename std::remove_reference::type& t) noexcept; 12 | template 13 | T&& forward(typename std::remove_reference::type&& t) noexcept; 14 | ``` 15 | 这里,std::remove_reference用于去除T的引用属性,确保forward能正确处理T本身。然后,通过两个重载版本,forward可以判断其参数是左值还是右值,并相应地返回左值引用或右值引用。 16 | 17 | ## 3. 完美转发的应用示例 18 | 考虑一个通用的"Wrapper"函数,它接受任意类型的参数并将其转发给另一个函数: 19 | ```cpp 20 | template 21 | void callFunc(Func f, Args&&... args) { 22 | f(std::forward(args)...); // 使用完美转发 23 | } 24 | ``` 25 | 在这个例子中,Args&&... args是通用引用,它可以接收任何类型的参数,同时保持参数的左值/右值属性。当调用f并将args...通过std::forward传递时,每个参数的原始属性都会被保留。这意味着,如果args之一是一个临时对象(右值),那么这个右值性会被传递给f,允许f执行移动语义(如果支持的话),从而提高效率。 26 | 27 | ## 4. 注意事项 28 | 完美转发仅在转发到另一个函数或构造函数时有意义,目的是为了保持调用者提供的参数特性。 29 | 当使用完美转发时,需要确保转发的目标函数能够正确处理转发的参数类型(左值或右值引用)。 30 | 在设计接受通用引用的函数或方法时,应谨慎考虑何时使用std::forward,避免不必要的或错误的转发行为。 31 | 32 | 33 | -------------------------------------------------------------------------------- /c++/语言特性相关/左值和右值.md: -------------------------------------------------------------------------------- 1 | # 左值和右值,左值引用和右值引用 2 | ## 左值(Lvalue) 3 | - 定义:左值是指向内存位置的表达式,该位置可以被多次读写。简而言之,左值是有名字的持久对象,可以出现在赋值运算符的左侧。例如,变量名、解引用的指针或引用都是左值。 4 | - 通俗理解:左值是指具有对应的可由用户访问的存储单元,并且能由用户改变其值的量。如一个变量就是一个左值,因为它对应着一个存储单元,并可由编程者通过变量名访问和改变其值。 5 | - 左值(Lvalue) →→ Location 6 | - 表示内存中可以寻址,可以给它赋值(const类型的变量例外) 7 | - 特点:可寻址,可修改(除非是const或volatile修饰的)。 8 | - 例子:int x; 中的 x,或者 ptr->value(假设ptr是一个有效指针)。 9 | 10 | ## 右值(Rvalue) 11 | - 定义:右值是临时的、通常不可修改的表达式,通常用于表示数据的流动方向。右值要么是没有名字的对象(如字面量、临时对象),要么是将要被销毁的对象(如函数返回的局部变量)。 12 | - 分类: 13 | - 纯右值(prvalue):最典型的临时对象,非引用返回的临时变量( int func(void))、运算表达式产生的临时变量(b+c)、原始字面量(2)、lambda表达式等。 14 | - 将亡值(xvalue):表示即将“消亡”的对象,通常由std::move操作产生,用于转移资源所有权。将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值。 15 | - 特点:不可寻址,通常不可修改,生命周期短暂。 16 | - 例子:42,std::string("Hello"),或 std::move(someString)。 17 | 18 | ### 纯右值(prvalue) 19 | - 定义:纯右值是没有具体存储位置的临时对象,它们通常是新创建的对象或是计算的结果。纯右值不关联到任何持久存储上,因此它们既不能被取地址也不能被修改。 20 | - 例子:字面量(如42、"Hello")、大多数临时对象(如函数返回的非引用类型对象)、以及通过类型转换产生的临时对象。 21 | - 用途:常用于初始化新对象或作为非引用参数传递给函数。在C++17之后,某些情况下纯右值也可以直接初始化引用,但这是特例。 22 | 23 | ### 将亡值(xvalue) 24 | - 定义:将亡值是表达式的结果表示一个对象的资源即将被转移(通常是移动)给另一个对象,表明这个对象即将“消亡”。它是一个可以被移动的对象的“占位符”。 25 | - 例子:通过std::move函数转换的左值或右值引用表达式,或者某些类型的重载了&&操作符的对象的表达式。 26 | - 用途:将亡值主要用于实现高效的资源移动操作,避免不必要的深拷贝。当一个对象不再需要时,其资源可以被“移动”到另一个对象中,而不是复制,这样可以大大提高效率,特别是在处理大型对象或容器时。 27 | 28 | ### 纯右值和将亡值区别与联系 29 | - 本质差异:纯右值不涉及任何已有对象的资源转移,而将亡值则明确表示资源即将被转移。 30 | - 共同点:两者都是右值,意味着它们都可以绑定到右值引用上,且通常用于非保留的、一次性的操作。 31 | - 应用上的区别:在实现移动语义时,纯右值可能直接用于初始化或移动操作,而将亡值更强调对象资源的主动“放弃”,常用于优化资源管理。 32 | 33 | 34 | ## 左值和右值 35 | 通俗的讲,左值就是能够出现在赋值符号左面的东西,而右值就是那些可以出现在赋值符号右面的东西, 比如 int a = b + c;,a 就是一个左值,可以对a取地址,而b+c 就是一个右值,对表达式b+c 取地址会报错。 36 | 37 | 一个典型的例子: 38 | **a++**: 先使用a的值,再给a加1,作为**右值** 39 | ```cpp 40 | // a++的实现 41 | int temp = a; 42 | a = a + 1; 43 | return temp; 44 | ``` 45 | **++a**: 先加再用,作为**左值** 46 | ```cpp 47 | a = a + 1; 48 | return a; 49 | ``` 50 | 在C++中,临时对象不能作为左值,但是可以作为常量引用,const &。 51 | 52 | C++ 11中的std::move可将左值引用转化成右值引用。 53 | 54 | C++11中右值又由两个概念组成:将亡值和纯右值。 55 | 56 | ## 左值引用与右值引用 57 | - 左值引用(T&):绑定到左值,延长左值的生命周期,但不改变其可修改性。常规的引用一般都是左值引用。 58 | - 右值引用(T&&):设计用于绑定到右值,特别适用于转移资源的所有权(移动语义),如移动构造函数和移动赋值操作。 59 | 60 | ```cpp 61 | #include 62 | #include 63 | using namespace std; 64 | int main() 65 | { 66 | int var = 42; 67 | int &l_var = var; 68 | int &&r_var = var; // 错误:不能将右值引用绑定到左值上 69 | 70 | int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上 71 | return 0; 72 | } 73 | ``` 74 | 引用本身不拥有所绑定对象的内存,只是该对象的一个别名,左值引用就是有名变量的别名,右值引用是不具名变量的别名。因此**无论左值引用还是右值引用都必须立即进行初始化**。 75 | 76 | 通过右值引用,这个将亡的右值又“重获新生”,它的生命周期与右值引用类型变量的生命周期一样,只要这个右值引用类型的变量还活着,那么这个右值临时量就会一直活着,这是一重要特性,可利用这一点会一些性能优化,避免临时对象的拷贝构造和析构。 77 | 78 | 左值引用包括常量左值引用和非常量左值引用。非常量左值引用只能接受左值,不能接受右值;常量左值引用是一个“万能”的引用类型,可以接受左值(常量左值、非常量左值)、右值。不过常量左值所引用的右值在它的“余生”中只能是只读的。 79 | ```cpp 80 | int &a = 2; // 非常量左值引用 绑定到 右值,编译失败 81 | 82 | int b = 2; // b 是非常量左值 83 | const int &c = b; // 常量左值引用 绑定到 非常量左值,编译通过 84 | 85 | const int d = 2; // d 是常量左值 86 | const int &e = d; // 常量左值引用 绑定到 常量左值,编译通过 87 | const int &f =2; // 常量左值引用 绑定到 右值,编译通过 88 | ``` 89 | 右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。比如: 90 | ```cpp 91 | int a; 92 | int &&r1 = a; // 编译失败 93 | int &&r2 = std::move(a); // 编译通过 94 | ``` 95 | 96 | 简单总结: 97 | - 左值引用, 即&i, 是一种对象类型的引用; 右值引用, 即&&i, 是一种对象值的引用; 98 | - move() 函数可以把左值引用, 转换为右值引用; 99 | - 左值引用是固定的引用, 右值引用是易变的引用, 只能引用字面值(literals)或临时对象(temporary object); 100 | - 右值引用主要应用在移动构造器(move constructor) 和移动-赋值操作符(move-assignment operator)上面; 101 | 102 | ```cpp 103 | #include 104 | #include 105 | 106 | int main (void) { 107 | int i = 42; 108 | int &lr = i; 109 | int &&rr = i*42; 110 | const int &lr1 = i*42; 111 | int &&rr1 = 42; 112 | int &&rr2 = std::move(lr); 113 | std::cout << "i = " << i << std::endl; 114 | std::cout << "lr = " << lr << std::endl; 115 | std::cout << "rr = " << rr << std::endl; 116 | std::cout << "lr1 = " << lr1 < 59 | #include 60 | 61 | int main() { 62 | std::map mapExample = {{"apple", 1}, {"banana", 2}, {"cherry", 3}}; 63 | 64 | for (const auto& pair : mapExample) { 65 | std::cout << pair.first << ": " << pair.second << '\n'; 66 | } 67 | // 输出: 68 | // apple: 1 69 | // banana: 2 70 | // cherry: 3 71 | } 72 | ``` 73 | 74 | 范围for语句不仅简化了代码,减少了潜在的错误(如迭代器的误用),还提高了代码的可读性和维护性。 75 | 76 | -------------------------------------------------------------------------------- /c++/面向对象/MyClass.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 实现一个标准的类 3 | */ 4 | 5 | #include 6 | #include 7 | #include // 用于std::move和std::swap 8 | 9 | class ExampleClass { 10 | public: 11 | // 默认构造函数 12 | ExampleClass() : data_(0), name_("Default") { 13 | std::cout << "Default constructor called." << std::endl; 14 | } 15 | 16 | // 构造函数 17 | explicit ExampleClass(int data, const std::string& name = "") 18 | : data_(data), name_(name) { 19 | std::cout << "Constructor with parameters called." << std::endl; 20 | } 21 | 22 | // 析构函数 23 | ~ExampleClass() { 24 | std::cout << "Destructor called." << std::endl; 25 | } 26 | 27 | // 拷贝构造函数 28 | ExampleClass(const ExampleClass& other) 29 | : data_(other.data_), name_(other.name_) { 30 | std::cout << "Copy constructor called." << std::endl; 31 | } 32 | 33 | // 拷贝赋值运算符 34 | ExampleClass& operator=(const ExampleClass& other) { 35 | if (this != &other) { 36 | data_ = other.data_; 37 | name_ = other.name_; 38 | } 39 | std::cout << "Copy assignment operator called." << std::endl; 40 | return *this; 41 | } 42 | 43 | // 移动构造函数 44 | ExampleClass(ExampleClass&& other) noexcept 45 | : data_(other.data_), name_(std::move(other.name_)) { 46 | other.data_ = 0; 47 | other.name_.clear(); 48 | std::cout << "Move constructor called." << std::endl; 49 | } 50 | 51 | // 移动赋值运算符 52 | ExampleClass& operator=(ExampleClass&& other) noexcept { 53 | if (this != &other) { 54 | data_ = other.data_; 55 | name_ = std::move(other.name_); 56 | other.data_ = 0; 57 | other.name_.clear(); 58 | } 59 | std::cout << "Move assignment operator called." << std::endl; 60 | return *this; 61 | } 62 | 63 | // 地址操作符 64 | ExampleClass* operator&() { 65 | return this; 66 | } 67 | 68 | // 常量取地址操作符 69 | const ExampleClass* operator&() const { 70 | return this; 71 | } 72 | 73 | private: 74 | int data_; 75 | std::string name_; 76 | }; 77 | 78 | int main() { 79 | ExampleClass obj1(42, "Object1"); 80 | ExampleClass obj2(obj1); // 拷贝构造 81 | ExampleClass obj3(100); 82 | obj3 = obj1; // 拷贝赋值 83 | 84 | ExampleClass obj4(std::move(obj3)); // 移动构造 85 | obj4 = std::move(obj2); // 移动赋值 86 | 87 | // 显示使用地址操作符 88 | std::cout << "Address of obj4: " << &obj4 << std::endl; 89 | std::cout << "Const address of obj4: " << (&obj4) << std::endl; 90 | 91 | return 0; 92 | } -------------------------------------------------------------------------------- /c++/面向对象/this指针.md: -------------------------------------------------------------------------------- 1 | # this指针 2 | 3 | ## 定义: 4 | 5 | this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。 6 | 7 | > 所谓当前对象,是指正在使用的对象。例如对于stu.show();,stu 就是当前对象,this 就指向 stu。 8 | 9 | - this 实际上是成员函数的一个隐式形参,本质上是成员函数的局部变量。在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。 10 | - this指针只能在成员函数中使用,并且只有在通过对象调用成员函数时才给 this 赋值。 11 | - 在全局函数、静态成员函数中都不能用this。 12 | - this指针是类的指针,指向对象的首地址。 13 | - this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。 14 | 15 | ## 特点 16 | - 隐含性:this指针不需要开发者显式定义或初始化,它会在每个非静态成员函数被调用时自动被编译器处理。 17 | 18 | - 常量性:this指针是一个指向常量的指针,这意味着它所指向的对象不能通过this指针被修改。但是,对象的成员可以通过this指针被修改,因为成员不是常量。 19 | 20 | - 唯一性:对于类的每个对象,每次调用其成员函数时,都会有一个唯一的this指针与之对应,确保了成员函数能够操作正确的对象数据。 21 | 22 | ## this指针的用处 23 | 24 | - 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。 25 | - this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针) 26 | - 编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针, 编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。 27 | 28 | ## 需要显示使用this指针的场景 29 | - 区分重名成员:当局部变量或者函数参数与类的成员变量或成员函数重名时,可以使用this指针来区分 30 | - this->n = n (不能写成n = n) 31 | - 链式调用:在某些设计模式中,例如链式调用,this指针可以返回当前对象的引用,从而实现连续调用同一个对象的多个方法。 32 | - 在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this; 33 | - 避免参数传递:在类的方法中,使用this指针可以直接访问所属对象的数据成员,无需将对象本身作为参数传递给方法。 34 | 35 | ## 类的this指针有以下特点 36 | 37 | 1. this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个 参数为T * const this 38 | 39 | ```c++ 40 | class A 41 | { 42 | public: 43 | int func(int p){} 44 | }; 45 | 其中,func的原型在编译器看来应该是: 46 | int func(A * const this,int p); 47 | ``` 48 | 49 | 2. 由此可见,this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个 函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this 参数传递进去。 50 | 51 | ```c++ 52 | A a; 53 | a.func(10); 54 | //此处,编译器将会编译成: 55 | A::func(&a,10); 56 | ``` 57 | 58 | > 看起来和静态函数没差别,对吗?不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,例如VC通常是通过ecx(计数寄存器)传递this参数的。 59 | 60 | 3. 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用 61 | 62 | ## 几个this指针的易混问题 63 | 64 | ### this指针是什么时候创建的? 65 | 66 | this在成员函数的开始执行前构造,在成员的执行结束后清除。 67 | 但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new的方式创建对象的话,在堆里分配内存,new操作符通过eax(累加寄存器)返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx,之后构造 函数里面怎么处理请看上面的回答 68 | 69 | ### this指针存放在何处?堆、栈、全局变量,还是其他? 70 | 71 | this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在 内存中,它们并不是和高级语言变量对应的。 72 | 73 | ### this指针是如何传递类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针 又是如何找到“类实例后函数的”? 74 | 75 | 大多数编译器通过ecx(计数寄存器)寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。 76 | 在call之前,编译器会把对应的对象地址放到eax中。this是通过函数参数的首参来传递的。this指针在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有 为函数分配空间。自从类的函数定义完成后,它就在那儿,不会跑的 77 | 78 | ### this指针是如何访问类中的变量的? 79 | 80 | 如果不是类,而是结构体的话,那么,如何通过结构指针来访问结构中的变量呢?如果你明白这一点的 话,就很容易理解这个问题了。 81 | 在C++中,类和结构是只有一个区别的:类的成员默认是private,而结构是public。 82 | this是类的指针,如果换成结构体,那this就是结构的指针了。 83 | 84 | ### 我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗? 85 | 86 | this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。 87 | 88 | ### 每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数? 89 | 90 | 普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数 才会被放到函数表中。但是,即使是虚函数,如果编译期就能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。正是由于this指针的存在,用来指向不同的对象,从而确保不同对象之间调用相同的函数可以互不干扰。 91 | 92 | ### this指针的一个完整示例: 93 | 94 | ```c++ 95 | this指针的一个完整示例: 96 | #include 97 | using namespace std; 98 | 99 | class Student{ 100 | public: 101 | void setname(char *name); 102 | void setage(int age); 103 | void setscore(float score); 104 | void show(); 105 | private: 106 | char *name; 107 | int age; 108 | float score; 109 | }; 110 | 111 | void Student::setname(char *name){ 112 | this->name = name; 113 | } 114 | void Student::setage(int age){ 115 | this->age = age; 116 | } 117 | void Student::setscore(float score){ 118 | this->score = score; 119 | } 120 | void Student::show(){ 121 | cout<name<<"的年龄是"<age<<",成绩是"<score< setname("李华"); 127 | pstu -> setage(16); 128 | pstu -> setscore(96.5); 129 | pstu -> show(); 130 | 131 | return 0; 132 | } 133 | 运行结果: 134 | 李华的年龄是16,成绩是96.5 135 | ``` 136 | 137 | - this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。 138 | - 本例中成员函数的参数和成员变量重名,只能通过 ``this``区分。以成员函数 ``setname(char *name)``为例,它的==形参==``name``,和==成员变量==``name``重名,如果写作 ``name = name``;这样的语句,就是给==形参==``name``赋值(成员函数作用域内,形参会屏蔽成员变量),而不是给==成员变量==``name``赋值。而写作 ``this -> name = name;``后,==\=左边的name==就是==成员变量==,==右边的name==就是==形参==,一目了然。 139 | 140 | > 注意,this 是一个指针,要用->来访问成员变量或成员函数。 141 | -------------------------------------------------------------------------------- /c++/面向对象/匿名对象.md: -------------------------------------------------------------------------------- 1 | # 匿名对象 2 | 在C++中,匿名对象是指没有名字的对象实例,它们在创建时没有与之关联的标识符。这类对象通常用于临时操作或在不需要长期保存对象实例的场景下。下面是关于匿名对象的详细解释: 3 | 4 | ## 创建与生命周期 5 | 1. 创建方式:匿名对象的创建非常简单,只需在类的构造函数之后紧跟一对圆括号即可。例如,如果有类 Person,创建一个匿名对象的方式是 Person()。 6 | 7 | 2. 生命周期:匿名对象的生命周期很短,一般情况下仅在创建它的那条表达式中有效。一旦该表达式执行完毕,匿名对象就会被销毁,除非它被用来初始化另一个需要持久化的对象。 8 | 9 | ## 用途 10 | 1. 函数调用:可以将匿名对象作为参数直接传递给需要对象实例的函数,这在只需要对象来完成一次调用操作时非常有用。 11 | ```cpp 12 | void printName(Person p) { /* ... */ } 13 | printName(Person("Alice")); // 使用匿名对象调用函数 14 | ``` 15 | 16 | 2. 类型转换:匿名对象可以作为构造函数的隐式类型转换机制的一部分,用于将一种类型转换为另一种类型。 17 | 18 | 3. 返回对象:当函数需要返回一个对象时,可以返回一个匿名对象,这在实现工厂模式或创建并返回新对象的场景中常见。 19 | ```cpp 20 | Person createPerson() { return Person("Bob"); } 21 | Person anonymousBob = createPerson(); // 接收返回的匿名对象 22 | ``` 23 | 24 | 4. 对象初始化:匿名对象可以用来初始化另一个同类型的对象,此时匿名对象的生命周期会延续到接受初始化的对象生命周期中。 25 | ```cpp 26 | Person person = Person("Charlie"); // 匿名对象用于初始化person 27 | ``` 28 | 29 | 30 | ## 特点 31 | - 立即销毁:除了上述提到的用于初始化另一个对象的情况外,匿名对象在表达式结束后会被销毁。 32 | - 没有名称:匿名对象没有可供直接引用的名称,它们存在于表达式的上下文中。 33 | - 节省资源:在不需要长期保持对象实例时,匿名对象可以减少内存占用和提高效率。 34 | - 灵活性:提供了一种简洁的方式来使用对象的功能,特别是在不需要保留对象状态的情况下。 35 | 36 | ## 注意事项 37 | - 虽然匿名对象使用方便,但在需要长时间保持对象状态或多次访问同一对象时,应该创建有名称的对象实例。 38 | - 由于其短暂的生命周期,匿名对象不适合用于需要跨越多个表达式或作用域的情景。 39 | -------------------------------------------------------------------------------- /c++/面向对象/委派构造函数.md: -------------------------------------------------------------------------------- 1 | # 委派构造函数 2 | 委派构造函数是C++11引入的一项特性,它允许在一个类的构造函数中直接调用该类的另一个构造函数,从而实现构造过程中的代码重用和逻辑简化。 3 | 4 | ## 提出背景:构造函数冗余造成重复 5 | 委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时 间。通过委派其他构造函数,多构造函数的类编写更加容易。 6 | ```cpp 7 | class Info 8 | { 9 | public: 10 | Info() 11 | :_type(0) 12 | ,name('a') 13 | { 14 | InitRSet(); 15 | } 16 | 17 | Info(int type) 18 | :_type(type) 19 | ,name('a') 20 | { 21 | InitRSet(); 22 | } 23 | 24 | Info(char a) 25 | :_type(0) 26 | ,name(a) 27 | { 28 | InitRSet(); 29 | } 30 | private: 31 | void InitRSet(){ 32 | //初始化其他变量 33 | } 34 | private: 35 | int _type; 36 | char _name; 37 | //... 38 | }; 39 | ``` 40 | **上述构造函数除了初始化列表不同之外,其他部分都是类似的,代码重复。** 41 | 初始化列表可以通过:类内部成员初始化进行优化,但是构造函数体的重复在C++98中无法解决。 42 | 能否将:构造函数体中重复的代码提出来,作为一个基础版本,在其他构造函数中调用呢? 43 | 44 | 45 | ## 委派构造函数 46 | 所谓委派构造函数:就是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式。 47 | 48 | 基本概念:委托构造函数(Delegating Constructor):在类的构造函数定义中,通过在其初始化列表中调用同一类的另一个构造函数来完成对象的初始化。这样可以避免构造函数代码的重复,使得构造逻辑更加集中和易于维护。 49 | 50 | ## 语法 51 | 假设有一个类Example,它有两个构造函数,其中一个构造函数可以通过委派调用另一个构造函数来完成初始化: 52 | ```cpp 53 | class Example { 54 | public: 55 | Example(int a) : value(a) {} // 目标构造函数 56 | Example(): Example(0) {} // 委派构造函数,委托给上面的构造函数 57 | private: 58 | int value; 59 | }; 60 | ``` 61 | 在上面的例子中,Example()是一个委派构造函数,它通过在其初始化列表中调用Example(int)构造函数(目标构造函数),实现了对value成员的初始化。 62 | 63 | ## 作用 64 | 1. 减少代码重复:当多个构造函数有相似的初始化步骤时,可以将公共部分放在一个构造函数中,然后其他构造函数通过委派调用它,避免了重复编写相同的初始化代码。 65 | 2. 逻辑集中:使得构造逻辑更加集中和易于理解,因为共同的初始化逻辑只在一个地方定义。 66 | 3. 提高可维护性:如果初始化逻辑需要改变,只需要修改被委派的构造函数,所有通过委派调用它的构造函数都会自动采用新的初始化逻辑。 67 | 68 | ## 举例 69 | ```cpp 70 | class Info 71 | { 72 | public: 73 | //目标构造函数 74 | Info() 75 | :_type(0) 76 | ,name('a') 77 | ,InitRSet() 78 | { 79 | 80 | } 81 | 82 | //委派构造函数 83 | Info(int type) 84 | :Info() 85 | { 86 | _type = type; 87 | } 88 | 89 | //委派构造函数 90 | Info(char a) 91 | :Info() 92 | { 93 | _char = a; 94 | } 95 | private: 96 | void InitRSet(){ 97 | //初始化其他变量 98 | } 99 | private: 100 | int _type; 101 | char _name; 102 | //... 103 | }; 104 | ``` 105 | 在初始化列表中调用”基准版本”的构造函数称为委派构造函数,而被调用的”基准版本”则称为目标构造函数。 106 | 107 | ## 注意事项 108 | 1. 循环委托:委派构造函数不能形成循环委托,即构造函数A不能委托构造函数B,而B又直接或间接地委托回A,这会导致编译错误。 109 | 2. 初始化列表:在委派构造函数中,除了委托调用外,不能包含其他成员的直接初始化。也就是说,所有成员的初始化都应当在被委托的构造函数中完成。 110 | 3. 构造函数链:可以形成构造函数链,即一个构造函数委派给另一个,另一个再委派给下一个,以此类推,但最终必须有一个构造函数不进行委派调用,实际执行成员的初始化。 111 | 112 | ## 使用场景 113 | 委派构造函数特别适用于那些具有多种构造方式,但每种构造方式都包含一部分相同初始化逻辑的类。例如,一个复杂的类可能需要从文件、网络或用户输入等多种来源加载数据,虽然数据来源不同,但加载后的初始化处理可能是相同的,这时候就可以利用委派构造函数来简化代码结构。 114 | 115 | -------------------------------------------------------------------------------- /c++/面向对象/嵌套类-内部类.md: -------------------------------------------------------------------------------- 1 | # 嵌套类/内部类 2 | C++中的嵌套类(Nested Class),也通常称为内部类,是指在一个类的定义内部定义的另一个类。嵌套类可以是外部类的私有、保护或公有成员,它的使用场景多样,可以增强封装性、表达复杂的逻辑关系或实现特定的设计模式。下面是对C++嵌套类的详细说明: 3 | 4 | ## 定义与访问权限 5 | 嵌套类的定义方式很简单,直接在外部类的定义中声明一个新的类即可。嵌套类可以访问外部类的所有成员,包括私有和保护成员,这是因为嵌套类是外部类的一部分,共享其作用域。 6 | ```cpp 7 | class OuterClass { 8 | public: 9 | class NestedClass { 10 | public: 11 | void accessOuterMember() { 12 | // 可以访问外部类的私有和保护成员 13 | outerMember = 10; 14 | } 15 | private: 16 | int nestedVar; 17 | }; 18 | 19 | void useNestedClass() { 20 | NestedClass nestedObj; 21 | nestedObj.accessOuterMember(); 22 | } 23 | 24 | private: 25 | int outerMember; 26 | }; 27 | ``` 28 | 29 | ## 访问嵌套类 30 | - 外部访问:外部代码访问嵌套类通常需要通过外部类的实例,除非嵌套类是静态的(static nested class),这时可以直接通过外部类访问。 31 | 32 | - 外部类访问嵌套类:外部类可以直接访问嵌套类的所有成员,包括创建嵌套类的对象。 33 | 34 | ## 嵌套类的类型 35 | - 非静态嵌套类(Non-static Nested Class/Inner Class):与外部类的实例绑定,通常需要一个外部类的实例才能创建。 36 | 37 | - 静态嵌套类(Static Nested Class):不依赖于外部类的实例,可以直接通过外部类名访问,类似于外部类的一个静态成员。 38 | 39 | ## 用途 40 | 1. 增强封装性:嵌套类可以访问外部类的私有成员,但外部代码不能直接访问嵌套类(除非提供访问方法),有助于隐藏实现细节。 41 | 2. 逻辑分组:当一个类与另一个类有很强的逻辑联系时,将其嵌套可以提高代码的组织性和可读性。 42 | 3. 实现特定设计模式:如访问者模式、迭代器模式等,嵌套类可以简化设计并保持数据的安全性。 43 | 4. 模板方法模式:外部类定义算法的骨架,嵌套类实现算法的某些步骤。 44 | 45 | ## 注意事项 46 | - 内存占用:嵌套类本身不增加外部类实例的大小,除非外部类中包含嵌套类的实例作为成员变量。 47 | - 命名空间:嵌套类在其外部类的命名空间内,因此要注意避免命名冲突。 48 | 49 | ## 外部类与内部类的关系 50 | 1. 外部类与内部类的友元关系:这句话表述不够准确。在C++中,并不是说外部类“天生”就是内部类的友元,而是内部类自动具有访问外部类所有成员的权限,包括私有和保护成员。这是由于内部类定义在外部类的作用域内,自然可以访问这些成员,这并不等同于友元关系的明确声明,但效果上类似。 51 | 52 | 2. 访问权限:确实,外部类可以直接访问内部类的所有公开成员(包括公共和受保护成员,如果内部类没有定义为私有)。但是,如果内部类是私有的或者保护的,外部类以外的代码不能直接访问这个内部类,更不用说其成员了。内部类要访问外部类的成员,是基于其定义环境的自然权限,而不是说它能反过来访问外部类的私有成员(除非通过外部类提供的接口)。 53 | 54 | 3. 大小关系:sizeof(外部类)的确与内部类的大小没有直接关系。sizeof运算符计算的是特定类型对象占用的字节数,它不考虑类中定义的内部类或其他类型定义。内部类作为一个独立的类型定义,并不会增加外部类实例的大小,除非外部类有一个内部类类型的成员变量。换句话说,内部类更多地体现在逻辑上的组织和访问控制上,而不直接影响外部类实例的内存布局。 55 | 56 | -------------------------------------------------------------------------------- /c++/面向对象/虚函数与纯虚函数.md: -------------------------------------------------------------------------------- 1 | # 虚函数与纯虚函数 2 | 虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。 3 | 4 | 纯虚函数: 5 | - 纯虚函数在类中声明时,加上 =0; 6 | - 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法; 7 | - 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。 8 | 9 | 说明: 10 | - 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型; 11 | - 可以声明抽象类指针,可以声明抽象类的引用; 12 | - 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。 13 | 14 | ## 虚函数和纯虚函数的区别? 15 | - 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类) 16 | - 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用; 17 | - 定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0; 18 | - 虚函数必须实现,否则编译器会报错; 19 | - 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写; 20 | - 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。 21 | 22 | ## 虚函数的实现机制 23 | **实现机制**:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数 24 | 25 | ## 虚函数表相关知识点: 26 | - 虚函数表存放的内容:类的虚函数的地址。 27 | - 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。 28 | - 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。 29 | 30 | 注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。 31 | 32 | 实例: 33 | 无虚函数覆盖的情况: 34 | ```cpp 35 | #include 36 | using namespace std; 37 | 38 | class Base 39 | { 40 | public: 41 | virtual void B_fun1() { cout << "Base::B_fun1()" << endl; } 42 | virtual void B_fun2() { cout << "Base::B_fun2()" << endl; } 43 | virtual void B_fun3() { cout << "Base::B_fun3()" << endl; } 44 | }; 45 | 46 | class Derive : public Base 47 | { 48 | public: 49 | virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; } 50 | virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; } 51 | virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; } 52 | }; 53 | int main() 54 | { 55 | Base *p = new Derive(); 56 | p->B_fun1(); // Base::B_fun1() 57 | return 0; 58 | } 59 | ``` 60 | 61 | ### 单继承和多继承的虚函数表结构 62 | 编译器处理虚函数表: 63 | - 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。 64 | - 如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。 65 | - 如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。 66 | - 如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。 67 | 68 | ### 为什么构造函数不能为虚函数? 69 | 虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。 70 | 71 | ### 为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题? 72 | 防止内存泄露,delete p(基类)的时候,它很机智的先执行了派生类的析构函数,然后执行了基类的析构函数。 73 | 如果基类的析构函数不是虚函数,在delete p(基类)时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。 74 | 75 | 举例说明: 76 | 子类B继承自基类A;A *p = new B; delete p; 77 | 1. 此时,如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,只释放了B对象中的A部分,而派生出的新的部分未释放掉。 78 | 2. 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间。 79 | 补充: B *p = new B; delete p;时也是先调用B的析构函数,再调用A的析构函数。 80 | 81 | ### 不能声明为虚函数的有哪些? 82 | 1.静态成员函数; 2.类外的普通函数; 3.构造函数; 4.友元函数 83 | 84 | 虚函数是为了实现多态特性的。虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数,其是有有如下几点需要注意: 85 | 86 | 1. 类的构造函数不能是虚函数 87 | 构造函数是为了构造对象的,所以在调用构造函数时候必然知道是哪个对象调用了构造函数,所以构造函数不能为虚函数。 88 | 2. 类的静态成员函数不能是虚函数 89 | 类的静态成员函数是该类共用的,与该类的对象无关,静态函数里没有this指针,所以不能为虚函数。 90 | 3. 内联函数 91 | 内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略。 92 | 4. 友元函数 93 | 友元函数与该类无关,没有this指针,所以不能为虚函数。 94 | -------------------------------------------------------------------------------- /c++/面向对象/虚表与虚表指针,虚基类表与虚基类表指针.md: -------------------------------------------------------------------------------- 1 | # 虚表与虚表指针,虚基类表与虚基类表指针 2 | ## 虚函数表和虚函数指针 3 | 1. 虚函数表指针: 4 | 虚函数表指针又称为:虚表指针。虚表指针指向的是一张虚函数表。 5 | 6 | 2. 虚函数表: 7 | 虚函数表又称为:虚表。虚表中存放的是虚函数指针,因此虚表可以看成是一个函数指针数组。虚表在编译阶段生成。 8 | 9 | 3. 虚表指针在哪里? 10 | 虚表指针存在于有虚函数的类对象中; 11 | 12 | 4. 虚表在哪里? 13 | 虚表存在于.rodata段; 14 | 15 | 5. 虚函数在哪里? 16 | 虚函数保存在代码段; 17 | 18 | 6. 基类虚表的构建规则: 19 | 按照虚函数在基类中的声明顺序存放在虚函数表中。 20 | 21 | 7. 派生类虚表的构建规则: 22 | 将基类虚表中的内容拷贝一份保存到派生类的虚表中; 23 | 如果派生类中重写了基类中的虚函数,那么在派生类的虚表中,派生类重写后的虚函数地址将替换(覆盖)掉继承下来的虚函数地址; 24 | 派生类自己增加的虚函数,按照在派生类中的声明顺序存放在派生类的虚表的后边;(注意:在VS下监视窗口查看时,没有显示); 25 | 即派生类虚表中保存的是继承的未被重写的虚函数,派生类重写之后的虚函数以及派生类自己的虚函数的地址; 26 | 27 | 8. 虚函数表作用: 28 | 用来解决保存虚函数的问题以及实现多态的基础; 29 | 30 | ## 虚基表和虚基表指针 31 | 1. 虚基类表指针: 32 | 虚基类表指针又称为:虚基表指针;是虚继承下来的派生类中的成员;指向一张虚基表; 33 | 34 | 2. 虚基类类表: 35 | 虚基类表又称为:虚基表;虚基表中记录了虚基类与派生类的偏移地址;通过偏移地址,这样就找到了虚基类成员; 36 | 虚基表中存放两个成员:一个是0(可以认为是虚基类与自己的偏移量),另一个是虚基类与派生类的偏移量; 37 | 38 | 3. 虚基表指针在哪里? 39 | 虚基表指针存在于虚继承的派生类对象中; 40 | 41 | 4. 虚基类表的作用 42 | 虚基类表就是用来解决继承过程中,菱形继承的二义性和数据冗余的问题。 43 | 44 | 5. 虚基表指针和虚表指针对比 45 | 虚基表指针 虚表指针 46 | 生成条件 续集继承时产生 有虚函数时产生 47 | 指向 指向虚基表 指向虚表 48 | 指向内容 派生类对象中的基类成员对于该对象首地址的偏移量 类中所有虚函数的地址 49 | 作用 解决菱形继承时的二义性和数据冗余的问题 50 | 51 | 52 | -------------------------------------------------------------------------------- /c++/面向对象/重载、重写、隐藏.md: -------------------------------------------------------------------------------- 1 | ## 重载、重写、隐藏的区别 2 | 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。 3 | ```cpp 4 | class A 5 | { 6 | public: 7 | void fun(int tmp); 8 | void fun(float tmp); // 重载 参数类型不同(相对于上一个函数) 9 | void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数) 10 | void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数) 11 | int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型 12 | }; 13 | ``` 14 | 15 | 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。 16 | ```cpp 17 | #include 18 | using namespace std; 19 | 20 | class Base 21 | { 22 | public: 23 | void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; } 24 | }; 25 | 26 | class Derive : public Base 27 | { 28 | public: 29 | void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数 30 | }; 31 | 32 | int main() 33 | { 34 | Derive ex; 35 | ex.fun(1); // Derive::fun(int tmp) 36 | ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided 37 | return 0; 38 | } 39 | ``` 40 | 说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。 41 | 42 | 重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。 43 | ```cpp 44 | #include 45 | using namespace std; 46 | 47 | class Base 48 | { 49 | public: 50 | virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; } 51 | }; 52 | 53 | class Derived : public Base 54 | { 55 | public: 56 | virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数 57 | }; 58 | int main() 59 | { 60 | Base *p = new Derived(); 61 | p->fun(3); // Derived::fun(int) : 3 62 | return 0; 63 | } 64 | ``` 65 | 66 | 重写和重载的区别: 67 | - 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。 68 | - 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。 69 | - virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。 70 | 71 | 隐藏和重写,重载的区别: 72 | - 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。 73 | - 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual修饰,基类函数都是被隐藏,而不是重写。 -------------------------------------------------------------------------------- /c++/面向对象/面向对象.md: -------------------------------------------------------------------------------- 1 | # 面向对象 2 | 3 | ## 面向过程和面向对象 4 | 5 | ### **面向过程编程(Procedural Programming)** 6 | 7 | 1. 基本思想:面向过程编程侧重于程序的行为步骤,即如何通过一系列过程(函数、子程序)来解决问题。它以过程(或称为函数)为核心,将计算过程分解为一系列函数调用。 8 | 2. 特点: 9 | - 程序组织围绕功能模块,模块之间通过函数调用进行通信。 10 | - 数据和操作数据的过程分离,数据可以被多个函数共享和修改。 11 | - 侧重于执行步骤和逻辑控制流,易于理解和实现小型项目。 12 | 3. 优点:简单直观,执行效率高,对于小型项目或算法密集型应用尤为适合。 13 | 4. 缺点:随着项目规模的扩大,代码难以维护和复用,难以应对需求变更。 14 | 15 | ### **面向对象编程(Object-Oriented Programming, OOP)** 16 | 17 | 1. 基本思想:面向对象编程强调的是将问题领域中的实体抽象成对象,通过对象的属性(数据成员)和行为(成员函数)来描述和模拟现实世界。它基于“万物皆对象”的概念,通过对象的封装、继承和多态等机制来设计和实现软件。 18 | 2. 特点: 19 | - 程序围绕对象构建,对象包含了数据(属性)和对数据的操作(方法)。 20 | - 封装性保证了数据的安全性和隐藏性,继承支持代码复用,多态提供了灵活性和可扩展性。 21 | - 通过类和对象的概念,使得代码结构更加清晰,易于维护和扩展。 22 | 3. 优点:提高了代码的复用性、灵活性和可维护性,更适合大型复杂项目的开发。 23 | 4. 缺点:相比面向过程编程,面向对象编程可能引入更多的间接层,降低运行效率;学习曲线相对陡峭,设计不当可能导致系统结构复杂。 24 | 25 | ## 面向对象的三大特性 26 | 27 | - 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。 28 | 29 | - 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。 30 | 31 | - 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。 32 | 33 | ### 1.封装(Encapsulation) 34 | 35 | - 核心思想:封装的核心思想是将数据(属性)和操作数据的方法(行为)捆绑在一起,并限制对这些数据的直接访问。在C++中,封装是通过访问修饰符(public, protected, private)来实现的。 36 | - public:公有成员对所有类内外的代码都是可见的。 37 | - protected:受保护的成员对派生类可见,但对类外的代码不可见。 38 | - private:私有成员仅对定义它们的类内部可见。 39 | - 重要性:提高数据安全性,减少耦合度,使得组件更容易独立修改而不影响其他部分。 40 | - 深入理解:封装不仅仅是隐藏数据,更重要的是暴露有意义的接口给外部使用,确保类的使用者只能通过这些接口与对象交互,从而维护对象的完整性和一致性。 41 | 42 | ### 2.继承(Inheritance) 43 | 44 | - 基本概念:继承允许创建一个新类(派生类),继承现有类(基类)的属性和行为,并可以添加或重写这些行为。 45 | - 基类:提供共通的属性和方法。 46 | - 派生类:继承基类的属性和方法,并可以添加或重写以满足特定需求。 47 | - 目的:促进代码复用,支持类型和行为的层次结构。 48 | - 注意事项:滥用继承可能导致结构复杂、耦合度高。应遵循里氏替换原则(Liskov Substitution Principle),即派生类应当能够替换掉基类,并且不会影响到程序的正确性。 49 | 50 | ### 3.多态(Polymorphism) 51 | 52 | - 含义:多态是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以有多种实现方式。 53 | - 类型:主要包括编译时多态(通过函数重载、模板实现)和运行时多态(通过虚函数和动态绑定实现)。 54 | - 价值:增加代码的灵活性和可扩展性,使得程序可以在不修改原有代码的情况下应对变化。 55 | 56 | #### 静态多态(编译时多态) 57 | 58 | 静态多态性是在编译阶段就确定下来的,最常见的形式是函数重载和运算符重载。 59 | 60 | - 函数重载:允许在同一作用域内有多个同名函数,只要它们的参数列表不同(参数类型或个数),编译器就会根据传入的参数类型自动选择正确的函数版本。这是编译时多态的一个例子,因为调用哪个函数在编译期间就能决定。 61 | 62 | - 运算符重载:类似于函数重载,允许为运算符提供多种实现,具体调用哪个版本同样在编译时决定。 63 | 64 | #### 动态多态(运行时多态) 65 | 66 | 动态多态性是在程序运行时确定的,C++主要通过虚函数(virtual functions)和抽象类(abstract classes)来实现这一特性。 67 | 68 | - 虚函数:在基类中声明为virtual的成员函数,允许在派生类中被重写。使用虚函数的关键在于动态绑定(或称为迟绑定、晚期绑定),这意味着调用虚函数时,真正执行的函数版本直到运行时才能确定,取决于所调用对象的实际类型。虚函数的调用通常通过基类的指针或引用完成,这样编译器会在运行时查找虚函数表(vtable),以确定应该调用哪个派生类的函数。 69 | 70 | - 抽象类:包含至少一个纯虚函数(在声明时加上= 0的虚函数)的类,不能直接实例化,通常作为接口使用,迫使派生类实现纯虚函数,从而保证了多态性。 71 | 72 | - 虚析构函数:为了确保通过基类指针删除派生类对象时,派生类的析构函数也能被正确调用,建议将基类的析构函数也声明为虚函数。 73 | 74 | **实现细节:** 75 | 76 | - 虚函数表(VTable):C++编译器为含有虚函数的每个类生成一个虚函数表,这张表中存储了该类及其所有基类的虚函数地址。每个对象都有一个指向相应虚函数表的指针(虚指针,vptr),这个指针在对象创建时初始化。 77 | 78 | - 多态的调用过程:当通过基类指针或引用调用一个虚函数时,编译器实际上访问的是对象的vptr,然后通过vptr找到虚函数表,进而找到正确的函数地址并调用。 79 | -------------------------------------------------------------------------------- /tools/cmake/bibilicmake教程/001工欲善其事必先利其器.md: -------------------------------------------------------------------------------- 1 | # 安装 -------------------------------------------------------------------------------- /tools/cmake/cmake demo.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/tools/cmake/cmake demo.rar -------------------------------------------------------------------------------- /tools/cmake/但丁总结/image/01cmake编译流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/tools/cmake/但丁总结/image/01cmake编译流程.png -------------------------------------------------------------------------------- /tools/cmake/但丁总结/image/cmake/1683940342147.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/tools/cmake/但丁总结/image/cmake/1683940342147.png -------------------------------------------------------------------------------- /tools/cmake/但丁总结/单文件项目/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # 单文件CMake 2 | 3 | # 指定版本 4 | cmake_minimum_required(VERSION 3.0) 5 | 6 | # 指定C++标准:-std=c++11 7 | set(CMAKE_CXX_STANDARD 11) 8 | 9 | # 工程名 10 | project(demo) 11 | 12 | # set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") #设置c++的编译选项 13 | # set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99") #设置c的编译选项 14 | 15 | file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) 16 | # 指定输出的路径 17 | set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) 18 | 19 | add_executable(main ${SRC}) 20 | -------------------------------------------------------------------------------- /tools/cmake/简单cmake教程/01cmake交互式配置界面.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/tools/cmake/简单cmake教程/01cmake交互式配置界面.webp -------------------------------------------------------------------------------- /tools/cmake/简单cmake教程/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #设置cmake最小的版本要求 2 | cmake_minimum_required(VERSION 3.10) 3 | #设置项目名 4 | project(project_name) 5 | #添加可执行文件(第一个参数是生成的可执行文件的名字,第二个参数是源文件) 6 | add_executable(project_name xxx.cpp) 7 | -------------------------------------------------------------------------------- /tools/cmake/简单cmake教程/cmake.md: -------------------------------------------------------------------------------- 1 | # cmake简介 2 | 3 | CMake是一个跨平台的安装/编译链工具,它能用简单的语句描述所有平台的安装/编译过程。通常在UNIX环境下,CMake根据CMakeLists.txt生成Makefile,在Makefile中定义了具体的编译过程。 4 | 5 | ## C++项目运行 6 | 7 | 第一步,在CMakeLists.txt所在目录创建build子目录,切换至build目录并执行“cmake ..”生成Makefile,即本地构建系统。 8 | 9 | 第二步,此时还应在build目录中,执行“cmake --build .”或“make”生成目标文件或可执行文件。 10 | 11 | ## 常用指令 12 | 13 | ### 1.cmake_minimum_required 14 | 15 | 最低版本号要求 16 | cmake_minimum_required(VERSION 3.10):指定运行此配置文件所需的 CMake 的最低版本;一般只需指定最小版本。 17 | 18 | ```cmake 19 | cmake_minimum_required (VERSION 3.1) 20 | ``` 21 | 22 | #### 2.project 23 | 24 | 项目信息 25 | project:参数值是 Demo1,该命令表示项目的名称是 Demo1 。 26 | 27 | ```cmake 28 | project (Demo1) 29 | ``` 30 | 31 | ### 3.指定C++标准 32 | 33 | set(CMAKE_CXX_STANDARD 11) 34 | 35 | set(CMAKE_CXX_STANDARD_REQUIRED True) 36 | 37 | ### 4.add_executable 38 | 39 | 指定生成目标 40 | 41 | ```cmake 42 | # 第一种:Normal Executables 43 | add_executable( [WIN32] [MACOSX_BUNDLE] 44 | [EXCLUDE_FROM_ALL] 45 | [source1] [source2 ...]) 46 | # 第二种:Imported Executables 47 | add_executable( IMPORTED [GLOBAL]) 48 | # 第三种:Alias Executables 49 | add_executable( ALIAS ) 50 | ``` 51 | 52 | 使用指定的源文件来生成目标可执行文件。具体分为三类:普通、导入、别名。此处我们就以普通可执行文件进行说明。 53 | 54 | 其中``是可执行文件的名称,在cmake工程中必须唯一。WIN32用于在windows下创建一个以WinMain为入口的可执行文件。MACOSX_BUNDLE用于mac系统或者IOS系统下创建一个GUI可执行应用程序。 55 | 56 | ### 5.aux_source_directory 57 | 58 | 查找指定目录下的所有源文件,然后存进指定变量名 59 | aux_source_directory:该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。 60 | 语法: 61 | 62 | ```cmake 63 | aux_source_directory( ) 64 | ``` 65 | 66 | e.g. 67 | 68 | ```cmake 69 | # 查找当前目录下的所有源文件 70 | # 并将名称保存到 DIR_SRCS 变量 71 | aux_source_directory(. DIR_SRCS) 72 | 73 | # 指定生成目标 74 | add_executable(Demo ${DIR_SRCS}) 75 | ``` 76 | 77 | ### 6.add_library 78 | 79 | 根据源码来生成一个库供他人使用 80 | CMake中的add_library命令用于使用指定的源文件向项目(project)中添加库,其格式如下: 81 | 82 | ```cmake 83 | add_library( [STATIC | SHARED | MODULE] 84 | [EXCLUDE_FROM_ALL] 85 | [...]) 86 | ``` 87 | 88 | add_library根据源码来生成一个库供他人使用。``是个逻辑名称,在项目中必须唯一。完整的库名依赖于具体构建方式(可能为`lib.a` or `.lib`)。 89 | 90 | STATIC指静态库,SHARED指动态库,MODULE指在运行期通过类似于dlopen的函数动态加载。 91 | 92 | ### 7.add_subdirectory 93 | 94 | 将子目录添加到构建系统中 95 | 96 | ```cmake 97 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) 98 | ``` 99 | 100 | 将子目录添加到构建系统中。source_dir指定一个目录,其中存放CMakeLists.txt文件和代码文件。binary_dir指定的目录存放输出文件,如果没有指定则使用source_dir。 101 | 102 | ### 8.link_libraries 103 | 104 | 为目标文件链接依赖的库。 105 | 已弃用,参考target_link_libraries 106 | 107 | ```cmake 108 | link_libraries([item1 [item2 [...]]] 109 | [[debug|optimized|general] ] ...) 110 | ``` 111 | 112 | ### 9.target_link_libraries 113 | 114 | 为目标文件链接依赖的库。 115 | 116 | ```cmake 117 | target_link_libraries( 118 | ... 119 | [ ...]...) 120 | ``` 121 | 122 | 为目标文件链接依赖的库。PUBLIC修饰的库或目标会被链接,并成为链接接口的一部分。PRIVATE修饰的目标或库会被链接,但不是链接接口的一部分。INTERFACE修饰的库会追加到链接接口中,但不会用来链接目标文件``。 123 | 124 | ### 10.include_directories 125 | 126 | 将指定目录添加到编译器的头文件搜索路径之下. 127 | 128 | ```cmake 129 | include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...]) 130 | ``` 131 | 132 | 将指定目录添加到编译器的头文件搜索路径之下,指定目录被解释为当前源码路径的相对路径。[AFTER|BEFORE]定义了追加指定目录的方式在头还是尾。[SYSTEM]告诉编译器在一些平台上指定目录被当作系统头文件目录。 133 | 134 | ### 11.include 135 | 136 | 从指定的文件加载、运行CMake代码。 137 | 138 | ```cpp 139 | include( [OPTIONAL] [RESULT_VARIABLE ] 140 | [NO_POLICY_SCOPE]) 141 | ``` 142 | 143 | 从指定的文件加载、运行CMake代码。如果指定文件,则直接处理。如果指定module,则寻找module.cmake文件,首先在${CMAKE_MODULE_PATH}中寻找,然后在CMake的module目录中查找。 144 | 145 | ### 12.link_directories 146 | 147 | ```cmake 148 | link_directories([AFTER|BEFORE] directory1 [directory2 ...]) 149 | ``` 150 | 151 | 为链接器添加库的搜索路径,此命令调用之后生成的目标才能生效。link_directories()要放在add_executable()之前。 152 | 153 | ### 13.find_package 154 | 155 | 加载外部库到项目中,并且会加载库的细节信息。 156 | 157 | ```cmake 158 | ## 共支持两种模式 159 | # mode1: Module, 此模式需访问Find.cmake文件 160 | find_package( [version] [EXACT] [QUIET] [MODULE] 161 | [REQUIRED] [[COMPONENTS] [components...]] 162 | [OPTIONAL_COMPONENTS components...] 163 | [NO_POLICY_SCOPE]) 164 | 165 | # mode2: Config, 此模式需访问-config.cmake or Config.cmake 166 | find_package( [version] [EXACT] [QUIET] 167 | [REQUIRED] [[COMPONENTS] [components...]] 168 | [OPTIONAL_COMPONENTS components...] 169 | [CONFIG|NO_MODULE] 170 | [NO_POLICY_SCOPE] 171 | [NAMES name1 [name2 ...]] 172 | [CONFIGS config1 [config2 ...]] 173 | [HINTS path1 [path2 ... ]] 174 | [PATHS path1 [path2 ... ]] 175 | [PATH_SUFFIXES suffix1 [suffix2 ...]] 176 | [NO_DEFAULT_PATH] 177 | [NO_PACKAGE_ROOT_PATH] 178 | [NO_CMAKE_PATH] 179 | [NO_CMAKE_ENVIRONMENT_PATH] 180 | [NO_SYSTEM_ENVIRONMENT_PATH] 181 | [NO_CMAKE_PACKAGE_REGISTRY] 182 | [NO_CMAKE_BUILDS_PATH] # Deprecated; does nothing. 183 | [NO_CMAKE_SYSTEM_PATH] 184 | [NO_CMAKE_SYSTEM_PACKAGE_REGISTRY] 185 | [CMAKE_FIND_ROOT_PATH_BOTH | 186 | ONLY_CMAKE_FIND_ROOT_PATH | 187 | NO_CMAKE_FIND_ROOT_PATH]) 188 | ``` 189 | 190 | find_package一般用于加载外部库到项目中,并且会加载库的细节信息。如上find_package有两种模式:Module与Config。 191 | 192 | ## CMake 常用变量和常用环境变量 193 | 194 | CMAKE_SOURCE_DIR:根源代码目录,工程顶层目录。暂认为就是PROJECT_SOURCE_DIR 195 | CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的路径 196 | PROJECT_SOURCE_DIR:工程顶层目录 197 | CMAKE_BINARY_DIR:运行cmake的目录。外部构建时就是build目录 198 | CMAKE_CURRENT_BINARY_DIR:The build directory you are currently in.当前所在build目录 199 | PROJECT_BINARY_DIR:暂认为就是CMAKE_BINARY_DIR 200 | -------------------------------------------------------------------------------- /tools/cmake/简单cmake教程/cmake示例.md: -------------------------------------------------------------------------------- 1 | # cmake 例子 2 | 3 | ## 单个源文件 4 | 5 | ```cmake 6 | #设置cmake最小的版本要求 7 | cmake_minimum_required(VERSION 3.10) 8 | 9 | #设置项目名 10 | project(project_name) 11 | 12 | #添加可执行文件(第一个参数是生成的可执行文件的名字,第二个参数是源文件) 13 | add_executable(project_name xxx.cpp) 14 | ``` 15 | 16 | ## 同一目录,多个源文件 17 | 18 | 1.现在假如把 power 函数单独写进一个名为 MathFunctions.c 的源文件里,使得这个工程变成如下的形式: 19 | 20 | ```cpp 21 | ./Demo2 22 | | 23 | +--- main.cc 24 | | 25 | +--- MathFunctions.cc 26 | | 27 | +--- MathFunctions.h 28 | ``` 29 | 30 | 这个时候,CMakeLists.txt 可以改成如下的形式: 31 | 32 | ```cpp 33 | # CMake 最低版本号要求 34 | cmake_minimum_required (VERSION 2.8) 35 | 36 | # 项目信息 37 | project (Demo2) 38 | 39 | # 指定生成目标 40 | add_executable(Demo main.cc MathFunctions.cc) # 依次加入该目录下的所有源文件 41 | ``` 42 | 43 | 2.但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作。更省事的方法是使用 aux_source_directory 命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下: 44 | 45 | ```cmake 46 | aux_source_directory( ) 47 | ``` 48 | 49 | 可以修改 CMakeLists.txt 如下: 50 | 51 | ```cmake 52 | # CMake 最低版本号要求 53 | cmake_minimum_required (VERSION 2.8) 54 | 55 | # 项目信息 56 | project (Demo2) 57 | 58 | # 查找当前目录下的所有源文件 59 | # 并将名称保存到 DIR_SRCS 变量 60 | aux_source_directory(. DIR_SRCS) 61 | 62 | # 指定生成目标 63 | add_executable(Demo ${DIR_SRCS}) 64 | ``` 65 | 66 | 这样,CMake 会将当前目录所有源文件的文件名赋值给变量 DIR_SRCS ,再指示变量 DIR_SRCS 中的源文件需要编译成一个名称为 Demo 的可执行文件。 67 | 68 | ==总结== 69 | 70 | 1. 可以将多个文件都添加到add_executable()中。 71 | 2. 使用aux_source_directory将所有源文件赋值给一个变量,添加该变量到add_executable()中。 72 | 73 | ## 多个目录,多个源文件 74 | 75 | 现在进一步将 MathFunctions.h 和 MathFunctions.cc 文件移动到 math 目录下。 76 | 77 | ```cpp 78 | ./Demo3 79 | | 80 | +--- main.cc 81 | | 82 | +--- math/ 83 | | 84 | +--- MathFunctions.cc 85 | | 86 | +--- MathFunctions.h 87 | ``` 88 | 89 | 对于这种情况,需要分别在项目根目录 Demo3 和 math 目录里各编写一个 CMakeLists.txt 文件。为了方便,我们可以先将 math 目录里的文件编译成静态库再由 main 函数调用。 90 | 根目录中的 CMakeLists.txt : 91 | 92 | ```cmake 93 | # CMake 最低版本号要求 94 | cmake_minimum_required (VERSION 3.1) 95 | 96 | # 项目信息 97 | project (Demo3) 98 | 99 | # 查找当前目录下的所有源文件 100 | # 并将名称保存到 DIR_SRCS 变量 101 | aux_source_directory(. DIR_SRCS) 102 | 103 | # 添加 math 子目录 104 | add_subdirectory(math) 105 | 106 | # 指定生成目标 107 | add_executable(Demo main.cc) 108 | 109 | # 添加链接库 110 | target_link_libraries(Demo MathFunctions) 111 | ``` 112 | 113 | 该文件添加了下面的内容: 第3行,使用命令 add_subdirectory 指明本项目包含一个子目录 math,这样 math 目录下的 CMakeLists.txt 文件和源代码也会被处理 。第6行,使用命令 target_link_libraries 指明可执行文件 main 需要连接一个名为 MathFunctions 的链接库 。 114 | 子目录中的 CMakeLists.txt: 115 | 116 | ```cmake 117 | # 查找当前目录下的所有源文件 118 | # 并将名称保存到 DIR_LIB_SRCS 变量 119 | aux_source_directory(. DIR_LIB_SRCS) 120 | 121 | # 生成链接库 122 | add_library (MathFunctions ${DIR_LIB_SRCS}) 123 | ``` 124 | 125 | - 在该文件中使用命令 add_library 将 src 目录中的源文件编译为==静态链接库==。 126 | 127 | ==总结==: 128 | 129 | 1. 先在每个子目录中,将所有源文件保存为一个变量并生成链接库 130 | 2. 在根目录里添加所有子目录 131 | 3. 生成目标 132 | 4. 添加子目录里的链接库 133 | 134 | ## 自定义编译选项 135 | 136 | CMake 允许为项目增加编译选项,从而可以根据用户的环境和需求选择最合适的编译方案。 137 | 138 | 例如,可以将 MathFunctions 库设为一个可选的库,如果该选项为 ON ,就使用该库定义的数学函数来进行运算。否则就调用标准库中的数学函数库。 139 | 140 | ### 1.修改 CMakeLists 文件 141 | 142 | 我们要做的第一步是在顶层的 CMakeLists.txt 文件中添加该选项: 143 | 144 | ```cmake 145 | # CMake 最低版本号要求 146 | cmake_minimum_required (VERSION 2.8) 147 | 148 | # 项目信息 149 | project (Demo4) 150 | 151 | # 加入一个配置头文件,用于处理 CMake 对源码的设置 152 | configure_file ( 153 | "${PROJECT_SOURCE_DIR}/config.h.in" 154 | "${PROJECT_BINARY_DIR}/config.h" 155 | ) 156 | 157 | # 是否使用自己的 MathFunctions 库 158 | option (USE_MYMATH 159 | "Use provided math implementation" ON) 160 | 161 | # 是否加入 MathFunctions 库 162 | if (USE_MYMATH) 163 | include_directories ("${PROJECT_SOURCE_DIR}/math") 164 | add_subdirectory (math) 165 | set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions) 166 | endif (USE_MYMATH) 167 | 168 | # 查找当前目录下的所有源文件 169 | # 并将名称保存到 DIR_SRCS 变量 170 | aux_source_directory(. DIR_SRCS) 171 | 172 | # 指定生成目标 173 | add_executable(Demo ${DIR_SRCS}) 174 | target_link_libraries (Demo ${EXTRA_LIBS}) 175 | ``` 176 | 177 | 其中: 178 | 179 | 1. 第7行的 configure_file 命令用于加入一个配置头文件 config.h ,这个文件由 CMake 从 config.h.in 生成,通过这样的机制,将可以通过预定义一些参数和变量来控制代码的生成。 180 | 2. 第13行的 option 命令添加了一个 USE_MYMATH 选项,并且默认值为 ON 。 181 | 3. 第17行根据 USE_MYMATH 变量的值来决定是否使用我们自己编写的 MathFunctions 库。 182 | 183 | ### 2.修改 main.cc 文件 184 | 185 | 之后修改 main.cc 文件,让其根据 USE_MYMATH 的预定义值来决定是否调用标准库还是 MathFunctions 库: 186 | 187 | ```cmake 188 | #include 189 | #include 190 | #include "config.h" 191 | 192 | #ifdef USE_MYMATH 193 | #include "math/MathFunctions.h" 194 | #else 195 | #include 196 | #endif 197 | 198 | 199 | int main(int argc, char *argv[]) 200 | { 201 | if (argc < 3){ 202 | printf("Usage: %s base exponent \n", argv[0]); 203 | return 1; 204 | } 205 | double base = atof(argv[1]); 206 | int exponent = atoi(argv[2]); 207 | 208 | #ifdef USE_MYMATH 209 | printf("Now we use our own Math library. \n"); 210 | double result = power(base, exponent); 211 | #else 212 | printf("Now we use the standard library. \n"); 213 | double result = pow(base, exponent); 214 | #endif 215 | printf("%g ^ %d is %g\n", base, exponent, result); 216 | return 0; 217 | } 218 | ``` 219 | 220 | ### 3.编写 config.h.in 文件 221 | 222 | 上面的程序值得注意的是第2行,这里引用了一个 config.h 文件,这个文件预定义了 USE_MYMATH 的值。但我们并不直接编写这个文件,为了方便从 CMakeLists.txt 中导入配置,我们编写一个 config.h.in 文件,内容如下: 223 | 224 | ```cpp 225 | #cmakedefine USE_MYMATH 226 | ``` 227 | 228 | 这样 CMake 会自动根据 CMakeLists 配置文件中的设置自动生成 config.h 文件。 229 | 230 | ### 4.编译项目 231 | 232 | 现在编译一下这个项目,为了便于交互式的选择该变量的值,可以使用 ccmake 命令(也可以使用 cmake -i 命令,该命令会提供一个会话式的交互式配置界面): 233 | ![CMake的交互式配置界面](01cmake交互式配置界面.webp) 234 | CMake的交互式配置界面 235 | 从中可以找到刚刚定义的 USE_MYMATH 选项,按键盘的方向键可以在不同的选项窗口间跳转,按下 enter 键可以修改该选项。修改完成后可以按下 c 选项完成配置,之后再按 g 键确认生成 Makefile 。ccmake 的其他操作可以参考窗口下方给出的指令提示。 236 | 237 | 我们可以试试分别将 USE_MYMATH 设为 ON 和 OFF 得到的结果: 238 | 239 | USE_MYMATH 为 ON 240 | 241 | 运行结果: 242 | 243 | ```c 244 | [ehome@xman Demo4]$ ./Demo 245 | Now we use our own MathFunctions library. 246 | 7 ^ 3 = 343.000000 247 | 10 ^ 5 = 100000.000000 248 | 2 ^ 10 = 1024.000000 249 | ``` 250 | 251 | 此时 config.h 的内容为: 252 | 253 | ```cpp 254 | #define USE_MYMATH 255 | ``` 256 | 257 | USE_MYMATH 为 OFF 258 | 运行结果: 259 | 260 | ```c 261 | [ehome@xman Demo4]$ ./Demo 262 | Now we use the standard library. 263 | 7 ^ 3 = 343.000000 264 | 10 ^ 5 = 100000.000000 265 | 2 ^ 10 = 1024.000000 266 | ``` 267 | 268 | 此时 config.h 的内容为: 269 | 270 | ```cpp 271 | /* #undef USE_MYMATH */ 272 | ``` 273 | -------------------------------------------------------------------------------- /tools/cmake/简单cmake教程/编译项目.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newcleanbird/CppNotes/c3d7489083bbfa50a6b2c4587bef2d587182d4a6/tools/cmake/简单cmake教程/编译项目.png --------------------------------------------------------------------------------