Before:粗略地看了一遍翁恺的 C++ 课程,然后在这里看面试总结


- 预处理:头文件、宏定义插入与替换
- 编译:语法分析...翻译成汇编代码(Java 是字节码)
- 汇编:汇编语言转为机器语言
- 链接:将有关的目标文件和库文件彼此连接
m.elecfans.com/article/663…
www.cnblogs.com/magicsoar/p…
面向对象与面向过程
面向过程:以步骤划分问题
面向对象:以功能划分问题
以上课的过程作为例子:
- 面向过程:同学们走进教室坐下来,老师走进来,铃声响了,老师开始讲话,同学开始听课……
- 面向对象:上课要有教室,要有一个老师,要有很多学生,或者有电脑,他们有很多属性(功能)和关系,比如老师可以发出声音,学生可以记笔记,电脑可以放课件
内存管理
一个由 C/C++ 编译的程序占用的内存分为以下几个部分:
- 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等
- 堆区(heap) — 由程序员分配和释放,若程序员不释放,程序结束时可能由 OS 回收
- 全局区 / 静态区存储区(static)— 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域
- 文字常量区 — 常量字符串就是放在这里的
- 程序代码区 — 存放函数体的二进制代码
内联函数(编译时期展开函数)
interview.huihut.com/#/?id=inlin…
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
虚函数可以是内联函数,但是当虚函数表现多态性的时候不能内联,因为虚函数表现多态是在运行期绑定的。当编译器知道所调用的对象是哪个类(如 Base::who()
)才行
struct / class
C 和 C++ 中 struct 的区别
首先,在面向 C 过程中,这里的 struct 是一种数据类型,那么里面肯定不能定义函数,否则报错,C++ 可以包含函数
struct 和 class 的区别
- 默认访问权限和默认继承权限不同,前者 public 后者 private
- class 可用于定义模板参数,而 struct 不可以
一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值:blog.csdn.net/firefly_200…
内存对齐
按最宽数据类型对齐(如果下一个元素能摆下,就继续放;摆不下,就从头开始摆。下方例子)
优点:内存对齐主要是为了提高程序的性能,数据结构特别是栈,应尽可能的在自然边界上对齐,经对齐后 CPU 的内存访问速度大大提升(未对齐可能需要两次访存)
// 按 4 对齐
class Data {
char c;
// int 摆不下了,要从头开始
int a;
char d;
};
cout << sizeof(Data) << endl; 12
class Data {
char c;
// 第二个 char 仍然能摆下
char d;
int a;
};
cout << sizeof(Data) << endl; 8
static
- 修饰普通变量,修改变量的存储区域和生命周期(延长生命周期),使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
consts
- 修饰变量,修饰指针
- 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改
- 修饰成员函数,说明该成员函数内不能修改成员变量
const 可用于对重载函数的区分
this
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。- 当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 - 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
this
指针被隐含地声明为:ClassName *const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。- 在以下场景中,经常需要显式引用
this
指针:- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。
指针常量和常量指针
const 后边的内容为“常量”,英文更好理解
blog.csdn.net/qq_36132127…
int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // * 在前,定义为指针常量
- 常量指针(pointer to const / 指向常量的指针):不能修改指向地址的内容
int main()
{
int a = 2;
int const *b = &a;
// 报错
*b = 3;
printf("albert:%d\n",a);
}
- 指针常量(const pointer):指针指向的地址不可以修改,但内容可以改变
int main()
{
int a = 2;
int b = 3;
int *const c = &a;
printf("albert:%p\n", c);
// 报错
c = &b;
printf("albert:%p\n",c);
}
多态、动态绑定(和 Java 不同)
多态可以分为静态多态和动态多态,所谓静态多态就是通过函数重载、模板、强制类型转换实现的,静态多态是在函数编译阶段就决定调用的机制,即在编译链接截断将函数的入口地址给出,而动态多态是在程序运行时刻才决定调用机制,而在 C++ 中动态多态是通过虚函数实现的(还有一些注意点看这里)
- 通过 virtual 动态绑定,要通过指针或引用?
A a;
B b;
a = b;
这样是不行的,因为 b 的 vtable( 有几个基类就有几个虚函数表) 没有赋给 a(对象无法访问虚函数表),而是要 A *a = &b
虚函数的调用关系:this -> vptr -> vtable -> virtual function

-
父类指针或引用指向子类对象
-
析构函数也要 virtual,不然只调用父类的析构
-
Java 默认就动态绑定
纯虚函数(Java 接口)、虚继承等
具体:interview.huihut.com/#/?id=纯虚函数
-
虚函数指针(占用类的空间)、虚函数表(不占用类的空间):songlee24.github.io/2014/09/02/…
-
虚继承:用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。虚基类指针(表)
-
带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类
-
抽象类含有纯虚函数,接口类仅含有纯虚函数
拷贝构造函数
拷贝构造函数的调用时机
- 函数的参数为类的对象
- 函数的返回值是类的对象
- 对象需要通过另外一个对象进行初始化
默认拷贝构造函数仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值(指针则是复制地址)。默认拷贝构造函数没有处理静态成员变量
浅拷贝、深拷贝
浅拷贝

在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,出现错误
深拷贝

此时 rect1 的 p 和 rect2 的 p 各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是“深拷贝”
参数为什么必须是引用
为了防止递归引用
一个对象需要以值方式传递时,编译器会调用它的拷贝构造函数以生成一个复本。若拷贝构造函数里的参数也是值传递,那么会继续调用拷贝构造函数以生成一个复本,继而陷入递归
静态变量

静态成员变量的初始化不能省略
class A {
public:
//声明但未定义
static int a;
};
//此处定义了静态成员变量,同时初始化,不能省略
int A::a = 3;
int main() {
printf("%d", A::a);
return 0;
}
重载运算符
class Integer {
public:
Integet(int n = 0) : i(n) {}
// 若没有第一个 const 则返回值可以作为左值,类似:1 = a
// 后两个 const 代表两个操作数不能被修改
const Integer operator+(const Integer &n) const {
return Integer(n.i + i);
}
private:
int i;
};
- 类型转换
重载运算符和拷贝构造函数都可以实现

上述代码中 f(a)
会出错,因为写了两种类型转换的方法,编译器不知道用哪个。可以通过在拷贝构造函数前加上 explicit
来关闭隐式类型转换
volatile
volatile int i = 10;
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
- volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
- const 可以是 volatile (如只读的状态寄存器)
- 指针可以是 volatile
friend 友元类和友元函数
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递、具有单向性
- 友元声明的形式及数量不受限制
- 友元函数没有 this 指针
例子:www.runoob.com/cplusplus/c…
new、delete
-
new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)
-
delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间
-
🚀new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数
-
用 free 来释放 new 出来的东西会发生什么?
答:对于简单数据类型,和 delete 一样;对对象来说,free 不会调用析构函数
C++ 函数调用的过程
blog.csdn.net/HDong99/art…
www.cnblogs.com/sddai/p/976…
-
为什么参数是从右到左入栈的?
因为存在不定长参数的函数,如 printf,编译器通过 format 参数中的 % 占位符的个数来确定参数的个数,现在我们假设参数的压栈顺序是从左到右的,由于 format 先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到 format,而要找到 format,必须要知道参数的个数。 -
再问,不是有 EBP 栈底指针吗?
看这张图,
参数是属于调用者的栈帧
四种强制类型转换
dynamic_cast
- 用于多态类型的转换
- 执行运行时类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换、向下转换
static_cast
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int)
- 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
const_cast
用于删除 const 和 volatile 关键字
reinterpret_cast
- 允许将任何指针转换为任何其他指针类型(如
char*
到int*
或One_class*
到Unrelated_class*
之类的转换,但其本身并不安全) - 也允许将任何整数类型转换为任何指针类型以及反向转换
静态库和动态库区别
静态库:在链接阶段,会将汇编生成的目标文件 .o 与引用到的库一起链接打包到可执行文件中。
静态链接器主要完成以下两个任务:
- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来
- 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置
静态库缺点:
- 空间浪费。多个程序都用到某个静态库,则静态库在内存存在多份拷贝
- 如果静态库 libxx.lib 更新了,所有使用它的应用程序都需要重新编译、发布给用户(全量更新)
动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新
智能指针
智能指针自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr 允许多个指针指向同一个对象, unique_ptr 则“独占”所指向的对象。标准库还定义了一种名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象,这三种智能指针都定义在 memory 头文件中
shared_ptr
创建智能指针时必须提供额外的信息,指针可以指向的类型
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
当进行拷贝和赋值时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象
auto p = make_shared<int>(42); // make_shared 函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
auto q(p); // p 和 q 指向相同的对象,此对象有两个引用者
当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象,它是通过析构函数完成销毁工作的
weak_ptr
解决 shared_ptr 循环引用的问题:blog.csdn.net/albertsh/ar…
- weak_ptr 接受 shared_ptr 类型的变量赋值,但是反过来是行不通的,需要使用lock函数
- 不增加引用计数
unique_ptr
一个 unique_ptr “拥有“他所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定的对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁
auto_ptr
STL 容器
顺序容器
是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。顺序容器包括:vector、list、deque
关联容器
关联式容器是非线性的树结构,更准确的说是红黑树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。元素是有序的集合,默认在插入的时候按升序排列。关联容器包括:map(按 key 大小存储)、set、multimap、multiset
map 的所有元素都是 pair,同时拥有 key & value
容器类自动申请和释放内存,因此无需 new 和 delete 操作
在 C++ 11 中新出 4 个关联式容器:unordered_map/unordered_set/unordered_multimap/unordered_multiset
unordered_set(hash_map) 的实现:www.jianshu.com/p/56bb01df8…

一些注意点
-
对象使用 new 和不使用 new 的区别
blog.csdn.net/cscmaker/ar… -
构造函数要写成 public,private 只能被成员函数和友元访问,否则创建对象时,对象无法访问构造函数
-
编译只对一个文件进行编译,生成
.s
文件(汇编代码);汇编阶段把汇编代码变可执行文件.o
-
当使用
extern
关键字修饰变量(未初始化),表示变量声明。当在另一个文件中,为extern
关键字修饰的变量赋值时,表示变量定义。声明可以拷贝 n 次,但是定义只能定义一次 -
include
双引号表当前目录找,尖括号表示引用标准库头文件,在系统目录 -
初始化列表(构造函数中进行的是赋值):
- 更高效,可以少调用一次默认构造函数
- 哪些必须要用初始化列表初始化:blog.csdn.net/sinat_20265…
- 初始化列表在构造函数执行前执行,他不能初始化静态成员,静态成员的初始化见上方。
- 初始化方式不是按照列表的的顺序,而是按照变量声明的顺序
-
C++ 中子类隐藏父类同名函数(只需同名,参数、返回值无关)、变量;Java 中必须参数、返回值相同才能隐藏父类方法
指的是子类引用指向子类对象(不是动态绑定)
-
静态成员函数没有 this 指针,因为 this 指向调用该成员函数的那个对象,而静态成员函数属于整个类拥有
-
#ifdef #endif (一般写在头文件里)作用:防止头文件被多次引用,blog.csdn.net/fly_yr/arti…
-
extern "C":
extern "C"
的作用是让 C++ 编译器将extern "C"
声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。void foo(int x, int y)
函数被 C 编译器修饰后在符号库中的名字是_foo
,而 C++ 编译器则会产生像_foo_int_int
之类的名字。interview.huihut.com/#/?id=exter… -
explicit:修饰单参数的构造函数时,可以防止隐式转换和复制初始化
class A { public: explicit A(int a) { cout << "A(int a)" << endl; } A(const A& a) { cout << "A(const A& a)" << endl; } private: int _a; }; void doA(A a) {} int main() { A a1(1); // 复制初始化不通过 A a2 = 1; // explicit 修饰构造函数的对象不可以从 int 到 A 的隐式转换 doA(1); }
-
assert:断言,是宏,而非函数。用于判断一个表达式,在表达式条件为 false 的时候触发异常
-
sizeof()
- 对数组,得到整个数组所占空间大小
- 对指针,得到指针本身所占空间大小
-
命名空间:可作为附加信息来区分不同库中相同名称的函数、类、变量等。www.runoob.com/cplusplus/c…
-
using:using 声明和指示。interview.huihut.com/#/?id=using
-
:: 范围解析运算符,例子:interview.huihut.com/#/?id=-范围解析…
- 全局作用域符(
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
- 全局作用域符(
-
为何 static / const 成员函数不能为 virtual?
- static 成员函数没有 this 指针,而 vptr 是通过 this 指针访问的
- const 修饰符用于表示函数不能修改成员变量的值,该函数必须是含有 this 指针的类成员函数
-
strcpy 和 memcpy:blog.csdn.net/qq_36389327…
-
如何定义一个只能在堆上(栈上)生成对象的类?interview.huihut.com/#/?id=如何定义一…
-
lambda 表达式是一种匿名函数(内联函数),即没有函数名的函数;该匿名函数是由数学中的 λ 演算而来的。通常情况下,lambda 函数的语法定义为:
[capture] (parameters) mutable ->return-type {statement}