C++ 基础常见面试题总结(上)
本文采用面试问答形式,系统梳理 C++ 基础知识中最高频的考点。⭐ 标记为高频重点题。
一、基础概念与常识
C++ 语言有哪些特点?
- 面向对象:支持封装、继承、多态三大特性。
- 高性能:编译为机器码,无虚拟机开销,适合系统级开发。
- 兼容 C:几乎完全兼容 C 语言,可直接调用 C 的库。
- 多范式:同时支持面向过程、面向对象、泛型编程、函数式编程。
- 零成本抽象:模板、内联等机制在编译期展开,运行时没有额外开销。
- 手动内存管理:开发者可以精确控制内存分配和释放(也可借助智能指针自动管理)。
- 标准库丰富:STL 提供了容器、算法、迭代器等大量工具。
⭐ 编译型语言和解释型语言的区别?C++ 属于哪种?
| 特性 | 编译型语言 | 解释型语言 |
|---|---|---|
| 执行方式 | 先编译为机器码,再执行 | 逐行解释执行 |
| 执行速度 | 快 | 相对慢 |
| 可移植性 | 需针对不同平台编译 | 通常跨平台 |
| 代表语言 | C++、C、Rust、Go | Python、JavaScript、Ruby |
C++ 是编译型语言。源代码经过预处理 → 编译 → 汇编 → 链接,最终生成可执行的二进制文件。
⭐ C++ 程序的编译过程是怎样的?
源代码(.cpp/.h) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接 → 可执行文件
| 阶段 | 说明 | 产物 |
|---|---|---|
| 预处理 | 展开 #include、#define、条件编译 | .i 文件 |
| 编译 | 语法分析、语义分析、优化,生成汇编代码 | .s 文件 |
| 汇编 | 将汇编代码转为机器码(目标文件) | .o / .obj 文件 |
| 链接 | 合并目标文件,解析外部符号,链接库 | 可执行文件 |
bash
# 可以用 g++ 分步查看:
g++ -E main.cpp -o main.i # 预处理
g++ -S main.i -o main.s # 编译
g++ -c main.s -o main.o # 汇编
g++ main.o -o main # 链接
静态链接和动态链接的区别?
| 特性 | 静态链接 | 动态链接 |
|---|---|---|
| 时机 | 编译时 | 运行时 |
| 库文件 | .a(Linux)/ .lib(Windows) | .so(Linux)/ .dll(Windows) |
| 可执行文件大小 | 较大(包含库代码) | 较小(引用共享库) |
| 内存占用 | 每个程序各自一份 | 多个程序共享一份 |
| 更新维护 | 需重新编译 | 替换库文件即可 |
⭐ C++ 和 C 的区别?
| 特性 | C | C++ |
|---|---|---|
| 编程范式 | 面向过程 | 多范式(面向对象+泛型+函数式) |
| 封装 | struct(无访问控制) | class / struct + public/private/protected |
| 继承/多态 | 不支持 | 支持(虚函数、纯虚函数) |
| 内存管理 | malloc / free | new / delete + 智能指针 |
| 函数重载 | 不支持 | 支持 |
| 引用 | 不支持 | 支持 & |
| 模板 | 不支持 | 支持(函数模板、类模板) |
| 异常处理 | 不支持 | try / catch / throw |
| 标准库 | stdio.h 等 | STL(容器、算法、迭代器) |
| 命名空间 | 不支持 | namespace |
| 布尔类型 | C99 引入 _Bool | 内置 bool |
⭐ C++ 和 Java 的区别?
| 特性 | C++ | Java |
|---|---|---|
| 编译/运行 | 编译为机器码 | 编译为字节码,JVM 解释执行 |
| 内存管理 | 手动(+ 智能指针) | 垃圾回收(GC) |
| 指针 | 支持原始指针 | 无指针(只有引用) |
| 多继承 | 支持 | 不支持(用接口替代) |
| 运算符重载 | 支持 | 不支持 |
| 模板/泛型 | 模板(编译期展开,零开销) | 泛型(类型擦除) |
| 头文件 | 需要 .h 头文件 | 不需要 |
| 预处理器 | #define、#include 等 | 无 |
| 性能 | 通常更高 | 略低(JIT 可优化) |
二、基本语法
注释有哪几种形式?
cpp
// 1. 单行注释
int a = 10; // 行尾注释
/* 2. 多行注释(块注释) */
/*
* 这是
* 多行注释
*/
/**
* 3. Doxygen 文档注释(用于生成 API 文档)
* @brief 计算两数之和
* @param a 第一个数
* @param b 第二个数
* @return 两数之和
*/
int Add(int a, int b) { return a + b; }
⭐ C++ 中的关键字有哪些?
C++ 共有约 90+ 个关键字,以下按类别列出最常用的:
| 类别 | 关键字 |
|---|---|
| 数据类型 | int char float double bool void long short unsigned signed auto |
| 类与对象 | class struct union enum this friend operator virtual override final |
| 访问控制 | public private protected |
| 流程控制 | if else switch case default for while do break continue return goto |
| 类型修饰 | const constexpr volatile mutable static extern register inline |
| 内存管理 | new delete sizeof alignof alignas |
| 异常处理 | try catch throw noexcept |
| 模板 | template typename concept requires |
| 命名空间 | namespace using |
| 类型转换 | static_cast dynamic_cast const_cast reinterpret_cast |
| 其他 | nullptr decltype typedef typeid explicit export co_await co_yield co_return |
⭐ #include <xxx> 和 #include "xxx" 的区别?
| 形式 | 搜索路径 | 用途 |
|---|---|---|
#include <iostream> | 系统/标准库头文件目录 | 引用标准库和第三方库 |
#include "myheader.h" | 先搜索当前目录,再搜索系统目录 | 引用自定义头文件 |
⭐ 声明(declaration)和定义(definition)的区别?
| 声明 | 定义 | |
|---|---|---|
| 作用 | 告诉编译器某个名字的存在和类型 | 为名字分配存储空间或提供实现 |
| 内存 | 不分配内存 | 分配内存 |
| 次数 | 可以多次声明 | 只能定义一次(ODR 原则) |
cpp
// 声明(不分配内存)
extern int globalVar; // 变量声明
void Foo(int x); // 函数声明
class MyClass; // 前向声明
// 定义(分配内存 / 提供实现)
int globalVar = 42; // 变量定义
void Foo(int x) { /*...*/ } // 函数定义
class MyClass { /*...*/ }; // 类定义
⭐ struct 和 class 的区别?
在 C++ 中,struct 和 class 几乎完全相同,唯一的区别是 默认访问权限:
struct | class | |
|---|---|---|
| 默认成员访问权限 | public | private |
| 默认继承方式 | public 继承 | private 继承 |
cpp
struct A {
int x; // 默认 public
};
class B {
int x; // 默认 private
};
struct C : A {}; // 默认 public 继承
class D : A {}; // 默认 private 继承
⚠️ 习惯约定:
struct通常用于简单的数据聚合(POD 类型),class用于有复杂行为的类。
⭐ 自增自减运算符 i++ 和 ++i 的区别?
cpp
int i = 5;
int a = i++; // a = 5, i = 6(先取值,后自增)
int b = ++i; // b = 7, i = 7(先自增,后取值)
i++(后置) | ++i(前置) | |
|---|---|---|
| 返回值 | 返回自增前的旧值(临时对象) | 返回自增后的新值(引用) |
| 效率 | 需要创建临时对象,稍慢 | 无临时对象,更高效 |
| 建议 | 需要旧值时使用 | 优先使用(特别是迭代器) |
cpp
// 对于自定义类型(如迭代器),前置更高效
for (auto it = vec.begin(); it != vec.end(); ++it) { // 推荐 ++it
// ...
}
sizeof 运算符的常见结果?
cpp
// 基本类型(64 位系统常见值)
sizeof(char) // 1
sizeof(short) // 2
sizeof(int) // 4
sizeof(long) // 4 或 8(平台相关)
sizeof(long long) // 8
sizeof(float) // 4
sizeof(double) // 8
sizeof(bool) // 1
sizeof(void*) // 8(64 位)/ 4(32 位)
// 数组
int arr[10];
sizeof(arr) // 40(10 * 4)
sizeof(arr) / sizeof(arr[0]) // 10(元素个数)
// ⚠️ 数组退化为指针后
void Foo(int arr[]) {
sizeof(arr); // 8(指针大小,不是数组大小!)
}
typedef 和 using 的区别?
两者都可以定义类型别名,但 using(C++11)更强大:
cpp
// typedef(传统方式)
typedef int Int32;
typedef void (*FuncPtr)(int, int); // 函数指针别名(语法复杂)
typedef std::vector<int> IntVec;
// using(C++11,推荐)
using Int32 = int;
using FuncPtr = void (*)(int, int); // 更直观
using IntVec = std::vector<int>;
// using 可以用于模板别名,typedef 不行!
template <typename T>
using Vec = std::vector<T>; // ✅ using 支持
Vec<int> v; // std::vector<int>
🌈 建议:优先使用
using,语法更清晰,且支持模板别名。
⭐ #define 宏和 const / constexpr 的区别?
| 特性 | #define 宏 | const | constexpr(C++11) |
|---|---|---|---|
| 处理阶段 | 预处理期(文本替换) | 编译期 | 编译期 |
| 类型检查 | ❌ 无 | ✅ 有 | ✅ 有 |
| 作用域 | 全局(无作用域) | 遵守作用域规则 | 遵守作用域规则 |
| 调试 | ❌ 无法调试 | ✅ 可调试 | ✅ 可调试 |
| 内存 | 不占内存 | 可能占内存 | 编译期求值,通常不占 |
cpp
#define PI 3.14159 // 宏:无类型,全局替换
const double PI = 3.14159; // 编译期常量,有类型
constexpr double PI = 3.14159; // 编译期确定值,有类型
// 宏的陷阱
#define SQUARE(x) x * x
int a = SQUARE(3 + 1); // 展开为 3 + 1 * 3 + 1 = 7(不是 16!)
// 正确做法
#define SQUARE(x) ((x) * (x)) // 加括号
// 更好的做法:使用 constexpr 函数
constexpr int Square(int x) { return x * x; }
⚠️ 建议:尽量用
const/constexpr替代#define,避免宏的副作用。
三、基本数据类型
⭐ C++ 中的基本数据类型有哪些?
| 类别 | 类型 | 大小(常见) | 范围 |
|---|---|---|---|
| 整型 | char | 1 字节 | -128 ~ 127 |
short | 2 字节 | -32768 ~ 32767 | |
int | 4 字节 | 约 ±21 亿 | |
long | 4/8 字节 | 平台相关 | |
long long | 8 字节 | 约 ±9.2×10¹⁸ | |
| 浮点 | float | 4 字节 | 精度约 6-7 位 |
double | 8 字节 | 精度约 15-16 位 | |
long double | 8/12/16 字节 | 平台相关 | |
| 布尔 | bool | 1 字节 | true / false |
| 空 | void | — | 无值 |
| 宽字符 | wchar_t | 2/4 字节 | 平台相关 |
⚠️ C++ 标准只规定了类型的最小大小,具体大小取决于编译器和平台。可以用
sizeof确认。
⭐ 隐式类型转换的规则是什么?有什么风险?
C++ 会自动进行隐式类型转换(也叫"类型提升"),基本规则:
char/short → int → unsigned int → long → unsigned long → long long → float → double → long double
cpp
int a = 3;
double b = 2.5;
auto c = a + b; // a 隐式转为 double,c 是 double 类型
// ⚠️ 风险1:有符号和无符号混用
int x = -1;
unsigned int y = 1;
if (x < y) {
// 这个分支不会执行!x 被转为 unsigned int,变成一个很大的正数
}
// ⚠️ 风险2:精度丢失
int big = 2000000000;
float f = big; // float 精度不够,可能丢失数据
// ⚠️ 风险3:缩窄转换
double d = 3.14;
int i = d; // 小数部分被截断,i = 3
🌈 建议:使用
{}初始化来检测缩窄转换:cppint i{3.14}; // ❌ 编译错误:缩窄转换
浮点数精度丢失的原因?如何解决?
原因:浮点数在计算机中以 IEEE 754 标准存储,使用二进制表示,很多十进制小数(如 0.1)在二进制中是无限循环小数,无法精确表示。
cpp
double a = 0.1 + 0.2;
std::cout << (a == 0.3) << "\n"; // 输出 0(false!)
std::cout << std::setprecision(20) << a << "\n"; // 0.30000000000000004
解决方案:
cpp
// 方法1:使用 epsilon 比较
#include <cmath>
bool AlmostEqual(double a, double b, double epsilon = 1e-9) {
return std::fabs(a - b) < epsilon;
}
// 方法2:整数运算(如金额用"分"而非"元")
int price_cents = 199; // 1.99 元
// 方法3:使用第三方高精度库(如 Boost.Multiprecision)
⭐ auto 关键字的用法和注意事项?
auto(C++11)让编译器自动推导变量类型:
cpp
auto i = 42; // int
auto d = 3.14; // double
auto s = std::string("hello"); // std::string
auto p = std::make_unique<int>(10); // std::unique_ptr<int>
// 与范围 for 循环配合
std::vector<int> vec = {1, 2, 3};
for (auto& v : vec) { v *= 2; } // 引用,可修改
for (const auto& v : vec) { /*...*/ } // 常量引用,不可修改(推荐)
// 函数返回类型推导(C++14)
auto Add(int a, int b) { return a + b; }
注意事项:
| 场景 | 说明 |
|---|---|
auto 会忽略顶层 const 和引用 | const int& r = x; auto a = r; → a 是 int(不是 const int&) |
| 需要引用必须显式写 | auto& ref = x; |
| 需要 const 必须显式写 | const auto& cref = x; |
| 不能用于函数参数(C++20 前) | void foo(auto x) 仅 C++20 起支持 |
| 不能用于类成员变量 | auto member = 10; ❌ |
decltype 的用法?和 auto 有什么区别?
decltype(C++11)获取表达式的类型,不会求值:
cpp
int x = 10;
decltype(x) y = 20; // y 的类型是 int
decltype(x + 0.5) z; // z 的类型是 double
// 与 auto 的区别
const int& rx = x;
auto a = rx; // a 是 int(丢掉了 const 和 &)
decltype(rx) b = x; // b 是 const int&(完整保留类型)
// 常用场景:获取返回类型
template <typename T, typename U>
auto Add(T t, U u) -> decltype(t + u) {
return t + u;
}
四、变量
⭐ 局部变量、全局变量和静态变量的区别?
| 特性 | 局部变量 | 全局变量 | 静态局部变量 | 静态全局变量 |
|---|---|---|---|---|
| 作用域 | 函数/块内 | 整个程序 | 函数/块内 | 当前文件 |
| 生命周期 | 函数执行期间 | 程序运行期间 | 程序运行期间 | 程序运行期间 |
| 存储位置 | 栈 | 全局数据区 | 全局数据区 | 全局数据区 |
| 默认初始值 | 未初始化(随机值) | 0 | 0 | 0 |
cpp
int g = 100; // 全局变量
static int sg = 200; // 静态全局变量(仅当前文件可见)
void Foo() {
int local = 10; // 局部变量,函数结束后销毁
static int count = 0; // 静态局部变量,只初始化一次
++count;
std::cout << count << "\n"; // 每次调用递增
}
⭐ C++ 的内存分区(内存布局)是怎样的?
┌──────────────────┐ 高地址
│ 内核空间 │
├──────────────────┤
│ 栈 (Stack) │ ↓ 向低地址增长
│ │ 局部变量、函数参数、返回地址
├──────────────────┤
│ 堆 (Heap) │ ↑ 向高地址增长
│ │ new/malloc 动态分配
├──────────────────┤
│ 全局/静态数据区 │ 全局变量、静态变量
│ (.data / .bss) │ .data: 已初始化 .bss: 未初始化
├──────────────────┤
│ 常量区 (.rodata)│ 字符串字面量、const 全局变量
├──────────────────┤
│ 代码区 (.text) │ 编译后的机器指令
└──────────────────┘ 低地址
| 区域 | 管理方式 | 存储内容 |
|---|---|---|
| 栈 | 编译器自动分配释放 | 局部变量、函数参数 |
| 堆 | 手动 new / delete | 动态分配的对象 |
| 全局/静态区 | 程序启动时分配,结束时释放 | 全局变量、static 变量 |
| 常量区 | 只读 | 字符串字面量、const 全局常量 |
| 代码区 | 只读 | 程序指令 |
⭐ 栈和堆的区别?
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 管理方式 | 编译器自动管理 | 手动管理(new/delete) |
| 分配速度 | 非常快(移动栈指针) | 较慢(需要查找空闲内存) |
| 大小限制 | 较小(通常 1-8 MB) | 较大(受物理内存限制) |
| 碎片 | 无碎片(连续分配) | 可能产生碎片 |
| 增长方向 | 高地址 → 低地址 | 低地址 → 高地址 |
| 生命周期 | 随作用域自动销毁 | 手动释放或程序结束 |
cpp
void Example() {
int stackVar = 10; // 栈上分配
int* heapVar = new int(20); // 堆上分配
auto smartPtr = std::make_unique<int>(30); // 堆上分配(智能指针管理)
delete heapVar; // 必须手动释放
// smartPtr 离开作用域自动释放
}
extern 关键字的作用?
extern 用于声明一个在其他文件中定义的全局变量或函数:
cpp
// file1.cpp
int globalVar = 42; // 定义
void Foo() { /*...*/ } // 定义
// file2.cpp
extern int globalVar; // 声明(不分配内存)
extern void Foo(); // 声明(函数可省略 extern)
void Bar() {
std::cout << globalVar; // 使用 file1 中定义的变量
Foo();
}
const 和 constexpr 的区别?
| 特性 | const | constexpr(C++11) |
|---|---|---|
| 含义 | 运行时常量(承诺不修改) | 编译时常量(必须编译期可求值) |
| 初始化时机 | 可以在运行时初始化 | 必须在编译时初始化 |
| 用于函数 | ❌ 不能修饰普通函数 | ✅ 修饰函数,要求编译期可执行 |
cpp
const int a = 10; // ✅ 编译期确定
const int b = GetValue(); // ✅ 运行时确定也可以
constexpr int c = 10; // ✅ 编译期确定
// constexpr int d = GetValue(); // ❌ 除非 GetValue 也是 constexpr
constexpr int Square(int x) { return x * x; }
constexpr int val = Square(5); // 编译期计算,val = 25
int arr[Square(3)]; // ✅ 可以用于数组大小
五、函数
⭐ 值传递、引用传递和指针传递的区别?
| 方式 | 语法 | 是否拷贝 | 能否修改原值 | 能否为空 |
|---|---|---|---|---|
| 值传递 | void f(int x) | ✅ 拷贝 | ❌ 不能 | — |
| 引用传递 | void f(int& x) | ❌ 不拷贝 | ✅ 能 | ❌ 不能为空 |
| 常量引用 | void f(const int& x) | ❌ 不拷贝 | ❌ 不能 | ❌ 不能为空 |
| 指针传递 | void f(int* x) | 拷贝指针 | ✅ 能 | ✅ 可以为空 |
cpp
void ByValue(int x) { x = 100; } // 不影响原值
void ByRef(int& x) { x = 100; } // 修改原值
void ByConstRef(const int& x) { /* 只读 */ } // 不拷贝,不修改
void ByPtr(int* x) { if (x) *x = 100; } // 需要判空
int a = 1;
ByValue(a); // a 仍为 1
ByRef(a); // a 变为 100
ByPtr(&a); // a 变为 100
🌈 最佳实践:
- 小类型(
int、double)→ 值传递- 大对象只读 →
const引用- 需要修改 → 非
const引用- 可能为空 → 指针
⭐ 函数重载(Overload)是什么?重载的规则?
函数重载是指同一作用域内,多个同名函数的参数列表不同:
cpp
int Add(int a, int b) { return a + b; }
double Add(double a, double b) { return a + b; }
int Add(int a, int b, int c) { return a + b + c; }
Add(1, 2); // 调用第一个
Add(1.0, 2.0); // 调用第二个
Add(1, 2, 3); // 调用第三个
重载规则:
| 可以用于区分重载 | 不能用于区分重载 |
|---|---|
| ✅ 参数个数不同 | ❌ 仅返回类型不同 |
| ✅ 参数类型不同 | ❌ 仅参数名不同 |
| ✅ 参数顺序不同 | |
| ✅ const 修饰(成员函数) |
cpp
// ❌ 错误:仅返回类型不同,不构成重载
int Foo(int x);
double Foo(int x); // 编译错误
// ✅ const 重载(成员函数)
class MyClass {
void Print() { std::cout << "non-const\n"; }
void Print() const { std::cout << "const\n"; } // ✅ 可以重载
};
默认参数的规则?
cpp
// 默认参数从右往左
void Foo(int a, int b = 10, int c = 20); // ✅
// void Foo(int a = 1, int b, int c); // ❌ 错误
// 声明和定义分离时,默认参数只能在声明中写
// header.h
void Foo(int a, int b = 10);
// source.cpp
void Foo(int a, int b) { // 这里不能再写默认值
// ...
}
⭐ inline 内联函数的作用?
内联函数在编译时将函数体展开到调用处,避免函数调用的开销:
cpp
inline int Max(int a, int b) {
return (a > b) ? a : b;
}
// 调用 Max(3, 5) 会被编译器替换为 (3 > 5) ? 3 : 5
| 特性 | 说明 |
|---|---|
| 优点 | 减少函数调用开销(压栈/跳转/返回) |
| 缺点 | 代码膨胀(函数体在每个调用处展开) |
| 适用 | 短小频繁调用的函数(几行以内) |
| 注意 | inline 只是建议,编译器可能忽略 |
| 类内定义 | 类内定义的成员函数默认是 inline |
⚠️ 现代编译器已经很智能,通常会自动决定是否内联,不需要手动指定。
⭐ 什么是函数指针?什么是 std::function?
cpp
// 函数指针
int Add(int a, int b) { return a + b; }
int (*funcPtr)(int, int) = Add; // 传统语法
auto funcPtr2 = Add; // 或用 auto
int result = funcPtr(3, 5); // 调用
// std::function(C++11,更通用)
#include <functional>
std::function<int(int, int)> func;
func = Add; // 普通函数
func = [](int a, int b) { return a + b; }; // Lambda
func = std::bind(Add, std::placeholders::_1, 10); // 绑定参数
int r = func(3, 5);
| 特性 | 函数指针 | std::function |
|---|---|---|
| 可存储 | 普通函数、静态成员函数 | 任何可调用对象 |
| Lambda | ❌ 无捕获的 Lambda 才行 | ✅ 支持 |
| 成员函数 | 语法复杂 | ✅ 配合 std::bind |
| 开销 | 零开销 | 有少量开销(类型擦除) |
六、运算符
⭐ 逻辑运算符的短路求值是什么?
cpp
// && 短路:左边为 false,不执行右边
if (ptr != nullptr && ptr->IsValid()) {
// 如果 ptr 为空,不会执行 ptr->IsValid(),避免崩溃
}
// || 短路:左边为 true,不执行右边
if (IsDefault() || LoadConfig()) {
// 如果 IsDefault() 为 true,不会执行 LoadConfig()
}
⚠️ 利用短路特性可以做安全判断,但不要在短路表达式中放有副作用的操作(难以维护)。
⭐ 位运算符有哪些?常见应用?
| 运算符 | 含义 | 示例 |
|---|---|---|
& | 按位与 | 5 & 3 → 1 |
| | 按位或 | 5 | 3 → 7 |
^ | 按位异或 | 5 ^ 3 → 6 |
~ | 按位取反 | ~5 → -6 |
<< | 左移 | 1 << 3 → 8 |
>> | 右移 | 16 >> 2 → 4 |
cpp
// 常见应用
// 1. 判断奇偶
bool isOdd = (n & 1);
// 2. 交换两个数
a ^= b; b ^= a; a ^= b;
// 3. 乘除 2 的幂
int x = n << 1; // n * 2
int y = n >> 1; // n / 2
// 4. 标志位操作
const int READ = 1 << 0; // 0001
const int WRITE = 1 << 1; // 0010
const int EXEC = 1 << 2; // 0100
int perm = READ | WRITE; // 设置权限
bool canRead = perm & READ; // 检查权限
perm &= ~WRITE; // 清除权限
perm ^= EXEC; // 切换权限
💬 评论