C++ 常考面试点总结

文章目录

一:new 和 malloc 的区别 ?(11 点区别)

  • 性质不同

    1. new/delete 是C++的运算符。malloc/free 是库函数。所以malloc/free不在编译器控制权限之内,他们由OS控制,所以不能够把执行构造函数和析构函数的任务强加给 malloc/free。
  • new 操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从上动态分配内存。

  • 返回指针不同

    1. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • 内存分配失败时的返回值

    1. new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
  • 是否需要指定内存大小

    1. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
class A{...}
A * ptr = new A;
A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A); 
  • 是否调用构造函数/析构函数

    1. new 分三步:
      1. 调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
      2. 编译器运行相应的构造函数以构造对象,并为其传入初值。
      3. 对象构造完成后,返回一个指向该对象的指针
    2. delete 分为两步:
      1. 调用对象的析构函数
      2. 编译器调用operator delete(或operator delete[])函数释放内存空间。
  • 分配数组的处理不同

    1. C++提供了new[]与delete[]来专门处理数组类型。而malloc还要自己手动计算数组大小。
  • operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。

void * operator new (sieze_t size)
{
    if(void * mem = malloc(size)
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
    free(mem);
}
  • 是否可以被重载

    1. 一个是运算符,一个是库函数。运算符可以被重载
  • 是否能够直观地重新分配内存

    1. 使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域
    2. new没有这样直观的配套设施来扩充内存。
  • 处理内存分配不足的方式不同

    1. operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型,用户可以指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:
namespace std
{
    new_handler set_new_handler(new_handler p ) throw();
}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向旧的new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

在这里插入图片描述

比较主要的区别是:

1.一个是运算符,一个是库函数。这样就决定了是否能够被重载
2.一个在自由存储区分配内存,一个在堆上分配内存。是否相同需要取决于operator new 的实现。
3.一个调用构造函数,一个不调用。这是由编译器实现的
4.一个内存分配失败可以指定处理函数, 一个不可以。只能看着返回NULL.
5.一个在申请空间时,不需要手动计算大小,一个需要手动计算大小。包括分配数组的时候。

参考:https://www.cnblogs.com/QG-whz/p/5140930.html

  1. 自由存储区和堆的区别:

基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

  1. C++对象内存布局:
  2. C++的类型安全性(类型转换):

二:关于map 和 multimap

红黑树实现。有序。
插入,删除,更新 的时间复杂度是O(logn)。

map 中的 key 如果是个对象的话,怎么办?

key的类型必须能够进行 < 运算,且唯一,必须定义比较的方法,默认排序是按照从小到大

template < class Key, class T, class Compare = less<Key>,  
           class Allocator = allocator<pair<const Key,T> > > class map;  

其中第三、四个均包含默认参数,可以不指定。我们可以通过指定 Compare 类来指定排序的顺序。其中less是stl里面的一个函数对象(即调用操作符的类,其对象常称为函数对象)

具体的可操作方法见:

C++小点之范型算法自定义比较函数的五种方法

mutilmap 是如何处理键值相同的元素的,如何取出相同的所有的元素,然后取出的元素是如何组织起来的?

内部实现是红黑树,所以就是二叉查找树,只要定义元素相等时向左放置或者向右放置的规则就行了。具体使用插入的接口是 insert_equal()而不是insert_unique()

如何取出key相同的元素?(三种方法)

  • 1.使用find 和 count
auto count = mmp.count(key);
auto iter = mmp.find(key);
while (count)
{
    cout << iter->second << endl;
    iter++;
    count--;
}
  • 2.使用 equal_range()函数
    equal_range()返回一个pair,是两个迭代器。一个指向第一个匹配位置,另一个指向最后一个匹配元素之后的位置。如果没有找到,则指向关键字可以插入的位置。
for (auto pos = mmp.equal_range(key); pos.frist != pos.second; pos.frist++)
{
    cout << pos.frist->second << endl;
}

-3.使用迭代器 lower_bound()和upper_bound()

lower_bound():返回第一个匹配位置
upper_bound():返回最后一个匹配元素之后的位置

如果不存在,两个返回相同的迭代器。都指向关键字可以插入的位置。

for (auto beg = mmp.lower_bound(key),
 end = mmp.upper_bound(key); 
 beg != end;
 beg++)
{
    cout << beg->second << endl;
}

取出来的组织形式是用链表来组织的。因为内部实现是二叉查找树嘛!!!

三:STL各种容器迭代器失效的问题

迭代器不指向任何元素。无法通过迭代器存取迭代器所指向的内存。

标准STL序列容器:vector、string、deque和list。

  • vector(实现:数组) 和 string
    • 在内存重新分配时将失效(它所指向的元素在该操作的前后不再相同),所有的迭代器,指针,引用 都将失效。
    • 当删除,添加一个元素时,指向被删除/添加元素以后的任何元素的迭代器都将失效。

vector的 resize(), reserve(),capacity(),size()函数

  1. resize():改变容器中元素的个数。如果比原来少,多的就删除。如果比原来多,就需要添加
  2. reserve():通知容器应该准备保存多少个元素。如果目前的capacity()是10,reserve(20)就会改变 vector 的容量。
  3. capacity():不扩张内存的情况下可以容纳多少个元素。
  4. size():目前有多少元素

当增长的元素个数超过容量即capacity()时, 扩充为两倍.不足时更大

  • deque (实现:数组)

    • 增加任何元素都将使 deque 的迭代器失效(如果是在首尾添加元素,则指向存在的元素的引用和指针不会失效)。
    • 在deque的中间删除元素将使所有迭代器,引用,指针都失效。在deque的头或尾删除元素时,只有指向该元素的迭代器失效。
  • list和forward_list(实现:链表)

    • 增加任何元素都不会使迭代器失效。删除元素时,除了指向当前被删除元素的迭代器外,其它迭代器都不会失效。

标准STL关联容器:set、multiset、map和multimap。

如果迭代器所指向的元素被删除,则该迭代器失效。其它任何增加、更新元素的操作都不会使迭代器失效。
想想其内部实现(类似于链表的形式)

四:指针和引用的区别

区别与联系:

C++primer中对 对象的定义:对象是指一块能存储数据并具有某种类型的内存空间
一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值

  • (1)指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的对象实质上是同一个东西,只不过是原对象的一个别名而已。类似于软硬链接
  • (2)指针占用内存大小, 而引用不占内存大小。这个就是不能定义引用的引用的原因。所以引用本身不是一个对象,也即没有对应的内存空间。
  • (3)指针的值在定义的时候可以暂时不进行初始化,引用在定义的时候必须初始化
  • (4)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就与初始化对象绑定,不能再改变引用大多数情况下的实现是 const 指针

一般什么情况下用指针?什么情况下用引用?

使用引用参数的主要原因有两个:
  • 程序员能修改调用函数中的数据对象
  • 通过传递引用而不是整个数据–对象,避免一次实参到形参的拷贝。可以提高程序的运行速度
一般的原则:
  • 对于使用引用的值而不做修改的函数:
    • 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
    • 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
    • 如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
    • 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)
  • 对于修改函数中数据的函数:
    • 如果数据是内置数据类型,则使用指针
    • 如果数据对象是数组,则只能使用指针
    • 如果数据对象是结构,则使用引用或者指针
    • 如果数据是类对象,则使用引用

Linux下的软硬链接:
const 与static 的区别:

更多参考:c++中,引用和指针的区别是什么? 知乎

五:关于智能指针

  • unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:
  1. 拥有它指向的对象。某个时刻只能有一个 unique_ptr 指向一个给定的对象。

  2. 不支持普通的拷贝,赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作,std::move

  3. 当 unique_ptr 销毁时,会使用给定的删除器释放它指向的对象。

    • 可以通过{ release():放弃对指针的控制权,reset():释放指向的对象。}将原指针的所有权转移给另一个指针。
  • shared_ptr 资源可以被多个指针共享,它使用引用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

    • 缺点是会出现循环引用的情况。
  • weak_ptr

  1. 是一种不控制所指向对象生存期的智能指针。它指向一个shared_ptr管理的对象。不会增加shared_ptr 的引用计数,一旦引用计数为0,对象就会被释放,即使还存在 weak_ptr
  2. 创建weak_ptr时,要使用一个 shared_ptr 来初始化它。会解决 shared_ptr 循环引用的问题。
  3. 由于对象可能不存在,所以不能直接使用,必须先调用lock()去检查指向的对象是否存在。如果不存在,则返回一个空的 shared_ptr 。

使用智能指针一定能够避免内存泄漏吗?

  • shared_ptr 的循环引用会造成内存泄漏。
  • 没有为多态基类声明 virtual 析构函数。造成局部销毁的情况。
  • 析构函数产生异常,导致析构不完整。

shared_ptr 是否是线程安全的 ?(答:不是)

(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。

所以如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁

  • shared_ptr 的数据结构
    在这里插入图片描述
    ref_count 指针,指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图所示,其中就包含了use_count 的值!

  • 所以 shared_ptr 的复制操作就有两步:

  1. 复制 ptr 指针:
  2. 复制 ref_count 指针,导致引用计数加 1

在这里插入图片描述
既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。

参考文章:为什么多线程读写 shared_ptr 要加锁? 陈硕

右值,左值,std::move与移动构造函数:

六:多态基类析构函数为什么需要声明为 virtual

  • 对于多态的构造函数与析构函数的执行:

普通继承或虚函数继承,子类指针指向子类实例:

父类构造函数>>>子类构造函数
子类析构函数>>>父类析构函数

普通继承,父类指针指向子类实例:

父类构造函数>>>子类构造函数
父类析构函数

虚函数继承,父类指针指向子类实例:

父类构造函数>>>子类构造函数
子类析构函数>>>父类析构函数

由以上结果及测试情况得出以下结论:

  1. 无论如何继承,指针如何指向,构造函数都以最终实例化为准,顺序始终是先父类后子类
  2. 析构函数遵从类的多态性,非虚析构函数则以指针类型为准,虚析构函数则以最终实例为准,存在继承关系时顺序是先子类后父类

答:因为当derived class 对象经由一个base class的指针被delete删除的时候是调用不到derived class的析构函数的。这就造成了局部销毁。 示例:

#include <iostream>
using namespace std;

class base
{
public:
    base() {}
    ~base()
    {
        cout << "~ base" << endl;
    }
};
class derived : public base
{
public:
    derived() {}
    ~derived()
    {
        cout << "~ derived" << endl;
    }
};
int main()
{
    base *pb = new derived();
    delete pb;
}

七:为啥构造与析构中不使用virtual函数呐?

class base
{
public:
    base()
    {
        fun();
    }
    virtual void fun()
    {
        cout << " base 的普通虚函数 " << endl;
    }
    ~base()
    {
        cout << "~ base" << endl;
    }
};
class derived : public base
{
public:
    derived() {}
    void fun()
    {
        cout << " dervied 的普通虚函数 " << endl;
    }
    ~derived()
    {
        cout << "~ derived" << endl;
    }
};
int main()
{
    derived dd;
}

在这里插入图片描述
先构造base,然后base中调用的fun不会下降到dervied class。所以有问题。
构造函数中的调用不会下降到子类函数。

八:尽量少做类型转换

C++小点之四种强制类型转换

  • const_cast(expression) :去除const属性
  • dynamic_cast(expression):安全的向下转型。就是将基类(包含虚函数的基类)的指针/引用强制转换为派生类指针/引用
  • reinterpret_cast(expression):用来进行各种不同类型的指针,引用,以及指针和能容纳的下指针的整数类型之间的转换。转换方式:逐个拷贝比特(不可移植)
  • static_cast(expression):强迫隐式转换。int->double,non const->const
关于dynamic_cast失败的问题:

基类指针向派生类指针的转取决于这个基类指针是不是真的指向一个派生类对象 ,如果是,那转换之后完全OK,如果不是,那么将它强制转换为派生类指针时就会出现问题 。

九:C++空类的大小

  • 定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求sizeof,得到的结果时多少?

空类型的实例不包括任何信息,本来求sizeof应该是0,但是我们在声明该类型实例的时候,必须给实例在内存中分配一定的空间,否则无法使用该实例。由于空类型不含任何信息,故而所占的内存大小由编译器决定。一般为 1

if(empty())
	x = 1 ;
else 
	...

切忌:一旦类中有其他的占用空间成员,则这1个字节就不在计算之内

在这里插入图片描述

  • 在该类中添加构造函数和析构函数,再对该类型求sizeof,得到的结果时多少?

仍然为1Byte。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例不相关。编译器不会因为这两个函数在实例中添加任何额外的信息。对于其他成员函数(非虚函数),此规则也适用

  • 如果析构函数标记为虚函数呢?再对该类型求sizeof,得到的结果时多少?

C++ 编译器一旦发现类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4B;在64位的机器上,一个指针占8B。

问:如果实例化10次子类?会有几张虚表?

答:一张。只会在每个示例中添加一个指向虚函数表的指针。

拓展:C++多态 虚函数表的实现

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值