在阅读本篇文章前,建议您先阅读一下专栏中前面的文章。
目录
前言
本篇文章接着前一篇文章继续介绍关于操作符的知识。
一、结构成员访问操作符
在介绍这种操作符之前,我们需要先了解一下什么是结构体。我们在C语言中已经提供了很多的内置类型,比如说char、short、int、long、float、double等等,但有的时候,只有这些内置类型是完全不够用的。比如说我如果想描述一个学生或一本书,他们的特征类型都是不唯一的,那我们就没办法用一个单一的类型进行描述。为了解决上面这种问题,C语言中增加了结构体这种自定义的数据类型,来让程序员可以自己创造适合的类型。这个结构本质上是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如标量、数组、指针,甚至是其他结构体。
我们通常采取这种语法来声明结构体:
cpp
struct tag
{
member-list;
}variable-list;
如果说我们想要去描述一个学生,我们就会这样去描述:
cpp
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
同样,结构体作为变量也是需要定义和初始化的。我们首先可以看一下它的定义方式:
cpp
//方式一:在定义结构体的同时定义变量
struct Point
{
int x;
int y;
}p1;
//方式二:先定义结构体类型,后面再定义变量
//需要注意的是struct和Point都不能省略
struct Point p2;
int main() {
//p1、p2均为全局变量,p3、p4、p5为局部变量
struct Point p3;
struct Point p4;
struct Point p5;
return 0;
}
其初始化变量的方式则如下所示:
cpp
//定义结构体的时候同时初始化变量
struct Point p6 = { 10, 20 };
struct Point p7 = { .y = 30, .x = 20 };//指定成员初始化
cpp
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
上面这个代码涉及到了指针的知识,关于指针具体的部分我们会在下篇文章着重进行详解。对于目前来说,我们只是想通过这个代码来展示结构体的嵌套。
那么在初步了解了结构体之后,我们就可以介绍一下结构成员访问操作符了,我们首先来介绍一下结构体成员的直接访问方式,这种方式是通过点操作符 . 来实现的,它能够接受两个操作数。我们键入如下代码来测试一下:
cpp
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
其运行结果如下:

但并非所有时候我们得到的都是一个结构体变量,而是通过一个指向结构体的指针来间接访问,我们可以键入如下的代码来试验一下:
cpp
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
其结果如下:

可以看到我们是间接改变了结构体内变量的取值。
我们借用下面这个代码来综合举例进行演示:
cpp
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
其运行结果如下:

那么到这,我们对于结构体的讲解暂时就这样了,之后我们会详细讲解关于它的内容。
二、操作符的优先级和结合性
C语言中的操作符有2个重要的属性,也就是优先级和结合性,这两个属性同时也决定了表达式求值的计算顺序。
优先级指的是如果一个表达式包含多个运算符,哪个运算符应该优先去执行,各种运算符的优先性是不一样的。而如果两个运算符优先级相同,优先级没办法决定先计算哪个,这时候就要看看结合性了,根据运算符是左结合还是右结合决定我们执行的顺序。但是大部分运算符都是左结合,少数是右结合(比如=)。我们把操作符的优先级大致列在下面:

或者可以访问下面的参考链接:C 运算符优先级 - cppreference.com
三、表达式求值
首先我们来研究一下关于整型提升的问题,在C语言中,整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换被称为整型提升。
那么它究竟有什么意义呢?表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。 通用CPU是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。在运算完成之后,结果就会被截断,然后再存储在变量中。
那么是如何实现整型提升的呢?这就分为有符号整数和无符号整数了,有符号整数提升是按照变量的数据类型的符号来提升的,而无符号整数提升时高位直接补0。
cpp
//负数的整型提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个⽐特位:
1111111
因为char为有符号的char
所以整型提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整型提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个⽐特位:
00000001
因为char为有符号的char
所以整型提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整型提升,高位补0
我们可以举一个这样的例子:
cpp
#include <stdio.h>
int main()
{
//整型提升
char a = 3;
//00000011
char b = 127;
//01111111
char c = a + b;
printf("%d\n", c);
return 0;
}
你觉得会输出什么呢?会不会是130呢?我们运行代码试验一下:

嗯?怎么结果是这样的?那么这个例子就很好的为我们演示出了整型提升的效果!我们来分析一下这个代码运行的思路:
cpp
//整型提升
char a = 3;
//a = 00000011
char b = 127;
//b = 01111111
char c = a + b;
//c = 100000010
printf("%d\n", c);
//%d打印的是int类型,char类型在参与运算时会提升为int类型
//c提升之后是这样的 11111111 11111111 11111111 10000010
//转成它的原码形式看看
//10000000 00000000 00000000 01111101
//10000000 00000000 00000000 01111110 --这就是-126的原码
那么整型提升到这里就结束了,我们接下来介绍一下算数转换的问题。如果某个运算符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。我们将下面这种层次体系称为寻常算数转换:
cpp
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在这个列表中的排名靠后,那么就要首先转换为另一个操作数的类型后执行运算。
四、问题表达式解析
在学习完上述概念之后,我们可以看一些问题表达式,并尝试进行分析解决。
首先我们看一下这个表达式:
cpp
a*b + c*d + e*f
那么这个表达式执行的顺序应该是什么样子的呢,你可以先想一想。可能出现的运算顺序大致来说应该是以下这两种:
cpp
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
cpp
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
在计算的时候,由于*的优先级是高于+的,但是我们只能保证*的计算是先于+的计算的,但是优先级和结合性没法为我们保证第三个*比第一个+早执行,所以会出现上面这两种可能。如果上面这六个字母均为变量的话,其实影响会稍微小一些;但如果其中几个为表达式,并且对同一个变量进行了改变值的操作,例如自增自减,就很有可能导致整个表达式的运行结果差强人意。所以我们就要避免这种写法,这时你可以选择去加括号,或者把这个复杂的式子拆解成几个简单的。
然后我们看下下面这个表达式:
cpp
c + --c
这个表达式的问题和上面的一样,操作符的优先级只能保证自减操作在加法前运行,但是我们没法得知加法操作符左操作数的获取在右操作数之前还是之后执行,所以这种结果是不可预测,有歧义的。
然后是第三段代码:
cpp
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
你可以先自己试着计算一下结果是什么?

怎么样,和你计算的结果一样吗?这个表达式的结果是受编译器影响的,在不同的编译器下,运行得出的结果是不同的。我们列举一下:

所以这就再次启示我们千万不要把表达式描述得如此复杂!
我们再看看第四段代码:
cpp
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
这段代码有没有实际的问题呢?有大问题! 虽然说在大多数的编译器中我们运行得出的结果是相同的,但是我们上述的代码只能保证先算乘法,后算加法,但是是先算乘法两端的操作数还是加法两端的操作数,我们无法给出保证,这是因为函数的调用先后顺序无法通过操作符的优先级来确定。
好,让我们看看最后一个代码是什么样子的:
cpp
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这个代码的运算结果是什么样子的呢?
在gcc编译器下结果:

在VS 2022下结果:

明明是相同的代码1,却产生了不同的结果,这是因为第一个+在执行的时候,第三个++是否执行是不确定的,仅仅依靠操作符的优先级和结合性是无法确定第一个加号和第三个自增的先后顺序的。
所以通过上面五个问题表达式,我们主要想表达出的意思就是,即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那么这样的话我们的表达式就是具有潜在风险的,所以建议不要写出特别复杂的表达式。
总结
本文介绍了C语言中结构体成员访问操作符的使用方法,包括直接访问的.和指针访问的->操作符。同时讲解了操作符的优先级和结合性对表达式求值顺序的影响,并通过多个问题表达式示例说明复杂表达式可能导致的歧义结果。文章重点强调了整型提升机制和算术转换规则,最后通过不同编译器下的测试结果,指出应避免编写过于复杂的表达式以确保代码的可预测性和可移植性。全文约150字。