类基本概念1

先给出一个类的定义例子:

[cpp]  view plain copy
  1. class Sales_item  
  2. {  
  3. private:  
  4.     std::string isbn;  
  5.     unsigned units_sold;  
  6.     double revenue;  
  7.   
  8. public:  
  9.     double ave_price() const;  
  10.     bool same_isbn(const Sales_item &rhs) const  
  11.     {  
  12.         return isbn == rhs.isbn;  
  13.     }  
  14.   
  15.     Sales_item():units_sold(0),revenue(0) {}  
  16. };  
  17.   
  18. double Sales_item::ave_price() const  
  19. {  
  20.     if (!revenue)  
  21.     {  
  22.         return 0;  
  23.     }  
  24.     return revenue / units_sold;  
  25. }  


一、类定义

所有成员必须在类的内部声明,一旦类定义完成之后,就没有任何方式可以增加成员了。

构造函数一般使用一个构造函数初始化列表,来初始化对象的数据成员。

构造函数初始化类表由成员名和带括号的初始值组成,跟在构造函数的形参表之后,并以冒号(:)开头。注意这里是定义,而在函数里面定义不是真正意义的定义,而是赋值操作,对于const和引用变量,必须使用初始化列表,之后会做具体解释)

在类内部定义的函数默认为内联函数(inline)

而在类的外部定义的函数必须指明它们是在类的作用域中。

 就const关键字加在形参表之后,就可以将成员函数声明为常量:const必须同时出现在声明和定义中,若只出现在一处,则会出现编译时错误!

函数定义为const的作用是表示函数内部不能对成员变量的值进行修改


二、数据抽象和封装


  数据抽象是一种依赖于接口和实现分离的编程技术:类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节

    封装是一项将低层次的元素组合起来形成新的、高层次实体的技术!函数和类都是封装的具体形式。其中类代表若干成员的聚集。大多数(设计良好的)类类型隐藏了实现该类型的成员。

classstruct的区别:】

   如果类是用struct关键字定义的,则在第一个访问标号之前的成员是公有的;如果是用class定义,则在第一个访问标号之前的成员是私有的!

【建议:具体类型和抽象类型】

    并非所有类型都必须是抽象的。标准库中的pair类就是一个实用的、设计良好的具体类而不是抽象类,具体类会暴露而非隐藏其实现细节。

    但是具体类通常还有成员函数。特别地,如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。类的使用都也可以初始化或赋值数据成员,由类来做更不易出错

【关键概念:数据抽象和封装的好处】

    1)避免类内部出现无意的、可能破坏对象状态的用户级错误。

    2)随着时间的推移,可以根据需求改变或缺陷报告来完善类实现,而无需改变用户级代码。


三、关于类定义

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     //...  
  5.     typedef std::string::size_type index;  
  6.   
  7. private:  
  8.     std::string contents;  
  9.     index cursor;  
  10.     index height,width;  
  11. };  

【类型别名】

在类定义中可以使用别名,将index定义放在public部分,外部用户也可以使用,但是在外部使用的时候必须加上类作用域符号也就是需要完成的Screen::index  

【成员函数重载】

 成员函数只能重载本类的其他成员函数。

   重载的成员函数和普通函数应用相同的规则:两个重载成员的形参数量和类型不能完全相同。调用非成员重载函数所用到的函数匹配过程也应用于重载成员函数的调用。

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     typedef std::string::size_type index;  
  5.     //一下两个函数构成重载  
  6.     char get() const  
  7.     {  
  8.         return contents[cursor];  
  9.     }  
  10.     char get(index ht,index wd) const;  
  11.   
  12. private:  
  13.     std::string contents;  
  14.     index cursor;  
  15.     index height,width;  
  16. };  

使用:

[cpp]  view plain copy
  1. Screen myScreen;  
  2. char ch = myScreen.get();  
  3. ch = myScreen.get(0,0);  
【inline函数】

 在类内部定义的成员函数,默认就是inline函数。但是也可以显式的将成员函数指定为inline

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     typedef std::string::size_type index;  
  5.     char get() const  
  6.     {  
  7.         return contents[cursor];  //默认就是inline函数  
  8.     }  
  9.     inline char get(index ht,index wd) const;  
  10.     index get_cursor() const;   
  11. private:  
  12.     std::string contents;  
  13.     index cursor;  
  14.     index height,width;  
  15. };  
  16.   
  17. //已经在类体中声明为inline了,就没必要再次声明  
  18. char Screen::get(index r,index c) const  
  19. {  
  20.     index row = r * width;  
  21.     return contents[row + c];  
  22. }  
  23. //即使没有在类体中声明,也可以在外面补上...  
  24. inline Screen::index Screen::get_cursor() const  
  25. {  
  26.     return cursor;  
  27. }  

【最佳实践】

    像其他inline一样,inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中


四、类声明和类定义

  在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的

  可以声明一个类而不定义它:

[cpp]  view plain copy
  1. //前向声明  
  2. class Screen;  

 在声明之后、定义之前,类Screen是一个不完全类型,即:已知Screen是一个类型,但不知道包含哪些成员。

 不完全类型(incompletetype)只能以有限方式使用不能定义该类型的对象不完全类型只能用于定义指向该类型的指针引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。

 在创建类的对象之前,必须完整的定义该类。必须是定义类,而不是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样的,在使用引用或指针访问类的成员之前,必须已经定义类


五、类对象

 定义一个类时,也就是定义了一个类型。一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配内存空间,但(一般而言)定义类型时不进行存储分配

    通过类定义出的对象都具有自己的类数据成员的副本。修改其中一个对象不会改变其他该类对象的数据成员。


1、定义类类型的对象

定义了一个类类型之后,可以按以下两种方式使用。

   1)将类的名字直接用作类型名。

   2)指定关键字classstruct,后面跟着类的名字:

[cpp]  view plain copy
  1. Screen scr;  
  2. //两条语句作用相同  
  3. class Screen scr;  

【最佳实践:】

    通常,将对象定义成类定义的一部分是个坏主意!!!这样做,会使所发生的操作难以理解。对读者而言,将两个不同的实体(类和变量)组合在一个语句中,也会令人迷惑不解。


六、this指针

 在前面提到过,成员函数具有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含形参命名为this,与调用成员函数的对象绑定在一起。

成员函数不能定义this形参,而是有编译器隐含地定义。成员函数可以显式的使用this指针,但不是必须这么做。

  有一种情况下,我们必须显式使用this指针:当需要将一个对象作为整体引用而不是引用对象的一个成员时。

    比如在类Screen中定义两个操作:setmove,可以使得用户将这些操作的序列连接成一个单独的表达式:

[cpp]  view plain copy
  1. myScreen.move(4,0).set('#');  //<前两个语句相当于是返回了一个对象
  2. //其等价于  
  3. myScreen.move(4.0);  
  4. myScreen.set('#');  

  在单个表达式中调用move和 set操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     Screen &move(index r,index c);  
  5.     Screen &set(char);  
  6.     Screen &set(index,index,char);  
  7. };  

这样,每个函数都会返回调用自己的那个对象。使用this指针可以用来访问该对象:

[cpp]  view plain copy
  1. Screen &Screen::set(char c)  
  2. {  
  3.     contents[cursor] = c;  
  4.     return *this;  
  5. }  
  6.   
  7. Screen &Screen::move(index r,index c)  
  8. {  
  9.     index row = r * width;  
  10.     cursor = row + c;  
  11.     return *this;  
  12. }  

 在普通的const成员函数,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。

    不能从const成员函数返回指向类对象的普通引用const成 员函数只能返回*this作为一个 const引用。

    我们可以给Screen类增加一个const成员函数:display操作。如果将display作为 Screen的 const成员,则 display内部的 this指针将是一个constScreen* 型的const。然而:

[cpp]  view plain copy
  1. myScreen.move(4,0).set('#').display(cout);  //OK  非const成员可以进行const函数的调用
  2. myScreen.display().set('*');    //Error   不能从const成员函数返回指向类对象的普通引用

问题在于这个表达式是在由display返回的对象上运行set。该对象是const,因为display将其对象作为const返回。我们不能在const对象上调用set

基于const的重载

    为了解决以上问题,我们必须定义两个display操作:一个是const,一个不是const。基于成员函数是否为const,可以重载一个成员函数;同样的,基于一个指针形参是否指向const,也可以重载一个函数。非const对象可以使用任一成员,但非const版本是一个更好的匹配。

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     //As before  
  5.   
  6.     Screen &display(std::ostream &os)  
  7.     {  
  8.         do_display(os);  
  9.         return *this;  
  10.     }  
  11.     const Screen &display(std::ostream &os) const  
  12.     {  
  13.         do_display(os);  
  14.         return *this;  
  15.     }  
  16.   
  17. private:  
  18.     void do_display(std::ostream &os) const  
  19.     {  
  20.         os << contents;  
  21.     }  
  22.     //As before  
  23. };  

调用:

[cpp]  view plain copy
  1. Screen myScreen(5,3);  
  2. const Screen blank(5,3);  
  3. myScreen.set('#').display(cout);    //调用非const版本  
  4. blank.display(cout);                //调用const版本  

可变数据成员

    有时,我们希望类的数据成员(甚至是在const成员函数中)可以修改。这可以通过将它们声明为mutable来实现。

    可变数据成员永远都不能为const,甚至当它们是const对象的成员时也如此。因此,const成员函数可以改变mutable成员。

[cpp]  view plain copy
  1. class Screen  
  2. {  
  3. public:  
  4.     //...  
  5.   
  6. private:  
  7.     mutable size_t access_ctr;  
  8.   
  9.     //使用access_ctr来跟踪Screen成员函数的调用频度  
  10.     void do_display(std::ostream &os) const  
  11.     {  
  12.         ++ access_ctr;      //OK  
  13.         os << contents;  
  14.     }  
  15. };  

【建议:用于公共代码的私有实用函数】

  do_display函数并不复杂,但是我们仍然将他写成一个函数的原因

    1)一般愿望是避免在多个地方编写同样的代码

    2display操作预期会随着类的演变而变得复杂。当涉及到的动作变得更复杂时,只在一处而不是两处编写这些动作有更显著的意义

    3)很可能我们会希望在开发时给do_display增加调试信息,这些调试信息将会在代码的最终成品版本中去掉。如果只需要改变一个do_display的定义来增加或删除调试代码,这样做将更容易。 4这个额外的函数调用不需要涉及任何开销。我们使do_display成为内联的,所以调用do_display与将代码直接放入display操作的运行时性能应该是相同的


你可能感兴趣的:(C++,类,指针,Const)