string部分分析(Class with pointer member)
//string.h
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);
String(const String& str); //拷贝构造
String& operator=(const String& str);//操作符重载 拷贝赋值
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
//构造函数
inline
String::String(const char* cstr=0)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
inline
String::~String()
{
delete[] m_data;
}
//拷贝赋值函数
inline
String& String::operator=(const String& str)
{
//一定要检查 self assignment
if (this == &str)//检测自我赋值
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
//拷贝构造
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
//output 函数
#include <iostream>
using namespace std;
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
//string_test.cpp
#include "string.h"
#include <iostream>
using namespace std;
int main()
{
String s1();
String s2("hello");
String s3(s1);//拷贝
cout << s3 << endl;
s3 = s2;//赋值
cout << s3 << endl;
cout << s2 << endl;
cout << s1 << endl;
}
1、Big Three(拷贝构造、拷贝赋值、析构函数)
析构函数:
inline
String::~String()
{
delete[] m_data;
}
与complex类不同的是,我们自定义了string类的析构函数(即使我们没有在complex中写出析构函数,但是编译器会提供默认析构函数)。但在string类中,存在动态分配的内存(上述构造函数中),需要我们在析构函数中手动的释放内存,而默认析构函数不会这样做。这也是Class with pointer member和Class without pointer member的显著区别。
拷贝构造(copy ctor)和 拷贝赋值(copy op=):
Class with pointer member 必须有copy ctor 和 copy op=!
默认拷贝构造函数:
如果程序员没有为类定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认拷贝构造函数会逐个成员地拷贝源对象的成员变量到新对象。对于基本数据类型,这通常是足够的,但对于指针(动态分配的资源,如动态数组或动态内存)逐个成员拷贝会出现一些问题:
-
新对象的指针成员会复制源对象的指针成员的值(即指针的地址)。
-
结果是两个指针指向同一块内存区域。
这种逐个成员拷贝的行为被称为“浅拷贝”。对于指针,浅拷贝会导致以下问题:
-
悬挂指针:如果源对象被销毁,其指针成员指向的内存可能被释放,而新对象的指针仍然指向这块内存,这将导致悬挂指针问题。
-
双重释放:如果两个对象的指针都尝试释放同一块内存,可能会导致运行时错误或程序崩溃。
自定义拷贝构造函数:
为了避免这些问题,通常需要为包含指针的类定义自定义拷贝构造函数,实现深拷贝!
拷贝构造(copy ctor):
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
拷贝赋值(copy op=):
inline
String& String::operator=(const String& str)
{
//一定要检查 self assignment
if (this == &str)//检测自我赋值
return *this;
//如果没有上述自我检测,当执行 a=a; 时,无法new一个合适大小的空间(m_data已经释放内存)
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
2、堆、栈与内存管理
出现一种创建对象的新的方式:
Complex c1(1,2);//隐式调用构造函数
Complex c2= Complex(1,2);//显式的调用构造函数
Complex * p =new Complex(3);//新的创建对象的方式
stack objects 的生命期:
在stack objects 的作用域结束之际,自动地调用析构函数进行清理,因此又称为auto objects。
static local objects 的生命期:
global objects 的生命期:
写在任何作用域之外的对象(任何{}之外的对象)即为global objects。
heap objects的生命期:
注:通常在C++中,我们可以显式地或隐式地调用构造函数,以Qt中的QLabel为例:
QLabel label1 = QLabel("Hello word!");//显式地调用构造函数
QLabel label2("Hello word!");//隐式地调用构造函数
然而,当我们隐式地调用默认构造函数时,需要特别小心,可能一不小心就声明了一个函数 :
QLabel label();//declares a function
QLabel label;//calls default constructor
但当你使用 new
关键字创建对象时,可以省略括号也可以不省略,如果构造函数接受默认参数。对于 QLabel
类,其默认构造函数没有参数,因此你可以写:
QLabel *label = new QLabel;
这里,new QLabel
相当于调用了 QLabel
类的默认构造函数 QLabel()
,即使没有显式地写出括号。
然而,如果你想要调用一个接受参数的构造函数,或者想要更明确地表示你正在调用构造函数,你可以包含括号,即使里面没有参数:
QLabel *label = new QLabel();
这样做可以提高代码的可读性,特别是在调用接受参数的构造函数时,它清楚地表明了构造函数的调用。
new 与 delete:
new 在这里被编译器分为三步:
-
分配内存,调用operator new 函数(内部为malloc函数);
-
转型<Complex *>
-
调用构造函数,通过指针pc调用构造函数
delete 在这里被编译器分为两步:
-
调用析构函数
-
释放内存,内部为free函数
动态分配所得到的内存块:
在new 与 delete 部分我们提到其底层是c语言中的malloc和free函数,在这里我们了解一下使用malloc到底的到多大的空间。
如果去创建一个complex对象,理论上我们会申请到8bit内存空间,但事实是在debug模式和release模式下分别得到64bit和16bit内存空间。string类型也是这样。
与上述一样,分配数组时也不是理论上的空间大小,在VC编译器下,分配的空间还存储了数组的大小以及其他内容。
当使用new 数组但delete没有[]时,造成内存泄露的空间并不是数组的空间,而是数组元素所指对象空间的泄露,在此处即string对象中的构造函数为m_data申请的空间。如果你对complex类进行array new,即时没有array delete 也不会出错,因为数组元素所指对象中没有动态分配到空间。 但我们要养成一个好习惯:array new 一定要搭配array delete。