整理有关cpp的面试题目 参考
基本数据类型
大端和小端
大端,高位字节放在低位地址,低位字节放在高位地址。 小端,高位字节存高位地址,低位字节存低位地址。
举个例子,数值 0x12345678
,其中 0x12
这一端是高位字节,0x78
这一端是低位字节。
int main() {
int a = 0x1234;
char *p = (char *)&a;
printf("%02x\n", *p); // 读低地址
printf("%02x\n", *(p + 1)); //
printf("%02x\n", *(p + 2));
return 0;
}
大端: 00,12 小端: 34
new 和 malloc区别
new | malloc | |
---|---|---|
运算符 | 函数 | |
自动计算分配空间大小 | 手工计算 | |
类型安全 | 返回值是指针,需要强制类型转换 | |
会调用构造函数 | 仅分配内存 | |
失败bad_malloc | 失败抛出null |
delete和free区别
delete会调用析构对象,free仅释放内存
c和cpp区别
- cpp里边new/delete,c里边malloc、free
- cpp允许函数重载
- cpp支持try,catch和throw异常处理机制
cpp内存
ELF
堆和栈的区别
堆 | 栈 | |
---|---|---|
管理 | 手动分配 | 操作系统自动分配 |
大小 | GB级别 | MB级别 |
方式 | 动态分配 | 静态动态都有 |
效率 | 低 | 高,有专门的寄存器处理 |
malloc原理
参考malloc 的实现原理 内存池 mmap sbrk 链表
- 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。内存释放回到内存池。
- 若分配内存大于 128k ,调用 mmap() ,匿名映射(fd = -1)的方法在文件映射
- 区域中分配一块内存。这种内存释放的时候会直接还给操作系统。
void *mmap(void * addr , size_t length , int prot , int flags ,
int fd , off_t offset );
- malloc采用内存池,先申请一块大内存,切分成大小不同的内存块,用户申请选一块相近的内存块。
- 不是malloc之后立刻占用实际内存,第一次访问时产生缺页中断才分配物理内存。
分配方式:分离的空闲链表,维护多个空闲链表。特例是伙伴系统分配器的主要优点是它的快速搜索和快速合并。
free原理
free
只接受一个指针,却可以释放恰当大小的内存,这是因为在分配的区域的首部保存了该区域的大小。
深拷贝 vs 浅拷贝区别
深拷贝:深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。 浅拷贝:拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
仿函数
仿函数就是一种具有函数特质的对象,需要重载函数运算符()
如sort(vec.begin(),vec.end(),less<int>())
const
用途
防止修改;节省空间;注:非const默认是extern,const要在其他文件中使用不许指定为extern。
const和define区别
- define在预处理展开,const是编译使用;
- const有类型,编译器可进行安全检查;
- define不能被调试;
- define有多份copy,const全局只有一份copy
指针与const
- 底层const:const在
*
左边,const char* a``char const * a
指针所指对象是常量,指向常量的指针
注意:
const int *ptr;
// 可以不赋值,*ptr才是常量,ptr不是
// 不能通过指针*ptr 改对象值
// 必须用const void* 保存 const 对象地址
// 非const对象地址可以赋值给const对象指针
int val = 3;
*ptr = &val;
- 顶层const:const在
*
右边,char* const a
指针是常量,常量指针
int num = 0;
int* const ptr = #
// 必须初始化
// const常量不能赋值给常量指针
const int a = 10;
int * const ptr1 = &a; // error: const int * -> int *
const int * ptr2 = &a; // ok
const int * const ptr3 = &a; // ok
函数与const
- 引用传递不需要临时对象
- 函数:const对象只能访问const成员函数;非const对象访问任意成员函数,包括const成员函数
class A {
void getx () const;
void gety();
}
const A a;
a.getx(); //ok
a.gety(); //error
A b;
b.getx(); //ok
b.gety(); //ok
- 函数:const成员函数只能访问const成员函数,const成员变量
void
A::getx() const {
gety(); //error
}
- 变量:类中的const成员变量必须通过初始化列表进行初始化
constexpr
- const是编译期或运行期常量(仅修饰成员函数)
- constexpr修饰变量,编译期就可知
- constexpr修饰函数,实参是常量能在编译器出结果就直接出,否则推迟到运行期
static
静态局部变量
函数中静态局部变量:静态局部变量的空间只分配一次,函数再次调用会保留上次调用的值
void demo()
{
// static variable
static int count = 0;
cout << count << " ";
// value is updated and
// will be carried to next
// function calls
count++;
}
int main()
{
for (int i=0; i<5; i++)
demo(); // 0 1 2 3 4
return 0;
}
静态全局变量/函数
静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;因此可以防止命名冲突
静态成员变量
- 静态成员变量是所有对象共有的,静态成员变量只有一份;静态数据成员不占用类的内存,分配在全局数据区(在类外部初始化,类内部仅能声明不能初始化,静态数据的变量一般有默认初始化过程为0),没有类的实例就可以操作。
class A {
static int a;
};
int A::a = 100;
int main()
{
cout << sizeof(A) << endl;
}
- static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。
场景
同类的多个对象实现数据共享,节省存储空间。
静态成员函数
仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
原因:没有this指针,不能被声明为const、虚函数和volatile,非static任意访问
静态成员函数为什么不能声明为虚函数?
static成员函数不属于任何对象或实例,加virtual没意义;虚函数需要this指针访问vptr,static成员函数没有this指针。
this
- this并不是对象一部分,不会影响sizeof结果
- 使用:非静态成员函数中返回类对象本身,
return *this;
;形参与成员变量名称相同用于区分。 - 顶层const,
T * const this
是非静态成员函数的第一个参数
A a;
a.func(10); // A::func(&a,10);
- this在成员函数的开始前构造,在成员函数的结束后清除
inline
- 编译时,函数体嵌入调用处
- 代码膨胀,仅节省了函数调用开销
- 虚函数可以是inline,前提是虚函数不表现为多态性;表现多态性的话不能知道运行期间调用哪个代码。
class Base
{
public:
virtual void who()
{
cout << "I am Base\n";
}
};
class Derived: public Base
{
public:
void who()
{
cout << "I am Derived\n";
}
};
int main()
{
// note here virtual function who() is called through
// object of the class (it will be resolved at compile
// time) so it can be inlined.
Base b;
b.who();
// Here virtual function is called through pointer,
// so it cannot be inlined
Base *ptr = new Derived();
ptr->who();
return 0;
}
define和inline的区别:
- 宏函数 预处理时候文本替换,内联函数 编译时嵌入\
- 内联函数有类型检查和语法判断 \
- 编译器可以拒绝不符合条件的内联函数,宏函数直接替换\
- 宏不能访问类的私有成员
宏函数原地替换,遇到优先级不同可能带来问题,每个变量都带括号
sizeof
- 空类大小为1
- 虚函数本身,成员函数和静态成员不占存储
- 不管多少个虚函数,只有一个虚函数指针,vptr
虚函数
cloud.tencent.com/developer/a…
- 在运行时动态绑定,实现多态
- 虚函数表实现,虚函数地址保存在虚函数表vtable中,类的空间保存了虚函数指针vptr指向vtable。
- class单继承,每个class有自己的vtable,存在override就写入自己的虚函数地址,不存在就用父类对应的函数地址;其他的虚函数添加到vtable的末尾。
- class多(n)继承,派生类存在n个vptr,派生类自己的虚函数放在第一个虚函数表后边
- 虚函数表在编译期间创建,虚函数指针在运行时创建(构造函数自动加赋值函数)
- 动态联编:运行时才确定调用的函数;静态联编:编译时确定的函数调用关系。
- 虚函数调用取决于指向对象的类型;虚函数默认参数看指针本来的类型。默认参数是静态绑定的。
A a; // 静态联编
a.fun();
A *b; // 动态联编
b->fun();
class Base
{
public:
virtual void fun ( int x = 10)
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void fun ( int x = 20 )
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base *bp = &d1;
bp->fun(); // 10
return 0;
}
- 构造函数不能是虚函数,编译器自动在构造函数中创建vptr,构造函数是虚函数的话,需要vptr访问vtable,这个时候vptr还没产生。
overload override overwrite
- overload重载,同一个类中:参数不同(数目、类型、顺序),名字相同的函数
- override覆盖,派生类函数重写基类函数:名字相同,参数相同,基类有virtual限定
- overwrite隐藏,派生类隐藏基类函数:名字相同,参数不同无论基类virtual与否都是隐藏;参数相同基类没有virtual关键字。
多态
父类指针指向子类对象
纯虚函数
virtual void show() = 0;
- 只能作为基类来派生新类
- 不能创建抽象类的对象,但抽象类的指针可以指向派生类\抽象
Derived d;
Base &b = d;
Base *ptr = new Derived();
- 构造函数不能是虚函数,析构函数(尽量)是虚函数
vtable
- 每个使用虚函数的类都有虚拟表
- 与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针
为什么析构函数要被设置成虚函数
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() {
cout << "~shape()" << endl;
}
};
class Circle : public Shape{
public:
void draw() {
cout << "draw" << endl;
}
~Circle() {
cout << "~circle()" << endl;
}
};
int main() {
Shape* p = new Circle();
delete p;
return 0;
}
当父类指针指向子类对象的时候,释放父类指针,如果此时析构函数不是虚函数,那么将只会调用父类的析构函数,不会调用子类的析构函数,造成内存泄漏问题。
内存对齐
- 解决问题:内存对齐的主要目的是为了减少
CPU
访问内存的次数,加大CPU
访问内存的吞吐量。 static
成员变量不占用结构体的空间,由于静态成员变量在程序初始化时已经在静态存储区分配完成,所有该结构体实例中的静态成员都指向同一个内存区域;
强制类型转换
- const_cast 编译期完成,去除const/volatile
- static_cast 编译期完成,信息丢失需要程序员自己保证; 上行(派生类到基类)安全、下行不安全
- dynamic_cast
动态转换有类型检查,用于下行(基类转派生类)安全,可实现
能否安全将对象指针转换为目标指针
,转换失败(派生类到基类,不完整的对象)返回nullptr;上行等同于static_cast;
指针、引用转换到类
需要编译器的**Run-Time Type Information (RTTI)**支持
- reinterpret_cast 编译期完成,强制类型转换
volatile
- 不要优化
- 每次从内存取,不从寄存器取
- const可以有volatile
// 需要加上volatile修饰,运行时才能看到效果
const volatile int MAX_LEN = 1024;
auto ptr = (int*)(&MAX_LEN);
*ptr = 2048;
cout << MAX_LEN << endl; // 输出2048
const表示read only,需要编译器优化;volatile会禁止优化,因此可以被修改 4. 指针可以是volatile
运算符重载
- 前缀形式重载调用operator ++ () ,后缀形式重载调用 operator ++ (int)。
int 在括号内是为了向编译器说明这是一个后缀形式,而不是表示整数。
内存管理
智能指针
- 利用RAII技术,构造分配,析构释放。
- 智能指针有一个通用的规则,就是
->
表示用于调用指针原有的方法,而.
则表示调用智能指针本身的方法。
unique_ptr
独占指针,对象析构自动调用dalete。
shared_ptr
共享指针,多个指针指向同一个对象。它会在创建时分配一个引用计数,每次复制构造或赋值时,引用计数加一,每次析构时,引用计数减一,当引用计数变为零时,自动删除内存。
weak_ptr
解决 shared_ptr 的循环引用问题(A引用B,B引用A;将一方改为weak_ptr即可避免内存泄漏)。它不会增加引用计数,也不会影响对象的生命周期,当对象被释放时,weak_ptr 会自动变成空指针,因此 weak_ptr 不拥有对象的所有权。
问题
- 共享指针能否转为独占指针? 共享权限被撤销,会影响其他指向该对象的共享指针。
- 独占指针能否转为共享指针? 独占指针的所有权被释放,允许多个指针访问。
assert
- 断言是宏,不是函数
- NDEBUG可以关闭assert
extern
声明 和 定义
- 声明:告知编译器变量类型和存在性。
- 定义:分配内存初始化 一个变量或函数只能被定义一次,但可以被多次声明。
extern
分离式编译,程序可被分割成若干文件独立编译,需要文件间共享数据。
在一个源代码文件定义变量或函数,其他文件extern声明即可。
extern "C"
cpp文件编译后生成的符号与c语言生成的不同,如果C++中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。
此时extern "C"
就起作用了:告诉链接器去寻找C语言符号,而不是经过C++修饰的符号。
ifdef/endif 作用
- 条件编译,如
#ifdef __cplusplus
#ifndef ADD_H
#define ADD_H
extern int add(int x,int y);
#endif
- 避免项目重定义:多个文件同时包含一个头文件,链接时会出现重定义报错。如a.h头文件包含了mongoose.h,而在b.c文件中#include a.h和mongoose.h)这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。