C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化

C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


目录

  • [C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化](#C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化)
    • 前言
    • 一、友元:有权限访问私有成员的外部角色
      • [1.1 为什么需要友元?](#1.1 为什么需要友元?)
      • [1.2 什么是友元函数?](#1.2 什么是友元函数?)
      • [1.3 一个函数可以成为多个类的友元](#1.3 一个函数可以成为多个类的友元)
      • [1.4 友元函数不受 public/private 位置影响](#1.4 友元函数不受 public/private 位置影响)
    • 二、友元类:整个类都获得访问权限
      • [2.1 什么是友元类?](#2.1 什么是友元类?)
      • [2.2 友元关系是单向的](#2.2 友元关系是单向的)
      • [2.3 友元关系不能传递](#2.3 友元关系不能传递)
      • [2.4 友元要少用](#2.4 友元要少用)
    • 三、内部类:定义在类里面的类
      • [3.1 什么是内部类?](#3.1 什么是内部类?)
      • [3.2 内部类是一个独立的类](#3.2 内部类是一个独立的类)
      • [3.3 内部类默认是外部类的友元](#3.3 内部类默认是外部类的友元)
      • [3.4 什么时候适合使用内部类?](#3.4 什么时候适合使用内部类?)
    • 四、匿名对象:只活一行的临时对象
      • [4.1 什么是匿名对象?](#4.1 什么是匿名对象?)
      • [4.2 匿名对象的生命周期](#4.2 匿名对象的生命周期)
      • [4.3 匿名对象和 Date d() 的区别](#4.3 匿名对象和 Date d() 的区别)
    • 五、对象拷贝时的编译器优化
      • [5.1 为什么同一段代码在不同编译器下输出不同?](#5.1 为什么同一段代码在不同编译器下输出不同?)
      • [5.2 连续构造和拷贝可能被合并](#5.2 连续构造和拷贝可能被合并)
      • [5.3 传值返回也可能被优化](#5.3 传值返回也可能被优化)
      • [5.4 优化不改变语义,只减少中间过程](#5.4 优化不改变语义,只减少中间过程)
    • 六、这几个知识点之间的关系
    • 七、本文总结

前言

前一篇我们讲了初始化列表、explicitstatic 成员。

这一篇继续梳理 C++ 类和对象几个补充知识点:

  • 友元
  • 内部类
  • 匿名对象
  • 对象拷贝时的编译器优化

这些内容不像构造函数、析构函数那样每天都会写,但它们在理解 C++ 类的边界、对象生命周期、编译器行为时非常重要。

其中友元和内部类主要和"封装边界"有关。

匿名对象和编译器优化主要和"临时对象生命周期、拷贝优化"有关。


一、友元:有权限访问私有成员的外部角色

1.1 为什么需要友元?

在 C++ 中,private 成员不能被类外直接访问。

这是封装的基本规则。

例如:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

类外不能直接写:

cpp 复制代码
cout << d._year << endl;

这很好,因为它保护了对象内部状态。

但有些场景下,外部函数确实需要访问类的私有成员。

比如我们重载流插入运算符:

cpp 复制代码
cout << d;

通常会把 operator<< 写成全局函数,而不是成员函数。

但是全局函数不是类成员,不能直接访问 _year_month_day

这时就可以使用友元。


1.2 什么是友元函数?

友元函数是在类内部用 friend 声明的外部函数。

它不是类的成员函数,但可以访问类的私有成员和保护成员。

示例:

cpp 复制代码
class Date
{
    friend ostream& operator<<(ostream& out, const Date& d);

private:
    int _year;
    int _month;
    int _day;
};

这里的 operator<<Date 的友元函数。

它可以在函数体中访问:

cpp 复制代码
d._year
d._month
d._day

注意:

友元声明只是授权,不是成员函数声明。

友元函数不属于这个类。

它没有 this 指针。

它的调用方式也不是:

cpp 复制代码
d.operator<<(...)

而是普通全局函数调用形式。


1.3 一个函数可以成为多个类的友元

有时候一个外部函数需要同时访问两个类的私有成员。

比如:

cpp 复制代码
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;
};

这个 func 同时是 AB 的友元函数。

因此它既能访问 A 的私有成员,也能访问 B 的私有成员。

cpp 复制代码
void func(const A& aa, const B& bb)
{
    cout << aa._a1 << endl;
    cout << bb._b1 << endl;
}

这里还有一个细节:

如果 A 的友元声明里用到了 B,但 B 还没定义,就需要提前写前置声明:

cpp 复制代码
class B;

否则编译器不认识 B


1.4 友元函数不受 public/private 位置影响

友元声明可以写在类中的任何访问区域里。

比如写在 publicprivateprotected 下面都可以。

它不受访问限定符影响。

因为 friend 本身只是告诉编译器:

这个外部函数被授予访问当前类私有成员的权限。


二、友元类:整个类都获得访问权限

2.1 什么是友元类?

除了友元函数,还有友元类。

如果在类 A 中声明:

cpp 复制代码
friend class B;

意思是:

B 类中的所有成员函数,都可以访问 A 类的私有成员和保护成员。

示例:

cpp 复制代码
class A
{
    friend class B;

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

class B
{
public:
    void func1(const A& aa)
    {
        cout << aa._a1 << endl;
    }

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

因为 BA 的友元类,所以 B 的成员函数可以访问 A 的私有成员。


2.2 友元关系是单向的

友元关系不是互相默认开放。

如果 A 声明:

cpp 复制代码
friend class B;

表示 B 可以访问 A 的私有成员。

但这并不表示 A 可以访问 B 的私有成员。

也就是说:

A 把 B 当朋友,不代表 B 也把 A 当朋友。

友元关系是单向的。


2.3 友元关系不能传递

友元关系也不能传递。

如果:

cpp 复制代码
A 是 B 的友元
B 是 C 的友元

不能推出:

cpp 复制代码
A 是 C 的友元

友元授权只对明确声明的关系生效。

这点很像现实生活中"我朋友的朋友不一定是我的朋友"。


2.4 友元要少用

友元确实方便。

但它也会带来问题:

友元会突破封装,增加类之间的耦合。

如果一个类的很多内部细节都依赖友元访问,说明这个类的接口设计可能不够合理。

所以友元适合在确实必要的场景使用,比如:

  • 流插入、流提取运算符重载;
  • 两个类关系非常紧密;
  • 某些工具类需要访问内部实现。

不要把友元当成偷懒访问私有成员的工具。


三、内部类:定义在类里面的类

3.1 什么是内部类?

如果一个类定义在另一个类的内部,这个类就叫内部类。

cpp 复制代码
class A
{
public:
    class B
    {
    public:
        void Foo()
        {
        }
    };
};

这里 B 就是 A 的内部类。

在类外使用时,需要通过类域访问:

cpp 复制代码
A::B b;

3.2 内部类是一个独立的类

这一点很重要。

内部类虽然定义在外部类里面,但它仍然是一个独立的类。

外部类对象中不会自动包含内部类对象。

例如:

cpp 复制代码
class A
{
public:
    class B
    {
    private:
        int _b;
    };

private:
    int _a;
};

sizeof(A) 只和 A 自己的成员有关。

不会因为 A 里面定义了 B,就自动把 B 的成员算进 A 对象里。

可以理解成:

内部类只是名字被放进了外部类的类域中,并不等于外部类对象包含了它。


3.3 内部类默认是外部类的友元

内部类有一个特别点:

内部类默认是外部类的友元类。

也就是说,内部类的成员函数可以访问外部类的私有成员。

例如:

cpp 复制代码
class A
{
private:
    static int _k;
    int _h = 1;

public:
    class B
    {
    public:
        void foo(const A& a)
        {
            cout << _k << endl;
            cout << a._h << endl;
        }
    };
};

这里 BA 的内部类。

B::foo 可以访问 A 的静态成员 _k,也可以通过对象 a 访问 A 的普通私有成员 _h

注意,访问非静态成员仍然需要具体对象:

cpp 复制代码
a._h

因为 _h 属于某个具体的 A 对象。


3.4 什么时候适合使用内部类?

内部类本质上也是一种封装手段。

如果一个类主要是给另一个类内部使用的,而且和外部类关系非常紧密,就可以考虑定义成内部类。

例如:

  • 某个容器内部的迭代器类;
  • 某个算法类内部的辅助节点类;
  • 某个类专用的工具类型。

如果这个内部类不希望外部随便使用,还可以把它放到 private 区域。

这样它就成了外部类的专属实现细节。


四、匿名对象:只活一行的临时对象

4.1 什么是匿名对象?

平时我们创建对象,一般会给对象取名字:

cpp 复制代码
A aa1;
A aa2(2);

这种叫有名对象。

匿名对象没有名字,写法是:

cpp 复制代码
A();
A(1);

它表示临时创建一个对象,用完就销毁。


4.2 匿名对象的生命周期

匿名对象的生命周期通常只在当前这一行。

例如:

cpp 复制代码
A();
A(1);

执行完这一行之后,对象就会被销毁,析构函数会被调用。

所以匿名对象适合"临时用一下"的场景。

比如:

cpp 复制代码
Solution().Sum_Solution(10);

这里:

cpp 复制代码
Solution()

创建了一个匿名对象。

然后马上调用它的成员函数:

cpp 复制代码
.Sum_Solution(10)

调用结束后,这个匿名对象就销毁了。


4.3 匿名对象和 Date d() 的区别

前面学构造函数时提到过:

cpp 复制代码
Date d();

这不是创建对象,而是函数声明。

但是:

cpp 复制代码
Date();

这是创建匿名对象。

区别在于:

cpp 复制代码
Date d();

有一个名字 d,会被解析成函数声明。

cpp 复制代码
Date();

没有对象名,就是创建一个临时匿名对象。


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

5.1 为什么同一段代码在不同编译器下输出不同?

学习拷贝构造时,我们经常会通过打印构造、拷贝构造、析构来观察对象行为。

例如:

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

    A(const A& aa)
    {
        cout << "A(const A& aa)" << endl;
    }

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

然后测试:

cpp 复制代码
f1(A(2));
A aa2 = f2();
aa1 = f2();

有时你会发现:

不同编译器、不同编译模式下,输出结果不完全一样。

这是因为现代编译器会尽量减少不必要的临时对象和拷贝构造。

这种优化通常叫:

拷贝消除,或者返回值优化。


5.2 连续构造和拷贝可能被合并

比如:

cpp 复制代码
f1(A(2));

不优化时,可能是:

  1. 构造临时对象 A(2)
  2. 再拷贝构造形参;
  3. 函数结束析构。

但编译器可能会直接优化成:

直接在形参位置构造对象。

这样就少了一次拷贝构造。

所以你打印出来的构造次数可能比理论分析更少。


5.3 传值返回也可能被优化

看这个函数:

cpp 复制代码
A f2()
{
    A aa;
    return aa;
}

不优化时,可能会出现局部对象到临时返回对象的拷贝。

但现代编译器可能会把局部对象和返回对象合并。

如果写:

cpp 复制代码
A aa2 = f2();

有些编译器甚至会进一步把返回对象和 aa2 合并,直接构造 aa2

这就是为什么同一个例子在 VS2019、VS2022、GCC 不同设置下可能看到不同输出。


5.4 优化不改变语义,只减少中间过程

要注意:

编译器优化的前提是:

不改变程序最终语义。

它只是尽量减少中间临时对象、拷贝构造和析构的过程。

所以学习时可以先从"不优化"的角度理解完整过程。

理解清楚以后,再知道:

实际编译器可能会把连续的构造、拷贝过程合并掉。

在 Linux 下,如果想观察更原始的构造和拷贝过程,可以尝试关闭拷贝消除相关优化:

bash 复制代码
g++ test.cpp -fno-elide-constructors

这样有助于观察对象拷贝过程。


六、这几个知识点之间的关系

这一篇内容看起来比较散,但其实可以按两条线理解。

第一条线是封装边界:

  • private 默认不让外部访问内部数据;
  • 友元允许特定函数或类突破访问限制;
  • 内部类把强关联类型放进类域内部;
  • 友元和内部类都要控制使用范围,不要滥用。

第二条线是对象生命周期:

  • 匿名对象是临时对象,通常只活一行;
  • 传值传参、传值返回可能产生临时对象;
  • 编译器会尽量优化掉不必要的拷贝;
  • 不同编译器和不同编译选项下,构造/拷贝/析构打印结果可能不同。

把这两条线分清楚,这一篇就不会显得零散。


七、本文总结

这一篇主要讲了 C++ 类和对象后半部分的几个补充知识点。

友元:

  • 可以让外部函数或其他类访问当前类的私有成员;
  • 分为友元函数和友元类;
  • 友元关系是单向的,不能传递;
  • 友元会破坏封装,所以不宜滥用。

内部类:

  • 定义在另一个类内部;
  • 是独立的类;
  • 受外部类类域和访问限定符限制;
  • 内部类默认是外部类的友元类;
  • 适合表示强关联的内部辅助类型。

匿名对象:

  • 没有名字;
  • 生命周期通常只在当前语句;
  • 适合临时创建对象并立即使用;
  • 要区分 A()A aa()

对象拷贝优化:

  • 编译器会尽量减少不必要的临时对象和拷贝;
  • 连续构造和拷贝可能被合并;
  • 传值返回可能触发返回值优化;
  • 不同编译器的打印结果可能不同,但语义不变。

友元和内部类是在处理"类的边界",匿名对象和编译器优化是在处理"对象的生命周期"。


相关推荐
ch.ju1 小时前
Java Programming Chapter 4——Error in compilation: it cannot be overwritten.
java·开发语言
xxie1237941 小时前
参数Parameter,形参Formal Parameter,实参Actual Argument
开发语言·python
Irissgwe1 小时前
C++ STL 详解:stack 和 queue 的介绍使用与模拟实现
c++·stl·queue·stack
结城明日奈是我老婆1 小时前
stm32的TIM和PWM学习笔记
笔记·stm32·学习
小短腿的代码世界1 小时前
高性能订单路由与智能拆单算法:Qt在量化交易系统中的核心架构——毫秒级延迟下如何隐藏你的交易意图?
开发语言·qt·架构
油炸自行车1 小时前
【bug】Qt 6 Q_NAMESPACE 跨 DLL 链接错误:LNK2019 无法解析 staticMetaObject
数据库·c++·qt·bug·link2019·q_namespace_exp·namespaceexport
阿正的梦工坊1 小时前
【Rust】20-Rust 编译器架构与 MIR/LLVM 优化管线
开发语言·架构·rust
AI_零食1 小时前
HarmonyOS ArkTS 数据格式化技术深度解析
学习·华为·harmonyos·鸿蒙
在放️1 小时前
Python 爬虫 · XML、xpath 与 lxml 模块基础
开发语言·爬虫·python