详解C++类和对象

 

一.构造函数

1.构造函数的定义:

构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,保证每个数据成员都有 一个合适的初始值,并且 在对象的生命周期内只调用一次 。 其实构造函数的作用就是完成成员变量的初始化 ,但不同于c语言的初始化构造函数可以实在创造对象的同时就完成成员变量的初始化。

2.构造函数的特征:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器 自动调用 对应的构造函数。

4. 构造函数可以重载。

3.构造函数的实现:

构造函数的实现主要有三种,

1.当用户没有实现构造函数的话系统会默认创造一个,此时系统会将内置类型的成员变量赋予随机值,而对于自定义类型的成员变量则会调用他们的构造函数。(注:内置类型一般指的是:int char double float等这些定义好的类型,自定义类型指的是:struct这种类型以及class类这种)。

2.当然用户也可以自己实现构造函数,一种为无参构造

3.类一种为带参构造,但是在带参构造中我们使用全缺省构造。我们用代码展示一下:

3.1.系统默认的构造函数

我们可以看到当我们没有在Data类进行函数构造的时系统将会自己默认创建构造函数,对内置类型变量赋予随机值,自定义类型调用自己的构造函数(若自定义类型也没有定义构造函数那么此例子中的_a1和_a2也会被赋予随机值)

3.2无参构造

3.3 带参构造

这里出一个问题对于代码风格造成的问题:成员变量year最后的结果是多少呢?

class A{public:A(int year){year = year;}private:int year;};int main(){A a(20);}

答案是:随机值。那么为什么是随机值呢?这里主要是变量之间它采用了就近原则,所以等式左边的year会直接寻找离他最近的变量所以会将等式右边的year直接赋值给它自己,所以year最后的值就是随机值。

我们继续来说带参的构造函数,我们一般推荐使用的是全缺省的构造函数(注:

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

 

二 析构函数

构造函数时完成对象的初始化,那么一个对象又是怎么样被销毁的呢?

1.析构函数的定义

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

2.析构函数的特征

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

2. 无参数无返回值。

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

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

这里我们用栈的例子来说明析构函数的实现以及作用。

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()
		{
			// 像Stack这样的类,对象中的资源需要清理工作,就用析构函数
			free(_a);
			_a = nullptr;
			_top = _capacity = 0;
		}
	private:
		int* _a;
		int _top;
		int _capacity;
	};

这里是完成构造函数,有自己定义的析构函数的效果。同构造函数一样对于内置成员变量析构函数会置为随机值,而自定义类型则会去调用他们的析构函数。

 

三 拷贝函数

如果某些时候我们需要去复制一个对象,这样的话我们该怎么样去解决呢?

这里我们就需要引入拷贝函数。那么什么叫做拷贝函数呢?我们应该去怎么实现呢?有什么注意事项呢?这里我们一一来说道。

1.拷贝函数定义

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

2.拷贝函数的特性

1. 拷贝构造函数 是构造函数的一个重载形式 。

2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用 。

3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷 贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

3.拷贝函数的实现

拷贝函数的实现分为两种一种是系统默认,一种是自己定义。我们分别来看其效果

class A 
{
public:
	A() 
	{ 
       _a1 = 1;
		 _a2 = 2;
	}
	~A() 
	{
		cout << "A()" << endl;
	}
private:
	int _a1;
	int _a2;
};
class Data
{
public:
	/*Data() 
	{
		_year = 2021;
		_month = 12;
		_day = 12;
	}*/
	//Data(int year, int month, int day) 
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	Data(int year = 2022, 
		int month = 12, 
		int day = 12) 
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	A a;
};
int main() 
{
	Data s;
	//拷贝函数的调用
	Data s2(s);
	return 0;
}

调用系统默认生成拷贝函数(注:这里拷贝函数的拷贝对自定义类型和内置类型的成员变量处理都是一致的完成字节序的值拷贝)

图1 调用系统默认生成的拷贝函数

图2 调用用户自己定义的拷贝函数

在这里我们顺便说一下在自定义拷贝函数的时候一定要使用引用不然会出现无限递归例如 Data(Data s){}正确的使用是Data (const Data & s){}其中const是为了保护原数据不被轻易改动。

class A 
{
public:
	A() 
	{ 
       _a1 = 1;
		 _a2 = 2;
	}
	~A() 
	{
		cout << "A()" << endl;
	}
private:
	int _a1;
	int _a2;
};
class Data
{
public:
	/*Data() 
	{
		_year = 2021;
		_month = 12;
		_day = 12;
	}*/
	//Data(int year, int month, int day) 
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	Data( const Data &s) 
	{
		_year = s._year;
		_month = s._month;
		_day = s._day;
	}
	Data(int year = 2023, 
		int month = 12, 
		int day = 12) 
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	A a;
};
int main() 
{
	Data s;
	//拷贝函数的调用
	Data s2(s);
	return 0;
}

我们可以发现s2均完整的赋值了s的内容,但是这里真的就没有问题了吗?如果我们使用系统默认生成的拷贝函数成员变量中含有指针那么会出现什么样的问题呢?

class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s;
String s1(s);
}

我们可以看到虽然虽然s1拷贝了s的内容但是最后系统还是抛出了错误那么这个错误来自那里呢?

我们看这幅图

这里就是我们之前说的系统默认生成的拷贝函数是浅拷贝,那么怎么去完成深拷贝我们后边在继续讲解。

 

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程宝库的更多内容!

字符串string str = "this is a string"; for(auto ch : str) cout << ch << endl;等价于for(int i = ...