目录
1.类的定义
类中的变量称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数
使用class关键字命名类,类中的成员变量默认是私有的,类中的成员函数默认是inline函数,如下图,简单定义了一个stack类,成员变量有一个数组,表示栈顶的top,表示栈大小的size
注意类中还具有访问限定符,通过访问权限选择性将接口给外部使用,如果没有被限定访问限定符,那么默认为private,在struct结构体中则默认为public
public修饰的成员可以在类外直接被访问,protected和private修饰的成员在类外不可直接被访问

1.1类域
类定义了一个新的作用域,所有的成员都在类的作用域中,在类外定义成员时,需要使用域操作符指明成员属于哪个类域,类域会影响编译器的查找,例如在域外定义一个成员函数,使用到了内部的成员变量,如果不指定类域,此时就会发生报错,例如下面这个函数在类外进行定义时,使用到了内部的数组以及top,那么如果不指定stack域,编译器就找不到这些变量从而产生报错

2.实例化
类定义完,相当于是一个房子的图纸,给了一个基本结构,而实例化才是建成的房子,所以类只是一个模型一样的东西,只有实例化对象时,才会分配空间,类本身只是一个声明,没有分配空间
如下图,创建了一个日期的类,构造函数之后再讲,这里只要知道可以用这样的方式将类实例化,d1就是通过一个图纸创建出来的实例,一个图纸可以构造很多实例出来

2.1实例对象的大小
类实例化出的每个成员变量都要开辟一块空间,所以成员变量一定在对象中,而函数在编译后是一串指令,使用call调用,没办法存储在对象中,而存储在一个单独的区域,所以对于函数要么就用指针指向,然后存储在对象中
但是如果我们实例化了很多对象,几百个几千个,对于函数来讲它们的函数都是一样的,而成员变量一般是不一样的,所以使用指针去存储函数非常浪费空间,所以函数是不包含在对象中的,所以对象中只包含成员变量,而没有成员函数
实例化对象的内存大小的计算方式,和结构体是一致的,不了解的可以看一下这篇博客 结构体深入学习-CSDN博客
注意如果没有成员变量,还是要给这个对象一个字节的大小,因为如果没有内存这个对象怎么还会存在,所以给一个字节当作占位标识符
3.this指针
类的成员函数会默认在形参的第一个位置增加一个当前类型的指针,叫做this指针,它存储在栈区,相当于将使用这个函数的对象的一个指针传入,这样就可以对这个对象内部的成员变量进行操作了
例如这个输出函数,其实括号里有一个隐藏参数,所以这里才能正确使用成员变量_year,_month和_day
虽然在形参位置不能将this指针写出来,但是这里在使用成员变量的时候可以直接写成员变量,也可以写this->变量名
cpp
void Print(/*Date const* this*/){
//成员函数
cout << _year << "/" << _month << "/" << _day << endl;
}
注意在使用函数的时候,使用的是call指令,所以即使我们用指针实例化一个对象,并将它置为nullptr,如果函数内部没有使用到成员变量,那么是不会报错的,因为使用函数并没有对空指针进行解引用,但是如果要使用成员变量,那么相当于要对空指针进行解引用,是会报错的
注意对空指针解引用是运行时的错误而不是编译错误,编译时语法是通过的
cpp
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
//cout << _a << endl;
//此时会报错,因为在对空指针解引用
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
4.构造函数
构造函数的主要任务不是开辟空间创造对象,而是在实例化对象时初始化对象,本质是替代之前栈和队列中的初始化函数,因为在实例化时会自动调用,省去忘记写初始化函数的缺点
构造函数的特点
1.构造函数名和类名相同
2.没有返回值
3.实例化时会自动调用对应的构造函数
4.构造函数可以重载
5.如果没有显式定义的构造函数,会自动生成一个无参的默认构造函数
6.无参构造,全缺省构造以及不写构造编译器默认生成的构造函数都称为默认构造函数,简单来说就是不传参就可以调用的构造叫做默认构造
例如创建了一个日期的类,有以下几种构造函数,注意构造函数也是可以重载的,只要满足函数重载的条件了,注意第二第三个构造函数不构成函数重载,要选择一个注释掉或者更改一下参数顺序使得它们构成重载
cpp
class Date {
public:
//构造函数与类名相同
//并且无返回值,只写与类名相同的函数名即可
//无参构造函数
Date() {
_year = 2025;
_month = 12;
_day = 22;
}
//全缺省构造函数
Date(int year = 2026,int month = 3,int day = 19) {
_year = year;
_month = month;
_day = day;
}
//传入三个参数
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
//构造函数可以重载,类似函数重载
//参数类型不同,参数个数不同,参数顺序不同都可以算重载
private:
int _year;
int _month;
int _day;
};
5.析构函数
析构函数用于释放类中动态开辟的资源,在对象销毁时会自动调用,相当于实现了之前写过的销毁函数的功能,注意如果没有资源需要释放,那么析构函数可以不写,反过来说如果有资源需要释放,一定要自己写析构函数,不然会造成内存泄漏
析构函数的特点
1.析构函数的名字是类名前加上一个~
2.没有返回值也没有参数
3.一个类只能写一个析构函数,如果没有显式定义,则会生成默认的析构函数
4.在对象生命周期结束时,会自动调用析构函数
5.对于自定义类型的成员变量,会调用它自己的析构函数去释放资源
6.拷贝构造函数
拷贝构造函数是一种特殊的构造函数,特点为第一个参数为自身类类型的引用,并且其他额外参数都i有默认值,将具有这种特点的构造函数叫做拷贝构造
1.拷贝构造是构造函数的一个重载
2.第一个参数必须是本类类型的引用,如果进行传值调用,会引发编译器报错,因为传值调用,要先对这个对象进行拷贝构造,要进行拷贝构造就要先进行传值调用,那么就会陷入无限循环导致函数崩坏,后续参数必须都有缺省值
3.C++规定,自定义类型的拷贝行为都要调用拷贝构造,所以传值传参和传值返回也都会调用拷贝构造
4.如果没有显式写出拷贝构造的函数,那么编译器会自动生成拷贝构造函数,但是这个拷贝构造只会进行浅拷贝,也就是说如果有动态开辟的数组这种成员变量,那么不会拷贝一个新的数组而是直接指向同一块内存,肯定不符合拷贝的要求,所以如果有这种成员变量,要自己写一个显式的深拷贝的构造函数
5.如果一个类内部也是自定义类型的成员变量,那么如果该成员变量自己内部已经写了正确的拷贝构造,那么该类可以直接调用成员变量的拷贝构造完成拷贝
6.传值引用返回时,如果是函数内部的临时变量进行返回,此时会产生类似野指针的情况,因为传值引用返回不会进行拷贝,所以出了函数之后,该临时变量进行销毁,对它进行引用就像是一个野指针一样,所以如果要进行传值引用返回,要确保返回的对象函数结束后仍然存在
cpp
Date(const Date& date) {
_year = date._year;
_month = date._month;
_day = date._day;
}
cpp
#include"class.h"
int main()
{
Date d1;
//d2就是对d1进行一个拷贝
Date d2(d1);
return 0;
}
7.赋值运算符重载
7.1运算符重载
当用类去使用操作符时,直接使用肯定是不可以的,但是C++可以对这些运算符进行重载,使其可用于类的运算
1.运算符重载函数的名字由operator和需要重新定义的运算符共同构成,并且具有返回值和参数列表以及函数体
2.重载运算符的参数个数和该运算符需要的运算对象数量一样多,一元操作符有一个参数,二元操作符有两个参数,对于二元操作符第一个参数在操作符左侧,第二个参数在操作符右侧
3.如果该重载函数作为成员函数,那么第一个参数会传给隐式的this指针,所以参数个数要少一个
4.运算符重载后,优先级和结合性和原来保持一致
5.不能重载语法中不存在的符号,且不能重载以下五个运算符
.* :: sizeof ?: .
6.对于一个类要重载哪些运算符,要根据重载后是否具有意义来判断
7.对于一元运算符前置++和后置++,前置++不传参,后置++在参数位置写一个int
8.对于<<和>>这两个运算符,进行重载时,如果定义为成员变量,那么使用时就是对象<<cout这样的写法了,不利于可读
例如此时我们定义了一个日期的类,以及一系列运算符重载函数
注意函数的返回类型,有些是传值引用返回,有些是传值返回, 这也是根据运算符的意义来判断的,例如+=运算符,运算完我们希望调用操作符的对象发生了改变,那么就要传值引用返回,这样才是对原对象进行改变,但是+运算符,我们希望得到加完的结果并且原对象不发生改变,那么就要用传值返回,拷贝一份临时对象进行返回
cpp
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
~Date() {
cout << "~Date()" << endl;
}
Date(const Date& date) {
_year = date._year;
_month = date._month;
_day = date._day;
}
Date& operator+=(int day);//日期的增加
Date operator+(int day);//原日期不发生改变
Date& operator-=(int day);//日期的减少
Date operator-(int day);//原日期不发生改变
Date& operator++();//前置++
Date operator++(int);//后置++
int operator-(const Date& d);//日期减日期,返回相差的天数
private:
int _year;
int _month;
int _day;
};
下面通过讲解+=函数和+函数来详细感受重载运算符的写法
我们额外创建一个文件,用于重载运算符的编写,对于+=操作符,相当于一个日期加上天数,然后返回加完天数后的日期,如果日期大的情况下,肯定会涉及月份和年份的进位,那么每个月多少天以及是否是闰年等因素就十分重要,因此我们先写一个获取某年某月天数的函数,传入年份和月份就可以返回这个月的天数
那么现在我们传入了需要加的天数,与原来的天数进行相加,然后判断它是否大于当前这个月的天数,如果大于就减去这个月的天数,并将月份加一,如果此时月份超出了12月,对年进行进位,且将月置为1月,然后继续判断,直到天数小于等于当前月的天数,返回*this也就是修改完成后的对象,这样就完成了+=的编写,注意到还写了一段传入参数小于零的时候的情况,相当于是减去day天,那么知道了+=的写法,类似的编写-=的函数,然后直接将day取反传入该函数然后进行返回,就方便很多了
然后就是+函数的编写,我们直接拷贝一份该对象,然后对拷贝完的这个临时对象进行+=操作,然后传值返回即可完成+函数的操作
cpp
#include"class.h"
int GetMonthDay(int year, int month) {
int months[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month > 12 || month < 1) {
return -1;
}
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)){
return 29;
}
return months[month];
}
Date& Date::operator+=(int day) {
if (day < 0) {
day = -day;
*this -= day;
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
if (day < 0) {
Date d = *this;
d -= day;
return d;
}
Date d1 = *this;
d1 += day;
return d1;
}
下面是剩下运算符重载函数的代码,简单讲一下-=函数的写法,此时我们先将原天数减去传入的参数,然后判断是否大于0,如果小于0说明要往前一个月移动,然后加上前一个月的天数继续判断,直到天数大于0,并且注意年份的减少即可
cpp
Date& Date::operator-=(int day) {
if (day < 0) {
day = -day;
*this += day;
return *this;
}
_day -= day;
while (_day < 0) {
if (_month > 1) {
_month--;
_day += GetMonthDay(_year, _month);
}
else if (_month == 1) {
_month = 12;
_day += GetMonthDay(_year, _month);
_year--;
}
}
return *this;
}
Date Date::operator-(int day) {
if (day < 0) {
day = -day;
Date d = *this;
d += day;
return d;
}
Date d1 = *this;
d1 -= day;
return d1;
}
Date& Date::operator++() {
return *this += 1;
}
Date Date::operator++(int) {
Date d = *this;
*this += 1;
return d;
}
7.2赋值运算符重载
赋值运算符重载函数需要传入一个当前类类型的参数,并且是引用传入,这样可以减少拷贝的行为,减少内存的损耗,并且因为不希望传入类发生改变,那么可以用const引用
还是用的Date类,对于每个成员变量依次进行拷贝,如果内部有动态开辟的成员变量,那么就需要进行深拷贝的操作,和拷贝函数中的写法差不多
cpp
Date& Date::operator=(const Date& d) {
if (*this != d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
return *this;
}
