c++类临时对象、复制构造函数、析构函数 VS2017 与gcc 编译器对比

探讨C++中函数调用时临时对象的创建机制,对比VS2017与g++的不同行为,深入分析参数传递及返回过程中的对象复制细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在看《C和C++程序员面试秘籍》的时候,看到第6章28与29条,有些疑惑:

  • 作者说函数调用时,参数传递进来,以及返回时,都会构造临时对象。
    也就是说,会有两个临时对象被创建,一个是在参数传递进来的时候,一个是在返回的时候。

先说实验结论:

  • 书中的代码在 VS2017中实测没有问题,输出结果完全一致;
  • 但在 GCC 下,代码有问题,输出结果有一些不一致(即:g++ 编译器在函数返回时,不一定创建临时变量;且不接受临时构建的类对象作为复制构造函数的实参)。

表述的可能不严谨,实验如下

先写一个用来测试的类,其所有的构造函数,赋值operator等,只做基本的数据赋值,和输出信息:

myclass.h 内容:

#ifndef _MY_CLASS_
#define _MY_CLASS_

#include <iostream>
#include <string>
namespace myclass {

class A {
public:
  // 默认构造函数
  A() : data_(0), name_("default") {
    count_++;
    std::cout << "default constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }
  // 含参构造函数
  A(int data, std::string name = "default") : data_(data), name_(name) {
    count_++;
    std::cout << "param constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }
  // 复制构造函数
  A(A &other) {
    count_++;
    data_ = other.data_;
    name_ = other.name_ + "_copy";
    std::cout << "copy constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }

  // 下面是右值复制构造函数,没有这个函数,面试题28在 g++ 编译器下无法编译通过,会输出如下错误信息:
  // error: invalid initialization of non-const reference of type 'myclass::A&' from an rvalue of type 'myclass::A'
  //   A t1 = Play(5);
  // 意思是临时类对象(右值)无法用来初始化类的左值引用(即:A&)
  // 但在VS2017下,没有这个右值复制构造函数,是可以编译通过的,会调用上面定义的复制构造函数

  A(A &&other) {
    count_++;
    data_ = other.data_;
    name_ = other.name_ + "_copy";
    std::cout << "copy [rvalue] constructor, data: " << data_
              << ", name: " << name_ << ", count: " << count_ << std::endl;
  }

  A &operator=(const A &a);

  // name_ setter and getter
  const std::string &name() { return name_; }
  void set_name(const std::string &name) { name_ = name; }
  // destructor
  virtual ~A() {
    std::cout << "destructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }

  static int count_;

private:
  int data_;
  std::string name_;
};

int A::count_ = 0;

A &A::operator=(const A &a) {
  std::cout << "operator = function, before assignment data: " << data_
            << ", name:" << name_ << std::endl;
  if (this == &a) {
    std::cout << "                   afeter assignment[same obj] data: "
              << data_ << ", name:" << name_ << std::endl;
    return *this;
  }

  data_ = a.data_;
  name_ = a.name_;
  std::cout << "                     afeter assignment data: " << data_
            << ", name:" << name_ << std::endl;
  return *this;
}

} // namespace myclass

#endif

A 里面的数据只有一个 data_ 和一个 name_ ,和一个静态变量 count_ , 它们主要用来输出的时候查看是哪一个类被创建和析构。

面试题28:

主函数(main_28.cpp):

#include <iostream>
#include <string>
#include "myclass.h"

using namespace std;
using namespace myclass;

A Play(A a) { return a; }

int main(int argc, char *argv[]) {
  A t1 = Play(5);
  cout << "t1.name = " << t1.name() << endl;
  t1.set_name("t1");

  A t2 = Play(t1);
  cout << "t2.name = " << t2.name() << endl;
  t2.set_name("t2");

  return 0;
}

运行结果如下:

  • VS2017 输出

param constructor, data: 5, name: default, count: 1
copy [rvalue] constructor, data: 5, name: default_copy, count: 2
destructor, data: 5, name: default, count: 2
t1.name = default_copy
copy constructor, data: 5, name: t1_copy, count: 3
copy [rvalue] constructor, data: 5, name: t1_copy_copy, count: 4
destructor, data: 5, name: t1_copy, count: 4
t2.name = t1_copy_copy
destructor, data: 5, name: t2, count: 4
destructor, data: 5, name: t1, count: 4

  • g++ 6.2.0 输出:

    param constructor, data: 5, name: default, count: 1
    copy [rvalue] constructor, data: 5, name: default_copy, count: 2
    destructor, data: 5, name: default, count: 2
    t1.name = default_copy
    copy constructor, data: 5, name: t1_copy, count: 3
    copy [rvalue] constructor, data: 5, name: t1_copy_copy, count: 4
    destructor, data: 5, name: t1_copy, count: 4
    t2.name = t1_copy_copy
    destructor, data: 5, name: t2, count: 4
    destructor, data: 5, name: t1, count: 4


可以看到,结果完全一样。
其中,第2行输出说明:临时变量在传入Play() 函数时,确实利用copy构造函数构造了临时类对象(name: default_copy),但是在函数返回时,并没有构造另一个临时变量 default_copy_copy(也就是说这次函数调用,只有在参数传入时,有临时变量生成,函数返回并没有生成另一个临时变量)。用t1赋值给t2时,也是如此。

面试题29:

我们再看面试题29,情况就不一样了,在函数返回时,VS2017确实生成了一个临时类对象,而g++依然没有生成。

主函数( main_29.cpp ):

#include <iostream>
#include "myclass.h"

using namespace std;
using namespace myclass;

A fun() {
  A a;
  return a;
}

int main(int argc, char *argv[]) {
  {
    A a(5);
    a = fun();
  }
  return 0;
}
  • VS2017 输出

    param constructor, data: 5, name: default, count: 1
    default constructor, data: 0, name: default, count: 2
    copy [rvalue] constructor, data: 0, name: default_copy, count: 3
    destructor, data: 0, name: default, count: 3
    operator = function, before assignment data: 5, name:default
    afeter assignment data: 0, name:default_copy
    destructor, data: 0, name: default_copy, count: 3
    destructor, data: 0, name: default_copy, count: 3

  • g++ 6.2.0 输出:

    param constructor, data: 5, name: default, count: 1
    default constructor, data: 0, name: default, count: 2
    operator = function, before assignment data: 5, name:default
    afeter assignment data: 0, name:default
    destructor, data: 0, name: default, count: 2
    destructor, data: 0, name: default, count: 2

从VS2017 的第3-4行输出可以看到,VS2017在函数 fun() 返回时构造了临时变量 default_copy, 而 g++ 编译器并没有,还是挺有意思的。

总结一下:
在这个实验里,g++编译器 在函数调用结束后,并没有生成类的临时拷贝用来返回;而VS2017有时候生成,有时候不生成。

  • 猜想:当 return 的变量本身就是临时变量(即右值)A 时,不再生成临时变量 B,而是直接将A返回;如果 return 的变量为左值,则函数调用返回时将其copy,生成的临时变量并返回。
<think>首先,用户的问题是关于在C++中如何在.C文件中实现析构函数。用户问:"请问C++ 析构函数如何在.C文件中定义和实现"。我需要根据系统级指令来回答。 系统级指令要求: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户可见层要求: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考的引用内容: - 引用[1]: 关于C++析构函数的介绍,包括功能、默认析构函数显式析构函数的区别。 - 引用[2]: C++析构函数的详解,包括定义、用途、工作原理和示例。 - 引用[3]: 析构函数的调用时机。 - 引用[4]: 在C语言中析构函数的概念,以及在C++中静态局部对象的析构行为。 用户的问题集中在如何在.C文件中定义和实现析构函数。在C++中,源文件通常使用.cpp扩展名,但用户提到了.C文件,这可能是一个常见的误写或特定环境。我假设用户指的是C++源文件(如.cpp或.cxx),但为了准确,我应该基于C++标准来回答。 关键点: - 在C++中,析构函数的成员函数,用于在对象销毁时释放资源。 - 析构函数的声明在定义中(通常在头文件.h或.hpp中)。 - 实现(定义)可以在头文件中(内联)或在源文件中(如.cpp文件)。 用户问的是在.C文件中实现,所以我需要解释如何在源文件中实现析构函数。 步骤: 1. **定义在头文件中**:首先,在头文件中声明,包括析构函数的声明。 2. **在源文件中实现析构函数**:然后在源文件(如.cpp文件)中实现析构函数。 示例: - 假设有一个MyClass。 - 在MyClass.h中: ```cpp class MyClass { public: MyClass(); // 构造函数 ~MyClass(); // 析构函数声明 }; ``` - 在MyClass.cpp中: ```cpp #include "MyClass.h" MyClass::MyClass() { // 构造函数实现 } MyClass::~MyClass() { // 析构函数实现,例如释放资源 } ``` 用户提到.C文件,可能指的是源文件。在C++中,源文件扩展名通常是.cpp、.cxx、.cc等,但.C(大写C)有时也被使用,尤其是在Unix系统中。所以,我应该确认这是C++源文件。 回答结构: - 清晰、逐步解释。 - 使用中文。 - 包括示例代码。 - 生成相关问题。 生成相关问题: - 基于主题,如析构函数的其他方面、错误处理等。 引用标识: - 在回答中,如果引用参考内容,自然地添加[^n]。但用户提供的引用是关于析构函数的一般信息,不是针对具体实现。我需要确保引用相关部分。 例如: - 引用[2]提到"资源管理",我可以引用它来强调析构函数的重要性。 - 引用[3]讨论调用时机。 - 引用[4]提到静态对象的析构。 在回答中,自然地融入引用。 完整回答框架: 1. 介绍:简要解释析构函数的作用和在源文件中实现的重要性。 2. 步骤:分步说明如何在头文件中声明和在源文件中实现。 3. 示例:提供代码示例。 4. 注意事项:讨论关键点,如为什么需要显式实现、内存管理等。 5. 相关问题:生成2-3个相关问题。 确保LaTeX:用户的问题中没有数学表达式,但如果有,我会使用。例如,如果有公式,如$O(n)$复杂度,但在析构函数上下文中可能不相关。 最终回答: - 用中文。 - 结构清晰。</think>### C++ 析构函数在源文件(.C 文件)中的定义和实现 在 C++ 中,析构函数的成员函数,用于在对象生命周期结束时自动释放资源(如动态内存、文件句柄等),避免内存泄漏。默认情况下,编译器会生成一个隐式析构函数,但如果涉及资源管理(如指针成员),必须显式定义析构函数。源文件(通常扩展名为 `.cpp`、`.cxx` 或 `.C`)用于实现析构函数的具体逻辑,而声明通常在头文件(`.h` 或 `.hpp`)中完成。下面我将逐步解释如何实现。 #### 步骤 1: 在头文件中声明析构函数 首先,在头文件中声明和其析构函数析构函数使用 `~` 符号前缀,声明在 `public` 区域以确保对象销毁时可访问。 ```cpp // MyClass.h #ifndef MYCLASS_H #define MYCLASS_H class MyClass { public: MyClass(); // 构造函数声明 ~MyClass(); // 析构函数声明(注意:无参数,无返回值) private: int* data; // 示例:指针成员,需要显式资源释放 }; #endif // MYCLASS_H ``` 这里,`~MyClass()` 是析构函数的声明。如果涉及动态资源(如 `data` 指针),显式定义析构函数是必要的,否则会导致资源泄漏[^2]。 #### 步骤 2: 在源文件(.C 文件)中实现析构函数 在源文件(例如 `MyClass.C` 或 `MyClass.cpp`)中实现析构函数。使用名和作用域解析运算符 `::` 来定义函数体,确保包含头文件。 ```cpp // MyClass.C #include "MyClass.h" #include <iostream> // 构造函数实现 MyClass::MyClass() { data = new int[100]; // 动态分配内存 std::cout << "构造函数调用:资源已分配。" << std::endl; } // 析构函数实现:释放资源 MyClass::~MyClass() { delete[] data; // 释放动态内存 std::cout << "析构函数调用:资源已释放。" << std::endl; } ``` - **关键点**: - 析构函数名必须名完全一致,包括 `~` 符号。 - 在实现中,通过 `MyClass::~MyClass()` 指定作用域。 - 析构函数对象销毁时自动调用(如局部对象超出作用域、`delete` 操作或程序结束)[^3]。 #### 注意事项 1. **资源管理**:析构函数常用于释放动态分配的内存、关闭文件或网络连接。如果未显式定义,编译器生成的默认析构函数可能无法正确释放资源,导致泄漏[^2]。 2. **调用时机**:析构函数在以下情况调用: - 局部对象:函数结束时(如 `main()` 退出)。 - 动态对象:使用 `delete` 时。 - 静态对象:程序结束时(但静态局部对象只在首次定义时初始化,析构在程序结束时调用)[^4]。 3. **错误处理**:析构函数不应抛出异常,否则可能导致未定义行为。使用 `noexcept` 关键字可增强安全性。 4. **文件扩展名**:`.C` 文件在 C++ 中是有效的源文件扩展名(尤其在 Unix 系统),但需确保编译器C++ 编译(如使用 `g++` 而非 `gcc`)。 #### 完整示例 假设一个简单的文件管理: ```cpp // FileHandler.h class FileHandler { public: FileHandler(const char* filename); ~FileHandler(); // 声明析构函数 void writeData(const char* data); private: FILE* file; // 文件指针 }; // FileHandler.C #include "FileHandler.h" #include <cstdio> FileHandler::FileHandler(const char* filename) { file = fopen(filename, "w"); if (!file) { perror("文件打开失败"); } } FileHandler::~FileHandler() { if (file) { fclose(file); // 确保文件关闭 std::cout << "文件已关闭。" << std::endl; } } void FileHandler::writeData(const char* data) { if (file) { fprintf(file, "%s\n", data); } } ``` 在此示例中,析构函数 `~FileHandler()` 在源文件中实现,确保文件资源被安全释放[^2]。 ### 总结 在 C++ 中,在源文件(如 `.C` 文件)中实现析构函数需: 1. 在头文件中声明析构函数(如 `~ClassName();`)。 2. 在源文件中使用 `ClassName::~ClassName() { ... }` 实现资源释放逻辑。 3. 确保析构函数处理所有动态资源,避免泄漏。显式定义析构函数是资源管理的核心实践,尤其在涉及指针或外部资源时[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值