C++ Primer(类)

第12章 类

12.1类的定义和声明

12.1.1 类定义:扼要重述

1.每个类可以没有成员,也可以定义多个成员,成员可以是数据、函数或类型别名。一个类可以包含若干公有的、私有的和受保护的部分。在 public 部分定义的成员可被使用该类型的所有代码访问;在 private 部分定义的成员可被其他类成员访问。所有成员必须在类的内部声明。

2.构造函数是一个特殊的、与类同名的成员函数,用于给每个数据成员设置适当的初始值。构造函数一般就使用一个构造函数初始化列表,来初始化对象的数据成员:
Sales_item(): units_sold(0), revenue(0.0) { }

3.声明成员函数是必需的,定义成员函数是可选的。

4.关键字const加在形参表之后,将成员函数声明为常量:

  double avg_price() const;

const 成员函数不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中,若只出现在其中一处,就会出现一个编译时错误。

12.1.2 数据抽象和封装

1.数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。

2.封装是一项低层次的元素组合起来的形成新的、高层次实体珠技术。
3.可以在任意的访问标号出现之前定义类成员。如果类是用 struct 关键字定义的,则在第一个访问标号之前的成员是公有的;如果类是用 class 关键字是定义的,则这些成员是私有的。
4.具体类型和抽象类型:并非所有类型都必须是抽象的。标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节。一些类,例如 pair,确实没有抽象接口。pair 类型只是将两个数据成员捆绑成单个对象。在这种情况下,隐藏数据成员没有必要也没有明显的好处。在像 pair 这样的类中隐藏数据成员只会造成类型使用的复杂化。

12.1.3关于类定义的更多内容

1.成员函数可以重载:两个重载成员的形参数量和类型不能完全相同。
2.inline 成员函数:在类内部定义的成员函数,将自动作为 inline 处理。可以在类定义体内部指定一个成员为inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline 都是合法的。在类的外部定义 inline 的一个好处是可以使得类比较容易阅读。(详见inline函数)

12.1.4 类声明和类定义

一旦定义了类,那以我们就知道了所有的类成员,以及存储该类的对象所需的存储空间。

A).在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
B).可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为前向声明(forward declaraton),在程序中引入了类类型的 Screen。在声明之后、定义之前,类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。
不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。
C).在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样地,在使用引用或指针访问类的成员之前,必须已经定义类。
D).为类的成员使用类声明:只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:
class LinkScreen {
  Screen window;
  LinkScreen *next;
  LinkScreen *prev;
};

12.1.5 类对象

1.定义了一个类类型之后,可以按以下两种方式使用。
A.将类的名字直接用作类型名。
B.指定关键字 class 或 struct,后面跟着类的名字:
Sales_item item1; // default initialized object of type Sales_item
class Sales_item item1; // equivalent definition of item1

2。类的定义分号结束。分号是必需的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束:
class Sales_item { /* ... */ };
class Sales_item { /* ... */ } accum, trans;

12.2 隐含的this指针

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

1.最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。如下列情形:
希望用户能够将这些操作的序列连接成一个单独的表达式:
// move cursor to given position, and set that character
myScreen.move(4,0).set('#');
2.返回 *this
在单个表达式中调用 move 和 set 操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:
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;
}
3.从 const 成员函数返回 *this.
不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用。
4.基于 const 的重载:const 成员函数重载可看做是对隐含的指针this的参数重载。
5.可变数据成员:有时(但不是很经常),我们希望类的数据成员(甚至在 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;
}
12.3 类作用域

1.两个不同的类具有两个的类作用域。


2.在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 . 或 -> 来访问。这些操作符左边的操作数分别是一个类对象或指向类对象的指针。跟在操作符后面的成员名字必须在相关联的类的作用域中声明
  一些成员使用成员访问操作符来访问,另一些直接通过类使用作用域操作符(::)来访问。一般的数据或函数成员必须通过对象来访问。定义类型的成员,如 Screen::index,使用作用域操作符来访问。
3.尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样。
4.形参表和函数体处于类作用域中:在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
5.函数返回类型不一定在类作用域中:与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。
6.必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
7.编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名,该名字就不能被重复定义:
typedef double Money;
class Account {
public:
Money balance() { return bal; } // uses global definition of Money
private:
// error: cannot change meaning of Money
typedef long double Money;
Money bal;
// ...
};
8.类作用域中的名字查找:
A.首先,在使用该名字的块中查找名字的声明。只考虑在该项使用之前声明的名字。
B.如果找不到该名字,则在包围的作用域中查找。
如果找不到,则编译错误
类定义实际上是在两个阶段中处理:首先,编译成员声明;只有在所有成员出现之后,才编译它们的定义本身。
9.类成员声明的名字查找:
A.按以下方式确定在类成员的声明中用到的名字。
B.检查出现在名字使用之前的类成员的声明。
如果第 A步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明。
必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义. 而且,一旦一个名字被用作类型名,该名字就不能被重复定义
typedef double Money;
class Account {
public:
Money balance() { return bal; } // uses global definition of Money
private:
// error: cannot change meaning of Money
typedef long double Money;
Money bal;
// ...
};
10.类成员定义中的名字查找:
按以下方式确定在成员函数的函数体中用到的名字。
A.首先检查成员函数局部作用域中的声明。
B.如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
C.如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明。
11.尽管全局对象被屏蔽了,但通过用全局作用域确定操作符来限定名字,仍然可以使用它。
// bad practice: Don't hide names that are needed from surrounding scopes
void dummy_fcn(index height) {
cursor = width * ::height;// which height? The global one
}

12.4构造函数

构造函数的工作是保证每个对象的数据成员具有合适的初始值。只要创建该类型的一个对象,编译器就运行一个构造函数
Sales_item::Sales_item(const string &book):isbn(book), units_sold(0), revenue(0.0) { }

构造函数的名字与类相同,并且不能指定返回类型。可以没有形参,也可以定义多个形参。

1.构造函数可以被重载

Sales_item(const std::string&);

Sales_item(std::istream&);

Sales_item();

2.用于初始化一个对象的实参类型决定使用哪个构造函数

12.4.1 构造函数初始化式

构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。

构造函数可以定义在类的内部或外部。构造函数的初始化式只在构造函数的定义中而不是声明中指定。

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

2.如果类没有默认构造函数,为了初始化数据成员,必须提供初始化式。

初始化const或引用类型数据成员的唯一机会是在构造函数初始化列表中。

class ConstRef{

public:ConstRef(int ii);

private:

    int i;

    const int ci;

    int &ri;

};

ConstRef::ConstRef(int ii): i(ii),ci(i),ri(ii){}

3.每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类中被声明的顺序决定的。

4.初始化式可以是任意表达式:一个初始化式可以是任意复杂的表达式。

例如,可以给 Sales_item 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:
Sales_item(const std::string &book, int cnt, double price):isbn(book), units_sold(cnt), revenue(cnt * price) { }

5.类类型的数据成员的初始化式:初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。

例如,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) {}

12.4.3 默认构造函数

默认构造函数:只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
1.一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
2.只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
3.合成的默认构造函数(synthesized default constructor)使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
4. 如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
5.此外,每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义的状态。除了作为赋值的目标之外,以任何方式使用一个未定义的成员都是错误的。如果每个构造函数将每个成员设置为明确的已知状态,则成员函数可以区分空对象和具有实际值的对象。
6.使用默认构造函数定义一个对象的正确方式是去掉最后的空括号:
// oops! declares a function, not an object
Sales_item myobj; //而不是Sales_item myobj();
下面这段代码也是正确的:
// ok: create an unnamed, empty Sales_itemand use to initialize myobj
Sales_item myobj = Sales_item();

12.4.4隐式类类型转换

C++ 语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型。
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
定义了两个构造函数的 Sales_item 版本:
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 类型对象的地方,可以使用一个 string 或一个 istream:
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(null_book);
1.抑制由构造函数定义的隐式转换:可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数.
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它.
2. 为转换而显式地使用构造函数:
除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
任何构造函数都可以用来显式地创建临时对象。

12.4.5类成天的显式初始化

尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:
struct Data {
int ival;
char *ptr;
};
// val1.ival = 0; val1.ptr = 0
Data val2 = { 1024, "Anna Livia Plurabelle" };
根据数据成员的声明次序来使用初始化式。

12.5友元

友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。

1.友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。

2.必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。

3.类必须将重载函数集中每一个希望设为友元的函数都声明为友元:

//overloaded storeOn functins

extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
// ostream version of storeOn may access private parts of Screen objects
friend std::ostream& storeOn(std::ostream &, Screen &);
// ...
};

12.6 static类成员

通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。

1.使用 static 成员而不是全局对象有三个优点。
A。static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
B。可以实施封装。static 成员可以是私有成员,而全局对象不可以。
C。通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。

2.使用类的 static 成员:可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。像使用其 他成员一样,类成员函数可以不用作用域操作符来引用类的 static 成员

12.6.1 static 成员函数

Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处:
class Account {
public:
// interface functions here
void applyint() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
 void Account::rate(double newRate)
{
interestRate = newRate;
}
2.static 函数没有 this 指针:
A.static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。通过使用非 static 成员显式或隐式地引用 this 是一个编译时错误。
B.因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。static 成员函数也不能被声明为虚函数。

12.6.2 static数据成员

A.static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
B.static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
C.定义 static 数据成员的方式与定义其他类成员和变量的方式相同:先指定类型名,接着是成员的完全限定名。
可以定义如下 interestRate:
   // define and initialize static class member
    double Account::interestRate = initRate();
D.像使用任意的类成员一样,在类定义体外部引用类的 static 成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为 static。

1.特殊的整型 const static 数据成员:
A.一般而言,类的 static数据成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static 数据成员通常在定义时才初始化。
B.这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化:
class Account {
public:
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
static const int period = 30; // interest posted every 30 days
double daily_tbl[period]; // ok: period is constant expression
};
C.用常量值初始化的整型 const static 数据成员是一个常量表达式。同样地,它可以用在任何需要常量表达式的地方。
D.const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。如不定义也可编译正常,但有本质区别
#include <iostream>
class A{
public:
static const int a = 10;
};
//const int A::a;
int main()
{
    std::cout << &A::a << std::endl;//无法编译通过,会显示“undefined reference to A::a 的错误”.去掉const int A::a的注释后,运行正常
}
//被注释掉那句要不要加,为什么?
//加与不加的主要区别是,类外加上定义后,才会为a申请空间,否则,a只能作为常量表达式使用,在编译结束后a就没了,不能对其做取地址操作。
2.static 成员不是类对象的组成部分:普通成员都是给定类的每个对象的组成部分。static 成员独立于任何对象而存在,不是类类型对象的组成部分。因为 static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非 static 数据成员而言是不合法的。
A.例如,static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar {
public:
// ...
private:
static Bar mem1; // ok
Bar *mem2; // ok
Bar mem3; // error
};
B.类似地,static 数据成员可用作默认实参:
class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground = '#';
};
非 static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。使用非 static 数据成员作默认实参,将无法提供对象以获取该成员的值,因而是错误的。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值