欢迎来到C++面试的“底层视角”。本文将从编译器与内存的隐秘规则出发,为你揭示那些语法背后的深层逻辑。我们将直面整型长度在不同平台下的潜规则,剖析const在变量、指针与引用中的三重语义,并探究static与inline如何借助局部性原理影响性能。同时,define与typedef在编译期的不同行为、new/malloc背后的内存区域本质,以及constexpr如何通过编译期计算重塑程序性能,都将逐一呈现。最后,我们聚焦于++操作符的前后置差异——一个常考却易被误解的典型问题。透过这些细节,你不仅会获得面试的答案,更将建立起对C++更深层的理解。

整型 short、int、long和long long

头文件中climits定义了符号常量:例如,INT_MAX表示int的最大值,INT_MIN表示int的最小值。
无符号类型
const关键字 根据const放置的位置不同这里有两个容易混淆的概念:
以下解释来自《C++ primer第五版》,
指针本身就是常量(顶层const),声明必须要进行初始化,初始化之后就不能改变其值(也就是指针存放的地址),换句话说就是不能指向其他对象。但是可以修改该指针指向的对象的值。
指针指向一个常量(底层const),不能通过该指针来修改指向的对象的值。但是因为指针本身不是常量,所以可以修改指针指向的对象(也就是可以指向别的地址)。
const关键字的作用 const关键字主要用于指定变量、指针、引用、成员函数等的性质:
static关键字 static关键字主要用于控制变量和函数的声明周期、作用域和访问权限:
define、typedef和inline的区别
【补充】C++中的inline限制:
const和define的区别 const用于定义常量;define也用于定义宏的,而宏也可以定义常量。对于定义常量这一点,两者存在一定区别:
new和malloc的区别
总结示意图:

这里要注意一下两之间的关系:
如果将一个成员函数或者变量标记为constexpr,那么就顺带将其标记为了const。反之则不成立,一个函数或者变量是const的,并不就是constexpr的。
d我们可使用constexpr来声明一个变量,由编译器来验证一个变量的值是否是常量表达式:

也就是说使用constexpr来声明变量,必须使用常量表达式进行初始化。
如果我们使用constexpr来声明定义一个指针,constexpr只对于指针有效,与指针所指向的对象无关:
constexpr int* p1 = nullptr; // 编译期常量指针,指针值在编译期已知,语义类似 int* const
const int* p2 = nullptr; // 底层 const,指向常量的指针:指针可改,指向的值不可改
int* const p3 = nullptr; // 顶层 const,常量指针:指针本身不可改,指向的值可改constexpr是能够在编译期被求值的函数,换句话说是能够用于常量表达式的函数。
C++11要求,constexpr修饰的函数的返回类型,所有的形参都是字面值类型,函数体有且只有一条return语句。
例如:
constexpr int new() {return 42;}值得注意的是,为了constexpr修饰的函数能够在编译期展开,会被隐式地转换成内联函数。
构造函数肯定不能是const的,但是对于字面值常量类的构造函数可以是constexpr的:
class Point { // 这是一个字面值常量类
int x, y;
public:
/constexpr构造函数
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
};
//constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。
//对象调用的成员函数必须使用 constexpr 修饰这里补充一下什么是字面值常量类:
constexpr构造函数
这是一个与const绝对对立的,类型修饰符。该修饰符会影响编译器编译的结果,用该关键字修饰的变量,表示该变量随时可能发生改变,告诉编译器所有与改变变量有关的运算不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。
下面举一个例子进行说明:
int flag = 0;
void wait() {
while (flag == 0) {
// 空循环等待
}
}
//正常情况下,编译器可能优化为:
//1. 将flag加载到寄存器
mov eax, [flag]
//2. 循环只检查寄存器,不从内存重新读取
loop_start:
test eax, eax ; 只检查寄存器中的值
je loop_start ; 如果为0继续循环那么这个时候如果其他线程修改了flag的值,就可能导致死循环。
而如果我们使用volatile:
volatile int flag = 0; // 告诉编译器:这个变量可能被意外修改
void wait() {
while (flag == 0) {
// 现在编译器不会优化掉内存访问
}
}
//那么生成的汇编如下:
//每次循环都从内存重新加载
loop_start:
mov eax, [flag] ; 每次都从内存读取
test eax, eax
je loop_start ; 为0则继续循环self &operator++() {
node = (linktype)((node).next);
return *this;
}
const self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}两者性能上的区别:
C++为了区分前后置,后置++有一个int哑元参数,调用的时候编译器会默认给后置++生成一个0。
面试可能提问:
因为后置为了返回旧值,创建了一个临时对象来接收,但是在函数结束时这个临时对象被销毁了,如果此时返回引用,引用的绑定目标都没有了。
这是为了防止误用i++++,连续的两次调用后置++重载,我们举例进行说明,
如果我们不加const,那么可能出现下面情况:
// 用户可能期望:
c++++; // "c应该变成7吧?"
// 实际上:
// 第一次c++:返回值为5的临时对象,c变成6
// 第二次++:对临时对象自增(临时对象变成6)
// 临时对象被销毁,c仍然是6!
// 结果:用户期望c=7,实际c=6程序会成功运行,但是结果是错误的,所以为了防止不可预测的后果,我们使用const来直接禁止这种使用的合法化:
// 尝试连续后置++
c++++; //编译错误!
// 第一次c++:返回const Counter临时对象
// 第二次++:尝试对const对象调用operator++
// 编译器报错:不能修改const对象也就是如果遇到i++++用法,直接报错!
最好使用前置++,因为它不会创建临时对象,进而不会带来构造和析构的额外开销。