第一部分:类和对象(中)— 取地址运算符重载

好的,我来为你总结"类和对象(中)"关于取地址运算符重载,以及"类和对象(下)"的重点内容。我会尽量用通俗易懂的方式讲解,帮你打好基础。

第一部分:类和对象(中)--- 取地址运算符重载

这个部分其实非常简单,但在理解它之前,我们需要先搞懂一个更基础的概念:const成员函数

1. const成员函数:让对象"只读"

想象一下,你有一个Date日期对象,它被声明为const Date d1(2024,7,5);(常量对象)。这意味着你不希望d1的日期被任何函数意外修改。

  • 如何实现? 在成员函数的参数列表后面加上const

  • 作用 :这个const实际上修饰的是隐藏的this指针。它把this指针从Date* const this(一个不能改变指向的指针)变成了const Date* const this(一个不能改变指向,并且不能通过它修改对象内容的指针)。

    class Date {
    public:
    // 这个Print函数被const修饰,意味着它不能修改调用它的对象的成员变量
    void Print() const { // const修饰的是隐藏的this指针
    cout << _year << "-" << _month << "-" << _day << endl;
    }
    // 非const函数,可以修改成员变量
    void SetDay(int day) {
    _day = day; // 这是允许的
    }
    private:
    int _year, _month, _day;
    };

    int main() {
    const Date d1(2024, 7, 5);
    d1.Print(); // 正确:Print是const成员函数,可以被const对象调用
    // d1.SetDay(10); // 错误!SetDay不是const成员函数,不能保证不修改d1
    }

简单说 :如果一个对象是常量(const),那么它只能调用那些被声明为const的成员函数,因为这些函数承诺不会修改对象。

2. 取地址运算符重载:operator&

取地址运算符重载就是重载&这个符号,它分为两种:

  1. 普通取地址重载:用于普通对象。

  2. const取地址重载:用于const对象。

    class A {
    public:
    // 1. 普通对象的取地址重载
    A* operator&() {
    cout << "A* operator&()" << endl;
    return this; // 正常情况下就是返回对象自己的地址(this)
    }
    // 2. const对象的取地址重载
    const A* operator&() const {
    cout << "const A* operator&() const" << endl;
    return this;
    }
    };

核心要点

  • 99%的情况你不需要自己写 :编译器会自动生成这两个函数,而且生成的就完全够用了,直接返回对象地址(this)。

特殊用途 :只有在非常特殊的场景下你才需要自己实现。比如,你不想让别人获取到这个对象的真实地址 (出于安全或设计目的),你就可以胡乱返回一个地址,或者返回nullptr

复制代码
class Secret {
public:
    // 重载&,不让别人知道我的真实地址
    Secret* operator&() {
        return nullptr; // 或者 return (Secret*)0x12345678;
    }
};

取地址重载小结 :它很简单,了解编译器会自动生成即可。记住const成员函数的概念,这个更重要。


第二部分:类和对象(下)--- 核心知识点精讲

这部分内容是关于类的更深层次的特性和技巧。

1. 再探构造函数:初始化列表

之前我们初始化成员变量是在构造函数体内赋值,像这样:

复制代码
Date(int year, int month, int day) {
    _year = year;   // 这是赋值,不是初始化!
    _month = month;
    _day = day;
}

更专业、更正确的方式是使用初始化列表

复制代码
Date(int year, int month, int day)
    : _year(year)   // 这才是真正的初始化
    , _month(month)
    , _day(day)
{}

为什么必须用初始化列表?

有三种特殊的成员变量必须在初始化列表中初始化:

  1. 引用成员变量int& _ref):引用必须在创建时绑定到一个实体。

  2. const成员变量const int _n):常量必须在创建时赋予初始值,之后不能修改。

  3. 没有默认构造函数的类类型成员 :如果一个类成员(比如Time _t)它的类没有提供无参或全缺省的构造函数,你就必须通过初始化列表告诉编译器如何构造它。

    class Time {
    public:
    Time(int hour) { ... } // 只有带参构造,没有默认构造函数
    };
    class Date {
    private:
    int _year;
    const int _n; // const成员
    int& _ref; // 引用成员
    Time _t; // 无默认构造的类成员
    public:
    // 错误写法:这些成员无法在函数体内"初始化"
    // Date(int year, int n, int& ref) { ... }

    复制代码
     // 正确写法:使用初始化列表
     Date(int year, int n, int& ref, int hour)
         : _year(year)
         , _n(n)         // 初始化const成员
         , _ref(ref)     // 初始化引用成员,绑定到外部变量
         , _t(hour)      // 调用Time的带参构造函数
     {}

    };

重要规则 :成员变量的初始化顺序只取决于它们在类中的声明顺序,与在初始化列表中的书写顺序无关。建议保持一致以避免混淆。

2. static成员:属于类,不属于某个对象
  • 静态成员变量 :用static修饰。它不属于任何一个对象,而是被所有同类对象共享。它存放在静态区。 必须在类外进行初始化 (在全局作用域),语法是类型 类名::变量名 = 值;

  • 静态成员函数 :用static修饰。它没有隐藏的this指针,因此不能访问普通的成员变量(因为不知道访问哪个对象的),只能访问静态成员变量。 调用方式:可以通过对象调用(obj.func()),更推荐通过类名调用(ClassName::Func()),这直接表明了它是类的函数。

    class A {
    private:
    static int _scount; // 声明:用来统计创建了多少个A对象
    public:
    A() { ++_scount; } // 构造时计数+1
    A(const A& a) { ++_scount; } // 拷贝构造时计数+1
    ~A() { --_scount; } // 析构时计数-1

    复制代码
      // 静态成员函数,没有this指针,用来获取计数
      static int GetACount() {
          return _scount;
      }

    };
    // 定义并初始化静态成员变量(必须在类外)
    int A::_scount = 0;

    int main() {
    A a1, a2;
    A a3 = a1;
    cout << A::GetACount() << endl; // 输出:3
    {
    A a4;
    cout << A::GetACount() << endl; // 输出:4
    } // a4析构
    cout << A::GetACount() << endl; // 输出:3
    }

3. 友元(friend):打破封装的黑客

友元提供了突破privateprotected访问权限的方法。

  • 友元函数:一个外部的全局函数,被声明为类的友元后,就可以直接访问这个类的私有和保护成员。

  • 友元类 :类B被声明为类A的友元后,类B的所有成员函数都可以直接访问类A的私有和保护成员。

    class A {
    private:
    int _secret = 10;
    // 声明友元函数
    friend void HackA(const A& a);
    // 声明友元类
    friend class B;
    };
    // 友元函数的实现
    void HackA(const A& a) {
    cout << "I know A's secret: " << a._secret << endl; // 直接访问私有成员,合法!
    }
    class B {
    public:
    void PeekA(const A& a) {
    cout << "B knows A's secret: " << a._secret << endl; // 合法!
    }
    };

注意

  • 友元关系是单向的(A把B当朋友,B不一定要把A当朋友)。
  • 友元关系不能传递(A是B的朋友,B是C的朋友,但A不是C的朋友)。
  • 慎用友元 :它破坏了封装性,增加了耦合度。但在重载<<(输出)和>>(输入)运算符时非常有用。
4. 内部类:类中类

一个类可以定义在另一个类的内部,它就是内部类。它像一个被封装在外部类里面的独立类。

  • 特性 : 内部类天生就是外部类的友元 ,可以访问外部类的私有静态成员、以及通过对象访问私有成员。 内部类不占用外部类对象的大小 ,它是独立的。 受外部类的类域和访问限定符(public/private/protected)限制。

    class Outer {
    private:
    static int _static_val;
    int _private_val;
    public:
    class Inner { // Inner是Outer的友元
    public:
    void AccessOuter(const Outer& o) {
    cout << _static_val << endl; // 可以直接访问外部类的静态私有成员
    cout << o._private_val << endl; // 可以通过外部类对象访问其私有成员
    }
    };
    };
    int Outer::_static_val = 100;

5. 匿名对象:用完即焚

匿名对象的生命周期只有它所在的那一行,声明方式为类名(参数)

复制代码
class Solution {
public:
    int Sum_Solution(int n) {
        return n;
    }
};
int main() {
    // 有名对象
    Solution s;
    s.Sum_Solution(10);

    // 匿名对象:不需要名字,直接使用,这一行结束就销毁
    Solution().Sum_Solution(10);
}

用途:当你只需要临时用一个对象来调用某个方法,之后不再需要它时,使用匿名对象非常方便。

6. 编译器优化:聪明的编译器

现代C++编译器会在不改变程序逻辑的前提下,尽可能地减少对象拷贝,提升效率。

常见优化场景:

  • 连续构造+拷贝构造 -> 优化为直接构造

  • 传值返回时,局部对象拷贝到临时对象,再拷贝到接收对象 -> 优化为直接构造接收对象

    A GetA() {
    return A(10); // 理论上:构造A(10) -> 拷贝给临时对象 -> 拷贝给main中的aa
    } // 实际上(优化后):直接在GetA函数里为main中的aa分配空间并构造
    int main() {
    A aa = GetA(); // 优化后,可能只有一次构造操作
    }

注意:优化因编译器而异,但理解这些优化能帮助你写出更高效的代码。

总结与联系

特性 核心思想 为什么重要?
取地址重载/const成员函数 控制对象地址的获取和对象的"只读"性。 完善对对象行为的控制,是理解C++封装和常量正确性的基础。
初始化列表 真正初始化成员的地方,尤其是特殊成员。 正确的初始化是避免未定义行为的关键,是编写稳健类的基石。
static成员 属于类本身的变量和函数,被所有对象共享。 实现对象间通信、管理类级别资源(如计数器)的必备工具。
友元 授予特定函数或类访问私有成员的"特权"。 在需要突破封装时(如输入输出流重载)提供灵活性。
内部类 将紧密关联的类封装在一起,增强代码内聚性。 用于设计更清晰、更模块化的代码结构。
匿名对象 临时使用的对象,生命周期极短。 简化代码,避免创建不必要的命名对象。
编译器优化 编译器自动减少不必要的对象拷贝。 理解编译器行为,有助于写出性能更高的C++代码。

内部类 | 将紧密关联的类封装在一起,增强代码内聚性。 | 用于设计更清晰、更模块化的代码结构。 |

| 匿名对象 | 临时使用的对象,生命周期极短。 | 简化代码,避免创建不必要的命名对象。 |

| 编译器优化 | 编译器自动减少不必要的对象拷贝。 | 理解编译器行为,有助于写出性能更高的C++代码。 |

希望这份总结能帮助你清晰地理解这些概念!这些知识是学习后面更复杂的C++特性(如模板、智能指针等)的重要基础。如果还有不清楚的地方,可以随时再问。

相关推荐
Selegant2 小时前
告别传统部署:用 GraalVM Native Image 构建秒级启动的 Java 微服务
java·开发语言·微服务·云原生·架构
__万波__2 小时前
二十三种设计模式(十三)--模板方法模式
java·设计模式·模板方法模式
动亦定2 小时前
微服务中如何保证数据一致性?
java·数据库·微服务·架构
王桑.2 小时前
Spring中IoC的底层原理
java·后端·spring
Liii4032 小时前
Java集合详细讲解
java·开发语言
落羽的落羽2 小时前
【C++】哈希扩展——位图和布隆过滤器的介绍与实现
linux·服务器·开发语言·c++·人工智能·算法·机器学习
fish_xk2 小时前
类和对象(二)
开发语言·c++·算法
lly2024062 小时前
Python 列表(List)详解
开发语言
Han.miracle2 小时前
Spring Boot 项目从入门到排障:核心结构、依赖管理与启动全解析
java·jar