类和对象详解(1)
一、类的定义
1.1 定义格式与核心要素
- 关键字与结构:用
class或struct定义类,格式为class 类名{成员变量+成员函数};,末尾分号不可省略。- 类体中的变量称为成员变量 或 属性,函数称为成员函数 或 方法。
- 成员变量命名惯例:加前缀
_(如_year)或m_(如m_capacity),非 C++ 强制要求,仅为区分成员变量与局部变量。
- class 与 struct 的区别:
特性 class struct 默认访问权限 private(类外不可直接访问) public(类外可直接访问) 兼容性 仅 C++ 特性 兼容 C 的结构体用法,同时支持定义函数 推荐场景 定义面向对象的类 兼容 C 代码或简单数据结构 - 成员函数特殊规则:定义在类内部的成员函数,默认视为
inline(内联函数),适合代码量少的函数;类外定义需用::(作用域操作符)指定类域。
1.2 访问限定符
- 作用:实现 C++ 的 “封装” 特性,控制类成员在类外的访问权限,保护数据不被随意修改。
- 三种限定符及规则:
限定符 访问权限 作用域范围 public 类内、类外均可访问 从当前限定符开始,到下一个限定符结束 protected 类内可访问,类外不可访问(继承时生效区别) 同 public private 类内可访问,类外不可访问 同 public -
使用建议:成员变量通常设为
private或protected,提供public的成员函数(如Init、Push)供类外间接操作数据,确保数据安全性。
1.3 类域
- 概念:类定义了独立的作用域,所有成员(变量 / 函数)均属于该类域,编译器通过类域查找成员声明 / 定义。
- 类外定义成员函数:需显式用
类名::成员函数名指定类域,否则编译器会将函数视为全局函数,导致无法找到类内成员而报错。
-
class Stack { public:void Init(int n = 4); // 类内声明 private:int* _a; }; // 类外定义,指定Stack类域 void Stack::Init(int n) {_a = (int*)malloc(sizeof(int) * n); }
二、类的实例化
2.1 实例化概念
- 核心逻辑:类是 “抽象模板”(如建筑设计图),仅声明成员变量(未分配内存);实例化是用类类型创建对象(如按设计图建房子),此时才为对象分配物理内存,存储成员变量。
- 关键特性:
- 一个类可实例化多个对象,每个对象拥有独立的成员变量存储空间(存储各自数据)。
- 成员函数不存储在对象中,而是统一存放在代码段(公共区域),所有对象共享同一套成员函数指令,避免内存浪费。
2.2 对象大小计算
- 计算规则:对象仅存储成员变量,大小需遵循内存对齐规则(与结构体内存对齐一致),成员函数不占对象内存。
- 内存对齐规则:
- 第一个成员变量对齐到对象偏移量为 0 的地址。
- 其他成员变量对齐到 “对齐数” 的整数倍地址,对齐数 = 编译器默认对齐数与成员大小的较小值(VS 默认对齐数为 8)。
- 对象总大小为 “最大对齐数”(所有成员对齐数的最大值)的整数倍。
- 嵌套结构体时,嵌套结构体对齐到自身最大对齐数的整数倍,整体大小为包含嵌套结构体在内的所有最大对齐数的整数倍。
- 特殊情况:无成员变量的类(如
class C{}),实例化对象大小为1 字节,仅用于 “占位标识对象存在”,无实际数据存储意义。class A { private: char _ch; int _i; }; // 大小:8(char占1字节,对齐到4字节,int占4字节,总8字节) class B { public: void Print(); }; // 无成员变量,大小1字节
三、this 指针
3.1 核心作用
解决 “多个对象调用同一成员函数时,如何区分当前操作的对象” 的问题。C++ 编译器会为每个非静态成员函数隐含添加一个this 指针参数,指向当前调用函数的对象。
3.2 特性与使用规则
- 隐含传递:this 指针无需在函数形参 / 实参中显式声明,编译器自动处理。例如
Date类的Init函数:- 实际原型:
void Init(Date* const this, int year, int month, int day) - 调用时:
d1.Init(2024,3,31)自动转换为Init(&d1, 2024,3,31)
- 实际原型:
- 访问成员变量:函数体内访问成员变量(如
_year = year),本质是this->_year = year,this 指针可显式使用(如this->_month = month)。 - const 限制:this 指针默认是 “指向当前类类型的 const 指针”(如
Date* const this),不可修改指向(即不能赋值this = nullptr)。
3.3 典型问题测试
通过两个例题理解 this 指针的访问逻辑:
- 例题 1:空指针调用无成员变量访问的函数
A* p = nullptr; p->Print(); // Print仅输出字符串,不访问成员变量- 结果:正常运行。成员函数存于代码段,调用时仅需函数地址,无需访问对象内存(this 指针虽为 nullptr,但未解引用)。
- 例题 2:空指针调用访问成员变量的函数
A* p = nullptr; p->Print(); // Print中输出this->_a(成员变量)- 结果:运行崩溃。需通过 this 指针(nullptr)解引用访问成员变量,触发空指针访问错误。
- this 指针存储位置:答案为A(栈)。this 指针是成员函数的隐含形参,函数调用时形参存储在栈区。
四、C++ 与 C 语言实现 Stack 的对比
以 “栈(Stack)” 数据结构为例,体现 C++ 面向对象封装的优势:
| 对比维度 | C 语言实现 | C++ 实现 |
|---|---|---|
| 数据与函数组织 | 结构体(仅存数据)+ 全局函数(需传结构体指针) | 类(数据 + 函数封装),无需传对象指针 |
| 数据安全性 | 可直接修改结构体成员(如ps->top = 10),无保护 | 成员变量设为 private,仅通过 public 函数修改 |
| 语法便捷性 | 需用 typedef 定义结构体别名,函数需显式传指针 | 类名直接作为类型,this 指针隐含传递,支持缺省参数(如 Init (n=4)) |
| 封装性 | 无封装,数据与操作分离 | 封装数据与操作,通过访问限定符控制权限 |
代码片段(核心差异)
- C 语言:
typedef struct Stack { int* a; int top; } ST; void STInit(ST* ps) { ps->top = 0; } // 需传结构体指针 int main() { ST s; STInit(&s); s.top = 10; } // 可直接修改成员 - C++:
class Stack { public:void Init(int n=4) { _top = 0; } // this指针隐含指向当前对象 private:int* _a; int _top; // 私有成员,类外不可直接修改 }; int main() { Stack s; s.Init(); } // 无需传指针,无法直接修改_top
完整代码
c语言
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>// 定义栈存储的数据类型(方便后续修改存储类型)
typedef int STDataType;// 定义栈的结构体(存储栈的核心信息)
typedef struct Stack
{STDataType* a; // 指向栈底层数组的指针int top; // 栈顶位置(指向栈顶元素的下一个位置,空栈时为 0)int capacity; // 栈的最大容量(避免频繁扩容)
} ST;// 1. 初始化栈
void STInit(ST* ps)
{assert(ps); // 断言:确保传入的结构体指针非空,避免非法访问ps->a = NULL; // 初始时栈无空间,数组指针置空ps->top = 0; // 空栈的栈顶位置为 0ps->capacity = 0; // 初始容量为 0
}// 2. 销毁栈(释放内存,避免内存泄漏)
void STDestroy(ST* ps)
{assert(ps);free(ps->a); // 释放底层数组的内存ps->a = NULL; // 指针置空,避免野指针ps->top = ps->capacity = 0; // 重置栈顶和容量为初始状态
}// 3. 入栈(将元素 x 压入栈顶,满栈时自动扩容)
void STPush(ST* ps, STDataType x)
{assert(ps);// 满栈判断:栈顶 == 容量,需扩容if (ps->top == ps->capacity){// 扩容策略:初始容量为 0 时扩为 4,否则扩为原来的 2 倍int newcapacity = (ps->capacity == 0) ? 4 : ps->capacity * 2;// 重新分配内存(realloc 可直接使用 NULL 指针,等价于 malloc)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++;
}// 4. 出栈(删除栈顶元素,需确保栈非空)
void STPop(ST* ps)
{assert(ps);assert(!STEmpty(ps)); // 断言:栈非空才能出栈ps->top--; // 栈顶位置前移,等效删除栈顶元素(后续入栈会覆盖)
}// 5. 获取栈顶元素(需确保栈非空)
STDataType STTop(ST* ps)
{assert(ps);assert(!STEmpty(ps)); // 断言:栈非空才能获取栈顶return ps->a[ps->top - 1]; // 栈顶指向元素下一个位置,故减 1
}// 6. 判断栈是否为空(空栈返回 true,非空返回 false)
bool STEmpty(ST* ps)
{assert(ps);return (ps->top == 0); // 栈顶为 0 时为空栈
}// 7. 获取栈中元素的个数
int STSize(ST* ps)
{assert(ps);return ps->top; // 栈顶位置即为元素个数(空栈为 0,有 n 个元素时 top 为 n)
}// 测试函数:演示栈的基本操作
int main()
{ST s; // 定义栈对象STInit(&s); // 初始化栈(需传递结构体地址)// 入栈操作:压入 1、2、3、4STPush(&s, 1);STPush(&s, 2);STPush(&s, 3);STPush(&s, 4);// 遍历栈:从栈顶到栈底打印元素(出栈方式)printf("栈中元素(从顶到底):");while (!STEmpty(&s)){STDataType top = STTop(&s); // 获取栈顶元素printf("%d ", top);STPop(&s); // 出栈(删除当前栈顶,下一轮获取新栈顶)}printf("\n");// 销毁栈(释放内存,避免内存泄漏)STDestroy(&s);return 0;
}
c++
#include <iostream>
#include <cstdlib> // 包含 malloc、free 函数
#include <cassert> // 包含 assert 断言
using namespace std; // 简化标准库使用(入门阶段常用)// 定义栈存储的数据类型
typedef int STDataType;// 定义 Stack 类(封装数据和操作)
class Stack
{
public:// 1. 初始化栈(带缺省参数:默认初始容量为 4)void Init(int n = 4){// 分配 n 个 STDataType 大小的内存_a = (STDataType*)malloc(sizeof(STDataType) * n);if (_a == nullptr) // 内存分配失败判断(C++ 推荐用 nullptr 代替 NULL){perror("malloc fail"); // 打印错误信息return;}_capacity = n; // 初始化容量为 n_top = 0; // 初始化栈顶为 0(空栈)}// 2. 销毁栈(释放内存)void Destroy(){free(_a); // 释放底层数组内存_a = nullptr; // 指针置空,避免野指针_top = 0; // 重置栈顶_capacity = 0; // 重置容量}// 3. 入栈(压入元素 x)void Push(STDataType x){// 满栈判断:栈顶 == 容量,需扩容if (_top == _capacity){// 扩容策略:初始容量为 0 时扩为 4,否则扩为 2 倍int newcapacity = (_capacity == 0) ? 4 : _capacity * 2;// 重新分配内存STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));if (tmp == nullptr){perror("realloc fail");return;}_a = tmp; // 更新数组指针_capacity = newcapacity; // 更新容量}// 压入元素,栈顶后移(this 指针隐含访问成员变量,可省略)_a[_top++] = x;}// 4. 出栈(删除栈顶元素)void Pop(){assert(_top > 0); // 断言:栈非空才能出栈(等价于 !Empty())_top--; // 栈顶前移,等效删除栈顶}// 5. 获取栈顶元素STDataType Top(){assert(_top > 0); // 断言:栈非空return _a[_top - 1]; // 返回栈顶元素}// 6. 判断栈是否为空bool Empty(){return (_top == 0); // 栈顶为 0 则为空}// 7. 获取栈中元素个数int Size(){return _top; // 栈顶位置即为元素个数}private:// 成员变量(私有:仅类内成员函数可访问,避免外部随意修改)STDataType* _a; // 指向底层数组的指针(前缀 _ 区分成员变量与局部变量)size_t _capacity; // 栈的最大容量(size_t 为无符号整数,更贴合容量语义)size_t _top; // 栈顶位置(无符号整数,避免负索引)
};// 测试函数:演示栈的操作
int main()
{Stack s; // 定义 Stack 类对象(自动分配内存,无需手动定义结构体)s.Init(); // 初始化栈(可传参指定初始容量,如 s.Init(8))// 入栈:压入 1、2、3、4s.Push(1);s.Push(2);s.Push(3);s.Push(4);// 遍历栈:从顶到底打印cout << "栈中元素(从顶到底):";while (!s.Empty()){cout << s.Top() << " "; // 调用成员函数获取栈顶s.Pop(); // 调用成员函数出栈}cout << endl;// 销毁栈(释放内存)s.Destroy();return 0;
}
