文章目录
- 一、数据类型介绍
- 1.字符型
- 2.整型
- 3.浮点型
- 4.布尔类型
- 二、变量
- 1.变量的创建
- 2.变量的分类
- 三、数据类型的长度(字节)
- 1.sizeof 操作符
- 2.各种数据类型的长度
- 3.sizeof中表达式不计算
- 四、各种类型的取值范围
- 1.signed和unsigned
- 2.数据类型的取值范围
- 五、整型提升
- 练习1
- 练习2
- 练习3
- 练习4
- 六、算术转换
一、数据类型介绍
C语言提供了丰富的数据类型来描述生活中的各种数据。使用整型类型来描述整数,使用字符类型来描述字符,使用浮点型类型来描述小数。所谓“类型”, 就是相似的数据所拥有的共同特征,编译器只有知道了数据的类型,才知道怎么操作数据。
C语言提供的各种数据类型:
1.字符型
char //单词character的缩写
[signed] char //有符号的
unsigned char //无符号的
2.整型
//短整型
short [int]
[signed] short [int]
unsigned short [int]
//整型
int
[signed] int
unsigned int
//长整型
long [int]
[signed] long [int]
unsigned long [int]
//更长的整型
//C99中引入
long long [int]
[signed] long long [int]
unsigned long long [int]
注意:上面加[ ]的关键字,意思是书写这个类型时,加[ ]的关键字可以省略。
3.浮点型
float
double
long double
4.布尔类型
C语言原来并没有为布尔值单独设置一个类型,而是使用整数0表示假,非零值表示真。
在 C99 中也引入了布尔类型,是专门表示真假的:
_Bool
布尔类型的使用得包含头文件<stdbool.h>
布尔类型变量的取值是:true 或者 false
#include<stdio.h>
#include<stdbool.h>
int main()
{
_Bool flag = true;
if (flag)
printf("i like C\n");
return 0;
}
程序运行结果:
布尔类型的取值为true和false,我们可以在C语言中转到这两个符号的定义处查看:
可以看到true和false代表的其实就是1和0。
注意:本篇只讲解内置类型;至于自定义类型,我已经发布在我的主页了。
二、变量
1.变量的创建
了解清楚了类型,我们使用类型做什么呢?类型是用来创建变量的。什么是变量呢? C语言中把经常变化的值称为变量,不变的值称为常量。变量创建的语法形式如下:
数据类型就是上面我们学习的各种类型,至于变量名是自定义的,注意:变量名不要与C语言的关键字重名,变量名最好起得有意义,能够见名知意。
例如:
int age; //整型变量
char ch; //字符变量
double weight; //浮点型变量
变量在创建的时候就给一个初始值,就叫初始化。
int age = 18;
char ch = ‘w’;
double weight = 48.0;
unsigned int height = 100;
2.变量的分类
🍑全局变量:在大括号外部定义的变量就是全局变量,全局变量的使用范围更广,在整个工程中想使用,都是有办法使用的。
🍑局部变量:在大括号内部定义的变量就是局部变量,局部变量的使用范围比较局限,只能在自己所在的局部范围内使用。
#include<stdio.h>
int global = 2024;//全局变量
int main()
{
int local = 2020;//局部变量
printf("%d\n", local);
printf("%d\n", global);
return 0;
}
如果局部变量和全局变量的名字相同呢?
#include<stdio.h>
int n = 100;
int main()
{
int n = 10;
printf("%d\n", n);
return 0;
}
程序运行结果:
通过上面的运行结果可以知道,当局部变量和全局变量同名时,局部变量优先使用。
全局变量和局部变量在内存中是存储在哪里的呢?
一般我们在学习C/C++语言的时候,我们会关注内存中的三个区域:栈区、堆区、静态区。
🥑1.局部变量是放在内存的栈区
🥑2.全局变量是放在内存的静态区
🥑3.堆区是用来动态内存管理的
三、数据类型的长度(字节)
每一种数据类型都有自己的长度,使用不同的数据类型,能够创建出长度不同的变量,变量长度的不同,存储的数据范围就有所差异。
1.sizeof 操作符
sizeof是一个关键字,也是操作符,是专门用来计算sizeof的操作数的类型长度的,单位是字节。
sizeof操作符的操作数可以是类型,也可是变量或者表达式。
1. sizeof( 类型 )
2. sizeof 表达式
🍋sizeof 的操作数如果不是类型,是表达式的时候,可以省略掉后边的括号。
🍋sizeof后边的表达式是不真实参与运算的,根据表达式的类型来得出大小。
🍋sizeof的计算结果是 size_t 类型的。
sizeof 运算符的返回值,C语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定,sizeof 到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int,也有可能是unsigned long,甚至是unsigned long long对应的printf()占位符分别是%u、%lu 和 %llu 。这样不利于程序的可移植性。
C语言提供了一个解决方法,创造了一个类型别名size_t,用来统一表示 sizeof 的返回值类型。对应当前系统的sizeof的返回值类型,可能是unsigned int,也可能是unsigned long long。
#include<stdio.h>
int main()
{
int a = 10;
printf("%zd\n", sizeof(a));
printf("%zd\n", sizeof a);
printf("%zd\n", sizeof(int));
printf("%zd\n", sizeof(3 + 3.5));
return 0;
}
程序运行结果:
2.各种数据类型的长度
#include<stdio.h>
int main()
{
printf("%zd\n", sizeof(char));
printf("%zd\n", sizeof(_Bool));
printf("%zd\n", sizeof(short));
printf("%zd\n", sizeof(int));
printf("%zd\n", sizeof(long));
printf("%zd\n", sizeof(long long));
printf("%zd\n", sizeof(float));
printf("%zd\n", sizeof(double));
printf("%zd\n", sizeof(long double));
return 0;
}
程序运行结果:(在VS2022 X86配置下的输出)
3.sizeof中表达式不计算
#include<stdio.h>
int main()
{
short s = 2;
int b = 10;
printf("%d\n", sizeof(s = b + 1));
printf("s = %d\n", s);
return 0;
}
程序运行结果:
由程序运行的结果可知,sizeof中的表达式是不参与计算的。sizeof 在代码进行编译的时候,根据表达式的类型就确定了大小(单位字节),而表达式的执行却要在程序运行期间才能执行,在编译期间已经将sizeof处理掉了,所以在运行期间就不会执行表达式了。
四、各种类型的取值范围
1.signed和unsigned
🥒🥒C 语言中使用 signed 和 unsigned 关键字来修饰字符型和整型类型。signed关键字,表示一个类型带有正负号,包含负值;unsigned 关键字,表示该类型不带有正负号,只能表示零和正整数。对于 int 类型,默认是带有正负号的,也就是说 int 等同于 signed int。由于这是默认情况,关键字 signed 一般都省略不写,但是写了也不算错。
signed int a;
//等同于int a;
int 类型也可以不带正负号,只表示非负整数。这时候就必须使用关键字 unsigned 声明变量。
unsigned int a;
整数变量声明为 unsigned 的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如,16位的 signed short int 的取值范围是:-32768~32767,最大是32767;而unsigned short int 的取值范围是:0~65535,最大值增大到了65535。32位的 signed int 的取值范围可以参看 limits.h 中给出的定义。下面的定义是VS2022环境中,limits.h 中的相关定义:
#define SHRT_MIN (-32768) //有符号16位整型的最小值
#define SHRT_MAX 32767 //有符号16位整型的最大值
#define USHRT_MAX 0xffff //无符号16位整型的最大值
#define INT_MIN (-2147483647 - 1) //有符号整型的最小值
#define INT_MAX 2147483647 //有符号整型的最大值
unsigned int 里面的 int 可以省略,所以上面的变量声明也可以写成下面这样:
unsigned a;
字符类型 char 也可以设置 signed 和 unsigned:
signed char c; //范围为 -128 到 127
unsigned char c; //范围为 0 到 255
注意:C语言规定 char 类型默认是否带有正负号,是由当前系统决定的。这就是说,char 不等同于 signed char,它有可能是 signed char,也有可能是unsigned char。这一点与 int 不同,int 就是等同于 signed int。(在VS编译器上的char类型是有符号的,即signed char)
2.数据类型的取值范围
上述的数据类型很多,尤其数整型类型就有short、int、long、long long 四种,为什么呢? 其实每一种数据类型都有自己的取值范围,也就是存储的数值的最大值和最小值的区间,有了丰富的类型,我们就可以在适当的场景下去选择适合的类型。如果要查看当前系统上不同数据类型的极限值:
🍑limits.h 文件中说明了整型类型的取值范围
🍑float.h 这个头文件中说明浮点型类型的取值范围
可能还有小伙伴不理解,这些数据类型的取值范围是怎么得来的,下面详细讲解一下:
Ⅰ. char类型的取值范围
char类型占1个字节,1个字节是8个bit位。如果是有符号char的话,最高位会被当做符号位,剩下的7位就是数值位:
如果是无符号的char,那就不存在符号位了,8个bit位全是数值位:
Ⅱ.对于整型取值范围的计算,就像上面char类型的计算一样,只不过 int 是占4个字节的,也就是32个bit位。了解了这些类型的取值范围是怎么计算来的,我们就不再一一对各种类型的取值范围进行详细计算了,在limits.h这个文件中进行查看即可:
#define CHAR_BIT 8 //char类型所占的bit位数
#define SCHAR_MIN (-128) //有符号char类型的最小值
#define SCHAR_MAX 127 //有符号char类型的最大值
#define UCHAR_MAX 0xff //无符号char类型的最大值
#define SHRT_MIN (-32768) //有符号short类型的最小值
#define SHRT_MAX 32767 //有符号short类型的最大值
#define USHRT_MAX 0xffff //无符号short类型的最大值
#define INT_MIN (-2147483647 - 1) //有符号int类型的最小值
#define INT_MAX 2147483647 //有符号int类型的最大值
#define UINT_MAX 0xffffffff //无符号int类型的最大值
#define LONG_MIN (-2147483647L - 1) //有符号long int类型的最小值
#define LONG_MAX 2147483647L //有符号long int类型的最大值
#define ULONG_MAX 0xffffffffUL //无符号long int类型的最大值
#define LLONG_MAX 9223372036854775807i64 //有符号long long int类型的最大值
#define LLONG_MIN (-9223372036854775807i64 - 1) //有符号long long int类型的最小值
#define ULLONG_MAX 0xffffffffffffffffui64 //无符号long long int类型的最大值
可以看到上面都是通过#define定义了符号来表示各种类型的取值范围的最大值和最小值,如果你记不住各中类型的取值范围,那记住上面#define定义的符号就可以了。相应的还有浮点型的取值范围,可以在float.h这个文件中查看。
五、整型提升
C语言中整型算术运算总是至少以缺省(默认)整型类型的精度来进行(计算)的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前会被转换为普通整型(int类型),这种转换称为整型提升。
🍇🍇🍇整型提升的意义:
🍅表达式的整型运算要在CPU(中央处理器)的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
🍅因此,即使两个char类型相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
🍅通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节的相加指令)的。所以表达式中各种常量、变量的长度可能小于int类型的长度,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
注意:char是字符类型,但也属于整形家族,因为在内存中存储的是字符的ASCII码值,ASCII码值是整数。
那是如何进行整型提升的呢?
🍓1.有符号整数提升是按照变量的数据类型的符号位来提升的(高位补的是符号位)
🍓2.无符号整数的提升,高位补0
举一个例子:
#include<stdio.h>
int main()
{
char a = 5;
char b = 125;
char c = a + b;
printf("%d\n", c);
return 0;
}
程序运行结果:
上面的代码中就发生了整型提升,首先整数5在内存中的补码是:
00000000 00000000 00000000 00000101
char类型的大小为1个字节(8个bit位),要把整数5存进一个char类型的变量中,就要发生截断,因为char只能存8个bit位,所以把5的低位的8个bit位:00000101存进变量a中。同理,要把整数125存进一个char类型的变量b中,125在内存中的补码为:
00000000 00000000 00000000 01111101
就要发生截断,会把低位的01111101存进变量b中。然后把a+b的值赋给char类型的变量c,可能有人会想,这里的a加b应该就是上面的00000101+01111101=10000010,就是把10000010赋给char类型的变量c。其实不然,这里会发生整型提升,会把变量a和b提升为普通整型,才能进行相加。因为上面说过,CPU内整型运算器(ALU)的操作数的字节长度一般是int的字节长度,因为a和b都是char类型的变量,长度是小于int类型的长度的,所以要发生整型提升。对于a和b要提升为整型,就要根据上面的规则进行补位。在VS编译器中,char类型就是signed char类型(最高位是符号位)。所以对于变量a和b要提升为整型,高位就全补符号位:
00000000 00000000 00000000 00000101 —— a
00000000 00000000 00000000 01111101 —— b
00000000 00000000 00000000 10000010 —— a+b
之后要把a和b相加后的值赋给变量c,由于变量c是char类型的,所以这里也会发生截断,会把低位的10000010赋给变量c。再然后以%d(有符号十进制整数)的形式打印变量c的值,这里又会发生整型提升。因为现在变量c中存的是10000010,由于变量c是signed char类型的变量,所以最高位就是符号位,按照符号位来进行整型提升,最高位全补1得到:
11111111 11111111 11111111 10000010
由于数据在内存中都是以补码的形式进行存储,所以上面的二进制序列就是一个补码,我们要把它换算成原码才能得到真实的值,所以上面的补码换算为原码为:
10000000 00000000 00000000 01111110
上面的这个二进制序列换算为有符号十进制的整数就是-126,以%d的形式进行打印的结果就是-126。
(注意:整型提升针对的是表达式中有char类型和short类型的变量进行运算时,才会发生整型提升。如果表达式中就是整型,就不会发生提升。而且需要注意整型提升的补位规则,是看变量的类型是有符号还是无符号,比如上面的char类型就是有符号char类型,则高位补的就是符号位。上面将5的补码截断后存进了变量a中,那变量a中的值就是00000101。当a发生整型提升时,由于变量a是有符号char类型,就要看00000101的最高位,变量a的最高位就是符号位。由00000101的最高位是0,则a整型提升后,高位就全补0,补齐32个bit位)
练习1
学习了上面的知识,现在我们来做几个练习:
//代码1
#include<stdio.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);
return 0;
}
程序运行结果:
在VS编译器中char类型就是signed char类型,首先知道-1在内存的补码形式为:
11111111 11111111 11111111 11111111
则要将-1存进变量a中,就会发生截断,会把低位的11111111存进变量a中。同样的,将-1存进signed char类型的变量b中,也是存11111111。第三个变量c是无符号char类型,要将-1存进c中,截断后,还是存11111111。重点是,这个三个变量都以%d(有符号整数)的形式进行打印。那变量a、b、c就要发生整型提升,除了变量c是无符号char类型以外,变量a和b都是有符号char类型,那整型提升以后的二进制补码为:
11111111 11111111 11111111 11111111 —— a
11111111 11111111 11111111 11111111 —— b
00000000 00000000 00000000 11111111 —— c
以%d的形式打印变量a、b、c,会认为内存中存的是一个有符号整数,内存中存的是补码,要转化为原码才是真正打印的值:
10000000 00000000 00000000 00000001→ -1 → a
10000000 00000000 00000000 00000001→ -1 → b
00000000 00000000 00000000 11111111→ 255 → b
练习2
再看第二个代码:
//代码2
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
程序运行结果:
首先 -128的二进制补码形式为:
11111111 11111111 11111111 10000000
要将 -128存进一个char类型的变量a中,就要发生截断,会把低位的10000000存进变量a中。现在又要以%u(无符号整数)的形式打印a的值,那就要整型提升变量a,因为a是一个有符号char类型的变量,所以a整型提升高位补的就是符号位。由变量a现在存储的值为10000000(二进制补码),符号位为1,则高位全补1,补齐32个bit位:
11111111 11111111 11111111 10000000 —— a
以%u的形式打印a的值,会认为内存中存储的是一个无符号整数,所以直接将上面的二进制序列换算成无符号的整数就是4294967168。
练习3
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
上面这段代码的运行结果是:死循环的打印"hello world"。
分析上面的代码:首先在{ }的外面定义了一个无符号char类型的全局变量i,之后在main函数里,使用for循环打印字符串"hello world",循环条件是i<=255,让变量i从0遍历到255。由于i是一个无符号char类型的变量,所以unsigned char类型的取值范围是0~255。当i遍历到255时,进入循环体打印了"hello world"后,i++会使i变成256,而256已经超出了无符号char类型的取值区间。由255的补码为:
00000000 00000000 00000000 11111111 —— 255
255加1以后,变成256:
00000000 00000000 00000001 00000000 —— 256(补码)
因为unsigned char类型只能存8个bit位,所以256的补码会发生截断,会把低位的00000000赋给i,这样其实又让i回到了0,那i<=255的条件就会一直成立,所以就造成了死循环的打印"hello world"。
那这里就有一个小的知识点就是:如果让一个无符号char类型的变量从0遍历到256,由于无符号char类型的取值范围就是0~255,所以存储0到255的值就没有问题。当变量遍历到256时,由于表示256最少需要9个bit位,而无符号char类型的变量只能存8个bit位,所以会发生截断,只会存储低位的00000000。而无符号char类型的变量整型提升时,高位补的都是0,所以让无符号char类型的变量存储256,实际上就是存储了0。所以让无符号char类型的变量从0遍历到256其实就是一个无限的循环:
练习4
#include<stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
上面这段代码也是死循环的进行打印,跟上面的练习3类似,因为这里的变量i是一个无符号int类型的变量,所以不可能存负数,它只认为内存中存的是无符号的整数(零或正整数),所以上面的代码当从9打印到0后,i–后i会变成-1,但i并不认为在内存中存的是-1,而是一个无符号整数(即认为-1的最高位也是数值位),所以i>=0是满足的,以%u的形式打印出来就是一个很大的正数,则当i–又回到-1时,其实就是一个死循环。
六、算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则运算就无法进行。下面的层次体系称为寻常算术转换。
1 long double
2 double
3 float
4 unsigned long int
5 long int
6 unsigned int
7 int
如果某个操作数的类型在上面这个列表中的排名靠后,那么首先要转换为另外一个操作数的类型后才能执行运算。(意思是上面的这个排序,是排名靠后的类型向排名靠前的类型进行转换)