当前位置:   article > 正文

C语言知识总结01——2021/3/13_空格是不是是无法被打印和显示吗

空格是不是是无法被打印和显示吗

温故

2021/3/13 本周所学知识汇总,参考书籍:《C语言程序设计》,《C与指针》。

一、九种数据类型

  • 程序是计算机对数据进行操作的步骤,数据与操作构成了程序的两个要素。数据既是程序必要组成部分,也是程序处理的对象。而C语言为数据提供了九种基本数据类型,这些数据类型用来描述程序中不同数据的数据结构,数据取值范围以及数据在内存中的存储等性质。

  • 不同的数据类型都分为两类讨论,常量以及变量

  • 我们可以将九种数据类型分为三类:

  • 基本类型:整型(int),浮点型,(float,double),字符型(char),枚举型(enum),指针型(*);
    聚合类型【也称构造类型】:数组型(【】),结构型(struct),联合型(union);
    其他类型:空类型(void);


二、关键字的分类

标识符

  • 标识符计算机处理的对象是数据,程序用来描述数据处理的过程。在程序中,通过名字建立对象定义与使用的关系。因此,在C语言中用于标识名字的有效字符序列成为标识符,标识符(identifier)也就是变量,函数,类型等的名字,使编译器通过识别标识符来确定这是一个单独的数据实体。
  • 标识符的组成每个标识符都由字母,数字及下划线组成的随机序列
  • 标识符相关规则1.标识符的第一个字符必须是英文字母或者下划线。2.标识符讲究大小写区分。

关键字

  • 关键字:**关键字是具有特定含义的,专门用来说明C语言的特定程分的一类标识符。**例如,关键字int用来告诉编译器,该标识符后面接着的变量数据为一个整型数据,则自然地该变量就拥有了整型数据地存储空间,也因为被分配了int类型的存储空间大小,也就限制了该变量的取值范围等等,总而言之就是使其有了这个关键字本身所代表的属性。
  • 由ASCII标准定义的C语言关键字共32个
  • 数据类型关键字(8个):int,float,double,char,enum,struct,union,viod。(注意:指针类型是通过符号(*)来告诉编译器为指针类型的
  • 修饰数据关键字(4个):short,long,signed,unsigned。
  • 控制语句关键字(12个):条件语句类:if,else,switch,case,default;循环语句类:for,while,do(do-while),continue,break,goto;其他类型:return。
  • 存储类型关键字(4个):extern,static,auto,register。
  • 其他类型关键字(4个):const,sizeof,typedef,volatile。

三、字符

C语言的字符集(ASCII字符集)

C语言字符集:组成C语言源程序代码的基本字符成为C语言字符集,它是构成C语言的基本元素。C语言允许使用的,并且能被编译器翻译转换成执行程序源代码的可执行运行的基本字符集。
C语言允许使用的基本字符(ASCII–字符常量)如下:

  • 所有的英文字母,区分大小写:a-z,A-Z;
  • 数字0-9;
  • 特殊字符:!" # ’ ( ) * + , - . / : ;< > = ? [ ] \ ^ _ { } | ~ (这里我将空格罗列到了另外一个集合)
    { 以上全是键盘上可输入字符 (当然空格也输入可输入字符)}
  • 空白字符:空格,水平制表符,垂直制表符,换行,换页 (这里除了空格以外的空白字符都是不可打印字符)
  • 不可打印字符:null字符(用于字符串的结束),警报(alert),退格 等等 (也就是ASCII码中为0-31与127的这32个字符)

补充(Supplement)

  • 编译器转换程序源代码时,所处的环境称为翻译环境(translation environment);编译后程序执行时,所处的环境称为运行环境(execution environment)。
  • 对C语言来说,翻译环境和运行环境是不同的。因此,C语言定义了两个字符集(character set):源代码字符集与运行字符集。
    源代码字符集(source character set)是用于组成C源代码的字符集合;
    运行字符集(execution character set)是可以被执行程序解释的字符集合。
  • 在许多C语言的实现版本中,这两个字符集是一样的。如果不一样,则编译器会把源代码中的字符常量和字符串字面量转换成运行字符集中的对应元素。
  • 另外:这些字符都各自对应ASCII码表中的一个数值(0~127),若给一个char类型变量赋值超过0-127的范围,则编译器无法识别并翻译成相应的字符对应到可执行程序中。
字符常量的细分(基本字符集和不可打印字符集)
  • 所谓字符常量,就是用英文单引号括起来的一个字符。
    字符常量分为:普通字符常量(基本字符集)以及转义字符常量(不可打印字符集)(所属ASCII,只是ASCII的另一种代表形式)。
普通字符常量
  • 普通字符常量键盘上可以直接输入上去的字符,它们也称为C语言的基本字符集。
转义字符常量
  • 转义字符常量转义字符是C语言中表示特殊字符的一种特殊形式。==因为ASCII码中有部分无法从键盘输入的字符,比如警鸣,回车键等等,C语言于是设计了转义字符,通过“+字符”来表示这些字符常量。==因此,不可打印字符满足同时满足以下两个条件:1.
    不能在屏幕上显示。2. 无法从键盘上输入。满足以上两种条件的被称为不可打印字符,对应ASCII码中的0~31和127号字符
  • 而这些不可打印字符若想在C语言程序中输入且实现其功能,则C语言提供了两种途径间接表达
  • 1. 转义字符
    2. 直接赋值ASCII码中对应数值
  • 比如你想给字符变量beep(该变量按照单词字面意思使其在C语言程序中具有报警功能)赋值警鸣,而你无法通过键盘来赋值 (scanf输入\a意思是两个字符,但申请的地址空间里只能存放前面的反斜杠字符,其他字符如#¥%都可以通过键盘直接赋值,比如char money=‘¥’;或者用scanf在键盘上直接输入¥给其赋值,则再使用printf("%c",money);即可使变量money就打印成¥的符号),那么则需要用转义字符来直接赋值。
    如下示例:
    1. char beep=’\a’; printf("%c",beep);
    2. 或者直接赋ASCII码里对应数值: char beep=7;
      (注意:不是赋值’7’,单引号括起来的意思是字符7,而字符7被翻译成ASCII码里的数值是55) 这样就让beep有了警报的提示作用。

补充(Supplement)

  • 转义字符虽然包含两个或多个字符,但它只代表一个字符。编译系统在见到字符“\”时,会接着找它后面的字符,把它处理成一个字符,在内存中只占一个字节。

  • 因此我们可以认为:转义字符是特殊的ASCII码,也就是特殊的字符常量,故字符常量中明确包含了转义字符(ASCII中的33个控制字符及通讯专用字符)和键盘上可以按出来的普通字符。

  • ASCII中的控制字符以及通讯专用字符:33个,范围:0~31和127,不是所有的控制字符都可以被转义字符来特殊输出,相反地,大多数ASCII控制字符单独都具有控制功能,因此它们不是转义字符。

字符常量是C语言所提供的最基本字符集合,所有的其他字符集(字符数组)都是通过不同的基本字符组合而成

另外(Addition)

  • 格式转换符是另一类字符集,不是字符常量,本身没有被赋予ASCII码值(没有量)。其作用意义是将数字转化成特定的形式输入或者输出,如scanf %d则是将键盘上敲出的数字码以10进制形式输入到程序中。

四、enum关键字的作用及用途

enum(enumerate)枚举类型在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型显然是不妥当的(比如说星期一如果要用数值代表那就是只能为1,因为1是最清楚能代表星期一的,而如果定义成整型int,则int型变量数值可以是任意能变化的,这样就失去了一个被限定取值的变量的意义)。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

枚举类型的用法:

1. 枚举类型的定义和枚举变量的定义: 在定义枚举变量之前,先定义枚举类型。枚举类型定义格式如下: enum 枚举类型名 { 枚举元素表 }; 例如:enum Day {sum,mon,tue,wed,thu,fir,sat};
定义具有day枚举类型的枚举变量定义可以采用以下两种方法中的任意一种:
enum Day{sum,mon,tue,wed,thu,fir,sat} day1;
等价于:enum{sum,mon,tue,wed,thu,fir,sat} day1;
或者 :

  1. enum Day{sum,mon,tue,wed,thu,fir,sat};
  2. enum Day day1;

2. 枚举类型变量的赋值和使用:
在使用枚举类型时,需要注意以下几点:

  • (1). 在C语言中,对枚举元素是按常量处理的,它们不是变量,不能被赋值,只能被初始化。

  • (2). 枚举元素作为常量,它们是有值的,C语言编译时按定义的顺序依次对它们从整型数据0开始赋值。例如以上元素若全无初始化,则sum=0,mon=1,……,sat=6。
    若以上元素中进行了初始化(初始化可以是任意整型值):

  • 1°:部分初始化:如mon=1,fir=5,则编译时未初始化的按照顺序依次赋值直到被初始化的元素的前一个。如这里按照顺序自动赋值只会给sum赋值,sum=0。mon已经初始化了,其值就是初始化的值,mon后面的元素依次以mon初始化值为基准,后一个元素依次以前一个元素的值为基准赋值前一个元素的值加整型数值1,如tue被自动赋值2((int)mon+1),同理wed被自动赋值(int)tue+1也就是3,……,一直到下一个被初始化的元素,若此后没有第二个被初始化的元素,则一直按照此方法赋值到结尾。因此这里一直到fir停止顺序赋值,fir被初始化赋值为5,下一个元素sat又没初始化了,则继续按照刚刚的规则sat=(int)fir+1。
    (注意:如果这里我不按照正常逻辑,我给mon初始化66,fir初始化100,则该枚举类型元素的值依次为:sum=0,mon=66,tue=67,wed=68,thu=69,fir=100,sat=101,另外:数值顺序无所谓先后,比如我只给sum初始化100,fir=55依然是正确的初始化形式,这样的化mon=101,……,sat=56)

  • 2°:全部初始化:也就是给每个元素初始化你想给定的值即可

  • (3). 一个整数值不能直接赋予一个枚举变量,因为数据类型不对应。 例如不能这样赋值:day1=2;
    这样是错误的,因为左边是enum类型数据,而2是整型常量为int型
    因此如果我们不用enum所定义好的类型里的元素给enum变量赋值,则需要用到强制转换,如:day1=(enum
    Day)2;等价于day1=tue; 同理,虽然这些元素被赋予了整型值,但依然不能给int型变量直接赋值,如:int
    a=mon;是错误的,也需要强制转换:int a=(int)mon;这样是对的。

  • (4). 编译器给枚举变量分配的存储单元大小与整型量相统,枚举变量在输出时,只能输入对应的枚举元素的值(序号)。
    枚举变量可以通过printf()函数输出其枚举元素对应的值,即整型数值。而不能直接通过printf()函数输出其标识符。要想输出其标识符,可以通过数组或者switch语句将枚举值转换为相应的字符进行输出。

  • (5). 当enum与sizeof联用,无论enum声明的类型中的字面型常量集合的元素个数如何,sizeof返回的字节大小永远是4字节。 如 sizeof(enum)=4,sizeof(enum Day)=4,sizeof(enum Day day1)=4。至于为什么不管里面元素个数,返回的储存空间大小都为4 bety,可能是因为enum里的元素为一种狭义宏定义,enum是一种基本数据类型,其类型本身意义是一个字面整型常量的集合,无论定义多少个元素,都相当于是告诉编译器有多少个被宏定义的标识符,因此相当于没有元素,(enum)和(enum 某集合标识符)也因此被理解为只是一种声明某个常量集合的类型说明符,它们都只是一种标识符,一种枚举(集合)类型说明的标识符,其作用仅是告诉编译器程序中定义且引入这些字面型常量,又因sizeof是返回一个变量或者某数据类型所占内存的字节大小,而enum由此可见实质上是一种声明整型常量(枚举/集合的元素)的数据类型(如int这个数据类型描述的是一种占4字节大小空间的整型数值的数据结构,sizeof返回的即是int描述的该数据类型所占内存大小)所以sizeof一个未细分的enum或者sizeof一个细分后的enum,都是相当于返回其所描述对象的数据类型所占空间值。另外,这些标识符用于后续程序中就是被相应整型值替换的数值常量,这些都是被存到代码区的,也就是说enum的元素被存到常量区,相当于告诉了编译器这些自定义的字面值常量。。

    如: enum a {mon=55,tue=66,wed=88};
    printf("%d,%d,%d,b=%d\n",mon,tue,wed);被预编译(预处理)的时候替换等价为:printf("%d,%d,%d,b=%d\n",55,66,88);
    打印的结果为:55,66,88;因此元素既然是被“宏定义”,所以一样可以直接格式转换来打印,因为会被数值替换。**

  • (6). 若某个枚举类型变量未被初始化或者被赋值,则实际上就相当于定义了一个拥有某enum数据类型的而值为整型值的变量,其值是随机的整型值。
    如:
    int a;未被初始化,后续也没赋值就直接输出a的值,a的值为随机整型值
    同理:enum Day day1;
    day1的值也为随机整型值
    两者都是随机整型值是因为它们都被存储到栈区中。(这是栈区变量赋值的一种默认情况,默认在没有确定值的情况下,就赋予随机值)

以下是一个关于enum用法的实例

/*说明一个枚举类型enum month,它的枚举元素为Jan,Feb,…,Dec。编写能
显示上个月名称的函数last_month。例如,输入Jan时能显示Dec。再编写另一个函数
printmon,用于打印枚举变量的值。最后编写主函数调用上述函数生成一张12个月份
及其前一个月份的对照表。*/
#include<stdio.h>

enum month{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec};

char *p[12]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};

void last_month(enum month m)
{
	if(m>=2&&m<=12)
	printf("%s\n",p[m-2]);
	if(m==1)
	printf("%s\n",p[11]);
}

void printmon(enum month m)
{
	printf("%s\n",p[m-1]);
}


void main()
{
	enum month m;
	printf("the last month list:\n");
	for(m=Jan;m<=Dec;m=(enum month)(m+1)) /*注意:这里不能m++,因为m是数值常量,常量不能自增自减,因此这个expression的意思是(m+1)也就是m先从数值上加了1,再将这个(m+1)数值常量强制转换成(enum month)对应到该enum类型的元素,注意m=(m+1)也不对,因为赋值语句右侧的(m+1)是一个int型的数值常量,而左侧m是enum变量,其数据类型是enum main::m,只能接受相应的enum month中的字面值常量*/
		last_month(m);
	printf("the month print:\n");
	for(m=Jan;m<=Dec;m=(enum month)(m+1))
		printmon(m); /*在除了赋值语句以外的情况下,m等价于元素里的每个标识符,而每个标识符又被数值替换,因此m就是一个整型数常量,也就是m本身虽然具有enum的数据类型属性,但其值就是一个数值常量,而赋值语句要考虑数据类型属性是因为涉及到不同数据类型是不同的数据结构*/
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

根据这个例子,总的来看似乎是实现了变量意义的可视化,定义一个显示月份的变量m,循环里的expression3即m等于下一个月份,enum使整个程序清晰明了。但需要注意的是:enum里的元素不能直接输出其标识符,这里是通过字符串指针数组实现的输出打印。enum变量值只用来判断是第几个月份,而这个判断的值通过指针来输出相应月份的标识。

3. enum和#define的区别

- 宏是在预处理阶段直接进行替换并且不进行类型检查,而枚举则是在程序运行之后才起作用,编译后程序识别到enum类型标识符,才进行相应的enum元素声明。
- 宏定义的标识符可以被任意文本内容替换,而枚举只能定义被整型值替换的字面值常量。也就是说define没有类型,但enum有,enum的变量同样具有一个变量该具有的特点,若作用域,值等等,enum变量的值是被限定范围的,且用enum定义的字面型常量去赋予
- 宏定义不会主动赋值,而enum会自动赋值
- 宏定义一次只能定义一个,而enum可以一次定义大量的相关常量
- 宏定义不是实体,而枚举常量是一种实体
  • 1
  • 2
  • 3
  • 4
  • 5

五、typedef关键字的作用及用途

typedef 类型定义符为了适应用户的习惯和便于程序的移植,除了可以直接使用C语言提供的标准类型和自定义的类型(数组,结构体,联合和枚举)外,C语言允许用户通过类型定义将已有的各种类型定义成新的类型标识符。经类型定义后,新的类型标识符与标准类型名一样,可以用来定义相应的变量。

typedef的用法

  • 类型定义的一般形式为:

typedef 原数据类型 新的类型名;
例如:typedef int Length;
typedef int Height;
则此时Length和Height两个类型名标识符都具有了int的属性,可以用这两个新的类型名来定义变量,如Length x;这样x就能很清楚地视为一个表示整型长度的变量。

  • 定义比较复杂的类型形式:

1. 数组:

例如:typedef char Character[20]; 则char str1[20]; 等价于Character str1[20];

2. 指针:

例如:typedef float,* PFLOAT; 则float *p1;等价于PFLOAT p1;

3. 结构体:

例如:typedef struct student{参数列表}List; 则struct student a1;等价于 List a1;

4. 函数:

例如:typedef char DFCH();则char af();等价于DFCH af;

  • 说明(Instruction)
    (1). 用typedef只是给已有类型增加一个别名,并不能创造一个新的类型。只是更加直观与方便。
    (2). typefef与#define有相似之处,但二者是不同的:前者是由编译器在解析时处理的;后者是由编译器在预处理的时候处理的,而且只能做简单的字符串替换,而typedef定义的新的类型名依然能继承被替换的原数据类型的属性。
  • 类型定义符的三个好处
    (1). 简化程序源代码的书写
    (2). 使变量的意义更加明显
    (3). 提高源代码的移植能力

六、空白符与分隔符

空白符

  • 空格、制表符、换行符等统称为空白符(space character),它们只用来占位,并没有实际的内容,也显示不出具体的字符。

  • 程序员要善于利用空白符:缩进(制表符)和换行可以让代码结构更加清晰,空格可以让代码看起来不那么拥挤。

  • 空行用于分隔不同的逻辑代码段,它们是按照功能分段的。

  • c语言由于书写自由。因此我们更应该注意正确优雅的格式,在相邻的不同标识符(包括关键字,用户标识符,变量名,函数名,类型说明符等等)之间必须出现至少一个或者多个空白字符(或注释),不然它们会被编译器解释为单个标记。在绝大多数操作符的使用中,中间都隔以空格。

分隔符

  • 这里我们从scanf的输入来讲,scanf使用所有格式转换符的时候(%c)除外,从键盘上输入的值之前所有的空白符(空格,制表符,换行符等)都将被跳过,这些空白符在输入的值之间是起到分隔输入的不同的各个值的作用,以此来告诉编译器这是两个或多个不同的值输入进来了,因此在值后面的空白符表示告诉编译器该值输入结束。因此在用%s格式输入字符串时,中间不能包含空白,因为空白符在scanf里被视为分隔,以空白来提示数值的跳过或者字符串的结束,因此空白符会被转换成空字符也就是字符串的结束标志‘\0’。
  • 空格、制表符、换行符等统称为空白符,它们只用来占位,并没有实际的内容,也显示不出具体的字符。当C语言对于输入的参数列表,只有遇到分隔符时才会给系统一个指令进入到下一个存储单元里,将下一个参数放入存储单元中。但如果你用逗号分隔,那么就会产生字符偏差,因为逗号也是内容,在分隔的同时也占字符空间。
  • c语言中,分隔符用来分隔多个变量、数据项、表达式等的符号。包括逗号、空白符、分号和冒号。

(1)逗号作为分隔符用来分隔多个变量(数组元素之间逗号隔开)和函数参数;
(2)空白符常用来作为多个单词间的分隔符,也可以作为输数据时自然输入项的缺省分隔符;
(3)分号常用于for循环语中for后面,圆括号内的三个表达式之间;
(4)冒号用于语句标号与语句之间。


七、隐式声明

C语言中有几种声明,它的类型名可以省略。

  1. 例如,函数如果不显式地声明返回值的类型,它就默认返回整型。:function(int x){ statement }; 这里function函数默认返回值为int型
  2. 例如,如果省略了形式参数的类型,编译器就会默认它为整型。:int function(x){ statement }; 这里形式参数x就被默认为整型。
  3. 例如,如果编译器可以得到充足的信息,推断出上一条语句实际上是一个声明的时候,如果这条语句中定义的变量缺少数据类型说明符,编译器会假定它为整型。:上一条语句:int a[10]; 接着:b[5];d; 则在有的编译器里会将数组b和变量d都默认为整型,但这在ANSI C中却是非法的。

八、交叉赋值及格式转换的问题

  • 进行赋值运算时,需要赋值号右边表达式的值赋予左边变量,同时左边变量必须满足是左值表达式。但如果赋值号两边的数据类型不一致,则需要进行强制类型转换,转换的结果总是将右边表达式的类型转换成赋值号左边变量的类型,如果不用强制转换符,则为隐式强制转换。而隐式强制转换,是一种根据转换规则的自动转换,规则是低等级的自动向高等级类型转换。char->int->float->double

    所以,在进行赋值运算时,赋值号两边的数据类型最好一致,不要交叉赋值,至少右边的数据类型要比左边的数据类型级数低,或者右边数据的值在左边数据类型的取值范围内,否则,将会导致运算精度降低(如:(int)2.8=2,也可用于小数的取整),甚至从高类型到低类型的转换会发生存储字节空间的减少,从而可能会引发因数据截断造成的数据损害。

    另外涉及到数据截断的问题,可以知道在我们使用格式转换符的时候,一定要与打印值的数据类型对应起来,否则会造成数据丢失。如:printf("%f\n",3);
    这里3是int型,而%f是转换打印输出浮点型数据,因此会造成最终结果丢失。


九、其他剩余部分

  1. 双引号直接扩起来的被c语言系统认定为字符串常量,被存放在代码区不可修改,如char *str=“字符串”;意义是把后面双引号的常量字符串的内存地址给常量指针(const char )初始化;而char a[]=“字符串”;是编译器自己根据双引号内的字符字节大小分配一块连续的内存空间,其中数组名是char const类型,是只读的。因此对一个常量进行修改可能殃及及程序中其他字符串常量。因此,许多ANSI编译器不允许对字符串常量修改,如果想随意修改你输入的字符串,请用数组去存储。

  2. 字符串通常存储在数组中,因此c语言才没有额外存在一种字符串类型的数据结构。而选择NUL字符(空字符)作为字符串的终止符,是因为它不是一个可打印的字符。

  3. 位于一对花括号之间的所有语句称为一个代码块。函数主体由一对花括号括起来,括起来的函数主体也就是个大的代码块,该大的代码块中又有如条件语句,循环语句等等括起来的小代码块。另外,语句的结束标志为分号;代码块的结束和函数的结束为反花括号;字符串结束为空字符。

  4. c语言并不具备任何输入/输出语句(注意:是说的输入输出语句!并没有这种标识输入输出语句的关键字):I/O是通过调用函数库的输入输出函数实现的。另外,c语言也不具备任何异常处理语句,它们也是通过调用库函数来完成的。

  5. if,while等语句后面的括号也是该语句完整的一部分,因此if语句实际上完整来说是if()语句。

  6. 如果在switch-case语句中,想要同一个语句的范围扩大,则可以将其前面需要扩大的范围内的case(i)语句都设为空语句,如:case 1: case 2: case 3: statement- list;break;case···default 如果扩的范围太大,可能换成一系列的if语句会更好,其中default为默认值的意思,如果case中的预设情况没有一种能与现实情况对应,则会自动进入默认情况default语句。

  7. 制表符\t是以绝对位置(8个空格)8 col(列)为单位。如printf(“字符串\t”);的作用效果是先打印字符串,再将光标移到该行的后八位空格的位置。因此如果反过来printf(“\t字符串”);则是从空了8列的位置开始打印字符串。

  8. C语言最简单的语句就是空语句,它本身只包含一个分号。


知新


一、基本概念——环境

环境(Environment)

在ANSI C(ANSI即American National Standards Institute,美国国家标准学会。ANSI C是 美国国家标准协会(ANSI)对 C语言发布的标准。)的任何一种实现中,存在两种不同的环境。第一种是翻译环境(translation environment),在这个环境里,源代码被转换为可执行的机器指令。第二种是执行环境(execution environment),它用于实际执行代码。标准明确说明,这两种环境不必位于同一台机器上。例如,交叉编译器(cross compiler)就是在一台集器上运行,但它所产生的可执行代码运行于不同类型的机器上。操作系统也是如此。标准同时讨论了独立环境(freestanding environment),就是不存在操作系统的环境。你可能在嵌入式系统种(如微波炉控制器)遇到这种类型的环境。

翻译环境
翻译(编译+链接)
  • 翻译阶段由两个步骤组成,组成一个程序的每个(有可能有多个)源文件(.cpp)通过编译过程分别转换成为目标代码(object
    code)。然后,各个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
编译(Compile)
  • C语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于CPU,C语言代码就是天书,根本不认识,CPU只认识几百个二进制形式的指令。这就需要一个工具,将C语言代码转换成CPU能够识别的二进制指令,也就是将代码加工成
    .exe 程序的格式;这个工具是一个特殊的软件,叫做编译器(Compiler)。

    编译器能够识别代码中的词汇(关键字,标识符)、句子(条件结构,循环结构)以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。
    (这里所说的词汇也就是上述文章里所谈到的C语言字符集,以及系统内部定义的关键字所表示语句等等)

  • 编译分为两个阶段组成:

  • 首先是预处理(preprocessor)处理。在这个阶段,预处理器在源代码上执行一些文本操作和带#的操作指令。例如,用目标文本代替由#define指令定义的符号以及读入由#include指令包含的文件的内容,复制到该源文件内。

  • 其次是解析(parse)处理。在这个阶段,判断它的语句的意思,这个阶段是产生绝大多数错误和警告信息的地方。
    随后便产生了目标代码(二进制代码,储存到了代码区)

链接(Link)
  • C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual
    C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o。

    链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。

    链接器同时会引入标准C函数库种任何被该程序所用到的函数,而且它也可以搜索程序员个人的程序库,将其中需要使用的函数链接到程序中。

执行环境
执行(Execute)

程序的执行过程也需要4个步骤

  1. 首先,程序必须载入到内存中。 在宿主环境中(也就是具有操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值,如静态分配到静态存储区的变量(extern,static),它们被储存到普通内存中。在独立环境中,程序的载入必须由手工安排,也可能是通过把可执行代码置入只读内存(ROM)来完成。

  2. 然后,程序的执行便开始。在宿主环境中,==通常一个小型的启动程序与程序链接在一起。它负责处理一系列日常事务,如收集命令行参数以便程序能够访问它们。==接着,便开始调用main函数。

  3. 接着,开始执行程序代码。**在绝大多数机器里,程序将使用一个运行时堆栈(heap/stack),它用于存储函数的局部变量和返回地址。

    这样的话,程序很好地将自己所需要长期使用的变量和临时调用的变量进行区分,它们的生命期不同,不浪费额外内存空间

  4. 最后,就是程序执行的终止。它可以由多种不同的原因引起。正常的终止就是main函数返回。除此之外,程序也可能是因为用户按下break键或者电话连接的挂起而终止,另外也可能是在执行过程中出现错误而自行中断。


二、数据结构和数据抽象

数据结构是在整个计算机科学与技术领域中被广泛使用的术语,它用来反映一个数据的内部构成。 数据抽象是**把系统中需要处理的数据和这些数据上的操作结合在一起,根据其功能、性质、作用等因素抽象成不同的抽象数据类型(abstract data type,ADT)。**抽象数据类型是用户进行软件设计时从问题的数学模型中抽象出来的逻辑数据结构和逻辑上的一组操作,而不考虑计算机的具体存储结构和操作的具体实现。抽象数据类型的表示和实现都可以封装起来,便于移植和重用

数据结构和数据类型

  • 数据结构反映数据的内部构成,即数据由哪些成分构成,以什么方式构成,以及数据元素之间呈现什么结构。数据结构就是数据存在的形式。 具有相同数据结构的数据归为一类,可以用数据类型来定义。

    另外,数据结构有逻辑上的数据结构和物理上的数据结构之分。逻辑上的数据结构反映各数据之间的逻辑关系;物理上的数据结构反映各数据在计算机内的存储安排。

  • 数据类型是一个值的集合和定义在此集合上的一组操作的总称。数据类型一般指数据元。数据元( Data Element),也称为数据元素,是用一组属性描述其定义、标识、表示和允许值的数据单元,在一定语境下,通常用于构建一个语义正确、独立且无歧义的特定概念语义的信息单元。 可以认为,数据类型是在程序设计语言中已经实现了的数据结构。

  • 数据类型可分为简单类型和构造类型。简单类型中的每个数据是无法再分割的整体,如一个整数,浮点数,字符等都是无法再分割的整体,所以它们所属的类型均为简单类型。在构造类型中,允许各数据本身具有复杂的数据结构,允许复合嵌套,如由若干个整数构成的整数数组。

  • 对于构造类型,其数据结构就是相应元素的集合和元素之间所含关系的集合。简单来说就是数据结构=数据类型(一种或多种)+成员之间的关联方式。

抽象数据类型
  • 抽象数据类型是用户进行软件设计时,从问题的数学模型中抽象出来的逻辑数据结构和逻辑数据结构上的一组操作{重点在于是仅用逻辑模拟出来的一种结构形式(例如结构体是一种数据结构,而在逻辑上不同的结构体串联起来就形成一个链表,链表就是一种抽象数据类型)},而不考虑计算机的具体存储结构和操作的具体实现算法。 使用抽象数据类型可以更加方便地描述现实世界,如用线性表描述学生成绩表,用树或者图来描述遗传关系。

三、三字母词

标准还定义了几个三字母词(trigrph),三字母词就是几个字符的序列,合起来表示另一个字符。三字母词使C环境可以在某些缺少一些必需字符的字符集上实现。 这里列出了一些三字母词以及它们所代表的字符。

  • ??( --> [ ??< --> { ??= --> #
  • ??) --> ] ??> --> } ??/ --> \
  • ??! --> | ??" --> ^ ??- --> ~

转义字符’反斜杠+?'在书写连续多个问号时使用,防止连续的问号被解释为三字母词。


四、数据(变量的三种属性)

程序对数据进行操作。变量数据具有3个属性——作用域,链接属性以及存储类型。这3个属性决定了一个变量的“可视性”(也就是它可以在什么地方使用)和“生命期”(它的值将保持多久)。

作用域

当变量在程序的某个部分被声明时,它只有在程序的一定区域内才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。

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

(1). 代码块作用域
  • 位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope),表示它们可以被这个代码块中所有语句访问。当标识符不在开始位置声明时,作用域为从声明的位置开始,一直到代码块结束。当代码块处于嵌套状态时,声明于内层代码的标识符的作用域达到该代码块的尾部时便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将屏蔽外层的标识符——外层的那个同名标识符无法在内层代码块中通过名字访问。(如在主函数中有一段代码块内调用了子函数,而子函数中也存在一个与主函数代码块中同名的标识符,则在调用期间系统访问这个名字的标识符是子函数中的那个,而不是主函数中的,这就好比在我们调用函数通过值传递的时候,通常定义的形参列表是与实参同名的变量,它们不会产生纠缠,只会在各自的作用域内作用。)

    另外:函数形参的作用域开始于形参的声明处,结束于该函数体原型的结尾。 如果在函数体内部声明了名字与形参相同的局部变量,则意味着该局部变量与形参作用域相同,因此形参的作用域设定是ANSI C很好地规避了形参与内部局部变量重叠的这种错误可能性,它把形参的作用域设定为整个子函数代码块,这样声明于函数内部的局部变量就无法与形参同名,因为它们的作用域相同,会出现报错。

    如:

    #include<stdio.h>
    
    int f (int x) {
          int x;
          return x+1; }
    
    void main(void) {
          int x=2;
          x=f(x);
          printf("x=%d\n",x); } //该程序会出现报错,因为在f函数内部定义了一个与形参相同名字的局部变量:error C2082: redefinition of
    formal parameter 'x' ```
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
(2). 文件作用域

任何在所有代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们声明之处开始一直作用到它所在的源文件结尾,在本源文件区域内访问。 在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。应该指出的是,在头文件中编写并通过#include指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。

(3). 原型作用域

原型作用域(prototype scope)只适用于在函数原型中声明的参数名,也就是形参。其作用范围开始于形参的声明处,结束于该函数体原型的结尾。 在形参被实参赋予了相应的值后,形参可以被理解为成为了该函数的一个代码块作用域的同名同类型的局部变量。

(4). 函数作用域

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

另外:goto语句的使用方法

  • 形式:goto 语句标签;
    用法:要使用goto语句则必须在希望跳转的语句的方法前面加上标签,语句标签就是标识符后面加个冒号。只有一种情况下值得用goto,就是立即从深层循环中跳出。由于break只能跳出内层的当前循环层,因此在深循环中goto可以很适合。

链接属性

当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数将链接在一起,形成可执行程序。然而,如果相同的标识符出现在几个不同的源文件中时,它们是像Pascal那样表示同一个实体,还是表示不同的实体?标识符的 链接属性(linkage) 决定如何处理在不同文件中出现的标识符。链接属性一共有3种——external(外部)、internal(内部)以及none(无链接属性)。

external
  • 属于external链接属性的标识符不论在不同的文件中声明多少次,位于多少个不同的源文件,它们都是表示同一个实体。

    另外:在这里补充一下对extern类型变量的作用域和external链接属性的变量的区别与联系

    • 在文件作用域处定义的函数或者变量,都具有全局(外部)属性,先来讨论一下他们的作用域:若这两者都是隐式外部变量(省去了extern关键字)则它们作用域为从定义处到源文件结束,若想要变量作用域提升到整个源文件,则需要在后置位外部变量的前面加上被省略的extern关键字;同理,如果函数想要作用域为整个源文件,则也需要加上extern关键字,但还有一种办法,就是在文件开头先提前声明所有函数,也是一样的效果。
    • 我们再来讨论一下链接属性:只要是在文件作用域定义的函数或者变量都具有external的链接属性,都可以在该程序的所有文件中共享,但共享有个前提条件,就是其他要引用该external变量或者函数的话,需要在它们前面加上extern告诉编译器它们是外部链接属性的,向整个程序提供它们的函数原型,只有这样才能算是同一个变量或者函数在一个文件中定义,而整个程序都得以共享使用。(如
      file.1中 int a;为全局变量,若在file.2里想要访问该变量a,则需要声明句:extern int
      a;则此时两个文件里的a为同一个)

      因此总结一下:具有external链接属性的标识符一定是存在于文件作用域的,而作用域体现的是一个变量在本文件中的访问,链接属性体现的是一个变量在整个程序(所有文件)中的访问。
internal
  • 属于internal链接属性的标识符在同一个源文件内的所有声明(即冠有static且在文件作用域定义的变量)都指同一个实体(因为static变量属于静态分配并存储于静态区,其生命期为整个程序,因此在源文件编译的时候一个标识符代表的静态变量只能存在一个实体,否则编译器无法识别分辨),而位于不同源文件的多个声明则分属于不同的实体。(因为这些同名的internal链接属性的标识符,都在各自文件中编译,虽然它们名字一样,但在代码区却被分配了不同的物理地址,再由链接器链接起来,然后由执行步骤中的第一步,将对应着不同的物理地址的这些static变量赋予初值载入内存中,不会造成程序识别错乱。)
none
  • none是指在代码块内定义的局部变量,由于其生命周期只在自己的代码块内,因此都是单独的个体,也就是说同一个标识符的多个声明都是不同个体。
链接属性的修改

关键字extern和static用于在声明中修改标识符的链接属性。

  1. 如果某个声明(外部变量/全局变量 省略了关键字extern)在正常情况下具有external链接属性,在它前面加上static关键字可以使它的链接属性变为internal。 (static只对缺省链接属性为external的声明才有改变链接属性的效果)
  2. extern关键字它为一个标识符指定external链接属性,这样就可以访问在其他任何位置定义的这个实体。如局部变量若被extern修饰,则会升级为全局变量,具有external链接属性。
  3. 当extern关键字用于源文件中一个标识符的第一次声明时,它指定的该标识符才具有external链接属性。但是,如果它用于该标识符的第二次或以后的声明时,它并不会更改由第一次显性声明所指定的链接属性。
    例如:
static int i;
int func()
{
      //statements;
      extern int i;    //这里的声明并不会修改首行对i变量的声明的原始的链接属性,i变量依然是internal链接属性
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

存储类型

变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定了变量何时创建,何时销毁以及它的值将保持多久。 有3个地方可以用于储存变量(注意,这里所说的是变量的储存区,像函数,标识符等代码都是储存于程序代码区的(存放程序的代码,即CPU执行的机器指令),以及数组名,常量或字符串常量等储存在文字常量区):普通内存(静态区),运行时堆栈,硬件寄存器

1°. 静态内存分配
  • 静态分配内存:是在程序编译和链接时就确定好的内存。
普通内存(静态/全局区 Static)
  • 变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的缺省储存类型变量总是存储于静态内存中,这类变量称为静态内存分配变量。静态内存分配变量包括:全局变量(extern修饰)和静态变量(static修饰)。 对于这类变量,无法为它们指定其他存储类型。(如局部变量除了本身可以被自动存储于栈区以外,还可以手动为其分配堆区内存,即malloc函数的引用)静态内存分配变量在程序运行之前创建(在执行步骤中的第一步,见上文),在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋予一个不同的值或者程序结束由操作系统回收释放内存空间。

    另外,静态内存分配的变量若未初始化赋值,则系统会自动为其赋值。若是数值型变量则赋值0,若是字符型则赋值’\0’。数组亦是一样,static
    int a[5];则系统在其定义声明时自动赋予五个0,static char str[5];则系统在定义声明时自动赋五个’\0’。

2°. 动态内存分配
  • 动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。
    在代码块内部声明的变量的缺省存储类型都默认为自动型(auto),也就是说它存储于堆栈中,称为动态内存分配变量,即自动变量。
栈区(Stack)
  • 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。栈区之中的数据的作用范围过了之后,系统就会回收自动管理栈区的内存(分配内存 ,回收内存),不需要开发人员来手动管理。栈区就像是一家客栈,里面有很多房间,客人来了之后自动分配房间,房间里的客人可以变动,是一种动态的数据变动。
堆区(Heap)
  • 程序运行时,由程序员调用malloc()函数来主动申请的,需使用free()函数来释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。

  • 在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量(栈区)便自动销毁。

  • 另外,函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。并且动态内存分配的变量若未初始化赋值,则系统会为其赋予随机值。若数值型变量就赋值随机数值,字符型变量就赋予随机字符,数组也同理。

3°. 寄存器存储
  • 最后,关键字register用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常,寄存器变量比存储于内存的变量访问起来效率更高。但是编译器并不一定要理睬register关键字,如果有太多的变量被声明为register,它只会选取前几个实际存储与寄存器中,其余的就按照普通自动变量处理。如果一个编译器字节具有一讨寄存器优化方案,它也可能忽略register关键字,其依据是由编译器来决定哪些变量存储于寄存器中要比人脑的决定更为合理一些。在经典情况下,我们希望把使用频率最高的那些变量声明为寄存器变量。

  • 寄存器变量的创建,销毁时间和自动变量相同,但它需要一些额外的工作。当函数开始执行时,它把需要使用的所有寄存器的内容都保存到堆栈中,当函数返回时,这些值再复制回寄存器中。

对于变量三种属性的总结与体会

  • static关键字
  • 当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性(从external修改为internal),但标识符的存储类型和作用域不受影响。用这种方式声明的函数或者变量只能在声明它们的源文件中访问。
  • 当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性 (注意:链接属性依然是none,在不同代码块内的同名变量依然是不同的实体。) 和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。(至于原因,可能是因为链接属性的优先级高于存储类型吧。。这里优先判断了该变量属于代码块内部拥有了none链接属性,再判断了该变量的存储类型为static,于是在程序执行之前进行静态内存分配,因此实质上告诉编译器同名标识符是否是同一个实体的是它们的链接属性,是none则会识别为两个不同的实体,分配到两个存储单元)
#include<stdio.h>

void fun()
{
	static int b;
	printf("&b=%d,b=%d\n",&b,b); // 打印结果:&b=4357684,b=0
}

void main()
{
	static int b;
	 b=1;
	 fun();
	 printf("&b=%d,b=%d\n",&b,b); // 打印结果:&b=4357688,b=1
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • (由此可见,链接属性描述的是一个变量与其他变量的关联性,和不同文件之间的访问情况。而作用域描述的是变量只在该源文件内自身的作用范围)
  • 具有external链接属性的实体(在文件作用域的缺省存储类型的变量或者在文件作用域和代码块中第一次冠有extern修饰的变量)总是具有静态存储类型属性,而静态存储类型的实体其链接属性一定不是external,如果在代码块区定义的static变量则是none链接属性,在文件作用域内定义的static变量则是internal链接属性
  • 变量的生命期由存储方式决定,也就是由四个存储方式的关键字决定;变量的作用域由其定义的位置决定,会被存储类型关键字影响,如extern修饰了局部变量则升级成为一个全局变量,作用域是本源文件;变量的链接属性也由其定义的位置决定,代码块内部为none,代码块外部为external,也会被存储类型关键字影响,如static修饰了省略存储类型的外部变量,则降级成了internal的全局变量。
  • 局部变量由函数内部使用,不能被其他函数通过名字引用。它再缺省情况下的存储类型为自动,这是基于两个原因:其一,当这些变量需要时才为他们分配存储,这样可以减少内存的总需求量;其二,在堆栈上为它们分配存储可以有效地实现递归。

总结

  • 本周学习了关于C语言的相关基础概念,从一个程序(代码标记规则)由什么组成,源代码被创建以后要经过哪些翻译步骤才能被计算机读懂,成为目标代码(也就是可执行代码)(编译(预编译–>解析)–>链接)从而能被计算机执行(载入内存–>读入命令行参数–>调用main函数–>逐步执行程序细节–>程序执行结束(main函数返回))。而这些代码都是对C语言的数据进行处理的,接着学习了我们所能填入的数据的九种类型,以及数据分为常量和变量两种模式,最后讨论了关于变量存在与作用的三种属性(作用域,链接属性,存储类型)。数据得以了解后,我们需要了解的是便是什么样子的代码对这些数据进行了我们想要进行的操作与处理,最后解决了我们的实际问题。这些语句就是空语句,表达式语句,条件语句,循环语句等,通过语句之间的不同结合,嵌套,这些框架使数据得以一种模式化运算。而这些运算的最基本的细节,如加减乘除等等就涉及到了操作符与表达式,接下来将要继续去探索。

----------------------------------------------------------------------------------------------------------------------------- Stay hungry,Stay foolish .

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/154257
推荐阅读
相关标签
  

闽ICP备14008679号