类与对象(中)
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
但是空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
隐含意思就是,有些类需要自己写,有些类,编译器默认生成就可以用了。
2.构造函数
2.1概念
class Date
{
public:
void Init(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.Init(2023, 6, 17);
d1.Print();
Date d2;
d2.Init(2023, 6, 18);
d2.Print();
return 0;
}
对于Date类,我们需要调用Init函数进行初始化。每次初始化对象都要调用这个函数,未免有点麻烦,那么有没有一种方法可以在对象实例化的时候,就初始化对象呢?
为了方便,C++引入了构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:
1.函数名与类名。
2.无返回值。
3.对象实例化的时候编译器自动调用对应的构造函数。
4.构造函数可以重载。
class Date
{
public:
/*void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//构造函数
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(2023, 6, 17);
return 0;
}
上面构造函数是带参数的,还有一种不带参数的构造函数,如下
class Date
{
public:
/*void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//构造函数
//有两个构造函数,编译也不会报错,构造函数支持重载
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date()
{
_year = 2023;
_month = 6;
_day = 18;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 17);//调用有参的构造函数
Date d2;//调用无参的构造函数
Date d3();//错误写法
//不带参的构造函数,调用的是后面是不能加括号的,所d3这种是错的
//因为d3这样写,就是声明一个返回值为Date的函数,要记住。
return 0;
}
为了方便,我们写构造函数可以把参数写成带缺省值的,传参和不参数就共用一个构造函数了。
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,6,17);
Date d2;
return 0;
}
- 如果类中没有自己定义一个构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成,会使用用户自己定义的构造函数。
//默认构造函数
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
创建d1的时候,调用默认构造函数,我们希望对d1成员变量进行初始化,但是d1成员变量为什么还是随机值呢?这样看起来编译器自动生成的默认构造函数似乎没有作用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
private:
//自定义类型
Stack _Push;
Stack _Pop;
};
int main()
{
MyQueue q;
return 0;
}
总结:编译器生成的默认构造函数,对内置类型不处理,对自定义类型调用它的默认构造函数(自己写了构造,就会调用自己的构造函数)
我们看见默认构造函数对自定义类型会调用它的默认构造函数,对内置类型不处理,感觉会有点别扭;但是这是大C++大佬,在设计这门语言,初期没有考虑完全,有缺陷的地方;我们应该报着学习的角度而不是批评的角度。并且修改不足的地方,应该是向前兼容,而不是直接删除。
所以,C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值(缺省值)。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
private:
//内置类型
size_t size=0;
//自定义类型
Stack _Push;
Stack _Pop;
};
int main()
{
MyQueue q;
return 0;
}
在vs2019测试,即使没有给内置类型赋缺省值,等调用默认构造函数size还是会被初始化为0,在vs2013就还是会是随机值,下来可以测试一下。但是C++语言本身是对内置类型不处理,自定义类型调用它的构造函数。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date
{
public:
//构造函数这样写编译就会报错
Date(int year, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
//error C2512: “Date”: 没有合适的默认构造函数可用
//因为我们需要传参过去
return 0;
}
总结:不传参数就可以调用的构造函数,就叫默认构造函数
到这里就有个疑问,我们什么时候需要写构造函数,什么时候不需要写呢?
答:如果默认生成构造函数能够满足我们的需求,就不要自己写了,如果不能,就需要自己写一个构造函数。
就比如Date,Stack,MyQueue三个类,Date和Stack需要我们自己写,不写内置类型就是随机值,除非声明时候给缺省值。MyQueue就不要自己写,因为自定义类型会调用自己默认构造函数。
3.析构函数
3.1概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2特性
析构函数是特殊的成员函数,其特征如下
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
//对象生命周期结束时,C++编译系统系统自动调用析构函数。
return 0;
}
由上图发现,当对象生命周期结束时,会自动调用析构函数,完成对象中资源的清理工作。
如果我们自己不写析构函数,编译器就会默认生成一个析构函数。
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
/*~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}*/
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
我们对比上面图片和这张图片发现,系统调用的默认析构函数,也似乎没有作用。没有完成对象资源清理工作。
注意:编译器生成的默认析构函数,和构造函数一样,对内置类型不处理,对自定义类型会调用它的析构函数
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
private:
//自定义类型
Stack _Push;
Stack _Pop;
};
int main()
{
MyQueue q;
return 0;
}
问:那什么时候需要写析构函数呢?
答:有资源需要释放,需要写自己写析构函数,但是这是不确定的,就比如说MyQueue这个类,需要释放资源,但是我们不用去写它的析构函数。
总之编译器默认生成的析构函数能够满足我们的需求就不用写析构函数,如果不能满足自己需要写一个
这里补充一个知识点
class MyQueue
{
private:
//自定义类型
Stack _Push;
Stack _Pop;
};
int main()
{
MyQueue q1;
MyQueue q2;
return 0;
}
问:q1,q2对象生命周期结束时,都会调用析构函数,那么谁会先调用呢?
答:q2先调用,q1在调用,这里符合栈先进后出的思想。
4.拷贝构造函数
4.1概念
在现实生活中,可能存在一个和你一模一样的人,我们称其为双胞胎
那么在创建对象的时候,可否创建一个与对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
//构造函数
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(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(2023,6,17);
//拷贝构造函数,对对象拷贝初始化,拿同类型对象的值来初始化自己
Date d2(d1);
return 0;
}
如果拷贝构造函数参数不是该类类型对象的引用,而是临时拷贝的话,编译器就会报错
这里是编译器给我们做了处理,在第一次的时候就阻止递归,不然就是一直递归下去。
为什么拷贝构造函数参数必须是该类类型对象的引用呢?
我们看下面一段代码
class Date
{
public:
//构造函数
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(Date& d)
{
cout << "Date拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << "Func1()" << endl;
}
void Func2(Date& d)
{
cout << "Func2()" << endl;
}
int main()
{
Date d1;
Func1(d1);
Func2(d1);
return 0;
}
当我们调用Func1这个函数的时候,Func1是传值传参,形参是实参的临时拷贝,会调用拷贝构造函数,调用Func2的时候,因为Func2是传引用传参,不会调用拷贝构造函数。因此为了避免递归下去,选择传引用传参。
注意拷贝构造函数形参需要加一个const
class Date
{
public:
//构造函数
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
//加const主要为了防止这种情况,
d._day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,6,17);
Date d2(d1);
return 0;
}
d1调用构造函数初始化,而d2调用的是拷贝构造函数,会直接把d1对象的值赋给d2,而刚开始的时候d2对象的值都是随机值,形参又是引用,d._day = day;这样写,不仅d2的day是随机值,还是把初始化号的_day也会变成随机值。所以加引用就解决了这样的问题。
3.若我们自己没写拷贝构造函数,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Date
{
public:
//构造函数
Date(int year=1, 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;
}*/
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 18);
Date d2(d1);
return 0;
}
注意:构造函数对内置类型不处理,自定义类型会调用它的默认拷贝构造函数,而拷贝构造函数,会对内置类型和自定义类型都会处理,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
//...扩容
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1);
return 0;
}
我们没写拷贝构造函数,是完成了值拷贝,值都是一样的。但是在析构的时候出现了问题
析构函数就是释放类里面的资源,我们写的析构出现了问题,代码很简单,可以定位到是free出现了问题,free是库提供给我使用的函数,本身应该没有问题,那么就是释放这块空间出现了问题。
看到了吗,编译器默认生成的拷贝构造函数,是浅拷贝也是值拷贝,和我们所想要的拷贝,st1和st2是两块独立的空间不一样,结果导致这块空间释放了两次,因此报错。
为了解决这块问题,我们需要自己写一个深拷贝,
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//拷贝构造函数
//Stack st2(st1);
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(int x)
{
//...扩容
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(st1);
return 0;
}
现在两块就是独立的空间,析构就没有问题了。
问:什么时候需要写拷贝构造函数?
答:需要自己写析构函数的类,都需要写深拷贝的拷贝构造函数,不需要自己写析构函数的类,默认生成的浅拷贝的拷贝构造函数就可以用
5.赋值运算符重载
5.1运算符重载
class Date
{
public:
//构造函数
Date(int year = 1, 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(2023, 6, 21);
Date d2(d1);
//我们如果想比较内置类型,int,char...
//可以直接比较
//但是如果想比较日期类谁大谁小或相等,编译器会支持我们吗?
d1 == d2;//报错,不能直接比较,因为类是我们自己定义的,编译器不知道类里面是怎么实现的,
//不像int,char编译器会自动比较。
return 0;
}
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
C++引入运算符重载,主要是为了让自定义类型也能使用运算符。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)。
class Date
{
public:
//构造函数
Date(int year = 1, 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;
};
//运算符重载函数
bool operator==(Date d1, Date d2)
{
return d1._year == d2._year
&& d1._month == d2._month;
&& d1._day == d2._day;
}
int main()
{
Date d1(2023, 6, 21);
Date d2(d1);
d1 == d2;//我们的编译器非常聪明,会自动把d1 == d2 转换成 operator==(d1,d2);
operator==(d1,d2);//因此这样写也对,但是我们建议上面那种写法,增强了代码可读性。
//但是还是有问题。注意类的成员变量是私有的,外面不能直接访问,
//如果把运算符重载函数写外面,外面要访问成员变量,必须要把私有要变共有。
return 0;
}
但是封装性该如何保证呢?
因此,我们可以把运算符重载函数写类的里面。
class Date
{
public:
//构造函数
Date(int year = 1, 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;
}
//运算符重载函数,错误写法
bool operator==(Date d1, Date d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 21);
Date d2(d1);
d1 == d2;
return 0;
}
这里报这样一个错误,参数太多了。因为隐藏的有this指针
注意:运算符重载,运算符有几个操作数,就有几个参数
class Date
{
public:
//构造函数
Date(int year = 1, 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;
}
//运算符重载函数
//bool operator==(Date d)
//这样写还不是最好的,因为,传值参传这里会调用拷贝构造函数,会有时间消耗
//因此引用传参,最好在加上const,安全
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(2023, 6, 21);
Date d2(d1);
d1 == d2;//这里编译器会自动转换为d1.operator(d2);
//d1.operator(d2);//这样写也可以;但不建议
return 0;
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5. .* 、 :: 、 sizeof 、 ?: 、 . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
5.2赋值运算符重载
class Date
{
public:
//构造函数
Date(int year = 1, 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;
}
//加法重载
//这里传值返回,因为函数调用结束,返回对象不在了
Date operator+(int day)
{
Date ret(*this);
//简易写法,没有具体实现细节
ret._day + day;
return ret;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 21);
Date d2(d1);//拷贝构造函数,当创建一个对象时,拿同类型对象进行拷贝初始化
Date d3;
d3 = d2;//赋值重载函数,已近存在两个对象之间的拷贝
Date d4 = d1;//也是拷贝构造函数,注意区分
d4+1;
return 0;
}
注意,构造函数,析构函数,可以看成一对,拷贝构造函数,赋值重载函数也可以看成一对。
构造函数和析构函数,如果不写,编译器自动生成默认的,都是对内置类型不处理,自定义类型调用自己的构造函数或析构函数。而拷贝构造函数和赋值重载函数,是一样的,如果不写,编译器自动生成默认的,对内置类型和自定义类型都处理,内置类型按照字节方式直接进行拷贝(值拷贝),自定义类型调用自己的拷贝构造函数或赋值重载函数。
class Date
{
public:
//构造函数
Date(int year = 1, 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;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 21);
Date d2(d1);//拷贝构造函数,当创建一个对象时,拿同类型对象进行拷贝初始化
Date d3;
d3 = d2;//赋值重载函数,已近存在两个对象直接的拷贝
return 0;
}
对于日期类的而言,编译器默认生成的赋值重载函数够用了,不需要自己实现,那么其他类呢?
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//拷贝构造函数
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
//默认生成的赋值重载函数
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(4);
st1.Push(1);
st1.Push(2);
Stack st2(10);
st2.Push(10);
st2.Push(20);
//默认生成的赋值重载函数会报错
st1=st2;
return 0;
}
如果栈类,使用默认的赋值重载函数进行拷贝,那问题就大了,
这里首先会内存泄漏,其他当对象生命周期结束时,调用析构函数也会报错,同一块空间释放两次。
因此需要我们自己写个赋值重载函数。实现深拷贝
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//拷贝构造函数
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
//赋值重载函数
//这样写还有个问题,如果自己给自己赋值了怎么办
Stack& operator=(const Stack& st)
{
free(_a);
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
return *this;
}
void Push(int x)
{
_a[_top++] = x;
}
//....
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(4);
st1.Push(1);
st1.Push(2);
Stack st2(10);
st2.Push(10);
st2.Push(20);
st1=st2;
st1=st1;//自己给自己赋值,虽然编译不会报错,但是由于我们上面那种写法,栈内的值变成了随机值。
return 0;
}
正确写法如下
//赋值重载函数
Stack& operator=(const Stack& st)
{
//针对自己给自己赋值的优化处理
if (this != &st)
{
free(_a);
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == NULL)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
问:什么时候需要写赋值重载函数,什么时候不需要写?
答:如果要自己写析构函数,就需要写赋值重载函数,不用自己写析构函数,默认生成的赋值重载函数就够用。
5.3>> <<运算符
接下来我们在看两个运算符重载 << , >> 函数
在前面的时候我们学过<<是流插入,>>是流提取,并且cout和cin(输出,输入)是自动识别类型的,用着比较方便,我们来深究一下原因,为什么可以这样?
#include<iostream>
using namespace std
int main()
{
int i = 1;
double d= 12.12;
cout << i ;
cout << d ;
return 0;
}
我们知道C++把类型分为内置类型和自定义类型。内置类型有int,char等等,而且我们不用做任何特殊处理,直接就可以用,
而cin和cout是两个全局对象,它们类型分别是istream,ostream,属于自定义类型。使用它们的时候需要包含头文件。自定义类型是一个非常复杂的东西,每个人定义的类型都是不一样的,而内置类型都是一样的。因此编译器在设计的时候就对内置类型进行处理好了,自动转化为自己认识的汇编语言,而对自定义类型,需要自己处理。
那么对于 cout << i ; cout << d; 一个内置类型和一个自定义类型这样在一块,编译器是不会自动生成的,需要进行特殊处理,但是我们为什么可以什么都不做就直接使用了呢?
其实这里面包含了运算符重载和函数重载,是大佬写好了直接给我们用的。所以不用自己写,包个头文件就可以使用了。
int main()
{
Date d1(2023, 6, 21);
cin >> d1;//报错,cin里面没有这个运算符重载函数
}
我们自己写个运算符重载函数
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;
}
// 赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 析构函数
//日期类不需要写析构函数
//~Date();
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
//>>运算符重载函数
//这样有些别扭,因为this指针指向日期类的对象
/*void operator>>(const Date& d)
{
}*/
//但是这样写会报错
void operator>>(istream& in)
{
in>>_year>>_month>>_day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 21);
cin >> d1;//报错
d1.operator>>(cin);//没问题
d1 >> cin;//没问题
}
因为类成员函数第一个位置默认是this指针,默认指向类的对象,Date对象就是左操作数。
因此>>流提取,<<流插入重载一般不写成成员函数
//Date.h
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
Date(const Date& d);
// 赋值运算符重载
Date& operator=(const Date& d);
// 析构函数
//日期类不需要写析构函数
//~Date();
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
//>>运算符重载函数
//这样有些别扭,因为this指针指向日期类的对象
/*void operator>>(const Date& d)
{
}*/
//即使这样写,编译器还是会报错
//void operator>>(istream& in)
//{
// in >> _year >> _month >> _day;
//}
private:
int _year;
int _month;
int _day;
};
//写成全员函数,访问类的成员变量,必须要把私有变成共有
//不仅封装没法得到保证,并且链接会有重定义的问题
void operator>>(istream& in, const Date& d)
{
in >> d._year >> d._month >> d._day;
}
//Date.cpp
// 全缺省的构造函数
Date::Date(int year ,int month , int day )
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
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;
}
//test.cpp
int main()
{
Date d1;
cin >> d1;
}
这是因为在预处理阶段,头文件会完成包含,test.cpp和Date.cpp都会有这个函数,并且都会在汇编的时候都会进入符号表,在链接时候合并符号表就出现了这个问题。
三种解决方法
//方法1
/*static void operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
}*/
加上static,就是静态函数,只能在当前.cpp文件中使用,别的.cpp用不着,不会进入符号表
//方法2
//声明定义分离
//Date.h
void operator>>(istream& in, Date& d);
//Date.cpp
void operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
}
注意这里还有个链式访问的问题,
int main()
{
Date d1;
Date d2;
cin >> d1 >> d2;//会有这样的需求
}
解决方法
//方法1
static istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
//方法2
//声明定义分离
istream& operator>>(istream& in, Date& d);
//Date.cpp
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
返回in,这样就可以链式访问了,但是这里还是不能解决封装性,因此引入了有元函数
//Date.h
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
Date(const Date& d);
// 赋值运算符重载
Date& operator=(const Date& d);
// 析构函数
//日期类不需要写析构函数
//~Date();
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
//在任意位置声明一下,可以让函数使用私有成员
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
//有元函数
/*istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}*/
//最完美的再加个内联inline,频繁调用的小函数建议使用内联
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
cin和cout运算符重载函数,差不多注意加上const就行了,因为只是打印,不改变d的成员变量
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << '/' << d._month << '/' << d._day << endl;
}
6.const成员
6.1const修饰类的成员函数
//Date.cpp
void Date::Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
//test.cpp
int main()
{
Date d1;
d1.Print();
const Date d2(2023, 6, 21);
d2.Print();//有时可能会有const修饰的对象假如打印,但是报错了,为什么呢?
return 0;
}
C++解决方法
//const Date* const this
void Date::Print() const
{
cout << _year << '/' << _month << '/' << _day << endl;
}
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
总结:只要在成员函数中不改变成员变量,也就是*this对象数据,这些成员函数都应该加上const;
这里const成员函数,主要是针对传参时形参时const修饰的
bool operator>(const Date& d) const
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
else
{
return false;
}
}
//日期-日期
int operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
n++;
++min;
}
return n*flag;
}
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
答:
1.不可以,权限放大
2.可以,权限缩小
3.不可以,权限放大
3.可以,权限缩小
7.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
//Date.cpp
//取地址
Date* Date::operator&()
{
return this;
}
const Date* Date::operator&() const
{
return this;
}
//test.cpp
int main()
{
Date d1;
cout << &d1 << endl;
}
期待你的点赞,收藏,加评论,咱们下期再见~