跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 面向对象编程核心:继承机制深度解析

C++ 继承机制允许派生类复用基类成员,提升开发效率。本文详解继承定义格式、三种访问限定符规则及对象赋值转换中的切片现象。重点剖析作用域隐藏、默认成员函数调用顺序(构造自顶向下、析构自底向上)以及菱形继承引发的数据冗余和二义性问题,并通过虚拟继承给出解决方案。最后对比继承与组合的设计差异,明确 is-a 与 has-a 关系的应用场景,为 C++ 面向对象设计提供实践指导。

292440837发布于 2026/3/27更新于 2026/6/919 浏览
C++ 面向对象编程核心:继承机制深度解析

在 C++ 中,继承 (inheritance) 允许程序员在保持原有类(基类/父类)特性的基础上,扩展功能生成新的类(派生类/子类)。这是区别于函数复用的类设计层次复用,完美契合了从简单到复杂的认知逻辑。

简单来说,继承让派生类天然拥有基类的所有成员(成员变量 + 成员函数),无需重复编写代码,极大提升了开发效率和代码可维护性。比如定义表示'人'的 Person 类,再通过继承派生出 Student 和 Teacher 类,二者可直接复用 Person 的姓名、年龄等属性和打印方法,只需新增各自的特有属性(学号、工号)即可。

#include <iostream>
#include <string>
using namespace std;

// 基类/父类:Person
class Person {
public:
    void Print() {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter"; // 姓名
    int _age = 18;          // 年龄
};

// 派生类/子类:Student 公有继承 Person
class Student : public Person {
protected:
    int _stuid; // 学号(特有属性)
};

// 派生类/子类:Teacher 公有继承 Person
class Teacher : public Person {
protected:
    int _jobid; // 工号(特有属性)
};

int main() {
    Student s;
    Teacher t;
    s.Print(); // 复用基类 Print 方法
    t.Print(); // 复用基类 Print 方法
    return 0;
}

从代码中能清晰看到,Student 和 Teacher 未定义 Print 方法,却能直接调用,这就是继承带来的代码复用效果。

继承的基本语法与访问控制

要灵活使用继承,首先要掌握其定义格式和核心的访问限定规则,这是避免继承中成员访问错误的基础。

继承的定义格式

派生类的定义遵循固定格式,核心是派生类 + 继承方式 + 基类。其中继承方式和基类的访问限定符共同决定了基类成员在派生类中的访问权限。

class 派生类名 : 继承方式 基类名 {
    // 派生类的成员
};

示例中 class Student : public Person 就是标准格式,public 为继承方式,Person 为基类。

三大继承方式与访问限定符

C++ 提供三种继承方式:public(公有继承)、protected(保护继承)、private(私有继承);类成员的访问限定符同样有这三种,二者组合后,基类成员在派生类中的访问权限遵循严格的规则。

基类成员 / 继承方式public 继承protected 继承private 继承
基类 public 成员派生类 public派生类 protected派生类 private
基类 protected 成员派生类 protected派生类 protected派生类 private
基类 private 成员不可见不可见不可见

基类 private 成员始终不可见:基类的私有成员会被继承到派生类对象中,但语法上限制派生类无论在类内还是类外都无法访问,这是封装性的体现。

protected 的专属价值:若基类成员不想被类外访问,但需要让派生类访问,就定义为 protected——保护成员限定符是因继承而诞生的。

权限取最小值:基类非私有成员在派生类中的访问权限 = Min(成员在基类的访问限定符,继承方式),权限优先级:public > protected > private。

默认继承方式:使用 class 定义类时,默认继承方式为 private;使用 struct 时,默认继承方式为 public,建议显式写出继承方式,提升代码可读性。

实战首选公有继承:实际开发中几乎只使用 public 继承,protected/private 继承会让派生类的成员仅能在类内使用,扩展和维护性极差,不推荐使用。

基类与派生类的对象赋值转换

继承体系中,基类和派生类的对象、指针、引用之间存在特定的赋值转换规则,核心被称为切片(切割)——将派生类中属于基类的那部分成员'切下来'赋值给基类对象,具体规则如下,这是面试高频考点。

合法的赋值转换

派生类对象可以直接赋值给基类的对象、基类的指针、基类的引用,这是编译器自动完成的隐式转换,本质就是切片。

class Person {
protected:
    string _name;
    string _sex;
    int _age;
};

class Student : public Person {
public:
    int _No; // 学号
};

void Test() {
    Student sobj;
    // 合法:子类对象赋值给父类对象/指针/引用(切片)
    Person pobj = sobj;
    Person* pp = &sobj;
    Person& rp = sobj;
}

对象切片:把派生类对象里属于基类的成员,拷贝赋值给新的基类对象(派生类特有成员被丢弃);

指针 / 引用切片:基类指针(引用)直接指向(绑定)到派生类对象中属于基类的那部分内存,并非新建对象,只是只能访问基类成员。

注意:子类对象赋值给父类对象不会产生临时变量,而是直接进行拷贝构造或赋值操作。

非法的赋值转换

基类对象不能直接赋值给派生类对象,因为基类对象缺少派生类的特有成员,无法完成完整的赋值。

强制类型转换的注意事项

基类的指针 / 引用可以通过强制类型转换赋值给派生类的指针 / 引用,但仅当基类指针 / 引用指向派生类对象时才安全,否则会导致越界访问。

void Test() {
    Student sobj;
    Person pobj;
    Person* pp = &sobj;
    
    // 安全:基类指针指向派生类对象,强制转换后可访问派生类成员
    Student* ps1 = (Student*)pp;
    ps1->_No = 10;
    
    pp = &pobj;
    // 危险:基类指针指向基类对象,强制转换后访问派生类成员会越界
    Student* ps2 = (Student*)pp;
    ps2->_No = 10; // 未定义行为
}

若基类是多态类型,可使用 dynamic_cast 进行安全的类型转换(依赖 RTTI 运行时类型识别),后续讲解多态时会详细说明。

继承中的作用域

继承体系中,基类和派生类拥有相互独立的作用域,这是理解成员隐藏的关键。当子类和父类出现同名成员时,会触发隐藏(重定义) 规则,这是继承中最容易踩坑的点之一。

成员变量的隐藏

子类和父类的同名成员变量,子类成员会屏蔽父类对同名成员的直接访问,若想在子类中访问父类的同名成员,需通过 基类::基类成员 显式指定。

class Person {
protected:
    string _name = "小李子";
    int _num = 111; // 身份证号
};

class Student : public Person {
public:
    void Print() {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl; // 显式访问父类同名成员
        cout << "学号:" << _num << endl;             // 访问子类自身成员
    }
protected:
    int _num = 999; // 学号:与父类_num 同名,触发隐藏
};

void Test() {
    Student s1;
    s1.Print(); // 输出:小李子 111 999
}

成员函数的隐藏

成员函数的隐藏只需函数名相同即可触发,与函数的参数列表、返回值无关,这一点与函数重载(同一作用域、函数名相同 + 参数列表不同)有本质区别。

class A {
public:
    void fun() { cout << "func()" << endl; }
};

class B : public A {
public:
    // 函数名相同,触发隐藏,与参数无关
    void fun(int i) {
        A::fun(); // 显式调用父类同名函数
        cout << "func(int i)->" << i << endl;
    }
};

void Test() {
    B b;
    b.fun(10); // 调用子类的 fun(int)
    // b.fun(); // 编译错误:父类 fun 被隐藏,需显式调用 A::fun()
}

思考:在继承体系中,尽量不要定义同名的成员,无论是成员变量还是成员函数,都会增加代码的混淆度,提升调试难度。

派生类的默认成员函数

C++ 中每个类都有六个默认成员函数(构造、拷贝构造、赋值重载、析构、取地址重载、const 取地址重载),若程序员不写,编译器会自动生成。在继承体系中,派生类的默认成员函数并非完全独立生成,而是需要调用基类的对应成员函数,完成基类部分的初始化和清理,核心规则共 7 条,是继承的核心重点。

核心规则

  1. 基类无默认构造函数时,派生类必须在初始化列表显式调用基类构造函数。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造,完成基类成员的拷贝初始化。
  3. 派生类的 operator= 必须调用基类的 operator=,完成基类成员的赋值。
  4. 派生类的析构函数执行完毕后,编译器会自动调用基类的析构函数,保证先清理派生类成员、再清理基类成员的顺序。
  5. 派生类对象的初始化顺序:先调用基类构造,再调用派生类构造。
  6. 派生类对象的析构顺序:先调用派生类析构,再调用基类析构(与构造顺序相反)。
  7. 析构函数的隐藏:编译器会将所有析构函数名统一处理为 destructor(),因此父类析构函数不加 virtual 时(后续多态会进行讲解),子类析构函数与父类析构函数构成隐藏。

总结一下:基类负责初始化和清理自己的成员,派生类负责初始化和清理自己新增的成员,两者分工明确,互不越界。

代码演示

class Person {
public:
    // 基类构造函数
    Person(const char* name = "peter") : _name(name) {
        cout << "Person()" << endl;
    }
    // 基类拷贝构造
    Person(const Person& p) : _name(p._name) {
        cout << "Person(const Person& p)" << endl;
    }
    // 基类赋值重载
    Person& operator=(const Person& p) {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p) _name = p._name;
        return *this;
    }
    // 基类析构函数
    ~Person() {
        cout << "~Person()" << endl;
    }
protected:
    string _name;
};

class Student : public Person {
public:
    // 派生类构造:初始化列表显式调用基类构造
    Student(const char* name, int num) : Person(name), _num(num) {
        cout << "Student()" << endl;
    }
    // 派生类拷贝构造:初始化列表显式调用基类拷贝构造
    Student(const Student& s) : Person(s), _num(s._num) {
        cout << "Student(const Student& s)" << endl;
    }
    // 派生类赋值重载:显式调用基类赋值重载
    Student& operator=(const Student& s) {
        cout << "Student& operator=(const Student& s)" << endl;
        if (this != &s) {
            Person::operator=(s); // 调用基类赋值重载
            _num = s._num;
        }
        return *this;
    }
    // 派生类析构:编译器自动调用基类析构
    ~Student() {
        cout << "~Student()" << endl;
    }
protected:
    int _num;
};

注意:取地址重载和 const 取地址重载在继承中无特殊规则,编译器自动生成的版本即可满足需求,实战中几乎无需自定义实现。

为什么析构函数的调用顺序是:派生类、基类?

class Parent {
public:
    char* buf;
    Parent() { buf = new char[1]; }
    ~Parent() {
        delete[] buf;
        buf = nullptr;
        cout << "父类析构:buf 已释放" << endl;
    }
};

class Child : public Parent {
public:
    ~Child() {
        // 子类析构使用父类已释放的 buf → 野指针访问
        buf[0] = 'x';
        cout << "子类析构:使用父类 buf(野指针)" << endl;
    }
};

继承体系中,派生类析构可能使用基类动态资源,若先析构基类会释放资源产生野指针导致崩溃;而基类析构不会使用派生类资源,因此必须先析构派生类、再析构基类,确保资源安全释放。

继承的特殊场景:友元与静态成员

继承与友元

基类的友元可以访问基类的私有和保护成员,但无法访问派生类的私有和保护成员,即友元关系不具有传递性。

class Student;
class Person {
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name;
};

class Student : public Person {
protected:
    int _stuNum; // 派生类保护成员
};

// 友元函数可访问基类_name,但无法直接访问派生类_stuNum(编译错误)
void Display(const Person& p, const Student& s) {
    cout << p._name << endl;
    // cout << s._stuNum << endl; // 错误:友元关系不能继承
}

继承与静态成员

基类中定义的 static 静态成员,在整个继承体系中只有一份实例,无论派生出多少个子类,所有类的对象共享这一个静态成员。

class Person {
public:
    Person() { ++_count; }
    static int _count; // 静态成员:统计人数
protected:
    string _name;
};

int Person::_count = 0; // 静态成员类外初始化

class Student : public Person {
protected:
    int _stuNum;
};

class Graduate : public Student {
protected:
    string _seminarCourse;
};

void TestPerson() {
    Student s1, s2, s3;
    Graduate s4;
    cout << "人数:" << Person::_count << endl; // 输出:4(所有对象共享_count)
    Student::_count = 0;
    cout << "人数:" << Person::_count << endl; // 输出:0(修改子类静态成员,基类也会变化)
}

静态成员的访问方式:基类::静态成员 或 派生类::静态成员,本质访问的是同一个实例。

菱形继承及其解决方案

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

菱形继承:菱形继承是多继承的一种特殊情况,例如类 D 同时继承 B 和 C,而 B 和 C 都继承自 A。

菱形继承的问题

菱形继承的底层问题是数据冗余和二义性,即派生类对象中会包含多份基类成员,导致访问基类成员时无法确定具体访问哪一份。

class A {
public:
    int _a;
};
class B : public A {
public:
    int _b;
};
class C : public A {
public:
    int _c;
};
class D : public B, public C {
public:
    int _d;
};

用域作用限定符解决了二义性问题,但是没有解决数据冗余问题。

菱形虚拟继承

虚拟继承是 C++ 专门为解决菱形继承问题设计的特性,在菱形继承的中间层子类(B、C)继承基类(A)时,添加 virtual 关键字,即可让最终的派生类(D)只保留一份基类成员,同时解决二义性和数据冗余。

class A {
public:
    int _a;
};
class B : virtual public A {
public:
    int _b;
};
class C : virtual public A {
public:
    int _c;
};
class D : public B, public C {
public:
    int _d;
};

虚拟继承的底层通过虚基表指针和虚基表实现:

  • 在虚拟继承的中间层子类(如 B、C)对象中,会增加一个虚基表指针,指向对应的虚基表。
  • 虚基表中存储的是虚基类成员在派生类对象中的偏移量,中间层子类通过该偏移量,即可在运行时定位到唯一的基类成员实例。
  • 在最终的派生类(D)对象中,虚基类(A)的成员会被放置在对象内存布局的最底部,由所有中间层子类共享,从而保证整个继承体系中仅存在一份基类实例,避免了数据冗余与二义性。

注意:虚拟继承的设计初衷是解决菱形继承问题,不应在其他场景随意使用,其底层的虚基表与偏移量查找机制会带来一定的性能开销。

常见问题思考

Q: 菱形虚拟继承为啥用虚基表存储偏移量?

当有多个虚基类时,会产生多个偏移量。如果直接将这些偏移量存在对象中,创建大量对象时会重复存储相同数据,造成内存浪费。而将偏移量统一存到类级别的虚基表中,每个对象只需用一个指针指向该表,即可共享所有偏移量,大幅节省内存,同时保证在不同继承场景下都能正确定位虚基类。

Q: 什么时候需要用偏移量访问共享数据?

  1. 直接访问(无需偏移量):当你用最终派生类对象(如 D d)直接访问虚基类成员时,虚基类 A 的位置在编译期就已确定,编译器可以直接计算出 _a 的地址,不需要在运行时通过虚基表动态查找偏移量来计算。
  2. 切片访问(必须用偏移量):当你用基类指针(如 B* pb)指向派生类对象(如 D d),并通过该指针访问虚基类成员时,独立 B 对象编译期就能确定 B→A 的固定偏移量,但 D 中切片的 B 子对象编译期不知道该用哪个偏移量,所以只能在运行时通过虚基表查'当前场景下的正确偏移量'。

Q: 菱形虚拟继承是否解决了数据冗余问题?

虽然虚拟继承会引入虚基表指针(图中每个指针占 4 字节),看起来多了一点内存开销,但当虚基类 A 的成员越多、体积越大时,这份指针的开销就越微不足道,整体来看内存效率反而更高。

Q: 菱形虚继承中构造函数的调用顺序?

对于菱形虚拟继承来说,虚基类 A 会被整个继承体系共享,不再属于 B 和 C 各自私有,因此在构造最底层的 D 对象时,A 只会被构造一次。调用顺序是 A → B → C → D,这是因为:

  1. 虚基类 A 总是最先被构造。
  2. 非虚基类 B 和 C 的顺序,取决于 D 类的继承声明顺序(public B, public C),而非构造函数初始化列表的顺序。
  3. 最后才会执行派生类 D 自身的构造函数。

至于为啥基类先构造,是因为派生类的构造函数可能会使用基类的成员。如果先构造派生类,就会出现使用未初始化基类成员的风险,从而导致程序错误。

继承和组合的设计原则

掌握了继承的所有语法规则后,更重要的是理解继承的设计原则——何时该用继承,何时该用更优的组合?这是体现 C++ 设计思维的关键。

对 C++ 多继承的客观认知

多继承是 C++ 语法复杂的重要体现,菱形继承和菱形虚拟继承的底层实现繁琐,易引发问题,实际开发中应尽量避免设计多继承,坚决避免菱形继承。

多继承被认为是 C++ 的缺陷之一,后续的面向对象语言(如 Java、C#)都取消了多继承,仅保留单继承 + 接口的方式,规避了菱形继承的问题。

继承与组合的区别

类之间的关系主要分为两种,对应两种复用方式:继承(is-a) 和组合(has-a),二者的设计思想和适用场景有本质区别。

特性继承(is-a 关系)组合(has-a 关系)
关系描述每个派生类对象都是一个基类对象假设 B 组合了 A,每个 B 对象中都有一个 A 对象
复用类型白箱复用:基类内部细节对子类可见黑箱复用:被组合类内部细节对组合类不可见
封装性破坏基类封装,基类修改会影响派生类保持封装,被组合类修改对组合类影响极小
耦合度高耦合:派生类与基类强依赖低耦合:组合类与被组合类弱依赖
扩展性派生类受基类限制,扩展性差基于接口组合,扩展性强

实战面试高频考点

1. 什么是菱形继承?

菱形继承是 C++ 多继承中的一种特殊场景,比如有一个基类 A,类 B 和 C 都继承自 A,然后类 D 又同时继承 B 和 C,整个继承结构画出来就像一个菱形。

2. 菱形继承的问题是什么?

它主要会带来两个核心问题:

  • 数据冗余:基类 A 的成员会在 D 里存两份,一份来自 B,一份来自 C,这会浪费内存。
  • 二义性:当你在 D 中访问 A 的成员时,编译器不知道该用 B 分支的还是 C 分支的,直接编译报错。

而且这种结构会让代码耦合度变高,后续维护起来也很麻烦。

3. 什么是菱形虚拟继承?它是如何解决数据冗余和二义性的?

菱形虚拟继承就是在 B 和 C 继承 A 时加上 virtual 关键字,让 A 成为整个继承体系的虚基类。这样,在最底层的 D 中,A 就只会存在一份实例,彻底解决了数据冗余。

同时,因为只有一份 A,访问时也就不存在二义性了。需要注意的是,虚基类 A 是由最底层的 D 来直接初始化的,而不是由 B 或 C 初始化。

4. 继承和组合的区别是什么?

最核心的区别在于它们的语义和复用方式:

  • 继承(is-a 关系):代表'是一种'的关系,比如 Student 是一种 Person。它是白盒复用,子类可以直接访问父类的 public 和 protected 成员,好处是复用代码很直接,但坏处是耦合度高,父类的改动可能会影响到子类。
  • 组合(has-a 关系):代表'包含一个'的关系,比如 Car 包含一个 Engine。它是黑盒复用,外部类只能通过成员对象的 public 接口来使用,看不到内部实现,好处是耦合度低,代码更稳定、灵活。

5. 什么时候用继承?什么时候用组合?

用继承的场景:当类之间有明确的 is-a 关系,并且满足里氏替换原则(子类可以完全替代父类)时。比如 Square 继承 Shape,因为正方形确实是一种形状,而且可以在任何需要 Shape 的地方用 Square 代替。

用组合的场景:当类之间是 has-a 的包含关系,或者你想降低耦合、保持代码灵活性时。比如 Computer 组合 CPU,电脑包含 CPU,但它们不是'是一种'的关系。组合也是实现依赖倒置原则的常用手段,能让代码更容易扩展。

当然也有这样的经验法则:在一个场景中,如果既可以用组合也可以用继承,那就优先选择组合,因为组合的耦合度更低,扩展性更好;如果场景中只能用继承(比如必须复用基类接口并满足 is-a 关系),那就只能选择继承。

目录

  1. 继承的基本语法与访问控制
  2. 继承的定义格式
  3. 三大继承方式与访问限定符
  4. 基类与派生类的对象赋值转换
  5. 合法的赋值转换
  6. 非法的赋值转换
  7. 强制类型转换的注意事项
  8. 继承中的作用域
  9. 成员变量的隐藏
  10. 成员函数的隐藏
  11. 派生类的默认成员函数
  12. 核心规则
  13. 代码演示
  14. 为什么析构函数的调用顺序是:派生类、基类?
  15. 继承的特殊场景:友元与静态成员
  16. 继承与友元
  17. 继承与静态成员
  18. 菱形继承及其解决方案
  19. 菱形继承的问题
  20. 菱形虚拟继承
  21. 常见问题思考
  22. 继承和组合的设计原则
  23. 对 C++ 多继承的客观认知
  24. 继承与组合的区别
  25. 实战面试高频考点
  26. 1. 什么是菱形继承?
  27. 2. 菱形继承的问题是什么?
  28. 3. 什么是菱形虚拟继承?它是如何解决数据冗余和二义性的?
  29. 4. 继承和组合的区别是什么?
  30. 5. 什么时候用继承?什么时候用组合?
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 算法题讲解:位运算应用(两数之和、只出现一次的数字、消失的两个数字)
  • AI 文本转语音与语音转文本:构建专属领域识别模型
  • Python Selenium Web 自动化测试实战:博客系统全流程解析
  • Python 程序性能优化的 7 个关键习惯
  • Vivado FPGA 资源利用率优化技巧
  • 飞算 JavaAI 专业版在 Java 微服务重构中的效率提升实践
  • Qwen-Image-2512:消费级 GPU 本地部署 AI 文生图方案
  • Llama 3-8B-Instruct 在昇腾 NPU 上的 SGLang 性能实测
  • 硕士论文盲审降 AI 率指南:评委是否查看检测报告
  • MyBatis 动态 SQL 语句常用元素
  • VB6 实现简易扫雷游戏
  • DGX Spark 部署 vLLM + Open WebUI 运行 Qwen3-Coder-Next-FP8(CUDA 13.0 版)
  • MySQL 数据类型详解:从数值到字符串的实战指南
  • Fish Speech 1.5 结合 Whisper 打造语音处理自动化闭环
  • Python + AI 实战指南:数据分析、LLM 应用与自动化办公三大方向
  • Pyenv 指定 Python 版本:灵活对接 Miniconda 环境
  • Java 泛型擦除机制:原理与限制分析
  • DeepSeek 与 GPT 技术架构深度解析
  • 前端设计与布局常用术语中英对照速查表
  • Ubuntu 部署 OpenClaw 并接入飞书机器人

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online