C++:类的默认成员函数

简介: C++:类的默认成员函数



类的六个默认成员函数

如果一个类中什么成员都没有,那么它就是一个空类。

但是空类中真的什么都没有吗?并不是的,类在什么都不写的时候,会生成六个默认成员函数

理解这六个函数是类的一大重点,本博客将以这六个函数为出发点讲解知识。


① [ - 构造函数 - ]

构造函数是一个六大默认成员函数之一,其用于初始化对象

class Date
{
public:
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

现在我们有一个Date日期类,其内部有年,月,日三个成员变量。如果我们直接通过这个类定义一个对象出来,那么这三个成员变量就会是随机值。此时我们就可以通过构造函数来完成初始化,让其初始值变为我们需要的初始值。

那么我们要如何书写一个构造函数?

构造函数有以下基本特征:

  1. 函数名与类名相同
  2. 函数没有返回值

我先写一个构造函数帮助大家理解:

class Date
{
public:
  Date()
  {
    //代码
  }
  
private:
  int _year;
  int _month;
  int _day;
};

以上代码中:

Date()
{
  //代码
}

这就是一个构造函数。

首先其函数名为Date,和类名Date一致;其次在Date函数名的左侧没有声明voidint等返回值类型,也就是没有返回值。

接下来我们就可以在构造函数内部实现代码,初始化我们的类了。

比如我想让我的默认时间为1970年1月1日。

Date()
{
  _year = 1970;
  _month = 1;
  _day = 1;
}

这样我们的构造函数就可以实现将时间默认设置为1970年1月1日了。

那么我们要如何调用这个构造函数?

其实构造函数不需要我们调用,在创建对象时,编译器会自动调用这个函数,也就是说我们后续创建的所有类,一创建出来时间就是1970年1月1日。

那么我们能否在创建一个日期类的对象时,就直接将其初始化为我们想要的值,让每次初始化的值都不同?

是可以的,这就需要用到构造函数的重载与缺省了。


构造函数重载与缺省

构造函数是可以进行重载和参数缺省的,可以根据我们输入的不同值进行初始化。

重载

Date()
{
  _year = 1970;
  _month = 1;
  _day = 1;
}
Date(int year)
{
  _year = year;
  _month = 1;
  _day = 1;
}
Date(int year, int month)
{
  _year = year;
  _month = month;
  _day = 1;
}
Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
}

现在我们在类中重载了四个构造函数,每个重载的参数数目都不一样。当我们输入一个值,就调用函数Date(int year),修改年份,月份与日都是初始值1;当我们输入两个值,就调用Date(int year, int month),修改年份和月份,日为初始值1;当我们输入三个值,就调用Date(int year, int month, int day),就修改年月日。

那么我们要如何调用这些函数,在实例化时将传输传进去?

我们只需要在实例化时,在变量名后加一个小括号,小括号内写入参数

比如这样:

int main()
{
  Date d1;
  Date d2(2024);
  Date d3(2024, 2);
  Date d4(2024, 2, 20);
  return 0;
}

对于Date d2(2024);,其变量名为d2,小括号内写入了一个值2024,也就是只有一个值,此时调用第一个构造函数Date(int year),日期初始化为2024/1/1。


对于Date d3(2024, 2);,其变量名为d3,小括号内写入了(2024, 2),也就是传入了两个参数,此时调用第二个构造函数Date(int year, int month),日期初始化为2024/2/1。


对于Date d4(2024, 2, 20);,其变量名为d4,小括号内写入了(2024, 2, 20),也就是传入了三个参数,此时调用第三个构造函数Date(int year, int month, int day),日期初始化为2024/2/20。

最后对于Date d1;这就是一个基本的初始化对象的方式,但是要注意不能写成Date d1();

当对象不需要传值进行初始化,不能()内为空,而是将括号也省略。

缺省

我们的构造函数还可以缺省,我们刚刚的代码很明显是冗余的,完全可以用缺省参数来代替。

以上四个重构可以化作一个缺省的函数:

Date(int year = 1970, int month = 1, int day = 1)
{
  _year = year;
  _month = month;
  _day = day;
}

这样我们也可以达成一样的效果。

对于需要根据传入参数的数目不同,而初始化为不同值的类,我们经常使用全缺省参数来达成效果。

接下来我们对比三个构造函数:

Date()
{
  _year = 1970;
  _month = 1;
  _day = 1;
}
Date(int year)
{
  _year = year;
  _month = 1;
  _day = 1;
}
Date(int year = 1970, int month = 1, int day = 1)
{
  _year = year;
  _month = month;
  _day = day;
}

首先,对于Date(int year)这个函数,我们一定要传一个值才可以调用到。

而对于Date()Date(int year = 1970, int month = 1, int day = 1),我们都可以不传值就可以调用。这种无需传值就被调用的构造函数,叫做默认构造函数。而默认构造函数只有三种,一种是无参数的构造函数,一种是全缺省参数的构造函数,最后一种就是编译器自动生成的构造函数

一个类中,必须存在且只能存在一个默认构造函数。如果一个类中没有默认构造函数,那么编译器就会报错。而上述三种默认构造函数中,也只能存在一种。

那么什么是编译器自动生成的构造函数

构造函数是类的六大默认成员函数之一,对于这六大默认成员函数,如果用户没有显式定义,那么编译器会自动生成,而当用户一旦定义了,则编译器不再自动生成,改用用户定义的函数。

那么这个编译器自动生成的构造函数都会干什么呢?

C++把类型分为内置类型(基本类型)和自定义类型。

基本类型:C++语言自己提供的数据类型,如:int,char…

自定义类型:我们使用class/struct/union等自己定义的类型。

编译器自动生成的构造函数,对于基本类型,不做处理;对于自定义类型,去调用其相应的默认构造函数

比如我们刚刚的日期类Date,其内部有三个int类型的成员变量,它们属于基本类型,所以编译器不做处理。

再看到以下代码:

class Date
{
public:
  Date()
  {
    cout << "我被调用了" << endl;
  }
  
private:
  int _year;
  int _month;
  int _day;
};
class A
{
  int abc;
  Date d;
};
int main()
{
  A a1;
  return 0;
}

我们为Date日期类定义了一个构造函数,当这个函数被调用,会输出”我被调用了“

我们又定义了一个类AA内有一个内置类型的abc,以及一个自定义类型Dated

当我们再main函数中创建了A a1;变量,此时会调用A的构造函数,而此时A的构造函数我们没有写出来,那么就是编译器自动生成的构造函数,此时这个A的构造函数会调用Date的构造函数,最后输出”我被调用了“

那么这个编译器自动生成的构造函数只处理自定义类型,而不处理内置类型,是不是有点不太公平?这是一个长久以来的缺陷,在C++11中引入了成员变量默认值来补足缺陷。


成员变量默认值

成员变量默认值是指:

内置类型在类中声明时,可以设置一个默认值,如果后续构造函数没有给这个内置类型赋值,那么这个内置类型就得到默认值。

先讲解一下语法:

比如这个日期类:

class Date
{ 
private:
  int _year;
  int _month;
  int _day;
};

现在对这些成员变量设置默认值:

class Date
{ 
private:
  int _year = 1970;
  int _month = 1;
  int _day = 1;
};

这样一来,默认值就设置好了,只要后续构造函数不对这些成员赋值,那么这些成员变量就会得到默认值。

编译器自动生成的构造函数不就是一个不会对内置类型做处理的构造函数吗?所以这个特性搭配编译器自动生成的构造函数一起使用,就可以在不写构造函数的情况下,也完成内置类型的初始化了。


类型转换

构造函数不仅可以构造与初始化对象,还具有类型转换的作用。

在不同的内置类型进行赋值时,C++底层会进行一个隐式类型转化:

int a = 1;
double b = a;

比如此代码中,aint类型的,但是由于被赋值给了double,其会被隐式转化为double类型。

而我们的自定义类型也可以完成这样的转化。

看到以下类:

class A
{
public:
  A()
  {
    _a1 = 0;
    _b1 = 1.0;
  }
  A(int a)
  {
    _a1 = a;
  }
  
  A(double b)
  {
    _b1 = b;
  }
private:
  int _a1;
  double _b1;
};

在以上这个A类中,重构了三个构造函数,分别是默认构造函数A();含有一个int参数的A(int a),这个函数会把参数赋值给_a1;含有一个double参数的A(double b)

根据之前的学习,我们可以知道,直接定义一个A类对象,会调用A()初始化对象,而传入不同参数,会调用不同函数来初始化对象。

但是此处的构造函数也可以用于类型转化:

int main()
{
  A abc;
  abc = 3;
  abc = 5.0;
  return 0;
}

以上代码中,我们先定义了一个A类的对象abc,随后abc = 3就是一个转化过程,本来abc作为一个A类对象,int类型的1是无法转化为这个A类型的。但是由于我们有一个只需一个int参数的构造函数,所以也就有了把数字1转化为A类型的相应规则,此时abc = 3就会调用函数A(int a)从而完成转化。

而对于abc = 5.0;,则是调用A(double b)这个构造函数,将double类型的5转化为A类型的过程。

那么如果是有多个参数的构造函数:

A(int a, double b)
{
  _a1 = a;
  _b1 = b;
}

这个也可以进行类型转化吗?

在C++98标准是不允许的,但是C++11又新增了特性,允许多个参数的构造函数进行类型转化。

语法:

int main()
{
  A abc;
  int x = 1;
  double y = 3.0;
  abc = {x, y};
  return 0;
}

对于多个参数的构造函数,只需要把参数用花括号括起来,类型一一对应即可。


explicit

explicit关键字是一种禁止类型转化的关键字。如果你不希望你写出的构造函数允许类型转化,可以把这个关键字放在函数名前,那么此时这个构造函数的类型转化就被禁止了。

如下:

class A
{
public:
   A()
  {
    _a1 = 0;
    _b1 = 1.0;
  }
  A(int a)
  {
    _a1 = a;
  }
  
  explicit A(double b)
  {
    _b1 = b;
  }
private:
  int _a1;
  double _b1;
};

上述代码中,A(double b)这个构造函数被加上了explicit,那么double类型的数据就无法转化为A类型了。但是A(int a)这个构造函数前面没有,int类型的数据的转化是不受影响的。


初始化列表

C++的初始化列表是一种语法特性,用于在对象的构造函数中初始化成员变量。初始化列表使用冒号(:)和逗号(,)来分隔成员变量和初始值。

在C++中,对象的成员变量可以在构造函数的函数体中进行初始化或者利用参数默认值。然而,初始化列表提供了一种更加简洁和高效的方式来初始化成员变量,尤其是对于复杂的对象或者const常量成员变量。

使用初始化列表的语法如下:

Classname(parameters)
 : member1(value)
 , member2(value)
 , ...
{
    // 构造函数函数体
}

其中,Classname是类的名称,parameters是构造函数的参数列表。冒号后面的部分就是初始化列表,其中每个成员变量都包含成员变量名称和初始值,用逗号分隔。

下面是一个示例:

class Example {
public:
    Example(int n, double d) 
    : num1(n)
    , num2(d)
     {
        // 
    }
private:
    int num1;
    double num2;
};

在这个示例中,Example类有两个成员变量num1num2。构造函数使用初始化列表来对这两个成员变量进行初始化,而不是在构造函数的函数体中进行赋值。

我们区分一下成员变量的默认值初始化列表的区别。

class Example {
public:
    Example(int n, double d) 
    : num1(n)
    , num2(d)
     {
        // 
    }
private:
    int num1 = 5;
    double num2 = 3.0;
};

以上代码中

int num1 = 5;
double num2 = 3.0;

是一个设置成员默认值的过程。

: num1(n)
, num2(d)

则是一个初始化列表。

首先:

成员变量的默认值只能指定数据,它是不可变的。

初始化列表则可以在括号中写入表达式,变量等等,这给了第一次赋值极强的灵活性。

其次:

成员变量的默认值并没有完成真正的赋值,它是处于对变量的声明阶段,只是告诉编译器我将要使用一个名字为xxx的变量,还没有正式开辟空间。

初始化列表则是对变量的定义阶段,也就是完成了变量的第一次赋值。

也正是这个区别,导致对于const常量以及引用类型的成员变量,只能用初始化列表来进行初始化。

比如这一个类:

class Example {
public:
    Example(int& x, int y) 
    : ref(x)
    , _b(y)
     {
        // 
    }
private:
    int& ref; //引用
    const int _b; //常量
};

这个类有两个成员,一个ref是引用类型int&,另一个_b是常量类型const int。这两个类型的共同点就是:定义时必须初始化,后续无法改变内容

既然要在定义时就初始化,此时就只有初始化列表可以做到了,因为初始化列表这个阶段就是定义阶段。

最后再说一个小小的注意点:初始化列表的执行顺序是类中从上到下的顺序,而非初始化列表中从上到下的顺序。

比如这个类:

class Example {
public:
    Example(int& x, int y) 
    : _b(y)
    , ref(x)
     {
        // 
    }
private:
    int& ref; //引用
    const int _b; //常量
};

在类中是ref在上方,_b在下方。所以在初始化列表中,虽然_b排在前面,但是ref会先初始化。


② [ - 析构函数 - ]

现在我们知道了一个对象是如何初始化的,那么一个对象是如何被销毁的呢?

这就需要通过析构函数了。

C++的析构函数是一个特殊的成员函数,用于在对象被销毁时进行内部的清理工作。它的名称是在类的名称前面加上一个波浪符号~

当一个对象的生命周期结束时,即对象不再被使用时,析构函数会自动调用。通过析构函数,可以释放对象使用的资源,如动态分配的内存、打开的文件等。析构函数的工作是清理对象所拥有的资源,以防止内存泄漏或资源泄漏。

析构函数的定义在类的声明中。它不需要任何参数,也没有返回值。通常,析构函数的定义放在类的实现文件中。

比如以下类:

class Example {
public:
  //构造函数
  Example()
  {
    a = (int*)malloc(sizeof(int) * 4);
    size = 4;
  }
    // 析构函数
    ~Example() {
        free(a);
        a = nullptr;
        size = 0;
    }
    
private:
    int* a;
    int size;
};

类中有一个成员a,指向了一块空间,在创建Example类的对象时,构造函数被调用,自动初始化了一块空间给a。如果这块空间不处理,那么当程序结束,就会造成内存泄漏。所以我们要在析构函数~Example()中释放a指向的空间。而析构函数的优点就是无需我们自己调用,当对象离开生命周期,这个函数会被自动调用

与构造函数不同的是:一个函数只允许存在一个析构函数,不允许重载。

而与构造函数相同的是:当我们不写析构函数,编译器会默认生成一个析构函数,这也是六大默认成员函数的特性。

那么默认的析构函数会做什么事情呢?

对于默认的析构函数:如果成员变量中存在其它类的对象,调用其它类的析构函数

这个过程是在释放其它类的对象调用的资源,而非这个对象。对象是创建在栈帧中的,会随着程序结束一起销毁,但是不能销毁的是动态内存之类的空间。所以我们如果开辟了动态内存,需要自己编写析构函数,将其free掉。如果没有,那么直接用编译器默认生成的析构函数即可。


③ [ - 拷贝构造 - ]

在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

其实我们在很多时候已经在不知不觉的拷贝对象了,比如函数传参。

我先为大家展示两段自定义类型的函数传参:

class Date
{
public:
  Date(int year = 1970, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
void func(Date d)
{
  //函数体
}
int main()
{
  Date d1;
  func(d1);
  return 0;
}

以上代码中,我们定义了一个日期类Date,内部有三个成员_year_month_day。这三个都是内置类型的成员。

随后我们实例化了一个对象d1,并调用func函数,将d1作为参数传递了进去。形参是实参的一份临时拷贝,那么此时形参d就是实参d1的一份拷贝,那么d1d的过程就会发生一次拷贝。

那么拷贝是如何实现的?这里我们先看到下一个例子,一起讲解:

class Stack
{
public:
  Stack(size_t capacity = 10)
  {
    _array = (int*)malloc(capacity * sizeof(int));
    if (nullptr == _array)
    {
      perror("malloc fail!");
      return;
    }
    _size = 0;
    _capacity = capacity;
  }
  
  ~Stack()
  {
    if (_array)
    {
      free(_array);
      _array = nullptr;
      _capacity = 0;
      _size = 0;
    }
  }
  
private:
  int* _array;
  size_t _size;
  size_t _capacity;
};
void func(Date s)
{
  //函数体
}
int main()
{
  Stack s1;
  func(s1);
  return 0;
}

在这个代码中,我们有一个栈类stack,在实例化时,会调用构造函数初始阿虎,malloc一段动态内存。此时我们调用函数func,将对象s1作为参数传入函数中,此时s就是s1的一份拷贝。

但是,如果按照直接拷贝,那么形参s的成员_array会与实例s1的成员_array值一样,此时形参和实参就指向了同一块空间了。

我们既然没有传引用传参,当然是希望形参改变不要影响实参,但是此时两个对象指向同一块空间,就会互相影响了。甚至由于我们存在一个析构函数~Stack,析构函数会free_array指向的空间,而ss1离开生命周期时,都会调用一次析构函数,此时_array指向的空间会被释放两次,这是不允许的。

可见,如果我们让编译器直接进行拷贝,是有可能会发生错误的。所以类中设立了一个拷贝函数,每当对象需要进行拷贝操作时,都会调用这个拷贝函数。

也就是说,我们每次把对象作为函数形参传递时,都会调用拷贝函数来进行拷贝(这句话很重要)。

拷贝函数是构造函数的一种重载,所以称为拷贝构造函数,简称拷贝构造。既然拷贝构造是构造函数的重载,那么其函数名也就是类名,与构造函数一致。

结构:

ClassName(const ClassName& obj)
{
    // 拷贝构造函数的实现逻辑
}

拷贝构造有要求:必须只有一个参数,且参数类型必须是ClassName&,也就是对这个类的引用。为什么呢?

还记得刚刚的话吗:我们每次把对象作为函数形参传递时,都会调用拷贝函数来进行拷贝(这句话很重要)。

如果我们此处的参数类型是ClassName,那么就会发生无限递归。

比如:我们调用了func函数,需要传入一个类的拷贝,此时调用这个类的拷贝构造,由于拷贝构造需要传入类的拷贝,于是又要调用第二个拷贝构造,而第二个拷贝构造又要调用这个类的拷贝,此时调用第三个拷贝构造…

以此类推,永远递归下去。程序必然会崩溃。

所以我们的拷贝构造必须传递引用来调用,防止无限递归。而一般而言,传入的被拷贝对象,我们是不希望其被修改的,所以还会加一个const修饰。最后我们的第一个参数就写为了const ClassName&

要如何调用拷贝构造?

首先就是函数传参,表达式中等的隐式调用,会在底层偷偷调用拷贝构造函数。

另一种就是我们主动调用:

由于是构造函数的重构,所以可以在创建对象时用该构造函数的语法:

Stack s1;
Stack s2(s1);

即用一个小括号传参,把被拷贝的对象传入。

另一种则是在创建对象时用赋值语句:

Stack s1;
Stack s2 = s1;

拷贝构造是六大默认函数之一,所以如果没有显式地定义拷贝构造函数,C++编译器会默认生成一个默认的拷贝构造。默认的拷贝构造对于内置类型,会直接拷贝,对于自定义类型,则会调用相应的拷贝构造

也就是说,默认的拷贝构造是无法解决我们刚刚两个指针指向同一块空间的问题的,所以我们要自己写一个拷贝构造函数,才可以让被拷贝出来的对象与原对象独立。

如下:

Stack(const Stack& other)
{
    _array = (int*)malloc(other._capacity * sizeof(int));
    if (nullptr == _array)
    {
        perror("malloc fail!");
        return;
    }
    
    memcpy(_array, other._array, other._size * sizeof(int));
    
    _size = other._size;
    _capacity = other._capacity;
}

上述拷贝构造函数将创建一个新的堆栈对象,其容量与参数中的堆栈对象容量相同。然后,它将使用memcpy函数将参数中的堆栈对象数组的内容复制到新的数组中。最后,它将设置新对象的大小和容量与参数对象相同。

这样拷贝出来的对象与原对象就独立了。


运算符重载

根据C++的基本语法,我们知道intint+操作符处理,是两者相加,被==操作符处理是两者判等。

那么我们是不是可以让这些操作符,在自定义类型中也发挥作用,比如一个Date+int可不可以实现某个日期加几天,Date == Date实现两个日期判等?这就需要通过运算符重载,给这些运算符赋予规则。


基本运算符重载

运算符重载是指对已有的运算符进行重新定义,使其可以用于自定义的数据类型或者实现不同的操作逻辑。通过运算符重载,可以为用户自定义的类型创建与内置类型相似的运算操作。

也就是说我们可以通过运算符重载,来设置某些情况下的运算符的功能。

运算符重载的一般语法为:

返回值类型 operator 运算符(参数列表)
{
    // 实现运算符功能的代码
}

其中,operator是关键字,用于标识运算符重载函数;运算符是要重载的运算符;返回值类型是运算符重载函数的返回类型;参数列表是运算符重载函数的参数。

比如这个示例:

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  int _year;
  int _month;
  int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
  return d1._year == d2._year
    && d1._month == d2._month
    && d1._day == d2._day;
}

这个operator==operator代表这是一个运算符重载的函数,而==是被重载的运算符。我们希望实现两个日期判等,所以我们此时的参数是两个日期类。

注意:操作符原本可以处理几个参数,那么就要传入几个参数,其中第一个参数为左操作数,第二个参数为右操作数。

上述代码逻辑:将==这个操作符重载,当左右的类型都为Date时,执行代码中的逻辑,只要两者的_year_month_day都相等,那么返回true,否则返回false

后续我们就可以直接用了:

int main()
{
  Date d1(2024, 1, 24);
  Date d2(2024, 1, 24);
  Date d3(2023, 12, 25);
  d1 == d2;//true
  d1 == d3;//false
  return 0;
}

当然也可以以正常调用函数的方式:函数名(参数)

int main()
{
  Date d1(2024, 1, 24);
  Date d2(2024, 1, 24);
  Date d3(2023, 12, 25);
  operator==(d1, d2);//true
  operator==(d1, d3);//false
  return 0;

刚刚我们的运算符重载被放在了全局中,此时只能访问Date中的公有成员变量,但是其也可以放在类中:

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  bool operator==(const Date& d2)
  {
    return _year == d2._year
    && _month == d2._month
      && _day == d2._day;
  }
private:
  int _year;
  int _month;
  int _day;
};

此时这个运算符重载就可以访问到Date中的私有变量了。

由于成员函数会自带一个this指针作为第一个参数,所以当运算符重载放在类内部时,只需要写右操作数,左操作数默认为this;如果是单目操作符,则无需参数,默认为this作为操作数。

而在操作符重载函数的内部,由于this指针无需显式表达,所以对于第一个操作数,其成员变量可以直接访问,无需.点操作符。

对于操作符重载有以下注意点:

  1. 不能通过操作符重载创建新的操作符。
  2. 操作符重载必须有一个类类型的参数。
  3. 内置类型的运算符,不能被重载。比如将int+int重载,这是不允许的。
  4. .*::sizeof:?.这五个操作符不允许重载。

自增自减运算符重载

自增自减运算符的重载比较特别,由于++aa++的操作符都是++。我们无法在operator后很好的区别前置和后置自增。为此C++特别规定,对于++--操作符,可以额外传入一个int类型的参数,如果有int类型参数,那么就是后置的,如果没有就是前置的。

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  Date& operator++()
  {
    //代码
  }
  Date& operator++(int)
  {
    //代码
  }
private:
  int _year;
  int _month;
  int _day;
};

以上类中Date& operator++()就是前置++的重载,而Date& operator++(int)就是后置++的重载。


④ [ - 赋值重载 - ]

赋值重载是一个比较特殊的重载,它属于操作符重载,但是属于六大默认函数之一,也就是说对于所有的类,如果不定义赋值重载的函数,编译器会自动为我们定义一个赋值重载。

其内部规则和拷贝构造几乎一致,也就是说,对于编译器生成的赋值重载,其会拷贝内置类型,对于自定义类型,则会调用对应的赋值重载。

先看到赋值重载的调用规则,和操作符重载的调用规则是一致的:

Date d1(2024, 1, 24);
Date d2(2023, 12, 25);
d1 = d2;
operator=(d1, d2);

我们再看到拷贝构造的调用方式:

Date d1(2024, 1, 24);
Date d2(d1);
Date d3 = d1;

对比两者,有一个地方容易搞混:d1 = d2;Date d3 = d1;

d1 = d2;是在后续赋值,调用的是赋值重构。

Date d3 = d1;则是在定义新变量,此时调用的是拷贝构造。

也就是说=这个操作符,在定义时调用拷贝构造,在赋值时调用赋值重构。


⑤ [ - 取地址重载 - ]

对于取地址重载和const取地址重载,就是对&操作符的重载,使用这个操作符可以取出对象的地址。

它是六大默认成员函数之一,也就是默认生成的函数。而这个重构在默认生成时就已经可以完成取地址的操作了,所以大部分情况下我们不会对其重构,用编译器生成的即可。

简单展示一下代码:

Date* operator&()
{
  return this;
}

⑥ [ - const取地址重载 - ]

const取地址重载,是对取地址重载的const版本,其用于返回某些const对象的地址。其也是六大默认成员函数之一,编译器自动生成的就已经够用了。

代码:

const Date* operator&() const
{
  return this;
}

相关文章
|
21小时前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
17 0
|
21小时前
|
C++ 编译器 程序员
C++ 从零基础到入门(3)—— 函数基础知识
C++ 从零基础到入门(3)—— 函数基础知识
|
21小时前
|
C++ Linux
|
21小时前
|
编译器 C++
【C++】继续学习 string类 吧
首先不得不说的是由于历史原因,string的接口多达130多个,简直冗杂… 所以学习过程中,我们只需要选取常用的,好用的来进行使用即可(有种垃圾堆里翻美食的感觉)
7 1
|
21小时前
|
算法 安全 程序员
【C++】STL学习之旅——初识STL,认识string类
现在我正式开始学习STL,这让我期待好久了,一想到不用手撕链表,手搓堆栈,心里非常爽
16 0
|
21小时前
|
存储 安全 测试技术
【C++】string学习 — 手搓string类项目
C++ 的 string 类是 C++ 标准库中提供的一个用于处理字符串的类。它在 C++ 的历史中扮演了重要的角色,为字符串处理提供了更加方便、高效的方法。
16 0
【C++】string学习 — 手搓string类项目
|
21小时前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
34 1
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
|
21小时前
|
Java C++ Python
【C++从练气到飞升】06---重识类和对象(二)
【C++从练气到飞升】06---重识类和对象(二)
|
21小时前
|
编译器 C++
【C++从练气到飞升】06---重识类和对象(一)
【C++从练气到飞升】06---重识类和对象(一)