C++类与对象三
上期我们介绍了类的实例化,大小计算,还有this指针。这期我们继续深入更高层次的用法
类的六个默认函数
如个一个类里面没有成员,就是空类,但是空类里面真的什么都没有吗,并不是,在编译器中,会默认生成六个默认成员函数,但是这只是隐式生成的,用户是看不见的。
默认成员函数:用户没有显示显现,编译器自动生成的成员函数称为默认成员函数
VS2022自动生成的默认成员函数
六个默认函数主要功能
构造函数
概念
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 _day;
int _month;
int _year;
};
int main() {
Date d1;
d1.Init(2011, 02, 05);
d1.Print();
Date d2;
d2.Init(2024, 10, 10);
d2.Print();
}
对于以上函数,我们对d1
和d2
对象初始化时,都可以调用Init
函数来初始化赋值,但是每次创建一个对象进行初始化,就必须调用一次Init
函数,非常的不方便。有没有一种函数,在创建对象时,默认帮你完成初始化呢?
构造函数是一个特殊的成员函数,名字和类相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
注意是**自动调用!!!**极大节省调用函数时的燃烧的卡路里
特性
构造函数是特殊的成员函数,虽然名字叫构造,但是主要任务不是开空间创建对象,而是初始化对象
1、构造函数与类名相同
2、无返回值
3、对象实例化时编译器自动调用对应的构造函数
4、构造函数可以重载
主要分为两种构造方式
1、无参构造
2、有参构造
class Date {
public:
Date() {
}//无参构造
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}//有参构造
void Print() {
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main() {
//无参构造
Date d1;
//有参构造
Date d2(2014,11,11);
d2.Print();
Date d3();
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
return 0;
}
无参和有参的区别
如果用户没有显示定义构造函数,编译器会自动生成一个无参的默认的构造函数,一旦用户显示定义编译器就不再生成。
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 _day;
int _month;
int _year;
};
int main() {
//无参构造
Date d1;
return 0;
}
注释掉有参构造,可以识别到编译器默认的构造函数
放开有参构造,则编译器自动生成的构造函数会变成有参构造,因此系统无法识别无参调用
注意事项
一、有关于系统的默认构造,有很多同学有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d1
对象调用了编译器生成的默认构造函数,但是d1
对象_year/_month/_day
,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
为了验证上面的看法,我们需要在不同环境的编译器下测试以下代码
VS2022
class Stack {
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a==nullptr) {
perror("malloc fail!!");
}
_capacity = capacity;
_top = 0;
}
private:
int* _a;
int _capacity;
int _top;
};
class MyQueue {
Stack _push;
Stack _pop;
int _size;
};
int main() {
Stack t1;
MyQueue m1;
return 0;
}
我们给Stack
显示写上构造函数,而MyQueue
这个类是没有显示写构造函数的,是编译器默认生成的构造函数。
在监视窗口下,t1
对象是显示的构造函数,m1
是编译器生成的。
可以看到m1
对象没有显示的构造函数,但是_size
的值却变成了0!!!!
首先,C++把类型分成内置类型和自定义类型。内置类型就是语言提供的数据类型,例如int、char、double.....
,自定义类型就是struct/class/union
等自己定义的类型。让我们看看下面的代码
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;
int _month;
int _day;
//这三个都是内置类型
Time _t1;//自定义类型
};
int main() {
Date d1;
return 0;
}
调试以上代码调试发现,C++对内置类型是不初始化的,在自定义类型中,有显示的构造函数。
可以看见在Date
中三个内置类型是随机值,而自定义类型中是用户自己写的显示构造函数。当我们注释掉Time
类中的构造函数,使用系统生成的默认构造函数时
发现**他们都成了乱码!!!**回顾前面内容
class MyQueue {
Stack _push;
Stack _pop;
int _size;
};
为什么这段代码中int _size
明明是内置类型,为什么又被初始化了!!!
其实最根本的原因还是VS2022的锅,这是新版本的优化!!!因此这是个巨坑。稍有不慎就会因为初始化的问题出现各种问题,当切换到VS2013调试MyQueue
他就原形毕露(由于这里不方便展示,只能说出结论)
同学们到这里可能就乱了,首先我们要记住:C++对自定义类型是会初始化的,但是会调用他的默认构造函数,对内置类型,是不会初始化的!,因此VS2022这里就是一个妥妥的渣男,欺骗人感情。因此_size
实际上是随机值的。
针对内置成员不初始化的问题,在C++11中,新增了一个补丁,在定义内置类型时,可以给缺省值
class Date {
private:
int _year=1970;
int _month=01;
int _day=01;
Time _t1;
};
那么这三个自定义类型是有内存的吗?答案是没有,因为这个是缺省值。
二、无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、用户没有写编译器默认生成的构造函数,都是默认构造函数
class Date {
public:
//无参构造
Date() {
_year = 1970;
_month = 01;
_day = 01;
}
//有参构造
Date(int year=1970,int month=01,int day=01) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
return 0;
}
以上代码会报错,因为只能有一个默认构造函数
写法
根据上面对构造函数的了解,可以给出参考写法—全缺省构造
class Date {
public:
//有参构造
Date(int year=1970,int month=01,int day=01) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
Date d2(2024, 10, 10);
return 0;
}
一举两得!
析构函数
概念
通过前面的构造函数,我们知道一个对象是怎么来的,那么对象又是怎么没的呢。
析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部变量销毁是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对本对象的资源清理
特性
1、析构函数名是在类名前加上字符~
2、无参数无返回值
3、一个类只能有一个析构函数,用户没有显示定义,则编译器会自动生成默认的析构函数
4、对象生命周期结束时,C++编译器自动调用
5、析构函数不能重载
class Stack {
public:
Stack(int capacity = 4) {
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr) {
perror("malloc fail");
}
_capacity = capacity;
_size = 0;
}
void Push(int x) {
_array[_size++] = x;
}
~Stack() {
if (_array) {
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int* _array;
int _capacity;
int _size;
};
int main() {
Stack s1;
Stack s2;
s1.Push(1);
s2.Push(3);
return 0;
}
当上面的对象生命周期结束时,会自动调用析构函数~Stack()
清理内部资源
生命周期结束资源清理
那么系统默认生成的析构函数又是怎么样的呢
class Time {
public:
~Time() {
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private:
int _year = 1970;
int _month = 01;
int _day = 01;
Time _t1;
};
int main() {
Date d1;
return 0;
}
运行结果是创建哪一个类就运行哪一个类的默认析构函数
这段代码一共运行了倆个析构函数,第一个是
Time
类显示定义的,第二个是Date
系统默认生成的(注意:内置类型是由系统回收资源的,而自定义类型是需要析构函数去清理的)
因此,析构函数是非常重要的,可以在对象生命周期自动调用,清理不需要的资源,降低内存泄漏的风险