【c++】类和对象 (中) (类的默认成员函数)

类的默认成员函数

在C++中,如果你定义了一个类但没有显式地提供特定的成员函数(比如构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等),编译器会为这些函数生成默认的实现。这些默认生成的成员函数称为类的默认成员函数。那么既然编译器会默认生成,那么我们学习什么呢?

第一:我们要能判断默认生成的函数能否则满足我们的需求

第二:如果默认生成的函数不能满足我们的需求,那么我们该如何实现这些函数

实际上是八个,后面的后续补充。接下来我们依次来学习这些函数。

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数完成的并不是在内存中开空间的过程。当创建一个类的对象时,系统会为这个对象分配一块内存空间,以存储对象的数据成员。接下来才会调用构造函数,用来完成初始化这个对象,使申请的内存处于一个可用的状态。构造函数完美的替代了以前用c语言实现数据结构时用来初始化的Init。

构造函数的特点:

  1. 函数名与类名相同。

  2. 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

  3. 对象实例化时系统会自动调用对应的构造函数。

  4. 构造函数可以重载。

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。

  6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成 函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造。

  7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始 化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始 化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;
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;
	}
	// ...

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列

class MyQueue
{
public:
	//编译器默认生成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}

对自定义的成员变量会调用这个成员变量的默认构造。

如果这个成员变量没有默认构造就会报错

同时要注意对内置类型的处理c++标准没有规定,不同编译器处理的不同,为了规避错误,我们要自己手动处理内置类型。

接下来我们来手动的实现几个构造函数:

无参的构造函数

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include <iostream>
using namespace std;

//创建一个类Date

class Date
{
public:
	// 1.无参构造函数

	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.print();

	return 0;
}

在监视窗口可以看到在实例化对象之后,构造函数已经被自动调用了。

下面的是用c语言实现的链表的初始化,相比之下我们可以发现c++中的构造函数并不需要显式的调用

带参构造函数

cpp 复制代码
#include <iostream>
using namespace std;

//创建一个类Date

class Date
{
public:
	// 1.无参构造函数

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

	//2.带参的构造函数

	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;
	d1.print();*/

	Date d2(2024, 8, 8);
	d2.print();

	return 0;
}

相比于无参的构造函数,带参的构造函数能够在初始化的时候手动设置初始化的数据,具有更高的自由度。

全缺省构造函数

cpp 复制代码
#include <iostream>
using namespace std;

//创建一个类Date

class Date
{
public:
	// 1.无参构造函数

	//Date()
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}

	//2.带参的构造函数

	/*Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

	//3.全缺省的构造函数
	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;
	d1.print();*/

	/*Date d2(2024, 8, 8);
	d2.print();*/
	Date d3(2024);
	d3.print();

	return 0;
}

需要注意,全缺省构造函数和无参构造函数虽然构成函数重载,但是在不传入参数时会产生调用歧义,所以不能同时存在。全缺省和带参的构造函数只是默认参数的差别,不构成函数重载。

析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对 象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

  1. 析构函数名是在类名前加上字符~。

  2. 无参数无返回值。(这里跟构造类似,也不需要加void)

  3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。

  4. 对象生命周期结束时,系统会自动调用析构函数。

cpp 复制代码
#include<iostream>
using namespace std;
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;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列

class MyQueue
{
public:
	//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack str1;
	return 0;
}
  1. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
cpp 复制代码
#include<iostream>
using namespace std;
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;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列

class MyQueue
{
public:
	//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}
  1. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类 型成员无论什么情况都会自动调用析构函数。

  2. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。

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

拷贝构造函数

如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数 也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

拷贝构造的特点:

  1. 拷贝构造函数是构造函数的⼀个重载。

  2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语 法逻辑上会引发无穷递归调用。

  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成。

  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

  5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷贝,所以不需要我们显⽰实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器⾃动⽣成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要 我们⾃⼰实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是⾃定义类型 Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显⽰实现 MyQueue的拷贝构造。这里还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显示写拷贝构造,否则就不需要。

  6. 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没 有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。

接下来我们根据上述特点来自己实现一下拷贝构造函数:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include <iostream>

using namespace std;
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;

};

void Func1(Date d)
{
	cout << &d << endl;
}

int main()
{
	Date d1(2024, 8, 11);
	Date d2(d1);
	d1.Print();
	d2.Print();

	return 0;
}

执行结果:

拷贝构造函数是构造函数的重载,可以通过已经存在的对象来初始化新开的对象。

和构造函数一样,如果没有显示定义拷贝构造函数,编译器会自动生成,与默认生成的构造函数不同的是,自动生成的拷贝构造函数会对内置类型进行处理,会对内置类型进行浅拷贝/值拷贝。

cpp 复制代码
#include <iostream>

using namespace std;
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;

};

void Func1(Date d)
{
	cout << &d << endl;
}

int main()
{
	Date d1(2024, 8, 11);
	Date d2(d1);
	d1.Print();
	d2.Print();

	return 0;
}

但是默认生成的拷贝构造函数只能进行值拷贝

cpp 复制代码
#include<iostream>
using namespace std;
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;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
class MyQueue
{
public:

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack s1;
	Stack s2(s1);

	return 0;
}

用编译器自动生成的拷贝构造函数初始化栈,s1和s2的地址也是完全一样的。析构的时候对同一块地址会析构两次,在第二次析构时,要通过this指针访问成员变量,但是这个时候this指针已经不能指向成员变量了(内存被释放了),会报错。

所以当对象指向资源时,我们要自己写拷贝构造函数 。

cpp 复制代码
#include<iostream>
using namespace std;
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& s)
	{
		_a = (STDataType*)malloc(sizeof(s._a) * s._capacity);//先开空间
		if (_a = nullptr)
		{
			perror("malloc fail");
			return;
		}
		memcpy(_a, s._a, sizeof(STDataType) * s._top);
		_capacity = s._capacity;
		_top = s._top;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
class MyQueue
{
public:

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack s1;
	Stack s2(s1);

	return 0;
}

这样就不会出现地址相同,析构两次的错误。

同样的,自定义类型的变量会自动调用它的析构函数。

cpp 复制代码
#include<iostream>
using namespace std;
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& s)
	{
		_a = (STDataType*)malloc(sizeof(s._a) * s._capacity);//先开空间
		if (_a = nullptr)
		{
			perror("malloc fail");
			return;
		}
		memcpy(_a, s._a, sizeof(STDataType) * s._top);
		_capacity = s._capacity;
		_top = s._top;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
class MyQueue
{
public:

private:
	Stack pushst;
	Stack popst;
};
int main()
{
	/*Stack s1;
	Stack s2(s1);*/
	MyQueue m1;
	MyQueue m2(m1);
	return 0;
}

赋值运算符重载

运算符重载

• 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

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

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

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

cpp 复制代码
//运算符重载
#include <iostream>
using namespace std;
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 ==(Date x1)
	{
		return _year == x1._year
			&& _month == x1._month 
			&& _day == x1._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2049);
	Date d2(2052);
	d1.operator== (d2);
	return 0;
}

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

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

• .* :: sizeof ?: . 注意以上5个运算符不能重载。(选择题里面常考,大家要记⼀下)介绍一下.*

cpp 复制代码
#include <iostream>

class A {
public:
    void foo() { std::cout << "foo" << std::endl; }
};

typedef void(A::*PF)(); // 定义了一个指向A类成员函数的指针类型,指向无参数且返回void的函数

int main() {
    A a;                // 创建类A的对象
    PF p = &A::foo;     // p是指向A::foo的成员函数指针,c++规定要加&才能取到函数指针

    (a.*p)();           // 调用a对象的foo成员函数,等同于a.foo()
    
    return 0;
}

• 重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: operator+(int x, int y) int

• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意 义,但是重载operator+就没有意义。

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

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

赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象的拷贝赋值。注意与拷贝构造函数区分,赋值运算符重载是用于两个已存在的对象之间的拷贝赋值;拷贝构造函数是用已经存在的对象来初始化新的对象。

赋值运算符重载的特点:

  1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝。

  2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。

  3. 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型 成员变量会调用他的赋值重载。 (这里和拷贝构造函数是相似的,如果显示实现了析构函数,对象指向了资源就需要自己手动实现赋值运算符重载)

  4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就 可以完成需要的拷贝,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是 内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我 们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部 主要是⾃定义类型Stack成员,编译器自动生成的赋值运算符重载会调⽤Stack的赋值运算符重载, 也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现 了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

接下来我们来实现一下:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//赋值运算符重载
	void operator = (const Date& d)//可以传参,用const传引用不用调用拷贝构造,节省时间
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	//拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << _month << _day << endl;
	}
	bool operator ==(Date x1)
	{
		return _year == x1._year
			&& _month == x1._month
			&& _day == x1._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2049);
	Date d2(2052);
    

	//赋值重载拷贝
	d2 = d1;
	//拷贝构造函数
	Date d3 = d2;
	Date d4(d2);

	return 0;
}

但是这样实现的函数没有返回值,不能够实现连续赋值。为了能够实现连续赋值,我们可以这样写:

cpp 复制代码
#include <iostream>
using namespace std;
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;
	}

	//拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << _month << _day << endl;
	}
	bool operator ==(Date x1)
	{
		return _year == x1._year
			&& _month == x1._month
			&& _day == x1._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2049);
	Date d2(2052);


	//赋值重载拷贝
	d2 = d1;
	//拷贝构造函数
	Date d3 = d2;
	Date d4(d2);

	d1 = d2 = d3 = d4;

	return 0;
}

在这个函数执行完之后,this指针不会销毁。直接返回会形成一个拷贝,所以我们可以返回引用,用来提高效率。

相关推荐
DARLING Zero two♡17 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
7年老菜鸡18 分钟前
策略模式(C++)三分钟读懂
c++·qt·策略模式
Gu Gu Study19 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
Ni-Guvara27 分钟前
函数对象笔记
c++·算法
似霰31 分钟前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
芊寻(嵌入式)41 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭43 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风1 小时前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵1 小时前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript