C++学习 十四、类的进阶(5)动态内存
前言
本片继续类的进阶学习,动态内存。
类与动态内存
动态内存与类的关系主要有两种:使用动态内存存储对象,对象开辟动态内存。
第一类,使用className objectPointer = new className;
分配动态内存,然后使用delete objectPointer;
释放内存。
第二类,构造函数中使用new
开辟一块动态内存,在析构函数中使用delete
。
第二类非常容易出现内存泄露问题,也是本篇的核心问题。
动态内存与析构函数
在类基础篇中提到,析构函数用于销毁对象释放空间。然而,默认析构函数只能释放对象本身占用的内存(即成员的内存),不能自动释放对象的指针成员指向的内存。
因此,如果在构造函数使用new
开辟了一块动态内存,必须在析构函数中使用delete
释放该内存,否则就会内存泄漏:
#include <cstdio>
#include <iostream>
#include <cstring>
using std::ostream;
class A{
private:
char* str_;
public:
A();
A(const char*);
~A();
int len();
char* get();
};
A::A(){
str_ = new char [14];
strcpy(str_, "Hello, World!");
std::cout << "str_: " << str_ << '\n';
}
A::A(const char* str){
int len = strlen(str);
str_ = new char [len];
strcpy(str_, str);
std::cout << "str_: " << str_ << '\n';
}
A::~A(){
std::cout << "str_: " << str_
<< " is destroyed" << '\n';
delete [] str_;
std::cout << "str_: " << str_
<< " is destroyed" << '\n';
}
int A::len(){
return strlen(str_);
}
char* A::get(){
return str_;
}
int main(){
A s1;
return 1;
}
/*
str_: Hello, World!
str_: Hello, World! is destroyed
str_: is destroyed
*/
上面就是一个简单的类内动态内存分配示例。
然而,如果main
函数出现了下面的使用方式:
A s1;
A s2 = s1;
就会报free(): double free detected in tcache 2
的double free错误。下面开始详细的类与动态内存讲解。
类的特殊成员函数
类的动态内存问题基本上是由类的特殊成员函数引起的。
C++将自动为类提供成员函数,包括:
- 默认构造函数,如果没有定义构造函数;
- 默认析构函数,如果没有定义析构函数;
- 复制构造函数,如果没有定义复制构造函数;
- 赋值运算符,如果没有定义赋值运算符;
- 地址运算符,如果没有定义地址运算符。
默认构造函数
默认构造函数之前已经记录过,小结一下:
- 如果没有定义构造函数,那么编译器自动提供一个参数列表为空且不执行任何操作的默认构造函数。
- 定义了构造函数后,还希望在创建对象时不显式初始化,则需要手动定义一个不需要接收参数的默认构造函数;
- 只能有手动定义一个默认构造函数。
默认析构函数
没有定义析构函数时,编译器自动提供一个默认析构函数,在销毁对象时,自动释放各成员的内存。析构函数由程序自动调用。
复制构造函数
复制构造函数使用已有对象初始化新对象,函数声明为className(const className&);
下面的初始化语句都会调用复制构造函数:
A s2(s1);
A s3 = s1;
A s4 = A(s1);
A* ps5 = new A(s1);
其中,A s2(s1);
与A s3 = s1;
等效,A s4 = A(s1);
显式调用了复制构造函数,A* ps5 = new A(s1);
初始化了一个匿名对象,然后将指针初始化给ps5
。
当函数按值传递对象时,也会调用复制构造函数:
void func(A s){}
当函数返回对象时,生成临时也会调用复制构造函数:
A funcc(){
A s;
return s;
}
注意:由于现代编译器如gcc
会优化返回值(称为RVO),默认在函数返回对象时不再生成临时变量,也就不会调用拷贝构造函数。可以在gcc编译时使用-fno-elide-constructors
参数关闭RVO。
默认复制构造函数
编译器默认提供的复制构造函数,就是将已有对象成员的值逐个复制到新对象中。
因此,已有对象new
了动态内存,新对象和已有对象在销毁时都会调用一次析构函数,也就出现了double free。
复制构造函数,浅拷贝和深拷贝
浅拷贝,可以看作对象成员的值复制,当对象内开辟了动态内存时,将出现double free的问题,原因在于值复制后的两个对象内的指针成员仍然指向同一块内存。
深拷贝,在拷贝源对象时,为新对象的指针重新开辟了一块内存,因此两个对象的指针成员指向不同内存,从而避免double free的问题。
默认复制构造函数只能进行浅拷贝。
下面定义一个用于深拷贝的复制构造函数:
class A{
private:
char* str_;
public:
A();
A(const char*);
A(const A&); // copy constructor
~A();
};
A::A(const A& s){
int strLength = s.len();
str_ = new char [strLength];
strcpy(str_, s.get());
}
这样,初始化语句A s1; A s2(s1);
就不会因为默认浅拷贝而报错了。
报错:上面这个程序中的函数,A::A(const A&)
由const
限制了已有对象s
只读,但成员函数A::len(),A::get()
并没有通过const
限制*this
只读,因此编译器将报error: passing 'const A' as 'this' argument discards qualifiers(传递const A的同时,this指针丢弃了修饰词)
。
修改:类声明中int len() const; char* A::len() const;
复制构造函数的深拷贝,主要就是new
一块新的动态内存。
赋值运算符
类的赋值运算符,将已有对象赋给另一个已有对象,原理同浅拷贝,因此也会出现double free问题。
如果构造函数使用了new
,那么也需要在类中将赋值运算符重载为深拷贝。
A& A::operator= (const A& s){
std::cout << "assign\n";
if (this == &s)
return *this;
delete [] this->str_;
str_ = new char [s.len()];
strcpy(str_, s.get());
}
类的赋值运算符重载深拷贝需要注意:
- 首先检查被拷贝对象与当前对象是否是相同。相同则直接返回当前对象,不同则先将当前对象的动态内存释放,不然就出现了内存泄漏,然后再开辟另一块动态内存。
- 返回当前对象的引用。
注意:第一点中,不检查而直接释放动态内存,如果赋值的两个对象相同,就直接销毁丢失了动态内存中存放的内容。
上述示例代码
#include <cstdio>
#include <iostream>
#include <cstring>
using std::ostream;
class A{
private:
char* str_;
public:
A();
A(const char*);
A(const A&); // copy constructor
~A();
int len() const;
char* get() const;
A& operator= (const A&);
};
A::A(){
str_ = new char [14];
strcpy(str_, "Hello, World!");
std::cout << "str_: " << str_ << '\n';
}
A::A(const char* str){
int len = strlen(str);
str_ = new char [len];
strcpy(str_, str);
std::cout << "str_: " << str_ << '\n';
}
A::~A(){
std::cout << "str_: " << str_
<< " is destroyed" << '\n';
delete [] str_;
std::cout << "str_: " << str_
<< " is destroyed" << '\n';
}
int A::len() const {
return strlen(str_);
}
char* A::get() const {
return str_;
}
A::A(const A& s){
std::cout << "copy\n";
int strLength = s.len();
str_ = new char [strLength];
strcpy(str_, s.get());
}
A& A::operator= (const A& s){
std::cout << "assign\n";
if (this == &s)
return *this;
delete [] this->str_;
str_ = new char [s.len()];
strcpy(str_, s.get());
return *this;
}
int main(){
A s1;
A s2(s1);
A s3 = s1;
s3 = s1;
return 1;
}
后记
本篇记录了类的进阶中,动态内存的使用与几个重要的成员函数:默认构造函数,默认析构函数,复制构造函数,赋值运算符。下一篇将总结一下类与动态内存的内容。