跳转至

指针

指一类存储 内存地址 的变量

指针的基类型

告诉编译器指针指向的 内存块大小编码方式

一般存的数据是什么类型,基类型就是什么类型

比如:

int val=65;
int * p=&val; // p 存的是 val 所在的内存块第一个字节的地址
// int 告诉编译器:解引用时,按 int 大小往后取 4 字节并按 int 的编码方式翻译

指针算术

指针的偏移

合法的偏移运算符有:+-++--+=-=

如果没有指定偏移量,则默认根据基类型指定偏移规则

int * p=&a; // p+1 加 sizeof(int) = 4 Byte

也可用 [n] 自行指定偏移量

偏移量 \(=\) \(n×\mathrm{sizeof(}\) 基类型 \()\)

int (*p)[2]=&a; // 每次偏移 2*sizeof(int) = 8 Byte
char (*pp)[8]=&a; // 每次偏移 8*sizeof(char) = 8 Byte

Warning

void* 指针的偏移在不同平台上可能有差别,尽量避免使用

如:ANSI C 规定 void* 指针不能偏移;而 GNU 规定 void* 指针的偏移规则与 char* 完全相同

注意与指针数组区别

int (*p)[8]=&a; // 指针
int *p[8]; // 指针数组

Note

不建议将 int (*p)[8] 称作 数组指针,否则容易以为 sizeof(p)=8*sizeof(int)

指针的比较

比较两指针所存地址的 相对大小

int a[3][2];
*p1=&a[1][1];
*p2=&a[1][2];
*p3=&a[2][2];
// 则有 p1 < p2 < p3

两指针相减

只有 基类型相同 的指针才能相减,得到两指针间 基类元素个数,类型为 long int ,符号表示两指针相对位置,大减小为正

int a[5]={1,2,3,4,5};
int *p1=&a[1];
int *p2=&a[3];
printf("%ld %ld",p2-p1,p1-p2); // Result: 2 -2

指针的强制类型转换

格式:(基类型(指针级数)[偏移量])待转换指针

Note

指针级数:几级指针就是几个 * ,比如,普通指针就是一个 * ,指向指针数组的指针就是 **

int a[5]={1,2,3,4,5};
char (*p)[4]=(char(*)[4])a;
printf("%d %d\n",*(int*)p,*(int*)(p+3)); // Result: 1 4
return 0;

Warning

上面的程序有两点需要注意:

  1. 赋值运算符 = 左右两边类型要相同

  2. 编译器是根据基类型,而不是偏移量决定数据解释方式。也就是说,不会因为 p 的偏移量为 4 就按 int 类型解释 *p,所以在 printf 中一定要告诉编译器 *(int*)p ,才会按 int 类型解释

指针与数组名

再次强调,数组名 ≠ 指向数组首地址的指针

相同

数组名和指针有许多相似之处,比如:

数组名可以当做指针访问数组中的元素,偏移量为数组中 单个元素所占的内存大小

int arr[5]={1,2,3,4,5};
printf("%d %d\n",arr[0],*arr); // Result: 1 1
printf("%d %d\n",arr[2],*(arr+2)); // Result: 3 3

区别

但指针与数组名也有许多不同:

  1. sizeof 运算符

    \(\mathrm{sizeof}(\) 指针 \()=\) 指针变量所占内存大小

    \(\mathrm{sizeof}(\) 数组名 \()=\) 数组所占内存大小

    Note

    一般来说,指针变量所占内存大小一般等于当前 CPU 的最大位数

    int arr[5]={1,2,3,4,5};
    printf("%lu\n",sizeof(arr)); // Result: 20 = 5 * sizeof(int)
    int* pi=nullptr;
    double* pd=nullptr;
    char* pc=nullptr;
    void* pv=nullptr;
    printf("%lu\n",sizeof(pi)); // Result: 8
    printf("%lu\n",sizeof(pd)); // Result: 8
    printf("%lu\n",sizeof(pc)); // Result: 8
    printf("%lu\n",sizeof(pv)); // Result: 8
    

    对于二维数组

    int arr[2][2]={1,2,3,4};
    printf("%lu\n",sizeof(arr)); // Result:16
    printf("%lu\n",sizeof(arr[0])); // Result: 8
    

    可以看到 arr[i] 实际上就是取出了 arr 的第 i

  2. 对数组名取地址

    对数组名取地址会得到一个 指向数组首地址的常量指针,并且这个指针的 偏移量等于数组所占内存大小 ,也就是 &str 会得到一个类型为 const char(*)[sizeof(str)] 的指针

    也可以认为,数组名并不是一个实体的变量,没有被分配内存(不像指针变量,有属于自己的内存),只是依附于数组的一些信息,和数组捆绑存储,这样,对数组名取地址得到的自然还是数组的首地址

  3. 你可以修改指针指向的地址,但你不能修改数组名的指向

数组名退化为指针

众所周知,在数组名作为参数传给函数时,数组名会退化为指针。因为用来接收数组名的形参是指针,而指针只能存一个地址,而不能存数组大小等信息,因此会发生信息的丢失,也就是我们说的 数组名退化

void test(int* a){
    printf("%#x\n",a); // Result: 0xee01a8d0
    printf("%#x\n",&a); // Result: 0xee01a8a8
}
int main(){
    int arr[5];
    printf("%#x\n",arr); // Result: 0xee01a8d0
    printf("%#x\n",&arr); // Result: 0xee01a8d0
    test(arr);
    return 0;
}

通过这个程序我们可以看到,arra 都存储了数组的首地址 0xee01a8d0,而 a 的地址 &a 不同于数组首地址,这说明编辑器新建了一个指针变量,并将数组首地址存进去了,实际上,这一过程就是我们常说的 传值传参

既然问题出在传参上,那我们常用的防止拷贝复制的传参方式 传引用 ,能不能用在这里防止数组名退化呢?1

void test(int (&a)[5]){
    printf("%#x\n",a); // Result: 0xee01a8d0
    printf("%#x\n",&a); // Result: 0xee01a8d0
    printf("%lu\n",sizeof(a)); // Result: 20
}
int main(){
    int arr[5];
    printf("%#x\n",&arr); // Result: 0xee01a8d0
    test(arr);
    return 0;
}

可以看到,改用传引用的方式传递数组名后,没有发生数组名退化为指针的情况,对 a 使用 sizeof 运算符也能得到数组所占内存大小 20

但这一方法要求我们在形参中指定传入数组的大小,如果形参指定的大小和传入数组的大小不一样,就会报错

这一问题可以使用模板解决:

template<int T>
void test(int (&a)[T]){
    printf("%lu\n",sizeof(a));
    for(auto i:a)
        printf("%d ",i);
    putchar('\n');
}
int main(){
    int a[3]={1,2,3};
    int b[6]={1,1,4,5,1,4};
    test(a);
    test(b);
    return 0;
}

通过模板,我们引进了一个 int非类型参数(表示一个值而非一个类型),当调用 print 函数时,编译器就会从实参中推断出参数数组的大小 T,并且实例化对应的函数模板

对于二维数组也是类似的,函数声明作如下修改即可:

template<int T>
void test(int (&a)[T][T])

这样我们就可以愉快地在函数里用 sizeofa[i][j] 了!

Question

C 没有模板怎么搞呢?

指针与二维数组

二维数组的实质

二维数组 int a[n][m] 存的是 \(n\) 个一维数组,所以有:

  1. a[i] 可以得到二维数组 a[n][m] 中存储的第 i一维数组的数组名

    注意,得到的是 一维数组的数组名 而不是 首地址,也就是说 sizeof(a[i]) 会得到 m*sizeof(int) ,而不是 sizeof(int*)

    int a[3][2]={{1,2},{3,4},{5,6}};
    printf("%lu\n",sizeof(a)); // Result: 3*2*sizeof(int)=24
    printf("%lu\n",sizeof(a[0])); // Result: 2*sizeof(int)=8
    printf("%#x %#x\n",a[0],&a[0]); // Result: 0xec720850 0xec720850
    

    可以看到,a[0] 具有数组名的性质

    利用这一点,我们可以使用 指针数组指向指针数组的指针(多级指针) 对二维数组进行模拟:

    int a[3][2]={1,2,3,4,5,6};
    int *p[3]={a[0],a[1],a[2]}; // 指针数组
    int **pp=p; // 指向指针数组的指针 (多级指针)
    // pp[i][j] 与 a[i][j] 完全等效
    
  2. 二维数组名的指针运算

    作为数组名肯定是不能自增自减的,只能 +i-i

    a[n][m] 存的是 \(n\) 个一维数组 ”,这告诉我们,二维数组中的元素是一维数组,那 +1 就应该是 加一个一维数组的大小

手动寻址

二维数组在 内存中连续存储,也就是说,二维数组里存的若干个一维数组,在内存中是 首尾相接 的,这是数组名退化为指针后手动寻址的基础

记数组 a[n][m] 的数组名 a 退化为指针 p,则有:

手动寻址公式:a[i][j]==*(p+n*i+j)

对于二维数组,最方便的传参方式还是上面提到的 传引用方式

const 修饰的指针

  1. 常指针const int* p=&a

    指向常量的指针,指针所指地址可以变,但所指地址存的东西不能变

  2. 指针常量int* const p=&a

    指针类型的常量,指针所指地址不能变,但所指地址存的东西可以变

  3. 指向常量的指针常量const int* const p=&a

    所指地址,和所指地址存的东西都不能变

Note

可以这样记忆:const 修饰谁,谁就不能变

const int* p 中,const 修饰了 *p,所以所指地址存的东西不能变

int* const pconst 只修饰了 p 所以是指针所指地址不能变

void 指针

即没有指定基类型的指针,可以声明函数返回值和形参为任何类型,这一点常用于 实现泛型

比如 memcpy 可以将一块内存中的内容拷贝到另一块内存中去,很明显,这一过程中我们没必要知道内存中存的数据是什么类型的,所以我们可以使用 void 指针( void* memcpy(void* dest,const void* src,size_t len)

Warning

void* 指针的偏移在不同平台上可能有差别,尽量避免使用

如:ANSI C 规定 void* 指针不能偏移;而 GNU 规定 void* 指针的偏移规则与 char* 完全相同

函数指针

函数名和数组名非常类似,也不是实体变量,没有分配内存,对其取地址、解引用得到的都是它自己

也就是:function==&function==*function

幸运的是,函数名不像数组名那样复杂,可以近似地认为不管对函数名干什么,得到的都是 函数的入口地址

可以声明一个指向函数的指针(即 函数指针),格式如下:返回值类型 (*指针名)(形参列表)

函数指针常用与传入比较函数,提供比较规则 2

Note

提供比较规则的常用手段还有:重载运算符声明比较类

bool cmp(const item & x,const item & y){
    return x<y; // 从小到大排序
}
bool (*p)(const item&,const item&)=cmp;
std::sort(d.begin(),d.end(),cmp); // cmp 形参就是一个函数指针
cout<<(*p)(3,4)<<endl; // Result: 1

评论