初识C++ - 类与对象(中篇·上半)

目录 

类的6个默认成员函数

构造函数 

概念

特性

析构函数 

概念

特性

拷贝构造

概念 

特征

扫描二维码关注公众号,回复: 15164499 查看本文章

运算符重载

 概念

未完持续…… 


注意本篇文章代码默认并没有加入以下内容

#include <iostream>

using  std::endl;
using  std::cout;

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下 6 个默认成员
函数。
默认成员函数: 用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

以下就是一个空类, 

class Date {};

构造函数 

概念

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

特性

        构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。

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

	//构造函数是可以写多个的
	//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(2022, 10, 8);     // 调用带参的构造函数
	Date d2(2022, 10, 9);
	Date d3;	//不可以加 ()  // 调用无参构造函数


	//无参的不要像如下这样子写
	//Date d4();	//这样子在VS里面会报警告,但是在别的编译器中可能会直接报错
	//Date func();	//上面这样子写就类似于函数的声明了,会出错的

	d1.Print();
	d2.Print();
	d3.Print();

	return 0;
}

构造函数的使用

        在使用的时候编译器会自动调用构造函数,用以做某些工作,这样子可以避免很多因为忘记而导致的出错,比如忘记 Inint(初始化) 、忘记 destroy(释放内存) 

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会
生成默认的构造函数。但是看起来默认构造函数又没什么用? d 对象调用了编译器生成的默
认构造函数,但是 d 对象 _year/_month/_day ,依旧是随机值。也就说在这里 编译器生成的
默认构造函数并没有什么用??
解答: C++ 把类型分成 内置类型 ( 基本类型 ) 自定义类型 。内置类型就是语言提供的数据类
型,如: int/char... ,自定义类型就是我们使用 class/struct/union 等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 调用的它的默认构造成员
函数。

内置类型:int / char / double ... 指针(自定义类型的指针也算)

自定义类型:class / struct ... Stack / queue / Person 

内置类型不处理,自定义类型会调用它的默认构造函数

分析下面的代码

class A
{
public:
	A()
	{
		_a = 0;
		cout << "A()构造函数" << endl;
	}
private:
	int _a;
};

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;
	}

private:
	// 内置类型
	int _year;     // 年
	int _month;    // 月
	int _day;      // 日

	// 自定义类型
	A _aa;
};

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

	return 0 ;
}

        通过上面的代码发现内置类型是会调用默认构造函数,但是并不会做处理(初始化),因为自定义类型会调用构造函数

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()    //这里是一个析构函数, 函数名与类名一致,但是前面需要加上 ~(C语言中的取反)
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	void Push(int x)
	{
		// ....
		// 扩容
		_a[_top++] = x;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	void push(int x)
	{
		_pushST.Push(x);
	}

	Stack _pushST;    //自动调用了 Stack 里面的构造函数
	Stack _popST;

	//size_t _size;
};

int main()
{

	MyQueue mq;
	mq.push(1);
	mq.push(2);

	return 0;
}

        当有默认构造函数并且满足需求的时候就不需要再写一个函数了,可以直接调用(下面再介绍析构函数)

     

基于以上种种不方便,后面在C++11中又对其进行了优化,打了个补丁,可以直接给默认值(缺省值)

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。

那么有这个补丁的话上面的代码可以如下这样写

        注意这里都是缺省值,不是初始化,它并没有为其开辟空间,上述将其描述为蓝图,空有方法,但是并没有实体。

        同样的符合缺省值的特性,在没有给定值的时候,会直接使用缺省值进行初始化,而当有值的时候就不会使用缺省值了,如下就不会被初始化为 0,而全部初始化为 1

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

析构函数 

概念

 通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
         析构函数: 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会 自动 调用析构函数,完成对象中资源的清理工作

特性

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
     注意:析构函数不能重载
4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数
如下是一个析构函数的使用
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		_array[_size] = data;
		_size++;
	}

	~Stack()    //析构函数,在对象生命周期结束时自动调用
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity = 4;
	int _size = 0;
};

void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}


int main()
{
	TestStack();

	return 0;
}

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器
生成的默认析构函数,对自定类型成员调用它的析构函数。

//析构函数 补
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

上面的输出如下

在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
        因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
        但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
        注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

例题案例(出至题目 232. 用栈实现队列 - 力扣(LeetCode))

    当然有时候有些是不需要写析构函数的,因为编译器会在没有析构函数的时候自动生成,比如上面的日期类就没必要写,一切看需要使用. 

拷贝构造

概念 

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

        构造是初始化的意思,那么拷贝构造就是拷贝初始化

        有些地方叫做复制构造函数

特征

拷贝构造函数也是特殊的成员函数,其 特征 如下: 
1. 拷贝构造函数 是构造函数的一个重载形式
2. 拷贝构造函数的 参数只有一个 必须是类类型对象的引用 ,使用 传值方式编译器直接报错, 因为会引发无穷递归调用。

        内置类型(编译器提供的,编译器对内置类型了如指掌)所以没有拷贝构造函数,但是自定义类型(比较复制)就需要一个拷贝构造函数来帮助完成

如下代码

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

	Date(const Date& d)	//使用 &(无限循环访问构造函数) 并且要加上 const (防止写反,导致被反向赋值)
	{
		cout << "Date 拷贝构造" << endl;

		_year = d._year;
		_month = d._month;
		_day = d._day;

		// 形参加const,防止写反了,下面问题就可以检查出来
		//d._day = _day;
	}

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

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

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

int main()
{
	Date d1(2022, 9, 22); // 构造 - 初始化
	Func1(d1);
	Func2(d1);

	// 拷贝一份d1
	//Date d2(d1); // 拷贝构造 -- 拷贝初始化    会调用拷贝构造函数

	return 0;
}

不加 & 会无限调用构造函数

不加 const  容易出现以下错误

        还有一点需要注意的是,这里的 & 理论上是可以使用指针来替代完成的,但是用起来会使得代码看起来不整洁,更重要的一点是,使用指针完成该操作已经不算是拷贝构造了,应该叫它为构造函数

        

3. 若未显式定义(自己写),编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了 ,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。
     还是得看情况(无论是自定义类型还是内置类型的)
如下情况

浅拷贝
// 这里会发现下面的程序会崩溃掉?这里就需要写一个深拷贝去解决。

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
			_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}


	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size = 0;
	size_t _capacity = 0;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);

	Stack s2(s1);	//浅拷贝

	return 0;
}

        会发现程序崩溃了(析构函数析构同一块空间两次),这里会根据栈的性质,后定义的先析构(后进先出),先析构了 s2,然后又析构了 s1,但是由于编译器自动生成的析构函数是浅拷贝,导致两个指针指向了同一块空间,所以会直接崩溃掉程序

深拷贝

//深拷贝
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 4)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		_array[_size] = data;
		_size++;
	}

	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);	//利用memcpy进行拷贝,利用 st._size(数据的个数)更好点

		_size = st._size;
		_capacity = st._capacity;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size = 0;
	size_t _capacity = 0;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);

	Stack s2(s1);	//深拷贝
	s2.Push(10);

	return 0;
}

监视发现,两者不在指向同一空间,并且两者互不干扰 

总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5. 拷贝构造函数典型调用场景:
1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象

        所以为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。         

运算符重载

下篇会提及赋值运算符重载

 概念

        C++为了 增强代码的可读性 引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
        函数名字为:关键字operator 后面接需要重载的运算符符号
        函数原型:返回值类型  operator 操作符 ( 参数列表 )

注意:

1.不能通过连接其他符号来创建新的操作符:比如 operator@
2.重载操作符必须有一个 类类型参数(不能改变内置类型的运算符)
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐
藏的 this
.*      ::     sizeof     ?:     .    注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。

第一个比较少见,是古老的运算符了,不过仍需要注意一下

如下是一个操作符重载

//运算符的重载

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

	// 我们不写,编译器生成的默认拷贝构造函数
//private:
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

int main()
{
	Date d1(2022, 9, 22);
	Date d2(2023, 1, 1);

	//d1 > d2;
	cout << (d1 == d2) << endl; // 转换成operator==(d1, d2);

	d1 == d2;
	// 也可以显示调用,不过一般不这样 ,上面的与下面的写法是一样的,相同的意思
	operator==(d1, d2);

	return 0;
}

不过还是不好,为了使用操作符重载而让私有变量转化为共有的是不好的行为。

为此我们需要把函数放入类里面,不过我们需要进行一点修改,因为有 this 指针的存在

假如直接放进去不加以修正的话,编译器会报下面的错误 

修改过后如下

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

	bool operator==(const Date& d)
	{
		return _year == d._year		//_year 前面默认有一个 this指针,下面同理 
			&& _month == d._month
			&& _day == d._day;
	}

	// 我们不写,编译器生成的默认拷贝构造函数
private:    //可以被类里面的成员函数访问了
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 9, 22);
	Date d2(2023, 1, 1);

	cout << (d1 == d2) << endl; // 编译器会转换成 d1.operator == (d2),所以在使用的时候要注意运算符的优先级(这里用括号括起来)

	d1 == d2;
	// 也可以显示调用,不过一般不这样写 ,上面的与下面的写法是一样的,相同的意思
	cout << (d1.operator == (d2)) << endl; // 

	return 0;
}

    综上所述一定不能忘记 this指针,可以说任何的成员函数(包括构造函数)都含有隐藏的this指针,除了后面的 静态成员函数

有点太长了,于是就分篇写了

未完持续…… 

猜你喜欢

转载自blog.csdn.net/weixin_67595436/article/details/127210298
今日推荐