类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date {};
构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
C++对象实例化的时候必须调用构造函数!
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
看下面这段代码:
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(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.Print();
Date d2(2024, 3, 31);
d2.Print();
return 0;
}
解析:
全缺省代替无参:
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(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.Print();
Date d2(2024, 3, 31);
d2.Print();
return 0;
}
运行结果:
注意一个细节:
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
看下面这段代码:
class A
{
public:
A()
{
cout << "Print(A)" << endl;
tmp = 1;
}
int tmp;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
运行结果:
在以上这段代码中,我们并没有对Date中的内置类型进行初始化处理,但是打印结果时随机值,也就是编译器自动生成一个默认构造函数,然后使用了它,而这个自动生成的默认构造函数对内置类型时不做处理的,因此打印结果为随机值;
但是对于自定义类型,我们在该类当中声明了一个构造函数,因此编译器会自动调用我们事先定义好的构造函数,因此结果打印了一条我们设定好的语句。
那么我们有什么办法在可以不写构造函数的前提下可以让内置类型不是随机值吗?
当然有,在声明的地方给出缺省值。
举例:
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//声明给缺省值
int _year = 1;
int _month = 1;
int _day = 1;
A _aa;
};
我们在类中内置类型声明处给出缺省值,编译器就将该成员变量设置为缺省值。
总结:分析成员变量和初始化需求,构造函数要自己写的我们就自己实现,大多数情况下,构造函数我们都是要自己区实现的。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
这个点一看,有的人直接都蒙了,我们刚刚讲的难道不是默认构造函数吗?我们刚刚讲的是构造函数,但不都是默认构造函数,默认构造函数只有3个:
无参构造函数、全缺省构造函数、没写构造函数时编译器默认生成的构造函数;
而且我们刚刚说到,无参的构造函数和全缺省的构造函数时不能同时出现的,不然编译器会报警告,同时写了构造函数编译器就不会生成构造函数了,因此这三个默认构造函数是不会同时出现的,只会出现一个。
我们拿这三个默认构造函数进行比较,很显然,全缺省的默认构造函数性价比是最高的。
下面举例:
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 _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
编译:
我们这里的代码中并没有出现刚刚说的3个默认构造函数中的任何一种,因此我们要自己实现或者不写,这样才能保证程序的正常运行。
析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
下面直接上代码:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << _year << endl;
cout << "~Date" << endl;
}
void Print()
{
cout << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1);
d1.Print();
Date d2(2024);
d2.Print();
return 0;
}
运行结果:
从运行结果可以看到:在调用Print函数的时候,是先定义的对象先调用;但是调用析构函数的时候,后定义的类对象先调用析构函数进行空间清理。
上面的日期类比较简单,没有在堆上申请空间,我们不写析构函数也是不会出现问题的:因为程序结束的时候,由于日期类中成员变量都是内置类型的数据,因此程序结束会自动销毁在栈上开辟的空间。而如果是较为复杂的类对象,比如堆栈,我们就需要手动写一个析构函数让编译器在程序结束的时候调用。
对比一下,析构函数其实就是我们之前学习堆栈时候写的Destroy函数,那个时候我们同学可能确实实现了Destroy函数,但是程序结束的时候忘记调用,空间没有返回给内存,虽然我们写的程序结束会自动释放内存,但是这依旧是一个值得注意的细节。所以我们的C++祖师爷发明了析构函数,在函数结束的时候自动调用!哈哈哈
拷贝构造函数
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
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;
}
通过调试我们可以看到d2中的数据已经完成拷贝。
为什么这里要用传引用传参而不能使用传值传参?
这里的调用拷贝构造是不会进入到函数中的,因此就算再函数内部设置结束或者返回条件也是行不通的。
传引用传参就是最好的解决方案。
传引用这里其实有一个风险:
Date( Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
如果程序员在写代码的时候喝了一点酒或者少吃了几粒花生米,可能就会写出上述代码,导致原本拷贝的模板也被破坏了,人财两空,因此为了导致这种事情的发生我们通常情况下都会加一个const修饰一下:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这样就能有效防止原先数据正确的对象被破坏了。
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注:注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
下面我们来看一种场景:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
上述代码是没有实现拷贝构造函数的,我们看一下调试结果:
我们知道,拷贝构造函数也是一种默认成员函数,在没有实现拷贝构造函数的情况下,系统会自己生成一份拷贝构造函数,而且这个拷贝构造函数居然把内置类型完成了拷贝,因此得到结论:编译器自己生成的拷贝构造函数可以将内置类型完成拷贝。
我们再来看下一种情况:
class Time
{
public:
Time() = default;//后来添加
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
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
上述代码编译报错:
我们没写默认构造函数编译器不是自己会生成一个吗?为什么会出现这样的情况?
原因:拷贝构造函数也属于构造函数,我们写了编译器就不会自己生成了,因此这里编译器会报错。
这里交给大家一个小技巧:
我们加入这条语句,强制编译器生成默认构造函数,再次运行:
我们可以看到,当一个相同类型的d1拷贝构造d2,会引发编译器调用自定义类型的拷贝构造,因此打印上述语句。
我们看下面这段代码,可以发现值拷贝的问题:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
运行结果:
可以看到,这里我们创建一个栈,用已经创建好的st1拷贝构造st2,最后程序崩溃。
首先,我们是没有显式实现拷贝构造的,因此这里编译器调用的是自己实现的拷贝构造,也就是值拷贝,最后导致程序崩溃,下面我们分析一下原因:
这里我们就需要升级一下我们的拷贝方式,自己来实现一个拷贝构造,也就是深拷贝,就此完成这件编译器无法完成的工作!
程序改进:
Stack(const Stack& st1)
{
DataType* tmp = (DataType* )malloc(sizeof(DataType) * st1._capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, st1._array, sizeof(DataType) * st1._size);
_array = tmp;
_size = st1._size;
_capacity = st1._capacity;
}
在类中加入这个我们自己实现的拷贝构造函数就可以解决这个问题了。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
注意以上5个运算符不能重载。这个经常在笔试选择题中出现
下面我们直接写上一段代码:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool DateEqual(const Date& d1, const Date& d2)
{
return d1._day == d2._day
&& d1._month == d2._month
&& d1._year == d2._year;
}
int main()
{
Date d1(2024, 4, 1);
Date d2(2024, 4, 28);
cout << DateEqual(d1, d2) << endl;
return 0;
}
上述代码就是比较日期类的大小,判断两个日期是否相等,当然我们这里要注意一个细节:
我们将该函数放入到类对象中:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool DateEqual(const Date& d2)
{
return _day == d2._day
&& _month == d2._month
&& _year == d2._year;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 1);
Date d2(2024, 4, 28);
cout << d1.DateEqual(d2)<< endl;
return 0;
}
该函数放入到类对象之后,函数的使用方式和参数个数就会发生相应的变化,这里要注意。
下面我们将上述的代码发生以下改变,融入以下我们这一节的知识点:运算符重载!
还是老规矩,看下面的代码:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _day == d2._day
&& _month == d2._month
&& _year == d2._year;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 1);
Date d2(2024, 4, 28);
cout << d1.operator==(d2)<< endl;
cout << (d1 == d2) << endl;
return 0;
}
解析:
赋值运算符重载
1.赋值运算符重载格式
1.参数类型:const T&,传递引用可以提高传参效率
2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回*this :要复合连续赋值的含义
老样子,我们这里先来上要一段代码:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year<<"/" << _month<<"/" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 2);
Date d2(2024, 4, 1);
d1.Print();
d2.Print();
d1 = d2;
d1.Print();
d2.Print();
return 0;
}
运行结果:
解析:
再看下面这段代码:
int main
{
int i, j, z;
i = j = z;
}
我们知道,赋值操作符是支持连续赋值的,那么我们重载过的操作符如何支持连续赋值呢?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Print()
{
cout << _year<<"/" << _month<<"/" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 2);
Date d2(2024, 4, 1);
d1.Print();
d2.Print();
d1 = d2;
d1.Print();
d2.Print();
Date d3(2, 2, 2);
d1 = d2 = d3;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
运行结果:
解析:
这个时候就可能有人要问了:操作符重载之后支持自己给自己赋值吗?
当然支持,虽然这样做并没有什么意义,当然步伐有人写代码昏头了会写成这样子,因此自己给自己赋值这种操作重载后的操作符是支持这样的操作的,这里就不给大家分析了。
当然为了防止有人写出这样无意义的代码,通过情况下会将代码设置成这样:
Date& operator=(const Date& d)
{
if (&d != this)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
当然同样是默认成员函数,赋值运算符重载不写编译器会自己生成吗?
下面看代码:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*Date& operator=(const Date& d)
{
if (&d != this)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}*/
void Print()
{
cout << _year<<"/" << _month<<"/" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 2);
Date d2(2024, 4, 1);
d1.Print();
d2.Print();
d1 = d2;
d1.Print();
d2.Print();
Date d3(2, 2, 2);
d1 = d2 = d3;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
依旧是上面的代码,将赋值运算符重载部分屏蔽掉之后,编译运行结果:
依旧完成预期的结果,这里给出结论:
2. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
小结
默认生成函数行为总结:
其它运算符重载,我们这里就不一一赘述了,下面将一个特殊一点的:
<<流插入、>>流提取重载。
流插入重载
我们当前使用的六插入操作符只能对内置类型进行操作,因此在对自定义类型进行操作时,显得很无奈,所以我们需要对<<六插入操作符进行重载,实现对自定义类型的操作.
下面看代码:
//.h文件中
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//友元声明
friend void operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const Date& d);
//.cpp文件中
void operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
}
//.cpp文件中
int main()
{
Date d1(2024, 4, 9);
Date d2(2024, 4, 10);
cout << d1;
return 0;
}
解析:
为什么不在类中声明函数?
解答:
在内置类型的使用中,cout是支持连续使用的,即:cout<<x<<y;
那么我们重置之后的类型也可以这样子吗?我们下面对原来的函数进行一个简单的改进:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
这里只写了函数定义处的修改,其他地方对应着修改以下就好,这样子就可以实现连续输出了。
我们看一下效果:
流提取重载
当然,有流插入必须是要有流提取的。
下面实现一个流提取:
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
这里给出定义,其他部分都差不多,用法和cin也是一样的。
const成员
我们在调用类成员函数Print的时候可能会出现意外情况:
当类对象d2被const修饰的时候,在调用类成员函数时发生权限的放大,因此这里报错。
那么我们怎么解决这个问题呢?
下面看代码:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
这里问年对于类中的函数进行一个小小的改动,也就是在函数声明后加入const,将该函数的权限也进行缩小,达到前后一致的效果,这样就不会报错了。
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!