一. 类的6个默认函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情
况下,都会自动生成下面6个默认成员函数(c++98)分为:初始化和清理: 构造函数 --- 初始化
析构函数 --- 清理
拷贝和赋值: 拷贝构造 --- 拷贝 (涉及深浅拷贝)
重载赋值 --- 赋值 (涉及深浅拷贝)
取地址重载 operator& 和 operator& const (很少自己实现)
二. 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员
都有 一个合适的初始值,并且在对象的生命周期内只调用一次构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主
要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
class Date{
public:
Date(int year = 0, int month = 0; int day = 0){ // 构造函数一般给一个全缺省
_year = year;
_month = month;
_day = day;
}
// Date(int year = 0, int month = 0; int day = 0)
// : _year(year), _month(month), _day(day) //使用初始化列表更好
// {}
private:
int _year;
int _month;
int _day;
};
int main(){
// Date d1(); 错误
Date d1; // 无参构造不能用上面的写法, 调用全缺省 0, 0, 0
Date d2(2019, 1, 1); // 有参构造
return 0;
}
自己如果不写构造函数系统会提供一个默认的无参构造函数, 默认的构造函数的行为可以写一段代码验证:
class B {
public:
B() {
cout << "B 构造"<<endl;
}
};
class A {
public:
int num1;
int num2;
B b;
};
int main() {
A a;
cout << a.num1 << a.num2 << endl;
}
B 构造
-858993460-858993460可以看到系统的构造函数对于自定义类型和内置类型不同的初始化方式,
对于内置类型不初始化
对于自定义类型会去调用这个自定义类型的默认构造函数
默认构造函数: 默认构造函数简单来说就是不需要参数就可以调用的构造函数, 有三种情况:
1. 没有写任何的构造函数, 系统默认提供的构造函数就是默认构造函数
2. 自己写了一个有参构造, 一个无参构造, 无参构造就是默认构造函数
3.写了一个全缺省构造, 这个全缺省就是默认构造函数
每个类最好都有一个默认构造, 方便调用
三. 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而
对象在销毁时会自动调用析构函数,完成类的一些资源清理工作其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
析构函数完成资源的手动释放(free, delete)
系统默认提供的析构函数和构造类似,
对于内置类型什么也不做,
对于自定义类型会去调用那个自定义类型的析构函数
class B {
public:
B() {
cout << "B 构造"<<endl;
}
~B() {
cout << "B 析构" << endl;
}
};
class A {
public:
A() {
cout << "A 构造" << endl;
}
~A() {
cout << "A 析构" << endl;
}
B b;
};
int main() {
{A a; }
}
这个顺序是
B构造 -- > A 构造 -- > A析构 -- >B析构
四. 拷贝构造
拷贝就是拿一个已经创建的对象去初始化另一个对象(另一个对象还没有创建)
Date date(2019, 1, 1) ;
Date copy(date);
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
class Date{
...
// 拷贝构造
// const 表示这个参数在函数中不会被修改
Date(const Date& date){
_year = date.year;
_month = date.month;
_day = date.day;
}
};
若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
贝,这种拷贝我们叫做浅拷贝,或者值拷贝直接 使用 Date d2(d1) 也能完成正确的拷贝
如果一个类的成员都是内置数值类型, 使用默认的拷贝就可以, 不需要自己写, 如果是指针类型就有问题, 下面的重载赋值也一样
五. 重载赋值
赋值就是拿一个已经创建的对象去给另一个对象赋值(另一个对象已经存在)
Date date(2019, 1, 1) ;
Date assign;
Date assign = date;
对象初始化调用拷贝构造, 赋值调用 = 重载, 下面这种形式:
Date date(2019, 1, 1) ;
Date copy = date;
这种虽然用的是 = , 但是这也是拷贝构造
class Date{
...
// 重载 =
// 返回值是为了支持连等
Date& operator&(const& date){
if (this != &date){
_year = date.year;
_month = date.month;
_day = date.day;
}
return *this;
}
};
赋值运算符主要有四点:
1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值 (在深拷贝中这一点很重要)
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
六. 深浅拷贝
浅拷贝就是系统默认支持的逐字节拷贝, 对于数值类型完全没问题, 但是对于指针类型
class String{
public:
String(const char* str = ""){
if (str == nullptr) {
assert(false);
}
int len = strlen(str);
_str = new char[len + 1];
strcpy(_str, str);
}
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* c_str;
};
int main() {
{
String str("Hello World!!");
String copy(str);
String assign;
assign = str;
}
}
main 函数中创建了一个对象 str,
用系统默认的拷贝构造和赋值, 创建了copy 和 assign,
通过调试可以发现这三个对象中的c_str成员的地址都相同,
因为系统默认拷贝构造和赋值是浅拷贝,
修改其中一个, 其他两个也会修改,
最后析构的时候, 一个空间多次被delete 会引发错误
所以这种指针类型做成员就不能用系统默认的拷贝构造和赋值, 需要我们自己手动开辟空间
String(const String& s)
:_str(new char [strlen(s) + 1]),
{
strcpy(_str, s._str);
}
String& operator=(const String& s) {
if (this != &s) {
char* pStr = new char[strlen(s) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}
深拷贝就是在另开一段一样大的空间将数据拷贝过去
赋值重载中:
char* pStr = new char[strlen(s) + 1];
strcpy(pStr, s._str);delete[] _str;
_str = pStr;这个pStr 临时变量的作用, 是防止new 失败的异常,
如果delete[] _str; _str = pStr; 一旦new出异常, _str的空间也被delete了, 有可能产生错误
上面的拷贝构造和赋值是传统写法, 还有现代写法, 利用交换临时对象, 初始化
String(const String& s) : _str(nullptr){
String tmp(s._str);
Swap(tmp);
}
String& operator=(String s) {
Swap(s);
return *this;
}
void Swap(String& s2) {
std::swap(_str, s2._str);
std::swap(_size, s2._size);
std::swap(_capacity, s2._capacity);
}
七. 重载&
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比
如不想让别人获取到对象的地址