第一章 C语言

  C语言是一门通用计算机编程语言,也是很多软件开发者的入门语言,应用十分广泛。我们后面章节会介绍iOS开发,其所使用的Object-C语言就是基于C语言的。

  这里有一个C语言视频教程,把C语言介绍的很全面,笔者就不再冗述。

  本章只是将C语言中的各个知识点的语法等细节知识罗列出来,适合已经学习过C语言的读者阅读,如果你完全不懂C语言,那么请先学习上面的视频教程。

第一节 概述

  计算机语言的种类非常的多,总的来说可以分成机器语言汇编语言高级语言三大类。


机器语言

-  机器语言是直接用二进制0、1代码指令表达的计算机语言,一条指令(是用0和1组成的一串代码)就是机器语言的一个语句,由操作码和地址码组成。
-  如:“C7 06 0000 0002”表示在IBM PC上使用的Intel 8x86处理器将数字2移至地址0000(16进制)的指令。
-  缺点:用机器语言编制程序效率低、可读性差,也难以理解、修改和维护。因此人们设计了汇编语言。


汇编语言

-  在汇编语言中,用助记符(Memoni)代替操作码(如使用ADD代表加法),用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。如:“MOV X, 2”。
-  特点:
   -  使用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序。
   -  汇编程序把汇编语言翻译成机器语言的过程称为汇编。
-  缺点:
   -  虽然使用汇编语言编写程序的效率和程序的可读性有所提高,但汇编语言是面向机器的语言,其书写格式在很大程度上取决于特定的计算机机器指令。机器语言与汇编语言又被称为“低级语言”。
-  提示:汇编语言是面向具体机型的,它离不开具体计算机的指令系统,因此,对于不同型号的计算机,有着不同的结构的汇编语言,而且,对于同一问题所编制的汇编语言程序在不同种类的计算机间是互不相通的。


高级语言

-  高级语言是相对于低级语言来说的。由于汇编语言依赖于硬件体系,且助记符量大难记,于是人们又发明了更加易用的所谓高级语言。高级语言基本脱离了机器的硬件系统,用人们更易理解的方式编写程序。
-  特点:
   -  高级语言与计算机的硬件结构及CPU的指令系统无关。
   -  高级语言的代码同样不可以被计算机直接识别,需要使用“编译器”对其编译。
   -  高级语言编译生成的程序代码一般比用汇编语言设计的程序代码要长,执行的速度也慢。
-  常见的高级语言有:C、C++、Java、Python等。
-  高级程序语言所写的代码被称为源程序。但是计算机不能直接识别源程序代码,因此必须将源程序代码翻译成机械语言(0,1代码),只有这样咱们写的代码才能被计算机执行。因此,根据将源程序翻译成机器语言的方式,可以进一步将高级语言划分为:编译型语言和解释型语言。C语言就是编译型语言。


C语言的执行流程
  程序的执行流程为:

-  第一步:建立一个后缀名为.c的文件,然后将代码写入到这个文件中。
-  第二步:使用编译器编译这个.c文件 如果编译通过会生成一个.obj文件。
-  第三步:将这个.obj文件与在该文件中使用的系统库函数或其他目标程序连接起来,形成一个.exe的文件。
-  第四步:用户双击这个.exe文件,将程序送入内存,开始执行这个程序。

  说白了,一个c语言代码要经过:.c.obj.exe三个阶段。


源文件的组成
  C语言程序都是写在若干个后缀名为.c的文件中。一般来说,一个简单的C语言源文件由两部分组成:

-  第一部分:导入头文件语句。
-  第二部分:函数。


  范例1:HelloWorld

1
2
3
4
#include<stdio.h>
main() {
printf("Hello,World!\n");
}

语句解释:
-  首先,要知道,在使用C语言编写的代码中,函数是最基本的组成部分,main(){}就是一个函数。这个函数在程序执行的时候,会被系统自动调用。它是每一个程序的入口。C程序,从它开始执行,也是从它结束。
-  接着,上面的第1行代码是一条导入头文件语句。因为在程序中使用了printf函数,而printf函数是在stdio.h头文件中的,但是编译器却不知道这一点。所以,需要告诉编译器,printf在什么地方,然后编译器会把printf导入进来。
-  最后,printf()函数用来输出数据。 在本例中会向控制台输出一条语句:“HelloWorld”。所谓的控制台就是指程序执行时弹出的dos界面。 再说白点就是那个黑色窗口。


  范例2:输出一个图形。

1
2
3
4
5
6
#include<stdio.h>
main() {
printf("****************************\n");
printf("* 世界,你好 *\n");
printf("****************************\n");
}

语句解释:
-  在C语言里,所有的语句都必须写在函数中。所有的语句都以分号“;”结束。字符 “\n”代表换行的意思。


本节参考阅读:

第二节 基础知识

数据类型、变量和常量


C语言的常见数据类型

-  基本数据类型(8种)
   - 数值类型
      -  整数类型:  short(2字节)、int(4字节)、long(4字节)
      -  浮点数类型:float(4字节)、double(8字节)、long double(16字节)
   - 字符类型:  char(1字节) 
   - 枚举类型
-  指针类型
-  构造类型
   - 数组
   - 结构体
   - 共用体
-  空类型:void


常量
  常量:程序中值不可以改变的量,分为字面常量符号常量

-  字面常量:如 1、'c'、2342f、"String"、false等
-  符号常量:#define PI 3.14


变量
  变量:程序中值可以改变的量。

-  变量就像是一个容器,用来保存一个数据。一个变量同一时间内只能保存一个数据,当为一个变量赋一个新值的时候,变量中的旧值就会被覆盖了。
-  变量是有数据类型的,比如:“int i = 5;”就是定义一个整型的变量,并且这个变量保存一个数字:5。在编程中,“=”代表的是赋值的意思,“==”才是代表判断两个数是否相等。


深入了解变量
  请记住,内存才是程序运行的舞台。所有的程序如果想运行,那么它就必须要被加载入内存,并且得到CPU的控制权。 计算机中,存储数据的硬件叫硬盘,而不叫内存
  其实变量名就代表内存中的一个存储空间,当对一个变量进行赋值的时候,其实就是在将这个值放到变量名所代表的空间中去。 如int i = 10;则此语句的意思就是:将数字10放到i所代表的空间当中去。


标识符规则
  所谓的标识符,就是指程序中:变量函数数组等元素的名称,比如int i=10,就是定义一个名为i的变量,这个i就是一个标识符。

-  标识符必须由大小写字母、下划线、数字组成,且不能以数字开头。
-  C语言中标识符区分大小写。变量a和变量A是不同的两个变量。
-  标识符不能使用C语言的保留字。
-  保留字又称为“关键字”,即保留字就是C语言系统专用的标识符,用户无权使用。如:int、long、if 等。
-  在C语言中标识符的组成,最好不要超过8个字符。如果两个变量名称的前8个字符相同,则编译器可能将它们认为是同一个变量。


注释

-  单行注释   使用: //
-  多行注释   使用: /* */

整型数据

  整型数据分为:整型常量整型变量

-  整型常量,如:45、12、-1 等数字都是整型常量。
-  整型变量,如:“int i ”中的i就是一个整型变量。


整型常量
  整型常量有三种表示方法:

-  十进制表示法。如 1、23、-5等都是十进制表示。
-  八进制表示法。以数字0开头,随后的数不能大于7,如 01、023、-05等都是八进制表示。
-  十六进制表示法。以0x开头,随后的数不能大于F(A代表10,F代表15),如-0x12就等于十进制数-18。


整型变量
  整型变量按照范围可以分为如下三类:

-  基本整型。使用int关键字来定义此类型的变量。
-  短整型。使用short关键字来定义此类型的变量。
-  长整型。使用long关键字来定义此类型的变量。

  C语言标准没有具体规定以上各个数据类型所占的字节数,只要求long型的数据长度不短于int型,int型不短于short型。具体如何实现由计算机系统自行决定。但是一般来说:

-  short型变量占2个字节,取值范围在 -32768 ~ 32767 之间。
-  int型变量在16位的编译器下占2字节,在32、64位编译器下占4字节。当它占据4字节时,取值范围在 -2147483648 ~ 2147483647之间。
-  long型变量在16、32位编译器下占4个字节,在64位编译器下占8字节。

  注意:范围并不是越大越好,除非必要,否则使用int就可以了,使用long型会降低运算速度。


无符号变量
  定义:无符号变量就是指这个变量只能保存正数,不能保存负数。
  作用:保存的数据的正数的范围是有符号变量的2倍。
  语法:unsigned int i = 10;
  解释:如果int4个字节,那么有符号的int就可以表示+-21亿之间的数字,而unsigned int则可以表示0 ~ 42亿之间的数字。
  其他:无符号变量有三种:unsigned intunsigned shortunsigned long
  提示:int a 代表有符号整型变量。signed int a也代表有符号整型变量。其中signed关键字完全可以不写。


整型数据在内存中的表示
  数据在内存中是以二进制(0、1码)形式存放的。
  如果定义一个整型变量int i =10;则会将10转换成它的二进制形式(1010)进行存储。那么变量i的二进制码就是:00000000 00000000 00000000 00001010。注意,若未特别指明,则本章中都将int型数据看作占4个字节。1Byte(字节) = 8 bit(位)。 1bit就代表一位二进制数。

  我们已经知道数据最终是以二进制形式存储在内存中的,如果进一步深入的话,二进制码又可以分为原码反码补码(它们都是二进制形式)。在计算机中,数值都是以补码的形式存在的。

-  正数的补码就是它的原码,也就是它的二进制码。
-  负数的补码 = 其绝对值的补码 所有位上的数全部取反(所谓的取反,就是指0变1,1变0)后 + 1 。
   -  如由于1是正数,所以1的补码就是它的原码。
      -  数字1个补码:00000000 00000000 00000000 00000001
      -  数字1个补码取反:11111111 11111111 11111111 11111110
      -  数字-1的补码就是:11111111 11111111 11111111 11111111

  无符号数的原理就是,最高位(左端)不再代表符号位,而代表数值位。因此无符号变量的在正数范围内的表数范围是有符号数的2倍。


  范例1:加法运算。

1
2
3
4
5
6
7
8
#include<stdio.h>
main() {
// 将数字10赋值给变量x,变量的赋值有点特殊,即从右向左赋值。
int x = 10;
int y = 20;
int z = x + y;
printf("x + y = %d\n",z);
}

语句解释:
-  本例中定义3个整型的变量:x 、y 、z 。
-  在printf函数中,双引号中的数据会被原样输出,因此如果想输出一个数据则需要指定一个标志,以防止该数据被原样输出。
   -  %d 用来输出一个整数(short、int、long)。


数据溢出

  范例1:物极必反。

1
2
3
4
5
6
#include<stdio.h>
main() {
short x = 32767;
short z = x + 1;
printf("z = %d\n",z);
}

语句解释:
-  short占2个字节,其只能表示 -32768 ~ 32767 之间的数。
-  32767的补码为:01111111 11111111 。
-  32767的补码加1后为:10000000 00000000 。而这个编码恰巧又是-32768的补码。
-  当使用%d输出一个整型变量的时候,默认以有符号10进制的形式输出。因此将10000000 00000000转换成-32768输出。
-  定理:达到最小变最大,达到最大变最小。所谓,物极必反。这也就是所谓的数据溢出。


整数默认数据类型
  如果一个整数在-32768 ~ 32767之间则默认为int类型(假设int型占2字节)。
  如果一个整数超过了上面的范围,则默认为long型。如果此时将这个数字赋值给short型变量,则就会数据溢出。
  如果intshort型表数的范围是一致的,则int型的变量同时也是一个short类型的变量。
  在一个整常量后面加一个字母L或者l,则此常量就默认为long型的。

实型数据

  实数又称为浮点数。实型数据分为:

-  实型常量。如:4.5、1.2f、-1. 等都是实型常量。
-  实型变量。如:“float f”中的f就是一个实型变量。


实型常量
  实型常量有两种表示方法:

-  十进制小数形式:
   -  由数字和小数点组成(注意必须要有小数点,但小数点之后,可以没有内容),如 1.2f、2.3、-5. 等都是十进制小数形式。
-  指数形式:
   -  指数形式是实型数据特有的表示方法。比如,12E3或者12e3都是12000的指数形式。
   -  e就代表10的次幂。E3就代表10的3次幂。E和e大小写任意。
   -  e的左右两边都必须有数字,且右边的数字必须是整数。但可以是负整数。
   -  规范化指数形式:在字符e的左面的数字中,小数点的左面有且仅有一位非零数字。如:1.2e2 就是规范化指数形式。


实型变量
  C语言中实型变量分为单精度(float)型、双精度(double)型、长双精度(long double)型。

-  单精度型。使用float关键字来定义此类型的变量。占4字节。
-  双精度型。使用double关键字来定义此类型的变量。占8字节。
-  长双精度型。使用long double关键字来定义此类型的变量。占16字节。


实型数据的舍入误差

  范例1:大家都有极限。

1
2
3
4
5
#include<stdio.h>
main() {
float a = 123456.789e5;
printf("a = %f\n",a);
}

语句解释:
-  一个小数默认为double类型的常量,如果将一个double常量赋值float的变量,则就可能损失数据精度,因为double的表数范围比float的广。因此程序在编译的时候会出警告信息。
-  如果一个小数后面加上一个F或者f ,则该数就默认为float型的常量。如: float f = 1.2f 。 则编译就不会出警告。
-  %f用来输出一个float和double型的变量或常量,输出long double 可以使用%lf 。
-  一个float的变量只能表示7位的有效数字,后面的数字是无意的。就算有的时候,第八位数字也是正确的。但是,无意义。
-  实型数据(float、double) 都会默认保留6位小数。实型数据没有无符号这一说。

字符型数据

  字符型数据分为:

-  字符型常量。如'a'、'\n'等都是字符型常量。
-  字符型变量。如:“char c”中的c就是一个字符型变量。


字符型常量
  C语言的字符型常量是使用单引号括起来的一个字符。如:'a''A'等都是字符型常量。注意'a''A'是不同的两个字符常量。
  除了以上形式的字符常量外,C语言还允许用一个特殊形式的字符常量,就是以一个'\'开头的字符序列。 这种字符被称为:转义字符。如:咱们在前面使用的'\n'就是一个转义字符,代表换行。

1
2
3
4
5
6
7
8
9
10
11
字符形式                                           含义
\n 换行
\t 跳到下一个tab位置。
\b 退一格
\r 回车符。回到当前行的开头。
\f 换页
\\ 输出一个‘\’
\' 输出一个单引号‘'’
\" 输出一个双引号‘"
\ddd 138进制数所代表的字符
\xhh 1216进制数所代表的字符

  提示:

键盘中Tab键一般代表8个空格。但是,不是从当前位置上向后位移8个空格,而是将整个窗口按每8个空格划分为一个tab位,当使用\t的时候,是以当前位置为起点,跳到下一tab位置。


字符型变量
  字符型变量用来存储字符常量。但是一个字符变量只能保存一个字符常量。在C语言中一个字符必须用一对单引号包括起来。

-  如:char ch = 'A';是正确的。
-  如:char ch = 'ab';是错误的。


字符型数据在内存中的表示
  将一个字符常量赋值给一个字符变量。实际上并不是把该字符本身放到内存单元中去,而是将该字符的ASCII码放到存储单元中。如:

-  'A'的ASCII码为65。
-  'a'的ASCII码是97。
-  数字'0'的ASCII码是48。

  既然字符是以ASCII码形式存储的,那么存储的就是一个整数。很显然,这和整型数据在内存中的存储是一样的。


  范例1:他们可以互换。

1
2
3
4
5
6
7
#include<stdio.h>
main() {
char ch = 'c';
int i = 97;
printf("c= %c,i= %c\n",ch,i);
printf("c= %d,i= %d\n",ch,i);
}

语句解释:
-  使用%c可以输出一个字符型的数据。也可以输出一个整型的数据。
-  当使用%c输出一个整数的时候,就先将这个整数转换成ASCII码中的一个字符,然后在输出。
-  返过来也同理。将一个字符变量按照%d的方式输出的时候,会将该字符的ASCII码输出。


  范例2:超过了限制。

1
2
3
4
5
#include<stdio.h>
main() {
char ch = 153;
printf("c= %d\n",ch);
}

语句解释:
-  C语言中有符号的char类型变量的取值范围是-128~127,本范例为一个char类型的变量赋值153,会导致数据溢出。此时,用%d格式符输出时,就会得到一个负整数(如果你使用XCode编译可能不会)。


  范例3:又现无符号。

1
2
3
4
5
#include<stdio.h>
main() {
unsigned int i = 256+97;
printf("%c\n",i); // 输出a
}

语句解释:
-  字符型也有有符号和无符号之分。


字符串常量
  C语言除了允许使用字符常量外,还允许使用字符串常量。字符串常量使用一对双引号包含。如:"abc""张三、李四"


  范例1:HelloWorld

1
2
3
4
#include<stdio.h>
main() {
printf("HelloWorld\n"); //输出一个字符串常量HelloWorld
}

语句解释:
-  注意:'a'和"a"是完全不同的。一个代表字符a,另一个则代表字符串a 。


字符与字符串的区别
  C语言规定,在每一个字符串的结尾都必须有一个“字符串结束标志”,以便系统据此判断字符串是否结束。 这个字符串结束标志,就是字符'\0',这个字符的ASCII码为0,它代表一个“空操作字符”,而且是一个不可显示的字符。


  范例1:所谓的“字符串结束标志”。

1
2
3
4
#include<stdio.h>
main() {
printf("A\0B");
}

语句解释:
-  程序只会输出A,不会再输出B。因为输出A之后,扫描到了\0,则printf函数就认为此字符串结束了,也就停止输出了。


  如果有一个字符串:"a",实际上在内存中存储的是"a\0",此字符串的长度不是1,而是2。 但是不要人为的为每一个字符串都写上一个\0,这个工作系统会自动帮我们做。因此说'a'"a"是完全不同的。

各数据类型混合运算

  整型、实型、字符型可以混合运算。各数据类型的优先级:

1
shortcharintlongfloatdouble


  自动转换:

-  将一个低优先级的常量或变量赋给一个高优先级的变量时,会产生自动转换。
   -  long i=5;      5默认为int型的常量此时赋给一个long型变量,就会进行转换。
   -  double d=2.4f  


  强制转换:

-  将一个高优先级的常量或变量赋给一个低优先级的变量时,需要手工进行强制转换。
   -  int i=(int) 1.5   将double型的常量强制转换成int型,结果为1,即强制截断。


  自动类型提升:

-  a和b作某种运算,先把低级类型转成高级类型后在开始运算。
-  a和b中最高是double,结果就是double 。
-  a和b中最高是float,结果同样是double 。
-  a和b中最高是long,结果就是long 。
-  除此之外,结果都是int 。

运算符与表达式


运算符划分

1
2
3
4
5
6
算术运算符:    +    -    *   /   %    ++  --
关系运算符: > < >= <= == !=
赋值运算符: = += -= *= /= %= ^= &= |= <<= >>=
逻辑运算符: && || ! & |
位运算符: ! & | >> << ^
条件运算符: ? :


运算符优先级




逻辑运算符简介

  C语言提供了三种逻辑运算符:

-  与运算:参与运算的两个数同时为true时,结果才为true。
-  或运算:参与运算的两个数有一个为true时,结果就为true。
-  非运算:参与运算的一个数若为true则结果为false,若为false则结果为true。


  在三种逻辑运算的基础上,又可以分为:简洁运算全部运算

-  所谓的简洁运算就是指,如果第一个参数就能决定表达式的值,则系统不会去查看第二个参数。
-  所谓的全部运算就是指,即使第一个参数能决定表达式的值系统也会去查看第二个参数。


  简洁运算符:&&(简洁与)、||(简洁或)、!(非)。

1
2
3
a && b       // 如果a为false 则不再判断b,表达式的值自动为false。
a || b // 如果a为true 则不再判断b,表达式的值自动为true。
!a // 如果a为true 则表达式的值为false,反过来一样。

  全部运算符:&(与)、|(或)、!(非),其中非的作用与上面一样。


表达式
  与各种运算符相对应的有各种表达式:

-  算术表达式  如: 3 * 4
-  关系表达式  如: 3 > 4
-  赋值表达式  如: a = 4
-  逻辑表达式  如: 3 == 4
-  条件表达式  如: 3 > 4?  true : false
-  位运算表达式  如: 3 | 4


  范例1:整数相除。

1
2
3
4
5
6
#include<stdio.h>
main() {
int i = 3;
int j = 2;
printf("%d\n",i/j);
}

语句解释:
-  程序执行的结果为:1 。 因为在编程中2个整数相除的结果就一定是一个整数。小数部分会被截断。注意:是截断而不是四舍五入。


  范例2:强制类型转换。

1
2
3
4
5
6
#include<stdio.h>
main() {
double d = 5.3f;
int j = (int)d;
printf("%d,%f\n",j,d); // 输出 5,5.300000
}

语句解释:
-  (int)d;仅仅是将变量d中的值取出来后,再将该值转化为int类型,但这个转换操作并不会改变变量b中的值。


  范例3:取模运算。

1
2
3
4
5
6
7
#include<stdio.h>
main() {
int x = 5;
int y = 2;
int z = x % y ;
printf("%d\n",z);
}

语句解释:
-  所谓的取模运算,就是计算两个数的余数。
-  使用 % 进行取模操作。
-  在C语言中,只有整型数据可以进行%运算。实型数据则不可以。


  范例4:自增、自减。

1
2
3
4
5
6
#include<stdio.h>
main() {
int x = 5;
int y = ++x;
printf("%d\n",y);
}

语句解释:
-  ++x表示先使x内保存的值增加1,然后在将x中的新值赋给变量y 。而x++则代表,先将x中的值赋给变量y ,然后在使x中的值增加1 。
-  自减也是同样的道理。


  范例5:不要做无为的挣扎。

1
2
3
4
5
6
#include<stdio.h>
main() {
int x = 3;
int y = (x++)+(x++)+(x++);
printf("%d\n",y);
}

语句解释:
-  在实际开发中不要这么写。


  范例6:赋值语句。

1
2
3
4
5
6
#include<stdio.h>
main() {
int x = 3;
x+=5; // 相当于 x = x + 5;
printf("%d\n",x);
}

语句解释:
-  其他几个用法是一样的。


  范例7:逗号表达式。

1
2
3
4
5
6
#include<stdio.h>
main() {
int x = 5;
int y = (x-=3,x*3,x+5);
printf("%d\n",y); // 输出7
}

语句解释:
-  逗号表达式的值,是最右边的一个表达式的值。
-  并不是任何地方出现的逗号都代表逗号表达式。如:printf(“%d,%d”,5,6)  此时的逗号就只是起到间隔的作用。

第三节 IO与流程控制

C语言IO

  所谓IO其实就是指:输入/输出(input/output) 。在前面程序中一直都在使用的一条语句#include<sidio.h>中的stdio全称就是:标准输入/输出(standard input /output)。
  C语言本身没有提供输入输出语句,输入输出的操作是由函数来完成的。如咱们一直在用的一个printf()函数,就是用来完成输出操作。
  C编译系统C库函数是分开设计的,因此不同的计算机系统所提供的函数的数量、名称、功能不完全相同。不过有些通用的函数(printfscanf等)还是一样的。


字符数据的IO

  范例1:putchar()

1
2
3
4
5
6
7
8
#include<stdio.h>
main() {
int x = 97;
putchar(x);
putchar(98);
putchar('c');
putchar('\n');
}

语句解释:
-  putchar函数也是在stdio.h头文件中定义的。
-  putchar向标准输出流中输出数据,默认是向控制台中输出。
-  putchar顾名思义,就是输出一个字符,字符可以是一个变量、整数、字符常量、转义字符。


  范例2:getchar()

1
2
3
4
5
6
7
8
#include<stdio.h>
main() {
int x ;
x = getchar(); // 当程序执行到此行时,会停住,等待用户输入。假设用户了一个字符'a'。
putchar(x); // 将用户输入的字符'a'打印到控制台。
getchar(); // 读取回车符
putchar(getchar()); // 再次要求用户输入新字符。
}

语句解释:
-  getchar函数也是在stdio.h头文件中定义的。
-  getchar顾名思义,就是从标准输入流中读入一个字符,默认是从键盘中读入。
-  但是有一点要注意,程序中当用户通过键盘输入第一个字符a的时候,会产生2个字符,一个字符是'a'另一个字符是用户按的回车键。因此第6行的一个getchar()用来将回车键给读出来。


  范例3:大小写转换 。

1
2
3
4
5
#include<stdio.h>
main() {
int x =97;
putchar(x-32);
}

语句解释:
-  小写字符的ASCII码比大写字母的ASCII码 大32 。


  范例4:HelloWorld

1
2
3
4
5
#include<stdio.h>
main() {
printf("\"Hello World\""); // 输出 "Hello World"
putchar('\n'); // 输出一个换行符
}

语句解释:
-  双引号必须通过转义符号,才可以输出。单引号也是一样的。


格式化IO
  所谓的格式化IO,就是让程序可以按照我们规定的格式,来输出数据以及读入数据。


  printf函数


  范例1:原样输出。

1
2
3
4
5
6
7
8
#include<stdio.h>
main() {
  // 语法格式:printf(格式控制列表,输出列表),
  // - “格式控制列表”就是一个字符串常量。
  // - “输出列表”会按照顺序,依次替换“格式控制列表”中的格式控制符。
  // “格式控制列表”里面的非格式控制符都会被原样输出,格式控制符就是我们之前用到的“%d %c %f”等等。
printf("Hello World\n",4,5,2); // 输出 Hello World
}

语句解释:。
-  由于在格式控制列表中没有使用格式控制符,因此,就算指定了输出列表,数据也不会被程序输出。


  范例2:格式控制符。

1
2
3
4
#include<stdio.h>
main() {
printf("%s说: %d+%d=%f%c","Tomcat",1,1,2.F,'\n'); // 输出 Tomcat说: 1+1=2.000000
}

语句解释:
-  %s可以输出一个字符串,或者字符数组。
-  %d可以输出一个整数。如:int 、short 、long 。
-  %f可以输出一个浮点数。如:float 、double 、long double。
-  %c可以输出一个字符。即char 。
-  在格式控制列表中出现的所有非格式控制符的字符都会被原样输出,格式控制符会被输出列表中对应的数据所替代。格式控制符以%开头。


  范例3:那要是这样呢?

1
2
3
4
#include<stdio.h>
main() {
printf("%s说: %d+%d=%f%c\n");
}

语句解释:
-  如果不指定输出列表或者输出列表中的参数比格式控制列表中的格式控制符少,程序的结果就是,随机输出数据。
-  至少数值类型(%d %f )是这样的。
-  如果有兴趣,可以测试一下,使用%d输出实型数据是什么结果,然后再试试其他的。虽然这并没有什么卵用,权当娱乐。


  范例4:指定列宽啊。

1
2
3
4
#include<stdio.h>
main() {
printf("%3d,%3d,%3d\n",100,5,123); // 输出 100, 5,123
}

语句解释:
-  如果在%d中间增加一个数字,则代表指定列宽。
-  本例中,使用在%d中间增加一个数字3,代表要输出的数字占3列。
   -  如果数字不足3位,则左端补空格。
   -  如果超过3位,则指定的3就无效了,程序会保证数字正常显示。


  范例5:输出长整型。

1
2
3
4
5
#include<stdio.h>
main() {
long l = 222222222;
printf("%ld\n",l);
}

语句解释:
-  %d是按照int型输出数据的。如果int和long所占的字节不同。则就不能使用%d输出long型的数据,此时需要使用%ld来输出。
-  在VC++6.0中int和long都占4个字节。因此使用%d输出long型数据,完全没有问题。


  范例6:进制啊进制。

1
2
3
4
5
6
#include<stdio.h>
main() {
int i = 10;
printf("%o\n",i); // 输出 12
printf("%x\n",i); // 输出 a
}

语句解释:
-  使用%o将数字以八进制的形式输出,%d以十进制形式输出,%x以十六进制的形式输出。
-  对于八进制和十六进制来说,若想输出进制的符号,则可以加上一个“#”号。如:输出八进制数据“%#o”或十六进制数据“%#x”。


  范例7:无符号数输出。

1
2
3
4
5
#include<stdio.h>
main() {
int i = -10;
printf("%u\n",i); // 输出 4294967286
}

语句解释:
-  使用%u将数据以无符号十进制的形式输出。记住:达到最大变最小,达到最小变最大。


  范例8:右端补空格。

1
2
3
4
5
6
#include<stdio.h>
main() {
float f1 = 5.f;
float f2 = 6.f;
printf("%-10f,%10f\n",f1,f2);
}

语句解释:
-  %f 也可以指定列宽输出。
-  列宽为负数,则右端补空格。如果数字超过指定列宽,则列宽自动失效。
-  实型数据,输出的时候,总是会保留6位小数。


  范例9:%e 。

1
2
3
4
5
#include<stdio.h>
main() {
float f1 = 5123.f;
printf("%e\n",f1);
}

语句解释:
-  按标准化指数形式输出数据。指数部分占3列。
-  程序结果:5.123000e+003


  范例10:%g 。

1
2
3
4
5
#include<stdio.h>
main() {
float f1 = 5123.f;
printf("%g\n",f1);
}

语句解释:
-  %g 就是自动选择符。 其实输出实型数据只有2种方式:%f 和 %e 。
-  使用%g 就是自动选择 %f 和%e 两者中,输出当前这个数据时,所占列最小的一个选择符,然后用他进行输出操作。且不会输出没意义的数字0 。


  范例11:最后的%s 。

1
2
3
4
#include<stdio.h>
main() {
printf("%3.7s\n","HelloWorld Tomcat");
}

语句解释:
-  %s同样可以指定列宽。
-  本例中指定的为3.7 ,含义为:
   -  本字符串占3列。如果超过3列,则指定的列宽自动无效。
   -  但是只输出字符串中的前7个字符。
-  如果想输出%,可以使用“%%”输出。如: printf("%%");


  scanf函数


  范例1:一个数字,10。

1
2
3
4
5
6
7
#include<stdio.h>
main() {
int i;
// scanf(格式控制列表,地址列表)
scanf("%d",&i);
printf("%d\n",i);
}

语句解释:
-  一个变量就代表内存中的一个存储空间,“&”符号,在C语言中有多个含义,此处就是“取得内存地址”的意思。
-  &i 就是把变量i所代表的存储空间,在内存中的地址取出来交给系统,系统再将数据存储到那个空间中去。


  范例2:按规矩来,准没错。

1
2
3
4
5
6
#include<stdio.h>
main() {
int i,j;
scanf("i=%d,j=%d",&i,&j);
printf("i=%d,j=%d\n",i,j);
}

语句解释:
-  在scanf()函数中,所有非格式控制字符,在咱们输入数据的时候都必须原样重写一遍。
   -  由于本例中scannf中的格式控制列表中写的是:"i=%d , j=%d"。
   -  因此咱们输入数据的时候,要原样重写一遍:i=4 , j=1。

  其他:

-  输入数据的时候,不能指定精度。如:scanf(“%7.3f”,&a) 这是错误的。
-  如果在%后面有一个“*”附加说明符,表示跳过它指定的列数。
   -  scanf(“%2d  %*3d %2d”)
   -  其中%*3d就代表读取3位整数,但是不将它赋值给任何变量。具体的解释,请参阅老谭的《C语言程序设计》。
-  如果有scanf(“%d  %d”,&a,&b);两个%d之间存在2个空格键,则输入数据的时候,在两个数字之间必须存在至少2个空格建。


  范例3:计算三角形的面积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<math.h>
main() {
// 面积公式:sqrt(s*(s-a)(s-b)(s-c))
// 提示:其中s = (a+b+c)/2 。 sqrt()函数代表平方根。
float a , b ,c , s ;
double area ;
printf("请输入三角形的三个边长(空格间隔):");
scanf("%f%f%f",&a,&b,&c);
s = (a+b+c)/2;
area = sqrt(s*(s-a)*(s-b)*(s-c));
printf("面积为:%.2f\n",area);
}

// 请输入三角形的三个边长(空格间隔):3 4 6
// 面积为:5.33

语句解释:
-  本例中scanf()函数中的格式控制列表为"%f%f%f" 其内的3个%f之间没有任何字符,函数scanf()默认的间隔符就是空格,因此输入的数据的时候,相邻的数据间只需要间隔一个空格即可。
-  函数sqrt()是math头文件中的函数。因此需要导入math.h文件。
-  本例中printf("面积为:%.2f\n",area); 的含义为:
   -  输出一个实型变量 area ,但是要保留两位小数。即.2代表保留两位小数的意思。
   -  %5.3f 含义:输出一个浮点数,数字占5列,并且该数以保留3位小数的形式输出。

结构化程序设计

  在结构化程序设计中有三种结构:顺序结构分支结构循环结构


顺序结构
  所谓的顺序结构,说白了就是指,程序从函数的开头开始顺序执行,一直到函数的结尾。


  范例1:一元二次方程ax2+bx+c=0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<math.h>
main(){
float a , b ,c ;
double x1, x2, temp;
printf("请输入a,b,c三个数(逗号间隔):");
scanf("%f,%f,%f",&a,&b,&c);
temp = sqrt(b*b - 4*a*c);
x1 = (-b + temp )/2*a;
x2 = (-b - temp )/2*a;
printf("x1=%.2f,x2=%.2f\n",x1,x2);
}

// 请输入a,b,c三个数(逗号间隔):1,3,2
// x1=-1.00,x2=-2.00


分支结构
  分支结构又称为选择结构。在C语言中有两种实现方式: 单分支(if)和多分支(switch)。


  范例1:if语句。

1
2
3
4
5
6
7
8
#include<stdio.h>
main(){
int a = 5;
// 表达式 a>=5 的返回值为1;而1就是非0,而非0就是真。
if(a>=5){
printf("a>=5\n");
}
}

语句解释:
-  在C语言中没有boolean型数据,而使用非0和0来代表真与假。
-  如果if语句中的表达式的值为非0(正数和负数均可)则执行if语句中的子语句。


  范例2:闰年。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
main(){
int year = 2004;
if((year%4==0 && year%100!=0) || (year%400==0)){
printf("是闰年");
}else{
printf("不是闰年");
}
putchar('\n');
}

语句解释:
-  一个年份如果能整除4但是不能整除100则是闰年。
-  一个年份如果能整除400则也是闰年。
-  否则就是平年。


  范例3:else if语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
main(){
int year = 2004;
if(year<=1000){
printf("小于等于1000");
} else
if(year<=2000){
printf("小于等于2000");
} else {
printf("无法计算");
}
putchar('\n');
}

语句解释:
-  if-else其实是一个语句,它们是一体的。
-  if单独使用的时候,它代表一条语句。当if和else配合使用的时候,它们就组合成为了一条语句。
-  else是不可以单独拿出来使用的,if却可以。


  范例4:switch语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
main(){
int score = 89;
switch(score/10){
case 10: case 9:
printf("优秀");break;
case 8:
printf("良好");break;
case 7:case 6:
printf("一般");break;
default:
printf("不及格");break;
}
putchar('\n');
}

语句解释:
-  程序会依次使用switch()中的变量与case后的常量进行匹配。
   -  如果匹配成功则,会执行case后面的语句。
   -  如果匹配失败则,会继续匹配下一个case 。
-  匹配成功后,不会在重新匹配,会一直执行完之后的所有语句。因此需要break关键字。
-  如果所有的case都失配了,则会执行dedault后面的语句。如果没有写default关键字,则程序会直接跳出switch语句。继续执行switch之后的其他语句。
-  如果多个case的处理方法是相同的,可以将多个case放在一起写。
-  case和default的排列顺序可以任意。


  范例5:条件运算符。

1
2
3
4
5
6
#include<stdio.h>
main(){
int a = 10 , b;
b = a>5? 100:200;
printf("%d\n",b);
}

语句解释:
-  条件运算符: 表达式1? 表达式2:表达式3
   -  如果表达式1的值为真,则此整个条件表达式的值为表达式2个值。
   -  如果表达式1的值为假,则为表达式3的值。


循环结构
  循环结构:分为直到循环和当型循环。

-  直到循环:程序流程一旦执行到循环体,至少会循环一次,使用do-while()实现。
-  当型循环:只有当条件满足时,循环体才会执行,最少时一次都不执行。使用while和for来实现。

  循环结构在C语言中有四种实现方式:gotoforwhiledo-while


  范例1: 不招人喜欢的goto

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
main(){
int i = 1,sum = 0;
loop:
sum += i;
i+=1;
if(i<=100)
goto loop;
printf("1加到100的和为:%d\n",sum);
}

语句解释:
-  关于goto了解即可,loop是一个标签,可以任意修改。


  范例2:for语句。

1
2
3
4
5
6
7
8
#include<stdio.h>
main(){
int i ,sum = 0;
for(i=1;i<=100;i++){
sum += i;
}
printf("1加到100的和为:%d\n",sum);
}

语句解释:
-  for语句的语法:
   -  for(表达式1;表达式2;表达式3)
      -  表达式1用来初始化循环控制变量。
      -  表达式2用来判断循环是否应该结束。
      -  表达式3用来迭代循环控制变量。
-  其中3个表达式都可以省写,但是2个分号不能省写。如果省写表达式2,则默认为真。


  范例3:while语句。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
main(){
int i=0 , sum = 0;
while(1){
i++;
sum += i;
if(i==100){
break;
}
}
printf("1加到100的和为:%d\n",sum);
}

语句解释:
-  在循环语句中可以使用break跳出当前层的循环。 如果只有一层循环,则直接跳出去。程序会接着从循环语句之后的第一条语句开始执行。
-  while()和if语句是一样的。只要表达式的值为非0,则循环就会一直进行下去。
-  for循环中也可以使用break语句。


  范例4:do-while语句。

1
2
3
4
5
6
7
8
#include<stdio.h>
main(){
int i=101 ,sum = 0;
do{
sum +=i;
}while(i<=100);
printf("和为:%d\n",sum);
}

语句解释:
-  程序本意是如果i<=100才执行循环体。程序i的值初为101,是不应该执行循环体的。
-  但是由于do-while()语句的特殊性。至少执行一次,然后再判断循环条件。因此程序会输出101。
-  注意,do-while();最后的那个分号不可以丢掉。


  范例5: 九九乘法表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
main(){
int i , j ;
for(i=1;i<=9;i++){
for(j=1;j<=9;j++){
if(j<=i){
printf("%d*%d=%2d ",j,i,(i*j));
}else{
break;
}
}
putchar('\n');
}
}

语句解释:
-  本例就使用了双层循环+break关键字。


  范例6: continue

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
main(){
int i ;
for(i=1;i<=10;i++){
if(i%2==0){
continue;
}else{
printf("%d ",i);
}
}
putchar('\n');
}

语句解释:
-  continue关键字。用来结果本层循环体的当前一轮循环。


  范例7: 做个题吧,输出如下图形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
main(){
int space=8,xing=-1;
int i,j,k;
for(i=1;i<=7;i++){
if(i<=4){
space -= 2;
xing += 2;
}else{
space += 2;
xing -= 2;
}
for(j=1;j<=space;j++){
putchar(32); //空格符的ASCII码为32
}
for(k=1;k<=xing;k++){
printf("* ");
}
putchar('\n');
}
putchar('\n');
}

第四节 数组

  所谓的数组,其实就是多个类型相同的变量,组合而成的一个连续的序列。
  数组是有数据类型的,如:整型数组、实型数组、字符数组。它们分别能保存整数、实型数据、字符型数据。


一维数组

  范例1:整型数组。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
main(){
int array[10],i;
for(i=0;i<10;i++){
array[i]=i;
}
for(i=0;i<10;i++){
printf("%d ",array[i]);
}
putchar('\n');
}

语句解释:
-  在C语言中数组的中括号必须放在数组的名字后面,并且在定义数组的同时需要指定数组的长度。数组的长度只能是常量或者符号常量,不能是变量。
-  数组的元素下标从0开始,到数组的长度-1结束。 本例中数组的长度为10,因此10个元素的下标依次从0~9。


  范例2:定义的同时初始化。

1
2
3
4
5
6
7
8
9
#include<stdio.h>
main(){
int array[10]={1,2,3,4,5};
int i;
for(i=0;i<10;i++){
printf("%d ",array[i]);
}
putchar('\n');
}

语句解释:
-  数组可以在定义的同时初始化,在此种初始化数组的方式中,只要给其中一个元素赋值,那么其他元素就会自动赋默认值。
-  未赋值的元素值:
   -  整型数组默认为0。
   -  float型数组默认为0.0f 。
   -  字符型数组默认为‘\0’。
-  如果数组中的元素未赋值,但是却直接输出数组中的元素,则输出的数据是不确定的。


  范例3:数组的初始化2.0。

1
2
3
4
5
6
7
8
9
#include<stdio.h>
main(){
int array[]={1,2,3,4,5};
int i;
for(i=0;i<5;i++){
printf("%d ",array[i]);
}
putchar('\n');
}

语句解释:
-  定义数组的同时,为数组初始化,可以不指定数组的长度。此时数组的长度,根据元素的个数,自动确定。


  范例4:斐波那契。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
main(){
int array[20]={1,1};
int i;
for(i=2;i<20;i++){
array[i] = array[i-1]+array[i-2];
}
for(i=0;i<20;i++){
printf("%d ",array[i]);
}
putchar('\n');
}

语句解释:
-  所谓的斐波那契数列就是指,一个数列的前两个数是1,从第三个数开始,每个数都是其前两个数之和。


二维数组

  范例1:二维数组。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
main(){
int array[5][4]={0};
int i,j;
for(i=0;i<5;i++){
for(j=0;j<4;j++){
printf("%d ",array[i][j]);
}
putchar('\n');
}
putchar('\n');
}

语句解释:
-  上面定义一个5行4列的二维数组,二维数组的长度同样不能是变量。
-  遍历二维数组需要使用两层循环。所谓的遍历:就是指将数组的所有元素都,使用一遍。


  范例2: 二维数组倒置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
main(){
// 可以使用一个大括号来为二维数组初始化。
int array[3][3]={1,2,3,4,5,6,7,8,9};
int i,j,temp;
printf("倒置之前数组为:\n");
for(i=0;i<3;i++){
for(j=0;j<3;j++){
printf("%d ",array[i][j]);
}
putchar('\n');
}
for(i=0;i<3;i++){
for(j=i+1;j<3;j++){
temp = array[i][j];
array[i][j] = array[j][i];
array[j][i] = temp;
}
}
printf("倒置之后数组为:\n");
for(i=0;i<3;i++){
for(j=0;j<3;j++){
printf("%d ",array[i][j]);
}
putchar('\n');
}
putchar('\n');
}

语句解释:
-  所谓的数组倒置,就是指将数组行上和列上的元素交换位置。
-  在一个数组内部进行倒置,要求这个数组的行和列的长度必须相同。


字符数组
  字符数组,主要用来存储字符串。

  范例1:"HelloWrold"

1
2
3
4
5
#include<stdio.h>
main(){
char str[] = "Hello World";
printf("%s\n",str);
}

语句解释:
-  将一个字符串赋值给一个字符数组是完全可以的。
-  程序会自动将字符串中的每一个字符都拆分出来,分别赋值给对应的数组元素,此时字符数组的最后一个字符就是‘\0’。可以使用%s操作符,来输出一个字符数组。


  范例2:字符数组。

1
2
3
4
5
6
7
8
#include<stdio.h>
main(){
char str[] ={'a','b','c'};
int i;
for(i=0;i<3;i++)
putchar(str[i]);
putchar('\n');
}

语句解释:
-  此时的用法就和整型数组是一样的了。


  范例3:键盘接受一个字符串。

1
2
3
4
5
6
#include<stdio.h>
main(){
char str[23];
scanf("%s",str);
printf("%s\n",str);
}

语句解释:
-  使用%s可以从键盘接受一个字符串。但是字符串中不能包含空格键,遇到空格程序会认为这个是%s的间隔符,因此就停止继续读字符。
-  从程序中发现,接受字符串的时候,scanf()函数中的str并没有使用“&”符号,进行取地址操作。 那么,请记住,数组的名称就代表这个数组中第一个元素的地址。当程序使用%s从键盘读取字符串的时候,会将读到的字符依次赋值到这个数组的第一个元素开始及之后的位置中去。
-  使用scanf()函数为一个数组赋值一个字符串之前,必须先为数组指定长度。


  范例4:简单点吧!!!!!

1
2
3
4
5
#include<stdio.h>
main(){
char str[]="简单一点吧";
puts(str);
}

语句解释:
-  使用puts()函数可以输出一个字符数组或字符串常量,并在最后输入一个换行符‘\n’。


  范例5:再简单点吧。

1
2
3
4
5
6
#include<stdio.h>
main(){
char str[23];
gets(str);
puts(str);
}

语句解释:
-  使用gets()函数,可以读取一个字符串,并且字符串中可以包含空格键。
-  如果有兴趣可以尝试一下,输入字符串的长度比数组的长度大,程序会是什么情况?
-  注意,在C语言中,每一个字符串都有一个结束标记。即使使用gets()函数读取也不例外,程序同样会自动在其后面增加一个‘\0’。


  范例6:字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<string.h>
main(){
char str[33];
int len ;
gets(str);
len = strlen(str);
printf("长度为:%d\n",len);
}
//程序输入:张三Hello
//长度为:11

语句解释:
-  函数strlen()用来计算一个字符串的实际字符个数,不包括‘\0’。
-  值得注意的是,不同的编译器输出的内容可能不同,有的编译器认为一个汉字占2个长度,所以程序可能会输出9。
-  函数strlen()是string.h头文件中的,puts()和gets()是stdio.h中的函数。


  范例7:字符串连接。

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<string.h>
main(){
char str1[30]="abcd";
char str2[]="XYZ";
strcat(str1,str2);
puts(str1); // 输出:abcdXYZ
}

语句解释:
-  strcat()连接两个字符串,将第二个参数连接到第一个参数的后面。第一个字符串数组要有足够的长度。


  范例8:字符串copy。

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<string.h>
main(){
char str1[30]="abcd";
char str2[]="XYZ";
strcpy(str1,str2);
puts(str1); // 输出:XYZ
}

语句解释:
-  strcpy()将第二字符串中的内容,赋值到第一个字符数组中去。并且在最后增加一个‘\0’,第一个字符串数组要有足够的长度。
-  前面说了数组的名称就是这个数组中第一个元素的地址,数组名说白了就是一个地址常量。它是一个具体的数字,因此不能通过如下语句,复制字符串。
   -  str1 = str2 
-  这是错误的。因为它们都是代表一个地址常量。常量是不可改变的。它和“5=15”是一样的。


  范例9:字符串copy2.0。

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<string.h>
main(){
char str1[]="abc";
char str2[]="XYZ";
strncpy(str1,str2,2);
puts(str1); // 输出:XYc
}

语句解释:
-  函数strncpy()将str2中的前n个字符,复制到str1中去。


  范例10:字符串比较。

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<string.h>
main(){
char str1[]="183";
char str2[]="100";
int i = strcmp(str1,str2);
printf("%d\n",i); // 输出:8
}

语句解释:
-  如果字符串1 == 字符串2 则返回0 。
-  如果字符串1 > 字符串2 则返回一个大于0的数。
-  如果字符串1 < 字符串2 则返回一个小于0的数。

第五节 函数


函数的定义
  一个C语言编写的程序,总体的结构如图所示:



  函数是C语言的基本单位。一个源程序文件却是一个编译单元。函数的分类:

-  按用户使用的角度看,函数有两种:
   -  标准函数,即库函数。 是由系统提供的,用户不必自己定义这些函数,可以直接使用它们。不过不同的C系统提供的库函数的数量和功能是不完全相同的。但一些基本的函数是相同的。
   -  用户自定义函数。
-  从函数的形式看,函数同样分为两种:
   -  无参函数。 就是调用此类函数的时候,不需要给函数传递参数。
   -  有参函数。 就是调用此类函数的时候,需要给函数传递参数。


  基本语法:

1
2
3
4
5
6
7
函数返回值类型  函数名(参数列表){
执行语句
}

函数分为:函数首部和函数体。
- 函数首部为上面的:“函数返回值类型 函数名(参数列表)”
- 函数体为为上面的:“{ 执行语句 }”


  范例1:定义一个函数。

1
2
3
void print(){
puts("Hello World");
}

语句解释:
-  本例中定义一个函数,函数名为print 。函数不接受任何参数。函数的返回值为void,即没有返回值。


函数的调用

  范例1:加法计算。

1
2
3
4
5
6
7
8
9
#include<stdio.h>

void plus(int a,int b){
printf("%d\n",a+b);
}
main(){
int a = 5,b = 5;
plus(a,b);
}

语句解释:
-  调用的函数的时候,直接使用函数名调用即可。如果需要传递参数,直接传递即可。
-  实参:调用函数时,传递过去的参数。plus(a,b) 其中a和b就称为实参。
-  形参:函数定义时,规定了函数所能接受的参数。
   -  void plus(int a,int b) 中的a和b就是形参,形参和实参的名称可以不同。


  范例2:函数的返回值。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

int add(int a, int b){
int c = a + b ;
return c;
}
main(){
printf("%d\n",add(5,3));
printf("%d\n",add(4,3));
}

语句解释:
-  如果函数有返回值,则可以使用return关键字将值返回到函数的调用处。
-  函数返回值的类型要和return返回的数据类型相兼容。如果return语句返回的数据的类型与函数返回值类型不同,则以函数返回值类型为准。
-  如果一个函数没有写返回值类型,则默认为返回int类型。


  调用函数分两种情况:

-  调用库函数。要先导入该库函数所在的头文件。
-  调用自定义函数。又分为两种情况:
   -  被调函数定义的位置在主调函数之前。直接在主调函数使用函数名调用被调函数即可。
   -  被调函数定义的位置在主调函数之后。需要先在调用语句之间先重写一遍函数原型后,再使用函数名调用被调函数。


  范例3:重写函数原型。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

main(){
int add(int a,int b);
printf("%d\n",add(5,3));
printf("%d\n",add(4,3));
}
int add(int a, int b){
int c = a + b ;
return c;
}

语句解释:
-  此时,函数add()称为被调函数。main()函数称为主调函数。
-  所谓的函数原型就是指,函数的首部。
-  由于被调函数的定义位置,在主调函数之后。所以在主调函数中就重写了一遍被调函数的函数原型。这就是在告诉编译器,在本函数中调用的那个函数,已经在后面定义了。
-  如果不在主调函数内重写函数原型,则编译的时候会出现警告信息。具体阅读谭浩强《C语言程序设计》一书。


  问题:为什么要重写一遍函数原型?
  老谭的答案:

-  函数原型的主要作用是利用它,可以在程序的编译阶段对调用函数的合法性进行全面的检查。
-  在编译源程序文件的时候,是从上到下逐行执行的,如果没有对函数的声明,当编译到上例中的“printf("%d\n",add(5,3));”语句的时候:
   -  第一,编译系统不知道add是不是函数名。
   -  第二,就算它是一个函数,但是也无法判断实参5和3的类型及个数是否和函数add( )的函数首部一致。
-  因而在编译阶段无法进行正确性的检查。只有在运行时,才会发现实参与形参的类型或个数不一致,出现运行错误。在运行中发现错误,然后重新修改程序,是十分麻烦的。
-  当咱们在主调函数中对被调函数进行了声明后,编译系统就记下了这个函数的首部,当编译到被调函数的时候,就会拿出刚才声明的函数原型和被调函数的首部进行比较。如果不相符,则直接在编译阶段就报错。

  事实上,重写函数原型时,可以不指定形参的名称,因为编译器根本不会去记录形参的名称,比如:int add(int,int);


  范例4:声明所有原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>

int add(int,int); //在此处声明函数原型。
int minus(int, int); //本文件中的所有函数都可以相互调用其他函数。
int multiply(int, int); //因为编译器已经记住了这些函数的函数原型。

main(){
printf("%d\n",add(5,3));
printf("%d\n",minus(5,3));
printf("%d\n",multiply(5,3));
}
int add(int a, int b){
int c = a + b ;
return c;
}
int minus(int a, int b){
int c = a - b ;
return c;
}
int multiply(int a, int b){
int c = a * b ;
return c;
}

语句解释:
-  如果在所有函数之外对本文件中的函数的所有函数原型都进行声明。那么所有函数都可以任意调用其他函数了。


  范例5:数组作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>

void print(int a[]){
int i ;
for(i=0;i<10;i++){
printf("%d ",a[i]);
}
}
void alter(int a[]){
int i;
for(i=0;i<10;i++){
a[i] = a[i] +i;
}
}
main(){
int a[] = {1,2,3,4,5,6,7,8,9,0};
alter(a);
print(a);
}

语句解释:
-  在函数调用的时候,如果传递的实参是普通的变量,则进行的是值传递。值传递时,相当于将实参的值复制一份给形参,此时函数形参的值改变,不会改变实参的值。
-  在函数调用的时候,如果传递的实参是数组名,则进行的是地址传递。地址传递时,相当于将实参和形参同时指向一块内存空间,此时函数形参的值改变,则也会改变实参的值。


  范例6:二维数组作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>

void print(int a[][3]){
int i,j ;
for(i=0;i<3;i++){
for(j=0;j<3;j++){
printf("%d ",a[i][j]);
}
putchar('\n');
}
}
main(){
int a[][3] = {1,2,3,4,5,6,7,8,9};
print(a);
}

语句解释:
-  如果一个二维数组作为函数的形参,则形参数组的中括号只能省写最高维的长度。


局部变量和全局变量
  局部变量:在一个函数内部定义的变量称为局部变量。局部变量只能在其所在的函数中内引用。函数的形参也是局部变量。
  全局变量:在函数外部定义的变量称为全局变量。全局变量的作用范围从其定义的位置开始,到文件的末尾。一般都会将全局变量名的第一个字母大写。除非必要,一般来说应减少定义全局变量。


  范例1:全局变量。

1
2
3
4
5
6
#include<stdio.h>

int a =2;
main(){
printf("%d",a);
}

语句解释:
-  在main()函数中引用全局变量a 。如果a的定义位置在main()函数之后,则main()函数就无法引用变量a。


  范例2:全局变量与局部变量同名。

1
2
3
4
5
6
#include<stdio.h>
int a =2;
main(){
int a = 3;
printf("%d",a); // 输出:3
}

语句解释:
-  此时,在局部变量的作用范围内,全局变量被屏蔽。程序输出:3 。


变量的存储类别
  从变量的作用域可将变量分为:局部变量全局变量
  从变量的存在时间可将变量分为:

-  静态存储方式:此类变量,在程序运行期间都不会被释放,程序运行结束的时候,这些变量才会消失。
   -  全局变量属于静态存储方式,在程序开始执行的时候,会给全局变量分配存储区,程序执行完毕就释放。
-  动态存储方式:此类变量,在程序运行的期间,会不断的创建、消失。
   -  函数的形参:在函数调用的时候,会分配空间,函数调用结束,这些变量就自动消失了。
   -  自动变量:在一个函数中,未使用static关键字修饰的变量都是自动变量。咱们前面一直使用的都是自动变量。


  在C语言中每一个变量和函数都有两个属性:数据类型数据的存储类别

-  数据类型:int、float等。
-  数据的存储类别:数据在内存中的存储方法,总体分为静态存储和动态存储两大类。具体分为四类:
   - 自动的(auto)
   - 静态的(static)
   - 寄存器的(register)
   - 外部的(extern)


  范例1:自动变量。

1
2
3
4
5
#include<stdio.h>
main(){
auto int a = 3;
printf("%d",a);
}

语句解释:
-  auto关键字写于不写都没所谓。


  有时希望函数中的局部变量,在函数调用结束的时候,其内的值不会消失。再下次再调用此函数的时候,这个变量的值可以继续使用。 那么就可以使用static关键字修饰这个变量。


  范例2:阶乘。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>

int fact(int i){
static int sum =1;
sum*=i;
return sum;
}
main(){
int i = 1;
for(;i<10;i++){
printf("%d!=%d\n",i,fact(i));
}
}

语句解释:
-  此时变量sum的值在函数结束调用的时候不会被清空。静态局部变量是在编译时赋初值的,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值,而是保留上次函数调用结束时的值。
-  如果在定义局部变量的同时不为变量赋初值:
   -  对于静态变量来说,编译时自动赋初值0或者空字符‘\0’
   -  对于自动变量来说,它的值是一个不确定的值。
-  虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。
-  静态变量的生存期长,存储时要多占内存,降低程序可读性等,因此如非必要,否则不要过多使用。


  首先,要知道,计算机中速度最快的存储器,是CPU中的寄存器。
  然后,在一般情况下,变量的值是存在内存中的。
  接着,当程序中用到哪一个变量的值时,由CPU的控制器发出指令,将内存中的该变量的值送到CPU的运算器中。
  最后,运算结束后,如果需要存储,则再从运算器中将数据送到内存中去。
  因此,如果一些变量要频繁使用(如一个函数循环10000000次,每次都引用某一个变量的值),则此时程序因为存取变量的值,就浪费了不少时间。
  解决的方法:将一个变量定义成寄存器变量,寄存器是在CPU内部的,定义一个寄存器变量,就是将一个变量的值放到CPU的寄存器当中,以此来减少存取变量值的时间。使用register关键字声明。


  范例3:寄存器变量。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

main(){
register int i = 1;
int sum = 0;
for(;i<100000;i++){
sum+=i;
}
printf("%d\n",sum);
}

语句解释:
-  只有形参和自动变量可以使用register关键字修饰,局部静态变量不可以使用register关键字修饰。
-  一个计算机系统中的寄存器的数目是有限的。
-  当今优化过的编译器会自动将使用频繁的变量放到寄存器中去,因此对寄存器变量了解即可。


  外部变量,就是咱们前面所说的全局变量。外部变量的作用域从其定义位置开始,到源程序文件结束。


  范例4:扩大全局变量的作用域。

1
2
3
4
5
6
#include<stdio.h>
extern a;
main(){
printf("%d\n",a);
}
int a = 23;

语句解释:
-  使用extern关键字来扩大全局变量的作用范围。这个和重写函数原型的思路是类似的。
-  在使用的时候也可以加上全局变量的类型:“extern int a;”,它的含义为:告诉编译器变量a在本文件中的后面或者在其他文件中定义。
-  编译器遇到“extern int a;”这条语句的时候,会先在本文件中查找变量a,如果没有发现,则会在连接时从其他文件中找,如果还找不到,按出错处理。如果找到了,则将此变量的作用域扩展到本文件中来。


  有时希望一个全局变量只能在本文件中被引用,则可以使用static关键字修饰这个全局变量,那么此时,即使在别的文件中使用extern关键字,也无法引用本文件中的这个变量。


  范例5:static全局变量。

1
2
3
4
5
#include<stdio.h>
static int a = 23;
main(){
printf("%d\n",a);
}

语句解释:
-  此类变量也被称为静态外部变量。
-  全局变量本身就是静态存储方式,加上static只不过是限制了变量的作用范围而已。
-  static修饰的局部变量是使一个变量从动态存储方式变为了静态存储方式。


内部函数和外部函数
  函数本质上都是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数不能被其他文件调用。

-  内部函数:一个函数不能被其他文件所引用。
-  外部函数:一个函数能被其他文件所引用。


  范例1:内部函数。

1
2
3
4
5
6
7
#include<stdio.h>
static void print(){
puts("Hello World");
}
main(){
print();
}


  范例2:外部函数。

1
2
3
4
5
6
7
#include<stdio.h>
extern void print(){
puts("Hello World");
}
main(){
print();
}

语句解释:
-  如果一个函数没有使用static关键字修饰(或者使用extern关键字修饰)则就称其为外部函数。
-  其实函数原型的含义,就是告诉编译器:此函数在本文件中的后面或者在其他文件中定义。编译器只需要先记住这个函数原型即可。

第六节 预处理命令

  首先,预处理命令是为了提高C语言的编程效率而存在的。
  然后,预处理命令不隶属于C语言,C语言的编译器无法编译预处理命令。
  接着,预处理命令是在程序编译之前,就已经“预处理”好了。
  最后,当编译器开始编译源文件的时候,源文件中以及没有预处理命令了。
  说白了,一个源文件需要如下过程:预处理 → 编译 → 连接。
  C语言中,提供的预处理命令主要有如下三种:宏定义文件包含条件编译


宏定义
  宏定义,使用关键字#define来定义一个宏。


  范例1:符号常量。

1
2
3
4
5
6
#include<stdio.h>
#define X 5
#define Y 5
main(){
printf("%d\n",X+Y);
}

语句解释:
-  本范例定义了X和Y两个宏,然后在程序中通过这个别名来引用这个常量,在预处理的时候,会把源程序中所有的X和Y都替换成5。
-  宏定义就是进行简单的替换操作,这个替换的过程被称为:“宏展开”。


  范例2:替换?

1
2
3
4
5
6
#include<stdio.h>
#define PT printf("%d\n"
main(){
PT,10);
PT,20);
}

语句解释:
-  以本例来说,它仅仅是在编译之前将源程序中所有的PT换成了“printf("%d\n"”,每次替换后,都会使源程序的代码量增长。
-  使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量。
-  使用宏名可以达到一改全改的目的。
-  宏定义只负责进行字符串替换,不会去检查语法是否合法。如果替换后的语句不合法,那么在编译阶段就可以查出来。
-  宏定义不是C语言的语句,因此最后不应该加上分号。如果加了分号,则会将分号一起替换。
-  宏定义的作用域,从其定义位置开始,到源文件结束。
-  使用#undef命令可以终止宏定义的作用域。


  范例3:替换风暴。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#define PI 3.1415926
#define L PI*2*r
#define S PI*r*r
#define PT printf("%f\n"
main(){
int r = 5;
PT,L);
PT,S);
}

语句解释:
-  在进行宏定义时,可以用已定义的宏名,可以层层置换。
-  在L和S中都使用到了r,请自行体会。


  范例4:有些事,强求不得。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#define PI 3.1415926
#define L PI*2*r
#define S PI*r*r
#define PT printf
main(){
int r = 3;
PT("L=%f\n",L);
PT("S=%f\n",S);
}

语句解释:
-  在双引号“”内的字符,即使和宏名是一样的,也不会进行宏名替换。


  范例5:带参宏。

1
2
3
4
5
6
7
#include<stdio.h>
#define PI 3.1415926
#define AREA(r) PI*r*r
main(){
int r = 3;
printf("S=%f\n",AREA(r));
}

语句解释:
-  没什么新鲜的玩意。 宏展开时就是:PI*3*3 然后将这个式子放到下面的程序中去。
-  带参宏宏名和括号之间不能存在空格。
-  总之记住一句话:宏定义就是字符串替换,然后以不变应万变。


  范例6:这需要点技巧。

1
2
3
4
5
6
#include<stdio.h>
#define PI 3.1415926
#define AREA(r) PI*(r)*(r)
main(){
printf("S=%f\n",AREA(1+4));
}

语句解释:
-  本例中将PI中的r用括号给括起来了。
-  如果不括起来,在宏展开时,结果:3.1415926*1+4*1+4 。显然,这是不行的。


  带参宏和函数的区别:

-  参数传递不同:
   -  函数调用的时候,会先计算出实参表达式的值,然后代入形参。
   -  带参宏调用的时候,只是进行字符替换。如上面的范例6。 
-  处理时间不同:
   -  函数调用是在程序运行时处理的,分配临时的内存单元。
   -  宏展开是在编译之前进行的,在展开时并不会分配内存单元,不进行值传递,也没有“返回值”的概念。
-  参数类型要求不同:
   -  函数调用的时候,形参和实参都要定义类型,二者的类型都要求一致。
   -  宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表。宏定义时,字符串可以是任何类型的数据。
-  调用一个函数只能得到一个返回值,一个带参宏可以设法得到多个返回值。


  范例7:多返回值。

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#define PI 3.1415926
#define AREA(l,s,r) l=PI*2*r;s=PI*(r)*(r)
main(){
double l , s ,r=3;
AREA(l,s,r);
printf("L=%f\n",l);
printf("S=%f\n",s);
}


文件包含
  所谓的“文件包含”处理,是指一个源文件可以将另一个的全部内容包含进来,即将另外的文件包含到本文件之中。C语言提供了#include命令用来实现“文件包含”的操作。
  语法:

-  #include "文件名"
   -  通知预编译器去系统存放C库函数头文件所在的目录中去寻找要包含的文件。这种方式称为标准方式。若查找不到则报错。
-  #include <文件名>
   -  通知预编译器先在当前文件所在的目录下查找文件,如果查找不到,则再按标准方式进行查找。若仍找不到,则报错。


  范例1:print.c

1
2
3
4
#include<stdio.h>
void print(){
puts("Hello World");
}


  范例2:hello.c

1
2
3
4
#include "print.c"
main(){
print();
}

语句解释:
-  首先,不要编译print.c文件,直接编译hello.c文件即可。
-  然后,一个include命令只能指定一个被包含的文件,如果要包含多个文件,要使用多个include命令。
-  最后,被包含文件与其所在的文件,在预编译后,已经成为同一个文件,而不是两个文件。因此,如果file2.h中有全部静态变量,它也在file1.c文件中有效。
-  使用文件包含命令的时候,就是将文件的内容导入到include命令所在的位置(可以导入a.txt,可以导入多次)。
-  文件包含是可以嵌套的。如:f1中包含了f2,而f2中又包含了f3,那么在f1中就可以使用f3中定义的函数、宏定义等对象了。


条件编译
  一般情况下,源程序中所有的行都参加编译,但是有时希望对其中一部分内容只在满足一定条件才进行编译(这样可以缩小生成的exe文件的大小),也就是对一部分内容指定编译的条件,这就是“条件编译”
  比如当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。


  范例1:条件编译 。

1
2
3
4
5
6
7
8
9
10
//#define NO1
#ifdef NO1
main(){
puts("世界,你好!");
}
#else
main(){
puts("Hello World");
}
#endif

语句解释:
-  如果程序中存在了宏名为NO1的宏定义 则输出“世界你好”,否则输出“HelloWorld”。
-  程序中,可以没有#else关键字,但是必须要有#endif关键字。


  范例2:如果“未定义” 。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#ifndef ABC
main(){
puts("世界,你好!");
}
#else
main(){
puts("Hello World");
}
#endif

语句解释:
-  如果程序中未定义名为ABC的宏 则就输出“世界你好”。


  范例3:如果“表达式为真” 。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#if 0
main(){
puts("世界,你好!");
}
#else
main(){
puts("Hello World");
}
#endif

语句解释:
-  如果if之后的表达是为真(非0) 则输出“世界,你好”

第七节 指针


地址和指针的概念

  问:什么是地址?
  答:在内存中,每一块区域都有一个编号。从内存中存、取数据的时候,就是按照这个编号进行的。而这个编号,就称为是地址。简单的说:地址就是内存中的一个位置编号。

  问:什么是指针?
  答:说白了,指针就是地址。 那为什么将地址称为指针? 这是术语,或者说,这样说更形象化。

  问:什么是变量的指针?
  答:变量的指针又称为变量的地址。顾名思义,一个变量所代表的那块存储空间在内存中的位置编号,就是变量的指针。

  问:什么是指针变量?
  答:所谓的指针变量,首先它是一个变量,但是这个变量有点特殊,保存的值不是基本数据类型的常量,而是变量的地址(也就是变量的指针)。


指针与变量


  范例1:使指针变量指向一个变量。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
main(){
// 指针变量和普通变量的定义格式不同,指针变量名称的前面都要增加一个*号。
// 下面定义一个指针变量p,这个变量是int类型的,用来保存一个int型变量的地址。
int *p;
int i = 10;
// 使用“&”符号用来获取变量的地址(指针)。
p = &i;
// 使用“*”符号用来获取指针变量所指向的内存空间中的值。
// 输出指针变量p中的值。
printf("%d\n",*p);
}

语句解释:
-  首先,指针变量只能保存地址,它不能保存一个常量:“p = 123;”是条错误的语句,编译时会报错。
-  然后,int类型的指针变量只能保存int类型变量的地址,不能保存其他类型的变量地址,否则,编译时出警告,执行时数据会不正确。
-  接着,咱们在scanf()函数中就使用“&”符号将变量的地址取出,在此也是如此。

  提示:

-  如果有 p = &a 则:&a == &*p ;
-  其实这很好证明:
   -  将“p = &a”代入到“&a == &*p ;”中得:
   -  &a == &*&a
   -  等式两边同时约掉& 得: a == *&a
   -  由于:&和*是互逆运算,因此化简得: a == a


  范例2:比较大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
main(){
int a,b,*p1,*p2,*temp;
printf("请输入2个数,比较大小(空格间隔):");
scanf("%d%d",&a,&b);
p1 = &a; // p1指向a
p2 = &b; // p2指向b
if(*p1>*p2){ // 如果a的值比b大
temp = p1; // 则让p1指向b
p1 = p2; // p1总是指向值小的变量。
p2 = temp;
}
printf("从小到大排序为:%d,%d\n",*p1,*p2);
}


  范例3:指针变量与函数参数。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
void change(int *p){
*p = 100;
}
main(){
int a = 10;
change(&a);
printf("a=%d\n",a);
}

语句解释:
-  以前,如果函数的实参不是数组名,则进行的是值传递。形参值改变了,不会影响到实参。
-  现在,使用了指针后,传递的是地址。此时,实参和形参指向同一个内存空间,当形参的值改变时,实参的值也会随着改变。


  范例4:不会有人范的错误。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void change(int *p){
int i = 100;
p = &i;
}
main(){
int a = 10,*p;
p = &a;
change(p);
printf("a=%d\n",*p);
}

语句解释:
-  形参指针改变指向不会影响到实参指针,请仔细体会这一点。


指针与数组

  问:什么是数组指针变量?
  答:所谓的数组指针变量,首先它也是一个指针变量。它指向数组中的第一个元素在内存中的地址。

  问:什么是指针数组?
  答:所谓的指针数组,就是它就一个数组,但是这个数组有点特殊,其内的每一个元素都是一个指针变量。


  范例1:遍历数组—下标法。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
main(){
int a[] = {1,2,3,4},*p,i;
p = a;
for(i=0;i<4;i++){
printf("%d ",p[i]);
}
putchar('\n');
}

语句解释:
-  数组名就是一个指针变量,它指向了数组中第一个元素的内存地址。
-  我们可以使用*a来访问第一个元素的值,也可以将一个数组名赋值给一个指针变量,即p[i]和a[i]是等价的。


  范例2:遍历数组—地址法。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
main(){
int a[] = {1,2,3,4},*p,i;
p = a;
for(i=0;i<4;i++){
printf("%d ",*(p+i));
}
putchar('\n');
}

语句解释:
-  首先,要知道,指针变量+1不是简单的+1,而是加上数据类型所占的字节数。
-  本例中,p是int型的,假设int型占4个字节,那么p+1就意味着跳过4个字节。
   -  p的值始终没有改变。程序中只是不断的在p的基础上,增加一个位移量,来达到遍历数组的目的。
   -  使用下标法遍历的时候,其实就是先将下标p[i] 转换成*(p+i) 后,在输出的。


  范例3:遍历数组—指针法。

1
2
3
4
5
6
7
8
#include <stdio.h>
main(){
int a[] = {1,2,3,4},*p;
for(p=a;p<a+4;p++){
printf("%d ",*p);
}
putchar('\n');
}

语句解释:
-  请仔细体会。


  范例4:这是二维数组。

1
2
3
4
5
6
7
#include <stdio.h>
main(){
int a[][2] = {1,2,3,4},*p;
for(p=a[0];p<a[0]+4;p++){
printf("%d ",*p);
}
}

语句解释:
-  二维数组在内存中也是顺序存放的。如一个2行2列的二维数组a,a[0][1]后面紧跟着就是a[1][0] 。
-  所以可以使用一个普通的指针变量遍历二维数组。
-  二维数组中a[0]和&a[0][0]和a的地址是相同的。


  范例5:指向一维数组的指针变量。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
main(){
int a[][2] = {1,2,3,4},(*p)[2],i,j;
p = a;
for(i=0;i<2;i++){
for(j=0;j<2;j++){
printf("%d ",*(*(p+i)+j));
}
putchar('\n');
}
}

语句解释:
-  本例中“(*p)[2]”代表:建立一个int类型的指针变量,它指向一个一维数组,并且这个一维数组,每行中的元素的个数都是2 。
-  关于指针,在本节的最后会有一个总结,如果有兴趣可以现在就去看看。


  范例6:字符串指针变量。

1
2
3
4
5
#include <stdio.h>
main(){
char *p = "张三:你好!";
puts(p);
}

语句解释:
-  将一个字符串常量,赋给一个字符指针,其实就是将字符串中首字母的内存地址赋值给这个指针变量。
-  当使用puts()输出这个指针变量的时候,会从第一个位置开始输出字符,直到遇到‘\0’为止。


  范例7:错误的范例。

1
2
3
4
5
6
#include <stdio.h>
main(){
char p[22] ;
p = "张三:你好!";
puts(p);
}

语句解释:
-  数组名代表一个地址常量,不可以被赋值。
-  但是如果把“char p[22]”换成 “char  *p” 则不会出现问题了。因为指针变量是一个变量。
-  如果没有为数组初始化,则数组名指向的位置是不确定的。


  范例8:字符串复制。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
main(){
char str1[20] ;
char str2[] = "12345678";
char *p1,*p2;
p1 = str1;
p2 = str2;
while(*p1++=*p2++);
puts(str1); // 输出:12345678
}

语句解释:
-  非0即为真。如果str2到达串尾,则会将‘\0’赋值给str1,此表表达式的值为0 。因此循环也就结束了。


  范例9:指针数组。

1
2
3
4
5
6
7
8
#include <stdio.h>
main(){
char *p[4]={"张三","李四","王五","赵六"};
int i ;
for(i=0;i<4;i++){
puts(*(p+i));
}
}

语句解释:
-  本类中char *p[4] 用来定义一个具有4的元素的指针数组。由于[]的优先级比*号高,所以p先和[]结合,组成一个数组。然后再和*结果,最终成为一个指针数组。


  范例10:指向指针变量的指针变量。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
main(){
char *p="张三";
char **p1;
char ***p2;
p1 = &p;
p2 = &p1;
puts(**p2);
}

语句解释:
-  使用2个*号来定义一个指向指针变量的指针变量。
-  使用3个*号来定义一个指向指向指针变量的指针变量的指针变量。这个…有点不常见。


  范例11:指针数组排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <string.h>
void sort(char *p[4],int n){
int i , j , k;
char *temp;
for(i=0;i<n-1;i++){
k = i;
for(j=i+1;j<n;j++){
if(strcmp(p[k],p[j])>0){
k = j;
}
}
if(k != i){
temp = p[k];
p[k] = p[i];
p[i] = temp;
}
}
}
main(){
char *p[4]={"a张三","c李四","d王五","b赵六"};
int i ;
sort(p, 4);
for(i=0;i<4;i++){
puts(*(p+i));
}
}


  范例12:使用“指向指针变量的指针变量”遍历数组。

1
2
3
4
5
6
7
8
#include <stdio.h>
main(){
char *p[4]={"张三","李四","王五","赵六"};
char **p1 ;
for(p1=p;p1<p+4;p1++){
puts(*p1);
}
}



  小结:

  指针一共有几种形态:

1
2
3
4
5
形式                                  解释
int *p 普通的指针变量。
int (*p)[4] 指向一维数组的指针变量。
int *p[4] 指针变量数组。
int **p 指向指针变量的指针变量,即二级指针。


  普通的指针变量:int * p

-  如果a是一个普通的变量,p可以保存a的地址:int *p = &a;
-  如果a是一个普通的数组,p也可以保存a的首地址:int *p = a;
-  如果a是一个二维数组,由于二维数组的特点(所有元素排列在一起),p甚至可以指向二维数组:int *p = *a(或者a[0]);
-  变量p只是一个普通的指针变量,它最多只能指向一排数据。


  指向一维数组的指针变量:int (*p)[4]

-  如果a是一个普通的变量,p不可以指向a的地址,因为p至少保存的是一维数组的地址。
-  如果a是一个一维数组,p可以指向a的地址:int (*p)[4] = &a,输出数组中具体的元素使用:*(*p+i)
-  如果a是一个二维数组,p可以直接指向a: int (*p)[4] = a,输出数组中具体的元素使用:*(*(p+i)+j)。


  指针数组:int *p3[4]

-  指针数组是若干个连续的指针变量组成的集合。每一个元素都保存一个地址。
-  用法:
  -  char *p3[] = {"张三","李四","王五","赵六"}
  -  int *p3[]= {&a[0], &a[1], &a[2], &a[3]}


  指向指针变量的指针变量(二级指针):int **p4,二级指针只能指向指针变量的地址。


  小结中的总结:

-  int *p与一维数组是平级的,因为可以将一维数组名直接赋值给p。
-  int (*p)[4]与一个二维数组是平级的,因为可以将二维数组名直接赋值给p。
-  二级指针和指针数组是平级的,因为可以将指针数组名直接赋值给二级指针。


指针与函数

  问:什么是函数指针?
  答:顾名思义,函数指针就是一个指针变量,它指向一个函数在内存中的入口地址。

  问:什么是指针函数?
  答:所谓的指针函数,就是指一个函数,它的返回值是一个指针变量。


  范例1:函数指针。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
void print(char *str){
puts(str);
}
main(){
// 函数指针的定义语法:函数返回值类型 (*指针名)()。
void (*p)();
p = print;
(*p)("Tomcat-1");
p("Tomcat-2");
}

语句解释:
-  本例中定义一个函数指针p,p指向一个没有返回值(void)的函数。
-  语句“p = print;”将函数print的入口地址赋给指针变量p,此时p+1是无意义的。
-  调用函数的时候,直接使用指针名或者(*指针名)都可以调用。


  范例2:函数指针作为形参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int add(int a,int b){
return a+b;
}
int minus(int a, int b){
return a-b;
}
int calls(int a,int b,int (*p)()){
return p(a,b) ;
}
main(){
printf("add=%d\n",calls(5,4,add));
printf("minus=%d\n",calls(5,4,minus));
}


  范例3:指针函数。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
// 指针函数的定义语法:函数返回值类型 *函数名(参数表)。
int* plus(int a,int b){
int c,*p;
c = a+b;
p = &c;
return p;
}
main(){
printf("plus=%d\n",*plus(5,5));
}

语句解释:
-  本例中定义一个返回int*类型的函数,因此在输出的时候需要使用*号。


  指针变量可以有空值int *p = NULL,在stdio.h文件中就有NULL的宏定义:#define NULL 0

第八节 结构体与共用体

结构体

  有很多时候,需要在程序中表示一个现实中的事物,比如,一个学生管理系统,需要在程序中模拟一个学生的实体。此时就可以使用结构体。


  范例1:定义结构体变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

// 使用struct关键字来定义一个结构体,下面定义一个名为Student1的结构体。
// name、age、sex都称为是结构体的成员、域、字段,这个结构体所占的字节数,是所有成员所占字节数的总和。
struct Student1{
char name[20];
int age;
char sex[3];
};

// 定义一个名为Studen2的结构体,同时定义两个结构体变量stu1, stu2。
struct Student2{
char name[20];
int age;
char sex[3];
} stu1, stu2;

// 定义一个匿名的结构体,用它来创建一个结构体变量stu3。此结构体只能使用一次。
struct {
char name[20];
int age;
char sex[3];
} stu3;

int main(){
// 创建一个i额Student1类的结构体变量。
struct Student1 stu0;
}

语句解释:
-  结构体中的成员也可以是一个结构体。


  范例2:初始化变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu .age,stu.sex);
}
main(){
struct Student stu ={"张三",40,"男"};
print(stu);
}

语句解释:
-  引用成员的格式为:结构体变量名.成员名。
-  如果结构体的成员本身也是一个结构体,则需要一级一级的找下去:stu.birthday.day


  范例3:定义的同时初始化变量。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
}stu ={"张三",40,"男"};
void print(struct Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}
main(){
print(stu);
}


结构体数组

  范例1:鸟枪换炮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#define SIZE 3
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}
main(){
struct Student stu[SIZE] = {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
};
int i ;
for(i=0;i<SIZE;i++){
print(stu[i]);
}
}

语句解释:
-  定义结构体数组的语法和普通数组一样。


  范例2:定义的同时初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
}stu[] = {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
};
void print(struct Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}
main(){
int i ;
for(i=0;i<3;i++){
print(stu[i]);
}
}


指针与结构体

  范例1:结构体变量指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student *p){
printf("姓名:%s\t年龄:%d\t性别:%s\n",(*p).name,(*p).age,(*p).sex);
}
int main(){
struct Student stu = {"张三",40,"男"};
print(&stu);
}

语句解释:
-  使用指针引用结构体变量的成员的语法:(*指针名).成员名。


  范例2:换种方式吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student *p){
printf("姓名:%s\t年龄:%d\t性别:%s\n",p->name,p->age,p->sex);
}
int main(){
struct Student stu = {"张三",40,"男"};
print(&stu);
}

语句解释:
-  指针变量可以使用“->”(一个减号加一个大于号)来引用成员。


  范例3:指向结构体数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student *p){
printf("姓名:%s\t年龄:%d\t性别:%s\n",p->name,p->age,p->sex);
}
int main(){
struct Student stu[] = {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
},*p;
for(p=stu;p<stu+3;p++){
print(p);
}
}

语句解释:
-  咱们前面总结的int *p和一维数组是平级的。
-  值得注意的是p+1意味着跳过n个字节,这个n是根据指针p的类型来决定的。如果p是char类型的指针,则跳1个字节。在本例中p是struct Student类型的指针,因此跳过的是27个字节(int占4个字节)。


  范例4:也许并是不27字节。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
void print(struct Student *p){
printf("姓名:%s\t年龄:%d\t性别:%s\n",p->name,p->age,p->sex);
}
int main(){
printf("%d\n",sizeof(struct Student)); // 输出:28
}

语句解释:
-  使用sizeof关键字来计算一个类型所占的字节数。指针类型的数据类型,所占的字节数与当前操作系统所支持的内存的容量有关。32位操作系统中,sizeof(int*)通常占4字节。
-  在VC++6.0中结果返回的是28字节,而且仔细试了试,发现每增加4个字节,结果才变一次。不管是27字节还是28字节,只要知道p+1的含义即可。至于跳多少字节,还是老话,自己探索。


  范例5:它们并不是一回事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
};
int main(){
struct Student stu[] = {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
},*p;
p = (struct Student *)&stu[0].age;
printf("年龄:%d\n",*p);
p++;
printf("年龄:%d\n",*p);
}

语句解释:
-  语句“p = (struct Student *)&stu[0].age;”指向的是结构体中的一个成员的地址。
-  但是由于age是int类型的变量,而p是struct Student类型的指针,他们不兼容,因此进行了强制类型转换。
-  此时p+1 就意味着跳到下一个学生的age成员上面。

链表


  范例1:简单链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
struct Student{
char name[20];
int age;
char sex[3];
struct Student *next;
};
struct Student* insert(struct Student *head,struct Student *temp){
struct Student *p1,*p2;
p1 = p2 =head;
while(p2!=NULL){
p1 = p2;
p2 = p2->next;
}
if(head == NULL){
head = temp;
}else{
temp->next = NULL;
p1->next = temp;
}
return head;
}
void print(struct Student *head){
struct Student *p = head;
while(p != NULL){
printf("姓名:%s\t年龄:%d\t性别:%s\n",p->name,p->age,p->sex);
p = p->next;
}
}
int main(){
struct Student *head=NULL,stu[] = {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
};
head = insert(head,&stu[2]);
head = insert(head,&stu[0]);
print(head);
putchar('\n');
head = insert(head,&stu[1]);
print(head);
}

语句解释:
-  学习编程要始终记住一句话:想创新,先模仿。
-  最开始学C的时候,就是照着老谭的链表“模仿”。
-  对咱们这行来说,前人的经验都是,各位前辈知识精华,所以,不要怀疑咱们前辈们的智商。但是,也不要一直走不出自己的风格来。
-  链表属于《数据结构》的范畴。 在此不对其深入讲解。

共用体

  一块内存空间被多个变量共同使用,就是所谓的共用体。

  范例1:定义共用体。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
// 使用union关键字来定义一个共用体。
union data{
int age;
float score;
char name[20];
};
int main(){
union data e1;
e1.age=20;
e1.score=98;
printf("年龄:%d\t成绩:%f\n",e1.age,e1.score);
}

语句解释:
-  共用体的定义语法和结构体的语法类似,引用成员的语法也和结构体类似。
-  共用体变量中的所有成员共同使用一块空间。 也就是说,共用体变量所占的字节数,由其成员中占最大字节数的那个成员来决定。
-  共用体中的所有成员,在同一时间中,只有一个成员有效。
-  本例中,先给成员age赋值,后给score赋值。则此时age中的值就无效了,内存中目前保存的是score变量的值。
-  共用体变量中起作用的成员是最后一次存放数据的成员。在为某一成员存入一个新值后,原有的成员就失去作用了。
-  共用体变量的地址和它的各成员的地址都是相同的。
-  不能把共用体变量作为函数的参数,也不能使函数带回共用体变量,但可以使用指向公用体变量的指针。
-  共用体类型可以出现在结构体中,也可以定义共用体数组。反之,结构体也可以出现在共用体中。

枚举类型

  枚举类型是C语言的基本数据类型。


  范例1:定义枚举类型。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
enum Color{
BLUE,
RED,
GREEN
};
int main(){
enum Color color = RED;
printf("%d,%d,%d\n",BLUE, color, GREEN);
}

语句解释:
-  使用enum关键字来定义一个枚举类型。
-  枚举类型中的成员,被称为“枚举常量”。枚举常量间用逗号间隔,最后一个常量后面不需要逗号。
-  建立一个枚举变量,语法和结构体类似。
-  每个枚举常量都有一个编号,从0开始依次排列。


  范例2:赋值数字。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
enum Color{
BLUE,
RED,
GREEN
};
int main(){
enum Color color = (enum Color)2;
printf("%d\n",color);
}

语句解释:
-  将2强制类型转换为(enum Color)类型。


  范例3:改变顺序。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
enum Color{
BLUE=100,
RED,
GREEN
}
main(){
enum Color color = RED;
printf("%d\n",color);
}

语句解释:
-  此时RED的编号就是101 。

Typedef关键字

  Typedef关键字,用来给一个已经存在的数据类型,设置一个别名。


  范例1:改名Integer

1
2
3
4
5
6
7
#include <stdio.h>
// 给int定义一个别名Integer。
typedef int Integer;
int main(){
Integer i = 5;
printf("%d\n",i);
}

语句解释:
-  就是相当于给int换了一个名字而已,然后用这个新名字去定义一个int类型的变量。


  范例2:谁说没用的?

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
typedef struct{
char name[20];
int age ;
char sex[3];
} Student;
int main(){
Student stu = {"张三",40,"男"};
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}

语句解释:
-  关键字typedef在结构体中最常用,此时定义变量的时候就可以省写很多字符了。


  范例3:定义一个数组类型。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
typedef int Integer[3];
int main(){
Integer i ={1,2,3};
int x;
for(x=0;x<3;x++){
printf("%d ",i[x]);
}
}

语句解释:
-  typedef不是在定义新类型,只是给已有的类型起一个别名。

第九节 位运算


  范例1:与运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
printf("%d ",1&1); // 输出:1
}
/*
00000000 00000001
00000000 00000001
---------------------------------
00000000 00000001
*/

语句解释:
-  与运算,参与运算的两位数,同为1,则结果才为1 。


  范例2:或运算。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(){
printf("%d ",3|4); // 输出:7
}
/*
00000000 00000011
00000000 00000100
---------------------------------
00000000 00000111

*/

语句解释:
-  或运算,参与运算的两位数,有一个为1,则结果就为1 。


  范例3:非运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
printf("%d ",~-1); // 输出:0
}

/*
11111111 11111111
---------------------------------
00000000 00000000
*/

语句解释:
-  非运算,按位取反,1变0,0变1。


  范例4:异或运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
printf("%d ",3^6); // 输出:5
}
/*
00000000 00000011
00000000 00000110
---------------------------------
00000000 00000101
*/

语句解释:
-  异或运算,参与运算的两位数,相同则为0,不同则为1 。


  范例5:左移运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
printf("%d ",3<<2); //将3左移2位。输出12
}

/*
00000000 00000011
---------------------------------
00000000 00001100
*/

语句解释:
-  在数据不溢出的前提下,左移一位,相当于乘以2 。位运算不能对float和double型常量进行操作。


  范例6:右移运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
printf("%d ",12>>2); //将12右移2位。输出3
}

/*
00000000 00001100
---------------------------------
00000000 00000011
*/

语句解释:
-  在数据不溢出的前提下,右移一位,相当于除以2 。
-  如果不能整除2,则小数部分直接舍弃。如11>>2的最终结果为2 。

第十节 文件操作


C文件概述


  在C语言中,根据数据的组织形式,将文件分为:

-  ASCII文件:又称为文本文件,他的每一个字节都用来存储一个ASCII字符。
-  二进制文件:把内存中数据按其在内存中的存储形式原样输出到磁盘上存储。
   -  比如有一个整数10000,在内存中占4个字节,如果按照ASCII码形式输出,则占5个字节(每一位数字都被当作一个字符),而按二进制输出,在磁盘上只占4个字节。


  以前,C语言有两种对文件的处理方法:

-  缓冲文件系统:系统自动在内存区中为每一个程序中正在使用的文件开辟一个缓冲区。
   -  输出数据时:从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。
   -  读入数据时:从磁盘向内存读入数据,一次从磁盘文件将一批数据输入到内存缓冲区(直到充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量)。缓冲区的大小各个C版本互不相同,一般为512字节。
-  非缓冲文件系统:系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。

  一般来说,都是用缓冲文件系统处理字符文件,用非缓冲文件系统处理二进制文件。现在,ANSI C标准规定不采用非缓冲文件系统,而只采用缓冲文件系统。也就是说,既用缓冲文件系统处理文本文件,也用它来处理二进制文件。

文件指针

  程序运行时,每一个被使用的文件都会在内存中开辟一个区域,用来存放与该文件的有关信息(文件的名称、状态、文件的当前位置等)。
  这个区域由系统定义在stdio.h中的,取名为FILE的结构体的变量来表示,一个文件对应一个FILE类型的结构体变量。


  范例1:文件的打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main(){
/*
文件打开的语法格式:
FILE *fp;
fp = fopen(文件所在位置,打开的方式)
*/
FILE *fp = NULL; // 定义
fp = fopen("D:\\a.txt","w+"); // 初始化
// 如果文件打开成功,则在控制台输入一行文本。
if(fp == NULL){
puts("the file open lost");
}
// 关闭文件。fclose()函数当关闭成功时返回0,关闭失败则返回EOF(即-1),可以使用ferror函数来测试。关闭文件的之前,会先刷空缓冲区。
fclose(fp);
}

语句解释:
-  fopen()函数带回一个FILE*类型的返回值,此时文件指针fp就指向了这个文件。


  范例2:文件读写方式。

-  用“r”方式打开文件:
   -  第一,只能从文件中读,不能向文件中写。
   -  第二,文件必须已经存在,否则就出错。
-  用“w”方式打开文件:
   -  第一,只能向文件中写,不能从文件中读。
   -  第二,如果文件不存在,则尝试建立这个文件。如果文件存在,则删除它,然后重新建立一个新文件。
-  用“a”方式打开文件:
   -  第一,a是append的缩写,意味着追加。打开文件时,位置指针自动跳到文件末尾。
   -  第二,文件必须已经存在,否则无法追加,而且会报错。
-  用“r+”、 “w+”、 “a+”方式打开文件:
   -  第一,它们都是用读写的方式开打一个文本文件。可以读,也可以写。
   -  第二,但是有所不同的是:
      -  以“r+” 打开, 文件必须存在,否则出错。
      -  以“w+” 打开,文件如果不存在,则新建,如果存在,则删除重建。
      -  以“a+” 打开,文件必须存在,否则出错。追加写。


  如果不能实现“打开”任务,fopen()函数会返回NULL,意味着文件打开时出错了。错误的原因:以“r”的方式打开一个不存在的文件、磁盘出现故障、磁盘已满无法建立新文件等。
  有的C版本不用“r+”“w+”“a+”方式打开文件,而用“rw”“wr”“ar”方式打开文件。
  在程序开始运行时,系统自动打开3个标准文件,通常这3个文件都与终端相联系。因此以前我们所用到的从终端输入或输出都不需要打开终端文件。系统自定义了3个文件指针stdinstdoutstderr分别指向标准输入标准输出标准出错输出。如果程序中指定要从stdin所指的文件读入数据,就是指从终端键盘读入数据。


文件读写

  范例1:写一个字符。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
FILE *fp = NULL;
fp = fopen("D:\\a.txt","w");
if(fp != NULL){
printf("%c\n",fputc('C',fp));
fclose(fp);
}
}

语句解释:
-  使用fputc(数据,目标文件)函数向文件中写一个字符:
   -  写入成功,返回写入的字符。
   -  写入失败,返回EOF。 EOF是在stdio.h中定义的符号常量,其值为 -1。
-  其实putchar()是一个宏定义,它利用了fputc()函数。putchar()在stdio.h中定义的。


  范例2:读一个字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
FILE *fp = NULL;
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
}
while(!feof(fp)){
putchar(fgetc(fp));
}
putchar('\n');
fclose(fp);
}

语句解释:
-  文件读到结尾的时候,会返回-1 。一般可以使用EOF来判断。
   -  但是,有的时候,在读取的某一个数据的值却可能是-1 ,此时再使用EOF来判断显然是不行的。
   -  因此以后统一使用feof()函数来判断文件是否到达结尾。如果到达文件结尾,则返回1,否则返回0 。
-  使用fgetc()读取一个字符,可以读汉字。


  范例3:写一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
typedef struct{
char name[20];
int age ;
char sex[3];
}Student;
int main(){
FILE *fp = NULL;
Student stu[]= {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
};
// 使用wb方式打开文件,数据将以二进制的形式保存到文件中,二进制的文件无法被阅读。
fp = fopen("D:\\a.txt","wb");
if(fp == NULL){
puts("the file open lost");
}
fwrite(stu,sizeof(Student),3,fp);
fclose(fp);
}

语句解释:
-  使用fwrite()函数向文件中写一个数组。
-  函数原型:fwrite(buffer,size,count,fp)
   -  buffer 即数据源。  
   -  count  写出的单位的个数。
   -  size 单位数据所占的字节。  
   -  fp 目标文件。
-  用一段话来解释:从buffer中取出count个size大小的数据,写入到fp所指向的文件中去。


  范例4:读一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#define COUNT 3
typedef struct{
char name[20];
int age ;
char sex[3];
}Student;
void print(Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}
int main(){
FILE *fp = NULL;
Student stu[COUNT];
int i ;
fp = fopen("D:\\a.txt","rb");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
fread(stu,sizeof(Student),COUNT,fp);
for(i=0;i<COUNT;i++){
print(stu[i]);
}
fclose(fp);
}

语句解释:
-  函数exit(0) 代表终止程序执行。 其在stdlib.h中定义。
-  使用fread()函数读取数据,用一句话来解释:从fp所指向的文件中读取COUNT个sizeof(Student)大小的数据,存放在stu中。
-  一般使用fread()和fwrite()读写字节(二进制)文件。


  范例5:格式化输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
typedef struct{
char name[20];
int age ;
char sex[3];
}Student;
#define COUNT 3
int main(){
FILE *fp = NULL;
Student stu[]= {
{"张三",40,"男"},
{"李四",43,"男"},
{"王五",41,"男"}
};
int i ;
fp = fopen("D:\\a.txt","wb");
if(fp == NULL){
puts("the file open lost");
}
for(i=0;i<COUNT;i++){
fprintf(fp,"姓名:%s\t年龄:%d\t性别:%s\r\n",stu[i].name,stu[i].age,stu[i].sex);
}
fclose(fp);
}

语句解释:
-  fprintf 函数原型:fprintf(文件指针,格式列表,输出列表)
-  注意,windows记事本的换行符是“\r\n”而不是单纯的“\n”。


  范例6:格式化输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#define COUNT 3
typedef struct{
char name[20];
int age ;
char sex[3];
}Student;
void print(Student stu){
printf("姓名:%s\t年龄:%d\t性别:%s\n",stu.name,stu.age,stu.sex);
}
int main(){
FILE *fp = NULL;
Student stu[COUNT];
int i ;
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
for(i=0;i<COUNT;i++){
fscanf(fp,"姓名:%s\t年龄:%d\t性别:%s\n",&stu[i].name,&stu[i].age,&stu[i].sex);
print(stu[i]);
}
fclose(fp);
}

语句解释:
-  记住,要想正确的读入数据,要重写scanf中控制列表中的所有字符。


  范例7:写一个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = NULL;
fp = fopen("D:\\a.txt","w");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
fputs("世界,你好!\r\nHello World",fp);
fclose(fp);
}

语句解释:
-  函数原型: fputs(字符串,文件指针)


  范例10:读一个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = NULL;
char str[50];
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
puts(fgets(str,100000,fp));
fclose(fp);
}

语句解释:
-  函数原型 fgets(字符数组,n,fp)
-  一句话: 从fp中读取n个字符,将字符存放到字符数组中去。
-  如果在读到n个字符之前,遇到了换行符或者EOF,则停止读取。
-  函数fgets(),返回值为字符数组的首地址。
-  此函数只会读取n-1个字符,然后在最后加一个‘\0’字符。程序一共得到n个字符。


文件定位


  范例1:回到文件开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = NULL;
char str[50];
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
puts(fgets(str,100000,fp));
rewind(fp);
puts(fgets(str,100000,fp));
fclose(fp);
}

语句解释:
-  使fp所指向的文件中的位置指针,回到文件的开头。


  范例2:随机读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = NULL;
char str[50];
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
puts(fgets(str,100000,fp));
fseek(fp,2,SEEK_SET);
puts(fgets(str,100000,fp));
fclose(fp);
}

语句解释:
-  函数原型:fseek(文件指针,位移量,起点)
   -  一句话:让位置指针回到,从起点开始,位移量个位置处。
   -  以本例来说:让fp中的位置指针回到,从SEEK_SET开始,位移2个位置处。
-  起点有3个:
   -  SEEK_SET 代表文件头。也可以用数字0代表。
   -  SEEK_CUR 代表当前位置。也可以用数字1代表。
   -  SEEK_END 代表文件结尾。也可以用数字2代表。
-  文件操作这部分写的有点简略,主要的原因是:
   -  我本人是用Java的,因此不需要对C语言了解太深(但是也是学的C语言,入的编程的门)。
   -  之所以回来复习C,主要是为了自己和朋友们考程序员。
   -  如果您以后是使用C++ 开发,那么就仔细研究一下C语言吧。
-  fseek()主要用于二进制文件。


  范例3:ftell() 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = NULL;
char str[50];
fp = fopen("D:\\a.txt","r");
if(fp == NULL){
puts("the file open lost");
exit(0);
}
puts(fgets(str,100000,fp));
printf("%d\n",ftell(fp));
fclose(fp);
}

语句解释:
-  ftell() 用来返回当前位置指针所在的位置。 如果返回-1L 则表示出错。

附录


  范例1:system函数。

1
2
3
4
5
6
7
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("开始复制\n");
system("copy D:\\a.txt D:\\b.txt");
printf("复制结束\n");
}

语句解释:
-  使用system函数可以执行操作系统的命令,如在Window中,此函数可以执行cmd中的所有命令。如:cls(清屏)、pause(暂停)等。
-  system函数被定义在stdlib.h头文件中。


  范例2:执行java类。

1
2
3
4
5
#include<stdio.h>
#include<stdlib.h>
int main(){
system("java ShowDialog");
}

语句解释:
-  所有在cmd中可以执行的命令,在C语言中都可以将该命令以字符串的方式传递给system函数来执行。


  范例3:rand和Sleep函数。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main(){
int i=0;
for(;i<10;i++){
printf("%d\n",rand());
Sleep(1000);
}
}

语句解释:
-  使用stdlib.h头文件中的rand函数可以随机出一个int类型的整数。
-  使用windows.h头文件中的Sleep函数可以让程序暂停执行指定的毫秒数。


其他:

-  第一,C语言共有32个关键字、9种控制语句、34种运算符号。
-  第二,C语言对语法的限制不太严格。如:不检查数组下标越界。
-  第三,C语句可以直接对硬件操作。它兼具高低级语言的功能,既可以编写系统软件也可以编写应用软件。再次强调:“C博大精深,咱们在此讨论的仅仅是皮毛而已!切记!”。
-  第四,C语言是面向过程的语言,C++是面向对象与面向过程相结合的语言。
-  第五,C文件的后缀为:.c 。C++文件的后缀:.cpp 。 p是plus(‘加’的意思)。
-  第六,C++是由C发展而来的,主要用来编写大型软件。
-  第七,C++对C程序是兼容的,因此可以用C++的编译系统编译C程序。
-  第八,在一个常量后面加上u或者U,则此常量就是无符号类型的。
-  第九,其实在文件的头部可以不加<stidio.h> 但是会有警告信息。
-  第十,高级语言的语句用来向计算机系统发出操作指令,一个语句在经过编译后会产生若干条机器指令。
-  第十一,%d和%i功能是一样的。
-  第十二,库函数并不是C语言的组成部分,而是C语言编译系统为方便用户使用而提供的公共函数。
-  第十三,getch()函数从键盘读入一个字符,用户不需要按回车。
-  第十四,结构体变量之间可以相互赋值。如:Student stu={“张三”,32};Student stu2 = stu 。