面试题
c++
#define与inline区别:
特性 | #define (宏) |
inline (内联函数) |
---|---|---|
处理阶段 | 预处理阶段(文本替换) | 编译阶段(编译器决策) |
类型检查 | 无类型检查,直接文本替换 | 有类型检查,遵循函数参数规则 |
作用域 | 全局(从定义处开始生效直到#undef) | 遵循作用域规则(类内、命名空间等) |
调试支持 | 替换后代码难以调试 | 可生成调试符号(即使内联失败) |
安全性 | 易因运算符优先级或副作用导致错误 | 行为与普通函数一致,更安全 |
auto
auto类型推导通常与模板类型推导相同,但是auto可以推导{}是初始值列表
auto必须初始化,不然编译错误
auto可以使用尾随返回类型,省略类中枚举类型的作用域
auto推导两个bool相加为int,对bool类型提升了
decltype
decltype
是一个关键字,用于查询变量或表达式的类型。具体应用与函数模板中推导复杂的返回类型,例如:
1 | template<typename Func, typename... Args> |
c++14后可以将上面尾随返回类型替换成decltype(auto)用来推导return返回的类型
内存分区
在C++中,内存主要分为以下五个分区,每个分区负责管理不同类型的数据,具有不同的生命周期和访问
分区 | 分配方式 | 生命周期 | 管理权限 | 典型数据 | 补充 |
---|---|---|---|---|---|
栈 | 自动 | 函数作用域 | 编译器 | 局部变量、参数 | 1.自动分配和释放效率高;2.内存容量有限(默认约几MB),过度使用可能导致栈溢出(如递归过深)。 |
堆 | 手动 | 直到显式释放 | 程序员 | new /malloc 申请的内存 |
容量大(受系统虚拟内存限制),但管理不当会导致内存泄漏或碎片化。 |
全局/静态 | 程序启动时 | 整个程序运行期 | 编译器/系统 | 全局变量、static 变量 |
.data段:已初始化的全局/静态变量。 .bss段:未初始化的全局/静态变量(默认初始化为0) |
常量区 | 编译时初始化 | 程序运行期 | 只读 | 字符串常量、const 全局变量 |
只读,修改会导致段错误(如尝试修改字符串常量) |
代码区 | 编译时 | 程序运行期 | 只读 | 函数代码、指令 | 只读,防止程序被意外修改。 可能被多个进程共享(如动态库代码)。 |
static
1、静态成员变量
static静态成员不属于类实例,在.data/.bss分配内存,在程序的生命周期内仅分配一次,所有对象共享一份;因此,在类中只有声明,必须在类外部的全局作用域中显式定义静态成员。
2、静态成员函数:
特性 | 成员函数 | 静态成员函数 |
---|---|---|
调用方式 | 需通过对象调用(obj.func() ) |
可直接通过类名调用(MyClass::func() ) |
this 指针 |
有(隐式传递对象地址) | 无 |
访问权限 | 可访问非静态和静态成员 | 仅能访问静态成员 |
用途 | 操作对象的具体状态 | 执行与类相关的通用操作 |
静态成员不能是虚函数,const |
3、静态初始化顺序:
在不同单元.cpp中,全局静态变量初始化顺序是随机,倘若A依赖与B,A初始化时可能B没初始化,获取不到B内容
解决:可以将B放在函数中变成静态局部变量,被调用时即初始化
重载,重写,覆盖区别
当有两个函数名称相同、返回类型相同,但参数数量或类型,或者限定符不同时,就构成了函数重载。
基类有virtual修改函数,子类重新实现这个虚函数即重写,只能函数类容不一致,可以通过override说明符确保没有错误地重写基类函数。
在子类重新定义一个同名的函数,函数名相同,其他都可以不同,这样即覆盖
虚函数
子类重写基类的virtual修饰的函数,用实现多态,返回类型,函数名,函数参数类型以及个数必须都一致,不然变成覆盖了
对象的前4个或8个是一个vptr,指向vtable,每个类中有一张虚表,虚表存放着每一个虚函数指向的函数指针,当基类指针指向派生类时,vptr指向派生类的虚表,调用虚函数时就去虚表寻找真正绑定的函数指针,这个过程叫做动态绑定。
1、将基类析构函数声明为虚函数可确保派生类对象被正确析构,即基类和派生类的析构函数都会被调用
2、构造函数不能为虚函数:子类构造时先构造基类,这时候子类对象未实例化,根本无法动态绑定
协变返回类型
是工厂模式的重要概念,表现为:produce函数返回基类指针,指向new分配的子类对象
虚继承
虚继承是一种C++技术,它确保孙子派生类仅继承基类成员变量的一份副本。用来解决菱形继承问题
当虚继承时会生成一个vbptr虚基表指针,指针指向虚基表,虚基表存放基类的偏移量,通过这个机制来保证只有一份基类成员变量
虚继承的顺序必须正确才有效,即当前D类继承两个类B,C发生了领先继承问题时,才让B,C类虚继承A类
D类对象的内存布局是这样的:{ B{ vbptr, b }, C{ vbptr, c} d, A{ a } }
私有继承
继承的访问说明符不会影响实现的继承。实现总是基于函数的访问级别进行继承。继承的访问说明符仅影响类接口的可访问性。
继承用于表达 “是一个(is - a)” 的关系,私有继承将这种关系变为”用来父类实现(is-implemented-in-terms-of)” 关系。
表现为:子类私有继承父类后,父类的所有成员变量和函数变为私有,只能通过子类实现接口调用父类的成员,父类和子类没有公开继承关系,无法将派生类指针隐式转换为基类指针
=delete说明符
从C++11开始,我们可以使用delete
说明符来限制某些类型的拷贝、移动,实际上甚至可以限制其多态使用。 但是= delete
的用途不止于此。它可以应用于任何函数,无论是成员函数还是自由函数。 例如,我不想要add函数接收double类型的参数,可以使用delete删除重载的版本
1 | double add(double, double) = delete; |
lambda
立即调用lambda函数,通过这种方式,你可以对const
变量进行复杂的初始化
捕获:auto l = [&args...] { return g(args...);};
模板变参
c语言支持函数变参,通过va_list, va_start, va_arg, va_end系列函数获取参数,但是函数并不知道到参数的具体类型,va_arg获取参数的原理是按照类型去通过指针去参数列表取得的,只适合用于POD类型
c++实现了变参模板,即同时传递变量和类型;模板参数包和函数参数包可以将多个参数打包看作一个参数,然后在模板推导时通过包扩展将其解包。…在变量前则声明参数包,在变量后则进行参数解包
1 | template<typename ...Arg> |
如何才能利用模板参数包及包扩展,使得模板能够接受任意多 的模板参数,且均能实例化出有效的对象呢?
通过递归的方法,参套包含,并且指定边界条件
1 | template<typename... T> class tuple; |
聚合初始化
使用初始值列表 {} 进行初始化有三个好处:
- 不会出现解析问题,因为c++中任何能被解释为声明的内容,都会被解释为声明。Myclass a()则会被视为函数声明
- 当出现窄化转换时,编译错误
- 直接初始化容器,不再需要一个一个插入
用户定义字面量
用户定义字面量允许通过定义用户定义的后缀,让整数、浮点数、字符和字符串字面量生成用户定义类型的对象。
1 | constexpr int operator"" _pow(int x) { return x*x; } |
用户定义字面量可用于整数、浮点数、字符和字符串类型的转换。
用户定义字面量可用于辅助强类型,例如相比于直接传数值给构造函数,可以像数值转换成对应的对象指标
类型别名
使用using代替typedef的好处:
- 在函数指针的情况下,别名声明的可读性更强:
typedef
不支持模板化,而别名声明支持。
RAII
资源获取即初始化,这里所说的资源包括:
- 分配的堆内存;
- 执行线程;
- 打开的套接字;
- 文件;
- 锁定的互斥锁;
- 磁盘空间;
- 数据库连接。
但RAII不仅关乎资源获取,还涉及资源释放。RAII还确保在控制对象的生命周期结束时,所有资源都会以获取的相反顺序被释放。同样,当一个对象的资源获取失败时,该对象或其任何成员已经成功获取的所有资源都必须以相反顺序释放。
另一方面,如果你考虑原始指针,它们并不遵循RAII概念。当一个指针超出作用域时,它不会自动被销毁,你必须在它丢失并造成内存泄漏之前手动删除它。而标准库中的智能指针(std::unique_ptr
、std::shared_ptr
)提供了这样一种封装机制。
智能指针
unique_ptr:独享对象所有权的智能指针,禁用拷贝构造和赋值函数,但可以通过右值引用进行拷贝,原来的对象指针变为nullptr
默认情况下,你应该选择std::unique_ptr
。它是一种小巧、快速且仅支持移动操作的智能指针,用于管理具有独占所有权语义的资源。
C++14引入了std::make_unique
来简化创建过程,新的指针创建方式更安全,防止将同一个原始指针传递给两个新的unique_ptr
shared_ptr:共享对象所有权的智能指针,通过过share_count计数实现拷贝,当析构时计数为0才真正删除所指内存
与std::unique_ptr
或原始指针相比,std::shared_ptr
对象通常要大两倍,因为它们不仅包含一个原始指针,还包含另一个指向动态分配内存区域的原始指针,在这个区域进行引用计数。
通过std::make_shared
,更加安全,因为这样可以避免意外地传入同一个原始指针两次,而且还能避免为引用计数内存进行动态分配的开销。避免从原始指针类型的变量创建std::shared_ptr
,因为这样难以维护,也很难判断所指向的对象何时会被销毁。
使用std::shared_ptr
进行共享所有权的资源管理。
weak_ptr:share_ptr的弱引用,使用时必须转换为share_ptr,解决环状引用
环状引用:当a析构时A的计数为1,b析构时B的计数为1,造成内存泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13 class B;
class A{
public:
shared_ptr<B> pb;
};
class B{
public:
shared_ptr<A> pa;
}
shared_ptr<A> a(new A);
shared_ptr<B> b(new B);
a->pb = b;
b->pa = a;
弱指针std::weak_ptr
是一种智能指针,它不会影响对象的引用计数,因此它所指向的对象可能已经被销毁。
如果使用共享指针,它们之间会形成循环依赖,导致无法被销毁,进而产生资源泄漏。weak_ptr解决这种情况。
它在缓存和观察者模式中也很有用。
std::enable_shared_from_this
在类中返回包裹当前对象(this)的一个std::shared_ptr对象给外部使用
陷阱一:不应该共享栈对象的 this 给智能指针对象,智能指针管理的是堆对象
陷阱二:避免std::enable_shared_from_this的循环引用问题
new和make_share和make_unique比较
与直接使用new
相比,make
函数消除了源代码的重复,提高了异常安全性,并且std::make_shared
生成的代码更简洁、运行速度更快。
当使用
new
时,如果在构造过程中抛出异常,在某些情况下可能会导致资源泄漏,因为此时指针还没有被make
函数“处理”。std::make_shared
也比直接使用new
更快,因为它只分配一次内存来存储对象和用于引用计数的控制块。而使用new
则需要进行两次内存分配(即分配对象又要分配智能指针)。make函数避免创建两份智能指针内存指向同一个原始指针,不仅更加安全还减少内存分配
share_ptr和weak_ptr实现原理:shared_ptr封装了原生指针和一个计数块cblock,可以看到share_ptr通过shared_count计数,并且析构后如果还有weak_ptr在观察,则不会释放cblock;而weak_ptr想要使用必须通过lock转换share_ptr并且在data_指针存在时才能转换
1 | struct cblock { |
左值、右值:
c++表达式有两个特性:
- has identity? —— 是否有唯一标识,比如地址、指针。有唯一标识的表达式在 C++ 中被称为 glvalue(generalized lvalue)。
- can be moved from? —— 是否可以安全地移动(编译器)。可以安全地移动的表达式在 C++ 中被成为 rvalue。
根据这两个特性,可以将表达式分成 4 类:
- has identity and cannot be moved from - 这类表达式在 C++ 中被称为 lvalue。左值
- has identity and can be moved from - 这类表达式在 C++ 中被成为 xvalue(expiring value)。将亡值
- does not have identity and can be moved from - 这类表达式在 C++ 中被成为 prvalue(pure rvalue)。纯右值
- does not have identity and cannot be moved -C++ 中不存在这类表达式。
右值引用,引用折叠,完美转发
右值引用可以直接移动给左值对象,而不需要进行开销较大的深拷贝(deep copy)。
移动语义通过std::move
实现,它返回一个右值引用,而右值引用是移动操作的候选对象
c++类增加了移动构造函数和移动赋值操作符来实现对象转移,当一个函数返回一个对象时编译器会优先寻找移动构造函数,再去寻找拷贝构造函数
如果函数模板参数的类型为T&&
(T
为推导类型),或者对象使用auto&&
声明,那么这个参数或对象就是一个万能引用。当传入类型时发生引用折叠:只有&& &&才会折叠成右值引用
完美转发使函数参数保持原来的类型,因为当形参为右值引用,传入实参后,在函数中就有了标识,变成了左值,失去了原本右值引用的类型
constexpr
constexpr修饰的表达式一定是常量表达式,并且在编译时期求值
constexpr
函数总是线程安全的,并且会被内联(inlined)。
noexcept
六个生成的特殊函数是隐式的noexcept
函数。
如果在函数声明了noexcept
的情况下仍然抛出异常,就会调用std::terminate
。
正如C++核心准则所指出的,当程序崩溃比实际处理异常更好时,你可以使用noexcept
。
struct和class区别
在C++中,类和结构体之间的区别很小,表现为:结构体中成员变量和方法的默认访问权限是公共(public)的,而类中是私有(private)的
C语言中,结构体不支持函数(或方法)。
string_view
c++17引入新目标,在处理大型字符串时避免不必要的复制。典型的实现需要两个信息:指向字符序列的指针和字符序列的长度。字符序列可以是C++字符串,也可以是C风格字符串。
view一词说明其不包含字符串的管理权,相当引用,string则拥有内存。
主要用于访问和比较字符串内容。
不过,它有一个缺点。由于在底层,你可能使用std::string
或std::string_view
,这样就失去了隐式的空字符终止。如果你需要这个特性,就必须继续使用std::string (const&)
。
pimpl
即Pointer to Implementation(也有人认为是Private Implementation):将类中敏感的成员变量和成员函数封装到类中定义的impl类里或结构中,只留下一个指向impl的指针pimpl
这个方法的优点:
核心数据成员被隐藏;
核心数据成员被隐藏,不必暴露在头文件中,对使用者透明,提高了安全性。
降低编译依赖,提高编译速度;
由于原来的头文件的一些私有成员变量可能是非指针非引用类型的自定义类型,需要在当前类的头文件中包含这些类型的头文件,使用了pimpl惯用法以后,这些私有成员变量被移动到当前类的cpp文件中,因此头文件不再需要包含这些成员变量的类型头文件,当前头文件变“干净”,这样其他文件在引用这个头文件时,依赖的类型变少,加快了编译速度。
接口与实现分离。
使用了pimpl惯用法之后,即使CSocketClient或者Impl类的实现细节发生了变化,对使用者都是透明的,对外的CSocketClient类声明仍然可以保持不变。例如我们可以增删改Impl的成员变量和成员方法而保持SocketClient.h文件内容不变;如果不使用pimpl惯用法,我们做不到不改变SocketClient.h文件而增删改CSocketClient类的成员。
C++注解标签
1. #pragma
(编译器指令)
- **
#pragma once
**:替代头文件保护宏,防止重复包含; - **
#pragma pack(n)
**:控制结构体/类的内存对齐(字节对齐); - **
#pragma message("text")
**;在编译时输出自定义消息;
2. __declspec
(MSVC 特有)
- **
__declspec(dllexport/dllimport)
**:标记 DLL 导出/导入的类或函数; - **
__declspec(align(n))
**:指定内存对齐(C++11 后可用alignas
替代);
3. __attribute__
(GCC/Clang 特有)
- **
__attribute__((packed))
**:取消结构体对齐(紧凑内存布局); - **
__attribute__((aligned(n))
**:指定对齐方式(C++11 后可用alignas
替代);
C++11 引入了标准化的属性语法 [[attribute]]
,逐步替代部分编译器扩展:
- **
[[nodiscard]]
**:返回值不可忽略。 - **
[[deprecated]]
**:标记废弃。 - **
[[maybe_unused]]
**:抑制未使用警告。 - **
[[noreturn]]
**:函数不会返回。 - **
[[fallthrough]]
**:允许 switch-case 穿透。
函数调用的三种约定
__ cdecl、__ stdcall、__fastcall是C/C++里中经常见到的三种函数调用方式。
我们常用的函数调用方式有**__cdecl、__stdcall,C++的非静态成员函数的调用方式是__thiscall**,这些调用方式,函数参数的传递本质上是函数参数的入栈的过程,而这三种调用方式参数的入栈顺序都是从右往左的
__cdecl是C/C++默认的调用方式
_stdcall是windows API函数的调用方式,WINAPI宏代替
区别:两者参数都是参右向左入栈,void f(int a, int b)即push b,push a,此时esp指向变了,而__cdecl会在ecall指令返回后执行add esp 8;__stdcall则是在函数ret时不带参数返回esp需要增加的量,少了一条指令
__fastcall快速调用方式。这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。但是需要使用ecx和edx时由callee保证将数据放入内存,返回时放回寄存器
操作系统
gdb
命令名称 | 命令缩写 | 命令说明 |
---|---|---|
run | r | 运行一个程序 |
continue | c | 让暂停的程序继续运行 |
break | b | 添加断点 |
条件断点 | break [lineNo] if [condition]或者condition 断点编号 断点触发条件 | |
tbreak | tb | 添加临时断点 |
backtrace | bt | 查看当前线程的调用堆栈 |
frame | f | 切换到当前调用线程的指定堆栈 |
info | info | 查看断点/线程等信息 |
enable | enable | 启用某个断点 |
disable | disable | 禁用某个断点 |
delete | del | 删除断点 |
list | l | 显示源码 |
p | 打印或修改变量或寄存器值,set print element 0完整打印字符串 | |
ptype | ptype | 查看变量类型 |
thread | thread | 切换到指定线程 |
next | n | 运行到下一行 |
step | s | 如果有调用函数,进入调用的函数内部,相当于step into |
until | u | 运行到指定行停下来 |
finish | fi | 结束当前调用函数,到上一层函数调用处 |
return | return | 结束当前调用函数并返回指定值,到上一层函数调用处 |
jump | j | 将当前程序执行流跳转到指定行或地址 |
disassemble | dis | 查看汇编代码 |
set args | 设置程序启动命令行参数 | |
show args | 查看设置的命令行参数 | |
watch | watch | 监视某一个变量或内存地址的值是否发生变化,数据断点 |
display | display | 监视的变量或者内存地址,当程序中断后自动输出监控的变量或内存地址 |
dir | dir | 重定向源码文件的位置 |
set scheduler-locking on/step/off | ||
set follow-fork mode parent/child |