本文是 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_year | MFC/Windows 风格 |
class Date { private: int _year; // 下划线前缀 int month_; // 下划线后缀 int m_day; // m 前缀 // 三种风格都可以,但要保持一致 };3.struct 也可以定义类
C++ 兼容 C 语言的struct,同时将其升级为类。
class与struct的区别:
| 特性 | class | struct |
|---|---|---|
| 默认访问限定符 | private | public |
| 使用场景 | 复杂对象(有私有数据) | 纯数据结构(POD) |
class 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; // 默认 private(class 默认) 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; // 12(3个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 中默认对齐数为 8
Linux/GCC 中默认对齐数为 8
4.结构体总大小 =最大对齐数的整数倍
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/Destroy
2.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 成员函数。
有任何疑问或需要补充的内容,欢迎随时交流!