【C++ primer】第7章 类 (1)


Part I: The Basics
Chapter 7. Classes


(classes) 的基本思想是数据抽象和封装。

数据抽象 (data abstraction) 是一种依赖于接口和实现的分离的编程(和设计)技术。
类的接口 (interface) 由该类的用户可以执行的操作组成。
类的实现 (implementation) 包括类的数据成员,构成接口的函数体以及定义该类所需的任何私有函数。

封装 (encapsulation) 强制将类的接口和实现分开。封装的类会隐藏其实现:该类的用户可以使用接口,但无法访问实现。

使用数据抽象和封装的类定义抽象数据类型 (abstract data type)。使用该类的程序员不必知道该类型如何工作,他们可以抽象地思考类型的作用。


7.1 定义抽象数据类型

第2章中的 Sales_data 类不是抽象数据类型。它允许用户访问其数据成员,并要求用户编写操作。
要使 Sales_data 成为抽象类型,需要定义可供用户使用的操作。一旦 Sales_data 定义了它自己的操作,就可封装(隐藏)它的数据成员。

设计 Sales_data 类

Sales_data 类接口应包含以下操作:

  • isbn 成员函数,返回对象的 ISBN
  • combine 成员函数,将一个 Sales_data 对象加到另一个对象上
  • add 函数,两个 Sales_data 对象相加
  • read 函数,从 istream 读取数据到一个 Sales_data 对象中
  • print 函数,输出一个 Sales_data 对象的值到 ostream 中

使用改进的 Sales_data 类

Sales_data total;         // variable to hold the running sum
if (read(cin, total))  {  // read the first transaction
	Sales_data trans;     // variable to hold data for the next transaction
	while(read(cin, trans)) {      //  read the remaining transactions
		if (total.isbn() == trans.isbn())   // check the isbns
			total.combine(trans);  // update the running total
		else {
			print(cout, total) << endl;  // print the results
			total = trans;               // process the next book
		}
	}
	print(cout, total) << endl;          // print the last transaction
} else {                                 // there was no input
	cerr << "No data?!" << endl;         // notify the user
}

定义改进的 Sales_data 类

struct Sales_data {
	// new members: operations on Sales_data objects
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;
	// data members are unchanged from § 2.6.1 (p. 72)
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0; 
};
// nonmember Sales_data interface functions
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

成员函数的声明必须在类的内部,它可以定义在类的内部或外部。
非成员函数,作为接口的组成部分,其声明和定义都在类的外部。

定义在类内部的函数是隐式内联的。

引入 this

std::string isbn() const { return bookNo; }

成员函数通过一个额外的形参 this 来访问被调用的对象。当调用成员函数时,this 初始化为请求该函数的对象的地址。
例如,当调用 total.isbn() 时,编译器将 total 的地址传递给 isbn 的隐式 this 形参。

// pseudo-code illustration of how a call to a member function is translated
Sales_data::isbn(&total)

this 形参是隐式定义的。虽然没有必要,但在成员函数体内部使用 this 是合法的:

std::string isbn() const { return this->bookNo; }

因为 this 总是指向“这个”对象,this 是一个 const 指针。不能改变 this 保存的地址。

引入 const 成员函数

isbn 函数的形参列表之后是关键字 const。这里的 const 的作用是修改隐式 this 指针的类型。

默认情况下,this 的类型是一个 const 指针,指向类类型的 非const 版本。
例如,在 Sales_data 成员函数中,this 的类型默认是 Sales_data *const。此时,不能在 const 对象上调用普通成员函数。

常量成员函数 (const member functions):成员函数的形参列表后面加上 const。此处的 const 表明 this 是指向常量的指针。
例如,isbn 函数中的 this 类型是 const Sales_data *const

在类的外部定义成员函数

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

定义一个返回“这个”对象的函数

Sales_data& Sales_data::combine(const Sales_data &rhs) {
	units_sold += rhs.units_sold; // add the members of rhs into
	revenue += rhs.revenue;       // the members of "this" object
	return *this; // return the object on which the function was called
}

total.combine(trans); // update the running total 

注意函数的返回类型和返回语句。

定义类相关的非成员函数

尽管 add 等函数定义的操作从概念上来说是类的接口的部分,但它们并不是类本身的部分。

一般来说,如果非成员函数是类的接口的组成部分,应该声明在类本身的头文件中。

定义 read 和 print 函数

// input transactions contain ISBN, number of copies sold, and sales price 
istream &read(istream &is, Sales_data &item) {
	double price = 0;
	is >> item.bookNo >> item.units_sold >> price;
	item.revenue = price * item.units_sold;
	return is;
} 
ostream &print(ostream &os, const Sales_data &item) {
	os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
	return os; 
}

read 和 print 函数分别接受各自 IO 类类型的引用作为其参数。因为 IO 类不是能被拷贝的类型,所以只能通过引用传递它们。另外,读取或写入流会改变流,所以函数接受普通引用,而不是常量引用。

print 函数没有输出换行符。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样用户代码可以自己决定是否需要换行。

定义 add 函数

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
	Sales_data sum = lhs;  // copy data members from lhs into sum
	sum.combine(rhs);      // add data members from rhs into sum
	return sum; 
}

构造函数

构造函数 (constructors):类为了控制对象初始化的一个或多个特殊成员函数。任务:初始化类对象的数据成员。无论何时,只要类类型的对象被创建,构造函数就会运行。

构造函数的名字与类的名字相同,没有返回类型。

构造函数不能被声明为 const。当创建类类型的一个 const 对象时,直到构造函数完成该对象的初始化后,对象才获取 const 属性。因此,构造函数可以在 const 对象的构造过程中向其写入数据。

合成的默认构造函数

Sales_data 类没有定义任何构造函数,但使用了该类对象的程序可以正确的编译和运行。

默认构造函数 (default constructors):控制默认初始化的构造函数。默认构造函数无须任何实参。
如果类没有显式定义任何构造函数,那么编译器会隐式地定义一个默认构造函数。

合成的默认构造函数 (synthesized default constructor):编译器生成的构造函数。
对于大多数类来说,这个合成的默认构造函数会使用下面的方式初始化类的数据成员:
①如果存在类内初始值,用它来初始化成员;
②否则,默认初始化该成员。

某些类不能依赖于合成的默认构造函数

类必须定义它自己的默认构造函数。原因如下:

  1. 只有在类没有定义任何构造函数时,编译器才会生成默认构造函数。如果定义了其他构造函数,而没有定义默认构造函数,那么类就没有默认构造函数。这条规则的依据:如果类在某种情况下需要控制对象初始化,那么类很可能在所有情况下都需要控制。
  2. 对于某些类来说,合成的默认构造函数会执行错误操作。如果定义在块内的内置或复合类型(比如数组或指针)的对象被默认初始化,它们的值是未定义的。
  3. 有时编译器不能为某些类合成默认构造函数。例如,一个类有一个成员是类类型,且这个成员的类类型没有默认构造函数,那么编译器不能初始化该成员。

定义 Sales_data 的构造函数

struct Sales_data {
	// constructors added
	Sales_data() = default;
	Sales_data(const std::string &s): bookNo(s) { }
	Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
	Sales_data(std::istream &);
	// other members as before
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0; 
};

在C++11标准中,可以通过在形参列表后面写上 = default,来要求编译器生成默认构造函数。

构造函数初始值列表

Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }

冒号以及冒号与花括号之间的代码,这部分称为构造函数初始值列表 (constructor initializer list),为创建的对象的一个或多个数据成员指定初始值。

// has the same behavior as the original constructor defined above 
Sales_data(const std::string &s): bookNo(s), units_sold(0), revenue(0){ }

构造函数不应覆盖类内初始值,除非使用不同的初始值。
如果不能使用类内初始值,每个构造函数应该显式初始化每个内置类型的成员。

在类的外部定义构造函数

Sales_data::Sales_data(std::istream &is) {
	read(is, *this); // read will read a transaction from is into this object
}

复制、赋值和析构

一般来说,编译器生成的版本将对对象的每个成员执行复制、赋值和销毁操作。
尽管编译器可以合成复制、赋值和析构操作,但对某些类来说,默认版本无法正确工作。

很多需要动态内存的类可以(一般应该)使用 vectorstring 管理必要的存储空间。使用 vectorstring 的类可以避免分配和释放内存带来的复杂性。


7.2 访问控制和封装

在C++语言中,使用访问说明符 (access specifiers) 加强封装:

  • 定义在 public 说明符之后的成员在整个程序内可被访问。public 成员定义类的接口。
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但不能被使用该类的代码访问。private 部分封装(即隐藏)实现细节。
class Sales_data {
public:            // access specifier added
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
	Sales_data(const std::string &s): bookNo(s) { }
	Sales_data(std::istream&);
	std::string isbn() const { return bookNo; }
	Sales_data &combine(const Sales_data&);
private:            // access specifier added
	double avg_price() const { return units_sold ? revenue/units_sold : 0; }
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0; 
};

使用 struct 或 class 关键字

使用 classstruct 定义类的唯一区别是默认访问级别。

如果使用 struct 关键字,定义在第一个类型说明符前面的成员是 public
如果使用 class 关键字,定义在第一个类型说明符前面的成员是 private

友元

类可以允许其他的类或函数访问它的非共有成员,方法是令那个类或函数成为它的友元 (friend)。

class Sales_data {
// friend declarations for nonmember Sales_data operations added
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
// other members and access specifiers as before
public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
	Sales_data(const std::string &s): bookNo(s) { }
	Sales_data(std::istream&);
	std::string isbn() const { return bookNo; }
	Sales_data &combine(const Sales_data&);
private:
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0; 
};
// declarations for nonmember parts of the Sales_data interface
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);

友元声明只能出现在类定义的内部,但可以出现类的任何位置。
友元不是类的成员,不受它们声明的所在区域的访问控制的影响。

一般来说,最后在类定义的开始或结束前的位置集中声明友元。

关键概念:封装的益处

  1. 用户代码不能无意间破坏封装对象的状态。
  2. 被封装的类的具体实现细节可以随时更改,而不需要更改用户级别的代码。

虽然当类的定义改变时不需要更改用户代码,但使用该类的源文件必须重新编译。

友元的声明

友元声明只是指定权限,而非通常意义上的函数声明。

有些编译器允许友元函数在没有普通声明的情况下就可以调用它。即使编译器支持这种行为,最后还是为友元函数提供一个独立的声明。


7.3 类的其他特性

类成员再探

定义一对相互关联的类 ScreenWindow_mgr 来展示类的特性。

定义一个类型成员

class Screen {
public:
	typedef std::string::size_type pos;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents; 
};

将 pos 定义在 Screen 的 public 部分,这样用户就可以使用这个名字。Screen 的用户不应该知道 Screen 使用了一个 string 对象存放它的数据。通过将 pos 定义成 public 成员,隐藏 Screen 的实现细节。

// alternative way to declare a type member using a type alias
using pos = std::string::size_type; 

与普通成员不同,定义类型的成员必须出现在被使用之前。因此,类型成员通常出现在类开始的地方。

Screen 类的成员函数

class Screen {
public:
	typedef std::string::size_type pos;
	Screen() = default; // needed because Screen has another constructor
	// cursor initialized to 0 by its in-class initializer
	Screen(pos ht, pos wd, char c): height(ht), width(wd), contents(ht * wd, c) { }
	char get() const              // get the character at the cursor
		{ return contents[cursor]; }       // implicitly inline
	inline char get(pos ht, pos wd) const; // explicitly inline
	Screen &move(pos r, pos c);      // can be made inline later
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents; 
};

令成员 inline

可以在类的内部将一个成员函数显式声明为 inline。同样,可以在类的外部使用 inline 修饰函数定义。

inline                   // we can specify inline on the definition
Screen &Screen::move(pos r, pos c) {
	pos row = r * width; // compute the row location
	cursor = row + c ;   // move cursor to the column within that row
	return *this;        // return this object as an lvalue
}
char Screen::get(pos r, pos c) const // declared as inline in the class
{
	pos row = r * width;      // compute row location
	return contents[row + c]; // return character at the given column
}

虽然无需同时在声明和定义时说明 inline,但这种做法是合法的。不过,最好只在类外面的定义说明 inline,这使类更容易阅读。

注意:inline 成员函数应该与相应的类定义在相同的头文件中。

重载成员函数

Screen myscreen;
char ch = myscreen.get();// calls Screen::get()
ch = myscreen.get(0,0);  // calls Screen::get(pos, pos)

mutable 数据成员

可变数据成员 (mutable data member) 永远不会是 const,即使它是 const 对象的一个成员。因此,const 成员函数可以更改一个 mutable 成员。

class Screen {
public:
	void some_member() const;
private:
	mutable size_t access_ctr; // may change even in a const object
	// other members as before
};
void Screen::some_member() const {
	++access_ctr;    // keep a count of the calls to any member function
	// whatever other work this member needs to do
}

类类型数据成员的初始值

在C++11标准中,默认初始化类类型数据成员的最好的方式,是将其指定为类内初始值。

class Window_mgr {
private:
	// Screens this Window_mgr is tracking
	// by default, a Window_mgr has one standard sized blank Screen
	std::vector<Screen> screens{Screen(24, 80, ' ') }; 
};

类内初始值必须使用 = 形式的初始化,或者花括号括起来的直接初始化形式。

返回 this 的函数

class Screen {
public:
	Screen &set(char);
	Screen &set(pos, pos, char);    // other members as before 
};
inline Screen &Screen::set(char c) {
	contents[cursor] = c; // set the new value at the current cursor location
	return *this;         // return this object as an lvalue
} 
inline Screen &Screen::set(pos r, pos col, char ch) {
	contents[r*width + col] = ch;  // set specified location to given value
	return *this;                  // return this object as an lvalue 
}

set 成员返回的是被调用对象的引用。返回引用的函数是左值,意味着它们返回对象本身,而不是对象的副本。

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

从 const 成员函数返回 this

Screen myScreen;
// if display returns a const reference, the call to set is an error
myScreen.display(cout).set('*');

如果一个 const 成员函数返回 *this 作为引用,它的返回类型是 const 引用。

基于 const 的重载

class Screen {
public:
	// display overloaded on whether the object is const or not
	Screen &display(std::ostream &os) { do_display(os); return *this; }
	const Screen &display(std::ostream &os) const { do_display(os); return *this; }
private:
	// function to do the work of displaying a Screen
	void do_display(std::ostream &os) const {os << contents;}
	// other members as before
};
Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout);   // calls non const version
blank.display(cout);               // calls const version

建议:对于公共代码使用私有功能函数

定义一个单独的 do_display 操作的原因:

  1. 避免在多处使用同样的代码。
  2. 我们预期随着类的发展,display 操作将变得更加复杂。这时,把相应的操作写在一处而非两处的作用更明显。
  3. 我们可能想在开发期间将调试信息添加到 do_display 中,而在最终产品代码版本中删除这些信息。如果只需要更改 do_display 的定义以添加或删除调试代码,这样做会更容易。
  4. 这个额外的函数调用不会增加运行时开销。因为 do_display 是在类内定义的,它是隐式 inline 的。

在实践中,设计良好的 C++程序往往含有大量类似于 do_display 的小函数,它们被调用来完成某些其他函数的“实际”工作。

类类型

每个类定义唯一的类型。即使两个类有相同的成员列表,它们也是不同的类型。

struct First {
	int memi;
	int getMem();
};
struct Second {
	int memi;
	int getMem();
};
First obj1;
Second obj2 = obj1; // error: obj1 and obj2 have different types

可以使用类名作为类型名,直接指向类类型。或者,在关键字 class 或 struct 后面使用类名。

Sales_data item1;       // default-initialized object of type Sales_data
class Sales_data item1; // equivalent declaration

第二种方法继承自 C 语言,在 C++ 中同样有效。

类声明

可以在没有定义类的情况下声明类:

class Screen; // declaration of the Screen class

这个声明有时被称作前向声明 (forward declaration),引入名字 Screen 到程序中,并指明 Screen 是一种类类型。
在声明之后,且在看到定义之前,类型 Screen 是一个不完全声明 (incomplete type) —— 已经知道 Screen 是一个类类型,但不知道这个类型包含什么成员。

使用不完全类型的情况:可以定义指向这种类型的指针或引用,可以声明(但不能定义)使用不完全类型作为形参或返回类型的函数。

在创建类类型的对象之前必须定义该类,否则编译器不知道对象需要多少存储空间。
因为直到类的主体完成才是定义了类,所以一个类的数据成员类型不能是它自己。
但是,一旦一个类名可见,它就被认为是已声明(但尚未定义)。因此,类的数据成员类型可以是指向其自身类型的指针或引用:

class Link_screen {
	Screen window;
	Link_screen *next;
	Link_screen *prev;
};

友元再探

类可以定义普通的非成员函数作为友元。
类还可以使其他的类成为其友元,或者可以声明其他类(已定义)特定的成员函数作为友元。
友元函数可以定义在类的内部。这样的函数是隐式内联的。

类之间的友元关系

class Screen {
	// Window_mgr members can access the private parts of class Screen
	friend class Window_mgr;
	// ... rest of the Screen class
};

如果一个类指定了友元类,则友元类的成员函数可以访问该类的所有成员,包括非公有成员。

class Window_mgr {
public:
	// location ID for each screen on the window
	using ScreenIndex = std::vector<Screen>::size_type;
	// reset the Screen at the given position to all blanks
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
	// s is a reference to the Screen we want to clear
	Screen &s = screens[i];    // reset the contents of that Screen to all blanks
	s.contents = string(s.height * s.width, ' ');
}

注意:友元关系不是传递性的。即,如果类 Window_mgr 有自己的友元,这些友元没有访问 Screen 的特权。

令成员函数作为友元

当把一个成员函数声明为友元时,必须指明函数是哪个类的成员。

class Screen {
	// Window_mgr::clear must have been declared before class Screen
	friend void Window_mgr::clear(ScreenIndex);
	// ... rest of the Screen class
};

令成员函数作为友元,需要仔细组织程序的构造,以适应声明和定义之间的相互依赖性。
在本例中,必须按以下顺序编写程序:

  1. 首先,定义 Window_mgr 类,它声明但不能定义 clear。在 clear 使用 Screen 的成员之前必须声明 Screen。
  2. 接着,定义 Screen 类,包括对于 clear 的友元声明。
  3. 最后,定义 clear,此时它可以使用 Screen 的成员。

友元声明和作用域

类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在友元声明中时,这个名字隐式地假定在当前作用域中。然而,友元本身实际上不是声明在那个作用域中。
即使在类中定义了这个函数,仍必须在类的外部提供该函数的声明,以使该函数可见。即使只是从声明友元的类的成员中调用友元,也必须存在声明:

struct X {
	friend void f() { /* friend function can be defined in the class body   */ }
	X() { f(); } // error: no declaration for f
	void g();
	void h();
};
void X::g() { return f(); } // error: f hasn't been declared
void f();                   // declares the function defined inside X
void X::h() { return f(); } // ok: declaration for f is now in scope

友元声明影响访问权限,但不是普通意义上的声明。


练习:Person 类

// person.h
class Person {
friend std::ostream &print(std::ostream&, const Person&);
friend std::istream &read(std::istream&, Person&);
public:
	Person() = default;
	Person(const std::string &s): name(s) { }
	Person(const std::string &sn, const std::string &sa): name(sn), address(sa) { }
	explicit Person(std::istream &);
	std::string getName() const { return name; }
	std::string getAddress() const {return address; }
private:
	std::string name;
	std::string address;
};
// nonmember Sales_data interface functions
std::ostream &print(std::ostream&, const Person&);
std::istream &read(std::istream&, Person&);
// person.cpp
#include <iostream>
#include "person.h"
using namespace std;

Person::Person(std::istream &is) {
	read(is, *this);
}

ostream &print(ostream &os, const Person &item) {
	os << item.getName() << " " << item.getAddress();
	return os; 
}

istream &read(istream &is, Person &item) {
	is >> item.name >> item.address;
	return is;
}

学习目录:【C++ primer】目录

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值