《类和对象》(下篇)

news2024/11/10 18:43:02

本文主要讲解类和对象的一些其他小知识。

文章目录

  • 前情回顾
    • 一、用运算符重载写一个输入流和输出流
      • ①流插入
      • ②流提取
      • ③流提取和流插入的优化
    • 二、const成员
    • 三、用运算符重载改变数组
  • 1、再谈构造函数
    • 1.1 构造函数体赋值(不相当于初始化)
    • 1.2 初始化列表
      • ①引出初始化列表
      • ②怎么用初始化列表
      • ③初始化列表的规定
  • 2、 explicit关键字
  • 3、 static成员
    • 面试题:
      • ①方法一:创建一个全局变量。
      • ②方法二:static成员
      • ③方法三:static成员函数
      • 静态成员变量小语法
  • 4、 匿名对象
  • 5、 友元
    • 3.1 友元函数
    • 3.2 友元类
  • 6、 内部类
  • 7、 编译器的优化


前情回顾

关于上节的日期类,我们使用了运算符重载实现了流插入和流提取,这里我们详细实现以下。

一、用运算符重载写一个输入流和输出流

①流插入

我们C++中我们输入流和输出流就是用cincout。
他们最大的特点就是能够自动识别类型。

我们平常用的cin>>dcout<<d,这个其实就是运算符重载,<<>>就是运算符。

我们要想看明白下面的内容,理解这个是必要的。
cincout其实就是类的对象。
同时大家需要知道ostreamcout对象的类。
istreamcin对象的类

为什们能够支持自动识别类型?
在这里插入图片描述
支持自动识别,是因为库里面已经帮我们写了,并且支持 函数重载。当我们写不同类型的时候我们就会调用不同的函数。
所以我们写的 cout << d其实就是 cout.operator <<(int),通过你传的参,去识别类型。

如果我们想要输入输出一个自定义类型,但是对于自定义类型来说,编译器并不能自动识别,所以需要我们自己用运算符重载一个。

//输出流,放到我们上面的日期类中。
void Date::operator<<(ostream& out)
{
    out << _year << "年" << _month << "月" << _day << "日"<< endl;
}

但是我们使用的时候,对于我们写的这种类型,不能像cout << d这样使用,这样会报错。(d是我们日期类的对象)

利用我们现有的知识,对于运算符重载规定
第一个参数是左操作数,第二个参数是右操作数,
如果是在类中,那么传的时候会将第一个参数省略,传的是this指针,
对于我们这个函数,this指的是d,第二个参数才是cout
但是如果cout << d这样写,传第一个参数就是cout,第二个参数是d
所以我们应该这样写。
d.operator>>(cout);,也可以这样d1 << cout;

但是这样写跟我们想的完全不一样。
所以我们这时可以不用将我们的运算符重载放到我们的类中。
可以将我们的运算符重载放到全局。
按照需要的参数顺序传参,就没有this指针了。
具体传参为 operator<<(cout, d1);或 cout << d1;
但是可能面对的问题就是不能访问私有的成员变量。这个其实有办法解决。

//加返回值是为了实现连续输出
//为了实现这种场景	cout << d1 <<d2 << endl;
//定义
ostream& operator<<(ostream& out,const Date& d)
{
    out << _year << "年" << _month << "月" << _day << "日" << endl;
    return out;
}

访问私有成员变量的方法:
友元函数
具体做法就是将我们这个函数的声明放到我们的类中,在前面加上 friend。
下面会详细介绍,如果不懂可直接跳到下面的友元函数。

流提取的运算符重载

//声明
class Date
{
    //友元函数的声明
    friend ostream& operator<<(ostream& out, const Date& d);
public:
	//成员函数
private:
    int _year;
    int _month;
    int _day;
};
//在类外面定义
ostream& operator<<(ostream& out,const Date& d)
{
    out << _year << "年" << _month << "月" << _day << "日" << endl;
    return out;
}

在类里面声明前面加上friend就可以访问类内成员变量了

②流提取

跟上面一样,将运算符重载定义,放到全局中。
并且有返回值,是为了能够多重打印。
cout << d1 <<d2 << endl;

流提取在类中声明,友元函数的声明

    friend istream& operator>>(istream& in,Date& d);

流提取的运算符重载

istream& operator>>(istream& in, Date& d)
{
    
    in >> d._year >> d._month >> d._day;
    return in;
}

③流提取和流插入的优化

因为流插入和流提取的运算符重载都很简单,所以我们可以将他们变成内联函数,然后都放到头文件中。

内联函数,声明和定义都必须放在同文件中,不能将声明和定义分开。

class Date
{
    
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
public:
  
private:
    int _year;
    int _month;
    int _day;
};
inline ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;

    return out;
}
inline istream& operator>>(istream& in, Date& d)
{
    
    in >> d._year >> d._month >> d._day;
    return in;
}

二、const成员

我们先看一个例子

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a = 10;
};

int main()
{
	const A aa;
	aa.Print();
	return 0;
}

我们的程序出现错误。其实这涉及到一个问题,权限的放大。
我们定义了一个对象aa,但是我们把他设置为了const
此时由于对象在调用函数的时候会将对象的地址传递给this指针
&aa的类型为const A*,(*this不能改)
但是this指针的默认类型为 A* const this,虽然this指针不能改,但是*this可以改

所以要解决这个问题,就是将this设置为const,
void Print() const,规定const修饰*this.注意修饰的并不是this

class A
{
public:
	void Print() const
	{
		cout << _a << endl;
	}
private:
	int _a = 10;
};

int main()
{
	const A aa;
	aa.Print();
	return 0;
}

所以我们以后内部不改变成员变量的成员函数就用const修饰,const对象和普通对象都可以调用。

三、用运算符重载改变数组

//用运算符重载改变数组
//静态顺序表
class A
{
public:
	//1:运算符重载[],使其可以对自定义类型使用--可以写
	int& operator[](int i)
	{
		assert(i < 10);
		return _a[i];
	}

	//2:类对象是const定义的不能改变的--重载一个
	const int& operator[](int i) const
	{
		assert(i < 10);
		return _a[i];
	}



private:
	int _a[10];
};

void Fun(const A& aa)
{
	for (int i = 0; i < 10; i++)
	{
		cout << aa[i] << " ";
	}
}
int main()
{
	A x;
	for(int i = 0; i < 10; i++)
	{
		x[i] = i * 2;
	}

	for (int i = 0; i < 10; i++)
	{
		cout << x[i] << " ";
	}
	cout << endl;

	Fun(x); //传的是const

	return 0;
}




1、再谈构造函数

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

int main()
{
	Date a;//对象整体的定义
}

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始
化一次,而构造函数体内可以多次赋值

1.2 初始化列表

①引出初始化列表

我们知道对象实例化的时候(Date a;),是对象整体的定义。
这时就会有一个问题,每个成员什们时候定义的呢?
这时有人就会说,不就是在整体定义的时候就将其成员定义了吗?
其实话说是这样说,但是类好像并不是。
我举个例子

int main()
{
	//判断是否正确
	const int i;
	return 0;
}

我们看上面的代码,我们看他是典型的错误。
i被const修饰所以它具有常性,在定义完之后就不可在修改了,如果你不在定义的时候初始化,那么以后他将没有机会初始化了。

结论:const修饰的变量必须在定义的时候初始化。
那我们看下面的类

class A
{
public:
	
private:
	//这个只是声明,并不是定义
	const int _x;
};


int main()
{
	A a;//对象整体的定义

	return 0;
}

首先上面这个程序是错误的。
对于这个类,成员变量是常变量,我们必须在定义的时候初始化,但是声明也不能初始化。对象整体的定义的时候也没有初始化。

所以就抛出了下面这个问题。

每个成员什们时候定义的呢?

  1. 第一种,其实我们在上面默认构造函数的时候说了,他对内置类型不做处理,其实是一个bug,在C++11,这时打了一个补丁。
    用在声明的时候用缺省参数可以很好的解决问题。在调用默认构造函数的时候就用这个缺省参数定义了。具体看下面代码。
class A
{
public:
	
private:
	//用缺省参数
	const int _x = 1;
};

int main()
{
	A a;//对象整体的定义

	return 0;
}

  1. 但是上面这种方法是在2011年,才打的补丁,C++在98年就出来,并且就有这个问题,那么在这段期间是这么解决的呢?
    其实就是用到了初始化列表。

②怎么用初始化列表

因为上面的问题,所以必须给每个成员变量找一个定义的位置,否则就会像const的变量就没有办法初始化。

对于对象来说,初始化列表是成员变量定义的地方。

初始化列表:

  1. 初始化列表的位置,在构造函数名和函数体之间
  2. 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
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成员变量
    ③自定义类型成员(且该类没有默认构造函数时)
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
    一定会先使用初始化列表初始化。
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
    次序无关

接下来我们一个一个解释
🐯特点1:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

class A
{
public:
	A()
		:_x(1)
		,_a2(1)
		,_a2(2)//错误的代码不能出现两次,只能出现一次。
	{
		_a1++;
		_a2++;
	}
private:
	//用缺省参数
	int _a1 = 1;
	int _a2 = 2;
	const int _x;
};


🐯特点2
类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(且该类没有默认构造函数时)

对于前两个还比较好理解,引用如果不初始化,就会产生一个问题,他是谁的别名,这个就非常奇怪。

下来我们详细看一下为什们第三个不可以。

class B
{
public:
	B()
		:b(1)
	{

	}
private:
	int b;
};

class A
{
public:
	A()
		:_x(1)
		,_a2(1)
		//引用的初始化
		,ret(_a1) //表示ret是_a1的引用。
	{
		_a1++;
		_a2++;
	}
private:
	//用缺省参数
	int _a1 = 1;
	int _a2 = 2;
	const int _x;
	int& ret;
	B _bb;
};

首先上面这个程序没有任何问题
这时有人会问,为什们没有在初始化列表中让自定义类型初始化,怎么他还是对着呢?

我们第三点,规定的是没有默认构造的自定义类型,我们必须初始化。

但是类B,它有默认构造函数(上面将默认构造函数写了:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数),我们对于自定义类型,会调用他的默认构造函数,将其初始化,这是默认构造函数对于自定义类型的特点。
所以这是没有问题的,B类的对象_bb调用自己的默认构造函数初始化了。

再看下面例子,判断是否正确。

class B
{
public:
	B(int)
		:b(1)
	{

	}
private:
	int b;
};

class A
{
public:
	A()
		:_x(1)
		,_a2(1)
		//引用的初始化
		,ret(_a1) //表示ret是_a1的引用。
	{
		_a1++;
		_a2++;
	}
private:
	//用缺省参数
	int _a1 = 1;
	int _a2 = 2;
	const int _x;
	int& ret;
	B _bb;
};

int main()
{
	A a;//对象整体的定义

	return 0;
}


先说答案:上面的代码是错的,
对于自定义类型B,他没有默认构造函数,但有构造函数。
但是这个构造函数它既不是全缺省,也不是无参构造函数,并且他写了,系统也不会自动生成默认构造函数。

因此,这个类对象B既无法调用默认的构造函数(因为显示定义了一个有参的),也无法调用显示定义的构造函数(因为类型不匹配,没有给参数),无法完成初始化

所以我们在初始化列表中,我们必须将无默认构造函数的自定义类型初始化。
初始化过程看下面代码

class B
{
public:
	B(int)
		:b(1)
	{}
private:
	int b;
};

class A
{
public:
	A()
		:_bb(2) //可以随意给值,有了参数就可以调用它的构造函数。
	{}
private:
	B _bb;
};

int main()
{
	A a;//对象整体的定义

	return 0;
}

🐯特点3不管是否在初始化列表写,编译器的每个变量都会走初始化列表,在初始化列表定义和初始化

class A
{
public:
	A()
		:_x(1)
		,_a2(1)
	{
		_a1++;
		_a2++;
	}
private:
	//用缺省参数
	int _a1 = 1;
	int _a2 = 2;
	const int _x;
};

int main()
{
	A a;//对象整体的定义

	return 0;
}

答案是:a1=2,a2=2,x=1;

虽然我们利用缺省参数对a2初始化为2了,
但是不管你是不是在初始化列表初始化,我们的编译器的每个变量都会走一遍初始化列表
初始化列表里面我们又将a2重新初始化为1了*
再进入函数体后,a2++后为2

🐯特点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先声明,所以走初始化列表时候先初始化a2,不管你初始化列表前面有多少
但是我们的a2是用a1初始化的,我们的a1由于还没有初始化,所以是随机值,所以我们的a2=随机值
然后再使用a = 1来初始化a1

2、 explicit关键字

总结:用explicit修饰构造函数,将会禁止构造函数的隐式转换

构造函数不仅可以构造与初始化对象,
对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
	// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
			:_year(year)
			{}
	
	/*
	// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
	有类型转换作用
	// explicit修饰构造函数,禁止类型转换
	explicit Date(int year, int month = 1, int day = 1)
			: _year(year)
			, _month(month)
			, _day(day)
			{}
	*/
	Date& operator=(const Date& d)
	{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1(2022);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个无名的Date类对象,最后用无名对象给d1对象进行赋值(调用拷贝构造)
	d1 = 2023;
	// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转
	换的作用
}

3、 static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量
static修饰的成员函数,称之为静态成员函数

注意:静态成员变量一定要在类外进行初始化

初始化的方式:
类型+ 类名::变量 = 0;
int A::count = 0;

面试题:

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

//这是我们的类,计算主函数中,我们一共创建了几个类。
class A
{
public:
	//构造函数
	A(int a = 0)
	{}
	//拷贝构造
	A(const A&aa)
	{}
};

void Func(A a)
{}
int main()
{
	A aa1;
	A aa2(aa1);
	Func(aa1);
	A aa3 = 1;
	return 0;
}

分析:我们创造键一个类。
他只有两条路,一条是走构造函数,一条是走拷贝构造函数。
所以我们只要计算我们进行了几次构造函数和拷贝构造函数即可。

①方法一:创建一个全局变量。

//全局变量
int n = 0;
class A
{
public:
	//构造函数
	A(int a = 0)
	{
		n++;
	}
	//拷贝构造
	A(const A&aa)
	{
		n++;
	}
};

void Func(A a)
{}
int main()
{
	//构造函数一次
	A aa1;
	//拷贝构造一次
	A aa2(aa1);
	//进行了一次拷贝构造
	Func(aa1);
	//隐式类型转换优化为一次构造函数
	A aa3 = 1;
	cout << n << endl;
	return 0;
}

这种方式可行,但是这种方法的缺陷还是太明显,我们n是全局变量,我们在主函数里随便可以随便修改,一改就变了,结果就会不对。

②方法二:static成员

什么是 static成员?

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

注意:静态成员变量 ,不属于某个对象,属于所有对象,属于整个类.

为什们静态成员变量一定要在类外进行初始化?

我们在类中初始化的必须是类的对象,但是静态成员变量不属于某个对象,所以必须类外初始化

class A
{
public:
private:
	//不属于某个对象,属于所有对象,属于整个类
	static int count;//声明
};

int A::count = 0;//定义初始化

有了这个知识,我们还需要一个GetCount()函数获取count,
具体代码如下

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		count++;
	}
	//拷贝构造
	A(const A&aa)
	{
		count++;
	}
	int GetCount()
	{
		//在这个函数中不受访问限定符是限制
		//如果在外面直接调用,就可以拿到count

		//只可以读,不可以写
		return count;
	}
private:
	//不属于某个对象,属于所有对象,属于整个类
	static int count;//声明
};

int A::count = 0;//定义初始化

void Func(A a)
{}
int main()
{
	//构造函数一次
	A aa1;
	//拷贝构造一次
	A aa2(aa1);
	//进行了一次拷贝构造
	Func(aa1);
	//隐式类型转换优化为一次构造函数
	A aa3 = 1;
	A aa4[10];
	cout << aa3.GetCount() << endl;
	return 0;
}

但是有一个缺陷,他在最后调用的时候,只用用我们的创建的对象才能进行调用(因为我们把他定义为了成员函数),这样是不是感觉很难受。
如果我们没有创建对象,想去调用好像没有办法

这是就有了静态成员函数。

③方法三:static成员函数

我们想要调用我们的成员函数,必须先创建一个对象,再通过Get函数得到私有 的成员变量。

我们不想要我们的通过创建一个对象来调用函数从而得到数据,那我们有没有什么方法呢?

其实我们的静态成员函数就可以很好的帮我们解决这个问题。


static成员函数怎么用?

就是在成员函数前加一个 static,就变成了静态成员函数。


static成员函数的特点

就是没有 this指针,我们知道了域名,就可以直接调用。


为什们没有this指针,就可以直接调用?

我们知道,类中的成员函数都有一个隐藏的this指针,可以帮我们指向对应的成员变量。
所以我们在传参的时候,我们必须传一个this指针,就像 aa3.GetCount(),它的this指针就是aa3对象的指针。
所以静态成员函数突破了这个限制,在传参的时候不传this,就可以用域名找到。


static成员函数能不能调用我们的非静态成员变量?

答案肯定是不可以,
因为调用我们的成员变量,都是用 this指针进行调用,但是我们上面说了静态成员函数没有 this,肯定就不可以调用呀


什么时候用 static成员函数?

当我们需要访问静态成员变量时,并且可以用域名直接找到静态成员函数。

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		count++;
	}
	//拷贝构造
	A(const A&aa)
	{
		count++;
	}
	//静态成员函数
	static int GetCount()
	{
		return count;
	}
private:
	//不属于某个对象,属于所有对象,属于整个类
	static int count;//声明
};

int A::count = 0;//定义初始化

void Func(A a)
{}
int main()
{
	//构造函数一次
	A aa1;
	//拷贝构造一次
	A aa2(aa1);
	//进行了一次拷贝构造
	Func(aa1);
	//隐式类型转换优化为一次构造函数
	A aa3 = 1;
	A aa4[10];
	cout << A::GetCount() << endl;
	return 0;
}

静态成员变量小语法

我们知道静态成员变量不能在定义的时候给缺省值,只能在函数外面定义。
因为静态成员变量属于整个对象,在初始化列表中并不定义静态成员变量。

但是这个小语法:如果是const整型的静态成员变量 就可以在定义用缺省值。而其他的不可以

class A
{
private:
	static const int count = 1;
};

4、 匿名对象

匿名对象的样式

类名直接加括号。例如:上面类的匿名对象是 A()。

匿名对象的特征

他没有名字,并且声明周期只在它这一行,一旦跳过这一行,直接析构。

举个例子:
对于上面的那个面试题,我们还可以对方法2改进一下,使用匿名对象调用成员函数,然后-1

class A
{
public:
	//构造函数
	A(int a = 0)
	{
		count++;
	}
	//拷贝构造
	A(const A&aa)
	{
		count++;
	}
	//
	 int GetCount()
	{
		return count;
	}
private:
	//不属于某个对象,属于所有对象,属于整个类
	static int count;//声明
};

int A::count = 0;//定义初始化

void Func()
{
	A aa4[10];
}
int main()
{
	Func();
	//这个就是匿名对象。
	cout << A().GetCount() -1 << endl;
	return 0;
}

5、 友元

友元:

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

友元分为:友元函数和友元类


3.1 友元函数

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

说明:

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

之前日期类的问题:重载operator<<,然后发现没办法将operator<<重载成成员函数

因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置this指针默认是第一个参数也就是左操作数了
但是实际使用中cout需要是第一个形参对象,才能正常使用。
所以要将operator<<重载成全局函数但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

将<<写成成员函数的形式(不符合常规调用)

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

解决办法:

将函数定义写在类外,然后使用frend关键字将函数在类内的任意位置声明,使其成为类的友元函数。

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

3.2 友元类

他是一个但单向关系,你的是我的,我的还是我的。
友元类的特征

  1. 友元关系是单向的,不具有交换性。
  2. 友元关系不能传递
  3. 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  4. 友元关系不能继承,在继承位置再给大家详细介绍。

对于下面这个DateTime的友元,所以Date可以访问Time,但是Time不能访问Date

class Time
{
	// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类 中的私有成员变量
    friend class Date;   
    
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;
};

对于特点:其实很好理解

class A
{
	friend class B;  //B是A友元
}

class B
{
	friend class C;//C是B的友元
}

class C
{
	
}

这里我们可以显而易见的看到,B可以访问A的所有成员,因为B在A里面声明了友元类,B是A的友元
C可以访问B的所有成员,因为C在B里面声明了友元类,C是B的友元

但是ABC是都是独立的类,不写友元类根本没办法访问

6、 内部类

概念如果一个类定义在另一个类的内部,这个内部类就叫做内部类
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员
外部类对内部类没有任何优越的访问权限。

注意内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性:

  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;//可以直接访问static成员
		cout << a.h << endl;//可以访问所有成员
		}
	};
};

int A::k = 1;
int main()
{
	A::B b;  //创建对象时候要写清楚域,因为B在A里面
	b.foo(A());
	return 0;
}

拥有内部类的类的大小

计算上面A类的大小。
答案是:4.
所以我们可以初步判断一下,A类的大小只有他自己,并没有计算B。

内部类,其实和将类定义到全局没有区别。他是独立的
用上面举个例子,类B是类A的内部类,但是类B是独立的,只是受域的限制,必须再A域中才能找到B。

7、 编译器的优化

优化的过程都在代码中的注释,大家好好研究

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 fun(A aa)
{

}
void func(const A& aa)
{

}
int main()
{
    //隐式类型转化
    //构造+拷贝构造+优化为==>构造
    A aa1 = 1;

    //传值传参
    //拷贝构造,没有优化
    fun(aa1);

    //构造+拷贝构造+优化为==>构造
    fun(2);

    //构造+拷贝构造+优化为==>构造
    fun(A(3));

    //引用传参
    //没有优化,啥都不调用
    func(aa1);

    //无优化,隐式转换为直接引用
    func(2);

    //无优化 就是构造完直接引用
    func(A(3));
    return 0;
}

再来一种形式

A function()
{
    //进行构造函数
    A aa;
    //在返回的时候进行拷贝构造
    return aa;
}
int main()
{
    function();

    //function()函数中一个构造,一个拷贝构造,
    //然后再一个拷贝构造
    //被优化为一个构造+拷贝构造。
    A aa1 = function();
}

因为上面的函数function()是两个步骤,编译器没办法优化

再来一种形式

A function2()
{
    //直接返回一个匿名对象。
    //进行一个构造函数+拷贝构造
    //被优化为一个构造
    return A();
}
int main()
{
    function2();

    //上面说函数被优化为一个构造
    //在下面那就进行一个构造+拷贝构造
    //被优化为==>一个构造
    A aa1 = function2();
}

优化总结:
对象返回总结

  1. 接受返回值对象,尽量拷贝构造方式接收,不要赋值接收
  2. 函数中返回对象时,尽量返回匿名对象函数传参优化(例如第二个例子,两步没办法优化)
  3. 尽量使用 const &传参。也就是引用传参

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

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

相关文章

GBDT+LR论文翻译

0.摘要 在线广告允许广告客户仅针对可衡量的用户响应进行出价和付费&#xff0c;例如广告点击。因此&#xff0c;点击预测系统是大多数在线广告系统的核心。伴随每日活跃用户超过7.5亿&#xff0c;活跃广告客户超过100万的情况&#xff0c;预测Facebook广告点击是一项具有挑战…

【题解】P4055 [JSOI2009] 游戏

link 题目大意 题目说得比较清楚。 题解 前置知识&#xff1a;二分图最大匹配、基础博弈论。 每个点只能走一次的四联通点阵&#xff0c;可以想到二分图匹配。 将其套路地奇偶分点&#xff0c;相邻两点连边&#xff08;显然不能为 #&#xff09;。 先求一个最大匹配。 …

单片机 0~10V 输出电路的实现

聊聊单片机实现 0~10V 输出电路的几种方案 ...... by 矜辰所致目录 前言一、MCU 的 DAC二、PWM 加滤波电路2.1 PWM 输出 DAC2.2 PWM 接滤波器的RC值选择说明2.3 0~ 3.3V PWM 输出 0 ~10V方案一&#xff1a;RC 滤波器方案二&#xff1a;三极管 三、专用转换芯片结语 前言 好…

操作系统(3.1)--处理机调度和作业

目录 一、处理机调度层次 1.高级调度(High Level Scheduling) 2.低级调度&#xff08;Low Level Scheduling) 3.中级调度(Intermediate Scheduling) 二、处理机调度算法的目标 1.处理机调度算法的共同目标 2.批处理系统的目标 3.分时系统的目标 三、批处理系统中的作业…

【k8s完整实战教程3】k8s集群部署kubesphere

系列文章&#xff1a;这个系列已完结&#xff0c;如对您有帮助&#xff0c;求点赞收藏评论。 读者寄语&#xff1a;再小的帆&#xff0c;也能远航&#xff01; 【k8s完整实战教程0】前言【k8s完整实战教程1】源码管理-Coding【k8s完整实战教程2】腾讯云搭建k8s托管集群【k8s完…

【C++】海量数据面试题

海量数据面试题 文章目录 海量数据面试题一、哈希切割二、位图应用1.给定100亿个整数&#xff0c;设计算法找到只出现一次的整数2.求两个文件交集3.在100亿个整数中找到出现次数不超过2次的所有整数 三、布隆过滤器1.求两文件交集&#xff08;近似算法&#xff09;2.求两文件交…

气传导和骨传导耳机哪个好?简单科普这两种蓝牙耳机

在生活中&#xff0c;我们经常会用到耳机&#xff0c;特别是在日常娱乐听歌、运动休闲、户外通勤的时候&#xff0c;一款舒适的耳机是必不可少的。 而最近几年&#xff0c;随着科技的发展&#xff0c;各大品牌也相继推出了各种类型的耳机&#xff0c;其中比较热门的就有气传导…

如何在电脑上使用wink一键高清优化短视频画质

如何在电脑上使用wink一键高清优化短视频画质 文章目录 如何在电脑上使用wink一键高清优化短视频画质1.软件简介1.1痛点1.2解决方案 2.实际操作2.1准备工作2.1.1下载雷电模拟器2.1.2下载wink 2.2.安装软件2.2.1安装雷电模拟器2.2.2在雷电模拟器中安装wink 2.3雷电模拟器基本设置…

软件测试实验:Junit单元测试

目录 前言 实验目的 实验内容 实验要求 实验过程 题目一 题目一测试结果 题目二 题目二实验结果 总结 前言 软件测试是软件开发过程中不可缺少的一个环节&#xff0c;它可以保证软件的质量和功能&#xff0c;发现并修复软件的缺陷和错误。软件测试分为多种类型&…

《数据结构》---术语篇

目录 前言: 一.术语 1.1数据 1.2数据结构 1.3逻辑结构和物理结构 二.数据类型和抽象数据类型 ​​​​​​​ ❤博主CSDN&#xff1a;啊苏要学习 ▶专栏分类&#xff1a;数据结构◀ 学习数据结构是一件有趣的事情&#xff0c;希望读者能在我的博文切实感受到&#xff0c…

Numpy从入门到精通——随机生成数组|特定生成数组|规则生成数组

这个专栏名为《Numpy从入门到精通》&#xff0c;顾名思义&#xff0c;是记录自己学习numpy的学习过程&#xff0c;也方便自己之后复盘&#xff01;为深度学习的进一步学习奠定基础&#xff01;希望能给大家带来帮助&#xff0c;爱睡觉的咋祝您生活愉快&#xff01; 这一篇介绍《…

Qt内存管理及泄露后定位到内存泄漏位置的方法

Qt内存管理机制 Qt使用对象父子关系进行内存管理。在创建类的对象时&#xff0c;为对象指定父对象指针。当父对象在某一时刻被销毁释放时&#xff0c;父对象会先遍历其所有的子对象&#xff0c;并逐个将子对象销毁释放。 Qt内存管理代码示例 QLabel *label new QLabel;这里…

【==是判断相等吗?---错辣】C++和JAVA中判断字符串值相等的区别

文章目录 先上结论C中stringJAVA中String回顾结论 参考文章&#xff1a;这里&#xff1b;这里&#xff1b;这里 先上结论 C中的string类型可以使用和!来判断两个字符串的值是否相等&#xff1b;而JAVA不行&#xff0c;JAVA中和!是用来判断两个字符串的地址是否相同&#xff08…

c++学习之类与对象3

目录 成员变量和函数的存储 this指针 this指针的工作原理 this指针的应用 const修饰的成员函数 友元 友元的语法 1.普通全局函数成为类的友元 2.类的某个成员函数作为另一个类的友元 整个类作为另一个类的友元 运算符重载 1 运算符重载的基本概念 2 重载加号运算符…

MySQL数据库学习笔记之存储引擎

存储引擎 MySQL体系结构 连接层 最上层是一些客户端和连接服务&#xff0c;主要完成一些类似于连接处理、授权认证、以及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。 服务层 第二层架构主要完成大多数的核心服务功能&#xff0c;如SQL接口&am…

【JavaScript】6.DOM

文章目录 DOM1. 简介2. 获取元素2.1 根据 ID 获取2.2 根据标签名获取2.3 通过 HTML5 新增的方法获取2.4 特殊元素获取 3. 事件基础3.1 事件概述3.2 事件三要素3.3 执行事件步骤 DOM 1. 简介 文档对象模型&#xff08;Document Object Model&#xff0c;简称 DOM&#xff09;&…

web自动化测试框架落地实施全过程-测试环境搭建 (Selenium+Python)

一、什么是web自动化测试? Web自动化测试是指使用自动化工具模拟用户在Web浏览器中执行的操作&#xff0c;通过编写脚本来自动化执行测试用例&#xff0c;以验证Web应用程序的功能、性能和兼容性等方面的质量。其主要目的是降低测试成本和时间&#xff0c;并提高测试效率和准…

LDAP未授权漏洞验证

因为工作需要&#xff0c;这里验证了下LDAP未授权。 以下是收集到的资料&#xff0c;最后是具体使用&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 更新 2&#xff09;连接ad域有两个地址&#xff1a; ldap://http://XXXXX.com:389 和 ldap://http://XXXXX.…

算法的时间复杂度和空间复杂度(2)

计算斐波那契递归Fib的时间复杂度&#xff1f; long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) Fib(N-2); } 因为递归先递推后回归&#xff0c;看起来规律像等比数列&#xff0c;也可以用错位相减法&#xff0c;因为斐波那契数列到第二项就不会再计算了&a…

传输层重点协议之【UDP协议】

1. UDP协议端格式 2. UDP的特点 2.1 无连接 知道对端的IP和端口号就直接传输&#xff0c;不需要建立连接 2.2 不可靠 没有任何的安全机制&#xff0c;发送端发送数据报后&#xff0c;如果因为网络故障数据报无法发送对方&#xff0c;UDP协议层也不会给应用层返回任何错误信…