码字不易,若有错误,欢迎指正!
+ | - | * | / | % |
---|---|---|---|---|
加 | 减 | 乘 | 除 | 模(整数取余) |
#include <stdio.h>
int main(){
int ret1 = 4 / 2;
printf("%d\n", ret1);//%d是指以有符号整型的形式打印ret1
int ret = 5 / 2;
printf("%d\n", ret2);
return 0;
}
运行结果:
#include <stdio.h>
int main(){
double ret1 = 4.0 / 2;
double ret2 = 5.0 / 2;
double ret3 = 5 / 2.0;
double ret4 = 5.0 / 2.0;
printf("ret1 = %d\n",ret1);
printf("ret2 = %d\n",ret2);
printf("ret3 = %d\n",ret3);
printf("ret4 = %d\n",ret4);
return 0;
}
运行结果:
模操作符**只适用于整数间的运算,**结果是两个整数相除的余数并且是一个整数。
#include <stdio.h>
int main() {
int ret1 = 5 % 2;
int ret2 = 4 % 2;
printf("ret1 = %d\n", ret1);
printf("ret2 = %d\n", ret2);
//字符是以整数(ASCII码值)在内存中进行储存的,也可以看做整数。
char a = 10;
char b = 3;
int c = a % b;
printf("%d\n", c);
return 0;
}
运行结果:
左移操作符 | 右移操作符 |
---|---|
<< | >> |
这里的位指的是整数在内存中储存的补码的二进制位
符号位:正整数的最高位是符号位,默认是0。 原码:由相应类型的整数直接写出的二进制形式,二进制位不够则在高位补0 正整数和0的原码、反码、补码均相同,故其补码就是其原码。
如 int类型的整数
10
的二进制形式是1010
,int在内存中假设占32个bit位,则补二进制位00000000 00000000 00000000 00001010
,得到int型整数10的原码,同时也是反码、补码。
符号位:负数的最高位是符号位,默认为1。 反码:符号位不变,其它位按位取反得到的二进制数。取反:0->1,1->0; 补码:反码的二进制数+1得到的二进制数。
以
-10
为例 二进制形式:1010
原码:10000000 00000000 00000000 00001010
反码:11111111 11111111 11111111 11110101
补码:11111111 11111111 11111111 11110110
左移操作符的操作数只能是整数。
规则:左边抛弃,右边补零 如:
#include<stdio.h>
int main(){
int n = 5;
int m = n << 1;
printf("%d\n", n);
printf("%d\n", m);
return 0;
}
运行结果:
把n的二进制位左移1位的结果赋值给m,故m中存放的是n << 1的值,但左移操作符并不改变n的值。
左移操作符的一个特点:以n=1,n=5为例
n=1 | n<<1 | n<<2 | n<<3 | n<<4 | n<<5 |
---|---|---|---|---|---|
2 | 4 | 8 | 16 | 32 | |
n=5 | n<<1 | n<<2 | n<<3 | n<<4 | n<<5 |
10 | 20 | 40 | 80 | 160 |
n相同的情况下,随着左移位数的增加,n与n<<位数成倍数关系。
规则:
左边用原符号位填充,右边丢弃。
不考虑符号位,左边直接用0补充,右边丢弃。
一般情况下是算术右移,因为考虑了原符号位,更加合理。 这两种右移规则对于正整数和0而言均相同,应为他们的二进制位最高位为0,不管哪种右移左边均补0。 对于负数而言这两种右移规则才有区别:
#include <stdio.h>
int main(){
int n = -1;
int m = n >> 1;
printf("n = %d\n", n);
printf("m = %d\n", m);
return 0;
}
分析:(以-1为例)
vs2019上的运行结果:为算数右移
C语言的算术右移对于移动负数位的行为没有进行定义,是错误的。想实现效果合理使用左移右移就可以满足。
& | 按位与 |
---|---|
| | 按位或 |
^ | 按位异或 |
注意:
二进制对应的为相与,二者同时为1时这一位的结果才为1,否则为0。
#include <stdio.h>
int main(){
int n = 1 & 5;
printf("%d\n", n);
return 0;
}
运行结果:
二进制对应的位相或,二者都为0时这一位的结果才为0,否则为1。
#include <stdio.h>
int main(){
int n = 1 | 5;
printf("%d\n", n);
return 0;
}
运行结果:
二进制对应的位相异或,二者相异时为1,相同时为0。
#include <stdio.h>
int main(){
int n = 1 ^ 5;
printf("%d\n", n);
return 0;
}
运行结果:
借助临时变量交换:
#include <stdio.h>
int main(){
int a = 5;
int b = 10;
//借助临时变量t
int t = 0;
t = a;
a = b;
b = t;
printf("a=%d,b=%d", a, b);
return 0;
}
不借助临时变量交换:
思路1:借用其中一个整数(a)存放两个整数(a+b)的和,再借助另一个整数(b)完成交换。 缺点:当a与b没有超出整型的范围但a与b的和超出了整型的范围时结果会发生错误。
#include <stdio.h>
int main(){
int a = 5;
int b = 10;
printf("a=%d,b=%d", a, b);
//a中存放a+b的值
a = a + b;
//b中得到原来a的值
b = a - b;
//a中得到原来b的值
a = a - b;
printf("a=%d,b=%d", a, b);
return 0;
}
思路2:借助按位异或操作符实现 一个知识点:异或满足交换律, 且有
a^b^a等价于a
,a^b^b等价于a
,任何整数与0异或均是自身,
#include <stdio.h>
int main(){
int a = 5;
int b = 10;
//a与b异或的结果存放在a中
a = a ^ b;
//相当于a^b^b,得到a,赋值给b
b = a ^ b;
//相当于a^b^a,得到b,赋值给a
a = a ^ b;
return 0;
}
运行结果:
借助移位操作符
<<
和按位与操作符&
。 思路1:对于int型在内存中占4个字节或8个字节(这里以4个字节为例子),32个bit,xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
。 考虑32个位按位逐个检查,借助1,其在内存中的补码为00000000 00000000 00000000 00000001
,1与一个整数按位与之后得到的结果的32个位只有最低位可能为1,其他位由于都是0,不管x是0还是1&
后都是0。故可以通过判断t & 1
的结果判断t二进制补码的最低位是否为1,若是0则最低位是0,若是非0则最低位为1。借助一个循环分别判断t的每一个二进制位即可。
#include <stdio.h>
int main() {
int t1 = 10;
int t2 = -10;
int i = 0;
int cnt1 = 0;
int cnt2 = 0;
for (i = 0; i < 32; i++) {
if (t1 & (1 << i)) {
cnt1++;
}
if (t2 & (1 << i)) {
cnt2++;
}
}
printf("cnt1 = %d, cnt2 = %d\n", cnt1, cnt2);
return 0;
}
运行结果:
#include <stdio.h>
int main(){
int num = -1;
int i = 0;
int cnt = 0;//计数
while(num){
cnt++;
num = num&(num-1);
}
printf("cnt = %d\n",cnt);
return 0;
}
= | 赋值 |
---|---|
+= | 加等 |
-= | 减等 |
*= | 乘等 |
/= | 除等 |
%= | 模等 |
&= | 按位与等 |
|= | 按位或等 |
^= | 按位异或等 |
<<= | 左移等 |
>>= | 右移等 |
一般赋值使用:
int a = 10;
double b = 3.14;
连续赋值:
int a,b,c;
a = b = c = 10;
//等价于
c = 10;
b = c;
a = b;
复合赋值操作符:
int a,b;
a = 5;
b = 10;
a += b;
//等价于a = a + b
a &= b;
//等价于a = a ^ b
+ | 正值 |
---|---|
- | 负值 |
++ | 自增(有前缀和后缀两种形式) |
– | 自减(有前缀和后缀两种形式) |
! | 逻辑取反 |
~ | 按位取反(二进制形式) |
& | 取地址 |
* | 解引用(间接访问) |
(类型) | 强制类型转换 |
sizeof | 操作数的类型长度(单位是字节) |
+:是一个操作符,但很少使用,对于整数不需要,对于负数不起作用。 -:把一个正数变为负数,把一个负数变为正数。
int a = 10;
a = +a;//a == 10
a = -a;//a == -10
int b;
b = -a;//b == 10
++:
int a = 10;
a++;
--a;
//等价于a = a + 1;
//等价于a += 1;
前缀形式
++a
符合先自增再使用,a先增加1,接着再使用a,用的是a自增后的值。 后缀形式a++
符合先使用再自增,a先使用,接着再自增a,用的是a自增前的值。
–:同++。
C语言在判断真假时,以0表示假,非0表示真。 对0进行逻辑取反
!0
结果是真(非0)。 对非零值进行逻辑取反如!10
结果是假(0)。
#include <stdio.h>
int main(){
int a = 0;
printf("!a = %d\n", !a);
int b = 10;
printf("!b = %d\n", !b);
return 0;
}
运行结果:
按一个整数的二进制补码形式取反,0变1,1变0。
int a = 10;
int b = ~a;
printf("b = %d\n", b);
00000000 00000000 00000000 00001010
10的二进制补码11111111 11111111 11111111 11110101
~1011111111 11111111 11111111 11110100
~10的反码10000000 00000000 00000000 00001011
~10的原码
运行结果:
取出操作数在内存中的地址。 操作数可以是变量,字符串常量,函数,数组,结构体等。
#include <stdio.h>
int main(){
int a = 10;
//创建一个指针变量p存放变量a的地址
int* p = &a;
printf("%p\n", &a);
printf("%p\n", p);
return 0;
}
运行结果:
*
得到变量a的值,获得使用和改变变量a的值的权限(如果对p没有主动做出限制的话)。#include <stdio.h>
int main(){
int a = 10;
int p = &a;
printf("*p = %d\n", *p);
*p = -10;
printf("%d\n", *p);
return 0;
}
运行结果:
计算的是操作数类型内存中所占字节的大小,以字节为单位。
注意:类型本身并不占内存的大小,即不在内存中开辟空间。但当创建了某种类型的变量时就会在内存中开辟该类型相应大小的空间。
#include <stdio.h>
int main(){
int a = 10;
printf("sizeof(a) = %d", sizeof(a));
printf("sizeof(int) = %d", sizeof(int));
//sizeof操作符的括号可以省略,但在计算类型时是不能省略的
printf("sizeof(a) = %d", sizeof a);
//printf("sizeof int = %d", sizeof int);此为错误用法
return 0;
}
运行结果:
在数组进阶中已经提到过,在回顾一下。 数组名一般情况下表示数组首元素的地址,但有两个例外。
#include <stdio.h>
void test(char s[]);
int main(){
char str[10] = {0};
//这里计算的是整个字符数组的大小
printf("sizeof(str) = %d\n", sizeof(str));
test(str);
return 0;
}
//这是个测试
//等价于
//void test (char* s)
void test(char s[]){
//这里计算的是一个字符指针的大小。
printf("sizeof(s) = %d\n", sizeof(s));
}
一个指针的大小可能是4字节或8字节。
运行结果:
sizeof
计算的是实际占内存中的储存单元的大小。sizeof
是操作符,strlen()
是C语言的(头文件string.h
)库函数。strlen()
函数计算的是字符串的长度,不包括字符串末尾的字符'0'
。sizeof
的括号可以去掉,但strlen()
的括号不能去掉,这是strlen()
函数的一个特征。一个例子:
#include <stdio.h>
#include <string.h>
int main(){
char str[20] = "Hello,World!";
printf("sizeof(str) = %d\n", sizeof(str));
printf("strlen(str) = %d\n", strlen(str));
return 0;
}
把操作数强制转换为想要的类型。若不使用强制类型转化可能会产生警告。
#include <stdio.h>
int main(){
//这种情况会产生警告信息,3.14被默认为浮点型数据,被赋值给int型变量a时会丢失精度,是隐式类 //型转换
int a = 3.14;
printf("a = %d\n", a);
//强制类型转化,明确把浮点型数据转换为int型数据
int b = (int)3.14;
printf("b = %d\n", b);
return 0;
}
> | 大于 |
---|---|
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
!= | 不等于 |
== | 等于 |
#include <stdio.h>
int main(){
double a = 3.14;
float b = 3.14;
//1e-5 == 0.00001
if(fabs(a-b) < 1e-5){
printf("a==b\n");
}
else if(a-b > 1e-5){
printf("a > b\n");
}
else{
printf("a < b\n");
}
return 0;
}
运行结果:
注意与按位与
&
按位或|
的区别:
&
、|
是对二进制位进行操作。&&
、||
关注的是值的真和假,不是二进制的位。&& | 逻辑与 |
---|---|
|| | 逻辑或 |
&&
、||
是双目操作符,有两个操作数。
对于
&&
,当两个操作数都为真时,表达式的结果才为真(1),其他情况均为假(0)。 对于||
,当两个操作数都为假时,表达式的结果才为假(0),其他情况均为真(0)。 这里的操作数可以使复杂的表达式。
例子:判断闰年
#include <stdio.h>
int main(){
int year = 0;
scanf("%d", &year);
if(year%4 == 0 && year%100 != 0 || year%400 == 0){
printf("isleap\n");
}
else{
printf("is not leap");
}
return 0;
}
a与b均表示表达式。
&&
,如a&&b
,当子表达式a为假(0)时,整个表达式的值就是0,后面的子表达式b不在计算,子表达式b相当于被短路了。||
,如a||b
,当子表达式a为真(非0)时,整个表达式的值就是1,后面的子表达式b不再计算,子表达式b相当于被短路了。格式:表达式1 ? 表达式2 : 表达式3
用法类似于ifelse语句,表达式1为真时整个表达式的值是表达式2的值,表达式1为假时整个表达式的值是表达式3的值。
一个例子:
#include <stdio.h>
int main() {
int a, b;
scanf("%d%d", &a, &b);
int max = a > b ? a : b;
printf("max = %d", max);
return 0;
}
运行结果:
格式:表达式1,表达式2,表达式3,...,表达式n;
逗号表达式的最终的值是表达式n的值,表达式n之前的表达式可能会影响表达式n的值,也就是影响逗号表达式的值。
一个例子:
#include <stdio.h>
int main() {
int a, b, c, d;
int t1 = (a = 5, b = 3, c = 1, d = 10);
int t2 = (a = 5, b = 3, c = 1, d = a + b + c);
printf("t1 = %d\n", t1);
printf("t2 = %d\n", t2);
return 0;
}
运行结果:
格式 数组名[索引值]
,操作数有两个:数组名与索引值(或者说下标)。索引值范围0~数组长度-1。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int a = arr[3];//3[arr]与arr[3]等价
访问时,这里的
arr[3]
与3[arr]
等价,下标引用操作符是二元操作符,两个操作数可以互换,类比其他二元操作符。但类似3[arr]不好理解。 arr[3]可以写为*(arr+3)或*(3+arr)
数组定义时则不行。
格式 函数名(可能有的一个或多个函数参数)
函数调用操作符的操作数至少有一个函数名,函数参数可能有一个或多个也可能没有函数参数。
一个例子:
#include <stdio.h>
int test1(int a, int b);
void test2();
int main(){
int a,b;
scanf("%d%d", &a, &b);
test1(a, b);
test2();
return 0;
}
//测试1
int test1(int a, int b){
return a > b ? a : b;
}
//测试2
void test2(){
printf("test2\n");
return;
}
运行结果:
. | 圆点访问操作符(结构体) |
---|---|
-> | 箭头访问操作符(结构指针) |
#include <stdio.h>
//定义一个学生的结构体类型并用typedef重命名为STU
typedef struct student{
char name[20];
char id[15];//学号
int score;
}STU;
int main() {
STU stu;
scanf("%s %s %d", stu.name, stu.id, &stu.score);
printf("%s %s %d\n", stu.name, stu.id, stu.score);
STU* p = &stu;
scanf("%s %s %d", stu.name, stu.id, &stu.score);
printf("%s %s %d\n", p->name, p->id, p->score);
return 0;
}
C的整型算术运算总是至少以缺省(或者说是默认)整型类型的精度进行的。
按照变量的数据类型的符号位进行提升的。
对于无符号整数高位直接补0 对于有符号整数
正数:char a = 1; 1的二进制补码为
00000001
整型提升(高位补0)之后00000000 00000000 00000000 00000001
负数:char b = -1; -1的二进制补码为11111111
整型提升(高位补1)之后11111111 11111111 11111111 11111111
#include <stdio.h>
int main(){
//char、short、int默认为有符号类型,若整型提升则进行的是有符号整数的整形提升
char a = 0xf6;
//11110110
short b = 0xf600;
//11110110 00000000
int c = 0xf6000000;
//11110110 00000000 00000000 00000000
//参与运算之前先对char、short进行整形提升为int
if(a == 0xf6){
//11111111 11111111 11111111 11110110
printf("a == 0xf6\n");
}
if(b == 0xf600){
//11111111 11111111 11110110 00000000
printf("b == 0xf600\n");
}
if(c == 0xf6000000){
//11110110 00000000 00000000 00000000
printf("c == 0xf6000000\n");
}
return 0;
}
运行结果:
#include <stdio.h>
int main(){
char a = 1;
printf("sizeof(a) = %u\n", sizeof(a));
//+a是一个表达式,sizeof计算的是a整形提升之后的大小,但+a、-a是实际上没有进行计算的。
printf("sizeof(+a) = %u\n", sizeof(+a));
//-a是一个表达式,sizeof计算的是a整形提升之后的大小
printf("sizeof(-a) = %u\n", sizeof(-a));
return 0;
}
同一个操作符的操作数类型不同时便无法继续进行运算,必须要转换为同一种类型才能继续进行算术运算。
long double | 多精度浮点类型(长精度浮点类型) |
---|---|
double | 双精度浮点型 |
float | 单精度浮点型 |
unsigned long int | 无符号长整型 |
long int | 长整型 |
unsigned int | 无符号整型 |
int | 有符号整型 |
若运算中出现多种类型的数据,优先转换顺序为:int ------>long double
如果不遵守优先转换顺序可能会造成数据精度丢失。
与表达式的属性相区别:值属性与类型属性。
两个相邻的操作符,先看优先级的高低,优先级高的先进行运算; 若优先级相同,则看结合性,按结合性规定的顺序进行运算。
注意:
例如:int a = 1*2 + 3*4 + 5*6;
有优先级知道: 第一个号比第一个+号先进行运算; 第二个号比第一个+号和第二个+号先进行运算; 第三个号比第二个+号先进行运算; 但: 不知道第一个号是否比第二个+号运算的早; 不知道第三个*号是否比第一个+号运算的早。
一个有歧义的表达式:
int a = b + --b;
知道
--
操作符比+
操作符先进行运算; 但 不知道b
与--b
两个操作数谁先进行。
闭坑指南:我们写表达式时要确定表达式有唯一确定的值,如果不确定就需要重新写或对表达式进行合理拆分,使其成为几个简单的表达式。
操作符 | 描述 | 用法 | 结果类型 | 结合性 | 是否控制求值顺序 |
---|---|---|---|---|---|
() | 括号 | (表达式) | 与表达式相同 | 无 | 否 |
() | 函数调用 | 函数名(函数参数) | 函数返回类型 | L | 否 |
[ ] | 下标引用 | 数组名[下标] | 数组元素类型 | L | 否 |
. | 结构成员访问 | 结构名.结构成员 | 结构成员类型 | L | 否 |
-> | (结构指针)箭头访问 | 结构指针->结构成员 | 结构成员类型 | L | 否 |
++ | 后缀自增 | a++ | 自身类型 | L | 否 |
– | 后缀自减 | a++ | 自身类型 | L | 否 |
! | 逻辑反 | !a | R | 否 | |
~ | 按位取反 | ~a | R | 否 | |
| 正值 | +a | 自身类型 | R | 否 |
- | 负值 | -a | 自身类型 | R | 否 |
++ | 前缀自增 | ++a | 自身类型 | R | 否 |
– | 前缀自减 | –a | 自身类型 | R | 否 |
* | 间接访问 | *a | 指针所指向的类型 | R | 否 |
& | 取地址 | &a | 指针类型 | R | 否 |
sizeof | 计算长度,单位字节 | sizeof(a) | 整型 | R | 否 |
(类型) | 强制类型转换 | (类型)a | 强制转换的类型 | R | 否 |
* | 乘法 | a*b | L | 否 | |
/ | 除法 | a/b | L | 否 | |
% | 模 | a%b | 整型 | L | 否 |
| 加法 | a+b | L | 否 | |
- | 减法 | a-b | L | 否 | |
<< | 左移 | a<<1 | 整型 | L | 否 |
>> | 右移 | a>>1 | 整型 | L | 否 |
> | 大于 | a>b | 整型 | L | 否 |
>= | 大于等于 | a>=b | 整型 | L | 否 |
< | 小于 | a<b | 整型 | L | 否 |
<= | 小于等于 | a<=b | 整型 | L | 否 |
== | 等于 | a==b | 整型 | L | 否 |
!= | 不等于 | a!=b | 整型 | L | 否 |
& | 按位与 | a & b | 整型 | L | 否 |
^ | 按位异或 | a ^ b | 整型 | L | 否 |
| | 按位或 | a | b | 整型 | L | 否 |
&& | 逻辑与 | a && b | 整型 | L | 是 |
|| | 逻辑或 | a || b | 整型 | L | 是 |
? : | 条件操作符 | a ? b : c | 无 | 是 | |
= | 赋值 | a = 1 | R | 否 | |
+= | 加等 | a += 1 | R | 否 | |
-+ | 减等 | a -= 1 | R | 否 | |
*= | 乘等 | *a = 1 | R | 否 | |
/= | 除等 | a /= 1 | R | 否 | |
%= | 模等 | a %= 2 | 整型 | R | 否 |
<<= | 左移等 | a <<= 1 | 整型 | R | 否 |
>>= | 右移等 | a >>= 1 | 整型 | R | 否 |
&= | 按位与等 | a &= 1 | 整型 | R | 否 |
^= | 按位异或等 | a ^ 1 | 整型 | R | 否 |
|= | 按位或等 | a |= 1 | 整型 | R | 否 |
, | 逗号 | a,b,c,…,n | L | 是 |
END