C+±面向对象编程-001-类和对象-2020-3-16
目录
一、C++类
1.类概述
面向对象中的对象需要通过定义类来声明,对象一词是一种形象的说法,在编写代码过程中则是通过定义一个类来实现。
C++类不同定义于汉语中的类、分类、类型,它是一种特殊的概念,可以是对统一类型事物进行抽象处理,也可以是一个层次结构中的不同层次节点。例如,将客观世界看成一个Object类,动物是客观世界中的一小部分,定义为Animal类,狗是一种哺乳动物,是动物的一类,定义为Dog类,鱼也是一种动物,定义为Fish类,类的层次关系如图
类是一个新的数据类型,它和结构体有些相似,是由不同数据类型组成的集合体,但类要比结构体增加了操作数据的行为,这个行为就是函数。
2.类的声明与定义
类的声明格式如下:
class 类名标识符
{
[public:]
[数据成员的声明]
[成员函数的声明]
[private:]
[数据成员的声明]
[成员函数的声明]
[protected]
[数据成员的声明]
[成员函数的声明]
};
类的声明格式的说明如下:
1.class 是定义类结构体的关键字,大括号内被称为类体或类空间。
2.类名标识符指定的就是类名,类名就是一个新的数据类型,通过类名可以声明对象。
3.类的成员有数据和函数两种类型。
4.大括号内是定义和声明类成员的地方,关键字public、private、protected是类成员访问的修饰符。
类中的数据成员的类型可以是任意的,包含整型、浮点型、字符型、数组、指针和引用等,也可以是对象。另一个类的对象可以作为该类的成员,但是自身类的对象不可作为该类的成员,而自身类的指针或引用又是可以作为该类的成员的。
定义类结构体和定义结构体时大括号后要有分号。
例如,给出一个员工信息类声明:
class CPerson
{
/*数据成员*/
int m_iIndex;//声明数据成员
char m_cName[25];//声明数据成员
short m_shAge;//声明数据成员
double m_dSalary;//声明数据成员
/*成员函数*/
short getAge();//声明成员函数
int setAge(short sAge);//声明成员函数
int getIndex()://声明成员函数
int setIndex(int iIndex);//声明成员函数
char *getName();//声明成员函数
int setName(char cName[25]);//声明成员函数
double getSalary();//声明成员函数
int setSalary(double dSalary);//声明成员函数
在上面的代码中,class关键字是用来定义类这种类型的,CPerson是定义的员工信息类名称,在大括号中包含了4个数据成员分别表示CPerson类的属性,包含了8个成员函数表示CPerson类的行为。
3.类的实现
上节只是在CPerson类中声明了类的成员,然而要使用这个类中的方法,即成员函数,还要对其进行定义具体的操作。下面来看一下是如何定义类中的方法的。
第一种方法是将类的成员函数都定义在类体内。
以下代码都在Person.h头文件内,类的成员函数都定义在类体内。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
class CPerson
{
public:
//数据成员
int m_iIndex;
char m_cName[25];
short m_shAge;
double m_dSalary;
//成员函数
short getAge(){return m_shAge;}
int setAge(short sAge)
{
m_shAge=sAge;
return 0;//执行成功返回0
}
int getIndex(){return m_iIndex;}
int setIndex(int iIndex)
{
m_iIndex=iIndex;
return 0;//执行成功返回0
}
char *getName(){return m_cName;}
int setName(char cName[25])
{
strcpy(m_cName,cName);
return 0;//执行成功返回0
}
double getSalary (){return m_dSalary;}
int setSalary(double dSalary)
{
m_dSalary=dSalary;
return 0;//执行成功返回0
}
};
第二种方法,也可以将类体内的成员函数的实现放在类体外,但如果类成员定义在类体外,需要用到域运算符"::",放在类体内和类体外的效果是一样的。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
class CPerson
{
public:
//数据成员
int m_iIndex;
char m_cName[25];
short m_shAge;
double m_dSalary;
//成员函数
short getAge();
int setAge(short sAge);
int getIndex();
int setIndex(int iIndex);
char *getName();
int setName(char cName[25]);
double getSalary ();
int setSalary(double dSalary);
};
//类成员函数的实现部分
short CPerson::getAge(){return m_shAge;}
int CPerson::setAge(short sAge)
{
m_shAge=sAge;
return 0;//执行成功返回0
}
int CPerson::getIndex(){return m_iIndex;}
int CPerson::setIndex(int iIndex)
{
m_iIndex=iIndex;
return 0;//执行成功返回0
}
char *CPerson::getName(){return m_cName;}
int CPerson::setName(char cName[25])
{
strcpy(m_cName,cName);
return 0;//执行成功返回0
}
double CPerson::getSalary (){return m_dSalary;}
int CPerson::setSalary(double dSalary)
{
m_dSalary=dSalary;
return 0;//执行成功返回0
}
前面两种方式都是将代码存储在同一个文件内。C++语言可以实现将函数的声明和函数的定义放在不同的文件内,一般在头文件放入函数的声明,在实现文件放入函数的实现文件。同样可以将类的定义放在头文件中,将类成员函数的实现放在实现文件内。存放类的头文件和实现文件最好和类名相同或相似,例如,将CPerson类的声明部分放在Person.h文件内,程序代码如下:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
class CPerson
{
public:
//数据成员
int m_iIndex;
char m_cName[25];
short m_shAge;
double m_dSalary;
//成员函数
short getAge();
int setAge(short sAge);
int getIndex();
int setIndex(int iIndex);
char *getName();
int setName(char cName[25]);
double getSalary ();
int setSalary(double dSalary);
};
将CPerson类的实现部分放在Person.cpp文件中,程序代码如下:
#include"Person.h"
//类成员函数的实现部分
short CPerson::getAge(){return m_shAge;}
int CPerson::setAge(short sAge)
{
m_shAge=sAge;
return 0;//执行成功返回0
}
int CPerson::getIndex(){return m_iIndex;}
int CPerson::setIndex(int iIndex)
{
m_iIndex=iIndex;
return 0;//执行成功返回0
}
char *CPerson::getName(){return m_cName;}
int CPerson::setName(char cName[25])
{
strcpy(m_cName,cName);
return 0;//执行成功返回0
}
double CPerson::getSalary (){return m_dSalary;}
int CPerson::setSalary(double dSalary)
{
m_dSalary=dSalary;
return 0;//执行成功返回0
}
此时整个工程所有文件如下
Main.cpp
Person.cpp
Person.h
Sample.dsp
Sample.dsw
Sample.ncb
Sample.opt
关于类的实现有以下两点说明:
(1)类的数据成员需要初始化,成员函数还要添加实现代码。类的数据成员不可以在类的声明中初始化。
class CPerson
{
//数据成员
int m_iIndex=1; //错误写法,不应该初始化的
char m_cName[25]="Mary"; //错误写法,不应该初始化的
short m_shAge=22; //错误写法,不应该初始化的
double m_dSalary=1700.00; //错误写法,不应该初始化的
//成员函数
short getAge();
int setAge(short sAge);
int getIndex();
int setIndex(int iIndex);
char *getName();
int setName(char cName[25]);
double getSalary ();
int setSalary(double dSalary);
};
上面的代码是不能通过编译的。
(2)空类是C++中最简单的类,其声明方式如下:
class CPerson{};
空类只是起到占位的作用,需要的时候再定义类成员及实现。
4.对象的声明
定义一个新类后,就可以通过类名来声明一个对象,声明的形式如下:
类名 对象名表
类名是定义好的新类的标识符,对象名表中是一个或多个对象的名称,如果声明的是多个对象就用逗号运算符分隔。
例如,声明一个对象如下:
CPerson p;
声明多个对象如下:
CPerson p1,p2,p3;
声明完对象,就是对象的引用了,对象的引用有两种方式,一种是成员引用方式,一种是对象指针方式。
(1)成员引用方式
成员变量引用的表示如下:
对象名.成员名
这里“.”是一个运算符,该运算符的功能是表示对象的成员。
成员函数引用的表示如下:
对象名.成员名(参数表)
例如:
CPerson p;
p.m_iIndex;
(2)对象指针方式
对象声明形式中的对象名表,除了是用逗号运算符分隔的多个对象名外,还可以是对象名数组、对象名指针和引用形式的对象名。
声明一个对象指针:
CPerson *p;
但要想使用对象的成员,需要“->”运算符,它是表示成员的运算符,与“.”运算符的意义相同。“->”用来表示对象指针所指的成员,对象指针就是指向对象的指针,例如:
CPerson *p;
p->m_iIndex;
下面的对象数据成员的两种表示形式是等价的:
对象指针名->数据成员
与
(*对象指针名).数据成员
同样,下面的成员函数的两种表示形式是等价的:
对象指针名->成员名(参数表)
与
(*对象指针名).成员名(参数表)
例如:
CPerson *p;
(*p).m_iIndex;//对类中成员进行引用
p->m_iIndex;//对类中成员进行引用
对象引用
在本实例中,利用前文声明的类定义对象,然后使用该对象引用其中成员。
#include<iostream>
#include"Person.h"
int main()
{
int iResult=-1;
CPerson p;
iResult=p.setAage(25);
if(iResult>=0)
cout<<"m_shAge is:"<<p.getAge()<<endl;
iResult=p.setIndex(0);
if(iResult>=0)
cout<<"m_iIndex is:"<<p.getIndex()<<endl;
char bufTemp[]="Mary";
iResult=p.setName(bufTemp);
if(iResult>=0)
cout<<"m_cName is:"<<p.getName()<<endl;
iResult=p.setSalary(1700.25);
if(iResult>=0)
cout<<"m_dSalary is:"<<p.getSalary()<<endl;
}
在本实例中可以看到,首先使用CPerson类定义对象p,然后使用p引用类中的成员函数。
p.setAge(25)引用类中的setAge成员函数,将参数中的数据赋值给数据成员,设置对象的属性。函数的返回值赋给iResult变量,通过iResult变量值判断函数setAge为数据成员赋值是否成功。如果成功再使用p.getAge()得到赋值的数据,然后将其输出显示。
之后使用对象p依次引用成员函数setIndex、setName和setSalary,然后通过对iResult变量的判断,决定是否引用成员函数getIndex、getName和getSalary。
二、构造函数
1.构造函数概述
在类的实例进入其作用域时,也就是建立一个对象,构造函数就会被调用,那么构造函数的作用是什么呢?当建立一个对象时,常常需要做某些初始化的工作,例如,对数据成员进行赋值设置类的属性,而这些操作刚好放在构造函数中完成。
前文介绍过结构的相关只是,在对结构进行初始化时,可以使用下面的方法,例如:
struct PersonInfo
{
int index;
char name[30];
short age;
};
void InitStruct()
{
PersonInfo p={1,"mr",22};
}
但是类不能像结构体一样初始化,其构造方法如下:
class CPerson
{
public:
CPerson();//构造函数
int m_iIndex;
int getIndex();
};
//构造函数
CPerson::CPerson()
{
m_iIndex=10;
}
CPerson()是默认构造函数,如果不显示地写上函数地声明也可以。
构造函数是可以有参数的,修改上面的代码,使其构造函数带有参数,例如:
class CPerson
{
public:
CPerson(int iindex);//构造函数
int m_iIndex;
int setIndex(int iIndex);
}
//构造函数
CPerson::CPerson(int iIndex)
{
m_iIndex=iIndex;
}
使用构造函数进行初始化操作
#include<iostream>
using namespace std;
//定义CPerson类
class CPerson
{
public :
CPerson();
CPerson(int iIndex,short m_shAge,double m_dSalary);
int m_iIndex;
short m_shAge;
double m_dSalary;
int getIndex();
short getAge();
double getSalary();
};
//在默认构造函数中初始化
CPerson::CPerson()
{
m_iIndex=0;
m_shAge=10;
m_dSalary=1000;
}
//在带参数的构造函数中初始化
CPerson::CPerson(int iIndex,short m_shAge,double m_dSalary)
{
m_iIndex=iIndex;
m_shAge=m_shAge;
m_dSalary=m_dSalary;
}
int CPerson::getIndex()
{
return m_iIndex;
}
//在main函数中输出类的成员值
int main()
{
CPerson p1;
cout<<"m_iIndex is:"<<p1.getIndex()<<endl;
CPerson p2(1,20,1000);
cout<<"m_iIndex is:"<<p2.getIndex()<<endl;
}
m_iIndex is:0
m_iIndex is:1
--------------------------------
Process exited with return value 0
Press any key to continue . . .
程序声明了两个对象p1和p2,p1使用默认构造函数初始化成员变量,p2使用带参数的构造函数初始化,所以在调用同一个类成员函数getIndex时输出结果不同。
2.复制构造函数
复制构造函数就是函数的参数是一个已经初始化的类对象。
使用复制构造函数。
在头文件Person.h中声明和定义类,如下
class CPerson
{
public:
CPerson(int iIndex,short shAge,double dSalary); //构造函数
CPerson(CPerson©Person); //复制构造函数
int m_iIndex;
short m_shAge;
double m_dSalary;
int getIndex();
short getAge();
double getSalary();
};
//构造函数
CPerson::CPerson(int iIndex,short shAge,double dSalary)
{
m_iIndex=iIndex;
m_shAge=shAge;
m_dSalary=dSalary;
}
//复制构造函数
CPerson::CPerson(CPerson ©Person)
{
m_iIndex=copyPerson.m_iIndex;
m_shAge=copyPerson.m_shAge;
m_dSalary=copyPerson.m_dSalary;
}
short CPerson::getAge()
{
return m_shAge;
}
int CPerson::getIndex()
{
return m_iIndex;
}
double CPerson::getSalary()
{
return m_dSalary;
}
在主程序文件中实现类对象的调用,如下
#include<iostream>
#include "person.h"
using namespace std;
int main()
{
CPerson p1(20,30,100);
CPerson p2(p1);
cout<<"m_iIndex of p1 is:"<<p2.getIndex()<<endl;
cout<<"m_shAge of p1 is:"<<p2.getAge()<<endl;
cout<<"m_dSalary of p1 is:"<<p2.getSalary()<<endl;
cout<<"m_iIndex of p2 is:"<<p2.getIndex()<<endl;
cout<<"m_shAge of p2 is:"<<p2.getAge()<<endl;
cout<<"m_dSalary of p2 is:"<<p2.getSalary()<<endl;
}
m_iIndex of p1 is:20
m_shAge of p1 is:30
m_dSalary of p1 is:100
m_iIndex of p2 is:20
m_shAge of p2 is:30
m_dSalary of p2 is:100
--------------------------------
Process exited with return value 0
Press any key to continue . . .
程序中先用带参数的构造函数声明对象p1,然后通过复制构造函数声明对象p2,因为p1已经是初始化完成的类对象,可以作为复制构造函数的参数,通过输出结果可以看出,两个对象是相同的。
三、析构函数
构造函数和析构函数是类体定义中比较特殊的两个成员函数,因为它们两个都没有返回值,而且构造函数名标识符和类名标识符相同,析构函数名标识符就是在类名标识符前面加“~”符号。
构造函数主要是用来在对象创建时,给对象中的一些数据成员赋值,主要目的就是来初始化对象。析构函数的功能是用来释放一个对象的,在对象删除前,用它来做一些清理工作,它与构造函数的功能正好相反。
使用析构函数
在头文件Person.h中声明和定义类,如下
#include<iostream>
#include<cstring>
using namespace std;
class CPerson
{
public:
CPerson();
~CPerson(); //析构函数
char *m_pMessage;
void ShowStartMessage();
void ShowFrameMessage();
};
CPerson::CPerson()
{
m_pMessage=new char[2048];
}
void CPerson::ShowStartMessage()
{
strcpy(m_pMessage,"Welcome to MR");
cout<<m_pMessage<<endl;
}
void CPerson::ShowFrameMessage()
{
strcpy(m_pMessage,"*************");
cout<<m_pMessage<<endl;
}
CPerson::~CPerson()
{
delete[]m_pMessage;
}
在主程序文件中实现类对象的调用,如下
#include<iostream>
using namespace std;
#include"person.h"
int main()
{
CPerson p;
p.ShowFrameMessage();
p.ShowStartMessage();
p.ShowFrameMessage();
}
*************
Welcome to MR
*************
--------------------------------
Process exited with return value 0
Press any key to continue . . .
程序在构造函数中使用new为成员m_pMessage分配空间,在析构函数中使用delete释放由new分配的空间。成员m_pMessage为字符指针,在ShowStartMessage成员函数中输出字符指针所指向的内容。
使用析构函数的注意事项如下:
1.一个类中只可能定义一个析构函数。
2.析构函数不能重载。
3.构造函数和析构函数不能使用return语句返回值,不能加上关键字void。
构造函数和析构函数的调用环境:
(1)自动变量的作用域是某个模块,当此模块被激活时,自动变量调用构造函数,当退出此模块时,会调用析构函数。
(2)全局变量在进入main函数之前会调用构造函数,在程序终止时会调用析构函数。
(3)动态分配的对象在使用new为对象分配内存时会调用构造函数;使用delete删除对象时会调用析构函数。
(4)临时变量是为支持计算,由编译器自动产生的。临时变量的生存期的开始和结尾会调用构造函数和析构函数。
四、类成员
1.访问类成员
类的三大特点之一就是具有封装性,封装在类里面的数据可以设置成对外可见或不可见,通过关键字public、private、protected可以设置类中数据成员对外是否可见,也就是其他类是否可以访问该数据成员。
关键字public、private、protected说明类成员是公有的、私有的,还是保护的。这3个关键字将类划分为3个区域,在public区域的类成员可以在类作用域外被访问,而private区域和protected区域只能在类作用域内被访问,如图
这3种类成员的属性
public属性的成员对外可见,对内可见
private属性的成员对外不可见,对内可见
protected属性的成员对外不可见,对内可见,且对派生类是可见的。
如果在类定义的时候没有加任何关键字,默认状态类成员都在private区域
例如,在头文件Person.h中
class CPerson
{
int m_iIndex;
int getIndex(){return m_iIndex;}
int setIndex(int iIndex)
{
m_iIndex =iIndex;
return 0; //执行成功返回0
}
};
实现文件Person.cpp中:
#include<iostream.h>
#include"Person.h"
int main()
{
CPerson p;
p.m_iIndex=100; //错误
cout<<"m_iIndex is:"<<p.getIndex()<<endl; //错误
}
上面错误是
因为在默认状态下,类成员的属性为private,这样的话类成员只能被类中的其他成员访问,而不能被外部访问。例如,CPerson类中的m_iIndex数据成员,只能在类体的作用域内被访问和赋值,数据类型为CPerson类的对象p,就无法对m_iIndex数据成员进行赋值。
有了不同区域,开发人员可以根据需求来进行封装,将不想让其他类访问和调用的类成员定义在private区域和protected区域,这就保证了类成员的隐蔽性。需要注意的是,如果将成员的属性设置为protected,那么继承类也可以访问父类的保护成员,但是不能访问类中的私有成员。
关键字的作用范围是,直到下一次出现另一个关键字为止,例如
class CPerson
{
private :
int m_iIndex; //私有属性成员
public:
int getIndex(){return m_iIndex;} //公有属性成员
int setIndex(int iIndex) //公有属性成员
{
m_iIndex=iIndex;
return 0;
}
};
在上面的代码中,private访问权限控制符设置m_iIndex成员变量为私有。public关键字下面的成员函数设置为公有,从中可以看出private的作用域到public出现时为止。
2.内联成员函数
在定义函数时,可以使用inline关键字将函数定义为内联函数。在定义类的成员函数时,也可以使用inline关键字将成员函数定义为内联成员函数。其实,对于成员函数来说,如果其定义是在类体中,即使没有使用inline关键字,该成员函数也被认为是内联成员函数,例如:
class CUser //定义一个CUser类
{
private:
char m_Username[128];
char m_Password[128]; //定义数据成员
public:
inline char*GetUsername()const //定义一个内联成员函数
};
char *CUser::GetUsername()const //实现内联成员函数
{
return (char*)m_Username;
}
程序中,使用inline关键字将类中的成员函数设置为内联成员函数。此外,也可以在类成员函数的实现部分使用inline关键字标识函数为内联成员函数,例如:
class CUser //定义一个CUser类
{
private: //定义数据成员
char m_Username[128];
char m_Password[128];
public :
char *GetUsername()const; //定义成员函数
};
inline char *CUser::GetUsername()const //函数为内联成员函数
{
return (char *)m_Username; //设置放回值
}
程序中的代码演示了在何处使用关键字inline。对于内联函数来说,程序会在函数调用的地方直接插入函数代码,如果函数体语句较多,则会导致程序代码膨胀。如果将类的析构函数定义为内联函数,可能会导致潜在的代码膨胀。
3.静态类成员
静态类成员,允许使用类名直接访问。静态类成员是在类成员定义前使用static关键字标识,例如:
class CBook
{
public:
static unsigned int m_Price; //定义一个静态数据成员
}
在定义静态数据成员时,通常需要在类体外部对静态数据成员进行初始化,例如:
unsigned int CBook::m_price=10; //初始化静态数据成员
对于静态成员来说,不仅可以通过对象访问,还可以直接使用类访问,例如:
int main(int argc,char*argv[])
{
CBook book; //定义一个CBook类对象book
cout<<CBook::m_price<<endl; //通过类名访问静态成员
cout<<book.m_price<<endl; //通过对象访问静态成员
return 0;
}
在一个类中,静态数据成员是被所有的类对象所共享的,这就意味着无论定义多少个类对象,类的静态数据成员只有一份,同时,如果某一个对象修改了静态数据成员,其他对象的静态数据成员(实际上是同一个静态数据成员)也将改变。
对于静态数据成员,还需要注意以下几点:
(1)静态数据成员可以是当前类的类型,而其他数据成员只能当前类的指针或应用类型。
在定义类成员时,对于静态数据成员,其类型可以是当前类的类型,而非静态数据成员则不可以,除非数据成员的类型为当前类的指针或引用类型,例如:
class CBook
{
public:
static unsigned int m_Price;
CBook m_Book; //非法的定义,不允许在该类中定义所属类的对象
static CBook m_VCbook; //正确,静态数据成员允许定义类的所属类对象
CBook *m_pBook; //正确,允许定义类的所属类型的指针类型对象
};
(2)静态数据成员可以作为成员函数的默认参数。
在定义类的成员函数时,可以为成员函数指定默认参数,其参数的默认值也可以是类的静态数据成员,但是不同的数据成员则不能作为成员函数的默认参数。例如:
class CBook //定义CBook类
{
public :
static unsigned int m_Price; //定义一个静态数据成员
int m_Pages; //定义一个普通数据成员
void OutputInfo(int data=m_Price) //定义一个函数,以静态数据成员作为默认参数
{
cout<<data<<endl; //输出信息
}
void OutputPage(int page=m_Pages) //错误的定义,类的普通数据成员不能作为默认参数
{
cout<<page<<endl; //输出信息
}
};
在介绍完类的静态数据成员之后,下面介绍类的静态成员函数。定义类的静态成员函数与定义普通的成员函数类似,只是在成员函数前添加static关键字,例如:
static void OutputInfo(); //定义类的静态成员函数
类的静态成员函数只能访问类的静态数据成员,而不能访问普通的数据成员。例如:
class CBook
{
public:
static unsigned int m_Price;
int m_Pages;
static void OutputInfo()
{
cout<<m_Price<<endl;
cout<<m_Pages<<endl;
}
};
在上述代码中,语句cout<<m_Pages<<endl;是错误的,因为m_Pages是非静态数据成员,不能在静态成员函数中访问。
此外,对于静态成员函数不能定义为const成员函数,即静态成员函数末尾不能使用const关键字,例如,下面的静态成员函数的定义是非法的
static void OutputInfo()const; //错误的定义,静态成员函数不能使用const关键字
在定义静态数据成员函数时,如何函数的实现代码处于类体之外,则在函数的实现部分不能再标识static关键字,例如,下面的函数定义是非法的。
static void CBook ::OutputInfo() //错误的函数定义,不能使用static关键字
{
cout<<m_price<<endl; //输出信息
}
上述代码如果去掉static关键字则是正确的,例如:
void CBook::OutputInfo() //正确的函数定义
{
cout<<m_price<<endl; //输出信息
}
4.隐藏的this指针
对于类的非静态成员,每一个对象都有自己的一份拷贝,即每个对象都有自己的数据成员,不过成员函数却是每个对象共享的,通过类中隐藏的this指针调用共享的成员函数找到自己的数据成员。
下面通过对例子的讲解来说明this指针的作用
例如,每个对象都有自己的一份拷贝。
class CBook //定义一个CBook类
{
public:
int m_Pages; //定义一个数据成员
void OutputPages() //定义一个成员函数
{
cout<<m_Pages<<endl;
}
};
int main(int argc,char*argv[])
{
CBook vbBook ,vcBook; //定义两个CBook类对象
vbBook.m_Pages=512; //设置vcBook对象的数据成员
vcBook.m_Pages=570; //设置vcBook对象的数据成员
vbBook.OutputPages(); //调用OutputPages方法输出vbBook对象的数据成员
vcBook.OutputPages(); //调用OutputPages方法输出vcBook对象的数据成员
return 0;
}
vbBook和vcBook两个对象均有自己的数据成员m_Pages,在调用OutputPages成员函数时输出的均是自己的数据成员。在OutputPages成员函数中只是访问了m_Pages数据成员,每个对象在调用OutputPages方法时是通过this指针区分自己的数据成员。在每个类的成员函数(非静态成员函数)中都隐含包含一个this指针,指向被调用对象的指针,其类型为当前类类型的指针类型,在const方法中,为当前类类型的const指针类型。当vbBook对象调用OutputPages成员函数时,this指针指向vbBook对象;当vcBook对象调用OutputPages成员函数时,this指针指向vcBook对象。在OutputPages成员函数中,用户可以显式地使用this指针访问数据成员,
例如
void OutputPages()
{
cout<<this->m_Pages<<endl; //使用this指针访问数据成员
}
实际上,编译器为了实现this指针,在成员函数中自动添加了this指针对数据成员地方法,类似于上面的OutputPages方法。此外,为了将this指针指向当前调用对象,并在成员函数中能够使用,在每个成员函数中都隐含包含一个this指针作为函数参考,并在函数调用时将对象自身的地址隐含作为实际参数传递。例如,以OutputPages成员函数为例,编译器将其定义为:
void OutputPages(CBook*this) //隐含添加this指针
{
cout<<this->m_Pages<<endl;
}
在对象调用成员函数时,传递对象的地址到成员函数中。以vc.OutputPages();语句为例,编译器将其解释为vbBook.OutputPages(&vbBook);,这就使得this指针合法,并能够在成员函数中使用。
5.嵌套类
C++语言允许在一个类中定义另一类,这被称为嵌套类。例如,下面的代码在定义CList类时,在内部又定义了一个嵌套CNode。
#define MAXLEN 128 //定义一个宏
class CList //定义CList类
{
piblic: //嵌套类为共有的
class CNode{ //定义嵌套类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) //判断指针是否为空
{
strcpy(m_Node.m_Name,pchData); //访问CNode类的公有数据
}
}
void SetNodeTag(int tag) //定义成员函数
{
m_Node.m_Tag=tag; //访问CNode类的私有
}
};
上述代码在嵌套类CNode中定义了一个私有成员m_Tag,定义了一个公有成员m_Name,对于外围类CList来说,通常它不能够访问嵌套类的私有成员,虽然嵌套类是在其内部定义的。但是,上述代码在定义CNode类时将CList类作为自己的友元类,这使得CList类能够访问CNode类的私有成员。
对于内部的嵌套类来说,只允许其在外围的类域中使用,在其他类域或者作用域中是不可见的,例如,下面是定义是非法的。
int main(int argc,char*argv[])
{
CNode node; //错误的定义,不能访问的CNode类
return 0;
}
上述代码在main函数的作用域中定义了一个CNode对象,导致CNode没有被声明的错误。对于main函数来说,嵌套类CNode是不可见的,但是可以通过使用外围的类域作为限定符来定义CNode对象。如下的定义将是合法的。
int main(int argc,char*argv[])
{
CList::CNode node; //合法的定义
return 0;
}
上述代码通过使用外围类域作为限定符访问到了CNode类。但是这样做通常是不合理的,也是有限制条件的,因为既然定义了嵌套类,通常都不允许在外界访问,这违背了使用嵌套类的原则。其次,在定义嵌套类时,如果将其定义为私有的或受保护的,即使使用外围类域作为限定符,外界也无法访问嵌套类。
6.局部类
类的定义可以放在头文件中,也可以放在源文件中。还有一种情况,类的定义也可以设置在函数中,这样的类被称为局部类。
例如,定义一个局部类CBook。
void LocalClass() //定义一个函数
{
class CBook //定义一个局部类CBook
{
private: //定义一个私有数据成员
int m_Pages;
public: //定义公有成员函数
void SetPages(int pages)
{
if(m_Pages!=pages)
m_Pages=page; //为数据成员赋值
}
int GetPages() //定义公有成员函数
{
return m_Pages; //获取数据成员信息
}
};
CBook book; //定义一个CBook对象
book.SetPages(300); //调用SetPages方法
cout<<book.GetPages()<<endl; //输出信息
}
上述代码在LocalClass函数中定义了一个类CBook,该类被称为局部类。对于局部类CBook,在函数之外是不能够被访问的,因为局部类被封装在了函数的局部作用域中。
五、友元
1.友元概述
使用friend关键字可以让特定的函数或者别的类的所以成员函数对私有数据成员进行读写,这即可以保持数据的私有性,又能够使特定的类或函数直接访问私有数据。
有时候,普通函数需要直接访问一个类的保护或私有数据成员。如果没有友元机制,则只能将类的数据成员声明为公共的,从而任何数据都可以无约束地访问它。
普通函数需要直接访问类的保护或私有数据成员的原因主要是为提高效率
没使用友元函数的情况如下:
#include<iostream>
using namespace std;
class CRectangle
{
public:
CRectangle()
{
m_iHeight=0;
m_iWidth=0;
}
CRectangle(int iLeftTop_x,int iLeftTop_y,int iRightBottom_x,int iRightBottom_y)
{
m_iHeight=iRightBottom_y-iLeftTop_y;
m_iWidth=iRightBottom_x-iLeftTop_x;
}
int getHeight()
{
return m_iHeight;
}
int getWidth()
{
return m_iWidth;
}
protected:
int m_iHeight;
int m_iWidth;
};
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;
}
在代码中可以看到,ComputerRectArea函数定义时只能对类中的函数进行引用,因为类中的函数属性都为公有属性,对外是可见的,但是数据成员的属性为受保护属性,对外是不可见的,所以只能使用公有成员函数得到想要的值。
下面来看一下使用友元函数的情况:
#include<iostream>
using namespace std;
class CRectangle
{
public:
CRectangle()
{
m_iHeight=0;
m_iWidth=0;
}
CRectangle(int iLeftTop_x,int iLeftTop_y,int iRightBottom_x,int iRightBottom_y)
{
m_iHeight=iRightBottom_y-iLeftTop_y;
m_iWidth=iRightBottom_x-iLeftTop_x;
}
int getHeight()
{
return m_iHeight;
}
int getWidth()
{
return m_iWidth;
}
friend int ComputerRectArea(CRectangle & myRect); //声明为友元函数
protected:
int m_iHeight;
int m_iWidth;
};
int ComputerRectArea(CRectangle &myRect) //友元函数的定义
{
return myRect.m_iHeight*myRect.m_iWidth;
}
int main()
{
CRectangle rg(0,0,100,100);
cout<<"Result of ComputerRectArea is:"<<ComputerRectArea(rg)<<endl;
}
在ComputerRectArea函数的定义中可以看到使用CRectangle的对象可以直接引用其中的数据成员,这是因为在CRectangle类中将ComputerRectArea函数声明为友元了。
从中可以看到使用友元保持了CRectangle类中数据的私有性,起到了隐藏数据成员的好吃,有使得特定的类或函数可以直接访问这些隐藏数据成员。
2.友元类
对于类的私有方法,只有在该类中允许访问,其他类是不能访问的,但在开发程序时,如果两个类的耦合度比较紧密,能够在一个类中访问另一个类的私有成员会带来很多的方便。C++语言提供了友元类和友元方法(或者称为友元函数)来实现访问其他类的私有成员。当用户希望另一个类能够访问当前类的私有成员时,可以在当前类中将另一个类作为自己的友元类,这样在另一个类中就可以访问当前类的私有成员了,例如,
定义友元类:
class CItem //定义一个CItem类
{
private:
char m_Name[128]; //定义私有的数据成员
void OutputName() //定义私有的成员函数
{
printf("%s\n",m_Name); //输出m_Name
}
public:
friend class CList; //将CList类作为自己的友元类
void SetltemName(const char*pchData) //定义公有成员函数,设置m_Name成员
{
if(pchData!=NULL) //判断指针是否为空
{
strcpy(m_Name,pchData); //赋值字符串
}
}
CItem() //构造函数
{
memset(m_Name,0,128)l //初始化数据成员m_Name
}
};
class CList //定义类CList
{
private:
CItem m_Item; //定义私有的数据成员m_Item
public:
void OutputItem(); //定义公有成员函数
};
void CList::OutputItem() //OutputItem函数的实现代码
{
m_Item.SetItemName("BeiJing"); //调用CItem类的公有方法
m_Item.OutputName(); //调用CItem类的私有方法
}
在定义CItem类时,使用friend关键字将CList类定义为CItem类的友元,这样CList类中的所有方法都可以访问CItem类中的私有成员了。在CList类的OutputItem方法中,语句m_Item.OutoutName()演示了调用CItem类的私有方法OutputName。
3.友元方法
在开发程序时,有时需要控制另一个类对当前类的私有成员的方法,例如,假设需要实现只允许CList类的私有成员,而不允许其他成员函数访问CItem类的私有数据,这可以通过定义友元函数来实现。在定义CItem类时,可以将CList类的某个方法定义为友元方法,这样就限制了只有该方法允许访问CItem类的私有成员。
定义友元方法。
class CItem; //前导声明CItem
class CList //定义CList类
{
private:
CItem *m_pItem; //定义私有数据成员m_pItem
public:
CList(); //定义默认构造函数
~CList(); //定义析构函数
void OutputItem(); //定义OutputItem成员函数
};
class CItem //定义CItem类
{
friend void CList::OutputItem(); //声明友元函数
private:
char m_Name[128]; //定义私有数据成员
void OutputName() //定义私有成员函数
{
printf("%s\n",m_Name); //输出数据成员信息
}
public:
void SetItemName(const char*pchData) //定义公有方法
{
if(pchData!=NULL) //判断指针是否为空
{
strcpy(m_Name,pchData); //赋值字符串
}
}
CItem() //构造函数
{
memset(m_Name,0,128); //初始化数据成员m_Name
}
};
void CList::OutputItem() //CList类的OutputItem成员函数的实现
{
m_pItem->SetItemName("Beijing"); //调用CItem类的公有方法
m_pItem->OutputName(); //在友元函数中访问CItem类的私有方法OutputName
}
CList::CList() //CList类的默认构造函数
{
m_pItem=new CItem(); //构造m_pItem对象
}
CList::~CList() //CList类的析构函数
{
delete m_pItem; //释放m_pltem对象
m_pItem=NULL; //将m_pltem对象设置为空
}
int main(int argc,char*argv[]) //主函数
{
CList list; //定义CList对象list
list.OutputItem(); //调用CList的OutPutItem方法
return 0;
}
在上面的代码中,在定义CItem类时,使用friend关键字将CList类的OutputItem方法设置为友元函数,在CList类的OutputItem方法中访问了CItem类的私有方法OutputName。
对于友元函数来说,不仅可以是类的成员函数,还可以是一个全局函数,例如
class CItem //定义CItem类
{
friend void OutputItem(CItem*pItem); //将全局函数OutputItem定义为友元函数
private:
char m_Name[128]; //定义数据成员
void OutputName() //定义私有成员
{
printf("%s\n",m_Name); //输出信息
}
public:
void SetltemName(const char *pchData) //定义公有方法
{
if(pchData!=NULL) //判断指针是否为空
{
strcpy(m_Name,pchData); //赋值字符串
}
}
CItem() //定义构造函数
{
memset(m_Name,0,128); //初始化数据成员
}
};
void OutputItem(CItem*pItem) //定义全局函数
{
if(pItem!=NULL) //判断参数是否为空
{
pItem->SetltemName("同一个世界"); //调用CItem类的公有方法
pItem->OutputName(); //调用CItem类的私有方法
}
}
int main(int argc, char *argv[]) //主函数
{
CItem Item; //定义一个CItem类对象Item
OutputItem(&Item); //通过全局函数访问CItem类的私有方法
return 0;
}
在上面的代码中,定义全局函数OutputItem,在CItem类中使用friend关键字将OutputItem函数声明为友元函数,而CItem类中OutputName这个函数的属性是私有的,那么对外是不见得。因为OutputItem是CItem类得友元函数,所以可以引用类中得私有成员。
六、命名空间
1.使用命名空间
在一个应用程序的多个文件中可能会存在同名的全局对象,这样会导致应用程序的链接错误。使用命名空间是消除命名多冲突的最佳方式。
例如,下面的代码定义了两个命名空间:
namespace MyName1
{
int iInt1=10;
int iInt2=20;
};
namespace MyName2
{
int iInt1=10;
int iInt2=20;
};
在上面的代码中,namespace是关键字,而MyName1和MyName2是定义的两个命名空间名称,大括号中是所属命名空间中的对象。虽然在两个大括号中定义的变量是一样的,但是因为在不同的命名空间中,所以避免了标识符的冲突,保证了标识符的唯一性。
总而言之,命名空间就是一个命名的范围区域,程序员在这个特定的范围内创建的所有标识符都是唯一的。
2.定义命名空间
命名空间的定义格式如下:
namespace 名称
{
常量、变量、函数等对象的定义
}
定义命名空间要使用关键字namespace,例如
namespace MyName
{
int iInt1=10;
int iInt2=20;
}
在上面的代码中,MyName是定义的命名空间的名称,在大括号中定义了两个整型变量iInt1和iInt2,那么这两个整型变量就是属于MyName这个命名空间范围内的。
命名空间定义完成,使用“::”来引用空间中的成员。引用空间成员的一般形式如下:
命名空间名称::成员;
例如,引用MyName命名空间中的成员:
MyName::iint1=30;
定义命名空间
#include<iostream>
using namespace std;
namespace MyName1
{
int iValue=10;
}
namespace MyName2
{
int iValue=20;
}
int iValue=20;
int main()
{
cout<<MyName1::iValue<<endl;
cout<<MyName2::iValue<<endl;
cout<<iValue<<endl;
return 0;
}
10
20
20
--------------------------------
Process exited with return value 0
Press any key to continue . . .
还有一种引用命名空间中成员的方法,就是使用using namespace语句,一般形式如下:
using namespace 命名空间名称;
例如,在源程序中包含MyName命名空间:
using namespace MyName;
iInt=30;
如果使用using namespace 语句,则在引用空间中的成员时直接使用就可以。
使用using namespace 语句
#include<iostream>
namespace MyName
{
int iValue=10;
}
using namespace std;
using namespace MyName;
int main()
{
cout<<iValue<<endl;
return 0;
}
10
--------------------------------
Process exited with return value 0
Press any key to continue . . .
在程序中先定义命名空间MyName,之后使用using namespace 语句,使用MyName命名空间,这样在main函数中使用的iValue变量就是指MyName命名空间中的iValue变量。
需要注意的是,如果定义多个命名空间,并且在这些命名空间中都有相同标识符的成员,那么使用using namespace 语句进行包含在引用成员时就会产生歧义性,这时最好还是使用作用域限定符来进行应用。
3.在多个文件中定义命名空间
在定义命名空间时,通常在头文件中声明命名空间中的函数,在源文件中定义命名空间中的函数,将程序的声明与实现分开,例如,在头文件中声明命名空间函数:
namespace Output
{
void Demo();
}
在源文件中定义函数:
void Output::Demo()
{
cout<<"This is a function!\n";
}
在源文件中定义函数时,注意要使用命名空间作为前缀,表面实现的是命名空间中定义的函数,否则将是定义一个全局函数。
在多个文件中定义命名空间。
-----------
//Detach.h头文件中
namespace Output
{
void Demo();
}
//Detach.cpp源文件中
#include<iostream>
#include"Detach.h"
using namespace std;
void Output::Demo()
{
cout<<"This is a function!\n";
}
int main()
{
Output::Demo();
return 0;
}
在Detach.cpp源文件中还可以定义Output命名空间,例如
namespace Output
{
void show()
{
cout<<"This is show function"<<endl;
}
}
此时,命名空间Output中的内容为两个文件Output命名空间内容的"总和",因此,如果在Detach.cpp文件的Output命名空间中定义一个函数名称为Demo是非法的,因为进行了重复定义,这时编译器会提示Demo已经有一个函数体。
4.定义嵌套的命名空间
命名空间可以定义在其他的命名空间中,在这种情况下,仅仅通过使用外部的命名空间作为前缀,程序便可以引用在命名空间之外定义的其他标识符,然而,在命名空间内不定的标识符需要作为外部命名空间和内部命名空间名称的前缀出现,例如
namespace Output
{
void Show()
{
cout<<"Output's function!"<<endl;
}
namespace MyName
{
void Demo()
{
cout<<"MyName's function!"<<endl;
}
}
}
上述代码中,在Output命名空间中又定义了一个命名空间MyName,如果程序中访问MyName命名空间中的对象,可以使用外层的命名空间和内层的命名空间作为前缀,例如
Output::MyName::Demo(); //调用MyName命名空间中的函数
用户也可以直接使用using命令引用嵌套的MyName命名空间,例如:
using namespace Output::MyName; //引用嵌套的MyName的命名空间
Demo(); //调用MyName命名空间中的函数
上述代码中,using namespace Output::MyName;语句只是引用了嵌套在Output命名空间中的MyName命名空间,并没有引用Output命名空间,因此试图访问Output访问Output命名空间中定义的对象是非法的,例如:
using namespace Windows::GDI;
show(); //错误的访问,无法访问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;
}
Output's function!
Myname's function!
--------------------------------
Process exited with return value 0
Press any key to continue . . .
5.定义未命名的命名空间
尽管为命名空间指定名称是有益的,但是C++中也允许在定义中省略命名空间的名称来简单地定义未命名地命名空间。
例如,定义一个包含两个整型变量地未命名地命名空间:
namespace
{
int iValue1=10;
int iValue2=20;
}
事实上在无命名空间中定义的标识符被设置为全局命名空间,不过这样就违背了命名空间的设置原则,所以,未命名的命名空间没有被广泛应用。