一觉醒来之全世界C语言水平倒退100年,我在C语言领域当大佬:C语言基础学习第一天之数据类型和变量

150 阅读19分钟

1. 数据类型介绍

C语⾔提供了丰富的数据类型来描述⽣活中的各种数据。 使⽤整型类型来描述整数,使⽤字符类型来描述字符,使⽤浮点型类型来描述数。 所谓“类型”,就是相似的数据所拥有的共同特征,编译器只有知道了数据的类型,才知道怎么操作数据。

下⾯盘点⼀下C语⾔提供的各种数据类型,本章节主要探讨内置数据类型。

graph LR
A[数据结构] -->B([内置类型]) --> C(字符型)
B-->D(整形)
B-->E(浮点型)
B-->F(布尔类型)
A -->H([自定义类型])--> J(数组)
H([自定义类型])--> K(结构体-struct)
H([自定义类型])--> L(枚举-enum)
H([自定义类型])--> M(联合-unio)

1.1 字符型

char
[signed] char
unsigned char

1.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]

1.3 浮点型

float
double
long double

1.4 布尔类型

C 语⾔原来并没有为布尔值单独设置⼀个类型,⽽是使⽤整数0 表⽰假,⾮零值表⽰真。

#include <stdio>
int main()
{
	//C语言中0表示假
	if(2 == 1 + 1)
	{
		printf("hehe\n");
	}	
	return 0;
}
运行结果:
hehe

C99 中也引入了 布尔类型 ,是专门表示真假的。

 _Bool

布尔类型的使⽤得包含头⽂件 <stdbool.h> 布尔类型变量的取值是: true 或者 false

#define bool _Bool
#define false 0
#define true 1

代码演示:

#include <stdio.h>
#include <stdbool.h>
int main()
{
	_Bool flag = true;
	if (flag)
	{
		printf("hehe\n");
	}
	return 0;
}
运行结果:
hehe

因为在头文件<stdbool.h>中,用#defined定义了bool这个符号_Bool,所以可以使用bool代替_Bool,但它的意思还是_Bool

代码演示:

#include <stdio.h>
#include <stdbool.h>
int main()
{
	bool flag = true;
	if (flag)
	{
		printf("hehe\n");
	}
	return 0;
}
运行结果:
hehe

1.5 各种数据类型的⻓度

每⼀种数据类型都有⾃⼰的⻓度,使⽤不同的数据类型,能够创建出⻓度不同的变量,变量⻓度的不同,存储的数据范围就有所差异。

1.5.1 sizeof操作符

sizeof 是⼀个关键字,也是操作符,专⻔是⽤来计算sizeof操作符数的类型长度的,单位是字节

sizeof 操作符的操作数可以是类型,也可是变量或者表达式。

sizeof( 类型 )
sizeof 表达式

sizeof 的操作数如果不是类型,是表达式的时候,可以省略掉后边的括号的。 sizeof 后边的表达式是不真实参与运算的,根据表达式的类型来得出⼤⼩。 sizeof 的计算结果是 size_t 类型的。

#include <stdio.h>
//sizof 计算的结果的类型是size_t,size_t是一种无符号整形,这种整数值在打印的时候,使用%zd
//int  --- %d
//char --- %c
//字符串 --- %s
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;
}

结果如下: 在这里插入图片描述

sizeof 运算符的返回值,C 语⾔只规定是⽆符号整数,并没有规定具体的类型,⽽是留给系统⾃⼰去决定, sizeof 到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int ,也有可能是 unsigned long ,甚⾄是 unsigned long long ,对应的 printf() 占位符分别是 %u 、 %lu 和 %llu 。这样不利于程序的可移植性。 C 语⾔提供了⼀个解决⽅法,创造了⼀个类型别名 size_t ,⽤来统⼀表⽰ sizeof 的返回值类型。对应当前系统的 sizeof 的返回值类型,可能是 unsigned int ,也可能是unsigned long long 。

也就是说sizeof操作符的返回值不可能是是负数,因为sizeof操作符是计算一个类型所占字节的大小,最小也是整数,所以C语言规定了sizeof返回值是无符号数,但是并没有具体规定是什么类型,这个不同系统的返回值类型可能不一样,那么在打印的时候,格式就会不一样,C语言为了解决这个问题,C语言就专门创造了一个类型size_t来表示sizeof的返回值的类型,这个类型的格式就统一用%zd来表示,这样即使不同系统的使用的无符号整形类型不一样,但是这个无符号类型用size_t来表示,那么在打印的时候,格式就会都一样了 。

下面是系统定义size_t类型的截图 在这里插入图片描述 这里简单说一下#ifdef是条件编译指令,typedef是重定义的意思,__int64就是整形的大小是64位,也就是8个字节相当于long long大小,大概意思就是如果是在64位平台,size_t的类型就是unsigned __int64t类型,否则类型就是unsigned int

#include <stdio.h>
int main()
{
 int a = 10;
 printf("%zd\n", sizeof(a));
 printf("%zd\n", sizeof a);//a是变量的名字,可以省略掉sizeof后边的()
 printf("%zd\n", sizeof(int));
 return 0;
}

运行结果如图: 在这里插入图片描述 注意: 如果sizeof里面放的是变量或者直接使用的值类型可以省略,但是类型不能省略否则会报错

#include <stdio.h>
int main()
{
 printf("%zd\n", sizeof (3.14));//double
 printf("%zd\n", sizeof(3.14f));//float
 return 0;
}

运行结果如图: 在这里插入图片描述 编译器默认小数是double类型,3.14f,这里的f是后缀表示是float类型,sizeof最终还是根据数值对应类型计算的

1.5.2 sizeof中表达式不计算

//测试: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;
}

结果如图: 在这里插入图片描述 这里b是短整形1是整形,不同类型进行算术运算时会进行算术转换,b就会进行整形提升成int类型,但是最终的类型还是由s的类型决定的,这里就会发生截断,这个表示的的类型最终就是short类型,但是这里因为能直接确定表达式的类型就是short,所以并不会进行计算,编译器会直接计算s的类型,并不会计算这个表达式。

准确来说sizeof在代码进⾏编译的时候,就根据表达式的类型确定了,类型的使⽤,⽽表达式的执⾏却要在程序运⾏期间才能执⾏,在编译期间已经将sizeof处理掉了,所以在运⾏期间就不会执⾏表达式了。

2. signed 和 unsigned

C 语⾔使⽤signed unsigned 关键字修饰字符型和整型类型的。 注意:浮点数不能用signed unsigned 关键字修饰 signed 关键字,表⽰⼀个类型带有正负号,包含负值; unsigned 关键字,表⽰该类型不带有正负号,只能表⽰零和正整数。 对于 int 类型,默认是带有正负号的,也就是说int 等同于 signed int。 由于这是默认情况,关键字 signed ⼀般都省略不写,但是写了也不算错。

signed int a;
// 等同于int a;

int 类型也可以不带正负号,只表⽰⾮负整数。这时就必须使⽤关键字 unsigned 声明变量

unsigned int a;

整数变量声明为 unsigned 的好处是,同样⻓度的内存能够表⽰的最⼤整数值,增⼤了⼀倍。⽐如,16位的 signed short int 的取值范围是:-3276832767,最⼤是32767;⽽signed short int 的取值范围是:065535,最⼤值增⼤到了65,535。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

3. 数据类型的取值范围

上述的数据类型很多,尤其数整型类型就有short、int、long、long long 四种,为什么呢?其实每⼀种数据类型有⾃⼰的取值范围,也就是存储的数值的最⼤值和最⼩值的区间,有了丰富的类型,我们就可以在适当的场景下去选择适合的类型。如果要查看当前系统上不同数据类型的极限值:limits.h ⽂件中说明了整型类型的取值范围。 float.h 这个头⽂件中说明浮点型类型的取值范围。为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使⽤这些常量。

• SCHAR_MIN , SCHAR_MAX :signed char 的最⼩值和最⼤值。
• SHRT_MIN , SHRT_MAX :short 的最⼩值和最⼤值。
• INT_MIN , INT_MAX :int 的最⼩值和最⼤值。
• LONG_MIN , LONG_MAX :long 的最⼩值和最⼤值。
• LLONG_MIN , LLONG_MAX :long long 的最⼩值和最⼤值。
• UCHAR_MAX :unsigned char 的最⼤值。
• USHRT_MAX :unsigned short 的最⼤值。
• UINT_MAX :unsigned int 的最⼤值。
• ULONG_MAX :unsigned long 的最⼤值。
• ULLONG_MAX :unsigned long long 的最⼤值。

4. 变量

4.1 变量的创建

了解清楚了类型,我们使⽤类型做什么呢?类型是⽤来创建变量的。 什么是变量呢?C语⾔中把经常变化的值称为变量,不变的值称为常量。

变量创建的语法形式是这样的:

data_type name;
   | 	   |
   | 	   |
数据类型  变量名
int age; //整型变量
char ch; //字符变量
double weight; //浮点型变量

变量在创建的时候就给⼀个初始值,就叫初始化:

#include <stdio.h>
int main()
{
	int age;
	printf("%d\n",age);
	return 0;
}

这个代码运行不了,因为age未初始化,里面放的是随机值

#include <stdio.h>
int main()
{
	int age = 0;//初始化--在变量创建的同时,给一个值叫初始化
	printf("%d\n",age);
	return 0;
}

这样代码就没问题了,所以在创建变量的时候最好给变量初始化一个值,比较常用的就是初始化一个 0

4.2 变量的分类和作用域

• 全局变量:在⼤括号外部定义的变量就是全局变量 全局变量的使⽤范围更⼴,整个⼯程中想使⽤,都是有办法使⽤的。 • 局部变量:在⼤括号内部定义的变量就是局部变量 局部变量的使⽤范围是⽐较局限,只能在⾃⼰所在的局部范围内使⽤的。

作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用 的而限定这个名字的可用性的代码范围就是这个名字的作用域。

  1. 局部变量的作用域是变量所在的局部范围。
  2. 全局变量的作用域是整个工程。
#include <stdio.h>
int b = 100;//全局变量
int main()
{
	{
	int a = 10;//局部变量
	printf("%d",a);
	}
}
运行结果:
10
#include <stdio.h>
int b = 100;//全局变量
int main()
{
	{
	int a = 10;//局部变量
	printf("%d\n",a);
	}
	printf("%d\n",a);
	return 0;
}

这里会报错因为大括号里面的变量是局部变量只能在打大括号里面使用

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

#include <stdio.h>
int n = 1000;
int main()
{
	 int n = 10;
	 printf("%d\n" n);//打印的结果是多少呢?
	 return 0;
}
运行结果:
10

其实当局部变量和全局变量同名的时候,局部变量优先使⽤。

4.3 变量的生命周期

生命周期

变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段

  1. 局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
  2. 全局变量的生命周期是:整个程序的生命周期。
#include <stdio.h>
int main()
{
	{
	int a = 10;//局部变量
	printf("%d\n",a);
	}
	printf("%d\n",a);
	return 0;
}

对于局部变量a来说在括号里面可以使用,但出了括号就不能使用了,既然不能使用a也就是说a的生命周期结束了,但是a进入进了括号它就得用,就得有呀,,所以我们可以这样理解,这个局部变量a应该是进入它所在的范围,就会创建,一旦进入这个括号,a就有可能被使用了,这时候它就要创建,生命周期开始,而一旦处这个范围,a就不能使用了,这时候它的生命周期就应该到了,a变量就会被销毁,就不能被使用了。

那么全局变量的生命周期呢? 这里我们分析一下,这个全局变量在主函数开始就能使用,在主函数结束之前也能使用,而程序从主函数开始,然后执行一直到主函数结束程序也结束了,所以一个程序的生命周期好像跟主函数的生命周期是一样的,主函数开始执行程序的生命周期开始,主函数结束这个程序生命周期结,在主函数的开始到结束都可以使用全局变量,所以全局变量在整个程序的生命周期中它都在,所以就可以得出一个结论,全局变量的生命周期其实是和程序的生命周期一样的。 变量的作用域约等于它的生命周期,作用域好像决定了它的生命周期,生命周期好像又跟它的作用域有关系

全局变量和局部变量在内存中存储在哪⾥呢? ⼀般我们在学习C/C++语⾔的时候,我们会关注内存中的三个区域:栈区、堆区、静态区

  1. 局部变量是放在内存的栈区
  2. 全局变量是放在内存的静态区
  3. 堆区是⽤来动态内存管理的

如图所示: 在这里插入图片描述

5. 算术操作符:+、-、*、/、%

在写代码时候,⼀定会涉及到计算。 C语⾔中为了⽅便运算,提供了⼀系列操作符,其中有⼀组操作符叫:算术操作符。分别是: + - * \ % ,这些操作符都是双⽬操作符。 注:操作符也被叫做:运算符,是不同的翻译,意思是⼀样的。

5.1 + 和 -

  • 和 - ⽤来完成加法和减法。
  • 和 - 都是有2个操作数的,位于操作符两端的就是它们的操作数,这种操作符也叫双⽬操作符。
#include <stdio.h>
int main()
{
	int x = 4 + 22;
	int y = 61 - 23;
	printf("%d\n", x);
	printf("%d\n", y);
	return 0;
}

结果如图: 在这里插入图片描述

5.2 *

运算符 * ⽤来完成乘法。

#include <stdio.h>
int main()
{
	int num = 5;
	printf("%d\n", num * num); // 输出 25
	return 0;
}
运行结果:
25

5.3 /

运算符 / ⽤来完成除法。 除号的两端如果是整数,执⾏的是整数除法,得到的结果也是整数。


```c
#include <stdio.h>
int main()
{
	printf("%d\n",8 / 2);
	printf("%d\n",7 / 2);
	return 0;
}

结果截图: 在这里插入图片描述 在数学上7 / 2不应该是3.5吗?,这个为什么是3呢?其实在计算机中如果操作符两边都是整数那么除法的结果也是一个整数,所以在计算机中除法得到的是商,如果7 / -2 呢?结果是-3还是-4,这里举例说明一下

#include <stdio.h>
int main()
{
	printf("%d\n",8 / -2);
	printf("%d\n",7 / -2);
	return 0;
}

在这里插入图片描述 可以看到这里结果是-3,说明除法的结果就是舍弃小数后的结果

如果我们想得到一个精确的值怎么办呢? 如果希望得到浮点数的结果,两个运算数必须⾄少有⼀个浮点数,这时 C 语⾔就会进⾏浮点数除法

#include <stdio.h>
//%f -- float
//%lf -- double
int main()
{
 float x = 6.0 / 4; // 或者写成 6 / 4.0
 printf("%f\n", x); // 输出 1.500000
 return 0;
}
运行结果:
1.500000

如果我们以后希望通过除法算得一个小数,就必须要除号的两端至少有一个是小数。

再看⼀个例⼦:

#include <stdio.h>
int main()
{
 int score = 5;
 score = (score / 20) * 100;
 return 0;
}

上⾯的代码,你可能觉得经过运算, score 会等于 25 ,但是实际上 score 等于 0 。这是因为score / 20 是整除,会得到⼀个整数值 0 ,所以乘以 100 后得到的也是 0 。为了得到预想的结果,可以将除数 20 改成 20.0 ,让整除变成浮点数除法。

#include <stdio.h>
int main()
{
 int score = 5;
 score = (score / 20.0) * 100;
 printf("%d",score);
 return 0;
}
运行结果:
20

5.4 %

运算符 % 表⽰求模运算,即返回两个整数相除的余值。这个运算符只能用于整数,不能用于浮点数

#include <stdio.h>
int main()
{
	int x = 6 % 4; // 2
	printf("%d",x);
	return 0;
}
运行结果:
2

负数求模的规则是,结果的正负号由第⼀个运算数的正负号决定。

#include <stdio.h>
int main()
{
 printf("%d\n", 11 % -5); // 1
 printf("%d\n",-11 % -5); // -1
 printf("%d\n",-11 % 5); // -1
 return 0;
}

结果如图: 在这里插入图片描述 上面示例中,第⼀个运算数的正负号( 11 或 -11 )决定了结果的正负号。 其实关于负数求模的应用场景还是还少的,但是C语言支持这种运算

6. 赋值操作符:= 和复合赋值

在变量创建的时候给⼀个初始值叫初始化,在变量创建好后,再给⼀个值,这叫赋值

int a = 100;//初始化
a = 200;//赋值,这⾥使⽤的就是赋值操作符

赋值操作符 = 是⼀个随时可以给变量赋值的操作符。

6.1 连续赋值

int a = 3;
int b = 5;
int c = 0;
c = b = a+3;//连续赋值,从右向左依次赋值的。

C语⾔虽然⽀持这种连续赋值,运算规则是从右边向左边依次赋值,但是写出的代码不容易理解,建议还是拆开来写,这样⽅便观察代码的执⾏细节。

int a = 3;
int b = 5;
int c = 0;
b = a+3;
c = b;

这样写,在调试的是,每⼀次赋值的细节都是可以很⽅便的观察的。

值得一提的是C语言不允许在赋值的时候连续初始化。

在这里插入图片描述 可以看到这里这里报错的原因是未定义的标识符b,为什么会这样呢?原因很简单,因为赋值语句的运算规则是从右向左依次执行,所以在把6赋给b时,由于这里的int只对a起作用,也就是说这里只定义的a这个变量,但是把6赋给b的时候,由于b没有被定义所以找不到b,也就是未定义的标识符b。

C语⾔中提供了复合赋值符,⽅便我们编写代码,这些赋值符有:

1 += -=
2 *= /= %=
//下⾯的操作符后期讲解
3 >>= <<=
4 &= |= ^=

代码举例:

#include <stdio.h>
int main()
{
	int a = 3;
	int b = 0;
	a  = a + 3; //a += 3
	b = b - 3;//b -= 3

7. 单⽬操作符:++、- -、+、-

前⾯介绍的操作符都是双⽬操作符,有2个操作数的。C语⾔中还有⼀些操作符只有⼀个操作数,被称 为单⽬操作符。 ++、--、+(正)、-(负) 就是单⽬操作符的。

7.1 ++和- -

++是⼀种⾃增的操作符,⼜分为前置++和后置++,--是⼀种⾃减的操作符,也分为前置--和后置--.

7.1.1 前置++

#include <stdio.h>
int main()
{
	int a = 10;
	int b = ++a;//++的操作数是a,是放在a的前⾯的,就是前置++
	//先加一,后使用
	//a=a+1,b=a;
	printf("a=%d b=%d\n",a , b);
	return 0;
}

运行结果:
a=11 b=11

计算口诀:先+1,后使用;

a原来是10,先+1,后a变成了11,再使⽤就是赋值给b,b得到的也是11,所以计算技术后,a和b都 是11,相当于这样的代码:

int a = 10;
a = a+1;
b = a;
printf("a=%d b=%d\n",a , b);

7.1.2 后置++

#include <stdio.h>
int main()
{
	int a = 10;
	int b = a++;//++的操作数是a,是放在a的后⾯的,就是后置++
	//后置++
	//先使用,后+1
	//b=a,a=a+1
	printf("a=%d b=%d\n",a , b);
	return 0;
}
运行结果:
a=11 b=10

计算口诀:先使用,后+1

a原来是10,先使⽤,就是先赋值给b,b得到了10,然后再+1,然后a变成了11,所以直接结束后a是 11,b是10,相当于这样的代码:

int a = 10;
int b = a;
a = a+1;
printf("a=%d b=%d\n",a , b);

7.1.3 前置- -

如果你听懂了前置++,那前置--是同理的,只是把加1,换成了减1;

计算口诀:先-1,后使用

int a = 10;
int b = --a;//--的操作数是a,是放在a的前⾯的,就是前置--
//a=a-1,b=a;
printf("a=%d b=%d\n",a , b);//输出的结果是:9 9

7.1.4 后置--

同理后置--类似于后置++,只是把加⼀换成了减⼀

计算⼝诀:先使⽤,后-1

int a = 10;
int b = a--;//--的操作数是a,是放在a的后⾯的,就是后置--
//b=a,a=a-1;
printf("a=%d b=%d\n",a , b);//输出的结果是:9 10

如果我们只是为了让一个变量减一个,使用前置减减和后置减减都可以,但是如果我们想在减一之前把它的值存下来,那我们应该使用后置减减,如果我们希望减一之后付给别人,那我们使用后置减减

7.2 + 和 -

这⾥的+是正号,-是负号,都是单⽬操作符。

运算符 + 对正负值没有影响,是⼀个完全可以省略的运算符,但是写了也不会报错。

int a = +10; 等价于 int a = 10;

运算符 - ⽤来改变⼀个值的正负号,负数的前⾯加上 - 就会得到正数,正数的前⾯加上 - 会得到负 数。

int a = 10;
int b = -a;
int c = -10;
printf("b=%d c=%d\n", b, c);//这⾥的b和c都是-10
int a = -10;
int b = -a;
printf("b=%d\n", b); //这⾥的b是10

8. 强制类型转换

在操作符中还有⼀种特殊的操作符是强制类型转换,语法形式很简单,形式如下:

int a = 3.14;
//a的是int类型, 3.14是double类型,两边的类型不⼀致,编译器会报警告

在这里插入图片描述 如果我们希望消除这个警告,需要用到强制类型转换

int a = (int)3.14;//意思是将3.14强制类型转换为int类型,这种强制类型转换只取整数部分

我们使⽤强制类型转换都是万不得已的时候使⽤,如果不需要强制类型转化就能实现代码,这样⾃然更好的。