类与对象(中)

简介: 类与对象(中)

类的6个默认成员函数


当类中没有任何成员时,称作空类

但是呢,编译器会自动生成6个默认成员函数,所以当一个类中没有任何成员时,还是存在6个默认函数的


默认成员函数:使用者没有实现,编译器自动生成;使用者自己实现,则使用已实现的


49d0b93a0ba9f1bc08db33c7fcb1bf5b_5a8264f1d1e94e5d84fbeb033421378c.png


构造函数


概念


观察下面代码


class Date
{
public:
  void Init(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.Init(2022, 12, 4);
  d1.Print();
  return 0;
}


对于 Date类而言,难道只能通过共有函数 Init给对象进行初始化吗?而且如果每次创建对象都需要调用此方式初始化,是不是很麻烦呢?

由此,便引入构造函数的概念,可以在对象创建时就进行初始化


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

功能类似于C语言中的Init


特性


构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称为构成,但其功能不是开辟空间创建对象,而是初始化对象


函数名与类名相同

没有返回值

对象实例化(创建)时编译器会自动调用相应的构造函数

构造函数可以重载

class Date
{
public:
  //无参构造函数
  Date()
  {
  }
  //含参构造函数
  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;
};
void test()
{
  Date d1;//调用无参构造函数
  d1.Print();
  Date d2(2022, 12, 4);//调用含参构造函数
  d2.Print();
}


c0b62e58a96e4f9274ae6f951acfe06f_9b29b70d97494d27b4b83df4e2cbe9ee.png


这里有一个点需要注意,Date d1调用无参构造函数时,不可以加上(),因为会造成函数声明


结果如下


bfb0b62d0d93edaa7a73eb097d9c062d_fa1b21550a144599b1c630b41edf237e.png


如果使用者没有在类中实现构造函数,编译器便会自动生成一个无参的默认构造函数;不过如果使用者已经实现,则编译器不会再生成

class Date
{
public:
  //没有构造函数
  void Print()
  {
  cout << _year << " " << _month << " " << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
void test()
{
  Date d1;
  d1.Print();
}


6ee3201f619711222fdef2c4513e278f_08a66942cb5144d6ba4ba959d12e19b2.png


在5中可以观察到,当使用者不在类中实现构造函数,编译器会自动生成默认构造函数,但是呢,运行的结果却是随机值,似乎没有任何用处。既然这样的话,还不如使用者自己在类中实现需要的构造函数,但真的是如此吗???

原因是在C++中,把类型分为内置类型(基本类型)和自定义类型。内置类型:语言提供的数据类型,例如int/char/double等;自定义类型:使用者自己定义的类型,例如class/struct/union等

编译器生成的默认构造函数只会对自定义类型起作用

之后为了解决默认构造函数不处理内置类型的问题,规定内置类型成员变量在类中声明时可以进行赋值


只有无参构造函数,全缺省构造函数和编译器默认生成的构造函数称为默认构造函数,并且默认构造函数只能存在一个

不传参数就可以调用的构造函数,就称作默认构造


class Date
{
public:
  //无参构造函数
  Date()
  {
  _year = 2022;
  _month = 12;
  _day = 5;
  }
  //全缺省构造函数
  Date(int year = 2022, int month = 12, int day = 5)
  {
  _year = year;
  _month = month;
  _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
void test()
{
  Date d;
}


因为默认构造函数只能存在一个,所以程序会崩溃,也就是二义性


41605318acd008cdc4f8fe256c764df1_435d8361e86446388d206089df68d0d8.png


析构函数


概念


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


特性


析构函数名是在类名前面加上~

无参数无返回值类型

一个类只能有一个析构函数,若使用者没有在类中实现,系统便会自动生成默认的析构函数。析构函数不能重载

对象生命周期结束时,编译器才会调用析构函数。这里的生命周期包括生:局部域,全局域,malloc申请的空间

class Stack
{
public:
  Stack(int capacity = 4)
  {
  cout << "Stack(int capacity = 4)" << endl;
  _a = (int*)malloc(sizeof(int) * capacity);
  if (_a == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  _top = 0;
  _capacity = capacity;
  }
  ~Stack()
  {
  cout << "~Stack()" << endl;
  free(_a);
  _a = nullptr;
  _top = _capacity = 0;
  }
  void Push(int x)
  {
  //...
  _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack sk;
  sk.Push(1);
  sk.Push(2);
  sk.Push(3);
  sk.Push(4);
  return 0;
}


在创建对象的同时构造函数进行初始化,结果如下


06a87c0751600a9a07ddc81b6f949671_18d6f8b6d7c04f3094b5d99e59354135.png


将四个数值全部插入栈之后,结果如下


1c69573ff83766f687defca92b836e79_e51024f5e0964e7aad4613ae45f0ed4a.png


此时_top的值为4,表示此时栈中已经存在四个数值


当程序跑到return 0时,主函数生命周期结束(全局域)调用析构函数,此时监视结果如下


ac9bab50fce3819c6013ad359870c8b7_e29c02beb36b411c881dbc84b7065dbb.png


默认构造函数只处理自定义类型成员变量,所以类似的,编译器生成的默认析构函数,对自定义类型成员变量才会调用它的析构函数

class M
{
public:
  ~M()
  {
  cout << "~M()析构函数" << endl;
  _m = 0;
  }
private:
  int _m;
};
class Date
{
public:
  void Init(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << _year << " " << " " << _month << " " << _day << endl;
  }
private:
  //内置类型
  int _year;
  int _month;
  int _day;
  //自定义类型
  M _m;
};
int main()
{
  Date d1;
  return 0;
}


6b266566c911a9dcd074689369ac964c_46abb5f567894746bdfe7bbb74fb61f5.png


对象 d1的成员变量,包括内置类型和自定义类型。其中内置类型在 d1销毁时不需要资源清理,也就是不需要调用析构函数;自定义类型在 d1销毁时需要调用其本身的析构函数,也就是调用 M类的析构函数,从而销毁 _m。虽然 Date中没有显示析构函数,但编译器会默认生成一个析构函数,其目的是在内部调用 M的析构函数,也就解释了运行结果为什么会打印 ~M()析构函数


判断析构函数是否需要使用者实现的方法是,如果类中没有申请资源,析构函数可以不写,直接使用编译器默认生成的析构函数;如果有资源申请,一定要写,否则会造成资源泄漏

总结

面对需求:编译器默认生成的就可以,就不要自己写,不满足就自己写

Stack的析构函数需要自己写

Date的不需要自己写,默认生成的就可以


拷贝构造函数


概念


拷贝构造函数也称拷贝初始化,只有一个形参,且整个形参是对相同类型对象的引用(一般由const修饰),再用已存在的类类型对象创建对象时由编译器自动调用


特性


拷贝构造函数是构造函数的一个重载形式

拷贝构造函数的参数只有一个且必须是相同类类型对象的引用;如果使用传值方式,将会引发无穷递归,编译器会报错

class Date
{
public:
  Date(int year = 2022, int month = 12, int day = 5)
  {
  _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;
};
void test()
{
  Date d1;
  Date d2(d1);
}

5135364f1decd2f4922d786b26b77899_3a0a8e2a3ef64e8e8d7c62de67b523ee.png


传值调用的本质就是拷贝一份数据传递给相应的函数


c9b60fa0d3ace250a9fdbd449b619224_9b2bf1d3572342b782e2a8af0f7d1be9.png


若使用者没有在类中实现拷贝构造函数,编译器会生成。拷贝对象时按照内存存储字节序完成拷贝,称为浅拷贝

class Date
{
public:
  Date(int year = 2022, int month = 12, int day = 5)
  {
  _year = year;
  _month = month;
  _day = day;
  }
private:
    //内置类型
  int _year;
  int _month;
  int _day;
};
void test()
{
  Date d1;
  Date d2(d1);
}
int main()
{
  test();
  return 0;
}


监视结果如下


a7cf00cdf9599f07a52bc3ccd6da4a95_1c2f527a86e04ba68c5e2c23395ffbee.png


在编译器生成的默认拷贝构造函数中,内置类型按照字节方式直接拷贝,自定义类型是调用其拷贝构造函数完成拷贝


既然编译器生成的默认构造函数已经可以完成字节序的值拷贝,那么还有自己在类中实现的必要吗???

观察下面的代码


class Stack
{
public:
  Stack(int capacity = 4)
  {
  cout << "Stack(int capacity = 4)" << endl;
  _a = (int*)malloc(sizeof(int) * capacity);
  if (_a == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  _top = 0;
  _capacity = capacity;
  }
  ~Stack()
  {
  cout << "~Stack()" << endl;
  free(_a);
  _a = nullptr;
  _top = _capacity = 0;
  }
  void Push(int x)
  {
  //...
  _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack sk1;
  sk1.Push(1);
  sk1.Push(2);
  sk1.Push(3);
  sk1.Push(4);
  Stack sk2(sk1);
  return 0;
}


运行结果如下


db678efb1fb178e7e3c7bfa225e801c1_e8870c8d518546caa927c1ed1cf49b6a.png


程序直接崩溃,为什么呢,上面也是没有自己写拷贝构造函数,程序正常运行,在这里为什么就不行呢


接下来用一张图来进行解释


2abb59dc244aff53d6575a4bef4eecb7_fe150aeb672d4403a967b49cc8018ecc.png


如果类中没有涉及资源申请,拷贝构造函数便不需要自己实现;

如果涉及到资源申请,拷贝构造函数必须要自己实现,否则就是浅拷贝,程序便会崩溃


改进如下


Stack(const Stack& sk)
  {
  cout << "Stack(const Stack& sk)" << endl;
  _a = (int*)malloc(sizeof(int) *sk._capacity);
  if (_a == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  memcpy(_a, sk._a, sizeof(int) * sk._top);
  _top = sk._top;
  _capacity = sk._capacity;
  }


997571e2db85f5f4258fa707bf3d8761_4c53c102a0784ba796b297bed6dcbb93.png


sk1,sk2中_a所指的不是同一块空间,便完成了深拷贝


需要写析构函数的类,都需要写深拷贝的拷贝构造

不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以满足


拷贝构造函数使用场景

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

函数参数类型是类类型对象

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


赋值运算符重载


运算符重载


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


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

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


不能通过连接其他符号来创建新的操作符:例如operator@

重载操作符必须有一个类类型参数

用于内置类型的运算符,其含义不能改变

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

.* / :: / sizeof / ?: / . 这五个运算符不能重载

重载 ==

operator==


class Date
{
public:
  Date(int year = 1, 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;
};


int main()
{
  Date d1(2022, 12, 5);
  Date d2(2022, 12, 25);
  cout << (d1 == d2) << endl;
  return 0;
}


511dcc82873f4f0e9362d41145fb0e66_e0be9927ee6a4b6289f44af5b42eec7a.png


重载>


bool operator>(const Date& d2)
  {
  if (_year > d2._year)
  {
    return true;
  }
  else if (_year == d2._year && _month > d2._month)
  {
    return true;
  }
  else if (_year == d2._year && _month == d2._month && _day > d2._day)
  {
    return true;
  }
  return false;
  }



重载>= 只需要赋用上面两种运算符即可


bool operator>=(const Date& d2)
  {
  return *this > d2 || *this == d2;
  }


重载+=和重载+


class Date
{
public:
  //判断日期的有效性
  int Getmonthday(int year, int month)
  {
  int monthdayarray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
  {
    return 29;
  }
  else
  {
    return monthdayarray[month];
  }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
    //重载+=
  Date& operator+=(int day)
  {
  _day += day;
  while (_day > Getmonthday(_year, _month))
  {
    _day -= Getmonthday(_year, _month);
    _month++;
    if (_month == 13)
    {
    ++_year;
    _month = 1;
    }
  }
  return *this;
  }
    //重载+
  Date operator+(int day)
  {
  Date ret(*this);
  ret += day;
  return ret;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d(2022, 12, 5);
  d += 50;
  return 0;
}


72b999d928b75fdbfd329daaeaef092f_53aacd228a9c4fa784cae9af4c41cb48.png


赋值运算符重载


赋值运算符重载格式

参数类型:const T(类名)&,传递引用可以提升传参效率

返回值类型:T(类型)&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回this:要复合连续赋值的含义


Date类,没有资源的申请赋值重载较为简单


class Date
{
public:
  //判断日期的有效性
  int Getmonthday(int year, int month)
  {
  int monthdayarray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
  {
    return 29;
  }
  else
  {
    return monthdayarray[month];
  }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = 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(2022, 12, 5);
  Date d2;
  d2 = d1;
  return 0;
}


fe0bfd5af478debc844c089e82d14083_8c22587dae994418aa16bce0393be2b7.png


Stack类,有资源的申请,赋值重载较为复杂


class Stack
{
public:
  Stack(int capacity = 4)
  {
  cout << "Stack(int capacity = 4)" << endl;
  _a = (int*)malloc(sizeof(int) * capacity);
  if (_a == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  _top = 0;
  _capacity = capacity;
  }
  Stack& operator=(const Stack& sk)
  {
  if (this != &sk)
  {
    free(_a);
    _a = (int*)malloc(sizeof(int) * sk._capacity);
    if (_a == nullptr)
    {
    perror("malloc fail");
    exit(-1);
    }
    memcpy(_a, sk._a, sizeof(int) * sk._top);
    _top = sk._top;
    _capacity = sk._capacity;
  }
  return *this;
  }
  ~Stack()
  {
  cout << "~Stack()" << endl;
  free(_a);
  _a = nullptr;
  _top = _capacity = 0;
  }
  void Push(int x)
  {
  //...
  _a[_top++] = x;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack sk1;
  sk1.Push(1);
  sk1.Push(2);
  sk1.Push(3);
  Stack sk2;
  sk2.Push(10);
  sk2.Push(20);
  sk2.Push(30);
  sk1 = sk2;
  return 0;
}

存在三种情况,

sk1中的_a申请的空间比sk2中的_a申请的空间大,相等,小,为了简便处理,在赋值时,先将sk1中_a申请的空间释放,接着将sk2整体拷贝给sk1即可


赋值运算符只能重载成类的成员函数不能重载成全局函数

因为类中的成员变量是私有的,在类外不能进行访问的

赋值运算符如果使用者没有在类中实现,编译器会生成一个默认的


使用者没有在类中实现赋值运算符重载时,编译器会生成一个默认的,且以值的方式逐字节拷贝(浅拷贝)。内置类型成员变量是直接赋值,而自定义类型成员变量需要调用相应类的赋值运算符重载完成赋值

既然编译器生成的默认构造函数已经可以完成字节序的值拷贝,那么还有自己在类中实现的必要吗???

这里与上面拷贝构造的思想类似,就不加赘述

如果类中没有涉及资源管理,赋值运算符不需要写;如果涉及到资源管理使用者必须在类中实现


前置++ 后置++ ++重载


先区分,前置与后置的区别,主要区别就是,返回的结果不同,前置返回的结果是++后的数值;后置返回的结果是++之前的数值,也就是本身;对于- -也是同样的道理


//前置++
Date& operator++()
{
  *this += 1;
  return *this;
}
//后置++,多一个参数,为了与前置区分
Date operator++(int)
{
  Date ret(*this);
  *this += 1;
  return ret;
}


const成员函数


const修饰的成员变量称为const成员变量,const修饰成员函数,实际是修饰该成员变量隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << _year << " " << _month << " " << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2022, 12, 5);
  d1.Print();
  const Date d2;
  d2.Print();
  return 0;
}


e4829f38d4565b21b4c3ac0f45b08afb_76864b5db9554107b54aef24625153e2.png


改善之后

const修饰成员函数,实际是修饰该成员变量隐含的this指针


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


20a39ce5ad67f97e7bd05bb6ce62979e_5f14bc9ca8ad43b1aae8696beb6b3b0b.png


总结:

凡是内部不改变成员变量的,也就是*this对象数据的,此类成员函数都应该加上const进行修饰


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


这里两类默认成员函数一般不需要重新定义,编译器会默认生成


目录
相关文章
|
8天前
|
存储 编译器 程序员
C++:类和对象(中)
C++:类和对象(中)
44 1
|
2天前
|
存储 安全 编译器
类与对象(一)
类与对象(一)
|
8天前
|
编译器 C语言 C++
类与对象(2)
类与对象(2)
8 0
|
8天前
|
存储 编译器 C语言
C++类与对象
C++类与对象
21 0
|
8天前
|
存储 编译器 C语言
【C++】什么是类与对象?
【C++】什么是类与对象?
31 1
|
8天前
|
存储 编译器 C语言
C++:类与对象(1)
C++:类与对象(1)
|
6月前
|
存储 编译器 C语言
C++类和对象(中)
C++类和对象(中)
|
9月前
|
算法 编译器 数据安全/隐私保护
c++类与对象详解
c++类与对象详解
114 0
|
9月前
|
设计模式 编译器
类与对象(下)
类与对象(下)
29 0
|
9月前
|
程序员 编译器 C语言
c++学习之类与对象2
c++学习之类与对象2
50 0

热门文章

最新文章