前言:在之前,我们对类和对象的上篇进行了讲解,今天我们我将给大家带来的是类和对象中篇的学习,继续深入探讨【C++】中类和对象的相关知识!!!

目录
首先,我们直接给出类中有哪六类默认1. 类的6个默认成员函数
此时,我们可以会想到,为什么要有这些默认成员函数?这些默认成员函数会带来什么作用呢?
要弄清楚这个问题,我们先来引入一个“空类”的概念。
👉空类的定义:如果一个类中什么成员都没有,即一个类中没有成员变量,也没有成员函数,简称为空类。定义形式如下:
class Date
{
};
通过如上代码发现,空类中什么都没有放,此时请大家认真思考一下,👉空类中难道真的什么都没有吗?
因此,这就给我们解答了为什么要引入这六个默认成员函数,具体大家可以这样理解:
具体还可以像如下这样分,大家可以直观的感受各个函数的区别与功能:
至此,这个六个默认成员函数的作用与由来便给大家将清楚了,接下来我们逐个去认识!!!
首先,在正式的给出【构造函数】具体的概念前,我们通过代码的方式来为大家做个前情铺垫,这样大家可以直观的感受,通过之前类和对象(上)的学习,我相信大家都能写出一个如下日期类,如果对其还有疑惑的话,可以参考【类和对象(上)】
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;
};
int main()
{
Date d1;
d1.Init(2023, 3, 15);
d1.Print();
Date d2;
d2.Init(2023, 2, 14);
d2.Print();
return 0;
}
对于上述这样一个日期类,当我们构造出来之后一般都会先对其进行“初始化”的操作:
但是有时候我们可能会忘记进行初始化操作,直接对对象进行操作,这时当我们不初始化就直接用可能就会出现问题:
当我们进行调试时也可以直观的看到:
因此,为了解决当我们构造出函数之后,未进行初始化就直接对对象进行操作的情况,【C++】就给出了今天我们将要学习的知识——构造函数。有了构造函数,当我们每创建完一个对象,就不用再去手动的调用【Init】函数,因为在创建对象时编译器会自动去调用构造函数对对象进行初始化。
有了上述的认知之后,在这里我给出构造函数的具体概念:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值,不能指定返回类型,即使是void也不行。
3. 对象实例化时编译器自动调用对应的构造函数。虽然在一般情况下,构造函数不被显式调用,而是在创建对象时自动被调用。但是并不是不能被显式调用。
4. 构造函数可以重载。
我们通过代码进行举例说明,此时我们已经创建出了一个【Date】类:
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(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;
};
int main()
{
Date d1; // 调用无参构造函数
//d1.Init(2023, 3, 15);
d1.Print();
Date d2(2023, 3, 15);// 调用带参的构造函数
//d2.Init(2023, 2, 14);
d2.Print();
Date d3();
d3.Print();
return 0;
}
解析:
此时当我们运行【Date.d1】和【Date.d2】时,集合上面说到的我们可以发现运行结果如下,传参就调用有参数的构造函数,不传参就调用不传参的构造函数:
而当我们此时运行【Date.d3】时,我们会发现程序出现了报错的情况:
👉此时就需要注意一点,如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 ,声明了【d3】函数,该函数无参,返回一个日期类型的对象
到这大家思考一下,当我们写出这两个代码时,是否可以进行合并为一个代码呢?
class Date
{
public:
Date()
{
_year = 1;
_month = 2;
_day = 3;
}
Date(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;
};
答案当然是可以的,那么我们要怎么做呢?这就需要用到之前学习的缺省参数的知识:
Date(int year=1 , int month=2 , int day=3 )
{
_year = year;
_month = month;
_day = day;
}
此时,当我们运行它时,如果开始我们不传参,就用默认的,如果传了,就用我们传的。
这跟之前那个比较就显得很“高级”,不仅如此,这个功能相对比上面那种写法还更多,因此这里支持缺省参数,例如:
因此,这里就给大家说明一个点:一个类从大部分场景来说,当能提供构造函数的情况下尽可能提供全缺省或者至少是半缺省,就会显得十分好用。
紧接着就是一点小细节的问题,大家注意以下这两个函数可以同时存在吗?
我们浅浅的分析一波:
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
在这时,我们将构造函数删除掉了,当我们去进编译时,我们会发现可以编译通过。为什么呢?因为之前说过默认会生成一个构造函数。
那么此时大家是否会有这样的想法,既然编译器自己就有默认的,那么是不是我们就不需要在去构造了呢?事实真的是这样的吗?当我们的代码运行起来时,大家可以看到下图:
为什么这是随机值呢?
接下来我们还是通过下图代码来进行相关的理解:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
return 0;
}
👉解析:
我们运行程序,结果如下:
此时,我们就会注意到自定义类型不写构造函数就没法初始化,这不是一个妥妥的【bug】吗?
因此,祖师爷呢在后来也发现了这个问题,并在C++11中针对内置类型不初始化的缺陷打了一个补丁,即:内置类型成员变量在 类中声明时可以给默认值。
什么意思呢?意思就是如果你不写构造函数,那么就默认用这个缺省值;如果你写构造函数了就不会用这个缺省值 。注意,这里不是初始化(千万要分辨清楚,这里没开空间哟!!!)
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
这里我们就需要引入有关于虚构函数的知识:
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
我们通过以上代码来做分析:
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?
下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
当我们运行程序之后,我们会发现结果是输出了【~Time()】,具体如下:
此时,问题就来了。在【main】方法中根本没有直接创建【Time】类的对象,为什么最后会调用【Time】类的析构函数?
👉注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?因此这就引出了拷贝构造函数的概念:
此时,我们在通过代码来进行直观的理解:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 15)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2008,2,5);
Date d2(d1);
d2.Print();
return 0;
}
解析:
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
根据上述的基本知识,在这里我们知道可以通过如下去写:
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
但是这样呢就会发生报错的情况,具体情况如下,如果语法强制这里编译不会通过,通过了这里它也会发生无穷递归的情况,就是不能使用传值传参,这里需要使用引用:
要理解上述问题我们需要了解这里为什么不能使用传值传参:
我们先来理解下面这几行代码的意思:
// 传值传参
void Func1(Date d)
{
}
// 传引用传参
void Func2(Date& d)
{
}
int main()
{
Func1(d1);
Func2(d1);
return 0;
}
👉解析:
这里的【func1】是传值传参,是一个拷贝,即理解为新开一片空间,把【d1】拷贝给【d】,传引用传参即【d】是【d1】的别名,形参是实参的拷贝。内置类型,编译器可以直接拷贝;自定义类型的拷贝,需要调用拷贝构造。
解析:
到这里我们在解释上述提到的为什么要是无穷递归:
大家想想,那是一个对象实例化,对象实例化就需要用到构造函数,对应的构造函数又是拷贝构造,调拷贝构造之前需要先传参,传值传参又是一个拷贝构造,拷贝构造又需要传参,这样的不断循环,最终就是无穷递归,因此编译器构造的不能是传值传参!!!
因此这里可以怎么做呢?答案是在这里我们可以使用引用的基本方法去解决这个问题,具体如下:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 16)
{
_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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
加引用之后要怎么理解呢?
同时还有一种写法也是拷贝构造,编译器也可以允许像如下这样去写:
int main()
{
Date d1;
Date d2(d1);
Date d3 = d1;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
程序运行结果如下:
👉注意:
这里有个小细节的地方问问大家,上面代码中,可以发现我们加入了【const】,大家知不知道为什么要加上这个【const】的?
对于为什么要加入【const】,我们还是以代码为例进行直观的了解,当我们不小心写反的时候,如果不加其中的【const】会出现什么情况呢?具体如下:
Date( Date & d)
{
d._year = _year;
d._month= _month;
d._day= _day;
}
当我们去编译这个程序时,却不会出现报错的情况。但是当我们一运行这个程序,结果就会出现报错的情况。
此时我们浅浅的分析一波:
因此如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用,加上【const】,传递过来的不管是不是加了【const】都可以进行接收,但是如果不加【const】就会引起权限放大的问题,编译器是不允许这种情况出现的。出现任何有可能导致 【d】的值被修改的语句,都会引发编译错误。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝这上面已经说过。
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
我们通过下列栈类的进行举例:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
上述代码当我们去运行时, 这里会发现下面的程序会崩溃掉。什么原因呢?这里就需要我们理解深拷贝去解决。
在上面我们已经浅浅的谈到过这个问题,会发生析构两次的问题,那么到底谁先析构呢?在这里我们仔细分析一下。
👉分析如下:
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
而对于深拷贝我们后面会具体去讲,这里我们先浅浅的谈一下:
对于上述的【Stack】进行浅拷贝就会导致两个栈对象指向了同一块空间了,所以才会出现崩溃的情况,那么深拷贝是怎么做的呢?其实深拷贝解决这个问题的原理就是让这两个对象各自拥有独立的空间。这样做对两个对象之间就不会互相影响了。
用代码浅浅的实现一下:
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_array = (DataType*)malloc(sizeof(DataType)*st._capacity);
if (nullptr == _array)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType)*st._size);
_size = st._size;
_capacity = st._capacity;
}
此时,我们会看到两个地址空间不同,此时问题就解决了:
5. 拷贝构造函数典型调用场景:
接下来我们一一进行分析,复制构造函数在以下三种情况下会被调用:
1) 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 【d2】。
Date d2(d1);
Date d2 = d1;
👉注意,上面说过这两条语句是等价的。第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
Date d1,d2; d1 = d2;
d1 = d2;
这条语句不会引发复制构造函数的调用,因为 【d1】早已生成,已经初始化过了。
2) 如果函数 【d1】的参数是类 【Date】的对象,那么当 【d1】被调用时,类 【Date】的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
class Date
{
public:
Date()
{};
Date(const Date& d1)
{
cout << "Time()" << endl;
}
};
void Func(Date d1)
{ }
int main()
{
Date d1;
Func(d1);
return 0;
}
输出结果为:
这是因为 Func 函数的形参 【d1】在初始化时调用了拷贝构造函数。
前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 【d1】 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。
3) 如果函数的返冋值是类 【Date】的对象,则函数返冋时,类 【Date】的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:
class Date
{
public:
Date(int year = 2023, int month = 3, int day = 16)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d):" << this << endl;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2008, 2, 15);
Test(d1);
d1.Print();
return 0;
}
程序的输出结果是:
调用了 Test函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是return 语句所返回的 【temp】。复制构造函数在之前确实完成了复制的工作,所以函数的返回值为赋值的。
所以,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。
在讲解这个知识点之前我们先了解一个以及回顾一下之前的:
初始化和赋值的区别:
当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。
对于简单的类,默认的赋值运算符一般就够用了,我们也没有必要再显式地重载它。但是当类持有其它资源时,例如动态分配的内存、打开的文件、指向其他数据的指针等,默认的赋值运算符就不能处理了,我们必须显式地重载它,这样才能将原有对象的所有数据都赋值给新对象。
基于上述情况,我们这里就引出了关于赋值运算符重载的概念。
在学习正式学习赋值运算符重载,我们先来学习运算符重载的基本知识,有了对这个理解,当我们讲解赋值运算符重载大家才会轻松上手,因为赋值运算符重载是属于运算符重载的。
为什么引入运算符重载:
还是以我们写过的日期类来举例子,在平常生活中我们是不是经常比较两个日期啊,想着多少天是几号,多少多少号跟今天相差几天这种情况。那么是否支持进行比较判别呢?答案当然是支持:,此时我们用日期类来实例化出两个对象,具体如下:
int main()
{
Date d1(2008, 2, 15);
Date d2(2023, 3, 16);
return 0;
}
当我们实例化出两个对象d1,d2后,此时大家就会思考一个问题,现在我们想比较这两个对象是否相等,该怎么办呢?根据我们之前学过的知识,当然是用函数来封装它呀!写了功能函数就可以了。
bool Equal(const Date& x1, const Date& x2)
{
//......
}
这时大家会有这样的想法,这样的方法可行肯定是可行的,但是有没有更加直观的呢?就像我们下面这样去写:
d1 == d2;
为此,当C++引入了运算符重载之后,再去判断就直接像上面的代码这样去操作。但是我们要知道一点那就是自定义类型是不能直接作为这些操作符的操作数的,它不想内置类型一样可以直接进行操作。具体原因如下:
为了解决这个问题,就引入了运算符重载的概念!!!使得可以像【d1=d2】这样去进行操作。
- 函数名字为:关键字operator后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator操作符(参数列表)
对比的逻辑思路也很简单,只需比较各个成员变量是否相等。即如下代码:
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
但是此时当我们编译时,会出现报错的情况:
咦....什么原因呢这是?我们浅浅的分析一波:
那么如何解决呢?在这里我给出几种解决方法:
1.第一种就是我们刚才已经把问题 分析出来了,我们就会想到既然你是私有的我无法访问,那我把你直接变为公有的不就可以了吗?因此第一种方法就是先全部都变为公有的,即屏蔽我们的【private】
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2008, 2, 15);
Date d2(2023, 3, 16);
operator ==(d1, d2);
d1 == d2; // 转换成去调用这个operator==(d1, d2);
return 0;
}
此时在当我们去运行代码的时候,就可以正常的运行了
当程序能够正常编译后,我们就会想着去运行,既然这个是函数,那我们可以打印一下这个结果,会有一个返回值,但是当我们运行时又会出现报错:
cout << operator==(d1, d2)<< endl;
cout << d1 == d2 << endl;
这又是什么原因呢?答案很简单,是因为【<<】的优先级比【==】高,因此为了限制这种情况我们需要加个括号:
cout << operator==(d1, d2) << endl;
cout << (d1 == d2) << endl;
此时,当我们再次运行时,结果显示就为正确:
从上可以看出这种办法可以解决这个问题,但是大家是否能够发现这样做存在的问题呢?上述方式,我们把全部都变为了公有,那么问题来了,封装性如何保证?
2.因此,基于以上方法存在的问题,我们给出了第二种方法。我们可以把这个函数重载到类里面,干脆重载成成员函数。
但是当我们放到类里面去后,我们再次运行代码,咦....怎么出错了呢?你不是说放到类里面去可以吗?别急,我们先看报错报的是什么?
👉它说我们的参数太多了,什么意思呢?
==】运算符,正常情况下只有两个操作数,所以只需要两个参数就够了。但是大家是否还记得我们默认的还有一个【*this】这个隐藏的参数呀!!!因此这里只需给一个参数就可以了。
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
此时,我们就需要这样去打印:
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
这样我们再去运行程序,此时程序就正常运行的了。
👉注意:
接下来,我们在多写几个来进行练习。
1.第一个先写一个日期类的【<】
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;
}
else
{
return false;
}
}
或者直接这样写:
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
以上这两种都是可以的,那么到底是不是呢?我们直接运行程序,可以看到结果是正确的:
如果我们还想要实现其他的话,是不是看着麻烦呀!这门一大堆的东西。其实根本没必要在像以上这样去写了,上面我们已经写好了【==】和【<】,在写其他的直接复用这个就可以了。
2.例如写个【!=】,我们可以这样去写:
bool operator!=(const Date& d)
{
return !(*this == d);
}
3.对于【<=】
// d1 <= d2
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
4.对于【>】
// d1 > d2
bool operator>(const Date& d)
{
return !(*this <= d);
}
5.对于【>=】
// d1 >= d2
bool operator>=(const Date& d)
{
return !(*this < d);
}
当我们像这样做是不是就会很大程度上的减少我们的工作量呀!!!
接下来我们正式的介绍关于赋值运算符重载的知识。
1. 赋值运算符重载格式
首先我们先来看看【=】即赋值怎么操作的把。通过上述的知识学习,我们不难写出这样的一个代码
//d1 = d2;
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
当我们写出这样的代码时,接下来我们就需要去验证这个代码的正确性,紧接着我们直接运行程序,运行结果如下:
从上可以得出代码的运行结果是正确的,我们在调试去看看是否真的这样。
上述我们也可以发现程序有去调对应的函数,同时记住一点,在转化的时候并不是让编译器把它给改了,而是编译的时候编译器识别,它看你有没有实现赋值,有实现赋值就转化为去【call】这个函数
然后根绝我们之前的学习经历,赋值往往会有连续赋值这一说法,就像【i=j=k】这样,不断的去连续赋值,然而上述代码当我们去进行这样的操作的时候,我们会发现是编译不通过的。
遇到困难不要害怕我们浅浅的分析一波:
因此最终这里却是【d2 = d3】调用了重载函数,而我们上面实现的函数并没有返回值。因此这里就会出现报错的情况,那么怎么解决呢?很简单,我们只需要在这里添加个返回值即可,具体如下:
// 返回值为了支持连续赋值,保持运算符的特性
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
最终的运行结果如下:
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数,这个我们之前已经说过了,不再具体讲解。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
既然如此,我们是不是可以理解为上述的那个日期类的赋值就不需要我们自己去写相应的程序,编译器自动生成的是不是就可以帮我们完成任务。那当我们屏蔽时,会不会正常运行呢?结果如下:
可以是可以,但是这里的问题是不是就跟上面讲到的拷贝构造一样了,一样的,这里就不讲了。
接下来,我们在理解以下代码是什么意思:
Date d5 = d1;// 拷贝构造
Date d6(d1);// 拷贝构造
在类中,如果你不希望某些数据被修改,可以使用
const关键字加以限定。const 可以用来修饰成员变量和成员函数。
const成员变量
const成员函数(常成员函数)
【const 】成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。【const 】成员函数也称为常成员函数。
还是通过代码来进行直观的举例说明,例如当我们运行下列代码时,程序时可以正常运行的,这个大家学到这了应该不陌生了:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2023, 3, 17);
d1.Print();
return 0;
}
然而当我们这样写的时候呢?即加入【const】时:
const Date d2(2008, 1, 13);
d2.Print();
此时当我们再去编译的时候,我们发现程序就会出现报错的情况:
还是浅浅的分析一下:
问题分析,如何解决呢?可以看到如果不想让权限放大,我们必须在【*】的前面加上【const 】由于【this】是隐形的,所以编译器规定在函数括号后面加【const】来表示此对象不可被修改。即如下表示方法:
void Print()const
{
cout << "Print()" << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
在当我们去运行代码时,就不会出现报错的情况了。
这里回答几个小问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
最后再来区分一下 const 的位置:
总结:
到底要不要使用【const】去修饰成员函数,就看你函数中的变量需不需被修改,如果不希望被修改,则加上即可。
取地址成员函数也是''类的六大默认成员函数''之一。其分为两种,普通取地址操作符、【const】取地址操作符。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成,用编译器默认生成的取地址的重载即可
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cout << &d1 << endl;
return 0;
}
小结:
总结:
最后,如果本文对你有帮助的话,记得点赞三连哟!!!
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev
我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调
我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser
我正在尝试用ruby中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin