@TOC
①.拷贝构造函数
Ⅰ.概念
在创建对象时,能否创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类型相同的对象的引用,一般是用const修饰,在用已存在的对象创建一个同类型的新对象时由编译器自动调用。
为什么要用const修饰?
防止将拷贝对象修改,我们只是要将拷贝对象,并不能将对象修改了。所以加上const来修饰拷贝的对象,防止错误修改。
Ⅱ.特征
拷贝构造函数也是特殊的成员函数。它的特征如下:
1.重载形式之一
拷贝构造函数是构造函数的一个重载形式。 函数名字跟类名是一样的,只是参数列表不同
Data(int year =2023, int month = 5, int day=4)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//两个函数构成重载
Data(const Data& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
2.参数唯一
拷贝构造的函数参数只有一个,理论上是两个的,一个是已存在的要被拷贝的对象,一个是新创建的要拷贝的对象,但新创建的对象传给了隐藏的this指针了。所以显示的只有一个参数。
3.形参必须传引用
构造函数的参数只有一个且必须是类类型对象的引用,如果使用传值方式编译器会直接报错,因为这样会引发无穷递归调用。
class Data
{
public:
Data(int year =2023, int month = 5, int day=4)
{
_year = year;
_month = month;
_day = day;
}
Data (const Data d)//这种形式是错误的,不可以这样写,不能传值过去
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Data(const Data& d)//正确的写法是这样,传引用过去
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year <<"-"<< _month <<"-"<< _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 5, 3);
Data d2(d1);
d2.Print();
}
我们知道调用函数需要先传参,而对应内置类型,传参是直接以字节形式拷贝过去,但自定义类型就必须要用拷贝构造形式去完成。
也就是C++规定:在传参过程中 1.内置类型是直接拷贝过去的。 2.自定义类型必须调用拷贝构造完成拷贝。
所以对于自定义类型传参,如果使用传值形式,则传参就相当于又形参了一个新的拷贝构造函数,为什么呢?
因为采用传值形式的话,那么形参就是实参的一份拷贝。 将实参传过去,那么就必须调用一次拷贝构造函数形成形参。 所以如果传值过去,编译器会强制检查发现这样会引发无穷递归调用拷贝构造函数,然后报错。
所以形参必须给该类对象的引用。 当调用拷贝构造函数时,传参就不需要再调用拷贝函数了,因为传参使用的是引用传参,参数不是实参的一份临时拷贝,而就是实参本身,只不过是别名。
因为拷贝构造函数也是特殊的成员函数,是由编译器自动调用的,所以我们可以不去显示的去调用,编译器会帮我们调用,这是在拷贝构造函数已经写的情况下。
4.编译器的拷贝函数
如果没有显式的定义拷贝函数,那么编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按照字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
默认生成的拷贝函数: 1.内置类型完成值拷贝/浅拷贝。2.自定义类型会调用相对应的拷贝构造。
对于不需要申请动态资源的对象,浅拷贝就可以完成工作。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& s)
{
_hour = s._hour;
_minute = s._minute;
_second = s._second;
cout << "Time(const Time& s)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Data
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private://内置类型
int _year=1;
int _month=1;
int _day=1;
//自定义类型
Time _t;
};
int main()
{
Data d1;
//用已存在的d1拷贝构造d2,这里会调用Data类的拷贝构造
//但Data类没有显式的定义,所以编译器会生成一个默认的拷贝构造。
//默认生成拷贝构造能否完成拷贝工作呢?
Data d2(d1);
d2.Print();
}
默认生成的拷贝构造是可以完成上面的拷贝任务的,因为默认的拷贝构造会对对象进行浅拷贝,而该场景就适合浅拷贝,因为没有动态资源的开辟,虽然Time定义的是自定义类型,但是浅拷贝可以完成任务就行了。编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那还需要自己显式的写拷贝构造函数吗?当然对于Data日期类的是没有写的必要,但并不是每个类都是像日期类一样的。 当对象的拷贝需要深度拷贝时,就不能单单使用浅拷贝,这样会出问题的。 比如下面这个栈:
typedef int DataType;
struct stack//class可以定义一个类
{
public://访问限定符
stack(int capacity = 4)//缺省值
{
cout << "stack(int capacipty=4)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
{
return;
}
--_size;
}
int Empty()
{
return _size == 0;
}
~stack()
{
cout << "~stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private://访问限定符
DataType* _array;
int _capacity;
int _size;
};
int main()
{
stack s1;//定义一个对象
stack s2(s1);
根据调试可以发现d1初始化,然后将已存在的d1拷贝给新创建的d2。看起来都完美,但其实有一个致命的错误。
我们知道默认生成的拷贝函数是按照浅拷贝进行拷贝的,浅拷贝就是完全一样,全部复制过来。 这样是存在危险的,因为可能存在这样的情况:拷贝对象与被拷贝对象的成员指向了同一块空间。
这种情况会存在这样的问题:
1.同一块空间会析构两次,会报错。
当s2对象生命周期结束时,系统自动调用析构函数来清理数据,那么*a指向的空间就被销毁了。 而当s1对象生命周期结束时,又析构一次相同的空间,这样同一块空间就析构两次了。
编译器会出错的。
2.一个变量修改会影响另一个变量,因为两个变量都存在同一块空间里。
【注意:】=类中如果没有涉及资源的申请时,拷贝构造函数是否写都是可以的;一旦涉及资源的申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝了。
编译器默认生成的拷贝构造只能完成浅拷贝,而需要深度拷贝时还必须用我们自己写的拷贝构造函数。其实自己写的拷贝构造函数就是为自定义类型的深度拷贝准备的。所以总结一下,适合深度拷贝和浅拷贝的场景:
1.对于Data和MyQueue这样的类我们不需要自己写拷贝构造,因为浅拷贝就可以完成任务。(MyQueue就是用栈来实现队列,而栈里面是要用自己写的拷贝函数,但实现队列时就不需要了)
class MyQueue
{
private:
stack pushst;
stack popst;
};
2.对于Stack这样的类,我们是需要自己写拷贝构造的,因为里面涉及要深度拷贝,有动态资源的开辟。
5.典型调用场景
1.使用已存在的对象创建新对象。2.函数参数类型为类类型对象。3.函数的返回值类型为类类型对象。
class Data
{
public:
Data(int year =2023, int month = 5, int day=4)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)//正确的写法是这样,传引用过去
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year <<"-"<< _month <<"-"<< _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//典型调用场景:返回值是类类型对象,参数是类类型对象
Data Test(Data d)
{
Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
return tmp;//返回对象tmp
}
int main()
{
Data d1(2023, 5, 3);
Data d2(d1);
Data tmp=Test(d2);
d2.Print();
}
注意:
为了提高此程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
就比如上面的Test函数,对象传参我们最好使用引用类型,那样就减少了一次调用拷贝函数的工作,提高了效率
Data Test(Data& d)
{
Data tmp(d);//用已存在的d(其实是d2)来创建新对象tmp
return tmp;//返回对象tmp
}
那能不能给返回值也使用引用呢?答案是不能,要根据实际场景来对返回值使用引用,这样是对局部对象返回,不能使用引用,因为局部对象返回后,这个对象就销毁了,使用引用取别名那就对已经销毁的空间的非法访问了。所以不可以。
②.总结:
- 1.拷贝构造函数是构造函数的一个重载形式。
- 2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 4.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
- 5.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
- 6.拷贝构造函数典型调用场景: 使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象