经典问题解析三(三十)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典问题解析三(三十)相关的知识,希望对你有一定的参考价值。
当我们学习了 C++ 一段时间后,便会产生好多问题。我们今天就几个问题来讨论下,一是关于赋值的疑问,那便是什么时候需要重载赋值操作符?编译器是否提供默认的赋值操作?编译器为每个类默认的重载了赋值操作符,默认的赋值操作符仅完成浅拷贝。当需要进行深拷贝时必须重载赋值操作符,赋值操作符与拷贝构造函数有相同的存在意义。
下来我们以代码为例来进行分析
#include <iostream> #include <string> using namespace std; class Test { int* m_pointer; public: Test() { m_pointer = NULL; } Test(int i) { m_pointer = new int(i); } void print() { cout << "m_pointer = " << hex << m_pointer << endl; } ~Test() { delete m_pointer; } }; int main() { Test t1 = 1; Test t2; t2 = t1; t1.print(); t2.print(); return 0; }
我们看到在类 Test 中实现了在构造函数中申请内存,在析构函数在进行 delete。print 函数是用来进行显示指针的地址的,在 main 函数中将 t1 赋值给 t2。我们来看看编译结果
我们看到在运行的时候发生段错误了,那我们来分析下这个问题。我们在程序中并没有提供拷贝构造函数,也就是说用的是编译器提供的默认的拷贝构造函数。这时它就只能进行浅拷贝,仅仅只时对象的物理状态相同,逻辑并不相同。如下图所示
由打印结果我们可以看出来,t1 和 t2 对象内的指针地址相同,如果进行 t2 对象的析构时会发生去释放一个野指针。这便是产生段错误的真正原因了。那么一般性的规则是:在重载复制操作符时,必然要进行深拷贝!!!
下来我们在上面程序的类中添加一个拷贝构造函数和重载赋值操作符,函数的程序如下
Test(const Test& obj) { m_pointer = new int(*obj.m_pointer); } Test& operator = (const Test& obj) { if( this != &obj ) { delete m_pointer; m_pointer = new int(*obj.m_pointer); } return *this; }
我们再来看看编译效果呢
完美运行结束。下来我们将之前的数组类进行下优化,如果进行赋值操作符的重载,就没必要进行拷贝构造函数了,因为它们存在的意义是相同的。
IntArray.h 源码
#ifndef _INTARRAY_H_ #define _INTARRAY_H_ class IntArray { private: int m_length; int* m_pointer; IntArray(int len); bool construct(); public: static IntArray* NewInstance(int length); int length(); bool get(int index, int& value); bool set(int index, int value); int& operator [] (int index); IntArray& operator = (const IntArray& obj); IntArray& self(); ~IntArray(); }; #endif
IntArray.cpp 源码
#include "IntArray.h" IntArray::IntArray(int len) { m_length = len; } bool IntArray::construct() { bool ret = true; m_pointer = new int[m_length]; if( m_pointer ) { for(int i=0; i<m_length; i++) { m_pointer[i] = 0; } } else { ret = false; } return ret; } IntArray* IntArray::NewInstance(int length) { IntArray* ret = new IntArray(length); if( !(ret && ret->construct()) ) { delete ret; ret = 0; } return ret; } int IntArray::length() { return m_length; } bool IntArray::get(int index, int& value) { bool ret = (0 <= index) && (index <= length()); if( ret ) { value = m_pointer[index]; } return ret; } bool IntArray::set(int index, int value) { bool ret = (0 <= index) && (index <= length()); if( ret ) { m_pointer[index] = value; } return ret; } int& IntArray::operator [] (int index) { return m_pointer[index]; } IntArray& IntArray::operator = (const IntArray& obj) { if( this != &obj ) { int* pointer = new int[obj.m_length]; if( pointer ) { for(int i=0; i<obj.m_length; i++) { pointer[i] = obj.m_pointer[i]; } m_length = obj.m_length; delete[] m_pointer; m_pointer = pointer; } } return *this; } IntArray& IntArray::self() { return *this; } IntArray::~IntArray() { delete[] m_pointer; }
Test.cpp 源码
#include <iostream> #include <string> #include "IntArray.h" using namespace std; int main() { IntArray* a = IntArray::NewInstance(5); IntArray* b = IntArray::NewInstance(10); if( a && b ) { IntArray& array = a->self(); IntArray& brray = b->self(); cout << "array.length() = " << array.length() << endl; cout << "brray.length() = " << brray.length() << endl; array = brray; cout << "array.length() = " << array.length() << endl; cout << "brray.length() = " << brray.length() << endl; } delete a; delete b; return 0; }
我们在 test 文件中做了将数组 brray 赋值给数组 array,也就是说最后数组 array 和 brray 最后变成了 length 为 10 的数组,我们来看看编译结果
结果是正确的。在面试中,面试官如果问:如果我们定义一个空类,表示什么?我们能说这个类中什么都没有吗?肯定不是的!有编译器给我们默认提供的函数,如下图所示
二是关于 string 的疑问,在下面的这个代码中,它的输出是什么?为什么呢?
#include <iostream> #include <string> using namespace std; int main() { string s = "12345"; const char* p = s.c_str(); cout << p << endl; s.append("abcde"); cout << p << endl; return 0; }
我们的猜想原作者是想将字符串 abcde 接在 12345 后面。我们来编译运行下看看结果
我们看到两次打印的都是 12345。下来我们就来仔细分析下这个问题,指针 p 指向了字符串 s,我们第一次打印 p 的时候当然打印出的是 12345,但是在给 s 接上 abcde 的时候,这时 C++ 已经重新申请了一片内存,因为原来的内存不足以存放这些内容。但是指针 p 指向的还是原来的地址(相当于指针 p 已经成为了野指针了),所以就造成了我们两次打印的都是 12345,其实 s 已经改变成了 12345abcde 了,如下所示
我们直接打印 s 的值看看
我们看到其实 s 已经改变了,那么之所以会造成上面的情况是因为我们在 C++ 中使用 了 C 语言编程的思想。在 C++ 中,我们就应该使用 C++ 的面向对象的编程思想,尽量避免两种思想混合的编程。这样极易出 bug。
下来我们再来看个示例,还是关于 string 的,输出是什么?为什么?
#include <iostream> #include <string> using namespace std; int main() { const char* p = "12345"; string s = ""; s.reserve(10); for(int i=0; i<5; i++) { s[i] = p[i]; } if( !s.empty() ) { cout << s << endl; } return 0; }
我们编译看下结果
我们看到什么都没有,也就是说字符串 s 为空,怎么可能呢?前面就是一个简单的数组的复制啊,怎么会出错呢?我们来打印下它复制的结果,在 for 循环中加入 cout << s[i] << endl;
我们看到已经复制进去了啊。下来我们就来分析下这个问题,for 循环执行前它是 10 个字节,执行后它是 12345 + 五个字节。但是它记录的字符串长度一直都是 0,也就是相当于没有字符串,如下图所示
出现这样的情况完全就是由于我们的编码不规范,C 和 C++ 的思想混用,才导致出现莫名的 bug。通过对一些问题的分析,总结如下:1、在需要进行深拷贝的时候必须重载赋值操作符;2、赋值操作符和拷贝构造函数有同等重要的意义;3、string 类通过一个数据空间保存字符数据,通过一个成员变量保存当前字符串的长度;4、C++ 开发时尽量避开 C 语言中惯用的编程思想。
欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083。
以上是关于经典问题解析三(三十)的主要内容,如果未能解决你的问题,请参考以下文章