1.1 C/C++ 针对关键字的18个常考问题
一、C 语言宏中的 # 和 ## 的用法
1、#:字符串操作符
将宏定义中的传入参数转为字符串
#define example( instr ) printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr //当使用该宏定义时:
example( abc ); // 在编译时将会展开成:printf("the input string is:\t%s\n","abc")
string str = example1( abc ); // 将会展成:string str="abc"
2、##:符号连接操作符
将宏定义的多个形参转换成一个实际参数名。
#define exampleNum( n ) num##n int num9 = 9; int num = exampleNum( 9 ); // 将会扩展成 int num = num9,连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。同时会阻止继续宏定义展开,
二、关键字 volatile 有什么含义?
(1)禁止编译器对变量的访问进行优化;(2)提醒编译器这个变量的值可能会被外部因素改变,编译后的程序每次都会从内存中读取数据。(3)这个关键字实际上在现代c++编译器中已经完全被忽略了。(哈哈哈,大部分人的面经都已经落后了,但依旧得苦逼背,因为还有老式编译器可能在支持。)
底层原理:
- 强制从内存读取(而非寄存器缓存)
- 禁止编译器重排相关指令(但CPU仍可能重排,需配合屏障)
使用场景:
1、设备的硬件寄存器
存储器映射的硬件寄存器必须加上 volatile ,防止编译器对该存储值进行假设。
XBYTE[2]=0x55; XBYTE[2]=0x56; XBYTE[2]=0x57; XBYTE[2]=0x58; //对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)。
2、一个中断服务程序中修改的供其他程序检测的变量。
3、多线程应用中被几个线程共享的变量。
- 不保证原子性:
volatile不解决多线程竞争问题,仍需互斥锁/原子操作 - 不保证顺序性:可能仍需内存屏障(memory barriers)
- 与
std::atomic的区别:C++11后,线程共享变量更推荐用atomic
4、防止编译器优化掉无用代码
volatile int i;
for(i = 0; i < 1000; i++); // 不会被优化掉
volatile int flag = 0;
while(flag == 0) { /* 每次都会重新读取flag的值 */ }
//否则可能被优化为 while(true)
与const的对比:
const表示"程序本身不应修改"volatile表示"程序外部的修改可能发生"- 可组合使用:
const volatile uint32_t *hw_reg;(硬件只读寄存器)
三、const 有什么作用?
注意:const 修饰的变量只能在初始化阶段对其进行赋值;const 修饰的变量不能赋给非 const 修饰的变量,反过来可以。
1、修饰变量:定义变量为常变量。
2、修饰函数参数:表明函数不会对指针/引用参数指向内容进行修改。
3、修饰函数的返回指针和函数值:不允许对返回指针所指向的内容进行修改,不允许对返回值进行修改。
- 如果是返回值,通常是个 静态变量。
4、c++ 里面的常成员函数,表明该函数调用不会修改该对象的成员变量,除非成员变量用 mutable 修饰。
class Student {
public:
string getName() const { // 承诺不修改对象状态
// m_age = 20; // 编译错误!
return m_name;
}
void modifyAge() const {
m_mutableAge = 25; // 允许修改mutable成员
}
private:
string m_name;
mutable int m_mutableAge; // 可被const成员函数修改
int m_age;
};
- 关键规则:
- const 成员函数不能修改非 mutable 成员变量
- 非 const 对象可以调用 const 和非 const 成员函数
- const 对象只能调用 const 成员函数
5、常量指针 和 指针常量
- 常量指针:const int *p 或者 int cosnt *p;指针所指向的内容不可以更改,但指针本身可以重新指向新的内存。
- 指针常量:int * const p;指针所指向的内容可以进行修改,但指针不可以重新指向新内存。
6、const 在 c 语言 和 c++ 的连接属性不一样。c 中是外部连接,如果在头文件中定义,会发生重定义错误;c++ 中是内部连接,类似 static 会生成多个独立副本。
四、关键字 static
核心原理:static的核心是 生命周期 和 链接属性:
- 延长局部变量的生命周期:从自动存储期(函数调用结束销毁)变为静态存储期(程序全程存在)。静态变量存储在.data(已初始化)或.bss(未初始化)段,不同于堆/栈分配。
- 限制全局变量/函数的可见性:从外部链接(整个程序可见)变为内部链接(仅当前文件可见)
初始化时机:
- 全局静态变量:在main()之前初始化(顺序不确定)
- 局部静态变量:首次执行到声明处时初始化
五种典型用法:
1、静态局部变量
- 首次执行时初始化,后续调用保持上次的值
- 内存存储在静态区(非栈)
- 线程不安全(需手动加锁)
2、全局静态变量
- 仅当前源文件可见(防止命名污染)
- 生命周期同程序运行周期
3、静态函数
- 类似全局静态变量,限制函数作用域
4、类静态成员
- 所有类实例共享同一内存
- 必须在类外定义(除const整型),成员变量可以加 inline 在类内初始化。
- 可通过类名::成员直接访问
5、静态局部对象
- 实现单例模式(线程安全,C++11起保证)
class A {
public:
static A& getInstance() {
static A instance; // 局部静态变量初始化是线程安全的
return instance; // 修正拼写
}
private:
A() = default;
~A() = default;
// 删除拷贝构造函数和拷贝赋值运算符
A(const A&) = delete;
A& operator=(const A&) = delete;
// 删除移动构造函数和移动赋值运算符
A(A&&) = delete;
A& operator=(A&&) = delete;
};
五、extern”C” 的作用是什么?
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
// main.cpp
#include <iostream>
// 使用 extern "C" 声明 C 函数
extern "C" {
int add(int a, int b);
}
int main() {
int result = add(3, 4);
std::cout << "Result: " << result << std::endl;
return 0;
}
六、new/delete 与 malloc/free 的区别是什么?
1、new/delete 是c++的操作符;malloc/free 是标准库函数。
2、new 为对象申请分配内存空间时,可以自动调用对象构造函数;delete 也可以自动调用析构函数。而 malloc/delete 不会进行初始化和析构。
class Widget {
public:
Widget() { std::cout << "构造\n"; }
~Widget() { std::cout << "析构\n"; }
};
int main() {
// malloc/free 不调用构造/析构
Widget* w1 = (Widget*)malloc(sizeof(Widget));
free(w1); // 仅释放内存,可能泄漏资源
// new/delete 自动管理生命周期
Widget* w2 = new Widget();
delete w2; // 正确调用析构
}
3、new 可以自动计算所申请的内存大小,malloc 需要显示指定申请内存大小。
4、new 返回的指针类型会自动推导;malloc 返回的是 void*, 必须强制转换指针类型。
5、失败处理对比:malloc 分配失败返回 NULL;new 分配失败抛出异常。
int* p = (int*)malloc(1000000000000ULL); // 分配失败
if (p == NULL) {
std::cerr << "分配失败\n";
}
#include <new> // 包含 bad_alloc 定义
try {
int* p = new int[1000000000000ULL];
} catch (const std::bad_alloc& e) {
std::cerr << "分配失败: " << e.what() << "\n";
}
七、sizeof 和 strlen 区别?
- sizeof 是运算符;strlen 是函数。
- sizeof 返回类型是 size_t ( unsigned int )。
- strlen 只能用 char * 作为参数且必须以 \0 作为结尾。
- 当把函数名传给 sizeof 时,返回的是函数返回类型的大小。
- 当把数组名作为 sizeof 参数时,返回的是整个数组的大小。
- strlen("\0") =0; sizeof("\0")=2
八、C 语言中 struct 与 union 的区别是什么?
union 的长度是其最长成员的长度。
union 只能同时对一个变量赋值,其他变量会被覆盖。
九、struct 与 class 区别
在 C++ 中,struct 和 class 在功能上几乎相同,都可以包含成员变量、成员函数、构造函数、析构函数等,之间的主要区别通常体现在访问权限和继承的默认方式上。
默认访问权限:struct 是默认成员公有;class 默认成员私有。
默认继承方式:struct 默认公有继承;class 默认私有继承。
使用习惯:struct 默认构造简单数据结构;class 用于面向对象,实现复杂数据类型。
默认构造函数和析构函数:struct不会自动生成的默认构造与析构函数;class 会自动生成。
十、&& 具有 0 短路特性,|| 具有 1 短路特性
十一、++a 和 a++ 有什么区别?两者是如何实现的?
//a++,需要一个临时变量 int temp = a; a=a+1; return temp; //++a a=a+1; return a;
十二、register 关键字(其实这个关键字也被现代编译器忽略了)
register 是一个编译器提示,建议将局部变量存在 CPU寄存器 中,从而提高访问速度。编译器不一定会遵守这个建议,具体实现取决于编译器和硬件架构。
register 关键字通常用于 频繁访问的局部变量,如循环变量。
使用 register 声明的变量无法取地址。
十三、explicit 的作用及用法
防止单参数的构造函数被隐式调用,只允许显示的调用单参数的构造函数。
explicit 不能防止所有类型的隐式转换:explicit 只防止通过构造函数进行的隐式转换,但它不能防止其他类型的隐式转换,如通过转换操作符(operator T())进行的转换。
class Implicit {
public:
Implicit(int x) : val(x) {} // 允许隐式转换
int val;
};
class Explicit {
public:
explicit Explicit(int x) : val(x) {} // 禁止隐式转换
int val;
};
void process(Implicit i) {}
void processEx(Explicit e) {}
int main() {
// 隐式转换测试
process(10); // 合法:隐式构造 Implicit(10)
// processEx(20); // 错误!必须显式构造
// 显式构造
processEx(Explicit(20)); // 正确
// 转换操作符示例(不受explicit限制)
struct Convertible {
operator Implicit() { return Implicit(30); }
};
Convertible c;
process(c); // 合法:通过 operator Implicit() 转换
}
十四、mutable 关键字
mutable 用于修饰类的成员变量,它允许这些成员变量在常量成员函数(const 修饰的函数)中被修改。默认情况下,const 成员函数不能修改类的任何成员变量,因为它们被视为“不可修改”的。然而,mutable 修饰的成员变量可以在常量成员函数中被修改。适用场景: 主要用于那些即使对象是常量,仍然需要在常量成员函数中更新的内部状态。例如:缓存、日志计数、操作计数等。
十五、extern
extern 可以多次声明,但只能有一个定义。告诉编译器先别分配内存,链接时进行查找。
头文件声明时加 extern,定义时不要加。
十六、auto 与 decltype
auto 适用于所有类型:可以用于推导基本数据类型、指针类型、引用类型,甚至是模板类型。但 auto 使用时必须初始化。
decltype 用于根据表达式的类型推导变量类型,且不会实际计算表达式的值。
constautoa=42; //正确,auto推导为 const int auto b; //错误,auto需要初始化 decltype(expression) variable_name;
二者区别:auto 根据初始化表达式推导类型;decltype 根据给定表达式的类型来推导变量类型,decltype 不要求初始化,可以只声明。
int x = 10; auto a = x; // a的类型为int decltype(x) b = 20; // b 的类型为 int,与 x 相同 decltype(x) c; // 正确
十七、override 和 final 关键字
override 关键字用于显式地指示派生类中的成员函数是对基类中的虚函数的重写。使用 override 可以提高代码的可读性,并确保派生类的函数正确地覆盖基类中的虚函数。会进行编译时检查,当返回类型(返回协变指针/引用 除外,协变类型是指派生成度更高、更具体的类型)、函数签名不一致时报错。
final 关键字用于指示一个类或虚函数不能被继承或重写。
十八、说出 3 个使用 switch 时应注意的事项
1、switch 后的表达式类型必须是整型(包括char类型,因为char本质上也是整型的一种),不能是浮点型等其他类型。
2、case 后面的值必须是常量表达式,即编译期间就能确定值的表达式,不能是变量。
3、在switch语句中,case分支如果没有break语句,程序会继续执行下一个case分支的代码,直到遇到break或者switch语句结束。
一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。

