类和对象(下)

类和对象(下)

一、默认成员函数

定义:默认成员函数是指不需要显示实现,编译器自动生成的函数。
分类:一共有六种默认成员函数。重点的是四种:构造函数、析构函数、拷贝构造、赋值重载。取地址重载了解即可。在C++11之后还会有两种默认成员函数:移动构造和移动赋值。

二、构造函数

  • 构造函数不是开辟空间,而是初始化对象。
    构造函数的特点:
  • 函数名与类名相同。
  • 函数无返回值。
  • 构造函数如果没有显示实现,编译器会自动生成构造函数。
  • 编译器生成的构造函数对于内置类型来说,构造函数不会对其进行初始化,所以只不确定。对于自定义类型,会调用自定义类型的构造函数,如果没有构造函数,那么就会编译报错。
  • 默认构造函数:无参构造函数,全缺省构造函数,编译器自动生成的构造函数。三者在类中有且只能有一个,不能同时出现。
  • 如果是无参构造函数或者是全缺省构造函数,初始化对象后面不需要带(),否则不知道到底是函数还是构造。
cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1991;
		_month = 5;
		_day = 6;
	}
	//全缺省构造函数
	//Date(int year = 1992, int month = 5, int day = 6)
	//{
		//_year = year;
		//_month = month;
		//_day = day;
	//}
	//普通的构造函数
	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(1955, 6, 9);
	d2.print();
	return 0;
}

三、析构函数

析构函数的任务是:对类内申请的资源(在堆区进行申请空间)进行清理释放。

析构函数的特点:

  • 函数名是在类名前加" ~ "
  • 函数无返回值。
  • 析构函数如果没有显示实现,编译器会自动生成析构函数。
  • 编译器生成的析构函数对于内置类型不做任何处理;对于自定义类型来说,会自动调用自定义类型的析构函数,如果没有,编译报错,会出现内存泄露的问题。
cpp 复制代码
#include <iostream>
#include <assert.h>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
		}
		size = 0;
		capacity = n;
		cout << "Stack(int n=4)" << endl;
	}
	void push(int x)
	{
		cout << "push(int x)" << endl;
		if (size == capacity)
		{
			capacity = 2 * capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * capacity);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(1);
			}
			_a = tmp;
		}
		_a[size++] = x;
	}
	void pop()
	{
		cout << "pop()" << endl;
		assert(this);
		assert(size);
		size--;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		size = capacity = 0;
	}
private:
	
	int* _a;
	int size;
	int capacity;
};
int main()
{
	Stack st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);

	st1.pop();
	
}
  • 当对象要销毁时,先调用析构函数,再销毁对象。
  • 多个对象出现时,析构顺序遵循:先构造后析构。

四、拷贝构造

  • 拷贝构造是使用相同类型的类对新构造的对象进行初始化。所以说拷贝构造是特殊的构造函数。
  • 拷贝构造函数的第一参数必须是相同类类型,可以有其他参数,但是其他参数必须是缺省参数。
  • 拷贝构造的过程:如果拷贝构造函数的参数是类类型,因为是传值方式,所以要产生临时拷贝,用临时拷贝来拷贝构造函数第一个参数,又因为是传值,所以又产生临时拷贝,又会出现相同的场景,所以会造成无穷递归 。如果是类引用,那么拷贝构造函数参数引用的对象就是实参,所以就不会产生无穷递归,同时也能减少拷贝(这就是为啥引用在传值传参时能够减少拷贝)。
  • 如果拷贝构造函数的参数中的类不发生改变,建议使用const 引用。
  • 拷贝构造函数传值返回会产生临时对象,引用则会减少拷贝。如果是返回引用,并且引用对象的作用域仅在拷贝构造函数内部,那么返回后会销毁栈帧,进而造成野引用;如果引用对象作用域是出了拷贝构造函数还存在,那么可以返回引用。
  • 编译器自动生成的拷贝构造对于内置类型,会一个一个字节地进行拷贝,这就是浅拷贝/值拷贝。而像Stack中_a也是内置类型,但_a申请了资源,如果仍是浅拷贝/值拷贝,就会出现指向同一块内存空间,析构时就会释放资源两次,出现错误。因此对于有资源申请的,我们需要显示实现,也就是深拷贝。对于自定义类型,会自动调用自定义类型的拷贝构造。
  • 技巧:如果显示实现析构函数,那么就要显示实现拷贝构造函数。
cpp 复制代码
//浅拷贝
class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1991;
		_month = 5;
		_day = 6;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._month;
		cout << "拷贝构造" << endl;
	}
	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.print();

	Date d2(d1);
	d2.print();
	return 0;
}
cpp 复制代码
//深拷贝
class Stack
{
public:
	Stack(int n = 4)
	{

		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
		}
		size = 0;
		capacity = n;
		cout << "Stack(int n=4)" << endl;
	}
	void push(int x)
	{
		cout << "push(int x)" << endl;
		if (size == capacity)
		{
			capacity = 2 * capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * capacity);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(1);
			}
			_a = tmp;
		}
		_a[size++] = x;
	}
	void pop()
	{
		cout << "pop()" << endl;
		assert(this);
		assert(size);
		size--;
	}
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st.capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(1);
		}
		memcpy(_a, st._a, st.size);
		size = st.size;
		capacity = st.capacity;
		cout << "拷贝构造" << endl;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		size = capacity = 0;
	}
private:

	int* _a;
	int size;
	int capacity;
};
int main()
{
	Stack st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);

	Stack st2(st1);

	return 0;
}
  • 拷贝构造的写法有两种:
cpp 复制代码
Stack st1;
//第一种
Stack st2(st1);
//第二种
Stack st3 = st1;

五、赋值运算符重载

5.1 运算符重载

我们现在所熟知的运算符能够帮我们解决一些问题,但是有些问题并不能很好的解决,像日期,运算符并不能很好地实现日期 -日期,这就需要我们自己来实现这样的功能。

复制代码
格式:
     数据类型 operator 运算符(参数)
  • 如果重载运算符函数定义在全局区域,需要使用成员变量时因其为私有不能访问,有以下方法:
  • 将成员变量变为私有
  • 变成友元函数(之后会说)
  • 将运算符重载函数变成成员函数。
  • 写一个成员函数来获得成员变量。
  • 运算符重载函数的参数个数与运算符作用对象个数相同。比如:一元运算符,参数为1个;二元运算符的参数为2个,左操作数为第一个参数,右操作数为第二个参数。
  • 运算符重载函数成为成员函数后,第一个参数隐式传给this指针,因此参数个数会比运算符作用对象少一个。
  • .*::?:.这些运算符是不能重载的。
  • 重载运算符至少有一个为类类型的参数,不能通过运算符重载改变内置类型运算符的本身含义。
  • 前置++与后置++区分。前置++:operator++(),后置++operator++(int)。通过参数中是否有整形参数来区分前置++和后置++。
  • 重载<< 和 >>时,应该重载为全局函数。因为我们希望的是cout << 对象,但是如果运算符重载函数是成员函数,那么第一个参数隐式传递this指针,因此现实因该为对象 << cout ,这样不符合习惯使用,所以将它重载为全局函数,参数就可以按照顺序传参,cout的类类型为ostream。
cpp 复制代码
class Date
{
public:
	//无参构造函数
	Date()
	{
		_year = 1991;
		_month = 5;
		_day = 6;
	}
	//拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._month;
		
	}
	void func()
	{
		cout << "func" << endl;
	}
	//重载为成员函数
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	//函数后面加const后面再学
	/*int Get_year()const
	{
		return _year;
	}
	int Get_day()const
	{
		return _day;
	}int Get_month()const
	{
		return _month;
	}*/
	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	//改为共有,不推荐
private:
	int _year;
	int _month;
	int _day;
};
class A
{
public:
	A()
	{
		_x = 1;
		_y = 2;
	}
	A(const A& a)
	{
		_x = a._x;
		_y = a._y;
	}
	//前置++
	A& operator++()
	{
		_x += 1;
		_y += 1;
		return *this;
	}
	//后置++
	A operator++(int)
	{
		A tmp = *this;
		_x += 1;
		_y += 1;
		return tmp;
	}
	void print()
	{
		cout << _x << " " << _y << endl;
	}
private:
	int _x;
	int _y;
};
////重载为全局函数
//bool operator==(const Date& d1, const Date& d2)
//{
//	return d1.Get_year() == d2.Get_year()
//		&& d1.Get_month() == d2.Get_month()
//		&& d1.Get_day() == d2.Get_day();
//}
//定义成员函数指针为PF
typedef void (Date::* PF)();
int main()
{
	Date d1;
	//Date d2;

	//两种写法
	//d1 == d2
	//d1.operator==(d2);

	//PF pf = nullptr;
	//C++规定必须加上&才能取到函数指针的地址
	//pf = &Date::func;
	//调用成员函数
	//(d1.*pf)();
	//前置++和后置++
	A a;
	//A ret1 = a++;
	//编译器会自动转为下面这行代码
	A ret1 = a.operator++(0);
	ret1.print();

	A b;
	//A ret2 = ++b;
	//编译器会自动转为下面这行代码
	A ret2 = b.operator++();
	ret2.print();
	return 0;
}

5.2 赋值运算符重载

赋值运算符重载是一个默认成员函数。赋值运算符重载的前提是左右操作对象都是已经建好的对象,二者进行拷贝赋值。一定要与拷贝构造区分开,拷贝构造是用一个已经创建好的的对象来初始化另一个新的对象。

  • 赋值运算符重载必须是成员函数。参数建议是const引用类类型,减少传值传参拷贝。
  • 赋值运算符重载有返回值,返回值建议是类类型引用,这样会减少拷贝,提高效率。
  • 如果没有显示实现,编译器会自动生成一个。对于内置类型,和拷贝构造类似,完成值拷贝/浅拷贝。对于自定义类型,会自动调用他们自己的赋值运算符重载。对于申请资源的对象,仍然是要使用深拷贝。
  • 技巧:如果析构函数显示实现,那么赋值运算符重载也要显示实现。
cpp 复制代码
class Date
{
public:
	//普通的构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._month;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2007, 2, 19);
	cout << "d1: ";
	d1.print();
	Date d2(2026, 8,2);
	cout << "d2: ";
	d2.print();

	//两种写法
	//d2.operator=(d1);
	d2 = d1;
	cout << "赋值之后d2: ";
	d2.print();
	return 0;
}

六、取地址运算符重载

const成员函数

  • 由const修饰的成员函数称为const成员函数。
  • const加在成员函数参数列表的后面。
  • const实际是修饰的*this,这样成员变量的值就不能被修改。
cpp 复制代码
class Date
{
public:
	Date()
	{
		_year = 1995;
		_month = 5;
		_day = 30;
	}
	void print()const
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.print();
	return 0;
}

取地址运算符重载

  • 取地址运算符重载也是默认成员函数。
  • 取地址运算符重载有两种:普通取地址运算符重载和const运算符重载。
  • 编译器自动生成的已经能够满足我们的需求,所以不需要显示实现。
cpp 复制代码
class Date
{
public:
	//普通运算符重载
	/*Date* operator&()
	{
		return this;
	}*/
	//const运算符重载
	const Date* operator&()
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

七、初始化列表

  • 构造函数的一种。初始化列表是每个成员变量初始化定义的地方。
  • 结构:冒号开头,成员变量(表达式/初始值),逗号分隔。
  • 初始化列表中的成员变量只能初始化一次。
  • const修饰的成员变量、引用成员变量、没有默认构造的类类型的成员变量都必须在初始化列表中进行初始化。
  • 每个成员变量不管是否在初始化列表初始化,都要走一次初始化列表。
  • 初始化列表中初始化顺序:按照成员变量声明顺序进行初始化。
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		_a = 2;
		_b = 3;
	}
private:
	int _a;
	int _b;
};
class Date
{
public:
	//按照成员变量声明顺序进行初始化
	Date(int year = 2005, int month = 5, int day = 30)
		//初始化列表
		:_year(year)
		, _month(month)
		, _day(day)
		//表达式或初始值
		,ptr2((int*)malloc(sizeof(int)*3))
	{
		if (ptr1 == nullptr || ptr2 == nullptr)
		{
			perror("malloc fail");
			return;
		}
	}
private:
	//成员变量的声明
	//给缺省值
	int _year=2004;
	int _day=24;
	int _month=3;
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2;
	A a;
};
int main()
{
	Date d1(1995, 5, 14);
	Date d2;
	return 0;
}

八、类型转换

  • C++支持内置类型隐式类型转化,但需要相关内置类型的构造函数。
  • 如果不想支持类型转化,只需要再构造函数名前加上explicit
  • 类类型的对象之间也支持类型转化,但要有相关类型的构造函数。
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	//explicit A(int a)
	A(int a)
	{
		_a = a;
	}
	//explicit A(int a,int b)
	A(int a, int b)
	{
		_a = a;
		_b = b;
	}
	void print()
	{
		cout << _a << " " << _b << endl;
	}
	int Get_a()const
	{
		return _a;
	}
	int Get_b()const
	{
		return _b;
	}
private:
	int _a;
	int _b;
};
class B
{
public:
	B(const A& a)
	{
		_c = a.Get_a();
		_d = a.Get_b();
	}
	void print()
	{
		cout << _c << " " << _d << endl;
	}
private:
	int _c;
	int _d;
};
int main()
{
	//在没有优化之前,1先构造一个A的临时对象,然后再拷贝构造给a1
	A a1 = 1;
	const A& a2 = 1;
	a1.print();

	//C++11之后支持多参数转化
	A a3 = { 2,3 };
	const A& a4 = { 2,3 };
	a3.print();

	//类类型的对象之间的转化
	B b1 = a3;
	const B& b2 = a3;
	b1.print();
	return 0;
}

九、static成员

  • 由static修饰的成员是 static成员。static成员有静态成员函数和静态成员变量。
  • 静态成员变量为同一个类多个对象所共有,静态成员变量存储在堆区,并不存储在对象的内存空间中。
  • 静态成员变量类内声明,类外定义,因此不走初始化列表,不能在声明地方给缺省值。
  • 静态成员函数没有this指针,所以不能访问非静态成员变量;普通成员函数既可以访问静态成员变量,也可以访问分静态成员变量。
  • 静态成员也是类的成员,也受到访问限定符的限制。
cpp 复制代码
class A
{
public:
	A(int a = 6)
	{
		_a = a;
	}

	void print()
	{
		cout << _a << endl;
	}
	static int GetCount()
	{
		return _b;
	}
private:
	//类内声明
	static int _b;
	int _a;
};
//类外初始化
int A::_b = 10;
int main()
{
	A a;
	a.print();

	cout << A::GetCount() << endl;
	return 0;
}

十、友元

  • 解决的问题:在类外面的函数想要访问类内的私有成员。
  • 分类:有友元类和友元函数。
  • 结构:在你想要访问的类中公共区域声明一下:friend class 类名或者friend 函数
  • 关系:

1.一个函数可以是多个类的友元函数。
2.友元之间的关系是单向性、无传递关系。

cpp 复制代码
class A
{
public:
	//友元类的声明
	friend class B;
	//友元函数声明
	friend void FuncA(const A& a);
	A(int a = 6)
	{
		_a = a;
	}

	void print()
	{
		cout << _a << endl;
	}

private:
	int _a;
};
class B
{
public:
	B()
	{
		_b = 5;
	}
	void FuncB(const A& a)
	{
		cout << "FuncB()" << endl;
		cout << "_a= " << a._a << endl << "_b= " << _b << endl;
	}
private:
	int _b;
};

void FuncA(const A& a)
{
	cout << "FuncA()" << a._a << endl;
}
int main()
{
	A a;
	//FuncA(a);
	//a.print();
	B b;
	b.FuncB(a);
	return 0;
}

十一、内部类

  • 内部类是在一个类中又建立了一个类。内部类是独立的类,在其外部类中并不占据空间,仅仅受到类域访问限制和访问限定符的限制。
  • 内部类默认是外部类的友元类。
  • 如果内部类在外部类中是私有的,则在外部不能访问。
cpp 复制代码
class A
{
public:
	A(int a)
	{
		_a = a;
	}
	
	void print()
	{
		cout << _a << " " << endl;
	}
	class B
	{
	public:
		B(const A& a)
		{
			_c = a._a;
		}
		void print()
		{
			cout << _c << " " << endl;
		}
	private:
		int _c;
	};
private:
	int _a;
};
int main()
{
	A a(5);
	a.print();
	A::B b = a;
	b.print();
	return 0;
}

十二、匿名对象

  • 解决问题:对象仅用一次,不用额外创建命名变量,较为方便。
  • 结构: 类名()
  • 匿名就是没有给对象名字,生命周期仅在代码这一行。
cpp 复制代码
class A
{
public:
	A()
		:_a(5)
	{ }
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A().print();

}
相关推荐
皓月斯语1 小时前
P5736 【深基7.例2】质数筛
c++·算法·题解
鸿芯微控科技2 小时前
MFC质量流量控制器流量不准怎么排查?设定值、反馈值与参考流量对比,附Python代码
c++·python·mfc
拂拉氏2 小时前
【知识讲解】 让指针拥有“自我意识”?--C++智能指针相关内容讲解
c++·智能指针
大尚来也2 小时前
用 Python 自动整理文件、重命名、分类电脑文件夹
开发语言·python
CVer儿2 小时前
c++二级指针
开发语言·c++
mlidongfeng3 小时前
[AI][C++26] SIMD 编程模型思考
开发语言·c++·人工智能
梓䈑3 小时前
【用 Vibe Coding 实现的 C++17 在线判题系统】OpenCode 入门指南:从环境搭建到上下文管理
c++·ai编程
j7~3 小时前
【Linux】二十七.线程篇四《Linux多线程编程:线程互斥(互斥量的底层到封装)、线程安全和冲入、死锁》---详解
linux·开发语言·c++·线程安全·死锁·线程互斥·重入
啦啦啦啦啦zzzz3 小时前
贪心算法和动态规划
c++·算法·贪心算法·动态规划