一、本章重点
1. 各种操作符的介绍。
二、操作符
分类:
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
三、算术操作符
+ - * / %
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
如int a = 5/2;//商2余1
a的值就是2
假如想得到a= 2.5,
那么我们首先需要把分子5或者分母2改写成5.0或者2.0,再把a的类型改成double.
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
四、移位操作符
<< 左移操作符
>> 右移操作符
左移操作符 移位规则:
左边抛弃、右边补0
有乘2的效果。
右移操作符 移位规则:
首先右移运算分两种:
1. 逻辑移位 左边用0填充,右边丢弃
2. 算术移位 左边用原该值的符号位填充,右边丢弃
通常都是算术右移。
右移有除以2的效果。
int a = -1;
int b = a >> 1;
cout << b;
输出结果是-1。
四-1、原码,反码,补码
正数的三者相同。
对于正数负数,在内存中均以补码存储。
以-1为例:
原码:10000000000000000000000000000001
最高位叫符号位,负数为一,正数为零;
反码:11111111111111111111111111111110
符号位不变,其他位取反
补码:11111111111111111111111111111111
反码加一
也就是说,-1在内存中的码是:11111111111111111111111111111111
【警告⚠ : 对于移位运算符,不要移动负数位,这个是标准未定义的。】
例如:
int num = 10;
num>>-1;//error
五、位操作符(二进制位操作符)
位操作符有三种:
& //按位与
| //按位或
^ //按位异或
注:他们的操作数必须是整数。
示例代码:
#include <stdio.h>
int main()
{
int num1 = 1;
int num2 = 2;
num1 & num2;
num1 | num2;
num1 ^ num2;
return 0;
}
结果分别为:0,3,3。
首先,先把1和2转化成二进制:
01
10
然后按位与:得到00,转化为10进制是0;
按位或:得到11,转化为十进制是3;
按位异或:相同为0,不同为1,得到11,转化为十进制是3。
一道变态的面试题:
不能创建临时变量(第三个变量),实现两个数的交换。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
使用加减法。
用
a = a+b;
b= =a-b;
a = a-b;
算法缺陷:如果ab很大,加起来会造成位丢失,只能解决部分问题。
所以采用异或法来改进:
核心公式:
( a ^ b ) ^ b = a
可以用在密码学里解密。
a = a^b;
b = a^b;
a = a^b;
第一步:
a 011 //3
b 101 //5
c 110 //6
c用来存放a和b异或的结果,再存到a里;
第二步:
用新的a,也就是a^b,再和b异或,得到原来的a,存放到b里。
第三步:
同理,再用新的a,即a^b,再和新的b,也就是原来的a异或,
得到原来的b。
这种方法不会溢出,因为不会进位。
练习:
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
参考代码:
//方法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;
}
原理:
也就是统计二进制中一个数的补码中有几个1。
我们先以十进制位得到各个位为例:
123
先%10得到3,个位,
得到3以后不需要了,
用123/10 = 12;
再用12%10得到2,
再12/10等于1;
最后用1%10得到1。
再用1/10等于0,
这时候跳出循环。
同理,二进制中:
不断%2,再/2,直到最后一个参与运算的数是0,停止运算。
思考这样的实现方式有没有问题?
有问题。
当我们算负数的时候,会出现问题。
比如-1:
-1%2 = 0余-1,
此时计数器不增加;
然后-1/2 = 0,
跳出循环。
但-1的补码应该有32个1,
所以不对。
我们可以把一个整型的32个比特位都拿到,
然后和1按位与。
这样就能得到最低位是否是1.因为1的高位全是0,0和谁与都是0。
如:
目标数:00000000000000000000000000000011
一: 00000000000000000000000000000001
结果: 00000000000000000000000000000001
如:
//方法2:
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( ((num>>i)&1) == 1 )
count++;
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
思考还能不能更加优化,这里必须循环32次的。
//方法3:
#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;
}
//这种方式是不是很好?达到了优化的效果,但是难以想到
当num==0时,证明后面都是0,不会再有1,所以直接跳出循环,优化了时间。
六、赋值操作符
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.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;//复合赋值
//其他运算符一样的道理。这样写更加简洁。
七、单目操作符
单目操作符有哪些呢?
!:逻辑反操作
int a = 10;
printf("%d\n",a);
打印结果为10;
printf("%d\n",!a);
打印结果为0;
因为a不等于10了,所以为假,即0。
-:负值
+:正值
&:取地址
int a = 10;
int* p = &a;
可以推出*p = a;
sizeof:操作数的类型长度(以字节为单位,ASCII编码中,一个英文一个字节,一个汉字两个字节)
计算数组大小:
int arr[10] = {0};//10个整型元素的数组
大小为10*sizeof(int) = 40;
也就是这个arr[10]的大小为40字节
~:对一个数的二进制按位取反
int a = 0;
int b = ~a;
a:0000000000000000000000000000
b:11111111111111111111111111111111
b的值输出为-1。
这里涉及到原码,反码,补码。
负数在内存中存储的时候,存储的是二进制的补码。
这里的b是有符号整型,b的二进制最高位是1,代表它是一个负数。
打印的是这个数的原码
原码--符号位不变,其他位取反--反码--加1--补码
补码--减1--反码--符号位不变,其他位取反--原码
b的补码:11111111111111111111111111111111
减1: 11111111111111111111111111111110
取反: 1000000000000000000000000001
得到原码
所以对应十进制数为-1。
现在有一个数11,
二进制原码:00000000000000000000000000001011
我想让其第三位变成1,怎么变?
让他或上一个00000000000000000000000000000100即可。
这个数是c = 1<<2
得到:00000000000000000000000000001111
最后代码为:a = a| (1<<2);
假如我们想变回去,把1111变成1011,
只需要用其与上一个1111111111111111111111111111101即可
而这个数正好是c的全部按位取反,即~c。
代码:a = a&(~(1<<2))
--:
++:
1、后置++,a++
int a = 10;
int b = a++;
a = 11,b = 10
先使用a的值,再把a的值自增1;
2、前置++,++a
int a = 10
int b = ++a;
a= =11;b= =11
a先自增1,再使用a的值
*:
间接访问操作符(解引用操作符)
(类型):
强制类型转换
int a = (int)3.14;
把3.14强制转换为int类型
示例代码:
#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;
}
关于sizeof其实我们之前已经见过了,可以求变量(类型)所占空间的大小。
其中,sizeof(char) = 1,sizeof(char*) = 4。
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)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
1,2分别输出:40和4
其中,
1:sizeof(arr) = 10*int = 10*4 = 40
2:传过去的是arr的首元素地址,接受他的即一个指针,int arr[] (int* arr),他的大小是4。
3、4分别输出:10和4
其中,
1:sizeof(ch) = 10*char = 10*1 = 10
2:传过去的是ch的首地址,接受他的即一个指针,char ch[] (char* ch),他的大小也是4。
例2:
short s = 0;
int a = 10;
sizeof(s = a+5) = ?
s = ?
这里答案是2。
不管a的类型是什么,只要放到s短整型中,那么最终结果就是短整型。
最后s的值是0。
因为sizeof里的表达式是不会真实发生运算改变原值的。
++和--:
//++和--运算符
//前置++和--
//前置++和--:
#include <stdio.h>
int main()
{
int a = 10;
int x = ++a;
//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
int y = --a;
//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
return 0;
}
//后置++和--
#include <stdio.h>
int main()
{
int a = 10;
int x = a++;
//先对a先使用,再增加,这样x的值是10;之后a变成11;
int y = a--;
//先对a先使用,再自减,这样y的值是11;之后a变成10;
return 0;
}
八、关系操作符
>
>=
<
<=
!=
==
这些关系运算符比较简单,没什么可讲的,但是我们要注意一些运算符使用时候的陷阱。
警告: 在编程的过程中== 和=不小心写错,导致的错误。
九、逻辑操作符
&& 逻辑与
int a = 3;
int b = 5;
int c = a&&b;
c=1,为真;
int a = 0;
int b = 5;
此时c = 0,因为a为0,则是假
|| 逻辑或
int c = a||b
a与b只要有1个为真,则为真
区分逻辑与和按位与
区分逻辑或和按位或
逻辑与和或关注的是这个数本身是真还是假
按位与和按位或关注的是这个数的二进制序列的每一位分别是真还是假
1&2----->0
1&&2---->1
1|2----->3
1||2---->1
逻辑与和或的特点:
360笔试题
#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;
}
//程序输出的结果是什么?
首先执行a++&&++b,
因为a++此时是0,0&&上任何数都是0,
只要左边是假,右边就不参与运算,
所以此时++b就不运算了,结果是0,
同理,这个0再与后面的d++进行&&操作,
后面的d++也不会运算。
所以i的结果是0,
而这句话运行完成后,a自增1,
所以a,b,c,d的结果分别是1,2,3,4。
再看下面这个运算:
i = a++||++b||d++;
运行结果是:
首先,a是0,无法判断 a++||++b的值,
所以开始算++b;
++b的值是3,
所以 a++||++b的值是1。
又因为左式子已经为真,
1和任何数或都为真,
所以右式不再计算。
本条语句结束,
a++,
所以最后结果是
a = 1
b = 3
c = 3
d = 4
‘综上,以下两个结论:
与运算,只要左式为假,即0,那么右边的式子不再计算;
或运算,只要左式为真,即非0,那么右边的式子不再计算。
十、条件操作符 (三目操作符,具有三个操作数)
exp1?exp2:exp3
expN为表达式
若表达式1为真,则表达式2被执行,且值为整个表达式的结果;
若表达式1为假,则表达式3被执行,且值为整个表达式的结果。
求最大值:
int a = 10;
int b = 20;
int max;
if(a>b)
max = a;
else
max = b;
return 0;
可用条件操作符来表示:
max = (a > b ? a : b);
a>b吗
大于,则max的值为a;
不大于,则max的值为b。
十一、逗号表达式
exp1, exp2, exp3, …expN
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最 后一个表达式的结果。
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, d > 0)
//代码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中,
第一个表达式a>b不产生结果;
第二个表达式a = b+10,即a = 12;
第三个表达式b = a+1,即b = 13.
最后再把13赋给c.
代码3中,是逗号表达式对while循环的简化。
可以少用,因为可读性不高。
十二、下标引用、函数调用和结构成员
[] 下标引用
() 函数调用
. 结构成员
-> 结构成员
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;
}
操作数至少一个,就是函数名。
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;
}