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

目录
相关文章
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
7月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
6月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
6月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
11月前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
88 0
|
6月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
8月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
165 19
|
8月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
202 13
|
7月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
7月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。