《C++初阶之类和对象》【友元 + 内部类 + 匿名对象】
【友元 + 内部类 + 匿名对象】目录
- 前言:
- ---------------友元---------------
- 什么是友元?
- 友元有哪三种形式?
- 怎么使用友元函数?
- 怎么使用友元类?
- 关于友元我们有哪些需要注意的事情?
- ---------------内部类---------------
- 什么是内部类?
- 内部类使用需要注意什么?
- 怎么使用内部类?
- 一道练习题,快来试一试吧!
- 题目介绍:
- 方法一:static成员
- 方法二:内部类
- ---------------匿名对象---------------
- 什么是匿名对象?
- 匿名对象有哪些特点?
- 匿名对象的使用场景有哪些?
- 怎么使用匿名对象呢?
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
【初始化列表 + 自定义类型转换 + static成员】
前言:
hi~ 各位代码巫师学徒们(◕‿◕✿), 今天我们要学习的是习得难度为 C++ 的:【友元🎫 + 内部类🪆 + 匿名对象👤】 ✨
准备好接受这些神奇的 C++ 魔法了吗?🔮 让我们开始今天的咒语学习吧!(≧∇≦)ノ 🧙♂️
---------------友元---------------
什么是友元?
友元(Friend)
:是一种允许非成员函数
或其他类
访问某个类的私有(private
)和保护(protected
)成员的机制。
- 通过友元,类可以有选择地向特定的外部实体开放其封装的细节,增强灵活性的同时保持封装性的控制。
友元有哪三种形式?
友元的三种的形式分别是:
- 友元函数
- 友元类
- 友元成员函数
友元函数(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的私有成员 }
怎么使用友元函数?
#include<iostream>
using 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,我们需要将其设为public
public: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; // 也可以访问_b2cout << 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. 注意事项:* - 友元函数不是类的成员函数,只是被特别授权访问私有成员*/
怎么使用友元类?
#include<iostream>
using 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 和 3bb.func2(aa); // 输出:2 和 4return 0;
}/** 关键点总结:* 1. 友元类声明:* - 在A类内部使用 friend class B; 声明* - 这使得B类的所有成员函数都可以访问A的私有成员** 2. 访问权限:* - B类可以访问A的私有成员_a1和_a2* - 但A类不能访问B的私有成员(友元关系是单向的)*/
关于友元我们有哪些需要注意的事情?
1. 访问权限相关
突破封装
:友元函数或友元类能访问对应类的private
和protected
成员,打破了类的常规封装机制,让特定外部代码可操作类的内部数据,增强了灵活性,但也一定程度破坏了封装性,使用时需谨慎。访问权限不受限
:在类中声明友元时,不管放在public
、private
还是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 classreturn 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(); // 需通过对象访问内部类的公有成员} };
访问限定符影响
:内部类的访问权限由外部类的访问限定符(public
、private
、protected
)决定。
public
内部类可在外部被直接访问。private
内部类只能被外部类的成员访问。protected
内部类可被外部类及其派生类访问 。class Outer { private:class PrivateInner { /* ... */ }; // 私有内部类 public:class PublicInner { /* ... */ }; // 公有内部类 };// 合法:可访问公有内部类 Outer::PublicInner publicObj;// 错误:无法访问私有内部类 // Outer::PrivateInner privateObj;
独立性:
对象独立性
:内部类对象可独立于外部类对象存在,有自己独立的生命周期,二者的生命周期互不影响 。继承与成员关系
:内部类并不自动继承外部类的成员,也不与外部类有隐含的继承关系 。
与外部类的关系:
无默认友元关系
:内部类和外部类之间不存在默认的友元关系,若要让一方访问另一方的私有成员,需显式声明友元 。依赖关系
:内部类的定义依赖于外部类,但外部类并不依赖内部类,即便没有实例化内部类,外部类也可正常使用 。
怎么使用内部类?
#include<iostream>
using namespace std;/*----------------------外部类----------------------*/
//外部类,包含一个静态成员和一个内部类B
class A
{
private:static int _k; // 静态私有成员变量,属于类Aint _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 类的静态成员变量 _i
int Sum::_i = 1;
//2.类外初始化 Sum 类的静态成员变量 _ret
int 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 类的静态成员变量 _i
int Solution::_i = 1;
//2.类外初始化 Sum 类的静态成员变量 _ret
int 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 <iostream>
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)); // 传递匿名对象给函数funcreturn 0;
}
2.
临时调用成员函数
仅需临时使用对象并调用其成员函数,后续不再使用该对象时,可创建匿名对象。
#include <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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. 生命周期说明:* - 普通对象:作用域结束时析构* - 匿名对象:当前语句执行完毕后立即析构*/