类和对象是 C++ 面向对象编程的核心概念,它们为代码提供了更好的封装性、可读性和可维护性。本文将从类的定义开始,逐步讲解访问限定符、类域、实例化、对象大小计算、this 指针等关键知识,并对比 C 语言与 C++ 在实现数据结构时的差异,快速掌握类和对象的基础用法。
1. 类的定义
类是对现实事物的抽象描述,它封装了数据(成员变量)和操作数据的方法(成员函数)。在 C++ 中,我们通过class
关键字定义类,其基本结构如下:
1.1 类定义格式
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省
略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或
者成员函数。
C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是
struct中可以定义函数,一般情况下我们还是推荐用class定义类。
定义在类面的成员函数默认为 inline 函数,声明和定义分离就不是了。
class 类名 {// 成员变量(属性)// 成员函数(方法)
}; // 注意:分号不能省略
示例:Stack 类
下面是一个栈(Stack)类的定义:
#include<iostream>
#include<assert.h>
#include<stdlib.h>
using namespace std;class Stack {
public:// 成员函数:初始化栈void Init(int n = 4) {array = (int*)malloc(sizeof(int) * n);if (nullptr == array) {perror("malloc申请空间失败");return;}capacity = n; // 容量top = 0; // 栈顶指针(指向待插入位置)}// 成员函数:入栈void Push(int x) {// 扩容逻辑(简化)array[top++] = x;}// 成员函数:取栈顶元素int Top() {assert(top > 0); // 确保栈非空return array[top - 1];}// 成员函数:销毁栈void Destroy() {free(array);array = nullptr;top = capacity = 0;}private:// 成员变量(加_区分普通变量)int* array; // 存储数据的数组size_t capacity; // 容量size_t top; // 栈顶指针
};
1.2 成员命名习惯
为了区分成员变量和普通变量,通常会在成员变量前加_
或m_
(如_year
、m_capacity
)。这不是 C++ 的强制规定,而是工程中的常见惯例,便于代码阅读。
示例:Date 类
class Date {
public:void Init(int year, int month, int day) {_year = year; // 成员变量_year_month = month; // 成员变量_month_day = day; // 成员变量_day}private:int _year; // 年份int _month; // 月份int _day; // 日期
};
1.3 class 与 struct 的区别
C++ 中的struct
也可以定义类,它与class
的主要区别是:
class
默认访问权限为private
(私有);struct
默认访问权限为public
(公有),且兼容 C 语言中struct
的用法(如仅作为数据结构)。
实际开发中,推荐用class
定义类,struct
多用于纯数据结构(如链表节点)。
示例:C++ 的 struct 类
// C++中struct可定义函数
struct ListNodeCPP
{void Init(int x){ // 成员函数:初始化节点val = x;next = nullptr;}ListNodeCPP* next; // 成员变量int val;
};//C++中不用 typedef //补充:C语言中
typedef struct ListNodeC
{int val;struct ListNodeC* next;
}LN;
2. 访问限定符
访问限定符用于控制类成员在类外的访问权限,是 C++ 实现封装的核心手段。C++ 提供三种访问限定符:
2.1 访问权限规则
限定符 | 权限说明 |
---|---|
public | 类内、类外均可访问(公有) |
private | 仅类内可访问,类外不可直接访问(私有) |
protected | 类内可访问,子类可访问,类外不可直接访问(继承时体现差异)(私有) |
访问权限的作用域从该限定符出现的位置开始,直到下一个限定符或类结束(}
)为止。
2.2 封装的意义
通过访问限定符,我们可以将成员变量设为private
,仅通过public
的成员函数操作数据,避免外部代码随意修改数据导致的错误。例如 Stack 类中,array
、top
等变量被private
保护,外部只能通过Push
、Top
等函数操作,确保栈的逻辑正确。
3. 类域
类定义了一个独立的作用域(类域),类的所有成员都属于这个作用域。在类外定义成员函数时,需要用::
(作用域操作符)指明所属的类。
3.1 类外定义成员函数
如果成员函数的声明和定义分离(类内声明,类外定义),必须通过类名::函数名
指定类域,否则编译器会将函数视为全局函数。
示例:类外定义 Stack::Init
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);if (nullptr == array) {perror("malloc失败");return;}capacity = n;top = 0;
}
3.2 类域的作用
类域确保了不同类的成员可以重名而不冲突。例如,两个类都可以有Init
函数,通过类域区分:Stack::Init
和Date::Init
。(注意:类域和命名空间域只影响名字的隔离,不影响生命周期)
4. 类的实例化
用类创建对象的过程称为实例化。类本身只是一个 “模板”,不占用实际内存(可以说是一个声明,声明不占空间);实例化后的对象才会分配内存,存储成员变量。
4.1 实例化的意义
- 类:抽象的 “设计图”,描述对象有哪些属性和方法(如 “房子设计图”);
- 对象:根据类创建的具体实例,占用内存,可存储数据(如 “用设计图盖的房子”)。
示例:实例化 Date 对象
int main() {Date d1; // 用Date类实例化对象d1Date d2; // 实例化对象d2d1.Init(2024, 3, 31); // 调用d1的Init方法d2.Init(2024, 7, 5); // 调用d2的Init方法return 0;
}
4.2 对象存储的内容
对象中只存储成员变量,成员函数被所有对象共享(存储在代码段)。原因是:
- 成员变量是每个对象的独立数据(如 d1 和 d2 的
_year
可以不同); - 成员函数是相同的操作逻辑(如 d1 和 d2 的
Init
函数代码完全一样,无需重复存储)。
5. 对象大小计算
5.1 对象存储方式
函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。
再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year/_month/_day存储各自的数据,但是d1和d2的成员函数 Init 指针却是一样的,存储在对象中就浪费了。
如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。
对象的大小由成员变量决定,遵循内存对齐规则;成员函数不占用对象的内存。
5.2 内存对齐规则
- 第一个成员在偏移量为 0 的地址;
- 其他成员对齐到 “对齐数” 的整数倍地址(对齐数 = min (编译器默认对齐数8,成员大小),VS 默认对齐数为 8);
- 对象总大小是最大对齐数的整数倍;
- 嵌套结构体时,嵌套对象对齐到自身最大对齐数的整数倍,总大小包含嵌套对象的对齐数。
5.3 示例:计算对象大小
class A {
public:void Print() { cout << _ch << endl; }
private:char _ch; // 大小1int _i; // 大小4
};class B {
public:void Print() {} // 无成员变量
};class C {}; // 空类int main() {A a;B b;C c;cout << sizeof(a) << endl; // 8(1+3填充+4)cout << sizeof(b) << endl; // 1(占位,标识对象存在)cout << sizeof(c) << endl; // 1(空类大小为1)return 0;
}
如果不对齐,则会导致编译器读取,出现错误。(编译器规定每次读的时候,是从固定的整数倍开始读固定字节的,为的是提高机器效率)
- 类 A:
_ch
(1 字节)+ 3 字节填充(对齐到 4)+_i
(4 字节),总 8 字节; - 类 B 和 C:无成员变量,但为了标识对象存在,占用 1 字节。
6. this 指针
当多个对象调用同一个成员函数时,函数如何区分操作的是哪个对象?C++ 通过隐藏的this
指针解决这个问题。
6.1 this 指针的特性
this
指针是成员函数的隐含参数,类型为类名* const
(指向当前对象,不可修改指向);- 调用成员函数时,编译器自动将对象地址作为
this
指针传入; - 函数体内可显式使用
this
指针访问成员变量(如this->_year = year
),但不能在形参或实参中显式声明。 - C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
示例:this 指针的隐含传递
年月日,本质是通过 this 指针访问的,this 不可修改:
#include<iostream>
using namespace std;
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->_day = day;}// void Print(Date* const this)void Print(){cout << this->_year << "/" << this->_month << "/" << _day << endl;}private:// 这里只是声明,没有开空间int _year;int _month;int _day;
};int main()
{// Date类实例化出对象d1和d2Date d1;Date d2;// d1.Init(&d1, 2024, 3, 31);d1.Init(2024, 3, 31);// d1.Print(&d1);d1.Print();// d2.Init(&d2, 2024, 7, 5);d2.Init(2024, 7, 5);// d2.Print(&d2);d2.Print();return 0;
}
6.2 经典问题:空指针调用函数
问题 1:以下代码运行结果?
class A {
public:void Print() { cout << "A::Print()" << endl; }
private:int _a;
};int main() {A* p = nullptr;p->Print(); // 正常运行?崩溃?return 0;
}
答案:正常运行。
解答:
Print
函数未访问成员变量(无需解引用this
),即使p
为 nullptr,仍可调用。
class a
{
public:void print(){cout << this << endl;cout << "a::print()" << endl;}
private:int _a;
};int main()
{a* p = nullptr;// mov ecx pp->print(); // call 地址//p->_a = 1;a aa;return 0;
}
this 是指向对象的地址,指向对象的指针,是为了方便访问成员变量的,这里 p 就是对象的地址。
问题 2:以下代码运行结果?
class A {
public:void Print() { cout << _a << endl; } // 访问成员变量
private:int _a;
};int main() {A* p = nullptr;p->Print(); // 正常运行?崩溃?return 0;
}
答案:运行崩溃。
解答:
Print
函数访问_a
(即this->_a
),而this
为 nullptr,解引用空指针导致崩溃。
问题3:this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
答案:A. 栈
解答:
this 是形参,参数在栈帧中。
7. C 与 C++ 实现 Stack 对比
面向对象三大特性:封装、继承、多态
面向对象的封装特性让 C++ 的代码更简洁、安全。对比 C 语言和 C++ 实现的 Stack:
特性 | C 语言实现 | C++ 实现 |
---|---|---|
数据与方法 | 分离(如STInit 与ST 结构体) | 封装在类中(成员变量 + 成员函数) |
访问控制 | 无限制(可直接修改ST 的成员) | 用private 保护成员,仅通过public 函数访问 |
调用方式 | 需显式传递结构体地址(如STPush(&s, 1) ) | 隐含this 指针(如s.Push(1) ) |
代码可读性 | 需记住函数与结构体的关联 | 类内函数直接操作成员,逻辑清晰 |
C 语言实现核心代码
typedef struct Stack {int* a;int top;int capacity;
} ST;void STInit(ST* ps) { // 需显式传参ps->a = NULL;ps->top = 0;ps->capacity = 0;
}void STPush(ST* ps, int x) { // 需显式传参// 入栈逻辑
}
C++ 实现核心代码
class Stack {
public:void Init(int n = 4) { // 隐含this指针// 初始化逻辑}void Push(int x) { // 隐含this指针// 入栈逻辑}
private:int* _a;int _top;int _capacity;
};
C++,相当于来说,更加规范,因为进行了封装:
一个直观的感受,访问栈顶元素:
C语言中:
但是,在C++中:
是不能实现的。
在 C++ 面向对象编程中,类的默认成员函数是实现封装、简化代码的核心机制。当我们未显式定义某些成员函数时,编译器会自动生成它们,以保证类的基本功能可用。