前言: 前面C++的学习,我们认识了一些C++的入门的语法知识,经此,我们可以认识一些简单的C++代码,本篇博客我们将踏入C++中比较重要的知识点,类和对象,事实上,C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。而C++呢,是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完 成。听起来是比较抽象,请看下文如何娓娓道来!

上面我们说:C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完 成。
啥意思,挺抽象的,看下面这个图

面向过程事实上强调完成一个人物整体的一个流程也即是逻辑,例如我们洗衣服

面向对象强调的是不同对象完成不同的任务,对象与对象之间相互交互完成最终的任务。再如洗衣服这个例子而言如下: 总共有四个对象:人、衣服、洗衣粉、洗衣机 整个洗衣服的过程: 人将衣服放进洗衣机、倒入洗衣粉,启动洗衣机,洗衣机就会完 成洗衣过程并且甩干 整个过程主要是:人、衣服、洗衣粉、洗衣机四个对象之间交互完成的,人不需要关 新洗衣机具体是如何洗衣服的,是如何用干的

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct ZSTU {
int a;
int b[20];
void print()
{
cout << "浙江理工大学" << endl;
}
};
int main()
{
ZSTU hab;
hab.print();
return 0;
}上述代码就可以调用结构体中的函数,这要是在C语言中是不允许的,但是C++中我们一般不用结构体,而是用class(类)
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略 事实上就是结构体换个名字而已,其中的细节变更一下。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者 成员函数。
类有两种定义方式:
1️⃣声明和定义全部放在类体中
注意:成员函数如果在类中定义,编译器默认(可能)会将其当成内联函数处理。
当然这只是给编译器一建议 具体会不会被当作内联要看代码长度 (这在前面我们不是说了,编译器会自我优化,如果代码太长它就不会当作内联)
class ZSTU {
int a;
int b[20];
void print()
{
cout << "浙江理工大学" << endl;
}
};这个就是声明和定义全都放在类中,大家可能有这样的疑惑,声明声明的,C语言当中.h文件给一句话就是声明,它啥意思,声明和定义的区别:事实上声明就是一种承诺,就是说一声,我有这个东西,但是没有 具体的实现,定义就是这个东西确确实实有,不是给你说空话。
2️⃣类声明放在.h文件中,成员函数定义放在.cpp文件中
注意:成员函数名前需要加类名::,前面我们也介绍了域这个概念,这也是这个意思,一个类就有新的作用域,那在.cpp写函数的实现的时候,理应表示出它属于谁,对不对。
.h
struct ZSTU {
int a;
int b[20];
void print();
};.cpp
#include"XXX.h"
void ZSTU::print()
{
cout << "浙江理工大学" << endl;
}一般而言比较推荐第二种方式,日常我们为了方便,使用第一种无可厚非,看自己喜好,但是还是推荐第二种,听人劝,吃饱饭🤪
补充:
struct ZSTU {
public:
int Add(int one, int two)
{
one = one;
two = two;//看起来是不是很别扭,啥玩意这是,这里的two到底是成员变量,还是函数形参?
return one + two;
}
private:
int one;
int two;
};
//所以对于成员变量我们一般这样命名
struct ZSTU {
public:
int Add(int one, int two)
{
_one = one;
_two = two;
return _one + _two;
}
private:
int _one;
int _two;
};C++中类的限定符有三类,如下所示:

访问限定符说明:
有点抽象,事实上就是,公有的就是让你看,私有的或者是保护的就不让你看,例如我们的手机号码,以及密码什么的,比较隐私,我们就设置成私有或者保护的,在此我们先认为私有和保护是一样的,我们经过一段代码来进行分析,如下:
class ZSTU {
public:
int Add(int one, int two)
{
one = one;
two = two;
return one + two;
}
private:
int a;
int b[20];
};
int main()
{
ZSTU hab;
hab.a = 10;//会报错
return 0;
}可以发现私有限定符内的变量我们在类外面是不可以访问的,但是在类里我们是可以访问的。访问权限是介于限定符与限定符之间的,对于最后一个是到 } 结束,class默认权限是私有的,struct是公有的,等下面我们验证。学习的时候,我的脑子犯傻了,你说把上面的private去掉,那些变量是公有还是私有的,你可别说是私有的,说私有你就跟我犯一样的错误了,虽说private去掉了,但是人家上面还有public呢,所以此时是公有的。


小问题一个:C++中struct和class的区别是什么? 解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。 注意:在继承和模板参数列表位置,struct和class也有区别,后面我们再进行了解。
//C语言当中定义结构体
struct Stack{ //其中struct Stack叫做它的类型,而不是Stack
};
//C++中无论是struct或者是class定义类的时候,类名就是它的类型
class Stack{ //Stack就是它的类型
}; //当然上面这个问题说了C++兼容C语言,所以结构体那套和C语言是一样的
//当然用stuct定义类就和C++那套是一样的,总之很灵活就对了
struct Stack{
struct Stack* ListNode;
Stack* ListNode;//两种在一个里面定义都可以,一般我们用这种,谁愿多敲几个字呢对不对
};需要注意的是: 无论你公有还是私有,我类中的成员函数是始终可以访问你的成员变量的
用类类型创建对象的过程,称为类的实例化
1️⃣ 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2️⃣一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
class ZSTU {
public:
int Add(int one, int two);//这是声明
int a;//这也是声明
int b[20];
};
int main()
{
cout << sizeof(ZSTU) << endl;//打印结果为84
ZSTU.a = 10;//这就会报错,因为类没有实例化,ZSTU是没有空间的
return 0;
}也就是说什么意思呢?定义一个类并不是说就是开辟空间,它仅仅是声明,但是当你sizeof(ZSTU)时你会发现竟然有数值。 嗯?什么鬼,你不是说不开辟空间,这怎么打印了结果,这不和你的结论相悖了吗? 别着急,听我慢慢讲来,首先一点可以知道的是,在ZSTU.a = 10时确确实实是报错的,那就说明没有这个空间放置10这个数字,但是为什么sizeof有数值,那就说明是sizeof这边出现了问题,而不是我们的结论出现了漏洞,事实上sizeof计算的是类的"潜在大小",而不是类声明本身的大小,可以这样想: 想象一个建筑蓝图:
sizeof):告诉你未来建筑会有多大
那也不对啊,那这样讲84也不对啊,函数为什么不计算? 这个知识下一章节进行介绍。 所以结论如下: 1️⃣ 类声明(Class Declaration):
2️⃣ 对象实例化(Object Instantiation):
sizeof(类名)计算的是对象大小:它表示当创建对象时,每个对象需要的内存大小
sizeof结果
事实上类就像是一个盖房子的图纸,空谈,实例化就是在图纸的基础上盖房子。如下所示:

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算 一个类的大小?
上面我们简单的看到了,sizeof(类名)的大小,事实上那就是类对象的大小,也可以观察到,里面只计算了成员变量的大小,并没有成员函数的大小,为什么呢?请看下文:
结论:类对象只保存成员变量,成员函数存放在公共的代码段,因此计算类对象的大小也即是计算其中成员变量的大小(考虑内存对齐)
为什么?说了这么多,为什么,怎么就不能类对象里面存储函数,说明事由倒也简单,我们说一个东西不能,是不是从两方面说,就是这种方式有缺陷,而其它方式有优点是不是就解决了。因此,假设是类对象里面存储函数,那么一个类在实例化出N个对象,每个对象的成员变量当然是自己的可以存储不同的值,但是调用的函数却是同一个,你每个对象都是一样的,却各自都存储了一份,这是不是浪费空间。那我把这部分函数放置在公共的区域,供你们都可以使用,但是我只有一份,是不是在保留功能的基础上节约了空间。因此结论来了:
函数定义必然占内存:只要函数有实现体({}内的代码),编译器就会生成对应的机器指令,这些指令在程序加载时就存在于内存的代码段中
与实例化无关:
即使从未创建任何ZSTU对象,Add()的代码仍然在内存中
创建1000个ZSTU对象,Add()在内存中仍然只有一份副本
类比普通函数:成员函数和全局函数在内存占用上没有本质区别
空说无凭,你给我看下,行我们看下汇编代码证明一下,如下:
class ZSTU {
public:
int Add(int one, int two)//这是声明
{
return one + two;
}
int a;//这也是声明
int b[20];
};
int main()
{
ZSTU zstuer1;
ZSTU zstuer2;
ZSTU zstuer3;
zstuer1.Add(1, 2);
zstuer2.Add(1, 2);
zstuer3.Add(1, 2);
return 0;
}
做几个题?感受一下,判断下面几个类的大小,你是否都能答对?
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};揭晓答案:A1大小是4 A2大小是1 A3大小是1
嗯?A1是4我理解,A2和A3为什么是1,你不是说函数在类里面不占空间,为什么是1,这不是占空间了,你讲错了😡。停!别走🥲,我还有话说,我知道你有疑惑,先别急,听我说,A1大家肯定都理解,上面知识点在那,还不懂的再看下,对于A2和A3,为什么都是1,说明它们都是一样的对不,那A3没有函数,A2却有函数,这不正说明,函数在类里面不占有空间,至于为什么是1,直接上结论:为什么是1,不是0?因为开1个字节的空间不是为了存储数据,而是为了占位,来表示对象存在,如果没有这1个字节空间,我们A2和A3实例化的对象,怎么表示它们的不同,它们连空间都没有,更别说表示空间的地址。
下面再回顾个知识点,先看个代码
class A1 {
public:
void f1() {}
private:
int _a;
char _b;
};这个答案是多少?你可别说是5,嗯?怎么不是5,char不是1个字节吗?好了,你学习的C语言已经还给课本了,先说答案吧,这个类实例化对象的大小是8,这里牵扯到内存对齐这个知识点,我们在这里回顾一下,加深一下之前的学习,想要仔细了解的请看C语言-自定义类型(结构体、枚举、联合)如此简单。
结构体内存对齐规则:
所以上述最终,最大对齐数是4,那么结构体的总大小就是4的整数倍,也就是8,则第5个字节之后都是填充的了。
再看几个例子:
struct Example1 {
char a; // 1字节 (地址0)
int b; // 4字节 (需要4对齐 → 地址4-7)
char c; // 1字节 (地址8)
};
// 大小:1 + 3(填充) + 4 + 1 + 3(填充) = 12字节[a][填充][填充][填充][b0][b1][b2][b3][c][填充][填充][填充]struct Example2 {
int b; // 4字节 (地址0-3)
char a; // 1字节 (地址4)
char c; // 1字节 (地址5)
};
// 大小:4 + 1 + 1 + 2(填充) = 8字节[b0][b1][b2][b3][a][c][填充][填充]struct WithArray {
char a; // 1字节 (地址0)
double b[3]; // 每个8字节 (需要8对齐 → 地址8-31)
int c; // 4字节 (地址32-35)
};
// 大小:1 + 7(填充) + 24 + 4 + 4(填充) = 40字节class ZSTU {
public:
int Add(int one, int two); // 函数不影响大小
char a; // 1字节
int b; // 4字节
double c; // 8字节
short d; // 2字节
};
//最大对齐值 = max(1,4,8,2) = 8
//成员布局:
//char a (1字节,地址0)
//填充3字节(使int对齐到4)
//int b (4字节,地址4-7)
//double c (8字节,地址8-15) ← 8是8的倍数
//short d (2字节,地址16-17)
//总大小:17字节 → 向上填充到最大对齐值8的倍数:24字节1️⃣ 结构体怎么对齐? 为什么要进行内存对齐?
2️⃣ 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
3️⃣ 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
问题1:怎么对齐如上所述,对齐的原因是硬件访问效率和安全性要求。
问题2:现在无法回答,待到经历后续的学习之后,我们再回头解决这个问题。
问题3:大端字节序:把数据的低位字节序的内容存放在高地址处,高位字节序的内容存放在低地址处。小端字节序:把数据的低位字节序的内容存放在低地址处,高位字节序的内容存放。联合体法就可以检测,想要具体了解的可以看之前的博客:C语言-自定义类型(结构体、枚举、联合)如此简单,应用场景在网络协议、文件格式、跨平台通信、嵌入式寄存器访问等会应用到。
对于检测大小端也可以用指针检测,或者标准库检测(C++20),如下:
//指针检测
#include <iostream>
bool isLittleEndian() {
uint32_t test = 0x01020304;
return *(uint8_t*)&test == 0x04; // 检查最低地址字节
}
int main() {
std::cout << (isLittleEndian() ? "Little Endian" : "Big Endian");
return 0;
}
//标准库检测
#include <bit>
if constexpr (std::endian::native == std::endian::little) {
// 小端处理
}闲话少叙,直接上代码,我们以一段代码为例:
class Time
{
public:
void Init(int hour, int minute, int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time d1, d2;
d1.Init(17, 39, 20);
d2.Init(17, 39, 21);
d1.Print();
d2.Print();
return 0;
}不知道大家是否有这样的疑问,你上面说,函数是放置在公共代码段,这我认了,我接受它,可你不能把我当猴耍,一来你print不传参数,竟然用了,还能打印出结果,二来两个不同的对象拥有不同的参数,你竟然在没有传参的情况下把各自不同的参数都打印出来,真视吾眼睛瞎乎?😤稍安勿躁,稍安勿躁,一切的一切皆因一个名叫this指针的家伙在背后搞的鬼,我们现在就来解决它。
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。
啥意思呢?上面一大段看着也挺费眼,直接上代码:
class Time
{
public:
void Init(int hour, int minute, int second)//该指令等效为:void Init(Time* this, int hour, int minute, int second)
{
this->_hour = hour;//就是说this以形参的形式,默认被编译器传了过来
_minute = minute;//在函数体内,上述this->使用也行,直接使用也行。一般使用这种方式,谁愿多敲字呢?
_second = second;
}
void Print()//等效为:void Print(Time* this) //但是你不能在函数的形参位置对this定义,这就会报错,因为编译器内部这就已经传了
{
cout << _hour << ":" << _minute << ":" << _second << endl;
//cout << this->_hour << ":" << this->_minute << ":" << this->_second << endl;//这都是等效的
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time d1, d2;
d1.Init(17, 39, 20);//该语句也就是等效成为d1.Init(&d1, 17, 39, 20)
d2.Init(17, 39, 21);//该语句也就是等效成为d2.Init(&d2, 17, 39, 20)
d1.Print();//该语句也就是等效成为d1.Print(&d1)
d2.Print();//该语句也就是等效成为d2.Print(&d2)
return 0;
}此时上面的问题便迎刃而解,因为你内部竟然讲不同对象将对象的地址偷摸的传给了函数,那当然访问它们的变量也即是不同的。
看两个小问题:
1️⃣ this指针存在哪里?
2️⃣ this指针可以为空吗?
问题1:上述第四点就回答了
问题2:我们通过一个代码来回答它
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();//A、编译报错 B、运行崩溃 C、正常运行
p->PrintA();// A、编译报错 B、运行崩溃 C、正常运行
return 0;
}先说答案吧,p->Print()是正常运行的,p->PrintA()是报错误的
A* p = nullptr,p->Print(),大家理应会觉得是nullptr->Print(),那就会觉得两者都是空指针解引用导致错误,那先问下如果是这样,是编译错误呢还是运行崩溃?我不知道哪个错了反正就是空指针解引用错了,好,听我说。
第一个知识点:空指针访问是不会在编译阶段被检查出来,编译阶段:编译器只能分析代码的静态结构和语法,也就是诸如:词法错误(拼错单词)、语法结构错误(如函数不带{})、类型错误(字符类型给整型)等(就是说编译阶段能处理的错误静态的、一些单词结构之类的)。而空指针解引用是运行时内存访问属于运行时错误,还有包括:
运行时错误:
错误类型 | 示例 | 原因 |
|---|---|---|
空指针解引用 | int* p = nullptr; *p=1; | 运行时内存访问 |
除零错误 | int x=0; 1/x; | 运行时计算 |
数组越界 | int arr[3]; arr[5]=1; | 运行时内存访问 |
资源泄漏 | new int[100]; | 运行时资源管理 |
数据竞争 | 多线程共享变量无保护 | 运行时线程交互 |
逻辑错误 | 死循环、算法错误 | 程序语义而非语法 |
编译阶段错误检测边界总结:
可检测类型 | 不可检测类型 |
|---|---|
语法结构错误 | 运行时内存访问错误 |
类型系统违规 | 多线程同步问题 |
声明/定义不一致 | 算法逻辑错误 |
模板实例化问题 | 资源管理泄漏 |
常量表达式违规 | 输入相关错误 |
访问权限冲突 | 环境依赖行为 |
简单静态分析警告 | 复杂控制流空指针 |
所以纵使发生了错误它的答案也绝不是编译错误,此排除A也,那是不是运行崩溃?好,第二个知识点来了。 第二个知识点:空指针调用成员函数不崩溃 原因: 1️⃣成员函数的调用机制
this 指针参数
p->PrintA()
A::PrintA(p)(编译器转换后的形式)
2️⃣成员函数代码的位置
因此,上述代码p->Print()是不报错的,并且照常打印Print(),而p->PrintA()是报错的,但是它的报错不是因为p->而是因为类里面函数cout << _a << endl;_a部分,前面我们说了隐含传递了this指针,这才是真正的this->_a,发生了空指针解引用导致了错误。 总结一下: 情况行为原因调用非虚成员函数函数能执行直到访问成员变量函数代码不依赖对象实例访问成员变量导致段错误/崩溃需要解引用 this 指针调用虚函数立即崩溃需要访问虚表指针 (vptr)静态成员函数安全调用无 this 指针
这篇博客我们了解了 C++ 面向对象相关内容。从面向过程与面向对象的区别讲起,着重阐述了 C++ 类的引入、定义方式(包括 struct 和 class 的差异、成员函数定义位置等),类的访问限定符(public、protected、private)及封装要点。还详细说明了类的实例化过程,即对象的创建和内存分配情况。此外,深入探讨了类对象模型,包括如何计算类对象大小(只与成员变量及其内存对齐相关),并举例分析了不同类的成员变量布局和大小计算。最后介绍了 this 指针的概念、特性及其在成员函数中的关键作用,同时分析了通过空指针调用成员函数可能出现的问题。