从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

目录
相关文章
|
10天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
29 10
|
15天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
42 9
|
10天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
18天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
18天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
23天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
18天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
18天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。
|
18天前
|
安全 编译器 C++
【C++】string类的使用②(元素获取Element access)
```markdown 探索C++ `string`方法:`clear()`保持容量不变使字符串变空;`empty()`检查长度是否为0;C++11的`shrink_to_fit()`尝试减少容量。`operator[]`和`at()`安全访问元素,越界时`at()`抛异常。`back()`和`front()`分别访问首尾元素。了解这些,轻松操作字符串!💡 ```
|
18天前
|
存储 编译器 Linux
【C++】string类的使用②(容量接口Capacity )
这篇博客探讨了C++ STL中string的容量接口和元素访问方法。`size()`和`length()`函数等价,返回字符串的长度;`capacity()`提供已分配的字节数,可能大于长度;`max_size()`给出理论最大长度;`reserve()`预分配空间,不改变内容;`resize()`改变字符串长度,可指定填充字符。这些接口用于优化内存管理和适应字符串操作需求。