C语言数据类型、变量及数据类型的长度、取值范围

文章目录

一、数据类型介绍

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

c 复制代码
#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.变量的分类

🍑全局变量 :在大括号外部定义的变量就是全局变量,全局变量的使用范围更广,在整个工程中想使用,都是有办法使用的。
🍑局部变量:在大括号内部定义的变量就是局部变量,局部变量的使用范围比较局限,只能在自己所在的局部范围内使用。

c 复制代码
#include<stdio.h>
int global = 2024;//全局变量
int main()
{
	int local = 2020;//局部变量
    printf("%d\n", local);
	printf("%d\n", global);
	return 0;
}

如果局部变量和全局变量的名字相同呢?

c 复制代码
#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。

c 复制代码
#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.各种数据类型的长度

c 复制代码
#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中表达式不计算

c 复制代码
#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 语言中使用 signedunsigned 关键字来修饰字符型整型 类型。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 中的相关定义:

c 复制代码
#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这个文件中进行查看即可:

c 复制代码
#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

举一个例子:

c 复制代码
#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

学习了上面的知识,现在我们来做几个练习:

c 复制代码
//代码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

再看第二个代码:

c 复制代码
//代码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

c 复制代码
#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

c 复制代码
#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

如果某个操作数的类型在上面这个列表中的排名靠后,那么首先要转换为另外一个操作数的类型后才能执行运算。(意思是上面的这个排序,是排名靠后的类型向排名靠前的类型进行转换)

相关推荐
不悔哥16 分钟前
openwrt wsdd模块介绍
linux·c语言·网络·tcp/ip·智能路由器
只想摆烂@20 分钟前
C# winfrom 如何多窗体优雅的回调方法
开发语言·c#
西猫雷婶22 分钟前
python画图|中秋到了,尝试画个月亮(球体画法)
开发语言·python
星迹日24 分钟前
C语言:结构体
c语言·开发语言·经验分享·笔记
会敲代码的小张36 分钟前
设计模式-观察者模式
java·开发语言·后端·观察者模式·设计模式·代理模式
jyan_敬言40 分钟前
虚拟机centos_7 配置教程(镜像源、配置centos、静态ip地址、Finalshell远程操控使用)
linux·运维·服务器·c语言·数据结构·tcp/ip·centos
宗浩多捞40 分钟前
C++设计模式(更新中)
开发语言·c++·设计模式
学习使我变快乐3 小时前
C++:析构函数
开发语言·c++
我言秋日胜春朝★3 小时前
【C++】继承详解
开发语言·c++
fhvyxyci4 小时前
【数据结构初阶】顺序结构二叉树(堆)接口实现超详解
c语言·数据结构