C++之类与对象(3)(上)

简介: C++之类与对象(3)

前言

上个章节讲述了构造函数和析构函数,本节将讲解拷贝构造函数和赋值运算符重载等知识。

1.拷贝构造函数

1.1函数定义

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

1.2函数特点

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

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

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

4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

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

代码解释

#include <iostream>
using namespace std;
class Date {
public:
  Date(int year = 1, 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(Date* d) {
    _year = d->_year;
    _month = d->_month;
    _day = d->_day;
  }
  void print(){
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
void Func1(Date d)
{
  cout << &d << endl;
  d.print();
}
void Func3( Date& d) {//const Date&d时,public中的void print()改为void print()const
  // 这里的 d 是对原始 Date 对象的引用,不会拷贝
  cout << &d << endl; // 输出d对象的地址
  d.print();          // 调用d的print函数
}
Date& Func2()
{
  Date tmp(2024, 7, 25);
  tmp.print();
  return tmp;
}
int main() {
  
  Date d(2024, 7, 24);
  /*
  // 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
  Date d1(&d);
  d1.print();
  //这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
  Date d2(d);
  d2.print();
  Date d3 = d;
  d3.print();
  */
 
  Func1(d);//按值传递
  Func3(d);//传引用
  // Func2返回了⼀个局部对象tmp的引⽤作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
  Date ret = Func2();
  ret.print();
  return 0;
}

补充:

在C++中,当通过值传递自定义类型的对象时,确实会调用拷贝构造函数进行对象的拷贝。这种方式在某些情况下可能会导致性能的下降,尤其是当对象较大或者拷贝构造函数比较复杂时。

传值与传引用的区别

1. 传值:

- 当函数参数以值的方式传递时,编译器会创建该对象的副本。

- 调用拷贝构造函数,这可能涉及内存分配和数据复制,这是一个相对较重的操作。

- 如果 `Date` 对象比较大,频繁的拷贝可能会影响性能。

void Func1(Date d) {
    // 这里的 d 是 Date 对象的副本
}

2. 传引用:

- 通过引用传递参数时,不会创建副本,而是直接使用原始对象。

- 这样可以避免拷贝构造函数的调用,从而提高性能。

- 还可以对传入的对象进行修改(如果传递的是非常量引用)。

void Func1(const Date& d) {
    // 这里的 d 是对原始 Date 对象的引用,不会拷贝
    d.print();
}

故在实际编程中,如果不需要在函数内部修改传入的对象,并且想要提高性能,使用引用作为函数参数是一种推荐的做法。这种方式可以避免不必要的对象拷贝,特别是在处理较大或复杂对象时。不过,如果你确实需要在函数中对参数进行修改,使用非常量引用可以让你直接操作原始对象。

6. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显⽰实现拷贝构造。像Stack这样的类,虽然也都是内置类型, 但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要 我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现

MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就

需要显示写拷贝构造,否则就不需要。

#include<iostream>
using namespace std;
typedef int Datatype;
class Stack
{
public:
  Stack(int n = 4)
  {
    _a = (Datatype*)malloc(sizeof(Datatype) * n);
    if (nullptr == _a)
    {
      perror("malloc申请空间失败");
      return;
    }
    _capacity = n;
    _top = 0;
  }
  
  Stack(const Stack& st)
  {
    // 需要对_a指向资源创建同样⼤的资源再拷⻉值
    _a = (Datatype*)malloc(sizeof(Datatype) * st._capacity);
    if (nullptr == _a)
    {
      perror("malloc申请空间失败!!!");
      return;
    }
    memcpy(_a, st._a, sizeof(Datatype) * st._top);
    
    _top = st._top;
    _capacity = st._capacity;
  }
  void Push(Datatype x)
  {
    if (_top == _capacity)
    {
      int newcapacity = _capacity * 2;
      Datatype* tmp = (Datatype*)realloc(_a, newcapacity *
        sizeof(Datatype));
      if (tmp == NULL)
      {
        perror("realloc fail");
        return;
      }
      _a = tmp;
      _capacity = newcapacity;
    }
    _a[_top++] = x;
  }
  ~Stack()
  {
    cout << "~Stack()构析函数调用" << endl;
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
  }
private:
  Datatype* _a;
  size_t _capacity;
  size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:
  Stack pushst;
  Stack popst;
};
int main()
{
  Stack st1;
  st1.Push(1);
  st1.Push(2);
  // Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
  // 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
  Stack st2 = st1;
  MyQueue mq1;
  // MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
  // 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题
  MyQueue mq2 = mq1;
  return 0;
}


C++之类与对象(3)(下):https://developer.aliyun.com/article/1624937

目录
相关文章
|
2月前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
40 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
87 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)
|
2月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
63 1