C++面试题目汇总 基础知识
- 基本语言
- 说一下static关键字的作用
- C++和C的区别
- c++中四种cast转换
- C/C++ 中指针和引用的区别
- 给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内,给出思路并手写代码
- c++中的smart pointer四个智能指针
- 怎么判断一个数是`2`的倍数, 怎么求一个数中有几个`1`,说一下思路并手写代码
- 数组和指针的区别
- 野指针是什么?
- 智慧指针内存泄漏的情况
- 为什么析构函数必须是虚函数? 为什么C++默认的析构函数不是虚函数
- 说一下`fork`函数
- `C++` 中的析构函数的作用
- 静态函数与虚函数的区别
- 重载和覆盖
- 说一下`strcpy`和`strlen`
- 理解的虚函数和多态
- `++i`和`i++`的区别
- `++i`和`i++`的实现
- 在main()之前执行前运行
- 修改一个字符使得代码输出`20`个`hello`
- 说一下`shared_ptr`的实现
- 字面值常量和左右值
- `C++`怎么定义常量,以及常量所在的位置
- `const` 修饰成员函数的目的
- 同时定义两个同名同参数的函数, 区别仅在于是否带`const`, 会出问题么?
- 说一下隐式变换
- `C++`函数栈空间的最大值
- 说一下`extern "C"`
- `new/delete` 和 `malloc/free`的区别
- `RTTI`
- 虚函数表具体怎么实现的运行时多态
- `C语言`是怎么进行函数调用的?
- `C语言`参数入栈顺序
- `C++`如何处理返回值
- `C++`中拷贝赋值函数的形参能否使用值传递
- `malloc`和`new`的区别
- 说一下`select`
- `fork`,`wait`,`exec`函数
- 请你回答一下静态函数和虚函数的区别
- 请你说一说重载和覆盖
- 容器和算法
基本语言
说一下static关键字的作用
-
全局静态变量
- 在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
- 内存中的位置:静态存储区,在整个程序运行期间一直存在。
- 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
- 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
-
局部静态变量
- 在局部变量之前加上关键字
static
,局部变量就成为一个局部静态变量。 - 内存中的位置:静态存储区
- 初始化:未经初始化的全局静态变量会被自动初始化为0(对于自动对象,如果没有显示初始化,会调用零参数构造函数,如不存在则编译失败);
- 作用域:作用域仍为局部作用域,
- 当定义它的函数或者语句块结束的时候,作用域结束。
- 但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
- 在局部变量之前加上关键字
-
静态函数
- 在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
- 函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
- warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
-
类的静态成员
- 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。
- 因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
-
类的静态函数
- 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
- 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
- 不能被virtual修饰,静态成员函数没有this 指针,虚函数的实现是为每一个对象分配一个vptr 指针,而vptr 是通过this 指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
C++和C的区别
- 设计思想上:
- C++是面向对象的语言,而C是面向过程的结构化编程语言
- 语法上:
- C++具有封装、继承和多态三种特性
- C++相比C,增加多许多类型安全的功能,比如强制类型转换、
- C++支持范式编程,比如模板类、函数模板等
c++中四种cast转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
- 1、const_cast
- 用于将const变量转为非const, (并不能实现类型转换)
- 2、static_cast
- 用于各种隐式转换,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;(如果派生类继承于两个父类,则使用此转换为第二个父类对象的时候会有指针问题)
- 3、dynamic_cast
- 用于动态类型转换。
- 只能用于含有虚函数的类,用于类层次间的向上和向下转化。
- 只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。
- 要深入了解内部转换的原理。1
- 向上转换:指的是子类向基类的转换
- 向下转换:指的是基类向子类的转换
- 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
- 用于动态类型转换。
- 4、reinterpret_cast
- 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
- 5、为什么不使用C的强制转换?
- C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
C/C++ 中指针和引用的区别
- 1.指针有自己的一块空间,而引用只是一个别名;2
- 2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
- 3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
- 4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
- 5.可以有const指针,但是没有const引用;
- 6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
- 7.指针可以有多级指针(**p),而引用至于一级;
- 8.指针和引用使用++运算符的意义不一样;
- 9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
对象通过指针获取申请的堆上内存,指针是指向动态内存区域的唯一方式,而引用实质是对象的一个别名,对象被析构之后,引用将会失效,所以可能会使得堆上的内存空间没有及时释放,造成内存泄露。
给定三角形ABC和一点P(x,y,z),判断点P是否在ABC内,给出思路并手写代码
- 根据面积法,如果P在三角形ABC内,那么三角形ABP的面积+三角形BCP的面积+三角形ACP的面积应该等于三角形ABC的面积。算法如下:
- 三角形求面积的原理为: 向量的外积表示由此两个向量构成的四边形的面积 S = a x b
#include <iostream> #include <math.h> using namespace std; #define ABS_FLOAT_0 0.0001 struct point_float { float x; float y; }; /** @brief 计算三角形面积 **/ float GetTriangleSquar(const point_float pt0, const point_float pt1, const point_float pt2) { point_float AB, BC; AB.x = pt1.x - pt0.x; AB.y = pt1.y - pt0.y; BC.x = pt2.x - pt1.x; BC.y = pt2.y - pt1.y; return fabs((AB.x * BC.y - AB.y * BC.x)) / 2.0f; } /** @brief 判断给定一点是否在三角形内或边上 **/ bool IsInTriangle(const point_float A, const point_float B, const point_float C, const point_float D) { float SABC, SADB, SBDC, SADC; SABC = GetTriangleSquar(A, B, C); SADB = GetTriangleSquar(A, D, B); SBDC = GetTriangleSquar(B, D, C); SADC = GetTriangleSquar(A, D, C); float SumSuqar = SADB + SBDC + SADC; if ((-ABS_FLOAT_0 < (SABC - SumSuqar)) && ((SABC - SumSuqar) < ABS_FLOAT_0)) { return true; } else { return false; } }
c++中的smart pointer四个智能指针
- 为什么要使用智慧指针
- 智慧指针的作用是管理指针,因为在堆上申请的内存空间,是需要手动释放的,如果未释放就会造成内存泄漏,而智慧指针的使用可以很大程度的避免这个问题.
- 因为使用智慧指针管理内存的本质是: 栈对象管理堆内存, 而栈对象是超出作用域或者程序意外终止,都会自动调用析构函数,而智慧指针的析构函数会自动释放资源,从而避免内存泄漏.
auto_ptr
: 采用所有权模式, 但是其允许所有权剥离,所以有内存崩溃的风险, 在c++11
中已经放弃了unique_ptr
:采用独占式拥有,保证同一时间只有一个智慧指针可以指向该对象.unique_ptr
不允许所有权剥离,除非本身是临时变量或者使用move指令.shared_ptr
:采用共享式拥有,多个共享指针可以指向相同的对象,该对象和其相关的资源会在最后一个共享指针
被销毁时释放. 注意shared_ptr
有交叉引用相互锁死的问题,即两个对象相互持有对方的共享指针, 造成用不释放的问题weak_ptr
: 是一种不控制对象生命周期的智慧指针,他是共享指针的附属品, 主要是为了避免shared_ptr
的死锁问题,具体表现为weak_ptr
只能从shared_ptr
或另一个weak_ptr
构造,持有它不会造成shared_ptr
的引用计数增加,以及并不能通过weak_ptr
直接访问推向.
怎么判断一个数是2
的倍数, 怎么求一个数中有几个1
,说一下思路并手写代码
- 判断一个数是不是
2
的倍数,可以判断该数的二进制末位是不是0;a%2==0
或者a & 1 ==0
- 求一个数中
1
的个数,可以直接除10取余
- C/C++ 各种进制的表示方法/ 进制前缀
- C/C++ 各种进制的表示办法:
- 二进制:
0b
例:int x = 0b1001; // x = 9
- 八进制:
0
例:int y = 074; // x = 60
- 十六进制:
0x
例:int z = 0xa3; // x = 163;
- 二进制:
数组和指针的区别
- 参考:数组和指针的区别与联系(详细)
- 概念:
- 数组:数组用于储存多个相同类型数据的集合。
- 指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。
- 区别:
-
赋值: 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
-
储存方式:
- 数组:数组在内存中开辟一块连续的内存空间。可以根据下标进行访问, 它不在静态区,就在栈上
- 指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
-
求
sizeof
:sizeof(数组名)
: 数组所占存储空间的内存sizeof(指针名)
:在32位平台
都是4
(无论指针的类型), 在64位平台
都是8
(无论指针的类型)。
-
数组传参时,会退化为指针(首元素的地址), 而不会进行
copy
, 如果是二维数组,需要指定最后一个的维度
-
指针 | 数组 |
---|---|
保存数据的地址 | 保存数据 |
间接访问数据,首先获得指针的内容,然后将其作为地址,再从该地址获取数据 | 直接访问数据 |
通常用于动态数据结构 | 通常用于固定数目且数据类型相同的元素 |
通过Malloc 分配内存,free 释放内存 | 隐式的分配和删除 |
通常指向匿名数据,操作匿名函数 | 自身即为数据名 |
野指针是什么?
- 野指针就是一个指向已删除的对象 或者 为申请访问受限内存区域的指针
智慧指针内存泄漏的情况
- 当两个对象使用
shared_ptr
相互引用,就会使计数器失效,从而导致内存泄漏,解决方案是使用weak_ptr
打破循环引用
为什么析构函数必须是虚函数? 为什么C++默认的析构函数不是虚函数
- 将可能被继承的基类的析构函数设置为虚函数,可以保证当我们使用基类指针指向派生类对象时,依旧可以正常的
delete
, 不至于引起内存泄漏. - 在类中定义虚函数会增加额外的开销,包括虚函数表和虚表指针,对于非基类而言,构造函数如果设置为虚函数会浪费内存.
说一下fork
函数
Fork
函数: 创建一个和当前进程一样的进程Fork
调用- 成功调用
fork
后会创建一个和当前进程几乎一模一样的新进程,两个进程都会继续运行. - 在子进程中
fork
返回值为0, 在父进程中fork
返回值为子进程pid
. 如果出现错误会返回负值. - 现代系统通常采用写时复制, 如果子父进程的数据未发生修改不会立即复制
- 成功调用
Fork
常见用法- 用
Fork
创建一个进程之后,然后使用exec
载入一个二进制映像,替换掉当前进程的映像. 这种情况下,派生的子进程,会执行一个新的二进制可执行文件的映像.
- 用
- 了解一下
exec
函数族 - linux c语言 fork() 和 exec 函数的简介和用法
C++
中的析构函数的作用
- 析构函数名与类名相同,只是在函数名前增加了取反符号
~
以区别于构造函数,其不带任何参数, 也没有返回值. 也不允许重载. - 析构函数与构造函数的相反, 当对象生命周期结束的时候,如对象所在函数被调用完毕时,析构函数负责结束对象的生命周期. 注意如果类对象中分配了堆内存一定要在析构函数中进行释放.
- 和拷贝构造函数类似,如果用户未定义析构函数, 编译器并不是一定会自动合成析构函数, 只有在成员变量或则基类拥有析构函数的情况下它才会自动合成析构函数.
- 如果成员变量或则基类拥有析构函数, 则编译器一定会合成析构函数, 负责调用成员变量或则基类的析构函数, 此时如果用户提供了析构函数,则编译器会在用户析构函数之后添加上述代码.
- 类析构的顺序为: 派生类析构函数, 对象成员析构函数, 基类析构函数.
静态函数与虚函数的区别
- 静态函数在编译的时候已经确定运行时机了, 而虚函数则时运行时动态绑定的, 因为虚函数使用虚函数表机制进行映射,它只能是运行时确定的. 注意只有通过指针调用虚函数,虚函数表映射机制才会生效, 通过对象进行调用是不会生效的
重载和覆盖
- C++中重载、重写(覆盖)和隐藏的区别
- 重载: 同一可访问区域内声明了多个具有不同参数列表的同名函数, 根据参数列表确定那个函数被调用, 重载不关心函数返回类型.
- 覆盖/重写: 是指派生类中重新定义了基类中的
virtual
函数. 其函数名,参数列表,返回值类型,所有都与基类中被重写的函数一致。派生类对象通过派生类指针或则基类指针调用时都会调用派生类的重写函数。 - 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,只要函数名相同,基类函数都会被隐藏. 不管参数列表是否相同。
- 重载和重写的区别:
- 范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
- 参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
virtual
区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
- 隐藏和重写,重载的区别:
- 与重载范围不同:隐藏函数和被隐藏函数在不同类中。
- 参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;
说一下strcpy
和strlen
strcpy
是字符串拷贝函数,char* strcpy(char* dest, const char* src);
src
和dest
所指内存区域不可以重叠且dest
必须预分配足够的空间。- 从
src
逐字节的拷贝到dest
, 直到遇到\0
结束,因为没有指定长度,可能会导致拷贝越界, 造成缓冲区溢出漏洞,安全版本是strncpy
函数
char *strncpy(char *dest, const char *src, int n)
- 把
src
所指向的字符串中以src
地址开始的前n
个字节复制到dest
所指的数组中,并返回被复制后的dest
- 如果
src
的长度小于n
个字节,则用NULL
填充dest
直到满n
个字节。
- 把
strlen
函数计算字符串的长度的函数, 返回从开始到\0
之间的字符个数, 这个函数也有末尾没有\0
的风险
理解的虚函数和多态
- 多态的实现主要分为动态多态和静态多态, 静态多态就是重载, 在编译的时候就已经确定了, 动态多态是用虚函数机制实现的, 在运行期间动态绑定. 例如如果父类中定义了虚函数,而派生类重写了此函数,此时通过一个指向派生类对象的父类指针调用此函数式, 子类中重写的函数将被调用.
- 虚函数的实现: 在存在虚函数的类中会存在一个指向虚函数表的指针, 虚函数表中存放了虚函数的地址,当子类继承父类时也会继承其虚函数表, 当子类重写父类的虚函数时, 会将其继承的虚函数表中的地址替换为重写的函数地址. 使用虚函数,会增加访问内存的开销, 降低效率.
++i
和i++
的区别
++i
先自增1, 再返回. 函数定义式无形参, 实现过程中不会有临时变量生成,i++
先返回i
,再增加1. 函数定义式有一形参, 实现过程中由于需要返回自增前的量,所以需要一个临时变量.
++i
和i++
的实现
// ++i
int & int::operator++(){
*this += 1;
return *this;
}
// i++
int & int::operator++(int){
int t = *this;
++ *this;
return t;
}
在main()之前执行前运行
GCC
编译器// 在main之前 __attribute((constructor)) void before_main(){ printf("befor\n"); } // 在main之后 __attribute((deconstructor)) void after_main(){ printf("befor\n"); }
修改一个字符使得代码输出20
个hello
- 原代码如下:
for(int i=0; i< 20; i--){ cout <<"hello" <<endl; }
- 修改如下:
for(int i=0; i+20; i--){ cout <<"hello" <<endl; }
- !!! 强制类型转换 !!!
- 常值转
bool
的基本规则为!=0
, - 指针转
bool
的基本围着为!=nullptr
说一下shared_ptr
的实现
- 内存管理函数:
构造函数
,拷贝构造函数
,赋值操作
,析构函数
,取计数器
- 基本功能函数:
取原始指针
,成员访问操作
,取对象操作
,加n操作
,相减操作
// 自己写的
template<typename T>
class shared_ptr{
private:
T *ptr;
long *use_count;
public:
shared_ptr(T* p);
shared_ptr(const shared_ptr<T> & orig);
~shared_ptr();
shared_ptr<T>& operator=(const shared_ptr<T> &orig);
T operator*();
T* operator->();
T* operator+(int i);
unsigned int operator-(shared_ptr<T> &t1, shared_ptr<T> &t2);
long getcount();
}
template<typename T>
shared_ptr<T>::shared_ptr(T& *p){
ptr = p;
try{
use_count = new long(1);
}catch(...){
delete ptr;
ptr = nullptr;
p=nullptr;
use_count = nullptr;
}
}
template<typename T>
shared_ptr<T>::shared_ptr(const shared_ptr<T> &orig){
use_count = orig->use_count;
ptr = orig->ptr;
++(*use_count);
}
template<typename T>
shared_ptr<T>& shared_ptr<T>::operator=(const shared_ptr<T> &orig){
if(--(*use_count)==0){
delete ptr;
ptr = nullptr
delete use_count;
use_count = nullptr;
}
++(*orig->use_count);
ptr = orig->ptr;
use_count = orig->use_count;
return *this;
}
template<typename T>
shared_ptr<T>::~shared_ptr(){
if(--(*use_count)==0){
delete ptr;
ptr = nullptr;
delete use_count;
use_count = nullptr;
}
}
template<typename T>
T shared_ptr<T>::operator*(){ return *ptr;}
template<typename T>
T* shared_ptr<T>::operator->(){ return ptr;}
template<typename T>
T* shared_ptr<T>::operator+(int i){ return ptr + i;}
template<typename T>
unsigned int shared_ptr<T>::operator-(shared_ptr<T> &t1, shared_ptr<T> &t2){ return t1->ptr - t2->ptr;}
template<typename T>
long shared_ptr<T>::getcount(){return *use_count;}
字面值常量和左右值
- 简述四行代码的区别
const char* arr = "123"; // "123" 为字符串类型字面值, 其储存于常量区, 其值不可修改, arr表示一个指向字符串类型的const指针, 如果试图通过此指针修改字符串的指,编译器会组织这一行为,导致编译失败 char * brr = "123"; // "123" 为字符串类型字面值, 其储存于常量区, 其值不可修改, brr表示一个指向字符串类型的指针, 这里潜在的逻辑错误, 当我们试图通过brr修改所指向的字符串时, 编译器并不会阻止这一行为, 可编译通过, 但是运行时候,可能发生 DEADLYSIGNAL(致命错误) const char crr[] = "123" // 声明了一个以"123"为初值的常量数组, 通常情况下应该是存在于栈区, 但是使用了const修饰,编译器可能会将其放在常量区 char drr[] = "123"; // 声明了一个以"123"为初值的字符串数组, 应该是储存于栈区, 可通过drr对数组进行修改
- C/C++的四大内存分区和常量的存储位置
std::forward
的作用: C++完美转发为什么必须要有std::forward- 左值右值的区别: C++中的左值与右值(二)
C++
怎么定义常量,以及常量所在的位置
- 常量的定位方式: 通过使用
Top-level const
修饰对象类型从而定义的变量, 常量类型必须进行初始化.- 对于局部变量, 常量储存于栈区,
- 对于全局变量, 常量储存于静态储存区
- 对于字面值, 常量存放在常量储存区.
- 关于
Top-Level const
和low-level const
的分别 - c++ top-level const and low-level const
- c++ 顶层(top-level)const 和底层(low-level)const
摘自英文版
Primer C++
2.4.3.Top-Level const
- As we’ve seen, a pointer is an object that can point to a different object. As a result, we can talk independently about whether a pointer is const and whether the objects to which it can point are const. We use the term top-level const to indicate that the pointer itself is a const. When a pointer can point to a const object, we refer to that const as a low-level const.
const
修饰成员函数的目的
const
修饰成员函数表示设计者承诺调用此函数不会对修改对象任何内容,const
成员函数内部获取到的this
带有const属性
,因此不能对对象成员进行修改,也不能调用非const函数
const
成员函数本身即可以由const对象
调用, 也可以由非const对象
调用- 事实上如果确认不会对对象做更改,就应该给函数加上
const
限定, 这样无论const
对象还是普通对象都可以调用该函数. - 注意, 尽管
const函数
成员函数的设计者承诺不修改任何对象成员,但是事实上他是可以通过const_cast
移除this指针
的const属性
,进而调用非const函数
或者修改对象成员.
同时定义两个同名同参数的函数, 区别仅在于是否带const
, 会出问题么?
- 不会出问题,相当于函数重载.
- 通过
const指针或对象
将调用const
修饰的函数 - 通过
非const指针或对象
将调用无const
修饰的函数
说一下隐式变换
- 首先, 对内置类型, 低精度变量会给高精度变量赋值时会发生隐式类型转换,
- 其次, 对于只存在单参数构造函数或除了一个参数之外, 其他所有参数都带缺省值的类, 函数调用可以直接传递该参数, 编译器会自动调用其构造函数生成临时变量.
- 对于刚说的这种对象的隐式构造, 可以通过增加
explicit
关键之阻止.
C++
函数栈空间的最大值
- 默认为
1M
, 可以调整
说一下extern "C"
C++
调用C函数需要extern C
, 因为C语言
没有函数重载- 由于
C++
支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中, 而不仅仅是函数名; - 而
C语言
并不支持函数重载,因此编译C语言
代码的函数时不会带上函数的参数类型,一般之包括函数名。
- 由于
new/delete
和 malloc/free
的区别
- 首先:
new/delete
是c++
关键字, 而malloc/free
是c语言
的库函数 - 其次:
malloc/free
需要指明申请的空间的大小, 且并不负责构造和析构new/delete
是在malloc/free
上增加了一层封装, 他们根据类的确定所需要的内存大小并调用malloc
分配空间, 还负责调用构造和析构函数
RTTI
- 运行时类型检查, 在
C++
层面主要体现在dynamic_cast
和typeid
, 虚函数表中存放了一个type_info
的指针. - 对于虚函数的类型,
typeid
和dynamic_cast
都会查询type_info
虚函数表具体怎么实现的运行时多态
- 对于存在虚函数的类对象中总是保存着一个指向虚函数表的指针, 子类若重写父类的虚函数, 虚函数表中该函数的地址会被替换.
- 当通过对象指针访问虚函数的时候, 实际上是一个间接调用过程.
- 首先通过虚函数指针访问虚函数表,然后从虚函数表中得到得到调用函数的入口地址,
- 然后再通过此地址调用函数
C语言
是怎么进行函数调用的?
- 参考: C语言函数调用栈(一)
- 每个运行中的函数都对应于一个栈帧.
- 函数调用时候的入栈顺序为:
- 实参倒序(N-1)入栈,
- 调用函数返回地址入栈,
- 调用函数栈基指针入栈
- 设置当前栈顶为被调函数栈底
- 被调函数局部变量顺序(1-N)入栈
- 在被调函数内栈基(向上)分别为返回地址, 1-N的实参
C语言
参数入栈顺序
- 倒序入栈,参考上一个问题
C++
如何处理返回值
- 函数的返回值用于初始化在调用函数时创建的临时对象:
- 返回值为非引用类型:会将函数的返回值复制给临时对象。
- 返回值为引用类型:没有复制返回值,返回的是对象本身。(但是不能返回局部变量的引用, 可以是主函数以引用方式传递的对象,或则是堆对象的引用)
int& abc(int a, int b, int c, int& result){
result = a + b + c;
return result;
}
// 这种形式也可改写为:
int& abc(int a, int b, int c, int *result){
*result = a + b + c;
return *result;
}
// 但是,如下的形式是不可以的:
int& abc(int a, int b, int c){
return a + b + c;
}
C++
中拷贝赋值函数的形参能否使用值传递
- 不能. 如果这种情况在调用拷贝构造函数的时候,首先会将实参传递给形参,这个过程需要调用拷贝构造函数, 从而构成了循环,最终无法完成拷贝, 而且会栈满终止.
malloc
和new
的区别
malloc
是C语言
关键字, 需要指定申请的内存大小,分配之后需要进行强转为指定类型new
是C++
关键字, 不需要指定内存大小, 返回指针也不用强转
说一下select
select
在使用前,先将需要监控的描述符对应的bit位
置1
,然后将其传给select
,- 当有任何一个事件发生时,
select
将会返回所有的描述符, - 需要在应用程序遍历检查哪个描述符上有事件发生,效率很低,并且其不断在内核态和用户态进行描述符的拷贝,开销很大
fork
,wait
,exec
函数
- 父进程首先使用
fork
拷贝出来一个父进程的副本, 此时只拷贝了父进程的页表,两个进程采用写时复制策略, 当有进程写的时候才调用拷贝机制分配内存.fork
一次调用将会在父进程
和子进程
中分别返回一直, 在父进程中返回子进程pid
, 在子进程中返回0
exec函数族
可以用于加载函数镜像替换掉当前进程, 从而完全改变该进程的功能.exec
执行成功后并不会返回到该函数的调用处,这和函数无返回值调用有本质的区别, 但是当执行失败后,函数返回-1
,并继续执行当前进程的后续指令.- 调用了
wait
的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0
,错误返回-1
。
请你回答一下静态函数和虚函数的区别
- 静态函数在编译的时候就已经确定运行时机,而虚函数在运行的时候动态绑定。
- 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
- 另外
- 静态成员函数是类中特殊的成员函数,
- 它被储存于静态储存区, 被该类的所有对象之间共享, 并不属于某个具体的类, 可以直接通过类名访问, 也可以通过对象访问
- 它没有
this
指针,因此它只能访问类中的静态成员函数或者静态成员变量, 而不能直接访问类中的非静态成员变量或非静态成员函数
- 参考: C++中静态成员函数
- 静态成员函数总结:
- 静态成员函数是类中的特殊的成员函数
- 静态成员函数没有隐藏的
this
指针:当调用一个对象的非静态成员函数时,系统会将该对象的起始地址赋值给成员函数的this指针.但是,静态成员函数不属于某个对象,为该类的所有对象共享,所以静态成员函数没有this指针. - 静态成员函数可以通过类名直接访问
- 静态成员函数可以通过对象访问
- 静态成员函数只能直接访问静态成员变量(函数),而不能直接访问普通成员变量(函数)
请你说一说重载和覆盖
- 重载:是指在相同作用域, 两个相同具有函数名,不同参数列表的函数, 返回值类型不做要求, 编译器能够根据参数类型确定调用哪个版本的函数.
- 重写:指在父类中定义了虚函数,在子类中被重新定义, 这种情况是重写.
容器和算法
map
和set
有什么区别,分别又是怎么实现的?
-
map
和set
都是STL
中的关联容器,其底层实现都是红黑树(RB-Tree
)。由于map
和set
所开放的各种操作接口,RB-tree
也都提供了,所以几乎所有的map
和set
的操作行为,都只是转调RB-tree
的操作行为。 -
map
和set
区别在于:map
中的元素是key-value(关键字—值)对
:关键字起到索引的作用,值则表示与索引相关联的数据;set
只是关键字的简单集合,它的每个元素只包含一个关键字。set
的迭代器是const
的,不允许修改元素的值;而map
虽然不允许修改关键字(Key)
,但是允许修改value
。
其原因是map
和set
都是根据关键字排序来保证其有序性的,如果允许修改key
的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map
和set
的结构,导致iterator
失效。所以STL
中将set
的迭代器设置成const
,不允许修改迭代器的值;而map
的迭代器则不允许修改key
值,允许修改value
值。map
支持下标操作,set
不支持下标操作。
map
可以用key
做下标,map
的下标运算符[ ]
将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type
类型默认值的元素至map
中,因此下标运算符[ ]
在map应用中需要慎用,const_map
不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type
类型没有默认值也不应该使用。如果find
能解决需要,尽可能用find
。
请你来介绍一下STL
的allocator
STL
的分配器用于封装STL
容器在内存管理上的底层细节。- 在
C++
中,其内存配置和释放包括两个关键之:new
和delete
:new
运算分两个阶段:1) 调用::operator new
配置内存;2) 调用对象构造函数初始化对象delete
运算分两个阶段:1) 调用对象析构函数;2) 调用::operator delete
释放内存
- 在
STL allocator
将以上阶段分作四个函数分别负责:allocate函数
负责分配内存空间,deallocate函数
负责内存释放,construct
负责对象构造,destroy
负责对象析构. - 为了提升内存管理效率, 减少申请小内存造成的内存碎片化问题,
SGI STL
采用两级分配至, 当分配空间的大小超过128B
的时候,会使用第一级空间配置器, 当分配空间大小小于128B
时,采用第二级空间配置器.- 一级空间配置器直接使用
malloc
,realloc
,free
函数进行内存空间分配和释放. - 二级空间配置器使用内存池技术管理内存, 使用
16
个链表维护8-128byte
的16级别的小内存块.
- 一级空间配置器直接使用
STL
迭代器删除元素
- 这个主要考察的是迭代器失效的问题。
- 对于序列容器
vector
,deque
来说,使用erase(itertor)
后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase
会返回下一个有效的迭代器 - 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
- 对于序列容器
STL
中MAP
数据存放形式
- 红黑树。
unordered map
底层结构是哈希表
STL
有什么基本组成
STL
主要由六大部分组成:容器
,迭代器
,仿函数
,算法
,适配器
,配置器
- 他们之间的关系:
- 配置器为容器提供空间, 它是对空间动态分配,管理和释放的实现
- 迭代器实现了容器和算法的衔接, 算法通过迭代器获取容器中的内容
- 仿函数可以协助算法完成各种操作,适配器用来套接适配仿函数
STL
中map
, unordered_map
, multimap
map
,unordermap
以及multimap
都是关联容器,实现从键值(Key
)到实值(Value
)的映射, 注意shared_ptr
等未重写小于操作符
的类型不能作为键值
- 单映射
Map
:map
中的所有元素都是pair
,pair
的第一元素为键值,第二元素为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。- 底层实现:红黑树
- 适用场景:有序键值对不重复映射
- 多重映射
Multimap
:multimap
中的所有元素都是pair
,pair
的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。- 底层实现:红黑树
- 适用场景:有序键值对可重复映射
- 无序映射
unordered_map
:unordered_map
中的所有元素都是pair
,pair
的第一元素被视为键值,第二元素被视为实值。其不允许键值重复, 其不会根据键值的大小自动排序.- 底层实现:
hash
表 - 使用场景: 无排序需求的键值对不重复映射
vector
和list
的区别,应用,越详细越好
-
vector
: 在堆上分配空间, 连续存储的容器, 支持动态调整空间大小- 底层实现:数组(
array
) - 容器内存空间增长:
vector
增加(插入)新元素时,如果未超过此时的容量(还有剩余空间),那么直接添加到最后(插入指定位置), 然后调整迭代器。- 如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
- 性能:
- 访问:O(1)
- 插入:
- 在最后插入(空间够):很快
- 在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
- 在中间插入(空间够):内存拷贝
- 在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。
- 删除:
- 在最后删除:很快
- 在中间删除:内存拷贝
- 适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
- 底层实现:数组(
-
List
动态链表: 在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。- 底层:双向链表
- 性能:
- 访问:随机访问性能很差,只能快速访问头尾节点。
- 插入:很快,一般是常数开销
- 删除:很快,一般是常数开销
- 适用场景:经常插入删除大量数据
-
区别:
底层
,内存的连续性
,插入和删除的影响
,内存分配时机
,随机访问性能
vector
底层实现是数组;list
是双向 链表。vector
支持随机访问,list
不支持。vector
是连续的内存空间,list
不是。vector
在中间节点进行插入删除会导致内存拷贝,list
不会。vector
一次性分配好内存,不够时才进行2
倍扩容;list
每次插入新节点都会进行内存申请。vector
随机访问性能好,插入删除性能差;list
随机访问性能差,插入删除性能好。
-
应用
vector
拥有连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector
。list
拥有不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list
。
STL
中迭代器的作用,有指针为何还要迭代器?
-
迭代器
Iterator
- (总结)
Iterator
使用聚合对象, 使得我们在不知道对象内部表示的情况下, 按照一定顺序访问聚合对象的各个元素. Iterator
模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。- 由于
Iterator
模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL
的list
、vector
、stack
等容器类及ostream_iterator
等扩展iterator
。
- (总结)
-
迭代器和指针的区别
- 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、—等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,—等操作。
- 迭代器返回的是对象引用而不是对象的值。
-
迭代器产生原因
Iterator
类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构就可以实现集合的遍历,是算法和容器之间的桥梁.
从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的
是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,
而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。
符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),
而引用对象则不能修改。 ↩︎