如果一个类中什么函数都没有定义,这时编译器会自动生成6个默认成员函数
在前面数据结构的学习中,我们经常需要调用一个初始化函数,对结构体对象进行初始化,如果我们忘记了初始化,程序就会出现错误,所以在C++中,就引入了构造函数
构造函数是特殊的成员函数,需要注意的是:构造函数虽然叫做“构造”,但它的作用是对对象进行初始化,不是开空间构造对象
构造函数特点:
下面我们看一下无参的构造函数
class Date
{
public:
//无参构造函数
Date()
{
}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //调用无参构造函数
Date d2(2000, 1, 1);//调用含参构造函数
return 0;
}
这里需要注意的一点,调用无参构造函数就不用加括号了,如果写成
Date d1();
,就写成函数声明了
以及在栈类中的带缺省参数的构造函数
class Stack
{
public:
//带缺省的构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
如果类中没有显示定义的构造函数,编译器会自动生成一个无参的默认构造函数,如果用户显示定义了一个构造函数,那么编译器便不会自动生成默认构造函数
关于编译器默认生成的构造函数,内置类型不做处理(有些编译器会处理),自定义类型会去表用它自己的默认
构造函数
基本类型/内置类型:int,char,double……还有指针类型
自定义类型:class struct ……
下图也能看出编译器自动生产的构造函数可以对自定义类型进行处理:
所以,一般情况下,有内置类型成员,就要自己写构造函数,不能用编译器自己生成的
全部是自定义类型,可以不用写构造函数,用编译器自己生成的默认构造函数
针对编译器生成的默认构造函数不能对基本类型进行初始化的缺陷,同时也需要兼容以前的操作
在C++11中针对内置类型成员不初始化的缺陷打了补丁,内置内省成员变量在声明时可以给默认值
class Date
{
public:
void Print()
{
cout << _year << " - " << _month << " - " << _day << endl;
}
private:
int _year = 0;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
编译器默认生成的构造函数称为默认构造函数,其实无参的构造函数和全缺省的构造函数也时默认构造函数(不传参就调用的就是默认构造函数)
但是无参的构造函数和全缺省的构造函数在调用的时候有歧义
默认构造函数只能有一个,所以我们优先选择全缺省的构造函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
~
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = 0;
}
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
当用户没有显示定义析构函数时,编译器会自己生成一个默认的析构函数
这个默认析构函数对内置类型不做处理,自定义类型才会处理,会去调用自定义类型它的析构函数
一般情况下,有动态申请资源,需要显示写析构函数
没有动态申请资源,不用写析构函数
需要释放资源的成员都是自定义类型,默认析构函数会调用它们自己的析构函数,所以不用单独写一个析构
在创建对象时,是否可以创建一个与已存在对象一样的新对象呢?
所以就有了拷贝构造函数
我们先写一个拷贝构造函数:
//错误写法
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
其实上面这么写是错的
C++规定,内置类型直接拷贝,自定义类型必须调用拷贝构造函数
对于自定义类型传参我们可以这么理解:传参是实参拷贝一份给形参,对于对象来说,拷贝一份自然要调用拷贝构造函数。
如果这么写,当运行Date d2(d1)
时,会调用拷贝构造函数,因为传参是对象,还需要调用拷贝构造函数,调用拷贝构造函数还需要传参,还要拷贝构造函数,就这样重复调用,无穷无止。
所以为了解决这个问题,拷贝构造函数要传对象的引用
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
同时,因为是拷贝构造,不应该改变原有的那个对象,所以参数还可以加const
修饰
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这里注意一点,一般调用拷贝构造函数是这样:
int main()
{
Date d1;
Date d2(d1);//调用拷贝构造
}
其实还有一种写法,这种写法是由编译器特殊处理过:
int main()
{
Date d1;
Date d2 = d1;
}
Date d2 = d1
这种写法是拷贝构造是因为用一个已经存在的对象初始化另一个对象,这里不要与后面的赋值重载函数搞混。
假如用户没有显式定义,编译器会生成默认拷贝构造函数.默认拷贝构造函数对象按字节序序完成拷贝,这种叫做浅拷贝/值拷贝
默认拷贝构造函数对 内置类型成员完成值拷贝/浅拷贝,对自定义类型调用它的拷贝构造
对于Date类,它的成员变量都是内置类型,所以不用写拷贝构造函数,用编译器默认生成的构造函数就可以。
再来看一下有深拷贝的动态开辟空间的类:
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
进入调试,可以看到,在没有显式定义拷贝构造函数的情况下,s1
中的值也都拷贝到了s2
中
继续运行,发现程序崩溃了,这是怎么回事?
原因就是因为编译器默认生成的拷贝构造函数只能进行浅拷贝,就导致s1
中的_a
和s2
中的_a
指向同一块空间,在自动调用析构函数时,对同一块动态空间free
了2次,所以才出错
这里再考虑一点:通过浅拷贝得到的新对象,它们俩中的动态资源都是一块空间,也就是说两个其中的一个修改会影响另一个。
所以遇到有需要动态开辟的成员时,就需要自己定义能够深拷贝的拷贝构造函数,通过深拷贝,再开辟一块空间,让新拷贝对象中的_a
指向新空间
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_capacity = st._capacity;
_top = st._top;
}
从上面的可以知道,类中如果没有涉及资源申请时,拷贝构造是否写都可以,一旦涉及到资源申请时,拷贝构造函数时一定要写的,不然就是浅拷贝
通过对深浅拷贝的对比可以得值为什么C++传对象要调用拷贝构造函数了
如果全都按照基本类型传参,作为参数的对象如果有动态资源,那么形参和实参中共用一块空间,也就会导致其中一个修改会影响另一个,并且对象析构时,析构两次程序会崩溃。
int main()
{
Date d1;
Date d2;
d2 = d1;
}
观看上面代码,先定义了2个对象,然后d2 = d1
把d1
的值赋值给了d2
这里就会调用赋值运算符重载
注意要分清赋值运算符重载和拷贝构造函数
拷贝构造函数特点是:用一个已经存在的对象初始化另一个对象
赋值运算符重载是:已经存在的2个对象间赋值
下面我们来具体区分一下:
int main()
{
Date d1;
Date d2;
d2 = d1;//赋值重载
Date d3(d1);//拷贝构造
Date d4 = d2;//拷贝构造
}
上面代码中,已经存在了d1
和d2
2个对象
d2 = d1
是已经存在的2个对象间赋值,所以是赋值重载
Date d3(d1)
,是用已经存在的对象d1
初始化之前并不存在的对象d3
,是拷贝构造,这也是拷贝构造调用的常见写法
Date d4 = d2
看似像是复制重载,其实是已经存在的对象d2
初始化d4
,所以还是拷贝构造函数
赋值运算符重载:
Date类的赋值重载函数:
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
运算符重载必须写成成员函数,因为:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
我们可以重载赋值运算符,不论形参的类型是什么,赋值运算符都必须定义为成员函数
如果没有显式定义,编译器会自动生成一个默认的赋值运算符重载函数,以值的方式逐字节拷贝
默认生成赋值重载跟拷贝行为一样:
所以如果类中未涉及到动态资源,是否显式定义赋值重载函数都可以,一旦涉及到动态资源就必须显式定义
现在我们写一个Date的打印函数
void Date::print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
如果我们定义了一个对象:const Date d1(2022, 1, 1)
,通过d1
去调用print()
函数,会发现发错
这里报错的原因是:用const对象 d1调用print()中的this
指针的类型本应该是const Date* const
const Date* const
,中的第一个const
修饰的是this
指针本身,第二个const
修饰的是*this
,其保证了不能修改成员变量
而print()
中this指针的类型是Date* const
从const Date* const
->Date* const
是权限的放大,C++中允许权限的缩小而不允许权限的缩大
所以为了解决这个问题,需要在成员函数定义时,将this指针修饰成const
void Date::print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
之所以在函数名后面加修饰的
const
,是因为C++中规定在函数的实参形参中不可以显式出现this
,没办法直接在this上修饰,所以只能在函数名后加const
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
成员函数加const
后,普通对象和const对象都可以调用,这里只有权限的平移和缩小,所以可以说任何对象都可以调用const成员函数
不是所有成员函数都可以加const,只有那些不会修改成员变量的函数才可以加const
C++中还有取地址操作符重载函数和const取地址操作符重载函数:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
如果没有显式定义,编译器会自动生成默认的取地址函数
这2个运算符一般不需要重载,使用编译器默认生成的即可
如果有特殊情况,才需要重载,不如像让别人获取到指定的内容。