c++的类/对象

版权声明:转载需注明出处,若有不足,欢迎指正。 https://blog.csdn.net/qq_28992301/article/details/53425763

c++的类/对象

类,是一种特殊的自定义类型,在实例化为对象时,会自动对成员进行初始化

1.类/对象的一些特性

对象的创建细节

  • 当一个类实例化为一个对象时,会为其中的成员分配内存空间(所有成员默认private)。其中,成员变量集中放在对象的内存中(就和结构体一样),而成员函数是同一个类的所有对象公用的,单独定义在代码段,并不以函数指针的形式放在对象的内存中,由编译器在编译时直接链接到调用处
  • 然后编译器连它们的初始化都给包办了,自动进行赋值,编译器先执行初始化列表,再执行构造函数,来为成员变量赋初始值
  • 所以可以认为,从同个类中实例化而来的对象,它们的成员函数实质是同一个函数(入口地址相同)。那么问题来了,众所周知,成员函数可以直接访问成员变量,但是从同个类中实例化而来的对象,其成员函数如何区分自己和别人的成员变量呢?答案是隐藏的参数“this”,this的本质是一个指向当前对象自己的指针
class Test
{
    int mi;
public:
    Test(int i){  /*成员函数中隐藏的参数this,Test(int i, this)*/                
        mi = i;  /*实际执行的是this->mi = i;*/                                 
    };
};

int main()
{/*t1和t2是不同的对象,但同一个类,它们的构造函数Test(int i)实质是同一个函数,那么Test(int i)中如何确定mi是t1.mi还是t2.mi呢?*/
    Test t1(1);
    Test t2(2);
}

成员变量的访问权限

类中的所有成员默认为private,而成员变量的访问权限一般都应设为private/protected

  • private权限范围是当前类(即可被当前类中的成员函数直接获取),protected权限范围是当前类和子类(即可被当前类和子类中的成员函数直接获取)。注意它们的范围是类,而不是对象
  • 所以说,同一个类的不同对象,它们的成员函数是可以互相访问对方的成员变量的,一般用引用参数来实现:
class Test
{
    int var;
public:
    void Test(const Test &ori)
    {
        cout << ori.var << endl; //可用这种方式直接访问同类的成员变量
        cout << var << endl; //对于自己的成员变量则可直接访问,这的var本质是this —> var
    }

};

int main()
{
    Test t1;
    Test t2;
    t1.test(t2);
    return 0;       
}

静态成员函数与成员变量

静态成员函数与成员变量,它们的性质很类似于普通函数与普通全局变量

  • 通过前面代码不难得知,成员函数依赖于对象传入的参数“this”,所以只能以对象名.函数名()的形式来调用。如果没有实例化对象,是不能单独去调用成员函数的。如果我们实在要单独调用,则可以使用“静态成员函数”,只需用static修饰成员函数,就可以使它不依赖于对象而被单独调用,可以在任何地方以类名::函数来访问。但是这么做的代价就是,由于没有参数“this”,“静态成员函数”无法访问对象中的成员变量,只能去访问类中公用的成员变量,即“静态成员变量”

  • 所谓“静态成员变量”,即使用static修饰成员变量,并将其在全局定义。它会被分配到全局数据区,生命周期为永久,所有的对象将共用这个成员变量(类似成员函数的共用)。“静态成员变量”和全局变量唯一不同的是,它可以是private的,相对于全局变量更加安全,同时更低耦合高聚态;但是如果设置为public,可以在任何地方以类名::变量名来访问,那就和全局变量没有任何区别了…..

class Test
{
    static int cCount;
};

int Test::cCount = 0;//必须要在全局定义并初始化,这样编译器就会为其在全局分配空间

2.成员变量的初始化

构造函数

  • 构造函数是一种特殊的成员函数,要求和类同名、无返回值。它将在类实例化时被自动调用,也可当作成员函数在外面手动调用
  • 构造函数也是可以重载的,当类实例化时可以向构造函数传参,可以根据参数,选择成员变量初始化配置并赋值
  • 未指定任何构造函数的情况下,编译器默认提供一个空的无参构造函数,这样成员变量的初始值是不定的。一但指定了任意的构造函数,默认的无参构造函数立即失效,所以,假如我们定义了一个有参构造函数,实例化时却未传参,编译器会提示找不到无参构造函数
  • 拷贝构造函数是一种特殊的构造函数,要求和类同名、参数为“const 类名&”,在该函数中,我们应该实现类中成员变量的拷贝
class Test
{
private:
    int i;
public:
    void func1(void);
    Test()/*构造函数可以重载*/
    {
        i = 1;  
    }
    Test(int v)
    {   
        swich(v)
        case xxxx
        /*可以根据参数v,来选择不同的初始化配置*/         
    }
    Test(const Test& t)/*一个简单的拷贝构造函数*/
    {
        i = t.i;
    }
};


int main(void)
{   
    Test t0;//自动触发构造函数Test()
    Test t1(123);//自动触发构造函数Test(int v)
    Test t2 = 123;
    Test t3 = Test(123);//极不推荐这种写法,这会产生临时对象
    Test t4 = t1;//自动触发拷贝构造函数
    Test t5(t1);
}
  • 若未提供拷贝构造函数,则编译器也默认提供一个拷贝构造函数,它会进行所有成员变量的无脑拷贝,这称之为“浅拷贝”,然而一些“动态申请的资源”也会原封不动的拷贝,它们经过“浅拷贝”后再去释放则极易发生错误,如下:
class Test
{
private:
    int *p;
public:
    Test()
    {
        p = new int;    
    }
    void free(void)
    {
        delete p;
    }
};

int main(void)
{   
    Test t0;//触发构造函数Test()
    Test t1 = t0;//触发默认的拷贝构造函数
    t0.free();
    t1.free();//重复释放了空间,报错
}
  • 故类中涉及到“动态申请的资源”:如堆内存、文件描述符、网络端口。千万别用默认的拷贝构造函数,保险起见还是自己定义一个拷贝构造函数,在里面进行进行申请资源

初始化列表

  • 初始化列表会在构造函数之前进行成员变量的初始化,并且可以为const成员变量赋初始化值、类成员变量传参,这些事情构造函数都无法做到
  • 尤其注意的是,初始化顺序与表中的顺序无关,只与类中成员声明的顺序有关。在程序开发中,这一点往往会引发大量的bug
class Value
{
private:
    int mi;
public:
    Value(int i)
    {
        printf("i = %d\n", i);
        mi = i;
    }
};

class Test
{
private:
    const int ci;
    Value m2;
    Value m3;
    Value m1;
public:
/*实际的初始化顺序为ci、m2、m3、m1,如果这些成员变量的初始化顺序有讲究的话,这里必须特别注意*/
    Test() : m1(1), m2(2), m3(3), ci(100)
    {
    /*构造函数里根本无法完成const成员变量初始化、类成员变量带传参的初始化*/
    }
};

3.创建对象的顺序

  • 由于类在实例化为对象时,会自动初始化成员,并且初始化采用的参数可以是其它对象。而当把对象定义成全局变量时,对象之间的互相依赖是十分危险的,因为相对于局部变量,全局变量的定义先后顺序是未知的。同样的道理,在多线程中也要小心
class Test
{
private:
    int mi;
public:
    Test(int i)
    {
        mi = i;
    }
    Test(const Test& obj)
    {
        mi = obj.mi;
    }
};
//假设这两个对象是全局变量
Test a1 = 1;//编译器有可能先定义a2,再定义a1,程序运行就会出现错误
Test a2 = a1;
  • 所以,我们在实例化类时,要注意以下几点
    • 尽量减小对象间的耦合度
    • 尽量别用全局变量
    • 在多线程中警惕对象间的耦合

4.对象的销毁

  • 所谓被销毁,就是对象所占的内存被释放。若对象定义为局部变量,它会在函数结束时被销毁;若对象定义在堆内存上,它会在堆内存释放时被销毁;若对象定义为全局变量,那自然就不会被销毁
  • 对象在被销毁时,会自动调用析构函数,它是一种特殊的成员函数,要求和类同名(前面加~)、无返回值、无参。尤其注意的是,析构函数是唯一的,不可重载,专门用来释放构造函数申请的资源
class Test
{
private:
    int *p;
public:
    Test()
    {
        p = new int;    //构造函数一般用来申请资源
    }
    ~Test()
    {
        delete p;    //析构函数和构造函数成对出现,释放构造函数申请的资源
    }
};
  • 不论对象定义为局部变量还是全局变量,当作用域结束时,其销毁(析构)的顺序与其实例化(构造)的顺序相逆,类似“倒影式结构”

神秘的临时对象

  • 普通的成员函数,它们可以在类的内部互相调用。但是在类内部调用构造函数,会产生一个神秘的临时对象,所以千万不能再类内部调用构造函数
class Test {
    int mi;
public:
    Test(int i) {
        mi = i;
    }
    Test() {
        Test(0);//这里会诞生一个临时对象,赋值操作并不会作用到当前对象
    }
};

int main(void)
{
    Test t;
}
  • 临时对象的生命周期和作用域只限于一条语句中,临时对象和野指针一样,是需要特别警惕的灰色地带。在类内部,普通的成员函数之间可以随意调用,并不会产生临时对象

5.二阶构造模式

当一个类实例化时,若构造函数在初始化成员变量时,进行申请资源操作而没有得到理想结果(比如new一个变量失败),这时诞生的对象就称为“半成品对象”。这是一种危害很大的bug,因为资源申请并不是每次都会失败,所以将导致bug很难复现

  • 构造函数中,成员变量的初始化可以分为两类:资源无关操作、申请资源操作(内存、文件描述符、网络端口)。其中,申请资源操作可能会出现异常
  • 二阶构造模式可以解决这个问题:将构建过程分为两步,第一阶段负责资源无关操作;第二阶段负责申请资源操作,并判断是否成功,若失败则删除“半成品对象”
class TwoPhaseCons 
{
private:
    int mi;
    TwoPhaseCons(int i) // 第一阶段构造函数,设为私有
    {
        //在这里进行资源无关操作
        mi = i;   
    }
    bool construct() // 第二阶段构造函数,设为私有
    { 
        //在这里进行申请资源操作
        return true; 
    }
public:
    static TwoPhaseCons* NewInstance(int i); // 对象创建函数,设为公开
};

TwoPhaseCons* TwoPhaseCons::NewInstance(int i) 
{
    //在对象创建函数中,创建对象,触发第一阶段构造函数
    TwoPhaseCons* ret = new TwoPhaseCons(i);

    // 若第二阶段构造失败,返回 NULL    
    if( !(ret && ret->construct()) ) {
        delete ret;
        ret = NULL;
    }

    return ret;
}


int main()
{
    //利用对象创建函数,来获得指向对象的指针,从而实现对象的创建
    TwoPhaseCons* obj = TwoPhaseCons::NewInstance(1);
    if(obj == NULL){
        return -1;
    }
    //到这为止,整个对象就构建完了

    delete obj;
    return 0;
}
  • 如此一来,即可完美避免“半成品对象”的产生

猜你喜欢

转载自blog.csdn.net/qq_28992301/article/details/53425763