从结构体到对象:正式学习C++类的骨架、封装与this指针

文章目录

    • [1. C++在C后的延伸](#1. C++在C后的延伸)
    • [2. 类的基本骨架:成员、类域和定义位置](#2. 类的基本骨架:成员、类域和定义位置)
      • [2. 类定义中放什么](#2. 类定义中放什么)
      • [2.2 类内定义与类外定义](#2.2 类内定义与类外定义)
      • [2.3 class 和 struct 的差别](#2.3 class 和 struct 的差别)
    • [3. 访问限定符:封装不是仅仅把数据藏起来](#3. 访问限定符:封装不是仅仅把数据藏起来)
      • [3.1 public 和 private](#3.1 public 和 private)
      • [3.2 protected 先知晓边界即可](#3.2 protected 先知晓边界即可)
    • [4. 对象大小:成员变量属于对象,成员函数不按对象重复保存](#4. 对象大小:成员变量属于对象,成员函数不按对象重复保存)
    • [5. this 指针:成员函数如何找到当前对象](#5. this 指针:成员函数如何找到当前对象)
      • [5.1 从两个对象调用同一个函数](#5.1 从两个对象调用同一个函数)
      • [5.2 `this` 的几个性质](#5.2 this 的几个性质)
      • [5.3 空指针调用成员函数为什么有时不马上崩溃](#5.3 空指针调用成员函数为什么有时不马上崩溃)
    • [7. 小结](#7. 小结)

1. C++在C后的延伸


C++在保留了大量C的表达方式的同时,增加了更强的类型组织和抽象工具,一些数据与操作数据的函数可以被组织到同一个类中,这正是C++面向对象的开始。

在C中,我们可能这样表示一个日期:

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

void DateInit(Date* d, int year, int month, int day)
 {
    d->year = year;
    d->month = month;
    d->day = day;
}

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

调用时必须把对象地址显式传给每一个函数:

cpp 复制代码
Date d;
DateInit(&d, 2026, 9, 24);
DatePrint(&d);

这种写法可以工作,但是它把"哪些函数属于Date"交给了命名习惯来维护。函数写错参数、直接修改不应该暴露的数据与忘记检查日期等等操作是否合乎规则,都只能依靠调用者自觉,安全性较低。

而C++类则把这层关系写进类型本身:

cpp 复制代码
class Date 
{
public:
    void Init(int year, int month, int day);
    void Print() const;

private:
    int _year = 0;
    int _month = 0;
    int _day = 0;
};

调用时变成:

cpp 复制代码
Date d;
d.Init(2026, 9, 24);
d.Print();

2. 类的基本骨架:成员、类域和定义位置


2. 类定义中放什么

cpp 复制代码
class Counter 
{
public:
    void Add(int value) 
    {
        _value += value;
    }

    int Get() const 
    {
        return _value;
    }

private:
    int _value = 0;
};

这里定义的是一个计数器对象,其拥有其的私有成员变量_value,然后拥有两个成员函数,一个用于获取当前计数,一个用于增加指定数量。

class Counter { ... }; 中的内容叫类体。类体里的变量叫成员变量,函数叫成员函数。类定义最后的分号不能省略,因为整个类定义在语法上也是一条声明。

下面我们创建两个对象first和second,让其增加不同的计数:

cpp 复制代码
Counter first;
Counter second;

first.Add(3);
second.Add(10);
std::cout << "first当前计数:" << first.Get() << std::endl;
std::cout << "second当前计数:" << second.Get() << std::endl;

运行示例:

可以看到,first和second是两个对象,各自拥有一份_value。

2.2 类内定义与类外定义

代码量较少的成员函数可以直接写在类内;较长的函数则通常在类外定义,让类体更像一份接口说明:

cpp 复制代码
class Counter 
{
public:
    void Add(int value);
    int Get() const;

private:
    int _value = 0;
};

void Counter::Add(int value) 
{
    _value += value;
}

int Counter::Get() const 
{
    return _value;
}

Counter::Add 中的::表示"到 Counter 这个类域中寻找Add"。如果省略 Counter::,编译器会把它当成普通的全局函数,无法直接访问 _value。

对于成员函数的函数头后加的const表示的是此函数不会修改对象的相关成员变量,是保护数据的一种方式,如果函数不需要修改成员变量的话建议加上,增加代码可靠性。

2.3 class 和 struct 的差别

C++ 中的 struct 也可以包含成员函数和访问限定符:

cpp 复制代码
struct Point 
{
    void Print() const;

    int _x = 0;
    int _y = 0;
};

但两者的主要默认规则不同:class 的默认访问权限是 private,struct 的默认访问权限是 public。

C++ 中可以继续使用 struct 表示简单数据,也可以使用 class 表示需要封装行为的类型。关键不在于关键字哪个"更高级",而在于你是否清楚哪些成员应该暴露给外部

  • struct:适合纯数据载体(类似 C 结构体),数据默认公开,简单方便;
  • class:适合需要封装的类型,内部数据默认私有,控制对外接口。

3. 访问限定符:封装不是仅仅把数据藏起来


3.1 public 和 private

cpp 复制代码
class Counter 
{
public:
    void Add(int value) 
    {
        if (value > 0) 
        {
            _value += value;
        }
    }

    int Get() const 
    {
        return _value;
    }

private:
    int _value = 0;
};

当外部想要使用成员变量_value时,只能通过 Add 和 Get 使用:

cpp 复制代码
SafeCounter counter;
counter.Add(5);
counter._value = -100; // 编译错误:_value 是 private

封装的价值是把规则集中在类内部。以后如果计数器需要限制最大值,只改 Add 就可以;调用者不需要知道 _value 的具体表示方式。

访问限定符的作用范围从它出现的位置开始,一直持续到下一个访问限定符或类定义结束(默认是private):

cpp 复制代码
class Example 
{
    int _privateValue = 0; // 默认 private

public:
    void Set(int value) 
    {
        _privateValue = value;
    }

private:
    int _anotherValue = 0;
};

3.2 protected 先知晓边界即可

protected 在类外不能直接访问,和 private 在当前阶段的表现相同。它主要会在继承中体现差异。开始学习类和对象时,只需要重点明白:需要对外提供的操作放到 public,表示细节的数据和辅助函数通常放到 private。

4. 对象大小:成员变量属于对象,成员函数不按对象重复保存


观察下面的类型:

cpp 复制代码
class Small 
{
public:
    void Print() const {}

private:
    int _value = 0;
};

class Empty 
{
public:
    void Print() const {}
};

第一个Small类中有一个成员变量,而Empty中无成员变量,让我们看看其各自的对象大小:

cpp 复制代码
std::cout << "Small类大小:" << sizeof(Small) << std::endl;
std::cout << "Empty类大小:" << sizeof(Empty) << std::endl;

运行示例:

可以看到,Small 至少需要容纳一个 int(4个字节)。Empty 没有成员变量,通常仍然占 1 个字节,用来区分不同对象的地址。

成员函数编译后是指令,不需要在每个对象里复制一份,因此一般不计入对象大小。

这就引出了成员变量与成员函数的不同存放方式:

成员变量 vs 成员函数

  • 成员变量: 每个对象独立一份,其对应内部的成员变量有各自的内存空间。
  • 成员函数: 整个程序里只有一份,所有对象共用。

成员函数的代码通常保存一份,调用时会根据对象不同而操作对应的成员变量。

其内部会依靠编译器隐式传入的this指针实现。

类的大小实际和C中结构体相似,还是会受到内存对齐影响,规则类似:

  1. 第一个成员通常从对象起始地址开始;
  2. 后续成员可能为了满足对齐要求产生填充;
  3. sizeof 是编译器根据布局规则计算出的结果,不要只用成员大小简单相加。

5. this 指针:成员函数如何找到当前对象


5.1 从两个对象调用同一个函数

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

    void Print() const 
    {
        std::cout << _year << '/' << _month << '/' << _day << std::endl;
    }

private:
    int _year = 0;
    int _month = 0;
    int _day = 0;
};

int main() 
{
    Date d1;
    Date d2;
    d1.Init(2026, 9, 24);
    d2.Init(2027, 1, 1);
    d1.Print();
    d2.Print();
}

Init 的函数体没有写 d1 或 d2,但两次调用结果不同。原因就是是非静态成员函数会有一个编译器隐含传入的参数:this。

可以把调用过程近似理解为:

text 复制代码
d1.Init(2026, 9, 24) -> 类似 Init(&d1, 2026, 9, 24)
d2.Init(2027, 1, 1); -> 类似 Init(&d2, 2027, 1, 1)

在真实的 C++ 语法中,我们不需要在调用时手写这个参数,但成员函数内部确实可以使用 this:

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

在编写时通常可以省略 this->,因为编译器知道 _year 是当前对象的成员。显式写出this指针常用于成员变量和参数同名时,或者是为了强调当前对象。

5.2 this 的几个性质

  • this 是成员函数内部可用的指针;
  • 它指向调用当前成员函数的对象;
  • 不能修改 this 让它指向另一个对象;
  • 静态成员函数没有 this,因为它不依附于某个具体对象。

5.3 空指针调用成员函数为什么有时不马上崩溃

观察并运行以下代码:

cpp 复制代码
class Demo 
{
public:
    void Hello() const 
    {
        std::cout << "hello" << std::endl;
    }

    void PrintValue() const 
    {
        std::cout << _value << std::endl;
    }

private:
    int _value = 10;
};

int main() {
    Demo* ptr = nullptr;
    ptr->Hello();       // 可能看起来能运行
    ptr->PrintValue(); // 访问成员数据,通常会崩溃
}

运行示例:

从语言规则上说,通过空指针调用非静态成员函数本身就是未定义行为,不能因为某次运行没有崩溃就认为它是安全的。

PrintValue 需要通过this 找到 _value,所以更容易触发崩溃。

注意不要把这种使用方式当作技巧,而是通过这个例子帮助我们理解this指针的存在与作用。

7. 小结


类不是把 C 语法换一种写法,而是把数据、操作和访问边界组织到一个类型中。对象拥有自己的状态,成员函数通过隐含的 this 找到当前对象,访问限定符则决定外部能否直接触碰这些状态。理解这三层关系后,构造函数、析构函数和运算符重载就不再是孤立语法,而是继续完善"对象类型"的工具。

相关推荐
乌暮1 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
2601_962218612 小时前
C语言中三种库的编译和使用方法
开发语言·c++
jimy12 小时前
派生类重写基类虚函数(二):override的作用
开发语言·c++
小雪崩2 小时前
嵌入式学习 day60:ARM汇编——触摸屏与SPI
汇编·arm开发·学习
ebiobiz2 小时前
基于 GD32 Embedded Builder (GEB) 与 Nimmake 的 MCU 工程搭建指南
c++·python·单片机·嵌入式硬件·mcu
hetao17338372 小时前
2026-09-29 hetao1733837 的刷题记录
c++·算法
:mnong2 小时前
几何内核“四大金刚“:ACIS、Parasolid、CGM、Open CASCADE 核心特征深度解析
c++·cad·cax
by209993 小时前
从 malloc/free 到 new/delete:真正理解 C++ 中的内存管理与对象生命周期
c语言·c++·经验分享·动态内存管理
问天_观心3 小时前
大模型训练与推理优化(二)
人工智能·深度学习·学习·大模型·transformer