C++ 中的 const 、 mutable与new
C++ 中的 const
与 mutable
在 C++ 中,const
可以看作是一种“承诺”:承诺某些东西在程序执行过程中不会改变。它不仅可以修饰变量,还可以修饰指针、类成员函数、函数参数等,是一种广泛用于只读保护和接口契约的机制。
1. 基本常量
const int MAX_AGE = 90;
MAX_AGE
是只读变量,不能被修改。对比普通变量,这是一个编译期约束。
2. 指针与 const
2.1 指向常量的指针
const int* a = new int;
a = &MAX_AGE; // 合法,指针本身可变
*a = 2; // ❌ 不合法,不能修改指针指向的数据
const
在*
左边 → 指针指向的数据是常量,指针可变。
2.2 常量指针
int* const a = new int;
a = &MAX_AGE; // ❌ 不合法,指针本身不能改
*a = 2; // 合法,可以修改指向的数据
const
在*
右边 → 指针本身是常量,指向的数据可变。
2.3 指向常量的常量指针
const int* const a = &MAX_AGE;
// a 和 *a 都不可修改
3. 类成员函数中的 const
在类成员函数后加 const
,表示该方法不会修改对象的成员变量(除了 mutable
修饰的成员)。这样,常量对象或常量引用就可以调用该方法。
class Entity {
private:int m_X, m_Y;
public:int GetX() const {// m_X = 2; // ❌ 不合法return m_X;}
};
尾部
const
使方法成为只读方法。如果对象是
const Entity e;
或通过const Entity& e
访问,只能调用const
成员函数。
3.1 可同时提供 const / 非 const 版本
为了兼容常量对象和非常量对象,可以提供两个版本的成员函数:
class Entity {
private:int m_X, m_Y;
public:int GetX() const { // const 版本return m_X;}int GetX() { // 非 const 版本return m_X;}
};
对于
const Entity& e
,默认调用 const 版本。对于普通对象
Entity e
,调用非 const 版本。
4. mutable
mutable
修饰成员变量,即使在 const 方法中,也可以修改它。常用于缓存或记录状态。
class Entity {
private:int m_X, m_Y;mutable int var;
public:int GetX() const {var = 2; // 合法return m_X;}
};
这样可以在 const 方法中修改
var
,而不会破坏 const 约束。
5. const 指针与成员函数结合示例
void PrintEntity(const Entity* e) {e = nullptr; // ✅ 合法,修改指针本身std::cout << e->GetX(); // ✅ 可以调用const成员函数
}
⚠️问题:
const Entity* e
→ 指针e
可以指向其他地址,但不能修改它指向的对象内容(*e
)。调用
GetX()
合法,因为GetX()
是const
。void PrintEntity(const Entity& e) {// e = Entity(); // ❌ 不合法,不能修改对象内容std::cout << e.GetX() << std::endl; // ✅ 可以调用const成员函数 }
const Entity& e
→e
是 对常量对象的引用,不能修改对象。如果
GetX()
没有const
,调用就会报错:因为编译器认为可能修改对象。
调用
const
成员函数是安全的。e = Entity();
本质上是:
创建了一个临时的
Entity
对象。临时对象通常在表达式结束时就会被销毁。尝试把它 赋值给
e
,也就是修改e
指向的对象内容。e
是 const 引用,指向的对象不可修改。赋值操作会修改对象内容,所以编译器报错。
6. lambda 表达式中的 mutable
默认捕获
[=]
是按值捕获,内部变量是 const 副本,不能修改。可以用
mutable
来允许修改副本。
int x = 8;
auto f = [=]() mutable {x++;std::cout << x << std::endl;
};
f(); // 输出 9,外部 x 不变
捕获引用
[&]
时,直接修改外部变量。
int x = 8;
auto f = [&]() {x++;
};
f();
std::cout << x << std::endl; // 输出 9,外部 x 被修改
7. 总结
用法 | 说明 |
---|---|
const int x | x 只读,不可修改 |
const int* p | 指向常量的指针,*p 不能改,p 可改 |
int* const p | 常量指针,p 不可改,*p 可改 |
const int* const p | 指向常量的常量指针,p 和 *p 都不可改 |
int GetX() const | const 成员函数,不修改对象 |
mutable int var | 即使在 const 方法中也可修改 |
const T& | 常量引用,安全高效传参 |
mutable + lambda | 允许修改按值捕获的副本 |
C++ 中的 new
操作符与动态内存管理
在 C++ 中写程序时,尤其是涉及类和对象的时候,内存管理和性能优化是必须关心的问题。new
是 C++ 提供的一个关键工具,用于在 堆(heap) 上分配内存。
1. new
的基本用法
new
的主要作用是在堆上分配内存并调用构造函数。例如:
int* a = new int; // 分配单个 int,默认初始化
int* b = new int(5); // 分配并初始化为 5
int* c = new int[10]; // 分配 10 个 int 的数组
对于类对象:
Entity* e1 = new Entity(); // 默认构造函数初始化
Entity* e2 = new Entity[10]; // 分配对象数组,每个对象调用构造函数
释放内存
动态分配的内存必须手动释放:
delete e1; // 释放单个对象
delete[] e2; // 释放对象数组
2. 默认初始化 vs 值初始化
在使用 new
时,初始化方式不同,行为也不同:
Entity* e = new Entity(); // 值初始化Entity* e = new Entity; // 默认初始化
值初始化:
内置类型成员(
int
、float
、指针等)会被 零初始化。类类型成员(如
std::string
)会调用 默认构造函数。
默认初始化:
内置类型成员保持 随机垃圾值。
类类型成员同样调用默认构造函数。
3. new
的工作原理
new
本质上是一个操作符,可以理解为函数:
在堆上分配指定大小的内存。
返回一个
void*
指针(无类型的指针)。调用对象的构造函数进行初始化。
返回指定类型的指针。
实际上,
new
底层通常会调用 C 函数malloc
来分配内存,然后再调用构造函数。
例如:Entity* e = new Entity(); // 相当于 Entity* e = (Entity*)malloc(sizeof(Entity)); // 再调用构造函数初始化
为什么需要类型指针?
指针只是内存地址,之所以有类型,是为了知道如何从该地址读取多少字节以及如何操作这些字节。
4. 堆内存管理注意事项
new
分配的内存不会自动释放,必须调用delete
。使用
new[]
分配数组时,必须用delete[]
释放。动态内存管理不当会导致:
内存泄漏(未释放)
内存重用问题(未释放的内存仍被占用)
5. placement new(定位 new)
placement new
允许你在已分配的内存上直接构造对象,而不分配新的堆内存:
int* buffer = new int[200]; // 分配内存
Entity* e = new(buffer) Entity(); // 在 buffer 地址上构造对象
这不会分配新内存,只调用构造函数。
常用于 内存池 或 自定义分配策略。
6. 对比 malloc 与 new
特性 | malloc | new |
---|---|---|
分配内存 | 仅分配 | 分配内存并调用构造函数 |
返回类型 | void* | 指定类型指针 |
初始化 | 不初始化 | 可值初始化或默认初始化 |
释放方式 | free | delete / delete[] |
C++ 推荐方式 | 不推荐 | 推荐,支持构造函数调用 |
7. 小结
new
是 C++ 的动态内存操作符,负责堆内存分配 + 构造函数调用。使用
delete
或delete[]
来释放内存。优先使用 值初始化 保证内置类型成员为零,类成员正确构造。
对性能敏感时,可以使用 placement new 或内存池优化。