Visual C++实用教程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1 类和对象

类是面向对象程序设计的核心,它实际上是一种新的数据类型,也是实现抽象类型的工具,因为类是通过抽象数据类型的方法来实现的一种数据类型。类是对某一类对象的抽象;而对象是某一种类的实例,因此,类和对象是密切相关的。

2.1.1 类的定义

类的定义一般分为声明部分和实现部分。声明部分用来声明该类中的成员,包含数据成员(或称“成员变量”)的声明和成员函数的声明。成员函数是用来对数据成员进行操作的,又称方法。实现部分用来对成员函数进行定义。概括地说,声明部分是告诉使用者“干什么”,而实现部分是告诉使用者“怎么干”。

C++中定义类的一般格式如下:

class <类名>
{
    private:
        [<私有型数据和函数>]
    public:
        [<公有型数据和函数>]
    protected:
        [<保护型数据和函数>]
};
<各个成员函数的实现>

其中,class是定义类的关键字,class的后面是用户定义的类名(它通常用大写的C字母开始的标识符来描述,C表示Class,以与对象、函数及其他数据类型名相区别)。类中的数据和函数是类的成员,分别称为数据成员和成员函数。由一对花括号构成的是类体。注意,类体中最后一个花括号后面的分号“;”不能省略。

类中的关键字public、private和protected声明了类中的成员与程序其他部分(或类外)之间的关系,称为访问权限。对于public成员来说,它们是公有的,能被外面的程序访问;对于private成员来说,它们是私有的,不能被外面的程序所访问。数据成员只能由类中的函数所使用,成员函数只允许在类中调用。而对于protected成员来说,它们是受保护的,具有半公开性质,可在类中或其子类中访问(以后还会讨论)。

<各个成员函数的实现>是类定义中的实现部分,这部分包含所有在类体中声明的函数的定义(即对成员函数的实现)。如果一个成员函数在类体中定义,则其实现部分将不需要。如果所有的成员函数都在类体中定义,则实现部分可以省略。需要说明的是,当类的成员函数的函数体在类的外部定义时,必须由作用域运算符“::”来通知编译系统该函数所属的类。例如:

class  CMeter
{
public:
    double m_nPercent;                // 声明一个公有数据成员
    void StepIt();                    // 声明一个公有成员函数
    void SetPos(int nPos);            // 声明一个公有成员函数
    int  GetPos()
    {
            return m_nPos;
    }                                // 声明一个公有成员函数并定义
private:
    int  m_nPos;                     // 声明一个私有数据成员
};                                   // 注意分号不能省略
void CMeter::StepIt()
{
    m_nPos++;
}
void CMeter::SetPos(int nPos)
{
    m_nPos = nPos;
}

类CMeter中,成员函数GetPos是在类体中定义的,而StepIt和SetPos是在类的外部定义的,注意两者的区别。另外,定义类时还应注意:

(1)类中的数据成员的类型可以是任意的,包含整型、浮点型、字符型、数组、指针和引用等,也可以是另一个类的对象,但不允许对所定义的数据成员进行初始化,也不能指定除static之外的任何存储类型。例如,类CMeter中,下面的定义是错误的:

class  CMeter
{   //…
private:
    int  m_nPos=10;           // 错误:不能直接对数据成员进行初始化
    auto int n;               // 错误:不合法的存储类型
    //…
};

(2)在“public:”、“protected:”或“private:”后面定义的所有成员都是公有、保护或私有的,直到下一个“public:”、“protected:”或“private:”出现为止。若成员前面没有任何访问权限的指定,则所定义的成员是private(私有),这是类的默认设置。事实上,结构也可看成类的一种简单形式,只是其成员的默认访问权限是公有的。一般来说,当只需要描述数据结构而不想在结构中进行数据操作时,则用结构较好。而若既要描述数据又要描述对数据的处理方法时,则用类为好。

(3)关键字public、protected和private可以在类中出现多次,且前后的顺序没有关系;但最好先声明公有成员,后声明私有成员,因为public成员是用户最关心的。每个访问权限关键词为类成员所确定的访问权限是从该关键词开始到下一个关键词为止的。

(4)在进行类设计时,通常将数据成员声明为私有的,而将大多数成员函数声明成公有的。这样,类以外的代码就不能直接访问类的访问权限私有数据,从而实现了数据的封装。而公有成员函数可为内部的私有数据成员提供外部接口,但接口实现的细节在类外又是不可见的,这就是C++类的优点之一。

(5)尽量将类单独存放在一个文件中,或将类的声明放在.h文件中,而将成员函数的实现放在与.h文件同名的.cpp文件中。以后将会看到,Visual C++ 6.0为用户创建的应用程序框架中都是将各个类以.h和同名的.cpp文件组织的。

2.1.2 对象的定义

作为一种复杂的数据构造类型,类声明后,就可以定义该类的对象。与结构类型一样,它也有三种定义方式:声明之后定义、声明之时定义和一次性定义。但由于“类”比任何数据类型都要复杂得多,为了提高程序的可读性,真正将“类”当成一个密闭、“封装”的盒子(接口),在程序中应尽量在对象的声明之后定义方式,并按下列格式进行:

<类名> <对象名表>

其中,类名是用户已定义过的类的标识符,对象名可以有一个或多个,多个时要用逗号分隔。被定义的对象既可以是一个普通对象,也可以是一个数组对象或指针对象。例如:

CMeter  myMeter,*Meter,Meters[2];

这时,myMeter是类CMeter的一个普通对象,Meter和Meters分别是该类的一个指针对象和对象数组。

一个对象的成员就是该对象的类所定义的数据成员(成员变量)和成员函数。访问对象的成员变量和成员函数和访问变量和函数的方法是一样的,只不过要在成员前面加上对象名和成员运算符“.”,其表示方式如下:

<对象名>.<成员变量>
<对象名>.<成员函数>(<参数表>)

例如:

myMeter.m_nPercent,  myMeter.SetPos(2),  Meters[0].StepIt();

需要说明的是,一个类对象只能访问该类的公有型成员,而对于私有型成员则不能访问。上述成员m_nPercent、SetPos、StepIt都是public访问类型。

若对象是一个指针,则对象的成员访问形式如下:

<对象指针名>-><成员变量>
<对象指针名>-><成员函数>(<参数表>)

“->”是一个表示成员的运算符,它与“.”运算符的区别是:“->”用来表示指向对象的指针的成员,而“.”用来表示一般对象的成员。

需要说明的是,下面的两种表示是等价的:

<对象指针名>-><成员变量>
(*<对象指针名>).<成员变量>

这对于成员函数也适用。另外,对于引用类型对象,其成员访问形式与一般对象的成员访问形式相同。例如:

CMeter  &other=one;
cout<< other.GetPos()<<endl;

2.1.3 类作用域和成员访问权限

类的作用域是指在类的定义中由一对花括号所括起来的部分。从类的定义可知,类作用域中可以定义变量,也可以定义函数。从这一点上看,类作用域与文件作用域很相似。但是,类作用域又不同于文件作用域,在类作用域中定义的变量不能使用auto、register和extern等修饰符,只能用static修饰符,而定义的函数也不能用extern修饰符。另外,在类作用域中的静态成员和成员函数还具有类外的连接属性(以后会讨论)。文件作用域中可以包含类作用域,显然,类作用域小于文件作用域。一般地,类作用域中可包含成员函数的作用域。

1.类名的作用域

如果在类声明时指定了类名,则类名的作用范围是从类名指定的位置开始一直到文件结尾,即类名是具有文件作用域的标识符。若类的声明是放在头文件中的,则类名在程序文件中的作用范围是从包含预处理指令位置处开始一直到文件结尾。

需要说明的是,如果在类声明之前就需要使用该类名定义对象,则必须用下列格式在使用前进行提前声明(注意,类的这种形式的声明可以在相同作用域中出现多次):

class <类名>;

例如:

class COne;                       // 将类COne提前声明
class COne;                       // 可以声明多次
class  CTwo
{   //…
private:
    COne a;                       // 数据成员a是已定义的COne类对象
};
class  COne
{   //…
};

2.类中成员的可见性

(1)在类中使用成员时,成员声明的前后不会影响该成员在类中的使用,这是类作用域的特殊性。例如:

class  A
{
    void f1()
    {
        f2();                  // 调用类中的成员函数f2
        cout<<a<<endl;         // 使用类中的成员变量a
    }
    void f2(){}
    int a;
};

(2)由于类的成员函数可以在类体外定义,因而此时由“类名::”指定开始一直到函数体最后一个花括号为止的范围也是该类作用域的范围。例如:

class  A
{
    void f1();
    //…
};
void A::f1()
{   //…
}

则从A::开始一直到f1函数体最后一个花括号为止的范围都是属于类A的作用域。

(3)在同一个类的作用域中,不管成员具有怎样的访问权限,都可在类作用域中使用,而在类作用域外却不可使用。例如:

class  A
{
public:
    int a;
    //…
};
a=10;                       // 错误,不能在A作用域外直接使用类中的成员

3.类外对象成员的可见性

对于访问权限public、private和protected来说,只有在子类中或用对象来访问成员时,它们才会起作用。在用类外对象来访问成员时,只能访问public成员,而对private和protected均不能访问。

2.1.4 构造函数和析构函数

事实上,一个类总有两种特殊的成员函数:构造函数和析构函数。构造函数的功能是在创建对象时,使用给定的值将对象初始化。析构函数的功能是用来释放一个对象,在对象删除前,用它来做一些内存释放等清理工作,它与构造函数的功能正好相反。

1.构造函数

前面已提及,在类的定义中是不能对数据成员进行初始化的。为了能给数据成员设置某些初值,就要使用类的特殊成员函数——构造函数。构造函数的最大特点是在对象建立时它会被自动执行,因此用于变量、对象的初始化代码一般都放在构造函数中。

C++规定,一个类的构造函数必须与相应的类同名,它可以带参数,也可以不带参数,与一般的成员函数定义相同,可以重载,也可以有默认的形参值。例如:

class  CMeter
{
public:
    CMeter(int nPos)                    // 带参数的构造函数
    {
        m_nPos = nPos;
    }
    //…
}

这样若有:

CMeter oMeter(10), oTick(20);

则会自动调用构造函数CMeter(int nPos),从而使得对象oMeter中的私有成员m_nPos的值为10,使得对象oTick中的私有成员m_nPos的值为20。

2.对构造函数的几点说明

虽然构造函数的定义方式与一般成员函数没有什么区别,但要注意:

(1)构造函数的约定使系统在生成类的对象时自动调用。同时,指定对象括号里的参数就是构造函数的实参,例如,oMeter(10)就是oMeter. CMeter(10)。故当构造函数重载及设定构造函数默认形参值时,要避免出现二义。

CPerson(char*str,float h=170,float w=130)   //A
{
    strcpy(name, str);
    height = h;
    weight = w;
}
CPerson(char*str)                           //B
{
    strcpy(name, str);
}

则当执行“CPerson other("DING");”时,即“other.CPerson("DING");”,因编译无法确定是上述哪一个构造函数的调用,从而出现编译错误。

(2)定义的构造函数不能指定其返回值的类型,也不能指定为void类型。事实上,由于构造函数主要用于对象数据成员的初始化,因而无须返回函数值,也就无须有返回类型。

(3)若要用类定义对象,则构造函数必须是公有型成员函数,否则类无法实例化(即无法定义对象)。若类仅用于派生其他类,则构造函数可定义为保护型成员函数。

3.默认构造函数

实际上,在类定义时,如果没有定义任何构造函数,则编译自动为类隐式生成一个不带任何参数的默认构造函数,由于函数体是空块,因此默认构造函数不进行任何操作,仅仅为了满足对象创建时的语法需要。其形式如下:

<类名>()
{}

例如,对于CMeter类来说,默认构造函数的形式如下:

CMeter()                         // 默认构造函数的形式
{}

默认构造函数的目的是使下列对象定义形式合法:

CMeter  one;                      //one.CMeter(); 会自动调用默认构造函数

此时,由于对象one没指定任何初值,因而编译会自动调用类中隐式生成的默认构造函数对其初始化。需要说明的是:

(1)默认构造函数对数据成员初值的初始化还取决于对象的存储类型。例如:

CMeter  one;                        // 自动存储类型,数据成员的初值为无效值
static CMeter  one;                 // 静态存储类型,数据成员的初值为空值或0

(2)若类定义中指定了构造函数,则隐式的默认构造函数不再存在,因此,对于前面定义的CMeter类来说,若有:

CMeter  four;                      // 错误

则因为找不到默认构造函数而出现编译错误。此时,在类中还要给出默认构造函数的具体定义,即定义一个不带任何参数的构造函数,称为显式的默认构造函数,这样才能对four进行定义并初始化。

(3)在定义对象时,不能写成“CMeter four();”,因为这是一个函数的声明。

4.析构函数

与构造函数相对应的是析构函数。析构函数是另一个特殊的C++成员函数,它只是在类名称前面加上一个“~”符号(逻辑非),以示与构造函数功能相反。每一个类只有一个析构函数,没有任何参数,也不返回任何值。例如:

class  CMeter
{
public:
    //…
    ~CMeter()                    // 析构函数
    {
    }
    //…
}

析构函数只有在下列两种情况下才会被自动调用:

(1)当对象定义在一个函数体中,该函数调用结束后,析构函数被自动调用。

(2)用new为对象分配动态内存,当使用delete释放对象时,析构函数被自动调用。

与默认构造函数类似,若类的声明中没有定义析构函数,则编译也会自动生成一个隐式的不做任何操作的默认析构函数。

5.应用示例

类的构造函数和析构函数的一个典型应用是在构造函数中用new为指针成员开辟独立的动态内存空间,而在析构函数中用delete释放它们。

【例Ex_Name】 使用构造函数和析构函数

#include <iostream.h>
#include <string.h>
class CName
{
public:
    CName()                             //A:显式默认构造函数
    {
        strName=NULL;                   // 空值
    }
    CName(char*str)                     //B
    {
        strName = str;
    }
    ~CName() {}                         // 显式默认析构函数
    char*getName()                      // 获取字符串
    {
        return strName;
    }
private:
    char *strName;                      // 字符指针,名称
};
int  main()
{
    char*p=new char[5];                  // 为p开辟内存空间
    strcpy(p,"DING");                   // p指向的内存空间的值为"DING"
    CName one(p);                        // 对象初始化
    delete[]p;                          // 释放p的内存空间
    cout<<one.getName()<<endl;
    return 0;
}

由于“CName one(p);”调用的是B重载构造函数,从而使得私有指针成员strName的指向等于p的指向。而p指向new开辟的内存空间,其内容为“DING”,一旦p指向的内存空间删除后,p的指向就变得不确定了,此时strName指向也不确定,所以此时运行结果为:

m葺葺葺葺?

显然,输出的是一个无效的字符串。因此,为了保证类的封装性,类中的指针成员所指向的内存空间必须在类中自行独立开辟和释放。因此,类CName应改成下列代码:

class CName
{
public:
    CName()                          //A:显式默认构造函数
    {
        strName=NULL;                // 空值
    }
    CName(char*str)                  //B
    {
        strName = (char *)new char[strlen(str)+1];
        // 因字符串后面还有一个结束符,因此内存空间的大小要多开辟1个内存单元
        strcpy(strName,str);         // 复制内容
    }
    ~CName()
    {
        if(strName)    delete[]strName;
        strName=NULL;                // 一个好习惯
    }
    char *getName()
    {
        return strName;
    }
private:
    char *strName;                 // 字符指针,名称
};

这样,主函数中的代码才会有正确的运行结果:

DING

总之,为了使类具有通用性,通常将类中的字符串数据用char指针来描述,如char *strName,但此时应将strName在构造函数中另开辟独立的内存空间来存储字符串,然后在析构函数中释放,以保证成员数据在类中的封装性。若在构造函数中将指针成员直接指向字符串或指向外部的存储字符串的内存空间,则会出现潜在的危险。

1.赋值

在C++中,一个类的对象的初值设定可以有多种形式。例如,对于前面的类CName来说,则可有下列对象的定义方式:

CName o1;                         // 通过A显式默认构造函数设定初值
CName o2("DING");                 // 通过B重载构造函数设定初值

2.1.5 对象赋值和拷贝

等都是合法有效的。但是若有:

o1=o2;                           // 通过赋值语句设定初值

则虽合法,但因为同类型的变量可以直接用“=”赋值,运行后却会出现程序终止的情况,这是为什么呢?这是因为对于“CName o1;”这种定义方式,编译会自动调用相应的默认构造函数,此时显式的默认构造函数使私有指针成员strName为空值;而“o1 = o2;”中,C++赋值运算符的操作是将右操作对象的内容拷贝(复制,余同)到左操作对象的内存空间,由于左操作对象o1中的strName没有指向任何内存空间,因此试图将数据复制到一个不存在的内存空间中,程序必然异常终止。所以“o1 = o2;”看上去合法,但实际上是不可行的。

C++还常用下列形式的初始化来将另一个对象作为对象的初值:

<类名> <对象名1>(<对象名2>)

例如:

CName o2("DING");                 //A:通过构造函数设定初值
CName o3(o2);                     //B:通过指定对象设定初值

B语句是将o2作为o3的初值,与o2一样,o3这种初始化形式要调用相应的构造函数,但此时找不到相匹配的构造函数,因为CName类没有任何构造函数的形参是CName类对象。事实上,CName还隐含一个特殊的默认构造函数,其原型为CName(const CName &),这种特殊的默认构造函数称为默认拷贝构造函数。在C++中,每一个类总有一个默认拷贝构造函数,其目的是保证B语句中对象初始化形式的合法性,其功能就等价于“CName o3 = o2;”。但语句“CName o3(o2);”与语句“o1 = o2;”一样,也会出现程序终止的情况,其原因和“o1 = o2;”的原因一样。但是,若有类CData:

class CData
{
public:
    CData( int data = 0)
    {
        m_nData = data;
    }
    ~CData()  {}
    int getData()
    {
        return m_nData;
    }
private:
    int  m_nData;
};

则下列初始化形式却都是合法有效的:

CData a(3);                            // 通过重载构造函数设定初值
CData b(a);                            // 通过默认拷贝构造函数设定初值
                                      // 等价于 CData b=a;
cout<<a.getData()<<endl;              // 输出 3
cout<<b.getData()<<endl;              // 输出 3

可见,与变量一样,在C++中类对象的初始化也可以有两种方式:赋值方式和默认拷贝方式。这两种方式是等价的,例如,CData b(a);和CData b = a;是等价的。

为什么CData对象的赋值和默认拷贝初始化是可行的,而CName对象的赋值和默认拷贝初始化却是不行的呢?问题就出在其数据成员的内存空间上。

CName的数据成员strName是一个“char *”指针,由于其自身的内存空间是用来存放指针的地址,因而其数据的存储还需另辟一个不依附外部的独立的内存空间。而CData的数据成员m_nData自身的内存空间就是用来存储数据的,因此CData对象初始化所进行的数值拷贝是有效的。

解决CName对象初始化的内容拷贝问题,在C++中有两种手段,一是给“=”运算符赋予新的操作,称为运算符重载(以后会讨论);二是重新定义或重载默认拷贝构造函数。

2.浅拷贝和深拷贝

前面已说过,每一个C++类都有一个隐式的默认拷贝构造函数,其目的是保证对象初始化方式的合法性,其功能是将一个已定义的对象所在的内存空间的内容依次拷贝到被初始化对象的内存空间中。这种仅仅将内存空间的内容拷贝的方式称为浅拷贝。也就是说,默认拷贝构造函数是浅拷贝方式。

事实上,对于数据成员有指针类型的类来说,均会出现如CName类的问题,由于默认拷贝构造函数无法解决,因此必须自己定义一个拷贝构造函数,在进行数值拷贝之前,为指针类型的数据成员另辟一个独立的内存空间。由于这种拷贝还需另辟内存空间,因而称其为深拷贝

3.深拷贝构造函数

拷贝构造函数是一种比较特殊的构造函数,除遵循构造函数的声明和实现规则外,还应按下列格式进行定义。

<类名>(参数表)
{}

可见,拷贝构造函数的格式就是带参数的构造函数。由于拷贝操作的实质是类对象空间的引用,因此C++规定,拷贝构造函数的参数个数可以是1个或多个,但左起的第1个参数必须是类的引用对象,它可以是“类名 &对象”或是“const类名 &对象”形式,其中“类名”是拷贝构造函数所在类的类名。也就是说,对于CName的拷贝构造函数,可有下列合法的函数原型:

CName(CName&x);                     //x为合法的对象标识符
CName( const CName &x );
CName(CName&x,…);                  // “…”表示还有其他参数
CName( const CName &x,…);

需要说明的是,一旦在类中定义了拷贝构造函数,则隐式的默认拷贝构造函数和隐式的默认构造函数就不再有效了。

【例Ex_CopyCon】 使用拷贝构造函数

#include <iostream.h>
#include <string.h>
class CName
{
public:
    CName()
    {
        strName = NULL;
    }
    CName( char *str )
    {
        strName = (char *)new char[strlen(str)+1];
        strcpy(strName,str);              // 复制内容
    }
    CName(CName&one)                      // A:显式的默认拷贝构造函数
    {
        // 为strName开辟独立的内存空间
        strName = (char *)new char[strlen(one.strName)+1];
        strcpy(strName,one.strName);      // 复制内容
    }
    CName(CName&one,char*add)             // B:带其他参数的拷贝构造函数
    {   // 为strName开辟独立的内存空间
        strName = (char *)new char[strlen(one.strName) + strlen(add) +1];
        strcpy(strName,one.strName);      // 复制内容
        strcat(strName,add);              // 连接到strName中
    }
    ~CName()
    {
        if(strName)    delete[]strName;
        strName=NULL;                     // 一个好习惯
    }
    char *getName()
    {
        return strName;
    }
private:
 char *strName;                            // 字符指针,名称
};
int main()
{
    CName o1("DING");                      // 通过构造函数初始化
    CName o2(o1);                          // 通过显式的默认拷贝构造函数来初始化
    cout<<o2.getName()<<endl;
    CName o3(o1,"YOU HE");                  // 通过带其他参数的拷贝构造函数来初始化
    cout<<o3.getName()<<endl;
    return 0;
}

代码中,类CName定义了两个拷贝构造函数A和B,其中A称为显式的默认拷贝构造函数, B称为重载拷贝构造函数,它还带有字符指针参数,用来将新对象的数据成员字符指针strName指向一个开辟的动态内存空间,然后将另一个对象one的内容复制到strName中,最后调用cstring头文件定义的库函数strcat,将字符指针参数add指向的字符串连接到strName中。

程序运行结果如下:

DING

DING YOU HE

2.1.6 对象成员的初始化

在实际应用中,一个类的数据成员除了普通数据类型变量外,还往往是其他已定义的类的对象,这样的成员就称为对象成员,拥有对象成员的类常称为组合类。此时,为提高对象初始化效率,增强程序的可读性,C++允许在构造函数的函数头后面跟一个由冒号“:”来引导的对象成员初始化列表,列表中包含类中对象成员或数据成员的拷贝初始化代码,各对象初始化之间用逗号分隔,如下列格式:

以前已讨论过,数据成员的初始化是通过构造函数来进行的。这就意味着,类的对象成员也可在类的构造函数体中进行初始化。这样一来,类的对象成员的初始化就可以有两种方式:一是在构造函数体中进行,称为函数构造方式;二是使用由冒号“:”来引导的对象成员初始化列表的形式,称为对象成员列表方式。

先来看看第一种方式(函数构造方式),例如:

class CPoint
{
public:
    CPoint( int x, int y)
    {
        xPos=x;  yPos=y;
    }
private:
    int xPos, yPos;
};
class CRect
{
public:
    CRect( int x1, int y1, int x2, int y2)
    {
        {L-End}
m_ptLT  =CPoint(x1,y1);
        {L-End}
m_ptRB  =CPoint(x2,y2);
    }
private:
    {L-End}
CPoint m_ptLT, m_ptRB;
};
int  main()
{
    CRect rc(10, 100, 80, 250);
    return 0;
}

虽然,类CRect中的对象成员m_ptLT和m_ptRB的初值的设定是在构造函数中完成的,但此时编译却出现“找不到匹配的CPoint默认构造函数”的编译错误。这是因为当主函数main中定义并初始化CRect对象rc时,它首先为类CRect的数据成员m_ptLT和m_ptRB作定义并分配内存空间,由于m_ptLT和m_ptRB是CPoint对象,因而编译会查找其构造函数进行初始化,此时m_ptLT和m_ptRB不带任何初值,故需要调用CPoint类的默认构造函数,而CPoint类已定义了带参数的构造函数,因此CPoint类的默认构造函数不再存在,所以会出现编译错误。

但若使用第二种方式(对象成员列表方式),即使用由冒号“:”来引导的对象成员初始化列表的形式,如下面的代码:

class CPoint
{//…
};
class CRect
{
public:
    CRect( int x1, int y1, int x2, int y2)
        {L-End}
: m_ptLT(x1, y1), m_ptRB(x2, y2)
    {}
private:
    CPoint m_ptLT, m_ptRB;
};
int main()
{
    CRect rc(10, 100, 80, 250);
    return 0;
}

则编译会顺利通过。这是因为第二种由冒号“:”来引导的对象成员初始化列表的形式实际上是将对象成员的定义和初始化同时进行。当在main函数中定义了CRect对象rc时,编译首先根据类中声明的数据成员次序,为成员分配内存空间,然后从对象初始化列表中寻找其初始化代码,若查找不到,则调用相应的构造函数进行初始化,若可查找到,则根据对象成员的初始化形式调用相应的构造函数进行初始化。显然,在对象成员初始化列表中由于存在m_ptLT(x1, y1)和m_ptRB(x2, y2)对象初始化代码,因此成员m_ptLT和m_ptRB构造时调用的是CPoint( int , int)形式的构造函数,而类CPoint刚好有此形式的构造函数定义,故编译能通过。可见:

(1)函数构造方式实际上是将对象成员进行了两次初始化:第一次是在对象成员声明的同时自动调用默认构造函数进行的,而第二次是在构造函数体中执行的初始化代码。

(2)对象成员列表方式虽是将对象成员的定义和初始化代码分在两个地方书写,但却是同时运行。对比函数构造方式可以看出,由冒号“:”来引导的对象初始化列表的形式能简化对象初始化操作,提高对象初始化效率。

(3)在对象成员列表方式下,成员初始化的顺序是按成员的声明次序进行的,而与成员在由冒号“:”来引导的对象初始化列表中的次序无关。