什么是默认函数?
默认函数就是当你使用这个类对象时,这个类会自动调用的函数C++中有六个默认成员函数,并且作用各不相同,下面我们来一一进行介绍
什么是构造函数?构造函数是干什么的?
什么是析构函数?析构函数是干什么的?
我们以栈为例,每一次我们在使用栈的时候我们都要先定义它,并且每次在使用完栈之后还要去销毁它,为了释放空间防止内存泄漏。然而我们在实际操作时经常会忘记去定义,特别是最后的销毁。祖师爷为了防止你的遗忘,专门设计出了这两个默认函数来减小你的压力
构造函数就是在定义对象的同时,就对成员变量进行初始化。
#include
using namespace std;
class Date
{
public:
//过去
/*void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//现在
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(2002, 01, 01);
Date D1(2002, 01, 01);
D1.Print();
return 0;
}
一般情况下,我们会定义一个Init函数,再定义完一个对象后,再调用Init函数对成员变量进行初始化,这样显得有些繁琐,那么可不可以选择在定义对象时就对成员变量进行初始化呢?
答案是肯定的,我们可以通过构造函数来对成员变量进行初始化。
默认构造函数,其实类中是有默认构造函数的,就是在你创建这个对象时,它就会自动调用这个函数,对成员变量进行初始化,只不过有时候它默认生成的值并不是我们想要的,所以就需要我们自己去定义。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造(成员)函数,一旦用户显式定义编译器将不再生成。
无论何时只要类的对象被创建,就会执行构造函数。C++规定内置类型不做处理,自定义类型会去调用而它的默认构造。但是有些也会对内置类型做处理,但那只是编译器的个性化行为,C++并没有规定。
构造函数怎么去定义呢?首先构造函数的取名就与其他函数不同,它的名字与类名相同,且不需要返回值,这里的不需要返回值是指他不需要加返回类型,void也不用加。
什么时候自己写构造函数呢?
如果类成员函数中有自定义类型就要去自己写构造函数,如果类成员全是自定义类型就可以选择不写。
构造函数参数可以给缺省值,且可以重载。
还有一个问题当我们在定义对象时调用无参的构造函数,为什么不写成这样?
Date D1();
因为这样编译器很可能会把它当做返回类型为Date的函数声明。但是如果我们传的是值
Date D1(2002, 01, 01);
编译器就会识别出来它是变量的初始化而不是函数声明。
注意:区分一些概念,我们不写编译器自己生成的是默认成员函数,也属于默认构造函数,默认构造函数是一个大类包括我们自己显示写的和编译器自己生成的。
我们不传参自动调用的构造函数就是默认构造函数,必须要传参的不属于默认构造函数,属于构造函数。
默认构造函数有三种:一种是编译器自己生成的,一种是参数全缺省的,另一种是无参的构造函数,且这三个默认构造函数只能有一个。如果是自己写的构造函数,编译器就不会自动生成构造函数。
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 2, int month = 2, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
上面两个构造函数,符合语法规定构成重载,但是C++规定只能有一个默认构造函数,所以我们只能写其中的一个,且上面两个在函数调用的时候存在歧义,当你不写形参的时候,编译器不知道你是要调用哪一个。
构造函数可以重载,那默认构造函数与半缺省的构造函数可不可以同时写呢?
Date(int year = 2, int month = 2, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
Date(int year = 3, int month = 2)
{
_year = year;
_month = month;
//_day = day;
}
这样也是不行的,半缺省与默认构造函数放在一起时也会存在调用歧义。
例如:
void Print(int x = 2, int y = 2)
{
cout << x << " " << y << endl;
}
void Print(int x = 3, int y = 3, int z = 5)
{
cout << x << " " << y << " " << z << endl;
}
这种虽然语法上来说也是构成重载的,但是在你传一个参数或两个参数时会有多个参数参数列表匹配,就会存在调用歧义。
void Print(int x, int y )
{
x = 2;
y = 3;
cout << x << " " << y << endl;
}
void Print(int x, int y, int z )
{
x = 5;
y = 6;
z = 8;
cout << x << " " << y << " " << z << endl;
}
上面这种才是真正的符合重载,不会再冲调用歧义。
对类成员初始化方式不同时才考虑使用重载构造函数,比如对一个栈的操作你可能想初始化时只对成员变量赋值,也可能想直接插入很多数据,比如将一个数组都插入,这时才真正符合重载,就是参数类型不同。在有缺省的情况下,你再定义一个缺省函数就很有可能会造成调用歧义,除非你传的实参个数不符合其它的重载函数。
全局对象先于局部对象进行构造
局部对象按照出现的顺序进行构造,无论是否为static
析构函数的函数名也与类名相同,但是要在函数名前加上按位取反符号‘~’,析构函数的作用是在函数调用结束之后释放空间,注意不是释放这个对象,对象是在栈上开辟的,出了作用域栈上的空间会自动释放,只有动态开辟的空间才需要用到析构函数去释放,就是在堆上开辟的空间,其它的像临时变量等都是在栈上开辟的空间它会自动释放,此外析构函数没有参数,所以析构函数不可以重载,一个类中只有一个析构函数,如果不显示的去写,编译器也会默认生成构造函数,且对内置类型不做处理,自定义类型会去调用它的构造函数。出了作用域系统会自动调用析构函数。
~Stack()
{
if (_array)//_array是动态开辟的空间
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
什么时候写析构函数?
析构函数释放的空间是释放堆上的空间,就是我们动态开辟的空间。
1.一般情况下,有动态开辟的空间,就需要我们显示的去写析构函数去释放空间。
2.没有动态申请资源,就不需要写析构。像对象这种临时变量,不是动态开辟的空间,是在栈上开辟的,出了作用域系统会自动清除栈上的资源。例如日期类类里面就没有动态申请的资源。
3.如果需要释放的资源都是自定义类型,就不需要写析构函数,默认生成的析构函数足够用了。例如:
class Myque
{
private:
Stack _pushst;
Stack _popst;
};
在我们传参数的过程中,可能会选择值传参,值传参实际上就是对形参进行初始化,对于内置类型变量赋给另一个变量的时候就是值拷贝也称为浅拷贝不用调用拷贝构造函数,而对于自定义类型在值拷贝时,会调用拷贝构造函数。每一个类都有它的默认拷贝构造函数,但是这个默认构造函数是浅拷贝,也就是说这个拷贝它是按字节赋给新的变量,并没有开辟新的空间。如果这个自定义类型中包含指针,那么新变量和被拷贝指针变量就会指向同一个空间,如果是这样的话就会出问题,当这两个对象空间释放时,就会导致同一块空间被析构两次,同一空间是不能被析构两次的,此外如果其中一个对象值发生改变,也会导致另一个对象值发生改变。任何类型的指针都是内置类型,包括自定义类型。如果我们自己定义拷贝构造函数,就会解决上述问题,我们选择深拷贝,深拷贝就是新开辟一块空间,并将原指针指向的空间地址的值赋给新的空间,这样就完成了拷贝,指针指向的空间不同,但是值是相同的。
那么我们可不可以直接用自定义类型作为形参?
答案是否定的,首先我们在对自定义类型进行拷贝时,调用的就是拷贝构造函数,调用它的时候又要进行传参,而在传参的过程中,就是相当于对自定义类型的形参进行初始化,而这种赋值,就又会调用拷贝构造函数,而调用拷贝构造函数就要传参,传参就要调用拷贝构造,最后会形成无限递归。
C语言当中自定义类型传参就直接是浅拷贝,而在C++当中在传参时要先调用拷贝构造,如果想避免上面的问题,还要进行深拷贝,也就是调用我们自定义的拷贝构造。
那么怎么解决自定义类型对象的拷贝呢?
上述传自定义类型时在拷贝时都会调用拷贝构造函数,那么可以选择传地址或者用引用去解决上述问题。因为自定义类型时才要拷贝构造,传指针或引用不需要,因为任何类型的指针都是内置类型,不会调用拷贝构造函数,直接把地址赋给新变量就可以,而传引用就是传这个对象的别名,引用对应的变量并没有开辟新的空间,这里重要的是这个类变量的地址和它的引用的地址是相同的,所以起引用名时不存在什么拷贝。
引用的地址是相同的
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,实际上是有两个参数一个是显示的被拷贝的对象的引用,另一个是隐式的this指针
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
运算符重载的作用:比如日期类我们想让两个日期进行相减,如果我们直接让这两个对象进行相减就会报错,因为自定义类型不是内置类型,编译器不知道他们是怎么进行运算的,所以我们要自己定义出这些符号具体的操作。运算符重载的关键字是字operator然后后面加上重载符号。
#include
using namespace std;
class Date
{
public:
Date(int year = 2005, int month = 4, int day = 6)
{
_year = year;
_month = month;
_day = day;
}
public:
int _year;
int _month;
int _day;
};
//重载操作符定义在类外
bool operator<(Date d1, 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(2005, 02, 03);
Date d2(2006, 6, 10);
d1 < d2;
cout << operator<(d1, d2) << endl;
return 0;
}
因为重载操作也是一个函数所以也可以直接通过调用这个函数来对这两个对象进行比较。
且注意如果是定义在类外的那么类成员就不能是私密的应是public,因为在类外不能直接访问私密的类成员
上面的重载操作符是全局函数即定义在类外的,所以显示的形参个数与比较对象的个数相同。如果重载操作符函数是定义在类内的,那么显示传参的个数就要比实际比较的对象个数少一个,因为定义在类内就会有一个隐式的参数this指针存在
#include
using namespace std;
class Date
{
public:
Date(int year = 2005, int month = 4, int day = 6)
{
_year = year;
_month = month;
_day = day;
}
bool operator<(Date d)//只有一个显式的参数因为此时还有一个隐式的this
{
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;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2005, 02, 03);
Date d2(2006, 6, 10);
//定义为全局的重载操作运算符时的调用
/*d1 < d2;
cout << operator<(d1, d2) << endl;*/
//定义在类内时的调用
d1 < d2;
d1.operator<(d2);
return 0;
}
重载运算符的参数中必须有一个是自定义类型的参数,不能全是内置类型。
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
.* //千万别忘了这个运算符也不能重载
:: sizeof ? : . 注意以上5个运算符不能重载。
运算符重载中有一个特殊的,就是赋值运算符重载,像大于,小于,加,减等,如果你不写那么这个自定义类型就不能进行这些运算符操作,但是赋值运算符=,如果你不写,编译器也会自动生成默认的赋值运算符重载,只不过默认生成的是按字节赋值的浅拷贝,如果自定义类型中成员变量有动态开辟的空间,那么就需要我们自己手动的写上。特别注意,其它的运算符重载既可以定义成全局的,也可以定义在类内,但是赋值运算符只能定义在类内,因为其它的运算符编译器不会自动生成,而赋值运算符编译器会在类内自动生成默认的赋值运算符重载,如果你在类外定义就会造成调用不明确,出现歧义,在类内定义了,编译器就不会自动生成。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。另外赋值运算符,我们要有返回值且选择返回它的引用,当然也可以返回值,只不过是浅拷贝,且传值返回会调用拷贝构造,而返回引用不会调用拷贝构造,且会减少空间的消耗,这个是定义在类里面的,出了作用域 this 还在,只不过是在调用时的中介this不在了,this是这个对象,对象的声明周期不是在这个类里的 。
对象的传值才要调用拷贝构造,内置类型传值不调用拷贝构造,任何类型的指针都是内置类型,包括自定义类型的指针,传引用就是在传别名也不会调用拷贝构造。
赋值运算符是作用于两个已经存在的对象,一个对象的值赋给另一个对象。而构造函数是用一个已经存在的对象去初始化另一个对象。