目录
1、main函数之前执行和之后执行的代码可能有哪些?
main函数之前:
- 设置栈指针,开辟main函数栈帧,main函数有参数的话进行参数压栈;
- 初始化静态变量和全局变量,也就是 .data 段内容;
- 将未初始化的全局变量进行初始化为0(其中bool型为false),也就是 .bss 段内容;
- 全局对象的初始化,在main函数之前调用构造函数;
main函数之后:
- 全局对象的析构函数会在main函数之后执行;
- 由atexit注册的函数,会在main函数执行之后执行;可以多次调用注册多个,但执行顺序与注册顺序相反;(atexit称为注册函数)
2、结构体对齐方式?
- 结构体内成员按照声明的顺序存储,第一个成员地址和整个结构体地址相同;
- 未特殊说明时,按结构体中size最大的成员对齐,(若有double成员,按8字节对齐);
注意:C++11以后引入两个关键字 alignas 和 alignof
- alignas:可以指定结构体的对齐方式,但是若指定的小于自然对齐的最小单位,则忽略;
- alignof:可以获取类型的对齐方式;
- 若想使用单字节对齐,可以使用 #pragma pack(1);
3、指针和引用的区别?
抛出重点:引用是类型更加安全的一种指针,定义一个指针和定义一个引用,二者在汇编代码上是完全一样的。安全是指:定义一个指针可以不用初始化,因此在程序中使用该指针的时候我们首先要判断一下指针是否合法;定义一个引用必须进行初始化,所以更加安全。
- 指针是一个变量,存储的是一个地址,引用和原来的变量本质上是同一个东西,是原变量的别名;
- 指针可以有多级指针,引用只有一级;
- 指针在初始化之后可以改变指向,引用在初始化之后不可再改变;
- 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个指针变量,在函数中改变这个变量的指向并不影响实参(也就是值传递),而引用却可以,在函数中对参数的改变就相当于是对实参的直接操作;
- 引用本质是一个指针,所以对引用本身来说,在32位操作系统上大小也是4个字节;
- 不存在指向空值的引用,但存在指向空值的指针;
4、在函数传参时,什么时候传指针,什么时候传引用?
- 需要返回函数内局部变量的内存的时候用指针,使用指针传参需要开辟内存,用完之后记得释放指针,不然会造成内存泄漏;而返回局部变量的引用是没有意义的。
- 对栈空间大小比较敏感(比如递归)的时候使用引用,因为引用传递不需要创建临时变量,开销更小。
- 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式。
5、堆和栈哪个快?
抛出结论:栈更快
- 操作系统在底层为栈提供支持,会分配专门的寄存器存放栈的地址,栈的出入栈操作也十分简单,并且有专门的指令执行,所以栈的效率高且快;
- 堆的操作是由C/C++函数库提供的,在分配堆内存时需要一定的算法寻找合适大小的内存,并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
6、宏定义、函数、typedef的区别?
- 宏定义在预处理阶段完成文本替换,不存在函数调用的过程;函数在运行阶段需要跳转到具体 的函数,存在函数调用开销;typedef属于编译的一部分,主要用于定义类型别名;
- 宏定义参数没有类型,不进行类型检查;函数和typedef要进行类型检查;
7、strlen和sizeof的区别?
- sizeof是运算符,编译时得到类型的大小;strlen是库函数,运行时得到大小;
- sizeof参数可以是任何数据类型;strlen函数的参数只能是字符指针且末尾是 ‘\0’ 的字符串;
- sizeof的值在编译时期确定,所以不能用来得到动态分配存储空间的大小。
8、override 和 final 关键字?
- override:表明子类重写父类虚函数,如果函数名写错会报错,会提示我们;
- final:通过在类名或者虚函数后面添加final关键字,表明该类不可被继承、不希望该虚函数被重写;
9、野指针和空悬指针?
- 野指针:定义指针变量但未进行初始化,解决办法:初始化或者置空;
- 空悬指针:调用free或delete后,没有置空,解决方法:释放后立即置空或者使用智能指针;
10、深拷贝和浅拷贝?
- 浅拷贝:只拷贝一个指针,并没有开辟新的内存,拷贝的指针和原来的指针指向同一块内存,如果原来的指针所指向的资源进行释放了,那么再释放当前指针的资源就会出现错误;
- 深拷贝:不仅拷贝指针,还会开辟内存存放新的值,即时原先的对象进行资源释放了,也不会影响新的对象。
注意:一般在进行类对象的拷贝构造时,若类的成员数据有指向外部资源的变量,则需要我们自己定义对象的拷贝构造,因为类默认提供的拷贝构造是浅拷贝。
11、如何用代码判断大小端存储?
注意:地址从低到高
- 若低地址存数据的高字节,则称为大端存储;
- 若低地址存数据的低字节,则称为小端存储;
大小端指的就是低地址存放的是数据的高字节还是低字节。对于我们的操作系统,主机字节序是按照小段存储,也就是先存放低字节,然后存放高字节;但是在网络中,网络字节序是大端存储,(符合我们人类的观察方式,从左往右先看到高位然后地位,可读性高),所以在socket编程中,要进行ip地址的大小端转换。
例如:32bit的数字0x12345678
通过代码进行判断:
方法1: 通过强制类型转换
#include<iostream>
using namespace std;
int main()
{
int a = 0x1234;
char c = (char)a;
if(c==0x12)
{
cout << "大端存储..." << endl;
}
else if(c==0x34)
{
cout << "小端存储..." << endl;
}
return 0;
}
运行结果:
方法2:使用联合体
#include<iostream>
using namespace std;
// 巧用联合体
union Test
{
int a;
char c;
};
// a 和 ch 共用4个字节的空间
int main()
{
Test t;
t.a = 0x1234;
if(t.c == 0x12)
{
cout << "大端存储..." << endl;
}
else if(t.c == 0x34)
{
cout << "小端存储..." << endl;
}
return 0;
}
运行结果:
12、什么情况下会调用拷贝构造函数?
- 用类的一个实例化对象去初始化另一个对象;
- 函数的参数是类对象的时候,会先调用拷贝构造用实参生成一个临时对象(形参);
- 函数的返回值是函数类局部对象时,会先调用拷贝构造在main函数栈帧上生成一个临时对象;
13、C++里面几种new?
1.普通new --- plain new
就是我们平时使用的new,在C/C++中定义如下:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
注意:由此可见new与malloc的一个区别,malloc申请内存失败会返回NULL,所以我们可以根据返回值来判断申请是否成功;new申请内存失败会抛出异常bad_alloc,所以我们可以根据捕获异常来判断申请内存是否成功。
2.不抛出异常的new --- nothrow new
nothrow new 在空间分配失败的情况下是不抛出异常,而是返回NULL;定义如下
void* operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
3. 定位new --- placement new
定位new允许在一块已经分配成功的内存上重新构造对象或者对象数组,所以不用担心内存分配失败,因为它根本就不会分配内存,它所做的唯一一件事就是调用对象的构造函数。定义如下:
void* operator new(size_t,void*);
void operator delete(void*,void*);
使用定位new,注意以下两种情况:
1、定位new的主要作用就是反复利用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组;
2、通过定位new构造的对象数组,一定要显式的调用他们的析构函数来销毁(析构函数并不释放内存,因为我们可能还会在该空间构造其它对象),千万不要使用delete,因为定位new构造的对象或者对象数组大小并不一定等于原来空间的大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
14、值传递、指针传递、引用传递的区别和效率?
- 值传递:有一个实参向形参拷贝构造生成临时对象的过程,临时对象出函数作用域还要进行析构,效率很低;
- 指针传递:有针对地址的临时拷贝,也是在函数栈帧生成临时指针,效率比值传递高;
- 引用传递:相当于地址直接进行传递,函数里面直接对原始数据进行操作,效率更高;
总结:指针传递和引用传递比值传递效率高,一般主张使用引用传递,代码逻辑上更加紧凑,清晰。
15、静态变量什么时候初始化?
- 初始化只有一次,但可以多次赋值,在主程序之前,编译器就已经为其分配好了内存;
- 静态局部变量和全局变量一样,数据存放在全局区域;在C和C++中静态局部变量的初始化节点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存后,就会进行初始化。C++中,初始化是在代码运行起来后,执行到相关代码才会进行初始化。
16、const关键字的作用?
- 阻止一个变量被改变,定义一个const变量通常要进行初始化,因为后续就没有机会改变了;
- 对指针来说,无非就是 int const *p 与 int * const p 的区别,不再赘述;
- 在函数中,形参被const修饰,表明它是一个输入参数,在函数内部不能对他进行改变;
- 修饰类的成员函数,表明是一个常方法,内部不能试图修改成员变量的值,而且类的常对象只能访问常方法;
- 修饰类的成员函数的返回值,使得其返回值不能为左值;
- const 类型的变量可以同过 const_cast类型转换去除常量属性;
- const类型的变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
- 函数值传递时,形参加不加const对于实参都没啥影响,因为之前也说了值传递会生成临时对象,操作的都是临时对象;指针传递和引用传递就不一样了,因为它指向的是实参本身,所以加const才会实实在在的保护实参;
17、从汇编层解释引用?
抛出结论:引用和指针在汇编层次是一模一样的,引用是类型更加安全的指针,必须进行初始化;
int x = 1; /* mov dword ptr[ ebp - 4 ],1; */
int &b = x; /* lea eax, [ ebp - 4 ] , mov dword ptr [ ebp - 8 ], eax;
x 的地址为 ebp - 4,b 的地址为 ebp - 8,因为栈是从高地址向低地址进行分配,所以 b 的地址比 x 低;
lea eax, [ ebp - 4 ];这条语句是把x的地址 ebp-4 放到寄存器 eax 里面;
mov dword ptr [ ebp - 8 ], eax;这条语句是把 eax 里面的内容(x 的地址)存入变量 b 中,和指针一模一样;
18、C++中string与C语言中 char* 的区别?
- string是对char*的封装,封装的string包含了char*数组,容量、长度等属性;
- string可以进行动态扩展,在每次扩展时候另外申请一块原空间大小两倍的空间,然后将原字符串拷贝过去,并加上新增的内容;