学习C语言中的各种操作符的使用方法及使用场景,可以让我们对于底层编译器的运算逻辑和过程更加清楚,同时能够减少开发中出现的小错误。本篇博客详细总结C语言中的操作符的使用方法、适用场景、及注意事项,学完本篇博客,可以让我们对于各种运算更加清晰,这也是开发人员的基本功。
操作符分类:
进位制/位置计数法是一种记数方式,故亦称进位记数法/位值计数法,可以用有限的数字符号代表所有的数值。可使用数字符号的数目称为基数或底数,基数为n,即可称n进位制,简称n进制。现在最常用的是十进制,通常使用10个阿拉伯数字0-9进行记数。 对于任何一个数,我们可以用不同的进位制来表示。常用的进制有:二进制、八进制、十进制、十六进制,在计算机中,数据的存储其实存储的是数据的二进制!整数在内存中存放的是数据的补码,因此在内存中对于整数的运算也是以补码进行运算的。下期博客总结数据在内存中的存储,相信会有更加深刻的理解!加油!
数据在内存中都是以二进制的形式存储的,对于整数来说,整数二进制有3种表示形式:原码、反码、补码;有符号整数的三种表示方法均有符号位和数值位两部分,二进制序列中,最高位的1位是被当做符号位,剩余的都是数值位。符号位都是用0表示“正”,用1表示“负”。对于无符号的整数来说,三种表示方法所有位均是数值位。
(1)正整数:原码、反码、补码相同;
(2)负整数:原码、反码、补码必须要进行计算,按照如下规则进行计算:
- 按照数据的数值直接写出的二进制序列就是原码;
- 原码的符号位不变,其他位按位取反,得到的就是反码;
- 反码+1,得到的就是补码;
- 补码得到原码也是可以使用:取反,+1的操作。
对于整数来说,整数中在内存中存储的是补码,这是因为:在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一计算处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
//举例理解为什么存放的是补码? #include
int main() { // 1-1=0如何在内存中计算?CPU只有加法器,因此执行以下运算: 1-1相当于1+(-)1 //方式1:假设存储的是原码 //1的原码:00000000 00000000 00000000 00000001 //-1的原码:10000000 00000000 00000000 00000001 //结果为:10000000 00000000 00000000 00000010 (-2) 不正确 //方式2:假设存储的是反码 //1的反码:00000000 00000000 00000000 00000001 //-1的反码:11111111 11111111 11111111 11111110 //结果为:11111111 11111111 11111111 11111111 (一个非常大的负数) 不正确 //方式3:假设存储的是补码 //1的补码:00000000 00000000 00000000 00000001 //-1的补码:11111111 11111111 11111111 11111111 //结果为:100000000 00000000 00000000 00000000 上述为33位,最高位会被舍弃,因此最终结果为: 00000000 00000000 00000000 00000000 (0) 结果正确 //因此,如果使用补码,计算更加合适! return 0; }
因此,在计算机内部,数据是以二进制的补码进行存储的,在进行计算的过程当然也是以二进制的补码进行计算的,这是由于数据写入内存与IO设备连接是通过总线通断电来传输数据的,内存条是一个非常精密的部件,包含了上亿个电子元器件,它们很小,达到了纳米级别。这些元器件,实际上就是电路;电路的电压会变化,要么是 0V,要么是 5V,只有这两种电压。5V 是通电,用1来表示,0V 是断电,用0来表示。所以,一个元器件有2种状态,0 或者 1。前面指针初识的时候学过,内存条划分的基本单位为一个字节byte(8个比特位),因此才会有各种数据类型,一个原因是:描述生活中的各种数据,另一个原因是:节省内存使用,更加高效的使用内存。
内存的换算单位如下:
为方便快捷进行二进制和十进制的转换,请记住以下数据:
2^0=1; 2^1=2; 2^2=4; 2^3=8; 2^4=16; 2^5 =32;
2^6=64; 2^7=128; 2^8=256; 2^9=512; 2^10=1024;
//举例理解正数和负数在内存中存放
#include
int main()
{
int a = 200; //正数的原码反码补码均相同
int b =-200; //负数便需要进行计算
//a和b在内存中的存储如下:4个字节,一共32个比特位:
// a的补码: 00000000 00000000 00000000 11001000 (2^7+2^6+2^3=128+64+8=200)
// b的原码:10000000 00000000 00000000 11001000 (与正数相比,只是符号位变成1了 )
// b的反码:11111111 11111111 11111111 00110111
// b的补码:11111111 11111111 11111111 00111000 (内存中存放的便是这个二进制序列)
//注意:补码往回推原码方法相同!
return 0;
}
生活中一个数字默认就是十进制的,表示一个十进制数字不需要任何特殊的格式。但是,表示一个二进制、八进制或者十六进制数字就不一样了,为了和十进制数字区分开来,必须采用某种特殊的写法,具体来说,就是在数字前面加上特定的字符,也就是加前缀。进制也就是进位制。进行加法运算时逢X进一(满X进一),进行减法运算时借一当X,这就是X进制,这种进制也就包含X个数字,基数为X。十进制有 0~9 共10个数字,基数为10,在加减法运算中,逢十进一,借一当十。
二进制由 0 和 1 两个数字组成,使用时必须以0b
或0B
(不区分大小写)开头,二进制数运算规律是逢二进一。
在计算机内部,数据都是以二进制的形式存储的,二进制是学习编程必须掌握的基础。
二进制加减法和十进制加减法的思想是类似的:
- 对于十进制,进行加法运算时逢十进一,进行减法运算时借一当十;
- 对于二进制,进行加法运算时逢二进一,进行减法运算时借一当二。
八进制由 0~7 八个数字组成,使用时必须以0
开头(注意是数字 0,不是字母 o),八进制数运算规律是逢八进一。
八进制有 0~7 共8个数字,基数为8,加法运算时逢八进一,减法运算时借一当八。
十进制由 0~9 十个数字组成,没有任何前缀,和我们平时的书写格式一样,十进制数运算规律是逢十进一,不再赘述。
在printf函数中%d打印的便是有符号的十进制数(因此要把补码转换成原码,然后计算对应的十进制数才是结果),%u打印的是无符号的十进制数(因此,它的所有位都是数值位,直接按位加权求和即可!)
十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写,它们分别表示十进制数10~15),组成,使用时必须以0x
或0X
(不区分大小写)开头 。十六进制数运算规律是逢十六进一。
十六进制中,用A来表示10,B表示11,C表示12,D表示13,E表示14,F表示15,因此有 0~F 共16个数字,基数为16,加法运算时逢16进1,减法运算时借1当16。
在C语言中,地址占4个字节,一共32个比特位,为了展示方便通常用十六进制,因此8位十六进制数便对应着一个地址编号!
二进制、八进制和十六进制向十进制转换都非常容易,就是“按权相加”。所谓“权”,也即“位权”。
假设当前数字是 N 进制,那么:
- 对于整数部分,从右往左看,第 i 位的位权等于N^i-1
- 对于小数部分,恰好相反,要从左往右看,第 j 位的位权为N^-j。
更加通俗的理解是,假设一个多位数(由多个数字组成的数)某位上的数字是 1,那么它所表示的数值大小就是该位的位权。
将十进制转换为其它进制时比较复杂,整数部分和小数部分的算法不一样,下面我们分别讲解。
下表列出了前 17 个十进制整数与二进制、八进制、十六进制的对应关系:
其实,任何进制之间的转换都可以使用上面讲到的方法,只不过有时比较麻烦,所以一般针对不同的进制采取不同的方法。将二进制转换为八进制和十六进制时就有非常简洁的方法,反之亦然。
//举例理解对于负数的转换(内存中不论是正数还是负数,都是以补码进行计算,不过整数的原码反码补码均相同)
//十进制数-100转二进制:
-100的原码:10000000 00000000 00000000 01100100
-100的反码:11111111 11111111 11111111 10011011
-100的补码:11111111 11111111 11111111 10011100
//因此-100的二进制为:11111 11111111 11111111 10011100
//二进制数(有符号) 11011100转十进制数
//最高位为符号位代表这是负数,然后推回原码,计算对应的十进制数
//按位取反: 10100011
//加1得到原码: 10100100进而计算十进制数 -(2^5+2^2=32+4)=-36
同数学运算一样,C语言中也有 +、- 、*、/、%算术操作符,/ 结果为商的部分,%为余数。
注意事项:
- 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
- % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
#include
int main()
{
/*第一种:结果为:1
int a = 6 / 5; //1.2
printf("%d\n",a);
第二种:结果为:1.000000
float a=6/5;
printf("%f\n",a);
第三种:结果为:1.200000
float a = 6.0/5;
printf("%f\n",a);
第四种:结果为:1.200000
float a = 6/5.0;
printf("%f\n", a);
第五种:结果为:1.200000
float a = 6.0/5.0;
printf("%f\n", a);*/
return 0;
}
- >>:右移操作符
- <<:左移操作符
注意事项:
移位操作符针对的是该数在内存中的补码,移位操作符的操作数只能是整数。并且对于位移运算不要移动负数位,这个是标准未定义的!
int num=10; num>>-1;//error
移位规则:左边抛弃、右边补0
#include
int main()
{
int num = 10;
int n = num << 1;
printf("n=%d\n", n);
printf("num=%d", num);
return 0;
}
移位规则:首先右移运算分两种:
1.逻辑右移:左边用0填充,右边丢弃
2.算术右移:左边用原该值的符号位填充,右边丢弃
#include
int main()
{
int num = 10;
int n = num >> 1;
printf(" n=%d\n", n);
printf(" num=%d\n", num);
return 0;
}
从以上结果可以看出,VS编译器遵从的是算术右移,且-1的补码全是1(当作常识)
& 按位与(同为1,结果为1)
| 按位或 (有1个1,结果为1)
^ 按位异或 (不同为1,相同为0)
注意事项:
注:他们的操作数必须是整数,同时这些运算同样在内存中以补码形式计算。
应用编程技巧:
1.两个相同的数(自身和自身异或)异或结果为0,0和任何数异或结果仍为这个数!!!
#include
int main() { int a = 3; //这里为了书写方便只写四位 printf("%d",a^a); //a:0011 0011 异或结果: 0000,结果为0 printf("%d",a^0); //a:0011 0000 异或结果: 0011,结果仍为3 return 0; } 2. 结合移位操作符和按位操作符,会有意想不到的作用效果,详见练习.
(1与任何位按位与不改变这个位,0与任何位按位与为0,不论这个位是什么,想当于置0)
(0与任何位按位或不改变这个位,1与任何位按位或为1,不论这个位是什么,想当于置1)
1的补码为: 00000000 00000000 00000000 00000001
-1的补码为:11111111 11111111 11111111 11111111 (全为1)
作为常识记住!结合移位操作符可以产生不同的作用!
#include
int main()
{
int num1 = -3;
int num2 = 5;
//-3的补码计算如下:
原码:10000000 00000000 00000000 00000011
反码:11111111 11111111 11111111 11111100
补码:11111111 11111111 11111111 11111101
//5的补码:
补码:00000000 00000000 00000000 00000101
printf(" %d \n", num1 & num2);
//按位与
11111111 11111111 11111111 11111101
00000000 00000000 00000000 00000101
结果为:00000000 00000000 00000000 00000101 (是个正数原码补码反码相同:5)
printf(" %d \n", num1 | num2);
//按位或
11111111 11111111 11111111 11111101
00000000 00000000 00000000 00000101
结果为:11111111 11111111 11111111 11111101(补码)
则往回推反码: 10000000 00000000 00000000 00000010
原码: 10000000 00000000 00000000 00000011 (-3)
printf(" %d \n", num1 ^ num2);
//按位异或
11111111 11111111 11111111 11111101
00000000 00000000 00000000 00000101
结果为: 11111111 11111111 11111111 11111000 (补码)
则往回推反码: 10000000 00000000 00000000 00000111
原码: 10000000 00000000 00000000 00001000 (-8)
return 0;
}
题目一:编写代码实现:不能创建临时变量(第三个变量),实现两个数的交换。
//解法一:酱油和醋颠倒做法,但是它申请了临时变量tmp
#include
int main()
{
int a = 1;
int b = 2;
printf("交换前:a=%d,b=%d\n", a, b);
int tmp = a;
a = b;
b = tmp;
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
//解法二:利用数据的运算,但是数据过大时,会发生溢出
#include
int main()
{
int a = 1;
int b = 2;
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;
}
//解法三:利用位运算
#include
int main()
{
int a = 1;
int b = 2;
printf("交换前:a=%d,b=%d\n", a, b);
a = a ^ b;
b = a ^ b; //这一步相当于b=a^b^b <=> b=a
a = a ^ b;
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
//可以理解为:
第一步得到中间密码,这个密码和b异或得到a赋值给b,在拿这个密码和此时的b异或得到b赋值给a
题目二:编写代码实现:求一个整数存储在内存中的二进制中1的个数。
//解法一:计数器思想,移位逐个判断
#include
int main()
{
int num = 10;int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2; //相当于右移1位num=num>>1
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
//解法二:
#include
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) ) //-1的补码:11111111 11111111 11111111 11111111
//1的补码 :00000000 00000000 00000000 00000001
count++;
}
printf("二进制中1的个数 = %d\n",count);
return 0;
}
//解法三:
#include
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;
}
题目三:编写代码实现:把整型数字13的二进制数,从低到高的第五位由0改成1。
#include
int main()
{
int a=13;
a=a|(1 << 4);
printf("a =%d\n",a);
//a的补码: 00000000 00000000 00000000 00001101
//1的补码: 00000000 00000000 00000000 00000001
//1左移4位: 00000000 00000000 00000000 00010000
(0与任何位按位或不改变这个位,1与任何位按位或为1,不论这个位是什么,想当于置1)
//a|(1 << 4)00000000 00000000 00000000 00011101 (29)
return 0;
}
赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
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;
这样的写法是不是更加清晰爽朗而且易于调试。
注意事项:
==是判断是否相等
=是赋值操作符
1.比较两个字符串是否相等,使用的是strcmp()字符串比较函数,而不能使用==
2.对字符串赋值不能使用=,使用的是strcpy()字符串拷贝函数,而不能使用=
原因在于:字符串名是字符串首元素的起始地址,是一个常量,不可以作为左值进行赋值!
复合赋值符:
+=、 -=、 *=、 /=、 %=、 >>=、 <<=、 &=、 |=、 ^=、
int x = 10; x = x+10; x += 10;//复合赋值 //其他运算符一样的道理。这样写更加简洁。
单目操作符指的是只有一个操作数的操作符。主要包括以下操作符:
在C语言的逻辑表达中,非零表示真,零表示假;使用场景如下:
#include
int main()
{
int flag = 5;
//flag为真,做某事
if (flag)
{
//执行语句
}
//flag为假,做某事
if (!flag)
{
//执行语句
}
return 0;
}
一个数如果想变成负数直接加负号即可,这和数学上一样,正号可以省略。
C语言中的sizeof
操作符用来计算数据类型或表达式所占用的内存字节数。
注意事项:
sizeof()是操作符而不是函数,因为这个括号可以省略,一般不省略,而在函数进行调用时,括号不可以省略!
语法格式:sizeof(类型名/或者变量名);
sizeof运算符计算的不是表达式的值,而是类型的大小。更准确地说,
sizeof
运算符返回它操作数类型的大小,单位为字节。如果操作数是一个类型,那么它直接返回该类型的大小;如果操作数是一个表达式
,那么它返回表达式类型的大小。表达式计算和赋值可能涉及类型转换。在sizeof(a + b)中:
- a + b是一个表达式,计算结果是int类型,sizeof不计算a + b的实际值,而是直接返回其类型int的大小。
- sizeof(s = c + 1); s = c + 1是一个赋值表达式,计算c+1的值,结果是int类型。但赋值的对象是short类型变量s,所以sizeof返回short类型的大小,即2字节,sizeof返回类型大小,不计算表达式值。
原因在于:
sizeof 在代码进行编译的时候,就根据表达式的类型确定了,类型的常用,而表达式的执行却要在程序运行期间才能执行,在编译期间已经将 sizeof 处理掉了,所以在运行期间就不会执行表达式了
注意:
正确做法是:在主函数利用公式计算好数组大小作为一个参数传递给被调函数!!
规则:按位取反操作符是把一个数的二进制补码的0变成1,1变成0
#include
int main()
{
int a=-1;
int b=~a;
//a的补码为:11111111 11111111 11111111 11111111 (-1)
//a进行取反:00000000 00000000 00000000 00000000 (0)
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
#include
int main()
{
int a=13;
a = a & ~(1<<3);
//13的补码:00000000 00000000 00000000 00001101
// 1的补码:00000000 00000000 00000000 00000001
//1左移3位:00000000 00000000 00000000 00001000
//按位取反:11111111 11111111 11111111 11110111
//(1与任何位按位与不改变这个位,0与任何位按位与为0,不论这个位是什么,想当于置0)
a = a & ~(1<<3):00000000 00000000 00000000 00000101 (5)
printf("%d",a);
return 0;
}
这两种操作符分为前置和后置,并且针对的是变量的自增或者自减;使用方法:
(1)单独构成一条语句,前置++和后置++结果相同(自减一样)
(2)不单独构成一条语句,和其他运算符结合使用或和函数传参结合使用时:
前置++(++num):先取num的值,自增1,后和其他运算符结合;
后置++(num++): 先取num的值,然后和其他运算符结合,后自增1;
//前置++和--
#include
int main()
{
int a = 10;
int x = ++a;
//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
int y = --a;
//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
return 0;
}
//后置++和--
#include
int main()
{
int a = 10;
int x = a++;
//先对a先使用,再增加,这样x的值是10;之后a变成11;
int y = a--;
//先对a先使用,再自减,这样y的值是11;之后a变成10;
return 0;
}
在C语言中,取地址操作符
&
用于获取变量的地址。它是一元运算符,放置在变量名之前,用于返回该变量在内存中的地址。深入理解:地址就是指针,它所指向内存中的某一块内存单元,使用指针就是使用指针变量存放的地址,通过对指针变量进行解引用,便可以访问这块内存单元,从而对该内存单元存放的数据进行修改!
像数组、字符串、动态分配的内存都是一系列数据集合,没有办法通过一个参数传入函数的内部,只能传递他们的指针,也就是地址,在函数内部通过指针解引用来修改或者访问这些数据集合!!!
#include
int main()
{
int a = 10;
int *pa = &a;//将取出a的地址存放在整型指针变量p中,保存的是整型变量的地址
*pa = 0;
//解引用操作,通过pa里面存放的地址找到a,是一种间接访问方法,从而修改a的值,相当于a=0
return 0;
}
强制类型转换有四条规则:
(1) 大数据类型转换为小数据类型,必须显式转换(要有强制转换符号)
(2) 高精度数据类型转换为低精度数据类型,必须显式转换(要有强制转换符号)
(3) 小数据类型转换为大数据类型,不需要显式转换,编译器自动转换(空位用符号位填充)
(4)指针也可以强制转换,这与指针加1的能力有关!
用于比较两边操作数的大小关系,比较简单,注意=和==的区别!
在c语言中,非零即为真,零为假;
注意:逻辑操作符是短路运算符,前面操作数成立后面的则不会计算!
比如对于逻辑与:前面操作数如果逻辑为假(0值)后面的则不会计算!对于逻辑或:前面操作数如果逻辑为真(非0值)后面的则不会计算!
#include
int main()
{
int a=1;
int b=2;
printf("%d\n",a&&b);//逻辑与 两个都非零为真,结果为1
printf("%d\n",a&b); //按位与
printf("%d\n",a||b);//逻辑或 两个都非零为真,结果为1
printf("%d\n",a|b); //按位或
return 0;
}
//a的补码:00000000 00000000 00000000 00000001
//b的补码:00000000 00000000 00000000 00000010
//a&b: 00000000 00000000 00000000 00000000 (0)
//a|b: 00000000 00000000 00000000 00000011 (3)
#include
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
逻辑与为短路运算符,只要前面的表达式逻辑为假,后面则不会参与运算!
#include
int main()
{
int i = 0,a=0,b=2,c =3,d=4;
i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
逻辑或同样为短路运算符,只要前面的表达式逻辑为假,后面则不会参与运算!
#include
int main()
{
int i = 0,a=1,b=2,c =3,d=4;
i = a++ && ++b && d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
#include
int main()
{
int i = 0,a=1,b=2,c =3,d=4;
i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
//程序输出的结果是什么?
作用如下:表达式1成立,计算表达式2,整个表达式的结果是表达式2的结果,如果表达式1不成立,计算表达式3,整个表达式的结果是表达式3结果;
用法:可以用来替换双分支if...else...使代码更加简便。
#include
int main()
{
printf("请输入两个数\n");
int a, b;
int max;
scanf("%d%d", &a, &b);
if (a > b)
{
max = a;
}
else
{
max = b;
}
printf("这两个数中的较大数为:%d\n", max);
return 0;
}
//另一种写法
#include
int main()
{
printf("请输入两个数\n");
int a, b;
int max;
scanf("%d%d", &a, &b);
max=a>b?a:b;
printf("这两个数中的较大数为:%d\n", max);
return 0;
}
逗号表达式,就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
虽然有四个表达式组成逗号表达式,但是整个表达式的结果是最后一个表达式的结果,即b=a+1这个表达式的结果为13,赋值给c所以c的值是13。
//代码2
if (a =b + 1, c=a / 2, d > 0)
这个if判断是对逗号表达式进行判断,单从if进行判断的角度来看可以等价于if(d>0)
//代码3
a = get_val();
count_val(a);
while (a > 0)
{
//业务处理
a = get_val();
count_val(a);
}
代码结构冗杂,a = get_val();count_val(a);连续出现两次,分析结构后我们可以使用逗号表达式改写成此代码的等价形式:
如果使用逗号表达式,改写:
while (a = get_val(), count_val(a), a>0)
{
//业务处理
}
这个操作符有两个操作数:一个数组名,另一个是索引值
int arr[10];//创建数组
arr[9] = 10;//使用下标引用操作符
下标引用操作符[ ]的两个操作数是arr和9
#include
int main()
{
int arr[]={4,5,8,4,1,3,11,24,15,62,14,22};
int len =sizeof(arr)/sizeof(arr[0]);
for(int i=0;i
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include
void test1()
{
printf("helloworld\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1();//这里的()就是作为函数操作调用符 ,操作数为test1 参数无
test2("hello 123");//同样的这里的()也是函数操作调用符,操作数为test2 参数"hello 123"
return 0;
}
数组可理解为一种自定义数据类型,它存放的是一组相同数据类型数据的集合,每个数据可看作是一个变量,但是现实生活只有这些内置类型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。需要不同的数据类型,因此,C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构体是一种存放不同数据类型的数据集合,同样也是一种自定义数据类型。
这部分在初识C语言已经总结过,这里结合typedef重命名,代码理解即可
1、结构体的声明:
结构体的声明必须放在函数的外部,它告知编译器这个结构体包括哪些成员,这些成员是基本数据类型、指针、数组、其他结构体(结构体允许嵌套)
//结构体声明结合typedef使用 typedef struct Address { char name[20]; char area[20]; }Address; //分号不能少 typedef struct Student { char * name; //字符串指针 const char * name; //字符串指针,用const修饰,只可通过指针解引用访问,不可进行修改 char name[20]; //字符数组 int id; float score; Address address; //结构体嵌套 }Student,*Ps; //利用typedef对结构体和结构体指针进行重命名 //相当于:typedef struct Student Student //相当于:typedef struct Student* Ps
注意事项:
在对结构体成员设计时,要考虑两个方面:
- 考虑数据的长度,用合适的数据类型变量保存!
- 对于较大的数据,可以用字符串进行保存!
如:字符数组 char str[20]; 内存开辟在栈区,保存的是实际的数据
字符串指针 char * str; 字符串存储在常量区,指针开辟在栈区,保存的是这个数据的地址
//整型数据int存放不下时,也可使用上述方法2、结构体定义和初始化:
结构体和数组类似,因此初始化方式和数组一样,采用花括号赋值!
typedef struct Address { char name[20]; char area[20]; }Address,*Pa; typedef struct Student { char * name1; //字符串指针 const char * name2; //字符串指针,用const修饰,只可通过指针解引用访问,不可进行修改 char name3[20]; //字符数组 int id; float score; Address address; //结构体嵌套 }Student,*Ps; int main() { //定义结构体变量同时初始化 Student s={"张三","李四","王五",2220320078,99.5,{"王恒","西安市"}}; //定义结构体指针保存结构体变量的地址,以便后期进行解引用访问 Ps p = &s; return 0; }
注意事项:
数组与结构体类似,但是存在以下两点不同之处:
- 结构体变量名和数组名不同,数组名在表达式中会被转换成指针/地址,而结构体变量名并不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址必须要加取地址符&;
- 数组在内存中存放的,并且随着下标的增大,数组元素的地址也随着增大,数组所占的字节数等于数组元素个数乘以单个元素所占字节数,而结构体所占内存空间的大小,需要结合内存对齐问题进行计算!
结构体成员的访问,一共有3种方式,但实际为了开发方便,常常使用2种
1.通过结构体变量访问结构体成员,语法形式:结构体变量.结构体成员名
2.通过结构体指针变量先解引用找到该结构体变量再 . 访问,
语法形式:(*结构体指针变量).结构体成员名
3.为了简化第二种引入指向符: -> ,语法形式:结构体指针变量->结构体成员名
实际开发,常常用的是第1种和第3种,第3种更加实用。
#include
typedef struct Address
{
char name[20];
char area[20];
}Address,* Pa;
typedef struct Student
{
const char* name1; //字符串指针,用const修饰,只可通过指针解引用访问,不可进行修改
int id;
float score;
Address address; //结构体嵌套
}Student, * Ps;
int main()
{
Student s = { "张三",22203,99.5,{"王恒","西安市"} };
Ps p = &s;
//使用代码实现访问s中的成员address中的area成员(嵌套访问)
//第一种访问方式:通过结构体变量访问结构体成员:结构体变量.结构体成员名
printf("%s ""%d ""%f ""%s ""%s \n",s.name1,s.id,s.score,s.address.name,s.address.area);
//第二种访问方式:通过结构体指针变量先解引用找到该结构体变量再 . 访问
printf("%s ""%d ""%f ""%s ""%s \n", (*p).name1, (*p).id, (*p).score, (*p).address.name, (*p).address.area);
//第三种访问方式:为了简化第二种引入指向符: ->访问
printf("%s ""%d ""%f ""%s ""%s ", p->name1, p->id, p->score, p->address.name, p->address.area);
return 0;
}
结构体数组,侧重点在于它是一个数组,数组的元素全是结构体类型!实际开发结构体数组常被用来表示一个拥有相同数据结构的群体,如一个班的学生。实际开发,这可以与动态内存结合申请一个动态的结构体数组,方便对于数据的操作!!!
//设计银行卡
typedef struct
{
int c_id; //银行卡账号
int c_passwd; // 银行卡 密码
float money; //余额
float withdraw_limit;//日取款上限
bool c_islocked; //卡是否被锁 ---> 密码错上限
char create_date[DATESIZE]; //"2024-1-1 星期几 20:20:20"
//结构体嵌套的使用
//储户名 电话 身份证
User user;
//流水明细 很多条(时间 事情 钱)
WaterRecord records;
//办卡地址 {“建行”,北大街,“招商”,东大街 ,"工商":“西大街”}
BankAddress bankaddress; //银行名+办卡地址
}BankCard;
//一堆卡 一堆管理员 5个
//不定长顺序表
typedef struct
{
BankCard* cards;// [1] [2] [3]. .//通过malloc动态申请,因此定义为一个指针方便操作每一张卡
int c_size; //卡数组 有效卡的数量
int c_capacity; //卡数组 卡容量
}CardArray;
//将卡槽封装动态数组也叫做顺序表,可以进行动态改变
表达式求值的顺序一部分是由操作符的优先级和结合性决定。 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。 主要有两种:隐式类型转换和算术转换。
1、整形提升的概念:
C语言中整型算术运算总是⾄少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
2、整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
//实例1
char a,b,c;
a = b + c;b和c的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于a中。
提升规则:
- 有符号整数提升是按照变量的数据类型的符号位来提升的;
- ⽆符号整数提升,高位补0;
举例1:
有符号数的整型提升
//负数的整型提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有1个字节(8个⽐特位):1111111
因为 char 为有符号的 char,所以整型提升的时候,⾼位补充符号位,即为1,提升之后的结果是:
11111111 11111111 11111111 11111111
//正数的整型提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有1个字节(8个⽐特位):00000001
因为 char 为有符号的 char,所以整型提升的时候,⾼位补充符号位,即为0,提升之后的结果是:
00000000 00000000 00000000 00000001
//⽆符号整型提升,⾼位补0
unsigned char d = 1;
变量d的⼆进制位(补码)中只有1个字节(8个⽐特位)且是无符号数:00000001
因为 char 为无符号的 char,所以整型提升的时候,⾼位补0,提升之后的结果是:
00000000 00000000 00000000 00000001
举例2:
#include
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if(a==0xb6)
printf("a");
if(b==0xb600)
printf("b");
if(c==0xb6000000)
printf("c");
return 0;
}
a,b要进行整形提升,但是c不需要整形提升 a,b整形提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表 达式 c==0xb6000000 的结果是真. 所程序输出的结果是: c
举例3:
#include
int main()
{
char c = 1;
printf("%d\n", sizeof(c));
printf("%d\n", sizeof(+c));
printf("%d\n", sizeof(-c));
return 0;
}
c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字 节。 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.。
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。 警告: 但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。
(1)优先级:优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
(2)结合性:如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(=)。
运算符的优先级顺序很多,可以进行查表,下面是部分运算符的优先级顺序(按照优先级从高到低排列)。建议大概记住这些操作符的优先级就行,其他操作符在使用的时候查看下面表格就可以了。由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。
例1:
//表达式的求值部分由操作符的优先级决定。
//表达式1
a*b + c*d + e*f
表达式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:
//表达式2
c + --c;
同上,操作符的优先级只能决定⾃减 (--) 的运算在 (+)的运算的前⾯,但是我们并没有办法得知,(+) 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
例3:
#include
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);//输出多少?
return 0;
}
这个代码有没有实际的问题?有问题!虽然在大多数的编译器上求得结果都是相同的。 但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。函数的调用先后顺序无法通过操作符的优先级确定。
总结:
即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式。
以上便是操作符的所有内容, 相信你一定大有收获,可以留下你们点赞、关注、评论,您的支持是对我极大的鼓励,下期再见!