在 C++ 的学习中,类和对象是面向对象编程的基石,它实现了面向对象三大特性中的封装,让代码的管理和复用性大幅提升。本文将从类的定义、实例化、对象大小计算、this 指针四个核心维度,梳理 C++ 类和对象的基础知识点,同时对比 C 语言的实现方式,理解 C++ 封装的本质。
一、类的定义:抽象事物的属性与行为
类是对现实事物的抽象描述,将事物的属性 (成员变量)和行为 (成员函数)封装在一起,是创建对象的 "模板"。
1.1 类的定义格式
使用class关键字定义类,语法格式如下:
cpp
class 类名
{
// 访问限定符 + 类的成员(成员变量/成员函数)
};// 注意:分号不能省略!
- 成员变量 :也叫类的属性,建议添加特殊标识(如
_前缀 / 后缀、m_开头)区分普通变量,这是行业惯例而非 C++ 语法强制要求。 - 成员函数 :也叫类的方法,定义在类内的成员函数默认是
inline(内联函数),减少函数调用开销。 - 特殊说明 :C++ 兼容 C 的
struct,并将其升级为类 ------struct中可以定义成员函数,且无需typedef即可直接作为类型使用。class和struct的核心区别是默认访问权限不同(后文详述)。
1.2 访问限定符:实现封装的核心手段
封装是面向对象的核心特性之一,C++ 通过访问限定符 选择性地开放类的接口,限制外部对类内部成员的直接访问,本质是对数据和方法的严格管理。C++ 提供三种访问限定符,核心规则如下
C++ 提供三种访问限定符,核心规则如下:
| 访问限定符 | 类外访问性 | 作用域 |
|---|---|---|
public |
可直接访问 | 从当前位置到下一个限定符 / 类结束 |
protected |
不可直接访问 | 从当前位置到下一个限定符 / 类结束 |
private |
不可直接访问 | 从当前位置到下一个限定符 / 类结束 |
关键注意点:
protected和private在类外的访问权限一致,区别仅在继承中体现;- 类内成员无访问限制,即使是
private成员,类内的成员函数也可访问; - 默认访问权限:
class默认private,struct默认public(兼容 C 的设计); - 工程惯例:成员变量设为
private/protected(避免外部随意修改),对外提供的接口函数设为public。
1.3 类域:类的专属作用域
类定义了一个新的局部作用域 ,类的所有成员都属于这个类域。当类的声明和定义分离 (成员函数写在类外)时,需要使用 作用域操作符:: 指明成员所属的类,否则编译器会将其当作全局函数处理。
示例:类外定义成员函数
cpp
class Stack
{
public:
void Init(int n = 4); // 类内声明
private:
int* array;
size_t capacity;
size_t top;
};
// 类外定义,需用Stack::指明类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
// ... 初始化逻辑
}
二、类的实例化:从 "模板" 到 "实体"
2.1 实例化的概念
用类类型在物理内存中创建对象 的过程,称为类的实例化。核心认知需明确:
- 类是抽象的模板 ,仅声明成员变量,不分配实际物理内存,无法直接存储数据;
- 对象是类的具体实例,实例化时会为对象分配物理内存,存储成员变量;
- 一个类可以实例化多个独立对象 ,每个对象拥有专属的成员变量,互不干扰。
形象类比:类是建筑设计图,仅规划房间布局;对象是根据设计图建造的房子,拥有实际的空间,可存储物品(数据),可以建多个房子。
示例:countday 类的实例化
cpp
#include <iostream>
using namespace std;
class countday
{
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;
};
int main()
{
countday d1, d2; // 实例化对象,分配物理内存
d1.Init(2024, 3, 31); // 调用成员函数初始化
d2.Init(2024, 7, 5);
d1.Print(); // 调用成员函数打印
d2.Print();
return 0;
}
2.2 对象的大小计算:仅存储成员变量
类的成员分为成员变量 和成员函数 ,但对象的内存中仅存储成员变量 ,成员函数会被存储在公共代码段(所有对象共享),原因如下:
- 成员函数编译后是一段指令,无需为每个对象重复存储,否则会造成严重的内存浪费;
- 所有对象调用的成员函数是同一个,仅操作的成员变量不同(通过 this 指针区分,后文详述)。
因此,对象的大小 = 其成员变量的大小之和 + 内存对齐的填充字节 ,完全遵循 C++ 的内存对齐规则。
1. 内存对齐规则(与结构体一致)
- 第一个成员在偏移量为 0 的地址处;
- 其他成员对齐到对齐数的整数倍地址处(对齐数 = 编译器默认对齐数 与 成员大小的较小值,VS 默认 8,GCC 默认 4);
- 类的总大小为最大对齐数的整数倍;
- 嵌套类 / 结构体时,嵌套的部分对齐到自身最大对齐数的整数倍,整体大小为所有最大对齐数的整数倍。
2. 特殊情况:空类 / 无成员变量的类
空类(如class C{})或仅含成员函数的类,实例化的对象大小为1 字节 。这 1 字节无实际数据意义,仅为占位标识,证明对象在内存中存在。
示例:对象大小计算
cpp
class A
{
public:
void Print() {}
private:
char _ch; // 1字节
int _i; // 4字节
};
class B { public: void Print() {} };
class C {};
// 内存对齐后:A的大小=8(1+3填充+4),B=1,C=1
cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C) << endl;
三、this 指针:解决对象的成员区分问题
3.1 this 指针的产生原因
同一个类的多个对象,共享类的成员函数,但拥有独立的成员变量。那么成员函数如何知道操作的是哪个对象的成员变量 ?C++ 通过隐含的 this 指针 解决该问题:编译器会为每个非静态成员函数,自动添加一个形参第一个位置的类类型 const 指针,即 this 指针,用于指向调用该函数的对象。
3.2 this 指针的核心特性
- 隐式传递 :编译器自动在形参添加
类名* const this,调用函数时自动将对象地址作为实参传递,程序员不能在形参 / 实参位置显式书写; - 显式使用 :函数体内可显式使用 this 指针访问成员变量 / 成员函数,如
this->_year = year;,编译器默认省略 this 指针; - 不可修改 :this 指针是
const指针,指向不可变(不能执行this = nullptr;),保证始终指向调用函数的对象; - 存储位置 :this 指针作为函数形参,存储在栈区(而非对象内部,不占用对象的内存空间)。
示例:this 指针的隐式与显式使用
cpp
class Date
{
public:
// 编译器实际处理为:void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
this->_year = year; // 显式使用this
_month = month; // 隐式使用this,编译器自动补全this->_month
this->_day = day;
}
private:
int _year, _month, _day;
};
int main()
{
Date d1;
d1.Init(2024, 3, 31); // 编译器实际处理为:d1.Init(&d1, 2024, 3, 31);
return 0;
}
3.3 this 指针的经典考题分析
通过两道考题理解 this 指针的工作机制:
cpp
class A
{
public:
void Print() { cout << "A::Print()" << endl; }
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // 结果:正常运行(C)
return 0;
}
分析 :Print函数是成员函数,存储在公共代码段,调用时仅需传递 this 指针(p=nullptr),但函数体内未访问任何成员变量 (无需解引用 this 指针),因此不会触发空指针访问崩溃。
cpp
class A
{
public:
void Print() { cout << _a << endl; } // 隐含this->_a
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print(); // 结果:运行崩溃(B)
return 0;
}
分析 :函数体内访问_a,编译器会补全为this->_a,而 this 指针为 nullptr,解引用空指针会导致程序运行崩溃。
四、C++ 与 C 实现栈的对比:理解封装的优势
C 语言是面向过程的编程语言,数据和函数分离;C++ 通过类实现了数据和方法的封装,结合访问限定符和 this 指针,让代码更简洁、安全、易维护。以下是 C 和 C++ 实现 Stack 的核心区别:
| 特性 | C 语言实现 | C++ 实现 |
|---|---|---|
| 数据与函数关系 | 数据(结构体)和函数分离,函数需传结构体地址 | 数据和函数封装在类内,通过 this 指针隐式传递对象地址 |
| 访问控制 | 无访问限定符,可直接修改结构体成员 | 成员变量设为 private,仅通过 public 接口操作,避免乱修改 |
| 语法便捷性 | 需 typedef 重命名结构体,函数参数繁琐 | 类名直接作为类型,成员函数可设缺省参数,调用更简洁 |
| 封装性 | 无封装,数据安全性低 | 封装性强,数据和方法统一管理,安全性高 |
五、核心知识点总结
- 类是抽象模板,对象是类的具体实例,实例化才会分配物理内存 ,对象仅存储成员变量,成员函数共享在公共代码段;
- 访问限定符(public/protected/private)是 C++ 实现封装的核心,工程中成员变量私有化,接口公有化;
- 类域是专属作用域,类外定义成员函数需用
::指明类域; - this 指针是编译器隐式添加的 const 指针,指向调用成员函数的对象,存储在栈区,解决对象的成员区分问题;
- 对象大小遵循内存对齐规则,空类 / 无成员变量的类对象大小为 1 字节(占位标识);
- C++ 的封装本质是对数据和方法的严格管理,将数据和操作数据的方法封装在一起,提升代码的安全性和可维护性。