%% # 纲要 > 主干纲要、Hint/线索/路标 - 指针 - 指针类型的意义 # Q&A #### 已明确 ![[02-开发笔记/01-cpp/类型相关/cpp-指针相关#^8awn4s]] #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% > [!tip] 理解 "**指针**" 声明的含义,最好是从 "**右到左**" 的顺序阅读,**将 `*` 翻译为 `pointer to`**。 # 指针 C++中创建指针时,只会**分配用来存储地址的内存**,不会分配用来**存储指针所指向的数据的内存**(即**野指针**) ```cpp int * fellow; *fellow = 233333; // 段错误, 指针没有被初始化, 可能指向任何位置, 是完全未知的; ``` > [!NOTE] C++中的单目运算符 `&` 与 `*` > > - `&` :"取址运算符",获取数据对象的地址,将**返回一个指向数据对象的类型的指针**, > - `*`:"解引用/间址访问运算符",应用于一个指针时,表示**访问该指针所指地址处的内存空间** <br> ## 指针类型的意义 > ❓<font color="#c0504d">指针只存储地址,为什么对指针需要声明所指的类型?</font> ^8awn4s 「指针类型」表示的是 "**指针所指对象的类型**",指示了**编译器==如何解释==某个特定地址起始的 "==内存内容==" 及其==大小==**[^1]: - "**地址**" 只指示了 **对象所占内存空间的==起始地址==(所占连续字节的最低地址)**; - "**指针类型**" 则指明了 **==对象的字节大小==** 以及 **==对内存内容的解读方式==**。 > [!NOTE] 正因如此,通用指针 `void*` 仅能存储一个地址,而无法通过该指针访问内存地址上的对象 > [!info] 对指针进行 "强制类型转换",只改变指针类型,即对指针的解读方式 > [!NOTE] > > **指向 `int` 的指针和指向 `double` 的指针,所存储的地址都占 8 字节**,唯有明确指针类型后,才能够明确: > > 1. **使用==间址运算符==时 `*p` 如何解读**?(从起始地址起,读 4 字节还是 8 字节?) > 2. **对==指针进行算术运算==时,需要==基于指针所指地址==加减多少==偏移量==**? > > [!NOTE] C++中能独立分配且用原生指针指向的最小对象是 `char` 类型对象[^7] > > 机器最小寻址单位是字节,C++标准规定 `char` 类型只占 1 字节。 <br><br><br> ## 指针运算 #### 解引用运算符 对指针使用**解引用运算符 (间址运算符) `*`** 表示 **访问指针所指的内存空间**。 #### 方括号运算符 方括号运算符用于**访问指针指向的数组元素**,等价于**对指针进行=="算术运算" 后再解引用==**。 表达式 `ptr[idx]` 等价于 `*(ptr + idx)`: 编译器会**基于指针当前指向的内存地址**,计算得到**第 `idx` 个数据对象的地址**,然后**访问该内存位置的值**。 #### 指针的算术运算 指针类型仅支持下列**基本运算**,会以 "**指针==所指向的数据类型的大小==**" 作为偏移量进行自动计算结果地址。 - **指针与整型间的==加减==运算**、**指针的自增自减** - **两指针间的加减运算** - **两指针间的比较** 如要对**指针所存的地址**进行复杂运算,例如**乘除、位运算**等,可先**用 `uintptr_t` 存储指针地址**,再进行操作。 > [!NOTE] `uintptr_t` 为无符号整型,用于存储指针地址,位于头文件 `<stdint.h>` 中 > > `uintptr_t` 主要用于需要**将指针所存地址当做 "整数" 操作** 而进行复杂运算的场景,例如计算**乘除、取模、位运算**。 > > 示例:求给定地址按 2 的整数幂上取整对齐后的地址 > > ```cpp > void* align_pointer(void* ptr, size_t alignment) { // 要求alignment是2的整数次幂 > uintptr_t raw = reinterpret_cast<uintptr_t>(ptr); // 将指针转为uintptr_t类型. > uintptr_t aligned = (raw + alignment - 1) & ~(alignment - 1); // 上取整对齐2的整数次幂alignment > return reinterpret_cast<void*>(aligned); > } > ``` > ###### (1)指针加减整数值 & 自增自减 对**指针变量 $+1$ (或 $-1$)** ,则增加(或减少)的**地址量**等于**所指向的类型占用的字节数**; > [!NOTE] 对于指向 T 类型的指针 `T* p = addr`,则 `p + i` 的值等于 `addr + sizeof(T) * i`。 ```cpp int arr[] = {1, 2, 3}; int* p = arr; // p指向 arr[0] p++; // p指向 arr[1], 以sizeof(int)作为自增的偏移量. ``` ###### 两指针相减 - **两个指针相减**的结果为**它们之间的==元素数量**==,即 "**两地址之差除以该数据类型的大小**" - (参与运算的**两指针必须指向==同一数组中的元素==**) - 两指针相减的结果类型为 **`ptrdiff_t`,带符号类型,定义于 `<cstddef>` 头文件中**。 ```cpp #include <cstddef> int* p1 = &arr[3]; int* p2 = &arr[0]; ptrdiff_t n = p1 - p2; // n == 3 ``` ###### 指针比较 指针之间可以进行比较操作,如等于(`==`)、不等于(`!=`)、小于(`<`)、大于(`>`)、小于等于(`<=`)、大于等于(`>=`)。这些操作通常用于**确定两个指针的相对位置**或**检查指针是否指向同一个内存位置**。 ```cpp if (p1 > p2) { // p1 指向 p2 后面的内存地址 } ``` <br><br><br> ## C/C++中指针运算对应的汇编指令 > 参见[^2] > [!example] C/C++中指针运算对应的汇编指令示例 > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-指针相关.assets/IMG-cpp-指针相关-1A1D68AC9F552E06E74A733A19B612CD.png|790]] > > ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-指针相关.assets/IMG-cpp-指针相关-D9EA2689798633E53755D145EBA30A61.png|778]] > > ^0aldns <br><br><br> # "指向常量的指针"与"常量指针" > 参见 [^6] #### 指向常量的指针 pointer to const 指向常量的指针: **不能通过该指针来修改对象内容**(底层 `const`) > [!NOTE] > > "**指向常量的指针**",以及"**指向常量的引用**",都是仅**对"指针"或"引用"本身的操作做了限制**,即**不能通过该指针或该引用来修改所指对象的值**,但**并未限定对象本身必须是常量,其所指的对象可能是变量,对象本身的值是可修改的**,只是不能通过该指针或引用来修改。 ```cpp // 两种声明等效,都是指向int常量的指针. const int *p; // 指向整型常量的指针 int const *p; // 指向整型常量的指针 ``` #### 常量指针 const pointer 常量指针:**指针所指的"位置"不能改变**,即 **"指针"本身是个常量**(顶层 `const`) - 常量指针所指位置不可变, 但**指针==所指向的位置中的内容可变==**; - 常量指针**必须在声明的同时对其初始化**,不允许先声明一个指针常量随后再对其赋值,这与声明一般的常量是一样的。 ```cpp int * const p; // 指向整型的常量指针 const int * const p; // 指向整型常量的常量指针 int const * const p; // 指向整型常量的常量指针 ``` <br><br><br> # "数组指针"与"指针数组" ```cpp // 运算符`[]`的优先级高于*, 因此`*pa[3]`表明这是一个"包含三个指针"的指针数组. // 当使用括号时`(*pa)[3]`, 这表明这是一个指向 "包含三个元素的数组" 的指针 T *pd[3] // an array of 3 pointers 指针数组. 数组里包含3个指向T类型的指针. T (*pd)[3] // a pointer to an array of 3 elements 指向"包含3个T类型元素的数组类型"的指针 ``` #### 数组指针—指向数组的指针 指向数组的指针——Pointer to a array ```cpp // 指向数组的指针 int *p = new int[10]; // 指向int型变量或数组的指针; 指针类型为 int(*) int a[3] = {1, 2, 3} int (*p)[3] = &a; // 指向"包含3个int型元素的数组"的指针; 指针类型为 int(*)[3]; ``` #### 指针数组 指针数组——Array of pointers ```cpp // 指针数组 int *p[10]; // 包含10个int*指针变量的指针数组 int b = 10; p[0] = &b; // 包含5个"指向长度为10的int型数组的指针"的指针数组; 5个指针, 每个指针类型都是int(*)[10]. int (*p[5])[10]; int a[10]; p[0] = &a; // 函数指针数组 // 包含3个函数指针的数组, 每个函数指针的类型为 const double *(*)(coust double *, int); const double * (*pa[3])(const double *, int); ``` <br> # 指向指针的指针 ```cpp int val = 1024; int *pi = &val; // 指向一个int型的数 int **ppi = &pi; // 指向一个int型的指针 int ***pppi = &ppi; // 指向"指向一个int型的指针"的指针 ``` <br><br><br> # 字符指针 #### 将字符串字面量初始化或赋值给字符指针 > [!info] "**字符串字面量**" 是一个**左值**,本质上是 "**指向==常量字符数组==的指针**" > > 因此,只能且必须用一个 `const char*` 指针来指向 "字符串字符量"。 > > ```cpp > const char* ptr = "Hello"; // ptr指向的内容不可被修改 > ``` > "**字符串字面量**" 是指向 "**常量字符数组的指针**",该 **常量字符数组** 位于内存空间中 "**==初始化数据段中的 `.rodata` 只读数据段==**",程序中**所有==相同内容的 "字符串字面量"== 都指向该区域的==同一块内存地址==**。 ```cpp const char* str1 = "Hello World"; const char* str2 = "Hello World"; assert(str1 == str2); // str1与str2指向同一内存地址! ``` <br><br> # void* 通用指针 `void*` 表示 "**指向==未知类型==对象的指针**",其仅能用于 "**==存储地址==**",但**不能对解引用**(所指类型未知),支持以下操作[^7]: - 除 "**函数指针**" 与 "**类成员指针**" 外,指向任意类型的指针均可被赋值给 `void*` 指针(C++支持任意指针类型到 `void*` 的隐式转换) - **比较两个 `void*` 指针是否相等**; - **两个 `void*` 指针之间进行赋值**; - **将 `void*` 指针==强制转换==为任意类型指针**(C++不支持 `void*` 到其他指针类型的隐式转换,必须是 "**强制类型转换**") 不支持以下操作: - **不能对其解引用**; - **不能对 `void*` 指针进行算术运算**; ![[02-开发笔记/01-cpp/类型相关/cpp-类型转换#^fmg7gb]] ![[02-开发笔记/01-cpp/类型相关/cpp-类型转换#^jj8rpd]] <br><br> # 函数指针 函数指针通过 **函数类型** 进行声明,指向某种 **==特定类型==的函数**(可指向该类型的任意函数,而非某个特定函数)。 函数指针的声明:`返回类型 (*指针变量名)(参数类型列表);` 示例: ```cpp int foo(double, char); // 函数类型是`int(double, char)`. int (*ptr)(double, char) = &foo; // 声明函数指针 int (*ptr)(double, char) = foo; // 与上语句等价 ``` <br> ## 函数名用作函数指针 将**函数名**作为一个值使用时,该**函数名将自动被转为指针**(指向函数地址)。 > [!caution] **成员函数**的函数名不存在自动转换,需要通过 `&` **显式取址**,例如 `&MyClass::mem_func;` > [!NOTE] 当函数存在==重载版本==时,**函数名无法被自动转换**,可通过 `static_cast<>()` **显式指明转换的==函数指针类型==**。 > > 如下例所示,在用于 `std::function` 或 `std::bind` 时,都需要为函数名指定显式转换。参见 [^3] > > ```cpp > void func(int arg) {} > void func(int arg1, int arg2) {} > > int main(int argc, char* argv[]) { > std::function<void(int)> fc1 = static_cast<void(*)(int)>(func); > std::function<void(int,int)> fc2 = static_cast<void(*)(int, int)>(func); > auto fc1_b = std::bind(static_cast<void(*)(int)>(func), 5); > auto fc2_b = std::bind(static_cast<void(*)(int, int)>(func), 6, placeholders::_1); > return 0; > } > ``` > > 对于 "**==类的成员函数==**" 也是一样,当存在多个重载版本时,需要通过 `static_cast<>()` 显式指明转换的 **==函数指针类型==**。 > 示例如下,通过 `static_cast<void (MyClass::*)(param_list)>` 显式指明函数版本。 > > ```cpp > struct MyClass { > void func(int) {} > void func(double) {} > }; > > MyClass obj; > auto func_int = std::bind(static_cast<void (MyClass::*)(int)>(&MyClass::func), &obj, std::placeholders::_1); > auto func_double = std::bind(static_cast<void (MyClass::*)(double)>(&MyClass::func), &obj, std::placeholders::_1); > ``` > > ^03j3v1 <br><br> ## 函数指针数组 ```cpp // 函数原型 const double *f1(const double [], int); // 对应的函数指针 const double * (*p1)(const double *, int) = f1; // 对应的"函数指针"数组 // 注: 运算符[]的优先级高于*, 因此*pa[3]表示这是一个包含三个指针的指针数组. // 而声明语句的其它部分, 则指出了该数组包含的元素类型是什么样的. const double * (*pa[3])(const double *, int) = {f1, f2, f3}; // 通过"函数指针"数组中的函数指针调用函数: const double *px = pa[0](av, 3); // 两调用语句等价 const double *py = (*pa[1])(av, 4); // 一个指向"包含三个函数指针元素的数组"的指针 // 注: // - 最内层的(*pb)表明这是一个指针 // - *(*pb)[3]表明这是一个指向"包含三个指针的指针数组"的指针 // - const double *(counst double *, int) 是数组中每个函数指针对应的函数类型; const double *(*(*pb)[3])(const double *, int)) = &pa; // pb指向一个函数指针数组. const double *pz = (*pb)[0](av, 3); // 两调用语句等价 const double *pm = (*(*pb)[0])(av, 4); // 外层解引用是对函数指针解引用 ``` <br> ## 函数指针别名 使用 `using`(**推荐**): ```cpp using FuncType = int(int, int); // "函数类型"的别名 using FuncPtrType = int(*)(int, int); // "函数指针类型"的别名 ``` 使用 `typedef`: ```cpp title:func_pointer.cpp int MyFunc(int, int) { return 0; }; typedef int FuncType(int, int); // "函数类型"的别名 typedef int (*FuncPtrType)(int, int); // "函数指针类型"的别名 typedef decltype(MyFunc)* FuncPtrType2; // 使用'decltype' 得到函数类型. FuncType* pFunc = MyFunc; FuncPtrType pFuncPtr = MyFunc; FuncPtrType2 pFuncPtr2 = MyFunc; ``` <br> ## 使用函数指针 可直接**通过函数指针调用函数**,而**无需先解引用**。 可以为函数指针**赋值为 `nullptr` 或者 `0`**,表示**该指针没有指向任何一个函数**。 函数指针使用示例: ```cpp // 函数原型 bool lengthCompare(const string&, const string&); bool (*pf)(const string&, const string&); // 声明函数指针, 未初始化 pf = lengthCompare; // 函数名作为值使用, 直接被转为指针 pf = &lengthCompare; // 等价的赋值语句, 取址运算符是可选的 bool b1 = pf("hello", "goodbye"); // 直接通过函数指针调用函数 bool b2 = (*pf)("hello", "goodbye"); // 等价语句, 先对函数指针解引用再调用. // 可为函数指针赋0值或nullptr, 表示不指向任何函数 pf = 0; pf = nullptr; ``` <br> ### 函数指针作为形参 "函数类型" 不能作为**函数形参**或者**函数的返回类型**,但可以定义 **==函数指针==** 作为**形参或返回类型**,从而**传递或返回函数** ```cpp //下面两种声明是等价的 void Foo(int i, void(*pFoo)(int)); void Foo(int i, void pFoo(int)); // 形式上是函数类型, 但实际上会自动被转为函数指针 ``` ```cpp // 声明形参为函数指针 // 写法一: (形式上是函数类型, 但实际上会自动被转为函数指针) void useBigger(const string &s1, const string &s2, bool pf(const string&, const string&)); // 写法二: 显式指明是函数指针 void useBigger(const string &s1, const string &s2, bool (*pf)(const string&, const string&)); // 可直接将函数名当作实参使用, 会自动转为函数指针 useBigger(s1, s2, lengthCompare); ``` <br> ### 函数指针作为返回类型 定义一个返回类型为"函数指针"的函数: 格式为:`函数指针对应的函数返回类型 (*函数名(函数参数)) (函数指针对应的函数参数)`: - `int (*FuncName(int))(int*, int);` - `auto FuncName(int) -> int (*)(int*, int);` ← 或使用尾置返回类型 ```cpp // 一个指向`int(int*, int)`函数类型的函数指针: int (*ptr)(int*, int); // 当该类型的函数指针作为函数返回类型, 声明一个函数原型时: // 该函数接受一个int参数, 返回一个`int(*)(int*, int)`的函数指针 int (*func1(int))(int*, int); // 通过尾置返回类型的方式声明函数原型: auto func1(int) -> int (*)(int*, int); // 为函数指针或函数类型声明别名, 简化代码 using F = int(int*, int); // 通过using声明一个函数类型的类型别名 using PF = int(*)(int*, int); // 通过using声明一个函数指针类型的类型别名 F *func1(int); // 声明函数func1, 返回类型为指向函数类型F的指针 PF func2(int); // 声明函数func2, 返回类型为PF. ``` <br> ### 函数指针使用建议 直接使用函数指针类型显得冗长烦琐,可以使用类型别名 `typedef` / `using` 和 `decltype` 简化代码。 ```cpp using Foo = void(int, int) //Foo是函数 using pFoo = void(*)(int, int) //pFoo是函数指针类型 ``` ```cpp // ------------------1.为函数类型声明别名 // 为一个函数类型声明别名 typedef bool Func1(const string&, const string&); // 为lengthCompare函数的函数类型声明别名; 等价于上述语句 typedef decltype(lengthCompare) Func2; // ------------------1.为函数指针声明别名 // 为一个函数指针声明别名 typedef bool (*Func1P)(const string&, const string&); // 为一个指向lengthCompare函数的函数类型的函数指针声明别名 typedef decltype(lengthCompare) *Func2P; // Func2P是函数指针 // 通过using声明一个"函数类型"的类型别名 using F = int(int*, int); F func1(int); // 通过using声明一个函数指针类型的类型别名 using FuncType = decltype(foo); Functype* funcptr = &foo; // 函数指针 using PF = int(*)(int*, int); PF funcptr2 = &f; // 函数指针 ``` <br><br><br> # 类成员指针与成员函数指针 通过**类成员指针**访问数据成员,或是通过**成员函数指针**调用成员函数,<br>都需要使用 **`.*` 或 ` ->.*` 运算符** 将该指针**绑定到特定的对象上**。 - 通过 **类成员指针** 访问数据成员: - `obj.*dataPtr` - `objPtr->*dataPtr` - 通过 **类成员函数指针** 调用成员函数: - `(obj.*funcPtr)(args)` - `(objPtr->*funcPtr)(args)` > [!info] 两种成员指针访问运算符:`.*` 与 `->*` => 基于一个实例对象来解引用 "成员指针 or 成员函数指针"。 > [!example] 使用示例 > > 这两个指针需要由 "**类的非静态成员的地址**" 进行初始化或赋值. > > ```cpp > class MyClass { > public: > int mem_data = 5; > void* mem_func (int, char) const { return nullptr; } > }; > > int main() { > MyClass obj, *ptr = &obj; > int MyClass::*memptr_data = &MyClass::mem_data; > void* (MyClass::*memptr_func)(int, char) const = &MyClass::mem_func; > > // 使用成员指针, 成员函数指针. > obj.*memptr_data = 10; // 访问数据成员 > (obj.*memptr_func)(11, 'G'); // 函数调用 > ptr->*memptr_data = 32; > (ptr->*memptr_func)(99, 'g'); > } > ``` > > <br> ## (1)类成员指针 成员指针(Pointer to members)是指向 "**==类的非静态数据成员==**" 的指针,指向**类中成员**,而非类的实例对象[^4]。 **直到==使用成员指针==时,才需要提供==成员所属的实例对象==**。 > [!NOTE] 对 "类的非静态数据成员" 取址 `&`,得到 "**指向该类成员的成员指针**"。 > [!NOTE] 类的静态成员不属于任何对象,无需特殊类型的指针,**类的静态成员由普通指针指向**。 <br> ### 类成员指针的类型 声明类成员指针:`指向类型 类名::*指针名 = &类名::数据成员名` ,即`T C::*pmem = &C::member;` > [!NOTE] 成员指针中既包含 "类类型" 也包含指针所指的 "成员类型" **类成员指针的类型**仅仅与它指向的 **==数据成员本身==** 是否为**常量**有关,与其**指向的对象**是否为常量无关。 - 指向 "**常量成员**" 的成员指针, 可以用以指向一个**非常量成员**, 但是将不能通过该指针来修改该非常量成员. - 可以为指向 "**常量成员**" 的成员指针提供 "**常量或非常量对象**",但提供非常量对象时,不能通过该指针修改这一成员的值,而只能"读"。 - 可以为指向 "**非常量成员**" 的成员指针提供 "**常量或非常量对象**"。但提供常量对象时,不能通过该指针修改这一成员的值。 类成员指针声明示例: ```cpp class MyClass { public: const int constMember; int nonConstMember; }; // 声明一个指向类的"常量成员"的成员指针 // (使用指针时, 可提供常量对象或非常量对象, 无论哪种情况, 对该成员都"只读") const int MyClass::*const_ptr = &MyClass::constMember; // 指针类型是`const int MyClass::*` // 声明一个指向类的"非常量成员"的成员指针 // (使用指针时, 可提供常量对象或非常量对象, 但提供常量对象时, 不能通过指针修改该成员数据) int MyClass::*non_const_ptr = &MyClass::nonConstMember; // 声明一个指向"常量成员"的成员指针, 该指针可用以指向一个非常量成员, // 其作用是不能通过该指针来修改该非常量成员. const int MyClass::*const_ptr2 = &MyClass::nonConstMember; ``` > [!NOTE] 类的数据成员通常声明为 `private`,该情况下不能直接获得数据成员的指针。 > > 类可以提供一个公有的静态成员函数,**返回一个成员指针**。 > > ```cpp > class MyClass2 { > private: > int nonConstMember; > public: > explicit MyClass2(int x) : nonConstMember(x) {} > // 提供一个类的静态成员函数, 为私有成员返回一个const成员指针. > static const int MyClass2::* getMemberPointer() { // 返回成员指针, 要包含类作用域 > return &MyClass2::nonConstMember; // 将类的成员的地址作为一个成员指针. > } > }; > > void exam2() { > // 通过类的static成员函数获取一个指向其私有数据成员的成员指针 > const int MyClass2::*ptr = MyClass2::getMemberPointer(); > MyClass2 obj(99); > auto res = obj.*ptr; // ptr是指针, 通过解引用运算符访问了obj对象的私有数据成员; > cout << res << endl; > } > ``` > <br><br> ## (2)成员函数指针 成员函数指针指向 **==类的成员函数==** [^5]。 成员函数指针通过 **==类作用域下的函数类型==** 进行声明,可指向 **类中同一函数类型的成员函数**。 > [!caution] > > - **类的成员函数名**与指针之间不存在自动转换,因此 **必须对类的成员函数显式取地址** 再赋给指针。 > - 若成员函数是 **const 成员** 或 **引用成员**,则声明成员函数指针时必须包含 **const 限定符** 或 **引用限定符**。 #### 使用方式 - **声明**成员函数指针:`返回类型 (类::*ptr)(参数列表) [const限定符或引用限定符] = &类::成员函数 ` - 通过成员函数指针**调用函数**:`(obj.*ptr)(args)` ```cpp class MyClass { public: void memberFunc(int x) { std::cout << "Value: " << x << std::endl; } int constFunc(int x) const { std::cout << "Value: " << x + 5 << std::endl; } } int main() { // 声明成员函数指针 void (MyClass::*ptr)(int) = &MyClass::memberFunc; // 对成员函数必须显式取址 MyClass obj; (obj.*ptr)(5); // 通过成员函数指针来调用obj对象的成员函数 MyClass *pObj = &obj; (pObj->*ptr)(10); // 通过成员函数指针来调用指针pObj所指对象的成员函数 // 声明指向const成员函数的成员函数指针 int (MyClass::*ptr2)(int) const = &MyClass::constFunc; const MyClass obj2; (obj2.*ptr2)(5); const MyClass *pObj2 = &obj2; (pObj2->*ptr2)(10); } ``` > [!example] 一个优雅的成员函数指针使用示例 > > ```cpp > class Screen { > public: > using Action = Screen& (Screen::*)(void); // 成员函数指针类型 > enum Directions { HOME = 0, FORWARD, BACK, UP, DOWN }; > Screen& move(Directions); // 其会根据传入参数调用下列成员函数. > > Screen& home() { cout << "move home" << endl; return *this; } > Screen& forward() { cout << "move forward" << endl; return *this; } > Screen& back() { cout << "move back" << endl; return *this; } > Screen& up() { cout << "move up" << endl; return *this; } > Screen& down() { cout << "move down" << endl; return *this; } > > private: > static Action Menu[]; // 函数表: 指向成员函数指针的指针. > }; > > Screen::Action Screen::Menu[] = { > &Screen::home, > &Screen::forward, > &Screen::back, > &Screen::up, > &Screen::down > }; > > Screen& Screen::move(Screen::Directions d) { > return (this->*Menu[d])(); // `Menu[d]`获取成员函数指针, 通过`this->*`调用. > } > > int main() { > Screen myscreen; > myscreen.move(Screen::HOME); > myscreen.move(Screen::FORWARD); > myscreen.move(Screen::BACK); > myscreen.move(Screen::UP); > myscreen.move(Screen::DOWN); > } > ``` > <br> #### 成员函数指针作为形参 示例: 形参为成员函数指针,可以指向**类中同一函数类型的任意成员函数**。 ```cpp #include <iostream> class MyClass { public: void functionA() const { std::cout << "Function A called!" << std::endl; } void functionB() const { std::cout << "Function B called!" << std::endl; } }; // 定义一个参数为 "MyClass对象引用" 以及 "指向MyClass的const成员函数的指针" 的函数 void invoke(const MyClass& obj, void (MyClass::*func)() const) { (obj.*func)(); // 使用成员函数指针调用成员函数 } int main() { MyClass myObj; invoke(myObj, &MyClass::functionA); // 输出:Function A called! invoke(myObj, &MyClass::functionB); // 输出:Function B called! } ``` #### 为成员函数指针生成可调用对象 为 "**成员函数指针**" 生成 "**可调用对象**" 的四种方式: - 使用 `function<>` 对象; - 使用 `bind` - 使用 `lambda` - 使用 `mem_fn` 说明示例: ```cpp int main() { vector<string> vec { "hello world", "ZZZZZZ", "", "gdgsdf" }; // 方式一: 使用function<>对象 function<bool(const string&)> fcn = &string::empty; auto it1 = find_if(vec.begin(), vec.end(), fcn); // 方式二: 使用bind绑定 auto fcn_b = bind(&string::empty, ::placeholders::_1); auto it2 = find_if(vec.begin(), vec.end(), fcn_b); // 方式三: 使用lambda auto fcn_lam = [](const string& str) -> bool { return (str.*(&string::empty))(); }; auto it3 = find_if(vec.begin(), vec.end(), fcn_lam); // 方式四: 使用mem_fn auto it4 = find_if(vec.begin(), vec.end(), mem_fn(&string::empty)); } ``` ##### 方式一:使用function为成员函数指针直接生成一个可调用对象 > [!caution] `function<>` 包装成员函数指针时,其**首项必须额外添加一个参数**,用于**隐式的接收==执行成员函数的对象=="**。 需要对 function 中的函数类型做修改, 在参数列表中**额外添加一个==对实例对象的指针或引用==**,作为**首项参数**。 - 可以将**成员函数的地址**直接赋给function对象 - 可以将**成员函数指针**直接赋给function对象 在这一方式下, 在**调用function对象时可以传入不同的实例对象**。 示例一: ```cpp class MyClass { public: void memberFunc(int x) { std::cout << x << std::endl; } void constFunc(int x, int y) const { // const成员函数 std::cout << x << ", " << y << std::endl; } }; void exam1() { // 1. 将成员函数的地址直接赋给function对象 std::function<void(MyClass&, int)> fcn = &MyClass::memberFunc; std::function<void(const MyClass&, int, int)> fcn_const = &MyClass::constFunc; MyClass obj; const MyClass const_obj; fcn(obj, 1); fcn_const(obj, 2, 3); fcn_const(const_obj, 4, 5); // 2. 将成员函数指针直接赋给function对象, void (MyClass::*ptr)(int) = &MyClass::memberFunc; void (MyClass::*constPtr)(int, int) const = &MyClass::constFunc; std::function<void(MyClass &, int)> fcn2 = ptr; std::function<void(const MyClass &, int, int)> fcn_const2 = constPtr; fcn2(obj, 11); fcn_const2(obj, 22, 33); fcn_const2(const_obj, 44, 55); } ``` 示例二: ```cpp vector<string> vec; function<bool(const string&)> fcn = &string::empty; // 使用function对象封装"函数指针" auto it = find_if(vec.bgein(), vec.end(), fcn); // 找出第一个为空的字符串. vector<string*> pvec; function<bool(const string*)> fcn = &string::empty; auto it = find_if(vec.begin(), vec.end(), fcn); ``` ##### 方式二:使用bind为成员函数指针生成一个可调用对象 有两种方式: 1. 使用bind绑定成员函数指针时**不提供实例对象**,而**在调用bind生成的可调用对象时才传入实例对象** 参数列表中需要额外增加一项第一参数,**调用时需要传入一个对实例对象的指针或引用。** (类似于方式一中function的用法;) 2. 使用bind绑定成员函数指针时**提供具体实例对象**,而后可以直接调用。 ```cpp void exam2() { MyClass obj; const MyClass const_obj; void (MyClass::*ptr)(int) = &MyClass::memberFunc; void (MyClass::*const_ptr)(int, int) const = &MyClass::constFunc; // 2.1 使用bind绑定时 "不提供实例对象" // std::bind(, args)的参数列表args中需要额外添加一项第一参数, // 调用时需要传入一个对实例对象的指针或引用. auto bind_func11 = std::bind(&MyClass::memberFunc, std::placeholders::_1, std::placeholders::_2); auto bind_const_func11 = std::bind(&MyClass::constFunc, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); bind_func11(&obj, 10); bind_const_func11(&obj, 20, 30); bind_const_func11(&const_obj, 40, 50); auto bind_func12 = std::bind(ptr, std::placeholders::_1, std::placeholders::_2); auto bind_const_func12 = std::bind(const_ptr, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); bind_func12(&obj, 10); bind_const_func12(&obj, 20, 30); bind_const_func12(&const_obj, 40, 50); // 2.2 使用bind绑定时 "提供具体的实例对象" // std::bind()的第一项参数接受成员函数指针, // 第二项参数接受一个实例对象的引用或指针, 剩余参数为成员函数的参数列表. auto bind_func21 = std::bind(&MyClass::memberFunc, &obj, std::placeholders::_1); auto bind_const_func21 = std::bind(&MyClass::constFunc, &const_obj, std::placeholders::_1, std::placeholders::_2); auto bind_const_func211 = std::bind(&MyClass::constFunc, &obj, std::placeholders::_1, std::placeholders::_2); bind_func21(1); bind_const_func21(11, 22); bind_const_func211(31, 32); auto bind_func22 = std::bind(ptr, &obj, std::placeholders::_1); auto bind_const_func22 = std::bind(const_ptr, &const_obj, std::placeholders::_1, std::placeholders::_2); auto bind_const_func222 = std::bind(const_ptr, &obj, std::placeholders::_1, std::placeholders::_2); bind_func22(3); bind_const_func22(44, 55); bind_const_func222(66, 77); } ``` ##### 方式三:使用lambda为成员函数指针生成一个可调用对象 ```cpp // 方式三: 通过lambda生成可调用对象 void exam3() { MyClass obj; const MyClass const_obj; void (MyClass::*ptr)(int) = &MyClass::memberFunc; void (MyClass::*constPtr)(int, int) const = &MyClass::constFunc; // lambda捕获外部的实例对象和成员函数指针, 并在函数体里通过实例对象和成员函数指针完成调用. auto lambda_func = [&obj, &ptr](int x) { (obj.*ptr)(x); }; auto lambda_const_func = [&const_obj, &constPtr](int x, int y) { (const_obj.*constPtr)(x, y); }; lambda_func(1); lambda_const_func(11, 22); } ``` ##### 方式四:使用mem_fn生成一个可调用对象 ```cpp // 方式四: 通过mem_fn生成可调用对象 // mem_fn为成员函数指针生成的函数, 含有一对重载的函数调用运算符, 一个接受实例对象的引用, 一个接受实例对象指针 void exam4() { MyClass obj; const MyClass const_obj; MyClass *obj_ptr = &obj; const MyClass *const_obj_ptr = &const_obj; auto wrapped_mem_fun = std::mem_fn(&MyClass::memberFunc); auto wrapped_const_mem_fun = std::mem_fn(&MyClass::constFunc); wrapped_mem_fun(obj, 10); wrapped_mem_fun(obj_ptr, 10); wrapped_const_mem_fun(const_obj, 20, 30); wrapped_const_mem_fun(const_obj_ptr, 20, 30); wrapped_const_mem_fun(obj, 40, 50); wrapped_const_mem_fun(obj_ptr, 40, 50); } ``` <br><br><br> # 指针的状态 ## 空指针 > 空指针:**未指向任何有效地址的指针**。 **将 `0` 或 `NULL` 或 `nullptr` 赋给一个指针时**,该指针即为 "**==空指针==**"。 ```cpp // 定义空指针 int *p1 = nullptr; // C++11关键字, 等价于下一行语句; int *p2 = 0; // `NULL`是值为0的常量宏, 在<cstddef> C++标准库头文件中定义 int *p3 = NULL; ``` > [!info] 空指针中存储的 "值" 通常为 0 > > 当一个指针是 "**空指针**" 时,**指针==内部存储的 "地址值"==** 由编译器的实现决定。 > 在绝大多数平台上实现为,**空指针内部存储的值为`0`**。 > 在 OS 层面上,**==地址 0== 通常保留作为 "无效地址" 而禁止访问**,故**访问空指针**将触发 "**段错误**"(非法内存访问)。 > > ```cpp > int main() { > int *p = nullptr; > cout << p << endl; // 输出0 > } > ``` > > [!caution] C++中 `ostream` 对**字符指针** `char*` 重载了 `<<` 运算符,将**输出指针所指的以空字符(`'\0'`)结尾的 C 风格字符串**。 > > 若**一个字符指针为空指针**,则 `cout <<` 该字符指针将会触发 "**段错误**"(非法内存访问)! > > ```cpp > int main() { > int *ip = nullptr; > char *cp = nullptr; > printf("%p\n", ip); // 输出0000000000000000 > printf("%p\n", cp); // 输出0000000000000000 > cout << ip << endl; // 输出0 > cout << cp << endl; // 将触发段错误! > } > ``` > <br><br> #### 关于 NULL `NULL` 是一个**宏**,在 C& C++11 之前用于**表示空指针**,**在 C 和 C++中该宏的定义不相同**。 | | `NULL` 的宏定义 | 说明 | | --- | -------------------- | --------------------------- | | C | `#define ((void*)0)` | **==值为 0 的 `void*` 类型指针==** | | C++ | `#define NULL 0` | **==值为 0 的宏常量==** | > [!caution] 在 C++中 **`NULL` 不能定义为 `(void(*)0)`**,因为 C++**不支持从 `void*` 到任何其它指针类型**的隐式转换,如果这样定义**则无法将 `NULL` 赋值给指针**。 > [!example] 在 `MinGW` 实现中对`NULL` 宏的定义 > > ```cpp > #if defined(__need_NULL) > #undef NULL > #ifdef __cplusplus > # if !defined(__MINGW32__) && !defined(_MSC_VER) > # define NULL __null > # else > # define NULL 0 > # endif > #else > # define NULL ((void*)0) > #endif > ``` > > #### 关于 `nullptr` > [!info] `nullptr` 是一个特殊的**字面量值**,其类型是 `std::nullptr_t`,可被隐式转换为指向任何类型的指针 ^u7d9yw `nullptr` 可以被自动转换为**任何指针类型**,但**不会被转换为任何整数类型**。 使用 `nullptr` 来取代 `0` 或者 `NULL`,能够帮助在“null pointer” 被解释为一个整数值时避免误解。例如: ```cpp void f(int); void f(void*); f(0); // call f(int) f(NULL); // call f(int) if NULL is 0, ambiguous otherwise f(nullptr); // call f(void*) ``` <br><br> ## 野指针 > A wild pointer is a pointer that has **not been correctly initialized** and therefore p**oints to some random piece of memory**. It is a serious error to have wild pointers. **==野指针==**(wild pointer):**尚未初始化**为已知地址的指针。 野指针不是空指针,野指针**指向的内存地址是未知的** (随机的,不正确的,无明确意义的)。 成因:**指针未初始化**: 指针变量刚被创建时不会自动成为空指针,它的缺省值是**随机**的; ```cpp int *ptr; // 未初始化,指向任意内存地址 // 使用ptr是非常危险的,因为不知道它指向哪里 ``` <br><br><br> ## 悬空指针 > A dangling pointer is a pointer that **used to point to a valid address but now no longer does**. This is usually due to that **memory location being freed up and no longer available**. There is nothing wrong with having a dangling pointer unless you try to access the memory location pointed at by that pointer. It is always best practice not to have or leave dangling pointers. ==**悬空指针**==(dangling pointer):当一个指针被分配给内存,而**这块内存又被释放或删除后**,如果没有将该指针置为 null(或其他有效地址),该指针就成为了悬空指针。 悬空指针的成因:**指针释放后未置空**——指针在 `free` 或 `delete` 后未赋值 NULL,此时**指向无效内存**。 ```cpp int *ptr = new int(7); delete ptr; // 内存被释放 // 此时,ptr是一个悬空指针,因为它不再指向已分配的内存 ``` ### 其它情况 - **指针越界**:指针指向的范围超出了有效范围 - **指针超出变量的作用域**: - 调用函数时**返回指向局部变量 (栈内存)的指针或引用**,函数结束时,栈区上局部变量的内存空间被释放; - 指针指向代码块中的局部变量,代码块执行结束后,变成野指针; <br><br><br> # 参考资料 # Footnotes [^1]: 《深度探索 C++对象模型》P28 [^2]: 《深入理解计算机系统》 [^3]: 《C++ Primer》P513 [^4]: 《C++ Primer》P739-P741 [^5]: 《C++ Primer》P741-P746 [^6]: [如何理解常量指针与指针常量?](https://www.zhihu.com/question/19829354) [^7]: 《C++程序设计语言》P149