目录
1.类的6个默认成员函数
2. 构造函数
2.1 构造函数概念的引出
2.2 构造函数的特性
3. 析构函数
3.1 析构函数的概念
3.2 特性
未使用构造与析构的版本
使用了构造与析构函数的版本
4. 拷贝构造函数
4.1 拷贝构造函数的概念
4.2 特性
结语
本节我们来认识一些类的默认成员函数。
1.类的6个默认成员函数
如果一个类中什么都不写,我们简称它为空类。
但空类中真的什么都没有吗?
不是这样的,任何类在我们什么都不写的情况下,编译器会自动生成六个默认成员函数。
默认成员函数:当用户没有显式定义时,编译器自动生成的成员函数。
2. 构造函数
构造函数是六个默认成员函数中最为重要的成员函数。
2.1 构造函数概念的引出
为什么要有构造函数呢?
我们来看看这段示例代码:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void showinfo()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 2, 1);
d1.showinfo();
Date d2;
d2.Init(1997, 1, 1);
d2.showinfo();
return 0;
}
对于Date类,创建对象时我们可以调用公有方法Init() 来初始化对象,但我们每次创建对象时都需要调用它,会显得有一些麻烦,那么有没有一种方法,在我们创建对象的同时就能完成对他的初始化呢?
答案是有的。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 构造函数的特性
构造函数是特殊的成员函数,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。我们将上面的Date类代码进行改造:
class Date { public: Date()//无参构造函数 {} Date(int year, int month, int day)//带参构造函数 { _year = year; _month = month; _day = day; } void showinfo() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//调用无参构造函数 d1.showinfo(); Date d2(1997, 1, 1);//调用带参构造函数 d2.showinfo(); Date d3(); return 0; }
在这里我们看到,d3的用法是调用无参函数,调用无参函数是不能在对象后加括号的。
当我们调用无参构造函数时,发现输出的成员变量都是随机值,我们接着往下看。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。我们将显式定义的构造函数全部注释,然后再调用无参默认构造,结果如下:
6. 关于编译器生成的默认成员函数,很多朋友会有疑惑:不实现构造函数的情况下,编译器会
生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默
认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的
默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数。
class Time { public: Time() { _hour = 1; _minute = 30; _second = 47; } private: int _hour; int _minute; int _second; }; class Date { public: void showinfo() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; Time _t; }; int main() { Date d1; d1.showinfo(); return 0; }
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值
使用如下:class Time { public: Time() { _minute = 30; _second = 47; } private: int _hour=1; int _minute; int _second; }; class Date { public: void showinfo() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year=1997; int _month=10; int _day=9; Time _t; }; int main() { Date d1; d1.showinfo(); return 0; }
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数,所以默认构造函数有三种。(不需要传参就可以调用的,都可以称为默认构造函数。)
下面这段代码就无法正常执行。
class Date { public: Date() {} Date(int year=2020, int month=1, int day=1) { this->_year = year; this->_month = month; this->_day = day; } void showinfo() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0; }
因为全缺省构造函数与无参构造函数都属于默认构造函数,调用不明确。默认构造函数只能有一个。
3. 析构函数
3.1 析构函数的概念
学习了上面的构造函数,我们知道了对象是如何创建的,那么对象是如何销毁的呢?
简单来说,对象通过调用析构函数清空对象的内容,然后由编译器销毁空间。
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
4. 对象生命周期结束时,编译系统自动调用析构函数。
这里我们借用c++实现Stack的部分代码做实例。如果想看完整代码的同学可以点击链接查看。
目录九:c++实现类封装Stack
未使用构造与析构的版本
typedef int DataType;
class Stack
{
public:
//初始化
void STInit()
{
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = 4;
}
void STPush(DataType x)
{
_a[_size] = x;
_size++;
}
//销毁
void STDestory()
{
if (_a == NULL)
return;
free(_a);
_a = NULL;
_size = 0;
_capacity = 0;
}
private:
DataType* _a;
int _size;
int _capacity;
};
int main()
{
Stack ST;//创建对象
ST.STInit();//初始化对象
ST.STPush(1);//压栈
ST.STPush(2);
ST.STDestory();//清空对象
return 0;
}
使用了构造与析构函数的版本
typedef int DataType;
class Stack
{
public:
//初始化
Stack()
{
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = 4;
}
void STPush(DataType x)
{
_a[_size] = x;
_size++;
}
//销毁
~Stack()
{
if (_a == NULL)
return;
free(_a);
_a = NULL;
_size = 0;
_capacity = 0;
}
private:
DataType* _a;
int _size;
int _capacity;
};
int main()
{
Stack ST;//创建对象,同时系统自动调用构造函数进行初始化
ST.STPush(1);//压栈
ST.STPush(2);
return 0;//对象生命周期结束,系统自动调用析构函数清空对象内容。
}
我们看这段代码:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
}
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1)
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
return 0;
}
在这段代码里有两个类,Date类的成员变量中有Time类的对象,对于两个类的构造与析构我们都只是做了打印函数名的操作。
我们看到,只是创建了一个Date类的d1对象,但却调用了两个类的构造与析构,根据打印内容,我们可以明晰函数的调用顺序。那么,为什么会出现上面这个结果呢?
内置类型成员的销毁不需要资源清理,而销毁类中的自定义类型变量时,需要调用其析构函数进行清理。
因为Time类的对象是Date类的成员变量,因此必须先创建Time类的对象,才能创建Date类对象。同时必须先调用Date类的析构函数,然后调用Date类中自定义类型的析构函数。
也可以这样理解:
//在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
//_day三个是
// 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对
// 所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
main函数
// 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date
类的析构函
// 数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部
调用Time
// 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析
构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
有兴趣的同学可以研究一下不同存储区对象的析构。
4. 拷贝构造函数
4.1 拷贝构造函数的概念
在现实生活中,我们见过两个一模一样的人,并称其为双胞胎。
那么在创建对象时,能不能创建一个与已存在对象一模一样的新对象呢?
在之前的学习中,我们可以用拷贝来实现,在c++中同样可以。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
下面这段代码是正确示范:
class Date
{
public:
Date(int year = 2020, 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;
}
void showinfo()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year=1997;
int _month=10;
int _day=9;
};
int main()
{
Date d1(2022, 9, 13);
Date d2(d1);
d2.showinfo();
return 0;
}
关于第二点,为什么传值方式会无穷递归呢?
这是因为形参就是原对象的拷贝,需要调用拷贝函数,但拷贝函数需要传入对象的形参,因此构成了无限递归拷贝的现象。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Date
{
public:
Date(int year = 2020, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void showinfo()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year=1997;
int _month=10;
int _day=9;
};
int main()
{
Date d1(2022, 9, 13);
Date d2(d1);
d2.showinfo();
return 0;
}
这段代码里我们并没有显式实现拷贝构造函数,但结果依旧。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的
既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
我们来看看Stack类是怎么做的
typedef int DataType;
class Stack
{
public:
//初始化
Stack()
{
_a = (DataType*)malloc(sizeof(DataType) * 4);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = 4;
}
void STPush(DataType x)
{
_a[_size] = x;
_size++;
}
//销毁
~Stack()
{
if (_a == NULL)
return;
free(_a);
_a = NULL;
_size = 0;
_capacity = 0;
}
private:
DataType* _a;
int _size;
int _capacity;
};
int main()
{
Stack ST;
ST.STPush(1);
ST.STPush(2);
ST.STPush(3);
Stack st(ST);
return 0;
}
在这段代码里,我们并没有显式实现Stack类的拷贝构造,上面的Date类同样没有,但他们的结果却截然不同。
很明显,这样做是错的。那这是为什么呢?
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5. 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
小建议:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
下期再见!