U3D游戏面试中出现的C++问题()

如果公司没有明确写要求会C++,但是只要简历上写了会C++,就一定会问,斟酌写C++(既然你诚心诚意地写了,那我就大发慈悲地问你)。在此先分享两篇,同时本文进行了参考:

腾讯社招面试复习系列之一,C++篇_c++社招-CSDN博客

C++面试复习总结 - zhxmdefj - 博客园 (cnblogs.com)

参考文章:

哔哩哔哩up主 骑猪撞宝马71的文章  骑猪撞宝马71投稿视频-骑猪撞宝马71视频分享-哔哩哔哩视频 (bilibili.com)

内存泄漏

内存泄漏指的是应用程序分配某段内存后,失去了对该段内存的控制,因而造成了内存的浪费。

原因有:

1.在类的构造函数和析构函数中没有匹配地调用new和delete函数

2.没有正确地清除 嵌套的对象指针

3.在释放 对象数组 时没有使用delete[]

4.指向对象的指针数组不等同于对象数组

       指向对象的指针数组和对象数组的销毁方式不同。

       指向对象的指针数组在销毁时不需要调用析构函数,只需要释放指针数组占用的内存空间,如果指针指向的是动态分配的对象,还需要手动释放对象占用的内存空间,否则会造成内存泄漏。

       对象数组在销毁时需要调用析构函数,释放对象占用的资源。

5.缺少 拷贝构造函数重载赋值运算符:隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。

6.没有将基类的 析构函数 定义为虚函数

野指针:指向被释放的或者访问受限内存的指针。

造成野指针的原因:

1.指针变量没有被初始化(如果值不定,可以初始化为NULL)

2.指针被free或者delete后,没有置为NULL,free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL

3.指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针

避免内存泄漏:智能指针

怎么解释上述的第五条

拷贝构造函数用于根据已有的对象创建一个新的对象,例如:

Point p1(1, 2); // 使用普通构造函数创建一个点对象
Point p2(p1); // 使用拷贝构造函数创建一个和p1一样的点对象

赋值运算符是一种特殊的运算符,它用于将一个对象的值赋给另一个对象,例如:

Point p1(1, 2); // 使用普通构造函数创建一个点对象
Point p2; // 使用默认构造函数创建一个点对象
p2 = p1; // 使用赋值运算符将p1的值赋给p2

        如果我们不自己定义拷贝构造函数或赋值运算符,编译器会为我们生成默认的版本,它们的行为是逐个拷贝对象的成员变量,这被称为浅拷贝。这在一些情况下是没有问题的,例如上面的点类,它的成员变量都是基本类型,没有涉及到动态内存分配或其他资源的管理。但是,在一些情况下,浅拷贝会导致一些问题,例如内存泄漏、重复释放、数据错误等。这通常发生在对象的成员变量中包含指针或其他复杂类型的时候,例如:

class String {
    public:
        String(const char* str) { // 使用普通构造函数根据C风格字符串创建一个字符串对象
            _str = new char[strlen(str) + 1]; // 动态分配内存空间
            strcpy(_str, str); // 复制字符串内容
        }
        ~String() { // 使用析构函数释放动态分配的内存空间
            delete[] _str;
        }
    private:
        char* _str; // 使用指针存储字符串内容
};

在这个例子中,如果我们不自己定义拷贝构造函数或赋值运算符,编译器会为我们生成默认的版本,它们的行为是拷贝指针的值,而不是指针指向的内容这就导致了两个对象拥有指向同一个动态分配的内存空间的指针。例如:

String s1("hello"); // 使用普通构造函数创建一个字符串对象,_str指向一个动态分配的内存空间,存储"hello"
String s2(s1); // 使用拷贝构造函数创建一个和s1一样的字符串对象,_str拷贝了s1的_str的值,也就是指向了同一个内存空间

这样就会出现以下问题:

•  当s1或s2的生命周期结束时,它们会调用析构函数,释放_str指向的内存空间,这就导致了另一个对象的_str指向了一个已经被释放的内存空间,这就是内存泄漏,也可能引发无效指针的错误。

•  当我们想要修改s1或s2的内容时,例如:

s1 = "world"; // 使用赋值运算符将一个新的字符串赋给s1

•  首先,我们需要为"world"这个字符串创建一个临时的String对象,然后调用赋值运算符将它的值赋给s1,这就涉及到了拷贝构造函数和赋值运算符的调用,如果我们没有自己定义它们,就会使用编译器生成的默认版本,它们的行为是拷贝指针的值,而不是指针指向的内容,这就导致了s1的_str和临时对象的_str指向了同一个内存空间,存储"world"。

•  然后,我们需要释放临时对象占用的资源,调用析构函数,释放_str指向的内存空间,这就导致了s1的_str指向了一个已经被释放的内存空间,这就是内存泄漏,也可能引发无效指针的错误。

•  最后,我们需要注意的是,由于s1和s2的_str指向了同一个内存空间,所以当我们修改s1的内容时,也会影响到s2的内容,这就是数据错误,也可能不是我们想要的结果。

为了避免这些问题,我们需要自己定义拷贝构造函数和赋值运算符,让它们的行为是拷贝指针指向的内容,而不是指针的值,这被称为深拷贝。例如:

class String {
public:
    String(const char* str) { // 使用普通构造函数根据C风格字符串创建一个字符串对象
        _str = new char[strlen(str) + 1]; // 动态分配内存空间
        strcpy(_str, str); // 复制字符串内容
    }
    String(const String& s) { // 使用拷贝构造函数根据另一个字符串对象创建一个字符串对象
        _str = new char[strlen(s._str) + 1]; // 动态分配内存空间
        strcpy(_str, s._str); // 复制字符串内容
    }
    String& operator=(const String& s) { // 使用赋值运算符将另一个字符串对象的值赋给当前对象
        if (this != &s) { // 避免自赋值
            delete[] _str; // 释放原来的内存空间
            _str = new char[strlen(s._str) + 1]; // 动态分配内存空间
            strcpy(_str, s._str); // 复制字符串内容
        }
        return *this; // 返回当前对象的引用,方便连续赋值
    }
    ~String() { // 使用析构函数释放动态分配的内存空间
        delete[] _str;
    }
    private:
        char* _str; // 使用指针存储字符串内容
};

智能指针

将基本类型指针封装为类对象指针,并在析构函数里编写delete语句删除指针指向的内存空间。

auto_ptr<string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; vocaticn = ps;

如果 ps 和 vocation 是常规指针,则两个指针将指向同一个 string 对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是 ps 过期时,另一次是 vocation 过期时。要避免这种问题,方法有多种:

1.定义赋值运算符,使之进行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。

2.建立 所有权 (ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。

3.创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为 引用计数。例如,赋值时,计数将加 1,而指针过期时,计数将减 1,当减为 0 时才调用 delete。这是 shared_ptr 采用的策略。

所有的智能指针类都有一个explicit 构造函数,以指针作为参数。因此不能自动将指针转换为智能指针对象,必须显式调用

shared_ptr<double> pd; 
double *p_reg = new double;
pd = p_reg;                               // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)

全部三种智能指针都应避免的一点:

pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。

string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No

auto_ptr(在C++11中被废弃

auto_ptr对象销毁时,它所管理的对象也会自动delete掉。

auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为 NULL,拷贝后原对象变得无效,再次访问原对象时会崩溃。

unique_ptr

由C++11引入,以代替不安全的auto_ptr。

unique_ptr禁止了copy语义,但提供了移动语义,即可使用std::move()来进行控制权限的转移。

它持有对对象的 独有权——两个unique_ptr不能指向同一个对象,即unique_ptr 不共享它所管理的对象。内存资源所有权可以转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源。实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于存 C++ 对象时,可使用 unique_ptr,构造 unique_ptr 时,可使用 make_unique 函数。

//智能指针的创建  
unique_ptr<int> u_i; 	//创建空智能指针
u_i.reset(new int(3)); 	//绑定动态对象  
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d);	//创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

//所有权的变化  
int *p_i = u_i2.release();	//释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());	//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

shared_ptr

shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:

1.shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;

2.时间上的开销主要在初始化和拷贝操作上, * 和 -> 操作符重载的开销跟 auto_ptr 是一样;

3.开销并不是我们不使用 shared_ptr 的理由,,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。

引用计数的核心原理:

shared_ptr内部维护了一个计数器,来跟踪有多少个shared_ptr对象指向了某一个资源。当计数器减少到0的时候,shared_ptr就会调用delete来释放资源。

什么时候增加计数器:

1、新建一个shared_ptr并且指向了一个资源

2、复制构造函数创建了一个新的shared_ptr

3、用赋值运算符将一个shared_ptr给另一个shared_ptr对象赋值时

什么时候减少计数器:

1、当一个shared_ptr对象被销毁时,比如局部变量离开当前作用域;类成员变量析构时。

2、当一个shared_ptr对象不再指向一个资源时,例如通过reset方法或者赋值运算符指向另一个资源时。

环形引用:智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。

weak_ptr(主要是解决shared_ptr循环引用的问题)

weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。

New 和 malloc的区别

malloc实际是返还一段内存区域的中间地址,我们可用的内存只可以是这个地址之后的,地址前面的内存是根据系统固定规则存放着该内存区域的信息,这样free就可以知道malloc出来的地址前面的内容,从而确定整个内存区域的大小。

1、性质不同:new是c++中的一个操作符,malloc是c语言中来分配内存的函数。new只能在c++中使用,但是malloc二者皆可使用。

2、内存分配方式不同:malloc分配的内存是未初始化的,而new不仅分配了内存而且还调用了函数的构造函数来初始化对象。

3、使用语法不同:malloc使用时需要分配内存的大小,例如malloc(sizeof(int)),但new不用,直接使用 new 类型

4、返回类型不同:malloc返回void*指针,需要显示转换成其他指定类型。而new直接返回相应的数据类型的指针,无需类型转换。

        所以new类型安全。new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了。

5、错误处理:内存分配失败后,malloc返回NULL,而new会抛出std::bad_alloc异常

6、配对操作:malloc分配内存用free释放,而new分配用delete释放

使用 new 操作符分配内存,可以使用 free 释放吗?

不可以。new操作符不仅会分配内存,还会调用对象的构造函数,而free函数只会释放内存,不会调用对象的析构函数,这可能会导致内存泄漏或其他问题。使用new操作符分配内存,应该使用delete运算符来释放内存,这样才能保证对象的正确销毁和资源回收。

使用 new[ ]操作符分配内存,可以使用 delete 释放吗?

不可以。new[]操作符和delete运算符是不匹配的,它们的内存管理方式是不同的。new[]操作符会分配一块连续的内存空间,用于存储多个对象,并且会记录对象的数量,而delete运算符只会释放一个对象占用的内存空间,不会考虑对象的数量。使用new[]操作符分配内存,应该使用delete[]运算符来释放内存,这样才能保证所有对象的正确销毁和资源回收。

使用new运算符发生了什么?

1、调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。

2、编译器运行相应的构造函数以构造对象,并为其传入初值。

3、对象构造完成后,返回一个指向该对象的指针。

使用delete运算符发生了什么?

1、调用对象的析构函数

2、编译器调用operator delete(或operator delete[])函数释放内存空间。

重写operator new可以做什么?

重写operator new可以实现自定义的内存分配策略,例如,可以提高内存分配的效率,减少内存碎片,增加内存利用率,或者可以实现内存池,内存跟踪,内存对齐等功能。

Placement new:Placement new是一种特殊的new运算符,它可以在一个已经分配好的内存空间上构造一个对象,而不是从堆上分配新的内存空间。Placement new的语法形式为:

new (address) type (arguments); (address是一个已经分配好的内存空间的地址,type是要构造的对象的类型,arguments是要传递给构造函数的参数。Placement new的作用是调用对象的构造函数,初始化对象的状态,返回一个指向对象的指针。)本质上是对 operator new 的重载,定义于#include 中,它不分配内存,调用合适的构造函数在 ptr 所指的地方构造一个对象,之后返回实参指针ptr

new handler:new handler是一种特殊的函数指针,它可以被注册为一个全局的处理函数,当operator new函数分配内存失败时,会调用该函数,以便进行一些错误处理或资源回收的操作,或者抛出一个异常或终止程序。new handler的类型定义为:

typedef void (*new_handler)();


nothrow:nothrow是一个标准库提供的常量对象,它的类型为:

const std::nothrow_t nothrow;

它可以作为一个参数传递给operator new函数,表示在分配内存失败时不抛出异常而是返回一个空指针。这样可以避免异常处理的开销,也可以让调用者自己检查分配结果,进行相应的处理。例如:
 

int* p = new (std::nothrow) int; // 使用nothrow分配内存
if (p == nullptr) { // 检查分配结果
// 处理内存分配失败的情况
}

重载 

opeartor new /operator delete可以被重载,而malloc/free并不允许重载

特征new/deletemalloc/free
分配内存的位置自由存储区
分配成功返回完整类型指针返回void*
分配失败默认抛出异常返回NULL
分配内存的大小由编译器根据类型计算得出必须显式指定字节数
处理数组有处理数组的new版本new[]需要用户计算数组的大小后进行内存分配
已分配内存的扩充无法直观地处理使用realloc简单完成
是否相互调用可以,看具体的operator new/delete实现不可调用new
分配内存时内存不足客户能够指定处理函数或重新制定分配器无法通过用户代码进行处理
函数重载允许不允许
构造函数与析构函数调用不调用

Unordermap和ordermap区别,在什么场景下用什么?

指针和引用的区别

指针是存放内存地址的一种变量,指针的大小不会像其他变量一样变化。声明时可以 暂时不初始化

指针在使用时一定要做检查,防止空指针、野指针的情况

引用的本质是“变量的别名”,就一定要有本体。声明时就 必须初始化

C++中的引用本质上是一种被限制的指针,引用是占据内存的。

如果一个int指针赋给一个double指针,double的地址是什么?

C++强转是什么?

C++强制类型转换-CSDN博客

dynamic_cast是怎么知道类型的?

因为它存储了运行时类型信息RTTI(runtime type information),这是增加开销和时间的,但是它允许你做动态类型转换之类的事情。

Sizeof的大小是多少?

Vector和list的底层实现和区别是什么?

vector

动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新值大于当前大小才会重新分配内存。

push_back()pop_back()insert(),访问时间是O(1),erase()时间是O(n),查找O(n)

list

双向循环链表

元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供[]操作符的重载。但是由于链表的特点,它可以很有效率地支持在任意地方删除和插入操作。

增删erase()都是O(1),访问O(n),查找头O(1),其余查找O(n)。

vector:快速的随机储存,快速的在最后插入删除元素
需要高效随机储存,需要高效的在末尾插入删除元素,不需要高效的在其他地方插入和删除元素

list:快速在任意位置插入删除元素,快速访问头尾元素
需要大量插入删除操作,不关心随机储存的场景

Vector的扩容机制是怎么做的?

面试题:C++vector的动态扩容,为何是1.5倍或者是2倍_vector扩容-CSDN博客

倍数开辟二倍的内存,旧的数据开辟到新内存,释放旧的内存,指向新内存。

函数的多态是怎么实现的,虚函数是什么?

子类继承基类,为什么使用子类会调用到子类的虚函数,会不会是基类的?

析构函数是什么,做了什么?

基类的析构函数为什么是虚的?

为什么子类要覆写基类的析构函数?

内联函数可不可以是虚函数?

虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联

函数的inline属性是在编译时确定的, 然而,virtual的性质是在运行时确定的,这两个不能同时存在,只能有一个选择,文件中的inline关键字只是对编译器的建议,编译器是否采纳是编译器的事情。

1. 内联函数是个静态行为,而虚函数是个动态行为,他们之间是有矛盾的。

2. 我们之所以能看到一些像内联函数的虚函数,是因为某个函数是否是内联函数不是由我们说的算,而是由编译器决定的。我们只能向编译器建议,某个函数可以是内联函数(inline关键字),但是编译器有自己的判断法则。所以可能出现这样的情况:

        我们用inline声明的函数却没有inline

        我们没有用inline声明的函数却是inline

        对于inline函数,编译器仍然将它编译成一个有地址的函数


inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

#include <iostream>  
using namespace std;
class Base
{
public:
   inline virtual void who()
   {
   	cout << "I am Base\n";
   }
   virtual ~Base() {}
};
class Derived : public Base
{
public:
   inline void who()  // 不写inline时隐式内联
   {
   	cout << "I am Derived\n";
   }
};

int main()
{
   // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,
   // 编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
   Base b;
   b.who();

   // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,
   // 所以不能为内联。  
   Base *ptr = new Derived();
   ptr->who();

   // 因为Base有虚析构函数(virtual ~Base() {}),
   //所以 delete 时,会先调用派生类(Derived)析构函数,
   //再调用基类(Base)析构函数,防止内存泄漏。
   delete ptr;
   ptr = nullptr;

   system("pause");
   return 0;
} 

参考文章:C++面试题:虚函数(virtual)可以是内联函数(inline)吗?_虚函数声明为inline-CSDN博客

什么是拷贝构造函数?

        拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。

一文看懂C++类的拷贝构造函数所有用法(超详细!!!)_类拷贝函数怎么用-CSDN博客

什么情况下要自己写拷贝构造函数?

类有指针数据成员时

        因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。

什么是移动构造函数?

        这里引入了移动语义,所谓移动语义(Move语义),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

        移动语义的具体实现就是C++移动构造函数。

class A {
public:
	int x;
    //构造函数
	A(int x) : x(x)
	{
		cout << "Constructor" << endl;
	}
 
    //拷贝构造函数
	A(A& a) : x(a.x)
	{
		cout << "Copy Constructor" << endl;
	}
 
    //移动构造函数
	A(A&& a) : x(a.x)
	{
		cout << "Move Constructor" << endl;
	}
};

C++11右值引用和移动构造函数详解 - 知乎 (zhihu.com)

深拷贝和浅拷贝区别

浅拷贝

        对于基本数据类型的成员变量,浅拷贝直接进行值传递,也就是将属性值复制了一份给新的成员变量。

        对于引用数据类型的成员变量,比如成员变量是数组、某个类的对象等,浅拷贝就是引用的传递,也就是将成员变量的引用(内存地址)复制了一份给新的成员变量,他们指向的是同一个实例。在一个对象修改成员变量的值,会影响到另一个对象中成员变量的值。

深拷贝

        对于基本数据类型,深拷贝复制所有基本数据类型的成员变量的值。

        对于引用数据类型的成员变量,深拷贝申请新的存储空间,并复制该引用对象所引用的对象,也就是将整个对象复制下来。所以在一个对象修改成员变量的值,不会影响到另一个对象成员变量的值。

虚函数表是什么,原理是什么?

参考文章:C++虚函数表原理浅析 - zhxmdefj - 博客园 (cnblogs.com)C++ 虚函数表剖析 - 知乎 (zhihu.com)

        当一个类在实现时,如果存在一个或以上的虚函数,这个类便会包含一张虚函数表。而当一个子类继承了基类,子类也会有自己的一张虚函数表。

        当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。

        对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址。这张虚函数表是在编译时产生的,否则这个类的结构信息中也不会插入虚指针的地址信息。

        每个类用一个虚函数表,每个类对象用一个虚表指针。

        多重继承的派生类有多个虚函数表,就像一个二维数组。

虚函数表底层是怎么存放的?

虚函数表本质为一个指针数组,虚函数表存的是指向虚函数的指针,所以值就是这些虚函数的地址。

C++的编译器会保证虚函数表的指针存在于对象实例中最前面的位置(为了保证取虚函数表有最高的性能,在有多层继承或是多重继承的情况下),这意味着我们通过对象实例的地址得到这张虚函数表的地址,然后就可以遍历其中函数指针,并调用相应的函数。

pFun = (Fun) * ((int*) * (int*)(&bObj));
    // (Fun) * ((int*) * (int*)(&bObj) + 1);	// Base::g()
    // (Fun) * ((int*) * (int*)(&bObj) + 2);	// Base::h()
没有覆盖:

1、虚函数按照其声明顺序放于表中

2、父类的虚函数在子类的虚函数前

单继承(有覆盖):

1、覆盖的f()函数被放到了虚表中原来父类虚函数的位置

2、没有被覆盖的函数依旧

多继承(无覆盖):

在多继承时加入了多个隐藏成员,也就是说我们现在有多个虚函数表

多继承(有覆盖):

虚函数表是在什么阶段建立的

编译时期。虚表存放在类定义模块的数据段中。模块的数据段通常存放在定义在该模块的全局数据和静态数据,虚表可以看作是模块的全局数据和静态数据。

注:构造函数调用的时候,如果存在虚函数,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。之后进入到构造函数体内。

构造函数为什么不能用虚函数?

虚函数表在构造函数的参数列表初始化。想调用这个构造函数,但是对象都没初始化,虚函数表指针也没有初始化,不能调用。

构造函数里可以用虚函数呢?

虚析构函数是动态多态还是静态多态?

菱形继承怎么解决?

map和hashmap有什么区别

哈希表的底层原理是什么?

你如何构造一个哈希表

Vector什么时候会访问失效(迭代器失效)

迭代器失效的几种情况-CSDN博客

红黑树的底层原理是什么,用法是什么

红黑树的复杂度,哈希表的复杂度

什么时候用map,什么时候用hashmap

Map,你要从里面删一堆数据,你要怎么做

右值引用是什么?为什么要右值引用?

C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;

#include <iostream>
using namespace std;
 
int main()
{
	int num = 10;
	//int && a = num;  //右值引用不能初始化为左值
	int && a = 10;
	a = 100;
	cout << a << endl;//输出100
 
	system("pause");
	return 0;
}

        引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。

什么是匿名函数?匿名函数一般用在哪?

有时候要具体命名函数,有时候要用匿名函数,什么场合?

匿名函数可以节省栈的消耗,为什么? 

动态多态和静态多态的区别?给一个静态多态的具体例子 

静态多态:编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

函数重载:包括普通函数的重载和成员函数的重载

重载函数的关键是函数参数列表——也称函数特征标。包括:函数的参数数目和类型,以及参数的排列顺序。所以,重载函数与返回值、参数名无关

函数模板的使用

        函数模板是通用的函数描述,也就是说,使用泛型来定义函数,其中泛型可用具体的类型(int 、double等)替换。通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。

// 交换两个值,但是不清楚是int 还是 double,如果不使用模板,则要写两份代码
// 使用函数模板,将类型作为参数传递
template<class T>
class Swa(T a,T b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
};
函数签名

为什么C语言中没有重载?

C编译器的函数签名不会记录参数类型和顺序

C++中的函数签名(function signature):包含了一个函数的信息——包括函数名、参数类型、参数个数、顺序以及它所在的类和命名空间,普通函数签名 并不包含函数返回值部分。所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。

调用协议

stdcall是Pascal方式清理C方式压栈,通常用于Win32 Api中,参数入栈规则是从右到左自己在退出时清空堆栈
cdecl是C和C++程序的缺省调用方式,参数入栈规则也是从右至左由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用stdcall函数的大。
fastcall调用的主要特点就是快,通过寄存器来传送参数,从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈

        函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。C语言和C++语言间如果不进行特殊处理,也无法实现函数的互相调用。

C语言:

__stdcall编译后,函数名被修饰为“_functionname@number”
__cdecl编译后,函数名被修饰为“_functionname”
__fastcall编译后,函数名给修饰为“@functionname@nmuber”

C++:

        C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和 按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。

        所以,C++编译器能识别函数特征标的不同,从而实现重载

__stdcall编译后,函数名被修饰为“?functionname@@YG******@Z”
__cdecl编译后,函数名被修饰为“?functionname@@YA******@Z”
__fastcall编译后,函数名给修饰为“?functionname@@YI******@Z”

动态多态:运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法

动态绑定
1.通过基类类型的引用或者指针调用虚函数

静态类型:对象声明时的类型,编译时确定

动态类型:目前所指对象的类型,运行时确定

2.必须是虚函数(派生类一定要重写基类中的虚函数)
class Base
{   
public :
    virtual void FunTest1( int _iTest){cout <<"Base::FunTest1()" << endl;}
};
class Derived : public Base
{
public :
    void FunTest1( int _iTest){cout <<"Derived ::FunTest1()" << endl;}
}

重载(overload):函数名相同,参数列表不同,overload只是在类的内部存在

重写(override):也叫覆盖。子类重新定义父类中有相同名称和参数的虚函数(virtual)

        1.被重写的函数不能是static的,且必须是virtual的

        2.重写函数必须有相同的类型,名称和参数列表

        3.重写函数的访问修饰符可以不同。尽管父类的virtual方法是private的,派生类中重写为public、protected也是可以的。这是因为被virtual修饰的成员函数,无论他们是private/public/protected的,都会被统一写到虚函数表中。

        对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数表,因此就允许子类对这些虚函数进行重写。

虚函数表:见上面

虚析构

        如果析构函数不被声明为虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指向的派生类对象析构不完全。

虚构造

        实例化一个对象时,首先会分配对象内存空间,然后调用构造函数来初始化对象。

        vptr变量是在构造函数中进行初始化的。又因为执行虚函数需要通过vptr指针来调用。如果可以定义构造函数为虚函数,那么就会陷入先有鸡还是先有蛋的循环讨论中。

讲一下volatile关键字

讲一下staticcast与dynamiccast区别

讲一下map和unordered_map的底层原理

map内部实现了一个红黑树(所有元素都是有序的),unordered_map内部实现了一个哈希表(元素的排列是无序的)

map
优点:有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作;内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
缺点:空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
适用:对于那些有顺序要求的问题,用map会更高效一些

unordered_map
优点:因为内部实现了哈希表,因此其查找速度非常的快
缺点:哈希表的建立比较耗费时间
适用:对于查找问题,unordered_map会更加高效一些

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值