【C++ 面试 - 面向对象】每日 3 题(十一)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/fYaBd

📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~

❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

31. 类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

  1. 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。

    这两种方式的主要区别在于:

    对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

    列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

    另外,有三种情况是必须使用成员初始化列表进行初始化的:

    1. 常量成员的初始化,因为常量成员只能初始化不能赋值

    2. 引用类型

    3. 没有默认构造函数的对象必须使用成员初始化列表的方式进行初始化

  2. 一个派生类构造函数的执行顺序如下:

    ① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

    ② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

    ③ 类类型的成员对象的构造函数(按照初始化顺序)

    ④ 派生类自己的构造函数。

  3. 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++ 的赋值操作是会产生临时对象的,临时对象的出现会降低程序的效率。

32. 说说移动构造函数

  1. 我们用对象 a 初始化对象 b,后对象 a 我们就不再使用了,但是对象 a 的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把 a 对象的内容复制一份到 b 中,那么为什么我们不能直接使用 a 的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

  2. 拷贝构造函数中,对于指针,我们一定要采用深层复制 ,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。

  3. 所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如 a->value)置为 NULL,这样在调用析构函数的时候,由于有判断是否为 NULL 的语句,所以析构 a 的时候并不会回收 a->value 指向的空间;

  4. 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个 move 语句,就是将一个左值变成一个将亡值。

如果只有拷贝构造函数:

cpp 复制代码
class Test
{
public:
    int *p;
    Test(const Test &t)
    {
        p = new int (*(t.p)); cout<<"copy construct"<<endl;
    }
    Test(){ p=new int; cout<<"construct"<<endl; };
    ~Test(){ delete p; cout<<"destruct"<<endl; };
};
 
Test getTest()
{
    return Test();
}
 
void main()
{
    {
        Test t = getTest();
    }
}

使用 vs2012 运行,结果为:

html 复制代码
construct                 //执行 Test()
destruct                  //销毁 t

但需要注意的是,这是 vs 编译器对拷贝构造函数优化后的结果。禁止优化,结果为:

html 复制代码
construct                 //执行 Test()
copy construct            //执行 return Test()
destruct                  //销毁 Test() 产生的匿名对象
copy construct            //执行 t = getTest()
destruct                  //销毁 getTest() 返回的临时对象
destruct                  //销毁 t

可以看到,进行了两次的深拷贝,对于对内存要求不高、本例这种占内存比较小的类 Test 而言(申请的堆空间小),可以接受。

但如果临时对象中的指针成员申请了大量的堆空间,那将严重影响程序的执行效率。

C++11 为了解决这一问题(深拷贝占用大量空间 ),引入移动构造函数

所谓的移动,就是将其他的内存资源,"移为己有",这些资源通常是临时对象 ,比如右值

cpp 复制代码
class Test
{
 public:
 int *p;
 Test(Test &&t) //移动构造函数
 {
     p = t.p;
     t.p = nullptr; //将临时对象的指针赋值为空
     cout<<"move construct"<<endl;
 }
 Test(const Test &t) //拷贝构造函数
 {
     p = new int (*(t.p));
     cout<<"copy construct"<<endl;
 }
  Test(){ p=new int; cout<<"construct"<<endl; };
  ~Test(){ delete p; cout<<"disconstruct"<<endl; };
};
Test getTest()
{
    return Test();
}
void main()
{
    {
        Test t = getTest();
    }
}

禁止 vs 优化,结果为:

html 复制代码
construct                 //执行 Test()
move construct            //执行 return Test()
destruct                  //销毁 Test() 产生的匿名对象
move construct            //执行 t = getTest()
destruct                  //销毁 getTest() 返回的临时对象
destruct                  //销毁 t

可以看到,定义了移动构造函数后,临时对象的创建使用移动构造函数创建,没有在堆上创建对象,减少了开销。

33. 构造函数、析构函数的执行顺序?

1) 构造函数顺序

① 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。但是在有虚继承和一般继承存在的情况下,优先虚继承。

② 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

③ 派生类构造函数。

2) 析构函数顺序

① 调用派生类的析构函数;

② 调用成员类对象的析构函数;

③ 调用基类的析构函数。

相关推荐
秋意钟5 分钟前
Spring新版本
java·后端·spring
青莳吖9 分钟前
Java通过Map实现与SQL中的group by相同的逻辑
java·开发语言·sql
Buleall17 分钟前
期末考学C
java·开发语言
重生之绝世牛码19 分钟前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
小蜗牛慢慢爬行25 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
Algorithm157634 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明44 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
A小白59081 小时前
Docker部署实践:构建可扩展的AI图像/视频分析平台 (脱敏版)
后端
Monly211 小时前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu1 小时前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa