目录
🙊 构造函数🙊
试想以下当用 c 语言实现一个数据结构,一般都需要进行初始化和销毁,如果忘记做初始化和内存清理的话,会造成内存泄漏等等问题,这十分的不方便,而 c 语言每次初始化和销毁的时候,都需要传递参数,所以 c++ 就对其进行了改进,让编译器自动来做这件事情,就有了前面介绍过的 this 指针。
而针对这个问题,初始化对象在 c++ 中有一个特殊的名字–构造函数,注意这里的构造函数虽然叫构造函数,但是并不是在构造一个对象,而是在初始化对象。
💖 构造函数的特征
构造函数是特殊的成员函数,是祖师爷钦点的成员函数,其特征如下:
1、函数名和类名相同
2、不用写返回值
3、对象实例化时编译器自动调用对应的构造函数
4、构造函数可以重载,一个类有多个构造函数,也就是说一个对象有多种初始化方式
💖 构造函数的使用方式及注意事项
以下是构造函数的代码,而根据构造函数的特征,这里写了两个可以构成函重载的构造函数。
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = nullptr;
_size = _capacity = 0;
}
Stack(int n)
{
_a = (int*)malloc(sizeof(int) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_size = 0;
}
private:
// 成员变量
int* _a;
int _size;
int _capacity;
};
此时主函数里面创建一个对象的时候,不需要再调用初始化函数,就可以直接进行对象的初始化。
int main()
{
Stack st;
return 0;
}
此时运行程序执行完第一条指令后,可以看到对象 st 被初始化了,这就说明构造函数是对象实例化的时候自动调用的。
而构造函数可以发生重载,如下面的这段代码,对象 st1 就调用的是第二个构造函数。
int main()
{
Stack st;
Stack st1(4);
return 0;
}
因为使用 c 的时候,会经常忘记调用初始化函数,所以 c++ 直接将其设计到类里面,就避免了忘记调用初始化函数而导致程序内存泄漏等现象。
注意:
Stack st1(4) 不可以理解为与 st1.Stack(4) 等价,因为构造函数是一个特殊函数,应该是先实例化出对象再进行调用,可以理解为 Stack st1(4) 与 Stack st1; + st1.Stack(4); 等价。但是这种显示调用写法和 c 语言就没有差别了,所以这里就变成了 c++ 的一个特例:类名 + 对象(参数列表),调用带参的构造函数,或者:类名 + 对象,调用无参的构造函数。
问题:
为什么无参构造的时候不加括号,而带参构造的时候加括号呢?
因为如果无参构造加括号,就无法判断是函数声明还是调用无参构造,会出现编译器的报错。
🙊析构函数🙊
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,因为对象是创建在栈上的,函数结束栈帧销毁对象才被销毁。
局部对象销毁工作是由编译器完成的,对象在出了作用域就代表对象的生命周期结束,此时需要被销毁,销毁时会自动调用析构函数完成对象中资源的清理工作比,如动态开辟的空间需要释放需要调用析构函数。
💖 析构函数的特征
析构函数是特殊的成员函数,其特征如下:
1、 析构函数名是在类名前加上字符 ~。
2、无参数无返回值类型。
3、 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4、 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。
💖 析构函数的使用方式及注意事项
析构函数使用方法示例如下面代码所示:
class Stack
{
public:
Stack()
{
_a = nullptr;
_size = _capacity = 0;
}
Stack(int n)
{
_a = (int*)malloc(sizeof(int) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_size = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
// 成员变量
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st;
Stack st1(4);
return 0;
}
c 语言实现和 c++ 实现代码对比分析:
注意 return 相当于宣告函数生命周期已经结束,所以当执行到 return 语句的时候,会自动跳转到析构函数,将其进行释放。
💖 定义一个类并初始化
若我们想定义一个日期类,将如何进行初始化呢?请看如下代码:
class Date
{
public:
//不带参的构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//带参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//无参初始化
Date d1();
//带参初始化
Date d2(2023, 2, 4);
}
此时这里有两个构成函数重载的构造函数,有了前面学习的缺省参数中全缺省的概念其实可以进一步合并,代码如下:
class Date
{
public:
不带参的构造函数
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
带参的构造函数
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
可以看到全缺省构造函数使用起来十分方便,运行结果如下:
但是构造函数和全缺省构造函数可不可以同时存在呢?
我们知道在语法上二者是构成函数重载,可以同时存在,但是实际中二者是不能同时存在,调用的时候会报错,因为不传参的时候,可以调用无参也可以调用全缺省。
注意构造函数和析构函数都是天选之子,如果我们不写,编译器也会自动生成,如果我们实现了,编译器就不会自动生成。
c++ 中规定,对象在实例化的时候,必须调用构造函数,看如下代码:
class Date
{
public:
//构造函数
Date(int year)
{
}
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//无参初始化
Date d1;
d1.Print();
return 0;
}
我们自己定义了构造函数,编译器就不会生成构造函数,此时编译就会报错。
那么问题来了,编译器会默认生成构造函数,那还需不需要我们自己写呢?
下面来看看不写默认构造函数,程序的执行结果是什么,代码如下:
class Date
{
public:
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//无参初始化
Date d1;
d1.Print();
return 0;
}
最后是随机值,执行结果如下:
💖 补充说明
💖 构造函数的补充说明
构造函数的一些说明:
1、关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?
2、对象调用了编译器生成的默认构造函数,但是 d 对象 _year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:
C++把类型分成内置类型 (基本类型) 和 自定义类型。内置类型就是语言提供的数据类型,如:int/char…指针等,自定义类型就是我们使用 class / struct / union 等自己定义的类型。
💖 默认生成的构造函数特性
默认生成的构造函数有以下几个特性:
1、内置类型成员不做处理
2、自定义类型的成员,会去调用它的默认构造(不用传参数的那个构造:无参、全缺省、编译器生成)
那么当我们自定义一个数据类型的时候,不写构造函数会调用默认构造函数,自动对其进行初始化,代码如下:
class Stack
{
private:
// 成员变量
int* _a;
int _size;
int _capacity;
};
class MyQueue {
public:
// 默认生成构造函数,对自定义类型成员,会调用他的默认构造函数
// 默认生成析构函数,对自定义类型成员,会调用他的析构函数
void push(int x) {
}
//....
Stack _pushST;
Stack _popST;
int _size = 0;
};
int main()
{
//Date d1;
//d1.Print();
MyQueue q;
return 0;
}
这里 MyQueue 是自定义类型,会自动调构造函数和析构函数,所以 Stack _pushST 和 Stack_popST 不需要手动调用析构函数,因为出了 q 的作用域,MyQueue 会调用系统自动生成的析构函数,自动生成的析构函数会对自定义类型的结构成员进行处理。执行结果如下:
注意:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给缺省值。
如以下代码所示:
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
注意:
1、缺省值不是初始化,如果不写构造函数,内置类型就会使用缺省值进行初始化。
2、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
3、无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。默认构造函数就是不传参就可以调用的构造函数,一般建议每个类都提供一个默认构造函数。
如以下代码调用的是编译器自己生成的默认构造函数。
class Date
{
public:
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
//MyQueue q;
return 0;
}
如果显示写一个构造函数,编译器就不会生成默认构造函数,而导致报错
class Date
{
public:
Date(int year)
{ }
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
//MyQueue q;
return 0;
}
报错内容如下:
如果不用编译器自动生成的,自己写一个无参的默认构造函数或者全缺省的默认构造函数也可以
代码如下:
```cpp
class Date
{
public:
//无参默认构造函数
Date()
{ }
//全缺省默认构造函数
Date(int year = 0)
{ }
//打印函数
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
//MyQueue q;
return 0;
}
🙊拷贝构造函数🙊
💖 拷贝构造函数介绍
拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
💖 拷贝构造函数举例刨析
看如下代码,如何用 d1 初始化 d2 呢?
int main()
{
Date d1(2023, 2, 4);
Date d2(d1);
return 0;
构造函数可以重载,现在要用同类型的对象进行初始化,就需要用到拷贝构造函数。那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?看如下代码能否用这种方式对 d1 进行拷贝?
class Date
{
public:
Date(int year = 1000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 4);
Date d2(d1);
return 0;
}
d1 将其传递给 d,然后左边成员通过 this 指针访问 d2,看似没有问题,但结果是编译器报错了
问题分析:
编译器会认为这样的拷贝构造会无穷递归,所以 c++ 规定一定要使用引用
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
为什么不用引用会导致无穷递归呢?以下面的代码举例:
void Func1(Date d)
{
}
// 传引用传参
void Func2(Date& d)
{
}
int main()
{
//创建对象 d1 并调用构造函数进行初始化
Date d1(2023, 2, 3);
Date d2(d1);
Func1(d1);
Func2(d1);
return 0;
}
Func1 是传值传参,Func2 是传引用传参,因为我们知道,传值传参实际上是拷贝,就是开辟一块空间,将实参 d1 拷贝给形参 d,而传引用传参实际上形参是实参的别名,以前我们传的是内置类型,编译器知道怎么进行拷贝,但是自定义类型编译器不能随便拷贝,只能调用拷贝构造。
为什么呢?我们再举一个样例进行更加深入的说明:
如果是自定义的 Date 类,其里面的成员只有十几个字节,如果编译器还像之前那样直接把 d2 拷贝给 d1,由于日期类对象只有十几个字节,编译器可以进行浅拷贝将 d2 逐字节拷贝给 d1,此时看起来并没有什么问题。
但如果是 Stack 类,编译器如果按照逐字节的方式进行浅拷贝,会导致指向同一块空间的问题,而两个对象都会调用析构函数,所以会造成空间的重复释放的问题。图示如下:
基于上述问题,c++ 规定,对于自定义类型,编译器会调用拷贝构造函数来避免类似的问题,至于拷贝构造函数是深拷贝还是浅拷贝,需要程序员自己去判断完成。
我们通过以下程序进行验证:
class Date
{
public:
Date(int year = 1000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
}
// 传引用传参
void Func2(Date& d)
{
}
int main()
{
Date d1(2023, 2, 3);
Date d2(d1);
Func1(d1);
Func2(d1);
return 0;
}
给此处打一个断点
调试运行发现逐语句调试直接跳到了拷贝构造函数,并没有进入 func1 函数。
解释说明:
1、因为要调用 func1 就要先传参(如果不用引用就是传值传参),而传值传参就是对参数的拷贝,而前面讲到 c++ 规定自定义类型的拷贝不管是浅拷贝还是深拷贝,都要调用拷贝构造函数。而用引用传参就不需要调用拷贝构造函数。
2、这里 d1 为已经实例化的对象,而 d1 要拷贝给 d2,对 d2 进行实例化,由于 d2 是自定义类型的函数,所以需要调用拷贝构造函数,我们知道调用函数之前需要先传参,再为函数建立栈帧,由于这里使用传值传参,传值传参不是传实参本身,而是传实参的拷贝,这又是一个拷贝的过程,又需要调用拷贝构造函数,而再调用拷贝构造函数之前还需要先传参,再建立函数栈帧,传值传参又是一个拷贝的过程,又会调用拷贝构造函数,调用拷贝构造函数又会先传参。。。所以就形成了一个死循环。
而传引用的时候,d 是d1 的别名,this 就是 d2,所以就完成了 d1 传给 d2 的过程。
//Date d2(d1);
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
还需注意,拷贝构造还需加 const 进行修饰,避免 d 被修改,因为这里 d 是 d1 的别名,d 是可读可写的,d1 变成只读的,这是权限的缩小,编译器允许权限的缩小而不允许权限的放大,所以一般拷贝构造需要加 const。
//Date d2(d1);
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
除此之外还有最后一点,就是拷贝构造有两种书写方式,使用哪一种都可以:
int main()
{
Date d1(2023, 2, 3);
Date d2(d1);
Date d3 = d1;
Func1(d1);
Func2(d1);
return 0;
}
🙊日期计算器实现🙊
我们根据以上介绍的相关内容实现一个日期计算器,计算 n 天以后的日期,类似加法进位的思想,天满了,进月位,月满了进年位,但是天和月的进位不规则,有些月是 30 天,有些月是 28 天,还需要考虑闰年的问题,那么根据这个思路,我们首先需要知道那个月都有多少天,才能进行进位判断。
💖 获取每个月的天数
按照上面的思路,我们这里实现一个函数获取每个月的天数,我们可以使用数组来进行处理, 这里给定了大小为 13 的数组,因为第 0 个位置的数可以不去使用,只需要将每个月的月数与数组的位置匹配即可。
注意:
这里需要特别考虑 2 月,因为闰年的 2 月天数是不固定的,需要单独进行判断,而闰年的规则:四年一闰、百年不闰、四百年为闰。根据这个规则写出以下获取月份天数的代码。
代码如下:
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
💖 获取 x 天以后的日期
这里先考虑将 x 先加到 “ 天 “ 位,如果天大于当前月的天数,就要进位,如果 “ 月 ” 位等于 13 就将其置为 1,然后 “ 年 ” 位加 1。而最后只需要返回 *this 即可,因为 this 是对象的指针,而 *this 是这个对象。
代码如下:
Date GetAfterXDay(int x)
{
_day += x;
while (_day > GetMonthDay(_year, _month))
{
//进位
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 12)
{
_year++;
_month = 1;
}
}
}
💖 代码实现以及分析
通过以上分析先写出实现的总代码:
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;
}
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
Date GetAfterXDay(int x)
{
_day += x;
while (_day > GetMonthDay(_year, _month))
{
//进位
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 12)
{
_year++;
_month = 1;
}
}
return *this;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
Date d2 = d1.GetAfterXDay(100);
d1.Print();
d2.Print();
// 实现一个函数,获取多少以后的一个日期
return 0;
}
运行结果如下:
问题:
我们发现,d1 和 d2 都被更改了,为什么呢?
因为 d1 调用 GetAfterXDay() 函数,其 _year、_month、_day 都是 d1 的变量。
那么怎么能不对 d1 进行更改呢?
应该先拷贝 d1 生成一个临时对象,而调用函数中,this 就是 d1 的地址,所以这里直接调用一个拷贝构造函数将 this 拷贝给 tmp,然后只改变 tmp 的成员,最后返回 tmp 就达到了效果。
代码如下:
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;
}
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
Date GetAfterXDay(int x)
{
Date tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
//进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 12)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
Date d2 = d1.GetAfterXDay(100);
d1.Print();
d2.Print();
// 实现一个函数,获取多少以后的一个日期
return 0;
}
执行结果如下:
注意:
1、上述代码中 tmp 是一个局部对象,d2 = d1.GetAfterXDay(100) 类似于一个日期加一个天数,因为我们知道 j = i + 100,其 i 的值是不改变的。
2、同理 d1 的日期 + 100天结果给了 d2,d1 也是不变的。注意这里不能使用引用返回,因为 tmp 出了作用域就销毁了,传值返回返回的是 tmp 的拷贝,调用拷贝构造函数拷贝出一个临时对象进行返回。
3、而类似我们也可以实现 += 的逻辑,而 += 这里就是改变自己。注意 += 的实现可以使用引用返回,因为 *this 出了作用域还存在,所以可以返回引用,而不用调用拷贝构造函数。
代码对比:
// +
Date Add(int x)
{
Date tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
// 进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
// +=
Date& AddEqual(int x)
{
_day += x;
while (_day > GetMonthDay(_year, _month))
{
// 进位
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
💖 总代码
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造
// Date d2(d1);
Date(const Date& d)
{
cout << "Date(Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
// +
Date Add(int x)
{
Date tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
// 进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
// +=
Date& AddEqual(int x)
{
_day += x;
while (_day > GetMonthDay(_year, _month))
{
// 进位
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
void Print()
{
//cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 3);
Date d2 = d1.Add(100);
Date d3 = d1.Add(150);
d1.Print();
d2.Print();
d3.Print();
d1.AddEqual(200);
d1.Print();
// 实现一个函数,获取多少以后的一个日期
return 0;
}
💖💖💖💖💖至此构造函数、析构函数、拷贝构造函数都已经介绍完了,希望可以帮助到大家。💖💖💖💖💖💖💖