一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。但是空类中并不是真的什么都没有,任何类在什么都不写的时候,编译器会自动生成以下 6 个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date{ // 空类 };
二、构造函数
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, 1, 26); d1.Print(); Date d2; d2.Init(2023, 8, 9); d2.Print(); return 0; }
对于 Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,或者有时候忘记初始化,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数, 名字与类名相同 ,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内 只调用一次 。
2、特性
构造函数是特殊的成员函数,不能以普通函数的定义和调用规则去理解,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是 初始化对象 。
特征:
- 函数名与类名相同。
- 无返回值,也不用写 void。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
class Date { public: // 1、无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 写法相同 /*Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }*/ // 2、带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2(2023, 9, 12); // 调用带参的构造函数 Date d3(); // 声明了d3函数,该函数无参,返回一个日期类型的对象 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义呢?) }
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
5、如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date { public: /* // 如果用户显式定义了构造函数,编译器将不再生成 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; // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用 return 0; }
将 Date 类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数。
将 Date 类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成。
6、关于编译器生成的默认成员函数,在不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是 d 对象 _year / _month / _day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?
C++ 把类型分成内置类型(基本类型)和自定义类型。
- 内置类型就是语言提供的数据类型,如:int / double / char / 指针等。
- 自定义类型就是我们使用 class / struct / union 等自己定义的类型。
从下面的代码中可以发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数。也就是说,默认生成的构造函数对内置类型成员不作处理,对自定义类型成员会去调用它的默认构造函数。这个设计是 C++ 早期设计的一个缺陷,本来应该内置类型也一并处理。
- 对于类中的内置类型成员 —> 不处理(为随机值,除非声明时给了缺省值 - C++11)
- 对于类中的自定义类型成员 —> 自动调用它的默认构造函数(不要参数就可以调用的,比如 无参构造函数 或 全缺省构造函数)
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 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值,注意这里不是初始化,是声明,给缺省值。
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 = 1; // 声明 int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
7、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意 :默认构造函数:(不传参数就可以调用的)
- 无参构造函数。
- 全缺省构造函数。
- 没写,编译器默认生成的构造函数。
class Date { public: // 1、无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 2、带参构造函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; // 无法通过编译 - 调用存在二义性 return 0; }
构造函数的特点:不用传参数就可以调用。
- 一般的类都不会让编译器默认生成构造函数,大部分都会自己去写。显示写一个全缺省,很好用。
- 特殊情况才会默认生成构造函数(例如 Myqueue 这样的类)。
- 每个类最好都要提供默认构造函数。
三、析构函数
1、概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
构造函数是为了替代 Init,析构函数是为了替代 Destroy。
2、特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数,无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。
- 后定义的先析构,跟数据结构中的栈性质相同。
typedef int DataType; class Stack { public: Stack(size_t capacity = 4) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } // 其他方法... ~Stack() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: DataType* _array; int _capacity; int _size; }; void TestStack() { Stack s; s.Push(1); s.Push(2); }
6、关于编译器自动生成的析构函数,从下面的程序可以看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。
- 对于类中的内置类型成员 —> 不处理。
- 对于类中的自定义类型成员 —> 调用它的析构函数完成清理工作。
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()
在 main 方法中根本没有直接创建 Time 类的对象,为什么最后会调用Time类的析构函数?
因为 main 方法中创建了 Date 对象 d,而 d 中包含 4 个成员变量,其中 _year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而 _t 是 Time 类对象,所以在 d 销毁时,要将其内部包含的 Time 类的 _t 对象销毁,所以要调用 Time 类的析构函数。但是 main 函数中不能直接调用 Time 类的析构函数,实际要释放的是 Date 类对象,所以编译器会调用 Date 类的析构函数,而 Date 没有显式提供,则编译器会给 Date 类生成一个默认的析构函数,目的是在其内部调用 Time 类的析构函数,即当 Date 对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main 函数中并没有直接调用 Time 类析构函数,而是显式调用编译器为 Date 类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
7、如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date 类;有资源申请时,一定要写,否则会造成资源泄漏,比如 Stack 类。
默认生成析构函数的特点(跟构造函数类似):
- 一些内置类型的类不作处理。比如 Date这样的类,没有资源需要清理;比如 MyQueue 也可以不写,默认生成的就可以。
- 自定义类型成员回去调用它的析构函数,比如:Stack、Queue...
四、拷贝构造函数
1、概念
在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数 :只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2、特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错,因为会引发无穷递归调用。
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) // 正确写法 //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; }
使用引用作为拷贝构造函数的参数可以有效避免无穷递归调用的问题。因为引用在初始化时不会调用拷贝构造函数,而是直接将引用绑定到已经存在的对象上。
tips:建议拷贝构造函数的参数类型前加上 const,防止其值误被修改。
3、若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
浅拷贝:
- 一个对象修改会影响另一个对象。
- 会析构两次,程序崩溃。
解决方法:自己实现深拷贝。
如果没有显式定义,编译器自动生成的拷贝构造函数,它会做哪些事情呢?
- 对于类中的内置类型成员 —> 值拷贝。
- 对于类中的自定义类型成员 —> 自动调用它的拷贝构造函数来完成拷贝初始化。
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 = 1; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2(d1); return 0; }
用已经存在的 d1 拷贝构造 d2,此处会调用 Date 类的拷贝构造函数。但 Date 类并没有显式定义拷贝构造函数,则编译器会给 Date 类生成一个默认的拷贝构造函数。
注意 :在编译器生成的默认拷贝构造函数中,内置类型是 按照字节方式直接拷贝 的,而自定
义类型是 调用其拷贝构造函数 完成拷贝的。
4、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?
像日期类这样的类是没必要的。但有些需要深拷贝的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成深拷贝的。
// 下面这个程序会崩溃掉,因为这里就需要深拷贝去解决。 typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); // 会造成指向的同一块区域的内容被两次释放 return 0; }
注意 :类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
如图:指向了同一块空间。
那么会引发什么问题呢?会导致 _str 指向的空间被释放两次,引发程序崩溃。
5、拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象。
- 函数参数类型为类类型对象。
- 函数返回值类型为类类型对象。
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(2023 ,9, 13); Test(d1); return 0; }
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
一些类需要显示写拷贝和赋值,比如:Stack、Queue...
一些类不需要显示写拷贝和赋值。比如 Date 这样的类,默认生成就会完成值拷贝 / 浅拷贝,比如 MyQueue 这样的类,默认生成就会调用它的自定义类型成员 Stack 的拷贝和赋值。
五、赋值运算符重载
1、运算符重载
C++ 为了增强代码的可读性引入了 运算符重载 ,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字:关键字 operator 后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator 操作符(参数列表)。
内置类型可以直接使用运算符运算,编译器知道要如何运算。
自定义类型无法直接使用运算符,编译器不知道要如何运算。
注意 :
- 不能通过连接其他符号来创建新的操作符:比如 operator@。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型 +,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this。
- .* :: sizeof ?: . 注意以上 5 个运算符不能重载。
// 全局的operator== class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private: int _year; int _month; int _day; }; // 这里会发现运算符重载成全局的就需要成员变量是公有的,那么要如何保证封装性呢? // 这里其实可以用友元解决,或者干脆重载成成员函数。 bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } void Test () { Date d1(2023, 9, 13); Date d2(2023, 9, 14); cout << (d1 == d2) << endl; }
运算符重载,一般有两种方式:
- 重载成类的成员函数(形参数目看起来比该运算符需要的参数少一个,因为成员函数有隐含的 this 指针,且函数的第一个形参就是 this 指针)。
- 重载成类的友元函数(必须有一个参数要是类的对象)(一般不这样做,而是重载成成员函数)。
下面这种写法更好:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // bool operator==(Date* this, const Date& d2) bool operator==(const Date& d2) // 注意左操作数是隐藏的this,指向调用函数的对象 { return _year == d2._year && _month == d2._month && _day == d2._day; } private: int _year; int _month; int _day; }; void Test () { Date d1(2023, 9, 13); Date d2(2023, 9, 14); cout << d1.operator==(d2) << endl; // d1.operator==(&d1, d2); }
【C++】类和对象(中)(2)https://developer.aliyun.com/article/1514568?spm=a2c6h.13148508.setting.18.4b904f0ejdbHoA