文章目录
二次修订版本,date: 2024 3 12;
类的六个默认成员函数
当我们在类中什么都不写的时候,编译器会在类内默认生成六个成员函数。
分别是:构造函数,析构函数,拷贝构造,赋值运算符重载,取地址重载和const取地址。
构造函数
构造函数是负责对象的初始化工作的,并不是创建对象。
构造函数会在对象实例化的时候自动调用完成对象的初始化工作。
构造函数的特征:函数名与类名相同,无返回值。创建对象的时候由编译器自动调用,可以保证每个对象都有一个初始值,在对象的生命周期内只会调用一次。构造函数可以重载。
这里我们写了一个Date()的构造函数,创建对象后并不需要我们主动调用这个对象就已经初始化好了。
注意:void并不是无返回值而是空返回值,无返回值就直接不写。
当然如果想要主动传参也是可以的。就如下面这样:
class Date
{
public:
Date()
{
_year = 2022;
_month = 6;
_day = 18;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 6, 23);
return 0;
}
这两个构造函数构成了函数重载(这就使得我们可以有多种初始化对象的方式)可以使用默认值也可以使用给定值初始化。但是这样写两个函数略显复杂,可以使用全缺省的构造函数替代。
class Date
{
public:
Date(int year= 2022, int month = 6, int day = 18)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
就像上面这个样子,我们不传参的时候自动调用就是使用缺省值。
但是要注意下面这种写法,是错误的
class Date
{
public:
Date()
{
_year = 2022;
_month = 6;
_day = 18;
}
Date(int year= 2022, int month = 6, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
这里既写了无参的构造函数,又写了全缺省,当不传参的时候,编译器不知道调用无参的构造函数,还是调用全缺省的构造函数使用默认值初始化,这个错误叫做调用不明确也就是二义性。
如果不写构造函数,那编译器会自动生成一个默认构造函数
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
默认生成的构造函数对于内置类型(就是int,int*,包括Date*等)不会进行初始化,但是对于自定义类型,就会调用这个类的默认构造函数对这个类的对象进行初始化,如果这个类没有默认构造函数会报错。( 数据成员“Date::_a”不具备相应的 默认构造函数 或重载解决不明确,因此已隐式删除函数)
class A
{
public:
A()
{
_a = 10;
_b = 20;
}
private:
int _a;
int _b;
};
class Date
{
public:
private:
int _year;
int _month;
int _day;
A _a;
};
int main()
{
Date d1;
return 0;
}
上面的代码,在Date类里面包含了一个A的类的对象,在调用构造函数的时候对于int这些内置类型,编译器不会进行处理,对于A这样的自定义类型的对象,会去调用他的默认构造函数进行初始化。
使用栈实现队列的时候,MyQueen的构造函数可以不用写,系统生成的会调用Stack的默认构造函数完成初始化
最后注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。默认成员函数就是不需要传参就可以调用的函数。
析构函数
构造函数不是创建对象而是负责初始化对象,析构函数也不是负责销毁对象,而是在对象销毁之前调用,用来清理资源。
特性
- 析构函数名是类名,但是前面要加~
- 无返回值无参数
- 一个类只有一个析构函数,如果不写系统自动生成。
- 析构函数会在对象销毁(生命周期结束)的时候由C++编译系统自动调用。
C++的析构函数本质上对于内置类型是没有什么用的,只对使用malloc申请过空间的才有用,用来防止内存泄漏,在服务器方面,内存泄漏是很严重的问题而且相比于不初始化,忘记释放内存是不会报错的,这就是隐藏的利刃,当服务器因为内存泄漏崩溃之后,你才能发现内存泄漏的问题。
C++不像java还有垃圾回收器(GC),C++只能靠析构函数和智能指针来防止内存泄漏。
使用
class SList
{
public:
SList(int size = 10)
{
_arr = (int*)malloc(sizeof(int) * size);
_size = 0;
_capacity = size;
}
~SList()
{
if (_arr)
{
free(_arr);
_arr = nullptr;
_size = 0;
_capacity = 0;
}
}
private:
int* _arr;
int _size;
int _capacity;
};
int main()
{
SList _new;
return 0;
}
这里我们就是自己定义的析构函数,在_new这个对象生命周期结束的时候,就会自动释放对象内的_arr,malloc出来的空间,又叫清理资源。
int main()
{
SList n1;
SList n2;
SList n3;
return 0;
}
这里定义了三个对象,在定义的时候,构造顺序是n1,n2,n3,在析构的时候是n3,n2,n1;因为我们的对象创建在栈上,栈区的性质是后进先出,最后入栈的n3会是最先出栈的,因此也是n3先析构的。
析构函数也是默认成员函数,不写的时候也是会默认生成一个的,他的处理方式和构造函数也是一样的。对于内置类型不起作用,对于自定义类型会去调用它的析构函数。
因此,如果我们malloc申请了空间,那么就需要自己写出析构函数进行内存释放,方便的是不需要我们手动去调用。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
这段代码可以看出,编译器默认生成的析构函数对于自定义类型是去调用它自身的析构函数。
如果对象内没有资源需要清理,不需要写,系统自动生成就够用,而且在release版本下,编译器会自动去掉没有资源需要清理的析构函数。
对同一块内存空间释放两次,会发生错误。
总结构造函数和析构函数
对于内置类型,构造函数和析构函数都是不起作用的,对于自定义类型会调用这个自定义类型对象的默认构造和析构函数。
拷贝构造函数
拷贝构造函数的应用场景是:创建一个对象用另一个同类的对象对他进行初始化。
就像是内置类型中的int a = b;用b给a初始化值。
拷贝构造的代码
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_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);
Date d3 = d1;
return 0;
}
拷贝构造函数是构造函数的重载。下面定义d2,d3对象的时候初始化都是调用了拷贝构造函数,这里两种形式都可以。第二种实际上是重载赋值运算符然后调用了拷贝构造函数。
特性
1,拷贝构造是构造函数的重载函数
2,拷贝构造函数的参数只有一个且只能是类的引用类型的(隐含的类型*this指针不算)如果参数不是引用会引发无限递归。
原因:如果使用传值,参数是Date d,那么值传参的时候就要将实参的值拷贝给形参,这时候就需要调用拷贝构造,然后就无限递归了。
当不写拷贝构造函数,编译器默认生成的拷贝构造函数完成的是浅拷贝,也就是值拷贝,在Date类这里是没有错误的,但是一旦成员对象有指针,指针指向的空间是malloc出来的时候就会出错了。
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.SetDate(2022, 6, 18);
Date d2(d1);
Date d3 = d1;
return 0;
编译器默认生成的拷贝构造函数对于我们的Date类是完全没有问题的。浅拷贝完全能满足Date类对象的需求。
那么现在来看下面这段代码
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
~stack()
{
if (_a != nullptr)
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
cout << "free is ok" << endl;
}
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
stack s1;
stack s2(s1);
return 0;
}
这段代码我们手动实现了构造和析构,然后调用编译器默认的拷贝构造函数给对象s2初始化成与s1相同的对象,但是程序运行到最后的时候却崩溃了。
上面崩溃的图中我们可以看出,完成了一次s2对象的析构,对s1进行析构的时候程序挂了?从调试图看到两个对象的成员对象_a指向的是同一块空间,原因就是对同一块空间释放了两次所以程序崩溃了。这也说明了系统默认生成的拷贝构造函数进行了浅拷贝。
因此对于这种类,就需要手动实现深拷贝构造函数
至于为什么释放两次就会崩溃,系统方面的问题就是,这块空间收回来之后,有可能又给了别人用,别人还没用完你给他释放了这是错误的。所以不能两次释放同一块空间。
拷贝构造总结
对于date这种类,没有涉及到malloc或者指针之间的关系的时候是可以不写的,系统默认生成的拷贝构造函数实现的浅拷贝完全够用,但是到了stack这种类的时候就不能用默认拷贝构造函数了。
赋值运算符的重载
运算符重载
内置类型想要比较大小或者加减乘除赋值,都可以用运算符来实现,±*/,但是自定义类型想进行运算只能通过函数来实现,不仅麻烦而且可读性也不高。所以C++引入了运算符重载,让自定义类型也可以通过运算符重载后使用运算符进行运算。
运算符重载的特性:
1,运算符重载函数名是operator后面接上要重载的符号。
2,函数的返回值与普通函数类似,可以根据情况自定义。运算符重载函数的参数必须有一个类的类型的参数或者枚举类型的参数。
3,重载的操作符不能改变其原来的含义,比如+不能重载成减法。
4,如果运算符重载函数定义为全局的函数,要显示写出类类型的参数,定义成成员函数就会少一个,有一个参数是this指针。(this指针一定是第一个形参)
5,“.*”,“::”,“:?”,“sizeof”,“.” 这五个运算符,点星,域作用访问限定符,三目运算符,sizeof,和点操作符,是不可以重载的。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造和析构都可以使用默认。
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
if (d1 == d2)
cout << "yes" << endl;
return 0;
}
这段代码我们重载了==来判断自定义的类是不是相等。
赋值运算符重载
特性:
1,参数类型(参数类型是引用,可以减少传值的时候拷贝构造的性能消耗)
2,返回值(为了可以使用连续赋值,返回值要是第一个对象参数的引用)
3,检测一下是不是在给自己赋值,如果是直接返回即可。
4,返回的是第一个对象的引用,因为第一个对象出函数也不会销毁所以返回值是*this
5,如果这个类没有手动写,那编译器会自动生成一个赋值运算符的重载函数。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//这里日期类拷贝构造和析构都可以使用默认。
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,6,19);
Date d2;
d2 = d1;
Date d3;
d3 = d2 = d1;
return 0;
}
通过返回类的引用可以实现连续赋值。
赋值运算符是默认成员函数,就算我们不写,编译器也会自动生成。结论是:这个默认生成的赋值运算符重载函数,与默认生成的拷贝构造函数是类似的,只能实现按照字节序拷贝也就是浅拷贝,如果遇到Stack那种存在malloc空间的类就会出现和拷贝构造函数类似的错误,对同一块空间释放了两次。
总结拷贝构造函数和赋值运算符重载
1,拷贝构造函数和构造函数形成了函数重载,参数是类对象的引用,无返回值
2,赋值运算符重载是一个函数名operator=的函数,参数是对象类的引用,返回值是对象的引用,因为这个对象出了函数作用域并不会销毁。
3,拷贝构造函数和赋值运算符重载函数,如果不写,系统默认生成的都是实现的浅拷贝或者说是值拷贝,对于日期类这种,没有问题,但是对于Stack就会出现问题。
关于operator<<重载
operator<<是不可以在类内实现的,因为类内默认第一个参数为this指针,调用的时候就是
d1<<cout
,因此operator<<一般在类外定义,可以通过1,友元声明访问private成员。2,通过类内的get方法获取成员。
tips:全局函数或者变量定义在.h文件中的时候,如果被两个cpp文件包含会现重定义的情况,解决方案:
- 全局函数的声明与定义分离
- 加static修饰函数(static会改变函数的链接属性,只在当前文件可见)
- 用inline修饰operator<<,内联函数会在调用的地方展开不会进入符号表。
日期类实现
//Date.h
#pragma once
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
// 获取某年某月的天数
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;
}
//打印日期
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
if (month > 12 || day<0 || day>GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
*this = d;//复用赋值运算符重载
}
// 赋值运算符重载
// 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 > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
// 日期-天数
Date operator-(int day)
{
Date ans(*this);
ans -= day;
return ans;
}
// 日期-=天数
// 日期-=day可以复用 日期 += -day;
Date& operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date ret = *this;
*this += 1;
return ret;
}
// 后置--
Date operator--(int)
{
Date ret = *this;
*this -= 1;
return ret;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day > d._day)
{
return true;
}
}
}
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
// >=运算符重载
inline bool operator >= (const Date& d)
{
if (_year < d._year)
{
return false;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return false;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return false;
}
}
}
return true;
}
// <运算符重载
bool operator < (const Date& d)
{
return !((*this) >= d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
return !(*this > d);
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
int operator-(const Date& d)
{
int count = 0;
int flag = 1;
Date max = *this;
Date min = d;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
while (max != min)
{
min++;
count++;
}
return count * flag;
}
private:
int _year;
int _month;
int _day;
};
//test.cpp测试文件
#include"Date.h"
void Test1()
{
Date d1;
d1.Print();
Date d2(2022, 5, 3);
d2.Print();
Date d4(2022, 7, 31);
d4.Print();
Date sum = d4 + 1000;
sum.Print();
d4.Print();
}
void Test2()
{
Date d1(2022, 1, 10);
Date d2 = d1 - 20;
d2.Print();
d1.Print();
Date d3(2022, 12, 31);
Date ret = d3++;
ret.Print();
d3.Print();
}
void Test3()
{
Date d1(2022, 2, 28);
Date d2 = --d1;
d2.Print();
d1.Print();
}
void Test4()
{
Date d1(2022, 2, 28);
Date d2(2022, 2, 28);
cout << (d1 != d2) << endl;
}
void Test5()
{
Date d1(2022, 6, 19);
Date d2 = d1;
Date d3(2022, 7, 18);
cout << (d3 - d1) << endl;
}
int main()
{
Test5();
return 0;
}
可以看到在上面代码的实现中,很多函数是可以复用的。比如拷贝构造函数可以复用赋值运算符重载。比大小的我们只需要实现一个>和 ==的实现,其他的都可以复用这两个函数加上取反符号来实现
**注意:**最后的日期减日期的实现可以使用找出较大的那个日期和较小的那个日期,然后让小的那个不断自增等和较大的那个相等了这时候计数器里面的值就是他们之间的天数差。
留一个问题,int占位符如何是保证后置++能调用到正确地重载函数呢?
答案是:后置++编译器在调用的时候会默认传一个int类型的参数过去,参数加上一个int来占位,与前置++的函数构成重载。
const 修饰的成员函数
const对象调用函数的时候,需要this指针指向的对象不可以被修改,比如Print,或者是>这类比较重载的,否侧const对象调用不了这些函数。
bool operator==(const Date& d)const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
//bool operator==(const Date* const this,const Date& d)
直接在函数后面加上const即可,这时候函数就变成了注释掉的样子。
下面来思考这几个问题
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
答案:
1,不可以,const对象不可以调用非const对象,const对象只读,如果变成了普通对象就是可读可写的,权限放大了。
2,可以,非const的普通对象是可读可写的,调用const成员函数被修饰成只读的,权限缩小了。
3,不可以,const成员函数的this指针是const属性的,调用非const函数会变成可读可写的this,权限放大了。
4,可以,非const函数调用const的成员函数,普通的this指针,传给了const的this指针,权限缩小。
取地址重载以及const取地址重载
//&运算符重载
const Date* operator&()const
{
return this;
}
Date* operator&()
{
return this;
}
//第一个是给const对象调用的,第二个是给普通对象调用的。
取地址并不需要什么重载函数,取这个对象的地址的时候直接取就可以了。
唯一可能的用法就是下面这种。
//&运算符重载
const Date* operator&()const
{
return nullptr;
}
Date* operator&()
{
return nullptr;
}
那就是不想让你取到这个类的对象的地址,可以像上面这样子写。