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

前言

本文介绍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/20678.shtml

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

相关文章

mysql表GEOMETRY记录的读取与增加

两张表&#xff0c;均有GEOMETRY字段&#xff0c;合并成一张表。 直接读取记录并insert into 出错&#xff0c;需用AsText(SHAPE) 及 GeomFromText($v[1])转换 一、读取 $tStr "select OGR_FID,AsText(SHAPE),name,area,perimeter,cnty_,cnty_id,cnty_code,pyname,post…

CentOS8安装opensips-cli

环境&#xff1a;阿里云 操作系统CentOS8.5 opensips 3.x版本废弃了之前的配置管理脚本opensipsctl&#xff0c;引入了一个新的python工具叫opensips-cli。本文描述如何在CentOS8安装这个工具。 升级python CentOS 8默认的ptyhon版本是3.6。这不能满足opensips-cli的要求&…

信息化业务运维的必要性和重要性

随着信息技术的飞速发展&#xff0c;企业信息化已经成为提升竞争力的关键手段。然而&#xff0c;仅仅拥有先进的信息化系统并不足以保证企业的高效运转&#xff0c;对这些系统进行科学、有效的运维同样至关重要。本文将深入探讨信息化业务运维的必要性和重要性。 一、信息化业…

什么是股票,新手如何入门

股票是代表公司所有权的证券&#xff0c;当你持有某公司的股票时&#xff0c;你实际上拥有了该公司的一部分。作为股东&#xff0c;你有权分享公司的利润&#xff08;如通过股息&#xff09;&#xff0c;以及在公司解散时对公司剩余资产的索取权。以下是关于股票的一些基本概念…

【TB作品】MSP430F5529,单片机,电子秒表,秒表

硬件 MSP430F5529开发板7针0.96寸OLED /* OLED引脚分配 绿色板子DO(SCLK)------P4.3D1(DATA)------P4.0RES-----------P3.7DC------------P8.2CS------------P8.1 */ 程序功能 该程序是一个用C语言编写的&#xff0c;用于msp430f5529微控制器上的简单电子秒表应用。它使用…

iOS与前端:深入解析两者之间的区别与联系

iOS与前端&#xff1a;深入解析两者之间的区别与联系 在数字科技高速发展的今天&#xff0c;iOS与前端技术作为两大热门领域&#xff0c;各自在移动应用与网页开发中扮演着不可或缺的角色。然而&#xff0c;这两者之间究竟存在哪些差异与联系呢&#xff1f;本文将从四个方面、…

1882java密室逃脱管理系统 Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java密室逃脱管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助采用了java设计&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用web模式&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&…

7.2 Go 使用error类型

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

最大数位置c++

题目描述 输入n个整数,存放在数组a[1]至a[n]中&#xff0c;输出最大数所在位置(n≤1000)。 输入 第一行&#xff0c;数的个数n; 第二行&#xff0c;n个正整数&#xff0c;每个数在232−1之内。 输出 最大数所在位置。 样例输入 5 67 43 90 78 32 样例输出 3 代码如下…

数论1---整除

概念与基本性质就不说了 例题1&#xff1a;已知a|n&#xff0c;b|n.且axby1,求证&#xff1a;ab|n 即&#xff1a; 所以&#xff1a;ab|n 例题2&#xff1a;设m是一个大于2的正整数&#xff0c;证明&#xff1a;对于任意正整数n&#xff0c;都有 由于我不想打公式了直接拍照…

「前端+鸿蒙」核心技术HTML5+CSS3(七)

1、浮动简介、元素浮动后的特点、浮动后的影响、解决浮动产生的影响、浮动布局 浮动简介: 浮动(Float)是CSS中一种布局机制,它允许元素脱离常规的文档流,沿其容器的左侧或右侧排列。浮动元素仍然保留在页面布局中,可能会影响其他元素的布局。 元素浮动后的特点: 元素会…

react-native 默认停用 flipper 通知

react-native 0.74 默认停用 flipper &#xff0c;但仍然可以手动安装 flipper 官方声明文档 英语好的可以直接阅读。 integration with React Native will no longer be enabled 原因 增加编译时间有时候会有连接问题升级会导致不能使用 之后调试推荐 我们建议团队使用 A…

【Qt知识】Qt窗口坐标系

Qt的窗口坐标体系遵循标准的计算机图形坐标系统规则 Qt窗口坐标体系特点 坐标原点&#xff1a;窗口坐标体系的原点位于窗口的左上角&#xff0c;即坐标(0, 0)位置。 轴方向&#xff1a; X轴&#xff1a;向右为正方向&#xff0c;随着X坐标值的增加&#xff0c;元素在窗口中从…

opencv-python(二)

马赛克 img cv2.imread(./bao.jpeg)print(img.shape)img2 cv2.resize(img,(35,23))img3 cv2.resize(img2,(900,666))cv2.imshow(bao,img3)cv2.waitKey(0)cv2.destroyAllWindows()img2 cv2.resize(img, (90,66))img3 np.repeat(img2, 10, axis 0) # 重复行img4 np.repeat(…

使用Bash脚本确保定时任务的单例执行

简介&#xff1a; 在Linux系统中&#xff0c;定时任务是自动化运维的重要组成部分。然而&#xff0c;有时候我们可能需要确保某个定时任务在任何给定时间点只运行一次&#xff0c;以避免资源冲突或数据不一致。本文将介绍如何使用Bash脚本和文件锁来实现单例定时任务。 什么是…

数据结构---时间复杂度与空间复杂度

文章目录 1. 知识背景2. 什么是时间复杂度&#xff1f;3. 空间复杂度4 .大O渐进表示法&#xff1a;对于一些算法的时间复杂度存在最好&#xff0c;最坏&#xff0c;平均的情况&#xff1a; 5. 常见的时间复杂度举例总结&#xff1a;6. 空间复杂度的举例与总结&#xff1a;总结&…

腾讯 InstantMesh,单图生成 3D 模型,10 秒内完成,性能超越 SOTA

前言 近年来&#xff0c;3D 内容创作在游戏、动画、虚拟现实等领域发挥着越来越重要的作用。然而&#xff0c;传统的 3D 模型制作流程繁琐&#xff0c;需要专业人员花费大量时间和精力。为了简化 3D 内容创作流程&#xff0c;腾讯 ARC 实验室推出了 InstantMesh&#xff0c;一…

开源代码分享(32)-基于改进多目标灰狼算法的冷热电联供型微电网运行优化

参考文献&#xff1a; [1]戚艳,尚学军,聂靖宇,等.基于改进多目标灰狼算法的冷热电联供型微电网运行优化[J].电测与仪表,2022,59(06):12-1952.DOI:10.19753/j.issn1001-1390.2022.06.002. 1.问题背景 针对冷热电联供型微电网运行调度的优化问题&#xff0c;为实现节能减排的目…

prometheus-alert使用

说明&#xff1a;本文介绍一款可接管alertmanager报警&#xff0c;简化alertmanager配置的组件prometheus-alert。可以将prometheus检测到的异常指标&#xff0c;通过alertmanager转给prometheus-alert&#xff0c;由prometheus-alert通知到各个应用。 如下&#xff1a; 上图来…

# linux 系统下,使用 docker 启动 mysql 后,通过 sqlyog 连接 mysql 报“错误号码2058“

linux 系统下&#xff0c;使用 docker 启动 mysql 后&#xff0c;通过 sqlyog 连接 mysql 报“错误号码2058“ 一、错误描述&#xff1a; 在 ubuntu 系统上&#xff0c;刚安装的 docker 启动 mysql 后&#xff0c;想通过图形界面 SQLyong 等工具连接 mysql 出现“错误号码2058…