前言
本文主要涵盖了以下三部分的内容:
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语言中的声明优先级规则
首先 声明从它的名字开始读取,然后按照优先级顺序依次读取
优先级顺序由高到低依次是:
声明中被括号括起的内容。例如: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)();
按照我们所提的声明优先级规则分步解决:
首先,声明中被括号括起的部分优先级最高。故先看(*funp),所以funp是一个指针。
其次,(),[ ]优先考虑。所以funp是一个指向函数的指针。
再处理 "*" 前缀与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博客