C/C++ 关键关键字面试指南 (const, static, volatile, explicit)
目录
1. const (常量)
核心用法与考点:
1.1 修饰变量和指针
1.2 修饰函数参数和返回值
1.3 修饰类成员函数(C++ 特有)
1.4 代码示例:
2. static (静态)
核心用法与考点:
2.1 修饰局部变量(改变存储期)
2.2 修饰全局变量和函数(改变链接性)
2.3 修饰类成员变量(C++ 特有)
2.4 修饰类成员函数(C++ 特有)
2.5 代码示例:
3. volatile (易失性)
核心用法与考点:
3.1 阻止编译器优化
3.2 常见应用场景:
3.3 与 const 的结合
4. explicit (显式)
核心用法与考点:
4.1 修饰单参数构造函数
4.2 修饰转换函数(C++11/14)
1. const (常量)
定义: 用于声明常量或指定不允许修改的数据。它强制执行“常量正确性”(Const Correctness),是 C++ 中提高代码健壮性和可读性的核心机制。
核心用法与考点:
1.1 修饰变量和指针
理解 const
靠近谁,谁就是常量。
声明方式 | 含义 | 可修改性 |
---|---|---|
| 常量整数 |
|
| 指向常量的指针 |
|
| 常量指针 |
|
| 指向常量的常量指针 |
|
1.2 修饰函数参数和返回值
-
修饰参数: 使用
const
引用 (const T&
) 传递对象,可以避免不必要的拷贝(效率高),同时保证函数内不会修改原始对象(安全性高)。 -
修饰返回值: 限制调用者不能修改返回的对象。
1.3 修饰类成员函数(C++ 特有)
在成员函数末尾加上 const
,表示该函数是一个 常成员函数。
-
常成员函数不能修改类的任何非
static
成员变量(除非该成员被声明为mutable
)。 -
常对象只能调用常成员函数。
1.4 代码示例:
#include <iostream>
#include <vector>//-------------------------------------------
// 1.1 const 修饰变量 / 指针
//-------------------------------------------
void demo_pointer() {int x = 5, y = 10;const int a = 42; // a 只读// a = 43; // ❌ 编译错误:a 是常量const int* p1 = &x; // *p1 只读,p1 可改// *p1 = 100; // ❌ 不能改指向的值p1 = &y; // ✅ 可以改指向int* const p2 = &x; // p2 只读,*p2 可改*p2 = 100; // ✅ 可以改值// p2 = &y; // ❌ 不能改指针本身const int* const p3 = &x; // 全只读// *p3 = 200; p3 = &y; // ❌ 都动不了
}//-------------------------------------------
// 1.2 const 修饰函数参数 & 返回值
//-------------------------------------------
// 参数:大对象只读传参,零拷贝又安全
void printVec(const std::vector<int>& v) { // v 只能读// v.push_back(9); // ❌ 编译错误:v 是 constfor (int n : v) std::cout << n << ' ';std::cout << '\n';
}// 返回值:阻止调用者改临时量
const int getMax(const int& a, const int& b) {return (a > b ? a : b);
}//-------------------------------------------
// 1.3 const 成员函数
//-------------------------------------------
class Counter {int value_ = 0;mutable int access_ = 0; // mutable 允许在 const 函数里改
public:int get() const { // ① 常成员函数++access_; // ✅ mutable 成员可改return value_;}void inc() { ++value_; } // 普通函数可改成员// 重载:const / 非 const 版本int& data() { return value_; } // 非 const 对象调用const int& data() const { return value_; } // const 对象调用
};int main() {demo_pointer();std::vector<int> nums{1, 2, 3};printVec(nums); // 1 2 3int m = getMax(3, 7);std::cout << "max=" << m << '\n'; // max=7// getMax(3,7) = 99; // ❌ 返回的是 const int,不能当左值Counter c1;c1.inc();std::cout << "c1=" << c1.get() << '\n'; // 1const Counter c2; // ② 常对象// c2.inc(); // ❌ 只能调 const 成员函数std::cout << "c2=" << c2.get() << '\n'; // 0// c2.data() = 5; // ❌ 返回 const int&,不能赋值
}
4 句话再总结
-
const
靠谁,谁只读;靠*
左边还是右边,决定“值”还是“指针”只读。 -
const T&
传参 = 零拷贝 + 防篡改,是 C++ 最常用签名。 -
成员函数末尾加
const
= 隐式 this 指针前加 const,内部不能改普通成员。 -
常对象只能调常成员函数;需要统计/缓存时用
mutable
打洞。
2. static (静态)
定义: static
关键字改变了变量或函数的存储期(Storage Duration)、作用域(Scope)和链接性(Linkage)。
核心用法与考点:
2.1 修饰局部变量(改变存储期)
-
作用: 局部变量的生存期从函数调用结束延伸到整个程序运行期间。
-
特点: 只初始化一次。
2.2 修饰全局变量和函数(改变链接性)
-
作用: 将全局变量或函数的外部链接性(External Linkage)改为 内部链接性(Internal Linkage)。
-
特点: 它们只能在当前文件(编译单元)内访问和使用,对其他文件隐藏。这有助于避免命名冲突。
2.3 修饰类成员变量(C++ 特有)
-
作用: 静态成员变量 属于整个类,而不是任何特定的对象。
-
特点: 整个类只有一个副本,所有对象共享。必须在类外进行定义和初始化。
2.4 修饰类成员函数(C++ 特有)
-
作用: 静态成员函数 属于整个类,不接收隐式的
this
指针。 -
特点: 静态成员函数只能直接访问类的静态成员(变量和函数),不能访问非静态成员。
2.5 代码示例:
//============ file: main.cpp ============
#include <iostream>//----------------------------------------
// 2.2 全局 static:内部链接,别的文件看不到
//----------------------------------------
static int g_hidden = 999; // 只在 main.cpp 有效
static void hiddenFunc() { // 同上,链接器对外“隐藏”std::cout << "hiddenFunc: " << g_hidden << '\n';
}//----------------------------------------
// 2.1 局部 static:存储期变成“整个程序运行期”
//----------------------------------------
void visitCount() {static int count = 0; // ① 只有第一次进来才初始化++count;std::cout << "第 " << count << " 次调用 visitCount\n";
}//----------------------------------------
// 2.3 + 2.4 类静态成员
//----------------------------------------
class Logger {
public:// 非静态成员:每个对象各一份std::string name_;// 静态数据成员:整个类只有一份,必须类外再定义一次static int instanceCount; // ② 只是“声明”,不是“定义”// 静态成员函数:没有 this,只能访问静态成员static void printCount() {std::cout << "当前实例总数 = " << instanceCount << '\n';// std::cout << name_; // ❌ 编译错误:没有 this,不能访问非静态成员}// 构造函数:每创建一个对象就 ++explicit Logger(const std::string& n) : name_(n) {++instanceCount;}// 析构函数:对象销毁就 --~Logger() { --instanceCount; }
};// ② 类静态数据成员的“真正定义”放在类外(只能有一次)
int Logger::instanceCount = 0;//============ main 函数:演示全部效果 ============
int main() {std::cout << "----- 局部 static -----\n";visitCount(); // 第 1 次visitCount(); // 第 2 次visitCount(); // 第 3 次std::cout << "\n----- 全局 static -----\n";hiddenFunc(); // 能调到,因为同文件std::cout << "\n----- 类 static -----\n";Logger::printCount(); // 0 ,还没有对象{Logger a("A");Logger::printCount(); // 1Logger b("B");Logger::printCount(); // 2} // a、b 析构Logger::printCount(); // 0 再次回到 0// 不需要对象,直接类名调用静态函数Logger::printCount();
}
4 句话再总结
-
局部
static
让变量跨越函数调用继续存活。 -
全局
static
让名字跨不出当前源文件。 -
类静态数据成员只有一份,类外必须再定义一次。
-
类静态成员函数没有 this,只能玩静态成员,调用时可以不要对象。
3. volatile (易失性)
定义: 用于告诉编译器,变量的值可能会在程序控制流之外被意外地修改(即“易失”)。
核心用法与考点:
3.1 阻止编译器优化
当编译器看到一个变量的值在两次使用之间没有被程序修改时,它可能会进行优化,将该值缓存在 CPU 寄存器中,而不是每次都从内存中重新读取。
-
用途:
volatile
关键字指示编译器,每次访问该变量时都必须从 内存 中重新读取它的值,而不是使用寄存器中的缓存值。
3.2 常见应用场景:
-
并行设备(硬件): 访问内存映射的 I/O 寄存器,这些寄存器的值可能被外部硬件改变。
-
中断服务程序(ISR): 全局变量被 ISR 和主程序同时访问。
-
多线程环境(并发): 多个线程共享的全局变量(注意:
volatile
本身不能解决竞态条件,仍需要互斥锁等同步机制,但它是第一道防线)。
示例:
// 假设 status 寄存器的值可能被硬件修改
volatile int status_register;void wait_for_hardware() {// 如果没有 volatile,编译器可能优化为只读一次 status_registerwhile (status_register == 0) {// 等待硬件设置 status_register 为非零值}
}
3.3 与 const
的结合
-
volatile const int READ_ONLY_REG;
:表示这是一个不能被程序修改的常量(const
),但它可能会被外部硬件修改(volatile
)。
4. explicit (显式)
定义: (C++ 特有) 用于修饰类的构造函数或转换函数,以禁止(抑制)编译器进行隐式的类型转换。
核心用法与考点:
4.1 修饰单参数构造函数
-
问题: 如果一个类有一个只接受一个参数的构造函数,C++ 允许编译器使用这个构造函数将参数类型隐式转换为类类型。
-
解决方案: 使用
explicit
关键字可以避免这种不期望的隐式转换。
示例:
class Data {
public:// 没有 explicit:允许隐式转换Data(int x) { /* ... */ } // 使用 explicit:禁止隐式转换// explicit Data(int y) { /* ... */ }
};void process(Data d) { /* ... */ }// 如果构造函数没有 explicit:
Data d1 = 10; // 允许:隐式调用 Data(10)
process(20); // 允许:隐式调用 Data(20)// 如果构造函数使用了 explicit:
// Data d1 = 10; // 错误:禁止隐式转换
Data d2(30); // 允许:显式调用
process(Data(40)); // 允许:显式构造临时对象
4.2 修饰转换函数(C++11/14)
explicit
也可以修饰自定义类型转换操作符(如 operator bool()
),防止对象被隐式转换为目标类型。
总结: explicit
关键字提高了代码的清晰度和安全性,防止在不经意间发生类型转换,从而避免潜在的 bug。