C++类和对象深度解析:定义、内存布局和this指针

2025博客之星年度评选已开启 10w+人浏览 1.4k人参与

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到sayfall的文章

在这里插入图片描述

🌈 say-fall:个人主页
🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》
💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。

前言:

在C++编程中,类与对象是面向对象思想的核心载体,也是从C语言结构化编程过渡到面向对象编程的关键桥梁。我们日常使用的栈、日期管理等功能,本质上都是通过类封装数据与方法实现的。本文会从类的定义格式、对象实例化、内存布局,到this指针的底层原理与经典问题,一步步拆解核心知识点。无论是刚接触C++的初学者,还是想夯实基础的开发者,都能通过本文理清类与对象的核心逻辑,理解C++是如何通过封装让代码更简洁、规范且易维护的。



正文:

1. 类的定义

1.1 类定义格式_class关键字

classstruct可以定义类,类里面可以包含 属性(数据)方法(函数)
类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。

类中的成员分为三个“状态”,由访问限定符来处理三种“状态”:public(公有)、protected(保护)、private(私有)

struct可以看出来“类”可以看作是C语言中结构体的升级版,实际确实如此:
所以c++也兼容了struct用来定义类,一般class定义默认私有,struct定义默认公有,我们一般使用class

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;


class Stack//Stack为类名 ==》类名就是这个类的类型
{
	void Push()
	{
	}
/*
函数内部可以添加  public:  来改变类内部成员的“状态”
一种“状态”会持续到下一个访问限定符 或者 } 就会结束
*/
public://表示公有
	void Pop()
	{
	}
private://表示私有,除此之外还有访问限定符protected表示保护

//一般情况下把数据放在类下方,函数放在类上方
	int* a;
	int top;
	int capacity;
};


class Date
{
	int dateInit(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	int Print()
	{
	cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
	}
	
	//==》有的时候成员变量会和参数名一样
	//为了防止混淆与方便使用,在变量前面加 _ 或者 m
	int _year;
	int _month;
	int _day;
};

1.2 默认inline与类域

  • 定义在类里面的函数默认添加inline(内联函数)
    当然也可以进行声明和定义的分离(一个在类里面一个在类外面)

这就引出了另一个问题:定义在类外面的函数怎么去找
我们使用Date::date()进行函数的调用即可

实际上我们之前也提到过c++有类域的概念
这里插入类域的概念:

类域

  • 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作用域操作符指明成员属于哪个类域。
  • 类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
int main()
{
	Stack st;//用类名实例化对象
	//用st.访问
	st.Pop();//公有的可以调用
	//st.Push();//私有的不能调用
	
	return 0;
}

2. 实例化

2.1 实例化对象

像是 Stack st; 这样的操作叫做实例化,有点类似于变量的初始化,不同的是实例化创造出来的是对象

  • ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象
    在类被实例化出对象之前,他里面的数据是没有空间分配的,实例化以后有了空间分配
    用一个类可以实例化出很多对象,类似于类是一张蓝图,可以创造出很多基于蓝图的实物

2.2 对象的大小

既然实例化以后对象有了内存分配,那 对象的大小是多少 呢?
类里面有成员函数和成员变量,那么对象里是否都包含呢?下面我们一个一个分析:

  1. 成员变量:
    首先成员变量是肯定存在的,因为不同对象的差别就在于他们当中的数据是不一样的
    就像是区分相同小区不同楼宇,主要就是看他们的位置不同,所以成员变量一定被对象包含
  2. 成员函数:
    思考一下成员函数要存储的话存储的是什么?首先不可能是代码块,因为代码段作为一段指令其一定不能被包含
    那就只可能是成员函数的指针了,也就是说使用函数的时候就跳转到这个函数的地址处调用
    那对于一个函数来说,所有的对象调用的都是相同的函数,这就不需要每个对象都存储函数指针
    实际上函数指针是包含在类中的公共代码区,这个我们以后再详解
    也就是说,对象中就只包含了成员变量,这点 和结构体是完全相同
    所以说,对象的内存大小规则(存在内存对齐)也是和结构体完全相同的:

内存对齐:
1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的 较⼩值 。
VS 中默认的值为 8
Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

  • 例:
    在这里插入图片描述
    下面我们来看一下三段代码:
#include<iostream>
using namespace std;
// 计算⼀下A/B/C实例化的对象是多⼤? 
class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};

class B
{
public:
	void Print()
	{
		//...
	}
};

class C
{};

int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}
  • 对于a这个对象来说,它的大小根据内存对齐来说就是8 byte
  • 而对于bc来说,类里面没有成员变量,是不是就没有内存的?
    答案是有的,但只有1 byte,他们当中确实没有成员变量所以这1 byte 就是证明他们是存在的,1 byte 纯起占位作用。
  • 空类:像是C这种没有显式定义任何成员(成员变量、成员函数、嵌套类型等)的类被称为空类

3. this关键字

3.1 this指针

下面我们还是以Date这个类来讲解this指针,this是一个指针,也是一个关键字。

#include <iostream>
using namespace std;

class Date
{
public:
	int Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	int Print()
	{
	cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
	}
	
	// 有的时候成员变量会和参数名一样
	// 为了防止混淆与方便使用,在变量前面加 _ 或者 m
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
 // Date类实例化出对象d1和d2 
 Date d1;
 Date d2;
 
 d1.Init(2024, 3, 31);
 d1.Print();
 d2.Init(2024, 7, 5);
 d2.Print();
 return 0;
}

我们在分别对d1d2使用InitPrint的时候,有没有想过都是同一个函数,甚至我们知道他们连函数调用地址都是相同的,那为什么他们是对正确的对象进行的操作呢?为什么d1.Print();打印的不是d2的数据呢?

这里实际上隐含了一个指针,编译器编译以后成员函数形参的第一个位置上默认会增加一个当前类类型的指针,实际上就是指向对象自身的指针,叫this指针
(如果了解过python其实了解到this其实和self类似)

Init的真实原型:int Init(Date* const this,int year, int month, int day)

类的成员函数访问成员变量,实际上都是通过this指针访问的,如Init()函数里面赋值,本质上可以理解为:this->_year = year;

但是值得注意的是,C++中的this是隐式的,不能在形参和实参的位置写this指针,因为编译器会处理,但是可以在函数体内使用this指针:

void Init(int year, int month, int day)
 {
 _year = year;
 this->_month = month;//这里使用this指针是被允许的
 this->_day = day;
 }

3.2 this指针经典题目

  1. 这个程序执行后会怎样?
#include<iostream>
using namespace std;
class A
{
public:
 void Print()
 {
 cout << "A::Print()" << endl;
 }
private:
 int _a;
};

int main()
{
 A* p = nullptr;
 p->Print();
 return 0;
}
  1. 这个程序执行后会怎样?
#include<iostream>
using namespace std;
class A
{
public:
 void Print()
 {
 cout << "A::Print()" << endl;
 cout << _a << endl;
 }
private:
 int _a;
};

int main()
{
 A* p = nullptr;
 p->Print();
 return 0;
}
  1. this指针存在内存哪个区域的?

  1. 第一个程序会正常运行:
  • 我们来看主函数里面,主要是这两句代码:
A* p = nullptr;
p->Print();

首先定义一个 A类指针 并且置空;
注意:这里与this指针还无关

然后用指针去调用Print()函数,我们在这里假设存在一个d1p所指向的实例

A  d1;
A* p = &d1;
p = nullptr;
d1.Print();

伪代码:A::Print(p);
注意看,这里作用是完全等价的,都在调用Print()函数,那p的作用其实就是作为一个指针参数传递给了Print()函数,也就是说:
调用了一个类域内的Print()函数,他的this指针变量被赋值为了nullptr;
然后我们看print()函数内部:

void Print()
 {
 cout << "A::Print()" << endl;
 }

打印一个 “A::Print()” ,显然是没有任何问题的,所以程序会正常执行。

第二个程序会运行崩溃:

主函数内容都和一题中内容一样,不在解释,我们直接看函数定义:

void Print()
 {
 cout << "A::Print()" << endl;
 cout << _a << endl;
 }

注意看:cout << _a << endl;里面是有成员变量_a调用的,而我们刚才说_a
的本质是this->_a,但是this是空指针,这就构成了空指针的解引用,会导致程序崩溃。

this指针是存在堆里面的,它属于形参,不过有些编译器会优化,把它放在寄存器里
参考下面内存分区:

┌─────────────────────────────────────────────────────────────┐
│  内核空间(操作系统占用,用户程序不可访问)                     │
├─────────────────────────────────────────────────────────────┤
│  栈区(Stack)                                               │
│  存储内容:局部变量、函数形参、函数返回地址、this指针副本        │
│  特性:自动分配/释放(函数调用时分配,返回时释放)              │
│        地址从高到低增长 | 空间大小有限(通常几 MB)            │
├─────────────────────────────────────────────────────────────┤
│  内存映射段(Memory Mapping Segment)                        │
│  存储内容:动态库(.so/.dll)、共享内存、文件映射               │
│  特性:高效 IPC(进程间通信) | 按需加载/卸载                  │
├─────────────────────────────────────────────────────────────┤
│  堆区(Heap)                                                │
│  存储内容:动态分配的对象/数据(new/malloc 分配的内容)         │
│  特性:手动分配/释放(需 delete/free,否则内存泄漏)            │
│        地址从低到高增长 | 空间大小灵活(通常几 GB)             │
├─────────────────────────────────────────────────────────────┤
│  全局/静态存储区(Data Segment + BSS Segment)               │
│  细分 1:数据段(已初始化)                                   │
│          存储:已初始化的全局变量、已初始化的静态变量(static) │
│  细分 2:BSS 段(未初始化)                                   │
│          存储:未初始化的全局变量、未初始化的静态变量           │
│  特性:程序启动时分配,退出时释放 | 默认初始化为 0              │
├─────────────────────────────────────────────────────────────┤
│  代码段(Code Segment / Text Segment)                       │
│  存储内容:可执行指令(函数体、语句)、常量字符串(const char*) │
│  特性:只读(防止篡改指令) | 编译期确定内容                    │
└─────────────────────────────────────────────────────────────┘

4. C++与C对比

通过本节,我们初步了解到了C++的类和对象,面向对象编程的三大特性:

封装、继承、多态

C++通过将函数和变量放到类中,实现了数据和方法的 封装,另外通过 缺省参数隐式this指针调用实现了代码的简化,并且通过类来定义结构体也省去了 typedef的繁琐

下面我们来看一下C++和C实现栈的代码:

C实现Stack代码

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
 STDataType* a;
 int top;
 int capacity;
}ST;
void STInit(ST* ps)
{
 assert(ps);
 ps->a = NULL;
 ps->top = 0;
 ps->capacity = 0;
}
void STDestroy(ST* ps)
{
 assert(ps);
 free(ps->a);
 ps->a = NULL;
 ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
 assert(ps);
// 满了, 扩容 
 if (ps->top == ps->capacity)
 {
 int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
 STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * 
sizeof(STDataType));
 if (tmp == NULL)
 {
 perror("realloc fail");
 return;
 }
 ps->a = tmp;
 ps->capacity = newcapacity;
 }
 ps->a[ps->top] = x;
 ps->top++;
}
bool STEmpty(ST* ps)
{
 assert(ps);
 return ps->top == 0;
}
void STPop(ST* ps)
{
 assert(ps);
 assert(!STEmpty(ps));
 ps->top--;
}
STDataType STTop(ST* ps)
{
 assert(ps);
 assert(!STEmpty(ps));
 return ps->a[ps->top - 1];
}
int STSize(ST* ps)
{
 assert(ps);
 return ps->top;
}
int main()
{
 ST s;
 STInit(&s);
 STPush(&s, 1);
 STPush(&s, 2);
 STPush(&s, 3);
 STPush(&s, 4);
 while (!STEmpty(&s))
 {
 printf("%d\n", STTop(&s));
 STPop(&s);
 }
 STDestroy(&s);
 return 0;
}

C++实现Stack代码

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
 // 成员函数 
 void Init(int n = 4)
 {
 _a = (STDataType*)malloc(sizeof(STDataType) * n);
 if (nullptr == _a)
 {
 perror("malloc申请空间失败");
 return;
 }
 _capacity = n;
 _top = 0;
 }
 void Push(STDataType x)
 {
 if (_top == _capacity)
 {
 int newcapacity = _capacity * 2;
 STDataType* tmp = (STDataType*)realloc(_a, newcapacity * 
sizeof(STDataType));
 if (tmp == NULL)
 {
 perror("realloc fail");
 return;
 }
 _a = tmp;
 _capacity = newcapacity;
 }
 _a[_top++] = x;
 }
 void Pop()
 {
 assert(_top > 0);
 --_top;
 }
 bool Empty()
 {
 return _top == 0;
 }
 int Top()
 {
 assert(_top > 0);
 return _a[_top - 1];
 }
 void Destroy()
 {
 free(_a);
 _a = nullptr;
 _top = _capacity = 0;
 }
private:
// 成员变量 
 STDataType* _a;
 size_t _capacity;
 size_t _top;
};
int main()
{
 Stack s;
 s.Init();
 s.Push(1);
 s.Push(2);
 s.Push(3);
 s.Push(4);
 while (!s.Empty())
 {
 printf("%d\n", s.Top());
 s.Pop();
 }
 s.Destroy();
 return 0;
}
  • 可以看到C++的代码是简洁和规范很多的

  • 本节完…
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值