C++ primer plus 第六版中文 第十二章重点内容总结

1.动态内存和类

在类中使用动态分配内存,要深入怎么书写类成员以及,new和delete的工作原理。

Class Stringbad

{

Private:

   Char *str;

   Int len;

   Static int num_strings;

Public:

   Stringbad(const char *s);

   Stringbad();

   ~stringbad();

   Friend std::ostream & operator<<(std::ostream &os,const stringbad &st);

};

 

注意以上示例中,首先它使用char指针(而不是char数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串的长度。其次将num_strings声明为静态存储类,静态类成员有一个特点,无论创建了多少对象,都只创建一个静态类变量副本,类的所有对象共享一个同一个静态成员

 

在cpp实现类方法的文件中,要在内部作用域(类声明之外)直接初始化静态类成员变量,如

Int Stringbad::num_strings=0;//注意不用再次使用static关键字

不能在类声明中初始化静态静态成员变量。因为类声明仅仅描述如何分配内存,但并不分配内存。且静态类成员是单独存储的,而不是对象的组成部分。

 

有一种例外:静态数据成员是const整数类型或者枚举类型,则可以在类声明中初始化。

 

构造函数实现:

Stringbad::stringbad(const char *s)

{

   Len=std::strlen(s);

   Str=new char [len+1];

   Std::strcpy(str,s);

   Num_strings++;

}

初始化对象时,可以给构造函数传递一个字符串指针:

String boston(“boston”);

Strlen返回字符串长度,但不包括末尾的空字符,申请内存时应该+1。并且不能使用str=s;这只保存了地址而没有创建字符串副本。在析构函数中需要delete []str;因为对象过期时,str指针仍被分配,删除对象可以释放对象本身所占的内存,但并不能自动释放属于对象成员的指针指向的内存。

 

关于析构函数何时被调用:

对象生命周期结束被销毁时

Delete指向对象的指针时,或delete指向对象的基类类型指针时,而其基类虚构函数是虚函数时。

对象i是对象o的成员,o的析构函数被调用时,i的析构函数也被调用。

 

对于在全局作用域中定义的对象,它们的构造函数是在文件中所有其他函数(包括main)开始执行之前被调用的(但无法保证不同文件的全局对象构造函数的执行顺序)。对应的析构函数是在终止main之后调用的。

 

自动局部变量的构造函数是在程序的执行到达定义这个对象的位置时调用的,而对应的析构函数是在程序离开这个对象的作用域时调用的(即定义这个对象的代码完成了执行)。每次执行进入和离开自动对象的作用域时,都会调用它的构造函数和析构函数。如果程序调用了exit或abort函数而终止,则不会调用自动对象的析构函数。

 

静态局部对象的析构函数只调用一次,即执行首次到达定义这个对象的位置时。对应的析构函数是在main终止或程序调用exit函数时调用的。

全局对象和静态对象是以创建它们时相反的顺序销毁的。如果程序由于调用了exit函数而终止,则不会调用静态对象的析构函数。最后创建的最先消失。

 

当类对象传值传递给某函数后,那么有可能在该函数结束时,会执行一次析构函数。且关于类对象的初始化在使用动态内存的时候非常讲究:

Stringbad sailor=sports;相当于

Stringbad sailor=stringbad(sports);

即当使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(复制构造函数,因为它创建一个对象的副本)。自动生成的构造函数不知道需要更新静态变量num_string,因此计数方案会乱。

 

2.特殊成员函数

 

C++自动提供了以下这些默认构造函数,默认析构函数,复制构造函数,赋值运算符,地址运算符(在这些函数都没有定义的情况下)

 

默认构造函数:如果未提供任何构造函数,则c++将创建默认构造函数,如Klunt::Klunt(){},编译器将提供一个不接受任何参数,也不执行任何操作的构造函数。这就允许值在初始化时是未知的。若定义了构造函数,那c++将不会定义默认构造函数,同时又希望在创建对象时不用显示初始化,那么就必须显示的定义默认构造函数。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。但注意每个类只能有一个默认构造函数。

 

复制构造函数:用于将一个对象复制到新创建的对象中,用于初始化过程中(包括按值传递参数)一般原型是class_name(const class_name &);

 

何时调用复制构造函数:新建一个对象并将其初始化为同类现有对象,如

Stringbad ditto(motto);

Stringbad metoo=motto;

Stringbad alse=stringbad(motto);

Stringbad *pstringbad=new stringbad(motto);//使用motto初始化一个匿名对象并将新对象的地址赋值给pstring指针。

 

当函数按值传递对象时或者返回对象时,都将使用复制构造函数,按值传递意味着创建原始变量的一个副本,编译器生成临时对象时,也将使用复制构造函数。例如void callme2(stringbad sb);是原型,调用时采用callme2(headline2);程序使用复制构造函数初始化sb,也就是函数原型的形参。按值传递对象将调用复制构造函数,因此因该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间。

 

默认的复制构造函数的功能:默认的复制构造函数逐个复制非静态成员的值(成员复制也成为浅复制。)静态函数不受影响,因为它们属于整个类而不是各个对象。注意默认复制构造函数不说明行为不指出创建过程,也不会更新静态变量,但析构函数更新了计数,并且在任何对象过期时都将被调用,而不管对象是如何创建的。解决这个问题可以创建一个对计数进行更新的显示复制构造函数,而不再仅仅在构造函数中更新计数。

Stringbad::Stringbad (const Stringbad &s)

{

   Num_strings++;

}

即如果类中包含这样的静态数据成员,即其值将在新对象创建时发生变化,那么应该提供一个显示复制构造函数来处理计数问题。

 

另一个重要问题,隐式复制构造函数是按值进行复制的,相当于

Sailor.str=sport.str;而str是一个char*类型。即这里复制的并不是字符串,而是一个指向字符串的指针。即最后得到的是两个指向同一个字符串的指针,当operator<<()函数使用指针来显示字符串时并不会出现问题,但当析构函数被调用时将引发问题。析构函数sringbad释放str指针指向的内存,因此释放sailor的效果如下:
delete [] sailor.str;即释放了该指针指向的内存,同时sports.str也同样指向该内存块,所以当再次使用析构sports对象的时候,其str指针指向的内存已经被释放,因此就会发生大错误。

 

解决类设计中的这种问题的方法是进行深度复制。也就说复制构造函数应当复制字符串并将副本的地址赋值给str成员,而不仅仅是复制字符串的地址。这样每个对象都有自己的字符串,调用析构函数将释放不同的字符串。如下:

Stringbad::Strinbbad(const stringbad &s)

{

   Num_strings++;

   Len=s.len;

   Str=new char[len+1];

   Std::strcpy(str,s.str);

  

}

如果类中包含使用new初始化的成员,应当定义一个复制构造函数。以复制指向的数据而不是指针,这里成为深度复制。而浅复制(成员复制)仅仅复制指针值,并不会深入挖掘以复制指针引用的结构。

 

赋值运算符:默认的复制运算符,c++允许为类对象赋值,这是通过自动为类重载赋值运算符实现的。运算符原型如下:

Class_name &class_name ::operator=(const class_name &);它接受一个类对象的引用,并且返回一个类对象的引用。

 

赋值运算符的功能以及何时使用它:

将已有的对象赋给另一个对象时,将使用重载的赋值运算符。

Stringbad headline1(“hahah”);//使用重载的赋值运算符

Stringbad knot;

Knot=headline1;//初始化对象时并不一定使用赋值运算符

Stringbad metoo=knot;//使用复制构造函数,可能的步骤是使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制大盘新对象中,也就是初始化总是会调用复制构造函数,而使用=也允许调用赋值运算符。

 

与复制构造函数类似,赋值运算符的隐式实现也对成员进行逐个复制。静态数据成员不受影响。因此赋值运算符的隐式实现也是浅复制,这将导致重复删除一块内存的错误(与复制构造函数的浅复制类似)。解决方法仍然是提供深度复制。

 

解决默认赋值运算符的赋值问题:

 

由于目标对象可能引用了以前分配的数据,所以函数应使用delete[],来释放这些数据。函数应当避免将对象赋值给自身,否则给对象赋值前,释放内存操作可能删除对象的内容。含税返回一个指向调用对象的引用(这个操作可使得像常规赋值操作那样进行连续赋值)。如:

Stringbad & stringbad::operator=(const stringbad &st)

{

   If(this==&st)

      Return *this;

   Delete [] str;

   Len=st.len;

   Str=new char [len+1];

   Std::strcpy(str,st.str);

   Return *this;

}

赋值操作并不创建新的对象,因此不需要调整静态数据成员的值。

 

3.改进后的新string类

注意若在默认构造函数中使用new类分配内存,那么析构函数中像哟delete释放,且默认构造函数中str=new char[1];str[0]=’\0’;而不是str=new char,因为前者与析构函数的delete 【】str兼容。注意delete与new初始化的指针和空指针(str=0,str=nullptr)兼容,其他形式的指针不兼容。

 

对于中括号运算符来说,city[0]中第一个操作数是city,第二个操作数是0.

假设

String opera(“the magic flute”);则对于opera【4】,c++将查找名称和特征标与此相同的方法。

String::operator[](int i)

即用下面的函数调用替代原来的调用

opera.operator[](4);

char &string::operator[](int i)

{

   Return str[i];

}

在重载的时候,c++将区分常量和非常量函数的特征标。常量对象不能调用非常量方法,因此可以在这时提供一个const函数版本。

 

静态成员函数:

静态成员函数声明必须包含关键字static,若函数定义是独立的,则其中不能包含关键字static。不能通过对象调用静态成员函数,静态成员函数甚至不能使用this指针,若其在公有部分声明则可以使用类名和和作用域解析符来调用它,如可以给string类添加一个静态函数成员howmany(),在类声明中添加如下原型:

Static int howmany(){return num_strings;}

调用它的方式

Int count=string::howmany();

由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。不能访问其他私有成员。

 

进一步重载赋值运算符:
string name;

Char temp[40];

Cin.getline(temp,40);

Name=temp;

最后一条语句需要经过三步骤,首先使用构造函数string(const char*)来创建一个临时的string对象,其中包含temp的字符串副本。再使用string&string::operator=(const string&)函数将临时对象中的信息复制到name对象中。程序调用析构函数删除临时对象。为提高效率,使之能够直接使用常规字符串,下面是一种可能的实现。

String &string::operator=(const char *)

{

   Delete []str;

   Len=std::strlen(s);

   Str=new char[len+1];

   Std::strcpy(str,s);

   Return *this;

}

必须释放str指向的内存,并为新的字符串分配足够的空间。

 

3.在构造函数中使用new时应注意的事

 

若在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。

 

new和delete必须互相兼容,new对应于delete,new【】对应于delete【】。

 

如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号要么都不带。然而可以在一个函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空。因为delete(无论带不带中括号)都可以用于空指针。

 

应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。赋值构造函数应该分配足够的空间来存储复制的数据,并复制数据而不是仅仅复制数据的地址。另外还应该更新所有可能受影响的静态类成员。

 

应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象,检查自我赋值的情况,释放程远指针以前指向的内存,复制数据而不仅仅是数据的地址。并返回一个指向调用对象的引用。

 

包含类成员的类的逐成员复制:

Class Magazine

{

Private:

   String title;

   string publisher;

};

String和string都是动态内存分配,如果将一个magazine对象复制给另一个magazine对象的时候,逐成员复制将采用成员类型定义的复制构造函数和赋值运算符。也就是说复制成员title时将使用String的复制构造函数,而将成员title赋值给另一个magazine对象,将使用String 的赋值运算符。

 

4.返回对象的说明

有三种返回方式:可以返回指向对象的引用,指向对象的const引用,或者const对象。

 

返回指向const对象的引用:可以提高效率,如果函数返回传递给它的对象,可以通过返回引用来提高其效率。首先返回类对象的话将调用复制构造函数,而返回引用不会。其次,引用指向的对象应该在调用函数执行时存在。选择按引用返回,必须遵照返回的对象的类型写函数标头。

 

返回指向非const对象的引用:两种常见是重载赋值运算符与cout一起使用的<<运算符。前者是为了提高效率,而后者必须这样做。前者用于连续赋值的时候,后面的赋值操作可以返回类对象或者类对象的引用,而引用效率更高。但是operator<<()的返回值用于串接输出时:

String s1(“good struff”);

Cout<

Operator<<(cout,s1)的返回值成为一个用于显示字符串的“is coming”的对象,返回类型必须是ostream,而不能是ostream。因为ostream没有公有的复值函数,直接返回对象的时候需要调用复制构造函数。

 

返回对象:如果返回对象是被调用函数中的局部变量,则不应该按引用方式返回它。因在在被调用函数执行完毕时,局部对象将调用其析构函数。通常被重载的算数运算符属于这一类。

Vector Vector::operator+(const vector &b) const

{

   Return Vector(x+b.x,y+b.y);

}

返回const对象:
在上面的返回对象时,net=force1+force2;和force1+force2=net都能执行,都用赋值构造函数创建一个临时对象,一个赋值给net后消失,另一个被net值覆盖然后消失。为了避免这种情况,使用const Vector返回类型。则语句1仍然合法,但2则报错。

 

总之如果方法或者函数要返回局部对昂,则应选择返回对象,而不是指向对象的引用。在这种情况下将使用复制构造函数来生成返回的对象。如果方法或者函数要返回一个没有公有复制构造函数的类的对象的时候,它必须返回一个指向这种对象的引用。当两者都可以选的时候,那么应该选择引用。提高效率。

 

5.使用指向对象的指针

通常如果class_name 是类,value的类型为type_name,则下面的语句。

Class_name *pclass=new class_name(value);

将调用如下的构造函数,class_name(type_name);

这里可能还有一些琐碎的转换:class_name(const Type_name &);

另外如果不存在二义性,则将发生由原型匹配导致的转换(如int到double)

下面的初始化方式将调用默认构造函数

Class_name *ptr=new class_name;

 

上述例子中其实在两个层面使用了new和delete。首先它使用new为创建的每一个对象的名称字符串分配内存空间。这是在构造函数中完成的,因为字符串是一个字符数组,所以析构函数使用的是带中括号的delete。当对象被释放时用于存储字符串内容的内存将自动释放,另外使用new来为整个对象分配内存。

String *favorite=new string(saying[choice]);

这不是为要存储的字符串分配内存,而是为对象分配内存。为保存字符串地址的str指针和len成员分配内存。对象是单个的,因此程序使用不带中括号的delete。这将只释放保存str指针和len成员的空间,并不释放str指向的内存,这个任务由析构函数完成。如下:

Class act{…};

Act nice;

Int main()

{

   Act *pt =new act;

   {

   Act up;

}//执行到定义代码块末时,将调用自动对应up的析构函数。

   Delete pt;//对指针调用delete,将调用动态对象*pt的析构函数

}整个程序结束时将调用静态对象nice的析构函数。

 

使用对象的总结:

声明指向类对象的指针:string *glamour;

将指针初始化为已有的对象:string *first=&sayings[0];

使用new和默认构造函数对指针进行初始化:string *gleep=new string;

使用new和string(const char*)类构造函数对指针进行初始化。

String *glop=new string(“my my my”);

使用new和string(const string&)类构造函数对指针进行初始化。

String *favorvate=new string(saying[choice]);

使用->通过指针访问类方法:

If(saying[i].length()length());

使用*解除引用操作符从指针获得对象:
if(saying[i]<*first)

 

比如 string *pveg=new string (“hahaha”);过程首先是

1.为对象分配内存;开辟地址为2400,存放str指针和len成员

2.调用类构造函数,string(const char*)它为“hahaha”分配空间,假设地址为2000,将“hahaha”复制到分配的内存单元中(即相当于拷贝hahaha的副本)。再将“hahaha”的地址赋给str,将值19赋给len。更新num_strings。

3。创建pveg变量

4.将新对象的地址赋给pveg变量。

 

注意定位new运算符:

要使用不同的定位内存单元,程序员需要提供两个位于缓冲区的不同地址。并确保两个内存单元不重叠。如

Pc1=new(buffer) justtesting;

Pc3=new(buffer+sizeof(justtestung))justtesting(“bad idea”,7);

其中指针pc3相对于pc1的偏移量是相对于pc1的偏移量为justingtest对象的大小。

 

另外注意delete并不能与定位new运算符配对使用!对于如上定位运算符来说,delete[]buffer;释放使用常规运算符分配的整个内存块,但是它没有为定位new运算符在该内存块中创建的对象调用析构函数。此时就需要显示调用析构函数,指定销毁对象。

Pc3->~justtesting();

Pc1->~justttesting();

注意对于使用定位new运算符创建的对象,应该以创建顺序相反的顺序进行删除。

 

7设计一个银行.队列类

队列是一种抽象的数据类型(ADT),可以存储有序的项目序列。新项目被添加在队尾,并可以删除队首的项目,队列有点像栈,但是栈是后进先处(LIFO),而队列是先进先出(FIFO)的。实现银行队列问题,需要删除队首,添加队尾,用数组比较麻烦还需要更新索引,链表能够很好的满足队列的需求。链表由节点序列构成,每一个节点都包含要保存到链表中的信息以及指向下一个节点的指针。对于这里的队列来说,数据部分是一个item类型的值。因此可以使用下面的结构来表示节点。

Struct Node

{

   Item item;

   Struct Node *next;

};

需要一个单向链表,即每一个节点只包含一个指向其他节点的指针。大概还需要一个指向队首和队尾的指针,追踪所用的最大项目书和当前项目数。注意:在类声明中声明的结构,类或者枚举被称为是嵌套在类中,其作用域为整个类。如果类声明是在类的私有部分进行的,则只能在这个类使用被声明的类型,如果是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。

 

调用构造函数时,对象将在括号中的代码执行之前被创建(此时还未执行任何代码,但对象已经创建,而const和引用都只能在创建时初始化,const不能被赋值)。因此当需要在构造函数中修改const成员时,必须在执行到构造函数体之前,即创建对象时进行初始化。C++提供了一种特殊语法完成上述工作:成员初始化列表。成员初始化列表由逗号分隔的初始化列表组成(前面代冒号)。它位于参数列表的右括号之后,函数图左括号之前。如果数据成员的名称是mdata,并想将它初始化为val。则初始化器为mdata(val)。如下:

Queue::Queue(int qs):qsize(qs)

{

   Front=rear=NULL;

   Items=0;

}

通常初值可以是常量或者构造函数的参数列表中的参数。可以将queue类的构造函数写成如下格式:

Queue::Queue(int qs):qsize(qs),front(NULL),rear(NULL),items(0)

{

}

只有构造函数可以使用这种初始化列表语法,对于const类成员必须使用这种语法,并且要求是非静态const成员。另外被声明为引用的类成员也必须使用这种语法。数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序有关。

 

注意,当不想使用默认构造函数和默认赋值运算符时,并且暂时未设计可使用的版本时,可以将默认构造函数和默认赋值运算符的声明放置在类的私有部分。避免了本来将自动生成的默认方法定义,且因为这些方法是私有的,所以不能被广泛使用。与其面对无法预料的运行故障,不如得到一个易于跟踪的编译错误。

你可能感兴趣的:(c++)