目录
1.操作符分类
算术操作符: + 、- 、* 、/ 、%
移位操作符: >
位操作符: & | ^ `
赋值操作符: = 、+= 、 -= 、 *= 、 /= 、%= 、>= 、&= 、|= 、^=
单⽬操作符: !、++、--、&、*、+、-、~ 、sizeof、(类型)
关系操作符: > 、>= 、< 、<= 、 == 、 !=
逻辑操作符: && 、||
条件操作符: ? :
逗号表达式: ,
下标引⽤: []
函数调⽤: ()
结构成员访问: . 、->
2.二进制和进制转换
2.1.二进制的介绍
例如:数值15的各种进制转换:
15的2进制:1111=2^3+2^2+2^1+2^0
15的8进制:17=8^1+7*8^0
15的10进制:15=10^1+5*10^0
15的16进制:F
8进制的:017——0开头的是8进制数字
16进制的:0x5——0x开头为16进制数字
10进制:
(1)10进制中满10进1;
(2)10进制的数字每⼀位都是0~9的数字组成 ;
(3)10进制的每一位*对应的权重值10^(n-1);
2进制:
(1)2进制中满2进1;
(2)2进制的数字每⼀位都是0~1的数字组成;
(3)2进制的每一位*对应的权重值2^(n-1);
2.2.二进制转十进制
10进制的125转换的2进制:
125/2=62......1
62/2=31......0
31/2=15......1
15/2=7......1
7/2=3......1
3/2=1......1
1/2=0......1
由上往下依次得到的余数就是10进制转换得到的2进制:1111101
2.3.二进制转八进制
8进制的数字每⼀位是0~7的,0~7的数字,各⾃写成2进制,最多有3个2进制位就⾜够了。
如:2进制的01101011,换成8进制为0153(0开头的数字,会被当做8进制):
二进制: 0 1 | 1 0 1 | 0 1 1
八进制: 1 5 3
2.4.二进制转十六进制
16进制的数字每⼀位是0~9,a ~f 的,0~9,a ~f的数字,各⾃写成2进制,最多有4个2进制位就⾜够了。
如:2进制的01101011,换成16进制:0x6b(16进制表⽰的时候前⾯加0x):
二进制: 0 1 1 0 | 1 0 1 1
十六进制: 6 b
3.原码、反码、补码
(1)整数的二进制表示方法有三种:原码、反码、补码;
(2)有符号整数的三种表示方法均有符号位和数值位两部分;二进制中,最高1位被当作符号位,其余均为数值位;
(3)符号位用"0"表示正,用"1"表示负;
(4)正整数的原码、反码、补码都相同;
(5)负整数中:
1>原码:将数值按照正负数的形式翻译为二进制即可
2>反码:符号位不变,其他位按位取反
3>补码:反码+1=补码
(6)对于整形来说:数据存放内存中其实存放的是补码。
int main()
{
int num = 10;//signed int num = 10;
//10存放在整型变量num中,占4个字节 == 32bit位
//00000000000000000000000000001010 - 原码
//00000000000000000000000000001010 - 反码
//00000000000000000000000000001010 - 补码
int num2 = -10;
//-10存放在整型变量num2中,占4个字节 == 32bit位
//10000000000000000000000000001010 - 原码
//11111111111111111111111111110101 - 反码
//11111111111111111111111111110110 - 补码
return 0;
}
//1-1
//1+(-1)
//00000000000000000000000000000001 ---> 1的原码
//10000000000000000000000000000001 ---> -1的原码
//10000000000000000000000000000010 -> -2
//
//使用补码就可以
//00000000000000000000000000000001 ---> 1的补码
//10000000000000000000000000000001
//11111111111111111111111111111110
//11111111111111111111111111111111 ----> -1的补码
//00000000000000000000000000000001
//00000000000000000000000000000000 ---> 0
4.移位操作符
<< 左移操作符
>> 右移操作符
(1)移位操作符的操作数只能是整数;
(2)对于移位操作符,不要移动负数位,这个是标准未定义的;
4.1.左移操作符
移位规则:左边抛弃,右边补0
int main()
{
int m = 10;
int n = m << 1;
printf("n = %d\n", n);
printf("m = %d\n", m);
return 0;
}
4.2.右移操作符
移位规则:
1>逻辑右移:左边用0填充,右边抛弃;
2>算术右移(大部分):左边用原值得符号位填充,右边抛弃;
到底为逻辑右移还是算术右移取决于编译器。
int main()
{
int m = -10;
int n = m >> 1;
//10000000000000000000000000001010
//11111111111111111111111111110101
//11111111111111111111111111110110
//n = m>>1
//11111111111111111111111111111011
//10000000000000000000000000000100
//10000000000000000000000000000101
//-5
printf("m = %d\n", m);
printf("n = %d\n", n);
return 0;
}
5.位操作符
& 按位与 只要为0则为0,同时为1才为1
| 按位或 只要为1则为1,同时为0才为0
^ 按位异或 相同为0,相异为1
异或操作符特点:a^a=0; 0^a=a
~ 按位取反
int main()
{
int a = 3;
int b = -5;
int c = a & b;
//按(2进制)位与
//计算规则:对应的二进制位进行与运算
//只要有0就是0,同时为1才是1
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101 --- -5的原码
//11111111111111111111111111111010 反码
//11111111111111111111111111111011 --- -5的补码
//00000000000000000000000000000011 --- 3的补码
//进行按位与计算
//00000000000000000000000000000011
printf("c = %d\n", c);
return 0;
}
int main()
{
int a = 3;
int b = -5;
int c = a | b;
//按(2进制)位或
//计算规则:对应的二进制位进行或运算
//只要有1就是1,同时为0才是0
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101 --- -5的原码
//11111111111111111111111111111010 反码
//11111111111111111111111111111011 --- -5的补码
//00000000000000000000000000000011 --- 3的补码
//进行按位或计算
//11111111111111111111111111111011
printf("c = %d\n", c);
return 0;
}
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
//按(2进制)位异或
//计算规则:对应的二进制位进行异或运算
//相同为0,相异为1
//00000000000000000000000000000011 --- 3的补码
//10000000000000000000000000000101 --- -5的原码
//11111111111111111111111111111010 反码
//11111111111111111111111111111011 --- -5的补码
//00000000000000000000000000000011 --- 3的补码
//进行按位异或运算
//11111111111111111111111111111000
//10000000000000000000000000000111
//10000000000000000000000000001000
printf("c = %d\n", c);
return 0;
}
一道面试题:
不能创建临时变量(第三个变量),实现两个数的交换:
//方法1
//异或只适用于整数,不适用于浮点数
int main()
{
int a = 3;
int b = 5;
//交换a和b的值
printf("a=%d b=%d\n", a, b);
a = a ^ b;
b = a ^ b;//=a^(b^b)=a^0=a
a = a ^ b;//=(a^a)^b=0^b=b
printf("a=%d b=%d\n", a, b);
return 0;
}
//方法2
//效率最高
int main()
{
int a = 3;
int b = 5;
//交换a和b的值
printf("a=%d b=%d\n", a, b);
int c = a;
a = b;
b = c;
printf("a=%d b=%d\n", a, b);
return 0;
}
//方法3
//不可行:a和b的如果非常大,求和后的结果可能会超过整型最大值
int main()
{
int a = 3;
int b = 5;
//交换a和b的值
printf("a=%d b=%d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("a=%d b=%d\n", a, b);
return 0;
}
练习:
(1)求⼀个整数存储在内存中的⼆进制中1的个数:
a&1=1---说明a最低位为1;a&1=0---说明a最低位为0
//统计2进制中1的个数
//方法1
int main()
{
int a = 10;
int i = 0;
int count = 0;
for (i = 0; i < 32; i++)
{
if (((a >> i) & 1) == 1)//内存中存储的就是补码,所以正数负数都没关系
{
count++;
}
}
printf("%d ", count);
return 0;
}
//方法2
//需要考虑正负数的情况
//求10进制中1的个数
int count_one_bit(unsigned int n)//改为 unsigned int以后转换为无符号位,不用考虑正负数
{
int count = 0;
while (n)
{
if (n % 2 == 1)//余数为1则说明满足二进制为1的情况(10进制转换成2进制就是/2留下的余数组成的)
count++;
n = n / 2;//除以2去掉后一位
}
return count;
}
int main()
{
int num = 0;
scanf("%d", &num);
//求一个整数存储在内存中的二进制中1的个数
int n = count_one_bit(num);
printf("%d ", n);
return 0;
}
//方法3
//n=n&(n-1)
//只要有0则为0,同时为1才为1
//效果:把n的最右边的1逐个去掉
int count_one_bit(int n)
{
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
return count;
}
int main()
{
int num = 0;
scanf("%d", &num);
//求一个整数存储在内存中的二进制中1的个数
int n = count_one_bit(num);
printf("%d ", n);
return 0;
}
(2)⼆进制位置0或者置1:编写代码将13⼆进制序列的第5位修改为1,然后再改回0
13的2进制序列:00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
int main()
{
int n = 13;
n = n | (1 << 4);
printf("%d\n", n);
n = n & ~(1 << 4);
printf("%d\n", n);
return 0;
}
(3)打印整数二进制的奇数位和偶数位:
//打印整数二进制的奇数位和偶数位
//获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
#include<stdio.h>
void Print(int n)
{
int i = 0;
// printf("%d ", i);
// while (i >= 0)
// {
// printf("%d ", (n >> i) & 1);
// i--;
// //因为得到的二进制位通常是从下而上排序的
// //使结果正向打印
// }
// printf("\n");
printf("奇数位:");
for (i = n; i >= 0; i -= 2)//下标为偶数的数位奇数
{
printf("%d ",(n>>i)&1 );
}
printf("\n");
printf("偶数位:");
for (i = n+1; i >= 1; i -= 2)//下标为奇数的数为偶数
{
printf("%d ", (n >> i) & 1);
}
printf("\n");
}
int main()
{
int n = 0;
scanf("%d", &n);
Print(n);
return 0;
}
(4)输入两个整数,求两个整数二进制格式有多少个位不同:
#include<stdio.h>
void Print(int n, int m)
{
int i = 0;
int count = 0;
int num = n ^ m;//相同为0,相异为1
for (i = 0; i < 33; i++)
{
if (num & 1 == 1)
{
count++;
}
num = num >> 1;
}
printf("%d ", count);
}
int main()
{
int n = 0;
int m = 0;
scanf("%d %d", &n, &m);
Print(n, m);
return 0;
}
(5)在一个整型数组中,只有一个数字出现一次,其他数组都是成对出现的,请找出那个只出现一次的数字:
//在一个整型数组中,只有一个数字出现一次,其他数组都是成对出现的,请找出那个只出现一次的数字。
//例如:
//数组中有:1 2 3 4 5 1 2 3 4,只有5出现一次,其他数字都出现2次,找出5
#include <stdio.h>
int find_single(int arr[], int sz)
{
int i = 0;
int ret = 0;
for (i = 0; i < sz; i++)
{
ret ^= arr[i];
}
return ret;
}
int main()
{
int arr[] = { 1,2,3,4,5,1,2,3,4 };
int sz = sizeof(arr) / sizeof(arr[0]);
int single = find_single(arr, sz);
printf("%d ", single);
}
6.单目操作符
特点:只有一个操作数
!、++、--、&、*、+、-、~ 、sizeof、(类型)
7.逗号表达式
用逗号隔开的多个表达式:从左到右依次执行,整个表达式的结果是最后一个表达式的结果
exp1, exp2, exp3, …expN
8.下标访问、函数调用操作符
(1)[]下标引用操作符:操作数:一个数组名+一个索引值(下标);
(2)函数调用操作符(不可省略):最少一个操作数(无参数时只有函数名)
接受一个/多个操作数:第一个操作数为函数名,剩余为传递给函数的参数;
9.结构成员访问操作符
(1)结构体:自定义的数据类型,用于描述复杂对象;
(2)结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚⾄是其他结构体。
(3)使用方式:
1>结构体变量.成员名
2>结构体变量->成员名
//想描述一个学生
//名字+年龄+成绩
struct Stu
{
char name[20];
int age;
float score;
} s3 = { "wangwu", 33, 66.0f }, s4 = {"翠花", 18, 100.0f};//全局变量
int main()
{
struct Stu s1 = {"zhangsan", 20, 95.5f};//局部变量
struct Stu s2 = {"lisi", 18, 87.5f};
struct Stu s5 = {.score= 98.5f, .name="hehe", .age = 18};
//. 结构成员访问操作符
//结构体变量.成员名
printf("%s %d %f\n", s1.name, s1.age, s1.score);
printf("%s %d %f\n", s4.name, s4.age, s4.score);
//结构体指针
struct Stu* ps = &s1;//取出s1的地址
printf("%s %d %f\n", ps->name, ps->age, ps->score);
//结构体指针->成员名
return 0;
}
10.操作符的属性
(1)优先级:相邻操作符:优先级高的先计算,优先级低的后计算;
(2)结合性:根据运算符是左结合还是右结合决定执行顺序;
(3)常用的操作符优先级:
圆括号( () )
⾃增运算符( ++ ),⾃减运算符( -- )
单⽬运算符( + 和 - )
乘法( * ),除法( / )
加法( + ),减法( - )
关系运算符( < 、 > 等)
赋值运算符( = )
由于圆括号的优先级最高,可以使用它来改变其他运算符的优先级
参考链接:https://zh.cppreference.com/w/c/language/operator_precedence
11.表达式求值
11.1.整型提升
C语⾔中整型算术运算总是⾄少以缺省整型类型(默认)的精度来进⾏的。为了获得这个精度,表达式中的字符(char)和短整型操作数(short)在使⽤之前被转换为普通整型(int),这种转换称为整型提升(字符属于整型家族,因为字符是以ASCII码值的形式存储的)。
进行整型提升的方式:
(1)有符号整数提升是按照变量的数据类型的符号位来提升的;
(2)⽆符号整数提升,⾼位补0;
int main()
{
//char类型的取值范围是-128~127
//char 是占用1个字节的,1个字节是8个bit位
char c1 = 125;
//00000000000000000000000001111101
//发生截断
//01111101 -c1
//
char c2 = 10;
//00000000000000000000000000001010
//00001010 -c2
//
//00000000000000000000000001111101 -c1 - 有符号的char
//00000000000000000000000000001010 -c2 - 有符号的char
//00000000000000000000000010000111
//10000111 -c3
//
char c3 = c1 + c2;//char为整型,故要发生整型提升
//11111111111111111111111110000111 ---补码
//10000000000000000000000001111000 ---反码
//10000000000000000000000001111001 ---原码
//-121
printf("%d\n", c3);//-121
printf("%d\n", c1+c2);//不存到c3中,不发生截断,结果就为00000000000000000000000010000111
//00000000000000000000000001111101
//00000000000000000000000000001010
//00000000000000000000000010000111
//
//%d 打印有符号的整数(原码)
//
return 0;
}
11.2. 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就⽆法进⾏。下⾯的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
11.3.问题表达式
//表达式的求值部分由操作符的优先级决定
//表达式1
a*b + c*d + e*f
(1)表达式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;
(2)操作符的优先级只能决定⾃减--的运算在+的运算的前⾯,但是我们并没有办法得知+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是有歧义的。
//表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
(3)表达式3在不同编译器中测试结果不同:⾮法表达式程序的结果
#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;
}
(4)上述代码answer=fun()-fun()*fun();中我们只能通过操作符的优先级得知:先算乘法再算减法。
函数的调⽤先后顺序⽆法通过操作符的优先级确定。
//表达式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环境下都执⾏,看结果
//gcc编译器:10 4
//vs2022:12 4
(5)这段代码中的第⼀个+在执⾏的时候,第三个++是否执⾏,这个是不确定的,因为依靠操作符的优先级和结合性是⽆法决定第⼀个+和第三个前置++的先后顺序。
(6)即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式就是存在潜在⻛险的,建议不要写出特别复杂的表达式。