本文是 C 类和对象系列的第一篇涵盖类的定义、访问限定符、类域、实例化、对象大小、this 指针以及 C 封装相比 C 语言的优势。每个知识点都有完整代码示例一类的定义1.基本语法类是 C 面向对象编程的核心。类将数据成员变量和操作数据的方法成员函数封装在一起。#include iostream #include string using namespace std; class Stack { //class定义一个类 public: // 成员函数类内定义默认为 inline void Init(int n 4) { _a (int*)malloc(sizeof(int) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } void Push(int x) { if (_top _capacity) { int newcapacity _capacity * 2; int* tmp (int*)realloc(_a, newcapacity * sizeof(int)); if (tmp nullptr) return; _a tmp; _capacity newcapacity; } _a[_top] x; } int Top() { assert(_top 0); return _a[_top - 1]; } void Destroy() { free(_a); _a nullptr; _top _capacity 0; } private: //成员变量 int* _a; size_t _capacity; size_t _top; };语法要点1.class关键字定义类{}内是类体2.类体末尾的分号不能省略新手最容易犯的错误3.类中的变量称为成员变量或属性4.类中的函数称为成员函数或方法2.成员变量的命名惯例为了区分成员变量和普通变量常见的命名习惯有风格示例说明下划线后缀year_Google 风格下划线前缀_year常见于竞赛/教学代码m 前缀m_yearMFC/Windows 风格class Date { private: int _year; // 下划线前缀 int month_; // 下划线后缀 int m_day; // m 前缀 // 三种风格都可以但要保持一致 };3.struct 也可以定义类C 兼容 C 语言的struct同时将其升级为类。class与struct的区别特性classstruct默认访问限定符privatepublic使用场景复杂对象有私有数据纯数据结构PODclass MyClass { int x; // 默认 private外部不能访问 }; struct MyStruct { int x; // 默认 public外部可以访问 };4.类内定义的成员函数默认为 inline定义在类内部的成员函数编译器会默认将其视为inline函数建议短小的函数定义在类内复杂的函数声明在类内、定义在类外。class Example { public: // 短小函数定义在类内自动 inline int getValue() const { return _value; } // 复杂函数只声明 void complexFunction(); }; // 类外定义 void Example::complexFunction() { // 复杂的实现... }二访问限定符1.基本用法限定符作用类外访问public公有成员✅ 可以private私有成员❌ 不可以默认protected保护成员❌ 不可以派生类可访问class Date { public: // 公有成员函数对外接口 void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() const { cout _year / _month / _day endl; } // 公有成员变量通常不推荐 int version 1; private: // 私有成员变量隐藏内部数据 int _year; int _month; int _day; void privateHelper() { // 私有函数仅供类内部使用 cout 内部辅助函数 endl; } }; int main() { Date d; d.Init(2024, 7, 5); // public可以调用 d.Print(); // public可以调用 d.version 2; // public可以访问但不推荐 // d._year 2024; // private编译错误 // d.privateHelper(); // private编译错误 return 0; }2.访问限定符的作用域访问限定符从它出现的位置开始一直作用到下一个访问限定符出现为止。class Demo { int a; // 默认 privateclass 默认 public: // 从这里开始 public int b; int c; private: // 从这里开始 private int d; int e; protected: // 从这里开始 protected int f; }; // 类结束3.封装的设计原则数据隐藏 接口暴露class BankAccount { public: // 对外接口通过方法访问和修改数据 void deposit(double money) { if (money 0) { _balance money; } } double getBalance() const { return _balance; } private: // 内部数据外部无法直接修改 double _balance 0; }; int main() { BankAccount account; account.deposit(100); cout account.getBalance() endl; // 100 // account._balance 1000; // 编译错误不能直接修改 return 0; }三类域1.类外定义成员函数class Stack { public: // 声明不定义 void Init(int n 4); private: int* _a; size_t _capacity; size_t _top; }; // 类外定义必须用 类名:: 指明属于哪个类域 void Stack::Init(int n) { _a (int*)malloc(sizeof(int) * n); _capacity n; _top 0; }为什么必须指定类域如果不指定Stack::编译器会把Init当成全局函数找不到_a、_capacity、_top的声明2.类域影响编译查找规则class Example { public: void func(); static void staticFunc(); private: int _value; static int _staticValue; }; // 正确指定了类域编译器知道这些成员属于 Example void Example::func() { _value 10; // 在类域中找到 _value staticFunc(); // 在类域中找到 staticFunc } // 错误不指定类域编译器找不到成员 // void func() { // _value 10; // 找不到 _value // }四实例化1.什么是实例化类是对象的抽象描述相当于建筑设计图不占用内存空间。实例化是用类类型在物理内存中创建对象的过程相当于盖房子占用实际内存。class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() const { cout _year / _month / _day endl; } private: int _year; // 声明没有开空间 int _month; int _day; }; int main() { // 实例化对象此时才分配内存 Date d1; Date d2; d1.Init(2024, 7, 5); d2.Init(2024, 8, 10); d1.Print(); // 2024/7/5 d2.Print(); // 2024/8/10 return 0; }2.类与对象的关系生活类比概念生活类比编程类比类建筑设计图class Date { ... };实例化照图纸盖房子Date d;对象具体的房子d是一个具体的对象3.一个类可以实例化多个对象Date d1, d2, d3; // 三个独立的对象各自有各自的 _year/_month/_day d1.Init(2024, 1, 1); d2.Init(2024, 2, 2); // d1 和 d2 的数据互不影响五对象大小与内存对齐1.对象中存储什么结论对象中只存储成员变量不存储成员函数。成员函数被编译成一段指令存储在代码段。如果每个对象都存储成员函数的指针会浪费大量内存。class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } void Print() const { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1, d2; cout sizeof(d1) endl; // 123个int每个4字节 // d1 和 d2 的成员函数是同一份代码不重复存储 return 0; }2.空类的大小class B { public: void Print() {} }; class C { }; int main() { cout sizeof(B) endl; // 1空类占位 cout sizeof(C) endl; // 1空类占位 return 0; }为什么空类大小为 1如果一个字节都不给创建对象时无法在内存中标识它的存在。1 字节纯粹是为了占位表示对象存在。3.内存对齐规则C 规定类实例化的对象也要符合内存对齐规则。对齐规则1.第一个成员在偏移量为 0 的地址处2.其他成员要对齐到对齐数的整数倍地址3.对齐数min(编译器默认对齐数, 成员大小)VS 中默认对齐数为 8Linux/GCC 中默认对齐数为 84.结构体总大小 最大对齐数的整数倍5.嵌套结构体时内部结构体对齐到自己的最大对齐数的整数倍class A { public: void Print() { cout _ch endl; } private: char _ch; // 1字节偏移0 int _i; // 4字节对齐数min(8,4)4偏移4 }; // 总大小 8最大对齐数4的倍数 class D { char c1; // 1字节偏移0 int i; // 4字节对齐数4偏移4-7 char c2; // 1字节偏移8 }; // 总大小 12最大对齐数4的倍数 → 12 class E { char c1; // 1字节偏移0 char c2; // 1字节偏移1 int i; // 4字节对齐数4偏移4-7 }; // 总大小 8最大对齐数4的倍数 → 8 int main() { cout sizeof(A) endl; // 8 cout sizeof(D) endl; // 12 cout sizeof(E) endl; // 8 return 0; }六this 指针1.this 指针的本质this指针是 C 为成员函数隐含添加的一个参数指向当前对象。class Date { public: // 你写的代码 void Init(int year, int month, int day) { _year year; _month month; _day day; } // 编译器实际处理的代码 // void Init(Date* const this, int year, int month, int day) { // this-_year year; // this-_month month; // this-_day day; // } };2.this 指针的使用class Date { public: void Init(int year, int month, int day) { // 可以直接访问成员变量编译器自动转换成 this- _year year; // 也可以显式使用 this this-_month month; this-_day day; // this nullptr; // 编译错误this 是 const 指针不能被修改 } // 返回 this 对象支持链式调用 Date SetYear(int year) { this-_year year; return *this; // 返回当前对象 } Date SetMonth(int month) { _month month; return *this; } Date SetDay(int day) { _day day; return *this; } void Print() const { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { Date d; d.Init(2024, 7, 5); // 链式调用因为每个 Set 函数都返回 *this d.SetYear(2025).SetMonth(8).SetDay(10); d.Print(); // 2025/8/10 return 0; }3.经典面试题空指针调用成员函数class Test { public: void Print1() { cout Print1() called endl; // 不访问成员变量正常运行 } void Print2() { cout _a _a endl; // 访问成员变量需要 this崩溃 } void Print3() { cout this this endl; // 打印 this 地址不崩溃 } private: int _a 10; }; int main() { Test* p nullptr; p-Print1(); // 输出Print1() called // 原因不需要访问成员变量相当于调用一个普通函数 // p-Print2(); // 崩溃 // 原因需要读取 _a相当于 this-_a但 this nullptr p-Print3(); // 输出this 0不崩溃 return 0; }结论 空指针调用成员函数只要不访问成员变量即不解引用 this就不会崩溃。4.this 指针存在哪里this是作为隐含参数传递给成员函数的通常通过寄存器如 ECX传递不存储在对象中也不存储在固定的内存区域。七C 与 C 实现 Stack 对比封装体现1.C 语言实现 Stack过程式// C 风格 Stack用 C 语法模拟 #include stdio.h #include stdlib.h #include assert.h typedef int STDataType; typedef struct CStack { STDataType* a; int top; int capacity; } CStack; void CStackInit(CStack* ps) { ps-a NULL; ps-top 0; ps-capacity 0; } void CStackDestroy(CStack* ps) { free(ps-a); ps-a NULL; ps-top ps-capacity 0; } void CStackPush(CStack* ps, STDataType x) { if (ps-top ps-capacity) { int newcap ps-capacity 0 ? 4 : ps-capacity * 2; STDataType* tmp (STDataType*)realloc(ps-a, newcap * sizeof(STDataType)); if (tmp NULL) return; ps-a tmp; ps-capacity newcap; } ps-a[ps-top] x; } int CStackTop(CStack* ps) { assert(ps-top 0); return ps-a[ps-top - 1]; } void CStackPop(CStack* ps) { assert(ps-top 0); --ps-top; } int CStackEmpty(CStack* ps) { return ps-top 0; } int main() { CStack s; CStackInit(s); CStackPush(s, 1); CStackPush(s, 2); CStackPush(s, 3); while (!CStackEmpty(s)) { printf(%d\n, CStackTop(s)); CStackPop(s); } CStackDestroy(s); return 0; }C 风格的缺点需要手动传递结构体指针s数据和方法分离相关代码不在一起没有数据保护外部可以直接修改s.top等成员容易忘记调用Init/Destroy2.C 实现 Stack封装// C 风格 Stack封装 #include iostream #include cassert #include cstdlib using namespace std; typedef int STDataType; class CPPStack { public: // 构造函数自动初始化 CPPStack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } // 析构函数自动清理 ~CPPStack() { free(_a); _a nullptr; _top _capacity 0; } void Push(STDataType x) { if (_top _capacity) { int newcap _capacity * 2; STDataType* tmp (STDataType*)realloc(_a, newcap * sizeof(STDataType)); if (tmp NULL) return; _a tmp; _capacity newcap; } _a[_top] x; } void Pop() { assert(_top 0); --_top; } int Top() { assert(_top 0); return _a[_top - 1]; } bool Empty() { return _top 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; int main() { CPPStack s; // 自动调用构造函数初始化 s.Push(1); s.Push(2); s.Push(3); while (!s.Empty()) { cout s.Top() endl; s.Pop(); } // 离开作用域时自动调用析构函数清理 return 0; }3.对比总结对比维度C 实现C 实现封装数据和方法分离封装在一个类中数据保护无外部可直接访问修改有private 保护成员调用方式CStackPush(s, 1)s.Push(1)更自然this 指针手动传递结构体指针自动隐式传递初始化/清理手动调用Init/Destroy构造/析构自动调用类型使用需要typedef类名直接当类型缺省参数不支持C语言支持更方便析构、构造等函数在中篇会介绍4.封装的本质封装是一种更严格规范的管理避免数据被随意访问和修改。// C 风格外部可以直接修改内部数据 CStack s; s.top 100; // 危险可以绕过 Push 直接修改 // C 风格外部无法直接修改私有成员 CPPStack s; // s._top 100; // 编译错误_top 是 private s.Push(100); // 只能通过公有接口操作这保证了数据的完整性和一致性。上篇总结知识点核心内容类的定义class 成员变量 成员函数访问限定符public/private/protected控制访问权限类域成员函数类外定义需要类名::实例化类不占内存对象占内存对象大小只存储成员变量符合内存对齐this 指针隐含参数指向当前对象封装对比C 将数据和方法封装比 C 更安全、更方便下一篇预告类和对象中篇—— 默认成员函数构造、析构、拷贝构造、赋值重载、const 成员函数。有任何疑问或需要补充的内容欢迎随时交流