2. 类的6个默认成员函数
我们将什么成员都没有的类称为空类,但是空类中并不是什么都没有。任何类中都会存在6个默认成员函数,这6个默认成员函数如果用户没有实现,则会由编译器默认生成。
6个默认成员函数包括:负责初始化工作的构造函数;负责清理工作的析构函数;在用同类对象对创建对象进行初始化时用到的拷贝构造;用于对象之间赋值的赋值操作符重载;还有两个很少自己实现的取地址操作符重载和const修饰的取地址操作符重载。
2.1 构造函数
构造函数是一个特殊的成员函数,帮助我们对新创建的对象进行初始化。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//void Init(int year = 2000, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
Date d1;
Date d2(2024, 2, 26);
//d1.Init(2024, 2, 26);
d1.Print();
d2.Print();
}
构造函数说明
对于构造函数,我们需要对其进行一些说明与强调:
①构造函数属于默认成员函数,当发现我们没有写时编译器会默认生成一个,当我们写了编译器就不再会生成。
②我们写构造函数时需要注意:构造函数的函数名应于类名相同;构造函数没有返回值;返回类型的void不写;构造函数可以重载。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date()
{}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
};
int main()
{
Date d1(); //error 调用无参构造函数时,如果跟上()就成了函数声明
Date d1;
Date d2(2024, 2, 26);
}
③构造函数会在创建对象的时候自动调用,且在该对象的生命周期内仅调用这一次。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
Date d1(2024, 2, 26);
d1.Print(); //2024-2-26
}
④构造函数的任务不是开辟空间创造对象,而是对创建的对象进行初始化。
⑤编译器默认生成的构造函数,对内置类型(int、char等)不会做处理,对自定义类型(类等)会调用该自定义类型的构造函数。
class Time
{
private:
int _hour;
int _minute;
int _second;
public:
Time()
{
cout << "Time()" << endl;
}
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;
public:
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
Date d1;
d1.Print();
//output
//Time()
//随机值
}
⑥C++11规定:内置类型可以在类中给默认值。
class Date
{
private:
//在类中给默认值
int _year = 2000;
int _month = 1;
int _day = 1;
public:
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
};
int main()
{
Date d1;
d1.Print(); //2000-1-1
}
2.2 析构函数
构造函数是在对象创建后默认调用,用于初始化的。析构函数则是在对象的生命周期结束时调用自动析构函数,用来销毁对象,完成对象中资源的清理工作。
析构函数说明
①析构函数也属于默认成员函数,所以当我们没有写时编译器会默认生成一个,当我们写了编译器就不再会生成。
②注意析构函数形式:析构函数的函数名为 ~类名 ;析构函数没有返回值和返回类型;析构函数不可以重载。
class Stack
{
private:
int* _arr;
int _capacity;
int _top;
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == nullptr)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
void Push(int x)
{
//CheckCapacity();
_arr[_top++] = x;
}
~Stack()
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_top = 0;
}
};
③析构函数会在对象生命周期结束的时候自动调用。
④编译器默认生成的析构函数,对内置类型不做资源清理,系统会自动将其内存回收;对自定义类型会调用该自定义类型的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
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;
//output:
//~Time()
}
⑤如果类中成员变量没有申请资源时,析构函数可以不需要写,使用默认即可。但是如果存在资源申请(如malloc),需要自己写出对应的析构函数来正确释放空间。
⑥在此处我们总结一下对于局部、全局、静态对象调用构造和析构的顺序问题:
class Date
{
public:
Date(int year = 1)
{
_year = year;
cout << "Date()->" << _year << endl;
}
~Date()
{
cout << "~Date()->" << _year << endl;
}
private:
int _year;
};
void func()
{
Date d3(3);
static Date d4(4);
}
Date d5(5);
static Date d7(7);
Date d6(6);
static Date d8(8);
int main()
{
Date d1(1);
Date d2(2);
func();
return 0;
}
我们创建了一批对象,再令main函数正常结束,观察输出,可以总结出构造函数和析构函数调用的顺序:
构造函数:按照代码执行顺序进行创建。①顺序执行全局(包括全局静态)对象的创建;②进入main函数,顺序执行;③遇到函数,进入后顺序执行。
析构函数:按照声明周期结束时间进行调用。局部对象(后定义先析构)->全局和静态对象(后定义先析构)。
2.3 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,也是在初始化的时候发挥作用。拷贝构造函数只有一个由const修饰的本类类型对象的引用作为参数,在用已存在的类类型对象创建新对象时自动调用。
拷贝构造函数说明
①拷贝构造函数实际上是构造函数的一个重载形式,也是一个默认成员函数。当我们写了构造函数没有写拷贝构造函数时,编译器会默认生成一个拷贝构造函数;
当我们写了拷贝构造函数而不写构造函数时,编译器会认为拷贝构造函数属于构造函数,就不会再生成构造函数。这个时候再创建新对象就会报错找不到默认构造。这时除了自己写一个构造函数外,还可以使用default关键字,强制让编译器生成一个默认构造函数。
class Time
{
private:
int _hour;
public:
Time() = default; //强制编译器生成默认构造
Time(const Time& t)
{
_hour = t._hour;
}
};
class Date
{
private:
int _year;
public:
Date(int year)
{
_year = year;
}
};
int main()
{
Date d1(2024);
Date d2(d1); //只有构造函数,会生成拷贝构造
Time t1; //error 不存在默认构造 只有拷贝构造函数,不会生成构造函数
Time t2(t1);
}
②注意拷贝构造函数形式:拷贝构造函数是构造函数的重载形式,所以除了参数和构造函数一样。拷贝构造函数的参数只有一个,是const修饰的本类类型对象的引用。但是我们了解了this指针的存在,所以要注意分清调用的形式,实参应该是已经拷贝的“原件”。
class Date
{
private:
int _year;
int _month;
int _day;
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
};
int main()
{
Date d1(2024,2,27);
d1.Print();
Date d2(d1);
d2.Print();
}
③对于参数的说明:拷贝构造的一个参数必须是类类型对象的引用,如果不传引用而传值则会导致无穷递归调用引发报错。
void func(Date d)
{}
int main()
{
Date d1(2024,2,27);
func(d1);
}
对于以上代码,在func函数使用了值传递,实际上就是将d1的值传给参d。类似于C语言中的结构体传值调用,C++的自定义类型值传递时都需要调用拷贝构造,在执行到这里的时候编译器将d1的值拷贝到d中,所以这时就需要调用Date类的拷贝构造函数。
Date(const Date d) //error
{
_year = d._year;
_month = d._month;
_day = d._day;
}
那么如果在实现拷贝构造函数的时候传值,那么在调用拷贝构造函数的时候,参数为了拿到值会再去调用拷贝构造,这次的拷贝构造仍然是值传递,为了获取参数的值会去调用下一次拷贝构造,因而产生无穷递归。
④编译器默认生成的拷贝构造函数,对内置类型是对内存存储中字节完成拷贝,这种拷贝称为浅拷贝,或值拷贝。对于自定义类型则是调用它的拷贝构造函数。
class Time
{
public:
Time()
{
_hour = 1;
}
Time(const Time& t)
{
_hour = t._hour;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 2000;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
//oupput:
//Time::Time(const Time&)
}
⑤深拷贝和浅拷贝:浅拷贝仅仅是对字节进行逐一拷贝,这种拷贝方式在面对正常情况并无问题,但是一旦遇到申请资源的成员,浅拷贝便会产生问题。
class Stack
{
private:
int* _arr;
int _capacity;
int _top;
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == nullptr)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
对于上述栈这个类,如果仅仅是浅拷贝的方式,那么st2的_arr成员的值就会和st1的_arr值相同。但是我们知道这个_arr成员指向的是对上开辟的空间,如果我们的两个栈st1和st2的_arr成员相同,那就意味着是二者共用一块空间,这就出现了问题。
对于这种申请了资源的情况,我们就需要深拷贝,即对对象下所管理的深层空间也进行拷贝,此时就需要自己实现拷贝构造函数了。
class Stack
{
private:
int* _arr;
int _capacity;
int _top;
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == nullptr)
{
perror("malloc fail");
return;
}
_capacity = capacity;
_top = 0;
}
Stack(const Stack& st)
{
_arr = (int*)malloc(sizeof(int) * st._capacity);
if (_arr == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_arr, st._arr, sizeof(int) * st._capacity);
_capacity = st._capacity;
_top = st._top;
}
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
⑥拷贝构造函数自动调用的场景:a.用存在的对象初始化新对象;b.函数参数类型为类类型对象;c.函数的返回值类型为类类型对象。