移位操作符
<< 左移操作符
>>右移操作符
-
左移操作符 移位规则:
左边抛弃、右边补0
-
右移操作符 移位规则:
首先右移运算分两种:
1.逻辑移位 左边用0填充,右边丢弃
2.算术移位 左边用原该值的符号位填充,右边丢弃
警告⚠ : 对于移位运算符,不要移动负数位,这个是标准未定义的。例如:
int num = 10;
num>>-1;//error
sizeof 和数组
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(2)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(1)
printf("%d\n", sizeof(ch));//(3)
test1(arr);
test2(ch);
return 0;
}
问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
答:
(1)
输出为:40(2)
输出为:8(3)
输出为:10(4)
输出为:8
这些输出结果的原因如下所述:
在 main 函数中,sizeof(arr) 表示整型数组 arr 的大小,即 10 个整型元素,每个整型占据 4 个字节(32 位系统下)。因此,sizeof(arr) 的结果为 10 * 4 = 40 字节。
在 main 函数中,sizeof(ch) 表示字符型数组 ch 的大小,即 10 个字符元素,每个字符占据 1 个字节。因此,sizeof(ch) 的结果为 10 字节。
在 test1 函数中,arr 参数虽然声明为整型数组,但在函数参数中数组会被转换为指针,因此 sizeof(arr) 实际上返回的是指针的大小,而不是整型数组的大小。在这里,指针的大小是 8 字节(64 位系统下)。
在 test2 函数中,同样地,ch 参数虽然声明为字符型数组,但在函数参数中数组也会被转换为指针,因此 sizeof(ch) 返回的是指针的大小,而不是字符型数组的大小。在这里,指针的大小是 8 字节(64 位系统下)。
区分逻辑与(或)和按位与(或)
1&2----->0
1&&2---->1
1|2----->3
1||2---->1
1&2 -----> 0
:
这里使用的是按位与(&)运算符,对应二进制的每一位进行与操作。1 的二进制表示为 01,2 的二进制表示为 10。按位与操作后,得到的结果是 00,即 0。
1&&2 ----> 1
:
这里使用的是逻辑与(&&)运算符,它是逻辑运算符,用于判断两个条件是否同时为真。在大多数编程语言中,逻辑与会进行短路求值,即如果第一个条件为假,则不会再计算第二个条件,直接返回假。因此,1&&2 中的 1 和 2 都被视为真,因此结果是 1。
1|2 -----> 3
:
这里使用的是按位或(|)运算符,对应二进制的每一位进行或操作。1 的二进制表示为 01,2 的二进制表示为 10。按位或操作后,得到的结果是 11,即 3。
1||2 ----> 1
:
这里使用的是逻辑或(||)运算符,用于判断两个条件是否有一个为真。逻辑或也会进行短路求值,即如果第一个条件为真,则不会再计算第二个条件,直接返回真。因此,1||2 中的 1 被视为真,因此结果是 1。
在很多编程语言中,比如 C、C++、Java 等,“&&” 是逻辑与运算符(logical AND operator)。当使用 “&&” 运算符时,它会对两个条件进行逻辑与操作,只有当两个条件都为真时,整个表达式的结果才为真(true),否则结果为假(false)。
在上述表达式"1&&2" 中,1 和 2 被视为条件,即非零值被视为真。因此,根据逻辑与运算符的规则,只有当两个条件都为真时,结果才为真。在这种情况下,1 和 2 都被视为真,所以整个表达式的结果为真,即 1。
一道笔试题
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
现在我们来分析代码的执行过程:
初始时,a = 0, b = 2, c = 3, d = 4。
执行 i = a++ && ++b && d++:
首先计算 a++,a 先赋值给 i(i = 0),然后 a 自增为 1。此时 a 的值为 0,表示为假。
因为第一个条件已经为假,后续的条件不再执行,即 ++b 和 d++ 都不会被执行。
整个表达式因为第一个条件为假,所以结果为假,即 i 的值为 0。
因此,最终输出的结果是:
a = 1
b = 2
c = 3
d = 4
但是如果是注释掉的那一行,那么结果为,
a = 1
b = 3
c = 3
d = 4
逗号表达式
逗号表达式会依次计算每个表达式,并返回最后一个表达式的值作为整个表达式的值。
例如:
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
c为13
访问结构体
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
void set_age1(struct Stu stu)
{
stu.age = 18;
}
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}
int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问
stu.age = 20;//结构成员访问
set_age1(stu);
pStu->age = 20;//结构成员访问
set_age2(pStu);
return 0;
}
这段代码中,首先定义了一个结构体 Stu,包括姓名、年龄、性别和分数四个成员变量。然后定义了两个函数 set_age1 和 set_age2,分别用来设置学生的年龄。在 main 函数中,创建了一个 Stu 类型的对象 stu,并创建了一个指向该对象的指针 pStu。
接下来分析代码的执行过程:
- stu.age = 20; // 将 stu 的年龄设置为 20
- set_age1(stu); // 传递参数时会复制结构体,所以在 set_age1 函数中对参数进行的修改不会影响原始的 stu 对象。
- pStu->age = 20; // 通过指针 pStu 访问 age 成员,将年龄设置为 20
- set_age2(pStu); // 传递指针参数,可以直接修改原始的结构体对象。
因此,经过上述步骤后,stu 对象的年龄应该是 20,而 pStu 指向的对象的年龄应该是 18。所以最终输出的结果是:stu.age = 20,pStu->age = 18。
大小端
大小端(Endian)是指在存储多字节数据时,字节序的不同排列方式。主要有两种类型:大端序(Big-endian)和小端序(Little-endian)。
- 大端序(Big-endian):数据的高位字节存储在低地址,低位字节存储在高地址。即数据的最高有效字节存储在最低的地址,依次类推。
举例:十六进制数 0x12345678 在大端序中存储为:
地址 数据
0x00 -> 12
0x01 -> 34
0x02 -> 56
0x03 -> 78
小端序(Little-endian):数据的低位字节存储在低地址,高位字节存储在高地址。即数据的最低有效字节存储在最低的地址,依次类推。
举例:十六进制数 0x12345678 在小端序中存储为:
地址 数据
0x00 -> 78
0x01 -> 56
0x02 -> 34
0x03 -> 12
在计算机系统中,不同的处理器架构采用不同的字节序,而网络通信和数据交换等需要统一字节序以确保数据正确传输和解析。因此,在跨平台开发和数据通信时,需要注意数据的字节序问题。
为什么要存在大小端?
大小端的存在主要是由于不同的计算机体系结构和处理器架构在存储和处理多字节数据时的方式不同。以下是一些原因:
-
处理器架构差异:不同的处理器架构采用了不同的字节序。例如,x86 架构使用小端序,而某些 RISC 架构(如 ARM、PowerPC)使用大端序。这种差异导致在进行跨平台开发、数据交换和网络通信时需要考虑字节序的转换。
-
数据传输:在网络通信中,不同的系统之间需要传输数据。为了确保数据的正确传输和解析,发送方和接收方需要在数据传输过程中统一字节序。否则,接收方可能会错误地解释数据,导致数据损坏或解析错误。
-
数据存储:在文件和存储设备上存储数据时,字节序的一致性也很重要。如果不同系统上的程序读取和写入数据时使用不同的字节序,那么数据的解析将会出错。
因此,大小端的存在是为了解决不同系统间数据交换和解析的问题,确保数据的正确性和一致性。
判断大小端的程序:
#include<stdio.h>
#include<windows.h>
int main()
{
union
{
int a;
char c;
}un;
un.a = 1;
if (un.c) {
printf("小端\n");
}
else {
printf("大端\n");
}
system("pause");
return 0;
}
解释:
这段代码使用了 C 语言中的联合(union)来判断当前系统的字节序是大端序还是小端序。具体解释如下:
- 定义了一个联合 un,其中包含一个整型变量 a 和一个字符变量 c。
- 将整型变量 a 赋值为 1。
- 利用联合的特性,修改 a 的同时也会影响到 c,因为它们共享同一块内存空间。
- 判断 c 的值,如果 c 的值为非零,则说明当前系统采用小端序;如果 c 的值为 0,则说明当前系统采用大端序。
- 最后通过打印输出来显示当前系统的字节序。
整型提升
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
整形提升是按照变量的数据类型的符号位来提升的。
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
用一个例子来体会一下:
#include<stdio.h>
//实例1
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;
}
上述代码的速出结果为 c ,因为 a,b 要进行整形提升,但是c不需要整形提升 a,b整形提升之后,变成了负数,所以表达式a==0xb6 , b==0xb600 的结果是假,但是c不发生整形提升,则表达式 c==0xb6000000 的结果是真。
另一个例子:
#include<stdio.h>
//实例2
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(!c));
return 0;
}
结果分析:
-
sizeof ( c ):c 是一个字符型变量,占用一个字节。所以 sizeof© 的结果是 1。
-
sizeof(+c):在 C 语言中,一元正号操作符会将操作数提升为整数类型。因此,+c 的结果将是一个 int 类型,占用 4 个字节。所以 sizeof(+c) 的结果应该是 4 而不是 1。
-
sizeof(!c):逻辑非操作符 ! 会返回 0 或 1,而不改变数据类型的大小。所以 sizeof(!c) 的结果应该是 1。
-
表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof© ,就是1个字节
截断
截断(truncation)是指将一个值的小数部分舍弃,只保留整数部分的操作。截断通常发生在从浮点数到整数的类型转换过程中。
如果将字节多的数据类型赋给一个占字节少的变量类型,会发生“截断”。
举例说明:
- 将浮点数截断为整数:
- 假设有一个浮点数 x = 3.75,通过截断操作可以将其转换为整数 3。小数部分 .75 被舍弃,只保留了整数部分 3。
- 可以使用不同的编程语言提供的截断函数或类型转换函数来进行这样的操作,例如在 Python 中使用 int() 函数,或在 C 语言中使用 (int) 强制类型转换。
- 截断位操作:
- 在计算机领域,有时候我们需要对二进制数进行截断操作。例如,假设有一个 8 位的二进制数 10101101,如果我们只需要保留前 4 位,那么截断操作就可以将其变为 1010。
- 在具体实现中,可以通过与运算(bitwise AND)来实现截断位操作。例如,在 C 语言中可以使用按位与操作符 &,如 result = number & 0xF0,其中 number 是原始的二进制数,0xF0 是一个掩码,表示前 4 位都为 1,其余位都为 0。这样的截断操作就可以保留目标位上的数值。
举个例子:
#include<stdio.h>
#include<windows.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d\n", a, b, c);
system("pause");
return 0;
}
以上代码的输出结果是:
a=-1, b=-1, c=255
这是因为在C语言中,char类型默认被定义为有符号类型(signed char),其取值范围为-128到127。当使用-1赋值给char类型变量a时,会将-1视为有符号数,因此a的值也为-1。
而对于signed char类型的变量b,虽然也是将-1赋值给它,但由于已经明确指定为有符号类型,所以它的值仍然是-1。
对于unsigned char类型的变量c,它是无符号类型,其取值范围为0到255。当将-1赋值给unsigned char类型变量c时,会发生截断操作。由于c是无符号类型,截断后的结果相当于对256取余,即-1+256=255。因此c的值为255。