C++-面试

C++11新特性

auto decltype 就是可以自动推到变量或者数据类型 auto会自动被释放 栈区
auto和decltype区别在于auto一定要初始化 并且表达式的值就是初始化的值 而decltype就无关
lambda表达式
就是类似于一个匿名函数 可以有引用传递 值传递 混合传递 优点就在于定义完就立刻运行 而且在要使用的地方可以立刻定义 方便更改 一般配合auto 形成函数指针使用 如果用引用可能会造成悬挂引用 就是在引用前这个对象就被销毁了 造成指向被清理的内存空间
右值引用 && 什么是右值就是放在赋值号右边的值 一般就是不能被修改的值 右值引用也一定要初始化 左值可以寻址 右值不行 右值一般在常量区 全局区 而左值一般就是在堆栈区
move函数

智能指针
把普通的指针封装成栈对象 生命周期结束后就会在析构函数中释放掉内存
shared_ptr 每次被拷贝赋值初始化时 比如初始化 作为形参传入 或者指向别人的地址 都会计数+1 当值为0的时候就会自动销毁 他创建就等于内部new了一个地址 然后如果不初始化的话 系统自动赋初值为0
union_ptr 不允许拷贝和赋值
weak_ptr 主要是用来打破shared_ptr的指向循环造成的死锁问题 我们就用weak_ptr打破这个循环 weak_ptr就只有观测权 在使用时就会变成shared_ptr
C和C++的区别
C是面向过程C++面向对象 就是添加了类对象之类的 以类驱动程序运行
C主要是用于嵌入式和硬件打交道 C++主要是和操作系统应用层打交道
C++增加了强大的STL容器 泛型编程 重载 异常处理等
与JAVA就是什么垃圾回收啊 多重继承 虚拟机上运行 与开发平台无关 C++直接编译成可执行文件

重写 重载 隐藏的区别

隐藏:只需要同名并且父类不写virtual 如果参数不同 父类不写virtual也是隐藏
重写(覆盖)就是子类继承来的函数可以实现重写 函数名 返回类型 参数要完全一致
基类也要有virtual修饰
重载是同一个类 或者一个文件中一个函数可有多个类型 主要重载在于参数个数的不同 const也能实现重载

sizeof和strlen

strlen是库函数 字符数组作为形参调用sizeof的时候会被降为指针 所以是4 strlen不会
strlen一定要是char*类型变量 而sizeof都行 sizeof以\0结束

static

作用于类中的函数和成员变量时 就使该对象和类只和类有关 和对象无关
作用于局部变量 改变了生命周期 在程序结束时销毁
作用于全局变量和函数时改变了作用于 只能在当前文件使用 不具有全局性
类的静态成员只能访问类的静态成员变量 静态成员函数 主要是因为static实际上是取消了类的this指针 所以不能使用virtual const 和volatile
const和volatile主要就是修饰this指针 将他变成const 现在取消了自然就没有意义了
静态成员变量要在类内声明类外初始化
静态成员变量是被所有对象和继承类 继承对象共享
静态成员变量也可以在类中作为其他成员函数的参数 普通成员变量不行

const

就是将变量变为常量 不能修改 相较于宏定义 可以进行类型检查
修饰函数参数 使函数参数在函数中不能被修改
修饰成员函数就是修饰this 自然就不能修改成员类的任何成员变量
const成员变量只能在狗仔函数初始化列表初始化 这个const成员变量只是相对于某个对象的 因为每个对象在创建的时候都是可以初始化一次这个变量

局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。

const和宏的区别

宏是直接将字符替换 用多少次就替换多少次 所以占用内存较大 不会进行任何类型判断
const是在编译阶段确定值 会进行安全性检查 在静态区 只有一份

typedef和宏的区别

typedef是在编译时处理 有类型检查功能 主要用来定义类型的别名

内联函数

内联函数时在编译的时候就替换了函数 所以他是不能作为虚函数的
不像正常函数需要将参数压入栈 再寻找函数地址 将参数从栈中拿出 得出结果再返回原来运行处 再进行调用 而是直接将函数在调用处展开 减少了调用函数的开销 提高了运行效率 类内定义的成员函数会自动声明成内联函数

new和malloc的区别

malloc 和 free 是库函数,而new 和 delete 是运算符 关键字
new在申请内存也会调用对象的构造函数 而且会自动计算大小
malloc 只会申请内存 指定大小
new申请空间返回的就是该对象类型的对象指针 会自动推断
malloc是先返回void* 然后 就是(int*)malloc(sizeof(int)) 前面那个(int*)就是强制转换
new分配失败的时候返回异常 malloc分配失败时返回空指针
new是在自由存储区为对象动态分配内存 而malloc是在堆上分配
delete 在释放内存之前 会调用对象的析构函数 free 只会释放内存。

虚函数的实现机制

虚函数的地址存放虚函数表中 类的对象地址中存储了指向虚函数表的虚函数表指针vptr
虚函数表在编译阶段建立 虚表指针vptr放在类对象的最前面

构造函数为什么不能定义成虚函数

第一 虚函数需要虚函数指针才能调用 但是构造函数是在对象构造时调用 此时还没有虚函数指针 所以没办法找到虚函数的位置

析构函数为什么要设置成虚函数

因为如果用基类指针指向子类时 在销毁时 如果不将析构函数设置成虚函数 只会调用基类的析构函数 会造成内存泄漏

空类问题

空类定义时会自动生成六个函数
请添加图片描述
字节为1 主要是唯一表示该类的位置

类对象的初始化顺序

基类构造函数–>派生类成员变量的构造函数–>派生类自己的构造函数
就是要注意中间那个 如果派生类的成员变量中有父类对象 则也会再调用一次父类的构造函数

#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
};

class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
};

class Test : public A, public B // 派生列表
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }

private:
    B ex1;//第二个B()A()是调用了这里对象的构造函数
    A ex2;
};

int main()
{
    Test ex;
    return 0;
}
/*
运行结果:
A()
B()
B()
A()
Test()
~Test()
~A()
~B()
~B()
~A()
*/

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efurq1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

深拷贝浅拷贝

仔细去看一下

指针和引用

指针会分配内存地址 引用不会 引用必须被初始化且不能再改变指定 而指针可以

指针

指针是在栈空间创建了一个对象存储一个地址 可以是堆空间地址
指针作为形参传入

int* func(int* a)
{
	return a;
}

创建了一个局部变量指针 这个指针存储了参数的地址 而他自己有一个地址
这个指针在函数结束是会被销毁的 返回的是这个指针指向的地址 如下

/*地址作为参数 我们创建了一个局部指针c用来存储传入的指针指向的地址 
也就是b的地址 然后我们返回的是这个局部指针的地址 但是很明显这个局部指针的地址会被释放 所以传回去肯定获取不到原来的值3了

*/
int** func(int* c)
{
	return &c;
}
int main()
{
	int b = 3;
	int* a = &b;
	cout<<a<<endl;//输出的就是b的地址
	int **c = func(a);//传入b的地址
	cout << "c:" << **c << endl;//1077481 并不是3
}

几个stl容器的底层实现

vector

动态数组 当空间不足的时候会动态申请 原来空间大小的两倍空间 然后将新的元素拷贝到新空间去

deque

双端数组 可以再队头和队尾进行删除和插入操作 底层是动态分段空间
是一小段动态连续内存空间+多个缓存区构成 每个缓存区用来存放数据 而动态连续内存空间是用来存放缓存区的地址的 这样就方便找到缓存区 他能在队头增加删除的原因就在于是分段的 类似于链表和数组的结合体
请添加图片描述
就类似于这样 中控区就是那段连续的地址空间 而缓冲区的地址就保存在中控区中
请添加图片描述
这也是为什么迭代器可以直接q.begin()或者q.end()的原因 cur就是当前指向这个缓冲区的第几个 而node表示是第几个缓冲区 所以他相较于vector会多一个node
没有capacity()和reverse()

关于让迭代器++

self& operator++() 
{
	++cur;	// 切换至下一个元素
	if (cur == last) 	//如果cur达到所在缓冲区尾端
	{
		set_node(node + 1);	//	切换至下一个节点
		cur = first;	// cur指向新缓冲区的首端
	}
	return *this;	
}

list

双向循环链表 不提供迭代器 底层就是链表

queue

队列 不提供迭代器 对deque再封装

stack

堆 不提供迭代器 他们俩没迭代器的原因在于不需要遍历 底层是deque

set

所有值胡根据大小进行排序 底层是红黑树 支持set[]操作
multiset可以排序带有重复的值 set不行

map

底层红黑树

unordered_map

底层哈希表

迭代器

官方定义:迭代器是一个可以遍历stl容器全部或者部分元素的 对象

什么是迭代器

他类似于一个指针 所以我们可以对他解引用(就算用*得到他指向的值) 和运算
但是他并不是指针 指针指向的是地址 而迭代器指向的只是元素在容器中的相对位置
他貌似是一个struct对象

迭代器的作用

他最主要的作用还是用来遍历容器 主要还是方便程序员用相同方式处理不同的数据类型
隔离底层的实现 使用时提供相应接口就行
我们可以通过指针循环实现迭代器的功能 例如vector用for(int i)这种去遍历 但是如果将vector换成list就不行了 所以他相较于指针 迭代器可以对空间不连续的数据进行访问
模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。

迭代器类型

输出型迭代器
输入型迭代器
双向迭代器
正向迭代器
随机访问迭代器 这种迭代器具有所有迭代器的功能
为什么有了随机访问迭代器 还要有其他迭代器 主要是为了在编写算法的时候使用要求最低的迭代器 因为使用级别最低的迭代器可以使算法适用于所有容器 例如find()算法 就所有容器都可以使用 而sort需要使用的是最高级的随机访问迭代器 就导致很多容器不能使用

迭代器说的算法

实际上就是stl里面内置的函数 例如find 和sort

迭代器失效问题

删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置 因为迭代器是指向的是在容器中的相对位置 所以如果删除一个元素就会使后面的迭代器都失效
直接使用erase会使后面的失效 但是我们可以
iter = cont.erase(iter); 这样erase会返回下一个有效的迭代器
而对于关联性的容器来说例如map, set,multimap,multiset
erase一个元素并不会使后面的迭代器失效 只是让当前的迭代器失效
一般采用erase(iter++)让迭代器指向下一个就行了 但是并不能erase(iter);iter++;因为erase已经失效了 ++没有意义
分析erase(iter++) 先把iter传值到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了。

重点
数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

注意:经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite

[]和at()的区别

at()会检查是否越界 如果越界了会返回错误 []不会

类模板和模板类的区别

sort函数

STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。

STL下内存分配失败应该怎么办

什么是大小端模式?如何判断某一个系统是大端模式还是小端模式? 2. 请简述C++中的虚函数的实现机制。(可以举例说明

什么是内存对齐?内存对齐有什么意义?2. 请简述C++中函数调用过程并解释栈帧的概念。(可以举例说明)

简述封装继承多态

其实这三个都是为了方便 使代码不会看起来那么冗余
要从三个方面去讲 what how why
什么是封装 就是将方法和成员放在一个类或者结构体中
封装的目的是什么
封装是为了保护隔离 也是为了方便使用吧 隐藏了具体实现细节 使代码模块化
主要表现就是类和结构体 将成员和相应方法封装 不让类外程序直接访问或修改到类内的成员 这也是面向对象的一种体现吧

而继承是什么
继承的几种表现就是
类和结构体的继承
public projecte private
继承的作用是什么
重用和扩展现有的代码模块

什么是多态
重载 覆盖 具体要去看一下

内存的分配方式有几种

为什么要使用堆空间

c++是不是内存安全的

c++是不是类型安全的

不是 因为可以类型之间进行强制转换

什么是双层级配置器 什么是一级配置器

main函数并不是程序的入口

voliate关键字

当用voliate关键字修饰的变量改动时,cpu会通知其他线程,缓存已被修改,需要更新缓存。这样每个线程都能获取到最新的变量值。
用voliate修饰的变量,可以防止cpu指令重排序

几种排序的应用场景

冒泡排序

稳定
适用于基本有序 数据量较少
改进方法就是设置标志位 没有发生交换就代表前面的那部分已经是有序的了 标识下一次从那里开始就行

插入排序(基本有序就用这个)

稳定
就是抓扑克牌的思想 也是适合基本有序 数据量小的 优化就是使用二分查找目标要插入的位置更快

选择排序

不稳定 主要思想就是每次找出最大或者最小的放在最前或者最后 优化就是每次找出最大和最小的

堆排序

不稳定 nlogn 用数组模仿一个大顶堆 每次取出堆顶最大的数 并且再维护 适合处理数据量大的

归并排序(Merge Sort)

稳定 数据量大并且要求稳定

快排

不稳定

希尔排序

不稳定
对插入的一种改进(就是步长变长了) 所以特性差不多

mutable(一定可以修改)

被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中

100个数,每次踢掉第奇数个,最后剩的是几

第一次去掉的就是奇数的 第二次去掉的就是21的倍数的 第三次就是22
很明显 第二次值全都是2的倍数 此时再去掉奇数位置的 就会发现这个等比再次增大 就变成全是4的倍数的了 大概就是这个意思

堆栈的区别

堆寻址从低到高 栈从高到低
堆由程序员自己分配释放 栈由操作系统自动分配释放
堆频繁分配释放会产生碎片程序越来越慢 栈先进后出的特性不会产生碎片
堆分配效率低 栈分配效率高
造成这样效率不同的原因是:
栈是操作系统提供的数据结构 有专门的寄存器 堆是c++提供的 需要各自分配内存的算法 也就是堆每次分配都需要计算

main函数之前或之后执行

定义在main( )函数之前的全局对象、静态对象的构造函数在main( )函数之前执行 还有全局变量的初始化 内存分配
例如

class A
{
public:
    A()
    {
        cout << "A" << endl;
    }
};
A a;
A* c = new A;
int main()
{
    cout << "main" << endl;
    return 0;
}

或者

int fun()
{
    cout << "fun" << endl;
    return 0;
}
int b = fun();
int main()
{
    cout << "main" << endl;
    return 0;
}

下面这部分是gcc中的 不是c++的
attribute((constructor)) 在main() 之前执行,attribute((destructor)) 在main()执行结束之后执行。

__attribute((constructor))void before()
{
    printf("before main\n");
}

之后执行

_onexit(int fun()) ,其中函数fun()必须是带有int类型返回值的无参数函数
无论函数_onexit() 放到main中任意位置,它都是最后执行
_onexit()在main()中越靠后,则其执行顺序越靠前

#include<cstdlib>
int fun()
{
    cout << "fun" << endl;
    return 0;
}
int fun2()
{
    cout << "fun2" << endl;
    return 0;
}
int main()
{
    _onexit(fun);
    _onexit(fun2);
    cout << "main" << endl;
    return 0;
}

先输出main再输出fun2最后输出fun

什么是main函数

在控制台程序中,main函数是用户定义的执行入口点,当程序编译成功之后,链接器(Linker)会将mainCRTStartup连接到exe中,exe执行时,一开始先mainCRTStartup,这是因为程序在执行时会调用各种各样的运行时库函数,因此执行前必须要初始化好运行时库,mainCRTStartup函数会负责相应的初始化工作,他会完成一些C全局变量以及C内存分配等函数的初始化工作,如果使用C++编程,还要执行全局类对象的构造函数。最后,mainCRTStartup才调用main函数。

强制转换

进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

const_cast

将不可修改的常量类型转换为可修改的
const_cast(varible)中的type必须是指针,引用,或者指向对象类型成员的指针

const 修饰指针指向对象

    int b = 3;
    const int* a = &b;//不能改变指针指向的地址存储的值 const(*a)
    //*a = 4;//这样是不被允许的
    //a = &d;//这样是可以的
    *(const_cast<int*>(a)) = 4;//这两种都行
    //int* c = const_cast<int*>(a);
    //*c = 4;
    cout << *a;//4

const 修饰指针 这个现在还没弄懂

const修饰变量

    int b = 3;
    int d = 4;
     const int a = b;//不能改变指针的指向
    //a = 4;//这样是不允许的
    //a = d;//这样是不允许的
    const_cast<int&>(a) = d;//这两种都行
    cout << a;//4

dynamic_cast

将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数
而且基类一定要有虚函数 要不然会报错 而且一定要是指针进行转换 不能是对象

dynamic_cast和强制转换的区别

实际上就是在下行转换的时候会返回0 通知你出错了 但是你强制转其实还是可以转过去的
有动态类型检查 但是不推荐 因为消耗资源太多 而且一定要父类有虚函数 他为什么能知道父类有虚函数 主要在于 虚函数表的-1位置存放了指向type_info的指针 对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

    A* a = new A;
    A* b = new B;
    A* c = new C;
    //a->fun();
    //b->fun();
    //c->fun();

    //这是基类转子类 下行转换 不安全
    if(B* aa = dynamic_cast<B*>(a))
        cout << "动态上行转换成功"<<endl;
    else
        cout << "动态上行转换失败" << endl;
    if(B* aaa = (B*)(a))
        cout << "强制上行转换成功" << endl;
    else
        cout << "强制上行转换失败" << endl;

    //这是子类转基类 上行转换 安全
    if(A* bb = dynamic_cast<A*>(b))
        cout<<"动态上行转换成功" << endl;//上行转换
    else
        cout << "动态上行转换失败" << endl;
    if(A* bbb = (A*)(b))
        cout << "强制上行转换成功" << endl;//上行转换
    else
        cout << "强制上行转换失败" << endl;

	B* aa = dynamic_cast<B*>(a)//其实你强行用也可以 不会报错

缺点是耗费重大运行成本

static_cast

应该就是强制转换

reinterpret_cast

可以用于任意类型的指针之间的转换,对转换的结果不做任何保证
也是重新定义这个类型
比如

unsigned char*ptr = new unsigned char[1024];
//int *iptr = static_cast<int*>(ptr)//这样是会报错的不允许的 但是我们就想这样转
int *iptr = reinterpret_cast<int*>(ptr);//这样就是可以的

拷贝赋值函数的形参能否进行值传递?

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满

检查内存泄漏的

#define _CRTDBG_MAP_ALLOC
#include<crtdbg.h>
using namespace std;
void main()
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);//就是这句
    int* a = new int;
}

请添加图片描述
要用f5调试

不会的

什么叫有限状态机(不知道)
//STL中不同容器用的是浅拷贝还是深拷贝(不知道)

手撕算法 字符串的哈夫曼编码长度(没听过)这是必刷题啊,这都不会!
手撕算法 用牛顿迭代法求方根C++代码(啥叫牛顿迭代法啊,不会)
手撕算法 LeetCode两数之和(三种方法)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值