好的,我来为你总结“类和对象(中)”关于取地址运算符重载,以及“类和对象(下)”的重点内容。我会尽量用通俗易懂的方式讲解,帮你打好基础。
第一部分:类和对象(中)— 取地址运算符重载
这个部分其实非常简单,但在理解它之前,我们需要先搞懂一个更基础的概念: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&
取地址运算符重载就是重载&这个符号,它分为两种:
- 普通取地址重载:用于普通对象。
- 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) {}为什么必须用初始化列表?
有三种特殊的成员变量必须在初始化列表中初始化:
- 引用成员变量(
int& _ref):引用必须在创建时绑定到一个实体。 - const成员变量(
const int _n):常量必须在创建时赋予初始值,之后不能修改。 - 没有默认构造函数的类类型成员:如果一个类成员(比如
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):打破封装的黑客
友元提供了突破private和protected访问权限的方法。
- 友元函数:一个外部的全局函数,被声明为类的友元后,就可以直接访问这个类的私有和保护成员。
- 友元类:类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++特性(如模板、智能指针等)的重要基础。如果还有不清楚的地方,可以随时再问。