指针声明

来自cppreference.com
< cpp‎ | language

声明指针或指向成员指针类型的变量。

语法

指针的声明是简单声明,它的声明符拥有下列形式:

* 属性(可选) cv限定符(可选) 声明符 (1)
嵌套名说明符 * 属性(可选) cv限定符(可选) 声明符 (2)
1) 指针声明符:声明 S* D;D 声明为指向 嵌套名说明符 S 所确定类型的指针。
2) 成员指针声明符:声明 S C::* D;D 声明为指向 C嵌套名说明符 S 所确定类型的非静态数据成员的指针。
嵌套名说明符 - 名字和作用域解析运算符 :: 的序列
属性 - (C++11 起) 属性列表
cv限定符 - 应用到被声明指针的 const/volatile 限定(而并非被指向类型,它的限定是 声明说明符序列 的一部分)
声明符 - 除引用声明符之外的任意声明符(不存在指向引用的指针)。它可以是另一指针声明符(允许指向指针的指针)

不存在指向引用的指针和指向位域的指针。 当没有详述地提及“指针”时,通常不包含指向(非静态)成员的指针。

指针

指针类型的值是下列之一:

  • 指向对象或函数的指针(此时我们说该指针指向函数或对象),或
  • 对象末尾后指针,或
  • 该类型的空指针值,或
  • 无效指针值

指向对象的指针表示该对象所占用的内存的首字节的地址。对象的末尾后指针表示,内存中该对象所占用的存储之后的首个字节的地址。

注意,两个表示同一地址的指针也可能拥有不同的值。

struct C
{
    int x, y;
} c;
 
int* px = &c.x;   // px 的值是“指向 c.x 的指针”
int* pxe= px + 1; // pxe 的值是“ c.x 的尾后指针”
int* py = &c.y;   // py 的值是“指向 c.y 的指针”
 
assert(pxe == py); // == 测试两个指针是否表示相同地址
                   // 可能或可能不引发断言
 
*pxe = 1; // 即使未引发断言,行为也未定义

通过无效指针值间接寻址,和将无效指针值传递给解分配函数的行为均未定义。无效指针值的任何其他用法的行为都由实现定义。部分实现会将复制无效指针值的行为定义为造成由系统生成的运行时错误。

对象指针

对象指针能以应用于任何对象类型(包含另一指针类型)的表达式的取址运算符的返回值初始化:

int n;
int* np = &n;          // 指向 int 的指针
int* const* npp = &np; // 指向【指向非 const int 的 const 指针】的非 const 指针
 
int a[2];
int (*ap)[2] = &a;     // 指向含有 int 的数组的指针
 
struct S { int n; };
 
S s = {1};
int* sp = &s.n;        // 指向作为 s 的成员的 int 的指针

对象指针可以作为内建间接寻址运算符(一元 operator*)的操作数,并返回指代被指向对象的左值表达式

int n;
int* p = &n;     // 指向 n 的指针
int& r = *p;     // 绑定到指代 n 的左值表达式的引用
r = 7;           // 在 n 存储 int 7
std::cout << *p; // 左值到右值隐式转换从 n 读取值

指向类对象的指针也能作为成员访问运算符 operator->operator->* 的左侧操作数。

由于存在数组到指针的隐式转换,可以以数组类型的表达式初始化指向数组首元素的指针:

int a[2];
int* p1 = a; // 指向数组 a 首元素 a[0](一个 int)的指针
 
int b[6][3][8];
int (*p2)[3][8] = b; // 指向数组 b 首元素 b[0] 的指针,
                     // 被指者为含有 3 个【含有 8 个 int 元素的数组】元素的数组

由于存在指针的派生类到基类的隐式转换,可以以派生类的地址初始化指向基类的指针:

struct Base {};
struct Derived : Base {};
 
Derived d;
Base* p = &d;

如果 Derived多态的,那么这种指针可用于进行虚函数调用

某些加法、减法自增和自减运算符对于指向数组元素的指针有定义:这种指针满足老式随机访问迭代器 (LegacyRandomAccessIterator) 要求,并使得 C++ 库算法可以用于原始数组。

某些情况下,比较运算符对指针有定义:两个表示相同地址的指针比较相等,两个空指针值比较相等,指向同一数组中的元素的指针的比较与各元素的数组下标的比较方式相同,而指向拥有相同成员访问的非静态数据成员的指针以各成员的声明顺序进行比较。

多数实现也为随机来源的指针提供严格全序,例如将它们实现为连续虚拟地址空间中的地址。未能做到的实现(例如,其中并非指针的所有位都是内存地址的一部分因而在比较时必须忽略之,或者要求附带的计算,或者指针与整数并非一对一关系),为指针提供了具有此项保证的 std::less 特化。这使得可在关联容器(如 std::setstd::map)中使用所有随机来源的指针。

指向 void 的指针

指向任意类型对象的指针都可以被隐式转换成指向 void 的指针(可有 cv 限定);它的值不会改变。逆向的转换要求 static_cast显式转型,并生成它的原指针值:

int n = 1;
int* p1 = &n;
void* pv = p1;
int* p2 = static_cast<int*>(pv);
std::cout << *p2 << '\n'; // 打印 1

如果原指针指向某多态类型对象中的基类子对象,则可用 dynamic_cast 获得指向最终派生类型的完整对象的 void*

指向 void 的指针与指向 char 的指针拥有相同的大小、表示及对齐。

void 指针被用来传递未知类型的对象,这在 C 接口中常见:std::malloc 返回 void*std::qsort 期待接受两个 const void* 参数的用户提供回调。pthread_create 期待接受并返回 void* 的用户提供的回调。所有情况下,调用方负责在使用前将指针转型到正确类型。

函数指针

函数指针能以非成员函数或静态成员函数的地址初始化。由于存在函数到指针的隐式转换,取址运算符可以忽略:

void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // 与 &f 相同

与函数或函数的引用不同,函数指针是对象,从而能存储于数组、被复制、被赋值等:

void (a[10])(int);  // 错误:函数数组
void (&a[10])(int); // 错误:引用数组
void (*a[10])(int); // OK:函数指针数组

注意:涉及函数指针的声明经常可以通过类型别名简化:

using F = void(int); // 用来简化声明的具名类型别名
F a[10];  // 错误:函数数组
F& a[10]; // 错误:引用数组
F* a[10]; // OK:函数指针数组

函数指针可用作函数调用运算符的左侧操作数,这会调用被指向的函数:

int f(int n)
{
    std::cout << n << '\n';
    return n * n;
}
 
int main()
{
    int (*p)(int) = f;
    int x = p(7);
}

解引用函数指针生成标识被指向函数的左值:

int f();
int (*p)() = f;  // 指针 p 指向 f
int (&r)() = *p; // 将标识 f 的左值绑定到引用
r();             // 通过左值引用调用函数 f
(*p)();          // 通过函数左值调用函数 f
p();             // 直接通过指针调用函数 f

如果只有一个重载匹配指针类型,那么函数指针可以从可包含函数、函数模板特化及函数模板的一个重载集进行初始化(细节见重载函数的地址):

template<typename T>
T f(T n) { return n; }
 
double f(double n) { return n; }
 
int main()
{
    int (*p)(int) = f; // 实例化并选择 f<int>
}

相等比较运算符对于函数指针有定义(如果指向同一函数,那么它们比较相等)。

成员指针

数据成员指针

指向作为类 C 的成员的非静态数据成员 m 的指针,能准确地以表达式 &C::m 初始化。在 C 的成员函数中,如 &(C::m)&m 这样的表达式不构成指向成员指针。

这种指针能用作成员指针访问运算符 operator.*operator->* 的右侧操作数:

struct C { int m; };
 
int main()
{
    int C::* p = &C::m;          // 指向类 C 的数据成员 m
    C c = {7};
    std::cout << c.*p << '\n';   // 打印 7
    C* cp = &c;
    cp->m = 10;
    std::cout << cp->*p << '\n'; // 打印 10
}

指向一个可访问且无歧义的非虚基类的数据成员的指针,可以隐式转换成指向派生类的同一数据成员的指针:

struct Base { int m; };
struct Derived : Base {};
 
int main()
{
    int Base::* bp = &Base::m;
    int Derived::* dp = bp;
    Derived d;
    d.m = 1;
    std::cout << d.*dp << ' ' << d.*bp << '\n'; // 打印 1 1
}

相反方向的转换,即从指向派生类的数据成员的指针到指向无歧义非虚基类的数据成员的指针,允许由 static_cast显式转型来进行,即使基类没有该成员(但当用该指针访问时,最终派生类中有):

struct Base {};
struct Derived : Base { int m; };
 
int main()
{
    int Derived::* dp = &Derived::m;
    int Base::* bp = static_cast<int Base::*>(dp);
 
    Derived d;
    d.m = 7;
    std::cout << d.*bp << '\n'; // OK:打印 7
 
    Base b;
    std::cout << b.*bp << '\n'; // 未定义行为
}

成员指针的被指向类型也可以是成员指针自身:成员指针可有多级,而且在每级可以有不同的 cv 限定。指针和成员指针的混合也可以多级组合:

struct A
{
    int m;
    // 指向非 const 成员的 const 指针
    int A::* const p;
};
 
int main()
{
    // 指向(A 的)数据成员的非 const 指针
    // 该成员是一个指向【(A 的)非 const 成员】的 const 指针
    int A::* const A::* p1 = &A::p;
 
    const A a = {1, &A::m};
    std::cout << a.*(a.*p1) << '\n'; // 打印 1
 
    // 指向一个【指向(A 的)非 const 成员的 const 指针】的常规非 const 指针
    int A::* const* p2 = &a.p;
    std::cout << a.**p2 << '\n'; // 打印 1
}

成员函数指针

指向作为类 C 的成员的非静态成员函数 f 的指针,能准确地以表达式 &C::f 初始化。在 C 的成员函数内,如 &(C::f)&f,这样的表达式不构成成员函数指针。

这种指针可以用作成员指针访问运算符 operator.*operator->* 的右操作数。它的结果表达式只能用作函数调用运算符的左侧操作数:

struct C
{
    void f(int n) { std::cout << n << '\n'; }
};
 
int main()
{
    void (C::* p)(int) = &C::f; // 指向类 C 的成员函数 f 的指针
    C c;
    (c.*p)(1);                  // 打印 1
    C* cp = &c;
    (cp->*p)(2);                // 打印 2
}


指向基类的成员函数的指针可以隐式转换成指向派生类的同一成员函数的指针:

struct Base
{
    void f(int n) { std::cout << n << '\n'; }
};
struct Derived : Base {};
 
int main()
{
    void (Base::* bp)(int) = &Base::f;
    void (Derived::* dp)(int) = bp;
    Derived d;
    (d.*dp)(1);
    (d.*bp)(2);
}

相反方向的转换,即从指向派生类的成员函数的指针到指向无歧义非虚基类的成员函数的指针,允许由 static_cast显式转型来进行,即使基类没有该成员函数(但在用该指针进行访问时,最终派生类有):

struct Base {};
struct Derived : Base
{
    void f(int n) { std::cout << n << '\n'; }
};
 
int main()
{
    void (Derived::* dp)(int) = &Derived::f;
    void (Base::* bp)(int) = static_cast<void (Base::*)(int)>(dp);
 
    Derived d;
    (d.*bp)(1); // OK:打印 1
 
    Base b;
    (b.*bp)(2); // 未定义行为
}

成员函数指针可用作回调或函数对象,通常在应用 std::mem_fnstd::bind 之后:

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
 
int main()
{
    std::vector<std::string> v = {"a", "ab", "abc"};
    std::vector<std::size_t> l;
    transform(v.begin(), v.end(), std::back_inserter(l),
              std::mem_fn(&std::string::size));
    for(std::size_t n : l)
        std::cout << n << ' ';
}

输出:

1 2 3

空指针

每个类型的指针都拥有一个特殊值,称为该类型的空指针值(null pointer value)。值为空的指针不指向对象或函数(解引用空指针的行为未定义),并与所有值同样为的同类型指针比较相等。

需要将指针初始化为空或赋空值给既存指针时,可以使用空指针字面量 nullptr、空指针常量 NULL 或从整数值 0隐式转换

零初始化值初始化也初始化指针为它对应的空值。

空指针可以用来指示对象不存在(例如 function::target()),或作为其他错误条件的指示器(例如 dynamic_cast)。通常,接受指针实参的函数始终需要检查值是否为空,并以不同方式处理该情况(例如,delete 表达式在传递空指针时不做任何事)。

常量性

  • 如果指针声明中 限定符* 之前出现,则它是 声明说明符序列 的一部分,并应用到被指向的对象。
  • 若指针声明中 限定符* 之后出现,则它是 声明符 的一部分,并应用到所声明的指针。
语法 含义
const T* 指向 const 对象的指针
T const* 指向 const 对象的指针
T* const 指向对象的 const 指针
const T* const 指向 const 对象的 const 指针
T const* const 指向 const 对象的 const 指针
// pc 是一个指向 const int 的非 const 指针
// cpc 是一个指向 const int 的 const 指针
// ppc 是一个指向【一个指向 const int 的非 const 指针】的非 const 指针
const int ci = 10, *pc = &ci, *const cpc = pc, **ppc;
// p 是一个指向非 const int 的非 const 指针
// cp 是一个指向非 const int 的 const 指针
int i, *p, *const cp = &i;
 
i = ci;    // OK:复制 const int 值到非 const int
*cp = ci;  // OK:能(通过 const 指针)修改非 const int
pc++;      // OK:能修改非 const 指针(指向 const int)
pc = cpc;  // OK:能修改非 const 指针(指向 const int)
pc = p;    // OK:能修改非 const 指针(指向 const int)
ppc = &pc; // OK:const int 的指针的地址是 const int 的指针的指针
 
ci = 1;    // 错误:不能修改 const int
ci++;      // 错误:不能修改 const int
*pc = 2;   // 错误:不能修改被指向的 const int
cp = &ci;  // 错误:不能修改 const 指针(指向非 const int)
cpc++;     // 错误:不能修改 const 指针(指向 const int)
p = pc;    // 错误:非 const int 的指针不能指向 const int
ppc = &p;  // 错误:const int 的指针的指针不能指向非 const int 的指针

通常,从一个多级指针到另一个的隐式转换遵循限定转换指针比较运算符中所描述的规则。

缺陷报告

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

缺陷报告 应用于 出版时的行为 正确行为
CWG 73 C++98 指向对象的指针与指向数组尾后的指针无法比较相等(即使地址相同) 对非空非函数指针比较它们表示的地址
CWG 903 C++98 任何求值为 0 的整型常量表达式都是空指针常量 仅限值为 0 的整数字面量
CWG 1438 C++98 以任何方式使用无效指针值的行为都未定义 除了间接寻址和传递给解分配函数
以外的行为都由实现定义

参阅