操作符的分类:
算数运算符 |
移位操作符 |
位操作符 |
赋值操作符 |
单目操作符 |
关系操作符 |
逻辑操作符 |
条件操作符 |
逗号表达式 |
下标引用、函数调用和结构成员 |
1.算数操作符
不做过多介绍,无非就是加减乘除,外加一个求余(+ - * / %),不过这里要提几个注意事项:
1.除了求余操作符(%)之外,其他的几个操作符都阔以作用于整数和浮点数。
2.对于操作符“/”如果两个操作数都是整数的话,就执行整数除法。但是捏,但凡其中有一个操作数是浮点数,那就执行浮点数除法。(举个栗子)
eg1:
#include<stdio.h>
int main()
{
float num = 0.0;
num = 10 / 3;
printf("num = %f", num);
return 0;
}
eg2:
#include<stdio.h>
int main()
{
float num = 0.0;
num = 10.0 / 3;
printf("num = %f", num);
return 0;
}
3.“%”操作符的两个操作数必须是整数啊喂(返回的是整除后的余数)
2.移位操作符(<<、>>)
小提一嘴,移位操作符的操作数只能说整数昂~
所以在这里不得不提一句整数的二进制表示形式。
整数的二进制表示形式有三种:
原码
反码
补码
整数在计算机中存储的方式为二进制的补码,其中正整数的原码,反码,补码都是一样的,其中负整数的反码就是原码除符号位不变,其他位都改变。其中补码有负整数的反码加1所得到。下面举一个例子
-3的二进制原码表示为:
1000 0000 0000 0000 0000 0000 0000 0011
-3的二进制反码表示为:
1111 1111 1111 1111 1111 1111 1111 1100
-3的二进制补码表示为:
1111 1111 1111 1111 1111 1111 1111 1101
把话题扯回来,这里的移位操作符操作的就是整数的补码
2.1左移操作符<<
关于左操作数移位的规则。左边抛弃,右边补0
eg:
2.2右移操作符>>
右移操作符的移位规则有两种运算方式:
1.逻辑移位:左边雨0填充,右边丢弃。
2.算术移位:左边用原符号位填充,右边丢弃。
eg:
由于标准未定义移动负数位,所以对于移位运算符来说,不要移动负数位
eg:
int num = 10;
num >> -1//error
3.位操作符
位操作符有以下三种:
1.“&”按位与
2.“|”按位或
3.“^”按位异或(这个腻害了,居然支持交换律)
同样他们的操作数必须是整数昂~
下面做个“小小的练习”
题:在不创建临时变量的情况下,实现两个整数的交换。
大家阔以看到,其实交换整数并不难,难的是“不创建临时变量”,这怎么做呢?这里位操作符“^”就可以帮上忙了。
答案如下:
int main()
{
int a = 10;
int b = 20;
printf("交换前\n");
printf("a = %d,b = %d\n", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后\n");
printf("a = %d,b = %d\n", a, b);
return 0;
}
运行结果
做个正式的练习:
求一个整数存储内存中的二进制中1的个数。
.................................................................................................................................................................................................................................
先做个错误示范:
#include <stdio.h>
int main()
{
int num = 10;
int count = 0;//计数
while (num)
{
if (num % 2 == 1)
count++;
num = num / 2;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
以10为例:运行结果:
没毛病对吧,但要是以-10为例呢?
我们先看看运行结果:
这就明显有问题呀,-10在存储二进制的补码为:
11111111 11111111 11111111 11110110
明显不是0呀,为什么呢?负整数求余结果是负的呀
正确做法
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for (i = 0; i < 32; i++)
{
if (num & (1 << i))
count++;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
运行结果
这种方法都是我们可以想得到的,但是,有没有更好的方法呢?答案是有的。
不过在此之前要先跟大家分享一个小芝士:
某个数每次和比自己小1的数做一次&运算,其对应的二进制数会少一个1,直到最后变成0。
这是为什么捏?咱阔以先验证一下。
以int num = 10;为例
此时就不难发现,每次num&(num-1)之后最后面的1被0代替掉了,所以每次之后二进制中1的数量都减少一个。好了,问题又扯回来,咱来看看最优解:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while (num)
{
count++;
num = num & (num - 1);
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
当二进制中每少一个1,count就++一下用来记录一共消掉多少个1,直到num为0时count的大小便是该整数存储二进制中1的个数咯~
4.赋值操作符
赋值操作符就不讲了,直接上段代码自己感受一下吧:
#include <stdio.h>
int main()
{
int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。
return 0;
}
这里稍微提一嘴,大家要养成好的代码风格,尽量让自己的代码通俗易懂,不要折磨别人,也折磨自己,比如看下面这段代码:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值
同样的语义再看看这段:
x = y+1;
a = x;
赋值运算符
+= |
-= |
*= |
/= |
%= |
>>= |
<<= |
&= |
|= |
^= |
这些运算符都可写成复合的效果,举个栗子:
int x = 10;
x = x+10;
x += 10;//复合赋值
5.单目操作符
5.1单目操作符介绍
#include <stdio.h>
int main()
{
int a = -10;
int* p = NULL;
printf("%d\n", !2);
printf("%d\n", !0);
a = -a;
p = &a;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
//printf("%d\n", sizeof a);//这样写行不行?
printf("%d\n", sizeof int);//这样写行不行?
return 0;
}
我们看看这种情况的编译结果:
由此得知:
1.这种方法是错误的,不能这么写。
5.2 sizeof与数组
我们先来看一个小问题,请看下列代码:
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = {0};
char ch[10] = {0};
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
好的,让我们接下来思考两个问题:
1.(1)、(2)两个地方分别输出什么?
2.(3)、(4)两个地方分别输出什么?
好的,接下来答案揭晓:
这是为什么呢?
sizeof(数组名)时,数组名表示整个数组。
数组名传参时,数组名表示首元素地址。
这里提一嘴前置++前置--与后置++后置--:
前置++(--)先将自身加一(减一)再参与运算;
后置++(--)先参与运算再自身加一(减一)。
6.关系操作符
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
7.逻辑操作符
逻辑操作符有哪些:
接下来思考一下程序输出来的结果
#include <stdio.h>
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
//i = a++ && ++b && d++;
i = a++||++b||d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
好的,接下来我们看一看结果:
由此我们可以得知逻辑与和逻辑或的特点:
1.当编译器遇到逻辑与(&&)时,若&&左边为假,则右边不再运算
2.当编译器遇到逻辑或(||)时,若||左边为真,则右边也不再计算
9.条件操作符
条件操作符表达式很简单,如下所示:
exp1:exp2?exp3
下面做个练习:
if (a > 5)
b = 3;
else
b = -3;
转换成条件表达式,是什么样?答案揭晓。
b = a > 5? 3 : -3
再做一个小练习
使用表达式实现找两个数中较大值。好的,下面直接给出答案。
max = a > b ? a : b;
10.逗号表达式
exp1, exp2, exp3, ...expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
让我们看看结果。
显然,逗号表达式就是从左运算到右,最后的结果取最后一个表达式的值。
所以我们可以得出结论,逗号表达式从从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
if (a =b + 1, c=a / 2, d > 0)
逗号表达式还可以简化代码,比如说:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
10.下标引用、函数调用和结构成员
10.1下标引用就不多说了,数组里面经常要用到的
10.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;
}
我们来看一下结果:
10.3访问一个结构体的成员
. 结构体.成员名
-> 结构体指针->成员名
举个栗子:
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 20;//结构成员访问
set_age1(stu);
pStu->age = 20;//结构成员访问
set_age2(pStu);
return 0;
}
11.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有一些表达式的操作数在求值的过程中可能需要转换为其他类型。
11.1隐式子类型转换
C的整形算术运算总是至少以缺省整型类型的精度来进行
11.2算数类型转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
但是算术转换要合理,要不然会有一些潜在的问题
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
12.3 操作符的属性
复杂表达式的求值有三个影响的因素。
1.操作符的优先级
1.操作符的优先级..
2.操作符的结合性
3.是否控制求值顺序
函数的调用先后顺序无法通过操作符的优先级确定。