1.类的六个默认成员函数
默认成员函数:用户没有显式实现,编译器自动生成的函数。
如果一个类是空类,难道就什么都没有吗?
并不是,编译器会自动生成生成六个默认成员函数。
2.构造函数
2.1概念
有以下Date类:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
可以看到,我们使用Init()函数初始化年、月、日。构造函数就与Init()函数类似,完成初始化。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象是由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2特性
注意一个点:虽然函数名叫做“构造函数”,但是不是进行开空间创造对象,只是初始化!
1.函数名与类名相同。
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载(可以写多个,有多种初始化方式)
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); // 调用带参的构造函数 }
5.如果类中没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date { public: /*void Init(int year = 2024, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }*/ void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; } int main() { //生成无参默认构造函数 Date d1; d1.Print();//年月日为随机值(当然有的编译器会自动初始化为0) }
6.C++把类型分成内置类型和自定义类型。内置类型就是语言提供的数据类型,入:int/char/指针…,自定义类型就是我们使用class/struct/union等自己定义的类型,内置类型会生成随机值(有的编译器会处理成0),看下面的程序,编译器会对自定义类型成员_t调用的它的默认构造函数。
class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { public: void Init(int year = 2024, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: //自定义类型 int _year; int _month; int _day; //内置类型 Time _t; }; int main() { Date d1;//输出结果:Time() return 0; }
7.关于第6点说的什么是默认构造函数?
无参的构造函数、全缺省的构造函数和编译器默认生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意: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 = 2024; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
实践中总结:
1、般情况下都需要我们自己生成构造函数。
2、只有少数情况下可以让编译器自动生成构造函数,类似MyQueue,成员全是自定义类型。MyQueue函数自动生成构造函数,其调用了Stack的构造,完成2个成员的初始化。
class Stack { Stack(int n = 4) { arr = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == arr) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } } // ... private: STDataType* arr; int capacity; int top; }; class MyQueue { Stack s1; Stack s2; };
3.析构函数
3.1概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2特性
析构函数是特殊的成员函数:
- 析构函数名是在类名前加上字符“~”。
- 无参数无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时,C++编译器自动调用析构函数。
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() { 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); }
编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。(跟构造函数类似:a.内置类型不做处理。b.自定义类型去调用他的析构,而且是在任何情况下都会自动调用析构函数)
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; }
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时(比如有使用malloc等),一定要写,否则会造成资源泄露,如Stack类。
实践中总结:
1、有资源需要显式清理,就需要写析构。如Stack。
2、有两种场景不需要写析构,默认生成就可以了。
a、没有资源需要清理,如Date;
b、内置类型成员没有资源需要清理。剩下都是自定义类型成员。如:MyQueue
class Stack { //… }; class MyQueue { Stack s1; Stack s2; int size = 0; };
4.拷贝构造
4.1概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2特性
拷贝构造函数是特殊的成员函数
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,会引发无穷递归调用。C++规定进行拷贝时要调用拷贝构造,所以传值传参和传值返回都要调用拷贝构造。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1);//Date d2 = d1;等价于第一种写法,到以后也多用第二种 return 0; }
正常情况:
- 会引发无穷递归调用的情况:
若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝/值拷贝。
class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time(const Time& t) { _hour = t._hour; _minute = t._minute; _second = t._second; cout << "Time::Time(const Time&)" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 2024; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数 // 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数 Date d2(d1); return 0; }
注意:编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
Stack类必须自己写拷贝构造函数
6. 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名,没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么这时的引用就相当于一个野引用。
实践中总结:
1、如果没有管理资源,一般情况下不需要写拷贝构造,默认生成的拷贝构造就可以。如Date。
2,、如果都是自定义成员,内置成员也没有资源管理,默认生成的拷贝构造就可以。如MyQueue。
3、一般情况下,不需要写析构函数就是不需要写拷贝构造。
4、如果内部有指针或一些值指向资源,需要显式写析构释放,通常就要完成深拷贝。如二叉树、队列。栈等。
题目:
选B
解析:
1、类的析构函数一般按照构造函数调用顺序的逆序调用,但是要注意static对象的存在,会改变对象的生存作用域,需要等待程序结束时才会调用析构函数,释放对象。
2、全局对象先于局部对象进行构造。
3、局部对象按照出现顺序进行构造,无论是否为static。
4、所以构造的顺序为c a b d。
5、析构函数的调用相反,但是要注意static修饰的对象,会放在局部对象之后进行析构。
6、得出答案:b a d c。