做《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
友元类。