
🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
在C++编程中,类与对象是面向对象思想的核心载体,也是从C语言结构化编程过渡到面向对象编程的关键桥梁。我们日常使用的栈、日期管理等功能,本质上都是通过类封装数据与方法实现的。本文会从类的定义格式、对象实例化、内存布局,到this指针的底层原理与经典问题,一步步拆解核心知识点。无论是刚接触C++的初学者,还是想夯实基础的开发者,都能通过本文理清类与对象的核心逻辑,理解C++是如何通过封装让代码更简洁、规范且易维护的。
class 和 struct可以定义类,类里面可以包含 属性(数据) 和 方法(函数)
类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
类中的成员分为三个“状态”,由访问限定符来处理三种“状态”: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;
};inline与类域inline(内联函数)
当然也可以进行声明和定义的分离(一个在类里面一个在类外面)这就引出了另一个问题:定义在类外面的函数怎么去找
我们使用Date::date()进行函数的调用即可
实际上我们之前也提到过c++有类域的概念 这里插入类域的概念:
类域
int main()
{
Stack st;//用类名实例化对象
//用st.访问
st.Pop();//公有的可以调用
//st.Push();//私有的不能调用
return 0;
}首先明确一下什么叫做对象,物理意义上开空间都叫做对象,用类开空间叫做对象,用int这样的内置类型开空间也叫对象。
像是 Stack st; 这样的操作叫做实例化,有点类似于变量的初始化,不同的是实例化创造出来的是对象
既然实例化以后对象有了内存分配,那 对象的大小是多少 呢? 类里面有成员函数和成员变量,那么对象里是否都包含呢?下面我们一个一个分析:
内存对齐: 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 byteb、c来说,类里面没有成员变量,是不是就没有内存的?
答案是有的,但只有1 byte,他们当中确实没有成员变量所以这1 byte 就是证明他们是存在的,1 byte 纯起占位作用。下面我们还是以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;
}我们在分别对d1和d2使用Init和Print的时候,有没有想过都是同一个函数,甚至我们知道他们连函数调用地址都是相同的,那为什么他们是对正确的对象进行的操作呢?为什么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;
}this指针经典题目#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;
}#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;
}A* p = nullptr;
p->Print();首先定义一个 A类指针 并且置空;
注意:这里与this指针还无关
然后用指针去调用Print()函数,我们在这里假设存在一个d1是p所指向的实例
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*) │
│ 特性:只读(防止篡改指令) | 编译期确定内容 │
└─────────────────────────────────────────────────────────────┘通过本节,我们初步了解到了C++的类和对象,面向对象编程的三大特性:
封装、继承、多态 |
|---|
C++通过将函数和变量放到类中,实现了数据和方法的 封装,另外通过 缺省参数和 隐式的 this指针调用实现了代码的简化,并且通过类来定义结构体也省去了 typedef的繁琐
下面我们来看一下C++和C实现栈的代码:
#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;
}#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;
}