目录
前言
这期我们将继续讲C语言的操作符,正文开始。
一、上期结束问题:
我们还是利用上一章n&(n-1)的思想
int main()
{
int n = 0;
scanf("%d", &n);
if ((n & (n - 1)) == 0)
printf("%d是2的n次方", n);
else
printf("%d不是2的n次方", n);
return 0;
}
运行看看:
why?
2的n次方有什么规律?
10
100
1000
10000
.......
只有一个一,那我们去掉一个1不就是0了吗,那这个代码就不难看懂了;
二、单⽬操作符
单⽬操作符有这些:
!、++、--、&、*、+、-、~ 、sizeof、(类型)
单⽬操作符的特点是只有⼀个操作数,在单⽬操作符中只有&和*没有介绍,这2个操作符,我们放在学习指针的时候学习。
三、逗号表达式
语法:
exp1, exp2, exp3, …expN
逗号表达式,就是⽤逗号隔开的多个表达式。
计算规则:逗号表达式,从左向右依次执⾏。整个表达式的结果是最后⼀个表达式的结果。
话不多说,上代码:
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("%d\n", c);
return 0;
}
想想再看结果,不要直接看:
刚刚讲了,逗号表达式虽然结果是最后一个表达式,但是前面的表达式要执行,这里,前面先执行了a = b + 10=12,所以结果是13;
如何使用:
看一段伪代码:
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
如果使⽤逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
有效的减少了代码冗余;
四、下标访问[]、函数调⽤()
1. [ ] 下标引⽤操作符
这个我们已经很熟悉了,在这我们不做过多讲解:
操作数:⼀个数组名 + ⼀个索引值
int arr[10];//创建数组
arr[9] = 10;//实⽤下标引⽤操作符。
[ ]的两个操作数是arr和9。
2. 函数调用操作符
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //这⾥的()就是作为函数调⽤操作符。
test2("hello bit.");//这⾥的()就是函数调⽤操作符。
return 0;
}
五、结构成员访问操作符
1. 结构体
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类
型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:
标量、数组、指针,甚⾄是其他结构体。
1.1 结构的声明
struct tag
{
member-list;
}variable-list;
例如:描述⼀个学⽣:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
1.2 结构体变量的定义和初始化
定义:
有多种方法进行定义:
- 我们可以在创建结构体的时候进行定义,这时定义的就是全局变量;
- 还可以在main函数里进行定义。
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
初始化:
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
1.3 结构体嵌套初始化
struct S
{
char c;
int n;
};
struct B
{
struct S s;
int* p;
char arr[10];
float sc;
};
int main()
{
struct B b = { {'w',99},NULL,"hehe", 32.2f };
//这里的f是让编译器知道这是个float类型的数,而不是double类型
return 0;
}
2.结构成员访问操作符
2.1 结构体成员的直接访问
使⽤⽅式:结构体变量.成员名
struct Student
{
char name[20];
int age;
int high;
float weight;
char id[16];
}s4, s5, s6;
//S4,S5,S6是结构体变量(全局)
struct Student s7; //全局
int main()
{
struct Student s1 = { "张三", 20,180, 75.1f, "202204071055" };
struct Student s2 = { .age = 30, .name = "wuxs", .weight = 80.5f, .high = 177, .id = "202204071055" };
printf("%s %d %d %.1d %s\n", s1.name, s1.age, s1.high, s1.weight, s1.id);
return 0;
}
运行结果:
如果是嵌套的呢:
struct S
{
char c;
int n;
};
struct B
{
struct S s;
int* p;
char arr[10];
float sc;
};
int main()
{
struct B b = { {'w',99},NULL,"hehe", 32.2f };
//这里的f是让编译器知道这是个float类型的数,而不是double类型
printf("%d\n", b.s.n);
return 0;
}
思路也是一样的:
更多关于结构体的知识,后期在《⾃定义类型:结构体》中讲解。
六、操作符的属性:优先级、结合性
C语⾔的操作符有2个重要的属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
1.优先级
优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
3 + 4 * 5;
上⾯⽰例中,表达式 3 + 4 * 5 ⾥⾯既有加法运算符( + ),⼜有乘法运算符( * )。由于乘法
的优先级⾼于加法,所以会先计算 4 * 5 ,⽽不是先计算 3 + 4 。
我们如果想要知道更多的优先级:
官文:
C 运算符优先级 - cppreference.comhttps://zh.cppreference.com/w/c/language/operator_precedence
2.结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。
5 * 6 / 2;
上⾯⽰例中, * 和 / 的优先级相同,它们都是左结合运算符,所以从左到右执⾏,先计算 5 * 6 ,
再计算 6 / 2 。
运算符的优先级顺序很多,下⾯是部分运算符的优先级顺序(按照优先级从⾼到低排列),建议⼤概记住这些操作符的优先级就⾏,其他操作符在使⽤的时候查看上面表格就可以了。
新手应该记得的:
• 圆括号( () )
• ⾃增运算符( ++ ),⾃减运算符( -- )
• 单⽬运算符( + 和 - )
• 乘法( * ),除法( / )
• 加法( + ),减法( - )
• 关系运算符( < 、 > 等)
• 赋值运算符( = )
由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。
七、表达式求值
1.整型提升
C语⾔中整型算术运算总是⾄少以缺省整型类型的精度来进⾏的。为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。
因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓度。
通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为int或unsigned int,然后才能送⼊CPU去执⾏运算。
示例:
//实例1
char a,b,c;
...
a = b + c;
b和c的值被提升为普通整型,然后再执⾏加法运算。
加法运算完成之后,结果将被截断,然后再存储于a中。
如何进⾏整体提升呢?
1. 有符号整数提升是按照变量的数据类型的符号位来提升的 (常见的编译器:char = signed char)
2. ⽆符号整数提升,⾼位补0 (unsigned char)
我们来看看这个具体例子:
//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0
我们具体来看一个程序:
int main()
{
char a = 3; //char -- signed char
//00000000000000000000000000000011
//00000011 -> a
char b = 127;
//00000000000000000000000001111111
//01111111 -> b
char c = a + b;
//00000000000000000000000000000011 计算机中都是用补码计算的,正数的补码与原码一致
//00000000000000000000000001111111
//00000000000000000000000010000010
//但是c只有一个字节,那就将多余的舍去:
//10000010 char -- signed char
//c提升后:
//11111111111111111111111110000010
//10000000000000000000000001111110 原码
printf("%d\n", c);
return 0;
}
我们来看看结果对不对:
如我们上面分析的所料;
2.算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就⽆法进⾏。下⾯的层次体系称为寻常算术转换。
long double //从上到下排名依次下降
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算。
如果一个int类型与long double 进行运算,会将int类型转化为long double类型;
3.问题表达式解析
3.1 表达式1
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
表达式1在计算的时候,由于 * ⽐ + 的优先级⾼,只能保证, * 的计算是⽐ + 早,但是优先级并不
能决定第三个 * ⽐第⼀个 + 早执⾏。
所以表达式的计算机顺序就可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或:
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
也就是说我们写的代码可能会不按照我们预想的思路走下去,这是非常危险的,有人可能会说,不管怎么算结果不都一样吗,真的是这样吗?如果这三个乘法换成三个表达式,而表达式间还有相同的变量,那结果还一样吗?
所以,这个代码是有问题的:那怎么解决?
加括号或分步写出
3.2 表达式2
//表达式2
c + --c;
同上,操作符的优先级只能决定⾃减 -- 的运算在 + 的运算的前⾯,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
怎么解决呢?遇到这种表达式,我们就要将他进行拆分,不要想着一步登天。
3.3 表达式3
//表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
这个代码其实来自一本书--《c和指针》
表达式3在不同编译器中测试结果:⾮法表达式程序的结果
这里因为我们还没有讲到指针,所以我们不做讲解,等将指针学完我们在来回来看看这个代码;
3.4 表达式4
#include <sdtio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
这个代码有没有实际的问题?有问题!
虽然在⼤多数的编译器上求得结果都是相同的。
但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先
算乘法,再算减法。
函数的调⽤先后顺序⽆法通过操作符的优先级确定
我们可以看看VS2022上的运行结果是什么:
有结果可知,vs2022的运算顺序与结果如上图;
3.5 表达式5
//表达式5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
//尝试在linux 环境gcc编译器,VS2013环境下都执⾏,看结果。
我们先来看看在VS2022上算的结果:
所以我们应该就能猜到vs2022是 先将所的++i算出,得4,在将其加到一起的
我们可以来看看:
我们可以看到,编译器连续三次执行了自增的操作;这里看不懂的可以去看看小编往篇写的函数栈帧:
这个结果在不同编译器上不一致,大家可以去试试。
总结
这期我们将C语言中的操作符进行了总体汇总理解,我们还明白了:即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式就是存在潜在⻛险的,建议不要写出特别负责的表达式。
下期见!