系列文章目录
第一章 C语言基础知识
第二章 C语言控制语句
第三章 C语言函数详解
第四章 C语言数组详解
第五章 C语言操作符详解
第六章 C语言指针详解
第七章 C语言结构体详解
文章目录
1. 数据类型
1.1 基本数据类型
1.2 派生数据类型
2. 整形在内存中的存储
2.1 原码、反码、补码
2.2 大端小端
2.3 代码示例:
3. 浮点型在内存中的存储
3.1 代码示例
3.2 浮点数存储规则
1. 数据类型
1.1 基本数据类型
整型(int):用于表示整数,包括正整数、负整数和零。 int x = 10;
short:短整型
long:长整型
long long:更长的整形
字符型(char):用于表示单个字符,可以是字母、数字或特殊字符。char ch = 'A';
浮点型(float、double):用于表示带有小数部分的数值。float num = 3.14;
布尔型(bool):用于表示逻辑值,只有两个取值:true(非零)和false(零)。bool flag = true;
占用存储空间:
char
- char类型通常占用1个字节(8位)的内存空间。
- 存储的数据范围为-128到127(有符号char)或0到255(无符号char)。
short
- short类型通常占用2个字节(16位)的内存空间。
- 存储的数据范围为-32768到32767(有符号short)或0到65535(无符号short)。
int
- int类型的大小通常为系统的字长,例如在32位系统中占用4个字节(32位),在64位系统中占用8个字节(64位)。
- 存储的数据范围为-2147483648到2147483647(有符号int)或0到4294967295(无符号int)。
long
- long类型通常占用4个字节(32位)或8个字节(64位)的内存空间,取决于系统的字长。
- 存储的数据范围与int类型相似,但更大。
float
- float类型通常占用4个字节(32位)的内存空间。
- 存储的数据范围为IEEE754标准中的单精度浮点数范围。
double
- double类型通常占用8个字节(64位)的内存空间。
- 存储的数据范围为IEEE754标准中的双精度浮点数范围。
1.2 派生数据类型
数组(Array)
数组是一种存储相同类型数据元素的连续内存区域,通过下标来访问数组中的元素。在C语言中,数组的声明形式为type name[size],其中type表示数组中元素的类型,name表示数组的名称,size表示数组的大小。
int arr[5] = {1, 2, 3, 4, 5};
结构体(Struct)
结构体是一种用户自定义的数据类型,用于将多个不同类型的数据组合在一起形成一个新的数据类型。在C语言中,结构体的声明形式为struct关键字后跟结构体的名称,然后是一对大括号内部包含各个成员变量的声明。
struct Point {
int x;
int y;
};
指针(Pointer)
指针是一种特殊的数据类型,用于存储变量的地址。通过指针可以实现对变量的间接访问,以及动态内存分配和释放等功能。
int *ptr = &x;
枚举(Enum)
枚举是一种用于定义一组有限的命名常量集合的数据类型。枚举类型可以用于提高程序的可读性,使代码更加清晰易懂。
enum Color { RED, GREEN, BLUE };
联合(Union)
联合是一种特殊的数据类型,用于存储不同类型的数据,但在同一时间只能存储其中的一种类型。联合的大小等于其最大成员的大小。
union Data {
int i;
float f;
};
2. 整形在内存中的存储
2.1 原码、反码、补码
计算机中的整数有三种二进制表示方法,即原码、反码和补码。 三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位:正数的原、反、补码都相同, 负整数的三种表示方法各不相同。
原码(Sign-Magnitude):
原码是最直观的表示方式,直接将数值按照正负数的形式翻译成二进制就可以得到原码。 其中最高位表示符号位(0表示正数,1表示负数),其余位表示数值的绝对值。
例如,+5的原码表示为00000101,-5的原码表示为10000101。
反码(Ones' Complement):
反码是将原码中的正数不变,负数按位取反(0变为1,1变为0)得到的编码方式。
例如,+5的反码和原码相同(00000101),-5的反码为11111010。
补码(Two's Complement):
补码是将原码中的正数不变,负数取反后再加1得到的编码方式。也就是反码+1就得到补码。 补码可以解决反码的问题,即负零和两个零的存在。补码中只有一个零,即00000000。
例如,+5的补码和原码相同(00000101),-5的补码为11111011。
在计算机中,整数类型的数据存储在内存中时,通常采用补码形式。这主要是为了简化算术运算和减少硬件设计的复杂性。补码具有以下几个优点:
-
唯一表示零:补码能够唯一地表示零,而原码和反码都存在正零和负零的问题。在补码中,只有一个零的表示方式,即所有位均为0。
-
简化加法和减法运算:在补码中,加法和减法的运算规则是一致的,无需额外的逻辑操作。例如,将两个补码相加,然后将结果直接截取为指定位数即可,而无需考虑正负数的特殊情况。
-
统一处理溢出:在补码中,溢出时会自然地从最高位溢出到符号位,从而实现了对于正数和负数溢出的统一处理方式。
-
硬件实现简单:补码的加法和减法可以使用同一套逻辑电路来实现,简化了硬件设计的复杂性。
2.2 大端小端
大小端(Endian)是指在多字节数据存储时,低字节的存放位置和高字节的存放位置的不同排列方式。主要分为大端序(Big Endian)和小端序(Little Endian)两种。
-
大端序(Big Endian):
- 在大端序中,高位字节(Most Significant Byte,MSB)存放在低地址处,低位字节(Least Significant Byte,LSB)存放在高地址处。
- 例如,十六进制数0x12345678,在内存中的存储顺序是:12 34 56 78。
-
小端序(Little Endian):
- 在小端序中,低位字节(LSB)存放在低地址处,高位字节(MSB)存放在高地址处。
- 例如,十六进制数0x12345678,在内存中的存储顺序是:78 56 34 12。
2.3 代码示例:
// 代码1:通过判断低地址处的字节内容来确定系统的字节序(小端或大端)
#include <stdio.h>
// 检查系统字节序的函数
int check_sys() {
int i = 1; // 创建一个整数 i,赋值为 1
return (*(char *)&i); // 返回 i 的低地址处的字节内容
}
int main() {
int ret = check_sys(); // 调用 check_sys 函数,返回字节内容并赋给 ret
if (ret == 1) { // 如果返回值为 1,则表示小端字节序
printf("小端\n"); // 打印小端字节序
} else {
printf("大端\n"); // 否则打印大端字节序
}
return 0;
}
这段代码中,check_sys()
函数首先创建一个整数 i
,然后通过将 i
的地址强制转换为字符型指针 char *
,再取其指向的内容,即低地址处的字节。如果当前系统是小端字节序,那么该字节的值应该为 1
,因为整数 1
的低字节就是 1
,所以函数返回 1
,表示小端字节序;反之,如果当前系统是大端字节序,那么该字节的值应该为 0
,因为整数 1
的低字节是 0
,所以函数返回 0
,表示大端字节序。
// 代码2:使用联合体检查系统字节序
// 检查系统字节序的函数
int check_sys() {
union { // 定义一个联合体,用于共享同一段内存空间
int i; // 整数
char c; // 字符
} un; // 联合体变量 un
un.i = 1; // 将整数 i 赋值为 1
return un.c; // 返回联合体中字符 c 的值,即 i 的低地址处的字节内容
}
这段代码使用了一个联合体 union
,联合体中包含一个整数 i
和一个字符 c
。由于联合体的所有成员共享同一段内存空间,所以当给 i
赋值为 1
后,c
的值就是 i
中低地址处的字节,这个值就是用来判断字节序的。因此,和代码1的原理类似,返回的结果也是 1
或 0
,表示小端或大端字节序。
补码示例:
#include <stdio.h>
int main()
{
char a= -1; // 将 -1 赋值给 char 类型的变量 a,这里的 -1 在转换成补码后就是 11111111
signed char b=-1; // signed char 类型也是有符号的,所以 -1 在转换成补码后仍然是 11111111
unsigned char c=-1; // unsigned char 类型是无符号的,但 -1 在转换成补码后也是 11111111,因为 char 是 8 位的,无符号范围是 0~255,-1 被当作 255 处理
printf("a=%d,b=%d,c=%d",a,b,c); // 输出变量 a、b、c 的值,分别是 -1、-1、255
return 0;
}
#include <stdio.h>
int main()
{
char a = -128; // char 类型是有符号的,范围是 -128~127,所以 -128 被当作 -128 处理
printf("%u\n",a); // 格式化输出 a 的值,由于使用了 %u,-128 在按无符号打印时被当作 4294967168 处理
return 0;
}
#include <stdio.h>
int main()
{
char a = 128; // char 类型是有符号的,范围是 -128~127,所以 128 被当作 -128 处理
printf("%u\n",a); // 格式化输出 a 的值,由于使用了 %u,128 在按无符号打印时被当作 4294967168 处理
return 0;
}
#include <stdio.h>
int main()
{
int i= -20; // 定义有符号整型变量 i,赋值为 -20
unsigned int j = 10; // 定义无符号整型变量 j,赋值为 10
printf("%d\n", i+j); // 输出 i+j 的值,-20+10= -10,按照补码的形式进行运算,最后格式化成为有符号整数,结果为 -10
return 0;
}
3. 浮点型在内存中的存储
浮点数在内存中的存储通常采用 IEEE 754 标准来进行表示,这个标准规定了浮点数的存储格式,包括单精度浮点数(float)和双精度浮点数(double)。
3.1 代码示例
#include <stdio.h>
int main() {
int n = 9; // 定义一个整型变量 n,初始值为 9
float *pFloat = (float *)&n; // 将 n 的地址强制转换为 float 类型的指针 pFloat
printf("n的值为:%d\n", n); // 打印 n 的值
printf("*pFloat的值为:%f\n", *pFloat); // 通过指针 *pFloat 打印 n 所指向的浮点数值
*pFloat = 9.0; // 修改 *pFloat 指向的值为 9.0,实际上也就修改了 n 的值
printf("num的值为:%d\n", n); // 再次打印 n 的值,此时已经被修改为 1092616192
printf("*pFloat的值为:%f\n", *pFloat); // 通过指针 *pFloat 打印 n 所指向的浮点数值,此时为 9.0
return 0;
}
3.2 浮点数存储规则
对于上一个示例:num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
这是因为浮点数和整数在内存中的存储格式是不同的。虽然 num
和 *pFloat
在内存中表示的是相同的二进制数据,但它们的解读方式不同:
num
是一个整数类型,它会按照整数的解读方式来解释内存中的二进制数据。因此,当我们将整型变量num
的值打印出来时,会按照整数的格式来解读,得到的结果是整数值9
。*pFloat
是一个浮点数指针,它会按照浮点数的解读方式来解释内存中的二进制数据。即使这个内存中的二进制数据实际上是一个整数,但在浮点数的解读方式下,它会被解释为一个浮点数值。这种解读方式会导致我们得到一个较大的浮点数值,而不是我们期望的整数值。
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
0 01111110 00000000000000000000000
9 -> 0000 0000 0000 0000 0000 0000 0000 1001
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130
0 10000010 001 0000 0000 0000 0000 0000