C语言定义的数据类型/声明内容的理解(int *p[]与int (*p)[])

前言

本文主要涵盖了以下三部分的内容:

  • C语言中的运算优先级.

  • C语言中的声明优先级

  • C语言下怎么理解声明内容.

本文是一些我自己关于C语言定义的数据类型的理解,希望能解开C语言初学者对于一些较复杂的数据类型(如int *p[] , int (*p)[])等内容的一些疑惑。

一、了解C语言的运算符优先级

如果一个表达式中的两个操作数具有相同的优先级,那么它们的结合律(associativity)决定它们的组合方式是从左到右或是从右到左。

优先级

运算符

结合律

1

后缀运算符:[] () · -> ++ --(类型名称){列表}

从左到右

2

一元运算符:++ -- ! ~ + - (正负号) * & sizeof_Alignof

从右到左

3

类型转换运算符:(类型名称)

从右到左

4

乘除法运算符:* / %

从左到右

5

加减法运算符:+ -

从左到右

6

移位运算符:<< >>

从左到右

7

关系运算符:<<= >>=

从左到右

8

相等运算符:== !=

从左到右

9

位运算符 AND:&

从左到右

10

位运算符 XOR:^

从左到右

11

位运算符 OR:|

从左到右

12

逻辑运算符 AND:&&

从左到右

13

逻辑运算符 OR:||

从左到右

14

条件运算符:?:

从右到左

15

赋值运算符: = += -= *= /= %= &= ^= |=

从右到左

16

逗号运算符:,

从左到右

表1运算符优先级和结合律

由上表我们可以看到,在运算符优先级中,后缀运算符 () , [] 优先级最高的,超过一元运算符 *

可以以此猜测并记忆C语言中的声明优先级规则。

补充:由于优先级运算符从左到右的结合律较多,我们可以着重记忆结合律为从右到左的相应运算符。

二、C语言中的声明优先级规则

  1. 首先 声明从它的名字开始读取,然后按照优先级顺序依次读取

  1. 优先级顺序由高到低依次是:

  • 声明中被括号括起的内容。例如:int *p[2] 与 int (*p)[2]

  • () 与 [] 。例如:int *fun(int x, int y) 与 int *a[2];

()表示这是一个函数

[]表示这是一个数组

  • 指针 *(* 表示"指向'读取到的下一部分'的指针")。例如:

定义

含义理解(方法一)

含义理解(方法二)

int *p

p为指向一个整形数据(int)类型的指针变量

p为指针变量,它指向一个整型数据

int **p

p为指向一个整形数据(int)类型指针的指针变量

p为指针变量,它指向一个整型数据类型的指针变量

  • const/volatie关键字:

const:如果const后面紧跟类型说明符(int\short\long等)时,const作用于类型说明符

若const后紧跟类型说明符(类型说明符在const前面)时,const作用于它左边部分

接下来我们以“指向常量的指针 与 指针类型的常量“来理解上述内容。

我们先来看“指向常量的指针/常量指针”。

int const *a;            //指向常量的指针
const int *a;            //指向常量的指针

第一行、第二行声明意义相同,都把a声明为指向const int的指针(即指向整型常量的指针)。

指针a指向的变量被const限定,因此不能在后续操作中间接(通过指针修改)修改它的值。但是a并没有被限定,故而a本身的值是可以被修改的,即a所指向的地址是可以修改的。

// test1 -- 指向常量的指针
# include <stdio.h>
int main(void)
{
    const int *p;
    const int a = 2;
    int b = 1;
    p = &a;
    printf("This is a pointer that points to a changliang. val_a:%d\n",*p);

    p = &b;        //p本身的值可以修改
    printf("This is a pointer that points to a changliang. val_b:%d",*p);

    
    b = 10;
    printf("This is a pointer that points to a changliang. val_b:%d",*p);

 //   *p = 10;      //不能通过指针间接修改b的值
 //   a = 10;       //a被const修饰,为常量,不能改变值
 //   b = 10;       //b没被const修饰,为变量,可以改变值
    return 0;
}
/* **********运行结果********** */
/*

This is a pointer that points to a changliang. val_a:2
This is a pointer that points to a changliang. val_b:1
This is a pointer that points to a changliang. val_b:10

*/

在上面的例子中,指针p开始时赋予指向一个整型常量(即a),后来指向整型变量(即b)。

整型常量a的值是无法修改的,但p 的值却可以修改,整型变量b的值也可以修改。

再看“指针类型的常量/指针常量”。

int *const a;        //a是常量指针

此时a被声明为指向int(即整型变量)的常量指针。指针a被const所限制,指向的地址赋值后便不能改变。但指向的变量(若为const常量会产生错误)是通过直接或间接方法改变的。

// test2 -- 指针常量
# include <stdio.h>
int main(void)
{
    const int a = 2;
    int b = 1;
//  int *const p = &a;      //initializing 'int *const' with an expression of type 'const int *' discards qualifiers(类型不匹配)
    int *const p = &b;
//  p = &a;   p = &b;       //不能修改p指向的地址
    printf("This is a pointer that is a changliang. val_pb1:%d, val_p:%p\n",*p,p);

    *p = 0;                 //p所指向的值可通过间接方式(*p)修改
    printf("This is a pointer that points to a changliang. val_pb2:%d, val_p:%p\n",*p,p);

    b = 10;                 //p所指向的值可通过直接方式(直接给指向的变量赋值)修改
    printf("This is a pointer that points to a changliang. val_pb3:%d, val_p:%p",*p,p);

    return 0;
}

/* **********运行结果********** */
/*

This is a pointer that is a changliang. val_pb1:1, val_p:0x7fff3da12f94
This is a pointer that points to a changliang. val_pb2:0, val_p:0x7fff3da12f94
This is a pointer that points to a changliang. val_pb3:10, val_p:0x7fff3da12f94

(我们可以看到p指向的地址一直不变)
*/

volatile关键字声明情况与const类似。

最后我们再看一个例子加以理解:

char *const *(*funp)();

按照我们所提的声明优先级规则分步解决:

  1. 首先,声明中被括号括起的部分优先级最高。故先看(*funp),所以funp是一个指针。

  1. 其次,(),[ ]优先考虑。所以funp是一个指向函数的指针。

  1. 再处理 "*" 前缀与const。所以funp是一个函数指针,指向一个返回一个字符类型的指针常量的*的函数(即该funp指向的函数返回一个字符变量的地址的地址,该字符变量的地址为常量)。

三、C语言定义的数据类型(声明内容)理解

通过文章的上述内容,我们对于C语言中的声明优先级规则有了一些了解。接下来我们通过几个例子(int *p, int *p[], int (*p)[]来进一步理解声明内容。

首先,我们知道 ' * ' 作为运算符作用是内容,需要地址' & '地址,需要内容。则我们定义一个变量中有' * ',则它的前一个环节需要它一个地址。这样说明似乎不太好理解,我们后面用例子使其直观体现。

例一:

int *p;            //p是一个指针,指向int类型的变量

我们看到,定义的p前有' * ',所以p是一个指向int的指针;又根据' * ' 作为运算符作用是内容,需要地址,所以我们赋给p的值应该是(&)地址。综合起来就是,我们需要给p一个int类型变量的地址。

# include <stdio.h>
void swap(int * u, int * v)
{  
    int temp;
    temp = *u; 
    *u = *v; 
    *v = temp;
}

int main(void)
{    
    int x = 5,y = 10;                      //我们定义一个整型的变量
    double a = 1.234, b = 2.345;           //a, b变量类型不为int,测试是否需要int类型的地址
    printf( "before : x=%d,y=%d\n",x,y );
    swap(&x, &y);                          //int *u = &x, int *v = &y
//  swap(&a, &b);                          // 出现报错
//   warning: incompatible pointer types passing 'double *' to parameter of type 'int *' [-Wincompatible-pointer-types]
    printf( "after : x=%d,y=%d\n",x,y );

    return 0;
}

/* **********运行结果********** */
/*

before : x=5,y=10
after : x=10,y=5

*/

int **p 与 int *p思路相同。p为指针变量,它指向一个指向整型数据的指针变量(即它取两次地址后得到一个整型数据,倒过来思考,也等价于我们需要给它一个整型变量地址的地址)


例二:

int *p[n];                //n个指向整型数据的指针变量组成的指针数组p

依据上文所提到的优先级,我们可以知道p为一个数组,且该数组中的n个量是指向整型数据的n个指针。又根据' * ' 作为运算符作用是内容,需要地址,所以我们赋给p的值应该是一个储存着n个int类型变量地址的数组

//      *********        To test int *p[]         *********      //
# include <stdio.h>
int test_fun(int *p[], int n, int m)            // 输出矩阵形式,其中n为行数,m为列数
{
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < m; j++)
        {
            printf("%d ",*(*(p + i) + j));
        }
            printf("\n");
    }
    return 0;
}
int main(void)
{    
    int num1[3] = {1, 2, 3},
        num2[3] = {4, 5, 6}, 
        num3[3] = {7, 8, 9};
    int *p[3] = {num1,num2,num3};           
    // num1,num2,num3是三个数组,直接使用等价于给出&num1[0],&num2[0],num3[0];
    test_fun(p, 3, 3);

    return 0;
}

/* **********运行结果********** */
/*

1 2 3 
4 5 6 
7 8 9 

*/

由上述例子我们可以发现,(p[] == *p),int *p[] 等价于 int **p. 倒过来思考,我们也可以直接赋予它一个整型变量地址的地址

int x = 0;
int *p = &x;
void try[int *p[]];           
int main(void)
{
    try(&p); //放到VS中可以发现没报错

    return 0;
}   

注意:

指针数组元素的作用相当于二维数组的行名,但指针数组中元素是指针变量,二维数组的行名是地址常量.


例三:

int (*p)[n];       //p为指向含n个元素的一维整型数组的指针变量

同理,我们按照优先级分析该声明。不难看出,p为一个指针变量,指向一个整型数组。倒过来想,我们需要赋给它一个整型数组的地址。又*p == p[] , int (*p)[] 与 int p[][]等价。

# include <stdio.h>
int main(void)
{   
      static int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
      int  i, j, (*p)[4];
      for( p=a, i=0; i<3; i++,p++)
      {
         for(j=0; j<4; j++)
              printf("%d ",*(*p+j));  //等价于 p[0][j]
         printf("\n");
      }
}

图示:


练习

最后,我们将通过两个简单的练习,来巩固上面所提到的知识。

例1

试理解以下语句:

const int a;
int const a;
const int * a;
int * const a;
例2

试理解以下语句:

(1)int  *p[3];
(2)int   (*p)[3];
(3)int  *p(int);
(4)int  (*p)(int);
(5)int  *(*p)(int);
(6)int (*p[3])(int);
(7)int  *(*p[3])(int);
(8)int  (*p[2])[3];

参考答案:

例一:

1.  const int a;等价于int const a,都是一个整型常量的声明方式。
2.  const int * a;表示一个指向整型常量的指针。
3.  int * const a;表示一个指向整型变量的常量指针。

例二:

1. 指针数组
2. 指向一维数组的指针
3. 返回指针的函数
4. 指向函数的指针,函数返回int型变量
5. 指向函数的指针,函数返回int型指针
6. 函数指针数组,函数返回int型变量
7. 函数指针数组,函数返回int型指针
8. 指针数组,每个元素指向一维数组

以上就是对C语言定义的数据类型/声明内容(int *p[]与int (*p)[])的理解,文章中出现错误或者小伙伴对以上内容有所疑问,欢迎大家在评论区留言。


参考资料

[1] 《C Primer Plus》. [美] Stephen Prata 著.

[2] 博客: 指针常量和常量指针_qq_36132127的博客-CSDN博客_指针常量和常量指针

[3] 博客: C语言中const的详细用法及声明规则_RMSnow的博客-CSDN博客

[4] 博客: C语言中的声明优先级规则_hanhanLiao的博客-CSDN博客_c语言 声明优先级

[5] 博客: http://c.biancheng.net/view/285.html

  • 12
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值