C++ 类和对象(一):从概念到实践,吃透类的核心基础

作为 C++ 面向对象编程(OOP)的基石,类和对象是从面向过程思维转向面向对象思维的关键。这篇文章是「C++ 类和对象系列」的第一篇,将带你从类的定义、访问控制、实例化到 this 指针,循序渐进理解核心概念,每个知识点都搭配代码示例和通俗解释,帮你夯实基础~

一、类的定义:把数据和行为 "打包"

在面向过程编程中,我们习惯把数据和函数分开定义(比如 C 语言中用结构体存数据,单独写函数操作数据)。而类的核心思想是封装------ 把数据(属性)和操作数据的函数(方法)放在一起,形成一个独立的 "模块"。

1. 类的定义格式

cpp 复制代码
class 类名 {
    // 访问限定符(public/protected/private)
    // 成员变量(属性)
    // 成员函数(方法)
}; // 注意:分号不能省略!
  • class:定义类的关键字,相当于 "模板" 的声明;
  • 类名:自定义标识符(如 Stack、Date),后续可作为类型使用;
  • 类体:包含成员变量和成员函数,由访问限定符控制访问权限;
  • 分号:类定义结束的标志,不可遗漏(哪怕类体为空)。

2. 成员的命名规范

为了区分成员变量和普通局部变量,通常会给成员变量加特殊标识

  • 前缀加_:如_year_a(最常用);
  • 前缀加m_:如m_topm_capacity(表示 member);
  • 后缀加_:如age_name_。

3. class vs struct:都能定义类

C++ 兼容 C 语言的struct,同时将其升级为类:

  • struct中可以定义函数(C 语言中不行);
  • 核心区别:访问权限默认值不同
    • class默认私有(private);
    • struct默认公有(public)(为了兼容 C 语言结构体的使用习惯);
  • 建议:优先用class定义类,struct仅在需要兼容 C 或表示简单数据集合时使用。

4. 成员函数的两种定义方式

(1)类内定义(默认 inline)

成员函数直接写在类体中,编译器会默认将其视为inline函数(内联函数,适合短小函数):

cpp 复制代码
class Stack {
public:
    // 类内定义,默认inline
    void Push(int x) {
        // 逻辑实现
    }
private:
    int* _a;
    int _top;
    int _capacity;
};

(2)类内声明 + 类外定义(非 inline)

成员函数仅在类内声明,定义写在类外,需要用::(作用域解析符)指明所属类域:

cpp 复制代码
class Date {
public:
    // 类内声明
    void Init(int year, int month, int day);
private:
    int _year;
    int _month;
    int _day;
};

// 类外定义,必须加 Date:: 指明类域
void Date::Init(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
}

⚠️ 注意:类外定义时,::不可省略!否则编译器会把函数当成全局函数,导致 "未声明" 错误。

5. 类名就是类型

和 int、char 一样,类名可以直接作为类型使用(无需 typedef,这是 C++ 的优势):

cpp 复制代码
struct ListNodeCPP { // struct定义的类
    void Init(int x) {
        _next = nullptr;
        _val = x;
    }
    ListNodeCPP* _next; // 类名作为指针类型
    int _val;
};

// 直接用类名定义对象,无需typedef
ListNodeCPP node;
node.Init(10);

二、访问限定符:控制 "访问权限"

封装的核心不仅是 "打包",还要 "控制访问"------ 哪些成员能在类外直接使用,哪些只能在类内使用。C++ 提供 3 种访问限定符:

1. 访问限定符说明

|-----------|------|------|--------|
| 限定符 | 类内访问 | 类外访问 | 继承中使用 |
| public | 可以 | 可以 | 子类可访问 |
| protected | 可以 | 不可以 | 子类可访问 |
| private | 可以 | 不可以 | 子类不可访问 |

2. 关键规则

  • 访问权限的作用域:从该限定符出现的位置开始,到下一个限定符出现或类结束为止;
cpp 复制代码
#include <iostream>
using namespace std;

class Stack {
    // 无访问限定符,class默认private
    void Push(int x) {} // private:类外不可访问
public:
    void Pop() {} // public:类外可访问
    int Top() { return 0; } // public:类外可访问
private:
    int* _a; // private:类外不可访问
    int _top;
    int _capacity;
};

int main() {
    Stack st;
    st.Pop(); // 正确:public成员
    st.Top(); // 正确:public成员
    // st.Push(10); 错误:private成员,类外无法访问
    // st._a = nullptr; 错误:private成员,类外无法访问
    return 0;
}

3. 为什么需要访问控制?

  • 保护数据安全:避免类外代码随意修改成员变量(比如栈的_top指针,随意修改会导致栈结构混乱);
  • 隐藏实现细节:类外只需关注 "如何使用"(public 方法),无需关心 "如何实现"(private 成员),降低耦合度。

三、类域:解决 "命名冲突"

类定义了一个独立的作用域(类域),类的所有成员都在这个作用域内。这意味着:

  • 不同类可以定义同名成员(函数 / 变量),不会冲突;
  • 类外访问成员时,必须通过对象 / 指针 / 引用,或用::指明类域(仅静态成员);
  • 类外定义成员函数时,必须用::指明类域(否则视为全局函数)。
cpp 复制代码
// 类A的类域
class A {
public:
    void Print() { cout << "A::Print()" << endl; }
};

// 类B的类域,可定义同名函数Print
class B {
public:
    void Print() { cout << "B::Print()" << endl; }
};

int main() {
    A a;
    B b;
    a.Print(); // 调用A::Print()
    b.Print(); // 调用B::Print(),无冲突
    return 0;
}

四、实例化

类本身只是一个 "模板"(相当于设计图纸),不占用物理内存;只有通过实例化(创建对象),才会在内存中分配空间,存储成员变量。

cpp 复制代码
class Date { // 类:模板,不占内存
public:
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year; // 成员变量:仅声明,不占内存
    int _month;
    int _day;
};

int main() {
    Date d1; // 实例化:创建对象d1,分配内存(存储_year/_month/_day)
    Date d2; // 一个类可以实例化多个对象,每个对象独立存储成员变量
    
    d1.Init(2025, 11, 28);
    d2.Init(2025, 11, 29);
    return 0;
}

2. 对象的大小:只存成员变量,不存成员函数

遵循内存对齐规则。
对象中仅存储成员变量,成员函数存储在代码段(所有对象共享)。

为什么?

  • 成员函数是一组固定的指令,所有对象执行时逻辑完全相同,没必要每个对象都存一份(浪费空间);
  • 成员函数的地址在编译链接时就已确定(静态绑定,除了动态多态),调用时直接通过地址执行,无需对象存储。
cpp 复制代码
#include <iostream>
using namespace std;

class A {
public:
    void Print() {} // 成员函数,不占对象空间
private:
    char _ch; // 1字节
    int _i; // 4字节
};

class B {}; // 空类:无成员变量
class C {};

int main() {
    A a;
    B b;
    C c;
    cout << sizeof(a) << endl; // 8字节(内存对齐后)
    cout << sizeof(b) << endl; // 1字节
    cout << sizeof(c) << endl; // 1字节
    return 0;
}

3. 内存对齐:对象大小的 "隐形规则"

为什么class A的对象大小是 8 字节,而不是 1+4=5 字节?------ 因为内存对齐

(1)内存对齐的目的

  • 硬件限制:CPU 读取内存时,不是逐字节读取,而是按 "块大小"(如 4 字节、8 字节)读取;
  • 性能优化:如果数据跨越两个 "块",CPU 需要读取两次;对齐后只需读取一次,用空间换时间

(2)空类的大小为什么是 1 字节?

空类(无成员变量)的对象大小为 1 字节,目的是占位------ 表示这个对象在内存中存在,避免多个空类对象地址重叠。

五、this 指针

当我们调用对象的成员函数时,函数如何知道操作的是哪个对象的成员变量?答案是**this指针。**

1. this 指针的本质

  • this指针是编译器自动添加到成员函数形参列表的隐含指针,指向当前调用该函数的对象;
  • 它的类型是类名* const(如Date* const this),表示指针本身不可修改(不能指向其他对象),但可以修改指向对象的成员;
  • 我们不能在形参或实参中显式写this,但可以在成员函数内部显式使用

2. 编译器的 "幕后操作"

我们写的代码:

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

int main() {
    Date d1;
    d1.Init(2025, 11, 28);
    return 0;
}

编译器实际处理后的代码(简化):

cpp 复制代码
// 编译器给成员函数添加this形参
void Date::Init(Date* const this, int year, int month, int day) {
    this->_year = year; // 隐含通过this访问成员变量
    this->_month = month;
    this->_day = day;
}

int main() {
    Date d1;
    // 编译器自动传入对象地址作为this实参
    Date::Init(&d1, 2025, 11, 28);
    return 0;
}

3. this 指针的使用场景

(1)区分成员变量和局部变量

当成员变量和局部变量同名时,用this指针明确指向成员变量:

cpp 复制代码
class Person {
public:
    void SetAge(int age) {
        this->age = age; // this->age 指成员变量,age指局部变量
    }
private:
    int age;
};

(2)返回当前对象本身

在链式调用中常用(如 string 类的 append、operator+=):

cpp 复制代码
class Date {
public:
    Date& AddDay(int day) {
        _day += day;
        return *this; // 返回当前对象的引用
    }
private:
    int _year, _month, _day;
};

// 链式调用
Date d;
d.Init(2025, 11, 28);
d.AddDay(1).AddDay(2); // 先加1天,再加2天

4. this 指针的存储位置

  • 通常存储在栈上(作为函数形参)
  • 部分编译器(如 VS)会优化,将其存储在寄存器(如 ecx)中,提高访问效率;
  • 注意:调用成员函数时,对象必须存在
相关推荐
CoderYanger1 小时前
优选算法-优先级队列(堆):75.数据流中的第K大元素
java·开发语言·算法·leetcode·职场和发展·1024程序员节
TracyCoder1232 小时前
MySQL 实战宝典(八):Java后端MySQL分库分表工具解析与选型秘籍
java·开发语言·mysql
非凡的世界2 小时前
为什么我和越来越多的PHP程序员,选择了 Webman ?
开发语言·php·workman·webman
Dream it possible!2 小时前
LeetCode 面试经典 150_图_克隆图(90_133_C++_中等)(深度优先:DFS)
c++·leetcode·面试·
MarkHD2 小时前
车辆TBOX科普 第45次
java·开发语言
一个平凡而乐于分享的小比特2 小时前
UCOSIII笔记(十四)时间戳
笔记·时间戳·ucosiii
还债大湿兄2 小时前
阿里通义千问调用图像大模型生成轮动漫风格 python调用
开发语言·前端·python
鸭子程序员2 小时前
c++ 算法
开发语言·c++·算法
YJlio2 小时前
ShareEnum 学习笔记(9.5):内网共享体检——开放共享、匿名访问与权限风险
大数据·笔记·学习