1.类的六个默认函数
如果说一个类什么成员都没有,简称空类。但是空类中并不是什么都没有,任何一个类创建完成的情况下,都会自动生成一下六个默认函数
calss Date
{};
就算是这样的一个空类,也会编译器也会自动生成六个默认函数
接下来就让我们讲讲这六个函数的用处
2.构造函数
2.1概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1 . 函数名与类名相同。
2 . 无返回值。
3 . 对象实例化时编译器自动调用对应的构造函数。
4 . 构造函数可以重载。
接下来我们继续用我们的Date类型举例
class Date
{
public :
Date(int year, int month = 1, int day = 1)//带参构造函数
{
_year = year;
_month = month;
_day = day;
cout << "1Date()" << endl;
}
Date()//无参构造函数
{
_year = 2;
_month = 2;
_day = 2;
cout << "2Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(10);//调用带参构造函数
Date d3(1, 2, 3);
return 0;
//注意:如果通过无参构造函数创建对象的时候,对象后面不需要加括号,否则就变成了函数声明
//Date d3();这是一个函数的声明,返回值是Date类型,函数名是d3,函数无参
}
通过调试
还有程序的运行结果我们可以看到在三个对象的创建过程中,确实调用了构造函数,并且就算不传参数它也会自动调用只是定义,它其实也是默认调用了构造函数
5.如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,则编译器将不再生成。所以我们没有定义构造函数,对象也可以创建成功,因为创建过程中会调用编译器自动生成的构造函数
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
7. 关于编译器生成的默认成员函数,大家可能会有疑惑,因为在Date类型对象的创建过程中,如果我们调用的是编译器自动生成的默认函数,就会发现对象中的_year_month\day还都是随机值。也就是说系统生成的构造函数好像没有啥作用。
其实在这个地方,C++将类型分为内置类型(基本类型)和自定义类型进行区别对待。内置类型就是语法已经定义好的类型,如int/char/float等。自定义类型是我们使用class/union/struct自己定义出来的类型。当我们的自定义类型(类型1)初始化时,如果这个自定义类型中还套有另外一个自定义类型(类型2),那么编译器生成的默认构造函数会让类型2自动调用类型2的默认构造函数,这里比较绕,具体和下图一起结合理解
类型1 Date 类型2 Stack
实验1:此时我们没有放入自定义类型
class Stack//类型2(是1个栈)
{
public:
Stack(int capacity = 4)//Stack的默认构造函数
{
if (capacity <= 0)
{
capacity = 0;
int* _a = nullptr;
_capacity = _size = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
}
private:
int* _a;
int _size;
int _capacity;
};
class Date//类型1
{
//此时我们没有显示定义Date的默认构造函数
private:
int _year;
int _month;
int _day;
//Stack _st;//没有放入我们的自定义类型
};
int main()
{
Date d1;
return 0;
}
通过调试我们看一下结果
此时程序已达末尾但是d1并没有显著改变,里面的值还是随机值。
实验2
在Date类型的成员变量中添加一个Stack类型(类型2)的成员,然后创建一个Date类型的对象
class Stack//类型2(是1个栈)
{
public:
Stack(int capacity = 4)//Stack的默认构造函数
{
if (capacity <= 0)
{
capacity = 0;
int* _a = nullptr;
_capacity = _size = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
}
private:
int* _a;
int _size;
int _capacity;
};
class Date//类型1
{
//此时我们没有显示定义Date的默认构造函数
private:
int _year;
int _month;
int _day;
Stack _st;
};
int main()
{
Date d1;
return 0;
}
调试
此时我们并没有显式定义Date的默认构造函数,但是Date中的_year,_month,_day被初始化为0,而其中的自定义类型Stack _st调用了自己的默认构造函数。
结论:编译器自动生成的默认构造函数并不是什么事情都不做。编译器默认生成的构造函数,会对自定义类型成员调用它的默认构造函数
注:实验2中这样的情况,内置类型可能会改变初始化为0,也可能继续是随机值,根据编译器不同而定
8.成员变量的命名风格
(一)用_(下划线开头)
class Date
{
private:
int _year;
int _month;
int _day;
Stack _st;
};
(二)用m_(下划线)开头
class Date
{
private:
int m_year;
int m_month;
int m_day;
Stack _st;
};
目的
将成员变量和成员函数的形参区分开来,防止混淆
class Date
{
public:
Date(int year)
{
year = year;
}
private:
int year;
}
若不加以区分,很容易出现形参和成员变量名字相同情况。
3.析构函数
3.1概念
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
3.2特性
析构函数是特殊的成员函数
其特性如下:
1.析构函数名是在类名前加上字符~。
2.没有参数,并且没有返回值。
3.一个类只有一个析构函数。若没有显式定义,系统会自动生成默认的析构函数。
4.对象的声明周期结束时,C++编译器会自动调用析构函数。
class Stack
{
public:
Stack(int capacity = 4)
{
if (capacity <= 0)
{
capacity = 0;
int* _a = nullptr;
_capacity = _size = 0;
}
else
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
}
~Stack()//析构函数
{
free(_a);
_a = nullptr;
_capacity = _size = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
~Stack()就是我们的析构函数,当一个Stack类型的对象声明周期结束时,编译器会自动调用,可以释放堆上的空间,并且将指针置为空。防止忘记free导致内存泄漏。
5.和构造函数相同,编译器自动生成的析构函数,会对自定义类型成员调用它的析构函数
4.拷贝构造函数
4.1概念
构造函数:只有单个形参,该形参是对本类类型的对象的引用(常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
4.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个并且必须要引用传参,使用传值会引发无穷递归调用
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);//使用拷贝构造函数创建对象
return 0;
}
假若传值而非引用会发生以下这种情况
Date 的形参d是d1的临时拷贝,d的构建就等同于const Date d(d1),因为此时创建了d,则就要取调用d的拷贝构造函数,从而发生无穷递归调用
`
3.若未显示定义,系统生成默认的拷贝函数。默认的拷贝函数对象按照存储的字节序完成浅拷贝,又是可能会发生错误。当对象中包含自定义类型时,会调用自定义类型的默认拷贝函数。
例如:
class Stack
{
int* a;
int* size;
int* capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
如图我们创建两个栈,st2 是 st1 的拷贝,系统默认调用拷贝函数进行浅拷贝,则st2.a = st1.a,则此时两个栈都指向同一块空间,公用同一个数组,改变st1就等于改变st2。假如Stack类包含析构函数,则st2销毁时调用一次,st1销毁时还得调用一次.但是他们两个指向同一块空间,同一块空间释放两次就会造成系统报错。
所以像Stack这样的类,编译器默认生成的拷贝构造函数无法满足我们的需求,需要我们自己实现深拷贝。深拷贝是一个复杂的过程,具体后面讲解
5.赋值运算符重载
5.1概念
C++为了增加代码的可读性引入运算符重载,运算符重载是具有特殊函数名的函数,让自定义类型可以像内置类型一样使用运算符,需要哪个运算符就重载哪个运算符
函数名字:关键字operator 后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
.* :: sizeof ?: . 注意以上五个运算符不能重载
5.2特性
//在类型创建中进行的运算符重载
void operator=(const Date&d);
//此时d1 = d2 等价与 d1.operator=(d2);
void operator=(const Date&d)
{
_year = d.year;
_month = d.month;
_day = d.day;
}
注意:以上函数时存在问题的,因为我们在实际运用过程中还可能遇到连续赋值的情况,例如a=b=c,在这种情况下我们的函数就无法满足需求。所以我们对以上函数进行优化
Date& operator=(const Date&d)
{
_year = d.year;
_month = d.month;
_day = d.day;
return *this;
}
我们进行连续赋值d1 = d2 = d3 ,此时d2和d3之间调用运算符重载函数,按道理来说应该返回d2,而此时我们只有d2的地址(this),所以我们需要解引用来返回d2。
这里我们还要思考,传值返回,编译器会生成一个临时对象接收运算符重载函数的返回值,此时就等于又创建了一个Date类型的对象,又要调用拷贝函数,所以返回d2也不是最优解
为了提高效率,我们选择传引用返回,此时返回值是d2的引用,这样就不需要再创建新的对象来接收返回值
判断是否传引用返回的重要依据是,返回值出了函数作用域生命周期是否结束,结束传值,没结束传引用
为了防止出现自己给自己赋值降低效率的现象,我们可以加一个判断语句进行优化
Date& operator=(const Date& d)
{
if (this != &d)//检查是否自己给自己赋值
{
_year = d.year;
_month = d.month;
_day = d.day;
return *this;
}
}
以上即是完整的函数运算符重载函数的写法
与其他默认函数相同,即使我们不写编译器也会自动生成一个默认成员函数。 编译器默认生成赋值运算符跟拷贝构造的特性是一样的
a、针对内置类型,会完成浅拷贝,也就是说像Date这样的类不需要我们自己写,但是像Stack这样的类,就得我们自己写。
b、针对自定义类型,则会调用自定义类型自己的运算符重载完成拷贝
6.总结
构造函数和析构函数的特性是类似的,编译器对内置类型不做处理,自定义类型调用它们自己的析构函数
拷贝构造和赋值重载的特性是类似的,编译器对内置类型浅拷贝,而自定义类型会调用它的拷贝构造和赋值重载
下面再思考一个小问题
Date d1;
Date d2 = d1;//等价与 Date d2(d1)
请问这个调用的是拷贝构造函数还是赋值重载函数呢
拷贝构造 : 拿一个已经存在的对象去构造初始化另一个要创建的对象
赋值重载 : 两个已经存在的对象之间的拷贝
所以调用的是拷贝构造函数
最后我们完整的创建一个Date类
//Date。h
#pragma once
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1);
//打印日期
void Print();
//析构、拷贝构造、赋值重载可以不写,默认生成的就够用了
//+操作符重载
Date operator+(int day);
//-操作符重载
Date operator-(int day);
//+=操作符重载
Date& operator+=(int day);
//-=操作符重载
Date& operator-= (int day);
//++d前置加加重载
Date& operator++();
//后置加加重载,int没什么用,只是占位符用于区别前置和后置,构成函数重载
Date operator++(int);
//--前置减减重载
Date& operator--();
//--后置减减重载
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
inline int GetMonthDay(int year, int month)
{
static int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = month_day[month];
//判断是否是闰年二月
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day = 29;
}
return day;
}
//Date.cpp
#include "Date.h"
Date::Date(int year, int month, int day)
{
//保证日期的合法性
if (year > 0 &&
month > 0 && month < 13 &&
day > 0 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "日期不合法" << endl;
assert(false);
}
}
void Date::Print()
{
printf("%d year %d month %d day\n", _year, _month, _day);
}
Date Date::operator+(int day)
{
Date ret(*(this));
ret._day += day;
//如果大于该月天数
while (ret._day > GetMonthDay(ret._year, ret._month))
{
ret._day -= GetMonthDay(ret._year, ret._month);
ret._month += 1;
//如果month大于12,year+1
if (ret._month > 12)
{
ret._month -= 12;
ret._year += 1;
}
}
return ret;
}
Date Date::operator-(int day)
{
Date ret(*this);
ret._day -= day;
while (ret._day <= 0)
{
ret._month -= 1;
if (ret._month < 1)
{
ret._month += 12;
ret._year -= 1;
}
ret._day += GetMonthDay(ret._year, ret._month);
}
return ret;
}
Date& Date::operator-=(int day)
{
Date ret(*this);
(*this) = (*this) - day;
return (*this);
}
Date& Date::operator+=(int day)
{
Date ret(*this);
(*this) = (*this) + day;
return (*this);
}
Date& Date::operator++()
{
return *this += 1;
}
Date Date::operator++(int)
{
Date ret(*this);
*this += 1;
return (*this);
}
Date& Date::operator--()
{
return *this -= 1;
}
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return (*this);
}
以上即是博客的全部内容,感谢观看。
上一篇:【初识C++】2.1类与对象(上)
下一篇:【初识C++】2.3类与对象(下)