[C++] 由浅入深理解面向对象思想的组成模块
文章目录
- (一) 类的默认成员函数
- (二) 构造函数
- 构造函数的特征
- 构造函数示例
- 无参构造
- 带参构造
- 冲突:全缺省参数的构造函数与无参构造函数
- (三)析构函数
- 特性
- 析构函数的析构过程解析
- (四)拷贝构造函数
- 什么是拷贝构造?
- 特性
- 为什么拷贝构造函数参数一定要以引用的形式?
- 值拷贝/浅拷贝
- 深拷贝
- 返回值为引用
- (五)运算符重载
- 如何定义及使用
- 定义格式及使用
- 前置++/后置++
- 前置++
- 后置++
- 重载“”
- (六)取地址运算符重载
- const成员函数
- 取地址运算符重载
类和对象其他学习链接:
- 类和对象敲门砖
- ;类和对象思维
(一) 类的默认成员函数
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
六个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 复制重载函数
- 取地址重载函数(普通对象,const对象)
(二) 构造函数
在C++中,构造函数是专门用于初始化对象的方法。当创建类的新实例时,构造函数会自动被调用。通过构造函数,我们可以确保对象在创建时就被赋予合适的初始状态。下面将详细解释如何使用构造函数进行初始化操作,并以Date类为例进行说明。
// 创建一个Date类 class Date { public: // 成员函数... private: int _year; int _month; int _day; };
构造函数的特征
- 构造函数的特点:
- 函数名与类名相同。
- ⽆返回值。
- 对象实例化时系统会⾃动调⽤对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
- ⽆参构造函数、全缺省构造函数、拷贝构造这三个我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
- C++把类型分为内置类型和自定义类型,内置类型就是语言提供的原生数据类型,例:int/char/double/指针等;自定义类型指的是我们用class/struct自己定义的类型。编译器的默认生成的构造函数与之相关:
- 内置类型:编译器默认生成的构造对内置类型没有初始化的要求。
- 自定义类型:生成的构造函数会调用自定义类型的构造函数,所以在自定义类型的构造函数中需要对内置类型进行初始化。
- 请注意第8条特征
构造函数示例
无参构造
无参构造函数允许我们创建Date对象而不提供任何参数。但是,需要注意的是,如果我们不在无参构造函数中初始化成员变量,那么这些变量的初始值将是未定义的,这可能会导致程序出错。
Date d1; // 调用无参构造函数
class Date { public: // 1. 无参构造函数 Date() { // 在这里可以添加一些初始化代码,例如设置默认日期 // 例如:_year = 2000; _month = 1; _day = 1; } // 其他成员函数... private: int _year; int _month; int _day; };
带参构造
带参构造可以和无参构造函数重载,因为在之后调用的时候不会受影响,可以与之后讲解的全缺省构造函数和无参构造函数之间的不能函数重载的进行区别。
带参构造函数可以在对对象进行初始化的时候进行传参,传参的数值会直接进行初始化对象中的成员变量。
Date date2(2023, 3, 15); // 调用带参构造函数创建对象,并初始化日期为2023年3月15日
class Date { public: // 1. 无参构造函数 Date() { // ... } // 2. 带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // 其他成员函数... private: int _year; int _month; int _day; };
在这个带参构造函数中,我们通过参数year、month和day来初始化_year、_month和_day成员变量。这样,我们就可以在创建Date对象时直接指定日期了。
注意区别创造对象的格式:
Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数
冲突:全缺省参数的构造函数与无参构造函数
C++11 😗*内置类型成员变量在类中声明时可以给默认值。 **
全缺省参数的构造函数结构类似于以下代码:
Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
特点:会在参数列表中进行类似于赋值的操作
这个构造函数接受三个参数,并且每个参数都有一个默认值。这意味着,在创建Date对象时,你可以选择性地提供这些参数。如果你没有为任何一个参数提供值,那么它们将使用默认值(即1900年1月1日)。
思考:以下代码是否可以编译通过?
class Date { public: Date() { _year = 1900; _month = 1; _day = 1; } Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
结论:无法通过。
原因:
- 语法可以存在、调用存在歧义。
- 无参构造和全缺省存在歧义,当使用不传参创建对象Date d;的时候编译器无法抉择选择构造函数。
- 推荐使用全缺省参数的构造函数。
(三)析构函数
对象在销毁时(生命周期结束时)会自动调用析构函数,完成对象中资源的清理工作(如释放动态分配的内存、关闭文件等)。
特性
- 析构函数名是在类名前面加上“ ~ ” :~Stack() { } ;
- 无参数和返回值;
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数;
- 对象生命周期结束时,C++编译系统系统自动调用析构函数,即使我们显式写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
注意:
- 析构函数主要是为了清理申请的资源,防止内存泄漏;
- 同一域内后定义的对象先析构。
typedef int DataType; class Stack { public: Stack(size_t capacity = 3) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(DataType data) { if (_size == _capacity) { // 扩展数组大小 _capacity *= 2; _array = (DataType*)realloc(_array, sizeof(DataType) * _capacity); if (nullptr == _array) { perror("realloc扩展空间失败!!!"); return; } } _array[_size] = data; _size++; } // 其他方法... ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _capacity; size_t _size; }; void TestStack() { Stack s; s.Push(1); s.Push(2); } int main() { TestStack(); return 0; }
析构函数的析构过程解析
当正确使用析构函数后就不用担心程序中有内存泄漏的情况了,因为在每次该对象生命周期结束后都会自动调用析构函数,流程如下:
- ①准备出生命周期
- ②出生命周期,进入析构函数
- ③析构函数执行完毕,对象销毁
(四)拷贝构造函数
什么是拷贝构造?
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数
也叫做拷⻉构造函数,拷⻉构造是⼀个特殊的构造函数。
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成**(只要是拷贝行为就会调用拷贝构造)**。
- 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造(深拷贝)。
为什么拷贝构造函数参数一定要以引用的形式?
// 错误的写法 Date(const Date d) { _year = d._year; _month = d._month; _day = d._day; }
以日期类举例:若使用Date(const Date d)传参进行拷贝构造时,在传参的时候例如是以Date(d2)来传参那么就相当于用d = d2,这样的话由于是在构造一个新的对象d2,所以会继续调用拷贝构造函数,如此下去就会造成无限循环的去调用拷贝构造函数而不会执行结束。
Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
所以正确的写法应该如上代码所示。
在写的参数的时候用const是为了保证数据的安全性,防止被修改。
值拷贝/浅拷贝
浅拷贝是指在创建对象的副本时,只复制对象本身,而不复制对象所持有的资源(如动态分配的内存)。浅拷贝可能导致的问题是,如果原始对象和副本对象都尝试释放相同的资源,就可能发生内存泄漏或双重释放错误。
深拷贝
深拷贝是指在创建对象的副本时,不仅复制对象本身,还复制对象所持有的所有资源。这意味着如果对象包含指针指向动态分配的内存,深拷贝会为副本对象分配新的内存,并复制原始内存中的数据。
- 对于每个指针成员,分配新的内存并复制数据。
- 对于非指针成员,直接复制值。
- 通过深拷贝即可解决浅拷贝中:释放相同的资源错误的问题。
在默认生成的拷贝构造函数和赋值运算符重载中使用的是浅拷贝还是深拷贝取决于自定义成员变量的拷贝构造函数,当没有空间申请的时候一般会使用浅拷贝,但是在有空间申请的时候会进行深拷贝,前提是自定义成员变量的拷贝构造函数有申请空间进行拷贝,这样上一级自动生成的默认构造函数才会进行正确调用。
例如:用两个栈实现队列
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指向资源创建同样⼤的资源再拷⻉值 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); return; } memcpy(_a, st._a, sizeof(STDataType) * st._top); _top = st._top; _capacity = st._capacity; } void Push(STDataType x) { if (_top == _capacity) { int newcapacity = _capacity * 2; STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } _a = tmp; _capacity = newcapacity; } _a[_top++] = x; } ~Stack() { cout public : private: Stack pushst; Stack popst; }; int main() { MyQueue mq1; MyQueue mq2 = mq1; return 0; } Date tmp(2024, 7, 5); tmp.Print(); return tmp; } Date tmp(2024, 7, 5); tmp.Print(); return tmp; } _day += day; while (_day GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); ++_month; if (_month == 13) { ++_year; _month = 1; } } return *this; } out *this += 1; // 修改当前对象 return *this; // 返回当前对象的引用 } Date temp = *this; // 保存当前对象状态 *this += 1; // 修改当前对象 return temp; // 返回保存的临时对象 } out cout cout return this; // return nullptr; }
- ③析构函数执行完毕,对象销毁
- ②出生命周期,进入析构函数
- ①准备出生命周期