类的6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
务并不是开空间创建对象,而是初始化对象(类似于Init()函数的功能)
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(不需要写void)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。(可以有多个构造函数,多种初始化方式,但必须满足函数重载条件)
5.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
注意1:通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明(如果可以带括号,怎么和函数声明区分呢?)
注意2:下面的无参构造函数,和全缺省构造函数同时存在时,会存在调用歧义(因为都可以不传参调用),因此我们一般选择全缺省构造函数(因为更好用)
如果没有主动写构造函数,会不会有构造函数呢?
答案:有的,是编译器自动生成的
(有内置类型/基本类型 int/char/double...)
(自定义类型 class /struct....)
(编译器自动生成构造函数,对于内置类型的成员变量,一般不C++内置类型没有规定要处理)!(但根据编译器有不同))
【编译器自动生成构造函数,对于自定义类型的成员变量会调用它的自定义类型的不传参就可以调用的构造函数【如果没有默认构造函数而有传参才可以调用的构造函数,它会编译报错】(但如果自定义类型没有写构造函数,他会自动生成构造函数,但对自定义类型的成员变量不作处理)】
#include<iostream>
using namespace std;
class Date
{
public:
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 2.带参构造函数
/*Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
// 3.带参全缺省构造函数
/*Date(int year=2024, int month=6, int day=28)
{
_year = year;
_month = month;
_day = day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参构造函数
//Date d2(2015, 1, 1); // 调用带参的构造函数
//全缺省构造函数和无参构造函数都是默认构造函数,一个类里面只能有一个默认构造函数,所以将此类型初始化屏蔽
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
//Date d3();
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值(缺省值) 。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;//给缺省值
int _month = 1;
int _day =1;
};
int main()
{
Date d1;
d1.Print();
Date d2(2024,6,28);//事实上先用了给的缺省值初始化,然后再用传的值进行覆盖
d2.Print();
return 0;
}
总结:
(自定义类型的尽头都是由内置类型构成的)
1.一般情况下构造函数都需要我们自己显示的去实现
2.只有少数情况下可以让编译器自动生成 构造函数(例:MyQueue(由两个栈实现队列),类成员变量里只有两个自定义类型成员变量(即stack))
析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作(Destroy()函数)
特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数//无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.如果我们没有显示的写析构函数,编译器会自动调用析构函数,和构造函数类似
(a:内置类型不做处理 b:自定义类型去调用它的析构函数)(这样可能会造成内存泄漏,对于需要主动写析构函数的类因为懒惰没主动写)
实践中总结:
1.有资源需要显示清理,就需要写析构。如stack,list
2.有两种情况不需要写析构函数,默认生成就可以了
a.没有资源需要清理,如Date
b.内置类型成员没有资源需要清理,剩下的都是自定义类型(如MyQueue,因为它里面自定义类型stack里面有主动显示写了析构,这样就不会造成内存泄漏,所以MyQueue不需要主动写析构)
Example:
Stack.h文件
#pragma once
#include<stdlib.h>
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int n = 4);
~Stack();
void Push(int x);
bool Empty();
void Pop();
int Top();
private:
// 成员变量
int* _a;
int _top;
int _capacity;
};
Stack.cpp文件
#include"Stack.h"
//stack构造函数带参定义
Stack::Stack(int n)
{
_a = (int*)malloc(sizeof(int)*n);
_top = 0;
_capacity = n;
}
//stack析构函数定义
Stack::~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
void Stack::Push(int x)
{
//代码不完整仅作参考
_a[_top++] = x;
}
bool Stack::Empty()
{
return _top == 0;
}
void Stack::Pop()
{
--_top;
}
int Stack::Top()
{
return _a[_top - 1];
}
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。(自定义类型传值传参要调用拷贝构造)
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//为什么加const,为什么要加有引用'&'
//加const是为了防止传参对象的内部被修改
//不加引用会引发无穷递归调用
Date(const Date& d)
{
_year = d._yaer;
_month = d.month;
_day = d.day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 给缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2024,6,8);
d1.Print();
Date d2(d1);//用同类对象拷贝初始化,编译器便会自动调用拷贝构造
//Date d2 = d1;这样的写法也是可以的,两者写法等价
d2.Print();
return 0;
}
下面的自定义类型传值传参会调用拷贝构造
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//为什么加const,为什么要加有引用'&'
//加const是为了防止传参对象的内部被修改
//不加引用会引发无穷递归调用
Date(const Date& d)
{
cout<<"Date拷贝构造"<<endl;
_year = d._yaer;
_month = d.month;
_day = d.day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 给缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
void func(Date d)//这里相当于调用拷贝构造,将d1拷贝给d
{
d.Print();
}
int main()
{
Date d1(2024,6,8);
func(d2);
return 0;
}
如何避免拷贝构造
1.利用引用,如下面的func(const Date& d)
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//为什么加const,为什么要加有引用'&'
//加const是为了防止传参拷贝对象的内部被修改
//不加引用会引发无穷递归调用
Date(const Date& d)
{
cout<<"Date拷贝构造"<<endl;
_year = d._yaer;
_month = d.month;
_day = d.day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 给缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
void func(const Date& d)//这里是引用,d是d1的别名,为了防止修改d1数据,应该加上const
{
d.Print();
}
int main()
{
Date d1(2024,6,8);
func(d2);
return 0;
}
2.利用指针
为什么传参拷贝会引发无穷递归
因为传自定义类型的参数会引发拷贝构造,而拷贝构造是传参式的,拷贝构造又会调用新的拷贝构造,新的拷贝构造仍然是传参式的,又会引发新的拷贝构造,如此反复,形成了无穷递归。
而采用引用式,传参调用拷贝构造后(因为是引用类型的,便不会引发新的拷贝构造),如此拷贝构造便完成结束
事实上,传参式的拷贝构造会引发编译器报错,编译不过去。
若未显式定义拷贝构造,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
下面的程序会报错吗?(下面程序屏蔽了主动写的拷贝构造(深拷贝),编译器便默认采用浅拷贝按字节挨着挨着进行复制 )
#include<iostream>
using namespace std;
typedef int datatype;
class stack
{
public:
stack(size_t capacity = 3)
{
cout << "stack(size_t capacity = 3)" << endl;
_array = (datatype*)malloc(sizeof(datatype) * capacity);
if (null == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
// stack st2 = st1;
/*stack(const stack& st)
{
//当熟悉之后this指针就可以不写了
this->_array = (datatype*)malloc(sizeof(datatype) * st._capacity);
if (null == _array)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(this->_array, st._array, sizeof(datatype) * st._size);
this->_size = st._size;
this->_capacity = st._capacity;
}*/
void push(datatype data)
{
// checkcapacity();
_array[_size] = data;
_size++;
}
bool empty()
{
return _size == 0;
}
datatype top()
{
return _array[_size - 1];
}
void pop()
{
--_size;
}
// 其他方法...
~stack()
{
cout << "~stack()" << endl;
if (_array)
{
free(_array);
_array = null;
_capacity = 0;
_size = 0;
}
}
private:
datatype* _array;
int _capacity;
int _size;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
Stack st2 = st1;
return 0;
}
答案:会的
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
实践总结:
1.如果没有管理资源,一般情况不需要写拷贝构造,默认生成的拷贝构造就可以(如Date)
2.如果都是自定义类型成员,内置类型成员没有指向资源,也类似默认生成的拷贝构造就可以(如MyQueue)
3.一般情况下,不需要显示写析构函数,就不需要写拷贝构造
4.如果内部有指针或者一些指向资源,需要显示写析构函数释放,通常就需要显示写拷贝构造完成深拷贝。(如:Stack Queue List)