C++类和对象中
目录
1. 类的6个默认成员函数:
2. 构造函数 :
3. 析构函数
4. 拷贝构造函数
5. 赋值运算符重载
1. 类的6个默认成员函数:
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2. 构造函数 :
定义:对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
在涉及C++的时候,那些大佬们深深的觉得有些东西很容易忘记写,像写栈的时候很容易忘记初始化和销毁的工作,这里大佬们就分别涉及了构造函数(相当于完成里初始化),以及析构函数(相当于释放资源)去解决这个问题。
下面我们看看怎么使用构造函数:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
我们可以看到这里编译器不会报错,是因为编译器自动生成无参的构造函数,不过这些值没有作用。
6. 关于编译器生成的默认成员函数,但我们不难思考:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默 认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用??
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。
class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
在后来C++11中又补充:内置类型成员变量在 类中声明时可以给默认值。
这里并不是给它开辟空间进行赋值的意思,这里的意思是给缺省值。如果后面你没有给它赋值,那么开辟空间之后编译器就会给它们赋上缺省值。
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
3. 析构函数
概念 :与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
下面以数据结构栈来举例:
class Stack { public: //构造函数: Stack(int capacity = 4) { _a = (int*)malloc(capacity * sizeof(int)); if (_a == NULL) { perror("malloc fail"); exit(-1); } _capacity = capacity; _size = 0; } void Push(int x) { _a[_size++] = x; } //析构函数: ~Stack() { if (_a) { free(_a); _a = nullptr; _size = 0; _capacity = 0; } } private: int* _a; int _size; int _capacity; };
通过调试,我们不难发现,在main函数结束的时候会自动调动析构函数。
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器 生成的默认析构函数,对自定类型成员调用它的析构函数。class Time { public: ~Time() { cout << "~Time()" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
这里其实编译器没有直接调用Time类的析构函数,这里是Date类调用了析构函数,因为Time类在Date类中,属于自定义类型,所以就间接调用了Time类的析构函数,这样做的目的的是为了保证每个类在结束时都能释放空间。
6.如果类中 没有申请资源时,析构函数可以不写 ,直接使用编译器生成的默认析构函数,比如 Date类; 有资源申请时,一定要写 ,否则会造成资源泄漏,比如 Stack 类。(内存泄露是件很大的事)
4. 拷贝构造函数
class Date
{
public:
Date(int year = 2022, int month = 1, int day = 3)
{
_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;
}
这样就会无限递归下去。所以如果这样写编译器都会直接报错。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
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
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构
//造函数
Date d2(d1);
return 0;
}
结果很显然,编译器会自动生成一个默认的拷贝构造。
为什么呢?因为两个stack的数组用的同一块地址,其中一个先调用了析构函数,把空间释放了,另外一个又调用了析构函数,重复释放同一块空间,导致报错。
下面使用深拷贝来解决这个问题:
下面通过调试来观察结果:
我们可以看到这两个地址不一样了,这说明了确实完成了深拷贝。
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d) {
Date temp(d);
return temp; }
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0; }
我们先看看结果:
不认真的代码都很难发现这是怎么情况,下面通过画图来演示:
5. 赋值运算符重载
5.1 运算符重载C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )注意:不能通过连接其他符号来创建新的操作符:比如 operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this.* :: sizeof ?: . 注意以上5个运算符不能重载。在全局去定义赋值运算符重载显然不合适,因为这样不能访问到类里面的私人成员,这样封装性就没有办法保证了。因为是类和类成员的比较,所以我们可以直接放在类里面。5.2 赋值运算符重载1. 赋值运算符重载格式参数类型 : const T& ,传递引用可以提高传参效率返回值类型 : T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回 *this :要复合连续赋值的含义2. 赋值运算符只能重载成类的成员函数不能重载成全局函数:
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。这里我们会发现编译器会报错,就是因为没有在类里面,没有this指针导致的。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time& operator=(const Time& t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; return 0; }
通过调试窗口就可以发现编译器会生成默认的赋值运算符重载。
既然 编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了 ,还需要自己实现吗?当然像日期类这样的类是没必要的。但是像栈这种涉及资源的问题的时候,浅拷贝显然是不能满足需求的,这个前面也已经验证过了。在S1有了数据之后,要让S2也有相同的数据,必须要进行深拷贝,不然就会出现这样的错误。
这里程序会崩溃。这跟之前的析构函数出现的问题很像。
最后在调用析构函数的时候释放同一块空间而导致程序崩溃。
5.3 前置 ++ 和后置 ++ 重载因为前置++和后置++所得到数值不一样,所以C++有规定:后置 ++ 重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,编译器自动传递下面是代码演示:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator++() { _day += 1; return *this; } Date operator++(int) { Date tmp; _day += 1; return tmp;//因为这里返回值是临时变量所以不能使用引用返回 } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; d1 = d1++; d2 = ++d2; return 0; }
通过调试不难得到结果:
通过上面的学习,在最后我们再完成一个日期类:下面是代码实现:class Date { public: //int GetMonthDay(int year, int month) //{ // static int arr[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; // } // return arr[month]; //} //获得一个月的天数 int GetMonthDay(int year, int month) { static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day = days[month]; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { day += 1; } return day; } // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 拷贝构造函数 // d2(d1) Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } // 赋值运算符重载 // d2 = d3 -> d2.operator=(&d2, d3) Date& operator=(const Date& d) { if (this != &d)//这里要判断一个两者是否为不同的类对象,不然程序会崩溃 { _year = d._year; _month = d._month; _day = d._day; } return *this; } // 析构函数 ~Date() { _year = 0; _month = 0; _day = 0; } // 日期+=天数 Date& operator+=(int day) { //天数全部加上去之后在通过加月去减天数即可 _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year += 1; _month = 1; } } return *this; } // 日期+天数 Date operator+(int day) { Date tmp(*this);//加是返回原来的日期,所以要创建临时变量 tmp += day; return tmp; } // 日期-天数 Date operator-(int day) { Date tmp(*this); tmp -= day; return tmp; } // 日期-=天数 Date& operator-=(int day) { _day -= day; while (_day < 0) { _day += GetMonthDay(_year, _month); _month--; if (_month == 0) { _month = 12; _year--; } } return *this; } // 前置++ Date& operator++() { _day++; //判断一个最后一年的最后一个++ if (_day > GetMonthDay(_year, _month)) { _month++; if (_month > 12) { _month = 1; _year++; } _day = 1; } return *this; } // 后置++ Date operator++(int) { Date tmp(*this); *this += 1; return tmp; } // 后置-- Date operator--(int) { Date tmp(*this); *this -= 1; return tmp; } // 前置-- Date& operator--() { _day--; //判断一年最开始的一个-- if (_day < 0) { _month--; if (_month < 0) { _month = 12; _year--; } _day = GetMonthDay(_year, _month); } return *this; } // >运算符重载 bool operator>(const Date& d) { //年大则大,年同看月,月同看天 if (this->_year > d._year ) { return true; } else if (this->_year == d._year && this->_month > d._month) { return true; } else if (this->_year == d._year && this->_month == d._month) { if (this->_day > d._day) { return true; } } return false; } // ==运算符重载 bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } // >=运算符重载 bool operator >= (const Date& d) { return *this > d || *this == d; } // <运算符重载 bool operator < (const Date& d) { if (this->_year < d._year) { return true; } else if (this->_year == d._year && this->_month < d._month) { return true; } else if (this->_year == d._year && this->_month == d._month) { if (this->_day < d._day) { return true; } } return false; } // <=运算符重载 bool operator <= (const Date& d) { return *this < d || *this == d; } // !=运算符重载 bool operator != (const Date& d) { return !(_year == d._year && _month == d._month && _day == d._day); } // 日期-日期 返回天数 int operator-(const Date& d) { //日减完到月到年 int day = _day - d._day; while (_month != d._month) { _month--; day += GetMonthDay(_year, _month); if (_month == 0) { _month = 12; _year--; } } //减年 while (_year != d._year) { _year--; if ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0)) { day += 366; } else { day += 365; } } return day; } private: int _year; int _month; int _day; };
在最后总结一下:对于构造函数和析构函数,我们可以近似理解为初始化,以及销毁,但是它的功能有没有那么强大,所以,在自己要使用 资源的时候,必须自己初始化,以及销毁。因为编译器不可能知道你想干什么。对于拷贝构造来说,它的职责就是给同类对象进行拷贝,但是编译器做到的只是简单的字节拷贝,就是浅拷贝,要资源使用的时候就要进行深拷贝。可以理解为:需要调用析构函数的都要深拷贝,不需要调用析构函数的,就可以不写,使用编译器默认的拷贝构造函数。对于赋值运算符,我们要知道.* :: sizeof ?: . 注意以上5个运算符不能重载。其他的赋值运算符我们可以自己理解并实现即可。