C++数组指针、函数指针、成员函数指针

操作符名称

  • & 取地址符(Address-Of operator)
  • * 间接寻址运算符(Indirection operator)
  • .-> 成员访问运算符(Member-Access operators),用于取对象的成员。
  • .*->* 指向成员的指针运算符(Pointer-To-Member operators), 用于成员函数指针和成员变量指针的取对象。
  • () 函数调用运算符(Function-Call operator)
  • :: 范围解析运算符(Scope-Resolution operator)

如何定义一个指针变量

假设类型T, 变量名称name, 指针的定义如下:

T* name;

标识变量名字name, 它是T类型的指针。例如

int n = 0;
int* p_n = &n;

p_n是int指针类型, 指向某个int型的对象。

指针变量的修饰

指针实际上也是一种变量类型, 只是它保存的内容有些特别, 是指定类型的地址值,通过间接寻址运算符(indirection operator)*, 可以访问到指针指向地址上的指定类型。

指针也可以用constvolatile修饰。 const intint const均表示一个变量类型是int, 且该变量不能修改。以下两种写法都可以:

const int a = 1;
int const b = 2;

既然指针是也一种变量类型,同样支持被const修饰, 表示指针的值/指针的指向不允许修改, 指针所指向的那个变量是否允许修改, 那是另外修饰。写法如下:

int a = 1;
int b = 2;
int* const cp_a = &a; // 指针的修饰词,放在*号后面。
*cp_a = 10; // 指针指向的值可以修改
cp_a = &b; // 指针不能被修改,报错!

总结带修饰的指针的格式:
只要记住修饰词总是放在被修饰的内容后面

cv表示const / volatile修饰词。指针定义形式如下:

T [cv for T] * [cv for pointer] name

注意对T的修饰放在T的前面也是合法的写法。

const int const c = 2;

在mscv编译器下也不会报错。

完整的格式:
[cv for T] T [cv for T] * [cv for pointer] name

Syntaxmeaning
const T*

pointer to constant object

T const*pointer to constant object
T* constconstant pointer to object
const T* constconstant pointer to constant object
T const* constconstant pointer to constant object

上面格式中T还可以是一种指针, 指针的指针仍然是按照修饰词总是修饰前面的标识(T或者*)来确定修饰的意图。

int a = 1;
int b = 2;

int* p_a = &a;
*p_a = 10; // 合法
p_a = &b; //合法

const int* cp_a = &a; // const修饰int类型, 并非修饰指针
*cp_a = 11; //报错! const int类型不能修改
cp_a = &b; // 合法, 指针没有const修饰,指针可以修改。

int* const pc_a = &a; // const修饰指针。类型没有const修饰
*pc_a = 12; // 合法, 因为类型没有const修饰,可以修改。
pc_a = &b; //报错! 指针被const修饰, 不能修改指针。

int const* const cpc_a = &a; // int类型被它后面的const修饰, 指针符号*后面也有const修饰
*cpc_a = 13; // 报错! 类型被const修饰,不能修改。
cpc_a = &b; // 报错! 指针被const修饰,不能修改。

 更复杂的指针的指针

int a = 1;
int b = 2;
int* p1 = &a;
int* p2 = &b;
const int* ct_p1 = &a; // ct for const type
const int* ct_p2 = &b; // ct for const type

// int * * pp1; 指向(int*)类型的指针
int** pp1 = &p1;  
pp1 = &p2; // 合法, 
pp1 = &ct_p1; // 报错! 类型不匹配。 (int*)不能指向(const int*)

// (const int) * * pp1; 指向((const int) *)类型的指针
const int** ct_pp1 = &ct_p1;  
ct_pp1 = &ct_p2; // 合法
ct_pp1 = &p1; // 合法!(const int*) 可以指向(int*)类型。

// (const int) (*const)
const int * const ct_cp1 = &a; // 指针也不能修改
const int * const ct_cp2 = &b; // 指针也不能修改
ct_cp1 = &b; // 报错!指针有const修饰

// (const int) (* const) *  指向((const int) (*const))的指针
const int* const * ct_cp_p1 = &ct_p1;  
ct_cp_p1 = &ct_cp2; // 合法, 指针的指针并没有const修饰, 指向的指针有const修饰
*ct_cp_p1 = &a; // 报错!等价于操作ct_cp2,  指向的指针是带const修饰的不能修改

// (const int) (* const) (*const)  
// 指向((const int) (*const))的指针,且该指针被const修饰
const int* const * const ct_cp_cp1 = &ct_cp1; 
ct_cp_cp1 = &ct_cp2; // 报错! 指针的指针被const修饰, 不能修改指针指向。

一行声明多个变量

类型 + 名称定义一个变量。
变量的前面可以加*号修饰, 表示指针, 一个星号代表一层间接。**表示指针的指针。

int a, *b, *c, d, **e;
a = 0;
d = 1;
b = &a;
c = &d;
e = &b; // e为int**类型 指针的指针
e = &c; // e为int**类型 指针的指针

也可以用括号包围变量和*号。

int (a), (*b), (*c), (d), (**e); // 合法定义。

括号可以省略,某些情况, 个人感觉加上括号更清晰一些。例如

int (a), (const *b), (*const c) = &a, (const d), (const* const* const e) = &c;

写成

int a, const *b, *const c = &a, const d, const* const* const e = &c;

更重要的是, 后面我们表达数组指针,以及函数指针时,括号是不可缺少的, 带括号的表达更加统一。

数组指针

数组基本表达

int a[10];  // 定义了类型是int, 元素个数是10的一个数组。

由于c++要支持一行定义一个类型的多个变量。 所以数组的[]时放在名称后面的。虽然我觉得

int[10] a;

这样的写法更符合类型 名称的思维, 但是如果类型都这么写的话, 没法兼容以下的写法:

int a = 0, *b = nullptr, c[20], **d = nullptr;

c++标准规定如此,但我们可以通过每一行只定义一个变量的写法, 类型会更加清晰。

int a = 0;
int* b = nullptr; // 指针int*
int c[20];
int** d = nullptr; // 指针的指针int**

数组的名称是什么类型

数组元素类型的指针,可以直接指向数组。 并且数组跟指针一样,可以通过下标去访问元素。

int a[10];
int* p = a; // 指向a数组的第一个元素
a[1] = 1;
p[1] = 1; // 效果与a[1] = 1一样。

数组可以当作T* const来使用, 但是又与T* const有些不同。sizeof()的结果不一样。

int a[10];
int b[10];
int* const p_a = a;
a[0] = 1; // 合法。 数组的元素可以修改。
p_a[0] = 1; // 效果与a[0] = 1一样。

a = b; // 报错! 数组本身的指向不能修改。

// 所以数组a可以当作int* const来使用
int *const& ref1 = a; //正确。
int *& ref2 = a; // 报错!

// 但是又跟int* const有些区别。
assert(sizeof(p_a) == 4); // 32bit程序。
assert(sizeof(a) == 4*10); // 32bit程序

数组跟元素指针的作用很相似,都可以通过下标去访问元素, 但调用sizeof()函数的结果不一样。元素指针的sizeof()返回值是4(32-bit应用)或者8(64-bit应用), 数组的sizeof()返回值是数组实际占用的空间。数组可以当作指向第一个元素地址的T* const来用其实就是我们常说的数组到指针的隐式转换。当数组作为函数参数传递后,会自动退化成T* const, 在被调用的函数内部调用sizeof()的返回值跟T* const指针大小一样。 数组传递作为函数参数后, 在被调用函数的内部与T* const是没有任何区别,只有在数组定义的可见范围内sizeof()才有获取数组占用空间大小的效果。

以下3个函数翻译成汇编以后,汇编代码是一样的。

void Func1(int* p_ary)
{
    assert(sizeof(p_ary) == 4); // 32-bit
    p_ary[1] = 1;
}

void Func2(int ary[])
{
    assert(sizeof(ary) == 4); // 32-bit
    ary[1] = 1;
}

void Func3(int ary[10])
{
    assert(sizeof(ary) == 4); // 32-bit
    ary[1] = 1;
}

int main(int argc, char** argv)
{
    int a[10];
    int b[20];
    Func1(a);
    Func2(a);
    Func3(a);
    Func3(b); // 退化成int* const了, 即使数组长度不匹配也不会报错。
    return 0;
}

多维数组

一个3行,4列的数组, 结构如下:

int a[3][4];
column 0column 1column 2column 3
row 0a[0][0]a[0][1]a[0][2]a[0][3]
row 1a[1][0]a[1][1]a[1][2]a[1][3]
row 2a[2][0]a[2][1]a[2][2]a[2][3]

数组初始化

int a[3][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};

 实际上多维数组和1维数组在开启速度优化后,翻译成汇编代码是一样的。

void Print(int* p_ary);

void Test1()
{
    int a[10];
    a[3] = 3;
    a[7] = 7;
    Print(&a[0]);
}

void Test2()
{
    int a[2][5];
    a[0][3] = 3;
    a[1][2] = 7;
    Print(&a[0][0]);
}

很自然地,多维数组也支持用1维数组的方式去初始化。

int a[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

既然多维数组与1维数组没什么区别, 为什么还需要多维数组?

假设有一幅RGB图像720*576个像素,每个像素有RGB三个通道,每个通道的值是8bit大小。给出图像的首地址p_rgb_image, 我们要取第40行,第50个像素的R,G,B值。代码如下:

unsigned char* p_rgb_image;
unsigned char r = p_rgb_image[40*720*3 + 50 + 0];
unsigned char g = p_rgb_image[40*720*3 + 50 + 1];
unsigned char b = p_rgb_image[40*720*3 + 50 + 2];

类似这样的场景, 采用多维数组的写法, 有点类似以索引为参数,可读性更高。相当于程序员和编译器打了一个配合。

enum
{
 Red = 0,
 Green = 1,
 Blue = 2
};
unsigned char rgb_image[576][720][3];
int row = 40;
int col = 50;
unsigned char r = rgb_image[row][col][Red];
unsigned char g = rgb_image[row][col][Green];
unsigned char b = rgb_image[row][col][Blue];

数组指针以及与指针数组的区别

  • 数组指针,是一个指针, 指向的对象是数组。 数组指针的赋值,要求数组的长度匹配,否则会报错。当指向1维数组时, 需用用*取得数组对象的引用,再用下标来访问数组元素。
  • 指针数组,是一个数组, 数组保存的元素的类型是指针。

数组指针的定义

  1. 数组指针定义先定义一个数组。
    int a[10];
  2. 然后对数组里的名称用括号括起来后再在变量名称前面加个*
    int (*a)[10];
    后面你会发现函数指针定义类似。
    // 各类定义对比
    int a, *b, **c, d[10], e[10][20], *f[10], (*g)[10], *(*h)[10];

    int *f[10]; // 指针数组, f是包含10个元素的数组, 数组里每一个元素的类型都是int*
    int *(f2[10]); // 指针数组。另外一种定义方式。
    int(*f3[10]); // 指针数组。另外一种定义方式。
    int(f4)[10]; // int数组
    int(*g)[10]; // 数组指针, g是一个指针, 这个指针可以指向类型是int,元素个数是10的数组
    int* (*h)[10]; // 数组指针, h是一个指针, 这个指针可以指向类型是int*,元素个数是10的指针数组

    int d[10];
    g = &d;

    int* e[10];
    h = &e;

数组指针的使用

数组指针一般先通过*号取得指针指向的数组对象, 然后再用下标操作访问元素。

int a[10];
int(*p_ary)[10] = &a; // p_ary是一个指针, 指向"int (*)[10]"类型的数组
for (int i = 0; i < 10; i++) {
 // p_ary是一个指向数组的指针, 需要先通过间接寻址运算符*(indirection operator)取得数组对象
 // 再通过[i]下标操作访问元素。
 (*p_ary)[i] = i; 
}

int b[10];
int c[20];
p_ary = &b; // 合法
p_ary = &c; // 报错! 不能将 "int (*)[20]" 类型的值分配到 "int (*)[10]" 类型的实体

数组指针指向多维数组的子数组

int a[10];
int b[4][10];
int(*p_ary)[10] = &a;
for (int i = 0; i < 10; i++) {
 (*p_ary)[i] = 1; 
}

p_ary = &b[2]; // 多维数组,可以看作数组的数组,
// b[2][0] ~ b[2][9]的值都被改成2了
for (int i = 0; i < 10; i++) {
 (*p_ary)[i] = 2;
}

多维数组指针

多维数组指针,是一种指针,指向的对象是个多维数组,支持多个下标操作。

int a[2][5][10];
int(*p_ary1)[10] = &a[1][2]; // 1维数组指针
int(*p_ary2)[5][10] = &a[1]; // 2维数组指针
for (int row = 0; row < 5; row++) {
    for (int col = 0; col < 10; col++) {
        (*p_ary2)[row][col] = row * col;
    }
}

数组指针和指针数组对比实例

数组指针还是记住两步法即可

  1. 定义一个数组
  2. 括号包围1中定义的名称,再在名称前加个*号。
int a[10];
int(*ary_pointer1)[10] = &a; // 数组指针
int* pointer_ary1[10]; // 指针数组。元素类型是int*
int *(ponter_ary2[10]); // 指针数组。另外一种定义方式。
int (*ponter_ary3[10]); // 指针数组。另外一种定义方式。
int c, *d, (*ary_pointer2)[10], *pointer_ary3[10]; // 排列定义比较。

// 指针数组可以把每个元素指向数组对应位置的地址。
// 这样遍历指针数组, 可以达到遍历数组元素的效果,但是注意每个元素都是指针,
// 需要访问原数组的值的话, 需要对指针用*间接寻址运算符。
int* pointer_ary[10]; 
for (int i = 0; i < 10; i++) {
    pointer_ary[i] = &a[i];
}
// 类似遍历原数组效果。
for (int i = 0; i < 10; i++) {
    *pointer_ary[i] = i; // 修改原数组。
}

函数指针

取得函数地址

函数的名称作为参数被传递时,会隐式转换成函数指针, 和在函数名称前加取地址符&等价。建议带上更加统一和清晰。

void f(int);
int main()
{
    void (*p1)(int) = &f;
    void (*p2)(int) = f; // same as &f
    return 0;
}

翻译成汇编代码, p1和p2的赋值是一样的。

函数指针的声明

单个函数指针变量定义步骤

  1. 定义一个函数。void fun1(int a, int b); int fun2(double a);
  2. 用括号把函数名称包围起来,然后在名称前面加*号。void (*fun1)(int a, int b); int (*fun2)(double a);
  3. 如果要定义函数指针数组,在定义单个函数指针的基础上,在名称后面加上[数组长度]void (*fun1[2])(int a, int b); int (*fun2[10])(double a);

typedef定义函数指针

可读性高比单个定义要高,特别是声明多个同类型的函数指针,或者函数指针数组。

typedef定义函数指针的语法

typedef有两种做法, 一种就是定义一种函数对象,另外一种就是定义函数指针。用法稍稍不同,效果是一样。其中函数对象不支持赋值, 但是支持引用。

typedef int FuncObject(int a, int b); // FuncObject类型是函数对象
typedef int (*FuncPointer)(int a, int b); // FuncPointer类型是函数指针
FuncObject* f1 = &Add;
FuncPointer f2;
f2 = f1; // f1, f2类型一样, 都是形式为int(int, int)的函数的指针。
FuncObject f3 = Add; // 报错! 函数对象不支持拷贝
FuncObject f4 = &Add; // 报错!&Add是函数指针,与函数对象类型不匹配
FuncObject& f5 = Add; // 正确
int ret = f5(2, 3); // 正确
FuncObject& f6 = &Add; // 报错!&Add是函数指针,与函数对象引用类型不匹配

如何记住typedef定义函数指针的步骤

  1. 像定义一个函数指针那样, 指定一个名称。int (*CalFun)(int a, int b);
  2. 在这个函数指针变量声明前面加上typedef。typedef int (*CalFun)(int a, int b);

完整例子

typedef int(*CalFun)(int a, int b);

int Add(int a, int b)
{
    return (a + b);
}

int Sub(int a, int b)
{
    return (a - b);
}

int main(int argc, char** argv)
{
    CalFun f1 = Add;
    CalFun f2 = Sub;
    int a = f1(2, 3);
    int b = f2(10, 5);

    // typedef定义的函数指针数组。
    CalFun f_ary[2];
    f_ary[0] = Add;
    f_ary[1] = Sub;

    // 单个定义的函数指针数组。
    int(*f_ary2[2])(int a, int b);
    f_ary2[0] = Add;
    f_ary2[1] = Sub;
    
    return 0;
}

using别名定义函数指针

c++11以后的类型别名定义--using也可以用于定义函数指针, typedef的好处它都有,个人感觉比typedef更直观。using类型别名同样分函数对象和函数指针两种方式。

typedef int FuncObject(int a, int b); // FuncObject类型是函数对象
using FuncObject = int(int a, int b);
typedef int (*FuncPointer)(int a, int b); // FuncPointer类型是函数指针
using FuncPointer = int(*)(int a, int b);

函数指针的调用

  • 函数指针和函数对象都可以直接后加括号调用
int f();
int (*p)() = f;  // pointer p is pointing to f
int (&r)() = *p; // the lvalue that identifies f is bound to a reference
r();             // function f invoked through lvalue reference
(*p)();          // function f invoked through the function lvalue
p();             // function f invoked directly through the pointer
  • 如果函数有重载, 函数指针会指向匹配的那个版本。
template<typename T>
T f(T n) { return n; }
 
double f(double n) { return n; }
 
int main()
{
    int (*p)(int) = f; // instantiates and selects f<int>
}

成员函数指针

静态成员函数,除了增加了访问控制以外,跟普通的函数指针没什么区别,所以普通函数指针可以直接指向类的静态成员函数。但非静态的成员函数与普通函数指针不太一样,声明时需要指定函数归属的类名,并且调用需要指定对象实例。

成员函数指针定义。

  1. 像定义类成员函数实现那样写, 并任意指定名称,这里作func。void ClassName::func(int);
  2. 括号把类名、范围解析运算符::、名称包围起来。void (ClassName::func)(int);
  3. 在名称的前面加个*号void (ClassName::*func)(int);
  4. 成员函数也支持typedef和using的定义方式。typedef void(C::* MemberFunc)(int); using MemberFunc = void(C::*)(int);

成员函数指针如何调用。

假设成员函数指针名字为func

void (ClassName::*func)(int);

对象式调用。

ClassName c; // 被调用的对象
  1. 成员函数指针名字当作正常函数那样写。
    c.func(3);
  2. 成员函数指针是指针, func名称前面需加上间接寻址运算符*,变成函数对象。
    c.*func(3);
  3. 最后用括号把调用对象、成员访问运算符.、间接寻址运算符*、和成员函数指针的名称包围起来。
    (c.*func)(3);
    为何要加上括号? 根据c++的优先级标准,取成员运算符. > 函数调用() > 间接引用符*。 *号优先级比函数调用要低, 成员函数指针还没取得对象就被调用了,自然报错。 另外
    (c.(*func))(3);
    这样的写法也不行。 .*->*是整体作为一个运算符的,中间不能用括号隔开。

指针式调用

ClassName* p; // 被调用的对象的指针
  • 成员函数指针名字当作正常函数那样写。
p->func(3);
  • 成员函数指针是指针, func名称前面需加上间接寻址运算符*,变成函数对象。
p->*func(3);
  • 最后用括号把调用对象、成员访问运算符->、间接寻址运算符*、和成员函数指针的名称包围起来。
(p->*func)(3);

函数指针使用完整例子

struct Cal 
{ 
    int add(int a, int b); 
    int sub(int a, int b);
};

int main()
{
    int (Cal::*fun)(int, int) = &Cal::add;
    fun = &Cal::sub;

    Cal* p_cal = new Cal();
    int r1 = (p_cal->*fun)(2, 3);
    delete p_cal;

    Cal local_cal;
    int r2 = (local_cal.*fun)(8, 6);
}

成员变量指针

成员变量指针比成员函数指针还要简单些,没有函数调用, 无需考虑函数调用和间接引用符*的优先级问题。

成员变量指针的定义

假如以下结构体C。

struct C 
{ 
    int m; 
};

单个成员变量指针定义 

  1. 假设名称为p, 类似静态成员变量定义那样声明
    int C::p;
  2. 在名称前面加上指针标识号* 
    int C::*p;

typedef或using方式定义

typedef int C::*MemberPointer;
using MemberPointer = int C::*;

成员变量指针的使用。

类似成员函数指针那样,直接使用指向成员的指针运算符:.* 和->*即可。
成员变量指针, 能让我们实现一些遍历成员的动态功能。 例如把一个类/结构体的多个同类型的成员变量放进一个容器里,然后遍历访问这些成员变量。

 完整例子

struct C { int m; };
int main()
{
    int C::* p = &C::m;          // pointer to data member m of class C
    C c = {7};
    std::cout << c.*p << '\n';   // prints 7
    C* cp = &c;
    cp->m = 10;
    std::cout << cp->*p << '\n'; // prints 10
}

  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值