【C++语言】类和对象--默认成员函数 (中)

简介: 【C++语言】类和对象--默认成员函数 (中)

前言

本节是要学习六个默认成员函数。主要是从四个方面讲解:

1)什么是该默认成员函数?

2)默认成员函数做了什么?

3)一些易错的注意事项

4)什么时候用默认成员函数,什么时候显式实现?

本篇用 日期类(Date)、栈(Stack) 、队列(Queue)三种类来举例


类的六个默认成员函数:

  • 如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

ee2df86bc866b0e4ffe8a87296c944e0_f67c5d63d9b64d3897b75142ecf3b61b.png

1. 构造函数

构造函数就与我们所写的Init()方法一样,用于类对象属性的初始化。但这个构造函数不用用户调用,而是在类对象实例化时自动调用。


概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。


特性

我们围绕第一个问题展开:什么是构造函数?

其有如下特征:

  1. 函数名与类名相同
  2. 无返回值
class Date
{
public:
  //函数名和类名相同,无返回值
  Date(int year, int month, int day)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
  1. 函数可以重载
class Date
{
public:
  //无参数
  Date()
  {
  _year = year;
  _month = month;
  _day = day;
  }
  /*
  //全缺省:注意全缺省和无参数不能同时存在,他们实例化方式可以相同,编译器无法辨别
  Date(int year=2024,int month=4,int day=27)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  */
  //半缺省
  Date(int year,int month=4,int day=27)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  //不缺省
  Date(int year, int month, int day)
  {
  _year = year;
  _month = month;
  _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
  1. 对象实例化时编译器自动调用
  2. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。后面实例化对象按照显式定义的函数调用(举例看下面代码注释)

Date类举例:

class Date
{
public:
  /*
  // 5.如果用户显式定义了构造函数,编译器将不再生成默认构造函数,后面也不能用Date d1;这样实例化,而是采用Date d1(2024,4,27);这样来实例化对象;
  Date(int year, int month, int day)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  */
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Print();

  return 0;
}

这里我们用Date实例化了个对象d1,调用Print函数打印日期,发现个问题如下图:

我们发现我们的实例化对象d1并没有初始化啊,那默认构造函数到底干了什么呢?

做了什么?

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。默认生成的构造函数对两者处理不同:

1)内置类型不做处理;

2)自定义类型调用它自己的默认构造函数;


易错注意:

  1. 实例化对象,错误:
int main()
{
  Date d1;     //表示实例化一个Date类对象;
  Date d1();   //表示一个返回值为Date的d1()函数方法;
  return 0;
}

2.无参数构造函数、全缺省构造函数、默认构造函数三种都可以当做默认构造函数,只能存在其中一个,不然会发生实例化时编译器不知道调用哪一个构造函数的错误。因为都可以用Date d1;来实例化对象。

3.C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class Date
{
public:
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  //可以在这里给缺省值
  int _year=2024;
  int _month=4;
  int _day=27;
};
int main()
{
  Date d1;
  d1.Print();

  return 0;
}

显式定义和默认构造函数

那么什么时候我们采用显式定义,什么时候采用默认构造函数呢?

我给出的答案是,一般都自己显式定义比较好。

默认构造函数在以下几种情况下可以使用:

  1. 内置类型成员都具有缺省值(默认值);
  2. 类中全是自定义类型,如:Queue;

2. 析构函数

析构函数是一个特殊的成员函数,其名称与类名相同,前面加上波浪号(~)作为前缀。析构函数的主要作用是执行对象生命周期结束时的清理工作。当一个对象的生命周期结束时,无论是因为超出作用域、被显式删除,还是因为其所在的动态内存分配被释放,析构函数都会被自动调用。


概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特征

那么什么是析构函数呢?

它有以下特征:

  1. 函数名就是在类名前加~;
  2. 无参数、无返回类型;
  3. 一个类只能由一个析构函数。若没有显示定义,系统自动调用默认析构函数。注意:析构函数不能够重载;
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。


Stack类 举例:

class Stack {

public:
  //构造函数
  Stack(int capacity=4) 
  {
    _a = (int*)malloc(sizeof(int) * capacity);
    if (NULL == _a)
    {
      perror("malloc fail");
      return;
    }

    _capacity = capacity;
    _size = 0;
  }
  //析构函数:显式定义
  ~Stack()
  {
    //释放动态调用的空间资源
    free(_a);
    _a = NULL;
  }

private:
  int* _a;
  int _size;
  int _capacity;
};

int main()
{
  {
    Stack s1(10);
  }
  
  //s1作用域结束自动调用 ~Stack();
  //不论显示定义,还是默认析构函数,都不需要显示调用;
  return 0;
}

做了什么?

默认析构函数做了什么?

  1. 内置类型不做处理;
  2. 自定义类型调用它的析构函数;

内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可

注意事项:

  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
  2. Queue类,成员全是自定义类型,会调用成员各自的析构函数。所以不用调用析构函数。
  3. 只有堆上的资源需要手动释放。


3.拷贝构造函数

拷贝构造函数是C++中的一种特殊的构造函数,用于创建一个新对象,该对象是已存在对象的副本。拷贝构造函数在多种情况下会自动被调用。

概念

概念:拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

特征

拷贝构造函数有以下特征:


  1. 拷贝构造函数是构造函数的重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用。
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

Date类 举例:

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //拷贝构造函数 一个参数,必须是对象引用哦!!!
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};


int main()
{
  Date d1;
  //拷贝构造函数
  Date d2(d1);
  return 0;
}

做了什么?

默认拷贝构造:依旧是一样的,对内置类型进行浅拷贝,自定义类型调用它们自己的拷贝构造。不需要显示定义的类,比如:Date、Queue。需要显式定义的类,比如:Stack。因为有动态空间的开辟,所以需要深拷贝。注意事项:

这个比较容易错,大家请注意!


1.默认拷贝构造只是浅拷贝,需要深拷贝的对象需要显示定义拷贝构造函数。

举个Stack的例子:我们如果只是浅拷贝来处理Stack会出现错误。

比如:Stack s2(s1);
如果只是浅拷贝: s2._a=s1._a; 只是这种两个指针指向同一个开辟的空间;
  • 问:这种会出现怎样的问题呢?
  • 答:在函数结束的时候,s1调用一次析构函数,把空间释放了;s2也会在调用一次,此时原本空间已经被释放了,无法再次释放,会报错。
  1. 强调,显示定义拷贝构造函数,只能传一个参数并且必须为该类的引用。不能传值。
    原因:如果进行传值传参,会在过程中调用拷贝构造去拷贝一个data临时对象,用于传值。如下图,便无限的调用下去,没有结束点,进入死循环。所以发生报错。

4.赋值运算符重载

运算符重载

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

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

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

注意:

  1. 不能新增加运算符;
  2. 保持运算符具有原有语意;
  3. 不改变运算符原有操作数个数;(比如+,就只能两个对象进行)
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。


赋值运算符的重载

1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义


Date类 举例:

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //拷贝构造函数
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  //赋值运算符重载
  Date& operator=(const Date& d)
  {
    if (this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;
  }
private:
  int _year;
  int _month;
  int _day;
};


int main()
{

  Date d1(2024,5,5);
  //拷贝构造函数
  Date d2(2024,5,6);
  Date d3;
  d3 = d1 = d2;

  return 0;
}

注意事项:


  1. 赋值运算符重载是默认构造函数,不写,会进行简单的赋值和浅拷贝类似。通常需要为包含动态分配内存的类重载赋值运算符,以执行深拷贝操作。深拷贝意味着为新对象分配新的内存,并复制原对象所指向的内存内容,从而确保两个对象独立拥有自己的内存资源。
  2. 注意区分什么是赋值,什么时候是构造:
Date d1(2024,5,5); //构造
Date d2 = d1;      //构造
Date d3; 
d3=d1;             //赋值
辨别方法:已存在的对象初始化另一个对象叫构造;两个都存在的对象,则是赋值。
//小技巧:看前面有没有Date 类

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};


这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!


总结

本章节介绍了四个主要的默认成员函数,还有两个不常用就没有过多介绍。


相关文章
|
21小时前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
8天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
9天前
|
数据安全/隐私保护 C++
|
8天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
8天前
|
存储 编译器 C语言
【C++】string类的使用①(默认成员函数
本文介绍了C++ STL中的`string`类,它是用于方便地操作和管理字符串的类,替代了C语言中不便的字符数组操作。`string`基于`basic_string`模板,提供类似容器的接口,但针对字符串特性进行了优化。学习资源推荐[cplusplus.com](https://cplusplus.com/)。`string`类提供了多种构造函数,如无参构造、拷贝构造、字符填充构造等,以及析构函数和赋值运算符重载。示例代码展示了不同构造函数和赋值运算符的用法。
|
8天前
|
编译器 C++
【C++】类和对象⑤(static成员 | 友元 | 内部类 | 匿名对象)
📚 C++ 知识点概览:探索类的`static`成员、友元及应用🔍。
|
9天前
|
算法 C++ 容器
|
9天前
|
存储 安全 编译器
|
13天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
13天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。