C语言基础

低级语言→高级语言:

  • 低级语言:如汇编语言:有助记符,应用于底层或嵌入式

  • 高级语言:C/C++/Java/python/go等

c++不是c的替代,而是针对不同的需求,c底层,c++中层

IDE:包括编辑、编译、调试、图形等的开发软件

编译器:把机器看不懂的语言(如高级语言)翻译成能看懂的机器语言

一、初识C语言

  • C是编译型语言

  • 优点:设计特性、高效性、可移植性(跨系统)、强大而灵活、面向程序员、可以很好的控制硬件

  • 嵌入式系统编程流行语言,微处理器都用C

1.语言标准

  • C很依赖库,UNIX实现提供的库称为现在的标准库

    ISO C和ANSI C是完全相同的标准,ANSI/ISO标准的最终版通常叫做C89/C90

    ANSI C是最常用的C标准,后续还有C99、C11等标准

2. 编程机制(尚未修改)

  • 编译时会生成可执行文件,其中是机器能直接运行的二进制代码,如java文件编译为class文件

编程机制:

  1. 编写的源代码存储在文本文件xxx.c中

  2. 编译器把源代码文件转化为目标文件(二进制代码,这个程序并不完整,只是源代码的编译,还缺少库和启动代码)

  3. 连接器把目标代码、系统标准启动代码(程序和操作系统的接口)、库代码(本程序用到的)合成可执行文件(二进制代码)

VS9(IDE))不在把C作为项目类型的选项,由于几乎所有的C都能与C++兼容,C大多数程序仍可用VS编译,只需通过项目类型(扩展名)来区分C和C++。新建项目选择C++,选择空项目,将默认源文件扩展名.cpp改为.c,编译器讳改为使用C的规则

windows和Linux的程序可以再对方上运行。但win系统不能访问L文件,反之可以

*注意:本书采用斜体表示占位符,具体项可以替换这些占位符

二、C语言概述

1.简单示例

#include<studio.h> 
int main(void) //
{
	int num;//声明,编译器会为变量num在内存中分配空间
	num = 1;
	//"My name is"是主函数传递给函数printf的信息,是参数,是实参
	printf("My name is");
	printf("Sam.\n");
	printf("My favorite number is %d",num)//转换说明%d是一个占位符
}
  1. 头文件header:C程序顶部的信息集合。如#include<>,#define,函数声明等

    1. C编译前要预处理,#表明C预处理器在编译器接手之前处理这条指令,如#include和#define指令。#include<stdio.h>叫指令也叫代码

    2. include提供了一种方便的途径共享许多程序共有的信息。include<studio.h>把stdio.h的内容全部拿来。

    3. stdio.h:标准输入输出文件

  2. 注释:

  3. 函数头:int main(void)

    1. 圆括号表明main()是一个函数

    2. 规定第一个被运行的函数名必须是main。其他函数的函数名可以自定名

  4. 声明:int num1,num2

    1. 声明把特定标识符与内存某个位置联系起来,同时也确定了存储在某位置的信息类型。编译器会以此为变量num在内存中分配空间

    2. num就是标识符(变量名)

    3. C99标准中,允许需要时才声明变量。

    4. 没有 int n1,int n2,int n3 这样的写法

  5. 赋值:声明时编译器预留内存空间,赋值时把值存入相应空间

  6. 转义序列:用于代表难以或无法表示的字符(\是反斜杠)

  7. %d是一个占位符,是转换说明。表明把数据作为十进制整数打印。8位用%o、16位用%x。注意用%d先生float类型的值,其值不会转换成Int类型

  8. return:

    1. C中一种跳转语句

    2. 无返回值可省,程序运行至最后的'}'时会自动返回0;有返回值的函数不可省略。建议函数中要写

2.多个函数

函数原型:

  • 一种声明形式,告诉编译器正在使用某函数,所以也叫函数声明。告诉编译器如何使用该函数

  • 指明了函数属性

C标准建议,为所有用的的函数提供函数原型。如stdio.h包含了printf()的原型、include文件(包含文件)为标准库函数提供库函数原型。

3.调试程序

  • 优先修正第一条/前几条错误

4.关键字和保留字符

1.标识符:变量、函数或其他实体的名称

关键字:

  • C语言的词汇

  • 定义:控制语句的开始或结束以及执行特定操作时具有关键作用、特定作用的符号。

  • 不能作为标识符

保留标识符:

  • 包括一下划线开头的标识符和标准库函数名如printf( )

  • 定义:被保留的、将来可能被用作关键字。C已经指定用途或保留其使用权,尽管有效名称但也不能用来表示其他意思所以不建议将之作为标识符

三、数据和C

1.示例

交互性scanf-printf:输入Enter是告知计算机已完成输入数据

#include<stdio.h>
int main(void)
{
	float weight;
	float value;

	printf("Are u worth ur weight in platinum?\n");
	printf("Let's check it out.\n");
	printf("Pls enter ur weight in pounds:");

	/*% f处理浮点值 .2用于精确控制输出,指定输出的浮点数只显示小数点后两位*/
    
	scanf("%f", &weight);//&获取变量地址

	value = 1700.0 * weight * 14.5833;//14.5833也是常量
	printf("Ur weight in platinum is worth $%.2f.\n",value);
	printf("U are easily worth that! If platinum prices drop,\n");
	printf("eat more to maintain ur value.\n");

	return 0;
}

2.变量与常量数据

数据:承载信息的数字和字符,分为常量变量

  • 常量:上述程序中的14.5833是,int num = 2 的2也是

    • 程序使用前已设定好,程序运行期间没有变化

  • 变量:int num的num就是变量

    • 程序运行期间可能改变或被复制

3.数据:数据类型关键字

变量和常量不同,不同数据类型之间也有差异

  • 常量:通过用户书写的形式来识别类型(42是整数,42.10是浮点数)

  • 变量:声明时指定类型(int num;)

数据类型关键字:int代表基本整数类型

  • long、short、unsigned、signed用于提供基本整数类型int的变式:如unsigned short int

  • char表示字母和一些字符(如#、¥、%、*),也可以表示较小的整数

!所以按照关键字类型:基本数据类型分为整数类型浮点类型数两大类!

b、byte、word:

  • b:计算机中最小的数据单位,每一位只能是0/1

  • byte:是cpu访问内存的基本单位,可以存储一个字母或半个汉字

    • 存储单元:即1byte(大多8b,极少16b),CPU访问存储器的基本单位

    • b(1b)-byte(8b)-word(32b\64b)

    • C标准中并没有具体给出规定那个基本类型应该是多少字节数,而且这个也与机器、OS、编译器有关。但有几条铁定的原则(ANSI/ISO制订):

    sizeof(short int)<=sizeof(int)<=sizeof(long int)
    short int//至少应为16位(2字节)
    long int//至少应为32位

3.1 整数和浮点数

  • 对人:书写方式区分二者

  • 对计算机,存储方式区分二者(不同数据存储方式区别很大)

3.1.2 整数

7换成二进制为00000111,1前面都是0

3.1.3 浮点数

存储方式:略....

3.1.4 整数和浮点数区别

  • 浮点数表示范围更大

  • 浮点数易损失精度(范围有限,数越大损失精度越大)

  • 浮点数通常是实际值的近似值。如7.0可能被存储为浮点值6.99999

  • 浮点数运算慢。但现在的cpu有浮点处理器,速度慢的很少

  • 二者本质不同:存储和运算区别很大

4.C语言基本数据类型

4.1int

int是有符号整数,大多整数用int表示就够。非常大的除外(long常量和long long等)

1.1 声明int变量

声明会为变量创建和标记存储空间,并为其指定初始值

//横竖两种都可以,
int n1;
int n2,n3,n4;

1.2 初始化变量

给变量赋值有三种方式:

  1. 赋值

    n1 = 1;
  2. 函数获取

    scanf("%f", &weight);
  3. 初始化变量! (初始化直接在声名中完成)

    int n1 = 1;//声明为变量创建和标记存储空间,并为其指定初始值
    int n2 = 2,n3 = 3
    int n2,n3 = 12 //不建议,这里只初始化了n2

1.3 int类型常量

int n1 = 2,n2 = 3; 
value = -1700 * weight * 14;

上述2、3、-1700、14都是整型常量整型字面量

1.4 打印int值

可用printf

printf("%d",num);
    
//粗心遗漏了两个参数,打印出的是内存中的任意值
printf("%d minus %d is %d\n",ten);

1.5 八进制和十六进制(略)

C假定整型常量都是十进制

1.6 显示八进制和十六进制(略)

1.7 其他整数类型(略)

4.2 *其他整数类型(未整理)

有符号:

  • short int(简写short):占用存储空间比int(固定4字节)少,用于较小数值

  • Long int(int):占用比int多,用于大数值

  • long long int(long long):占用比long多,用于更大数值

    *在任何有符号类型数字前添加关键字signed,可显示表面是有符号类型,如signed short

无符号:表征负号的位用于表征另一个二进制位,故可以表示更大的数

  • unsigned int(unsigned):用于非负场合

  • 在有符号数前面加上unsigned可以变为无符号数

注:详细参考书上第三章Int节

4.3 char

1.char字符类型:

定义:

  • 作用:存储字符,包括字母、标点符号、符号数字

  • 大小:C规定占用1byte

  • 本质:本质是整数类型。机器用特定整数表示特定字符,字符实际上作为(1byte)整数存储,如65是A(都是整数,但char和整数类型存储空间大小不同)

  • char是最小的整数类型

编码:

  • ascii:美国通用ascii码。范围是0~127,只用7b表示

  • Unicode:商用统一码,能表示世界内多种字符集

  • C会保证char类型足够大

char itable,latan;
//和第一句一个功能,但前提是使用ASCII码(A=65).并不是好习惯,
char grade = 65;

2.声明:

与其他类型变量(整型变量)声明方式相同

3.初始化与赋值、字符常量

  • 与其他类型变量声明方式相同

  • 字符常量:单引号括起来的字符

    //A是字符常量
    char grade = 'A';
    
    //4是字符4,而非数值4
    char grade = '4';
    
    //A是字符串
    char grade = "A";
    • 两种赋值方式等价

      char grade = 'A';
      char grade = 65//前提是使用ascii编码,不是好的编程风格

4.非打印字符

单引号只适合字符、数字和标点,有些ASCII字符(退格、换行、终端响铃、蜂鸣等)打不出来,有两种方法

  1. 使用ascii码,如蜂鸣字符ascii值为7

    char beep = 7
  2. c使用转义序列来表示(如用\n表示换行)

    char nerf = '\n';

5.打印字符

printf()用%c指明打印的字符

char和int可相互打印:

  • 对于char:%c打印出字符,用%d则会打印char类型变量对应的整数值 对于int:%c打印出对应的字符,%d打印出值

char ch;
int num = 65;

//回车标志输入结束!!!
scanf("%c", &ch);
printf("The code for %c is %d.\n", ch, ch);//输入ab,输出a 97
printf("%c", num);//输出A

4.4 _Bool

  • 无符号

  • C用1表示true,0表示false。所以_Bool也是整数类型:但原则上仅占用1为存储空间

4.5 可移植类型:stdint.h和inttypes.h(略)

C有很多整数类型,但是谬写类型名在不同系统中功能不一样,C99新增两个头文件stdint.h和inttyoes.h来保证C的类型在各个系统中的功能相同

4.6 浮点数变量

浮点数变量:float、double和long double

浮点数能表示更大范围的数

float < double < long double

1.声明

声明和初始化与整形相同

2.浮点型常量

默认情况下,编译器假定浮点型常量是double类型的精度

3.打印浮点值

  • printf用%f转换说明打印十进制float和double类型

  • 给未在函数原型中显示说明参数类型的函数(printf)传参时,C编译器会把float类型的值自动转化为double类型!所以没有float类型的转换说明

4.浮点值的上溢和下溢(略)

4.7 复数和虚数类型

4.8 其他类型

C的一些从基本类型衍生的其他类型:数组、指针、结构、联合

4.9 类型大小

通过sizeof获取本电脑各类型大小

//sizeof的转换说明为%zd
printf("%zd", sizeof(int));//4
printf("%zd", sizeof(float));//4
printf("%zd", sizeof(char));//1,这是系统规定的

5.使用数据类型

C对类型匹配不严格,甚至允许二次初始化,激活高级警告时编译器可能会给出警告

类型不匹配后果示例

//int←float:会直接丢弃小数,成为截断
int cost= 12.99; //值为12
//float←double:损失一些精度
float pi = 3.1415926536;//值为3.1415929(保留6位)

6.参数和陷阱

参数:

  • 传递给函数的信息

  • C用逗号分割函数中的参数

    //1个参数
    printf("Hello world");
    
    //2个参数
    scanf("%d",weight);

    C通过函数原型检查函数调用时参数的个数和类型是否正确,但是对printf和scanf没用,因为二者参数个数可变

7.转义字符示例(略)

7.1 刷新输出

printf把输出发送至屏幕:

  • 最初,printf把输出发送到缓冲区

  • 然后,缓冲区内容不断发送到屏幕

缓冲区内容何时发送到屏幕(文件):这个过程称为刷新缓冲区

  • 缓冲区满:又叫完全缓,缓冲区被填满才刷新缓冲区(内容被发送至目的地),常出现在文件输入

  • 遇到换行符:又叫行缓冲

  • 需要输入:

四、字符串和格式化输入输出

1.字符串

1.1字符串

  • 本质:以空字符'\0'结尾的char类型数组。表面上:双引号不是字符串,双引括起来的才字符串。单引号也一样

  • 类型:C没有专门存储字符串的变量类型,字符串都存储与char类型数组中

  • 对于空字符,C会自己加上,不必操心

    • scanf读取输入时,已经完成将'\0'放入字符串末尾

    • 同样也不用再字符串常量PRAISE末尾添加空字符

    #define PRAISE "Hello world"//同样也不用再字符串常量PRAISE末尾添加空字符
    
    char name[40];
    scanf("%s",name);//scanf读取输入时,已经完成将'\0'放入字符串末尾
  • 字符串和字符:'x'≠"x"

    • 前者是基本类型,后者是派生类型

    • 'x':x , "x":x \10

1.2 strlen(对比sizeof)

定义:返回字符串长度(不包括最后的空字符)

使用:strlen、sizeof的转换说明:%zd

对比:

  • int num = 1;
    char name[40]="Hellow world";//数组没存满,剩下的空间是垃圾数据H e l l o w  w o r l d \0 .........
    
    printf("%zd",strlen(name));
    
    printf("%sd",sizeof(name));//括号可有可无
    printf("%sd",sizeof(int));//括号要有
    
    scanf("%d",num);
    scanf("%s",&name);

C把函数库中相关的函数归为一类,并为每类函数提供一个头文件。

  • printf、scanf属于标准输入输出函数,使用<stdio.h>

  • strlen()和一些与字符串相关的函数,使用<string.h>

#include<stdio.h>
#include<string.h> //提供strlen()函数原型
#define PRAISE "U are an extrordinary being."
int main(void)
{
	char name[40];

	printf("What's ur name?\n");
	scanf("%s", name);
	printf("Hello,%s.%s\n", name, PRAISE);
    //两种处理长printf语句
    //1.分行,但不要从双引号中间断开
	printf("Ur name of %zd letters occupies %zd memroy cells.\n",
		strlen(name), sizeof name);
    //两个Printf打印
	printf("The phrase of praise has %zd letters",
		strlen(PRAISE));
	printf("and occupies %zd memory cells.\n", sizeof PRAISE);
	
	return 0;
}	
/*
sizeof():占用内存的大小,以byte为单位,char num[40]为40,int num[40]为160,因为一个int类型元素占用4字节(char占用1字节),40×4。 此处为40
stringlen():给出字符串中的字符长度。此处为11
*/

2.常量和C预处理器

2.1 使用define定义明示常量(针对常量)

  1. 符号常量:建议使用符号常量:提高可读性和可维护性

    //pi就是符号常量
    float pi = 3.141592;
  2. 符号常量创建:

  • 方法一://先声明一个变量,在将变量设置为所需常量

float taxrate;
taxrate = 0.015;
  • 方法二:C预处理器 预处理器指令:#后面的指令

//末尾没有';',常量约定大写
#define TAXRATE 0.015

编译时替换:编译程序时,程序内所有TAXRATE都会被替换为0.015,这样的常量也成为明示常量

#include<stdio.h>
#define PI 3.14159
int main(void)
{
	float area, circum, radius;

	printf("What is the radius of ur pizza?\n");
	scanf("%f", &radius);
	area = PI * radius * radius;
	circum = 2.0 * PI * radius;
	printf("Ur basic pizza parameters are as follows:\n");
	//%1.2f:结果四舍五入为两位小数输出
	printf("circumference = %1.2f,area = %1.2f\n", circum, area);

	return 0;
}

#define还可以定义字符和字符串常量

#define BEEP '\a'
#define OOPS "Now u have done it!"

2.2 const限定符(针对变量)

使用const声明数组:把变量设置为只读 const和define的区别:

  • define定义的是不带类型的常数。只是简单的字符替换。预编译阶段

  • const的定义的是变量。只是限定变量为只读。编译运行阶段(比define灵活,但不是用来替代define)

//在前面加个const即可
const int days[MONTHS];

3.输入输出函数scanf、printf

过程:

  • scanf会把输入的文本转化为对应的类别(整数、浮点数、字符、字符串)

  • printf会把整数、浮点数、字符、字符串转化为屏幕上的文本

转换说明:即翻译说明(这里要好好理解翻译的意思,暂时不做深究)。字面上:

  • pritnf:把值按指定类型输出为文本。所以需要值类型和指定类型一致,当然整数和字符本质上一样,可以替换

  • scanf:把键入文本当作对应类型尝试读取

  • 示例:键盘输入类型:输入36L

    • 用scanf%d,C认为输入是整数,当作整数36读取(读到6发现L不是整数就截至)。

    • 用scanf%c,C认为输入的是字符,读取'3'

    • *书上说输入都是文本,甚至是字符串,这里似乎不对或者有待我深入了解,暂不予理会

缓冲区:

  • printf:printf执行时?会把输出发送到缓冲区,缓冲区中的内容在不断被发送到屏幕

  • scanf:scanf从缓冲区中读取内容,如果缓冲区没有内容,就等着键盘输入(到缓冲区)

相似:

  • 都是万金油函数,可输入或输出不同类型的数据。后面有一些针对性较强的输入输出函数

  • 都没有参数个数限制

  • 都使用格式字符串参数列表

    char names[40];
    //格式字符串:My name is %s
    scanf("%s",names);
    printf("My name is %s",names);

区别:参数列表

  • printf使用变量、常量、表达式。scanf使用指向变量的指针

3.1 printf

转换说明:

转换说明是我们希望printf打印输出的类型,所以转换说明要与带打印变量类型一致

转换说明:%d让printf认为待打印的值是int类型 然后把给定的值翻译成十进制整数文本并打印。所以要保持一致 转换说明意义:转换说明把计算机中二进制值转换成字符串便于显示。如:76→01001100→字符7字符6→显示为76

'%'是标识符,用于标识转换说明

int num = 3;,num是整数类型,printf打印时要用%d把存储的二进制整数类型数据num转化为十进制整数

printf打印'%'符号时,打印两个就行

printf("我想打印%%符号");

大部分函数有返回值;表达式都有一个值

1.printf的转换说明修饰符(略)

对于浮点数转换说明,printf和scanf好像还有不同,暂时略过

2.打印较长字符串

两种方法处理长printf语句(本质上利用了字符串面量的串联特性):

//1.分行,但不要从双引号中间断开	
printf("Ur name of %zd letters occupies %zd memroy cells.\n",
		strlen(name), sizeof name);
//2.一句话用两个printf打印
	printf("The phrase of praise has %zd letters",
		strlen(PRAISE));
	printf("and occupies %zd memory cells.\n", sizeof PRAISE);

注意:以下方式等价

printf("Hello,world");

printf("Hello" ",world");

printf("Hello"
	",world");

printf("Hello");//可用于拆分长语句
printf(",world")

3.2 使用scanf()

scanf会读取回车

2.1 格式

第二个参数是地址

scanf("%d",num);//数组本身是地址,无需&符号
scanf("%d",&weight);//变量

2.2 读取原则

原则:scanf会跳过非空白字符前的所有空白字符,读取直到再次遇到空白字符或者与正在读取的字符不匹配的字符。除"%c"外,因为会读取空格。scanf读取字符串输入时,已经完成将'\0'放入字符串末尾。scanf会读取回车

示例:空格、制表符都属于空格,而换行符表示输入结束

//共输入三个参数,只要在每个输入项之间输入至少一个换行符、空格、制表符即可,可以在一行或多行输入。但唯一例外的是%c转换说明,%c会让scanf读取每个字符,包括空白
#include<stdio.h>
int main(void)
{
	int age;
	float assets;
	char pet[30];

	printf("Enter ur age,assests, and favorite pet.");
    //输入两个参数!  
	scanf("%d %f", &age,&assets);
	scanf("%s", pet);
	printf("%d $%.2f %s\n", age, assets, pet);

	return 0;
}

scanf详解:如果scanf希望法一个数字字符或符号(-+)。如果能找到便保存并读取下一个字符,一直读到非数字字符。此时它认为读到了整数的末尾,然后scanf把非数字字符放回输入。下一次读取输入时,继续从缓冲区读取这些非数字字符

2.3 返回值:

scanf返回一个整数值:该值等于scanf读取成功的个数(如果没有读取任何项,且需要读取一个数字二用户缺输入一个非数值字符串,便返回0)或 EOF(读到文件结尾时返回EOF,而fgets会返回空指针NULL)

2.4 格式字符串中的普通字符(略)

格式要求严格

2.5 printf和scanf的*修饰符(略)

二者都可利用 * 来修转换说明的含义,但用法不一样

2.6 printf()用法提示:使用转换说明控制输出的外观:宽度,小数位,字段内布局

转义字符用法:%d,%05d,%-5d,%.5d的区分:

  • %d是普通的输出 %5d是将数字按宽度为5,采用右对齐方式输出,若数据位数不到5位,则左边补空格 %-5d就是左对齐 %05d,和%5d差不多,只不过左边补0 %.5d从执行效果来看,和%05d一样

//输出结果:
2			printf("%d\n", 2);
    2		printf("%5d\n", 2);
2			printf("%-5d\n", 2);//加-(负号)即为左对齐
00022		printf("%05d\n", 22);
2			printf("%0d\n", 2);
00002		printf("%.5d\n", 2);

%4.1f:

  • 4同上,表示宽度

  • 1表示保留小数点后1位

示例:要把数据打印成列,指定固定字段宽度很有用。默认的字段宽度是待打印数字的宽度,很容易乱。可采用两个方法

  • 方法一:在两个转换说明中间插入一个空白字符(空白是普通字符,会跟着被打印)

  • 方法二:在文字中嵌入一个数字,通常指定一个小于或等于该数字宽度比较好

    int num = 99;
    char names[40] = "Sam";
    printf("%d\n", num);
    //输入字段宽度应为2,但整数有3位,所以字段宽度自动扩大以符合整数长度
    printf("%2d\n", num);
    printf("%10d\n", num);//右对齐,字段宽度为10
    printf("%-10d\n", num);
    //若要在文档中嵌入一个数字,通常指定一个小于或等于数字宽度的字段会比较方便
    printf("My name is %d",names);
    
    /*
    99
    99
            99
    99
    My name is Sam
    */

五、运算符、表达式、和语句

1.基本算术运算运算符=、+、-、*、/

1.1 赋值运算符:=

作用:把值存储到指定内存位置

赋值表达式的:左侧运算对象的值

特殊结合律:左←右。'='左侧必须是可赋值的存储位置,最简单的办法就是变量名(及指针:可指向一个存储位置),记为可修改的左值

左值(不同于对象)与右值:(赋值语句中的概念):

  • 右值:只能是字面常量(常量本身就是值,不能给常量赋值)、能赋值给左值的量,且自身不是左值。

  • 左值:用于标识特定数据对象的标签,如变量、指针,一切变量都是左值

  • 可修改的左值:表示可赋值的数据对象的标签(const变量除外,因为不能修改赋值)

  • 数据对象:用于存储值的数据区域,对象是实际的数据存储

  • int ex,why,zee;
    const int TWO = 2;
    //why、zee是可修改的左值,表达式(why+zee)是右值,不能表示特定内存位置也不能被复制,是程序的一个临时值,计算完会被丢弃
    ex = TWO * (why + zee)
    //三个都是可修改的左值,因为每个变量都指标识了一个可被赋值的数据对象,但salary+bribes是一个右值
    income = salary+bribes;

C允许三重赋值

  • int jane,tarzan,cheeta;
    //左←右
    cheeta = tarzan = jane = 68;

1.2 加法运算符:+

运算对象

  • 运算符操作的对象。如'下面='左侧的可修改左值y

  • 运算对象可以使常量、变量或二者组合,如下面的c/d

y = x+(2*3+c/d)

1.3 减法运算符:-

二元运算符:即需要两个运算对象才能完成,减法运算符和加法运算符都是

1.4 符号运算符:-和+

  1. 减法还可用于表面或改变一个值的代数符号:这样的符号称为一元运算符(一元是指需要1个运算对象)

    //smokey结果为12
    rocky = -12;
    smokey = -rocky;
  2. 一元+运算符无法改变运算对象的值或符号,只能这使用

    dozen = +12;

1.5 乘法运算符:*(略)

1.6 除法运算符:/

整数除法与浮点数除法不同:

  1. 整数除法:结果为整数

    5/3=1,小数部分被丢弃(截断)而不是四舍五入
    • 负整数除法:趋零截断,C99规定直接丢弃结果的小数。如:-3.8转化为-3,3.8转化为3

  2. 浮点数除法:结果为浮点数

  3. 混合除法:计算机不能真正用浮点数除以整数,编译器会把两个运算对象转化为相同类型(后面也有提到)

    //本例整数会转化为浮点数
    printf("mixed division: 7./4 is %1,2f \n",7./4);

1.7 运算符优先级

  • 当两个运算符优先级相同且处理同一个运算对象,按结合律,左→右顺序执行(=和一元+-除外 )

    //先执行60*n,再执行除法
    butter = 25+60*n/scale;
  • 当两个优先级相同且操作对象不同时,系统根据硬件进行选择,可以提高效率

    //并不能确定先执行6*12还是5*20
    y = 6/2+5*20(1+2);
  • 结合律:给出运算符如何和运算对象结合。只适用于共享同一运算对象的运算符

2.其他运算符

2.1 sizeof运算符和、size_t类型

1.sizeof:

为什么是运算(操作)符而非函数?

  • 如果是函数,那么形参数组会退化为(指向指代其自己的)指针,按理说sizeof(数组)会输出地址长度,但实际使用sizeo(ar),输出的是整个数组的长度,所以sizeof不是函数

  • 另一方面,c没有函数会以变量类型作为输入sizeof(int)

byte为单位返回运算对象大小

  • char name[40];//这里不必初始化
    
    //都加上括号就行了
    printf("%sd",sizeof(name));//括号随意
    printf("%sd",sizeof(int));//括号要有

2.size_t(以后再看)

sizeof返回size_t类型的值,size_t是一个无符号整数类型

C有一个typedef机制,可为现有类型创造别名

//real就是double的别名
typedef double real;

2.1 求模运算%

  • 只用于整数

  • 负数求模(同负整数除法):按照C99趋零截断,即结果正负取决于第1个数的正负

    //	除法		求模
     11/ 5= 2   11% 5= 1;
     11/-5=-2,  11%-2= 1;
    -11/-5= 2  -11%-5=-1;
    -11/ 5=-2  -11% 5=-1;

2.2 递增运算符++

定义:

  • ++a:先递增,后赋值

  • a++:先赋值,后递增

  • 单独使用递增时,哪种形式都OK!!!

示例:

//使程序更简洁,比原来少了个++shoe,但shoe初值从3变成2
shoe = 2.0;
//先递增1,再比较
while(++shoe<18.5) //shoe++也可以,但是要改值
{
	foot = SCALE*shoe+ADJUST;
	printf("....")
}

以后不要使用x = x+1;这种形式

优先级:

  • 仅次于括号

    x*y++相当于(x)*(y++),但并不知道先执行括号里的哪个 //另外要知道(x*y)++无效,因为递增运算符只能影响一个变量。或者说,只能影响一个可修改的左值,x*y本身是字面常量,用完及弃

不要自作聪明:

  • 一个变量出现在一个函数的多个参数中,不要对该变量递增、递减

  • 一个变量多次出现在一个表达式,不要对该变量递增或递减

    //对于这个程序,程序获取待打印值时,不一定总左往右顺序执行,可能先执行num*num++,也可能直接从右往左执行(理由参照点1)
    printf("%10d %10d\n",num,num*num++);

3.表达式和语句

3.1 表达式:

  • 目的:表达式是为了求值,表达式都一个值

  • 定义:运算符+运算对象。最简单的表达式是不带运算符的单独的运算对象(常量或变量)

    • 子表达式:较小的表达式

    //c/d是整个表达式的子表达式
    a*(b+c/d)/20//注意没有';'
    • 表达式语句:表达式;

    • 完整表达式:这个表达式不是另一个更大表达式的子表达式。表达式语句中的表达式while循环中作为测试条件的表达式都是完表达式

  • 每个表达式都有一个值(不是函数返回值):有'='的表达式的值是左值逗号表达式的值是右值

    q = 5*2;//值为10
    q > 11;//值为0,真假的范畴

3.2 语句(区分指令):

  • 定义:C中大部分语句以';'结尾。包括:赋值表达式语句函数表达式语句空语句复合语句等。一条语句相当于一台完整的计算机指令

    • toes = 12;//赋值表达式语句
      printf("%d",num);//函数表达式语句!
      ;//空语句
    • 复合语句(块):由花括号括起来的一条或多条语句构成

    • 注意:即使while、for、if由复合语句构成,整个while语句仍被视为一条语句。该语句从while开始到第一个';'结束。在使用复合语句情况下,到右花括号结束

  • a*(b+c/d)/20//表达式
    a*(b+c/d)/20;//表达式语句,不过没用。++x,x=a*(b+c/d)/20;才是有用的

    注意:C认为声明(指int num;)不是表达式语句

3.3 *指令:

定义:y=5。一条语句相当于一条完整的指令,但不是指令都是语句

//y+5是一条完整的指令,但只是语句的一部分,所以;'用于区分这种情况下的语句
x = 6 + (y = 5);

3.4 副作用和序列点

副作用:对数据对象或文件的修改。可以说主要目的赋值就是他的副作用。printf显示的信息就是他的副作用(printf返回值是待显示字符的个数)

序列点:程序执行的点,在该点上,所有副作用都在进入下一步之前发生。

  • ';'是序列点:在一个语句中,赋值运算符、递增、递增 、递减运算符对运算对象做的改变必须在程序执行吓一跳语句之前完成。

  • 任何完整表达式的结束是序列点

  • ','是序列点

序列点意义:有助于分析后缀递增何时发生

//表达式guests++<10是while循环的测试条件,所以是一个完整表达式,所以他的结束就是一个序列点。所以C保证了程序转移至printf之前发生副作用即递增guests。同时后缀形式保证了guests完成比较后才进行递增
while(guests++<10)
	printf("%d",gusets);
//4+x++不是完整表达式,所以C无法保证x在子表达式4+x++求职后立刻递增x。整个表达式是完整表达式,所以C保证执行下一条之前递增x两次。C未指明在子表达式求值之后再递增x,还所有表达式求值后再递增x。所以要尽量避免编写类似语句
y = (4+x++)+(6+x++);

4.类型转换?什么情况下发生自动转换什么情况发生强制类型转换呢

C允许混合数值类型的表达式,但是算术运算要求对象都是相同类型,因此C会进行自动类型转换

在语句和表达式中应尽量使用类型相同的变量和常量,不然就会产生被动或主动的类型转换

4.1 自动类型转换

  1. 类型转换出现在表达式:char、short都会自动转为int

  2. 涉及两种类型的运算,两个值会分别被转化为两种类型的更高级别

  3. 赋值表达式语句中:计算的最终结果会被转换成被赋值变量的类型,这个过程可能导致类型升级或者降级。降级可能带来麻烦

    • 截断就是浮点型降级为整数型

4.2 强制类型转换

//mice = 3.3
mice = 1.6+1.7;

//mice = 1+1 = 2(发生截断)
mice = (int)1.6 + (int)1.7;

尽量避免自动类型转换,尤其是降级

7.带参数的函数

形参:是变量

变量名是函数私有:函数中定义的变量名不会和别处的相同名称发生冲突

六、循环

1.while语句

//while通用形式
while (expression)
	statement//statement可以是简单语句或花括号扩起来的复杂语句(for、if同理)

最大正值+1一般会得到一个负值,最小赋值-1一般会得到最大正值。变完后会终止循环

while (--index<5)//但这样的写法并不好
	printf(".....");

语法要点:

  • 只有测试条件后面的单独语句(简单或复合语句)才是循环部分。即使while/for语句本身使用复合语句,语法上也是一条单独语句(for、if同理)。该语句从while/for关键字开始到第一个';'结束。在使用复合语句情况下,到右花括号结束。事实上缩进只是为了阅读方便(C自动忽略缩进)

    //屏幕会无限输出n = 0
    int n = 0;
    
    while (n<3);
    	printf("n is %d",n);
    	n++;//不属于循环部分
  • 注意分号位置:测试条件后面的单独分号是空语句,什么也不做

    //为提高可读性,应让分号另起一行。最好的处理方法是continue
    while(scanf("%d",&num) == 1)
    	;//跳过整数输入

2.用关系运算符和表达式比较大小

关系运算符:< <= == >= > !=

while、if等的测试条件通常都是关系表达式,或逻辑表达式!!!!!!!!!11!

关系表达式可用于比较字符,比较的是字符码(如ASCII码)。但不能比较字符串 比较浮点数尽量用< >。因为浮点数有舍入误差。如1/3=0.99999≠1.0=3*1/3

2.1 真与while

while的循环条件是测试表达式为真。 那么C如何表示真:用值表示。C用1表示真, 那么C如何处理while测试条件:用值处理。表达式为真的值都是1(表达式是为了求值,表达式都个值)。所以while的测试表达式为真本质上是通过值1,所以数值1或表达式值为1都可实现while循环 但C对真的概念很宽,只要非0就视为真。所以只要非0或表达式非0while就会循环

//新手版
while(goats != 0)
	.....

//大佬版
while(goats)
	.....

2.2 新的_Bool类型

C一直用int类型变量表示真假值。C针对这种类型变量新增_Bool类型

用Bool类型表示真假,Bool只能存储0、1,其他非0值赋给布尔型变量都会变为1..现在C99提供头文件<stdbool.h>头文件,让bool成为BOOL的别名

_Bool只能存储0、1,其他非0值赋给Bool都会变为1.

stdbool.h头文件让bool成为Bool别名

2.3 优先级

赋值运算符<关系运算符<算术运算符

3.不确定循环和计数循环

  • while在执行循环之前就已决定是否执行循环

  • 计数循环:执行循环之前就知道要执行多少

  • 不确定循环:一些while是不确定循环,在测试表达式为假之前并不知道要执行多少次,比如用户交互获得数据来计算

    status = scanf("%1d,&num");
    while (status == 1)
    	......

4.while缺点

初始化、测试、更新步骤多,繁琐。

int count = 1;   //声明+初始化

while (count<5) //测试,可以写成++count<5
{
	printf("...");
	count++; ?   //更新
}

for循环可以避免这些麻烦

5.出口条件循环:do while

while和for都是入口条件循环:即循环之前检查测试条件

do while是出口条件循环,每次循环之后检查测试条件,这样保证循环体内容至少执行一次

do
	statement //同样,可以是简单语句也可以是花括号包裹的复杂语句
while (expression);

6.for循环

for(初始化,测试,更新),把三部曲都放在了括号里。括号里的叫做控制表达式,也是完整表达式(while括号里是测试表达式,也是完整表达式)

更新可以是任意形式的表达式,绝不仅限于++、--

  • 初始化:仅在循环开始前执行一次

  • 测试:循环开始前执行

  • 更新:循环结束时执行

for(int count = 22; count< 5; count++)
	printf("...");

//可以用字符代替数字:字符在内部以整数方式存在,该循环实际上是用整数计数。while的测试表达式也可以比较字符,都是一个道理
for (ch = 'a';ch<'z';ch++)
    printf("...");

//可以省略一个或多个表达式,但';'不可省(一般省略第三个,放到循环内部更新)
for (n = 3;ans<=25;)
    ans = ans*n;//起到更新作用

//第一个表达式也不一定是给变量赋初值,也可以用printf
int num = 6;
for (printf("Keep entering numbers!\n");num! = 6;)
    scanf("%d",&num)

7.其他赋值运算符:+=、-=、*=、/=、%=

优点:代码更紧凑,生成的机器代码效率也更高

特点:都是在自身基础上改变才可使用

x += 1	x = x+1
x -= 1	x = x-1
x *= 1	x = x*1
x /= 1	x = x/1
x %= 1	x = x%1

应用:以下等价

x = x*(3*y+12);

x *= 3*y+12

优先级:同'='

8.逗号运算符

逗号表达式把两个表达式通过','连接成一个表达式

逗号运算符最常用于for循环,可以扩展for循环的灵活性,使其在循环体中包含更多的表达式

const int FIRST_OZ = 46;
const int NEXT_OZ = 20;

for(int ounces = 1,int cost = FIRST_OZ; ounces <=16; ounces++,cost+= NEXT_OZ)
    printf("......")

逗号运算符的两个性质:

  1. 保证了被逗号分割的表达式从左向右执行(即','是一个序列点,其左侧副作用都在执行右侧之前发生)

    保证了先递增ounces,再执行右侧
    ounces++,cost = ounces * FIRST_OZ
  2. 整个逗号表达式的值是右侧的值

    //先把3给y,y递增为4,再把4和2相加得到6,6+5=11,最后把11赋给x
    x = (y = 3,(z = ++y+2) + 5);
    
    //显示houseprintce = 249,再是一个语句或子表达式500;
    houseprintce = 249,500;
  3. 逗号用作分隔符,而不是分隔符

    char ch,data;
    pritnf("%d %d",num1,num2);//,'分隔符分割参数

9.如何选择循环

先确定入口循环还是出口循环

对于入口条件循环,看个人喜好,也看具体情况。如涉及索引计数用for,单个条件测试用while

  • 如果让for循环看起来像while循环可以省略第一个和第三个表达式

for(;test;)
while(test)
  • 如果让while更像for,可以再while循环前面初始化变量,并在while循环体中更新语句

初始化;
while(测试)
{
	其他语句;
	更新语句;
}

10.嵌套循环

常用于:

  1. 按行和列显示数据

  2. 外层循环控制内层循环。如用外层循环次数决定内存打印个数

11.数组简介

连续存储单元构成、相同类型数据元素的有序序列

用于识别数组元素的数字称为下表标索引/或偏移量

//一个元素4byte,每个元素存储int类型值
int num[20];有80个存储单元!!!

C编译器不会检查数组的下标是否正确,如果给超过下标的位置赋值,会导致数据被放置在已被其他数据占用的地方,可能影响其他程序

debts[20] = 88.32;//如果超过了数组边界,C也不会察觉

可以把字符串存储在char型数组中,如果char类型数组末尾包含一个表示字符串末尾的空字符\0,则该数组的内容构成一个字符串。

数组常用于for循环,存储很方便

1.在for中使用数组

int类型数组元素用法与int类型变量用法类似:scanf("%d",&score[index])

#include<stdio.h>
#define SIZE 10
int main(void)
{
	int index, score[SIZE];//整数和整数数组同时声明
	int sum = 0;
	float averge;

	printf("Enter %d golf scores:\n", SIZE);//逗号做分隔符
	for (index = 0; index < SIZE; index++)
		scanf("%d", &score[index]);//读取10个分数

	printf("The scores read in are as follows:\n");
	for (index = 0; index < SIZE; index++)
		printf("%5d", score[index]);//验证输入
	printf("\n");

	return 0;
}

8.小结

现代编程习惯把程序分为接口部分实现部分

  • 接口部分:描述如何使用一个特征,如函数原型

  • 实现部分:描述具体行为,如函数定义

七、C控制语句:分支和跳转

1.if语句

整个if语句被视作单独一条语句

if语句结构和while(和for又都是循环)非常相似。区别在于满足条件时,if只测试和执行一次,而while可测试和执行多次

if (expression)
    statement//同前

2.二选一:if else语句:

技术角度:整个if else语句被视为一条语句(当然里面的if、else又是单独的语句)

注意:if和else之间只能有一条语句(简单或复合语句,而复合语句必须'{}'扩起来),因为else不紧跟着if,会报错

if (x>0)
	printf("...");
	x++;//这是错误的,x++;是独立于if语句之外的语句,会让编译器以为else没有所属的if,产生错误
else
	printf("...");

3.多重选择:else if(略)

用else if扩展if else结构

4.else与if配对

  • 如果没有'{}',else与最近的If匹配(因为编译器忽略缩进)

    if (number > 6)
    	if (number<12)//else所对应的if
    		printf("...");
    else
    	printf("...");
  • 如果最近的if被'{}'扩起来:else与内含If语句的第一个if语句匹配

5.getchar、putchar

getchar和putchar是字符输入/输出函数(前面学过万金油scanf、printf)

  • getchar、scanf都会获取'\n'

  • getchar、putchar只处理字符,不需要转换说明,所以比scanf、printf更快更简洁

  • getchar、printf的定义在<stdio.h>中,且通常是预处理宏,不是真正的函数

  • getchar可以输入一连串字符,但一次智能获取一个

5.1 getchar

也会读取'\n'

无参,从输入队列中返回下一个字符

//等价
char ch = getchar();
scanf("%c",ch)

5.2 putchar

单字符不会添加换行符,但可以打印换行符

可以输入int型打印为char型。参见下面的'程序实例'

有参,打印它的参数

putchar('\n');//直接打印字符使用单引号'' !!!
//等价
printf("%c",ch);
putchar(ch+1);//再次说明字符本质作为整数存储

程序示例:

该程序把一行输入重新打印,如果字符空白,原样打印,否则打印原字符在ASCII序列中的下一个字符

#include<stdio.h>
#define SPACE ' '
int main(void)
{
    char ch;
    
	//一行未结束时
    while ((ch = getchar()) != '\n')// '=' < '!='必须括起来,不然就出错了
    {
        if (ch != SPACE)//不能if(ch)啊,' '的ascii是32,'\0'的ascii才是0!
            ch += 1;//不同类型计算,这里ch自动转化为int类型。然后被传递给接受int类型参数的outchar,该函数只根据最后一个字节确定显示哪个字符
           
        putchar(ch);
    }
    //执行到此ch里面是'\n'
    return 0;
}

要注意:对于ch = getchar != '\n',按照优先级先执行!=,再给ch赋值

5.3 scanf、printf和getchar、putchar

  • scanf从标准输入设备获取文本,只能获取数据类型的一种,接收字符串可以用转换说明%s 要注意:1.%c模式获取字符,只能一个字母,hhh不是一个字符是字符串。2.%c模式输入空格也会当做字符读取(因为空格也是字符) 为什么两行scanf只会执行第一个。其实在我们第一次输入并按下回车的时候,控制台一共获得了四个字符,分别是:a、b、c、回车(enter)。但是因为scanf()方法遇到非字符的时候会结束从控制台的获取,所以在输入’abc’后,按下 ‘回车(enter)’ 的同时,将’abc’这个值以字符串的形式赋值给了类型为 ‘char’ 的 ‘m’ 数组,将使用过后的字符串: ‘回车(enter)’ 保存在控制台输入的缓冲区,然后继续执行下一段输出代码,然后又要求用户输入。此时,因为上一次被使用过后的字符串被保存在缓冲区,现在scanf()方法从控制台的缓冲区获取上一次被使用过后的字符串,并只截取第一个字符: ‘回车(enter)’ ,此时控制台缓冲区才算使用完了。所以在看似被跳过的输入,其实已经scanf()方法已经获取了我们的输入了,这个输入就是一个 ‘回车(enter)’ 。 那么如果解决呢,可以在两个scanf之间加一个getchar(循环)来清楚回车

  • getchar是stdio.h中的库函数,它的作用是从stdin流中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。

  • 相同点:1.scanf、getchar返回值均为int。scanf返回值为成功输入的个数。getchar的值是读取的字符的值,所以用char接收getchar返回值其实是不对的。 2.两者均通过enter结束,本质是两者进入缓冲区读取字符,getchar有循环就会可以全部读出

  • 两者作用都是从stdin流(存在缓冲区)中读入内容,如果stdin有数据就不用输入而是直接读取。所以第一次确实需要人工输入,但如果输入较多有剩余,以后的scanf和getchar再执行就直接从缓冲区读取。

  • Enter问题,键入的内容都会存到缓冲区,一旦键入回车二者就会进入缓冲区读取内容,但回车依然作为一个字符存于缓冲区,下一个scanf或getchar依然会顺序执行,这也是为什么连着写两个scanf只能执行第一个

3.ctype.h系列函数

C有一系列专门处理字符的函数,ctype.h头文件包含了这些函数的原型,这些函数接受一个字符作为参数,如果该字符属于对应的类别,则返回非零值(真),否则返回假

isalpha( )函数的参数是一个字母,返回非零值

#include<stdio.h>
#include<ctype.h>
#define SPACE ' '
int main(void)
{
    char ch;
    
    while ((ch = getchar()) != '\n')
    {
        if (isalpha(ch))//这里做了优化
            ch += 1;
           
        putchar(ch);
    }
    //执行到此ch里面是'\n'
    return 0;
}

5.逻辑运算符

5.1 与、或、非

与:&& 或:|| 非:!

程序示例:计算输入的一行句子中字符的数量

#include<stdio.h>
#include<ctype.h>
#define SPACE '.'
int main(void)
{
    char ch;
    int count = 0;

    while ((ch = getchar()) != SPACE)
    {
        if (ch != '"' && ch != '\'')//注意这里双引号正常表示,单引号用\'!
            count++;//没必要count += 1;
    }
    printf("There are %d non-quote characters.\n", count);

    return 0;
}

C是美国用标准美式键盘开发的语言,为了方便其他键盘,C99新增可地带逻辑运算符的拼写,他们定义在iso646.h中。

  • 与:&& 、 and 或:|| 、 or 非:! 、 not

5.2 优先级:非>或>与

!:与++优先级相同

赋值运算符<||<&&<关系运算符

5.3 求值顺序

除了共享一个运算对象情况之外,C通常不保证先对复杂表达式中哪部分求值。但是对于逻辑运算符,程序保证逻辑表达式求值从左到右

  • &&和||都是序列点(',' ';' 完整表达式结束),而且C保证一旦发现某个元素让整个表达式无效,便立刻停止求值。所以C才可以写出如下结构

//没有上述保证,程序可能无法先对c赋值
while ((c = getchar()) != ' ' && '\n')
//如果number=0,那么第一个表达式为假且不再对关系表达式求值,这样避免了把C作为除数
if (number!=0 && 12/number==2)
    
//保证左侧表达式求值之前,左侧已经实现递增
while (x++<10 && x+y<20)

逻辑算符的运算对象通常是关系表达式

5.4 范围

测试score是否在90~100之间

//错误写法,这样写语义有错误,<=运算符求值顺序是从左向右,对于编译器会理解为(90<=score)<=100,,左侧表达式值要么是0要么是1,一定会小于等于100.
if (90<=score<=100)
    statement

//正确写法
if (socre>=90 and score<=100)
	statement

范围测试可测试一个字符ch是否是小写字母:仅对ascii码有效,因为这些编码中相邻字母与相邻数字一一对应

if (ch>='a' and ch<='z')
	statement;

//当然也可以用ctype.h系列中的islower函数
if(islower(ch)) 
    statement;

6.条件运算符:?:

三元运算符,if else语句的便捷方式

  • expression1?expression2:expression3

  • expression1是一个关系表达式,为真则表达式值为expression2,否则为expression3

  • expression1、expression2可以是字符串

//等价,但有时三元更简洁
max=(a>b)?a:b;

if (a>b)
	max = a;
else
	max = b;

7.循环辅助continue和break

程序在下一次循环测试前会执行完循环体中所有语句,而continue、break可以根据循环体中测试结果忽略一部分循环内容

continue、break、*goto都可以使程序跳转至某处二者的意义在于简化程序、提高可读性

7.1 continue

功能:跳过本次迭代剩余部分,开始下一轮迭代

范围:可用于三个循环,不可用于switch。但如果switch在循环中,continue便可作为switch的一部分

优点:

  • continue可以减少进一级的缩进,提高代码可读性

  • continue作为占位符

    //';'很难注意到
    while (getchar() != '\n')
    	;	
    //优质写法,可读性更高 
    while (getchar() != '\n')
        continue;

ru和避免使用continue

  • 省略continue,把剩余部分放在else块中!

  • 对if里的关系表达式进行逻辑上的修改

continue让程序从跳过循环体余下部分,那么从何处继续循环?

  • 首先,continue在多个嵌套while中只会影响其所在的while

  • 对于while、do while:执行continue后的下一个行为是对循环的测试表达式求值

  • 对于for:执行congtinue后的下一个行为是更新,然后对求值

7.2 break

功能:终止当前循环执行循环后面的语句

范围:可用于三种循环和switch

对于嵌套循环,嵌套内层的break只会让程序跳出当前内层循环,若要跳出外层循环,则还需要一个break

8.多重选择:switch case和break

8.1 switch case

功能:在多个选项中选择其一,同if else if...else

要求:expression和case标签都必须是整数值(包括char类型)

优点:在多个选项中进行选择可以使用if else if...else完成,但大多情况下使用switch更方便

示例:

if (islower(grade))
{
    switch (grade){//匹配具体的值
    case 'A': //case 'A'是一个标签
        System.out.println("优秀");
        //break可选,如果不选且匹配到这一项会把这一项及后面同样没有break的都执行,直到遇到有break的
        break;
    case 'B':
        System.out.println("良好");
        break;
    case 'C':
        System.out.println("及格");
        break;
    case 'D':
        System.out.println("再接再厉");
        break;
    case 'E':
        System.out.println("挂科");
        break;//其实这里可以删除,不过为了防止以后再在之后添加时遗漏这个break,可以留着
    default://可选,如果上面没有匹配的就跳转至改行,否则程序继续执行switch后面的语句
		printf("未知等级")
            
}

8.2 只读每行的首字符

当在上述switch里,输入BCA也会执行,并且只处理了第一个字符

,这是一种丢弃其他字符的行为

//只读取输入行首字母
while (getchar() =! '\n')//注意函数的返回值并没有赋给ch,只是读取并丢弃
	continue;

//如果用户一开始就摁下Enter键,那么独到的收个字符就是换行符,可按如下处理
if (ch == '\n')
    continue;

9.如何选择if else和switch

switch运行更快、代码更少

如果根据浮点类型或表达式值来选择 或 根据变量在某范围内决定程序流去向,就不好使用switch

10.goto语句

goto 标签

直接跳转到程序的某个地方。break、continue是goto的特殊形式且不需要标签

不建议使用

八、字符输入/输出和输入验证

本章逻辑:操作系统需要知道文件什么时候结束,常用ctrl+z标识结尾 或 存储文件大小(使用较多) 不管操作系统如何识别文件结尾,c的函数getchar、scanf读到文件结尾会返回EOF 但如果是键盘输入如何模拟文件结尾呢,本系统用ctrl+z

有些特殊系统有特殊的I/O函数,本章针对所有系统通用的I/O函数,并假设所有输入都是缓冲输入

0.EOF、NULL、'\0'、' '等

文件结尾:操作系统为检测文件结尾,会在末尾放置特殊字符标记或存储文件大小。无论操作系统如何检测文件结尾,C的scanf、getchar读到文件结尾会返回特殊值即EOF。本系统用键盘输入模拟文件时用首行输入Ctrl+Z模拟文件结尾字符EOF,scanf、getchar读到了就会返回EOF(暂时这么理解就可以了)

EOF:是一个整数类型的值:-1。-1

//为什么不能char ch:字符对应的ascii均为正,如果读到文件末尾返回EOF,把-1赋给char类型就不对了
int ch;
ch = getchar();

空指针:常数0在指针上下文编译时会被转化为空指针(char*pi = 0),其实就是0地址,这个地址在C语言里面是不允许访问的(而未初始 化的指针则可能指向任何地方)

NULL:在程序里看到太多的0并不好,所以在stdio.h中把NULL定义为空指针常数0,编译时预处理器会把所有的NULL都还原为0。所以 NULL就是0(这么说其实不太准确)

'\0':ascii为0,C用来标记字符串结束.也记为NUL

' ':ascii为32

'0':ascii为48

1.单字符I/O:getchar和putchar

文本:计算机的一种文档类型。该类文档主要用于记载和储存文字信息,而不是图像、声音和格式化数据。常见的文本文档的扩展名有txt、doc、doc、wps等

文本格式:一种由若干行字符构成的计算机文件,文本格式有txt、doc、docx、wps。文本文件存在于计算机文件系统中,文本文件可以包含纯文本。一般来说,计算机可以分为文本文件和二进制文件两类

这两个函数每次只处理一个字符,对于人来说非常笨拙,但却很适合计算机,并且是绝大多数文本(普通文字)处理程序的核心方法

2.缓冲区

函数从缓冲区读取成功的话,被读取的数据就从缓冲区取出了,缓冲区就没有刚刚被读取的数据了!!!

定义:缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区输出缓冲区(所以无论是从内存读取还是外部输入内存,中间都有个缓冲区)。关键要理解输入和输出函数获得实参后会在内存中的缓冲区分配存储空间用于存储实参,即缓冲区是专门针对输入输出函数的一块内存

作用:加快运行速度。因为计算机对缓冲区操作速度更快

  • 无缓冲输入:正在等待的程序可立即使用输入的字符。有些地方会用到,比如游戏中希望按下一个键就执行相应指令

  • 缓冲输入:摁下Enter之前不会重复打印。输入的字符被收集并存储在一个被称为缓冲区的临时存储区,摁下Enter之后程序才可使用用户输入的字符。把若干字符当作一个块进行传输比逐个发送这些字符节约时间。如果打错字也可通过键盘直接修改

缓冲分为两类:完全缓冲I/O和非完全缓冲I/O,

  • 完全缓冲I/O:缓冲区被填满才刷新缓冲区(内容被发送至目的地),常用于文件输入

  • 行缓冲I/O:出现换行符时刷新缓冲区。常用于键盘输入,所以摁下Enter键后才刷新缓冲区

ANSI C把缓冲输入作为标准,因为一些计算机不允许无缓冲输入

3.结束接盘输入

3.1 文件、流和键盘输入

  • 文件:存储器中存储信息的区域。文件通常保存在某周永久存储器中((硬盘、U盘或DVD等)),C程序就保存在文件中

  • 从较低层面上:C可使用 主机操作系统的基本文件工具 直接处理文件,这些直接调用操作系统的函数被称为底层I/O。由于各个于计算机系统差异,不可能为普通底层I/O函数创建标准库

  • 从较高层面上:C可通过标准I/O包来处理文件,这涉及创建用于处理文件的标准模板和一套标准I/O函数,在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面

  • 差异:1.系统存储文件方面,有的系统把文件内容存储在一处,文件相关信息存储在另一处。而有的系统在文件中创建一份文件描述。2.处理文件方面,有些系统使用单个换行符标记末尾,有的可能使用回车符+换行符组合表示行末尾。3.还有的以最小字节衡量文件大小,有的以字节块大小衡量。

  • 如果使用标准I/O包就不用考虑这些差异!可以用 if (ch == '\n') 检查换行符。即使系统实际用的时回车符和换行符的组合来标记末尾行,I/O函数会在两种表示法之间相互转换(stdio.h标准输入输出头文件,stand intput & output)

  • 从概念上:C程序处理的是流而不是直接处理文件。意味着不同属性和不同种类的输入有属性更统一的流来表示。于是打开文件的过程就是把流与文件关联

  • 本文着重理解C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个C程序自动打开的文件。stdin流表示键盘输入,stdout流表示屏幕输出。getchar、putchar、printf\scanf函数都是标准I/O包成员,处理两个流。

  • 可以用处理文件的方式处理键盘输入。程序读文件时要能检测文件结尾才知道如何停止,所以C输入函数内置了文件结尾检测器,既然键盘输入视为文件那么同理可用文件结尾检测器结束键盘输入。下面先学习文件

3.2 文件结尾

标准输入与文件输入不一样,无法事先知道输入流的大小,必须手动输入一个字符,用来标识文件流结尾,Unix使用ctrl+D标识文件结尾。MS-DOS环境下使用字符ctrl+z标识文件的结束。如果以文本模式打开这样的文件,c可以认出这个字符是标识文件结尾的字符,

无论操作系统使用何种方法检测文件结尾,在C中,用getchar(scanf也一样)读取文件检测到文件结尾时将返回一个特殊,EOF(end of file)。通常EOF定义在stdio.h中。之所以用-1,因为-1不对应任何字符。如果包含stdio.h并使用EOF符号,就不必担心EOF值的问题

//头文件stdio.h中会发现如下代码
#define EOF(-1)

如何在程序中使用EOF?把getchar返回值和EOF比较,不同则说明没到末尾。

while ((ch = getchar()) != EOF)

如果正在读取的是键盘输入而不是文件该怎么办,绝大部分系统有办法通过键盘模拟文件结尾条件 对于本系统,一行开始处的Ctrl+Z标识为文件结尾信号。我们在一行开始处用ctrl+z模拟文件结尾(getchar和scanf读到就会返回EOF!乌拉!)

/*
这个函数有很强的功能:
文件→.c→屏幕
键盘→.c→文件
文件→.c→文件
*/
#include<stdio.h>
int main(void)
{
	int ch;
    
/*
要注意的是:在终端(黑框)中手动输入时,系统并不知道什么时候到达了所谓的“文件末尾”,因此需要用<Ctrl + z>组合键然后按 Enter 键的方式来告诉系统已经到了EOF,这样系统才会结束while
*/
	while ((ch = getchar()) != EOF)/EOF换-1也一样,因为EOF的值就是-1
		putchar(ch);//会打印ch的值的等价字符

	return 0;
}

4.重定向和文件(略)

stdin标准输入流:

  • stdin是C中的标准输入流,是缓冲输入方式

  • C的标准输入指键盘的输入,stdin输入就是从键盘上读取字符

如果希望输入函数和数据类型不变,进改变程序查找数据的位置,那么程序如何知道去哪里查找

默认情况下,C使用标准I/O包查找标准输入作为输入源,就是前面介绍过的stdin流,他是把数据读入计算机的常用方式。现在我们希望它到别出去查找输入,尤其是让程序从文件中查找而不是键盘

4.1各操作系统重定向

Windows命令提示行可以重定向输入输出。重定向输入使用文件而不是键盘来输入,重定向输出让程序输出至文件

5.创建更友好的用户界面(略)

5.1 使用缓冲输入

5.2 混合数值和字符输入

6.输入验证(略)

6.1 分析程序

6.2 输入流和数字

7.菜单浏览(略)

7.1 任务

7.2 使执行更顺利

7.3 混合字符和数值输入

九、函数

1.复习函数

函数和变量一样有多种类型

声明和调用才用';',定义函数不用

如果把函数单独放在一个文件中,要把#define和#include指令也放进去

1.1 定义带形参的函数

函数里的变量和形参(形参就是括号里的变量)都是是局部变量,函数私有,其他函数也可以创造同名变量

ANSI C要求函数定义中,每个变量前都要声明其类型:

//×
void dibs(int x,y,z)

//√
void dubs(int x,int y,int y)

1.2 形参与实参

形参:是函数定义的函数头中申明的变量,是被调函数的变量

实参:是函数调用圆括号中的表达式,是主调函数赋给被调函数的具体值

实参与形参类型不同:程序会把实参类型转化为形参类型(还有返回值和函数类型不同)

1.3 黑盒视角

函数内的变量对于主程序是根本不知道的,发生了什么对主调程序也是不可见的,主调函数只是拷贝调用函数的值

1.4 使用return从函数中返回值

主调函数通过实参把信息传给被调函数,而被调函数通过返回值把信息传回主调函数(变量被调函数私有,只有(或地址)可以进出被调函数,但值也不是只可以传回变量的值,也可以是任意表达式)

函数类型:即返回值的类型

返回值不仅可以赋给变量,也可用作表达式的一部分:

返回值用作表达式的一部分
answer = 2*imin(z,zstar) + 25;
printf("%d\n",imin(-32+answer,LIMIT));

返回值不一定是变量的值,也可以是任意表达式的值:

/* 版本1 */
int inim(int n,int m)
{
    int min;
    if (n<m)
        min = n;
    else
        min = m;
    return min
}

/* 版本2 */
int inim(int n,int m)
{
	return (n<m)?n:m;//返回表达式的值,括号可选
}

函数(返回值)类型与函数(声明)类型不匹配怎么样?

  • 实际得到的返回值相当于把 函数的返回值(z)赋给 与函数(声明)类型相同的变量(int) 所得到的值

//假如调用为int what_if(64),那么赋给z的值是1.5625,但是return语句返回int类型的值1(截断了)
int what_if(int n)
{
	double z = 100.0/(double) n;
	return z;
}

return的另一个作用:终止函数,并把控制返回给主调函数的下一句,所以可有如下写法

/* 版本3 */
int imin (int n,int m)
{
	if (n<m)
		return n;//直接把声明的形参的值返回了
	else
		return m;
    printf("孙百康是大傻叉")//如果这句话是学生写的,那我可能永远也不会知道
}

只有在void函数中才能用到 单独的return

1.5 函数类型

ANSI C标准库中,函数被分成多个系列,每个系列都有各自的头文件,这些头文件除了其他内容还包含了本系列所有函数的声明(但函数代码在另一个库函数文件中)

2.ANSI C函数原型

函数原型可以放在主函数内声明处;如果把函数定义放在函数调用之前,那么函数原型就可省

函数原型指明了函数的很多信息,这些信息叫做该函数的签名

主调函数把他的参数存储在叫栈(存放基本变量类型和实际值)的临时存储区:如调用函数imin(3,4),3和4会被放在栈中,当imin开始执行时会从栈中读取3和4

float类型作为被参数传递时会被自动升级为double类型

函数声明的形式:

但函数原型并没有实际创建变量,如char仅代表了一个char类型的变量
方式一:√
int imax(int,int);
方式二:
int imax(int a,int b);//变量名是假名,不必与函数定义的形式参数名一致

如果类型不匹配,编译器会把实际参数的转换类型转化为形式参数的类型.注意一个是实际参数和形式参数类型的矛盾,一个是返回值和函数类型的矛盾

3.递归

定义:C允许函数调用它自己,这种过程称为递归。

特性:更简洁。1.但是每次递归都会创建一组变量(每级递归都有自己的变量),所以占用内存大,每次递归调用都会把一组新变量放在栈中,递归调用的数量受限于内存空间。2.每次函数调用要花费一定时间,所以执行速度慢于循环

尾递归:把递归调用置于函数末尾,即正好在return语句之前。最简单的递归形式

//阶乘计算函数
//虽然递归调用不是在最后一行,但n>1时,它是函数执行的最后一条语句,所以也尾递归
int rfact(int n)
{
    int num;

    if (n > 1)
        num = n * rfact(n - 1);
    else
        num = 1;

    return num;
}

//十进制转换为二进制:还记的十进制怎么→二进制计算方法吗
void Bconversion(int num)
{
	int r = num % 2;

	if (num >= 2)
		Bconversion(num/2);//递归括号里一般是这样子的
	putchar(r == 0 ? '0':'1');//注意是从左向右打印

	return ;

}

调用一般只出现一次,且多为括号里一次次变小

所有C函数皆平等:可以互相调用,但main()有些特殊,main是函数中最先开始执行的。虽然可以,但我们很少在main内部或其他函数中调用main

4 编译多源代码文件的程序

4.1 UNIX、Linux、DOS命令行编译器、Windows和苹果的IDE编辑器(略)

4.2 使用头文件

  • 把main放在第一个文件中,把函数定义放在第二个文件中,那么第一个文件仍然要使用函数原型。但把函数原型放在头文件中,就不用每次使用函数文件时都去屑函数的原型。C标准库就是这么做的(I/O函数原型放在stdio.h中,数学函数原型放在math.h中),我们也可以用自定义的函数文件

  • 另外程序经常使用C预处理器定义符号常量(明示常量),一个好的做法是把#define指令也放进头文件,然后在每个源文件中使用#include指令包含该文件即可

综上,我们可以把函数原型和#define都放进头文件,这是一个好习惯

5.查找地址:一元&运算符

(学习指针先从学习&运算符开始)

&变量名:表示变量的地址(地址通常16进制表示)

//时刻记住,只有值或地址能进出被调函数,变量不会
#include<stdio.h>
void mikado(int);
int main(void)
{
 int a = 2, b = 3;

 printf("In main,a = %d and &a = %d\n", a, &a);
 printf("In main,b = %d and &b = %d\n", b, &b);
 mikado(a);//只是把2这个值给了被调函数的一个变量b

 return 0;
}

void mikado(int b)
{
 int a = 4;

 printf("In mikada,a = %d and &a = %d\n", a, &a);
 printf("In mikada,b = %d and &b = %d\n", b, &b);

 return;
}

//地址+1相当于+1byte

6.更改主调函数中的变量

现尝试通过调用一个交换函数来交换主函数的x和y

//发现a、b并未交换值
#include<stdio.h>
void exchange(int,int);
int main(void)
{
 int a = 2, b = 3;

 exchange(a, b);
 printf("a = %d\n", a);
 printf("b = %d\n", b);

 return 0;
}

void exchange(int a,int b)
{
 int temp;

 temp = a;
 a = b;
 b = a;

 return;
}

会发现a和b并未发生变化,还是因为只有值能进出被调函数,变量并不会传递。我们可以让主调函数用被调函数return返回的值修改自己的值,比如return(u)、a = interchange(a,b),但return语句只能把被调函数中的一个值传回主调函数进行修改,这样只能修改一个a,b并未被修改。所以我们要尝试通过地址同时修改x和y,需要使用指针 (接下来进行学习)

7.指针简介

指针:值为 地址 的一个变量(或数据对象) int类型变量的值是整数,而指针变量的是地址。是变量就可以作为函数参数使用

//变量名 常量
ptr = &bah;

(既然指针是变量,想使用就要先声明,那么指针变量如何声明呢,通过*运算符)

7.1 间接运算符:*

*指针名(地址):表示地址对应的,又叫'解引用运算符

//以下两个模块效果是一样的,都是把bah的值赋给val
ptr = &指向bah;//这里已经假设ptr指向bah,即ptr是指针
val = *ptr;

val = bah;

(现在可以学习声明指针了)

7.2 声明指针

声明变量必须指明类型,同理声明指针类型时必须指定指针所指向变量的类型。因为不同变量类型占用空间不同,一些指针操作要求知道操作对象大小且程序需要知道存储在指定指针上的数据类型

声明时:pi是变量名,☆用来声明pi是指针 解引用变量时:*会找出pi指向的值

/*
一*和指针名之间的空格可有可无通常:声明时使用空格,解引用变量时省略空格
*/

//指针指向的类型 与 指针的类型
int * pi = &x;	//指针pi的类型是int *,描述为指向int类型的指针。pi是指向的类型是int类型
char * pc;	//pc是指向char类型变量的指针变量名
float *pf, *pg  //pf、pg是指向float类型变量的指针变量名

pi就是指针(变量(名))。指向的值是int类型(即*pi是int类型),而pi本身的类是:指向int类型的指针

  • 大部分系统中:指针里的地址由无符号整数表示,但不代表指针就是整数,一些整数操作不能用于指针操作(比如乘法),所以指针实际上是个新类型。ANSI C专门为指针提供了%p格式的转换说明

7.3使用指针在函数间通信

通过指针改变主调函数中的值

#include<stdio.h>
void exchange(int *,int *);
int main(void)
{
 int a = 2, b = 3;

 exchange(&a, &b);//指针的值就是地址,所以这里赋地址
 printf("a = %d\n", a);
 printf("b = %d\n", b);

 return 0;
}

void exchange(int * a,int * b)
{
 int temp;
 
 //注意这里的交换逻辑
 temp = *a;//a的值给temp
 *a = *b;//b的值给a
 *b = temp;//a的值给b

 return;
}

8.变量:名称、地址和值

变量的名称、地址、值之间关系密切。

  • 编写程序时:可认为变量有两个属性:名称和值(还有类型等)

  • 编译和加载程序后:可认为变量有两个属性:地址和值

  • 所以,可以说地址就是变量在计算机内部的名称!!!

十、数组和指针

逻辑:数组名(的值)实际上是首元素地址(或者说他的值是地址),所以数组实际上是指针,所以形参要传入数组的函数形参要是指针。因为数组名是指针,二者可以相互表示(但底层上肯定有区别,有待探索),所以形参也可以使用数组(名)的形式,这里要明白程序处理数组本质上还是用指针

处理数组的函数,必须要知道数组的开始和结尾,本章讲了三种方式。为别为形参只有数组名(或指针);形参有数组名和数组长度;形参是指针开始和结尾

1.数组

(数组和其他变量类似,可以把数组创建成不同的存储类别,以后会学。现在只需记住:本章的数组属于自动存储类型。即数组在函数内部声明,且声明未使用关键字static。目前为止本书所有变量和数组都是自动存储类别)

由一系列相同类型元素构成的有序序列,通过声明告诉编译器数组中有多少元素以及元素的类型。普通变量可以使用的类型数组元素都可以

float candy[365];  //内含365个float类型元素的数组
char code[12];  //内含12个char类型元素的数组
int states[50];  //内含50个int类型元素的数组

访问元素通过下标(索引)访问

1.1 初始化数组

#include<stdio.h>
#define MONTHS  12 //这里没有';'和'='!!!
int main(void)
{
	int days[MONTHS] = { 31,28,31,30,31,30,31,31,30,31,30,31};//MONTHS是已定义的一个整数,表示数组长度
	
	for (int index = 0; index < MONTHS; index++)
	{
		printf("Month %d has %d days\n", index+1, days[index]);
	}

	return 0;
}

初始化可能出现问题:

  • 未初始化:数组元素和未初始化的普通变量一样,存储的都是垃圾值

  • 数组元素个数>初始化列表项数:剩余元素被初始化为0

  • 数组元素个数<初始化列表项数:报错

当然我们当然可以省略方括号里的数字,让编译器自动匹配数组大小并初始化列表中的项数(但是这种自动计数的方式会让我们无法察觉初始化列表中的项数错误,下列程序漏了最后两个月就没有察觉)。如果创建一个随后填充的数组,就必须声明时指定大小

//让计算机计算数组大小(元素个数)
#include<stdio.h>
int main(void)
{
	int days[] = { 31,28,31,30,31,30,31,31,30,31};//[]里就不用再写了
	//sizeof经常衡量数组大小
    //index < sizeof days/sizeof days[0]:数组元素个数:整个数组大小除以单个元素大小(sizeof计算时均以字节为单位)
	for (int index = 0; index < sizeof days/sizeof days[0]; index++)
	{
		printf("Month %d has %d days\n", index+1, days[index]);
	}

	return 0;
}

1.2 指定初始化器(C99)

在传统的初始化语法中,若想初始化最后一个元素,必须初始化之前所有的元素才能初始化它。

//传统的语法
int arr[4] = {1,2,3,4};

C99增加了新特征:指定初始化器。该特性可初始化指定的数组原始。

  • //直接把最后一个元素初始化
    int arr[4] = {[3] = 4};//前面说过,其他未初始化的一般会为0
  • 要注意:1.如果指定初始化器后面有更多值,如[4] =31,30,31那么后面的值30、31将成为初始化指定元素后面的元素。2.如果再次初始化指定的值,那么会覆盖取代之前的初始化。总之记住从左向右去写元素就行

    #include<stdio.h>
    #define MONTHS 12
    int main(void)
    {	//	元素:31,29,0,0,31,30,31,0,0,0,0,0
    	int days[MONTHS] = { 31,28,[4] = 31,30,31,[1] = 29 };
    	return 0;
    }
    //days=[31,29,0,0,31,30,31,0,0,0,0,0]
  • 如果未指定元素大小:编译器会把数组的大小设置为足够装得下初始化的值

    //会有7个值
    int stuff[] = {1,[6] = 23};
    //会有9个值
    int staff[] = {1,[6] = 4,9,10};

1.3 给数组元素赋值(初始化也是一种赋值)

注意:

  • C不允许把数组作为一个单元赋给另一个数组

  • 除初始化外不再允许用花括号列表的形式赋值

    #include<stido.h>
    #define SIZE 5
    int main(void)
    {
    	int oxen[SIZE] = {5,3,2,8}; //初始化
    	int yaks[SIZE];
    	
    	yaks = oxen; //不允许数组赋数组
    	yaks[SIZE] = oxen[SIZE] //数组下标越界
    	yaks[SIZE] = {5,3,2,8} //不起作用
    }

1.4 指定数组大小

常量:不变的值

整形常量:1。不变的整数类型的值

符号常量:用一个标识符来表示一个常量。

#define 标识符 常量
#define SIZE 4//SIZE就是符号常量,但它代表的是一个整数,所以这里的SIZE也属于整数(符号)常量

整型常量表达式:5、5*2+1、sizeof(int) + 1

我们之前所学的都是用整型常量来声明数组

#define SIZE 4
int main(void)
{
	int arr[SIZE];//整数符号常量
	double lots[144];//整数字面常量
	...

2. 多维数组

对于计算机来说,多维数组在内存中还是按照一维存储,多维数组只是人类逻辑上的概念

如何理解二维数组(数组的数组):float rain[5].[12];

  • 先看中间的float rain[5].[12]:表示rain是一个内含5个元素的数组

  • 再看剩余的float rain[5].[12]:表示每个元素是内涵12个float类型元素的数组

n维数组在计算机内部按顺序存储,从第1个内含12个元素的数组开始,然后就是第2个内含12个元素的数组

/* 二维数组做形参:为什么C要求形参声明必须给出第二个维度的大小,而不能是如下形式?

int sum(arr[][]);

因为从实参传递过来的时数组的起始地址,数组在内存中按行存放并不区分行和列,如果形参中不说明列数,则系统无法决定应为多少行多少列,所以必须指定第二维 */

示例:降雨量

#include<stdio.h>
#define YEARS 5
#define MONTHS 12
int main(void)
{
	int year, month;
	float total,subtot;
	const float rain[YEARS][MONTHS] =
	{
		{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
		{8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
		{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
		{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
		{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
	};

	puts("YEAR    RAINFULL    (inches)");//尽量把printf、puts写前面
	for (year = 0,total = 0; year < YEARS; year++)
	{
		for (month = 0,subtot = 0; month < MONTHS; month++)//初始化在每个循环中只执行一次。total只是一开始是0,循环开始后也不会再还原为0!
			subtot += rain[year][month];
		printf("%d    %12.1f\n", year+2010, subtot);//16是从 '第一个%d表示的字段宽度+两个转换说明之间的距离' 开始算
		total += subtot;
	}

	printf("\nThe yearly average is %.1f inches\n\n", total/YEARS);
	puts("MONTHLY AVERAGES:\n");
	puts("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");
	for (month = 0; month < MONTHS; month++)
	{
		for (year = 0, subtot = 0; year < YEARS; year++)
			subtot += rain[year][month];
		printf("%3.1f ", subtot / YEARS);
	}

	return 0;
}

2.1 初始化二维数组

前面讨论的数据个数和数组大小不匹配的问题同样适用于这里的每一

const float rain[YEARS][MONTHS] =
	{
		{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},//括号也可以省,但如果初始化值不够,则按照先后顺序初始化,最后没有初始化的则同理变成0
		{8.5,9.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
		{9.1,8.5,6.7,4.3,2.1,0.8,0.4,0.0,6.1,7.4,3.6,8.4},
		{0.4,0.0,6.1,7.4,3.6,8.4,9.1,8.5,6.7,4.3,2.1,9.1},
		{4.3,4.3,4.3,3.0,2.0,9.1,8.5,6.7,4.3,2.1,9.1,9.3},
	};

2.2 其他多维数组(略)

一维:一行数据 二维:数据表 三维:一叠数据表

也可以根据下标简单的理解

3.指针和数组(梦开始的地方)

指针的程序效率很高,所以计算机硬件指令非常依赖地址,尤其是指针能有效处理数组。数组表示法其实就是在变相使用指针

硬盘:存储数据的硬件。电脑游戏、操作系统、打字软件都存储与硬盘,使用时需要调入内存运行才能真正使用

内存:(临时)存放程序数据的硬件。程序执行前需要先放到内存才能被cpu(内有寄存器但容量太小)处理。有代码段、数据段等之分。运行结束后里面就没有数据了

程序从编写到内存:编写(n个)源代码.c→编译→(n个)目标文件→链接→装入(1个)模块→装入→内存

值传递、指针传递、引用传递

  • 值传递:最普通传递方式,不能导致外部变化。 sum(int a);

  • 指针(地址)传递:导致外部变化。 sum(int *a);

  • 引用传递:onlyC++,导致外部变化。 sum(int &a),这里&是引用而非取值,调用方式类似值调用,但可以导致外部变化

函数copy机制:函数被调用时,程序会为该函数的变量分配存储空间,然后把实参放进去,也就是新建副本。函数内部变量返回后就会销毁并收回内存

数组名是不是数组首元素地址(或指针常量)?:不是,根本不是同一种(派生)类型的实体!(数组对应内存一块内存,对于数组来说,数组名表示数组首地址,是右值,即数组名代表整个数组,值上代表一个地址常量)但可退化 (单向、失去其特有内涵) 为指针。例如把ar用在ar+1时,ar已经从1个数组隐式转变为了1个指针,而ar作为数组是不参与运算的!(理解这句话,这里的ar数组似乎有点逻辑上的意思)

数组何时退化为指针?:除了sizeof、&、以及初始化时的"字符串面量",其他时候都会退化为指针常量(但这个指针指向的是内存动态存储区的数组副本,直接用指针指向的是原本在静态存储区的数组) ——来源于CSDN

  • 详解:运算符作用于数组名时数组名会退化为指针常量(sizeof、&除外)

    sizeof(ar);//ar是数组
    sizeof(ar+0);//ar是指针
    &ar;//ar是数组
    &(ar+0);//ar是指针
        
    ar[0];//ar退化为指针,程序理解为*(ar+0)。对于ar[i]可理解ar退化成的指针常量为汇编的段地址,而下标i相当于偏移地址,就很好理解*(ar+i)了

    实例证明:

    char ch[] = "My name is Sam.";
    
    sizeof ar:ar是数组
    sizeof (ar + 0):ar是指针,本系统指针是4byte
    printf("sizeof ch = %d\nsizeof (ch+0) = %d", sizeof ar, sizeof(ar + 0));

  • 还要特别记得数组作为形参时的退化:C语言是值拷贝传递,对于数组,必须传递指针值,因为这样效率高(这样的地址传递还会修改原数组),如果函数按值传递数组,则函数运行时必须为形参分配一个存储空间来存储拷贝的原数组副本,效率很低。如此,C函数只通过指针处理函数,在函数体内部,一切的操作都是对于这个形参指针而和传入的指针常量没有任何关系,而这个指针形参并不是常量,可以自增等等,由于不能确定数组的大小,需要我们再传入一个参数或者再函数体中自行输入。另外常量不占用内存,通常放在常量区?

  • 数组名作为实参传入函数,也会退化为指针,把数组起始地址传给形参指针。傻呀,形参是指针,实参的值不就得是地址吗,实参不就得是指针吗!

指针+1的含义:C递增它所指向类型的大小(以字节为单位)

#include<stdio.h>
#define SIZE 4
int main(void)
{
	short index;

	short dates[SIZE];
	short * pti;

	double bills[SIZE];
	double * ptf;

	//把数组地址赋给指针
	pti = dates;
	ptf = bills;

	printf("%23s %15s/n", "short", "double");
	for (index = 0; index < SIZE; index++)
	{
		printf("pointers + %d:%10p % 10p\n", index, pti + index, ptf + index);
	}

	return 0;
}

//********************************************************结果
                short      double
               (递增两byte) (递增8byte)   
pointers + 0:  00CFFAE8   00CFFAB4
pointers + 1:  00CFFAEA   00CFFABC
pointers + 2:  00CFFAEC   00CFFAC4
pointers + 3:  00CFFAEE   00CFFACC

*C中指针+1值增加一个存储单元(8b、1字节?但是这样逻辑就不对了)。对数组而言,意味着地址是下一个元素的地址(以字节为单位)而非下一个在字节的地址,所以声明指针必须要指向对象类型

4.函数、数组和指针

示例:

  1. 现在要编写一个处理数组的函数sum,函数sum返回所有元素之和

    total = sum(marbles);//可能的函数调用
  2. 那么函数sum的函数原型里的形参应该是什么?前面提到过,C语言是值拷贝传递,对于数组,必须传递指针值,因为这样效率高(这样的地址传递还会修改原数组),如果函数按值传递数组,则函数运行时必须为形参分配一个存储空间来存储拷贝的原数组副本,效率很低。如此,C函数只通过指针处理函数。(用数组表示法也可以,这个数组会退化成指针 )

    //对应的函数原型,函数定义处的形参也一样
    int sum(int *);
    int sum(int []);
  3. 相应函数定义:

    //函数要处理数组必须知道何时开始、何时结束。由于不能确定数组的大小,需要我们告诉函数:在函数体中自行输入或再多传入一个长度参数或者传递两个针分别表明数组的开始与结束。
    //方式1:多传入一个长度参数。这个方法更好,因为可以让同一个函数处理不同大小的函数
    int sum(int* ar, int n)
    {
     	int i;
     	int total;
    
     	for (i = 0, total = 0; i < n; i++)
        //虽然没有定义数组ar,但ar[i]也能执行,估计还是被转化为了*(ar+i)?
      		total += *(ar+i);//形参是指针,这里也写指针,尽量保持格式一致
    
     return total;
    }
    
    
    //另一种方式:传递两个针分别表明数组的开始与结束
    //要注意end指向的位置实际上在数组末尾的下一个位置。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。不过C虽然保证marbles+SIZE有效,但是对marbles[SIZE]未做任何保证,所以程序不能访问该位置
    int sum1(int *start,int *end)
    {
     int total = 0;
    
     while (start < end)//地址可用于关系表达式作比较
      total += *(start++);//相当于total = total +start++,所以要先给total初始化
    
     return total;
    }
  4. 关于函数形参:虽然指针和数组可以相互表示,但只有在函数原型函数定义头中,才可以使用int ar[]代替int *ar(其实是说虽然数组是指针,但是指针用于很多地方,不能随便用数组代替)。或者说int ar[]和int *ar都表示ar是一个指向int的指针,但int ar[]只能用于声明形式参数。不过int ar[]可以提醒读者ar不仅是指针还是数组

    //函数原型中等效方式(不是等价是等效)
    int sum(int *ar,int n);
    int sum(int ar[],int n);
    int sum(int *,int);
    int sum(int [],int);
    //函数定义中等价方式(函数定义不可省略参数名,不然函数定义内部怎么知道定义的是谁)
    int sum(int *ar,int n);
    int sum(int ar[],int n);

示例:编写一个程序,打印原始数组大小和表示该数组的函数形参的大小

#include<stdio.h>
#define SIZE 10
int sum(int*, int);//不要忘记函数原型
int main(void)
{
 int ch[SIZE] = { 10,9,8,7,6,5,4,3,2,1 };

 sum(ch,SIZE);

 printf("The size of ch is %zd bytes.\n", sizeof ch);//数组大小为40

 return 0;
}

int sum(int *pi,int n)//int pi[]也一样,因为也退化为指针了,后面的操作都是指针操作,但是新参尽量和函数体里面形式一致
{
 int i;
 int total = 0;

 for (i = 0; i < n; i++)
  total += *(pi + i);

 printf("The size of ar is %zd bytes.\n",sizeof(pi));//pi这个变量是一个指针,本系统地址大小为4byte
 printf("The total number of marbles is %d.\n", total);

 return total;
}

4.1 指针表示法和数组表示法

处理数组的函数实际上使用的都是指针变量:编写这样的函数可以选择数组表示法或指针表示法,前者让处理对象是数组这一意图更加明显,但编译器会把数组表示法转换成指针表示法

至于C语言,虽然ar[i] 和 *(ar+i) 等效,但是ar作为数组退化为指针时是常量不可修改,只有当ar是指针时,才能使用ar++这样的表达式

5.指针操作

操作:

  1. 赋值:把地址赋给指针

  2. 解引用:给出指针指向地址上存储的值

  3. 取址:取指针本身的地址

  4. 指针与整数相加(减):不论相加(减)还是递增(减),整数都会和指针所指类型相乘再和地址相加,即给指针增加n,其值会增加对应类型大小的数值。例如int型指针加4表示指针加了16个byte(所以看起来是增加了4个元素)。所以指针的类型很重要,

  5. 递增(减)指针

  6. 指针求差(不能指针相加):通常求差的两个指针指向同一个数组的不同元素,通过求差求出两元素之间的距离。差值的单位与数组类型单位相同。比如ptr1-ptr2=2,表示两个指针指向的元素相隔两个2int,而不是2字节

  7. 比较:使用关系运算符比较两个指针(前提是指向相同类型)的值

注意:不要解引用未初始化得值,可能导致出错

  • //pt为被初始化,其值是一个随机值,所以不知道将5存于何处,这可能导致出大错。所以必须先初始化指针
    int * pt;
    *pt = 5; //严重错误

指针的一个用法是在函数间传递信息(在被调函数中修改主调函数变量),另一个用法是数组操作

有效与无效语句:

int urn[3];//这里urn到底是指针还是数组,应该是一个数组,但我觉得不应该这么绝对吧,有些底层的东西,不去学就会很难理解
int * ptr1, * ptr2;//ptr表示指针
//有效语句			无效语句
ptr1++;				urn++;
ptr2 = ptr1 + 2;	ptr2 = ptr2 + ptr1;//指针不可相加
ptr2 = urn + 1;		ptr2 = urn * ptr1;

6.保护数组中的数据

C语言是值拷贝传递,对于数组,必须传递指针值,因为这样效率高,并且这样的地址传递会修改原数组,很多时候也恰好需要修改原数组,但如果恰好不想修改该怎么保护数组呢?

6.1 对形参使用const

建议对形参使用关键字const:向形参传递指针会导致一些问题,可能本意不想修改却意外修改了原数组。可以在函数原型函数定义中声明形式参数时使用关键字const。const会告诉编译器,该函数不能修改ar指向的数组中的内容,让编译器在 处理数组时 将其视为常量(不是要求原数组是常量)

同时,由于(后面所讲的)数组声明和指针声明时的差异,最好不要用指针指向字符串面量

//函数原型
int sum(const int ar[],int n);

//函数定义
int sum(const int ar[],int n)
{
	...
}

6.2 const的其他内容(略)

double rates[3] = {11.1,12.2,13.3};
//下面的代码把pd指向的double类型的值声明为const,表示不能用pd来更改它所指向的值
const double * pd = rates;

c 语言 arr[i] 中 的中括号 [] 不是标点符号,而是 地址 “运算符”。prt[i] == *(ptr+i) == arr[i]

double rates[5] = {2.1,1.2,3.4,5.1};
double * pd = rates;//pd是指向数组的首元素

//这是可以的,pd[2]
pd[2] = 1.22//修改了数组的1.2

略......

7.指针和多维数组(有待好好学习)

处理多维数组要用到指针(牢记数组名是首元素地址)

//内含int数组的 数组
int zippo [4][2];
  • zippo是数组,所以是数组首元素(含有两个int整数的数组)地址。即zippo == &zippo[0] zippo[0]是数组,所以是数组首元素(一个整数)地址。即zippo[0] == &zippo[0][0】 即zippo是一个占用2个int大小对象的地址,zippo[0】是一个占用1个int大小对象的地址

  • 给指针或地址加1,值会增加对应类型大小的数值。这方面zippo+1与zippo[0]+1不同

  • zippo是地址的地址,解引用zippo才可以获得初始值

可以发现这部分非常抽象,少个程序去练习有机会补上,所以一个程序恰巧使用指向二维数组的指针并要获取该值时,最好用数组表示法而非指针表示法

1. 指向多维数组的指针

如何声明一个指针变量pz指向一个二维数组:假设有二维数组zippo[4].[2],zippo是首元素地址,该元首元素是一个内含连个int类型的一维数组。所以指针必须指向一个内含两个int类型的的数组

//pz指向一个内含两个int类型值的数组
int (* pz)[2];

//pz是一个内含两个指针元素的数组,每个指针元素都指向int类型
int * pz[2];//*优先级高于[]!!
int zippo[4][2] = {{2,4}.{6,8}.{1,3},{5,7}}
int (*pz)[2]; //int pz[][2]也可以
pz = zippo;

pz虽然是一个指针不是数组名,但是同一维数组那里一样也可以使用pz[2][1】这样的写法。可以用数组表示法或指针表示法来表示一个数组

2. 指针兼容性

指针间的赋值比数值类型之间更严格。int可以直接赋给double,而不同类型的指针则不能(一个指向内含两个Int值的数组,一个指向int类型也是不同类型)

一般把const指针赋给非const指针会不安全

略....

3. 函数和多维数组(略)

略......

8.VLA

我们之前接触的都是普通数组即静态数组,采用静态内存分配——数组长度固定。静态数组定义后系统会为其分配对应长度的内存空间。但是不同的数组需要的空间是不一样的,为了适用所有数组,我们一般会给数组设置一个较大的长度值,这样满足了一般的运行需求,但极大的浪费了内存空间。于是引出了动态数组的概念。"动态"体现在 数组长度可以由用户自己定义 上。

很多人在编写C语言代码的时候很少使用动态数组,不管什么情况下通通使用静态数组的方法来解决,在当初学习C语言的时候我就是一个典型的例子,但是现在发现这是一个相当不好的习惯,甚至可能导致编写的程序出现一些致命的错误。尤其对于搞嵌入式的人来所,嵌入式系统的内存是宝贵的,内存是否高效率的使用往往意味着嵌入式设备是否高质量和高性能,所以高效的使用内存对我们来说是很重要的。那么我们在自己编写C语言代码的时候就应该学会使用动态数组。动态数组有两种实现方式:(实现方式本质上是用动态分配内存,分配的是动态内存)

方式1:C99变长数组

C不允许用变量表示数组长度。而C99新增变长数组VLA,允许用变量表示数组维度。变长数组必须是声明在块中的自动存储类别,所以不能使用static和extern。不能在声明中初始化,所以C11把变长数组作为可选特性,不必强制实现

int quarters = 4;//用x、y代替的话,虽然看起来简单但是回看却不好理解编写的含义和目的
int regions = 5;
double sales[regions][quarters];

注意:'变'不是可以修改以创建数组的大小,数组一旦创建,大小则保持不变。'变'指的是:创建数组时,可以用变量指定数组维度(这个维度指一维和二维方括号里的值)。 并且,函数定义中的形参列表中的VLA并未实际创建数组,VLA名做形参也是个指针。所以待变长数组形参的函数实际上是在原始数组中处理数组

前两个形参用作第3个形参的两个维度,因为ar的声明必须使用rows和cols,所以形参列表必须在声明ar之前先声明这两个参数

//新的sum2d()可以处理任意大小的二维int数组
int sum2d(int rows,int cols int ar[rows][cols]);//rows和cols是变量不是符号常量
int sum2d(int ,int ,int ar[*][*])//省略原型参数名时必须用*代替数组维度

const和数组大小:是否可以在声明数组时使用const变量?

//不允许声明数组时使用const变量
const int SZ = 80;
...
double ar[SZ];//不允许

C90不可能允许:const声明的是变量。数组大小必须是给定的整型常量表达式,如20、sizeof或其他不是const的内容。C实现可以扩大整型常量表达式的范围,所以可能会允许使用const,但可能无法移植

C99/11允许声明变长数组时使用const变量。所以该数组定义必须时声明在块中的自动存储类别数组???

//但是这样时可以的,这样表明把数组设置为只读
const int days[MONTHS] = {31,28,31,.....};

方式2:内存管理函数

即在编译时确定数组大小C语言提供了一系列的内存管理函数来帮助我们来按需要动态的分配和回收内存空间。这恰恰就是动态数组另一种实现方式的基础,我们可以利用内存管理中的内存申请和释放函数,在程序的运行过程中进行数组长度的指定。第十二章会介绍

9.复合自变量(略)

10.小结

大部分数组操作,都是把数组当作指针

数组建立在其他类型的基础上,所以C把数组看做派生类型

数组名作为实参时,传递给函数的不是整个数组而是数组的地址。有两种方法,第二种更普遍,因为可以让一个函数处理不同大小的数组

数组和指针关系密切,既可以用数组表示法也可以指针表示法,但实际上都是指针表示法

本章着重介绍了int类型数组,其他类型数组也一样,不过字符串末尾有空字符导致其有些特殊。有了空字符,函数可以通过检测字符串末尾来知道何时停止,就不用传入数组大小了

11.课后题

牢记数组操作基本上都是指针操作

T1

1.修改10.7(降雨量那个),改为用指针计算(因为不想修改,仍然要声明并初始化数组)

分析:要修改的只有涉及到数组的两行,分别在两个内层for循环中

#include<stdio.h>
#define YEARS 5
#define MONTHS 12
int main(void)
{
	int year, month;
	float total,subtot;
	const float rain[YEARS][MONTHS] =
	{
		{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
		{8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
		{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
		{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
		{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
	};

	puts("YEAR    RAINFULL    (inches)");
	for (year = 0,total = 0; year < YEARS; year++)
	{
		for (month = 0,subtot = 0; month < MONTHS; month++)//初始化在每个循环中只执行一次。total只是一开始是0,循环开始后也不会再还原为0!
			subtot += *(*(rain+year)+month);
		printf("%d    %12.1f\n", year+2010, subtot);//16是从 '第一个%d表示的字段宽度+两个转换说明之间的距离' 开始算
		total += subtot;
	}

	printf("\nThe yearly average is %.1f inches\n\n", total/YEARS);
	puts("MONTHLY AVERAGES:\n");
	puts("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec");
	for (month = 0; month < MONTHS; month++)
	{
		for (year = 0, subtot = 0; year < YEARS; year++)
			subtot +=  *(*(rain + year)+month) ;
		printf("%3.1f ", subtot / YEARS);
	}

	return 0;
}

T2

2.把1个数组的内容拷贝至另外3个数组,分别采取不同的形式

#include<stdio.h>
void copy_arr(double target[], double source[], int n);
void copy_ptr(double* target, double* source, int n);
void copy_ptrs(double* target, double* source_begin, double* source_end);
#define YEARS 5
#define MONTHS 12
int main(void)
{
	double source[5] = { 1.1,2.2,3.3,4.4,5.5 };
	double target1[5];
	double target2[5];
	double target3[5];

	puts("target1:");
	copy_arr(target1, source, 5);
	for (int i = 0; i < 5; i++)	//最常用的打印数组元素方法
		printf("%.1f ", target1[i]);
	puts("");//换行

	puts("target2:");
	copy_ptr(target2, source, 5);
	for (int i = 0; i < 5; i++)
		printf("%.1f ", target2[i]);
	puts("");

	puts("target3:");
	copy_ptrs(target3, source, source + 5);
	for (int i = 0; i < 5; i++)
		printf("%.1f ", target3[i]);

	return 0;
}

//三个函数的形参可以用一样的名字,别拘束
void copy_arr(double target[], double source[], int n)
{
	for (int i = 0; i < n; i++)//这样就行
		target[i] = source[i];
}

void copy_ptr(double * target, double * source, int n)
{
	for (int i = 0; i < n; i++)
		*(target + i) = *(source + i);
}

//重点!!!
void copy_ptrs(double* target, double* source_begin, double* source_end)
{
	for (double* i = source_begin; i < source_end; i++)//地址可以比较大小
		*target++ = *i;//好好学
}

T3

3.编写一个程序,返回存储在int类型数组中的最大值,并在一个简单程序中测试该函数

//排序用算法,这只是个找最值,用排序大题小用
int max(int* arr, int num)//变量名尽量能代表含义
{
	int temp = arr[0];
	
	for (int i = 0; i < num - 1; i++)
	{
        //这两句就是求最值关键句
		if (temp < arr[i + 1])
			temp = arr[i + 1];
	}
	return temp;
}

T4

编写一个函数,返回存储在double类型数组中最大值的下标,并在一个简单程序中测试该函数

int max_index(double* arr, int num)
{
	double temp = arr[0];
	int max_index;
	
	for (int i = 0; i < num - 1; i++)
	{
		if (temp < arr[i + 1])
		{
			temp = arr[i + 1];
			max_index = i + 1;
		}
	}
	return max_index;
}

T5

编写一个函数,返回存储在double类型的最大值和最小值的差值,并在一个简单程序中测试

double minus(double* arr, int num)
{
	double max = arr[0];//相当于*(arr+0),正好传入的arr就是指针
	double min =arr[0];
	//double temp = arr[0];

	for (int i = 0; i < num; i++)
	{
        //没想到把两个求最值关键句合起来就行了
		if (max < arr[i])
			max = arr[i];
		if (min > arr[i])
			min = arr[i];
	}
	double result = max - min;

	return result;
}

十一、字符串和字符串函数

1.表示字符串和字符串I/O

1.字符串:以空字符(\0)结尾的char类型数组。所以有些东西和数组很像

使用多种方法定义字符串:字符串常量、char类型数组、指向char类型的指针,也可以用标准的数组初始化形式但十分繁琐(不赘述)

#include<stdio.h>
//字符串常量
#define MSG "I am a man"
#define MAXLENGTH 81
int main(void)
{
    //char类型数组
    char words[MAXLENGTH] = "I am a string in an array.";//独特的字符串声明方式,没有{}符号
    //指向char类型的指针
    const char* pt1 = "Something is pointing at me.";//这一句仍然是重点
    puts("他说:\"他是帅哥\"");//在字符串内部使用双引号,必须在内部的双引号前面加一个\,之前还见过"\'"表示单引号
    puts(MSG);
    puts(words);
    puts(pt1);
    words[8] = 'p';
    puts(words);

    return 0;
}

puts和printf一样属于stdio.h系列的输入输出函数。puts只显示字符串,且自动在显示的字符串末尾加上换行符

1.1 在程序中定义字符串

1.1.1 字符串字面量

字符串字面量:双引号括起来的内容,也叫字符串常量。双引号中的字符和编译器自动加入末尾的\0字符(空字符)都作为字符串存储在内存中

字符串面属于(内存中)静态存储类别,这说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序生命周期内存在,即使函数被多次调用

字符串常量的串联特性:如果字符串字面量之间没有间隔间隔或用空白字符分割,对于C来说相当于没隔(前排拆分printf整洁格式的部分做法就是这个原理)

char str[50] = "I""am" "Sam";

char str[50] = "I am"
    "Sam";

char str[50] = "I am Sam";

关于"双引号括起来的内容被视为指向字符串(首元素)存储位置的指针,类似于把数组名作为指向该数组位置的指针":现在知道这句话是不对的,因为字符串属于数组一种,所以按照上一章数组理解即可

  • printf("%s, %p, %c","We","are",*"space farers");//注意最后一个输出是s

1.1.2 字符串数组和初始化

声明(字符)数组:必须让编译器知道需要多少空间。

  1. 常用方法是用足够空间的数组存储字符串

    const char m1[40] = "Limit yourself to one line's worth";//const表明不会更改这个字符串

    用数组初始化形式会很麻烦

    //这种形式必须在最后加上'\0',不然这就是一个char数组而非字符串
    const char m1[40] = {'a','b','c','d','e','f','g','\0'};

    在指定数组大小时,数组的元素个数至少比字符串长度多1以容纳空字符(\0),所有未被使用的元素会被自动初始化为0(0指char形式的空字符/0而不是数字字符0)

  1. 前面学过数组在初始化中可以省略声明数组的大小,编译器会自动计算数组大小。让编译器计算大小只能用于初始化,如果创建一个随后填充的数组,就必须声明时指定大小

    const char m1[] = "hhhhhhhh";

也可以用指针表示法创建字符串数组。尽管如此,这两种形式并不完全相同

//char类型数组
char words[MAXLENGTH] = "I am a string in an array.";
//指向char类型的指针
const char* pt1 = "Something is pointing at me.";

1.1.3 数组和指针(重点理解部分)

数组形式和指针形式有何不同:

  • 数组形式ar[]:字符串作为可执行文件一部分存储在数据段(数据段是可执行文件的一部分)。当把程序载入内存时,可执行文件的数据段中的字符串被载入存储区的静态存储器(前面提到过),但程序运行时才会为数组ar[]分配动态内存,将字符串拷贝到数组中,每个元素初始化为对应的元素内容。注意此时字符串有两个副本,一个是在静态内存中的字符串字面量,一个是存储在ar1数组中的字符串。(这里我理解为程序运行也有顺序,先放字符串再分配动态内存) 然后编译器把数字名ar识别为该数组首元素地址(&ar[0])的别名,这里关键要理解,数组形式中,数组名(ar)可隐式退化为地址常量!!!不可修改,所以不能++ar,因为递增运算符只可用于变量名前(可修改的左值)而非常量,改了就意味改变了数组的存储位置,可以ar+1标识下一个元素(前面说过把ar放在ar+1中时,ar已经从1个数组隐式转变为了1个指针,而ar作为数组是不参与运算的!)

  • 指针形式*pt:也使编译器为字符串在静态存储区预留29个元素空间,程序执行时,会为指针变量pt留出一个存储位并把字符串地址存储在其中,指向字符串首字符,pt是变量,所以可用递增运算符

  • 总之,初始化数组把 静态存储区里的字符串 拷贝一个副本到 存储在动态存储区域的数组,而初始化指针只把在 内存静态存储区的字符串的地址 拷贝给存储在动态存储区域的指针

  • 示例:①可以看到只有ar的地址不同 ,说明数组形式和指针形式指向的不同的地方,一个是静态区一个是动态区 ②虽然字符串面量"My name is Sam."在printf中出现2次,但编译器可以把完全相同的字面量都存储在一个地方 (另一个编译器可能在不同位置存储3个"My name is Sam.") ③静态数据使用的内存与ar使用的动态内存很不同

    #include<stdio.h>
    #define MSG "My name is Sam."//字符串字面量存储在可执行文件的数据段中,运行后字符串放在静态存储区
    int main(void)
    {
     char ar[] = MSG;//运行后为ar在存储区开辟一块区域存放字符串
     char* pt = MSG;//运行后给pt开辟一个区域存储字符串地址
    
     printf("               adress of ar: %p\n", ar);
     printf("adress of \"My name is Sam.\": %p\n", "My name is Sam.");
     printf("               adress of pt: %p\n", pt);
     printf("adress of \"My name is Sam.\": %p\n", "My name is Sam.");
    
     return 0;
    }

1.1.4 数组和指针的区别(重点理解部分)

初始化字符数组存储内容和初始化指针指向内容有何区别:

char heart[] = "I like young girl!";
char *head = "I like young girl";

主要区别:如1.1.3所说,数组名heart是常量,指针名head是变量

相同点:①都可以使用数组表示法 heart[i] == head[i]。(深层次理解等价,指针数组化操作) ②都能进行指针加法操作 *(heart+i) == *(head+i),但是只有指针可以递增。(数组指针化操作)

③如果想让head和heart统一,可以:heart = head;这使得head指针指向heart数组首元素(从指向静态到指向动态,这样操 作后静态地址的"I love young girl"就无法再访问了)。但是不可以反过来,因为变量不可赋值给常量

④可以用改变数组中元素的信息:heart[7] = 'M' 或 *(heart+7) = 'M'。注意所谓数组中元素的信息是指存储在动态存储区域 的字符串面量,这里只是讲修改数组中的元素而不是使用指针修改数组中的元素,至于原因在下一条 不同点:最好不要用指针修改数组元素:head[i] = 'M' 或 *(head+i) = 'M'。因为编译器可以使用内存中的一个副本表示所有完全相同 的字符串面量,所做的修改会影响所有使用该字符串的代码(实验室电脑直接权限不允许

char * ch = MSG;

*(ch + 0) = 'm';
printf("My name is Sam.");
/*会显示my name is Sam

建议(这里和前面对形参使用const联系起来)把指针初始化为字符串面量(或普通数组)时,使用const限定符。不过非const初 始化为字符串字面量却不会出现类似问题,因为数组获得的是原始字符串的副本。总之如果打算修改字符串,就不要用指针指向 字符串字面量(这里和前面保护数组小节搭配学)

const char * pt = "My name is Sam."

1.15 字符串数组(略)

???二维和二维指针不是很懂,暂且略过

const char *myar[5] = //指向字符串的指针数组
{
	"h","hh","hhh","hhhh","hhhhh"
};

char yourar[5][40]	//char类型数组
{
	"o","oo","ooo","oooo","ooooo"
}

2.键盘输入常识

'\r'是回车,前者使光标到行首,(carriage return) '\n'是换行,后者使光标下移一格,(line feed)

\r 是回车,return \n 是换行,newline

Windows系统里面,键盘上的回车键实际上是先后输入了'\r'+'\n'两个字符,所以每行结尾是“<回车><换行>”,即“\r\n”

C语言在向计算机输入文本文件时,将回车符换行符转换为换行符,在输出时把换行符转换成回车和换行两个字符

C一些函数(如gets)不读取换行符而是留在缓冲区,但输入回车键本身会使输入行换行(目前)

3.字符串输入

本章学的是函数都是针对字符串,包括scanf、printf。字符串输入函数本质上都是接收一个指针

已学过的组合:scaanf-printf getchar-putchar gets-puts fgets-fputs

0.安全函数、不安全函数

safe function: scanf()、gets()、fgets()、strcpy()、strcat() 等都是C语言自带函数,都是标准函数,但是都有一个缺陷,就是不安全,可能会导致数组溢出或者缓冲区溢出,让黑客有可乘之机,从而发起“缓冲区溢出”攻击。

scanf_s()、gets_s()、fgets_s()、strcpy_s()、strcat_s() 是微软自己发明的安全函数,它们仅适用于 VS,在其它编译器下无效。这些安全函数在读取或操作字符串时要求指明长度,这样一来,过多的字符就会被过滤掉,避免了数组或者缓冲区溢出

目前没有学到如何避免缓冲区溢出,甚至不知道缓冲区有多大,但是肯定会满足大部分的输入长度,所谓安全函数不过是多输入了一个用于限制读取长度的参数来有效避免数组溢出(数组应该会小于缓冲区大小,所以也有效避免了缓冲区溢出?)

1. 分配空间

必须先预留存储空间-才能读入字符串。

不要乱用未初始化的指针(事实上我都看不懂这句话想干啥)

char *name;//这样的声明只会给指针分配存储空间
scanf("%s",name);

我们可以在声明时显示指明数组大小

char name[40];//这样的声明会给40byte大小的内存空间
scanf("%s",name);

2. gets(×)

gets(ch):

  • 不读取换行符。缓冲区会留下'\n'

  • 读取整行输入直至遇见换行符就结束

  • ,然后丢弃换行符存储器其余字符,并在字符末尾添加一个空字符使其成为一个C字符串

  • 返回值:gets返回指向ch的指针。正常情况下返回的地址与传入的第1个参数相同。但如果读到文件末尾将返回特殊指针:空指针NULL

示例:

gets(ch);

//下面两个等价
puts(ch);//添加换行符
printf("%s\n", ch);//不添加换行符

数组做形参只是指针,所以get只知道数组开始不知结束,输入字符串过长会导致缓冲区溢出。所以get已被废弃,后面介绍一些替代字符串输入函数

3. fgets

fgets(ch,n,stdin):gets的替代(针对文件的定制版,不是特别好用)

  • 读取换行符

  • fgets第2个参数限制字符读入最大量防止数组溢出。该值如果是n,那么fgets将读入n-1个字符(要留1个给空字符),或直至遇见第一个换行符为止(注意缓冲区是是...\n\0,即换行符作为字符串一部分而不是在空字符后面

  • fgets第3个参数指明要读入的文件,若读入键盘输入数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中

  • 返回值:fgets返回指向char的指针。正常情况下返回的地址与传入的第1个参数相同。但如果读到文件末尾将返回特殊指针:空指针NULL

示例1:

  • #include<stdio.h>
    #define STLEN 10
    int main(void)
    {
     char words[STLEN];
    
     puts("Enter strings (empty line to quit):");
     while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n')//因为NULL值为0,所以可用数字0代替,但实际第一个条件可以只写fgets(words,STLEN,stdin)
      fputs(words, stdout);
     puts("Done.");
    
    
     return 0;
    }
    /*
    程序分析: fgets开启输入输入,发现缓冲区没有东西,fgets便等待键盘输入到输入缓冲区,fgets只读取输入缓冲区的前9个(此时输入缓冲区前9个就取出了、没了)到数组并按.........\0格式存储到数组,摁下回车后将其发送到fputs函数(fputs输出到输出缓冲区,缓冲区的东西再发送到屏幕上)。进入下一轮循环,fgets继续从输入缓冲区读取一行剩余字符并存储为...\n\0到数组然后发送到fgets函数
    */
  • 如果不想要fget读取的换行符该怎么办,针对两种情况

    • 在已存储的字符串中查找字符串并替换为空字符

      //但这样好像只能找到第一个换行符,后面的就找不到可
      while(words[i] == '\n')
      {
      	words[i] = '\0';
      	i++;
      }

    • 如果有目标数组装不下一整行输入,一个可行办法是丢弃多出字符

      while(getchar != '\n')//这里读取但不存储输入,包括'\n'
      	continue;
  • 示例1++:

    #include <stdio.h>
    #define SIZE 10
    int main(void)
    {
    	char words[SIZE];
    	int i;
    
    	puts("Enter strings (empty line to quit):");//用printf还得添加'\n'
    	while (fgets(words, SIZE, stdin) != NULL && words[0] != '\n')
    	{
    		i = 0;//注意i的赋值放在这里和初始化在执行结果上是有区别的
    		while (words[i] != '\n' && words[i] != '\0')//目标是数组里的结尾'\0'之前字符
    			i++;
    		if (words[i] == '\n')
    			words[i] = '\0';
    		else//word[i] == '\0'执行,说明此时已经到达了字符串最后的'\0',现在要舍弃输入行的剩余字符
    			while (getchar() != '\n')//这里说明对于输入函数,缓冲区都是用的一个?
    				continue;
    		puts(words);//不用fputs是因为前面的回车都被换成'\0'了,另起一行输入需要puts添加换行符
    	}
    	puts("Done.");//不是fputs哦
    	return 0;
    
    }

空字符和空指针:

  • 空字符'\0':整数(char)类型标记字符串末尾,ascii码为0(其他字符ascii码不可能为0)

  • 空指针NULL:指针类型,有一个值为0,通常不会与任何有效数据的地址对应,通常函数使用它返回一个有效地址表示遇到文件末尾或未能按照预期执行

  • 虽然二者都可用数值0表示,但是从概念挥着占用内存上都不相同

注意:

  • 空格字符 ≠ 空字符。 空格字符''的ascii是32,是整数类型,是字符 空字符'\0'的ascii是0 空指针'\0'的ascii是0 ,是指针 ,是地址,(又叫NULL) 正常文本输入不会有空字符,所以空字符经常用来判断是否到达文本结尾

  • NULL是对象的,值为0,虽然和0的值一样但是NULL用于指针和对象,0用于数值(空字符'\0'的值也是0,但它既不是指针也不是数值,一看就知道是字符串结尾)

  • NULL有什么作用:1.C语言的精髓是指针,指针是可以指向内存地址,程序员可以直接读写内存,好处是效率高,坏处是如果访问的内存是不该访问的内存地址,就会造成段错误或者非法修改数据导致程序运行异常。所以在C语言中,对指针变量的引用要谨慎。 2.当我们定义一个局部指针变量时,因为临时变量都是从栈申请的,变量的初值都是随机的,导致刚申请的指针变量指向的内存地址是不可预知的。。如果我们定义了一个变量而没有去初始化,直接解引用,会导致段错误或者修改了不该访问的内存地址处的数据。导致段错误已经是非常好的情况的,会生成core文件,比较容易发现;最怕的是程序没有报错,而是指针刚好指向了一个可以访问的内存地址,通过这个指针变量把一些不该修改的数据给改掉了。 3.简单来说,NULL的作用就是当一个指针变量没有被显性初始化时,将该指针变量的值赋为NULL。解引用前判断指针变量是否为NULL,如果为NULL说明该指针没有初始化过,不可解引用

  • 为什么要将未初始化的指针变量指向NULL? 1.首先解引用一个指向未知地址的指针变量是很危险的,由此我们需要判断一个指针变量是否已经被初始化。于是行形成一个规定,定义一个指针变量时就将其赋值为NULL,只要我们判断一个指针是NULL,就是未初始化不可解引用的。这样可以防止我们错误的解引用指针。 2.NULL是一个特殊的地址,在操作系统中定义该地址是不可以访问的,我猜测NULL也就是0地址是操作系统内核的地址,用户是不可访问的。一旦我们去解引用NULL地址,就会报段错误。报错误这已经是非常好的结果了,可以让我们及时的发现错误。

  • 空指针或NULL有一个值,该值不会与任何数据的有效地址对于,通常函数使用它安徽一个有效地址表示某些特殊情况发生,比如遇到文件结尾或未能按照预期执行

4.gets_s(略)

5.s_gets

将上一个示例转化为一个函数模块:读取整行输入并用空字符代替换行符,或读取部分输入并丢弃剩余部分

//模仿fgets函数,fgets函数返回指向原数组的指针
char * s_gets(char* st, int n)//这个函数要修改字符串,所以不加const
{
	char * ret_val;
	int i = 0;//因为不是while,所以下面的赋值也可以变为这里的初始化

	ret_val = fgets(st, n, stdin);//所以fgets的第一个参数实际上应该是指针;
	if (ret_val)//我认为不该有这一句?
	{
		while (st[i] != '\n' && st[i] != '\0')//这里绝对绝对要写成'\n'而不是'\n ',C要求很严格的!
			i++;
		if (st[i] == '\n')
			st[i] = '\0';
		else
			while (getchar() != '\n')//任何字符的ascii码都不是0,字符0的ascii是48
				continue;
	}
	return ret_val;
}

5.scanf(单词获取函数)

不读取换行符 scanf和gets、fgets区别在于如何确定字符串末尾:如果预留存储区装得下输入行scanf更像获取单词函数

  • (gets、fgets会读取第1个'\n'之前的所有字符

  • scanf有两种方式:但都是从第1个非空白字符作为字符串开始

    • 如果使用%s转换说明,以下一个空白字符(空行、空格、制表符、换行符)作为字符串结束

    • 如果指定字段宽度%ns,那么scanf将读取n个字符或读到第1个空白字符停止

    • 前面介绍过scanf返回一个整数值,该值为成功读取的项数或EOF(-1)

示例:会发现输入超过n的限制就不读了

#include<stdio.h>
int main(void)
{
	char name1[11], name2[11];
	int count;

	printf("Please enter 2 names.\n");
	count = scanf("%5s %10s", name1, name2);//不仅是n的限制,函数写入数组的字符串长度也不能超过数组本身大小,不然容易崩溃
	printf("I read the %d names %s and %s.\n", count, name1, name2);

	return 0;
}

4.字符串输出

C有三个标准库函数用于打印字符串:puts、fputs、printf

1.puts

puts简单:puts(ch)、puts("Pls enter...")(注意是双引号区分与puts('ch'))。容易溢出,不安全

  • 添加换行符

  • 只需把字符串地址作为参数传递给它,遇到空字符'\0'停止(而字符串最后一个就是空字符),不是scanf那样遇到空格停止!

示例:

#include<stdio.h>
#define DEF "I am a#defined string."
int main(void)
{
	char str1[80] = "An array was initialized to me";
	const char* str2 = "A point was initialized to me";
	
	puts("I am an argument to puts().");
	puts(DEF);
	puts(str1);
	puts(str2);
	puts(&str1[5]);//括号内表示第6个元素r,puts从这里开始输出
	puts(str2+4);//括号内指向'i'的存储单元,puts从这里开始输出

	return 0;
}

错误示例:dont不是字符串,puts会一直读下去直到找到空字符。而编译器会把side_a数组放存储到dont数组之后。所以puts一直 输出 到side_a中的空字符。通常内存中有很多空字符,但是这样很不靠谱

#include<stdio.h>
int main(void)
{
	char side_a[] = "Side A";
	char dont[] = { 'W','O','W','!' };
	char sode_b[] = "Side B";

	puts(dont);

	return 0;
}

2.fputs

fputs(words,stdout):是puts的针对文件的定制版本(同fgets一样)

  • 不添加换行符(因为文件中有很多行,行与行之间已经有换行符)

  • fputs的第二个参数指明要写入的文件,如果是屏幕就用stdout(标准输出)

示例:编写一个循环,读入一行,另起一行打印该输入

//方法一:
char line[20];
//gets读到文章末尾会返回NULL,NULL值为0
while (gets(line)) //(相当于while(gets(line)! = NULL)
	puts(line);//添加换行符

//方法一:
char line[20];
//gets读到文章末尾会返回NULL,NULL值为0
while (fgets(line,20,stdin)) //省略了!= NULL
	fputs(line,stdout);

3.printf

printf:同puts一样把字符串地址作为参数

  • 不添加换行符

示例:

//两个等价
printf("%s\n",string);

puts(string)

pritnf不如puts方便,但可以同时输出多个字符串,也可以格式化不同数据类型

5.自定义输入输出函数

以getchar、putchar为基础,可以定义自己所需的I/O函数

示例1:需要一个类似puts但是不会自动添加换行符的函数

#include <stdio.h>
#define SIZE 20
void puts1(const char*);
int main(void)
{
	char words[SIZE];

	gets(words);
	puts1(words);

	return 0;

}

void puts1(const char * string)
{
	while (*string)//不要忘了这种写法。读到字符串最后的'\0'就退出
		putchar(*string++);//不要忘了这种写法
}

注意上面为什么是'const char * string'而不是'const char string[]'呢,技术方面这俩等价。数组形式、双引号括起来的字符串、指针形式都可以,用*string可以更好地提醒用户,实际参数不一定是数组(且使用数组形式,后面还要多一行int i = 0以操作数组元素)

示例2:设计一个类似puts的函数,要能给出待打印字符的个数

#include <stdio.h>
#define SIZE 20
int puts2(const char*);
int main(void)
{
	int num;

	num = puts2("beautifull girl!");//是可以直接输入字符串的
	printf("%d", num);

	return 0;

}

int puts2(const char * string)
{
	int count = 0;

	while (*string)
	{
		putchar(*string++);
		count++;
	}
	putchar('\n');//单引号双引号使用场所要牢记

	return count;
}

示例3:puts1和puts2嵌套使用

#include<stdio.h>
void puts1(const char *);
int puts2(const char *);
int main(void)
{
	puts1("My name is:");
    puts1("Sam");
    printf("I count %d characters.\n",
           puts2("I like beautiful girl!"))

	return 0;
}

void put1(const char* string)
	...

int put2(const char * string)
	...

6.字符串函数

C库有多个处理字符串的函数,ANSI C把这些函数的函数原型放在string.h头文件中,如strlen、strcat、strcmp、strncmp、strcpy、strncpy。另有sprintf原型在stdio.h

1.strlen

统计字符串长度(不包括'\0')

示例:定义了一个缩短字符串长度的函数并使用

#include<stdio.h>
#include<string.h>//为什么我一开始没写这一行strlen也可以执行....?
void fit(char*, int);
int main(void)
{
    //利用了字符串常量串联特性
	char mesg[] = "Things should be as simple as possible,"//打算修改字符串就不要用指针指向字符串面量
		"but not simpler.";
	puts(mesg);
	fit(mesg,38);
    puts(mesg);//puts会在'\0'处停止,剩余字符还在缓冲区,下面一行的puts会打印出来
	puts(mesg+39);//mesg+39是mesg[39]的地址

	return 0;
}

void fit(char* string,int size)//这个函数要修改字符串,所以不加const
{
	if (strlen(string) > size)
		*(string + size) = '\0';

}

2.strcat

strcat(ch1,ch2):拼接字符串

  • 接受2个字符串作为参数,把第2个字符串的备份附加在第1个字符串末尾,并把拼接形成的新字符串作为第1个字符串,第2个字符串不变

  • stract函数类型是char *。也就是说她和fgets一样返回一个指针

    //strcat函数原型
    char *strcat(char * restrict s1,const char * restrict s2);//要把第2个拼到第1个字符串,所以第1个不加const第2个加const·

示例:

#include<stdio.h>
#include<string.h>
#define SIZE 80
char * s_gets(char *, int);
int main(void)
{
	char flower[SIZE];
	char addon[] = "s smell like old shoes.";

	puts("What is ur favorite flowers?");
	if (s_gets(flower, SIZE))//可以去掉if
	{
		strcat(flower, addon);
		puts(flower);
		puts(addon);
	}
	else
		puts("End of file encountered!");
	puts("Bye");

	return 0;
}

//模仿fgets函数,fgets函数返回指向原数组的指针
char * s_gets(char* st, int n)//这个函数要修改字符串,所以不加const
{
	char * ret_val;
	int i = 0;//因为不是while,所以下面的赋值也可以变为这里的初始化

	ret_val = fgets(st, n, stdin);//所以fgets的第一个参数实际上应该是指针;
	if (ret_val)//我认为不该有这一句
	{
		while (st[i] != '\n' && st[i] != '\0')//这里绝对绝对要写成'\n'而不是'\n ',C要求很严格的!
			i++;
		if (st[i] == '\n')
			st[i] = '\0';
		else
			while (getchar() != '\n')//任何字符的ascii码都不是0,字符0的ascii是48
				continue;
	}
	return ret_val;
}

3.strncat

strncat(ch1,ch2,n):拼接字符串升级版

strcat无法检查第1个数组能否容纳第2个字符串,容易发生溢出问题/.而strncat第三个参数指定了最大添加字符数,即在添加到第n个字符或空字符时停止

示例:

#include<stdio.h>
#include<string.h>
#define SIZE 30
#define BUGSIZE 13
char * s_gets(char *, int);
int main(void)
{
	char flower[SIZE];
	char addon[] = "s smell like old shoes.";

	char bug[BUGSIZE];
	int available;//最大能增加字符个数

	puts("What is ur favorite flowers?");
	s_gets(flower, SIZE);
	if (strlen(addon) + strlen(flower) + 1 <= SIZE)
		strcat(flower, addon);
	puts(flower);

	puts("What is ur favorite bug?");
	s_gets(bug, SIZE);
	available = BUGSIZE - strlen(bug) - 1;
	strncat(bug, addon, available);
	puts(bug);

	return 0;
}

//模仿fgets函数,fgets函数返回指向原数组的指针
char * s_gets(char* st, int n)//这个函数要修改字符串,所以不加const
{
	char * ret_val;
	int i = 0;//因为不是while,所以下面的赋值也可以变为这里的初始化

	ret_val = fgets(st, n, stdin);//所以fgets的第一个参数实际上应该是指针;
	if (ret_val)//我认为不该有这一句
	{
		while (st[i] != '\n' && st[i] != '\0')
			i++;
		if (st[i] == '\n')
			st[i] = '\0';
		else
			while (getchar() != '\n')//任何字符的ascii码都不是0,字符0的ascii是48
				continue;
	}
	return ret_val;
}

4.strcmp、strncmp

4.1 strcmp

1.strcmp

strcmp是字符串版的关系运算符

strcmp(ch1,ch2):比较字符串。同为0,否则返回非零值(没说是1哦)

如何比较:比较 数组中的字符串 而非 整个数组,所以可以比较存储在不同大小数组中的字符串。实际上是依次比较所有的字符的机器排序序列,即字符的数值 (通常使用ascii码),直至发现不同字符

  • //最后比较的是s和空字符,由于空字符在ascii值为0排第一,s一定大于空字符,所以返回正值
    strcmp("apples",:apple) is 115

示例:

#include<stdio.h>
#include<string.h>
#define answer "Grant"
#define SIZE 30
char * s_gets(char *, int);
int main(void)
{
	char try[SIZE];

	s_gets(try, SIZE);
    //想清楚逻辑
	while (strcmp(answer, try))//不能直接(try != answer),这样比较的是两个指针的值,肯定不同
	{
		puts("No,that's wrong.Try again.");
		s_gets(try, SIZE);//会覆盖try数组之前的元素
	}
	puts("Ur right!");
	
	return 0;
}

//模仿fgets函数,fgets函数返回指向原数组的指针
char * s_gets(char* st, int n)//这个函数要修改字符串,所以不加const
	...

2.返回值

ASCII规定:(比较的是ascii码,返回的也是ascii码之差,ascii中空字符排第一,大写字母一定在小写字母之前)

  • 参数1>参数2:返回负数

  • 参数1=参数2:返回0

  • 参数1<参数2:返回正数

3.注意

strcmp比较的是字符串而非数组更不是字符,所以她的参数是2个字符串。而char类型实质为Int类型,所以可以用关系运算符来比较

#define word "a";
char ch = 'a';

//下面语句都有效
if(strcmp(word,"quit"))//ch和'q'不能做为strcmp的参数
	
if(ch == 'q')

我认为还是输入的是指针?

自编写示例:检查程序是否要停止输入(书上编写的吊)

#include<stdio.h>
#include<string.h>
#define END "quit"
#define SIZE 30
char * s_gets(char *, int);
int main(void)
{
	char ch[SIZE];

	puts("Enter \"quit\" to live.");//要输出双引号,每个双引号前面要加个'\'
	puts("Pls enter somrthing:");
	if(s_gets(ch, SIZE))//省略!= NULL
	{
		while (strcmp(ch, END))
		{
			puts("Pls enter somrthing:");
			s_gets(ch, SIZE);//会覆盖try数组之前的元素
		}
	}
	
	puts("See u next time.");
	
	return 0;

4.2 strncmp

strncmp(ch1,ch2,n):比较两个字符串到指定位置 或 只比较n指定的字符数

//查找以"astro"开头的字符串
strncmp(list,"astro",5)

5.strcpy、strncpy

5.1 strcpy

1.strcpy

strcpy(目标字符串,源字符串):拷贝字符串

  • strcpy相当于字符串赋值运算符

  • strcpy接受两个字符串指针作为参数,可以把指向源字符串的第2个实参指针声明为指针、数组名、字符常量;而指向源字符串副本的第1个实参指针应指向一个数据对象(如数组)。要记住,声明数组分配存储数据的空间,声明指针只分配存储地址的空间

示例:要求输入q开头的单词,该程序把输入拷贝到临时数组,如果第1个字母是q,程序调用strcpy把整个字符串从临时数组拷贝至目标数组中

#include<stdio.h>
#include<string.h>
#define SIZE 40
#define LTM 5
char * s_gets(char *, int);
int main(void)
{
	char qwords[LTM][SIZE];
	char temp[SIZE];
	int i = 0;

	printf("Enter %d words beginning with q:\n", LTM);
	while (i < 5 && s_gets(temp, SIZE))//这里谁放前面是由讲究的,s_gets放前面会导致五次成功后还要输入第6次(逻辑表达式求值从左到右)
	{
		if (temp[0] == 'q')//也可以写为:if(strncmp(temp,"q",1))
		{
			strcpy(qwords[i], temp);
			i++;
		}
		else
			printf("%s doesn't begin with q!\n",temp);
	}
	puts("Here are some wors accepted:");
	for (i = 0; i < LTM; i++)
		puts(qwords[i]);

	return 0;
}

char * s_gets(char *, int);

注:命令行键入Enter和函数本身是否接收'\n'到缓冲区没关系,键入Enter他就会换行

2.strcpy的其他属性

  1. strcpy返回值类型为char *,即第1个参数的值

  2. 第1个参数不必指向数组开始。这个属性可用于拷贝数组的一部分

5.2 strncpy

strncpy(目标字符串,源字符串,n):strcpy同strcat一样不能检查目标空间书否能容纳源字符串副本

第三个参数知名可拷贝的最大字符数,但是如果拷贝到第n个字符还未拷贝完整个源字符串就不会拷贝'\0'。所以拷贝的副本不一定有空字符,所以可以把n设置为比目标数组大小少1,并把目标数组最后一个元素设置为'\0'

6.sprintf

sprintf(ch0,"%s,%s",ch1,ch2,...)

函数声明在<stdio.h>中,用法和printf相同,只不过sprintf把组合后的字符串存储在数组ch0中而不是显示在屏幕上

示例:但我输出钱数一直溢出,不知道怎么回事

#include<stdio.h>
#include<string.h>
#define MAX 20
char* s_gets(char*, int);
int main(void)
{
	char first[MAX];
	char last[MAX];
	char formal[10 * MAX + 10];
	double prize;

	puts("Enter ur first name:");
	s_gets(first, MAX);

	puts("Enter ur last name:");
	s_gets(last, MAX);

	puts("Enter ur prize money:");
	scanf("%1f", &prize);

	sprintf(formal, "%s,%-19s:  $%6.2f\n", last, first, prize);//用法和printf相同,存储在第一个参数中而不是显示在屏幕上
	puts(formal);

	return 0;
}

//模仿fgets函数,fgets函数返回指向原数组的指针
char* s_gets(char* st, int n)//这个函数要修改字符串,所以不加const

7.字符串示例:字符串排序

示例:给字符串按字母表顺序排序并输出

#include<stdio.h>
#include<string.h>
#define SIZE 80
#define LTM 20
char* s_gets(char* ch, int n);
void stsrt(char* strings[], int n);
int main()
{
	char input[LTM][SIZE];
	char* ptstr[LTM];//内涵5个指针的数组
	int ct = 0;//输入计数
	int k;//输出计数

	printf("Input up to %d lines,and I will sort them.\n", LTM);
	printf("To stop,press the Enter key at line's at a line's start.\n");
	while (ct < LTM && s_gets(input[ct], SIZE) && input[ct][0] != '\0')
	{
		ptstr[ct] = input[ct];//如果input是因为就不行了,因为一维数组元素不是地址
		ct++;
	}
	stsrt(ptstr, ct);
	puts("\nHere's the sorted list:\n");
	for (k = 0; k < ct; k++)//ct此时是5不是0
		puts(ptstr[k]);

	return 0;
}

//s_gets指针版
char* s_gets(char* ch, int n)//两个特点:返回指针,处理数组一般需要传入长度
{
	char* ret_val;
	int i = 0;

	ret_val = fgets(ch, n, stdin);
	if (ret_val)//即if(ch != NULL),意思是读到文件结尾就不用指向内部了.一定分清楚fgets本身还是数组ch本身
	{
		while (*(ch + i) != '\n' && *(ch + i) != '\0')
			i++;
		if (*(ch + i) == '\n')
			*(ch + i) = '\0';
		else
			while (getchar() != '\n')
				continue;
	}
	return ret_val;
}

//选择排序算法
void stsrt(char* strings[], int n)//注意这里的参数1
{
	char* temp;
	int top, seek;

	for (top = 0; top < n - 1; top++)//这两个for都没加{}哦
		for (seek = top + 1; seek < n; seek++)
			if (strcmp(strings[top], strings[seek]) > 0)
			{
				temp = strings[top];
				strings[top] = strings[seek];
				strings[seek] = temp;
			}
}

上述程序详解:

排序指针而非字符串

该程序巧妙之处在于排序的是指向字符串的指针而不是字符串本身:排序过程把ptrst里的指针元素重新排序,并未改变input,这样的方法比用strcpy交换两个input字符串内容简单的多,而且还保留了input数组中的原始顺序

选择排序算法

简单且直观:第一次从待排序序列中找出最小(大)的一个元素,存放在已排序序列起始位置。然后再从剩余待排序序列中找到最小(大)元素放到已排序序列末尾位置。以此类推。

7.ctype.h字符函数和字符串

ctype.h中有很多与字符相关的函数,这些函数不能处理整个字符串,但可以处理字符串中的字符

8.命令行参数

图形界面普及之前都使用命令行界面,如DOS、UNIX

命令行就是在命令行环境中,用户为运行程序输入命令的行

现文件中有一个名为repeat.c的的程序:

#include<stdio.h>
int main(int argc, char* argv[])
{
	int count;

	printf("The command line has %d arguments:\n", argc - 1);
	for (count = 1; count < argc; count++)
		printf("%d: %s\n", count, argv[count]);
	printf("\n");
	
	return 0;
}

把该程序编译为可执行文件repeat。下面是通过命令行运行该程序后的输出

他的原理如下:

C编译器允许main没有参数或有2个参数。当main有2个参数时,第1个参数是命令行中的字符串数量,惯例记为argc。系统用空格表示1个字符串的结束和下一个字符串的开始。所以上述例子包括命令名共有4个字符串,其中后三个供repeat使用。字符串存储在内存中,而地址则存储在指针数组中,即main的第2个参数,惯例记为argv,大多系统会把程序本身的名称赋给argv[0]

此外:char **argv与char *ar[]等价,也就是书,argv是一个指向(指针)的指针,它所指向的指针 指向char

十二、存储类别、链接和内存管理

本书所有数据都存储在内存,C通过内存管理系统指定变量的作用域生命周期,实现对程序的控制。合理使用内存存储数据是设计程序的一个要点

1.存储类别

存储期对象在内存保留时间

作用域链接:描述标识符可见性,表明程序哪部分可以使用它

不同的存储类别具有不同的存储期、作用域、链接

C提供多种不同的模型或存储类别在内存中存储数据,先学习作用域、链接、存储器,再学习具体存储类别

1.1 访问内存

/*

标识符:编程中起的名字

左值:标识对象的标识符表达式。变量名、指针都属于左值.数组名不属于左值

面向对象编程的对象:类对象,其定义包括数据和允许对数据进行的操作。而C不是面向对象编程语言

对象:硬件上,存储一个或多个值的内存。对象可能未存储实际的值,但在存储适当值时具有相应的大小

*/

核心句:软件上,程序需要一种方法访问对象:主要通过声明变量完成访问内存,我认为可以说是主要通过左值(左值的定义决定的,并且变量属于左值) 完成访问内存

  • int entity = 3,变量名entity用来指定内容特定内容3,为程序访问内存的方式

  • int *pt = &entity,int ranks[10]

    • 表达式★pt不是变量名是表达式,指定了一个对象(特定内容3),所以同entity都是左值,指定了一个对象(指针pt才是变量名,也是左值)

    • 表达式★(ranks+entity)不是标识符是表达式,也指定了一个对象,属于左值。(ranks+entity)是表达式但不指向内存内容,不是左值没有指向指定对象

    • 另外一提:ranks的声明创建了1个大对象,而每个元素都可以访问,也是对象。而数组名在值上是一个右值,代表数组首地址,是常量,是右值

  • const char * pc = "Behold a string literal!":const限定的是字符串本身而不是指针变量pc

    • 内含字符值的字符串面量是不可修改的左值,也是对象。每个字符都可以访问,也是对象

    • 变量pc可以指向别处,是可修改的左值

    • 此处的表达式*pc指向'B',是不可修改的左值

1.2 作用域

作用域:程序中可访问标识符的区域

变量的作用域:块作用域、函数作用域、函数原型作用域、文件作用域

复合语句:由花括号括起来的一条或多条语句构成

:整个函数体、任意复合语句(不是非得for、if那些才有复合语句,参考复合语句的定义,只要是括号扩起来的语句就是复合语句)

1.块作用域

  • 定义在块中的变量具有块作用域

  • 范围:从定义到块末尾

  • 形参虽然声明在括号前,但也具有块作用域,属于函数体这个块

  • 目前为止使用的局部变量(包括形参)都具有块作用域(有的是自动存储期,有的是静态存储期)

  • C99允许具有块作用域的变量在块的任何位置声明,因此可以在for循环头声明变量,变量i被视为整个循环块(for语句(这里说明for语句是指整个循环))的一部分,他的作用域仅限for循环,一旦程序离开for这个块,就不能再访问i(但还可能存在,比如静态变量) 为适应这个新特性,C99把块的概念扩展到for、while、do while的循环体(叫子块,循环是整个循环的子块)(没有花括号的简单语句也是循环体)

    //整个循环块(for语句)是一个(循环)块
    for(int i = 0;i<10;i++)//i具有块作用域
    	printf("S C99 feature: i = %d",i);//这个就是循环体,由复合语句或简单语句构成,不管有没有花括号都是for语句的子块

    嵌套块/函数的内外变量重名情况:子块变量因此块变量/内部变量隐藏外部变量(如果变量不重名就直接没事了)

    #include<stdio.h>
    int main()
    {
    	int n = 8;//原始n
    	
    	for(int n = 1;n<3;n++)//这个函数头的n会隐藏最上面的n
    		printf("...");
        for(int n = 1;n<3;n++)//索引n
        {
            //循环体n被创建、销毁了3次
         	int n;//循环体n。索引n先隐藏原始n,循环体时使用循环体n,一轮迭代结束后,循环体n就消失,循环头在使用所以n进行测试
            ...    
        }
        
        return 0;
    }

2.函数作用域

仅用于goto 语句的标签,即使一个标签首次出现在函数内层块中,它的作用域也延伸至整个函数

*goto语句由goto和标签名两部分组成,标签命名遵循变量命名规则

goto parts2;
parts:printf("Refined analysis:\n");

3.函数原型作用域

  • 用于函数原型中的形参名(变量名)

  • 范围:形参定义处到原型声明结束。这意味着编译器处理函数原型时只关心它的类型,而形参名(如果有的话)通常无关紧要,即使有也不必与函数定义中一样(只有在变长数组???中,形参名才有用,变长数组有待研究)

4.文件作用域

  • 变量定义在所有函数外面,具有文件作用域(具有文件作用域的变量必定是静态变量,static 局部变量也是静态变量)

  • 范围:内部链接的文件作用域:仅限于一个翻译单元。也叫做文件作用域 外部链接的文件作用域:延伸至其他翻译单元的作用域。也叫做全局作用域程序作用域 这样的变量可用于多个函数,所以文件作用域变量被称为全局变量(所在文件从定义处开始后面的函数都可以用,其他文件用 extern引用声明式后可用)

    //这是一个源代码文件,包含多个头文件
    #include<stdio.h>
    int units = 0;	//units具有文件作用域,是全局变量
    void critic(void)
    int main(void)
    {
    	...
    }
    void critic(void)
    {
    	...
    }

翻译单元和文件:源.c是源代码文件 .h是头文件 .c中要包括1个或多个.h

多个文件在编译器中可能以1个文件出现,列如通常在源代码中包含一个或多个头文件。头文件会一次包含其他头文件,所以会包含多个单独的物理文件。但是C预处理器实际上是用包含的头文件内容替换#include指令(应该是把头文件内容拷贝过来了) 。所以,编译器把源代码文件和所有头文件都看出是一个包含信息的单独文件。这个文件被称为翻译单元。

描述一个具有文件作用域的变量时,它默认可见范围是程序的所有翻译单元(static 全局变量可见范围是所在翻译单元)。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成,每个翻译单元均对应1个源代码文件和它所包含的文件

即:程序——1个文件(或多个文件)——编译为1个翻译单元(或多个翻译单元)。 所以特别要明白:教学程序可能只有一个源代码文件,而程序可能有好几个源代码文件(但只能有一个main函数)

1.3 链接(static)

C变量有三种链接属性:外部链接、内部链接、无连接(注意外部和内部有时代表物理位置有时代表连接属性)

  • 具有块作用域、函数作用域、函数原型作用域的变量都是无连接变量,这意味这这些变量只属于定义他们的块、函数或原型私有

  • 只有具有文件作用域的变量可以是外部链接或内部链接

    • 外部链接变量:可以在多程序中使用

    • 内部链接变量:只能单元中使用

static区分文件作用域变量内部与外部链接:外部定义是否使用存储类别说明符static,static变量是内部连接变量

//程序的该文件和其他文件都可以使用变量giants。而变量dodgers该文件私有,该文件内的任意函数(只要没有重名局部变量)都可使用它
int gaints = 5;	 //外部链接的静态变量
static int dodgers = 3;	 //内部链接的静态变量
int main()
{
	...
}

1.4 存储期

描述通过标识符访问的对象的生存期 静态存储器、线程存储器、自动存储期、动态分配存储期

/*

程序设计:

  • 顺序程序设计:程序模块按语句顺序执行

  • 并发程序设计:将一个程序分成若干同时可执行的程序模块,每个程序模块和它执行时所处理的数据就组成一个进程

*/

1.静态存储期(static)

  • 程序执行期间一直存在

  • 文件作用域变量(无论链接属性如何)都具有(也必须是)静态存储区

  • static:

    • 对于块作用域变量,static表明静态存储期,但只有进入其所在块时才能访问其内容

    • 对于文件作用域变量,static表明内部连接而非静态存储期

      void more(int number)
      {
      	int index;//块作用域变量index具有自动存储期
      	static int ct = 0;//块作用域变量ct具有静态存储期,但只有进入more函数块才能使用ct访问它所指定的对象(但可通过提供该存储区地址给其他函数以简介访问该对象,如通过指针形参或返回值)
      	...
      }

2.*线程存储期

  • 用于并发程序设计,程序执行可分为多个线程。具有线程存储区的对象,从声明到线程结束一直存在

  • 以关键字_Thread_local声明对象时,每个线程都获得该变量的私有备份

3.自动存储期

  • 块作用域的变量具有自动存储期(前面加上static会属于静态存储期)

  • 程序进入定义这些变量的块时(书上明确说不是声明处,为什么???????)为这些变量分配内存,退出这个块时释放刚才分配的内存。这样的做法相当于把自动变量占用的内存视为一个可重复实用的工作区或暂存区

  • 变长数组稍有不同,是从声明处到块的末尾

4.动态存储期

1.5 存储类别

自动变量/auto 自动变量

register 自动变量:寄存器变量

static 自动变量:块作用域的静态变量(表中第5个,但是放到第3个讲的)(内部静态存储类别、局部静态变量)

全局变量:外部连接的静态变量(外部存储类别、外部变量)

static 全局变量:内部链接的静态变量

局部变量与全局变量是针对作用域(针对作用域也可以理解为位置上是在函数外面还是里面),静态变量是针对存储期

  • 局部变量:块作用域变量。定义在函数内部,xxxx存储期,位于栈内存,没有默认值必须手动赋值

  • 全局变量:文件作用域变量。定义在函数外面,静态存储期,位于堆内存,没有赋值的话会有默认值(规则和数组一样)

  • 静态变量:定义在函数内部的变量。作用域xxxxxxxxxxxx,静态存储期,位于xxxxxx,没有赋值的话会有默认值(规则和数组一样)

可以说:全局变量==外部变量,内部变量==局部变量,都是互通的。但有时候外部变量-内部变量也只是一种嵌套、位置上的关系概念,具体书本具体分析即可

1.自动变量

自动存储类别变量:默认情况下声明在块或函数头中的任何变量都属于自动存储类别,具有自动存储期、块作用域、无链接(前面加上存储类别说明符register就可能成为寄存器变量)(自动变量是块作用域所以是局部变量,具有自动存储期,用完就没。后面讲到我们可以创建块作用域的静态变量)

关键字auto

  • 显示使用auto只是为了更清楚的表达是自动变量的意图(例如有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别)

    int main(void)
    {
    	auto int plox;
    }
  • auto是存储类别说明符,auto关键字在C与C++中的用法完全不同,如果想编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符

块作用域和无连接意味着另一个函数可以使用同名变量,但该变量是存储在不同内存位置上的另一个变量 自动存储期意味着程序进入变量声明所在块时变量存在,退出块时变量消失。原来该变量占用的内存位置现在可做它用

示例:这两个示例块作用域由讲

  • 嵌套块情况:块中声明的变量仅限该块及其包含块使用(自动变量只有声明时才被分配内存,退出块时释放

    int loop(int n)
    {
    	int m;//m作用域开始
    	scanf("%d",&m)
    	{//块开始
    		int i;//i仅内层可见
    		for (i = m;i<n;i++)
    			puts("i is local to a sub_block\n");
    	}//块结束
    	return m;//m的作用域,而i已消失
    }
  • 如果内存块中声明的变量与外层块中的变量同名会怎么样:内层块会因此外层的定义,但离开内层块后,外层块变量的作用域又回到了原来的作用域:在while块中声明的变量每次迭代都会 新建 再 取消

    #include<stdio.h>
    int main(void)
    {
    	int x = 30;//外层x
    
    	printf("x in outer block: %d at %p\n", x, &x);
    	{
    		int x = 77;//新的x,隐藏了外层x
    		printf("x in outer innner: %d at %p\n", x, &x);
    	}
    	printf("x in outer block: %d at %p\n", x, &x);
    
    	//每迭代结束一次,新的x就没,然后再创建再消失
    	while (x++ < 33);//外层x
    	{
    		int x = 100;//新的x,隐藏了外层x
    		x++;
    		printf("x in while loop: %d at %p\n", x, &x);
    	}
    	printf("x in outer block: %d at %p\n", x, &x);
    
    	return 0;
    }

自动变量的初始化:自动变量不会初始化,除非显式声明。如果未初始化,add变量的值是之前占用分配给add空间中的任意值(如果有的话)(静态变量都会自动初始化为0

int num = 1;
int add ;

2.寄存器变量

寄存器变量:存储在cpu的寄存器中,块作用域(所以也是局部变量)、无连接、自动存储期(可以看作自动变量的衍生)

  • 寄存器是最快的可用内存中(但不是内存),与普通变量相比,访问和处理这些变量的速度更快

  • 因为存储在寄存器而非内存,所以无法获取寄存器变量的地址

  • 绝大多数方面,寄存器变量和自动变量都一样 (是块作用域、无连接、自动存储期)

存储类别说明符register:用于声明寄存器变量(只作用于块作用域变量)

  • void macho(register int n)//在函数头中使用关键字register,便可请求形参是寄存器变量
    {
    	register int quick;//在函数体中使用register
  • 但register不像命令更像请求,编译器必须根据寄存器或最快可用内存的数量衡量请求,或直接忽略请求,所以可能不会如我们所愿 这种情况下寄存器变量就变成普通自动变量 即使是这样仍然不能对该变量使用地址运算符

  • 可声明为register的数据类型有限:寄存器空间有限,如double类型就不行

3.块作用域的静态变量

static 自动变量(不能在函数形参中使用static)

静态:指静态存储期(静态就这一个意思)。静态载入内存时分配内存,自动变量在程序执行块时分并在离开时销毁配

示例:执行后会发现静态变量stay保存了它被递增1后的值,但是fade变量每次都是1。这表明了初始化的不同:每次调用trystat会初始化fade,但是stay只在编译trystat时被初始化一次

#include<stdio.h>
void trystat(void);

int main(void)
{
	int count;//自动变量

	for (count = 1; count <= 3; count++)
	{
		printf("Here comes iteration %d");
		trystat();
	}
	
	return 0;
}

void trystat(void)
{
	int fade = 1;//自动变量
	static int stay = 1;//块作用域的静态变量

	printf("fade = %d and stay = %d\n", fade++, stay++);
}

上述例子中trystat函数里的两个声明很相似:

int fade = 1;
static int stay = 1;

第1条声明肯定是trystat函数的一部分,每次调用都会执行这条声明。这是运行时行为,第2条声明实际上并不是trystat函数的一部分。逐步调试会发现程序似乎跳过了这条声明,这是因为静态变量在程序被载入内存时已执行完毕。这条声明放在trystat函数中是为了告诉编译器只有trystat函数才能看到该变量。这条声明并未在运行时执行

4.外部链接的静态变量

具有文件作用域、外部链接、静态存储期。是默认的全局变量(加上static就是内部链接的静态变量)

存储类别说明符extern:

  • extern不用来创建外部定义,而是显式指明声明的变量定义在别处,程序遇到extern会认为实际定义在程序别处而不分配存储空间

  • 如果包含extern的声明在块外,则引用变量必须具有外部链接(这时变量定义式声明一般在别的文件)

    • 如果.c使用的外部变量定义在另一个.c,则必须用extern在本.c中引用式声明该变量

  • 如果包含exter的声明在块内(这时变量定义式声明一般在本文件函数外面),则引用的变量连接属性随意

    • 如果这个变量定义在本文件,则在函数中的extern引用式声明可选

示例1:main中的两条声明均为可选,仅仅为了说明main函数要使用这两个变量,去掉Errupt的extern则会创建新的独立局部变量Errupt。如果局部变量不得已要与外部变量重名,最好在局部变量前面加上auto

int Errupt;//外部连接的静态变量
double Up[100];//外部连接的静态数组
extern char Coal;//如果Coal被定义在另一个.c 则必须加上extern

int Pocue;
int main(void)
{
    extern int Errupt;//可选的声明
    extern double Up[];//可选的声明,前面已经声明过,无需指明数组大小
}

示例2:

int Hocus;//外部变量,对main、Hocus均不可见,但对该文件为创建局部的其他函数可见
int magic();
int main (void)
{
	int Hocus;//自动变量,隐藏了外部Hocus
	...
}
int Pocus//外部变量,对main不可见magick可见
int magic()
{
	auto int Hocus;//显示声明自动变量,隐藏了外部Hocus
}

声明:未显式初始化都会被自动初始化为0 (咱就是说,静态变量都是可以自动初始化为0) 外部数组元素也一样。与自动变量不同,外部变量只能用常量表达式初始化文件作用域变量

int x = 10;//10是常量表达式
size_t z sizeof(int);//sizeof(int)也是常量表达式
int x2 = 2*x;//不行,x是变量
int main (void)
{
	...
}

定义和声明区别:第一次声明为变量预留存储空间,第二次声明只告诉编译器使用之前创建的tern变量(外部链接的静态变量只在一个文件中定义一次,在其他文件必须通过引用式声明才能实现共享,本文件函数内使用则随意)(声明,函数原型他爹!)

int tern = 1;//定义式声明。不要用extern,否则编译器会假设tern在程序别处被定义
int main()
{
	extern int tern;//引用式声明,extern表明该声明不是定义。这此声明不是定义告诉程序使用在别处定义的tern
}

外部变量只能初始化一次,且必须在定义该变量时进行

//file_one.c
char permis = 'N';
...

//file_two.c
extern char permis = 'Y'//错误,因为file_one.c中的定义式声明已经创建并初始化permis

5. 内部连接的静态变量

static 全局变量。具有静态存储期、文件作用域、内部链接

static int svil = 1;//静态变量,内部链接
int main (void)
...

extern也适用于对内部链接的静态变量,且不改变连接属性

int q1 = 1;//外部连接
static int q2 = 2;//内部链接
int main()
{
	extern int q1;//使用定义在别处的q1,可选
	extern int q2;//使用定义在别处的q2,可选
}

6.多文件

程序由多个翻译单元构成时,才体现区别内部连接和外部链接的重要性。复杂代码通常由多个单独源代码文件组成。有时需要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说除了一个定义式声明外,其它(引用式)声明都要使用extern关键字,并且只有定义式声明才能初始化变量

注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明(用extern)它。也就是说在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它

一个复杂程序往往有多个文件,一个有主函数的文件(内含标准头文件) + 存放函数定义的文件+自定义头文件

关于声明的问题,比如主函数文件调用存放函数定义文件,extern的引用式声明就发挥了作用,静态变量在载入程序时就已执行完毕

关于使用其他文件内部链接的静态变量,静态变量和内部链接表明:它在整个程序期间都存在,只是其所在文件的函数才可见它,调用其他文件的函数即可调用其他文件的内部链接的静态变量

7.存储类别说明符

C有6关键字作为存储类别说明符:除了所学4个还有_Thread_local、typedef。typedef关键字与任何内存存储无关(该从从内存角度思考存储类别说明符名字的含义了),归于此类有语法上的原因。 一般不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分???,唯一例外时Thread_local,它可以和static或extern一起用

static、extern的含义:二者在块内和块外意义不同

auto只用于块作用域变量,主要是为了明确表达要使用与外部变量同名的局部变量的意图

register也只用于块作用域变量,把变量归位寄存器存储类别请求最快速度访问该变量并保护该变量地址不被获取

static说明创建对象具有静态存储期。static用于文件作用域声明,作用域受限于该文件;static作用域文件块作用域声明,作用域受限于该块(有意思的角度)

extern表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用变量具有外部链接;如果包含extern的声明具有块作用域,则引用的变量可能具有外部连接或内部连接,这取决于该变量的定义式声明

好的设计可以不使用文件作用域变量

8.存储类别和函数

函数也有存储类别,有外部函数(默认)或静态函数(C99新增内联函数将在16章介绍),外部函数可被其他文件的函数访问,但静态函数只能被本文件函数调用

示例:同一个程序的其他函数可以调用gamma和delta,不能调用beta,因为static表明其为该文件私有。这样避免了名称冲突,在其他文件中可以使用与beta同名的函数。通常做法是用extern声明定义在其他文件中的函数,这样是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明默认为extern(而引用其他文件外部变量则必须用extern)

double gamma(double);//默认为外部函数
static double beta(int,int);//该文件私有
extern double delta(double,int);

9.存储类别的选择

对于“使用哪种存储类别”的回答:大多数是"自动存储类别"。默认存储类别就是自动存储类别。多多使用外部变量似乎不错,但这样很有可能造成变量在其他地方被篡改,经验表明,随意使用外部存储类别的变量坏处远远超过带来的便利

唯一例外的是const数据,他们在初始化后就不会被修改,所以不必担心它们被意外篡改

保护程序的黄金法则是:"按需知道"。尽量在函数内解决该函数的任务,只共享需要共享的变量。使用非自动存储类别之前要先思考是否有必要

2.随机函数和静态变量

rand函数:随机数函数。ANSI C库提供了rand()函数生成随机数,生成随机数有多种算法,ANSI C允许C实现针对特定及其使用最佳算法(在stdlib.h中)。当然ANSI C还提供了一个可移植的标准算法,为了看清程序内部情况,我们使用可移植的标准算法

rand本身的种子是定值,所以每次的随机总是同一个数,是伪随机数,会生成可预测的实际序列

(rand()函数每次调用前都会查询是否调用过srand(seed),是否给seed设定了一个值,如果有那么它会自动调用srand(seed)一次来初始化它的起始值;若之前没有调用srand(seed),那么系统会自动给seed赋初始值,即srand(1)自动调用它一次srand函数。所以rand本身实际上是伪随机数,会生成可预测的实际序列。)

srand函数是随机数发生器的初始化函数,这个函数需要提供一个种子,如srand(1),用1来初始化种子

版本0:只有可移植rand()

/*rand-.c ==生成随机数*/
/*使用ANSI C可移植算法*/
//种子=1
static unsigned long int next = 1;//它对下面的文件不可见,但rand0可以使用它,主函数文件调用rand0而使用本来不可见的它就是这个道理!!!

unsigned int rand0(void)
{
	next = next * 1103515245 + 12345;
	return (unsigned int)(next / 65536) % 32768;//随机数范围在0~32767
}
/*r_dribe0.c -- 测试rand0()函数*/
/*与rand0.c一起编译*/
extern unsigned int rand0(void);//(定义式)声明,不写也一样执行

int main(void)
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d\n",rand0());
	}
	return 0;
}

多次允许这个程序,结果都是这个

版本1:为改变,引入srand1()重置种子来解决问题。关键是要让next成为只供rand1()和srand1()访问的内部链接静态变量,把srand1()加入rand1所在文件

/* s_and_r.c -- 包括 rand1() 和 srand()的文件 */
/*使用ANSI C可移植算法*/
static unsigned long int next = 1;//种子

int rand1(void)

{
	next = next * 1103515245 + 12345;
	return(unsigned int)(next / 65536) % 32768;
}

void srand1(unsigned int seed)
{
	next = seed;
}
/*r_dribe0.c -- 测试rand0()函数*/
/*与rand0.c一起编译*/
#include<stdio.h>
#include <stdlib.h>//这里引用这个头文件没用啊,删了程序也继续执行
extern void srand1(unsigned int x);
extern int rand1(void);

int main(void)
{
	unsigned seed;

	puts("Pls enter ur choice for seed.");
	while (scanf("%u", &seed) == 1)
	{
		srand1(seed); //重置种子
		for (int count = 0; count < 5; count++)
			printf("%d\n", rand1());
		printf("Pls enter next seed(q to quit):\n");
	}
	printf("Done\n");

	return 0;
}

time():

  • ANSI C有一个time()会返回自格林尼治时间1970年1月1日0点以来经过的秒数,这个返回值因系统而异但重点是一个可进行运算的类型,类型名是time_t,具体类型与系统有关

    //函数声明,可以看出返回值类型为time_t,形参类型为指向time_t类型的指针
    time_t time(time_t *t);
  • 两种使用方式:声明:time_t t1, t2

    • t1 = time(NULL) 或 t1 = time(0);将空指针传递给time函数,并将time返回值赋给t1

    • time(&t2);将t2的地址传递给time,函数把结果传递给t2,不需要赋值语句

自动重置种子:C实现允许访问一些可变的量,如时钟系统。可用这些值(可能会截断)初始化种子值(虽然time返回值的具体类型与系统有关,但我们可以使用强制类型转换)

//具体类型与系统有关这没关系
#include<time.h>
srand1((unsigned int) time(0));

3.掷骰子

现实生活中,只有4面、6面、8面、12面、20面5种正多面体,其他面数的骰子各个面不相等,朝上的几率也不相同;计算机不必考虑这种限制,可以设计任意面数的电子骰子

INT_MAX:定义在limits.h中的字符常量,表示最大整数,值为2147483647(2^32)

RAND_MAX:定义在stdlib.h中的字符常量,表示随机数最大值,值最小为32767(2^15),最大为INT_MAX。通常(本系统也是)为最小值32767

从6面骰子开始:想获得1~6的随机数。但rand()生成的随机数在0~RAND_MAX之间.因此需要进行一些调整:

  1. 把随机数求模6,获得的整数在0~5

  2. 结果+1,新值在1~6

  3. 为从6面拓展到其他面,把第1步的数字6替换成筛子面数

  4. #include<stdlib.h>//提供rand()原型
    int roll(int sides)
    {
    	int roll;
    
    	roll = rand() % sides + 1;
    	return roll;
    }

/*

函数声明不是必须但很有必要:作用是在编译阶段对函数调用格式和用法就进行检查(只有主函数才需要声明吧?)

头文件:包括宏定义, 全局变量, 函数原型声明。文件名后多用.h。有C的标准头文件,也有我们自定义的头文件

对C编译器来说.c还是.h甚至.txt.doc都没有区别:.h中一般放的是同名.c文件中定义的变量、数组、函数的声明(如stdio.h)

在include的地方,把.h里的内容原封不动的复制到引用该头文件的地方

头文件引用有两种形式:#include < stdio.h> 和 include "main.h “

  • < >引用的一般是编译器提供的头文件,编译时,会在指定的目录中去查找头文件。具体是哪个目录,编译器知道,我们不用关心

  • ” “引用的一般是自己写的头文件,编译时,编译器会在项目所在的文件夹中进行查找,如果还才存在子文件夹,则在Makefile中用-I(大写i)来指定头文件搜索目录

使用标准函数要在当前文件包含标准头文件<xxx.h>以声明标准函数,如果使用自定义函数则最好把函数原型放在一个新建的.h文件中然后在当前文件包含".h"声明自定义函数(其实一直都是这么干的,只是我没注意到)

*/

我们还想用一个函数提示用户选择任意面数的骰子,并返回点数之和

给的例子优点蹩脚,去看看好的例子

4.分配内存:malloc和free

前面讨论的存储类别一个共同之处:确定存储类别后,会根据内存管理规则自动选择作用域和存储期。然而有更灵活的选择,用库函数分配和管理内存

回顾内存分配,所以程序都必须预留足够内存来存储程序使用的数据,这些内存中有些是自动分配的,分配为静态内存或自动内存,示例为一个float类型的值和一个字符串预留了足够的内存,或者显示指定分配一定数量的内存

//声明为内存提供了一个标识符,可通过x、place识别数据
float x;
char place[] = "Danceing Oxen Creek";

int place[100];//浪费空间

C能做的不止这些。可以在程序运行时分配更多内存,主要工具是malloc()

molloc():

  • 接受一个参数:所需内存字节数。

  • malloc会找到合适的空闲内存块,这样的内存是匿名的。即malloc分配内存但不会为其赋名(静态分配就会)

  • 然而molloc会返回动态分配内存块的地址。因此可以把该地址赋给指针变量,并使用指针变量访问该内存块

    • 然而从ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个通用指针。malloc可用于返回指向数组的指针、指向结构的指针等,所以返回值会被强制转化为匹配的类型。在ANSI C中应坚持使用强制类型转换,提高代码可读性。把指向void的指针赋给任意类型的指针完全不用考虑类型匹配问题

      //指针类型之间的强制转换,将 指向void类型 强制转化为 指向double类型
      ptd = (double *) mallo(n * sizeof(double));
  • 内存分配失败会返回NULL

现尝试用malloc创建一个数组,并用一个指针记录分配内存的位置:

double * ptd;
ptd = (double *) mallo(n * sizeof(double));

以上代码为n个double类型的值请求内存空间,并让ptd指向该位置(注意是指向首地址而不是指向这整个块),所以可以像用数组名一样使用它,如用数组表示法ptr[0]访问块首元素

现在有3种创建数组方法:

  • 声明普通数组,用常量表达式表示数组维度,用数组名访问数组元素。可用静态内存或自动内存创建(注意这个说法,新的理解角度)

  • 声明变长数组,用变量表达式表示数组维度,用数组名访问数组元素。只能在自动内存创建

  • 声明1个指针,调用malloc,将返回值赋给指针,使用指针访问数组元素。该指针可以是静态或自动(变量)

前面学过,后两种可以创建动态数组,这种数组可以在程序运行时选择数组的大小和分配内存。并且第3种更灵活

  • double item[n];//c99之前不允许,必须是整型表达式,实际上我的VS2021也不允许
    
    ptd = (double *) malloc//这是允许的

free():和malloc搭配。free()的参数是之前malloc返回的地址,该函数释放malloc分配的内存。所以动态分配内存的存储期从调用malloc分配内存到调用free释放内存为止。(可以设想malloc和free管理者一个内存池,每次调用malloc分配内存给程序使用,每次调用free把内存归还内存池)free和malloc的原型都在stdlib.h中

示例:

#include<stdio.h>
#include<stdlib.h>//提供malloc、free原型

int main(void)
{
	int* ptd;
	int max;
	int number;
	int i = 0;

	puts("What's the maximum number of type double entries?");
	if (scanf("%d", &max) != 1)
	{
		puts("Number not correctly entered -- bye.");
		exit(EXIT_FAILURE);//符号常量值为1,表示程序异常终止
	}
	ptd = (double*)malloc(max * sizeof(double));
	if (ptd == NULL)
	{
		puts("Memory allocation failed.Goodbye.");
		exit(EXIT_FAILURE);
	}
	puts("Enter the values(q to quit):");
	while (i < max && scanf("%1f", &ptd[i]) == 1)
		++i;
	printf("Here are ur %d entries:\n", number = i);
	for (i = 0; i < number; i++)
	{
		printf("%7.2f", ptd[i]);
		if (i % 7 == 6)
			putchar('\n');
	}
	puts("Done.");
	free(ptd);

	return 0;
}

4.1 free()的重要性

静态内存数量在编译时固定、自动变量内存数量程序执行期间自动增加或减少,但动态分配的内存数量只会增加。除非用free释放。如果不用free及时释放内存池容易被耗尽所有内存,这类问题叫做内存泄漏,必须在程序末尾调用free避免

4.2 calloc()

calloc和malloc类似都可以动态分配内存:calloc接受两个无符号整数作为参数(ANSI规定是 size_t类型),第1个参数是所需的存储单元数量,第2个参数是存储单元大小(字节为单位)

//创建了100个4byte的存储单元,总共400字节
long *newmem;
newmem = (long*)calloc(100,sizeof(long))//用sizeof(long)而不直接写long的长度,可以提高代码移植性

calloc还有一个特性:它把块中所有位都设置为0(某些硬件系统中,不是把所有位都设置为0来表示浮点值)

free也可释放calloc分配的内存

4.3 动态内存分配和变长数组

注意:变成数组可以动态分配内存,但是属于自动存储类别变量

VLA和调用malloc()在某些功能上有重合。例如之前讲的,两者都可创建动态数组(在运行时确定大小的)

int vlamal()
{
	int n;
	int * pi;
	scanf("%d",&n);
	pi = (int *) malloc(n*sizeof(int));
	int ar[n];//VLA
	pi[2] = ar[2] = -5;
	...
}

不同的是VLA是自动存储类型,程序离开vlamal就自动释放所占内存,不必使用 free。另一方面malloc创建的数组不必局限在一个函数内访问。例如:被调函数创建一个数组并返回指针,供主调函数访问,然后主调函数在末尾调用free释放之前被调函数分配的内存。另外free所用的指针变量可以与malloc指针变量不同,但是两个指针必须存储相同的地址,并且不能释放同一块内存两次

4.4 存储类别和动态内存分配

存储类别和内存分配有何联系

理想化情况下:可将内存分为三部分:一部分供具有外部连接、内部链接、无连接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配

  • 随着程序调用函数和函数结束,自动变量所用内存数量也在相应增加和减少,这部分内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁

  • 动态内存在调用malloc或相关函数时存在(VLA是动态分配,但不属于动态内存),在调用free后释放。这部分内存由程序员管理而不是系统规则。所以内存卡可以在一个函数中创建,在另一个函数中销毁,所以这部分的内存用于动态内存分配会支离破碎。也就是说未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈慢、动态分配的数据区域通常叫做内存堆或自由内存

  • 总之,程序把静态对象、自动对象和动态分配的对象存储在不同区域

5.ANSI C类型限定符

我们通常用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性和易变性。这两个属性可以分别用关键字const和volatile声明。这两个关键字类型是限定类型。C99新增第3个 限定符:restrict,用于提高编译器优化。C11标准新增第4个限定符:_Atomic。C11提供可选库,由stdatomic.h管理,一直吃并发程序设计,而Atomic是可选支持项

C99为类型限定符增加了一个新属性:幂等的,意思是可以在一条声明中多次使用同一个限定符,多余的限定符将会被忽略:

  • const const const int n = 6;//与const int n = 6;等价
  • 可编写如下代码

    typedef const int zip;//typedef:为现有类型创造别名
    const zip q = 8;

5.1 const类型限定符

1.在指针和声明中使用const

声明普通变量和数组用const很简单。指针比较复杂,要区分是限定指针本身为const还是限定指针指向的值为const(回去搜搜指针类型)

  • const float * pt:pt指向的对象不能被修改,pt本身可以修改。例如设置该指针指向其他值。写成float const * pt也可以

  • float * const pt:pt本身不可以修改。pt必须指向同一个地址,但他指向的值可以修改

  • const float * float pt:pt既不能指向别处,指向的值也不能修改

const最常见的用法就是声明为函数形参的指针以保护数组(前面讲过保护数组部分)

2.对全局数据使用const

使用全局变量是冒险的做法,这样暴露了数据,程序任何部分都可以修改数据。如果把数据设置为const,就可以避免这样的风险,因此用const限定符声明全局数据很合理。可以创建const变量、const数组和const结构(结构是一种复合数据类型)。然而,在文件间共享const数据要小心。可以采用两个策略:

  • 遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern)

    /*file1.c -- 定义了一些外部const变量*/
    const double PI = 3.14159;
    const char * MONTHS[12] = {"Ianuary"....};
    
    /*file2.c -- 使用定义在别处的外部const变量*/
    extern const double PI;
    extern const * MONTHS[];
  • 另一种方法:头文件方案,把const变量放在一个头文件中,然后再其他文件中包含该头文件。这种方案必须在头文件用static声明全局const变量。如果去掉static,那么再file1.c和file2.c中包含constant.h将导致每个文件都有一个相同标识符的定义式声明,C标准不允许(所以include其实就是把后面的内容全部复制过来)。这种方法相当于给每个文件提供了一个单独的数据副本。由于每个副本只对该文件可见,所以无法用这些数据和其他文件通信。不过没关系,他们都是完全相同的const数据,这不是问题。

    /*constant.h --定义了一些外部const变量*/
    extern const double PI;
    extern char * MONTHS[12] = {"jANUARY"...};
    
    /*file1.c*/
    #include "constant.h"
    
    /*file2.c*/
    #include "constant.h"

    头文件方案的好处是,方便偷懒,不用点击再一个文件中使用定义式声明,再其他文件中使用引用式声明。所有的文件斗只需包含一个头文件即可。但他的缺点是数据重复,如果const数据包含庞大的数组,就不能视而不见了

5.2 volatile类型限定符

十三、与文件进行通信

1.与文件进行通信

...

1.1 文件是什么

文件:磁盘固态硬盘上一段已命名的存储区(如stdio.h是一个文件的名称)

对操作系统而言,文件更复杂一些。如大型系统会被分开存储或包含一些额外数据,方便操作系统确定文件种类,但这是操作系统关心的,程序员关心的是C如何处理文件

C把文件看作一系列连续的字节,每个字节能被单独读取,这与UNIX环境中(C发源地)的文件结构对应,由于其他环节可能无法完全对应这个模型,C提供两种文件模式:文本模式、二进制模式

1.2 文本模式和二进制模式

首先区分:文本内容,二进制内容 文本文件格式,二进制文件格式 文件的文本模式二进制模式

所有文件内容以二进制形式存储,但是如果文件最初使用二进制编码的字符(如ASCII)表示文本(像C字符串那样),该文件就是文本文件,其中包括文本内容。如果文件中的二进制值还代表机器语言代码或数值数据或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容

UNIX用同一种文件格式处理文本文件和二进制文件的内容...

为规范文本文件的处理,C提供两种访问文件的途径:文本模式二进制模式。二进制模式中,程序可以访问文件的每个字节。而文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行莫问或文件结尾映射为C模式

虽然C提供二进制模式和文本模式,但这两种模式的实现可以相同,前面提到过,因为UNIX使用同一种文件格式,这两种模式对于UNIX实现完全相同,Linux也是如此

1.3 I/O的级别

除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别):底层I/O、标准高级I/O

  • 底层I/O:使用操作系统提供的基本I/O服务

  • 标准高级I/O:使用C库的标准包和stdio.h头文件定义。因为无法保证所有的操作系统都是用相同的底层I/O模型,C标准支支持标准I/O包。有些实现会提供底层库,但C标准建立了可移植的I/O模型,我们主要讨论这些I/O

1.4 标准文件

C程序会自动打开3个文件,他们被称为标准输入标准输出标准错误输出。默认情况下,标准输入是系统的普通输入设备如键盘,标准输出和标准错误输出是系统的普通输出设备,通常为显示屏

通常,标准输入为程序提供输入,它是getchar()和scanf()使用的文件。程序通常输出到标准输出,他是putchar()、puts()、printf()使用的文件。第8章提到的重定向把其他文件视为标准输入或标准输出、标准错误输出提供了一个逻辑上不同的地方来发送错误消息。例如,如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容任然会被发送到屏幕上。这样很好,因为如果把错误消息发送至文件,就只能打开文件才能看到

2.标准I/O

比起底层I/O,标准I/O除可移植外还有两个好处

  • 标准I/O有许多专门的函数简化了处理不同I/O的问题。例如printf()把不同形式的数据转化成与终端相适应的字符串输出

  • 输入和输出都是缓冲的,即一次转移一大块信息而不是一字节信息(通常至少512字节)。

十四、结构和其他数据形式

有时候,仅靠简单变量甚至数组表示数据是不够的,C提供了结构变量提高我们表达数据的能力,能让我们创造新的形式

结构相当于一个超级数组;结构可以成为数组的元素,数组也可以成为结构的成员

1.示例问题:创建图书书目

比如想打印一本书的各种信息,需要一种可以包含各种类型数据的数组,这就是结构

结构:一种可以包含各种数据类型的数据形式,并能保持各信息独立

//* book.c -- 一本书的图书书目 */
#include<stdio.h>
#include<string.h>
char* s_gets(char* , int );
#define MAXTITL 41/*书名的最大长度+1*/
#define MAXAUTL 31/*作者姓名的最大长度+1*/

//结构模板:标记是book,有3个成员或字段
struct book {
	char title[MAXTITL]
	char author[MAXAUTL];
	float value;
};//结构模板结束

int main(void)
{
	struct book library;/*把library声明为1个book类型变量*/

	puts("Pls enter the book title.");
	s_gets(library.title, MAXTITL);//访问title部分
	puts("Now enter the author.");
	s_gets(library.author, MAXAUTL);
	puts("Now enter the value.");
	scanf("%f", &library.value);
	printf("%s by %s: $%.2f\n", library.title);
	puts("Done");

	return 0;
}

char* s_gets(char string, int n)
{
	char* ret_val;
	char* find;

	ret_val = fgets(string, n, stdin);
	if (ret_val)
	{
		find = strchr(string, '\n');
		if (find)
			*find = '\0';
		else
			while (getchar() != '\n')
				continue;
	}
	return ret_val;
}

2.建立结构声明

结构声明(模板)描述了一个结构的组织布局。结构的内部可以是任何C的类型包括结构本身(C++的模板更强大,非彼模板)

struct book{
	char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};

struct关键字表明后面是结构, book是可选标记(后面的程序可以使用标记引用对应结构),标记可选,如果一处定义结构,一处定义实际结构变量,则必须使用标记

struct book library;//创建了一个结构变量library,该变量的结构布局是book

结构声明放在函数外:声明之后的所有函数可用 结构声明放在函数内:仅限该函数内部使用

3.定义结构变量

结构有两层含义:

  • 结构布局:结构布局告诉编译器如何表示数据,但并不能让编译器为其分配空间(即结构的框架和内容,上面已经讲过)

  • 分配空间:下一步是创建一个结构变量,这就是结构的另一个含义

    如下代码创建了一个结构变量library。编译器用book模板为该变量分配空间

    struct book library

在结构声明中,struck book相当于一般声明的int、float。我们可以定义无数个struck book类型的变量、甚至是指向struck book的指针本质上:book结构声明创建了一个名为struct book的新类型

//doyle,panshin包含title、author、value部分,指针ptbook可以指向doyle、panshin或者任何book类型
struct book doyle,panshin,* ptbook;

//简化
strcut book liberary;

//完整
struct book{
    char title[MAXTITL];
    char author[AXAUTL];
    float value;
}liberary;

换言之:声明结构的过程和定义结构变量的过程可以组合成一个步骤(那光写上面完整的应该就可以)

struct {//不需要book标记
    char title[MAXTITL];
    char author[AXAUTL];
    float value;
}liberary;

如果打算多次使用结构模板,就要使用带标记的形式。或者使用后面介绍的typedef。当然这是定义结构变量的一个方面,尚未初始化

3.1 初始化结构

结构初始化和数组初始化类似。ANSI之后,可以使用任意存储类别初始化结构。前面讲过静态变量的初始化必须是常量,这对结构同样使用

注意,声明用 ';' 初始化用','

struct book liberary = {
    "My name is Sam",//每个成员单独一行只是为了对应之前声明结构的形式,方便阅读
    "My girl is xiao.ma",
    1.95
};

3.2 访问结构成员

结构类似一个超级数组。这个超级数组中有各种类型,数组通过下标访问元素,结构通过结构成员 运算符'.'+成员名 访问成员。如library.value可以访问liberary的value部分(本质上.value就是book结构的下标)

注意:虽然library是结构变量,但library.value浮点型,满足任何浮点数操作

//.的优先级高于&
scanf("%f",&library.value);

3.3 指定初始化器

前面讲过C为数组提供指定初始化器,用 方括号和下标 标识特定元素

//直接把最后一个元素初始化
int arr[4] = {[3] = 4};//前面说过,其他未初始化的一般会为0

同样C99、C11为结构提供指定初始化器,语法与数组指定初始化器类似,但是结构的指定初始化器用 点运算符和成员名 表示特定元素

struct book library {.value = 10.99//赋给value的值是0.25,因为0.25紧跟author之后,覆盖了10.99.个数组那里一个道理
                     .author = "xiao.ma"	//指定初始化顺序任意
                     0.25}

4.结构数组

一个内含多个结构变量的数组

4.1 声明结构数组

声明结构数组和声明其他类型数组类似

//数组名library本身不是结构名,他是一个数组名
struct book library[MAXBKS];
library[2];//library是第2个book类型的结构变量

以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的结构

4.2 标识结构数组的成员

library[1].values[1]

总结一下:

library;//一个book结构的数组
library[2];//一个数组元素,该元素是book结构
library[2].title;//title成员,该成员是个数组
library[2].title[4];//title数组的第5个元素

4.3程序讨论

manybook.c创建了一个内涵100个结构变量的数组。由于该数组是自动存储类别的对象,其中的信息被存储在栈中。该数组需要很大一块内存,可能会导致一些问题。如果在允许时出现错误,可能抱怨大小或栈溢出。可以通过编译器选项设置栈大小以容纳数组;或创建静态数组,这样就不会把数组放在栈中

/* manybook.c --  包含多本书的图书目录 */
#include<stdio.h>
#include<string.h>
char* s_gets(char* st, int n);
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 100

struct book {
	char title[MAXTITL];
	char author[MAXAUTL];
	float value;
};

int main(void)
{
	//创建结构数组
	struct book library[MAXBKS];
	int count = 0;
	int index;

	puts("Pls enter the book title");
	puts("Press [enter] at the start of a line to stop.");
	while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL
		&& library[count].title[0] != '\0')//第二个判断是否到达文件结尾
	{
		puts("Now enter the author.");
		s_gets(library[count].author, MAXAUTL);
		puts("Now,enter the value.");
		scanf("%f", &library[count++].value);
		while (getchar() != '\n')//清理输入行,scanf会把\n留在缓冲区,下一次输出程序读到会误以为用户输入了停止的信号
			continue;
		if (count < MAXBKS)
			printf("Enter the next title.\n");
	}
	if(count>0)
	{
		puts("Here is the list of ur book:");
		for (index = 0; index < count; index++)
			printf("%s by %s: $%.2f\n", library[index].title,
				library[index].author, library[index].value);
	}
	else
		printf("No books?Too bad.\n");

	return 0;
}


char* s_gets(char* st, int n)
{
	char* ret_val;
	char* find; 

	ret_val = fgets(st,n,stdin);
	if (ret_val)
	{
		find = strchr(st, '\n');
		if (find)
			*find = '\0';
		else
			while (getchar != '\n')
				continue;
	}
	return ret_val;
}

5.嵌套结构

/*friend.c -- 嵌套结构示例*/
#include<stdio.h>
#define LEN 20
const char* msgs[5] =
{
	"  Thank u for the wonderful evening,",
	"U certainly prove that a",
	"is a special kind of guy.We must get together",
	"over a delicious",
	"and have a few laughs"
};

//第1个结构
struct names {
	char first[LEN];
	char last[LEN];
};

//第2个结构
struct guy {
	struct names handle;//嵌套结构,就和普通的int 变量类型一样,只不过这里是结构类型
	char favfood[LEN];
	char job[LEN];
	float income;
};

int main(void)
{
	struct guy fellow = {//初始化一个结构变量
		{"Eweb","Villard"},
		"grilled salmon",
		"personality coach",
		68112.00
	};

	printf("Dear %s, \n\n", fellow.handle.first);//从左向右解释: fello的成员handle→handle的成员first
	printf("%s%s.\n", msgs[0], fellow.handle.first);
	printf("%s%s\n", msgs[1], fellow.job);
	printf("%s\n", msgs[2]);
	printf("%s%s%s", msgs[3], fellow.favfood, msgs[4]);
	if (fellow.income > 75000.0)
		puts("!!");
	else if (fellow.income > 75000.0)
		puts('!');
	else
		puts(".");
	printf("\n%40s%s\n", " ", "See u soon,");
	printf("%40s%s\n", " ", "Shalala");

	return 0;
}

6.指向结构的指针

使用 指向结构的指针 有很多好处:①指针比结构更容易操作②传递指针更有效率③一些结构中包含指向其他结构的指针④结构比较复杂,以前不能作为参数传递,但指针可以

6.1声明和初始化结构指针

声明一个指向struct guy类型的结构

struct guy*him;

注意:和数组名不同,结构变量名并不是结构变量的地址,因此要在结构变量名前面加上&运算符

//假设barney是一个guy类型的结构变量,可进行如下操作
him = &barney;

加入fellow是一个结构数组,这意味着fellow[0]是一个结构,现在让him指向fellow[0]

him = &fellow[0];

指针+1:总是增加所指向类型大小的量,him+1指向fellow[1]

6.2 用指针访问成员

指针him指向结构变量fellow[0],要通过him获得fellow[0]的成员,有两种方法:

  • ->运算符:如果him == &fellow[0],那么him->income == fellow[0].income。前面学过的'.'运算符是跟在结构后面,而不能跟在指向结构的指针后面,所以him.income是不对的

  • 利用&和*的互逆性:fellow[0].income == (★him).income

  • 总之:fellow[0].income == (★him).income == him->income

7.向函数传递结构信息

可以传递 指向结构的指针、结构、结构成员(用指针就可以实现叭)

7.1 传递结构成员

如果结构成员是具有单独值的而数据类型(如int、float),就可作为参数传递

注意:sum函数不关心也不知道实际的参数是否是结构成员,只要求传入数据是对应的double类型

/* funds1.c -- 把结构成员作为参数传递 */
#include<stdio.h>
#define FUNDLEN 50

struct funds {
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};

double sum(double, double);

int main(void)
{
	struct funds stan = {
		"Garlic-Melon Bank",
		4032.27,
		"Lucky's Savings and Loan",
		8543.94,
	};
	printf("Stan has a total of $%.2f.\n",
		sum(stan.bankfund,stan.savefund));

	return 0;
}

double sum(double x, double y)//sum函数不关心也不知道实际的参数是否是结构成员,质押排球传入数据是对应的double类型
{
	return(x + y);
}

前面讲过,如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址

7.2 传递给结构的地址

因为函数要处理funds结构,所以前面必须声明funds结构

/* funds2.c -- 传递指向结构的指针 */
#include<stdio.h>
#define FUNDLEN 50

struct funds {//必须声明funds结构
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};

double sum(const struct funds*);

int main(void)
{
	struct funds stan = {
		"Garlic-Melon Bank",
		4032.27,
		"Lucky's Savings and Loan",
		8543.94,
	};

	printf("Stan has a total of $%.2f.\n", sum(&stan));

	return 0;
}

//const会告诉编译器,该函数不能修改ar指向的数组中的内容,让编译器在 处理数组时 将其视为常量(不是要求原数组是常量)
double sum(const struct funds* money)//形参就是声明了一个变量,这里声明了一个指向funds结构的指针
{
	return(money->bankfund + money->savefund);
}

7.3 传递结构

调用sum()的时,编译器根据funds模板创建了一个名为imoolah的自动结构变量.然后该结构的各成员被初始化为stan结构变量相应成员的值的副本。即程序使用原结构的副本进行计算(指针使用原结构进行计算)

/* funds3.c -- 传递指向结构的指针 */
#include<stdio.h>
#define FUNDLEN 50

struct funds {
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};

double sum(const struct funds );

int main(void)
{
	struct funds stan = {
		"Garlic-Melon Bank",
		4032.27,
		"Lucky's Savings and Loan",
		8543.94,
	};

	printf("Stan has a total of $%.2f.\n", sum(stan));

	return 0;
}


double sum(const struct funds moolah)//调用sum()的时,编译器根据funds模板创建了一个名为imoolah的自动结构变量
{
	return(moolah.bankfund + moolah.savefund);
}

7.4 其他结构特性

数组之间不能赋值。但对于相同类型的结构,C允许把一个结构赋值给另一个结构,即便有成员是数组:

o_data= n_data;

另外还可以把一个结构初始化为相同类型的另一个结构:

struct names right_field = {"Ruthie","George"};
struct names captain = right_field;

现在的C,函数不仅能把结构本身作为作为参数传递,还能把结构作为返回值返回

示例1:通过结构指针双向通信

/* names1.c -- 使用指向结构的指针 */
#include<stdio.h>
#include<string.h>

#define NLEN 30
struct namect {
	char fname[NLEN];
	char lname[NLEN];
	int letters;
};

void getinfo(struct namect*);
void makeinfo(struct namect*);
void showinfo(const struct namect*);
char* s_gets(char* st, int n);

int main(void)
{
	struct namect person;

	getinfo(&person);
	makeinfo(&person);
	showinfo(&person);
	return 0;
}

//1.把信息传递给主调函数
void getinfo(struct namect* pst)
{
	puts("Pls enter ur first name.");
	s_gets(pst->fname, NLEN);//给结构成员赋值,之前讲的是初始化
	puts("Pls enter ur last name.");
	s_gets(pst->lname, NLEN);//pst->lname指向结构的lname成员,等价于char数组名
}

//2.双向传输方式传递信息
void makeinfo(struct namect* pst)
{
	pst->letters = strlen(pst->fname) + strlen(pst->lname);
}

//3.把信息从主调函数传递给自身
void showinfo(const struct namect* pst)//不改变结构内容,前两个函数都改变结构内容
{
	printf("%s %s,ur name contains %d letters.\n",
		pst->fname, pst->lname, pst->letters);
}

char* s_gets(char* st, int n)
{
	char* ret_val;
	char* find;

	ret_val = fgets(st, n, stdin);
	if (ret_val)
	{
		find = strchr(st, '\n');
		if(find)
			*find = '\0';
		else	
			while (getchar() != '\n')
				continue;//注意else内容的缩进
	}
	return ret_val;
}

示例2:通过结构参数和返回值完成相同任务 (每个函数都创建了自己的person北方,所以该程序使用了4个不同的结构,不像之前那样只是用一个结构)

/* names2.c -- 传递并返回结构 */
#include<stdio.h>
#include<string.h>

#define NLEN 30
struct namect {
	char fname[NLEN];
	char lname[NLEN];
	int letters;
};

struct namect getinfo(void);
struct namect makeinfo(struct namect);
void showinfo(struct namect);
char* s_gets(char* st, int n);

int main(void)
{
	struct namect person;

	person = getinfo();
	preson = makeinfo(person);//这里makeinfo返回的person已经不是上一行左侧的person了
	showinfo(person);
	return 0;
}

struct namect getinfo(void)//这个函数创价了自己的结构,下面的函数也创造了自己的结构,方法却不一样
{
	struct namect temp;

	puts("Pls enter ur first name.");
	s_gets(temp.fname, NLEN);
	puts("Pls enter ur last name.");
	s_gets(temp.lname, NLEN);

	return temp;//直接返回一个结构变量
}

struct namect makeinfo(struct namect info)
{

	info.letters = strlen(info.fname) + strlen(info.lname);

	return info;
}

void showinfo(struct namect info)
{
	printf("%s %s,ur name contains %d letters.\n",
		info.fname, info.lname, info.letters);
}

char* s_gets(char* st, int n)
{
	char* ret_val;
	char* find;

	ret_val = fgets(st, n, stdin);
	if (ret_val)
	{
		find = strchr(st, '\n');
		if(find)
			*find = '\0';
		else	
			while (getchar() != '\n')
				continue;//这样可以的	
	}
	return ret_val;
}

7.5 结构和结构指针的选择

使用结构指针较好:

  • 优点:什么版本的C都可以使用过;速度快

  • 缺点:无法保护数据。但新增的const限定符可以保护结构(所以不再是缺点了)

使用结构:

  • 优点:保护原始数据(所以不再是优点了)

  • 确定:老版C无法实现;浪费时间和存储空间,尤其是把大型结构传递给函数且只使用结构中一两个成员时

  • 所以常用于处理小型结构

7.6 结构中的字符数组和字符指针

之前结构中一直用字符数组存放字符串。使用 指向char类型的指针 可以代替字符数组,但是容易有风险、不建议

  • 对于names,字符串都存储在结构内部,结构总共要分配40字节存储姓名

  • 对panmes,字符串存储在编译器存储常量的地方,结构本身只存储了了两个地址,占用16byte,结构不用为字符串分配任何存储空间,使用的是存储在别处的字符串 这样做有危险,因为scanf把字符串放到treas.last表示的地址上,而treas.last未初始化,地址可以是任何值

#define LEN 20
//数组版
struct names{
	char first[LEN];
	char last[LEN];
};

//指针版
struct pnames{
    char * first;
    char * last;
}

struct names veep = {"Talia","Summers"};
struct pnames treas = {"Brad","Fallingjaw"};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值