C++ Primer 类与构造函数 三五法则

前言

本文介绍C++中类的基础知识,介绍所有的构造函数,和什么时候应该该写哪些构造函数,并介绍经典的三五法则。

在C++中,只是声明一个空类,不做任何事情的话,编译器会自动为你生成如下八个默认函数:

  1. 默认构造函数;
  2. 默认析构函数;
  3. 默认拷贝构造函数;
  4. 默认拷贝赋值运算符函数;
  5. 默认移动构造函数(C++11);
  6. 默认移动赋值操作符函数(C++11)。
  7. 默认取址运算符函数;
  8. 默认取址运算符const函数;
  • 只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个默认拷贝构造函数、一个默认重载赋值操作符函数和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建,当然这几个生成的默认函数的实现就是什么都不做。所有这些函数都是inline和public的。

  • C++11新增标识符default和delete,控制这些默认函数是否使用。

    • default:被标识的默认函数将使用类的默认行为,如:A() = default;
    • delete:被标识的默认函数将禁用,如:A() = delete;
    • override:被标识的函数需要强制重写基类虚函数;
    • final:被标识的函数禁止重写基类虚函数;

  • c++新标准规定,可以为数据成员提供一个类内初始值(in-class initialize)。
    创建对象时,类内初始值将用于初始化类内成员对象。
  • 类通常被定义在头文件中,且类所在的头文件文件名和类名保持一致。
  • 头文件通常包含只能定义一次的实体,如类、const和constexpr变量。
  • 一个空类时,编译器会默认生成默认构造函数、拷贝构造函数、析构函数。
  • 使用class和struct定义类的唯一区别就是默认的访问权限;
    当定义的类所有成员是public时,使用struct;反之,则使用class。
class Sales_data {
private:unsigned_sold = 0;  
}

构造函数

构造函数的任务是初始化类对象的数据成员,只要类的对象被创建,就会执行构造函数。

  • 构造函数不能被声明成const的。
    当创建类的一个const对象时,知道构造函数完成其初始化过程,对象才能真正取得其常量属性。因此,构造函数在const对象的构造过程中可以向其写值。

默认构造函数

如果我们的类没有显示的定义构造函数,则编译器会隐式的定义一个默认构造函数。

  • 默认构造函数将按照如下规则初始化类的数据成员:

    1. 如果存在类内的初始值,则用它来初始化成员
    2. 否则,默认初始化该成员
  • 只有当类没有声明任何构造函数时,编译器才会自动的生成默认构造函数。

  • 在c++11新标准中,如果我们需要默认的行为,可以通过添加= default来要求编译器生成默认构造函数。

class Person {
private:int member = 0;public:Person() = default;  // 使用 = default来声明这是一个默认构造函数Person(int a) : member(a) {}Person(int a, int b) : member(a + b) {}
};Person person(10); // 创建person对象
Person person2(); // Empty parentheses interpreted as a function declaration,定义一个person2()方法
Person person1 = 10; // 隐式类型转换

构造函数初始值列表

如下构造函数一个是进行赋值、一个是进行初始化,这种区别取决于数据成员的类型;事关底层效率问题,前者是先初始化数据成员后再赋值,后者是直接初始化数据成员。

class ConstRef {
public:// 是对成员变量进行赋值操作
//    ConstRef(int a) {
//        this->a = a;
//        ca = a; // Constructor for 'ConstRef' must explicitly initialize the const member 'ca'
//        ra = a; // Constructor for 'ConstRef' must explicitly initialize the reference member 'ra'
//    }// 这种构造函数就是构造函数初始值列表,是对成员进行初始化操作ConstRef(int number) : a(number), ca(number), ra(number) {};private:int a;const int ca;int &ra;
};

隐式类型转换

如果构造函数只接受一个实参,则他实际上定义了此类类型的隐式转换机制;可以通过关键字explicit禁止掉隐式构造。

  • 关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。

  • explicit构造函数只能用于直接初始化

class Person {
private:int member = 0;public:Person() = default;explicit Person(int a) : member(a) {}  // 使用explicit禁止隐式类型转换,提高代码可读性Person(int a, int b) : member(a + b) {}
};void testPerson(Person person) {
}Person getPerson2() {return 10;  // 错误,已禁止隐式类型转换
}Person person(10);  // 正确,直接初始化
Person person1 = 10;  // 错误,explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化
testPerson(10);  // 错误,已禁止隐式类型转换
getPerson2();

类的静态成员

因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的,即不是由的类的构造函数初始化的。
而且我们不能在类的内部初始化静态成员(除非是const或constexpr的),必须在类的外部定义和初始化静态成员。
静态成员和全局变量一样,存在于整个整个程序的生命周期。

// .h
struct Person {
private:const static int count0 = 10;constexpr static int count1 = 10;//    static int count2 = 10; // Non-const static data member must be initialized out of linestatic int count3;
}// .cpp,在类外初始化
int Person::count3 = 10;

拷贝控制

拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment constructor)、移动构造函数(move constructor)、移动赋值运算符(move-assignment constructor)、析构函数(destructor),来显示的或隐式的指定在此类型的对象拷贝、移动、赋值和销毁。
这些操作统称为拷贝控制操作。
编译期会自动为类添加这些操作,如果类本身没有进行定义的话。

拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么;
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么;
析构函数定义了当此类型对象销毁时做什么;

拷贝构造函数copy constructor

  • 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo {
public:Foo() = default; // 默认无参构造函数Foo(const Foo&); // 拷贝构造函数
}
  • 拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的

  • Copy constructor must pass its first argument by reference.
    拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。
    如果其参数不是引用类型,则调用永远也不会成功;为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。

  • 拷贝初始化发生在以下情况:

    • 使用 = 进行赋值或者定义变量的时候
    • 将一个对象是作为实参传递给一个非引用类型的形参
    • 从一个返回类型为非引用类型的函数返回一个对象(RVO、NRVO优化)
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    • 使用标准库容器的不同api也会有不同的效果
    string dots(10, ','); // 直接初始化string s(dots);            // 直接初始化string s2 = dots;          // 直接初始化string null_book = "123";  // 拷贝初始化string null_book2 = string(10, ','); // 拷贝初始化

拷贝赋值运算符copy-assignment constructor

  • 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
class Foo {
public:Foo() = default; // 默认无参构造函数Foo(const Foo&); // 拷贝构造函数Foo& operator=(const Foo&);  // 拷贝赋值运算符
}

析构函数

  • 构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。

  • 析构函数不接受参数,因此它不能被重载。对一个给定类,只会有唯一一个析构函数。

  • 在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。

  • 无论何时一个对象被销毁,就会自动调用其析构函数:

    • 变量在离开其作用域时被销毁。
    • 当一个对象被销毁时,其成员被销毁。
    • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
    • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁(参见12.1.2节,第409页)。
    • 对于临时对象,当创建它的完整表达式结束时被销毁。
class Foo {
public:Foo() = default; // 默认无参构造函数Foo(const Foo&); // 拷贝构造函数Foo& operator=(const Foo&);  // 拷贝赋值运算符~Foo(); // 析构函数
}

移动构造函数move constructor

移动构造函数的第一个参数是该类类型的一个右值引用,且不是const类型的。

  • 除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。

  • 如果我们的移动移动构造函数不抛出异常,则必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
    由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常;
    必须显式声明出该对象在移动时不会抛出异常,会有助于容器类选择移动构造而非拷贝构造。

class Foo {
public:Foo() = default; // 默认无参构造函数Foo(const Foo&); // 拷贝构造函数Foo& operator=(const Foo&);  // 拷贝赋值运算符~Foo(); // 析构函数Foo(Foo&&) noexcept; // 移动构造
}

移动赋值move-assignment constructor

  • 与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。

  • 移动赋值运算符需要检查自赋值情况
    如果相同,右侧和左侧运算对象指向相同的对象,我们不需要做任何事情。
    我们进行检查的原因是此右值可能是move调用的返回结果。

class Foo {
public:Foo() = default; // 默认无参构造函数Foo(const Foo&); // 拷贝构造函数Foo& operator=(const Foo&);  // 拷贝赋值运算符~Foo(); // 析构函数Foo(Foo&&) noexcept; // 移动构造Foo& operator=(Foo&&) noexcept; // 移动赋值
}

移后源对象必须可析构

  • 从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。
    因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。

  • 移动操作还必须保证对象仍然是有效的。
    对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。

  • 移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据。

  • 在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。

三五法则

有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。
在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。

  1. 需要析构函数的类也需要拷贝和赋值操作
    当我们决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。
    如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。

  2. 需要拷贝操作的类也需要赋值操作,反之亦然
    因为某些类所要完成的工作,只需要拷贝或赋值操作,不需要析构函数。所以有该法则。

  3. 如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
    定义了拷贝操作的类类通常拥有一个资源,而拷贝成员必须拷贝此资源。但是拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。

MyString类

举个例子,如如下String类。

class MyString {
private:char *mData;friend ostream &operator<<(ostream &out, MyString &myStr);public:MyString() {cout << "默认无参构造函数" << endl;mData = new char[1];*mData = '\0';}MyString(const char *data) {cout << "单参构造函数-隐式类型转换" << endl;mData = new char[strlen(data)];strcpy(mData, data);}~MyString() {if (mData) {cout << mData << "~析构函数" << endl;delete[] mData;mData = nullptr;} else {cout << "~析构函数" << endl;}}MyString(const MyString &other) {cout << "拷贝构造函数" << endl;mData = new char[strlen(other.mData) + 1];strcpy(mData, other.mData);}MyString(MyString &&other) noexcept: mData(other.mData) {cout << "移动构造函数" << endl;other.mData = nullptr;}MyString &operator=(const MyString &other) {cout << "拷贝赋值函数" << endl;if (this == &other) {return *this;}delete[] mData;mData = new char[strlen(other.mData) + 1];strcpy(mData, other.mData);return *this;}MyString &operator=(MyString &&other) noexcept {cout << "移动赋值函数" << endl;if (this == &other) {return *this;}delete[] mData;mData = other.mData;other.mData = nullptr;return *this;}bool empty() const {return mData == nullptr || std::strlen(mData) == 0;}
};ostream &operator<<(ostream &out, MyString &myStr) {return out << myStr.mData;
}
void test() {MyString s; // 默认初始化,栈对象MyString s1 = "s1"; // 隐式类型转换,栈对象MyString s2("s2"); // 值初始化,栈对象
//    s1 = s2; // 左值拷贝,拷贝赋值函数s1 = std::move(s2); // 右值移动,移动赋值函数cout << s1 << endl;
//    cout << s2 << endl; // 移动源对象不可再使用
}//    默认无参构造函数
//    单参构造函数-隐式类型转换
//    单参构造函数-隐式类型转换
//    移动赋值函数
//            s2
//    ~析构函数
//    s2~析构函数
//    ~析构函数

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android、Java 中的位运算使用小结

文章目录 位运算符如何记住?实际编程中有什么实际应用呢?1. 权限控制2. 图形处理&#xff1a;3. 网络通信&#xff1a;4. 数据加密&#xff1a;5. 数据压缩&#xff1a;6. 高效计算&#xff1a;7. 底层编程&#xff1a;8. 算法优化&#xff1a;9 .硬件交互&#xff1a;10. 位…

玩转微服务-GateWay

目录 一. 背景二. API网关1. 概念2. API网关定义3. API网关的四大职能4. API网关分类5. 开源API网关介绍6. 开源网关的选择 三. Spring Cloud Gateway1. 文档地址2. 三个核心概念3. 工作流程4. 运行原理4.1 路由原理4.2 RouteLocator 5. Predicate 断言6. 过滤器 Filter6.1. 过…

java —— 字符输入流/字符输出流

字符输入流/输出流&#xff0c;是指以字符为单位进行输入或输出的方式。其与字节输入流/字节输出流的用法如出一辙&#xff0c;只是输入/输出单位由字节改为字符。 一、字符输入流 创建步骤&#xff1a; ① 创建输入源文件的 File 对象&#xff1a;File filenew File("…

[图解]建模相关的基础知识-02

1 00:00:01,530 --> 00:00:05,200 第2个概念&#xff0c;谓词&#xff0c;Predicate 2 00:00:07,530 --> 00:00:10,800 或者叫断言&#xff0c;翻译各种各样都有 3 00:00:12,830 --> 00:00:15,050 实际上就是前面命题 4 00:00:15,060 --> 00:00:16,610 相当于常…

记录Nuxt 3 官网项目的一次部署

本来以为就是一次简单的部署&#xff0c;之前也是部署过几次nuxt项目了&#xff0c;所以&#xff0c;并没有要记录的想法。但是过程出现了很多问题&#xff0c;最后考虑还是写下来吧。留个记录&#xff08;完整的配置部署过程&#xff09; 这里我将要说明两种部署方式以供选择&…

开源网安软件安全国产化替代解决方案亮相2024澳门万讯论坛

近日&#xff0c;2024万讯论坛在澳门成功举办。本次论坛由万讯电脑科技主办&#xff0c;旨在引进国内尖端科技厂商&#xff0c;提供全方位的信创解决方案&#xff0c;分享信创化过程中所面临的挑战及阶段性转换经验。开源网安作为拥有软件安全领域全链条产品的厂商&#xff0c;…

如何使用 Vue CLI 创建和管理一个 Vue 项目

Vue CLI 是一个基于 Node.js 的命令行工具,可以快速创建和管理 Vue.js 项目。以下是使用 Vue CLI 创建和管理 Vue 项目的步骤: 1&#xff1a;安装 Vue CLI 打开终端或命令提示符,运行以下命令安装 Vue CLI: npm install -g vue/cli安装完成后,可以使用 vue --version 命令检查…

Analytical Model(分析模型)和Compact model(紧凑模型)有什么不同

Analytical Model&#xff08;分析模型&#xff09; 和 Compact Model&#xff08;紧凑模型&#xff09; 在电子工程和半导体物理领域有着不同的应用和特点&#xff1a; Analytical Model&#xff08;分析模型&#xff09;: 理论基础&#xff1a;分析模型基于物理原理和数学公…

jeecg dictText字典值

前端列表的字典值回显&#xff0c;配置了数据字典后&#xff0c;在本地测试可以回显中文的数据&#xff0c; 但在线上服务器不能正常回显出来&#xff1b; 原因是在前端拿到records的列表值时可以拿到dictText的字典&#xff0c;但是线上服务器没有dictText的值&#xff1b; …

聚焦 Navicat 17 新特性 | 模型设计优化与创新

随着 Navicat 17 的正式发布&#xff0c;受到了广泛的关注和讨论。Navicat 产品力又一次大跃迁。新引入的特性显著增强了用户的数据库管理和数据分析体验&#xff0c;包括&#xff1a;模型设计与同步、数据字典、数据分析&#xff08;data profiling&#xff09;、用户体验、查…

共享门店模式:快速打造连锁实体店

在数字化浪潮的冲击下&#xff0c;许多线下实体店正面临前所未有的挑战。然而&#xff0c;在这个变革的时代&#xff0c;共享门店模式&#xff0c;也被称为“共享股东”&#xff0c;正以其独特的魅力&#xff0c;为实体店带来新的生机。 一、共享门店模式的崭新定义 共享门店…

​水经微图Web版1.8.0发布

让每一个人都有自己的地图&#xff01; 水经微图&#xff08;简称“微图”&#xff09;新版已上线&#xff0c;在该版本中主要新增了注册登录功能&#xff0c;线与面图层新增矩形、圆或军标等绘制功能&#xff0c;以及其它功能的优化。 现在&#xff0c;为你分享一下本轮迭代…

SELinux:安全增强型Linux

SELinux&#xff1a;安全增强型Linux 作用&#xff1a; 可以保护linux系统的安全为用户分配最小的权限 状态&#xff1a; Enforcing&#xff1a;强制保护Permissive&#xff1a;宽松状态Disabled&#xff1a;禁用 为了安全性考虑&#xff0c;希望SELinux设置为Enforcing状态…

PostgreSQL调优工具:PGTune

PostgreSQL调优工具&#xff1a;PGTune 1&#xff0c;PGTune网址 https://pgtune.leopard.in.ua/#/ 参数解释&#xff1a; DB Version&#xff1a;数据库版本 OS Type&#xff1a;操作系统 DB Type&#xff1a;数据库类型&#xff0c;一般默认即可 Total Memory (RAM)&#x…

巨详细Linux安装MySQL

巨详细Linux安装MySQL 1、查看是否有自带数据库或残留数据库信息1.1检查残留mysql1.2检查并删除残留mysql依赖1.3检查是否自带mariadb库 2、下载所需MySQL版本&#xff0c;上传至系统指定位置2.1创建目录2.2下载MySQL压缩包 3、安装MySQL3.1创建目录3.2解压mysql压缩包3.3安装解…

2 - 力扣高频 SQL 50 题(基础版)

2.寻找用户推荐人 考点: sql里面的不等于&#xff0c;不包含null -- null 用数字判断筛选不出来 select name from Customer where referee_id !2 OR referee_id IS NULL;

UML行为图-状态图

概述 创建 UML 状态图的目的是研究类、角色、子系统或组件的实时行为。状态图不仅可用于描述用户接口、设备控制器和其他具有反馈的子系统&#xff0c;还可用于描述在生命期中跨越多个不同性质阶段的被动对象的行为&#xff0c;在每一阶段该对象都有自己特殊的行为。 一、状态…

2024最全软件测试面试八股文(答案+文档+视频讲解)

Part1 1、你的测试职业发展是什么&#xff1f; 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&#xff0c;按如何做好测试工程师的要点去要求自…

探索Python爬虫:实战演练,打造你的数据采集利器

在这个信息爆炸的时代&#xff0c;数据成为了最宝贵的资源之一。Python&#xff0c;以其简洁的语法和强大的库支持&#xff0c;成为了数据采集和处理的首选语言。本文将带领你走进Python爬虫的世界&#xff0c;通过一系列实战演练&#xff0c;教你如何构建自己的数据采集工具。…

Python-3.12.0文档解读-内置函数zip()详细说明+记忆策略+常用场景+巧妙用法+综合技巧

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 详细说明 基本用法 示例 特性 高级用法 注意事项 版本更新 示例代码 记忆策略…