3.1 深拷贝与浅拷贝
前置补充:new 堆内存完整用法
-
作用:在堆区开辟独立内存,返回对应类型指针;堆内存不会随局部函数销毁,必须手动 delete 释放。
-
两种开辟写法与配套释放规则
// 单个数据堆内存
int* p1 = new int(180);
delete p1; // 单个释放,不加[]// 数组堆内存
int* p2 = new int[10];
delete[] p2; // 数组释放必须加[] -
注意:栈与堆区分 局部变量
int a:存储在栈,函数执行完毕自动销毁;new int(10):存储在堆,不执行 delete 会内存泄漏,重复 delete 程序崩溃。
一、浅拷贝
定义:未手动编写拷贝构造时,编译器自动生成默认拷贝构造,仅逐字节复制所有成员值。 问题:若类包含堆指针,多个对象会共用同一块堆内存;析构时重复执行 delete,程序崩溃。 错误示例代码
class Person {
public:
int age;
int* height;
Person(int a, int h) {
age = a;
height = new int(h);
}
~Person() {
delete height;
height = nullptr;
}
};
void test() {
Person p1(18, 180);
Person p2 = p1; // 触发浅拷贝,共用堆地址
// 函数结束,先后释放p2、p1的堆内存,重复delete崩溃
}
二、深拷贝
核心逻辑:不复制原有堆地址,通过 new 开辟全新独立堆内存,拷贝原堆数值,每个对象独占堆空间,析构无冲突。 标准实现代码
class Person {
public:
int age;
int* height;
Person(int a, int h) {
age = a;
height = new int(h);
}
// 深拷贝构造函数
Person(const Person& p) {
age = p.age;
height = new int(*p.height); // 新建堆,复制数值
}
~Person() {
delete height;
height = nullptr;
}
};
void test() {
Person p1(18, 180);
Person p2 = p1; // 两块独立堆内存,无释放冲突
}
深浅拷贝对比表
| 对比维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 实现来源 | C++ 编译器自动生成 | 程序员手动编写拷贝构造函数 |
| 堆指针处理 | 仅复制堆地址,多个对象共用一块堆 | new 开辟全新独立堆,复制堆内数值 |
| 堆内存所有权 | 多个对象共享同一块堆 | 每个对象拥有专属堆空间,互不干扰 |
| 析构函数风险 | 重复释放同一块堆,程序直接崩溃 | 各自释放自有堆,无内存错误 |
| 适用类结构 | 类中无 new 创建的指针 / 数组成员 | 类包含 int*、数组等堆指针成员 |
| 底层执行逻辑 | 直接赋值所有变量:height = p.height |
先拷贝普通变量,再new int(*p.height)新建堆 |
| 赋值运算符隐患 | 重载 = 不处理也会重复释放 | 重载 = 时同步实现深拷贝即可规避 |
拓展:赋值运算符重载深拷贝实现
Person& operator=(const Person& p) {
// 1.释放自身旧堆,防止内存泄漏
if (height != nullptr) {
delete height;
}
// 2.新建堆内存
height = new int(*p.height);
// 3.拷贝普通成员
age = p.age;
return *this;
}
1. 函数头部:Person& operator=(const Person& p)
operator=:C++ 固定语法,代表重载赋值运算符=;- 参数
const Person& p:传入要拷贝的源对象,用引用避免拷贝构造,const 保证不会修改源对象; - 返回值
Person&:返回对象引用,核心作用:支持连续赋值p1=p2=p3如果返回值是Person(值),会产生临时对象,无法链式连续赋值。
注释: 重写赋值运算符=,两个对象用等号赋值时,自动做深拷贝:先清空自己旧堆,再新建独立堆复制数据,避免重复释放堆导致程序崩溃。
3.2 初始化列表
一、作用
构造函数专用初始化语法,执行时机早于函数体内赋值,运行效率更高;存在四类成员,仅能通过初始化列表完成初始化,函数体内赋值无效。
二、标准语法
类名(形参列表):成员1(初始值),成员2(初始值){ 构造逻辑 }
三、强制使用初始化列表的 4 种场景
-
const 常量成员:常量创建时必须初始化,创建后不可修改;
-
引用成员:引用定义时必须绑定内存,无法后期赋值;
-
无参构造不可调用的自定义类对象成员;
-
子类调用父类有参构造。 示例代码
class Test {
public:
const int num;
int& ref;
Test(int n, int& r):num(n), ref(r) {}
};
四、核心规则
成员初始化顺序仅由类内从上到下的声明顺序决定,与初始化列表书写顺序无关。 错误示例
class Person {
public:
int a;
int b;
Person(int x):b(x), a(b) {} // 先初始化a,此时b未创建,a为随机垃圾值
};
五、优势
- 内置类型:减少临时拷贝,提升执行效率;
- 自定义对象:直接调用对应构造,避免先默认构造再赋值的多余操作;
- 解决 const、引用成员无法在函数内赋值的语法限制。
3.3 类对象作为类成员
一、概念
一个类的成员变量为另一个类的实例对象,称为组合关系。 示例
class Phone {
public:
string brand;
Phone(string b) { brand = b; }
};
class Person {
public:
string name;
Phone phone; // Phone对象作为Person成员
Person(string n, string b):name(n), phone(b) {}
};
二、构造与析构固定执行顺序
构造顺序:先执行所有对象成员的构造函数 → 再执行当前类自身构造函数 析构顺序:先执行当前类自身析构函数 → 再依次析构所有对象成员 完整演示代码
#include<iostream>
#include<string>
using namespace std;
class Phone {
public:
string brand;
Phone(string b) {
brand = b;
cout << "【手机 构造】" << endl;
}
~Phone() {
cout << "【手机 析构】" << endl;
}
};
class Person {
public:
string name;
Phone phone;
Person(string n, string b):name(n), phone(b) {
cout << "【人类 构造】" << endl;
}
~Person() {
cout << "【人类 析构】" << endl;
}
};
void test() {
Person p("张三", "华为");
}
输出结果 【手机 构造】 【人类 构造】 【人类 析构】 【手机 析构】
三、硬性规则
若内部对象成员没有无参构造,当前类必须在初始化列表显式调用其有参构造,否则编译报错。
3.4 静态成员
静态成员分为静态成员变量、静态成员函数,统一归属整个类,不属于任意单个对象。
一、静态成员变量
-
核心特性 ① 内存位置:编译阶段分配至全局数据区; ② 生命周期:程序启动创建,程序结束释放; ③ 共享性:所有对象共用同一份数据,一处修改全局同步; ④ 语法限制:类内仅声明,必须在类外全局作用域初始化,不可在构造函数内初始化。
-
声明与初始化代码
class Person {
public:
static int m_A; // 类内声明,不分配内存
};
// 类外初始化,无需加static
int Person::m_A = 100; -
两种访问方式 方式 1(推荐):
Person::m_A无需创建对象 方式 2:Person p; p.m_A通过对象访问 -
权限规则:受 public/protected/private 权限控制,私有静态变量外部不可直接访问。
二、静态成员函数
-
核心规则 ① 无 this 指针,this 存储单个对象地址,静态不归属对象; ② 仅可访问静态成员变量、静态函数,无法访问普通成员; ③ 调用方式与静态变量一致,类名 / 对象均可调用; ④ 支持访问权限修饰,private 静态函数外部无法调用。 示例代码
class Person {
public:
static int m_A;
int m_B;
static void func() {
cout << m_A; // 合法
// cout << m_B; 报错,无法访问普通成员
}
};
int Person::m_A = 10;
普通 / 静态成员完整对比表
| 对比维度 | 普通成员变量 | 静态成员变量 | 普通成员函数 | 静态成员函数 |
|---|---|---|---|---|
| 归属主体 | 属于每一个独立对象 | 属于整个类,所有对象共享 | 调用时绑定单个对象 | 仅属于类,不绑定任何对象 |
| 内存存放位置 | 栈 / 堆,每个对象单独分配 | 全局数据区,程序启动分配 | 代码区,多对象共用一份函数体 | 代码区,全局唯一一份函数 |
| 是否存在 this 指针 | 存在,this 指向调用对象 | 不存在 | 存在 this 指针 | 不存在 this 指针 |
| 可访问成员范围 | 普通、静态成员均可访问 | 普通、静态函数均可访问 | 普通变量、静态变量均可访问 | 仅能访问静态变量、静态函数 |
| 初始化位置 | 构造函数 / 初始化列表 | 类外全局作用域初始化 | 无需初始化 | 无初始化概念 |
| 调用方式 | 必须实例对象调用p.m_A |
对象 / 类名Person::m_A均可 |
必须实例对象调用p.func() |
无需对象,直接Person::func() |
| 生命周期 | 对象创建产生,对象销毁释放 | 程序运行全程存在,程序结束销毁 | 对象存在即可调用 | 程序运行全程可调用 |
静态成员优缺点
优点
- 替代全局变量,封装在类内,命名无冲突、安全性更高;
- 无需创建对象即可直接使用;
- 全局仅分配一份内存,节省空间。
缺点
- 生命周期跟随程序,对象销毁不会释放静态内存;
- 多线程并发修改时易产生数据竞争;
- 静态函数无法操作每个对象独有的普通属性。
易错点补充
构造函数内对静态变量执行赋值,不等于初始化,多次创建对象会反复覆盖初始值,标准初始化必须写在类外部。
class Person {
public:
static int m_A;
Person() {
m_A = 100; // 仅赋值,不是初始化
}
};
// 标准初始化写法
int Person::m_A = 100;