类与对象(2)

简介: 类与对象(2)

类中的六个默认成员函数

构造函数

构造函数的作用:在创建对象时,同时帮助我们给对象一个初始值,并不是给对象开辟空间

构造函数的定义如下:

1️⃣ 函数名与类名一样

2️⃣ 构造函数是不带返回值的

3️⃣ 构造函数支持重载

4️⃣ 对象进行实例化时,编译器会自动调用构造函数

让我们来看一看以下这段代码,来看一下如何使用构造函数:

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

public:
  Date() {
    _year = 2023;
    _month = 11;
    _day = 2;
  }
  Date(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
  }
  void print()
  {
    cout << _year << _month << _day << endl;
  }
};

int main()
{
  Date d1();//编译器会当做是函数的声明,是要去调用函数
  Date d2;//调用Date类的不带参数构造函数
  d2.print();

  Date d3(2024, 11, 2);//调用有参数的构造函数
  d3.print();
  return 0;
}

如果自己不写构造函数,编译器也是会生成默认不带参数的构造函数!默认的构造函数有什么作用呢,让我们来看下面这段代码来进一步的理解:

class people {
private:
  int _age;
  double _height;
  double _weight;

public:
  people() {
    _age = 1;
    _height = 1;
    _weight = 1;
  }

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

  people p;

public:

  void print()
  {
    cout << _year<<" " << _month<<" "<< _day << endl;
  }
};

int main()
{
  Date d1();//编译器会当做是函数的声明,是要去调用函数
  Date d2;//调用Date类的默认构造
  d2.print();
}


我们可以发现,在d2这个对象中_year等是一个随机值,对于people这个类型的变量p是可以进行初始化的!事实上,在C++中,int/char/指针…等,也就是对于哪些可以直接拿来用的类型我们称为内置类型,像利用union/struct/class等关键字定义的类型,我们称为自定义类型。对于内置类型,构造函数是不进行处理的,对于自定义类型,是会去调用这个类型的默认构造!


编译器默认生成的无参构造函数,用户自己写的不带参数的默认构造函数,以及全缺省的构造函数我们都称之为默认的构造函数,而且一旦我们自己显式的写了构造函数,编译器就不会在生成构造函数了!


在C++11中,打了一个补丁,可以针对内置成员变量给定一个缺省值!注意此时成员变量仍然是一个定义,并没有因为给定这个缺省值就初始化了一个空间!


析构函数

与构造函数功能相反,析构函数的作用就是对象销毁时,自动调用析构函数,从而完成对象中的资源清理工作!

语法格式如下:


1️⃣ 也是与类名相同的函数,函数名前面要加上~

2️⃣ 析构函数同样也是无参数也无返回值

3️⃣ 析构函数不能构成重载,一个类中只可以有一个析构函数,若用户显式写出,编译器就不在生成

4️⃣ 析构函数会在对象生命周期结束后,自动调用!


结合下列代码:

#include <iostream>
using namespace std;

class Date {
private:
  int _year;
  int _month;
  int _day;
public:
  Date()
  {
    _year = 2023;
    _month = 11;
    _day = 2;
  }
  //析构函数
  ~Date()
  {
    cout << "Date()" << endl;
  }

  void print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
};

class Stack {
  int* _x;
  int _top;
  int _capacity;
public:
  Stack(int capacity = 3) 
  {
    _x = (int*)malloc(sizeof(int) * capacity);
    if (_x == nullptr)
    {
      cout << "申请空间失败" << endl;
      return;
    }
    _capacity = capacity;
    _top = 0;
  }
  void print()
  {
    cout << _top << " " << _capacity << endl;
  }
  ~Stack()
  {
    free(_x);
    _top = _capacity = 0;
    _x = nullptr;
  }
};
int main()
{
  Date d1;
  Stack st1;
  return 0;
}


析构函数类似于构造函数,对于内置类型,是不做处理的,对于自定义类型,是会去调用它的析构函数进行处理!对于我们之前所学的栈,一般就需要我们自己写析构函数,因为栈需要向系统申请资源开辟空间!这时我们需要利用析构函数,将这块资源进行清理,防止资源泄露!Date类可以使用默认的析构函数,Date类中全是内置类型,没有向系统申请空间!

图解如下:

5551ce686f74b81d9e677329c0591c7c_17398ca9683f426aa31a613cc4a8b336.png


拷贝构造

在C语言学习结构体中,我们知道,是可以传值传参的。在C++中,那么对于自定义类型进行传值传参会发生什么呢?

class Date {
private:
  int _year;
  int _month;
  int _day;
public:
  Date()
  {
    _year = 2023;
    _month = 11;
    _day = 2;
  }
  //析构函数
  ~Date()
  {
    cout << "Date()" << endl;
  }

  void print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
};

class Stack {
  int* _x;
  int _top;
  int _capacity;
public:
  Stack(int capacity = 3) 
  {
    _x = (int*)malloc(sizeof(int) * capacity);
    if (_x == nullptr)
    {
      cout << "申请空间失败" << endl;
      return;
    }
    _capacity = capacity;
    _top = 0;
  }
  ~Stack()
  {
    free(_x);
    _top = _capacity = 0;
    _x = nullptr;
  }
};
void func(Date a)
{
  a.print();
}
void func1(Stack st1)
{
  
}
int main()
{
  Date x;
  func(x);

  Stack st;
  func1(st);
  return 0;
}

运行代码,我们可以发现会报错!这是为什么呢?这是因为在进行传值传参的过程中,我们进行的只是值拷贝(浅拷贝),而在清理对象资源的时候,会自动调用析构函数,而对于Stack这样的类,同一块空间是会被释放两次!

上述代码图解过程:

在C++中,为了解决上述问题,规定,自定义类型在传值传参必须先调用拷贝构造函数!

拷贝构造函数的特点:

1️⃣ 无参数无返回值,函数名与类名相同,与构造函数构成重载

2️⃣ 函数的参数只有一个,是类类型的引用,不可以使用传值方式


在这里需要特别说明一下为什么拷贝构造函数不可以使用传值方式!先假设拷贝构造是传值传参的,调用函数进行传值传参需要先调用拷贝构造,而拷贝构造又是传值传参,又需要接着调拷贝构造……就这样套娃下去,形成一个无限递归!

改进代码如下:

//拷贝构造
Stack(const Stack& t)
{
  _x = (int*)malloc(sizeof(int) * t._capacity);
  if (_x == nullptr)
  {
    perror("malloc fail");
  }
  _capacity = t._capacity;
  _top = t._top;
  memcpy(_x, t._x,sizeof(int)*t._capacity);
}

只需要在Stack类中重新写构造函数,实现对资源的拷贝,这样就不会存在对一块空间释放两次的问题!


其实对于Date这样的类,就算我们使用默认生成的拷贝构造,也是可以的,因为Date类中没有向系统申请资源!而对于Stack这样的类就必须我们自己实现,因为它向系统申请开辟了空间,我们需要拷贝这块空间的内容!同时,为了防止拷贝过程中,改变引用中的内容,我们通常会加上const修饰!

class myqueue
{
  Stack _pushs;
  Stack _pops;
  int _size;
};
int main()
{
  myqueue q1;
  myqueue q2(q1);
  return 0;
}

在上面的代码基础上,我们定义一个myqueue类,通过打断点按F11可以发现:拷贝构造对于自定义类型会去调用它的拷贝构造,对于内置类型会完成值拷贝!

学习了拷贝构造,我们可以来看以下这段代码,再来理解一下引用的用途:

Date test()
{
  Date x;
  x.print();
  return x;
}
int main()
{
  return 0;
}

你觉得返回的是x还是x拷贝的临时对象呢?实际上,x出了test函数就会销毁,所以只可以返回x的拷贝!如果我们写出如下的代码:

Date& test()
{
  static Date x;
  x.print();
  return x;
}
int main()
{
  return 0;
}

此时,由于x出了作用域还没有销毁!所以我们可以通过传引用返回!这样会减少拷贝对象的时间,是不是会大大提高效率呢?


赋值运算符重载

我们知道,内置类型的比较是可以直接用符号来进行比较的!C++为了增强代码的可读性引入了运算符重载的!运算符重载是具有特殊函数名的函数!


函数名字:operator 后面接需要重载的运算符符号()

下面来展示一段代码:

class Date {
public:
  int _year;
  int _month;
  int _day;
public:
  Date()
  {
    _year = 2023;
    _month = 11;
    _day = 2;
  }
  void print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
};


bool operator==(Date& x,Date& y)
{
  return x._year == y._year
    && x._month == y._month
    && x._day == y._day;
}

int main()
{
  Date d1;
  Date d2;

  cout << (d1==d2) << endl;
  return 0;
}

为了便于理解,我们暂时先将类的成员改为public,我们通过operator关键字重载==符号,这样我们就可以像内置类型一样,通过 符号来判断是否相等! 在C++中,对于运算符重载,我们一般是放入类中处理,但是传参就要少一个了,因为有一个隐藏的this指针!具体代码可以参考下面:


  bool operator==( Date& y)
  {
    return _year == y._year
      && _month == y._month
      && _day == y._day;
  }

此外我们在介绍两个特殊的运算符重载:

  //前置++运算符重载
  Date& operator++()
  {
    *this += 1;
    return *this;
  }
  //后置++运算符重载
  Date operator++(int)
  {
    Date tmp(*this);
    *this += 1;
    return tmp;
  }

对于后置++运算符,我们通过给定一个整型来表示这是后置++(注意这个是语法规定)

编译器自己会根据我们所写的符号去调用相应的运算符重载函数(这个是需要我们自己写的)!编译器默认生成的成员函数是赋值运算符的重载,也就是说我们自己不写,也是会自动生成的!

  //赋值赋值运算符重载
  Date& operator=(const Date& y) //返回值的目的是为了进行连续赋值,引用可以提高返回的效率
  {
    //防止自己和自己赋值
    if (this != &y)
    {
      _year = y._year;
      _month = y._month;
      _day = y._day;
    }
    return *this;
  }

和拷贝构造的作用一样,对于内置类型会进行值拷贝(浅拷贝),自定义类型调用它的赋值重载运算符!但是对于Stack这样的类,向系统申请了额外资源的需要我们自己重写赋值重载实现深拷贝!

拷贝构造与赋值拷贝之间的区别

拷贝构造是针对还没有进行创建的对象,而赋值拷贝要求两边都是已经创建好了的对象进行赋值拷贝!代码如下:

int main()
{
  Date d1(2023,11,11);
  //下面都属于调用拷贝构造进行拷贝
  Date d2(d1);
  Date d3 = d2;
  //下面属于赋值拷贝
  d1 = d2;
  return 0;
}

对于运算符重载需要注意事项:

1️⃣ 对于内置类型运算符的含义不可以改变。例如:不能重载+符号,里面写成了-的含义了

2️⃣ 不可以通过连接其他符号创建新的操作符。例如:opertor@

3️⃣ 重载操作符必须要有一个类类型的参数

4️⃣ 作为类的成员函数重载时,其形参看起来比操作数数目少1,因为成员函数第一个参数是隐藏的this指针

5️⃣.* ::(域限定符) sizeof ?:(三目操作符) .这五个运算符是不可以运算符重载的!

const成员函数

被const修饰的成员函数被称为const成员函数!

  //大于重载
  bool operator>(Date& t)const //在函数参数后面加上const修饰
  {
    if (_year > t._year)
    {
      return true;
    }
    else if (_year == t._year && _month > t._month) {
      return true;
    }
    else if (_year == t._year && _month == t._month && _day == t._day)
    {
      return true;
    }
    return false;
  }

实际上const修饰的是隐藏的this指针,变成了const Date类型,表明在该成员函数中不能对类的任何成员修改!还需要注意的是当自定义类型的声明与定义分离时,我们如果要用const进行修饰,那么就要声明和定义一起加上!

当我们定义const对象和非const对象时,调用>重载运算符如图所示!这是为啥呢?原因就是d1是const Date类型对象,传入>重载运算符时第二个参数是Date*类型!可以理解为权限放大,这样是不允许的!权限只可以平移和缩小!


取地址操作符重载

最后两个默认的成员函数是取地址操作符的重载

  Date* operator&()
  {
    return this;
  }

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

一般这两个默认的成员函数不需要我们自己进行重载,直接拿来用就可以了!如果我们想要让别人获取指定的内容可以使用!

//让你只可以拿到地址为0x11223344的内容
Date* operator&()
{
  return (Date*)0x11223344;
}

const Date* operator&()const
{
  return (const Date*)0x11223344;
}
目录
相关文章
|
5天前
|
编译器 C# C++
【C++】类和对象(四)
【C++】类和对象(四)
|
7月前
|
存储 编译器 C++
|
7月前
|
编译器 C++
C++:类和对象(下)---对类和对象深入一些的理解
C++:类和对象(下)---对类和对象深入一些的理解
|
7月前
|
存储 安全 编译器
【C++】类和对象(中)(二)
【C++】类和对象(中)(二)
【C++】类和对象(中)(二)
|
5天前
|
编译器 C++
类和对象(3)
类和对象(3)
6 1
|
5天前
|
编译器 C++
C++类和对象(下)
C++类和对象
47 0
|
5天前
|
存储 编译器 程序员
C++类和对象(中)
C++类和对象
66 0
|
5天前
|
存储 安全 编译器
|
10月前
|
C#
C#——类和对象
C#——类和对象
55 0
|
5天前
|
存储 Java 编译器
C嘎嘎之类和对象中
C嘎嘎之类和对象中
24 0