类(Class)是一种自定义的数据类型。它像一个 “蓝图” 或 “模板”,用来描述具有相同属性(数据)和行为(函数)的一组对象的共同特征。
你可以把它想象成建造房子的设计图纸:
在 C++ 中,对象是类的具体化,类是对象的抽象。
在 C 语言等面向过程的语言中,数据和操作数据的函数是分离的。当项目变得复杂时,这种方式会导致代码结构混乱,难以维护。C++语言是面向对象的,面向对象有三大特征:封装、继承、多态,C++中的类就起到了封装的作用,数据和方法放到了一起,都放在了类里面,能够更好的管理和使用
// 定义一个类
class Stack
{
public:
// 成员函数
void Init(int n)
{
_arr = (int*)malloc(sizeof(int) * n);
_capacity = n;
_top = n;
}
void Push(int x)
{
// ...
}
// ...
private:
// 成员变量
// 为了区分成员变量,⼀般习惯上成员变量,会加⼀个特殊标识,如_ 或者 m开头
// int* m_arr;
int* _arr;
int _top;
int _capacity;
}; // 分号不能省略
//代码规范
//驼峰法 StackInit 类型,函数 首字母大写开头+单词大写区分
// initCapacity 变量 单词小写开头+单词首字母大写
// stack_init
// init_capacity
C++将struct升级为了类,在C++中struct中也可以定义函数,类名就表示类型;同时C++中也兼容C中struct的用法,因为C++就是在C的基础上优化的,所以它一般都可以兼容C中的许多东西
// 兼容C中结构体的用法
typedef struct A
{
// 没访问限定符,默认是public
int _a1;
}AA;
// 升级成了类,内部可以定义函数
struct B
{
int _b1;
void Init()
{}
};
//一般都用类,也有用struct的情况,比如链表
// 不再需要typedef,ListNode就可以代表类型
struct ListNode
{
int _val;
// struct ListNode* next;
ListNode* next; // 类名就是类型,不用加struct
};

class A
{
// 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止
// 如果后⾯没有访问限定符,作用域就到 } 即类结束
// 一般情况就是在同一个访问限定符的放在一起
public:
void func1()
{}
private:
int _a1;
public:
int _a2;
};
在C++中有局部域、全局域、类域、命名空间域。其中,局部域和全局域会影响生命周期,因为变量在函数栈帧销毁后也会销毁,相同的域不能定义同名变量,同名函数(函数重载除外),不同的域可以。
类中成员函数声明和定义分离时,定义函数时要指定类域
// Stack.h
#include<iostream>
using namespace std;
class Stack
{
public:
void Init(int n = 4);
private:
int* _array;
int _top;
int _capacity;
};
// Stack.cpp
// 类中成员函数声明和定义分离时,定义函数时要指定类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
#include<iostream>
using namespace std;
class Stack
{
public:
void Init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
_top = n;
_capacity = n;
}
private:
// 这里只是声明并没有开空间
int* _a;
int _top;
int _capacity;
};
int main()
{
// Stack实例化出对象st1和st2
// 实例化对象时,才会开辟空间
Stack st1;
Stack st2;
st1.Init();
st2.Init();
return 0;
}
类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,但是不包含成员函数。首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么如果对象中非要存储的话,只能是成员函数的指针。但是,如果类实例化多个对象,那么成员函数指针就重复存储多次,太浪费了,所以函数的指针也是不需要存储的。成员函数在被调用时会在编译(有函数的定义)或链接(函数声明和定义分离)的过程中找到函数的地址。总之,成员函数和成员函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。


综上,对象中值存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。
内对齐规则:
// 计算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()
{
cout << sizeof(A) << endl;//8
// 开1byte是为了占位,不存储实际数据,表示对象存在过
cout << sizeof(B) << endl;//1
cout << sizeof(C) << endl;//1
B b1;
B b2;
cout << &b1 << endl;
cout << &b2 << endl;
C c1;
C c2;
cout << &c1 << endl;
cout << &c2 << endl;
return 0;
}
没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个 字节呢?因为如果⼀个字节都不给,无法表示对象存在过,所以这里给1字节,纯粹是为了占位标识对象存在。
Data类中有 Init 和 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 和 Print 函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了 一个隐含的this指针解决这里的问题
编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。比如Date类的Init的真实原型为, void Init(Date* const this, int year, int month, int day)
类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this- >_year = year
C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用 this 指针。
class Date
{
public:
// void Init(Date* const this, int year, int month, int day)
// this指针在函数体内部可以显示使用,但是不建议
void Init(int year, int month, int day)
{
this->_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
// this指针在形参和实参中不能显示使用(编译时编译器会处理),但是在函数体内部可以显示使用
// d1.Init(&d1, 2025, 10, 1);
d1.Init(2025, 10, 1);
// d1.Print(&d1);
d1.Print();
return 0;
}
分析一下代码能否正常运行?
代码1:
class A
{
public:
void Print()
{
cout << "A::print()" << endl;
}
private:
int _a1;
};
// 情况1:
int main()
{
// 调用成员变量或成员函数,可以用对象或对象的指针访问
A* ptr = nullptr;
ptr->Print();
// 这里的 -> 表示的是调用成员函数,而不是解引用,通过一个空指针调用成员函数Print
// -> 只是表示调用这个成员函数,call 函数地址,而print函数(print地址编译成指令时)并没有存到p指向的对象里面(也就不存在解引用)
// 因为函数地址在编译或链接时就已经确定了,所以就不会通过解引用去调用这个函数
return 0;
}
// 正常运行
// ptr->Print(); 这句代码整体表示的含义就是调用A类中的Print()函数,ptr是作为参数传递给this指针
// 而在Print()函数内部不存在对this指针的解引用,所以能够正常运行
// 实际上被编译器处理为
A::Print(ptr); // 传递ptr作为this指针
// 情况2:
int main()
{
// 调用成员变量或成员函数,可以用对象或对象的指针访问
A* ptr = nullptr;
(*ptr).Print(); // 这句代码表示的含义与ptr->Print();是一样的,都是调用A类中的Print()函数
// 但是这个函数的地址并没有存在这个对象里面,所以就不会通过解引用去调用这个函数
// ptr是作为参数传递给this指针,因为函数体内部不存在对this指针的解引用,所以能正常运行
return 0;
}
// 正常运行
代码2:
class A
{
public:
void Print()
{
cout << _a1 << endl;
// 要访问成员变量_a1,存在对this指针的解引用
cout << "A::print()" << endl;
}
private:
int _a1;
};
int main()
{
A* ptr = nullptr;
(*ptr).Print();
return 0;
}
// 不能正常运行
结语
如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。