C++面向对象特点:继承、封装、多态
构造函数
任务是初始化对象的内存空间。
注意:
-
新对象被创建,由编译器自动调用构造函数,且在对象的生命周期之内只调动一次。不可以手动调用,类的成员方法依赖对象调用,即在构造函数完成后;
-
构造函数的名字和类名相同,没有返回值;
-
构造函数可以重载,实参决定了调用哪个构造函数;没有显示定义构造函数,编译器会提供一个默认的构造函数;
-
构造函数不能用const来修饰。const可以修饰类的成员函数,但是该函数不能修改数据成员。构造函数也属于类的成员函数,但是构造函数是要修改类的成员变量、给成员变量做初始化,所以类的构造函数不能申明成const类型的。
-
构造函数不能用static修饰,因为构造函数只关联一个对象而被调动,声明成static是没有意义的;
-
构造函数的初始化列表:初始化列表仅用于初始化数据成员,初始化只能初始化一次,不指定这些成员的初始化顺序。数据成员在类中的定义顺序就是在参数列表中的初始化顺序;
-
在构造函数体内可以使用this指针,在初始化列表中不能使用this,因为在初始化列表阶段还不知道对象的成员内存布局,无法访问;
-
默认的构造函数什么事都不做,用户提供了,则系统就不会再提供。默认的构造函数调用方式:
Date d1(); //函数声明
Date d2; //对象生成,调用默认的构造函数 -
在类中包含以下成员,必须要在初始化列表进行初始化:
(1)const修饰的成员变量:const具有常属性,必须初始化,它不能在别的地方被再次赋值修改。
(2)引用的数据成员:因为引用必须在定义时初始化,且不能重新赋值;
(3)类类型的成员(该类没有默认的构造函数):因为使用初始化列表不用调用默认的构造函数,而是直接使用拷贝构造函数初始化; -
当构造函数参数表中并不全部使用缺省参数时,具有缺省值的参数必须放置于参数表的最后。函数的默认值从右向左依次赋之,自左向右进行实参与形参的匹配。
以下:
coordinates (int i, int j=100) 的参数表是对的;
coordinates (int i=100, int j) 的参数表是错的。 -
对于只有一个参数的构造函数它默认的具有一种类型转化的功能——即使用内置类型对自定义类型进行赋值。 explicit(显示的)关键字修饰构造函数/拷贝构造函数,禁止隐式生成对象。
class Date
{
public:
Date(int year)
:_year(year)
{}
explicit Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2018);
d1 = 2019; // 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
}
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
析构函数
与构造函数功能相反,在对象被销毁时,由编译器自动调用,释放对象所占的资源。
注意:
- 析构函数名:~类名()
- 析构函数前对象存在,可以手动调用,但不建议这样做。手动调用析构函数会退化成普通方法,系统还会调用一次析构函数,造成资源再释放程序崩溃;
- 默认的析构函数什么事都不做;
- 没有参数、没有返回值;
- 一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省的析构函数;
- 对象生命周期结束时,C++编译系统系统自动调用析构函数;
- 注意析构函数体内并不是删除对象,而是做一些清理工作;
- 不可以重载;
拷贝构造函数
用一个已存在的对象来生成相同类型的新对象
注意:
- 它是构造函数的重载;
- 以下情况都会调用拷贝构造函数:
1.一个对象以值传递的方式传入函数体或从函数返回。
2.一个对象需要通过另外一个对象进行初始化。 - 它的参数必须使用同类型对象的引用传递,因为对象以值传递的方式进入函数体就会调用拷贝构造函数,这样就会形成无限递归导致栈溢出,程序崩溃。
- 如果没有显示定义,系统会自动合成一个默认的拷贝构造函数,默认的拷贝构造函数会依次拷贝类的数据成员完成初始化。
- 默认的拷贝构造函数是浅拷贝、有指针类型要考虑深拷贝;
- 如果不去改变实参的值的话,参数不加const的效果和加const的效果是一样的,而且不加const编译器也不会报错,因为函数的形参是引用,则调用函数时不需要复制实参,函数是直接访问调用函数中的实参变量的。但是为了整个程序的安全,还是加上const,防止对实参的意外修改。
赋值运算符重载函数
用一个已存在的对象给另一个已存在的对象赋值。
-
参数:
一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用
加const是因为:
(1)防止修改实参的值;
(2)加上const,可以接收隐式生成的临时量(常量)。对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
用引用是因为:这样可以避免在函数调用时对实参的一次拷贝,提高了效率。 -
返回值
一般地,返回值是类类型的引用原因是
(1)这样在函数返回时避免一次拷贝,提高了效率。
(2)更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的临时对象,然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。 -
程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。
-
赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。
之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?
为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的非静态成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。 -
设计流程:1、判断自赋值;2、释放旧资源;3、开辟新资源;4、赋值。
-
赋值运算符重载函数要避免自赋值
对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(如中的if (this != &rhs)一句)。
为什么要避免自赋值呢?
(1)为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。
(2)如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?
所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。