详解c++---类和对象(四)

news2024/11/20 14:25:05

这里写目录标题

  • const成员
  • 再谈构造函数
    • 为什么会有初始化列表
      • 第一个问题
      • 第二个问题
      • 第三个问题
    • 初始化列表的使用方式即注意事项
  • explicit关键字
  • static成员
    • static修饰类中的变量
    • 一些性质
    • static修饰成员函数
  • 友元
    • 友元函数
    • 友元类
    • 内部类
    • 匿名对象
  • 拷贝对象时的一些编译器优化

const成员

在介绍const成员之前我们先来看一段代码:

#include<stdio.h>
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 12, 9);
	d1.print();
	return 0;
}

我们这里简单的创建了一个日期的类,这个类中含有三个成员变量并且还有一个构造函数和一个打印函数,然后我们就在main函数中创建了一个对象,并且调用构造函数进行初始化,然后调用类里面的打印函数进行打印,那么我们这里来看看这段代码的打印结果:
在这里插入图片描述
我们可以看到这里正常的打印了类中的三个数据的值,我们这里实例化出来的对象是普通的类型,但是在我们的生活中有些对象它是const修饰的,比如说我们下面的代码:

int main()
{
	Date d1(2022, 12, 9);
	d1.print();
	const Date d2(2022, 12, 9);
	d2.print();
	return 0;
}

这里的d2就是const修饰的,那这样我们再调用类中的print函数,他还能正常的运行吗?我们来看看这段代码运行的结果:
在这里插入图片描述
我们发现这里报出了错误,而且这个错误还和this指针有关,我们知道this指针的类型是类类型*const,并且指针在进行赋值的时候是会出现权限放大这个错误的,比如说下面的代码在编译的时候就会报出错误:

int main()
{
	const int a = 0;
	int* pa = &a;
	return 0;
}

错误的原因就是因为权限的放大:
在这里插入图片描述
那将const类型的地址赋值给一个const类型的指针的话会出现权限放大的问题吗?我们可以通过下面的代码来验证一下:

int main()
{
	const int a = 0;
	int* const pa = &a;
	return 0;
}

在这里插入图片描述
这里报错的原因也是因为权限的放大,那通过这两个权限放大的例子我们知道了调用print函数报错的原因是权限的放大,那我们解决这个问题的方法就是将this指针的类型由类类型*const改成修改成const类类型*const,这样就可以解决权限放大的问题,但是这里有个问题就是this指针的定义和传递都是编译器自己完成的,不由我们操作者来完成,那我们如何来对其进行修改呢?所以为了解决这个问题c++就引入了const成员这个内容:将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。其形式就是在函数的括号后面加个const,比如说上面的print函数在括号后面加个const就不会在报错啦:

	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}

再将上面的代码运行一下就可以发现这里并没有报错了:
在这里插入图片描述
那这就是const成员的作用以及介绍,这里大家要注意的一点就是我们在写成员函数的时候,能加const成员就加const成员,因为权限放大很容易被我们疏忽,所以我们在写函数的时候能加就加,比如说我们上篇文章中写的>操作符的重载,==的操作符重载这些不改变this指针指向的数据的函数都可以加const成员,那这就是const成员的内容。

再谈构造函数

为什么会有初始化列表

在解释为什么之前,我们首先来想几个问题,

第一个问题

首先第一个就是const修饰的变量我们能修改他的值吗?答案是可以的,但是修改的机会只有一次,就是在该变量定义的时候我们可以修改它的值,比如说我们下面的代码:

int main()
{
	const int a = 1;
	return 0;
}

我们在定义a的时候,可以对这个const修饰的变量进行赋值,但是在其他的地方我们是不能对a的值进行任何的修改,如果修改的话我们的编译器就会报错:
在这里插入图片描述
那看到这里我们就得想一个问题:类中的构造函数可以初始化const修饰的变量吗?那我们来看看下面的代码:

#include<stdio.h>
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	const int _year;
	const int _month;
	const int _day;
};
int main()
{
	Date d1(2022, 12, 9);
	d1.print();;
	return 0;
}

我们运行的时候就可以看到这里报出了错误:
在这里插入图片描述
那么这就说明我们之前学的构造函数是无法初始化const修饰的变量,所以遇到这种情况我们是得用到初始化列表,那这时有小伙伴就问我们这里可以使用缺省值来对初始化这里的const变量吗?答案是可以的,比如说我们下面的代码:

#include<stdio.h>
class Date
{
public:
	Date()
	{
		;
	}
	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	const int _year = 1;
	const int _month = 1;
	const int _day = 1;
};
int main()
{
	Date d1;
	d1.print();;
	return 0;
}

我们将这个代码运行一下就可以看到这里正常的运行下去:
在这里插入图片描述
但是这种方法比较单调,只能初始化为一种值,如果我们想将这里的数据全部都初始化为2的话,我们就只能在类中对这里的缺省值进行修改,而不是在实例化对象的时候通过传参来进行修改,所以要想真正的符合我们的日常所需,我们还是得用初始化列表来进行初始化,因为
在这里插入图片描述
这里只是我们声明的过程,变量真正定义的过程是在初始化列表,而唯一一次修改值得机会是在变量定义得时候。

第二个问题

我们知道引用只能在定义的时候来指明他想要指向的对象,所以当类中存在引用变量的时候我们就得用初始化列表来对其进行初始化,所以这也是为什么得有初始化列表的原因。

第三个问题

当我们在一个类中实例化了另外一个类的时候,我们编译器自动生成的默认构造函数会自动调用该类的默认构造函数来对这个对象进行初始化,比如我们下面的代码:

class Date
{
public:
	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	 int _year = 1;
	 int _month = 1;
	 int _day = 1;
};
class double_date
{
public:
	void print() const
	{
		d1.print();
		d2.print();
	}

private:
	Date d1;
	Date d2;
};
int main()
{
	double_date date1;
	date1.print();
	return 0;
}

我们这里又创建了一个类将这个类的名字称为double_date,然后在这个类里面创建了两个date类型的对象,然后我们这里没有写构造函数,编译器自己生成的默认构造函数会自动的调用date类里面的默认构造构造函数来初始化对象d1和d2,但是我们date类里面我们也没有写构造函数,所以这里编译器又会自动生成默认构造函数,但是好在我们这里给了缺省值所以d1和d2这两个类中的数据都不会是随机值,我们运行一下这里的代码就可以看到屏幕上打印出来的值都是1:
在这里插入图片描述
但是这里有个问题就是,如果date类里面没有默认构造函数呢?我们自己写了一个构造函数,并且必须要传参呢?比如说我们下面的代码:

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	 int _year = 1;
	 int _month = 1;
	 int _day = 1;
};

首先我们知道这种情况编译器自己生成的默认构造函数已经无法使用了,我们运行一下就可以看到这里的编译器报错了:
在这里插入图片描述
因为编译器自己生成的默认构造函数只能调用其他类中的默认构造函数,而这里date类中已经没有默认构造函数了所以就报错了,所以现在的问题就是如何在double_date这个类中调用date这个类中构造函数呢?要想解决这个问题我们就必须得使用初始化列表。

初始化列表的使用方式即注意事项

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。比如说Date类中的初始化列表就是这么写的:

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

double_date类中的初始化列表就可以这么写:

	double_date(int year1, int month1, int day1,
				int year2, int month2, int day2)
		:d1(year1, month1, day1)
		, d2(year2, month2, day2)
	{}

那么为了检验我们这里的double_date是否写的正确,我们可以通过下面的代码来检验一下:

int main()
{
	double_date date1(2022,11,11,2022,12,12);
	date1.print();
	return 0;
}

我们运行一下就可以看到这里的代码写的是真确的:
在这里插入图片描述
注意事项1:
大家观察这里的形势就可以发现在初始化列表的下面还有一个大括号,那么这个大括号就是构造函数的函数体,虽然初始化列表能够干很多函数体无法做到的实行,但是很多时候初始化列表和函数体是相辅相成的存在,比如说我们之前写的stack类中的构造函数就得初始化列表和函数体相符相承:

#include<iostream>
#include<stdlib.h>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 4)
		:_a((int*)malloc(sizeof(int)* capacity))
		, _top(0)
		,_capacity(capacity)
	{
		cout << "Stack(int capacity = )" <<capacity<<endl;
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

初始化列表负责开辟空间,而在函数体中则负责对这个新开的空间进行检查,检查其空间是否开辟成功。
注意事项2:
类中的每个成员都要走初始化列表,就算我们不显示在初始化列表中写,它也会走,显示写了就会采用你给他的值,如果没有显示写对于内置类型它就会初始化为随机值,对于自定义类型它就会调用它的默认构造函数,所以大家以后在写构造函数的时候能在初始化列表中进行初始化就在初始化列表中进行初始化,因为你不写它还是会走,如果你写了你还能够提高你的效率,以免浪费。
注意事项3:
尽量在类中提供默认构造函数,因为在其他类中可能会用到,并且构造函数尽量都是全缺省的。
注意事项4:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,比如说我们下面的代码:

class A
{
public:
    A(int a)
       :_a1(a)
       ,_a2(_a1)
   {}
    
    void Print() {
        cout<<_a1<<" "<<_a2<<endl;
   }
private:
    int _a2;
    int _a1;
};
int main() {
    A aa(1);
    aa.Print();
}

我们将这个代码运行一下就可以发现这里的_a1的值是1,而_a2的值却是一个随机值,那么这里的原因就是因为这里是先初始化_a2再初始化_a1,而在初始化_a2的时候_a1还是随机值,所以这就是为什么a2的值是一个随机值的原因,我们来看看这个代码的执行结果:
在这里插入图片描述

explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。我们将上面日期类的成员变量进行简化,将三个成员变量简化为一个成员变量,其代码如下:

class Date
{
public:
	Date(int year)
		:_year(year)
	{}
	void print()
	{
		printf("year = %d", _year);
	}
private:
	int _year = 1;
};

那么这时我们在对这个变量进行实例化的时候就只用给一个参数,比如说我们下面的代码:

int main()
{
	Date d1(2022);
	d1.print();
	return 0;
}

代码执行的结果就如下:
在这里插入图片描述
但是大家在学习的时候一定看到过这样的实例化方式:

int main()
{
	Date d1(2022);
	d1.print();
	Date d2 = 2023;
	d2.print();
	return 0;
}

他这里直接采用等于号的方式来实例化一个对象,并且我们运行一下发现这里竟然还能运行成功:
在这里插入图片描述
那么之所以可以这样的原因是因为这里并不是将2023这个值赋值给这个对象,而是用这个2023构造出来一个临时对象,再用这个临时对象来拷贝构造这里的d2,所以这一个=的背后其实是有两个过程,一个是构造另外一个就是拷贝构造,但是这是对于一些古董编译器是这么进行的,对于很多现在的编译器里都会将这两个步骤优化为一个步骤直接进行构造,那么这里为了验证这一过程,我们就可以看看我们这个编译器(vs2022)是否使用了这样的优化,我们在构造函数里面加一个打印的语句来作为标记,并且自己再写一个拷贝构造函数在里面也加一个打印语句用来作为标记,其代码如下:

class Date
{
public:
	Date(int year)
		:_year(year)
	{
		cout << "构造函数" << endl;
	}
	Date(const Date& d1)
	{
		cout << "拷贝构造" << endl;
		_year = d1._year;
	}
	void print() const
	{
		printf("year = %d\n", _year);
	}
private:
	int _year = 1;
};
int main()
{

	Date d2 = 2023;
	d2.print();
	return 0;
}

我们运行一下就可以看到这里的结果就是这样:
在这里插入图片描述
并没有打印拷贝构造这几个字,说明我们的编译器这里是有优化的,那么看到这里大家应该能够想到这个过程其实是和变量的赋值的过程是类似的,比如说将一个double类型的变量强制类型转换为int类型在赋值给另外一个变量,那么这个过程他也是会创建一个临时变量出来的,用的这个临时变量来进行接下来的赋值,但是这个临时变量是有常性的,如果我们这里用引用的话是得加const来进行修饰的,我们之前讲过,那这里类跟上面的类似那我们在用引用的时候是不是也得加const来进行修饰呢?答案是肯定的比如说我们下面的代码:

int main()
{

	const Date& d2 = 2023;
	d2.print();
	return 0;
}

那么我们就称上述过程为隐式类型转换,如果大家以后在写代码的时候不想出现这种现象的话我们就可以采取在构造函数的前面加上一个explicit这个关键字来修饰这样他就不会出现上诉隐式类型转换的这种情况,那么这就是explicit关键字的作用,当然对于单参数的类型可以实现这样的隐式类型转换,在后序的语法中(c++11就支持了)对于多参数的情况也是可以的,但是我们得把多个参数用一个大括号括起来这样就可以实现多个参数的隐式类型转换,比如我们下面的代码:

class Date
{
public:
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "构造函数" << endl;
	}
	Date(const Date& d1)
	{
		cout << "拷贝构造" << endl;
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}
	void print() const
	{
		printf("year = %d\n", _year);
		printf("month = %d\n", _month);
		printf("day = %d\n", _day);
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1 = { 2022,12,12 };
	d1.print();
	return 0;
}

将其运行一下就可以发现这里确实是可以实现多个参数的隐式类型转换的:
在这里插入图片描述
那么这就是explicit的用法希望大家能够理解。

static成员

static修饰类中的变量

在讲这个之前我们首先来想一个问题就是如何实现统计调用了多少次构造函数和拷贝构造函数的次数这个功能,那这里有小伙伴就要说了这还不简单,我们直接创建一个全局变量N将其值初始化为0,在构造函数和拷贝构造函数里面添加一个代码N++,这样我们每次调用构造函数或者拷贝构造的时候全局变量N的值就会加一,这样在程序的末尾我们就可以通过观察变量N的值来判断一共调用了多少次拷贝构造和构造函数,比如说我们下面的代码:

int N = 0;
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "构造函数" << endl;
		N++;
	}
	Date(const Date & d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "拷贝构造" << endl;
		N++;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 11, 11);
	Date d2(d1);
	printf("%d", N);
	return 0;
}

我们这里创建出来了两个对象,一个采用的是拷贝构造一个采用的是构造,所以我们这里打印出来的值应该就是2 :
在这里插入图片描述
如果这里可以统计出来调用的构造函数和拷贝构造的次数的话,那么我们这里就可以用这个功能来看我们在传参的时候是否使用了这两个函数:

void test(Date d)
{
	;
}
int main()
{
	Date d1(2022, 11, 11);
	test(d1);
	printf("%d", N);
	return 0;
}

我们将这个代码运行一下就可以看到这里打印出来的结果就是N等于2,分别调用一次构造函数,一次拷贝构造,那么这就说明我们的函数在传参的时候是通过拷贝构造来进行传参的,而不是先构造出来一个临时对象再用这个临时对象来进行拷贝构造,那么传参过程有了优化,那传值返回也是这样的吗?我们再来看看下面的这个代码:

Date test()
{
	Date d1(2022, 11, 11);
	return d1;
}
int main()
{
	test();
	printf("%d", N);
	return 0;
}

我们来看看这个代码打印的结果:
在这里插入图片描述
我们发现依然是一个构造函数一个拷贝构造,这就说明函数在结束返回的时候也会对其进行相应的优化,那么看到这里我们就基本上实现了这个计数的功能,但是这里有个问题就是我们是通过全局变量来实现的这个功能,但是在c++当中我们是非常的不建议使用全局变量的,因为当我们定义了一个全局变量的时候我们整个程序都可以使用这个变量,这就导致稳定性降低,容易在其他的地方使用和修改这个变量的值,而导致程序错误,所以我们这里不采用全局变量的形式,我们使用static修饰的变量来记录这里的次数,但是这里的修饰又要分为好几个类型,一个是static修饰全局变量,一个是static修饰的局部变量,一个是static修饰的类中的变量,这三种不同类型的变量的生命周期都是整个程序,他们唯一的不同的就是生命周期不同,全局的static整个程序都可以使用,而局部的static只能在函数中使用,类中的static只能在类作用域使用,而我们这里的N他是用来记录类中调用了多少次构造函数和拷贝构造函数,所以我们这里只希望在类中能够使用到他,所以我们这里采用的就是在类中使用static修饰的N来记录这里的次数,而且这里还有个问题就是如果我们在类中创建static类型的变量的话我们得在类的外面实现初始化,并且还得指明类域和该变量的类型,那么这里我们的代码就如下:

class Date
{
public:
	static int N;
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "构造函数" << endl;
		N++;
	}
	Date(const Date & d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "拷贝构造" << endl;
		N++;
	}
private:
	int _year;
	int _month;
	int _day;
};
int Date::N = 0;

那么看到这里我们的该功能就算完成了,我们运行一下下面的代码就可以看到我们这里的实现没有啥问题:

Date test()
{
	Date d1(2022, 11, 11);
	return d1;
}
int main()
{
	test();
	cout << Date::N << endl;
	return 0;
}

在这里插入图片描述

一些性质

第一个:
大家通过上面的例子就可以知道,static修饰的变量所有由该类实例化出来的对象都可以使用,比如上面的这个代码我们明明是创建了两个对象使用的是两个不同的函数,但是加1却加到同一个N上去了。
第二个:
我们在构造函数里面不要初始化静态变量的值,因为我们每实例化出来一个对象都会调用构造函数,而类中的static变量又是所有对象中共享的,如果每次都改变必定会影响这里N的正确性。
第三个:
我们在main函数里面实例化出来的一些对象,这些对象里面是没有静态变量的,你想:这些变量创建的地方是在栈上,而static修饰的变量是在堆上,所以该对象中是肯定没有静态变量的,我们还可以通过计算类的大小来验证这里的结果,比如说下面的代码:

class A
{
public:
	static int a;
private:
	int b;
};
int A::a = 0;
int main()
{
	A a1;
	cout << sizeof(a1) <<  endl;
	return 0;
}

我们将这个代码运行一下就可以看到这里打印出来的值为:4
在这里插入图片描述
那这就说明对象a1中确实没有静态变量a。
第四点:
我们在类的外面访问这里的静态变量的时候,不仅可以通过类名访问到这里的变量,还可以通过该类实例化出来的对象来访问这里的静态变量,因为就算对象中没有静态变量,但是这个对象他也可以间接的代表类域,所以依然可以通过对象来访问到静态变量,比如说下面的代码:

int main()
{
	Date d1(2022, 11, 11);
	cout << Date::N << endl;
	cout << d1.N << endl;
	return 0;
}

我们就可以看到这里正常的打印出来了1和1
在这里插入图片描述

static修饰成员函数

通过上面的例子我们知道了static修饰的成员变量会有什么样的性质,那么在c++当中static不仅可以修饰变量,还可以修饰类中的成员函数,这些函数在修饰之后是没有this指针的,而我们知道编译器之所以能够区分是哪个对象调用的成员函数就是通过this指针的来实现,所以当一个类中的函数没有this指针的话,那么这也就说明我们在调用这个函数的时候可以无需通过对象来进行调用,比如说我们在Date的类中加一个函数,这个函数的作用就是获取这里的静态变量N的值,那么这里我们的代码就如下:

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "构造函数" << endl;
		N++;
	}
	Date(const Date & d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "拷贝构造" << endl;
		N++;
	}
	static int get_number_N()
	{
		return N;
	}
private:
	static int N;
	int _year;
	int _month;
	int _day;
};
int Date::N = 0;

那么这时我们想要调用这个该函数的话,我们就无需实例化对象出来而是直接的调用,比如说我们下面的代码:

int main()
{
	cout << Date::get_number_N() << endl;
	return 0;
}

但是如果我们这么做的话,对于该函数来说还是有一定的缺陷的就是static修饰的函数,它就只能访问到静态的数据,而不能访问到非静态的数据,比如说我们上面的静态函数get_number_N它就无法访问到非静态数据_year,_month等等。

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。

友元函数

之前我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的
输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作
数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,比如说我们下面的代码:

class Date
{
 friend ostream& operator<<(ostream& _cout, const Date& d);
 friend istream& operator>>(istream& _cin, Date& d);
public:
 Date(int year = 1900, int month = 1, int day = 1)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
private:
 int _year;
 int _month;
 int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
 _cout << d._year << "-" << d._month << "-" << d._day;
 return _cout; 
}
istream& operator>>(istream& _cin, Date& d)
{
 _cin >> d._year;
 _cin >> d._month;
 _cin >> d._day;
 return _cin;
}
int main()
{
 Date d;
 cin >> d;
 cout << d << endl;
 return 0;
}

我们将其运行一下来看看我们这里的代码写的是否是真确:
在这里插入图片描述
很显然我们这里的代码写的是真确的,然后我们再来看看友元类又是什么?那么这里有几个点需要大家注意一下的就是:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同

友元类

既然友元函数可以在类的外面访问到类中的私有数据,那么同样的道理友元类也可以在一个类的里面访问到另外一个类中的私有数据,比如说我们下面的代码:

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

但是这里大家要注意几点就是:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
但是友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。其次友元关系不能传递如果C是B的友元, B是A的友元,则不能说明C时A的友元。
友元关系不能继承,在继承位置再给大家详细介绍。

内部类

内部类的概念是概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。也就是说内部类他是一个白眼狼,他可以得到外部类的数据,但是外部类却无法得到他的任何数据,注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
内部类的特性为:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。

我们来看看下面的代码来理解理解内部类:

class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.foo(A());

	return 0;
}

我们将其运行一下就可以看到这里的b确实能够访问的A的私有数据,当然这里使用了匿名对象,我们来看看这段代码运行的结果为:
在这里插入图片描述

匿名对象

何为匿名对象?通过名字我们就知道匿名对象就是实例化出来的一个对象,但是这个对象却没有名字,比如说我们上面写的日期类,我们就可以这样创建对象:

int main()
{
	Date(2022, 11, 11);
	Date d1(2022, 12, 12);
	return 0;
}

这里的d1就是有名对象,而d1上面的就是匿名对象,对于匿名对象他的生命周期就只为一行,出了这一行他的生命周期就结束了,比如说上面的代码,当编译器执行到Date d1(2022, 12, 12);的时候,上面的匿名对象的生命周期就已经结束了编译器便会自动的调用析构函数来结束这个匿名对象,那么这个就是匿名对象的介绍,没啥复杂的东西。

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

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。
场景一:
这里的A是一个类的类型

A aa1 = 1;

那么根据上面的学习,我们知道这里是两个步骤,先是用1来构造出来一个临时对象,再用这个临时对象来拷贝构造这个aa1,但是这里就很明显这个构造出来的临时对象就是一个多余的步骤,所以编译器在这里就会做出一些简化,直接拿这个1来构造这个aa1.
场景二:

void f1(A aa)
{}
f1(A(1));  // 构造 + 拷贝构造  -> 优化 构造
f1(1);  

我们直接用匿名对象来进行传参,那么这样的话,这里的一步就会变成两个过程:先是构造,再是拷贝构造赋值给这里的形参aa,那么这样的话我们的编译器就会对其进行优化将其变成一步就是构造
场景三:

A f2()
{
	A aa;
	return aa;
}
	f2();	  // 构造+拷贝构造
	A ret = f2(); // 构造+拷贝构造+拷贝构造 ->优化 构造+拷贝构造

这里是传值返回,我们现在函数里面创建实例化了一个对象,再将这个对象进行传值返回,那么再传值返回的过程中,我们的编译器会拷贝构造出来一个临时对象,所以f2()实际上就是两个过程:构造+拷贝构造,当我们用一个变量来接收这里的函数返回值的时候,这里就会又多出一个步骤就是将函数的返回的那个临时对象拷贝构造给这里的ret,这里编译器就会发现创建出来的那个临时对象似乎没有什么用,所以就会直接优化为构造+拷贝构造。
场景四:

 f3()
{
	return A(10);
	
}
A ret = f3();  //  构造+拷贝构造+拷贝构造 -> 优化 -> 构造

当一个函数传值返回返回的是一个匿名对象的时候,编译器会将构造+拷贝构造+拷贝构造直接优化为构造。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【知识图谱】(task2)知识图谱表示

note 知识图谱的符号表示方法&#xff1a; 属性图是工业界最常见的图谱建模方法&#xff0c;属性图数据库充分利用图结构特点做了性能优化&#xff0c;实用度高&#xff0c;但不支持符号推理。RDF是W3C推动的语义数据交换标准与规范&#xff0c;有更严格的语义逻辑基础&#x…

dev_III笔记补充

1.在客户端显示form 思路&#xff1a; 想要在客户端显示form必须先把form放在 menu item中&#xff0c;然后通过url地址的方式打开form 步骤&#xff1a; 1.建立form 2.建立Menu Item &#xff08;特别注意&#xff1a;是display类型&#xff0c; 不是action类型 不是action类型…

【分布式技术专题】「架构设计方案」盘点和总结秒杀服务的功能设计及注意事项技术体系

秒杀应该考虑哪些问题 超卖问题 分析秒杀的业务场景&#xff0c;最重要的有一点就是超卖问题&#xff0c;假如备货只有100个&#xff0c;但是最终超卖了200&#xff0c;一般来讲秒杀系统的价格都比较低&#xff0c;如果超卖将严重影响公司的财产利益&#xff0c;因此首当其冲…

python爱心源代码集锦

python爱心源代码集锦 本文目录&#xff1a; 一、代码效果预览索引图 二、爱心源代码集锦 &#xff08;1&#xff09;、爱心图形1&#xff08;弧线型&#xff09;&#xff08;显示的文字写在代码里&#xff09; &#xff08;2&#xff09;、爱心图形2&#xff08;直线型&am…

MySQL 如何优化慢查询?

一、前言 在日常开发中&#xff0c;我们往往会给表加各种索引&#xff0c;来提高 MySQL 的检索效率。 但我们有时会遇到明明给字段加了索引&#xff0c;并没有走索引的Case。 进而导致 MySQL 产生慢查询。 严重场景下&#xff0c;甚至出现主从延迟、数据库拖垮的极端事故。 本…

微服务框架 SpringCloud微服务架构 28 数据同步 28.4 发送mq 消息

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构28 数据同步28.4 发送mq 消息28.4.1 直接开干28 数据同步 28.4 发送mq 消…

《Linux运维实战:使用Percona Backup for MongoDB逻辑备份与恢复Mongodb数据》

一、备份与恢复方案 Percona Backup for MongoDB 是一个开源、分布式和低影响的解决方案&#xff0c;用于MongoDB分片集群和副本集的一致备份。从版本1.7.0开始&#xff0c;Percona Backup for MongoDB支持物理和逻辑备份和恢复&#xff0c;仅支持对逻辑备份进行时间点恢复。 …

UIAutomator测试框架介绍

uiautomator简介 UiAutomator是Google提供的用来做安卓自动化测试的一个Java库&#xff0c;基于Accessibility服务。功能很强&#xff0c;可以对第三方App进行测试&#xff0c;获取屏幕上任意一个APP的任意一个控件属性&#xff0c;并对其进行任意操作&#xff0c;但有两个缺点…

软件测试面试笔试习题参考,你都会了吗?

目录 一、 简答题和应用题&#xff1a; 二、 填空题&#xff1a; 三、 判断题&#xff1a; 四、 选择题&#xff1a; 总结 重点&#xff1a;配套学习资料和视频教学 一、 简答题和应用题&#xff1a; 1. 什么是软件测试&#xff1f; 2. 比较软件测试过程和软件开发过程&a…

基于JavaScript中AES和MD5加密,以及简单二维码的生成

一、加密技术 1、对称加密&#xff1a;单密钥加密。一个密钥可以用来加密也可以用来解密 —- AES 2、非对称加密&#xff1a;有两把密码&#xff0c;公钥(用于加密)&#xff0c;私钥(用于解密) 3、摘要算法&#xff1a;把任意长度的输入&#xff0c;根据算法生成一串固定长度…

有环链表入口问题

有环链表入口问题 当快慢指针相遇时&#xff0c;我们可以判断到链表中有环&#xff0c;这时重新设定一个新指针指向链表的起点&#xff0c;且步长与慢指针一样为1&#xff0c;则慢指针与“新”指针相遇的地方就是环的入口。 图片来源:黑马程序员 证明: 设a为起点位置&#xff…

百变郁锦香,开创新典范,深化全球战略布局成就国际高端酒店品质之选

随着消费需求的不断升级&#xff0c;酒店消费场景也进行着多元化的发展&#xff0c;城市高端度假品牌正积极溯源消费需求&#xff0c;寻得品牌文化延伸的可靠路径。同时&#xff0c;各大酒店品牌也加快在市场布局的脚步&#xff0c;希望通过布局城市核心区域获得可持续发展的更…

Python绘制正二十面体

文章目录正二十面体的顶点绘制棱绘制面正二十面体的顶点 正20面体的12个顶点刚好可以分为三组&#xff0c;每一组都是一个符合黄金分割比例的长方形&#xff0c;而且这三个长方形是互相正交的。 所以&#xff0c;想绘制一个正二十面体是比较容易的 import numpy as np from …

大环配体配合物1407166-70-4,NODA-GA-NHS ester,NODA-GA-NHS 酯

●外观以及性质&#xff1a; NODA-GA-NHS ester产物呈固体或粘性液体&#xff0c;取决于PEG分子量&#xff0c;一般为白色固体&#xff0c;双功能大环化合物&#xff0c;大环配体配合物是指由多齿配体与环骨架上的O、N、P、S等多个配位原子形成的环配合物。 NODA-GA-NHS ester …

前端面试题2022-CSS篇

关于前端面试的题&#xff0c;最近整理了一些干货&#xff0c;经常被问到的一些问题&#xff0c;出现频率比较高的问题&#xff0c;如有不足之处&#xff0c;请高调指出&#xff0c;&#xff08;⭐代表难度&#xff0c;星星越多越难&#xff0c;以次类推&#xff09;&#xff0…

如何自动备份指定文件扩展名的文件?

关于文件扩展名 文件扩展名&#xff0c;一个点后跟几个字母&#xff0c;例如“.doc”或“.jpg”&#xff0c;构成计算机文档名称的结尾。保存文档时&#xff0c;请务必在单击“保存”之前输入文档名称和文件扩展名。 自动备份具有特定文件扩展名的文件 随着计算机的使用&…

第十章 鲁棒性检查(中)

文章目录10.5 时钟门控检查(Clock Gating Checks)高电平时钟门控(Active-High Clock Gating)10.5 时钟门控检查(Clock Gating Checks) 当一个门控信号&#xff08;gating signal&#xff09;可以控制逻辑单元中时钟信号&#xff08;clock signal&#xff09;的路径时&#xff…

最近面试遇到的面试题

先挖坑&#xff0c;有空再填坑&#xff01; 迭代器 生成器 常用高阶函数 MySQL 建索引几大原则 浅拷贝 深拷贝 区别 实现 Linux 进程名字 查看进程号 Linux 杀死一个进程 python Linux shell 脚本 数据库 主从复制 配置 nginx 权限设置 python 去除空行 pandas 保留最后一个空…

深入浅出 Swift 中的 some、any 关键字以及主关联类型(primary associated types)

问题现象 从 Swift 5.1 开始,Apple 陆续引入 some、any 关键字,并且从 Swift 5.7 开始对 any 关键字的用法做了增强,并且引入了主关联类型(primary associated types)的概念。 那么它们到底是什么意思?使用它们又能如何改进我们书写代码的范儿呢? 在本篇博文中,您将学…

自动依据你的数据库生成SQL练习题及答案,宝藏软件鉴赏

原创软件不易&#xff0c;方面多点赞、收藏、加关注鼓励&#xff01;后续领更多好用功能&#xff01; 引言 各位好&#xff0c;相信看见这篇文章的朋友&#xff0c;应该也去体验过了chatGPT了吧~&#xff0c;确实chatGPT拉近了我们与未来科技的距离&#xff0c;所有别人火也是…