【C++】类和对象(中)(万字详解)(二)

简介: 【C++】类和对象(中)


4.拷贝构造函数

1.概念

首先要知道,拷贝构造函数也是构造函数的重载。


那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?


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


举例:日期类:

class Date
{
public:
    //构造函数
    Date(int year = 1900, 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, 10, 10);
    Date d2(d1); //拷贝d1
    d1.print();
    d2.print();
    return 0;
}

2.特性

拷贝构造函数也是特殊的成员函数,其特征如下:

1. 拷贝构造函数是构造函数的一个重载形式。函数名相同,参数不同。

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

发生递归的原因:

Date(Date d)  //去掉引用,就是传值调用

{

   _year = d._year;

   _month = d._month;

   _day = d._day;

}

传值调用:当调用拷贝构造函数时,形参是实参的一份临时拷贝

调用拷贝构造Date d2(d1);,先得传参,传参调用完拷贝构造以后,本身又要调用拷贝构造,而后又要传参,然后一直自己调用自己,导致一直递归下去......

那么传地址可不可以?可以,但不就麻烦了吗?

Date(Date *d)  //去掉引用,就是传值调用

{

   _year = d->_year;

   _month = d->_month;

   _day = d->_day;

}


所以还是引用是最好的方式!


3.拷贝构造函数只有单个形参,该形参是对本类类型对象的引用,一般常用const修饰


为什么要加const呢?


Date(const  Date & d)  //去掉引用,就是传值调用

{

   _year = d._year;

   d._month=_month ; //有时候会写反,而改变了原本对象的值。

   _day = d._day;

}


Date d2(d1); //拷贝d1


所以加了const以后,就不能改变d1原对象了


4. 若未显示定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按

字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

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

义类型是调用其拷贝构造函数完成拷贝的。所以内置类型处理,自定义类型不处理,自定义类型可能复杂多变,需要自己写拷贝函数,这样就叫 深拷贝。

浅拷贝: 自己没有写,使用默认的  (日期类)

class Date
{
public:
   //构造函数
 Date(int year = 1900, int month = 1, int day = 1)
 {
 cout << "构造函数" << endl; //验证调用所需
 _year = year;
 _month = month;
 _day = day;
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
    Date d1(2022, 10, 10);
    Date d2(d1); //拷贝d1
    return 0;
}

是可以实现我们想要的功能的,所以不需要我们自己写。

举例:(栈类)

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 s1;
  s1.Push(1);
  s1.Push(2);
  Stack s2(s1);
}

当使用默认拷贝函数时,是否能实现我们想要的需求:

这是怎么了??编译器自己提供的默认拷贝构造函数居然把s1拷贝到s2类中时,两个类中的指针变量指向了同一块空间!!


那么,栈的特性就是先进后出,所以s2先调用析构函数,把空间清理了,然后s1再去调用析构函数,又去清理,这不是瞎搞嘛,造成内存重复释放!

所以默认的拷贝构造函数解决不了我们的需求,需要我们自己写。

//st2(st1)拷贝构造
  Stack(const Stack& st)
  {
    cout << "Stack(const Stack& st)" << endl;
    _a = (int*)malloc(sizeof(int)*st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int)*st._top);
    _top = st._top;
    _capacity = st._capacity;
  }

即解决了指针重复指向一块空间,又把已经创建好的s1对象和其中的内容都拷贝到了s2。


总结:需要写析构函数的类,都需要写深拷贝的拷贝构造,如:Stack


不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用 。如:Date/MyQueue


类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请

时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.运算符重载

1.为什么要重载运算符

为了让自定义类型可以使用 运算符


对于日期类,我们会经常用到,某个日期加多少天来得到另一个日期。


所以日期需要加减,也需要比较,那么使用+  =   -  ,在C++中,就需要运算符重载,与函数重载没有关系。


2.赋值运算符重载格式

返回值+operator+运算符 +(所需参数),参数个数就是参与运算符的参数个数

class Data
{
public:
  Data(int year=1,int month=1,int day=1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
bool operator==(Data& d1,Data& d2)
  {
    return d1._year == d2._year
      && d1._month == d2._month
      && d1._day == d2._day;
  }
int main()
{
  Data d1(2022, 10, 11);
  Data d2(2023, 1, 1);
    d1==d2;//可以显式调用 operator==(d1,d2);
  cout << (d1 == d2) << endl;
  cout << (d1 > d2) << endl;
}

1.定义运算符重载时,参数为引用的好处是,不需要调用拷贝函数,若是传值,参数为对象,则需要拷贝对象,调用拷贝函数。


2.定义在类外,需要将类的成员变量设置为公开,会造正麻烦,那么我们直接就定义到了类的内部。


3.若直接放入类的内部定义运算符重载,则会出现问题:

class Data
{
public:
  Data(int year=1,int month=1,int day=1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
      bool operator==(Data& d)
  {
    return this->_year == d2._year
      && this->_month == d2._month
      && this->_day == d2._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Data d1(2022, 10, 11);
  Data d2(2023, 1, 1);
    d1==d2;//可以显式调用 operator==(d1,d2);
  cout << (d1 == d2) << endl;
  cout << (d1 > d2) << endl;
}

那为什么在类中,显示的是一个参数呢?

     bool operator==(Data& d)

   {

       return this->_year == d2._year

           && this->_month == d2._month

           && this->_day == d2._day;

   }


目录
相关文章
|
1月前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
35 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
35 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
32 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
16 0
|
1月前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
30 0
|
1月前
|
编译器 C++
C++之类与对象(3)(下)
C++之类与对象(3)(下)
32 0
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)