Chinaunix首页 | 论坛 | 博客
  • 博客访问: 63345
  • 博文数量: 30
  • 博客积分: 1456
  • 博客等级: 上尉
  • 技术积分: 370
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-08 22:31
文章分类

全部博文(30)

文章存档

2011年(1)

2008年(29)

我的朋友
最近访客

分类: C/C++

2008-12-08 12:30:11

91、成员函数和友元函数的区别

成员函数是类定义的一部分,通过特定的对象来调用。成员函数可以隐式访问调用对象的成员,而无须使用成员操作符。友元函数不是类的组成部分,因此被称为直接函数调用。友元函数不能隐式访问类成员,而必须将成员操作符用于作为参数传递的对象。

92、静态类成员特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享一个静态成员。不能类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句指出了类型,并使用了作用域操作符。

初始化是在方法文件中,而不是在类声明中进行的,因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如过在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。

对于不能在类声明中出事静态数据成员的一种例外情况是,静态数据成员为整型或枚举型const

 

字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。

c++自动提供了下面这些成员函数:

1)默认构造函数,如果没有定义构造函数。

2)赋值构造函数,如果没有定义。

3)赋值操作符,如果没有定义。

4)默认析构函数,如果没有定义。

5)地址操作符,如果没有定义。

如果定义了构造构造函数,c++将不会定义默认构造函数,如果希望在创建对象时显式对它进行初始化,或需要创建对象数组时,则必须显式地定义默认构造函数。这种构造函数没有任何参数,但是可以使用它来设置特定的值。带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

但只能有一个默认构造函数,即不能这样做:

       Klunk(){kunk_ct=0;}

   Klunk(int n=0){kunk_ct=n;) 这将导致二义性。

第二个声明基于构造函数1(没有参数)匹配,也与构造函数2(使用默认参数0)匹配。这将导致编译器发出一条错误消息。

复制构造函数:用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中,而不是常规的复制过程中。复制构造函数原型通常如下:

       Class_name (const Class_name &);

它接受一个指向类对象的常量引用参数。

1)何时调用复制构造函数:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这种情况都可能发生,最常见的情况是将新对象显式地初始化为现有的对象。

比如如下例子:假设motto是一个StringBad的对象则下面4中声明都将调用赋值构造函数:

       StringBad ditto(motto);

       StringBad metoo=motto;

       StringBad also=StringBad(motto);

       StringBad *pStringbad=new StringBad(motto);

其中中间的两个可能会使用复制构造函数直接创建metooalso,也可能使用复制构造函数生成以临时对象,然后将临时对象的内容赋给metooalso,这取决与具体实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。

每当程序生成了对象副本时,编译器都将使用复制构造函数。具体说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。

复制构造函数的功能:

默认的复制构造函数逐个复制非静态成员(成员复制也称浅复制),复制的是成员的值。

下述语句:StringBad sailor=sports;

与下面代码等效

         StringBad sailor;

         sailor.str=sports.str;

         sailor.len=sports.len;

如果成员本省就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态函数不受影响,因为它们属于整个类,而不是各个对象。

 

隐式的复制构造函数是按值进行复制的。隐式的复制构造函数功能相当于sailor.str=sport.str;

这里复制的并不是字符串,而是一个指向字符串的指针,也就是说,将sailor初始化为sports后,得到的是两个指向同一个字符串的指针。当operator<<()函数使用指针来显式字符串时,这不会出现问题。但当析构函数被调用时,这将引发问题。析构函数StringBad释放str指针指向内存,因此释放sailor的效果如下:delete [] sailor.str;

sailor.str指针指向“spinach leaves bowl for dollars",因为它被复制为sports.strsports.str指向的正式上述字符串。所以delete语句将释放字符串"Spinach Leaves Bowl for Dollars"占用的内存。

然后释放的效果如下:delete [] sports.str;

sports.str指向的内存已经被sailor的析构函数释放掉了,这将导致不确定的、可能有害的后果。

所以英使用显式复制构造函数来解决问题:进行深度复制,也就是说,复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。调用析构函数时,都将释放不同的字符串,而不会试图去释放已经被释放的字符串。

可以像如下进行编码:

       StringBad::StringBad(const StringBad & st)

      {

       num_strings++;

         len=st.len;

         str=new char[len+1];

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

         cout << num_strings << ......;

       }

必须定义复制构造函数的原因在与,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。

赋值操作符

c++允许类对象赋值,这事通过自动为类重载赋值操作符实现的。这种操作符如下:

       class_name & class_name::operator=(const class_name &);

它接受并返回一个指向类对象的引用。

1)何时使用赋值操作符:将已有的对象赋给另外一个对象时,将使用重载的赋值操作符:

StringBad headline1("zhlzn");

...

StringBad knot;

knot=headline1;  //assignment operator invoked

初始化对象时,并不一定会使用赋值操作符:

StringBad metoo=knot; //use copy constructor,possibly assignment,too

这里,metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数

这种实现可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值赋值到新对象中。这就是说,初始化总是调用复制构造函数,而使用=操作符时也可能调用复制操作符

赋值操作符的功能:隐式实现对成员进行逐个赋值。如果成员本身就是类对象,则程序将使用为这个类定义的赋值操作符来复制该成员,但静态数据成员不受影响。

赋值问题的解决:提供赋值操作符(进行深度复制)定义。

1)由于目标对象可能引用了以前分配的数据,所以函数应使用delete []来释放这些数据。

2)函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。

3)函数返回一个指向调用对象的引用。

通过返回一个对象,函数可以像常规赋值操作那样,连续进行赋值,即如果S0,S1,S2都是StringBad的对象,则可以编写这样的代码:

       S0=S1=S2;

使用函数表示法时,上述代码为:S0.operator=(S1.operator=(32));

因此,S1.operator=(S2)的返回值是函数S0operator=()的参数。因为返回值是一个指向StringBad对象的引用,因此参数类型是正确的。

如下面代码

StringBad & StringBad::operator=(const String &st)

{

  if(this==&st)

    return *this;

  delete [] str;

  len=st.len;

  str=new char[len+1];

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

  return *this;

}

代码首先检查自我复制,这事通过查看赋值操作符右边的地址(&s)是否与接收对象(this)的地址相同来完成的,如果相同,程序将返回*this,然后结束。如果不同,释放str指向的内存,这事因为稍后将把一个新字符串的地址赋给str。如果不首先使用delete操作符,则上述字符串将保留在内存中。由于程序程序不再包含指向字符串的指针,一次这些内存被浪费掉。

 

使用中括号表示法来访问字符:对于中括号操作符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。

例如:假设opera是一个StrigBad对象:StringBad opera("The Magic Flute");

则对于表达式opera[4],c++将查找名称和特征标与次相同的方法:operator[](int i)

如果找到匹配的原型,编译器将使用下面的函数调用来替代表达式opera[4]:operator[](4)

静态类成员函数:

可以将成员函数声明为静态的(函数声明中必须包含关键字static,如果函数定义是独立的,则其中不能包含关键字static),这样做的后果:

1)不能通过对象调用静态成员函数,实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以是哟个类名和作用域解析操作符来调用它。比如:

原型:static int HowMany(){return num_strings;}

调用它的方式如下:int cout = StringBad::HowMany();

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

同样,也可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为。

92、在构造函数中使用new时应注意的事项

1)如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete

2newdelete必须相互兼容。new对应于deletenew[]对应于delete[]

3)如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么不带。因为只有一个析构函数,因此所有的构造函数都必须与它兼容。不过,可以在一个构造函数中使用new来初始化指针,而在另一个构造函数中将指针初始化为空(NULL0),这是因为delete(无论带还是不带中括号)可用与空指针。

4)应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。

5)具体地说,复制构造函数应分配足够多的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外还应该更新所有受影响的静态类成员。

6)应当定义一个赋值操作符,通过深度复制将一个对象复制给另一个对象。

 

93、有关返回对象的说明

1)返回指向const对象的引用

使用const引用的常见原因是旨在提高效率,如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过传递引用来提高方法的效率。

例如下面代码:

Vector force15060;

Vector force21070;

Vector max;

max=Max(force1,force2);

Vector Max(const Vector &v1,const Vector &v2)

{

  if(v1.magval()>v2.magval())

     return v1;

  else

     return v2;

}

const Vector & Max(const Vector &v1,const Vector &v2)

{

  if(v1.magval>v2.magval())

    return v1;

  else

    return v2;

}

首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本的工作更少,效率更高。其次,引用指向的对象调用函数执行时存在。

2)返回指向非const对象的引用

两种常见的返回值非const对象情形是,重载复制操作符以及重载与cout一起使用的<<操作符。前者这样做旨在提高效率,而后者必须这样做。

operator=()的返回值用于连续赋值:

String s1("Good stuff");

String s2,s3;

s3=s2=s1;

在上述代码中,s2.operator=(s2)的返回值被赋给s3

String s1("Good stuff");

cout << s1 << "is coming!";

在上述代码中,operator<<(cout,s1)的返回值成为一个用于显式字符串"is coming!"的对象。返回类型必须是ostream &,而不仅仅是ostream。如果使用返回类型ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数。

如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数,引用指向的对象将不再存在。这种情况下,应返回像而不是引用。通常,被重载的算数操作符属于这一类。

返回const对象:

比如下面语句:net=force1+force2;

             force1+force2=net;

             cout << (force1+force2=net).magval()<

这里提出三个问题,为何编写这样语句?这些语句为何可行?这些语句有何功能?

这个代码之所以可行,是因为复制构造函数将创建一个临时对象来表示返回值。因此,在前面代码中,表达式force1+force2的结果为一个临时对象。在语句1中,该临时对象被赋给net;在语句2中和3中将临时对象赋给该临时对象。使用该临时对象后,将把它丢弃。

 

在谈newdelete:比如StringBad *favorite=new StringBad(sayings[choice]);

                   delete favorite;              

其中sayings[choice]StringBad的对象。这个例子不是要存储的字符串分配内存,而是为对象分配内存,也就是说,为保存字符串地址的str指针和len成员内存(程序并没有给num_strings成员分配内存,这是因为num_string成员是静态成员,它独立与对象被保存)。创建对象将调用构造函数,后者分配用于保存字符串的内存,并将字符串的地址赋给str,然后,当程序不在需要该对象时,使用delete删除它。对象是单个的,因此,程序使用不带中括号的delete。这将只释放保存str指针和len成员的空间,并不释放str指向的内存,而该任务将由析构函数来完成。

 

在下述情况下析构函数将被调用

1)如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。

2)如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。

3)如果对象是new创建的,则仅当您显式使用delete删除对象时,其析构函数才会调用。

指针和对象小结:

1)使用常规表示法来声明指向对象的指针

2)可以将指针初始化为指向已有的对象

3)可以使用new初始化指针,这将创建一个新对象

4)对类使用new将调用相应类构造函数初始化新创建的对象

5)可以使用->操作符通过指针访问类方法

6)可以对对象指针应用解除引用操作符(*)来获得对象

第一个教训:程序员必须负责管理布局new操作符从中使用的缓冲区内存单元。要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。

比如:

  char buff[500];

  p1=new (buff) StringBad;

  p2=new (buff+sizeof(StringBad)) StringBad("lsdkfj");

第二个教训:如果使用布局new操作符来为对象分配内存,必须确保其析构函数被调用。对于在堆中创建的对象,可以这样做:delete p1;

然而对于使用布局new操作符创建对象,不能这样做。原因在于delete可与常规new操作符配合使用,但不能与布局new操作符配合使用。比如 char *buff=new char[500];

              delete p1;

              delete p2;

指针p2没有收到new操作符返回的地址,因此delete p2将导致运行阶段错误。在另一方面,指针p1指向的地址与buff相同,但buff是使用new[]初始化的,因此必须使用delete[]而不是delete来释放。即使buff是使用new而不是new[]初始化的,delete p1也将释放buff,而不是p1。这事因为new/delete系统知道已经分配的512字节块buffer,但对布局new操作符对该内存做了和中处理一无所知。

delete [] buff 释放了buffer;但它没有为布局new操作符在该内存块中创建对象调用析构函数。

因此解决的方案:显式的使用布局new操作符创建的对象调用析构函数。正常情况下,自动调用析构函数。显式地调用析构函数时,必须指定要销毁的对象。

       p1->~StringBad();

       p2->~StringBad();

       delete [] buff;

94、队列模拟

比如对于这样一个例子

class Queue

{

  private:

    const int qsize;

    Item item;

    ...

}

当执行下面语句时:

Queue::Queue(int qs)

{

  front=rear=NULL;

  items=0;

  qsize=qs;

}

这条语句却无法正常执行,原因在于qsize是常量,所以可以对它进行初始化,但不能给它赋值。从概念上将,调用构造函数时,对象将在括号中的代码执行之前被创建。因此,调用Queue(int qs)构造函数将导致程序首先给4个成员变量分配内存。然后,程序流程进入到括号中,使用常规的赋值方法将值存储到内存中。因此对于数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。c++提供了一种特殊的句法来完成上述工作,它叫做成员初始化列表(memeber initializer list)。成员初始化列表由逗号分隔的初始化列表组成。它位于参数列表的右括号之后、函数体左括号之前。如果数据成员的名称为mdata,并需要将它初始化为val,则初始化器为mdata(val),则下面是正确的写法:

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

       {

         front = rear= NULL;

         items=0;

     }

通常,初值可以是常量或构造函数的参数列表中的参数。并不局限于初始化常量,也可以这样:

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

{

}

只有构造函数可以使用这种初始化列表句法。必须用这种格式初始化非静态const数据成员。必须用这种格式来初始化引用数据成员。

 

定义伪私有方法:这样有两个作用

1)第一,它避免了本来将自动生成的默认方法定义。

2)因为这些方法是私有的,所以不能被广泛使用。

例子:

class Queue

{

  private:

    Queue(const Queue & q):qsize(0){}

    Queue & operator=(const Queue &q)(return *this);

  .....

}

习题:

1)假设String类有下私有成员:

class String

{

  private:

    char *str;

    int len;

    //...

};

a:下述默认构造函数有什么问题?String::String(){}

句法正确,但该构造函数没有将str指针初始化。该构造函数应将指针设置整NULL或使用new[]来初始化它

b:下述构造函数有什么问题?

       String::String(const char *s)

       {

         str=s;

         len=strlen(s);

       }

该构造函数没有创建新的字符串,而只是复制了原有字符串的地址。它应当使用new[]strcpy()

c:下述构造函数有什么问题?

       String::String(const char *s)

       {

         strcpy(str,s);

         len=strlen(s);

       }

它复制了字符串,但没有给它分配存储空间,应使用new char[len+1]来分配适当数量的内存。

2)如果你定义了一个类,其指针成员是使用new初始化的,请指出可能初相的3个问题以及如何纠正这些问题。

首先,当这种类型的对象过其实,对象的成员指针指向的数据仍保留在内存中,这将占用空间,同时不可访问,因为指针已丢失。可以让类析构函数删除构造函数中new分配内存,来解决这种问题。

其次,析构函数释放内存后,如果程序将这样的对象初始化,将复制指针值,但不复制指向的数据,这将使两个指针指向相同的数据。解决方法是,定义一个复制构造函数,是初始化复制指向的数据。

第三,将一个对象赋给另外一个对象也将导致这两个指针指向相同的数据。解决方法是重载赋值操作符,使之复制数据,而不是指针。

3)如果没有显式的提供了方法,编译器将自动生成那些类方法?请描述这些隐式生成的函数的行为。

c++自动提供下面的成员函数

默认构造函数,复制构造函数,赋值操作符,默认析构函数,地址操作符。

默认构造函数不完成任何工作,但使得能够声明数组和未初始化的对象。默认复制构造函数和默认复制操作符使用成员赋值,默认析构函数也不完成任何工作。隐式地址操作符返回调用对象的地址(this指针)

 

阅读(1125) | 评论(0) | 转发(0) |
0

上一篇:C++ primer 笔记 二

下一篇:C++ primer 笔记 四

给主人留下些什么吧!~~