c++汇编分析&内存布局

函数重载本质

  • c++运用了namemangling来实现。也可以叫函数签名。
void display() {}
void display(int i) {}
void display(int i, int j) {}
void display(long i) {}
void display(double i) {}

int main() {
    display();
    display(1);
    display(1, 2);
    display(1L);
    display(1.0);
    return 0;
}
  • ida反汇编

image.png

函数默认参数的本质

  • 方法签名唯一,编译器把默认参数传进去了而已。
void sum(int i, int j = 3) {
    cout << i + j << endl;
}

int main() {
    sum(1, 2);
    sum(10, 20);
    sum(1);
    return 0;
}
  • xcode看汇编

image.png

extern "C"标注的函数会按C方式编译。

  • 声明过的函数不能被重载。没有采用namemangling
#ifndef My_header
#define My_header
// 引入头文件...
#endif

#ifdef __cplusplus
extern "C" {

#include <math.h>
    void func(int v) {}
}

// 编译报错Conflicting types for 'func'
extern "C" void func(double v) {}
// 编译报错Conflicting types for 'func'
extern "C" void func() { }
#endif

int main() {
    func(1);
    return 0;
}
  • 汇编

image.png

内联函数

  • 内联展开,可减少函数调用的开销(堆栈平衡的开销)。会增大代码体积。
void func() {
    cout << 10 << endl;
}
// 内联
__attribute__((always_inline)) void func2() {
    cout << 20 << endl;
}
// 建议变成内联,不一定是内联,debug模式肯定不是内联
inline void func3() { 
    cout << 30 << endl;
}

int main() {
    func();
    func2();
    return 0;
}
  • 汇编

image.png

  • 内联函数和宏(宏替换)都可以减少函数调用开销。
  • 对比宏,内联函数多了语法检测和函数特性。
  • oc宏define CG_INLINE static inline

引用

  • 一个引用占用一个指针大小。占用8(和设备有关)个字节的空间。
  • 引用的本质就是指针,只是编译器弱化了它的功能,所以引用就是弱化了的指针。
struct Person1 {
    int age;
};
struct Person2 {
    int &age;
};

int main() {
    // 输出4 
    cout << sizeof(Person1) << endl;
    // 输出8
    cout << sizeof(Person2) << endl;
    return 0;
}

构造析构

  • 构造函数(Constructor)也叫构造器,在对像创建的时候自动调用(通过malloc分配的对像不会调用),一般用于完成对像初始化工作。
  • 可以重载,可以有多个构造函数。
  • 构造调构造需要放在初始化列表里。
  • 子类构造默认调用父类的无参构造。
struct Person {
    int age;
    Person(): Person(10) {
        
    }
    
    Person(int age): age(age) {
    }
};


struct Student: Person {
    int num;
    
    Student(){ }
    Student(int age, int num) :Person(age), num(num) {
        
    }
};
  • 并不是所有情况下,编译器都会为类生成一个空的无参构造函数。
  • 在特定情况下(比如,牵扯成员变量需要初始化时,基类有无参构造时),才会创建空无参构造函数。
struct Person {
//    int age; //不会生成空无参构造
    int age = 5; //会生成空无参构造
};

int main() {
    Person *p = new Person();
    return 0;
}

image.png

  • 析构函数(Destructor),也叫析构器,在对像销毁时自动调用(通过free释放的不会调用),一般用于完成对像的清理工作。
  • 含有虚函数的类应该把析构也声明为虚函数。

函数内存分布

  1. 调用一个函数会开辟一段栈空间给函数。
  2. 函数执行完,会回收开辟的空间。
  3. 叶子函数在函数内部不分配栈空间。
  4. 非叶子函数由于内部掉用其它函数会重复1。

内存与寄存器

  • 程序执行流。

image.png

  • cpu通过寄存器读写内存。

image.png

  • 一个寄存器可存8个(看cpu)字节数据。
  • 可通过内联汇编写汇编代码。也可以.s文件。
int main() {
    __asm {
        mov  eax, -0x8
    }
    return 0;
}

类对象内存布局

  • 创建一个类的对像时,分配的内存 >= 成员变量的所需空间(内存对齐)。
  • 类的方法,在内存中只有一份。
struct Person {
    int id;
    int age;
    int height;
    
    void display() {
        cout << id << "/t" << age  << "/t"  << height << endl;
    }
};

int main() {
    Person p;
    p.id = 2;
    p.age = 0xf;
    p.height = 10;
    // 12,成员都是int不需要内存对齐
    cout << sizeof(Person) << endl;
    return 0;;
}
  • 有虚函数,对像内存最前面会增加N(我的电脑上是8)个字节。一个虚表(虚函数表)的地址。
struct Car {
    int money;
    
    virtual void run() { }
    virtual void run2() { }
};

int main() {
    Car c;
    c.money = 12;
    return 0;
}

汇编可以看出虚表的头N个字节存放的run函数地址。
image.png

  • 函数内存在代码区。而person在栈上。因此,类方法的隐式第一个入参是对像本身thisthis指向函数调用者。
int main() {
    Person p;
    p.id = 2;
    p.display();
    return 0;;
}

先把 p 对像的地址值存到寄存器,再call。 p指针的地址就是 p对像id的地址(没有虚表地址)。
image.png

  • 类对像的内存是一维线性的。根据指针偏移读取内容。
struct Person {
    int id;
    int age;
    int height;
    
    void display() {
        cout << id << "\t" << age  << "\t"  << height << endl;
    }
};

int main() {
    int a = 100;
    Person p;
    p.id = 2;
    p.age = 3;
    p.height = 4;

    Person *p1 = (Person *)&p.age;
    p1->id = 11;
    p1->age = 12;
    p.display();// 2 11 12
    p1->display();// 11 12 32766
    return 0;;
}

image.png

  • 继承关系的内存布局是,父类成员内存在前。
struct Person {
    int age;
};

struct Student: Person {
    int num;
};

struct GoodStudent: Student {
    int money;
};

int main() {
    GoodStudent gs;
    gs.age = 10;
    gs.num = 5;
    gs.money = 12;
    return 0;
}

image.png

  • 每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域。
    • 代码段(代码区),用于存放代码。只读。
    • 数据段(全局区),用于存放全局变量等。程序运行期都在。
    • 栈空间,每调用一个函数就会给它分配一段连续的栈空间,函数调用完后自动释放。空间自动分配和回收。
    • 堆空间。主动去申请和释放内存。

image.png

堆空间

  • 在程序运行过程,为了能自由控制内存的生命周期,大小。
  • 申请/释放。

malloc/free:

int main() {
    int *p = (int *)malloc(4);
    *p = 10;
    free(p);
    return 0;
}

new/delete:

int main() {
// new int(); 会掉用 memset, 汇编查看(基于xcode)。
    int *p = new int;
    *p = 10;
    delete p;
    
    int *p1 = new int[4];
    delete [] p1;
    return 0;
}
  • 申请堆空间成功后,会返回那一段内存空间地址。
  • 申请和释放必须是1对1的关系,不然可能会内存泄漏。

对像的内存

  • 可以存在于3种地方:
  • 全局区(数据段):全局变量。
  • 栈空间:局部变量。
  • 堆空间:动态申请内存(malloc,new等)。

namespace

  • 命名空间不影响内存布局。
  • 命名空间可以嵌套。
  • 有个默认的全局命名空间。::
namespace LZ {
    void func() {
    
    }
}

void func() {
    
}

int main() {
    using namespace LZ;
    LZ::func();
    ::func();
    return 0;
}

多继承

  • 成员变量内存占用:先继承过来的排在前面。
struct Student {
    int score;
    //virtual void learning() {}
};

struct Worker {
    int time;
    int nb;
    //virtual void working() {}
};

struct GoodWorker: Student, Worker {
    int money;
};

int main() {
    GoodWorker gw;
    gw.score = 10;
    gw.money = 6;
    gw.time = 5;
    gw.nb = 15;
    
    return 0;
}

image.png

  • 父类加上虚函数后(上述代码,虚函数注释关掉),先继承的虚表和成员在前(内存对齐)。

image.png

  • 成员变量和父类可以一样(域不一样)。本质还是通过指针访问内存。
  • 菱形继承。不特殊说明各继承各的,size要大点。最底下子类从基类继承的成员变量冗余重复。
  • 虚继承可以解决菱形继承带来的问题。头部放虚表指针。继承过来的成员在尾部内存。

image.png

image.png

static

  • 静态成员变量存储在数据段(全局区),整个程序运行过程中只有一份内存。对比全局变量,它可以设定访问权限,达到局部共享目的。

image.png

  • 单例
class Person {
    Person(){ }
    static Person *ms_person;
public:
    static Person *share() {
        if (ms_person == NULL) {
            ms_person = new Person();
        }
        return ms_person;
    }
};
Person * Person::ms_person = NULL;

int main() {
    Person *p1 = Person::share();
    Person *p2 = Person::share();
    Person *p3 = Person::share();
    return 0;
}