目录
1.引言
会了C++基础后,接下来我们来讲讲C++中十分重要的一部分------类和对象
因为类和对象的内容很多,且学习难度相比于前面的内容比较高,所以我这里就分三篇来讲类和对象,上主要是用来初识类和对象并且能够定义基础的类(启蒙),中主要讲类和对象的重点(深入学习),下主要讲边边角角剩下的一些东西(收尾)。
我们这篇主要讲三个部分,类的定义,实例化和this指针,那么话不多说,接下来我们就进入类和对象(上)的内容------------------>

2.类的定义
在讲类之前,首先我们要先引入一个关键字------class,class是定义类的关键字,就跟结构体一样,struct是定义结构体的关键字
2.1.类定义格式
类定义的格式和结构体是十分相似的,结构体怎么定义的,类就怎么定义就可以了,类中 既可以定义变量,也可以定义函数。在C语言的时候我们讲了结构体只能定义变量,但是C++的结构体和C语言不同,C++的结构体也可以定义函数了 ,相当于C++是把结构体升级到了类。
我们来看关键点
1.定义类的框架和定义结构体的框架是一样的,只需要把定义结构体时的struct改为class就是定义类了。类中既可以定义变量,也可以定义函数。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数
2.为了区分成员变量,一般习惯上成员变量会加一个特殊标识,比如成员变量前面加_,这是一个好习惯,建议养成,一般公司也会有具体要求
3.C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类(因为class默认成员是私有的,但是struct默认成员是公有的,如果所有成员都是公开的不想用访问限定符的时候才会用到struct,私有公有我会在访问限定符部分具体讲解)
4.定义在类里面的成员函数默认为inline
5.类里面的成员函数可以声明和定义分离(这种就不是内联了)
接下来,我们来通过具体代码来看看类的使用
首先是定义一个类,代码如下
cpp
#include <iostream>
using namespace std;
class Stack
{
};
int main()
{
return 0;
}
在C语言中,我们实现栈的时候结构体和函数是分离的,但是到了C++中,因为类里面可以放函数,所以我们可以把函数放在对应的类里面,我们看下面的代码(public先不用管,这是访问限定符,这个表示公有,在访问限定符部分会讲)
cpp
#include <iostream>
using namespace std;
class Stack
{
public:
void push()
{
//...
}
int top;
int* a;
int capacity;
};
int main()
{
return 0;
}
那么这样相比于C语言有什么好处呢,比如说如果有很多的数据结构都在一个文件中,那么如果用C语言的方式不能直接用push,因为栈要push,队列要push,但是他们在一个域里,这就会导致同名函数的出现,而且C语言还没有函数重载,但是有了类的概念后,就可以在对应的数据结构类里写上成员函数,因为函数在类里,也就是在类域里,不同的类为不同的类域,不会放生冲突,就和命名空间一样,类域也会在后面详细讲解。
然后如果想要调用类域里的成员函数,就和之前结构体调用的方式一样就可以了,代码如下
cpp
#include <iostream>
using namespace std;
class Stack
{
public:
void push()
{
//...
}
int top;
int* a;
int capacity;
};
int main()
{
Stack st;
st.push();
return 0;
}
我们再来看第二点,为什么建议在成员变量前加上特殊标识呢,我们先看下面这个代码
cpp
#include <iostream>
using namespace std;
class Data
{
public:
void Init(int year,int month,int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
int main()
{
Data d;
d.Init(2026, 5, 16);
return 0;
}
我们可以发现初始化年月日的时候,参数是年月日,成员参数也是年月日,那这个时候就很难区分了,这个时候就只能改变形参名或者成员名,一般这个时候改的都是成员名,也就是在成员变量名前加个_,优化完就是这样,统一命名规则后,就能看出什么是成员变量,什么不是成员变量
cpp
#include <iostream>
using namespace std;
class Data
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d;
d.Init(2026, 5, 16);
return 0;
}
附:在类中,成员函数和成员变量定义的顺序可以随意,C++没有限制,一般情况下是成员函数在上,成员变量在下。
然后我们来看第三点,第三点已经讲了struct和class的区别以及struct的变化,struct已经被升格成了类,但是又因为C++兼容C,所以创建结构体变量的方式既可以用C语言创建结构体的方式创建,也可以用C++类的方式创建,也就是下面的代码
cpp
#include <iostream>
using namespace std;
struct Data
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data a;
struct Data b;
return 0;
}
a是用C++的方式创建的结构体,b是用C语言的方式创建的结构体
我们也可以用更直观的样例来对比一下
首先是C语言结构体方式的构建
cpp
struct ListNode
{
int val;
struct ListNode* next;
};
然后这是C++类的方式构建的
cpp
struct ListNode
{
int val;
ListNode* next;
};
这种方式在C语言中是编不过的,只有在C++中能编过,因为C++把结构体上升到了类的级别
第四点就不说了,我们来看第五点
类内的成员函数是可以声明和定义分离的,那么要怎么分离呢,首先,声明要在类内,定义要在类外,如下
cpp
#include <iostream>
using namespace std;
class Data
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
return 0;
}
但这个时候会有新的问题 ,因为_year,_month和_day是Data类的成员变量,把函数的定义放在全局域的时候访问不到类域的成员,这个时候怎么办呢,我们就要让编译器知道这个函数是属于哪个类的,就是用我下面的方式,这就表明了Init函数是Data类的成员函数,这跟访问命名空间里的对象的方式类似,具体会在类域部分讲解
cpp
#include <iostream>
using namespace std;
class Data
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Data::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Data a;
struct Data b;
return 0;
}
2.2.访问限定符
C++新增了一个叫访问限定符的东西,我们直接来看关键点
访问限定符有三个,public(公有),private(私有),protected(保护),这是C++实现封装的一种方式,类中有了成员函数和成员变量,对象很完善了,但是为了防止用户使用不该使用的东西,可以通过访问权限限定符,选择性的将其接口提供给外部的用户使用。
public修饰的成员在类外可以直接被访问
protected和private修饰的成员在类外不能直接被访问,在现阶段,把protected和private当成一样的就可以了,他们的区别要到继承部分才能体现出来
访问限定符的作用域是从该访问权限符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到类的结束
class定义成员没有被访问限定符修饰时默认为private,struct默认为public
一般成员变量都会被限制为private或protected,需要给别人使用的成员函数会放位public
**访问限定符可以出现多次,但一般不出现多次,一般会放在一起然后放上访问限定符,一般给变量的是私有,**保护一般是在继承里面用到
2.3.类域
类域是我们C++四大域中最后的一个域,类域和命名空间域一样不影响变量的生命周期
类域和命名空间域一样,是定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要使用::作用域操作符指名成员属于哪个类域。(这个在上面演示过就不再掩饰啦)
类域影响的是编译的查找规则,和命名空间域一样,如果指定类域的话,就会去对应的类域查找
3.实例化
类还有一个概念,便是类的实例化
3.1.实例化概念
我们先把类的实例化统揽一下
1.用类类型在物理内存中创建对象的过程,就称之为类实例化出对象(也就是类的实例化),简单的说就是创建类类型的对象
2.类中的成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间
3.一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
那么,我们如何知道类里的成员变量是声明还是定义呢,很简单,我们看下面这个代码
cpp
#include <iostream>
using namespace std;
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data::_year = 0;
return 0;
}
如果说类中的成员变量是定义的话,那么就能通过域作用限定符来访问,但是我们可以发现,如果我们这么写,就报错了,并且报错信息是不可访问,如下图

所以类中的成员变量其实是声明而不是定义
我们可以怎么想类这个东西呢,我们可以把类想象成房屋设计图纸,一个类可以实例化出很多的对象,房屋设计图纸也可以建造出很多的房子,类中的成员变量是声明,只有实例化后才会分配空间,房屋设计图也只是一张图纸,只有建造出来后才会占空间,才能住人
3.2.对象大小
类的对象大小要怎么算呢,成员对象的大小和C语言中我们所求结构体大小一样,要按照内存对齐的规则去进行计算
现在我们主要考虑的是要不要把成员函数的指针存到对象里面,结论是成员函数存在代码块里
原因:首先我们要想类实例化出对象后,每个对象都有独立的空间,空间中塞成员变量是没问题的,因为每个对象的成员变量不一定相同,但是,一个类的每个对象中的成员函数是相同的,那再额外给每个对象存放对应函数的指针就太浪费空间了,比如一个类实例化了1000次,如果把指针的空间也存放在对象中,那么相当于成员函数指针重复存储了1000次,这是相当浪费的。那么把成员函数存储在代码块里是最完美的,因为代码段里只需要存储一次,就可以频繁调用到了,这就和C语言中的字符串常量存储一样
附:函数被编译完了是一段指令,函数指针可以认为是第一句指令的地址,调用函数被 编译成汇编指令的时候,编译器会在编译链接的时候通过符号表找到函数的地址,所以函数指针不用往对象里存。当然有特例,只有动态多态是在运行时找,这个时候就需要存函数地址,这个放在后面讲
接下来就是内存对齐的规则,因为之前在C语言时候已经重点讲过了,所以这里就直接把要注意的点展示一下

那么,接下来,我们来看下下面的代码,分别算下A,B,C这三个对象的大小分别是多少
cpp
#include <iostream>
using namespace std;
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对象,已知成员函数是存放在代码段里的,所以只需要算成员变量就行,根据对齐规则可以算出A的大小是 8,随后对B和C这俩个对象都没有成员变量(简称空类),这是特殊情况,他们的大小 不是0是1,因为要表示对象存在过,毕竟也有取地址之类的操作,如果大小为0那岂不就是空指针了,所以这里的1相当于是为了占位表示对象存在。我们来看下运行结果

4.this指针
这部分的知识与上面那些相比会比较难
4.1.概念
首先通过上面的讲解,我们知道了类的对象有各自的成员变量,但是这些对象的成员函数的是一样的,那么他们是怎么找到对应的对象中的变量的呢,这个就用到了隐式的this指针
我们首先来看下面的代码进行思考
cpp
#include <iostream>
using namespace std;
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 d1, d2;
d1.Init(2022, 5, 5);
d1.Print();
d2.Init(2026,5, 17);
d2.Print();
return 0;
}
我们知道对象调用的成员函数都是一样的,那如何实现d1和d2输出的结果不同呢,C++就是新增了一个this指针的概念来访问对应的成员变量。
首先我们要知道Print函数的_year,_month,_day这些变量访问的不是类中的_year,_month,_day,因为类中的成员变量只是声明,声明是给编译的时候用的,而不是在实际执行的时候用的,他们访问的数据就是通过隐含的this指针来查找的
接下来我们就来统揽一下
先回复上面,Date类中有Init与Print俩个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象的呢,那么这里就要看到C++给了一个隐含的this指针解决这里的问题(所有非静态的成员函数都有隐式的this指针),this指针指向的就是调用的对象
编译器编译后,类的成员函数默认会在形参第一个位置,增加一个当前类类型的指针,叫做this指针,比如Date类的Init的真实原型其实是
void Init(Date* const this, int year, int month, int day),所以在调用的时候会隐式的传过去一个地址,也就是d1.Init(&d1,2022, 5, 5);
类的成员函数中访问成员变量,本质上都是通过this指针访问的,如Init函数中给_year赋值,this->_year = year,这种操作其实就和我们先前所学的结构体指针的方式相差无几了,这里就不过多赘述了
C++规定不能在实参和形参的位置显示的写this指针(因为编译时编译器会处理,如果想在实参传对象地址,形参接收就别用this指针,但有了this指针后,一般实参不传对象地址),但是在函数体里面可以显示的是用this指针,就比如下面这样写和上面写的效果是一样的,知识吧隐式this指针换成了显示this指针,如下
cppvoid Init(int year, int month, int day) { this->_year = year; _month = month; _day = day; }
附:this指针不能被修改,this指针指向的可以修改
4.2.训练
接下来我们来做几道题训练一下this指针的理解顺便拓展一下
第一题:
下面这个代码的运行结果是什么()
A. 编译错误 B.运行崩溃 C.正常运行
cpp
#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;
}
答案是C,首先这个代码创建了一个A类型的指针p并设置为了nullptr,所以p这个指针指向的是空,随后调用了Print函数,因为Print函数是在代码段里的,并不会对p进行解引用,所以这个时候不会出问题,但是这时候隐式传过去的this指针其实就是p,但是Print函数里面并没有用到this指针的地方,所以这个代码运行下来是没有问题的
第二题:
下面这个代码的运行结果是什么()
A. 编译错误 B.运行崩溃 C.正常运行
cpp
#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;
}
答案是B,这个代码和第一题的代码只有在Print函数部分有区别,那么我们只需要分析新增的那一行代码,因为那行代码用到了成员变量,所以隐式使用了this指针,但是this指针是nullptr,这就导致对空指针解引用了,所以运行崩溃了。
第三题:
this指针存在内存的哪个区域()
A.栈 B.堆 C.静态区 D,常量区 E,对象里面
答案是A,我们来分析一下,首先是E,如果this指针存在对象里面的话,我们先前的对象大小计算方式就是错的了,因为this指针也是占空间的,所以E选项可以排除,那么就说明this指针就是存放在正常内存区中,因为this指针是形参 ,所以存放在栈区,也就是A,当然有些编译器会优化,优化后就会放在寄存器里面
5.C++和C语言实现Stack对比
首先面向对象有三大特性:封装,继承,多态,下面的对比我们可以初步了解一下封装
我们首先看一下C语言版Stack的实现


我们可以发现,C语言版的Stack使用struct实现的,函数都是放在全局的
接下来我们来看一下C++版本的Stack实现


我们可以发现C++实现Stack是用类实现的,Stack的属性和相关函数都放在了类里面
我们总结一下俩者的区别
C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这里的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。
C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef而是用类名就很方便
在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等到我们后面看STL 中的用适配器实现的Stack,就会有翻天覆地的变化了
6.结语
那么,C++类和对象(上)的部分就讲解完毕啦,希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦
