6 数据抽象和类II
对象的内存布局
普通数据成员储存在栈区或者堆区。普通成员函数存储在程序的代码区,而不是对象实例中,所有对象共享同一份函数代码。如果每个对象都存储一遍成员函数,占用内存会增加。
静态联编:一种在编译阶段就确定函数调用与具体函数实现之间关联的机制,可以提升效率。当程序调用一个对象的方法时,编译器会根据该对象的声明类型(即变量定义时的类型),而不是它在运行时的实际类型,来决定调用哪个函数。类的普通成员函数(非虚函数)默认都采用静态联编。
拷贝构造函数
用一个已经存在的对象来初始化一个正在创建的新对象。
类名(const 类名& other)
- 为什么要用引用? 如果不用引用而是传值(不加&),那么在传递参数时又会调用拷贝构造函数,导致无限递归。
- 为什么要加 const? 为了保证在拷贝过程中,源对象的数据不会被意外修改。
触发时机:
- 对象初始化:用一个已有对象初始化新对象。
MyClass obj1; MyClass obj2 = obj1; 或 MyClass obj2(obj1); - 函数传参(按值传递):当对象作为参数传递给函数时,会生成一个副本。
- 函数返回值(按值返回):当函数返回一个对象时,通常会生成一个临时副本。
注:现代编译器中,当函数返回一个命名了的局部变量时,编译器会尝试将这个局部变量直接构造在调用者的返回值内存位置上,从而避免后续的拷贝或移动。
#include <iostream>
class Test {
public:
Test() { std::cout << "构造函数被调用\n"; }
~Test() { std::cout << "析构函数被调用\n"; }
};
Test createObject() {
Test local_obj; // 命名局部对象
return local_obj; // 编译器会尝试应用 NRVO
}
int main() {
Test obj = createObject();
std::cout << "--- main函数即将结束 ---\n";
return 0;
}
在这个程序中,只调用一次构造函数和一次析构函数,不调用拷贝构造函数。
Test obj;
obj = createObject();
如果这样写,obj 已经存在,只能接受“赋值”,无法享受 NRVO 的就地构造福利。但仍然不调用拷贝构造函数,因为这里会调用赋值运算符 = 。
含指针成员的类通常应重写以下内容:
- 构造函数(及拷贝构造函数)中分配内存,深复制策略
- = 操作重写,完成对象深复制策略
- 析构函数中释放内存
浅拷贝:
- 默认行为:如果你不写拷贝构造函数,编译器会送你一个默认的。它只会按字节复制内存中的数据。
- 问题:如果类中有指针成员(指向堆区内存),浅拷贝只会复制指针的地址。这意味着两个对象会指向同一块内存。当这两个对象析构时,同一块内存会被释放两次(Double Free),导致程序崩溃。
深拷贝:
- 自定义行为:你需要自己编写拷贝构造函数。
- 做法:不仅复制指针的地址,还要重新分配一块内存,并将原内存中的数据复制过去。这样两个对象就完全独立,互不干扰。
=运算符重载:
A a;
a = b;
类名& operator=(const 类名& other) {
if (this == &other) // 1. 自赋值检查
return *this;
// 2. 释放当前对象原有资源
delete[] 指针成员;
// 3. 深拷贝分配新内存并复制数据
指针成员 = new char[strlen(other.指针成员) + 1];
strcpy(指针成员, other.指针成员);
return *this; // 4. 返回自身引用
}
深入const
const 引用
- const 引用可以绑定右值,普通引用不行
- const 引用是只读的,不能修改
- const 引用遇到类型不匹配时,会生成临时变量绑定
1. const 引用延长临时对象的生命周期
在 C++ 中,你不能将一个非常量引用绑定到一个右值上,int& 不能绑字面量 15,因为右值是临时的,不能被修改。当你写 const int &c = 15; 时,编译器会做两件事:
- 创建一个临时的匿名变量(例如
temp)来存储15。 - 让引用
c指向这个临时变量。
const 引用会延长这个临时变量的生命周期,使其在当前作用域一直有效。
2. const 引用的“只读”特性
const 引用的本质是“只读视图”。
const int &a = b;让a成为b的别名。- 你不能通过
a去修改b(a = 12会报错),因为a是const的。 - 但是直接修改
b,a的值也会跟着变。
const 保护的是引用本身,而不是原始数据(原始数据可以通过其他非 const 的途径被修改)。
3. 隐式类型转换与临时对象
当引用的类型与初始化值的类型不匹配时(const int & 初始化为 double),编译器会介入。类型不匹配 → 生成临时变量
double b = 3.14;
const int &a = b;
b = 11;
double→int要转换- 编译器生成临时 int
a绑的是临时变量,不是b- 所以
b改成 11,a还是 3
4. 函数参数传递的规则
const 引用作为参数既能接收左值(具名变量),也能接收右值(字面量、临时对象)。
int const_ref_in_para(const string& s) { ... }
int const_ref_in_para(const int& s) { ... }
-
例子:
const_ref_in_para("I love c++");:字符串字面量是右值,编译器会构造一个临时string对象传给它。const_ref_in_para(3.14);:double转int产生了临时对象。
-
优势:通用性强,且避免了拷贝大对象的开销。
非 const 引用作为参数只能接收非 const 的左值。
int no_const_ref_in_para(string& s) { ... }
int no_const_ref_in_para(int& s) { ... }
no_const_ref_in_para("I love c++");和no_const_ref_in_para(3.14); 会报错。因为类型转换产生的也是临时对象(右值)。
const成员
const数据成员
用const修饰的类成员变量,不能在构造函数体内赋值,只能通过成员初始化列表完成初始化。
const成员函数
不能修改非静态成员变量
- 函数内不能调用非const成员函数(因为非const函数可能修改对象)
void print() const;
this指针的本质变化
- 普通成员函数的
this:A* const(可修改指向的对象) - const成员函数的
this:const A* const(不可修改指向的对象,只读)
const对象只能调用const成员函数
- 用
const修饰的对象,只能调用const成员函数,不能调用非const成员函数(因为非const函数可能修改对象,违反const语义)
const A obj; // const对象
obj.print(); // 合法:print是const成员函数
// obj.set(10); // 编译报错:set是非const成员函数
const成员函数仅可修改mutable修饰的成员变量
class A {
private:
mutable int cache = 0; // mutable成员
public:
void update() const {
cache++; // 合法:mutable成员可在const函数中修改
}
};