:类和对象(下)——类和对象·完结篇:友元、内部类,再次理解封装与面向对象)
0.1概要序論这里是白杨本篇是类和对象的完结篇——从命名空间一路走到这里C前期最大的BOSS终于要被我们彻底打完了本文会讲清楚友元友元函数、友元类与内部类概念、特性然后一起再次理解封装、再次理解面向对象。内容干货满满。让我们现在开始吧一友元1.0为什么需要友元在正式讲友元之前我们先解决一个历史遗留问题——流插入和流提取的重载。之前讲运算符重载的时候我们留了个伏笔和如果重载成成员函数会出大问题class Date { public: ostream operator(ostream out) // 成员函数版本 { out _year; return out; } private: int _year; }; int main() { Date d; d cout; // 调用形式变成了这样谁看谁迷糊 return 0; }因为成员函数的第一个参数永远是隐藏的this指针d会被强行塞到左操作数的位置cout只能去当右操作数——调用形式变成了d cout完全反人类。所以和必须重载成全局函数。但是……全局函数访问不了类的私有成员啊这就是友元登场的时刻。1.1友元函数1.1.1友元函数的定义友元函数是在类中用friend关键字声明的函数它不是类的成员函数但可以访问类的私有成员和保护成员。class Date { friend ostream operator(ostream out, const Date d); public: Date(int year 2026, int month 9, int day 22) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; ostream operator(ostream out, const Date d) { out d._year 年 d._month 月 d._day 日; return out; }这里稍微解释一下返回类型为什么是ostream是为了支持链式调用cout d1 d2 endl;每次返回的还是out本身可以继续往下插入。如果返回void就只能输出一次了。friend声明写在类的哪个位置任意位置都行不受public/private限制——因为它本来就不是成员访问限定符管不着它。小贴士:少数情况如图我们有一个函数同时是A类和B类的友元函数但是基于编译器的向上查找原则编译器运行到A时在查找不到B类型就会报错所以什么必须在整个程序的开头添加一个B的类型声明class B1.1.2友元函数的特点友元函数可以访问类的私有和保护成员但不是类的成员函数友元函数不能用const修饰它不是成员函数没有this指针友元函数可以在类中任意位置声明不受访问限定符限制一个函数可以是多个类的友元比如operator可以同时声明为两个类的友元同时访问两边的私有成员友元函数的调用和普通函数完全一样1.2友元类1.2.1友元类的定义如果一个类想访问另一个类的私有成员也可以把整个类声明为友元class Time { friend class Date; // 声明Date是Time的友元类 public: Time(int hour 0, int minute 0) : _hour(hour) , _minute(minute) {} private: int _hour; int _minute; }; class Date { public: void SetTimeOfDay(int hour, int minute) { _t._hour hour; // Date可以直接访问Time的私有成员 _t._minute minute; } private: Time _t; };1.2.2友元类的三大铁律先说结论友元关系是单向的、不能传递的、不能继承的。单向声明B是A的友元B可以访问A的私有成员但A不能访问B的私有成员。我把你当朋友你未必把我当朋友不能传递A是B的友元B是C的友元但A不是C的友元。不能继承友元关系不会被派生类继承继承是什么先别急下一篇就是它。1.2.3友元的利弊友元提供了一种便利但破坏了封装增加了类之间的耦合。可以这么理解封装是类给自己家上了锁友元就是类亲手配出去的一把钥匙——钥匙当然可以配但配得越多家里越不安全。原则能用公有接口解决就不要用友元。友元应当是最后的便利手段而不是日常操作。二内部类2.1概念2.1.1内部类的定义如果一个类定义在另一个类的内部这个类就叫内部类也叫嵌套类class A { public: class B // B就是内部类 { public: void Print() { cout 我是内部类 endl; } private: int _b; }; private: int _a; };2.1.2内部类与外部类的关系先说结论内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员。两个要点内部类可以访问外部类的私有成员从访问权限上说内部类相当于外部类的天然友元其中访问static成员不需要外部类的对象/类名访问非静态成员需要借助外部类的对象/指针外部类不能访问内部类的私有成员“我可以进你家你进不了我家”2.2特性内部类可以定义在外部类的public、protected、private任意区域。定义在private/protected时外部类之外无法使用A::B——这正是把实现细节藏起来的手段。内部类是独立的类不占外部类对象的空间sizeof(外部类)和内部类没有任何关系。内部类可以直接访问外部类的static成员不需要外部类的对象/类名。外部类不能访问内部类的私有成员。内部类可以像普通类一样有自己的成员、构造函数甚至可以再嵌套一个类。2.2.1sizeof验证class A { public: class B { int _b[100]; // 内部类B有400字节 }; private: int _a; }; int main() { cout sizeof(A) endl; // 4内部类B完全不影响外部类A的大小 return 0; }再次验证内部类是独立的类不占外部类对象的空间sizeof(外部类)和内部类没有任何关系。2.2.2内部类的一个实用场景内部类最常见的用途是把只有外部类才用得上的辅助类藏进private里class Stack { private: class Node // 只有Stack自己用的节点类外界根本看不见 { public: Node(int val) : _val(val) , _next(nullptr) {} private: int _val; Node* _next; }; Node* _top nullptr; size_t _size 0; };外界连Stack::Node这个名字都访问不到实现细节被彻底封死——这不就是封装思想的完美体现吗三再次理解封装友元和内部类都讲完了现在我们回头再看封装这两个字感受应该完全不一样了。3.1封装是什么封装 把数据 操作数据的方法打包在一起再用访问限定符决定哪些能见人、哪些不能。3.2封装到底封住了什么以栈为例class Stack { public: void Push(int x); void Pop(); int Top(); private: int* _a; size_t _top; size_t _capacity; };使用者只能通过Push/Pop/Top这些接口操作栈,_top、_capacity这些内部状态被藏了起来。好处是什么使用者不用关心内部是怎么实现的数组链表只关心接口内部实现随便改只要接口不变使用者的代码就不用动内部状态不会被误操作使用者没法直接把_top改成-13.3友元与封装的关系友元是封装的例外但这个例外的钥匙捏在类自己手里——只有类自己声明谁是友元谁才能进来。所以友元并没有打破封装由类自己掌控这个根本原则它更像是类在权衡便利性之后主动开的一扇小门。四再次理解面向对象4.1从面向过程到面向对象我们用洗衣服来对比两种思想面向过程拿盆 → 放水 → 放衣服 → 倒洗衣粉 → 搓 → 拧干 → 晾。关注的是步骤函数面向对象人、衣服、洗衣机、洗衣粉。关注的是对象以及对象之间的交互人把衣服放进洗衣机洗衣机负责洗C语言是典型的面向过程数据结构体和操作数据的函数是分离的。C是面向对象数据 操作数据的方法打包成一个类——这就是我们这整个专栏前半部分一直在干的事。4.2类与对象的关系类一种自定义类型是图纸、“模板”描述了对象长什么样、能干什么对象类的实例化是按图纸造出来的实物4.3面向对象的三大特性封装把数据和方法打包控制访问权限这一路我们讲完了继承让代码可以复用、可以在原有基础上扩展未来会讲多态同一接口不同表现同样未来会讲总结类和对象到这里就正式结束了。从命名空间一路走到这里你已经跨过了C前期最陡的一段坡。接下来我们就要进入继承与多态的世界了——新的篇章敬请期待好了本期内容就到这里如果对你有帮助不要忘记点赞三联一波哦我是白杨我们下期再见