第12章 类和动态内存分配
1. 静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整形或枚举型const,则可以在类声明中初始化。
P426-P427类静态成员的声明和初始化
//strnbad.h
class StringBad
{
private:
static int num_strings;
…
};
//strnbad.cpp
int StringBad::num_strings = 0;
不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。请注意,初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static。初始化是在方法文件中,而不是在类声明文件中进行的,如果在头文件中,当头文件被包括在其他几个文件中时,将出现多个初始化副本,从而引发错误。
2. C++类自动提供了下面这些成员函数:
1) 默认构造函数,如果没有定义构造函数;
2) 默认析构函数,如果没有定义;
3) 复制构造函数,如果没有定义;
4) 赋值运算符,如果没有定义;
5) 地址运算符,如果没有定义。
C++11提供了另外两个特殊成员函数,移动构造函数和移动赋值运算符。
3. 带参数的构造函数也可以是默认构造函数,只要所有参数都为默认值。
4. 复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数的原型通常如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。
5. 复制构造函数何时调用
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,这包括当函数按值传递对象或返回对象。(函数按引用传递对象可以节省调用构造函数的时间以及存储新对象的空间)
6. 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。静态成员不受其影响,因为它们属于整个类,而不是各个对象。
7. 如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这称为深度复制。浅复制(成员复制)只是复制指针值。先复制只浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
8. 赋值运算符
将已有的对象赋值给另一个已有的对象时,将使用重载的赋值运算符。
初始化对象时,并不一定会使用赋值运算符。例如:
StringBad metoo = knot;
这里,metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。然而实现时也可能会分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。
9. 赋值运算符函数的实现和复制构造函数相似,但也有一些差别。
1) 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
2) 函数应该避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
3) 函数返回一个指向调用对象的引用。
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;
}
10. NULL是一个表示空指针的C语言宏。C++11引入新关键字nullptr,用于表示空指针。
11. P439-P440提供两个中括号表示法函数访问字符的原因
12. 静态类成员函数
可以将成员函数声明为静态的,函数声明必须包含关键字static,如果函数定义是独立的,则其中不能包含关键字static。这样做有两个重要的后果:
1) 首先,不能通过对象调用静态成员函数;而且静态成员函数不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
2) 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
13. P449——P451
有关返回对象的说明
如果方法或函数要返回局部对象,则应返回局部对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法或函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应该首选引用,因为其效率更高。
14. 如果对象是用new创建的,则仅当您显式使用delete删除对象时,其析构函数才会被调用。
15. P456-P459显式地为使用定位new运算符创建的对象调用析构函数。
16. ostream & operator<<(ostream & os, const c_name & obj)
{
os << …;
return os;
}
c_name是类名。如果该类提供了能够返回所需内容的公有办法,则可在运算符函数中使用这种方法,这样便不用将它们设置为友元了。
17. 嵌套的结构和类
在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。
18. 成员初始化列表
从概念上讲,调用构造函数时,对象将在括号中的代码执行之前被创建(成员变量被分配内存),然后,程序进入到括号中,使用常规的赋值方式将值存储到内存中。对于const类成员和被声明为引用的类成员,必须在构造函数中使用初始化列表,使它们在创建时进行初始化。对于其它简单数据成员,使用初始化列表和在函数体中赋值没有什么区别。对于本身就是类对象的成员来说,使用成员初始化列表的效率更高,因为它直接使用复制构造函数,减少了再调用赋值运算符函数这个步骤。
语法:
Classy::Classy(int n, int m) : mem1(n), mem2(0), mem3(n * m + 2)
{
…
}
总之,请注意以下几点:
1) 这种格式只能用于构造函数;
2) 必须用这种格式来初始化非静态const数据成员;
3) 必须用这种格式来初始化引用数据成员。
数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
19. C++11的类内初始化
C++11允许您以更直观的方式进行初始化:
class Classy
{
int mem1 = 10;
cons tint mem2 = 20;
…
}
这与在构造函数中使用成员初始化列表等价。
如果同时使用了成员初始化列表的构造函数,则列表将覆盖这些默认初始值。
20. 伪私有方法:
如果类目前不需要提供复制构造函数和赋值运算符,然而将来的时候可能需要复制对象,您可能会忘记没有为复制提供适当的代码,在这种情况下,程序能够编译和运行,但结果确实混乱的,甚至会崩溃。这时候可以使用伪私有方法。
class Queue
{
private:
Queue(const Queue & q){}
Queue & operator=(const Queue & q){return *this} …
}
有两个作用:第一,避免了本来将自动生成的默认方法定义。第二,因为这些方法是私有的,所以不能被广泛使用。
Queue nick(nip); //not allowed
tuck = nip; //not allowed
所以,与其将来面对无法预料的运行故障,不如得到一个易于跟踪的编译错误,指出这些方法是不可访问的。另外,在定义其对象不允许被复制的类时,这种方法也很有用。