C++类和对象(中)- 默认成员函数
目录
1. 类的默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 运算符重载
5.1 运算符重载
5.2 赋值运算符重载
6. 取地址运算符重载
1. 类的默认成员函数
什么是默认成员函数?即用户没有显示去实现,编译器自动生成的成员函数。但实际上大多数情况会显示写?那这个默认成员函数到底有啥用?别着急,先慢慢看。
一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要(了解即可)。
这时存在两个疑惑?
1. 编译器自动生成的行为是什么?仅凭默认成员能否完成需求?
2. 什么情况下不会默认生成?不默认生成又该如何处理?
2. 构造函数
构造函数光听名字会觉得是开辟空间构造一个新的对象,但实际上并不是,这里的构造函数主要是初始化(对象实例化完成初始化),相当于我们以前Stack和Date类中写的Init函数的功能。
构造函数的特点:
1. 函数名和类名相同;
2. 不需要返回值(void也不用写,C++规定);
3. 函数实例化时自动调用构造函数;
4. 构造函数可以重载;
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;
6. 无参构造函数,全缺省构造函数,不显示写编译器自动生成的构造函数都是默认构造。但是需要注意的是上面的三个默认构造不能同时出现,只能出现一个。比如无参构造函数和全缺省构造函数,在上一节说明了,同时存在编译器不知道调用谁。不显示和前两个不能同时存在应该能理解。总结一下就是不传实参就可以调用的构造就叫默认构造。
class Date
{
public:// 无参函数Date(){_year = 2025;_month = 8;_day = 6;}// 带参函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 全缺省函数Date(int year = 2025, int month = 9, 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();Date d1;d1.Print();return 0;
}
7. 如果我们不写构造函数,那么编译器则会自己生成默认构造,但是编译器生成的默认构造对内置类型不做处理(即内置类型里面的值可能是随机值,具体取决于编译器),对于自定义类型去调用它的默认构造(即要求调用这个成员变量的默认构造函数初始化)。那这个该怎么初始化呢?这个下次讲解(留个悬念)
(注:内置类型(语言提供的原生数据类型):int,char,double.... 自定义类型:用class/struct等关键字定义的类型)
3. 析构函数
析构函数看着挺陌生,实际上就是destroy,对资源进行清理和销毁。C++规定对象在销毁时,会自动调用析构函数。就比如之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
1. 函数名为类名前加字符~ ;
2. 不需要返回值(也不需要写void,C++规定)
3. 一个类只能有一个析构函数,不能析构重载。如果不显示写,系统会自动生成默认的析构函数。
4. 对象生命周期结束,会自动调用析构函数。
5. 如果不写析构函数,编译器会自动生成默认析构。和构造一样,对内置类型不做处理,对于自定义类型会调用它对应的析构函数。
6. 如果显示写了析构,编译器对于自定义类型也会用它的析构函数。总的来说,自定义类型无论是否显示写,都会调用它对应的析构函数。
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public:~MyQueue() // 显示写会调用析构,同时自动调用Stack的析构{}
private:Stack pushst;Stack popst;
};
int main()
{//这里不会调用MyQueue的构造和析构,调用Stack的构造和析构MyQueue q1;return 0;
}
7. 如果没有申请资源(空间),编译器自动生成的析构函数就够了。但如果申请了资源,就必须得显示写析构函数,如果利用编译器自动生成的会造成内存泄漏,不安全。
8. 局部定义对象,调用析构的顺序是:先定义的后析构,后定义的先析构。(可以下去调试观看)
4. 拷贝构造函数
什么是拷贝构造?就是用一个已经存在的对象去构造另一个新对象。就比如我去捏瓷器,按照模板照猫画虎搞一个一摸一样的。
拷贝构造函数的特点:
1. 函数名和类名相同,是构造函数的一个重载;
2. 拷贝构造函数的第一个参数必须是类类型的引用,否则编译器会报错。拷贝构造函数也可以多参数,但是第一个参数必须是类类型的引用(忽略隐藏的this指针)
3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4. 如果不显示写拷贝构造,编译器会自动生成默认拷贝构造,这时内置类型是浅拷贝(值拷贝),自定义类型的拷贝则调用它的拷贝构造。
第2点为什么第一个参数必须是类类型对象的引用?(原因如下)
所以必须引用传参,否则会引发无穷递归。
6. 如果申请了资源,比如向stack和queue申请了空间资源,就得实现深拷贝,否则会出现报错(一块多次析构)。如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st){// 开辟一块同样大小的空间_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (_a == nullptr){perror("malloc fail!");}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};Queue{public:private:Stack pushst;Stack popst;};
如果是浅拷贝,则会:
7. 传值返回如果传的是局部对象,则要注意会调用拷贝构造;如果是传引用返回,就不会调用拷贝构造(传的是本身),但如果传的是局部对象的引用返回,则要注意局部对象出作用域会销毁,此时传引用返回就是野引用。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
// tmp出add会被销毁,此时是野引用
Date& Func2(){Date tmp(2024, 7, 5);tmp.Print();return tmp;}
int main(){Date d1;d1.Print();Date d2(d1);d2.Print();// 这样写也是拷贝构造Date d3 = d1;d3.Print();Date ret = Func2();ret.Print();return 0;}
5. 运算符重载
5.1 运算符重载
在C语言中比较大小,可以直接通过大于(>),小于(<)等直接进行比较。但是在类类型中是无法直接进行比较的,这时候C++提出了一种新的方法:通过运算符重载的新式赋予新的意义,可以实现类类型的比较。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
运算符重载的特点:
1. 函数名由operator和要重载的符号组成,具有其返回类型和参数列表以及函数体;
2. 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多,即一元运算符一个参数,二元运算符有两个参数;
3. 如果运算符重载函数定义在类里面,则要注意:不要忽略隐藏的this指针;
4. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
5. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
class Date
{
public:Date(int year = 2025, int month = 9, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}bool operator==(const Date& d){return _year== d._year&& _month == d._month&& _day == d._day;}void Print(){cout << _year << " " << _month << " " << _day << " " << endl;}~Date(){}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 9, 2);Date d2(2024, 6, 1);// << 的优先级比 == 高cout << (d1 == d2) << endl;return 0;
}
6. .* :: sizeof ?: . 注意以上5个运算符不能重载。(这里说明一下 .*)
7. 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: operator+(int x, int y)
8. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。这个int可以传任何值,没有限制。
// ++d1 -> d1.operator++()
date& date::operator++()
{*this += 1;return *this;
}
// d1++ -> d1.operator++(int)
date date::operator++(int)
{date tmp(*this);*this += 1;return tmp;
}
9. 重载<< 和 >>时,需要重载为全局函数。如果在类里面重载,第一个参数默认是this指针,此时cout<<d1就得写为d1<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。)
ostream& operator<<(ostream& out, 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;//输入之后我们需要检查是否合法if (!d.checkdate()){cout << "输入不合法,请重新输入" << endl;}return in;
}
// 如果定义类里面,传参就会变成d1 << cout;
5.2 赋值运算符重载
赋值运算符重载也是默认成员之一,两个已存在的对象的复制拷贝。注意和拷贝构造区别,拷贝构造是用一个对象去拷贝构造另一个对象(构造前还不存在)。
赋值运算符重载的特点:
1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const当前类类型引用,否则会传值传参会有拷贝;
2. 如果有返回值,那么最好写引用返回,减少拷贝,提高效率;
3. 如果不显示写拷贝构造,编译器会自动生成,这时内置类型成员变量是浅拷贝(值拷贝),自定义类型成员变量则会调用它对应的运算符重载函数。
4. 和拷贝构造一样,申请了资源得手动实现深拷贝。
bool operator==(const Date& d){return _year== d._year&& _month == d._month&& _day == d._day;}
注意:
int main()
{Date d1(2025, 9, 2);Date d2(2024, 6, 1);// 赋值重载d1 == d2;// 拷贝构造Date d3 = d1;return 0;
}
6. 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
此函数如果要显示写,一般是为了不想让别人拿到地址。
class Date
{
public:Date* operator&(){// return this;return nullptr;}const Date* operator&()const{return this;// return nullptr;}
private:int _year; int _month; int _day;
};