C++ 中,通过定义类来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以执行的操作。标准库类型 string、istream 和 ostream 都定义成类。
类定义以关键字 class 或 struct 开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。
1 2 3 4 5 6 7 8
classSales_item { public: // operations on Sales_item objects will go here private: std::string isbn; unsigned units_sold; double revenue; };
或者
1 2 3 4 5 6 7 8
structSales_item { // no need for public label, members // operations on Sales_item objects are public by default private: std::string isbn; unsigned units_sold; double revenue; };
如果使用 class 关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private。
尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
某种类可能具有某些操作,这些操作应该返回引用,Screen 类就是这样的一个类。迄今为止,我们的类只有一对 get 操作。逻辑上,我们可以添加下面的操作。
一对 set 操作,将特定字符或光标指向的字符设置为给定值。
一个 move 操作,给定两个 index 值,将光标移至新位置。
理想情况下,希望用户能够将这些操作的序列连接成一个单独的表达式:
1 2
// move cursor to given position, and set that character myScreen.move(4,0).set('#');
这个语句等价于:
1 2
myScreen.move(4,0); myScreen.set('#');
返回 *this
在单个表达式中调用 move 和 set
操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:
1 2 3 4 5 6 7 8
classScreen { public: // interface member functions Screen& move(index r, index c); Screen& set(char); Screen& set(index, index, char); // other members as before };
注意,这些函数的返回类型是 Screen& ,指明该成员函数返回对其自身类类型的对象的引用。每个函数都返回调用自己的那个对象。使用 this 指针来访问该对象。下面是对两个新成员的实现:
1 2 3 4 5 6 7 8 9 10 11
Screen& Screen::set(char c) { contents[cursor] = c; return *this; // 返回整个对象,需要用到 *this } Screen& Screen::move(index r, index c) { index row = r * width; // row location cursor = row + c; return *this; // 返回整个对象,需要用到 *this }
在这两个操作中,每个函数都返回 *this。在这些函数中,this 是一个指向非常量 Screen 的指针。如同任意的指针一样,可以通过对 this 指针解引用来访问 this 指向的对象。
classScreen { public: // interface member functions private: mutablesize_t access_ctr; // may change in a const members // other data members as before };
classSales_item { public: // default argument for book is the empty string explicitSales_item(conststd::string &book = ""): isbn(book), units_sold(0), revenue(0.0) {explicit Sales_item(std::istream &is); // as before };
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它:
1 2 3 4 5
// error: explicit allowed only on constructor declaration in class header explicitSales_item::Sales_item(istream& is) { is >> *this; // uses Sales_iteminput operator to read the members }
为转换而显式地使用构造函数
只要显式地按下面这样做,就可以用显式的构造函数来生成转换:
1 2 3 4
string null_book = "9-999-99999-9"; // ok: builds a Sales_item with 0 units_sold and revenue from // and isbn equal to null_book item.same_isbn(Sales_item(null_book));
类成员的显式初始化
对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:
// p points to default constructed object Sales_item *p = new Sales_item;
{ // new scope Sales_item item(*p); // copy constructor copies *p into item delete p; // destructor called on object pointed to by p } // exit local scope; destructor called on item
classScreen { // Window_Mgrmust be defined before class Screen friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index, Window_Mgr::index, Screen&); // ...rest of the Screen class };
Account ac1; Account *ac2 = &ac1; // equivalent ways to call the static member rate function double rate; rate = ac1.rate(); // through an Account object or reference rate = ac2->rate(); // through a pointer to an Account object rate = Account::rate(); // directly from the class using the scope operator
classAccount { public: staticdoublerate(){ return interestRate; } staticvoidrate(double); // sets a new rate private: staticconstint period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
classBar { public: // ... private: static Bar mem1; // ok Bar *mem2; // ok Bar mem3; // error };
类似地,static 数据成员可用作默认实参:
1 2 3 4 5 6 7 8
classScreen { public: // bkground refers to the static member // declared later in the class definition Screen& clear(char = bkground); private: staticconstchar bkground = '#'; };
// class that has a pointer member that behaves like a plain pointer classHasPtr { public: // copy of the values we're given HasPtr(int *p, int i): ptr(p), val(i) { }
// const members to return the value of the indicated data member int *get_ptr()const{ return ptr; } intget_int()const{ return val; }
// non const members to change the indicated data member voidset_ptr(int *p){ ptr = p; } voidset_int(int i){ val = i; } // return or change the value pointed to, so ok for const objects intget_ptr_val()const{ return *ptr; } voidset_ptr_val(int val)const{ *ptr = val; }
// private class for use by HasPtr only classU_Ptr { friendclassHasPtr; int *ip; size_t use; U_Ptr(int *p): ip(p), use(1) {} ~U_Ptr() { delete ip; } };
/* smart pointer class: takes ownership of the dynamically allocated * object to which it is bound * User code must dynamically allocate an object to initialize a HasPtr * and must not delete that object; the HasPtr class will delete it */ classHasPtr { public: // HasPtr owns the pointer; p must have been dynamically allocated HasPtr(int *p, int i): ptr(new U_Ptr(p)), val(i) { } // copy members and increment the use count HasPtr(const HasPtr &orig): ptr(orig.ptr), val(orig.val) { ++ptr->use; } HasPtr& operator=(const HasPtr&);
// copy control and constructors as before // accessors must change to fetch value from U_Ptr object int *get_ptr()const{ return ptr->ip; } intget_int()const{ return val; }
// change the appropriate data member voidset_ptr(int *p){ ptr->ip = p; } voidset_int(int i){ val = i; }
// return or change the value pointed to, so ok for const objects // Note: *ptr->ip is equivalent to *(ptr->ip)
// if use count goes to zero, delete the U_Ptr object ~HasPtr() { if (--ptr->use == 0) delete ptr; }
private: U_Ptr *ptr; // points to use-counted U_Ptr class int val; };
HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++rhs.ptr->use; // increment use count on rhs first if (--ptr->use == 0) delete ptr; // if use count goes to 0 on this object, delete it ptr = rhs.ptr; // copy the U_Ptr object val = rhs.val; // copy the int member return *this; }
/* * Valuelike behavior even though HasPtr has a pointer member: * Each time we copy a HasPtr object, we make a new copy of the * underlying int object to which ptr points. */ classHasPtr { public: // no point to passing a pointer if we're going to copy it anyway // store pointer to a copy of the object we're given HasPtr(constint &p, int i): ptr(newint(p)), val(i) {} // copy members and increment the use count HasPtr(const HasPtr &orig): ptr(newint (*orig.ptr)), val(orig.val) {}
HasPtr& operator=(const HasPtr&); ~HasPtr() { delete ptr; } // accessors must change to fetch value from Ptr object intget_ptr_val()const{ return *ptr; } intget_int()const{ return val; }
// change the appropriate data member voidset_ptr(int *p){ ptr = p; } voidset_int(int i){ val = i; }
// return or change the value pointed to, so ok for const objects int *get_ptr()const{ return ptr; } voidset_ptr_val(int p)const{ *ptr = p; } private: int *ptr; // points to an int int val; };
HasPtr& HasPtr::operator=(const HasPtr &rhs) { // Note: Every HasPtr is guaranteed to point at an actual int; // We know that ptr cannot be a zero pointer *ptr = *rhs.ptr; // copy the value pointed to val = rhs.val; // copy the int return *this; }
复制构造函数不再复制指针,它将分配一个新的 int 对象,并初始化该对象以保存与被复制对象相同的值。每个对象都保存属于自己的 int 值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针。
// calculate and print price for given number of copies, applying any discounts voidprint_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " << n << "\ttotal price: " // virtual call: which version of net_price to callresolved at run time << item.net_price(n) << endl; }
这个构造函数使用有两个形参 Item\_base 构造函数初始化基类子对象,它将自己的 book 和 sales\_price 实参传递给该构造函数。这个构造函数可以这样使用:
1 2
// arguments are the isbn, price, minimum quantity, and discount Bulk_item bulk("0-201-82470-1", 50, 5, .19);
要建立 bulk,首先运行 Item\_base 构造函数,该构造函数使用从 Bulk\_item 构造函数初始化列表传来的实参初始化 isbn 和 price。Item\_base 构造函数执行完毕之后,再初始化 Bulk\_item 的成员。最后,运行 Bulk\_item 构造函数的(空)函数体。
#### 只能初始化直接基类 ####
一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类 C 从类 B 派生,类 B 从类 A 派生,则 B 是 C 的直接基类。虽然每个 C 类对象包含一个 A 类部分,但 C 的构造函数不能直接初始化 A 部分。
相反,需要类 C 初始化类 B,而类 B 的构造函数再初始化类 A。这一限制的原因是,类 B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约。
作为更具体的例子,书店可以有几种折扣策略。除了批量折扣外,还可以为购买某个数量打折,此后按全价销售,或者,购买量超过一定限度的可以打折,在该限度之内不打折。
通过策略模式可以实现这一点:这些折扣策略都需要一个数量和一个折扣量。可以定义名为 Disc\_item 的新类存储数量和折扣量,以支持这些不同的折扣策略。Disc\_item 类可以不定义 net\_price 函数,但可以作为定义不同折扣策略的其他类(如 Bulk_item 类)的基类。
### 复制控制和继承 ###
#### 定义派生类复制构造函数 ####
// Base::operator=(const Base&) not invoked automatically Derived &Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // assigns the base part // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
classItem_base { public: // no work, but virtual destructor needed // if base pointer that points to a derived object is ever deleted virtual ~Item_base() { } };
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
1 2 3 4
Item_base *itemP = new Item_base; // same static and dynamic type delete itemP; // ok: destructor for Item_base called itemP = new Bulk_item; // ok: static and dynamic types differ delete itemP; // ok: destructor for Bulk_item called
派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供 using 声明。一个 using 声明只能指定一个名字,不能指定形参表,因此,为基类成员函数名称而作的 using 声明将该函数的所有重载实例加到派生类的作用域。将所有名字加入作用域之后,派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
纯虚函数
如果不想让某个类直接被实例化,可以将它声明为一个抽象基类(abstract base class)。含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。