【C++初阶】第三站:类和对象(中) -- 类的6个默认成员函数-2

简介: 【C++初阶】第三站:类和对象(中) -- 类的6个默认成员函数-2

【C++初阶】第三站:类和对象(中) -- 类的6个默认成员函数-1

https://developer.aliyun.com/article/1457017?spm=a2c6h.13148508.setting.20.2e124f0exuMLtA



拷贝构造函数

概念

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

int main()
{
  Date d1(2023, 7, 21);
  Date d2(d1);
//  Date d2 = d1;//等价上面的写法
    return 0;
}


拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存

在的类类型对象创建新对象时由编译器自动调用

特征

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


解释特性1:拷贝构造函数是构造函数的一个重载形式。

代码:

时间类拷贝构造:
Date(Date& d)
{
  cout << "Date(Date& d)" << endl;
  _year = d._year;
  _month = d._month;
  _day = d._day;
}
栈类拷贝构造 :
Stack(const Stack& s)
{
  cout << "Stack(Stack& s)" << endl;
  //深拷贝
  _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
  if (NULL == _array)
  {
    perror("malloc申请空间失败!!");
    return;
  }
  memcpy(_array, s._array, sizeof(DataType) * s._size);
  _size = s._size;
  _capacity = s._capacity;
}


       解释:主要负责在创建新对象时,以已存在的同类型对象作为参数,进行初始化工作。当一个对象通过值传递或直接使用另一个对象来初始化时,编译器会自动调用拷贝构造函数。这个特化的过程确保了新对象与原有对象具有相同的内部状态和属性,实现了对象的深复制或浅复制行为。因此,拷贝构造函数是一种对常规构造函数功能的重要补充和扩展,专门用于处理对象间的拷贝操作。

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

举一个代码例子:

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  //Date d2(d1);
  Date(Date d)//用一个对象接收,形参是实参的一份临时拷贝
  {
  cout << "Date(Date& d)" << endl;
  _year = d._year;
  _month = d._month;
  _day = d._day;
  }
  void Print()
  {
  cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  // 内置类型
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  Date d2(d1);//Date d2 = d1;
  return 0;
}


图解:


99d006b1f07f473397c98a45335b5a4e.png


       自定义类型传值传参必须要调用拷贝构造,自定义类型传参是一种对象间的拷贝初始化,要调用拷贝构造才能完成这个过程:①要调用拷贝构造,就得先传参。②使用传值传参的方式,导致对象间拷贝,拷贝引发拷贝构造函数的调用。③若调用拷贝函数,得先传值传参,接着又形成拷贝构造。


95d5b41d6d904a2ca1679f5eb54a46fe.png

问:传值传参引发对象拷贝之后,为什么要调用拷贝构造函数?


   

为了正确地初始化新创建的对象,该对象是对传值参数的副本。
        在传值传参过程中,系统需要分配新的内存空间并把原对象的状态完整复制到新对象中。拷贝构造函数就是专门设计用来执行这种对象间复制操作的,确保新对象与原始对象具有相同的内部状态。
        通过调用拷贝构造函数可以实现深拷贝或浅拷贝,并处理可能存在的资源管理问题(如动态分配的内存、文件句柄等)。

那这时候我们应该使用引用类型接收,因为引用只是同一块空间的别名,不会引发拷贝问题

Date(const Date& d)//加上const的原因是防止有人把d的位置写到左边去
{
  cout << "Date(Date& d)" << endl;
  _year = d._year;
  _month = d._month;
  _day = d._day;
}

解释特性3:若未显式定义,编译器会生成默认的拷贝构造默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)//默认构造函数
  {
  _year = year;
  _month = month;
  _day = day;
  }
//拷贝构造函数
  //Date d2(d1);
  //Date(const Date& d)
  //{
  //  cout << "Date(Date& d)" << endl;
  //  _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)
{
  d.Print();
}
int main()
{
  Date d1;
  Date d2(d1);//1
  func1(d2);//2
  d2.Print();
  return 0;
}


执行:


9b4f25cb96ef468ca093a02d2a78e2bd.png


       上段代码调用了两次拷贝构造,可是我们并没有实现拷贝构造函数的代码编写,由此得知,编译器自动调用了拷贝构造函数完成了对象的拷贝。


我们由上文可知,编译器默认生成的拷贝构造,跟之前的拷贝函数特性不一样


默认构造、析构函数:


 

对于内置类型的成员不会处理,而自定义类型的成员才会处理,会去调用这个成员的默认构造函数。
总结:一般情况都需要我们自己写构造函数,决定初始化方式
成员变量全是自定义类型,可以考虑不写构造函数

拷贝构造函数:


1.内置类型,值拷贝
2.自定义类型,调用他的拷贝

解释特性4:编译器自动生成的拷贝构造函数不能实现深拷贝

由上文可知


       在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,也就说值拷贝,值拷贝(浅拷贝)的意思是把对象里面的所有成员函数以及它们的值,一个一个、完完全全地赋值给另一个对象。

Date d1;
Date d2(d1);//用已存在的对象d1创建对象d2
//  || 等价于
//Date d2 = d1;

      但是值拷贝,在某些场景下可能会引发程序错误:


typedef int DataType;
class Stack
{
public:
  Stack(size_t capacity = 3)//默认构造函数
  {
  _array = (DataType*)malloc(sizeof(DataType) * capacity);
  if (NULL == _array)
  {
    perror("malloc申请空间失败!!!");
    return;
  }
  _capacity = capacity;
  _size = 0;
  }
    void Push(DataType data)
  {
  // CheckCapacity();
  _array[_size] = data;
  _size++;
  }
  void Print()
    {
        cout<<_array<<endl;
    }
private:
  // 内置类型
  DataType* _array;
  int _capacity;
  int _size;
};
int main()
{
  Stack s1;
  s1.Print();
  Stack s2(s1);
  s2.Print();
    return 0;
}


执行:


dee08be377da405bbc344bfacc19321e.png


C语言的拷贝为值拷贝,会导致两个对象指向同一块空间的问题,C++兼容C语言,如果按照正常C语言的思路写,那么也是浅拷贝


       两对象指向的地址是一样的,当两对象的生命周期结束时自动调用析构函数,也就意味着s2先调用析构,s1后调用析构,这时候s1访问这块空间会出现野指针访问异常的问题,因为同一块空间,被释放了两次,等到第二次析构时,s2指针指向了一块已经归还给操作系统的空间。



c32984a0ac544f978f88702e593040cd.png

a0ee7a072f104d19865baa7438f5879a.png


       用同一个对象去拷贝另一个对象的时候呢,这里用了拷贝构造函数去解决这个问题(深拷贝)

3124942e9950408687f7acae6324cfe8.png



        下面这个代码使用引用接收的原因,是因为引用只是对象s1的别名,属于同一块空间,假设使用值接收,就会引发无穷递归问题。


Stack(const Stack& s)
  {
  cout << "Stack(Stack& s)" << endl;
  // 深拷贝
  _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
  if (NULL == _array)
  {
    perror("malloc申请空间失败!!!");
    return;
  }
            //destination   source
  memcpy(_array, s._array, sizeof(DataType) * s._size);
  _size = s._size;
  _capacity = s._capacity;
  }
int main()
{
  Stack s1;
  s1.Print();
  Stack s2(s1);
  s2.Print();
    return 0;
}


所以注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。        

总结一下:

1.像Date类不需要我们写拷贝构造,默认生成就可以用
2.像Stack类需要我们自己实现深拷贝的拷贝构造,默认生成会出问题

解释特性5:拷贝构造函数典型调用场景

1️⃣使用已存在对象创建新对象
2️⃣函数参数类型为类类型对象
3️⃣函数返回值类型为类类型对象


测试代码:


class Date
{
public:
  Date(int year, int minute, int day)
  {
  cout << "Date(int,int,int):" << this << endl;
  }
  Date(const Date& d)
  {
  cout << "Date(const Date& d):" << this << endl;
  }
  ~Date()
  {
  cout << "~Date():" << this << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
Date Test(Date d)
{
  Date temp(d);
  return temp;
}
int main()
{
  Date d1(2022, 1, 13);
  Test(d1);
  return 0;
}


代码执行:


96cf3255d0a442229ddca67289583eeb.png


        为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用

尽量使用引用。

赋值运算符重载

运算符重载

概念:自定义类型不支持比较,是因为写了运算符重载才支持比较


e0c690aea416415eafef4c6d845d809a.png


      C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有

其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

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

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

特性


1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5..* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。


自定义类型与内置类型的区别:



3f869b971cb64dc8b0823c5bc21375c2.png

==运算符在Date类中重载


ffe1393f4d654b2e85e0dcbf7f0ea2e4.png


代码如下:


class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  bool operator==(const Date& d)
  {
  return _year == d._year
    && _month == d._month
    && _day == d._day;
  }
  void Print()
  {
  cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2024, 1, 1);
  Date d2(2024, 1, 1);
  cout << (d1 == d2) << endl;
    //等价于(d1.operator==(d2))
}


赋值运算符重载

f1a6f9121c804ae7876a3af708efd379.png


代码如下 :


class Date
{
public:
  Date(int year = 1900, 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& operator=(const Date& d)//赋值运算符重载函数
  {
  if (this != &d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  return *this;
  }
  void Print()
  {
  cout << _year << "/" << _month << "/" << _day<<endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Print();
  Date d2(2024, 1, 1);
  d2.Print();
  d1 = d2;
  d1.Print();
  d2.Print();
}

关于赋值运算符需要注意以下5点:


赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝


7a2b0b15ed884458afb43f0e16bb6ed4.png


既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实

现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }
    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2;
    s2 = s1;
    return 0;
}

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必

须要实现。


d712a716fb5a499ab67fef19bc9ede85.png


区分以下代码:

Date d1(2024, 1, 1);
Date d2(d1);
Date d3 = d1;

Date d1(2024, 1, 1)调用的是构造函数; Date d2(d1)调用的是拷贝构造函数。Date d3 = d1;注意:不是是赋值运算符重载函数,第三句代码也是拷贝构造函数。


注意区分拷贝构造函数和赋值运算符重载函数的使用场景:


拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。

赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。


图解:


ddef080705044ed6a69d3042da47a6e8.png


const成员

const修饰类成员函数

       将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数

隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。


0291824b5b434b0991d00736aa710957.png


90cbbac0695b4fd48441b67fb40eafef.png

代码如下:


class Date
{
public:
  Date(int year, int month, int day)
  {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print()
  {
  cout << "Print()" << endl;
  cout << "year:" << _year << endl;
  cout << "month:" << _month << endl;
  cout << "day:" << _day << endl << endl;
  }
  void Print() const
  {
  cout << "Print()const" << endl;
  cout << "year:" << _year << endl;
  cout << "month:" << _month << endl;
  cout << "day:" << _day << endl << endl;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};
int main()
{
  Date d1(2022, 1, 13);
  d1.Print();
  const Date d2(2022, 1, 13);
  d2.Print();
}


请思考下面的几个问题:


1.const对象可以调用非const成员函数吗?

       不可以,因为const修饰的对象为只读类型,若调用非const成员函数,属于权限放大行为,只读权限变成既可以只读又可以可写。


2.非const对象可以调用const成员函数吗?

       可以,因为非const对象权限拥有可读、可写权限,调用const成员函数属于权限缩小问题,权限变为只读。


3.const成员函数内可以调用其它的非const成员函数吗?

       不可以,因为const修饰的成员函数为只读类型,若调用非const非成员函数,属于权限放大行为,只读权限变成既可以只读又可以可写。


4.非const成员函数内可以调用其它的const成员函数吗?

       可以,因为非const成员函数权限拥有可读、可写权限,调用const成员函数属于权限缩小问题,权限变为只读。


图解:



6eb79a59fddb43368e9afeba684dd891.png

总结:权限只可以缩小,不能够放大。也就是我本身只能是可读的(const),不能传过去编程可读可写的了(非const)


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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。


代码如下:


class Date
{
public:
  Date* operator&()
  {
  return this;
  }
  const Date* operator&()const
  {
  return this;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};


这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需

要重载,比如想让别人获取到指定的内容!

本章总结:

      b253b9f6b2254be9a010daea47162d2d.png

相关文章
|
14小时前
|
C++ Linux
|
14小时前
|
编译器 C++
【C++】继续学习 string类 吧
首先不得不说的是由于历史原因,string的接口多达130多个,简直冗杂… 所以学习过程中,我们只需要选取常用的,好用的来进行使用即可(有种垃圾堆里翻美食的感觉)
7 1
|
14小时前
|
算法 安全 程序员
【C++】STL学习之旅——初识STL,认识string类
现在我正式开始学习STL,这让我期待好久了,一想到不用手撕链表,手搓堆栈,心里非常爽
15 0
|
14小时前
|
存储 安全 测试技术
【C++】string学习 — 手搓string类项目
C++ 的 string 类是 C++ 标准库中提供的一个用于处理字符串的类。它在 C++ 的历史中扮演了重要的角色,为字符串处理提供了更加方便、高效的方法。
16 0
【C++】string学习 — 手搓string类项目
|
14小时前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
17 0
|
14小时前
|
C语言 C++
【C++】string类(常用接口)
【C++】string类(常用接口)
21 1
|
14小时前
|
Java C++ Python
【C++从练气到飞升】06---重识类和对象(二)
【C++从练气到飞升】06---重识类和对象(二)
|
14小时前
|
编译器 C++
【C++从练气到飞升】06---重识类和对象(一)
【C++从练气到飞升】06---重识类和对象(一)
|
14小时前
|
存储 编译器 C语言
【C++从练气到飞升】02---初识类与对象
【C++从练气到飞升】02---初识类与对象