从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(中)

简介: 从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)

从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(上):https://developer.aliyun.com/article/1513646

3. 拷贝构造函数(默认成员函数)

我们在创建对象的时候,能不能创建一个与已存在对象一模一样的新对象呢?

Date d1(2023, 5, 3);
d1.Print();
 
Date d2(d1);//把d1拷贝给d2
d2.Print();
 
Date d3 = d1;//把d1拷贝给d3 (这也是拷贝构造,后面学的赋值是两个已存在的对象)
d3.Print();

当然可以,这时我们就可以用拷贝构造函数。


3.1 拷贝构造函数概念

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

Date(const Date& d) // 这里要用引用,否则就会无穷递归下去
{         
  _year = d._year;
  _month = d._month;
  _day = d._day;
}

3.2 拷贝构造函数特性和用法

拷贝构造函数也是一个特殊的构造函数,所以他符合构造函数的一些特性:

① 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。

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

拷贝构造函数的用法:

#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)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
 
    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }
 
private:
    int _year;
    int _month;
    int _day;
};
 
int main()
{
    Date d1(2023, 5, 3);
    d1.Print();
 
    Date d2(d1);//把d1拷贝给d2
    d2.Print();
 
    Date d3 = d1;//把d1拷贝给d3
    d3.Print();
 
    return 0;
}

为什么必须使用引用传参呢?

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

……

一直在传参这里出不去了,所以这个递归是一个无穷无尽的。

注意:如果参数在函数体内不需要改变,建议把 const 加上。


3.3 默认生成的拷贝构造

默认生成拷贝构造:

① 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去)。

② 自定义类型成员,会再调用它的拷贝构造。

       拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理的细节是不一样的,这个跟构造函数和析构函数是不一样的。

(把上面的代码中自己写的拷贝构造屏蔽了,运行结果还是一样:)

#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)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改
    //{
    //    _year = d._year;
    //    _month = d._month;
    //    _day = d._day;
    //}
 
    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }
 
private:
    int _year;
    int _month;
    int _day;
};
 
int main()
{
    Date d1(2023, 5, 3);
    d1.Print();
 
    Date d2(d1);//把d1拷贝给d2
    d2.Print();
 
    Date d3 = d1;//把d1拷贝给d3
    d3.Print();
 
    return 0;
}

059b6f436dbc48e6a7c84480d55941f7.png

       所以为什么要写拷贝构造?写它有什么意义?这里没有什么意义。当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的比如实现栈的时候,栈的结构问题,导致这里如果用默认的拷贝构造,会程序崩溃。按字节把所有东西都拷过来会产生问题,如果 Stack st1 拷贝出另一个 Stack st2(st1) ,会导致他们都指向那块开辟的内存空间,导致他们指向的空间被析构两次,导致程序崩溃,然而问题不止这些。

       其实这里的字节序拷贝是浅拷贝,下面几章我们会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于栈这样的类,默认生成的拷贝构造不能用。

4. 运算符重载

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

运算符重载简单来说:就是能让自定义类型和内置类型一样使用运算符。

4.1  运算符重载的概念

运算符重载是具有特殊函数名的函数,能让自定义类型和内置类型一样使用运算符。

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

operator+
operator>
operator==

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

返回值类型:看操作符运算后返回的值是什么。

参数:操作符有几个操作数,它就有几个参数。

注意事项:

  • 不能通过连接其他符号来创建新的操作符,比如operator@,只能对已有的运算符进行重载,也不能对内置类型进行重载。
  • 重载操作符必须有一个类类型或枚举类型的操作数。
  • 用于内置类型的操作符,其含义不能改变。比如内置的整型 +,不能改变其含义。
  • 作为类成员的重载函数时,其形参看起来比操作数数目少 1,成员函数的操作符有一个默认的形参 this,限定为第一个形参。
  • 不支持运算符重载的 5 个运算符:(这个经常在笔试选择题中出现)
.          (点运算符)
 
::         (域运算符)
 
.*         (点星运算符)(目前博客没讲过的)
 
?:         (条件运算符)
 
sizeof

       虽然点运算符( . )不能重载,但是箭头运算符( -> )是支持重载的,解引用(*)是可以重载的,不能重载的是点星运算符( .* )


4.2 运算符重载示例

我们重载一个判断日期类相等的运算符:==

#include <iostream>
using namespace std;
 
class Date 
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
 
 //private:    
  int _year;
  int _month;
  int _day;
};
 
bool operator==(const Date& d1, const Date& d2) 
{
  return d1._year == d2._year
    && d1._month == d2._month 
    && d1._day == d2._day;
}
 
int main() 
{
  Date d1(2023, 5, 2);
  Date d2(2023, 5, 3);
  
  cout << (d1 == d2) << endl;//这里的流插入运算符比我们重载的==优先级高,所以要加括号
 
  return 0;
}

       这里运算符重载成全局的,不得不将成员变成是公有的,得把 private 注释掉,那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决,如果现在不知道也没关系,后面会讲。用友元也是不好的,所以一般直接重载成成员函数:

#include <iostream>
using namespace std;
 
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;
  }
 
 private:    
  int _year;
  int _month;
  int _day;
};
 
int main() 
{
  Date d1(2023, 5, 2);
  Date d2(2023, 5, 3);
  
  cout << (d1 == d2) << endl;
//编译器自动转化为:
//  cout << (d1.operator==(d2)) << endl;
 
  return 0;
}


       既然要当成员函数,就得明白这里的 this 指的是谁。需要注意的是,左操作数是 this 指向的调用函数的对象。(关于运算符重载我们下一篇还会完整的实现一个日期类,重载各种运算符,比如日期减日期)

5. 赋值运算符重载(默认成员函数)

5.1 赋值运算符重载概念

赋值运算符重载主要是把一个对象赋值给另一个对象。

如果你不写,编译器会默认生成。

       赋值运算符只能重载成类的成员函数,不能重载成全局函数。原因:赋值运算符如果不显式在类内实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

要分清和拷贝构造的区别:

int main()
{
  // 一个已经存在的对象初始化一个马上创建实例化的对象
  Date d1(2023, 5, 3);
 
  Date d2(d1);  // 拷贝构造
  Date d3 = d1;  // 拷贝构造
 
  // 两个已经存在的对象,之间进行赋值拷贝
  Date d4(2023, 5, 4);
  d1 = d4; // 赋值 让 d1 和 d4 一样
 
  return 0;
}

5.2 赋值运算符重载使用

赋值运算符重载主要有以下四点:

① 参数类型

② 返回值

③ 检查是否给自己复制

④ 返回 *this

#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& operator=(const Date& d) 
  {
    if (this != &d)  // 防止自己跟自己赋值(这里的&d是取地址)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;   // 返回左操作数d1
  }
 
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
 
private:
  int _year;
  int _month;
  int _day;
};
 
int main()
{
  Date d1(2023, 5, 3);
  Date d2(2023, 5, 4);
 
  d1 = d2;
 
  d1.Print();
  d2.Print();
  return 0;
}


从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(下):https://developer.aliyun.com/article/1513648?spm=a2c6h.13148508.setting.33.5e0d4f0eCWTp6I

目录
相关文章
|
16天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
20 4
|
16天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
17 4
|
17天前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
15 0
|
21天前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
28 0
|
21天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
30 3
|
12天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
30 10
|
5天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
11天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
33 7
|
11天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
22 4
|
16天前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。