一. 在main函数执行之前和之后的代码可能是什么?
main函数之前,主要是初始化系统相关资源:
1. 设置栈指针。
2. 初始化静态static变量和global全局变量,即.data段的内容。
3. 将未初始化的部分全局变量赋初值:数值型short、int、long等设置为0,bool为FALSE,指针为NULL等,即.bss段的内容。
4. 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码
5. 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数
6. __attribute__((constructor)) ,这是 GCC 编译器的一个特性,它允许你定义一个函数,在程序开始执行之前,也就是 main
函数被调用之前,这个函数会被自动调用。
main函数执行之后:
1. 全局对象的析构函数。
2. 可以再atexit注册一个函数,它会在main之后执行。atexit
是 C 标准库中的一个函数,它用于注册一个函数,该函数会在程序正常终止时被调用,即在 main
函数执行完毕后,但在程序实际退出之前。这通常用于执行清理代码,比如关闭文件、释放内存、断开网络连接等。
3. __attribute__((destructor))
附上资料:(来源:《深入理解计算机系统》)
这里左边褐色和蓝色是在.data或者.bss段上,红色的是在.text段。
windows磁盘上有一个xxx.exe文件,但是CPU是不能运行的,先把这个文件加载到内存当中,这个内存不是物理内存。
用户空间:
空指针: 平时空指针指向的就是这个区域
指令在运行的时候放在.text段(代码段)
.rodata :只读数据段
char * p = "hello world"; *p = 'a'; //错误的
const char* p = "hello world"; //正确的
.data :数据段,存放初始化的,而且初始化不为0的
.bss : 数据段,存放未初始化的,而且初始化为0的(如int data; //全局变量,未初始化但是打印出来为0)
.heap:堆区(从上往下增长)
加载动态共享库(windows:*.dll linux: *so)
stack:栈空间(从下往上增长)
命令行和环境变量(如 :./a.out 192.168.1.110 90)
内核空间:
ZONE_DMA(16M)
ZONE_NORMAL(800M,进程控制块等内核)
ZONE_HIGHMEM(做地址映射用的)
二. 结构体内存对齐问题?
1. 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址是相同的。
2. 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐)。
扩展:C++11中引入了alignas和alignof。
alignas:指定结构体的对齐方式。
alignof:计算出类型的对齐方式。
但是alignas在某些情况下是不能使用的,具体例子:
// alignas 生效的情况
struct Info {
uint8_t a;
uint16_t b;
uint8_t c;
};
std::cout << sizeof(Info) << std::endl; // 6 2 + 2 + 2
std::cout << alignof(Info) << std::endl; // 2
struct alignas(4) Info2 {
uint8_t a;
uint16_t b;
uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 8 2 + 2 + 2 + 2
std::cout << alignof(Info2) << std::endl; // 4
alignas将内存对齐调整为4字节。故sizeof(Info2)的值变为8。
// alignas 失效的情况
struct Info {
uint8_t a;
uint32_t b;
uint8_t c;
};
std::cout << sizeof(Info) << std::endl; // 12 4 + 4 + 4
std::cout << alignof(Info) << std::endl; // 4
struct alignas(2) Info2 {
uint8_t a;
uint32_t b;
uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 12 4 + 4 + 4
std::cout << alignof(Info2) << std::endl; // 4
若alignas小于自然对齐的最小单位,则被忽略。
3. 如果想使用单字节对齐的方式,使用alignas是无效的。应该使用#pragma pack(push, 1)或者使用__attribute__((packed))
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#pragma pack(push,1)
#endif
struct Info {
uint8_t a;
uint32_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
std::cout << sizeof(Info) << std::endl; // 6 1 + 4 + 1
std::cout << alignof(Info) << std::endl; // 1
4. 确定结构体中每个元素大小可以通过下面这种方法:
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#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)
#undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 2
std::cout << alignof(Info) << std::endl; // 1
这种方式是alignas处理不了的。
三. 指针和引用的区别
1. 指针是一个变量,存储的是一个地址,指向内存的一个存储单元; 引用是原变量的别名,跟原来的变量实质上是同一个东西。
2. 指针可以为空,引用不能为NULL且在定义式必须初始化
3.指针可以有多级,引用只有一级。
4. 指针在初始化可以改变指向,而引用在初始化之后不可以再改变。
5. 把指针作为参数传递的时候,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但是不是同一个变量。在函数中改变这个变量的指向不影响实参,而引用却可以。
6.sizeof两者,指针是本指针的大小,引用是引用所指向变量的大小。
7.引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但是引用只能作为一个变量引用);指针变量可以重新指向别的变量。
8. 指针的声明和定义可以分开定义,引用在声明是必须初始化为另一变量。
在汇编层面,一些编译器将引用当成指针操作,因此引用会占用空间。是否占用空间,应该结合编译器分析。
引用的本质
引用是指针常量的伪装。
引用是编译器提供的一个有用且安全的工具,去除了指针的一些缺点,禁止了部分不安全的操作。
变量是什么?变量就是一个在程序执行过程中可以改变的量。
换一个角度,变量是一块内存区域的名字,它代表了这块内存区域,当我们对变量进行修改的时候,会引起内存区域中内容的改变。
在计算机看来,内存区域根本就不存在什么名字,它仅有的标志就是它的地址,因此我们若想修改一块内存区域的内容,只有知道他的地址才能实现。
所谓的变量只不过是编译器给我们进行的一种抽象,让我们不必去了解更多的细节,降低我们的思维跨度而已。
程序员拥有引用,但编译器仅拥有指针(地址)。
引用的底层机制实际上是和指针一样的。不要相信有别名,不要认为引用可以节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针。
引用和指针本质上没有区别。
void test(int *p)
{
int a=1;
p=&a;
cout<<p<<" "<<*p<<endl;
}
int main(void)
{
int *p=NULL;
test(p);
if(p==NULL)
cout<<"指针p为NULL"<<endl;
return 0;
}
//运行结果为:
//0x22ff44 1
//指针p为NULL
void testPTR(int* p) {
int a = 12;
p = &a;
}
void testREFF(int& p) {
int a = 12;
p = a;
}
void main()
{
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 = 20,等价于 *b = 20;自动转换为指针和自动解引用 。
四. 在传递函数参数的时候,使用指针和引用的场景
1. 需要返回函数内部局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的(出函数作用域就释放了)。
2. 对栈空间大小比较敏感(如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小。
3. 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式。
五. 堆和栈的区别
1. 申请方式不同。
栈由系统自动分配。
堆是自己申请和释放的。
2.申请的大小限制不同。
栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a 查看,由ulimit -s修改。
堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
3. 申请效率不同。
栈由系统分配,速度快,不会由碎片。
堆由程序员分配,速度慢,且会有碎片。
六. 堆快还是栈快?
七. 区别以下指针类型
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
1. int *p[10]表示指针数组,指数组,是一个数组变量,大小为10,数组内每个元素都是指向int类型的指针变量。
2. int (*p)[10]表示数组指针,指指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
3. int *p(int) 是函数声明,函数名是p,参数是int类型,返回值是int*类型的。
4.int (*p)(int)是函数指针,是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
八. new/delete与malloc/free的异同
相同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
1.前者是C++操作符,后者是C/C++语言标准库函数。
2.new自动计算要分配的空间大小,malloc需要手工计算。
3.new是类型安全的,malloc不是,例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译无错误
4. new调用operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用。
5.后者需要库文件支持,前者不用。
6.new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象。
九. new和delete是如何实现的
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new
- operator delete
十. malloc和new的区别
十一. 既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?
主要是看多了啥
十二. 被free回收的内存是立即返回给操作系统的吗?
十三. 宏定义和函数有何区别
十四. 宏定义和typedef区别
十五. 变量声明和定义区别
十六. strlen和sizeof区别
1. sizeof是运算符,并不是函数,结果在编辑时得到而非运行中获得;strlen是字符处理的库函数。
2. sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是 '\0' 的字符串。
3. 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是8
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
return 0;
}
注:
指针占用大小为8字节(64位)。
指针占用大小为4字节(32位)。
十七. 常量指针和指针常量区别
常量指针
语法:const 数据类型 *变量名;
不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。
注意:
- 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
- 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。
- 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义。
- 如果形参的值不需要改变,建议加上const修饰,程序可读性更好。
2)指针常量
语法: 数据类型 * const 变量名;
指向的变量(对象)不可改变。
注意:
- 在定义的同时必须初始化,否则没有意义。
- 可以通过解引用的方法修改内存地址中的值。
- C++编译器把指针常量做了一些特别的处理,改头换面之后,有一个新的名字,叫引用。
3)常指针常量
语法:const 数据类型 * const 变量名;
指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。
常引用。
常量指针:指针指向可以改,指针指向的值不可以更改。
指针常量:指针指向不可以改,指针指向的值可以更改。
常指针常量:指针指向不可以改,指针指向的值不可以更改。
记忆秘诀:*表示指针,指针在前先读指针;指针在前指针就不允许改变。
常量指针:const 数据类型 *变量名;
指针常量:数据类型 * const 变量名;