有些数据,在程序运行前并不能确定数据大小,比如字符串的字节数,可以使用动态内存分配的方法在程序运行过程中而不是编译时进行内存分配。
在类构造函数中使用new运算符在程序运行时分配内存,使用析构函数进行堆区内存释放。
对类成员使用动态内存分配
- 在构造函数中使用new对类成员进行动态内存分配
- 在析构函数中使用delete进行内存释放
- 有时候必须重载赋值运算符,以保证程序正常运行
特殊成员函数
如果没有定义,C++会自动提供以下这些的成员函数:
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符 (一个对象赋值给另一个对象)
- 地址运算符
但是有的时候,采用采用系统自动提供的这些成员函数会引起问题。
带参数的构造函数也可以是默认构造函数,只要所有参数都为默认值即可,但是注意这时就不要再定义一个无参数的默认构造函数了,这样会引发二义性矛盾,相当于同时存在量两个默认构造函数。
复制构造函数 (重点:何时调用,有何功能)
类的复制构造函数原型通常如下:
由于使用值传递的方式将调用复制构造函数,所以应该按引用传递对象,这样可以节省调用复制构造函数的时间,以及存储新对象的空间。加const来确保传入参数的值不会被改变,
class_Name(const class_Name &);
以下三种情况会导致赋值构造函数被调用
1 根据已有对象创建新对象时
StringBad ditto(motto); //使用括号法创建新对象
StringBad metto = mottol; //隐式法调用赋值构造函数
StringBad also = StringBad(mottol);//显示法调用默认构造函数
StringBad * pStringBad = new StringBad(metto);//使用metto初始化一个匿名对象,并将新对象的地址赋给pStringBad指针。
2. 值传递的方式给函数形参传值
void callme2(StringBad sb)
{
cout << "值传递:\n";
cout << " \"" << sb << "\"\n";
}
callme2(headline2);
//这里headline2按参数传递,将调用默认的复制构造函数,根据实参headline1,来复制一份形参对象sb
//如果类对象中含有指针数据成员,这种简单的浅拷贝将引发内存冲突,发生错误。
//在形参对象销毁时,将改变实参的指针指向的内存,这是我们不希望看到的,可以通过深拷贝来解决。。
3. 以值方式返回局部对象时
Person dowork2()
{
Person p1;
cout << &p1 << endl;
return p1; //值返回时,返回的不是局部变量p1,(局部变量p1在函数结束时就被销毁了),返回的是p1的一个拷贝
}
Person p = dowork2(); //这里p是函数内局部变量p1的一个拷贝
深拷贝与浅拷贝
系统提供的默认拷贝构造函数会不加区分的、逐个复制对象的非静态成员
静态成员不受影响,因为它们属于整个类,而不是各个对象.
系统自动提供的浅拷贝操作,堆区中同一块内存区域会被释放两次,引发内存冲突
StringBad::StringBad(const StringBad& s)
{
len = s.len;
str = s.str; //指针这里会出现错误
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rkwd3tvS-1655784578518)(2022-06-03-17-05-31.png)]
深拷贝操作中使用new在堆区复制了一份新的副本,避免了内存冲突
StringBad::StringBad(const StringBad& s)
{
num_strings++; //更新静态成员变量,正常的拷贝是不改变静态成员的,因为静态成员属于整个类,而不属于某个对象
len = s.len;
/****************************************/
//重新申请一个新的堆空间存储字符串,避免内存冲突
str = new char [len + 1];
strcpy_s(str, len + 1, s.str);
/****************************************/
cout << num_strings << ": \"" << str << "\" deep copy object created\n";
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3SoBOGl-1655784578521)(2022-06-03-17-21-23.png)]
将已有的对象赋值给另一个对象时,将使用重载的赋值运算符
C++允许类对象赋值,通过自动的为类重载赋值运算符实现,但是与默认提供的浅拷贝构造函数相似的,默认的赋值运算符重载,也会由于其隐式的对成员进行逐个简单赋值,在面对指针成员时,对引发内存冲突,导致数据受损,堆区中同一块内存单元可能被销毁两次。
赋值运算符重载的一般原型如下
//它接受并返回一个指向类对象的引用
Class_Name & Class_Name::operator=(const Class_Name &);
解决赋值的问题
由于默认的复制运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。其实现与深拷贝构造有些类似,但是也有一些差别
- 由于目标对象可能引用了以前分配的数据,所以函数应该使用delete来释放这些数据。
- 函数应避免将对象赋值给自身,否则,给对象重新赋值前,释放内存的操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用,可以进行连续赋值。
//返回对象的引用,链式编程
//赋值操作并不创建新的对象,因此不需要更新静态成员num_strings的值
StringBad& StringBad::operator=(const StringBad& st)
{
//检查自我赋值
if (this == &st) return *this;
//由于这里的对象*this调用者,或者叫被赋值的目标对象已经初始化过,所以str指针已经被分配了字符串,(也可能为空,但是指针已经被初始化过了)
//所以这里要先释放掉这个str,否则的话,后面要将一个新的地址分配给str,后面再没有指向这一块内存的指针,这块内存将被浪费掉
delete[] str;
//下面的操作就与深拷贝构造函数类似了。
len = st.len;
str = new char [len + 1];
strcpy_s(str,len+1,st.str);
return *this;
}
完整程序
#include <iostream>
#include<cstring>
using namespace std;
class StringBad
{
private:
char* str; // 指针型数据成员
//没有为字符串本身分配存储空间,而是在构造函数中使用new动态的分配存储空间
int len; // length of string
static int num_strings; // 静态数据成员:所有对象共享同一个副本
public:
StringBad(const char* s); // constructor
StringBad(); // default constructor
StringBad(const StringBad & s);
~StringBad(); // destructor
StringBad& operator=(const StringBad& st);
// friend function
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
// initializing static class member
int StringBad::num_strings = 0;
// class methods
// construct StringBad from C string
StringBad::StringBad(const char* s)
{
len = std::strlen(s); // set size
str = new char[len + 1]; // allot storage,多一个单位存放终止符\0
strcpy_s(str, len+1, s); // initialize pointer
num_strings++; // 设置对象数目
cout << num_strings << ": \"" << str
<< "\" object created\n"; // For Your Information
}
StringBad::StringBad() // default constructor
{
len = 4;
str = new char[4];
strcpy_s(str, len, "C++"); // default string
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // FYI
}
StringBad::~StringBad() // necessary destructor
{
cout << "\"" << str << "\"对象析构, "; // FYI
--num_strings; // required
cout << num_strings << " left\n"; // FYI
delete[] str; // required
}
StringBad& StringBad::operator=(const StringBad& st)
{
//检查自我赋值
if (this == &st) return *this;
//由于这里的对象*this调用者,或者叫被赋值的目标对象已经初始化过,所以str指针已经被分配了字符串,(也可能为空,但是指针已经被初始化过了)
//所以这里要先释放掉这个str,否则的话,后面要将一个新的地址分配给str,后面再没有指向这一块内存的指针,这块内存将被浪费掉
delete[] str;
//下面的操作就与深拷贝构造函数类似了。
len = st.len;
str = new char[len + 1];
strcpy_s(str, len + 1, st.str);
return *this;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
os << st.str;
return os;
}
StringBad::StringBad(const StringBad& s)
{
num_strings++; //更新静态成员变量,正常的拷贝是不改变静态成员的,因为静态成员属于整个类,而不属于某个对象
len = s.len;
/****************************************/
//重新申请一个新的堆空间存储字符串,避免内存冲突
str = new char[len + 1];
strcpy_s(str, len + 1, s.str);
cout << num_strings << ": \"" << str << "\" deep copy object created\n";
}
void callme1(StringBad&); // pass by reference
void callme2(StringBad); // pass by value
int main()
{
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("标题一内容");
StringBad headline2("标题二内容");
StringBad sports("运动");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
callme2(headline2);
cout << "headline2: " << headline2 << endl;
//这里headline2按参数传递,将调用默认的复制构造函数,根据实参headline1,来复制一份形参对象sb
//如果类对象中含有指针数据成员,这种简单的浅拷贝将引发内存冲突,发生错误。
//在形参对象销毁时,将改变实参的指针指向的内存,这是我们不希望看到的,可以通过深拷贝来解决。。
//拷贝复制
cout << "拷贝构造函数:\n";
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
//对象赋值
cout << "对象赋值:\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
// std::cin.get();
return 0;
}
void callme1(StringBad& rsb)
{
cout << "使用引用传递:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
cout << "使用值传递:\n";
cout << " \"" << sb << "\"\n";
}