Bootstrap

【C++ 类和对象 进阶篇】—— 逻辑森林的灵动精灵,舞动类与对象的奇幻圆舞曲

欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡
本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟
个人主页 👉 ZyyOvO
本文专栏➡️C++ 进阶之路

创作者

各位于晏,亦菲们请看

1、类的默认成员函数

类的默认成员函数是编译器在没有显式定义相应函数时自动生成的函数。这些函数通常是为了处理类对象的生命周期管理,包括对象的创建、复制、赋值和销毁等操作。确保即使开发者没有显式提供某些操作,编译器也能提供默认实现,以保证程序的基本功能。
通常包括以下几个函数:
类的六个默认成员函数

  • 默认构造函数
  • 拷贝构造函数
  • 拷贝赋值重载
  • 析构函数
  • 取地址操作符重载
    这些默认成员函数由编译器在没有显式定义时自动生成,它们通常执行逐成员的浅拷贝或简单的资源释放。对于包含动态内存或复杂资源管理的类,通常需要显式实现这些函数,确保资源的正确管理。
    C++11之后还引入了两个成员函数:
  • 移动构造函数(C++11引入)
  • 移动赋值重载(C++11引入)
    本文我们重点讲解前五个默认成员函数

2、构造函数

构造函数的特点:

  • 函数名称与类名相同。
  • 没有返回类型(甚至没有void)。
  • 自动调用:每当创建对象时,构造函数会被自动调用。
  • 可以有多个构造函数(构造函数重载)。
  • 不能被显式调用:构造函数只能在对象创建时由编译器调用。

构造函数的类型:

  • 默认构造函数(Default Constructor)
  • 定义:没有参数或者所有参数都有默认值的构造函数。 作用:初始化对象时如果没有传递参数,默认构造函数会被调用。
  • 编译器自动生成:如果没有定义任何构造函数,编译器会生成一个默认构造函数;如果定义了其他构造函数(如带参数的构造函数),则编译器不会自动生成默认构造函数。
  • 无参构造函数、全缺省构造函数、我们不显示定义时编译器默认生成的构造函数,都叫做默认构造函数
  • 但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造函数

编译器默认成的构造函数,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,取决于编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,那么就会编译出错,我们要初始化这个成员变量,需要用初始化列表!(初始化列表后面再讲)

以Date类为例:如果我们不显示定义构造函数,编译器会自动生成默认默认构造函数

class Date
{
public:
private:
	int _year;
	int _month;
	int _day;
};

无参构造函数:

Date()
{
	_year = 2025;
	_month = 1;
	_day = 15;
}

全缺省构造函数:

Date(int year=2025, int month=1, int day=15)
{
	_year = year;
	_month = month;
	_day = day;
}

以上两种构造函数以及编译器自动生成的构造函数都属于默认构造函数

  • 带参数构造函数(Parameterized Constructor)
  • 定义:包含一个或多个参数的构造函数,允许在创建对象时为对象成员传递初始值。
  • 作用:通过传递不同的参数,创建对象时可以初始化不同的值。
 Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

3、析构函数

  • 定义:析构函数在对象生命周期结束时调用,用于释放对象占用的资源。
  • 自动生成:如果你没有定义析构函数,编译器会自动生成一个默认析构函数。它会销毁对象并释放资源。但如果类中有动态内存分配,或者存在资源申请,则需要显式定义析构函数。

析构函数的特点:

  • 析构函数的名称必须与类名相同,但在前面加上波浪号 ~。例如,类 Date 的析构函数名称应该是 ~Date()。
  • 析构函数没有返回类型,(包括 void)。
  • 析构函数不能带有参数,因此无法重载
  • 自动调用:析构函数由编译器在对象生命周期结束时自动调用。当一个对象超出其作用域时,或显式调用 delete删除一个动态分配的对象时,析构函数会被调用。
  • 调用顺序: 对于 局部对象,析构函数在对象超出作用域时被自动调用。 对于 动态分配的对象,析构函数在 delete 语句执行时被调用。
    跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,对于自定义类型成员会调用它的析构函数。

析构函数的基本语法:

class Date 
{
public:
	~Date()
	{
		//完成对资源的清理和释放
	}
};

析构函数的作用:
析构函数的作用是清理对象在其生命周期内所占用的资源。常见的资源包括:

  • 动态内存: 通过 new 动态分配的内存需要在析构函数中通过 delete 或 delete[] 释放。
  • 文件句柄、数据库连接、网络资源:如果对象打开了文件或建立了网络连接,析构函数应负责关闭它们。
  • 其他资源:任何外部资源(例如锁、线程、内存映射等)都应在析构函数中清理。

析构函数的自动调用
析构函数的调用通常是在以下情况下自动发生:

  • 对象生命周期结束时:当局部对象超出作用域时,析构函数会自动调用。例如,当 main() 函数结束时,局部对象的析构函数会被调用。

  • 动态分配的对象:如果对象是通过 new 分配的,那么在调用 delete 时,析构函数会被自动调用。

  • ⼀个局部域的多个对象,C++规定后定义的先析构。

简单的析构函数示例:

#include<iostream>
using namespace std;
class MyClass {
public:
    MyClass() {
        ptr = new int[10];  // 动态分配内存
        cout << "Constructor: Memory allocated" << endl;
    }
    ~MyClass() {
        delete[] ptr;  // 释放内存
        cout << "Destructor: Memory deallocated" << endl;
    }
private:
    int* ptr;
};
int main() {
    MyClass obj;  // 创建对象时调用构造函数,释放内存时调用析构函数
    return 0;
}

4、拷贝构造函数

拷贝构造函数是一个特殊成员函数,用于通过另一个同类型的对象来初始化一个对象。如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。它通常在以下几种情况下被调用:

  • 对象作为函数参数传递时(按值传递)
  • 从函数返回一个对象(返回值时)
  • 初始化一个对象为另一个同类型对象的副本(拷贝初始化)

拷贝构造函数的特点:

  • 拷贝构造函数是构造函数的⼀个重载
  • 拷贝构造函数的第⼀个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值
  • C++规定自定义类型对象进行拷贝必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  • 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的逐字节拷贝),对自定义类型成员变量会调用他的拷贝构造。
    拷贝构造
    拷贝构造的基本语法:
 ClassName(const ClassName& other)
 {
 	//完成对象的拷贝
 }

ClassName 是类名。
const ClassName& other 是另一个同类型对象的引用,表示将要复制的数据。
关键点:

  • const:拷贝构造函数的参数必须是 const,这样可以保证传递的对象不被修改。
  • &:参数是引用类型,避免了对象的拷贝开销。
  • 传递方式:拷贝构造函数通常使用按引用传递对象,而不是按值传递,防止出现不必要的递归调用。

拷贝构造函数的使用场景:

  • 按值传递参数时: 当对象作为函数参数传递,并且该对象以值的方式传递时,会调用拷贝构造函数。
void function(MyClass obj) {
    // 对象 obj 会调用拷贝构造函数
}
MyClass obj1;
function(obj1);  // 这里会调用拷贝构造函数
  • 返回值时: 当一个函数返回一个对象时,编译器会使用拷贝构造函数来复制返回值。
MyClass createObject() {
    MyClass obj;
    return obj;  // 这里会调用拷贝构造函数
}
  • 拷贝初始化: 当一个对象被另一个相同类型的对象初始化时,也会调用拷贝构造函数。
MyClass obj1;
MyClass obj2(obj1);// MyClass obj2 = obj1; 或者写成这样
 // 这里会调用拷贝构造函数

默认的拷贝构造函数:
如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,通常执行浅拷贝。这意味着它会简单地复制成员变量的值,对于指针成员,它只会复制指针的值(即地址),不会分配新的内存。这样会导致多个对象共享相同的内存资源,可能会出现内存泄漏或意外的资源共享。

class MyClass {
public:
    int* data;
    MyClass(int val) {
        data = new int(val);
    }
    ~MyClass() {
        delete data;
    }
};

MyClass obj1(10);
MyClass obj2 (obj1);  // 默认拷贝构造函数,浅拷贝

深拷贝的需求
如果类的成员变量涉及动态内存分配,默认的浅拷贝可能导致问题。拷贝构造函数需要进行深拷贝,即为每个对象分配新的内存空间,以避免不同对象共享同一内存。

class MyClass {
public:
    int* data;
    MyClass(int val) {
        data = new int(val);
    }
    // 手动定义深拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data);  // 为每个对象分配新的内存
    }
    ~MyClass() {
        delete data;
    }
};
MyClass obj1(10);
MyClass obj2 = obj1;  // 调用深拷贝构造函数

注意:

  • 如果没有指针成员,且不涉及动态内存分配,可以使用默认的浅拷贝
  • 如果类包含指针成员或动态分配的内存,必须自定义拷贝构造函数,并使用深拷贝。
  • 注意自身赋值:在拷贝构造函数中,通常不需要处理自赋值问题(因为自身赋值问题通常出现在赋值运算符重载中),但实现赋值运算符重载时,需要特别注意自身赋值情况。

5、运算符重载

在 C++ 中,运算符重载(Operator Overloading)允许你为自定义类型定义或修改运算符的行为,使得可以使用运算符来操作类的对象。通过运算符重载,类的对象可以像内建类型一样使用运算符进行各种操作(如加法、减法等)。

  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
ReturnType operator符号(参数列表)
{
	//函数体
}

ReturnType:返回类型,通常是操作后的结果类型。
operator符号:运算符的标识符,例如 +、-、*、[]、= 等。
参数列表:指定操作数的类型和数量。

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元
    运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

  • 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。

  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。

  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。

  • .* :: (域作用解析符)sizeof ?: . 等运算符不能重载

  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。
    C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。

  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

举个例子:

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;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	Date& operator++()
	{
		cout << "前置++" << endl;
		//...
		return *this;
	}
	Date operator++(int)
	{
		Date tmp; 
		cout << "后置++" << endl;
		//...
		return tmp;
	}
private:
	int _year;
	int _month;
	int _day;
};

5.1、赋值运算符重载

赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟
拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。

为什么需要重载赋值运算符?
默认的赋值运算符执行浅拷贝,即直接复制对象的成员变量。如果类中有指针成员或者动态分配的内存,浅拷贝可能导致内存泄漏或多重释放等问题。因此,当类中涉及动态内存时,你必须手动实现赋值运算符,以确保深拷贝

赋值运算符重载的特点:

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝
  • 有返回值,且建议写成当前类类型引用。引用返回可以提高效率,有返回值目的是为了支持连续赋
    值场景。
  • 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷
    贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义
    类型成员变量会调用它的赋值重载函数。

如果需要显式实现赋值运算符重载要注意以下几点:

  • 检查自赋值:如果左边对象和右边对象是同一个对象(即自赋值),则无需执行任何操作。
  • 释放当前资源:在执行赋值之前,应该释放当前对象占用的资源,以避免内存泄漏。
  • 执行深拷贝:将右边对象的数据复制到左边对象的成员变量中。
  • 返回*this:以支持链式赋值操作。

实现简单的赋值运算符:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		cout << " Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 传引用返回减少拷贝
	// d1 = d2;
	Date& operator=(const Date& d)
	{
		// 注意自身赋值情况
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		// d1 = d2表达式的返回对象应该为d1,也就是*this
		return *this;
	}
	void Print()
	{
			cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

5.2、const成员函数

定义:

  • const 成员函数是指那些不会修改对象状态的成员函数。它是通过在成员函数的声明和定义后加上 const 关键字来标识的。const 成员函数保证不会修改对象的成员变量,确保对象的状态在调用该函数时保持不变。
  • const实际修饰该成员函数隐含的this指针。表明在该成员函数中不能对类的任何成员进行修改。

const成员函数的作用:

  • 保证不修改对象状态:const 成员函数承诺不会修改对象的数据,这对于许多场合(如传递常量对象或在多线程中共享对象)非常重要。
  • 编译时检查:通过将成员函数声明为 const,编译器可以帮助检查是否存在意外修改对象的情况。如果在 const成员函数中尝试修改对象的成员变量,编译器会报错。
  • 提高可读性和安全性:它明确表示该函数是只读操作,不会改变对象的状态,有助于代码的可读性和维护性。

例如Date类中的Print函数:修饰为const成员函数,它只读取对象的状态,不修改任何成员变量。

 void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}

const成员函数的限制:

  • 不能修改成员变量:const 成员函数中不能修改对象的成员变量,除非这些成员变量被声明为 mutable
  • 不能调用非 const成员函数:const 成员函数不能调用任何非 const 成员函数,因为后者可能会修改对象的状态。

5.3、 mutable关键字

在 C++ 中,mutable 关键字用于修饰类的成员变量,允许在 const 成员函数 中修改该成员变量。通常,const 成员函数表示该函数不会修改对象的状态,但有时我们希望某些成员变量在这些函数中仍然可以修改。使用 mutable 可以实现这一点。

mutable关键字的使用:

class MyClass {
public:
    MyClass(int counter=0)
     {
        _counter = counter;
    }
    // const 成员函数可以修改 mutable 成员变量
    void incrementCounter() const {
        _counter++;
    }
    void displayCounter() const {
        cout << "Counter: " << _counter << endl;
    }
private:
    mutable int _counter;  // mutable 成员变量
};
int main() {
    MyClass obj;
    obj.incrementCounter();  // 修改 mutable 成员变量
    obj.displayCounter();  // 输出:Counter: 1
    return 0;
}

mutable 的使用场景:

  • 缓存:有时我们希望在 const 成员函数中修改某些缓存数据,这时可以使用 mutable。
  • 计数器:在一些只读操作中(如访问次数计数器),需要更新计数器而不影响对象的逻辑状态。
  • 延迟计算:可以用于延迟计算或懒加载(lazy loading)等操作。

mutable的注意事项:

  • 只修改 mutable 成员:mutable 关键字只影响被修饰的成员变量,其他成员变量依然受到 const 限制
  • const成员函数的约束:mutable 使得 const成员函数能够修改成员变量,但不会改变对象的外部状态。它仍然保持对象的“逻辑不可变性”,但允许修改内部的、仅对函数内部有意义的数据。

5.4、取地址运算符重载

取地址运算符(&)用于获取对象的内存地址。虽然我们可以直接使用 & 来获取对象的地址,但如果你需要在自定义类中对取地址运算符进行重载,则可以通过重载 operator& 来实现,通常情况下是返回当前对象的this指针

  • 取地址运算符重载分为普通取地址运算符重载和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; // 日
};

6、完整的Date类实现

  • Date.h 头文件
class Date{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 2005, int month = 5, int day = 18);
	bool DateCheck();
	void print();
	int GetMonthDay(int year,int month)
	{
		static int ArrMonth[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
			return 29;
		}
		return ArrMonth[month];
	}
	
	bool operator<(const Date& d) const;
	bool operator<=(const Date& d) const;
	bool operator>(const Date& d) const;
	bool operator>=(const Date& d) const;
	bool operator==(const Date& d) const;
	bool operator!=(const Date& d) const;
	
	Date& operator+=(int day);
	Date operator+(int day);

	Date& operator-=(int day);
	Date operator-(int day);

	int operator-(const Date& d)const;

	Date& operator++();
	Date  operator++(int);

	Date& operator--();
	Date  operator--(int);

private:
	int _year;
	int _month;
	int _day;
};
  • Date.cpp 源文件
#include"Date.h"

bool Date::DateCheck()
{
	if (_month < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year, _month))
	{
		return false;
	}
	return true;
}


Date ::Date (int year , int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
	if (!DateCheck())
	{
		cout << "日期非法" << endl;
		print();
	}
}


void Date::print()
{
	cout << _year << "/" << _month << "/" << _day << endl;

}
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}

	_day += day;
	while (_day > GetMonthDay(_year,_month))
	{
		_day -= GetMonthDay(_year,_month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}

Date Date:: operator+(int day)
{
	Date tmp=*this;
	tmp._day += day;
	while (tmp._day > GetMonthDay(tmp._year,tmp. _month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			++tmp._year;
			tmp._month = 1;
		}
	}
	return tmp;
}

bool Date::operator<(const Date& d) const

{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			return _day < d._day;
		}
	}
	return false;
}
bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}
bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}
bool Date::operator>=(const Date& d) const
{
	return !(*this < d);
}
bool Date::operator==(const Date& d) const
{
	return _year == d._year &&
		_month == d._month &&
		_day == d._day;
}
bool Date::operator!=(const Date& d) const

{
	return !(*this == d);
}

int Date::operator-(const Date& d) const
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;
	while (min != max)
	{
		++min;
		++count;
	}
	if (1 == flag)
	{
		return count;
	}
	else
		return -count;
}

Date& Date::operator++()
{
	*this+=1;
	return *this;
}
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

Date& Date:: operator--()
{
	*this -= 1;
	return *this;
}
Date Date:: operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}


Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}
	_day-= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
			
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}


Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;
		if (!d.DateCheck())
		{
			cout << "输入日期非法:";
			d.print();
			cout << "请重新输入!!!" << endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

写在最后

如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏
如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉
祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!

祝福

;