> 技术文档 > 《C++初阶之类和对象》【友元 + 内部类 + 匿名对象】

《C++初阶之类和对象》【友元 + 内部类 + 匿名对象】


【友元 + 内部类 + 匿名对象】目录

  • 前言:
  • ---------------友元---------------
    • 什么是友元?
    • 友元有哪三种形式?
    • 怎么使用友元函数
    • 怎么使用友元类?
    • 关于友元我们有哪些需要注意的事情?
  • ---------------内部类---------------
    • 什么是内部类?
    • 内部类使用需要注意什么?
    • 怎么使用内部类?
    • 一道练习题,快来试一试吧!
      • 题目介绍:
      • 方法一:static成员
      • 方法二:内部类
  • ---------------匿名对象---------------
    • 什么是匿名对象?
    • 匿名对象有哪些特点?
    • 匿名对象的使用场景有哪些?
    • 怎么使用匿名对象呢?

在这里插入图片描述

往期《C++初阶》回顾:

/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】

前言:

hi~ 各位代码巫师学徒们(◕‿◕✿), 今天我们要学习的是习得难度为 C++ 的:【友元🎫 + 内部类🪆 + 匿名对象👤】
准备好接受这些神奇的 C++ 魔法了吗?🔮 让我们开始今天的咒语学习吧!(≧∇≦)ノ 🧙‍♂️

---------------友元---------------

什么是友元?

友元(Friend):是一种允许 非成员函数其他类 访问某个类的私有(private)和保护(protected)成员的机制。

  • 通过友元,类可以有选择地向特定的外部实体开放其封装的细节,增强灵活性的同时保持封装性的控制。

友元有哪三种形式?

友元的三种的形式分别是:

  1. 友元函数
  2. 友元类
  3. 友元成员函数

友元函数(Friend Function):允许外部函数访问类的私有 / 保护成员。
语法:在类中使用 friend 关键字声明函数。
示例

class Rectangle {private: int width, height;public: friend int getArea(const Rectangle& rect); // 友元函数声明};int getArea(const Rectangle& rect) { return rect.width * rect.height; // 可访问私有成员}

友元类(Friend Class):允许整个类访问另一个类的私有 / 保护成员。
语法:在类中使用 friend class 声明友元类。
示例

class A {private: int data; friend class B; // 类B是类A的友元};class B {public: void accessA(A& a) { a.data = 42; // 类B可访问类A的私有成员 }};

友元成员函数(Friend Member Function):允许某个类的特定成员函数访问另一个类的私有 / 保护成员。
语法:在类中声明另一个类的特定成员函数为友元。
示例

class A; // 前向声明class B {public: void func(A& a); // 成员函数声明};class A {private: int data; friend void B::func(A& a); // 声明B的成员函数为友元};void B::func(A& a) { a.data = 42; // 可访问类A的私有成员}

怎么使用友元函数?

#includeusing namespace std;// 前置声明// 因为func函数中需要用到B类,而B类的定义在后面// 所以需要提前告诉编译器B是一个类class B;/*------------------------------类A的定义------------------------------*/class A{ /*----------------------友元函数声明----------------------*/ // 友元函数声明:func可以访问A的私有成员 friend void func(const A& aa, const B& bb); // 注意:虽然func函数参数中用到了B类,但这里只需要B的前置声明private: int _b1 = 3; int _b2 = 4; };/*------------------------------类B的定义------------------------------*/class B{ // 注意:func不是B的友元,所以不能访问B的私有成员 // 为了让func能访问_b1,我们需要将其设为publicpublic: int _b1 = 5; private: int _b2 = 6; };/*----------------------友元函数的定义----------------------*//** * @brief 友元函数定义 * @param aa A类对象的常量引用 * @param bb B类对象的常量引用 * * 此函数可以访问A的私有成员,但不能访问B的私有成员 */void func(const A& aa, const B& bb){ cout << aa._b1 << endl; // 可以访问A的私有成员_b1(输出3) cout << aa._b2 << endl; // 也可以访问_b2 cout << bb._b1 << endl; // 只能访问B的公有成员_b1(输出5) //cout << bb._b2 << endl; // 错误!不能访问B的私有成员_b2}int main(){ A aa; // 创建A类对象 B bb; // 创建B类对象 func(aa, bb); // 调用友元函数 return 0;}/* * 关键点总结: * 1. 友元函数声明: * - 在A类内部用friend声明func为友元函数 * - func可以访问A的所有成员(包括私有成员) * * 2. 前置声明: * - 因为func的参数中用到了B类,而B类的定义在后面 * - 所以需要在文件开头使用class B;进行前置声明 * * 3. 访问权限: * - func可以访问A的私有成员_b1和_b2 * - func不能访问B的私有成员(除非B也声明func为友元) * * 4. 注意事项: * - 友元函数不是类的成员函数,只是被特别授权访问私有成员 */

在这里插入图片描述

怎么使用友元类?

#includeusing namespace std;/*------------------------------类B的定义------------------------------*///该类将B类声明为友元,允许B访问其私有成员class A{ // 友元类声明:B是A的友元类 // 这意味着B的所有成员函数都可以访问A的私有成员 friend class B;private: int _a1 = 1; int _a2 = 2; };/*------------------------------类B的定义------------------------------*///A的友元类,可以访问A的私有成员class B{public: /*----------------------友元函数的定义----------------------*/ /** * @brief 访问A的私有成员_a1和B的私有成员_b1 * @param aa A类对象的常量引用 */ void func1(const A& aa) { cout << aa._a1 << endl; cout << _b1 << endl; } /*----------------------友元函数的定义----------------------*/ /** * @brief 访问A的私有成员_a2和B的私有成员_b2 * @param aa A类对象的常量引用 */ void func2(const A& aa) { cout << aa._a2 << endl; cout << _b2 << endl; }private: int _b1 = 3; int _b2 = 4; };int main(){ A aa; // 创建A类对象 B bb; // 创建B类对象 // 调用B的成员函数,演示友元访问 bb.func1(aa); // 输出:1 和 3 bb.func2(aa); // 输出:2 和 4 return 0;}/* * 关键点总结: * 1. 友元类声明: * - 在A类内部使用 friend class B; 声明 * - 这使得B类的所有成员函数都可以访问A的私有成员 * * 2. 访问权限: * - B类可以访问A的私有成员_a1和_a2 * - 但A类不能访问B的私有成员(友元关系是单向的) */

在这里插入图片描述

关于友元我们有哪些需要注意的事情?

1. 访问权限相关

  • 突破封装:友元函数或友元类能访问对应类的privateprotected成员,打破了类的常规封装机制,让特定外部代码可操作类的内部数据,增强了灵活性,但也一定程度破坏了封装性,使用时需谨慎。
  • 访问权限不受限:在类中声明友元时,不管放在publicprivate还是protected部分,效果都一样,都能获得相应的访问权限。

2. 关系特性

  • 单向性:友元关系是单向的。
    • 若 A 类是 B 类的友元,不意味着 B 类是 A 类的友元,B 类仍不能随意访问 A 类的私有和保护成员。
    • 例如: A 类可访问 B 类私有成员,但 B 类对 A 类无此权限。
  • 非传递性:即便 A 类是 B 类的友元,B 类是 C 类的友元,A 类也不能自动成为 C 类的友元,不能访问 C 类的私有和保护成员。
  • 非继承性:友元关系不会被派生类继承。
    • 基类的友元不能访问派生类的私有和保护成员,派生类也不会自动成为基类友元类的友元。

3. 声明与定义相关

  • 声明的灵活性:友元声明可以在类定义的任意位置出现,位置不影响其访问权限的赋予。
  • 定义位置要求
    • 友元函数的定义可以在类内(成为内联函数),友元函数的定义也可在类外。
    • 友元类的定义则需在合适位置正常定义。
    • 但不管在哪定义,都要先在对应的类中进行友元声明。

4. 其他特性

  • 影响编译顺序:由于友元涉及不同类或函数间的相互访问,可能会影响编译顺序和依赖关系。
    • 例如: 先声明友元类,再定义友元类,需注意声明和定义的先后逻辑,否则可能出现编译错误。
  • 多类友元:友元函数可以是多个类的友元函数。
    • 这在某些场景下提供了便利,能方便地在不同类间共享特定操作逻辑。
    • 但同时,友元会增加类与类之间的耦合度,过度使用会严重破坏封装性,导致代码维护难度增大,所以友元不宜多用。

---------------内部类---------------

什么是内部类?

内部类(Nested Class):是定义在另一个类内部的类。

  • 内部类是一个独立的类,与定义在全局作用域相比,内部类仅受外部类的类域限制以及访问限定符的约束因此:外部类所定义的对象并不包含内部类的对象。

  • 内部类主要用于封装与外部类密切相关的辅助功能或数据结构,增强代码的模块化和可读性。

内部类的基本使用:

class Outer {public: class Inner //内部类定义 { public: void show() { cout << \"Inner class\\n\"; } };};int main() { Outer::Inner innerObj; //使用作用域解析符创建对象 innerObj.show(); //输出: Inner class return 0;}

内部类使用需要注意什么?

作用域与可见性:

  • 嵌套作用域:内部类定义在外部类内部,其作用域被限定在外部类中。在外部使用时,需通过外部类作用域来访问。
  • 名称隐藏:内部类名可与外部作用域中的其他名称相同,因为其作用域局限于外部类内,不会产生命名冲突 。

访问权限:

  • 内部类对外部类的访问

    • 内部类能直接访问外部类的 公有/私有静态成员
    • 但对于外部类的 非静态成员,需借助 外部类对象的引用或指针才能访问
    /*--------------------外部类--------------------*/class Outer {private: static int staticData; // 静态成员 int nonStaticData; // 非静态成员public: /*--------------------内部类--------------------*/ class Inner { public: void func(Outer& outer) { staticData = 10;  // 合法:访问外部类静态成员 outer.nonStaticData = 20; // 需通过对象访问非静态成员 } };};
  • 外部类对内部类的访问:外部类没有访问内部类 私有成员的特权,需通过内部类对象来访问其公有成员

    class Outer {public: void accessInner() { Inner inner; inner.innerFunc(); // 需通过对象访问内部类的公有成员 }};
  • 访问限定符影响:内部类的访问权限由外部类的访问限定符(publicprivateprotected )决定。

    • public内部类可在外部被直接访问。
    • private内部类只能被外部类的成员访问。
    • protected内部类可被外部类及其派生类访问 。
    class Outer {private: class PrivateInner { /* ... */ }; // 私有内部类public: class PublicInner { /* ... */ }; // 公有内部类}; // 合法:可访问公有内部类Outer::PublicInner publicObj; // 错误:无法访问私有内部类// Outer::PrivateInner privateObj;

独立性:

  • 对象独立性:内部类对象可独立于外部类对象存在,有自己独立的生命周期,二者的生命周期互不影响 。
  • 继承与成员关系:内部类并不自动继承外部类的成员,也不与外部类有隐含的继承关系 。

与外部类的关系:

  • 无默认友元关系:内部类和外部类之间不存在默认的友元关系,若要让一方访问另一方的私有成员,需显式声明友元 。
  • 依赖关系:内部类的定义依赖于外部类,但外部类并不依赖内部类,即便没有实例化内部类,外部类也可正常使用 。

怎么使用内部类?

#includeusing namespace std;/*----------------------外部类----------------------*///外部类,包含一个静态成员和一个内部类Bclass A{private: static int _k; // 静态私有成员变量,属于类A int _h = 1; // 普通私有成员变量,每个A对象独有一份public: /*----------------------内部类----------------------*/ class B { public: /*----------------------foo函数的定义----------------------*/ void foo(const A& a) { cout << _k << endl; // 直接访问A的静态私有成员(合法) cout << a._h << endl; // 通过对象访问A的非静态私有成员(合法) } int _b1; // 内部类B的公有成员变量 };};int A::_k = 1; //静态成员变量必须在类外初始化int main(){ /*----------------------测试1:查看A类的大小----------------------*/ // 静态成员_k不占用类对象的内存空间 // 只有普通成员_h占用空间(int类型通常4字节) cout << sizeof(A) << endl; // 输出:4(在32位系统中) /*----------------------测试2:创建内部类B的对象----------------------*/ //1.使用作用域解析运算符创建内部类对象 A::B bb; /*----------------------测试3:使用内部类访问外部类私有成员----------------------*/ //1.直接创建的外部类的对象 A aa; //2.使用内部类的对象调用内部类的成员函数 bb.foo(aa); // 输出: // 1(静态成员_k的值) // 1(aa对象的_h的值) return 0;}/* * 关键点总结: * * 1. 访问权限: * - B可以直接访问A的静态成员_k * - B需要通过A的对象访问普通成员_h */

在这里插入图片描述

一道练习题,快来试一试吧!

开始挑战:JZ64 求1+2+3+…+n

题目介绍:

在这里插入图片描述

方法一:static成员

/*----------------------------定义 Sum 类----------------------------*/class Sum //辅助计算累加和{public: /*-----------------------成员函数-----------------------*/ //1.定义构造函数 ---> 每次创建 Sum 对象时,执行累加逻辑 Sum() { //1.将静态成员变量 _i 的值累加到静态成员变量 _ret 中 _ret += _i; //2.静态成员变量 _i 自增,为下一次累加做准备 ++_i; } //2.定义静态成员函数 ---> 用于获取最终的累加结果 static int GetRet() { return _ret; }private: /*-----------------------成员变量-----------------------*/ //1.记录当前要累加的数值 static int _i; //2.记录累加的结果 static int _ret;};//1.类外初始化 Sum 类的静态成员变量 _iint Sum::_i = 1;//2.类外初始化 Sum 类的静态成员变量 _retint Sum::_ret = 0;/*----------------------------定义 Solution 类----------------------------*/class Solution //用于提供计算 1 到 n 累加和的功能{public: /*-----------------------成员函数-----------------------*/ int Sum_Solution(int n) //接收参数 n,计算 1 + 2 +... + n 的结果 { //1.定义一个变长数组 Sum arr[n]; /* 注意事项: * 1.创建一个包含 n 个 Sum 对象的数组,创建过程中会调用 Sum 的构造函数 n 次 * 2.每次构造函数调用会完成 _ret 的累加和 _i 的自增* */ //2.通过 Sum 类的静态成员函数 GetRet 获取累加结果并返回 return Sum::GetRet(); }};

方法二:内部类

/*----------------------------外部类----------------------------*/class Solution //解决方案类{public:/*----------------------------成员变量----------------------------*///1.记录当前累加的数值static int _i;//2.存储累加的总和static int _ret;/*----------------------------成员变量----------------------------*/int Sum_Solution(int n) //原理:通过创建n个Sum对象,触发n次构造函数执行累加逻辑{//1. 创建长度为n的Sum对象数组// 注意:每次创建Sum对象时,其构造函数会自动执行累加操作Sum arr[n];//2. 返回累加结果(此时_ret已完成1+2+...+n的计算)return _ret;}private:/*----------------------------内部类----------------------------*/class Sum //辅助类 ---> 利用构造函数执行累加逻辑{public://1.实现:“默认构造函数”Sum(){_ret += _i;_i++;}/* 注意事项:每次创建Sum对象时* 1.将当前_i的值累加到_ret* 2._i自增1*/};};//1.类外初始化 Sum 类的静态成员变量 _iint Solution::_i = 1;//2.类外初始化 Sum 类的静态成员变量 _retint Solution::_ret = 0;

---------------匿名对象---------------

什么是匿名对象?

匿名对象:是指在创建时未显式声明名称的对象。

  • 通常在表达式结束时立即销毁。

  • 它是 C++ 中一种高效的临时值表示方式,常用于函数传参、返回值等场景。

匿名对象有哪些特点?

匿名对象的特点:

  • 无标识符:在定义时没有名字,通过在类名后加一对空括号来实例化。
    • 例如ClassName(),可以是自定义类类型,也可以是内置类型(有了模板后,内置类型也可创建匿名对象 )
  • 生命周期:通常是临时的,在创建它们的表达式结束后很快就会被销毁 。
    • 例如:在函数返回对象时产生的临时匿名对象,当完成返回操作后就会被销毁,但如果用常量引用来引用匿名对象,其生命周期会延长至引用作用域结束 。
  • 右值特性:属于右值(无法取地址)
class Data {public: Data(int x) { cout << \"构造:\" << x << endl; } ~Data() { cout << \"析构\" << endl; }};int main() { Data(10); // 创建匿名对象,本行结束立即析构 cout << \"------\" << endl; return 0;}

匿名对象的使用场景有哪些?

匿名对象常见的使用场景:

1. 作为函数参数传递

当函数接受对象作为参数时,可直接传递匿名对象,尤其适用于有默认参数的函数。

#include using namespace std;class MyClass{public: MyClass(int num) : data(num) {} void printData() const { cout << \"Data: \" << data << endl; }private: int data;};void func(MyClass obj = MyClass(0)){ obj.printData();}int main(){ /*------------------------作为函数参数传递------------------------*/ func(MyClass(5)); // 传递匿名对象给函数func return 0;}

2. 临时调用成员函数

仅需临时使用对象并调用其成员函数,后续不再使用该对象时,可创建匿名对象。

#include using namespace std;class Printer{public: void printMessage(const string& msg) { cout << msg << endl; }};int main(){ /*------------------------临时调用成员函数------------------------*/ Printer().printMessage(\"Hello, World!\"); // 创建匿名Printer对象并调用成员函数 return 0;}

3. 避免不必要的拷贝构造(优化性能)

在函数返回对象时,返回匿名对象可让编译器进行返回值优化(RVO),减少对象拷贝。

#include using namespace std;class MyData{public: MyData(int num) : data(num) { cout << \"构造函数调用\" << endl; } MyData(const MyData& other) : data(other.data) { cout << \"拷贝构造函数调用\" << endl; } ~MyData() { cout << \"析构函数调用\" << endl; } int getData() const { return data; }private: int data;};MyData getMyData(){ /*-----------------------避免不必要的拷贝构造-----------------------*/ return MyData(10); // 返回匿名对象,利于编译器进行返回值优化}int main(){ MyData obj = getMyData(); cout << \"Data: \" << obj.getData() << endl; return 0;}

4. 类型转换
在涉及类型转换时,可利用匿名对象来满足类型要求。

#include using namespace std;class A{public: A(int num) : value(num) {} int getValue() const { return value; }private: int value;};void processInt(int num){ cout << \"processInt: \" << num << endl;}int main(){ A a(5); /*-----------------------类型转换-----------------------*/ processInt(A(5).getValue()); // 创建匿名A对象进行类型转换后传递给函数 return 0;}

在这里插入图片描述

怎么使用匿名对象呢?

#include using namespace std;/*----------------------A类定义----------------------*/class A{public: /*----------------------构造函数(带默认参数)----------------------*/ A(int a = 0) :_a(a) { cout << \"A(int a)\" << endl; } /*----------------------析构函数----------------------*/ ~A() { cout << \"~A()\" << endl; }private: int _a; };/*----------------------Solution类定义----------------------*/ class Solution {public: /*----------------------默认构造函数----------------------*/ Solution() { cout << \"Solution()\" << endl; } /*----------------------析构函数----------------------*/ ~Solution() { cout << \"~Solution()\" << endl; } /*----------------------计算求和的函数----------------------*/ int Sum_Solution(int n) { //... 实际实现逻辑省略 return n; }};int main(){ /*----------------------------普通对象定义(调用默认构造函数)----------------------------*/ A aa1; // 输出:A(int a) // 注意:aa1对象会在main函数结束时析构 //注意:以下写法会被编译器解析为函数声明,不是对象定义 // A aa1(); // 这声明了一个返回A类型的函数aa1,而不是创建对象 /*----------------------------匿名对象的使用(生命周期仅限当前行)----------------------------*/ A(); // 输出:A(int a) 和 ~A() (构造后立即析构) A(1); // 输出:A(int a) 和 ~A() (构造后立即析构) /*----------------------------显式调用带参构造函数----------------------------*/ A aa2(2); // 输出:A(int a) // aa2对象会在main函数结束时析构 /*----------------------------匿名对象的实际应用场景----------------------------*/ Solution().Sum_Solution(10); // 创建一个匿名Solution对象,调用其方法后立即销毁 // 适用于只需要临时使用一次对象的情况 return 0; // 输出(按顺序): // ~A() - aa2析构 // ~A() - aa1析构}/* * 关键点总结: * 1. 匿名对象特性: * - 语法:直接使用类名加构造函数参数(如:A() 或 A(1)) * - 生命周期:仅存在于定义它的那一行语句 * - 用途:临时使用对象,避免命名和长期保存 * * 2. 对象定义注意事项: * - A aa1; // 正确:调用默认构造函数 * - A aa1(); // 错误:被解析为函数声明 * - A aa1(1); // 正确:调用带参构造函数 * * 3. 生命周期说明: * - 普通对象:作用域结束时析构 * - 匿名对象:当前语句执行完毕后立即析构 */

在这里插入图片描述
在这里插入图片描述