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

我不是女神ヾ 2023-07-16 15:59 41阅读 0赞

文章目录

  • 返回对象
    • 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:重载赋值运算符

  1. StringBad & StringBad::operator=(const StringBad & st)
  2. {
  3. //首先禁止源和目标对象是同一个对象的情况,用地址判断
  4. if (this == &st)
  5. return *this;
  6. strLen = st.strLen;
  7. delete [] str;
  8. str = new char[strLen + 1];
  9. std::strcpy(str, st.str);
  10. return *this;
  11. }

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

例子2:重载<<运算符

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

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

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

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

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

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

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

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

  1. //main.cpp
  2. #include <iostream>
  3. #include "vector.h"
  4. int main()
  5. {
  6. using std::cout;
  7. using std::endl;
  8. VECTOR::Vector v1 = VECTOR::Vector();//默认构造函数
  9. VECTOR::Vector v2 = VECTOR::Vector(2.0, 0);//RECT模式
  10. VECTOR::Vector v3 = VECTOR::Vector(0.0, 2.0, VECTOR::Vector::RECT);//POL模式
  11. VECTOR::Vector v4(0.0, 1.0);
  12. cout << "v1: " << v1 << endl;
  13. cout << "v2: " << v2 << endl;
  14. cout << "v3: " << v3 << endl;
  15. cout << "v4: " << v4 << endl;
  16. VECTOR::Vector v5 = v1 + v2;
  17. cout << endl;
  18. cout << "v1: " << v1 << endl;
  19. cout << "v2: " << v2 << endl;
  20. cout << "v5: " << v5 << endl;
  21. v3 + v4 = v5;//这么奇怪的代码,顺利通过了编译
  22. cout << endl;
  23. cout << "v3: " << v3 << endl;
  24. cout << "v4: " << v4 << endl;
  25. cout << "v5: " << v5 << endl;
  26. cout << "(v3 + v4 = v5).magval() = " << (v3 + v4 = v5).magval() << endl;
  27. return 0;
  28. }

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

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

  1. v1: (x, y) = (0, 0)
  2. v2: (x, y) = (2, 0)
  3. v3: (x, y) = (0, 2)
  4. v4: (x, y) = (0, 1)
  5. v1: (x, y) = (0, 0)
  6. v2: (x, y) = (2, 0)
  7. v5: (x, y) = (2, 0)
  8. v3: (x, y) = (0, 2)
  9. v4: (x, y) = (0, 1)
  10. v5: (x, y) = (2, 0)
  11. (v3 + v4 = v5).magval() = 2

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

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

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

在这里插入图片描述

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

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

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

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

  1. Vector Vector::operator-(const Vector & v) const
  2. {
  3. //操作数顺序无法反转
  4. return Vector(x - v.x, y - v.y);
  5. }
  6. Vector Vector::operator-() const//方向取反,负号运算符
  7. {
  8. return Vector(-x, -y);//简洁版,但是这需要生成一个临时对象,然后复制出去,有点低效
  9. }
  10. Vector Vector::operator*(double n) const
  11. {
  12. //操作数顺序无法反转
  13. return Vector(x * n, y * n);//简洁版
  14. }

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

指向对象的指针

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

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

  1. //main.cpp
  2. #include <iostream>
  3. #include "StringBad.h"
  4. void eatline();
  5. typedef unsigned int uint;
  6. const uint ARSIZE = 10;
  7. int main()
  8. {
  9. {
  10. using std::cout;
  11. using std::cin;
  12. cout << "starting an inner block\n";
  13. StringBad sayings[ARSIZE];//默认构造函数
  14. cout << "Enter up to " << ARSIZE << " sayings (empty line to quit):\n>>";//输入提示符,很棒棒
  15. uint i;
  16. for (i = 0; i < ARSIZE; ++i)
  17. {
  18. if (!(cin >> sayings[i]) || sayings[i][0] == '\0')
  19. break;
  20. }
  21. if (i > 0)
  22. {
  23. cout << "Here is the " << i << " sayings:\n";
  24. StringBad * shortest = &sayings[0];//指向第一个对象,用复制构造函数初始化shortest指针指向一个已有的对象
  25. StringBad * first = new StringBad(sayings[0]);//用new给对象分配内存初始化匿名对象,并初始化为已有对象
  26. uint j;
  27. for (j = 0; j < i; ++j)
  28. {
  29. cout << sayings[j][0] << ": " << sayings[j] << '\n';
  30. if (sayings[j] < *first)
  31. first = &sayings[j];
  32. if (sayings[j].length() < (*shortest).length())//我又错误地写成sayings[j].strLen < sayings[shortest].strLen
  33. shortest = &sayings[j];
  34. }
  35. cout << "The shortest saying: " << *shortest << '\n';
  36. cout << "The first saying alphabetically: " << *first << '\n';
  37. delete first;//记得删除
  38. }
  39. else
  40. cout << "No sayings entered!\n";
  41. }
  42. std::cout << "Exiting the main() function\n";
  43. return 0;
  44. }
  45. starting an inner block
  46. 1: default object created!
  47. 2: default object created!
  48. 3: default object created!
  49. 4: default object created!
  50. 5: default object created!
  51. 6: default object created!
  52. 7: default object created!
  53. 8: default object created!
  54. 9: default object created!
  55. 10: default object created!
  56. Enter up to 10 sayings (empty line to quit):
  57. >>dfsaf
  58. Enter friend function: operator>>
  59. fdgfe
  60. Enter friend function: operator>>
  61. dfd
  62. Enter friend function: operator>>
  63. hj
  64. Enter friend function: operator>>
  65. kuib
  66. Enter friend function: operator>>
  67. kl
  68. Enter friend function: operator>>
  69. bnmsdfsvfhrtyhser
  70. Enter friend function: operator>>
  71. 23
  72. Enter friend function: operator>>
  73. Enter friend function: operator>>
  74. Here is the 8 sayings:
  75. d: dfsaf
  76. f: fdgfe
  77. d: dfd
  78. h: hj
  79. k: kuib
  80. k: kl
  81. b: bnmsdfsvfhrtyhser
  82. 2: 23
  83. The shortest saying: hj
  84. The first saying alphabetically: 23
  85. "" object deleted! 9 objects left!
  86. "" object deleted! 8 objects left!
  87. "23" object deleted! 7 objects left!
  88. "bnmsdfsvfhrtyhser" object deleted! 6 objects left!
  89. "kl" object deleted! 5 objects left!
  90. "kuib" object deleted! 4 objects left!
  91. "hj" object deleted! 3 objects left!
  92. "dfd" object deleted! 2 objects left!
  93. "fdgfe" object deleted! 1 objects left!
  94. "dfsaf" object deleted! 0 objects left!
  95. Exiting the main() function

对象指针用法集锦

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

在这里插入图片描述

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

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

  1. //main.cpp
  2. #include "StringBad.h"
  3. StringBad ext;//外部对象,external object,默认构造函数
  4. int main()
  5. {
  6. StringBad * pd = new StringBad;//匿名动态对象,dynamic object.用默认构造函数
  7. {
  8. StringBad aut;//automatic object,默认构造函数
  9. }//调用aut的默认析构
  10. delete pd;//调用动态对象*pd的默认析构函数
  11. return 0;//main函数结束之前调用静态外部对象ext的默认析构函数
  12. }
  13. 1: default object created!
  14. 2: default object created!
  15. 3: default object created!
  16. "" object deleted! 2 objects left!
  17. "" object deleted! 1 objects left!
  18. "" object deleted! 0 objects left!

在这里插入图片描述

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

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

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

  1. //main.cpp
  2. #include <iostream>
  3. #include <string>
  4. #include <new>
  5. typedef unsigned int uint;
  6. const uint BUF = 512;
  7. class JustTesting{
  8. private:
  9. std::string word;
  10. uint num;
  11. public:
  12. //参数全部是默认参数的内联构造函数,所以编译器无需自己生成默认构造函数了
  13. JustTesting(const std::string & w = "JustTesting", uint n = 0)
  14. {
  15. word = w;
  16. num = n;
  17. std::cout << w << " constructed!\n";
  18. }
  19. ~JustTesting()
  20. {
  21. std::cout << word << " destroyed!\n";
  22. }
  23. void show() const
  24. {
  25. std::cout << word << ", " << num << '\n';
  26. }
  27. };
  28. int main()
  29. {
  30. char * buffer = new char[BUF];
  31. JustTesting * p1, * p2;
  32. p1 = new (buffer) JustTesting;
  33. p2 = new JustTesting("Heap1", 20);
  34. std::cout << "Memory addresses:\n";
  35. std::cout << (void *)buffer << '\t' << p1 << '\t' << p2 << '\n';
  36. std::cout << "Contents:\n";
  37. std::cout << p1 << ": ";
  38. p1->JustTesting::show();
  39. std::cout << p2 << ": ";
  40. p2->JustTesting::show();
  41. std::cout << '\n';
  42. JustTesting * p3, * p4;
  43. p3 = new (buffer) JustTesting("Bad idea", 18);
  44. p4 = new JustTesting("Heap2", 40);
  45. std::cout << "Memory addresses:\n";
  46. std::cout << (void *)buffer << '\t' << p3 << '\t' << p4 << '\n';
  47. std::cout << "Contents:\n";
  48. std::cout << p3 << ": ";
  49. p3->JustTesting::show();
  50. std::cout << p4 << ": ";
  51. p4->JustTesting::show();
  52. delete p2;
  53. delete p4;
  54. delete [] buffer;//释放了buffer指向的内存块,但是却并没有为该内存块的对象们比如p4调用析构函数
  55. return 0;
  56. }

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

  1. JustTesting constructed!
  2. Heap1 constructed!
  3. Memory addresses:
  4. 0x81a638 0x81a638 0x816c50
  5. Contents:
  6. 0x81a638: JustTesting, 0
  7. 0x816c50: Heap1, 20
  8. Bad idea constructed!
  9. Heap2 constructed!
  10. Memory addresses:
  11. 0x81a638 0x81a638 0x81a478
  12. Contents:
  13. 0x81a638: Bad idea, 18
  14. 0x81a478: Heap2, 40
  15. Heap1 destroyed!
  16. Heap2 destroyed!

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

  1. p3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad idea", 18);//sizeof运算符可以用于自己定义的类诶!!
  2. JustTesting constructed!
  3. Heap1 constructed!
  4. Memory addresses:
  5. 0xaea638 0xaea638 0xae6c50
  6. Contents:
  7. 0xaea638: JustTesting, 0
  8. 0xae6c50: Heap1, 20
  9. Bad idea constructed!
  10. Heap2 constructed!
  11. Memory addresses:
  12. 0xaea638 0xaea654 0xaea478
  13. Contents:
  14. 0xaea654: Bad idea, 18
  15. 0xaea478: Heap2, 40
  16. Heap1 destroyed!
  17. Heap2 destroyed!

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

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

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

  1. p1->~JustTesting();
  2. delete p2;
  3. p3->~JustTesting();
  4. delete p4;

成功

  1. JustTesting destroyed!
  2. Heap1 destroyed!
  3. Bad idea destroyed!
  4. Heap2 destroyed!

后创建先析构

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

  1. delete p4;
  2. p3->~JustTesting();
  3. delete p2;
  4. p1->~JustTesting();
  5. Heap2 destroyed!
  6. Bad idea destroyed!
  7. Heap1 destroyed!
  8. JustTesting destroyed!

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

生活处处有惊喜

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

???

一脸懵逼

  1. //main.cpp
  2. #include <iostream>
  3. #include <string>
  4. #include <new>
  5. typedef unsigned int uint;
  6. const uint BUF = 512;
  7. class JustTesting{
  8. private:
  9. std::string word;
  10. uint num;
  11. public:
  12. //参数全部是默认参数的内联构造函数,所以编译器无需自己生成默认构造函数了
  13. JustTesting(const std::string & w = "JustTesting", uint n = 0)
  14. {
  15. word = w;
  16. num = n;
  17. std::cout << w << " constructed!\n";
  18. }
  19. ~JustTesting()
  20. {
  21. std::cout << word << " destroyed!\n";
  22. }
  23. void show() const
  24. {
  25. std::cout << word << ", " << num << '\n';
  26. }
  27. };
  28. int main()
  29. {
  30. char * buffer = new char[BUF];
  31. JustTesting * p1, * p2;
  32. p1 = new (buffer) JustTesting;
  33. p2 = new JustTesting("Heap1", 20);
  34. std::cout << "Memory addresses:\n";
  35. std::cout << (void *)buffer << '\t' << p1 << '\t' << p2 << '\n';
  36. std::cout << "Contents:\n";
  37. std::cout << p1 << ": ";
  38. p1->JustTesting::show();
  39. std::cout << p2 << ": ";
  40. p2->JustTesting::show();
  41. std::cout << '\n';
  42. JustTesting * p3, * p4;
  43. p3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad idea", 18);
  44. p4 = new JustTesting("Heap2", 40);
  45. std::cout << "Memory addresses:\n";
  46. std::cout << (void *)buffer << '\t' << p3 << '\t' << p4 << '\n';
  47. std::cout << "Contents:\n";
  48. std::cout << p3 << ": ";
  49. p3->JustTesting::show();
  50. std::cout << p4 << ": ";
  51. p4->JustTesting::show();
  52. std::cout << '\n';
  53. //后创建先析构
  54. delete p4;
  55. delete p3;
  56. delete p2;
  57. delete p1;
  58. delete [] buffer;
  59. return 0;
  60. }
  61. JustTesting constructed!
  62. Heap1 constructed!
  63. Memory addresses:
  64. 0x76a638 0x76a638 0x766c50
  65. Contents:
  66. 0x76a638: JustTesting, 0
  67. 0x766c50: Heap1, 20
  68. Bad idea constructed!
  69. Heap2 constructed!
  70. Memory addresses:
  71. 0x76a638 0x76a654 0x76a478
  72. Contents:
  73. 0x76a654: Bad idea, 18
  74. 0x76a478: Heap2, 40
  75. Heap2 destroyed!
  76. Bad idea destroyed!
  77. Heap1 destroyed!
  78. JustTesting destroyed!

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

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

  1. char temp[BUF];
  2. char * buffer = temp;

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

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

发表评论

表情:
评论列表 (有 0 条评论,41人围观)

还没有评论,来说两句吧...

相关阅读

    相关 指向成员/函数指针

    C++扩展了指针在类中的使用,使其可以指向类成员,这种行为是类层面的,而不是对象层面的。 指向类成员/函数的指针的本质并不是取地址.而是利用了对象地址的偏移量 我们创建了一