目录:
- 前言
- 一、基础引入
- 1.类的定义
- 2.类的权限
- 3.类的封装
- 4.类的实例化
- 5.计算类对象的大小
- 结构体内存对齐规则
- 空类的大小
- 二、this指针
- this引入
- this指针的特性
- 经典例题
- 三、类的六个默认成员函数
- 1、构造 && 析构
- 构造函数
- 析构函数
- 2、拷贝 && 赋值
- 拷贝构造函数
- 赋值运算符重载
- 运算符重载
- 注意:
- 赋值
- 前置++ 后置++
- 流插入 && 流提取
- 3、取地址 && const取地址
- 补充:const成员
- 总结
前言
打怪升级:第36天 |
---|
![]() |
C语言是面向过程的语言,c++是面向对象的语言,那么什么是面向过程和面向对象呢? 下面我们以点外卖为例:
一、基础引入
C语言中有自定义类型:结构体,我们可以在结构体中定义各种变量,但是不能定义函数,在c++中我们为了将函数一起封装到对象内,将struct的功能进行了扩展,比如:
struct Date
{
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
在c++中更喜欢用 class 代替 struct,如下:
class Date
{
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
1.类的定义
class className
{
};
class:类的关键字;
className:类的名字,我们可以自行设置 ;
大括号内为类的成员,可以有变量也可以有函数 ;
注意:结尾的分号不可少 ;
类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
类的定义分两种:
1.声明和定义都放在类内
注意:函数在类内声明可能会被编译器当做内联函数。
class Date
{
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
2.声明和定义分离
注意:函数名前需要在加上“ className:: ”注明作用域。
// Date.h
class Date
{
void Print();
int _year;
int _month;
int _day;
};
// Date.cpp
#include<date.h>
void Date::Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
一般更推荐第二种,但是我们下方为了方便起见使用第一种方式。
2.类的权限
示例:
#include<iostream>
using namespace std;
struct Date1
{
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
class Date2 // 和Date1“完全相同”
{
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int _year;
int _month;
int _day;
};
void Test02()
{
//struct Date1 d1; // 在C语言中我们在声明结构体变量时必须写上 struct
Date1 d1; // c++中则允许省略,因此两种写法都对
d1._year = 2023;
d1._month = 2;
d1._day = 5;
d1.Print();
Date2 d2;
d2._year = 2023;
}
这里就涉及到权限的问题了: 类的权限
c++实现封装的方式:将类的变量和函数封装到一起,通过访问权限的限制选择性的将其接口函数提供给使用者。
访问限定符说明:
- public修饰的成员在类外也可以被访问;
- protected和private修饰的成员在类外不能直接被访问(这里两者功能相似);
- 访问限定符的作用域从设置处开始到下一个限定符为止;
- 如果下方没有其他限定符,作用域就到},既类结束;
- class默认限定符为private,struct为了兼容c默认限定符为public。
这里我们我就可以理解为何上面的访问情况会失败啦,那么若想要成功访问我们该如何设置呢?这里希望大家结合上面所讲权限的概念自行测试。
3.类的封装
面向对象分为三大特点:封装、继承和多态。
在类和对象阶段,我们主要探究类的封装特性,那么什么是封装呢?封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外提供接口来和对象进行交互。
封装本质上是对对象进行管理,好方便用户的使用。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
4.类的实例化
class Date
{
public:
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
如上所示,我们定义了一个名为Date的类,那么此时这个类占用空间了吗?
–
–
如果无法确定我们来想一想int、float、double这些类型关键字在使用之前有占用空间吗?答案肯定是没有的,
那他们在什么时候占用空间呢?答案是:
任何时候都不会占用空间,使用它们声明变量时编译器会给变量分配内存空间,但是int、Date它们是不分配空间的,它们就像一张盖房子的图纸,我们按照图纸建造的房子会占用空间,但是图纸是不会占用物理空间的。
class Date
{
public:
void Init(int year)
{
year = year;
}
private:
int year;
int month;
int day;
};
因此我们平时描述类成员变量时会将其特殊化,如下:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
}
private:
int _year; // 这样可以
int _month;
int _day;
//int year_; // 这样也可
//int month_;
//int day_;
//int mDay; // 这样也可,这里全凭大家的喜好,可以加以区分即可。
//int mYear;
//int mMonth;
};
5.计算类对象的大小
在类里面,我们会有变量也会有函数,那么我们该如何计算类对象的大小呢?
这里有三种方法:
- 一个类的大小是成员变量所占空间和成员函数所占空间的和;
这种形式下每个类对象都有自己独立的变量和函数,但是我们知道函数是可以共用的,
那么我们就不需要这样每个类对象都拷贝一份成员函数,这样会造成大量的空间浪费,我们将它单独存储起来大家都调用同一份函数即可,但是变量则不行,每个对象的成员变量必须单独存储,因此就进行了下方的优化。
- 成员变量加上指向成员函数的指针;
- 只计算成员变量,成员函数存放到公共代码段。
那么c++到底采用的哪一种呢,我们来验证一番:
由此我们知道:类的大小就是成员变量的大小总和,成员函数会存放到公共代码段,不会影响类的大小。
结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
空类的大小
class Test
{
public:
void Print()
{
}
};
class Person
{
};
void Test03()
{
cout << "Test size = " << sizeof(Test) << endl;
cout << "Person size = " << sizeof(Person) << endl;
}
由于成员函数会存放到代码段,因此有无成员函数,对类对象的大小没有影响,那么我们怎么得到了两个1呢?
上面我们讲:使用类实例化对象是需要开辟空间的,而空类没有成员函数并不占用空间,但是我们还需要表明我们实例化出了一个对象,所以这个1字节的空间内并不存储数据,只是为了占位,表明我们实例化出了一个对象。
二、this指针
this引入
我们先来定义一个类
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;
};
void Test01()
{
Date d1;
d1.Init(2023, 2, 5);
d1.Print();
Date d2;
d2.Init(2025, 1, 1);
d2.Print();
}
其实是我们c++的祖师爷认为每次调用成员函数都需要传指针实在太麻烦,指针使用不方便,而且稍不留神就会出错,所以就让编译器自己去获取对象的地址,自己进行传参和操作;
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性
- this指针的类型为 类名 * const ,因此我们无法改变this指针的内容;
- this指针只能在成员函数内部使用(因为调用成员函数时才会自动传参);
- this指针本质上是成员函数的形参,当对象调用成员函数时将对象的地址作为实参传递给this形参,所以对象中不存储this指针;
- this指针是成员函数第一个隐含的指针形参,一般由编译器通过ecx寄存器自动传递,不需要用户进行传递。
我们在成员函数中使用成员变量时可以直接使用this指针,不过在熟练掌握之后更推荐省略不写。
补充:
class Person
{
public:
void Init(int val)
{
val = val; // 我们之前讲:如果成员变量和形参命名相同,我们就无法完成对成员变量的赋值。
}
private:
int val;
};
// 现在我们做以下修改
class Person
{
public:
void Init(int val)
{
this->val = val; // 显式使用this指针后,编译器就可以区分成员变量和形参;当然,我们还是更推荐使用不同的变量名
}
private:
int val;
};
经典例题
提示:总计三道例题,每题的代码下方就是答案,想要进行思考的朋友看过了代码后不要着急往下翻哦。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Person
{
public:
void Init(int val)
{
_val = val;
}
void Print()
{
cout << this << endl;
cout << "Person::Print" << endl;
}
private:
int _val;
};
void Test02()
{
Person* pd;
pd->Print();
}
失败原因:pd指针未进行初始化,发生野指针访问。
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Person
{
public:
void Init(int val)
{
_val = val;
}
void Print()
{
cout << this << endl;
cout << "Person::Print" << endl;
}
private:
int _val;
};
void Test02()
{
Person* pd = nullptr;
pd->Print();
}
这里我们就会有疑问了:pd是一个空指针哎,我们访问成员函数不应该使用类对象吗?
我们话转上文:“类的成员函数存放在公共代码段。” 也就是说成员函数并不在对象里,我们访问成员函数不需要实例化出对象,
那么我们在使用空指针调用成员函数是在代码段中进行查找,找得到,也就可以编译链接成功。
// 3.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class Person
{
public:
void Init(int val)
{
_val = val;
}
void Print()
{
cout << this << endl;
cout << "Person::Print" << endl;
}
private:
int _val;
};
void Test02()
{
Person* pd = nullptr;
pd->Init(2023);
}
这个和前一个的唯一区别就是调用的函数不同,但是为何结果也不相同了?
编译成功我们上面讲过:成员函数存在于代码段,我们使用空指针也可以访问成员函数;
那么为何程序会崩溃?
我们看一下Init函数的操作:给成员变量赋值,
那成员变量放在哪儿 – 在对象里,我们访问成员变量是需要实例化出来对象的;
那么我们就可以理解程序崩溃的原因了:这里访问成员变量,发生了空指针的解引用。
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在
类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。
而且每个方法不需要传递对象指针,编译器编译之后该参数会自动还原。
三、类的六个默认成员函数
下面,我们将来见识一下类中的 六位“天选之子” – 六个默认成员函数。
所谓默认成员函数就是当程序员自己不写的话编译器就自动帮他写,
也就是说:任何一个类里面都必须有对应的这六个函数,即使是一个空类(没有添加任何东西)也不例外; 其中前四个非常重要,后两个我们了解即可。
1、构造 && 析构
构造函数
我们以前使用栈和队列的时候每次开始都需要进行初始化,并且在使用结束时需要释放空间,防止内存泄漏。
如下示例:
typedef int STDataType;
class Stack
{
public:
// 初始化栈
void Init()
{
_a = (STDataType*)malloc(sizeof(STDataType) * 4);
_top = 0;
_capacity = 4;
cout << "初始化成功." << endl;
}
// ...
// 销毁栈
void Destroy()
{
free(_a);
_a = NULL;
_top = _capacity = 0;
cout << "栈销毁成功." << endl;
}
private:
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
};
int main()
{
Stack s;
s.Init();
// ...
s.Destroy();
return 0;
}
运行实例:
初始化还好说,如果不初始化程序会报错,但是销毁的操作很多时候都会被遗忘,而且此时编译器也不会有任何反应,
因此很多时候会被大家忽略掉,但是在做大型项目的时候,一个程序需要跑一个月甚至更久,刚开始不会有什么影响,
可是随着时间的推移,内存泄漏的问题会越来越严重,主机的内存越来越小直至崩溃,而且这种情况下并不容易发现问题。
而我们c++的祖师爷也“受够了”这种情况,想到:既然每次都需要进行初始化和销毁,那干脆就像this指针一样,将把这个工作交给编译器来做,这样不仅使用起来更方便,还可以杜绝忘记调用的问题,
因此,就有了我们的构造和析构函数。
构造函数是特殊的成员函数,值得注意的是,构造函数虽然名字是构造,但是并不是开辟空间创造对象,而是初始化对象。
特征如下:
- 函数名和类名相同;
- 没有返回值;
- 对象实例化出来时编译器自动调用构造函数(只会调用一次);
- 可以进行重载;
示例:
class Person
{
public:
Person()
{
_year = 2023;
_month = 3;
_day = 5;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Person p;
p.Print();
return 0;
}
运行实例:
上面所写的构造函数是无参构造函数(我们也称其为默认构造函数);
我们也可以使用缺省参数
// 添加默认缺省参数
Person(int year = 2023, int month = 3, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
// 下面问大家一个问题:下方构造函数写法正确吗,如果有错误,请问错在哪里。
// 大家可以想一想之后再往下解密答案欧~
class Person
{
public:
Person()
{
_year = 2023;
_month = 3;
_day = 5;
}
Person(int year = 2023, int month = 3, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Person p;
p.Print();
return 0;
}
运行结果:
> 由此我们一般只写全缺省的构造函数,因为全缺省函数也包含了无参构造。
- 如果类中没有显示定义构造函数,编译器会自动生成一个无参的默认构造函数,如果类中定义了,编译器将不再定义。
注意:用户定义了任何一个构造函数,编译器都不会再自动生成。
class Person
{
public:
Person(int year, int month = 3, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Person p;
p.Print();
return 0;
}
结果:
- 编译器自动生成无参的默认构造函数。
这里有同学就会有疑问了,既然我们不写,编译器就会自动生成对应的默认构造函数,那我们还需要“多此一举”自行实现吗?
下面我们来看一下编译器自行实现的构造函数到底有什么作用吧。
上面讲到:对于自定义类型会自动调用它的构造函数,那么我们就来验证一下:
typedef int STDataType;
class Stack
{
public:
// 初始化栈
Stack(int s = 4)
{
_a = (STDataType*)calloc(s, sizeof(STDataType));
_top = 0;
_capacity = s;
cout << "初始化成功." << endl;
}
// ...
// 销毁栈
~Stack()
{
free(_a);
_a = NULL;
_top = _capacity = 0;
cout << "栈销毁成功." << endl;
}
private:
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
};
class PPP
{
private:
Stack spush;
Stack spop;
};
void Test01()
{
PPP p;
// ...
}
补充:上面我们说了,析构函数对于内置函数类型不进行操作,这个本身就是不合理的,因此,在c++11中对它进行了一些补充:
内置类型成员变量在类中声明时可以给默认值,注意,这里是缺省值而不是初始化或者赋值,思考一下为什么?
- 无参的构造函数和全缺省的构造函数都可以称为默认构造函数,并且默认构造函数只能存在一个。
无参构造函数,全缺省的构造函数,以及编译器自动实现的构造函数都可以叫默认构造函数(不需要传参数的构造函数)。
析构函数
下面我们来了解一下析构函数,析构函数是和构造函数配套使用的,一个初始化一个销毁,当然,这个销毁并不是指对对象本身的销毁,
局部对象的销毁是由编译器来做的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
- 函数名为: ~类名;
- 无参数无返回值;
- 在对象销毁时自动调用;
- 一个类只能有一个析构函数,如果程序员不写编译器会自行实现(析构函数无重载);
- 编译器自行生成的析构函数对内置类型成员变量不做处理,对于自定义类型成员变量会调用它的析构函数。(和构造函数一模一样滴)
- 如果类中没有资源申请是可以直接使用编译器自动生成的析构函数,如Date类,
有资源申请时,一定要写,否则会造成内存泄漏,如Stack类。
2、拷贝 && 赋值
拷贝构造函数
拷贝拷贝,顾名思义,就是把一个变量再复制一份,我们这里说的拷贝是指:使用一个对象初始化一个新的对象,
新的对象就是另一个对象的“拷贝”。
拷贝构造函数是构造函数的一个重载,参数只有一个:就是相同类型的对象(这里有坑!!)
下面我们来模拟实现一下:
拷贝构造函数:
- 拷贝构造是构造函数的一个重载形式;
- 拷贝构造的参数只能是同类型对象的引用,采用值传递编译器直接报错,会发生无穷递归调用。
- 如果我们不写,编译器会默认生成拷贝构造函数,但是要注意,编译器自行提供的拷贝构造是按字节进行拷贝的,我们称之为浅拷贝或者值拷贝。
下面我们来见识一下:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 2023;
int _month = 3;
int _day = 6;;
};
void Test02()
{
Date d1;
Date d2(d1);
Date d3 = d1; // 这样写也是拷贝构造
d1.Print();
d2.Print();
d3.Print();
}
可见,我们这里使用编译器自行提供的拷贝构造函数是没有问题滴,
那么,下面我们肯定就要来看一个有问题的代码啦:
typedef int STDataType;
class Stack
{
public:
// 初始化栈
Stack(int s = 4)
{
_a = (STDataType*)calloc(s, sizeof(STDataType));
_top = 0;
_capacity = s;
cout << "初始化成功." << endl;
}
// ...
// 销毁栈
~Stack()
{
free(_a);
_a = NULL;
_top = _capacity = 0;
cout << "栈销毁成功." << endl;
}
private:
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
};
void Test01()
{
Stack s1;
Stack s2(s1);
}
欧~,这里又是怎么一回事儿呢,
让我们根据浅拷贝的定义来画图理解:
由此可见,当我们的类中有资源申请的时候就需要程序员自己来写拷贝构造函数,来防止浅拷贝问题(值拷贝),而程序员自我实现的拷贝构造函数我们称为深拷贝。
- 在编译器自动生成的默认拷贝构造函数中,对于内置类型变量会进行值拷贝,对于自定义类型变量会调用它的拷贝构造函数。
- 拷贝构造函数调用场景:
①使用类对象创建新对象;
②类对象作为函数参数;
③类对象作为函数返回值;
注:如果采用传引用传参或者引用返回就不需要调用拷贝构造(因为实际传的是指针),但是,
并不是所以情况都可以传引用(例如要返回的是局部变量)。
赋值运算符重载
下面,在介绍赋值成员函数之前我们先来了解一下c++中十分十分十分重要的一个知识:运算符重载!!
运算符重载
c++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,它也具有返回值、函数名以及参数列表,
其返回值和参数列表和普通函数类型。
- 关键字:operator;
- 功能:赋予运算符新的功能;
- 函数名: operator后跟需要重载的运算符
- 用法:返回值 operator运算符 (参数);
示例,重载赋值运算符:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
// void operator=(Date d) // 这里可以采用值传递吗,
// 可以,因为采用值传递的话会多走一步拷贝构造,和赋值没有关系。
void operator=(const Date& d) // 推荐使用引用,因为走一步拷贝构造完全没必要; 也推荐加const,防止因为疏忽,把d的值给改了
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 2023;
int _month = 3;
int _day = 6;;
};
void Test02()
{
Date d1;
Date d2(d1);
Date d3 = d1; // 这样写也是拷贝构造
Date d4;
d4 = d1; // 这样写是赋值
d1.Print();
d2.Print();
d3.Print();
d4.Print();
}
好的,那下面我们就又有新的疑问了:为什么需要重载赋值操作符,编译器都已经帮我们写好了,我们是不是就没有必要再来了解了?
答案嘛,肯定是否定的, 因为,编译器给我们提供的重载赋值操作符只是简单的值拷贝,并不能满足用户的需求,
并且,编译器只提供了一个重载赋值操作符,如果我们想要比较一下两个日期的大小,计算一下两个日期之间有多少天,简单地使用
大于、小于号或者减号是不对的,因为编译器并不知道我们到底是想要只比较年份还是只比较月份等等,
因此,这些都需要程序员自行实现,告诉编译器应当如何做。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@ ;
- 重载操作符必须有一个类类型参数;
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义;
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
看一下相等运算符重载的类内和类外实现:
这里大家可能会注意到:类外实现的operator==函数报了好多错误(红色波浪线),
这是因为我们的成员变量设置的权限为private,私有变量不可以在类外使用,解决方法除了把函数放到类内实现,也可以提供一个获取类内成员变量的函数接口,更甚者可以直接把权限改为public(上面博主我为了方便就这样写了,但是以后工作中如果胆敢这样会让我们的数据安全性降低,在公司里可能会吃的。 )
赋值
下面我们就继续往后讲解
才怪,
下面我们回到赋值运算符重载,
赋值嘛,我们上面就写过了不是,为什么还要再写一次呢,
欧·吼, 那当然是因为上面写的不够好喽。
赋值运算符重载的格式:
-
参数类型:const T& ,使用引用提高传参效率,加const防止修改数据;
-
返回值类型:T&,使用运用提高返回效率,添加返回值可以实现连续赋值;
-
检测是否自己给自己赋值;
-
返回 *this ,要符合连续赋值的含义。
(喏,我们上面写的只满足第一条的)。
(不要猜了,这个时间是熊猫的生日) -
赋值运算符只能重载为类的成员函数,不能重载为全局函数。
原因:赋值运算符重载是类的默认成员函数,如果编译器在类中没有找到赋值运算符重载,就会提供默认的,
结果就是:和我们写在全局的发生重载冲突。
有同学就会有疑问了,为什么前面的构造、析构和拷贝构造没有提这一点?
因为,这三个函数更加特殊,他们是没有返回值的,所以根本就不可能会写在类外。
那么有同学就又有其他疑问了,赋值运算符重载只能重载为类的成员函数,那么我们上面写的判断相等的重载为什么既可以又可以呢?
因为,只有赋值运算符重载是默认成员函数,如果没编译器会自行提供,而其他运算符重载如果程序员不写,就真的没有。
- 程序员没有显示提供时,编译器会提供一个默认赋值运算符重载,以值的方式逐字节拷贝(这里肯定开始会有出现问题的,反应快速的同学应该已经联想到了拷贝构造的深浅拷贝)。
因此此处也要注意:如果类中未涉及资源管理,是否自行实现都可以;而一旦涉及了资源管理就必须要实现。
前置++ 后置++
Date& operator++() // 前置++
{
// 前置++是先加1后使用,因此返回值可以直接返回 *this,返回类型为 Date&
++_day;
return *this;
}
// 前置++ 和后置++ 都是一元运算符,因此无法对它们进行区分
// c++规定:后置++重载时多传递一个 int型参数,但在函数调用时该参数不需要传递,编译器会自行设置
// 注意:后置++是先使用后++,因此返回值应该为++前的数,因此需要创建一个临时变量保存++前的数据
// 由于返回的是局部变量,那么返回时就不能返回引用,而要传值返回。
Date operator++(int)
{
Date tmp = *this;
++_day;
return tmp;
}
这就结束了嘛,没错,当然没有,
上面我们直接进行++,可是如果天数大于31,我们依然是直接进行天数+1吗,
那这个时候就需要进行月份进位了。
改进:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
// 判断该月份有多少天
int GetMonthDay()
{
int arr[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (_month == 2
&& ((_year % 4 == 0 && _year % 100 != 0) || _year % 400 == 0))
++arr[2];
return arr[_month];
}
Date& operator++() // 前置++
{
++_day;
if (GetMonthDay() < _day)
{
_day -= GetMonthDay();
++_month;
if(_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date operator++(int)
{
Date tmp = *this;
++*this; // 欧吼,复用了上面刚刚写的前置++
return tmp;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test02()
{
Date d1;
Date d2 = d1++;
Date d3 = ++d1;
d1.Print();
d2.Print();
d3.Print();
}
Perfect。
流插入 && 流提取
现在我们要来了解一下我们经常用到,却总是稀里糊涂地就使用起来了的两个运算符。
在C语言中我们输入输出都是通过 scanf 和 printf 两个输入输出函数,例如使用printf进行输出是我们需要表明参数的类型
int a, char str[20]; printf("%d %s\n", a, str);
但在c++中,所有内置类型都可以直接输入输出,例如cout << a << str << endl;
之前我们只知道这是c++的一个优点,使用起来比C语言要方便很多很多,那么我要问一问大家:cin、cout这么方便的原因到底是什么呢?
补充: >> 和 << 都是运算符, >> 是右移运算符,<< 是左移运算符。
那么,既然它们两个是移位操作符,那么和输入输出又有什么关系呢?
答案就是:如果仅仅是作为移位操作符使用那当然是毫无关系的,
但是,我们上面刚刚看过了运算符重载,我们是不是就可以赋予移位运算符新的意义呢?
步入正题:
**>>**在c++中称为流提取运算符(输入,从输入设备读取数据)
<< 在c++中称为流插入运算符(输出,将内容输出到输出设备)
cin是istream类对象,cout为ostream类对象,(i、ostream类型都包含在 < iostream >头文件中)
而流插入和流1提取都是通过运算符重载来达到输入输出目地的。
而对于自定义类型就需要程序员自行重载,下面我们进行尝试:
#include<iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void operator<<(ostream out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 2023;
int _month = 3;
int _day = 8;
};
void Test02()
{
Date d1;
cout << d1; // 错误示范
}
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
// friend ostream& operator<<(ostream& out, const Date& d); // 友元声明可以写在类内任何位置,一般写在最上面,避免和成员函数声明混淆
private:
int _year = 2023;
int _month = 3;
int _day = 8;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
void Test02()
{
Date d1;
//cout << d1;
//d1 << cout; // 成员函数使用形式
cout << d1 << endl << d1 << endl; // 全局函数使用形式
}
3、取地址 && const取地址
他来了他来了,他朝着终点走来了!!!
下面来到取地址操作符重载环节,掌声有请重载函数登场~~~
注意:编译器帮我们传过去的this指针可不就是该对象的地址嘛,所以直接返回this就好。
上面我们看到,输出d1和d2的地址的时候使用的是普通对象取地址,但是输出d3的地址的时候需要使用const对象取地址,不就是取个地址嘛为什么还要分是不是const对象,而且,const取地址重载里有一个 const 看起来好像有些 “不合群” 呐,孤零零在外面。
注:如果声明和定义分离,两边都需要加 const。
代码如下:
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year = 2023, int month = 3, int day = 8)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
这里的取地址重载基本上不需要我们自行实现,除非你不想让对方获取你的地址,比如:
补充:const成员
const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,
表明在该成员函数中不能对类的任何成员进行修改。
思考下面的几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
回答:
- const对象不可调用非const成员函数 – 权限扩大;
- 非const对象可以调用const成员函数 – 权限缩小;
- const成员函数中不可调用其它的非const成员函数 – 权限扩大 – const成员函数中的this指针为 const类型,调用非const成员函数权限或扩大;
- 非const成员函数可以调用其它的const成员函数 – 权限缩小。
总结
经过不知道多少多少天的努力,终于把类与对象的文章肝出来了啊!!,
本文介绍了类与对象的大部分内容,剩余还有一些细枝末节的知识熊猫会在下一篇文章中进行总结,
下面,对本文所讲内容进行回顾:
1. 类的权限 :public、protected、private;
2. 类的封装、声明、实例化;
3. 类大小的计算,内存对齐;
4. this指针;
5. 构造函数的七个特点、析构函数的六个特点;
6. 深浅拷贝、赋值;
7. 运算符重载:前置++和后置++,流插入和流提取运算符重载;
8. 取地址运算符重载;
9. const成员函数。
以上就是今天c++类与对象的全部内容,如果有什么疑问或者建议都可以在评论区留言,感谢大家对的支持。