目录
1.算数操作符
2.移位操作符
2.1 左移操作符
2.2 右移操作符
3. 位操作符
3.1 一道变态的面试题
3.2 经典面试题(需重视)
4.赋值操作符
5.单目操作符
5.2 sizeof和数组
5.3 ++和--运算符
6.关系操作符
7.逻辑操作符
8.条件操作符(三目操作符)
9.逗号表达式
10.下标引用、函数调用和结构成员
10.1 [ ]:下标引用操作符
10.2 ( ):函数调用操作符
10.3 访问一个结构的成员
11.表达式求值
11.1 隐式类型转换
11.2 算术转换
11.3 操作符的属性
+ - * / %
注:
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
void test1()
{
//6.0 % 2.0; //不是整型,err
int a= 6 / 2;
int b = 7 / 2;
double c = 7 / 2.0;
printf("a = %d b = %d c = %lf", a, b,c);//a=3 b=3 c=3.500000
}
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
移动的是二进制位
移位规则: 左边抛弃,右边补0
void test2()
{
int a = 7;//a是一个int变量,4个字节,32个bit位
int b = a << 1;
printf("b = %d\n", b); //14
}
移位规则:
首先右移运算分两种:
1. 逻辑移位:左边用0填充,右边丢弃
2. 算术移位:左边用原符号位填充,右边丢弃 (原来是正数则补0,反之补1)
补充:整数存放在内存中的时候,放的是二进制,表示形式有三种:
原码、反码、补码
对于-1:
10000000000000000000000000000001 原码
11111111111111111111111111111110 反码(符号位不变,其他位按位取反)
11111111111111111111111111111111 补码(反码 + 1)
对于整数,在计算机内是以补码形式存储的
对于int b = -1;
可见,-1的二进制为11111111111111111111111111111111 => 补码形式
void test3()
{
int a = 1;
int b = -1;
int c = a >> 1;
int d = b >> 1;
//b从11111111111111111111111111111111
//=> 11111111111111111111111111111111
printf("c = %d d = %d\n", c, d); //c = 0,d = -1 说明此时编译器用的是算数右移
}
警告⚠ : 对于移位运算符,不要移动负数位,这个是标准未定义的。 例如:
int num = 10;
num>>-1;//error
位操作符有:
& :按位与:有0结果就是0,全为1时结果为1
| :按位或:有1结果就是1,全为0时结果为0
^ :按位异或:对应的二进制位,相同为0相异为1
注:他们的操作数必须是整数。
void test4()
{
int a = 3;
int b = 5;
int c = a & b; // & - 按(二进制)位与 - 有0结果就是0,全为1时结果为1
//a:011
//b:101
//c:001
int d = a | b; // | - 按(二进制)位或 - 有1结果就是1,全为0时结果为0
//a:011
//b:101
//d:111
int e = a ^ b; // ^ - 按(二进制)位异或 - 对应的二进制位,相同为0相异为1
//a:011
//b:101
//e:110
printf("c = %d d = %d e = %d", c, d, e); //c = 1 d = 7 e = 6
}
不能创建临时变量(第三个变量),实现两个数的交换。
void test5()
{
int a = 3; //011
int b = 5; //101
a = a ^ b; // a = 110
b = a ^ b; // b = 011
a = a ^ b; // a = 101
printf("a = %d b = %d\n", a, b); //a = 5 b =3
}
结论:a^b^a = b,b^a^b = a ,a^a = 0,0^a = a
(a^b^a中: a与a抵消得到b,且结论符合"交换律",即a^a^b = b,两个相同的数字异或结果为000)
编写代码实现:求一个整数存储在内存中的二进制中1的个数
//方法一
int count_one1(int n)//该方法存在缺陷,计算负数会出错
{
int count = 0;
while (n) //只要不是0 二进制序列中就会有1
{
if (n % 2 == 1)
{
count++;
}
n = n / 2;
}
return count;
}
//方法二
//当a与1按位与:
//11111111111111111111111111111111
//00000000000000000000000000000001
//00000000000000000000000000000001
//可见:1的最后一位数字与a的最后一位保持一致
//所以判断一次就右移一次a,每次都与1相与 总共右移32次
int count_one2(int n)
{
int count = 0;
int i = 0;
for (i = 0; i < 32; i++)
{
if ( ( (n >> i) & 1 ) == 1 )
{
count++;
}
}
return count;
}
//方法三(最好的办法)
//n = n & (n-1) 假设n = 7
// 111 & 110 = 110
//再来一次:
//n = n & (n-1)
// 110 100 = 100
//结论:n = n & (n-1) 把n的二进制中从右向左的第一个1变成了0
//方法三的具体步骤为 : 在n = 0之前,不停地用n = n & (n-1),执行了几次,则有多少个1
int count_one3(int n)
{
int count = 0;
while (n != 0)
{
n = n & n - 1;
count++;
}
return count;
}
void test6()
{
int a = -1;
int ret = count_one3(a);
printf("%d\n", ret);
}
总结:
1.a & 1,可以达到的目的是:1的最后一位数字与a的最后一位保持一致,这样配合a的右移操作,可以计算出二进制中存在多少个1
2.n = n & (n-1),该方法把n的二进制中从右向左的第一个1变成了0,配合循环:在n = 0之前,不停地用n = n & (n-1),循环执行了几次,则有多少个1
void test7()
{
int a = 0;
a = 3; //赋值操作符
}
赋值操作符可以连续使用(但不建议使用):
void test8()
{
int a = 10;
int x = 0;
int y = 20;
a = x = y + 1;//连续赋值
printf("a = %d\n", a); //a = 21
}
先执行x = y + 1,再执行 a = x
复合赋值符 :+=、-=、*=、/=、%=、>>=、<<=、&=、|=、^= 这些运算符都可以写成复合的效果。 如:
int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。
练习:已知14的二进制位为:00000000000000000000000000001110
现在让倒数第5位的0变成1,实现后再变成0
思路:想让从右向左第 i 位从0变成1,其余位不变,则让原本的二进制序列和只有倒数第i位是1,其余位全是0的二进制序列相或操作如:
序列a:00000000000000000000000000001110
序列b:00000000000000000000000000010000 序列b由1向左移4 (i-1) 个单位得到
相或: ------------------------------------------------------
序列c:00000000000000000000000000011110
而想要从序列c变回a,即把倒数第5位从1变回0的方法:让序列c与除了倒数第5位为0,其余位全为1的序列d相与操作:
序列c:00000000000000000000000000011110
序列d:1 1 11 1 1 1 1111111111111111111101111
相与: ------------------------------------------------------
序列a:00000000000000000000000000001110
不难看出:序列d正是由序列b按位取反操作得到
所以代码如下:
void test10()
{
int a = 14;
a |= (1 << 4);
printf("%d\n", a); //30
a &= (~(1 << 4));
printf("%d\n", a); //14
}
! 逻辑反操作
- 负值
+ 正值
& 取地址 sizeof 操作数的类型长度(以字节为单位)
~ 对一个数的二进制按位取反
-- 前置、后置--
++ 前置、后置++
* 间接访问操作符(解引用操作符)
(类型) 强制类型转换
void test9()
{
3 + 5; // +有两个操作数 双目操作符
//单目操作符:只有一个操作数的操作符
//!操作符
int a = 10;
printf("a = %d\n", a); //10非0 - 真
printf("!a = %d\n", !a);//0
int flag = 1;
if (!flag)
{
//表示当flag为假的时候执行if语句
}
//&与*操作符 取地址与解引用
int b = 10;
int* pb = &b; //pb为指针变量,b的地址存在pa中
*pb = 20; //解引用操作 通过*pb访问或修改b的值
printf("b = %d\n", b); //20
//sizeof操作符 而不是函数! 还可以计算数组总大小
int c = 10;
int arr[10] = { 0 };
printf("sizeof(c) = %d\n", sizeof(c)); //4
printf("sizeof(int) = %d\n", sizeof(int)); //4
printf("sizeof(arr) = %d\n", sizeof(arr)); //40
printf("sizeof(int[10]) = %d\n", sizeof(int[10]) ); //40
// ~操作符 按位取反
int d = 0;
printf("d = %d\n", ~d); //-1
//00000000000000000000000000000000 //d的补码
//11111111111111111111111111111111 //按位取反后的补码
//11111111111111111111111111111110 //反码 (补码-1)
//10000000000000000000000000000001 //源码 (符号位不变,其他按位取反)
//-1
//(强制类型转换)
double e = 3.14;
printf("e = %d\n", (int)e); // e = 3
}
#include
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)、(2)两个地方分别输出多少? 答:40 10
(3)、(4)两个地方分别输出多少? 答:4 4(注意)
注:传入test2的是ch数组第一个元素的指针,为4或8个字节,不要理解成ch第一个元素的大小1
void test11()
{
int a = 10;
int b = 10;
int c = a++; //后置++:先使用再自增1
int d = ++b; //前置++ 先自增1后使用
printf("c = %d d = %d a = %d b = %d \n", c, d, a, b);//10 11 11 11
/*int e = 10;
int f = e + 1; //1
f = ++e; //带有副作用 - e变成了12*/
int g = 1;
printf("%d\n", g++); // 1
printf("%d\n", ++g); // 3
}
例题:下面代码的结果是多少?
#include
int main()
{
int a, b, c;
a = 5;
c = ++a;
b = ++c, c++, ++a, a++;
b += a++ + c;
printf("a = %d b = %d c = %d\n:", a, b, c);// a = 9 b = 23 c = 8
return 0;
}
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
这些关系运算符比较简单,但是我们要注意一些运算符使用时候的易错点:
1.在编程的过程中== 和=不小心写错,导致的错误
2.字符串是不能直接用==比较是否相等的,对象也不能用==判断相等
3.判断时不能直接用连等如:11 && a<7 (若用if(1
&& 逻辑与
|| 逻辑或
区分逻辑与和按位与,区分逻辑或和按位或
void test1()
{
int a = 2; //010
int b = 5; //101
int c = a & b; //按(2进制)位与:000
int d = a && b; //逻辑与:1
printf("c = %d d = %d\n", c, d);//0 1
int e = a | b; //按(2进制)位或:111
int f = a || b; //逻辑或:1
printf("e = %d f = %d\n", e, f);//7 1
}
面试题:
void test2()
{
//程序输出的结果是什么?
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++; //a=1 b=2 c=3 d=4
//i = a++||++b||d++; //a=1 b=3 c=3 d=4
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
}
注:当使用&&判断时(a && b),当a已经为假时,编译器不会再判断b的真假,因为结果肯定为假
同理,当使用 || 判断时(a || b),当b已经为假时,编译器不会再判断b的真假,因为结果肯定为真
表达式1 ? 表达式2 : 表达式3
表达式1若为真,则整个表达式的结果为表达式2的结果,若为假,则整个表达式的结果为表达式3的结果
void test3()
{
int a = 10;
int b = 20;
int max = 0;
max = a > b ? a : b;
printf("max = %d", max);
}
//上述代码等价于:
void test3()
{
int a = 10;
int b = 20;
int max = 0;
if (a > b)
max = a;
else
max = b;
printf("max = %d", max);
}
表达式1,表达式2,表达式3...表达式n
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
注:前面的表达式的结果可能会影响最后一个表达式,所以不能只看最后一个表达式而忽略前面的
void test4()
{
//代码1
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
int d = 0;
printf("c = %d", c); //13
//代码2
if (a = b + 1, c = a / 2, d > 10);
//该表达式最终以d>10的真假做判断
//代码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)
{
//业务处理
}
//判断条件仍是a>0,但前面的代码依旧会进行,该代码看起来更为简洁
}
操作数为:1.数组名 2.索引值
void test5()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
arr[4]; //下标引用操作符
//系统会把arr[4] ==> *(arr + 4)
printf("%d\n", *(arr + 4)); //5 注:整型指针+1 地址跳过4个字节
printf("%d\n", arr[4]); //5
printf("%d\n", 4[arr]); //5 说明数组名和索引值可以互换位置
}
int test6()
{
printf("hehe"); //函数调用操作符,即便没有传参也不能省略( )
}
. 结构体.成员名
-> 结构体指针->成员名
void test7()
{
struct Book
{
char name[20];
int price;
};
struct Book b = {"C语言程序设计", 35};
//结构体变量.成员变量
printf("书名:%s 定价:%d\n", b.name, b.price);
struct Book* pb = &b;
//结构体指针->成员变量
printf("书名:%s 定价:%d\n", pb->name, pb->price);
}
表达式求值的顺序一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符(char)和短整型(short)操作数在使用之前被转换为普通整型,这种转换称为整型提升。
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整体提升呢?
整形提升是按照变量的数据类型的符号位来提升的
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
整形提升的例子:
void test8()
{
char a = 3;
char b = 127;
char c = a + b;
//a和b要发生整型提升
//3的二进制: 00000000000000000000000000000011
//把3存入a中,此时的a:00000011,由于最高位为0,正数
//所以a整形提升为:00000000000000000000000000000011 -提升后的a
//127的二进制: 00000000000000000000000001111111
//存入b中后: 01111111
//整形提升后: 00000000000000000000000001111111 -提升后的b
// + 00000000000000000000000000000011
//a+b 00000000000000000000000010000010
//存入c中:10000010
//打印%d的c - 又要发生整形提升
//此时打印的c: 11111111111111111111111110000010 -提升后的c - 补码
// 11111111111111111111111110000001 - 反码
// 10000000000000000000000001111110 - 原码 -126
printf("c = %d\n", c); //-126
}
void test9()
{
char a = 0xb6; //10110110
short b = 0xb600; //1011011000000000
int c = 0xb6000000; //101101100000000000000000
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
//c
}
void test10()
{
char c = 1;
printf("%u\n", sizeof(c)); //1
printf("%u\n", sizeof(+c)); //4 +c是表达式,会发生整形提升(变成int 4字节)
printf("%u\n", sizeof(-c)); //4 与+c同理
}
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。(如:int和long int相加,会把int转换为long int)
但是算术转换要合理,要不然会有一些潜在的问题:
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
例题:求下列代码的结果
#include
int i; //全局变量 - 默认初始化为0
int main()
{
i--;
if (i > sizeof(i))
{
printf(">\n");
}
else
{
printf("<\n");
}
return 0;
}
输出结果为:>
解析:对于表达式i > sizeof(i),i是int类型,而sizeof(i)是unsigned int类型,此时会发生算数转换,i会转换为unsigned int类型,算数转换前i = -1,在内存中为:1111111111111111111111111111,此时二进制序列的符号位(最高位)为1表示是负数,转换为无符号整型后,最高位也为有效数字,i 成了一个非常大的整数,那么肯定大于4了
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序
一些问题表达式:
表达式的求值部分由操作符的优先级决定。
表达式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
表达式2
c + --c;
注释:注释:同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。 假设c = 1,那么计算顺序可能是 1 + 0 = 1,也可能是 0 + 0 = 0
//问题代码3
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(); 中我们只能通过操作符的优先级得知:先算乘法, 再算减法。,函数的调用先后顺序无法通过操作符的优先级确定。
//问题代码4
#include
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret); //12 4 + 4 + 4
printf("%d\n", i); //4
return 0;
}
但在linux系统下 输出的结果为 ret = 10(3+3+4) ,i = 4
总结:我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。