C++类和对象

2024-01-08 03:26:13
C++ 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完
成。

什么是类

C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。 比如:
之前在数据结构初阶中,用 C 语言方式实现的栈,结构体中只能定义变量 ;现在以 C++ 方式实现,
会发现 struct 中也可以定义函数。只不过在C++中,更喜欢用class 替换struct。
class 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面
号不能省略
类体中内容称为 类的成员: 类中的 变量 称为 类的属性 成员变量 ; 类中的 函数 称为 类的方法 或者
成员函数。

访问限定符

C++ 实现封装的方式: 用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选
择性的将其接口提供给外部的用户使用。
访问限定符分为三种:公有:public;私有:private;保护:protected
1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 ( 此处 protected private 是类似的 )
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C) 。
class Date
{
public:
private:
	int _year;
	int _month;
	int _day;
};

上面是一个日期类;

类的实例化

用类类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的 ,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没
有分配实际的内存空间 来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个
类,来描述具体学生信息。
2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
#include<iostream>
using namespace std;
class Date
{
public:
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;//实例化
	return 0;
}

类的6个默认成员函数

一个类会有六个默认成员函数,什么叫默认成员函数?

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

构造函数

构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证
每个数据成员都有 一个合适的初始值,并且 在对象整个生命周期内只调用一次 。相当于初始化行为。
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。
Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

上述写的是一个带参的构造函数,构造函数是可以重载的,我们也可以写成无参形式,也就是我们实例化对象时不需要传参让它具体是几年几日,我们默认都传1过去,即让它是1年1月1日。

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

}

我们看看效果

?

我们也可以给有参构造函数传缺省值,即

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

这样我们构建实例化对象时既可以传参也可以不传参。

我们开始说了构造函数是类的默认成员函数,对于内置类型我们不写是不会初始化的。对于自定义类型不写编译器是自动会生成一个默认的构造函数。

C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语言提供的数据类
型,如: int/char... ,自定义类型就是我们使用 class/struct/union 等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 调用的它的默认成员
函数。
所以对于日期类Date来说,它的成员变量都是内置类型,我们如果不写构造函数的话,我们可以看到创建一个对象是不会被初始化的

?

对于栈satck来说也是不会。

?但是后来C++11有了改变,我们在声明成员函数的时候给出缺省值就可以对自定义类型不写构造函数也可以初始化。

如果我们这个时候 把Stack的构造函数写出来

Stack(int capacity=4)
{
	_a = new int[capacity];
	_top = 0;
	_capacity = capacity;
}

对于用两个栈实现一个队列Myqueue这个类来说,它的两个成员变量是Stack 类型,这也就是自定义类型的,我们光定义出类,不写构造函数,我们创建出一个对象看结果:

?我们可以看到,并没有写Myqueue的构造函数,但是他初始化成功了,原因是他的成员函数是Stack类型,我们给Stack类写出了他的构造函数,对于自定义类型的Myqueue来说,编译器自己给Myqueue生成一个默认的构造函数调用。

析构函数

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载
4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。
析构函数主要完成资源清理工作,这个函数是比较简单的,他的实现就是把类名前加一个~,是无参的,所以析构函数是不能重载的。
Stack的析构函数
~Stack()
{
	delete[] _a;
	_a = nullptr;
	_top = _capacity = 0;
}

程序运行完就变成

?

同样他也是对内置类型需要自己写,自定义类型编译器自动会生成他的默认析构函数

?还是对于Myqueue这类来说可以满足,我们没有写他的构造函数,只给出了Stack类。如图初始化成功

?等到程序运行结束的时候,释放资源

拷贝构造函数?

拷贝构造函数 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存
在的类类型对象创建新对象时由编译器自动调用
1. 拷贝构造函数 是构造函数的一个重载形式。
2. 拷贝构造函数的 参数只有一个 必须是类类型对象的引用 ,使用 传值方式编译器直接报错
因为会引发无穷递归调用。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
拷贝构造函数名也是类名(一种特殊的构造函数),参数的话一般是引用传参,另外为了防止被宝贝的对象的值被改变,一般在参数前面加上const修饰。
拷贝构造对于不需要开空间的1类来说,自己不写编译器是自动会生成一个默认的拷贝构造的,以日期类为例:
并没有写拷贝构造,但是这里还是拷贝成功了。
接下来我们以需要开空间的Stack为例:
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = new int[capacity];
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		delete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}
	void Push(const int& data)
	{
		
		_a[_top] = data;
		_top++;
	}
	
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	Stack st2(st1);
	return 0;
}

我们没有写他的拷贝构造函数,我们在入栈3个数之后用st1拷贝构造一个st2运行,

?

?可以看到直接程序中断了,什么原因呢?

s1对象构造函数创建,在构造函数中,默认申请了4个元素的空间,然后里面存了3个元素1,2,3;s2对象使用s1拷贝构造,而Stack并没有显示定义拷贝构造函数,则编译器会给Stack生成一个默认的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,即将s1中的内容原封不动的拷贝到s2中。因此s1和s2指向了同一块内存空间。当程序运行结束时,s1和s2要销毁,s2先销毁,s2销毁调用析构函数,已经将指向的那一块空间释放了,s1并不知道,再次销毁,再再次释放那一块空间,一块空间多次释放,肯定就会崩溃。

所以这种需要开空间的类我们都需要自己写拷贝构造,否则编译器只会浅拷贝,导致程序崩溃。

Stack(const Stack& st)
{
	_a = new int[st._capacity];
	memcpy(_a, st._a, sizeof(int) * st._top);//把st1中的数据拷贝
	_top = st._top;
	_capacity = st._capacity;
}

我们自己写了拷贝构造之后,再运行程序就可以正常啦。

所以,对于需要开空间的类,切记需要自己去实现拷贝构造函数!!!

赋值运算符重载

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator 后面接需要重载的运算符符号
赋值运算符只能重载成类的成员函数不能重载成全局函数
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
对于日期类。如果我们不去实现赋值运算符重载,编译器也会自动生成一个供我们使用。
但是对于需要开空间的类呢?还是以Stack举例
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = new int[capacity];
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		delete[] _a;
		_a = nullptr;
		_top = _capacity = 0;
	}
	Stack(const Stack& st)
	{
		_a = new int[st._capacity];
		memcpy(_a, st._a, sizeof(int) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(const int& data)
	{
		
		_a[_top] = data;
		_top++;
	}
	
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	Stack st2;
	st2 = st1;
	return 0;
}

我把在st1中插入3个数据1,2,3后创建一个对象st2,再st2=st1;运行我们会发现报错。

?什么原因呢?

1.s1对象调用构造函数创建,在构造函数中,默认申请了4个元素的空间,然后存了3个元素1,2,3

2.s2对象调用构造函数创建,在构造函数中,默认申请了4个元素的空间,没有存储元素

3.由于Stack没有显式实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的赋值运算符重载即只要发现Stack的对象之间相互赋值,就会将一个对象中内容原封不动拷贝到另一个对象中4.s2= s1;当$1给s2赋值时,编译器会将$1中内容原封不动拷贝到s2中,这样会导致两个问题:
(1)s2原来的空间丢失了,存在内存泄漏;(2)s1和s2共享同一份内存空间,最后销毁时会导致同一份内存空间释放两次而引起程序崩溃。

所以和拷贝构造函数一样,赋值运算符重载也需要对开空间申请资源的类自己实现。

Stack operator=(const Stack& st)
{
	_a = new int[st._capacity];
	memcpy(_a, st._a, sizeof(int) * st._top);
	_top = st._top;
	_capacity = st._capacity;
	return *this;
}

接下来我们再运行就可以啦。

其它的运算符我们都需要自己实现,因为赋值运算符这类是每个编译器都自动写好的(除了需要开空间),后面我们需要什么运算符就需要自己去写,以日期类为例,我们如果比较两个日期的大小这个规则是我们自己定义的。下面我随便写几个运算符举例

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

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如 想让别人获取到指定的内容!
class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; 
 int _month ;
 int _day ; 
};

文章来源:https://blog.csdn.net/weixin_67131528/article/details/135441048
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。