1. 类的6个默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 赋值操作符重载
6. 默认拷贝构造与赋值运算符重载的问题
7. const成员函数
8. 取地址及const取地址操作符重载
(建议通过计算机设备进行观看,以获得更佳的视觉效果。若选择在移动设备上观看,可能会因屏幕限制导致部分内容的排版效果不理想,且信息展示过于密集,从而影响观看体验。)
( 本内容有大量借鉴比特课件,故特此说明!)
1. 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
2.构造函数
2.1概念
对于以下的日期类
(这节,会较多的使用这个日期类,主要是因为可以方便的练习各类知识)
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意: 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
(默认构造函数只能有一个,要是像上面那样,就会划红波浪,显示 类“ Date "包含多个默认构造函数)
7.关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?对象调用了编译器生成的默认构造函数,但是对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int / char ...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数
8.成员变量的命名风格
3.析构函数
3.1 概念
前面通过构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。(如malloc弄出的一些空间)
3.2 特性
析构函数是特殊的成员函数。
其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
(以下关于assert 的内容略看,我写主要是我对它没有过系统的了解,故)
( `assert`是一个宏,用于C和C++程序中的调试过程中。它的作用是检查程序中的某个条件是否为真。如果条件为假(即为`false`),`assert`会导致程序中断,并通常会打印出错误消息和断言的位置,这样开发者可以快速定位到问题所在。
### 基本用法:
#include <assert.h> // 在C中使用
// 或者
#include <cassert> // 在C++中使用
int main() {
int a = 5, b = 3;
// 检查a是否大于b
assert(a > b); // 如果a不大于b,程序会中断并报错
// 其他代码...
return 0;
}
### 作用:
1. **错误检测**:
`assert`用于检测代码中不应该发生的情况,比如数组越界、空指针引用、非法参数等。
2. **调试辅助**:
在开发和调试阶段,`assert`可以帮助开发者发现和修复逻辑错误。
3. **程序验证**:
`assert`可以用来验证程序的某些部分是否按照预期工作,例如算法的正确性。
4. **条件编译**:
`assert`通常与预处理器指令结合使用,以便在发布版本中禁用断言检查,从而避免性能损失和安全风险。
### 注意事项:
- 在发布版本的程序中,断言检查通常会被移除,因为它们会增加额外的运行时开销。
- `assert`不应该用于处理正常的程序流程,它主要用于检测异常情况。
- `assert`宏的参数应该始终是布尔表达式,而且这些表达式的结果在编译时通常是已知的。
- `assert`失败时,会调用`abort`函数,这会导致程序立即终止。在某些系统中,`assert`失败还可能触发一个信号处理程序(如`SIGABRT`),这可以用来进一步处理错误情况。
总的来说,`assert`是程序员在开发过程中用于检查程序正确性的重要工具,但它不应该在生产环境中使用,以避免性能损失和潜在的安全问题。)
5.关于编译器自动生成的析构函数,是否会完成一些事情呢? 下面的程序我们会看到,编译器生成的默认析构函数,会对自定义类型成员调用它的析构函数。
(也不知到为什么,我原本想套一下上面 SeqList 的代码,但是不行(就是跳过了析构,我调试的时候发现),哎,没弄明白)
(注意:先定义的后析构)
4.拷贝构造函数
4.1概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
!!! 使用传值方式会引发无穷递归调用 !!!
(如果不能理解的话,就看下面的代码(此举是为了表示理解不了就直接记住)
(去掉的话,它直接报错,划红波浪,它显示 " 类 “ Date " 的复制构造函数不能带有” Date "类型的参数 ”,当然它表明的不一定准确,有人可能会认为是 “ const " 的事,但去掉 ” const “ 后,它所显示的错误不变)
3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
4.那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢? 验证一下试试?
(我们简单的写了一个栈,可以看到,当我们按下 “ Ctrl + F5 " 后,直接报错,右下方的控制台界面是输入不了的,这就是编译器生成的默认拷贝构造函数造成的浅拷贝的后果,需要我们后序所述的深拷贝去解决)
(根本原因讲完了,来说直接原因,编译器生成的默认拷贝构造函数是一对一的去拷贝复制,这就导致后一个栈的指针指向了前一个的空间,进而两个指针指向了同一个空间,当结束时,调用析构函数,同一个空间被释放了两次,造成了程序崩溃(具体原因见下))。(形象如图)
(在编程中,尤其是在使用C或C++等需要手动内存管理的语言时,同一块内存被释放(free)两次可能会导致程序崩溃,原因通常与内存管理的数据结构和算法有关,当程序试图释放一个已经释放的内存块时,内存管理器可能会尝试将该内存块标记为空闲,但因为内存管理器的内部数据结构已经被第一次释放操作修改,这可能导致数据结构不一致,从而引发错误)
(注意:)(当看完后再来了解这个)
5.赋值运算符重载
(自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调用这个重载函数)
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为: 关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型 operator 操作符(参数列表)
注意:
a. 不能通过连接其他符号来创建新的操作符: 比如operator@
b. 重载操作符必须有一个类类型或者枚举类型的操作数
c. 用于内置类型的操作符,其含义不能改变,例如: 内置的整型 + ,不 能改变其含义
d. 作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的
e. 操作符有一个默认的形参this,限定为第一个形参
f. .* , :: , sizeof , ? : , . ,注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
(这个第 f 条的5个运算符分别是 1. 点星,2.两个冒号,3.sizeof ,4.三目运算符,5.点 )
5.2 赋值运算符重载
(是否要重载一个运算符,看的是这个运算符是否对这个类的对象有意义)
赋值运算符主要有四点:
1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
6.日期类的实现
头文件:
#pragma once
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
// 获取某年某月的天数
int GetDestiny(int year, int month) const;
// 全缺省的构造函数
Date(int year = 0, int month = 1, int day = 1);
// 拷贝构造函数
//d2(d1)
Date(const Date& d);
// 析构函数
~Date()
{ }
// 赋值运算符重载
// d2=d3 -> d2.operator(&d2,d3)
Date& operator=(const Date& d);
// == 运算符重载
bool operator == (const Date& d) const;
// > 运算符重载
bool operator>(const Date& d)const;
// < 运算符重载
bool operator<(const Date& d)const;
// <= 运算符重载
bool operator<=(const Date& d)const;
// >= 运算符重载
bool operator>=(const Date& d)const;
// != 运算符重载
bool operator!=(const Date& d)const;
// 日期 += 天数
Date& operator+=(int day);
// 日期 -= 天数
Date& operator-=(int day);
// 前置 ++
Date& operator++();
// 后置 ++
Date operator++(int);
// 前置 --
Date& operator--();
// 后置 --
Date operator--(int);
// 日期 + 天数
Date operator+(int day);
// 日期 - 天数
Date operator-(int day);
//日期 - 日期 返回天数
int operator-(const Date& d) const;
//取地址
const Date* operator&() const;
void DatePrintf() const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year >> d._month >> d._day;
return _cin;
}
.cpp 文件:
#include"Date.h"
int Date::GetDestiny(int year, int month) const
{
static int monthday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
if (month == 2)
return 29;
}
return monthday[month];
}
Date::Date(int year, int month, int day)
{
if (year >= 0 && month < 13 && month>0 && day <= GetDestiny(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
cout << "非法日期" << endl;
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
bool Date::operator == (const Date& d)const
{
if (_year != d._year)
return false;
if (_month != d._month)
return false;
if (_day != d._day)
return false;
return true;
}
bool Date::operator>(const Date& d)const
{
if (_year < d._year)
return false;
if (_month < d._month)
return false;
if (_day < d._day)
return false;
return true;
}
bool Date::operator<(const Date& d)const
{
return ((*this) > d ? false : true) && ((*this) == d ? false : true);
}
bool Date::operator<=(const Date& d)const
{
return (*this) > d ? false : true;
}
bool Date::operator>=(const Date& d)const
{
return (*this) < d ? false : true;
}
bool Date::operator!=(const Date& d)const
{
return (*this) == d ? false : true;
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetDestiny(_year, _month))
{
_day -= GetDestiny(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_day += GetDestiny(_year, _month - 1);
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
}
return *this;
}
Date& Date::operator++()
{
(*this) += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp = (*this);
(*this) += 1;
return tmp;
}
Date& Date::operator--()
{
(*this) -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp = (*this);
(*this) -= 1;
return tmp;
}
Date Date::operator+(int day)
{
Date tmp = (*this);
tmp += day;
return tmp;
}
Date Date::operator-(int day)
{
Date tmp = (*this);
tmp -= day;
return tmp;
}
int Date::operator-(const Date& d) const
{
int flag = 1;
Date max = *this;
Date min = d;
if (max < min)
{
Date tmp = max;
max = min;
min = tmp;
flag = -1;
}
int count = 0;
while (min != max)
{
min++;
count++;
}
return count * flag;
}
const Date* Date::operator&() const
{
return this;
}
void Date::DatePrintf() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
(可能看完有些迷,不要紧,还有些后面的知识)
7. const 成员
(复习一下:
解释:1 和 2 是 *p1与 *p2不能改;3 是 p3 不能改)
7.1 const 修饰类的成员函数
将 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改。
Look 一下这个代码
思考下面问题:
1.const 对象可以调用非 const 成员函数吗? //权限放大
2.非 const 对象可以调用 const 成员函数吗? //权限缩小
3.const 成员函数内可以调用其它的非 const 成员函数吗?
4.非 const 成员函数内可以调用其它的 const 成员函数吗?
(答案:1.不可 2.可 3.不可 4. 可)
(形象如下图)
( 关于 const 结论:什么时候会给成员函数加 const 呢?只要成员函数中不需要修改
成员变量时,最好都加上const)
8.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!(比如不想别人获得该地址,则 return nullptr 即可)