C++突击面试
- 1. 编译内存相关
- 2. 语言对比
- 3. 面向对象
- 4. 关键字库函数
- 4.1 sizeof 和 strlen 的区别
- 4.2 lambda 表达式(匿名函数)的具体应用和使用场景
- 4.3 explicit 的作用(如何避免编译器进行隐式类型转换)
- 4.4 C 和 C++ static 的区别
- 4.5 static 的作用
- 4.6 static 在类中使用的注意事项(定义、初始化和使用)
- 4.7 static 全局变量和普通全局变量的异同
- 4.8 const 作用及用法
- 4.9 define 和 const 的区别
- 4.10 define 和 typedef 的区别
- 4.11 用宏实现比较大小,以及两个数中的最小值
- 4.12 inline 作用及使用方法
- 4.13 inline 函数工作原理
- 4.14 宏定义(define)和内联函数(inline)的区别
- 4.15 new 的作用?
- 4.16 new 和 malloc 如何判断是否申请到内存?
- 4.17 delete 实现原理?delete 和 delete[] 的区别?
- 4.18 new 和 malloc 的区别,delete 和 free 的区别
- 4.19 malloc 的原理?malloc 的底层实现?
- 4.20 C 和 C++ struct 的区别?
- 4.21 为什么有了 class 还保留 struct?
- 4.22 struct 和 union 的区别
- 4.23 class 和 struct 的异同
- 4.24 volatile 的作用?是否具有原子性,对编译器有什么影响?
- 4.25 什么情况下一定要用 volatile, 能否和 const 一起使用?
- 4.26 返回函数中静态变量的地址会发生什么?
- 4.27 extern C 的作用?
- 4.28 sizeof(1==1) 在 C 和 C++ 中分别是什么结果?
- 4.29 memcpy 函数的底层原理?
- 4.30 strcpy 函数有什么缺陷?
- 4.31 auto 类型推导的原理
- 5. 类相关
- 6. 语言特性相关
- 6.1 左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?
- 6.2 std::move() 函数的实现原理
- 6.3 什么是指针?指针的大小及用法?
- 6.5 C++ 11 nullptr 比 NULL 优势
- 6.6 指针和引用的区别?
- 6.7 常量指针和指针常量的区别
- 6.8 函数指针和指针函数的区别
- 6.9 强制类型转换有哪几种?
- 6.10 如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?
- 6.11 参数传递时,值传递、引用传递、指针传递的区别?
- 6.12 什么是模板?如何实现?
- 6.13 函数模板和类模板的区别?
- 6.14 什么是可变参数模板?
- 6.15 什么是模板特化?为什么特化?
- 6.16 include " " 和 <> 的区别
- 6.17 泛型编程如何实现?
- 7. 设计模式
1. 编译内存相关
1.1. C++ 程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
-
编译预处理:处理以 # 开头的指令;
-
编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
-
汇编:将汇编代码 .s 翻译成机器指令 .o 文件;
-
链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。
链接分为两种: -
静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
-
动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
二者的优缺点:
-
静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
-
动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
1.2. C++ 内存管理
C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
- 栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
- 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
- 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在.bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
- 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
- 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
1.3. 栈和堆的区别
- 申请方式:栈是系统自动分配,堆是程序员主动申请。
- 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
- 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
- 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
- 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
1.4. 变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别:
C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
从作用域看:
- 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
- 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
- 局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
- 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:
- 静态存储区:全局变量,静态局部变量,静态全局变量。
- 栈:局部变量。
1.5. 全局变量定义在头文件中有什么问题?
如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。
1.6. 内存对齐
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
- 结构体每个成员相对于结构体首地址的偏移量 (offset)
都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding); - 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
进行内存对齐的原因:(主要是硬件设备方面的问题)
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap);
- 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
内存对齐的优点:
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
1.7. 什么是内存泄露
内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释:
- 并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。
- 常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。
- 使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete释放内存,否则这块内存就会造成内存泄漏。
- 指针重新赋值
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;
- 1
- 2
- 3
开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。
1.8. 怎么防止内存泄漏?内存泄漏检测工具的原理?
防止内存泄漏的方法:
- 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况)
- 智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。
内存泄漏检测工具的实现原理:
内存检测工具有很多,这里重点介绍下 valgrind 。
1.9. 智能指针有哪几种?智能指针的实现原理?
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 < memory > 头文件中。
C++11 中智能指针包括以下三种:
-
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
-
独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
-
弱指针(weak_ptr):指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。
智能指针的实现原理: 计数原理。
#include <iostream>
#include <memory>
template <typename T>
class SmartPtr
{
private :
T _ptr;
size_t _count;
public:
SmartPtr(T *ptr = nullptr) : _ptr(ptr)
{
if (_ptr)
{
_count = new size_t(1);
}
else
{
_count = new size_t(0);
}
}
<span class="token operator">~</span><span class="token function">SmartPtr</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">)</span><span class="token operator">--</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>_count <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token operator">-></span>_ptr<span class="token punctuation">;</span>
<span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token function">SmartPtr</span><span class="token punctuation">(</span><span class="token keyword">const</span> SmartPtr <span class="token operator">&</span>ptr<span class="token punctuation">)</span> <span class="token comment">// 拷贝构造:计数 +1</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span> <span class="token operator">!=</span> <span class="token operator">&</span>ptr<span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">this</span><span class="token operator">-></span>_ptr <span class="token operator">=</span> ptr<span class="token punctuation">.</span>_ptr<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token operator">-></span>_count <span class="token operator">=</span> ptr<span class="token punctuation">.</span>_count<span class="token punctuation">;</span>
<span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">)</span><span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
SmartPtr <span class="token operator">&</span><span class="token keyword">operator</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token keyword">const</span> SmartPtr <span class="token operator">&</span>ptr<span class="token punctuation">)</span> <span class="token comment">// 赋值运算符重载 </span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_ptr <span class="token operator">==</span> ptr<span class="token punctuation">.</span>_ptr<span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> <span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_ptr<span class="token punctuation">)</span> <span class="token comment">// 将当前的 ptr 指向的原来的空间的计数 -1</span>
<span class="token punctuation">{<!-- --></span>
<span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">)</span><span class="token operator">--</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_count <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token operator">-></span>_ptr<span class="token punctuation">;</span>
<span class="token keyword">delete</span> <span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">this</span><span class="token operator">-></span>_ptr <span class="token operator">=</span> ptr<span class="token punctuation">.</span>_ptr<span class="token punctuation">;</span>
<span class="token keyword">this</span><span class="token operator">-></span>_count <span class="token operator">=</span> ptr<span class="token punctuation">.</span>_count<span class="token punctuation">;</span>
<span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>_count<span class="token punctuation">)</span><span class="token operator">++</span><span class="token punctuation">;</span> <span class="token comment">// 此时 ptr 指向了新赋值的空间,该空间的计数 +1</span>
<span class="token keyword">return</span> <span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
T <span class="token operator">&</span><span class="token keyword">operator</span><span class="token operator">*</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token function">assert</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_ptr <span class="token operator">==</span> <span class="token keyword">nullptr</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token operator">*</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_ptr<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
T <span class="token operator">*</span><span class="token keyword">operator</span><span class="token operator">-></span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token function">assert</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>_ptr <span class="token operator">==</span> <span class="token keyword">nullptr</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">this</span><span class="token operator">-></span>_ptr<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
size_t <span class="token function">use_count</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> <span class="token operator">*</span><span class="token keyword">this</span><span class="token operator">-></span>count<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 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
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
1.10. 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?
借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。
// A 作为一个类
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
- 1
- 2
- 3
1.11. 使用智能指针会出现什么问题?怎么解决?
智能指针可能出现的问题:循环引用
在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
shared_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
<span class="token keyword">void</span> <span class="token function">doSomething</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>ChildPtr<span class="token punctuation">.</span><span class="token function">use_count</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token operator">~</span><span class="token function">Parent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token operator">~</span><span class="token function">Child</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 2
}
cout << wpp.use_count() << endl; // 1
cout << wpc.use_count() << endl; // 1
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
循环引用的解决方法: 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 同样被销毁。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
//shared_ptr<Child> ChildPtr;
weak_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
<span class="token keyword">void</span> <span class="token function">doSomething</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token comment">//new shared_ptr</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">-></span>ChildPtr<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token operator">~</span><span class="token function">Parent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token operator">~</span><span class="token function">Child</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
<span class="token punctuation">}</span>
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 1
}
cout << wpp.use_count() << endl; // 0
cout << wpc.use_count() << endl; // 0
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 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
2. 语言对比
2.1 C++ 11 新特性
1. auto 类型推导
auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。
auto 关键字基本的使用语法如下:
2. decltype 类型推导
decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
区别:
auto var = val1 + val2;
decltype(val1 + val2) var1 = 0;
- 1
- 2
- auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。
- auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。
3. lambda 表达式
lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。
lambda匿名函数的定义:
[capture list] (parameter list) -> return type
{
function body;
};
- 1
- 2
- 3
- 4
其中:
- 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, arr+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : arr){
cout << n << " ";
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
4. 范围 for 语句
for (declaration : expression){
statement
}
- 1
- 2
- 3
参数的含义:
- expression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector ,string等,这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。
- declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。
5. 右值引用
右值引用:绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。为了和右值引用区分开,常规的引用称为左值引用。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int var = 42;
int &l_var = var;
int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上
<span class="token keyword">int</span> <span class="token operator">&&</span>r_var2 <span class="token operator">=</span> var <span class="token operator">+</span> <span class="token number">40</span><span class="token punctuation">;</span> <span class="token comment">// 正确:将 r_var2 绑定到求和结果上</span>
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
2.2 C 和 C++ 的区别
首先说一下面向对象和面向过程:
- 面向过程的思路:分析解决问题所需的步骤,用函数把这些步骤依次实现。
- 面向对象的思路:把构成问题的事务分解为各个对象,建立对象的目的,不是完成一个步骤,而是描述某个事务在解决整个问题步骤中的行为。
区别和联系:
- 语言自身:C 语言是面向过程的编程,它最重要的特点是函数,通过 main
函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。C++ 是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主 main 函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了(实际上过程还是有的,就是主函数的那些语句。),以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理。 - 应用领域:C 语言主要用于嵌入式领域,驱动开发等与硬件直接打交道的领域,C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域。
- C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。
- C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。
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++ 定义声明才能使用,而且极其灵活,完全面向更上层的开发者。
3. 面向对象
3.1 什么是面向对象?面向对象的三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
- 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
- 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
- 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。
3.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 错误:注意重载不关心函数返回类型
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
#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 provided
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
说明:上述代码中 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) : 3
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
重写和重载的区别:
- 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
- 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
- virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。
隐藏和重写,重载的区别:
- 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
-
参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual
修饰,基类函数都是被隐藏,而不是重写。
3.3 如何理解 C++ 是面向对象编程
说明:该问题最好结合自己的项目经历进行展开解释,或举一些恰当的例子,同时对比下面向过程编程。
- 面向过程编程:一种以执行程序操作的过程或函数为中心编写软件的方法。程序的数据通常存储在变量中,与这些过程是分开的。所以必须将变量传递给需要使用它们的函数。缺点:随着程序变得越来越复杂,程序数据与运行代码的分离可能会导致问题。例如,程序的规范经常会发生变化,从而需要更改数据的格式或数据结构的设计。当数据结构发生变化时,对数据进行操作的代码也必须更改为接受新的格式。查找需要更改的所有代码会为程序员带来额外的工作,并增加了使代码出现错误的机会。
- 面向对象编程(Object-Oriented Programming, OOP):以创建和使用对象为中心。一个对象(Object)就是一个软件实体,它将数据和程序在一个单元中组合起来。对象的数据项,也称为其属性,存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。
面向对象编程进一步说明:
面向对象编程将数据成员和成员函数封装到一个类中,并声明数据成员和成员函数的访问级别(public、private、protected),以便控制类对象对数据成员和函数的访问,对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名、参数列表以及返回值类型即可,无需了解其函数的实现原理。当类内部的数据成员或者成员函数发生改变时,不影响类外部的代码。
3.4 什么是多态?多态如何实现?
多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
实现方法:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。
实现过程:
- 在类中用 virtual 关键字声明的函数叫做虚函数;
- 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
- 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl; }
<span class="token keyword">virtual</span> <span class="token keyword">void</span> <span class="token function">fun1</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> cout <span class="token operator"><<</span> <span class="token string">"Base::fun1()"</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">virtual</span> <span class="token keyword">void</span> <span class="token function">fun2</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> cout <span class="token operator"><<</span> <span class="token string">"Base::fun2()"</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()" << endl; }
<span class="token keyword">virtual</span> <span class="token keyword">void</span> <span class="token function">D_fun1</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> cout <span class="token operator"><<</span> <span class="token string">"Derive::D_fun1()"</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">virtual</span> <span class="token keyword">void</span> <span class="token function">D_fun2</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> cout <span class="token operator"><<</span> <span class="token string">"Derive::D_fun2()"</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token punctuation">}</span>
};
int main()
{
Base *p = new Derive();
p->fun(); // Derive::fun() 调用派生类中的虚函数
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
基类的虚函数表如下:
派生类的对象虚函数表如下:
简单解释:当基类的指针指向派生类的对象时,通过派生类的对象的虚表指针找到虚函数表(派生类的对象虚函数表),进而找到相应的虚函数 Derive::f() 进行调用。
4. 关键字库函数
4.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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char arr[10] = "hello";
cout << strlen(arr) << endl; // 5
cout << sizeof(arr) << endl; // 10
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。
#include <iostream>
#include <cstring>
using 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
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
-
strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;
-
sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。
4.2 lambda 表达式(匿名函数)的具体应用和使用场景
lambda 表达式的定义形式如下:
[capture list] (parameter list) -> reurn type
{
function body
}
- 1
- 2
- 3
- 4
其中:
- capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空,但如果函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。
- return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
举例:
lambda 表达式常搭配排序算法使用。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> arr = {3, 4, 76, 12, 54, 90, 34};
sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序
for (auto a : arr)
{
cout << a << " ";
}
return 0;
}
/*
运行结果:90 76 54 34 12 4 3
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
4.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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
上述代码中,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' requested
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
4.4 C 和 C++ static 的区别
- 在 C 语言中,使用 static 可以定义局部静态变量、外部静态变量、静态函数
- 在 C++ 中,使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。
4.5 static 的作用
作用:
static 定义静态变量,静态函数。
- 保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。
#include <iostream>
using namespace std;
int fun(){
static int var = 1; // var 只在第一次进入这个函数的时初始化
var += 1;
return var;
}
int main()
{
for(int i = 0; i < 10; ++i)
cout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 隐藏:static作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)
- static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。
#include<iostream>
using namespace std;
class A
{
private:
int var;
static int s_var; // 静态成员变量
public:
void show()
{
cout << s_var++ << endl;
}
static void s_show()
{
cout << s_var << endl;
// cout << var << endl; // error: invalid use of member 'A::a' in static member function. 静态成员函数不能调用非静态成员变量。无法使用 this.var
// show(); // error: cannot call member function 'void A::show()' without object. 静态成员函数不能调用非静态成员函数。无法使用 this.show()
}
};
int A::s_var = 1; // 静态成员变量在类外进行初始化赋值,默认初始化为 0
int main()
{
<span class="token comment">// cout << A::sa << endl; // error: 'int A::sa' is private within this context</span>
A ex<span class="token punctuation">;</span>
ex<span class="token punctuation">.</span><span class="token function">show</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">A</span><span class="token operator">::</span><span class="token function">s_show</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
4.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'
};
int main()
{
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。
#include <iostream>
using namespace std;
class A
{
public:
static A s_var; // 正确,静态数据成员
A var; // error: field 'var' has incomplete type 'A'
A *p; // 正确,指针
A &var1; // 正确,引用
};
int main()
{
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
static 静态成员函数:
- 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
- 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。
4.7 static 全局变量和普通全局变量的异同
相同点:
- 存储方式:普通全局变量和 static 全局变量都是静态存储方式。
不同点:
- 作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的;静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
- 初始化:静态全局变量只初始化一次,防止在其他文件中使用。
4.8 const 作用及用法
作用:
- const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
- const 修饰函数参数,使得传递过来的函数参数的值不能改变。
- const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
在类中的用法:
const 成员变量:
- const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。
- const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。
const 成员函数:
- 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
- 不能调用非常量成员函数,以防修改成员变量的值。
4.9 define 和 const 的区别
区别:
- 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
- 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
- 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。
- 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;cons定义的常量可以进行调试。
const 的优点:
- 有数据类型,在定义式可进行安全性检查。
- 可调式。
- 占用较少的空间。
4.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: int
INTPTR2 p3, p4; // p3: int *; p4: int *
<span class="token keyword">int</span> var <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> INTPTR1 p5 <span class="token operator">=</span> <span class="token operator">&</span>var<span class="token punctuation">;</span> <span class="token comment">// 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。</span>
<span class="token keyword">const</span> INTPTR2 p6 <span class="token operator">=</span> <span class="token operator">&</span>var<span class="token punctuation">;</span> <span class="token comment">// 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。</span>
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
4.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
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
4.12 inline 作用及使用方法
作用:
inline 是一个关键字,可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。
使用方法:
- 类内定义成员函数默认是内联函数
在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:
#include <iostream>
using namespace std;
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun(){
cout << var << endl;
}
};
int main()
{
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 类外定义成员函数,若想定义为内联函数,需用关键字声明
当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 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 << endl;
}
int main()
{
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
另外,可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。
4.13 inline 函数工作原理
- 内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。
- 普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。
4.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
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
4.15 new 的作用?
new 是 C++ 中的关键字,用来动态分配内存空间,实现方式如下:
int *p = new int[5];
- 1
4.16 new 和 malloc 如何判断是否申请到内存?
- malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
- new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bac_alloc 异常。
4.17 delete 实现原理?delete 和 delete[] 的区别?
delete 的实现原理:
- 首先执行该对象所属类的析构函数;
- 进而通过调用 operator delete 的标准库函数来释放所占的内存空间。
delete 和 delete [] 的区别:
- delete 用来释放单个对象所占的空间,只会调用一次析构函数;
- delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。
4.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 函数从堆上动态分配内存。(*存储区不等于堆)
4.19 malloc 的原理?malloc 的底层实现?
malloc 的原理:
- 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
- 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
malloc 的底层实现:
-
brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
-
mmap 内存映射原理:
1.进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
2.调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
3.进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
4.20 C 和 C++ struct 的区别?
- 在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。
- C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。
- C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;
4.21 为什么有了 class 还保留 struct?
- C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。
4.22 struct 和 union 的区别
说明:union 是联合体,struct 是结构体。
区别:
- 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
- 对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。
- 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。
#include <iostream>
using namespace std;
typedef union
{
char c[10];
char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;
typedef union
{
char c[10];
int i; // int 4 字节,按该类型的倍数分配大小
} u22;
typedef union
{
char c[10];
double d; // double 8 字节,按该类型的倍数分配大小
} u33;
typedef struct s1
{
char c; // 1 字节
double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
} s11;
typedef struct s2
{
char c; // 1 字节
char cc; // 1(char)+ 1(char)= 2 字节
double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;
typedef struct s3
{
char c; // 1 字节
double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
char cc; // 16 + 1(char)+ 7(内存对齐)= 24 字节
} s33;
int main()
{
cout << sizeof(u11) << endl; // 10
cout << sizeof(u22) << endl; // 12
cout << sizeof(u33) << endl; // 16
cout << sizeof(s11) << endl; // 16
cout << sizeof(s22) << endl; // 16
cout << sizeof(s33) << endl; // 24
cout <span class="token operator"><<</span> <span class="token keyword">sizeof</span><span class="token punctuation">(</span><span class="token keyword">int</span><span class="token punctuation">)</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token comment">// 4</span>
cout <span class="token operator"><<</span> <span class="token keyword">sizeof</span><span class="token punctuation">(</span><span class="token keyword">double</span><span class="token punctuation">)</span> <span class="token operator"><<</span> endl<span class="token punctuation">;</span> <span class="token comment">// 8</span>
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
4.23 class 和 struct 的异同
- struct 和 class 都可以自定义数据类型,也支持继承操作。
- struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。
- 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身,class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。
struct A{};
class B : A{}; // private 继承
struct C : B{}; // public 继承
- 1
- 2
- 3
举例:
#include<iostream>
using namespace std;
class A{
public:
void funA(){
cout << "class A" << endl;
}
};
struct B: A{ // 由于 B 是 struct,A 的默认继承级别为 public
public:
void funB(){
cout << "class B" << endl;
}
};
class C: B{ // 由于 C 是 class,B 的默认继承级别为 private,所以无法访问基类 B 中的 printB 函数
};
int main(){
A ex1;
ex1.funA(); // class A
B ex2<span class="token punctuation">;</span>
ex2<span class="token punctuation">.</span><span class="token function">funA</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// class A</span>
ex2<span class="token punctuation">.</span><span class="token function">funB</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// class B</span>
C ex3<span class="token punctuation">;</span>
ex3<span class="token punctuation">.</span><span class="token function">funB</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// error: 'B' is not an accessible base of 'C'.</span>
<span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- class 可以用于定义模板参数,struct 不能用于定义模板参数。
4.24 volatile 的作用?是否具有原子性,对编译器有什么影响?
volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,告知编译器不应对这样的对象进行优化。
volatile不具有原子性。
volatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。
4.25 什么情况下一定要用 volatile, 能否和 const 一起使用?
使用 volatile 关键字的场景:
- 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;
- 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。
volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。
4.26 返回函数中静态变量的地址会发生什么?
#include <iostream>
using namespace std;
int fun(int tmp){
static int var = 10;
var = tmp;
return &var;
}
int main() {
cout << *fun(5) << endl;
return 0;
}
/*
运行结果:
50
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
说明:上述代码中在函数 fun 中定义了静态局部变量 var,使得离开该函数的作用域后,该变量不会销毁,返回到主函数中,该变量依然存在,从而使程序得到正确的运行结果。但是,该静态局部变量直到程序运行结束后才销毁,浪费内存空间。
4.27 extern C 的作用?
当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern “C” 指出任意非 C++ 函数所用的语言。
举例:
// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
int strcmp(const char*, const char*);
}
- 1
- 2
- 3
- 4
4.28 sizeof(1==1) 在 C 和 C++ 中分别是什么结果?
C 语言代码:
#include<stdio.h>
void main(){
printf("%d\n", sizeof(1==1));
}
/*
运行结果:
4
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
C++ 代码:
#include <iostream>
using namespace std;
int main() {
cout << sizeof(1==1) << endl;
return 0;
}
/*
1
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4.29 memcpy 函数的底层原理?
void *memcpy(void *dst, const void *src, size_t size)
{
char *psrc;
char *pdst;
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">NULL</span> <span class="token operator">==</span> dst <span class="token operator">||</span> <span class="token constant">NULL</span> <span class="token operator">==</span> src<span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token keyword">return</span> <span class="token constant">NULL</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>src <span class="token operator"><</span> dst<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>src <span class="token operator">+</span> size <span class="token operator">></span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>dst<span class="token punctuation">)</span> <span class="token comment">// 出现地址重叠的情况,自后向前拷贝</span>
<span class="token punctuation">{<!-- --></span>
psrc <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>src <span class="token operator">+</span> size <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span>
pdst <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>dst <span class="token operator">+</span> size <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span>size<span class="token operator">--</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token operator">*</span>pdst<span class="token operator">--</span> <span class="token operator">=</span> <span class="token operator">*</span>psrc<span class="token operator">--</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">else</span>
<span class="token punctuation">{<!-- --></span>
psrc <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>src<span class="token punctuation">;</span>
pdst <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">char</span> <span class="token operator">*</span><span class="token punctuation">)</span>dst<span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span>size<span class="token operator">--</span><span class="token punctuation">)</span>
<span class="token punctuation">{<!-- --></span>
<span class="token operator">*</span>pdst<span class="token operator">++</span> <span class="token operator">=</span> <span class="token operator">*</span>psrc<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> dst<span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
4.30 strcpy 函数有什么缺陷?
strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
int var = 0x11112222;
char arr[10];
cout << "Address : var " << &var << endl;
cout << "Address : arr " << &arr << endl;
strcpy(arr, "hello world!");
cout << "var:" << hex << var << endl; // 将变量 var 以 16 进制输出
cout << "arr:" << arr << endl;
return 0;
}
/*
Address : var 0x23fe4c
Address : arr 0x23fe42
var:11002164
arr:hello world!
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
说明:从上述代码中可以看出,变量 var 的后六位被字符串 “hello world!” 的 “d!\0” 这三个字符改变,这三个字符对应的 ascii 码的十六进制为:\0(0x00),!(0x21),d(0x64)。
原因:变量 arr 只分配的 10 个内存空间,通过上述程序中的地址可以看出 arr 和 var 在内存中是连续存放的,但是在调用 strcpy 函数进行拷贝时,源字符串 “hello world!” 所占的内存空间为 13,因此在拷贝的过程中会占用 var 的内存空间,导致 var的后六位被覆盖。
4.31 auto 类型推导的原理
auto 类型推导的原理:
编译器根据初始值来推算变量的类型,要求用 auto 定义变量时必须有初始值。编译器推断出来的 auto 类型有时和初始值类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则。
5. 类相关
5.1 什么是虚函数?什么是纯虚函数?
虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。
#include <iostream>
using namespace std;
class A
{
public:
virtual void v_fun() // 虚函数
{
cout << "A::v_fun()" << endl;
}
};
class B : public A
{
public:
void v_fun()
{
cout << "B::v_fun()" << endl;
}
};
int main()
{
A *p = new B();
p->v_fun(); // B::v_fun()
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
纯虚函数:
- 纯虚函数在类中声明时,加上 =0;
- 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
- 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
说明:
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
- 可以声明抽象类指针,可以声明抽象类的引用;
- 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
5.2 虚函数和纯虚函数的区别?
- 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)
- 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
- 定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;
- 虚函数必须实现,否则编译器会报错;
- 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
- 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。
5.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: