委员长 发表于 2025-1-4 22:44:16

C++Primer学习笔记07.类

## 类

- 定义抽象数据类型
- 访问控制与封装
- 类的其他特性
- 类的作用域
- 构造函数再探
- 类的静态成员

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

类要想实现数据抽象和封装,需要首先定义一个抽象数据类型(abstract data type)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。

### 定义抽象数据类型

为什么要定义抽象数据类型?当我们设计类的接口时,应该考虑如何才能使得类易于使用;而当我们使用类时,不应该顾及类的实现机理。利用抽象数据类型来定义使用规则,而具体的实现由服务器端程序员自行实现。

#### 设计SalesData类

定义一个图书售卖类

```cpp
#include <iostream>

struct SalesData {
    // 这里const的作用是修改隐式this指针的类型,
    // 默认情况下,this的类型是指向类类型非常量版本的常量指针。
    std::string isbn() const { return bookNo; };

    SalesData& combine(const SalesData &);

    double avePrice() const;

    std::string bookNo;
    unsigned unitsSold = 0;
    double revenue = 0.0;
};

SalesData add(const SalesData &, const SalesData &);

std::ostream &print(std::ostream &, const SalesData);

std::istream &read(std::istream &, SalesData &);
```

C++ 类内部同样有一个隐式的 this,指向当前对象。

> 这里解释下为什么 `isbn() const` 后面要跟着 const。

const 的作用是修改隐式 this 指针的类型。在 SalesData 成员函数中,this 的类型是 `SalesData *const`。尽管 this 是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把 this 绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。

如果 isbn 是一个普通函数而且 this 是一个普通的指针参数,假如我们不希望在函数内部修改类的成员变量的值,因此我们应该把 this 声明成 const Sales_data *const。而且,把 this 设置为指向常量的指针有助于提高函数的灵活性(从何体现?)。

const 表示 this 是一个指向常量的指针。像这样使用 const 的成员函数被称作常量成员函数,类似于这样

```cpp
// 这样的代码是错误的,只是表达下意思
std::string SalesData::isbn(const SalesData *const this){
    return this->isbn;
}
```

> <b>类作用域和成员函数</b>

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

> <b>类的外部定义成员函数</b>

在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。

```cpp
double SalesData::avePrice() const {
    return this->revenue/unitsSold;
}
```

`::` 是作用域运算符,表示该函数被声明在 SalesData 的作用域内。

> <b>返回 this 对象</b>

```cpp
SalesData &SalesData::combine(const SalesData &data) {
    unitsSold += data.unitsSold;
    revenue += data.revenue;
    // 解引用拿到对象
    return *this;
}
```

#### 定义类相关的非成员函数

从上代码可知,add, print, read 不属于类本身,从概念上讲属于类接口的组成部分。

同样,通常把非成员函数的声明和定义分开。函数从概念是属于类,虽然不定义在类型中,也同样与类声明在同一个头文件中。

<span style="color:orange">一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类放在同一个头文件中</span>

完整代码如下

```cpp
// Sales_data.h
#include <iostream>

using std::cout;
using std::cin;
using std::endl;

struct SalesData {
    // 这里const的作用是修改隐式this指针的类型,
    // 默认情况下,this的类型是指向类类型非常量版本的常量指针。
    std::string isbn() const { return bookNo; };

    SalesData &combine(const SalesData &);

    double avePrice() const;

    std::string bookNo;
    unsigned unitsSold = 0;
    double revenue = 0.0;
};

SalesData add(const SalesData &, const SalesData &);
std::ostream &print(std::ostream &, const SalesData);
std::istream &read(std::istream &, SalesData &);
```

```cpp
// Sales_data.cpp 文件
#include"Sales_data.h"

SalesData &SalesData::combine(const SalesData &data) {
    unitsSold += data.unitsSold;
    revenue += data.revenue;
    // 解引用拿到对象
    return *this;
}

double SalesData::avePrice() const {
    return this->revenue;
}

SalesData add(const SalesData &item1, const SalesData &item2) {
    // 地址不一样,确实是只拷贝数据。
    // 在类中也是一样的,将数据拷贝过去。
    // 但是如果类中含有指针的话,指针指向的会是同一块内存空间!切记!
    cout << &item1 << ":" << &item2 << endl;
    SalesData sum = item1; // 把 item1 的數據拷貝給 sum
    sum.combine(item2);
    return sum;
}

std::ostream &print(std::ostream &os, const SalesData &item) {
    os << item.isbn() << " " << item.unitsSold << " " << item.revenue << " " << item.avePrice();
    return os;
}

std::istream &read(std::istream &is, SalesData &item) {
    double price = 0;
    is >> item.bookNo >> item.unitsSold >> price;
    item.revenue = price * item.unitsSold;
    return is;
}

int main() {
    SalesData data1;
    SalesData data2;
    SalesData data = add(data1, data2);
    cout << data.avePrice() << endl;
}
```

#### 构造函数

构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。注意:构造函数不能被声明成 const 的。

不定义构造函数的话,会有默认的无参构造函数。编译器在发现类不包含任何构造函数的情况下会替我们生成一个默认的构造函数。这点 Java 与 C++ 一致。

> <b>定义构造函数</b>

```cpp
#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

struct SalesData {
        // 定义默认构造函数,要求编译器生成构造函数。
    SalesData() = default;

    SalesData(const std::string &s) : bookNo(s) {}

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

    SalesData &combine(const SalesData &);

    double avePrice() const;

    std::string bookNo;
    unsigned unitsSold = 0;
    double revenue = 0.0;
};
// some code...
```

<span style="color:orange">注意:在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。</span>

> <b>构造函数初始值列表</b>

`SalesData(const std::string &s) : bookNo(s) {}`

`: bookNo(s)` 赋值的方式为类内初始值,C++11 的特性。

通常,构造函数使用类内初始值是一种好的选择,因为只要这样的初始值存在我们就能确保为成员赋予了一个正确的值。不过,如果所用的编译器不支持类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。

没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。在 SalesData 中 bookNo 将被初始化成 s ,而 units_sold 和 revenue 将是 0。

#### 拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等。当我们使用了赋值运算符时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁,当 vector 对象(或者数组)销毁时存储在其中的对象也会被销毁。

<span style="color:orange">如果我们不主动定义这些操作,则编译器会为我们提供默认的方法。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。</span>

编译器执行如下赋值语句时

```cpp
total = trans; // 处理下一本书的信息
```

它的行为与下面的代码相同

```cpp
// SalesData 的默认赋值操作等价于
total.bookNo = trans.bookNo;
total.unitsSold = trans.unitsSold;
```

> <b>需要时请自己定义这些方法!</b>

尽管编译器能替我们生成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说生成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。

### 访问控制与封装

使用访问控制修饰符来限制别人对类的使用,达到封装的目的,只将希望别人看到的展示给他。

- public:整个程序内可被访问,我们使用 public 成员定义类的接口,将这些接口暴露给用户程序员。
- private:成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了(即隐藏了)类的实现细节。

使用访问修饰符重新定义 SalesData 类

```cpp
#include <string>

class SalesData2 {
public:
    SalesData2() = default;
    // some code
private:
    std::string bookNo;
    unsigned unitsSold = 0;
    // some code
};
```

#### class和struct

struct 和 class 都可以用于定义类,但是 struct 和 class 的默认访问权限不太一样。

<span style="color:orange">如果我们使用 struct 关键字,则定义在第一个访问说明符之前的成员是 public 的;相反,如果我们使用 class 关键字,默认的访问权限是 private 的。</span>

当我们希望定义的类的所有成员是 public 的时,使用 struct;反之,如果希望成员是 private 的,使用 class。

#### 友元

SalesData 的数据成员是 private 的,我们的 read、print 和 add 函数也就无法正常编译了,这是因为尽管这几个函数是类的接口的一部分,但它们不是类的成员。类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。

<span style="color:orange">如果类想把一个函数作为它的友元,只需要增加一条以 friend 关键字开始的函数声明语句即可</span>

```c++
#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class SalesData2 {
    friend SalesData2 add(const SalesData2 &, const SalesData2 &);
    friend std::ostream &print(std::ostream &, const SalesData2);
    friend std::istream &read(std::istream &, SalesData2 &);

public:
    SalesData2() = default;

    SalesData2(const std::string &s) : bookNo(s), unitsSold(0), revenue(0) {}

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

    SalesData2 &combine(const SalesData2 &);

    unsigned getUnitsSold() {
      return this->unitsSold;
    }

    string getBookNo() {
      return this->bookNo;
    }

    double getRevenue() {
      return this->revenue;
    }

private:
    double avePrice() const;

    std::string bookNo;
    unsigned unitsSold = 0;
    double revenue = 0.0;
};
// SalesData2 接口的非成员组成部分的声明
SalesData2 add(const SalesData2 &, const SalesData2 &);
std::ostream &print(std::ostream &, const SalesData2);
std::istream &read(std::istream &, SalesData2 &);
```

<span style="color:orange">友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明!</span>

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。

不同编译器对友元的一些处理也不一样,,,但是上面的写法是统一的。

### 类的其他特性

介绍类的其他特性,包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用类类型及友元类的更多知识。

#### 类成员再探

定义两个类,Screen 和 WindowMgr。

```cpp
//
// Created by Admin on 2022/11/29.
//

#ifndef PRIMER_SCREEN_H
#define PRIMER_SCREEN_H

#include <iostream>

using std::string;

class Screen {
public:
    typedef std::string::size_type pos;

    Screen() = default;

    Screen(pos ht, pos wd, char c) :
            height(ht),
            width(wd),
            contents(ht * wd, c) {}

    // 隐式内联
    char get() const { return contents; }

    // 显示内联
    inline char get(pos ht, pos wd) const;

    Screen &move(pos r, pos c);

    void count() const;

    size_t getAccessCtr() { return access_ctr; }

private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
    // 即使在一个 const 对象内也可以被修改。用于寄了对象被创建的次数。
    mutable size_t access_ctr;
};

#endif //PRIMER_SCREEN_H
```

```cpp
#include"Screen.h"

inline Screen &Screen::move(pos r, pos c) {
    pos row = r * width;
    cursor = row + c;
    return *this;
}

/*
* 我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义:虽然我们无须在声明和定义的地方同时说明inline,但这么做其实是合法的。不过,最好只在类外部定义的地方说明inline,这样可以使类更容易理解。
* */
char Screen::get(pos r, pos c) const {
    pos row = r * width;
    return contents;
}

void Screen::count() const {
    this->access_ctr++;
}
```

从代码中可以看出,我们可以在类的内部把 inline 作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用 inline 关键字修饰函数的定义:虽然我们无须在声明和定义的地方同时说明 inline,但这么做其实是合法的。不过,最好只在类外部定义的地方说明 inline,这样可以使类更容易理解。

> <b>可变数据成员</b>

有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个 const 成员函数内。可以通过在变量的声明中加入 mutable 关键字做到这一点。

```cpp
// 测试
using std::cout;
using std::endl;

int main() {
    Screen screen;
    screen.count();
    screen.count();
    screen.count();
    // 3
    cout << screen.getAccessCtr() << endl;
    return 0;
}
```

如果去掉 mutable 修饰符,调用 count 方法时会报错。

```text
error: increment of member ‘Screen::access_ctr’ in read-only
object this->access_ctr++;
```

> <b>类数据成员的初始值</b>

如果我们希望 WindowMgr 类开始时总是拥有一个默认初始化的 Screen。在 C++11 新标准中,最好的方式就是把这个默认值声明成一个类内初始值

```cpp
class WindowMgr{
private:
        std::vector<Screen> screens{Screen(24,80,' ')};
}
```

<span style="color:orange">当我们提供一个类内初始值时,必须以符号=或者花括号表示。</span>

#### 返回 \* this 的成员函数

```cpp
inline Screen &Screen::move(pos r, pos c) {
    pos row = r * width;
    cursor = row + c;
    return *this;
}
```

\* this 解引用,拿到指针所指向的对象。返回 \* this 的话,后面可以链式调用成员函数。

假如当初我们定义的返回类型不是引用,则 move 的返回值将是*this 的副本(对象的副本),因此调用 set 只能改变临时副本,而不能改变 myScreen 的值。

```cpp
screen.move(4,0).set('#');
// 相当于
Screen tmp = screen.move(4,0);
tmp.set('#');
// 修改的是 tmp 这个副本而非原本的对象。
```

> <b>从 const 成员函数返回 \* this</b>

得到的是一个不可变的对象,尝试修改这个对象会发生错误。

> <b>基于 const 的重载</b>

非 const 修饰的可以被转型为 const,反之则不行。const 修饰的兼容性更强。

```cpp
//
// Created by Admin on 2022/11/29.
//

#ifndef PRIMER_SCREEN_H
#define PRIMER_SCREEN_H

#include <iostream>

using std::string;
using std::cout;
using std::cin;
using std::endl;

class Screen {
public:
    typedef std::string::size_type pos;

    Screen() = default;

    // const 修饰,则 this 指向的是一个常量对象,
    // 因此 *this 解析出来的是 const 对象,
    // 方法上需要加 const 前缀
    const Screen &display() const {
      doDisplay();
      return *this;
    }

    Screen &display() {
      doDisplay();
      return *this;
    }


private:
    void doDisplay() const {
      cout << "show some message" << endl;
    }
};

#endif //PRIMER_SCREEN_H
```

#### 类类型

```cpp
Screen scr0;
class Screen scr1;// C 语言风格
```

可以仅仅声明不定义

```cpp
int main(){
    class Screen; // Screen 类的声明
    return 0;
}
```

#### 友元类

有点破坏封装的特性了。给类加上 friend 关键字就可以了。

```cpp
#include<iostream>

using std::cout;
using std::cin;

class B; // 类 B 的前向声明
class A {};

// 声明类 A 是 B 的友元类
class B {
    friend class A;
};
```

WindowsMsg 声明为 Screen 的友元类,因此 WindowsMsg 可以访问 Screen 的私有成员。

```cpp
#include <iostream>
#include<vector>

using std::string;
using std::cout;
using std::cin;
using std::endl;
using std::vector;

class Screen {
public:
    typedef std::string::size_type pos;
    friend class WindowsMsg;
    Screen() = default;
    Screen(pos ht, pos wd, char c) :
            height(ht),
            width(wd),
            contents(ht * wd, c) {}
    std::string contents;

private:
    pos cursor = 0;
    pos height = 0, width = 0;
    // 即使在一个 const 对象内也可以被修改。用于寄了对象被创建的次数。
    mutable size_t access_ctr;
};

//==============================
#include "Screen.h" // 引入了 Screen 相当于拿到了 Screen 的定义
class WindowsMsg {
public:
    // 存放的非指针类型,不能用 new
    vector<Screen> screens{Screen(5, 5, 'c')};

    inline void test() {
      cout << screens.contents << endl;
    }
};

int main() {
    WindowsMsg msg;
    msg.test();
    return 0;
}
// ccccccccccccccccccccccccc
```

<span style="color:red">注意:友元关系不存在传递性,即朋友的朋友不是朋友。</span>

> <b>仅指定某些成员函数作为友元</b>

可以仅指定某写方法为类 BB 的友元方法。

```cpp
#include<iostream>

using std::cout;
using std::cin;

class BB; // 前向声明类 BB

class AA {
public:
    int func(); // 声明函数成员
};

class BB {
    friend int AA::func(); // 声明类 A 的成员函数 func 为 友元函数
private:
    int bbAge = 10;
};

```

一个完整的示例

```cpp
#include<iostream>
#include <math.h>

using namespace std;

class Point;

class Circle {
public:
    float getArea(Point &p1, Point &p2);

private:
    const float PI = 3.14;
};


class Point {
    // 为 Point 的友元函数
    friend float Circle::getArea(Point &p1, Point &p2);

public:
    Point(float x, float y);

    ~Point();

private:
    float x;
    float y;
};

inline Point::Point(float x, float y) {
    this->x = x;
    this->y = y;
}

inline Point::~Point() {}

// 要放在 Point 类后面定义才行。
// 先知道 Circle::getArea 被声明为了友元函数
// 才可以访问 Point 的私有变量
float Circle::getArea(Point &p1, Point &p2) {
    cout << abs(p1.x - p2.x) * abs(p1.y - p2.y) << endl;
}

int main() {
    Point p1(1, 1);
    Point p2(2, 2);
    Circle circle;
    cout << circle.getArea(p1, p2);
}
```

> <b>友元注意事项</b>

面向对象程序设计来讲,友元破坏了封装的特性。但由于友元简单易用,因此在实际开发中较为常用,如数据操作、类与类之间消息传递等,可以提高访问效率。使用友元需要注意以下几点:

①友元声明位置由程序设计者决定,且不受类中 public、private、protected 权限控制符的影响。

②友元关系是单向的,即类 A 是类 B 的友元,但 B 不是 A 的友元。

③友元关系不具有传递性,即类 C 是类 D 的友元,类 E 是类 C 的友元,但类 E 不是类 D 的友元。

④友元关系不能被继承。
页: [1]
查看完整版本: C++Primer学习笔记07.类