C++类与对象(二)

580 阅读9分钟

成员变量的初始化

struct Person {
    int m_age;
}

Person g_person;

void test() {

// 栈空间:没有初始化成员变量

/*Person person;

cout << person.m_age << endl;*/



// 堆空间:没有初始化成员变量

Person* p0 = new Person;

// 堆空间:成员变量初始化为0

Person* p1 = new Person();

cout << g_person.m_age << endl;

cout << p0->m_age << endl;

cout << p1->m_age << endl;

}


在类没有实现构造方法的时候:

  • 全局区的对象成员变量会被初始化为0
  • 栈空间并未初始化成员变量
  • new [类名] 的方式创建对象的,成员变量不进行初始化
  • new [类名]()的方式创建对象的,成员变量初始化为0

在类实现构造方法的时候:

  • 全局区的对象成员变量会被初始化为0
  • 栈空间并未初始化成员变量
  • new [类名]()的方式创建对象的,成员变量不进行初始化
  • new [类名] 的方式创建对象的,成员变量不进行初始化

数组的形式

image.png

image.png

在类没有实现构造方法的时候:

  • new 类名[数量]形式创建的,成员变量不会被初始化
  • new 类名[数量]()形式创建的,成员变量都会被初始化为0
  • new 类名[数量]{}形式创建的,成员变量都会被初始化为0

在类实现构造方法的时候:

  • new 类名[数量]形式创建的,成员变量不会被初始化
  • new 类名[数量]()形式创建的,成员变量不会被初始化
  • new 类名[数量]{}形式创建的,成员变量不会被初始化

总结:

如果自定义了构造函数,那么除了全局区,其他内存空间的成员变量均不会初始化成员变量

如果有比较多的成员变量,建议使用以下初始化方式

Person() {

    memset(this, 0, sizeof(Person));

}

析构函数

析构函数:

  • 在对象销毁的时候自动调用,一般用于完成对象的清理工作
  • 函数名以~开头,与类同名,无返回值,无参,不可以重载,有且只有一个析构函数
  • 通过malloc分配的对象free的时候不会调用析构函数
  • 构造函数、析构函数都要声明为public才能被外界调用

析构函数的主要目的是在于清理对象内申请的堆空间,比如在下面的代码中Person中有一个成员变量指针是指向堆空间的,那么在Person析构函数中要主动释放Car的内存空间,否则会造成内存泄露

struct Car {

    int m_price;

    Car() {

    m_price = 0;

    cout << "Car::Car()" << endl;

    }

    ~Car() {

    cout << "Car::~Car()" << endl;

    }

};



    struct Person {

    private:

    int m_age;

    Car *m_car;

    public:

    // 用来做初始化的工作

    Person() {

    m_age = 0;

    m_car = new Car;

    cout << "Person::Person()" << endl;

    }



    // 用来做内存清理的工作

    ~Person() {
    delete m_car;
    cout << "Person::~Person()" << endl;

    }

};

命名空间

命名空间的作用是避免命名冲突

例如两个个全局变量都叫g_age,利用命名空间就可以区别开来 image.png

不想每次都使用LA::的话可以用using namespace LA,此句的意思是从这行开始都使用LA的命名空间,那么如果有多个命名空间这里会产生歧义,比如下图

image.png

命名空间的嵌套

命名空间可以嵌套使用,其实存在一个全局的命名空间,我们使用的命名空间都是嵌套于其中

image.png

比如全局有一个g_age变量,LA空间内也有一个全局变量g_age,那么如何正确的的去调用这两个变量

image.png

使用::来调用全局命名空间中的g_age

再例如下图其实都是在调用第一处的func()

image.png

命名空间的合并

下面二者的写法是等价的 image.png

在类中声明和实现分开的情况下命名空间的用法

image.png

其他语言中命名空间的做法

  • C++使用命名空间的做法
  • Java中使用Package
  • Objc中使用类前缀

继承

继承中的成员变量

  • 继承可以让子类拥有父类所有的成员(变量和函数)
  • C++中没有Objc中基类的概念,创建一个类出来就可以看做是根类,它不默认继承于任何类,Objc中的基类是NSObject
  • 父类的成员变量在前,子类的成员变量在后

image.png

image.png

可以在内存中看看成员变量是如何分布的,顺序正是以父类的成员变量在前,子类的成员变量在后的规则来排序的,每个成员变量占4个字节,一共占12个字节

image.png

成员访问权限

成员访问权限、继承方式有3种:

  • public 公共的,任何地方都可以访问
  • protected 子类内部、当前类内部都可以访问
  • private 私有的,只有当前类内部可以访问

子类内部访问父类成员的权限是成员本身的访问权限和上一级父类的继承方式这二者中权限最小的那个

实际开发中使用最多的继承方式是public,因为能保留父类原来成员的访问权限

访问权限不影响内存布局

初始化列表

初始化列表:

  • 一种便捷的初始化成员变量的方式
  • 只能用于构造函数
  • 初始化顺序只与成员变量的声明顺序有关

在初始化成员变量时我们一般会选择在构造函数内部对成员变量进行初始化,例如下图

image.png

image.png

那么通过整理汇编指令可以看到


 push        14h  
 push        0Ah  
 lea         ecx,[ebp-10h]  
 call        0085109B


    10: m_age = age;

 mov         eax,dword ptr [ebp-8]  

 mov         ecx,dword ptr [ebp+8]  

 mov         dword ptr [eax],ecx  

     11: m_height = height;

 mov         eax,dword ptr [ebp-8]  

 mov         ecx,dword ptr [ebp+0Ch]  

 mov         dword ptr [eax+4],ecx


如果使用初始化列表再来看看汇编代码


push        14h  
push        0Ah  
lea         ecx,[ebp-10h]  
call        00AE109B

mov         eax,dword ptr [ebp-8]  
mov         ecx,dword ptr [ebp+8]  
mov         dword ptr [eax],ecx  

mov         eax,dword ptr [ebp-8]  
mov         ecx,dword ptr [ebp+0Ch]  
mov         dword ptr [eax+4],ecx  



可以看出使用初始化列表和自己在构造函数中进行初始化其实汇编代码是一样的,都是做了同样的操作

Person(int age, int height) :m_age(age), m_height(height) {}中的:m_age(age), m_height(height)可以等价为构造函数中

Person(int age, int height) {

    m_age = age;

    m_height = height;

}

那么m_age(age)等价于m_age = age的话,则m_age(10)或者m_age(func())这种形式也是可以的

并且如果参数使用调用函数的形式来观察的话同时可以证明初始化顺序只与成员变量的声明顺序有关

image.png

image.png

image.png

初始化列表与默认参数配合使用

初始化列表与默认参数使用如下图

image.png

image.png

如果函数声明是和函数实现是分离的,则:

  • 默认参数只能写在函数声明中
  • 初始化列表只能写在函数实现中,因为上述分析可以得出初始化列表和在构造函数内实现的语句原理是一样的

写法如下图所示

image.png

构造函数的互相调用

错误示范

image.png

想在一个构造函数内调用另一个构造函数,如果按照上图的做法,会得到想要的结果吗?

image.png

从打印结果看并不是我们想象的那样,看起来成员变量m_age和m_height都没有被初始化,接下来看看另一组打印,分别来看看地址是否为同一地址

image.png

image.png

结果发现地址竟不是同一组地址,为了更清晰的看出问题,现在转到汇编中来找答案

在创建对象调用无参的构造函数前,先将person的地址传到寄存器ecximage.png

Person::Person构造函数中可以看到先将外边person的地址传给了this指针,但是在调用有参数的构造函数前,同样把一个东西传给了ecx寄存器,但是并不是外部的person的地址,可以看出这里应该是创建了一个另外的对象,然后将1020赋值给了一个新的对象,所以导致之前打印看到的是成员变量未初始化 image.png

结论:

那么这里其实并不是在构造函数中调用了另一个构造函数,而是直接创建了一个新的对象

正确调用

正确的做法是构造函数中调用另一个构造函数只能放在初始化列表里来完成,如下

image.png

同样先来打印看看结果,结果发现是正确的

image.png

结果正确了,依然还是看看汇编代码

在创建对象调用无参的构造函数前,先将person的地址传到寄存器ecximage.png

但是与之前不同的地方在于,在调用另一个构造函数前,把this指针中的值也就是外部的person对象传给了寄存器ecx,那么在之后的赋值中就可以把正确的值赋给person的成员变量 image.png

子类和父类的构造函数

子类和父类的构造函数:

  • 子类的构造函数会默认调用父类的无参构造函数
  • 如果子类的构造函数显式的调用了父类的有参构造函数,就不会再去调用父类的无参构造函数
  • 如果父类缺少无参的构造函数,子类的构造函数则必须显式的调用父类的有参构造函数

子类的构造函数会默认调用父类的无参构造函数

image.png

image.png

可以看出先执行父类的构造函数后才执行子类的构造函数,并且子类是没有主动调用父类的构造函数的

image.png

image.png

再通过汇编代码同样可以看出,在子类的构造函数中会主动调用父类的无参构造函数

以下是子类调用父类的构造函数与类中调用其他构造函数的结合

image.png

image.png

在工程中这种写法应该是比较常用的

image.png

子类和父类的析构

通过之前的分析可以得出:父类的构造函数先于子类构造函数来调用,那么析构函数的顺序呢?

image.png

image.png

析构函数时子类先于父类

总结:

  • 构造函数是父类先于子类调用
  • 析构函数时子类先于父类调用