C++之旅(学习笔记)第6章 基本操作

C++之旅(学习笔记)第6章 基本操作

6.1 基本操作

class X{
public:
    X(Sometype);			// "普通的构造函数": 创建一个对象
    X();					// 默认构造函数
    X(const X&);			// 拷贝构造函数
    X(X&&);					// 移动构造函数
    X& operator=(const X&);	// 拷贝赋值操作符:清空目标对象并拷贝
    X& operator=(X&&);		// 移动赋值操作符:清空目标对象并移动
    ~X();					// 析构函数:清理资源
    //...
};

在下面5种情况下,对象会被移动或拷贝:

  • 赋值给其他对象。
  • 作为对象初始化。
  • 作为函数的实参。
  • 作为函数的返回值。
  • 作为异常。

如果希望显示地使用函数的默认实现:在函数后面加上 =default

class Y{
public:
    Y(Sometype);
    Y(const Y&) = default;	// 我确实需要默认的拷贝构造函数
    Y(Y&&) = default;		// 也确实需要默认的移动构造函数
};
  • 一旦显示地指定了某些函数地默认形式,编译器就不会再为函数生成其他默认定义了。
  • 当类中含有指针成员时,最好显示地指定拷贝操作和移动操作。如果不这样做,当编译器生成地默认函数试图delete指针对象时,系统将发生错误。

如果不想生成目标操作函数:在函数后面加上=delete

class Shape {
public:
    Shape(const Shape&) =delete;		// 禁止拷贝
    Shape& operator=(const Shape&) =delete;
    //...
};
void copy(Shape& s1, const Shape& s2)
{
    s1 = s2;							// 错误:Shape禁止拷贝
}
  • 试图使用=delete的函数会在编译时报错;=delete可以用于禁用任意函数,并非仅仅用于禁用基础成员函数。

6.1.2 转换

接受单个参数的构造函数同时定义了从参数类型到类类型的转换。

例如:complex提供了一个接受double类型的参数的构造函数:

class complex {
    double re,im;		// 成员变量:两个双精度浮点数
public:
    complex(double r,double i):re{r},im{i}{}	// 用两个标量构建该复数
    complex(double r):re{r},im{0}{}				// 用一个标量构建该复数
    complex():re{0},im{0}{}						// 默认的复数是:{0,0}
    //...
};
complex z1 = 3.14;	// z1变成 {3.14,0.0}
complex z2 = z1*2;	// z2变成 z1*{2.0,0} == {6.28,0.0}
  • 这种转换有时似乎合情理,有时则不然。

例如:Vector提供了一个接受int类型的参数构造函数:

class Vector{
private:
    double* elem;
    int sz;
public:
    Vector(int s):elem{new double[s]},sz{s}
    {
        for(int i = 0; i != s; ++i)
            elem[i] = 0;
    }
    ~Vector(){delete[] elem;}
    // ...
};
Vector v1 = 7;		// 可行:v1有7个元素
  • 通常情况下,该语句的执行结果并非如我们的预期,标准库vector禁止这种int到vector的转换。

解决该问题的办法是只允许显示进行类型转换:

class Vector{
public:
    explicit Vector(int s);	// 不能隐式地将int转化为Vector
    // ...
};
Vector v1(7);		// 可行:v1有7个元素
Vector v2 = 7;		// 错误:不能隐式地将int转化为Vector
  • 关于类型转换地问题,complex只是一小部分,大多数类型地情况与Vector类似。

所以除非你有充分地理由,否则最好把接受单个参数的构造函数声明成explicit的。

6.1.3 成员初始值设定项

定义类的数据成员时,可以提供默认的初始值,称其为默认成员初始值设定项。

如下:修订版本的complex

class complex {
    double re = 0;
    double im = 0;	// 表示两个默认值为0.0的double类型的成员
public: 
    complex(double r, double i) : re{r}, im{i} {}	// 从两个标量{r,i}构造complex
    complex(double r) : re{r} {}					// 从一个标量{r,0}构造complex
    complex() {}									// 默认值为{0,0}的complex
    // ...
};

对于所有构造函数没有提供初始值的成员,默认初始值都会起作用。

6.2 拷贝和移动

默认情况下,我们可以拷贝对象,不论是用户自定义类型的对象还是内置类型的对象。

拷贝的默认含义是逐成员地复制,即依次复制每个成员。

6.2.1 拷贝容器

当一个类被作为资源句柄时,换句话说,当这个类负责通过指针访问一个对象时,采用默认的逐成员复制方式通常意味着会产生灾难性的错误。

逐成员复制的方式会违反资源句柄的约束条件。

例如:默认拷贝将产生Vector的一份拷贝,而这个拷贝所指向的元素与原来的元素是同一个:

void bad_copy(Vector v1)
{
    Vector v2 = v1;	// 将v1的表层拷贝到v2
    v1[0] = 2;		// v2[0]也变成了2
    v2[1] = 3;		// v1[1]也变成了3
}

假设v1包含4个元素,则结果如下图所示:

在这里插入图片描述

类对象的拷贝操作可以通过两个成员来定义:拷贝函数与拷贝赋值操作符:

class Vector {
public:
    Vector(int s);
    ~Vector() { delete[] elem; }
    
    Vector(const Vector& a);				// 拷贝构造函数
    Vector& operator=(const Vector& a);		// 拷贝赋值操作符
    
    double& operator[](int i);
    const double& operator[](int i) const;
    
    int size() const;
private:
    double* elem;
    int sz;
};

对Vector来说,拷贝构造函数的正确定义应该首先为指定数量的元素分配空间,然后把元素复制到空间中。这样复制完成后,每个Vector就拥有自己的元素拷贝了:

Vector::Vector(const Vector& a) : elem {new double[a.sz]}, sz{a.sz}	//拷贝构造函数,分配元素所需要的空间
{
    for(int i = 0; i != sz; ++i)
        elem[i] = a.elem[i];
}

在这个示例中,v2=v1的结果现在可以表示成:

在这里插入图片描述

除了拷贝构造函数,我们还需要一个拷贝赋值操作符:

Vector& Vector::operator=(const Vector& a) 	// 拷贝赋值操作
{
    double* p = new double[a.sz];
    for(int i = 0; i != a.sz; ++i)
        p[i] = a.elem[i];
    delete[] elem;							// 删除旧元素
    elem = p;
    sz = a.sz;
    return *this;
}

其中,名字this被预定义在成员函数中,它指向调用该成员函数的那个对象。

元素拷贝发生在旧元素被删除之前,所以如果在拷贝的过程中抛出异常,Vector的旧值可得以保留。

6.2.2 移动容器

对于大容量的容器,拷贝过程有可能消耗巨大。

当给函数传递对象时,可通过使用引用类型来减少拷贝对象的代价,但是无法返回局部对象的引用(函数的调用者都没机会和返回结果碰面,局部对象就被销毁了)。

我们相比于拷贝一个Vector对象,更希望移动它。

class Vector {
    // ...
    Vector(const Vector& a);			// 拷贝构造函数(复制构造)
    Vector& operator=(const Vector& a);	// 拷贝赋值操作符(复制赋值)
    
    Vector(Vector&& a);					// 移动构造函数
    Vector& operator=(Vector&& a);		// 移动赋值操作符
};

基于上述定义,编译器将选择移动构造函数来执行从函数中移出返回值的任务。

定义Vector移动构造函数的过程非常简单:

Vector::Vector(Vector&& a) : elem {a.elem}, sz{a.sz}
{
    a.elem = nullptr;	//现在a中没有任何元素
    a.sz = 0;
}

符号&&的意思是”右值引用“,右值的含义与左值正好相反。

左值的大致含义是 ”能出现在赋值操作符左侧的内容“,因此右值大致上就是无法为其赋值的值,比如函数调用返回的一个整数就是右值。进一步地,右值引用地含义就是引用了一个别人无法赋值地内容,所以我们可以安全地”窃取“它的值。

移动构造函数不接受const实参:毕竟移动构造函数最终要删除它实参中的值。

6.3 资源管理

通过定义构造函数、拷贝操作、移动操作和析构函数,程序员就能对受控资源(比如容器中元素)的生命周期进行完全控制。

内存也不是唯一的一种资源。资源是指任何在使用前需要获取与(显示或隐式)释放的东西,除了内存,还有锁、套接字、文件句柄和线程句柄等非内存资源。

在C++标准库中,RAII无处不在:例如,内存(string、vector、map、unordered_map等)、文件(ifstream、ofstream等)、线程(thread)、锁(lock_guard、unique_lock等)和通用对象(通过unique_ptr和shared_ptr访问)。

6.4 建议

  1. 尽量让对象的构造、拷贝(复制)、移动和销毁、在掌控之中;

  2. 同时定义所有的基本操作,或者什么都不定义;

  3. 如果默认的构造函数、赋值操作符和析构函数符合要求、那么让编译器负责生成它们;

  4. 如果类含有指针成员,考虑这个类是否需要用户自定义或者删除析构函数、拷贝函数及移动函数;

  5. 默认情况下,把单参数的构造函数声明成explicit的;

  6. 如果默认拷贝函数不适合当前类型,则重新定义或禁止拷贝函数;

  7. 用传值的方式返回容器(依赖拷贝消除和移动以提高效率);

  8. 避免显示使用std::copy();

  9. 对于容量较大的操作数,使用const引用作为参数类型;

  10. 使用RAII管理所有资源——内存和非内存资源;

  11. 如果默认的构造函数、赋值操作符和析构函数符合要求、那么让编译器负责生成它们;

  12. 如果类含有指针成员,考虑这个类是否需要用户自定义或者删除析构函数、拷贝函数及移动函数;

  13. 默认情况下,把单参数的构造函数声明成explicit的;

  14. 如果默认拷贝函数不适合当前类型,则重新定义或禁止拷贝函数;

  15. 用传值的方式返回容器(依赖拷贝消除和移动以提高效率);

  16. 避免显示使用std::copy();

  17. 对于容量较大的操作数,使用const引用作为参数类型;

  18. 使用RAII管理所有资源——内存和非内存资源;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/129668.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

规划文献阅读——Obstacle Avoidance, Path Planning and Control for Autonomous Vehicles

摘要 自动驾驶汽车避障需要三个主要层面,即感知、路径规划和制导控制。在本文中,考虑到这三个层次之间的联系,提出了一个全局架构。在环境感知层面,采用基于证据占用网格的方法进行动态障碍物检测。因此,考虑物体的姿…

鸿蒙原生应用开发-DevEco Studio远程模拟器的使用

使用单设备模拟器运行应用/服务 Remote Emulator支持Phone、Wearable、Tablet、TV等设备类型,但不同区域(开发者帐号注册地)支持的设备类型可能不同,请以实际可申请的设备类型为准。 Remote Emulator中的单设备模拟器&#xff08…

Sprint Boot 学习路线 3

嵌入式服务器 Spring Boot 的嵌入式服务器功能是一项方便而强大的功能,它允许你在应用程序中直接运行 Web 服务器,无需将其部署到单独的独立 Web 服务器中。这使得开发、测试和部署 Web 应用程序变得容易,而且它还是轻量级的、易于启动和停止…

linuxC语言缓冲区及小程序的实现

文章目录 1.文件缓冲区1.1介绍1.2缓冲文件系统1.3冲刷函数fflush1.4认识linux下的缓冲区 2.linux小程序的实现2.1 回车\r和换行\n2.2倒计时程序2.3进度条小程序sleep/usleep代码运行结果 1.文件缓冲区 1.1介绍 为缓和 CPU 与 I/O 设备之间速度不匹配,文件缓冲区用以…

【Maven教程】(十):使用 Hudson 进行持续集成—— 从Hudson的安装到任务创建 ~

Maven 使用 Hudson 进行持续集成 1️⃣ 持续集成的作用、过程和优势2️⃣ Hudson 简介与安装3️⃣ 准备 Subversion 仓库4️⃣ Hudson 的基本系统设置5️⃣ 创建 Hudson 任务5.1 Hudson 任务的基本配置5.2 Hudson 任务的源码仓库配置5.3 Hudson 任务的构建触发配置5.4 Hudson …

python之SPC:计算Cpk

目录 1、Ca、Cp和Cpk的理解 2、python计算Cp,Cpk与Pp,Ppk 3、总结 1、Ca、Cp和Cpk的理解 Ca、Cp和Cpk是制程能力指数,它们分别代表制程准确度、制程精密度和制程能力指数。 制程准确度(Ca)反映实际平均值与规格中心值之一致性。对于单边…

GF0-57CQD-002 测量参数:加速度、速度、位移–现场可配置

GF0-57CQD-002 测量参数:加速度、速度、位移–现场可配置 GF0-57CQD-002 是一款创新的双通道变送器,专为精确的振动测量而设计。它激励并读取来自加速度计的信号,并将整体振动值作为电流/电压信号传输。它测量加速度、速度和位移等不同参数的振动。配置…

竞赛 车道线检测(自动驾驶 机器视觉)

0 前言 无人驾驶技术是机器学习为主的一门前沿领域,在无人驾驶领域中机器学习的各种算法随处可见,今天学长给大家介绍无人驾驶技术中的车道线检测。 1 车道线检测 在无人驾驶领域每一个任务都是相当复杂,看上去无从下手。那么面对这样极其…

Vxe table - 基于Vue的宝藏级 table 组件

文章目录 前言一、Vxe-table功能点计划 二,安装三,引入四,示例用法 前言 对于表格来说,也许我们会遇到一个需求就是表格中的单元格可编辑,如果我们使用的是ElementUI也许不太好办,因为官方没有可编辑的这个…

Spring封装数据结果

Spring封装数据结果 POST请求JSON格式 基本数据类型 public class Demo {private byte aByte;private short aShort;private int anInt;private long aLong;private float aFloat;private double aDouble;private char aChar;private boolean aBoolean; }没有传键 封装时就会…

【Spring】SpringBoot配置文件

SpringBoot配置文件 配置文件作用SpringBoot配置文件配置文件快速入手配置文件的格式properties配置文件说明基本语法读取配置文件properties缺点分析 yml配置文件说明yml基本语法yml使用进阶yml配置读取配置对象配置集合配置Mapyml优缺点 配置文件作用 计算机上有数以千计的配…

Unity 一些内置宏定义

在Unity中,有一些内置的宏定义可用于不同的平台。以下是一些常见的平台内置宏定义: 1、UNITY_EDITOR:在Unity编辑器中运行。 2、UNITY_EDITOR_WIN:在Unity编辑器运行在Windows操作系统时被定义。 3、UNITY_STANDALONE&#xff1a…

Linux学习第37天:Linux I2C 驱动实验(一):哥俩好

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 世界上的很多事物都是成双成对出现的。也包括在驱动开发的过程中,比如I2C中其实就是数据线和时钟线的相互配合才能完成的。 I2C常用于连接各种外设、…

Ubuntu 22.04 安装水星无线 USB 网卡

我的 USB 网卡是水星 Mercury 的, 在 Ubuntu 22.04 下面没有自动识别。 没有无线网卡的时候只能用有线接到路由器上,非常不方便。 寻思着把无线网卡驱动装好。折腾了几个小时装好了驱动。 1.检查网卡类型 & 安装驱动 使用 lsusb 看到的不一定是准确…

node插件MongoDB(四)—— 库mongoose 的条件控制(三)

文章目录 前言一、运算符二、逻辑运算1. $or 逻辑或2. $and 逻辑与 三、正则匹配 前言 在mongodb 不能使用 > < > < ! 等运算符&#xff0c;需要使用替代符号。 一、运算符 > 使用 $gt< 使用 $lt> 使用 $gte< 使用 $lte! 使用 $ne 例子&#xff1a;获…

Mysql 一步到位实现插入或替换数据(REPLACE INTO语句)

单条数据插入/替换 比如有一个数据表叫test_table&#xff0c;包含: 主键&#xff1a;key_id数据&#xff1a;value 运行&#xff1a; REPLACE INTO test_table (key_id,value) VALUES ("id_1","value_1"); REPLACE INTO test_table (key_id,value) VAL…

Qt 各种数据类型

目录 1. 基础类型 2. log 输出 3. 字符串类型 3.2 QByteArray 构造函数 数据操作 子字符串查找和判断 遍历 查看字节数 类型转换 3.3 QString 4. QVariant 4.1 标准类型 4.2 自定义类型 5. 位置和尺寸 5.1 QPoint 5.2 QLine 5.3 QSize 5.4 QRect 6. 日期和…

gcc [linux]

目录 背景知识 gcc如何完成 格式 预处理&#xff08;进行宏替换&#xff09; 编译&#xff08;生成汇编&#xff09; 汇编&#xff08;生成机器可执行码&#xff09; 连接&#xff08;生成可执行文件或库文件&#xff09; 函数库 静态库 静态链接优势 动态库 动态链…

Wsl2 Ubuntu在不安装Docker Desktop情况下使用Docker

目录 1. 前提条件 2.安装Distrod 3. 常见问题 3.1.docker compose 问题无法使用问题 3.1. docker-compose up报错 参考文档 1. 前提条件 win10 WSL2 Ubuntu(截止202308最新版本是20.04.xx) 有不少的博客都是建议直接安装docker desktop&#xff0c;这样无论在windows…

C#开发的OpenRA游戏之世界存在的属性(1)

C#开发的OpenRA游戏之世界存在的属性(1) 在游戏里,由于存在雷达,那么每个物品就可以在雷达上显示出来,但是雷达上显示不同的部分物品时,会采用不同的颜色来显示,那么它又是怎么样实现这种不同物品进行不同的颜色显示呢? 可以仔细观看下图: 可以看到矿产显示为绿色,…