C++、STL面试题总结(二)

1. 必须实现拷贝构造函数的场景

核心问题:默认拷贝构造的缺陷

C++ 默认的拷贝构造函数(浅拷贝),会直接拷贝指针 / 引用成员的地址。若类包含引用成员指向堆内存的指针,浅拷贝会导致 "多个对象共享同一份资源",引发 double free、数据混乱等问题。此时必须手动实现深拷贝的拷贝构造函数。

场景 1:类包含引用成员(int&

引用成员必须绑定有效对象,默认拷贝构造会让新对象的引用绑定到原对象的引用成员(共享同一块内存)。若原对象的引用成员失效(如局部变量销毁),新对象的引用会变成 "野引用"。

示例:默认拷贝构造的危险

cpp 复制代码
class Test {
public:
    int& ref;
    // 构造函数:绑定引用到外部变量
    Test(int& r) : ref(r) {} 
    // 默认拷贝构造(编译器生成,危险!)
    // Test(const Test& other) : ref(other.ref) {} 
};

int main() {
    int x = 10;
    Test t1(x); 
    // 用默认拷贝构造,t2.ref 也绑定到 x
    Test t2 = t1; 

    x = 20;
    // t1.ref 和 t2.ref 都变成20(共享x)
    cout << t1.ref << " " << t2.ref << endl; 

    // 若 x 是局部变量,t2 的引用可能失效
    // int y = 30;
    // Test t3(y);
    // Test t4 = t3;
    // y 销毁后,t4.ref 变成野引用
    return 0;
}

解决方案:手动实现拷贝构造

需让新对象的引用绑定到新的有效变量(或确保原引用的生命周期足够长)。若引用需绑定到独立变量,需重新构造引用关系(如示例中让新对象的引用绑定到原引用的值,而非原引用本身)。

cpp 复制代码
class Test {
public:
    int& ref;
    Test(int& r) : ref(r) {} 

    // 手动实现拷贝构造:让新对象的引用绑定到原引用的值
    Test(const Test& other) 
        // 用原引用的值,构造新的 int 变量,再绑定引用
        : ref(*new int(other.ref)) {} 

    // 注意:需手动管理内存,否则内存泄漏
    ~Test() { 
        // 释放 new 出来的 int(否则内存泄漏)
        delete &ref; 
    }
};
场景 2:类包含指针成员(指向堆内存)

默认拷贝构造会拷贝指针地址(浅拷贝),导致多个对象共享堆内存。若一个对象释放内存,其他对象的指针会变成 "野指针"。

示例:浅拷贝导致 double free

cpp 复制代码
class Test {
public:
    int* ptr;
    Test(int val) : ptr(new int(val)) {} 
    // 默认拷贝构造(浅拷贝,危险!)
    // Test(const Test& other) : ptr(other.ptr) {} 
    ~Test() { delete ptr; }
};

int main() {
    Test t1(10);
    // 浅拷贝,t2.ptr 和 t1.ptr 指向同一块内存
    Test t2 = t1; 

    // t1 析构时释放 ptr,t2.ptr 变成野指针
    // t2 析构时再次释放,引发 double free
    return 0;
}

解决方案:深拷贝的拷贝构造

cpp 复制代码
class Test {
public:
    int* ptr;
    Test(int val) : ptr(new int(val)) {} 

    // 深拷贝:新对象重新分配堆内存
    Test(const Test& other) 
        : ptr(new int(*other.ptr)) {} 

    ~Test() { delete ptr; }
};
总结:必须实现拷贝构造的时机

当类包含 ** 引用成员(需重新绑定)指针成员(需深拷贝)** 时,默认浅拷贝会引发未定义行为,必须手动实现拷贝构造函数,确保:

  • 引用成员绑定到新的有效对象(或值)。
  • 指针成员重新分配堆内存(深拷贝)。

2. 对 explicit 关键字的理解

核心作用:禁止单参数构造函数的隐式类型转换

C++ 中,单参数构造函数(或多参数但其余参数有默认值的构造函数)会被编译器用作隐式类型转换 :用一个类型的值(如int)直接构造另一个类型的对象(如MyInt)。explicit 关键字禁止这种隐式转换,强制要求显式构造对象。

场景 1:单参数构造函数的隐式转换(危险!)
cpp 复制代码
class MyInt {
public:
    int value;
    // 单参数构造函数:int → MyInt
    MyInt(int num) : value(num) {} 
};

void print(MyInt mi) {
    cout << mi.value << endl;
}

int main() {
    // 隐式转换:10 → MyInt(10)
    print(10); 
    return 0;
}

这种隐式转换可能让代码逻辑晦涩(读者难以发现类型转换),甚至引发错误(如意外触发构造函数的副作用)。

场景 2:explicit 禁止隐式转换

给构造函数加 explicit 后,编译器不再允许隐式转换,必须显式构造对象。

cpp 复制代码
class MyInt {
public:
    int value;
    // 禁止隐式转换
    explicit MyInt(int num) : value(num) {} 
};

void print(MyInt mi) {
    cout << mi.value << endl;
}

int main() {
    // 编译报错:无法隐式转换 int→MyInt
    // print(10); 

    // 必须显式构造
    print(MyInt(10)); 
    return 0;
}
扩展:C++11 后支持多参数的 explicit(配合列表初始化)

C++11 允许 explicit 修饰多参数构造函数(需配合{}列表初始化),禁止用括号初始化的隐式转换。

cpp 复制代码
class Point {
public:
    int x, y;
    // 多参数构造函数,explicit 禁止隐式转换
    explicit Point(int a, int b) : x(a), y(b) {} 
};

void draw(Point p) { /*...*/ }

int main() {
    // 编译报错:无法隐式转换 (1,2)→Point
    // draw((1,2)); 

    // 必须显式构造
    draw(Point(1,2)); 
    // 或用列表初始化(C++11)
    draw({1,2}); 
    return 0;
}
最佳实践
  • 所有单参数构造函数(或可能被隐式转换的构造函数)都应加 explicit,避免意外的隐式转换。
  • 仅在需要隐式转换的场景(如智能指针std::shared_ptr的构造),才不用 explicit

3. 无法被继承的函数

核心规则:构造函数、析构函数、赋值运算符、final 修饰的函数无法被继承
函数类型 无法继承的原因
构造函数 构造函数与类的初始化逻辑强绑定,子类需独立构造
析构函数 析构函数与类的资源释放逻辑强绑定,子类需独立析构
赋值运算符(operator= 赋值需处理子类新增成员,继承父类的赋值逻辑会遗漏子类成员
final 修饰的虚函数 final 显式禁止子类重写该虚函数
详细解析
(1)构造函数与析构函数
  • 构造函数:子类对象包含父类成员和子类成员,父类构造函数负责初始化父类成员,子类构造函数需调用父类构造函数(通过初始化列表),无法直接继承父类构造逻辑(否则无法初始化子类新增成员)。
  • 析构函数:子类析构函数需先释放子类资源,再调用父类析构函数。若继承父类析构函数,无法保证子类资源优先释放,可能导致内存泄漏。
(2)赋值运算符(operator=

父类的 operator= 仅处理父类成员,子类的 operator= 需处理子类新增成员。若继承父类的 operator=,会导致子类成员未被赋值(遗漏),引发数据不完整。

示例:继承赋值运算符的危险

cpp 复制代码
class Father {
public:
    int x;
    Father& operator=(const Father& other) {
        x = other.x;
        return *this;
    }
};

class Son : public Father {
public:
    int y;
    // 若继承父类 operator=,会遗漏 y 的赋值
    Son& operator=(const Son& other) {
        // 必须手动调用父类的 operator=
        Father::operator=(other); 
        y = other.y;
        return *this;
    }
};
(3)被 final 修饰的虚函数

final 是 C++11 引入的关键字,用于禁止虚函数被重写。若父类虚函数被 final 修饰,子类无法继承(重写)该函数,否则编译报错。

cpp 复制代码
class Father {
public:
    // 禁止子类重写
    virtual void func() final { /*...*/ } 
};

class Son : public Father {
public:
    // 编译报错:无法重写 final 函数
    // void func() override { /*...*/ } 
};

4. 继承中同名成员的处理

核心规则:子类同名成员会隐藏 父类同名成员(而非覆盖),需用作用域运算符 :: 显式访问父类成员。
场景 1:子类定义与父类同名的成员变量

父类的同名成员变量仍被继承到子类,但子类的同名成员会 "隐藏" 父类成员(默认访问子类成员)。

cpp 复制代码
class Father {
public:
    int money = 100; 
};

class Son : public Father {
public:
    // 子类同名成员,隐藏父类的 money
    int money = 50; 
};

int main() {
    Son son;
    // 访问子类的 money(输出50)
    cout << son.money << endl; 
    // 显式访问父类的 money(输出100)
    cout << son.Father::money << endl; 
    return 0;
}
场景 2:子类定义与父类同名的成员函数

若子类函数与父类函数同名但参数不同 (非虚函数),会隐藏父类的所有同名函数(无论参数是否相同)。若子类函数与父类函数同名且参数相同(虚函数),则重写父类函数(多态)。

示例:同名非虚函数的隐藏

cpp 复制代码
class Father {
public:
    void func(int x) { 
        cout << "Father: " << x << endl; 
    }
};

class Son : public Father {
public:
    // 同名但参数不同,隐藏父类的 func(int)
    void func(double x) { 
        cout << "Son: " << x << endl; 
    }
};

int main() {
    Son son;
    // 调用子类的 func(double)
    son.func(3.14); 
    // 编译报错:父类的 func(int) 被隐藏
    // son.func(10); 

    // 显式访问父类的 func(int)
    son.Father::func(10); 
    return 0;
}
最佳实践
  • 避免同名成员:设计类时,尽量让子类成员与父类成员名称不同,减少混淆。
  • 显式访问父类成员 :若必须同名,用 Father::moneyFather::func() 明确访问父类成员,增强代码可读性。

5. 继承中构造与析构的顺序

构造顺序:父类 → 成员对象 → 子类

对象构造时,需先确保父类和成员对象的构造完成(为子类提供初始化的基础),因此顺序为:

  1. 调用父类的构造函数(按继承顺序,从最顶层父类到直接父类)。
  2. 调用成员对象的构造函数(按成员在子类中的声明顺序)。
  3. 调用子类的构造函数体
示例:多层继承 + 成员对象的构造顺序
cpp 复制代码
class GrandFather {
public:
    GrandFather() { cout << "GrandFather 构造" << endl; }
};

class Father : public GrandFather {
public:
    Father() { cout << "Father 构造" << endl; }
};

class Member {
public:
    Member() { cout << "Member 构造" << endl; }
};

class Son : public Father {
public:
    Member m;
    Son() { cout << "Son 构造" << endl; }
};

int main() {
    Son son;
    // 输出顺序:
    // GrandFather 构造 → Father 构造 → Member 构造 → Son 构造
    return 0;
}
析构顺序:子类 → 成员对象 → 父类

对象析构时,需先释放子类资源,再释放成员对象和父类资源(避免父类资源先释放,子类资源访问无效内存),因此顺序与构造相反:

  1. 调用子类的析构函数体
  2. 调用成员对象的析构函数(按构造顺序的逆序)。
  3. 调用父类的析构函数(按继承顺序的逆序,从直接父类到最顶层父类)。
示例:析构顺序验证
cpp 复制代码
class GrandFather {
public:
    ~GrandFather() { cout << "GrandFather 析构" << endl; }
};

class Father : public GrandFather {
public:
    ~Father() { cout << "Father 析构" << endl; }
};

class Member {
public:
    ~Member() { cout << "Member 析构" << endl; }
};

class Son : public Father {
public:
    Member m;
    ~Son() { cout << "Son 析构" << endl; }
};

int main() {
    Son son;
    // 构造顺序:GrandFather → Father → Member → Son
    // 析构顺序:Son → Member → Father → GrandFather
    return 0;
}

6. 继承中的构造与析构顺序

继承体系中,构造函数和析构函数的调用顺序严格遵循 "先构造父类,再构造子类;先析构子类,再析构父类" 的规则,这是由 C++ 对象的内存布局和生命周期决定的。

一、构造函数的调用顺序
1. 子类对象创建时,先调用父类构造函数

原理

子类继承了父类的成员变量,这些成员的初始化依赖父类的构造逻辑。如果先构造子类,父类成员可能因未初始化而出现访问非法内存、数据未定义等问题。因此,C++ 强制规定:创建子类对象时,必须先确保父类构造完成,为继承的成员打好初始化基础。

示例验证

cpp 复制代码
class Father {
public:
    Father() {
        cout << "Father 构造函数调用" << endl;
    }
};

class Son : public Father {
public:
    Son() {
        cout << "Son 构造函数调用" << endl;
    }
};

int main() {
    Son son; 
    // 输出顺序:
    // Father 构造函数调用 → Son 构造函数调用
    return 0;
}

内存视角

子类对象的内存布局中,父类成员在前、子类成员在后。构造时必须先初始化父类部分(确保父类成员有效),再初始化子类独有的成员。

2. 父类有参数时,子类必须用初始化列表显式调用

场景

如果父类没有默认构造函数 (即构造函数需要传入参数,无法无参调用),子类构造函数必须在初始化列表显式调用父类的有参构造函数,否则编译器报错(因为父类无法自动用默认构造初始化)。

示例验证

cpp 复制代码
class Father {
public:
    int money;
    // 父类没有默认构造,必须传参初始化
    Father(int m) : money(m) {
        cout << "Father 带参构造:" << money << endl;
    }
};

class Son : public Father {
public:
    int toyNum;
    // 子类构造函数初始化列表,显式调用父类带参构造
    Son(int m, int toy) 
        // 调用父类有参构造,初始化父类成员 money
        : Father(m),  
          // 初始化子类成员 toyNum
          toyNum(toy)  
    {
        cout << "Son 带参构造:" << toyNum << endl;
    }
};

int main() {
    Son son(100, 5); 
    // 输出顺序:
    // Father 带参构造:100 → Son 带参构造:5
    return 0;
}

关键细节

  • 初始化列表的调用顺序不受书写顺序影响 ,而是严格按照成员变量在类中声明的顺序执行。
  • 若父类无默认构造,子类构造函数的初始化列表必须显式调用父类有参构造,否则编译报错(编译器无法自动推断父类的构造方式)。
二、析构函数的调用顺序

原理

对象销毁时,要保证 "先构造的后销毁,后构造的先销毁" 。子类对象依赖父类的资源(如父类成员变量、父类申请的堆内存),若父类先销毁,子类在析构时可能访问已销毁的父类成员,引发未定义行为。因此,析构顺序严格与构造顺序相反:先调子类析构函数,再调父类析构函数

示例验证

cpp 复制代码
class Father {
public:
    ~Father() {
        cout << "Father 析构函数调用" << endl;
    }
};

class Son : public Father {
public:
    ~Son() {
        cout << "Son 析构函数调用" << endl;
    }
};

int main() {
    Son son; 
    // 构造顺序:Father → Son
    // 析构顺序:Son → Father
    // 输出:
    // Son 析构函数调用 → Father 析构函数调用
    return 0;
}

内存视角

子类析构时,需先释放子类独有的资源(避免父类析构后,子类资源释放逻辑依赖父类成员但父类已销毁),再通知父类释放其资源。

7. 子类调用成员对象、父类有参构造的注意点

子类构造函数不仅要处理父类的构造,还要处理 ** 成员对象(子类中包含的其他类对象)** 的构造。成员对象的构造顺序与父类构造紧密相关,需特别注意默认构造和有参构造的调用规则。

一、默认构造的自动调用

规则

当父类和成员对象存在默认构造函数 (无参构造函数,或所有参数都有默认值的构造函数)时,子类构造时会自动调用它们的默认构造,无需手动在初始化列表中处理。

示例验证

cpp 复制代码
class Father {
public:
    // 父类默认构造(无参)
    Father() { 
        cout << "Father 默认构造" << endl; 
    }
};

class Member {
public:
    // 成员对象默认构造(无参)
    Member() { 
        cout << "Member 默认构造" << endl; 
    }
};

class Son : public Father {
private:
    // 子类包含的成员对象
    Member m; 
public:
    Son() { 
        cout << "Son 构造" << endl; 
    }
};

int main() {
    Son son; 
    // 输出顺序:
    // Father 默认构造 → Member 默认构造 → Son 构造
    return 0;
}

原理

编译器会自动在子类构造函数的初始化列表中,插入对父类默认构造和成员对象默认构造的调用,确保所有基类和成员对象都被正确初始化。

二、有参构造的强制调用(初始化列表)

规则

若父类或成员对象没有默认构造函数 (只有带参数的构造函数),子类构造函数必须在初始化列表显式调用它们的有参构造函数,否则编译器报错(因为无法默认初始化)。

调用规则细节

  • 父类 :用 父类名(参数) 调用,指定父类构造函数及参数。
  • 成员对象 :用 成员对象名(参数) 调用,指定成员对象构造函数及参数。

示例验证(父类、成员对象均为有参构造)

cpp 复制代码
class Father {
public:
    int money;
    // 父类有参构造(无默认构造)
    Father(int m) : money(m) { 
        cout << "Father 有参构造:" << money << endl; 
    }
};

class Member {
public:
    int toy;
    // 成员对象有参构造(无默认构造)
    Member(int t) : toy(t) { 
        cout << "Member 有参构造:" << toy << endl; 
    }
};

class Son : public Father {
private:
    // 子类包含的成员对象
    Member m; 
public:
    // 初始化列表:调用父类、成员对象的有参构造
    Son(int m_money, int m_toy) 
        // 调用父类有参构造,初始化父类成员 money
        : Father(m_money),  
          // 调用成员对象有参构造,初始化成员对象 m 的 toy
          m(m_toy)          
    { 
        cout << "Son 构造" << endl; 
    }
};

int main() {
    Son son(100, 5); 
    // 输出顺序:
    // Father 有参构造:100 → Member 有参构造:5 → Son 构造
    return 0;
}

避坑点

初始化列表的调用顺序由成员变量在类中的声明顺序决定,而非初始化列表的书写顺序。若顺序错误,可能导致未定义行为(如用未初始化的成员给其他成员赋值)。

8. 虚继承的原理

虚继承是 C++ 为解决多继承中的菱形继承问题 (多个子类继承同一父类,导致父类成员在子类中重复存储)而设计的机制。其核心是通过虚基类指针和虚基类表,让多个子类共享同一父类的成员,避免数据冗余和访问冲突。

一、菱形继承问题(不使用虚继承)

场景

A 是类 BC 的父类,类 D 同时继承 BC。此时,D 中会包含两份 A 的成员(一份来自 B,一份来自 C),导致数据冗余和二义性。

示例(问题代码)

cpp 复制代码
class A {
public:
    int data;
    A(int d) : data(d) {}
};

class B : public A {
public:
    B(int d) : A(d) {}
};

class C : public A {
public:
    C(int d) : A(d) {}
};

class D : public B, public C {
public:
    D(int d1, int d2) : B(d1), C(d2) {}
};

int main() {
    D d(10, 20);
    // 编译报错:data 二义性(B::data 和 C::data 冲突)
    // cout << d.data << endl; 
    return 0;
}

问题
D 中存储了两份 AdataB::dataC::data),访问 d.data 时编译器无法确定访问哪一份,引发二义性错误。

二、虚继承的解决方案

原理

虚继承通过在子类中添加虚基类指针(vbptr) ,指向虚基类表(vbtable) 。虚基类表中记录了从该指针到公共祖先(如 A)成员的偏移量,确保多个子类(如 BC)共享同一 A 的成员,避免数据冗余。

使用虚继承的代码

cpp 复制代码
class A {
public:
    int data;
    A(int d) : data(d) {}
};

// 虚继承 A
class B : virtual public A { 
public:
    B(int d) : A(d) {}
};

// 虚继承 A
class C : virtual public A { 
public:
    C(int d) : A(d) {}
};

class D : public B, public C {
public:
    // 必须显式调用 A 的构造(虚继承后,B 和 C 无法直接初始化 A)
    D(int d) : A(d), B(d), C(d) {} 
};

int main() {
    D d(10);
    // 输出10(B 和 C 共享 A::data)
    cout << d.data << endl; 
    return 0;
}

内存布局变化

  • BC 中各有一个虚基类指针(vbptr),指向虚基类表。
  • 虚基类表中存储了 B/CA 成员的偏移量,确保 B::dataC::data 实际指向同一 A::data
三、虚继承的优缺点

优点

  • 解决菱形继承的二义性和数据冗余问题,让公共基类成员只存储一份。

缺点

  • 增加内存开销(每个虚继承的子类需存储虚基类指针和虚基类表)。
  • 构造函数复杂(虚继承后,最终子类需显式调用公共基类的构造函数,无法依赖中间子类)。

最佳实践

虚继承是 "无奈之举",实际开发中应尽量避免多继承(用组合、接口继承替代),因为多继承的复杂性(如虚继承的理解成本、构造顺序问题)远大于其带来的便利。

9. 纯虚函数与抽象类

纯虚函数是 C++ 实现抽象类接口的核心机制。它强制子类必须实现某些函数,确保继承体系的行为统一。

一、普通纯虚函数的定义与特性

定义

纯虚函数是在基类中声明的虚函数,格式为 virtual 返回值类型 函数名(参数列表) = 0;。纯虚函数不需要(也不能强制要求)在基类中实现函数体 ,但子类必须以 override 关键字重写该函数(否则子类也会成为抽象类,无法实例化)。

示例

cpp 复制代码
class Base {
public:
    // 纯虚函数:基类只声明,不实现
    virtual void func() = 0; 
};

class Derived : public Base {
public:
    // 子类必须重写纯虚函数
    void func() override { 
        cout << "Derived::func 调用" << endl; 
    }
};

int main() {
    // 错误:抽象类不能实例化
    // Base b; 
    Derived d;
    d.func(); // 输出:Derived::func 调用
    return 0;
}
二、纯虚函数的特殊用法(基类实现函数体)

规则

纯虚函数可以在基类中实现函数体 ,但这不是强制的。子类可以选择调用基类的实现 (通过 Base::func()),但即便基类实现了函数体,子类仍需显式重写纯虚函数(否则子类是抽象类)。

示例验证

cpp 复制代码
class Base {
public:
    // 纯虚函数声明
    virtual void func() = 0; 
};

// 基类中实现纯虚函数的函数体(非强制,仅为示例)
void Base::func() { 
    cout << "Base::func 调用" << endl; 
}

class Derived : public Base {
public:
    // 子类必须重写纯虚函数
    void func() override { 
        // 调用基类的实现(可选)
        Base::func(); 
        cout << "Derived::func 调用" << endl; 
    }
};

int main() {
    Derived d;
    d.func(); 
    // 输出:
    // Base::func 调用 → Derived::func 调用
    return 0;
}

注意

基类纯虚函数的实现体通常用于提供 "默认逻辑",子类可选择复用或完全重写,但子类必须显式重写(否则无法实例化)。

三、纯虚函数对析构函数的影响

纯虚析构函数是特殊场景:基类若声明纯虚析构函数,必须提供函数体(否则链接报错,因为析构函数需要释放资源)。子类析构函数会自动调用基类析构函数,确保资源正确释放。

示例(纯虚析构函数)

cpp 复制代码
class Base {
public:
    // 纯虚析构函数,必须实现函数体
    virtual ~Base() = 0; 
};

// 基类纯虚析构函数的实现体
Base::~Base() { 
    cout << "Base 析构函数调用" << endl; 
}

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived 析构函数调用" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    // 销毁时:先 Derived 析构,再 Base 析构
    delete ptr; 
    // 输出:
    // Derived 析构函数调用 → Base 析构函数调用
    return 0;
}

关键

纯虚析构函数的函数体必须定义(否则链接阶段报错),子类析构函数会隐式调用基类析构函数,保证继承体系的资源释放顺序。

10. 多态成立的条件

多态是 C++ 面向对象设计的核心特性,允许程序根据对象的实际类型(而非声明类型)调用对应的函数。其成立需满足以下三个条件:

https://github.com/0voice

相关推荐
weixin_307779131 小时前
Redis Windows迁移方案与测试
c++·windows·redis·算法·系统架构
Absinthe_苦艾酒3 小时前
JVM学习专题(四)对象创建过程
java·jvm·后端
zm3 小时前
bool 类型转换运算符重载
c++
小指纹3 小时前
cf--思维训练
c++·算法·macos·ios·objective-c·cocoa
小指纹3 小时前
河南萌新联赛2025第(四)场【补题】
数据结构·c++·算法·macos·objective-c·cocoa·图论
菜鸟555553 小时前
河南萌新联赛2025第四场-河南大学
c++·算法·思维·河南萌新联赛
ohoy3 小时前
jvm之jconsole的使用
jvm
小坏坏的大世界3 小时前
C++中多线程和互斥锁的基本使用
开发语言·c++
2301_793086873 小时前
JVM 03 类加载机制
jvm