今天继续C++类与对象的学习
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } private: int _year; int _month; int _day; }; void main() { Date d1; }
概念:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
那么上面段话中有几个点呢,首先与类名(classname)相同的函数,就是构造函数,它主要是为了在咱们实例化对象的时候,对其进行初始化,让对象有一个咱们想要的初始值,并且只会在实例化对象的时候自动调用,且只调用一次,初始化完毕,对象实例完成后不会再次调用。
特性:
1.函数名与类名相同
2.没有返回值
3.在实例化对象时自动调用
4.构造函数可以重载
5.在我们没有显式定义构造函数时,编译器会自动生成一个默认的无参构造函数,如果我们显示定义了一个构造函数,编译器就不会生成这个默认的无参构造函数
6.默认的构造函数只能有一个
7.编译器会自动调用自定义类型自己的构造函数
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } ~Date() {} private: int _year; int _month; int _day; }; void main() { Date d1; }
概念:
在对象的生命周期结束时编译器自动调用析构函数,释放对象空间,完成资源清理的工作
特性:
1.函数名是构造函数前加一个 " ~ "
2.无参数无返回值
3.一个类只有一个析构函数,若没有显式定义,则编译器自动生成一个默认的析构函数
4.对象生命周期结束时,编译器自动调用析构函数
5.编译器会自动调用自定义类型自己的析构函数
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } ~Date() {} private: int _year; int _month; int _day; }; void main() { Date d1; Date d2 = d1; Date d2(d1); }
概念:
与构造函数名相同,但是会存在一个类类型的引用传参,因为我们是拷贝构造,所以我们一般用const修饰。
特性:
1.拷贝构造函数是构造函数的一种重载
2.拷贝构造函数的参数只有一个并且必须用引用传参,使用传值传参会引发无穷递归
无穷递归出现的解释:
因为拷贝构造定义了之后,我们在传值的时候相当于要构建一个形参对象d,但是在构建d时因为我们是在用一个对象来构建另一个对象,又会调用拷贝构造,如此往复,就出现了无限递归,那么为什么引用传参可以呢,因为引用只是我现在对象的一个别名,d1被起了另外的一个名字d,所以不需要去构建一个形参对象,然后将d也就是d1的值赋值给拷贝对象d2,来实现拷贝构造函数。
3.没有显式定义的话,系统会自动生成一个默认的拷贝构造函数,但是默认的拷贝构造函数只是按内存存储字节序来进行拷贝,这种拷贝我们称为浅拷贝,或者值拷贝
4.在编译器生成的默认拷贝构造函数时可以完成值拷贝时,我们就不必去重写拷贝构造函数,但是如果是字符串类呢,我们如果还使用默认的构造拷贝函数,我们就会发现一个问题,拷贝出来的对象跟被拷贝的对象的成员变量的值是同一个地址同一个值,并没有开辟新的空间
class String { public: String(const char* str = "jack") { _str = (char*)malloc(strlen(str) + 1); strcpy(_str, str); } ~String() { cout << "~String()" << endl; free(_str); } private: char* _str; }; void main() { String s1("hello"); String s2(s1); }
这个程序运行之后大家会发现程序崩溃了,那么为什么会崩溃呢,上面说到了,因为它只是把内存地址拷贝过去,没有重新开辟空间,导致一处空间被连续释放两次,所以我们才会看到程序崩溃。
概念:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:[返回值类型] operator [操作符] (参数列表)
特性:
1.参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
赋值运算符是双目运算符,我们需要连个操作数来实现,那么请看下面的代码。
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date operator = (const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; } ~Date() {} private: int _year; int _month; int _day; }; void main() { Date d1; Date d2 = d1; Date d2(d1); Date d3; d3 = d2 = d1; }
上面写到的赋值运算符重载大家疑惑可能的就是返回值类型和返回值,还有参数个数。
返回值类型——为什么是一个类名(classname)呢?这就要从我们平时如何使用“ = ”来解释了,x = m,这是大家都可以想到的,但是有时候我们是不是还会连等,例如:x = m = k,因为我们都知道,运算是从右到左的,如果是这样我们设置没有返回值,那么我们在 m = k 之后又用什么去给 x 赋值呢?所以我们需要返回一个值去给其赋值,那么为什么是类名,因为我们常用的都是对象之间的赋值,它的作用跟拷贝构造相似,但是需要提前实例化好对象比如说上面的d3。
返回值和参数个数咱们放一起说
上一篇文章咱们提到了this指针,我们知道在类内的非静态成员函数,他们又有一个隐藏的this指针,即:
Date operator = (Date* const this,const Date& d) { this->_year = d._year; this->_month = d._month; this->_day = d._day; return *this; }
这个就是函数实际的样子,这个隐藏的this就是咱们d2,d3在调用赋值运算符重载的时候,这个this就指向了它们(左操作数),完成了对它们的赋值,另外一个参数就是有操作数也就是d1,d2。
返回值为什么是 *this 呢,因为我们this指针指向了左操作数,这时候它里面保存的是地址,不是左操作数对象,所以我们需要将其解引用然返回,这个时候它的类型才是Date。
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date operator = (const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; } void Show_Date() { cout<<" Show_Date()"<<endl; cout<<_year<<" - "<<_month<<" - "<<_day<<" - "<< endl; } void Show_Date()const { cout<<" Show_Date() const"<<endl; cout<<_year<<" - "<<_month<<" - "<<_day<<" - "<< endl; } ~Date() {} private: int _year; int _month; int _day; }; void main() { Date d1; Date d2 = d1; Date d2(d1); Date d3; const Date d4; d3 = d2 = d1; d1.Show_Date(); d4.Show_Date(); }
如果我们给Show_Date()加上const,会发生什么呢?d1,和d4的调用结果又是什么?
void Show_Date(const Date* const this)const { cout<<" Show_Date() const"<<endl; cout<<_year<<" - "<<_month<<" - "<<_day<<" - "<< endl; }
这里的const就是用来修饰this指针,所以如果我们在这里面对_year,_month,_day进行修改的话,是不可以进行修改的。
我们运行结束后可以知道,常对象调用的就是常含数,普通对象调用的就是普通函数。
这里我们补充一下:
const对象不可以调用非const成员函数
非const对象可以调用const成员函数
const成员函数内不可以调用其它的非const成员函数
非const成员函数内可以调用其它的const成员函数
class Date { public: Date() { _year = 0; _month = 0; _day = 0; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date operator = (const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; } Date* operator&() { return this ; } const Date* operator&()const { return this ; } ~Date() {} private: int _year; int _month; int _day; }; void main() { Date d1; Date d2 = d1; Date d2(d1); Date d3; d3 = d2 = d1; }
这两个一般来说我们是不需要重载的,除非有一些特殊的需求,不然就直接使用默认的取地址和const取地址符。
今天学习的内容,是类内的一部分主要函数,也是每个类中都存在的成员函数,是大家需要熟练掌握,并且信手拈来的六个成员函数。希望这些咱整理的东西对大家有学习借鉴作用。