干货收藏:68道C语言与C++常见面试题(三)

39 说说强制类型转换运算符


static_cast


用于非多态类型的转换

不执行运行时类型检查(转换安全性不如 dynamic_cast)

通常用于转换数值数据类型(如 float -> int)

可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

dynamic_cast


用于多态类型的转换

执行行运行时类型检查

只适用于指针或引用

对不明确的指针的转换将失败(返回 nullptr),但不引发异常

可以在整个类层次结构中移动指针,包括向上转换、向下转换

const_cast


用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 ) reinterpret_cast

用于位的简单重新解释

滥用 reinterpret_cast 运算符可能很容易带来风险。除非所需转换本身是低级别的,否则应- 使用其他强制转换运算符之一。

允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)

也允许将任何整数类型转换为任何指针类型以及反向转换。

reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。

reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。

bad_cast


由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。

bad_cast 使用


try {

   Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);

}

catch (bad_cast b) {

   cout << "Caught: " << b.what();

}

40 谈谈你对拷贝构造函数和赋值运算符的认识


拷贝构造函数和赋值运算符重载有以下两个不同之处:


拷贝构造函数生成新的类对象,而赋值运算符不能。

由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象 是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉。

注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认 的。


41 在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?


不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。malloc /free的操作对象都是必须明确大小的。而且不能用在动态类上。new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。


42 用C++设计一个不能被继承的类


template <typename T> class A 
{ 
   friend T; 
    private: 
     A() {} 
    ~A() {} 
}; 
class B : virtual public A<B> 
{ 
   public: 
    B() {} 
   ~B() {} 
}; 
class C : virtual public B 
{ 
   public: 
     C() {} 
    ~C() {} 
}; 
void main( void ) 
{ 
    B b; 
    //C c; 
    return; 
}

注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才 是自己的。


43 C++自己实现一个String类

#include <iostream>
#include <cstring>
 
using namespace std;
 
class String{
public:
    // 默认构造函数
    String(const char *str = nullptr);
    // 拷贝构造函数
    String(const String &str);
    // 析构函数
    ~String();
    // 字符串赋值函数
    String& operator=(const String &str);
 
private:
    char *m_data;
    int m_size;
};
 
// 构造函数
String::String(const char *str)
{
    if(str == nullptr)  // 加分点:对m_data加NULL 判断
    {
        m_data = new char[1];   // 得分点:对空字符串自动申请存放结束标志'\0'的
        m_data[0] = '\0';
        m_size = 0;
    }
    else
    {
        m_size = strlen(str);
        m_data = new char[m_size + 1];
        strcpy(m_data, str);
    }
}
 
// 拷贝构造函数
String::String(const String &str)   // 得分点:输入参数为const型
{
    m_size = str.m_size;
    m_data = new char[m_size + 1];  //加分点:对m_data加NULL 判断
    strcpy(m_data, str.m_data);
}
 
// 析构函数
String::~String()
{
    delete[] m_data;
}
 
// 字符串赋值函数
String& String::operator=(const String &str)  // 得分点:输入参数为const
{
    if(this == &str)    //得分点:检查自赋值
        return *this;
 
    delete[] m_data;    //得分点:释放原有的内存资源
    m_size = strlen(str.m_data);
    m_data = new char[m_size + 1];  //加分点:对m_data加NULL 判断
    strcpy(m_data, str.m_data);
    return *this;       //得分点:返回本对象的引用
}


44 访问基类的私有虚函数

#include <iostream.h> 
class A
{ 
   virtual void g() 
   { 
      cout << "A::g" << endl; 
   } 
  private: 
   virtual void f() 
   { 
      cout << "A::f" << endl; 
   } 
}; 
class B : public A 
{ 
   void g() 
   { 
      cout << "B::g" << endl; 
   } 
   virtual void h() 
   { 
      cout << "B::h" << endl; 
   } 
}; 
typedef void( *Fun )( void ); 
void main() 
{ 
   B b; 
   Fun pFun; 
   for(int i = 0 ; i < 3; i++) 
   { 
      pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i ); 
      pFun(); 
   } 
} 

45 对虚函数和多态的理解


多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。


虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。


46 简述类成员函数的重写、重载和隐藏的区别


(1)重写和重载主要有以下几点不同。


范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。

参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一 定不同。

virtual 的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被 virtual 修饰,也可以没有。

(2)隐藏和重写、重载有以下几点不同。


与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。

参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。 当参数不相同时,无论基类中的参数是否被virtual 修饰,基类的函数都是被隐藏,而不是被重写。

注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完 全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。


47 链表和数组有什么区别


存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间, 长度可变,每个结点要保存相邻结点指针。

数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点, 效率低。

数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。

越界问题:链表不存在越界问题,数组有越界问题。

注意:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插 入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。


48 用两个栈实现一个队列的功能

typedef struct node 
{ 
 int data; 
 node *next; 
}node,*LinkStack; 
 
//创建空栈: 
LinkStack CreateNULLStack( LinkStack &S) 
{ 
 S = (LinkStack)malloc( sizeof( node ) ); // 申请新结点 
 if( NULL == S) 
 { 
  printf("Fail to malloc a new node.\n");
 
  return NULL; 
 } 
 S->data = 0; //初始化新结点 
 S->next = NULL; 
 
 return S; 
} 
 
//栈的插入函数: 
LinkStack Push( LinkStack &S, int data) 
{ 
 if( NULL == S) //检验栈 
 { 
  printf("There no node in stack!"); 
  return NULL; 
 } 
 
 LinkStack p = NULL; 
 p = (LinkStack)malloc( sizeof( node ) ); // 申请新结点 
 
 if( NULL == p) 
 { 
  printf("Fail to malloc a new node.\n"); 
  return S; 
 } 
 if( NULL == S->next) 
 { 
  p->next = NULL; 
 } 
 else 
 { 
  p->next = S->next; 
 } 
 p->data = data; //初始化新结点 
 S->next = p; //插入新结点 
 return S; 
} 
 
//出栈函数: 
node Pop( LinkStack &S) 
{ 
 node temp; 
 temp.data = 0; 
 temp.next = NULL; 
 
 if( NULL == S) //检验栈 
 { 
  printf("There no node in stack!"); 
  return temp; 
 } 
 temp = *S; 
 
 if( S->next == NULL ) 
 { 
  printf("The stack is NULL,can't pop!\n"); 
  return temp; 
 } 
 LinkStack p = S ->next; //节点出栈 
 
 S->next = S->next->next; 
 temp = *p; 
 free( p ); 
 p = NULL; 
 
 return temp; 
} 
 
//双栈实现队列的入队函数: 
LinkStack StackToQueuPush( LinkStack &S, int data) 
{ 
 node n; 
 LinkStack S1 = NULL; 
 CreateNULLStack( S1 ); //创建空栈 
 
 while( NULL != S->next ) //S 出栈入S1 
 { 
  n = Pop( S ); 
  Push( S1, n.data ); 
 } 
 Push( S1, data ); //新结点入栈 
 
 while( NULL != S1->next ) //S1 出栈入S 
 { 
  n = Pop( S1 ); 
  Push( S, n.data ); 
 } 
 return S; 
} 

注意:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个队列的功能呢?结果是否定 的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一 起都是先进先出,而无法实现先进后出。


49 vector的底层原理


vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。


当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间[vector内存增长机制]。


当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。


50 vector中的reserve和resize的区别


reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。

resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。

51 vector中的size和capacity的区别


size表示当前vector中有多少个元素(finish - start);

capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

52 vector中erase方法与algorithn中的remove方法区别


vector中erase方法真正删除了元素,迭代器不能访问了

remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

53 vector迭代器失效的情况


当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。

当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。

54 正确释放vector的内存(clear(), swap(), shrink_to_fit())


vec.clear():清空内容,但是不释放内存。

vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。

vec.shrink_to_fit():请求容器降低其capacity和size匹配。

vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

55 list的底层原理


ist的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。

list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

56 什么情况下用vector,什么情况下用list,什么情况下用deque


vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。

list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。

需要从首尾两端进行插入或删除操作的时候需要选择deque。

57 priority_queue的底层原理


priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。

58 map 、set、multiset、multimap的底层原理


map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。


红黑树的特性:


每个结点或是红色或是黑色;

根结点是黑色;

每个叶结点是黑的;

如果一个结点是红的,则它的两个儿子均是黑色;

每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

59 为何map和set的插入删除效率比其他序列容器高


因为不需要内存拷贝和内存移动

60 为何map和set每次Insert之后,以前保存的iterator不会失效?


因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

61 当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?


RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

62 map 、set、multiset、multimap的特点


set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。

map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。

map和set的增删改查速度为都是logn,是比较高效的。

63 为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?


存储的是结点,不需要内存拷贝和内存移动。

插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

64 为何map和set不能像vector一样有个reserve函数来预分配数据?


在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。

65 set的底层实现实现为什么不用哈希表而使用红黑树?


set中元素是经过排序的,红黑树也是有序的,哈希是无序的

如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(lgn)

66 hash_map与map的区别?什么时候用hash_map,什么时候用map?


构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。

存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。

总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。

如果考虑效率,特别当元素达到一定数量级时,用hash_map。

考虑内存,或者元素数量较少时,用map。

67 迭代器失效的问题


插入操作:


对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;

对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。 删除操作:

对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;

对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;

对于list和forward_list,所有的iterator,pointer和refercnce有效。

对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

68 STL线程不安全的情况


在对同一个容器进行多线程的读写、写操作时;

在每次调用容器的成员函数期间都要锁定该容器;

在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;

在每个在容器上调用的算法执行期间锁定该容器。


上一篇:干货收藏:68道C语言与C++常见面试题(二)


下一篇:熬夜为学弟学妹整理的网络编程基础知识(一)!