【C++】—— 类与对象(三)

9毫米的幻想 2024-08-08 09:05:09 阅读 58

【C++】—— 类与对象(三)

4、拷贝构造函数4.1、初识拷贝构造4.1.1、为什么要传引用4.1.2、引用尽量加上 const

4.2、深入拷贝构造4.2.1、为什么要自己实现拷贝构造4.2.2、传值返回先调用拷贝构造的原因4.2.3、躺赢的 MyQueue4.2.4、传值返回与引用返回

4.3、总结

5、取地址运算符重载5.1、const 成员函数5.2、取地址运算符重载

4、拷贝构造函数

4.1、初识拷贝构造

我们要先知道,拷贝构造是一个特殊的构造函数

拷贝构造的作用是:用一个自身类的对象来 初始化 当前的对象

拷贝构造基本特点

拷贝构造函数是构造函数的一个重载拷贝构造函数的<code>第一个参数必须是自身类类型的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。其他任何额外的参数都要有缺省值(默认值)C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成

我们先用

D

a

t

e

Date

Date 类型来感受一下拷贝构造:

在这里插入图片描述

运行结果:

在这里插入图片描述

拷贝构造的 调用方式 有两种:

一种是像上述代码一样类似于构造函数的调用方法:<code>Date d2(d1);另一种是类似赋值的调用:Date d2 = d1;

4.1.1、为什么要传引用

C++ 规定:对自定义类型传值传参要先调用拷贝构造

class date

{

public:

date(int year, int month, int day)

:_year(year)

, _month(month)

, _day(day)

{ }

date(const date& d)

{

*this = d;

}

private:

int _year;

int _month;

int _day;

};

void func(const date d)

{

cout << "heallo world" << endl;

}

int main()

{

date d1(2024, 1, 1);

func(d1);

return 0;

}

我们来调试来验证 一下:

在这里插入图片描述

对自定义类型,传值传参都会调用拷贝构造函数。

那这样的话,拷贝构造用传值传参会发生什么呢?

答: 无穷递归

我们通过图来理解一下:

在这里插入图片描述

而如果是传引用的话,

d

d

d 是

d

1

d1

d1 的别名,就不会形成新的拷贝构造。

所以,对于<code>自定义类型,传参都不建议使用传值传参。用传值传参还需要先调用拷贝构造,尤其是当实参特别大时,太费劲了

为什么传值传参要先调用拷贝构造呢?别急,我们学习完下一个知识点就来回答

4.1.2、引用尽量加上 const

使用引用传参,当函数体不需要改变外面的实参时,尽量都使用

c

o

n

s

t

const

const 引用!

因为加上

c

o

n

s

t

const

const 可以保护形参不被改变

假设,我要写一个判断逻辑,结果 “==” 不小心写成了 “=”。如果没加

c

o

n

s

t

const

const,那形参

d

d

d 就真被改了,我去给别人拷贝,结果我自己被改了,这合适吗?

Date(Date& d)

{

if (d._year = _year)

{

//···

}

}

而且,不使用

c

o

n

s

t

const

const 引用,当传的实参是只读性质时,会造成权限放大,编译不过去。使用了

c

o

n

s

t

const

const ,无论实参是不是只读,都能编过去

4.2、深入拷贝构造

拷贝构造的进阶特性:

若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造

传值返回会产生一个临时对象,产生临时对象会调用拷贝构造传引用返回,返回的是返回对象的别名(引用),没有产生拷贝

与前面的构造函数析构函数不同,如果我们没有显式实现,编译器默认生成的拷贝构造会对内置类型进行处理,会对内置类型进行值拷贝/浅拷贝。所谓值拷贝(也叫浅拷贝)就是一个字节一个字节进行拷贝,相当于

m

e

m

c

p

y

memcpy

memcpy函数的功能。

4.2.1、为什么要自己实现拷贝构造

那默认生成的拷贝构造不是挺好的吗,它都给你完成拷贝了,那我们还需要自己写吗?

我们来看下面这种情况:

现在,我们实现一个栈类,栈类的成员变量都是内置类型,看看编译器生成的拷贝构造能不能完成任务

typedef int STDataType;

class Stack

{

public :

Stack(int n = 4)

{

_a = (STDataType*)malloc(sizeof(STDataType) * n);

if (nullptr == _a)

{

perror("malloc申请空间失败");

return;

}

_capacity = n;

_top = 0;

}

~Stack()

{

cout << "~Stack()" << endl;

free(_a);

_a = nullptr;

_top = _capacity = 0;

}

private:

STDataType* _a;

size_t _capacity;

size_t _top;

};

int main()

{

Stack st1(10);

//两种都行

//Stack st2(st1);

Stack st2 = st1

return 0;

}

我们调试来看一下:

在这里插入图片描述

好像没有问题,

s

t

2

st2

st2 是完成了初始化的。

但我们继续往下运行,发现程序崩了

在这里插入图片描述

为什么呢?

因为编译器 仅仅完成了值拷贝/浅拷贝

对于_

t

o

p

top

top 和 _

c

a

p

a

c

i

t

y

capacity

capacity 来说他们并没有指向什么资源,只进行值拷贝/浅拷贝<code>没有问题

但对于 _

a

a

a 来说,虽然他是内置类型,但是 它指向一块开辟的空间

s

t

1

st1

st1 的 _

a

a

a 中存放的是指向的块空间的地址,将

s

t

1

st1

st1 中 _

a

a

a 的值拷贝

s

t

2

st2

st2 的 _a,此时

s

t

2

st2

st2 的 _

a

a

a 也存这那块空间的地址。也就是说

s

t

1

st1

st1 和

s

t

2

st2

st2 的 _

a

a

a 指向同一块空间

在这里插入图片描述

我们本来想的是他们指向不同的空间,空间中存放的是不同的数据(虽然现在没放数据)。虽然现在和我们想的有点不一样,但指向同一块空间也不至于让程序崩溃啊

答案出现在析构函数那。

m

a

i

n

main

main 函数结束,要销毁两个对象,<code>销毁对象前先调用自身析构函数。

后定义的先析构

s

t

2

st2

st2 调用自身析构,将 _

a

a

a 指向的空间释放,后

s

t

1

st1

st1 也调用析构,也要对 _

a

a

a 指向的空间进行释放,但此时空间已经被释放掉了,也就是说 同一块空间被释放了两次,自然程序崩溃了。

其实,不仅仅是析构两次,它的问题是很多的。比如:在函数内插入一个 1,再在函数外插入一个 2,2 会将 1 给覆盖

现在,我们自己给它加上拷贝构造函数

Stack(const Stack& st)

{

// 需要对_a指向资源创建同样⼤的资源再拷⻉值

_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);

if (nullptr == _a)

{

perror("malloc申请空间失败!!!");

return;

}

memcpy(_a, st._a, sizeof(STDataType) * st._top);

_top = st._top;

_capacity = st._capacity;

}

4.2.2、传值返回先调用拷贝构造的原因

现在,我们可以回答为什么传值传参要先调用拷贝构造了

想一想,传值传参传的是实参的拷贝,但这拷贝仅仅只是浅拷贝。像是拷贝上面的

s

t

a

c

k

stack

stack 类,要是在函数里面将 _

a

a

a 空间释放了,而你以为是传值传参,里面不影响外面,在外面再次将 _

a

a

a 指向的空间释放,程序就崩溃了。而 C语言只有浅拷贝,是很坑

所以 C++ 规定,传值传参要先调用默认构造函数,在默认构造中实现深拷贝( _

a

a

a 指向的空间也拷贝一份),这样就没这些问题啦

当然,对于自定义类型,函数传参是不建议用传值传参的。毕竟就算是正确拷贝,当拷贝的内容太大,也会占用很大空间,而且效率不高

自定义类型传参,尽可能用引用,如果不改变,尽可能加

c

o

n

s

t

const

const

4.2.3、躺赢的 MyQueue

当然,也不是所有有指向资源的类都需要自己写拷贝构造,比如

M

y

Q

u

e

u

e

MyQueue

MyQueue 类(用两个栈模拟实现队列)

// 两个Stack实现队列

class MyQueue

{

public :

private:

Stack pushst;

Stack popst;

};

int main()

{

MyQueue mq1;

MyQueue mq2 = mq1;

return 0;

}

在这里插入图片描述

m

q

2

mq2

mq2 是正常完成初始化的。

因为对自定义类型成员,编译器会调用它自身的拷贝构造

虽然MyQueue是躺赢,但这一切都是有

S

t

a

c

k

Stack

Stack 替他负重前行

4.2.4、传值返回与引用返回

传值返回会<code>产生一个临时对象调用拷贝构造,传引用返回,返回的是返回对象的别名(引用),没有产生拷贝

什么意思呢?我们来看看

Stack func()

{

Stack st;

return st;

}

int main()

{

Stack ret = func();

return 0;

}

像上述代码,调用

f

u

n

c

func

func 函数,使用传值返回。返回时,先调用拷贝构造

s

t

st

st 拷贝到一个临时对象,后再调用拷贝构造将临时对象中的值拷贝到 ret 中。(实际编译器会进行优化,不会真的执行两个拷贝,但从语法层面来讲是会执行两次拷贝的)

在这里插入图片描述

为了减少拷贝,我们会使用传引用返回,返回

s

t

st

st 的别名

<code>Stack& func()

{

Stack st;

return st;

}

但是这是不对的,st 出函数作用域就销毁了,此时返回的是野引用,类似于野指针的东西

使用引用返回,一定要确保返回的对象,当函数结束后还在,才能用引用返回

例如下面两种情况:

//情况一

Stack& func()

{

static Stack st;

return st;

}

//情况二

Stack& func(Stack& st)

{

st.push(1);

st.push(1);

st.push(1);

return st;

}

int main()

{

Stack st1;

Stack st2 = func(st1);

return 0;

}

4.3、总结

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

拷贝构造的特点:

拷贝构造函数是构造函数的一个重载拷贝构造函数的第一个参数必须是自身类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发 无穷递归 。若有其他参数必须给缺省值C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是 _a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型

S

t

a

c

k

Stack

Stack 成员,编译器自动生成的拷贝构造会调用

S

t

a

c

k

Stack

Stack 的拷贝构造,也不需要我们显式实现

M

y

Q

u

e

u

e

MyQueue

MyQueue 的拷贝构造。这里有一个小技巧,如果一个类显式实现了析构函数并释放了资源,那么他就需要写拷贝构造,否则不需要传值返回产生一个临时对象调用拷贝构造传值引用返回,返回的对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回

5、取地址运算符重载

5.1、const 成员函数

当类对象被

c

o

n

s

t

const

const 修饰会发生什么呢?

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;

};

int main()

{

const Date d(2024, 1, 1);

d.Print();

return 0;

}

d.Print();这句代码,我们知道到调用成员函数要传递地址给

t

h

i

s

this

this 指针的,这句代码实际上是d.Print(&d);

但是

d

d

d 的类型const Date,因此 &

d

d

d 传递的类型const Date*

t

h

i

s

this

this 指针 的类型Date* const this(这里

c

o

n

s

t

const

const 修饰的是指针本身,不是对象,可以直接忽略)

在这里插入图片描述

很显然,<code>const Date*传给Date*发生了权限放大,编译是无法通过的

这时要把

t

h

i

s

this

this指针 的类型变为const Date*

怎么做呢?要知道 C++ 规定,我们是不能在形参的位置显式写this指针的,这样我们就不能直接通过形参来修改

t

h

i

s

this

this 指针

为此,C++就给了一个偏方:在函数参数列表后面加

c

o

n

s

t

const

const

如:

void Print() const

{

cout << _year << "-" << _month << "-" << _day << endl;

}

那如果对象是非 const 还能不能调用

P

r

i

n

t

Print

Print 呢?

int main()

{

Date d1(2024, 7, 5);

d1.Print();

const Date d2(2024, 8, 5);

d2.Print();

return 0;

}

可以的,权限虽然不能放大,但是能缩小

所以,对于不用修改成员变量的成员函数,建议都加const,原因与引用加const类似

5.2、取地址运算符重载

取地址运算符重载分为普通取地址运算符重载

c

o

n

s

t

const

const 取地址运算符重载

Date* operator&()

{

return this;

}

const Date* operator&() const

{

return this;

}

注:两个都要写,因为普通对象返回Date*

c

o

n

s

t

const

const 对象要返回const Date*

取地址运算符重载也是一个默认成员函数,编译器会默认生成,往往不需要我们自己显式实现

除非你不想让别人取到该对象地址,那你就可以这样写:

Date* operator&()

{

return nullptr;

}

const Date* operator&() const

{

return nullptr;

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。