C++学习笔记——this关键字、对象生命周期(栈作用域)、智能指针、复制与拷贝构造函数

目录

[1. this关键字](#1. this关键字)

[1.1 this 的本质](#1.1 this 的本质)

[1.2 const 成员函数中的 this](#1.2 const 成员函数中的 this)

[2. 对象生命周期(栈作用域)](#2. 对象生命周期(栈作用域))

[2.1 基本概念](#2.1 基本概念)

[2.2 作用域指针](#2.2 作用域指针)

[3. 智能指针](#3. 智能指针)

[3.1 为什么需要智能指针?](#3.1 为什么需要智能指针?)

[3.2 std::unique_ptr(独占所有权)](#3.2 std::unique_ptr(独占所有权))

[3.3 std::shared_ptr(共享所有权)](#3.3 std::shared_ptr(共享所有权))

[3.4 std::weak_ptr(弱引用)](#3.4 std::weak_ptr(弱引用))

[3.5 注意事项](#3.5 注意事项)

[3.6 使用总结](#3.6 使用总结)

[4. 复制与拷贝构造函数](#4. 复制与拷贝构造函数)

[4.1 基本概念](#4.1 基本概念)

[4.2 浅拷贝 vs 深拷贝](#4.2 浅拷贝 vs 深拷贝)

[4.3 避免隐式拷贝](#4.3 避免隐式拷贝)

何时需要按值传递


1. this关键字

1.1 this 的本质

  • this 是一个指向当前对象 的指针(ClassName* const this),在非静态成员函数内部可用。

  • 它由编译器隐式传递给成员函数(作为第一个隐藏参数),指向调用该成员函数的对象实例。

    class Entity {
    private:
    int m_X;
    public:
    void SetX(int x) {
    // 编译器自动将 this 传入,this 指向调用该函数的对象
    this->m_X = x; // 等价于 m_X = x;
    }
    };

1.2 const 成员函数中的 this

  • const 成员函数 中,this 的类型是 const ClassName* const(指向常量的常量指针)。

  • 这意味着不能通过 this 修改任何非 mutable 成员变量。

cpp 复制代码
class Entity {
    int m_X;
public:
    int GetX() const {
        // this 的类型是 const Entity*
        // this->m_X = 10;  错误:不能修改
        return this->m_X;    // 允许读取
    }
};

2. 对象生命周期(栈作用域)

2.1 基本概念

  • 对象生命周期:从对象创建(构造函数执行)到销毁(析构函数执行)的时间段。

  • 栈作用域 :对象在进入作用域时创建,离开作用域时自动销毁(自动存储期)。这是 C++ 最安全、最高效的内存管理方式。

cpp 复制代码
void function() {
    Entity e;          // 进入作用域,构造
    // ... 使用 e
}                      // 离开作用域,自动析构

2.2 作用域指针

cpp 复制代码
class ScopedPtr {
private:
    Object* m_Ptr;
public:
    ScopedPtr(Object* ptr) : m_Ptr(ptr) {}
    ~ScopedPtr() { delete m_Ptr; }
};

int main() {
    {
        ScopedPtr e = new Object(5);   // 隐式转换:new Object(5) 传给构造函数
    }                                   // 离开作用域,~ScopedPtr 自动 delete m_Ptr
    std::cin.get();
}
  • 利用栈对象的自动析构,确保资源(内存、文件句柄、锁等)不会泄漏。

3. 智能指针

3.1 为什么需要智能指针?

  • 原始指针(Object*)需要手动 delete,容易忘记导致内存泄漏,或过早 delete 导致悬空指针。

  • 智能指针利用栈对象的析构自动释放堆内存,遵循 RAII 原则。

  • C++11 标准库提供了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

3.2 std::unique_ptr(独占所有权)

  • 独占 :同一时刻只能有一个 unique_ptr 指向一块内存。

  • 不可拷贝,只能移动 :拷贝构造和拷贝赋值运算符重载被删除,但支持移动语义(std::move)。

  • 开销小 :与原始指针大小相同,析构时自动 delete 内部指针。

cpp 复制代码
#include <memory>

// 推荐:使用 std::make_unique (C++14)
auto ptr = std::make_unique<Object>(5);

// C++11 需使用原始 new
std::unique_ptr<Object> ptr(new Object(5));

std::unique_ptr<Object> a = std::make_unique<Object>();
// std::unique_ptr<Object> b = a;   // 错误!不能拷贝
std::unique_ptr<Object> b = std::move(a); // 正确,a 转移所有权给 b,a 变为空

3.3 std::shared_ptr(共享所有权)

  • 多个指针共享同一对象 :内部使用引用计数 (reference count)记录有多少个 shared_ptr 指向同一块内存。

  • 当最后一个 shared_ptr 被销毁时(引用计数降为 0),自动释放对象内存。

  • 引用计数存储在控制块(control block)中,与对象内存分离。

cpp 复制代码
// 推荐:std::make_shared(一次分配,同时创建对象和控制块,效率更高)
auto ptr = std::make_shared<Object>(5);

// 也可以使用原始 new
std::shared_ptr<Object> ptr(new Object(5));

std::shared_ptr<Object> sp1 = std::make_shared<Object>(); // 引用计数 = 1
{
    std::shared_ptr<Object> sp2 = sp1;                     // 引用计数 = 2
    // sp2 离开作用域,引用计数变为 1
}
// 此时引用计数 = 1,对象仍未释放
sp1.reset();                                               // 引用计数 = 0,对象释放

3.4 std::weak_ptr(弱引用)

  • 不增加引用计数 :弱引用指向 shared_ptr 管理的对象,但不拥有所有权。

  • 可能悬空 :对象可能已被其他 shared_ptr 释放。

  • 必须转换为 shared_ptr 才能访问对象 (通过 lock() 方法)。

cpp 复制代码
std::shared_ptr<Object> sp = std::make_shared<Object>();
std::weak_ptr<Object> wp = sp;     // wp 观察 sp,引用计数不变

if (auto locked = wp.lock()) {     // 尝试提升为 shared_ptr
    locked->DoSomething();         // 安全访问
} else {
    // 对象已被释放
}

3.5 注意事项

  • 循环引用 :两个 shared_ptr 互相引用对方(例如双向链表),导致引用计数永远不为 0,内存泄漏。解决办法是使用 weak_ptr 打破循环。
cpp 复制代码
#include <iostream>
#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;   // 指向下一个节点
    std::shared_ptr<Node> prev;   // 指向前一个节点
    int value;

    Node(int val) : value(val) {
        std::cout << "Node(" << value << ") constructed\n";
    }

    ~Node() {
        std::cout << "Node(" << value << ") destructed\n";
    }
};

int main() {
    {
        auto node1 = std::make_shared<Node>(1);
        auto node2 = std::make_shared<Node>(2);

        node1->next = node2;   // node1 的 next 指向 node2
        node2->prev = node1;   // node2 的 prev 指向 node1

        // 此时引用计数:
        // node1: 自身 (1) + node2->prev (1) = 2
        // node2: 自身 (1) + node1->next (1) = 2
    } // 离开作用域,node1 和 node2 的 shared_ptr 局部变量销毁
      // 但 node1 和 node2 的引用计数各减 1 后变为 1(因为互相仍有引用)
      // 内存永远不会释放,析构函数不会调用

    std::cout << "End of main (memory leaked)\n";
    return 0;
}

解决方案:使用 weak_ptr 打破循环

cpp 复制代码
#include <iostream>
#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;   // 下一个节点:强引用
    std::weak_ptr<Node> prev;     // 前一个节点:弱引用(不增加引用计数)
    int value;

    Node(int val) : value(val) {
        std::cout << "Node(" << value << ") constructed\n";
    }

    ~Node() {
        std::cout << "Node(" << value << ") destructed\n";
    }
};

int main() {
    {
        auto node1 = std::make_shared<Node>(1);
        auto node2 = std::make_shared<Node>(2);

        node1->next = node2;                // node2 引用计数 +1 → 2
        node2->prev = node1;                // 弱引用,node1 引用计数不变(仍为 1)

        // 此时引用计数:
        // node1: 自身 (1)   (因为 prev 是 weak_ptr,不增加)
        // node2: 自身 (1) + node1->next (1) = 2
    } // 离开作用域:
      // node1 销毁 → 引用计数 1→0,释放 node1(调用析构)
      // node2 销毁 → 引用计数 2→1(因为 node1->next 已随 node1 销毁而销毁),
      // 然后 node2 引用计数变为 0,释放 node2

    std::cout << "End of main (no leak)\n";
    return 0;
}
  • 性能开销 :比 unique_ptr 多一份控制块内存和原子操作(线程安全的引用计数),适合真正需要共享所有权的场景。

3.6 使用总结

cpp 复制代码
{
    std::shared_ptr<Object> obj;
    {
        std::shared_ptr<Object> sharedObj = std::make_shared<Object>();
        // make_shared 一次分配:对象内存 + 控制块,引用计数初始为 1
        
        obj = sharedObj;            // 拷贝赋值,引用计数变为 2
        
        std::weak_ptr<Object> weakObj = obj;   // 弱引用,不增加计数
        
        std::unique_ptr<Object> object = std::make_unique<Object>();
        // std::unique_ptr<Object> obj = object; // 错误!unique_ptr 不可拷贝
        
        object->Print();
    }
    // 离开内层作用域:
    // - sharedObj 销毁,引用计数从 2 减为 1(因为 obj 仍持有)
    // - object(unique_ptr)销毁,自动 delete 其管理的 Object
    // - weakObj 销毁,不影响引用计数
}
// 离开外层作用域:
// - obj 销毁,引用计数从 1 减为 0,delete 管理的内存
  • make_shared 优于直接 new:一次分配(对象+控制块),异常安全,效率高。

  • shared_ptr 支持拷贝,引用计数随之增减。

  • weak_ptr 不增加计数,用于观察和打破循环。

  • unique_ptr 不可拷贝,只能移动。

4. 复制与拷贝构造函数

4.1 基本概念

  • 复制:用一个已存在的对象创建另一个新对象,新对象与原对象内容相同(但通常应是独立的内存副本)。

  • 拷贝构造函数 :一种特殊的构造函数,参数是当前类的 const 引用,用于定义"如何从另一个对象构造当前对象"。

  • 默认拷贝行为 :编译器会生成一个默认的拷贝构造函数,执行浅拷贝(逐成员复制,对于指针只复制地址,不复制指向的数据)。

4.2 浅拷贝 vs 深拷贝

浅拷贝(默认拷贝构造器)

cpp 复制代码
String(const String& other) : m_Buffer(other.m_Buffer), m_Size(other.m_Size) {}

两个对象的 m_Buffer 指向同一块堆内存 。当一个对象析构 delete[] 后,另一个对象的指针变成悬空指针;当第二个对象析构时,对同一块内存再次 delete 导致双重释放,程序崩溃。

深拷贝(可自定义拷贝构造)

cpp 复制代码
String(const String& other) : m_Size(other.m_Size) {
    m_Buffer = new char[m_Size + 1];
    memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}

为新对象分配独立的内存,完整复制数据内容。两个对象的指针指向不同内存,完全独立,互不影响,析构时各自释放自己的内存。

4.3 函数传递参数的发式

值传递: 形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递: 也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
引用传递: 实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯。

4.4 避免隐式拷贝

在函数传参时,按值传递会调用拷贝构造函数,对于大型对象或管理资源的类,这种隐式拷贝开销很大,且可能引入不必要的深拷贝。使用引用可以避免拷贝,提升性能(尤其对于大对象)const修饰引用 保证函数不会修改原对象,语义清晰。

cpp 复制代码
void PrintString(String s) {          // 按值传递,拷贝构造
    std::cout << s << std::endl;
}

String name = "Cherno";
PrintString(name);                    // 触发 String 的深拷贝,即使只读


void PrintString(const String& s) {   // const 引用,不拷贝
    std::cout << s << std::endl;
}

String name = "Cherno";
PrintString(name);                    // 无拷贝,直接使用原对象

何时需要按值传递

  • 需要在函数内修改副本且不影响原对象(例如对参数进行排序、变换)。

  • 对于小且简单 的类型(如 intdouble、指针),按值传递通常比引用更快(引用也有开销)。

相关推荐
lucky九年3 小时前
GO语言模拟C++封装,继承,多态
开发语言·c++·golang
温天仁3 小时前
西门子PLC编程实践教程:工控工程案例学习
开发语言·学习·自动化·php
漫随流水3 小时前
c++编程:D进制的A+B(1022-PAT乙级)
数据结构·c++·算法
tankeven4 小时前
HJ159 没挡住洪水
c++·算法
charlie1145141914 小时前
嵌入式C++教程实战之Linux下的单片机编程:从零搭建 STM32 开发工具链(5):调试进阶篇 —— 从 printf 到完整 GDB 调试环境
linux·c++·单片机·学习·嵌入式·c
Moqiqiuzi4 小时前
ET8.1-ECS组件式编程
笔记·学习
paeamecium4 小时前
【PAT】 - Course List for Student (25)
数据结构·c++·算法·pat考试
小黄人软件4 小时前
MFC为什么不报空指针异常 2宏定义不改源码 用替换 用Log函数替换printf等 #define printf Log 优化版底层类Log显示
c++·mfc
VelinX4 小时前
【个人学习||spring】spring ai
人工智能·学习·spring