C++入门基础—类和对象3

赋值运算符重载

运算符重载

运算符重载的本质 : 它是 C++ 赋予程序员的一种语法糖,让用户自定义类型能够像内置类型一样,使用 + - * / == != < > << >> 等运算符进行操作,从而大大提升代码的可读性和表达力

为什么有运算符重载 ,如果你写了一个日期类 Date,现在想比较两个日期是否相等

  • 没有重载时 :你可能得写 if (date1.isEqual(date2))

  • 重载之后 :你可以直接写 if (date1 == date2)

是不是这么一对比就显得很简洁

核心语法:operator 关键字

运算符重载的本质是函数调用, 它的函数名格式非常固定:返回值类型 operator运算符(参数列表)

operator使用

cpp 复制代码
#include <iostream>

class Date
{
public:
    // 构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Date& d)const
    {
        return _year == d._year &&
            _month == d._month &&
            _day == d._day;
    }
    void Print()
    {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;

};

int main()
{
    Date d1(2024, 3, 28);
    Date d2(2024, 3, 28);
    Date d3(2026, 1, 1);

    std::cout << "d1: " << std::endl;
    d1.Print();
    std::cout << "d2: " << std::endl;
    d2.Print();
    std::cout << "d3: " << std::endl;
    d3.Print();
    if (d1 == d2)//隐式类型转换
    {
        std::cout << "d1 != d2 " << std::endl;
    }
    else
    {
        std::cout << "d1 == d2 " << std::endl;
    }
    return 0;
}

先单独来看这段代码

cpp 复制代码
 bool operator==(const Date& d)const
    {
        return _year == d._year &&
            _month == d._month &&
            _day == d._day;
    }

operator 是关键字operator== 连在一起就是这个函数的函数名,当写 d1 == d2 时,编译器其实是在偷偷调用 d1.operator==(d2)

这里为什么要用 const&

  • &(引用) :如果不加 &,程序会把整个 d2 对象拷贝一份传进来,浪费内存和时间,用引用相当于传了个"别名",效率最高

  • const :由于我们是传引用,如果不加 const,函数内部万一不小心改了 d._year或者月或日,外面的 d2 也会跟着变,加上 const 是为了承诺:"我只看你的数据,绝不乱动"

为什么函数右边还有一个const?

  • 它的含义 :这个 const 修饰的是隐藏的 this 指针,它告诉编译器:"这个函数保证不会修改调用它的对象(即 d1)本身"。

  • 为什么必须加 :在 C++20 标准下,编译器会尝试对 == 进行对称匹配,如果你的函数不加 const,编译器会觉得这个函数"不够安全",在匹配时会产生歧义,加上它,代码的健壮性直接拉满

  • 只要一个成员函数不需要修改成员变量,就建议加上 const

既然能判断相等,按不相等是不是也可以试一下

cpp 复制代码
//#include <iostream>
//using namespace std;
//class Date
//{
//public:
//    // 构造函数
//    Date(int year = 1900, int month = 1, int day = 1)
//    {
//        _year = year;
//        _month = month;
//        _day = day;
//    }
//    bool operator=(const Date& d)const
//    {
//        if (*this != &d)
//        {
//                _year = d._year 
//                _month = d._month 
//                _day = d._day;
//        }
//    }
//    void Print()
//    {
//        std::cout << _year << "-" << _month << "-" << _day << std::endl;
//    }
//private:
//    int _year;
//    int _month;
//    int _day;
//};
//
//int main()
//{
//    Date d1(2024, 3, 28);
//    Date d2(2024, 3, 28);
//    Date d3(2026, 1, 1);
//
//    std::cout << "d1: " << std::endl;
//    d1.Print();
//    std::cout << "d2: " << std::endl;
//    d2.Print();
//    std::cout << "d3: " << std::endl;
//    d3.Print();
//    if (d2 != d3)
//    {
//        cout << "d2 != d3" << endl;
//    }
//    else
//    {
//        cout << "other" << endl;
//    }
//    return 0;
//}

#include <iostream>
using namespace std;
class Date
{
public:
    // 构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Date& d)const
    {
        return _year == d._year &&
            _month == d._month &&
            _day == d._day;
    }
    bool operator!=(const Date& d)const
    {
        return _year != d._year &&
            _month != d._month &&
            _day != d._day;
    }
    void Print()
    {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2024, 3, 28);
    Date d2(2024, 3, 28);
    Date d3(2026, 1, 1);

    std::cout << "d1: " << std::endl;
    d1.Print();
    std::cout << "d2: " << std::endl;
    d2.Print();
    std::cout << "d3: " << std::endl;
    d3.Print();
    if (d2.operator!=(d3))//显示类型转换
    {
        cout << "d2 != d3" << endl;
    }
    else
    {
        cout << "other" << endl;
    }
    return 0;
}

其实我们就可以把刚才的情况取反就可以了

但是我们还可以这样写

cpp 复制代码
//部分代码,省略部分和上面代码一致
    bool operator==(const Date& d)const
    {
        return _year == d._year &&
            _month == d._month &&
            _day == d._day;
    }
    bool operator!=(const Date& d)const
    {
        return !(*this == d);
    }
   

这个叫做代码复用

什么是代码复用

代码复用就是"不重复造轮子"

Date 类里,判断"不相等"逻辑其实就是"相等"逻辑的对立面

  • 常规写法(不推荐) :在 != 里再写一遍 _year != d._year ......这种写法一旦你以后想增加一个"时分秒"字段,你得改两个地方,漏改一个就是 Bug

  • 代码复用(推荐)!= 直接去问 ==:"兄弟,你俩相等吗?"然后把结果取反

核心思想: 只维护一套核心逻辑(==),其他相关逻辑(!=)都基于核心逻辑构建

编译器看到 *this == d,发现左边是一个 Date 对象,右边也是一个 Date 对象,于是它会去类里找 operator==

赋值运算符重载

赋值运算符重载,是指将一个已存在的对象赋值给另一个已经存在的对象

  • 拷贝构造:对象还没出生,用别人来"初始化"它

  • 赋值重载:对象已经出生了,现在要"更新"它的值

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
    // 构造函数
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
   Date& operator=(const Date& d)
    {
            _year = d._year;
            _month = d._month;
            _day = d._day;
            return *this;
    }
    void Print()
    {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2024, 3, 28);
    Date d2(2024, 3, 28);
    Date d3(2026, 1, 1);

    std::cout << "d1: " << std::endl;
    d1.Print();
    std::cout << "d2: " << std::endl;
    d2.Print();
    d3.operator=(d1);
    std::cout << "d3: " << std::endl;
    d3.Print();
  
    return 0;
}

我们先来看一下这一段代码

cpp 复制代码
Date& operator=(const Date& d)
 {
         _year = d._year;
         _month = d._month;
         _day = d._day;
         return *this;
 }

这里传入和返回都用的是引用,所以就不存在产生**临时对象,**把改变的日期会引用回去

初始化列表

初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成 员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地方

引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始 化,否则会编译报错

代码示例

cpp 复制代码
#include<iostream>
using namespace std;
class date
{
public:
	date(int year, int month, int day)//初始化列表

		:_year(year)
		, _month(month)
		, _day(day)
	{}
	void Print() {
		cout << _year<<"/"<<_month<<"/"<<_day <<endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	int x = 0;
	date d1(2020, 1, 1);
	d1.Print();
	return 0;
}

如果声明中的成员变量被const修饰 或者定义的是引用的话,就必须要走初始化列表

cpp 复制代码
#include<iostream>
using namespace std;
class date
{
public:
	date(int& x)//初始化列表
		: _n(1)
	{}
	void Print() {
		cout << _n << endl;
	}
private:
	//必须在初始化列表中初始化
	const int _n;
};
int main()
{
	int x = 0;
	date d1(x);
	d1.Print();
	return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;
class date
{
public:
	date(int& x)//初始化列表
		: _ret(x)
	{}
	void Print() {
		cout << _ret << endl;
	}
private:
	int& _ret;
};
int main()
{
	int x = 100;
	date d1(x);
	d1.Print();
	return 0;
}
cpp 复制代码
#include<iostream>
class date
{
public:
	date(int year, int month, int day)

		:_year(year)//尽可能走初始化列表
		, _month(month)


	{

	}

	void Print() {
		std::cout << _year << "/" << _month << "/" << _day << std::endl;
	}
private:
	//声明,缺省值->初始化列表
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{

	date d1(2020, 1, 10);
	d1.Print();
	return 0;
}

这里就算我们没有写"日"的初始化列表,但是在声明中我们给了初始值,就会调用初始值

题目

下面程序的运行结果是什么()

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出 1 随机值

E. 输出 1 2

F. 输出 2 1

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
	}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa(1);
	aa.Print();
}

成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照初始化列表中写的顺序

初始化过程(构造 A aa(1)):

  • 先初始化 _a2 (因为它先声明):
    • 初始化列表中写了 _a2(_a1),但此时 _a1还没有被初始化
    • _a1 仍然处于"未初始化"状态(虽然类中写了默认值 =2,但在初始化列表阶段,默认值不会覆盖初始化列表)。
    • 所以 _a2 被初始化为 _a1 当前的值 → 未定义行为(通常是垃圾值)
  • 再初始化 _a1
    • 初始化列表中写了 _a1(a),即 _a1 = 1

类型转换

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数

• 构造函数前面加explicit就不再支持隐式类型转换

• 类类型的对象之间也可以隐式转换,需要相应的构造函数支持

cpp 复制代码
 int i = 1;
 double j = i;

在这段代码中,我是先让i初始化为1,然后用i去初始化j,但是i和j不是相同的数据类型,所以在这个过程中会产生**临时变量,**先让i的值给这个编译器生成的临时变量,然后让j去接收这个临时变量

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
    //explicit A(int a = 0)
    A(int a)
    :_a1(a)
    { }
    void print()
    {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a1;
    int _a2;
};

int main()
{
    //类型转换
    A aa2 = 2;
    aa2.print();
    return 0;
}

这里我把整型类型的值初始化给类类型,就会产生临时对象,这个临时对象再给类类型,而这一步会被编译器优化掉,但从语法上是会产生临时对象

我们再来看一个新东西

这里的raa1初始化的值是aa2,但是raa2不能初始化为2,因为类型不同会产生临时变量,而临时对象具有常性,需要用const修饰

cpp 复制代码
int main()
{
    A aa2 = 2;
    A& raa1 = aa2;
   const A& raa2 = 2;
    return 0;
}

接下来在看一下这段代码

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:

	A(int a = 0)
	{
		_a1 = a;
	}

	A(const A& aa)
	{
		_a1 = aa._a1;
	}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
	}
    private:
    	int _a1;
    	int _a2;
    
};
class Stack
{
public:
    void push(const A& aa)
    {
        
    }
private:
    A _arr[10];
    int _top;
};
int main()
{
    A aa2 = 2;
    A& raa1 = aa2;
   const A& raa2 = 2;

   Stack st;
   	A aa3(3);
   	st.push(aa3);
    return 0;
}

首先我把aa3初始化为3,然后把aa3给push,直接将 aa3 的地址绑定到引用 aa,"st.push(aa3)"不会产生临时变量,但整个过程较为麻烦,那我们也可以这样写

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:

	A(int a = 0)
	{
		_a1 = a;
	}

	A(const A& aa)
	{
		_a1 = aa._a1;
	}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
	}
    private:
    	int _a1;
    	int _a2;
    
};
class Stack
{
public:
    void push(const A& aa)
    {
        
    }
private:
    A _arr[10];
    int _top;
};
int main()
{
   Stack st;
   	st.push(3);
    return 0;
}

这样我直接push3会产生临时变量,再把临时变量给push函数,所以我们需要加const修饰

友元函数

类的私有成员(private) 是被严格保护的,外部函数或普通函数无法直接访问

但在实际开发中,我们有时候希望某个外部函数能够直接访问类的私有成员,这时就需要用到 友元函数

cpp 复制代码
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}
相关推荐
深蓝轨迹2 小时前
黑马点评--达人探店模块
java·spring boot·redis
tankeven2 小时前
HJ154 kotori和素因子
c++·算法
寂静or沉默2 小时前
Java程序员技术面试:如何清晰描述项目难点?逻辑模板!Java的原因与解决方案最新发布!
java·开发语言·面试
llilian_162 小时前
ptp从时钟 ptp授时模块 如何挑选PTP从时钟授时协议模块 ptp从时钟模块
数据库·功能测试·单片机·嵌入式硬件·测试工具
municornm2 小时前
【MySQL】to_date()日期转换
数据库·mysql
东离与糖宝3 小时前
Gradle 9.4+Java26:大型项目构建提速100倍实战配置
java·人工智能
想进大厂的小徐3 小时前
maven的子模块和子pom的区别
java·maven
pengles3 小时前
基于RuoYi-Vue-Plus项目实现移动端项目
java·vue.js·uni-app
希望永不加班3 小时前
SpringBoot 编写第一个 REST 接口(Get/Post/Put/Delete)
java·spring boot·后端·spring