当前位置: 首页 > backend >正文

C++ 类和对象(1)

我们在学习完C++的入门基础之后,就要开始学习C++的下一板块的内容---类和对象。类和对象是 C++ 面向对象编程的灵魂,是从“写简单逻辑”到“开发复杂项目”的分水岭——掌握类和对象,才算真正入门 C++ 的核心语法。下面让我们进入内核对象的章节:

1. 类的定义

在 C++ 中,类(Class) 是面向对象编程(OOP)的核心概念,它是对现实世界中事物的抽象描述,定义了事物的属性(数据)和行为(操作)。

1.1 类的本质

  • 类的本质:封装数据与行为
  • 类( class )是 C++ 面向对象编程的核心,它的作用是 “封装”:把一组相关的数据(属性)和操作(函数)打包在一起,形成一个独立的“自定义类型”。
  • 比如“栈( Stack )”类:
  • - 数据(属性): array (存储元素的数组)、 capacity (栈容量)、 top (栈顶位置)。
  • - 操作(函数): Init (初始化)、 Push (入栈)、 Top (取栈顶)、 Destroy (销毁)。
  • 类本身 不占用内存空间,它更像一个“模板”或“蓝图”,描述了一类事物共有的属性和行为。例如:
  • - “汽车”可以抽象为一个类,属性包括颜色、排量、品牌等,行为包括启动、加速、刹车等。
  • - 这个类本身不代表某一辆具体的车,而是所有车的通用特征集合。

1.2 类的定义格式(语法与规范)

1. 基本语法

用 class 关键字定义类,格式:

class 类名 
{// 访问限定符(public/private/protected) + 成员(变量/函数)
};  // 注意:类定义结束必须有分号!

示例(栈类):

class Stack 
{
public:  // 访问限定符:public(公开成员)// 成员函数(操作)void Init(int n = 4) { /* 初始化逻辑 */ }void Push(int x) { /* 入栈逻辑 */ }private:  // 访问限定符:private(私有成员)// 成员变量(数据)int* array;     size_t capacity;size_t top;     
}; // 分号不能省略!

2. 类名与成员命名规范

  1. 类名:采用“大驼峰”命名(如  Stack 、 Date ),首字母大写,单词间无下划线。
  2. 成员变量:为区分普通变量,习惯加前缀(如  _array 、 m_capacity ),但不是语法要求,看团队规范。
  3. 成员函数:采用“小驼峰”或“大驼峰”,如  Init 、 Push 。

3.  class  vs  struct  的区别(C++ 中)

  • 默认访问权限:
  • -  class :未加访问限定符的成员,默认是  private (私有)。
  • -  struct :未加访问限定符的成员,默认是  public (公开)。
  • 使用场景:
  • -  class  用于需要严格封装的场景(如  Stack 、 Date )。
  • -  struct  常用于“轻封装”(如简单数据集合,或兼容 C 语言代码)。

1.3 访问限定符

访问限定符:控制成员的可见性

C++ 用 访问限定符 控制类成员的“可见范围”,实现封装。有 3 种限定符:

访问限定符作用域内可见性类外访问权限继承中的表现(后续学)
public类内、类外(通过对象)可见可直接访问( 对象.成员 )子类可访问
private 仅类内可见 类外无法访问(编译报错)子类不可访问(默认)
protected类内、子类可见(继承场景)类外无法访问(编译报错)子类可访问 

1. 核心规则

- 访问权限 从限定符出现的位置开始,到下一个限定符或类结束。
-  class  默认是  private , struct  默认是  public 。

2. 示例:

class Date 
{
public:  // 从这里开始,到下一个限定符前,成员是 publicvoid Init(int year, int month, int day) {_year = year;   // 类内可访问 private 成员_month = month;_day = day;}private:  // 从这里开始,到类结束,成员是 privateint _year;    //为区分普通变量,习惯加前缀(如 _array , m_capacity )int _month;int _day;
};int main() 
{Date d;d.Init(2024, 8, 5); // 合法:public 函数可访问// d._year = 2025;   // 错误:private 成员,类外无法访问return 0;
}

1.4 类域

类定义了一个新的作用域(类域),类的所有成员(变量、函数)都属于这个作用域。

1. 类域的核心规则

  • 类内定义成员:直接写成员名(如  Init  函数内访问  _year )。
  • 类外定义成员:必须用  类名::成员名  指明作用域(如  Stack::Init )。

2. 示例:类外定义成员函数

class Stack 
{
public:void Init(int n = 4); // 类内声明private:int* array;size_t capacity;size_t top;
};// 类外定义成员函数:必须用 'Stack::' 指明类域
void Stack::Init(int n) 
{  array = (int*)malloc(sizeof(int) * n); //类域内,可直接访问 private 成员if (array == nullptr) {perror("malloc 失败");return;}capacity = n;top = 0;
}int main() 
{Stack st;st.Init(); // 调用 public 函数return 0;
}

3. 类域的意义

  • 避免命名冲突:不同类的成员名可重复(如  Stack  和  Date  都有  Init  函数)。
  • 明确成员归属:通过  类名::  清晰区分“全局函数”和“类成员函数”。
  • 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
  • 类域影响的是编译的查找规则,程序中 Init 如果不指定类域 Stack ,那么编译器就把 Init当成全局函数,那么编译时,找不到 array 等成员的声明/定义在哪里,就会报错。指定类域 Stack ,就是知道 Init 是成员函数,当前域找不到 array 等成员,就会到类域中去查找。

1.5 类的完整示例

结合前面知识,看一个完整的 Stack 类实现,包含:

  1. 成员变量( private )。
  2. 成员函数( public ,类内/类外定义)。
  3. 访问限定符、类域的使用。
#include <iostream>
#include <cstdlib>  // malloc/free
#include <cassert>  // assert
using namespace std;class Stack 
{
public:  // public 成员:类外可访问(通过对象)// 类内声明,类外定义(需用 Stack::Init)void Init(int n = 4);  void Push(int x) {       // 类内定义(短小函数,可直接写逻辑)// 扩容逻辑(简化,实际需更严谨)if (top == capacity) {capacity *= 2;int* newArr = (int*)malloc(sizeof(int) * capacity);for (size_t i = 0; i < top; ++i) {newArr[i] = array[i];}free(array);array = newArr;}array[top++] = x;}int Top() const {   // const 成员函数(后续讲,标记“只读”)assert(top > 0); // 断言:栈非空return array[top - 1];}void Destroy() {  // 释放资源free(array);array = nullptr;capacity = 0;top = 0;}private:                   // private 成员:仅类内可访问int* array = nullptr;  // 栈空间size_t capacity = 0;   // 容量size_t top = 0;        // 栈顶(下一个插入位置)
};// 类外定义成员函数:需指定类域 Stack::
void Stack::Init(int n) 
{array = (int*)malloc(sizeof(int) * n);if (array == nullptr) {perror("malloc 申请空间失败");return;}capacity = n;top = 0;
}int main() 
{Stack st;st.Init(10);  // 调用 public 函数,初始化栈(容量 10)st.Push(1);   // 入栈st.Push(2);st.Push(3);cout << "栈顶元素:" << st.Top() << endl; // 输出 3st.Destroy(); // 释放资源return 0;
}

1.6 补充

类定义内的成员函数(如  Push 、Top ),编译器会默认视为  inline(内联函数),会尝试在调用处展开,减少开销。

class Stack 
{
public:void Push(int x) {  // 函数体直接写在类内 → 编译器默认 inlinearray[top++] = x; }
};

如果函数体复杂,编译器可能忽略  inline ,按普通函数处理(和之前讲的内联函数规则一致)。

1.7 总结

类通过 “封装” 实现“数据隐藏”和“逻辑复用”:

  • 数据隐藏:用  private  隐藏敏感成员(如  Stack  的  array ),只暴露  public  接口(如  Push 、 Top )。
  • 逻辑复用:类的成员函数封装通用逻辑(如  Init  初始化、 Destroy  销毁),避免重复写代码。

 访问限定符( public/private )是“封装”的实现手段,类域( 类名:: )是成员的作用范围,这些特性共同支撑 C++ 的面向对象编程。

后续学习继承、多态时,会更深刻体会类的设计——现在先掌握“封装”的基础,就能理解复杂类的结构!

2. 类的实例化

2.1 定义

用类类型在物理内存里创建对象的过程,就是类实例化出对象 。类是对对象的抽象描述,像“设计图”,限定成员变量(仅声明,未分配空间 );实例化出的对象,才真正在内存分配空间存储数据,好比按“设计图”造出能住人的“房子”。

2.2 特性

  1. 空间分配差异:类仅作抽象模型,成员变量无实际空间;实例化对象时,为成员变量分配物理内存,让数据有存储处。
  2. 多对象创建:一个类可实例化多个对象,每个对象占独立物理空间存各自成员变量数据,就像一套建筑设计图能造出多栋房子,每栋房子(对象 )存不同住户(数据 )。

2.3 实例

#include<iostream>
using namespace std;class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}//private:int _year;   // 声明int _month;int _day;
};int main()
{//Data类实例化出对象d1和d2Date d1;Date d2;d1.Init(2025, 8, 5);d1.Print();d2.Init(2025, 7, 5);d2.Print();}

1. 类与实例化基础关联

  • Date  类是“设计图”,定义了  _year / _month / _day  成员变量(仅声明,未占实际空间 ),还包含  Init(初始化)、 Print(输出)函数。 main 里 Date d1; Date d2; 是实例化,让  d1 / d2  成为真实对象,分配内存存各自数据。

2. 函数作用与执行逻辑

Init  函数(初始化逻辑)

  • 功能:给对象的成员变量赋值,把外部传入的  year / month / day ,存到对象自己的  _year / _month / _day  里。
  • 关联实例化:类本身不存数据,实例化出  d1 / d2  后,通过  d1.Init(2024, 3, 31);  调用,为对象分配的内存填充数据,让“设计图”造出的“房子”(对象 )有实际“家具”(数据 )。

 Print  函数(输出逻辑)

  • 功能:按  年/月/日  格式,把对象里存的  _year / _month / _day  打印出来。
  • 关联实例化:实例化出的对象( d1 / d2  ),通过  d1.Print();  调用,读取对象内存里的数据并展示,验证初始化是否成功,体现“房子”(对象 )能“住人”(存数据、用数据 )的价值。

3. 总结:函数是实例化对象的“操作工具”

  • 类里的函数,是给实例化对象用的“工具”:
  • Init  负责填充数据,让对象从“空房子”(仅分配内存 )变成“有家具的房子”(存了具体日期 );
  • Print  负责使用数据,把对象存的数据展示出来,体现实例化对象“能存、能用”的意义,让类从“设计图”真正落地成“可用的房子”(对象 )。

 简单说:实例化让对象“存在”,函数让对象“能用”,二者配合实现类从“抽象模型”到“实用实体”的完整逻辑 。

类是“蓝图”,实例化是“按蓝图造实物”,实物(对象 )存数据、占空间,类本身不存具体数据,这就是类实例化的核心逻辑 。

3. 类的对象大小

类的对象大小,指的是类实例化出的对象在内存中占用的字节数,它由类里的成员变量、内存对齐规则,以及空类特殊处理逻辑共同决定:

那我们分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year /_month /_day存储各自的数据,但是d1和d2的成员函数 Init/Print 指针却是一样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外啰嗦一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。

3.1 核心逻辑

  • 核心逻辑:对象只存“成员变量”
  • 类实例化的对象,仅存储「成员变量」(成员函数存放在代码段,所有对象共享,不占单个对象内存)。
  • 对象大小 = 成员变量总大小 + 内存对齐填充 + 空类占位(空类特殊规则),核心受 内存对齐规则 约束。

3.2 影响因素

1. 成员变量本身

成员变量的类型和数量直接影响,比如下面代码中 class A  里 char _ch(占1字节) + int _i(占4字节 ),理论上至少占 5 字节,但内存对齐会让实际更大。

class A
{
public:void Print(){cout << _ch << endl;}
private:char _ch;int _i;
};

2. 内存对齐规则(以 VS 编译器默认对齐数 8 为例)

  • 规则 1:第一个成员从偏移量 0 开始。
  • 规则 2:其他成员要对齐到 min(成员大小, 默认对齐数)  的整数倍地址。如 class A  中, _ch  存在偏移 0, _i  需对齐到 4 的倍数(因  min(4,8)=4  ), _ch  占 1 字节后,要填充 3 字节让  _i  从偏移 4 开始存。
  • 规则 3:对象总大小是最大对齐数(所有成员对齐数里最大的 )的整数倍。 class A  最大对齐数是 4,总大小 8 是 4 的倍数,所以最终占 8 字节。

3. 空类特殊处理

class B
{
public:void Print(){//...}
};class C
{};
  • 空类(无成员变量)实例化的对象,C++ 规定占 1 字节,用来占位标识对象存在,否则无法区分“有没有创建对象”,像  class B 和 class C  的对象大小都是 1 字节。

3.3 总结

类的对象大小,是成员变量在内存对齐规则下实际占用的字节数,加上空类占位的特殊处理,本质是“对象在物理内存里实实在在占了多少空间”,它体现了类从“抽象设计”到“具体实物”的内存占用结果,让我们清楚实例化对象对内存的消耗 。

4. this指针

在 C++ 里, this指针 是很关键的概念,专门用来处理对象和类成员之间的关联。

4.1 this指针的本质与作用

1. 本质:隐含的指针参数

  • 每个非静态成员函数(普通成员函数)里,编译器会悄悄传入一个指针参数,这个指针就是 this ,它指向当前调用该函数的对象。比如之前代码中的 d1.Init(2025, 8, 5);  调用  Init  函数时,编译器实际传了 &d1 给 this指针,让函数知道是哪个对象在调用它。
  • d1.Print(&d1, 2025, 8, 5);
  • 它是编译器自动处理的,写代码时不用显式声明,但在函数内部能直接用,帮我们区分“成员变量”和“函数参数/局部变量”。

2. 核心作用:区分对象,操作当前对象数据

类里成员变量和函数参数可能同名(像  Init 里的 year 和类的 _year ), this 能明确指定“当前对象的成员变量”。看代码:

void Init(int year, int month, int day) 
{// this->_year 明确表示当前对象的 _year 成员变量this->_year = year; this->_month = month;this->_day = day;
}

这里 this->_year 就是告诉编译器,把传入的 year 值赋给当前调用  Init 函数的对象的 _year  成员变量。要是没有 this ,编译器分不清是用传入的  year  还是类的 _year(虽然上面代码省略 this 也能运行,因为编译器默认会找成员变量,但同名时必须用 this 区分 )。

4.2 this指针的存在时机与传递

1. 存在时机:成员函数调用时

  •  this 只在非静态成员函数执行期间有效。当对象调用成员函数时,编译器在编译阶段,会把 this 作为隐含参数传给函数,函数里通过  this 访问当前对象的数据。一旦函数执行结束,this 指针的作用域也随之结束(不过指针指向的对象只要没销毁,内存还在 )。

2. 传递方式:编译器隐式处理

  • 写代码时,不用手动传 this 指针。比如  d1.Init(2025, 8, 5);  ,编译器实际处理成类似  Init(&d1, 2025, 8, 5);  的形式(伪代码,体现  this  传的是 d1 的地址 ),让函数内部能通过 this 操作 d1 的成员。

3. 显示位置

  • C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。

 4.3 this指针的特点

1. 类型:指向当前类类型的指针

对于 class Date , this 的类型是 Date* (如果是  const  成员函数,就是  const Date*  ,保证不能通过 this 修改对象数据 )。它严格指向当前调用函数的对象,类型和类绑定。

2. 不可修改性

this  指针本身的值(即指向的对象地址 ),在函数内部是不能被修改的。它的作用就是固定指向当前对象,让函数精准操作该对象的数据,你没法给 this 重新赋值让它指向别的对象。比如:

void Init(int year, int month, int day) 
{// 错误!不能修改 this 指针本身的值this = new Date; this->_year = year;// ...
}

 这样写编译器会报错,因为 this 的指向是编译器确定好的,不能手动改。

3. 空指针调用成员函数的问题

如果用空指针( NULL  或  nullptr  )调用成员函数,若函数内部没用 this 访问成员变量,程序可能不会崩溃(但这种写法本身危险 );可一旦用 this 访问了成员变量,就会触发解引用空指针,程序直接崩溃。比如:

class Date
{
public:void Print() {// 这里没访问成员变量,空指针调用可能不崩溃,但逻辑非法cout << "Print function called" << endl; }void Init(int year) {// 空指针调用时,this 是 nullptr,解引用崩溃this->_year = year; }
};int main() 
{Date* p = nullptr;p->Print();  // 可能输出内容,但行为未定义(危险)p->Init(2025); // 解引用空指针,程序崩溃 return 0;
}

这就是因为 p 是空指针,调用 Init 时 this 是 nullptr,给 this->_year 赋值就相当于给空地址写数据,直接出错。

4.4 this指针与对象内存 , 实例化的关联

类实例化出对象后,每个对象有独立内存存成员变量。this 指针就是在成员函数里,关联到当前调用对象的内存地址,让函数知道该操作哪个对象的数据。比如 d1 和 d2 是两个 Date 对象,调用 d1.Init(...)  时,this 指向 d1 的内存;调用 d2.Init(...)  时,this 指向 d2 的内存,这样就能精准给各自对象的成员变量赋值、操作,实现“同一套函数逻辑,处理不同对象数据” 。

4.5 this指针例题

  • 关键逻辑:指针 p 是 nullptr ,但调用的 Print 函数内部没有访问成员变量( _a  没被使用 )。C++ 中,通过空指针调用成员函数时,只要函数体里不访问成员变量(即不通过  this 指针解引用访问  _a  等 ),编译器不会报错,函数能正常调用执行。
  • 结论:程序会正常运行,输出 A::Print()  ,选 C 。

  • 关键逻辑:指针 p 是  nullptr ,调用 Print 函数时,函数体里执行 cout << _a << endl; ,这会通过 this 指针(此时 this 是 nullptr )解引用访问成员变量  _a ,触发空指针解引用错误。程序运行到这一步时,会因非法内存访问崩溃。
  • 结论:程序运行崩溃,选 B 。

  • this 指针是编译器在调用成员函数时,隐式传入的当前对象地址,作为函数的隐含参数存在。在函数调用过程中,它会被放在栈区(栈帧里 ),函数执行结束,栈帧销毁,this 指针也随之“失效”(但指向的对象可能还在其他内存区域 )。所以 this 指针存于栈区,选 A 。

4.6 总结

  • this  指针是 C++ 为成员函数和对象“牵线搭桥”的关键机制:
  • 它是编译器隐式传入成员函数的指针,指向当前调用函数的对象;
  • 主要用来区分同名变量、操作当前对象数据;
  • 有严格的存在时机和使用规则,涉及对象内存访问、空指针调用等场景时,得特别留意避免出错。
  • 理解它才能更清晰类的成员函数如何与具体对象交互,写出逻辑正确的面向对象代码。

5. 应用

在学习了今天的内容后,也就是学习了和类有关的基础知识后,我们来看一看C++和C语言实现Stack对比:

  • C++的面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。
  • 通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化。
  •  C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。
  • C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便
  • 在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等着我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力。

C实现Stack代码:

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>// 定义栈中存储的数据类型
typedef int STDatatype;
// 定义栈结构体
typedef struct Stack
{// 存储数据的动态数组STDatatype* a;// 栈顶指针(也可表示栈中元素个数,从 0 开始)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;
}

代码说明:

  • 1. 结构体定义: struct Stack 包含动态数组指针 a 、栈顶标识 top 和容量 capacity  。
  • 2. 核心操作:
  • STInit:初始化栈,动态数组置空,top 和 capacity 置 0 。
  • STPush:先检查扩容,再将数据放入栈顶并更新  top  。
  • STPop:直接移动 top 指针实现出栈(不真正销毁数据,后续入栈会覆盖 )。
  • STTop:返回栈顶元素(需保证栈非空 )。
  • STEmpty:通过 top == 0 判断栈是否为空 。
  • STDestroy:释放动态数组内存,重置栈状态 。
  • 3. 测试逻辑:main 函数中完成栈的初始化、入栈、遍历(输出 + 出栈 )和销毁,验证功能是否正常。 

这段代码完整实现了 C 语言中栈的基本操作,可直接在支持 C 语言的编译器(如  gcc  )中编译运行 。

C++实现Stack代码:

#include<iostream>
using namespace std;typedef int STDatatype;
class Stack
{
public:// 成员函数void Init(int n = 4){_a = (STDatatype*)malloc(sizeof(STDatatype) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 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;}int Top(){assert(_top > 0);return _a[_top - 1];}void Destroy(){free(_a);_a = nullptr;_top = _capacity = 0;}private:// 成员变量STDatatype* _a;size_t _capacity;size_t _top;
};int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);while (!s.Empty()){printf("%d\n", s.Top());s.Pop();}s.Destroy();return 0;
}

代码详细解释:
1. 类的定义与成员(栈的核心逻辑封装)

class Stack
{
public:// 成员函数(操作栈的接口)void Init(int n = 4);      // 初始化栈void Push(STDatatype x);   // 入栈void Pop();                // 出栈bool Empty();              // 判断栈空int Top();                 // 获取栈顶元素void Destroy();            // 销毁栈private:// 成员变量(栈的核心数据)STDatatype* _a;       // 动态数组,存储栈的实际数据size_t _capacity;     // 栈的容量(最多能存多少元素)size_t _top;          // 栈顶指针(也表示栈中有效元素个数)
};

设计思想:用类封装栈的操作,隐藏内部实现细节(成员变量  private ),仅暴露 Init / Push 等接口,体现 面向对象的“封装性” 。

2. 核心成员函数解析

1. Init : 初始化栈

void Init(int n = 4)
{_a = (STDatatype*)malloc(sizeof(STDatatype) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;
}

- 功能:动态分配内存( malloc  ),初始化栈的容量  _capacity  和栈顶  _top 。
- 细节: n = 4  是默认参数,调用时不传值则默认初始化容量为  4 ; _top = 0  表示栈初始为空(栈顶在数组起始位置 )。

2.  Push :入栈(数据压入栈)

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;
}

- 逻辑:
- 先判断栈是否满( _top == _capacity  ),满则用  realloc  扩容(容量翻倍 )。
- 把数据  x  放到  _a[_top] ,再通过  _top++  移动栈顶指针。
- 作用:保证栈空间不够时自动扩容,支持持续入栈。

3.  Pop :出栈(移除栈顶元素)

void Pop()
{assert(_top > 0);  // 断言:栈空时无法出栈--_top;
}

- 逻辑:直接移动栈顶指针( _top--  ),逻辑上“移除”栈顶元素(实际内存未销毁,后续入栈会覆盖 )。
- 断言: assert(_top > 0)  防止栈空时调用  Pop  崩溃,体现 “防御式编程”。

4.  Empty :判断栈是否为空

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

- 逻辑:栈顶指针  _top == 0  表示栈空,返回  true ;否则返回  false 。
- 作用:给外部提供判断依据(如  main  中循环遍历栈 )。

5.  Top :获取栈顶元素

int Top()
{assert(_top > 0);  // 断言:栈空时无栈顶元素return _a[_top - 1];
}

- 逻辑:返回数组中  _top - 1  位置的元素(栈顶元素的下标 )。
- 断言: assert(_top > 0)  防止栈空时访问非法内存。

6.  Destroy :销毁栈(释放资源)

void Destroy()
{free(_a);    // 释放动态数组内存_a = nullptr; // 置空指针,避免野指针_top = _capacity = 0; // 重置状态
}

- 作用:释放  malloc / realloc  申请的内存,防止内存泄漏;重置栈状态,避免后续非法访问。

3. main  函数:测试栈的功能 

int main()
{Stack s;s.Init();          // 初始化栈(默认容量 4)s.Push(1);         // 入栈:1s.Push(2);         // 入栈:2s.Push(3);         // 入栈:3s.Push(4);         // 入栈:4// 遍历栈:输出并出栈while (!s.Empty()) {printf("%d\n", s.Top()); // 输出栈顶s.Pop();                 // 出栈}s.Destroy();       // 销毁栈,释放内存return 0;
}

- 流程:
1. 定义栈对象  s ,调用  Init  初始化。
2. 连续入栈  1 、 2 、 3 、 4 。
3. 循环:判断栈非空( !s.Empty()  ),输出栈顶( s.Top()  )并出栈( s.Pop()  )。
4. 调用  Destroy  释放内存,避免泄漏。
- 输出结果:按“后进先出”顺序,依次打印  4 、 3 、 2 、 1 。

4. 代码整体设计思路

1. 封装性:用类隐藏栈的实现细节(动态数组、容量、栈顶指针 ),仅暴露简洁接口( Init / Push / Pop  等 ),让外部调用更简单、安全。
2. 动态扩容:通过  malloc / realloc  实现动态内存管理,栈满时自动扩容,支持灵活使用。
3. 防御式编程:用  assert  断言避免非法操作(如栈空时  Pop / Top  ),增强代码鲁棒性。

这段代码完整实现了栈的核心功能(初始化、入栈、出栈、销毁等 ),是 C++ 面向对象思想 + 动态内存管理的典型实践。

以下从语法特性和设计思想两个维度,对比 C++ 栈代码与 C 栈代码的差异,详细拆解 C++ 新增化的特性:

一. 语法层面:C++ 新增的核心特性 

1. 类与封装(最核心差异)


C 语言实现:
用 struct Stack 定义栈,成员(a / top / capacity )是全局可访问的(C 语言 struct 无访问控制 )。操作栈的函数(STInit / STPush )是独立的,调用时需传递 struct Stack* :

typedef struct Stack { ... } ST;
void STPush(ST* ps, int x); // 函数独立,需传栈指针

C++ 实现:
用  class Stack  封装,通过 public / private 控制访问:
- private 成员(_a / _capacity / _top ):外部无法直接访问,避免误操作(如直接修改  _top 破坏栈结构 )。
- public 成员函数( Init / Push ):仅暴露安全接口,强制外部通过接口操作栈。

class Stack 
{
private:int* _a; size_t _capacity;size_t _top;
public:void Push(int x) { ... } // 成员函数,直接访问 private 成员
};

核心价值:C++ 的封装性,让栈的实现更安全、逻辑更内聚,避免 C 语言中“成员被随意修改”的风险。

2. 默认参数(语法糖,简化调用)

C 语言:函数参数无默认值,调用时必须传全参数。例如初始化栈:

void STInit(ST* ps, int n); 
STInit(&s, 4); // 必须显式传 n=4

C++ 实现:
构造函数/初始化函数支持默认参数:

void Init(int n = 4) { ... } 
// 调用时可省略参数:
s.Init(); // 等价于 Init(4)

核心价值:简化调用,提升代码简洁性。

3. 成员函数隐式this指针


C 语言:操作栈的函数需显式传递  struct Stack* :

void STPush(ST* ps, int x) 
{ps->a[ps->top++] = x; // 必须用 ps 访问成员
}

C++ 实现:
成员函数隐含  this  指针,指向当前对象:

void Push(int x) 
{_a[_top++] = x; // 等价于 this->_a[this->_top++] = x
}

核心价值:省略显式传参,代码更简洁;同时强化“对象关联”,让函数与对象的绑定更自然。

4.  bool  类型原生支持

C 语言:无原生  bool  类型,通常用  typedef int bool;  或宏模拟:

typedef int bool; 
#define true 1
#define false 0
bool STEmpty(ST* ps) { return ps->top == 0; }

C++ 实现:
原生支持  bool  类型( true / false  是关键字 ),语义更清晰:

bool Empty() { return _top == 0; } // 直接返回 bool

核心价值:代码可读性更高,避免 C 语言模拟  bool  的繁琐和潜在 bug。

5.  assert  断言增强(可选,但更易用)

C 语言: assert  是标准库宏( <assert.h>  ),功能简单:

#include <assert.h>
void STPop(ST* ps) 
{assert(ps != NULL); // 断言指针非空assert(ps->top > 0); // 断言栈非空
}

C++ 实现:

用法类似,但因  this  指针隐含,断言更聚焦逻辑:

void Pop() 
{assert(_top > 0); // 直接断言成员变量,无需传指针
}

核心价值:结合封装性,断言逻辑更简洁,减少参数传递的冗余。

二. 设计思想:C++ 面向对象的强化

1. 数据与操作的“绑定”


C 语言:数据( struct Stack  )和操作函数( STPush / STPop  )是分离的。调用时需手动传递  struct Stack* ,逻辑分散:

ST s;
STInit(&s); 
STPush(&s, 1); // 每次调用都要传 &s

C++ 实现:
数据( class Stack  成员)和操作(成员函数 )绑定为一个整体。调用时无需显式传对象指针,通过  .  直接调用:

Stack s;
s.Init(); 
s.Push(1); // 隐含 this 指针,直接操作对象

核心价值:更贴近“对象 - 行为”的自然逻辑,符合面向对象编程(OOP) 思想。

2. 接口设计的“安全性”

C 语言: struct 成员暴露,外部可能误操作(如直接修改 ps->top ):

ST s;
s.top = -1; // 非法修改,破坏栈结构(C 语言无法阻止)

C++ 实现:
通过 private 隐藏关键成员( _a / _top  ),强制外部通过 Push / Pop 接口操作。若外部尝试访问  private  成员,编译器直接报错:

Stack s;
s._top = -1; // 编译报错:'_top' is a private member of 'Stack'

核心价值:用语法强制保证“数据安全”,避免非法操作,这是 C++ 封装性的核心优势。

3. 代码复用与扩展

C 语言:若实现多个栈(如  StackInt / StackChar  ),需重复写逻辑(或用宏/函数指针,复杂度高 )。
C++ 实现:
可通过模板(template) 进一步优化(示例简化版 ):

template <typename T>
class Stack 
{
private:T* _a; // ... 
public:void Push(T x) { ... } // 支持任意类型(int/char等)
};// 调用:
Stack<int> s1;   // 存储 int
Stack<char> s2;  // 存储 char

核心价值:C++ 模板让代码复用性更强,一套逻辑支持多种类型,而 C 语言需手动重复实现。

三. 总结:C++ 相对于 C 的核心升级

1. 语法糖与安全性:

  • 用 class 封装,通过 private 保护成员,避免非法访问。
  • 原生 bool 、默认参数、this 指针,让代码更简洁、语义更清晰。

2. 面向对象设计:

  • 数据与操作绑定(成员函数),符合 OOP 思想,逻辑更内聚。
  • 接口设计强制安全,减少人为错误。

3. 扩展性:

  • 模板(C++ 进阶)支持泛型编程,一套代码适配多种类型,远超 C 语言的复用能力。

简单说:C++ 用类和封装重新定义了“数据 + 操作”的组织方式,在语法和设计思想上都比 C 语言更贴近“面向对象”,让栈的实现更安全、更易维护、更具扩展性。

感谢大家观看!

http://www.xdnf.cn/news/17046.html

相关文章:

  • 【qt5_study】1.Hello world
  • SpringCloud学习------Hystrix详解
  • 奇偶校验码原理与FPGA实现
  • ubuntu自动重启BUG排查指南
  • Android 性能基准测试(Benchmark)完全指南:专业方法与最佳实践
  • 【RK3576】【Android14】Uboot下fastboot命令支持
  • 磁悬浮转子振动控制:主动电磁力如何成为高速旋转的“振动克星”
  • 基于Java AI(人工智能)生成末日题材的实践
  • 【docker】UnionFS联合操作系统
  • 《Linux编译器:gcc/g++食用指南》
  • 面试题:前端权限设计
  • # Kafka 消费堆积:从现象到解决的全链路分析
  • Spring小细节
  • lesson32:Pygame模块详解:从入门到实战的2D游戏开发指南
  • 关于为什么ctrl c退不出来SecureCRT命令行的原因及其解决方法:
  • 【25-cv-23395】宠物/婴儿玩具品牌BESTSKY商标维权!
  • MinIO02-Docker安装
  • STM32内部读写FLASH
  • “Why“比“How“更重要:层叠样式表CSS
  • 计算机网络:详解路由器如何转发子网数据包
  • MySQL 查询性能优化与索引失效问题全解析
  • 需求测试用例设计
  • 落霞归雁:从自然之道到“存内计算”——用算法思维在芯片里开一条“数据高速航道”
  • Vue3核心语法进阶(Props)
  • 【C# Winform】 Action事件驱动的多层数据传递
  • 8.5PPT总结各种攻击
  • 37.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--增加Github Action
  • Linux NFS 服务部署、客户端配置及 autofs 自动挂载操作指南
  • 嵌入式硬件中运放内部底层分析
  • 区块链:重构信任的价值互联网革命​