一 、语言基础
1.1 指针
野指针:指针指向的位置是不可知的
悬空指针:指针最初指向的内存已经被释放了的一种指针
两种指针都指向无效内存空间, 即不安全不可控 。需要在定义指针后且在使用之前完成初始化或者使用
智能指针来避免
智能指针的 作用 是管理指针, 避免使用普通指针申请的空间在函数结束时忘记释放, 造成内存泄漏。
因为智能指针是一个类, 当超出类的作用域时, 类会自动调用析构函数, 析构函数有释放资源的操作。
类型:
● auto_ptr 采用所有权模式( C11已弃用) , 使得一个该类型指针可以剥夺另一个该类型指针的所有 权, 使得被剥夺所有权的指针失效, 缺点是使用被剥夺的指针存在潜在的内存崩溃问题。
● unique_ptr 实现独占式拥有, 保证同一 时间内只有一个智能指针可以指向该对象, 避免上述内存崩 溃的出现 。只能通过 new 来创建。
● shared_ptr 实现共享式拥有, 可以用 new 来构造, 还可以引入其他智能指针来构造 。多个智能指 针可以指向相同的对象, 该对象和其相关资源会在最后一个引用 ( use_count() 查看引用数) 被销 毁时释放 。当调用 release() 时, 当前指针会释放资源所有权, 计数减一 。当计数等于0 时, 资源 会被释放 。资源消耗大, 因为创建时会调用两次new( 其中一次是引用计数对象)
● weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象 。进行该对 象内存管理的是 shared_ptr ,weak_ptr 只是提供了对管理对象的一个访问方法, 目的是为了协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 且不会引起引用计 数值的变化 。主要用来解决 空悬指针 和 循环引用 问题 。空悬指针是两个共享指针同时引用同一个 对象, 但是其中一个指针将该对象销毁, 另一个指针会指向为空, 可通过使用 weak_ptr 来判断指 向对象是否有效;循环引用是指两个对象之间相互引用, 则引用计数将无法减为0, 而其中一 方改 为 weak_ptr 则可检测是否有效, 且能将有效的指向对象转换为共享指针进行操作 。[1] [2]
指针和引用的区别
● 指针和引用都是一种内存地址的概念, 但是指针是一个实体, 可以声明为 void; 引用只是一个别 名, 不能为 void。
● 引用内部其实是一个指针, 引用比指针更安全;相对的, 引用没有指针灵活。
● 引用和指针都可以作为参数传递给函数, 用于更改函数作用域外的变量, 在传递大对象时避免复
制, 提升效率 。作为参数时也有不同, 传递指针的实质是传值, 传递的值是指针的地址;传引用的 实质是传地址, 传递的是变量的地址。
● 指针可以有多级指向, 但是引用只能有一级引用 。
● 引用是一块内存的别名, 在添加到符号表时, 是将“引用变量名-引用对象的地址”添加到符号表
中, 符号表一经完成不能改变, 所以引用只能在定义时被绑定到一块内存上, 后续不能更改引用对 象 。指针指向一块内存, 其值是所指向的内存的地址, 在编译的时候, 则是将“指针变量名-指针变
量的地址”添加到符号表中, 所以指针包含的内容是可以改变的, 允许拷贝和赋值。
数组、指针区别
1. 数组存放的是数据, 是直接访问数据的;指针存放的是变量的地址, 是间接访问数据的;
2. 数组通常存储在静态存储区或栈上;指针可以随时地指向任意类型的内存块;
3. 用运算符 sizeof 可以计算出数组的容量( 字节数) ;sizeof(p) 得到的是一个指针变量 p 的字节 数, 而不是 p 指针所指向的内存容量;
4. char a[] = "hello" 数组指向每个数组元素;char *p = "hello" 而 p 指向字符串首地址;
数组指针、指针数组区别
数组指针( 指向数组的指针)
1. 数组在内存中的表示:创建一个数组就是在内存里面开辟一块连续的空间;
1 int a [2][2] = {1,2,3,4}; //这是一个2*2的二维数组
2 int (*p)[2]; //数组指针
3 p = a; //令p指向数组a
指针数组( 存放指针的数组)
指针数组的好处:
送代器、指针区别
送代器:用于提供一种方法顺序访问一个聚合对象中各个元素, 而又无需暴露该对象的内部表示。
送代器和指针区别: 迭代器不是指针, 是类模板, 表现的像指针, 其本质是封装了原生指针, 提供了比 指针更高级的行为, 相当于一种智能指针, 可以根据不同类型的数据结构来实现递增 、递减等操作 。迭
代器返回的是对象引用而不是对象的值
strcpy 和 memcpy 区别
● 复制的内容不同 。st rcpy 只能复制字符串, 而 memcpy 可以复制任意内容, 例如字符数组 、整 型 、结构体 、类等;
● 复制的方法不同 。st rcpy 不需要指定长度, 它遇到被复制字符串的结束符 "\0" 才结束, 所以容易 溢出 。memcpy 则是根据其第 3 个参数决定复制的长度;
● 用途不同 。通常在复制字符串时用 st rcpy, 而需要复制其他类型数据时则一般用 memcpy。
1.2 内存管理与分配
内存分配与存储区
C/C++内存分配有三种方式[1]:
● 从静态存储区分配 。内存在程序编译的时候就已经分配好, 这块内存在程序的整个运行期间都存 在 。例如全局变量, static变量。
● 从栈上分配 。在执行函数时, 函数内局部变量的存储单元都可以在栈上创建, 函数执行结束时这些 存储单元自动被释放 。分配的内存容量有限。
● 从堆上分配, 亦称动态内存分配 。程序在运行的时候用 malloc 或 new 申请任意大小的内存, 程序 员自己负责在何时用 free 或 delete 释放内存。
C/C++程序编译时内存分为5大存储区:
● 栈区( stack) 。编译器自动分配与释放, 主要存放函数的参数值, 局部变量值等, 连续的内存空 间, 由高地址向低地址扩展。
● 堆区( heap) 。由程序员分配与释放;不连续的空间, 通过空闲链表进行连接 。堆是低地址向高 地址扩展, 空间较大 。频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
● 静态存储区 。存放全局变量和静态变量;分为全局初始化区和全局未初始化区。
● 常量存储区 。存放常量字符串;对于全局常量, 编译器一般不分配内存, 放在符号表中以提高访问 效率。
● 程序代码区 。存放函数体的二进制代码。
malloc / free
malloc 申请内存的方式
● 方式一 :通过 br k() 系统调用从堆分配内存: 如果用户分配的内存小于 128 KB, 就是通过 br k() 函 数将 Γ堆顶」指针向高地址移动, 获得新的内存空间 。free 释放内存的时候, 并不会把内存归还给 操作系统, 而是缓存在 malloc 的内存池中, 待下次使用;
● 方式二: 通过 mmap() 系统调用在文件映射区域分配内存 。free 释放内存的时候, 会把内存归还给 操作系统, 内存得到真正的释放。
malloc() 分配的是物理内存吗?
不是的, malloc() 分配的是虚拟内存 。如果分配后的虚拟内存没有被访问的话, 是不会将虚拟内存映射 到物理内存, 这样就不会占用物理内存了 。只有在访问已分配的虚拟地址空间的时候, 操作系统通过查 找页表, 发现虚拟内存对应的页没有在物理内存中, 就会触发缺页中断, 然后操作系统会建立虚拟内存
和物理内存之间的映射关系。
malloc(1) 会分配多大的虚拟内存?
malloc() 在分配内存的时候, 并不是按用户预期申请的字节数来分配内存空间大小, 而是会预分配更大 的空间 。具体会预分配多大的空间, 跟 malloc 使用的内存管理器有关。
new/delete 、malloc/free区别
都可以分配和回收空间, new/delete 是运算符, malloc/free 是库函数 。new得到的是经过初始化的空 间, 而malloc得到的是未初始化的空间。
执行new有两个过程:
1. 分配未初始化的内存空间( malloc) 。若出现问题则抛出异常
2. 使用对象的构造函数进行初始化 。若出现异常则自动调用delete释放内存
执行delete有两个过程:
1. 使用析构函数对对象进行析构
2. 回收内存空间(free)
volatile、extern区别
volatile
1. 数据重新从内存中读取
2. 告诉编译器, 不要对这个变量做优化, 保证其顺序性
extern
1. 用在变量或函数的声明前, 说明此变量/函数是在别处定义的, 要在此处引用
2. 在 C++ 中调用 C 库函数, 需要在 C++ 程序中用 extern "C" 声明要引用的函数, 告诉链接器在链 接的时候用 C 语言规范来链接 。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不
同, 以此来解决名字匹配的问题
拷贝构造函数
类的对象需要拷贝时, 会调用拷贝构造函数。
在未定义显式拷贝构造函数的情况下, 系统会调用默认的拷贝函数( 浅拷贝), 它能够完成成员的一一 复制 。但当数据成员中有指针时, 如果采用简单的浅拷贝, 则两类中的两个指针指向同一个地址, 当对 象快要结束时, 会调用两次析构函数 。深拷贝和浅拷贝的区别 就在于深拷贝会在堆内存中另外申请空间
来存储数据, 从而解决悬空指针的问题 。简而言之, 当数据成员中有指针时, 必须要深拷贝才安全。
有三种情况会需要拷贝构造函数:
1. 一个对象以值传递的方式传入函数体, 需要拷贝构造函数创建一个临时对象压入到栈空间
2. 一个对象以值传递的方式从函数返回, 需要拷贝构造函数创建一个临时对象作为返回值
3. 一个对象需要通过另一个对象进行初始化
为什么拷贝构造函数必须是引用传递?
防止递归调用 。当一个对象需要以值方式传递时, 编译器会生成代码调用它的拷贝构造函生成一个副
本, 如果该类的拷贝构造函数的参数不是引用传递, 而是值传递, 那么就需要创建传递给拷贝构造函数
参数的临时对象, 而又一次调用该类的拷贝构造函数, 这就是一个无限递归。
预处理、编译、汇编、链接
预处理阶段:预处理器根据 # 开头的命令, 修改原始的程序, 如把头文件插入到程序文本中, 删除所有 的注释等。
编译阶段:编译过程就是把预处理完的文件进行一系列的词法分析 、语法分析 、语义分析等, 最终产生 相应的汇编语言文件, 不同的高级语言翻译的汇编语言相同 。编译是对源文件分别进行的, 每个源文件
都产生一个目标文件。
汇编阶段:把汇编语言代码翻译成目标机器指令。
链接阶段:将有关的目标文件和库文件相连接, 使得所有的这些文件成为一个能够被操作系统装入执行
的统一整体 。链接处理可分为两种:
● 静态链接: 函数的代码将从其所在的静态链接库中被拷贝到最终的可执行文件中 。这样程序在被执 行时会将其装入到该进程的虚拟地址空间中 。静态链接库实际上是一个目标文件的集合, 其中的每 个文件含有库中的一个或者一组相关函数的代码。
● 动态链接: 函数的代码被 放到 称作是动态链接库或共享对象的某个目标文件中 。链接程序要做的 只是在最终的可执行文件中记录下相对应的信息 。在可执行文件被执行时, 根据可执行程序中记录 的信息, 将动态链接库的全部内容映射到相应运行进程的虚拟地址空间上。
对于可执行文件中的函数调用, 可分别采用动态链接或静态链接的方法 。使用动态链接能够使最终的可 执行文件比较短小, 并且当共享对象被多个进程使用时能节约一些内存, 因为在内存中只需要保存一份 此共享对象的代码 。但并不是使用动态链接就一定比使用静态链接要优越 。[1]
define/const/typedef/inline 区别
const | #define | typedef | inline | |
执行/作 用时间 | 编译阶段 、链 接阶段 | 预处理阶段( 文本替换) | 编译阶段 | 编译阶段( 复制) |
类型检查 | 有 | 没有 | 有 | 有 |
功能 | 定义常量, 无 法重定义 | 定义类型别名 、定义常量/变 量 、定义编译开关 、可重定义 (#undef) | 定义类型 别名 | 解决一些频繁调用的函数 大量消耗栈空间( 栈内 存) 的问题 |
作用域 | \ | 没有 | 有 | \ |
const、static 区别
static:控制变量的存储方式和可见性
1. 修饰局部变量:将存放在栈区且生命周期在包含语句块执行结束时便销毁的局部变量改变为存放在 静态存储区, 且生命周期会一直延续到整个程序执行结束, 但是作用域还是限制在其语句块。
2. 修饰全局变量/函数:对于全局变量, 既可以在本文件中被访问, 也可以被在同一工程中的其他源文 件访问( 添加 extern 进行声明) 。用 static 进行修饰改变了其作用域范围, 由原来整个工程可见 变为了本文件可见 。
3. 修饰类函数:表示该函数属于个类而非实例;若对类中某变量修饰, 则表示该变量被所有该类实 例所共有, static修饰的类变量先于对象存在, 所以其要在类外初始化
const:定义常量
1. 修饰基本数据类型:修饰符 const 可在类型说明符前或后, 其结果是一样的 。在使用时不可以改变
这些变量的值。
2. 修饰指针:
指针常量:常量,指向的地址不能被改变,但是可以改变地址内的内容。
1 int a, b;
2 int * const p=&a; //指针常量
3
4 *p = 9; //操作成功
5 p = &b; //操作错误
1 int a, b;
2 const int *p = &a; //常量指针
3
4 *p = 9; //操作错误
5 p = &b; //操作成功
3. 类中的用法:const成员变量, 只在某个对象生命周期内是常量, 而对于整个类而言是可以改变
的 。因为类可以创建多个对象, 不同的对象其const数据成员值可以不同 。const数据成员的初始化 只能在类的构造函数的初始化列表中进行 。const成员函数( 在参数列表后加const, 此时对this隐 式加const) 的主要目的是防止成员函数修改对象的内容 。常量对象只能调用常量函数。
声明和定义的区别
【C/C++面试必备】声明和定义的区别_Linux猿的博客-CSDN博客_定义和声明的区别
● 变量/函数可以声明多次, 变量/函数的定义只能一次。
● 声明不会分配内存, 定义会分配内存。
● 声明是告诉编译器变量或函数的类型和名称等, 定义是告诉编译器变量的值, 函数具体干什么。
C++11新特性
1. 空指针nullptr: 目的是为了替代NULL, 用来区分空指针和0, 能够隐式转换为任何指针的类型, 也 能和他们进行相等判断 。由于传统C++会把NULL和0视为同一种东西, 这取决于编译器如何定义
NULL, 有些编译器会将NULL定义为 ((void)0),有些会直接将其定义为0。C++不允许直接将void 隐式转换到其他类型, 但如果NULL被定义为前者, 那么当编译 char *ch = NULL 时, NULL只好 被定义为0 。而这依然会产生问题, 这导致了C++中重载特性会发生混乱
2. 智能指针
3. Lambda表达式:利用lambda表达式可以编写内嵌的匿名函数, 用以替换独立函数或者函数对象, 并且使代码更可读, 有值捕获和引用捕获两种方式获取外部对象。
4. 右值引用:右值引用特性允许我们对右值进行修改, 借此可以实现move, 即从右值中直接拿数据过
来初始化或修改左值, 而不需要重新构造左值后再析构右值。
5. constexpr :constexpr 告诉编译器这是一个编译期常量, 使得定义的变量( 无需加const) 也可 以作为数组大小的表示 。甚至可以把一个函数声明为编译期常量表达式
6. 统一 的初始化方法:均可使用 {} 进行初始化变量
7. 类型推导:提供 auto 和 decltype 来静态推导类型 。decltype 用于获取一个表达式的类型, 而不 对表达式求值
8. 基于范围的 for 循环
9. final 和 override:提供 final 来禁止虚函数被重写/禁止类被继承, override 来显式地重写虚函数 10. default 和 delete: 可以显式地指定和禁止编译器为类自动生成构造或析构函数等
11. 静态断言:static_assert 关键字可在编译期进行使用, 而之前的assert仅在运行期起作用 ( 模板检 查在编译期)
12. 初始化列表:提供 initializer_list 来接受变长的对象初始化列表
13. 正则表达式
const std::vector<int> v(1);
const int&& foo(); // 返回临终值:生命周期已结束但内存还未拿走
auto a = v [0]; // a 为 int
decltype(v [0]) b = 0; // b 为 const int&
auto c = 0; // c, d 均为 int
auto d = c;
decltype(c) e; // e 为 int, 即 c 的类型
decltype((c)) f = e; // f 为 int&, 因为 c 是左值
decltype(0) g; // g 为 int, 因为 0 是右值
● C 是面向过程的, C++ 是面向对象的 。因此C++语言中有类和对象 、继承和多态这样的OOP语言必 备的内容, 此外C++还支持模板, 运算符重载以及STL;
● 在输入输出方式上, C 是 printf/scanf 库函数, C++ 是 cout/c in, 即 ostream和 istream 类型的 对象;
● 在动态内存管理上, C 语言通过 malloc/free 来进行堆内存的分配和释放, 而 C++ 是通过 new/delete 来管理堆内存;
● 在强制类型转换上, C 的强制类型转换使用小括号里面加类型进行强转, 而 C++ 可以使用 const_cast, static_cast, reinterpret_cast 和 dynamic_cast 继续强转;
● 在C++中, struct 关键字不仅可以用来定义结构体, 也可以用来定义类;
● C++不仅支持指针, 还支持更安全的引用 。不过在汇编代码上, 指针和引用的操作是一样的;
● C++支持自定义命名空间, 而 C 不支持。
● 指针:Java虚拟机内部用到了指针, 程序员无法直接访问内存, 无指针概念
● 多重继承:C++支持多重继承但Java不支持 ,Java支持一个类实现多个接口
● 自动内存管理:Java自动进行无用内存回收, 而C++必须程序员释放内存资源
● 重载运算符:Java不支持
● 类型转换:C++可隐含转换 ,Java必须强制转换
● 字符串:C++中字符串是以NULL终止符代表字符串的结束;Java是用类对象实现的
● 预处理:Java不支持预处理功能, C++在编译过程中会有一个预编译阶段 ,Java没有预处理器, 但 提供了import与C++预处理器有类似的功能
● C++为编译型语言; python为解释型的脚本语言。
● C++运行效率高 。Python是解释执行的, 和物理机CPU之间多了解释器这层, 而C++是编译执行 的, 直接就是机器码, 编译的时候编译器又可以进行一些优化。
● 开发效率上, Python要比C++快很多
● 代码形式
在C++语言中, 初始化和赋值是两个完全不同的操作 。初始化和赋值的区别事关底层效率问题。
● 初始化:创建变量时赋予其一个初始值。
● 赋值:把对象的当前值删除, 并赋予一个新的值。
如果在变量初始化时没有指定初始值, 则变量进行默认初始化, 默认值是由变量类型和位置决定的。
内置类型的初始值由定义的位置决定:
● 定义在函数体之外的变量被初始化为0
● 定义在函数体内部的局部变量则未定义
● 对于函数体内部的局部静态变量, 如果没有显式初始化, 它将执行默认值初始化
类内成员的默认初始值由类自己决定:
● 在默认构造函数中进行了赋值, 则初始化值为默认构造函数的值
● 在默认构造函数中没有赋值, 但是该数据成员提供了类内初始值, 则创建对象时, 其初始值就是类 内初始值
● 若上述都无, 对于内置类型, 则其值未定义;对于类类型则调用其默认构造函数, 如果没有默认构 造函数, 则不能进行值初始化。
若某个类有一个类成员是类类型, 那么
● 若类通过在构造函数体内初始化, 会先调用成员类的默认无参构造函数, 再调用类的赋值运算符;
● 若类通过在初始化列表去初始化, 则只调用成员类的拷贝构造函数。
另外, 虽然对于成员类型是内置类型的情况, 通过上述两种情况去初始化是相同的, 但是为了标准化, 推荐使用初始化列表 。[1]
必须使用初始化列表去初始化类成员的情况:
● 成员类型是引用 / 常量;
● 成员类型是对象, 并且这个对象没有无参数的构造函数;
● 子类初始化父类的私有成员。
类成员的初始化顺序不是按照初始化列表的顺序来的, 而是按照类成员的声明顺序
重载 | 重写( 函数体) | 重定义 | |
同名函数 | 是 | 是 | 是 |
参数列表 | 参数个数 、参数类型或参数顺序三者中 必须至少有一种不同 | 同 | 可以不同 |
返回类型有关 | 可以相同, 也可以不相同 | 同 | 可以不同 |
用于 | 同一作用域 | 父类虚函数 | 父类非虚函数 |
重载: 函数名相同, 函数的参数个数 、参数类型或参数顺序三者中必须至少有一种不同 。函数返回值的
类型可以相同, 也可以不相同 。发生在一个作用域内。
重写:也叫覆盖(override), 一般发生在子类和父类继承关系之间 。子类重写父类中有相同名称和参数的 虚函数。
重定义: 子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) , 派生类的函数屏蔽了与其 同名的基类函数 。如果一个派生类, 存在重定义的函数, 那么, 这个类将会隐藏其父类的方法, 除非在 调用的时候, 强制转换为父类类型, 才能调用到父类方法 。否则试图对子类和父类做类似重载的调用是
不能成功的。
重写需要注意:
1 、 被重写的函数必须是 virtual 的;
2 、重写函数必须有相同的类型, 名称和参数列表;
3 、重写函数的访问修饰符可以不同。
重定义需要注意:
● 如果派生类的函数与基类的函数同名, 但是参数不同, 此时, 不管有无 virtual, 基类的函数被隐 藏。
● 如果派生类的函数与基类的函数同名, 参数也相同, 但是基类函数没有 vitual 关键字, 此时, 基类 的函数被隐藏( 如果有 Virtual 就是重写覆盖了) 。
static_cast: 明确指出类型转换, 一般建议将隐式转换都替换成显示转换, 因为没有动态类型检查, 派 生类转基类安全, 反之不安全, 所以主要执行非多态的转换
const_cast:专门用于const属性的转换, 主要用来去除 const 和 volatile 限定符 。具体用法是用于指 针或引用, 间接操作被const修饰的变量 [1]
reinterpret_cast:从底层对数据进行重新解释, 依赖具体的平台, 可移植性差
● 默认继承 、成员访问权限不一样
● 是否支持类模板
class A{}; // sizeof(A) = 1 (标识这是一个类)
class A{virtual Fun()}; // sizeof(A) = 8 (64位,有一个指向虚函数表的指针)
class
间) A{static int a;}; // sizeof(A) = 1 (静态变量存储在静态存储区,不占类空
class A{int a;} // sizeof(A) = 4
class A{int a; char c;} // sizeof(A) = 8
class A{Fun()}; // sizeof(A) = 1 (普通函数不占空间)
为什么进行内存对齐?
● 内存访问次数影响:为了访问未对齐的内存, 处理器需要作两次内存访问; 而对齐的内存访问仅需 要一次访问。
● 硬件平台支持问题:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在 某些地址处取某些特定类型的数据, 否则抛出硬件异常。
● 空间消耗问题:没有进行内存对齐的结构体或类会浪费一定的空间, 当创建对象越多时, 消耗的空 间越多。
封装:把客观事物封装成抽象的类 。一个类就是一个封装了数据以及操作这些数据的逻辑实现, 其中某 些代码或者数据是有不同的访问权限, 以防止程序被意外改变或使用对象的私有部分。
继承:指可以让某个类型的对象获得另一个类型对象属性的方法 。可以使用现有类的所有功能, 并在无 需重新编写原来类的情况下对这些功能进行扩展 。继承分为 实现继承 和 接口继承 两种, 实现继承是指
直接使用基类的属性和方法而无需额外编码的能力; 接口继承是指仅使用属性和方法的名称, 但是派生
类必须提供其实现。
多态:就是向不同的对象发送同一个消息, 不同对象在接收时会产生不同的行为( 即方法), 即一个接 口可以实现多种方法 。多态和非多态的实质区别是函数地址是早绑定还是晚绑定( 早绑定 是在编译器编
译期间就可以确定函数的调用地址, 并产生代码, 是静态的) 。
虚函数
哪些函数不能是虚函数
1. 构造函数
2. 内联函数: 内联函数在编译阶段进行函数体的替换操作, 而虚函数在运行期间进行类型确定, 所以
不能是虚函数
3. 静态函数:不属于对象属于类, 静态成员函数没有 this 指针, 设置为虚函数没有意义
4. 友元函数:不属于类的成员函数, 不能被继承
5. 普通函数:不属于类的成员函数, 不能被继承
虚函数表 与 虚函数内部实现原理
每个拥有虚函数的类都至少有一个虚函数指针, 所有的虚函数都是通过 虚函数指针 在虚函数表中调用 的, 虚函数表会记录这个类中所有的虚函数的地址 。对于派生类来说, 编译器( 编译期创建) 建立虚函
数表的过程分为三步:
1. 拷贝基类的虚函数表, 如果是多继承, 则拷贝每个有虚函数基类的虚函数表
2. 选取继承的第一个基类函数表, 将该类虚函数表与其合并来共用一个虚函数表( 一个虚函数表对应
一个虚函数指针)
3. 检测该类中是否有重写基类中的虚函数, 若有则替换成已重写的虚函数地址
基类析构函数为什么要使用虚函数?
当不使用多态时, 可正常运行并析构对象;
当使用多态时, 如不将基类析构设置为虚函数,则当对象销毁时派生类无法正常析构, 仅仅只有基类被
析构。
class F{
public:
F() { cout << "分配 f" << endl; }
// 基类析构不是虚函数,则无法正常析构派生类
~F() { cout << "析构 f" << endl; }
};
class A: public F {
public:
A() { cout << "分配 a" << endl; }
~A() { cout << "析构 a" << endl; }
};
int main() {
cout << "不使用多态 : " << endl;
{ A a; }
cout << "使用多态 : " << endl;
{
F *f = new A();
delete f;
}
return 0;
}
1.5 基础概念
回调函数: 当发生某种事件时, 系统或其他函数将会自动调用定义的一段函数 。回调函数就是一个通过 函数指针调用的函数, 如果把函数的指针( 地址) 作为参数传递给另一个函数, 当这个指针被调用时,
我们就说这是回调函数。
简述 fork/wait/exec 函数
fork 将父进程复制一份给子进程, 子进程从 fork 调用处继续执行, 之后的代码在父子进程中各自执行 一遍 。最终父进程的 fork 返回子进程的 pid, 子进程的 fork 返回 0 表示创建成功 。所以看起来仿佛 fork 有两个返回值, 其实是两个进程的 fork 各自的返回值。
exec 函数族可以根据指定的文件名或目录名找到可执行文件, 并用它取代原调用进程的数据段 、代码 段和堆栈段 。在执行完后, 原调用进程除了进程号外, 其他全部被新程序的内容替换。
wait 会暂时停止当前进程的执行, 直到有信号或子进程结束 。如果子进程已经结束, 则 wait 会立即返 回子进程结束状态值。