第3章 数据
本章将描述数据的各种类型、它的特点并如何声明他。也将描述变量的三个属性——作用域、链接属性和存储类型。
C语言仅有4种基本数据类型——整形、浮点型、指针和聚合类型(如数组和结构)。
整形包括字符、短整型、整形和长整型,均分为有符号和无符号两个版本。其中,标准并未规定长整形必须比短整型长,只是说不能比短整型短而已。
类型 | 最小范围 |
char | 0到127 |
signed char | -127到127 |
unsigned char | 0到255 |
short int | -32767到32767 |
unsigned short int | 0到65535 |
int | -32767到32767 |
unsigned int | 0到65535 |
long int | -2147483647到2147483647 |
unsigned long int | 0到4294967295 |
由图可见,short int至少16位,long int至少32位。具体由编译器决定,通常会自动选择最自然高校的位数。
需要注意,缺省的char类型变量要么是signed char,要么是unsigned char,这取决于编译器,这意味着不同机器上缺省的char可能拥有不同范围的值,当程序所使用的char型变量的值位于两者的交集中,这个程序才是可移植的。所以,显示地把这类变量申明为signed或unsigned,可以提高这类程序的可移植性。
但未必所有时候都合适,这样修改可能导致效率受损,或者因为一些处理字符的库函数,他们本身将字符申明为char,因此导致一些兼容性问题。
1.整形字面值
字面值这个术语是字面值常量的缩写,指定了自身的值,不允许修改。
十进制整形字面值可以是int、long、unsigned long。
八进制和十六进制字面值的类型可能是int、unsigned int、long或unsigned long。八进制例如0176,0177777,00060,开头必须为0。十六进制字面值例如0x7b,0xFFFF等以0x开头的数。
另外还有字符常量,他们的类型总是int。如果一个多字节字符常量的前面有一个L,那么它就是宽字符常量。如L'x',L'e^',仅当运行环境支持一种宽字符集时才可能使用。
如果一个值被当作字符使用,那么把这个值表示为字符常量可以使这个值的意思更为清晰,比如:
value = value - ‘0’;它用于表示把一个字符转换为二进制值,方便不管采用何种字符集,字符常量总会产生正确的值。 //待尝试
2.枚举类型
枚举类型就是指它的值为符号常量而不是字面值的类型,比方说他们以下面形式声明:
enum Jar_Type {Cup,PINT,QUART,HALF_GALLON,GALLON};
忘了说明一点,我们称这条语句声明了一个类型,称为Jar_Type;或者这么理解,我们声明了一个变量名为Jar_Type的枚举类型数据。这点细节在初次阅读时稍有不解,但并不影响我们理解枚举类型的数据变量如何使用。
枚举类型的变量实际上以整型的方式存储,这些符号名的实际值都是整型值。比如Cup是0,PINT是1,以此类推。也可以自己定义他们的整型值,如下:
enum Jar_Type {Cup = 8,PINT = 16,QUART = 32,HALF_GALLON = 64 ,GALLON = 128};
只对部分符号名进行赋值也是合法的,如果未显式指定一个值,那么他的值就比前面一个符号大1。
再次强调,在使用上枚举类型申明的字符变量仅仅是符号样式,其使用中将转换为整数类型,参考以下代码的结果。
#include <stdio.h>
int main(){
//枚举颜色
enum color{red=1, oreange=2, yellow=3, green=4, ching=5, blue=6, purple = 7} day;
enum mouth {year,llt,kkt};
printf("%d %d %d %d %d %d %d %d %d %d %d", red, oreange, yellow, green, ching, blue,purple,day,year,llt,kkt);
}
//输出结果为"1 2 3 4 5 6 7 0 0 1 2"
尝试过程中发现,VSCODE使用的C语言标准版本不同,跟书中描述有差异
无法使用enum mouth,year,llt,kkt;这种写法
因此组合使用两种定义方法时赋值也会混乱↓
enum Jar_Type {Cup,PINT,QUART,HALF_GALLON,GALLON} milk_jug , gas_can ,medicine_bottle ;
建议按color这一程序段的形式来书写
3.浮点类型
带小数,或者过大的可以转换为科学计数法的数据,可以用浮点数形式存储。
浮点数包括float、double、long double类型,分别提供单精度,双精度和扩展精度。标准规定了一个最小范围,所有浮点类型至少能够容纳从1e-37到1e37之间的任何值。
科学计数法,打比方,1e6就是1乘以10的六次方,就是100000
头文件float.h定义了名字FLT_MAX、DBL_MAX和LDBL_MAX,分别表示float,double,long double所能存储的最大值。同理,FLT_MIN等表示能存储的最小值,可以用以下代码来尝试。
#include <stdio.h>
#include <float.h>
int main()
{
printf("The maximum value of float = %.10e\n", FLT_MAX);
printf("The minimum value of float = %.10e\n", FLT_MIN);
printf("The number of digits in the number = %.10e\n", FLT_MANT_DIG);
}
输出结果为
The maximum value of float = 3.4028234664e+038
The minimum value of float = 1.1754943508e-038
The number of digits in the number = 1.1857575500e-322
浮点数字面值在缺省情况下都是double类型的,除非他的后面跟随一个 l 或 L 来表示他是long double类型,或者跟一个 f 或 F 表示他是flota类型,如下:
float a = 1.5;
float b = 1.5f;
区别在于,数据后面跟上f,l等字符,会更加严格地界定数据的类型和界限,在数据较大时防止意外。
4.指针类型
指针可以有效地实现诸如tree和list这类高级数据结构。其他语言也实现了指针,但是他们不允许在指针上执行算术或比较操作,也不允许以任何方式创建指向已经存在的数据对象的指针。正是由于这方面的限制,C语言可以比使用其他语言编写出更为紧凑和有效的程序。
但C对指针的使用不加限制使得程序经常报错,经常需要复查修改。
变量的值存储于计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用。指针只是地址的另一个名字罢了。指针变量就是其值是另一个内存地址。
无需理解指针常量这个概念,因为都是调用,而非自己去定义变量的常量值。(有一个例外,NULL指针,它可以用零值来表示,详见之后在标准库函数中的描述)
C语言存在字符串的概念:它就是一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是C语言没有显示的字符串类型的原因。由于NUL字节是用来终结字符串的,所以字符串内部不能有NUL字节。
之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。
字符串常量往往是这些字符存储的地址,而不是字符本身,所以不能赋值给字符数组。
5.基本申明
基本申明包括声明变量,声明数组,声明指针。
以上仅知道基本的数据类型,我们需要了解如何声明变量。变量声明的基本形式如下:
//说明符 声明表达式列表
int i;
char j,k,l;
说明符包含一些关键字,对于关键字的部分昨天已经描述过。说明符可用于改变标识符的缺省存储类型和作用域。
往往在声明一个标量变量之后,我们需要给他指定一个初始值,方法是在他的后面跟一个等号(赋值号),例如:
int k = 15;
声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个证书,指定数组中元素的个数,例子如下:
int values[20];
声明指针时,先给出一个基本类型,再跟随一个标识符列表,这些标识符组成表达式,用于产生基本类型的变量,例如:
int* a;
这条语句表达式有意思的一点是,原本可以写成“int *a”的情况,但我选择写成这种形式,是因为从叙述上而言,a被声明为类型为 int* 的指针,但实际上,星号是表达式*a的一部分,仅仅对跟随的标识符有效,在一个程序段内申明多个指针变量的时候,建议采用以下写法:
int *a,*b,*c;
书中还描述了隐式申明这种说法,比如:
f(x){
return x+1 ;
}
正常如下:
int f(x){
return x+1 ;
}
理论上,函数如果不显示地声明返回值地类型,他就默认返回整型。但尽可能规范书写,部分新编译器会检查这方面缺失的错误,不要依赖默认值,这样也方便个人对代码的检查。
6.typedef
C语言支持一种叫做typedef的机制,它允许你为各种数据类型定义新名字,其写法与普通的声明基本相同,只是把typedef这个关键字放在声明的前面,例如:
typedef char *ptr;
ptr a;
这个声明把标识符ptr作为指向字符的指针类型的新名字,以此来声明a是一个指向字符的指针。
使用该方法可以减少因声明复杂数据类型带来的危险,另外需要注意的是,应该使用typedef而不是#define,后者没法正确地处理指针类型,如下:
#define d_ptr char *
d_prt a,b;
#define仅正确声明了a,b却被声明为一个字符。
7.常量
ANSI C允许你声明常量,常量的样子和变量完全一样,只是他们的值不能修改,坚持选用一种方式来声明吧。
int const a;
const int a;
当然,由于a的值无法修改,所以可以在声明的时候对它进行初始化。
const int a = 14;
在函数中声明为const的形参在函数被调用时会得到实参的值。
但当涉及指针变量时,就得多加思考,因为指针变量有两种东西可以变为常量——指针地址和它所指向的实体,以下我将举一些例子:
int *pi;
int const *pci;
int * const cpi;
int const * const cpic;
这几个变量从上到下分别表示:
pi 是一个普通的指向整型的指针。
pci 是一个指向整型常量的指针,你可以修改指针的值,但不可以修改指针指向的值。
cpi 则表示它是一个指向整型的常量指针,不可以修改指针的值,但可以修改指针指向的值。
cpic 则表示无论是指针本身还是它所指向的值,都不能修改。
当你申明变量时,如果变量的值不会被修改,应当在声明时使用const关键字,这也是良好的编程习惯的一部分。
#define指令是另一种创建名字常量的机制,请看下例:
#define MAX_ELEMENTS 50
int const max_elements = 50;
在这种情况下,使用#define会比const变量要好,因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。const变量只能用于允许使用变量的地方。
8.作用域
当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域决定。标识符的作用域就是程序中该标识符可以被使用的区域。
例如,函数的局部变量的作用域局限于该函数的函数体。
编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。
位于一对花括号之间的所有语句成为一个代码块,在花括号内开头部分声明的标识符都具有代码块作用域,表示他们可以被这个代码块中的所有语句访问,例如:
int b {
int c;
...
...
}
避免在嵌套的代码块中使用相同的变量名,这没必要。
任何在代码块之外声明的标识符都具有文件作用域,他表示这些标识符从他们的声明指出知道它所在的源文件结尾处都是可以访问的。
在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。
原型作用域只适用于在函数原型中声明的参数名。
最后的函数作用域只适用于语句标签,语句标签用于goto语句,该作用域可以简化为一条规则,即一个函数中的所有语句标签必须唯一。
//也许这个概念学习到了,也不一定有确切要理解使用的场景。
9.链接属性
相同的标识符可能出现在不同的程序源文件中,这时候需要标识符的链接属性决定如何处理在不同文件中出现的标识符。标识符的作用域与他的链接属性有关,但这两个属性并不相同。
链接属性一共有3种——external(外部),internal(内部)和none(无)。
没有链接属性的标识符none总是被当作单独的个体,也就是说该标识符总是被当作单独的个体,或者说该标识符的多个声明被当作独立不同的实体。
internal链接属性的标识符在同一个源文件内的所有声明种都指同一个实体,位于不同源文件的多个相同声明则分属不同的实体。
external链接属性的标识符则不论声明多少次,位于几个源文件都表示同一个实体。
关键字extern和static用于在声明种修改标识符的链接属性。后者只对缺省链接属性为external的声明才有改变链接属性的效果,好处是可以防止它被其他源文件调用。前者指定external链接属性,就可以访问在其他位置定义的这个实体。
10.存储类型
变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
变量的缺省存储类型取决于它的声明位置。凡是在代码块之外声明的变量总是存储与静态内存中,也就是不属于堆栈的内存,这类变量被成为静态变量。对于这类变量,你无法为他们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个执行期间始终存在。
在代码块内部声明的变量的缺省存储类型是自动的,也就是说它存储于堆栈中,称为自动变量。有一个关键字auto就是用于修饰这种存储类型的,但很少使用,因为代码块中的变量在缺省情况下就是自动变量。自动变量在代码块执行完毕后就消失,代码块再次执行时再创建。
在代码块内部声明的变量,如果给它加上static,可以使它的存储类型从自动变为静态,然后该变量可以在整个程序执行过程中一直存在。
函数的形参不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
最后,关键词register可用于声明,该变量应该存储于机器的硬件寄存器而不是内存中,这类变量被称为寄存器变量。通常,寄存器变量的访问起来效率更高,但有时,编译器会自己优化选取几个变量存储于寄存器中,
在典型情况下,把一些指针声明为寄存器变量,程序的效率将得到提高,尤其是频繁执行间接访问的指针。
自动变量初始化后建议都进行显式的赋值,否则当变量被创建时,他们总是垃圾。常见于变成各种随机数。
11.static关键字
该关键字用于函数定义、或是代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明他们的源文件中访问。
当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量改为静态变量,但变量的链接属性和作用域不受影响。这种方式创建的变量在程序执行前生成,在代码块结束后并不销毁。
注意理清楚区别,接下来将实例说明。
12.作用域、存储类型示例
先上代码:
int a = 5;
extern int b;
static int c;
int d ( int e )
{
int f = 15;
register int b;
static int g = 20;
extern int a;
...
{
int e;
int a;
extern int h;
...
}
...
{
int x;
int e;
...
}
...
}
static int i ()
{
...
}
...
按顺序分析
变量a处在文件作用域下,且在缺省情况下为external链接属性,其他源文件也可以访问该变量。
变量b也是external属性,且它的前置修饰并无必要。
变量c被修改链接属性为internal,表示仅被当前源文件访问使用。
同时,变量a、b、c的存储类型为静态,表示他们并不是存储于堆栈中,因此这些值将在程序执行前创建并保持到程序结束。
但是代码块中局部声明的变量a和b将暂时隐藏同名的静态变量。
变量d也是在文件作用域下,起到函数原型作用,且具有external属性。如果修饰它为internal,那其他源文件将不能调用这个函数。剩下的存储类型不是问题,因为代码总是存储于静态内存中。
参数e不具有链接属性,我们只能从函数内部通过名字访问它。它具有自动存储类型,当函数返回时就销毁。
参数f、b、g声明的局部变量在函数结束时就销毁。他们不具有链接属性,所以他们不能再函数的外部通过名字访问。但g的存储类型是静态,所以它在程序整个执行过程中一直存在,当函数被重新调用时,它并不会被重新初始化。
第二次声明的参数a不被需要。
接下来的参数e和a,被声明为局部变量,具有自动存储类型,不具有链接属性,由于名字冲突,在这个代码块中之前声明的同名变量是不能被访问的。
变量h为全局变量,它具有external链接属性,存储于静态内存中,它必须使用extern关键字,否则就会变成一个局部变量。
参数x和e为局部变量,自动,无链接属性,作用域局限于本代码块。
最后,函数i 具有静态链接属性,防止被其他源文件调用。
具有external链接属性的实体总是具有静态存储类型。
总结如下:
变量类型 | 声明的位置 | 是否存于堆栈 | 作用域 | 如果声明为static |
全局 | 所有代码块之外 | 否 | 从声明处到文件尾 | 不允许其他源文件访问 |
局部 | 代码块起始处 | 是 | 整个代码块 | 变量不存储与堆栈中,它的值在程序整个执行期一直保持 |
形式参数 | 函数头部 | 是 | 整个函数 | 不允许 |
今日疑问
1.需要查阅一些各种各样的字符集
2.看一下#undef,#define这些,在标准库函数里的使用情况。比如float.h。
3.printf各个格式化输出的%再看一下
感言
1.比我早来一年的同事跟我说,C语言上手很快很简单的。就我们公司目前要求做功能,好像是只要寄存器读写,置位复位就行了,算法啥的根本不讲究,会调用各类函数接口就行了。优化的工作之后再处理。
或者换个方法,以考过计算机二级证书为目标就行,大学生很轻松就能考过的东西(我没考,当时觉得没用emmm),折腾过之后几年都不会忘记C语言。因为真的很简单,有什么好给压力的。
但是,其实这就是一个心理的坎,我自卑,逃避,迷茫,无知,我迈不过去,就像现在说出来,我还是在精神内耗。这些都迫使自己非常需要一个小成就,来给予我正向的信心,依此良性循环。
但无论哪种方法,都需要我坚持,用心地把一件事情做到位。
现在的自己,像一个无头苍蝇一样,盲目,浮躁地煽动翅膀,扰的是自己的心。
2.学完了之后可以尝试写一点速成方案,比如按自己理解,哪些内容是可以不用学就能直接上岗的糊弄技术部老大的,因为有些东西确实学了但很少用,这样也方便新入门的同志。
3.我不是那种非得把今天任务熬夜完成了才睡觉的人,本来身体就不好,11点多必须得睡觉了。
4.双休也有点不想学习。
5.以为前面的部分都是学的可以不用再学的,但是写博客的时候还是花了好久时间。以为双休可以赶很多进度,但不现实。
6.同事说很多博客都是github上搬过来的,我之后考虑学习一下大佬们的开源项目,都是要做的,慢慢来。
7.不是,好端端的双休,我时间呢?我以为一天至少能写个两章。
8.还没开始几天又想放弃写博客了。。可能是没想好自己接下来要干什么,或者单纯意志太过薄弱。
2024.3.31发布