类(上)

一个类:

class Sales_item {
public://程序的所有部分都可以访问带有 public 标号的成员。类型的数据抽象视图由其 public 成员定义。
    // operations on Sales_item objects
    double avg_price() const;
    bool same_isbn(const Sales_item &rhs) const//const 成员不能改变其所操作的对象的数据成员。
        { return isbn == rhs.isbn; }
    //构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员
    Sales_item(): units_sold(0), revenue(0.0) { }
private://使用类的代码不可以访问带有 private 标号的成员。private 封装了类型的实现细节。
    std::string isbn;
    unsigned units_sold;
    double revenue;
};

double Sales_item::avg_price() const
{
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}

类背后蕴涵的基本思想是数据抽象封装。数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。类设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。相反,使用一个类型的程序员仅需了解类型的接口,他们可以抽象地考虑该类型做什么,而不必具体地考虑该类型如何工作。

函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。

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

  • 具体类型和抽象类型

并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。一些类,例如 pair,确实没有抽象接口。pair 类型只是将两个数据成员捆绑成单个对象。在这种情况下,隐藏数据成员没有必要也没有明显的好处。在像 pair 这样的类中隐藏数据成员只会造成类型使用的复杂化。尽管如此,这样的类型通常还是有成员函数。特别地,如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。类的使用都也可以初始化或赋值数据成员,但由类来做更不易出错。

  • 成员函数可以被重载
class Screen {
public:
    typedef std::string::size_type index;
    // return character at the cursor or at a given position
    char get() const { return contents[cursor]; }
    char get(index ht, index wd) const;
    // remaining members
private:
    std::string contents;
    index cursor;
    index height, width;
};

与任意的重载函数一样,给指定的函数调用提供适当数目和/或类型的实参来选择运行哪个版本:

Screen myscreen;
     char ch = myscreen.get();// calls Screen::get()
     ch = myscreen.get(0,0);  // calls Screen::get(index, index)
  • 显示指定inline成员函数

在类内部定义的成员函数,例如不接受实参的 get 成员,将自动作为 inline 处理。也就是说,当它们被调用时,编译器将试图在同一行内扩展该函数。也可以显式地将成员函数声明为 inline

 class Screen {
     public:
         typedef std::string::size_type index;
         // implicitly inline when defined inside the class declaration
         char get() const { return contents[cursor]; }
         // explicitly declared as inline; will be defined outside the class declaration
         inline char get(index ht, index wd) const;
         // inline not specified in class declaration, but can be defined inline later
         index get_cursor() const;
         // ...
      };
     // inline declared in the class declaration; no need to repeat on the definition
     char Screen::get(index r, index c) const
     {
         index row = r * width;    // compute the row location
         return contents[row + c]; // offset by c to fetch specified character
     }
     // not declared as inline in the class declaration, but ok to make inline in definition
     inline Screen::index Screen::get_cursor() const
     {
         return cursor;
     }

可以在类定义体内部指定一个成员为inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline 都是合法的。在类的外部定义 inline 的一个好处是可以使得类比较容易阅读。

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

  • 类的声明与定义

一旦遇到右花括号,类的定义就结束了。并且一旦定义了类,那以我们就知道了所有的类成员,以及存储该类的对象所需的存储空间。可以声明一个类而不定义它:

class Screen; // declaration of the Screen class

这个声明,有时称为前向声明(forward declaraton),在程序中引入了类类型的 Screen。在声明之后、定义之前,类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样地,在使用引用或指针访问类的成员之前,必须已经定义类。

  • 为类的成员使用类声明

只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用

因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:

class LinkScreen {
         Screen window;
         LinkScreen *next;//类大小不知道,所以不能定义类的对象;但一个指针的大小是固定的,所以可以定义不完全类的指针
         LinkScreen *prev;
     };

类的前身声明一般用来编写相互依赖的类

  • 何时使用this指针

尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用

某种类可能具有某些操作,这些操作应该返回引用(即调用这个成员函数的对象本身),比如Screen 类只有一对 get 操作。逻辑上,我们可以添加下面的操作。

一对 set 操作,将特定字符或光标指向的字符设置为给定值。一个 move 操作,给定两个 index 值,将光标移至新位置。

理想情况下,希望用户能够将这些操作的序列连接成一个单独的表达式:

// move cursor to given position, and set that character
     myScreen.move(4,0).set('#');

这个语句等价于:

myScreen.move(4,0);
     myScreen.set('#');

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

class Screen {
     public:
          // interface member functions
          Screen& move(index r, index c);
          Screen& set(char);
          Screen& set(index, index, char);
          // other members as before
     };

注意,这些函数的返回类型是 Screen&,指明该成员函数返回对其自身类类型的对象的引用。每个函数都返回调用自己的那个对象。使用 this 指针来访问该对象。下面是对两个新成员的实现:

Screen& Screen::set(char c)
     {
         contents[cursor] = c;
         return *this;
     }
     Screen& Screen::move(index r, index c)
     {
         index row = r * width; // row location
         cursor = row + c;
         return *this;
     }

函数中唯一需要关注的部分是 return 语句。在这两个操作中,每个函数都返回 *this。在这些函数中,this 是一个指向非常量 Screen 的指针。如同任意的指针一样,可以通过对 this 指针解引用来访问 this 指向的对象

  •  从const成员函数返回this

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

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

例如,我们可以给 Screen 类增加一个 display 操作。这个函数应该在给定的 ostream 上打印 contents。逻辑上,这个操作应该是一个 const 成员。打印 contents 不会改变对象。如果将 display 作为 Screenconst 成员,则 display 内部的 this 指针将是一个 const Screen* 型的 const

如果 display 是一个 const 成员,且它的返回类型是 const Screen&。这个设计存在一个问题。如果将 display 定义为 const 成员,就可以在非 const 对象上调用 display,但不能将对 display 的调用嵌入到一个长表达式中。下面的代码将是非法的:

Screen myScreen;
     // this code fails if display is a const member function
     // display return a const reference; we cannot call set on a const
     myScreen.display().set('*');

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

而这样则是可行的:

 // move cursor to given position, set that character and display the screen
     myScreen.move(4,0).set('#').display(cout);

为了解决这个问题,我们必须定义两个 display 操作:一个是 const,另一个不是 const基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const(而非指针本身是否是const),可以重载一个函数。const 对象只能使用 const 成员因为,在使用非const成员函数时,隐含的传递了一个参数this,它是一个指向非const对象的const指针,而当你定义一个const对象,调用一个非const成员函数时,这个成员函数的this指针指向的就是一个const对象,const对象要求不可以被改变,而this指针又是指向非const对象的指针,可以改变它所指向的对象,矛盾出现了。所以,如果const对象使用的是const成员函数,那么隐含传递的是一个指向const对象的const指针,这个指向const对象的const指针接收的就是一个const对象,会正常执行,所以,const对象不能调用非const成员函数const 对象可以使用任一成员,但非 const 版本是一个更好的匹配

  • 可变的数据成员

有时(但不是很经常),我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。可变数据成员(mutable data member)永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前:

class Screen {
     public:
     // interface member functions
     private:
         mutable size_t access_ctr; // may change in a const members
         // other data members as before
      };

我们给 Screen 添加了一个新的可变数据成员 access_ctr。使用 access_ctr 来跟踪调用 Screen 成员函数的频繁程度:

void Screen::do_display(std::ostream& os) const
     {
         ++access_ctr; // keep count of calls to any member function
         os << contents;
     }

尽管 do_displayconst,它也可以增加 access_ctr。该成员是可变成员,所以,任意成员函数,包括 const 函数,都可以改变 access_ctr 的值。因此,mutable主要是用来在const函数中使用的、可以改变的数据成员

  •  构造函数

构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。

class Sales_item {
     public:
         // operations on Sales_itemobjects
         // default constructor needed to initialize members of built-in type
         Sales_item(): units_sold(0), revenue(0.0) { }
     private:
         std::string isbn;
         unsigned units_sold;
         double revenue;
     };

这个构造函数使用构造函数初始化列表来初始化 units_soldrevenue 成员。isbn 成员由 string默认构造函数指你定义的构造函数中没有参数的那个隐式初始化为空串。

  • 构造函数自动执行

只要创建该类型的一个对象,编译器就运行一个构造函数:

// constructor that takes a string used to create and initialize variable
     Sales_item Primer_2nd_ed("0-201-54848-8");
     // default constructor used to initialize unnamed object on the heap
     Sales_item *p = new Sales_item();

第一种情况下,运行接受一个 string 实参的构造函数,来初始化变量 Primer_2nd_ed。第二种情况下,动态分配一个新的 Sales_item 对象。假定分配成功,则通过运行默认构造函数初始化该对象。

 构造函数可以被重载:

class Sales_item;
     // other members as before
     public:
         // added constructors to initialize from a string or an istream
         Sales_item(const std::string&);
         Sales_item(std::istream&);
         Sales_item();
     };

实参决定我们使用哪个构造函数:

// uses the default constructor:
     // isbn is the empty string; units_soldand revenue are 0
     Sales_item empty;
     // specifies an explicit isbn; units_soldand revenue are 0
     Sales_item Primer_3rd_Ed("0-201-82470-1");
     // reads values from the standard input into isbn, units_sold, and revenue
     Sales_item Primer_4th_ed(cin);

用于初始化一个对象的实参类型决定使用哪个构造函数。在 empty 的定义中,没有初始化式,所以运行默认构造函数。接受一个 string 实参的构造函数用于初始化 Primer_3rd_ed;接受一个 istream 引用的构造函数初始化 Primer_4th_ed

  • 用于const对象的构造函数

构造函数不能声明为const:

class Sales_item {
     public:
         Sales_item() const;    // error
     };

const 构造函数是不必要的。创建类类型的 const 对象时,运行一个普通构造函数来初始化该 const 对象。构造函数的工作是初始化对象。不管对象是否为 const,都用一个构造函数来初始化化该对象

  • 构造函数的初始化式:

与任何其他函数一样,构造函数具有名字、形参表和函数体。与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表:

// recommended way to write constructors using a constructor initializer
     Sales_item::Sales_item(const string &book):
          isbn(book), units_sold(0), revenue(0.0) { }

构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。这个构造函数将 isbn 成员初始化为 book 形参的值,将 units_soldrevenue 初始化为 0。与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中而不是声明中指定

省略初始化列表在构造函数的函数体内对数据成员赋值是合法的。例如,可以将接受一个 stringSales_item 构造函数编写为:

// legal but sloppier way to write the constructor:
     // no constructor initializer
     Sales_item::Sales_item(const string &book)
     {
         isbn = book;
         units_sold = 0;
         revenue = 0.0;
     }

这个构造函数给类 Sales_item 的成员赋值,但没有进行显式初始化。不管是否有显式的初始化式,在执行构造函数之前,要初始化 isbn 成员。这个构造函数隐式使用默认的 string 构造函数来初始化 isbn。执行构造函数的函数体时,isbn 成员已经有值了。该值被构造函数函数体中的赋值所覆盖

从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前

在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0

在本节中编写的两个 Sales_item 构造函数版本具有同样的效果:无论是在构造函数初始化列表中初始化成员,还是在构造函数函数体中对它们赋值,最终结果是相同的。构造函数执行结束后,三个数据成员保存同样的值。不同之处在于,使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数版本在构造函数函数体中对数据成员赋值(初始化和赋值是两个概念)。这个区别的重要性取决于数据成员的类型。

  • 有些成员必须在构造函数初始化列表中进行初始化

如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式

有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化

因为内置类型的成员不进行隐式初始化,所以对这些成员是进行初始化还是赋值似乎都无关紧要。除了两个例外,对非类类型的数据成员进行赋值或使用初始化式在结果和性能上都是等价的。

例如,下面的构造函数是错误的:

class ConstRef {
     public:
         ConstRef(int ii);
     private:
         int i;
         const int ci;
         int &ri;
     };
     // no explicit constructor initializer: error ri is uninitialized
     ConstRef::ConstRef(int ii)
     {              // assignments:
          i = ii;   // ok
          ci = ii;  // error: cannot assign to a const
          ri = i;   // assigns to ri which was not bound to an object
     }

记住,可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中。编写该构造函数的正确方式为

// ok: explicitly initialize reference and const members
     ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }

注意,初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。

一个初始化式可以是任意复杂的表达式。例如,可以给 Sales_item 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:

Sales_item(const std::string &book, int cnt, double price):
         isbn(book), units_sold(cnt), revenue(cnt * price) { }
  • 类类型数据成员的初始化式

初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string 构造函数来初始化 isbn。也可以用 ISBN 取值的极限值来表示 isbn 的默认值,而不是用空字符串。可以将 isbn 初始化为由 10 个 9 构成的串:

// alternative definition for Sales_item default constructor
     Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
  • 默认实参与构造函数

再来看看默认构造函数和接受一个 string 的构造函数的定义:

Sales_item(const std::string &book):
               isbn(book), units_sold(0), revenue(0.0) { }
     Sales_item(): units_sold(0), revenue(0.0) { }

这两个构造函数几乎是相同的:唯一的区别在于,接受一个 string 形参的构造函数使用该形参来初始化 isbn。默认构造函数(隐式地)使用 string 的默认构造函数来初始化 isbn

可以通过为 string 初始化式提供一个默认实参将这两个构造函数组合起来:

class Sales_item {
     public:
         // default argument for book is the empty string
         Sales_item(const std::string &book = ""):
                   isbn(book), units_sold(0), revenue(0.0) { }
      Sales_item(std::istream &is);
      // as before };

在这里,我们只定义了两个构造函数,其中一个为其形参提供一个默认实参。

对于下面的任一定义:

Sales_item empty;
     Sales_item Primer_3rd_Ed("0-201-82470-1");

将执行的是这个构造函数:

Sales_item(const std::string &book = ""):
                   isbn(book), units_sold(0), revenue(0.0) { }

empty 的情况下,使用默认实参,而 Primer_3rd_ed 提供了一个显式实参。这两个类的定义中,给没给初始的string,都调用的是同一个构造函数。

只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数

如果是:

class Sales_item {
     public:
         // default argument for book is the empty string
         Sales_item(const std::string &book = ""):
                   isbn(book), units_sold(0), revenue(0.0) { }
         Sales_item(): units_sold(0), revenue(0.0) { }
     };

再定义

Sales_item empty;

会调用哪一个构造函数呢?

答案是会报错:

  • 合成的默认构造函数

一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数

合成的默认构造函数(synthesized default constructor)没有定义默认构造函数,而是由编译器自己生成的默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化

如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员//不要指望合成的默认构造函数为你初始化数据

参考:合成的默认构造函数的相关介绍:http://www.tuicool.com/articles/iYveIjR

此外,每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义的状态。除了作为赋值的目标之外,以任何方式使用一个未定义的成员都是错误的。如果每个构造函数将每个成员设置为明确的已知状态,则成员函数可以区分空对象和具有实际值的对象//如果你不定义一个初始化了内置、复合类型的默认构造函数,编译器也是不会报错的,但这就意味着你的数据成员没有初始化,后续使用的都是未初始化的数据,这样是很不规范的。

默认构造函数意味着什么?不定义默认构造函数不代表你的这个类在构造上有错,而仅仅在于使用当中很多数据没有初始化,不规范。

 

原文地址:https://www.cnblogs.com/predator-wang/p/5201933.html