第7章 类

1、类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。

  • 数据抽象是一种依赖于接口(interface)和实现(implemention)分离的编程技术。类的结构包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

Sale_data的接口应该包含以下操作:

  • 一个isbn成员函数,用于返回对象的ISBN编号
  • 一个combine成员函数,用于将一个Sale_data对象加到另一个对象上
  • 一个名为add的函数,执行两个Sales_data对象的加法
  • 一个read函数,将数据从istream读入到Sales_data对象中
  • 一个print函数,将Sales_data对象的值输出到ostream

2、定义成员函数

  • 尽管所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。
  • 成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化this。eg:
total.isbn()  等价于  Sale_data::isbn(&total)  //伪代码,用于说明调用成员函数的实际执行过程
  • this是一个常量指针,不允许改变this中保存的地址。
  • 默认情况下,this的类型是指向类类型非常量版本的常量指针。例如在Sales_data成员函数中,this的类型是Sales_data *const this,我们不能把this绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。
  • C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的尘缘函数被称为常量成员函数。
//伪代码,说明隐式的this指针是如何使用的
string isbn() const {return bookNo;}  等价于  string Sales_data::isbn(const Sale_data *const this) {return this->isbn;}
  • 常量对象、以及常量对象的引用或指针都智能调用常量成员函数。
  • 在类的外部定义成员函数必须使用作用域运算符来说明成员函数属于那个类。
  • 一般来讲,定义的函数类似与某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内的的赋值运算符返回它的左侧对象,返回对象是个左值。

3、定义类相关的非成员函数

  • 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。

4、构造函数

  • 构造函数不能被声明为const的。当我们创建类的一个const对象时,知道构造函数完成初始化过程,对象才能真正取得其“常量“属性。
  • 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。
  • 如果类包含有内置类型或符合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。

5、访问控制与封装

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
  • 使用Class和struct定义类唯一的区别就是默认的访问权限。

6、友元

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。只需要增加一条以friend关键字开始的函数声明语句即可。
  • 一般来说,最好在类定义开始或结束前的位置集中声明友元。
  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
  • 每个类负责控制自己的友元类或友元函数,友元关系不存在传递性。
  • 还可以只为某个成员函数作为友元,但要满足以下声明和定义的彼此依赖关系:(在Window_mgr和Screen例子中)
    1) 首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员函数之前必须先声明Screen。
    2)接卸来定义Screen,包括对于clear的友元声明。
    3)最后定义clear,此时它才可以使用Screen的成员。
  • 类和非成员函数的声明不是必须在它们的友元声明之前,当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域是可见的。

7、类的其它特性

定义一个类型成员

  • 除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名,可以是public或者private中的一种。
typedef std::sting::size_type pos;                  using pos = std::sting::size_type

令成员作为内联函数

  • 定义在类内部的成员函数是自动inline的,也可以在类内显示inline,或者在类的外部(函数定义处)指定inline。
char get() const {return contents[cursor];}        //隐式inline
inline char get(pos ht, pos wd) const;             //显示inline
Screen &move(pos r, pos c);                        //能在类的外部(函数定义处)被设为内联

类数据成员的初始值

  • 当我们提供一个类内初始值时,必须以符号=或者花括号表示。

从const成员函数返回*this

  • 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

基于const的重载

  • 因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数。
Screen &display(std::ostream &os) {do_display(os); return *this;}
const Screen &display(std::ostream &os) const {do_display(os)l return *this;}  //一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用

8、名字查找与类的作用域

名字查找的一般过程:

  • 首先,在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。

类内的成员函数名字查找过程:

  • 首先,编译成员的声明。
  • 知道类全部可见后才编译函数体。

9、构造函数再探

构造函数初始值列表

  • 当我们定义变量时习惯于立即对其进行初始化,而非先定义、再赋值:
sting foo = "Hello World!";		//定义并初始化
string bar;						//默认初始化成空string对象
bar = "Hello World!";			//为bar赋一个新值
  • 就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化
//虽然合法但比较草率:没有使用构造函数初始值
Sales_data::Sales_data(const sting &s, unsigned cnt, double price) {
	bookNo = s;
	units_sold = cntl
	revenue = cnt * price;
}
  • 构造函数的初始值有时比不可少。如果成员是const或者是引用的话,必须将其初始化。
  • 随着构造函数体一开始执行,初始化就完成了。
  • 成员初始化的顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
  • 最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员。

委托构造函数

  • 委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或全部)职责委托给了其他构造函数。
class Sales_data {
public:
	//非委托构造函数使用对应的实参初始化成员
	Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt * price) {}
	//其余构造函数全部委托给另一个构造函数
	Sales_data() : Sales_data(" ", 0, 0) {}
	Sales_data(std::string s) : Sales_data(s, 0, 0) {}
	Sales_data(std::istream &is) : Sales_data() {read(is, *this);}
};

隐式的类类型转换

  • 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor)。
  • 在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则。也就是说,在需要使用Sales_data的地方,我们可以使用string或者istream代替:
string null_book = "9-999-99999-9";
//构造一个临时的Sales_data对象
//该对象的units_sold和revenue等于0,bookNo等于null_book
item.combine(null_book);
  • 只允许一步类类型转换,编译器只会自动的执行一步类型转换:
//错误:需要进行两步转换
//(1)把“9-999-99999-9”转换成string
//(2)再把临时string转换成Sales_data
item.combine("9-999-99999-9");

//正确:显示地转换成string,隐式地转换成Sales_data
item.combine(string("9-999-99999-9"));
//正确:隐式的转换成string,显示地转换成Sales_data
item.combine(Sales_data("9-999-99999-9"));

抑制构造函数定义的隐式转换

  • 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:
explicit Sales_data(const std::string &s) : bookNo(s) {}
  • 关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit。
  • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。

explicit构造函数只能用于直接初始化

  • 发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=),此时,我们只能使用直接初始化而不能使用explicit构造函数。
Sales_data item1(null_book);		//正确:直接初始化
Sales_data item2 = null_book;	//错误:不能将explicit构造函数用于拷贝形式的初始化过程
  • 当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。

为转换显式的使用构造函数

  • 尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造韩式显式地强制进行转换:
//正确:实参是一个显示构造 的Sales_data对象
item.combine(Sales_item(null_book));

10、类的静态成员

  • 有的时候类需要它的一些成员与类本省直接相关,而不是与类的各个对象保持关联。例如,一个银行账户类可能需要一个数据成员来表示当前的基准利率。在此例中,我们希望利率与类关联,而非与类的每个对象关联。
  • 通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以是public的或private的。
class Account {
public:
	void calculate() {amount +=amount * interestRate;}
	static double rate() {return interestRate;}
	static void rate(double);
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate();	
};
  • 类的静态成员存在与任何对象之外,对象中不包含任何与静态数据成员有关的数据。因此,每个Account对象将包含两个数据成员:owner和amount。只存在一个interestRate对象而且它被所有Account对象共享。
  • 类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针,这一限制既适用于this的显示使用,也对调用非静态成员的隐式使用有效。

使用类的静态成员

  • 使用作用域运算符直接访问静态成员:
double r;
r = Account::rate();		//使用作用域运算符访问静态成员
  • 虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员:
Account ac1;
Account *ac2 = &ac1;
//调用静态成员函数rate的等价形式
r = ac1.rate();		//通过Account的对象或引用
r = ac2->rate();		//通过指向Account对象的指针
  • 成员函数不用通过作用域运算符就能直接使用静态成员:
class Account {
public:
	void calculate() {amount += amount * interestRate;}
private:
	static double interestRate;	
};
  • 和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。static关键字则只出现在类内部的声明语句中。
  • 因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是有类的构造函数初始化的。必须在类的外部定义和初始化每个静态成员。
  • 类似与全局变量,静态数据成员定义在任何函数之外,因此一旦它被定义,就将一直存在与程序的整个生命周期中。
double Account::interestRate = initRate();

猜你喜欢

转载自blog.csdn.net/weixin_42205011/article/details/87883509