C++:类和对象(中)---默认成员函数---运算符重载---const的含义

简介: C++:类和对象(中)---默认成员函数---运算符重载---const的含义

默认成员函数

首先要理解什么是默认成员函数:类在什么都不写的时,编译器会生成六个默认成员函数

用户没有显式实现,但编译器会生成的成员函数就是默认成员函数

下面我们对这些函数一一进行介绍

构造函数

在C语言中,无论是实现栈队列链表等各种数据结构,都避免不了要写Init初始化函数,这个函数的功能是给变量一个初始化的值,在C++中,认为C语言的这些问题有些许麻烦,于是进行了一定的优化,构造函数就是要在对象创建的时候,就把信息设置进去

构造函数是一个特殊的成员函数,名字和类名相同,创建类型对象的时候就由编译器自己自动调用,用来保证类中的每一个数据成员都有一个自己的初始值,在整个对象的生命周期中只调用一次

构造函数的特性

构造函数是特殊的成员函数,主要功能是用来初始化对象

它有下面的一些特点


  1. 函数名和类名相同
  2. 没有返回值
  3. 对象实例化的时候会自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式的构造函数,那么会自动生成一个无参的默认构造函数,如果用户定义了构造函数就不再生成
  6. 默认构造函数是在不写的时候会生成,且内置类型的成员不会进行处理(在C++11中的声明支持给缺省值),自定义类型的成员才会处理,回去调用这个成员的默认构造函数
  7. 无参的构造函数和全缺省的构造函数都是默认构造函数,这两个构造函数只能存在一个

既然它有这么多的特点,那么就一一举例论证它的特点

1. 函数名和类名相同
2. 没有返回值
3. 对象实例化的时候会自动调用对应的构造函数
4. 构造函数可以重载

在实际写代码时,尽量要写全缺省的构造函数,这样不管如何给参数都有一定的初始化值

class Date
{
public:
  Date(int year=1, int month=1, int day=1)
  {
    cout << "Date(int year, int month, int day)" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  Date d2(2004);
  Date d3(2004, 6);
  Date d4(2004,6,16);
  return 0;
}

5. 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户有显式定义就不再生成

这里也是我们接触到的第一个默认构造函数,我们用下面的代码证明它的存在性

下面继续验证它的后半段存在性

从中可以看出,当我们没有定义构造函数时,定义一个无参的对象编译器会执行默认构造函数给数据成员初始值,而现在我们定义了构造函数,那么默认构造函数不复存在,此时编译器也就不能执行默认构造函数给无参的d1初始化值,此时会报错—Date不存在默认构造函数,没有合适的构造函数可以使用

6. 默认构造函数是在不写的时候会生成,内置类型的成员不会进行处理(在C++11中的声明支持给缺省值),自定义类型的成员才会处理,回去调用这个成员的默认构造函数

简单来说,一般情况下都需要我们自己写构造函数,决定初始化方式,成员变量都是自定义类型,可以考虑不写构造函数,因为会调用自定义类型的构造函数

7. 无参的构造函数和全缺省的构造函数都是默认构造函数,这两个构造函数只能存在一个

后续还有初始化列表,我们后续进行讲解

析构函数

析构函数就相对简单一点

它存在的意义是什么呢?

我们实现栈顺序表链表等数据结构时,都要在堆上malloc开辟一段空间,但是是不是经常会忘记free呢?这样会造成内存泄漏的危险情况的发生

那么C++在开发的时候就想到了这个问题,因此析构函数就这样产生了,它存在的意义就是完成对象中资源的清理工作,它会在对象被销毁前自动调用

析构函数的特点如下

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。析构函数不能重载
  4. 对象生命周期结束时,C++编译自动调用析构函数。
  5. 调用顺序满足栈的顺序

拷贝构造函数

在实际代码运用中,我们经常会拷贝一个对象用来做其他事情,在C语言中,这个过程十分简单,把值直接全部从一个内容中拷贝到另外一个地方即可,但在C++中却不那么容易

拷贝构造函数的引入

首先要清楚堆创建后,除非通过free否则是不会被还原的,因此如果有这样的C语言代码:

void push(Stack ps)
{
  ps._a[0] = 10;
  ps._top++;
}
void test2()
{
  Stack s={0,0,0};
  StackInit(&s);
  push(s);
  printf("%d", s._a[0]);
}

这里的StackInit函数只是单纯的初始化,给栈开辟空间,而最后运行结果是10,原因就在于在push函数中,虽然是值传递,但是ps结构体中的成员_a依旧拥有改变堆内存的能力,具体可以用下面的图来表示

那么现在换到C++,引入类的概念后,整个就变得比C语言要复杂一点,原因如下:

首先,定义一个栈的类,并且完成一系列栈的操作

typedef int STDataType;
class Stack
{
public:
  Stack()
  {
    capacity = 4;
    a = (STDataType*)malloc(sizeof(STDataType) * capacity);
    if (a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    top = 0;
  }
  ~Stack()
  {
    top = capacity = 0;
    free(a);
    a = nullptr;
  }
  void Push(STDataType x)
  {
    if (capacity == top)
    {
      capacity *= 2;
      STDataType* tmp = nullptr;
      tmp = (STDataType*)realloc(a,sizeof(STDataType) * capacity);
      if (tmp == nullptr)
      {
        perror("realloc fail");
        exit(-1);
      }
      a = tmp;
    }
    a[top] = x;
    top++;
  }
  void Pop()
  {
    top--;
  }
  STDataType Top()
  {
    return a[top];
  }
private:
  STDataType* a;
  int top;
  int capacity;
};

和C语言的实现相同,假如我们直接进行传值拷贝,具体做法如下:

void func1(Stack s)
{
   s.Push(1);
}
int main()
{
   Stack s1;
   func1(s1);
   return 0;
}

再画出和上面相仿的图

看似和C语言基本相同,但实际相差很大,C++会执行构造函数和析构函数,那么在进入func1的栈帧后,销毁栈帧的时候就会执行析构函数,_a所指向的空间就被销毁掉了,那么回到main函数的栈帧后,结束程序依旧要进行析构函数,此时_a已经被销毁过一次了,程序就会崩溃,无法正常运行

这其实也就说明,C++中想要直接进行对象拷贝似乎不是一件容易的事,两个对象指向同一片空间就必然会出问题,C++语法就定义了拷贝函数来解决这个问题

拷贝构造函数的特征

拷贝构造函数也是特殊的成员函数,具体表现在:

  1. 拷贝函数是构造函数的一个重载
  2. 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
  4. 深拷贝就涉及到上面栈在堆上空间的问题
  5. 拷贝构造函数的典型应用场景

使用已存在的对象创建新对象

函数参数类型为类类型对象

函数返回值类型为类类型对象

下面根据拷贝构造函数的特征进行一一分析

1. 拷贝构造函数是构造函数的重载

这个很好解释,拷贝构造函数函数名和构造函数相同,只是函数参数不同

2. 拷贝函数的参数只有一个并且必须是类类型对象的引用,使用传值方式编译器会报错,因为涉及到了无穷递归调用

假设我们这里是这样实现拷贝构造函数:

//函数定义
Date(const Date d1)
{
  _day = d1._day;
  _month = d1._month;
  _year = d1._year;
}
//函数调用
Date d1(d2);

那么标准写法是如何写的呢

//函数定义
Date(const Date& d1)
{
  _day = d1._day;
  _month = d1._month;
  _year = d1._year;
}
//函数调用
void func2(Date d2)
{
  d2.Print();
}
int main()
{
  Date d1(2002, 10, 12);
  d1.Print();
  func2(d1);
  return 0;
}

3. 若未显式定义,编译器会生成默认的拷贝构造函数,默认拷贝构造函数会按对象按内存中的存储字节序完成拷贝,也叫做浅拷贝或值拷贝
4. 深拷贝就涉及到上面栈在堆上空间的问题

这里需要注意的是:

不写拷贝构造函数时,编译默认生成的拷贝构造,和之前的构造函数特性是不一样的

  1. 内置类型是值拷贝
  2. 自定义的类型是调用它的拷贝

简单来说,像Date类型的就不需要我们进行拷贝构造,但是Stack类型的就需要进行深拷贝

下面是深拷贝的拷贝构造函数

Stack(const Stack& s1)
{
  top = s1.top;
  capacity = s1.capacity;
  STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * capacity);
  if (tmp == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  a = tmp;
}

所谓深拷贝,就是重新在堆上开辟一个空间供构造出的栈使用,这样就避免了函数栈帧中的栈在结束时free掉了堆上的空间使得main函数崩溃的情况出现

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

运算符重载

C++在C的基础上的提升在运算符重载上也可以体现出

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

假设我们现在要比较日期谁大,那么这个场景就可以应用运算符重载

// 类体内定义运算符重载
bool operator <(const Date& d1)
{
  if (_year > d1._year)
  {
    return false;
  }
  else if (_year == d1._year && _month > d1._month)
  {
    return false;
  }
  else if (_year == d1._year && _month == d1._month && _day > d1._day)
  {
    return false;
  }
  else
  {
    return true;
  }
}
// 调用main函数
int main()
{
  int i = 10;
  int j = 20;
  int tmp1 = i < j;
  Date d1(2000, 10, 20);
  Date d2(2001, 8, 10);
  int tmp2 = d1 < d2;
  cout << tmp1 << endl;
  cout << tmp2 << endl;
}

我们转到汇编观察

从中也不难发现,运算符重载后调用小于实际上是调用了运算符重载函数

这样写代码的可读性大大提高

赋值运算符重载

关于拷贝构造和赋值运算符重载,你需要知道的…

1. 就运算符重载本身而言,这个函数本身是成员函数

就对上面的代码来说,假设这里执行下面的命令

d1<d2

实际上在施行的时候会转换成这样:

d1.operator<(d2)

再返回对应的值或其他形式

2. 赋值运算符重载和拷贝构造的区别

前面我们讲了拷贝构造要带引用,不带引用会递归

Date d1(const Date d2);            //错误写法
Date d1(const Date& d2);           //正确写法

那么反观运算符重载,表面上看也是这样:

bool operator <(const Date d1)
bool operator <(const Date& d1)

那上面的写法可以吗,其实是可以的,这里就需要对拷贝构造死循环有更深刻的理解

对于拷贝构造来说,它实质上是需要新建对象的,也就是说这里进行拷贝构造函数的时候要先传参再执行函数新建一个对象,而在传参的过程中就会陷入这是否也是拷贝构造的循环中,导致最后陷入死循环导致构造失败

而对于运算符重载来说,这里仅仅只是执行一个类内的成员函数,两个对象都已经创建好了,而这里传参的Date只是把参数传过来而已,如果使用的不是引用,则会创建一个形参,在内存中会有更多的消耗,经此而已,而如果使用引用就避开了创建形参这样的一个过程,相当于是减小了内存的消耗,因此这里并不一定必须传引用,传引用只是提升效率的一种方式

const的含义

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,后续有具体实际情况再进行分析

取地址及const取地址操作符重载

这是最后一个默认成员函数,但是使用场景极少

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载

相关文章
|
4天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
4天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
1天前
|
编译器 C语言 C++
|
1天前
|
编译器 C++
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
|
4天前
|
C语言 C++
【C++】日期类Date(详解)③
该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
|
4天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
4天前
|
定位技术 C语言 C++
C++】日期类Date(详解)①
这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
|
4天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
4天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
4天前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。