• 关于声明、定义、前向声明、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友元类。

  • 相关阅读:
    解决不同浏览器文件下载文件名乱码问题(Java)
    zabbix常见问题
    axiosapi,js结构化定义、调用业务api接口。 no
    Java基础:反射小结
    Java基础:函数式编程的函数的本质
    Java基础:Java方法的签名的定义
    Java基础:异常怎么分类的(面试题:Exception和Error的区别),看完这篇就都捋清了
    编程类型和代表性语言
    Java基础:构造器小结
    ubuntu搭建wifi热点,共享网络,超简单
  • 原文地址:https://www.cnblogs.com/jixiaohua/p/12578719.html
Copyright © 2020-2023  润新知