C++ Primer Plus12章 类和动态内存分配

12章 类和动态内存分配

在这里插入图片描述

1. 动态分配内存的原因

为了避免大量的内存被浪费,一般采用在程序运行时,而不是编译时,确定诸如使用多少内存的问题。

C++使用newdelete运算符来动态控制内存。

2. 在构造函数中使用new的注意事项
2.1 在构造函数中使用new,在析构函数中必须使用delete

1.为何需要分配内存:

如果不分配内存,直接用str=s,只会保存参数字符串的地址,并没有创建备份。

2.使用new分配内存的位置:

使用new分配的内存,位置在中;对象中仅保留了该位置的地址信息。

3.使用析构函数的原因:

当对象被删除时,对象本身所占用的内存被释放,但是对象成员指针所指向的内存不会被自动释放。因此,需要在析构函数中使用delete语句,从而在对象过期析构函数被调用时,释放函数中new分配的内存。

4.newdelete的使用方法:

/*声明*/
private:
char * str;				//字符串指针
int len;				//字符串长度
static int num_strings;	//对象的个数
/*构造函数使用new*/
StringBad::StringBad(const char* s)
{
    len = strlen(s);
    str = new char[len+1];	//分配内存
    strcpy(str, s);
    num_strings++;			//设置对象的个数
}
/*析构函数使用delete*/
StringBad::~StringBad()
{
    --num_strings;			//设置对象的个数
    delete [] str;			//释放内存
}
2.2 newdelete必须兼容。new对应deletenew []对应delete []

如果析构函数使用delete [],那么默认构造函数即使只对字符串赋空值,也要使用new []

String::String()
{
    len = 0;
    str = new char[1];
    str[0] = '\0';		//等同于str = nullptr;
}
2.3 多个构造函数必须使用相同的方式使用new
2.4 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。

1.复制构造函数的形式:StringBad(const StringBad &);

如果没有定义的话,C++会自动提供复制构造函数。

2.复制构造函数的调用时机:

一般来说是用现有的对象初始化一个新的对象时:

//将新对象显式地初始化为现有的对象
StringBad string1(string2);						//1
StringBad string1 = string2;					//2
StringBad string1 = StringBad(string2);			//3
StringBad *pStringBad = new StringBad(string2);	//4
//函数按值传递对象,或函数返回对象时
void callme2(StringBad sb) {}					//5
{return sb;}

第2或3种,可能会用复制构造函数直接创建string1;也可能先用复制构造函数生成一个临时对象,再将临时对象的内容复制到string1中。

第4种,是使用复制构造函数先创建一个匿名对象,再将新对象的地址赋值给string1

第5种,sb通过复制构造函数初始化。采用复制构造函数初始化会花费时间和空间,因此一般采用引用传递对象。

3.默认复制构造函数的功能:

默认复制构造函数会逐个复制非静态成员(也成为浅复制)的

如果成员本身是类对象,那么就会调用成员类的复制构造函数来复制该成员。

4.浅复制:

在这里插入图片描述

上图展示了在浅复制中,数据成员被逐个复制。

浅复制可能带来的后果是:当释放ditto对象时,析构函数被调用,导致str指向的字符串Home Sweet Home占用的内存被释放。在之后释放motto对象时,析构函数再次被调用,再次释放str指向的已经被释放过的内存,可能会导致不确定的、有害的后果。

例如,使用浅复制时,函数参数按值传递就会出现问题:

headline2直接作为函数参数来传递,会将headline2逐成员复制到sb中,包括headline2中成员指针指向的数据地址。

当函数调用结束,局部变量sb过期,其析构函数被调用,就会释放掉sb的成员指针所指向的数据。这数据也同样是headline2的成员指针指向的数据。

因此在headline2过期时,调用析构函数,就会再次释放成员指针指向的数据,导致不确定的、有害的后果。

void callme2(StringBad sb)
{
    cout << sb << endl;
}
callme2(headline2);	//使得析构函数被调用

5.深复制:

由于默认复制构造函数是在进行浅复制,在对象的析构函数被调用时会出现问题。因此,需要定义显式的复制构造函数以进行深度复制

在这里插入图片描述

深度复制是指,应该复制数据形成副本,并将副本的地址赋给指针成员。这样使得每个对象都有自己的数据,而不是引用另一个对象的数据。

可以看出,深度复制的必要性在于:

一些类成员使用 new初始化的、指向数据的指针,而不是数据本身。

6.新的复制构造函数:

StringBad::StringBad(const StringBad & st)
{
    len = st.len;
    str = new char [len+1];
    strcpy(str, st.str);
    num_strings++;
}
2.5 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。

1.C++会提供隐式的赋值运算实现。

2.赋值运算符的使用时机:

将已有的对象赋值给另一个对象时:

StringBad headline1("This is headline1");
StringBad headline2;
headline2 = headline1;				//使用赋值运算符
StringBad headline3 = headline1;	//使用复制构造函数或赋值运算符

在初始化对象时,不一定会使用赋值运算符。

3.赋值运算符的隐式实现:

赋值运算符的隐式实现也是进行浅复制,只逐个复制非静态成员的值。因此,也会导致对同一片内存多次释放的问题。

如果成员本身是类对象,将调用该成员类的赋值运算符来为该成员赋值。

4.赋值运算符的重载:

深度复制版本的赋值运算符所需的工作:

1.避免自我赋值

2.释放以前指向的内存

3.复制数据,而不是复制引用

4.返回调用对象的引用, 使得能够实现连续赋值

StringBad & StringBad::operator=(const StringBad & st)
{
    if(this == &st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char[len+1];
    strcpy(str, st.str);
    return *this;
}
3.静态类成员
3.1 静态类成员的特点:

静态类成员前有static关键字。

无论创建了多少对象,程序只创建一个静态类变量副本。

所有的类对象共享同一个静态成员。

3.2 静态类数据成员的初始化:

静态成员在类声明中声明,在包含类方法的文件中初始化。

如果静态数据成员是 const整数类型枚举型,可以在类声明中初始化。

初始化语句指明了类型、作用域解析运算符,但没有使用关键字static

//类声明文件中
private:
static int num_strings;
//类方法定义文件中
int StringBad::num_strings = 0;
3.3 静态类函数成员:

1.声明与定义:

函数声明必须加上static关键字,如果函数定义独立于函数声明,则函数定义不能包含关键字static

静态成员函数只能使用静态数据成员,因为它不与特定的对象相关联。

2.调用:

静态成员函数通过类名作用域解析运算符来调用(如果公有),不能通过对象调用,不能使用this指针。

//函数的声明与定义
public:
static int HowMany() {return num_strings;}
//函数的调用
int count = String::HowMany();
4.其它内容
4.1 使用 new创建对象的过程

在这里插入图片描述

4.2 析构函数的调用时机

如果对象是动态变量,执行完定义该对象的代码块时,将调用对象的析构函数。

如果对象是静态变量,则在程序结束时,调用对象的析构函数。

如果对象是用new创建的,仅当显式使用delete删除对象时,析构函数才会被调用。

4.3 函数的返回对象

如果要返回局部对象,那么必须返回对象。这种情况下,将通过复制构造函数生成返回的对象。

如果要返回一个没有公有复制构造函数的类(如ostream)的对象,那么必须返回引用。

如果既可以返回对象,又可以返回对象的引用,那么应该首选引用,因为引用效率更高。

4.4 特殊成员函数

如果没有定义的话,C++会自动的提供下面的成员函数:

  • 默认构造函数(如果没有提供构造函数);

大致形式StringBad::StringBad() {}

  • 默认析构函数;

  • 复制构造函数;

  • 赋值运算符;

  • 地址运算符。

隐式地址运算符返回调用对象的地址,一般与期望一致。

4.5 友元形式的比较函数

将比较函数作为友元,便于其他类型与类类型的比较:

friend bool operator==(const String &st1, const String &st2);

对于if("love" == answer)这样的情况, 可以先转换成operator==("love", answer),再通过只接受一个const char*类型参数的类构造函数进行转换,得到operator==(String("love"), answer)

如果不是友元函数,符号左侧的操作数类型必须为类类型,否则无法参与比较。

4.6 重载中括号运算符

将返回类型声明为char &,可以对特定元素进行赋值。

char & String::operator[](int i)
{
    return str[i];
}

上述方法可以实现字符的索引和修改,但没有使用const关键字,无法保证不对调用对象进行修改。

因此对于常量字符串,如果想要通过中括号进行索引,还需要提供一个新的版本:

const char & String::operator[](int i) const
{
    return str[i];
}
4.7 指向对象的指针

1.指针指向已有的对象

Shortestfirst不创建新的对象,只是指向已有的对象,因此不需要new来分配内存,也不需要delete来释放内存。

String *shortest = &sayings[0];
String *first = &sayings[0];
for(i=1; i<total; i++)
{
    if(sayings[i].length() < shortest->length())
        shortest = &sayings[i];
    if(sayings[i] < *first)
        first = &sayings[i];
}

2.指针指向新创建的匿名对象

favorite使用new整个对象分配内存,调用复制构造函数创建了新对象,需要使用delete来释放内存。

String *favorite = new String(sayings[choice]);
delete favorite;
4.8 将键盘输入行读入String对象的方法
istream & operator(istream & is, String & st)
{
    char temp[String:CINLIM];
    is.get(temp, String:CINLIM);
    //除去输入失败的情况
    if(is)
        st = temp;	//调用重载的赋值运算符
    //省略多余字符
    while(is && is.get()!='\n')
        continue;
    return is;
}
4.9 使用定位new运算符
char * buffer = new char[BUFF];
JustTesting *pc1, *pc2;
//指定内存位置
pc1 = new (buffer) JustTesting;
//确保两个内存单元不重叠
pc2 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6);
//显式地为使用定位new运算符创建的对象调用析构函数
//需要用正确的删除顺序——与创建顺序相反的顺序
pc2->~JustTesting();
pc1->~JustTesting();
//释放buffer所在区域
delete [] buffer;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值