【C++类和对象(中)】—— 我与C++的不解之缘(四)

简介: 【C++类和对象(中)】—— 我与C++的不解之缘(四)

前言:

接下来进行类和对象中的学习,了解类和对象的默认成员函数

一、类和对象默认成员函数

       默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数。

一个类,我们不显示实现的情况下,编译器就会默认生成一下留个默认成员函数。

       这里前4个(构造函数析构函数拷贝构造赋值重载)是重难点。

C++11以后还会增加两个默认成员函数,移动构造移动赋值

默认成员函数十分重要,从以下两个方面去深入了解:

  1. 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求
  2. 编译器默认生成的函数不满足我们的需求,我们需要直接实现,那么如何自己实现呢?

       1.1、构造函数

       构造函数,是特殊的成员函数;这里需要注意,虽然名字叫做构造函数,但是构造函数的主要任务不是开辟空间创建对象(我们经常使用的局部对象是栈帧创建时,空间就已经开辟 好了),而是对象实例化时初始化对象

       这里构造函数本质上是替代实现的Stack和Data类中所写的 Init 函数,构造函数自动调用这一特点就完美替代了 Init 函数

1.1.1、构造函数的特点

构造函数的特点如下:

1、函数名和类名相同。

2、无返回值(返回值不需要写,void也不需要)。

3、对象实例化时系统会自动调用对应的构造函数。

4、构造函数可以重载

5、如果类没有显示定义构造函数,C++编译器会自动生成一个无参的默认构造函数;如果显示写了构造函数,编译器就不会再生成。

6、无参构造函数、全缺省构造函数、我们不写时编译器默认生成的构造函数,这三个都叫做默认构造函数。

7、我们不写,编译器默认生成的构造函数,对内置类型成员变量的初始化没有要求(是否初始看编译器);对于自定义类型成员变量,要求调用这个成员函数的默认构造函数初始化(如果这个成员变量没有默认构造函数,就会报错(这里要初始化这个成员变量,需要使用初始化列表来解决,后面会学习到))。

1.1.2、构造函数

       这里来看一下构造函数的前几个特点。

首先就是,构造函数的函数名和类名相同而且无返回值(不需要写返回值)

class Data
{
public:
  Data()
  {
    _year = 1;
    _month = 1;
    _day = 1;
  }
 
private:
  int _year;
  int _month;
  int _day;
 
};

       这里Data类里面的Data函数就是显示实现的构造函数(显示实现了构造函数,编译器就不会默认生成);

再来看,构造函数可以重载,我们就可以这样写:

class Data
{
public:
  Data()
  {
    _year = 1;
    _month = 1;
    _day = 1;
  }
  Data(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
 
private:
  int _year;
  int _month;
  int _day;
 
};

       这里两个构造函数就形成了重载,再创建对象时,不给参数就会调用第一个构造函数,给参数就会调用第二个参数。

最后,来看对象实例化时会自动构造函数:

#include<iostream>
using namespace std;
class Data
{
public:
  Data()
  {
    cout << "Data()" << endl;
    _year = 1;
    _month = 1;
    _day = 1;
  }
  Data(int year, int month, int day)
  {
    cout << "Data(int year, int month, int day)" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
 
private:
  int _year;
  int _month;
  int _day;
 
};
int main()
{
  Data d1;
  Data d2(2006, 7, 20);
 
  return 0;
}

1.1.3、默认构造函数

默认构造函数有三种:

       无参构造函数全缺省构造函数和我们不写时编译器默认生成的构造函数

这三个函数有且只有一个存在(不能同时存在);

       这里虽然无参构造函数和全缺省构造函数形成函数重载,但是在函数调用时会存在歧义:

#include<iostream>
using namespace std;
class Data
{
public:
  Data()
  {
    cout << "Data()" << endl;
    _year = 1;
    _month = 1;
    _day = 1;
  }
  Data(int year = 1, int month = 1, int day = 1)
  {
    cout << "Data(int year, int month, int day)" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
 
private:
  int _year;
  int _month;
  int _day;
 
};
int main()
{
  Data d1;
 
  return 0;
}

这里总结一下,编译传实参就可以调用的构造就是默认构造。

1.1.4、编译器默认生成的默认构造函数        

       编译器默认生成的构造函数,对于内置类型(整型,浮点型,字符类型,指针等)初始化没有要求,可能会初始化,也可能不做任何处理;对于自定义类型成员变量初始化会调用这个成员变量的默认构造函数(如果不存在默认构造就报错)。

       所以,大多情况下我们都需要自己实现构造函数。

       1.2、析构函数

       析构函数与构造函数的功能相反,析构函数不是完成对象本身的销毁(局部对象是存在栈帧的,函数结束栈帧就销毁了,局部对象就自动释放了);C++规定在销毁时会自动调用析构函数,完成对像中资源的清理释放工作。

       相关函数的功能类比之前 Stack 栈实现的DesTroy 销毁功能,对申请资源进行释放。

1.2.1、析构函数特点

1、析构函数名是在类名前面加上字符 ~

2、无参数返回值(与构造函数一样,不需要加void)。

3、一个类只能有一个析构函数,如果没有显示定义,系统就会自动生成默认的析构函数。

4、对象生命周期结束时,系统就会自动调用析构函数。

5、与构造函数类似,我们不显示写,编译器默认生成的对内置类型不做处理,自定义类型就会调用它的析构函数。

6、这里需要注意,我们显示写了析构函数,对于自定义类型也会调用它的析构函数(也就是说,无论说明情况下,自定义类型都会自动调用析构函数。

7、如果类没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数,就比如Data(日期类);如果默认生成的析构就满足我们的需求,不用写,比如MyQueue(用两个栈实现的队列);如果有资源申请,一定要自己写析构,否则就会造成资源泄漏,比如Stack(栈)。

8、一个局部域的多个对象,C++ 规定后定义的先调用析构

1.2.2、析构函数

自己实现析构函数,这里拿栈Stack类来举例:

class Stack
{
public:
  //构造函数
  Stack(int capacity = 4)
  {
    cout << "Stack" << endl;
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == NULL)
    {
      perror("malloc fail");
      return;
    }
    _capacity = capacity;
    _top = 0;
  }
 
  //析构函数
  ~Stack()
  {
    cout << "~Stack" << endl;
    if (_arr)
      free(_arr);
    _arr = nullptr;
    _capacity = _top = 0;
  }
private:
  int* _arr;
  int _top;
  int _capacity;
};

       这里,对象生命周期结束时会自动调用析构函数,看一下释放真的调用了?

int main()
{
  Stack st;
 
  return 0;
}

1.2.3、自定义类型自动调用其析构函数

       对于自定义类型,无论我们写函数不写析构,都会自动调用其析构函数。

class Stack
{
public:
  //构造函数
  Stack(int capacity = 4)
  {
    cout << "Stack" << endl;
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == NULL)
    {
      perror("malloc fail");
      return;
    }
    _capacity = capacity;
    _top = 0;
  }
 
  //析构函数
  ~Stack()
  {
    cout << "~Stack" << endl;
    if (_arr)
      free(_arr);
    _arr = nullptr;
    _capacity = _top = 0;
  }
private:
  int* _arr;
  int _top;
  int _capacity;
};
class MyQueue
{
public:
 
private:
  Stack pushst;
  Stack popst;
};
int main()
{
 
  MyQueue mq;
 
  return 0;
}

       

       这里可以看到,自定义类型构造和析构都调用了它的构造函数和析构函数。

       1.3、拷贝构造函数

       如果构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数(也就是拷贝构造函数是特殊的构造函数)。

1.3.1、拷贝构造的特点

1、拷贝构造函数是构造函数的一个重载。

2、C++规定,自定义类的对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

3、拷贝构造的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归。

4、如果未显示定义拷贝构造,编译器会自动生成拷贝构造函数;

5、  像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。

       像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。

       像MyQueue这样的类型内部主要是⾃定义类型 Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现

MyQueue的拷⻉构造。

6、    传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少 拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

1.3.2、拷贝构造参数

       C++规定自定义类型对象拷贝时必须调用拷贝构造:

class Data
{
public:
  //构造
  Data(int year = 1 , int month = 1, int day = 1)
  { 
    cout << "Data(int year, int month, int day)" << endl;
    _year = year;
    _month = month;
    _day = day;
  }
  //拷贝构造
  Data(Data& d)
  {
    cout << "Data(Data&)" << endl;
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
private:
  int _year;
  int _month;
  int _day;
 
};
int main()
{
  Data d1(2006, 7, 20);
  Data d2(d1);
  Data d3 = d1;
  return 0;
}

       拷贝构造函数的第一个参数必须是类类型对象的引用,如果使用传值调用:

就会像下面这样,名称传参都会调用拷贝构造,调用完传参再次调用拷贝构造,无穷递归下去。

1.3.3、编译器默认生成的拷贝构造函数

       编译器默认生成的拷贝构造,对内置类型成员会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量就会调用它的拷贝构造函数。

二、赋值运算符重载

       2.1、运算符重载

1、当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

2、运算符重载是具有特殊名字的函数,他的名字是由operator后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。

3、重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

4、如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。

5、运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。

6、不能通过连接语法中没有的符号来创建新的操作符:比如operator@

7、.*    ::   sizeof   ?:   .   注意以上5个运算符不能重载。(选择题里面常考,要记一

重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int

operator+(int x,int y)

8、一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义。

9、  重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。

       C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。

10、重载<<>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。

有了运算符重载,对于自定义类型的对象,我们就可以像内置类型那样,直接进行+/-/+=/-= 等操作。(要 根据自定义类的需求写)。

应用举例:

       这里以日期类为例,实现一个运算符重载 ==

//运算符重载
// == 
bool Data::operator==(Data& d)
{
  if (_year == d._year && _month == d._month && _day == d._day)
  {
    return true;
  }
  return false;
}

常见的运算符重载

       我们基本上可以重载所以的算术运算符、关系运算符和赋值运算符等,

算术运算符:+、-、*、/ ,用于自定义类型的算术运算。

关系运算符:==、!=、<、>、<=、>= ,用于自定义类型的比较操作。

赋值运算符:=,用于自定义类型的赋值操作。(当自定义类型(栈)包含动态分配的内存时,需要深拷贝以避免悬挂指针等问题。)

自增自减运算符:++、--,用于自定义类型的自增和自减操作。

下标运算符:[ ],用于自定义类型的数组或类似数组的操作。

流插入和提取运算符:<<、>>,用于自定义类型的输入输出操作。

函数调用运算符:(),允许自定义类型的对象像函数一样被调用。

成员访问运算符:->,一般 与智能指针或类似智能指针的类一起使用,用于访问指针所指向对象的成员。

前置++和后置++重载

C++规定

后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。

前置++ 先使用再+1;而后置++是先+1再使用。

//前置++
Data& operator++();
//后置++
Data& operator++(int);

       2.2、赋值运算符重载

       赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。

1、赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成1.const 当前类类型引用,否则会传值传参会有拷贝

2、有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。

3、没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷3贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。

4、  像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就4可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。

       像Stack这样的类,虽然也都是内置类型,但是 a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。

       像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载也不需要我们显示实现MyQueue的赋值运算符重载。

这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。

       现在就来哦实现日期类巩固这方面的知识。

       2.3、日期类的实现

Data.h:

#pragma once
#include<iostream>
#include<assert.h>
class Data
{
  friend std::ostream& operator<<(std::ostream& out, const Data& d);
  friend std::istream& operator>>(std::istream& in, Data& d);
public:
  //构造函数
  Data(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
 
  //析构
  
  //拷贝构造
  Data(Data& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  //输出
  void Print();
 
  //运算符重载
  bool operator==(Data& d);
  bool operator!=(Data& d);
  bool operator<(Data& d);
  bool operator<=(Data& d);
  bool operator>(Data& d);
  bool operator>=(Data& d);
 
  Data& operator+=(int day);
  Data& operator+(int day);
  Data& operator-=(int day);
  Data& operator-(int day);
  int operator-(Data& d);
 
  Data& operator++();
  Data& operator++(int);
  Data& operator--();
  Data& operator--(int);
 
  //ostream& operator<<(ostream& out);
private:
  int _year;
  int _month;
  int _day;
};

Data.cpp:

#include"Data.h"
using namespace std;
 
//获得当前月份天数
int GetMonthDay(int year, int month)
{
  assert(month > 0 && month < 13);
  static int arr[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 40 == 0)))
  {
    return 29;
  }
  return arr[month];
}
//输出
void Data::Print()
{
  cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
//运算符重载
// == 
bool Data::operator==(Data& d)
{
  if (_year == d._year && _month == d._month && _day == d._day)
  {
    return true;
  }
  return false;
}
// !=
bool Data::operator!=(Data& d)
{
  return !(*this == d);
}
 
// < 
bool Data::operator<(Data& d)
{
  if (_year < d._year)
  {
    return true;
  }
  else if (_year == d._year && _month < d._month)
  {
    return true;
  }
  else if (_year == d._year && _month == d._month && _day < d._day)
  {
    return true;
  }
 
  return false;
}
//  <=
bool Data::operator<=(Data& d)
{
  return (*this) == d || (*this) < d;
}
// >
bool Data::operator>(Data& d)
{
  if (_year > d._year)
  {
    return true;
  }
  else if (_year == d._year && _month > d._month)
  {
    return true;
  }
  else if (_year == d._year && _month == d._month && _day > d._day)
  {
    return true;
  }
 
  return false;
}
// +=
bool Data::operator>=(Data& d)
{
  return (*this) == d || (*this) > d;
}
Data& Data::operator+=(int day)
{
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _month = 1;
      _year++;
    }
  }
  return *this;
 
}
// +
Data& Data::operator+(int day)
{
  Data tmp = *this;
  tmp += day;
  return tmp;
}
 
// -=
Data& Data::operator-=(int day)
{
  _day -=day;
  while (_day <= 0)
  {
    _month--;
    if (_month == 0)
    {
      _month = 12;
      _year--;
    }
    _day += GetMonthDay(_year, _month);
  }
  return (*this);
}
// - 天数
Data& Data::operator-(int day)
{
  Data d(*this);
  d -= day;
  return d;
}
 
// - 日期
int Data::operator-(Data& d)
{
  Data min(*this);
  Data max(d);
  if ((*this) > d)
  {
    min = d;
    max = *this;
  }int count = 0;
  while (min != max)
  {
    count++;
    min += 1;
  }
  return count;
  
}
//++
//前置++
Data& Data::operator++()
{
  (*this) += 1;
  return *this;
}
//后置++
Data& Data::operator++(int)
{
  Data d(*this);
  (*this) += 1;
  return d;
}
 
//前置--
Data& Data::operator--()
{
  (*this) -= 1;
  return *this;
}
//后置--
Data& Data::operator--(int)
{
  Data d(*this);
  (*this) -= 1;
  return d;
}
 
// <<
//ostream& Data::operator<<(ostream& out)
//{
//  out << _year << "年 " << _month << "月 " << _day << "日 " << endl;
//  return out;
//}
std::ostream& operator<<(std::ostream& out, const Data& d)
{
  cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
 
  return out;
}
//  >>
std::istream& operator>>(std::istream& in, Data& d)
{
  cout << "依次输入 年 月 日" << endl;
  in >> d._year >> d._month >> d._day;
  return in;
}

三、取地址运算符重载

       3.1、const 成员函数

1、 将const修饰的成员称之为从const成员函数,const成员放到成员函数参数列表的后面。

2、 const实际修饰该成员函数的this指针,表明在该成员函数中不能对类的任何成员进行修改。

3、 const修饰Data类的Print成员函数,

Print隐含的this指针由Data* const this 变为const Data* const this

#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  // void Print(const Date* const this) const
  void Print() const
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  // 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
  Date d1(2024, 7, 5);
  d1.Print();
  const Date d2(2024, 8, 5);
  d2.Print();
  return 0;
}

       3.2、 取地址运算符重载

       取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。

class Data
{
public:
  Data* operator&()
  {
    return this;
  }
  const Data* operator&()const
  {
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};

这里我们不想要访问到类对象的地址,也可以返回nullptr。

相关文章
|
1天前
|
算法 安全 Linux
【C++STL简介】——我与C++的不解之缘(八)
【C++STL简介】——我与C++的不解之缘(八)
|
1天前
|
存储 C语言 C++
【C/C++内存管理】——我与C++的不解之缘(六)
【C/C++内存管理】——我与C++的不解之缘(六)
|
1天前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1天前
|
存储 编译器 C++
【C++模版初阶】——我与C++的不解之缘(七)
【C++模版初阶】——我与C++的不解之缘(七)
|
1天前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)
|
1天前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
1天前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
3天前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
17 3
|
3天前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
14 2
|
3天前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
34 1