首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【小陈背八股-C++】Day02-C++核心特性面试指南:数据类型与内存管理完全解读

【小陈背八股-C++】Day02-C++核心特性面试指南:数据类型与内存管理完全解读

作者头像
小陈又菜
发布2025-12-24 11:10:34
发布2025-12-24 11:10:34
2100
举报

前言

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

1. 数据类型

整型 short、int、long和long long

  • C++整型数据长度标准
  1. short至少16位
  2. int至少与short一样长
  3. long至少32位,且至少与int一样长
  4. long long至少32位,且至少与long一样长

头文件中climits定义了符号常量:例如,INT_MAX表示int的最大值,INT_MIN表示int的最小值。


无符号类型

  • 即不存储负数值的整型,能够增大变量能够存储的最大值,数据的长度不变。
  • int被设置为自然长度,也就是计算机处理起来效率最高的长度,所以选择类型是一般选择int类型。

关键字

const关键字 根据const放置的位置不同这里有两个容易混淆的概念:

  • 指针常量
  • 常量指针

以下解释来自《C++ primer第五版》,

  • 常量指针:

指针本身就是常量(顶层const),声明必须要进行初始化,初始化之后就不能改变其值(也就是指针存放的地址),换句话说就是不能指向其他对象。但是可以修改该指针指向的对象的值。

  • 指针常量:

指针指向一个常量(底层const),不能通过该指针来修改指向的对象的值。但是因为指针本身不是常量,所以可以修改指针指向的对象(也就是可以指向别的地址)。


const关键字的作用 const关键字主要用于指定变量、指针、引用、成员函数等的性质:

  • 常量变量:声明常量,表示变量的值不能被修改。
  • 指针和引用:声明指针常量,表示指针所指向的值是常量,不能通过指针修改。声明引用常量,表示引用的值是常量,不能通过引用修改。
  • 常量对象:声明对象为常量,表示对象的成员变量不能被修改。
  • 常引用参数:声明函数参数为常量引用,表示函数不会修改传入的参数。
  • 常量指针参数:声明函数参数为指针常量,表示该函数不会通过该指针修改传入的参数。

static关键字 static关键字主要用于控制变量和函数的声明周期、作用域和访问权限:

  • 静态变量:
  1. 静态变量在程序的整个生命周期内存在,只被初始化一次,通常用于多次函数调用之间需要保留的值。
  2. 生命周期:静态变量在程序启动时分配内存,在程序结束时释放内存。
  3. 作用域:全局静态变量的作用域是整个程序,局部静态变量的作用域是声明该变量的函数或代码块内。
  4. 初始化:静态变量只被初始化一次,默认值为0。
  5. 存储位置:静态变量通常存储在全局数据区,而不是栈上。
  • 静态成员函数:
  1. 在类内部使用static关键字修饰的成员函数就是静态函数。
  2. 静态函数属于类而不是类的实例,可以通过类名直接调用,无需创建对象。
  3. 静态函数不能直接访问非静态成员函数、非静态成员变量。
  • 静态成员变量:
  1. 在类中使用static修饰的成员变量是静态成员变量。
  2. 所有类的对象共享一个静态成员变量的副本。
  3. 静态成员变量除了在类中声明,还需要在类外单独定义,方便为其分配内存。

define、typedef和inline的区别

  • define
  1. 只是简单的字符串替换,并没有类型检查
  2. 是在编译的预处理阶段起作用
  3. 可以用来防止头文件的重复引用
  4. 不分配内存,只是给出立即数,有多少次使用就进行多少次替换
  • typedef
  1. 有对应的数据类型,是要进行判断的
  2. 是在编译、运行的时候起作用
  3. 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
  • inline
  1. inline是将内联函数编译完成后生成的函数体直接插入被调用的地方,减少了压栈、跳转和返回的操作,节省了普通函数调用时的额外开销
  2. 内联函数是一种特殊的函数,会进行类型检查
  3. 它是一种对编译器的请求,编译器可能拒绝这种请求

【补充】C++中的inline限制:

  1. 不能存在任何形式的循环语句
  2. 不能存在过多的判断语句
  3. 函数体不能太过庞大
  4. 内联函数声明必须在调用语句之前

const和define的区别 const用于定义常量;define也用于定义宏的,而宏也可以定义常量。对于定义常量这一点,两者存在一定区别:

  1. const生效与编译阶段;define生效与预处理阶段
  2. const定义的常量会存储在内存当中的(也就是需要分配空间的);define定义的常量直接是操作数,直接进行替换,并不会分配内存
  3. const定义的常量是带有类型的,需要进行类型检查;define定义的常量不带类型

new和malloc的区别

  1. new分配内存失败会抛出bac_alloc异常,但是不会返回NULL;malloc分配内存失败会返回NULL
  2. 使用new操作符进行内存申请不需要指定内存块的大小;malloc需要显式支出申请的内存大小
  3. 可以使用operator对new进行重载;malloc不允许被重载
  4. new会调用对象的构造、析构函数;malloc并不会
  5. new\delete是C++运算符;malloc是C++/C标准库函数
  6. new操作符是从自由存储区上位对象动态分配内存;malloc函数是从堆上动态分配内存

总结示意图:


constexpr和const

  • const表示“只读”含义;constexpr表示“常量”含义
  • const可以定义编译器常量,也可以定义运行期常量;constexpr只能定义编译期常量

这里要注意一下两之间的关系:

如果将一个成员函数或者变量标记为constexpr,那么就顺带将其标记为了const。反之则不成立,一个函数或者变量是const的,并不就是constexpr的。

  • constexpr变量

d我们可使用constexpr来声明一个变量,由编译器来验证一个变量的值是否是常量表达式:

也就是说使用constexpr来声明变量,必须使用常量表达式进行初始化。

如果我们使用constexpr来声明定义一个指针,constexpr只对于指针有效,与指针所指向的对象无关:

代码语言:javascript
复制
constexpr int* p1 = nullptr; // 编译期常量指针,指针值在编译期已知,语义类似 int* const
const int* p2 = nullptr;     // 底层 const,指向常量的指针:指针可改,指向的值不可改
int* const p3 = nullptr;     // 顶层 const,常量指针:指针本身不可改,指向的值可改
  • constexpr函数

constexpr是能够在编译期被求值的函数,换句话说是能够用于常量表达式的函数。

C++11要求,constexpr修饰的函数的返回类型,所有的形参都是字面值类型,函数体有且只有一条return语句。

例如:

代码语言:javascript
复制
constexpr int new() {return 42;}

值得注意的是,为了constexpr修饰的函数能够在编译期展开,会被隐式地转换成内联函数。

  • constexpr构造函数

构造函数肯定不能是const的,但是对于字面值常量类的构造函数可以是constexpr的:

代码语言:javascript
复制
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 修饰

这里补充一下什么是字面值常量类:

  1. 所有数据成员都是字面值类型
  2. 至少有一个constexpr构造函数
  3. 析构函数是平凡的(trivial)或默认的
  4. 满足其他一些技术条件

volatile

这是一个与const绝对对立的,类型修饰符。该修饰符会影响编译器编译的结果,用该关键字修饰的变量,表示该变量随时可能发生改变,告诉编译器所有与改变变量有关的运算不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容

下面举一个例子进行说明:

代码语言:javascript
复制
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:

代码语言:javascript
复制
volatile int flag = 0;  // 告诉编译器:这个变量可能被意外修改

void wait() {
    while (flag == 0) {
        // 现在编译器不会优化掉内存访问
    }
}

//那么生成的汇编如下:

//每次循环都从内存重新加载
loop_start:
mov eax, [flag]  ; 每次都从内存读取
test eax, eax
je loop_start    ; 为0则继续循环

前置++与后置++

代码语言:javascript
复制
self &operator++() {
    node = (linktype)((node).next);
    return *this;
}

const self operator++(int) {
    self tmp = *this;
    ++*this;
    return tmp;
}

两者性能上的区别:

  • 前置++:直接修改对象,返回自身引用(没有临时对象)
  • 后置++:需要保存旧值,修改对象,然后返回旧值(有临时对象)

C++为了区分前后置,后置++有一个int哑元参数,调用的时候编译器会默认给后置++生成一个0。

面试可能提问:

  • 为什么后置返回对象而不是引用?

因为后置为了返回旧值,创建了一个临时对象来接收,但是在函数结束时这个临时对象被销毁了,如果此时返回引用,引用的绑定目标都没有了。

  • 为什么后置前面要加const?

这是为了防止误用i++++,连续的两次调用后置++重载,我们举例进行说明,

如果我们不加const,那么可能出现下面情况:

代码语言:javascript
复制
// 用户可能期望:
c++++;  // "c应该变成7吧?"

// 实际上:
// 第一次c++:返回值为5的临时对象,c变成6
// 第二次++:对临时对象自增(临时对象变成6)
// 临时对象被销毁,c仍然是6!
// 结果:用户期望c=7,实际c=6

程序会成功运行,但是结果是错误的,所以为了防止不可预测的后果,我们使用const来直接禁止这种使用的合法化:

代码语言:javascript
复制
// 尝试连续后置++
c++++;  //编译错误!
// 第一次c++:返回const Counter临时对象
// 第二次++:尝试对const对象调用operator++
// 编译器报错:不能修改const对象

也就是如果遇到i++++用法,直接报错!

  • 处理用户自定义类型

最好使用前置++,因为它不会创建临时对象,进而不会带来构造和析构的额外开销。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1. 数据类型
  • 关键字
    • constexpr和const
    • volatile
  • 前置++与后置++
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档