类和对象(4)
注: 本文接上一篇博文!// 本文所用例子
// ClassDemo.h
#ifndef _CLASS_DEMO_H_
#define _CLASS_DEMO_H_
namespace Hades
{
class ClassDemo
{
public:
ClassDemo(){}
ClassDemo(int num)
{
_num = num;
}
~ClassDemo(){}
int GetNum()
{
return _num;
}
private:
int _num;
};
}
#endif //!_CLASS_DEMO_H_
拷贝构造函数
// 是不是赋值?
ClassDemo demo(0);
ClassDemo demo2 = demo; 注意: 但凡有一个新的对象产生, 100%就需要调用构造函数.
所以, 它不是赋值.
那它是什么?
编译器会自动生成一个特殊的构造函数
ClassDemo(const ClassDemo& other)
{
// 所有成员变量都会拷贝
_num = other._num;
}这个函数被称为拷贝构造函数(复制构造函数)
拷贝构造函数的特点
拷贝构造函数格式是固定的!
在一个类中, 有且只有一个拷贝构造函数.
拷贝构造函数的参数一定是该类的引用.
为什么拷贝构造函数的参数需要是引用?
如果我们将拷贝构造函数设计成如下方式:
ClassDemo(const ClassDemo other)
{
// 成员拷贝代码
}它会引起 无限递归
为什么?
- 首先, 参数other是个
形参. 形参在被调用的时候, 会被
实参化.- 所谓实参化, 就是要被拷贝.
- 既然要拷贝, 就需要调用
拷贝构造函数. - 而拷贝构造函数中, 又发现形参other.
- 跳转到步骤2.
无限递归产生了
// 可配合例子进行输出结果展示
默认生成函数的小小总结
一个 空类 中会有那些方法?
class ClassDemo
{
public:
// 默认构造
// ClassDemo(){}
// 默认析构
// ~ClassDemo(){}
// 默认拷贝构造
// ClassDemo(const ClassDemo& other){}
// 默认赋值
// ClassDemo& operator=(const ClassDemo& other){return *this}
};我们来给上诉 ClassDemo 加上一个成员变量.
class ClassDemo
{
public:
// 默认构造
// ClassDemo(){}
// 默认析构
// ~ClassDemo(){}
// 默认拷贝构造
// 默认赋值
private:
int _num;
};在我们改变了之后,
- 默认构造函数没发生变化
- 默认析构函数没发生变化
- 默认拷贝构造函数改变了, 它进行了_num的赋值操作.
- 默认赋值函数改变了, 它进行了_num的赋值操作.
浅拷贝和深拷贝
通过以上总结, 我们知道,
编译器为我们默认生成的函数非常好, 都能自主根据成员变量来发生变化.
多么牛逼的东西, 多么贴心的设计!
那我们还需要自己来写吗?
#ifndef _HADES_STRING_H_
#define _HADES_STRING_H_
#include <cstring>
using namespace std;
class HadesString
{
private:
char* _szStr = nullptr;
int _iLen = 0;
public:
HadesString(char* str)
{
_iLen = strlen(str);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, str);
}
// 析构函数
~HadesString()
{
delete[] _szStr;
_iLen = 0;
}
// 获取字符串长度 记录的
int GetLen()
{
return _iLen;
}
// 获取字符串
char* GetString()
{
return _szStr;
}
// 设置字符串
char* SetString(char* str)
{
if (_szStr)
delete[] _szStr;
_iLen = strlen(str);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, str);
return _szStr;
}
};
#endif //!_HADES_STRING_H_
然后我们使用如下测试代码:
HadesString str("HadesStudio");
HadesString sb = str; // 复制构造函数
cout << sb.GetString() << endl;
sb = "Hello HadesStudio"; // 赋值函数
cout << sb.GetString() << endl;结果1: 
分析:
在我们调用默认的 赋值函数 的时候, 出现了问题.
首先, 我们知道,
sb = "Hello HadesStudio";这句代码等价于:- 构造一个临时的strTmp.
{ HadesString strTmp("Hello HadesStudio"); - 调用默认赋值函数.
sb.operator=(strTmp); - 析构临时strTmp对象.
}
- 构造一个临时的strTmp.
- 然后我们发现 sb 对象的 _szStr 指向的内存地址被delete[]了.
- 这就是
野指针. - 使用
野指针是未定义的行为.所以结果不正确.
根据我们上面的分析, 我们的 默认拷贝构造函数 为什么好用?
- 因为我们没有析构 str 对象.
- 我们析构之后在来看一下结果.
HadesString* str = new HadesString("HadesStudio");
HadesString sb = *str; // 复制构造函数
delete str;
cout << sb.GetString() << endl;结果2: 
所以我们需要自定义复制构造函数和赋值函数来进行自定义的字符串操作.
我们将程序改造如下:
#ifndef _HADES_STRING_H_
#define _HADES_STRING_H_
#include <cstring>
using namespace std;
class HadesString
{
private:
char* _szStr = nullptr;
int _iLen = 0;
public:
HadesString(char* str)
{
_iLen = strlen(str);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, str);
}
// 析构函数
~HadesString()
{
delete[] _szStr;
_iLen = 0;
}
// 拷贝构造函数
HadesString(const HadesString& other)
{
if (this != &other)
{
_iLen = other._iLen;
if (_szStr)
delete[] _szStr;
_iLen = strlen(other._szStr);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, other._szStr);
}
}
// 赋值函数
HadesString& operator=(const HadesString& other)
{
if (this != &other)
{
_iLen = other._iLen;
if (_szStr)
delete[] _szStr;
_iLen = strlen(other._szStr);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, other._szStr);
}
return *this;
}
// 获取字符串长度 记录的
int GetLen()
{
return _iLen;
}
// 获取字符串
char* GetString()
{
return _szStr;
}
// 设置字符串
char* SetString(char* str)
{
if (_szStr)
delete[] _szStr;
_iLen = strlen(str);
_szStr = new char[_iLen + sizeof(char)];
strcpy(_szStr, str);
return _szStr;
}
};
#endif //!_HADES_STRING_H_
我们在来进行测试:
HadesString str("HadesStudio");
HadesString sb = str; // 复制构造函数
cout << sb.GetString() << endl;
sb = "Hello HadesStudio"; // 赋值函数
cout << sb.GetString() << endl;结果3: 
是时候来一波总结了 对指针进行直接赋值的行为, 我们称之为 浅拷贝
_浅拷贝 没有 维护参数的生命周期_, 参数的生命周期交由别处控制.
浅拷贝是存在风险的
将内存整体进行拷贝的方式, 我们称之为 深拷贝
在对象中维护了所有参数的声明周期, 所有参数的声明周期都和我的对象是同步的.
深拷贝是安全的.
小知识
- 头文件中写的函数默认都会请求提升为 inline 函数.(当然依然是编译器在决定)
在
Visual Studio环境下, Debug(调试)模式中- 内存值 0xCCCCCCCC 代表
未经过初始化的值, VS会自动初始化为 0xCCCCCCCC. 内存值 0xDDDDDDDD 代表
已经被删除的值.- 但这不是绝对的, VS只能
尽量的来进行一些设置, 不一定100%能够覆盖.
- 但这不是绝对的, VS只能
- 内存值 0xCCCCCCCC 代表
- 这只是方便调试的一种调试手段. 是VS提供给开发人员的一点小便利.
下节预告:
运算符的重载
未完待续如有错误,请提出指正!谢谢.
本文由 花心胡萝卜 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: 2017-01-04 at 01:51 am