RoadMap8:C++中类的封装、继承、多态与构造函数

摘要:在本章中涉及C++最核心的内容,本文以C++中两种基础的衍生数据结构:结构体和类作为引子,从C++的封装、继承与多态三大特性全面讲述如何在类这种数据结构进行体现。在封装中,我们讲解了类和结构体的相似性;在继承中,我们讲解了一般的继承方式和多重继承,进一步地,我们讨论了如何通过虚继承的方式避免继承过程中产生的钻石继承问题;在多态中,我们解释为什么吧多态分为静态多态和动态多态,针对静态多态,我们讲解了函数重载和模板函数两种实现方式,在动态多态中,我们讲解了虚函数的概念;最后,我们就构造函数来讲解类的内存管理问题,具体地,我们主要讲解了:构造函数、拷贝构造函数、拷贝赋值构造函数、移动构造函数、移动赋值构造函数五种类型,在什么情况下会被调用和如何编写这五种构造函数。

关键词:多重继承、钻石继承与虚继承、虚函数、函数重载、模板函数、构造函数、右值引用

1. 结构体与类(封装)

结构体在C语言里面早有现身,而类时C++里面加入的。直观来讲,两者的相似性在于:两者都可以看作一个组/容器(此处与STL中的Vector无关,只是直观理解),这个组/容器可以将不同的数据格式的变量和函数打包放在一起。如:

#include<iostream>
using namespace std;struct Mystruture  // 结构体:默认全部为公有成员
{string name = "SturctureObj";void print() {cout << "I am a structure!" << endl;};
};class MyClass{	// 类:默认全部为私有成员private:string name = "ClassObj";public:void getname() {return name;};void print() {cout << "I am a class!" << endl;};
};int main(){Mystruture objStrut;cout << "objStrut name: " << objStrut.name << endl; // objStrut name: SturctureObjobjStrut.print();   // I am a structure!MyClass objClass;// cout << "objClass name: " << objClass.name << endl; // 无法访问类的私有成员;cout << "objClass name: " << objClass.getname() << endl; // objClass name: ClassObjobjClass.print();   // I am a class!
}

倘如只利用结构体/类作为变量/函数的容器 (封装的意义1:将属性和行为作为一个整体,表现生活中的事物),可以从上述例子看到,两者的用法除了某些关键字(如struct/class等)其他的用法几乎一致。其唯一的区别在于:默认方式控制,结构体struct默认是公有的(public),而类class默认是私有的(private) (封装的意义2:将属性和行为加以权限控制)

  1. 公有的:使用关键字 public: 进行定义,表示类的内部和外部都可以访问该类型的变量;
  2. 私有的:使用关键字 private: 进行定义,表示类的内部自己可以访问,派生类和外部都不可以访问;
  3. 受保护的:使用关键字 protected: 进行定义,表示类的内部和派生类可以访问,外部不能访问;

但是,倘若只把类当作一个具有权限差异容器,那简直是一种奢侈的浪费,围绕着类这个东西:构造/析构函数中提供了关于如何去初始化和释放类;继承提供了如何在一个新的类实现旧类的代码重用问题;多态提供了如何在一个新的类实现旧类的代码重用问题实现代码的修改问题。当然,在C++语言中,struct对C语言中的strcut进行了扩充,已经不仅仅是一个包含不同数据类型的数据结构体了,在C++语言中,strcut可以包含成员函数,可以实现继承,可以实现多态。但是结构体的使用频率远远没有类高。

2. 继承

继承,顾名思义:获取前人的遗产(类代码)供自己使用。能大大提高代码的重用,从而提升开发效率。

2.1 继承与多重继承

在类的继承中,我们把原来的旧类叫做 基类,我们把从旧类继承产生的新类叫做 派生类。派生类可以使用基类的任意成员函数/变量。在C++中,继承是通过使用冒号来实现的 :后面跟着基类的访问说明符和基类名称。其基础语法如下所示:

class 派生类名称 : 访问说明符 基类名称{// 类的核心代码
}

其中,访问说明符包括:public、protected 和 private 三种类型,与类封装中对成员函数/变量权限约束不同的是:

  1. 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  2. 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  3. 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

当然,一个派生类不仅仅可以来自于一个基类,也可以来自于多个基类,这被称作多重继承或者多继承,其继承的基础语法也极其相似:

class 派生类名称 : 访问说明符1 基类名称1, 访问说明符2 基类名称2, ..., 访问说明符n 基类名称n {// 类的核心代码
}

以下一个例子展示,如何使用基础的类继承:

#include<iostream>
using namespace std;// 基类1:动物类
class animal{public:void eat() { cout << "I can eat!" << endl;};
};// 基类2:哺乳动物类
class mamal{public:void breathe() { cout << "I can breathe!" << endl;};
};// 派生类1:鸟类,继承动物类
class bird : public animal{public:void fly() { cout << "I can fly!" << endl;};
};// 派生类2:狗类,同时继承动物类和哺乳动物类
class dog : public animal, public mamal{public:void bark() { cout << "I can bark!" << endl;};
};int main(){bird mybird;  //鸟类对象,继承动物类,可以调用动物类的吃函数mybird.eat();  //I can eat!  mybird.fly();  //I can fly!dog mydog;   //鸟类对象,同时继承动物类和哺乳动物类,可以调用动物类的吃函数和哺乳动物类的呼吸函数mydog.eat();    //I can eat!mydog.breathe();  //I can breathe!mydog.bark();    //I can bark!
}

2.2 钻石继承

钻石继承说的当然不是从基类上来继承钻石,成为百万富翁。而是说出现下图情况,存在 基类A 同时被 类B类C继承,而 派生类B派生类C 又被 派生类D 多重继承。那么这就会造成一个问题:当 派生类D 调用 基类A 的成员函数/变量时,会引起一些歧义。实际上,派生类D分别通过派生类B和派生类C继承了两份独立的基类A,这导致无法得知应该调用返回哪一份的结果。以下通过一个简单的例子展示这个问题。
在这里插入图片描述

//diamond.cpp
#include<iostream>
using namespace std;class A{ // 基类 A
public:A (int x) :  m_x(x) {} // 基类 A 构造函数为 m_x 成员变量赋值int m_x;     
};
// 派生类B,继承于基类A,set 函数的功能为给基类 A 的 m_x 成员变量赋值
class B : public A { 
public:B (int x) : A(x) {}   // 基类 B 构造函数,传递变量 x 到基类 A 的构造函数中void set(int x) {this -> m_x = x;}
};
// 派生类C,继承于基类A,get 函数的功能为查看基类 A 的 m_x 成员变量赋值
class C : public A {
public:C (int x) : A(x) {} // 基类 C 构造函数,传递变量 x 到基类 A 的构造函数中int get(void) {return this -> m_x;}
};
// 派生类D,继承于基类B,C
class D : public B,public C {
public:D (int x) : B(x),C(x) {} // 基类 D 构造函数,传递变量 x 到基类 B,C 的构造函数中
};int main(void) {D d(10);   // 实际上,b.x = 10, c.x=10d.set(20);   // b.x = 20, c.x = 10 cout << d.get() << endl; // 输出 10// cout << d.m_x << endl; // 报错,歧义,无法输出return 0;
}

为了解决钻石继承带来的问题,C++中在继承 访问说明 符前使用 vitual 关键字提供了虚继承的解决方法,它能够确保在最终派生类中只继承基类的一个副本,从而解决了菱形继承问题。其基础语法为:

class 派生类名称 : virtual 访问说明符 基类名称{// 类的核心代码
}

在上述砖石继承的例子中,只需要在 派生类B 和 派生类C 的声明中添加关键字 virtual 即可,并在派生类D的构造函数中添加基类A(x),则可以通过 派生类D 的对象 d.m_x 直接访问而不造成歧义。

#include<iostream>
using namespace std;class A{ // 基类 A//code...   
};
// 派生类B,继承于基类A,set 函数的功能为给基类 A 的 m_x 成员变量赋值
class B : virtual public A { //code...
};
// 派生类C,继承于基类A,get 函数的功能为查看基类 A 的 m_x 成员变量赋值
class C : virtual public A {//code...
};
// 派生类D,继承于基类B,C
class D : public B, public C {
public:D (int x) : A(x), B(x),C(x) {} // 基类 D 构造函数,传递变量 x 到基类 A,B,C 的构造函数中
};int main(void) {D d(10);   // 实际上,B::m_x = 10, C::m_x=10d.set(20);   // B::m_x = 20, B::m_x = 20 cout << d.get() << endl; // 输出 20cout << d.m_x << endl; // 输出 20, 无歧义return 0;
}

3. 多态

多态并不是类/结构体特有的一个概念。根据 什么时候将函数实现和函数调用关联起来 这个基本问题,将多态分为静态多态和动态多态。静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早绑定。静态多态往往也被叫做静态联编。动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定,动态多态往往也被叫做动态联编。

3.1 静态多态

静态多态是指在编译期间就可以确定函数的调用地址,其具体的实现形式包括:

  1. 函数重载:函数重载是一种创建多个名称相同但参数列表不同的函数的方法。编译器根据调用函数时使用的参数的类型和数量来确定要调用的正确函数。
  2. 模板函数:模板(template)是允许您创建通用函数或类。特定类型的实际代码是在编译时生成的。
#include<iostream>
using namespace std;// 函数重载:定义多个同名函数,但不同的参数类型或列表
void printer(int n){cout << "Int printer: " << n << endl;
}void printer(char* s){cout << "Char printer: " << s << endl;
}void printer(double num){cout << "Double printer: " << num << endl;
}// 模板函数定义的语法:
// 通过 template关键词 创建一个模板名称为 T 的模板类型
template<typename T>
void MyPrinter(T s){cout << "Template printer: " << s << endl;
}int main(){// 函数重载:编译器根据调用函数时使用的参数的类型和数量来确定要调用的正确函数printer(666);       // Int printer: 666printer("hello!");      // Char printer: hello!printer(3.14);      // Double printer: 3.14// 模板函数:创建通用函数或类。特定类型的实际代码是在编译时生成的。MyPrinter(666);       // Template printer: 666MyPrinter("hello!");      // Template printer: hello!MyPrinter(3.14);      // Template printer: 3.14
};

3.2 动态多态

动态多态指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定,动态多态往往也被叫做动态联编。动态多态往往与类的继承问题相挂钩,并通过虚函数进行实现。

首先,为什么需要动态多态?
场景是:我已有一个类,类里面大多数的成员函数都是我所需要的,但是有部分函数的功能我是需要修改的,那该怎么办?最勤奋的办法就是复制一下整个类,并手动给类换个名字,然后把需要修改的函数在新复制的类中修改。再懒一点的方法就是利用前面说的类的继承,继承出一个新的派生类,并在新派生类中使用其他函数名添加需要修改的函数。那问题是能不能在新的派生类直接重写基类中需要修改的 同名函数 呢?答案当然是:能,需要使用虚函数

那么,怎么借助虚函数在派生类中重写基类中的同名函数呢?

  1. 在基类中找到需要被重写的函数,并在其函数类型前添加 virtual 关键字;
  2. 在派生类中重新编写基类中的同名函数,并在其函数声明后添加 override 关键字;
#include<iostream>
using namespace std;// 基类1:动物类
class animal{public:virtual void eat() { cout << "吃吃吃!" << endl;};  // 在其函数类型前添加 virtual 关键字virtual void speak() { cout << "烫烫烫!" << endl;};  // 在其函数类型前添加 virtual 关键字
};// 派生类1:鸟类,继承动物类
class bird : public animal{public:void speak() override { cout << "叽叽叽!" << endl;};  // 在其函数声明后添加 override 关键字
};// 派生类2:狗类,继承与鸟类
class dog : public bird{public:void speak() override { cout << "旺旺旺!" << endl;};  // 在其函数声明后添加 override 关键字
};int main(){animal* p1; // 基类指针可以指向派生类,并调用派生类中和基类所共有的函数;p1 = new animal; p1->speak();  // 烫烫烫!p1 = new bird;p1->speak();  // 叽叽叽!p1 = new dog;p1->speak();  // 旺旺旺!
}

接着,最后一个问题是:C++是如何在运行时动态地选择哪一个虚函数的重写版本进行运行的?
通过虚函数表(virtual tables, Vtable),当一个类包含一个虚拟函数时,编译器会为该类创建一个虚函数表。此表包含指向类中定义的虚拟函数的函数指针。该类的每个对象都有一个指向其虚拟表(vptr,虚拟指针)的指针,该指针在对象构造过程中由编译器自动初始化。
如上例中的 bird 类中,编译器会自动为其创建一个虚函数表,表中包含两个函数指针指向虚函数,分别是:animal::eat // 从基类继承bird::speak ,bird类实例化的对象的_vptr_指针会自动指向该类对应的虚函数表,从而选择正确的虚函数版本。

4. 构造函数

类的构造/析构函数表明了一个类是如何实例化/删除一个对象的,其核心是交待如何去给对象的成员变量分配内存或进行初始化。根据构造方法的不同,可以把构造函数分为以下五类:(默认)构造函数、拷贝构造函数、拷贝赋值构造函数、移动构造函数、移动赋值构造函数。

4.1 (默认)构造函数

class MyClass{   // 创建一个类,但是没有显式地给出构造函数;public:char* m_name;  // 同学的姓名int m_nameLength;	// 同学的姓名的长度static int numPeople;  // 记录班级总人数
};

如果创建一个类没有显式地提供该类的构造函数,则编译器会自动创建默认的构造函数,这个构造函数既不接受任何参数也不做任何的操作。

//默认的构造函数
MyClass::Myclass(){};

当然,也可以手动创建一个与类名同名的自定义构造函数,使用该构造函数来实现类对象的初始化问题,如下所示:

#include<iostream>
#include<string.h>
using namespace std;class MyClass{public:char* m_name;    // 同学的姓名int m_nameLength;        // 同学的姓名的长度static int numPeople;  // 记录班级总人数// 自定义构造函数(与类同名)MyClass(char* name){  // 为了区分类成员和传入参数,通常在类成员名字前加入 m_m_nameLength = strlen(name);m_name = new char[m_nameLength + 1];    // 通过 new[] 动态分配内存strcpy(m_name, name);numPeople++;        // 记录班级总人数 + 1cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;}; // 自定义析构函数,析构函数的名称为:~类名~MyClass(){cout << "Goodbye, " << m_name << "!" << endl;delete[] m_name;        // 通过 new[] 动态分配内存,需要手动 delete[] 释放numPeople--;        // 记录班级总人数 - 1cout << numPeople << " students in class!" << endl;}
};int MyClass::numPeople = 0;  // 类的静态成员变量不属于任何一个对象,而是属于整个类int main(){MyClass jacket("jacket");
}

该程序的输出如下,因为当实例化一个对象时,就会自动调用构造函数,当mian主函数推出后,会自动调用析构函数。

// 实例化一个对象时,就会自动调用构造函数
Welcome, jacket!
1 students in class!
// main 主函数退出
Goodbye, jacket!
0 students in class!

4.2 拷贝构造函数

拷贝构造函数,又称复制构造函数。上述的普通构造函数通常传入的是类成员变量的取值作为参数。

而在什么时候需要拷贝构造函数呢?
答案是:当传入的参数是指向类对象的引用时,即用一个类对象去初始化另一个类对象时,就会调用拷贝构造函数。如:

MyClass Tony("Tony"); // 存在一个类对象
MyClass Jacket(Tony); // 用已存在的类对象 Tony 去初始化新的类对象 Jacket
MyClass Jacket = Tony;
MyClass Jacket = MyClass Tony;
MyClass* Jacket = new MyClass Tony("Tony");

那么该如何编写拷贝构造函数呢?
类似于上述普通的构造函数,如果不对拷贝构造函数进行显式的声明,那么就会执行一个默认的拷贝构造函数。这将导致程序运行时存在一个错误提醒。如将不改动上述的 MyClass 类,在main函数里面用 jacket 对象初始化类对象:

int main(){MyClass jacket("jacket");MyClass tony(jacket);  // 在main函数里面用 jacket 对象初始化类对象
}

程序的运行输出结果如下:

// 初始化 jacket 对象
Welcome, jacket!
1 students in class!
// 调用拷贝构造函数初始化 tony 对象
// main 主函数退出,调用 tony 对象的析构函数
Goodbye, jacket!
0 students in class!
// 调用 jacket 对象的析构函数,出现乱码并报错
Goodbye, �$%4!

报错的原因是析构函数中重复释放 m_name,因为在 jacket 对象和 tony 对象中的 m_name 共用同一块内存,这就导致该内存在调用 tony 对象的析构函数时被释放,再调用 jacket 对象的析构函数时 m_name 已经被释放,所以出现重复释放的报错。究其原因,是由于默认的拷贝复制函数采用了浅拷贝的方式,因此,再手动编写拷贝复制函数时需要注意对某些数据类型采用深拷贝的模式,如上述例子中添加拷贝构造函数如下:

class MyClass{// Same Code...// 自定义拷贝构造函数MyClass(const MyClass& obj){  // 我们并不希望 obj 对象受到改动,故而设为 constm_nameLength = obj.m_nameLength;m_name = new char[m_nameLength+1];strcpy(m_name, obj.m_name);numPeople++;        // 记录班级总人数 + 1cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;}
}

这样运行的代码就没有问题了,以下是代码的打印输出:

Welcome, jacket!
1 students in class!
Welcome, jacket!
2 students in class!
Goodbye, jacket!
1 students in class!
Goodbye, jacket!
0 students in class!

4.3 拷贝赋值构造函数

要了解拷贝赋值构造函数,首先要区分什么是赋值,什么是声明,什么定义。简单地,可以用以下例子理解:

//声明
int number1;
//定义=声明+初始化
int number2 = 5;
//赋值:针对两个已经声明的变量
number1 = number2

理解了什么是赋值后,那么下一个问题就是:什么时候会调用拷贝赋值构造函数?
答案是:当把一个已经声明的对象赋值给另一个已经声明的对象时调用。如:

MyClass Tony;
MyClass Jacket;
// 调用拷贝赋值构造函数
Tony = Jacket;

而在初始化调用的是上述的拷贝构造函数,如:

MyClass Tony;
// 调用拷贝构造函数
MyClass Jacket = Tony;

那么按照惯例,下一个问题是:该如何编写拷贝赋值构造函数呢?
拷贝赋值构造函数的编写,本质上就是重载运算符+拷贝构造函数的结合体,其基本的代码框架如下:

类名& operator=(const 类名& 对象形参){  // 我们并不希望 形参对象 受到改动,故而设为 constif(this == &对象形参){return *this;}//拷贝过程
}

不妨依然使用上面例子,在上面例子中添加拷贝赋值构造函数如下:

#include<iostream>
#include<string.h>
using namespace std;class MyClass{// Same Coding ...// 自定义拷贝赋值构造函数MyClass& operator=(const MyClass& obj1){if(this == &obj1){return *this;}delete[] m_name;m_nameLength = obj1.m_nameLength;m_name = new char[m_nameLength+1];strcpy(m_name, obj1.m_name);cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;return *this;};
};int MyClass::numPeople = 0;  // 类的静态成员变量不属于任何一个对象,而是属于整个类int main(){MyClass jacket("jacket");MyClass tony("tony");tony = jacket;  // 调用拷贝赋值构造函数
}

这样运行的代码就没有问题了,以下是代码的打印输出:

// 定义两个对象,Tony 和 jacket
Welcome, jacket!
1 students in class!
Welcome, tony!
2 students in class!
Welcome, jacket!
// 调用拷贝赋值构造函数
2 students in class!
// main 函数退出,自动调用析构函数
Goodbye, jacket!
1 students in class!
Goodbye, jacket!
0 students in class!

4.3 移动构造函数

可能很多同学也意识到了,拷贝构造函数或者拷贝赋值构造函数都存在着一个问题:很多时候,我们面对的场景是需要把一个旧对象的内容直接转移到新对象内容上,然后把旧对象给删除掉。如果使用拷贝构造函数的话,需要把对象的成员逐个进行拷贝,这将浪费大量的时间,能否直接使用指针把旧对象成员变量的所有权直接交给新对象,而不做多余的复制呢?

答案当然是可以的,只不过需要使用的移动构造函数/移动赋值构造函数。而移动构造函数/移动赋值构造函数的传入参数类型必须是右值引用,所以我们首先需要对右值引用有一个初步的了解。

4.3.1 右值引用

首先,熟悉C++基本操作的我们都知道,在C++的赋值操作中,等号的左边通常是一个表示数据的表达式(如变量名或者指针名称),程序可以直接获取该表达式的地址。而等号的右边通常是一些常量、运算的表达式或者带有返回值的函数。

// 合法的表达式
int a = 12;
int b = function(50);
// 不合法的表达
int c + d = 12;

我们不妨把合法表达式等号左边的表达式称为左值,等号右边的表达式称为右值。那么我们常见的引用如int &a = b; 都是在引用等号左边的内容,我们可以将其称为左值引用。那么很自然地,我们引用等号右边的内容则被称为右值引用。右值引用通过符号 && 表示。如:

// 代码程序int a = 10;int b = 20;int&& c = a + b;  // 右值引用cout << "Before change, c = " << c << ", loaction is: " << &c << endl;a, b = 10, 10;cout << "After change, c = " << c << ", loaction is: " << &c << endl;// 输出结果
Before change, c = 30, loaction is: 0x2347ffcbc
After change, c = 30, loaction is: 0x2347ffcbc

跟左值引用类似,我们也可以获取右值引用变量的地址,而且上述例子可以看到,右值引用变量c的地址关联到的是30,即使后面改变了a和b的取值,也不会影响到变量c。

4.3.2 移动构造函数

在了解完右值引用后,我们进行接着介绍移动构造函数。按照管理,第一个问题是:

什么时候会调用移动构造函数?
答案是传入参数是右值引用时,会自动调用移动构造函数,其模式与拷贝构造函数极其相似,如:

class MyClass{};
MyClass func(MyClass obj);
MyClass jacket("jacket");
// 调用 移动构造函数
MyClass tony(func(jacket));
// 调用 拷贝构造函数
MyClass lily(jacket);

可以观察到,jacket 对象属于左值,如果直接使用 jacket 作为参数初始化的话,会调用拷贝构造函数。而 func(jacket) 是MyClass类型函数的返回值,属于右值,故而调用移动构造函数。

很自然地,我们很多时候并没有 func 这个函数,能不能直接使用右值对象也能调用移动构造函数?
答案是可以的,通过强制转换,可以通过 static_cast<>std::move() (需要引入 utility 头文件)把左值的对象强制转化为右值对象,如:

#include<utility>
class MyClass{}
MyClass jacket("jacket");
MyClass jacket_new = std::move(jacket);

接下来第三个问题是:如何编写移动构造函数?

与拷贝构造函数非常类似,其基本格式为:

类名 (类名&& 右值对象){// 指针权限移动
}

它和拷贝构造函数的差异有两点:1. 不使用 const 修饰传入对象,因为在移动构造函数通常我们会把传入对象指针设置为空指针,避免造成重复释放的问题,而在拷贝构造函数我们希望传入对象不发生变换;2. 传入对象类型为右值引用,而非左值引用;以下编写一下与上述相同的例子:

#include<iostream>
#include<string.h>
#include<utility>
using namespace std;class MyClass{public:char* m_name;int m_nameLength;static int numPeople;  // 记录班级总人数// 自定义构造函数(与类同名)MyClass(char* name){  // 为了区分类成员和传入参数,通常在类成员名字前加入 m_m_nameLength = strlen(name);m_name = new char[m_nameLength + 1];    // 通过 new[] 动态分配内存strcpy(m_name, name);numPeople++;        // 记录班级总人数 + 1cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;}; // 自定义析构函数,析构函数的名称为:~类名~MyClass(){cout << "Goodbye, " << m_name << "!" << endl;delete[] m_name;        // 通过 new[] 动态分配内存,需要手动 delete[] 释放numPeople--;        // 记录班级总人数 - 1cout << numPeople << " students in class!" << endl;}// 自定义拷贝构造函数MyClass(const MyClass& obj){cout << "Oh shit, wrong way!" << endl;}MyClass(MyClass&& obj){m_name = obj.m_name;  // 转移传入对象的成员指针obj.m_name = nullptr; // 把传入对象的成员指针设置为空指针,避免重复释放;m_nameLength = obj.m_nameLength;numPeople++;        // 记录班级总人数 + 1cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;}
};int MyClass::numPeople = 0;  // 类的静态成员变量不属于任何一个对象,而是属于整个类int main(){MyClass jacket("jacket");MyClass tony (std::move(jacket));
}

最后的输出如下:

// 创建 jacket 类对象,调用普通的构造函数
Welcome, jacket!
1 students in class!
// 创建 tony 类对象,调用移动构造函数
Welcome, jacket!
2 students in class!
// 调用 tony 类对象析构函数
Goodbye, jacket!
1 students in class!
// 调用 jacket 类对象的析构函数,由于在 tony 类对象调用移动构造函数把传入的 jacket 对象的姓名指针指向空指针,
// 故而无法继续完整输出 Goodbye, jacket!和 班级剩余人数(0 students in class!) 
Goodbye,

4.4 移动赋值函数

移动赋值函数与拷贝赋值函数的定义方式非常类似,但是其传入的参数是非 const 类型的右值引用对象,并且需要把传入对象的指针成员设置为空指针,避免重复释放的问题。用相同的例子如下:

#include<iostream>
#include<string.h>
#include<utility>
using namespace std;class MyClass{// Same Code ...MyClass& operator=(MyClass&& obj1){if(this == &obj1){return *this;}delete[] m_name;m_nameLength = obj1.m_nameLength;m_name = obj1.m_name;obj1.m_name = nullptr;cout << "Welcome, " << m_name << "!" << endl;cout << numPeople << " students in class!" << endl;return *this;};
};int MyClass::numPeople = 0;  // 类的静态成员变量不属于任何一个对象,而是属于整个类int main(){MyClass jacket("jacket");MyClass tony("tony");tony = std::move(jacket);
}

5. 总结

在本章中涉及C++最核心的内容,本文以C++中两种基础的衍生数据结构:结构体和类作为引子,从C++的封装、继承与多态三大特性全面讲述如何在类这种数据结构进行体现。在封装中,我们讲解了类和结构体的相似性;在继承中,我们讲解了一般的继承方式和多重继承,进一步地,我们讨论了如何通过虚继承的方式避免继承过程中产生的钻石继承问题;在多态中,我们解释为什么吧多态分为静态多态和动态多态,针对静态多态,我们讲解了函数重载和模板函数两种实现方式,在动态多态中,我们讲解了虚函数的概念;最后,我们就构造函数来讲解类的内存管理问题,具体地,我们主要讲解了:构造函数、拷贝构造函数、拷贝赋值构造函数、移动构造函数、移动赋值构造函数五种类型,在什么情况下会被调用和如何编写这五种构造函数。

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

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

相关文章

自带恒压恒流环路的降压型单片车充专用芯片

一、基本概述 XL2009是一款高效降压型DC-DC转换器&#xff0c;固定180KHz开关频率&#xff0c;可以提供最高2.5A输出电流能力&#xff0c;具有低纹波&#xff0c;出色的线性调整率与负载调整率特点。XL2009内置固定频率振荡器与频率补偿电路&#xff0c;简化了电路设计。 PWM …

Docker(八)Python+旧版本chrome+selenium+oss2+fastapi镜像制作

目录 一、背景二、能力三、核心流程图四、制作镜像1.资源清单2.Dockerfile3.制作镜像 五、启动测试 一、背景 近几年我们线下的创业团队已从零到一开发过好几个小程序项目&#xff0c;都是和体育相关。其中生成海报分享图片好像都是不可或缺的功能。之前的项目老板给的时间都比…

从零学算法17

17.给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits “23” 输出&#xff1a;[…

Java进击框架:Spring-Web(八)

Java进击框架&#xff1a;Spring-Web&#xff08;八&#xff09; 前言DispatcherServlet拦截器异常视图解析重定向转发 语言环境日志 过滤器带注释的控制器声明映射请求其它注解验证 功能性端点URI Links异步请求CORSHTTP缓存视图技术MVC配置其他Web框架 前言 Spring Web MVC是…

519基于单片机的自动切割流程控制系统

基于单片机的自动切割流程控制系统[proteus仿真] 自动切割流程控制系统这个题目算是课程设计和毕业设计中常见的题目了&#xff0c;本期是一个基于单片机的自动切割流程控制系统 需要的源文件和程序的小伙伴可以关注公众号【阿目分享嵌入式】&#xff0c;赞赏任意文章 2&…

无人驾驶卡尔曼滤波

无人驾驶卡尔曼滤波&#xff08;行人检测&#xff09; x k a x k − 1 w k x_k ax_{k-1} w_k xk​axk−1​wk​ w k w_k wk​&#xff1a;过程噪声 状态估计 估计飞行器状态&#xff08;高度&#xff09; x k z k − v k x_k z_k - v_k xk​zk​−vk​ 卡尔曼滤波通…

了解什么是UV纹理?

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 什么是UV&#xff1f; UV 是与几何图形的顶点信息相对应的二维纹理坐…

CentOS找回root密码

很悲伤&#xff0c;你忘记了root密码。。。 那就来重置它吧~ 1、在启动时选择操作系统&#xff1a;在引导过程中&#xff0c;选择CentOS操作系统并按下键盘上的任意键来停止引导。 2、 进入编辑模式&#xff1a;在启动菜单中&#xff0c;找到并选择要编辑的CentOS条目&…

Prometheus实战篇:Prometheus监控mongodb

Prometheus实战篇:Prometheus监控mongodb 准备环境 docker-compose安装mongodb docker-compose.yaml version: 3 services:mongo:image: mongo:4.2.5container_name: mongorestart: alwaysvolumes:- /data/mongo/db: /data/dbport:- 27017:27017command: [--auth]enviromen…

VMware复制粘贴共享文件夹

win和虚拟机之间&#xff0c;无法复制粘贴&#xff0c;共享文件夹的解决方案。 安装VMware tools 1&#xff0c;先检查虚拟机设置部分。共享文件夹已启用。复制粘贴已启用。 2&#xff0c;安装tools.选择重新安装VMware tools. (此图片为安装过的截图) 成功后会显示如图。…

【2024最新-python3小白零基础入门】No1.python简介以及环境搭建

文章目录 一 python3 简介二 python语言的特点三 python安装四 安装开发工具-pycharm五 新建一个python项目1.新建项目2 配置虚拟环境3 运行项目 一 python3 简介 Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的可读性&a…

批量置入视频封面:一分钟教程,简单易学

在视频制作过程中&#xff0c;为视频添加引人注目的封面是吸引观众的关键。而当我们需要批量处理多个视频时&#xff0c;如何快速、准确地置入封面就显得尤为重要。本文将为您揭示这一高效技巧&#xff0c;让您在一分钟内学会批量置入视频封面&#xff0c;提升视频的吸引力与观…

vite 如何设置 pwa,让网页类似 app 那样运行,使用插件 vite-plugin-pwa

vite 如何设置 pwa&#xff0c;让网页类似 app 那样运行&#xff0c;使用插件 vite-plugin-pwa 一、概述 情况还是那么个情况&#xff0c;还是原来的项目 vue2 改为 vitetsvue3 遇到的问题&#xff0c;今天这个问题是如何 在 Vite 环境下设置 PWA。 PWA 就是网页应用可以像 a…

java是值传递还是引用传递

1.前言 java是值传递&#xff1b;值传递是指在调用方法时将实际参数拷贝一份传递到方法中&#xff0c;这样在方法中如果对参数进行修改&#xff0c;将不会影响到实际参数&#xff1b;当传的是基本类型时&#xff0c;传的是值的拷贝&#xff0c;对拷贝变量的修改不影响原变量&a…

krpano官网文档翻译-------krpano Action脚本参考文档【krpano Actions / Scripting Reference】

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》 ​ ​ ✨ 前言 摘要:本文通过大量代码案例,手把手教你如何使用krpano的Action脚本实现各种交互控制和酷炫…

OpenAI ChatGPT-4开发笔记2024-01:开发环境

ChatGPT发展一日千里。工具、函数少则数日&#xff0c;多则数月就加入了Deprecated行列不再如预期般工作。元旦闲来无事&#xff0c;用最新的ChatGPT重写一下各种开发场景&#xff0c;全部实测通过。 开发环境&#xff1a; 电脑&#xff1a;两台笔记本&#xff1a;HP和MacBoo…

服务端性能测试——性能测试体系

目录&#xff1a; 1.性能测试介绍 性能测试介绍性能体系&#xff1a;性能测试与分析优化&#xff1a;行业流行性能压测工具介绍行业流行性能监控工具介绍行业流行性能剖析工具介绍性能测试流程与方法性能测试计划 计划&#xff1a;DAU&#xff0c;PV(perday)&#xff0c;订单量…

HTTP 3xx状态码:重定向的场景与区别

HTTP 状态码是服务器响应请求时传递给客户端的重要信息。3xx 系列的状态码主要与重定向有关&#xff0c;用于指示请求的资源已被移动到不同的位置&#xff0c;需要采取不同的操作来访问。 一、301 Moved Permanently 定义&#xff1a; 服务器表明请求的资源已永久移动到一个新…

Fluids —— Volume VOP

P&#xff0c;当前体素位置&#xff1b;density&#xff0c;此场的值&#xff1b;ix, iy, iz&#xff0c;体素索引&#xff08;0 ~ res-1&#xff09;&#xff1b;resx, resy, resz&#xff0c;当前volume的精度&#xff1b;center&#xff0c;当前volume的中心点&#xff1b;o…

STM32入门教程-2023版【3-3】gpio输入

关注 星标公众号 不错过精彩内容 大家好&#xff0c;我是硬核王同学&#xff0c;最近在做免费的嵌入式知识分享&#xff0c;帮助对嵌入式感兴趣的同学学习嵌入式、做项目、找工作! 上两小节我们已经把GPIO的结构和8种输入输出模式都讲完了&#xff0c;到这里还不懂的可以回…