拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它用于创建一个新对象,这个新对象是另一个同类型对象的副本,只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

其语法形式为

1
2
类名 (const 类名& 对象名) 
{}

🤔为什么一定要引用传参呢?

当实参把对象传给形参的时候,如果不使用引用传参,那就是一个传值的过程,那么传值的时候要把实参传给一个中间变量,然后再由中间变量传给形参,这中间又涉及了对象的拷贝,然后以此类推,又要传值,又要拷贝,所以使用传值方式编译器直接报错,因为会引发无穷递归调用

🔥值得注意的是

  1. 已存在的对象初始化一个新对象时叫做拷贝已存在的对象初始化一个已存在的对象时叫做赋值
  2. const 关键字表示这个引用在函数内部不会被修改
  3. 拷贝构造函数构造函数的一个重载形式
  4. 拷贝构造函数的参数只有一个且必须是类类型对象的引用

默认拷贝构造函数

当程序员没有为一个类定义拷贝构造函数时,编译器会自动生成一个默认的拷贝构造函数

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <cstring>

// 定义一个简单的类,其内部有一个指针成员
class MyString
{
public:
char* str;

// 构造函数,用于初始化字符串
MyString(const char* s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
}

// 这里没有自定义拷贝构造函数,编译器会生成默认的拷贝构造函数(执行浅拷贝)
~MyString() {
// 析构函数,释放动态分配的内存
delete[] str;
}
};

int main() {
MyString s1("Hello");
// 使用默认的拷贝构造函数(浅拷贝)创建s2对象,相当于只是复制了指针str的值
MyString s2(s1);

std::cout << "s1的字符串内容: " << s1.str << std::endl;
std::cout << "s2的字符串内容: " << s2.str << std::endl;

// 修改s2所指向的字符串内容
strcpy(s2.str, "World");

std::cout << "修改后s1的字符串内容: " << s1.str << std::endl;
std::cout << "修改后s2的字符串内容: " << s2.str << std::endl;

return 0;
}

若未显式定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝默认拷贝构造函数会对类中的每个成员进行简单的复制,如果对于数据只是可读的话,直接浅拷贝即可;如果对于数据是读写的话,就需要深拷贝

深浅拷贝概念传送门:C++命运石之门代码抉择:C++入门(中)——3.2.4 引用的使用

🔥值得注意的是MyString s2(s1)MyString s2 = s1等价的

显式调用拷贝构造函数

显式调用拷贝构造函数是指在代码中通过明确的语法形式来触发拷贝构造函数的调用,而不是依赖编译器在某些隐式场景下自动调用

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}

因为这里涉及了资源空间分配的问题,如果只是调用默认拷贝构造函数的话,只能按值拷贝,那么两个对象将指向同一块空间,这明显是不合理的,所以这里我们就需要显式调用拷贝构造函数,进行深拷贝避免这个问题导致的程序崩溃

🔥总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

🚩拷贝构造常用场景
以下场景因为都涉及对象拷贝,所以要调用拷贝构造

  1. 使用已存在对象创建新对象
  2. 函数参数类型为类类型对象
  3. 函数返回值类型为类类型对象

在这里插入图片描述
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用

运算符重载函数

赋值运算符重载函数

赋值运算符重载函数是对类的赋值运算符(=)进行重新定义的成员函数非成员函数(通常作为成员函数实现),目的是让用户自定义类型(类对象)能够像内置数据类型那样使用赋值运算符进行赋值操作

其语法形式为

1
2
类型 operator运算符(参数) 
{}

🔥值得注意的是

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型 +不能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的this
  5. .* :: sizeof ?: .注意以上5个运算符不能重载,这个经常在笔试选择题中出现

默认赋值运算符重载函数

如果程序员没有为类自定义赋值运算符重载函数,编译器会自动生成一个默认的赋值运算符重载函数。这个默认函数会对类中的各个数据成员执行按成员的赋值操作,类似于默认拷贝构造函数执行的浅拷贝行为,以值的方式逐字节拷贝

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point 
{
public:
int x;
int y;
};

int main()
{
Point p1, p2;
p2 = p1;
return 0;
}

编译器生成的默认赋值运算符重载函数会把 p1.x 的值赋给 p2.x,把 p1.y 的值赋给 p2.y,因为 xy 都是基本数据类型成员

显式调用赋值运算符重载函数

🚩既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

Date(const Date& d)
{
_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;
}
private:
int _year;
int _month;
int _day;
};

返回类型、参数类型为引用提高传值效率this != &d 检测是否自己给自己赋值,返回*this 便于进行连续赋值操作

可以发现这里赋值运算符重载函数是作为成员函数存在

🧐为什么赋值运算符只能重载成类的成员函数不能重载成全局函数?

1
2
3
4
5
6
7
8
9
10
11
// 赋值运算符重载成全局函数,注意重载成全局函数时没有 this 指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}

赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数

🔥总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理必须要实现

const 取地址运算符重载函数

取地址运算符重载函数是对取地址运算符(&)进行重载的函数,在 C++ 中可以通过重载它来改变获取对象地址这一操作的默认行为。通常可以将其定义为类的成员函数,用于返回对象的地址或者经过自定义处理后的与地址相关的信息

取地址运算符重载的逻辑与赋值运算符重载类似,且不常用,这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!所以这里不重点讲述,感兴趣的读者可以自行了解

但我们可以思考几个问题

🚩const 对象是否可以调用非 const 成员函数?

一般情况下,const 对象不能调用非 const 成员函数。这是因为 const 对象被定义为其状态不能被修改,而非 const 成员函数可能会修改对象的数据成员

🚩非 const 对象是否可以调用 const 成员函数?

非 const 对象可以调用 const 成员函数。因为 const 成员函数承诺不会修改对象的数据成员,所以对于非 const 对象来说,调用这样的函数是安全的

🚩const 成员函数内是否可以调用其它的非 const 成员函数?
一般情况下,const 成员函数内不能直接调用非 const 成员函数。因为非 const 成员函数可能会修改对象的数据成员,这与 const 成员函数的承诺(不修改对象的数据成员)相冲突

🚩非 const 成员函数内是否可以调用其它的 const 成员函数?
非 const 成员函数内可以调用 const 成员函数。因为 const 成员函数不会修改对象的数据成员,所以在非 const 成员函数中调用它是完全合法的,并且这种调用方式在实际编程中很常见

🔥总结主要看被调用的函数有没有可能会修改对象的数据成员

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

在这里插入图片描述