【c++】类和对象(四)初始化列表 explicit static 友员 内部类 匿名对象

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

书接上文【c++】类和对象(三)运算符重载、赋值运算符重载、const成员函数、取地址运算符重载 详情请点击<—
本文由小编为大家介绍初始化列表 explicit static 友员 内部类 匿名对象


一、构造函数二次认识

1. 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,但是这里的函数体中的语句仅仅可以称为赋初始初值,不能称为初始化,初始化只能初始化一次,但是这里的赋值却可以有多次

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

二、初始化列表

1. 概念介绍

  1. 初始化列表是类的对象进行实例化时,类对象的成员变量进行定义/初始化的地方,每个成员变量只能定义/初始化一次
  2. 格式冒号:开头加成员变量加括号,括号中放初始值或表达式,并且使用,逗号分隔开数据成员列表
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;
};

2. 要点讲解

  1. 由于初始化进行一次,那么每个成员在初始化列表最多只能出现一次,当成员变量没有出现在初始化列表时,编译器会默认将成员变量“拿”到初始化列表进行初始化
  2. 类的成员变量中包含引用的成员变量,被const修饰的成员变量,自定义类型的成员变量且该成员变量没有默认构造函数,必须手动传值调用其构造函数进行初始化
class B
{
    
    
public:
	B(int b)
		:_b(b)
	{
    
    }

private:
	int _b;
};

class A
{
    
    
public:
	A(int& a,int n)
		:_a(a)
		,_n(n)
		,_b(1)
	{
    
    }

private:
	int& _a;
	const int _n;
	B _b;
};

int main()
{
    
    
	int a = 0, n = 1;
	A a1(a, n);

	return 0;
}

在这里插入图片描述
3. 在今后的学习中,我们尽可能的去使用初始化列表去初始化,因为不管你是否使用初始化列表去初始化,我们自定义类型的成员变量一定会使用初始化列表去初始化
4. 但是这不代表构造函数体赋值就没有用了,例如使用malloc函数的时候进行检查,初始化列表就没有办法去完成检查,再例如二维数组初始化开辟空间的时候初始化列表就无法完成,可以说初始化列表可以解决90%的初始化问题,还有10%是需要构造函数体赋值来解决的,读者朋友们要合理使用

//malloc检查
class A
{
    
    
public:
	A()
		:_a((int*)malloc(sizeof(int)))
	{
    
    
		if (_a == nullptr)
		{
    
    
			perror("malloc error");
			exit(-1);
		}
	}

private:
	int* _a;
};

//二维数组初始化
class B
{
    
    
public:
	B(int row,int col)
		:_a((int**)malloc(sizeof(int*)*row))
		,_row(row)
		,_col(col)
	{
    
    
		if (_a == nullptr)
		{
    
    
			perror("malloc error");
			exit(-1);
		}

		for (int i = 0; i < row; i++)
		{
    
    
			_a[i] = (int*)malloc(sizeof(int)*_col);
			if (_a[i] == nullptr)
			{
    
    
				perror("malloc error");
				exit(-1);
			}
		}
	}

private:
	int** _a;
	int _row;
	int _col;
};
  1. 成员变量在类中声明的顺序就是初始化列表初始化的顺序,初始化列表初始化的顺序与成员变量在初始化列表中出现的先后顺序无关
class C
{
    
    
public:
	C(int a=0,int b=1)
		:_a(a)
		,_b(b)
	{
    
    }
	
private:
	int _b;
	int _a;
};

int main()
{
    
    
	C c;

	return 0;
}

运行程序进行调试,观察最初_a和_b都没有进行初始化
在这里插入图片描述
观察按下F11,_b由于是在类中成员变量先进性声明的,所以_b先在初始化列表中先初始化
在这里插入图片描述
继续观察按下F11,_a由于是在类中成员变量后进性声明的,所以_a先在初始化列表中后初始化
在这里插入图片描述
所以成员变量在初始化列表中初始化的顺序与初始化列表中成员变量出现的先后次序无关,而是与在类中声明的顺序有关

三、explicit关键字

  1. 对于构造函数接收单个参数具有类型转换的作用,explicit放在构造函数的前面可以禁止掉类型转换
  2. 可以接收单个参数的构造函数有:1.全缺省的构造函数,2第一个参数无缺省值,余下的参数全部都有缺省值,3.只有一个参数的构造函数
    下面小编进行讲解一下发生类型转换的场景
#include <iostream>

using namespace std;

class A
{
    
    
public:
	A(int a)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}

	A(const A& a)
	{
    
    
		_a = a._a;
		cout << "A(const A& a)" << endl;
	}
	
private:
	int _a;
};

int main()
{
    
    
	A a1 = 2;//本质是隐式类型转换
	const A& a2 = 2;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
A a1=2的本质是进行了隐式类型转换,即发生了类型转换
在这里插入图片描述
我们可以看到对于A a1=2这一行仅仅是调用了构造函数,而没有小编所谓的构造函数+构造函数,那么到底有没有产生了小编所谓的临时对象呢?
那么进行观察const A& a2=2这一行,其调用了构造函数,我们知道单纯的引用仅仅是取别名,并不会调用类类型的构造函数,那么这里调用了构造函数,就必定是使用了2调用构造函数产生了临时对象,同时我们知道临时对象具有常性,权限为只读,进行引用如果不加const,那么引用完之后的权限为可读可写为权限的放大,这样必然是不行的,所以进行引用临时对象必须加const修饰,如果小编将const去掉,那么编译器必然会报错,如下
在这里插入图片描述

  1. 如果我们不想要我们的代码发生类型转换,不进行隐式类型转换产生临时对象的话,可以使用explicit关键字放在构造函数的前面进行限制即可
class A
{
    
    
public:
	explicit A(int a)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	
private:
	int _a;
};

int main()
{
    
    
	A a1 = 2;//本质是隐式类型转换
	A& a2 = 2;

	return 0;
}

当我们使用explicit关键字放在构造函数的前面进行限制之后,编译器在编译期间进行语法检查的时候就会限制这种隐式类型转换,进行报错
在这里插入图片描述

四、static成员

在类中,声明为static类型的成员称为类的静态成员,使用static修饰的成员变量称为静态成员变量,使用static修饰的成员函数称为静态成员函数,同时注意静态成员变量的初始化在类外

  1. 静态成员变量是类对象共同享有,不属于某一对象,静态成员变量是存储在静态区/数据段中
  2. 静态成员变量必须在类外进行初始化定义,格式为 静态成员变量类型 类类型::静态成员变量名=你所要初始化的值
  3. 类静态成员没有隐含的this指针,不能访问任何非静态成员,只能访问非静态成员
  4. 非静态成员可以访问静态成员,也可以访问非静态成员
  5. 类静态成员(静态变量或静态函数)的访问方式: 1. 类名 ::静态成员 2. 对象.静态成员
  6. 静态成员也是类的成员,受类域,类访问限定符public,protected,private的限制
  1. 现在让你统计类实例化了多少个变量,你将如何进行统计,我们知道对象的创建无非就是调用构造函数或者是拷贝构造函数进行创建,那么创建一个全局变量在构造函数和拷贝构造函数内进行统计次数不就可以了
  2. 注意当创建变量名count的时候,这个变量名会与stl中的变量名冲突,这样编译在调用count的时候会显示符号count不明确,所以建议使用_count进行命名

在这里插入图片描述
变量名会与stl中的变量名命名冲突
在这里插入图片描述

int _count = 0;

class B
{
    
    
public:
	B()
	{
    
    
		_count++;
	}

	B(const B& b)
	{
    
    
		_count++;
	}

private:
};

int main()
{
    
    
	B b1;
	B b2(b1);

	cout << _count << endl;

	return 0;
}

运行这样就可以统计出类实例化了多少个对象
在这里插入图片描述

  1. 但是这样你可以思考一下,对于全局变量_count,没有进行类似于类中成员变量的封装,那么就可以对全局变量_count的值进行任意修改了,我们不想要这样的事情发生,那么可不可以像类中成员成员变量一样进行封装全局变量呢?
  2. 那就是使用静态成员变量和静态成员函数进行封装和获取全局变量
class B
{
    
    
public:
	B()
	{
    
    
		_count++;
	}

	B(const B& b)
	{
    
    
		_count++;
	}

	static int Get_count()
	{
    
    
		return _count;
	}

private:
	static int _count;
};

int B::_count = 0;

int main()
{
    
    
	B b1;
	B b2(b1);

	cout << B::Get_count() << endl;

	return 0;
}
  1. 讲一下实现细节,我们将全局变量封装到类的成员变量中,但是这仅仅是声明静态成员变量,我们还需要定义初始化静态成员变量所以需要在类外进行int B::_count = 0初始化定义静态变量_count
  2. 由于在类中不受访问限定符的限制,非静态成员可以访问静态成员,所以直接在构造函数和析构函数中统计即可
  3. 由于静态成员变量_count我们进行使用private封装,所以我们无法在类外使用类名::静态成员变量拿到_count,由于在类域中非静态成员函数可以访问非静态成员变量并且不受访问限定符的限制,所以这时候我们使用静态成员函数进行获取静态成员变量_count即可
  4. 同时使用静态成员函数进行获取,在类外不需要类对象即可完成对静态成员函数的调用,直接使用类名::静态成员函数就可以进行调用静态成员函数

这样在类外就无法对我们封装好的静态成员变量进行修改,封装性好,安全,同时又可以使用类名::静态成员函数,进行调用静态成员函数拿到静态成员变量的值
在这里插入图片描述

五、友元

友元提供了一种突破类封装的方式,但是同时又在一定程度上破坏了类的封装性
友元分为友元函数和友元类

1.友元函数

友元函数可以直接访问类的私有和保护成员,它是定义在类外的普通函数,不属于任何类,在类中进行声明的时候需要在前面加friend关键字

  1. 友元函数可以访问类的私有和保护成员,友元函数不是类的成员,该函数不是在类中进行定义声明的,仅仅是在类中声明了该函数是类的友元函数
  2. 友元函数不可以使用const进行修饰
  3. 友元函数可以在类中的任意位置进行声明,不受访问类限定符的限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数相同
  1. 在日期类中,我们想要对象可以像内置类型一样使用标准流输出对象cout加流插入插入运算符<<进行将日期内容输出到屏幕上,那么我们就要进行重载<<,即operator<<,其实c++中cout自动识别类型也没有多么神奇,而是c++在std命名空间中重载<<了内置类型,当我们使用<<的时候通过c++中经过函数命名规则修饰后根据变量的类型在库中匹配就可以实现自动识别类型,而我们的日期类是自定义类型的,编译器也不知道我们究竟想要输出什么,所以需要我们自己进行重载<<,即operator<<
    在这里插入图片描述
  2. 那么在日期类中进行重载<<即可,但是在调用的时候要特别注意,由于在调用类的成员函数需要this指针,默认用于接收对象的地址的this指针就抢占了第一个参数位置(由于this指针是隐含的,所以该位置是不可见的),那么第一个操作数的位置就一定要是对象,那么进行调用的时候我们的cout就只能放在<<的后面进行调用,这与我们的习惯大不相同
  3. cout(标准输出流对象)是ostream类的对象,所以我们要使用ostream的引用作为参数进行接收,由于我们要支持连续输出,所以要有返回值为ostream的引用,因为out(out作为cout的别名)出了重载操作符还在
class Date
{
    
    
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
    
    }

	ostream& operator<<(ostream& out)
	{
    
    
		out << _year << "-" << _month << "-" << _day << endl;
		return out;
	}

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

int main()
{
    
    
	Date d;

	d << cout;

	return 0;
}

进行调用,对象放在第一个位置,我们的cout放在<<的后面进行调用即为d.operator<<(&d,cout)
在这里插入图片描述

这里注意如果我们想要使用常规的写法是不行的,因为这样的话就无法调用重载运算符operator<<并且this指针接收到的是cout的地址,无法接收到对象的地址,而按照重载运算符operator<<的参数格式对象就要作为类型为ostream类的对象out的引用,两种类对象的类都不相同肯定是无法进行取别名的,所以不可以进行使用我们的常规调用,那么我们该如何进性重载运算符operator<<呢?
在这里插入图片描述

  1. 那么我们将重载运算符operator<<放在类外面进行声明定义即可,并且使用友元函数在日期类中进行声明,重载运算符operator<<是日期类的朋友,友元函数重载运算符operator<<可以访问日期类的私有private和保护protected成员
  2. 例如小编在日期类的公有限定符的成员中添加一个获取年的函数,并且使用友元函数重载运算符operator<<进行调用看是否调用成功

如图,调用失败,所以友元函数只可以调用类的私有和保护成员,不可以调用类的公有成员
在这里插入图片描述
在这里插入图片描述

  1. 同时这里也要注意,在类中进行的是声明重载运算符operator<<是日期类的朋友,仅仅是声明朋友关系,所以声明的位置与类中的访问限定符无关,声明是朋友关系可以放在类中的任意位置,不受访问限定符的限制
class Date
{
    
    
	friend ostream& operator<<(ostream& out, const Date& d);

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

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

int main()
{
    
    
	Date d;

	cout << d;

	return 0;
}

ostream& operator<<(ostream& out, const Date& d)
{
    
    
	out << d._year << "-" << d._month << "-" << d._day << endl;

	return out;
}

2.友元类

友元类的所有成员函数都是友元类进行声明为友元类的那个类的友元函数,这些友元函数都可以进行访问那个类的保护protected和私有private成员,不可以访问那个类的公有public成员

  1. 友元关系是单项的不具有交换性
    例如:B是A的友元类,但是B不是A的友元类
  2. 友元关系不具有传递性
    例如:B是A的友元类,C是B的友元类,但是C不是A的友元类
  3. 友元关系不可以继承,具体详情请期待
class A
{
    
    
	friend class B;
public:

private:
	int _a = 1;
};

class B
{
    
    
public:
	int Get_a(const A& a)
	{
    
    
		return a._a;
	}
private:
	int _b = 2;
};

int main()
{
    
    
	A a;
	B b;

	cout << b.Get_a(a) << endl;

	return 0;
}

调用完成,我们可以使用类B的对象b的成员函数去访问类A的对象a的非公有成员
在这里插入图片描述

六、内部类

如果一个类定义在另一个类的内部,那么这个定义在另一个类内部的类我们就称为内部类。内部类是一个独立的类,它不属于外部类,外部类不可以使用对象去访问内部类的成员。外部类不可以使用对象去访问内部类的成员外部类对内部类没有任何天然的访问权限

  1. 内部类是外部类的友元类,但是由于友元类不具有交换性,所以内部类不是外部类的友元类
  2. 内部类可以定义在外部类的公有public、私有private、保护protected区域
  3. 内部类可以直接访问外部类的static成员,不需要外部类的类名或对象名
  4. sizeof(外部类)=外部类,外部类的大小和内部类没有半毛钱关系
  5. 内部类可以访问外部类的全部成员(外部类的成员函数和成员变量)
class A
{
    
    
public:
	int Get_b()
	{
    
    
		return _b;
	}

	class B
	{
    
    
	public:
		int Get_a(A& a)
		{
    
    
			cout << _c << endl;
			cout << a.Get_b() << endl;

			return a._a;
		}
	};

private:
	int _a = 1;
	int _b = 2;
	static int _c;
};

int A::_c = 3;

int main()
{
    
    
	A a;
	A::B b;

	cout << b.Get_a(a) << endl;

	return 0;
}

如下,我们使用类A创建对象a,使用类B(A::B )创建了b,可以访问类A的全部成员
在这里插入图片描述

七、匿名对象

  1. 匿名对象顾名思义就是没有对象名,即没有被命名的情况下被创建的临时对象,所以匿名对象具有常性
  2. 匿名对象的使用格式为 类名加() 即可
  3. 它的生命周期只有当前一行,使用完之后就调用析构函数了
  4. 由于匿名对象具有常性,权限为只读,所以对匿名对象进行引用的时候,引用如果不加const进行修饰那么权限为可读可写,这时候进行引用属于权限的放大,不可以进行引用,所以需要加const将引用后的权限也缩小为只读,这样进行引用之后的权限也是只读属于权限的平移,可以进行引用
  5. 如果使用const 类名 对匿名对象进行引用,那么匿名对象的生命周期被延长就变为当前函数的局部域
class A
{
    
    
public:

private:
	int _a = 1;
};

int main()
{
    
    
	A();//生命周期只有当前一行,匿名对象具有常性
	const A& a = A();
	//如果使用const A对匿名对象进行引用,那么匿名对象的生命周期被延长就变为当前函数的局部域了

	return 0;
}

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!