C++类和对象

Posted 小倪同学 -_-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象相关的知识,希望对你有一定的参考价值。

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情
况下,都会自动生成下面6个默认成员函数。

2.构造函数

2.1 构造函数的概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

如下面代码中成员函数Date就是构造函数,当调用该对象时,编译器会默认按构造函数对创建的变量进行初始化

class Date
{
public:
	Date(int year = 2021, int mouth = 10, int day = 12)// 构造函数
	{
		_year = year;
		_mouth = mouth;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
}

2.2 构造函数的特性

  1. 函数名与类名相同
  2. 构造函数无返回值
    这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void。
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数支持重载
    可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数。
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;
};
void TestDate()
{
	Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数

	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	Date d3(); // 声明了d3函数,该函数无参,返回一个日期类型的对象
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成(注意:这里的初始化为随机值)


6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数

d1对象调用了编译器自动生成的构造函数后,d1对象的_year/_month/_day依旧是随机值,那这编译器自动生成的构造函数还有什么意义?

编译器自动生成的构造函数机制:
 1、编译器自动生成的构造函数对内置类型不做处理。
 2、对于自定义类型,编译器会再去调用它们自己的默认构造函数

虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数。

析构函数

析构函数的概念

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

析构函数的特性

  1. 析构函数名是在类名前加上字符 ~
class Date
{
public:
	Date()// 构造函数
	{}
	~Date()// 析构函数
	{}
private:
	int _year;
	int _month;
	int _day;
};
  1. 析构函数无参数无返回值
  2. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  3. 对象生命周期结束时,C++编译系统系统自动调用析构函数
    虽然系统会自动生成析构函数释放空间,但是在堆上开辟的空间并不能得到释放,这时就需要我们自己创建析构函数了
typedef int DataType;
class SeqList
{
public:
	SeqList(int capacity = 10)
	{
		_pData = (DataType*)malloc(capacity * sizeof(DataType));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	~SeqList()
	{
		if (_pData)
		{
			free(_pData); // 释放堆上的空间
			_pData = NULL; // 将指针置为空
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _pData;
	size_t _size;
	size_t _capacity;
};

拷贝构造函数

拷贝构造函数的概念

构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

class Date
{
public:
	Date(int year = 2000, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2021, 10, 13);
	Date d2(d1); // 用d1创建d2
	return 0;
}

拷贝构造函数的特性

  1. 拷贝构造函数是构造函数的一个重载形式
    因为拷贝构造函数的函数名也与类名相同。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
  3. 若未显示定义,系统生成默认的拷贝构造函数
    默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	// 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。
	Date d2(d1);
	return 0;
}
  1. 编译器自动生成的拷贝构造函数不能实现深拷贝
    对于Date这样的类,我们无需自己实现拷贝构造函数只用默认的拷贝构造函数就能够实现拷贝目的,但是对于栈(Stack)类,编译器自动生成的拷贝构造函数不能满足我们的要求。
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = NULL;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack s1(8);
	Stack s2(s1);
	return 0;
}

上述代码拷贝工作是可以完成的,但是在代码结尾处程序会崩溃


这是为什么呢?

当程序运行周期结束后,系统会自动调用析构函数清理程序,s2是后入栈的,所以会先清理,s2._a指向的空间被释放,s2清理完成后析构函数接着清理s1,将已释放的s1._a空间再一次释放,此时程序崩溃。

赋值运算符重载

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数

例如比较两个日期

Date d1(2021, 10, 10);
Date d2(2020, 10, 13);
d1 == d2;		// 可读性高,书写简单
IsSame(d1, d2);	// 可读性差,书写麻烦

函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)

bool operator==(const Date& x)

注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
    操作符有一个默认的形参this,限定为第一个形参
  5. .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载

例:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// d1 == d2; -> d1.operator==(d2); -> d1.operator==(&d1, d2);
	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this指向的调用函数的对象
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1(2020, 10, 13);
	Date d2(2021, 10, 13);
	cout << (d1 == d2) << endl;
}

赋值运算符重载

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)// 构造函数 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date& operator=(const Date& d)// 赋值函数
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

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

赋值运算符有以下注意点

一,参数类型
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数
使用传值传参,会额外调用一次拷贝构造函数,所以函数的参数最好使用引用传参
如果不对参数修改,最好用const修饰
二,返回值
我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,返回值是赋值运算符的左操作数,即this指针指向的对象。
为了避免不必要的拷贝,我们还是使用引用返回,因为出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回。
三,检测是否自己给自己赋值
d1=d1这种自己给自己赋值的没有多大意义,我们可以提前检查一下,避免不必要的操作
四,返回*this
赋值操作结束时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this
五, 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,可以用于日期等浅拷贝。

区别以下代码所调用的函数:

	Date d1(2021, 6, 1);
	Date d2(d1);
	Date d3 = d1;
	d4=d1;

第一个就是简单的构造函数
第二个是拷贝构造函数,创建d2,并将d2的值初始化为d1
第三个是赋值构造函数,创建d3并将d1赋值给d3
第四个是赋值函数

const成员

const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

例,当我们使用打印函数时为了避免修改参数,我们常调用const函数

void Print()const// const修饰的打印函数,相当于void Print(const Date* this)
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗

不可以

非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。

  1. 非const对象可以调用const成员函数吗

可以

const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功

  1. const成员函数内可以调用其它的非const成员函数吗

不可以

在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败

  1. 非const成员函数内可以调用其它的const成员函数吗

可以

在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day; 
};
int main()
{
	Date d1;
	cout << &d1 << endl;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比
如想让别人获取到指定的内容

以上是关于C++类和对象的主要内容,如果未能解决你的问题,请参考以下文章

C++类和对象--继承

C++类和对象1

成功创建c ++类和对象[重复]

C++从青铜到王者第二篇:C++类和对象(上篇)

C++类和对象的简单应用举例

C++初阶类和对象