函数重载本质
- 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反汇编
函数默认参数的本质
- 方法签名唯一,编译器把默认参数传进去了而已。
void sum(int i, int j = 3) {
cout << i + j << endl;
}
int main() {
sum(1, 2);
sum(10, 20);
sum(1);
return 0;
}
- xcode看汇编
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;
}
- 汇编
内联函数
- 内联展开,可减少函数调用的开销(堆栈平衡的开销)。会增大代码体积。
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;
}
- 汇编
- 内联函数和宏(宏替换)都可以减少函数调用开销。
- 对比宏,内联函数多了语法检测和函数特性。
- 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;
}
- 析构函数(Destructor),也叫析构器,在对像销毁时自动调用(通过free释放的不会调用),一般用于完成对像的清理工作。
- 含有虚函数的类应该把析构也声明为虚函数。
函数内存分布
- 调用一个函数会开辟一段栈空间给函数。
- 函数执行完,会回收开辟的空间。
- 叶子函数在函数内部不分配栈空间。
- 非叶子函数由于内部掉用其它函数会重复1。
内存与寄存器
- 程序执行流。
- cpu通过寄存器读写内存。
- 一个寄存器可存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
函数地址。
- 函数内存在代码区。而
person
在栈上。因此,类方法的隐式第一个入参是对像本身this
。this
指向函数调用者。
int main() {
Person p;
p.id = 2;
p.display();
return 0;;
}
先把 p 对像的地址值存到寄存器,再call。 p指针的地址就是 p对像id的地址(没有虚表地址)。
- 类对像的内存是一维线性的。根据指针偏移读取内容。
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;;
}
- 继承关系的内存布局是,父类成员内存在前。
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;
}
- 每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域。
- 代码段(代码区),用于存放代码。只读。
- 数据段(全局区),用于存放全局变量等。程序运行期都在。
- 栈空间,每调用一个函数就会给它分配一段连续的栈空间,函数调用完后自动释放。空间自动分配和回收。
- 堆空间。主动去申请和释放内存。
堆空间
- 在程序运行过程,为了能自由控制内存的生命周期,大小。
- 申请/释放。
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;
}
- 父类加上虚函数后(上述代码,虚函数注释关掉),先继承的虚表和成员在前(内存对齐)。
- 成员变量和父类可以一样(域不一样)。本质还是通过指针访问内存。
- 菱形继承。不特殊说明各继承各的,size要大点。最底下子类从基类继承的成员变量冗余重复。
- 虚继承可以解决菱形继承带来的问题。头部放虚表指针。继承过来的成员在尾部内存。
static
- 静态成员变量存储在数据段(全局区),整个程序运行过程中只有一份内存。对比全局变量,它可以设定访问权限,达到局部共享目的。
- 单例
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;
}