《C++ Primer》第7章 类(一)

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

类的基本思想是数据抽象(data abstraction)封装(encapsulation)。数据抽象是依赖于**接口(interface)实现(implementaion)**分离的编程技术。类的接口包括用户能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体、私有函数。

7.1 定义抽象数据类型(P228)

7.1.1 设计Sales_data类(P228)

7.1.2 定义改进的Sales_data类(P230)

struct Sales_data {
	string isbn() const { return bookNo; }
	Sales_data& conbine(const Sales_data&);
	double avg_price() const;
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);

成员函数必须声明在类的内部,但即可以定义在类内部,也可以定义在类外部。定义在类内部的函数是隐式 inline 函数。

定义成员函数

引入this

我们调用成员函数时,实际上是替某个对象调用它。成员函数通过一个名为 this额外隐式参数来访问调用它的对象。当我们调用一个函数时,编译器负责用请求该函数的对象地址初始化 this

在成员函数内部,任何对类成员的直接访问都被看作 this 的隐式引用。因为 this 总是指向“这个”对象,所以 this 是一个指针常量。

引入const成员函数

上面的 isbn 函数在其参数列表紧跟了一个 const 关键字,作用是修饰 this 的类型。

由于默认情况下 this指向非常量的指针常量,所以普通成员函数是不能被常量对象调用的。通过 const 关键字将 this 设置为指向常量的指针常量可以提高函数的灵活性,这样的函数叫做常量成员函数

常量成员函数不能改变调用它的对象的内容。

类作用域和成员函数

编译器分两步处理类:先编译成员(数据成员和函数成员)的声明,然后再编译函数成员的函数体。(所以成员函数体可以随意使用类中的其他成员,而无需在意出现的次序)。

在类外部定义成员函数

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

定义一个返回this对象的函数

Sales_data& Sales_data::combine(const Sales_data &rhs){
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

7.1.3 定义类相关的非成员函数(P234)

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

7.1.4 构造函数(P235)

**构造函数(constructor)**的任务是初始化类对象的数据成员,只要类的对象被创建,就会执行构造函数。

构造函数的名字和类名相同,没有返回类型。构造函数不能声明为 const 。当我们创建一个类的 const 对象时,对象直到构造函数完成初始化过程后(注意:完成初始化过程不等于执行完构造函数的函数体)才获得“常量”属性。

struct DATA{
    int x;
    DATA(int x):x(x){
        x = 1;
    }
};

const DATA d(2);    // d.x的值为2

合成的默认构造函数

如果我们的类没有显式定义任何构造函数,编译器将为我们隐式定义一个默认构造函数(default constructor),称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,合成的默认构造函数按照如下规则初始化类的数据成员:

  • 如果存在类内初始值,用它来初始化成员。
  • 否则,执行默认初始化。

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

对于一个普通的类来说,必须定义它自己的默认构造函数,原因如下:

  • 编译器只有在类中不包含任何构造函数时才会合成默认构造函数。
  • 合成的默认构造函数可能执行错误的初始化,如内置类型的默认初始化。
  • 编译器不能为某些类合成默认构造函数,如类中包含一个其他类类型的成员,而这个成员的类型没有默认构造函数。

= default的含义

Sales_data() = default;

在 C++11 新标准中,允许我们通过在参数列表后面加上 = default 来要求编译器生成默认构造函数。

构造函数初始值列表

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

上述两个构造函数的定义中,使用了构造函数初始值列表(constructor initialize list),它负责为新创建的对象的数据成员赋初值。如果某个数据成员被构造函数初始值列表忽略,它将以与合成的默认构造函数相同的方式隐式初始化

在类的外部定义构造函数

7.1.5 拷贝、赋值和析构(P239)

拷贝、赋值和销毁对象操作和初始化一样,如果我们不主动定义,编译器将替我们合成它们。

7.2 访问控制与封装(P240)

我们使用**访问说明符(access specifiers)**加强类的封装性:

  • 定义在 public 之后的成员在整个程序内可以被访问,public 成员定义类的接口。
  • 定义在 private 之后的成员可以被类的成员函数访问。

一个类可以包含零个或多个访问说明符,某种访问说明符能出现多少次也没有规定。

使用classstruct关键字

类中所有出现在第一个访问说明符之前的成员使用默认的访问权限class 默认 privatestruct 默认 public

7.2.1 友元(P241)

类可以允许其他类或函数访问它的非公有成员,方式是令其他类或者函数称为它的友元(friend)

class Sales_data {
	friend Sales_data add(const Sales_data&, const Sales_data&);
	friend ostream& print(ostream&, const Sales_data&);
	friend istream& read(istream&, Sales_data);
public:
	string isbn() const { return bookNo; }
	Sales_data& conbine(const Sales_data&);
private:
	double avg_price() const;
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);

友元声明只能出现在类定义的内部,不受所在区域访问控制级别的约束。

友元的声明

友元的声明仅仅指定了访问权限,而非通常意义上的函数声明,所以我们需要在友元声明之外再对函数进行一次声明。

我们通常将对友元函数的额外声明与类本身放置在同一个头文件中。需要注意的是,很多编译器允许在尚无友元函数真正声明的情况下就调用它,但为了保险起见最好还是提供一个独立的声明。

7.3 类的其他特性(P243)

7.3.1 类成员再探(P243)

定义一个类型成员

类可以自定义某种类型在类中的别名,由类定义的类型别名和其他成员一样可以设置访问限制:

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

注意:类型成员必须先定义后使用(与其他成员不同),所以类型成员通常写在类开始的地方。

Screen类的成员函数

class Screen {
public:
	using pos = string::size_type;
	Screen() = default;    // 使用生成的默认构造函数
	Screen(pos ht, pos wd, char c):height(ht), width(wd),
		contents(ht*wd, c){}
	char get()const { return contents[cursor]; }    // 隐式内联
	inline char get(pos ht, pos wd)const;    // 显式内联
	Screen &move(pos r, pos c);    // 可以在之后被设为内联
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	string contents;
};

令成员作为内联函数

我们可以在类内部定义函数,从而使其自动成为 inline 函数,也可以在类内声明时显式添加 inline ,还可以在类外定义是再用 inline 修饰。

最好在类外部定义的地方说明 inline

重载成员函数

和普通重载函数相同。

可变数据成员

我们可以在变量声明中加入 mutable 来定义可变数据成员(mutable data member)。可变数据成员永远不会是 const ,即使它是 const 对象的成员。所以 const 成员函数可以修改可变成员的值。

类数据成员的初始值

class Window_mgr {
private:
	vector<Screen> screens{ Screen(24, 80, ' ') };
};

提供类内初始值时,必须使用 = 或花括号。

7.3.2 返回*this的成员函数(P246)

我们继续向上面的 Screen 类中添加一些函数:

class Screen{
public:
	Screen &set(char);
    Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char ch){
    contents[cursor] = ch;
    return *this;    // 返回对象本身
}
inline Screen &Screen::set(pos r, pos col, char ch){
    contents[r*width + col] = ch;
    return *this;    // 返回对象本身
}

返回引用的函数可以连用

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

const成员返回*this

假设我们需要在 Screen 中添加一个 display 操作,并希望它能和 moveset 等操作连用,那么 display 也应该返回 *this 。由于 display 只负责打印而不改变内容,所以我们应该将它定义成 const 成员,这时 *this 是常量对象,不能用来初始化普通对象的引用。

基于const的重载

class Screen {
public:
    // 根据对象是否为const选择重载函数
	Screen &display(ostream &os) { do_display(os); return *this; }
	const Screen &display(ostream &os) const { do_display(os); return *this; }
private:
	void do_display(ostream &os) const { os << contents; }
};

单独设计一个 do_display 私有成员的好处:

  • 避免在多出使用同样的代码。
  • 只在 do_display 一处进行修改比较简单。
  • 这个额外函数不会产生额外开销,因为它是隐式内联的。

7.3.3 类类型(P249)

对于两个类来说,即使它们的成员完全一样,这两个类也是不同的类型。

我们可以把类名当作类型的名字来使用也可以把类名跟在 classstruct 后面:

Sales_data item1;
class Sales_data item2;    // 等价的声明,从C语言继承而来

类的声明

我们可以仅仅声明类而暂时不定义:

class Screen;

这种声明被称作前向声明(forward declaration),在 Screen 被定义之前是一个不完全类型(incomplete type)。不完全类型只能在非常有限的场景下使用:可以定义指向这种类型的指针或引用、可以声明(不能定义)以不完全类型为返回类型或参数的函数。

一旦一个类的名字出现后,就被认为是声明过了(但尚未定义,所以此时类是不完全类型),所以类允许包含自身类型的引用或指针

7.3.4 友元再探(P250)

类除了可以把普通函数声明成友元外,还可以把其他类或类的成员函数声明成友元。友元函数也可以定义来类内部。

类之间的友元关系

假设我们为 Window_mgr 添加一个名为 clear 的成员,负责把一个指定的 Screen 对象的内容设为空白。显然,clear 需要访问 Screen 的私有成员,所以我们把 Window_mgr 声明成 Screen 的友元:

class Screen{
    friend class Window_mgr;
};

如果指定一个类为友元,则友元类的成员函数可以访问本类的所有成员。需要注意的是,友元关系不存在传递性,每个类负责控制自己的友元

令成员函数作为友元

把成员函数声明成友元时,须指定该成员函数属于哪一个类:

class Screen{
    friend void Window_mgr::clear(ScreenIndex);
};

这部分有点小坑,似乎和类的作用域有关,待更新。。。

函数重载和友元

重载函数虽然名字相同,但本质上是不同的函数,每个重载函数需要单独的友元声明。

友元声明和作用域

类和非成员函数的声明的声明不是必须在它们的友元声明之前。

前面提到,友元声明并不是真正的函数声明,所以我们就算在类内部定义了友元函数,也必须在类外部提供相应的声明。

class X {
public:
	friend void f() { cout << "hello"; }
	X() { f(); }    // 错误:f还没有被声明
};

友元声明不要求名字在之前被声明过,但要想使用这个名字,就必须在真正声明之后。

你可能感兴趣的:(《C++,Primer》,c++)