注:二进制的相关操作,在单片机、嵌入式程序非常常见
操作符的分类
算术操作符:+、-、*、/、%
移位操作符:<< >>
位操作符: & | ^
赋值操作符:>、>=、<、<=、==、!=
逻辑操作符: &&、||
条件操作符: ? :
逗号操作符: ,
下标引用: 【】
函数调用: ()
结构成员访问:. 、 ->
2进制、8进制、10进制、16进制是数值的不同表示形式。
16进制的数值之前写:0x
8进制的数值之前写:0
权重概念
权重:简单搜一下概念,各位理解一下。
进制转换
我们简单提一下进制间的转换:
在10进制转2进制时,如果数字较小,可以用凑的方法
如
三,2进制转8进制和16进制
8进制的数字每一位是0~7的,各自写成2进制,最多有三个二进制位就足够了,比如7的二进制是111,所以在2进制转8进制的时候,从2进制序列中右边低位开始向左每3个2进制位会换算一个8进制位,剩余不够3个2进制位的直接换算。
16进制的数字每一位都是0~9 a~f的,各自写成2进制,最多有4个2进制位就足够了,比如f的二进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位会换算一个16进制位,剩余不够4个二进制位的直接换算。
原码、反码、补码
数字:整数和小数
整数:整型
小数:浮点型
整数的2进制表示方法有三种,即原码、反码和补码
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号位,剩余的都是数值位。
符号位用0表示正,用1表示负。
正整数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译为二进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码
补码得到原码也是可以使用:取反,+1的操作。
对于整型来说:数据存放内容中其实存放的是补码。
对于整形来说:数据存放内存中其实存放的是补码。
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(cpu只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
移位操作符
<< 左移操作符
>> 右移操作符
注:移位操作符的操作数只能是整数。
#include <stdio.h>
int main()
{
int m = 10;
int n = m << 1;
printf("m=%d,n=%d", m, n);
return 0;
}
左移操作符:
移位规则:左边抛弃、右边补0
右移操作符:
1.逻辑右移:左边用0填充,右边丢弃
2.算术右移:左边用原该值的符号位填充,右边丢弃。
#include <stdio.h>
int main()
{
int m = 10;
int n = m >> 1;
printf("m=%d\n", m);
printf("n=%d\n", n);
return 0;
}
右移到底是算术右移还是逻辑右移?
取决于编译器的实现
大部分的编译器上算术右移
#include <stdio.h>
int main()
{
int m = -10;
int n = m >> 1;
printf("m=%d\n", m);
printf("n=%d\n", n);
return 0;
}
此时证明vs是执行算术右移
注:对于移位操作符,不要移动负数位,这个是标准没有定义的。
位操作符:& | ^ ~
位操作符操作的都是二进制位
&按位与
|按位或
^按位异或
~按位取反
注:位操作数必须是整数
&& 并且(逻辑与)--逻辑操作符
|| 或者(逻辑或)
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a & b;
//按位与
//计算规则:对应的二进制位进行与运算
//只要有0就是0,两个同时为1才是1
printf("c=%d\n", c);
return 0;
}
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a | b;
//按位或
//计算规则:对应的二进制位进行或运算
//只要有1就是1,两个同时为0才是0
printf("c=%d\n", c);
return 0;
}
#include <stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
//按(2进制)位异或
//计算规则:对应的二进制位进行异或运算
//相同为0,相异为1
printf("c=%d\n", c);
return 0;
}
//~ 按位取反
#include <stdio.h>
int main()
{
int a = 1;
int b = ~a;
//所有位按位取反
printf("%d\n", b);
return 0;
}
//~ 按位取反
#include <stdio.h>
int main()
{
int a = 0;
int b = ~a;
printf("%d\n", b);
return 0;
}
一道变态的面试题:
不能创建临时变量(第三个变量),实现两个数的交换。
如果能创建临时变量,我们引入一个变量tmp就能做到。
就类似两杯不同的水,还有一个空水杯
常规引入变量的方法我们已经知道了,因此不再赘述
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
a = a + b;
b = a - b;
a = a - b;
printf("a=%d b=%d\n", a, b);
return 0;
}
上述写法有缺陷:a和b如果非常大,求和后的结果超过了整型的最大值
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("a=%d b=%d\n", a, b);
return 0;
}
//异或操作符计算规则
//a^a=0;
//0^a=a
//000
//011
//011
编写代码实现:求一个整数存储在内存中的二进制中1的个数
·······························································
a&1==1,就说明a的二进制中最低位是1
a&1==0,就说明a的二进制中最低位是0
#include <stdio.h>
int main()
{
int a = 10;
int count = 0;
int i = 0;
for (i = 0; i < 32; i++)
{
if (((a >> i) & 1) == 1)
count++;
}
printf("%d\n", count);
return 0;
}
同理负数也一样(在内存中存储的)
那么我们什么时候要考虑正负数?
#include <stdio.h>
int haha(int n)
{
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n = n / 2;
}
return count;
}
int main()
{
int n;
scanf("%d", &n);
int ret = haha(n);
printf("ret=%d", ret);
return 0;
}
然后我们在该程序中如果输入一个负数,那么会出现什么情况呢?
此时结果就会有问题
此时如果想得到正确的值把函数中int n的前面加上unsigned,即改变int 的类型
#include <stdio.h>
int haha(unsigned int n)
{
int count = 0;
while (n)
{
if (n % 2 == 1)
count++;
n = n / 2;
}
return count;
}
int main()
{
int n;
scanf("%d", &n);
int ret = haha(n);
printf("ret=%d", ret);
return 0;
}
这样效率都不太高
n=n&(n-1)
效果:把n的二进制中最右边的1去掉了
#include <stdio.h>
int haha(int n)
{
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
return count;
}
int main()
{
int n;
scanf("%d", &n);
int ret = haha(n);
printf("ret=%d", ret);
return 0;
}
写代码需要积累,多见一些nb的代码
再比如:要判断一个数n是否是2的次方数
#include <stdio.h>
int main()
{
int n=0;
scanf("%d", &n);
if ((n & (n - 1)) == 0)
{
printf("Yes\n");
}
else
{
printf("No\n");
}
return 0;
}
练习: 把一位数的二进制改为0或改为1
#include <stdio.h>
int main()
{
int n = 13;
n = n | (1 << 4);
printf("%d\n", n);
return 0;
}
单目操作符
!、++、--、&、*、+、-、~、sizeof、(类型)
单目操作符的特点是只有一个操作数,自行理解吧老铁们
逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
#include <stdio.h>
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;
}
下标访问[ ] ,函数调用( )
结构成员访问操作符
结构体
C语言已经提供了内置类型,如char、short、int、long、float、double等,但是只有这些内置类型还是不够的。假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量,如:
标量、数组、指针,甚至是其他结构体。
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s1;
struct Stu s2;
return 0;
}
int类型怎么创建变量呢?
int…………………………>int a;
类似做月饼模具…………………………>月饼
|
|
|
莫惊讶,上图也是一种类型,类比做月饼,他也是一种模具,因此可以用它来做一个月饼…………>变量
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
}s3,s4;
int main()
{
struct Stu s1;
struct Stu s2;
return 0;
}
//s3,s4也是结构体变量
注意:s3 s4是全局变量,上图的s1,s2是局部变量
此时还可以初始化
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
}s3 = { "wang",18,80。0f }, s4 = { "wa",29,100。0f};
int main()
{
struct Stu s1 = { "yang",18,85.5f};
struct Stu s2 = { "zhang",19,95.5f };
return 0;
}
//不想全部初始化,只想初始化里面的其中一部分元素应该用或者不想按照顺序初始化
结构体里可以包含结构体:
struct Point
{
int x;
int y;
};
struct Data
{
int num;
struct Point p;
};
#include <stdio.h>
struct Point
{
int x;
int y;
};
struct Data
{
int num;
struct Point p;
};
int main()
{
struct Data d = { 100,{22,123} };
return 0;
}
结构成员的访问操作符
结构体的直接访问:
#include <stdio.h>
struct Point
{
int x;
int y;
};
struct Data
{
int num;
struct Point p;
};
int main()
{
struct Data d = { 100,{22,123} };
printf("num=%d p.x=%d p.y=%d\n", d.num, d.p.x, d.p.y);
return 0;
}
同理,打印有字符串的类似
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
}s3 = { "wang",18,80 }, s4 = { "wa",29,100 };
int main()
{
struct Stu s1 = { "yang",18,85.5f};
struct Stu s2 = { "zhang",19,95.5f };
struct Stu s3 = { .score=98.5f };
printf("%s %d %f\n", s1.name, s1.age, s1.score);
return 0;
}
结构成员访问操作符
结构体变量.成员名
结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
}s3 = { "wang",18,80 }, s4 = { "wa",29,100 };
int main()
{
struct Stu s1 = { "yang",18,85.5f};
struct Stu s2 = { "zhang",19,95.5f };
struct Stu s3 = { .score=98.5f };
//结构体指针
struct Stu* ps = &s1;//取出s1的地址
printf("%s %d %f\n", ps->name, ps->age, ps->score);
return 0;
}
指针里面存放了s1的地址,其实相当于ps指向了s1的地址,所以ps找到了s1成员内的对象的地址
结构体指针----->成员名
操作符的属性:优先级、结合性
优先级:相邻操作符,优先级高的先执行,优先级低的后执行
结合性:如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序,大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)。
表达式求值
整型提升
C语言中整型算术运算总是至少以缺省类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作符在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在cpu的相应运算器件内执行,cpu内整型运算器的字节长度一般就是int的字节长度,同时也是cpu的通用寄存器的长度。
因此,即使两个char类型的相加,在cpu执行时实际上也要先转换为cpu内整型操作数的标准长度。
#include <stdio.h>
int main()
{
char c1 = 125;
char c2 = 10;
char c3 = c1 + c2;
printf("%d\n", c3);
return 0;
}
缺省就是默认的意思
算术转换
如果某个操作符的各个操作数属于不同的类型,那么排除其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
总结:即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在风险的,建议不要写出特别复杂的表达式。
今天的分享就到这里,下次见。