前言:哈喽小伙伴们,从这篇文章开始,我们将正式进入C++的学习,C++真正的大菜现在才刚刚开始,紧随博主的脚步,让我们一起拿下C++类与对象!
目录
一.面向过程与面向对象
二.类
1.类的定义
2.类的访问限定符及封装
3.this指针
三.类的六个默认成员函数
1.构造函数
2.析构函数
3.拷贝构造函数
4.赋值运算符重载函数
总结
我们知道,C语言是一款面向过程的编程语言,关注的是过程,分析出求解问题的步骤,通过函数,来逐步解析出结果。
而C++则是一款基于面向对象的编程语言,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
在C语言中,我们通过struct在结构体中只能定义变量。但是在C++中,结构体被升级了,不仅可以定义变量,还可以定义函数,比如说我们定义一个学生结构体:
struct student
{
int age;
int Snumber;
void SInformation(int age, int Snumber)
{
//..
}
};
其中有变量age和Snumber,还可以有函数SInformation()。
在C++中,我们仍然可以通过struct来创建类,但是创建类有更官方的关键字class。
class 类名
{
//类体
};
类体中的内容被称为类的成员,其中的变量称为类的属性或成员属性,函数被称为类的方法或成员函数。
比如说,我们现在来定义一个日期类:
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
在C++中,我们推荐在成员变量名前边加下划线‘_’,这样方便我们在函数中进行访问的区分。
由类定义的变量成为类的对象,定义类的对象时,可以直接使用类名定义而无需添加class:
Date d;
class Date d;(同样可用,但不推荐)
其中d即为Date类的一个对象,创建对象的过程,又称为对象实例化。
(对象后边会重点讲,这里仅起到帮助了解类的作用)
在C++的类中,有三种访问限定符:
public(公有)
protected(保护)
private(私有)
由public修饰的成员在类外也可以被直接访问。
而由protected和private修饰的成员在类外不能被直接访问。(前期暂时认为两者功能一致)
访问权限的作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
class默认的访问权限为private,而struct的默认访问权限为public。
当类的成员的访问权限为私有时,我们是不能使用其内部的函数等功能的:
所以想要访问到这些函数,我们就需要解开它的权限:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 1, 28);
d1._day++;
d1.Print();
return 0;
}
注意public的位置在类的开头,所以我们类中的所有成员都变成了公有,我们便可以调用其中的成员函数,以及修改其成员变量,结果如下:
在类中,我们也可以实现函数的声明与定义分离,但在函数的定义时,其函数名前必须加类名:
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
我们知道,面向对象的编程方式都有三大特性:封装,继承和多态。
在C++中,我们所创建的数据和方法基本都是存放在类的里边,但是我们有时候是不希望这些数据和方法暴露出来能够被任何人使用,所以我们需要将它们封装起来。
实际上,上述访问限定符protected和private的作用就是将重要的数据和方法封装起来。
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
看到这个代码时,不知道小伙伴们有没有疑惑,这个Print函数明明没有year这三个变量的形参,又为何能够打印出这三个变量呢???
实际上,在类的每个成员函数中,都隐藏着一个形参,也就是this指针,这个指针会在编译时由寄存器自动传递,并不需要我们来使用它。
当对象调用成员函数时,会将对象的地址作为实参传递给this形参。
这样看来,上述Print函数实际上的写法为:
void Print(Date* this)
{
cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
}
何为默认成员函数???它的意思同上述的this指针类似,就是虽然在类的里边我们没有去定义使用,但是它依然隐含着存在并发挥作用,那么这些成员函数都是什么呢???
- 构造函数:完成初始化工作
- 析构函数:完成清理工作
- 拷贝构造函数:使用同类对象初始化创建对象
- 赋值运算符重载函数:把一个对象赋值给另一个对象
- 普通对象取地址重载函数
- const对象取地址重载函数
其中后两种函数因为不经常用到,所以我们没必要知道它们的具体构造,下面我们就来一一解析其余的四个函数。
构造函数是特殊的成员函数,虽然名为构造,但其实际作用确实初始化对象。
构造函数有以下特征:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
下面我们就拿日期类的初始化为例来进行讲解:
#include
using namespace std;
class Date
{
public:
Date()
{
_year = 2;
_month = 2;
_day = 2;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 28);
Date d2;
d1.Print();
d2.Print();
return 0;
}
在上述代码中,Date函数即为构造函数,它不需要返回值,构造函数可以没有参数,但必须在函数内进行初始化,也可以重载,成为带参函数,在函数外传值初始化。
实例化的两个对象d1和d2,d1因为传入了实参,所以它会调用含参构造函数,而d2没有传参,所以它调用无参的构造函数。如此看来,我们并没有去调用构造函数却能实现初始化,都是因为编译器已经帮我们做了。
结果如下:
如果我们在类中没有主动实现构造函数,编译器就会自动生成一个无参的默认构造函数。但是一旦我们主动定义了,就会将其取代。
构造函数也可以和缺省函数联动,全缺省的构造函数和无参构造函数均视为默认构造函数。
前边我们通过构造函数,完成了对象的初始化操作,那么当对象进行各种操作之后程序即将结束时,对象又是如何被销毁呢???
析构函数,拥有与构造函数相反的功能,它的作用就是清理对象各种操作后留下的资源。
要注意,析构函数并不是销毁对象本身,只是起到清理功能,对象的创建和销毁由编译器完成。
析构函数有以下特征:
- 析构函数名是在类名前加“~”,也就是C语言中的按位取反
- 析构函数无参数(不能重载),无返回值
- 一个类只能有一个析构函数,如果我们没有自己实现,就会默认生成一个
- 对象生命周期结束时,编译系统会自动调用析构函数
#include
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
~Date()
{
cout << "xiaohui" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 28);
d1.Print();
return 0;
}
来看这段代码,我们自己定义了析构函数,但是并没有主动去调用它,来看结果:
能够看出,析构函数会被编译器自动调用。
顾名思义,拷贝构造函数的本质还是一个构造函数,它的作用是使新创建的对象能够具有和某个已经存在的对象相同的特性。
拷贝构造函数有以下特征:
- 只有单个形参,该形参是对本类中要拷贝的对象的引用
- 是构造函数的一种重载形式
- 在已存在的类类型对象创建新对象时由编译器自动调用
#include
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 28);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
要用d2来拷贝d1的数据,就需要将d1作为实参传入,再看上边的第二个Date函数,其形参必须为引用。
此外,因为我们是要复制别人的信息,所以为了保证数据的安全性,我们推荐在形参前加上const关键字修饰起到保护作用。
当我们将上述的拷贝构造函数给删除之后,再运行我们的程序:
能够看出我们的拷贝仍然可以,这是因为类内部的默认拷贝构造函数起作用了。
默认的拷贝构造函数对象会按内存存储的字节序完成拷贝,这种拷贝叫做浅拷贝,或值拷贝。
那么既然有浅拷贝,就必然会有深拷贝。
我们知道,顺序表的本质是数组,其存放数据是通过指针来进行的,现在如果要将顺序表进行拷贝,那么结果就会是让一个新的指针去指向原有的顺序表的空间。这样就会有两个指针指向同一块空间。
如果这两个顺序表销毁,通过其中一个指针销毁完之后,就会导致另一个指针所指向的空间消失,从而成为野指针,这是绝对不允许的。
所以当我们实现数据结构这种,有开辟到空间并需要销毁的拷贝时,就需要使用深拷贝。
而深拷贝,其实就是在为拷贝者另开一片空间,通过memcpy来进行拷贝:
class SqList
{
public:
SqList(const SqList& s)
{
int* tmp = (int*)malloc(sizeof(int) * s._capacity);
if (tmp == nullptr)
{
perror("malloc");
exit(-1);
}
memcpy(tmp, s._arr, sizeof(int) * s._size);
_arr = tmp;
_size = s._size;
_capacity = s._capacity;
}
int* _arr;
int _size;
int _capacity;
};
像_size和_capacity这样的变量,它们的拷贝就还是浅拷贝的模式。
C++为了加强代码的可读性,引入了运算符重载。运算符重载是具有特殊函数名的函数,具有返回值类型,函数名字以及参数列表。
说的简单一点,就是利用一个函数来代替一个运算符去完成它在正常情况下不能完成的事情。
函数形式为:返回值类型 operator运算符符号(参数)。
使用运算符重载函数要注意以下问题:
- 不能通过连接其他符号来创建新的操作符:如operator@
- 重载操作符必须有一个类类型的函数
- 用于内置类型的运算符,其含义不能改变
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 这五个运算符不能重载
编译器在正常情况下是无法直接用运算符比较两个日期的大小的,但是可以写一个函数来比较。
现在我们来实现判断一个日期是否大于另一个日期的函数:
#include
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool operator>(Date& d)
{
if (_year > d._year)
return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
return _day > d._day;
else
return false;
}
else
return false;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 28);
Date d2(2024, 2, 10);
cout << d1.operator>(d2) << endl;
cout << d2.operator>(d1) << endl;
return 0;
}
下面我们来一步步的分析:
首先我们要知道,日期的比较比较复杂,要先从年开始比较,如果年相等,再去比较月份,月份也相等的话,进而去比较日,所以我们这个函数就要是多重判断的形式。
值得注意的是,我们这个函数是写在类内部的成员函数, 因为成员函数的第一个参数都是默认隐藏的this,所以我们只用再添加一个参数即可。
而我们调用函数的方式,就是用第一个对象调用成员函数,再将第二个对象作为实参传入,这样就达到了比较的作用,当然这两个对象可以相互交换位置,会得到不同的结果:
0代表false,说明日期d1小于d2,1则代表true,说明日期d2大于d1。
现在我要修改一些代码:
cout << d1.operator>(d2) << endl;
cout << d2.operator>(d1) << endl;cout << (d1 > d2) << endl;
cout << (d2 > d1) << endl;
将上边的两行代码,修改为下边的两行,这意味着我们不去调用运算符重载函数而直接去比较,这样能成功吗???
当然可以,不要忘记,运算符重载函数也是类中的默认成员函数,所以即使我们不去主动调用它,编译器也会自动帮助我们去调用,前提是我们已经定义出了对应的运算符的重载函数:
我们知道,赋值号“=”,也是一个运算符,说起赋值,可能小伙伴们都会认为它和拷贝类似,因为它们都是将一个对象给到另一个对象。
但是对于我们这里的拷贝构造和赋值重载来说,确是截然不同。
前者是新创建一个对象去拷贝,而后者则是将一个对象赋值给另一个已经创建好的对象。
那么赋值运算符的重载函数又该这么写呢???
#include
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void operator=(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 28);
Date d2(2024, 2, 10);
d1 = d2;
d1.Print();
d2.Print();
return 0;
}
其实就和运算符重载函数的写法一样,operator后的符号为“=”,这样我们就可以实现d2对d1的赋值了:
但是这样的函数写法,只能满足两个对象之间的赋值,如果想要实现多个对象的连续赋值,赋值重载函数就必须有返回值。
Date d1(2024, 1, 28);
Date d2(2024, 2, 10);
Date d3(2024, 3, 15);
d1 = d2 = d3;
比如说我现在要实现这样的赋值操作,按照赋值从右往左的方向,首先是d3向d2赋值,再将d2赋值给d1,这意味着,d2就要作为函数的返回值,所以函数形式如下:
Date& operator=(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
因为在函数中,d2是作为this这个隐藏形参的实参,所以返回值为d2,即*this。
至于返回类型为什么是引用,是因为值返回实际上是一个拷贝的过程,会生成一个临时变量,并调用一次默认的拷贝构造函数,这样会减少效率,所以我们传引用返回。
当然,赋值运算符重载函数和其他的运算符重载函数是不一样的,因为它本身就是类的默认成员函数,所以即使不去定义它,也能够以值的方式逐字节的进行赋值操作:
关于C++类与对象的前半段知识我们就分享到这里啦。
希望能够得到小伙伴们一键三连的支持!
我们下期再见啦!