您现在的位置是:首页 >技术杂谈 >【C++】类和对象(下)网站首页技术杂谈

【C++】类和对象(下)

平行线也会相交 2024-07-01 11:59:58
简介【C++】类和对象(下)

个人主页:平行线也会相交?
欢迎 点赞? 收藏✨ 留言✉ 加关注?本文由 平行线也会相交 原创
收录于专栏【C++之路】?
本专栏旨在记录C++的学习路线,望对大家有所帮助?‍
希望我们一起努力、成长,共同进步。?
在这里插入图片描述

一、再谈构造函数

先来回顾一下什么是构造函数:

在创造对象是,编译器通过调用构造函数,给对象中的各个成员变量进行一个合适的初始化赋值。(说白了就是给对象进行初始化的意思

我们首先要清楚的是一个类对象要进行初始化的话有两种方式,分别是函数体初始化(或者函数体赋值)初始化列表。本文对初始化列表进行重点分析。

1.1初始化列表

初始化列表格式::以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。请看举例:

//函数体类赋值
class Date
{
public:
 	Date(int year, int month, int day)
 	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
 		int _year;
 		int _month;
 		int _day;
};



//初始化列表
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
  • 初始化列表注意点
  1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化
    引用成员变量
    const成员变量
    自定义类型成员(且该类没有默认构造函数时)

第一:先来看引用成员变量和const成员变量
在这里插入图片描述
引用成员变量和const成员变量的一个很大的特征就是必须在定义的时候进行初始化,对于引用而言必须是谁的别名,而对于const成员变量而言只有一次进行初始化的机会(即在const成员变量定义的地方),那什么是它们定义的地方呢?我们先来回顾一下对象实例化:B bb(10, 1);,这里对象bb定义的时候要调用构造函数来完成初始化,B bb(10, 1);其实是对象整体进行定义,那对象的成员在哪里进行定义呢?祖师爷认为初始化列表才是对象的成员定义的地方,而函数体内赋值光听名字就知道这里仅仅只是赋值并不是真正意义上的初始化,我们的祖师爷规定初始化列表才是对象成员进行定义的地方
举个初始化列表的例子:
在这里插入图片描述
我们还应该注意一点:每个成员变量在初始化列表中最多只能出现一次,就比如说(下面是错误示范):
在这里插入图片描述
第二:现在我们来看一下自定义类型的成员变量的初始化:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面这种情况是存在默认构造函数的,编译器会去自动调用来对自定义类型进行初始化。默认构造函数一共分为3种(1.编译器自动生成的。2.无参的。3.全缺省的。),上面这种情况就是全缺省的默认构造。但是如果我们写了一个带参数的构造函数的话编译器就会报错了,请看:
在这里插入图片描述
既然上图中没有默认构造函数的话,我们就必须在初始化列表那里去显式的调用构造函数,请看:
在这里插入图片描述
所以这里如果没有默认构造的话就需要我们在初始化列表那里去显式的去调用构造函数。
但是如果这里有默认构造函数的话我们当然也可以在初始化列表那里去显式的去调用默认构造函数,请看举例:
在这里插入图片描述

现在还记得下面的代码吗,请看:

class MyQueue
{
public:
private:
	Stack _pushst;
	Stack _popst;
};

上述代码中,_pushstpopst会去调用其对应的默认构造函数。
在这里插入图片描述

在这里插入图片描述
还要注意一点:不传参数也可以进行初始化。,如果没有提供默认构造函数的话就必须进行传参来进行初始化。默认构造函数的特点是可以不传参数也可以初始化(加上缺省值)。

在这里插入图片描述

在这里插入图片描述
左图是错误的,不可以引用一块销毁的内存空间。所以,引用并不是绝对安全的加粗样式

请看下面代码:
在这里插入图片描述
这里对于自定义类型_pushst和_popst,编译器会自动调用其对用的默认构造函数,对于内置类型_size的话,_size也会走初始化列表,但是由于我们没有给值,所以_size最后初始化的结果是随机值。
倘若我们想给_size进行赋值的话,有两种方法:
方法一(在声明那里给个缺省值):
在这里插入图片描述
方法二
在这里插入图片描述
不过个人感觉方法一的缺省值用起来比较好用。

1.2初始化列表能够完全取代函数体赋值

通过前面我们已经介绍了初始化列表,可以看到初始化列表确实感觉还不错,那初始化列表能不能完全替代函数体赋值呢?答案当然是不可以啊。请看下面的例子:

class Stack
{
public:
	Stack(int capacity = 10)
		:_a((int*)malloc(capacity * sizeof(int)))
		,_top(0)
		,_capacity(capacity)
	{}
private:
	int* _a;
	int _top;
	int _capacity;
};

我们可以通过初始化列表来完成初始化,但是我们无法检查_a,我们无法判断是否内存是否开启成功或失败。所以初始化列表无法完全替代函数体赋值,所以我们在某些情况还是需要函数体赋值的,请看改正后:

class Stack
{
public:
	Stack(int capacity = 10)
		:_a((int*)malloc(capacity * sizeof(int)))
		,_top(0)
		,_capacity(capacity)
	{
		if (_a = nullptr)//检查是否开辟成功需要函数体赋值
		{
			perror("malloc申请空间失败");
			exit(-1);
		}
		//如果要求数组必须初始化一下依然需要函数体赋值
		memset(_a, 0, sizeof(int) * capacity);
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

所以,总有一些工作是初始化列表完成不了的,这些工作就得需要函数体赋值来完成了。动态开辟二维数组就是一个典型的例子:

//动态开辟二维数组
class ABC
{
public:
	ABC(int row = 10, int col = 5)
		:_row(row)
		,_col(col)
	{
		_a = (int**)malloc(sizeof(int*) * row);//数组指针
		for (int i = 0; i < row; i++)
		{
			_a[i] = (int*)malloc(sizeof(int) * col);
		}
	}
private:
	int** _a;
	int _row;
	int _col;
};

现在来看下面的代码:
在这里插入图片描述
这里执行的顺序有些特殊,是按照函数体值声明的顺序来执行的,所以会先执行_a2(_a1),然后再执行_a1(a),所以最后_a1的值为1,_a2的值为随机值所以这里建议声明的顺序和定义的顺序保持一致。

二、static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

static成员特性:

  • 1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  • 2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  • 3.类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  • 4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 5.静态成员也是类的成员,受public、protected、private 访问限定符的限制

请问下面程序中总共创建了多少个对象:
在这里插入图片描述
在这里插入图片描述
自定义类型传参要调用拷贝构造,所以19行为4个。
在这里插入图片描述
这里虽然调用了Func()函数两次,局部的静态对象在静态区中,不管调用了多少次函数都只会被定义一次。
上述代码中虽然我们把_scount定义在了全局,但是定义在全局的话可以对_scount随意进行修改、任意位置可以修改。

我们可以把_scount放在成员变量中去,也可以定义为静态成员变量。
对于成员变量而言,成员变量属于每一个类对象的。
对于静态成员变量而言,静态成员变量不属于某个对象,而是属于类的每个对象共享,因为静态成员变量存储在静态区中,同时静态成员变量的声明周期是全局的。
在这里插入图片描述
静态成员变量是不允许在初始化列表那里进行静态成员变量的初始化的,因为其是属于全局的
是每个类对象所共享的,故不可以在初始化列表那里进行初始化。

那静态成员变量应该在哪里进行初始化呢: 既然静态成员变量是全局的,那我们就在全局对其进行初始化,请看: 在这里插入图片描述
在这里插入图片描述
静态成员变量定义为公有时可以A::_scount进行访问,而定义为私有时就不可以。编译器不想让我们随便去进行访问。

下面来看公有的成员函数以及其调用方法:
在这里插入图片描述
同时在静态成员函数中,是无法对非静态成员变量进行访问的,因为静态成员函数没有this指针

在这里插入图片描述

非静态成员函数也可以调用静态成员函数:但是静态成员函数不可以调用不可以调用非静态成员函数(非静态成员函数调用需要this指针,但是静态成员函数没有this指针):
在这里插入图片描述

设计一个类,只能在栈或者堆上创建对象

class A
{
public:
	static A GetStackObj()
	{
		A aa;
		return aa;
	}
	static A* GetHeapObj()
	{
		return new A;
	}
private:
	A()
	{}
private:
	int _a1 = 1;
	int _a2 = 2;
};

//设计一个类,只能在栈或者堆上创建对象
int main()
{
	//static A aa1;   //静态区
	//A aa2;          //栈
	//A* ptr = new A; //堆

	A::GetStackObj();
	A::GetHeapObj();
	return 0;
}

三、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度(关联度更加紧密),破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类

友元函数

注意点:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数 。
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同。

友元类

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。
class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

四、内部类

普通的类是定义在全局的,而内部类是定义在一个类中的,请看举例:
计算下面**类A**的大小,请看:
在这里插入图片描述

如果我们想单独创建一个B类对象,请看:
在这里插入图片描述
注意,这里如果我们不想让其他人利用**内部类B**来创建对象,想要把内部类B隐藏起来的话,我们当然就把他设置为私有(private)了,请看:
在这里插入图片描述
所以内部类总共就分为两种,一种是公有的、另一种就是私有的(内部类可以受到访问限定符的限制)。另外,内部类是外部类的天生友元。

五、匿名对象

匿名对象也称为临时对象。
在这里插入图片描述
匿名对象即用即销毁:匿名对象只存在于构造该对象的那行代码离开构造匿名对象的哪行代码后立即调用析构函数比如说第九行调用匿名对象,到了第十行这个匿名对象就被销毁了)。而有名对象的生命周期在当前函数局部域中。

5.1匿名对象的使用场景

匿名对象看起来没有办法使用,但是下面这个场景可以很好的使用匿名对象:
在这里插入图片描述
如果使用有名对象来调用成员函数Sum_Solution,我们是这样进行调用的,请看:
在这里插入图片描述

匿名对象调用成员函数格式:Class().fun();
如果使用匿名对象来调用成员函数Sum_Solution,我们是这样进行调用的,请看,即Solution().Sum_Solution(20);

5.2匿名对象具有常属性

传值返回会产生临时对象,匿名对象跟临时对象都具有常性。

在这里插入图片描述
对于const A& ra = A(1);引用的生命周期决定了匿名对象的生命周期,引用的生命周期一旦结束,匿名对象的生命周期立马也跟着结束。const可以延长对象的生命周期。

好了,现在类和对象这里的博客就暂时更新到这里啦,但是类和对象的学习还远没有结束,有点地方还是要慢慢理解,慢慢感悟。
再见啦各位!!!

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。