Contents

C函数:指针

指针变量圈定的内存大小与编译器或者说地址总线有关。在32系统中,指针变量大小为4byte;在64位系统中,指针变量大小为8byte。将指针和指针变量的概念进行区分,指针代表这个指针变量的值,即内存地址;指针变量指的是一个变量,用于存放内存地址。

指针访问内存

访问指针指向的地址,需要考虑两个问题:1. 内存的可读可写性是什么;2. 内存的访问规则是什么

指针变量的初始化

#include <stdio.h>
#include <stdlib.h>

int b = 20;
int main()
{
    int a = 10;

    int *p1 = &a;
    int *p2 = &b;
    int *p3 = (int *)malloc(sizeof(int));

    if(!p3){
        return;
    }
    *p3 = 30;
    int *p;

    p = (int *)(100);

    printf("*p1 = %d, *p2 = %d, *p3 = %d \n", *p1, *p2, *p3);
    printf("*p1 = %d, *p2 = %d, *p3 = %d \n", *p1, *(&b), p3[0]);
    printf("p = %p \n", p);

    // 访问的内存不合法
    printf("*p = %d \n", *p);

    free(p3);

}

输出:

*p1 = 10, *p2 = 20, *p3 = 30 
*p1 = 10, *p2 = 20, *p3 = 30 
p = 0x64 
段错误 (核心已转储)

空指针和野指针

  • 空指针:值为0(NULL)的指针变量,如果访问了0地址就会出现非法访问的错误。
  • 野指针值为非法地址的指针变量

野指针案例:

#include <stdio.h>
#include <stdlib.h>

void main(void)
{
    int *ptr = (int*)malloc(sizeof(int));
    free(ptr);

    if(NULL != ptr){
        *ptr = 102;
    }

    int *p = (int*)malloc(sizeof(int));
    printf("*p = %d \n", *p);
    printf("ptr = %p \n", ptr);
    printf("p = %p \n", p);
    
}

申请ptr指针后立即释放掉,系统将内存地址标记为可用状态,但ptr指针变量中的值仍是原来分配的值,此时ptr就变成了野指针。当再申请新的内存时,将同一块地址给p,此时对ptr的操作会干扰p.

输出结果:

*p = 102 
ptr = 0x5f3763aac2a0 
p = 0x5f3763aac2a0 

避免野指针出现的一些技巧

  • 初始化:指针声明时要么指向有效内存,要么置空;
  • 校验:访问指针前先判断是否为 NULL/nullptr;
  • 置空:释放内存后立即将指针置空;
  • 规避:避免返回局部变量指针、避免裸指针越界;
  • 替代:C++ 用智能指针替代裸指针,从根源减少野指针。

指针访问内存的规则

指针访问内存的方式:*p, p[x], p->x

指针变量的定义形式是:数据类型 *p,因此,每次访问的大小由前面修饰的数据类型决定。

1. 标准数据类型指针

#include <stdio.h>

void main(void)
{
    int a = 0x12345678;

    int *p = &a;
    char *p1 = (char*)&a;

    printf(" *p = 0x%x \n", *p);
    printf(" *p1 = 0x%x \n", *p1);

    printf(" p1[0] = 0x%x \n", p1[0]);
    printf(" p1[1] = 0x%x \n", p1[1]);
    printf(" p1[2] = 0x%x \n", p1[2]);
    printf(" p1[3] = 0x%x \n", p1[3]);

}

指针根据前面的修饰符决定操作的内存大小,不管是大端还是小端,指针都指向低地址

2. 连续空间类型指针

在C语言中,连续空间分为两种,一种是结构体,一种是数组

#include <stdio.h>

struct abc {
    int a;
    int b;
    char c;
};

int arr[3] = {1,2,3};

int main()
{
    struct abc data =
    {
        .a = 1,
        .b = 2,
        .c = 3,
    };
    
    printf("size of struct abc = %ld\n", sizeof(struct abc));

    struct abc *p = &data;
    printf(" a = %d, b = %d, c = %d\n", p->a, p->b, p->c);

    int *p1 = (int *)&data;
    printf(" p1 = %d \n", *p1);
    printf(" struct p1[0] = %d \n", p1[0]);
    printf(" struct p1[1] = %d \n", p1[1]);
    printf(" struct p1[2] = %d \n", p1[2]);

    int *p2 = arr;

    printf(" arr p2[0] = %d \n", p2[0]);
    printf(" arr p2[1] = %d \n", p2[1]);
    printf(" arr p2[2] = %d \n", p2[2]);
}

在上面的代码示例中,abc是一个结构体类型,用int类型的指针p1访问abc data的数据时,p1操作的内存大小为4字节,因此直接打印*p1访问到的是data.a,而使用p1[2]访问data.c时,由于data.cchar类型,p1[2]访问的内存大小超出data.c圈定的内存大小,会导致取值错误,可以对其进行强制转换,修改后代码如下:

***
    printf(" struct p1[1] = %d \n", *(char *)&p1[2]);
    printf(" struct p1[1] = %d \n", (char)p1[2]);
***

3. linux第一宏container_of

container_of可以通过结构体的成员变量找到整个结构体的对象。结构体对象的地址和结构体中第一个成员变量的内存地址相同,当知道结构体的某个成员变量的内存地址和该成员变量相对首个成员变量的偏移地址,就可以计算出结构体对象的内存地址。

#include <stdio.h>

struct abc {
    int a;
    int b;
    char c;
};

void find_struct(int *member)
{
    printf(" address of member = %p \n", member);

    unsigned long offset = 0;
    struct abc *p = NULL;

    offset = (unsigned long)&((struct abc*)0)->b;
    printf(" member offect: %ld \n", offset);

    p = (struct abc*)((char *)member - offset);

    printf(" address of p = %p \n", &p);
    printf(" a: %d, b: %d, c: %d \n", p->a, p->b, p->c);
}

int main()
{
    struct abc data = {
        .a = 1,
        .b = 2,
        .c = 3
    };

    struct abc *p1 = &data;

    printf(" size of struct abc = %ld \n",  sizeof(struct abc));
    printf(" address of p1 = %p \n", p1);
    printf(" address of p1->a = %p \n", &p1->a);
    printf(" address of p1->b = %p \n", &p1->b);
    find_struct(&data.b);

    return 0;
}

上述代码实现了通过成员变量data.b找到对象data的功能,有两点需要注意:1. 成员地址偏移量是一个非负的字节数,可以用size_t或者unsigned long定义变量;2. C语言中指针运算是按类型大小缩放的,只有char *的指针加减是按1字节进行的,才能减去正确的字节级offect,计算出结构体对象地址。

4. 函数类型指针

函数类型指针定义需要括号保证优先级,并且传入参数、返回值需要完全一致。

#include <stdio.h>

void (*show)(void);

void func(void)
{
    printf("func call \n");
}

int main()
{
    show = func;
    show();

    return 0;
}

在实际工程中,通常结合typedef增强代码可读性:

typedef int (*func_t)(int, int);

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

func_t fp = add;

int r = fp(2, 3);

指针运算

1. 算术运算

指针运算的偏移单位等于指针所指向类型的sizeof(类型)

#include <stdio.h>

int main()
{
    int array[] = {1,2,3,4,5};
    int *p = array;

    printf(" *p = %d \n", *p);

    p = p + 2;
    printf(" *p = %d \n", *p);

    return 0;
}

2. 逻辑运算

在指针逻辑运算中,判断两个指针是否相等比较常用。

p==NULL
p1 == p
// Linux下MAX的实现
// 只有当类型完全一致时,指针比较才是合法的,通过指针比较触发编译期类型检查,以避免隐式类型比较带来的 bug
#define MAX(x,y)({
    typeof(x) _max1 = (x);
    typeof(y) _max2 = (y);
    (void) (&_max1 == &_max2);
    _max1 > _max2 ? _max1 : _max2;
    })

多级指针

1. 多级指针定义与访问

多级指针本质上也是一个指针,也需要一个指针变量来存放。这个指针变量的内存大小跟一级指针,跟系统有关。

int **p;

1. *p 第一'*'决定变量p是一个指针变量
2. **p 第二'*'决定指针p访问内存规则是以 指针类型 进行访问,所以*p的值还是一个指针
3. int 决定 指针(*p)访问内存是以 int类型 进行访问,所以*(*p)是 int类型

多级指针访问示例:

#include <stdio.h>

int main()
{
    int a = 100;
    int *p = &a;
    int **pp = &p;

    printf(" address of p = %ld\n", p);
    printf(" *pp = %ld \n", *pp);
    printf(" p[0] = %d \n", p[0]);
    printf(" **pp = %d, *pp = %d \n", **pp, *pp[0]);
    printf(" size of pp %ld \n", sizeof(pp));

    return 0;
}

输出:

 address of p = 140720738268420
 *pp = 140720738268420 
 p[0] = 100 
 **pp = 100, *pp = 100 
 size of pp 8 

2. 指针的地址传递

  • 指针值传递

指针值传递拷贝了原指针变量的值,可以访问原指针指向的内存地址,但是不能修改原指针变量中存储的值。

#include <stdio.h>

void func1(char *p)
{
    printf(" address of p = %p \n", p);
    printf(" func1: %s \n", p);
    p = "hello linux";
    printf(" address of p = %p \n", p);
}

int main()
{
    char *s = "hello world";
    printf(" address of s = %p \n", s);
    func1(s);
    printf(" address of s = %p \n", s);
    printf(" s = %s \n", s);

    return 0;
}

输出:

 address of s = 0x6010f1d02034 
 address of p = 0x6010f1d02034 
 func1: hello world 
 address of p = 0x6010f1d0201b 
 address of s = 0x6010f1d02034 
 s = hello world 
  • 多级指针通过指针修改指针本身

将指针变量的地址作为参数传入,可以操作指针变量的内存空间修改指针指向的内存地址。

#include <stdio.h>

void func2(char **ss)
{
    printf(" func2: %s \n", *ss);

    *ss = "hello linux";
}

int main()
{
    char *s = "hello world";
    printf(" address of s = %p \n", s);
    func2(&s);
    printf(" address of s = %p \n", s);
    printf(" s = %s \n", s);

    return 0;
}

输出:

 address of s = 0x5cc1aef6803f 
 func2: hello world 
 address of s = 0x5cc1aef68026 
 s = hello linux 

3. 逻辑映射(物理无序映射到逻辑有序)

#include <stdio.h>

int main()
{
    char *arr[3] = {"welcome", "to", "linux"};
    char **s = arr;

    printf("%s:%p\n", s[0], s[0]);
    printf("%s:%p\n", s[1], s[1]);
    printf("%s:%p\n", s[2], s[2]);

    printf("&s[0]:%p\n", &s[0]);
    printf("&s[1]:%p\n", &s[1]);
    printf("&s[2]:%p\n", &s[2]);

    printf("arr[0]: %p\n", arr[0]);
    printf("arr[1]: %p\n", arr[1]);
    printf("arr[2]: %p\n", arr[2]);

}

输出:

welcome:0x5e080c59a004
to:0x5e080c59a00c
linux:0x5e080c59a00f
&s[0]:0x7ffe7c46f0b0
&s[1]:0x7ffe7c46f0b8
&s[2]:0x7ffe7c46f0c0
arr[0]: 0x5e080c59a004
arr[1]: 0x5e080c59a00c
arr[2]: 0x5e080c59a00f

其中,arr是指针数组,里面存了3个char *指针;s是二级指针,指向arr[0](第一个指针元素)。内存分布为三层,arrs分布在栈区,字符串字面量"“welcome” “to” “linux"分布在只读数据区,指向关系如下:

s -> arr[0](s[0]) -> "welcome"
     arr[1](s[1]) -> "to"
     arr[2](s[2]) -> "linux"

为什么需要二级指针?

判据:是否需要二级指针取决于数组的元素是不是指针。

数组名在大多数表达式中会退化为指向数组首元素的指针,但它本身不是指针,是一个不可修改的地址常量。 一级指针访问数组元素,得到的是元素的值,当元素是普通类型(char、int、struct)时,一级指针可以获取相应数据类型的值;当元素是指针时,例如指针数组arr,使用一级指针只能获取数组中存储的指针,需要再一次寻址才能获取对应char类型的值:

#include <stdio.h>

int main()
{
    char *arr[3] = {"welcome", "to", "linux"};
    size_t *s = arr;

    printf("s[0]=%p\n", s[0]);
    printf("s[1]=%p\n", s[1]);
    printf("s[2]=%p\n", s[2]);

    printf(("data of s[0]=%s \n", (char *)s[0]));
    printf("\n");
}

通过二级指针可以直接实现两层间接寻址。

数组

数组定义及访问

数组名在大多数表达式中会退化为指向数组首元素的指针,但它本身不是指针,是一个不可修改的地址常量。

一维数组的访问

一维数组访问本质是指针加偏移再解引用。

#include <stdio.h>

int main()
{
    int a[] = {10, 20, 30, 40};

    printf("&a[0]:%p, &a[1]:%p, &a[2]:%p \n", &a[0], &a[1], &a[2]);
    printf("a:%p, (a+1):%p, (a+2):%p \n", a, a+1, a+2);
    printf("a[0]=%d, a[1]=%d, a[2]=%d \n", a[0], a[1], a[2]);
    printf("*a=%d, *(a+1)=%d, *(a+2)=%d \n", *a, *(a+1), *(a+2));

    return 0;
}

&a[0]:0x7ffc51aa33f0, &a[1]:0x7ffc51aa33f4, &a[2]:0x7ffc51aa33f8 
a:0x7ffc51aa33f0, (a+1):0x7ffc51aa33f4, (a+2):0x7ffc51aa33f8 
a[0]=10, a[1]=20, a[2]=30 
*a=10, *(a+1)=20, *(a+2)=30 

二维数组的访问

二维数组是“数组的数组”,访问时是先行偏移,再列偏移。

int a[3][4];

// 内存布局
a[0][0] a[0][1] a[0][2] a[0][3]
a[1][0] ...

访问等价形式:
a[i][j] == *(*(a+i)+j)

具体含义为:
- a  指向第 0 
- a + i   i 
- *(a + i)   i 行首地址
- + j   j 

a[i]退化为“指向第i行数组首元素的指针”,但a[i]本质上是一维数组,其类型信息仍然存在,不能简单当作指针使用。 这是数组与指针最核心的区别。

#include <stdio.h>

int main()
{
    int a[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    // a指向的是元素是数组,进行算术运算时,加减的单位是一个数组的大小
    printf("a:%p\n", a);
    printf("a+1:%p\n", a+1);

    // *(a+1)等价于一维数组的数组名
    // *(a+1)[0]访问第1个数组的第0个元素
    // *(*(a+1)+i)访问第1个数组的第i个元素
    printf("*(a+1)[0]:%d\n", *(a+1)[0]);
    printf("*(*(a+1)):%d\n", *(*(a+1)));

}

数组和指针的区别

数组是连续存储的数据集合,指针是变量,存的是地址。

指针数组vs数组指针

指针数组是“装指针的数组”,数组指针是“指向数组的指针”。

  • 指针数组

常用于字符串数组、参数列表

char *arr[3] = {"welcome", "to", "linux"};
int *p[5];
  • 数组指针
#include <stdio.h>

int main()
{
    int a[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    // a指向的是元素是数组,进行算术运算时,加减的单位是一个数组的大小
    printf("a:%p\n", a);
    printf("a+1:%p\n", a+1);

    // *(a+1)等价于一维数组的数组名
    // *(a+1)[0]访问第1个数组的第0个元素
    // *(*(a+1)+i)访问第1个数组的第i个元素
    printf("*(a+1)[0]:%d\n", *(a+1)[0]);
    printf("*(*(a+1)):%d\n", *(*(a+1)));

    // 定义数组指针
    int (*p)[3];

    p = a[2];
    printf("p[0]:%d\n", *p[0]);

}

二维数组和二级指针的关系

二维数组名不是二级指针,它退化后是“数组指针”。所以二维数组不可以用类型 **访问,因为类型部匹配。

二维数组由于内存连续且行宽固定,必须使用数组指针访问; 指针数组在每个元素都指向合法连续内存的前提下,可以通过二级指针进行访问,但它本质上并不是二维数组。