类和对象下(内部类,匿名对象,对象拷贝时的编译器优化)

5. 内部类

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

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

• 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

• 代码示例:

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

class Outer {
public:
    // 内部类定义在外部类内部
    class Inner
    {
    public:
        void show() 
        {
            cout << "Inner class method" << endl;
        }
    };

    void useInner() 
    {
        Inner in;
        in.show();
    }
};

int main() {
    Outer::Inner obj;  // 通过外部类访问内部类
    obj.show();
    return 0;
}

• 内部类默认是外部类的友元类,因此内部类可以直接访问外部类的私有成员:

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

class Outer    
{
private:
    int value = 100;
public:
    class Inner 
    {
    public:
        void accessOuter(Outer& o)    
          {
            // 内部类作为友元,可直接访问外部类的私有成员
            cout << "Outer private value: " << o.value << endl;
          }
    };
};

int main() 
{
    Outer o;
    Outer::Inner in;
    in.accessOuter(o);
    return 0;
}

6.匿名对象

• 用类型(实参)定义出来的对象叫做匿名对象,匿名对象的特点不用取名字,例如A();相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。

• 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。

• 代码示例:

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

class A {
public:
    A(int x = 0) 
    : _x(x) 
    {
        cout << "A(int)" << endl;
    }

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

    void show() const 
    {
        cout << "x = " << _x << endl;
    }

private:
    int _x;
};

// 匿名对象作为函数参数
void func(const A& a)    
{
    cout << "func called" << endl;
}

int main()    
{
    // 匿名对象:A(10) 没有名字,生命周期仅限当前这一行
    A(10).show();          // 临时调用成员函数,行末即析构

    func(A(20));           // 匿名对象作为函数实参,函数调用结束后析构

    cout << "main continues" << endl;
    return 0;
}

• 上例中 A(10)A(20) 都是匿名对象,它们没有对象名,生命周期只持续到当前语句结束。当该行执行完毕后,编译器会立即调用析构函数释放资源,因此匿名对象适合"临时用一下"的场景。

• 下面通过一个对比示例,直观展示有名对象和匿名对象在生命周期与资源释放时机上的差异:

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

class A {
public:
    A(int x = 0) : _x(x) 
    {
        cout << "A(int) 构造 _x = " << _x << endl;
    }

    ~A() 
    {
        cout << "~A() 析构 _x = " << _x << endl;
    }

    void show() const 
    {
        cout << "show() _x = " << _x << endl;
    }

private:
    int _x;
};

void func(const A& a)    
{
    cout << "func 调用中" << endl;
}

int main() 
{
    cout << "--- 有名对象 ---" << endl;
    A named(1);            // 有名对象,生命周期从定义处开始
    named.show();          // 可以多次使用
    named.show();          // 作用域结束前一直有效

    cout << "--- 匿名对象 ---" << endl;
    A(2).show();           // 匿名对象,仅当前这一行有效,行末立即析构
    func(A(3));            // 匿名对象作为实参,函数调用结束后析构

    cout << "--- main 结束前 ---" << endl;
    // 此时 named 仍存活,直到 main 结束才析构
    return 0;
}
// 输出结果:
// --- 有名对象 ---
// A(int) 构造 _x = 1
// show() _x = 1
// show() _x = 1
// --- 匿名对象 ---
// A(int) 构造 _x = 2
// show() _x = 2
// ~A() 析构 _x = 2        // 匿名对象 A(2) 行末立即析构
// A(int) 构造 _x = 3
// func 调用中
// ~A() 析构 _x = 3        // 匿名对象 A(3) 函数调用结束后析构
// --- main 结束前 ---
// ~A() 析构 _x = 1        // 有名对象 named 在 main 结束时才析构

• 对比可见:有名对象 named 从定义处开始存活,直到其作用域(这里是 main 函数)结束才调用析构函数释放资源,期间可以反复使用;而匿名对象 A(2)A(3) 没有名字,生命周期只持续到当前语句结束,行末或函数调用结束后编译器会立即调用析构函数释放资源。因此,如果只是临时用一下对象,使用匿名对象可以更及时地释放资源、减少内存占用。

7.对象拷贝时的编译器优化

• 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。

• 如何优化 C++ 标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。

• 不同编译器对拷贝优化的典型行为对比如下:

编译器 默认优化行为 关闭优化的编译选项
GCC 默认开启拷贝省略(copy elision),对连续表达式中的拷贝进行合并优化;在 C++17 下对按值返回的临时对象保证省略拷贝。 -fno-elide-constructors
Clang 默认行为与 GCC 基本一致,开启拷贝省略;对临时对象和按值返回的优化较为积极。 -fno-elide-constructors
MSVC 默认开启返回值优化(RVO)和命名返回值优化(NRVO),但具体行为受 /O1、/O2 等优化级别影响。 /Zc:noElideConstructors(或使用 /Od 关闭优化)

• 下面通过一个具体示例,直观对比开启优化(默认)和关闭优化(-fno-elide-constructors)时,传参和返回值过程中的拷贝次数差异。

cpp 复制代码
#include <iostream>
using namespace std;
class A {
public:
A(int x = 0) : _x(x) {
cout << "A(int)" << endl;
}
A(const A& a) : _x(a._x) {
    cout << "A(const A&) 拷贝构造" << endl;
}

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

int _x;
};
// 传参:按值接收,会触发拷贝构造
void func(A a) {
cout << "func 内 _x = " << a._x << endl;
}
// 返回值:返回局部对象,可能触发拷贝构造
A getA() {
A tmp(10);
return tmp;
}
int main() {
cout << "--- 传参 ---" << endl;
A obj1(1);
func(obj1);          // 把 obj1 拷贝给形参 a
cout << "--- 返回值 ---" << endl;
A obj2 = getA();     // 把 getA 的返回值拷贝给 obj2

cout << "--- main 结束 ---" << endl;
return 0;
}

• 编译运行对比:

bash 复制代码
# 默认开启优化(拷贝省略生效)
g++ test.cpp -o test_default
./test_default
关闭拷贝优化
g++ test.cpp -fno-elide-constructors -o test_noelide
./test_noelide

• 开启优化(默认)时,编译器会尽量省略中间拷贝,输出大致如下:

text 复制代码
--- 传参 ---
A(int)
func 内 _x = 1
--- 返回值 ---
A(int)
--- main 结束 ---
~A()
~A()
~A()

• 关闭优化(-fno-elide-constructors)时,传参和返回值过程中的拷贝不会被省略,输出大致如下:

text 复制代码
--- 传参 ---
A(int)
A(const A&) 拷贝构造
func 内 _x = 1
--- 返回值 ---
A(int)
A(const A&) 拷贝构造
--- main 结束 ---
~A()
~A()
~A()
~A()

• 对比可见:开启优化后,传参时省略了形参的拷贝构造,返回值时也省略了临时对象的拷贝构造,整体少调用了两次拷贝构造,析构次数也随之减少,程序运行效率更高。

• Linux 下可以将上面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。

相关推荐
其实防守也摸鱼18 分钟前
智能体推荐:精选 AI Agent 工具与实战指南
运维·开发语言·人工智能·学习·web安全·自动化
always_TT18 分钟前
【Python 字符串格式化:format() 方法】
android·开发语言·python
小义_23 分钟前
JDK 深度解析
java·linux·开发语言·python·面试
旖旎夜光33 分钟前
LeetCode 238:除自身以外数组的乘积(前缀和) —— 题解
数据结构·c++·算法·leetcode·前缀和
智购科技自动售货机工厂37 分钟前
2026自动售货机异常重启根因分析:从日志挖掘到内存取证的技术实践~YH
开发语言·数据库·单片机·嵌入式硬件·人机交互
一条大祥脚40 分钟前
26杭电暑期第八场(后半)快读|快写|tarjan|路径DP|mex转化|扫描线|前缀和|二分图
数据结构·算法·tarjan·杭电多校·强联通分量·动态规划dp
奈斯先生Vector43 分钟前
从“能调用”到“可运营”:AI Agent 进入多模型时代后的架构升级
java·javascript·数据库·人工智能·算法·架构·aigc
Escalating_xu44 分钟前
【C++类和对象(上)】从类的定义、封装与对象模型到内存对齐和 this 指针
开发语言·前端·c++
吃着火锅x唱着歌1 小时前
Effective C++ 学习笔记 条款45 运用成员函数模板接受所有兼容类型
c++·笔记·学习