C++学习笔记(三):类与对象拓展学习笔记

3.1 深拷贝与浅拷贝

前置补充:new 堆内存完整用法

  1. 作用:在堆区开辟独立内存,返回对应类型指针;堆内存不会随局部函数销毁,必须手动 delete 释放。

  2. 两种开辟写法与配套释放规则

    // 单个数据堆内存
    int* p1 = new int(180);
    delete p1; // 单个释放,不加[]

    // 数组堆内存
    int* p2 = new int[10];
    delete[] p2; // 数组释放必须加[]

  3. 注意:栈与堆区分 局部变量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)
  1. operator=:C++ 固定语法,代表重载赋值运算符=
  2. 参数 const Person& p:传入要拷贝的源对象,用引用避免拷贝构造,const 保证不会修改源对象;
  3. 返回值 Person&:返回对象引用,核心作用:支持连续赋值 p1=p2=p3 如果返回值是Person(值),会产生临时对象,无法链式连续赋值。

注释: 重写赋值运算符=,两个对象用等号赋值时,自动做深拷贝:先清空自己旧堆,再新建独立堆复制数据,避免重复释放堆导致程序崩溃。

3.2 初始化列表

一、作用

构造函数专用初始化语法,执行时机早于函数体内赋值,运行效率更高;存在四类成员,仅能通过初始化列表完成初始化,函数体内赋值无效。

二、标准语法

复制代码
类名(形参列表):成员1(初始值),成员2(初始值){ 构造逻辑 }

三、强制使用初始化列表的 4 种场景

  1. const 常量成员:常量创建时必须初始化,创建后不可修改;

  2. 引用成员:引用定义时必须绑定内存,无法后期赋值;

  3. 无参构造不可调用的自定义类对象成员;

  4. 子类调用父类有参构造。 示例代码

    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为随机垃圾值
};

五、优势

  1. 内置类型:减少临时拷贝,提升执行效率;
  2. 自定义对象:直接调用对应构造,避免先默认构造再赋值的多余操作;
  3. 解决 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 静态成员

静态成员分为静态成员变量、静态成员函数,统一归属整个类,不属于任意单个对象。

一、静态成员变量

  1. 核心特性 ① 内存位置:编译阶段分配至全局数据区; ② 生命周期:程序启动创建,程序结束释放; ③ 共享性:所有对象共用同一份数据,一处修改全局同步; ④ 语法限制:类内仅声明,必须在类外全局作用域初始化,不可在构造函数内初始化。

  2. 声明与初始化代码

    class Person {
    public:
    static int m_A; // 类内声明,不分配内存
    };
    // 类外初始化,无需加static
    int Person::m_A = 100;

  3. 两种访问方式 方式 1(推荐):Person::m_A 无需创建对象 方式 2:Person p; p.m_A 通过对象访问

  4. 权限规则:受 public/protected/private 权限控制,私有静态变量外部不可直接访问。

二、静态成员函数

  1. 核心规则 ① 无 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()
生命周期 对象创建产生,对象销毁释放 程序运行全程存在,程序结束销毁 对象存在即可调用 程序运行全程可调用

静态成员优缺点

优点
  1. 替代全局变量,封装在类内,命名无冲突、安全性更高;
  2. 无需创建对象即可直接使用;
  3. 全局仅分配一份内存,节省空间。
缺点
  1. 生命周期跟随程序,对象销毁不会释放静态内存;
  2. 多线程并发修改时易产生数据竞争;
  3. 静态函数无法操作每个对象独有的普通属性。

易错点补充

构造函数内对静态变量执行赋值,不等于初始化,多次创建对象会反复覆盖初始值,标准初始化必须写在类外部。

复制代码
class Person {
public:
    static int m_A;
    Person() {
        m_A = 100; // 仅赋值,不是初始化
    }
};
// 标准初始化写法
int Person::m_A = 100;
相关推荐
Brown.alexis1 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝2 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
计算机魔术师3 小时前
新兴多智能体系统的模式与问题
前端
数智化管理手记3 小时前
海量数据如何沉淀有效数据资产?数据标准化治理方案如何搭建?
java·大数据·人工智能
隔窗听雨眠3 小时前
GBase 8s并发控制深度解析:封锁机制、隔离级别与死锁处理全攻略
服务器·数据库·oracle
cfm_29143 小时前
SpringAI + Ollama 本地大模型
java·开发语言·人工智能·语言模型
用户059540174463 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css
满栀5854 小时前
状态管理:Redux、Vuex、Pinia 核心区别
前端·javascript·typescript
鲸采云SRM采购管理系统4 小时前
采购管理系统哪家好?2026最新测评对比
java·服务器·数据库
一拳不是超人4 小时前
DeepSeek Harness 为什么敢说"一切皆插件"?拆透 Cordis 引擎的五大核心机制
前端·人工智能·agent