C++ 类与对象(详细复习篇下)

  紧接着两个月前的类与对象的复习上篇,该篇博客即将讲解类与对象所剩下的所有内容,话不多说我们就此开始讲解该篇博客的知识。

1.4.5 隐藏的this指针

   对于类的非静态成员,每一个对象都有自己的一份复制,即每个对象都有自己的数据成员,不过成员函数却是每个类所共享的。那么我们调用共享的函数是如何寻找到自己的数据成员呢?那么该问题的解决就要利用到类中隐藏的this指针了。下面我们用一个例子来讲解类的隐藏指针的作用。

#include<iostream>
using namespace std;
class Cbook		//定义一个Cbook类 
{
	public:
		int m_pages;	//定义一个数据成员
		 
		void outputpages()	//定义一个成员函数 
		{
			cout<<m_pages<<endl;
		}	
};

int main()
{
	Cbook vbook,vcbook;		//定义两个类的对象 
	vbook.m_pages=520;		//给两个对象的数据成员赋值 
	vcbook.m_pages=1314;
	vbook.outputpages();	//调用两个对象的成员函数输出数据成员 
	vcbook.outputpages();
	return 0;
}

程序的运行结果如下图所示:

3c4ca0c7499f43128b71477699ae3d17.png

 从上述的输出结果来看,每一个对象都有自己的数据成员m_page,在调用其成员函数时输出的都是属于自己的数据成员,函数中也只是访问了m_page数据成员,每个对象在调用函数时通过指针区分自己的数据成员。在每个类的成员函数(非静态成员函数)中都隐藏包含一个this指针,只想被调用对象的指针,其类型为当前类类型的指针类型(即类的指针类型)在const方法中,为当前类类型的const指针类型。当vbook对象调用成员函数时,this指针指向vbook对象,剩下的一个对象调用this指针同理。在成员函数中,用户可以使用this指针访问数据成员。

void outputpages()

{

        cout<<this->m_pages<<endls;        //使用this指针访问数据成员

}

 实际上,编译器为了实现this指针的作用,在成员函数中自动添加了this指针对数据成员的方法,类似于上面成员函数方法。此外,为了将this指针指向当前调用的对象,并在成员函数中能够使用,编译器在每个成员函数中都隐藏了一个this指针作为函数参数,并在函数调用时将对象自身的地址隐含作为实际参数传递给this指针。以outputpages为例:

void outputpages(Cbook* this)        //隐含添加this指针

{

        cout<<this->m_pages<<endl;

}

在对象调用成员函数时,将对象的地址传递到成员函数中。以“vc.outputpages();"语句为例,编译器将解释为"vcbook.outputpages(&vcbook);",这就使得this指针合法,并能够在成员函数中使用。

1.4.5 嵌套类

  C++语言允许在一个类中定义另一个类,这被称之为嵌套类。例如。下面的代码在定义CList类时,在内部有定义一个嵌套类CNode。

#include<iostream>
using namespace std;
#define MAXLEN 128	//定义一个宏
class CList		//定义一个CList类 
{
	public:		//将嵌套类设置为公有的 
		class CNode		//定义嵌套类 
		{
			friend class CList;	//将CList类设置为自己的友元类 
			private:	//定义私有成员 
				int m_tag;
			public:		//定义共有数据成员 
				char m_name[MAXLEN];
		};	//CNode定义结束 
	public:		
		CNode m_node;	//定义一个CNode数据类型 
		void setnodename(const char *pchdata)	//定义成员函数 
		{
			if(pchdata != NULL)	//判断指针是否为空 
			{
				stcpy(m_node.m_name,pchdata);	//访问CNode类中的共有成员 
			}	
		}
		void setnodetag(int tag)	//定义成员函数 
		{
			m_node.m_tag=tag;	//访问CNode类中的私有成员 
		}
};

上述代码在嵌套类中定义了一个私有成员m_tag和共有成员m_name,对于外围的CList类来说虽然嵌套类是在它的内部定义的但是CList类仍然无法访问其嵌套类中的私有成员。上述代码在定义嵌套类的时候将CList类作为自己的友元类,这就使得CList类可以访问嵌套类中的私有成员m_tag。对于内部的嵌套类来说,只允许外围的类域中使用,在其他类域或者作用域中是不可见的(即只可以在其外围类中使用它)。例如下面的定义是非法的。

int main()

{

        CNode node;        //错误的定义,不能访问CNode类

        return 0;

}

在上述定义中出现CNode没有被声明的错误。对于main函数来说,嵌套类CNode是不可见的,但是可以通过使用外围的类域作为限定符来定义CNode对象。如下的定义是合法的。

int main()

{

        CList::CNode node;        //合法的定义

        return 0;

即访问时从最外层开始访问使用::来访问到里层 是合法的。

上述代码通过使用外围类域作为限定符访问到了CNode类。但是这样的做法通常是不合理的,也是有限制条件的。因为既然定义了嵌套类,通常都不允许在外界访问,这违背了使用嵌套类的原则。此外,在定义嵌套类时,如果将其定义为私有的或受保护的,及时使用外围类域作为限定符,外界也无法访问嵌套类。 

a6ecac048a344a0aa01e62982d52f527.png

ecf4b1fa18c74699971023f334cd86c3.png7aa9d8668b2442fa8a8debccb38c3ea4.png

 在此可以看出如果嵌套类为私有成员时,就算使用外围类的调用也不可以访问。

只用外围类来访问嵌套类的代码如下:

int main()
{
	CList c;
	char a[25];
	cin>>a;
	c.setnodetag(12);
	c.setnodename(a); 
	c.outputtag();
	c.outputname();
	return 0;
}

运行结果如下图所示:

3be1d474b1534884803ba3875f34c186.png

 当嵌套类为公有成员时使用外围类来在外界作用域中调用嵌套类。(将m_tag设置为公有成员)

int main()
{
	CList::CNode node;
	node.m_tag=10;
	node.outputtag();
}

运行的结果如下图所示,可以使用限定符在外界访问嵌套类里的内容。

5617a26bfc814bd687f07e21cd7f69e4.png

1.4.6 局部类

  类的定义可以放在头文件中,也可以放在源文件中。还有一种情况,将类的定义放置在函数中,这样的类被称之为局部类。(只可以在该函数中使用的类)

例如下面的示例:

#include<iostream>
using namespace std;
#include<cstring>
void Localclass()	//定义一个函数 
{
	class CBook		//定义一个局部类CBook 
	{
		private:
			int m_pages;	//定义一个私有成员 
		public:			//定义公有函数 
			void setpages(int page)
			{
				if(m_pages != page)
					m_pages = page;	//给数据成员函数赋值 
			}
			int getpages()
			{
				return m_pages;	//获取数据成员的信息 
			}
	};
	CBook c;	//定义一个类的对象 
	c.setpages(1225);	//调用函数进行赋值操作 
	cout << c.getpages() << endl;	//输出信息 
}

int main()
{
	Localclass();
	return 0;
}

运行之后的信息显示如下图所示:

c6d83233cc814d43aba34d916dbc2a6c.png

上述代码在Localclass()函数中定义了一个类CBook,该类被称为局部类。对于局部类CBook来说,在该函数之外之不可以被访问的,因为局部类被封装在了函数的局部作用域之中。

1.5 友元 

1.5.1 友元概述

  友元是一种定义在类外部的普通函数或类,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面要加上关键字friend。友元不是成员函数,但是它可以访问类中的私有成员。

  使用friend关键字可以让特定的函数或者别的类的所有成员函数对私有数据成员进行读写。这既可以保持数据的私有性,又能够使特定的类或函数直接访问私有数据。

  有时候普通函数需要直接访问一个类的保护或者私有数据成员。如果没有友元机制,则只能将类中所有的数据成员都改写为公共的,从而任何函数都可以无约束的访问它。

  普通函数需要直接访问类的保护或私有数据成员的原因主要是为了提高效率。

  例如,没有使用友元函数的情况如下:

#include<iostream>
using namespace std;
class CRectangle
{
	public:
		CRectangle()
		{
			m_height=0;
			m_width=0;
		}
		CRectangle(int x,int y,int a,int b)
		{
			m_height=a-y;
			m_width=b-x;
		}
		
		int getheight()
		{
			return m_height;
		}
		int getwidth()
		{
			return m_width;
		}
	protected:
		int m_height;
		int m_width;
};

int computerrectarea(CRectangle & myrect)		//不是友元函数的定义而是直接找一个对象地址 
{
	return myrect.getheight()*myrect.getwidth();
}

int main()
{
	CRectangle rg(0,0,100,100);
	cout<<"Result of computerrectarea is:"<<computerrectarea(rg)<<endl;
	return 0;
}

在代码中可以看到,computerrectarea函数定义时只能对类中的函数进行引用,因为类中的函数属性都是公有属性,对外是可见的,但是数据成员的类型则是受保护的属性,对外是不可见的,所以只能使用公有成员函数得到想要的值,然后执行自己的相关操作。

下面我们来使用友元函数来完成上面的操作:

#include<iostream>
using namespace std;
class CRectangle
{
	public:
		CRectangle()
		{
			m_height=0;
			m_width=0;
		}
		CRectangle(int x,int y,int a,int b)
		{
			m_height=a-y;
			m_width=b-x;
		}
		
		int getheight()
		{
			return m_height;
		}
		int getwidth()
		{
			return m_width;
		}
	friend int computerrectarea(CRectangle & myrect);		//声明友元函数 
	protected:
		int m_height;
		int m_width;
};

int computerrectarea(CRectangle & myrect)	//友元函数的定义 
{		
	return myrect.m_height*myrect.m_width;
}

int main()
{
	CRectangle rg(0,0,100,100);
	cout<<"Result of computerrectarea is:"<<computerrectarea(rg)<<endl;
	return 0;
}

两次运行的结果都如下图所示:

dab096a256454164bf195ed3235f85f4.png

在computerrectarea函数的定义中看到使用CRectangle的对象可以直接引用其中的数据成员,这是因为在CRectangle类中将computerrectarea函数声明为友元了。

使用友元保持了CRectangle类中数据的私有性,起到了隐藏数据成员的好处,又可以使特定的函数或类可以直接访问这些隐藏的数据成员。 

1.5.2 友元类

对于类的私有方法,只能在该类中允许访问,其他的类是不能够访问的。但在开发程序时,如果两个类的耦合度比较紧密,能够在另一个类中访问另一个类的私有成员会带来很大的方便。C++语言提供了友元类和友元函数(方法)来实现访问其他类的私有成员。当用户希望另一个类能够访问当前类的私有成员时,可以在当前类中将另一个类设置为自己的友元类,这样在另一个类中即可以访问当前类的私有成员了。例如定义友元类。

#include<iostream>
using namespace std;
#include<cstring>
class CItem	//定义一个CItem类 
{
	private:
		char m_name[128];	//定义私有的数据成员 
		void outputname()	//定义私有的成员函数 
		{
			cout<<m_name<<endl;	//输出m_name 
		}
	public:
		friend class CList;	//将CList类设置为自己的友元类 
		void setItemname(const char* pchdata)	//定义公有成员函数,设置m_name成员 
		{
			if(pchdata != NULL)
				strcpy(m_name,pchdata);	//赋值字符串 
		}
		CItem()	//构造函数 
		{
			memset(m_name,0,128);	//初始化数据成员 
		}
};

class CList		//定义类CList 
{
	private:	//定义私有的数据成员m_item 
		CItem m_item;
	public:
		void outputitem();	//定义公有函数 
};

void CList::outputitem()
{
	m_item.setItemname("露露");		//调用CItem类的私有函数 
	m_item.outputname();		//调用CItem类的公有函数 
}

int main()
{
	CList c;
	c.outputitem();
	return 0;
}

程序运行的结果如下图所示:

1030b2ceab8c4c05bb67bb0acb38c1d4.png

 在定义CItem类时,使用friend关键字将CList类定义为CItem类的友元,这样CList类中的所有方法都可以访问CItem类中的私有成员了。

1.5.3 友元方法(函数)

  在开发程序时,有时需要控制一个类对当前类的私有成员的方法。例如,假设需要实现只允许CList类的某个成员访问CItem类的私有成员,而不允许其他成员函数访问该私有数据。这可以通过定义友元函数来实现。在定义CItem类时,可以将CList类的某个方法定义为友元方法,这样就限制了只有该方法允许访问CItem类的私有成员。

#include<iostream>
using namespace std;
#include<cstring>
class CItem;	//声明CItem类 
class CList 	//定义CList类 
{
	private:		//定义私有成员m_pitem	
		CItem* m_pitem;
	public:
		CList();	//构造函数 
		~CList();	//析构函数 
		void outputitem();	//定义成员函数 
};

class CItem		//定义CItem类 
{
	friend void CList::outputitem();	//设置友元函数 
	private:
		char m_name[128];	//设置私有数据成员 
		void outputname()	//设置私有成员函数 
		{
			cout<<m_name<<endl;
		}
	public:		//设置公有函数 
		void setitemname(const char* pchdata)
		{
			if(pchdata != NULL)
				strcpy(m_name,pchdata);	
		}		
		CItem()	//构造函数 
		{
			memset(m_name,0,128);	//初始化数据成员 
		}
};

void CList::outputitem()	//定义成员函数 
{
	m_pitem->setitemname("lulu");
	m_pitem->outputname();
}
CList::CList()	//定义构造函数 
{
	m_pitem = new CItem();
}
CList::~CList()	//定义析构函数 
{
	delete m_pitem;		//释放m_pitem的对象 
	m_pitem = NULL;		//将m_pitem对象设置为空 
}

int main()	//主函数 
{
	CList c;
	c.outputitem();	//调用友元方法 
	return 0;
}

上述代码中,在定义CItem类时,使用friend关键字将CList类的outputitem方法设置为友元函数,在CList类的ouputitem方法中访问了CItem类的私有方法outputname。程序运行如图所示。

44a304c1383842de8d6cb33552f6dee6.png

 对于友元函数来说,不仅可以使类的成员函数,还可以是一个全局函数。例如:

#include<iostream>
using namespace std;
#include<cstring>
class CItem
{
	friend void outputitem(CItem* pitem);	//将全局函数设置为友元函数 
	private:
		char m_name[128];
		void outputname()
		{
			cout<<m_name<<endl;
		}
	public:
		void setname(const char* pchdata)
		{
			if(pchdata != NULL)
				strcpy(m_name,pchdata);	//赋值字符串 
		}
		CItem()		//定义构造函数 
		{
			memset(m_name,0,128);	//初始化数据成员 
		}
};

void outputitem(CItem* pitem)	//定义全局函数 
{
	if(pitem != NULL)
	{
		pitem->setname("同一个世界,同一个梦想!");
		pitem->outputname();
	}
}

int main()
{
	CItem item;
	outputitem(&item);	//通过全局函数来调用类的私有方法 
	return 0;
}

运行结果如下图:

3be218107e294c2ab56e2f6f8d440dd1.png

在上述的代码中,定义全局函数outputitem(),在CItem类中使用friend关键字将outputitem()函数设置为友元函数。而CItem类中私有成员则可以被友元函数所调用。

1.6 命名空间

1.6.1 使用命名空间

  在一个应用程序的多个文件中可能会存在同名的全局对象,这样会导致应用程序的链接错误。使用命名空间是消除命名冲突的最佳方式。

  例如,下面的代码定义了两个命名空间:

namespace myname1
{
	int iint1=10;
	int iint2=20;
};
namespace myname2
{
	int iint1=30;
	int iint2=40;
};

在上述代码中,namespace是关键字,而myname1和myname2是定义的两个命名空间的名称,大括号中是所属命名空间中的对象。虽然在两个大括号中定义的变量一样,但是因为在不同的命名空间中,所以避免了标识符的冲突,保证了标识符的唯一性。

  总而言之,命名空间就是一个命名的范围区域,程序员在这个特定的范围内创建的所有标识符都是唯一的。

1.6.2 定义命名空间

  在1.6.1小节中了解到有关命名空间的作用和使用的意义,本节来具体看一下是如何定义命名空间。命名空间的格式如下:

namespace 名称

{

        常量、变量、函数等对象的定义

定义命名空间要使用关键字namespace,例如

namespace myname
{
    int ii1=10;
    int ii2=20;
};

 在代码中,myname就是定义的命名空间的名称,在大括号里定义了两个整型变量ii1和ii2,那么这两个整型变量及时属于myname这个命名空间范围内的。

命名空间定义完成后,如何使用其中的成员呢?在讲解类时曾介绍过使用作用域限定符“::”来引用类中的成员,在这里仍然使用作用域限定符来引用命名空间中的成员。引用空间成员的一般形式如下所示:

命名空间名称::成员;

下面我们来经过示例来熟悉一下如何去使用命名空间。

#include<iostream>
using namespace std;
namespace myname1	//定义命名空间 
{
	int iint1=10;
};
namespace myname2	//定义命名空间 
{
	int iint1=30;
};

int iint1=20;	//全局变量 

int main()
{
	cout<<iint1<<endl;
	cout<<myname1::iint1<<endl;
	cout<<myname2::iint1<<endl;
	return 0;
}

该代码的运行结果如下图所示:

a6f98c4a15e24e77b4088780a0a97d47.png

程序中使用namespace关键字定义两个命名空间,分别就是myname1和myname2.在这两个命名空间范围中,都定义了变量iint1,并对其进行赋值为10,30.

  通过命名空间的方法,虽然定义相同的名称的变量表示不同的值,但是也可以正确的进行引用显示出来。

还有另一种引用命名空间中成员的方法,就是使用using namespace语句。一般形式为:

using namespace 命名空间名称; 

如果使用using namespace语句,在引用命名空间中的成员时可以直接使用变量名称。

下面我们就用一个实例来讲解在程序中引入命名空间:

#include<iostream>
using namespace std;

namespace myname	//定义命名空间 
{
	int ivalue=10;
}

using namespace myname;		//使用命名空间myname 

int main()
{
	cout<<ivalue<<endl;	//输出命名空间中的变量 
} 

运行的结果如下:

d81fc02fd4084df5a12bc329810bc249.png

在程序中先定义命名空间myname,之后使用using namesp语句,使用myname空间,这样在main函数之中就可以直接使用ivalue变量了。

需要注意的是,如果定义多个命名空间,并且在这些命名空间中都有相同的标识符的成员,那么使用using namespace语句就会产生歧义性。这时最好还是使用作用域限定符来进行引用。 

1.6.3 定义嵌套的命名空间

  命名空间可以定义在其他的命名空间中,在这种情况下,仅仅通过使用外部的命名空间作为前缀,程序便可以引用在命名空间之外定义的其他标识符。然而,在命名空间内不定的标识符需要作为外部命名空间和内部命名空间名称的前缀出现。例如:

#include<iostream>
using namespace std;
namespace output	
{
	void show()	//定义函数 
	{
		cout<<"Output's function!"<<endl;	
	}	
	namespace myname
	{
		void demo()	//定义函数 
		{
			cout<<"Myname's function!"<<endl; 
		}
	}
} 

在上述代码中,在output命名空间中又定义了一个命名空间mynmae,如果在程序中访问myname命名空间中的对象,可以使用外层的命名空间和内层命名空间作为前缀。

output::myname::demo();

用户也可以直接使用using命令引用嵌套的myname命名空间。如下

using namespace output::myname;        //引用嵌套的myname命名空间

demo();        //调用命名空间myname中的函数

上述代码中只是引用了mynmae的命名空间并没有引用output命名空间所以不可以调用output命名空间中的函数。

在下实例中定义嵌套命名空间,使用命名空间名称选择调用函数。

#include<iostream>
using namespace std;
namespace output	//定义命名空间 
{
	void show()	//定义函数 
	{
		cout<<"Output's function!"<<endl;	
	}	
	namespace myname	//定义嵌套命名空间 
	{
		void demo()	//定义函数 
		{
			cout<<"Myname's function!"<<endl; 
		}
	}
} 

int main()
{
	output::show();		//调用output命名空间中的函数 
	output::myname::demo();		//调用myname命名空间中的函数 
	return 0;
}

运行程序之后的显示情况如下图:

a0f5e4533b594682971d9730422ff8bc.png

1.6.4 定义未命名的命名空间

  尽管为命名空间指定名称是有益的,但是在C++中也允许在定义中省略命名空间的名称来简单的定义未命名的命名空间。

例如定义一个包含两个整型变量的未命名空间。


namesapce 
{
    int ivalue1=10;
    int ivalue2=20;
}

 事实上在无命名空间中定义的标识符被设置为全局命名空间,不过这样就违背了命名空间的设置原则。所以未命名空间没有被广泛的使用。

1.7 问题解答

1.7.1 结构体和类有何区别

  在C++语言中,结构体和类的区别只有一个:在声明类成员是共有的、私有的,还是保护的情况下,结构体默认该成员为公有的,类默认该成员为私有的。

1.7.2 可不可以不显示声明构造函数

  可以。如果程序员没有显示声明构造函数,则C++编译器会为类增加一个无参构造函数,如果已经有一个构造函数,C++编译器就不会增加这个默认的无参构造函数了。

就此该篇博客的所有关于类和对象的相关知识就已经讲解结束了,如果对之前的类与对象了解的不是很通透的可以去看看几个月前的详细讲解篇上,其中有关于类与对象的一些简单的操作与理解,在这里祝福大家开学快乐,愿疫情早点消失!

猜你喜欢

转载自blog.csdn.net/m0_61886762/article/details/126545101
今日推荐