C++从入门到实战(十)类和对象(最终部分)static成员,内部类,匿名对象与对象拷贝时的编译器优化详解

C++从入门到实战(十)类和对象(最终部分)static成员,内部类,匿名对象与对象拷贝时的编译器优化详解


前言

  • 在上一节的博客中,我们深入探讨了 C++ 初始化列表、类型转换与友元机制,掌握了对象初始化的高级技巧和类间访问权限的控制方法。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

  • 这篇博客将聚焦于 类和对象的终极核心内容,涵盖static 成员、内部类、匿名对象与编译器优化等高级主题。
  • 这些知识不仅是 C++ 面向对象编程的精髓,更是大厂面试高频考点

一、static成员

1. 什么是 static 成员

  • 在 C++ 里,static 成员是被 static 关键字修饰的类成员。
  • 它涵盖静态成员变量和静态成员函数。
  • 和普通的类成员不同,static 成员是为类的所有对象所共享的,并非属于某个特定的对象。

2. 静态成员变量

特点:

  • 所有类对象共同使用。
  • 不在对象里,而是存于静态区。
  • 必须在类外进行初始化。
  • 不能在声明的地方设置缺省值,因为缺省值是用在构造函数初始化列表的,而静态成员变量不属于任何对象,不经过构造函数初始化列表。

访问方式:可以借助类名 :: 静态成员或者对象 . 静态成员来访问。

cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 声明静态成员变量
    static int staticVar;
};

// 在类外初始化静态成员变量
int MyClass::staticVar = 10;

int main() {
    // 通过类名访问静态成员变量
    std::cout << "通过类名访问静态成员变量: " << MyClass::staticVar << std::endl;

    MyClass obj1, obj2;
    // 通过对象访问静态成员变量
    std::cout << "通过对象 obj1 访问静态成员变量: " << obj1.staticVar << std::endl;
    std::cout << "通过对象 obj2 访问静态成员变量: " << obj2.staticVar << std::endl;

    // 修改静态成员变量的值
    obj1.staticVar = 20;
    std::cout << "修改后通过类名访问静态成员变量: " << MyClass::staticVar << std::endl;
    std::cout << "修改后通过对象 obj2 访问静态成员变量: " << obj2.staticVar << std::endl;

    return 0;
}

3. 静态成员函数

特点

  • 没有 this 指针。
  • 能够访问其他的静态成员,不过无法访问非静态成员,原因是没有 this 指针
  • 非静态成员函数可以访问任意的静态成员变量和静态成员函数。
cpp 复制代码
#include <iostream>
class MyClass {
public:
    static int staticVar;
    int nonStaticVar=20;

    static void staticFunction() {
        std::cout << "静态成员函数访问静态成员变量: " << staticVar << std::endl;
        // 下面这行代码会报错,因为静态成员函数不能访问非静态成员
        // std::cout << nonStaticVar << std::endl; 
    }

    // 非静态成员函数
    void nonStaticFunction() {
        std::cout << "非静态成员函数访问静态成员变量: " << staticVar << std::endl;
        staticFunction();
    }
};

// 在类外初始化静态成员变量
int MyClass::staticVar = 10;

int main() {
    // 通过类名调用静态成员函数
    MyClass::staticFunction();

    MyClass obj;
    // 通过对象调用静态成员函数
    obj.staticFunction();
    // 调用非静态成员函数
    obj.nonStaticFunction();

    return 0;
}

4. 访问限定符的影响

  • 静态成员也属于类的成员,会受到 public、protected、private 访问限定符的约束。
  • 要是静态成员被声明为 private,那就只能在类的内部访问。
对比项 静态成员变量 静态成员函数
存储位置 静态区 代码区(函数都存储在此)
所属对象 为所有类对象所共享,不属于某个具体对象 为所有类对象所共享,不属于某个具体对象
初始化 必须在类外进行初始化,不能在声明位置给缺省值初始化 无需额外初始化
this 指针 无此概念 没有 this 指针
访问权限 publicprotectedprivate 访问限定符限制 publicprotectedprivate 访问限定符限制
可访问的成员 无此概念 可以访问其他静态成员,不能访问非静态成员
访问方式 通过 类名::静态成员变量对象.静态成员变量 访问 通过 类名::静态成员函数()对象.静态成员函数() 访问

二、内部类(C++实践中不爱用,java爱用,了解即可)

  • 如果一个类定义在另一个类的内部,这个内部类就叫做 内部类。它是一个独立的类,与全局类相比,仅受外部类的类域限制和访问限定符约束。

2.1 内部类的特点

特点 说明
独立类 内部类是独立的类,与外部类无继承关系,但受外部类类域限制。
不包含在外部对象中 外部类的对象不会包含内部类的对象,两者是独立的内存空间。
默认友元类 内部类默认是外部类的友元类,可以直接访问外部类的私有成员。
访问权限控制 内部类可以放在 publicprotectedprivate 中,限制外部访问。

2.2 内部类的应用场景

  • 当两个类 紧密关联 且内部类仅为外部类服务时(如链表节点类专为链表类服务),可将内部类设计为外部类的私有成员,实现封装和隐藏。

代码示例

cpp 复制代码
#include <iostream>

class Outer {
private:
    int privateVar = 10; // 外部类的私有成员

public:
    // 内部类定义在 public 中,外部可访问
    class Inner {
    public:
        void accessOuterPrivate(Outer& outer) {
            // 内部类作为友元,直接访问外部类的私有成员
            std::cout << "内部类访问外部类私有成员: " << outer.privateVar << std::endl;
        }
    };

    // 内部类定义在 private 中,外部不可直接访问
    class PrivateInner {
    public:
        void doSomething() { std::cout << "私有内部类" << std::endl; }
    };
};

int main() {
    // 实例化内部类(需通过外部类类名访问)
    Outer::Inner inner;
    Outer outer;
    inner.accessOuterPrivate(outer); // 输出:内部类访问外部类私有成员: 10

    // 尝试访问 private 内部类(会报错)
    // Outer::PrivateInner pInner; // 编译错误:PrivateInner 是私有的

    return 0;
}

关键细节

  1. 友元关系
    内部类默认是外部类的友元,因此可以访问外部类的所有成员(包括私有成员)。

  2. 访问内部类

    • 若内部类在 public 中:外部类名::内部类名
    • 若内部类在 private/protected 中:只能在外部类内部或友元中使用。
  3. 与静态成员的区别

    • 内部类是独立类,而静态成员是类的属性或方法。
    • 内部类需要实例化后才能使用,静态成员可直接通过类名访问。

三、匿名对象

3.1 什么是匿名对象

  • 在 C++ 里,我们通常会创建一个有名字的对象,之后通过这个名字来使用该对象
  • 不过有时候,我们只需要临时用一下对象,用完就不再需要它了,这时就可以创建匿名对象。
  • 匿名对象就是没有名字的对象,它的生命周期仅在创建它的那一行代码里,代码执行完这一行,匿名对象就会被销毁。
cpp 复制代码
#include <iostream>

// 定义一个简单的类
class Calculator {
public:
    // 加法方法
    int add(int a, int b) {
        return a + b;
    }
    // 减法方法
    int subtract(int a, int b) {
        return a - b;
    }
};

int main() {
    // 有名对象的使用
    Calculator calc;
    int result1 = calc.add(5, 3);
    std::cout << "有名对象计算结果: " << result1 << std::endl;

    // 匿名对象的使用
    int result2 = Calculator().add(5, 3);
    std::cout << "匿名对象计算结果: " << result2 << std::endl;

    return 0;
}
  • 匿名对象适合那种只需要临时使用一次,之后就不再需要的场景。
  • 比如,只需要调用一次对象的方法,而且不需要保留这个对象供后续使用。
  • 因为匿名对象没有名字,所以没办法在其他代码行再次使用它。要是需要多次使用同一个对象,就得创建有名对象。

四、对象拷贝时的编译器优化(了解即可)

  • 在 C++ 中,对象的拷贝操作(如拷贝构造函数和赋值运算符的使用)可能会带来一定的性能开销,尤其是在处理大型对象或者频繁进行拷贝操作时。
  • 现代编译器为了提高程序的执行效率,会在不影响程序正确性的前提下,尽可能地减少一些不必要的拷贝操作。

4.2 拷贝构造函数和赋值运算符

在深入了解编译器优化之前,我们先来简单回顾一下拷贝构造函数和赋值运算符

  • 拷贝构造函数用于创建一个新对象,该对象是另一个同类型对象的副本。赋值运算符则用于将一个对象的值赋给另一个已经存在的对象。

以下是一个简单的类,包含拷贝构造函数和赋值运算符

cpp 复制代码
#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass(int value) : data(value) {
        std::cout << "Constructor called with value: " << data << std::endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "Copy constructor called with value: " << data << std::endl;
    }

    // 赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            data = other.data;
        }
        std::cout << "Assignment operator called with value: " << data << std::endl;
        return *this;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destructor called with value: " << data << std::endl;
    }

private:
    int data;
};

4.2 编译器优化示例

示例 1:返回值优化(RVO,Return Value Optimization)

返回值优化是一种常见的编译器优化技术,它可以避免在函数返回对象时进行不必要的拷贝。

cpp 复制代码
MyClass createObject() {
    return MyClass(42);
}

int main() {
    MyClass obj = createObject();
    return 0;
}
  • 在这个示例中,createObject 函数返回一个 MyClass 对象。
  • 按照正常的逻辑,会先创建一个临时对象,然后将这个临时对象拷贝给 main 函数中的 obj
  • 但是,编译器可能会进行返回值优化,直接在 obj 的内存位置上构造对象,从而避免了拷贝操作。

输出结果

在开启优化的编译器中,可能只会看到一次构造函数的调用,而不会看到拷贝构造函数的调用。

plaintext 复制代码
Constructor called with value: 42
Destructor called with value: 42

示例 2:具名返回值优化(NRVO,Named Return Value Optimization)

  • 具名返回值优化与返回值优化类似,只不过返回的对象是一个具名对象。
cpp 复制代码
MyClass createNamedObject() {
    MyClass temp(42);
    return temp;
}

int main() {
    MyClass obj = createNamedObject();
    return 0;
}

同样,编译器可能会进行具名返回值优化,直接在 obj 的内存位置上构造对象,避免了拷贝操作。

输出结果

在开启优化的编译器中,可能只会看到一次构造函数的调用,而不会看到拷贝构造函数的调用。

plaintext 复制代码
Constructor called with value: 42
Destructor called with value: 42

注意事项

  • 编译器优化并不是标准规定的行为,不同的编译器可能会有不同的优化策略。有些编译器可能会在默认情况下开启优化,而有些则需要手动指定优化选项(如 -O1-O2-O3 等)。
  • 虽然编译器优化可以提高程序的性能,但在编写代码时,我们仍然应该遵循良好的编程习惯,避免不必要的拷贝操作。

总结(核心概念速记):

核心概念速记
类和对象(终章) = 共享机制(static) + 封装强化(内部类) + 性能优化(匿名对象与拷贝优化)

  • static成员特性

    • 共享存储:静态成员变量存于静态区,所有对象共享。
    • 无this指针:静态成员函数只能访问静态成员,无法操作非静态数据。
    • 初始化规则:静态成员变量必须在类外初始化,且不能在声明时赋默认值。
  • 内部类设计哲学

    • 默认友元:内部类可直接访问外部类私有成员,实现深度封装。
    • 独立存在:内部类对象不包含在外部类对象中,需显式实例化。
  • 对象生命周期优化

    • 匿名对象:临时使用场景下的高效选择,用完即销毁。
    • 编译器优化:RVO/NRVO技术消除不必要的拷贝构造,提升性能。

关键技术对比表

技术点 核心特性 典型应用场景
静态成员变量 共享存储、类外初始化、无this指针 统计类实例数量、全局配置参数
静态成员函数 无this指针、只能访问静态成员 工具类方法、单例模式实现
内部类 默认友元、独立类、受访问权限控制 链表节点类、状态机实现
匿名对象 无名称、临时使用、生命周期短 一次性方法调用、函数参数传递
编译器优化 RVO/NRVO消除拷贝构造、-O2/-O3选项生效 返回值优化、性能敏感代码

知识图谱

复制代码
C++类和对象(终章)  
├─ static成员  
│  ├─ 静态成员变量(共享存储、类外初始化)  
│  └─ 静态成员函数(无this指针、访问限制)  
├─ 内部类  
│  ├─ 默认友元关系  
│  ├─ 独立内存空间  
│  └─ 访问权限控制(public/private)  
├─ 匿名对象  
│  ├─ 临时使用场景  
│  └─ 与具名对象对比  
└─ 编译器优化  
   ├─ RVO(返回值优化)  
   └─ NRVO(具名返回值优化)  

重点提炼

  1. static成员核心

    • 静态成员变量必须在类外初始化,初始化时无需重复static关键字。
    • 静态成员函数不能调用非静态成员,因其没有this指针。
  2. 内部类设计原则

    • 内部类作为外部类的默认友元,可访问其所有成员。
    • 内部类定义位置影响其可见性(public/private)。
  3. 对象生命周期管理

    • 匿名对象适合只使用一次的场景,避免资源浪费。
    • 编译器优化技术(RVO/NRVO)可消除拷贝构造,提升性能。
  4. 性能优化实践

    • 优先使用引用传递代替值传递,减少拷贝开销。
    • 在release版本中开启优化选项(如GCC的-O2),充分发挥编译器优化能力。

典型错误场景

cpp 复制代码
// 错误1:静态成员变量在类内初始化  
class MyClass {  
    static int var = 0; // ❌ 错误,必须在类外初始化  
};  

// 错误2:静态成员函数访问非静态成员  
class MyClass {  
    int data;  
    static void func() {  
        data = 10; // ❌ 错误,静态函数无this指针  
    }  
};  

// 错误3:匿名对象重复使用  
MyClass().doSomething(); // ✅ 正确,临时使用  
MyClass().getResult();   // ✅ 正确,但两次创建匿名对象  

技术演进脉络

复制代码
C++类机制演进 ------ 普通成员 → static成员 → 内部类 → 智能指针管理对象  
   ↓          ↓          ↓           ↓  
功能增强 ------ 独立存储 → 全局共享 → 深度封装 → 自动资源管理  

以上就是这篇博客的全部内容,下一篇我们将继续探索更多精彩内容。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |

相关推荐
花月C5 分钟前
Spring IOC:容器管理与依赖注入秘籍
java·开发语言·rpc
ylfhpy11 分钟前
Java面试黄金宝典22
java·开发语言·算法·面试·职场和发展
Phoebe鑫18 分钟前
数据结构每日一题day9(顺序表)★★★★★
数据结构·算法
老友@23 分钟前
Kafka 全面解析
服务器·分布式·后端·kafka
Java中文社群25 分钟前
超实用!Prompt程序员使用指南,大模型各角色代码实战案例分享
后端·aigc
CYRUS_STUDIO28 分钟前
Frida Hook Native:jobjectArray 参数解析
android·c++·逆向
榆榆欸28 分钟前
4.Socket类、InetAddr类、Epoll类实现模块化
linux·c++·tcp/ip
..过云雨36 分钟前
11. 【C++】模板进阶(函数模板特化、类模板全特化和偏特化、模板的分离编译)
开发语言·c++
烁34744 分钟前
每日一题(小白)动态规划篇2
算法·动态规划
风象南44 分钟前
Spring Boot 实现文件秒传功能
java·spring boot·后端