目录
- 六个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 总结
六个默认成员函数
默认成员函数的概念:如果用户不显式写,编译器会自动生成的函数,就是默认成员函数
构造函数
构造函数是六个默认成员函数之一,构造函数的功能类似于init,起了初始化的功能,构造函数的名字和类的名字相同,构造函数可以无参,有参,有参全缺省,构造函数在创建对象时,编译器会自动调用。
特性:
- 函数名和类名相同
- 无返回值
- 类的实例化时会自动调用
- 构造函数可以重载
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
- 如果类中没有显式写构造函数,则编译器会自动生成一个无参的构造函数,如果显式写了构造函数,编译器则不会自动生成构造函数。
注意:在C++中,定义了构造函数会自动调用,但是在实际中vs是不会调用构造函数的,所以C++11打了一个补丁就是可以在声明类的成员的时候可以增加一个缺省值,在编译的过程中,就会根据成员变量的缺省值来对对象进行初始化。
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 = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
上面的代码在对类中的成员声明时就会根据参数的缺省值进行初始化,这是在没有显式写构造函数的时候。
注意:全缺省的构造函数、无参的构造函数、和编译器默认生成的构造函数都可以作为编译器默认的构造函数
并且默认的构造函数只能有一个,意思就是这三个构造函数只能有一个,如果前两个同时存在,虽然构成重载,但是调用时编译器会产生歧义。
析构函数
析构函数的工作类似于destroy,但是对于内置类型一般不需要调用析构函数,一般需要用析构函数的是
malloc
出空间,还有new
出来的空间。
特征:
- 析构函数和构造函数类似函数名和构造函数稍微有点区别,只需要在类名前面加上一个~,就是析构函数。
- 无参无返回值
- 第二条说无参,也就造成了析构函数不能进行函数重载
- 在对象的生命周期结束时,C++编译器会自动调用析构函数
让我们用下面的一个类来检测一下,编译器是否自动调用了析构函数
#include<iostream>
#include<cstdlib>
using namespace std;
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()
{
cout << "~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);
}
int main()
{
Stack st;
return 0;
}
可以看到在我们显式实现的析构函数中,我在析构函数中加了一个cout,如果编译器自动调用了,则会在屏幕上打印一个~Stack,反之则不会打印。
从下图运行结果看,可以看出,编译器会自动调用析构函数。
6. 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的析构函数,比如:Date类,如Stack类的就需要自己完善一个析构函数。
拷贝构造函数
拷贝构造函数和构造函数类似,是一种特殊的构造函数,拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征:
- 拷贝构造函数是构造函数的一种重载形式。
- 拷贝构造函数的参数只有一个就是传递的类的引用,如果进行传值调用的话就会产生无穷递归,编译器会报错。
对于第二点,为什么会产生无穷递归呢?
首先需要了解的一点是,编译器在对自定义类型进行传值传参的时候,会直接调用拷贝构造函数,所以当我有一个func函数的时候,需要传递一个自定义类型,则在传递之前,需要先调用拷贝构造函数,然后再去调用func函数。
了解上面之后,接下来我们就来讨论为什么传值传参会产生无穷递归,首先我们传值的话会调用拷贝构造函数,调用拷贝构造函数的话,因为调用拷贝构造函数的参数也是一个自定义类型,所以又会继续调用拷贝构造函数,接着就会一直进行递归调用,最后就会崩溃,接下来用图来展示::
- 如果没有显式定义拷贝构造函数则编译器会自动生成一个默认的拷贝构造函数,并且在调用的时候会成功,但是需要注意的是这里编译器生成的拷贝构造函数是浅拷贝,而不是深拷贝。
所谓的浅拷贝就是值拷贝,只拷贝值,深拷贝就是比如我原来有一块malloc出来的空间,深拷贝会自动申请一块和以前那块一样的空间,然后将值拷贝进去,而浅拷贝,则会和以前malloc出来的空间共用一个空间,这样会导致一个问题,我们拿栈来举例,如果我们用编译器自动生成的浅拷贝的话,当我们拷贝完成的时候,如果以前的空间再次进行push的话以前的size会++,但是新的size则不会++,还有一个严重的问题就是浅拷贝的话,析构函数会调用两次,对同一块空间进行两次释放,会产生很大问题。
如上图所示
5. 拷贝构造函数典型调用场景:
–使用已存在对象创建新对象
–函数参数类型为类类型对象
–函数返回值类型为类类型对象
总结
默认构造函数(Default Constructor):如果我们没有定义任何构造函数,编译器将会生成一个默认构造函数。默认构造函数不接受任何参数,并且执行成员变量的默认初始化。在很多情况下,这可能是合适的,但如果类的成员需要特定的初始化值,可能需要显式定义构造函数。
析构函数(Destructor):如果我们没有提供析构函数,编译器会生成一个默认的析构函数。默认析构函数会释放对象所占用的内存,如果对象包含有指针成员,可能不会正确地释放内存或执行其他必要的清理工作。如果类需要在对象销毁时执行特定操作,比如释放资源或者清理其他状态,就需要显式定义析构函数。
拷贝构造函数(Copy Constructor):如果我们没有定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认拷贝构造函数会执行浅拷贝(shallow copy),即对于指针成员只复制指针而不复制指针所指向的对象。这可能导致浅拷贝问题,即多个对象共享同一块内存。如果类含有指针成员,或者需要进行深拷贝或其他特殊处理,就需要显式定义拷贝构造函数。