C++面试题总结
在 C++ 中,模板的定义通常需要放在头文件中,而非源文件中。因为模板是在编译期根据使用的类型进行实例化的,若放在源文件中,其他源文件无法看到模板的完整定义,会导致链接错误。为例,在 64 位机中,类的大小计算需考虑成员变量的大小、内存对齐以及是否有虚函数(虚函数会带来虚函数表指针,占 8 字节)。2.当线程被notifiy_one或者notify_all唤醒,会立即重新获取锁,且wait的解锁、
1. 不能重载的运算符
在 C++ 里,并非所有运算符都能被重载,不能重载的运算符及其原因如下:
.
(成员访问运算符):用于访问对象的成员,若重载可能导致对象成员访问方式混乱,破坏封装性,编译器也无法确保访问的正确性。.*
(成员指针访问运算符):用于通过成员指针访问对象成员,逻辑复杂且与对象内存布局紧密相关,重载可能引发内存访问错误。::
(作用域运算符):用于区分不同作用域的标识符(如类、命名空间),其功能是编译器的作用域解析,不涉及运行时操作,重载无实际意义。? :
(条件运算符):是三目运算符,语法结构特殊(需要三个操作数),且运算逻辑与流程控制相关,重载会导致语法解析困难,破坏代码可读性。sizeof
(长度运算符):用于计算类型或变量的大小,其结果在编译期确定,与具体对象无关,重载会改变基本类型长度的计算规则,导致系统级错误。typeid
(类型识别运算符):用于获取变量或表达式的类型信息,是 C++ 运行时类型识别(RTTI)的核心机制,重载会破坏类型系统的一致性。
2. 函数重载判断依据
在 C++ 中,能作为函数重载判断依据的有:
- 参数类型:例如
void func(int x)
和void func(double x)
,因参数类型不同可构成重载。 const
:例如void func(int* ptr)
和void func(const int* ptr)
,因const
修饰符不同可构成重载。- 参数个数:例如
void func(int x)
和void func(int x, int y)
,因参数个数不同可构成重载。
而返回类型不能作为函数重载的判断依据,因为仅返回类型不同的函数,编译器无法在调用时区分。
3. list 删除元素与迭代器失效
在 C++ 中,list
删除任意一个元素不会导致其他迭代器失效。因为list
是双向链表结构,被删除元素两边的元素的迭代器指针会自动更新,其他迭代器不受影响。
4. C++ 中的类型转换
static_cast
(基础转换):用于基础类型之间的转换,如double d = static_cast<double>(42);
(整数转双精度浮点数)。dynamic_cast
(多态下行转换):用于多态场景下的下行转换,需类有虚函数,例如:class Base { virtual void f() {} }; class Derived : public Base {}; Base* b = new Derived; Derived* d = dynamic_cast<Derived*>(b);
const_cast
(去除const
):用于去除指针或引用的const
属性,例如const int* cptr = new int(10); int* ptr = const_cast<int*>(cptr);
。reinterpret_cast
(底层二进制转换):用于底层二进制位的重新解释转换,例如int* p = new int(5); long addr = reinterpret_cast<long>(p);
(指针转数值表示存储地址)。
5. 引用的本质
引用的本质是对指针的封装,底层是通过const
的指针实现的,因此本身引用具有const
语义,不能重新绑定。引用本质如int &ref = a;
等价于int * const ptr = &a;
。
6. 非静态成员函数与this
指针
在 C++ 中每个非静态成员函数都隐含一个指向当前对象的this
指针,其类型由成员函数是否为const
决定。非const
成员函数的this
指针类型为类类型* const
,可以修改类的成员变量;而const
成员函数的this
指针类型为const 类类型* const
,不能修改非mutable
修饰的类内变量,只能修改用mutable
修饰的变量(const
必须放在函数的后面,且只有类内的函数可以有这个const
)。例如:
class MyClass {
private:
int a; // 非mutable成员(受const函数限制)
mutable int b; // mutable成员(不受const函数限制)
public:
MyClass(int a_, int b_) : a(a_), b(b_) {}
// 非const成员函数,this指针类型为 MyClass* const
void modifyNonConst() {
a = 100; // 合法,通过非const this修改非mutable成员
b = 200; // 合法,修改mutable成员
}
// const成员函数,this指针类型为 const MyClass* const
void modifyConst() const {
// a = 300; // 非法,const this不能修改非mutable成员
b = 400; // 合法,mutable成员不受const限制
}
void print() const {
std::cout << "a = " << a << ", b = " << b << std::endl;
}
};
7. 万能引用与完美转发
万能引用是指用&&
从模板参数推导T&&
创造的右值引用或左值引用的类型。当使用完美转发时,T&&
会折叠为左值引用(当传入左值时)或右值引用(当传入右值时)。例如:
#include <iostream>
using namespace std;
// 模板函数中的万能引用(能推导的类型)
template <typename T>
void func(T&& x) { // x 是万能引用
cout << "确定成功" << endl;
}
int main() {
int a = 30;
func(a); // 万能引用绑定左值a,正确
func(30); // 万能引用绑定右值30(临时对象),正确
func(a + 7); // 万能引用绑定右值(表达式结果是临时对象),正确
return 0;
}
8. 类的大小计算
以class A
为例,在 64 位机中,类的大小计算需考虑成员变量的大小、内存对齐以及是否有虚函数(虚函数会带来虚函数表指针,占 8 字节)。
- 不含
virtual
的类:- 类中成员变量按声明顺序,遵循内存对齐规则排列,填充字节用于满足对齐要求。静态成员变量和非虚成员函数不占类的实例内存。
- 以示例类为例,
int64_t A
占 8 字节(地址 0 - 7),short B
占 2 字节(地址 8 - 9),char C
占 1 字节(地址 10),之后为满足void* p
(占 8 字节,需 8 字节对齐),填充 5 字节(地址 11 - 15),void* p
占 8 字节(地址 16 - 23),最终类大小需根据各成员及填充情况,按最大对齐数(此处为 8)调整为整数倍。
- 含
virtual
的类:- 会额外增加一个
vptr
(虚函数表指针,8 字节),通常位于类内存最开始位置。 - 同样按成员声明顺序和内存对齐规则计算,
vptr
占 8 字节(地址 0 - 7),之后int64_t A
占 8 字节(地址 8 - 15),short B
占 2 字节(地址 16 - 17),char C
占 1 字节(地址 18),为满足void* p
(8 字节对齐),填充 5 字节(地址 19 - 23),void* p
占 8 字节(地址 24 - 31),总大小按最大对齐数(8)调整,最终为 32 字节。
- 会额外增加一个
9. 多线程相关说法判断
下面的说法是否正确以及为什么?
1.使用condition_variable必须搭配互斥锁来保持临界区,通常配合lock_guard
2.当线程被notifiy_one或者notify_all唤醒,会立即重新获取锁,且wait的解锁、阻塞、唤醒、加锁是原子性的
3.调用wait(lock,predicate)时,必须先持有锁。该函数会检查predicate的条件,日光返回true则继续执行,否则就释放锁并阻塞线程直到被唤醒。
4.wait函数底层实现是一个if
-
错误。
condition_variable
需搭配unique_lock<mutex>
,而非lock_guard
。因wait
需临时释放锁再重新获取,lock_guard
不支持手动解锁 / 加锁,无法满足需求。 -
正确。
wait
的 “解锁→阻塞” 是原子操作,避免通知丢失;被唤醒后会立即重新获取锁,保证线程安全。 -
正确。调用
wait(lock,predicate)
需先持有锁,若条件为true
则继续;否则释放锁并阻塞,直到被唤醒后重新检查。 -
错误。
wait
底层用while
循环而非if
,以处理虚假唤醒,确保条件满足才继续执行。例如: -
while (!predicate()) { cv.wait(lock); }
16. 模板的定义位置
在 C++ 中,模板的定义通常需要放在头文件中,而非源文件中。因为模板是在编译期根据使用的类型进行实例化的,若放在源文件中,其他源文件无法看到模板的完整定义,会导致链接错误。例如将模板add
的定义放在头文件template_example.h
中:
// template_example.h
#ifndef TEMPLATE_EXAMPLE_H
#define TEMPLATE_EXAMPLE_H
template <typename T>
T add(T a, T b) {
return a + b;
}
#endif
在main.cpp
中包含该头文件并实例化模板:
#include "template_example.h"
#include <iostream>
int main() {
std::cout << add(2, 3) << std::endl; // 实例化int版本
std::cout << add(2.5, 3.5) << std::endl; // 实例化double版本
return 0;
}
17. vector
的reserve
与resize
reserve(100)
的作用:reserve(n)
的功能是预先分配至少能容纳n
个元素的内存空间,执行后v.capacity()
的值必然≥100(若原容量小于 100,则扩容至 100;若原容量已≥100,则不改变)。resize(50)
的作用:resize(n)
的功能是调整容器中实际元素的数量为n
(若n
小于当前元素数,则删除多余元素;若n
大于当前元素数,则添加默认构造的元素)。但resize
不会改变容器的容量(capacity
),更不会减小容量。
更多推荐
所有评论(0)