【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) 析构函数顺序

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

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

③ 调用基类的析构函数。

相关推荐
界面开发小八哥13 分钟前
「Qt Widget中文示例指南」如何实现一个系统托盘图标?(二)
开发语言·c++·qt·用户界面
月夕花晨37414 分钟前
C++学习笔记(24)
c++·笔记·学习
2301_7756023814 分钟前
二进制读写文件
开发语言
疑惑的杰瑞15 分钟前
[乱码]确保命令行窗口与主流集成开发环境(IDE)统一采用UTF-8编码,以规避乱码问题
java·c++·vscode·python·eclipse·sublime text·visual studio
自身就是太阳16 分钟前
深入理解 Spring 事务管理及其配置
java·开发语言·数据库·spring
喵手20 分钟前
Java零基础-多态详解
java·开发语言·python
running thunderbolt21 分钟前
C++:类和对象全解
c语言·开发语言·c++·算法
阿雄不会写代码25 分钟前
bt量化回测框架,bt.optimize 的详细讲解,bt策略参数优化的功能,比backtrader更简单!
开发语言·python
Bob999844 分钟前
电脑浏览器访问华为路由器报错,无法访问路由器web界面:ERR_SSL_VERSION_OR_CIPHER_MISMATCH 最简单的解决办法!
开发语言·javascript·网络·python·网络协议·华为·ssl
cat_fish_rain1 小时前
使用Qt 搭建简单雷达
开发语言·c++·qt