C++:构造/析构/赋值运算(Effective C++)

简介: C++:构造/析构/赋值运算(Effective C++)

写在前面

这是对Effective C++这本书中的部分内容进行的总结以及代码实践,主要是记录一些对我印象深刻的,确实能改善程序的内容,和有需要实践验证加深印象的一部分实践和我自己的理解

05:了解C++默默编写并调用哪些函数

当我们创建了一个类后,即使这个类中并没有写任何成员函数,但是编译器依旧会为我们生成六个成员函数,也叫做六大默认成员函数

这六个成员函数分别为:

  1. 构造函数:用来完成初始化
  2. 析构函数:用来完成清理
  3. 拷贝构造:用来初始化对象
  4. 赋值重载:用来对象间的赋值
  5. 普通对象取地址重载:取地址操作符重载
  6. const对象取地址重载:取地址操作符重载

其中需要注意的是,编译器默认生成的构造函数是一个浅拷贝,只是把非静态成员进行值拷贝,关于浅拷贝和深拷贝前文有讲述,因此完全使用编译器生成的构造函数有时是不够的,需要我们自己完善编写

这里对静态成员变量也进行一下补充:

  • 静态成员变量是该类所有对象所共有的内容,在整个函数生命周期内只分配一次内存
  • 静态成员变量存储在全局变量区,与对象的创建销毁无关

因此编译器默认生成的拷贝构造函数只对non-static成员变量奏效

关于构造函数的建议:

编译器默认生成的构造函数是无参的版本,而在实际应用中常常需要用到带参数的构造函数,因此这里对于一个类来说,自己写一个全缺省的构造函数会便利很多

关于赋值操作符重载:

赋值重载函数也是默认成员函数,但编译器是可以拒绝的,只有当生出的代码合法,并且有适当机会证明有意义编译器才会生成

#include <iostream>
#include <string>
using namespace std;
template <class T>
class nameobject
{
public:
  nameobject(string& name, const T& value)
    :namevalue(name)
    ,objectvalue(value)
  {}
private:
  string& namevalue;
  const T objectvalue;
};
int main()
{
  string newdog("Tom");
  string olddog("Jim");
  nameobject<int> a(newdog,1);
  nameobject<int> b(olddog,2);
  a = b;
  return 0;
}

对于上面的代码,编译器无法解析a=b究竟该如何解释,如果解释为将b中元素赋予给a,那么就违背了引用不能指向不同对象这一原则,编译器不知该如何进行操作,因此拒绝生成赋值重载函数,此时需要我们自己来完成这一函数

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数

06:若不想使用编译器自动生成的函数,就该明确拒绝

编译器默认会生成成员函数,但假设现在有这样的情景,我不允许外部使用类的时候调用拷贝构造和赋值重载函数,但如果我在类内不写编译器也会默认生成,此时应该如何解决?

解决方法是,将函数写到private类内

外部成员是不可以调用私有类成员的,因此将函数写到私有就可以解决这个问题

根据这个原理,可以考察一个有趣的面试题

如何定义一个只能在堆上或栈上生成对象的类?

  • 只能在堆上:将析构函数设置为私有,因为C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会优先检查类的析构函数的可访问性,如果析构函数不可访问,就不能在栈上创建对象
  • 只能在栈上:将new和delete重载为私有,因为在堆上生成对象,使用new关键词操作,其过程分为两个阶段:首先new在堆上寻找可用内存,分配给对象,第二阶段利用构造函数生成对象。如果new操作设置为私有,那么第一阶段就无法完成,就不能在堆上生成对象

为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现

10:令operator=返回一个reference to *this

这个条款比较简单,令赋值操作符返回一个reference to *this

这只是一个协议,并没有强制性,如果你不遵循它也不会报错,但是这样可以提高效率,同时也算是一个共识,我们最好随众

11:在operator=中处理"自我赋值"

赋值重载函数并没有想象那么简单,其中有很多小细节

如果有下面的代码:

#include <iostream>
#include <string>
using namespace std;
class B
{
public:
  B(int a=0,int b=1)
    :_a(a)
    ,_b(b)
  {}
private:
  int _a;
  int _b;
};
class A
{
public:
  A()
  {
    tmp = new B;
  }
  A& operator=(const A& p)
  {
    delete tmp;
    tmp = new B(*p.tmp);
    return *this;
  }
private:
  B* tmp;
};
int main()
{
  A a1;
  A a2;
  a1 = a2;
  return 0;
}

现在来看是没有问题的,很正常的完成了赋值的操作,但事实上是有问题的

如果这里赋值重载函数中的*this和形参是同一个对象,那么此时在执行代码的时候就会销毁掉这个对象的内容,同时赋值的结果就会导致拥有一个被删除的对象,这是错误的行为

解决方案1

因此可以在函数前加一个if语句判断条件,如果形参和this指针相同,就直接返回this指针,这是一种解法

解决方案2

A& operator=(const A& p)
{
  B* cur = tmp;
  tmp = new B(*p.tmp);
  delete tmp;
  return *this;
}

交换一下语句的顺序,在复制前做到不要删除原来的数据即可避免这样的情况发生

解决方案3

但最好的方法,是利用所谓的copy-and-swap方法:

A& operator=(const A& p)
{
  A tmp(p);
  swap(tmp);
  return *this;
}

这相当于是一个很巧妙的方法,利用形参在类内直接构造出一个对象,再将这个对象和*this进行交换,则此时就把形参的数据很巧妙的拷贝到了this指针中,完成了赋值的目的,同时也解决了前面出现的问题

确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap

确认任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

12:复制对象时勿忘其每一个成分

当类内的私有成员增添了某些成员后,如果构造函数为自己构建,要补上对应的构造函数,编译器不会对此做出报错

Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”

不要尝试以某个copying函数实现另一个copying函数,如果有重叠部分可以通过第三个函数调用

相关文章
|
8月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
331 77
|
6月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
322 6
|
8月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
236 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
8月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
178 12
|
8月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
130 9
|
8月前
|
存储 算法 测试技术
【C++数据结构——线性表】求集合的并、交和差运算(头歌实践教学平台习题)【合集】
本任务要求编写程序求两个集合的并集、交集和差集。主要内容包括: 1. **单链表表示集合**:使用单链表存储集合元素,确保元素唯一且无序。 2. **求并集**:遍历两个集合,将所有不同元素加入新链表。 3. **求交集**:遍历集合A,检查元素是否在集合B中存在,若存在则加入结果链表。 4. **求差集**:遍历集合A,检查元素是否不在集合B中,若满足条件则加入结果链表。 通过C++代码实现上述操作,并提供测试用例验证结果。测试输入为两个集合的元素,输出为有序集合A、B,以及它们的并集、交集和差集。 示例测试输入: ``` a c e f a b d e h i ``` 预期输出:
208 7
|
8月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
315 5
|
8月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】顺序表的基本运算(头歌实践教学平台习题)【合集】
本文档介绍了线性表的基本运算任务,涵盖顺序表和链表的初始化、销毁、判定是否为空、求长度、输出、查找元素、插入和删除元素等内容。通过C++代码示例详细展示了每一步骤的具体实现方法,并提供了测试说明和通关代码。 主要内容包括: - **任务描述**:实现顺序表的基本运算。 - **相关知识**:介绍线性表的基本概念及操作,如初始化、销毁、判定是否为空表等。 - **具体操作**:详述顺序表和链表的初始化、求长度、输出、查找、插入和删除元素的方法,并附有代码示例。 - **测试说明**:提供测试输入和预期输出,确保代码正确性。 - **通关代码**:给出完整的C++代码实现,帮助完成任务。 文档
183 5
|
11月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
118 1
|
JavaScript Java C语言
面向对象编程(C++篇3)——析构
面向对象编程(C++篇3)——析构
88 2