带你了解C++的类与对象(中)

类与对象

  • [1. 类的默认成员函数](#1. 类的默认成员函数)
  • [2. 构造函数](#2. 构造函数)
    • [2.1 无参构造函数(默认构造之一)](#2.1 无参构造函数(默认构造之一))
    • [2.2 带参构造函数(注,不算默认构造函数)](#2.2 带参构造函数(注,不算默认构造函数))
    • [2.3 全缺省构造函数(默认构造之一)](#2.3 全缺省构造函数(默认构造之一))
    • [2.4 编译器自己生成(默认构造之一 · 难点❗️)](#2.4 编译器自己生成(默认构造之一 · 难点❗️))
      • [2.4.1 成员变量为内置类型](#2.4.1 成员变量为内置类型)
      • [2.4.2 成员变量为自定义成员变量](#2.4.2 成员变量为自定义成员变量)
    • [2.5 总结](#2.5 总结)
  • [3. 析构函数](#3. 析构函数)
    • [3.1 析构函数的特点](#3.1 析构函数的特点)
    • [3.2 自己写析构](#3.2 自己写析构)
    • [3.3 编译器自动生成析构](#3.3 编译器自动生成析构)
    • [3.4 总结](#3.4 总结)
  • 便利之处
  • [4. 拷贝构造函数](#4. 拷贝构造函数)
    • [4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?](#4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?)
      • [4.1.1 为什么要进行深拷贝?](#4.1.1 为什么要进行深拷贝?)
      • [4.1.2 如何进行深拷贝?](#4.1.2 如何进行深拷贝?)
    • [4.2 什么时候会调用拷贝构造?](#4.2 什么时候会调用拷贝构造?)
      • [4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象](#4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象)
      • [4.2.2 自定义类型对象 作为 函数参数,进行值传递](#4.2.2 自定义类型对象 作为 函数参数,进行值传递)
      • [4.2.3 函数返回一个 局部自定义类型对象(值返回)](#4.2.3 函数返回一个 局部自定义类型对象(值返回))
    • [4.3 传值返回和传引用返回 在拷贝构造的区别](#4.3 传值返回和传引用返回 在拷贝构造的区别)
    • [4.4 总结](#4.4 总结)
  • [5. 赋值运算符重载](#5. 赋值运算符重载)
    • [5.1 运算符重载](#5.1 运算符重载)
    • [5.2 赋值运算符重载](#5.2 赋值运算符重载)
    • [5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?](#5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?)
  • [6. 取地址运算符重载](#6. 取地址运算符重载)
    • [6.1 const 成员函数](#6.1 const 成员函数)
    • [6.2 取地址重载](#6.2 取地址重载)

1. 类的默认成员函数

  • 默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数就叫默认成员函数。
    一个类,我们不写的情况下,编译器会默认生成以下6个默认成员函数。需要注意的是,这6个中,最重要的是前4个,最后两个取地址重载不重要,我们稍微了解一下即可。
  • 其次,就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个以后讲解。
  • 默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

    • 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
    • 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现时,如何自己实现?

2. 构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象

构造函数本质是要替代我们以前Stack类中写的Init函数的功能,构造函数自动调用的特点完美替代了Init()

构造函数的特点:

  • 基本特性:

    1. 函数名与类名相同。
    2. 无返回值。(返回值什么都不用给,也不需要写void)
    3. 对象实例化时,系统就会自动调用对应的构造函数。
    4. 构造函数可以重载。
  • 进阶特性:

    1. 如果类中没有显式定义的构造函数,C++编译器就会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成。

    2. 无参构造函数、全缺省构造函数、我们不写构造时,编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数中,有且仅有一个存在,不能同时存在。无参构造函数和全缺省构造函数,虽然构成函数重载,但是调用时会存在歧义。

注意:很多人会认为默认构造函数,是编译器默认生成的函数叫默认构造,实际上,无参构造函数、全缺省构造函数也是默认构造。
总结一下,就是不传实参就可以调用的构造函数,就叫默认构造函数。

  1. (难点❗️)我们不写,而编译器默认生成的构造
  • 内置类型成员变量的初始化没没有要求,也就是说,是否初始化是不确定的,看编译器。
  • 对于自定义类型成员变量 ,要求调用这个成员变量的 默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,下个章节再细细讲解。

注:C++把类型分为内置类型(基本类型)和自定义类型。

内置类型:语言提供的原生数据类型,如:int/char/double/指针等。

自定义类型:我们使用class/struct等关键字自己定义的类型。

2.1 无参构造函数(默认构造之一)

❗️注意,无参自动调用函数时,不需要加括号。

若给无参加括号,无法区分是在定义对象 还是 函数声明,所以不能给无参加括号

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	// 1. 无参构建函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;//自动调用,无参不用加括号
	d1.Print();

	return 0;
}

2.2 带参构造函数(注,不算默认构造函数)

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	// 2. 定义带参函数
	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()
{
	Date d2(2026, 4, 6);//带参把括号加到后面
	d2.Print();

	return 0;
}

2.3 全缺省构造函数(默认构造之一)

会和无参函数产生调用歧义,功能上也可以取代带参函数

所以用了全缺省就不用无参和带参了

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	// 3. 全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();// 结果:1/1/1
	
	Date d2(2026, 4, 6);
	d2.Print();// 结果:2026/4/6
	
	Date d3(2026);
	d3.Print();// 结果:2026/1/1

	return 0;
}

2.4 编译器自己生成(默认构造之一 · 难点❗️)

2.4.1 成员变量为内置类型

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	//4. 什么都不写,编译器自己生成

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();//随机值

	return 0;
}

2.4.2 成员变量为自定义成员变量

编译器自己生成的构造,只做相对浅显的初始化操作,因此大部分构造函数的初始化操作,都需要我们自己手搓;

但是!当类的成员变量,只包含自定义成员变量时,编译器自己生成的构造,就足够了。

例:用两个栈(Stack)来实现队列(MyQueue)

cpp 复制代码
#include<iostream>
using namespace std;

//自己实现栈,需要自己写
typedef int STDataType;
class Stack
{
public:
	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
	STDataType* _a;
	size_t _top;
	size_t _capacity;
};

//两个栈实现队列 - 不需要自己写
class MyQueue
{
public:
	//编译器默认生成 MyQueue 的构造函数,调用了 Stack 的构造
	//完成了两个成员的初始化
private:
	Stack pushst;//类类型 - 调用 Stack 的默认构造初始化✅️
	Stack popst;//类类型 - 调用 Stack 的默认构造初始化✅️
	
	//下面这个int x;只是为了说明内置类型是浅初始化
	//实际用两个栈实现队列时不需要写下面这句代码
	// int x; //内置类型 - 浅初始化(可能是随机值,也可能为0)
};

⭕️原理

编译器自己生成的构造 会调用每个自定义类型成员变量的【默认构造函数初始化】。

注: 对内置类型成员变量 的初始化没有要求,内置类型成员变量是随机值还是0,其实是不确定的,看编译器决定。

补充概念- 成员对象:成员变量里,自定义类型的对象。

2.5 总结

总结:大多数情况,构造函数都需要我们自己去实现

当类的成员变量,只包含自定义成员变量时,且自定义成员变量有默认构造时,编译器自动生成函数函数,就能用。(类似 MyQueue 且 Stack 有默认构造时,MyQueue 靠编译器自动生成构造函数就够了)。

3. 析构函数

析构函数 与 构造函数 的功能相反。

析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束,栈帧销毁,它就释放了,不需要我们管。

C++规定,对象在销毁时会自动调用析构函数,完成对象中资源清理释放工作。

析构函数的功能类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有 Destroy ,其实就是没有资源需要释放,所以严格来说 Date 是不需要析构函数的。

❗️注意:

如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;

如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;

但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。

3.1 析构函数的特点

  1. 析构函数名是类名前加上字符~(记忆方式:"~"在C语言中正好是"按位取反"的意思,而析构函数和构造函数的功能相反)
  2. 无参数,无返回值(这里分构造类似,也不需要加void)
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,系统会自动调用析构函数。
  5. 跟构造函数类似,我们不写,编译器自动生成的析构函数,对于内置联系成员不做处理,而对于自定义类型成员 则会调用他的析构函数
  6. 还需要注意的是,我们显式写析构函数时,对于自定义类型成员也会调用它的析构,也就是说,自定义类型成员,无论什么情况,都会自动调用析构函数。
  7. 如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。
  8. 一个局部域的多个对象,C++规定,后定义的先析构。

3.2 自己写析构

❗️注意:

  • 析构函数无参数,无返回值,并且一个类只能有一个析构函数。
  • 后定义的先析构
cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	STDataType* _a;
	size_t _top;
	size_t _capacity;
};

int main()
{
	Stack s1;
	Stack s2;//s2先被析构

	return 0;
}

3.3 编译器自动生成析构

💡原理:
遇到自定义类型成员,无论什么情况,编译器都会自动调用析构函数,所以,当类中包含自定义类型成员,且该成员已写好析构函数时,我们可以不管该成员。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;

//自己实现栈,需要自己写
typedef int STDataType;
class Stack
{
public:
	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}
private:
	STDataType* _a;
	size_t _top;
	size_t _capacity;
};

//两个栈实现队列 - 不需要自己写
class MyQueue
{
public:
	// 编译器默认生成 MyQueue 的构造函数,调用了Stack的构造
	// 编译器默认生成 MyQueue 的析构函数,调用了Stack的析构
	// 显式写析构,也会自动调用 Stack 的析构
	~MyQueue()
	{
		cout << "~MyQueue()" << endl;
	}
private:
	Stack pushst;//类类型 - 调用 Stack 的默认构造初始化✅️
	Stack popst;//类类型 - 调用 Stack 的默认构造初始化✅️
	// int x; //内置类型 - 浅初始化(随机值)
};

int main()
{
	MyQueue mq;

	return 0;
}

3.4 总结

后定义的函数先被析构。

如果类中没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如Date;

如果默认生成的析构就能用,也不需要显式写析构,如MyQueue;

但是,有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。

便利之处

有些人可能会想,构造函数和析构函数用起来十分奇怪,接下来,这边利用一道经典oj题:有效的括号,来讲解构造函数和析构函数的便利之处

代码实现:栈

cpp 复制代码
#include<iostream>
using namespace std;

//自己实现栈,需要自己写
typedef char STDataType;
class Stack
{
public:
		// 构造函数
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc fail");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    // 析构函数
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _capacity = _top = 0;
    }
    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = 2 * _capacity;
            STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _capacity = newcapacity;
            _a = tmp;
        }
        _a[_top++] = x;
    }
    void Pop()
    {
        _top--;
    }
    bool Empty()
    {
        return _top == 0;
    }

    STDataType Top()
    {
        return _a[_top - 1];
    }
private:
    STDataType* _a;
    size_t _top;
    size_t _capacity;
};

代码实现:判断括号是否有效

cpp 复制代码
//判断左右括号是否有效
bool isValid(const char* s)
{
    Stack st;
    // 自动调用默认构造函数初始化 ✅️
    while (*s)
    {
        //左括号入栈
        if (*s == '(' || *s == '[' || *s == '{')
        {
            st.Push(*s);
        }
        else
        {
            //右比左多
            if (st.Empty())
                return false;
            char top = st.Top();
            if ((top == '{' && *s != '}') ||
                (top == '(' && *s != ')') ||
                (top == '[' && *s != ']'))
            {
                return false;
            }
            st.Pop();
        }
        s++;
    }

    // 若不为空,则说明左比右多,不匹配
    // 若为空,则数量一致,匹配
   
    return st.Empty();

    // 在C语言中,我们需要储存返回值,再释放栈内数组
    // 而我们常常忘记释放内存,内存泄露是个很严重的问题,还不会报错
    // 而在C++,有了析构函数后,我们就可以直接返回了✅️
    //这就是C++的便利之处。   
}

也就是说,在C++里有了构造函数和析构函数后,我们在代码中使用类时,就不需要额外的调用初始化函数;在返回时,也可以直接返回,不用额外去释放内存了,不用担心内存泄露。

4. 拷贝构造函数

如果一个构造函数的第一个参数,是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,也就是说,拷贝构造是一个特殊的构造函数。

拷贝构造的特点:

  1. 拷贝构造函数,是构造函数的一个重载。
  2. 拷贝构造函数的第一个参数,必须是类类型对象的引用,且任何额外的参数都有默认值,使用传值方式传参,编译器会直接报错,因为语法逻辑上会引发无穷递归调用。
  3. C++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
  4. 未显式定义拷贝构造 ,编译器会自动生成拷贝构造函数
    自动生成的拷贝构造,对内置类型成员和自定义类型成员的操作如下------
    内置类型成员变量 :会完成值拷贝/浅拷贝(一个字节一个字节地拷贝)
    自定义类型成员:会调用它的拷贝构造。

这里用Date类来具体讲解拷贝构造函数的注意点:

❌️错误案例:直接传值传参

cpp 复制代码
	// error: "Date" 的复制构造函数不能带有 "Date" 类型的参数
	Date(Date d1)//直接传值传参❌️
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}
	int main()
	{
		Date d1(2026, 4, 11);

		Date d2(d1);
		d2.Print();
	
		return 0;
	}

✅️正确示范:传递自定义类型的引用

cpp 复制代码
	Date(const Date& d1)//传引用✅️
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}
	int main()
	{
		Date d1(2026, 4, 11);

		Date d2(d1);
		d2.Print();
	
		return 0;
	}

❓️ask:为什么要传递引用,而不是直接传值传参呢?

⭕️答:首先要明白,传值传参的过程中,会触发拷贝构造函数。

若对拷贝构造的参数进行传值 (而不是引用),则在进入拷贝构造函数体之前,实参d1需要形成自己的临时拷贝 ------形参d,而这就是一个传值传参 的过程,此时就会再次触发拷贝构造 的机制,形成一种新的拷贝构造,又开始传值传参...最后形成无穷递归。

简单来讲就是,传值传参->拷贝构造->传值传参->拷贝构造...无穷套娃。

若使用了引用 ,则相当于只是给拷贝的值,也就是实参d1取了个别名 d,没有开辟新的内存空间(意味着没有创建新对象 ),也就是说,这并没有进行传值传参,自然也不会再次触发拷贝构造操作,保证了拷贝构造函数正常执行。

4.1 拷贝构造函数什么时候需要显式写,什么时候不需要?

  • Date 类:这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。
  • Stack 类:虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝,并不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
  • MyQueue 类:内部主要是是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显式实现 MyQueue 的拷贝构造。
    💡小技巧:如果一个类显式实现了析构,并释放资源,那么它就需要显式写拷贝构造,否则不需要。

4.1.1 为什么要进行深拷贝?

接下来以 Stack 类来举例:

⭕️答:
Stack 不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝,会导致 st1st2 里的 _a 指针指向同一块资源 ,析构时会析构两次,程序崩溃。

所以需要我们进行深拷贝。

❌️错误案例:无显式实现拷贝构造,指向资源的指针导致多次析构,程序崩溃。

cpp 复制代码
#include<iostream>
using namespace std;

//自己实现栈,需要自己写
typedef char STDataType;
class Stack
{
public:
    // 构造函数
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc fail");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    // 析构函数
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _capacity = _top = 0;
    }
    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = 2 * _capacity;
            STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _capacity = newcapacity;
            _a = tmp;
        }
        _a[_top++] = x;
    }
private:
    STDataType* _a;
    size_t _top;
    size_t _capacity;
};

int main()
{
    Stack st1;
    st1.Push(1);
    st1.Push(2);

    // Stack 不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝
    // 会导致 st1 和 st2 里的 _a 指针指向同一块资源
    // 析构时会析构两次,程序崩溃
    Stack st2(st1);

    return 0;
}

由此可知,就需要我们进行深拷贝。

4.1.2 如何进行深拷贝?

✅️正确示范 :显式实现拷贝构造。

代码实现:

  1. 开辟同样大的内存空间
  2. 拷贝数据
cpp 复制代码
#include<iostream>
using namespace std;

    //拷贝构造函数
    // st2(st1) - 需要深拷贝
    Stack(const Stack& st)
    {
        // 需要对_a 指向资源创建同样大的资源再拷贝值
        _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        if (_a == nullptr)
        {
            perror("malloc fail");
            return;
        }
        // 拷贝数据
        memcpy(_a, st._a, sizeof(STDataType) * st._top);

        _top = st._top;
        _capacity = st._capacity;
    }

4.2 什么时候会调用拷贝构造?

总结:只要出现 用一个同类型的类对象,去初始化另一个新对象,就会调用拷贝构造。

4.2.1 用 旧自定义类型对象 初始化 新自定义类型对象

代码演示:

cpp 复制代码
A a1;
A a2 = a1;// 拷贝构造
A a3(a1);// 拷贝构造

4.2.2 自定义类型对象 作为 函数参数,进行值传递

代码演示:

cpp 复制代码
void func(A a)// 参数是值传递
{
	//...
}
int main()
{
	A a1;

	func(a1);//将a1拷贝一份给a -> 调用拷贝构造

	return 0;
}

注:若传的是对象的引用 ,则不会调用拷贝构造

代码演示:

cpp 复制代码
void func(const A& a)// 传对象的引用
{
	//...
}
int main()
{
	A a1;

	func(a1);
	// a是a1的别名,没有创建新对象,不会调用拷贝构造

	return 0;
}

4.2.3 函数返回一个 局部自定义类型对象(值返回)

代码演示:

cpp 复制代码
A func()
{
	A a;
	return a;// 利用a拷贝出临时对象返回->调用拷贝构造
}

4.3 传值返回和传引用返回 在拷贝构造的区别

传值返回:会产生一个临时对象拷贝构造。

传值引用返回:返回的是返回对象的别名(引用),没有产生拷贝。

但如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针。

传引用返回可以减少拷贝,但一定要确保返回对象,在当前函数结束后还在,才能使用引用返回。

  1. 传值返回
cpp 复制代码
A func1()
{
	A a1;
	return a1;//利用a1拷贝出一个临时对象后返回
}

int main()
{
	A ret = func1();

	return 0;
}

可以返回已创建的类对象的引用的值

cpp 复制代码
A func2(A& a)
{
	return a;
}

int main()
{
	A a;
	A ret = func2(a);

	return 0;
}
  1. 传引用返回
    ❌️错误做法:返回局部变量的引用
cpp 复制代码
A& func2()
{
	A a2;
	return a2;// 返回a2的别名,a2出函数后就销毁,相当于野引用
}

int main()
{
	A ret = func2();

	return 0;
}

改进方法:

✅️创建局部变量时,加上static 延长生命周期

cpp 复制代码
A& func2()
{
	static A a2;//static延长了a2的生命周期,使其出函数后不销毁
	return a2;
}

int main()
{
	A ret = func2();

	return 0;

4.4 总结

当函数的参数为自定义类型 时,最好不要使用传值传参,而是要传它的引用

5. 赋值运算符重载

5.1 运算符重载

  • 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式,指定新的含义。

    • C++规定类类型对象 使用运算符时,必须转换成调用对应运算符重载。
    • 若没有对应的运算符重载,则会编译报错
  • 运算符重载,是具有特殊名字的函数 ,它的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也有其返回类型参数列表 ,以及函数体

  • 重载运算符函数的参数个数 和 该运算作用的运算对象数量一样多。

    • 一元运算符有一个参数,二元运算符有两个参数。
    • 二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数,是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。

  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。

  • ❌️注意:不能连接语法中没有的符号,来创建新的运算符,比如:operator@

  • 重点.* :: sizeof ?: . 注意,以上5个运算符,不可以构成重载(选择题常考❗️)

  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator(int x, int y)

  • 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator- 就有意义,但是重载operator+ 就没有意义。

重载为全局时,面临对象访问私有成员变量的问题,有以下方法可以解决:

  1. 成员放公有(不推荐)
  2. Date提供getxxx函数
  3. 友元函数(在类和对象下会讲)
  4. 重载为成员函数(最推荐)

以下先用成员放公有的方式来展示运算符重载,实际不建议用!

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//private:
	int _year;
	int _month;
	int _day;
};

// 重载为全局时,面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有 - 不推荐
// 2、Date提供getxxx函数
// 3、友元函数
// 4、重载为成员函数

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(2024, 7, 5);
	Date d2(2024, 7, 6);
	// 运算符重载函数可以显式调用
	
	operator==(d1, d2);
	// 编译器会转换成 operator==(d1, d2);
	d1 == d2;
	return 0;
}

⭐最推荐的方法:重载为成员函数

❗️注意:如果一个重载运算符函数,是成员函数 ,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个

(注:如果全局中的重载和成员函数重载是一样的,则编译器会优先调用成员函数中的重载)

代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//重载为成员函数
	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(2024, 7, 5);
	Date d2(2024, 7, 6);

	d1.operator==(d2);
	// 编译器会转换成 d1.operator==(d2);
	d1 == d2;

	return 0;
}

5.2 赋值运算符重载

注意区分 赋值重载拷贝和拷贝构造:
赋值运算符重载 :是一个默认成员函数,用于完成两个已经存在的对象 直接拷贝赋值。
拷贝构造 :一个对象拷贝初始化给另一个要创建的对象

代码举例:

cpp 复制代码
	// 赋值重载
	//d1 = d2
	void operator=(const Date& d)//写成 Date d也可以
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	//拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

int main()
{
	Date d1(2024, 7, 5);
	Date d2(2024, 7, 6);

	// 赋值重载拷贝:两个已存在的对象直接拷贝赋值
	d1 = d2;

	// 拷贝构造:⼀个对象拷贝初始化给另⼀个要创建的对象。
	Date d3(d2);
	Date d4 = d2;

	return 0;
}

赋值运算符重载的特点:

  1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议 写成const 当前类类型引用,否则传值传参会有拷贝。(注:可以进行传值传参,在这里并不会引起无穷递归,但为了减少拷贝,还是建议去传 引用

赋值重载可以传值(类类型对象)的原因:

若传值d2,则类类型对象会调用拷贝构造,形成形参d,然后返回,进入到赋值重载中,不会进行无穷递归。

而拷贝构造,是类类型对象会调用拷贝构造,形成形参d,返回后仍在进行拷贝构造,会造成无穷递归。

  1. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景。

⭐补充 - 有返回值的情形

形如 int i = j = k = 1; 这种连续赋值的情况,所以,当需要连续赋值 时,赋值运算符重载也是有返回值 的。

代码演示:

cpp 复制代码
	// d3 = d1;
	Date& operator=(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

❓️ask1:为什么要返回*this*this代表什么?

⭕️答:为了能够进行连续赋值 ,我们需要返回 d3this 实际上是d3 的指针,为了返回d3 我们需要对this指针解引用 ,所以 *this 就是指的是d3。

注:虽然参数中不可以显式写this指针,但是在成员函数体内部是可以显式写this指针的。

❓️ask2:为什么要返回Date的引用(当前类类型的引用)?

⭕️答:因为d3 出当前函数后并不会销毁,可以传引用 ,直接返回d3本身,就不需要再进行拷贝操作了,提升效率。

若传值返回,则还需要多余的拷贝一遍临时对象再返回,拉低效率。

  1. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节地拷贝),对自定义类型成员变量会调用它的赋值重载函数。

5.2.1 赋值运算符重载 什么时候需要显式写,什么时候不需要?

(其实跟4.1的道理一样)

  • Date 类:这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现运算符重载。
  • Stack 类:虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载 完成的值拷贝/浅拷贝,并不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
  • MyQueue 类:内部主要是是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载 会调用 Stack 的拷贝构造,也不需要我们显式实现 MyQueue 的赋值运算符重载。
    💡小技巧 :如果一个类显式实现了析构,并释放资源,那么它就需要显式写赋值运算符重载,否则不需要。

6. 取地址运算符重载

6.1 const 成员函数

  • const 修饰的成员函数称之为 const 成员函数,const 修饰成员函数放到成员函数参数列表的后面

    • const 成员函数:void Print();
    • const 成员函数:void Print() const;
  • const 实际修饰该成员隐含的 this 指针,表明在该成员函数中,不能对类的任何成员进行修改。

    • 例如,const 修饰 Date 类的 Print 成员函数,Print 隐含的 this 指针由 Date* const this ,变为 const Date* const this

✅️非const对象 调用 const成员函数(权限缩小)

对象为非const ,本身可以改变,但是在const成员函数中,就不可以改变了,体现了权限缩小,因此是合法的。

❌️const对象 调用 非const成员函数 (权限扩大)

对象为const,本身不可改变,在非const成员函数中,有可能发生改变!权限扩大,因此是非法的。

  • 所以,如果成员函数中的成员不需要改变,我们就可以给成员函数加上const,保证其安全性。

6.2 取地址重载

取地址运算符重载,分为普通取地址运算符重载 和 const取地址运算符重载,一般这两个函数编译器自动生成的就够用啦,不需要显式实现。

除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

📃代码展示:

cpp 复制代码
class Date
{
public:
	Date* operator&() // 函数1
	{
		return this;
		// return nullptr;
	}

	const Date* operator&()const // 函数2
	{
		return this;
		// return nullptr;
	}

private:
	int _year;
	int _month;
	int _day;
};

注:函数1和函数2构成函数重载,可以同时存在。

虽然语法上只写2并不会报错(无论是传入const对象,还是非const对象,都不会报错)但是,请注意,函数2返回的是const类型的地址

若传入的是非const对象,返回的 const类型的地址,那么,逻辑上岂不是乱套了吗?所以还需要函数1。

而且编译器调用时,也会去调用最匹配的函数。

相关推荐
小苗卷不动2 小时前
UDP服务端收发流程
linux·c++·udp
Xiu Yan2 小时前
Java 转 C++ 系列:函数模板
java·开发语言·c++
小苗卷不动2 小时前
OJ练习之加减(中等偏难)
c++
我能坚持多久2 小时前
String类常用接口的实现
c语言·开发语言·c++
智者知已应修善业2 小时前
【数字稳压控制DAC/TLC5615驱动】2023-5-27
c++·经验分享·笔记·算法·51单片机
t***5442 小时前
Orwell Dev-C++和Embarcadero Dev-C++哪个更稳定
开发语言·c++
代码中介商3 小时前
C++运行时多态深度解析:从原理到实践
开发语言·c++·多态·虚函数
代码中介商3 小时前
C++ 继承与派生深度解析:存储布局、构造析构与高级特性
开发语言·c++·继承·派生
谭欣辰3 小时前
C++ 控制台跑酷小游戏2.0
开发语言·c++·游戏程序