八股文-C++语言部分
目录
- C语言和C++的区别
- 谈一谈函数重载和底层的原理
- 谈一下你对引用的理解
- 谈一谈你对内联函数的理解(深信服一面)
- 说一下struct和class的区别
- 谈一谈你对封装的理解
- 一个类的大小应该怎样计算, 成员函数放在哪儿了
- This指针存储在哪里, This指针可以为空吗?
- 在析构函数写 delete this会出现什么情况
- 请列举不能运算符重载的操作符
- 哪些成员必须放在初始化列表初始化?
- 讲讲explicit关键字的用处
- 谈一下new/delete 和 malloc/free 的区别
- 能聊一下new/delete的底层原理吗
- 什么是内存泄漏(腾讯一面)
- 什么是泛型编程, 给我讲一下模版的原理
- 为什么要有模版的特化, 特化是怎样实现的
- 手撕一个string类
- 谈一下vector的容量问题(腾讯一面)
- 谈一下vector的迭代器失效问题(迅雷一面)
- vector和list的区别
- 手撕一个链表
- 谈一谈你对继承的理解
- 继承基类成员的访问方式变化
- 说一下函数重载和函数隐藏的区别
- 谈一下派生类的默认成员函数的特性
- 怎样解决菱形继承的数据冗余和二义性问题?
- 继承和组合(is-a和has-a)的区别?哪些场景用哪个?
- 谈一谈你对多态的理解
- 谈一谈你对函数覆盖(重写)的理解
- override 和 final关键字的用处
- 什么是纯虚函数, 什么是抽象类
- 聊聊多态的原理
- 讲一讲运行时多态和编译时多态(动态多态和静态多态)
- 为什么在继承体系中最好把析构函数定义为虚函数?
- 二叉树的前中后序遍历的非递归版本
- 了解过高度平衡二叉树吗
- 了解过红黑树吗
- 什么是哈希冲突, 怎样解决
- 给40亿个不重复的整数, 如何快速判断一个数是否在这之中
- 给一个超过100G的logfile, log中存着IP地址, 设计算法找到出现次数最多的IP地址
- 给两个文件, 分别有100亿个请求, 只有1G内存, 如何找到两个文件交集?
- C++ 11 的新特性你都了解哪些?
- 什么是设计模式? 手撕一个单例模式?
- 了解过C++的强制类型转换吗?
C语言和C++的区别
A.C语言是面向过程的语言, 注重的是一步一步的调用函数解决问题, C++是面向对象的语言, 会将问题分解为若干个对象, 再围绕着对象去解决问题
B.C++的标准库比C语言的更强大, 引入了诸如STL库, 智能指针, 模版等新的内容, 使得程序员编写代码变得更简洁
谈一谈函数重载和底层的原理
A. 先谈什么是函数重载
C++允许在同一作用域中, 定义函数名相同但函数的形参列表(参数个数, 参数类型, 参数顺序)不同
B.再谈底层原理
编译阶段, 会根据函数名修饰规则, 就是将函数参数列表和函数名转换为函数符号, 在链接阶段再根据函数符号查找符号表找到函数地址, 程序运行时当需要调用某个函数时就可以直接通过这个函数地址来进行调用。
谈一下你对引用的理解
A.先谈宏观的定义
1.引用就是给一个已存在变量取别名, 编译器不会为引用变量开辟空间, 变量和它的引用共用同一份空间
2.引用必须在定义时初始化, 一个变量可以有多个引用, 但一旦引用一个实体后, 就不能再引用其他实体
B. 最经典的问题, 指针和引用的区别, 从两个角度回答问题
(1)语法使用角度:
1.引用没有额外开辟空间, 而指针变量会开辟4/8字节的空间
2.一旦引用一个实体, 就不能再引用另一个, 而指针可以指向任意同类型的实体
3.引用不存在空引用, 但是指针存在空指针. (引用的内容可以指向空, 但内容本身不能为空)
4.引用的类型大小是被引用的变量的大小, 而指针的大小就是固定的4/8字节
(2)底层实现角度:
我查看过引用和指针的汇编代码, 发现他们其实是一样的, 也就是说引用的底层是用指针实现的
谈一谈你对内联函数的理解(深信服一面)
用inline修饰的函数叫做内联函数, 在编译时会将调用内联函数的地方展开, 没有调用函数时的栈帧开销, 可以提高程序运行的效率, 内联函数只是给编译器的一个建议, 不是所有inline修饰的函数都会被展开
说一下struct和class的区别
1.C++把struct升级成为了类, struct的访问权限默认为public, 而class的访问权限默认为private.
2.在继承中, struct的默认继承方式是public, 而class的默认继承方式是private
3.在纯C语言中, struct只是数据的集合, 而在C++中, class是数据和方法的集合
谈一谈你对封装的理解
封装就是将数据和处理数据的方法统一进行整合, 并且将对象的属性和实现细节怪给隐藏了起来, 对外开放部分接口来和对象进行交互
一个类的大小应该怎样计算, 成员函数放在哪儿了
A. 一个类的大小是所有成员的大小之和(按照内存对齐规则来算)
空类占一个字节
B.成员函数放在了代码段, 在编译链接时会根据函数名去代码段找到函数的地址
This指针存储在哪里, This指针可以为空吗?
A.This指针是形参, 形参被存放在栈上, 也可能放在寄存器里
B.This指针可以为空, 只要保证在函数内部不会使用到this指针, 如果使用就是空指针解引用了
在析构函数写 delete this会出现什么情况
delete this会调用this的析构函数, 析构函数又会调用delete this, 会死循环
请列举不能运算符重载的操作符
一共五个
.* :: sizeof ?: .
哪些成员必须放在初始化列表初始化?
三种成员: const 成员, 引用成员, 没有默认构造函数自定义类型成员
特别的是, 初始化列表不能初始化静态成员, 所以 static const int 类型的成员不能放在初始化列表中
讲讲explicit关键字的用处
A.先将背景
对于单个参数或除了第一个参数无默认值其余参数都有默认值的构造函数具有隐式类型转换的作用, 比如 string str = “abcdef”, 这里编译器会用"abcdef"字符串构建一个匿名对象, 再将这个对象给str赋值
B.再讲explicit的作用
在构造函数前面加上explicit关键字可以阻止这种转换发生
谈一下new/delete 和 malloc/free 的区别
(1)malloc和free是函数, 而new和delete是操作符
(2)malloc申请空间要显示传需要申请的大小, 而 new 只用传类型和个数
(3)free时只需要传入要释放的指针, 而delete可能要加上[]
(4)malloc失败后返回NULL, 而2new 失败后会抛异常
(5)malloc返回值是void* 使用时要强转, 而new不用
(6)malloc申请的空间既不会初始化也不会调用自定义类型的构造函数, 而new申请空间布局会初始化, 还会调用自定义类型的构造函数
(7)free释放空间不会调用自定义类型的析构函数, 而delete释放空间会自动调用自定义类型的析构函数
能聊一下new/delete的底层原理吗
new底层调用operator new全局函数来申请空间
delete底层调用operator delete全局函数来释放空间
而operator new底层其实是调用malloc来申请空间
operator delete底层其实是调用free来释放空间
什么是内存泄漏(腾讯一面)
内存泄漏是指: 因为错误或者疏忽导致对程序没有释放已经不再使用的资源, 从而失去对这份资源的控制, 使得可用的资源越来越少, 比方说我知道的内存泄漏对象有: 堆空间, 套接字, 文件描述符
什么是泛型编程, 给我讲一下模版的原理
A.泛型编程就是编写与类型无关的通用代码, 是代码复用的一种手段
B.模版将本应该我们重复做的事情交给了编译器, 在编译阶段, 当编译器推演出你想要处理的变量的具体类型时, 比如说double类型, 编译器会专门生成一份处理double类型的代码
为什么要有模版的特化, 特化是怎样实现的
A.因为在特定场景下, 一些特殊的类型可能会得到错误的结果, 此时需要在原模版的基础上, 针对特殊类型做特化的处理方式
B.先说函数模版的特化
1.必须要先有一个基础的函数模版
2.再写上关键template后面加上<>
3.函数名后跟一堆尖括号<>, 并在尖括号内指定要特化的类型
4.函数形参个数要和基础模版相同, 参数类型写指定的类型
C.再说类模版的特化
类模版特化分为全特化和偏特化, 全特化就是将模版的所有参数都确定化, 其形式和函数模版的特化类似, 而偏特化又分为两种表现形式, 部分特化和对参数的进一步限制, 学长你看看需要细聊哪一点呢?
D.偏特化的细节
部分特化: 将模版参数列表中的参数一部分特化, 其实现方式和全特化类似
对参数的进一步限制: 即对模版参数做进一步条件限制, 比如将两个参数偏特化为指针类型, 或将两个参数偏特化为引用类型
手撕一个string类
总之是代码可以通过编译, 代码简单, 容易实现即可, char* str, int size, int capacity
先实现无参构造, 带参构造, 拷贝构造, 析构函数, operator=, operator[]
如果写的比较快可以加上C++11的移动构造和移动赋值
谈一下vector的容量问题(腾讯一面)
A.vector是如何进行扩容的
当向vector中插入元素时, 有效元素个数size和空间容量大小capacity相同时, vector会触发扩容知己, 会开辟新空间, 拷贝元素, 再释放旧空间
B.扩容会导致效率低下, 如何避免动态扩容
如果在插入元素前, 可以预估vector中存储元素的个数, 可使用reserve函数提前把底层的空间开辟好
C.Windows下和linux下的扩容机制有和不同
VS下选择1.5倍的方式扩容, g++下选择2倍的方式扩容
谈一下vector的迭代器失效问题(迅雷一面)
迭代器失效, 实际就是迭代器底层对应指针所指向的空间失效了, 如果继续使用已经失效的迭代器, 程序可能会崩溃
可能引起迭代器失效的操作:
(1)引起底层空间改变都有可能让迭代器失效, resize, reserve, insert, push_back等
(2)erase操作:删除一个元素后, 后面的元素会向前移动, 这不会有问题, 但若删除的是最后一个元素, pos指向end位置, end位置没有元素. 因此vs就认为删除vector上的任意元素时都算迭代器失效
如何避免出现迭代器失效问题:
在insert/erase一个pos位置后, 默认pos已经失效, 不要再访问它, 或在使用该迭代器前重新赋值
vector和list的区别
A.vector是顺序容器, 当底层的size等于capacity时会进行扩容, 开辟一份新空间并且拷贝之前的数据, list是双向链表不会有扩容的问题
B.vector的底层是连续的空间, 而list的底层是不连续的空间
C.vector能使用随机迭代器高效的访问数据, 而list只能用双向迭代器来访问数据
D.vector的插入和删除可能会移动或拷贝数据, 效率较低, 而list插入和删除数据效率较高
手撕一个链表
A.首先先封装一个ListNode类, 写上构造函数, 不要写模版
B.再封装一个迭代器类, 要实现构造函数, operator*, operator->, operator!=, operator++和operator–, 暂时只写非const的迭代器
C.List类要写无参构造, 析构函数, begin和end函数, insert和erase函数, 再复用insert写一个push_back, 再用push_back写一个拷贝构造
谈一谈你对继承的理解
继承是代码复用的一种手段, 它允许在基于已存在的类上做拓展, 增加新的功能, 体现除了面向对象程序设计的一种层次结构, 继承还是实现多态的基础, 是很重要的手段. C++中的一些设计模式都是基于继承和多态的.
继承基类成员的访问方式变化
public继承: 继承成员的原先访问属性不变
protected继承: 基类的public成员变成protected成员, 其余不变
private继承: 基类的所有成员都是private
基类的private成员在子类中不可见
说一下函数重载和函数隐藏的区别
构成函数重载必须在同一作用域中, 有同名函数
而函数隐藏则是在继承体系中, 基类和子类中有同名函数, 这两个函数就构成隐藏
谈一下派生类的默认成员函数的特性
A.创建对象时先构造父类再构造子类, 销毁时先析构子类再构造父类
B.拷贝构造和operator=都必须去调用在基类对应的函数(指定类域调用)
C.子类的析构和父类的析构, 构成隐藏, 子类析构不用显示调用父类析构
怎样解决菱形继承的数据冗余和二义性问题?
A.数据的二义性问题可以通过制定腰部类的类域来解决, 但这种方法不能解决数据冗余
B.虚拟继承可以解决这两个问题, 在腰部类继承时加上关键字virtual, 虚拟继承改变了腰部类的结构, 基类现在就只有一份了, 同时属于两个腰部类, 腰部类中会多出一个虚基表指针, 指向虚基表, 虚基表中存储了腰部类到基类的偏移量
继承和组合(is-a和has-a)的区别?哪些场景用哪个?
A.继承是一种is-a的关系, 每一个派生类对象都是一个基类对象, 父类和子类有很强的耦合度
B.组合是一种has-a的关系, 假设B组合了A, 那么B对象内都有一个A对象, 组合类之间没有很强的关联性, 耦合度低, 应该优先使用组合而不是继承
谈一谈你对多态的理解
A.先谈多态的定义:
多态就是多种形态, 是指在继承体系中, 不同对象去调用同一个函数会出现不一样的结果
B.再谈满足多态的条件:
1.必须是在继承体系中
2.必须通过基类的指针或引用调用虚函数
3.被调用的函数必须是虚函数, 且派生类必须对基类的虚函数进行重写
谈一谈你对函数覆盖(重写)的理解
若派生类中有一个和基类完全相同的虚函数(返回值, 函数名, 参数列表都相同), 则称子类的虚函数覆盖(重写)了基类的虚函数, 函数覆盖有两个特例:
1.子类虚函数不加virtual也构成覆盖
2.返回值可以不同, 基类虚函数可返回基类对象的指针或引用, 子类虚函数可返回子类对象的指针或引用(协变), 这个返回的基类和子类可以是其他继承体系中的.
函数重载, 隐藏和覆盖的区别?
覆盖是针对继承体系中的虚函数的,父子类中有同名函数并且不构成覆盖, 那么它们就构成隐藏.
对于函数重载, 是在同一作用域中出现同名函数, 在继承中的父子类不是在同一作用域
override 和 final关键字的用处
在一个基类的虚函数后面加上final, 代表此虚函数不能再被重写, 在一个基类后面加上final, 此类无法被继承
在基类虚函数后加上override, 会强制检查子类有没有重写此虚函数, 如果没有重写就报错
什么是纯虚函数, 什么是抽象类
在虚函数后面写上=0, 这个虚函数就叫纯虚函数, 而包含纯虚函数的类叫做抽象类
抽象类无法实例化出对象, 派生类继承抽象类之后, 必须重写虚函数才能实例化出对象
聊聊多态的原理
A.有虚函数的类对象中, 有一个虚函数表指针, 这个表中存放的虚函数的地址, 当子类没有重写父类虚函数时, 父子类的虚函数表中的虚函数的地址是一样的, 而当子类重写了父类的虚函数后, 子类会用新的地址覆盖父类虚函数的地址
B.调用虚函数时, 指针指向子类对象就调用子类虚表中的虚函数, 指向父类就调用父类虚函数表中的虚函数
讲一讲运行时多态和编译时多态(动态多态和静态多态)
运行时多态: 是通过虚函数和继承来实现的, 是指在程序运行时去到对象中虚函数表指针指向的虚函数表中找函数地址, 运行时才知道调用谁
编译时多态: 通过函数重载和运算符重载实现, 是指程序在编译阶段根据函数名, 来确定调用函数的地址, 编译时就知道调用谁了
为什么在继承体系中最好把析构函数定义为虚函数?
A.若不写为虚函数: 父类指针指向的是子类对象时, 析构函数就是普通的函数调用, 是编译时决定, 指针是父类的就会调用父类的析构函数, 就可能会有问题.
B.将析构写成虚函数后: 由于父子类的析构函数都会被解释为Destructer, 所以一旦写成虚函数就构成函数重写, 析构时是运行时决定, 指针指向父类就调用父类的析构, 指向子类就调用子类的析构
二叉树的前中后序遍历的非递归版本
了解过高度平衡二叉树吗
A.高度平衡二叉搜索树就是AVL树, 它是为了解决普通二叉树可能是单枝树从而导致查找效率低下的问题
B.AVL树的性质是当向AVL树种插入一个元素后, 要保证每个节点的左右子树高度不超过1, 若违反规则会旋转整个树重新达到平衡.
了解过红黑树吗
A.红黑树简介: 红黑树是一颗二叉搜索树, 通过对任意一条从根到叶子的路径上的各个节点的着色限制, 达到最长路径不超过最短路径的二倍
B.红黑树的性质:
(1)每个节点不是红色就是黑色
(2)根节点是黑色
(3)红色节点的两个孩子节点一定是黑色
(4)从一个节点到其任意后代叶子节点的路径上, 包含相同数量的黑色节点
(5)叶子节点是黑色(空节点是黑色)
什么是哈希冲突, 怎样解决
A.哈希冲突就是两个不同的关键字通过同一个哈希函数计算出相同的结果, 从而映射到哈希结构的同一个为止
B.通常使用闭散列(开放地址法)或开散列(链地址法)解决哈希冲突:
(1)闭散列: 发生哈希冲突时,若哈希表没有被填满就往后找空闲的位置放入(线性探测)
(2)开散列:通过哈希函数算出相同结果的数据放在同一个子集中, 每个子集称为桶, 各个桶中的元素通过单链表连接起来,链表的头节点放在哈希数组中
(3)闭散列的缺陷:同关键码可能占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低,应该使用链地址法
给40亿个不重复的整数, 如何快速判断一个数是否在这之中
使用位图, 40亿个整数只需要40亿个bit为来存储,也就是5亿字节=500MB的空间
给一个超过100G的logfile, log中存着IP地址, 设计算法找到出现次数最多的IP地址
使用哈希切分,读取每一个ip,利用哈希函数将所有的ip放入不同的小文件中, 相同的ip一定进入了相同的小文件,再使用unordered_map<string,int>对每个小文件进行统计
给两个文件, 分别有100亿个请求, 只有1G内存, 如何找到两个文件交集?
使用相同的哈希函数将A,B文件都拆分为1000份小文件,再使用两个set去编号相同的Ai和Bi的小文件找交集
C++ 11 的新特性你都了解哪些?
A.先说自己会的东西:
自动类型推导,范围for,智能指针,右值引l用,lambda表达式,线程库等
B.再问问需要细谈哪个? 下面把长问的都列举出来
C.什么是右值, 什么是左值, 怎样区分?
左值就是可以出现在等号左边的值,如变量名,而右值是只能出现在等号右边的值,比如字面常量,表达式返回值等,是将亡值.区别左值和右值最简单的方式就是是否可以取地址,能取地址的就是左值,不能取地址的就是右值
D.右值引用的使用场景?有什么用?
在传函数参数时,如果传递的是右值,右值本身就是将亡值,再进行深拷贝代价太大了,用右值引用进行移动拷贝,直接把将亡值拿过来.在容器中可以实现右值引用版本的移动构造和移动赋值函数,可以大大的增加效率
E.了解过万能引用和完美转发吗?
模板中的&&是万能引用,它既可以引用左值也可以引用右值。但在后续的使用中,都会退化为左值,想要解决这个问题需要使用forward完美转发在传参过程中保留原有属性
F.谈一下auto_ptr, unique_ptr, shared_ptr, weak_ptr?
(1)auto_ptr用p2拷贝构造p1后,p1(p2),p2就被置空了
(2)unique_ptr,既然auto_ptr拷贝有问题,那我就无法进行拷贝构造
(3)shared_ptr,采用引l用计数的方式,这是最正确的版本
(4)Weak_ptr是设计出来专门解决shared_ptr的循环引l用的问题的
G什么是shared_ptr的循环引用问题?
两个或多个shared_ptr对象相互持有对方的引l用,形成一个闭环,导致它们之间的引l用计数永远无法归零,从而造成内存泄漏。
手撕一个shared_ptr?
先不考虑定制删除器,成员有:T*_ptr,int*_count,mutex*_pmtx.++和–要加锁.
成员变量都在堆上申请空间,析构时都要delete
什么是设计模式? 手撕一个单例模式?
A.设计模式就是一套被大家反复使用的代码的经验总结
B.了解单例模式,一个类只能实例化出一个对象就是单例模式
C.手撕单例模式,饿汉模式:程序启动就创建对象,懒汉模式:使用时才创建(需要加锁)
了解过C++的强制类型转换吗?
C++的类型转换有四个,static_cast,dynamic_cast,reinterpret_cast,const_cast
(1)static_cast:用于两个相关的类型进行转换,不能用于父子类
(2)dynamic_cast:将父类对象的指针或引用转换为子类对象的指针或引l用(只能用于父类有虚函数的类)
(3)reinterpret_cast:用于两种不相干的类型之间的转换
(4)const_cast:用于删除变量的const 属性