说明:
- 面试题来源于网络书籍,公司题目以及博主原创或修改(题目大部分来源于各种公司);
- 文中很多题目,或许大家直接编译器写完,1分钟就出结果了。但在这里博主希望每一个题目,大家都要经过认真思考,答案不重要,重要的是通过题目理解所考知识点,好应对题目更多的变化;
- 博主与大家一起学习,一起刷题,共同进步;
- 写文不易,麻烦给个三连!!!
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((destructor))
2.结构体内存对齐问题?
- 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
- 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)
c++11以后引入两个关键字 alignas (opens new window)与 alignof (opens new window)。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式。
但是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 4 + 4
std::cout << alignof(Info2) << std::endl; // 4
// 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小于自然对齐的最小单位,则被忽略。
- 如果想使用单字节对齐的方式,使用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
- 确定结构体中每个元素大小可以通过下面这种方法:
#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处理不了的。
https://interviewguide.cn/notes/03-hunting_job/02-interview/01-01-01-basic.html
3.在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?
答案:
- 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
- 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
- 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
4.你觉得堆快一点还是栈快一点?
答案:
毫无疑问是栈快一点。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
5.被free回收的内存是立即返还给操作系统吗?
答案:
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片
6.宏定义和typedef区别?
答案:
- 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名
- 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
- 宏不检查类型;typedef会检查数据类型。
- 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
- 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。
7.C++中const和static的作用
答案:
static
- 不考虑类的情况
- 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
- 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
- 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
- 考虑类的情况
- static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
- static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
const
- 不考虑类的情况
- const常量在定义时必须初始化,之后无法更改
- const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}
- 考虑类的情况
- const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
- const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
- const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突
8.拷贝初始化和直接初始化
答案:
- 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下:
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
- 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
- 当拷贝构造函数为private时:语句3和语句4在编译时会报错
- 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
9.野指针和悬空指针
答案:
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
- 野指针 :指的是没有被初始化过的指针
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
- 悬空指针:指针最初指向的内存已经被释放了的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生
10.如何用代码判断大小端存储
答案:
大端存储:字数据的高字节存储在低地址中
小端存储:字数据的低字节存储在低地址中
例如:32bit的数字0x12345678
小端模式中的存储方式为:
大端模式中的存储方式为:
方式一:使用强制类型转换-这种法子不错
#include <iostream>
using namespace std;
int main()
{
int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
方式二:巧用union联合体
#include <iostream>
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
int a;
char ch;
};
int main()
{
endian value;
value.a = 0x1234;
//a和ch共用4字节的内存空间
if (value.ch == 0x12)
cout << "big endian"<<endl;
else if (value.ch == 0x34)
cout << "little endian"<<endl;
}
11.volatile、mutable和explicit关键字的用法
答案:
(1)volatile
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
volatile 指针
volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念
修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;volatile char* vpch;
指针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:
char* const pchc;char* volatile pchv;
注意:
- 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
- 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
- C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。
如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。
volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
(2)mutable
mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置。
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A = 10;//错误 不可修改值,this已经被修饰为常量指针
m_B = 20;//正确
}
};
class person
{
public:
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
};
int main()
{
const person p = person();//修饰常对象 不可修改类成员的值
p.m_A = 10;//错误,被修饰了指针常量
p.m_B = 200;//正确,特殊变量,修饰了mutable
}
(3)explicit
explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换,注意以下几点:
- explicit 关键字只能用于类内部的构造函数声明上
- 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换