类与对象下篇

前言

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

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

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

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

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

类与对象下篇_第1张图片

这就需要知道构造函数的初始化列表,初始化列表可以理解成对象成员变量定义的位置,引用变量、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调试观察,和预期结果是否一致:

类与对象下篇_第2张图片

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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;
}

运行结果:

类与对象下篇_第3张图片

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

运行结果:

类与对象下篇_第4张图片

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

运行结果:

类与对象下篇_第5张图片

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引用会延长匿名对象的生命周期,生命周期在引用对象的当前函数作用域

你可能感兴趣的:(C++初阶,c++,数据结构)