【类 + 类域 + 访问限定符 + 对象的大小 + this指针】目录
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
前言:
(✧ω✧) hi~,小伙伴们今天咱们又双叒来学习C++吧(✪ω✪) ,从今天开始我们就要开始学习类和对象的内容了
让我们一起继续努力吧,冲就完事了~(≧∇≦)ノ🚀
---------------类的定义---------------
什么是类?
类(Class)
:是对具有相同属性(数据)
和方法(行为)
的一组对象的抽象描述
类是 C++ 的核心特性之一,用于实现
面向对象编程(OOP)
中的封装类是对象的蓝图,定义了对象的 属性(数据成员) 和 行为(成员函数)
一个形象的例子:比如 “人类”,可以有姓名、年龄等属性,以及说话、行走等行为,在编程里就可据此定义一个 “人类” 的类。
怎么定义类?
class Person
{
public:
void setAge(int a)
{
age = a;
}
int getAge()
{
return age;
}
private:
};
C++使用
class关键字
定义,类中包含以下三部分的内容。类的基本结构:关键的三个组成部分
- 数据成员(属性):类的变量(如:
int age;
)- 成员函数(方法):类的函数(如:
void setAge(int a) 和 int getAge()
)- 访问修饰符:
public
:任何地方可访问protected
:类及其子类可访问private
:仅类内部访问(默认)
关于类需要注意什么?
下面博主挑选了四个关于“类”需要注意的事项,它们分别是关于:
- 类定义的语法规范
- 成员变量的命名约定
struct
与class
的区别- 类内定义的成员函数与内联
1. 类定义的语法规范:
分号结尾
:类定义结束时必须使用分号;
,这是 C++ 语法的强制要求。class MyClass // 类定义开始 { // 成员... }; // 分号不可省略!
2. 成员变量的命名约定:
命名前缀 / 后缀
:为避免成员变量与局部变量或函数参数重名,通常添加特殊标识:class Person { private: int m_age; // m_前缀(Microsoft风格) std::string _name; // 前缀(常见于开源项目) double salary_; // 后缀(Google风格) };
注意:C++ 标准并未强制要求此规则,具体风格需遵循团队或项目规范。
3.
struct
与class
的区别:
特性 | struct |
class |
---|---|---|
默认访问权限 | public |
private |
设计哲学 | 数据聚合(POD, Plain Old Data) | 封装复杂逻辑(OOP) |
常见用途 | 轻量级数据容器、C 兼容结构 | 封装、继承、多态 |
示例对比:
struct Point { int x, y; // 默认public void move(int dx, int dy); //注意:可定义函数 }; class Rectangle { int width, height; // 默认private public: int area() const { return width * height; } };
4. 类内定义的成员函数与内联:
隐式内联
:在类内部直接定义的成员函数会被编译器视为inline
函数(但最终是否内联由编译器决定)class Math { public: int add(int a, int b) // 隐式内联 { return a + b; } };
显式内联
:若在类外定义函数,需显式使用inline
关键字(通常在头文件中):// Math.h class Math { public: int multiply(int a, int b); // 声明 }; // Math.cpp inline int Math::multiply(int a, int b) // 显式内联 { return a * b; }
注意事项:
- 内联函数的定义必须在每个调用它的翻译单元(.cpp 文件)中可见,因此通常直接放在头文件中。
- 现代编译器会智能优化内联决策,即使未声明
inline
,简单函数也可能被自动内联。
C++中的struct关键字有什么变化?
刚才我们有提到使用关键字struct来定义类,相比C++中的struct和C语言中的struct已经不太一样了吧,那两者又有什么区别呢?
include<iostream>
using namespace std;
/*
* C++ 中对 struct 的升级:
* 1. struct 现在是一个完全的类(class),内部可以定义成员函数
* 2. struct 名称本身可以直接作为类型名使用(不再需要 typedef)
*/
// ------------------------- C 语言风格的链表节点 -------------------------
// C 中需要 typedef 简化类型名称
typedef struct SingleListNodeC
{
int val;
struct ListNodeC* next; // C 中必须带 struct 关键字
}SLTNode; // 通过 typedef 定义类型别名 LTNode
// ------------------------- C++ 风格的链表节点 -------------------------
// C++ 中 struct 本身就是类型名,且支持成员函数
struct SingleListNodeCPP
{
// 成员函数:初始化节点
void Init(int x)
{
val = x; // 设置节点值
next = nullptr; // 初始化指针为空
}
// 成员变量
int val;
SingleListNodeCPP* next; // 直接使用 SingleListNodeCPP 作为类型(无需 struct 关键字)
};
/*
* 对比说明:
* 1. C++ 的 struct 默认所有成员为 public(与 class 的唯一区别)
* 2. 完全支持构造函数、析构函数等类特性
* 3. 兼容 C 的语法,但更推荐 C++ 风格
*/
int main()
{
// C 风格用法(兼容模式)
SLTNode node_c;
node_c.val = 10;
node_c.next = NULL;
// C++ 风格用法
SingleListNodeCPP node_cpp;
node_cpp.Init(20); // 调用成员函数初始化
return 0;
}
对比维度 | C 语言风格 struct | C++ 风格 struct |
---|---|---|
类型定义方式 | 需用typedef 定义类型别名简化使用,定义时节点类型前需加struct 关键字 |
struct 名称本身就是类型名,可直接使用,无需typedef 和额外的struct 关键字 |
成员函数支持 | 仅能定义数据成员,不支持成员函数 | 可定义成员函数,如案例中SingleListNodeCPP 的Init 函数用于初始化节点 |
访问权限默认值 | 无明确访问权限概念(类似 C++ 中public ,所有成员都可直接访问 ) |
默认成员访问权限为public (与class 不同,class 默认private ) |
使用类有哪些好处?
接下来我们就使用C++的类再次重新实现一下我们在《数据结构初阶》中实现的栈这种数据结构,直观的感受一下使用类实现和之前实现的区别:
#include<iostream>
#include<cstdlib>
#include<cassert>
using namespace std;
// 定义一个栈(Stack)类,基于动态数组实现
class Stack
{
public:
/*-------------------------栈的“初始化”操作-------------------------*/
/*
* 初始化栈,默认初始容量为4
* 参数n:可选参数,指定初始容量,默认为4
*/
void Init(int n = 4)
{
//申请n个int大小的内存空间
array = (int*)malloc(n * sizeof(int));
// 检查内存是否申请成功
if (array==nullptr)
{
perror("malloc申请空间失败");
return;
}
// 初始化栈的容量和栈顶指针
capacity = n;
top = 0; //top指向下一个可插入的位置
}
/*-------------------------栈的“入栈”操作-------------------------*/
void Push(int x)
{
// 这里应该添加扩容逻辑,当top == capacity时需要扩容
// 当前实现没有扩容功能,会导致数组越界
//
array[top++] = x; // 元素放入数组,然后top指针后移
}
/*-------------------------栈的“获取栈顶元素”操作-------------------------*/
int Top()
{
assert(top > 0); //断言检查栈是否为空
return array[top - 1];
}
/*-------------------------栈的“销毁”操作-------------------------*/
void Destroy()
{
free(array); // 释放动态数组
array = nullptr; // 指针置空,防止野指针
top = capacity = 0; // 重置容量和栈顶指针
}
private:
/*---------------------成员变量---------------------*/
int* array; // 动态数组指针,存储栈元素
int capacity; // 栈的容量
int top; // 栈顶指针(指向下一个可插入位置)
};
int main()
{
Stack st; // 创建栈对象
st.Init(); // 初始化栈(使用默认容量4)
st.Push(1); // 压入元素1
st.Push(2); // 压入元素2
// 输出栈顶元素(应该是2)
cout << st.Top() << endl;
st.Destroy(); // 销毁栈,释放资源
return 0;
}
这里我们最能明显体会到的区别有以下三点:
维度 | C语言实现 | C++类实现 |
---|---|---|
数据与操作 | 分离(结构体+独立函数) | 整合(成员变量+成员方法) |
实例表示 | 显式传递结构体指针(Stack* ) |
隐含this 指针(st.Push() ) |
访问控制 | 无封装(所有字段可被外部直接修改) | 通过private 保护关键数据 |
---------------类的实例化---------------
什么是类的实例化?
类的实例化
:是指根据类的定义创建具体对象的过程。
- 实例化后,对象会获得类中定义的成员变量(数据)和成员函数(行为),并占用实际的内存空间。
实例化的核心概念:
类(Class)
:是对象的蓝图或模板,仅定义结构,不占用内存。对象(Object)
:是类的具体实例,占用内存,存储实际数据。类比:
- 类 ≈ 建筑设计图
- 对象 ≈ 按设计图建造的房子
在面向对象编程中,一个类可以实例化出多个对象,这些实例化出的对象会占用实际的物理空间,用于存储类成员变量。
类与对象的关系就好比建筑设计图与实际建筑:建筑设计图详细规划了房屋的结构,如房间数量、空间布局、功能分区等,但它只是概念性的图纸,并不具备实体形态,无法供人居住或使用;只有依照设计图施工建造出实际的房屋,才能满足居住、办公等实际需求。
同理,类是对数据和行为的抽象描述,定义了成员变量和成员函数,但它本身并不占据物理内存,也无法存储实际数据,仅作为对象创建的模板。
而通过类实例化得到的对象,就像建造完成的房屋,会在计算机内存中分配实际的空间,用于存储类所定义的成员变量的值。
- 例如:通过 “学生” 类可以实例化出张三、李四等具体学生对象,每个对象都有自己独立的存储空间,用于存放各自的姓名、年龄、成绩等成员变量数据,且不同对象的同一成员变量可以存储不同的值。
- 多个对象之间相互独立,各自存储和管理自己的数据,却又共享类所定义的行为和操作规范,使得程序能够高效地处理复杂的现实场景。
怎么进行类的实例化?
类实例化出对象的方法可谓是五花八门,从宏观的角度的去看,可以将这些实例化的方式分为两大类:
栈上实例化
和堆上实例化
除此之外还要一些其他的前置知识才能很好的理解实例化,这里大家就当是提前了解一下吧。
最简单的类的实例化方式:
类名 对象名
当然就算这是最简单的实例化,但是也是有许多的细节要注意(现阶段就不展开讲了)
#include<iostream>
using namespace std;
// 定义一个日期类 Date,用于表示和操作日期数据
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; // 存储日期
};
int main()
{
/*-------------Date类的实例化过程:创建两个Date对象d1和d2-------------*/
// 此时会为每个对象分配内存(包含_year,_month,_day三个成员变量的空间)
Date d1; // 实例化第一个Date对象d1
Date d2; // 实例化第二个Date对象d2
// 调用d1的Init方法初始化日期为2025年3月31日
d1.Init(2025, 3, 31);
// 调用d1的Print方法输出日期
d1.Print();
// 调用d2的Init方法初始化日期为2025年7月5日
d2.Init(2025, 7, 5);
// 调用d2的Print方法输出日期
d2.Print();
return 0;
}
---------------访问限定符---------------
什么是访问限定符?
访问限定符(Access Specifiers)
:是 C++ 中用于控制类成员(数据成员和成员函数)访问权限的关键字
,决定了类外代码或其他类能否访问这些成员。
- 它们是面向对象编程中封装性的核心实现手段。
访问限定符有哪些?它们的访问权限又是什么?
C++ 主要有以下三种访问限定符:
注:类的访问限定问题,博主初学的时候觉得挺简单,当时也就没太在意。
但随着学习深入,对于哪些能访问、哪些不能访问这类问题,总是傻傻的分不清。
网上虽然也有不少总结,可我觉得都不够清晰,讲的并不是很全面。
下面这张图,是博主踩过无数坑后整理的总结。
可能部分内容对于刚入门的小伙伴们来说,还不是很理解,但请相信它一定能让你对访问限定透彻掌握,做到了如指掌 。(可以先暂时收藏这篇博客🤭)
---------------类域---------------
什么是类域?
类域(Class Scope)
:是类定义内部的作用域
- 所有在类中声明的成员(变量、函数、类型等)都属于这个类的作用域。
类域怎么使用?
类域这个概念看似简单,就是类所定义的范围 ,类中的成员变量和成员函数都处于这个特定范围。
但是想要掌握好类域的使用却并不是一件容易的事情,因为类域总是和其他的知识点捆绑在一起,这里博主就精心挑选了关于类域使用需要注意六个使用场景,希望能对大家有所帮助~
成员直接可见
:类内成员(变量、函数)无需作用域限定即可互相访问。外部访问需限定
:类外访问成员必须通过对象、类名或ClassName::
显式指定。隐藏外部同名标识
:类成员优先级高于全局同名变量/函数,需用::
访问全局。函数定义分内外
:成员函数可类内(隐式内联)或类外(需ClassName::
)定义。嵌套类属外层域
:嵌套类需通过外层类名访问,形成层级作用域。静态成员类域生命周期
:静态成员属于类而非对象,类外单独定义分配内存。
1. 成员直接可见性
类内直接访问:
成员函数
和嵌套类
可以直接访问同类中的其他成员(变量、函数等),无需作用域限定。class Example { int _data; // 成员变量 public: void setData(int val) { _data = val; // 直接访问成员变量 } };
2. 类外访问需作用域限定
- 通过对象或类名访问:
- 非静态成员:通过对象实例(
obj.member
)- 静态成员:通过类名(
ClassName::staticMember
)class Example { int _data; public: void setData(int val) { _data = val; } }; Example obj; obj.setData(10); // 通过对象访问非静态成员 -------------------------------------------------------------------------------- class Math { public: static const double PI; }; double pi = Math::PI; // 通过类名访问静态成员
3. 隐藏外部同名标识符
- 优先级规则:类成员会隐藏同名的全局变量或函数。
/*------------------------类域:隐藏外部的标识符------------------------*/ #include <iostream> using namespace std; int value = 100; // 全局变量 class Test { int value = 10; // 类成员 public: void print() { cout <<"类成员value的值为" << value << endl; // 输出 10(类成员优先) cout << "类成员value的值为" << Test::value << endl; //直接访问同类中的其他成员,无需作用域限定 cout <<"全局变量value的值为" << ::value << endl; // 输出 100(全局变量需用 :: 访问) } }; int main() { Test test; test.print(); return 0; }
4. 支持成员函数的类内/类外定义
类内定义:隐式内联(适合简单函数)
类外定义:需用
ClassName::
指明类域class Date { int _year; public: void init(int year); // 声明 }; void Date::init(int year) // 类外定义 { _year = year; }
5. 嵌套类属于外层类域
- 嵌套类需通过外层类名访问。
class Outer { public: class Inner // 嵌套类 { void show(); }; }; void Outer::Inner::show() // 嵌套类成员定义 { cout << "Inner"; }
6. 静态成员拥有类域生命周期
静态成员属于类而非对象,需在类外单独定义(分配内存)
class Counter { public: static int count; // 声明 }; int Counter::count = 0; // 类外定义
类域的设计意义(简单了解即可)
- 封装性:将数据和操作绑定,隐藏实现细节。
- 避免命名冲突:类成员与全局或其他类成员可同名。
- 支持面向对象特性:如继承、多态、静态成员等。
最后我们再使用一个实际的案例看看类域带来的好处,案例还是重写数据结构栈(不过这次是再引入
“类”
的基础上再引入“类域”
的概念后的实现)
#include<iostream>
using namespace std;
// 定义一个栈(Stack)类,基于动态数组实现
class Stack
{
public:
/*-------------类内声明:栈的“初始化”成员函数-------------*/
void Init(int n = 4);
private:
// 成员变量(私有,外部无法直接访问)
int* array; // 动态数组指针,用于存储栈元素
int capacity; // 栈的总容量
int top; // 栈顶指针(指向下一个可插入位置)
};
/*-------------类外实现:栈的“初始化”成员函数-------------*/
void Stack::Init(int n) //注意:需要使用作用域解析运算符(::)指明属于Stack类
{
//申请n个int大小的内存空间
array = (int*)malloc(n * sizeof(int));
// 检查内存是否申请成功
if (array == nullptr)
{
perror("malloc申请空间失败");
return;
}
// 初始化栈的容量和栈顶指针
capacity = n;
top = 0; //top指向下一个可插入的位置
}
int main()
{
Stack st; // 创建Stack类的对象st
st.Init(); // 调用Init方法初始化栈(使用默认容量4)
return 0;
}
很明显可以感知到:在引入类域后,函数的声明与定义实现了清晰分离。
通过将
Init
函数的声明放在类内(作为接口的一部分),而将具体实现放在类外并使用Stack::Init
明确指定类域,既保持了类定义的简洁性,又将实现细节隐藏在类的外部。
---------------对象的大小---------------
想要判断出对象的大小,我们先要搞懂一个问题:类实例化后的对象包含哪些成员?
类实例化后的对象包含哪些成员?
在 C++ 中,类实例化后的对象成员可分为数据成员和函数成员,哪对象包含哪些成员?
这需要从内存布局
和函数调用机制
两方面来分析。
1. 对象的数据成员
类实例化的每个对象都拥有独立的数据空间,用于存储
成员变量
这些变量记录了对象的状态信息,不同对象的成员变量相互独立。
- 例如:一个
Date
类的对象会存储年、月、日三个成员变量,每个对象的这些值可以不同(如:2023-01-01 和 2024-05-09)
2. 成员函数的存储位置
成员函数的代码被编译后存储在代码段,这是一块独立于对象的数据区域。
所有对象共享同一套成员函数代码,无需为每个对象复制函数体。
3. 特殊情况:虚函数与动态绑定
(暂时了解即可)当类中包含虚函数时,对象会额外存储一个虚函数表指针(vptr)
虚函数表(vtable):每个包含虚函数的类都有一个虚函数表(指针),存储该类的虚函数地址。
动态绑定机制:通过虚函数指针在运行时查找函数地址,实现多态。
Shape* s = new Circle(); s->Draw(); // 运行时通过vptr查找Circle::Draw()的地址
注意:虚函数表本身是类级别的,不随对象数量增加而复制,仅每个对象需存储一个 vptr(通常 8 字节)
总结:对象的内存布局:
成员类型 | 存储位置 | 是否每个对象独有 |
---|---|---|
普通成员变量 | 对象的数据区 | 是 |
静态成员变量 | 全局数据区 | 否(类共享) |
成员函数(非虚) | 代码段 | 否(类共享) |
虚函数 | 代码段(通过虚函数表调用) | 否(类共享) |
虚函数表指针(vptr) | 对象的数据区(前 8 字节) | 是 |
#include <iostream>
using namespace std;
class A
{
int x; // 4字节
};
class B
{
int x; // 4字节
void func() {} // 不占对象空间
};
class C
{
int x; // 4字节
virtual void f() {} // 8字节(vptr)
};
int main()
{
cout << sizeof(A) << endl; // 输出: 4
cout << sizeof(B) << endl; // 输出: 4
cout << sizeof(C) << endl; // 输出: 16(4+4+8,考虑内存对齐)
return 0;
}
下面的内容是对类C的大小为什么是16的解释,会涉及内存对齐的知识点,不是很了解内存对齐的话,请先看下面关于内存对齐的讲解。
- 对齐基数:类的对齐值由其最大成员的对齐值决定。
int x
对齐值:4 字节vptr
对齐值:8 字节- 最终对齐值:8 字节(取两者最大值)
- 总大小计算:
int x
:4 字节- 填充(padding):4 字节(使
vptr
从8的倍数地址开始)vptr
:8 字节- 总计:4 + 4(padding) + 8 = 16 字节
类 C 的内存布局(VS 64位编译):
+------------------+
| int x (4字节) | // 偏移 0
+------------------+
| padding (4字节) | // 偏移 4(填充到8的倍数)
+------------------+
| vptr (8字节) | // 偏移 8
+------------------+
为什么对象不存储成员函数?
函数代码的共享性
所有对象调用的成员函数逻辑完全相同,重复存储会造成内存浪费。
- 例如:
Date
类的Print()
函数无论被哪个对象调用,执行的代码都是相同的。
编译期确定调用地址
非虚函数的调用在编译时就已确定地址,通过call 函数地址
指令直接调用,这种机制使得对象无需存储函数指针。d1.Print(); // 编译后转换为 call Date::Print 指令
内存效率优化
如果每个对象都存储成员函数指针,将导致大量冗余数据。
- 例如:实例化 100 个对象若每个存储 3 个函数指针,将额外消耗约 2.4KB 内存(64 位系统下)
什么是内存对齐?
内存对齐(Memory Alignment)
:是计算机内存管理中的一种优化策略,要求数据在内存中的存储地址必须是某个特定值(对齐值)的整数倍,主要目的是提高 CPU 访问内存的效率。
内存对齐的规则
(1)基本数据类型的对齐值
通常为数据类型的大小(如
char
为 1 字节,int
为 4 字节,double
为 8 字节)char:对齐值1字节 short:对齐值2字节 int:对齐值4字节 double:对齐值8字节(64位系统)
(2)结构体的对齐规则
成员对齐
:每个成员的起始地址必须是其对齐值的整数倍。
结构体总大小
:必须是最大成员对齐值的整数倍。
填充字节
:编译器自动插入填充字节(Padding)以满足对齐要求。struct Example { char a; // 1字节(地址0) // 编译器插入3字节padding(地址1-3),使int对齐到4 int b; // 4字节(地址4-7) double c; // 8字节(地址8-15) short d; // 2字节(地址16-17) // 编译器插入6字节padding(地址18-23),使结构体大小对齐到8(最大成员double的对齐值) }; // sizeof(Example) == 24(而非1+4+8+2=15)
内存布局:
Offset 0: [a][ ][ ][ ] // char a + 3字节padding
Offset 4: [b][b][b][b] // int b
Offset 8: [c][c][c][c] // double c
[c][c][c][c]
Offset 16:[d][d][ ][ ] // short d + 6字节padding(对齐到8)
好了,那我们就看一个案例分析一下下面的对象的大小吧
#include<iostream>
using namespace std;
/*
* 内存对齐规则说明:
* 1. 结构体/类的大小必须是其最大成员对齐值的整数倍
* 2. 每个成员的偏移地址必须是其自身大小的整数倍
* 3. 空类的大小为1字节(用于占位标识)
*/
class A
{
public:
void Print() // 成员函数不占用对象内存空间(存储在代码区)
{
cout << _ch << endl;
}
private:
char _ch; // 1字节(对齐值1)
// 编译器插入3字节padding(因为int需要4字节对齐)
int _i; // 4字节(对齐值4)
// 类A的总大小:1(_ch) + 3(padding) + 4(_i) = 8字节
};
class B
{
public:
void Print() // 成员函数不占用对象内存空间
{
}
// 此类没有任何成员变量
// 空类会被编译器插入1字节占位符
// 类B的总大小:1字节(空类最小大小)
};
class C
{
// 完全空类
// 同样会被编译器插入1字节占位符
// 类C的总大小:1字节
};
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(空类)
return 0;
}
为什么需要内存对齐?
1. 性能优化
- 未对齐访问:现代 CPU 通常按块(如:4 字节、8 字节)读取内存,若数据未对齐,可能需要多次访问。
- 例如:一个 int 型数据(4 字节)存储在地址 0x0002 开始的位置,CPU 需分两次读取才能获取完整数据。
- 对齐访问:数据起始地址是对齐值的整数倍时,CPU 可一次性读取完整数据。
- 例如:int 型数据存储在地址 0x0000 或 0x0004,CPU 只需一次读取操作。
2. 平台兼容性
- 某些硬件平台(如:ARM)强制要求数据对齐,否则会触发异常。
---------------this指针---------------
为什么要引入this指针?
此时,小伙伴我们先尝试想一下这个问题,之前我们说:“所有对象共享同一套成员函数代码,无需为每个对象复制函数体。”那么我们很容易顺理成章的想到下面这个问题:
在 C++ 中,类的成员函数是如何区分不同对象的呢?
回答:这就要归功于 C++ 的隐含机制 ——
this指针
1. 隐含的 this 指针
编译器在编译类的成员函数时,会隐式地在每个非静态成员函数的参数列表中添加一个指向当前对象的指针,称为
this
指针。this 指针类型:
类名* const
(不可修改指向的对象)例如:
Date
类的Init
函数在编译后的原型实际上是:void Init(Date* const this, int year, int month, int day);
2. 通过 this 指针访问成员变量
- 在成员函数内部,所有对成员变量的访问都是通过
this
指针完成的。void Init(int year, int month, int day) { _year = year; // 等价于 this->_year = year; // 显式使用this指针 this->_month = month; this->_day = day; }
3. 调用机制
- 当对象调用成员函数时,编译器会自动将对象的地址作为
this
指针传递给函数。Date d1, d2; d1.Init(2024, 5, 9); // 等价于 Init(&d1, 2024, 5, 9); d2.Init(2025, 5, 9); // 等价于 Init(&d2, 2025, 5, 9);
总结:
this
指针是 C++ 实现对象区分的核心机制,它通过以下方式工作:
- 编译器隐式为每个非静态成员函数添加
this
参数。- 调用成员函数时,对象的地址自动作为
this
实参传递。- 成员函数内部通过
this
指针访问对象的成员变量和其他成员函数。这种机制使得
相同的成员函数
代码可以操作不同对象的数据
,实现了代码复用与数据隔离。
#include<iostream>
using namespace std;
class Date
{
public:
/*---------------------------------初始化日期的成员函数---------------------------------*/
// 编译器实际处理:void Init(Date* const this, int year, int month, int day)
// this指针是编译器隐式添加的常量指针,指向调用该函数的对象
void Init(int year, int month, int day)
{
// 尝试修改this指针本身会报错(this是右值)
// this = nullptr; // 错误:this是Date* const类型,指针自身不可修改
// 三种等效的成员访问方式:
_year = year; // 方式1:隐式通过this访问(编译器自动添加this->)
this->_month = month; // 方式2:显式使用this指针
(*this)._day = day; // 方式3:解引用后访问(不推荐,仅作演示)
/* 编译器实际生成的代码:
this->_year = year;
this->_month = month;
this->_day = day;
*/
}
/*---------------------------------打印日期的成员函数---------------------------------*/
// 编译器实际处理:void Print(Date* const this)
void Print()
{
// 访问成员同样隐含this指针
cout << this->_year << "/" << _month << "/" << _day << endl;
}
private:
/*---------------------------------成员变量声明(实例化对象时才分配内存)---------------------------------*/
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
/*---------------------------------实例化两个Date对象(各自拥有独立的成员变量内存空间)---------------------------------*/
Date d1; // 调用默认构造函数
Date d2; // sizeof(d1) == 12(假设int为4字节)
// 调用成员函数时,编译器自动传入对象地址作为this参数
// d1.Init(&d1, 2024, 3, 31); // 实际底层调用方式
d1.Init(2025, 3, 31); // 语法糖:编译器自动转换为上述形式
d1.Print(); // 输出:2025/3/31
// d2对象独立拥有自己的数据成员
d2.Init(2025, 7, 5); // this指针指向d2的地址
d2.Print(); // 输出:2025/7/5
return 0;
}
什么是this指针?
this指针
:是一个隐式存在于每个非静态成员函数中的特殊指针,它指向调用该函数的对象实例。
- this指针是一个隐含的、由编译器自动生成的常量指针(
ClassName* const this
),指向当前对象的地址。- this指针是成员函数访问对象成员的桥梁,也是面向对象编程中实现对象自治的核心机制,理解
this
指针对于掌握类和对象的底层机制至关重要。
this
指针的本质:
隐式参数:每个非静态成员函数(包括构造函数/析构函数)的首参数默认是
this
指针,指向调用该函数的对象,但代码中无需显式声明obj.func(arg); // 源码 ClassName::func(&obj, arg); // 编译器转换后的实际调用
类型:this指针的类型取决于对象的类型,例如:
- 对于
class A
的对象,this
的类型是A* const
(不可修改指向的对象)- 对于
const
成员函数,this
的类型是const A* const
(既不可修改指向的对象,也不可通过this
修改对象内容)
this指针的特性有哪些?
不可修改性
:this
指针本身是常量指针,不能被赋值。this = nullptr; // 错误:不能修改this指针
不可显式传递
:不能在函数调用时显式传递this
指针。d1.Init(&d1, 2024, 5, 9); // 错误:编译器自动处理this指针
可在函数体内显式使用
:例如返回当前对象的引用。Date& SetYear(int year) { this->_year = year; return *this; // 返回当前对象的引用 }
空指针风险
:若通过空指针调用成员函数,this
将为nullptr
,可能导致程序崩溃。Person* p = nullptr; p->getName(); // 未定义行为,this为nullptr
静态成员函数中无this指针
:静态成员函数属于类,不关联特定对象。class A { public: static void func() { cout << this; // 错误:静态函数中没有this指针 } };
特性 | 说明 |
---|---|
类型 | ClassName* const (常量指针,自身地址不可修改) |
作用域 | 仅在成员函数内部有效 |
存储位置 | 通常通过寄存器传递,而非内存 |
不可显式赋值 | this = nullptr; 是语法错误 |
this指针有什么用?
1. 区分成员变量和参数
- 当函数参数与成员变量同名时,使用
this
指针显式访问成员变量。class Person { private: string name; int age; public: void setName(string name) { this->name = name; // this->name 指向成员变量,name 是参数 } };
2. 返回当前对象的引用
- 实现链式调用(如:
obj.func1().func2()
)时,this
指针可用于返回对象自身。class Calculator { private: int value; public: Calculator& add(int num) { value += num; return *this; // 返回当前对象的引用 } }; // 使用示例 Calculator calc; calc.add(5).add(3); // 链式调用
3. 在析构函数或拷贝构造函数中引用对象
- 确保操作的是当前对象实例。
class Resource { public: ~Resource() { delete[] this->data; // 使用this指针释放当前对象的资源 } };
this指针小练习,你敢试试吗?
哈哈😄,你一定可以得满分,
(因为答案都在注释里啦)
第二题:下⾯程序编译运行结果是()
A.编译报错
B.运行崩溃
C.正常运行
#include<iostream>
using namespace std;
class A
{
public:
/*-----------------------成员函数(属于类,而非对象)-----------------------*/
// 编译后实际形式:void Print(A* const this)
void Print()
{
// 此函数未访问任何成员变量(不依赖this指针指向的内存)
cout << "A::Print()" << endl;
/* 如果尝试访问成员变量会崩溃:
cout << _a << endl; // 等价于 cout << this->_a << endl;
// this为nullptr时解引用导致段错误
*/
}
private:
int _a; // 成员变量(未被Print函数使用)
};
int main()
{
A* p = nullptr; // 声明一个指向A类的空指针
// 通过空指针调用成员函数(安全,但危险!)
p->Print(); // 输出:A::Print()
/* 为什么不会崩溃?
1. 成员函数编译后实质是普通函数,首参数为this指针
2. 调用时相当于执行:A::Print(p); (传入nullptr作为this)
3. Print函数内部未访问this指向的内存(未使用_a)
4. 函数代码在编译期已确定地址,call指令直接跳转执行
*/
return 0;
}
答案【C】
第一题:下⾯程序编译运行结果是()
A.编译报错
B.运行崩溃
C.正常运行
#include<iostream>
using namespace std;
class A
{
public:
/*-----------------------成员函数(属于类,而非对象)-----------------------*/
// 成员函数(编译后实际形式:void Print(A* const this))
void Print()
{
cout << "A::Print()" << endl; // 这行能执行,因为不依赖this指针
// 危险操作:尝试访问成员变量_a(等价于this->_a)
// 此时this是nullptr,解引用会导致段错误(Segmentation Fault)
cout << _a << endl; // 崩溃点!
/* 底层行为分析:
1. 编译器将_a转换为this->_a
2. 实际执行:*(this + offset_of_a) (offset_of_a是_a的偏移量)
3. this为nullptr时,计算 nullptr + offset 仍为无效地址
4. 访问无效内存地址触发硬件异常
*/
}
private:
int _a; // 成员变量(默认未初始化)
};
int main()
{
A* p = nullptr; // 声明一个指向A类的空指针
// 通过空指针调用成员函数(危险行为!)
p->Print(); // 虽然能进入函数,但访问_a时崩溃
/* 为什么能进入函数但会崩溃?
1. 成员函数调用本质是:A::Print(p);
2. 调用时仅传递了nullptr作为this参数
3. 函数代码本身在代码段,call指令可以正常跳转
4. 但当函数内解引用this(如访问_a)时,CPU会触发内存访问异常
*/
return 0; // 实际上执行不到这里
}
答案【B】