非静态成员函数

来自cppreference.com
< cpp‎ | language

非静态成员函数是在类的成员说明中不带 staticfriend 说明符声明的函数。(这些关键词的效果见静态成员函数友元声明

class S
{
    int mf1(); // 非静态成员函数声明
    void mf2() volatile, mf3() &&; // 可以有 cv 限定符或引用限定符
        // 上面的声明与下面分开的两个声明等价:
        // void mf2() volatile;
        // void mf3() &&;
 
    int mf4() const { return data; } // 可以内联定义
    virtual void mf5() final; // 可以是虚函数,可以使用 final/override
    S() : data(12) {} // 构造函数也是成员函数
    int data;
};
int S::mf1() { return 7; } // 不内联定义就必须在命名空间定义

构造函数析构函数转换函数的声明语法是特殊的。本页描述的规则可能不适用于这些函数。细节可以参考它们对应的页面。

显式对象成员函数 是有显式对象形参的非静态成员函数。

隐式对象成员函数 是没有显式对象形参的非静态成员函数。

(C++23 起)

解释

允许任何函数声明,外加非静态成员函数专用的语法元素:纯说明符,cv 限定符,引用限定符,finaloverride 说明符 (C++11 起),以及成员初始化器列表

可以通过以下方式调用类 X 的非静态成员函数:

1)X 类型的对象使用类成员访问运算符调用
2)派生X 的类的对象调用
3)X 的成员函数体内直接调用
4) 从派生自 X 的类的成员函数体内调用直接调用

在类型不是 X 或派生自 X 的对象上调用类 X 的非静态成员函数的行为未定义。

X 的非静态成员函数的体内,任何解析为 XX 的某个基类的非类型非静态成员的标识表达式 e(例如一个标识符)都会被变换为成员访问表达式 (*this).e(除非它已经是成员访问表达式的一部分)。模板定义语境中不会发生这种变换,因此有时需要明确地对某个名字前附 this->,以使它成为待决的名字。

struct S
{
    int n;
    void f();
};
 
void S::f()
{
    n = 1; // 变换为 (*this).n = 1;
}
 
int main()
{
    S s1, s2;
    s1.f(); // 更改 s1.n
}

X 的非静态成员函数体内,任何解析到 XX 的某个基类的静态成员、枚举项或嵌套类型的无限定标识都会被变换为对应的有限定标识。

struct S
{
    static int n;
    void f();
};
 
void S::f()
{
    n = 1; // 变换为 S::n = 1;
}
 
int main()
{
    S s1, s2;
    s1.f(); // 更改 S::n
}

有 const 与 volatile 限定符的成员函数

非静态成员函数可以带 cv 限定符序列(constvolatileconstvolatile 的组合)声明,这些限定符在函数声明中的形参列表之后出现。带有不同 cv 限定符(或无限定)的函数具有不同类型,从而可以相互重载。

在有 cv 限定符的函数体内,*this 有同样的 cv 限定,例如在有 const 限定符的成员函数中只能正常地调用其他有 const 限定符的成员函数。(如果应用了 const_cast,或通过不涉及 this 的访问路径,那么仍然可以调用没有 const 限定符的成员函数。)

#include <vector>
 
struct Array
{
    std::vector<int> data;
    Array(int sz) : data(sz) {}
 
    // const 成员函数
    int operator[](int idx) const
    {                     // this 具有类型 const Array*
        return data[idx]; // 变换为 (*this).data[idx];
    }
 
    // 非 const 成员函数
    int& operator[](int idx)
    {                     // this 具有类型 Array*
        return data[idx]; // 变换为 (*this).data[idx]
    }
};
 
int main()
{
    Array a(10);
    a[1] = 1;  // OK:a[1] 的类型是 int&
    const Array ca(10);
    ca[1] = 2; // 错误:ca[1] 的类型是 int
}

有引用限定符的成员函数

非静态成员函数可以不带引用限定符,带有左值引用限定符(函数名后的 & 记号),或带有右值引用限定符(函数名后的 && 记号)声明。在重载决议中,按下列方式对待类 X 的非静态有 cv 限定符序列的成员函数:

  • 不带引用限定符:隐式对象形参具有到 cv 限定的 X 的左值引用类型,并额外允许绑定到右值隐含对象实参
  • 左值引用限定符:隐式对象形参具有到 cv 限定的 X 的左值引用类型
  • 右值引用限定符:隐式对象形参具有到 cv 限定的 X 的右值引用类型
#include <iostream>
 
struct S
{
    void f() &  { std::cout << "左值\n"; }
    void f() && { std::cout << "右值\n"; }
};
 
int main()
{
    S s;
    s.f();            // 打印“左值”
    std::move(s).f(); // 打印“右值”
    S().f();          // 打印“右值”
}

注意:与 cv 限定性不同,引用限定性不改变 this 指针的性质:即使在右值引用限定的函数中,*this 仍是左值表达式。

(C++11 起)

虚函数和纯虚函数

非静态成员函数可以声明为纯虚函数。细节见虚函数抽象类

显式对象形参

非静态成员函数的声明可以通过在第一个形参前附关键词 this 来指定该形参为显式对象形参。

struct X
{
    void foo(this X const& self, int i); // 同 void foo(int i) const &;
//  void foo(int i) const &;             // 错误:已经声明
 
    void bar(this X self, int i); // 按值传递对象:复制 *this
};

对于成员函数模板,显式对象形参的类型和值类别可以被推导,因此该语言特性也被称为“推导 this”:

struct X
{
    template<typename Self>
    void foo(this Self&&, int);
};
 
struct D : X {};
 
void ex(X& x, D& d)
{
    x.foo(1);       // Self = X&
    move(x).foo(2); // Self = X
    d.foo(3);       // Self = D&
}

这使得成员函数的带 const 限定和不带 const 限定版本只需要一次声明,编译器可根据推导结果选择合适的类外部的实现。

此外,显式对象形参会推导成派生类型,因此可以简化 CRTP

// 一个 CRTP 特性
struct add_postfix_increment
{
    template<typename Self>
    auto operator++(this Self&& self, int)
    {
        auto tmp = self; // Self 会被推导成 some_type
        ++self;
        return tmp;
    }
};
 
struct some_type : add_postfix_increment
{
    some_type& operator++() { ... }
};

在有显式对象形参的函数体内不能使用 this 指针:所有成员访问必须通过第一个形参进行,就像在静态成员函数中一样:

struct C
{
    void bar();
 
    void foo(this C c)
    {
        auto x = this; // 错误:不能使用 this
        bar();         // 错误:没有隐式的 this->
        c.bar();       // OK
    }
};

指向有显式对象形参的成员函数的指针是通常的函数指针,而不是到成员的指针:

struct Y 
{
    int f(int, int) const&;
    int g(this Y const&, int, int);
};
 
auto pf = &Y::f;
pf(y, 1, 2);              // 错误:不能调用指向成员函数的指针
(y.*pf)(1, 2);            // OK
std::invoke(pf, y, 1, 2); // OK
 
auto pg = &Y::g;
pg(y, 3, 4);              // OK
(y.*pg)(3, 4);            // 错误:pg 不是指向成员函数的指针
std::invoke(pg, y, 3, 4); // OK

有显式对象形参的成员函数不能是静态成员函数或虚函数,也不能带有 cv 或引用限定符。

(C++23 起)

特殊成员函数

一些成员函数是特殊的:在某些环境下,即使用户不定义编译器也会定义它们。它们是:

(C++11 起)
(C++11 起)

特殊成员函数以及比较运算符 (C++20 起)是仅有的能被预置的函数,即使用 = default 替代函数体进行定义(细节见其相应页面)。

示例

#include <exception>
#include <iostream>
#include <string>
#include <utility>
 
struct S
{
    int data;
 
    // 简单的转换构造函数(声明)
    S(int val);
 
    // 简单的显式构造函数(声明)
    explicit S(std::string str);
 
    // const 成员函数(定义)
    virtual int getData() const { return data; }
 
};
 
// 构造函数的定义
S::S(int val) : data(val)
{
    std::cout << "调用构造函数1,data = " << data << '\n';
}
 
// 此构造函数拥有 catch 子句
S::S(std::string str) try : data(std::stoi(str))
{
    std::cout << "调用构造函数2,data = " << data << '\n';
}
catch(const std::exception&)
{
    std::cout << "构造函数2失败,字符串'" << str << "'\n";
    throw; // 构造函数的 catch 子句应该始终重新抛出异常
}
 
struct D : S
{
    int data2;
    // 带默认实参的构造函数
    D(int v1, int v2 = 11) : S(v1), data2(v2) {}
 
    // 虚成员函数
    int getData() const override { return data * data2; }
 
    // 左值限定的赋值运算符
    D& operator=(D other) &
    {
        std::swap(other.data, data);
        std::swap(other.data2, data2);
        return *this;
    }
};
 
int main()
{
    D d1 = 1;
    S s2("2");
 
    try
    {
         S s3("不是数字");
    }
    catch(const std::exception&) {}
 
    std::cout << s2.getData() << '\n';
 
    D d2(3, 4);
    d2 = d1;   // OK:赋值给左值
//  D(5) = d1; // 错误:没有适合的 operator= 重载
}

输出:

调用构造函数1,data = 1
调用构造函数2,data = 2
构造函数2失败,字符串'不是数字'
2
调用构造函数1,data = 3

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 194 C++98 (非静态)成员函数的名字可以与所在类的名字相同 添加命名限制

参阅