C语言二级指针的介绍和使用

一、一级指针

一级指针的简单描述

①一级指针是一种以存储其他变量地址为目的的变量。一个T类型的指针变量(一级指针)就是一个存储了某T类型值变量的内存地址的引用。

②对指针的解引用(也就是*),是获取指针指向的内存空间中存储的值。

③当指针解引用做左值的时候,可以改变指针指向内存空间中存储的值。

④指针指向哪个变量,就将该变量的内存地址赋给该指针(用于指针变量做左值的时候)。

⑤改变指针变量的值(指针变量做左值时),就改变了该指针的指向。

二、二级指针的相关介绍

        多级指针(pointer to pointer to)是指向指针的指针,二级指针是指向一级指针的指针。

一级指针指向的是某个变量,也就是一级指针中存储的是某个变量的内存地址;二级指针指向一级指针,也就是二级指针中存储的是一级指针的内存地址。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int  main( void )
{
     int  a = 10;                         //声明一个变量a
     int  *p = &a;                        //声明指针p,指向变量a
     int  **q = &p;                       //声明二级指针q,指向一级指针p
     printf ( "a = %d\n" ,a);               //打印变量a的值
     printf ( "a的地址&a=%p\n" ,&a);         //打印变量a的地址
     printf ( "p = %p\n" ,p);               //打印p的值
     printf ( "p的地址&p=%p\n" ,&p);         //打印p的地址
     printf ( "p的解引用*p=%d\n" ,*p);       //打印p的解引用
     printf ( "q = %p\n" ,q);               //打印q的值
     printf ( "q的地址&q=%p\n" ,&q);         //打印q的地址
     printf ( "q的解引用*q=%p\n" ,*q);       //打印q的解引用
     printf ( "q的双重解引用**q=%d\n" ,**q);  //打印q的双重解引用
     return  0;
}

执行结果

a = 10

a的地址&a=0x7fff5fbff838

p = 0x7fff5fbff838

p的地址&p=0x7fff5fbff830

p的解引用*p=10

q = 0x7fff5fbff830

q的地址&q=0x7fff5fbff828

q的解引用*q=0x7fff5fbff838

q的双重解引用**q=10

内存结构示意图

wKiom1Xz386jNEX9AAGufVXL4L4041.jpg

        如上图的代码和执行后的结果,从结果中就可以看出变量a,一级指针p和二级指针q的关系。

在学习的过程中主要是对二级指针不是很理解,所以这里特别对二级指针说明一下。

变量q是一个二级指针,里面存放的是一级指针p的地址,对q的解引用就得到了q所指向的一级指针的值,也就是变量a的地址,对q双重解引用就得到了变量a的值,所以也可以通过二级指针来修改变量a的值。


三、指针与二位数组

        首先一点的是,虽然二维数组的数组名可以看做是一个指针,但是并不能将二维数组的数组名赋值给一个二级指针,也就是如下的代码

1
2
3
4
5
6
int  main( void )
{
     int  arr[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
     int  **p = arr;
     return  0;
}

        上面的代码在执行的时候会报错,这是因为两者数据类型是不相同的,所以不能赋值。

二维数组的数组名指向的是一维数组,也就是指向数组类型,但是二级指针指向的是一级指针,也就是指针类型,所以两者不能互相赋值。


下面详细介绍指针与二维数组的关系

声明一个二维数组

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

        要想理解指针和二维数组之间的关系,就要首先弄清楚各种数组名所代表的元素。

对于二维数组的存储,是按照先行后列的形式排列的,把每一行看做是一个一位数组,那二维数组就是由多个一位数组组成的数组,即二维数组是数组的数组。

        对于二维数组名,可以将它看做是指向行数组的指针,也就是说二维数组的元素是行数组,所以对于二维数组加减的变化是以行数组的大小为单位的,即arr指向arr[0]这个行数组,arr+1指向的是arr[1]这个行数组。对其进行解引用,得到的是每一行数组的首地址,即*arr表示的是第0行数组的首地址,和arr[0]相等,*(arr+1)表示的是第1行数组的首地址,和arr[1]是相等的。假如要取第1行第2个元素的地址,就是arr[1]+2,因为此时arr[1]代表的是一维数组,所以它的元素就是一个数字,在加减的时候移动的就是元素的大小,例如+1就表示该数组中第1个元素,+3就表示数组中的3个元素(以0开始)。因为

*(arr+1)和arr[1]相等,所以第1行第2个元素的地址也可以表示为*(arr+1)+2,对这个地址进行解引用,得到的就是数组元素的具体值,也就是*(*(arr+1)+2)。

所以有如下公式,假如一个二维数组每行有N个元素,二维数组名是arr,那第i行第j个元素的地址是

*(arr+i*N)+j,也可以表示为arr[i]+j。

元素的值可以表示为*(*(arr+i)+j),或者可以表示为arr[i][j]。

还用数组的形式来表示元素的地址和值,可以更方便编程人员的阅读,但是使用指针的方式更方便与C语言编译器阅读。

验证代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int  main( void )
{
     int  arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
     //分别使用不同的形式打印第2行,第3个元素的值和地址
     //根据推到出来的规律如下
     //该元素的地址
     printf ( "%p\n" ,*(arr+2*4)+3);
     printf ( "%p\n" ,arr[2]+3);
     printf ( "%p\n" ,&arr[2][3]);
     //该元素的值是12
     printf ( "%d\n" ,*(*(arr+2)+3));
     printf ( "%d\n" ,arr[2][3]);
     return  0;
}

执行结果如下

0x7fff5fbff88c

0x7fff5fbff82c

0x7fff5fbff82c

12

12

        对上述二维数组arr,虽然arr[0]、arr都是数组首地址,但二者指向的对象不同,arr[0]是一维数组的名字,它指向的是arr[0]数组的首元素,对其进行“*”运算,得到的是一个数组元素值,即arr[0]数组首元素值,因此,*arr[0]与arr[0][0]是同一个值;而a是一个二维数组的名字,它指向的是它所属元素的首元素,它的每一个元素都是一个行数组,因此,它的指针移动单位是“行”,所以arr+i指向的是第i个行数组,即指向arr[i]。对arr进行“*”运算,得到的是一维数组arr[0]的首地址,即*arr与arr[0]是同一个值。当用int *p;定义指针p时,p的指向是一个int型数据,而不是一个地址,因此,用arr[0]对p赋值是正确的,而用arr对p赋值是错误的。


四、数组指针

        既然不能将二位数组的数组名赋值给二位指针,那该用什么来表示二位数组呢。答案就是数组指针。数组指针就是指向数组的指针。

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

    int (*p)[4] = arr;

        如上图所示的声明方式,可以认为指针p就是指向数组的指针,这个数组中有4个int类型的元素,此时p的增量以它所指向的一维数组长度为单位。同时要想使用指针p来表示数组arr中的元素,上面总结的规律也是可以使用的。

如下图的代码,就是将上面的arr换成了指针p,同样可以使用。

int main(void)

{

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

    int (*p)[4] = arr;

    //分别使用不同的形式打印第2行,第3个元素的值和地址

    //根据推到出来的规律如下

    //该元素的地址

    printf("%p\n",*(p+2*4)+3);

    printf("%p\n",p[2]+3);

    printf("%p\n",&p[2][3]);

    //该元素的值是12

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

    printf("%d\n",p[2][3]);

    return 0;

}

使用数组指针指向一维数组

int a[3]={1,2,3}; 那么p就是指向这个数组a的指针。
int(*p)[3]=&a; // 这里赋值一定要用取地址符号。也就是取数组a的地址。

不可以这样赋值: int(*p)[3]=a; // error :类型不兼容。a本来是数组类型,是不可以赋值给int(*)[3]这个类型的。

但是这样是可以的int *p1=a; // ok 因为a可以隐式转换为int*类型,其值实际上就是数组第一个元素的地址,也就是&a[0]。


数组指针练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//打印数组
void  print_Array( char  (*p)[30], int  *len)
{
     for ( int  i=0;i<*len;i++)
     {
         printf ( "%s\n" ,p[i]);
     }
}
 
//数组排序
int  sort( char  (*p)[30], int  *len)
{
     int  ret = 0;
     char  tmp[100];
     if (p==NULL||len==NULL)
     {
         printf ( "function sort error" );
         ret = -1;
     }
     for ( int  i=0;i<*len;i++)
     {
         for ( int  j=i+1;j<*len;j++)
         {
             if ( strcmp (p[i],p[j])>0)
             {
                 strcpy (tmp,p[i]);
                 strcpy (p[i],p[j]);
                 strcpy (p[j],tmp);
             }
         }
     }
     return  ret;
}
 
int  main( void )
{
     char  array[10][30] = { "sdecs" , "codeq" , "owxja" , "qwer" , "bsdws" };
     int  count = 5;
     print_Array(array, &count);
     sort(array, &count);
     printf ( "排序之后的数组\n" );
     print_Array(array, &count);
     return  0;
}

执行结果

1
2
3
4
5
6
7
8
9
10
11
sdecs
codeq
owxja
qwer
bsdws
排序之后的数组
bsdws
codeq
owxja
qwer
sdecs

上面的代码是使用数组指针做函数参数,接受主调函数传递过来的二维数组的地址,然后利用数组指针对二位数组进行相关的操作。



五、指针数组

        说完了数组指针,现在接着要说一下指针数组,这两者并没有什么联系,放在一起是因为两者的声明比较像,有时候容易弄混,放在一起可以比较一下。

        指针数组就是数组里面存放的是指针这种数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int  main( void )
{
     int  a = 1;
     int  b = 2;
     int  c = 3;
     int  *d = &a;
     int  *e = &b;
     int  *f = &c;
     int  *p[3] = { d, e, f };
     printf ( "%d\n" ,*p[0]);
     printf ( "%d\n" , *d);
     printf ( "%p\n" , p[0]);
     printf ( "%p\n" , &a);
     return  0;
}

执行结果

1

1

0x7fff5fbff818

0x7fff5fbff818

如上图所示分别是指针数组的代码和相应的执行结果。

上面的p就是一个数组,这个数组有3个元素,每个元素都是int *类型的,也就是每个元素都是指向int类型的指针。


下面是指针数组的练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//数组打印
void  print_array( const  char  **p, const  int  *len)
{
     for ( int  i=0;i<*len;i++)
     {
         printf ( "%s\n" ,p[i]);
     }
}
 
//元素排序
int  sort( char  **p, int  *len)
{
     char  **str = p;
     int  *count = len;
     char  *tmp = NULL;
     //函数返回值
     int  ret = 0;
     if (str==NULL||len==NULL)
     {
         printf ( "function sort error" );
         return  ret;
     }
     for ( int  i=0;i<*count;i++)
     {
         for ( int  j=i+1;j<*count;j++)
         {
             if ( strcmp (str[i],str[j])>0)
             {
                 tmp = str[i];
                 str[i] = str[j];
                 str[j] = tmp;
             }
         }
     }
     return  ret;
}
 
int  main( void )
{
     char  *array[] = { "sdjd" , "asdjf" , "peroa" , "asoeq" };
     int  count =  sizeof (array)/ sizeof (array[0]);
     //排序前打印
     print_array(array, &count);
     //调用排序函数
     sort(array, &count);
     //排序后打印
     printf ( "排序后打印\n" );
     print_array(array, &count);
     return  0;
}

执行结果

1
2
3
4
5
6
7
8
9
sdjd
asdjf
peroa
asoeq
排序后打印
asdjf
asoeq
peroa
sdjd

在这里需要补充的一点是,C语言中没有字符串这种数据类型,字符串在C语言中使用字符数组来表示。

注意点:①在调用函数的时候,使用了二级指针接受main函数中的指针数组,同时可以看到在传递数组个数的时候仍然使用了实参传递地址,形参使用指针来接受参数的这种形式,这是指针在做函数参数的时候非常重要的应用。

在函数传递参数的时候,通过指针给主调函数中的变量间接复制,是指针存在最大的意义。例如想通过在被调函数中改变主调函数中变量的值,则可以在函数调用的时候将变量的地址传递过去,在被调函数中使用一级指针来接受,这样在被调函数中就可以通过指针来间接改变变量的值;同理,如果在被调函数中要改变主调函数中一级指针的值,则可以在被调函数中通过二级指针接受一级指针的地址,从而实现在被调函数中改变一级指针的值。同时这样做还有一个好处就是,因为被调函数只能有一个返回值,当想返回多个值的时候就很难实现,但是通过上面的那种方式,即在被调函数中通过地址来间接改变变量(指针也是一种变量)值的方式,就可以在被调函数中改变多个变量的值。而只是让函数返回函数执行状态的值,这样就可以判断出现了哪种情况。


注意上面这段代码(1代码)和之前的数组指针练习题代码(2代码)的比较

①在计算数组元素个数的时候,2代码如果也使用和1代码相同的方式求元素个数,得到的结果永远都是10,但实际元素个数并不是10,出现这种现象的原因是因为2代码中固定了数组元素的个数,但是1代码中并没有固定。

②在排序交换数组元素的时候,使用的方式也不相同,1代码使用的是交换指针指向的方法,也就是说,指针中存储的数组的地址改变了,指针的指向变了;2代码使用的是交换交换数组的值,也就是指针指向不变,但是指针所指向的内存空间中的值变了。


六、三级指针形参接受二级指针地址进行相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//排序
int  sort( char  ***p, int  *len)
{
     char  *tmp = NULL;
     int  ret = 0;
     if (p==NULL||len==NULL)
     {
         printf ( "function sort error" );
         ret = -1;
     }
     
     for ( int  i=0;i<*len;i++)
     {
         for ( int  j=i+1;j<*len;j++)
         {
             if ((*p)[i]<(*p)[j])
             {
                 tmp = (*p)[i];
                 (*p)[i] = (*p)[j];
                 (*p)[j] = tmp;
             }
         }
     }
     return  ret;
}
//给二级指针分配内存
int  getMem( char  ***p, int  count)
{
     int  ret = 0;
     if (p==NULL)
     {
         printf ( "function getMem error" );
         ret = -1;
     }
     *p = ( char  **) malloc ( sizeof ( char *)*(count));
     for ( int  i=0;i<count;i++)
     {
         (*p)[i] = ( char  *) malloc ( sizeof ( char )*100);
         sprintf ((*p)[i], "%d%d%d" ,i,i,i);
     }
     return  ret;
}
//打印数组
void  printArray( char  ***p, int  count)
{
     for ( int  i=0;i<count;i++)
     {
         printf ( "%s\n" ,(*p)[i]);
     }
}
//释放内存空间
int  freePoint( char  ***p, int  count)
{
     if (p==NULL)
     {
         return  -1;
     }
     for ( int  i=0;i<count;i++)
     {
         free ((*p)[i]);
     }
     free (*p);
     return  0;
}
 
int  main( void )
{
     char  **p = NULL;
     int  count = 5;
     //分配内存
     getMem(&p, count);
     //打印数组
     printArray(&p, count);
     //排序
     sort(&p, &count);
     //打印数组
     printArray(&p, count);
     //释放内存
     freePoint(&p, count);
     return  0;
}

如上图所示的代码,在main方法中声明了一个二级指针,然后先讲二级指针的地址做实参传递给甘薯getMem,在函数getMem中使用三级指针做形参接受二级指针,在函数中分配相应的内存空间,然后赋值。

代码注意点①在分配内存的时候,判断的是三级指针p是否为NULL,而不是二级指针

②在(*p)[i]的时候注意符号的优先级,上面的代码如果改成*p[i]就会出错,因为[]的优先级大于*,所以这样写会造成越界。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值