c++ 内存结构
C/C++ 内存结构
C++内存结构分类
全局变量,全局函数
static字符不影响全局变量和函数在内存中的表现形式,唯一区别是加了static修饰的值外部不可见。
全局函数都位于代码段,全局变量根据其是否赋有初值,是否被const修饰区分:
- 未被const修饰,赋初值:位于读写数据段;
- 未被const修饰,未赋初值: 位于.bss段;
- 被const修饰:位于只读数据段;
Q: 如果全局变量为一个结构体A,其中部分变量为const修饰,部分未被const修饰,该结构体在内存中的存储位置为?
A: 若该变量的声明方式未加const修饰,即声明为A a = …,则整个结构体都处于读写数据段,内部被const修饰的变量依靠编译器保证其const的性质,可以通过指针操作绕过编译器检查对其进行修改;
struct A {
char t;
char num;
const int t1;
};
A a = {1, 2, 3};
int main() {
int t = 5;
const int* p = &a.t1;
(*(int**)&(a.t1)) = &t;
return 0;
}
若声明为const A a = … ,则整个结构体处于只读数据段,未被const修饰的变量也无法被修改。该只读特性通过内存只读实现,使用指针无法绕开。
普通类的内存模型
没有继承关系,没有虚函数的类或结构体,由一个或多个,基础或复杂数据类型组成的一块连续内存。
类成员
非静态成员
内存中成员的排列顺序与代码中的声明顺序一致,但内存大小可能与类中所有成员内存大小的和不等且可能由于成员排列顺序导致类的内存大小出现较大差距,因为可能会发生内存对齐。
内存对齐的原则:
确定对齐单位:
-
若设置了#pragma pack(n), 且 sizeof(类成员中所占内存最小的基本数据数据类型) < n < sizeof(类成员中所占内存最大的基本数据数据类型) ,则取对齐单位为n;
-
没有设置#pragma pack(n),类以基本数据类型组成,则取类成员中所占内存最大的基本数据数据,为对齐单位;
-
若类中有自定义的数据类型,则以展开后的所占内存最大的基本数据类型,为对齐单位;
填充内存:
- 所占内存小于对齐单位的成员,扩展到一个对齐成员的大小,但如果连续数个成员的所占内存大小之和小于对齐单位,则这几个成员共享一个对齐单元。
静态成员
与全局变量的存储模式相同,不同的是可通过类名访问到该静态成员变量。被const修饰同样会被放到只读数据段,未被const修饰放到读写或未初始化数据段。
成员函数
非静态成员函数
与外部函数相同,保存于内存中的代码段。通过 &CLASS::FUNCTION 可以拿到该函数在代码段中的地址(该函数的指针), 每个非静态内部函数会加一个this指针用于区分是那个类对象在调用该函数。
非静态成员函数会被默认视为内联函数,但并不一定展开执行。
class A {
private:
char t;
char t2;
const int t1;
public:
A(const char& t,const char& t2, const int& t1):t(t),t2(t2),t1(t1) {}
void test() {}
void test2(const int& a) {}
int test3() const { return 0; }
};
int main() {
auto fptr = &A::test;//void(A::*)() 0x007713ac
auto fptr2 = &A::test2;//void(A::*)(const int &) 0x007713b6
auto fptr3 = &A::test3;//int(const A::*)() 0x007713b1
auto fptrStatic = &A::testStatic;//void(*)() 0x007713bb
return 0;
}
静态成员函数
保存位置上与外部函数,非静态成员函数一致。区别是,不会生成this指针参数,调用该函数时,由于没有this指针,所以无法在其内部裸调用类的非静态成员。
虚函数&纯虚函数
(待补充)
补充&总结
- 所有函数都在代码段上,与是否static修饰无关;
- 类外部的static修饰只影响外部可见性,内部的static影响变量的保存位置和函数的参数;
- 只有在只读段上的只读操作是被操作系统保护的,无法通过指针绕开;
- 未在只读区上的const限制都可以通过指针绕开;所有的访问域限制都是通过编译器实现的,都可以通过指针绕开;(可以绕开,但没有必要,一般如果需要绕开的private,static,const限制,是oop设计有问题)
- 关于内联函数与宏定义函数:
#include<stdio.h>
#define testM() \
printf("%s\n","test marco");
inline void testI() {
printf("%s\n","test inline");
}
void test() {
printf("%s\n","test");
}
int main() {
auto p = testI;
printf("%p\n",p);
testM();
testI();
test();
p();
return 0;
}
测试代码如上,测试环境linux,编译器g++ 5.4.0。
编译设置g++ -std=c++11 -O1 -g -o test main.cpp
反汇编main函数
gdb ./test
disassemble main
得到的反汇编如下:
sub $0x8,%rsp
mov $0x4005cb,%edx
mov $0x400689,%esi
mov $0x1,%edi
mov $0x0,%eax
callq 0x400430 <__printf_chk@plt> //print ptr of testI
mov $0x40068d,%edx
mov $0x400680,%esi
mov $0x1,%edi
mov $0x0,%eax
callq 0x400430 <__printf_chk@plt> //print test marco
mov $0x400674,%edx
mov $0x400680,%esi
mov $0x1,%edi
mov $0x0,%eax
callq 0x400430 <__printf_chk@plt> //print test inline
callq 0x400546 <test()> //print test
callq 0x4005cb <testI()> //print test inline
mov $0x0,%eax
add $0x8,%rsp
retq
内联函数和宏定义函数在汇编中都被一样的展开执行(没有call的消耗),内联函数有其他函数的一切性质,并不特殊(可以通过指针访问调用)。
问题在于对编译器的不同优化设置,会对内联函数是否展开造成影响。未开启编译器优化时,可能所有的函数(不管有没有inline修饰)都不会被展开,但当编译器优化等级设置的很高的
时候,没有inline修饰的函数,也有可能被优化为内联函数。inline相当于对编译器提出建议,建议修饰的函数内联,至于真正内联没有,不确定,需要去看编译结果。(gcc可以通过__attribute__((always_inline))代替inline,
来强制内联,attribute ((noinline))强制不内联,vc不清楚)。
另外,看反汇编的结果,也很容易看出内联函数宏定义函数的优,劣势:
Positive:减少跳转造成的损失
Negative:加大了内存消耗
如果能保证内联函数一定被展开而不是被跳转执行,内联函数基本优于宏定义函数。原因如下:
- 两种方式运行效率完全没有任何区别;
- 宏定义函数不直观,遇到复杂逻辑个人觉得看起来很难受,比如libuv库中,全部用宏实现红黑树(tree.h),如果在c++里实现用内联+模板去做看起来就清晰得多。
- 宏定义函数无法在写代码的时候提供语法检查,内联函数可以;
- 宏定义函数可能出现重名,内联函数可以通过static或加命名空间的方式,避免重名。