当前位置: 首页 > news >正文

上海永灿网站建设湛江网站制作系统

上海永灿网站建设,湛江网站制作系统,宁波网站建设信息推荐,网站改版的seo注意事项目录 1. 语言对比 1.1 C 11 新特性 2.2 C 和 C 的区别 2.3 Python 和 C 的区别 2. 编译内存相关 2.1. C 程序编译过程 2.2. C 内存管理 2.3. 栈和堆的区别 2.4. 变量的区别 2.5. 全局变量定义在头文件中有什么问题#xff1f; 2.6. 内存对齐 2.7. 什么是内存泄露 … 目录 1. 语言对比 1.1 C 11 新特性 2.2 C 和 C 的区别 2.3 Python 和 C 的区别 2. 编译内存相关 2.1. C 程序编译过程 2.2. C 内存管理 2.3. 栈和堆的区别 2.4. 变量的区别 2.5. 全局变量定义在头文件中有什么问题 2.6. 内存对齐 2.7. 什么是内存泄露 2.8. 怎么防止内存泄漏内存泄漏检测工具的原理 2.9. 智能指针有哪几种智能指针的实现原理 2.10 智能指针应用举例 2.11 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象 2.12 使用智能指针会出现什么问题怎么解决 2.13 VS检测内存泄漏定位泄漏代码位置方法 2.14 深拷贝与浅拷贝 2.15 虚拟内存 2.16 简单说一下大端、小端 3. 类相关 3.1 什么是虚函数什么是纯虚函数 3.2 虚函数和纯虚函数的区别 3.3 虚函数的实现机制 3.4 单继承和多继承的虚函数表结构 3.5 为什么构造函数不能为虚函数 3.6 为什么析构函数可以为虚函数如果不设为虚函数可能会存在什么问题 3.7 .不能声明为虚函数的有哪些 4. 语言特性相关 4.1 左值和右值的区别左值引用和右值引用的区别如何将左值转换成右值 4.2 std::move() 函数的实现原理 4.3 什么是指针指针的大小及用法 4.5 C 11 nullptr 比 NULL 优势 4.6 指针和引用的区别 4.7 常量指针和指针常量的区别 4.8 函数指针和指针函数的区别 4.9 强制类型转换有哪几种 4.10 如何判断结构体是否相等能否用 memcmp 函数判断结构体相等 4.11 参数传递时值传递、引用传递、指针传递的区别 4.12 什么是模板如何实现 4.13 函数模板和类模板的区别 4.14 什么是可变参数模板 4.15 什么是模板特化为什么特化 4.16 include 和 的区别 4.17 泛型编程如何实现 4.18 C命名空间 4.19 C STL六大组件 5. 面向对象 5.1 什么是面向对象面向对象的三大特性 5.2 重载、重写、隐藏的区别 5.3 如何理解 C 是面向对象编程 5.4 什么是多态多态如何实现 5.5 静态多态与动态多态 6. 关键字库函数 6.1 sizeof 和 strlen 的区别 6.2 lambda 表达式匿名函数的具体应用和使用场景 6.3 explicit 的作用如何避免编译器进行隐式类型转换 6.4 C 和 C static 的区别 6.5 static 的作用 6.6 static 在类中使用的注意事项定义、初始化和使用 6.7 static 全局变量和普通全局变量的异同 6.8 const 作用及用法 6.9 define 和 const 的区别 6.10 define 和 typedef 的区别 6.11 用宏实现比较大小以及两个数中的最小值 6.12 inline 作用及使用方法 6.13 inline 函数工作原理 6.14 宏定义define和内联函数inline的区别 6.15 new 的作用 6.16 new 和 malloc 如何判断是否申请到内存 6.17 delete 实现原理delete 和 delete[] 的区别 6.18 new 和 malloc 的区别delete 和 free 的区别 6.19 malloc 的原理malloc 的底层实现 6.20 C 和 C struct 的区别 6.21 为什么有了 class 还保留 struct 6.22 struct 和 union 的区别 6.23 class 和 struct 的异同 6.24 volatile 的作用是否具有原子性对编译器有什么影响 6.25 什么情况下一定要用 volatile 能否和 const 一起使用 6.26 返回函数中静态变量的地址会发生什么 6.27 extern C 的作用 6.28 sizeof(11) 在 C 和 C 中分别是什么结果 6.29 memcpy 函数的底层原理 6.30 strcpy 函数有什么缺陷 6.31 auto 类型推导的原理 6.32 malloc一次性最大能申请多大内存空间 6.33 public、protected、private的区别 7 git 分布式版本控制系统 7.1 什么是git 7.2 为什么要用git 7.3 简述集中式版本控制库和分布式版本控制库的区别 8 RAII机制 8.1 mmap基本原理和分类 8.2 RAII机制介绍 8.3 使用RAII机制的原因 8.4 RAII机制的使用方法 1. 语言对比 1.1 C 11 新特性 1. auto 类型推导 auto 关键字自动类型推导编译器会在 编译期间 通过初始值推导出变量的类型通过 auto 定义的变量必须有初始值。 2. decltype 类型推导 decltype 关键字decltype 是“declare type”的缩写译为“声明类型”。和 auto 的功能一样都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型但是不想用该表达式的值初始化变量这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。 区别 auto var val1 val2; decltype(val1 val2) var1 0; auto 根据 右边的初始值 val1 val2 推导出变量的类型并将该初始值赋值给变量 vardecltype 根据 val1 val2 表达式推导出变量的类型变量的初始值和与表达式的值无关。auto 要求变量必须初始化因为它是根据初始化的值推导出变量的类型而 decltype 不要求定义变量的时候可初始化也可以不初始化。 3. lambda 表达式 lambda 表达式又被称为 lambda 函数或者 lambda 匿名函数。 lambda匿名函数的定义: [capture list] (parameter list) - return type {function body; }; 其中 capture list捕获列表指 lambda 所在函数中定义的局部变量的列表通常为空。return type、parameter list、function body分别表示返回值类型、参数列表、函数体和普通函数一样。 #include iostream #include algorithm using namespace std;int main(){int arr[4] {4, 2, 3, 1};//对 a 数组中的元素进行升序排序sort(arr, arr4, [](int x, int y) - bool{ return x y; } );for(int n : arr){cout n ;}return 0; } 4. 范围 for 语句 for (declaration : expression){statement } 参数的含义 expression必须是一个序列例如用花括号括起来的初始值列表、数组、vector string等这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。declaration此处定义一个变量序列中的每一个元素都能转化成该变量的类型常用 auto 类型说明符。 5. 左值和右值左值引用和右值引用左值和右值 左值指表达式结束后依然存在的持久对象可以取地址具名变量或对象 。左值符号 通俗理解左值是指具有对应的可由用户访问的存储单元并且能由用户改变其值的量。如一个变量就是一个左值因为它对应着一个存储单元并可由编程者通过变量名访问和改变其值。 左值(Lvalue) →→ Location 表示内存中可以寻址可以给它赋值(const类型的变量例外) 右值表达式结束后就不再存在的临时对象不可以取地址没有名字。 右值符号 右值Rvalue) →→ Read 表示可以知道它的值例如常数 通俗的讲左值就是能够出现在赋值符号左面的东西而右值就是那些可以出现在赋值符号右面的东西 比如int a b c;a 就是一个左值可以对a取地址而bc 就是一个右值对表达式bc 取地址会报错。 一个典型的例子 a 先使用a的值再给a加1作为右值 // a的实现 int temp a; a a 1; return temp; a  先加再用作为 左值 a a 1; return a; 在C中临时对象不能作为左值但是可以作为常量引用const 。 C 11中的std::move可将左值引用转化成右值引用。 C11中右值又由两个概念组成将亡值和纯右值。 纯右值和将亡值 在C98中右值是纯右值纯右值指的是临时变量值、不跟对象关联的字面量值。包括非引用的函数返回值、表达式等比如 2、‘ch’、int func()等。将亡值是C11新增的、与右值引用相关的表达式。 纯右值非引用返回的临时变量( int func(void))、运算表达式产生的临时变量(bc)、原始字面量(2)、lambda表达式等。将亡值将要被移动的对象、T函数返回值、std::move返回值和转换为T的类型的转换函数的返回值。 将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时通过“盗取”的方式可以避免内存空间的释放和分配能够延长变量值的生命期。 右值引用和左值引用 右值引用绑定到右值的引用用 来获得右值引用右值引用只能绑定到要销毁的对象。是对一个右值进行引用的类型标记为T。因为右值不具名是以引用的形式找到它用引用来表示右值引用也是引用的引用我目前是这么想的。左值引用对一个左值进行引用的类型。常规的引用一般都是左值引用 #include iostream #include vector using namespace std; int main() {int var 42;int l_var var;int r_var var; // 错误不能将右值引用绑定到左值上int r_var2 var 40; // 正确将 r_var2 绑定到求和结果上return 0; } 引用本身不拥有所绑定对象的内存只是该对象的一个别名左值引用就是有名变量的别名右值引用是不具名变量的别名。因此无论左值引用还是右值引用都必须立即进行初始化。 通过右值引用这个将亡的右值又“重获新生”它的生命周期与右值引用类型变量的生命周期一样只要这个右值引用类型的变量还活着那么这个右值临时量就会一直活着这是一重要特性可利用这一点会一些性能优化避免临时对象的拷贝构造和析构。 左值引用包括常量左值引用和非常量左值引用。非常量左值引用只能接受左值不能接受右值常量左值引用是一个“万能”的引用类型可以接受左值常量左值、非常量左值、右值。不过常量左值所引用的右值在它的“余生”中只能是只读的。 int a 2; // 非常量左值引用 绑定到 右值编译失败int b 2; // b 是非常量左值 const int c b; // 常量左值引用 绑定到 非常量左值编译通过const int d 2; // d 是常量左值 const int e d; // 常量左值引用 绑定到 常量左值编译通过 const int f 2; // 常量左值引用 绑定到 右值编译通过 右值引用通常不能绑定到任何的左值要想绑定一个左值到右值引用通常需要std::move()将左值强制转换为右值。比如 int a; int r1 a; // 编译失败 int r2 std::move(a); // 编译通过 简单总结 左值引用, 即i, 是一种对象类型的引用; 右值引用, 即i, 是一种对象值的引用;move() 函数可以把左值引用, 转换为右值引用;左值引用是固定的引用, 右值引用是易变的引用, 只能引用字面值(literals)或临时对象(temporary object);右值引用主要应用在移动构造器(move constructor) 和移动-赋值操作符(move-assignment operator)上面; 代码如下 #include iostream #include utility int main (void) { int i 42; int lr i; int rr i*42; const int lr1 i*42; int rr1 42; int rr2 std::move(lr); std::cout i i std::endl; std::cout lr lr std::endl; std::cout rr rr std::endl; std::cout lr1 lr1 std::endl; std::cout rr1 rr1 std::endl; std::cout rr2 rr2 std::endl; } 6. 标准库 move() 函数 move() 函数通过该函数可获得绑定到左值上的右值引用该函数包括在 utility 头文件中。该知识点会在后续的章节中做详细的说明。 7. 智能指针 相关知识已在第一章中进行了详细的说明这里不再重复。 8. delete 函数和 default 函数 delete 函数 delete 表示该函数不能被调用。default 函数 default 表示编译器生成默认的函数例如生成默认的构造函数。 #include iostream using namespace std;class A { public:A() default; // 表示使用默认的构造函数~A() default; // 表示使用默认的析构函数A(const A ) delete; // 表示类的对象禁止拷贝构造A operator(const A ) delete; // 表示类的对象禁止拷贝赋值 }; int main() {A ex1;A ex2 ex1; // error: use of deleted function A::A(const A)A ex3;ex3 ex1; // error: use of deleted function A A::operator(const A)return 0; } 2.2 C 和 C 的区别 首先说一下面向对象和面向过程 面向过程的思路面向过程编程就是分析出解决问题的步骤然后把这些步骤一步一步的实现使用的时候一个一个的依次调用就可以了。面向对象的思路面向对象编程就是把问题分解成各个对象建立对象的目的不是为了完成一个步骤而是为了描述某个事物在整个解决问题的步骤中的行为。 举个例子玩五子棋 1用面向过程的思想来考虑就是开始游戏白子先走绘制画面判断输赢轮到黑子绘制画面判断输赢重复前面的过程输出最终结果。 2用面向对象的思想来考虑就是黑白双方两者的行为是一样的、棋盘系统负责绘制画面、规定系统规定输赢、犯规等、输出系统输出赢家。 面向对象就是高度实物抽象化功能划分、面向过程就是自顶向下的编程步骤划分 区别和联系 C和C一个典型的区别就在动态内存管理上了C语言通过malloc和free来进行堆内存的分配和释放而C是通过new和delete来管理堆内存的强制类型转换上也不一样C的强制类型转换使用()小括号里面加类型进行类型强转的而C有四种自己的类型强转方式分别是const_caststatic_castreinterpret_cast和dynamic_castC和C的输入输出方式也不一样printf/scanf和C的cout/cin的对别前面一组是C的库函数后面是ostream和istream类型的对象。C还支持namespace名字空间可以让用户自己定义新的名字空间作用域出来避免全局的名字冲突问题。应用领域C 语言主要用于嵌入式领域驱动开发等与硬件直接打交道的领域C 可以用于应用层开发用户界面开发等与操作系统打交道的领域。C 既继承了 C 强大的底层操作特性又被赋予了面向对象机制。它特性繁多面向对象语言的多继承对值传递与引用传递的区分以及 const 关键字等等。C 对 C 的“增强”表现在以下几个方面类型检查更为严格。增加了面向对象的机制、泛型编程的机制Template、异常处理、运算符重载、标准模板库STL、命名空间避免全局命名冲突。 面向过程语言 优点性能比面向对象高因为类调用时需要实例化开销比较大比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发性能是最重要的因素。 缺点没有面向对象易维护、易复用、易扩展 面向对象语言 优点易维护、易复用、易扩展由于面向对象有封装、继承、多态性的特性可以设计出低耦合的系统使系统 更加灵活、更加易于维护 缺点性能比面向过程低 2.3 Python 和 C 的区别 区别 语言自身Python 为脚本语言解释执行不需要经过编译C 是一种需要编译后才能运行的语言在特定的机器上编译后运行。运行效率C 运行效率高安全稳定。原因Python 代码和 C 最终都会变成 CPU指令来跑但一般情况下比如反转和合并两个字符串Python 最终转换出来的 CPU 指令会比 C 多很多。首先Python中涉及的内容比 C 多经过了更多层Python 中甚至连数字都是 object 其次Python 是解释执行的和物理机CPU 之间多了解释器这层而 C 是编译执行的直接就是机器码编译的时候编译器又可以进行一些优化。开发效率Python 开发效率高。原因Python 一两句代码就能实现的功能C 往往需要更多的代码才能实现。书写格式和语法不同Python 的语法格式不同于其 C 定义声明才能使用而且极其灵活完全面向更上层的开发者。 2. 编译内存相关 2.1. C 程序编译过程 编译过程分为四个过程编译编译预处理、编译、优化汇编链接。 编译预处理处理以 # 开头的指令产生 .i 文件 主要的处理操作如下 对全部的#define进行宏展开。处理全部的条件编译指令比方#if、#ifdef、#elif、#else、#endif;处理 #include 指令这个过程是递归的也就是说被包括的文件可能还包括其它文件;删除全部的注释 // 和 /**/加入行号和文件标识保留全部的 #pragma 编译器指令 ps:经过预处理后的 .i 文件不包括任何宏定义由于全部的宏已经被展开。而且包括的文件也已经被插入到 .i 文件里。 编译、优化将源码 .cpp 文件翻译成 .s 汇编代码 词法分析将源代码的字符序列分割成一系列的记号。语法分析对记号进行语法分析产生语法树。语义分析判断表达式是否有意义。代码优化目标代码生成生成汇编代码。目标代码优化 编译会将源代码由文本形式转换成机器语言编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后的.s是ASCII码文件。 汇编将汇编代码 .s 翻译成机器指令的 .o 或.obj 目标文件 汇编过程调用汇编器AS来完成是用于将汇编代码转换成机器可以执行的指令每一个汇编语句几乎都对应一条机器指令。汇编后的.o文件是纯二进制文件。 链接产生 .out 或 .exe 可运行文件 汇编程序生成的目标文件即 .o 文件并不会立即执行因为可能会出现.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体从而生成可执行的程序 .exe文件。 详细来说链接是将所有的.o文件和库动态库、静态库链接在一起得到可以运行的可执行文件Windows的.exe文件或Linux的.out文件等。它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向。 最基本的链接叫做静态链接就是将每个模块的源代码文件编译、汇编成目标文件Linux.o 文件Windows.obj文件然后将目标文件和库一起链接形成最后的可执行文件.exe或.out等。库其实就是一组目标文件的包就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库它是支持程序运行的基本函数的集合。 链接分为两种 静态链接代码从其所在的静态链接库中拷贝到最终的可执行程序中在该程序被执行时这些代码会被装入到该进程的虚拟地址空间中。 把目标程序运行时需要调用的函数代码直接链接到了生成的可执行文件中程序在运行的时候不需要其他额外的库文件且就算你去静态库把程序执行需要的库删掉也不会影响程序的运行因为所需要的所有东西已经被链接到了链接阶段生成的可执行文件中。 Windows下以.lib为后缀Linux下以.a为后缀。 动态链接代码被放到动态链接库或共享对象的某个目标文件中链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。 动态 “动” 在了程序在执行阶段需要去寻找相应的函数代码即在程序运行时才会将程序安装模块链接在一起 具体来说动态链接就是把调⽤的函数所在⽂件模块DLL 和调⽤函数在⽂件中的位置等信息链接进目标程序程序运⾏的时候再从 DLL 中寻找相应函数代码因此需要相应 DLL ⽂件的⽀持 。Windows 包含函数重定位信息的文件在Windows下以.dll为后缀Linux下以.so为后缀。 二者的区别 静态链接是 将各个模块的obj和库链接成一个完整的可执行程序而动态链接是程序在运行的时候寻找动态库的函数符号重定位即DLL不必被包含在最终的exe文件中 链接使用工具不同: 静态链接由称为“链接器”的工具完成动态链接由操作系统在程序运行时完成链接 库包含限制 静态链接库中不能再包含其他的动态链接库或者静态库动态链接库中还可以再包含其他的动态或静态链接库。 运行速度 静态链接运行速度快因为执行过程中不用重定位可独立运行动态链接运行速度慢、不可独立运行 二者的优缺点 静态链接浪费空间每个可执行程序都会有目标文件的一个副本这样如果目标文件进行了更新操作就需要重新进行编译链接生成可执行程序更新困难优点就是执行的时候运行速度快因为可执行程序具备了程序运行的所有内容。 动态链接节省内存、更新方便但是动态链接是在程序运行时每次执行都需要链接相比静态链接会有一定的性能损失。 2.2. C 内存管理 C的内存分布模型 从高地址到低地址一个程序由 内核空间、栈区、堆区、BSS段、数据段data、代码区组成。 常说的C 内存分区栈、堆、全局/静态存储区、常量存储区、代码区。 可执行程序在运行时会多出两个区域 栈存放函数的局部变量、函数参数、返回地址等由编译器自动分配和释放。栈从高地址向低地址增长。是一块连续的空间。栈一般分配几M大小的内存。堆动态申请的内存空间就是由 malloc 分配的内存块由程序员控制它的分配和释放如果程序执行结束还没有释放操作系统会自动回收。堆从低地址向高地址增长。一般可以分配几个G大小的内存。在堆栈之间有一个 共享区文件映射区。全局区/静态存储区.BSS 段和 .data 段存放全局变量和静态变量程序运行结束操作系统自动释放在 C 语言中程序中未初始化的全局变量和静态变量存放在.BSS 段中已初始化的全局变量和静态变量存放在 .data 段中C 中不再区分了。常量存储区.data 段存放的是常量不允许修改程序运行结束自动释放。代码区.text 段存放程序执行代码的一块内存区域。只读不允许修改但可以执行。编译后的二进制文件存放在这里。代码段的头部还会包含一些只读的常量如字符串常量字面值注意const变量虽然属于常量但是本质还是变量不存储于代码段 在linux下size命令可以查看一个可执行二进制文件基本情况 2.3. 栈和堆的区别 申请方式栈是系统自动分配堆是程序员主动申请。申请后系统响应分配栈空间如果剩余空间大于申请空间则分配成功否则分配失败栈溢出申请堆空间堆在内存中呈现的方式类似于链表记录空闲地址空间的链表在链表上寻找第一个大于申请空间的节点分配给程序将该节点从链表中删除大多数系统中该块空间的首地址存放的是本次分配空间的大小便于释放将该块空间上的剩余空间再次连接在空闲链表上。栈在内存中是连续的一块空间向低地址扩展最大容量是系统预定好的堆在内存中的空间向高地址扩展是不连续的。申请效率栈是有系统自动分配申请效率高但程序员无法控制堆是由程序员主动申请效率低使用起来方便但是容易产生碎片。存放的内容栈中存放的是局部变量函数的参数堆中存放的内容由程序员控制。 此题总结 1、申请方式的不同。 栈由系统自动分配而堆是人为申请开辟; 2、申请大小的不同。 栈获得的空间较小而堆获得的空间较大; 3、申请效率的不同。 栈由系统自动分配速度较快而堆一般速度比较慢; 4、 存储的内容不同。 栈在函数调用时第一个进栈的是主函数中后的下一条指令函数调用语句的下一条可执行语句的地址然后是函数的各个参数在大多数的C编译器中参数是由右往左入栈的然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后局部变量先出栈然后是参数最后栈顶指针指向最开始存的地址也就是主函数中的下一条指令程序由该点继续运行。 堆一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。 2.4. 变量的区别 全局变量、局部变量、静态全局变量、静态局部变量的区别 全局变量就是定义在函数外的变量。 局部变量就是函数内定义的变量。 静态变量就是加了static的变量。 例如static int value 1 各自存储的位置 全局变量存储在常量区静态存储区。静态变量存储在常量区静态存储区。局部变量, 存储在栈区。 注意: 因为静态变量都在静态存储区常量区所以下次调用函数的时候还是能取到原来的值。 各自初始化的值 局部变量, 存储在栈区。局部变量一般是不初始化的局部变量, 存储在栈区。全局变量和静态变量都是初始化为0的有一个初始值。局部变量, 存储在栈区。如果是类变量会调用默认构造函数初始化。 从作用域看 C 变量根据定义的位置的不同的生命周期具有不同的作用域作用域可分为 6 种全局作用域局部作用域语句作用域类作用域命名空间作用域和文件作用域。 全局变量具有全局作用域。全局变量只需在一个源文件中定义就可以作用于所有的源文件。当然其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。会一直存在到程序结束。静态全局变量全局作用域文件作用域所以无法在其他文件中使用。它与全局变量的区别在于如果程序包含多个文件的话它作用于定义它的文件里不能作用到其它文件里即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量它们也是不同的变量。局部变量具有局部作用域。比如函数的参数函数内的局部变量等等它是自动对象auto在程序运行期间不是一直存在而是只在函数执行期间存在函数的一次调用执行结束后变量被销毁其所占用的内存也被收回。静态局部变量具有局部作用域。它只被初始化一次 直到程序结束。自从第一次被初始化直到程序运行结束都一直存在它和全局变量的区别在于全局变量对所有的函数都是可见的而静态局部变量只对定义自己的函数体始终可见。 从分配内存空间看 静态存储区全局变量静态局部变量静态全局变量。栈局部变量。 各自的应用场景 局部变量就是我们经常用的进入函数逐个构造最后统一销毁。全局变量主要是用来给不同的文件之间进行通信。静态变量只在本文件中使用局部静态变量在函数内起作用可以作为一个计数器。 例子 void func(){static int count;count ;}int main(int argc, char** argv){for(int i 0; i 10; i)func();} 说说静态变量在代码执行的什么阶段进行初始化 static int value //静态变量初始化语句 对于C语言 静态变量和全局变量均在编译期进行初始化即初始化发生在任何代码执行之前。 对于C 静态变量和全局变量仅当首次被使用的时候才进行初始化。 助记 如果你使用过C/C你会发现C语言要求在程序的最开头声明全部的变量而C则可以随时使用随时声明这个规律是不是和答案类似呢 2.5. 全局变量定义在头文件中有什么问题 如果在头文件中定义全局变量当该头文件被多个文件 include 时该头文件中的全局变量就会被定义多次导致重复定义因此不能再头文件中定义全局变量。 2.6. 内存对齐 什么是内存对齐内存对齐的原则为什么要进行内存对齐有什么优点 内存对齐编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 内存对齐的原则 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除结构体每个成员相对于结构体首地址的偏移量 offset都是该成员大小与对齐基数中的较小者的整数倍如有需要编译器会在成员之间加上填充字节 internal padding结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍如有需要编译器会在最末一个成员之后加上填充字节trailing padding。 进行内存对齐的原因主要是硬件设备方面的问题 某些硬件设备只能存取对齐数据存取非对齐的数据可能会引发异常某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作相比于存取对齐的数据存取非对齐的数据需要花费更多的时间某些处理器虽然支持非对齐数据的访问但会引发对齐陷阱alignmenttrap某些硬件设备只支持简单数据指令非对齐存取不支持复杂数据指令的非对齐存取。 内存对齐的优点 便于在不同的平台之间进行移植因为有些硬件平台不能够支持任意地址的数据访问只能在某些地址处取某些特定的数据否则会抛出异常提高内存的访问效率因为 CPU 在读取内存时是一块一块的读取。 2.7. 什么是内存泄露 内存泄漏由于疏忽或错误导致的程序未能释放已经不再使用的内存。 进一步解释 并非指内存从物理上消失而是指程序在运行过程中由于疏忽或错误而失去了对该内存的控制从而造成了内存的浪费。常指堆内存泄漏因为堆是动态分配的而且是用户来控制的如果使用不当会产生内存泄漏。使用 malloc、calloc、realloc、new 等分配内存时使用完后要调用相应的 free 或 delete释放内存否则这块内存就会造成内存泄漏。指针重新赋值 char *p (char *)malloc(10); char *p1 (char *)malloc(10); p np; 开始时指针 p 和 p1 分别指向一块内存空间但指针 p 被重新赋值导致 p 初始时指向的那块内存空间无法找到从而发生了内存泄漏。 2.8. 怎么防止内存泄漏内存泄漏检测工具的原理 防止内存泄漏的方法 内部封装将内存的分配和释放封装到类中在构造的时候申请内存析构的时候释放内存。说明但这样做并不是最佳的做法在类的对象复制时程序会出现同一块内存空间释放两次的情况智能指针智能指针是 C 中已经对内存泄漏封装好了一个工具可以直接拿来使用将在下一个问题中对智能指针进行详细的解释。 VS下内存泄漏的检测方法CRT 在debug模式下以F5运行 #define CRTDBG_MAP_ALLOC #include stdlib.h #include crtdbg.h //在入口函数中包含 _CrtDumpMemoryLeaks(); //即可检测到内存泄露//以如下测试函数为例 int main(){char* pChars new char[10];_CrtDumpMemoryLeaks();return 0; } 2.9. 智能指针有哪几种智能指针的实现原理 智能指针是为了解决动态内存分配时忘记释放内存导致的内存泄漏以及多次释放同一块内存空间而提出的。C11 中封装在了 #include memory  头文件中。 C11 引入了 3 个智能指针类型 std::unique_ptr 独占资源所有权的指针。 std::shared_ptr 共享资源所有权的指针。 std::weak_ptr 共享资源的观察者需要和 std::shared_ptr 一起使用不影响资源的生命周期。 注std::auto_ptr 已被废弃。 共享指针shared_ptr资源可以被多个指针共享使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数可以通过 unique_ptr、weak_ptr 来构造调用 release() 释放资源的所有权计数减一当计数减为 0 时会自动释放内存空间从而避免了内存泄漏。 独占指针unique_ptr独享所有权的智能指针资源只能被一个指针占有该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造调用move() 函数即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象可以通过该方法进行赋值。 弱指针weak_ptr指向 shared_ptr 指向的对象能够解决由shared_ptr带来的循环引用问题。 智能指针的实现原理 计数原理。 2.10 智能指针应用举例 unique_ptr unique_ptr 的使用比较简单也是用得比较多的智能指针。当我们独占资源的所有权的时候可以使用 unique_ptr 对资源进行管理——离开 unique_ptr 对象的作用域时会自动释放资源。这是很基本的 RAII 思想。 自动管理内存 使用裸指针时要记得释放内存。 {int* p new int(100);// ...delete p; // 要记得释放内存 } 使用 unique_ptr 自动管理内存。 {std::unique_ptrint uptr std::make_uniqueint(200);//...// 离开 uptr 的作用域的时候自动释放内存 } unique_ptr 是 move-only 的也是实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象的方法 {std::unique_ptrint uptr std::make_uniqueint(200);std::unique_ptrint uptr1 uptr; // 编译错误std::unique_ptrT 是 move-only 的std::unique_ptrint uptr2 std::move(uptr);assert(uptr nullptr); } unique_ptr 可以指向一个数组 {std::unique_ptrint[] uptr std::make_uniqueint[](10);for (int i 0; i 10; i) {uptr[i] i * i;} for (int i 0; i 10; i) {std::cout uptr[i] std::endl; //0 1 4 9 ...81} } 也可以用向量 unique_ptrvectorint p (new vectorint(5, 6)); //n 5, value 6 std::cout *p-begin() endl;//6 shared_ptr shared_ptr 其实就是对资源做引用计数——当引用计数 sptr.use_count() 为 0的时候自动释放资源。其中assert(p);用于判断指针内容是否非空空指针nullptr 与什么未指向的野指针过不了assert {std::shared_ptrint sptr std::make_sharedint(200);assert(sptr.use_count() 1); // 此时引用计数为 1{ std::shared_ptrint sptr1 sptr;assert(sptr.get() sptr1.get());assert(sptr.use_count() 2); // sptr 和 sptr1 共享资源引用计数为 2} assert(sptr.use_count() 1); // sptr1 已经释放 } // use_count 为 0 时自动释放内存 和 unique_ptr 一样shared_ptr 也可以指向数组和自定义 deleter。 {// C20 才支持 std::make_sharedint[]// std::shared_ptrint[] sptr std::make_sharedint[](100);std::shared_ptrint[] sptr(new int[10]);for (int i 0; i 10; i) {sptr[i] i * i;} for (int i 0; i 10; i) {std::cout sptr[i] std::endl;} } 附 一个 shared_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique_ptr 对象略大。 无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行无需保存其它信息所以它的开销和裸指针是一样的。如果有自定义 deleter还需要保存 deleter 的信息。 shared_ptr 需要维护的信息有两部分 指向共享资源的指针。引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。 所以shared_ptr 对象需要保存两个指针。shared_ptr 的 的 deleter 是保存在控制信息中所以是否有自定义 deleter 不影响 shared_ptr 对象的大小。 当我们创建一个 shared_ptr 时其实现一般如下 std::shared_ptrT sptr1(new T); 最好使用make_shared实现 shared_ptrstring p1 make_sharedstring(10, 9); shared_ptrint p2 make_sharedint(42); 复制一个 shared_ptr std::shared_ptrT sptr2 sptr1; 为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针可不可以去掉 shared_ptr 对象中指向共享资源的指针以节省内存开销 答案是不能。 因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。 来看一个例子。 struct Fruit {int juice; };struct Vegetable {int fiber; };struct Tomato : public Fruit, Vegetable {int sauce; };// 由于继承的存在shared_ptr 可能指向基类对象 std::shared_ptrTomato tomato std::make_sharedTomato(); std::shared_ptrFruit fruit tomato; std::shared_ptrVegetable vegetable tomato; 此外在使用 shared_ptr 时会涉及两次内存分配一次分配共享资源对象一次分配控制块。C 标准库提供了 make_shared 函数来创建一个 shared_ptr 对象只需要一次内存分配所以推荐用make_shared 函数来创建对象。 weak_ptr weak_ptr 要与 shared_ptr 一起使用。 一个 weak_ptr 对象看做是 shared_ptr 对象管理的资源的观察者它不影响共享资源的生命周期 如果需要使用 weak_ptr 正在观察的资源可以将 weak_ptr 提升为 shared_ptr。当 shared_ptr 管理的资源被释放时weak_ptr 会自动变成 nullptr。 void Observe(std::weak_ptrint wptr) {if (auto sptr wptr.lock()) {std::cout value: *sptr std::endl;} else {std::cout wptr lock fail std::endl;} }std::weak_ptrint wptr; {auto sptr std::make_sharedint(111);wptr sptr;Observe(wptr); // sptr 指向的资源没被释放wptr 可以成功提升为 shared_ptr } Observe(wptr); // sptr 指向的资源已被释放wptr 无法提升为 shared_ptr 当 shared_ptr 析构并释放共享资源的时候只要 weak_ptr 对象还存在控制块就会保留weak_ptr 可以通过控制块观察到对象是否存活。 2.11 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象 借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象其目的是实现所有权的转移。 // A 作为一个类 std::unique_ptrA ptr1(new A()); std::unique_ptrA ptr2 std::move(ptr1); 2.12 使用智能指针会出现什么问题怎么解决 智能指针可能出现的问题循环引用 比如定义了两个类 Parent、Child在两个类中分别定义另一个类的对象的共享指针由于在程序结束后两个指针相互指向对方的内存空间导致内存无法释放。 循环引用的解决方法 weak_ptr 循环引用该被调用的析构函数没有被调用从而出现了内存泄漏。 weak_ptr 对被 shared_ptr 管理的对象存在非拥有性弱引用在访问所引用的对象前必须先转化为 shared_ptr weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题若这种环被孤立没有指向环中的外部共享指针shared_ptr 引用计数无法抵达 0内存被泄露令环中的指针之一为弱指针可以避免该情况 weak_ptr 用来表达临时所有权的概念当某个对象只有存在时才需要被访问而且随时可能被他人删除可以用 weak_ptr 跟踪该对象需要获得所有权时将其转化为 shared_ptr此时如果原来的 shared_ptr 被销毁则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。 2.13 VS检测内存泄漏定位泄漏代码位置方法 检查方法 在main函数最后面一行加上一句_CrtDumpMemoryLeaks()。调试程序自然关闭程序让其退出不要定制调试查看输出 Detected memory leaks! Dumping objects - {453} normal block at 0x02432CA8, 868 bytes long.Data: 404303374 34 30 34 33 30 33 33 37 34 00 00 00 00 00 00 00 {447} normal block at 0x024328B0, 868 bytes long.Data: 404303374 34 30 34 33 30 33 33 37 34 00 00 00 00 00 00 00 {441} normal block at 0x024324B8, 868 bytes long.Data: 404303374 34 30 34 33 30 33 33 37 34 00 00 00 00 00 00 00 {435} normal block at 0x024320C0, 868 bytes long.Data: 404303374 34 30 34 33 30 33 33 37 34 00 00 00 00 00 00 00 {429} normal block at 0x02431CC8, 868 bytes long.Data: 404303374 34 30 34 33 30 33 33 37 34 00 00 00 00 00 00 00 {212} normal block at 0x01E1BF30, 44 bytes long.Data: 60 B3 E1 01 CD CD CD CD CD CD CD CD CD CD CD CD {204} normal block at 0x01E1B2C8, 24 bytes long.Data: C8 B2 E1 01 C8 B2 E1 01 C8 B2 E1 01 CD CD CD CD {138} normal block at 0x01E15680, 332 bytes long.Data: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 {137} normal block at 0x01E15628, 24 bytes long.Data: (V (V (V 28 56 E1 01 28 56 E1 01 28 56 E1 01 CD CD CD CD Object dump complete. 取其中一条详细说明{453} normal block at 0x02432CA8, 868 bytes long. 被{}包围的453就是我们需要的内存泄漏定位值868 bytes long就是说这个地方有868比特内存没有释放。 在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序……程序中断了。查看调用堆栈 双击我们的代码调用的最后一个函数这里是CDbQuery::UpdateDatas()就定位到了申请内存的代码 好了我们总算知道是哪里出问题了这块内存没有释放啊。改代码修复好这个。然后继续…………直到调试输出中没有normal block 程序没有内存泄漏了。 记得加上头文件#include crtdbg.h 最后要注意一点的并不是所有normal block一定就有内存泄漏当你的程序中有全局变量的时候全局变量的释放示在main函数退出后所以在main函数最后_CrtDumpMemoryLeaks会认为全局申请的内存没有释放造成内存泄漏的假象。如何规避呢我通常是把全局变量声明成指针在main函数中new 在main函数中delete然后再调用_CrtDumpMemoryLeaks这样就不会误判了。 请自行查阅 Linux检测内存泄漏定位泄漏代码位置方法 2.14 深拷贝与浅拷贝 c默认的拷贝构造函数是浅拷贝 浅拷贝就是对象的数据成员之间的简单赋值如你设计了一个类而没有提供它的复制构造函数当用该类的一个对象去给另一个对象赋值时所执行的过程就是浅拷贝。当数据成员中没有指针时浅拷贝是可行的但当数据成员中有指针时如果采用简单的浅拷贝则两类中的两个指针将指向同一个地址当对象快结束时会调用两次析构函数而导致指针悬挂现象所以此时必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据而不是一个简单的赋值过程从而也就解决了指针悬挂的问题。 2.15 虚拟内存 物理内存 物理内存实际上是CPU中能直接寻址的地址线条数。由于物理内存是有限的例如32位平台下寻址的大小是4G并且是固定的。内存很快就会被分配完于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的。 虚拟内存 在进程创建的时候系统都会给每个进程分配4G的内存空间这其实是虚拟内存空间。进程得到的这4G虚拟内存进程自身以为是一段连续的空间而实际上通常被分隔成多个物理内存碎片还有一部分存储在外部磁盘存储器上需要的时候进行数据交换。 关于虚拟内存与物理内存的联系下面这张图可以帮助我们巩固。 虚拟内存机理及优点 虚拟内存是如何工作的 当每个进程创建的时候内核会为进程分配4G的虚拟内存当进程还没有开始运行时这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码比如.text .data段拷贝到物理内存中只是建立好虚拟内存和磁盘文件之间的映射就好叫做存储器映射。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时进程去寻找页表发现页表中地址没有存放在物理内存上而是在磁盘上于是发生缺页异常于是将磁盘上的数据拷贝到物理内存中。另外在进程运行过程中要通过malloc来动态分配内存时也只是分配了虚拟内存即为这块虚拟内存对应的页表项做相应设置当进程真正访问到此数据时才引发缺页异常。可以认为虚拟空间都被映射到了磁盘空间中事实上也是按需要映射到磁盘空间上通过mmapmmap是用来建立虚拟空间和磁盘空间的映射关系的 利用虚拟内存机制的优点 既然每个进程的内存空间都是一致而且固定的32位平台下都是4G所以链接器在链接可执行文件时可以设定内存地址而不用去管这些数据最终实际内存地址这交给内核来完成映射关系当不同的进程使用同一段代码时比如库文件的代码在物理内存中可以只存储一份这样的代码不同进程只要将自己的虚拟内存映射过去就好了这样可以节省物理内存在程序需要分配连续空间的时候只需要在虚拟内存分配连续空间而不需要物理内存时连续的实际上往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存 2.16 简单说一下大端、小端 换句话说 存在字符串 高有效位 → 12 34 56 78 → 低有效位br/小端 低地址位 → 7 8 56 34 12 → 高地址位br/大 端 低地址位 → 12 34 56 78 → 高地址位 助记 大端模式符合阅读习惯 高字节存放在低地址低字节存放在高地址类似于把数据当作字符串顺序处理小端模式低字节存放在低地址高字节存放在高地址即高地址部分权值高低地址部分权值低 3. 类相关 3.1 什么是虚函数什么是纯虚函数 虚函数被 virtual 关键字修饰的成员函数就是虚函数。 纯虚函数 纯虚函数在类中声明时加上 0含有纯虚函数的类称为抽象类只要含有纯虚函数这个类就是抽象类类中只有接口没有具体的实现方法继承纯虚函数的派生类如果没有完全实现基类纯虚函数依然是抽象类不能实例化对象。 说明 抽象类对象不能作为函数的参数不能创建对象不能作为函数返回类型可以声明抽象类指针可以声明抽象类的引用子类必须继承父类的纯虚函数并全部实现后才能创建子类的对象。 3.2 虚函数和纯虚函数的区别 虚函数和纯虚函数可以出现在同一个类中该类称为抽象基类。含有纯虚函数的类称为抽象基类使用方式不同虚函数可以直接使用纯虚函数必须在派生类中实现后才能使用定义形式不同虚函数在定义时在普通函数的基础上加上 virtual 关键字纯虚函数定义时除了加上virtual 关键字还需要加上 0;虚函数必须实现否则编译器会报错对于实现纯虚函数的派生类该纯虚函数在派生类中被称为虚函数虚函数和纯虚函数都可以在派生类中重写析构函数最好定义为虚函数特别是对于含有继承关系的类析构函数可以定义为纯虚函数此时其所在的类为抽象基类不能创建实例化对象。 3.3 虚函数的实现机制 实现机制虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中在类的对象所在的内存空间中保存了指向虚函数表的指针称为“虚表指针”通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题当用基类的指针来操作一个派生类的时候这张虚函数表就指明了实际应该调用的函数 虚函数表相关知识点 虚函数表存放的内容类的虚函数的地址。虚函数表建立的时间编译阶段即程序的编译过程中会将虚函数的地址放在虚函数表中。虚表指针保存的位置虚表指针存放在对象的内存空间中最前面的位置这是为了保证正确取到虚函数的偏移量。 注虚函数表和类绑定虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的但是每个对象都有自己的虚表指针来指向类的虚函数表。 实例 无虚函数覆盖的情况 #include iostream using namespace std;class Base { public:virtual void B_fun1() { cout Base::B_fun1() endl; }virtual void B_fun2() { cout Base::B_fun2() endl; }virtual void B_fun3() { cout Base::B_fun3() endl; } };class Derive : public Base { public:virtual void D_fun1() { cout Derive::D_fun1() endl; }virtual void D_fun2() { cout Derive::D_fun2() endl; }virtual void D_fun3() { cout Derive::D_fun3() endl; } }; int main() {Base *p new Derive();p-B_fun1(); // Base::B_fun1()return 0; } 主函数中基类的指针 p 指向了派生类的对象当调用函数 B_fun1() 时通过派生类的虚函数表找到该函数的地址从而完成调用。 3.4 单继承和多继承的虚函数表结构 编译器处理虚函数表 编译器将虚函数表的指针放在类的实例对象的内存空间中该对象调用该类的虚函数时通过指针找到虚函数表根据虚函数表中存放的虚函数的地址找到对应的虚函数。如果派生类没有重新定义基类的虚函数 A则派生类的虚函数表中保存的是基类的虚函数 A 的地址也就是说基类和派生类的虚函数 A 的地址是一样的。如果派生类重写了基类的某个虚函数 B则派生的虚函数表中保存的是重写后的虚函数 B 的地址也就是说虚函数 B 有两个版本分别存放在基类和派生类的虚函数表中。如果派生类重新定义了新的虚函数 C派生类的虚函数表保存新的虚函数 C 的地址。 3.5 为什么构造函数不能为虚函数 虚函数的调用需要虚函数表指针而该指针存放在对象的内存空间中若构造函数声明为虚函数那么由于对象还未创建还没有内存空间更没有虚函数表地址用来调用虚函数——构造函数了。 3.6 为什么析构函数可以为虚函数如果不设为虚函数可能会存在什么问题 防止内存泄露delete p基类的时候它很机智的先执行了派生类的析构函数然后执行了基类的析构函数。 如果基类的析构函数不是虚函数在delete p基类时调用析构函数时只会看指针的数据类型而不会去看赋值的对象这样就会造成内存泄露。 举例说明 子类B继承自基类AA *p new B; delete p; 1 此时如果类A的析构函数不是虚函数那么delete p将会仅仅调用A的析构函数只释放了B对象中的A部分而派生出的新的部分未释放掉。 2 如果类A的析构函数是虚函数delete p; 将会先调用B的析构函数再调用A的析构函数释放B对象的所有空间。 补充 B *p new B; delete p;时也是先调用B的析构函数再调用A的析构函数。 3.7 .不能声明为虚函数的有哪些 1、静态成员函数 2、类外的普通函数 3、构造函数 4、友元函数 虚函数是为了实现多态特性的。虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数其是有有如下几点需要注意 1 类的构造函数不能是虚函数 构造函数是为了构造对象的所以在调用构造函数时候必然知道是哪个对象调用了构造函数所以构造函数不能为虚函数。 2 类的静态成员函数不能是虚函数 类的静态成员函数是该类共用的与该类的对象无关静态函数里没有this指针所以不能为虚函数。 3内联函数 内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处这样代码运行到这里时候就不需要花时间去调用函数。inline是在编译器将函数类容替换到函数调用处是静态编译的。而虚函数是动态调用的在编译器并不知道需要调用的是父类还是子类的虚函数所以不能够inline声明展开所以编译器会忽略。 4友元函数 友元函数与该类无关没有this指针所以不能为虚函数。 4. 语言特性相关 4.1 左值和右值的区别左值引用和右值引用的区别如何将左值转换成右值 左值指表达式结束后依然存在的持久对象。 右值表达式结束就不再存在的临时对象。 左值和右值的区别左值持久右值短暂 右值引用和左值引用的区别 左值引用不能绑定到要转换的表达式、字面常量或返回右值的表达式。右值引用恰好相反可以绑定到这类表达式但不能绑定到一个左值上。右值引用必须绑定到右值的引用通过 获得。右值引用只能绑定到一个将要销毁的对象上因此可以自由地移动其资源。 std::move可以将一个左值强制转化为右值继而可以通过右值引用使用该值以用于移动语义。 #include iostream using namespace std;void fun1(int tmp) { cout fun1(int tmp): tmp endl; } void fun2(int tmp) { cout fun2(int tmp) tmp endl; } int main() { int var 11; fun1(12); // error: cannot bind non-const lvalue reference of type int to an rvalue of type intfun1(var);fun2(1); } 4.2 std::move() 函数的实现原理 std::move() 函数原型 template typename T typename remove_referenceT::type move(T t) {return static_casttypename remove_referenceT::type (t); } 说明引用折叠原理 右值传递给上述函数的形参 T 依然是右值即 T 相当于 T。左值传递给上述函数的形参 T 依然是左值即 T 相当于 T。 小结通过引用折叠原理可以知道move() 函数的形参既可以是左值也可以是右值。 remove_reference 具体实现 //原始的最通用的版本 template typename T struct remove_reference{typedef T type; //定义 T 的类型别名为 type };//部分版本特例化将用于左值引用和右值引用 template class T struct remove_referenceT //左值引用 { typedef T type; }template class T struct remove_referenceT //右值引用 { typedef T type; } //举例如下,下列定义的a、b、c三个变量都是int类型 int i; remove_refrencedecltype(42)::type a; //使用原版本 remove_refrencedecltype(i)::type b; //左值引用特例版本 remove_refrencedecltype(std::move(i))::type b; //右值引用特例版本 举例 int var 10; 转化过程 1. std::move(var) std::move(int ) 折叠后 std::move(int)2. 此时T 的类型为 inttypename remove_referenceT::type 为 int这里使用 remove_reference 的左值引用的特例化版本3. 通过 static_cast 将 int 强制转换为 int整个std::move被实例化如下 string move(int t) {return static_castint(t); } 总结 std::move() 实现原理 利用引用折叠原理将右值经过 T 传递类型保持不变还是右值而左值经过 T变为普通的左值引用以保证模板可以传递任意实参且保持类型不变然后通过 remove_refrence 移除引用得到具体的类型 T最后通过 static_cast 进行强制类型转换返回 T 右值引用。 4.3 什么是指针指针的大小及用法 指针 指向另外一种类型的复合类型。指针的大小 在 64 位计算机中指针占 8 个字节空间。 #includeiostreamusing namespace std;int main(){int *p nullptr;cout sizeof(p) endl; // 8char *p1 nullptr;cout sizeof(p1) endl; // 8return 0; } 指针的用法 1.指向普通对象的指针 #include iostreamusing namespace std;class A { };int main() {A *p new A();return 0; } 2.指向常量对象的指针常量指针 #include iostream using namespace std;int main(void) {const int c_var 10;const int * p c_var;cout *p endl;return 0; } 3.指向函数的指针函数指针 #include iostream using namespace std;int add(int a, int b){return a b; }int main(void) {int (*fun_p)(int, int);fun_p add;cout fun_p(1, 6) endl;return 0; } 4.指向对象成员的指针包括指向对象成员函数的指针和指向对象成员变量的指针。 特别注意定义指向成员函数的指针时要标明指针所属的类。 #include iostreamusing namespace std;class A { public:int var1, var2; int add(){return var1 var2;} };int main() {A ex;ex.var1 3;ex.var2 4;int *p ex.var1; // 指向对象成员变量的指针cout *p endl;int (A::*fun_p)();fun_p A::add; // 指向对象成员函数的指针 fun_pcout (ex.*fun_p)() endl;return 0; } 5.this 指针指向类的当前对象的指针常量。 #include iostream #include cstring using namespace std;class A { public:void set_name(string tmp){this-name tmp;}void set_age(int tmp){this-age age;}void set_sex(int tmp){this-sex tmp;}void show(){cout Name: this-name endl;cout Age: this-age endl;cout Sex: this-sex endl;}private:string name;int age;int sex; };int main() {A *p new A();p-set_name(Alice);p-set_age(16);p-set_sex(1);p-show();return 0; } 6.什么是野指针和悬空指针 悬空指针 若指针指向一块内存空间当这块内存空间被释放后该指针依然指向这块内存空间此时称该指针为“悬空指针”。 void *p malloc(size); free(p); // 此时p 指向的内存空间已释放 p 就是悬空指针。 野指针 “野指针”是指不确定其指向的指针未初始化的指针为“野指针”。 void *p; // 此时 p 是“野指针”。 4.5 C 11 nullptr 比 NULL 优势 NULL预处理变量是一个宏它的值是 0定义在头文件 中即 #define NULL 0。nullptrC 11 中的关键字是一种特殊类型的字面值可以被转换成任意其他类型。 nullptr 的优势 有类型类型是 typdef decltype(nullptr) nullptr_t;使用 nullptr 提高代码的健壮性。函数重载因为 NULL 本质上是 0在函数调用过程中若出现函数重载并且传递的实参是 NULL可能会出现不知和哪一个函数匹配的情况但是传递实参 nullptr 就不会出现这种情况。 4.6 指针和引用的区别 指针所指向的内存空间在程序运行过程中可以改变而引用所绑定的对象一旦绑定就不能改变。是否可变指针本身在内存中占有内存空间引用相当于变量的别名在内存中不占内存空间。是否占内存指针可以为空但是引用必须绑定对象。是否可为空指针可以有多级但是引用只能一级。是否能为多级 4.7 常量指针和指针常量的区别 常量指针 常量指针本质上是个指针只不过这个指针指向的对象是常量。 特点const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧无论它在类型名的左边或右边都表示指向常量的指针。可以这样理解* 左侧表示指针指向的对象该对象为常量那么该指针为常量指针。 const int * p; int const * p; 注意 1指针指向的对象不能通过这个指针来修改也就是说常量指针可以被赋值为变量的地址之所以叫做常量指针是限制了通过这个指针修改变量的值。 #include iostream using namespace std;int main() {const int c_var 8;const int *p c_var; *p 6; // error: assignment of read-only location * preturn 0; } 注意 2虽然常量指针指向的对象不能变化可是因为常量指针本身是一个变量因此可以被重新赋值。 例如 #include iostream using namespace std;int main(){const int c_var1 8;const int c_var2 8;const int *p c_var1; p c_var2;return 0; } 指针常量 指针常量的本质上是个常量只不过这个常量的值是一个指针。 特点const 位于指针声明操作符右侧表明该对象本身是一个常量* 左侧表示该指针指向的类型即以 * 为分界线其左侧表示指针指向的类型右侧表示指针本身的性质。 const int var; int * const c_p var; 注意 1指针常量的值是指针这个值因为是常量所以指针本身不能改变。 #include iostream using namespace std;int main() {int var, var1;int * const c_p var;c_p var1; // error: assignment of read-only variable c_preturn 0; } 注意 2指针的内容可以改变。 #include iostream using namespace std;int main(){int var 3;int * const c_p var;*c_p 12; return 0; } 4.8 函数指针和指针函数的区别 指针函数 指针函数本质是一个函数只不过该函数的返回值是一个指针。相对于普通函数而言只是返回值是指针。 #include iostream using namespace std;struct Type {int var1;int var2; };Type * fun(int tmp1, int tmp2){Type * t new Type();t-var1 tmp1;t-var2 tmp2;return t; }int main() {Type *p fun(5, 6);return 0; } 函数指针 函数指针本质是一个指针变量只不过这个指针指向一个函数。函数指针即指向函数的指针。 举例 #include iostream using namespace std; int fun1(int tmp1, int tmp2) {return tmp1 * tmp2; } int fun2(int tmp1, int tmp2) {return tmp1 / tmp2; }int main() {int (*fun)(int x, int y); fun fun1;cout fun(15, 5) endl; fun fun2;cout fun(15, 5) endl; return 0; } /* 运行结果 75 3 */ 函数指针和指针函数的区别 本质不同 1.指针函数本质是一个函数其返回值为指针。 2.函数指针本质是一个指针变量其指向一个函数。 定义形式不同 1.指针函数int* fun(int tmp1, int tmp2); 这里* 表示函数的返回值类型是指针类型。 2.函数指针int (fun)(int tmp1, int tmp2);这里 表示变量本身是指针类型。 用法不同 4.9 强制类型转换有哪几种 static_cast用于数据的强制类型转换强制将一种数据类型转换为另一种数据类型。 1.用于基本数据类型的转换。 2.用于类层次之间的基类和派生类之间 指针或者引用 的转换不要求必须包含虚函数但必须是有相互联系的类进行上行转换派生类的指针或引用转换成基类表示是安全的进行下行转换基类的指针或引用转换成派生类表示由于没有动态类型检查所以是不安全的最好用 dynamic_cast 进行下行转换。 3.可以将空指针转化成目标类型的空指针。 4.可以将任何类型的表达式转化成 void 类型。 const_cast强制去掉常量属性不能用于去掉变量的常量性只能用于去除指针或引用的常量性将常量指针转化为非常量指针或者将常量引用转化为非常量引用注意表达式的类型和要转化的类型是相同的。reinterpret_cast改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。dynamic_cast 1.其他三种都是编译时完成的动态类型转换是在程序运行时处理的运行时会进行类型检查。 2.只能用于带有虚函数的基类或派生类的指针或者引用对象的转换转换成功返回指向类型的指针或引用转换失败返回 NULL不能用于基本数据类型的转换。 3.在向上进行转换时即派生类类的指针转换成基类类的指针和 static_cast 效果是一样的注意这里只是改变了指针的类型指针指向的对象的类型并未发生改变。 4.在下行转换时基类的指针类型转化为派生类类的指针类型只有当要转换的指针指向的对象类型和转化以后的对象类型相同时才会转化成功。 #include iostream #include cstringusing namespace std;class Base { public:virtual void fun(){cout Base::fun() endl;} };class Derive : public Base { public:virtual void fun(){cout Derive::fun() endl;} };int main() {Base *p1 new Derive();Base *p2 new Base();Derive *p3 new Derive();//转换成功p3 dynamic_castDerive *(p1);if (p3 NULL){cout NULL endl;}else{cout NOT NULL endl; // 输出}//转换失败p3 dynamic_castDerive *(p2);if (p3 NULL){cout NULL endl; // 输出}else{cout NOT NULL endl;}return 0; } 4.10 如何判断结构体是否相等能否用 memcmp 函数判断结构体相等 需要重载操作符 判断两个结构体是否相等不能用函数 memcmp 来判断两个结构体是否相等因为 memcmp 函数是逐个字节进行比较的而结构体存在内存空间中保存时存在字节对齐字节对齐时补的字节内容是随机的会产生垃圾值所以无法比较。 利用运算符重载来实现结构体对象的比较 #include iostreamusing namespace std;struct A {char c;int val;A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}friend bool operator(const A tmp1, const A tmp2); // 友元运算符重载函数 };bool operator(const A tmp1, const A tmp2) {return (tmp1.c tmp2.c tmp1.val tmp2.val); }int main() {A ex1(a, 90), ex2(b, 80);if (ex1 ex2)cout ex1 ex2 endl;elsecout ex1 ! ex2 endl; // 输出return 0; } 4.11 参数传递时值传递、引用传递、指针传递的区别 参数传递的三种方式 值传递形参是实参的拷贝函数对形参的所有操作不会影响实参。指针传递本质上是值传递只不过拷贝的是指针的值拷贝之后实参和形参是不同的指针通过指针可以间接的访问指针所指向的对象从而可以修改它所指对象的值。引用传递当形参是引用类型时我们说它对应的实参被引用传递。 4.12 什么是模板如何实现 模板创建类或者函数的蓝图或者公式分为函数模板和类模板。实现方式模板定义以关键字 template 开始后跟一个模板参数列表。 模板参数列表不能为空模板类型参数前必须使用关键字 class 或者 typename在模板参数列表中这两个关键字含义相同可互换使用。 template typename T, typename U, ... 函数模板通过定义一个函数模板可以避免为每一种类型定义一个新函数。 对于函数模板而言模板类型参数可以用来指定返回类型或函数的参数类型以及在函数体内用于变量声明或类型转换。函数模板实例化当调用一个模板时编译器用函数实参来推断模板实参从而使用实参的类型来确定绑定到模板参数的类型 #includeiostream using namespace std;template typename T T add_fun(const T tmp1, const T tmp2){return tmp1 tmp2; }int main(){int var1, var2;cin var1 var2;cout add_fun(var1, var2);double var3, var4;cin var3 var4;cout add_fun(var3, var4);return 0; } 类模板类似函数模板类模板以关键字 template 开始后跟模板参数列表。但是编译器不能为类模板推断模板参数类型需要在使用该类模板时在模板名后面的尖括号中指明类型。 #include iostream using namespace std;template typename T class Complex{ public://构造函数Complex(T a, T b){this-a a;this-b b;}//运算符重载ComplexT operator(Complex c){ComplexT tmp(this-a c.a, this-b c.b);cout tmp.a tmp.b endl;return tmp;}private:T a;T b; };int main(){Complexint a(10, 20);Complexint b(20, 30);Complexint c a b;return 0; } 4.13 函数模板和类模板的区别 实例化方式不同函数模板实例化由编译程序在处理函数调用时自动完成类模板实例化需要在程序中显式指定。实例化的结果不同函数模板实例化后是一个函数类模板实例化后是一个类。默认参数类模板在模板参数列表中可以有默认参数。特化函数模板只能全特化而类模板可以全特化也可以偏特化。调用方式不同函数模板可以隐式调用也可以显式调用类模板只能显式调用。 函数模板调用方式举例 #includeiostream using namespace std;template typename T T add_fun(const T tmp1, const T tmp2){return tmp1 tmp2; }int main(){int var1, var2;cin var1 var2;cout add_funint(var1, var2); // 显式调用double var3, var4;cin var3 var4;cout add_fun(var3, var4); // 隐式调用return 0; } 4.14 什么是可变参数模板 可变参数模板接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包包括模板参数包和函数参数包。 模板参数包表示零个或多个模板参数函数参数包表示零个或多个函数参数。 用省略号来指出一个模板参数或函数参数表示一个包在模板参数列表中class… 或 typename… 指出接下来的参数表示零个或多个类型的列表一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时可以使用 sizeof… 运算符。 template typename T, typename... Args // Args 是模板参数包 void foo(const T t, const Args... rest); // 可变参数模板rest 是函数参数包 #include iostream using namespace std;template typename T void print_fun(const T t){cout t endl; // 最后一个元素 }template typename T, typename... Args void print_fun(const T t, const Args ...args){cout t ;print_fun(args...); }int main(){print_fun(Hello, wolrd, !);return 0; } /*运行结果 Hello wolrd ! */ 说明可变参数函数通常是递归的第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本打印绑定到 t 的实参并用来调用自身来打印函数参数包中的剩余值。 4.15 什么是模板特化为什么特化 模板特化的原因模板并非对任何模板实参都合适、都能实例化某些情况下通用模板的定义对特定类型不合适可能会编译失败或者得不到正确的结果。因此当不希望使用模板版本时可以定义类或者函数模板的一个特例化版 模板特化模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化 函数模板特化将函数模板中的全部类型进行特例化称为函数模板特化。类模板特化将类模板中的部分或全部类型进行特例化称为类模板特化。 特化分为全特化和偏特化 全特化模板中的模板参数全部特例化。偏特化模板中的模板参数只确定了一部分剩余部分需要在编译器编译时确定。 说明要区分下函数重载与函数模板特化 定义函数模板的特化版本本质上是接管了编译器的工作为原函数模板定义了一个特殊实例而不是函数重载函数模板特化并不影响函数匹配。 #include iostream #include cstring using namespace std; //函数模板 template class T bool compare(T t1, T t2){cout 通用版本;return t1 t2; }template //函数模板特化 bool compare(char *t1, char *t2){cout 特化版本;return strcmp(t1, t2) 0; }在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间创建和物理内存的映射关系。int main(int argc, char *argv[]){char arr1[] hello;char arr2[] abc;cout compare(123, 123) endl;cout compare(arr1, arr2) endl;return 0; } /* 运行结果 通用版本1 特化版本0 */ 4.16 include 和 的区别 include文件名 和 #include文件名 的区别: 查找文件的位置include文件名在标准库头文件所在的目录中查找如果没有再到当前源文件所在目录下查找#include文件名 在当前源文件所在目录中进行查找如果没有再到系统目录中查找。使用习惯对于标准库中的头文件常用 include文件名对于自己定义的头文件常用 #include文件名 4.17 泛型编程如何实现 泛型编程实现的基础模板。模板是创建类或者函数的蓝图或者说公式当时用一个 vector 这样的泛型或者 find 这样的泛型函数时编译时会转化为特定的类或者函数。 泛型编程涉及到的知识点较广例如容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。 容器涉及到 STL 中的容器例如vector、list、map 等可选其中熟悉底层原理的容器进行展开讲解。迭代器在无需知道容器底层原理的情况下遍历容器中的元素。模板可参考本章节中的模板相关问题。 4.18 C命名空间 使用命名空间的目的是对标识符的名称进行本地化以避免命名冲突。在C中变量、函数和类都是大量存在的。如果没有命名空间这些变量、函数、类的名称将都存在于全局命名空间中会导致很多冲突。比如如果我们在自己的程序中定义了一个函functionA()这将重写标准库中的functionA()函 数这是因为这两个函数都是位于全局命名空间中的。 4.19 C STL六大组件 为了建立数据结构和算法的一套标准并且降低他们之间的耦合关系以提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),诞生了STL。 STL提供了六大组件彼此之间可以组合套用这六大组件分别是:容器、算法、迭代器、仿函数、适配器配接器、空间配置器。 容器各种数据结构如vector、list、deque、set、map等,用来存放数据从实现角度来看STL容器是一种class template。 算法各种常用的算法如sort、find、copy、for_each。从实现的角度来看STL算法是一种function tempalte. 迭代器扮演了容器与算法之间的胶合剂共有五种类型从实现角度来看迭代器是一种将operator* , operator- , operator,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。 仿函数行为类似函数可作为算法的某种策略。从实现角度来看仿函数是一种重载了operator()的class 或者class template 适配器一种用来修饰容器或者仿函数或迭代器接口的东西。 空间配置器负责空间的配置与管理。从实现角度看配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte. STL六大组件的交互关系容器通过空间配置器取得数据存储空间算法通过迭代器存储容器中的内容仿函数可以协助算法完成不同的策略的变化适配器可以修饰仿函数。 5. 面向对象 5.1 什么是面向对象面向对象的三大特性 面向对象对象是指具体的某一个事物这些事物的抽象就是类类中包含数据成员变量和动作成员方法。 面向对象的三大特性 封装将具体的实现过程和数据封装成一个函数只能通过接口进行访问降低耦合性。继承子类继承父类的特征和行为子类有父类的非 private 方法或成员变量子类可以对父类的方法进行重写增强了类之间的耦合性但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时修饰的类不能继承修饰的成员不能重写或修改。多态多态就是不同继承类的对象对同一消息做出不同的响应基类的指针指向或绑定到派生类的对象使得基类指针呈现不同的表现方式。 5.2 重载、重写、隐藏的区别 重载是指同一可访问区内被声明几个具有不同参数列参数的类型、个数、顺序的同名函数根据参数列表确定调用哪个函数重载不关心函数返回类型。 class A { public:void fun(int tmp);void fun(float tmp); // 重载 参数类型不同相对于上一个函数void fun(int tmp, float tmp1); // 重载 参数个数不同相对于上一个函数void fun(float tmp, int tmp1); // 重载 参数顺序不同相对于上一个函数int fun(int tmp); // error: int A::fun(int) cannot be overloaded 错误注意重载不关心函数返回类型 }; 隐藏是指派生类的函数屏蔽了与其同名的基类函数主要只要同名函数不管参数列表是否相同基类函数都会被隐藏。 #include iostream using namespace std;class Base { public:void fun(int tmp, float tmp1) { cout Base::fun(int tmp, float tmp1) endl; } };class Derive : public Base { public:void fun(int tmp) { cout Derive::fun(int tmp) endl; } // 隐藏基类中的同名函数 };int main() {Derive ex;ex.fun(1); // Derive::fun(int tmp)ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 providedreturn 0; } 说明上述代码中 ex.fun(1, 0.01); 出现错误说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数可以加上类型名指明 ex.Base::fun(1, 0.01);这样就可以调用基类中的同名函数。 重写(覆盖)是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致只有函数体不同。派生类调用时会调用派生类的重写函数不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。 #include iostream using namespace std;class Base { public:virtual void fun(int tmp) { cout Base::fun(int tmp) : tmp endl; } };class Derived : public Base { public:virtual void fun(int tmp) { cout Derived::fun(int tmp) : tmp endl; } // 重写基类中的 fun 函数 }; int main() {Base *p new Derived();p-fun(3); // Derived::fun(int) : 3return 0; } 重写和重载的区别 范围区别对于类中函数的重载或者重写而言重载发生在同一个类的内部重写发生在不同的类之间子类和父类之间。参数区别重载的函数需要与原函数有相同的函数名、不同的参数列表不关注函数的返回值类型重写的函数的函数名、参数列表和返回值类型都需要和原函数相同父类中被重写的函数需要有 virtual 修饰。virtual 关键字重写的函数基类中必须有 virtual关键字的修饰重载的函数可以有 virtual 关键字的修饰也可以没有。 隐藏和重写重载的区别 范围区别隐藏与重载范围不同隐藏发生在不同类中。参数区别隐藏函数和被隐藏函数参数列表可以相同也可以不同但函数名一定相同当参数不同时无论基类中的函数是否被 virtual修饰基类函数都是被隐藏而不是重写。 5.3 如何理解 C 是面向对象编程 说明该问题最好结合自己的项目经历进行展开解释或举一些恰当的例子同时对比下面向过程编程。 面向过程编程一种以执行程序操作的过程或函数为中心编写软件的方法。程序的数据通常存储在变量中与这些过程是分开的。所以必须将变量传递给需要使用它们的函数。缺点随着程序变得越来越复杂程序数据与运行代码的分离可能会导致问题。例如程序的规范经常会发生变化从而需要更改数据的格式或数据结构的设计。当数据结构发生变化时对数据进行操作的代码也必须更改为接受新的格式。查找需要更改的所有代码会为程序员带来额外的工作并增加了使代码出现错误的机会。面向对象编程Object-Oriented Programming, OOP以创建和使用对象为中心。一个对象Object就是一个软件实体它将数据和程序在一个单元中组合起来。对象的数据项也称为其属性存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。 面向对象编程进一步说明 面向对象编程将数据成员和成员函数封装到一个类中并声明数据成员和成员函数的访问级别public、private、protected以便控制类对象对数据成员和函数的访问对数据成员起到一定的保护作用。而且在类的对象调用成员函数时只需知道成员函数的名、参数列表以及返回值类型即可无需了解其函数的实现原理。当类内部的数据成员或者成员函数发生改变时不影响类外部的代码。 5.4 什么是多态多态如何实现 多态多态就是不同继承类的对象对同一消息做出不同的响应基类的指针指向或绑定到派生类的对象使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字在派生类中重写该函数运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类就调用派生类的函数如果对象类型是基类就调用基类的函数。 实现方法多态是通过虚函数实现的虚函数的地址保存在虚函数表中虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。 实现过程 在类中用 virtual 关键字声明的函数叫做虚函数存在虚函数的类都有一个虚函数表当创建一个该类的对象时该对象有一个指向虚函数表的虚表指针虚函数表和类对应的虚表指针是和对象对应当基类指针指向派生类对象基类指针调用虚函数时基类指针指向派生类的虚表指针由于该虚表指针指向派生类虚函数表通过遍历虚表寻找相应的虚函数。 5.5 静态多态与动态多态 静态多态也称为编译期间的多态编译器在编译期间完成的编译器根据函数实参的类型(可能会进行隐式类型转换)可推断出要调用那个函数如果有对应的函数就调用该函数否则出现编译错误。 动态多态动态绑定即运行时的多态在程序执行期间(非编译期)判断所引用对象的实际类型根据其实际类型调用相应的方法。 ● 基类中必须包含虚函数并且派生类中一定要对基类中的虚函数进行重写。 ● 通过基类对象的指针或者引用调用虚函数。 #include iostream using namespace std;class Base { public:virtual void fun() { cout Base::fun() endl; }virtual void fun1() { cout Base::fun1() endl; }virtual void fun2() { cout Base::fun2() endl; } }; class Derive : public Base { public:void fun() { cout Derive::fun() endl; }virtual void D_fun1() { cout Derive::D_fun1() endl; }virtual void D_fun2() { cout Derive::D_fun2() endl; } }; int main() {Base *p new Derive();p-fun(); // Derive::fun() 调用派生类中的虚函数return 0; } 简单解释当基类的指针指向派生类的对象时通过派生类的对象的虚表指针找到虚函数表派生类的对象虚函数表进而找到相应的虚函数 Derive::f() 进行调用。 6. 关键字库函数 6.1 sizeof 和 strlen 的区别 strlen 是头文件 中的函数sizeof 是 C 中的运算符。strlen 测量的是字符串的实际长度其源代码如下以 \0 结束。而 sizeof 测量的是字符数组的分配大小。 strlen 源代码: size_t strlen(const char *str) {size_t length 0;while (*str)length;return length; } #include iostream #include cstringusing namespace std;int main() {char arr[10] hello;cout strlen(arr) endl; // 5cout sizeof(arr) endl; // 10return 0; } 3.若字符数组 arr 作为函数的形参sizeof(arr) 中 arr 被当作字符指针来处理strlen(arr) 中 arr 依然是字符数组从下述程序的运行结果中就可以看出。 #include iostream #include cstringusing namespace std;void size_of(char arr[]) {cout sizeof(arr) endl; // warning: sizeof on array function parameter arr will return size of char* .cout strlen(arr) endl; }int main() {char arr[20] hello;size_of(arr); return 0; } /* 输出结果 8 5 */ 4.strlen 本身是库函数因此在程序运行过程中计算长度而 sizeof 在编译时计算长度 5.sizeof 的参数可以是类型也可以是变量strlen 的参数必须是 char* 类型的变量 6.2 lambda 表达式匿名函数的具体应用和使用场景 lambda 表达式的定义形式如下 [capture list] (parameter list) - reurn type {function body } 其中 capture list捕获列表指 lambda 表达式所在函数中定义的局部变量的列表通常为空但如果函数体中用到了 lambda 表达式所在函数的局部变量必须捕获该变量即将此变量写在捕获列表中。捕获方式分为引用捕获方式 []、值捕获方式 []。return type、parameter list、function body分别表示返回值类型、参数列表、函数体和普通函数一样。 常见使用场景排序算法 bool compare(int a, int b) {return a b; }int main(void) {int data[6] { 3, 4, 12, 2, 1, 6 };vectorint testdata;testdata.insert(testdata.begin(), data, data 6);// 排序算法sort(testdata.begin(), testdata.end(), compare); // 升序// 使用lambda表达式sort(testdata.begin(), testdata.end(), [](int a, int b){ return a b; });return 0; } 6.3 explicit 的作用如何避免编译器进行隐式类型转换 作用用来声明类构造函数是显示调用的而非隐式调用可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数因为无参构造函数和多参构造函数本身就是显示调用的再加上 explicit 关键字也没有什么意义。 隐式转换 #include iostream #include cstring using namespace std;class A { public:int var;A(int tmp){var tmp;} }; int main() {A ex 10; // 发生了隐式转换return 0; } 上述代码中A ex 10; 在编译时进行了隐式转换将 10 转换成 A 类型的对象然后将该对象赋值给 ex等同于如下操作 为了避免隐式转换可用 explicit 关键字进行声明 #include iostream #include cstring using namespace std;class A { public:int var;explicit A(int tmp){var tmp;cout var endl;} }; int main() {A ex(100);A ex1 10; // error: conversion from int to non-scalar type A requestedreturn 0; } 6.4 C 和 C static 的区别 在 C 语言中使用 static 可以定义局部静态变量、外部静态变量、静态函数在 C 中使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C中有类的概念静态成员变量、静态成员函数都是与类有关的概念。 6.5 static 的作用 作用 static 定义静态变量静态函数。 保持变量内容持久static 作用于局部变量改变了局部变量的生存周期使得该变量存在于定义后直到程序运行结束的这段时间。隐藏static作用于全局变量和函数改变了全局变量和函数的作用域使得全局变量和函数只能在定义它的文件中使用在源文件中不具有全局可见性。注普通全局变量和函数具有全局可见性即其他的源文件也可以使用。static 作用于类的成员变量和类的成员函数使得类变量或者类成员函数和类有关也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意类的静态成员函数中只能访问静态成员变量或者静态成员函数不能将静态成员函数定义成虚函数。 6.6 static 在类中使用的注意事项定义、初始化和使用 static 静态成员变量 静态成员变量是在类内进行声明在类外进行定义和初始化在类外进行定义和初始化的时候不要出现 static关键字和private、public、protected 访问规则。静态成员变量相当于类域中的全局变量被类的所有对象所共享包括派生类的对象。静态成员变量可以作为成员函数的参数而普通成员变量不可以。 #include iostream using namespace std;class A { public:static int s_var;int var;void fun1(int i s_var); // 正确静态成员变量可以作为成员函数的参数void fun2(int i var); // error: invalid use of non-static data member A::var }; 4.静态数据成员的类型可以是所属类的类型而普通数据成员的类型只能是该类类型的指针或引用。 #include iostream using namespace std;class A { public:static A s_var; // 正确静态数据成员A var; // error: field var has incomplete type AA *p; // 正确指针A var1; // 正确引用 }; static 静态成员函数 静态成员函数不能调用非静态成员变量或者非静态成员函数因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。静态成员函数不能声明成虚函数virtual、const 函数和 volatile 函数。 6.7 static 全局变量和普通全局变量的异同 相同点 存储方式普通全局变量和 static 全局变量都是静态存储方式。 不同点 作用域普通全局变量的作用域是整个源程序当一个源程序由多个源文件组成时普通全局变量在各个源文件中都是有效的静态全局变量则限制了其作用域即只在定义该变量的源文件内有效在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内只能为该源文件内的函数公用因此可以避免在其他源文件中引起错误。初始化静态全局变量只初始化一次防止在其他文件中使用。 6.8 const 作用及用法 作用 const 修饰成员变量定义成 const 常量相较于宏常量可进行类型检查节省内存空间提高了效率。const 修饰函数参数使得传递过来的函数参数的值不能改变。const 修饰成员函数使得成员函数不能修改任何类型的成员变量mutable 修饰的变量除外也不能调用非 const 成员函数因为非 const 成员函数可能会修改成员变量。 在类中的用法 const 成员变量 const 成员变量只能在类内声明、定义在构造函数初始化列表中初始化。const 成员变量只在某个对象的生存周期内是常量对于整个类而言却是可变的因为类可以创建多个对象不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量类的对象还没有创建编译器不知道他的值。 const 成员函数 不能修改成员变量的值除非有 mutable 修饰只能访问成员变量。不能调用非常量成员函数以防修改成员变量的值。 6.9 define 和 const 的区别 区别 编译阶段define 是在编译预处理阶段进行替换const 是在编译阶段确定其值。安全性define 定义的宏常量没有数据类型只是进行简单的替换不会进行类型安全的检查const 定义的常量是有类型的是要进行判断的可以避免一些低级的错误。内存占用define 定义的宏常量在程序中使用多少次就会进行多少次替换内存中有多个备份占用的是代码段的空间const 定义的常量占用静态存储区的空间程序运行过程中只有一份。调试define 定义的宏常量不能调试因为在预编译阶段就已经进行替换了cons定义的常量可以进行调试。 const 的优点 有数据类型在定义式可进行安全性检查。可调式。占用较少的空间。 6.10 define 和 typedef 的区别 原理#define 作为预处理指令在编译预处理时进行替换操作不作正确性检查只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字在编译时处理有类型检查功能用来给一个已经存在的类型一个别名但不能在一个函数定义里面使用 typedef 。功能typedef 用来定义类型的别名方便使用。#define 不仅可以为类型取别名还可以定义常量、变量、编译开关等。作用域#define 没有作用域的限制只要是之前预定义过的宏在以后的程序中都可以使用而 typedef 有自己的作用域。指针的操作typedef 和 #define 在处理指针时不完全一样 #include iostream #define INTPTR1 int * typedef int * INTPTR2;using namespace std;int main() {INTPTR1 p1, p2; // p1: int *; p2: intINTPTR2 p3, p4; // p3: int *; p4: int *int var 1;const INTPTR1 p5 var; // 相当于 const int * p5; 常量指针即不可以通过 p5 去修改 p5 指向的内容但是 p5 可以指向其他内容。const INTPTR2 p6 var; // 相当于 int * const p6; 指针常量不可使 p6 再指向其他内容。return 0; } 6.11 用宏实现比较大小以及两个数中的最小值 #include iostream #define MAX(X, Y) ((X)(Y)?(X):(Y)) #define MIN(X, Y) ((X)(Y)?(X):(Y)) using namespace std;int main () {int var1 10, var2 100;cout MAX(var1, var2) endl;cout MIN(var1, var2) endl;return 0; } /* 程序运行结果 100 10 */ 6.12 inline 作用及使用方法 作用 inline 是一个关键字可以用于定义内联函数。内联函数像普通函数一样被调用但是在调用时并不通过函数调用的机制而是直接在调用点处展开这样可以大大减少由函数调用带来的开销从而提高程序的运行效率。 使用方法 类内定义成员函数默认是内联函数 在类内定义成员函数可以不用在函数头部加 inline 关键字因为编译器会自动将类内定义的函数构造函数、析构函数、普通成员函数等声明为内联函数代码如下 #include iostream using namespace std;class A{ public:int var;A(int tmp){ var tmp;} void fun(){ cout var endl;} }; 类外定义成员函数若想定义为内联函数需用关键字声明 当在类内声明函数在类外定义函数时如果想将该函数定义为内联函数则可以在类内声明时不加 inline 关键字而在类外定义函数时加上 inline 关键字。 #include iostream using namespace std;class A{ public:int var;A(int tmp){ var tmp;} void fun(); };inline void A::fun(){cout var end 另外可以在声明函数和定义函数的同时加上 inline也可以只在函数声明时加 inline而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。 6.13 inline 函数工作原理 内联函数不是在调用时发生控制转移关系而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。普通函数是将程序执行转移到被调用函数所存放的内存地址当函数执行完后返回到执行此函数前的地方。转移操作需要保护现场被调函数执行完后再恢复现场该过程需要较大的资源开销。 6.14 宏定义define和内联函数inline的区别 内联函数是在编译时展开而宏在编译预处理时展开在编译的时候内联函数直接被嵌入到目标代码中去而宏只是一个简单的文本替换。内联函数是真正的函数和普通函数调用的方法一样在调用点处直接展开避免了函数的参数压栈操作减少了调用的开销。而宏定义编写较为复杂常需要增加一些括号来避免歧义。宏定义只进行文本替换不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数会对参数的类型、函数体内的语句编写是否正确等进行检查。 #include iostream #define MAX(a, b) ((a) (b) ? (a) : (b)) using namespace std;inline int fun_max(int a, int b){return a b ? a : b; }int main(){int var 1;cout MAX(var, 5) endl; cout fun_max(var, 0) endl; return 0; } /* 程序运行结果 5 1 */ 6.15 new 的作用 new 是 C 中的关键字用来动态分配内存空间实现方式如下 int *p new int[5]; 6.16 new 和 malloc 如何判断是否申请到内存 malloc 成功申请到内存返回指向该内存的指针分配失败返回 NULL 指针。new 内存分配成功返回该对象类型的指针分配失败抛出 bac_alloc 异常。 6.17 delete 实现原理delete 和 delete[] 的区别 delete 的实现原理 首先执行该对象所属类的析构函数进而通过调用 operator delete 的标准库函数来释放所占的内存空间。 delete 和 delete [] 的区别 delete 用来释放单个对象所占的空间只会调用一次析构函数delete [] 用来释放数组空间会对数组中的每个成员都调用一次析构函数。 6.18 new 和 malloc 的区别delete 和 free 的区别 在使用的时候 new、delete 搭配使用malloc、free 搭配使用。 malloc、free 是库函数而new、delete 是关键字。new 申请空间时无需指定分配空间的大小编译器会根据类型自行计算malloc 在申请空间时需要确定所申请空间的大小。new 申请空间时返回的类型是对象的指针类型无需强制类型转换是类型安全的操作符malloc 申请空间时返回的是 void* 类型需要进行强制类型的转换转换为对象类型的指针。new 分配失败时会抛出 bad_alloc 异常malloc 分配失败时返回空指针。对于自定义的类型new 首先调用 operator new() 函数申请空间底层通过 malloc 实现然后调用构造函数进行初始化最后返回自定义类型的指针delete 首先调用析构函数然后调用 operator delete() 释放空间底层通过 free 实现。malloc、free 无法进行自定义类型的对象的构造和析构。new 操作符从自由存储区上为对象动态分配内存而 malloc 函数从堆上动态分配内存。自由存储区不等于堆 6.19 malloc 的原理malloc 的底层实现 malloc 的原理: 当开辟的空间小于 128K 时调用 brk() 函数通过移动 _enddata 来实现当开辟空间大于 128K 时调用 mmap() 函数通过在虚拟地址空间中开辟一块内存空间来实现。 malloc 的底层实现 brk() 函数实现原理向高地址的方向移动指向数据段的高地址的指针 _enddata。 mmap 内存映射原理 1.进程启动映射过程并在虚拟地址空间中为映射创建虚拟映射区域 2.调用内核空间的系统调用函数 mmap()实现文件物理地址和进程虚拟地址的一一映射关系 3.进程发起对这片映射空间的访问引发缺页异常实现文件内容到物理内存主存的拷贝。 6.20 C 和 C struct 的区别 在 C 语言中 struct 是用户自定义数据类型在 C 中 struct 是抽象数据类型支持成员函数的定义。C 语言中 struct 没有访问权限的设置是一些变量的集合体不能定义成员函数C 中 struct 可以和类一样有访问权限并可以定义成员函数。C 语言中 struct 定义的自定义数据类型在定义该类型的变量时需要加上 struct 关键字例如struct A var;定义 A 类型的变量而 C 中不用加该关键字例如A var; 6.21 为什么有了 class 还保留 struct C 是在 C 语言的基础上发展起来的为了与 C 语言兼容C 中保留了 struct。 6.22 struct 和 union 的区别 说明union 是联合体struct 是结构体。 区别 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时联合体只有一个有效的成员而结构体所有的成员都有效。对联合体的不同成员赋值将会对覆盖其他成员的值而对于结构体的对不同成员赋值时相互不影响。联合体的大小为其内部所有变量的最大值按照最大类型的倍数进行分配大小结构体分配内存的大小遵循内存对齐原则。 6.23 class 和 struct 的异同 struct 和 class 都可以自定义数据类型也支持继承操作。struct 中默认的访问级别是 public默认的继承级别也是 publicclass 中默认的访问级别是 private默认的继承级别也是 private。当 class 继承 struct 或者 struct 继承 class 时默认的继承级别取决于 class 或 struct 本身classprivate 继承structpublic 继承即取决于派生类的默认继承级别。 struct A{} class B : A{}; // private 继承 struct C : B{} // public 继承 class 可以用于定义模板参数struct 不能用于定义模板参数。 6.24 volatile 的作用是否具有原子性对编译器有什么影响 volatile 的作用当对象的值可能在程序的控制或检测之外被改变时应该将该对象声明为 violatile告知编译器不应对这样的对象进行优化。 volatile不具有原子性。 volatile 对编译器的影响使用该关键字后编译器不会对相应的对象进行优化即不会将变量从内存缓存到寄存器中防止多个线程有可能使用内存中的变量有可能使用寄存器中的变量从而导致程序错误。 6.25 什么情况下一定要用 volatile 能否和 const 一起使用 使用 volatile 关键字的场景 当多个线程都会用到某一变量并且该变量的值有可能发生改变时需要用 volatile 关键字对该变量进行修饰中断服务程序中访问的变量或并行设备的硬件寄存器的变量最好用 volatile 关键字修饰。 volatile 关键字和 const 关键字可以同时使用某种类型可以既是 volatile 又是 const 同时具有二者的属性。 6.26 返回函数中静态变量的地址会发生什么 #include iostream using namespace std;int * fun(int tmp){static int var 10;var * tmp;return var; }int main() {cout var * fun(5) endl;return 0; }/* 运行结果 50 */ 说明上述代码中在函数 fun 中定义了静态局部变量 var使得离开该函数的作用域后该变量不会销毁返回到主函数中该变量依然存在从而使程序得到正确的运行结果。但是该静态局部变量直到程序运行结束后才销毁浪费内存空间。 6.27 extern C 的作用 extern C的主要作用就是为了能够正确实现C代码调用其他C语言代码。加上extern C后会指示编译器这部分代码按C语言而不是C的方式进行编译。由于C支持函数重载因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中而不仅仅是函数名而C语言并不支持函数重载因此编译C语言代码的函数时不会带上函数的参数类型一般只包括函数名。 举例 // 可能出现在 C 头文件cstring中的链接指示 extern C{int strcmp(const char*, const char*); } 6.28 sizeof(11) 在 C 和 C 中分别是什么结果 C 语言代码4 C 代码1布尔值大小 6.29 memcpy 函数的底层原理 void *memcpy(void *dst, const void *src, size_t size) {char *psrc;char *pdst;if (NULL dst || NULL src){return NULL;}if ((src dst) (char *)src size (char *)dst) // 出现地址重叠的情况自后向前拷贝{psrc (char *)src size - 1;pdst (char *)dst size - 1;while (size--){*pdst-- *psrc--;}}else{psrc (char *)src;pdst (char *)dst;while (size--){*pdst *psrc;}}return dst; } 6.30 strcpy 函数有什么缺陷 strcpy 函数的缺陷strcpy 函数不检查目的缓冲区的大小边界而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间同时加上字符串终止符会导致其他变量被覆盖。 说明从上述代码中可以看出变量 var 的后六位被字符串 “hello world!” 的 “d!\0” 这三个字符改变这三个字符对应的 ascii 码的十六进制为\0(0x00)!(0x21)d(0x64)。 原因变量 arr 只分配的 10 个内存空间通过上述程序中的地址可以看出 arr 和 var 在内存中是连续存放的但是在调用 strcpy 函数进行拷贝时源字符串 “hello world!” 所占的内存空间为 13因此在拷贝的过程中会占用 var 的内存空间导致 var的后六位被覆盖。 6.31 auto 类型推导的原理 auto 类型推导的原理 编译器根据初始值来推算变量的类型要求用 auto 定义变量时必须有初始值。编译器推断出来的 auto 类型有时和初始值类型并不完全一样编译器会适当改变结果类型使其更符合初始化规则。 6.32 malloc一次性最大能申请多大内存空间 malloc并不是系统调用。因此并不是内存空间的终极管理者。最大能够申请多大空间并不是malloc一个人能说了算的。 malloc有多种实现不同的实现有不同的特点。一般情况下malloc是从系统获取内存分页然后将这些分页组织为不同大小的“块”。当用户程序申请内存的时候如果大小没有超过“块”的大小则malloc在内部匹配尺寸最为接近的不小于申请的尺寸的块记录分配信息之后返回地址。这样做的目的是加快分配的速度减少系统调用。 如果申请的空间很大则malloc会直接向系统申请多个分页将其map到用户程序的虚拟地址空间当中的一个连续的区间这个同样是系统调用然后返回地址。 分页的管理是由操作系统完成的。现代操作系统大多支持虚拟内存也就是不仅仅使用RAM还使用磁盘上的交换文件作为内存空间。操作系统将这些空间按照一定大小切分成页在一个被称为是页表的结构当中进行维护。操作系统级别的内存管理都是以页为单位的。 页的实际存储位置由操作系统管理。当多个应用程序同时执行时操作系统会将不活动的页从RAM当中移动到磁盘文件当中。当应用程序重新尝试访问该页的时候再从磁盘文件读入到RAM当中这个称为页交换。 也是因为页的这个特性操作系统完全可以分配实际上不存在的页给malloc直到该页被真正使用。所以说理论上malloc 1TB可能都没有问题虽然RAM和磁盘上的交换文件或者交换分区加起来往往也就是几十个GB。这就是因为操作系统实际上并没有分配完整的空间给这些页只是在页表当中记了一笔A程序需要XXXX页并留出相应的行在其中记录“未确保空间”。 当应用程序真的开始尝试对malloc返回的地址进行读写的时候如果操作系统检测到页其实还没有分配实际的空间这个时候才会去寻找合适的空间填写进页表。如果找不到合适的空间那么就会触发页错误overcommit错误。 在Linux当中我们是可以关闭overcommit功能要求操作系统每次分配分页的时候都确保分页可用那么malloc可分配的最大大小就受实际RAM交换文件/分区大小的限制。在Linux当中为保障操作系统自身的运行通常这个大小是RAM的一半交换区的大小。当然可改变设置。 另外单次可分配的大小和多次合计可分配的大小也不是同一个概念因为多次分配存在内存碎片化的问题。就好像一个饭店虽然有40个座位但是也许只需要几个人你就没办法找到一张8人桌一样。 所以在当代操作系统当中malloc可以分配的空间的最大大小首先当然受机器的bit数也就是可以直接寻址的空间大小制约在这个基础上进一步受操作系统的策略制约然后受实际可用资源量的制约再者还受malloc本身算法的制约最后还受应用程序申请方式和次数的制约。 6.33 public、protected、private的区别 第一: private,public,protected的访问范围 private: 只能由该类中的函数、其友元函数访问不能被任何其他访问该类的对象也不能访问protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问public: 可以被该类中的函数、子类的函数、其友元函数访问也可以由该类的对象访问 第二:类的继承后方法属性变化: 使用private继承父类的所有方法在子类中变为private;使用protected继承父类的protected和public方法在子类中变为protected而private方法不变;使用public继承父类中的方法属性不发生改变; 7 git 分布式版本控制系统 7.1 什么是git Git 是一个开源的分布式版本控制系统用于快速高效地处理任何或小或大的项目。最初是为了帮助管理Linux内核开发的一个开放源码的版本控制软件。 Git应用十分广泛小到我们平时使用github网站大到公司中多人合作的大型项目开发。 它速度快完全分布式允许成千上万个并行开发的分支容灾性能强。 git分为哪几个区分别是什么 ①工作区(Workspace)是电脑中实际的目录。 ②暂存区(Index)类似于缓存区域临时保存你的改动。 ③仓库区(Repository)分为本地仓库和远程仓库。 通常提交代码分为几步 ① git add 从工作区提交到暂存区 ② git commit 从暂存区提交到本地仓库③ git push 或 git svn dcommit 从本地仓库提交到远程仓库 7.2 为什么要用git 让你回答为什么用git其实就是让你说之前的版本控制系统为什么不好。 最开始的版本控制方法一般都是采用 人工手动控制。 缺点 千人千面不同版本命名随意 有时无法快速辨别版本新旧 无法快速知道版本1相比版本2自己改了什么 如果要是多人开发那命名和版本控制更为可怕 7.3 简述集中式版本控制库和分布式版本控制库的区别 备份 集中式存在单点故障一旦故障没法恢复所以备份极其重要分布式则更安全出现故障可以恢复数据每一个节点都是一个服务器。 服务器压力 集中式所有的操作都要与服务器交互操作首先于带宽不能移动办公分布式全是离线操作不受限于带宽可以移动办公。 安全性 集中式容易出现单点故障遭受黑客攻击分布式数据、提交全部使用SHA1哈希以保证数据完整性甚至提交可以使用PGP签名。 工作模式 集中式合适人数不多的项目集中管理分布式适合很多人的项目。 8 RAII机制 8.1 mmap基本原理和分类 在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间创建和物理内存的映射关系。 映射关系可以分为两种 1、文件映射 磁盘文件映射进程的虚拟地址空间使用文件内容初始化物理内存。 2、匿名映射 初始化全为0的内存空间。 而对于映射关系是否共享又分为 1、私有映射(MAP_PRIVATE) 多进程间数据共享修改不反应到磁盘实际文件是一个copy-on-write写时复制的映射方式。 2、共享映射(MAP_SHARED) 多进程间数据共享修改反应到磁盘实际文件中。 因此总结起来有4种组合 1、私有文件映射 多个进程使用同样的物理内存页进行初始化但是各个进程对内存文件的修改不会共享也不会反应到物理文件中 2、私有匿名映射 mmap会创建一个新的映射各个进程不共享这种使用主要用于分配内存(malloc分配大内存会调用mmap)。 例如开辟新进程时会为每个进程分配虚拟的地址空间这些虚拟地址映射的物理内存空间各个进程间读的时候共享写的时候会copy-on-write。 3、共享文件映射 多个进程通过虚拟内存技术共享同样的物理内存空间对内存文件 的修改会反应到实际物理文件中他也是进程间通信(IPC)的一种机制。 4、共享匿名映射 这种机制在进行fork的时候不会采用写时复制父子进程完全共享同样的物理内存页这也就实现了父子进程通信(IPC). 这里值得注意的是mmap只是在虚拟内存分配了地址空间只有在第一次访问虚拟内存的时候才分配物理内存。 在mmap之后并没有在将文件内容加载到物理页上只上在虚拟内存中分配了地址空间。当进程在访问这段地址时通过查找页表发现虚拟内存对应的页没有在物理内存中缓存则产生缺页由内核的缺页异常处理程序处理将文件对应内容以页为单位(4096)加载到物理内存注意是只加载缺页但也会受操作系统一些调度策略影响加载的比所需的多。 8.2 RAII机制介绍 RAII全程为Resource Acquisition Is Initialization资源获取即初始化RAII是C语法体系中的一种常用的合理管理资源避免出现内存泄漏的常用方法。以对象管理资源利用的就是C构造的对象最终会被对象的析构函数销毁的原则。RAII的做法是使用一个对象在其构造时获取对应的资源在对象生命期内控制对资源的访问使之始终保持有效最后在对象析构的时候释放构造时获取的资源。 8.3 使用RAII机制的原因 RAII是合理管理资源避免出现内存泄漏的常用方法。那么所谓的资源是如何进行定义的呢在计算机系统中资源是数量有限且对系统正常运行具有一定作用的元素。比如网络套接字、互斥锁、文件句柄和内存等等它们属于系统资源。由于系统的资源是有限的就好比自然界的石油铁矿一样不是取之不尽用之不竭的所以我们在编程使用系统资源时都必须遵循一个步骤 申请资源→使用资源→释放资源 如果在申请和使用资源后未进行资源的释放此时就造成了资源的泄漏。资源使用后必须要将其释放。 简单举例 #include iostream using namespace std; int main() { int *arr new int [10]; // Here, you can use the array delete [] arr; arr nullptr ; return 0; } 上述的的申请、使用、释放资源的程序较为简单但是如果程序很复杂的时候需要为所有的new 分配的内存delete掉导致极度臃肿效率下降更可怕的是程序的可理解性和可维护性明显降低了当操作增多时处理资源释放的代码就会越来越多越来越乱。如果某一个操作发生了异常而导致释放资源的语句没有被调用怎么办这个时候RAII机制就可以派上用场了。 使用RAII机制的优点 不需要显式地释放资源。采用这种方式对象所需的资源只在其生命期内始终保持有效。 8.4 RAII机制的使用方法 由于系统的资源不具有自动释放的功能而C中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来对资源操作都封装在类的内部在析构函数中进行释放资源。当定义的局部变量的生命结束时它的析构函数就会自动的被调用如此就不用程序员显示的去调用释放资源的操作了。 使用RAII机制的代码示例 #include iostream #include windows.h #include process.husing namespace std;CRITICAL_SECTION cs; int gGlobal 0;class MyLock { public:MyLock(){EnterCriticalSection(cs);}~MyLock(){LeaveCriticalSection(cs);}private:MyLock( const MyLock );MyLock operator (const MyLock ); };void DoComplex(MyLock lock ) { }unsigned int __stdcall ThreadFun(PVOID pv) {MyLock lock;int *para (int *) pv;// I need the lock to do some complex thingDoComplex(lock);for (int i 0; i 10; i){gGlobal;cout Thread *paraendl;coutgGlobalendl;}return 0; }int main() {InitializeCriticalSection(cs);int thread1, thread2;thread1 1;thread2 2;HANDLE handle[2];handle[0] ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)thread1, 0, NULL );handle[1] ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)thread2, 0, NULL );WaitForMultipleObjects(2, handle, TRUE , INFINITE );return 0; } 这个例子可以说是实际项目的一个模型当多个进程访问临界变量时为了不出现错误的情况需要对临界变量进行加锁上面的例子就是使用的Windows的临界区域实现的加锁。但是在使用CRITICAL_SECTION时EnterCriticalSection和LeaveCriticalSection必须成对使用很多时候经常会忘了调用LeaveCriticalSection此时就会发生死锁的现象。当我将对CRITICAL_SECTION的访问封装到MyLock类中时之后我只需要定义一个MyLock变量而不必手动的去显示调用LeaveCriticalSection函数。 RAII总结 RAII机制保证了异常安全并且也为程序员在编写动态分配内存的程序时提供了安全保证。但它也存在一些缺点有些操作可能会抛出异常如果放在析构函数中进行则不能将错误传递出去那么此时析构函数就必须自己处理异常。这在某些时候是很繁琐的。
http://www.hkea.cn/news/14348100/

相关文章:

  • 网站设计细节北京aso优化
  • 安徽网站优化中国最近的好消息
  • 哪个网站教做ppt公众号开发实践 pdf
  • 做电影网站会被捉吗网站上做旅游卖家要学什么软件
  • 有哪些做壁纸的网站学校html网站模板
  • 株洲网站建设工作室东营百度推广电话
  • 网站建设合同注意点株洲网站制作
  • 盛成广告传媒做网站的网站定制开发 广州
  • 凡科轻站在网站后台设置wap模板目录
  • 网站拍照的幕布周口网站制作公司哪家好
  • 网站建设补贴是经信局的政策吗wordpress+爱情主题公园
  • 常州网站开发公司推荐wordpress 创建文章
  • 校园网站建设资源wordpress本地做好如何改站点地址
  • 网站备案 更换接入商晋中建设网站
  • 2小时wordpress建站wordpress 云备份
  • 工业软件开发技术优化设计五年级下册语文答案2021
  • 网站建设的过程有哪些购物网站首页界面设计
  • 网站网络营销方案安徽网站建设有限公司
  • 城乡建设部注册建筑师网站博山区住房和城乡建设局网站
  • 做网站一个月30ip青岛做网站哪里好
  • 黑河商城网站建设怎么联系小程序开发者
  • 南宁网站建设liluokj青岛商业网站建设
  • 门户网站做公众号的好处郑州做定制网站的公司哪家好
  • 深圳市南山网站建设泗门网站建设
  • 企业网站建设电话安装百度到手机桌面
  • 建设集团网站报告书wordpress 搜索框位置
  • 网站开发维护岗位职责设计公司职位
  • 农村电商扶贫网站建设邯郸装修网站建设
  • 电子商务网站开发 什么框架北京网站优化策略
  • html5导航网站源码网站后台密码怎么改