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


目录
1. 数据类型
整型 short、int、long和long long
- C++整型数据长度标准
- short至少16位
- int至少与short一样长
- long至少32位,且至少与int一样长
- long long至少32位,且至少与long一样长
头文件中climits定义了符号常量:例如,INT_MAX表示int的最大值,INT_MIN表示int的最小值。
无符号类型
- 即不存储负数值的整型,能够增大变量能够存储的最大值,数据的长度不变。
- int被设置为自然长度,也就是计算机处理起来效率最高的长度,所以选择类型是一般选择int类型。
关键字
const关键字
根据const放置的位置不同这里有两个容易混淆的概念:
- 指针常量
- 常量指针
以下解释来自《C++ primer第五版》,
- 常量指针:
指针本身就是常量(顶层const),声明必须要进行初始化,初始化之后就不能改变其值(也就是指针存放的地址),换句话说就是不能指向其他对象。但是可以修改该指针指向的对象的值。
- 指针常量:
指针指向一个常量(底层const),不能通过该指针来修改指向的对象的值。但是因为指针本身不是常量,所以可以修改指针指向的对象(也就是可以指向别的地址)。
const关键字的作用
const关键字主要用于指定变量、指针、引用、成员函数等的性质:
- 常量变量:声明常量,表示变量的值不能被修改。
- 指针和引用:声明指针常量,表示指针所指向的值是常量,不能通过指针修改。声明引用常量,表示引用的值是常量,不能通过引用修改。
- 常量对象:声明对象为常量,表示对象的成员变量不能被修改。
- 常引用参数:声明函数参数为常量引用,表示函数不会修改传入的参数。
- 常量指针参数:声明函数参数为指针常量,表示该函数不会通过该指针修改传入的参数。
static关键字
static关键字主要用于控制变量和函数的声明周期、作用域和访问权限:
- 静态变量:
- 静态变量在程序的整个生命周期内存在,只被初始化一次,通常用于多次函数调用之间需要保留的值。
- 生命周期:静态变量在程序启动时分配内存,在程序结束时释放内存。
- 作用域:全局静态变量的作用域是整个程序,局部静态变量的作用域是声明该变量的函数或代码块内。
- 初始化:静态变量只被初始化一次,默认值为0。
- 存储位置:静态变量通常存储在全局数据区,而不是栈上。
- 静态成员函数:
- 在类内部使用static关键字修饰的成员函数就是静态函数。
- 静态函数属于类而不是类的实例,可以通过类名直接调用,无需创建对象。
- 静态函数不能直接访问非静态成员函数、非静态成员变量。
- 静态成员变量:
- 在类中使用static修饰的成员变量是静态成员变量。
- 所有类的对象共享一个静态成员变量的副本。
- 静态成员变量除了在类中声明,还需要在类外单独定义,方便为其分配内存。
define、typedef和inline的区别
- define
- 只是简单的字符串替换,并没有类型检查
- 是在编译的预处理阶段起作用
- 可以用来防止头文件的重复引用
- 不分配内存,只是给出立即数,有多少次使用就进行多少次替换
- typedef
- 有对应的数据类型,是要进行判断的
- 是在编译、运行的时候起作用
- 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
- inline
- inline是将内联函数编译完成后生成的函数体直接插入被调用的地方,减少了压栈、跳转和返回的操作,节省了普通函数调用时的额外开销
- 内联函数是一种特殊的函数,会进行类型检查
- 它是一种对编译器的请求,编译器可能拒绝这种请求
【补充】C++中的inline限制:
- 不能存在任何形式的循环语句
- 不能存在过多的判断语句
- 函数体不能太过庞大
- 内联函数声明必须在调用语句之前
const和define的区别
const用于定义常量;define也用于定义宏的,而宏也可以定义常量。对于定义常量这一点,两者存在一定区别:
- const生效与编译阶段;define生效与预处理阶段
- const定义的常量会存储在内存当中的(也就是需要分配空间的);define定义的常量直接是操作数,直接进行替换,并不会分配内存
- const定义的常量是带有类型的,需要进行类型检查;define定义的常量不带类型
new和malloc的区别
- new分配内存失败会抛出bac_alloc异常,但是不会返回NULL;malloc分配内存失败会返回NULL
- 使用new操作符进行内存申请不需要指定内存块的大小;malloc需要显式支出申请的内存大小
- 可以使用operator对new进行重载;malloc不允许被重载
- new会调用对象的构造、析构函数;malloc并不会
- new\delete是C++运算符;malloc是C++/C标准库函数
- new操作符是从自由存储区上位对象动态分配内存;malloc函数是从堆上动态分配内存
总结示意图:
constexpr和const
- const表示“只读”含义;constexpr表示“常量”含义
- const可以定义编译器常量,也可以定义运行期常量;constexpr只能定义编译期常量
这里要注意一下两之间的关系:
如果将一个成员函数或者变量标记为constexpr,那么就顺带将其标记为了const。反之则不成立,一个函数或者变量是const的,并不就是constexpr的。
constexpr变量
d我们可使用constexpr来声明一个变量,由编译器来验证一个变量的值是否是常量表达式:
也就是说使用constexpr来声明变量,必须使用常量表达式进行初始化。
如果我们使用constexpr来声明定义一个指针,constexpr只对于指针有效,与指针所指向的对象无关:
constexpr int* p1 = nullptr; // 编译期常量指针,指针值在编译期已知,语义类似 int* const const int* p2 = nullptr; // 底层 const,指向常量的指针:指针可改,指向的值不可改 int* const p3 = nullptr; // 顶层 const,常量指针:指针本身不可改,指向的值可改
constexpr函数
constexpr是能够在编译期被求值的函数,换句话说是能够用于常量表达式的函数。
C++11要求,constexpr修饰的函数的返回类型,所有的形参都是字面值类型,函数体有且只有一条return语句。
例如:
constexpr int new() {return 42;}值得注意的是,为了constexpr修饰的函数能够在编译期展开,会被隐式地转换成内联函数。
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构造函数析构函数是平凡的(trivial)或默认的
满足其他一些技术条件
volatile
这是一个与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。
面试可能提问:
- 为什么后置返回对象而不是引用?
因为后置为了返回旧值,创建了一个临时对象来接收,但是在函数结束时这个临时对象被销毁了,如果此时返回引用,引用的绑定目标都没有了。
- 为什么后置前面要加const?
这是为了防止误用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++++用法,直接报错!
- 处理用户自定义类型
最好使用前置++,因为它不会创建临时对象,进而不会带来构造和析构的额外开销。

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3vtuwevgbfms4
更多推荐






所有评论(0)