C++ 和 C 语言实现 Stack 对比

面向对象三大特性:封装、继承、多态。通过下面两份代码的对比,我们可以初步感受一下"封装"到底好在哪里。


目录

[1 先说结论:变的是形式,不变的是逻辑](#1 先说结论:变的是形式,不变的是逻辑)

[2 C 语言实现 Stack](#2 C 语言实现 Stack)

[C 语言版本的痛点](#C 语言版本的痛点)

[3 C++ 实现 Stack(入门版,含构造/析构)](#3 C++ 实现 Stack(入门版,含构造/析构))

[4 逐条对比:C++ 到底好在哪里](#4 逐条对比:C++ 到底好在哪里)

[变化 1:数据被保护起来了(封装的核心)](#变化 1:数据被保护起来了(封装的核心))

[变化 2:不再需要手动传结构体地址](#变化 2:不再需要手动传结构体地址)

[变化 3:Init 和 Destroy 再也不会忘](#变化 3:Init 和 Destroy 再也不会忘)

[变化 4:类型使用更方便](#变化 4:类型使用更方便)

[变化 5:缺省参数让初始化更灵活](#变化 5:缺省参数让初始化更灵活)

[5 拷贝构造的坑:浅拷贝 vs 深拷贝](#5 拷贝构造的坑:浅拷贝 vs 深拷贝)

问题场景

解决方案:自己写深拷贝构造函数

[6 MyQueue:自动组合的威力](#6 MyQueue:自动组合的威力)

[7 括号匹配问题:C vs C++ 的代码量对比](#7 括号匹配问题:C vs C++ 的代码量对比)

[C++ 版本(简洁)](#C++ 版本(简洁))

[C 语言版本(繁琐)](#C 语言版本(繁琐))

[8 面试常问问题整理](#8 面试常问问题整理)

Q1:构造函数和析构函数的调用顺序是什么?

Q2:什么叫默认构造函数?

Q3:拷贝构造的参数为什么必须是引用?

Q4:什么时候用深拷贝,什么时候用浅拷贝?

[Q5:Date d3(); 这行代码是什么意思?](#Q5:Date d3(); 这行代码是什么意思?)

Q6:赋值运算符重载和拷贝构造的区别?

[4.9 总结:C++ 的封装到底好在哪里](#4.9 总结:C++ 的封装到底好在哪里)


1 先说结论:变的是形式,不变的是逻辑

很多同学看到 C++ 的 Stack 之后会觉得"变了好多",但其实底层逻辑完全一样:动态数组 + top 指针 + realloc 扩容。真正变的是"数据和函数的组织方式"。

用一句话概括就是:

C 语言:数据和操作是分离的,你要自己把结构体地址传来传去。 C++:数据和操作被封装进类,对象自己知道自己的数据。


2 C 语言实现 Stack

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int STDataType;

typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

void STInit(ST* ps)
{
    assert(ps);
    ps->a = NULL;
    ps->top = 0;
    ps->capacity = 0;
}

void STDestroy(ST* ps)
{
    assert(ps);
    free(ps->a);
    ps->a = NULL;
    ps->top = ps->capacity = 0;
}

void STPush(ST* ps, STDataType x)
{
    assert(ps);
    if (ps->top == ps->capacity)
    {
        int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
        if (tmp == NULL)
        {
            perror("realloc fail");
            return;
        }
        ps->a = tmp;
        ps->capacity = newcapacity;
    }
    ps->a[ps->top] = x;
    ps->top++;
}

bool STEmpty(ST* ps)
{
    assert(ps);
    return ps->top == 0;
}

void STPop(ST* ps)
{
    assert(ps);
    assert(!STEmpty(ps));
    ps->top--;
}

STDataType STTop(ST* ps)
{
    assert(ps);
    assert(!STEmpty(ps));
    return ps->a[ps->top - 1];
}

int STSize(ST* ps)
{
    assert(ps);
    return ps->top;
}

int main()
{
    ST s;
    STInit(&s);   // ⚠️ 必须手动初始化

    STPush(&s, 1);
    STPush(&s, 2);
    STPush(&s, 3);
    STPush(&s, 4);

    while (!STEmpty(&s))
    {
        printf("%d\n", STTop(&s));
        STPop(&s);
    }

    STDestroy(&s);  // ⚠️ 必须手动销毁,否则内存泄漏

    return 0;
}

C 语言版本的痛点

问题 说明
每个函数都要传 ST* 因为数据和函数是分离的,函数不知道操作谁
忘记调用 STInit 程序直接崩溃,a 指针是野指针
忘记调用 STDestroy 内存泄漏,堆上的内存永远回不来
typedef struct Stack ST 使用类型还要 typedef,否则每次要写 struct Stack

3 C++ 实现 Stack(入门版,含构造/析构)

cpp 复制代码
#include <iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    // 构造函数:对象创建时自动调用,替代 STInit
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc 申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }

    // 析构函数:对象销毁时自动调用,替代 STDestroy
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _a = tmp;
            _capacity = newcapacity;
        }
        _a[_top++] = x;
    }

    void Pop()
    {
        assert(_top > 0);
        --_top;
    }

    bool Empty()
    {
        return _top == 0;
    }

    STDataType Top()
    {
        assert(_top > 0);
        return _a[_top - 1];
    }

    int Size()
    {
        return _top;
    }

private:
    // 成员变量:只有类内部可以访问
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack st;        // ✅ 自动调用构造函数,不需要手动 Init
    st.Push(1);
    st.Push(2);
    st.Push(3);
    st.Push(4);

    while (!st.Empty())
    {
        cout << st.Top() << endl;
        st.Pop();
    }

    // ✅ 函数结束,st 生命周期到了,自动调用析构函数释放内存
    // 不需要手动 Destroy
    return 0;
}

4 逐条对比:C++ 到底好在哪里

变化 1:数据被保护起来了(封装的核心)

html 复制代码
// C 语言:谁都能改 top,想怎么改怎么改
ST s;
STInit(&s);
s.top = -100;  // 合法!但会导致程序崩溃
html 复制代码
// C++:_top 是 private,外部根本访问不到
Stack st;
st._top = -100;  // ❌ 编译报错!不允许访问私有成员

这就是封装的本质:不是藏着掖着,而是通过访问限定符建立规范,防止数据被随意破坏。


变化 2:不再需要手动传结构体地址

html 复制代码
// C 语言:每个函数都要传 ST*
STPush(&s, 1);
STEmpty(&s);
STTop(&s);
html 复制代码
// C++:this 指针隐式传递,对象自己知道自己是谁
st.Push(1);
st.Empty();
st.Top();

st.Push(1) 本质上等价于 Push(&st, 1),只不过编译器帮你做了,写起来更自然。


变化 3:Init 和 Destroy 再也不会忘

复制代码
// C 语言新手最常犯的两个错误:
ST s;
// 忘写 STInit(&s);  → _a 是野指针,Push 直接崩
STPush(&s, 1);
// 函数结束忘写 STDestroy(&s); → 内存泄漏

// C++:构造函数保证初始化,析构函数保证清理
// 两件事都是自动的,想忘都忘不了
Stack st;    // 自动初始化
// ...
// 函数结束,自动析构

这是 C++ 相比 C 最实用的提升之一,在实际工程中能避免大量 bug。


变化 4:类型使用更方便

复制代码
// C 语言必须 typedef,否则每次要写 struct Stack
typedef struct Stack { ... } ST;

// C++ 中 class/struct 定义的类型,类名本身就是类型名
class Stack { ... };
Stack st;   // 直接用,不需要 typedef

变化 5:缺省参数让初始化更灵活

复制代码
// C 语言:STInit 里 capacity 写死了,或者要多写一个带参数版本
void STInit(ST* ps) { ps->capacity = 0; }

// C++:构造函数带默认参数
Stack st;       // 默认容量 4
Stack st2(10);  // 指定初始容量 10

5 拷贝构造的坑:浅拷贝 vs 深拷贝

这是面试必考点,也是新手最容易踩的坑。

问题场景

复制代码
Stack st1;
st1.Push(1);
st1.Push(2);

Stack st2 = st1;  // ⚠️ 如果没有自己写拷贝构造,会发生什么?

编译器默认生成的拷贝构造是浅拷贝 ------把 st1 的每个成员原样复制给 st2

复制代码
st1._a ──────→ [ 1 | 2 | _ | _ ]
                      ↑
st2._a ──────→ 同一块内存!

当程序结束,st1st2 都调用析构函数,对同一块内存 free 两次,直接崩溃

解决方案:自己写深拷贝构造函数

cpp 复制代码
Stack(const Stack& st)
{
    // 重新申请一块同样大的空间
    _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
    if (nullptr == _a)
    {
        perror("malloc 申请空间失败");
        return;
    }
    // 把数据复制过来
    memcpy(_a, st._a, sizeof(STDataType) * st._top);
    _top = st._top;
    _capacity = st._capacity;
}

深拷贝之后:

复制代码
st1._a ──────→ [ 1 | 2 | _ | _ ]   ← st1 独享
st2._a ──────→ [ 1 | 2 | _ | _ ]   ← st2 独享,两块不同的内存

记忆技巧 :如果一个类显式写了析构函数并释放了资源 ,那它几乎一定也需要写拷贝构造赋值运算符重载 。这三个往往同时出现,业界称之为"Rule of Three(三五法则)"。


6 MyQueue:自动组合的威力

用两个 Stack 实现队列时,C++ 的自动化优势更加明显。

html 复制代码
class MyQueue
{
public:
    // 什么都不写!
    // 编译器自动生成构造函数,调用 Stack 的构造函数初始化 pushst 和 popst
    // 编译器自动生成析构函数,调用 Stack 的析构函数释放资源
    // 编译器自动生成拷贝构造,调用 Stack 的深拷贝构造
private:
    Stack pushst;
    Stack popst;
};

int main()
{
    MyQueue mq;   // pushst 和 popst 都被自动初始化了
    MyQueue mq2 = mq;  // 深拷贝,两个 Stack 都被正确复制

    return 0;  // 所有资源自动释放
}

在 C 语言里,如果你用两个 ST 实现队列,你需要手动 Init 两个、手动 Destroy 两个,顺序还不能搞错。C++ 的构造/析构机制把这些全部自动化了。


7 括号匹配问题:C vs C++ 的代码量对比

这是一个经典的 LeetCode 题(20. Valid Parentheses),用来直观感受两个版本的差距。

C++ 版本(简洁)

cpp 复制代码
bool isValid(const char* s)
{
    Stack st;    // 自动初始化

    while (*s)
    {
        if (*s == '[' || *s == '(' || *s == '{')
        {
            st.Push(*s);
        }
        else
        {
            if (st.Empty())
                return false;  // ✅ 直接 return,不用担心内存泄漏

            char top = st.Top();
            st.Pop();

            if ((*s == ']' && top != '[')
             || (*s == '}' && top != '{')
             || (*s == ')' && top != '('))
                return false;  // ✅ 直接 return,析构自动处理
        }
        ++s;
    }

    return st.Empty();  // 函数结束,st 自动析构
}

C 语言版本(繁琐)

cpp 复制代码
bool isValid(const char* s)
{
    ST st;
    STInit(&st);   // 必须手动初始化

    while (*s)
    {
        if (*s == '(' || *s == '[' || *s == '{')
        {
            STPush(&st, *s);
        }
        else
        {
            if (STEmpty(&st))
            {
                STDestroy(&st);   // ⚠️ return 前必须手动销毁
                return false;
            }

            char top = STTop(&st);
            STPop(&st);

            if ((top == '(' && *s != ')')
             || (top == '{' && *s != '}')
             || (top == '[' && *s != ']'))
            {
                STDestroy(&st);   // ⚠️ 又一个 return,又要手动销毁
                return false;
            }
        }
        ++s;
    }

    bool ret = STEmpty(&st);
    STDestroy(&st);   // ⚠️ 正常 return 也要手动销毁

    return ret;
}

C 语言版本有 3 个 return,每一个都要写 STDestroy,漏掉任何一个就是内存泄漏。 C++ 版本完全不用操心这件事。


8 面试常问问题整理

Q1:构造函数和析构函数的调用顺序是什么?

cpp 复制代码
int main()
{
    Stack st1;  // 先构造 st1
    Stack st2;  // 再构造 st2

    return 0;
    // 先析构 st2(后构造的先析构)
    // 再析构 st1
}

结论:同一个局部域内,后定义的对象先析构(类似栈的 LIFO 顺序)。


Q2:什么叫默认构造函数?

很多人认为只有"编译器自动生成的那个"才叫默认构造。这是错的!

默认构造函数是指:不传参数就能调用的构造函数,包含三种:

  1. 无参构造函数 Stack() {}
  2. 全缺省构造函数 Stack(int n = 4) {}
  3. 编译器自动生成的构造函数

这三种有且只能有一个存在。无参和全缺省虽然构成重载,但调用时会产生歧义,编译器报错。


Q3:拷贝构造的参数为什么必须是引用?

复制代码
// 错误写法
Date(Date d)  { ... }  // ❌ 编译直接报错

// 正确写法
Date(const Date& d)  { ... }  // ✅

原因:如果参数是值传递,调用拷贝构造时需要先拷贝实参给形参,拷贝这个操作本身又要调用拷贝构造,无限递归,直到栈溢出。


Q4:什么时候用深拷贝,什么时候用浅拷贝?

情况 方案
成员变量全是内置类型,且没有指向堆上资源(如 Date 编译器自动生成的浅拷贝就够用
有成员指针指向堆上资源(如 Stack 里的 _a 必须自己写深拷贝
成员变量是其他自定义类型(如 MyQueue 包含 Stack 编译器自动调用成员的拷贝构造,不用自己写

记忆技巧显式写了析构 → 一定要写拷贝构造 → 一定要写赋值运算符重载。


Q5:Date d3(); 这行代码是什么意思?

复制代码
Date d1;    // ✅ 调用无参构造函数,创建对象 d1
Date d3();  // ⚠️ 这是函数声明!不是创建对象!
            // 编译器认为你在声明一个叫 d3、无参、返回 Date 的函数

结论:用无参构造函数创建对象时,后面不能加括号。


Q6:赋值运算符重载和拷贝构造的区别?

复制代码
Date d1(2024, 7, 5);

Date d2 = d1;   // 拷贝构造!d2 是新创建的对象
Date d3(d1);    // 拷贝构造!完全等价

Date d4(2024, 8, 1);
d4 = d1;        // 赋值运算符重载!d4 已经存在,是两个已存在对象之间的赋值

记忆方法

  • 拷贝构造 :用一个已有对象初始化 一个对象
  • 赋值运算符 :两个都已经存在的对象之间赋值

4.9 总结:C++ 的封装到底好在哪里

维度 C 语言 C++
数据安全性 任何人都能随意修改结构体成员 private 保护,只能通过成员函数访问
初始化 手动调用 Init,容易忘 构造函数自动调用,忘不了
资源释放 手动调用 Destroy,每个 return 都要写 析构函数自动调用,不会漏
函数调用 每次都要传结构体地址 &s this 指针隐式传递,调用更自然
类型使用 需要 typedef struct 简化 类名直接就是类型名
扩展性 函数和类型是松散关系 高内聚,方便维护和扩展

在入门阶段,C++ 的 Stack 看起来变化很大,但实质上变化不大。等我们后面学习 STL 中用适配器实现的 Stack,才能真正感受到 C++ 抽象和复用的威力。


下一章:初始化列表、static 成员、友元函数

相关推荐
WolfGang0073211 天前
代码随想录算法训练营 Day38 | 动态规划 part11
算法·动态规划
松☆1 天前
C++ 算法竞赛题解:P13569 [CCPC 2024 重庆站] osu!mania —— 浮点数精度陷阱与 `eps` 的深度解析
开发语言·c++·算法
(Charon)1 天前
【C++/Qt】C++/Qt 实现 TCP Server:支持启动监听、消息收发、日志保存
c++·qt·tcp/ip
爱编码的小八嘎1 天前
C语言完美演绎8-10
c语言
jr-create(•̀⌄•́)1 天前
正则化和优化算法区别
pytorch·深度学习·神经网络·算法
并不喜欢吃鱼1 天前
从零开始C++----七.继承及相关模型和底层(上篇)
开发语言·c++
li星野1 天前
刷题:数组
数据结构·算法
tankeven1 天前
HJ182 画展布置
c++·算法
W23035765731 天前
【改进版】C++ 固定线程池实现:基于调用者运行的拒绝策略优化
开发语言·c++·线程池
谭欣辰1 天前
C++ 控制台跑酷小游戏
c++·游戏