跳至主要內容

05. 指针类型

LincZero大约 9 分钟

05. 指针类型

指针

简概

指针

  • 定义:指针是一个存储值的地址的变量
  • 计算机存储数据时跟踪3种基本属性
    • 信息存储在何处
    • 存储的值是多少
    • 存储信息什么类型
  • 连续定义时
    • 有的系统先定义的在高位(Windows x86)
    • 有的系统先定义的在低位(Widnows x64)
    • 有的系统不会把不同类型的变量放在相邻的内存单元中
  • 与常规变量区别
    • 使用常规变量时:值是指定的量,而地址是派生量
    • 使用地址变量时:地址是指定的量,而值是派生量
  • 指针与数组
    • 基本等价,这是C和C++的优点之

使用

字面量方式

  • 初始化:例int * pi_e = &i_e中,i_e = *pi_e= 值&i_e = pi_e = 地址 补充:定义时*两侧的空格可有可无,一般都有 补充:这里定义时和函数原型中的*&[],不要理解成地址运算符、解除引用运算符这些,而应理解为类型标识符
  • 指定地址初始化int * pt = (int *) 0xB8000000
  • 定义空指针的三种方式
    • 0
    • NULL(需要NULL头文件)
    • nullptr(C++11新增)

符号操作

  • 取地址:用法&变量&运算符被称为地址运算符
  • 地址取值:用法*地址*运算符被称为间接值或解除引用运算符

指针 x const

两种标注方法

指针 x 异常

C++标准提供了一种在失败时返回空指针的new

深度剖析指针【专题】

内容很庞大的一章

C语言的指针玩得比较花,C++其实还是应该多研究模板、C++11+这些东西

各种指针

参考

  • https://blog.csdn.net/a22025340/article/details/124013311
  • https://blog.csdn.net/afei__/article/details/81985937

基本数据类型指针

int * pi_e = &i_e;
pi_e = &i_e = 地址;	// True
*pi_e = i_e =  值;  	 // True

容器指针

多维容器指针

嵌套指针

函数指针

建议阅读:【知乎】万字长文系统梳理一下C++函数指针open in new window

核心

  • 函数指针只在乎声明传入参数和返回参数信息,即相同参数数量类型和返回类型的函数都能用这个指针来表示
  • 非静态类函数指针除外,还需要多声明一个所属类

函数指针

函数指针的作用

  • 充当回调函数(以函数指针为参数的函数)
  • 其他

基本用法


double pam(int);			// 函数原型
double (*pf)(int) = pam;    // 声明函数指针
pf()						// 使用函数指针

// 复杂点的例子如下
void printdata(int (*p)(int,int),int a,int b){
    cout<<p(a,b)<<endl;
}
void (*pp) (int(*)(int,int),int,int) = printdata;
使用typedef简化
typedef int (*ptr)(int, int);
ptr p1 = get_sum;
使用auto简化
.

函数参数指针

  • 写法(数组作参和指针作参)

    • int sum_arr(int arr[], int n);		// 数组作参
      int sum_arr(int *arr, int n);		// 指针作参
      int sum_arr(char * str, char ch);	// 字符串作参
      int sum(int ar2[][4], int size);	// 多维数组作参
      int sum(int (*ar2)[4], int size);	// 多维指针作参
      char * buildstr(char c, int n);		// 字符串返回值
      
  • 函数原型中(可省略函数名),这些写法是等价的:

    • const double * f1(const double ar[], int n);
      const double * f2(const double [], int n);
      const double * f3(const double *, int n);
      

类指针

(需结合 “面向对象_继承_虚” 笔记一起使用)

类指针

类成员字段指针

类成员方法指针

静态的成员方法函数指针语法

(同C语言差不多) 声明时和普通函数指针区别不大。 赋值有所不同,要传的是一个类的静态成员函数的地址,要加上类名限定

void (*ptrStaticFun)() = &ClassName::staticFun;					// 声明
ptrStaticFun();													// 调用1
(*ptrStaticFun)();												// 调用2
非静态成员方法函数指针语法

(麻烦一点) 注意调用类中非静态成员函数的时候,使用的是 类名::函数名,而不是 实例名::函数名

void (ClassName::*ptrNonStaticFun)() = &ClassName::nonStaticFun;// 声明
(classObject.*ptrNonStaticFun)();								// 调用1。注意要用括号括起前面,不然优先级不对
(pClassObject->*ptrNonStaticFun)();								// 调用2。注意要用括号括起前面,不然优先级不对
// (*(classObject.*ptrNonStaticFun))();			// 没有这种用法
// (&(classObject.*ptrNonStaticFun))();			// 没有这种用法
// 没有像静态方法那样的变体,左边括号的极不是函数名也不是函数地址
区分两者

两者的本质区别是什么

记住一句最核心的一句话:「静态函数没有this指针。」 类内使用非静态成员变量或函数前面其实是有一个隐藏的 this->

非静态成员函数

  • 与静态函数不同,成员函数在被调用时,必须要提供this指针 不是仅仅需要函数地址就可以调用的

    因为在它被调用之前,自己也不知道哪个对象的此函数被调用。所以通过&拿到的不是实际的内存地址 只有调用的时候,C++才会结合this指针通过固定的偏移量找到函数的真实地址调用

  • 为了支持这种调用方式,这里C++给专门提供了特殊的几个操作符:::* .* ->*

    • 声明 void (Test::*fptr)();,类成员函数指针的声明,就必须加上类名限定,这就声明了一个函数指针变量fptr,他只能指向Test类的成员函数

    • 赋值 fptr = &Test::function

    • 调用 类的成员函数是无法直接调用的,必须要使用对象或者对象指针调用(这样函数才能通过对象获取到this指针)。

      • (t.*fptr)();,t是Test类的一个实例,通过对象调用。
      • (pt->*fptr)();,pt是一个指向Test类对象的指针,通过指针调用。
  • C++成员函数的调用需要至少3个要素:

    1. this指针;
    2. 函数参数(也许为空);
    3. 函数地址
应用举例
#include <stdio.h>
#include <iostream>
  
using namespace std;
  
class MyClass {
public:
    static int FunA(int a, int b) {
        cout << "call FunA" << endl;
        return a + b;
    }
  
    void FunB() {
        cout << "call FunB" << endl;
    }
  
    void FunC() {
        cout << "call FunC" << endl;
    }
  
    int pFun1(int (*p)(int, int), int a, int b) {
        return (*p)(a, b);
    }
  
    void pFun2(void (MyClass::*nonstatic)()) {
        (this->*nonstatic)();
    }
};
  
int main() {
    MyClass* obj = new MyClass;
    // 静态函数指针的使用
    int (*pFunA)(int, int) = &MyClass::FunA;
    cout << pFunA(1, 2) << endl;
     
    // 成员函数指针的使用
    void (MyClass::*pFunB)() = &MyClass::FunB;
    (obj->*pFunB)();
     
    // 通过 pFun1 只能调用静态方法
    obj->pFun1(&MyClass::FunA, 1, 2);
     
    // 通过 pFun2 就是调用成员方法
    obj->pFun2(&MyClass::FunB);
    obj->pFun2(&MyClass::FunC);
 
    delete obj;
    return 0;
}

虚函数的函数指针

虚函数其实就是一种特殊的成员函数,定义和声明同非虚成员函数

另外,指向虚函数的函数指针在涉及到多继承和指针强转的问题时,使用不当会踩到大坑:

  1. 不要使用static_cast将继承类的成员函数指针赋值给基类成员函数指针,如果一定要使用,首先确定没有问题。(这条可能会限制代码的可扩展性。)
  2. 如果一定要使用static_cast, 注意不要使用多继承。
  3. 如果一定要使用多继承的话,不要把一个基类的成员函数指针赋值给另一个基类的函数指针。
  4. 单继承要么全部不使用虚函数,要么全部使用虚函数。不要使用非虚基类,却让子类包含虚函数。

类成员方法指针_地址问题

(这一节搜出来的不是我想的那个意思,我想搜的其实是 如何打印函数指针的地址)

参考:

非静态类成员方法指针是特殊的,需要配合实例去使用,而且也不能通过一般方法去取地址

我整理了4种C++中取成员函数地址的方法

  • pointer_cast
  • union_cast
  • cdecl_cast
  • 汇编(该方法不能在VS6上编译通过)
pointer_cast - 通过静态转换
template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
    return *static_cast<dst_type*>(static_cast<void*>(&src));
}

union_cast - 通过联合体的共享储存机制

这种方法是最常规, 也是最好理解的一种方法了, 巧妙地利用了联合体的优点. 当然, 模板的使用恰到好处. 同时,传入参数时注意数据类型大小一致.

template<typename dst_type,typename src_type>
dst_type union_cast(src_type src)
{
    union{
        src_type s;
        dst_type d;
    }u;
    u.s = src;
    return u.d;
}
cdecl_cast - 通过C语言的可变参数不检测参数类型
__declspec(naked) void* __cdecl cdecl_cast(...)
{
    __asm{
        mov eax,dword ptr[esp+4]
        ret
    }
}
asm_cast - 通过汇编的offset语句取成员函数偏移得到地址

这个方法也比较巧妙, 不过貌似在VC6上编译不过. VS2012没问题.

#define asm_cast(var,addr)  \
{                           \
    __asm{                  \
        mov var,offset addr \
    }                       \
}    

Q:能否搞出指向构造函数和析构函数的函数指针?

C++标准明确规定:The address of a constructor or destructor shall not be taken.

写出来了编译也会报错:error: taking address of constructor "Test::Test"

用汇编看透成员函数本质

详见:https://blog.csdn.net/tangchuxian080/article/details/80711339

万能指针:空类型指针(仅C语言)

原理

万能指针能操作所有类型指针,但使用前要进行强制类型转换

void print(){
    cout<<"万能指针调用函数指针"<<endl;
}

void *p = print;
((void(*)())p)();		// 两种指针调用函数的方法
*((void(*)())p)();		// 两种指针调用函数的方法

// 剖析:
// 1)void print()的函数指针是 void(*pr)();
// 2)去掉变量名,所以函数指针的类型是 void(*)();
// 3)(void(*)())p == print,将万能指针强制类型转换成void(*)()
// 4)((void(*)())p)(); 将万能指针强制类型转换成void(*)(),再调用函数
// 4)*((void(*)())p)(); 取星号运算,也是调用函数

缺陷

函数指针不能直接用void*存,你只能取函数地址整数转成void*的地址,在使用的时候再把void*转成函数指针类型,这样才可以跳用,

关键一点:void*不能调用,只能传递

C++兼容性问题 / 无法将函数指针转换的问题

无法将函数指针转换的问题 将指针转换void *为不可行的方法,因为成员指针的大小通常大于sizeof(void *)void *在任何情况下强制转换它都会丢弃指针表示的一部分,从而不再保证唯一性.

群友说: 这是c类型的,如果你要在cpp里用,cpp是强类型的,不支持隐式转换。 例如像上面的转化函数指针为万能指针会报错:无效的转换从' void(*)() '到' void* '

但是像下面这样只是用来存基本类型指针则没有问题

int a = 5;
void* p = &a;
cout << *(int*)p << endl;

C++替代方案 —— 模板指针

为了避免一些问题,C++不用万能指针的

C++更好的选择是使用模板来代替万能指针,T*完事

Const修饰指针

指针的传递

智能指针

引用类型

使用场景:

  • 改参
  • 函数返回多个信息

原理剖析

  • Q:指针是地址,为什么声明类型?
  • A:以便获取长度
  • Q:函数指针的原理是什么,为什么声明变量和返回值?
  • A:不知道