C语言中基本数据类型、声明、作用域、存储类型

文章详细介绍了C语言的基本数据类型,包括整型、浮点型和指针,强调了枚举和字符串常量的特性。此外,还讨论了变量的声明、初始化、数组和指针的声明,以及常量的使用。文章还涵盖了作用域的概念,包括代码块、文件、原型和函数作用域,以及链接属性和存储类型,特别是`static`关键字的用途。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 基本数据类型

在C语言中,仅有四种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等),其他所有类型都是从这4种基本类型的某种组合派生而来。

1.1 整型

整型家族包括字符、短整型、整型和长整型,它们都分为有符号(signed)和无符号(unsigned)两种版本。听上去“长整型”所表示的值应该比“短整型”所能表示的值要大,但这个假设不一定正确。实际上规则规定的是:长整型至少应该和整型一样长,而整型至少应该和短整型一样长。所以长整型不是必须比短整型长,只是不比短整型短。

short int 至少16位,long int 至少32位,至于缺省的 int 究竟是16位还是32位,或者是其他值,则由编译器设计者决定。

尽管设计 char 类型变量的目的是为了让它们容纳字符型值,但字符在本质上是小整型值。缺省的 char 要么是 signed char,要么是 unsigned char,这取决于编译器,所以不同机器上的 char 可能拥有不同范围的值。只有当程序所使用的 char 型变量的值位于 signed char 和 unsigned char 的交集中,这个程序才是可移植的。例如 ASCII 字符集中的字符都是位于这个范围内的。当可移植性问题比较重要时,最佳方案就是把存储于 char 型变量的值限制在 signed char 和 unsigned char 的交集内,这可以获得最大程度的可移植性,同时又不牺牲效率。并且,只有当 char 型变量显式声明为 signed 或 unsigned 时,才对它执行算术运算。

1.1.1 整型字面值

字面值这个术语是字面值常量的缩写——这是一种实体,指定了自身的值,并且不允许发生改变。ANSI C允许命名常量(声明为 const 的变量)的创建,它与普通变量极为类似,区别在于当它被初始化以后,它的值便不能改变。

在整数字面值后面添加 L 或 l ,可以使这个整数被解释为 long 整型值,字符 U 或 u 则用于把数值指定为 unsigned 整型值。如果两个都添加,那么它被解释为 unsigned long 整型值。

在源代码中,用于表示整形字面值的方法有很多,其中最自然的方式是十进制整数值。整数也可以用八进制来表示,只要在数值前面以0开头。整数也可以用十六进制来表示,它以0x开头。在缺省情况下,字面值的类型是能表示该字面值类型中最短的但足以容纳整个值的类型。

另外还有字符常量,它们的类型总是 int,不能在它们后面添加 unsigned 或 long 后缀。

最后,如果一个多字节字符常量的前面有一个 L ,那么它就是宽字符常量。

1.1.2 枚举类型

这篇文章将枚举类型介绍的非常简洁易懂:https://www.runoob.com/cprogramming/c-enum.html

1.2 浮点类型

诸如 3.14159 和 6.023 * 1023 这样的数值无法按照整数存储,第一个数并非整数,而第二个数远远超出了计算机整数所能表达的范围。但是它们可以用浮点数的形式存储,浮点数通常以一个小数以及一个以某个假定数为基数的指数组成。

浮点数家族包括 float、double 和 long double 类型。通常这些类型分别提供单精度、双精度以及在某些支持扩展精度的机器上提供扩展精度。标准规定了一个最小范围:所有浮点类型至少能够容纳从 10-37 到 1037 之间的任何值。

浮点数字面值在缺省情况下都是double类型的,除非它的后面跟一个 L 或 l 表示它是一个 long double 类型的值,或者跟一个 F 或 f 表示它是一个 float 类型的值。

1.3 指针

指针是C语言为什么如此流行的一个重要原因,指针可以有效地实现诸如 tree 和 list 这类高级数据结构。在 pascal 和 Modula-2 中,也实现了指针,但它们不允许在指针上执行算术或比较操作,也不允许以任何方式创建指向已经存在的数据对象的指针。所以,C语言可以比其他语言编写出更为紧凑和有效的程序。

变量的值存储于计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用,就像一条街道上的房子由它们的门牌号码标识一样。指针只是地址的另一个名字罢了,指针变量就是一个其值为另外一个(一些)内存地址的变量。

1.3.1 指针常量

指针常量与非指针常量在本质上是不同的,因为编译器负责把变量赋值给计算机内存中的位置,程序员事先无法知道某个特定的变量将存储到内存中的哪个位置。所以应该通过操作符获得一个变量的地址而不是直接把它的地址写成字面值常量的形式,因为当一个函数每次被调用时,它的局部变量可能每次分配的内存位置都不相同。因此把指针常量表达为数值字面值的形式几乎没有用处,C语言内部并没有特地定义这个概念。

1.3.2 字符串常量

C语言不存在字符串类型,但C语言提供了字符串常量。字符串:一串以 NUL 字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是C语言没有显式的字符串类型的原因。之所以选择 NUL 作为字符串的终止符,是因为它不是一个可打印的字符。

字符串常量的书写方式是用一对双引号包围一串字符:"Hello" 。如果你需要修改字符串,最好将它存储于数组中。

之所以把字符串常量和指针放在一起讨论,是因为在程序中使用字符串常量会生成一个“指向字符的常量指针”。当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。所以你可以把字符串常量赋值给一个“指向字符的指针”,但不能把它赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。

标准C函数库包含了一组函数,它们专门用于操纵字符串,包括对字符串进行复制、连接、比较以及计算字符串长度和在字符串中查找特定字符的函数。

2. 基本声明

下表显示了所有这些变量声明的类型,同一个框里的所有声明都是等同的。signed 关键字一般只用于 char 类型,因为其他整型类型在缺省情况下都是有符号数。至于 char 是否是 signed,则因编译器而异。浮点类型在这方面要简单一些,因为除了 long double 之外,其余几个说明符(short、signed、unsigned)都是不可用的。
在这里插入图片描述

2.1 初始化

在一个声明中,你可以给一个标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号),后面是你想要赋给变量的值。例如:

int j = 15;

这条语句声明 j 为一个整型变量,其初始值为15。

2.2 声明简单数组

为了声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。例如下例:我们声明了一个整型数组,数组包含20个整型元素。

int values[20];

数组的下标总是从0开始,最后一个元素的下标是元素的数目减1。

C数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。一个良好的编程经验是:如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一个用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测, 确保它们位于有效的范围之内

2.3 声明指针

int *a;

这条语句表示表达式 *a 产生的结果类型是 int。知道了*操作符执行的是间接访问操作以后 ,我们可以推断 a 肯定是一个指向 int 的指针。

注意:C在本质上是一种自由形式的语言,如果你将上述声明写为int* a;与之前声明具有相同的意思,但这并不是一个好技巧,原因如下

int*  b,c,d;

人们很自然的以为这条语句把所有三个变量声明为指向整型的指针,但事实上并非如此。星号实际上是表达式 *b 的一部分,只对这个标识符有用。b是一个指针,但其余两个变量只是普通的类型。要声明三个指针,正确的语句如下:

int *b,*c,*d;

在声明指针变量时,你也可以为它指定初始值:

char *message = "hello world!";

这种类型的声明所面临的一个危险是你容易误解它的意思。在前面一个声明中,看上去初始值似乎是赋给表达式 *message ,事实上它是赋给 message 本身的。换句话说,前面一个声明相当于:

char *message;
message = "hello world!";

2.4 隐式声明

C语言中有几种声明,它的类型名可以省略。例如,函数如果不显式地声明返回值的类型,它就默认返回整型。如果使用旧风格声明函数的形式参数时,如果忽略参数的类型,编译器就默认它们为整型。如果编译器可以得到充足的信息推断出一条语句实际上是一个声明时,如果它缺少类型名,编译器也会假定它为整型。

3. typedef

C语言支持一种叫做 typedef 的机制,它允许你为各种数据类型定义新名字。例如下面这个声明:

char *ptr_to_char;

把变量 ptr_to_char 声明为一个指向字符的指针。但是在你添加关键字 typedef 后:

typedef char *ptr_to_char;

这个声明把标识符 ptr_to_char 作为指向字符的指针类型的新名字。你可以像使用任何预定义名字一样在下面的声明中使用这个新名字:

ptr_to_char a; //声明a是一个指向字符的指针

注意:应该使用 typedef 而不是 #define 来创建新的类型名,因为后者无法正确地处理指针类型。例如:

#define d_ptr_to_char char*;
d_ptr_to_char a,b;

正确地声明了a,但是b却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用 typedef 更为合适。

4. 常量

ANSI C允许你声明常量,常量的样子和变量完全一样,只是值不能修改。可以使用 const 关键字来声明常量,如下:

int const a;
const int a;

由于a的值无法修改,所以你无法把任何东西赋值给它。给它赋值的两种方法,其一可以在声明时对它进行初始化:

int const a = 15;

其二,在函数中声明为 const 的形参在函数被调用时会得到实参的值。

当涉及到指针变量时,指针变量和它所指向的实体都有可能成为常量,下面是几个声明的例子:

int *pi;
// pi是一个普通的指向整型的指针。

int const *pci;
// pci是一个指向整型常量的指针,你可以修改指针的值,但不能修改它所指向的值。

int * const cpi;
// pci为一个指向整型的常量指针,此时指针的值无法修改,但你可以修改它所指向的整型的值。

int const * const cpci;
// cpci无论是指针本身还是它所指向的值都是常量,不允许修改。

#define 指令是另一种创建名字常量的机制,例如下面这两个声明都为50这个值创建了名字常量:

#define MAX_ELEMENTS 50
int const max_elements = 50;

在这种情况下,使用 #define 比使用 const 常量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。const 变量只能用于允许使用变量的地方

5. 作用域

当变量在程序的某个部分被声明时,只有在程序的一定区域才能被访问。这个区域由标识符的作用域决定。例如函数的局部变量的作用域局限于该函数的函数体。这个规则意味着:1、其他函数无法通过这些变量的名字访问他们,因为变量在它们的作用域之外便不在有效;2、只要分属不同的作用域,你可以给不同的变量起同一个名字。

编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。

5.1 代码块作用域

位于一对花括号之间的所有语句称为一个代码块,任何在代码块的开始位置声明的标识符都具有代码块作用域,表示它们可以被这个代码块中的所有语句访问。

当代码块处于嵌套状态时,声明于内层代码块的标识符的作用域到达该代码块的尾部便告终止。如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,外层标识符无法在内层代码块中通过名字访问。例如下图声明 9 的 f 和声明 6 的 f 是不同的变量。
在这里插入图片描述

我们应该避免在嵌套的代码块中出现相同的变量名,因为这容易引起混淆。

声明于每个代码块的变量无法被另一个代码块访问,因为它们的作用域并无重叠之处。由于两个代码块的变量不可能同时存在,所以编译器可以把它们存储于同一个内存地址。例如,声明 10 的 i 可以和声明 9 的任何一个变量共享同一个内存地址。因为在任何时刻,两个非嵌套的代码块最多只有一个处于活动状态。

5.2 文件作用域

任何在所有代码块之外声明的标识符都具有文件作用域,它表示这些标识符从他们的声明之处直到它所在的源文件结尾处都是可以访问的。例如上图中的声明 1 和 2。

5.3 原型作用域

原型作用域只适用于在函数原型中声明的参数名,如上图的声明 3 和声明 8。在原型中,参数的名字并非必需,但是如果出现参数名,你可以随便取名字,不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。

5.4 函数作用域

函数作用域只适用于语句标签,语句标签用于 goto 语句。基本上函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。

6. 链接属性

当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。标识符的链接属性决定如何处理在不同文件中出现的标识符。

链接属性一共有3种—— external(外部)、internal(内部)、none(无)。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。属于 internal 链接属性的标识符在同一个源文件内的所有声明中都指向同一个实体,但位于不同源文件的多个声明则分属于不同的实体。属于 external 链接属性的标识符无论声明多少次、位于几个源文件都表示同一个实体。

在下图中,缺省情况下,标识符 b、c、f 的链接属性为 external,其余标识符的链接属性都为 none。
在这里插入图片描述

关键字 extern 和 static 用于在声明中修改标识符的链接属性。如果某个声明在正常情况下具有 external 链接属性,在它前面加上 static 关键字可以让它的链接属性变为 internal。例如第2个声明像下面这样书写变量b就将为这个源文件所私有:

static int b;

类似,也可以把函数声明为 static,如下:

static int c(int d)

这可以防止它被其他源文件调用。

static只对缺省链接属性为 external 的声明才有改变链接属性的效果。extern 关键字的规则更为复杂,一般而言它为一个标识符指定 external 链接属性,这样就可以访问在其他任何位置定义的这个实体。当 extern 关键字用于源文件中一个标识符的第1次声明时,它指定该标识符具有 external 链接属性,但如果它用于该标识符第2次或以后的声明时,它并不会更改由第1次声明所指定的链接属性

7. 存储类型

变量的存储类型是指存储变量值的内存类型。存储类型决定变量何时创建、何时销毁以及它的值将保持多久。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器

变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。静态变量在程序运行之前创建,在程序的整个执行期间始终存在,始终保持原先的值,除非给它赋一个不同的值或者程序结束。

在代码块内部声明的变量第缺省存储类型是自动的,也就是说它存储于堆栈中,称为自动(auto)变量。在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。如果该函数被反复调用,这些自动变量每次都将重新创建,在堆栈中所占据的内存位置有可能和原先的位置相同,也可能不同。

对于在代码块内部声明的变量,如果给它加上关键字 static,可以使它的存储类型从自动变为静态。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。

关键字 register 可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常,寄存器变量比存储于内存的变量访问起来效率更高。但编译器并不一定会理睬 register 关键字,如果有太多变量被声明为 register,它只会选取前几个实际存储于寄存器中,其余按普通自动变量处理。

寄存器变量的创建和销毁时间和自动变量相同,但它需要一些额外的工作。在一个使用寄存器变量的函数返回之前,这些寄存器先前存储的值必须恢复,确保调用者的寄存器变量未被破坏。机器可以使用运行时堆栈来完成这个任务,当函数开始执行时,它把需要使用的所有寄存器的内容都保存到堆栈中,当函数返回时,这些值再复制回寄存器中。

初始化

自动变量和静态变量的初始化存在一个重要的差别。在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。如果不显式地指定其初始值,静态变量将初始化为0。

因为程序链接时还无法判断自动变量的存储位置,所以自动变量没有缺省的初始值,而显式的初始化将在代码的起始处插入一条隐式的赋值语句。这会造成4种后果:1、自动化的初始化较之赋值语句效率并无提高;2、自动变量在程序执行到它们所声明的函数时,每次都将重新赋值;3、由于初始化在运行时执行,你可以用任何表达式作为初始化值;4、除非你对自动变量进行显式的初始化,否则当自动变量创建时,它们的值总是垃圾。

8. static 关键字

当它用于函数定义时,或用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 改为 internal,但标识符的存储类型和作用域不受影响。用这种类型声明的函数或变量只能在声明它们的源文件中访问

当它用于代码块内部的变量声明时,static 关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值