c/c++常考面试题(二)

本文详细探讨了C++编程中的关键概念,包括实现Memcpy函数、虚函数的作用及底层原理、STL中vector的实现、智能指针的原理、处理循环引用的方法,以及C++中的static和const关键字作用等。同时,还涉及了内存管理、构造函数和析构函数的使用规范,以及内联函数、静态成员函数与虚函数的关系。
摘要由CSDN通过智能技术生成
一、实现一个Memcpy函数
void *memcpy(void *dst, const void *src, size_t len)
{
 if(NULL == dst || NULL == src){
  return NULL;
 }

 void *ret = dst;

 if(dst <= src || (char *)dst >= (char *)src + len){
  //没有内存重叠,从低地址开始复制
  while(len--){
   *(char *)dst = *(char *)src;
   dst = (char *)dst + 1;
   src = (char *)src + 1;
  }
 }else{
  //有内存重叠,从高地址开始复制
  src = (char *)src + len - 1;
  dst = (char *)dst + len - 1;
  while(len--){
   *(char *)dst = *(char *)src;
   dst = (char *)dst - 1;
   src = (char *)src - 1;
  }
 }
 return ret;
}
二、简述C++虚函数作用及底层实现原理

要点是要答出虚函数表和虚函数表指针的作用。C++中虚函数使用虚函数表和 虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的 虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应 不同基类的虚函数表。

三、STL中vector的实现原理 (衍生:Map, Set等实现原理)

参考答案:
vector的数据安排以及操作方式,与array非常相似。两者的唯一区别在于空间的运用的灵活性。array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细都得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始要求一个大块头的array了,我们可以安心使用array,吃多少用多少。
vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector的旧有空间满载,如果客户端每新增一个元素,vector的内部只是扩充一个元素的空间,实为不智。因为所谓扩充空间(不论多大),一如稍早所说,是” 配置新空间/数据移动/释还旧空间 “的大工程,时间成本很高,应该加入某种未雨绸缪的考虑。稍后我们便可看到SGI vector的空间配置策略了。
另外,由于 vector维护的是一个连续线性空间,所以vector支持随机存取 。
注意:vector动态增加大小时,并不是在原空间之后持续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此, 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 。这是程序员易犯的一个错误,务需小心。

四、请简述智能指针原理,并实现一个简单的智能指针。

参考答案:
智能指针是一种资源管理类,通过对原始指针进行封装,在资源管理对象进行析构时对指针指向的内存进行释放;通常使用引用计数方式进行管理,一个基本实现如下:

class Object;
class SmartPointer;

class Counter
{
 friend class SmartPointer;
public:
 Counter()
 {
  ptr = NULL;
  cnt = 0;
 }
 Counter(Object* p)
 {
  ptr = p;
  cnt = 1;
 }
 ~Counter()
 {
  delete ptr;
 }
private:
 Object* ptr;
 int cnt;
};

class SmartPointer
{
public:
 SmartPointer(Object* p)
 {
  ptr_counter = new Counter(p);
 }
 SmartPointer(const SmartPointer &sp)
 {
  ptr_counter = sp.ptr_counter;
  ++ptr_count->cnt;
 }
 SmartPointer& operator=(const SmartPointer &sp)
 {
  ++sp.ptr_counter->cnt;
  --ptr_counter->cnt;
  if (ptr_counter->cnt == 0)
  {
   delete ptr_counter;
  }
  ptr_counter = sp.ptr_counter;
 }
 ~SmartPointer()
 {
  --ptr_counter->cnt;
  if (ptr_counter->cnt == 0)
  {
   delete ptr_counter;
  }
 }
private:
 Counter* ptr_counter; 
};
五、如何处理循环引用问题?

参考答案:
刚刚研究过这个问题。
何为循环引用
如果有两个或者以上的对象,它们彼此引用,就会造成循环引用。如下面的例子
class Node {
Node next ;
}
Node a = new Node ();
Node b = new Node ();
a . next = b ;
b . next = a ;

代码中,a对象引用了b对象,b对象也引用了a对象,这种情况下a对象和b对象就形成了循环引用。

引用计数GC处理
什么是引用计数
引用计数是一种垃圾回收的形式,每一个对象都会有一个计数来记录有多少指向它的引用。其引用计数会变换如下面的场景
当对象增加一个引用,比如赋值给变量,属性或者传入一个方法,引用计数执行加1运算。
当对象减少一个引用,比如变量离开作用域,属性被赋值为另一个对象引用,属性所在的对象被回收或者之前传入参数的方法返回,引用计数执行减1操作。
当引用计数变为0,代表该对象不被引用,可以标记成垃圾进行回收。
如何处理
实际上单纯的基于引用计数实现的计数器无法处理循环引用带来的问题。
CPython的垃圾回收就是采用引用计数,采用引用计数的主垃圾回收器会清理垃圾,对于那些因为循环引用无法清理的对象,CPython会不时启动一个辅助的基于引用遍历的垃圾回收器来清理它们。
引用遍历GC处理
什么是引用对象遍历
垃圾回收器从被称为GC Roots的点开始遍历遍历对象,凡是可以达到的点都会标记为存活,堆中不可到达的对象都会标记成垃圾,然后被清理掉。 GC Roots有哪些
类,由系统类加载器加载的类。这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用。注意,一般情况下由自定义的类加载器加载的类不能成为GC Roots
线程,存活的线程
Java方法栈中的局部变量或者参数
JNI方法栈中的局部变量或者参数
JNI全局引用
用做同步监控的对象
被JVM持有的对象,这些对象由于特殊的目的不被GC回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。但是具体有哪些前面提到的对象依赖于具体的JVM实现。
如何处理
基于引用对象遍历的垃圾回收器可以处理循环引用,只要是涉及到的对象不能从GC Roots强引用可到达,垃圾回收器都会进行清理来释放内存。
总结
基于引用计数的垃圾回收器无法处理循环引用导致的内存泄露问题,但是其在主流的JVM中很少,几乎所有的JVM都是采用引用对象遍历的方法,垃圾回收器都会处理循环引用潜在的问题。
具体可以参考这里的文章 http://droidyue.com/blog/2015/06/05/how-garbage-collector-handles-circular-references/

六、如何定义一个只能在堆上(栈上)生成对象的类?

在C++中,类的对象建立分为两种,一种是静态建立,如A
a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator
new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

那么如何限制类对象只能在堆或者栈上建立呢?下面分别进行讨论。

1、只能建立在堆上

类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。

容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator
new()函数,而operator()函数用于分配内存,无法提供构造功能。因此,这种方法不可以。

当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

因此,将析构函数设为私有,类对象就无法建立在栈上了。代码如下:

    class A  
    {  
    public:  
        A(){}  
        void destory()
        {
            delete  this;
        }  
    private:  
        ~A(){}  
  };  

试着使用A a;来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。

上述方法的一个缺点就是,无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。

另一个问题是,类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:

    class A
    {  
    protected:  
        A(){}  
        ~A(){}  
    public:  
      static A* create()  
      {  
          return new A();  
      }  

      void destory() 
      {  
          delete this;  
      }  
  };  

这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。
2、只能建立在栈上

只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator
new()设为私有即可。代码如下:

class A  
{ 
private:  
void* operator new(size_t t){}     
// 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} 
// 重载了new就需要重载delete
public:  
    A(){}  
    ~A(){}  
}; 
七、下面的结构体大小分别是多大(假设32位机器)?
struct A {
char a;
char b;
char c;
};

struct B {
int a;
char b;
short c;
};

struct C {
char b;
int a;
short c;
};

struct D {
char b;
int a;
short c;
};

参考答案:
3,8,12,8

八、引用和指针有什么区别?

参考答案:
本质:引用是别名,指针是地址,具体的:
• 指针可以在运行时改变其所指向的值,引用一旦和某个对象绑定就不再改变
• 从内存上看,指针会分配内存区域,而引用不会,它仅仅是一个别名
• 在参数传递时,引⽤用会做类型检查,而指针不会
• 引用不能为空,指针可以为空

九、const和define有什么区别?

参考答案:
本质:define只是字符串替换,const参与编译运行,具体的:
• define不会做类型检查,const拥有类型,会执行相应的类型检查
• define仅仅是宏替换,不占用内存,而const会占用内存
• const内存效率更高,编译器通常将const变量保存在符号表中,而不会分配存储空间,这使得它成 为一个编译期间的常量,没有存储和读取的操作

十、define和inline有什么区别?

参考答案:
本质:define只是字符串替换,inline由编译器控制,具体的:
• define只是简单的宏替换,通常会产生二义性;而inline会真正地编译到代码中
• inline函数是否展开由编译器决定,有时候当函数太大时,编译器可能选择不展开相应的函数

十一、malloc和new有什么区别?

参考答案:
1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2,对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3,因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4,C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

5、new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。

十二、C++中static关键字作用有哪些?

参考答案:
1、隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.
2、static的第二个作用是保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,
说到底static还是用来隐藏的。虽然这种用法不常见
3、static的第三个作用是默认初始化为0(static变量)
4、C++中的作用
1)不能将静态成员函数定义为虚函数。
2)静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误)
3)静态数据成员在<定义或说明>时前面加关键字static。

十三、C++中const关键字作用有哪些??

参考答案:
• 修饰变量
• 修饰成员函数,表示该成员函数不会修改成员变量

十四、C++中成员函数能够同时用static和const进行修饰?

否,因为static表⽰示该函数为静态成员函数,为类所有;而const是用于修饰成员函数的,两者相矛盾

十五、问答题

下面三个变量分别代表什么含义?
const int* ptr;
int const* ptr;
int* const ptr;

参考答案:
前两个代表指向const变量的指针,即指针所指向的对象是const的,不能使用指针修改;最后一个代表const指针,即指针本身是const的,不能指向其他地址

十六、C++中包含哪几种强制类型转换?他们有什么区别和联系?

参考答案:
• reinterpret_cast: 转换一个指针为其它类型的指针。它也允许从一个指针转换为整数类型,反之亦 然. 这个操作符能够在非相关的类型之间转换. 操作结果只是简单的从一个指针到别的指针的值的 二进制拷贝. 在类型之间指向的内容不做任何类型的检查和转换?
class A{};
class B{};
A* a = new A;
B* b = reinterpret_cast(a);
• static_cast: 允许执行任意的隐式转换和相反转换动作(即使它是不允许隐式的),例如:应用到类 的指针上, 意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换), 同 时, 也能够执行相反动作: 转换父类为它的子类
class Base {};
class Derive:public Base{};
Base* a = new Base;
Derive *b = static_cast(a);
• dynamic_cast: 只用于对象的指针和引用. 当用于多态类型时,它允许任意的隐式类型转换以及相 反过程. 不过,与static_cast不同,在后一种情况里(注:即隐式转换的相反过程),dynamic_cast 会检查操作是否有效. 也就是说, 它会检查转换是否会返回一个被请求的有效的完整对象。检测在 运行时进行. 如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL. 对于引用 类型,会抛出bad_cast异常
• const_cast: 这个转换类型操纵传递对象的const属性,或者是设置或者是移除,例如:
class C{};
const C* a = new C;
C *b = const_cast(a);

十七、简述C++虚函数作用及底层实现原理

参考答案:
要点是要答出虚函数表和虚函数表指针的作用。C++中虚函数使用虚函数表和 虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的 虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应 不同基类的虚函数表。

十八、在什么情况下,析构函数需要是虚函数?

参考答案:
若存在类继承关系并且析构函数中需要析构某些资源时,析构函数需要是虚函数,否则当使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄露等问题

十九、内联函数、构造函数、静态成员函数可以是虚函数吗?

参考答案:
都不可以。内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开; 构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类 的,因此不存在动态绑定的概念;静态成员函数是以类为单位的函数,与具体对象无关,虚函数是 与对象动态绑定的,因此是两个不冲突的概念;

二十、构造函数中可以调用虚函数吗?

参考答案:
可以,但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数

二十一、简述C++中虚继承的作用及底层实现原理?

参考答案:
虚继承用于解决多继承条件下的菱形继承问题,底层实现原理与编译器相关,一般通过虚基类 指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避 免菱形继承中的二义性问题。

二十二、简述Linux内存分配–伙伴系统 原理

参考答案:
伙伴系统,其思想是:把内存块分成不同的组(1,2,4,8,16,32….);分配内存时找到能够满足条件 的最小的块;如果找不到,就找大的块,然后一分为2,分配一块,留一块;回收时:如果有相邻的同样大小的块,则合并

二十三、简述Malloc实现原理

参考答案:
可以基于伙伴系统实现,也可以使用基于链表的实现
• 将所有空闲内存块连成链表,每个节点记录空闲内存块的地址、大小等信息
• 分配内存时,找到大小合适的块,切成两份,一分给用户,一份放回空闲链表
• free时,直接把内存块返回链表
• 解决外部碎片:将能够合并的内存块进行合并

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值