3. 变量、类型与类型转换
在C++面试中,变量与类型相关的问题几乎贯穿所有轮次。面试官关注的不是你是否背得出const的四种用法,而是你能否在真实代码中判断类型推导结果、识别未定义行为、解释类型转换的代价与风险。本章围绕基本类型、const、volatile、static、auto、decltype以及强制类型转换,结合典型面试题展开分析。
一、基本类型与对象模型:先分清“类型”与“对象”
C++的类型系统包含基本类型(int、double、char、bool等)、复合类型(指针、引用、数组、函数)以及用户自定义类型。面试常问:sizeof(int)是多少?标准并未固定为4字节,只保证sizeof(char)==1且sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)。在常见64位Linux/GCC环境下,int为4字节、指针为8字节,但这是平台相关事实,不是语言保证。
另一个高频陷阱是整型提升与符号问题。例如:
unsigned int a = 10;
int b = -20;
if (a + b > 0) {
std::cout << "positive";
} else {
std::cout << "negative";
}
由于a + b中b被转换为unsigned int,结果是一个极大的无符号数,输出为positive。面试中若被问到“这段代码输出什么”,正确回答需要指出:int与unsigned int混合运算时,int会按模运算转换为unsigned int,负值变成大正数。避免方式是在比较前显式转换或统一使用有符号类型。
二、const与volatile:语义约束与编译器假设
const的核心语义是“通过该名称不可修改”。它修饰变量、指针、引用、成员函数和返回值。面试常考指针的顶层与底层const:
const int* p1; // 指向常量的指针,*p1不可改,p1可改
int* const p2 = &x; // 常量指针,p2不可改,*p2可改
const int* const p3 = &x; // 两者都不可改
判断口诀是从右向左读:p1是pointer to const int,p2是const pointer to int。const成员函数则承诺不修改对象逻辑状态,mutable成员可例外。
volatile告诉编译器该对象可能被程序之外的因素改变,禁止优化掉对该对象的读写。它常用于内存映射寄存器、信号处理中的sig_atomic_t变量。但volatile不提供原子性,也不建立线程间同步。面试中若问“volatile能否替代std::atomic”,答案是否定的:volatile只保证编译器不合并或删除访问,不保证CPU层面的顺序与原子性。
三、static:生命周期、链接性与作用域
static在不同位置含义不同,面试中常要求分类说明:
- 函数内
static局部变量:生命周期贯穿程序运行,首次执行到声明处初始化,C++11起初始化线程安全。 - 全局/命名空间作用域
static变量或函数:内部链接,仅当前翻译单元可见。 - 类内
static成员:属于类而非对象,需在类外定义(C++17起可用inline static就地定义)。
一个典型面试题:统计函数被调用次数。
int counter() {
static int n = 0;
return ++n;
}
这里n只初始化一次,后续调用累加。若在多线程环境调用,++n本身不是原子操作,需要加锁或使用std::atomic<int>。
四、auto与decltype:类型推导的规则与差异
auto按模板参数推导规则工作,会忽略顶层const和引用(除非显式写auto&或const auto&)。例如:
const int x = 10;
auto a = x; // a为int,顶层const被丢弃
auto& b = x; // b为const int&
const auto c = x; // c为const int
decltype则保留完整类型,包括引用和const。对表达式decltype((x)),若x是变量名加括号,得到引用类型;decltype(x)得到变量声明类型。面试常考:
int i = 0;
decltype(i) a; // int
decltype((i)) b = i; // int&
decltype(i + 1) c; // int(右值表达式)
实际工程中,auto适合简化迭代器和lambda类型,decltype常用于返回类型推导(C++11尾置返回类型)和模板元编程。C++14的decltype(auto)则按decltype规则推导,保留引用与const,适合完美转发场景。
五、强制类型转换:四种cast的适用边界
C++提供四种显式转换,面试要求能说出各自用途与风险:
| 转换 | 典型用途 | 风险 |
|---|---|---|
static_cast | 相关类型间转换,如double转int、基类指针转派生类指针(下行,不检查) | 下行转换无运行时检查,可能未定义行为 |
dynamic_cast | 多态类型安全下行或交叉转换 | 需RTTI,有运行时开销,失败返回空指针或抛异常 |
const_cast | 移除或添加const/volatile | 修改原本为常量的对象是未定义行为 |
reinterpret_cast | 指针与整数、无关指针类型间转换 | 高度平台相关,极易破坏类型安全 |
面试题示例:
const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20; // 未定义行为
虽然语法合法,但x本身是const对象,写入是未定义行为。若原对象非const,仅通过const引用访问,则const_cast后修改是允许的。
六、类型安全陷阱与面试应对
常见陷阱包括:有符号与无符号比较、窄化转换、悬垂引用、auto推导出意外类型、decltype括号差异、reinterpret_cast破坏严格别名规则。面试中遇到“这段代码有什么问题”,建议按以下顺序检查:
- 是否存在隐式整型提升或符号转换?
auto是否丢失了const或引用?- 指针/引用是否可能悬垂?
- 强制转换是否越过了类型系统保证?
- 多线程下是否有数据竞争?
例如下面这段代码:
std::vector<int> v{1, 2, 3};
for (auto& e : v) {
e *= 2;
}
若写成for (auto e : v),则修改的是副本,原容器不变。面试官常借此考察你是否理解auto与引用的组合。
七、可执行检查清单
以下清单可直接用于代码审查或面试自测:
| 检查项 | 判断依据 |
|---|---|
| 混合运算类型 | 有符号与无符号混用前,是否显式统一类型? |
auto推导 | 是否需要const auto&以避免拷贝或丢失const? |
decltype括号 | 是否误用decltype((var))得到引用? |
const_cast | 原对象是否本身为const?写入是否未定义? |
static局部变量 | 多线程下是否需要原子或锁? |
volatile | 是否误以为它能保证原子性或线程同步? |
掌握这些规则后,面对“类型推导结果是什么”“这段代码是否安全”类问题,就能从语言标准与工程实践两个层面给出有依据的回答,而不是依赖记忆或猜测。