参考引用
- 阿秀的学习笔记
- 本博客对上述笔记进行较大程度的整合、精简与补充
41. C++ 代码报错 coredump
- coredump 是程序由于异常或者 bug 在运行时异常退出或者终止,在一定的条件下生成的一个叫做 core 的文件,这个 core 文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等,对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息,使用 gdb 工具对 core 文件进行调试
42. 静态类型/绑定 & 动态类型/绑定
- 静态类型:对象在声明时采用的类型,在编译期既已确定
- 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期
- 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的
- 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期
- 在继承体系中只有虚函数使用的是动态绑定(如此实现多态性),其他的全部是静态绑定
引用也能实现动态绑定
- 引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数(注意只能调用虚函数)
43. 怎样判断两个浮点数是否相等
- 不能直接用 == 来判断,对于两个浮点数比较只能通过相减并与预先设定的精度比较(记得要取绝对值)
44. 指针加减计算要注意什么
- 指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的
- 指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成十进制计算,计算结果除以类型长度取得结果
45. 类如何实现只能静态分配和只能动态分配
- 静态分配
- 把 new、delete 运算符重载为 private 属性
- 动态分配
- 把构造、析构函数设为 protected 属性,再用子类来动态创建
- 建立类的对象有两种方式
- 静态建立,由编译器为对象在栈空间中分配内存
- 动态建立,使用 new 运算符为对象在堆空间中分配内存
- 第一步执行 operator new() 函数,在堆中搜索一块内存并进行分配
- 第二步调用类构造函数构造对象
只有使用 new 运算符,对象才会被建立在堆上,因此只要限制 new 运算符就可以实现类对象只能建立在栈上,可以将 new 运算符设为私有
46. 函数指针
- 什么是函数指针?
- 函数指针指向的是特殊的数据类型,函数类型是由其返回的数据类型和其参数列表共同决定,而函数名则不是其类型的一部分,一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针
- 函数指针的声明方法
// 下面的 pf 就是一个函数指针,指向所有返回类型为 int,并带有两个 const int& 参数的函数 // 注意:*pf 两边的括号是必须的 int (*pf)(const int&, const int&); // 而这声明了一个函数 pf,其返回类型为 int*, 带有两个 const int& 参数 int *pf(const int&, const int&);
- 为什么有函数指针
- 函数与数据项相似也有地址,希望在同一个函数中通过使用相同的形参在不同时间使用产生不同效果
- 一个函数名就是一个指针,它指向函数的代码
- 一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数
- 两种方法赋值
- 指针名 = 函数名;
- 指针名 = &函数名;
47. printf 函数的实现原理
- 在C/C++中,对函数参数的扫描是从后向前的
- C/C++ 的函数参数是通过压入堆栈的方式来给函数传参(堆栈是一种先进后出的数据结构)
- 在计算机的内存中,数据有 2 块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针
- 最先压入的参数在所有参数的最后面,最后压入的参数在最前面(第一个),所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方
cout 和 printf 区别
- cout 是类 std::ostream 的全局对象
- cout << 后可跟不同的类型是因为已存在针对各种类型数据的重载,所以会自动识别数据的类型
- cout 是有缓冲输出(先将输出字符放入缓冲区,然后输出到屏幕),printf 是行缓冲输出
48. 静态成员与普通成员的区别
- 生命周期
- 静态成员变量从类被加载开始到类被卸载,一直存在
- 普通成员变量只有在类创建对象后才开始存在,对象结束它的生命期也结束
- 共享方式
- 静态成员变量是全类共享,普通成员变量是每个对象单独享用
- 定义位置
- 普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区
- 初始化位置
- 普通成员变量在类中初始化,静态成员变量只能在类外初始化
- 默认实参
- 可以使用静态成员变量作为默认实参
49. ifdef endif 的理解
- 一般情况下,源程序中所有的行都参加编译,但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是 “条件编译”:有时希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句
// 当标识符已经被定义过 (一般是用 #define 定义),则对程序段 1 进行编译,否则编译程序段 2 #ifdef 标识符 // 程序段 1 #else // 程序段 2 #endif
- 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量 “重定义” 错误,在头文件中使用 #define、#ifndef、#ifdef、#endif 能避免头文件重定义
50. strcpy、sprintf 和 memcpy 的区别
- 操作对象不同
- strcpy 的两个操作对象均为字符串
- sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串
- memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型
- 执行效率不同
- memcpy 最高,strcpy 次之,sprintf 的效率最低
- 实现功能不同
- strcpy 主要实现字符串变量间的拷贝
- sprintf 主要实现其他数据类型格式到字符串的转化
- memcpy 主要是内存块间的拷贝
51. int main(int argc, char *argv[]) 的内存结构
- 程序在命令行下运行时,需要输入 argc 个参数,每个参数是以 char 类型输入的,依次存在数组 argv[] 中,所有的参数在指针 char* 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称
52. Debug 和 Release 的区别
- Debug 调试版本
- 包含调试信息,所以容量比 Release 大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试
- Debug 模式下生成两个文件,除了 .exe 或 .dll 文件外,还有一个 .pdb 文件,该文件记录了代码中断点等调试信息
- Release 发布版本
- 不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的(调试信息可在单独的PDB文件中生成)
- Release 模式下生成一个文件:.exe 或 .dll 文件
53. 回调函数
- 回调函数相当于一个中断处理函数,由系统在符合设定的条件时自动调用,为此需要做三件事
- 1、声明
- 2、定义
- 3、设置触发条件(就是在函数中把回调函数名称转化为地址作为一个参数,以便系统调用)
- 回调函数就是一个通过函数指针调用的函数
- 如果把函数指针(地址)作为参数传递给另一个函数,当这个指针被用为:调用它所指向的函数时,就说这是回调函数
54. C++ 从代码到可执行程序的过程
- 预编译
- 主要处理源代码文件中的以“#”开头的预编译指令
- 编译
- 把预编译之后生成的 xxx.i 或 xxx.ii 文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件
- 汇编
- 将汇编代码转变成机器可以执行的指令(机器码文件)
- 链接
- 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
55. 静态链接和动态链接
-
使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件
- 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本
- 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序
- 运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快
-
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件
- 共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本
- 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标
- 性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失
55(补充). 动态库和静态库的区别
- 在 Linux 下,动态库文件以 .so 结尾,静态库以 .a 结尾
- 在 Windows 下,动态库以 .dll 结尾,静态库以 .lib 结尾
55.1 Linux 动态库和静态库
-
编写代码时经常用到已有的接口,他们是以库的形式提供,常见形式有两种
- 一种常以 .a(archive) 为后缀,叫静态库
- 另一种以 .so(shared object) 为后缀,叫动态库
-
目标文件
- 目标文件常常按照特定格式来组织,在 linux 下,它是 ELF 格式(Executable Linkable Format,可执行可链接格式),而在 windows 下是 PE(Portable Executable,可移植可执行)
- 目标文件有三种形式
- 可执行目标文件(ELF 文件)。即通常所认识的,可直接运行的二进制文件
- 可重定位目标文件(.o)。包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件
- 共享目标文件(.so)。它是一种在加载或者运行时进行链接的特殊可重定位目标文件
-
静态库
- 可重定位目标文件以一种特定的方式打包成一个单独的文件(静态库),并且在链接生成可执行文件时,从静态库中 “拷贝” 它自己需要的内容到最终的可执行文件中
-
动态库
- 动态库和静态库类似,但是它并不在链接时将需要的二进制代码都 “拷贝” 到可执行文件中,而是仅仅 “拷贝” 一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程
动态库和静态库的区别
- 链接时刻不同:静态库在编译时被链接到可执行文件中,而动态库则在运行时被加载到内存中
- 文件大小不同:静态库会被完整地复制到可执行文件中,因此可执行文件的大小会增加;而动态库是独立的文件,可执行文件只包含动态库的引用信息,因此可执行文件的大小较小
- 内存占用不同:静态库被完整地加载到内存中,因此静态库会增加可执行文件的内存占用;而动态库在运行时才被加载到内存中,多个可执行文件可以共享同一个动态库,从而减少内存占用
- 变动性不同:静态库在编译时就被链接到可执行文件中,如果静态库发生了变化,需要重新编译可执行文件;而动态库可以独立于可执行文件进行更新,不需要重新编译可执行文件,只需替换动态库文件即可
- 兼容性不同:由于动态库在运行时才被加载,可以动态地链接到不同的系统和平台上;而静态库在编译时就被链接到可执行文件中,因此需要针对不同的系统和平台进行编译
55.2 Windows 动态库和静态库
- 静态链接库 .lib 在链接时,编译器会将 .obj 文件和 .LIB 文件组织成一个 .exe 文件,程序运行时,将全部数据加载到内存
- 如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端
- 动态链接库 DLL:(Dynamic Link Library) 是一个被其他应用程序调用的程序模块,其中封装了可以被调用的资源或函数
- DLL 文件属于可执行文件,它符合 Windows 系统的 PE 文件格式,不过它是依附于 EXE 文件创建的的进程来执行的,不能单独运行
- 动态链接库有两种加载方式:隐式加载和显示加载
- 隐式加载又叫载入时加载,指在主程序载入内存时搜索 DLL,并将 DLL 载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受
- 显式加载又叫运行时加载,指主程序在运行过程中需要 DLL 中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好
56. 动态编译与静态编译
-
静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库
-
动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令
- 优点:一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源
- 缺点:但是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库,另外如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行
57. 友元函数和友元类
-
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制
- 通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员
- 友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差
-
友元函数
- 友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员,但是需要在类的定义中声明所有可以访问它的友元函数
- 一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数
class A { public: friend void set_show(int x, A &a); // 该函数是友元函数的声明 private: int data; }; void set_show(int x, A &a) { // 友元函数定义,为了访问类 A 中的成员 a.data = x; cout << a.data << endl; } int main(void){ class A a; set_show(1, a); return 0; }
-
友元类
- 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员),但是另一个类里面也要相应的进行声明
- 使用友元类注意事项
- 友元关系不能被继承
- 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明
- 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元,同样要看类中是否有相应的申明
class A { public: friend class C; // 这是友元类的声明 private: int data; }; class C { // 友元类定义,为了访问类 A 中的成员 public: void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;} }; int main(void) { class A a; class C c; c.set_show(1, a); return 0; }
58. 为什么 C++ 没有垃圾回收机制
- 实现一个垃圾回收器会带来额外的空间和时间开销
- 需要开辟一定的空间保存指针的引用计数和对他们进行标记 mark
- 然后需要单独开辟一个线程在空闲的时候进行 free 操作
59. 内存池及其实现
-
内存池(Memory Pool)是一种内存分配方式
- 通常直接使用 new、malloc 等申请内存,缺点:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能
- 内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用,当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升
-
《STL 源码剖析》中的内存池实现机制
- allocate 包装 malloc,deallocate 包装 free
- 一般是一次 20*2 个的申请,先用一半,留着一半
60. 类的成员变量和成员函数内存分布情况
- 一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址
- 从代码运行结果来看,对象的大小和对象中数据成员的大小是一致的,也就是说成员函数不占用对象的内存,这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数
- 静态成员函数与一般成员函数的唯一区别就是没有 this 指针,因此不能访问非静态数据成员
#include <iostream> using namespace std; class Person { public: Person() { this->age = 23; } void printAge() { cout << this->age <<endl; } ~Person(){} public: int age; }; int main() { Person p; cout << "对象地址:"<< &p <<endl; cout << "age地址:"<< &(p.age) <<endl; cout << "对象大小:"<< sizeof(p) <<endl; cout << "age大小:"<< sizeof(p.age) <<endl; return 0; }
对象地址:0x7fffec0f15a8 age地址:0x7fffec0f15a8 对象大小:4 age大小:4