天天看点

C++:33---类成员指针

成员指针概述:

  • 当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象
  • 直到使用成员指针时,才提供成员所属的对象
  • 成员指针是指可以指向类的非静态成员的指针
  • 一般情况下,指针指向一个对象,但是成员指针指向的是类的成员,而不是类的所创建出的对象
  • 类的静态成员不属于任何对象,因此无需特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有任何区别
  • 成员指针的类型囊括了类的类型以及成员的类型:
  • 下面我们定义一个类,作为本文讲解的基础:
class Screen {
public:
typedef std::string::size_type pos;
char get_cursor()const { return contents[cursor]; }
char get()const;
char get(pos ht, pos wd)const;
private:
std::string contents;
pos cursor;
pos height, width;
};      

一、数据成员指针

数据成员指针的定义

  • 特点:
  • 需要使用*来表示当前的变量是一个指针
  • 成员指针定义时必须包含所属的类
  • 指针的定义
  • 下面定义一个指向Screen对象的指针,指针的类型为string,并且指针为常量指针(因此不能通过这个指针修改值)
  • 由于指针的类型为string,因此该指针可以指向常量(非常量)Screen对象的string成员

const string Screen::*pdata;

  • 为指针赋值:
  1. //将pdata指向于Screen类的contents成员
  2. pdata = &Screen::contents;
  • 也可以在定义数据成员指针时直接初始化,并且使用auto或decltype来定义:

auto pdata = &Screen::contents;

使用数据成员指针

  • 指针定义之后,该指针没有指向于任何数据成员,指针只是指向于成员而非所属的对象,只有当解引用成员指针时我们才提供对象的信息
  • 例如:
  1. Screen myScreen;
  2. Screen *pScreen = &myScreen;
  3. //.*解引用*pdata以获得myScreen对象的contents成员
  4. auto s = myScreen.*pdata; //相当于myScreen.contents
  5. //->*解引用*pdata以获得myScreen对象的contents成员
  6. s = pScreen->*pdata; //相当于pScreen->contents

返回数据成员指针的函数

  • 在上面定义成员指针的时候,pdata不能出现在Screen类的外部,因为contents是private的(上面只是为了演示说明)
  • 为了体现封装性,我们通常定义一个成员函数,用该函数来返回成员的指针
  • 例如:
  1. class Screen {
  2. public:
  3. //成员函数,返回一个成员的指针
  4. static const std::string Screen::*data() {
  5. return &Screen::contents;
  6. }
  7. private:
  8. std::string contents;
  9. };
  • 我们将函数定义为static,其不属于任何对象,因此我们可以直接调用该函数来获得成员的指针
  • 当我们调用data时,就可以得到一个成员的指针。例如:
  1. int main()
  2. {
  3. //调用data()静态函数来获得一个成员的指针
  4. const std::string Screen::*pdata = Screen::data();
  5. return 0;
  6. }
  • 定义了之后,我们可以正常的使用这个指针。例如:
  1. int main()
  2. {
  3. Screen *pScreen = new Screen;
  4. const std::string Screen::*pdata = Screen::data();
  5. auto s = pScreen->*pdata; //等价于pScreen->contents
  6. return 0;
  7. }

二、成员函数指针

  • 与指向数据成员的指针类似,我们也可以声明一个指向于成员函数的指针
  • 语法注意事项:
  • 指向成员函数的指针也需要指定目标函数的返回类型和形参列表
  • 如果成员函数时const的或者是引用成员,则我们必须将const限定符或引用限定符包含进来

成员函数指针的定义

  • 最简单的方法就是使用auto来声明一个指向于成员函数的指针
  1. //pmf是一个函数指针,指向于get_cursor函数
  2. auto pmf = &Screen::get_cursor;
  • 因为定义成员函数指针时,需要同时指定目标函数的返回值类型和形参列表,因此对于有重载的成员函数不会造成冲突。例如:
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. char get_cursor()const { return contents[cursor]; }
  5. char get()const;
  6. char get(pos ht, pos wd)const;
  7. //...
  8. };
  9. int main()
  10. {
  11. //pmf2是一个成员函数指针,其指向于返回值为char,形参为两个Screen::pos类型的成员函数
  12. char (Screen::*pmf2)(Screen::pos, Screen::pos)const;
  13. //为pmf2指针赋值
  14. pmf2 = &Screen::get;
  15. return 0;
  16. }
  • 出于优先级的考虑,上述定义中指针两侧的括号不能缺少。如果没有这对括号,编译器将认为该声明是一个(无效的)函数声明:
  • 错误的原因:编译器会认为p是一个普通函数,并且返回Screen类的一个char成员。因为其是一个普通函数,所以不能使用const限定符
  1. //错误的语法,非成员函数p不能使用const限定符
  2. char Screen::*pmf2(Screen::pos, Screen::pos)const;
  • 和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则:
  1. char (Screen::*pmf2)(Screen::pos, Screen::pos)const;
  2. pmf2 = &Screen::get; //正确
  3. pmf2 = Screen::get; //错误,缺少&。在成员函数和指针之间不存在自动转换规则

使用成员函数指针

  • 和使用数据成员的指针一样,我们需要使用.*或者->*运算符作用域指向成员函数的指针,来调用类的成员函数
  • 例如:
  1. //pmf为成员函数指针,指向于get_cursor()函数
  2. auto pmf = &Screen::get_cursor;
  3. //pmf2为成员函数指针,指向于带有两个参数的get()函数
  4. char (Screen::*pmf2)(Screen::pos, Screen::pos)const = &Screen::get;
  5. Screen myScreen;
  6. Screen *pScreen = &myScreen;
  7. char c1 = (myScreen.*pmf)(); //等价于myScreen.get_cursor()
  8. char c2 = (pScreen->*pmf2)(0, 0);//等价于pScreen->get(0,0)
  • 在上面,在解引用成员函数的指针时需要在两侧加上括号,如果不加,那么会错误:
  • 错误的原因:我们想要调用名为pmf和pmf2的函数,然后使用这些函数的返回值作为指针指向成员运算符.*与->*的运算多想。然而pmf与pmf2并不是一个函数,因此代码错误
  1. char c1 = myScreen.*pmf(); //错误的
  2. //其等价于myScreen.*(pmf())
  3. char c2 = pScreen->*pmf2(0, 0); //错误的
  4. //其等价于myScreen->*(pmf2(0,0))

使用成员指针的类型别名

  • 使用类型别名或typedef可以让成员指针更容易理解
  • 例如,下面的类型别名将Action定义为两参数get函数的同义词:
  1. //Action是一种可以指向Screen成员函数的指针,其接受两个pos实参,返回值类型为char
  2. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  • 现在我们定义成员函数指针时就比较方便了:
  1. //get是一个指向成员函数的指针
  2. Action get = &Screen::get;
  • 和其他函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型。其中,指向成员的指针形参也可以拥有默认实参
  1. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  2. //action是一个函数,其中参数2为一个指针,并且其有默认实参,指向于Screen的get成员函数
  3. Screen& action(Screen&, Action = &Screen::get);
  • 当我们调用action函数的时候,只需将Screen的一个符合要求的函数的指针或地址传入即可:
  1. using Action = char (Screen::*)(Screen::pos, Screen::pos)const;
  2. Screen& action(Screen&, Action = &Screen::get);
  3. int main()
  4. {
  5. Screen myScreen;
  6. Action get = &Screen::get;
  7. action(myScreen); //使用默认实参
  8. action(myScreen, get); //参数2调用前面定义的指针变量get
  9. action(myScreen, &Screen::get); //参数2显式地传入地址
  10. return 0;
  11. }

成员指针函数表

  • 对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表中
  • 如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个
  • 假定Screen类含有几个成员函数,每个函数负责将光标向指定的方向移动:
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. //移动光标的一系列函数
  5. Screen& home();
  6. Screen& forward();
  7. Screen& back();
  8. Screen& up();
  9. Screen& down();
  10. private:
  11. pos cursor; //光标
  12. };
  • 这几个函数有一个共同点:都不接受任何参数,并且返回值是发生光标移动的Screen的引用
  • 现在我们开始设计函数表:
  • 在此之前,先定义一个静态成员Menu,该成员是指向光标移动函数的指针的数组
  • 定义一个move函数,使其可以调用上面的任意一个函数并执行对应的操作
  • 设计一个枚举用于函数传参
  1. class Screen {
  2. public:
  3. typedef std::string::size_type pos;
  4. Screen& home();
  5. Screen& forward();
  6. Screen& back();
  7. Screen& up();
  8. Screen& down();
  9. //函数指针
  10. using Action = Screen& (Screen::*)();
  11. //定义一个枚举
  12. enum Directions { HOME, FORWARD, BACK, UP, DOWN };
  13. //参数使用枚举来调用函数表中对应的函数
  14. Screen& move(Directions cm)
  15. {
  16. //必须使用this
  17. return (this->*Menu[cm])();
  18. }
  19. private:
  20. pos cursor;
  21. static Action Menu[]; //函数表
  22. };
  23. //初始化函数表,将内部移动光标的函数都添加进去
  24. Screen::Action Screen::Menu[] = {
  25. &Screen::home,&Screen::forward,
  26. &Screen::back,&Screen::up,
  27. &Screen::down };
  • 现在我们可以调用move函数了:
  1. int main()
  2. {
  3. Screen myScreen;
  4. myScreen.move(Screen::HOME); //调用muScreen.home
  5. myScreen.move(Screen::DOWN); //调用muScreen.down
  6. return 0;
  7. }

三、将成员函数用作可调用对象

成员指针不是可调用对象

  • 通过上面我们知道,想要调用成员函数指针,必须通过一个类配合.*运算符或->*运算符来调用。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符.
  • 因为成员指针不是可调用对象,所以我们不能直接将一个指向成员函数的指针传递给算法
  • 例如,下面在一个vector中寻找第一个空的string:
  1. std::vector<std::string> svec;
  2. auto fp = &std::string::empty; //fp指向string的empty函数,fp是一个成员函数指针
  3. //错误,必须使用.*或->*调用成员指针fp
  4. std::find_if(svec.begin(), svec.end(), fp);
  • find_if算法需要一个可调用对象,但我们提供给它的是一个指向成员函数的指针fp。因此在find_if的源码内部执行如下形式的代码,从而导致无法通过编译:
  1. //检查对当前元素的断言是否为真
  2. if(fp(*it)) //错误,想要通过成员指针调用函数,必须使用->*运算符
  • 显然该语句试图调用的是传入的对象,而非函数

①使用function生成一个可调用对象

  • function模板: 
  • 从指向成员函数的指针获取可调用对象的一种方法是使用function模板。下面的代码就是正确的了:
  1. std::vector<std::string> svec;
  2. //empty函数的返回值为bool,参数为const string&
  3. function<bool(const std::string&)> fcn = &std::string::empty;
  4. //现在是正确的了,fcn是一个可调用对象,使用.*调用empty
  5. std::find_if(svec.begin(), svec.end(), fcn);
  • 当一个function对象包含有一个指向成员函数的指针时,function类知道它必须使用正确的指向成员的指针运算符来执行函数调用。也就是说,我们可以认为在find_if内部有类似于下面的代码:
  1. //假设it是find_if内部的迭代器,则*it是一个string对象
  2. if(fcn(*it)) //fcn就是empty的函数指针,等价于empty(*it)
  • 其中,function将使用正确的指向成员的指针运算符。从本质上看,function类将函数调用转换成如下的形式:
  • 下面的调用代码与上面的原理是类似的
  1. std::vector<std::string*> svec;
  2. //empty函数的返回值为bool,参数为const string*
  3. function<bool(const std::string*)> fcn = &std::string::empty;
  4. //现在是正确的了,fcn是一个可调用对象,使用->*调用empty
  5. std::find_if(svec.begin(), svec.end(), fcn);

②使用mem_fn生成一个可调用对象

  • 通过上面知道,想要使用function,必须提供成员的调用形式。我们也可以采取另一种方法,就是使用标准库功能mem_fn来让编译器负责推断成员的类型
  • mem_fn也定义在functional头文件中,并且可以从成员指针生成一个可调用对象
  • 和function不同的是:
  • mem_fn可以根据成员指针的类型推断可调用对象的类型,而无须用户显式地指定
  • 例如:我们使用mem_fn生成一个可调用对象,该对象接受一个string实参,返回一个bool值(编译器自动推断的)
  • mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用:
  1. std::vector<std::string> svec;
  2. auto f = mem_fn(&std::string::empty); //f接受一个string或者一个string*
  3. f(*svec.begin()); //正确,传入一个string对象,f使用.*调用empty
  4. f(&svec[0]); //正确,传入一个string的指针,f使用.*调用empty
  • 实际上,我们可以认为mem_fn生成的可调用对象含有一个重载的函数调用运算符:一个接受string*,另一个接受string&

③使用bind生成一个可调用对象

  • 处于完整性的考虑,我们还可以使用bind从成员函数生成一个可调用对象:
  1. std::vector<std::string> svec;
  2. //选择范围中的每个string,并将其bind到empty的第一个隐式实参上
  3. auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
  • 和function类似的地方是,当我们使用bind时,必须将函数中用于表示执行对象的隐式形参转换成显式的。和mem_fn类似的地方是,bind生成的可调用对象的第一个实参既可以是string的指针,也可以是string的引用:
  1. std::vector<std::string> svec;
  2. auto f = bind(&string::empty._1);
  3. f(*svec.begin()); //正确,传入一个string对象,f使用.*调用empty
  4. f(&svec[0]); //正确,传入一个string的指针,f使用.*调用empty