C++六大默认成员函数

ops/2025/2/6 19:59:25/

C++六大默认成员函数

  • 默认构造函数
  • 默认析构函数
    • RAII技术
      • RAII的核心思想
      • 优点
      • 示例
      • 应用场景
  • 默认拷贝构造
    • 深拷贝和浅拷贝
  • 默认拷贝赋值运算符
  • 移动构造函数(C++11起)
  • 默认移动赋值运算符(C++11起)
  • 取地址及const取地址操作符重载
      • 取地址操作符重载
      • 常量取地址操作符重载
      • 示例代码
      • 输出结果
      • 注意事项
  • 扩展:前置++和后置++重载
      • 重载规则

C++中的六大默认成员函数是编译器在特定条件下自动生成的成员函数,用于管理对象的生命周期和资源操作。它们分别是:

默认构造函数

  • 作用:初始化对象,当类没有显式定义任何构造函数时生成。

  • 生成条件:用户未定义任何构造函数。

  • 注意:若类有其他构造函数(如带参数的构造函数),需显式使用 = default 声明默认构造函数。

class Person
{
public://Person()//{//} 不写的话默认自动生成void GetAge(){std::cout << _age << std::endl;}
private:int _age;
};int main()
{Person p;p.GetAge();
}

其特征如下:

  1. 函数名与类名相同
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
  6. 编译器生成默认的构造函数会对自定类型成员调用的它的默认成员
    函数。C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

默认析构函数

  • 作用:释放对象资源,默认析构函数调用成员变量的析构函数。

  • 生成条件:用户未定义析构函数。

  • 注意:若类管理动态资源(如堆内存),需自定义析构函数以避免内存泄漏

class Person
{
public://Person()//{//} 不写的话默认自动生成void GetAge(){std::cout << _age << std::endl;}~Person(){}
private:int _age;
};int main()
{Person p;p.GetAge();
}

RAII技术

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种管理资源的编程技术。它通过将资源的生命周期与对象的生命周期绑定在一起,利用C++的构造函数和析构函数来自动管理资源,从而避免了手动分配和释放资源可能带来的问题,如内存泄漏、资源未正确释放等。

RAII的核心思想

  • 资源在对象构造时获取:当一个对象被创建时,它的构造函数负责获取所需的资源(例如,动态内存分配、文件打开、网络连接等)。
  • 资源在对象销毁时释放:当对象离开作用域或被显式删除时,其析构函数会自动释放之前获取的资源。

优点

  1. 异常安全性:由于资源管理由构造和析构函数自动处理,即使程序中抛出了异常,也能确保资源得到正确释放。
  2. 简化代码:开发者不需要手动跟踪每个资源的状态,并且可以在不使用显式的try-finally块的情况下保证资源的释放。
  3. 防止资源泄露:只要对象被正确地创建并最终销毁,资源就会被正确释放。

示例

以下是一个简单的例子,展示了如何使用RAII来管理动态分配的内存:

#include <iostream>class ResourceHandler {
private:int* data;
public:// 构造函数:资源获取ResourceHandler() {data = new int(10); // 分配资源std::cout << "Resource acquired." << std::endl;}// 析构函数:资源释放~ResourceHandler() {delete data; // 释放资源std::cout << "Resource released." << std::endl;}void showData() const {std::cout << "Data: " << *data << std::endl;}
};void useResource() {ResourceHandler handler;handler.showData();// 不需要手动释放资源,handler离开作用域时会自动调用析构函数
}int main() {useResource();return 0;
}

在这个例子中,ResourceHandler类负责管理一个整数类型的动态分配内存。构造函数在对象创建时分配资源,而析构函数在对象销毁时释放这些资源。这样就确保了无论函数useResource如何退出(正常结束或因异常退出),资源都会被正确释放。

应用场景

RAII不仅限于内存管理,还可以应用于其他资源类型,如文件句柄、网络套接字、数据库连接等。标准库中的智能指针(如std::unique_ptrstd::shared_ptr)、锁机制(如std::lock_guardstd::unique_lock)都是RAII原则的实际应用案例。通过使用这些工具,可以有效地减少资源管理错误,提高代码的安全性和可靠性。

默认拷贝构造

  • 声明形式:ClassName(const ClassName&)

  • 作用:通过已有对象初始化新对象,默认执行浅拷贝。

  • 生成条件:用户未定义拷贝构造函数。

  • 注意:若类包含指针或动态资源,需自定义深拷贝防止重复释放。

class Person
{
public:Person(){}Person(const Person& person){this->_age = person._age;}~Person(){}void GetAge(){std::cout << _age << std::endl;}
private:int _age;
};int main()
{Person p;p.GetAge();
}

深拷贝和浅拷贝

class Stack
{
public://初始化Stack(){_array = new int[20];}//默认生成拷贝构造//析构~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(s1);
}

这里我没有写实际的拷贝构造函数,这里s2调用的默认的拷贝构造,所以s2_array的地址就是s1中_array的地址,这就叫浅拷贝:
在这里插入图片描述
这样代码就会有问题,因为一个地址会被析构两次:
在这里插入图片描述正确的方法应该是给s2的array开辟一块新的空间:

class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}//析构~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(s1);
}

在这里插入图片描述这样的拷贝我们称为深拷贝,再次运行程序:
在这里插入图片描述

默认拷贝赋值运算符

  • 声明形式:ClassName& operator=(const ClassName&)

  • 作用:将已有对象的值赋给另一个对象,默认浅拷贝。

  • 生成条件:用户未定义拷贝赋值运算符。

  • 注意:需处理自赋值问题,并在资源管理时实现深拷贝。

class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}//析构~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2;s2 = s1;
}

在这里插入图片描述

移动构造函数(C++11起)

  • 声明形式:ClassName(ClassName&&)

  • 作用:通过右值引用“窃取”资源,避免深拷贝开销。

  • 生成条件:用户未定义拷贝操作、移动操作或析构函数。

  • 注意:移动后源对象应处于有效但未定义状态(如空指针)。

class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}void swap(Stack& st){std::swap(_array, st._array);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}//移动构造函数Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0){swap(st);}//析构~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(std::move(s1));
}

在这里插入图片描述

默认移动赋值运算符(C++11起)

  • 声明形式:ClassName& operator=(ClassName&&)

  • 作用:通过右值引用转移资源所有权。

  • 生成条件:同移动构造函数。

  • 注意:需正确处理自移动赋值。

class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}void swap(Stack& st){std::swap(_array, st._array);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}//移动构造函数Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0){swap(st);}//移动复制构造Stack& operator=(Stack&& st){swap(st);st._array = nullptr;st._size = 0;st._capacity = 0;return *this;}//析构~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2;s2 = std::move(s1);
}

在C++中,前置++和后置++运算符可以通过成员函数或非成员函数的形式进行重载。两者的主要区别在于参数列表和返回值:

  • 前置++:增加对象的值,并返回增加后的对象引用。
  • 后置++:首先保存当前对象的状态,然后增加对象的值,最后返回之前保存的对象的副本。

取地址及const取地址操作符重载

在C++中,取地址操作符(&)和常量取地址操作符(const &)通常不需要显式地重载,因为编译器提供了默认的实现,它们分别返回对象或常量对象的内存地址。然而,在某些特定情况下,你可能想要自定义这些操作符的行为。

取地址操作符重载

当你重载取地址操作符时,你通常是为了改变其默认行为,例如返回一个代理对象的地址而不是原始对象的地址。不过这种情况非常少见,大多数时候并不需要这样做。

常量取地址操作符重载

类似地,重载常量版本的取地址操作符也是为了提供特定的行为,但同样,这并不是常见的需求。

示例代码

尽管不常见,这里还是给出如何重载这两种操作符的基本示例:

#include <iostream>class MyClass {
private:int value;
public:MyClass(int val) : value(val) {}// 重载取地址操作符int* operator&() {std::cout << "非const取地址操作符被调用" << std::endl;return &value;}// 重载const取地址操作符const int* operator&() const {std::cout << "const取地址操作符被调用" << std::endl;return &value;}
};int main() {MyClass obj(10);const MyClass constObj(20);int* addr = &obj;       // 调用非const版本const int* constAddr = &constObj; // 调用const版本std::cout << "*addr: " << *addr << std::endl;std::cout << "*constAddr: " << *constAddr << std::endl;return 0;
}

输出结果

非const取地址操作符被调用
const取地址操作符被调用
*addr: 10
*constAddr: 20

在这个例子中,我们为MyClass类重载了取地址操作符和常量取地址操作符。当通过非常量对象调用operator&()时,会调用非常量版本的操作符,并打印一条消息。而当通过常量对象调用operator&()时,则调用常量版本的操作符,并打印另一条不同的消息。

注意事项

  • 谨慎使用:一般情况下,不需要也不建议重载这两个操作符,除非有特别的需求。这是因为它们改变了标准语义,可能会导致混淆或者不可预期的行为。
  • 保持一致性:如果你决定重载这些操作符,请确保它们的行为符合逻辑且一致,避免引入错误。
  • 理解限制:需要注意的是,即使你重载了取地址操作符,也无法阻止使用内置的取地址操作来获取对象的实际地址。例如,&obj总是可以获得obj的实际地址,除非你完全隐藏了对象的访问方式。

总的来说,重载取地址操作符和常量取地址操作符是一种高级技巧,适用于特定场景下的特殊需求。在大多数日常编程任务中,这种重载是不必要的。

扩展:前置++和后置++重载

重载规则

  • 前置++只需要一个参数(即调用该运算符的对象本身),并且通常返回一个指向修改后的对象的引用。
  • 后置++需要两个参数:第一个是调用该运算符的对象本身,第二个是一个int类型的占位参数,用于区分前置和后置形式。后置++返回的是操作前对象的一个副本(通常是通过值返回)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;class Count
{
public://重载后置++Count operator++(){++_count;return *this;}//后置++Count operator++(int){Count temp = *this;++_count;return temp;}int getCount() const {return _count;}
private:int _count = 0;
};int main()
{Count myCounter;std::cout << "Initial count: " << myCounter.getCount() << std::endl;++myCounter; // 调用前置++std::cout << "After prefix increment: " << myCounter.getCount() << std::endl;Count d;d = myCounter++; // 调用后置++std::cout << "After postfix increment: " << d.getCount() << std::endl;return 0;
}

http://www.ppmy.cn/ops/156233.html

相关文章

Github 2025-02-02 php开源项目日报 Top10

根据Github Trendings的统计,今日(2025-02-02统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量PHP项目10Blade项目1Laravel:表达力和优雅的 Web 应用程序框架 创建周期:4631 天开发语言:PHP, BladeStar数量:75969 个Fork数量:24281 次…

手写instanceof、手写new操作符

文章目录 1 手写instanceof2 手写new操作符 1 手写instanceof instanceof&#xff1a;用于判断构造函数的prototype属性是否出现在对象原型链中的任何位置实现步骤&#xff1a; 获取类型的原型。获取对象的原型。一直循环判断对象的原型是否等于构造函数的原型对象&#xff0c…

RabbitMQ深度探索:SpringBoot 整合 RabbitMQ

需创建复合项目 父工程 Maven 依赖&#xff1a; <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.0.RELEASE</version></parent><!-- 父工程要打…

Hive:静态分区(分区语法,多级分区,分区的查看修改增加删除)

hive在建表时引入了partition概念。即在建表时&#xff0c;将整个表存储在不同的子目录中&#xff0c;每一个子目录对应一个分区。在查询时&#xff0c;我们就可以指定分区查询&#xff0c;避免了hive做全表扫描&#xff0c;从而提高查询率。 oracle和Hive分区的区别 orcale在…

sql表的增删改、替换

一、增加 1、向原表的字段中插入多条记录的方法 # mysql中常用的三种插入数据的语句: # insert into表示插入数据&#xff0c;数据库会检查主键&#xff0c;如果出现重复会报错&#xff1b; # replace into表示插入替换数据&#xff0c;需求表中有PrimaryKey&#xff0c; # 或…

网络安全-设备安全加固

网络设备也是网络边界很重要的设备&#xff0c;是整个数据中心的主干道&#xff0c;不能有任何闪失&#xff0c;这两天对Nexus网络设备进行了简单的安全加固&#xff0c;对主要的操作进行了下总结&#xff0c;如下&#xff1a; 1、 在Nexus建立了一个只读帐号&#xff0c;如要…

输出解析器的使用

文章目录 输出解析器的使用Pydantic JSON输出解析器结构化输出解析器 输出解析器的使用 输出解析器的使用主要依靠提示词模板对象的partial方法注入输出指令的字符 串&#xff0c;主要的实现方式是利用PromptTemplate对象的partial方法或在实例化PromptTemplate对象时传递parti…

leetcode刷题记录(七十二)——146. LRU 缓存

&#xff08;一&#xff09;问题描述 146. LRU 缓存 - 力扣&#xff08;LeetCode&#xff09;146. LRU 缓存 - 请你设计并实现一个满足 LRU (最近最少使用) 缓存 [https://baike.baidu.com/item/LRU] 约束的数据结构。实现 LRUCache 类&#xff1a; * LRUCache(int capacity)…