C++笔记

来源:互联网 发布:部落顶帖软件 编辑:程序博客网 时间:2024/05/21 12:45

cout 格式化输出
constint ival=17 ;// 'ival' is constant, so value never change
cout <<"oct :"<<oct<<ival<<endl ;// 21 : 8 进制
cout <<"dec :"<<dec<<ival<<endl ;// 17 : 10 进制
cout <<"hex :"<<hex<<ival<<endl ;// 11 : 16 进制
cout <<"hex :"<<hex<<17.01<<endl ;// 17.01 : 不受影响

在块作用域内可通过作用域运算符“::”来引用与局部变量同名的全局变量。
#include <iostream.h>
int  i= 100;
void main(void)
{
    int i , j=50;
    i=18;          //访问局部变量i
    ::i= ::i+4;    //访问全部变量i
    j= ::i+i;   //访问全部变量i和局部变量j
    cout<<”::i=”<<::i<<’\n’;
    cout<<”i=”<<i<<’\n’;
    cout<<”j=”<<j<<’\n’;
}

在C++中定义函数时,允许给参数指定一个缺省的值。在调用函数时,若明确给出了这种实参的值,则使用相应实参的值;若没有给出相应的实参,则使用缺省的值。(举例说明)
int  fac(int n=2)
{
    int t=1;
    for(int i=1;i<=n;i++)
        t=t*i;
    return t;
}
void main(void)
{
    cout<<fac(6)<<endl;
}
使用具有缺省参数的函数时,应注意以下几点:
1.不可以靠左边缺省      
正确:int area(int long ,  int width=2)
错误:int area(int long =4,  int width)
2.函数原型说明时可以不加变量名
   float v(float,float=10,float=20);
3.只能在前面定义一次缺省值,即原型说明时定义了缺省值,后面函数的定义不可有缺省值。

C++语言将字符串作为字符数组来处理。

字符串常量:“CHINA”,在机内被处理成一个无名的字符型一维数组。

C

H

I

N

A

‘\0’

C++语言中约定用‘\0’作为字符串的结束标志,它占内存空间,但不计入串长度。

字符串与字符数组的区别:
char a[]={'C','H','I','N','A'};//长度占5个字节
char c[]=“CHINA”;  //长度占6个字节,包括‘\0’
可以用字符串的形式为字符数组赋初值
char c[ ]={“I am a boy”};  /*长度11字节,以‘\0’结尾 */
char  a[ ]={‘I’, ‘ ’,‘a’, ‘m’, ‘ ’, ‘a’, ‘ ’ , ‘b’, ‘o’, ‘y’};  /* 长度10字节  */


结构体类型的静态成员
当把结构体类型中的某一个成员的存储类型定义为静态时,表示在这种结构类型的所有变量中,编译程序为这个成员只分配一个存储空间,即这种结构体类型的所有变量共同使用这个成员的存储空间。
<类型>  <结构体类型名>::<静态成员名>;
其中类型要与在结构体中定义该成员的类型一致,结构体类型名指明静态成员属于哪一个结构体。 

struct  s

{

    static int  id;
    int  eng;
};
int  s::id=50;
这时,未定义结构体变量,但已将静态成员的空间安排好。
若有定义:s s1,s2;则变量s1,s2的id成员占用同一存储空间(静态区)。

函数在编译时被分配给一个入口地址。这个入口地址就称为函数的地址,也是函数的指针。像数组一样,C++语言规定,函数名就代表函数的入口地址
专门存放函数地址的指针变量称为指向函数的指针变量。
函数类型 (*指针变量名)(参数类型 );
例子:int (*p)( int, int);


返回指针值的函数
被调函数返回的不是一个数据,而是一个地址。所以函数的类型为指针类型。
类型标识符 *函数名(参数表)
"类型标识符"指出返回是什么类型的地址
int *max(x, y)

指针数组的概念
一个数组,其元素均为指针类型的数据,称为指针数组。也就是说,指针数组中的每一个元素都是指针变量,可以放地址。
类型标识   *数组名[数组长度说明]
int  *p[4];            
p为数组名,内有四个元素,每个元素可以放一个int型数据的地址。            
int  (*p)[4];            
p为指向有四个int型元素的一维数组的行指针。

引用
对变量起另外一个名字 (外号),这个名字称为该变量的引用。
<类型>   &<引用变量名> = <原变量名>; 
其中原变量名必须是一个已定义过的变量。如:
int   max ;
int  &refmax=max;
refmax并没有重新在内存中开辟单元,只是引用max的单元。max与refmax在内存中占用同一地址,即同一地址两个名字。
对引用类型的变量,说明以下几点:
1、引用在定义的时候要初始化。
int  &refmax;//错误,没有具体的引用对象
int  &refmax=max;//max是已定义过的变量
2、对引用的操作就是对被引用的变量的操作。
3、 引用类型变量的初始化值不能是一个常数。
如:int  &ref1 = 5;    // 是错误的。
int  &ref=i;
4、引用同变量一样有地址,可以对其地址进行操作,即将其地址赋给一指针。
int  a, *p;
int  &m=a;//&是变量的引用
p=&m;//&是变量的地址
*p=10;
5、可以用动态分配的内存空间来初始化一个引用变量。
float  &reff = * new float ; //用new开辟一个空间,取一个别名reff
reff= 200; //给空间赋值
cout << reff ; //输出200
delete &reff; //收回这个空间
这个空间只有别名,但程序可以引用到。
float  *p,  a;
p=new  float;
float  a=* new  float;//错误!
指针与引用的区别:
1、指针是通过地址间接访问某个变量,而引用是通过别名直接访问某个变量。
2、引用必须初始化,而一旦被初始化后不得再作为其它变量的别名。

当&a的前面有类型符时(如int &a),它必然是对引用的声明;如果前面无类型符(如cout<<&a),则是取变量的地址。
 
以下的声明是非法的
1、企图建立数组的引用            int & a[9];
2、企图建立指向引用的指针        int & *p;
3、企图建立引用的引用            int & &px;

对常量(用const声明)的引用
void main(void)
{
    const int &r=8; //说明r为常量,不可赋值
    cout<<"r="<<r<<endl;
    // r+=15; //r为常量,不可作赋值运算
    cout<<"r="<<r<<endl;
}

引用与函数 
引用的用途主要是用来作函数的参数或函数的返回值。
引用作函数的形参,实际上是在被调函数中对实参变量进行操作。
void  change(int &x,  int &y)//x,y是实参a,b的别名
{
    int   t;
    t=x;
    x=y;
    y=z;
}
void main(void)
{
    int  a=3,b=5;
    change(a,b);  //实参为变量
    cout<<a<<'\t'<<b<<endl;
}  
输出: 5     3

引用作为形参,实参是变量而不是地址,这与指针变量作形参不一样。
void  change(int &x,  int &y)//形参为整型引用
{
    int   t;
    t=x;  x=y;   y=z;
}
void main(void)
{
    int  a=3,b=5;
    change(a,b);  //实参为变量
    cout<<a<<‘\t’<<b<<endl;
}  

void  change(int *x,  int *y)//形参为指针变量
{
    int   t;
    t=*x;  *x=*y;   *y=z;
}
void main(void)
{
    int  a=3,b=5;
    change(&a,&b); //实参为地址
    cout<<a<<‘\t’<<b<<endl;
}  

函数的返回值为引用类型
可以把函数定义为引用类型,这时函数的返回值即为某一变量的引用(别名),因此,它相当于返回了一个变量,所以可对其返回值进行赋值操作。这一点类同于函数的返回值为指针类型 。
int  a=4;
int  &f(int  x)//函数返回a的引用,即a的别名
{
    a=a+x;
    return  a;
}
void main(void)
{
    int   t=5;
    cout<<f(t)<<endl;//输出  9    (a=9)
    f(t)=20;//先调用,再赋值  a=20
    cout<<f(t)<<endl;//输出25  (a=25)
    t=f(t);//先调用,再赋值  t=30
    cout<<f(t)<<endl;  //输出60  (a=60)
}
一个函数返回引用类型,必须返回某个类型的变量。
语句:getdata()=8;
就相当于  int  &temp=8;
               temp=8 ;  
注意:由于函数调用返回的引用类型是在函数运行结束后产生的,所以函数不能返回自动变量和形参。
返回的变量的引用,这个变量必须是全局变量或静态局部变量,即存储在静态区中的变量。       

我们都知道,函数作为一种程序实体,它有名字、类型、地址和存储空间,一般说来函数不能作为左值(即函数不能放在赋值号左边)。但如果将函数定义为返回引用类型,因为返回的是一个变量的别名,就可以将函数放在左边,即给这个变量赋值。

const型指针
1)禁写指针
声明语句格式为:     数据类型    *  const   指针变量名
如:int  r=6;
    int * const  pr=&r;
则指针pr被禁写,即pr将始终指向一个地址,成为一个指针常量。它将不能再作为左值而放在赋值号的左边。(举例说明)
同样,禁写指针一定要在定义的时候赋初值。
虽然指针被禁写,但其间接引用并没有被禁写。即可以通过pr对r赋值。*pr=8;
void main(void)
{
    int  a,b;
    int  *const  pa=&a;  //一定要赋初值,pa是常量,不能在程序中被改变
    *pa=10; //可以间接引用
    pa=&b; //非法,pa为常量
}
2)禁写间接引用
声明语句格式如下:
   const  数据类型   *指针变量名;
所声明的指针指向一禁写的实体,即间接引用不能被改写。如:  const  int  *p;
所以程序中不能出现诸如  *p=  的语句,但指针p并未被禁写,因而可对指针p进行改写。
void main(void)
{
    int  a=3,b=5;
    const  int  *pa=&b; //可以不赋初值
    pa=&a; //指针变量可以重新赋值
    cout<<*pa<<endl;   //输出3
    *pa=10; //非法,指针指向的内容不能赋值
    a=100; //变量可以重新赋值
    cout<<*pa<<endl;  //输出100
}
即不可以通过指针对变量重新赋值
3)禁写指针又禁写间接引用
将上面两种情况结合起来,声明语句为下面的格式
const   数据类型    *const     指针变量名
如:const  int  *const  px=&x
说明:px是一个指针常量,它指向一禁写的实体,并且指针本身也被禁写,诸如:px=      *px=    此类的语句都是非法的。
在定义时必须赋初值。

类和对象 
对象是由一组属性和一组行为构成的。
在C++中,每个对象都是由数据和函数(即操作代码)这两部分组成的。 
每个对象都属于一个特定的类型。 
在C++中对象的类型称为类(class)。类代表了某一批对象的共性和特征。类是对象的抽象,而对象是类的具体实例(instance)。 

用关键字priviate限定的成员称为私有成员,对私有成员限定在该类的内部使用,即只允许该类中的成员函数使用私有的成员数据,对于私有的成员函数,只能被该类内的成员函数调用;类就相当于私有成员的作用域。 
用关键字public限定的成员称为公有成员,公有成员的数据或函数不受类的限制,可以在类内或类外自由使用;对类而言是透明的。
而用关键字protected所限定的成员称为保护成员,只允许在类内及该类的派生类中使用保护的数据或函数。即保护成员的作用域是该类及该类的派生类。


每一个限制词(private等)在类体中可使用多次。一旦使用了限制词,该限制词一直有效,直到下一个限制词开始为止。
如果未加说明,类中成员默认的访问权限是private,即私有的。

成员函数与成员数据的定义不分先后,可以先说明函数原型,再在类体外定义函数体。
在类体内定义成员函数:
class  A
{
    float  x, y;
    public:
    void Setxy(float a,float  b)
    {
        x=a;
        y=b;
    }
    void  Print(void)
    {
        cout<<x<<'\t'<<y<<endl;
    }
};

在类体内说明成员函数原型:
class  A
{
    float  x, y;
    public:
    void Setxy(float a,float  b);
    void  Print(void);
};
在类体外定义成员函数:
void A::Setxy(float a,float  b)
{
    x=a;
    y=b;
}
void  A::Print(void)
{
    cout<<x<<'\t'<<y<<endl;
}

在类体外定义成员函数的格式:
<type>   < class_name > :: < func_name > (<参数表>)
{
......  //函数体
}

在定义一个类时,要注意如下几点:
1、类具有封装性,并且类只是定义了一种结构(样板),所以类中的任何成员数据均不能使用关键字extern,auto或register限定其存储类型。
2、在定义类时,只是定义了一种导出的数据类型,并不为类分配存储空间,所以,在定义类中的数据成员时,不能对其初始化。如:
class  Test 
{
    int  x=5,y=6;   //是不允许的
    extern  float  x; //是不允许的  
}

在C++语言中,结构体类型只是类的一个特例。结构体类型与类的唯一的区别在于:在类中,其成员的缺省的存取权限是私有的;而在结构体类型中,其成员的缺省的存取权限是公有的。 


内联成员函数 
当我们定义一个类时,可以在类中直接定义函数体。这时成员函数在编译时是作为内联函数来实现的。 
同时,我们也可以在类体外定义类的内联成员函数,在类体内说明函数,在类体外定义时,在成员函数的定义前面加上关键字inline。 
class  A
{
    float  x, y;
    public:
        void Setxy(float a,float  b);
        void  Print(void);
};
//说明该成员函数为内联
inline   void A::Setxy(float a,float  b)
{
    x=a;
    y=b;
}
inline  void  A::Print(void)
{
    cout<<x<<'\t'<<y<<endl;
}

对象
在定义类时,只是定义了一种数据类型,即说明程序中可能会出现该类型的数据,并不为类分配存储空间。
只有在定义了属于类的变量后,系统才会为类的变量分配空间。
类的变量我们称之为对象。
对象是类的实例,定义对象之前,一定要先说明该对象的类。

对象的使用
一个对象的成员就是该对象的类所定义的成员,有成员数据和成员函数,引用时同结构体变量类似,用“.”运算符。
用成员选择运算符“.”只能访问对象的公有成员,而不能访问对象的私有成员或保护成员。若要访问对象的私有的数据成员,只能通过对象的公有成员函数来获取。 
同类型的对象之间可以整体赋值,这种赋值与对象的成员的访问权限无关。
class  A 
{
    float  x,y;
public:  
    float   m,n; 
    void Setxy( float a, float b  ){  x=a;   y=b;   }
    void  Print(void) {  cout<<x<<‘\t’<<y<<endl;  }
};
void main(void)
{
    A  a1,a2;
    a1.m=10;   a1.n=20; //为公有成员数据赋值
    a1.Setxy(2.0,5.0);   
    a2=a1;//同类型的对象之间可以整体赋值,相当于成员数据间相互赋值
    a1.Print();   a2.Print();
}

类作用域、类类型的作用域和对象的作用域 
类体的区域称为类作用域。类的成员函数与成员数据,其作用域都是属于类的作用域,仅在该类的范围内有效,故不能在主函数中直接通过函数名和成员名来调用函数。  
class  A 
{
    float  x,y;
public:  
    float   m,n; 
    void Setxy( float a, float b  ){  x=a;   y=b;   }
    void  Print(void) {  cout<<x<<‘\t’<<y<<endl;  }
};
void main(void)
{
    A  a1,a2;
    a1.m=20;   a1.n=10;   
    a1.Setxy(2.0,   5.0);
    a1.Print(); //用对象名调用
}
void main(void)
{
    A  a1,a2;
    m=20;  n=10;   
    Setxy(2.0,   5.0);
    Print(); //不能直接调用
}
类类型的作用域:在函数定义之外定义的类,其类名的作用域为文件作用域;而在函数体内定义的类,其类名的作用域为块作用域 。
对象的作用域与前面介绍的变量作用域完全相同 , 全局对象、局部对象、局部静态对象等。

类的嵌套
在定义一个类时, 在其类体中又包含了一个类的完整定义,称为类的嵌套 。
类是允许嵌套定义的 。

成员函数的重载 
一般是用于在一个类内实现若干重载的方法,这些方法的名称相同而参数形式不同。
类中的成员函数与前面介绍的普通函数一样,成员函数可以带有缺省的参数,也可以重载成员函数 。
重载时,函数的形参必须在类型或数目上不同。
可以有缺省参数的成员函数,若形参不完全缺省,则必须从形参的右边开始缺省。
缺省参数的成员函数:
class A
{
    float  x,y;
public:
    float Sum(void)
    {
        return  x+y;
    }
    void Set(float a,float b=10.0)
    {
        x = a;
        y = b;
    }
    void Print(void)
    {
        cout<<"x="<<x<<'\t'<<"y="<<y<<endl;
    }
};
void main(void)
{
    A a1,a2;


    a1.Set (2.0,4.0);//不缺省参数,a1.x=2,  a1.y=4
    cout<<"a1:  ";
    a1.Print();
    cout<<"a1.sum="<<a1.Sum ()<<endl;
    a2.Set(20.0);//缺省参数,a2.x=20,  a2.y=10
    cout<<"a2:  ";
    a2.Print();
    cout<<"a2.sum="<<a2.Sum ()<<endl;
}
返回引用类型的成员函数(可以返回私有数据成员的引用)
class A
{
    float  x,y;
public:
    float &Getx(void ) {   return  x;   }//返回x的引用
    void Set(float a,float b) {    x=a;   y=b;   }
    void Print(void) {   cout<<x<<'\t'<<y<<endl;   }
};
void main(void)
{
    A  a1,a2;


    a1.Set (3,5);
    cout<<"a1:  ";
    a1.Print ();
    a1.Getx()=30;//将a1对象中的x成员赋值
    cout<<"changed  a1: ";
    a1.Print ();
}

this指针
不同对象占据内存中的不同区域,它们所保存的数据各不相同,但对成员数据进行操作的成员函数的程序代码均是一样的。
当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,然后调用成员函数,每次成员函数存取数据成员时,也隐含使用this指针。
this指针具有如下形式的缺省说明:
   类名   *const    this;  
即 this 指针里的地址是一个常量
this为正在调用该函数的对象的地址。

构造函数和析构函数
构造函数和析构函数是在类体中说明的两种特殊的成员函数。
构造函数是在创建对象时,使用给定的值来将对象初始化。
析构函数的功能正好相反,是在系统释放对象前,对对象做一些善后工作。
构造函数可以带参数、可以重载,同时没有返回值。
构造函数是类的成员函数,系统约定构造函数名必须与类名相同。构造函数提供了初始化对象的一种简单的方法。

对构造函数,说明以下几点:
1.构造函数的函数名必须与类名相同。构造函数的主要作用是完成初始化对象的数据成员以及其它的初始化工作。
2. 在定义构造函数时,不能指定函数返回值的类型,也不能指定为void类型。
3. 一个类可以定义若干个构造函数。当定义多个构造函数时,必须满足函数重载的原则。
4.构造函数可以指定参数的缺省值。
5.若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其它类时,则可将构造函数定义为保护的成员函数。

每一个对象必须要有相应的构造函数
若没有显式定义构造函数,系统默认缺省的构造函数。

对局部对象,静态对象,全局对象的初始化对于局部对象,每次定义对象时,都要调用构造函数。
对于静态对象,是在首次定义对象时,调用构造函数的,且由于对象一直存在,只调用一次构造函数。
对于全局对象,是在main函数执行之前调用构造函数的。

缺省的构造函数 
在定义类时,若没有定义类的构造函数,则编译器自动产生一个缺省的构造函数,其格式为:
className::className() {  }
缺省的构造函数并不对所产生对象的数据成员赋初值;即新产生对象的数据成员的值是不确定的。 
关于缺省的构造函数,说明以下几点:
1、在定义类时,只要显式定义了一个类的构造函数,则编译器就不产生缺省的构造函数
2、所有的对象在定义时,必须调用构造函数
不存在没有构造函数的对象!
class A
{
    float x,y;
public:
    A(float a,float b) {x=a;y=b;}//显式定义了构造函数,不产生缺省的构造函数
    void Print(void){ cout<<x<<'\t'<<y<<endl;}
};
void main(void)
{
    A  a1;//error,定义时,没有构造函数可供调用
    A  a2(3.0,30.0);
}
3、在类中,若定义了没有参数的构造函数,或各参数均有缺省值的构造函数也称为缺省的构造函数,缺省的构造函数只能有一个。
4、产生对象时,系统必定要调用构造函数。所以任一对象的构造函数必须唯一。

构造函数与new运算符 
可以使用new运算符来动态地建立对象。建立时要自动调用构造函数,以便完成初始化对象的数据成员。最后返回这个动态对象的起始地址。
用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间。 
用new建立类的对象时,可以使用参数初始化动态空间。 

析构函数 
析构函数的作用与构造函数正好相反,是在对象的生命期结束时,释放系统为对象所分配的空间,即要撤消一个对象。
析构函数也是类的成员函数,定义析构函数的格式为:
ClassName::~ClassName( )
{
   ......
//         函数体;
}

析构函数的特点如下:
1、析构函数是成员函数,函数体可写在类体内,也可写在类体外。
2、析构函数是一个特殊的成员函数,函数名必须与类名相同,并在其前面加上字符“~”,以便和构造函数名相区别。
3、析构函数不能带有任何参数,不能有返回值,不指定函数类型。
4、一个类中,只能定义一个析构函数,析构函数不允许重载。
5、析构函数是在撤消对象时由系统自动调用的。
在程序的执行过程中,对象如果用new运算符开辟了空间,则在类中应该定义一个析构函数,并在析构函数中使用delete删除由new分配的内存空间。因为在撤消对象时,系统自动收回为对象所分配的存储空间,而不能自动收回由new分配的动态存储空间。
可以用new运算符为对象分配存储空间,如:
A   *p;
p=new A;
这时必须用delete才能释放这一空间。
delete   p;
用new运算符为对象分配动态存储空间时,调用了构造函数,用delete删除这个空间时,调用了析构函数。当使用运算符delete删除一个由new动态产生的对象时,它首先调用该对象的析构函数,然后再释放这个对象占用的内存空间。

不同存储类型的对象调用构造函数及析构函数 
1、对于全局定义的对象(在函数外定义的对象),在程序开始执行时,调用构造函数;到程序结束时,调用析构函数。
2、对于局部定义的对象(在函数内定义的对象),当程序执行到定义对象的地方时,调用构造函数;在退出对象的作用域时,调用析构函数。
3、用static定义的局部对象,在首次到达对象的定义时调用构造函数;到程序结束时,调用析构函数。
4、对于用new运算符动态生成的对象,在产生对象时调用构造函数,只有使用delete运算符来释放对象时,才调用析构函数。若不使用delete来撤消动态生成的对象,程序结束时,对象仍存在,并占用相应的存储空间,即系统不能自动地调用析构函数来撤消动态生成的对象。

动态构造及析构对象数组 
用new运算符来动态生成对象数组时,自动调用构造函数,而用delete运算符来释放p1所指向的对象数组占用的存储空间时,在指针变量的前面必须加上[ ], 才能将数组元素所占用的空间全部释放。否则,只释放第0个元素所占用的空间。
pa1=new  A[3];
..... 
delete  [ ]pa1; 

缺省的析构函数
若在类的定义中没有显式地定义析构函数时,则编译器自动地产生一个缺省的析构函数,其格式为:
ClassName::~ClassName() { };
任何对象都必须有构造函数和析构函数,但在撤消对象时,要释放对象的数据成员用new运算符分配的动态空间时,必须显式地定义析构函数。 

实现类型转换的构造函数
同类型的对象可以相互赋值,相当于类中的数据成员相互赋值;
如果直接将数据赋给对象,所赋入的数据需要强制类型转换,这种转换需要调用构造函数。
class A
{
    float x,y;
public:
    A(float a,float b) {x=a;y=b;cout<<"调用构造函数\n";}
    ~A() { cout<<"调用析构函数\n";}
    void Print(void) {    cout<<x<<'\t'<<y<<endl; }
};
void main(void)
{
    A  a1(1.0, 10.0); 
    a1.Print();
    a1=A(3.0 ,  30.0);  //产生临时对象,初始化并赋值后立即释放
    a1.Print();
    cout<<"退出主函数\n";
}
注意:当构造函数只有一个参数时,可以用= 强制赋值。
class B
{
    float x;
public:
    B(float a) {x=a;  cout<<"调用构造函数\n";}
    ~B() {  cout<<"调用析构函数\n";}
    void Print(void) {    cout<<x<<endl;  }
};
void main(void)
{
    B  b1(1.0);
    b1.Print();
    B  b2=100;   //单参数可以这样赋值 
    b2.Print();  
    b1=10;       //b1=B(10),产生一个临时对象
    b1.Print();
    cout<<"退出主函数\n";
}

完成拷贝功能的构造函数 
可以在定义一个对象的时候用另一个对象为其初始化,即构造函数的参数是另一个对象的引用,这种构造函数常为完成拷贝功能的构造函数。
完成拷贝功能的构造函数的一般格式为:
ClassName::ClassName(ClassName  &<变量名>)
{
......
//   函数体完成对应数据成员的赋值
}
class  A
{
    float   x,y;
public:
    A(float a=0, float b=0){x=a;  y=b;}
    A(A &a)//形参必须是同类型对象的引用
    { x=a.x;  y=a.y;}
};
void main(void)
{
    A   a1(1.0,2.0);
    A   a2(a1);//实参是同类型的对象
}

如果没有定义完成拷贝功能的构造函数,编译器自动生成一个隐含的完成拷贝功能的构造函数,依次完成类中对应数据成员的拷贝
A::A(A &a) //隐含的构造函数
{
    x=a.x;
    y=a.y;
}
例子:
class  A
{
    float   x,y;
public:
    A(float a=0, float b=0){x=a;  y=b; cout<<"调用了构造函数\n";}
    void  Print(void){  cout<<x<<'\t'<<y<<endl;  }
    ~A() {  cout<<"调用了析构函数\n";   }
};
void main(void)
{
    A   a1(1.0,2.0);
    A   a2(a1);//隐含了拷贝的构造函数
    A   a3=a1;//可以这样赋值
    a1.Print();
    a2.Print();
    a3.Print();
}
由编译器为每个类产生的这种隐含的完成拷贝功能的构造函数,依次完成类中对应数据成员的拷贝。
但是,当类中的数据成员中使用new运算符,动态地申请存储空间进行赋初值时,必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确实现数据成员的复制。

构造函数与对象成员
class B
{
    ....
};
class A
{
    int x , y;
    B   b1,b2; //在类A中包含类B的对象
};
对类A的对象初始化的同时还要对其成员数据类B的对象进行初始化,所以,类A的构造函数中要调用类B的构造函数。
对对象成员的构造函数的调用顺序取决于这些对象成员在类中说明的顺序,与它们在成员初始化列表中的顺序无关。
当建立类ClassName的对象时,先调用各个对象成员的构造函数,初始化相应的对象成员,然后才执行类ClassName的构造函数,初始化类ClassName中的其它成员。析构函数的调用顺序与构造函数正好相反。
class A
{
    float  x;
public:
    A(int a){  x=a;  cout<<“调用了A的构造函数\n”;}
    ~A(){cout<<“调用了A的析构函数\n”;}
};
class B
{
    float  y;
public:
    B(int a){  y=a;  cout<<“调用了B的构造函数\n”;}
    ~B(){cout<<“调用了B的析构函数\n”;}
};
class C
{
    float z;
    B  b1;
    A a1;
public:
    C(int a,int b,int c): a1(a),b1(b){z=c;cout<<“调用了C的构造函数\n”;}
    ~C(){cout<<“调用了C的析构函数\n”;}
};
void main(void)
{
    C  c1(1,2,3);   
}
打印为:
调用了B的构造函数
调用了A的构造函数
调用了C的构造函数
调用了C的析构函数
调用了A的析构函数
调用了B的析构函数

继承和派生类
在C++中所谓“继承”就是在一个已存在的类的基础上建立一个新的类。已存在的类称为“基类(base class)”或“父类(father class)”。新建立的类称为“派生类(derived class)”或“子类(son class)”。 
在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。
在建立派生类的过程中,基类不会做任何改变,派生类则除了继承基类的所有可引用的成员变量和成员函数外,还可另外定义本身的成员变量和处理这些变量的函数,由于派生类可继承基类的成员变量和成员函数,因此在基类中定义好的数据和函数等的程序代码可重复使用,这样可以提高程序的可靠性。
当从已有的类中派生出新的类时,可以对派生类做以下几种变化:
1、?可以继承基类的成员数据或成员函数。
2、可以增加新的成员变量。
3、可以增加新的成员函数。
4、可以重新定义已有的成员函数。
5、可以改变现有的成员属性。
在C++中有二种继承:单一继承和多重继承。当一个派生类仅由一个基类派生时,称为单一继承;而当一个派生类由二个或更多个基类所派生时,称为多重继承。
但派生并不是简单的扩充,有可能改变基类的性质。
有三种派生方式:公有派生、保护派生、私有派生。
默认的是私有派生。
class B:public   A{...};  
class B:protected   A{...};  
class B:private   A{...};  
class B:  A {...};  //A为私有派生









protected 成员是一种具有血缘关系内外有别的成员。它对派生类的对象而言,是公开成员,可以访问,对血缘外部而言,与私有成员一样被隐蔽。

抽象类与保护的成员函数
当定义了一个类,这个类只能用作基类来派生出新的类,而不能用这种类来定义对象时,称这种类为抽象类。当对某些特殊的对象要进行很好地封装时,需要定义抽象类。
将类的构造函数或析构函数的访问权限定义为保护的时,这种类为抽象类。 

当把类中的构造函数或析构函数说明为私有的时,所定义的类通常是没有任何实用意义的,一般情况下,不能用它来产生对象,也不能用它来产生派生类。 

类A是抽象类,类B是public继承类A的,则有如下法则:
在类B中不能定义A的对象;
在任何时候都不能定义A的对象;
但可以在初始化类B的对象时初始化原类A中的成员,因为类A的构造函数A()在类B中是可以被调用的。
class  A 
{
    int  x, y;
protected:
    A(int a,int b){x=a;y=b;}//基类初始化
public:
    void ShowA(){cout<< "x="<<x<<'\t'<<"y="<<y<<'\n';}
};
class B: public A
{
    int m;
    A  a1; //在派生类中也不可以定义A的对象,实际上还是类外调用
public:   
    B(int a,int b,int c):A(a,b)//可以在派生类中调用A的构造函数
    {
   m=c;
    }
    void Show(){      cout<<“m="<<m<<'\n';    ShowA();  }   
 };
void  main(void)
{
    B b1(1,2,3); //可以定义派生类对象
    b1.Show();
    A  aa; //不可定义A的对象          
}     

多重继承
可以用多个基类来派生一个类。
多重继承是单一继承的扩展
格式为(其中<Access>为继承方式):
class  类名:<Access>类名1,..., <Access>类名n
{
    private:     ...... ;   //私有成员说明;
    public:      ...... ;   //公有成员说明;
    protected:   ...... ;   //保护的成员说明;
};
例如:
class  D: public  A, protected B, private C 
{
    ....//派生类中新增加成员
};

初始化基类成员
构造函数不能被继承,派生类的构造函数必须调用基类的构造函数来初始化基类成员基类子对象。
派生类构造函数的调用顺序如下:
基类的构造函数(多继承时会从继承顺序第一个基类开始依次调用各个基类的构造函数);
子对象类的构造函数;
派生类的构造函数。

当撤销派生类对象时,析构函数的调用正好相反。

冲突
class  A
{
public:   int x;        
    void Show(){cout <<"x="<<x<<'\n';}
    A(int a=0){x=a;}
};
class B
{
public:   int x;
    void Show(){cout <<"x="<<x<<'\n';}
    B(int a=0){x=a;}
};
class C:public A,public B
{
    int y;
public: void Setx(int a){ x=a;}   //c1对象中有两个x成员
void Sety(int b){y=b;}
int Gety() {return  y;}
};
void  main(void)
{
    C c1;
    c1.Show(); //c1对象中有两个Show()函数
}
这时,可以利用类作用域符::来指明数据或函数的来源。
如:A::x=a;
    c1.B::Show();
支配规则
当派生类中新增加的数据或函数与基类中原有的同名时,若不加限制,则优先调用派生类中的成员。
class  A
{
public: int x;
    void Show(){cout <<"x="<<x<<'\n';}
};
class B
{
public: int y;
    void Show(){cout <<"y="<<y<<'\n';}
};
class C:public A,public B
{
    public: int y; //类B和类C均有y的成员
};
void  main(void)
{
    C c1;
    c1.x=100;
    c1.y=200;    //给派生类中的y赋值
    c1.B::y=300; //给基类B中的y赋值
    c1.A::Show();
    c1.B::Show(); //用作用域运算符限定调用的函数
    cout <<"y="<<c1.y<<'\n'; //输出派生类中的y值
    cout <<"y="<<c1.B::y<<'\n'; //输出基类B中的y值
}

基类与对象成员
任一基类在派生类中只能继承一次,否则,会造成成员名的冲突 
若在派生类中,确实要有二个以上基类的成员,则可用基类的二个对象作为派生类的成员。
把一个类作为派生类的基类或把一个类的对象作为一个类的成员,在使用上是有区别的:在派生类中可直接使用基类的成员(访问权限允许的话),但要使用对象成员的成员时,必须在对象名后加上成员运算符“.”和成员名。

基类和派生类赋值兼容规则
相互之间能否赋值?
可以将派生类对象的值赋给基类对象。反之不行。
例如基类是b,派生类是d,则可以b=d。
只是将从基类继承来的成员赋值。

可以将一个派生类对象的地址赋给基类的指针变量。
派生类对象可以初始化基类的引用。

虚基类 
类B  是类A的派生类
类C  是类A的派生类
类D  是类B和类C的派生类
这样,类D中就有两份类A的拷贝


这种同一个公共的基类在派生类中产生多个拷贝,不仅多占用了存储空间,而且可能会造成多个拷贝中的数据不一致 和模糊的引用。
D  d;
d.x=10;    //模糊引用

在多重派生的过程中,若使公共基类在派生类中只有一个拷贝,则可将这种基类说明为虚基类。
在派生类的定义中,只要在基类的类名前加上关键字virtual,就可以将基类说明为虚基类。
class B:public  virtual A
{
public:
    int y;
    B(int a=0, int b=0 ):A(b) { y=a;}
};

由虚基类派生出的对象初始化时,直接调用虚基类的构造函数。因此,若将一个类定义为虚基类,则一定有正确的构造函数可供所有派生类调用。

class  A
{
public:
int x;
    A(int  a=0) { x=a;}
};
class B:public  virtual A
{
public:
int y;
    B(int a=0,int b=0): A(a) {y=b;}
};
class C:public virtual A
{
public:
int z;
    C(int a=0,int c=0):A(a){z=c;  }
};
class D:public B,public C
{
public:
int dx;
    D(int a1,int b,int c,int d,int a2):B(a1,b),C(a2,c), A(a2)//直接在派生类中调用虚基类的构造函数
    { dx=d;  }
};
void  main(void)
{
D d1(10,20,30,40,50);    
    cout<<d1.x<<endl;  
    d1.x=400;
    cout<<d1.x<<endl;
    cout<<d1.y<<endl;
}
输出:
50
400
20
如果D(int a1,int b,int c,int d,int a2):B(a1,b),C(a2,c), A(a2)去掉A(a2)改为D(int a1,int b,int c,int d,int a2):B(a1,b),C(a2,c),没有对虚基类构造函数的调用,用缺省的构造函数,则输出如下:
0
400
20
再次强调,用虚基类进行多重派生时,若虚基类没有缺省的构造函数,则在每一个派生类的构造函数中都必须有对虚基类构造函数的调用 (且首先调用)。

友元函数
类中私有和保护的成员在类外不能被访问。
友元函数是一种定义在类外部的普通函数,其特点是能够访问类中私有成员和保护成员,即类的访问权限的限制对其不起作用。
友元函数需要在类体内进行说明,在前面加上关键字friend。
一般格式为:
friend  <type> FuncName(<args>);
例如:friend   float Volume(A &a);
友元函数不是成员函数,用法也与普通的函数完全一致,只不过它能访问类中所有的数据。友元函数破坏了类的封装性和隐蔽性,使得非成员函数可以访问类的私有成员。
一个类的友元可以自由地用该类中的所有成员。
友元函数只能用对象名引用类中的数据。
class A
{
    float  x,y;
public: 
    A(float a, float b){  x=a; y=b;} 
    float  Sum(){  return  x+y;  } //成员函数
    friend  float Sum(A &a){     return   a.x+a.y; } //友元函数,a.x和a.y是私有数据
};
void main(void)
{
    A   t1(4,5),t2(10,20);
    cout<<t1.Sum()<<endl;//成员函数的调用,利用对象名调用
    cout<<Sum(t2)<<endl;//友元函数的调用,直接调用
}
有关友元函数的使用,说明如下:
友元函数不是类的成员函数
友元函数近似于普通的函数,它不带有this指针,因此必须将对象名或对象的引用作为友元函数的参数,这样才能访问到对象的成员。
友元函数与一般函数的不同点在于:
友元函数必须在类的定义中说明,其函数体可在类内定义,也可在类外定义;
它可以访问该类中的所有成员(公有的、私有的和保护的),而一般函数只能访问类中的公有成员。
class A
{
    float  x,y;
public: 
    A(float a, float b){  x=a; y=b;}
    float  Getx(){  return x;  }
    float  Gety(){  return y; }
    float  Sum(){  return  x+y;  } //成员函数
    friend  float Sum(A &); //友元函数
};
float  Sumxy(A  &a){ return a.Getx()+a.Gety();  }//普通函数,必须通过共有函数访问私有成员
float Sum(A &a){  return a.x+a.y;  }//友元函数,可以直接调用类中私有成员
void main(void)
{
    A   t1(1,2),t2(10,20), t3(100,200);
    cout<<t1.Sum()<<endl;//对象调用成员函数
    cout<<Sum(t2)<<endl;//调用友元函数
    cout<<Sumxy(t3)<<endl;//调用一般函数
}

友元函数不受类中访问权限关键字的限制,可以把它放在类的私有部分,放在类的公有部分或放在类的保护部分,其作用都是一样的。换言之,在类中对友元函数指定访问权限是不起作用的。
友元函数的作用域与一般函数的作用域相同。
谨慎使用友元函数
通常使用友元函数来取对象中的数据成员值,而不修改对象中的成员值,则肯定是安全的。

大多数情况是友元函数是某个类的成员函数,即A类中的某个成员函数是B类中的友元函数,这个成员函数可以直接访问B类中的私有数据。这就实现了类与类之间的沟通。
例如:
class A
{
    ...
    void fun( B &);//既是类A的成员函数
};
class B
{
    ...
    friend void fun( B &);//又是类B的友元函数
};
注意:一个类的成员函数作为另一个类的友元函数时,应先定义友元函数所在的类。
例如:
类A中的成员函数fun()是类B的友元函数。即在fun()中可以直接引用类B的私有成员。
class   B; //先定义类A,则首先对类B作引用性说明
class   A
{
    ...... //类A的成员定义
    public:
    void  fun(B &);//函数的原型说明
};
class  B
{
    ......
    friend  void  A::fun(B &);//定义友元函数
};
void  A::fun(B  &b)   //函数的完整定义
{
    ...... //函数体的定义
}
具体例子:
类A中有一个函数可以直接引用类B的私有成员
class B;    //必须在此进行引用性说明,
class A
{
    float  x,y;
public: 
    A(float a, float b){  x=a; y=b;}
    float Sum(B &); //说明友元函数的函数原型,是类A的一成员函数
};
class B
{
    float  m,n;
public:
    B(float a,float  b){  m=a;n=b;  }
    friend  float A::Sum(B  &);//说明类A的成员函数是类B的友元函数
}
float  A::Sum(B  &b) //定义该友元函数
{
    x=b.m+b.n;  //直接引用类B的私有成员
y=b.m-b.n;  //直接引用类B的私有成员
return x;
}
void main(void)
{
    A   a1(3,5);
    B   b1(10,20);
    a1.Sum(b1); //调用该函数,因是类A的成员函数,故用类A的对象调用
}
结果是:
a1.x=30
a1.y=-10

友元类
类B是类A的友元
class A
{
    .....
    friend class B;
}
类B可以自由使用类A中的成员
class B
{
    .....
}
对于类B而言,类A是透明的
类B必须通过类A的对象使用类A的成员

类B中的任何函数都能使用类A中的所有私有成员。
const float PI =3.1415926;
class  A
{
    float r ;
    float h;
public: A(float a,float b){r=a; h=b;}
    float Getr(){return r;}
    float Geth(){return h;}
    friend class B;//定义类B为类A的友元
};
class B
{
    int number;
public: B(int n=1){number=n;}
    void Show(A &a)
    { cout<<PI*a.r*a.r*a.h*number<<endl; }//直接引用类A的私有成员,求类A的某个对象*n的体积
};
void  main(void)
{
    A a1(25,40),a2(10,40);
    B b1(2);
    b1.Show (a1); b1.Show (a2);
}

不管是按哪一种方式派生,基类的私有成员在派生类中都是不可见的。
如果在一个派生类中要访问基类中的私有成员,可以将这个派生类声明为基类的友元。
class Base 
{
    friend class  Derive;
    .....
}
class Derive 
{
    .....//直接使用Base中的私有成员
}

示例:
#include<iostream.h>
class M
{
    friend class N;  //N为M的友元,可以直接使用M中的私有成员
private:
    int i , j; 
    void show(void){cout<<"i="<<i<<'\t'<<"j="<<j<<'\t';}
public:
    M(int a=0,int b=0){ i=a; j=b;}
};
class N :public M   //N为M的派生类
{
public:
    N(int a=0,int b=0):M(a,b){   }
    void Print(void){  show();     cout<<"i+j="<<i+j<<endl;}  //直接引用类M的私有成员函数和私有成员
};
void main(void)
{
    N  n1(10,20);
    M  m1(100,200);
//  m1.show(); //私有成员函数,在类外不可调用
    n1.Print();
}

虚函数
多态性是面向对象的程序设计的关键技术。
多态性:调用同一个函数名,可以根据需要但实现不同的功能。
多态性:
(1) 编译时的多态性(函数重载)
(2) 运行时的多态性(虚函数)
运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定

可以将一个派生类对象的地址赋给基类的指针变量。
在基类和派生类中具有相同的公有函数area()。
class Point
{
    float x,y;
public:
Point(){}
    Point(float i,float j)  { x=i;y=j;}
    float area(void) {return 0.0;}
};
const float Pi=3.14159;
class Circle:public Point //类Point的派生类
{
    float radius;
public:
    Circle(float r)  { radius=r;}
    float area(void) {     return Pi*radius*radius;}
};
void main(void)
{
    Point *pp;     //基类指针,可以将派生类对象的地址赋给基类指针
    Circle c(5.4321);
    pp=&c;
    cout<<pp->area()<<endl; //调用的是基类中有的公有函数
}
在上面这种情况下,使用基类的指针时,只能访问从相应基类中继承来的成员,而不允许访问在派生类中增加的成员。输出为  0
如果要指向派生类新增的成员函数,需要将基类中的area()说明为虚函数。
若要访问派生类中相同名字的函数,必须将基类中的同名函数定义为虚函数,这样,将不同的派生类对象的地址赋给基类的指针变量后,就可以动态地根据这种赋值语句调用不同类中的函数。
class Point
{
    float x,y;
public:
    Point(){}
    Point(float i,float j){ x=i;y=j;}
    virtual  float area(void) {  return 0.0;   }  //声明为虚函数
};
const float Pi=3.14159;
class Circle:public Point //类Point的派生类
{
    float radius;
public:
    Circle(float r){ radius=r;}
    float area(void) {  return Pi*radius*radius;} //虚函数再定义
};
void main(void)
{
    Point *pp;     //基类指针,可以将派生类对象的地址赋给基类指针
    Circle c(5.4321);
    pp=&c;
    cout<<pp->area()<<endl;   //调用虚函数
}
输出:92.7011
将area()声明为虚函数,编译器对其进行动态聚束,按照实际对象c调用了Circle中的函数area()。使Point类中的area()与Circle类中的area()有一个统一的接口。

虚函数的定义和使用
    可以在程序运行时通过调用相同的函数名而实现不同功能的函数称为虚函数。定义格式为:
virtual  <type>  FuncName(<ArgList>);
一旦把基类的成员函数定义为虚函数,由基类所派生出来的所有派生类中,该函数均保持虚函数的特性。 
在派生类中重新定义基类中的虚函数时,可以不用关键字virtual来修饰这个成员函数 。

虚函数是用关键字virtual修饰的某基类中的protected或public成员函数。它可以在派生类中重新定义,以形成不同版本。只有在程序的执行过程中,依据指针具体指向哪个类对象,或依据引用哪个类对象,才能确定激活哪一个版本,实现动态聚束。

关于虚函数,说明以下几点:
1、当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回的类型也相同。若函数名相同,但参数的个数不同或者参数的类型不同时,则属于函数的重载,而不是虚函数。若函数名不同,显然这是不同的成员函数。
2、实现这种动态的多态性时,必须使用基类类型的指针变量,并使该指针指向不同的派生类对象,并通过调用指针所指向的虚函数才能实现动态的多态性。
即在程序运行时,通过赋值语句实现多态性
3、虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。
4、在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数。
5、可把析构函数定义为虚函数,但是,不能将构造函数定义为虚函数。
6、虚函数与一般的成员函数相比较,调用时的执行速度要慢一些。为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的。因此,除了要编写一些通用的程序,并一定要使用虚函数才能完成其功能要求外,通常不必使用虚函数。
7、一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性,以实现“一个接口,多个形态”。

虚函数的访问
用基指针访问与用对象名访问
用基指针访问虚函数时,指向其实际派生类对象重新定义的函数。实现动态聚束。
通过一个对象名访问时,只能静态聚束。即由编译器在编译的时候决定调用哪个函数。
下面例子中base1定义了虚函数,但是base1的基类base0没定义虚函数。
class base0
{
    public: void v(void){cout<<"base0\n";}
};
class base1:public base0
{
    public: virtual void v(void){  cout<<"base1\n";  }
};
class A1:public base1
{
    public: void v(){cout<<"A1\n";}
};
class A2:public A1
{
    public: void v(void){cout<<"A2\n";}
};
class B1:private base1
{
    public: void v(void){cout<<"B1\n";}
};
class B2:public B1
{
    public: void v(void){cout<<"B2\n";}
};
void main(void)
{
    base0 *pb;
    A1 a1;
    (pb=&a1)->v();
    A2 a2;
    (pb=&a2)->v();
    B1 b1;
    (pb=&b1)->v();//私有派生,在类外不能调用基类函数
    B2 b2;
    (pb=&b2)->v();//私有派生,在类外不能调用基类函数
}
输出:
base0
base0
例子2,main中使用的类不一样,上面例子使用的是base0,下面的是base1。
class base0
{
    public: void v(void){cout<<"base0\n";}
};
class base1:public base0
{
    public: virtual void v(void){  cout<<"base1\n";  }
};
class A1:public base1
{
    public: void v(){cout<<"A1\n";}
};
class A2:public A1
{
    public: void v(void){cout<<"A2\n";}
};
class B1:private base1
{
    public: void v(void){cout<<"B1\n";}
};
class B2:public B1
{
    public: void v(void){cout<<"B2\n";}
};
void main(void)
{
    base1 *pb;
    A1 a1;
    (pb=&a1)->v();
    A2 a2;
    (pb=&a2)->v();
}
输出:
A1
A2

纯虚函数
在基类中不对虚函数给出有意义的实现,它只是在派生类中有具体的意义。这时基类中的虚函数只是一个入口,具体的目的地由不同的派生类中的对象决定。这个虚函数称为纯虚函数。
class    <基类名>
{
    virtual <类型><函数名>(<参数表>)=0;
    ......
};
例子:
class  A   //抽象类
{
protected:
    int x;
public:
    A(){x =1000;}   
    virtual void  print()=0;  //定义纯虚函数
};
class B:public A  //派生类
{
private:
    int y;
public:
    B(){ y=2000;}
    void  print(){cout <<"y="<<y<<'\n';}//重新定义纯虚函数
};
class C:public A   //派生类
{
    int z;
public:
    C(){z=3000;}
    void  print(){cout <<"z="<<z<<'\n';}//重新定义纯虚函数
};
void  main(void)
{
    A  *pa;
    B  b;
    C  c;     
    pa=&b;
    pa->print();
    pa=&c;
    pa->print();
    A  a; //不能定义抽象类的对象
    pa=&a;
    pa->print();
}
输出:
y=2000
z=3000

1、在定义纯虚函数时,不能定义虚函数的实现部分。
2、把函数名赋于0,本质上是将指向函数体的指针值赋为初值0。与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。在没有重新定义这种纯虚函数之前,是不能调用这种函数的。
3、把至少包含一个纯虚函数的类,称为抽象类。这种类只能作为派生类的基类,不能用来说明这种类的对象。
其理由是明显的:因为虚函数没有实现部分,所以不能产生对象。但可以定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数,否则会产生程序的运行错误。
4、在以抽象类作为基类的派生类中必须有纯虚函数的实现部分,即必须有重载纯虚函数的函数体。否则,这样的派生类也是不能产生对象的。
综上所述,可把纯虚函数归结为:抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。

虚基类
多基派生中的多条路径具有公共基类时,在这条路径的汇合处就会因对公共基类产生多个拷贝而产生同名函数调用的二义性。
解决这个问题的办法就是把公共基类定义为虚基类,使由它派生的多条路径的汇聚处只产生一个拷贝。
class Base{ };
class A : public Base{ };
class B:  public Base{ };
class C: public A, public  B{ };
类C中继承了两个类Base,即有两个类Base的实现部分,在调用时产生了二义性。

由虚基类派生出的对象初始化时,直接调用虚基类的构造函数。因此,若将一个类定义为虚基类,则一定有正确的构造函数可供所有派生类调用。
用虚基类进行多重派生时,若虚基类没有缺省的构造函数,则在每一个派生类的构造函数中都必须有对虚基类构造函数的调用 (且首先调用)。
class base
{
public:
    virtual void a(){ cout<<"a() in base\n";}
    virtual void b(){ cout<<"b() in base\n";}
    virtual void c(){ cout<<"c() in base\n";}
    virtual void d(){ cout<<"d() in base\n";}
    virtual void e(){ cout<<"e() in base\n";}
    virtual void f(){ cout<<"f() in base\n";}
};
class A:public base
{
public:
    virtual void a(){ cout<<"a() in A\n";}
    virtual void b(){ cout<<"b() in A\n";}
    virtual void f(){ cout<<"f() in A\n";}
};
class B:public base
{
public:
    virtual void a(){ cout<<"a() in B\n";}
    virtual void b(){ cout<<"b() in B\n";}
    virtual void c(){ cout<<"c() in B\n";}
};
class C:public A,public B
{
public:
    virtual void a(){ cout<<"a() in C\n";}
    virtual void d(){ cout<<"d() in C\n";}
};
void main(void)
{
    C cc;
    base *pbase=&cc;//错误 将类C的地址赋值时产生歧义
    A *pa=&cc;
    pa->a();
    pa->b();
    pa->c();
    pa->d();
    pa->e();
    pa->f();
}
类C中有两个base,只有一个A。为避免这种情况,将base定义为虚基类。


class base
{
public:
    virtual void a(){ cout<<"a() in base\n";}
    virtual void b(){ cout<<"b() in base\n";}
    virtual void c(){ cout<<"c() in base\n";}
    virtual void d(){ cout<<"d() in base\n";}
    virtual void e(){ cout<<"e() in base\n";}
    virtual void f(){ cout<<"f() in base\n";}
};
class A:virtual public base
{
public:
    virtual void a(){ cout<<"a() in A\n";}
    virtual void b(){ cout<<"b() in A\n";}
    virtual void f(){ cout<<"f() in A\n";}
};
class B:virtual  public base
{
public:
    virtual void a(){ cout<<"a() in B\n";}
    virtual void c(){ cout<<"c() in B\n";}
};
class C:public A,public B
{
public:
    virtual void a(){ cout<<"a() in C\n";}
    virtual void d(){ cout<<"d() in C\n";}
};
void main(void)
{
    C cc;
    base *pa=&cc;
    pa->a(); //a() in C
    pa->b(); //b() in A
    pa->c(); //c() in B
    pa->d(); //d() in C
    pa->e(); //e() in base
    pa->f(); //f() in A
}
类C中只有一个base。


class base
{
public:
    void a(){cout<<"a() in base\n";}
    void b(){cout<<"b() in base\n";}
    void c(){ cout<<"c() in base\n";}
    void d(){cout<<"d() in base\n";}
    void e(){ cout<<"e() in base\n";}
    void f(){ cout<<"f() in base\n";}
};
class A:virtual public base
{
public:
    void a(){cout<<"a() in A\n";}
    void b(){cout<<"b() in A\n";}
    void f(){ cout<<"f() in A\n";}
};
class B:virtual  public base
{
public:
    void a(){cout<<"a() in B\n";}
    void c(){ cout<<"c() in B\n";}
};
class C:public A,public B
{
public:
    void a(){ cout<<"a() in C\n";}
    void d(){ cout<<"d() in C\n";}
};
void main(void)
{
    C cc;
    base *pa=&cc;
    pa->a(); //a() in base
    pa->b(); //b() in base
    pa->c(); //c() in base
    pa->d(); //d() in base
    pa->e(); //e() in base
    pa->f(); //f() in base
}
类C中只有一个base。

静态成员 
通常,每当说明一个对象时,把该类中的有关成员数据拷贝到该对象中,即同一类的不同对象,其成员数据之间是互相独立的。
当我们将类的某一个数据成员的存储类型指定为静态类型时,则由该类所产生的所有对象,其静态成员均共享一个存储空间,这个空间是在编译的时候分配的。换言之,在说明对象时,并不为静态类型的成员分配空间。
在类定义中,用关键字static修饰的数据成员称为静态数据成员。
不同对象,同一空间。
有关静态数据成员的使用,说明以下几点:
1、类的静态数据成员是静态分配存储空间的,而其它成员是动态分配存储空间的(全局变量除外)。当类中没有定义静态数据成员时,在程序执行期间遇到说明类的对象时,才为对象的所有成员依次分配存储空间,这种存储空间的分配是动态的;而当类中定义了静态数据成员时,在编译时,就要为类的静态数据成员分配存储空间。
2、必须在文件作用域中,对静态数据成员作一次且只能作一次定义性说明。因为静态数据成员在定义性说明时已分配了存储空间,所以通过静态数据成员名前加上类名和作用域运算符,可直接引用静态数据成员。在C++中,静态变量缺省的初值为0,所以静态数据成员总有唯一的初值。当然,在对静态数据成员作定义性的说明时,也可以指定一个初值。
class  A
{
    int  i,j;
    static int x,y;//定义静态成员
public:
    A(int a=0,int b=0,int c=0, int d=0){ i=a;j=b;x=c;y=d;}
    void Show()
    {
        cout << "i="<<i<<'\t'<<"j="<<j<<'\t';
        cout << "x="<<x<<'\t'<<"y="<<y<<"\n";
     }
};
int A::x=0; //必须对静态成员作一次定义性说明   
int A::y=0;    
void  main(void )
{
    A  a(2,3,4,5);
    a.Show();
    A  b(100,200,300,400);
    b.Show();
    a.Show();
cout <<"A::x="<<A::x<<'\n'; //可以直接用类名引用
}
a.x 和b.x在内存中占据一个空间.
a.y 和b.y在内存中占据一个空间.

3、静态数据成员具有全局变量和局部变量的一些特性。静态数据成员与全局变量一样都是静态分配存储空间的,但全局变量在程序中的任何位置都可以访问它,而静态数据成员受到访问权限的约束。必须是public权限时,才可能在类外进行访问。
4、为了保持静态数据成员取值的一致性,通常在构造函数中不给静态数据成员置初值,而是在对静态数据成员的定义性说明时指定初值。 

静态成员函数
可以将类的成员函数定义为静态的成员函数。即使用关键字static来修饰成员函数 。
class A 
{
    float x, y;
public :
    A( ){  }
    static   void sum(void)  { ..... }
};
对静态成员函数的用法说明以下几点:
1、与静态数据成员一样,在类外的程序代码中,通过类名加上作用域操作符,可直接调用静态成员函数。 
2、静态成员函数只能直接使用本类的静态数据成员或静态成员函数,但不能直接使用非静态的数据成员 (可以引用使用)。这是因为静态成员函数可被其它程序代码直接调用,所以,它不包含对象地址的this指针。 
class Tc
{
private:
    int A;
    static int B;//静态数据成员
public:
    Tc(int a){A=a;  B+=a;}
    static void display(Tc  c)//Tc的对象为形参
    {
        cout<<"A="<<c.A<<",B="<<B<<endl; //c.A非静态成员,用对象名来引用。B静态成员,直接引用
    }
};
int Tc::B=2;
void main(void)
{ Tc  a(2),b(4);
Tc::display (a);//直接用类名来调用静态成员函数
Tc::display (b);
}
3、静态成员函数的实现部分在类定义之外定义时,其前面不能加修饰词static。这是由于关键字static不是数据类型的组成部分,因此,在类外定义静态成员函数的实现部分时,不能使用这个关键字
4、不能把静态成员函数定义为虚函数。静态成员函数也是在编译时分配存储空间,所以在程序的执行过程中不能提供多态性。
5、可将静态成员函数定义为内联的(inline),其定义方法与非静态成员函数完全相同。

class Tc
{
private:
    int A;
    static int B;//静态数据成员
public:Tc(int a){A=a;  B+=a;}
    static void display(Tc  c);//函数原型,Tc的对象为形参
};
void  Tc::display(Tc  c)//类外定义,不用static修饰
{
    cout<<"A="<<c.A<<",B="<<B<<endl;
}
int Tc::B=2;
void main(void)
{
    Tc  a(2),b(4);
    Tc::display (a);
    Tc::display (b);
}


const、volatile对象和成员函数 
用const修饰的对象,只能访问该类中用const修饰的成员函数,而其它的成员函数是不能访问的。用volatile修饰的对象,只能访问该类中用volatile修饰的成员函数,不能访问其它的成员函数。
当希望成员函数只能引用成员数据的值,而不允许成员函数修改数据成员的值时,可用关键词const修饰成员函数。一旦在用const修饰的成员函数中出现修改成员数据的值时,将导致编译错误。
const和volatile成员函数
在成员函数的前面加上关键字const,则表示这函数返回一个常量,其值不可改变。 
const成员函数则是指将const放在参数表之后,函数体之前,其一般格式为:
    <type>  FuncName(<args>)  const;
其语义是指明这函数的this指针所指向的对象是一个常量,即规定了const成员函数不能修改对象的数据成员,在函数体内只能调用const成员函数,不能调用其它的成员函数。
用volatile修饰一个成员函数时,其一般格式为:
<type>  FuncName(<args>)  volatile;
其语义是指明成员函数具有一个易变的this指针,调用这个函数时,编译程序把属于此类的所有的数据成员都看作是易变的变量,编译器不要对这函数作优化工作。 
由于关键字const和volatile是属于数据类型的组成部分,因此,若在类定义之外定义const成员函数或volatile成员函数时,则必须用这二个关键字修饰,否则编译器认为是重载函数,而不是定义const成员函数或volatile成员函数。

指向类成员的指针
在C++中可以定义一种特殊的指针,它指向类中的成员函数或类中的数据成员。并可通过这样的指针来使用类中的数据成员或调用类中的成员函数。 
指向类中数据成员的指针变量
定义一个指向类中数据成员的指针变量的一般格式为:
    <type>  ClassName:: *PointName;
其中type是指针PointName所指向数据的类型,它必须是类ClassName中某一数据成员的类型。
1、指向类中数据成员的指针变量不是类中的成员,这种指针变量应在类外定义。
2、与指向类中数据成员的指针变量同类型的任一数据成员,可将其地址赋给这种指针变量,赋值的一般格式为:
    PointName = &ClassName::member;
这种赋值,是取该成员相对于该类的所在对象中的偏移量,即相对地址(距离开始位置的字节数)
如:mptr = &S::y;  
 表示将数据成员y的相对起始地址赋给指针变量mptr。
3、用这种指针访问数据成员时,必须指明是使用那一个对象的数据成员。当与对象结合使用时,其用法为:
    ObjectName.* PointName
4、由于这种指针变量并不是类的成员,所以使用它只能访问对象的公有数据成员。若要访问对象的私有数据成员,必须通过成员函数来实现。

指向类中成员函数的指针变量
定义一个指向类中成员函数的指针变量的一般格式为:
<type>  (ClassName:: *PointName)(<ArgsList>);
其中PointName是指向类中成员函数的指针变量;ClassName是已定义的类名;type是通过函数指针PointName调用类中的成员函数时所返回值的数据类型,它必须与类ClassName中某一成员函数的返回值的类型相一致;<ArgsList>是函数的形式参数表。
在使用这种指向成员函数的指针前,应先对其赋值
PointName= ClassName::FuncName;
同样地,只是将指定成员函数的相对地址赋给指向成员函数的指针。
在调用时,用(对象名.指针)( )的形式。
比较 :int max( int a,int b)
{ return (a>b?a:b);}
若有:int  (*f)( int, int ); f=max;
则调用时    (*f)(x,y);
所以:(s1.*mptr1)( );  (s1.*mptr2)(100);
或: (ps->*mptr1)();(ps->*mptr2)(100);

对指向成员函数的指针变量的使用方法说明以下几点:
1、指向类中成员函数的指针变量不是类中的成员,这种指针变量应在类外定义。
2、不能将任一成员函数的地址赋给指向成员函数的指针变量,只有成员函数的参数个数、参数类型、参数的顺序和函数的类型均与这种指针变量相同时,才能将成员函数的指针赋给这种变量。 
3、使用这种指针变量来调用成员函数时,必须指明调用那一个对象的成员函数,这种指针变量是不能单独使用的。用对象名引用。
4、由于这种指针变量不是类的成员,所以用它只能调用公有的成员函数。若要访问类中的私有成员函数,必须通过类中的其它的公有成员函数。
5、当一个成员函数的指针指向一个虚函数,且通过指向对象的基类指针或对象的引用来访问该成员函数指针时,同样地产生运行时的多态性。
6、当用这种指针指向静态的成员函数时,可直接使用类名而不要列举对象名。这是由静态成员函数的特性所确定的。

运算符重载
函数的重载
所谓函数的重载是指完成不同功能的函数可以具有相同的函数名。 
C++的编译器是根据函数的实参来确定应该调用哪一个函数的。 
1、定义的重载函数必须具有不同的参数个数,或不同的参数类型。只有这样编译系统才有可能根据不同的参数去调用不同的重载函数。
2、仅返回值不同时,不能定义为重载函数。 

运算符重载就是赋予已有的运算符多重含义。C++通过重新定义运算符,使它能够用于特定类的对象执行特定的功能。
运算符的重载从另一个方面体现了OOP技术的多态性,且同一运算符根据不同的运算对象可以完成不同的操作。 
为了重载运算符,必须定义一个函数,并告诉编译器,遇到这个重载运算符就调用该函数,由这个函数来完成该运算符应该完成的操作。这种函数称为运算符重载函数,它通常是类的成员函数或者是友元函数。运算符的操作数通常也应该是类的对象。
重载为类的成员函数
格式如下(operator是关键字,参数表是运算的对象):
<类名>  operator<运算符>(<参数表>)
{ 函数体 }
例如:
A  operator +  (A &);//重载了类A的“+”运算符
其中:operator是定义运算符重载函数的关键字,它与其后的运算符一起构成函数名。
class A
{
    int i;
public:
    A(int a=0){ i=a; }
    void Show(void){ cout<<"i="<<i<<endl;}
    void AddA(A &a,   A &b) //利用函数进行类之间的运算
    { i=a.i+b.i;}
    A operator +(A &a) //重载运算符+
    { A   t; t.i=i+a.i; return t;}
};
void main(void)
{
    A a1(10),a2(20),a3;
    a1.Show ();
    a2.Show ();
    a3=a1+a2; //重新解释了加法,可以直接进行类的运算,相当于a3=a1.operator+(a2)
    a3.AddA(a1,a2); //调用专门的功能函数
    a3.Show ();
}

重载运算符与一般函数的比较:
相同:1)均为类的成员函数;2)实现同一功能
void  AddA(A &a,   A &b)
{ i=a.i+b.i; }
函数调用:
a3.AddA(a1,a2);//由对象a3调用
A operator +(A &a)
{
    A   t;
    t.i=i+a.i;
    return t;
}
函数调用:
a3=a1+a2;
a3=a1.operator+(a2);//由对象a1调用

总结:
重新定义运算符,由左操作符调用右操作符。最后将函数返回值赋给运算结果的对象。
当用成员函数实现运算符的重载时,运算符重载函数的参数只能有二种情况:没有参数或带有一个参数。对于只有一个操作数的运算符(如++),在重载这种运算符时,通常不能有参数;而对于有二个操作数的运算符,只能带有一个参数。这参数可以是对象,对象的引用,或其它类型的参数。在C++中不允许重载有三个操作数的运算符
只能对C++中已定义了的运算符进行重载,而且,当重载一个运算符时,该运算符的优先级和结合律是不能改变的。

单目运算符的重载
只具有一个操作数的运算符为单目运算符,最常用的为++及--。
A    a;
++a;
a++;

A    a, b;
b=++a;
b=a++;
可以看出,虽然运算后对象a的值一致,但先自加或后自加的重载运算符函数的返回值不一致,必须在重载时予以区分。
++为前置运算时,它的运算符重载函数的一般格式为:
<type> operator ++( )
{    ......;}
++为后置运算时,它的运算符重载函数的一般格式为:
<type>  operator ++(int)
{    ......;}
例子:
class A
{
    float x, y;
public:
    A(float a=0, float b=0){  x=a;  y=b;  }
//因为结果要赋值,所以函数要有返回值
    A operator ++( ){A  t;  t.x=++ x; t.y=++y;  return t;}
    A operator ++(int) {  A  t;  t.x=x++;  t.y=y++;  return t;}
};
void main(void)
{
    A   a(2,3), b;
    b=++a;
    b=a++;
}
结果:
ic1.c1=11    ic3.c2=21
ic3.c1=11    ic3.c2=21
ic2.c1=20    ic2.c2=200
ic4.c1=100   ic4.c2=200

用成员函数实现运算符的重载时,运算符的左操作数为当前对象,并且要用到隐含的this指针。运算符重载函数不能定义为静态的成员函数,因为静态的成员函数中没有this指针。

运算符重载为友元函数
运算符重载为成员函数时,是由一个操作数调用另一个操作数。
A  a ,b ,  c;
c=a+b; 实际上是c=a.operator+(b);
c=++a; 实际上是c=a.operator++();
c+=a;  实际上是c.operator+=(a);
即函数的实参只有一个或没有。
友元函数是在类外的普通函数,与一般函数的区别是可以调用类中的私有或保护数据。
将运算符的重载函数定义为友元函数,参与运算的对象全部成为函数参数。
A  a ,b ,  c;
c=a+b; 实际上是 c=operator+(a, b);
c=++a; 实际上是 c=operator++(a);
c+=a;  实际上是 operator+=(c, a);

对双目运算符,友元函数有2个参数,对单目运算符,友元函数有一个参数。有些运算符不能重载为友元函数,它们是:=,(),[ ],->等
格式为:
friend <类型说明> operator<运算符>(<参数表>)
{......}
c=a+b;   //  c=operator+( a, b)
friend   A   operator + (A &a, A &b)
{.....}
例子:
class A
{
    int i;
public:public:
    A(int a=0) { i=a;}
    void Show(void) {cout<<"i="<<i<<endl;}
    friend A operator +(A &,A &);//友元函数,两个参数,为引用
};
A operator +(A &a , A &b)
{
    A t;
    t.i=a.i+b.i;
    return t;
}
void main(void)
{
    A a1(10),a2(20),a3;
    a1.Show ();
    a2.Show ();
    a3=a1+a2; //重新解释了加法,可以直接进行类的运算。相当于a3=operator+(a1,a2)
    a3.Show ();
}

++为前置运算时,它的运算符重载函数的一般格式为:
A operator ++(A &a)
{    ......;}
++为后置运算时,它的运算符重载函数的一般格式为:
A operator ++(A &a, int)
{    ......;}
例子:
class A
{
    int i;
public:
    A(int a=0) { i=a;}
    void Show(void) {cout<<"i="<<i<<endl;}
    friend A operator++(A &a){ a.i++;  retrurn a;}//前置
    friend A operator++(A &a, int n)//后置
    {  A  t; t.i=a.i;   a.i++;         return t;}                
};
void main(void)
{
    A a1(10),a2,a3;
    a2=++a1;//相当于a2=operator++(a1)
    a3=a1++;//相当于a3=operator++(a1,int)
    a2.Show();
    a3.Show ();
}
对双目运算符,重载为成员函数时,仅一个参数,另一个被隐含;重载为友元函数时,有两个参数,没有隐含参数。
一般来说,单目运算符最好被重载为成员函数;对双目运算符最好被重载友元函数。

转换函数
转换函数就是在类中定义一个成员函数,其作用是将类转换为某种数据类型。
1. 转换函数必须是类的成员函数。
2. 转换函数的调用是隐含的,没有参数。
格式为(其中ClassName为类名,operator是关键字,type是欲转换类型):
ClassName :: operator <type>( )
{........./*具体的转换算法*/}
例子1:
class A
{
    int i;
public:
    A(int a=0) { i=a;}
    void Show(void)
    { cout<<"i="<<i<<endl;}
    operator int( ){   returni;}
};
void main(void)
{
    A a1(10),a2(20);
    cout<<a1<<endl;
    cout<<a2<<endl;
}
例子2:
class Complex
{
    float Real,Image;
public:
    Complex(float real=0,float image=0)
    { Real=real;Image=image;}
    void Show(void)
    {cout<<"Real="<<Real<<'\t'<<"Image="<<Image<<endl;}
    operator float(); //成员函数,定义类转换 Complex->float
};
Complex::operator float ()
{
    return Real*Real+Image*Image;}
void main(void)
{
    Complex c(10,20);
    c.Show ();
    cout<<c<<endl;//可以直接输出c,因为已经进行类型转换
}
注意,转换函数只能是成员函数,不能是友元函数。转换函数的操作数是对象。转换函数可以被派生类继承,也可以被说明为虚函数。

赋值运算符与赋值运算符重载 “=”
同类型的对象间可以相互赋值,等同于对象的各个成员的一一赋值。
A   a(2,3), b;
b=a; 
但当对象的成员中使用了动态的数据类型时(用new开辟空间),就不能直接相互赋值,否则在程序的执行期间会出现运行错误。
这时,利用编译系统的默认赋值无法正确运行程序,必须重载赋值运算符“=”,即重新定义“=”。
格式为:
<函数类型>   <ClassName>::operator=(<参数表>)
赋值运算符必须重载为成员函数。
A  A:: operator = (A  &a)
左操作符调用右操作符
class  A
{
    char *ps;
public:
    A( ){ ps=0;  }
    A(char *s ){ ps =new char [strlen(s)+1];  strcpy(ps,s);  }
    ~A( ){ if (ps) delete ps;}
    void Show(void) {  cout<<ps<<endl;  }
    A&  operator=(A  &b);
};
A &A::operator=(A  &b)//重载赋值运算符
{
    if( ps )  delete  []ps;
    if( b.ps) 
    {
        ps = new  char[strlen(b.ps)+1];//重新开辟内存
        strcpy(ps, b.ps);
    }
    else
        ps =0;
    return *this;
}
void  main(void)
{
    A s1("China!"),s2("Computer!");
    s1.Show();
    s2.Show();
    s2=s1; //s2.ps重新开辟内存,存放“China!”
    s1.Show();
    s2.Show();
}

一个字符串类
在C++中,系统提供的字符串处理能力比较弱,都是通过字符处理函数来实现的,并且不能直接对字符串进行加法、减法,字符串的拼接,字符串之间的相互赋值等操作。可以通过应用C++提供的运算符重载机制,可以提供字符串的直接操作能力,使得字符串的操作与一般的数据一样方便。
字符串类只定义了指针,并没有开辟具体的空间以存放字符串的内容,所以,无论是构造、析构还是加减等,均需要考虑动态开辟空间的问题,这也是字符串类的难点。
若不定义字符串的析构函数,则可以不定义它的拷贝的构造及赋值函数,若定义了析构函数,必须重新定义这两个成员函数。
原则:每个对象都有自己的独立空间。

输入/输出流类库 
输入输出流(I/O Stream)
C++语言的I/O系统向用户提供一个统一的接口,使得程序的设计尽量与所访问的具体设备无关,在用户与设备之间提供了一个抽象的界面:输入输出流。
用标准流进行输入/输出时,系统自动地完成数据类型的转换。对于输入流,要将输入的字符序列形式的数据变换成计算机内部形式的数据(二进制或ASCII)后,再赋给变量,变换后的格式由变量的类型确定。对于输出流,将要输出的数据变换成字符串形式后,送到输出流(文件)中。 
重载输入(提取)和输出(插入)运算符
在C++中允许用户重载运算符“<<”和“>>”,实现对象的输入和输出。重载这二个运算符时,在对象所在的类中,将重载这二个运算符的函数说明该类的友元函数。

重载提取运算符的一般格式为:
friend  istream &  operater >>(istream &, ClassName &);
返回值类型:类istream的引用,cin中可以连续使用运算符“>>”。
第一个参数:是“>>”的左操作数cin类型,类istream的引用
第二个参数:是“>>”的右操作数,即欲输入的对象的引用.

重载输出(插入)运算符的一般格式为:
friend  ostream &  operater <<(ostream &, ClassName &);
与输入(提取)运算符比较:将输入流改为输出流。
例子:
class incount
{
    int c1,c2;
public:
    incount(int a=0,int b=0) {c1=a;c2=b;}
    void show(void) {cout<<"c1="<<c1<<'\t'<<"c2="<<c2<<endl;}
    friend istream & operator>>(istream &,incount &);
    friend ostream & operator<<(ostream &,incount &);//重载输出函数原型说明
};
istream & operator>>(istream &is,incount &cc)
{
    is>>cc.c1>>cc.c2;
    return is;
}
ostream &operator<<(ostream &os,incount &cc) //重载cout<<
{
    os<<"c1="<<cc.c1<<'\t'<<"c2="<<cc.c2<<endl;
    return os;
}
void main(void)
{
    incount x1,x2;
    cout<<x1<<x2;//调用输出函数
    cin>>x1; //调用输入函数
    cin>>x2;
    cout<<x1<<x2;
}

文件流 
C++在头文件fstream.h中定义了C++的文件流类体系 ,当程序中使用文件时,要包含头文件fstream.h。
当使用文件时,在程序头有:#include<fstream.h>,其中定义了各种文件操作运算符及函数。
在涉及文本文件的操作时,将输入文件看成键盘,将输出文件看成显示器,格式不变。只需在程序中增加打开与关闭文件的语句。
文件的操作
文本文件:
以ASCII表示的文件:记事本,*.cpp等
二进制文件:
用二进制形式表示的文件:可执行程序*.EXE等
不同的文件操作的函数、格式不同
文本文件的打开与关闭
在文件操作前,需要将程序与被操作的文件联系起来,使程序可以“引用”文件。
在程序内定义一个文件类的对象,由该对象与文件发生联系,程序内所有的与文件的操作都是对该对象的操作。
fstream   infile, outfile;  //两个对象,可以联系两个输入输出文件
ifstream  infile;  //对象只能联系输入文件
ofstream  outfile;  //对象只能联系输出文件

如何使文件类的对象与欲操作的文件发生联系?
用对象打开文件:
ifstream  infile;  //定义输入文件类对象
infile.open("myfile1.txt");//利用函数打开某一文件
打开文件的作用是,使文件流对象与要使用的文件名之间建立联系。
ofstream  outfile;  //定义输出文件类对象
outfile.open("myfile1.txt");//打开某一文件供输出

如何从文件中输入输出数据?
将文件类对象看成键盘和显示器即可。
ifstream  infile;  //定义输入文件类对象
infile.open("myfile1.txt");//利用函数打开某一文件
float  x , y;
infile>>x>>y;
用infile代替myfile1.txt进行操作。
ofstream  outfile;  //定义输出文件类对象
infile.open("myfile2.txt");//利用函数打开某一文件
float x=3, y=4;
outfile<<x<<'\t'<<y<<endl;
用outfile代替myfile2.txt进行操作。

用完文件后,使用成员函数关闭文件.

当用类fstream定义文件对象时,该对象即能定义输入文件对象,又能定义输出文件对象,所以打开文件时,必须在成员函数open()中的参数中给出打开方式(读或写)。
fstream    pfile1,pfile2;//定义了两个文件类的对象
pfile1.open(“file1.txt”,  ios::in);//pfile1联系到“file1.txt”,用于输入
pfile2.open(“file2.txt”, ios::out);//pfile2联系到“file2.txt”,用于输出
char  ch;
pfile1>>ch; //输入
pfile2<<ch; //输出
pfile1.close();
pfile2.close();
在打开文件后,都要判断打开是否成功。若打开成功,则文件流对象值为非零值;若打开不成功,则其值为0。 
打开输入文件时,文件必须存在。
打开输出文件时,若文件不存在,则建立文件;若文件存在,则删除原文件的内容,使其成为一个空文件。

涉及到字符串的文件读写
char  ch, str[300];
ifstream  infile("myfile1.txt"); 
ofstream  outfile("myfiel2.txt");
从键盘输入一个字符:cin.get(ch);
从文件输入一个字符:infile.get(ch);
向显示器输出一个字符:cout.put(ch);
向文件输出一个字符:outfile.put(ch);
从键盘输入一行字符:cin.getline(str,300);
从文件输入一行字符:infile.getline(ch,300);
从文件输入一字符或一行字符,当输入至文件尾时,函数返回值为0,可以据此来判断循环结束。
例子(实现两文件的拷贝的程序):
void main(void)
{
    char filename1[256],filename2[256];
    char buf[300];
    cout<<"Input source file name:  ";
    cin>>filename1;//输入文件(源文件)名
    cout<<"Input destination:  ";
    cin>>filename2;//输出文件(目的文件)名
    fstream infile,outfile;
    infile.open(filename1,ios::in);//用函数打开文件
    outfile.open(filename2,ios::out);
    while(infile.getline(buf,300))//从源文件中读取一行字符,至文件尾停止循环
        outfile<<buf<<endl;//将该行字符输出至目的文件
    outfile.close();//关闭文件
    infile.close();
}

二进制文件的读写操作
若在文件的打开方式中没有特别说明,打开的文件均为ASCII码文件,若要打开二进制文件,则要特别说明并用特定的读写函数。
fstream   infile,outfile;
infile.open("inf1.dat",  ios::in| ios::binary);//输入方式打开二进制文件
outfile.open("outf1.dat",  ios::out| ios::binary);//输出方式打开二进制文件
由于二进制文件中的数据不是ASCII码,故不能直接对其读写,必须要通过特定的函数予以转换。
输入函数:
infile.read(char  *, int )
int  a[10];
infile.read((char *)a, 10*sizeof(int));//从文件中输入十个整型数到a,地址要强制转换成字符型
输出函数:
outfile.write( char  *,   int )
int  a[10]={0,1,2,3,4,5,6,7,8,9};
outfile.write((char *)a,  10*sizeof(int));//向文件输出一个整型数组a,地址要强制转换成字符型
判断二进制文件是否读到文件尾?
infile.eof( )    
当到达文件结束位置时,该函数返回一个非零值;否则返回零。
void  main(void)
{
    char  filename1[256],filename2[256];
    char  buff[4096];
    cout <<”输入源文件名:”;   cin  >>filename1;
    cout <<”输入目的文件名:”;  cin  >>filename2;
    fstream  infile,outfile;
    infile.open(filename1,ios::in | ios::binary);
    outfile.open(filename2,ios::out | ios::binary);
    int n;
    while (!infile.eof())
    { //文件不结束,继续循环
        infile.read(buff,4096); //一次读4096个字节
        n=infile.gcount(); //取实际读的字节数
        outfile.write(buff,n); //按实际读的字节数写入文件
    }
    infile.close();
    outfile.close();


文件指针
当一打开文件,文件指针位于文件头,并随着读写字节数的多少顺序移动。
可以利用成员函数随机移动文件指针。
随机读取二进制文件
infile.seekg(int);//将文件指针移动到由参数指定的字节处
infile.seekg(100);//将文件指针移动到距离文件头100个字节处
infile.seekg(int, ios::_dir);
相对位置:
_dir:    beg:  文件头
         cur:  当前位置
         end:  文件尾
infile.seekg(100, ios::beg);//移动到距文件头100个字节
infile.seekg(-100, ios::cur);//移动到距当前位置前100个字节
infile.seekg(-500, ios::end);//移动到距文件尾前500个字节
例子:
void  main(void)
{
    ofstream  outfile("data1.dat",ios::out| ios::binary); 
    int  i;
    for(i=5;i< 1000;i+=2 )
    outfile.write((char*)&i,sizeof(int)); //将奇数写入文件
    outfile.close();//关闭文件
    ifstream  f1("data.dat",ios::in| ios::binary); 
    int x;
    f1.seekg(20*sizeof(int));//将文件指针移到第20个整数的位置
    for(i=0;i<10;i++)
    {
        f1.read((char *)&x,sizeof(int)); //依次读出第20~29个奇数到x
        cout<< x<< '\t';
    }
    f1.close();
}

0 0
原创粉丝点击