目录
前言
1.sizeof和数组(补充)
2.关系操作符
3.逻辑操作符
4.条件操作符
5.逗号表达式
6.下标引用、函数调用和结构成员
6.1 [ ] 下表引用操作符
6.2( ) 函数调用操作符
6.3访问一个结构的成员
7.表达式求值
7.1隐式类型转换
7.2算数转换
7.3操作符的属性
总结
前言
这里接着上一篇的文章内容接着学习和讲解,基本上本篇就把操作符这一部分全部讲解完毕。
这里我们接着再讲一下sizeof和数组之间的关系和使用方法。
1.sizeof和数组(补充)
#include <stdio.h>
void test1(int arr[])
{printf("%d\n",sizeof(arr));
}
void test2(char ch[])
{printf("%d\n",sizeof(ch));
}int main( )
{int arr[10]={0};char ch[10]={0};printf("%d\n",sizeof(arr));printf("%d\n",sizeof(ch));test1(arr);test(ch);return 0;
}
这里创建了两个不同类型的数组,一个是整形,一个是字符型,同时这里写了两个函数,在主函数里面调用这两个函数。
第一条打印数据因为sizeof里面是数组名,这里计算的是数组的大小,因为arr是十个整形元素,一个整形元素是4个字节,所以打印出来就是40,下一条打印也是计算数组的大小,因为ch里面有十个字符型元素,字符型元素占1个字节,所以这里打印的就是10。
而test1和test2是函数,函数传参传入的是数组名,而数组名是首元素的地址,也就是传入的是个指针,sizeof里面计算的是指针的大小,也就是4或者8,因为32位平台下指针位是4个字节,64位平台下指针位是8个字节,test2传入的也是指针,所以输出也是4或者8个字节。
只要是地址(指针)就是4或者8个字节,VS中X86就是32位环境,X64是64位环境。
2.关系操作符
>
>=
<
<=
!= 用来测试“不相等”
== 用于测试“相等”
这些关系运算符都比较简单,没有什么可以进行讲解的,但是我们要注意一些运算符使用的时候的陷阱。
注意:在编程的时候==和=不小心写错,导致的错误。
C语言中两个相等是用来判断的,比较的,但是一个等号就是赋值。
3.逻辑操作符
逻辑符号:
&& 逻辑与
| | 逻辑或
逻辑与又称为并且,逻辑或又称为或者,之前的按位与(&)和按位或(|)是通过二进制进行运算的。但这里的逻辑与和逻辑或是针对于真假来进行的。
对于逻辑与(&&),两边都为真才为真,有假就为假。
对于逻辑或(| |),有真就为真,两个为假才为假。
这里可以用自己的方法来记住,只要理解为并且和或者就可以,中文语义并且就是两个都的意思,或者就是只要有一个就行。
这里来看下列代码:
int main( )
{int a=10; //int a=0;int b=5; //int b=0if(a && b){pritnf("ok\n");}return 0;
}
这里两个都为真,才会打印ok,如果把其中的一个数改成0,那么就不会打印ok。
int main( )
{int a=10; //int a=0;int b=5; //int b=0if(a || b){pritnf("ok\n");}return 0;
}
这里就是只要两个有一个非0,也就是有一个为真,那么就会打印ok,只有两个都为假的时候才不会打印ok。
这里我们就可以通过这两个运算符来写一个判断是否为闰年的程序了,就不用套好几层 if 了:
int main( )
{int year=0;scanf("%d\n",&year);if(((year % 4 == 0) && ( year % 100 !=0)) || ( year % 400 ==0)){printf("Yes\n");}return 0;
}
逻辑与(并且)操作符来说,有一个特点,这里用一个程序来进行讲解:
int main( )
{int i = 0, a = 0, b = 3, d = 4;i = a++ && ++b && d++;pritnf("a = %d\nb = %d\nc = %d\nd = %d\n",a,b,c,d);return 0;
}
这里可以自己先思考一下,之后再看解析。
这里我们都以为输出是1 3 3 4,但是错了,正确答案是1 2 3 4 ,这里因为a++,因为是后置加加,所以这里还是0,0后面跟上逻辑与,由于逻辑与的特点就是里面要是有假的话,那么就为假。而逻辑与的特点就是左边(前面)只要有假了,后面就不需要进行运算了,只有左面(前面)为真的时候,才会继续往后进行判断。
逻辑或(或者)操作符来说:
int main( )
{int i = 0, a = 1, b = 2, c = 3, d = 4;i = a++ || ++b || d++;pritnf("a = %d\nb = %d\nc = %d\nd = %d\n",a,b,c,d);return 0;
}
对于逻辑或来说,左边要是为真,右边就无需进行计算,所以最后的结果就是2 2 3 4。因为在a++的时候因为是后置加加,所以只判断a,a这里是真,a判断完成之后再进行加1,因为第一个就为真了,所以后面无需进行计算,最后输出也就是 2 2 3 4。
4.条件操作符
exp1 ? exp2 : exp3
这里exp叫做表达式,上面就是三个表达式,如果表达式1的结果为真,那么表达式2就计算,表达式3不计算;如果表达式1为假,那么表达式2不计算,表达式3计算。
条件操作符也叫做三目操作符,涉及到了三个操作数。
可以看下面的例子:
int main( )
{int a =10;int b =20;int m =0;if(a>b)m =a;elsem =b;m=(a>b?a:b);return 0;
}
这里上面的一对代码就可以用下面的一行来搞定,如果a>b就执行a,如果不符合,就是b,所以这就是三目操作符。
5.逗号表达式
exp1, exp2, exp3, .... expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左到右依次执行。整个表达式的结果就是最后一个表达式的结果。
看下面的一个例子:
int a =1;
int b =2;
int c =(a>b, a=b+10, a, b=a+1);
表达式从左到右依次运算,最后的结果就是最后的一个表达式的结果。上述代码从左到右,第一个表达式没有影响,第二个表达式a被赋值为b+10,也就是赋值了12,表达式3就是a+1,也就是13。
if(a =b+1, c=a/2, d>0)
这里也是从左到右依次运算,最后的结果还是表达式3的最后的结果,所以这里if语句真正判断的就是表达式3(d>0)的条件。
接着再看一个例子:
a = get_val( );
count_val(a);
while(a>0)
{//业务处理a =get_val( );count_val(a);
}
这里是先对a进行赋值,之后再调用这个函数,再判断循环执行循环里面的语句。则这几行语句就可以通过逗号表达式改成下面的语句:
while(a =get_val( ), count_val(a), a>0)
{//业务处理
}
这里就可以用这一行来代替上面的这一堆代码。
6.下标引用、函数调用和结构成员
6.1 [ ] 下表引用操作符
操作数:一个数组名+一个索引值
int main( )
{int arr[10]={1,2,3,4,5};printf("%d\n",arr[4]);return 0;
}
这个[ ]就是下标引用操作符,这里面操作数就是arr和4,这里就是打印下标为4的数,也就是5。
6.2( ) 函数调用操作符
接受或者多个操作数:第一个操作数是函数名,剩余的操作数就是传给函数的参数。
int main( )
{int len = strlen("abcdef");return 0;
}
这里就是用了string库里面的strlen函数,()就是函数的调用操作符,操作数就是:strlen,"abcdef",函数调用操作符至少有一个操作数。
6.3访问一个结构的成员
. 结构体.成员名
-> 结构体指针->成员名
这里简单的讲一下结构体,后续还会进行深入讲解:
结构体(自定义类型)(聚合类型),生活中有些对象要被描述的话,就不能简单的使用内置类型,比如我们要描述一本书,那不可能只描述一个信息,例如:书名,作者,出版社,定价等等。
例如下列代码:
//类型
struct Book
{char name[20];//书名char author[30];//作者int price;
};int main( )
{struct Book b1={"C语言教程","张三",1000};struct Book b2={"这是书","李四",999};print("《%s》 %s %d\n",b1.name,b1.author,b1.price);return 0;
}
通过定义一个类型结构体,在结构体里面加入成员,定义每个成员,之后在主函数里面进行初始化,初始化过后就可以通过 . 来访问,分别访问里面的成员。
如果是结构体指针的话:
void printf(struct Book * p)
{printf("%s %s %d\n",(*p).name,(*p).author,(*p).price);printf("%s %s %d\n",p->name,p->author,p->price);
}
就用->后面接成员名来进行访问。结构体变量就用 . 进行访问。
7.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定。同样,有些表达式的操作数在求值的过程中可能需要转换为其它类型。
7.1隐式类型转换
C的整形算数运算总是至少以缺省整形类型的精度来进行的。为了获得这个精度,表达式中的字符和短整形操作数在使用之前被转换为普通整形,这种转换称为整形提升。
整形类型是用于类型小于整形的。
我们知道char 1个字节 short 2个字节 int 4个字节
整型提升就是把char或者是short类型变成int类型,之后再参与运算。
就是偷偷的进行着类型转换,它是在一直运行着,只是我们看不见。
int main( )
{char a =3;char b=127;char c= a + b;printf("%d\n",c);return 0;
}
上述代码就是一个例子。a,b都要提升为整形,之后才会进行运算。
整形提升的意义:
表达式的整形运算要在CPU的相应运算器件内执行,CPU内整形运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型相加,在CPU执行时实际上也要先转换为CPU内整形操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int 长度的整形值,都必须先转换为int或者unsigned int,然后才能送入CPU去执行运算。
如何进行整型提升?
假如是负数,那么提升整形的时候,高位补充符号位,负数的符号位是1,所以补的是1,如果有一个字符类型的负数,字符类型是一个字节,一个字节八个比特位,所以高位补充1也就是还需要前面补上24个1。
假如是正数的话,那么提升整形的时候,高位补充符号位,正数的符号位是0,所以高位补0。之后的与负数一样。
对于无符号整形的提升,高位补的是0。
提升之后是补码,还要转化成原码,也就是减一取反。(或者先取反再加1)
char 有符号取值范围:-128~127
无符号的取值范围:0~255
假设这里是有符号的char,因为char是一个字节(八个比特位),八个比特位的所有可能是总共256种,最高位是符号位,符号位为0的是正数,符号位为1的是负数,正数就按补码来读就行,也就到01111111,也就是127,所以正数就是0~127,负数因为是补码,所以先减去一,再按位取反,11111111就是-1,10000001就是-127,10000000直接被解析成-128,所以范围就是-129~127。
假设这里是无符号的char,所以就没有符号了,也就是0~255。
一般不同类型进行整型提升后它也就与之前的不同了,也就是不相等了,但是如果之前就是整形,那么就不会发生整形提升,比较后不变任然相等。
7.2算数转换
如果某个操作符的各个操作数属于不同类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算数转换:
long double
double
unsigned long int
long int
unsigned int
int
例如如果计算一个int和一个float类型,那么就先把int转换成float类型,之后再进行计算,按照上面的顺序向上转换,这也是一个隐式转换。
如果某个操作数的类型在上面这个列表中排名较低,那么就要先转换成那个排名高的那个类型,之后再进行执行运算。
算数转换要合理,要不然会出现一些潜在问题:
float f =3.15;
int num = f;
这里发生隐式转换,但是发生了精度缺失。
7.3操作符的属性
复杂表达式求值有三个影响的因素:
1.操作符的优先级
2.操作符的结合性
3.是否控制求值顺序
两个相邻的操作符先执行哪一个,取决于它们的优先级,如果两者的优先级相同,取决于它们的结合性。
下面就是操作符优先级的表:
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[长度] | 从左往右 | |
() | 小括号 | (表达式)或 函数名(形参表) | |||
. | 取成员 | 结构体名.成员 | |||
-> | 指针 | 结构体指针->成员 | |||
2 | - | 负号运算符 | -表达式 | 从右往左 | 单目运算符 |
() | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量或变量++ | 单目运算符 | ||
-- | 自减运算符 | --变量或变量-- | 单目运算符 | ||
* | 取内容 | *指针变量 | 单目运算符 | ||
& | 取地址 | &变量名 | 单目运算符 | ||
! | 逻辑非 | !表达式 | 单目运算符 | ||
~ | 按位取反 | ~整型表达式 | 单目运算符 | ||
sizeof | 求长度 | sizeof(表达式) | 单目运算符 | ||
3 | / | 除 | 表达式 / 表达式 | 从左往右 | 双目运算符 |
* | 乘 | 表达式 * 表达式 | 双目运算符 | ||
% | 取余 | 表达式 / 表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 从左往右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 从左往右 | 双目运算符 |
>> | 右移 | 变量<<表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 从左往右 | 双目运算符 |
>= | 大于或等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于或等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 从左往右 | 双目运算符 |
!= | 不等于 | 表达式!=表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 从左往右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 从左往右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 从左往右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 从左往右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 从左往右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 从右往左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 从右往左 | 双目运算符 |
/= | 除后再赋值 | 变量/=表达式 | |||
*= | 乘后再赋值 | 变量*=表达式 | |||
%= | 取余后再赋值 | 变量%=表达式 | |||
+= | 加后再赋值 | 变量+=表达式 | |||
-= | 减后再赋值 | 变量-=表达式 | |||
<<= | 左移再赋值 | 变量<<=表达式 | |||
>>= | 右移再赋值 | 变量>>=表达式 | |||
&= | 按位与再赋值 | 变量&=表达式 | |||
^= | 按位异或再赋值 | 变量^=表达式 | |||
|= | 按位或再赋值 | 变量|=表达式 | |||
15 | , | 逗号表达式 | 表达式,表达式,… | 从左往右 |
如果优先级不知道,就可以加括号。
只有相邻符号才讨论优先级。优先级高的先算,优先级低的后算。优先级相同的时候取决于结合性。有一些操作符会影响求值顺序,逻辑与、逻辑或、条件逻辑符、逗号表达式,都可以控制求值顺序。
注意在写代码时候的时候,不要写问题代码,那样的话编译器会不知道该如何运行,最后的结果就会出现错误,有些问题代码而且在不同的编译器下结果都不同。
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。
总结
这就是所有操作符的讲解和分析。