文章目录
1 拷贝构造函数
1.1 什么是拷贝构造函数?
拷贝构造函数是一种特殊的构造函数,它用于将一个对象复制到另一个对象中,或者将一个对象作为参数传递给函数时,自动创建一个新的对象。拷贝构造函数使用的方法与普通构造函数非常相似,但其参数是指向对象本身的引用,而不是指针或值。示例代码如下所示:
class MyClass {
public:
MyClass(); // 默认构造函数
MyClass(const MyClass& other); // 拷贝构造函数
// 其他成员函数和成员变量声明
};
在上面的示例代码中,我们定义了一个名为MyClass的类,并声明了一个拷贝构造函数。这个构造函数的参数是一个指向MyClass对象的常量引用。当我们将一个MyClass对象复制到另一个对象中时,将会自动调用这个拷贝构造函数来完成操作。
1.2 拷贝构造函数的必要性
- 为什么我们需要拷贝构造函数呢?在很多情况下,我们需要在程序的运行过程中复制对象。例如,如果我们在函数中传递对象作为参数,或者将对象赋值给另一个对象,就需要使用拷贝构造函数。
- 拷贝构造函数还有另一个重要作用,就是防止内存泄漏和指针错误(与构造函数和析构函数一起)。如果我们没有正确地实现拷贝构造函数,就会导致指针指向错误的内存地址或未释放的内存,从而引发不可预测的程序行为。
1.3 浅拷贝与深拷贝
必须注意的是,在拷贝一个对象时,有两种常见的方式,即浅拷贝和深拷贝。
浅拷贝是指将一个对象的数据成员直接复制到另一个对象中。这种方式速度非常快,但存在一些潜在的问题,如在析构函数中销毁指针时可能会导致指针指向未知地址或已释放的内存。
例如:若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成
拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调
用其拷贝构造函数完成拷贝的。
深拷贝是指为新对象分配内存,并将原始对象的数据成员复制到新对象中。由于这种方式需要额外的内存分配和拷贝操作,因此速度较慢,但能够确保新对象的数据成员与原始对象完全独立,不会受到原始对象的修改影响。
在实际开发中,应优先考虑采用深拷贝方式,以确保程序的正确性和稳定性。如果你无法保证对象的数据成员都是不可变的,就必须采用深拷贝方式。
1.4 实现拷贝构造函数的方法
在实现拷贝构造函数时,需要牢记以下几点:
- 将参数声明为常量引用类型,以避免拷贝参数对象。
- 在函数体内创建新对象,并将原始对象的数据成员复制到新对象中。
- 当原始对象包含指针时,必须使用深拷贝方式,以确保新对象与原始对象的指针指向不同的内存地址。
下面是一个简单的示例,演示了如何实现一个拥有指针成员的类的拷贝构造函数(深拷贝哦):
#include<iostream>
#include <cstring>
using namespace std;
class MyString {
public:
MyString();
MyString(const char* str); // 常规构造函数
MyString(const MyString& other); // 拷贝构造函数
~MyString();
char* getData() const { return m_pData; }
private:
char* m_pData;
int m_nLength;
};
MyString::MyString() {
m_pData = nullptr;
m_nLength = 0;
}
MyString::MyString(const char* str) {
m_nLength = strlen(str);
m_pData = (char*)malloc((m_nLength + 1)*sizeof(char));
strcpy(m_pData, str);
}
MyString::MyString(const MyString& other) {
m_nLength = other.m_nLength;
m_pData = (char*)malloc((m_nLength + 1)*sizeof(char));
strcpy(m_pData, other.m_pData);
}
MyString::~MyString() {
if (m_pData != nullptr) {
delete[] m_pData;
m_pData = nullptr;
m_nLength = 0;
}
}
在上面的示例代码中,我们定义了一个名为MyString的类,并实现了一个拷贝构造函数。在这个构造函数中,我们首先将要复制的对象的数据成员长度和指针都复制到新对象中,然后再使用malloc关键字为新对象分配内存,并将原始对象的数据成员复制到新对象中。这样就确保了新对象与原始对象的数据成员分离并互不影响。
注意,我们还定义了析构函数来释放指针成员所分配的内存。这是非常重要的,因为如果我们不释放这些内存,就会导致内存泄漏和指针错误。
1.5 拷贝构造函数的使用场景
在实际开发中,我们常常需要在函数中传递对象或者将一个对象赋值给另一个对象。这时,拷贝构造函数就派上了用场。
当我们将对象传递给函数时,拷贝构造函数将会自动创建一个新对象来代替原始对象。例如:
void foo(MyClass m); // 函数定义
MyClass obj; // 创建一个对象
foo(obj); // 调用函数
在这个示例中,我们定义了一个名为foo的函数,该函数接受一个MyClass对象作为参数。当我们将一个MyClass对象传递给foo函数时,将会自动调用MyClass的拷贝构造函数,将原始对象的值复制到新对象中,并将新对象传递给函数。
当我们需要将一个对象赋值给另一个对象时,也需要使用拷贝构造函数。例如:
MyClass obj1; // 创建原始对象
MyClass obj2(obj1); // 创建新对象,并将原始对象的值复制到新对象中
在这个示例中,我们创建了一个名为obj1的MyClass对象,并使用它来创建一个新的MyClass对象obj2。在创建新对象时,自动调用了MyClass的拷贝构造函数来复制原始对象的值。
1.6 拷贝构造函数的注意事项
在实现拷贝构造函数时,我们需要特别注意几个关键点,以确保程序的稳定性和正确性。
1.6.1 处理指针成员
如果一个类具有指针成员,就必须使用深拷贝来确保新对象与原始对象的指针指向不同的内存地址。否则,就可能导致未定义的行为,如指针的释放和删除操作可能会影响到另一个指针的地址。
例如,假设我们的MyString类具有指针成员m_pData,我们必须使用深拷贝方式来复制它,如下所示:
MyString::MyString(const MyString& other) {
m_nLength = other.m_nLength;
if (other.m_pData != nullptr) {
m_pData = (char*)malloc((m_nLength + 1)*sizeof(char));
strcpy(m_pData, other.m_pData);
} else {
m_pData = nullptr;
}
}
在上面的示例中,我们首先判断被复制的对象是否为空,然后才复制指针成员的值。这样就可以避免未定义的行为。
1.7 总结
我们必须合理地运用拷贝构造函数技术,并注意内存管理、异常处理、继承与多态等关键点,以确保程序的稳定性和正确性。
2 赋值运算符重载
2.1 运算符重载基础
运算符重载是C++中的一种特性,它允许程序员重新定义现有运算符的行为,使其适用于自定义类型(例如类)。这样一来,我们就可以像操作内置数据类型一样操作自定义类型。
2.2 重载的运算符种类
C++允许重载大部分运算符,包括:
- 算术运算符:+,-,* ,/,%等
- 关系运算符:==,!=,<,>,<=,>=
- 逻辑运算符:&&,||
- 位运算符:&,|,^,~,<<,>>
- 赋值运算符:=,+=,-=,*=,/=等
- 其他运算符:(),[],->,->*,new,new[],delete,delete[]等。
2.3 重载规则与限制
虽然C++允许我们重载大部分运算符,但仍有一些规则和限制需要遵守:
- 不能重载内置类型的运算符
- 不能创建新的运算符
- 有些运算符不能重载,如:.,.*,::,?:等
- 重载运算符的参数至少有一个是用户定义的类型
- 不能改变运算符的优先级和结合性
2.4 重载运算符的语法
重载运算符的语法与编写函数的语法类似,它由操作符关键字operator和要重载的运算符组成。例如,可以如下定义重载加法运算符方法:
ClassName operator+(const ClassName &obj)
{
// 方法实现
}
重载运算符的函数必须返回一个值,这个值不能是void类型。
2.5运算符重载的方法
重载运算符的方法有两种:
- 成员函数,重载的运算符在两个操作数中的一个是类的对象时使用;
- 非成员函数,重载的运算符不是类的成员函数,但是可以访问类的私有成员。
2.5.1成员函数
使用成员函数重载运算符时,第一个参数必须是仅代表所属类的实例的this指针。在下面的例子中,我们使用成员函数重载乘法运算符:
class ClassName {
public:
int operator*(const ClassName &obj)
{
// 方法实现
}
};
在这个例子中,我们可以使用下面这种方式来调用方法:
ClassName obj1, obj2;
int result = obj1 * obj2;
2.5.2 非成员函数
使用非成员函数重载运算符时,第一个参数应该是要操作的变量类型。在下面的例子中,我们提高运算符“+”为非成员函数:
ClassName operator+(const ClassName &a, const ClassName &b)
{
// 方法实现
}
在这个例子中,我们可以使用下面这种方式来调用方法:
ClassName obj1, obj2;
ClassName obj3 = obj1 + obj2;
2.6 运算符重载示例
下面是一个简单的运算符重载示例,它使用上述定义的成员函数和非成员函数重载+和*运算符:
#include <iostream>
using namespace std;
class MyInt {
public:
int value;
MyInt() {}
MyInt(int i) {value = i;}
MyInt operator+ (const MyInt& other) {
MyInt sum;
sum.value = value + other.value;
return sum;
}
};
int main() {
MyInt a(5), b(3), c;
c = a + b;
cout << c.value << endl; // 输出8
return 0;
}
在上面的代码中,我们定义了一个MyInt类,它包含一个整数成员变量value。我们重载了加号运算符,接受了一个MyInt对象的引用作为参数,并返回一个新的MyInt对象。
在main函数中,我们创建了两个MyInt对象a和b,分别初始化为5和3。然后将它们相加,将结果赋值给另一个MyInt对象c。最后输出c的值,即8。
这个示例较为简单,但是展示了运算符重载的基本语法和用法。
2.7 总结
通过运算符重载,我们可以为自定义数据类型定义自己的运算符。重载运算符需要注意操作数的数量、参数类型和返回类型。重载的运算符可以是成员函数或非成员函数,具体取决于操作数的定义。运算符重载是面向对象编程的重要概念,C++重载了大部分的运算符,使得我们更加便捷地进行编程。