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. 类名与成员命名规范
- 类名:采用“大驼峰”命名(如 Stack 、 Date ),首字母大写,单词间无下划线。
- 成员变量:为区分普通变量,习惯加前缀(如 _array 、 m_capacity ),但不是语法要求,看团队规范。
- 成员函数:采用“小驼峰”或“大驼峰”,如 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 类实现,包含:
- 成员变量( private )。
- 成员函数( public ,类内/类外定义)。
- 访问限定符、类域的使用。
#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 特性
- 空间分配差异:类仅作抽象模型,成员变量无实际空间;实例化对象时,为成员变量分配物理内存,让数据有存储处。
- 多对象创建:一个类可实例化多个对象,每个对象占独立物理空间存各自成员变量数据,就像一套建筑设计图能造出多栋房子,每栋房子(对象 )存不同住户(数据 )。
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 语言更贴近“面向对象”,让栈的实现更安全、更易维护、更具扩展性。
感谢大家观看!