关于声明、定义、前向声明、include、循环依赖、普通友元函数、友元类、友元成员函数的总结

做《C++ Primer》(第5版)253页练习题7.3.4有感,故总结之

1 声明

1.1 变量和函数的声明

常见的声明是声明一个变量或函数,一般在头文件.h中声明,例如:

pos cursor = 0;    // 给定初始值

char get(pos r, pos col) const;

1.2 类的声明

对于一个类,一般是直接在头文件中直接写 class ClassName { ... },这称之为类的定义,然后在类体{...}中又声明或定义了成员变量和成员函数。类的声明是没有类体,只有个类名:

class Screen;    // 声明一个类

这样声明的类是一个不完全的类型,无法创建它的对象或访问其成员。

2 定义

定义就是给函数定义函数体部分,或给类定义类体部分,或给变量赋值。一般在头文件中声明函数或变量,然后在对应的源文件中定义函数。

对于一个class,一般地是在头文件中定义的,类的成员是一个个的函数或变量声明,然后在源文件中#include xxx.h,再定义类的成员函数。

在.cpp文件中#include .h文件后,此时类已经定义完成了,所以可以访问它的成员。

3 前向声明

一般有函数或类的前向声明,类的前向声明一般用在头文件中,比如定义类的时候需要某一个类类型,可以不用访问其成员或创建对象,但此时该类还没有被定义,无法include它,此时可以写一个前向声明先用着,之后在include了这个头文件的地方(.h.cpp)对这个前向声明定义。函数的前向声明一般用在源文件中(如果头文件中的函数声明不叫前向声明的话,其实前向声明和声明没什么区别)。

4 include与循环依赖

include将类定义或函数声明引入到当前文件中,但是如果两个类在其定义的头文件中相互include,然后使用其类类型,此时A类定义的时候B类还未定义,反之亦然。就发生了循环依赖错误。所以include只能单向引用,就像一颗倒着的树,从树根到树干,下面的include上面的,不能去访问逻辑上不存在的东西。

解决这种循环依赖的问题,直接的方法就是理清调用的父子关系,比如规定B.h引用A,然后在A.h中通过B的前向声明使用B类型。当然这样仅仅只能使用B类型,而不能访问B的成员或创建对象。

而在两个类的.cpp文件中相互include是合法的,因为此时两个类都已在其头文件中定义完成。

5 普通友元函数

注意:友元函数只是对函数的权限的声明,不等于函数声明。

普通友元函数的声明只需要将加了friend关键字的函数声明放在类中就可以,比较简单,然后定义在源文件中。

例如:Sales_data.h

#pragma once
#include <string>
#include <ostream>
#include <istream>

class Sales_data
{
// 为Sales_data的非成员函数所作的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&); 
// 其他成员及访问说明符与之前一致
public:
    // 构造函数
    // 空参的默认构造函数,居然还有这种操作,C++11标准的东西,表示默认行为
    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) { } 

    // 不同于上面三个类内定义的构造函数,这个将在类外(cpp文件)定义
    Sales_data(std::istream &is);

    ~Sales_data() = default;  // 析构函数

    // 成员函数: 关于Sales_data对象的操作
    // 注意: 定义在类内部的函数是隐式的inline函数,这里的const与this指针有关,在下面另开篇幅单独说明
    std::string isbin() const { return bookNo; }        // 返回对象的ISBN编号
    Sales_data& combine(const Sales_data &rhs);         // 将一个Sales_data对象加到另一个对象上

private:
    // 数据变量: 定义成员的属性
    std::string bookNo;                                 // ISBN编号
    unsigned units_sold = 0;                            // 销量
    double revenue = 0.0;                               // 总销售收入
    double avg_price() const;                           // 返回出售书籍的平均价格
};

// Sales_data的非成员接口函数,上面已声明为类的友元,可以访问私有成员
Sales_data add(const Sales_data&, const Sales_data&);   // 执行两个Sales_data对象的加法,后期以重载运算符代替
std::ostream &print(std::ostream&, const Sales_data&);  // 将Sales_data对象的值输出到ostream
std::istream &read(std::istream&, Sales_data&);         // 将数据从istream读入到Sales_data中

7 友元类

友元类的声明也比较简单,只需要将加了friend关键字的类型声明放在主类中,这里使用前向声明的友元类:

例如,有一个类:Window_msg,Window_msg如下

#pragma once
#include <vector>
#include "Screen.h"

/**
 * 窗口管理类
 * 表示显示器上的一组Screen
 */
class Window_mgr
{
public:
    Window_mgr();
    ~Window_mgr();

    // 窗口中每个屏幕的编号类型
    using ScreenIndex = std::vector<Screen>::size_type;
    
    // 按照编号将指定的Screen重置为空白
    void clear(ScreenIndex sindex);
private:
    // 这个Window_mgr追踪的Screen
    // 类内初始值:默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
    std::vector<Screen> screens{Screen(25, 25, ' ')};
    // 如上所示,当我们提供一个类内初始值时,必须以 "=" 或 "{ }" 表示的直接初始化
};

将其声明为另一个类:Screen的友元,Screen.h文件如下:

#pragma once
#include <string>
#include <ostream>

// 前向声明,防止循环引用,Window_mgr.h单向引用了Screen.h,所以符合先声明再定义的原则
class Window_mgr;
/**
 * 窗口类
 */
class Screen
{
public:
    // 声明Window_mgr类为Screen类的友元,友元类的成员函数可以访问此类的所有成员
    // 友元可以是某个类,某个类的成员函数,或普通函数
    friend class Window_mgr;     // 此时只用到Window_mgr类型,而不用其成员,所以可以使用前向声明

    // 声明一个类型别名,或可使用typedef,用户可以使用这个名字
    using pos = std::string::size_type;

    Screen() = default;     // 默认构造函数,内联
    ~Screen() = default;    // 默认析构函数,内联

    // 自定义构造函数
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(width * height, c){ }

    // 读取光标处的字符,隐式内联
    char get() const { return contents[cursor]; }

    // 重载,类成员函数要想声明内联,必须在声明的同时定义,就像上一个get一样,否则无法编译
    char get(pos r, pos col) const;

    // 设置光标所在位置的字符
    Screen &set(char c);

    // 设置给定位置的字符
    Screen &set(pos r, pos col, char c);

    // 移动光标
    Screen &move(pos r, pos c);

    // 打印屏幕内容,返回当前对象的常量引用
    Screen &display(std::ostream &os);

    // 根据调用对象是否是const重载display函数
    const Screen &display(std::ostream &os) const;

private:
    pos cursor = 0;                 // 鼠标位置
    pos height = 0, width = 0;      // 屏幕宽高
    std::string contents;           // 屏幕内容

    // 可变数据成员,即使在一个const对象内也能被修改,用来对函数调用计数
    mutable size_t access_ctr;

    void do_display(std::ostream &os) const;
};

声明友元类后,友元类的所有函数都可以访问主类的所有成员,显然不太安全。

8 友元成员函数

如上所示两个类,仅将Window_msg类的clear成员函数声明为Screen的友元函数,而不是整个类,这就要麻烦许多。

此时要在Screen.h中访问Window_msg的clear成员,显然不能再使用前向声明,必须#include Window_msg.h,所以:

  • 首先定义Window_msg类,其中声明clear函数,但是不能定义它。因为在clear使用Screen的成员之前必须先定义Screen
  • 接下来定义Screen,包括对clear的友元声明
  • 最后定义clear,此时它才可以使用Screen的成员

上面铺垫了许多声明、定义、前向声明就是为了理解这几句话,实现起来如下:

Window_mgr.h

#pragma once
#include <vector>

// 该类会被Screen类引用,所以这里可以使用前向声明
// 前向声明只能使用类型名,不能访问成员
class Screen;

/**
 * 窗口管理类
 * 表示显示器上的一组Screen
 */
class Window_mgr
{
public:
    Window_mgr();
    ~Window_mgr();

    // 窗口中每个屏幕的编号类型
    using ScreenIndex = std::vector<Screen>::size_type;
    
    // 按照编号将指定的Screen重置为空白
    void clear(ScreenIndex sindex);
private:
    // 这个Window_mgr追踪的Screen
    // 类内初始值:默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
    std::vector<Screen> screens;    // 前向声明无法访问成员,所以这里不能赋予默认值
    // 如上所示,当我们提供一个类内初始值时,必须以 "=" 或 "{ }" 表示的直接初始化
};

Screen.h

#pragma once
#include <string>
#include <ostream>
#include "Window_mgr.h"

/**
 * 窗口类
 */
class Screen
{
public:
    // 声明Window_mgr类为Screen类的友元,友元类的成员函数可以访问此类的所有成员
    // 友元可以是某个类,某个类的成员函数,或普通函数

    // 声明友元成员函数:将Window_mgr类的clear成员声明为Screen的友元
    // 要访问Window_mgr的成员就必须先定义它,然后include到这里,不能是前向声明
    friend void Window_mgr::clear(ScreenIndex si);

    // 声明一个类型别名,或可使用typedef,用户可以使用这个名字
    using pos = std::string::size_type;

    Screen() = default;     // 默认构造函数,内联
    ~Screen() = default;    // 默认析构函数,内联

    // 自定义构造函数
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(width * height, c){ }

    // 读取光标处的字符,隐式内联
    char get() const { return contents[cursor]; }

    // 重载,类成员函数要想声明内联,必须在声明的同时定义,就像上一个get一样,否则无法编译
    char get(pos r, pos col) const;

    // 设置光标所在位置的字符
    Screen &set(char c);

    // 设置给定位置的字符
    Screen &set(pos r, pos col, char c);

    // 移动光标
    Screen &move(pos r, pos c);

    // 打印屏幕内容,返回当前对象的常量引用
    Screen &display(std::ostream &os);

    // 根据调用对象是否是const重载display函数
    const Screen &display(std::ostream &os) const;

private:
    pos cursor = 0;                 // 鼠标位置
    pos height = 0, width = 0;      // 屏幕宽高
    std::string contents;           // 屏幕内容

    // 可变数据成员,即使在一个const对象内也能被修改,用来对函数调用计数
    mutable size_t access_ctr;

    void do_display(std::ostream &os) const;
};

Window_mgr.cpp

#include "Window_mgr.h"
#include "Screen.h"     // 定义中需要访问Screen的成员,所以此时要include,不能再使用前向声明

Window_mgr::Window_mgr(/* args */)
{
}

Window_mgr::~Window_mgr()
{
}

void Window_mgr::clear(ScreenIndex sindex)
{
    // s是一个Screen类型的引用,指向我们想清空的那个屏幕
    Screen &s = screens[sindex];
    // 将选定的Screen重置为空白
    s.contents = std::string(s.height * s.width, ' ');
}

重点:在友元类Window_msg的头文件中使用主类前向声明,在源文件中使用include主类,定义成员函数并访问主类成员,避免循环依赖问题。在主类头文件中直接include友元类。

posted @ 2020-03-27 01:39  姬无华  阅读(511)  评论(0编辑  收藏  举报