第十二章 类和动态内存分配
2016-11-22 03:03
295 查看
前面说过,通常最好在程序运行时确定诸如使用多少内存的问题,方法就是使用new来创建动态内存并在使用完之后delete掉。怎么对类成员使用动态内存分配就是本章的主要内容了。这本书到这里也逐步加深了。
1. 动态内存和类
在类中使用new和delete运算符对类成员进行动态内存分配看似很强大,但代价是使用起来会导致许多新的问题,所以用起来必须小心。
1.1 这一小节举了一个StringBad类的例子,之所以叫Bad是因为这个类有缺陷。首先不管缺陷,这个类定义了一个静态类成员
static int num_string。
静态成员有一个特点,无论创建了多少对象,程序都只创建一个静态类变量副本,也就是所有类对象共享这个静态成员。另外不能在类声明中初始化静态成员变量,这是因为类声明只是描述如何分配内存,而不分配内存。所以初始化必须在类声明外,比如方法文件中。但是如果静态数据成员为整形或枚举型const,则可以在类声明中初始化。在类声明外初始化静态成员也很简单,使用类名加::限定符即可,例如书中
int StringBad::num_strings = 0;
StringBad有两个构造函数,一个析构函数。两个构造函数基本相同,只是默认构造函数把C++赋给新建的对象,而带参数的构造函数把参数赋给新建的对象。构造函数里面使用了new char[len+1] 来分配内存,这段内存的大小根据参数(或者默认值C++)的长度来决定。因为使用了new,所以析构函数里面使用了delete[]来释放这个内存。
咋一看这样好像没什么问题。但程序没有以我们期待的方式使用析构函数。首先主函数使用了一个以StringBad类对象的值传递的函数callme2(headline2), headline是一个StringBad类的对象。当子函数开始执行结束时,子函数调用了析构函数,之后headline2指向的值就不对了。然后当程序使用StringBad sailor = sports和knot = headline1时候又出现了问题。
1.2 特殊成员函数
上面一小节的问题是由特殊成员函数引起的,这些成员函数是自动定义的,所以可能与类设计不符。具体的说,C++自动提供了下面这些成员函数:
默认构造函数,如果没有定义构造函数;
默认析构函数;如果没有定义;
复制构造函数,如果没有定义;
赋值运算符,如果没有定义;
地址运算符;如果没有定义;
这样一看StringBad类中的问题是由隐式复制构造函数和隐式赋值运算符引起的。
默认构造函数已经讲了,如果没有定义构造函数,程序会自己定义一个空函数作为默认构造函数;如果定义了带参数的构造函数,则还必须定义一个不带参数的构造函数来作为默认构造函数。但如果带参数的构造函数都使用默认值,也可以作为默认构造函数。
复制构造函数用于将一个对象复制到新创建的对象中,即用于初始化过程中(包括上例callme2(headline2)按值传递参数),其原型通常如下:
Class_name(const Class_name &)
新建一个对象并将其初始化为同类现有对象是,这个函数将被调用,比如下面四种情况的任意一种:
StringBad ditto(motto);
StringbBad metoo = motto;
StringBad also = StringBad(motto)
StringBad * pStringBad = new StringBad(motto)
注意这个时候没有调用普通的构造函数,只调用了复制构造函数。
另外就是前面讲过的每当程序生成了对象副本是,编译器都将使用复制构造函数
1.3 StringBad类里面表示knot和sailor调用了复制构造函数,没有调用普通构造函数,但退出时调用了析构函数。析构函数在任何对象过期是都将被调用,而不管对象是如何被创建的。
同时因为使用了赋值语句sailor = sport,delete sailer 的char str[]时,把sport的char str[]也delete掉了,因为它们指向相同的内存。
上面这些问题的解决办法显然是必须得自己定义一个复制构造函数,正如书中所给出的一样
1.4 上面的问题同样也会出现在赋值上,所以还应写一个复制函数,赋值函数是将一个对象的成员值赋给一个已经存在的对象,而不是一个新创建的对象,这点和复制构造函数不同。
2. 就是修订后的String类了
首先是可以把默认构造函数修改了
str = new char[1].
之所以改成包含一个元素的数组,是为了和析构函数delete[] str对应
然后修订后的string类使用了几个友元函数对操作符>, <, ==, >>等进行了重载来比较或者输出string的不同对象,最好定义了使用中括号表示访问字符串的某个字符。
之后这个修订的String类定义了一个静态函数声明(包含关进子static),作用是调用前面声明的静态数据变量,也只能使用静态数据成员
最好如果想把一个C风格字符串赋给一个String对象,最好再写一个有针对性的赋值运算符的重载函数,以保证析构函数按预期的调用
3. 至此我们可以看出一个类只有一个析构函数,所以必须创建足够多的构造函数来保证new出来的内存能正确delete。如果new的是[],delete也要带[]。特别是应定义一个复制构造函数和复制运算符。
4. 当成员函数或独立的函数返回对象时,可以返回指向对象的引用,指向对象的const引用或const对象
如果方法或函数要返回局部对象,则应返回对象么若不是指向对象的引用,这个时候将使用复制构造函数来返回对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须分那好一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首先选择引用,因为其效率更高。
5. 使用new初始化对象
通常,如果Class_name是类,value的类型是Type_name,则下面的语句:
Class_name * pclass = new Class_name(value); 可以定义一个指向这个对象的指针。只适合将调用如下构造函数:
Class_name(Type_name);
然后调用一个复制构造函数来初始化*pclass;
当这个时候程序结束时还必须的加一行delete pclass来delete掉这个指向Class_name对象的指针,同时对象的析构函数也会被自动调用。
最好本章总结了一下各个要点,然后给出了一个队列的例子,可以自己敲一边应该就明白了。
1. 动态内存和类
在类中使用new和delete运算符对类成员进行动态内存分配看似很强大,但代价是使用起来会导致许多新的问题,所以用起来必须小心。
1.1 这一小节举了一个StringBad类的例子,之所以叫Bad是因为这个类有缺陷。首先不管缺陷,这个类定义了一个静态类成员
static int num_string。
静态成员有一个特点,无论创建了多少对象,程序都只创建一个静态类变量副本,也就是所有类对象共享这个静态成员。另外不能在类声明中初始化静态成员变量,这是因为类声明只是描述如何分配内存,而不分配内存。所以初始化必须在类声明外,比如方法文件中。但是如果静态数据成员为整形或枚举型const,则可以在类声明中初始化。在类声明外初始化静态成员也很简单,使用类名加::限定符即可,例如书中
int StringBad::num_strings = 0;
StringBad有两个构造函数,一个析构函数。两个构造函数基本相同,只是默认构造函数把C++赋给新建的对象,而带参数的构造函数把参数赋给新建的对象。构造函数里面使用了new char[len+1] 来分配内存,这段内存的大小根据参数(或者默认值C++)的长度来决定。因为使用了new,所以析构函数里面使用了delete[]来释放这个内存。
咋一看这样好像没什么问题。但程序没有以我们期待的方式使用析构函数。首先主函数使用了一个以StringBad类对象的值传递的函数callme2(headline2), headline是一个StringBad类的对象。当子函数开始执行结束时,子函数调用了析构函数,之后headline2指向的值就不对了。然后当程序使用StringBad sailor = sports和knot = headline1时候又出现了问题。
1.2 特殊成员函数
上面一小节的问题是由特殊成员函数引起的,这些成员函数是自动定义的,所以可能与类设计不符。具体的说,C++自动提供了下面这些成员函数:
默认构造函数,如果没有定义构造函数;
默认析构函数;如果没有定义;
复制构造函数,如果没有定义;
赋值运算符,如果没有定义;
地址运算符;如果没有定义;
这样一看StringBad类中的问题是由隐式复制构造函数和隐式赋值运算符引起的。
默认构造函数已经讲了,如果没有定义构造函数,程序会自己定义一个空函数作为默认构造函数;如果定义了带参数的构造函数,则还必须定义一个不带参数的构造函数来作为默认构造函数。但如果带参数的构造函数都使用默认值,也可以作为默认构造函数。
复制构造函数用于将一个对象复制到新创建的对象中,即用于初始化过程中(包括上例callme2(headline2)按值传递参数),其原型通常如下:
Class_name(const Class_name &)
新建一个对象并将其初始化为同类现有对象是,这个函数将被调用,比如下面四种情况的任意一种:
StringBad ditto(motto);
StringbBad metoo = motto;
StringBad also = StringBad(motto)
StringBad * pStringBad = new StringBad(motto)
注意这个时候没有调用普通的构造函数,只调用了复制构造函数。
另外就是前面讲过的每当程序生成了对象副本是,编译器都将使用复制构造函数
1.3 StringBad类里面表示knot和sailor调用了复制构造函数,没有调用普通构造函数,但退出时调用了析构函数。析构函数在任何对象过期是都将被调用,而不管对象是如何被创建的。
同时因为使用了赋值语句sailor = sport,delete sailer 的char str[]时,把sport的char str[]也delete掉了,因为它们指向相同的内存。
上面这些问题的解决办法显然是必须得自己定义一个复制构造函数,正如书中所给出的一样
1.4 上面的问题同样也会出现在赋值上,所以还应写一个复制函数,赋值函数是将一个对象的成员值赋给一个已经存在的对象,而不是一个新创建的对象,这点和复制构造函数不同。
2. 就是修订后的String类了
首先是可以把默认构造函数修改了
str = new char[1].
之所以改成包含一个元素的数组,是为了和析构函数delete[] str对应
然后修订后的string类使用了几个友元函数对操作符>, <, ==, >>等进行了重载来比较或者输出string的不同对象,最好定义了使用中括号表示访问字符串的某个字符。
之后这个修订的String类定义了一个静态函数声明(包含关进子static),作用是调用前面声明的静态数据变量,也只能使用静态数据成员
最好如果想把一个C风格字符串赋给一个String对象,最好再写一个有针对性的赋值运算符的重载函数,以保证析构函数按预期的调用
3. 至此我们可以看出一个类只有一个析构函数,所以必须创建足够多的构造函数来保证new出来的内存能正确delete。如果new的是[],delete也要带[]。特别是应定义一个复制构造函数和复制运算符。
4. 当成员函数或独立的函数返回对象时,可以返回指向对象的引用,指向对象的const引用或const对象
如果方法或函数要返回局部对象,则应返回对象么若不是指向对象的引用,这个时候将使用复制构造函数来返回对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须分那好一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首先选择引用,因为其效率更高。
5. 使用new初始化对象
通常,如果Class_name是类,value的类型是Type_name,则下面的语句:
Class_name * pclass = new Class_name(value); 可以定义一个指向这个对象的指针。只适合将调用如下构造函数:
Class_name(Type_name);
然后调用一个复制构造函数来初始化*pclass;
当这个时候程序结束时还必须的加一行delete pclass来delete掉这个指向Class_name对象的指针,同时对象的析构函数也会被自动调用。
最好本章总结了一下各个要点,然后给出了一个队列的例子,可以自己敲一边应该就明白了。
相关文章推荐
- 第十二章 类和动态内存分配
- 第十二章:类和动态内存分配
- 【c++ primer】第十二章 类和动态内存分配
- 第十二章 类和动态内存分配
- 第十二章-类和动态内存分配
- 第十二章
- [经济法]第十二章 会计与审计法律制度
- DELPHI基础教程 第十二章 异常处理与程序调试(一)
- 疯狂JAVA讲义---第十二章:Swing编程(六)微调控制器和列表框
- think in java第十二章读书笔记
- 第十二章 文件管理及Linux实现问与答
- UNIX环境高级编程学习之第十二章线程控制-可重入(线程安全)的getenv方法
- 《Programming in C++》第十二章
- 算法导论第十二章:二叉查找树
- WCF 第十二章 对等网 使用PNRP注册名字
- [转]写一个块设备驱动(第十二章)
- 第十二章 2 字符流
- Android[初级教程]第十二章 Notification的应用
- 第十二章:纤程
- 第十二章 使用结构和指针 [c和指针]