C++ 类和对象入门(一):从 class、访问限定符到 this 指针

C++ 类和对象入门:从 class、访问限定符到 this 指针


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


目录

  • [C++ 类和对象入门:从 class、访问限定符到 this 指针](#C++ 类和对象入门:从 class、访问限定符到 this 指针)
    • 前言
    • 一、类的基本认识
      • [1.1 为什么需要类?](#1.1 为什么需要类?)
      • [1.2 什么是类?](#1.2 什么是类?)
      • [1.3 类的基本定义格式](#1.3 类的基本定义格式)
      • [1.4 成员变量为什么常常加下划线?](#1.4 成员变量为什么常常加下划线?)
    • 二、访问限定符与封装
      • [2.1 什么是访问限定符?](#2.1 什么是访问限定符?)
      • [2.2 为什么成员变量通常放 private?](#2.2 为什么成员变量通常放 private?)
      • [2.3 封装的本质是什么?](#2.3 封装的本质是什么?)
    • [三、class 和 struct 的区别](#三、class 和 struct 的区别)
      • [3.1 C++ 中 struct 也可以定义成员函数](#3.1 C++ 中 struct 也可以定义成员函数)
      • [3.2 class 和 struct 的主要区别](#3.2 class 和 struct 的主要区别)
      • [3.3 什么时候用 class,什么时候用 struct?](#3.3 什么时候用 class,什么时候用 struct?)
    • 四、类域与成员函数定义
      • [4.1 什么是类域?](#4.1 什么是类域?)
      • [4.2 类外定义成员函数为什么要加类名?](#4.2 类外定义成员函数为什么要加类名?)
      • [4.3 类域的作用](#4.3 类域的作用)
    • 五、对象实例化
      • [5.1 类只是设计图,对象才真正占空间](#5.1 类只是设计图,对象才真正占空间)
      • [5.2 一个类可以实例化多个对象](#5.2 一个类可以实例化多个对象)
      • [5.3 类和对象的关系](#5.3 类和对象的关系)
    • 六、对象大小与内存布局
      • [6.1 对象里面到底存了什么?](#6.1 对象里面到底存了什么?)
      • [6.2 sizeof 对象时算什么?](#6.2 sizeof 对象时算什么?)
      • [6.3 内存对齐是什么?](#6.3 内存对齐是什么?)
      • [6.4 内存对齐的基本规则](#6.4 内存对齐的基本规则)
      • [6.5 空类对象为什么大小是 1?](#6.5 空类对象为什么大小是 1?)
    • [七、this 指针](#七、this 指针)
      • [7.1 问题:成员函数怎么知道当前对象是谁?](#7.1 问题:成员函数怎么知道当前对象是谁?)
      • [7.2 this 指针是什么?](#7.2 this 指针是什么?)
      • [7.3 this 指针能不能显式使用?](#7.3 this 指针能不能显式使用?)
      • [7.4 this 指针能不能被修改?](#7.4 this 指针能不能被修改?)
      • [7.5 this 指针存在哪里?](#7.5 this 指针存在哪里?)
    • 八、空指针调用成员函数的问题
      • [8.1 为什么有些空指针调用成员函数不崩?](#8.1 为什么有些空指针调用成员函数不崩?)
      • [8.2 为什么访问成员变量就可能崩?](#8.2 为什么访问成员变量就可能崩?)
      • [8.3 这类写法能不能在项目中使用?](#8.3 这类写法能不能在项目中使用?)
    • [九、C 和 C++ 实现 Stack 的对比](#九、C 和 C++ 实现 Stack 的对比)
      • [9.1 C 语言实现 Stack 的特点](#9.1 C 语言实现 Stack 的特点)
      • [9.2 C++ 实现 Stack 的方式](#9.2 C++ 实现 Stack 的方式)
      • [9.3 C++ 版本看起来改变了什么?](#9.3 C++ 版本看起来改变了什么?)
      • [9.4 C 和 C++ 实现 Stack 的核心区别](#9.4 C 和 C++ 实现 Stack 的核心区别)
    • 十、重新理解封装
      • [10.1 封装不是把代码写复杂](#10.1 封装不是把代码写复杂)
      • [10.2 封装让接口和实现分离](#10.2 封装让接口和实现分离)
    • 十一、常见错误总结
      • [11.1 类定义结尾忘记分号](#11.1 类定义结尾忘记分号)
      • [11.2 类外访问 private 成员](#11.2 类外访问 private 成员)
      • [11.3 类外定义成员函数忘记加类域](#11.3 类外定义成员函数忘记加类域)
      • [11.4 以为类定义时就分配了对象空间](#11.4 以为类定义时就分配了对象空间)
      • [11.5 以为成员函数存储在每个对象中](#11.5 以为成员函数存储在每个对象中)
      • [11.6 误以为 this 指针存在对象里](#11.6 误以为 this 指针存在对象里)
    • 十二、全文总结
      • [13.1 本文核心内容](#13.1 本文核心内容)

前言

学完 C 语言以后,再开始学习 C++,第一个真正有"C++味道"的内容通常就是:类和对象

很多同学刚看到类时,会觉得它像是"升级版结构体":

  • C 语言结构体只能放数据;
  • C++ 的类既可以放数据,也可以放函数;
  • 数据和函数放在一起之后,代码看起来更像一个整体。

这个理解没错,但还不够完整。

C++ 类和对象真正重要的地方在于:

它把数据和操作数据的函数封装到一起,并通过访问权限控制外部如何使用对象。

比如我们实现一个栈。

在 C 语言里,通常是结构体保存数据,函数单独放在外面:

cpp 复制代码
ST s;
STInit(&s);
STPush(&s, 1);
STPop(&s);

而在 C++ 中,我们可以写成:

cpp 复制代码
Stack s;
s.Init();
s.Push(1);
s.Pop();

这两种写法底层逻辑差别没有想象中那么大,但代码组织方式已经变了。

C++ 更强调:

对象自己管理自己的数据,对外只暴露必要的接口。


一、类的基本认识

1.1 为什么需要类?

在 C 语言中,我们可以用结构体描述一个对象的数据。

比如日期:

cpp 复制代码
struct Date
{
    int year;
    int month;
    int day;
};

这个结构体能说明一个日期应该有三个数据:

但是如果我们想初始化日期、打印日期,就需要额外写函数:

cpp 复制代码
void DateInit(struct Date* d, int year, int month, int day)
{
    d->year = year;
    d->month = month;
    d->day = day;
}

void DatePrint(struct Date* d)
{
    printf("%d/%d/%d\n", d->year, d->month, d->day);
}

调用时:

cpp 复制代码
struct Date d;
DateInit(&d, 2024, 3, 31);
DatePrint(&d);

这种写法能用,但数据和函数是分开的。

当代码规模变大以后,就会出现一些问题:

  • 谁都可以直接修改结构体成员;
  • 数据和操作数据的函数分散在不同地方;
  • 调用函数时总要手动传结构体地址;
  • 接口和内部细节边界不清楚。

C++ 的类就是为了解决这类问题而引入的重要机制。


1.2 什么是类?

类可以理解成一种自定义类型。

它不仅能描述对象有哪些数据,还能描述对象能做哪些操作。

比如定义一个日期类:

cpp 复制代码
#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;
};

这里的 Date 就是一个类。

它里面有两类成员:

成员类型 含义
成员变量 保存对象的数据,也叫属性
成员函数 操作对象数据的函数,也叫方法

在这个例子中:

cpp 复制代码
int _year;
int _month;
int _day;

是成员变量。

cpp 复制代码
void Init(...)
void Print()

是成员函数。


1.3 类的基本定义格式

C++ 使用 class 关键字定义类。

基本格式如下:

cpp 复制代码
class 类名
{
public:
    // 公有成员

private:
    // 私有成员

protected:
    // 保护成员
};

注意:类定义结束时,右大括号后面的分号不能省略。

cpp 复制代码
};

例如:

cpp 复制代码
class Stack
{
public:
    void Init()
    {
        // 初始化
    }

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

其中:

  • class 是定义类的关键字;
  • Stack 是类名;
  • {} 中是类体;
  • 类体中可以定义成员变量和成员函数;
  • 最后的 ; 必须写。

1.4 成员变量为什么常常加下划线?

在很多 C++ 代码中,成员变量经常写成:

cpp 复制代码
int _year;
int _month;
int _day;

也有一些代码会写成:

cpp 复制代码
int year_;
int month_;
int day_;

或者:

cpp 复制代码
int m_year;
int m_month;
int m_day;

这些写法不是 C++ 强制规定,而是命名习惯。

目的很简单:

区分成员变量、普通局部变量和函数参数。

例如:

cpp 复制代码
void Init(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

这里:

  • _year 是成员变量;
  • year 是函数参数。

这样写代码时,不容易混淆。


二、访问限定符与封装

2.1 什么是访问限定符?

C++ 中有三个常见访问限定符:

访问限定符 含义
public 公有成员,类外可以直接访问
private 私有成员,类外不能直接访问
protected 保护成员,类外不能直接访问,继承中会体现作用

入门阶段可以先这样理解:

  • public:对外开放的接口;
  • private:类内部维护的实现细节;
  • protected:暂时可以先理解为类似 private,后面学继承时再深入。

例如:

cpp 复制代码
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;
};

类外可以访问 public 成员:

cpp 复制代码
Date d;
d.Init(2024, 3, 31);
d.Print();

但是不能直接访问 private 成员:

cpp 复制代码
d._year = 2025; // 错误,_year 是 private

2.2 为什么成员变量通常放 private?

如果成员变量全部放到 public,外部代码就可以随便修改对象内部状态。

比如一个栈:

cpp 复制代码
class Stack
{
public:
    int* _a;
    size_t _capacity;
    size_t _top;
};

这样写以后,外部可以直接修改:

cpp 复制代码
Stack s;
s._top = 100000;
s._capacity = 1;

这会破坏栈的内部逻辑。

正常情况下:

  • _top 应该由 Push()Pop() 维护;
  • _capacity 应该由扩容逻辑维护;
  • _a 不应该被外部随便改。

更合理的写法是:

cpp 复制代码
class Stack
{
public:
    void Push(int x);
    void Pop();
    int Top();
    bool Empty();

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

这样,外部只能通过公开接口使用栈:

cpp 复制代码
s.Push(1);
s.Pop();
s.Top();

不能直接破坏内部数据。


2.3 封装的本质是什么?

封装不是简单地"把东西起来"。

更准确地说,封装做了两件事:

  1. 把相关的数据和函数组织到一个类里面;
  2. 通过访问权限限制外部对内部数据的直接访问。

封装的意义在于:

外部只需要知道怎么用,不需要知道内部怎么实现。

比如使用栈时,外部只需要知道:

cpp 复制代码
Push()
Pop()
Top()
Empty()

不需要关心:

cpp 复制代码
_a
_top
_capacity

这样代码会更安全,也更容易维护。


三、class 和 struct 的区别

3.1 C++ 中 struct 也可以定义成员函数

在 C 语言中,struct 主要用于组织数据。

但在 C++ 中,struct 被升级了,它也可以定义成员函数。

例如:

cpp 复制代码
struct ListNode
{
    void Init(int x)
    {
        val = x;
        next = nullptr;
    }

    int val;
    ListNode* next;
};

这在 C++ 中是合法的。

也就是说:

C++ 中的 struct 也可以看作一种类。


3.2 class 和 struct 的主要区别

classstruct 最主要的区别是默认访问权限不同。

例如:

cpp 复制代码
class A
{
    int _a;
};

这里 _a 默认是 private

而:

cpp 复制代码
struct B
{
    int _b;
};

这里 _b 默认是 public


3.3 什么时候用 class,什么时候用 struct?

实际写代码时,通常有一个习惯:

  • 如果只是简单保存数据,常用 struct
  • 如果需要封装数据和操作,常用 class

例如链表节点这种简单数据结构,可以写成 struct

cpp 复制代码
struct ListNode
{
    int val;
    ListNode* next;
};

而栈、队列、日期类这种需要维护内部逻辑的类型,更适合写成 class

cpp 复制代码
class Stack
{
public:
    void Push(int x);
    void Pop();

private:
    int* _a;
    size_t _top;
    size_t _capacity;
};

这不是硬性规定,而是工程代码中的常见风格。


四、类域与成员函数定义

4.1 什么是类域?

类本身会形成一个新的作用域。

类里面定义的成员变量和成员函数,都属于这个类的作用域。

例如:

cpp 复制代码
class Stack
{
public:
    void Init(int n = 4);

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

这里的 Init_a_capacity_top 都属于 Stack 这个类域。


4.2 类外定义成员函数为什么要加类名?

如果成员函数只在类里面声明,在类外定义,就必须加上类域。

例如:

cpp 复制代码
class Stack
{
public:
    void Init(int n = 4);

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

类外定义时要写:

cpp 复制代码
void Stack::Init(int n)
{
    _a = (int*)malloc(sizeof(int) * n);
    if (_a == nullptr)
    {
        perror("malloc fail");
        return;
    }

    _capacity = n;
    _top = 0;
}

这里的:

cpp 复制代码
Stack::Init

表示:

这个 Init 函数属于 Stack 类。

这里的使用方法可以参考之前我们名字空间使用的内容。

如果写成:

cpp 复制代码
void Init(int n)
{
    _a = ...
}

编译器会把它当成普通全局函数。

此时 _a_capacity_top 都找不到。


4.3 类域的作用

类域影响的是编译器查找名字的规则。

当编译器看到:

cpp 复制代码
void Stack::Init(int n)

它知道当前函数是 Stack 的成员函数。

所以函数体里访问:

cpp 复制代码
_a
_capacity
_top

时,编译器会到 Stack 类域中查找这些成员。

这就是为什么类外定义成员函数必须加:

cpp 复制代码
类名::函数名

五、对象实例化

5.1 类只是设计图,对象才真正占空间

类本身只是一个类型,是一种抽象描述。

比如:

cpp 复制代码
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;
};

这只是定义了 Date 类型。

此时还没有真正的日期对象。

只有当我们写:

cpp 复制代码
Date d1;
Date d2;

才真正创建了两个对象。

这个过程叫:

类实例化出对象。

可以用理解为:

类像建筑设计图,对象像根据图纸建出来的房子。

图纸说明房子应该怎么建,但图纸本身不能住人;

真正建出来的房子才占空间,才能使用。


5.2 一个类可以实例化多个对象

例如:

cpp 复制代码
Date d1;
Date d2;

d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);

d1.Print();
d2.Print();

输出:

cpp 复制代码
2024/3/31
2024/7/5

d1d2 都是 Date 类型。

它们结构相同,但数据不同。

也就是说:

  • d1 有自己的 _year_month_day
  • d2 也有自己的 _year_month_day
  • 两个对象的数据互不影响。

5.3 类和对象的关系

可以这样总结:

概念 含义
类型、模板、设计图
对象 根据类创建出的具体实体
成员变量 每个对象各自保存一份
成员函数 所有对象共享同一份函数代码

一句话:

类定义共同结构,对象保存具体数据。


六、对象大小与内存布局

6.1 对象里面到底存了什么?

当我们初学的时候会以为:

类里既有成员变量,也有成员函数,所以对象里应该也存成员变量和成员函数。

实际上,对象中主要存储的是成员变量。

成员函数代码不会在每个对象中保存一份。

原因很简单。

比如:

cpp 复制代码
Date d1;
Date d2;

d1d2 的日期数据不同,所以它们必须各自保存成员变量。

Init()Print() 的函数代码是一样的。

如果每个对象都存一份函数代码,就太浪费空间了。

所以:

对象保存成员变量,成员函数放在公共代码区,所有对象共享。


6.2 sizeof 对象时算什么?

看下面这个类:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

如果一个 int 是 4 字节,那么:

cpp 复制代码
sizeof(Date)

通常是:

cpp 复制代码
12

因为对象中有三个 int 成员变量。

但是对象大小不一定永远等于成员变量大小简单相加。

原因是:

对象大小还要遵守内存对齐规则。


6.3 内存对齐是什么?

看下面这个类:

cpp 复制代码
class A
{
private:
    char _ch;
    int _i;
};

很多环境下:

cpp 复制代码
sizeof(A)

不是 5,而是 8。

原因是内存对齐。

可以粗略理解成:

cpp 复制代码
_ch 占 1 字节
中间填充 3 字节
_i 占 4 字节

所以总大小是 8 字节。

内存对齐的目的主要是让 CPU 更高效地访问数据。


6.4 内存对齐的基本规则

入门阶段可以先记住这几条:

  1. 第一个成员从偏移量 0 开始;
  2. 后面的成员要放到合适的对齐位置;
  3. 对象整体大小通常要是最大对齐数的整数倍;
  4. 成员顺序可能影响对象大小。

比如:

cpp 复制代码
class A
{
private:
    char _ch;
    int _i;
};

通常布局大概是:

偏移量 内容
0 _ch
1~3 填充字节
4~7 _i

所以最终大小是 8 字节。


6.5 空类对象为什么大小是 1?

看下面两个类:

cpp 复制代码
class B
{
public:
    void Print()
    {
        cout << "B::Print()" << endl;
    }
};

class C
{
};

它们都没有成员变量。

但是:

cpp 复制代码
sizeof(B)
sizeof(C)

通常都是:

cpp 复制代码
1

为什么没有成员变量还要占 1 个字节?

原因是:

对象必须在内存中有一个可区分的地址。

如果空对象大小是 0,那么多个空对象就不好区分。

所以 C++ 会给空类对象分配 1 个字节作为占位。

注意,这 1 个字节不是用来存成员函数的,只是为了表示对象存在。


七、this 指针

7.1 问题:成员函数怎么知道当前对象是谁?

看下面代码:

cpp 复制代码
Date d1;
Date d2;

d1.Init(2024, 3, 31);
d2.Init(2024, 7, 5);

Init() 函数只有一份代码。

那么问题来了:

  • d1.Init(...) 时,函数怎么知道要修改 d1
  • d2.Init(...) 时,函数怎么知道要修改 d2

答案是:

C++ 会给非静态成员函数隐含传递一个 this 指针。


7.2 this 指针是什么?

this 指针指向当前调用成员函数的对象。

例如:

cpp 复制代码
d1.Init(2024, 3, 31);

可以粗略理解成编译器背后传了一个对象地址:

cpp 复制代码
this = &d1;

而:

cpp 复制代码
d2.Init(2024, 7, 5);

可以理解成:

cpp 复制代码
this = &d2;

所以成员函数内部访问成员变量时,本质上是通过 this 指针访问的。

比如:

cpp 复制代码
void Init(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

可以理解成:

cpp 复制代码
void Init(int year, int month, int day)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

7.3 this 指针能不能显式使用?

可以。

在成员函数内部,可以显式写 this->

cpp 复制代码
void Init(int year, int month, int day)
{
    this->_year = year;
    this->_month = month;
    this->_day = day;
}

一般情况下,不写也可以:

cpp 复制代码
_year = year;

编译器会自动识别 _year 是成员变量。

但是在讲解代码、区分成员来源时,显式写 this-> 会更直观。


7.4 this 指针能不能被修改?

不能。

this 指针可以理解成一个指针常量。

也就是说,它指向当前对象,不能被重新赋值。

下面写法是错误的:

cpp 复制代码
this = nullptr; // 错误

但是可以通过 this 访问成员:

cpp 复制代码
this->_year = year;

7.5 this 指针存在哪里?

this 是成员函数的隐含参数。

既然是函数参数,它通常和普通参数一样,在函数调用过程中存在。

它不存放在对象里面。

所以不要以为每个对象内部都有一个 this 指针。

正确理解是:

对象里主要保存成员变量;

调用成员函数时,编译器隐含传入当前对象地址,这个地址就是 this。


八、空指针调用成员函数的问题

8.1 为什么有些空指针调用成员函数不崩?

看下面代码:

cpp 复制代码
class A
{
public:
    void Print()
    {
        cout << "A::Print()" << endl;
    }

private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->Print();

    return 0;
}

有些环境下,这段代码可能会正常输出:

cpp 复制代码
A::Print()

原因是:

Print() 函数内部没有访问任何成员变量。

虽然 p 是空指针,但函数体中没有真正通过 this 去访问对象内部数据。

所以它可能暂时看起来"没事"。


8.2 为什么访问成员变量就可能崩?

再看这段代码:

cpp 复制代码
class A
{
public:
    void Print()
    {
        cout << "A::Print()" << endl;
        cout << _a << endl;
    }

private:
    int _a;
};

int main()
{
    A* p = nullptr;
    p->Print();

    return 0;
}

这次 Print() 内部访问了 _a

而访问 _a 本质上就是:

cpp 复制代码
this->_a

此时 this 是空指针。

所以程序很可能崩溃。


8.3 这类写法能不能在项目中使用?

不能。

空指针调用成员函数属于非常危险的行为。

即使某些情况下不崩,也不代表它是正确写法。

这类例子只适合理解:

  • 成员函数代码不在对象内部;
  • 成员变量访问依赖 this
  • 空指针调用成员函数存在风险。

实际写代码时,一定要保证指针有效后再调用成员函数。


九、C 和 C++ 实现 Stack 的对比

9.1 C 语言实现 Stack 的特点

在 C 语言中,通常先定义结构体:

cpp 复制代码
typedef int STDataType;

typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

然后再定义一组函数:

cpp 复制代码
void STInit(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
STDataType STTop(ST* ps);
bool STEmpty(ST* ps);
void STDestroy(ST* ps);

使用时:

cpp 复制代码
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);

while (!STEmpty(&s))
{
    printf("%d\n", STTop(&s));
    STPop(&s);
}

STDestroy(&s);

这种写法的特点是:

  • 数据放在结构体中;
  • 操作数据的函数在结构体外;
  • 每次调用函数都要手动传 &s
  • 如果结构体成员暴露,外部可以直接修改内部数据。

9.2 C++ 实现 Stack 的方式

C++ 可以把数据和函数都放到类里面:

cpp 复制代码
#include <iostream>
#include <cassert>
#include <cstdlib>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    void Init(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (_a == nullptr)
        {
            perror("malloc fail");
            return;
        }

        _capacity = n;
        _top = 0;
    }

    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
            if (tmp == nullptr)
            {
                perror("realloc fail");
                return;
            }

            _a = tmp;
            _capacity = newcapacity;
        }

        _a[_top++] = x;
    }

    void Pop()
    {
        assert(_top > 0);
        --_top;
    }

    STDataType Top()
    {
        assert(_top > 0);
        return _a[_top - 1];
    }

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

    void Destroy()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

使用时:

cpp 复制代码
int main()
{
    Stack s;
    s.Init();

    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);

    while (!s.Empty())
    {
        cout << s.Top() << endl;
        s.Pop();
    }

    s.Destroy();

    return 0;
}

9.3 C++ 版本看起来改变了什么?

从使用方式上看,C++ 版本更自然。

C 语言风格:

cpp 复制代码
STPush(&s, 1);
STPop(&s);
STTop(&s);

C++ 风格:

cpp 复制代码
s.Push(1);
s.Pop();
s.Top();

C++ 版本不需要手动传 &s

原因是:

成员函数调用时,this 指针会隐含传递当前对象地址。

例如:

cpp 复制代码
s.Push(1);

可以粗略理解成:

cpp 复制代码
Push(&s, 1);

只是这个过程由编译器自动完成。


9.4 C 和 C++ 实现 Stack 的核心区别

对比项 C 语言版本 C++ 类版本
数据保存 struct Stack class Stack
函数位置 全局函数 成员函数
调用方式 STPush(&s, x) s.Push(x)
对象地址传递 手动传结构体指针 this 隐含传递
数据保护 容易被外部直接修改 成员变量可以设为 private
接口表达 函数操作结构体 对象调用自己的方法

十、重新理解封装

10.1 封装不是把代码写复杂

有些初学者刚接触类时,会觉得:

以前 C 语言写结构体加函数挺清楚的,为什么 C++ 非要包成类?

其实类不是为了把代码写复杂。

它是为了在代码规模变大后,让代码更可控。

比如对于栈,外部真正需要使用的是:

cpp 复制代码
Push()
Pop()
Top()
Empty()

而不是直接修改:

cpp 复制代码
_a
_top
_capacity

这些内部成员应该由类自己维护。


10.2 封装让接口和实现分离

一个设计良好的类,通常会把成员分成两部分:

cpp 复制代码
public:
    // 对外接口

private:
    // 内部实现细节

外部通过 public 接口使用对象。

内部通过 private 成员维护对象状态。

这样做的好处是:

  • 外部使用更简单;
  • 内部数据更安全;
  • 后续修改实现时,对外影响更小。

比如以后 Stack 不想用动态数组实现,改成链表实现。

只要接口不变:

cpp 复制代码
Push()
Pop()
Top()
Empty()

外部使用代码就可以尽量不用改。


十一、常见错误总结

11.1 类定义结尾忘记分号

错误:

cpp 复制代码
class Date
{
private:
    int _year;
}

正确:

cpp 复制代码
class Date
{
private:
    int _year;
};

11.2 类外访问 private 成员

错误:

cpp 复制代码
Date d;
d._year = 2024;

如果 _yearprivate,类外不能直接访问。

正确做法是通过公开接口:

cpp 复制代码
d.Init(2024, 3, 31);

11.3 类外定义成员函数忘记加类域

错误:

cpp 复制代码
void Init(int year, int month, int day)
{
    _year = year;
}

正确:

cpp 复制代码
void Date::Init(int year, int month, int day)
{
    _year = year;
}

11.4 以为类定义时就分配了对象空间

类只是类型定义,不是对象。

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

这只是说明 Date 对象应该长什么样。

只有写:

cpp 复制代码
Date d;

才真正创建对象。


11.5 以为成员函数存储在每个对象中

成员函数不会在每个对象里保存一份。

对象主要保存成员变量。

成员函数代码存放在公共代码区,所有对象共享。


11.6 误以为 this 指针存在对象里

this 是成员函数调用时隐含传递的参数。

对象里面不会额外存一个 this 指针。


十二、全文总结

13.1 本文核心内容

本文主要讲了 C++ 类和对象入门中的几个核心知识点:

  • 类的定义;
  • 成员变量和成员函数;
  • 访问限定符;
  • classstruct 的区别;
  • 类域;
  • 对象实例化;
  • 对象大小;
  • 内存对齐;
  • this 指针;
  • C 和 C++ 实现 Stack 的区别;
  • 封装思想。

相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_5:技能测试——变量实战
java·开发语言·前端·javascript
赵民勇1 小时前
如何查看一个二进制程序是否设置了rpath或runpath?
linux·c++
Brilliantwxx1 小时前
【C++】 哈希表 unordered_map 与 unordered_set(底层原理 + 线性哈希表代码实现)
开发语言·c++·散列表
瑞雪兆丰年兮1 小时前
[0开始学Java|第二十四天]集合(Map&可变参数&集合工具类Collections)
java·开发语言·map·collections
AC赳赳老秦1 小时前
用 OpenClaw 整理团队技术分享:自动提取 PPT 内容、生成文字稿、同步到知识库
开发语言·python·自动化·powerpoint·wpf·deepseek·openclaw
whatever who cares1 小时前
android中fragment demo举例
android·java·开发语言
Vallelonga1 小时前
Rust 生命周期标注积累
开发语言·rust
ouliten1 小时前
C++笔记:C++20风格线程池
c++·笔记·c++20
caimouse1 小时前
mshtml/nsio.c 实现报告
c语言·开发语言