目录
一、🔺🔺基础语法
函数重载
-
函数重载的要求?
参数个数、类型、类型顺序 -
为什么 C 语言不支持重载?而 C++ 支持?
符号表中,C++ 存放的是修饰后的函数名+参数类型+命名空间 -
extern “C” 是什么?
external "C"用于与C语言函数进行交互,确保兼容性 -
extern、export、explicit
extern 关键字用于引用在其他文件中定义的变量或函数
export 关键字用于模板函数的外部调用,提供了更大的模块化和可维护性
explicit 阻止隐式类型转换 -
EXPORT_SYMBOL 宏功能描述
标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以在您的内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用。
在模块函数定义之后使用EXPORT_SYMBOL(函数名);
在调用该函数的模块中使用extern对之声明;
首先加载定义该函数的模块,再加载调用该函数的模块;
引用🔺
- 什么是引用?
引用就是给变量起别名。 - 使用场景?
- 输出型参数:可以通过形参的改变来直接改变实参;
- 返回值:函数返回时,出了函数作用域,如果返回对象还在(未被系统收回),则可以使用引用返回,如果还给系统了,必须使用传值返回。
- 减少了拷贝(提高了程序效率)
- 调用者可以直接修改返回对象
- 注意:
- 对于会发生隐式类型转换、产生临时变量的引用,引用的不是 d 是临时变量,临时变量具有常性,需要加 const。
- 对于会发生隐式类型转换、产生临时变量的引用,引用的不是 d 是临时变量,临时变量具有常性,需要加 const。
- 指针和引用的区别?
- 内存空间:指针是一个变量,它存储着一个内存地址,而引用是一个别名,它是已经存在的变量或对象的别名。因此,指针本身占据内存空间,而引用不占用内存空间;
“sizeof 指针”得到的是指针本身的大小,在32 位系统指针变量占用4字节内存,“sizeof 引用”得到的是所指向的变量(对象)的大小; - +1 的意义:指针是对内存地址自增,而引用是对值的自增;
- 解引用:指针需要解引用,引用使用时无需解引用 (*);
- 修改对象:指针可变,引用只能在定义时被初始化一次,之后不可变;
- 为空:指针可以为空,引用不能为空。
- 内存空间:指针是一个变量,它存储着一个内存地址,而引用是一个别名,它是已经存在的变量或对象的别名。因此,指针本身占据内存空间,而引用不占用内存空间;
内联 inline
- inline 价值和意义是什么?
- 提高程序运行效率:在内联函数被调用的地方进行代码展开,省去函数调用的时间;
- 使用更加安全:相比于宏函数,内联函数在代码展开时,编译器会进行语法安全检查或数据类型转换;
- 缺点:
- 代码膨胀,产生更多的开销;
- 如果内联函数内代码块的执行时间比调用时间长得多,那么效率的提升并没有那么大;
- 如果修改内联函数,那么所有调用该函数的代码文件都需要重新编译;
- 内联声明只是建议,是否内联由编译器决定,所以实际并不可控。
- 宏的缺点?
- 不能调试
- 没有类型安全的检查
- 有些场景下非常复杂。
- C++ 使用
const
和enum
替代宏常量,用inline
去替代宏函数
保留了宏的可维护性、不限制数据类型、不会开辟栈帧的优点
缺省参数
- 缺省参数必须满足什么条件?
必须是 全局变量、常量 - 半缺省要注意什么?
半缺省必须从右往左连续设置 - 全缺省要注意什么?
无参的构造函数 和 全缺省的构造函数 都称为 默认构造函数,两个只能存在一个,因为调用时会存在歧义。
namespace
就是域
static
- static 的初始化 在 C++11 之前有线程安全问题的,但是在 C++11 后就原子化了;
- static 变量未初始化会被默认初始化为 0;
面向过程:
- 静态变量 分为 全局静态 和 局部静态
- 都储存在 数据段/全局数据区,只会被初始化一次,只在该项目文件内能被读到;
- 局部静态变量,只初始化一次始,始终驻留在全局数据区,直到程序运行结束;
但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
- 静态函数
- 项目文件内能被读到, 不能被其它文件所用, 其它文件中可以定义相同名字的函数,不会发生冲突。
面向对象: 格式正确,都能调用
- 静态成员变量
- 同样储存在数据段,不过类内定义,需要在类外初始化(此时不同于全局静态变量,不会默认初始化为0,如果不初始化,访问会报错);
- 同样受域的限制,也受访问限制符的限制。
- 静态成员函数
- static 函数内没有 this 指针,调用的时候需注意
- 静态成员函数不能访问非静态成员函数和非静态数据成员(只能通过对象,对非静态成员进行访问)
main()的运行
一个程序正常来说:入口函数(环境初始化,调用 main())—> main(){} —> 返回入口函数(清理)—>终止处理程序
这里的入口函数工作包含:堆生成销毁、I/O打开关闭、全局变量构造析构、进程开启关闭…
-
能在main()开始前能执行的
- 静态 / 全局 的 变量 / 对象 的初始化(data段的内容)
-
能在main()结束后执行的
- 全局对象的析构
-
一些方法
- gcc 可用,__attribute__((constructor)) 关键字放在全局函数前,可以让函数在主函数前运行,进行一些数据初始化,模块加载验证等
- gcc 可用,__attribute__((destructor)) 关键字放在全局函数前,可以让函数在主函数后运行。
- 微信的 mars 库封装的宏函数:
- BOOT_RUN_STARTUP(函数),可以在main运行前调用函数
- BOOT_RUN_EXIT(函数)可以在main运行后调用函数
- MSVC 可用,
atexit(函数)
可以在main运行后调用函数
二、🔺🔺类和对象
这部分虽然主要是选择题部分,但是是 C++ 学习的基石,很重要。
概念理解:面向对象or过程、面对对象的三大特性🔺
- 面向对象和面向过程的理解? (结合样例解答)
- 过程关注过程本身;对象关注类型,以及类型之间的交互关系;
- 面向过程:相当于把事情拆分成几个步骤(相当于拆分成一个个的方法和数据),然后按照一定的顺序执行;
- 面向对象:会把事物抽象成对象的概念,先抽象出对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法。
- 面向对象的三大特性?封装、继承、多态,你对他们的理解是什么? (结合样例解答)
- 封装:
- 控制了访问方式,一方面选择性的将接口提供给给外部用户使用,另一方面降低了用户的使用成本;
- 更便于代码的分类、管理维护,降低了各个功能之间的耦合度;
- 迭代器就是封装的经典体现,还比如用 C 语言实现栈的话,栈中间的数据都可以被读到…
- 继承,基类子类的关系
- 多态,买票、异常…(很多丰富场景)
- 封装:
默认成员函数、初始化列表🔺
- 8 个默认成员函数,他们什么情况下会默认生成?默认生成的都干了什么?(选择题)
- 构造和析构:
- 内置类型不处理,自定义类型去调他的默认构造函数和默认析构函数
- 默认构造函数(即不用传参的构造函数)有: 全缺省构造函数、无参构造函数、我们没写编译器默认生成的构造函数
- 拷贝构造和拷贝赋值:
- 内置类型 值/浅拷贝,自定义类型调他们的构造和赋值
- operator& 不关注
- 移动构造和移动赋值:
- 对比的是拷贝构造和拷贝赋值,左值走拷贝构造拷贝赋值,右值走移动构造和移动赋值。
- 只有不写拷贝赋值和析构才会自动生成,对内置类型默认完成xxx,对自定义类型要看他有没有实现
- 构造和析构:
- 初始化列表有什么价值?哪些成员必须在初始化列表初始化?
- const 成员变量、引用成员变量、自定义类型成员(且该类 没有默认构造函数 时)(另 static 必须在类外初始化)
- 成员变量在类中声明次序才是初始化顺序,和初始化列表的顺序无关
对象实例化、this 指针、空指针->成员函数
- 对象实例化,对象的大小怎么算?空类的大小是多少?为什么是 1?
- 对象的大小,实际上就是该类中 “成员变量” 之和,计算使用 内存对齐🔗(结构体 max,其余成员 min)
- 当类中 只有成员方法 或干脆是个 空类 时,实例化这个类,编译器会给 1byte 的空间,这 1byte 不存放有效数据,只用来占位,标识对象已经被实例化定义出来了。
- this 指针是什么?存在哪儿?
- this 指针是调用成员函数时才产生的,是成员函数的形参,所以存在栈中。
- 用来分辨调用对象,即,this 指针是对象的地址,对象中不存储 this 指针
- 空指针调用成员函数会出现什么情况?
- 首先 nullptr->fun(),编译器用 call 在公共区域(代码段)找,即使是空指针,这部分的调用不会有问题
- 接下来具体情况具体分析,如果这个函数中会用到成员变量,就会出现 this->成员变量 实际上是越界访问了,此时就会运行崩溃;
- 如果没有调用 this 这个空指针,程序仍然可以正常运行;
- 所以不是所有 空指针+解引用符号 都会出问题~
- 这段代码的输出结果是什么?
class D { public: void printA() { cout<<"printA"<<endl; } virtual void printB() { cout<<"printB"<<endl; } }; int main(void) { D *d=NULL; d->printA(); d->printB(); }
- 输出 printfA 后程序崩溃,调用虚函数是对象通过 this 指针去调对应函数的过程,这里就是野指针访问的问题了。
其他
- 运算符重载的意义是什么?哪些运算符不能被重载?
- 让 自定义类型对象 可以使用运算符,便捷,更是为了提高代码的可读性
- 五个不能重载的:
.*
::
sizeof
?:
.
- static 成员
- 类内定义、类外初始化
- 被整个类共享,和普通静态变量一样,放在静态数据区
- 友元
- 匿名对象
只存在一行,用作传值或赋值,可以减少一次拷贝构造。 - 编译器对参数和返回值的优化
- 拷贝构造 + 拷贝构造 -> 拷贝构造
- 构造 + 拷贝构造 -> 拷贝构造
三、🔺🔺模板
考察的不多,但是是C++学习的基石。
需要会写函数 or 类模板,检验语法掌握
- 模板分类?
- 函数模板,推演实例化
- 类模板,显示实例化
- 模板的原理是什么?(实例化的方式?)🔺
- 模板的实例化是针对模板参数进行具体的类或者函数的实例化
- 非类型模板参数的要求是什么?
- 必须要是常量
- 模板特化是什么?使用场景是什么?
- 经典举例就是,hash 表默认支持 string 做 key,就是特化的一个场景
- 为什么模板不支持分离编译?怎么解决,原理是什么?
- 因为用的地方要实例化,却只有声明;定义的地方只有模板没有实例化。
- 解决原理:放一个.hpp 里,包含这个头文件去使用
模板 Ⅰ 总结
<class / typedef T> 两种写法都可取,T 是自定义名称 通常大写,多个参数时,用逗号间隔。
通常来说 函数模板 是推演实例化;类模板 是显示实例化。
使用模板函数时也可以将 参数类型 显式实例化。
函数模板和同名函数可以同时存在,优先调用用户定义的 同名函数。 (有同名函数时,可以显式实例化调用函数模板)
模板的 声明或定义 只能在 全局、命名空间、类 范围内进行。即不能在局部范围,函数内进行,比如不能在 main 函数中声明或定义一个模板。
如果定义和声名分开写,声明 也需要标记 template。
模板的 定义 和 声明 尽量写在 一个文件中,可以分开方式但会很麻烦。(具体方式见下篇)
类模板的名称:类名 <参数>
模板 Ⅱ 总结
非类型形参 只能用 整形常量,在模板中作为 常量 来使用。 整型家族:int、size_t、short、long、char、bool… ( 浮点数、类对象 以及 字符串 是 不允许
作为非类型模板参数的)模板特化需要在有原模版的基础之上建立,分为全特化、偏特化,偏特化又有部分特化、进一步限制参数。
函数模板 的特化,绝大多数都可以直接使用 仿函数 去实现需要的功能。
模板的 定义 和 声明 尽量写在 一个文件中。
- 模板参数都有默认构造函数:有了模板后,为了适应自定义类型的默认构造函数,内置类型也可以认为有默认构造函数了。可以直接调模板参数的 匿名对象 T()。
// 举例 template<class T> struct list_node // 用 struct 也可以定义类,在我们需要将成员全部开放的时候,就用 struct { list_node<T>* _next; list_node<T>* _prev; T _data; // 构造节点 // 这里的 T() 是一个匿名对象,缺省值会取 T 的默认构造,包括内置类型(有了模板后,可以认为他们也有默认构造函数了) list_node(const T& x = T()) //需要提供一个缺省值,否则会提示,list_node 没有默认构造可以使用 :_next(nullptr), _prev(nullptr), _data(x) {} };
如果模板有 缺省参数,只能设置在一处,建议设在定义处。 两处都设置会报错(重定义默认参数)
不光是声明模板,友联一个模板类,也需要加上 template<xxx>。(毕竟友联也是一种声明)
没有被实例化的模板 取内置类型都要加上关键字
typename
使用,不然编译器报错(会不确定这个东西是 类模板里面的静态变量 还是 类型)
四、🔺🔺🔺继承和多态
超重点,各种考察
- 什么是继承?什么是多态?
- 多态举例:异常等等
函数重载,重写,隐藏(重定义)的对比
- 函数重载
- 两个函数必须在同一个类域中
- 函数名相同,参数类型、个数、顺序不同
- 重写
- 两个函数分别在基类和派生类的类域中
- 函数名、参数、返回值都必须相同(斜变除外,基类虚函数返回基类的对象的指针或引用,派生类返回派生类的指针或引用成为斜变)
- 重写的两个函数是虚函数
- 隐藏 / 重定义
- 两个函数分别在基类和派生类的类域中
- 函数名相同即可
- 重定义的两个函数不构成重载,构成隐藏
虚函数和虚表
- 原理
- 当类中定义虚函数的时候,相对应的对象就会多一个成员 _vfptr 指针,这个指针指向的是函数指针数组(虚函数表),这张表是用来放虚函数的地址的,每个虚函数的地址存放在这张表是固定的。
- 当派生类去重写基类的虚函数的时候,则派生类的虚函数表会将相对应的虚函数的地址给修改为派生类重写的虚函数的地址。
- 如果派生类没有重写基类的虚函数,自然虚表中就是存的基类的虚函数地址。
- 虚表中的最后一个值为 nullptr。
- 多继承派生类的虚表
- 一个派生类如果继承多个有虚函数的基类,那么该派生类就会存在多个虚函数表。每个虚函数表对应一个基类(及派生类重写后)的所有虚函数地址。
- 多继承的派生类的 非重写的虚函数 放在第一继承基类的虚函数表中。
- 对象中的虚表指针是在什么阶段的初始化呢?虚表又是在什么阶段生成的呢?
- 对象中的 虚表指针 是在构造函数的 初始化列表 阶段开始初始化的。
- 虚表 是在 编译 的时候生成的。
- 虚表是存在进程地址空间中的哪个区域的?(栈,堆等)
- 打印各个段的数据存放的地址,然后打印虚表地址,进行比对就可以得出虚表在哪个区域。(虚表的地址存放在虚表指针中,拿到虚表指针就可以打印出虚表地址。)
- 结果得出,虚表地址的代码段数据是最相近的,所以在 vs 编译器中,虚表是存在于 代码段 的。
- sizeof(Base1) 和 sizeof(Base2) 的大小?
class Base1 { public: void func1(){} private: int _a = 1; } class Base2 { public: virtual void func1(){} private: int _a = 1; }
- Base1 对象的大小是 4 个字节,Base2 对象的大小是 8 个字节,因为 Base2 中存在虚函数,所以 Base2 的对象最前面包含一个 4 个字节的虚表指针。
纯虚函数与抽象类
- 在虚函数的后面写上
=0
,则这个函数称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能定义出具体的对象,派生类继承后也不能定义出对象,只有派生类重写纯虚函数才能定义出对象。
继承后的成员权限(了解)?
派生类的默认成员函数(了解)
- 析构函数的重写(基类与派生类的析构函数名不同)
- 如果基类的析构函数是虚函数,那么无论派生类的析构函数是否定义为虚函数,派生类的析构函数都重写了基类的析构函数。(原理是在编译阶段,编译器对所有类的析构函数名称做了处理,统一命名为 destructor,这才能构成函数重载的条件。)
- 如果基类的析构函数为虚函数,而派生类未定义析构函数,编译器所生成的析构函数也为虚函数。
- 子类到父类对象之间的复制兼容转换
- 多继承 - - 菱形继承:菱形继承的问题是什么?如何解决?
- 虚继承
- 虚继承是如何解决菱形继承的?
- 切记不要跟虚函数多态混了,两个地方都有 virtual,但他们之间没有关联关系。
- 继承和组合使用情景?
- 符合 is a 就用 继承
- 符合 has a 用 组合
- 既符合 is 又符合 has 用 组合
多态的条件是什么?原理是什么?
- 构成多态的 两个条件:
- 调用的函数的参数必须时基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 多态的原理:
- 静态多态:在程序 编译期间 确定了程序的行为,比如:函数重载。
- 动态多态(动态绑定):即运行时的多态,在程序 执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
- 具体说来,当将基类的成员函数声明为 virtual 虚函数时,编译器在 编译阶段 就发现基类中有虚函数。此时编译器就会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数值中存放每个虚函数的地址,这个数组最后是一个 nullptr。如果一个类包含一张虚表,那么该类定义出来的对象中包含一个虚表指针。
- 由于不同的对象看到的虚表是不一样的,所以函数在调用的时候,会根据虚表中虚函数指针找到相对应的虚函数。
菱形继承会造成什么问题?解决方法是什么?
A
/ \
B C
\ /
D
-
会造成的问题:
- 二义性:如果类 B 和类 C 中都定义了相同的成员函数或变量,那么在类 D 中就会出现二义性,编译器无法确定应该使用哪个版本。
- 内存浪费:如果类 B 和类 C 中都包含类 A 的成员变量,而类 D 继承了类 B 和类 C,那么类 D 中就会包含两份相同的类 A 成员变量,造成内存浪费。
-
解决方法:
- 虚继承(Virtual Inheritance):基类被标记为虚基类,派生类只继承虚基类的一个实例,这样可以避免内存浪费和二义性。
- 虚拟继承的原理是在继承链中创建一个共享的基类子对象,以保证派生类只包含一份虚基类的成员。这样可以避免内存浪费和二义性,但也会增加一些运行时开销,因为需要额外的指针或偏移量来访问虚基类的成员。
class A { public: int data; }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {};
inline / static 是否可以是虚函数?
- inline 可以 定义为虚函数
- 不过 inline定义为虚函数后,编译器就会忽略其 inline 属性(即,没有函数地址,在调用处直接展开),这个函数就不再是 inline,因为虚函数要放到虚表中去。
- static 不可以 定义为虚函数
- 因为虚函数表在对象中,而静态成员函数属于类,不属于对象、没有 this 指针,使用类型::成员函数 的调用方式无法访问到虚函数表,所以静态成员函数无法放进虚函数表。
构造函数 可以是虚函数吗?
不能,因为虚函数表指针是在构造函数的初始化阶段才进行初始化的。
关于多态的代码阅读
- 以下程序的输出结果是什么:_________
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl; } virtual void test(){ func();} }; class B : public A { public: void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc, char* argv[]) { B* p = new B; p->test(); return 0; }
- 解析:p 是 B 类的指针,当他调用 test 函数时,test 函数的参数中有一个隐藏的 B 类的 this 指针,当 test 函数去调用 func 函数时,自然会去调用 B 类的 func,因为 func 重写了 A 类的虚函数,重写虚函数只是重写了函数内部的实现,不会对重写函数的声明,也就是 void func(int val=1) 不会被重写,因此 B 类的 func 函数是 void func(int val=1){ std::cout<<“B->”<< val <<std::endl; },输出结果是 B->1;