C++ Primer Plus第6版18个重点笔记(二)
造函数。不过,正如前面指出的,实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值操作符将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=操作符时也可能调用赋值构造函数。
?
与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
?
赋值运算符和拷贝构造函数在实现上的区别:
?
由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
函数应当避免将对象赋给自身;否则给对象重新赋值前,释放内存操作可能删除对象的内容。
函数返回一个指向调用对象的引用(方便串联使用),而拷贝构造函数没有返回值。
下面的代码说明了如何为StringBad类编写赋值操作符:
?
复制代码
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;
}
复制代码
代码首先检查自我复制,这是通过查看赋值操作符右边的地址(&s)是否与接收对象(this)的地址相同来完成的,如果相同,程序将返回*this,然后结束。
?
如果不同,释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete操作符,则上述字符串将保留在内存中。由于程序程序不再包含指向字符串的指针,一次这些内存被浪费掉。
接下来的操作与复制构造函数相似,即为新字符串分配足够的内存空间,然后复制字符串。
赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。
?
重载运算符最好声明为友元
比如将比较函数作为友元,有助于将String对象与常规的C字符串进行比较。例如,假设answer是String对象,则下面的代码:
if("love" == answer)
将被转换为:
if(operator == ("love", answer))
然后,编译器将使用某个构造函数将代码转换为:
if(operator == (String("love"), answer))
这与原型是相匹配的。
?
在重写string类时使用中括号访问字符时注意:
(1)为什么重载的[]返回值是个char &而不是char?
(2)为什么有两个重载[]的版本,另一个是const版本?
?
解答(1):
将返回类制声明为char &,便可以给特定元素?值。例如,可以编写这样的代码:
String means ("might");
means [9] = ' r';
第二条语句将被转换为一个重载运算符函数调用:
means.operator[][0] = 'r';
这里将r?给方法的返回值,而函数返回的是指向means.str[0]的引用,因此上述代码等同于下面的代码:
means.str[0] = 'r';
代码的最后一行访问的是私有数据,但由于operator 是类的一个方法,因此能够修改数组的内容。 最终的结果是“might”被改为“right”。
?
解答(2):
假设有下面的常量对象:
const String answer("futile");
如果只有上述operator定义,则下面的代码将出错:
cout << answer[1]; // compile-time error
原因是answer是常量,而上述方法无法确保不修改数据(实际上,有时该方法的工作就是修改数据, 因此无法确保不修改数据)。
但在重载时,C++将区分常量和非常量函数的特征标,因此可以提供另一个仅供const String对象使用 的 operator版本:
// for use with const String objects
const char & string::operator const {
return str[i];
}
有了上述定义后,就可以读/写常规String对象了 :而对于const Siring对象,则只能读取其数据。
?
静态成员函数在类声明外定义实现时不能再加static关键字,与静态成员变量一样。
?
实现has-a关系的两种方法:
?
组合(或包含)方式。这是我们通常采用的方法。
c++还有另一种实现has-a关系的途径—私有继承。使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这意味着基类方法将不会称为派生对象公有接口的一部分,但可以派生类的成员函数中使用它们。而使用公有继承,基类的公有方法将称为派生类的公有方法。简言之,派生类将继承基类的接口:这是is-a关系的一部分。使用私有继承,基类的公有方法将称为派生类的私有方法,即派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
使用私有继承,类将继承实现。例如,如果从String类派生出Student类,后者将有一个String类
组件,可用于保存字符串。另外,Student方法可以使用String方法类访问String组件。
?
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们使用术语子对象来表示同继承或包含添加的对象。
因此,私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
?
使用包含还是使用私有继承?
由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用何种方式呢?大多数C++程序员倾向于使用包含。
?
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义 虚函?,则应使用私有继承。
首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。
其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共亨祖先的独立基类。
总之,使用包含不太可能遇到这样的麻烦。
另外,包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中足可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
另—种需要使用私有继承的情况是需要重新定义?函数。派生类可