【c++类与对象 】
目录:
- 前言
- 一、基础引入
- 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语言中我们在声明结构体变量时必须写上 structDate1 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++类与对象的全部内容,如果有什么疑问或者建议都可以在评论区留言,感谢大家对的支持。
相关文章:

【c++类与对象 】
目录:前言一、基础引入1.类的定义2.类的权限3.类的封装4.类的实例化5.计算类对象的大小结构体内存对齐规则空类的大小二、this指针this引入this指针的特性经典例题三、类的六个默认成员函数1、构造 && 析构构造函数析构函数2、拷贝 && 赋值拷贝构造…...

【C++】内联函数auto范围for循环nullptr
🏖️作者:malloc不出对象 ⛺专栏:C的学习之路 👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈 目录前言一、内联函数1.1 内联函数概念1.2…...

运维效率狂飙,都在告警管理上
随着数字化进程的加速,企业IT设备和系统越来越多,告警和流程中断风险也随之增加。每套系统和工具发出的警报,听起来像是一场喧嚣的聚会,各自谈论不同的话题。更糟糕的是,安全和运维团队正在逐渐丧失对告警的敏感度&…...

【每日随笔】中国当前社会阶层 ( 技术无关 | 随便写写 )
文章目录一、阶层划分根据收入划分的阶层根据分工逻辑划分根据权利划分二、根据社会地位和掌握的资源划分的阶层三、赚钱的方式四、如何进入高阶层看了一个有意思的视频 , 讲的是中国当前的社会阶层 , 感觉好有道理 , 搜索了一些资料 ; 参考资料 : 关于中国的社会阶层社会在分…...

【13种css选择器】学css选择器,这一篇就够了
举例形象让你学会,不搞官方话css所有的选择器相邻兄弟选择器后续兄弟选择器后代选择器子代选择器并集选择器(多重选择器)属性选择器伪类选择器伪元素选择器class选择器(类选择器)id选择器*选择器(通配符选择器)标签选择…...

1-1 微服务架构概述
文章目录微服务架构概述1-1. 系统进化理论概述集中式系统:分布式系统1-2. 系统进化理论背景1-3. 什么是微服务架构1-4. 微服务架构的优缺点1-5. 为什么选择 Spring Cloud 构建微服务认识 Spring Cloud2-1. Spring Cloud 是什么2-2. Spring Cloud 的版本2-3 Spring C…...

uniapp传参
//子传父子页面:sumbit() {console.log(this.formData, 传过去的内容对象)let pages getCurrentPages();let prevPage pages[pages.length - 2]; //上一个页面prevPage.$vm.getParams(this.formData); //重点$vmuni.navigateBack();},父页面接收:metho…...

面试官:说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?
一、是什么 函数是 JavaScript 应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块 在 TypeScript 里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScript 为 JavaScript 函数添加了额外的功能…...

【测试】HD-G2L-IO评估板测试结果表
1. 测试对象HD-G2L-IOT基于HD-G2L-CORE V2.0工业级核心板设计,双路千兆网口、双路CAN-bus、2路RS-232、2路RS-485、DSI、LCD、4G/5G、WiFi、CSI摄像头接口等,接口丰富,适用于工业现场应用需求,亦方便用户评估核心板及CPU的性能。H…...

[2.2.1]进程管理——调度的概念、层次
文章目录第二章 进程管理调度的概念、层次(一)调度的基本概念(二)调度的三个层次(1)高级调度(2)低级调度(3)中级调度补充知识:进程的挂起态与七状…...

【JavaScript UI库和框架】上海道宁与Webix为您提供用于跨平台Web应用程序开发的JS框架及UI小部件
Webix是Javascript库 一种软件产品 用于加速Web开发的 JavaScript UI库和框架 Webix用于跨平台Web应用程序开发的JS框架,为您提供102个UI小部件和功能丰富的CSS/HTML5 JavaScript控件 开发商介绍 Webix团队由由热衷于创建高质量网络产品的专业人士组成ÿ…...

【微信小程序】-- WXS 脚本(二十九)
💌 所属专栏:【微信小程序开发教程】 😀 作 者:我是夜阑的狗🐶 🚀 个人简介:一个正在努力学技术的CV工程师,专注基础和实战分享 ,欢迎咨询! &…...

案例19-遇见问题的临时解决方案和最终解决方案
目录1、背景介绍2、两种解决方案的概念1、临时解决方案:2、最终解决方案:3、排查问题过程4、总结站在用户的角度思考作为软件开发者5、升华1、背景介绍 首先说明这是系统很早之前的时候的一个功能,当时和学习通还有很强的耦合关系。在学习通…...

自指(Self-reference)
文章目录1. 在逻辑、数学和计算方面2. 在生物学中3. 在艺术4. 在语言中5. 在流行文化中6. 在法律中自我参照(Self-reference)是一个涉及指代自己或自己的属性、特征或行为的概念。它可以发生在语言、逻辑、数学、哲学和其他领域。 在自然语言或形式语言…...

关于Hanoi塔的实现
关于Hanoi塔的实现 首先,在此之前,我们需要了解一下递归这个东西; 在我看来,递归这个东西就是栈的进出; 向下:进栈回溯:出栈 在进栈之前标记状态,输入到栈中; #incl…...

原始套接字(Raw Socket)
原始套接字允许对较低层次的协议进行访问,如: IP协议,ICMP协议等一般用于自定义协议的实现,处理IP协议没有处理过的数据运输层下IP数据不关注内核是否已有注册的句柄来处理这些数据,都会将这些IP数据复制一份传递给与协议类型匹配的原始套接字,没有的话,直接丢弃该数据,并返回主…...

SparkSQL与Hive交互
SparkSQL与Hive交互一、内嵌Hive应用二、外部Hive应用三、运行Spark SQL CLI四、IDEA操作外部HiveSparkSQL可以采用内嵌Hive,也可以采用外部Hive。企业开发中,通常采用外部Hive。 一、内嵌Hive应用 内嵌Hive,元数据存储在Derby数据库。 &am…...

「题解」日常遇到指针面试题
🐶博主主页:ᰔᩚ. 一怀明月ꦿ ❤️🔥专栏系列:线性代数,C初学者入门训练,题解C,C的使用文章 🔥座右铭:“不要等到什么都没有了,才下定决心去做” …...

实习生JAVA知识总结目录
一.JAVA基础学习 JAVA知识点全面总结1:零散知识 JAVA知识点全面总结2:面向对象 JAVA知识点全面总结3:String类的学习 JAVA知识点全面总结4:异常类学习 JAVA知识点全面总结5:IO流的学习 JAVA知识点全面总结6&…...

GMPC认证有哪些内容?
【GMPC认证有哪些内容?】GMP(GMP Good Manufacturing Practice)即良好生产规范,最早是美国国会为了规范药品生产而于1963年颁布的。这也是世界上第一部GMP。由于GMP在规范药品的生产,提高药品的质量,保证药品的安全方面效果非常明显…...

D2-Net: A Trainable CNN for Joint Description and Detection of Local Features精读
开源代码:D2-Net 1 摘要 在这项工作中,我们解决了在困难的成像条件下寻找可靠的像素级对应的问题。我们提出了一种由单一卷积神经网络发挥双重作用的方法:它同时是一个密集的特征描述符和一个特征检测器。通过将检测推迟到后期阶段…...

Java基础面试题
目录 一,Java基础 1.1.JDK和JRE有什么区别? 1.2.JAVA中的几种基本类型,各占用多少字节? 1.3.和equals的区别是什么? 1.4.final,finally,finalied有什么区别? 1.15.Java 中操作字符串都有哪些类?它们…...

SQL和MongoDB对比
关系型数据库如MySQL和非关系型数据库MongoDB的对应关系:SQLMongoDBdatabasedatabasetablecollectionrowdocument or Bson documentcolumnfieldindexindextable joins$lookupprimary keyprimary key指定任何唯一的列或列组合作为主键主键会自动设置为_id字段aggrega…...

研究链表空间销毁问题
💯💯💯 1.研究链表空间销毁问题 当链表使用完后,需要将链表销毁,那么该如何销毁呢? void SLTDestroy(SLTNode* phead)//销毁单链表 {SLTNode* cur phead;while(cur){free(cur);cur cur->next;} }你…...

Linux面试总结
一.常用命令1.目录切换cd / 切换到根目录cd ../ 切换到上级目录cd ~ 切换到home目录2.查看目录ls 列出当前目录下所有的文件ls [路径]ls / 查看根目录 ls -l 相当于 ll 最常用的命令,用了表的方式列出当前目录的内容3.查看当前目录pwd-4.创建一组空文件touch5.显示文件内容cat6…...

anaconda的linux版本以及jupyter的安装和DataSpell连接linux的jupyter服务器
anaconda安装:官网:https://www.anaconda.com/拷贝下载网址后,在Linux里进行下载:wget https://repo.anaconda.com/archive/Anaconda3-2022.10-Linux-x86_64.sh执行sh:./Anaconda3-2022.10-Linux-x86_64.sh 安装完后&a…...

Zookeeper集群和Hadoop集群安装(保姆级教程)
1. HA HA(Heigh Available)高可用 解决单点故障,保证企业服务 7*24 小时不宕机单点故障:某个节点宕机导致整个集群的宕机 Hadoop 的 HA NameNode 存在单点故障的可能,需要配置 HA 解决引入第二个 NameNode 作为备份同…...

利用matlab的newff构建BP神经网络来实现数据的逼近和拟合
假设P是原始数据向量; T是对应的目标向量; 现在需要通过神经网络来实现P->T的非线性映射。 net newff(minmax(P),[16,1],{tansig,purelin},trainlm); net.trainParam.epochs 2000; net.trainParam.goal 1e-5; net init(net); net train(n…...

【经验分享】电路板上电就挂?新手工程师该怎么检查PCB?
小伙伴们有没有经历过辛辛苦苦,加班加点设计的PCB,终于搞定下单制板。接下来焦急并且忐忑地等待PCB板到货,焊接,验证,一上电,结果直接挂了... 连忙赶紧排查,找问题。最终发现,是打过…...

运筹系列68:TSP问题Held-Karp下界的julia实现
1. 介绍 Held-Karp下界基于1tree下界,但是增加了点权重,如下图 通过梯度下降的方法找到最优的π\piπ。 这里用到的1tree有下面几种: 全部点用来生成最小生成树,再加上所有叶子结点第二短的边中数值最大的那个任意选一个点&…...