c++抽象数据类型定义,抽象数据类型定义
目录1概念1.1内部类型和自定义类型1.2类设计思想2设计类2.1自定义类型2.2成员函数2.2.1常量成员函数2.2.2静态成员函数2.3非成员函数2.4结构和类2.5访问权限2.5 .1公共2.5.2私有2.5.3朋友2.5.4受保护
设计类的常用函数3.1访问器函数3.2构造器3.2.1是否定义构造器3.2.2默认构造器3.2.2.1初始化程序列表和函数体中的初始化3.2.2.2显示或隐式定义默认构造器3.2.3带参数的构造器3 .2.3.1复制构造器3.2.4构造器的自动转换3.3破坏者3.4重载运算符函数3.4.1索引运算符函数3.4.2赋值运算符3.4.3输出运算符3.4.4复合加法赋值运算符函数3.4.5加法运算符函数3.4
原则4.1三位一体原则
1.1内部类型和自定义类型C类型可以分为两种,一种是内部类型和自定义类型(类类型)。
内部:定义为语言核心的一部分,如char、int、double等。
用户定义的类型:在数据结构中组合相关的数据值,如字符串、向量和istream。
1.2类的设计思想除了输入输出库中的一些底层的、系统特定的例程,库中的类所依赖的语言工具与任何程序都可以定义的特殊类型的语言工具是一样的。
许多C设计依赖于这样一种设计思想,即我们应该让程序员创建像内部类型一样易于使用的类型。除了大量的语言支持之外,创建一个界面简洁直观的类型需要我们在类的设计过程中有经验和解释。
二级设计
2.1自定义类型为了防止用户直接访问数据,隐藏对象存储方式的实现细节,我们要求用户只能通过函数访问对象。
因此,我们需要为用户提供对对象的便捷操作,这些操作构成了我们的类接口(函数)。
在头文件中自定义类时,要使用限定名(如std:string等。).是供他人使用的代码,应该包含最少数量的必要声明。
2.2成员函数的一般规则:如果一个函数可以改变一个对象的状态,那么它就应该是这个对象的成员。
索引运算符函数和赋值运算符函数必须是成员函数。
成员函数:一个类对象的成员。
1为了调用成员函数,用户必须指出被调用的函数是对象的成员。(使用:[作用域运算符]) 2在对象的成员函数中,不需要创建新的对象来作为参数传递。当调用对象的成员函数时,必须指明要使用哪个向量或字符串。(如字符串s;可以在中调用s.size(),但是如果不指定string对象,就不能调用string类的size()函数。3在成员函数中,可以直接访问对象的数据元素。不需要使用限定形式来引用成员函数中的成员,因为它引用的是操作对象的成员。
示例://class
结构信息{
std:字符串名称;
//成员函数
STD:istream read(STD:istream);
}
//调用成员函数
istream信息:读取(istream in){
名义上;
返回;
}对比版:
//类
结构信息{
std:字符串名称;
}
istream info:read(istream in,info s){
在s . name;
返回;
}如果你把:放在一个名字前面,说明我们要用这个名字的某个版本,用的版本不能叫任何东西的成员。
如下例所示,如果我们想调用非成员函数add(带一个参数),我们就用:否则,编译器会认为我们指示的是info:add,从而提示错误——,因为调用中使用的参数。(这个非成员函数应该在头文件中声明)
2.2.1常量成员函数在下面的程序中,add成员函数声明为const (const加在参数表后面),表示函数中不能修改对象的值。
所以我们可以用一个常量对象来调用它。但是我们不能在常量对象上调用非常量函数。例如,常量info对象不能调用read函数,因为它可以修改对象的状态。
调用成员函数时,如果函数是对象的成员,则对象不能是参数。因此,不可能在参数列表中指示该对象是const。
但是我们可以对函数本身进行限制,这样就可以称之为常数(成员函数)。限定这个常量应该包含在类中定义的函数声明和函数定义中。
注意:即使一个程序从未直接创建过任何常量对象,在函数调用过程中仍然有可能创建许多对常量对象的引用。如果将一个非常数对象传递给一个具有常量引用参数的函数,该函数会将该对象视为常量,编译器只允许它调用该对象的常量成员。
综上所述,非常数非成员函数可以访问常量和非常数类成员,而常量非成员函数只能访问常量类成员函数。
示例程序
头文件:
#ifndef测试信息
#定义测试信息
双加(double,double);//必要的声明,以便在源文件中使用它:
双加(double);
双倍交易(双倍,双倍);
#endif#include
使用STD:CIN;使用STD:cout;
使用STD:endl;
#包括
使用STD:string;
#包含“test.h”
结构信息{
std:字符串名称;
双中,决赛;
double add()const;
STD:istream read(STD:istream);
};
istream信息:读取(istream in){
名义上是期末考试;
返回;
}
double info:add() const {
return :add(mid,final);
}
Add (double mid,double final){//如果分数为零,则给出错误。
if(mid==0 final==0){
throw domain_error(grade为null );
}
return add(交易(中期,最终));
}
双定(双中,双决赛){//计算总分
int grade=mid * 0.3 final * 0.7
返回等级;
}
加(双等级){//把分数从150改成100。
int final _ grade=grade/150 * 100;
返回final _ grade
}
int main(int argc,char** argv)
{
信息记录;
record . read(CIN);
cout record . add();
返回0;
}
2.2.2静态成员函数静态成员函数不能对类的对象进行操作。与其他成员函数不同,静态成员与类相关联,而不是与特定类型的对象相关联。因此,静态成员函数不能访问类型对象的非静态数据成员。
1静态成员函数的地址可以用普通函数指针存储,而普通成员函数的地址需要用类成员函数指针存储。2静态成员函数不能调用类的非静态成员。因为静态成员函数不包含这个指针。3静态成员函数不能同时声明为虚函数、常量函数和易变函数。静态数据成员:
类1中的静态数据成员:实际上是类域中的全局变量。因此,静态数据成员的定义(初始化)不应该放在头文件中。因为在大多数情况下,这样会导致重复定义的错误。示例:
头文件:
类别基础{
私人:
静态整数计数;
}源文件:
int计数;//定义(初始化)时不受私有和受保护访问的限制。2静态数据成员由类的所有对象共享(包括派生类)。3静态数据成员可以是成员函数的可选参数,普通数据成员不能。4静态数据成员的类型可以是其所属类的类型,普通数据成员不能。普通数据成员只能声明为指针或其类类型的引用。
2.3非成员函数,如输入操作符函数,必须声明为非成员函数,以保持与库规则相同的输入和输出形式。
2.4结构和类结构struct和class class对成员有不同的默认保护方法。
如果使用class,则第一个{和第一个受保护的标识符之间的所有成员都是私有的,
使用struct,则第一个{和第一个保护标识符之间的所有成员都是公共的,
保护标识符:public的成员可以被访问,private的成员只能被类的成员访问。
保护标识符可以以任何顺序出现,也可以多次出现。
例如
课程信息{
公共:
双等级;
};等于
结构{
双等级;
};此外:
课程信息{
std:字符串名称;//默认为私有
//其他私有成员
公共:
double add()const;
//其他公共成员
};等于
结构信息{
私人:
std:字符串名称;
//其他私有成员
公共:
double add()const;
//其他公共成员
}你可以对一个结构或者类做同样的处理操作,用户不看代码是看不出来的。
类或者结构的选择在编程中可以起到很好的作用。一般来说,编程风格是保持结构来表示简单类型,我们希望公开这些类的数据结构。
2.5访问权限
2 . 5 . 1 public的成员可以访问类内外的函数。
2 . 5 . 2 private private的成员只能被类内的成员函数访问。
2.5.3好友好友功能拥有与会员功能相同的访问权限。
友元函数的声明可以添加在类定义中的任何地方:添加在私有标签之后和添加在公共标签之后没有区别。因为友元函数有特殊的访问权限,所以它是类接口的一部分。一般友元函数的所有声明都放在一起作为一个相对独立的组。
2.5.4 protected赋予派生类访问基类中受保护成员的能力,同时可以防止这些成员被该类的其他用户访问。
设计类别3的常见功能
3.1访问器函数访问器函数:我们被允许对一些数据结构有读访问权,但是没有写访问权。
我们经常使用这种数据结构来简单地访问数据,但这次它破坏了程序的封装。
在下面的程序中,除了info的对象成员,其他函数都不能访问对象中的名称。
类别信息{
公共:
istream read(STD:istream);
double add()const;
私人:
std:字符串名称;
双等级;
};为了访问信息中的名称,我们编写了一个访问器函数:
类别信息{
公共:
istream read(STD:istream);
double add()const;
STD:string name()const { return n;}//访问器函数
私人:
STD:string n;
双等级;
};
3.2构造函数构造函数:定义对象的初始化方法。
无法显示和调用构造函数。相反,当一个自定义类型的对象被创建时,一个适当的构造函数将被调用作为它的副作用。
例如,当定义一个字符串或向量时,如果你不指定初始值,你将得到一个空的字符串或向量。两种stringvector类型都允许我们为新对象指定初始值。例如,我们可以指定一个长度或一个数字和一个填充字符。
3.2.1定义一个构造函数是不是一个好习惯:* *确保所有的数据成员在任何时候都有有意义的值。* *为后期(程序员或代码维护人员)检查数据成员的操作不会失败。
如果没有定义构造函数,编译器会为我们合成一个。组合构造函数将初始化数据成员,成员的初始值取决于对象是如何创建的。
如果对象是局部变量。默认情况下,成员将被初始化。
如果有以下三种情况,数据成员将被数值初始化。
1对象用于初始化容器元素;2在映射表中添加一个新元素,对象是这个添加动作的副作用;3定义一个特定长度的容器,对象就是这个容器的元素。总而言之:
1如果对象是内部类型,数值初始化方法会将其设置为零,而默认初始化方法会给它一个为。2如果对象属于一个自定义类型,并且这个自定义类型定义了一个或多个构造函数,那么适当的构造函数将完全控制该类的对象的初始化。否则,该对象只能是未定义任何构造函数的自定义类型。在这种情况下,对象的数值或默认初始化将使用相应的数值或默认值初始化其每个数据成员。如果任何数据成员属于具有自己的构造函数的自定义类型,初始化过程将是递归的。在下面的示例程序中,info类属于第三种情况,这是一种自定义类型,没有构造函数。
如果我们定义一个info变量,n会被初始化为一个空字符串,mid和final会被初始化为一个未定义的值,这意味着它们会在创建时获得的内存区域中保存任何无用的值。这将导致后面定义的一些操作失败。
类别信息{
公共:
istream read(STD:istream);
double add()const;
STD:string name()const { return n;}//访问器函数
私人:
STD:string n;
双中,决赛;
};实际上,我们要设置两个构造函数,一个不带参数,创建一个空的info对象;第二个构造函数引用输入流,从流中读取信息记录,从而初始化该对象。
完美课堂:
类别信息{
公共:
info();//创建空的info对象
info(STD:istream);//读取流以构造对象
istream read(STD:istream);
double add()const;
STD:string name()const { return n;}//访问器函数
私人:
STD:string n;
双中,决赛;
};
3.2.2默认构造函数默认构造函数:没有任何参数的构造函数。
它的工作是确保对象的所有数据成员都被正确初始化。
初始化3.2.2.1程序列表和函数体。创建新的类对象时,将采取以下步骤:
1分配内存以保存该对象;2根据构造函数初始化程序列表初始化对象;3执行构造函数的函数体。实现对象会初始化该对象的所有数据成员。无论这些成员是否出现在构造函数的初始化列表中,构造函数的函数体都可能在以后更改这些值,但初始化程序列表会出现在构造函数的函数体之前。
一般来说,在构造函数的函数体中初始化成员并不是一个非常理想的操作,但最好是显式地为成员指定一个初始化值。除了当我们想用一个类成员初始化另一个类成员的时候,为了避免相互依赖,我们应该在构造函数体中给这些成员赋值,而不是在构造函数初始化器中初始化。
原因是:
1在某些情况下,必须使用初始化程序列表。但是,当成员是引用数据成员、常量数据成员和对象数据成员时,不能赋值,只能初始化。2效率。在函数体中初始化时,通常会同时调用构造函数和复制操作函数。重复的函数调用是对资源的浪费,尤其是当构造函数和赋值操作符分配内存时。在一些大型类中,可能有一个构造函数和一个赋值操作符调用同一个Init函数来分配大量内存空间。在这种情况下,必须使用初始化列表来避免两次不必要的内存分配。使用初始化列表只调用一次构造函数。注意:使用初始化程序列表时,应该按照类中类成员声明的顺序进行初始化,而不是按照初始化列表中的顺序。
对于前面的信息类,我们希望初始化数据表明我们还没有读取记录。
在下面的程序中,
Between:和{是构造函数的初始值设定项,它们命令编译器初始化给定的成员,并在初始化过程中使用出现在相应括号之间的值。
和mid final显示为初始化为0,而n将隐式初始化。
info:info():mid(0),final(0){ }
3.2.2.2显示或隐式定义默认构造函数。默认构造函数有一个默认操作。
如果类中没有定义构造函数,编译器将自动生成一个不带任何参数的默认构造函数。这个自动生成的构造函数以对象本身在初始化过程中采用的方式递归初始化对象的成员数据。
如果上下文需要默认初始化,它将默认初始化数据成员;如果上下题需要数值初始化,就会初始化数据。
如果类中定义了任何构造函数(包括复制构造函数),编译器不会自动为该类生成默认构造函数。
默认构造函数在某些情况下是必要的:其中之一是生成默认构造函数本身。为了默认初始化每个数据成员,要求成员数据有一个对应的默认构造函数。
比如在定义其他构造函数的时候,因为定义了构造函数,所以编译器不会生成默认的构造函数。为了默认初始化,有必要写一个默认的构造函数。
3.2.3带参数的构造函数一般用于读取数据和初始化数据成员。
复制构造函数是一种特殊的构造函数。
3.2.3.1复制构造函数复制构造函数也是一个与类名同名的成员函数。
它用于复制相同类型的现有对象,以初始化新对象。
因此,复制构造函数引用与类本身类型相同的参数。因为我们定义了可复制性的效果,包括产生一个函数参数的副本,所以当参数是引用的时候就会产生问题。此外,因为复制对象不会更改对象的值,所以复制构造函数的参数使用常量引用类型。
通常,复制构造函数用现有对象中的新对象“复制”每个数据元素。有时候,复制操作不仅仅是复制数据的内存,它还可能执行一些其他的操作。
当类中有指针成员时,如果指针的值被复制,则被复制的对象和被复制的对象都指向内存中的相同数据。这将导致任何对象的数据元素的改变影响另一个对象的值。
解决方案是通过值传递将对象作为参数传递,这样复制对象中的操作就不会影响原始对象中的值。
3.2.4构造函数的自动转换类中定义的类型转换包括两种定义:那种其他类型转换为类类型,或者类类型转换为其他类型。
其他类型转换为此类型:通常,类型转换是通过定义只有一个参数的构造函数来定义的。
比如:
类别字符串{
公共:
//生成一个Str对象并用空字符末尾的字符数组初始化它
Str(const char* cp){
std:copy(cp,cp std:strlen(cp),STD:back _ inserter(data));
}
私人:
Vec char数据;
}通过构造函数将const char* type转换为Str。
将此类类型转换为其他类型:通过转换操作函数(函数名是operator加上目标类型名)。
3.3析构函数一个在局部区域创建的对象,在其存在范围之外会被自动删除;动态分配的内存对象只有在我们用delete函数删除它的时候才会被删除。因此,当一个类具有分配内存的能力时,就需要一个构造函数。
它定义了如何删除对象实例。析构函数的函数名是在类名前面加一个波浪前缀~的。析构函数没有参数,也没有返回值。
通过默认析构函数删除指针变量时,指针所指向的对象所占用的内存空间不会被释放。
通常,类不需要析构函数,也不需要定义复制构造函数或赋值运算符函数。
3.4重载运算符函数必须有一个带几个参数的函数名,并指定其返回类型。
将运算符放在运算符之后。
一般来说,运算符的种类(一元运算符或二元运算符)决定了函数需要多少个参数。
如果运算符是函数而不是成员,则函数的参数个数与运算符的操作数一样多;第一个参数必须是左操作数,而第二个参数是右操作数。如果一个运算符被定义为成员函数,那么它的操作数必须是调用该运算符的对象。因此,成员函数的运算符函数比简单运算符函数少一个参数。
3.4.1索引运算符函数必须是成员函数。
索引运算符在数组中定位正确的元素位置,并返回该元素的引用。通过返回的引用,可以修改存储在Vec类型对象中的数据。
返回类型是引用而不是值,避免在容器很大的时候复制容器中的对象,这样不仅浪费时间,还会影响运行速度。
3.4.2赋值运算符必须是成员函数。
一个类可以定义几个不同的赋值操作符(由不同的参数重载),以常量引用类本身作为参数的版本比较特殊。它定义将一个自定义类型值(对象)赋给另一个自定义类型(对象)。
复制操作符必须返回值,所以为它设置一个返回类型。
赋值操作不同于复制构造函数:
1赋值总是删除一个现有值(操作符左边的对象),然后用一个新值(操作符右边的对象)替换它。2.复制时,先创建一个新对象,这样就不需要删除已有的对象了。自我赋值判断要在赋值的时候做。如果不是自赋值,会删除原对象并释放内存,然后复制新对象。
如果去掉这个判断,左操作数对象的元素将被删除,它所占用的内存将被释放。同时,右操作数将被删除,因为左操作数和右操作数指向同一个对象。但是要复制正确的操作对象,会带来灾难。
这通常用于判断该关键字仅在成员函数内部有效,并表示指向该函数所操作的对象的指针。例如,在Vec:operator=函数中,它的类型是Vec*。对于二元运算,如赋值运算,它总是指向左操作数。
Return是对这个的间接引用,从而得到它所指向的对象,然后返回对象的引用。
因为返回了对对象的引用,所以引用所指向的对象在函数返回后仍然存在。如果对本地对象返回引用,可能会发生灾难,函数返回时被引用的对象会被删除,导致引用错误。
在赋值操作中,我们以操作数对象的形式返回对表达式的引用调用,其生命周期比赋值操作长,保证了函数在返回时不会被删除。
示例:
模板类T类Vec{
公共:
//在模板文件的范围内,C允许我们忽略编译器的具体类型名。
Vec运算符=(const Vec);//必须是成员函数
}
模板类T
Vec T Vec T:operator=(const Vec RHS){
如果(rhs!=this){//判断字符赋值。
un create();//删除运算符左测试的数组。
create(rhs.begin()、RHS . end());//从右边的元素复制到左边
}
返回* this
}赋值不是初始化,因为赋值(operator=)总是删除一个旧值,而初始化没有这一步。初始化包括创建一个新对象并赋予它一个初始值。
初始化通常发生在:
1当变量被声明时;2在函数入口处使用函数参数时;3当函数返回值用于函数返回时;4当结构初始化时。例如:
string s= hello//初始化
字符串s2(s.size(), );//初始化
字符串S3;//初始化
s3=s2//赋值
3.4.3输出运算符在判断一个函数是否应该是成员函数时,一般的判断方法是看该函数是否会改变对象的状态。
输入操作符函数当然会改变对象的状态,因为在使用输入操作时,它会将新值重新读入现有对象。
对于二元运算符函数,其左操作数必须是函数的第一个参数,其右操作数必须是函数的第二个参数。如果运算符函数是成员函数,默认情况下,第一个参数(即左操作数)将始终传递给成员函数。
如果将输入运算符用作成员函数,将会导致以下效果:
我们希望的是:
cin的;
//相当于
cin .操作员;它调用对象cin的重载操作符。此行为意味着操作员必须是istream的成员。
由于我们没有权限修改istream的定义,因此无法向其添加此操作。
如果我们使用operator作为Str的成员,那么用户将不得不以如下方式输入Str:
实际效果:
美国运营商(CIN);
//相当于
CIN;这和整个库的语法规则不一样,所以输入函数不能作为类的成员函数。输出函数是一样的。
3.4.4复合加法赋值运算符函数因为=运算符函数会改变运算符的左操作数,所以写成成员函数。
示例:
类别字符串{
公共:
字符串运算符=(常量字符串){
std:copy(s.data.begin(),s.data.end(),STD:back _ inserter(data));
返回* this/间接引用,返回对象
}
}
3.4.5加法运算符函数因为加法运算符函数不改变左右操作数,所以写成非成员函数。
首先定义一个局部变量R,然后将其初始化为S的副本,生成一个新的Str类型对象。在这个初始过程中,使用Str的复制构造函数,然后调用=运算辅助函数,用T连接R,作为R的新值,最后返回R(所以再次调用复制构造函数)作为结果。
字符串运算符(常量字符串s,常量字符串t){
str r=s;
r=t;
return r;
}对于
Str name=韩降雪
Str greeting=你好, name !;相当于以下内容
str temp 1( Hello );//Str:Str(const char*)
Str temp 2=temp1名称;//运算符(常量字符串,常量字符串
字符串temp3(!);//Str:Str(const char*)
Str S=temp2 temp3//operator (const Str,const Str)其中转换类型的构造函数:
类别字符串{
公共:
//生成一个Str对象并用空字符末尾的字符数组初始化它
Str(const char* cp){
std:copy(cp,cp std:strlen(cp),STD:back _ inserter(data));
}
私人:
Vec char数据;
}由于operator函数的参数都是Str类型,所以每次遇到混合类型的表达式,都要调用构造函数将const char* type转换为Str类型,这会导致代码使用大量的临时变量,所以这种方法会消耗大量的内存。实际上,真正的标准库的string类并不依赖于类型转换来添加混合类型的操作数,而是重置加号运算符来为每个可能的操作数类型连接定义一个版本。
3.4.6转换运算函数转换运算函数:函数名运算符加上目标类型名。
示例:
班级学生信息{
公共:
运算符double()const;
//.
}将学生类的对象转换为double类型的变量。转换的具体含义由操作函数的具体定义决定。
例如:假设vs是vector Student_info的一个对象,为了计算所有学生的平均成绩:
vector Student _ info vs
//填充vs
双d=0;
for(int I=0;我=!=vs . size();i){
d=vs[I];//vs[i]自动转换为double类型的值
operation函数在将自定义类型转换为c内置的类型时经常被调用,有时也可以用来将一个类的类型转换为另一个我们没有代码类的类型。
在这两种情况下,我们都不能向目标类添加构造函数,而只能在我们有代码的类中将转换函数定义为类的一部分。
例如:
以下istream调用转换函数。
if(cin x){/*.*/}
//相当于
cin十世;
If(cin){/*.*/}对if语句进行条件语句判断,生成bool值。当使用任何其他数学类型或指针类型的值时,它将首先被转换为bool类型的值。
Istream既不是指针,也不是数学类型,但是标准库定义了从istream类型到void类型的转换,void是指向void类型的指针(通用指针:指向void的指针,不能被间接引用)。标准库总是定义一个方法,通过验证istream:operator void的未使用状态来确定istream是否有效,并返回0或自定义的非零void*值来指示流的状态。
该引用可用于编写使函数成为条件语句的判断条件,
类Str_c{
公共:
//操作功能
运算符void *()const { return is _ not _ empty();}
//字符串长度,不包括\0
size _ type size()const { return avail-d-1;}
私人:
char * d;//指向字符串第一个字符的指针
char * avail//指向最后一个有价值字符的最后两位数字的指针
char * limit//指向字符串最后一个字符的指针
//操作功能
void * is _ not _ empty()const { return size()0?d:0;}
}
//调用
int main(){
Str_c name=韩降雪;
如果(姓名)
cout Hello name !endl
返回0;
}istream类被定义为运算符void*而不是运算符bool,这样编译器可以检测到以下不正确的用法:
int x;
cin十世;//应该是cin xistream类定义为operator类定义为operator bool,那么这个表达式会调用istream:operator bool将cin转换为bool,然后将bool转换为int值,将值左移x,然后丢弃这个结果。
由于被定义为从void*等其他类型转换而来,标准版的标准库允许将istream类型用作判断条件,但不允许将其用作算术值。
类型有时会有这种内存管理缺陷。
如果我们想在空字符的末尾提供从Str类到字符数组类型的转换。
类别字符串{
公共:
运算符char *();
运算符const char *()const;
私人:
Vec字符数据
} Str s;
if stream in(s);//将S转换为int类型,然后打开这个名为S的流,这段代码几乎不可能完成正确的转换,因为需要一个字符数组类型的数据,但是程序提供了一个错误的Vec char类型。即使类型匹配,返回的数据变量仍然会违反Str类的封装。如果在Str类型对象被删除的情况下,用户仍然试图使用指针,指针就会指向一块内存,并释放返回给系统的内存,造成灾难。
这个问题可以通过提供到const char*的转换来解决,但是这种左倾并不能阻止用户删除Str对象,继续使用指向数据的指针。
为了解决这个问题,你可以分配一个新的内存空间,然后将数据复制到这个内存中,然后返回一个指向新分配的内存空间的指针。用户以后只能操作这个新的内存,在不需要的时候删除内容,释放空间。
str s;
if stream in(s);//隐式转换但由于转换可能隐式发生,这是用户没有要删除的指针。因为如果Str有对应的转换运算函数,就会说当S传递给ifstream的构造函数时,Str类型变量会隐式转换成构造函数需要的const char* type参数。这种转换会为存储分配新的空间内存,但不会返回指向新分配的内存的显示指针,因此用户无法释放该内存。
在设计类时,我们不希望用户在编写无害的代码时遇到困难。解决方案是要求用户只显式调用这种类型转换,让用户明白在获取指针副本时会带来的隐患。
3.4.7比较运算函数允许类比较大小。
示例:字符串类的非成员函数:
内联布尔运算符==(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r . c _ str())==0;
}
内联布尔运算符!=(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r.c_str())!=0;
}
内联布尔运算符(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r . c _ str())0;
}
内联布尔运算符=(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r . c _ str())=0;
}
内联布尔运算符(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r . c _ str())0;
}
内联布尔运算符=(const Str_c l,const Str_c r){
return std:strcmp(l.c_str()、r . c _ str())=0;
}
3.4.8设计二元运算符在二元运算符的设计中,参数类型转换的地位非常重要。
如果一个类支持类型转换,那么将这个二元运算符定义为非成员函数是一个好习惯,这样可以保证两个操作数的对称性。如果运算符函数是类的成员函数,则该运算符的左操作数不能是自动转换的结果。因为程序员在写x y这样的表达式时,编译器不会检查整个程序,找出用成员运算符把x转换成类型的可能性。因此,编译器(程序员)必须亲自检查作为非成员函数的操作符函数和x类的操作符成员函数,如果允许转换左操作数,也许可以将操作数转换成这种类型的对象,放在一个临时变量中,最后给这个临时变量赋一个新值。赋值操作完成后,我们无法访问新生成的对象*。因此,就像赋值函数一样,所有的复制操作都必须是类的成员函数。将二元运算符函数定义为成员函数也会引入操作数的不对称性:右边的操作数可能是自动转换的结果,而左边的操作数则不是。对于非运算符函数的左操作数和所有全运算符函数的右操作数:操作数可以是能够转换为指定类型的参数的任何类型。这种不对称对于一个固有的非对称函数如=,不会造成问题,但是在对称操作数的环境下,这种要求会让人困惑,可能会导致错误。所以我们都希望两个操作数完全对称,这就要求我们将操作符函数定义为非成员函数。
4.1三位一体原则在编写一个管理资源(如内存资源)的类时,要特别注意对copy函数的控制。通过,缺省操作对于这个类是不够的。如果不能很好的控制每一个元素,会让用户困惑,最终导致运行时出错。
如果没有定义析构函数,将调用默认析构函数。这个默认的析构函数只会删除对象的指针,但是删除指针并不会释放指向对象的指针所占用的内存,最终会导致内存泄漏。
如果只提供一个析构函数,并且不显示写出复制构造函数和赋值操作符函数,情况可能会更糟。和前面的例子一样,复制构造函数传递指针而不是值,导致两个Vec对象共享一个内存空间。当其中一个对象被删除时,析构函数会释放共享内存空间,任何对释放内存的引用都会导致意想不到的后果。
在其构造函数中具有动态资源分配的类要求该类的每个对象正确处理这些资源。这些类几乎需要一个析构函数来释放资源。如果一个类需要一个析构函数,它几乎肯定需要一个复制构造函数和一个赋值操作符成员函数。
三位一体原则:如果一个类需要一个析构函数,它可能还需要一个复制构造函数和一个赋值运算符成员函数。
对于这三个函数,如果类中没有显式定义这些操作,编译器会自动为该类生成相应的默认版本的函数,并执行一些默认操作。
这些默认函数被定义为一系列递归操作。
每个成员数据根据其对应的类型被复制、分配或删除。如果成员变量是一个类的对象实例,那么在复制、赋值或删除时,会调用相应类的构造函数、赋值运算符函数或析构函数。如果成员变量是C自带的变量类型,那么它们的值在被复制或赋值时会被复制或拷贝,这样的变量在被删除时不需要任何额外的工作,即使其他变量类型是指针。注意:通过默认析构函数删除指针变量时,指针所指向的对象所占用的内存空间不会被释放。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。