C++ day20 用动态内存开发类 (三)函数返回对象,指向对象的指针
文章目录
- 返回对象
- 1 按引用传递,效率高(首选;当返回调用对象 或 返回作为参数传入的对象)
- 1.1 返回指向const对象的const引用
- 1.2 返回指向非const对象的非const引用
- 例子1:重载赋值运算符
- 例子2:重载<<运算符
- 2 按值传递,效率低,可有时候是唯一选择(次选,没法传引用才选)
- 2.1 返回const对象(防止a + b = c;这种赋值语句通过编译,尽量选这个)
- 2.2 返回非const对象(比如:重载算术运算符,不能返回const对象才选这个)
- 总结(优先级:const引用 > 非const引用 > const对象 > 非const对象)
- 指向对象的指针
- 对象指针用法集锦
- 外部对象,自动对象,动态对象以及他们的析构函数什么时候被调用
- 定位new运算符(1 容易不小心覆盖内存处原有内容;2 创建在指定内存的对象的析构函数在delete该指定内存时竟不被调用)
- 后创建先析构
- But, 为啥我对定位new创建的对象delete,没报错,还成功调用了他们的析构函数???
返回对象
好好掰扯掰扯当普通函数或者成员函数需要返回对象时的几种情况
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指向的就不是堆内存,而是栈内存,但是结果还是完美无瑕,???
至今仍是迷雾重重,不得其解
还没有评论,来说两句吧...