类与对象下篇

前言

在类与对象上篇我们讲解了类的基础框架,中篇我们讲解了类的基本内容,下篇我们将补充类的一些零散知识点。

一、构造函数的初始化(初始值)列表

构造函数:创建类对象时自动调用,给对象中各个成员变量一个合适的初始值。

1、引入

我们都知道,有一些对象,在定义时就必须初始化,如:

  • 引用变量
  • const变量
  • 没有默认构造函数自定义类型

代码示例:

class A
{
public:
	A(int a)
	{
		_a = a;
	}
private:
	int _a;
};

class B
{
public:
	B(int a,int &ref,int n)
	{
		_aobj(a);
		_ref = ref;
		_n = n;
	}
private:
	A _aobj;//没有默认成员函数的自定义类型
	int& _ref;//引用变量
	const int _n;//const变量
};

该代码,编译报错,如下图:

在这里插入图片描述

这就需要知道构造函数的初始化列表,初始化列表可以理解成对象成员变量定义的位置,引用变量、const变量、没有默认构造函数的自定义类型变量都必须在定义时就初始化,在构造函数体中的赋值时变量已经创建好了,这时的赋值只能将其称为赋初值,而不能称为初始化,所以编译报错。

初始化与默认初始化:

  • 初始化
    • 初始化是指在变量定义的同时给变量指定初始值
    • 初始化只能初始化一次
  • 默认初始值
    • 默认初始化是指变量定义时没有指定初值,此时变量被赋予一个“默认值”
    • 默认值由变量类型决定,同时定义变量的位置也会对默认值有影响
    • 内置类型的变量如果没有(显式)初始化,它的值由定义的位置决定——①全局变量会被默认初始化为0;②局部变量将不被初始化,为随机值;(特殊:静态局部变量会被默认初始化为0)
  • 所以成员变量有内置类型,一般需要自己显式实现构造函数

构造函数的函数体:

  • 当初始化列表为空时(且内置类型不做处理),使用构造函数体赋初值之后,可以称对象中有了一个初始值,但是不能将其称为对对象中成员变量的初始化
  • 构造函数体中的语句只能将其称为赋初值,而不能称作初始化
  • 初始化只能初始化一次,而构造函数的函数体可以多次赋值
  • 构造函数体除了可以给成员变量赋初值,还可以做一些额外的工作。例如:使用malloc申请动态资源时,需要在函数体中判断是否开辟成功。

2、初始化列表

(1)概念

初始化列表:是成员变量定义的地方,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每一个“成员变量”后面跟一个放在括号中的初始值或表达式。

代码示例:

class A
{
public:
	A(int a)
	{
		_a = a;
	}
private:
	int _a;
};

class B
{
public:
	//初始化列表:成员变量定义的地方
	B(int a, int& ref, int n) :_aobj(a), _ref(ref), _n(n)
	{}
private:
	//这里只是声明
	//以下三个成员变量都有一个特征:在成员定义时就必须初始化!
	A _aobj;//没有默认成员函数的自定义类型
	int& _ref;//引用变量
	const int _n;//const变量
};

int main()
{
	//B b(1, 2, 3);//编译报错,因为第二个实参涉及引用权限放大
	int a = 2;
	//对象整体定义的地方,在创建时自动调用构造函数给成员变量赋初值
	B b(1, a, 3);
	return 0;
}

tip:

  • 初始化列表是成员变量定义的地方,所以每一个成员变量在初始化列表中最多只能出现一次(即初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置初始化:
    • 引用成员变量
    • const成员变量
    • 没有默认构造函数的自定义类型成员
    • 因为这三种成员变量有一个共同特征,在定义时就必须初始化,所以必须在初始化列表位置初始化
  • 尽量使用初始化列表初始化成员变量,因为不管你是否使用初始化列表,对于成员变量,一定会先使用初始化列表定义成员变量
    • 如果在成员列表显式初始化成员变量,使用指定的初值初始化成员变量
    • 如果成员变量没有显式使用初始化列表:
      • 如果在成员变量声明时提供缺省值,初始化列表使用缺省值初始化成员变量
      • 如果在成员变量声明时没有提供缺省值,初始化列表将对该成员执行默认初始化(内置类型与没有默认构造函数的自定义类型不被处理)
class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}
private:
	int _a;
};

class B
{
public:
	//初始化列表:不显示初始化,如果成员有缺省值,使用缺省值初始化
	B() 
	{}

	//初始化列表:显示初始化,使用指定的值初始化,不使用缺省值
	B(int a,int n):_aobj(a),_n(n)
	{}

private:
	A _aobj;//没有默认成员函数的自定义类型
	const int _n = 1;//const变量,给该成员变量提供一个缺省值,在初始化列表没有显示初始化时,就会使用该缺省值
};

int main()
{
	B b1;
	B b2(10, 10);
	return 0;
}

F10调试观察,和预期结果是否一致:

在这里插入图片描述

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class B
{
public:
	//初始化列表:先使用_a1给_a2初始化,再使用a给_a1初始化
	B(int a)
		:_a1(a), _a2(_a1)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};


int main()
{
	B b(1);
	b.Print();
	return 0;
}

//代码分析:
//A、输出1 1
//B、程序崩溃——数组越界,野指针等严重情况才会
//C、编译不通过——语法错误
//D、输出1 随机值

//正确选项:D

运行结果:

在这里插入图片描述

回顾给成员变量内置类型给定缺省值:

  • C++11针对内置类型成员不初始化的缺陷,打了个补丁,即内置类型成员变量在类中声明时可以给缺省值。
  • 该缺省值是给初始化列表用的

(2)总结

  1. 初始化列表是构造函数的一部分,是成员变量定义的地方。
  2. 建议成员变量初始化都是用初始化列表,因为在初始化列表不管你是否显式初始化成员变量,成员变量都会先使用初始化列表初始化
  3. 构造函数体与初始化列表结合使用,因为总有一些事情是初始化列表不能完成的,根据实际开发结合使用。

代码示例:在堆区开辟一个二维数组

class A
{
public:
	A(int row = 10, int col = 10)
		:_row(row), _col(col)
	{
		//指针数组
		_a = (int**)malloc(sizeof(int*) * row);
		//判断是否开辟失败
		if (nullptr == _a)
		{
			//开辟失败,提示并退出
			perror("malloc");
			exit(-1);
		}
		//循环,指针数组的每一个元素指向一维数组
		for (int i = 0; i < row; i++)
		{
			_a[i] = (int*)malloc(sizeof(int) * col);
			if (nullptr == _a[i])
			{
				//开辟失败,提示并退出
				perror("malloc");
				exit(-1);
			}
		}
	}
private:
	int** _a;
	int _row;//行
	int _col;//列
};

二、explicit关键字修饰构造函数

1、隐式的类类型转换

构造函数不仅可以初始化对象,如果构造函数只接受一个参数,还具有隐式类型转换的作用。

代码示例:

class Date
{
public:
	//1、单参构造函数,具有类型转换作用
	//Date(int year = 2024)
	//	:_year(year)//建议使用初始化列表初始化,虽然内置类型在函数体也可以完成赋初值的操作
	//{}

	//2、对于多个参数的构造函数,
	//(1)所有参数都设缺省值——全缺省
	//(2)除第一个参数无默认值其余参数均有默认值
	//具有类型转换作用,建议使用第一种
	//注意:两者不构成重载,只能存在一种
	Date(int year = 2001, int month = 1, int day = 1)
		:_year(year),_month(month),_day(day)
	{
		//观察是否调用构造函数
		cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
	}

	//拷贝构造
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;//观察是否调用拷贝构造函数
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1 = 2001;//隐式类型转换,整形转换成自定义类型
	//语法:①2001先构造一个Date的临时对象;②临时对象再拷贝构造d1
	//tip:同一个表达式连续的构造+拷贝构造,一般都会优化成直接构造

	//编译器会优化成调用直接构造,怎么证明隐式转化的发生呢?
	//答案是:利用临时对象具有常性与引用权限的特点证明会发生隐式转换
	//Date& d2 = 2001;//error C2440: “初始化”: 无法从“int”转换为“Date &”
	const Date& d2 = 2001;
	return 0;
}

运行结果:

在这里插入图片描述

tip:

  • 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则,例如:
    • 单参构造函数
    • 多个参数的构造函数,除第一个参数外,其余参数必须均有缺省值
  • 只允许一步类类型转换,即编译器每次只能执行一种类类型的转换
//编译器每次只能执行一种类类型的转换
class A
{
public:
	//支持字符、整形、浮点型、布尔型隐式转换为A类型
	A(double d)
		:_d(d)
	{
		cout << "A(double d)" << endl;
	}
private:
	double _d;
};

class B
{
public:
	//支持A类型隐式转换为B类型
	B(A a)
		:_a(a)
	{
		cout << "B(A a)" << endl;
	}
private:
	A _a;
};

void func(const B& a)
{}

int main()
{
	//错误示例:
	//func(1);// error C2664: “void func(const B &)”: 无法将参数 1 从“int”转换为“const B &”

	//因为编译器每次只能执行一种类类型的转换,所以需要我们定义;两种转换
	//1、把整形1转换成A
	//2、再把这个A转换成B
	//正确示例1:显示地转换成A,隐式转换成B
	func(A(1));
	//正确示例2:隐式转换成A,显示转换成B
	func(B(1));
	return 0;
}

2、explicit构造函数

explicit修饰构造函数,禁止隐式类型转换。

tip:

  • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
  • 关键字explicit只对发生隐式转换的构造函数有效,所以对不能用于执行隐式转换的构造函数无须将其指定为explicit。
  • 注意:尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制进行转换。

代码示例:

class Date
{
public:
	//explicit修饰构造函数,禁止隐式类型转换
	explicit Date(int year = 2001, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{
		//观察是否调用构造函数
		cout << "Date(int year = 2001, int month = 1, int day = 1)" << endl;
	}

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

int main()
{
	//Date d1 = 2001;//报错,因为explicit修饰构造函数,禁止隐式类型转换
	
	Date d1 = (Date)2001;//强制类型转换
	return 0;
}

补充:标准库中类含有单参数的构造函数

  • 接受一个单参数的const char*的string构造函数不是explicit的
  • 接受一个容量参数的vector构造函数是explicit的

三、拷贝对象时一些编译器优化

1、代码示例1

自定义类型参数与返回值是非引用时,需要调用拷贝构造函数。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		//观察是否调用构造函数
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a(aa._a)
	{
		//观察是否调用拷贝构造函数
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		//观察是否调用赋值重载函数
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		//观察是否调用析构函数
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}

int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	A ra;
	ra = f2();
	cout << endl;
	return 0;
}

运行结果:

在这里插入图片描述

2、代码示例2

同一行一个表达式中连续的构造+拷贝构造,一般编译器会优化合二为一,减少对象的拷贝,在传参和传返回值等场景下还是非常有用的。

代码示例:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		//观察是否调用构造函数
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a(aa._a)
	{
		//观察是否调用拷贝构造函数
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		//观察是否调用赋值重载函数
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		//观察是否调用析构函数
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}

int main()
{
	A aa1;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

运行结果:

在这里插入图片描述

tip:

  • 成员函数的调用
    • 构造函数:对象实例化时自动调用,初始化对象
    • 析构函数:对象销毁时自动调用,清理对象资源
    • 拷贝构造函数:用一个已经存在的对象初始化另一个对象时,自动调用
    • 赋值重载函数:已经存在的两个对象之间复制拷贝
  • 同一行一个表达式中连续的构造+拷贝构造,一般编译器会优化合二为一
    • 隐式类型转换,连续构造+拷贝构造——》优化为直接构造
    • 一个表达式中,连续构造+拷贝构造——》优化为一个构造
    • 一个表达式中,连续拷贝构造+拷贝构造——》优化为一个拷贝构造
    • 注意:一个表达式中,连续拷贝构造+赋值重载——》无法优化
  • 建议在传参和传返回值等场景,使用连续构造,因为编译器会优化。

四、static成员

1、引入

面试题:实现一个类,计算程序中创建出了多少个类对象。

思路:

  • 一个新对象创建的同时会调用构造函数初始化,销毁时会调用析构函数清理对象,所以创建一个变量,每调用一次构造变量+1,每调用一次析构变量
  • 注意:该变量必须满足以下条件
    • 该变量属于类的每一个对象共享
    • 该变量的生命周期=程序的生命周期,即存储在静态区

综上我们可以使用全局变量来计算程序创建出了多少个类对象,但是全局变量有一个缺点——没有封装,任何地方都可以随意改变,不安全。

所以这个时候就得引入静态成员了。

2、概念

声明为static的类成员称为类的静态成员

  1. 用static修饰的成员变量,称之为静态成员变量;
  2. 用static修饰的成员函数,称之为静态成员函数。

3、特性

class A	
{
public:
	A() 
	{
		++_scount; 
	}
	A(const A & t) 
	{
		++_scount;
	}
	~A() 
	{ 
		--_scount;
	}
	//静态成员函数
	static int GetACount() 
	{ 
		//tip:1、静态成员函数没有隐藏的this指针,不能访问任何非静态成员
		//2、因为静态成员变量一般定义在private下,类外无法访问,只能通过静态成员函数访问,所以静态成员函数常与静态成员变量配套使用,
		return _scount; 
	}
private:
	//成员变量属于类对象,存储在对象里面
	int _a = 1;//缺省值给初始化列表使用
	//静态成员变量属于所有类对象所共享,不属于某个具体的对象。存放在静态区
	//tip:
	// 1、一般情况下,静态成员变量不能给缺省值,因为缺省值是给初始化列表用的,初始化列表是初始化对象的成员,静态成员变量不属于类的任何一个对象。
	// 2、静态成员也是类的成员,受访问限定符的限制
	//		(1)静态成员变量建议定义在private下,将其封装在类中,不能在类外随意改变,安全
	//		(2)静态成员函数建议定义在public下,与静态成员变量配套使用

	static int _scount;
};
//静态成员必须在类的外部定义和初始化
//tip:
//	1、当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
//	2、定义静态成员变量的方式与在类外部定义成员函数类似,需要指定对象的类型名,然后是类名,作用域运算符以及成员自己的名字
//	3、一个静态成员变量只能定义一次,为了确保只定义一次,建议把静态成员的定义与其他非内联函数的定义放在同一个文件
int A::_scount = 0;

int main()
{
	//静态成员可以通过类名::静态成员或者对象.静态成员来两种方式访问
	//静态成员只要能突破类域和访问限定符就可以访问
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	return 0;
}

tip:

  • 静态成员属于类为所有类对象共享,不属于某个具体的对象,存放在静态区
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制
    • 静态成员变量一般在private下声明——将其封装在类中,在类外不能使用,相较全局变量安全
    • 静态成员函数一般在public下声明——因为静态成员变量被封装在类中,类外无法访问,可通过静态成员函数获取,所以静态成员变量常与静态成员函数配套使用
  • 一般静态成员变量不能在声明时给缺省值,因为缺省值是给初始化列表使用的,初始化列表是初始化对象的成员变量,而静态成员变量不属于类的任何一个对象。
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。常与静态成员变量配套使用。
  • 当在类的外部(即在全局位置)定义静态成员时
    • 不能重复static关键字,该关键字只能出现在类内部的声明语句
    • 必须指明成员所属类名
    • 静态成员变量只能定义一次,为了确保只定义一次,建议将静态成员变量的定义与其他非内联函数的定义放在同一个文件中
  • 静态成员只要能突破类域和访问限定符就可以访问,即可用类名::静态成员或者对象 .静态成员两种方式访问

问题:

  1. 非静态成员函数能否调用静态函数?
    可以,静态成员只要能突破类域和访问限定符就可以访问
  2. 静态成员函数可以调用非静态成员函数吗?
    不可以,因为非静态成员函数调用需要this指针,而静态成员函数没有this指针

4、应用

1. 求1+2+……+你,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句

思路:每创建一个对象都需要调用构造函数,所以可以在构造函数体中累加计算。

代码示例:

class Sum
{
public:
    Sum()
    {
        _ret += _i;
        _i++; 
    }
    static int GetRet()
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;

class Solution {
public:
    int Sum_Solution(int n) {
        //创建n个对象的数组(支持变长数组),就会调用n次构造
        Sum a[n];
        return Sum::GetRet();
    }
};
  1. 设计一个类,在类外面只能在栈或者堆创建对象

思路:先将构造函数定义在private下,在设计两个函数分别在栈与堆上创建对象,在类外通过调用这两个函数来创建栈或者堆的对象

问题:在类外调用成员函数时,需要通过对象来调用,所以这个时候产生了先有鸡还是先有蛋的问题——为了解决该问题将其设计为静态成员函数

代码示例:

class A
{
public:
	static A GetStackObj()
	{
		//创建一个栈上的对象
		A aa;
		//出了函数体aa销毁,所以只能值返回
		return aa;
	}
	static A* GetHeapObj()
	{
		return new A;//new在堆区创建对象,后面我们会讲解
	}
private:
	//将构造函数定义在private下,类外不能随意创建对象
	A()
	{}
	//成员变量
	int _a = 1;
};


int main()
{
	A::GetStackObj();
	A::GetHeapObj();
	return 0;
}

五、友元

1、引入

在开发过程中,在类外有些时候需要我们能访问到类中的私有成员,比如输入输出的重载函数。

输入输出运算符必须是非成员函数: 因为输入输出运算符的左操作数分别是istream和ostream,而成员函数的左操作数是隐含的this指针,所以输入输出运算符必须是非成员函数。

问题: 在类外想要访问类中的私有成员,就需要突破类的封装,突破类封装的方式有如下两种:

  1. 类中的public下,提供SetXx和GetXx两个函数。
  2. 友元

tip: 虽然友元可以突破封装,提供了便利。但是友元会增加耦合度(即两者关系更紧密了,例如:类中的成员改变,类外也要随之改变),破坏封装,所以友元不建议多用。

2、友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部普通函数,不属于任何类,但是需要在类的内部声明,声明时需要加friend关键字。

代码示例:

class Date
{
	//声明operator<<和operator>>这两个函数为Date类的友元类
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	//tip:输入输出运算符重载,不能是类的成员函数,需要定义在类外
	/*ostream & operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}*/
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	//<<运算符从左向右结合,可以连续打印,所以要返回ostream的形参
	return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
	//输入运算符必须处理输入可能失败的情况
	int year, month, day;
	_cin >> year >> month >> day;
	if (_cin)
	{
		//输入成功
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		//输入失败,提示并断言
		cout << "输入失败" << endl;
		assert(false);
	}
	return _cin;
}

int main()
{
	Date d1;
	cout << d1 << endl;
	cin >> d1;
	cout << d1 << endl;
	return 0;
}

tip:

  • 友元函数可访问类的私有保护成员成员,但不是类的成员函数
  • 友元函数不能用const修饰,因为const修饰的是成员函数的this指针
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制,因为访问限定符限制的是类的成员
  • 建议: 最好在类定义开始或结束前的位置集中声明友元
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

补充:

  • 成员函数作为友元
    • 把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类
    • 想要令某个成员函数作为友元,我们必须仔细组织程序的结构满足声明和定义的彼此依赖关系,例如令A类中的成员函数func作为类B的友元,我们必须按照如下设计程序
      • 首先定义A类,其次声明func函数,但是不能定义它。
      • 接下来定义B,包括对于func的友元声明。
      • 最后定义func,此时func才可以使用B的成员。注意:在使用B成员之前必须要先声明B。
  • 函数重载和友元
    • 尽管重载函数的名字相同,但他们是不同的函数。所以,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。

3、友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

代码示例:

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

tip:

  • 友元关系是单向的,不具有交换性。例如:上述代码中Time类中声明Date为其友元类,那么在Date类中可以直接访问Time类中的私有成员变量,但在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递。例如:C是B的友元,B是A的友元,但不能说明C是A的友元。
  • 友元关系不能继承。在后续讲解继承时再详细介绍。

4、友元的声明

友元的声明仅仅指定访问的权限,而非一个通常意义上的函数声明。

所以为了使友元对类的用户可见,建议把友元的声明与类本身放置在同一个头文件中(类的外部)。

**tip:**一些编译器允许在尚未友元函数的初始声明的情况下就调用它。但是建议还是提供一个独立的函数声明,提高可移植性。

六、内部类

1、概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

2、特性

代码示例:

class A
{
private:
	static int k;
	int h;
public:
	class B // 内部类是外部类的天生友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	//1、sizeof(外部类) = 外部类,大小和内部类无关,因为类实例化之后才占空间
	cout << sizeof(A) << endl;
	//要创建B的对象,必须要突破外部域和访问限定符
	A::B b;
	b.foo(A());

	return 0;
}

tip:

  • sizeof(外部类)= 外部类,大小与内部类无关,因为类实例化之后才占空间。
  • 内部类是外部类的天生友元,所以可以直接访问外部类的成员。
  • 内部类定义在外部类中,受访问限定符的限制
  • 建议把成员都定义在外部类,优点是成员在外部类和外部类都可以访问,因为内部类天生是外部类的友元。

七、匿名对象

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	void func()
	{}
private:
	int _a;
};

int main()
{
	//1、匿名对象的生命周期在当前行
	A aa1;//有名对象——生命周期在当前函数局部域
	A();//匿名对象——生命周期在当前行

	aa1.func();
	//匿名对象不传参也要带括号,因为类型不能调用函数,需要对象来调用函数
	A().func();

	//2、匿名对象具有常性
	//A& ra = A();//报错

	//3、const引用延长匿名对象的生命周期,生命周期在当前函数局部域
	const A& ra = A();
	aa1.func();
	return 0;
}

tip:

  • 匿名对象的生命周期在当前行
  • 匿名对象具有常性
  • const引用会延长匿名对象的生命周期,生命周期在引用对象的当前函数作用域

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/358239.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多模态大模型综述整理

论文&#xff1a;MM-LLMs: Recent Advances in MultiModal Large Language Models 论文地址&#xff1a; https://arxiv.org/pdf/2401.13601.pdf 表1&#xff1a;26种主流多模态大型语言模型&#xff08;MM-LLMs&#xff09;概要 输入到输出模态&#xff08;I→O&#xff09;…

[React源码解析] Fiber (二)

在React15及以前, Reconciler采用递归的方式创建虚拟Dom, 但是递归过程不可以中断, 如果组件的层级比较深的话, 递归会占用线程很多时间, 那么会造成卡顿。 为了解决这个问题, React16将递归的无法中断的更新重构为异步的可中断更新, Fiber架构诞生。 文章目录 1.Fiber的结构2…

MySQL前百分之N问题--percent_rank()函数

PERCENT_RANK()函数 PERCENT_RANK()函数用于将每行按照(rank - 1) / (rows - 1)进行计算,用以求MySQL中前百分之N问题。其中&#xff0c;rank为RANK()函数产生的序号&#xff0c;rows为当前窗口的记录总行数 PERCENT_RANK()函数返回介于 0 和 1 之间的小数值 selectstudent_…

Ubuntu22.04 网络图标突然消失

本来好好的&#xff0c;突然就发现没有网络了&#xff0c;图标也不见了。 特别是Ubuntu虚拟机&#xff0c;容易出现此问题。 修复办法 1. sudo service network-manager stop2. sudo rm /var/lib/NetworkManager/NetworkManager.state3. sudo service network-manager start到…

通过Nacos权重配置,模拟微服务金丝雀发布效果(不停机部署)

在微服务项目迭代的过程中&#xff0c;不可避免需要上线&#xff1b;上线对应着部署&#xff0c;或者升级部署&#xff1b;部署对应着修改,修改则意味着风险。 传统的部署都需要先停止旧系统&#xff0c;然后部署新系统&#xff0c;之后需要对新系统进行全面的功能测试&#xf…

腾讯云SDK并发调用优化方案

目录 一、概述 二、 网关的使用 2.1 核心代码 三、腾讯云SDK依赖包的改造 一、概述 此网关主要用于协调腾讯云SDK调用的QPS消耗&#xff0c;使得多个腾讯云用户资源能得到最大限度的利用。避免直接使用腾讯云SDK 时&#xff0c;在较大并发情况下导致接口调用异常。网关的工…

AtCoder Beginner Contest 338 A~F

A.Capitalized?&#xff08;模拟&#xff09; 题意&#xff1a; 给一个字符串 s s s&#xff0c;询问 s s s的第一个字母是不是大写&#xff0c;并且其他字母都是小写。 分析&#xff1a; 使用 A S C I I ASCII ASCII码&#xff0c;单独判断第一个字母&#xff0c;循环判断…

三步万能公式解决软件各种打不开异常

程序员都知道,辛苦做的软件发给客户打不开那是一个大写的尴尬,尴尬归尴尬还是要想办法解决问题. 第一步清理环境. 目标机台有环境和没有运行环境的,统统把vs环境卸载了,让目标机台缺少环境.第二步打包环境 源代码添加打包工程,setup,重新编译.![添加setup ](https://img-blo…

vue3项目中让echarts适应div的大小变化,跟随div的大小改变图表大小

目录如下 我的项目环境如下利用element-resize-detector插件监听元素大小变化element-resize-detector插件的用法完整代码如下&#xff1a;结果如下 在做项目的时候&#xff0c;经常会使用到echarts&#xff0c;特别是在做一些大屏项目的时候。有时候我们是需要根据div的大小改…

一文说清楚仿真与数字孪生的关系

获取更多资讯&#xff0c;赶快关注上面的公众号吧&#xff01; 文章目录 何为仿真何为数字孪生 最近看群里的小伙伴在疯狂讨论数字孪生&#xff0c;今天我也谈谈自己的理解。 之前还在北航读博的时候&#xff0c;北航陶飞教授已经算是数字孪生领域的领军人物&#xff0c;也专门…

【C++】2024.01.29 克隆机

题目描述 有一台神奇的克隆机&#xff0c;可以克隆任何东西。将样品放进克隆机&#xff0c;可以克隆出一份一样的“复制品”。小明得到了 k 种珍贵的植物种子&#xff0c;依次用 A,B,C,D,...,Z 表示&#xff08;1≤k≤26&#xff09;。一开始&#xff0c;每种植物种子只有…

PyFlink使用教程,Flink,Python,Java

环境准备 环境要求 Java 11 Python 3.7, 3.8, 3.9 or 3.10文档&#xff1a;https://nightlies.apache.org/flink/flink-docs-release-1.17/zh/docs/dev/python/installation/ 打开 Anaconda3 Prompt > java -version java version "11.0.22" 2024-01-16 LTS J…

信息安全考证攻略

&#x1f525;在信息安全领域&#xff0c;拥有相关的证书不仅能提升自己的专业技能&#xff0c;更能为职业生涯增添不少光彩。下面为大家盘点了一些国内外实用的信息安全证书&#xff0c;让你一睹为快&#xff01; &#x1f31f;国内证书&#xff08;认证机构&#xff1a;中国信…

网工,这才是跳纤的正确姿势!

晚上好&#xff0c;我的网工朋友。 当你们看到下面这张图&#xff0c;内心是什么感想&#xff1f; 这时你是不是巴不得把所有线全部拔了&#xff0c;来重新整一遍哈哈哈哈。那话说到这&#xff0c;到底该如何跳纤呢&#xff1f;有没有什么秘诀呢&#xff1f;遵循什么原则&#…

GLOBALCHIP GC3909Pin to Pin兼容A3909/allegro电机驱动芯片产品参数分析,应用于摇头机,舞台灯,打印机,白色家电等

GLOBALCHIP GC3909 12V H 桥驱动器芯片替代A3909/Allegro产品概述: GC3909是一款双通道12V直流电机驱动芯片&#xff0c;为摄像机、消费类产品、玩具和其他低压或者电池供电的运动控制类应用提供了集成的电机驱动解决方案。芯片一般用来驱动两个直流电机或者驱动一个步进电机。…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之DataPanel组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之DataPanel组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、DataPanel组件 数据面板组件&#xff0c;用于将多个数据占比情况使用占比图进…

网络安全全栈培训笔记(59-服务攻防-中间件安全CVE复现lSApacheTomcataNginx)

第59天 服务攻防-中间件安全&CVE复现&lS&Apache&Tomcata&Nginx 知识点&#xff1a; 中间件及框架列表&#xff1a; lIS,Apache,Nginx,Tomcat,Docker,Weblogic,JBoos,WebSphere,Jenkins, GlassFish,Jira,Struts2,Laravel,Solr,Shiro,Thinkphp,Sprng,Flask,…

Linux实验记录:使用iptables

前言&#xff1a; 本文是一篇关于Linux系统初学者的实验记录。 参考书籍&#xff1a;《Linux就该这么学》 实验环境&#xff1a; VmwareWorkStation 17——虚拟机软件 RedHatEnterpriseLinux[RHEL]8——红帽操作系统 备注&#xff1a; 防火墙作为公网与内网的屏障&#…

【linux】磁盘空间不足-常用排查和处理命令

【linux】磁盘空间不足-常用排查和处理命令 1.通查一下 df -h #查看服务器磁盘空间情况 du -hs * 2>/dev/null #列出各目录所占空间大小 或 du -h -d 1 2>/dev/null #列出各目录所占空间大小 1.1情况一 df 磁盘空间和du 目录空间占用相等&#xff0c…

C++中 this指针、构造函数、析构函数

1.this指针 我们定义一个日期类来举例子 对于上述类&#xff0c;有这样一个问题&#xff0c;Date类中有Init和Print这两个成员函数&#xff0c;函数体中没有关于不同对象的区分&#xff0c;那d1调用函数的时候&#xff0c;编译器是如和来确定d1而不是d2呢&#xff1f;C通过引入…
最新文章