【C++对象的可靠知识】

前言

对象模型对于c++来说是至关重要的,只有深入理解了对象模型,才能在程序性能优化方面做到游刃有余,才能避免程序开发过程中一些不易发现的内存错误,从而改善程序性能,提高程序质量。本章就来讲讲对象模型那些事。


对象的生命周期

对于不同创建方式创建的对象,其生命周期各不相同。生命周期可以简单理解为某个对象从创建到销毁的过程;创建对象需要分配一定的内存空间,销毁对象则会释放这部分内存。

创建对象的方式可以分为三种
1.定义变量的方式
2.new 的方式
3.通过实现创建对象

通过定义变量的方式创建对象

#include <stdio.h>
#include <stdlib.h>

class A
{
    
    
public:
    A()
    {
    
    
        printf("A 创建\n");
    }

    ~A()
    {
    
    
        printf("A 销毁\n");
    }
};

class B
{
    
    
public:
    B()
    {
    
    
        printf("B 创建\n");
    }

    ~B()
    {
    
    
        printf("B 销毁\n");
    }
};

A a;

void test()
{
    
    
    printf("test()开始\n");
    A a1;
    static B b;
    printf("test()结束\n");
}

int main()
{
    
    
    printf("main()开始\n");
    test();
    test();
    static B b1;
    printf("main()结束\n");

    return 0;
}

在这里插入图片描述

  • 通过定义变量创建对象时,变量的作用域决定了对象的生命周期。当进入变量的作用域时,对象被创建;退出变量的作用域时,对象被销毁。

  • a是全局变量,所以其作用域是整个程序,其在main函数之前被创建,main函数结束后再被销毁。

  • 第一次进入test时a1和b都会被创建,但是b是静态变量,所以第二次进入test函数时只创建了a1。声明为静态变量的对象在第一次进入作用域时会被创建,直到程序退出时才被销毁。

  • 最后b1与b类似,只会创建一次,在程序结束后销毁。

通过new 的方式创建对象

class A
{
    
    
public:
    A()
    {
    
    
        printf("A 创建\n");
    }

    ~A()
    {
    
    
        printf("A 销毁\n");
    }
};

A* createA()
{
    
    
    return new A();
}

void deleteA(A* p)
{
    
    
    delete p;
    p = NULL;
}

int main()
{
    
    
    A* a = createA();
    std::cout << "赋值前地址:" << pA << std::endl;
    a = createA();
    std::cout << "赋值后地址:" << pA << std::endl;
    deleteA(a);
    return 0;
}

在这里插入图片描述

  • 当时用new去创建一个对象时,系统会在堆上分配相应的内存空间,这块空间不会自己释放,需要调用delete去主动释放,这就很容易造成内存的泄露。

  • 上述代码中,createA函数中创建了一个A对象后将地址返回并存储到指针变量a中;随后再次调用了createA函数创建了一个A对象,将地址赋值给了a,此时a指针指向的是第二次创建的对象,这从结果图中可以看到。而第一次创建的对象已经没有了指针指向。调用deleteA销毁对象时,其实只销毁了第二次创建的对象,第一次创建的对象会一直存在,直到程序结束。虽然程序没有报错,但其实已经出现了内存的泄露。

  • 注意:指针a指向的对象被销毁,但指针a还存在于栈中,此时a变成一个悬空指针,还指向堆中对象被销毁的位置,所以要将NULL赋值给a。

通过实现创建对象

通常是指一些隐藏的临时对象的创建和销毁,其通常拷贝构造函数创建。

class A
{
    
    
public:
    A()
    {
    
    
        printf("A 创建\n");
    }

    A(const A& other)//拷贝构造函数
    {
    
    
        printf("A 通过拷贝创建\n");
    }

    ~A()
    {
    
    
        printf("A 销毁\n");
    }
};

A test(A a)
{
    
    
    std::cout << "进入函数test"<< std::endl;
    A b;
    return b;
}

int main()
{
    
    
    A a;
    a = test(a);
    return 0;
}

在这里插入图片描述

  • 上述代码中,test函数是通过值传递,在调用test函数时,需要构造一个a的副本,调用一次拷贝构造函数,创建了一个临时对象变量,会影响程序性能。
  • 可以通过传递引用的方式去避免中间对象的产生。

对象内存布局

一个c++对象中主要包括成员函数与成员变量,成员函数包括静态成员函数,非静态成员函数,虚函数;成员变量则包括静态成员变量与非静态成员变量。

只有非静态成员变量属于类的对象上。

class A
{
    
    
public:
    static int n;
    double value;
    //int value;
    char flag;

    A()
    {
    
    
        printf("A 创建n\n");
    }

    virtual ~A()
    {
    
    
        printf("A 销毁n\n");
    }

    double getValue()
    {
    
    
        return value;
    }

    static int getn()
    {
    
    
        return n;
    }

    virtual void test()
    {
    
    
        printf("virtual void test()\n");
    }
};

int main()
{
    
    
    A a;
    printf("A start address: 0x%X\n", &a);
    printf("a size: %d\n", sizeof(a));
    printf("A size: %d\n", sizeof(A));
    return 0;
}

上边的代码,使用X64跑一下,结果如下:对象a的大小为24个字节。在这里插入图片描述

  • 因为value为double类型,占8个字节,flag为char类型,为1个字节,但由于内存对齐,也占用了8个字节,64位编译器中虚函数指针占用8个字节,所以一共为24个字节。

如果value改为int类型呢?没错,此时对象a只占16个字节,因为类中最大占用内存为8,所以flag只需要再补3个字节即可。
在这里插入图片描述
这又是为什么呢?

c++ 内存对齐原则

  1. 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。(有虚函数指针的虚函数指针放最前边)
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。譬如int类型变量,要从4的倍数开始。
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍,不足的要补齐。vs默认为8,linux默认为4。
  4. 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

来个例子巩固一下:

class B
{
    
    
public:
    static int n;
    double value;
    //int value;
    char flag;

    B()
    {
    
    
        printf("A 创建n\n");
    }

    virtual ~B()
    {
    
    
        printf("A 销毁n\n");
    }

    double getValue()
    {
    
    
        return value;
    }

    static int getn()
    {
    
    
        return n;
    }

    virtual void test()
    {
    
    
        printf("virtual void test()\n");
    }
};

我们用vs自带的Developer Command Prompt工具去查看。进入类B所在文件夹,运行一下命令:

cl /d1 reportSingleClassLayoutB "文件名"

在这里插入图片描述
从上图可以看出,虚函数表指针占据了前4个字节,所以变量char i从4开始,又因为规则3,所以value只能从8开始,系统会给i补上3个字节。flag只占一个字节,从12开始,但因为规则4,系统也会为flag补上3个字节。所以一共16个字节。

总结

本文介绍了一些关于c++对象的相关知识,如有纰漏,请多指正。

猜你喜欢

转载自blog.csdn.net/Done_for_me/article/details/130215323