一、友元类

当一个函数被声明为友元函数时,它可以访问其它类中的私有成员变量及方法。友元函数不是定义在类中的成员函数,但是它可以访问该类中的私有成员,并且友元函数的执行与类的对象无关。

1.1友元函数实例

在C++中,要声明一个函数为友元函数,需要在函数的声明或定义中使用 friend 关键字。例如:

class MyClass {
  private:
    int privateVar; // 私有成员变量

  public:
    MyClass(int x) : privateVar(x) {}

    friend void printPrivateVar(MyClass obj); // 声明 printPrivateVar() 函数为友元函数
};

void printPrivateVar(MyClass obj) {
  std::cout << "The private variable is: " << obj.privateVar << std::endl;
}

int main() {
  MyClass obj(10);
  printPrivateVar(obj); // 友元函数可以访问私有成员变量
  return 0;
}

《深入理解C++友元函数和拷贝对象时的编译器优化》_c++

当声明一个函数为友元r函数时,该函数并不是类的成员函数, 所以它没有this指针,因此也不能直接访问类成员变量。但是,由于该函数被声明为友元函数,因此可以访问类的私有成员变量,并且函数中的参数还可以是该类的对象,而不是普通的参数。

1.2非友元函数访问私有成员变量

当一个函数没有声明为另一个类的友元函数,又想访问和修改另一个类的成员变量,一般我们可以使用写getset两个函数,通过调这个类的公共函数对私有成员变量进行修改,这个地方和java对私有变量的访问和修改方法很像。

#include<iostream>

class MyClass {
private:
    int privateVar; // 私有成员变量

public:
      //初始化列表
    MyClass(int x) 
    : privateVar(x)
    {}
    //get方法
    int getPrivateVar()
    {
        return this->privateVar;
    }
    //set方法
    void setPrivateVar(int num)
    {
        this->privateVar = num;
    }
    //friend void printPrivateVar(MyClass obj); // 声明 printPrivateVar() 函数为友元函数
};

void printPrivateVar(MyClass obj) {
    std::cout << "The private variable is: " << obj.getPrivateVar() << std::endl;
}

int main() {
    MyClass obj(10);
    printPrivateVar(obj); // 友元函数可以访问私有成员变量
    return 0;
}

《深入理解C++友元函数和拷贝对象时的编译器优化》_友元函数_02

1.3友元函数运算符重载

友元函数的一个常见用途是重载运算符。下面是一个示例,演示如何在C++中使用友元函数来实现运算符的重载:

class Complex {
  private:
    double real, imag;

  public:
  //初始化列表
    Complex(double r = 0, double i = 0) 
      : real(r)
      , imag(i) 
      {}

    Complex operator+(Complex const &obj) {
      return Complex(real + obj.real, imag + obj.imag);
    }

    friend std::ostream& operator<<(std::ostream& os, Complex const &obj) {
        os << obj.real << " + " << obj.imag << "i";
        return os;
    }
};

int main() {
  Complex a(1, 2), b(3, 4);
  Complex c = a + b;
  std::cout << c << std::endl;
  return 0;
}

在这个例子中,我们重载了 + 运算符,使它可以用于两个复数类型的对象相加。我们还通过一个友元函数重载了插入运算符 <<,以便我们可以使用 std::cout 输出 Complex 对象。

友元函数可以方便地访问其他类的私有成员变量和方法,因此经常在类的实现中使用。但是,过多地使用友元函数可能会破坏封装性,因此要谨慎使用。


二、友元函数的优缺点

2.1友元函数的优点:

  1. 友元函数可以访问类的所有私有成员,这提供了更多的灵活性和便利性,同时也使得代码更加简洁易懂。
  2. 友元函数可以作为非成员函数来扩展类的功能和操作,这提供了一种方便的方法来对类进行扩展。
  3. 友元函数可以提高代码效率,因为它们不需要通过类的对象来访问类的成员,从而避免了创建对象的开销。

2.2友元函数的缺点:

  1. 友元函数会破坏类的封装性,因为它可以直接访问类的私有成员。这可能导致代码复杂性增加,难以维护,并且可能会导致数据安全问题。
  2. 友元函数会增加代码的复杂度,因为需要在类定义中添加额外的声明和定义,从而可能增加代码的阅读难度和错误概率。
  3. 友元函数只能访问一个类的私有成员,无法访问多个类的私有成员,这可能会限制其灵活性和可用性。

总的来说,友元函数虽然具有许多优点,但是如果使用不当,也可能带来一些问题和缺点。因此,在编写代码时应该慎重选择是否使用友元函数,以及何时和如何使用它们。所以尽量不适用除非必须使用。


三、拷贝对象时的编译器优化问题

随着编译器的不断迭代升级,编译器对原来c++中一些语法的操作(开销比较大的)进行了优化,目的是减少不必要的过程,提高编译器编译速度和程序运行速度和内存开销。 编译器的优化主要出现在传参和传返回值两个地方

#include<iostream>
using namespace std;


class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
		A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	 //传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

《深入理解C++友元函数和拷贝对象时的编译器优化》_编译器优化_03

3.1传值传参和传值返回

在调用函数的时候传入一个对象就会涉及到如何进行传参和返回。

A aa1;

f1(aa1);

先进行了创建对象调用了构造函数,再给函数f1传入参数,如何决定这个是传值还是传址传参呢?

void f1(A aa) {}

这个 A aa说明了没有进行一个引用在传参的时候是传值传参需要进行一次拷贝构造(复制一份)

同理传值返回也是如此。看函数有没有进行一个引用。

3.2 隐式类型优化

f1(1); 从1到对象aa实际是需要进行隐式类型转化,进行了连续构造+拷贝构造 ------->优化为直接构造

3.3 表达式的优化

// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;

3.4 编译器优化问题的总结

如何能够了解编译器做了那些优化,最重要的是要知道那些是直接构造,那些进行了拷贝构造,如何传参如何返回值等细节知识,进行不断的测试。就能知道编译器优化了哪些问题。