C++——类和对象[中]
0.关注博主有更多知识
C++知识合集
目录
1.类的默认成员函数
2.构造函数和析构函数基础
3.构造函数进阶
4.析构函数进阶
5.拷贝构造函数
6.运算符重载
7.日期类
7.1输入&输出&友元函数
8.赋值运算符重载
9.const成员函数
9.1日期类完整代码
10.取地址重载
1.类的默认成员函数
事实上一个空类它并不是什么都没有,它会有6个默认成员函数。默认成员函数指的是当我们没有显式的实现时编译器自动生成的成员函数。现在的任务不是去理解什么是默认成员函数,现在需要理解的是空类并不是什么都没有就够了。
以下是6个默认成员函数的示意图:
2.构造函数和析构函数基础
我们现在可以使用简单的类定义一个栈:
#include <cstdlib>
using namespace std;class Stack
{
public:void Init(int capacity = 4){_a = (int*)malloc(sizeof(int)*capacity);_top = 0;_capacity = capacity;}void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}void Destroy(){free(_a);_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};
在说明我的目的之前先介绍一个全新的头文件<cstdlib>,这个头文件实际上与C语言库的<stdlib.h>没有区别,这是C++的升级手法,C++嫌弃C语言的原生库,因为那样会带来命名冲突,所以索性将C语言的库封装进C++的std命名空间中。其中在原有的<stdlib.h>中加上一个"c"作为前缀,表示该头文件是C++头文件,再将其后缀".h"去掉,表示该头文件封装在std命名空间中。
接下来进入主题。定义了一个类之后,其成员变量需要被初始化,所以我们定义了一个名为Init的成员函数;当不需要使用该对象时或对象销毁时,我们希望释放它所动态开辟的空间,所以我们定义了一个名为Destroy的成员函数。那么假设某个程序员的记性很差,在定义Stack对象时忘记初始化,在对象销毁时忘记释放动态开辟的空间,那么就会造成无法向栈push元素,还会造成内存泄漏。实际上C++的祖师爷考虑到了这个问题,提出了一种名为构造函数的成员函数,其目的就是为了定义对象时自动初始化;还提出了一种名为析构函数的成员函数,其目的就是在对象销毁时自动释放动态开辟的资源(例如malloc、new出来的空间或者fopen打开的文件)。
前面说过,构造函数和析构函数属于6个默认成员函数,也就是说我们不显式的实现这两个函数,编译器自动帮我们生成。也就是说在上面的Stack类当中是存在构造函数和析构函数的,只不过是编译器隐式生成,我们看不到。
构造函数的定义:
1.构造函数是特殊的成员函数,其目的是为了初始化对象(不是对象创建时开辟空间,是初始化对象当中的数据),构造函数即使是一个特殊的成员函数,但是它依然持有this指针(没有this指针的成员函数称为静态成员函数)
2.定义构造函数的规则:
1)函数名与类名相同
2)无返回值:这个"无返回值"不是指构造函数的返回类型为void,而是指在定义构造函数时不写返回类型
3)类实例化对象时,由编译器自动调用对应的构造函数
4)构造函数可以传参
5)构造函数可以重载(因为可以传参)
那么我们对上面的Stack类进行修改:
#include <cstdlib>
using namespace std;class Stack
{
public:Stack(int capacity = 4)// 构造函数{_a = (int*)malloc(sizeof(int)*capacity);_top = 0;_capacity = capacity;}void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}void Destroy(){free(_a);_a = nullptr;_top = _capacity = 0;}
private:int* _a;int _top;int _capacity;
};
我们在实例化Stack对象的时候就可以这么玩了:
int main()
{Stack s1;// 自动调用构造函数,不传参,使用缺省参数// Stack s1();// 不传参为什么不这样写?Stack s2(100);// 传参,其容量为100return 0;
}
我们注意到了,当要对构造函数进行传参时,直接在对象名后面更上参数即可。那么为什么不能像注释中所说的那样定义不传参给构造函数的对象呢?我们换一种视角看它:
Stack func();
这是一个函数声明!不是定义对象!
析构函数的定义:
1.析构函数也是一个特殊的成员函数,目的是释放对象当中动态开辟的资源,它也有this指针
2.定义析构函数的规则:
1)函数名和类名相同,但是要在前面加一个"~"。"~"的愿意是按位取反,析构函数加一个"~"是为了与构造函数区分、对立
2)无返回值,与构造函数相同
3)对象即将销毁时,由编译器自动调用析构函数
4)析构函数不能带参数(除了默认的this指针)
5)不能重载
上面的一段代码是存在内存泄露的,我们对Stack类、主函数做出修改,以证明对象销毁时编译器自动调用析构函数:
#include <cstdlib>
#include <iostream>
using namespace std;class Stack
{
public:Stack(int capacity = 4)// 构造函数{_a = (int*)malloc(sizeof(int)*capacity);_top = 0;_capacity = capacity;}void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;cout << "~Stack" << endl;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack s1;// 自动调用构造函数,不传参,使用缺省参数Stack func();Stack s2(100);// 传参,其容量为100return 0;
}
在这里再插一段题外话,上面的s1和s2和哪个对象先销毁,哪个对象后销毁?答案是s2先销毁,s1后销毁。我们是在main函数定义的对象,就是说这两个对象遵循入栈和出栈顺序,其中s2后进,那它就应该先出。
3.构造函数进阶
前面说过,如果构造函数没有显式定义,那么编译器会为我们默认生成一个。编译器默认生成的构造函数是无参的构造函数,还是以Stack类为例子,观察该构造函数的作用:
#include <cstdlib>
#include <iostream>
using namespace std;class Stack
{
public:void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;cout << "~Stack" << endl;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack s1;return 0;
}
在监视窗口中,我们发现,编译器默认生成的构造函数似乎什么作用也没起,Stack类的所有成员都没有被初始化,甚至空间都没有动态开辟。事实上这不代表默认构造函数没有用,只不过作用不在这里。
编译器默认生成的构造函数的作用:对内置类型不处理,自动调用自定义类型的默认构造函数(默认构造函数指的是无参的、全缺省的、编译器默认生成的构造函数中的其中一个,它们三个只能同时存在一个)。
在C++中,内置类型指的是C++的原生类型,即我们熟知的int、double、char、指针类型等等;自定义类型指的是我们通过struct或者class定义出的类类型。如果我们在上面的Stack类当中再增加一个自定义类型的对象,就能看到编译器默认生成的构造函数的效果:
#include <cstdlib>
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造{_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
class Stack
{
public:void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;cout << "~Stack" << endl;}
private:int* _a;int _top;int _capacity;Date _d;
};int main()
{Stack s1;return 0;
}
编译器默认生成的构造函数的使用场景:在我们定义类时,如果成员变量都是自定义类型的,那么就不需要显式地定义构造函数,因为编译器默认生成的构造函数会自动调用自定义类型的默认构造函数。也就是说,当编译器默认生成的构造函数足够我们使用时,就不需要显式地定义构造函数。
如果我们定义的类当中存在自定义类型和内置类型,那么编译器默认生成的构造函数是不够用的,此时我们必须显式地定义构造函数,那么对于自定义类型的初始化要通过初始化列表,不过在这篇博客中不会提到。除了初始化列表初始化自定义类型的方式,在C++11当中打了一个补丁,即内置类型成员变量可以给缺省值,构造函数(无论哪种构造函数)会使用该缺省值初始化成员:
#include <cstdlib>
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造{_year = year;_month = month;_day = day;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};
class Stack
{
public:void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}
private:// 内置类型成员变量可以给出缺省值int* _a = (int*)malloc(sizeof(int)*4);int _top = 0;int _capacity = 4;Date _d;
};int main()
{Stack s1;return 0;
}
需要注意的是,内置类型给定缺省值之后并不代表在类中定义了变量,它仅仅是一个声明。同理,如果类中全是内置类型的成员变量,那么它们都可以使用缺省值。
4.析构函数进阶
如果我们没有显式定义析构函数,编译器也会生成一个无参的默认析构函数。与构造函数一样,编译器自动生成的默认析构函数会自动调用自定义类型的析构函数:
#include <cstdlib>
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造{_year = year;_month = month;_day = day;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};
class Stack
{
public:void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}
private:int* _a;int _top;int _capacity;Date _d;
};int main()
{Stack s1;return 0;
}
编译器默认生成的析构函数的使用场景:如果定义类时,类中没有进行任何动态资源的申请,那么析构函数可以不写。
5.拷贝构造函数
如果我们没有显式定义拷贝构造函数,那么编译器会自动生成一个拷贝构造函数。拷贝构造函数的本质也是构造函数,其目的也是为了初始化,不过拷贝构造函数是将已存在的同类类型对象拷贝给正在初始化的对象:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}
private:int _year = 1;int _month = 1;int _day = 1;
};int main()
{Date d1(2023, 4, 26);Date d2(d1);// 使用d1对象作为初始化数据,初始化d2对象return 0;
}
拷贝构造函数的显式定义:
1.拷贝构造函数是构造函数的一个重载
2.拷贝构造函数的参数只能有一个,并且必须是类类型对象的引用
3.如果拷贝构造函数的参数不是类类型对象的引用,而是一个普通的、传值的类类型对象形参,那么编译器会阻止这种行为,因为该行为会引发无穷递归
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}Date(Date& d)// 拷贝构造{_year = d._year;_month = d._month;_day = d._day;}
private:int _year = 1;int _month = 1;int _day = 1;
};
接下来解析为什么构造函数不能传值调用:如果拷贝构造函数是以传值传参的方式进行调用,那么拷贝构造的函数的参数就是一个局部对象,在调用过程中,实参就会拷贝给该局部对象,那么此时涉及到拷贝,就要调用拷贝构造函数,而拷贝构造函数是以传值方式调用的......如此往复就会触发无穷递归
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}Date(Date d)// 拷贝构造{_year = d._year;_month = d._month;_day = d._day;}
private:int _year = 1;int _month = 1;int _day = 1;
};
另外,还建议拷贝构造的参数是类类型对象的常引用,因为不排除程序员的失误写出下面这段代码:
Date(Date& d)// 拷贝构造
{_year = d._year;_month = d._month;d._day = _day;// 程序员的失误把赋值方向弄反了
}
所以我们将其修改成:
Date(const Date& d)// 建议使用类类型对象的常引用
{_year = d._year;_month = d._month;//d._day = _day;// 报错,常量不能被修改_day = d._day;
}
编译器默认生成的拷贝构造的作用:
1.对内置类型逐字节进行拷贝:例如本小节中的第一份代码,我们没有显式定义拷贝构造,那么编译器就会默认生成一个拷贝构造。而Date类中恰好都是内置类型,所以直接逐字节拷贝
2.对于自定义类型则是调用自定义类型的拷贝构造完成拷贝:如果类中的成员存在自定类型的对象,那么该成员对象通过它的拷贝构造来完成拷贝。例如下面这个例子:
#include <cstdlib>
using namespace std;class Stack
{
public:void Push(int in){// 暂时不考虑扩容..._a[_top++] = in;}
private:int* _a = (int*)malloc(sizeof(int)*4);int _top = 0;int _capacity = 4;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}
private:int _year = 1;int _month = 1;int _day = 1;Stack _st;
};int main()
{Date d1(2023,4,27);Date d2(d1);return 0;
}
、
在此例中,Date类中定义了一个Stack类型的对象,Date的构造函数没有显式初始化该对象,那么该对象就会调用自己默认构造。d1对象拷贝正在实例化的d2对象时,d1对象中的内置类型部分逐字节拷贝给d2对象,d1对象中的Stack类型部分调用Stack类型的拷贝构造拷贝给d2对象中Stack类型对象。Stack类当中的成员都是内置类型,而且没有显式定义拷贝构造,所以编译器默认生成的构造函数会逐字节地拷贝这些内置类型变量。
我们注意到,使用编译器默认生成的拷贝构造进行拷贝时,会将指针变量当中保存的地址也按字节拷贝过去,这就会导致在某些场景下发生崩溃:
#include <cstdlib>
using namespace std;
class Stack
{
public:~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}
private:int* _a = (int*)malloc(sizeof(int)* 4);int _top = 0;int _capacity = 4;
};int main()
{Stack st1;Stack st2(st1);return 0;
}
发生崩溃的原因就在于重复析构。因为Stack类中的_a成员变量获得了一块在堆上的空间的地址,又因为Stack类没有显式定义拷贝构造,所以st1拷贝st2时是逐字节拷贝的(Stack类的成员都是内置类型),所以st2中的_a和st1中的_a都指向了同一块空间:
逐字节拷贝的过程称为浅拷贝。浅拷贝只适用于不需要显式定义析构函数的场景。
那么像上面会引发重复析构的解决方案就是使用深拷贝,其作用就是要让某些需要动态申请资源的成员拥有一块独立的空间,这个过程是由程序员去控制的,我们将上面引发崩溃的代码修改一下:
#include <cstdlib>
#include <cstring>
using namespace std;
class Stack
{
public:~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}Stack()// 需要注意,拷贝构造是构造一个重载形式,所以定义了拷贝构造编译器就不会生成默认构造{}Stack(const Stack& st){_a = (int*)malloc(sizeof(int)* 4);// 拥有一块独立的空间memcpy(_a, st._a, st._capacity);// 再逐字节拷贝// 不需要深拷贝的直接逐字节拷贝_top = st._top;_capacity = st._capacity;}
private:int* _a = (int*)malloc(sizeof(int)* 4);int _top = 0;int _capacity = 4;
};
两条规则希望读者牢记于心:
1.不需要显式定义析构函数的类类型使用浅拷贝即可满足要求
2.需要显式定义析构函数的类类类型需要使用深拷贝才可以满足要求
拷贝构造的调用并不只是我们显式去调用才会发生,也有可能在某些场景中隐式发生拷贝而调用拷贝构造:
1.传值传参时调用拷贝构造
2.传值返回时调用拷贝构造
6.运算符重载
说明一点,这里介绍的运算符重载不是类的默认成员函数中的赋值运算符重载,赋值运算符重载在后面将会介绍。
运算符重载也需要区别于函数重载,运算符的重载与函数重载不是一个概念。
什么是运算符重载?当我们要比较内置类型对象的大小时,我们只需要使用运算符">"、"<"、"=="等等诸如此类的运算符即可;想要对内置类型对象进行计算时,使用"+"、"-"、"*"等等诸如此类的运算符即可。那么对于自定义类型来说,则需要调用函数,我们以日期类为例:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}
public:// 注意这里是公有int _year;int _month;int _day;
};bool isMore(const Date& d1, const Date& d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day){return true;}return false;
}
int main()
{Date d1(2023, 4, 27);Date d2(2024, 4, 27);// 想要比较这两个对象的大小,必须通过函数bool ret = isMore(d1, d2);return 0;
}
确实,我们通过这样的方式确实能够比较两个对象的大小,但是这不是C++,这是C语言。C++提供了一种名为运算符重载的机制,通俗的来说,它的作用就是让我们可以将运算符的名字作为函数名,我们这样修改上面的代码:
bool operator>(const Date& d1, const Date& d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day){return true;}return false;
}
int main()
{Date d1(2023, 4, 27);Date d2(2024, 4, 27);// 想要比较这两个对象的大小,必须通过函数bool ret = d1 > d2;//bool ret = operator>(d1, d2);//效果与上面等价,但是不会这么用return 0;
}
是的,运算符重载也是一个函数,其函数声明的格式为[返回类型 opeartor操作符(参数列表)]。但是,上面的代码也是不是C++风格的代码,如果我们将Date类的成员修改为私有成员,那么就会喜提以下报错信息:
解决这个问题的方法之一便是将该函数作为Date类的成员函数,但是我们又会额外获得一些报错信息:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}bool operator>(const Date& d1, const Date& d2){if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day){return true;}return false;}
private:int _year;int _month;int _day;
};
原因在于类中的成员函数是有一个隐藏的this指针,所以上面的函数看上去有两个参数,实际上有三个参数。运算符重载的原则是:根据运算符的性质来确定函数参数,例如"=="运算符就是个二元运算符,所以它需要两个参数。那么我们再将其修改一下:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}bool operator>(const Date& d){if (_year > _year){return true;}else if (_year == d._year && _month > d._month){return true;}else if (_year == d._year && _month == d._month && _day > d._day){return true;}return false;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 4, 27);Date d2(2024, 4, 27);// 想要比较这两个对象的大小,必须通过函数bool ret = d1 > d2;//bool ret = d1.operator>(d2);//编译器自动转换,也可以这样显式调用return 0;
}
以上就是运算符重载的基本用法,后面还有更多的代码、更多的场景使用运算符重载。那么在关于运算符重载,C++给出了以下几点要求:
1.不能通过"operator"连接其他符号来创造新的运算符,例如[operator@]就是错误的用法
2.运算符重载的函数参数必须有一个类类型参数,即必须有自定义类型,否则C++就会认为我们正在尝试修改C++默认定义的运算规则,例如下面的例子就是错误的用法:
int operator+(int x, int y)// C++:以下犯上?
{return x - y;
}
int main()
{return 0;
}
3.当运算符重载作为类的普通成员函数时,其显式的形参不是真正参数个数,还需要考虑this指针
4.运算符重载的参数与操作数是一一对应的,第一个参数代表左操作数,第二个参数代表右操作数。这是C++规定的行为
5.有五个运算符不能被重载,即"::"、"sizeof"、"?:"、"."和".*",".*"这个运算符我们没有见过,我也从来没有见过,别关注它。
7.日期类
我们运用已有的知识来封装一个日期类,日期类要支持">"、">="、"=="、"!="等等这些比较,还需要支持"+"、"+="、"-"、"-="等等这些运算。我们首先搭好一个框架,用来明确我们将要实现什么功能:
#include <iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{_year = year;_month = month;_day = day;}void Print(){cout << _year << ":" << _month << ":" << _day << endl;}bool operator>(const Date& d);bool operator== (const Date& d);bool operator>=(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator!=(const Date& d);Date& operator+=(int day);Date operator+(int day);Date& operator-=(int day);Date operator-(int day);Date& operator++();Date& operator++(int);Date& operator--();Date& operator--(int);int operator-(const Date& d);private:int _year;int _month;int _day;
};
实现operator>:
其实这个函数已经实现过了,具体的逻辑就是先比较年份,如果年份大,那么日期就大;如果年份相同,就比较月份,如果月份大,那么日期九大;如果年份、月份都相同,再比较日,日大则日期大;如果都不满足,那就是不大于
bool operator>(const Date& d)
{if (_year > d._year)// 比较年份{return true;}else if (_year == d._year && _month > d._month)// 年份相同比较月份{return true;}else if (_year == d._year && _month == d._month && _day > d._day)// 年份、月份相同比价日{return true;}return false;// 不大于
}
实现operator==:
这个逻辑也非常简单,年份、月份、日三者都相等即为相等
bool operator== (const Date& d)
{if (_year == d._year && _month == d._month && _day == d._day){return true;}return false;
}
实现operator>=:
一定要抛弃固有思维!这里可以复用函数!
bool operator>=(const Date& d)
{return *this > d || *this == d;
}
实现operator<:
这里也可以复用代码
bool operator<(const Date& d)
{if (*this >= d)// 如果 >= ,那就不小于{return false;}return true;// 不然就是小于
}
实现operator<=:
都是代码复用
bool operator<=(const Date& d)
{if (*this > d)//如果>就不是<={return false;}return true;
}
实现operator!=:
bool operator!=(const Date& d)
{if (*this == d){return false;}return true;
}
实现operator+=:
这里就比较复杂了。我们重载加法运算符的目的是计算当前日期N天后是什么日期。我们的思路是这样的:直接让天数相加,如果天数超过了该月的天数,那么月就进位;如果月超过了12,那么就让年进位:
那么这里涉及到关于月份的计算,那么就需要考虑闰年和平年,我们将获取某年某月的天数封装成一个函数(可以作为类的私有成员):
int getMonthDay(int year, int month)
{int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 如果是2月又是闰年if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}return day[month];
}
此时再实现operator+=的逻辑:
Date& operator+=(int day)
{_day += day;// 直接让天数相加while (_day > getMonthDay(_year, _month))//如果天数大于本月最大天数{_day -= getMonthDay(_year, _month);++_month;// 月份++if (_month == 13)// 如果月份超过12月{++_year;_month = 1;}}return *this;
}
注意这里使用的是引用返回,因为"*this"得到的是对象,对象出了该函数作用域还没有销毁,就可以返回引用。 至于为什么要有返回值,就是另外一种使用场景,即连续赋值场景,不排除有这样的应用场景:
int main()
{Date d1(2022, 4, 28);Date d2(d1 += 12);// d1+=12的返回值作为已存在的对象拷贝给d2(d2 += 20).Print();// d2+=20的返回值作为已存在的对象调用成员函数return 0;
}
实现operator+:
与"operator+="不同的是,"operator+"并不会修改对象的内容(x+3并不会修改x的值),所以这里需要特殊处理一下,但是我们仍然可以复用函数
Date operator+(int day)
{Date tmp(*this);// tmp为*this的拷贝tmp += day;return tmp;
}
实现operator-=:
具体的算法分析过程就不展示了,最终实现的逻辑如下:
Date& operator-=(int day)
{_day -= day;// 天数先相减while (_day <= 0)// 当天数不合法的时候{--_month;if (_month == 0){--_year;_month = 12;}_day += getMonthDay(_year, _month);}return *this;
}
实现operator-:
与实现operator+一样,复用函数:
Date operator-(int day)
{Date tmp(*this);tmp -= day;return tmp;
}
写到这里,日期类差不多就算完成了,但是我们不排除有些人写出下面这样的代码:
int main()
{Date d1(2023, 4, 28);d1 += -2000;d1.Print();Date d2(2023, 4, 28);d2 -= -2000;d2.Print();return 0;
}
所以要进一步修改operator+=和operator-=(operator+和operaor-都是复用,不需要修改):
Date& operator+=(int day)
{if (day < 0)// 如果天数是负数{return *this -= abs(day);// 取day的绝对值}_day += day;// 直接让天数相加while (_day > getMonthDay(_year, _month))//如果天数大于本月最大天数{_day -= getMonthDay(_year, _month);++_month;// 月份++if (_month == 13)// 如果月份超过12月{++_year;_month = 1;}}return *this;
}Date& operator-=(int day)
{if (day < 0)// 如果天数是负数{return *this += abs(day);}_day -= day;// 天数先相减while (_day <= 0)// 当天数不合法的时候{--_month;if (_month == 0){--_year;_month = 12;}_day += getMonthDay(_year, _month);}return *this;
}
实际上写到这里,日期类已经实现的很不错了,但还不够完美,我们需要日期类能够支持前置++和后置++,我们给出的函数声明是这样的:
Date& operator++();// 前置++
Date operator++(int);// 后置++
首先我们需要知道,"++"是一个单操作数的运算符,而我们要实现前置++和后置++光靠一个参数是区分不开的,所以C++规定,前置++的运算符重载只需要有一个参数(上面没写是因为作为类的成员函数,有this指针),后置++的运算符要有两个参数,第二个参数规定了必须是int类型,它没有什么实际意义,目的就是为了与前置++区分,构成函数重载。返回值的设计也有讲究,对象调用前置++运算符重载后,其值立马发生改变;后置++运算符重载被调用后,其对象的值并没有立马改变,而是要"延迟"一会。我们看它们的具体实现:
Date& operator++()// 前置++
{*this += 1;return *this;// 轻轻松松
}Date operator++(int)// 后置++
{Date tmp(*this);*this += 1;return tmp;
}
我们再实现一个前置--和一个后置--:
Date& operator--()// 前置--
{*this -= 1;return *this;
}Date operator--(int)// 后置--
{Date tmp(*this);*this -= 1;return tmp;
}
综上,我们可以发现,无论是++还是--,只要是后置,那么空间的开销一定比前置大(后置比前置多了两次拷贝),所以在C++中,能用前置++或者前置--就尽量用。
我们不排除在出初始化日期类对象时,有人写出下面这样的代码:
int main()
{Date d1(2023, 4, 99);d1 += -2000;d1.Print();Date d2(2023, 4, 2023);d2 -= -2000;d2.Print();return 0;
}
这样的初始化数据是不合理的,所以在初始化的时候就应该阻止这样的行为,即在构造函数内存添加一些条件:
Date(int year = 1, int month = 1, int day = 1)// 默认构造函数
{if (year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <= getMonthDay(year, month))){// 只有日期合法才进行初始化_year = year;_month = month;_day = day;}else{cout << "日期初始化数据不合法!" << endl;exit(1);// 直接让该程序退出}
}
需要注意,因为知识有限,现阶段我们把构造函数内的赋值语句称为"初始化",实际上这不是初始化。具体内容在类和对象下的博客中讲解。
上面的运算符都是"日期+天数"的形式,现在我们有新需求,即计算两个日期的差(即计算两个日期相差几天),很明显,这也是一个"-"运算符重载,不过现在我们实现的与上面已定义的要构成函数重载,即参数不一样:
int operator-(const Date& d)
{// 假设*this > dDate max = *this;Date min = d;int flag = 1;// 那么*this - d计算的结果就是正数if (*this < d)// 假设错误{max = d;min = *this;flag = -1;}int n = 0;while (min != max){++n;++min;// 小的日期一直++,直到与max相等。++多少次就是相差多少天}return n;
}
7.1输入&输出&友元函数
我们的日期类已经初步完工,并且能够打印出正确的测试结果(部分测试):
int main()
{Date d1(2023, 5, 22);Date d2(2015, 4, 29);(d1 + 30).Print();// d1 + 30d1.Print();// d1 不变(d2 - 88).Print();// d2 - 88d2.Print();// d2 不变cout << (d1 - d2) << endl;return 0;
}
同时我们注意到,想要按照我们的格式输出Date对象时,需要调用Print成员函数,这种方法不是不可以,而是太low,不符合C++的使用意愿。我们在前面提到过输入流与输出流,即cout对象和cin对象,还有流插入(<<)运算符和流提取(>>)运算符。那么cout是一个对象,那么它就有对应的类,即ostream类,cin对应istream类:
并且流插入和流提取作为运算符,不难猜测ostream类和istream类对其进行了运算符重载:
因为运算符重载是一个函数,不同的参数类型可以构成函数重载,如上图。所以在我们使用cout和cin时,就能理解为什么传入不同类型的参数就可以"自动识别类型",其原因就在于函数重载。也就是说,我们想要cout或cin对我们的Date类生效,只需要再添加一个重载函数即可:
class Date
{
public:
private:int _year = 1;int _month = 1;int _day = 1;
};ostream& operator<<(ostream& out, const Date& d)
{out << d._year << ":" << d._month << ":" << d._day;return out;
}istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
这两个函数可不能作为Date类的成员函数,因为我们说过运算符重载的函数参数与操作符左右操作数是有对应关系的。如果将这两个函数作为成员函数,那么this指针就占用了第一个参数的位置,也就相当于让对象去做操作符的左操作数,使用输出或输入时就有可能得这么写"d1 << cout"或者"d1 >> cin",这显然是不合理的。
关键点在于这两个函数并不能通过编译,因为我们在类外访问了类的私有成员。一种解决方案是将类成员的"private"属性修改成"public",不过这么干就不是C++程序员了;另一种方案是使用友元函数,在这里简单介绍一下友元函数的作用:能够在类外访问类的私有成员,用法就是将函数声明写进类的任意位置,再在声明前面加上friend关键字。我们就可以这么操作:
class Date
{
public:// 友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);private:int _year = 1;int _month = 1;int _day = 1;
};
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << ":" << d._month << ":" << d._day;return out;// 返回值是为了连续输出的场景
}istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;// 返回值是为了连续输入的场景
}int main()
{Date d1, d2;cin >> d1 >> d2;// 连续输入cout << d1 << " " << d2 << endl;// 连续输出return 0;
}
此时operator<<和operator>>是作为普通全局函数的,并且上面所有有关日期类的大代码都是在一个源文件中实现的。我想表达的是,如果在头文件中定义了全局函数,那么该头文件被多个源文件包含时会产生链接错误:
// Date.h
#pragma once#include <iostream>
using namespace std;
class Date
{
public:friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);private:int _year = 1;int _month = 1;int _day = 1;
};
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << ":" << d._month << ":" << d._day;return out;// 返回值是为了连续输出的场景
}istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;// 返回值是为了连续输入的场景
}
// main.cpp
#include "Date.h"
int main()
{// 哪怕不使用头文件定义的函数,也会报错return 0;
}
// func.cpp
#include "Date.h"
造成链接错误的原因是这样的:头文件中定义的函数被多个源文件包含,预处理的时候就会将头文件的内容展开,展开之后多个源文件就有了一模一样的函数定义,在各个源文件编译时就会生成符号表,函数就会被修饰进,这些符号表之间就会有相同的函数,这就会导致链接器在检查时发现有多个重定义的函数。解决方案是将函数定义为static函数,或者定义为内联函数,static修饰函数时能够修改其外部链接属性,也就说将该函数在符号表当中"隐藏"起来,而内联函数则是直接不进符号表,所以链接器在检查时就查不到重定义了。
其实不需要有头文件的介入,只要在多个源文件中定义相同的函数,都会产生链接错误:
// func.cpp
void func()
{}
// main.cpp
void func()
{}
int main()
{return 0;
}
而修改成static函数或者内联函数就不会报错。
8.赋值运算符重载
赋值运算符重载其实也是运算符重载,只不过赋值运算符特殊的地方在于它是类的6个默认成员函数之一,即我们写编译器会自动生成。那么赋值的作用就是完成拷贝,它与拷贝构造的区别在于:拷贝构造是用一个已存在的同类类型对象初始化另一个正在创建的对象;赋值运算符重载是已经存在的对象之间的拷贝关系。
那么先抛开编译器自动生成的运算符重载,先考虑如何自己实现一个赋值运算符重载:
class Student
{
public:Student(char* name = "", int id = 0, int age = 0){_name = name;_id = id;_age = age;}void operator=(const Student& s)// 要赋值过来的对象不改变,所以使用常引用{_name = s._name;_id = s._id;_age = s._age;}
private:char* _name;int _id;int _age;
};
int main()
{Student s1("张三", 123456, 21);Student s2;s2 = s1;// 已存在的对象之间的赋值return 0;
}
这样看似实现了赋值的功能,但实际上可能会存在这样的代码:
int main()
{Student s1("张三", 123456, 21);Student s2;Student s3;s3 = s2 = s1;// void operator=()return 0;
}
我们实现的赋值运算符重载没有返回值,所以不能够适用于连续赋值的场景。思路其实很简单,设计一个返回值即可。但是难点在于,返回值如何设计?是返回s1还是返回s2?返回值是返回引用还是传值返回?
赋值运算符重载的返回值设计:
1.原则一:返回值返回赋值符号的左操作数
2.原则二:返回值设计为引用返回,记住,不能是常引用。因为返回引用可以减少一次拷贝构造和减小空间开销
将原则一与原则二牢记于心,我们就可以解释以下代码:
class Student
{
public:Student(char* name = "", int id = 0, int age = 0){_name = name;_id = id;_age = age;}Student& operator=(const Student& s)// 要赋值过来的对象不改变,所以使用常引用{_name = s._name;_id = s._id;_age = s._age;//return s;// 如果返回右操作数,返回类型就要设计为常引用return *this;// 返回右操作数,明智之举}
private:char* _name;int _id;int _age;
};
int main()
{Student s1("张三", 123456, 21);Student s2;Student s3;s3 = s2 = s1;// operator=有返回值就可以连续赋值// 假设Student支持++操作//(s3 = s2 = s1)++;// 返回值为普通引用就可以支持++操作return 0;
}
以上便是实现一个最简单的赋值运算符重载,现在来介绍编译器默认生成的赋值运算符重载。实际上赋值运算符重载与拷贝构造一样,我们不写编译器会自动生成,它们都是对内置类型完成逐字节拷贝,对自定义类型调用它们的赋值运算符重载或拷贝构造。要不要显式写赋值运算符重载与要不要显式写拷贝构造一样,如果该类不需要显式写析构函数,那么就不需要显式定义赋值运算符重载。不过,赋值运算符重载的显式定义要考虑的情况比构造函数多,我们以Stack类为例:
#include <cstdlib>
#include <cstring>
using namespace std;class Stack
{
public:~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;}Stack(int capacity = 4){_a = (int*)malloc(sizeof(int)* capacity);_top = 0;_capacity = capacity;}Stack(const Stack& st)// 拷贝构造->用于初始化{_a = (int*)malloc(sizeof(int)* st._capacity);memcpy(_a, st._a, st._capacity);_top = st._top;_capacity = st._capacity;}Stack& operator=(const Stack& st){// 这样写就完了吗?_a = (int*)malloc(sizeof(int)* st._capacity);memcpy(_a, st._a, st._capacity);_top = st._top;_capacity = st._capacity;return *this;}
private:int* _a;int _top;int _capacity;
};
Stack类需要写析构函数,所以我们显式定义拷贝构造和赋值运算符重载,使得对象与对象之间拥有独立的堆空间,这个思路是正确的,但是我们忽略了一点,即赋值是已存在对象之间的赋值,我们不得不防止有以下的情况发生:
int main()
{Stack s1(10000);Stack s2;s1 = s2;// 内存泄露return 0;
}
这段代码会造成很严重的内存泄漏,即使运行过程中没有任何错误。原因就在于s1对象本在堆上拥有了10000个int类型的空间,而"s1 = s2"直接让s1的_a换了一个指向,那么以前的10000个int类型空间何去何从?
所以赋值运算符重载内部必须要对以前拥有的空间作处理,我们修改一下赋值运算符重载:
Stack& operator=(const Stack& st)
{free(_a);// 将以前的空间释放掉_a = (int*)malloc(sizeof(int)* st._capacity);memcpy(_a, st._a, st._capacity);_top = st._top;_capacity = st._capacity;return *this;
}
还没有结束,也不排除有的人写出这样的代码:
int main()
{Stack s1(10000);Stack s2;s1 = s1;// 自我赋值return 0;
}
这段代码是很危险的,因为我们的赋值运算符重载内部第一件事就是释放掉原有的空间,那么此时赋值符号两边都是同一个对象,这就会导致拷贝未定义的数据。因为s1对象创建之后如果执行了push操作,就会向该空间内添加有效数据(我们的例子没写push)
这种自我赋值的语法编译器是允许的,我们是不能阻止的,我们可以这么修改赋值运算符重载:
Stack& operator=(const Stack& st)
{if (this != &st)// 如果赋值符号两边不是一个对象才允许赋值{free(_a);// 将以前的空间释放掉_a = (int*)malloc(sizeof(int)* st._capacity);memcpy(_a, st._a, st._capacity);_top = st._top;_capacity = st._capacity;}return *this;
}
在这里再讲一个冷知识,我们看下面的代码:
int main()
{Stack s1(10000);Stack s2;// 这两个"="有什么区别?Stack s3 = s1;s2 = s1;return 0;
}
这段代码乍一看似乎都是赋值运算符重载被调用,实际上不是。s3对象正在创建的时候将s1拷贝给它,所以"Stack s3 = s1"发生一次拷贝构造;"s2 = s1"是两个已存在的对象之间的赋值,是一次赋值,即调用赋值运算符重载。
9.const成员函数
对象不可能只有普通对象,还有const对象,以上面的日期类为例,const对象调用成员函数是调不动的:
int main()
{Date d1(2023, 5, 1);d1.Print();const Date d2(2023, 4, 29);d2.Print();// 调用失败return 0;
}
原因在于this指针。如我们对d2对象取地址,那么它的指针类型为"const Date*",而Print作为成员函数,其this指针的类型为"Date* const";所以编译器在隐式传递对象指针的时候,d2的指针是没有匹配的this指针类型的,所以调用不了成员函数。也可以理解为"const *"类型的指针不能传递给"* const"类型的指针,因为这是一次权限放大。
解决方案为在成员函数的参数列表之后加上const关键字:
这个const的含义修饰的是this指针指向的内容,即将this指针原本的"Date* const"修改为"const Date* const"类型。像这样的函数我们称为const成员函数,const成员函数任何对象都可以调用(因为权限可以被平移和缩小):
int main()
{Date d1(2023, 5, 1);d1.Print();// 非const对象能调用const Date d2(2023, 4, 29);d2.Print();// const对象也能调用return 0;
}
当然,如果我们不想让const对象和非const共用一个成员函数,我们完完全全可以重载一下成员函数:
实际上在类外定义const对象的场景是比较少的,const对象通常都在成员函数中定义。我们拿日期类的某一成员函数举例:
int operator-(const Date& d)// 这里是一个const对象
{Date max = *this;Date min = d;int flag = 1;//if (*this < d)// 这么调是可以的,因为this指针的类型为 Date* constif (d > *this)// 这么写是错的,因为d为const对象,调用的>运算符重载没有对应的this指针类型{max = d;min = *this;flag = -1;}int n = 0;while (min != max){++n;++min;}return n;
}
所以在改进日期类之前,先说明一下const成员函数的定义规则:
1.需要被const对象调用的成员函数要加const
2.成员函数被调用时,其this指针的对象不发生改变时,可以加const
3.如果成员函数的声明和定义分离,那么声明和定义都要有const
4.如果想让非const对象调用非const成员函数,const成员函数调用const成员函数,可以单独定义一个非const成员函数和一个const成员函数
9.1日期类完整代码
了解const成员函数之后,那么最终版本的日期类就可以写出来了:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)// 默认构造函数{if (year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <= getMonthDay(year, month))){// 只有日期合法才进行初始化_year = year;_month = month;_day = day;}else{cout << "日期初始化数据不合法!" << endl;exit(1);// 直接让该程序退出}}void Print() const// const对象专用{cout << _year << ":" << _month << ":" << _day << endl;}void Print()// 非const对象专用{cout << _year << ":" << _month << ":" << _day << endl;}bool operator>(const Date& d) const//*this不改变,可以加const{if (_year > d._year)// 比较年份{return true;}else if (_year == d._year && _month > d._month)// 年份相同比较月份{return true;}else if (_year == d._year && _month == d._month && _day > d._day)// 年份、月份相同比价日{return true;}return false;// 不大于}bool operator== (const Date& d) const//*this不改变,加const{if (_year == d._year && _month == d._month && _day == d._day){return true;}return false;}bool operator>=(const Date& d) const//*this不改变,加const{return *this > d || *this == d;}bool operator<(const Date& d) const{if (*this >= d){return false;}return true;}bool operator<=(const Date& d) const{if (*this > d)//如果>就不是<={return false;}return true;}bool operator!=(const Date& d) const{if (*this == d){return false;}return true;}Date& operator+=(int day)// *this改变,不能加const{if (day < 0)// 如果天数是负数{return *this -= abs(day);// 取day的绝对值}_day += day;// 直接让天数相加while (_day > getMonthDay(_year, _month))//如果天数大于本月最大天数{_day -= getMonthDay(_year, _month);++_month;// 月份++if (_month == 13)// 如果月份超过12月{++_year;_month = 1;}}return *this;}Date operator+(int day) const//*this不改变,加const{Date tmp(*this);// tmp为*this的拷贝tmp += day;return tmp;}Date& operator-=(int day){if (day < 0)// 如果天数是负数{return *this += abs(day);}_day -= day;// 天数先相减while (_day <= 0)// 当天数不合法的时候{--_month;if (_month == 0){--_year;_month = 12;}_day += getMonthDay(_year, _month);}return *this;}Date operator-(int day) const{Date tmp(*this);tmp -= day;return tmp;}Date& operator++()// 前置++{*this += 1;return *this;// 轻轻松松}Date operator++(int)// 后置++{Date tmp(*this);*this += 1;return tmp;}Date& operator--()// 前置--{*this -= 1;return *this;}Date operator--(int)// 后置--{Date tmp(*this);*this -= 1;return tmp;}int operator-(const Date& d) const// *this不改变,加const{Date max = *this;Date min = d;int flag = 1;//if (*this < d)if (d > *this){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++n;++min;// 小的日期一直++,直到与max相等。++多少次就是相差多少天}return n;}
private:int _year;int _month;int _day;int getMonthDay(int year, int month){int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 如果是2月又是闰年if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}return day[month];}// 友元函数的声明可以出现在类中的任意位置friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);};
10.取地址重载
我们直到,取地址(&)也是个运算符。如果我们没有重载取地址运算符,那么对象的地址是取不到的。C++考虑到了这一点,将取地址运算符重载归类成了类的默认成员函数,即我们不写,编译器自动生成,并且生成两个版本,非const对象取地址重载和const对象取地址重载:
#include <iostream>
using namespace std;class Coords//坐标类
{
public:Coords(int x = 0, int y = 0){_x = x;_y = y;}
private:int _x;int _y;
};int main()
{Coords c1;const Coords c2;// 编译器自动生成的取地址重载cout << &c1 << " " << &c2 << endl;return 0;
}
如果我们想要显式定义取地址重载,我们可以这么写:
class Coords//坐标类
{
public:Coords(int x = 0, int y = 0){_x = x;_y = y;}Coords* operator&(){return this;}const Coords* operator&() const{return this;}
private:int _x;int _y;
};
实际上显式定义这两个取地址重载没什么意义,现实当中应用的场景几乎没有,如果硬要说一种应用场景,那可能就是老六面试官让你设计一个无法对对象取地址的类:
#include <iostream>
using namespace std;class Coords//坐标类
{
public:Coords(int x = 0, int y = 0){_x = x;_y = y;}Coords* operator&(){// 这里也可以写断言、异常之类的,反正取地址就终止return nullptr;}const Coords* operator&() const{return nullptr;}
private:int _x;int _y;
};int main()
{Coords c1;const Coords c2;cout << &c1 << " " << &c2 << endl;return 0;
}
相关文章:
C++——类和对象[中]
0.关注博主有更多知识 C知识合集 目录 1.类的默认成员函数 2.构造函数和析构函数基础 3.构造函数进阶 4.析构函数进阶 5.拷贝构造函数 6.运算符重载 7.日期类 7.1输入&输出&友元函数 8.赋值运算符重载 9.const成员函数 9.1日期类完整代码 10.取地址重载 …...
Symbol.iterator和Symbol.asyncIterator
Symbol是什么? symbol是ES6标准中新增的一种基本数据类型,symbol 的值是通过 Symbol()函数返回的,每一个 symbol 的值都是唯一的,即使传入相同的描述值。 注:Symbol 函数不允许通过 new 的方式调用 Symbol的作用是什…...
忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的司机”
忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的sj” 一头白发,满山青葱 在那斑驳的物件褶皱中,透过泛黄的相片,掩藏着岁月的冲刷和青葱的时光。曾经的青年早已经不复年轻,但是那份热爱…...
Python中True、False、None的判断(避坑)
2.4 Python中True、False、None的判断 在Python中,所有的空值和0在作为条件表达式时,隐式的进行bool转换后都是False,比如:空列表:[]、空字符串:‘’、空字典:{}等等。 from icecream import …...
Spring Bean定义有哪些方式?
概述 对于学习Spring的兄弟姐妹来说,觉得这个问题很熟悉,若是要把它回答得很清楚,却是很为难?平时写代码的时候,不会在意这些概念问题,但面试时这个问题出现的频率却是很高,所以还是必须要掌握…...
JVM内存模型的演变
1,背景 class文件、类的加载过程。我们的class文件就要进入到JVM内存里,我们沿着经典的JDK1.6,JDK1.7,JDK1.8看看在其中都经历了哪些改变 概念的统一: 方法区: 方法区可以看作是JVM逻辑上管理一片区域的…...
DataX3同步Mysql数据库数据到Mysql数据库和DataX3同步mysql数据库数据到Starrocks数据库
DataX3同步Mysql数据库数据到Mysql数据库和DataX3同步mysql数据库数据到Starrocks 一、认识DataX二、DataX3概览三、DataX3框架设计四、DataX3插件体系五、DataX3核心架构六、DataX 3六大核心优势1.可靠的数据质量监控2.丰富的数据转换功能3.精准的速度控制4.强劲的同步性能5.健…...
你是否曾经为自己写的代码而感到懊恼?那如何才能写出高质量代码呢?
这里写目录标题 一、 前言二、高质量代码的特征三、编程实践技巧1. 遵循编码规范2. 使用有意义的变量名和函数名3. 减少代码重复4. 使用注释5. 编写单元测试6. 使用设计模式7. 使用版本控制工具8. 保持代码简洁9. 优化代码性能10. 学习和借鉴他人的代码总结 一、 前言 写出高质…...
常用 Composition API【VUE3】
二、常用 Composition API 7. 计算属性与监视 7.1 computed函数 与Vue2.x中computed配置功能一致写法 <template><h1>一个人的信息</h1>姓:<input type"text" v-model"person.firstName"><br><br>名&a…...
--商业模式--
O2O O2O,网络用语中指Online To Offline的缩写,即在线离线/线上到线下,是指将线下的商务机会与互联网结合,让互联网成为线下交易的平台。 O2O概念最早来源于美国。O2O的概念非常广泛,既可涉及到线上,又可…...
JavaWeb《HTML基础标签》
本笔记学习于Acwing平台 MDN官方文档https://developer.mozilla.org/zh-CN/ 目录 1. html文件结构 2. 文本标签 3. 图片 4. 音频和视频 5. 超链接 6. 表单 7. 列表 8. 表格 9. 语义标签 10. 特殊符号 1. html文件结构 文档结构 html的所有标签为树形结构ÿ…...
ChatGpt 能取代人类吗?
目录 前言 一、ChatGpt是什么? 二、ChatGpt能做什么 总结 前言 随着人工智能的不断发展,很多人都开启了学习机器学习,以及现在ChatGpt的出现,对人类社会带来了很多变化。 智能化交流方式:ChatGpt的出现为人们提供了…...
PHP内存溢出Allowed memory size of 解决办法
以前追踪过这个问题,但是那个时候工具用的不太好,没看的这么细,这次搞的比较细,修正了偶以前的看法 .于是写小文一篇总结一下. PHP偶尔会爆一下如下 错误Allowed memory size of xxx bytes exhausted at xxx:xxx (tried to allocate xxx bytes) 不想看原理的,直接跳到最后…...
重回代码,学习总结
回顾加总结 2021年 自动化测试 1.ETL 数据库开发维护(oracle pl/sql) 2.自动化测试(javaseleniumcucumber) 2022年 功能测试 1.功能测试(学习测试用例,postman测试) 2.性能测试(jmeter初学) 2023年 测试开发 1.学习了…...
【Leetcode -86.分隔链表 -92.反转链表Ⅱ】
Leetcode Leetcode -86.分隔链表Leetcode -92.反转链表Ⅱ Leetcode -86.分隔链表 题目:给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每…...
算法记录 | 48 动态规划
198.打家劫舍 思路: 1.确定dp数组(dp table)以及下标的含义:dp[i]:前 i 间房屋所能偷窃到的最高金额。 2.确定递推公式:dp[i] max(dp[i - 2] nums[i-1], dp[i - 1]) i间房屋的最后一个房子是nums[i−…...
CRM部署Always on 后 CRM报无法更新数据库,数据库只读,且读写分离不正常
CRM部署Always on 后 CRM报无法更新数据库,数据库只读,读写分离不正常 问题描述背景信息问题原因解决方案 问题描述 CRM部署Always on 后 CRM报无法更新数据库,数据库只读 读写分离不正常,出现错乱链接。 背景信息 1.2个节点配置SQL serve…...
麓言信息设计创意思维,打开设计师思路
在现在快速发展的时代,信息纷杂繁琐,如果一个设计不能让人眼前一亮,印象深刻,只会沦为平凡作品,无亮点无用处。正所谓,无设计不创意,这句口号正是喊出对设计的要求。 伴随着时代的发展、…...
POJ3704 括号匹配问题 递归方法
目录 题目 算法 完整代码 题目 参考 递归: https://blog.csdn.net/qq_45272251/article/details/103257953 利用了递归, 但思路稍复杂了 循环: https://blog.csdn.net/weixin_50340097/article/details/114579805 (看起来是递归其实是循环. 每次递归其实是循环内一次迭…...
leetcode — JavaScript专题(三):完全相等的 JSON 字符串、复合函数、 分组、柯里化、将对象转换为 JSON 字符串
专栏声明:只求用最简单的,容易理解的方法通过,不求优化,不喜勿喷 2628. 完全相等的 JSON 字符串 题面 给定两个对象 o1 和 o2 ,请你检查它们是否 完全相等 。 对于两个 完全相等 的对象,它们必须包含相…...
OGNL 的表达式
目录 概念 基本原理 基本语法 1、访问Root区域对象基本语法 2、访问Context区域对象基本语法 符号含义 概念 Object-Graph Navigation Language 对象-图形导航语言, 主要用于访问对象的数据和方法。 基本原理 主要由3部分构成:1.OGNL引擎 …...
JAVA面试中遇到的那些坑,80%的人都种过招
面试,是很多学完Java开发的人不得不面对的问题。小编经常听到学员抱怨,明明觉得自己学的不错,为什么到了面试的时候就凉凉了?为什么有的面试官会一直问业务层面的问题,让人措手不及? 其实,我们在学习Java知识的同时…...
【测试开发】单元测试、基准测试和性能分析(以 Go testing 为例)
一、为什么需要测试🤔️ 你写不出 bug-free 的代码。你认为自己写出了 bug-free 的代码,但它在你意想不到的地方出错了。你觉得自己写出了永不出错的代码,但它的性能十分糟糕。 二、在开发过程中做好测试(理想情况下)…...
linux中一条命令查询当前端口的进程,然后拿到进程pid,作为另一条杀死进程的参数
1. 可以使用lsof命令来查询端口对应的进程,然后使用awk命令提取PID,最后将其作为另一条命令的参数。 例如,如果要查询端口为8080的进程: lsof -i :8080 | awk NR2{print $2}其中,-i选项指定查询网络连接,…...
程序员找工作难吗?我用亲身经历来告诉大家
我看到很多同学说今年的程序员找工作难。我的心里也有一定预期,但直到我出来之后才真正地感受到这股寒冬有多么凛冽。 一个外包公司有四五个招聘人员,然后外包公司有十来个,一个公司的岗位会分发给这些各个不同的外包公司。所以你看到我沟通…...
【Web服务】HTTP和DNS重要知识
304状态码 HTTP状态码中的304状态码表示"未修改",通常在客户端发起了一个带有If-Modified-Since头部的GET请求时会得到这个响应。服务器通过比较If-Modified-Since头部指定的时间戳和资源的最后修改时间来判断资源是否被修改过,如果没有修改则…...
【C++】-关于类和对象的默认成员函数(中)-拷贝构造函数和赋值运算符重载函数
💖作者:小树苗渴望变成参天大树 ❤️🩹作者宣言:认真写好每一篇博客 💨作者gitee:gitee 💞作者专栏:C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点…...
c++11上篇
c11 1.C11简介2.列表初始化2.1 {}初始化2.2 std::initializer_list 3.变量类型推导3.1 auto3.2 decltype3.3 nullptr 4.范围for循环5.final与override6.智能指针7.新增加容器---静态数组array、forward_list以及unordered系列8.默认成员函数控制9.右值引…...
异构无线传感器网络路由算法研究(Matlab代码实现)
目录 💥1 概述 📚2 运行结果 🎉3 参考文献 👨💻4 Matlab代码 💥1 概述 无线传感器网络(Wireless Sensor Networks, WSN)是一种新型的融合传感器、计算机、通信等多学科的信息获取和处理技术的网络,…...
MySQL数据库——MySQL TRUNCATE:清空表记录
MySQL 提供了 DELETE 和 TRUNCATE 关键字来删除表中的数据。下面主要讲解一下 TRUNCATE 关键字的使用。 TRUNCATE 关键字用于完全清空一个表。其语法格式如下: TRUNCATE [TABLE] 表名 其中,TABLE 关键字可省略。 例 1 新建表 tb_student_course&…...
软件定制 开发/seo的含义是什么意思
jpg和png格式的图片在日常生活中是使用广泛的两种图片,那么如果想将这两种不同格式的静态图片合成一张gif动态图片的话,该如何来进行操作呢?下面教大家使用gif合成工具,轻松在线合成png和jpg图片动图制作的方法,有需求…...
太原网站建设 网站制作/成都百度搜索排名优化
1、首先查看数据库有没有Classes数据库2、我们看到并没有,我们就可以创建数据库注意:在这个数据库中,我们要输入中文数据,所以在创建数据库时,编码格式是utf8形式3、创建成功后,我们要开始使用数据库4、在这…...
wordpress主题列表封面/西安seo教程
我正在尝试平均化熊猫的一组数据。csv文件中的数据。我有一个系列节目叫“轨道”。在前面的阶段中,我使用了dropna()方法来删除在读取csv文件时导入的一些空白行。在我使用的方法是平均5行以上的列。我不能使用滚动平均法,因为我希望使用当前值之前的两行…...
wordpress 定时间隔/赚钱平台
手机APP软件怎么做,就现目前的互联网市场环境来看,主要分为两种,自建技术团队以及选择专业APP外包公司开发,企业商家只有选择合适的开发方式才能开发出更高质量的APP应用程序,下面杭州APP开发公司-七猫网络跟大家详细讲…...
鳌江网站建设/东莞seo网络培训
http://blog.csdn.net/u010509774/article/details/50593231一、rpm包安装方式步骤:1、找到相应的软件包,比如soft.version.rpm,下载到本机某个目录;2、打开一个终端,su -成root用户;3、cd soft.version.rp…...
2018做网站前景好么/正规教育培训机构
思路:回顾了下网络流中最简单暴力的一个方法:Ford-Fulkerson算法。 本文不再讲解具体算法原理,直接上代码(我这里采用的邻接矩阵,希望各位不要学我,还是用邻接表比较稳哦): #inclu…...