【C++】C++类与对象4: C++友元、内部类、匿名对象与编译优化要点

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • 一、友元函数
    • [1.1 为什么需要友元?](#1.1 为什么需要友元?)
    • [1.2 前置声明的作用](#1.2 前置声明的作用)
    • [1.3 友元的特点](#1.3 友元的特点)
  • 二、友元类
  • 三、内部类
    • [3.1 什么是内部类](#3.1 什么是内部类)
    • [3.2 内部类的特性](#3.2 内部类的特性)
    • [3.3 外部类的大小不包含内部类](#3.3 外部类的大小不包含内部类)
  • 四、匿名对象
    • [4.1 什么是有名对象 vs 匿名对象](#4.1 什么是有名对象 vs 匿名对象)
    • [4.2 匿名对象的特点](#4.2 匿名对象的特点)
    • [4.3 匿名对象的典型用法](#4.3 匿名对象的典型用法)
  • 五、编译器优化(拷贝优化)
    • [5.1 传值传参时的拷贝](#5.1 传值传参时的拷贝)
    • [5.2 传值返回时的优化(NRVO)](#5.2 传值返回时的优化(NRVO))
    • [5.3 现代编译器的优化结果](#5.3 现代编译器的优化结果)
  • 六、知识点汇总
  • 七、常见面试题
    • [🐾 </h3>Q1:友元函数可以写在类的private区域吗?](#🐾 Q1:友元函数可以写在类的private区域吗?)
    • [🐾 </h3>Q2:内部类可以访问外部类的私有成员吗?](#🐾 Q2:内部类可以访问外部类的私有成员吗?)
    • [🐾 </h3>Q3:匿名对象和有名对象的区别?](#🐾 Q3:匿名对象和有名对象的区别?)
    • [🐾 </h3>Q4:编译器一定会做NRVO优化吗?](#🐾 Q4:编译器一定会做NRVO优化吗?)
  • 八、总结
  • 九、本文所有代码
    • [🐾 </h3>Test.cpp](#🐾 Test.cpp)

前言

💡 在前几篇文章中,我们学习了类的基础和const成员函数。这一篇我们来聊几个更"进阶"的特性:

  • 友元:让外部函数或其他类访问你的私有成员
  • 内部类:把一个类定义在另一个类内部
  • 匿名对象:不取名字的对象,用完就销毁
  • 编译器优化:编译器悄悄帮你减少拷贝,提高效率

这些特性在实际开发中经常用到,理解它们能让我们的代码更灵活、更高效。

🐶 🐾 ✨ 🐾 🐶


一、友元函数

1.1 为什么需要友元?

类的私有成员默认只能被类内部访问。但有时候,我们想让某个外部函数也能访问私有成员, 比如 重载<<>>时。

  • 解决办法:友元函数------在类内部用friend关键字声明一个外部函数。
cpp 复制代码
class B;  // 前置声明,因为func的参数中用到了B

class A
{
    friend void func(const A& aa, const B& bb);  // 友元声明
private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
    friend void func(const A& aa, const B& bb);  // 友元声明
private:
    int _b1 = 3;
    int _b2 = 4;
};

// 友元函数实现:可以访问A和B的私有成员
void func(const A& aa, const B& bb)
{
    cout << aa._a1 << endl;  // ✅ 可以访问A的私有成员
    cout << bb._b1 << endl;  // ✅ 可以访问B的私有成员
}

int main()
{
    A aa;
    B bb;
    func(aa, bb);  // 输出:1  3
    return 0;
}

1.2 前置声明的作用

编译器是向上查找的。当A的友元声明中用到B类型时,必须在前面先声明class B;,否则编译器不认识B。

cpp 复制代码
class B;  // 前置声明:告诉编译器B是一个类,后面会定义

class A {
    friend void func(const B& bb);  // 现在编译器知道B是一个类型
};

1.3 友元的特点

特点 说明
单向性 A是B的友元,不代表B是A的友元
不可传递 友元关系不能继承
不受访问限定符影响 声明放在public/private都可以
突破封装 谨慎使用,破坏了封装性

🐶 🐾 ✨ 🐾 🐶


二、友元类

一个类可以声明为另一个类的友元,此时友元类的所有成员函数都可以访问另一个类的私有成员。

cpp 复制代码
class A
{
    friend class B;  // B是A的友元类,B的所有成员函数都可以访问A的私有成员
private:
    int _a1 = 1;
    int _a2 = 2;
};

class C
{
    friend class B;  // B也可以是C的友元类(一个类可以是多个类的友元)
private:
    int _c1 = 0;
    int _c2 = 0;
};

class B
{
public:
    void func1(const A& aa, const C& cc)
    {
        cout << aa._a1 << endl;  // ✅ 访问A的私有成员
        cout << cc._c1 << endl;  // ✅ 访问C的私有成员
        cout << _b1 << endl;     // ✅ 访问自己的私有成员
    }

private:
    int _b1 = 3;
    int _b2 = 4;
};

int main()
{
    A aa;
    B bb;
    C cc;
    bb.func1(aa, cc);  // 输出:1 0 3
    return 0;
}

注意: 友元关系是单向的。B是A的友元,但A不是B的友元,A不能访问B的私有成员。

🐶 🐾 ✨ 🐾 🐶


三、内部类

3.1 什么是内部类

一个类定义在另一个类的内部,叫做内部类。

cpp 复制代码
class A
{
public:
    class B   // 内部类
    {
    public:
        void foo(const A& a)
        {
            cout << _k << endl;    // ✅ 可以访问A的静态成员
            cout << a._h << endl;  // ✅ 可以访问A的私有成员(B是A的友元)
        }
    private:
        int _b1;
    };

private:
    static int _k;
    int _h = 1;
};

int A::_k = 1;  // 静态成员类外初始化

int main()
{
    cout << sizeof(A) << endl;  // 4(只包含_h,不包含内部类B)

    A aa;
    A::B b;      // 通过域作用符访问内部类
    b.foo(aa);   // 输出:1 1
    return 0;
}

3.2 内部类的特性

特性 说明
独立类 内部类是一个独立的类,外部类对象不包含内部类
受外部类域限制 访问内部类需要外部类::内部类
自动友元 内部类自动成为外部类的友元(C++特性)
反向不行 外部类不是内部类的友元
可访问静态成员 内部类可以直接访问外部类的静态成员

3.3 外部类的大小不包含内部类

cpp 复制代码
class A
{
    class B {
        int _b1;
        int _b2;  // 8字节
    };
    int _h;       // 4字节
};

sizeof(A);  // 4,不是12

内部类只是在编译器的类型命名空间里多了一个类型,不会成为外部类的成员变量。

🐶 🐾 ✨ 🐾 🐶


四、匿名对象

4.1 什么是有名对象 vs 匿名对象

cpp 复制代码
class A
{
public:
    A(int a = 0) : _a(a) { cout << "A(int a)" << endl; }
    ~A() { cout << "~A()" << endl; }
private:
    int _a;
};

int main()
{
    A aa1;    // 有名对象:生命周期贯穿整个main函数
    A();      // 匿名对象:生命周期只有这一行
    A(1);     // 匿名对象:带参数的匿名对象

    // A aa2();  // ❌ 错误!被编译器当成函数声明
    return 0;
}

// 输出:
// A(int a)    <-  构造aa1
// A(int a)    <-  构造匿名对象A()
// ~A()        <-  销毁匿名对象A()
// A(int a)    <-  构造匿名对象A(1)
// ~A()        <-  销毁匿名对象A(1)
// ~A()        <-  销毁aa1

4.2 匿名对象的特点

特点 说明
不需要取名字 类型(参数) 直接定义
生命周期极短 只在当前语句有效,立即构造立即析构
典型使用场景 作为函数实参,用完即毁
注意歧义 A aa2(); 会被编译器当成函数声明

4.3 匿名对象的典型用法

cpp 复制代码
// 方案类调用单次函数
int result = Solution().Sum_Solution(10);  // 匿名对象,用一次就销毁

// 传参时直接用匿名对象
void func(A aa);
func(A(10));  // 直接传匿名对象,不需要先定义有名对象

🐶 🐾 ✨ 🐾 🐶


五、编译器优化(拷贝优化)

5.1 传值传参时的拷贝

cpp 复制代码
class A
{
public:
    A(int a = 0) : _a1(a) { cout << "A(int a)" << endl; }
    A(const A& aa) : _a1(aa._a1) { cout << "A(const A& aa)" << endl; }
    ~A() { cout << "~A()" << endl; }
    A& operator=(const A& aa) { cout << "A& operator=" << endl; return *this; }
private:
    int _a1 = 1;
};

void f1(A aa)  // 传值传参
{
    // aa是形参,通过拷贝构造得到
}

int main()
{
    A aa1;      // 构造
    f1(aa1);    // 传值传参 → 拷贝构造
    return 0;
}

// 输出:
// A(int a)            <-  构造aa1
// A(const A& aa)      <-  拷贝构造形参
// ~A()                <-  销毁形参
// ~A()                <-  销毁aa1

优化建议: 用引用传参可以避免拷贝。

cpp 复制代码
void f1(const A& aa)  // const引用,不拷贝

5.2 传值返回时的优化(NRVO)

cpp 复制代码
// 未优化时的理论流程:
// 构造局部aa → 拷贝构造临时对象 → 销毁aa → 返回临时对象

A f2()
{
    A aa;      // 构造局部对象
    return aa; // 传值返回
}

int main()
{
    f2();                // 场景1:不接收返回值
    A aa2 = f2();        // 场景2:用返回值构造新对象
    aa1 = f2();          // 场景3:赋值给已有对象
    return 0;
}

5.3 现代编译器的优化结果

在很多编译器(如VS、GCC开启优化)下,输出结果是:

场景 构造次数 拷贝构造次数 赋值次数
f2() 1 0 0
A aa2 = f2() 1 0 0
aa1 = f2() 1 0 1(赋值)

这就是NRVO(Named Return Value Optimization,具名返回值优化):编译器直接在接收返回值的内存上构造aa,省略了中间的拷贝构造。

🐶 🐾 ✨ 🐾 🐶


六、知识点汇总

知识点 核心要点
友元函数 friend声明,可访问私有成员,需前置声明
友元类 友元类的所有成员函数都是友元
友元特点 单向、不可传递、破坏封装
内部类 定义在类内部,自动成为外部类友元
内部类独立性 不占外部类空间,访问需外部类::内部类
匿名对象 类型(参数),生命周期仅一行
匿名对象用途 函数实参、单次调用的临时对象
传值传参优化 用const &避免拷贝
传值返回优化 NRVO:编译器省略中间拷贝

🐶 🐾 ✨ 🐾 🐶


七、常见面试题

🐾 Q1:友元函数可以写在类的private区域吗?

  • 可以。friend声明不受访问限定符影响,放在public/private/protected都可以。

🐾 Q2:内部类可以访问外部类的私有成员吗?

  • 可以。内部类自动成为外部类的友元。

🐾 Q3:匿名对象和有名对象的区别?

  • 匿名对象没有名字,生命周期只有当前语句;有名对象有名字,生命周期直到作用域结束。

🐾 Q4:编译器一定会做NRVO优化吗?

  • 不一定。NRVO是编译器优化,不同编译器、不同优化级别行为不同。依赖优化结果写代码是不安全的。

🐶 🐾 ✨ 🐾 🐶


八、总结

这一篇我们学习了C++的五个进阶特性:

  • 友元:突破封装,但需谨慎使用
  • 内部类:组织代码的一种方式,自动友元
  • 匿名对象:临时使用的对象,用完即毁
  • 编译器优化:减少拷贝,提高效率

🐶 🐾 ✨ 🐾 🐶


九、本文所有代码

🐾 Test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//////////////////////////////////////////////////////////////////////////////
// //友元函数
// // 友元提供了一种突破类访问限定符封装的方式,友元分为友元函数和友元类
// //在函数声明前面或者类声明前面加friend,并且把友元声明放到一个类的里面
//
//class B;
//前置声明,因为当一个函数是多个类的友元时,
// 必须要用前置声明将后面的类先进行声明,否则A的友元函数声明编译器不认识B,

//编译器原则是当要用到的任何类型和变量都是向上查找

//class A
//{
//	//友元函数声明,或者友元类声明:friend class B;
//	friend void func(const A& aa, const B& bb);
//
//private:
//	int _a1 = 1;
//	int _a2 = 2;
//
//};
//
//class B
//{
//	//友元声明
//	friend void func(const A& aa, const B& bb);
//private:
//	int _b1 = 3;
//	int _b2 = 4;
//};
//
//void func(const A& aa, const B& bb)
//{
//	cout << aa._a1 << endl;
//	cout << bb._b1 << endl;
//}
//
//int main()
//{
//	A aa;
//	B bb;
//	func(aa, bb);
//	
//	return 0;
//}
///////////////////////////////////////////////////////////////////////////////////
////友元类
////友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一类中的私有和保护成员
//class A
//{
//	//友元类声明
//	friend class B;
//	//此时B类中的所有成员函数都是A类的友元函数
//	//即B中所有的成员函数都能使用A中的所有东西
//
//private:
//	int _a1 = 1;
//	int _a2 = 2;
//};
//
//class C
//{
//	//友元类的声明
//	friend class B;		//B类的成员也可以供C用
//private:
//	int _c1 = 0;
//	int _c2 = 0;
//};
//
//class B
//{
//public:
//	/*void func1(const A& aa)
//	{
//		cout << aa._a1 << endl;
//		cout << _b1 << endl;
//	}
//	void func2(const A& aa)
//	{
//		cout << aa._a2 << endl;
//		cout << _b2 << endl;
//	}*/
//
//	void func1(const A& aa, const C& cc)
//	{
//		cout << aa._a1 << endl;
//		cout << cc._c1 << endl;
//		cout << _b1<< endl;
//	}
//	void func2(const A& aa, const C& cc)
//	{
//		cout << aa._a2 << endl;
//		cout << cc._c2 << endl;
//		cout << _b2 << endl;
//	}//函数一样,一个类也可以是多个类的友元类
//
//private:
//	int _b1 = 3;
//	int _b2 = 4;
//};
//int main()
//{
//	A aa;
//	B bb;
//	C cc;
//	bb.func1(aa, cc);
//	bb.func2(aa, cc);
//	return 0;
//}

/////////////////////////////////////////////////////////////////////////////
//内部类
//一个类定义在另一个类的内部叫做内部类,但他也是一个独立的类,
//只受外部类类域限制和访问限定符限制,所以外部类定义的对象不包含内部类

//class A
//{
//private :
//	static int _k;
//	int _h = 1;
//
//public:
//	class B
//	{
//	public:
//		//foo是一个函数名,通常用作示例/测试的占位符名称,类似"test、func"
//		void foo(const A& a)		//foo函数:接收A的常量引用
//		{
//			cout << _k << endl;		//输出为1:B默认是A的友元,能使用A的所有成员
//
//			cout << a._h << endl;		//1
//		}
//	private:
//		int _b1;
//	};
//
//	//A(int aa = 0)
//	//{
//	//	B::_b1++;//err:对非静态成会"A::B_b1"的非法引用
//	//}
//	//B默认是A的友元,能使用A的所有成员;但A不是B的友元,无法访问B的私有成员
//};
//
////类外成员初始化
//int A::_k = 1;
//
//int main()
//{
//	//输出A类的大小:仅包含非静态成员_h(int 占4字节)
//	cout <<sizeof(A)<<endl;	//B类中也有类型int的成员变量,但A类的大小不为8而为4,
//							//说明A类并不包含b类这个内部类
//							//只是B类受到了A类类域的限制,仍然是一个独立的类
//
//	A aa;				//定义A的变量
//	A::B b;				//B作为内部类受到A类类域的限制和访问限定符限制
//						//要用域作用限定符::才能访问B类
//	b.foo(aa);			//调用foo函数,传入aa对象
//	return 0;
//}


/////////////////////////////////////////////////////////////////////////////


//匿名对象
// 用类型(形参)定义出来的对象,叫匿名对象
// 类型对象名(实参)定义出来的叫有名对象
// 
//class A
//{
//public:
//	//带默认参数的构造函数
//	A(int a = 0)
//		:_a(a)		//使用初始化列表:_a(a),直接初始化成员变量_a,构造时输出"A(int a)"
//					//等价于A(int a=0)
//					//		{
//					//			_a=a;		//这是赋值,不是初始化
//					//		 }
//					//初始化列表:对象创建时,直接给成员变量赋初值
//					//函数体内赋值:对象现被默认值初始化,再被覆盖
//	{				
//		cout << "A(int a)" << endl;
//	}
//	~A()			//析构函数,对象生命周期结束时自动调用,析构时输出"~A()"
//	{
//		cout << "~A()" << endl;
//	}
//private:
//	int _a;
//
//};
//
////class Solution
////{
////public:
////	int Sum_Solution(int n)
////	{
////		return 0;
////	}
////};
//int main()
//{
//	A aa1;	//有名对象,生命周期贯穿整个main函数,直到main结束时才会析构
//
//				//A aa1();		//err:"aa1":重定义;以前的定义是:"数据变量"
//				//编译器无法识别是否为一个函数声明,还是对象定义
//				//可以定义匿名对象---不用取名字
//
//	A();	//匿名对象
//	A(1);
//			//匿名对象的生命周期只有这一行,后边自动调用析构函数
//	return 0;
//
//
//	//运行结果
//	//A(int a)		//构造aa1
//	
//	//A(int a)		//构造匿名对象A()
//	//	~A()		//销毁匿名对象A()
//	
//	//A(int a)		//构造匿名对象A(1)
//	//	~A()		//销毁匿名对象A(1)
//	
//	//	~A()		//销毁aa1
//}

/////////////////////////////////////////////////////////////////////////////////////////////
//
////对象拷贝时的编译器优化
////传值传参

class A
{
public:
	//带默认参数的构造函数
	A(int a = 0)
		:_a1(a)		//初始化列表
	{
		cout << "A(int a)" << endl;
	}

	//拷贝构造函数
	A(const A& aa)		//拷贝构造:_a1=aa._a1
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	~ A()				//析构函数
	{
		cout << "~A()" << endl;
	}

	//赋值运算符重载 operator =
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)		//防止自赋值
		{
			_a1 = aa._a1;		//成员变量赋值,将aa的_a1赋值给当前对象的_a1
		}
		return *this;			//返回自身引用,支持链式赋值
	}

private:
	int _a1=1;
};

void f1(A aa)
{

}

//传值返回函数
//理论构造流程:构造aa->拷贝构造临时对象->销毁aa

//- 编译器优化(NRVO:具名返回值优化):
//很多编译器会直接在接收返回值的内存上构造  aa ,省略中间的拷贝构造
A f2()
{
	A aa;		//构造局部对象 aa
	return aa;//传值返回
	
 }

int main()
{
	{
		//传值传参
		//构造+拷贝构造

		A aa1;		//调用构造初始化对象aa1
		f1(aa1);	//传值传参会调用拷贝构造,进行引用传参就可以减少拷贝
	
		cout << endl;

		cout << "***************************************" << endl;

		//传值返回
		f2();
		cout << endl;

		A aa2 = f2();
		cout << endl;

		aa1 = f2();
		cout << endl; 
		
		return 0;
	}

}

🐶 🐾 ✨ 🐾 🐶


🐾 下一篇我们继续学习:

  • 初始化列表
  • 类型转换
  • static成员

谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论