C++类成员指针

文章详细介绍了C++中的成员指针,包括数据成员指针和成员函数指针的声明、初始化和使用。成员指针并不直接指向对象,而是指向类的特定成员。使用.*或->*运算符可以访问成员。成员函数指针需要与对象结合使用,不能直接调用,可以通过function或bind等工具转化为可调用对象。文章还提到了成员指针在函数表和可调用对象中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

成员指针是指可以指向类的非静态成员的指针。

一般情况下,指针可以指向一个对象,但是成员指针指的是类的成员,而非类的对象。

类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。

成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象:直到使用成员指针时,才提供成员所属的对象。

数据成员指针

以下面的类为例

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;
}

和其他指针一样,在声明成员指针时我们也可以使用*来表示当前声明的名字是一个指针。与普通指针不同的是,成员指针还必须包含成员所属的类。因此,我们必须在*之前添加classname:: ,以表示当前定义的指针可以指向classname的成员。例如:

const string Screen::*pdata;
//pdata可以指向一个常量(非常量)Screen对象的string成员。

上述语句将pdata声明成"一个指向Screen类的const string"成员的指针。常量对象的数据成员本身也是常量,因此将我们的指针声明成指向const string成员的指针意味着,pdata可以指向任何Screen对象的一个成员,而不管该Screen对象是否是常量。作为交换条件,我们只能使用pdata读取它所指的成员,而不能向它写入内容。

当我们初始化一个成员指针时(或向它赋值)时,需指定它所指的成员,例如,我们可以令pdata指向某个非特定Screen对象的contents成员。

pdata = &Screen::contents;

其中,我们将取地址运算符作用于Screen类的成员而非内存中的一个该类对象。

当然,在C++11新标准中声明成员指针最简单的方法是使用autodecltype

auto pdata = &Screen::contents;

使用数据成员指针

必须清楚的一点是,当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。

与成员访问运算符.->类似,也有两种成员指针访问运算符: .*->* ,这两个运算符使得我们可以解引用指针并获得该对象的成员:

Screen myScreen, *pScreen = &myScreen;
// .*解引用pdata以获得myScreen对象的contents成员
auto s = myScreen.*pdata;
// ->*解引用pdata以获得pScreen所指对象的contents成员
s = pScreen->*pdata;

返回数据成员指针的函数

常规的访问控制规则对成员指针同样有效。例如,Screencontents成员是私有的,因此之前对于pdata的使用必须位于Screen类的成员或友元内部,否则程序将发生错误

因为数据成员一般情况下是私有的,所以我们通常不能直接获得数据成员的指针。如果一个像Screen这样的类希望我们可以访问它的contents成员,最好定义一个函数,令其返回值是指向该成员的指针:

class Screen{
public:
    // data是一个静态成员,返回一个成员指针
    static const std::string Screen::*data()
        { return &Screen::contents; }
   //其他成员与之前一致
};

我们为Screen类添加了一个静态成员,令其返回指向contents成员的指针。显然该函数的返回类型与最初的pdata指针类型一致。从右向左阅读函数的返回类型,可知data返回的是一个指向Screen类的const string成员的指针。函数体对contents成员使用了取地址运算符,因此函数将返回指向Screencontents成员的指针。

当我们调用data函数时,将得到一个成员指针:

// data()返回一个指向Screen类的contents成员的指针
const string Screen::*pdata = Screen::data();

一如往常,pdata指向Screen类的成员而非实际数据。如想要使用pdata,必须把它绑定到Screen类型的对象上:

//获得myScreen对象的contents成员
auto s = myScreen.*pdata;

成员函数指针

也可以指定指向类的成员函数的指针。
与指向数据成员的指针类似,对于我们来说,要想创建一个指向成员函数的指针,最简单的方法是使用auto来推断类型:

// pmf是一个指针,它可以指向Screen的某个常量函数
// 前提是该函数不接受任何实参,并且返回一个char
auto pmf = &Screen::get_cursor;

和指向数据成员的指针一样,我们使用classname::*的形式声明一个指向成员函数的指针。类似于任何其他函数指针,指向成员函数的指针也需要指定目标函数的返回类型和形参列表。如果成员函数是const成员或者引用成员,则我们必须将const限定符或引用限定符包含进来。

和普通的函数指针类似,如果成员存在重载的问题,则我们必须显式地声明函数类型以明确指出我们想要使用的是哪个函数。例如,我们可以声明一个指针,令其指向含有两个形参的get

char (Screen::*pfm2)(Screen::pos, Screen::pos) const;
pm2 = &Screen::get;

由于优先级的考虑,上述声明中Screen::*两端的括号必不可少。如果没有这对括号的话,编译器将认为该声明是一个(无效的)函数声明:

//错误:非成员函数p不能使用const限定符
char Screen::*p(Screen::pos, Screen::pos) const;

这个声明试图定义一个名为p的普通函数,并且返回Screen类的一个char成员。因为它声明的是一个普通函数,所以不能使用const限定符。

和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则

// pmf指向一个Screen成员,该成员不接受任何实参且返回类型是char
pmf = &Screen::get;  //必须显式地使用取地址运算符
pmf = Screen::get;   //错误:在成员函数和指针之间不存在自动转换规则

使用成员函数指针

和使用指向数据成员的指针一样,我们使用.*或者->*运算符作用于指向成员函数的指针,以调用类的成员函数:

Screen myScreen, *pScreen = &myScreen;
// 通过pScreen所指的对象调用pmf所指的函数
char c1 = (pScreen->*pmf)();
// 通过myScreen对象将实参0, 0传给含有两个实参的get函数
char c2 = (myScreen.*pmf2)(0, 0);

之所以(myScreen->*pmf)()(pScreen.*pmf2)(0, 0)的括号必不可少,原因是调用运算符的优先级要高于指针指向成员运算符的优先级。

假设去掉括号的话,

myScreen.*pmf()

其含义将等同于下面的式子:

myScreen.*(pmf())

这行代码的意思是调用一个名为pmf的函数,然后使用·该函数的返回值作为指针指向成员运算符(.*)的运算对象。然而pmf并不是一个函数,因此代码将发生错误。

因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(C::*p)(param)(obj.*p)(args)

使用成员指针的类型别名

使用类型别名或typedef可以让成员指针更加容易理解。
例如,下面的类型别名将Action定义为两参数get函数的同义词:

// Action 是一种可以指向Screen成员函数的指针,它接受两个pos参数,返回一个char

using Action = 
char (Screen::*)(Screen::pos, Screen::pos) const;

Action是某类型的另外一个名字,该类型是"指向Screen类的常量成员函数的指针,其中这个成员函数接受两个pos形参,返回一个char"。通过使用Action,我们可以简化指向get的指针定义。

Action get = &Screen::get;  //get指向Screen的get成员。

和其他函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型。其中,指向成员的指针形参也可以拥有默认实参。

Screen & action(Screen&, Action = &Screen::get);

action是包含两个形参的函数,其中一个形参是Screen对象的引用,另一个形参是指向Screen成员函数的指针,成员函数必须接受两个pos形参并返回一个char。当我们调用action时,只需将Screen的一个符合要求的函数的指针或地址传入即可:

Screen myScreen;
// 等价的调用
action(myScreen);     //使用默认实参
action(myScreen, get);     //使用我们之前定义的变量get
action(myScreen, &Screen::get);     //显式地传入地址

通过使用类型别名,可以令含有成员指针的代码更易读写。

成员指针函数表

对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个。假定Screen类含有几个成员函数,每个成员函数负责将光标向指定的方向移动:

class Screen{
public:
    Screen& home();  //光标移动函数
    Screen& forward();
    Screen& back();
    Screen& up();
    Screen& down();  
};

这几个新函数有一个共同点:它们都不接受任何参数,并且返回值是发生光标移动的Screen的引用。

我们希望定义一个move函数,使其可以调用上面任意一个函数并执行其对应的操作。为了支持这个新的函数,我们将在Screen中添加一个静态成员,该成员是指向光标移动函数的指针的数组:

class Screen{
pulbic:
    // 其他接口和实现成员与之前一致
   // Action是一个指针,可以用任意一个光标移动函数对其赋值
   using Action = Screen& (Screeb::*)();
   //指定具体要移动的方向
   enum Directions { HOME, FORWARD, BACK, UP, DOWN }
   Screen& move(Directions);
private:
    static Action Menu[]; //函数表
};

数组Menu依次保存每个光标移动函数的指针,这些函数将按照Directions中枚举成员对应的偏移量存储。move函数接受一个枚举成员并调用相应的函数:

Screen & Screen::move(Directions cm)
{
    //运行this对象中索引值为cm的元素
    return (this->*Menu[cm])();    // Menu[cm]指向一个成员函数
}

move中的函数调用的原理是:首先获取索引值为cmMenu元素,该元素是指向Screen成员函数的指针。我们根据this所指的对象调用该元素所指的成员函数。

当我们调用move函数时,给它传入一个表示光标移动方向的枚举成员:

Screen myScreen;
myScreen.move(Screen::HOME);  //调用myScreen.home
myScreen.move(Screen::DOWN);  //调用myScreen.down

剩下的工作就是定义并初始化函数表本身了:

Screen::Action Screen::Menu[] = {
    &Screen::home,
    &Screen::forward,
    &Screen::back,
    &Screen::up,
    &Screen::down
};

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

如我们所知,要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*运算符或->*运算符将该指针绑定到特定的对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。

因为成员指针不是可调用对象,所以我们不能直接将一个指向成员函数的指针传递给算法。

举个例子,如果我们想在一个stringvector中找到第一个空string,显然不能使用下面的语句:

auto fp = &string::empty;  // fp指向string的empty函数
//  错误,必须使用.*或->*调用成员指针
find_if(svec.begin(), svec.end(), fp);

find_if算法需要一个可调用对象,但我们提供给它的是一个指向成员函数的指针fp
因此在find_if的内部将执行如下形式的代码,从而导致无法通过编译:

//  检查对当前元素的断言是否为真
if(fp(*it))  //  错误:要想通过成员指针调用函数,必须使用->*运算符

显然该语句试图调用的是传入的对象,而非函数。

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

从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function

function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);

我们告诉function一个事实,即empty是一个接受string参数并返回bool值的函数。通常情况下,执行成员函数的对象将被传给隐式的this形参。当我们想要使用function为成员函数生成一个可调用对象时,必须首先“翻译”该代码,使得隐式的形参变成显式的。

当一个function对象包含有一个指向成员函数的指针时,function类知道它必须使用正确的指向成员的指针运算符来执行函数调用。也就是说,我们可以认为在finc_if当中含有类似于如下形式的代码:

// 假设it是find)if内部的迭代器,则*it是给定范围内的一个对象
if(fcn(*it))  //假设假设fcn是find_if内部的一个可调用对象的名字

其中,fucntion将使用正确的指向成员的指针运算符。从本质上来看,function类将函数调用转换成了如下形式:

//  假设it是find_if内部的迭代器,则*it是给定范围内的一个对象
if(     ( (*it).*p)  ()   )

当我们定义一个function对象时,必须指定该对象所能表示的函数类型,即可调用对象的形式。如果可调用对象是一个成员函数,则第一个形参必须表示该成员是在哪个(一般是隐式的)对象上执行的。同时,我们提供给function的形式中还必须指明对象是否是以指针或引用的形式传入的。

以定义fcn为例,我们想在string对象的序列上调用find_if,因此我们要求function生成一个接受string对象的可调用对象。又因为我们的vector保存的是string的指针,所以必须指定function接受指针:

vector<string *> pvec;
function<bool (const string*)> fp = &string::empty;
//  fp接受一个指向string的指针,然后使用->*调用empty
find_if(pvec.begin(), pvec.end(), fp);

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

  • 待办

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

  • 待办
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

barbyQAQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值