accelerated c++ 第十二章笔记
第十二章 使类对象像一个数值一样工作
当我们自行创建了一个类时,我们便能够调控其扩展能力,使这些对象能够以数值形式发挥作用.恰当地实现复制和赋值操作,将有助于使这些对象的行为类似于数值.换句话说,开发者可以通过设计确保各个实例相互独立.
开发人员能够掌控类型转换过程及处理与之相关的对象操作,并能实现类似C++自带类型对象的功能。例如,在标准库中string类就是一个典型代表,它提供了一系列函数来执行自动转换操作。本章将介绍一种简化的String类实现,并将其命名为Str。主要探讨如何为该新接口设计一个直观且易于使用的功能集合。
12.1 一个简单的string类
class Str{
public:
typedef Vec<char>::size_type size_type;
//默认构造函数,创建一个空的Str
Str(){}
//生成一个Str对象,包含c的n个复件
Str(size_type n,char c):data(n,c){}
//生成一个Str对象并用一个空字符结尾的字符数组来初始化
Str(const char* cp){
std::copy(cp,cp+std::strlen(cp),std::back_inserter(data));
}
//生成一个Str对象并用迭代器b和e之间的内容对它进行初始化
template<class In>Str(In b,In e){
std::copy(b,e,std::back_inserter(data));
}
private:
Vec<char> data;
};
最后一种自定义构建方案作为其核心组成部分出现于源代码中。该方案基于模板机制实现方式设计而成,并通过不同迭代器类型的实例化实现了功能扩展。这一模块不仅支持根据输入类型生成相应的字符串对象(如字符组),还能够根据输入类型生成相应的字符串对象(如向量或其他数据结构)。
该Str类未定义复制构造函数;同时也没有为赋值运算符或析构函数做出任何定义。由于存在默认的实现。
该类无需自行分配内存的能力。
它将管理内存的细节交由编译器完成,
从而由编译器自动生成所需的相关功能。
这些功能则通过调用Vec中的相关成员函数来执行操作。
为了更好地理解默认操作的本质,
我们观察到该类无需定义析构函数。
实际上,
如果有必要编写该类的析构函数,
这个解析构造函数也没有什么工作要做。
一般而言,
一个无需定义析构函数的类
也无需显式地定义复制构造函数或赋值运算符重载。
12.2 自动转换
在Str类中已经提供了一个特定类型的构造函数(它接受一个const char*类型的参数)。因此,在编译器生成一个Str对象而程序仅提供const char*时会自动调用该构造函数(当我们将一个const char*指针赋值给Str变量时也会触发该构造函数)。在我们使用s="hello"这种表达式时,在编译器层面会创建一个无名、局部且临时的Str对象来表示这个字符串常量(然后会自动将该临时值通过编译器自动生成的赋值运算符功能赋予变量s)。
12.3 Str操作
cin >> s;
cout << s;
s[i];
s1 + s2;
如果一个被定义的运算符是一个成员函数对象,则其中的一个参数可以通过隐式传递
class Str{
public:
//构造函数同前
char& operator[](size_type i){return data[i];}
const char& operator[](size_type i) const {return data[i];}
private:
Vec<char> data;
};
在Vec类中仅能处理非常量对象的那个索引操作函数返回一个指向字符位置的引用,因此允许对那个字符进行赋值
我们返回的是一个const char引用对象而非简单的char类型。这是因为我们需要与标准string类实现兼容性要求,并确保数据的有效引用机制得到满足。
12.3.1 输入输出运算符
cin >> s;
//等价于
cin.operator>>(s);
该程序调用对象cin重载了的>>运算符执行功能。这一行为表明>>运算符必须属于类istream这个成员函数。然而我们无法修改类istream本身的定义因此我们无法将这个操作附加到该类中。如果我们将其作为一个成员函数加入Str类中那么我们的用户在使用Str时必须采用以下方式进行输入:
s.operator(cin);
//或者使用它的等价形式
s >> cin;
这一点与整个库遵循的语法规则存在差异。因此,可以得出这样的结论:输入/输出函数不允许作为类成员函数。
为了进一步优化Str类的性能和功能特性, 我们需要在Str.h头文件中添加输入输出运算符函数的声明
std::istream& operator>>(std::istream&,Str&);
std::ostream& operator<<(std::ostream&,const Str&);
运算符函数模块的功能非常容易理解:它会逐个读取Str对象的所有字符,并依次获取每个字符。
ostream& operator<<(ostream& os,Str& s)
{
for(Str::size_type i = 0;i!=s.size();++i)
os << s[i];
return os;
}
这种用法还要求我们再给Str类增加一个size函数:
class Str{
public:
size_type size() const {return data.size();}
}
12.3.2 友元函数
简化版输入运算符
istream& operator>>(istream& is,Str& s)
{
//抹去存在的值
s.data.clear();
//按序读字符并忽略前面的空格字符
char c;
while(is.get(c) && isspace(c))
; //只判断循环条件,不进行其他工作
//如果读到非空格字符,重复以上操作直到遇到遇到一个空格字符
if(is){
do s.data.push_back(c); //产生一个编译错误,因为data是私有成员数据
while(is.get(c)&&!isspace(c));
//如果遇到一个空格字符,把它放在输入流的后面。
}
return is;
}
这段代码无法通过编译程序运行,在Str对象中operator>>并不是一个成员函数字段的操作符定义域限制导致无法访问s类对象中的私有成员数据data字段
将输入操作符函数声明为Str类中的一个友元函数。友元函数与成员函数享有相同的访问权限。
class Str{
friend std::istream& operator>>(std::istream&,Str&);
};
友元函数可以在类定义中的任何位置进行声明。由于其特殊的访问权限特性, Friend functions具有特殊的行为规范, 因此它们是class interface的重要组成部分。通常在class定义之前或者接近public interface的地方将所有Friend functions集中在一个相对独立的部分进行管理。
12.3.3 其他二元运算符
完成+运算符
为了使`s1+s2+s3$要求加法运算符的返回类型指定为Str类型。这意味着应将加法运算符函数定义为一个非成员形式。
Str operator+(const Str&,const Str&);
要想方便的实现operator+函数,最好先写出operator+=函数。
class Str{
friend std::istream& operator>>(std::istream&,Str&);
public:
Str& operator+=(const Str& s){
std::copy(s.data.begin(),s.data.end();std::back_inserter(data));
return *this;
}
typedef Vec<char>::size_type size_type;
Str(){}
Str(size_type n,char c):data(n,c);
Str(const char* cp){
std::copy(cp,cp+std::strlen(cp),std::back_inserter(data));
}
template<class In> Str(In i,In j){
std::copy(i,j,std::back_inserter(data));
}
char& oeprator[](size_type i){return data[i];}
const char& oeprator[](size_type i) const {return data[i];}
size_type size() const {return data.size();}
private:
Vec<char> data;
};
std::ostream& operator<<(std::ostream&,const Str&);
Str opertor+(const Str&,const Str&);
现在可以用operator+=函数来实现operator+函数了
Str operator+(const Str& s,const Str& t)
{
Str r = s;
r += t;
return r;
}
12.3.4 混合类型表达
Str greeting = "Hello, " + name + "!";
上式运行后greeting对象的结果与下面的代码的运行结果相同。
Str temp1("Hello"); //Str::Str(const char*)
Str temp2 = temp + name; //operator+(const Str&,const Str&)
Str temp3("!"); //Str::Str(const char*)
Str s = temp2 + temp3; //operator+(const Str&,const Str&)
在代码中频繁使用临时变量会导致较大的内存开销。实际上,在C++的标准库实现中,并不依赖于自动转换来处理混合类型的操作数相加问题。相反地,在这种情况下会产生大量的临时对象实例以实现类似的功能。因此,并非所有的类型都能通过简单的自动生成混合类型操作数相加的方式实现类似的效果。
12.3.5 设计二元运算符
类型转换在二元运算符实现中的地位显得尤为重要。如果一个类能够进行类型转换,则将二元运算符定义为非成员函数被视为一种良好做法。采用这种处理方式能够确保操作的对称性。
如果将该二元运算符函数定义为一个成员函数,则这将导致操作间不对等的情况出现;其中右边的操作对象可能已经是进行了隐式转换后的结果;然而左边的操作对象则无法进行这样的转换;就如+=等固有的不对等运算符而言;但是一旦在具有对称的操作对象环境中时,则容易让人感到困惑,并可能导致错误行为。
在此定义的二元运算符函数中, 我们明确规定左侧操作数的数据类型必须与当前类一致. 如果允许左侧操作数进行类型转换, 则可能导致其被存储为当前类的对象于一个临时存储空间中. 由于这是一个临时变量, 在赋值结束后将无法获取到刚生成的对象. 因此, 如同该运算符函数一样, 所有的复合赋值行为都应被视为类型成员行为.
12.4 有些转换是危险的
通常将用于构建对象体系的构造函数明确定为 explicit 型。其中一些参数最终会成为对象的一部分,在这种情况下,则无需将这些构造函数明确定为 explicit 型。
在vector类和Vec类中存在一种带有Vec::size_type类型参数且声明为explicit的构造函数。这些特定类型的构造函数通过其参数值来计算并分配给对象足够的内存空间。值得注意的是,这些参数主要影响了对象的结构特性而非其内容填充。
12.5 类型转换操作函数
开发人员可以为类显式地实现类型转换操作,并明确将一个对象从其原有的数据类型映射到目标数据类型的机制。此操作必须被实现为类的一个成员函数,并且该成员函数的操作符名称由关键字'operator'与目标数据类型的名称组合而成。
class Student_info{
public:
operator double();
//...
};
在涉及将一个double类型的值传递给某个地方的情况下(这里),程序返回的是一个Student_info对象而不是double类型对象(这显然不符合预期),因此编译器会自动调用这个转换操作函数以进行必要的转换(以便满足类型要求)。
vector<Student_info> vs;
//填充vs
double d = 0;
for(int i = 0;i!vs.size();++i)
d += vs[i]; //vs[i]被自动转换还成double类型的值
cout << "Average grade:" << d/vs.size() << endl;
if(cin>>x);
//等效于
cin >> x;
if(cin);
如果表达式用于条件判断时,在这种情况下会被系统转换为布尔类型。当使用其他任何一种数据类型或指针类型的数据时,系统会将其转换为布尔类型。
在标准库中实现了从iostream到void类型的映射关系,其中pointer void标识的是指向无内容数据的空间.该操作符通过检查特定状态标志来确定流的有效性,并返回0或一个自定义的非零值以指示流的状态.
有几个用途?最基础的应用就是用作指向类型的指针。有时候会被称作通用指针。这种类型的指针能够指向任意对象;然而需要注意的是,在这种情况下你无法对其进行间接引用;此外,由于它所指向的对象类型未知
istream类被重载为operator void*而非operator bool以便于使编译器能够识别以下的错误使用
int x;
cin << x; //原本应该写成cin>>x的
假设istream类被重新定义为拥有operator bool成员函数,则以下这个表达式将会触发该成员函数的执行流程:首先将cin变量转化为布尔类型;随后将该布尔值转换为整型数值;接着对该整型值执行左移x位操作;最后并丢弃计算后的最终结果。值得注意的是,在C++标准库中,默认情况下允许将istream对象当作条件表达式的参与方(即当其被赋值或比较时),但这种情况下,默认情况下无法将其视为数值参与运算。
12.6 类型转换与内存管理
将字符串Str传递给一个以空字符结尾的字符数组类型的参数存在内存管理方面的漏洞
标准库的string类支持用户获取存储于字符数组中的字符串副本这一操作,并且仅限于通过明确的方式实现这一功能。该类定义了三个成员函数用于从string类型对象中获取字符数组数据。
