【友元 + 内部类 + 匿名对象】目录
往期《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; // 也可以访问_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. 注意事项:
* - 友元函数不是类的成员函数,只是被特别授权访问私有成员
*/
怎么使用友元类?
#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 和 3
bb.func2(aa); // 输出:2 和 4
return 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 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(); // 需通过对象访问内部类的公有成员 } };
访问限定符影响
:内部类的访问权限由外部类的访问限定符(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; // 静态私有成员变量,属于类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 类的静态成员变量 _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)); // 传递匿名对象给函数func
return 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. 生命周期说明:
* - 普通对象:作用域结束时析构
* - 匿名对象:当前语句执行完毕后立即析构
*/