1.常量指针和指针常量

  1. const修饰指针 — 常量指针— 指向的值是常量
    1. const int * p = &a;
    2. 特点:指针的指向可以改,但是指针指向的值不可以改
  2. const修饰常量 — 指针常量— 指向是常量
    1. int * const p = &a;
    2. 特点:指针的指向不可以改,但是指针指向的值可以改

2.程序调试

3.数值和指针

#define _CRT_SECURE_NO_WARNINGS 1

#include<bits\stdc++.h>

using namespace std;

int main()

{

    //数组名作用

    //1、可以通过数组名统计整个数组占用空间的大小

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

    cout << "整个数组占用空间:" << sizeof(arr) << endl;

    cout << "整数类型占用内存为:" << sizeof(arr[0]) << endl;

    cout << "数组中元素的个数为:" << sizeof(arr) / sizeof(int) << endl;

    //2、可以通过数组名查看数组首地址

    cout << "数组的首地址为:" << (void*)arr << endl;

    cout << "数组中第一个元素的地址为:" << (void*)&arr[0] << endl;

    cout << "数组中第二个元素的地址为:" << (void*)&arr[1] << endl;

    //3、数组名是常量,不可以赋值

    // arr = arr + 1; // 错误,数组名不可改变

    //4、声明一个指针,指向数组的第一个元素

    int* p = arr;

    cout << "指针p的值(数组首地址):" << (void*)p << endl;//把指针当做一个地址值来处理,通常用于打印地址或者进行通用指针的操作

    cout << "指针p所指向的第一个元素的值:" << *p << endl;//*p解引用,指向第一个元素的值

    // 指针可以改变指向位置

    p = p + 1; // 指向第二个元素

    cout << "指针p加1后指向第二个元素,值为:" << *p << endl;

    // 指针变量占用的大小

    cout << "指针变量p占用空间:" << sizeof(p) << endl;

    // 指针可以赋值

    int b[5] = { 100, 200, 300, 400, 500 };

    p = b;

    cout << "指针p指向新数组b的第一个元素,值为:" << *p << endl;

    char str[] = "hello";//sizeof(str) = 6

    return 0;

}

4.C++定义常量的两种方式

方面 #define 宏常量 const 修饰的变量
本质 预处理指令,文本替换 编译时常量,具有类型
类型检测 无类型,不进行类型检查 有类型,编译器会进行类型检查
调试时信息 不保留调试信息,宏展开后看不到原名称 有符号信息,可以在调试器中查看名称和值
作用域 没有作用域,宏在定义后到文件末尾有效 有作用域,遵循C/C++作用域规则
表达能力 只能实现简单文本替换,不能定义复杂表达式 可以定义复杂的表达式和常量对象
内存占用 无内存开销,直接替换 可能占用内存(尤其在C中,C++中编译器可能优化掉)
安全性 易出错,宏替换可能导致意外行为 类型安全,减少错误
示例 #define PI 3.14159 const double pi = 3.14159;

5.分文件编写

  1. 创建.h后缀的头文件 写函数的声明 swap.h  #include<iostream>

#ifndef SWAP_H

#define SWAP_H

// 交换两个int变量的值

void swap(int& a, int& b);

#endif // SWAP_H

  1. 创建.cpp后缀的源文件 写函数的定义
  2. 在源文件中包含头文件,#include ”swap.h”

#include "swap.h"

void swap(int& a, int& b)

{

    int temp = a;

    a = b;

    b = temp;

}

  1. 其他源文件直接包含头文件即可使用 #include ”swap.h”

#include <iostream>

#include "swap.h"

int main()

{

    int x = 5;

    int y = 10;

    std::cout << "交换前:x = " << x << ", y = " << y << std::endl;

    swap(x, y);

    std::cout << "交换后:x = " << x << ", y = " << y << std::endl;

    system("pause");

    return 0;

}

用g++编译g++ main.cpp swap.cpp -o swap_test

用cmake编译:先在源文件目录下写一个CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(my)

set(CMAKE_CXX_STANDARD 17)

add_executable(my main.cpp swap.cpp)

然后新建一个build目录,cd进去,执行cmake -G "MinGW Makefiles" <CMakeLists.txt 文件路径>

会生成makefile文件,然后cmake --build .

6.C++内存分区

C++将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理
  • 全局区:存放已初始化的全局变量和静态变量以及常量

BSS 段 存放未初始化的全局变量和静态变量

  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

代码运行前

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读,只存一份
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局变量 和 字符串变量
  • 局部变量、const修饰的局部变量(局部常量)不在全局区中

代码运行后

栈区(Stack)

  • 栈是一块由编译器自动管理的内存区域,用于存储函数调用的局部变量函数参数、返回地址等
  • 栈具有 先进后出(LIFO, Last In First Out) 结构特点。
  • 分配速度快,通常由 CPU 寄存器维护栈指针自动分配释放空间。
  • 栈空间大小一般有限(几 MB 左右),超出栈空间会导致栈溢出(Stack Overflow)。
  • 内存由操作系统自动分配和释放,不需要程序员干预。

堆区(Heap)

  • 堆是程序运行时由程序员手动申请和释放的一块动态内存区域。
  • 堆的大小仅受限于系统虚拟内存大小,通常比栈大得多。
  • 堆内存的分配和释放由程序员负责(new 和 deletemalloc 和 free)。
  • 动态分配,适合存储大小或生命周期不确定的数据。
  • 分配速度相对较慢,因为需要维护更复杂的数据结构(空闲链表等)。

7.new delete和malloc free

#define _CRT_SECURE_NO_WARNINGS 1

#include<bits\stdc++.h>

int main(){

    // 分配内存,但是不自动调用构造函数(本例中为int,不涉及构造函数)

    int* p = (int*)malloc(sizeof(int) * 1);

    // 需要手动赋值,不会自动初始化

    *p = 10;

    // 释放内存

    free(p);

    // 分配数组

    int* arr = (int*)malloc(sizeof(int) * 5);

    // 释放数组内存

    free(arr);

    // 分配一个int,并初始化为10

    int* p2 = new int(10);

    // 使用完释放内存

    delete p2;

    // 分配一个整型数组,长度为5

    int* arr2 = new int[5];

    // 释放动态数组

    delete[] arr2;

    return 0;

}

特性 new / delete malloc / free
语言 C++ 原生操作符 C 标准库函数
头文件 无需包含头文件 需包含 <cstdlib> 或 <stdlib.h>
内存分配 分配指定类型大小,自动调用构造函数 分配任意字节大小,不调用构造函数
内存释放 自动调用析构函数后释放 不调用析构函数,直接释放内存
返回类型 返回指定类型指针,无需强制类型转换 返回 void*,一般需要强制转换成相应类型

8.static关键字和静态成员

static 关键字的几种用法

1.1 在全局变量或函数

将变量或函数的链接属性改为内部链接,只在当前文件可见,避免与其他文件中同名符号冲突。

1.2 在函数内部的变量

  • 该变量变为静态局部变量,生命周期从程序开始直到结束,但作用域仍限于函数内部。
  • 变量只初始化一次,且保存其状态。

void func() {

    static int counter = 0;  // 只初始化 1 次

    counter++;

    std::cout << counter << std::endl;

}

调用两次func(),第二次输出的是2

1.3 在类成员变量或成员函数

  • 静态成员属于类本身,而不是某个对象实例。
  • 静态成员变量被所有对象共享。
  • 静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为调用时没有对象实例。

class MyClass {
public:
    static int count;               // 静态成员变量声明
    static void printCount();       // 静态成员函数声明
};

int MyClass::count = 0;            // 静态成员变量定义,必须在类外初始化

void MyClass::printCount() {
    std::cout << "Count = " << count << std::endl;
}

9.C++对象模型和this指针

C++ 对象模型描述的是C++中类对象在内存中是如何布局和组织的,涉及成员变量、成员函数、继承、多态等方面。

  • 非静态成员变量
    • 每个类对象都会拥有一份非静态成员变量的内存空间。
    • 成员变量在对象中是连续存储的,顺序通常按声明顺序排列(但编译器可适当调整对齐填充)。
  • 静态成员变量
    • 静态成员不存储在对象的内存空间中,只有一份静态成员变量存储在类的静态内存区,所有对象共享。
  • 成员函数
    • 成员函数的代码只存储一份,不随对象而重复。函数指针不会放在对象中。

this 是一个指向当前对象的隐式指针,类型为 ClassName*。成员函数内部自动包含 this 指针,指向调用该函数的对象。

  • 通过 this 指针,成员函数可以访问调用它的具体对象的成员变量和函数。
  • 解决名字隐藏(当局部变量和成员变量同名时,区分成员变量需要用 this->)。
  • 可用于返回对象自身的指针,支持链式调用。
  • 不是所有函数都有 this 指针,静态成员函数没有 this 指针,因为它们不属于某个对象。

这里返回类型必须是Person& 而不是Person,否者会执行拷贝函数

加&相当于:Person& p'= p2;创建一个对 p2 的引用, 任何对 p' 的更改都会影响到 p2

不加&相当于Person p' = p2,创建 p2 的一个副本,即 p' 是一个新的 Person 对象

10.const关键字和常函数常对象

const 修饰符用于修饰变量、对象或函数,表示不可修改的含义。

常函数:在成员函数后加 const,表示这个成员函数不会修改对象的任何非 mutable 成员变量。

常对象:用 const 修饰的对象,其成员变量不能被修改,只能调用常成员函数。

mutable 关键字允许常成员函数修改这个成员变量。

class MyClass {
    mutable int cache;  // 即使在 const 成员函数也可以修改
public:
    int getCache() const {
        return cache++;
    }
};

11.友元

友元(friend)机制允许你打破类的封装,某些函数或类可以访问该类的私有(private)和受保护(protected)成员。

1. 友元函数(Friend Function)

class A {
private:
    int x;
public:
    A(int val) : x(val) {}
    friend void showX(const A& a); // 友元函数声明
};

void showX(const A& a) {
    std::cout << "x = " << a.x << std::endl; // 访问私有成员
}

#include <iostream>
using namespace std;

class MyNumber {
private:
    int value;
public:
    MyNumber(int v = 0) : value(v) {}

    // 重载 + 运算符(成员函数)
    MyNumber operator+(const MyNumber& other) const {
        return MyNumber(this->value + other.value);
    }

    // 重载 = 运算符
    MyNumber& operator=(const MyNumber& other) {
        if (this != &other) {
            this->value = other.value;
        }
        return *this;
    }

    // 友元形式,重载 << 运算符(输出流)
    friend ostream& operator<<(ostream& os, const MyNumber& num);

    int getValue() const { return value; }
};

// 实现友元函数 << 运算符重载
ostream& operator<<(ostream& os, const MyNumber& num) {
    os << num.value;
    return os;
}

int main() {
    MyNumber a(10);
    MyNumber b(20);
    MyNumber c = a + b;  // 使用重载的 +

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;

    MyNumber d;
    d = c;  // 使用重载的 =
    cout << "d = " << d << endl;

    return 0;
}

2. 友元类(Friend Class)

  • 一个类可以声明另一个类为其友元类。
  • 友元类的所有成员函数均可以访问该类的私有和受保护成员。

class A {
private:
    int x = 10;
    friend class B;  // B 是 A 的友元类
};

class B {
public:
    void showA(const A& a) {
        std::cout << "A::x = " << a.x << std::endl; // 访问 A 的私有成员
    }
};

3. 友元成员函数(Friend Member Function)

只让某个类的特定成员函数成为友元。

class B;//先声明

class A {
private:
    int x = 10;
public:
    friend void B::printX(const A& a);  // B类中的printX成员函数是A的友元
};

class B {
public:
    void printX(const A& a) {
        std::cout << "A::x = " << a.x << std::endl;
    }
};

12.模板函数

//template <typename T> 表示定义一个模板,其中 T 是模板参数,可以理解为占位符类型。

template <typename T>

返回值类型 函数名(参数列表){

// 函数体中可以使用类型T }

swapValues<int>(x, y);  // 显示指定模板类型为int

//多个函数模版

template <typename T1, typename T2>
void printPair(const T1& a, const T2& b) {
    cout << a << " and " << b << endl;
}

13.静态多态和动态多态

静态多态(Static Polymorphism)

  • 编译期确定调用哪个函数。
  • 主要通过函数重载(Function Overloading)、运算符重载(Operator Overloading)和模板机制实现。
  • 调用开销小,没有运行时额外负担。

动态多态(Dynamic Polymorphism)

  • 运行时决定调用哪个函数。
  • 通过**虚函数(virtual functions)**实现。
  • 允许通过基类指针或引用调用派生类重写的方法。
  • 实现了接口的动态绑定,是面向对象的经典多态实现。
  • 需要额外内存空间(如虚函数表指针)和运行时开销(虚函数调用开销)。
  • 每个有虚函数的类,编译器会创建一个虚函数表(vtable),里面存放该类所有虚函数的地址。
  • 每个对象实例中隐藏一个虚函数表指针(vptr)(通常是对象的第一个成员),指向它所属类的虚函数表。

class Base {
public:
    virtual void show() {
        std::cout << "Base show" << std::endl;
    }
    virtual ~Base() {}  // 虚析构函数确保正确析构
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived show" << std::endl;
    }
};

int main() {
    Base* p = new Derived;

/*

  • p 指向一个 Derived 对象,内存中该对象的 vptr 指向 Derived 类的 vtable。
  • 调用 p->show() 时,并不是简单调用编译时绑定的函数地址。
  • 而是先从对象中取得 vptr,然后通过 vptr 找到所属类的 vtable,访问 vtable 中对应 函数的地址。
  • 调用这个地址,即调用了 Derived::show,实现动态绑定。

*/

    p->show();  // 输出 "Derived show",动态绑定
    delete p;//// 同时调用 Derived 和 Base 的析构函数

//虚析构函数保证通过基类指针删除派生类对象时,能正确调用派生类析构函数,释放派生类特有的资源。不写虚析构会导致Derived的析构函数未被调用,导致内存泄漏

}

C++ 中没有虚构造函数

虚函数依赖于对象存在

  • 虚函数机制依赖于对象的 vptr 指针,而 vptr 是对象被构造出来后才会设置的。
  • 在构造函数执行期间,对象还不完全成立,特别是基类构造函数执行时,派生类部分尚未构造,vptr 指向基类对应的虚函数表。
  • 因此,构造函数不能多态调用派生类版本,也就是说不支持虚构造函数。

14.C++ 继承基础和虚继承

C++ 继承是面向对象编程的核心特性之一,它支持代码复用多态。其中,菱形继承(Diamond Inheritance)是多继承中经典的问题,C++通过虚继承机制解决这个问题。

C++ 继承基础

  • 子类(派生类)可以继承父类(基类)的成员变量成员函数(具体访问权限受 publicprotectedprivate 控制)。
  • 继承类型:
    • 公有继承(public):基类的 public 和protected 成员分别变为派生类的 public 和 protected 成员。
    • 保护继承(protected) :基类的 public 和 protected 成员变为派生类 protected
    • 私有继承(private):基类所有成员在派生类中都变为 private
  • 支持单继承和多继承,多继承允许一个类从多个基类继承。

多继承中成员变量重名问题

class A {
public:
    int a = 1;
};

class B {
public:
    int a = 2;
};

class C {
public:
    int a = 3;
};

class D : public A, public B, public C {};

要明确告诉编译器你想访问哪个基类的变量,方式是通过加上类名限定符:

cout << d.A::a << endl;  // 访问 A 的 a -> 输出 1
cout << d.B::a << endl;  // 访问 B 的 a -> 输出 2
cout << d.C::a << endl;  // 访问 C 的 a -> 输出 3

解决菱形继承:虚继承(Virtual Inheritance)

class B : virtual public A { ... };
class C : virtual public A { ... };

class D : public B, public C {};//虚继承保证 D 中只存在一份 A 的子对象。

#include <iostream>
using namespace std;

class A {
public:
    int a = 1;
};

class B {
public:
    int a = 2;
};

class C {
public:
    int a = 3;
};

class D : public A, public B, public C {};

int main() {
    D d;
    cout << d.A::a << endl;  // 1
    cout << d.B::a << endl;  // 2
    cout << d.C::a << endl;  // 3

    // cout << d.a << endl;  // 编译错误:二义性

    return 0;
}

15.对比vector、list和deque

vector

是可变大小的动态数组,可以采用下标对vector的元素进行访问,而且它的大小会被容器自动处理。初始时,vector 通过分配一定大小连续内存空间来存储元素,空间由分配器(allocator)管理。当插入元素超过当前容量时,vector 会进行扩容。

[start ------------- finish ---------- end_of_storage]
 |------ 当前存储数据 ------|   |---- 预留空间 ----|

注意,迭代器end()会获取最后一个元素的下一个位置。

  • 空间不足时,vector 申请新的更大的一块连续内存,vs下capacity是按1.5倍增长的,g++是按2倍增长的,来减少频繁扩容带来的性能开销。
  • 扩容时,原有元素会被拷贝或移动到新内存中(调用元素的拷贝构造或移动构造函数)。
  • 扩容后,释放旧的内存块。

常用的算法:

reserse(n)函数改变容器的最大容量,resize(n)函数改变容器中的有效元素个数。

size():当前元素数量,capacity():当前已分配可用容量,empty():判空

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {10, 20, 30, 40, 50};

    // 1. 元素访问
    std::cout << "元素访问:" << std::endl;
    std::cout << "v[0] = " << v[0] << std::endl;           // 通过 operator[] 访问,不做范围检查
    std::cout << "v.at(1) = " << v.at(1) << std::endl;     // 通过 at() 访问,有范围检查

    std::cout << "遍历所有元素:" << std::endl;
    for (size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << " ";
    }

    // 2. 插入元素
    std::cout << "插入元素:" << std::endl;
    v.push_back(60);  // 尾部插入
    v.insert(v.begin() + 2, 25);  // 在第三个位置插入值为 25 的元素

    // 打印插入后的元素
    for (int x : v) {
        std::cout << x << " ";
    }

    // 3. 删除元素
    std::cout << "删除元素:" << std::endl;
    v.erase(v.begin() + 3);         // 删除第四个元素
    v.pop_back();                   // 删除末尾元素

    // 打印删除后的元素
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

vector的迭代器在底层实际上就是一个指针。迭代器失效就是指迭代器底层对应指针所指向的空间被销毁了,指向的是一块被释放的空间,如果继续使用已经失效的迭代器,程序可能会崩溃。

list

是一个双向链表容器,实现了链式存储结构。不像 vector,它的元素不是连续存放的,而是通过节点指针相互连接

  • 优点是插入和删除任意位置元素都非常高效,时间复杂度为 O(1),不需要像 vector 那样移动元素。
  • 缺点是不支持随机访问,访问元素只能通过迭代器顺序遍历,时间复杂度为 O(n)。
  • 相比 vector 的连续存储,list 的底层并没有一块连续内存,而是通过节点结构体链接实现。

#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {10, 20, 30, 40, 50};

    // 1. 访问元素(只能顺序访问,不能通过索引)
    std::cout << "list元素遍历: ";
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        std::cout << *it << " ";
    }

    // 2. 插入元素
    lst.push_back(60);   // 末尾插入
    lst.push_front(5);   // 头部插入

    auto it = lst.begin();
    std::advance(it, 3);  // 将迭代器移动到第四个元素
    lst.insert(it, 25);   // 在第四个位置插入25

    std::cout << "插入元素后:";
    for (auto x : lst) {
        std::cout << x << " ";
    }

    // 3. 删除元素
    lst.pop_back();        // 删除末尾元素
    lst.pop_front();       // 删除头部元素

    it = lst.begin();
    std::advance(it, 2);   
    lst.erase(it);         // 删除第三个元素

    std::cout << "删除元素后:";
    for (auto x : lst) {
        std::cout << x << " ";
    }

    // 4. 其他操作
    std::cout << "list大小: " << lst.size() << std::endl;
    std::cout << "list是否为空: " << (lst.empty() ? "是" : "否") << std::endl;

    lst.clear();   // 清空所有元素
    std::cout << "清空后是否为空: " << (lst.empty() ? "是" : "否") << std::endl;

    return 0;

/*  void splice (iterator position, list& x);

  • position:目标容器中的插入位置。
  • x:待“拼接”的源列表,拼接后其元素会从 x 中被移除到目标列表。

*/

}

在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

deque

双端队列(Double-Ended Queue)的简称。

  • 支持在头部和尾部高效插入和删除元素(时间复杂度均为 O(1))。
  • 既支持类似 vector 的随机访问通过下标访问元素),又支持在两端频繁插入删除,比 vector 更灵活。
  • 内存布局不像 vector 是连续存储,而是分段连续,适合频繁头尾操作。
  • deque 内部由多个固定大小的缓冲区块组成,这些块被连续管理起来。
  • 通过一个指向块指针的数组管理这些缓冲区,实现高效插入删除与随机访问。

#include <iostream>
#include <deque>

int main() {
    std::deque<int> dq;

    // 在尾部添加元素
    dq.push_back(10);
    dq.push_back(20);

    // 在头部添加元素
    dq.push_front(5);
    dq.push_front(1);

    std::cout << "Deque中的元素有:";
    for (int x : dq) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // 访问元素
    std::cout << "第一个元素: " << dq.front() << std::endl;
    std::cout << "最后一个元素: " << dq.back() << std::endl;

    // 删除元素
    dq.pop_front();
    dq.pop_back();

    std::cout << "删除首尾元素后:";
    for (int x : dq) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // 使用下标访问
    if (!dq.empty()) {
        std::cout << "第一个元素(下标访问): " << dq[0] << std::endl;
    }

    return 0;
}

deque 综合了性能和功能的平衡,成为 STL 中很多容器(尤其是适合头尾操作的适配器容器)的默认底层容器。

对比

容器 支持随机访问 头部插入删除 尾部插入删除 内存连续性
vector
deque 否(分段连续)
list 否(链表结构)
操作类型 vector deque list
随机访问 O(1)(常数时间) O(1)(常数时间) O(n)(线性时间)
尾部插入 平均 O(1),扩容时 O(n) O(1) O(1)
头部插入 O(n) O(1) O(1)
尾部删除 O(1) O(1) O(1)
头部删除 O(n) O(1) O(1)
中间插入/删除 O(n) O(n) O(1)(给定迭代器位置)

16.C++类中的六大成员函数

class MyClass {
public:
    int* data;

    // 1. 默认构造函数
    MyClass() : data(new int(0)) {
        cout << "默认构造函数" << endl;
    }

    // 2. 带参数构造函数
    MyClass(int val) : data(new int(val)) {
        cout << "带参数构造函数" << endl;
    }

    // 3. 拷贝构造函数
    MyClass(const MyClass& other) : data(new int(*other.data)) {
        cout << "拷贝构造函数" << endl;
    }

    // 4. 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        cout << "移动构造函数" << endl;
        other.data = nullptr;
    }

    // 5. 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        cout << "拷贝赋值运算符" << endl;
        if (this == &other) return *this;
        delete data;
        data = new int(*other.data);
        return *this;
    }

    // 6. 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        cout << "移动赋值运算符" << endl;
        if (this == &other) return *this;
        delete data;
        data = other.data;
        other.data = nullptr;
        return *this;
    }

    // 析构函数
    ~MyClass() {
        cout << "析构函数" << endl;
        delete data;
    }
};

更多推荐