C++ day20 用动态内存开发类 (三)函数返回对象,指向对象的指针

返回对象

好好掰扯掰扯当普通函数或者成员函数需要返回对象时的几种情况

1 按引用传递,效率高(首选;当返回调用对象 或 返回作为参数传入的对象)

返回对象要调用复制构造函数,返回引用不需要,所以返回引用需要做的工作更少,效率自然更高

但是返回引用要求引用指向的对象在调用函数结束后还存在。这是很简单的道理。

1.1 返回指向const对象的const引用

因为参数本身是const引用,当然必须把返回值声明为const引用,否则报错。很简单。不能把const给非const

例子:
在这里插入图片描述
在这里插入图片描述

1.2 返回指向非const对象的非const引用

例子1:重载赋值运算符
StringBad & StringBad::operator=(const StringBad & st)
{
	//首先禁止源和目标对象是同一个对象的情况,用地址判断
	if (this == &st)
		return *this;
	strLen = st.strLen;
	delete [] str;
	str = new char[strLen + 1];
	std::strcpy(str, st.str);
	return *this;
}

返回的是指向调用对象(一个非const对象)的引用、

例子2:重载<<运算符

istream类的对象在函数执行结束后会被改变,所以是非const的

std::istream & operator>>(std::istream & is, StringBad & st)
{
	delete [] st.str;//释放目标对象的指针成员指向的地址,否则会内存泄漏
	char temp[StringBad::CINLIM];//忘记StringBad::
	if (is.get(temp, StringBad::CINLIM))
	    st = temp;
	else
        is.clear();//清除错误标记
    eatline();//清空输入缓存
	return is;
}

2 按值传递,效率低,可有时候是唯一选择(次选,没法传引用才选)

返回对象本身要调用复制构造函数,返回引用不需要,但是有时候必须要接受返回对象的开销,因为别无选择。比如当需要返回函数中的局部对象时,只能按值返回,否则如果返回引用,函数结束后局部对象就释放了,引用指向nowhere。

一般,重载算术运算符必须返回对象本身。因为必须计算,得用一个临时对象保存计算后的值。

2.1 返回const对象(防止a + b = c;这种赋值语句通过编译,尽量选这个)

以前说过一次,当时觉得好牛逼,现在竟然全忘了

用矢量类举例,这里只写主程序,类声明和类定义代码看这篇文章

//main.cpp
#include <iostream>
#include "vector.h"

int main()
{
    using std::cout;
    using std::endl;
	VECTOR::Vector v1 = VECTOR::Vector();//默认构造函数
	VECTOR::Vector v2 = VECTOR::Vector(2.0, 0);//RECT模式
	VECTOR::Vector v3 = VECTOR::Vector(0.0, 2.0, VECTOR::Vector::RECT);//POL模式
	VECTOR::Vector v4(0.0, 1.0);

	cout << "v1: " << v1 << endl;
    cout << "v2: " << v2 << endl;
    cout << "v3: " << v3 << endl;
    cout << "v4: " << v4 << endl;

    VECTOR::Vector v5 = v1 + v2;
    cout << endl;
    cout << "v1: " << v1 << endl;
    cout << "v2: " << v2 << endl;
    cout << "v5: " << v5 << endl;

    v3 + v4 = v5;//这么奇怪的代码,顺利通过了编译
    cout << endl;
    cout << "v3: " << v3 << endl;
    cout << "v4: " << v4 << endl;
    cout << "v5: " << v5 << endl;
    cout << "(v3 + v4 = v5).magval() = " << (v3 + v4 = v5).magval() << endl;
    return 0;
}

v3 + v4 = v5;
编译器会先计算v3 + v4,得到的结果存储在一个临时对象中,如果赋值表达式是v5 = v3 + v4; 那编译器就把匿名临时对象复制给v5(调用编译器自定义的复制构造函数,浅复制)。

现在表达式是 v3 + v4 = v5; 所以v5被复制给这个匿名临时对象,而临时对象的计算结果则丢了。(v3 + v4 = v5)就是这个临时对象。所以(v3 + v4 = v5).magval()是临时对象的幅值,即v5的幅值了

v1: (x, y) = (0, 0)
v2: (x, y) = (2, 0)
v3: (x, y) = (0, 2)
v4: (x, y) = (0, 1)

v1: (x, y) = (0, 0)
v2: (x, y) = (2, 0)
v5: (x, y) = (2, 0)

v3: (x, y) = (0, 2)
v4: (x, y) = (0, 1)
v5: (x, y) = (2, 0)
(v3 + v4 = v5).magval() = 2

这是因为重载的加法运算符的返回类型是非const对象,下面会说

Vector Vector::operator+(const Vector & v) const
{
	//操作数顺序无法反转
	return Vector(x + v.x, y + v.y);//直接按矩形模式计算,用构造函数生成一个对象返回,注意这里省略了this指针
}

我改为返回const对象后(原型和定义前面都要加const哈),这句奇怪赋值立马藏不住了,编译器立刻报错

在这里插入图片描述

不是说你会编写那么奇怪的代码,而是怕你不小心把==写为=,从而意外得到这种表达式
在这里插入图片描述

2.2 返回非const对象(比如:重载算术运算符,不能返回const对象才选这个)

一般,重载算术运算符必须返回对象本身。因为必须计算,得用一个临时对象保存计算后的值。当然是非const的。
在这里插入图片描述
在这里插入图片描述

其他示例,之前矢量类的三个算术运算符,都用构造函数新建一个对象然后返回,返回时会调用复制构造函数

Vector Vector::operator-(const Vector & v) const
{
	//操作数顺序无法反转
	return 	Vector(x - v.x, y - v.y);
}

Vector Vector::operator-() const//方向取反,负号运算符
{
	return Vector(-x, -y);//简洁版,但是这需要生成一个临时对象,然后复制出去,有点低效
}

Vector Vector::operator*(double n) const
{
	//操作数顺序无法反转
	return Vector(x * n, y * n);//简洁版
}

总结(优先级:const引用 > 非const引用 > const对象 > 非const对象)

指向对象的指针

类声明头文件和类定义方法文件在这里

其中使用下标找最短字符串和字符最前字符串,但本文使用**指向对象的指针(动态对象)**实现,只需对原来主程序做一丢丢改动,很简单。但是原理概念还是要好好弄明白的。

//main.cpp
#include <iostream>
#include "StringBad.h"
void eatline();
typedef unsigned int uint;
const uint ARSIZE = 10;

int main()
{
	{
		using std::cout;
		using std::cin;

		cout << "starting an inner block\n";
		StringBad sayings[ARSIZE];//默认构造函数
		cout << "Enter up to  " << ARSIZE << " sayings (empty line to quit):\n>>";//输入提示符,很棒棒
		uint i;
		for (i = 0; i < ARSIZE; ++i)
		{
			if (!(cin >> sayings[i]) || sayings[i][0] == '\0')
				break;
		}

		if (i > 0)
		{
			cout << "Here is the " << i << " sayings:\n";


			StringBad * shortest = &sayings[0];//指向第一个对象,用复制构造函数初始化shortest指针指向一个已有的对象
			StringBad * first = new StringBad(sayings[0]);//用new给对象分配内存初始化匿名对象,并初始化为已有对象

			uint j;
			for (j = 0; j < i; ++j)
            {
                cout << sayings[j][0] << ": " << sayings[j] << '\n';
                if (sayings[j] < *first)
                    first = &sayings[j];
                if (sayings[j].length() < (*shortest).length())//我又错误地写成sayings[j].strLen < sayings[shortest].strLen
                    shortest = &sayings[j];
            }

            cout << "The shortest saying: " << *shortest << '\n';
            cout << "The first saying alphabetically: " << *first << '\n';
            
            delete first;//记得删除
		}
		else
			cout << "No sayings entered!\n";


	}
	std::cout << "Exiting the main() function\n";

	return 0;
}
starting an inner block
1: default object created!
2: default object created!
3: default object created!
4: default object created!
5: default object created!
6: default object created!
7: default object created!
8: default object created!
9: default object created!
10: default object created!
Enter up to  10 sayings (empty line to quit):
>>dfsaf
Enter friend function: operator>>
fdgfe
Enter friend function: operator>>
dfd
Enter friend function: operator>>
hj
Enter friend function: operator>>
kuib
Enter friend function: operator>>
kl
Enter friend function: operator>>
bnmsdfsvfhrtyhser
Enter friend function: operator>>
23
Enter friend function: operator>>

Enter friend function: operator>>
Here is the 8 sayings:
d: dfsaf
f: fdgfe
d: dfd
h: hj
k: kuib
k: kl
b: bnmsdfsvfhrtyhser
2: 23
The shortest saying: hj
The first saying alphabetically: 23
"" object deleted! 9 objects left!
"" object deleted! 8 objects left!
"23" object deleted! 7 objects left!
"bnmsdfsvfhrtyhser" object deleted! 6 objects left!
"kl" object deleted! 5 objects left!
"kuib" object deleted! 4 objects left!
"hj" object deleted! 3 objects left!
"dfd" object deleted! 2 objects left!
"fdgfe" object deleted! 1 objects left!
"dfsaf" object deleted! 0 objects left!
Exiting the main() function

对象指针用法集锦

StringBad * glamour;//声明指向类对象的指针,但不创建对象
StringBad * first = &sayings[0];//调用复制构造函数创建匿名动态对象
StringBad * favourite = new StringBad;//调用默认构造函数创建匿名动态对象
StringBad * gleep = new StringBad(sayings[4]);//调用复制构造函数创建匿名动态对象,原型:StringBad(const StringBad &);
StringBad * glop = new StringBad("my my my");//调用原型为StringBad(const char *);的构造函数

在这里插入图片描述

外部对象,自动对象,动态对象以及他们的析构函数什么时候被调用

就是把存储期,作用域的知识用在对象身上

//main.cpp
#include "StringBad.h"
 StringBad ext;//外部对象,external object,默认构造函数

 int main()
 {
     StringBad * pd = new StringBad;//匿名动态对象,dynamic object.用默认构造函数
     {
        StringBad aut;//automatic object,默认构造函数
     }//调用aut的默认析构
     delete pd;//调用动态对象*pd的默认析构函数

     return 0;//main函数结束之前调用静态外部对象ext的默认析构函数
 }
1: default object created!
2: default object created!
3: default object created!
"" object deleted! 2 objects left!
"" object deleted! 1 objects left!
"" object deleted! 0 objects left!

在这里插入图片描述

定位new运算符(1 容易不小心覆盖内存处原有内容;2 创建在指定内存的对象的析构函数在delete该指定内存时竟不被调用)

定位new运算符可以指定分配内存的位置,本示例将他和常规new运算符对比,发现2个问题,如题

之前说过定位new,第一个问题我们早就知道,也可以解决;但是现在进一步加深,说说析构函数为啥不被调用,并且我们该怎么确保析构函数被调用——显式调用析构。

//main.cpp
#include <iostream>
#include <string>
#include <new>
typedef unsigned int uint;
const uint BUF = 512;

class JustTesting{
private:
	std::string word;
	uint num;
public:
	//参数全部是默认参数的内联构造函数,所以编译器无需自己生成默认构造函数了
	JustTesting(const std::string & w = "JustTesting", uint n = 0)
	{
		word = w;
		num = n;
		std::cout << w << " constructed!\n";
	}
	~JustTesting()
	{
		std::cout << word << " destroyed!\n";
	}
	void show() const
	{
		std::cout << word << ", " << num << '\n';
	}
};

int main()
{
    char * buffer = new char[BUF];

    JustTesting * p1, * p2;
    p1 = new (buffer) JustTesting;
    p2 = new JustTesting("Heap1", 20);
    std::cout << "Memory addresses:\n";
    std::cout << (void *)buffer << '\t' << p1 << '\t' << p2 << '\n';
    std::cout << "Contents:\n";
    std::cout << p1 << ": ";
    p1->JustTesting::show();
    std::cout << p2 << ": ";
    p2->JustTesting::show();

    std::cout << '\n';
    JustTesting * p3, * p4;
    p3 = new (buffer) JustTesting("Bad idea", 18);
    p4 = new JustTesting("Heap2", 40);
    std::cout << "Memory addresses:\n";
    std::cout << (void *)buffer << '\t' << p3 << '\t' << p4 << '\n';
    std::cout << "Contents:\n";
    std::cout << p3 << ": ";
    p3->JustTesting::show();
    std::cout << p4 << ": ";
    p4->JustTesting::show();

    delete p2;
    delete p4;
    delete [] buffer;//释放了buffer指向的内存块,但是却并没有为该内存块的对象们比如p4调用析构函数

	return 0;
}

可以看到,p3对象的内容把p1的覆盖了,这是因为定位new两次分配同一个位置

JustTesting constructed!
Heap1 constructed!
Memory addresses:
0x81a638        0x81a638        0x816c50
Contents:
0x81a638: JustTesting, 0
0x816c50: Heap1, 20

Bad idea constructed!
Heap2 constructed!
Memory addresses:
0x81a638        0x81a638        0x81a478
Contents:
0x81a638: Bad idea, 18
0x81a478: Heap2, 40
Heap1 destroyed!
Heap2 destroyed!

要想不覆盖,就自己改一下定位new运算符后面圆括号的指针,加个偏移量,偏移量的大小可以用sizeof运算符计算:

p3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad idea", 18);//sizeof运算符可以用于自己定义的类诶!!
JustTesting constructed!
Heap1 constructed!
Memory addresses:
0xaea638        0xaea638        0xae6c50
Contents:
0xaea638: JustTesting, 0
0xae6c50: Heap1, 20

Bad idea constructed!
Heap2 constructed!
Memory addresses:
0xaea638        0xaea654        0xaea478
Contents:
0xaea654: Bad idea, 18
0xaea478: Heap2, 40
Heap1 destroyed!
Heap2 destroyed!

那现在聚焦于第二个问题,为啥对象p3的析构函数没被调用,p1就不管了,毕竟被p3覆盖了,已经没了

这是因为和delete搭配的是常规new,不是定位new,所以程序中我们没有delete p2, delete p4,这会导致运行阶段错误(可是后面我试了,并没有任何错误???)

正确的办法是显式在程序中自己调用析构函数

p1->~JustTesting();
delete p2;
p3->~JustTesting();
delete p4;

成功

JustTesting destroyed!
Heap1 destroyed!
Bad idea destroyed!
Heap2 destroyed!

后创建先析构

但是我这么做还不够正确,必须要把后创建的对象先析构(后创建先析构),因为后创建的对象有可能会依赖先创建的一些对象,如果按照先创建先析构的顺序,也许会出现错误(这里后来者没有依赖于先来者,所以没出错)

delete p4;
p3->~JustTesting();
delete p2;
p1->~JustTesting();
Heap2 destroyed!
Bad idea destroyed!
Heap1 destroyed!
JustTesting destroyed!

But, 为啥我对定位new创建的对象delete,没报错,还成功调用了他们的析构函数???

生活处处有惊喜

本来想写成这样看看报什么错误,结果没报错没警告,也没有运行时异常,一切完美,甚至还调用了定位new创建的对象的析构函数!!!

???

一脸懵逼

//main.cpp
#include <iostream>
#include <string>
#include <new>
typedef unsigned int uint;
const uint BUF = 512;

class JustTesting{
private:
	std::string word;
	uint num;
public:
	//参数全部是默认参数的内联构造函数,所以编译器无需自己生成默认构造函数了
	JustTesting(const std::string & w = "JustTesting", uint n = 0)
	{
		word = w;
		num = n;
		std::cout << w << " constructed!\n";
	}
	~JustTesting()
	{
		std::cout << word << " destroyed!\n";
	}
	void show() const
	{
		std::cout << word << ", " << num << '\n';
	}
};

int main()
{
    char * buffer = new char[BUF];

    JustTesting * p1, * p2;
    p1 = new (buffer) JustTesting;
    p2 = new JustTesting("Heap1", 20);
    std::cout << "Memory addresses:\n";
    std::cout << (void *)buffer << '\t' << p1 << '\t' << p2 << '\n';
    std::cout << "Contents:\n";
    std::cout << p1 << ": ";
    p1->JustTesting::show();
    std::cout << p2 << ": ";
    p2->JustTesting::show();

    std::cout << '\n';
    JustTesting * p3, * p4;
    p3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad idea", 18);
    p4 = new JustTesting("Heap2", 40);
    std::cout << "Memory addresses:\n";
    std::cout << (void *)buffer << '\t' << p3 << '\t' << p4 << '\n';
    std::cout << "Contents:\n";
    std::cout << p3 << ": ";
    p3->JustTesting::show();
    std::cout << p4 << ": ";
    p4->JustTesting::show();
    std::cout << '\n';
    
    //后创建先析构
	delete p4;
	delete p3;
	delete p2;
    delete p1;
    delete [] buffer;

	return 0;
}
JustTesting constructed!
Heap1 constructed!
Memory addresses:
0x76a638        0x76a638        0x766c50
Contents:
0x76a638: JustTesting, 0
0x766c50: Heap1, 20

Bad idea constructed!
Heap2 constructed!
Memory addresses:
0x76a638        0x76a654        0x76a478
Contents:
0x76a654: Bad idea, 18
0x76a478: Heap2, 40

Heap2 destroyed!
Bad idea destroyed!
Heap1 destroyed!
JustTesting destroyed!

我猜想大概是因为buffer的地址也是new分配的,所以也是堆内存,所以可以和delete搭配,没有出错。但是这个猜想立刻被证明是错误的:

因为我把char * buffer = new char[BUF];换为

char temp[BUF];
char * buffer = temp;

这样buffer指向的就不是堆内存,而是栈内存,但是结果还是完美无瑕,???

至今仍是迷雾重重,不得其解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值