【C++】类和对象(六) -- 友元、内部类、匿名对象、对象拷贝时的编译器优化

🫧个人主页:小年糕是糕手

💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》

🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!



目录

一、友元

1.1、初识友元

1.2、前置声明

1.3、友元类

1.4、识别友元

1.5、总结

二、内部类

2.1、初识内部类

2.2、内部类与外部类的联系

2.3、总结

三、匿名对象

1.1、初识匿名对象

1.2、匿名对象的应用

1.3、总结

四、对象拷贝时的编译器优化

1.1、合并优化

1.2、省略部分拷贝

1.3、总结


一、友元

1.1、初识友元

我们之前就简单了解过友元,大致讲的就是我现在有一个A类和B类,我想在C类中访问A类和B类,我就需要在A类和B类中声明一下:C是友元,通俗来说就是A和B都将C当成了朋友,将家里的钥匙给了C,C想要来玩的时候可以随时来玩(即C可以访问A和B中的私有变量),但这不代表B和A是认识的(相关的)

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
	friend class C;
public:
	//...
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	friend class C;
public:
	//...
private:
	int _b1 = 1;
	int _b2 = 2;
};

class C
{
public:
	void func(const A& aa, const B& bb)
	{
		cout << aa._a1 << endl;
		cout << bb._b1 << endl;
	}
private:
	int _c1 = 1;
	int _c2 = 1;
};

int main()
{
	A aa;
	B bb;
	C cc;
	cc.func(aa, bb);
	return 0;
}

我们运行代码:

1.2、前置声明

下面我们来考虑一下如果有一个函数想要同时用A类和B类中的变量,那A类和B类中肯定要定义这个函数的友元:

这时候我们会发现事情好像没有我们想的那们简单,代码出现了一点小问题,这时候就要来说我们这个标题了:前置声明!(我们要使用任何的函数、变量...编译器都要寻找他的出处,编译器一般是去向上寻找的,这样也是为了节省编译时间,如果不前置声明我们会发现A类中的func找不到B类)修改后的代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

//前置声明,A的友元函数声明编译器不认识B
//我们要使用任何的函数、变量...编译器都要寻找他的出处
//都是向上去找的,如果不前置声明一下就找不到
class 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;
};

//一个函数要是想访问一个类的私有成员需要类内部有这个函数的友元声明
//通俗来说就是这个函数是这个类的朋友,这个类为这个函数开放了权限
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;
}
1.3、友元类

根据上述的内容演变过来,我们现在有一个类C和D,我们D这个类中有函数需要大量访问C中的私有变量,我们就需要在C中定义D是他的友元类:

cpp 复制代码
//友元类
#include<iostream>
using namespace std;

//C中有个友元声明,说明C将D看作了朋友
//也就是说在D类中可以访问C类中的私有
class C
{
	// 友元声明 
	friend class D;

private:
	int _a1 = 1;
	int _a2 = 2;
};

//D这个类要大量访问C这个类的私有
class D
{
public:
	void func1(const C& aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}

	void func2(const C& aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}

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

int main()
{
	C aa;
	D bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}
1.4、识别友元

我们要知道友元是一种单向的关系,就像生活中的交际圈,你往往给别人当成朋友,但是在他眼里你不一定被当成了朋友,A类中声明了B是友元,这就代表了B类中可以使用A的私有,但这并不代表A中可以使用B的私有,同时友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。

我们看到这里发生了一点小错误:只是单纯的C将D看成了友元,但是D中并没有C的友元(友元是一种单向关系)

这里有人肯定要说了我们可以定义一个前置声明class C但是你要记住前置声明只是声明了一个类型,但是这个类里面有没有其他私有变量是不知道的!

这时候我们通常的解决办法就是声明和定义分离:

cpp 复制代码
#include<iostream>
using namespace std;

class C
{
	//友元声明,这里已经声明过了D就是一个class(类)
	friend class D;
public:
	void func(const D& dd);
private:
	int _a1 = 1;
	int _a2 = 2;
};

class D
{
	//C将D看成了友元,所以我们就在D中可以访问C
public:
	void func1(const C& aa);
	void func2(const C& aa);

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

void C::func(const D& dd)
{

}

void D::func1(const C& aa)
{
	cout << aa._a1 << endl;
	cout << _b1 << endl;
}

void D::func2(const C& aa)
{
	cout << aa._a2 << endl;
	cout << _b2 << endl;
}

int main()
{
	//...
	return 0;
}

这样的代码就是正确的,一般我们在项目中都是将声明全部防在.h文件中,定义都放在.cpp文件中(我们还是要记住编译器都是向上去寻找我们定义的变量/函数...,有时候代码出了Bug大家也可以往这上面想想)

1.5、总结
  • 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  • 一个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,但是 B 类不是 A 类的友元。
  • 友元类关系不能传递,如果 A 是 B 的友元,B 是 C 的友元,但是 A 不是 C 的友元。
  • 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

二、内部类

2.1、初识内部类

我们首先先来看下面一段代码:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
private:
	//静态成员不存在对象,存在与静态区
	static int _k;
	int _h = 1;
public:
	//内部类
	class B
	{
	public:
	private:
		int _b1;
	};
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

我们已经知道了他是存在于static成员静态区的,所以现在计算大小时只剩下一个_h和内部类,我们运行完结果发现是4,此时我们就要知道 -- 内部类不是成员,B在A的内部并不代表B是A的成员.

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在 全局想比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

简单来说就是我们不能像之前一样简单的去定义B对象了(默认定义都只会去全局搜索不会在类域中去搜索):

这里就会报错,那这时候我们能做的就只有:指定类域

但是也不是说指定类域就可以了,有时候还受到访问限定符的限制:

如果我是私有的也访问不了,这时候B就变成了A的专属子类。

2.2、内部类与外部类的联系

内部类默认是外部类的友元类(内部类可以直接访问外部类的成员)

cpp 复制代码
class A
{
private:
	//静态成员不存在对象,存在与静态区
	static int _k;
	int _h = 1;
public:
	//内部类
	class B //B默认就是A的友元
	{
public:
	void foo(const A& a)
	{
		cout << _k << endl;//OK
		cout << a._h << endl;//OK
	}
private:
		int _b1;
	};
};

内部类默认了A中定义了一个友元B,A将B视为了朋友,所以B就可以访问A中的私有变量(我们也可以理解为B在A的里面,他的权限比较高)

如果将内部类放到private / protected位置,那么它就变成了一个专属内部类,其他地方就都用不了了。

2.3、总结
  • 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类本质也是一种封装,当 A 类跟 B 类紧密关联,A 类实现出来主要就是给 B 类使用,那么可以考虑把 A 类设计为 B 的内部类,如果放到 private/protected 位置,那么 A 类就是 B 类的专属内部类,其他地方都用不了。

三、匿名对象

我们之前定义出来的都是有名对象,那什么是匿名对象呢?

1.1、初识匿名对象

匿名对象我们通过名字来理解:定义变量的时候没有变量名,听起来是不是很奇怪,但这就是匿名对象(只可以是类中的)我们来看下面一段代码(注意看注释):

cpp 复制代码
#include<iostream>
using namespace std;

class Solution 
{
    //内部类
    class Sum 
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };
public:
    int Sum_Solution(int n) 
    {
        // 变⻓数组
        Sum* ptr = new Sum[n];
        delete[] ptr;

        return _ret;
    }

    ~Solution()
    {
        cout << "~Solution()" << endl;
    }

private:
    static int _i;
    static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

int main()
{
    //他的生命周期是整个main函数
    Solution s;//有名对象
    //有名对象定义时候不能加括号要和函数声明区分开

    cout << s.Sum_Solution(10) << endl;

    //他的生命周期只在当前这一行,下一行就会销毁
    //就是一次性的对象,用完就销毁
    Solution();//匿名对象
    //匿名对象这里要加括号!!!(括号里面可以带参)

    int i = 0;

    return 0;
}
1.2、匿名对象的应用

匿名对象可以当场用当场销毁

cpp 复制代码
#include<iostream>
using namespace std;

class Solution
{
    //内部类
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };
public:
    void Clear()
    {
        _i = 1;
        _ret = 0;
    }

    int Sum_Solution(int n)
    {
        // 变⻓数组
        Sum* ptr = new Sum[n];
        delete[] ptr;

        return _ret;
    }

    ~Solution()
    {
        cout << "~Solution()" << endl;
    }

private:
    static int _i;
    static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

int main()
{
    Solution s;
    cout << s.Sum_Solution(10) << endl;
    s.Clear();
    //我只在当前这一行使用我就可以定义匿名对象
    cout << Solution().Sum_Solution(10) << endl;

    int i = 0;

    return 0;
}

引用也是可以引用匿名对象的,但是他们都具有常属性

这里是具有常属性的,我们是传不过去的,会造成权限放大,所以要加上const

那我们思考一下:如果我们想给Solution缺省值该怎么给呢?

下面我们给出代码(大家注意看注释):

cpp 复制代码
#include<iostream>
using namespace std;

class Solution
{
    //内部类
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };
public:
    void Clear()
    {
        _i = 1;
        _ret = 0;
    }

    int Sum_Solution(int n)
    {
        // 变⻓数组
        Sum* ptr = new Sum[n];
        delete[] ptr;

        return _ret;
    }

    ~Solution()
    {
        cout << "~Solution()" << endl;
    }

private:
    static int _i;
    static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

//以前我们给缺省值都是给全局变量或者常量
//我们可以用匿名对象做缺省值(类类型对象经常会这么用)
void func(const Solution& s = Solution(), int i = 1)
{}

int main()
{
    Solution s;
    cout << s.Sum_Solution(10) << endl;
    s.Clear();
    //我只在当前这一行使用我就可以定义匿名对象
    cout << Solution().Sum_Solution(10) << endl;

    func(Solution());
    func(s);
    func();

    return 0;
}

**小tip:**这里的const引用会延长匿名对象的生命周期,此时他的生命周期就跟着s走了

1.3、总结
  • 用类型 (实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名 (实参) 定义出来的叫有名对象
  • 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
cpp 复制代码
#include<iostream>
using namespace std;

class Solution
{
    //内部类
    class Sum
    {
    public:
        Sum()
        {
            _ret += _i;
            ++_i;
        }
    };
public:
    void Clear()
    {
        _i = 1;
        _ret = 0;
    }

    int Sum_Solution(int n)
    {
        // 变⻓数组
        Sum* ptr = new Sum[n];
        delete[] ptr;

        return _ret;
    }

    ~Solution()
    {
        cout << "~Solution()" << endl;
    }

private:
    static int _i;
    static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

//以前我们给缺省值都是给全局变量或者常量
//我们可以用匿名对象做缺省值(类类型对象经常会这么用)
//这时候匿名对象的生命周期就跟着s走了
void func(const Solution& s = Solution(), int i = 1)
{}

int main()
{
    Solution s;
    cout << s.Sum_Solution(10) << endl;
    s.Clear();
    //我只在当前这一行使用我就可以定义匿名对象
    cout << Solution().Sum_Solution(10) << endl;
    //const引用会延长对象生命周期,生命周期跟const引用一样
    //此时匿名对象的生命周期就是ref,ref的生命周期就是main函数
    //所以匿名对象的生命周期也是整个main函数
    const Solution& ref = Solution();

    func(Solution());
    func(s);
    func();

    return 0;
}

四、对象拷贝时的编译器优化

1.1、合并优化

我们在隐式类型转换中就说过连续的拷贝会进行优合并化

cpp 复制代码
#include<iostream>
using namespace std;
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& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

int main()
{
	// 构造 + 拷贝构造 优化 -> 构造
	A aa1 = 1;
	return 0;
}
1.2、省略部分拷贝
cpp 复制代码
#include<iostream>
using namespace std;
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& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}


int main()
{
	//构造 + 拷贝构造 优化 -> 构造
	A aa1 = 1;
	cout << "==================" << endl;

	f1(aa1);
	cout << "==================" << endl;

	f1(1);
	cout << "==================" << endl;

	f1(A(1));
	cout << "==================" << endl;
}

如果我们还想看不优化的场景就需要我们学习Linux再来继续看了

1.3、总结
  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
  • 如何优化 C++ 标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更 "激进" 的编译器还会进行跨行跨表达式的合并优化。
  • linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。
cpp 复制代码
#include<iostream>
using namespace std;
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& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}


int main()
{
	// 传值传参 
 // 构造+拷⻉构造 
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,连续构造+拷⻉构造->优化为直接构造 
	f1(1);
	// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造 
	f1(A(2));
	cout << endl;
	cout << "***********************************************" << endl;
	// 传值返回 
	// 不优化的情况下传值返回,编译器会⽣成⼀个拷⻉返回对象的临时对象作为函数调⽤表达式的返回值

	// ⽆优化 (vs2019 debug) 
	// ⼀些编译器会优化得更厉害,将构造的局部对象和拷⻉构造的临时对象优化为直接构造(vs2022 debug)
	f2();
	cout << endl;
	// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019 debug) 
	// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉的临时对象
	// 和接收返回值对象aa2优化为⼀个直接构造。(vs2022 debug)
	A aa2 = f2();
	cout << endl;
	// ⼀个表达式中,开始构造,中间拷⻉构造+赋值重载->⽆法优化(vs2019 debug) 
    // ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉临时对象合并为⼀个直接构造(vs2022 debug)
	aa1 = f2();
	cout << endl;
	return 0;
}

相关推荐
ShineLeong1 小时前
C的第一次
数据结构·算法
大佬,救命!!!1 小时前
C++本地配置OpenCV
开发语言·c++·opencv·学习笔记·环境配置
噜啦噜啦嘞好1 小时前
Linux:线程池
linux·运维·c++
一 乐1 小时前
宠物店管理|基于Java+vue的宠物猫店管理管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
天天摸鱼的小学生1 小时前
【Java泛型一遍过】
java·开发语言·windows
BD_Marathon1 小时前
【JavaWeb】JS_数据类型和变量
开发语言·javascript·ecmascript
酷酷的佳1 小时前
用C语言写一个可以排序的程序
c++
是宇写的啊1 小时前
算法-前缀和
算法
SunkingYang1 小时前
如何下载dump(C++程序生成)文件所需要的pdb文件,包含自动下载和手动拼接下载
c++·windbg·dump·dmp·pdb下载·手动下载·拼接下载