知识点目录
1、在main执行之前和之后执行的代码可能是什么?
main函数在执行前,主要就是初始化系统相关资源:
- 设置栈指针
- 初始化静态static变量和global全局变量,即.data段的内容
- 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容
- 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
- 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数
__attribute__((constructor))
main函数执之后:
- 全局对象的析构函数会在main函数之后执行
- 可以用atexit注册一个函数,它会在main之后执行
- attribute((constructor))
2、结构体内存对齐问题?
- 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
- 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)
- c++11以后引入两个关键字alignas与alignof。其中alignof可计算出类型的对齐方式,alignas可以指定结构体的对齐方式。
//但是alignas在某些情况下是不能使用的,具体见下面的例子://
#include <stdint.h>
using namespace std;
/*
char 1B=8bit = -2^8 ~ 2^8-1
int 4B=32bit = -2^31 ~ 2^31-1
long long 8B
float 4B
double 8B
*/
typedef unsigned char uint8_t
typedef unsigned short int uint16_t
typedef unsigned int uint32_t
//alignas生效情况
struct Info{
uint8_t a;//unsigned char
uint16_t b;//unsigned short int
uint8_t c;//unsigned int
};
cout<<sizeof(Info)<<endl;
cout<<alignof(Info)<<endl;//类型对齐alignof
struct alignas(4)Info2{//结构体对齐alignas
uint8_t a;
uint16_t b;
uint8_t c;
};
cout<<sizeof(Info2)<<endl;//8 4+4
cout<<alignof(Info2)<<endl;//4
//alignas将内存对齐调整为4个字节。所以sizeof(Info2)的值变为了8//
//alignas失效的情况
struct Info{
uint8_t a;
uint32_t b;
uint8_t c;
};
cout<<sizeof(Info2)<<endl;//12 4+4+4
cout<<alignof(Info2)<<endl;//4(类型对齐)
//若alignas小于自然对齐的最小单位,则被忽略//如alignas(1),默认uint16_t还是2
//如果想使用单字节对齐的方式,使用alignas是无效的。应该使用#pragma pack(push,1)或者使用__attribute__((packed))
//可以使用#pragma pack(4) ,最后又想使用默认对齐方式时,可以使用#pragma pack() ;
#if defined(__GNC__) || defined(__GNUC__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#define pack(push,1)
#endif
struct Info{
uint8_t a;
uint16_t b;
uint8_t c;
}ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
#undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
#pragma pack(pop)
#undef ONEBYTE_ALIGN
#endif
cout<<sizeof(Info)<<endl;//6 1+4+1
cout<<alignof(Info)<<endl;//6
//确定结构体中每个元素大小可以通过下面这种方法
/*#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4字节对齐
相当于 #pragma pack (push,4) */
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#pragma pack(push,1)//#pragma pack (push,1)作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#endif
/**
*
* 0 1 3 6 8 9 15
* +-+---+-----+---+-+-------------+
* | | | | | | |
* |a| b | c | d |e| pad |
* | | | | | | |
* +-+---+-----+---+-+-------------+
*
*/
struct Info {
uint16_t a : 1;
uint16_t b : 2;
uint16_t c : 3;
uint16_t d : 2;
uint16_t e : 1;
uint16_t pad : 7;
} ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
#undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
#pragma pack(pop)//#pragma pack(pop)作用:恢复对齐状态
#undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 2
std::cout << alignof(Info) << std::endl; // 1
//这种处理方式是 alignas 处理不了的。
3、指针和引用的区别
- 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
- 指针可以有多级,引用(相当于是原变量一种标签)只有一级
- 指针可以为空,引用不能为NULL且在定义时必须初始化
- 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
- sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
- 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
- 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(,具体情况还要具体分析)。
- 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量
- 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
- 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
void test(int* p)//为改变p的地址
{
int a=1;
p=&a;
cout<<p<<" "<<*p<<endl;
}
void main(void)
{
int* p=NULL;
test(p);
if(p==NULL) cout<<"指针p为NULL"<<endl;'
}
//====================
void testPTR(int* p)//函数内部栈申请的内存,用完即释放
{
int a = 12;
p = &a;
}
void testREFF(int& p)
{
int a = 12;
p = a;
}
void main(void)
{
int a = 10;
int* b = &a;
testPTR(b);
cout << a << endl;// 10
cout << *b << endl;// 10
a = 10;
testREFF(a);
cout << a << endl;//12
}
//在编译器看来,int a=10;int &b=a;
//等价于:int* const b=&a;//b的地址不可变
//等价于 *b = 20; 自动转换为指针和自动解引用
//就是我们呢可以把引用看成对变量的一种标签,同一个变量值不通过名字
4、堆和栈的区别
- 申请方式不同。
- 栈由系统自动分配。
- 堆是自己申请和释放的。
- 申请大小限制不同。
- 栈顶(esp)和栈底(ebp)是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改。
- 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
- 申请效率不同。
- 栈由系统分配,速度快,不会有碎片。
- 堆由程序员分配,速度慢,且会有碎片
- 栈空间默认是4M, 堆区一般是 1G - 4G
- 栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
- 堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大
5、区别以下指针类型?
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
- int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
- int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
- int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
- int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
6、基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间
- 首先整理一下虚函数表的特征:
- 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
- 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表(函数指针的数组),即虚函数表不是函数,不是程序代码,不可能存储在代码段
- 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中
- 根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示:
- 虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别
- 由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
- 一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区
- C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
- 简述虚函数表的原理c++虚函数表
7、new / delete 与 malloc / free的异同
- 相同点:
- 都可用于内存的动态申请和释放
- 不同点:
- 前者是C++运算符,后者是C/C++语言标准库函数
- new自动计算要分配的空间大小,malloc需要手工计算
- new是类型安全的,malloc不是。例如
int *p=new float[2];//error int *p=(int*)malloc(2*sizeof(double));//编译无错误
- new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用。
- 后者需要库文件支持(#include <stdio.h>),前者不用
- new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象
8、new和delete是如何实现的?
- new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
- delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
9、malloc和new的区别?
- malloc和free是标准库函数,支持覆盖(函数覆盖);new和delete是运算符,并且支持重载。(运算符重载)
- malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
- malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
int* p=(int*)malloc(200*sizeof(int));
- delete只会调用一次析构函数。delete[]会调用数组中每个元素的析构函数。
10、宏定义和函数有何区别?
- 宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
- 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
- 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
- 宏定义不要在最后加分号。