详细介绍了类的默认成员函数中的构造函数、析构函数和拷贝函数
1.赋值运算符重载
1.1运算符重载
运算符重载是C++中一种特殊的函数,它允许开发者为自定义的类类型对象赋予特定的运算符行为。通过运算符重载,类对象可以像内置类型一样使用运算符进行操作,从而提高代码的直观性和可读性。
-
运算符重载的基本概念:
- 运算符重载是通过定义特定名称的函数来实现的,函数的名称由
operator
关键字和后面要重载的运算符组成。例如,operator+
用于重载加法运算符。 - 运算符重载函数与其他函数类似,它有返回类型、参数列表和函数体。
- 运算符重载时,参数的数量与运算符操作对象的数量一致。通常:
- 一元运算符有一个参数。
- 二元运算符有两个参数。
- 左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
- 运算符重载是通过定义特定名称的函数来实现的,函数的名称由
-
成员函数与非成员函数:
- 如果运算符重载函数是成员函数,第一个参数是隐式的
this
指针,指向调用运算符的对象,因此成员函数的参数个数比运算符对象的数量少一个。 - 如果运算符重载函数是非成员函数(全局函数),则必须显式地传递所有的参数,包括左右操作数。
- 如果运算符重载函数是成员函数,第一个参数是隐式的
-
运算符优先级与结合性:
- 运算符重载后,其优先级和结合性与内置类型的运算符保持一致。
-
不可重载的运算符:
- 以下五个运算符不能被重载:
.*
(成员指针访问运算符)::
(作用域解析运算符)sizeof
(大小运算符)?:
(三目运算符),
(逗号运算符)
- 这些运算符不能通过重载定义新的含义。
- 以下五个运算符不能被重载:
-
运算符重载的意义:
- 运算符重载的目的是为了让特定的操作更符合类的语义。例如,对于
Date
类,重载operator-
可能有意义(比如日期相减),但重载operator+
可能没有实际意义。
- 运算符重载的目的是为了让特定的操作更符合类的语义。例如,对于
-
前置和后置运算符重载:
- 在重载递增(
++
)运算符时,必须区分前置和后置版本:- 前置递增(
++obj
)和后置递增(obj++
)共享一个函数名operator++
。 - 后置递增需要额外添加一个
int
类型的参数,以便与前置递增区分开来。
- 前置递增(
- 在重载递增(
-
重载
<<
和>>
运算符:- 对于流插入(
<<
)和流提取(>>
)运算符,应该将它们重载为全局函数,而不是成员函数。原因是:- 成员函数会将
this
指针作为第一个参数,这会导致运算符的语法与习惯不符,如object << cout
会变成cout << object
,这不符合常规的可读性。 - 重载为全局函数时,第一个参数是
ostream
或istream
,第二个参数才是类类型对象,这样符合使用习惯。
- 成员函数会将
- 对于流插入(
1.1.1如何实现运算符重载?
成员函数实现:
- 一元运算符通过成员函数实现时,无需参数。
- 二元运算符通过成员函数实现时,右侧操作数是函数参数。
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
// 重载一元运算符 -
MyClass operator-() const {
return MyClass(-value);
}
// 重载二元运算符 +
MyClass operator+(const MyClass& other) const {
return MyClass(value + other.value);
}
};
全局函数实现:
- 使用友元函数可以访问类的私有成员。
- 通常用于流插入和提取运算符 (
<<
和>>
)
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
// 声明友元
friend ostream& operator<<(ostream& os, const MyClass& obj);
};
// 全局函数实现
ostream& operator<<(ostream& os, const MyClass& obj) {
os << obj.value;
return os;
}
特殊情况
前置与后置递增/递减:
- 前置递增:
operator++()
- 后置递增:
operator++(int)
,通过增加一个虚拟的int
参数来区分后置版本。
class Counter {
public:
int count;
Counter(int c) : count(c) {}
// 前置++
Counter& operator++() {
++count;
return *this;
}
// 后置++
Counter operator++(int) {
Counter temp = *this;
++count;
return temp;
}
};
流插入 (<<
) 和流提取 (>>
):
- 必须使用全局函数重载,因为成员函数会占用左操作数的位置,无法实现标准用法。
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
friend ostream& operator<<(ostream& os, const MyClass& obj);
};
ostream& operator<<(ostream& os, const MyClass& obj) {
os << "Value: " << obj.value;
return os;
}
总结
- 运算符重载让类对象的操作更加直观和类似于内置类型,但需谨慎使用,以确保符合类的语义。
- 常用的重载运算符包括
+
、-
、==
、=
、<<
和>>
。 - 注意不可重载的运算符及遵守C++规则,例如不能改变内置类型的运算行为。
1.2 赋值运算符重载
赋值运算符重载是C++中的一个特殊运算符重载,用于实现两个已经存在对象之间的赋值操作(区别于拷贝构造函数,它用于新对象的初始化)。
-
必须是成员函数
- 赋值运算符重载(
operator=
)只能作为成员函数实现,不能是全局函数。 - 参数建议使用
const
当前类的引用,避免传值传参时的额外拷贝。
- 赋值运算符重载(
-
返回类型为当前类的引用
- 赋值运算符重载通常返回当前类的引用(
*this
),以支持链式赋值。
- 赋值运算符重载通常返回当前类的引用(
A a, b, c;
a = b = c; // 链式赋值
-
默认赋值运算符的行为
- 如果没有显式定义赋值运算符重载,编译器会生成一个默认版本:
- 对于内置类型成员变量:执行值拷贝(浅拷贝)。
- 对于自定义类型成员变量:调用其拷贝赋值运算符。
- 这种默认行为可能无法正确处理动态资源。
- 如果没有显式定义赋值运算符重载,编译器会生成一个默认版本:
-
何时需要自定义赋值运算符重载
- 对于像
Date
类这样的简单类(只有内置类型成员变量且不涉及资源管理),编译器生成的默认赋值运算符通常足够。 - 对于涉及动态资源(如指针)的类(如
Stack
类),默认的赋值运算符只会完成浅拷贝,可能导致多个对象指向同一资源,进而引发问题。因此需要手动实现深拷贝。 - 如果类实现了析构函数以释放资源,就应该显式定义赋值运算符重载以确保正确管理资源。
- 对于像
-
自动调用链
- 像
MyQueue
类,其成员是自定义类型(如Stack
),编译器生成的赋值运算符会自动调用Stack
的赋值运算符重载。如果Stack
的赋值运算符正确实现,MyQueue
通常不需要额外实现赋值运算符重载。
- 像
-
浅拷贝与深拷贝的需求
- 浅拷贝:直接复制指针值,两个对象共享同一块资源。
- 深拷贝:为每个对象分配独立的资源,复制内容而非指针。
#include <iostream>
#include <cstring>
using namespace std;
class Stack {
public:
Stack(int n = 4) {
_a = new int[n];
_capacity = n;
_top = 0;
}
// 赋值运算符重载
Stack& operator=(const Stack& other) {
if (this != &other) { // 防止自赋值
// 释放当前资源
delete[] _a;
// 分配新资源并拷贝数据
_a = new int[other._capacity];
memcpy(_a, other._a, other._top * sizeof(int));
_capacity = other._capacity;
_top = other._top;
}
return *this;
}
~Stack() {
delete[] _a;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
注意
- 赋值运算符重载是用于处理两个已存在对象之间的赋值,需显式实现以满足类的特定需求。
- 默认行为:内置类型执行浅拷贝;自定义类型调用其赋值运算符。
- 深拷贝:适用于管理动态资源的类。
- 只要类中涉及动态资源,或者实现了析构函数释放资源,就应显式实现赋值运算符重载以防止资源管理问题。
#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) {
cout << "Date(const Date& d)" << endl;
_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(2024, 7, 5);
Date d2(d1); // 拷贝构造
Date d3(2024, 7, 6);
d1 = d3; // 赋值运算符重载
// 注意:这里是拷贝构造,不是赋值运算符重载
// 拷贝构造用于一个对象拷贝初始化另一个新对象
// 而赋值运算符用于两个已存在对象之间的拷贝赋值
Date d4 = d1;
return 0;
}
写到这里,就可以引入关键的日期类的实现了(太长所以放另一篇了)
2.取地址运算符重载
在 C++ 中,取地址运算符 &
是一个非常特殊的运算符,它用于返回变量或对象的内存地址。通常情况下,取地址运算符是不能被重载的,因为它是编译器内置的操作。但在 C++ 中,实际上可以 重载“取地址运算符”,让它的行为有所不同。
2.1 const成员函数
2.1.1 什么是 const
成员函数?
在 C++ 中,const
成员函数是指在函数声明的末尾加上 const
关键字,表明该成员函数不会修改对象的状态。也就是说,const
成员函数可以被 const
对象调用,因为它保证不会改变对象的任何数据成员。
2.1.2 const
成员函数的作用
const
成员函数的主要作用是保证不会修改对象的状态。这对于保持类的不变性(即对象一旦创建,其状态不会被改变)非常重要,尤其是当你有 const
类型的对象时,编译器需要确保你只能调用那些不改变对象的函数。
为什么需要 const
成员函数?
- 只读操作:有些成员函数只做查询操作,不需要修改对象的成员变量,这时你应该将它们声明为
const
成员函数,以明确表明不会修改对象。 - 适应
const
对象:如果你有一个const
对象,只能调用const
成员函数,否则会导致编译错误。const
成员函数保证不会修改对象,因此它可以安全地应用于const
对象。
2.1.3 const
成员函数的声明和定义
const
成员函数的声明和定义非常简单。只需要在函数声明的末尾加上 const
,告诉编译器该函数不会修改类的数据成员。
语法
return_type function_name(parameters) const;
这里,const
是放在成员函数声明的末尾,表示该函数在调用时不会修改成员变量。
2.1.4 代码示例:使用 const
成员函数
举个简单的例子
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int value) : _value(value) {}
// 非 const 成员函数:修改 _value
void setValue(int value) {
_value = value;
}
// const 成员函数:不修改 _value
int getValue() const {
return _value;
}
private:
int _value;
};
int main() {
MyClass obj(42);
// 调用非 const 成员函数,可以修改 _value
obj.setValue(100);
// 调用 const 成员函数,不能修改 _value
cout << "Value: " << obj.getValue() << endl;
return 0;
}
setValue
函数是一个普通成员函数,它修改了类的成员变量_value
。getValue
函数是一个const
成员函数,它只是读取_value
的值,不会修改它。- 在
main
函数中,obj.getValue()
被调用时,它没有修改对象的状态,这使得它成为一个const
函数,可以被const
对象调用。
2.1.5 const
对象与 const
成员函数
const
对象只能调用 const
成员函数。这是为了确保 const
对象的状态不会被修改。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int value) : _value(value) {}
// const 成员函数:不会修改 _value
int getValue() const {
return _value;
}
// 非 const 成员函数:修改 _value
void setValue(int value) {
_value = value;
}
private:
int _value;
};
int main() {
const MyClass obj(42); // const 对象
// 调用 const 成员函数
cout << "Value: " << obj.getValue() << endl;
// 错误!不能调用非 const 成员函数
// obj.setValue(100); // 编译错误:无法对 const 对象调用非 const 函数
return 0;
}
obj
是一个const
类型的对象。getValue
是一个const
成员函数,它可以被const
对象obj
调用,因为它不会修改对象的状态。setValue
是一个非const
成员函数,它试图修改_value
,因此不能被const
对象调用。
在高版本vs下无法演示,但想展示的是会输入
error: 'void MyClass::setValue(int)' cannot be called on a const object
vs的话会直接成功
但我们不能指望所以编译器都能自己解决这个错误不是(
2.1.6 const
成员函数访问 const
成员
const
成员函数只能访问 const
成员变量,并且不能修改它们。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int value) : _value(value) {}
// const 成员函数:不能修改 _value
int getValue() const {
return _value;
}
// 这是一个普通成员函数,能够修改 _value
void setValue(int value) {
_value = value;
}
private:
mutable int _value; // 允许在 const 成员函数中修改该成员
};
int main() {
MyClass obj(42);
// 调用 const 成员函数,安全地访问成员
cout << "Value: " << obj.getValue() << endl;
// 调用非 const 成员函数,修改成员
obj.setValue(100);
// 调用 const 成员函数,再次打印
cout << "Updated Value: " << obj.getValue() << endl;
return 0;
}
mutable
关键字
mutable
关键字允许在const
成员函数中修改该成员,即使该成员通常在const
成员函数中是不可修改的。- 在这个示例中,我们使用
mutable
关键字使_value
成员在const
成员函数中仍然可以被修改。
2.2 取地址运算符(&
)
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别⼈取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。
2.2.1 取地址运算符(&
)的默认行为
首先,你应该知道,C++ 中的取地址运算符 &
是一个内建的运算符,它用于获取一个对象的内存地址。
比如,你有一个变量 int x = 10;
,你可以通过 &x
获取 x
变量的内存地址。
int x = 10;
cout << &x << endl; // 输出 x 的内存地址
在这里,&x
返回的是 x
变量的内存地址,通常是一个指针类型。
2.2.2 为什么要重载取地址运算符?
通常情况下,取地址运算符 &
会返回对象的内存地址,但是你可以重载它,改变它的默认行为,让它返回你想要的内容。就像你可以在 operator+
里定义两个数相加的规则一样,你也可以在 operator&
里定义如何取地址。
这就是取地址运算符重载的真正目的!你可以通过重载它,来改变对象“取地址”的行为。例如,你可能不想返回对象的直接地址,而是返回某个特殊的指针、内存位置,或者做一些特定的处理。
2.2.3 重载取地址运算符的基本语法
class MyClass {
public:
int data;
// 重载取地址运算符
int* operator&() {
cout << "调用了重载的&运算符!" << endl;
return &data; // 返回成员变量 data 的地址
}
};
在这个例子中,operator&()
是我们重载的取地址运算符,它改变了取地址的行为。每当我们通过 &
操作符去获取 MyClass
类的对象的地址时,实际调用的是这个重载的 operator&()
方法,而不是默认的行为。
2.2.4 实际例子
比如说...智能指针,智能指针的目标是像普通指针一样使用,但是它会负责内存的管理,防止内存泄漏。我们可以通过重载取地址运算符,让智能指针更加灵活。
#include <iostream>
using namespace std;
class SmartPointer {
private:
int* ptr;
public:
SmartPointer(int value) {
ptr = new int(value);
}
~SmartPointer() {
delete ptr;
cout << "内存已释放" << endl;
}
int* operator&() {
cout << "访问智能指针的内存地址!" << endl;
return ptr;
}
int getValue() const {
return *ptr;
}
};
int main() {
SmartPointer sp(10);
cout << "值: " << sp.getValue() << endl;
int* p = &sp; // 调用 operator&()
cout << "指针地址: " << p << endl;
cout << "值: " << *p << endl;
return 0;
}
详细可见 智能指针讲解-优快云博客
2.2.5 为什么要重载取地址运算符?
通过重载取地址运算符,智能指针不仅能像普通指针一样访问对象,还能提供额外的功能,例如:
- 打印信息:在取地址时输出调试信息(比如
cout << "访问智能指针的内存地址!"
),方便我们调试程序。 - 资源管理:控制如何访问和管理内存。通过重载
operator&
,你可以灵活地控制指针的行为,而不仅仅是简单的返回内存地址。
2.2.6 注意事项
- 滥用风险:重载运算符虽然很方便,但滥用可能会导致代码变得不易理解。例如,改变取地址的默认行为可能让其他程序员感到困惑,难以理解代码的原意。
- 与引用运算符区分:在 C++ 中,
&
运算符既有取地址的作用,也有作为引用的意义。取地址和引用运算符虽然使用相同的符号,但实际上是两个不同的概念。
下一章讲构造函数和静态成员,大概还有类型转换 类与对象(4)-优快云博客