前言
QT可以调用很多第三方的C、C++库,了解一些基础知识是有必要的。C++能兼容大部分C的特性,有些是基于设计的考虑不兼容。提“兼容”只是为了指出可察的“延续性”,不深入“兼容”这个话题。本篇侧重QT开发者的角度去理解,指针和结构体的知识是为类进行铺垫,主要是方便QT开发者阅读,对于C开发者来说,也仅是基础知识。
结构体
在C语言中,结构体是方便开发者自定义数据类型,准确的说是“数据聚合器”,即把开发者需要的类型按顺序集合在一个地址空间中。
在C++中,基于设计哲学(OOP),结构体就是一个特殊的类,可以理解为一个轻量级的类,特殊点是:类成员默认为私有类型,结构体默认为公开类型。
特性 | 结构体(struct) | 类(class) |
|---|---|---|
成员变量 | ✅ | ✅ |
成员函数 | ✅ | ✅ |
继承 | ✅ | ✅ |
多态 | ✅ | ✅ |
模板 | ✅ | ✅ |
访问控制 | ✅ | ✅ |
默认访问权限 | public | private |
C风格的结构体用法
结构体与专属的操作是分类的
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stddef.h> typedef struct Buffer{ char* data; size_t size; } stBuffer; void buffer_init(stBuffer* buf, size_t size) { if (buf == NULL) return; buf->data = (char*)malloc(size); if (buf->data != NULL) { buf->size = size; memset(buf->data, 0, size); } else { buf->size = 0; } } void buffer_free(stBuffer* buf) { if (buf == NULL) return; if (buf->data != NULL) { free(buf->data); buf->data = NULL; } buf->size = 0; } void buffer_copy(stBuffer* dst, const stBuffer* src) { if (dst == NULL || src == NULL || src->data == NULL) return; if (dst->data != NULL) { free(dst->data); } dst->data = (char*)malloc(src->size); if (dst->data != NULL) { memcpy(dst->data, src->data, src->size); dst->size = src->size; } else { dst->size = 0; } } void print_buffer(const stBuffer* buf, const char* name) { printf("%s: data=%p, size=%zu\n", name, (void*)buf->data, buf->size); if (buf->data != NULL) { printf("Content: "); for (size_t i = 0; i < buf->size && i < 10; i++) { printf("%02x ", (unsigned char)buf->data[i]); } printf("\n"); } } int main() { printf("Buffer Management System Test\n"); printf("=============================\n\n"); stBuffer buf1; printf("Test 1: Initialize buffer\n"); buffer_init(&buf1, 16); print_buffer(&buf1, "buf1"); printf("\n"); stBuffer buf2; printf("Test 2: Copy buffer\n"); buffer_copy(&buf2, &buf1); print_buffer(&buf2, "buf2"); printf("\n"); printf("Test 3: Write data to buffer\n"); if (buf1.data != NULL) { strcpy(buf1.data, "Hello Buffer!"); printf("buf1 content: %s\n", buf1.data); } printf("\n"); printf("Test 4: Deep copy with data\n"); stBuffer buf3; buffer_copy(&buf3, &buf1); print_buffer(&buf3, "buf3"); printf("buf3 content: %s\n", buf3.data); printf("\n"); printf("Test 5: Free buffers\n"); buffer_free(&buf1); buffer_free(&buf2); buffer_free(&buf3); print_buffer(&buf1, "buf1 after free"); print_buffer(&buf2, "buf2 after free"); print_buffer(&buf3, "buf3 after free"); return 0; }C++风格的结构体用法
#include <iostream> #include <vector> #include <string> #include <cstring> struct Buffer { std::vector<char> data; // 默认构造函数 Buffer() = default; // 带大小参数的构造函数 Buffer(size_t size) : data(size) {} // 从C字符串构造 Buffer(const char* str) { if (str) { size_t len = strlen(str); data.resize(len); memcpy(data.data(), str, len); } } // 从std::string构造 Buffer(const std::string& str) { data.resize(str.size()); memcpy(data.data(), str.c_str(), str.size()); } // 复制数据函数 void copyFrom(const Buffer& other) { data = other.data; } // 清空缓冲区 void clear() { data.clear(); } // 打印缓冲区信息 void printInfo(const std::string& name) const { std::cout << name << " - 大小: " << data.size(); if (!data.empty()) { std::cout << ", 内容: "; // 显示十六进制数据 for (size_t i = 0; i < data.size() && i < 15; i++) { printf("%02x ", static_cast<unsigned char>(data[i])); } if (data.size() > 15) { std::cout << "..."; } } std::cout << std::endl; } }; int main() { std::cout << "1. 不同构造函数:" << std::endl; Buffer buf1(10); // 指定大小 Buffer buf2("Hello C++ Buffer!"); // 从C字符串构造 Buffer buf3(std::string("From std::string")); // 从std::string构造 Buffer buf4; // 默认构造 std::cout << "2. 复制操作:" << std::endl; buf4.copyFrom(buf2); buf4.printInfo("buf4 复制后"); std::cout << std::endl; std::cout << "3. 清空缓冲区:" << std::endl; buf2.clear(); return 0; }这里先不讲解C++的结构体里面为什么能加入函数,在类中讲解。
指针
“慎用”、“越界”、“安全释放”,初学C时没少被吓唬。为了“高效”,数据存储/读取、运算、排序中都有指针的影子,在C++中就很少,因为C++做了很多封装,比如常用的STL容器(迭代器就是一个节点指针),new一个类型/结构体/类对象(底层封装了malloc)。
在使用第三方库时,通常都会接触到指针函数和函数指针,这两个容易搞混,下面详细讲解。
指针函数
字面意思,返回指针的函数。QT调用第三方库时,有不少的机会遇到。以下代码我是之前分享的的博文(https://blog.csdn.net/liangyuna8787/article/details/144730835?spm=1001.2014.3001.5502【QT常用技术讲解】excel表格处理两种方式:QAxObject和qtxlsx)的代码片段
#include "xlsxdocument.h" #include "xlsxworksheet.h" void MainWindow::saveexcel(QTableWidget *tableWidget, const QString &fileName) { QXlsx::Document xlsx; QXlsx::Worksheet *worksheet = xlsx.currentWorksheet(); // 设置表头和数据 for (int row = 0; row < tableWidget->rowCount(); ++row) { for (int col = 0; col < tableWidget->columnCount(); ++col) { QTableWidgetItem *item = tableWidget->item(row, col); if (item) { worksheet->write(row + 1, col + 1, item->text()); } else { worksheet->write(row + 1, col + 1, ""); } } } //合并表头 xlsx.mergeCells(QXlsx::CellRange(1, 1, 1, tableWidget->columnCount())); // 保存到文件 xlsx.saveAs(fileName); }可以看到有两个函数返回了指针,一般涉及层级、节点的库,都会用指针进行高效操作。
函数指针
它是一个指向某种类型函数的指针变量,即这个指针变量指向某个类型(编译器会检测类型是否匹配)的函数。
通俗的讲,它首先是一个变量,因为具备指针的“借尸还魂”的指向特性,可以指向某个代码片段。
比较常见的案例是C、C++动态库的函数调用,以下分享Linux下C、C++动态库调用的案例
C函数指针--动态库调用
// 动态库文件mathlib.c int add(int a, int b) { return a + b; } int multiply(int a, int b) { return a * b; } //编译命令:gcc -fPIC -shared -o libmath.so mathlib.c用dlopen方式调用动态库,以下提供两种函数指针类型定义的方法
// main.c #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> // 必须包含此头文件 // 函数指针类型定义(与动态库中的函数签名一致) int (*add)(int, int); typedef int (*multiply_func_t)(int, int); int main() { void *handle; multiply_func_t multiply; char *error; // 打开动态库 handle = dlopen("./libmath.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "dlopen error: %s\n", dlerror()); exit(EXIT_FAILURE); } // 清除之前的错误 dlerror(); // 获取 add 函数地址 *(void **)(&add) = dlsym(handle, "add"); error = dlerror(); if (error != NULL) { fprintf(stderr, "dlsym 'add' error: %s\n", error); dlclose(handle); exit(EXIT_FAILURE); } // 获取 multiply 函数地址 *(void **)(&multiply) = dlsym(handle, "multiply"); error = dlerror(); if (error != NULL) { fprintf(stderr, "dlsym 'multiply' error: %s\n", error); dlclose(handle); exit(EXIT_FAILURE); } // 调用函数 printf("add(3, 5) = %d\n", add(3, 5)); printf("multiply(4, 6) = %d\n", multiply(4, 6)); // 关闭动态库 dlclose(handle); return 0; }C++函数指针--动态库调用
动态库不仅仅可以传递函数,类也可以传递,以及直接传递类中的函数。编写动态库时,需要额外加上extern "C" {},之前的博文分享传递类和函数的样例:
#pragma once #include <iostream> class MyClass { public: MyClass(); ~MyClass(); void printMessage(const char* msg); }; //普通函数需要使用extern "C"声明,外部才可调用 #ifdef __cplusplus extern "C" { #endif MyClass* createMyClass(); // 创建对象 void destroyMyClass(MyClass*); // 销毁对象 #ifdef __cplusplus } #endif详细讲解请查看博文:【Linux C/C++开发】编译及引用so动态库
注意了,以上分享函数指针的样例,是让你感知到指针强大的“跨域”引用能力,能更好的理解类。
类
从C结构体的延伸
类是面向对象编程(oop)的核心设计,C++本身也是为了改进C语言,从底层语言向高级语言迈进(调侃~)。
前面已经分享了在C++结构体是一个特殊的类。从C语言的角度来看,也可以理解为C++中的类是从结构体延伸出去的成熟体。
这个是“C语言的角度”:了解C语言基础知识的人都知道C是没有类的,但结构体+函数指针是可以看出类的雏形,下面提供个代码案例:
#include <stdio.h> #include <stdlib.h> #include <string.h> // 模拟一个"矩形类" typedef struct Rectangle { // 数据成员 double width; double height; // 函数指针(模拟成员函数) double (*area)(struct Rectangle* self); double (*perimeter)(struct Rectangle* self); void (*setSize)(struct Rectangle* self, double w, double h); void (*display)(struct Rectangle* self); } Rectangle; // 成员函数实现 double rectangle_area(Rectangle* self) { return self->width * self->height; } double rectangle_perimeter(Rectangle* self) { return 2 * (self->width + self->height); } void rectangle_setSize(Rectangle* self, double w, double h) { if (w > 0) self->width = w; if (h > 0) self->height = h; } void rectangle_display(Rectangle* self) { printf("矩形: 宽=%.2f, 高=%.2f, 面积=%.2f, 周长=%.2f\n", self->width, self->height, self->area(self), self->perimeter(self)); } // 构造函数 Rectangle* Rectangle_create(double width, double height) { Rectangle* rect = (Rectangle*)malloc(sizeof(Rectangle)); if (rect) { rect->width = width; rect->height = height; // 绑定函数指针 rect->area = rectangle_area; rect->perimeter = rectangle_perimeter; rect->setSize = rectangle_setSize; rect->display = rectangle_display; } return rect; } // 析构函数 void Rectangle_destroy(Rectangle* rect) { if (rect) { free(rect); } } //---↑↑↑↑↑--以上是结构体"矩形类"相关定义-----↑↑↑↑↑ //---↓↓↓↓↓--以下模拟"继承"特性--------------↓↓↓↓↓ // 模拟继承:彩色矩形继承自矩形 typedef struct ColoredRectangle { Rectangle base; // 基类(模拟继承) char color[20]; // 新增的函数指针 void (*setColor)(struct ColoredRectangle* self, const char* color); void (*displayColor)(struct ColoredRectangle* self); } ColoredRectangle; // 子类特有的函数 void coloredRectangle_setColor(ColoredRectangle* self, const char* color) { strncpy(self->color, color, sizeof(self->color) - 1); self->color[sizeof(self->color) - 1] = '\0'; } void coloredRectangle_displayColor(ColoredRectangle* self) { printf("彩色"); self->base.display((Rectangle*)self); // 调用基类方法 printf("颜色: %s\n", self->color); } // 子类构造函数 ColoredRectangle* ColoredRectangle_create(double width, double height, const char* color) { ColoredRectangle* crect = (ColoredRectangle*)malloc(sizeof(ColoredRectangle)); if (crect) { // 初始化基类部分 crect->base.width = width; crect->base.height = height; // 绑定基类函数指针 crect->base.area = rectangle_area; crect->base.perimeter = rectangle_perimeter; crect->base.setSize = rectangle_setSize; crect->base.display = rectangle_display; // 初始化子类部分 strncpy(crect->color, color, sizeof(crect->color) - 1); crect->color[sizeof(crect->color) - 1] = '\0'; // 绑定子类函数指针 crect->setColor = coloredRectangle_setColor; crect->displayColor = coloredRectangle_displayColor; } return crect; } // 子类析构函数 void ColoredRectangle_destroy(ColoredRectangle* crect) { if (crect) { free(crect); } } //---↑↑↑↑↑--以上模拟"继承"特性--------------↑↑↑↑↑ int main(){ // 1. 创建和使用矩形对象 printf("1. 矩形类使用:\n"); Rectangle* rect = Rectangle_create(10.0, 5.0); rect->display(rect); rect->setSize(rect, 8.0, 6.0); printf("修改大小后: "); rect->display(rect); printf("面积: %.2f\n", rect->area(rect)); printf("\n"); // 2. 继承使用 printf("2. 继承使用:\n"); ColoredRectangle* credRect = ColoredRectangle_create(12.0, 8.0, "红色"); credRect->displayColor(credRect); credRect->setColor(credRect, "蓝色"); credRect->base.setSize((Rectangle*)credRect, 15.0, 10.0); printf("修改后: "); credRect->displayColor(credRect); printf("\n"); // 3. 资源清理 printf("4. 资源清理:\n"); Rectangle_destroy(rect); ColoredRectangle_destroy(credRect); return 0; }C语言是面向过程的编程语言,C++是面向对象的编程语言。不只是设计理念,语法、编译器也不一样。以下是几个演变过程:
从C的结构体到C++的类:C语言的结构体只能包含数据成员,而C++的结构体可以包含成员函数、访问控制(public、private、protected)、继承、虚函数等。
虚函数表(vtable):为了实现多态,C++引入了虚函数。每个包含虚函数的类(或结构体)都会有一个虚函数表,这是一个函数指针数组,每个虚函数对应一个指针。每个对象会有一个隐藏的指针(vptr)指向这个表。当调用虚函数时,通过vptr找到虚函数表,再通过偏移量调用正确的函数。
成员函数:非虚成员函数在编译时就可以确定地址,因此不需要通过指针间接调用。它们与普通函数类似,只是隐含了一个指向对象的指针(this指针),自动在函数参数列表开头添加一个隐藏参数:
ClassType* this
QT的信号-槽机制其实也是在维护一个”表“,某个地方发出信号之后,信号就遍历这个”信号-槽“表,匹配上信号的就触发槽函数的调用。
虚函数表,以及指向对象的指针(this指针)机制,都是C++为了让类准确的找到虚函数/成员函数。
以上只是从映射表的逻辑来描述C结构体和C++的关联性,辅助理解,并非是全部执行逻辑。
类的特性
以上是与C有关的知识,下面记录下QT开发中使用到的类特性。
类的封装
类默认是private访问控制,即私有性质,外部无法直接调用。除了构造函数,需要通过显示接口对外暴露调用,尽可能避免外部代码直接修改内部状态。
比如myclass类有一个成员变量bool m_status;,如果设置为public访问,就会被外部代码直接引用并修改,可以这样操作,但是不安全,违背封装性,通常是提供一个对外暴露的接口函数bool getmyclassstatus() const;来获取支持,如果需要修改此值,也是提供对外暴露的接口函数 void setstatus(bool);来使用。
private访问控制不代表不能访问,友元(friend)机制就是一个特例机制。
类的继承
定义一个新类(派生类 / 子类)基于已有类(基类 / 父类),从而复用代码、扩展功能、实现多态。
//常用的单继承 class MainWindow : public QMainWindow{ }; //可以多继承 class Document : public Printable, public Serializable { };基类中的访问权限 | 继承方式 | 在派生类中的访问权限 |
|---|---|---|
public | public | public |
protected | public | protected |
private | public | 不可访问 |
public | protected | protected |
protected | protected | protected |
private | protected | 不可访问 |
public | private | private |
protected | private | private |
private | private | 不可访问 |
类的构造、析构
调用规则
构造函数的主要职责是初始化对象。它在对象内存分配完成后、对象正式可用之前自动执行。
#include <iostream> #include <vector> using namespace std; // 自定义测试类,打印所有构造/析构的调用信息 class MyClass { private: int val; public: // 普通构造函数 MyClass(int v = 0) : val(v) { cout << "[调用普通构造函数] val = " << val << endl; } // 拷贝构造函数 MyClass(const MyClass& other) { val = other.val; cout << "[调用拷贝构造函数] val = " << val << endl; } // 移动构造函数 MyClass(MyClass&& other) noexcept { val = other.val; other.val = -1; cout << "[调用移动构造函数] val = " << val << endl; } // 析构函数 ~MyClass() { cout << "[调用析构函数] val = " << val << endl; } }; // ---------------- 1. 全局对象 ---------------- MyClass globalObj(100); // main执行前就会调用构造函数 void testStackObj() { cout << "\n--- 进入testStackObj函数,准备定义栈上对象 ---" << endl; // 场景1:栈上局部对象,执行到这行代码时立即调用构造 MyClass stackObj(1); cout << "--- 即将离开testStackObj作用域,栈对象马上析构 ---" << endl; } void testStaticLocalObj() { // 场景3:函数内静态局部对象,仅第一次执行到该行时调用构造 static MyClass staticObj(30); } // 按值传参,会触发拷贝构造 void passByValue(MyClass obj) { cout << "--- 按值传参的函数内部执行 ---" << endl; } // 返回局部对象,触发移动/拷贝构造 MyClass returnObj() { MyClass temp(20); return temp; } int main() { cout << "=== main函数正式开始执行 ===" << endl; // 场景1:测试栈上对象 testStackObj(); // 场景2:堆上动态对象 cout << "\n--- 准备执行new创建堆对象 ---" << endl; MyClass* heapObj = new MyClass(2); cout << "--- 准备执行delete释放堆对象 ---" << endl; delete heapObj; // 手动调用析构 // 场景3:测试静态局部对象 testStaticLocalObj(); testStaticLocalObj(); // 第二次调用验证静态对象只构造一次 // 场景4:临时对象、拷贝/移动场景 cout << "\n--- 按值传递参数场景 ---" << endl; MyClass a(5); passByValue(a); // 场景5:函数返回局部对象场景 MyClass ret = returnObj(); //场景6: vector扩容触发移动构造场景 vector<MyClass> vec; // 预先预留空间可以避免扩容,这里故意不预留来触发扩容 vec.emplace_back(10); vec.emplace_back(20); return 0; }析构函数的主要职责是清理资源(如释放内存、关闭文件句柄等)。它在对象生命周期结束时自动执行,用户通常不能手动调用。
析构函数的自调用时机也受程序的作用域影响,下面的表格是列举上面代码例子的析构函数调用时间点
| 对象 | 析构触发时机 |
|---|---|
// 场景1:testStackObj() | 函数执行完、离开testStackObj作用域的瞬间就调用析构函数进行销毁 |
| // 场景2:new MyClass(2) | 手动执行delete heapObj的时候立刻调用析构 |
| // 场景3:static MyClass | 在main()函数执行完return 0之后、操作系统正式回收进程资源之前,由C++运行时的全局退出清理逻辑统一调用它的析构函数。 |
| // 场景4:passByValue(MyClass obj) | passByValue函数执行完、返回的瞬间就销毁 |
main里的局部对象a、ret、局部容器vec:比如场景5和场景6 | 在main()函数执行完return 0之后、操作系统正式回收进程资源之前,由C++运行时的全局退出清理逻辑统一调用它的析构函数。 |
调用顺序
上面已经介绍了单个类的构造函数和析构函数的调用规则,这也是它们在类中的声明周期。下面介绍的是在类继承的情况下,构造函数和析构函数的调用顺序。
单继承顺序
构造时,子类对象包含基类子对象,所以必须先把基类部分初始化好,子类才能安全使用基类成员。析构时反过来,子类资源通常依赖基类资源,所以必须先清理子类自己的资源,最后再清理基类部分。所以顺序如下:
构造:先基类,后子类。
析构:先子类,后基类。
也可以计算机的栈的工作机制来理解:
构造时:先把基类(构造)资源压入栈,再把子类(构造)资源压入栈;
析构时:只有先把子类(构造)资源从栈中释放掉,才能继续把基类(构造)资源释放掉。
#include <iostream> class Base { public: Base() { std::cout << "Base 构造\n"; } ~Base() { std::cout << "Base 析构\n"; } }; class Derived : public Base { public: Derived() { std::cout << "Derived 构造\n"; } ~Derived() { std::cout << "Derived 析构\n"; } }; int main() { Derived d; } /*打印输出如下*** Base 构造 Derived 构造 Derived 析构 Base 析构 ****************/如果是多层级继承,也是上面的规则,如下:
class Derived : public Base1, public Base2 { ... }; //构造:Base1 → Base2 → Derived //析构:Derived → Base2 → Base1类的多态
使用统一的接口处理不同的数据类型。
运行时多态(动态多态):虚函数机制
QT开发中,经常接触的有重写虚函数,比如事件过滤器eventFilter()是QObject类中已经声明了虚函数,
virtual bool eventFilter(QObject *watched, QEvent *event);默认实现是返回false(表示不处理事件,继续传递),如果需要拦截某个对象的事件,需要重写(override)eventFilter()
protected: bool eventFilter(QObject *watched, QEvent *event) override;//重写事件过滤器在基类中virtual 标明的虚函数,目的规范接口的统一入口,引用的类需要自己具体实现业务逻辑。
继承+多态可以有以下一个现象:
A类继承了base类,重写了event函数,B类继承了A类,B类也可以重写event函数。
有一种多重套娃的感觉,但是如果在A类中重写event函数时,加上final,就可以防止B类进一步重写。
class A { // final防止进一步重写 void event() override final { cout << "Derived(final)" << endl; } }编译时多态(静态多态):函数重载、运算符重载、模板
函数重载
同一函数名,参数类型不同
class Calculator { public: // 同一函数名,不同参数列表 int add(int a, int b) { cout << "整数加法: "; return a + b; } double add(double a, double b) { cout << "浮点数加法: "; return a + b; } string add(const string& a, const string& b) { cout << "字符串连接: "; return a + b; } int add(int a, int b, int c) { cout << "三个整数加法: "; return a + b + c; } }; // 使用 Calculator calc; cout << calc.add(5, 3) << endl; // 整数加法: 8 cout << calc.add(2.5, 3.7) << endl; // 浮点数加法: 6.2 cout << calc.add("Hello", "World") << endl; // 字符串连接: HelloWorld cout << calc.add(1, 2, 3) << endl; // 三个整数加法: 6运算符重载
对常用的运算符"+"、”-“、”==“等进行重载
class Complex { private: double real, imag; public: Complex(double r = 0, double i = 0) : real(r), imag(i) {} // 运算符重载 Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); } Complex operator-(const Complex& other) const { return Complex(real - other.real, imag - other.imag); } // 关系运算符重载 bool operator==(const Complex& other) const { return real == other.real && imag == other.imag; } // 输出运算符重载(友元函数) friend ostream& operator<<(ostream& os, const Complex& c) { os << c.real << " + " << c.imag << "i"; return os; } }; // 使用 Complex c1(3, 4), c2(1, 2); Complex c3 = c1 + c2; // 多态:+运算符根据操作数类型执行不同操作 cout << c3 << endl; // 输出: 4 + 6i模板
需要进行多种类型的运算时,抽象出一个模板比函数重载更好管理。
// 没有模板 - 需要为每种类型重载函数 int max(int a, int b) { return (a > b) ? a : b; } double max(double a, double b) { return (a > b) ? a : b; } string max(const string& a, const string& b) { return (a > b) ? a : b; } // 有模板 - 一个模板处理所有类型 template<typename T> T max(T a, T b) { return (a > b) ? a : b; }类中的元素也可以做成模板
template<typename T> class Stack { private: vector<T> elements; public: void push(const T& value) { elements.push_back(value); } T pop() { if (elements.empty()) { throw out_of_range("Stack is empty"); } T value = elements.back(); elements.pop_back(); return value; } T top() const { if (elements.empty()) { throw out_of_range("Stack is empty"); } return elements.back(); } bool empty() const { return elements.empty(); } size_t size() const { return elements.size(); } }; // 使用 int main() { Stack<int> intStack; intStack.push(1); intStack.push(2); intStack.push(3); while (!intStack.empty()) { cout << intStack.pop() << " "; // 输出: 3 2 1 } cout << endl; Stack<string> stringStack; stringStack.push("World"); stringStack.push("Hello"); cout << stringStack.pop() << " " << stringStack.pop() << endl; // Hello World return 0; }