面试常见问题-c++

1. 声明与定义的区别

变量定义:为变量分配存储空间,可以为变量指定初始值
变量声明:向程序表明存在一个变量
定义也是声明,加上了extern的声明不是定义,表明变量定义在其他地方
在一个程序中,变量只能定义一次,可以声明多次

函数定义:带有{}的是定义
函数声明:没有{}的是声明

int a; // 定义
int b = 0; // 定义,初始化
extern int c; // 声明,非定义
int double funca(int a, int b); // 声明函数
int double funcb(int a, int b){return max(a, b);} // 定义函数
extern double funcc(int a, int b); // 声明函数

2. extern和static的区别

extern用于声明变量或函数,并且表明这个变量或函数的定义在其他地方
static用于定义静态变量或函数,并且该变量或函数只对当前源文件可见

3. static的作用

当使用static定义一个全局变量或函数时,这个全局变量或函数成为静态全局变量或静态全局函数,它只对当前源文件可见,会对其他源文件隐藏

当使用static定义一个局部变量时,这个局部变量成为静态局部变量,它的存储区域不在栈上而在静态数据区,当这个局部执行结束时,静态局部变量的存储区域不会被释放,当第二次访问到这个静态局部变量时,它的值不会变化

当使用static定义类中的成员变量或函数时,这个成员变量或函数会成为整个类的成员,而不属于某一个具体的对象

static会对变量进行自动初始化

4. x = x + 1, x += 1, x++的效率

第一个效率最低,因为首先要读取右边x的地址,然后加一操作,然后读取左边x的地址,然后传值给它

第二个效率第二低,它首先读取x的地址,然后加一操作,然后直接传值给x

第三个效率最高,它首先读取x的地址,然后直接x自增加一

5. const和宏定义的区别

const和define都可以用来定义一个常量
define在预处理阶段对程序代码进行字面上的替换,没有数据类型,不会进行类型检查;const定义一个只读变量,有数据类型,有类型检查
由于define是做字面替换,有多少地方使用就替换多少次,宏常量在内存中有多个备份;const定义的变量存储在静态数据区,使用的时候去静态数据区调用,只有一个备份

6. strcpy和memcpy的区别

strcpy只用于字符串的复制,memcpy可以复制任意类型
strcpy在复制时不需要指定长度,遇到字符串结束符就结束,可能发生溢出,memcpy需要指定复制的长度

char* strcpy(char* dest, const char* src){
    if((src == NULL) || (dest == NULL)) // 判断是否有效
        return NULL;
    strdest = dest; // 保存dest首地址
    while(*src != 0){
        *dest = *src;
        dest++;
        src++;
    } // 逐个复制
    return strdest;
void* memcpy(void* dest, const void* src, size_t size){
    if((src == NULL) || (dest == NULL)) // 判断是否有效
        return NULL;
    memdest = dest;
    while(size--){
        *dest = *src;
        dest++;
        src++;
    }
    return memdest;
}

7. new和malloc的区别

new是c++的关键字,malloc是c的库函数
new对应的是delete,malloc对应的是free
在动态分配内存的时候,new不需要指定具体分配内存的大小,编译器会自动根据类型计算,malloc需要显式指出分配内存大小
new返回的是对应类型的指针,malloc返回的是void*
new分配失败会抛出异常,malloc分配失败返回NULL
new可以自动初始化,malloc需要用户另外初始化

8. 构造函数和析构函数可不可以是虚函数

构造函数不能是虚函数
虚函数调用需要用到虚函数指针,虚函数指针存放在对象的内存空间中,如果将构造函数声明为虚函数,则对象还没创建时,没有虚函数指针,无法调用虚函数,无法调用构造函数

析构函数可以是虚函数
当存在基类和派生类的时候,最好将析构函数声明为虚函数。如果有一个基类指针指向一个派生类对象,并且析构函数不是虚函数,当delete基类指针时,就会调用基类的析构函数,派生类的新部分不会被释放;如果将析构函数声明为虚函数,当delete基类指针时,就会调用派生类的析构函数,释放派生类对象的所有空间

9. 如何限制一个类只能在堆/栈上分配空间

只能在堆上分配空间:只能用new分配,不能直接调用类的构造函数
将构造函数和析构函数设置为protected,然后写新的public的函数来构造和析构,用来构造的函数通过new来构造对象,用来析构的函数通过delete来销毁对象

class A{
protected:
    A(){}
    ~A(){}
public:
    static A* create(){
        return new A();
    }
    void destroy(){
        delete this;
    }
};

只能在栈上分配空间:不能用new分配,将operator new()设置成私有

class A{
public:
    A(){}
    ~A(){}
private:
    A* operator new(size_t t){}
    void operator delete(void* ptr){}
};

10. 拷贝构造函数能够使用值传递

不能。如果使用值传递,在调用拷贝构造函数的时候,形参需要调用拷贝构造函数给实参传值,这样就会无限调用拷贝构造函数,无法完成拷贝

11. C++的内存分配

程序占用内存分为以下几个部分:
栈区:由编译器自动分配释放,存放运行时函数中的参数、局部变量、返回值、返回地址等,操作类似数据结构中的栈,后进先出
堆区:由用户通过new、malloc分配和delete、free释放,分配类似链表,可能会产生碎片,如果在堆上分配了空间忘了释放,会造成内存泄漏
全局区:存放全局变量、静态数据、const常量,分为未初始化区bss和已初始化区data
文字常量区:存放常量字符串
代码区:存放代码

12. 栈和堆的区别

管理方式不同:栈由编译器自动分配和释放,堆是程序员手动申请和释放
空间大小不同:栈比堆小很多
生长方向不同:栈往下长,朝着内存地址减小的方向,堆往上长,朝着内存地址增大的方向
是否产生碎片:栈后进先出,弹出一个元素前,上一个元素已经弹出,不会产生碎片
分配效率不同:栈的分配效率比堆高,因为栈有专门的结构存放栈顶地址,压栈出栈比较快,堆的分配要搜索能够使用的内存,比较慢

13. 动态内存分配

有两种数据结构存放内存使用情况:
空闲分区表:每个空闲分区对应一个表项,含有分区号、分区大小、分区起始地址等信息
空闲分区链:每个分区的起始和末尾有指向上一个和下一个空闲分区指针,起始部分还存有分区大小等信息

动态分配算法:
首次适应:从头到尾找到一个适合的分区,查找耗时,实现简单
最佳适应:优先使用更小的分区,保留更多的大分区块,满足大进程需求
最坏适应:优先使用更大的分区,减少难以利用的小碎片
邻近适应:从当前位置查找适合的分区,查找简单,实现简单

伙伴内存管理:
将空闲分区块根据大小分组,2^i大小的分为一组,每组形成一个链表,分配时直接到相应的链表查找有无空闲块
如果没有,就到更大的空闲块链表查找,找到后将其拆分,剩余的空闲块插入到相应大小的链表
释放的时候,如果相邻的位置有空闲块,就合并后插入到更大的内存块链表

14. struct的字节对齐

成员变量的起始地址为其长度的整数倍,结构体的总大小为其最大长度成员变量大小的整数倍
定义变量的顺序不一样,整个结构体的大小会不一样

struct A{
    char a;
    short b;
    int c;
};

sizeof(A) = 8

struct B{
    char a;
    int b;
    short c;
};

sizeof(B) = 12

15. 智能指针

智能指针能够自动释放它所指向的对象
有三种智能指针:unique_ptr、shared_ptr、weak_ptr

unique pointer独占它所指向的对象,shared pointer允许多个指针指向同一个对象,weak pointer是弱引用,指向shared pointer管理的对象,用来解决shared pointer中会出现的死锁现象

每一个shared pointer都有一个关联的计数器,统计它所指向对象的引用计数,当引用计数变为零的时候,就会释放所管理的对象内存

weak pointer不会影响shared pointer指向对象的引用计数,当引用计数为零的时候,不管有没有weak pointer指向对象,对象的内存都会被释放

class A;
class B;
class A{
public:
    shared_ptr pb;
};
class B{
public:
    shared_ptr pa;
};
void func(){
    shared_ptr pa(new A());
    shared_ptr pb(new B());
    pa->pb = pb;
    pb->pa = pa;
}

当函数func执行完毕跳出,pa、pb被释放,pa、pb指向对象的引用计数都减一变为1,所以A和B的具体对象并没有被释放,造成内存泄漏

class A;
class B;
class A{
public:
    weak_ptr pb;
};
class B{
public:
    shared_ptr pa;
};
void func(){
    shared_ptr pa(new A());
    shared_ptr pb(new B());
    pa->pb = pb;
    pb->pa = pa;
}

当函数体执行完毕前,A的引用计数是2,B是1,pa、pb被释放,A和B的引用计数都减一,B变为0,会被释放,B中的pa指针被释放,A对象的引用计数减一变为0,A被释放

16. 封装、继承、多态

是C++面向对象编程的特点

封装是将数据和操作数据的方法封装在一个类中,在类的内部实现对数据的操作
将复杂的操作实现放在类的内部,为外部提供接口,使得程序更容易被理解和分工
类的内部成员有不同的权限,能防止程序中无关部分修改成员,数据更加安全

继承是指派生类继承基类,派生类扩展基类的成员和方法,使得代码得到更好的复用

多态指一个函数存在多种实现方法
有两种,编译时多态和运行时多态
编译时多态指的是重载,就是一个类中的函数有相同的函数名,但是参数列表不同
运行时多态指的是重写,通过虚函数实现,在基类和派生类中有函数名称和参数列表都相同的函数,但是函数名前有virtual的关键字

17. 四种类型转换

static cast:用于风险小的类型转换,比如浮点数和整数的转换,派生类指针到基类指针的转换

const cast:用于去除常量属性

reinterpret cast:用于不同类型的指针之间的转换,以及能容纳得下指针的整数和指针之间的转换

dynamic cast:用于含有虚函数的类的向基类或向派生类的转换

18. 指针和引用的区别

指针存储一个内存地址,引用相当于变量的别名
系统会为指针分配内存,不会给引用分配内存
指针可以修改所存的地址,引用一和变量绑定就无法再绑定其他变量
引用必须初始化

19. 指针和数组的区别

指针保存地址,数组保存相同数据类型的数据
指针通过地址间接访问数据,数组直接访问数据
指针需要显式分配和释放内存,数组自动分配和释放
指针动态分配内存大小,数组在定义时就确定大小

20. 野指针是什么

指向非法内存地址的指针
非法内存:未分配的区域

21. 为什么不默认析构函数为虚函数

因为含有虚函数的类需要虚函数表,其对象需要虚函数指针,将不含虚函数的类的析构函数声明为虚函数需要额外的空间开销

22. 重载和重写的区别

重载:同一个类中,相同函数名的函数,参数列表不相同,在调用的时候根据参数列表选择调用函数

重写:基类和派生类中有函数名称和参数列表都相同的函数,函数声明为virtual,在运行的时候,根据具体的对象类型调用函数

23. 虚函数如何实现

通过虚函数表和虚函数指针来实现
每个类都有一个虚函数表,表中存放虚函数的入口地址
每个对象有一个虚函数指针,指向虚函数表
派生类会继承基类的虚函数表,当派生类重写了基类的方法时,派生类的虚函数表中相应的函数的入口地址会更改

24. 隐式类型转换

一种是用低精度变量给高精度变量赋值
一种是在含有单参数的构造函数的类,用相应的单参数给类的对象赋值

25. 函数指针

指向函数入口地址的指针
可以用来调用函数或者作为函数的参数

26. struct和class的区别

默认访问权限不同,struct是public,class是private
class可以用于声明模板,struct不可以

27. 内存泄漏和场景

内存泄漏指的是不再使用的内存没被释放

常见的发生场景
new分配后没有delete
没有把基类的析构函数声明为虚函数,一个基类指针指向派生类对象,delete基类指针时没有准确的释放派生类对象扩充的部分

28. inline函数和宏定义

inline在编译时将代码嵌入相应位置,宏定义预处理阶段简单替换
inline函数有语法词法分析,可以查出语法词法错误,宏定义没有
inline函数不会有歧义,宏定义可能产生歧义

你可能感兴趣的:(面试常见问题-c++)