NJUのC++课:面向对象之封装

「C++ 40 周年」主题征文大赛(有机会与C++之父现场交流!) 10w+人浏览 705人参与

C++的面向对象实现方案

OO 控制:核心是访问权限管控

C 语言结构体成员直接可见,无访问限制;C++ 的 OO 控制仅聚焦访问权限,通过private等限定符隔离,不改变成员本质,仅语法层面限制访问范围。

  • private 的 “不可见” 原理

    private阻止外部调用的核心是编译时检查:外部访问私有成员时,编译器直接报错终止编译,从源头拦截非法访问,属于语法校验而非内存隔离。

多文件组织与成员存储

类多文件组织需用::明确成员所属类,存储特性如下:

  • 成员变量:每个对象独占独立内存,互不干扰;
  • 成员函数:所有对象共享,固定存储在 DATA 区;
  • 头文件放成员函数:需用inline(仅适用于简单函数),可降低调用开销。

关键认知

OO 机制仅是程序概念上的实现,未改变底层内存模型,本质是 C++ 新增的语法规则与编译检查,实现封装的逻辑抽象。

构造函数

在学习构造函数前需明确:程序运行时内存分为全局静态区、栈空间、堆空间,不同内存区域的分配 / 释放规则不同,而构造函数的核心作用,就是对对象(无论存储在哪个区域)进行初始化。

构造函数:对象的 “初始化器”

构造函数是类中用于初始化对象的特殊成员函数,核心规则如下:

  • 命名规则:与类名完全相同,无返回类型(连void都不能写);
  • 调用方式:对象创建时自动调用,无法在代码中直接显式调用;
  • 核心特性:支持重载,可定义多个参数不同的构造函数,适配不同初始化场景。
  • 默认构造函数的关键规则
    • 生成条件:当类中未自定义任何构造函数时,编译器会自动生成无参数的默认构造函数;
      • 这里的参数在静态区默认为0,堆栈则随机
    • 失效场景:一旦自定义构造函数(无论是否带参数),编译器不再自动生成;
    • C++11 扩展:可用=default显式要求编译器生成默认构造函数,用=delete禁用指定构造函数。

构造函数的访问权限

  • 常规用法:定义为public,允许外部代码直接创建对象;
  • 特殊场景:定义为private时,外部无法直接创建对象,需通过类的静态成员函数等方式接管创建(如单例模式)。

委托构造:代码复用的简化方式

委托构造允许一个构造函数调用同类的其他构造函数,核心目的是复用初始化逻辑,语法简洁:例:C():C(1){}(无参构造函数委托给带参的C(1),复用其初始化逻辑)。

TDate() : TDate(2000) {  // 委托给TDate(int y)
    cout << "调用了无参构造函数(委托)" << endl;
}

成员初始化表:解决特殊成员的初始化难题

构造函数体内的赋值操作有局限性:对于const int(常量成员)、int&(引用成员)这类必须“定义时立即绑定值”的成员,构造函数体内赋值无效——它们要求在对象创建的同时完成初始化,而非后续赋值。此时必须使用成员初始化表,在构造函数参数列表后、函数体前完成初始化。

  • 核心规则与优势

    1. 执行顺序:严格按照类中成员的声明次序执行(而非初始化表中的书写顺序),这是易踩坑点;
    2. 效率优势:直接对成员进行初始化,仅需一次操作;而构造函数体内赋值是“先默认初始化,再赋值”,多一次开销。
  • C++11 扩展特性

    • 就地成员初值:用=给成员指定默认值(如int a = 10;),本质是“默认匹配初始化”,与成员初始化表作用一致;
    • 统一初始化:用{}初始化(支持窄化检查,如int b{3.14}会编译报错),语法更统一,类似“批量/逐元素初始化”(类比for-each的遍历逻辑)。
  • 四特性联动示例

    class Test {
    private:
        int x = 5;          // 就地成员初值(C++11)
        const int y;        // 常量成员,必须初始化
        int& z;             // 引用成员,必须绑定
        int arr[3];         // 数组成员
    public:
        // 成员初始化表 + 统一初始化联动,覆盖所有特性
        Test(int& ref, int y_val) : y(y_val), z(ref), arr{1,2,3} {}
        // 说明:x用就地初值5初始化,y通过初始化表赋值,z绑定外部引用,arr用统一初始化{}赋值
    };
    
    int main() {
        int num = 20;
        Test t(num, 100);  // 联动效果:x=5(就地)、y=100(初始化表)、z绑定num、arr=[1,2,3](统一初始化)
        return 0;
    }
    
    

    注:示例中联动了“const成员、引用成员、就地初值、统一初始化、成员初始化表”,既体现了初始化表的核心作用,也展示了C++11特性的配合使用,同时规避了“声明次序≠初始化表顺序”的坑。

析构函数

析构函数:对象的“资源清理者”

析构函数是类的特殊成员函数,核心作用是在对象消亡时自动调用,清理对象所持有的资源——这是C++基于RAII(资源获取即初始化)思想的核心实现,确保资源释放的确定性。

与Java GC的核心区别

特性C++ 析构函数Java GC
触发时机对象生命周期结束时自动调用(确定)不定时触发(依赖虚拟机调度)
清理范围内存资源 + 非内存资源(如文件句柄、网络连接)仅回收内存资源
并发问题无(确定性调用)可能引发并发冲突

简单说:GC是“被动、局限的内存回收”,而析构函数是“主动、全面的资源清理”。

资源释放的关键规则

析构函数中需手动释放堆内存(堆资源由程序员申请,需手动回收),但无需释放栈内存和静态内存(栈内存随函数栈帧销毁,静态内存随程序结束释放):

class FileHandler {
private:
    FILE* file;       // 堆分配的文件资源(非内存资源)
    int* data;        // 堆内存资源
public:
    // 构造函数:获取资源
    FileHandler(const char* filename) {
        file = fopen(filename, "r");  // 打开文件(非内存资源)
        data = new int[100];          // 申请堆内存
    }
    // 析构函数:释放资源
    ~FileHandler() {
        if (file != nullptr) fclose(file);  // 释放非内存资源(文件句柄)
        delete[] data;                      // 手动释放堆内存
        // 无需释放栈/静态内存:data是指针(栈上),指向的堆空间才需释放
    }
};

private析构函数:自主控制对象存储分配

析构函数可定义为private,此时外部无法直接通过对象生命周期触发析构(编译器禁止),核心作用是强制自主控制对象的存储分配

  • 外部不能定义栈对象(栈对象生命周期结束时会自动调用析构,而外部无访问权限,编译报错);
  • 只能在堆中创建对象,通过类提供的public接口(如静态成员函数)手动触发销毁。

示例:通过private析构实现“堆对象专属”的类:

class HeapOnly {
private:
    int val;
    ~HeapOnly() {  // 私有析构:外部无法直接调用
        cout << "释放HeapOnly对象" << endl;
    }
public:
    HeapOnly(int v) : val(v) {}
    // 提供public静态接口:创建和销毁堆对象
    static HeapOnly* create(int v) {
        return new HeapOnly(v);  // 堆上创建对象(析构由类自主控制)
    }
    static void destroy(HeapOnly* ptr) {
        delete ptr;  // 类内部可访问private析构,手动释放堆对象
    }
};

// 外部使用:
int main() {
    // HeapOnly obj(10);  // 编译报错:外部无法创建栈对象(析构不可访问)
    HeapOnly* p = HeapOnly::create(10);  // 只能通过接口创建堆对象
    // 业务逻辑...
    HeapOnly::destroy(p);  // 手动调用接口销毁,触发析构
    return 0;
}

核心总结

析构函数的核心是“确定性释放资源”,RAII思想让资源的获取(构造)和释放(析构)强绑定;private析构则通过限制析构调用权限,强制对象只能在堆上分配,实现存储分配的自主控制。

拷贝构造函数

拷贝构造函数是C++中特殊的构造函数,核心用途是用同类已存在的对象,初始化新创建的对象——它不是“赋值操作”,而是“创建新对象时的初始化行为”。

核心语法格式

类名(const 类名& 源对象) {
    // 拷贝逻辑(默认或自定义)
}
  • 参数必须是“同类对象的const引用”(避免无限递归拷贝,const保证源对象不被修改);
  • 是构造函数的一种,无返回类型,名称与类名一致。

默认拷贝构造的行为

当类中未自定义拷贝构造函数时,编译器会自动生成默认拷贝构造函数,其行为是:

  • 执行浅拷贝:对每个成员变量递归调用其自身的拷贝构造函数(基本类型直接复制值,自定义类型调用对应拷贝构造);
  • 可通过=delete显式禁用默认拷贝构造(如类名(const 类名&) = delete;),禁止对象按值拷贝。

3. 自定义拷贝构造:深拷贝示例

默认浅拷贝在成员包含堆内存时会出问题(如重复释放内存),此时需自定义拷贝构造实现深拷贝

class String {
private:
    char* str;  // 堆内存成员
public:
    // 普通构造:申请堆内存
    String(const char* s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    // 自定义拷贝构造:深拷贝(避免浅拷贝的堆内存共享问题)
    String(const String& other) {
        // 为新对象重新申请堆内存,复制源对象内容
        str = new char[strlen(other.str) + 1];
        strcpy(str, other.str);
    }
    // 析构函数:释放堆内存
    ~String() { delete[] str; }
    // 禁用默认拷贝构造(若需禁止拷贝,直接写:String(const String&) = delete;)
};

// 使用:深拷贝确保两个对象的str指向独立堆内存
String s1("hello");
String s2 = s1;  // 调用自定义深拷贝构造,s2.str是新申请的内存

4. 拷贝构造的调用场景

只有“用已有对象初始化新对象”时才会调用,常见形式:

A a;
A b = a;    // 拷贝构造(初始化新对象b)
A c(a);     // 拷贝构造(与上等价,更直观的初始化写法)
// 注意:A d; d = a; 这是赋值操作,不调用拷贝构造!

5. 关键坑点:子对象的显式拷贝

若类中包含“子对象”(即其他类的成员对象),且自定义了拷贝构造函数,编译器不会自动递归调用子对象的拷贝构造,而是会调用子对象的默认构造函数——这会导致子对象未按源对象拷贝,需在成员初始化表中显式调用子对象的拷贝构造

// 子对象类
class SubObj {
public:
    int val;
    SubObj(int v) : val(v) {}
    SubObj(const SubObj& other) : val(other.val) {  // 子对象的拷贝构造
        cout << "SubObj拷贝构造调用" << endl;
    }
};

// 包含子对象的主类
class MainObj {
private:
    SubObj sub;  // 子对象
public:
    // 自定义MainObj拷贝构造:必须显式调用SubObj的拷贝构造
    MainObj(const MainObj& other) : sub(other.sub) {  // 初始化表显式拷贝子对象
        // 若不写sub(other.sub),会调用SubObj的默认构造(而非拷贝构造)
    }
    MainObj(int v) : sub(v) {}  // 普通构造:初始化子对象
};

// 使用:
MainObj m1(10);
MainObj m2 = m1;  // 调用MainObj拷贝构造,同时显式触发SubObj拷贝构造

核心总结

拷贝构造的核心是“初始化拷贝”,默认浅拷贝适用于无堆内存的简单类;含堆内存时必须自定义深拷贝;自定义拷贝构造时,子对象需在初始化表显式调用其拷贝构造,避免编译器误用默认构造。

移动构造函数

先理清基础:左值、右值与引用绑定

要理解移动构造,首先得明确左值、右值及引用的绑定规则:

  • 左值:可直接访问、有明确内存地址的对象(如变量、数组元素);

  • 右值:临时产生、无法通过代码直接访问的对象(如字面量、函数返回的临时变量);

  • 左值引用(&):仅能绑定左值;

  • 右值引用(&&):仅能绑定右值,但绑定后自身会变成左值;

  • 特殊规则:常量左值引用(const T&)可绑定左值或右值(万能绑定)。

  • 简单示例:

    int a = 10;       // a是左值(可访问、有地址)
    int& ref1 = a;    // 左值引用绑定左值(合法)
    // int& ref2 = 20; // 错误:左值引用不能绑定右值(20是右值)
    
    int&& ref3 = 20;  // 右值引用绑定右值(合法)
    // int&& ref4 = a; // 错误:右值引用不能绑定左值
    
    const int& ref5 = a;  // 常量左值引用绑定左值(合法)
    const int& ref6 = 20; // 常量左值引用绑定右值(合法,万能绑定)
    
    

移动构造函数:解决临时对象的拷贝浪费

拷贝构造函数(无论浅拷贝、深拷贝)都会为新对象重新分配资源(如new申请内存),但对于函数返回的临时对象(右值),这种“完整拷贝”完全是浪费——临时对象用完就销毁,与其拷贝资源,不如直接“接管”它的资源。

移动构造的核心语法

参数必须是右值引用,核心逻辑是“资源转移”(窃取原对象的资源),而非“拷贝”:

class MyArray {
private:
    int* arr;
    int size;
public:
    // 普通构造:申请堆内存
    MyArray(int n) : size(n) {
        arr = new int[size];
        cout << "普通构造:申请内存" << endl;
    }

    // 移动构造函数:参数为右值引用
    MyArray(MyArray&& other) : size(other.size), arr(other.arr) {
        other.arr = nullptr; // 关键:原对象指针置空,避免析构时重复释放
        cout << "移动构造:接管资源" << endl;
    }

    // 析构函数:释放内存
    ~MyArray() {
        if (arr) delete[] arr;
    }
};

2. 移动构造的调用场景

当用右值初始化新对象时自动调用,常见两种情况:

  • 函数返回的临时对象(天然右值);
  • std::move()显式将左值转换为右值。

3. 完整示例(含调用场景)

// 生成临时对象(返回右值)
MyArray generateArray() {
    return MyArray(10); // 返回临时对象(右值)
}

int main() {
    // 场景1:临时对象初始化新对象(触发移动构造)
    MyArray arr1 = generateArray();
    // 输出:普通构造→移动构造(接管临时对象的资源,无额外拷贝)

    // 场景2:std::move转换左值为右值(触发移动构造)
    MyArray arr2(5);       // 普通构造
    MyArray arr3 = std::move(arr2); // 显式转换为右值,触发移动构造
    // 注意:arr2的arr已被置空,后续不可再使用

    return 0;
}

核心总结

移动构造的本质是“资源转移”,针对右值(临时对象)避免冗余拷贝,提升效率;其参数是右值引用,调用时机为“右值初始化新对象”(含std::move转换的左值),核心步骤是“窃取资源+原对象置空”,防止重复释放。

动态对象和动态对象数组

动态对象指在堆内存上创建的对象(区别于栈对象——栈对象随作用域结束自动销毁),C++通过new(创建)和delete(销毁)运算符管理,核心是“手动分配内存+自动调用构造/析构”,实现对象生命周期的自主控制。

new运算符:动态对象的创建

new是C++专属的动态创建运算符,不仅负责堆内存分配,还会自动调用构造函数初始化对象,支持基本类型和自定义类型,执行步骤固定:

  1. 分配堆内存;
  2. 调用构造函数(自定义类型)或直接初始化(基本类型);
  3. 返回对象的内存地址(需用指针接收)。
  • new的三种核心语法(附示例)

    // 1. 基本类型(无初始化)
    int* p1 = new int;  // 堆上分配int内存,默认值不确定
    
    // 2. 带初始化的基本类型
    int* p2 = new int(10);  // 初始化值为10
    int* p3 = new int{20};  // C++11统一初始化,效果同上
    
    // 3. 类对象(带/不带参数构造)
    class Test {
    public:
        Test() { cout << "默认构造" << endl; }
        Test(int a) { cout << "带参构造:" << a << endl; }
    };
    Test* t1 = new Test;     // 调用默认构造
    Test* t2 = new Test(30); // 调用带参构造
    Test* t3 = new Test{40}; // 统一初始化语法
    
    
  • new的关键特点

    • 动态对象是匿名对象,无法直接访问,必须通过指针间接操作;

    • 指针大小仅与系统位数有关(32位系统4字节,64位8字节),与指向对象的大小无关;

    • 支持重载:可自定义new的内存分配逻辑(极简示例):

      // 全局重载new(仅演示语法,实际需处理内存分配)
      void* operator new(size_t size) {
          cout << "自定义new分配" << size << "字节" << endl;
          return malloc(size); // 底层仍可调用malloc
      }
      Test* t4 = new Test; // 调用自定义new,输出"自定义new分配4字节"(假设Test占4字节)
      
      

delete运算符:动态对象的销毁

deletenew的配套销毁运算符,核心是“先销毁对象,再释放内存”,执行步骤:

  1. 调用析构函数(自定义类型,释放对象持有的资源);
  2. 释放堆内存(归还给操作系统);
  3. 建议将指针置空(p = nullptr),避免悬空指针。
  • 核心示例:

    // 销毁基本类型(无需析构,直接释放内存)
    delete p1;
    p1 = nullptr; // 置空避免悬空
    
    // 销毁类对象(自动调用析构)
    delete t1;
    t1 = nullptr;
    delete t2;
    t2 = nullptr;
    
    

动态对象数组:创建与销毁的关键规则

动态对象数组的创建语法与普通数组类似,但销毁时必须用delete[]不可省去[]

  • 数组的创建与销毁示例

    // 创建:动态数组(10个Test对象,均调用默认构造)
    Test* arr = new Test[10];
    
    // 销毁:必须用delete[],不可写delete arr!
    delete[] arr;
    arr = nullptr;
    
    
  • 为什么不能省去[]?(底层原理)

    动态对象数组的内存布局中,编译器会在数组指针arr前侧额外分配一块空间,存储数组的对象个数(长度)。

    • delete[]时:编译器读取这块“长度空间”,遍历所有对象调用析构函数,再释放整个数组内存;
    • 省去[](用delete arr)时:编译器不会读取长度信息,仅调用数组第一个对象的析构函数,释放的也只是第一个对象的内存——导致内存泄漏+析构不完整(后续9个对象未调用析构,资源无法释放)。

核心总结

  • new/delete是成对使用的:new创建→delete销毁,new[]创建→delete[]销毁,不可混用;
  • new的核心优势:比C的malloc多了“自动调用构造函数”,delete比free多了“自动调用析构函数”;
  • 动态对象数组的[]是“析构遍历开关”,省去则会导致严重的内存和资源问题,是高频易错点。

Const成员

const成员变量与const成员函数:对象的“只读控制”

const成员变量的初始化规则

const成员变量(如const int x)必须在对象创建时完成初始化,且初始化后不可修改:

  • 老版C++中,需通过成员初始化表完成初始化(无法在构造函数体内赋值);
  • 新版C++支持就地成员初值(如const int x = 10;),语法更便捷,但核心原则不变——const成员必须在定义时绑定值。

示例(初始化表方式,兼容所有版本):

class A {
    const int x;  // const成员变量,必须初始化
public:
    A(int c) : x(c) {}  // 唯一合法方式:初始化表中绑定值
};

const成员函数:“不修改对象”的语法承诺

const成员函数是类中承诺“不会修改对象普通成员变量”的特殊成员函数,核心语法是在函数声明后加const

  • 底层实现原理:this指针的const修饰

    成员函数的底层都隐含this指针参数,const成员函数的本质是对this指针的双重限制:

    • 非const成员函数的thisA* const this (指针本身不可改,但可修改指向的对象成员,如this->x = 1);
    • const成员函数的thisconst A* const this (指针本身不可改,且指向的对象成员也不可改,禁止this->x = 1)。
  • 核心调用规则(高频考点)

    class A {
        int x, y;
    public:
        void f() { x = 1; y = 1; }        // 非const成员函数:可修改成员
        void show() const { cout << x << y; }  // const成员函数:不可修改成员
    };
    
    int main() {
        const A a(0, 0);  // const对象
        a.f();            // 错误:const对象只能调用const成员函数(this不匹配)
        a.show();         // 正确:const对象匹配const成员函数的this
        A b(0, 0);        // 非const对象
        b.f();            // 正确:非const对象可调用所有成员函数
        b.show();         // 正确:非const对象也可调用const成员函数
    }
    
    

    总结规则:

    • const对象 → 仅能调用const成员函数;
    • 非const对象 → 可调用const/非const成员函数;
    • const成员函数内 → 不可修改普通成员变量(受const A* this限制)。

mutable修饰符:突破const的“君子协定”

若需在const成员函数中修改某个成员(且该修改不影响对象对外行为,如内部缓存),可使用mutable修饰该成员——这是C++的“君子协定”:允许修改内部状态,但对外仍保持“只读”形象。

示例(内部缓存场景):

struct Fib {
    int n_;
    mutable bool cached = false;  // mutable:可在const函数中修改
    mutable int cache = 0;        // mutable:可在const函数中修改

    int value() const {  // const成员函数:对外承诺“不修改对象”
        if (!cached) {
            cache = fib(n_);  // 允许修改mutable成员(内部缓存)
            cached = true;    // 允许修改mutable成员(缓存标记)
        }
        return cache;
    }
};

核心用途:适用于内部缓存、计数等“外部不可见”的成员,既保持const函数的只读承诺,又支持必要的内部状态更新。

新的Const

这里主要讲的是constexpr和constval,这两个修饰符会把运行期的计算提前到编译期,提升效率、更早暴露错误。

constexpr:可在编译期 / 运行期求值

  • 规则constexpr修饰的变量 / 函数,若参数是常量表达式,则在编译期求值;否则在运行期求值

  • 适用场景:数组大小、模板参数、switchcase标签等 “必须是编译期常量” 的场景。

  • 示例(对应 PPT Example1/2):

    // Example1:constexpr运算符,编译期计算BAD|EOF
    constexpr int operator|(Flags f1, Flags f2) { return Flags(int(f1)|int(f2)); }
    
    // Example2:constexpr对象/数组,编译期初始化
    constexpr Point origo(0,0);
    constexpr Point a[] = {Point(0,0), Point(1,1)}; // 编译期创建数组
    

consteval(C++20):必须在编译期求值

  • 规则:只能修饰函数,调用时参数必须是常量表达式,否则编译报错(强制编译期计算);
  • 区别于constexprconstexpr是 “可选编译期”,consteval是 “强制编译期”。

示例(对应 PPT Example3):

consteval int pow2(int n) { return 1 << n; }

constexpr int M = pow2(8); // 正确:8是常量表达式,编译期计算
// int r = pow2(y);        // 会报错误:y是变量,不是常量表达式

静态成员

静态成员:类级别的数据共享方案

C语言中无“类共享数据”的原生支持,C++的静态成员(静态成员变量+静态成员函数)核心目标,就是解决“多个对象如何安全共享同一数据”的问题——既避免普通成员变量“每个对象独有一份”的隔离性,又规避全局变量“数据无保护、命名污染”的缺陷,让共享数据归属于“类本身”,同时遵循类的访问控制规则

静态成员变量:所有对象共享的“类级变量”

  • 核心规则:声明与初始化

    • 基础语法:类内声明,类外定义初始化(必须遵循,否则会报链接错误): 注意:不可在头文件中进行类外定义,否则多次包含头文件会导致变量重复定义。

      class A {
      private:
          static int shared_val; // 类内声明(需加static关键字)
      };
      // 类外定义+初始化:必须写类名::限定,且只能放在.cpp文件中(避免重复定义)
      int A::shared_val = 0;
      
      
  • C++17简化方案:inline static 直接类内初始化,编译器自动处理类外定义逻辑:

    class A {
    private:
        inline static int shared_val = 0; // C++17支持,无需类外额外定义
    };
    
    
  • 关键特点

    • 存储特性:所有对象共享同一份数据(全局唯一拷贝),存储在全局静态区,生命周期贯穿程序运行全程(早于对象创建,晚于对象销毁);
    • 访问控制:遵循类的public/private规则(如private static变量仅类内可访问,外部无法直接修改,比全局变量更安全)。

静态成员函数:操作静态成员的专属函数

  • 规则与限制
    • 语法要求:类内声明时加static,类外定义时不能再加static

      class A {
      public:
          static void show_val(); // 类内声明加static
      };
      // 类外定义:无static关键字,需用类名::限定
      void A::show_val() {
          cout << shared_val << endl; // 仅能访问静态成员变量
      }
      
      
    • 核心限制:this指针(因为属于类而非对象),因此只能访问静态成员变量/静态成员函数不能访问非静态成员(无法定位具体对象的非静态成员)。

  • 调用方式(推荐直接用类名调用)
    • 类名直接调用(更直观,体现“类级操作”):A::show_val();
    • 对象调用(语法允许,但不推荐,掩盖了静态特性):A a; a.show_val();

静态成员的核心价值

  1. 安全的共享数据:比全局变量多了访问控制,避免数据被随意修改;
  2. 无需创建对象即可调用:静态成员属于类,程序启动后无需实例化对象就能使用(适合工具类功能、单例模式等场景);
  3. 对象状态统计:轻松实现“类的对象总数”等跨对象统计功能。

典型应用场景示例

  • 统计类的对象数量

    class Counter {
    private:
        static int obj_count; // 静态变量:统计对象总数
    public:
        Counter() { obj_count++; }  // 构造时计数+1
        ~Counter() { obj_count--; } // 析构时计数-1
        static int get_obj_num() { // 静态函数:返回当前对象数
            return obj_count;
        }
    };
    int Counter::obj_count = 0; // 类外初始化
    
    // 使用:
    Counter c1, c2;
    cout << Counter::get_obj_num(); // 输出2(当前2个对象)
    {
        Counter c3;
        cout << Counter::get_obj_num(); // 输出3(局部对象c3创建)
    }
    cout << Counter::get_obj_num(); // 输出2(c3析构,计数减少)
    
    
  • 单例模式(全局唯一实例)

    class Singleton {
    private:
        static Singleton* instance; // 静态指针:存储唯一实例
        Singleton() {} // 私有构造:禁止外部创建对象
        Singleton(const Singleton&) = delete; // 禁用拷贝构造,防止多实例
    
    public:
        // 静态函数:获取唯一实例(不存在则创建)
        static Singleton* get_instance() {
            if (instance == nullptr) {
                instance = new Singleton;
            }
            return instance;
        }
        static void destroy() { // 静态函数:销毁实例
            delete instance;
            instance = nullptr;
        }
    };
    Singleton* Singleton::instance = nullptr; // 类外初始化
    
    // 使用:全局仅能通过get_instance()获取一个实例
    Singleton* p1 = Singleton::get_instance();
    Singleton* p2 = Singleton::get_instance();
    cout << (p1 == p2); // 输出true(同一实例)
    Singleton::destroy();
    
    

核心总结

静态成员的本质是“类级别的成员”,而非“对象级别的成员”:

  • 静态成员变量:类所有对象共享,类外初始化(C++17用inline static简化);
  • 静态成员函数:无this指针,仅操作静态成员,可通过类名直接调用;
  • 核心价值:安全共享数据、无需实例化调用,典型应用是对象统计、单例模式等。

友元

就像上面我们需要在const成员函数中改变其中的成员变量会用到mutable修饰符,在这里我们也会需要在外部用到内部的private变量的情况 友元机制:封装与访问灵活性的折中方案

友元的三类形式(类内用friend声明)

友元的核心是“精准授权”,根据授权范围分为三类,均需在被访问的类内部用friend关键字声明:

  • 友元函数:授权全局函数访问私有成员

    当某个全局函数需要频繁访问多个类的private成员时,可将其声明为这些类的友元函数,直接操作私有数据,无需通过public接口。

    示例(矩阵与向量相乘):

    class Matrix {
    private:
        int* p_data; // 私有成员:存储矩阵数据
    public:
        // 声明multiply为友元函数,允许其访问private成员
        friend void multiply(Matrix& m, Vector& v, Vector& res);
    };
    
    class Vector {
    private:
        int* p_data; // 私有成员:存储向量数据
    public:
        // 同样声明multiply为友元函数
        friend void multiply(Matrix& m, Vector& v, Vector& res);
    };
    
    // 全局函数:可直接访问Matrix和Vector的private成员p_data
    void multiply(Matrix& m, Vector& v, Vector& res) {
        // 直接操作私有数据,避免多次接口调用的冗余
        for (int i = 0; i < 3; i++) {
            res.p_data[i] = m.p_data[i*3] * v.p_data[0] +
                           m.p_data[i*3+1] * v.p_data[1] +
                           m.p_data[i*3+2] * v.p_data[2];
        }
    }
    
    
  • 友元类:授权整个类的所有成员函数访问

    若类B需要频繁访问类A的private成员,可将类B声明为类A的友元类——此时类B的所有成员函数都能直接访问类A的私有成员,授权范围比友元函数更广。

    示例:

    class A {
    private:
        int x; // 私有成员
    public:
        friend class B; // 声明B为友元类,授权B的所有成员函数
    };
    
    class B {
    public:
        void setA(A& a, int val) {
            a.x = val; // B的成员函数可直接修改A的private成员x
        }
        int getA(A& a) {
            return a.x; // 直接访问A的private成员x
        }
    };
    
    
  • 友元类成员函数:精准授权单个成员函数

    比友元类更精细的授权方式:仅允许其他类的某个特定成员函数访问当前类的private成员,避免过度开放权限。核心注意:需先做前置声明(告知编译器目标类存在),否则会因“未定义类”报错。

    示例(含前置声明):

    // 前置声明:告诉编译器“类C存在”(不完整声明,仅用于友元声明)
    class C;
    
    class A {
    private:
        int x; // 私有成员
    public:
        // 仅声明C的成员函数f()为友元,其他函数无访问权限
        friend void C::f(A& a);
    };
    
    // 定义类C,此时编译器已知晓C的结构
    class C {
    public:
        void f(A& a) {
            a.x = 10; // C::f()可直接访问A的private成员x
        }
        void g(A& a) {
            // a.x = 20; // 错误:仅f()是友元,g()无访问权限
        }
    };
    
    

友元的关键注意点(高频易错点)

  1. 前置声明不可少:若友元涉及未定义的类/函数,必须先做前置声明(如class C;),否则编译器无法识别目标实体,会报“未声明标识符”错误——尤其适用于类之间互相引用、友元成员函数的场景。
  2. 友元不具有传递性:若A是B的友元,B是C的友元,A并不自动成为C的友元。友元是“单向、精准授权”,不传递、不继承,避免滥用导致封装失效。
  3. 友元是“单向授权”:类A声明类B为友元,仅表示B能访问A的私有成员,A不能直接访问B的私有成员——授权是单向的,需双向授权需各自声明。

友元的使用原则:不滥用,坚守封装

友元是“必要时才使用的工具”,而非常规方案:

  • 日常开发优先通过get/setpublic接口访问成员,保持类的接口“最小化且完备”,避免直接暴露私有数据;
  • 仅在“接口访问效率极低”“需跨类协同操作私有数据”时使用友元(如数学运算、工具类协作),遵循迪米特法则(最少知识原则),不滥用友元破坏封装的安全性。

核心总结

友元的核心价值是“精准授权访问”,解决封装与灵活性的矛盾:

  • 三类形式:友元函数(全局函数)、友元类(整个类)、友元类成员函数(单个函数),均通过friend声明;
  • 关键规则:需前置声明、无传递性、单向授权;
  • 使用原则:坚守封装,仅在必要时使用,避免过度开放权限。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值