C++ primer -- 第十二章 类和动态内存分配
本章内容是对《C++ primer》第六版的第十二章的笔记整理,只为自己增加对C++的理解。主要内容如下:
对类成员使用动态内存分配
隐式和显式的复制构造函数
隐式和显式重载复制运算符
在构造函数中使用new
使用静态类成员
将placement new作用于对象
使用指向对象的指针
实现队列抽象
1.动态内存和类
对于静态类成员:无论创建了多少个对象,程序都只创建一个静态类变量副本。也就是说所有的静态类对象共享同一个静态成员。对于类静态成员的初始化,不能在类声明中初始化静态成员变量。因为,声明描述了如何分配哦内存,但不分配内存。而且静态类成员说单独存储的,不是对象的的组成部分。在类外初始化的时候,指出了类型并用作用域运算符,但是不需要要关键字static.这里又个例外,就是当它为const类型时,可以在类声明中初始化。
类中new出来的内存,让一个指针指向这块内存的地址,对于这块内存的调用都是通过指针来实现。在对象过期的时候,删除对象可以删除类象本身占用的内存,但是不能自动删除对象指针指向内容的内存,因此,必须使用delete来释放new出来的内存。
C++在类中会自动的提供如下的成员函数:
默认构造函数,如果没有定义构造函数
默认析构函数,如果没有定义构造函数
复制构造函数,如果没有定义构造函数
赋值构造函数,如果没有定义构造函数
地址构造函数,如果没有定义构造函数
另外,C++11中还提供了移动构造函数(move constructor)和移动赋值运算符(move assignment operator)
默认构造函数
因为类在创建对象的时候总是要调用构造函数,编译器会给类提供一个不接受任何参数,也不执行任何操作的构造函数。如果定义构造函数,C++不会定义默认的构造函数。
带参数的构造函数也可以说默认构造函数,只要所有的参数有默认值。但是要记住,消除构造函数的二义性。
复制构造函数
用于将一个对象复制到新创建的对象中,格式如class_name (const StringBad &)
何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都被调用,最常见的情况是将新对象显示地初始化为现有对象的对象,例如motto是stringBad对象,以下的四种声明都将调用复制构造函数:
StringBad ditto(motto); // calls StringBad(const StringBad &)
StringBad metto = motto; // calss StringBad(const StringBad &)
StringBad also = StringBad(motto); // calls StringBad(const StringBad &)
StringBad * pStringBad = new StringBad(motto); // calls StringBad (const StringBad &)
注意按值传递函数参数的时候,会产生参数的一个副本,在这种例子中,会调用复制构造函数来实现。因此,应该按引用传递对象,可以节省调用构造函数的时间以及存储对象的空间。
默认的复制构造函数的功能
默认的构造函数诸葛复制非静态成员(成员复制也称为浅复制),复制的是成员的值,解决类中由于类的默认复制构造带来的二次析构问题是,显示的定义复制构造函数,并且在类中复制字符的时候,采用深拷贝。
赋值构造的问题
对于赋值构造和复制构造类似,但是有些特别的需要注意的地方:
由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据
函数应当避免将对象赋给自身,否则,对象赋值前,释放内存操作可能删除对象的内容
函数返回一个指向调用对象的引用
改进类
String类的改造,String类的默认构造函数如下:
String::String()
{
len = 0;
str = new char[1];
str[0] = '\0'; //default string
}
这里使用 str = new char[1];
而不是str = new char;
,因为前者可以和类的析构函数delete [] str;
注意在C++98中国,字面值0有两个含义:可以表示数值0和空指针。C++11中为避免0的使用误解的情况。引入新的关键字nullptr,用于表示空指针。用0表示空指针依然可以,但是建议使用nullptr。
中括号表示法访问字符
C++中两个中括号组成一个运算符--中括号运算符,可以使用方法operator[]()来重载该运算符。通常二元C++运算符(带两个操作数)位于两个操作数之间但对以中括号操作符,一个操作数载中括号的前面,另一个操作数载中括号的后面。假设opera=是一个String对象:string opera("The Magic Flute")
,在编译器中opera[4],按照opera.operator[](4)
3.在构造函数中使用new时应注意的事项
如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
new和delete必须相互兼容,new对应delete,new[]对应delete[]
如果有多个析构函数,则必须以相同的方式使用new,要么都带中括号,要么都不带,因为只有一个析构函数,所有的构造函数都必须与它兼容
NULL, 0, nullpte: 以前空指针可以用0或NULL.C程序员通常使用NULL,而不是0,C++在传统上更喜欢使用简单的0,但是推荐的做法是使用nullptr
应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象,通常这种构造函数形式如下“
String::String(const String& st)
{
num_sgtring++; //handle static member update if is necessary
len = st.len; //same length as copied string
str = new char[len + 1]; //allocate space
std::strcpy(str, st.str); //copy string to new location
}
4. 有关返回对象的说明
当成员函数或独立函数返回对象的时候,有三种返回方式可供选择:
指向对性的引用
指向对象的const引用
const对象
返回对象将调用复制构造函数而返回引用不会。因此返回引用的效率更高。
如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的指针。在这种情况下,殷食欲哦那个复制构造函数来生成返回的对象。可以的情况下,优选返回引用。
5.使用指向对象的指针
调用 String * favourite = new String(sayings[choice]);
不是为要存储的字符串分配内存,而是为对象分配内存,具体说是为对象的成员变量分配内存,静态变量除外
定位new运算符
定位new运算符能够在分配内存时指定内存位置,但将它用于类对象时情况有些不同。
对于使用new定位符得到的类对象,需要显示的调用对象的析构函数来清除申请到的空间。
6.复习各种技术
6.1 重载<<运算符,以便它和cout一起用来显示对象的内容
ostream & operator<< (ostream & os, const c_name & obj)
{
os << ...; // display object contents
return os;
}
其中c_name为类名。
6.2 转换函数
将单个值转换成类类型,需要创建原型如c_name(type_name value)
,其中cname是类名,typename是要转换的类型的名称
6.3 构造函数使用new的类
如果类使用new来分配成成员的内存,需要注意以下的预防措施:
对于指向的内存是由new分配的所有类成员,都应在类的析构函数中使用delete
如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针或设置为空指针
构造函数中要么使用new[]要么使用new,不能混用,如果构造函数使用new[],析构函数应使用delete[]
定义一个重载复制运算符的类成员函数,其函数定义如下
classname (const className &)
应定义一个重载值运算符的类成员函数实例如下:
cname & cname::operator=(const c_name & cn)
{
if (this == & cn)
return *this;
delete [] c popinter;
cpointer = new typename[size];
...
return *this;
}
7.队列模拟
在C++的一项特性是:类中嵌套结构或类声明。通过这种方式,类中声明的结构的作用域为Queue类。如果某些编译器中不支持这种形式,那就只能把这个定义放在类外, 定义为全局的。
注意,这种声明不会创建数据对象,只是**指定了类中使用的类型。如果在类的私有部分进行,只能这个类使用,如果声明在共有部分,可以在类外声明类作用域使用。
在类对象构造函数中,对常量只能初始化,不能复赋值。这就需要用到成员参数化列表。对于普通的数据成员使用成员参数化列表和在函数中使用赋值没有什么区别,但对于本身就是类对象的成员,使用这种方式的效率更高。
使用成员初始化列表的时候需要注意:
这种格式只能用与构造函数
只能初始化非静态的const数据成员
必须用于初始化引用数据成员
评论