C语言程序设计
阅读须知:
本篇是基于谭浩强编写的《C程序设计》第五版做出的学习笔记。
本篇建议有其他高级语言基础的人士阅读。
由于是作为第二语言项,本篇将忽略很多基础和高级内容,如:判断语句、循环语句、文件操作等。
本篇偏向于基础向,主要用于快速使用,并未深入探讨。
一、基本使用
位、字节和字是描述计算机数据单元或存储单元的术语,这里主要指存储单元。
- 位(bit):最小的存储单元,可以存储0或1。
- 字节(byte):常用计算机存储单位,1字节为8位。那么8个字节就有256(2的8次方)种可能的0、1组合。
- 字(word):设计计算机时给定的自然存储单元。
编译机制:
C编程的基本策略是,用程序把源代码文件转换为可执行文件,其典型的C实现是通过编译和链接这两个步骤完成的。
首先通过编译器把源代码编译为机器语言代码,并把所有结果放在目标代码文件中,但不能直接运行,因为它不是一个完整的程序。
目标文件还缺少启动代码和所引入的库代码,这些都由链接器实现。启动代码充当着程序和操作系统之间的接口,因此不同系统都有不同的启动代码。而库代码则是目标文件所使用的库函数所需的其他库文件中的目标代码,一般都是预编译的。链接器的作用就是把编写的目标代码文件、启动代码和库代码合并成一个文件,即可执行文件。
因此可执行文件也是由机器语言指令组成的。
1. 常量和变量
在计算机高级语言中,数据有两种表现形式:常量和变量。
1.1 常量
值不能改变的量称为常量,有以下几种:
- 整型常量。如12、0、-233等。
- 实型常量。
- 十进制小数形式。如12.3、0.33、-3.2等。
- 指数形式。如2.23e2(2.23*10^2)等。
- 字符常量。
- 普通字符。如’a’,’@'等,仅能有一个字符,并以二进制的形式存储在计算机存储单元中。
- 转义字符。以" \ "开头的字符序列。其表如下:
转义字符 | 字符值 | 结果 |
---|---|---|
\' | 一个单引号 | 输出’ |
\" | 一个双引号 | 输出" |
\? | 一个问号 | 输出? |
\\ | 一个反斜线 | 输出\ |
\a | 警告 | 产生警告信号 |
\b | 退格 | 将光标当前位置后退一个字符 |
\f | 换页 | 将光标当前位置移到下一页开头 |
\n | 换行 | 将光标当前位置移到下一行开头 |
\r | 回车 | 将光标当前位置移到下本行开头 |
\t | 水平制表符 | 将光标当前位置移到下一个Tab位置 |
\v | 垂直制表符 | 将光标当前位置移到下一个垂直制表对其点 |
\o 、\oo 、\ooo ,o代表一个八进制数字 | 与八进制码对应的ASCII字符 | 输出与八进制码对应的ASCII字符 |
\xh[h...] ,h代表一个十六进制数字 | 与十六进制码对应的ASCII字符 | 输出与十六进制码对应的ASCII字符 |
- 字符串常量。如"dog"、"123"等。
- 符号常量。通过
#define
宏指令,指定一个符号名称代表一个常量。注意行末无分号。
#define PI 3.1415926
通过#define
宏指令,预处理器会对PI进行处理,把所有PI全部替换为3.1415926。好处就是含义清楚、一改全改。
需要注意的是,不要把符号常量误认为是变量。与其他常量相比,符号常量并不占内存空间,在预编译后这个符号就不会存在了,所有这个符号都会替换为指定值。在习惯上,我们把常量名都用大写表示。
1.2 变量
变量代表一个有名字、有特定属性的一个存储单元,它用来存放变量的值,并且在运行期间是可以改变的。
变量必须先定义,后使用。定义时指定变量的名字和类型。变量名实际是以一个名字代表的存储地址,在编译链接时由编译系统给每一个变量名分配对应的内存地址。取值和赋值的过程就是通过变量名找到内存地址,再进行存放和读取。
1.3 常变量
在C99中我们还可以使用const
关键字来定义常变量。其含义为该变量值不能改变。
const int a = 3;
常变量与常量的区别就在于,常变量本身就是变量,拥有类型、占存储单元和名字,只是变量值不能改变。而常量是没有名字的不变量。
2. 数据类型
所谓类型,就是对数据分配存储单元的安排,包括存储单元的长度以及数据的存储形式。见下表(*表示C99功能):
2.1 整型数据
(1)基本整型int
编译系统分配给int
型数据2个字节或4个字节(由编译系统决定)。在存储单元中的存储方式是:用整数的补码形式存放。一个正数的补码是此数的二进制;一个负数的补码是此数的绝对值写成二进制,然后取反再加1。
如求-5的补码:
在存放整数的存储单元中,首位是用来表示符号的,0表示正数,1表示负数。
如何确定整型的取值范围,假如系统给整型数据分配了2个字节,也就是16位,那么它能存放的最大值为0111111111111111,即215-1,也就是32767;最小值就为1000000000000000,即-215,也就是-32768。超过这个范围的话结果就溢出了。在后面还会有无符号整数,那么范围自然也就不一样了,因为二进制首位不再表示符号,而是单指数值了。
(2)短整型short int
类型名为short
或者short int
,短整型的存储单元长度不会超过基本整型,至少为16位。
(3)长整型long int
类型名为long
或者long int
,长整型的存储单元长度大于等于基本整型,至少为32位。
(4)双长整型long long int
类型名为long long
或者long long int
,一般分配8个字节,也就是64位。
由于存储单位长度没有具体的规定,我们可以
sizeof()
运算符来测量类型或变量长度。
sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
(5)修饰符unsigned和signed
我们可以通过加上修饰符unsigned
来指定整型变量为无符号整数类型,如unsigned int a
。在默认情况下,未指定的为有符号整数类型,即signed int a
和int a
是等价的。
由于无符号整型变量左最高位不再表示符号,所以无符号整型变量存放的正数范围比有符号整型变量正数范围扩大一倍。
- 只有整型(包括字符型)数据可以使用
unsigned
和signed
修饰符,实型数据不能使用。- 对无符号整型数据用
"%u"
格式输出,它表示无符号十进制的格式输出。
2.2 字符型数据
(1)ASCII字符集
在系统中并不是所有的字符和字符代码都能被程序识别,只能使用系统的字符集中的字符。现大多数系统采用ASCII字符集,其中包含了127个字符集。
- 字母:大写字母A ~ Z,小写字母a ~ z。
- 数字:0~9.
- 其他符号:29个。
- 空格符:空格、水平制表符、垂直制表符、换行、换页。
- 转义字符:空字符
'\0'
、警告'\a'
、退格'\b'
、回车'\r'
等。
字符是以整数形式存放在内存单元中,而其字符代码最多用7个二进制位就可以表示,所以在C语言中,指定一个字节存储一个字符,而字节左最高位为0。
(2)字符变量
字符变量是通过char
定义的。
char a = '?';
代码定义a为字符变量并赋值字符’?’,而它的ASCII代码为63,因此系统把63赋值给了变量a。实际上,字符变量是一个字节的整型变量,由于它常用于存放字符,所以称为字符变量。我们可以把0~127之间的整数赋给字符变量。
在输出一个字符变量时,我们可以选择十进制整数形式输出,也可以以字符形式输出。
printf("%d , %c ", a, a);
//输出结果: 63 , ?
2.3 浮点型数据
浮点型数据是用来表示具有小数点的实数的。在C语言中,实数是以指数形式存放在存储单元中的。浮点数类型包括float、double、long double。
(1)单精度浮点型float
float a = 3.14159;
编译系统为float
型变量分配4个字节,能得到6位有效数字,数值范围为-3.4*10^-38~3.4*10^38
。数值以规范化的二进制指数形式存放在存储单元中,在存储时,系统将实型数据分成小数部分和指数部分分别存放。如3.14159:
实际上计算机是用二进制表示小数部分,2的幂次表示指数部分的。而用多少位分配小数部分和指数部分,则由编译系统自行决定。
由于用二进制表示一个实数并且存储单元的长度也是有限的,因此不能得到完全精确的值,只能存储成有限的精确度。
小数部分占的位数越多,数的有效数字越多,精度越大;指数部分占的位数越多,则能表示的数值范围越大。
(2)双精度浮点型double
编译系统为double
型变量分配8个字节,得到15位有效数字,数值范围为-1.7*10^-308~1.7*10^308
。
为了提高运算精度,在C语言中进行浮点数的算术运算时,会将
float
型数据都自动转换为double
型,再进行运算。
(3)长双精度浮点型long double
不同的编译系统对long double
型处理方法不同,如Turbo C分配16个字节,Visual C++分配8个字节。
下面是关于所有实型数据的情况:
类型 | 字节数 | 有效数字 | 数值范围(绝对值) |
---|---|---|---|
float | 4 | 6 | 0、1.2 x 10^-38~3.4 x 10^38 |
double | 8 | 15 | 0、2.3 x 10^-308~1.7 x 10^308 |
long double(Visual C++) | 8 | 15 | 0、2.3 x 10^-308~1.7 x 10^308 |
long double(Turbo C) | 16 | 19 | 0、3.4 x 10^-4932~1.1 x 10^4932 |
如何确定常量的类型,对于字符就不说明了,在对于数值常量,通常按照以下规律:
- 对于整型常量,不带小数点的数值并且在
int
类型有效范围内,则作为int型处理,超过范围的则会作为long int
型处理,甚至long long
型。在整数末尾加L
或l
,则表示为long int
型。- 对于浮点型常量,凡是以小数形式或指数形式出现的实数均是浮点型常量,并且编译系统都会按照
double
型处理,分配8个字节。当然,在浮点型常量后加F
或f
,则强制表示为float
型,在浮点型常量后加L
或l
,则强制表示为long double
型。
3. 运算符和表达式
- 算术运算符(
+ - * / % ++ --
) - 关系运算符(
> < == >= <= !=
) - 逻辑运算符(
! && ||
) - 位运算符(
<< >> ~ | ^ &
) - 赋值运算符(
=
及其扩展赋值运算符) - 条件运算符(
?:
) - 逗号运算符(
,
) - 指针运算符(
* &
) - 求字节数运算符(
sizeof
) - 强制类型转换运算符(
(类型名)
) - 成员运算符(
. ->
) - 下标运算符(
[]
) - 其他(函数调用运算符
()
)
3.1 优先级和结合性问题
C规定了运算符的优先级,还规定了结合性。如果在一个运算对象两侧的运算符的优先级相同,则按规定的“结合方向”计算。对于算术运算符都是自左向右(左结合),赋值运算符是自右向左(右结合)。
下面是常用运算符的优先级与结合性:
优先级顺序 | 运算符种类 | 例子 | 结合方向 |
---|---|---|---|
1 | 单目运算符 | ! ~ - ++ -- 强制类型转换符 | 右结合 |
2 | 算术运算符 | * / % 高于 + - | 左结合 |
3 | 关系运算符 | < <= > >= 高于 == != | 左结合 |
4 | 逻辑运算符 | !、&& 高于 || | 左结合 |
5 | 赋值运算符 | = += -= *= /= %= &= ^= |= <<= >>= | 右结合 |
6 | 逗号运算符 | , | 左结合 |
3.2 类型转换问题
(1)赋值运算中的类型转换
在一个赋值语句中,如果赋值运算符左侧变量的类型和右侧表达式的类型不一致,那么将发生自动类型转换。规则是:将右侧表达式值的类型转换为左侧变量的类型。
需要注意的是,将取值范围小的类型转换为取值范围大的类型是安全的,反之则不安全,可能会出现溢出、丢失等问题。
(2)表达式中的类型转换
表达式运算中,相同类型的操作数进行运算的结果类型与操作数类型相同。但如果表达式中混有不同类型的常量和变量,则先把它们转换为同一类型,然后在进行计算。简单的讲,C编译器将所有操作数都转换成占内存字节数最大的操作数类型,称为类型提升。
表达式中的类型自动转换规则:
4. 数据输入和输出
通过调用C的标准库函数来实现输入和输出操作。要使用这些标准输入输出函数,只需在程序开始位置加上预处理命令即可:
#include <studio.h>
作用是将输入输出函数的头文件stdio.h
包含到源文件中。
而#include
指令还有一种形式:
#include "studio.h"
它们之间的区别在于:用尖括号形式时,编译系统从存放C编译系统的子目录中去找所要包含的文件,这是标准方式。而用双引号形式,在编译时,编译系统先在用户的当前目录中寻找文件,若找不到,再按照标准方式查找。
4.1 printf()函数输出
一般格式为:printf(格式控制,输出表列)
-
格式控制:是一个字符串,它由两部分组成。
- 格式声明:由"%"和格式字符组成,如%d、%f等。作用是将输出的数据转换为指定的格式后输出。
- 普通字符:需要在输出时原样输出的字符。
-
输出表列:表示需要输出的一些数据,可以是变量、常量和表达式。
printf("%d, %c", i, c);
printf()
函数常用的格式字符:
格式字符 | 说明 |
---|---|
d,i | 以十进制有符号形式输出整数(正数不输出符号)。 |
o | 以八进制无符号形式输出整数(不输出前导符0)。 |
x,X | 以十六进制无符号形式输出整数(不输出前导符0x),用x则输出十六进制数的a~f时以小写形式输出,用X则以大写形式输出。 |
u | 以十进制无符号形式输出整数。 |
c | 以字符形式输出,只输出一个字符。 |
s | 输出字符串。 |
f | 以小数形式输出单、双精度数,隐含输出6位小数。 |
e,E | 以指数形式输出实数,用e时指数以“e”表示,用E时指数以“E”表示。 |
g,G | 用来输出浮点数,系统自动判断使用%e和%f格式输出,选择其中长度较短的格式,不输出无意义的0。用G时,若以指数形式输出,则指数以大写表示。 |
p | 以主机格式显示指针,即变量的地址。 |
% | 输出%。 |
在函数printf()
的格式说明中,在%和格式符之间的位置,还可以插入格式修饰符,用于指定输出数据的最小宽域、精度、对其方式等。
格式为:"% 修饰字符 格式字符"
printf("%d, %15.5f", i, num);
printf()
函数常用的格式修饰字符:
修饰字符 | 说明 |
---|---|
l | 加在格式符d、i、、o、x、u之前用于输出long 型数据。 |
L | 加在格式符f、e、g之前用于输出long double 型数据。 |
h | 加在格式符d、i、、o、x之前用于输出short 型数据。 |
m | 整数。指定输出项输出时所占的列数,当m为正整数时,若输出数据宽度小于m,则在域内向右靠齐,左边多余位补空位;当输出数据宽度大于m时,按实际宽度全部输出;若m有前导符0,则左边多余位补0;若m为负整数,则输出数据在域内向左靠齐。 |
.n | 大于等于0的整数。精度修饰符位于最小宽域修饰符m之后,由一个圆点及其后的整数构成。对于浮点数,用于指定输出的浮点数的小数位数;对于字符串,用于指定从字符串左侧开始截取的子串字符个数。 |
- | 输出的数字或字符在域内向左靠。 |
+ | 在正数前面输出一个加号,在负数前面输出一个减号。 |
* | 当最小域宽m和显示精度.n用*代替时,表示它们的值不是常数,而由printf() 函数的输出项按顺序依次指定。 |
0 | 在输出的数据前面加上前导符0,以填满域宽。 |
4.2 scanf()函数输入
一般格式为:scanf(格式控制,地址表列)
- 地址表列:由若干个地址组成的表列,可以是变量的地址,或字符串的首地址。
scanf("a=%f, b=%f, c=%f", &a, &b, &c);
scanf()
函数常用的格式字符:
格式字符 | 说明 |
---|---|
d、i | 输入十进制整数 |
o | 输入八进制整数 |
x | 输入十六进制整数 |
c | 输入一个字符,空白字符(包括空格、回车、制表符)也作为有效字符输入 |
s | 输入字符串,遇到第一个空白字符时结束 |
f、e | 输入浮点数,以小数或指数形式输入均可 |
% | 输入% |
scanf()
函数要求必须指定用来接收数据的变量的地址,每个格式字符都对应一个存储数据的目标地址。如果没有指定存储数据的目标地址,虽然编译器不会提示出错信息,但会导致数据无法正确地读入指定的内存单元中。
当成功调用时,返回值为成功赋值的数据项数;当出错时,则返回EOF(“End Of File”的缩写,表示文件结尾,它是一个在头文件<stdio.h>中定义的整数型的符号常量,C标准将EOF定义为一个负整数,通常是-1,但不一定,也是根据系统决定的)。
scanf()
函数常用的格式修饰字符:
格式修饰符 | 说明 |
---|---|
l | 加在格式符d、i、、o、x、u之前用于输出long 型数据。加在格式符f、e之前用于输出double 型数据。 |
L | 加在格式符f、e之前用于输出long double 型数据。 |
h | 加在格式符d、i、、o、x之前用于输出short 型数据。 |
m | 指定输入数据的宽度(列数),系统自动按此宽度截取所需数据。 |
* | 表示对应的输入项在读入后不赋给相应的变量,既让scanf() 函数从输入流中读取任意类型的数据并将其丢弃,也称为赋值抑制字符。 |
scanf()
函数没有显示精度.n格式修饰符,即不能指定精度。
注意,在用scanf()
函数输入非字符型数据时,以下几种情况都认为数据输入已结束:
- 输入空格符、回车符、制表符
- 达到指定域宽
- 输入非数字字符
4.3 字符函数输入输出
除了使用printf()
函数和scanf()
函数输入输出字符外,C函数库还提供了一些专门用于输入输出字符的函数。
通过使用putchar()
函数来实现字符输出。
char a = 'A';
putchar(a);
putchar()
函数的参数可以是字符常量、整型常量、字符变量或整型变量(其值在ASCII代码范围内)。
通过使用getchar()
函数来实现字符输入。
char a;
a = getchar();
getchar()
函数作用是从计算机终端(键盘)输入一个字符。getchar()
函数的值就是从输入设备得到的字符,且只能接收一个字符。
通过输入字符并按下Enter键,字符才会送到计算机。输入的字符可以是一个或多个,在按下Enter键前,字符先暂存到键盘的缓冲器中,按下Enter键后,才会把输入的一个或多个字符输入到计算机中,然后按先后顺序分别赋值给相应的变量。
还需注意的是,Enter键也可以作为字符输入计算机中,因此须小心不要让回车键赋值给变量。
二、数组
- 数组是一组有序数据的集合。
- 用一个数组名和下标来唯一地确定数组中的元素。
- 数组中的每一个元素都属于同一个数据类型。
1. 一维数组
定义一维数组的一般形式:
类型说明符 数组名[常量表达式]
//定义
int a[10];
//引用赋值
a[0] = 2;
//初始化
int b[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
常量表达式可以包括常量和符号常量,如“int a[3+5]
”是合法的,但不能是变量。换句话说,C语言不允许对数组的大小作动态定义。
在定义数值型数组时,指定长度并初始化,未初始化的数组元素,系统会默认对它们初始化为0(如果是字符型数组,则是’\0’;如果是指针型数组,则是NULL,即空指针)。
2. 二维数组
定义二维数组的一般形式:
类型说明符 数组名[常量表达式] [常量表达式]
//定义
float f[2][2];
//引用赋值
f[0][1] = 2;
//初始化,第一种形式
int b[2][2] = {{0, 1},{2, 3}};
//初始化,第二种形式
int c[][2] = {0, 1, 2, 3};
3. 字符数组
用于存放字符数据的数组是字符数组,定义形式与数值型数组相似。
//定义
char c1[5];
//引用赋值
c1[0] = 'd';
//初始化,第一种形式
char c2[] = {'h', 'a', 'p', 'p', 'y'};
//初始化,第二种形式
char c3[] = {"happy"};
//初始化,第三种形式
char c4[] = "happy";
//二维数组
char c5[][3] = {{'g', 'o', 'o'},{'g', 'l', 'e'}};
由于字符型数据是以整数形式(ASCII代码)存放的,因此也可以用整型数组来存放字符数据:
int c[] = {'a', 'b'};
3. 1 字符串使用
在C语言中,是将字符串作为字符数组来处理的。实际工作中,我们关心的往往是字符串的有效长度而不是字符数组的长度。因此,C语言规定了一个字符串结束标志,以空字符'\0'
作为结束标志。如果字符数组有若个字符,前面9个字符都不是空字符,而第十个字符是空字符'\0'
,则认为数组有一个字符串,其有效长度为9。
C系统在用字符数组存储字符串常量时会自动加一个
'\0'
作为结束符。因此,我们无需手动添加结束符。
有了结束标志,字符数组的长度就显得不那么重要了,程序中依靠检查结束标志的位置来判定字符串是否结束和决定字符串长度。
char c1[] = "happy";
//c1等价c2
char c2[] = {'h', 'a', 'p', 'p', 'y', '\0'};
示例1:
字符数组并不要求它最后一个字符为'\0'
,甚至可以不包含'\0'
。
char c[5] = {'C', 'h', 'i', 'n', 'a'};
由于系统在处理字符串常量存储时会自动加一个'\0'
,因此,为了使处理方法一致,便于测定字符串的实际长度,往往在字符数组中人为地加上一个'\0'
。
char c[6] = {'C', 'h', 'i', 'n', 'a', '\0'};
示例2:
定义以下字符数组:
char c[] = {"hello"};
由于系统自动在字符串常量的最后一个字符后面加了一个'\0'
,因此c数组的存储情况为:
若想用一个新的字符串you
代替原有字符串hello
,则输入you
分别赋值给c数组中前面3个元素:
如此,新字符串和旧字符串连成一片,输出:youlo
。但若在you
后面加一个'\0'
,则存储情况为:
输出字符数组中的字符串时,遇到'\0'
就会结束输出,则输出结果为you
。从这里可以看出'\0'
的作用。
3.2 字符串输入输出
字符数组的输入输出可以有两种方法。
(1)逐个字符输入输出。用格式符"%c"
输入或输出一个字符。
# include <stdio.h>
int main(){
char c[] = "hello";
int i = 0;
//死循环
while (1)
{
printf("%c", c[i]);
i++;
//若下一个输出字符是'\o',则跳出死循环
if (c[i] == '\0')
{
break;
}
}
//输出结果:hello
return 0;
}
(2)将整个字符串一次输入输出。用格式符"%s"
。
# include <stdio.h>
int main(){
char c[] = "hello";
printf("%s", c)
//输出结果:hello
return 0;
}
若同时输入多个字符串,应在输入时用空格分隔(当然也可以回车一个一个输入),系统把空格字符作为输入的字符串之间的分隔符。如:
# include <stdio.h>
int main(){
char c1[6], c2[6], c3[6];
scanf("%s%s%s", c1, c2, c3);
printf("%s\n%s\n%s\n", c1, c2, c3);
return 0;
/**
输入:
yes no hello
输出:
yes
no
hello
*/
}
数组中未被赋值的元素的值自动设置为'\0'
。
其他要点:
输出的字符串中不包括结束符
'\0'
。如果一个字符数组包含一个以上
'\0'
,则遇到的第一个'\0'
时输出结束。在使用
scanf()
函数输入字符串时,输入的字符串应当短于已定义的字符数组的长度。
scanf()
函数中的输入项如果是字符数组名,不要加地址符&,因为在C语言中数组名代表该数组第一个元素的地址。
3.3 字符串处理函数
C函数库中提供了一些专门处理字符串的函数。
(1)puts()
函数
作用是输出字符串。
char str[] = "hello";
puts(str);
(2)gets()
函数
作用是输入字符串,其返回值是字符数组str的第一个元素的地址。一般使用gets()函数的目的是向字符数组输入一个字符串,而不是关心其函数返回值;
char str[8];
gets(str);
(3)strcat()
函数
作用是把两个字符数组中的字符串连接起来,把字符串2接到字符串1的后面,结果放在字符串1中,其函数返回值为字符数组1的地址。
char str1[15] = "hello ";
char str2[] = "world";
printf("%s", strcat(str1, str2));
//输出:hello world
注意:
- 字符数组1必须足够大,以便容纳连接后的新字符串。
- 连接钱两个字符串的后面都有
'\0'
,连接时会将字符串1的'\0'
取消,只保留字符串2的'\0'
。
(4)strcpy()
和strncpy()
函数
strcpy()
函数作用是将字符串2复制到字数组1中去。
char s[10];
strcpy(s, "hello");
printf("%s", s);
//输出:hello
注意:
- 字符数组1要足够大,不应小于字符串2的长度。
- 参数1必须是字符数组名,参数2可以是字符数组名,也可以是字符串常量。
- 字符串复制并不是全部替换,若字符数组1已有值,字符串2复制到字符数组1时只会替换字符数组1前字符串2长度的元素,后面原有数据不变。
- 不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组。因为字符数组名是一个地址常量,它无法改变值。
strncpy()
函数作用是将字符串2前面n个字符复制到字符数组1中。
char s[10];
strncpy(s, "hello",3);
printf("%s", s);
//输出:hel
(5)strcmp()
函数
作用是比较字符串1和字符串2,比较规则是将两个字符串自左向右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或'\0'
为止。
- 若全部字符相同,则认为两个字符串相等,函数返回值为0。
- 若出现不同字符,则以第1对不相同的字符的比较结果为准。字符串1大于字符串2,则函数返回值为一个正整数;字符串1小于字符串2,则函数返回值为一个负整数。
如果两个字符串都由英文字母组成,则有一个简单规律:
- 在英文字典中位置越后面越大
- 小写字母大于大写字母
char s1[] = "hello";
char s2[] = "helln";
printf("%d", strcmp(s1, s2));
//输出结果:1
(6)strlen()
函数
作用是测字符串的长度,函数返回值是字符串的实际长度(不包含'\0'
)。
char s1[] = "hello";
printf("%d", strlen(s1));
//输出结果:5
(7)strlwr()
函数
作用是将字符串中大写字母换成小写字母。
(8)strupr()
函数
作用是将字符串中小写字母换成大写字母。
三、函数
一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。
源程序文件则由一个或多个函数以及其他有关内容(如指令、变量声明、定义等)组成。
在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
C程序的执行是从main函数开始的,在main函数中结束的。
函数之间是相互独立、分别进行的,即函数不能嵌套定义,但可以互相调用(main函数除外)。
从用户使用的角度看,函数有两种:
- 库函数,由系统提供,用户不必自己定义,可直接使用。
- 用户自己定义的函数。
从函数的形式看,函数分两类:
- 无参函数。
- 有参函数。
1. 函数定义
1.1 无参函数
指函数名后的括号是空的或void
,没有任何参数。
#include <stdio.h>
int main(){
//函数声明
void hello();
//调用函数
hello();
return 0;
}
//定义函数
void hello(){
printf("%s", "hello");
}
//输出结果:hello
1.2 有参函数
指函数名后的括号里有调用函数需要的参数。
#include <stdio.h>
int main(){
//函数声明
void hello(char str[]);
//调用函数
hello("hello");
return 0;
}
//定义函数
void hello(char str[]){
printf("%s", str);
}
//输出结果:hello
1.3 空函数
在程序设计中有时会用到空函数。该函数无任何实际作用,主要用于在程序编写的开始阶段,可以在将来准能扩充功能的地方写一个空函数,只是这些函数暂时还未编写好,先占一个空位,等以后需要扩展时用一个写好的函数代替它即可。
void addUser(){
}
2. 函数调用
调用的一般形式:函数名(实参表列)
2.1 数据传递
在定义函数时函数名后面括号中的变量名称为形式参数。在主调函数中调用一个函数时,函数名后面括号中的参数为实际参数。
在调用函数过程中,系统会把实参的值传递给被调用函数的形参,该值在函数调用期间有效,这种过程称为虚实结合。
- 实参可以是常量、变量、表达式,但要求它们有确定的值。
- 实参与形参的类型应相同或赋值兼容。
- 实参向形参的数据传递是“值传递”,这是单向传递,只能由实参传给形参,反之则不行。这是因为实参和形参在内存中占有不同的存储单元,实参是无法得到形参的值。
函数调用过程分以下几个阶段:
- 在未出现函数调用时,它们并不占用内存中的存储单元。在发生函数调用时,形参才会被分配存储单元。
- 将实参的值传递给对应形参。
- 执行函数体,期间可以对形参进行相关运算。
- 通过
return
语句将函数值带回到主调函数。 - 函数调用结束,形参内存单元释放。
函数的返回值通过return
语句获得。其返回值的类型则由定义函数时指定的类型确定,即函数类型决定返回值的类型。对于不带返回值的函数,应当在定义函数时指定类型为void
。
#include <stdio.h>
int main(){
//函数声明
char* hello();
//调用函数并输出函数值
printf("%s", hello());
return 0;
}
//定义函数
//char*表示返回char型指针变量(地址),由于要返回字符串,则返回类型实际上是char数组的第一个字符地址
char* hello(){
return "hello";
}
2.2 函数声明
在一个函数中调用另一个函数需要具备以下条件:
- 首先被调用的函数必须是已经定义的函数。
- 如果使用库函数,应该在本文件开头用
#include
预处理指令将调用有关库函数时所需用到的信息包含到本文件中。 - 如果使用用户自定义的函数,而该函数的位置在主调函数的后面(同一文件),应该在主调函数中对被调用的函数作声明。
//库函数所需的库文件
#include <stdio.h>
int main(){
//函数声明
void hello(char str[]);
//调用函数
hello("hello");
return 0;
}
//定义函数
void hello(char str[]){
//库函数直接调用
printf("%s", str);
}
可以发现函数声明和函数首部基本上相同,因此写函数声明时,可以简单地照写已定义的函数首部,再加一个分号,就成了函数声明,函数首部又称为函数原型。
使用函数的首部作为函数声明,是为了便于对函数调用的合法性进行检查。因为在函数的首部包含了检查调用函数是否合法的基本信息(包括函数名、函数值类型、参数个数、参数类型和参数顺序),在检查函数调用时要求基本信息与函数声明一致,否则就按出错处理。使用函数原型作声明是C的一个重要特点。
函数声明的一般形式有两种:
- 函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,…,参数类型n 参数名n);
- 函数类型 函数名(参数类型1,参数类型2,…,参数类型n);
此外,如果在主调函数外进行函数声明(即文件开头),则不必在主调函数中再重复进行声明,写在所有函数前面的外部声明在整个文件范围中有效。
2.3 数组作为函数参数
数组元素也可以作为实参传递值,其用法和变量相同,但不能用作形参,其原因是函数被调用时无法为一个数组元素单独分配一个临时存储单元(数组是一个整体,在内存中占连续的一段存储单元)。
除了可以用数组元素作为函数参数外,还可以用数组名作函数参数(包括实参和形参),则传递的就不是值而是数组首元素的地址,这是一种地址传递。
//库函数所需的库文件
#include <stdio.h>
int main(){
//函数声明
void hello(char str[]);
//调用函数
hello("hello");
return 0;
}
//定义函数
void hello(char str[]){
//库函数直接调用
printf("%s", str);
}
形参数组的大小无需指定,若指定则不起任何作用,因为C编译系统并不会检查形参数组大小,只是将实参数组的首元素地址传给形参数组名。
在使用多维数组名作为函数的实参和形参时需要注意,形参数组定义时可以指定每一维的大小,也可以省略第一维的大小,但不能省略第二维以及更高维度的大小。
//形参数组定义第一种形式
int score[2][3];
//形参数组定义第二种形式
int score[][3];
3. 局部变量和全局变量
在多个函数中都可以定义变量,那么在一个函数中定义的变量,能否被其他函数引用?这是一个变量作用域问题。
定义变量有三种情况:
- 在函数开头定义
- 在函数内的复合语句内定义
- 在函数的外部定义
3.1 局部变量
在函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内它们才能被引用,函数外是不能的。而在符合语句内的变量也只在本符合语句范围内有效,在符合语句以外是不能使用的。以上两种情况,都被称为局部变量。
#include <stdio.h>
int main(){
int x, y, z;
for(int i = 0; i <= 3; i++){
//……
}
{
int m, n;
}
return 0;
}
void hello(int a){
int b, c
}
/**
x,y,z只在main函数中有效
i只在for语句中有效
m,n只在该复合语句块中有效
a,b,c只在hello函数中有效
*/
- 主函数不能使用其他函数中定义的变量。
- 不同的函数可以定义同名的变量,它们互不干扰。
- 形式参数也是局部变量。
3.2 全局变量
还剩下一种情况,在函数之外定义的变量称为外部变量,外部变量是全局变量。全局变量可以为本文件中其他函数所共用,有效范围从定义变量的位置开始到本文件结束。
#include <stdio.h>
int q, w;
int main(){
int x, y, z;
return 0;
}
void hello(int a){
int b, c
}
/**
q,w在本文件中有效,本文件其他函数都可以调用
x,y,z只在main函数中有效
a,b,c只在hello函数中有效
*/
全局变量的作用是增加了函数间数据联系的桥梁。
4. 变量存储方式和生存期
先前我们从变量作用域的角度观察,变量可以分为全局变量和局部变量,我们还可以通过变量的生存期来观察。
变量的存储有两种:
-
静态存储方式:指在程序运行期间由系统分配固定的存储空间的方式。
-
动态存储方式:指在程序运行期间根据需要进行动态的分配存储空间方式。
内存中供用户使用的存储空间可以分3部分:程序区、静态存储区、动态存储区,数据则分别存放在静态存储区和动态存储区中。
全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序完毕后释放。
而在动态存储区中则存放以下数据;
- 函数形式参数。调用函数时分配存储空间。
- 函数中没有用
static
声明的变量,即自动变量。 - 函数调用时的现场保护和返回地址等。
对以上数据,在函数调用时分配存储空间,函数结束时释放存储空间。因此,两次调用函数所分配给局部变量的存储空间地址可能是不同的。
在C语言中,每一个变量和函数都有两个属性:数据类型和数据存储类别。存储类别指的是数据在内存中存储的方式。在定义变量和函数时,一般同时指定其数据类型和存储类别,也可以采用默认方式指定(系统隐式指定)。
C的存储类别包括4种:
- 自动的(auto)
- 静态的(static)
- 寄存器的(register)
- 外部的(extern)
4.1 局部变量的存储类别
(1)auto变量
函数中的局部变量,如果不专门声明为static
存储类别,则都是动态地分配存储空间的,数据存储在动态存储区中。这种局部变量称为自动变量,用关键字auto
作存储类别的声明。
auto int a = 3;
实际上,auto
可省略,默认情况下变量隐性指定为自动存储类别,它属于动态存储方式,在动态存储区分配存储单元,函数调用结束即释放。
(2)static变量
有时希望函数中的局部变量在函数调用结束后继续保留原值,不释放存储空间,在下一次调用该函数时,继续使用上次所保留的值,这种局部变量称为静态局部变量,用static
作存储类别的声明。
#include <stdio.h>
int main(){
//函数声明
void test();
//第一次调用输出:3
test();
//第二次调用输出:4
test();
return 0;
}
void test(){
static int a = 2;
a++;
printf("%d\n", a);
}
静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在整个程序运行期间都不释放。
虽然静态局部变量在函数调用结束后仍然存在,但是其他函数依然不能引用它。因为它是局部变量。
(3)register变量
一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当需要使用变量时,由控制器发出指令将内存中该变量的值取出来到运算器中,使用结束后若需要存值,则再一次从运算器将数据送到内存中。如果有一些变量使用频繁,则需要花费不少时间。为提高效率,允许将变量的值存放在CPU的寄存器中,由于寄存器的存取速度远大于内存,因此提高了执行效率。这种变量叫做寄存器变量,用关键字register
作声明。
由于现在计算机的速度越来越快,性能越来越高,优化的编译系统能够识别使用频率高的变量。从而自动地将这些变量放在寄存器中。因此,
register
声明的必要性不大。
4.2 全局变量的存储类别
全局变量都是存放在静态存储区中的。
(1)在一个文件中扩展外部变量的作用域
如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束,在定义点之前的函数不能引用该外部变量。如果希望在定义点之前的函数能引用该外部变量,则应该在引用之前用关键字extern
对该变量作外部变量声明,表示把外部变量的作用域扩展到此位置。
#include <stdio.h>
int main(){
void test();
test();
return 0;
}
//将外部变量c的作用域扩展到此处
extern char C[];
void test(){
printf("%s\n", C);
}
char C[] = "hello";
- 提倡将外部变量的定义放在引用它的所有函数之间,这样可以避免多加一个
extern
声明。- 用
extern
声明外部变量时,类型名可以写也可以不写,如:"extern int A
“可以写成”extern A
"。因为它不是定义变量。
(2)将外部变量的作用域扩展到其他文件
一个C程序是由一个或多个源文件组成的。那么在一个文件中如果想引用另一个文件已定义的外部变量,则须在未定义外部变量的那个文件中用extern
作外部变量声明,这也是extern
的第二个作用。
//test.c文件
int A = 2;
//main.c文件
#include <stdio.h>
extern int A;
int main(){
//输出:2
printf("%d", A);
return 0;
}
用这种方法扩展全局变量的作用域需要谨慎使用,因为如果改变了这个全局变量的值,则整个引用该变量的文件都会受到影响。
(3)将外部变量的作用域限制在本文件中
若希望外部变量只限于被本文件引用,而不能被其他文件引用。则使用关键字static作声明,这种只能用于本文件引用的外部变量称为静态外部变量。
//test.c文件
static int A = 2;
//main.c文件
#include <stdio.h>
extern int A;
int main(){
//报错,变量A为静态外部变量,无法被其他文件引用
printf("%d", A);
return 0;
}
static
用于声明局部变量的存储类型和声明全局变量的存储类型的含义是不同的:
- 对于局部变量,
static
声明存储类型的作用是指定变量存储的区域(静态存储区和动态存储区)。- 对于全局变量,
static
声明存储类型的作用是变量作用域的扩展问题。因此,不要误认为对外部变量加
static
声明后才采取静态存储方式,不加static
声明的外部变量同样是静态存储方式。
4.3 总结
下面从不同角度做些归纳:
(1)从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:
(2)从变量存在的时间(生存期)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行期间都存在的,而动态存储则是在调用函数临时分配单元。
(3)从变量值存放的位置来区分,可分为:
5. 内部函数和外部函数
函数本质上是全局的(即外部函数),因为定义一个函数的目的就是要被另外的函数调用。如果不加声明的话,一个文件中的函数可以被本文件中其他函数调用,也可以被其他文件中的函数调用。但是,也可以指定某些函数不能被其他文件调用。
5.1 内部函数
如果一个函数只能被本文件中其他函数所调用,称为内部函数,又称静态函数,用关键字static
定义内部函数。
static void test(){
printf("%s\n", C);
}
5.2 外部函数
如果一个函数可以供其他文件调用,称为外部函数,用关键字extern
定义外部函数。C语言规定,如果在定义函数时省略extern
,则默认认为外部函数。但调用其他文件中的函数,对这个外部函数作声明时,要加extern
,表示它来自其他文件。
//test.c文件
//可省略extern
void test(){
printf("%s\n", "hello");
}
//main.c文件
#include <stdio.h>
int main(){
//作声明时不可省略extern
extern void test();
test();
return 0;
}
四、指针
通过变量地址找到该变量单元,我们形象化地把这种地址称为指针。
在C语言中,数据是分类型的,对不同类型的数据,在内存中分配的存储单元大小(字节数)和存储方式是不同的(如整数以补码形式存放,实数以指数形式存放)。因此,为了有效地存放一个数据,除了需要位置信息外,还需要有该数据的类型信息。
C语言中的地址包括位置信息(内存编号,即纯地址)和它所指向的数据的类型信息。
对变量的访问都是通过地址进行的。
1. 指针变量
通常的,我们都是直接按变量名进行访问,这种访问我们称为直接访问。
//把键盘输入的值送到该地址的整型存储单元
scanf("%d",&i);
//从两个地址中分别取出i和j的值,然后相加再送入k的地址所在存储单元中
k = i + j;
还有一种称为间接访问,即变量i
的地址存放再另一个变量中,然后通过该变量来找到变量i
的地址,从而访问变量i
。这种有一个变量专门用来存放另一个变量的地址(指针),称为指针变量。指针变量就是地址变量,用来存放地址,指针变量的值就是地址。
1.1 定义指针变量
定义指针变量的一般形式:类型名 * 指针变量名;
int * pointer;
左端的int
是在定义指针变量时必须指定的基类型,它是用来指定此指针变量可以指向的变量的类型,如上一个例子中变量pointer
只能指向整型变量。
- 指针变量前的
*
表示该变量为指针变量。指针变量名是pointer
,而不是* pointer
。- 在定义指针变量时必须指定基类型。
- 指针的含义包括两个方面,一是以存储单元编号表示的纯地址,一是它指向的存储单元的数据类型。
- 指针变量只能存放地址,不能将一个整数等赋给一个指针变量。
- 在程序中是不能用数值代表一个地址的,地址只能用地址符
&
得到并赋给一个指针变量,如p = &a;
。
1.2 引用指针变量
int a = 8;
//初始化指针变量
int * p = &a;
//输出引用指针变量所指向的变量
printf("%d", *p);
//输出引用指针变量的值(即地址)
printf("%d", p);
要熟练掌握两个有关的运算符:
&
:取地址运算符。&a
是变量a
的地址。*
:指针运算符。*p
代表指针变量p
指向的对象。
指针变量还可以作为函数参数,下面举一个例子:通过指针实现两个变量值的交换。
#include <stdio.h>
int main(){
void swap(int * p1, int * p2);
int a = 3, b = 5;
int * pointer1 = &a;
int * pointer2 = &b;
//a = 3, b = 5
printf("a = %d, b = %d\n", a, b);
swap(pointer1, pointer2);
//a = 5, b = 3
printf("a = %d, b = %d", a, b);
return 0;
}
void swap(int * p1, int * p2){
int temp = * p1;
* p1 = * p2;
* p2 = temp;
}
2. 通过指针引用数组
在前面数组的学习我们知道,数组名就是数组首元素的地址,因此,下面两个语句等价:
int * p = &a[0];
int * p = a;
通常我们引用数组元素可以用下标法,如a[3]
,现在我们可以使用指针法,即通过指向数组元素的指针找到所需的元素。指针法能使目标程序质量更高(占内存少,运行速度快)。
2.1 指针的运算
当指针指向数组元素的时候,允许对指针进行加减的运算。譬如,指针变量p
指向数组元素a[1]
时,我们可通过加减的形式(指针法)去寻找其他数组元素,如:
p+1
指向同一数组中的下一个元素,即a[2]
。p-1
指向同一数组中的上一个元素,即a[0]
。((p+1)-(p-1))/数组元素所占字节数
的结果等于两个元素之间的相对位置,即结果为2,表示p+1
所指的元素与p-1
所指的元素之间差2个元素。
执行指针运算并不是单纯地加减,而是根据其基类型所占字节数加减的。如:对于int
型指针变量p,其指向的数组元素占4个字节,则p+1
就表示使p
的值(地址)加4个字节,以使它指向下一个元素。若p
的值为2000,则p+1
的值就为2004,而不是2001。
- 两个地址不能相加,如
((p+1)-(p-1))
是无意义的。[]
实际上是变址运算符,如a[i]
按a+i
计算地址,然后找出此地址单元中的值。
2.2 通过指针引用数组元素
通过学习指针的运算,我们可以使用另一种方法进行数组元素的引用,那就是指针法。利用*
地址运算符和指针运算可以实现对数组元素的操作。
因此,我们有三种方法来实现数组元素的引用:
- 下标法
- 通过数组名计算数组元素地址
- 指针变量指向数组元素,即指针法
我们通过三个例子进行对比:
下标法
#include <stdio.h>
int main(){
int a[] = {1, 2, 3, 4, 5, 6, 7};
for (int i = 0; i < 7; i++){
printf("%d ", a[i]);
}
return 0;
}
//输出结果:1 2 3 4 5 6 7
通过数组名计算数组元素地址
#include <stdio.h>
int main(){
int a[] = {1, 2, 3, 4, 5, 6, 7};
for (int i = 0; i < 7; i++){
printf("%d ", *(a + i));
}
return 0;
}
//输出结果:1 2 3 4 5 6 7
指针法
#include <stdio.h>
int main(){
int a[] = {1, 2, 3, 4, 5, 6, 7};
int *p = a;
for (; p < a + 7; p++){
printf("%d ", *p);
}
return 0;
}
//输出结果:1 2 3 4 5 6 7
从三个例子可以得出结论:
- 第1种方法和第2种方法执行效率是相同的。C编译系统是将
a[i]
转换为*(a+i)
处理的,即先计算元素地址。 - 第3种比第1种和第2种快,用指针变量直接指向元素,不必每次重新计算地址,大大提升了效率。
- 用下标法比较直观,能直接知道是第几个元素。
需要注意的是,不能对数组名进行变换,因为数组名
a
是数组首元素的地址,它是一个指针型常量,它的值是固定不变的,所以a++
是无法实现的。
2.3 用数组名作函数参数
之前学习函数时已经了解了数组名作函数参数。通过学习指针我们知道,指针和数组名是等价的,实际上,C编译都是将形参数组名作为指针变量来处理的。
//两种方式互相等价
void test(int * arr){}
void test(int arr[]){}
下面用变量名作为函数参数和用指针变量作为函数参数做一比较:
实参类型 | 要求形参的类型 | 传递的信息 | 通过函数调用能否改变实参的值 |
---|---|---|---|
变量名 | 变量名 | 变量的值 | 不能 |
数组名或指针变量 | 数组名或指针变量 | 实参数组首元素地址或变量地址 | 能 |
C语言调用函数时虚实结合的方法都是采用值传递方式,当用变量名作为函数参数时传递的是变量的值。当用数组名或指针变量作为函数参数时,由于它们都代表地址,因此传递的值是地址,所以要求形参为指针变量,我们也可以称之为地址传递。
实参数组名代表一个固定的地址,或者说是指针型常量,但形参数组名不是一个固定地址,因为C编译系统将它处理为了指针变量。
2.4 通过指针引用多维数组
多维数组的指针运用,本质上就是叠加式的指针、数组中的数组,并且数组名不仅仅是二维数组首元素地址了,而是多个元素组成的一维数组,我们通过一张图来了解。
通过以下问题进行讨论:
- 为何
a*
和a
的值都是2000?我们可以这么理解,a
表示二维数组名,值为二维数组首元素地址,二维数组首元素也是一维数组名,它的值为一维数组首元素地址。因此,二者的纯地址是相同的,但他们的基类型是不同的,即指针指向的数据类型是不同的,前者是一维数组,后者是整型数据。 - 为何
a+1
的值不是2004而是2012?这是因为a指向的是二维数组首元素,也就是一维数组,它的跨越单位是一维数组,因此加1表示跨一单位的一维数组。而每个一维数组都是3个int型数据组成的,因此加1也表示在地址上加12个字节。从a[0]+3
的值为2012也可以看出,a[0]是一维数组名,指向的是一维数组首元素,它的跨越单位是int型数据,因此加3表示跨三单位的int型数据,也就是12个字节。
2.5 指向一维数组的指针变量
对于指向int
型一维数组的指针变量p
,其基类型是一维数组,长度是16字节。因此p
每加1,地址就增加16个字节。这也就解释了上一节二维数组指针运算问题。
那么如何去单独的创建指向一维数组的指针变量,对于int * p
的方式去定义,它是指向整型数据的而不是数组,显然是错的。
指向一维数组的指针变量定义的一般形式为:int (*p)[len]
通过一例子来简单使用:
#include <stdio.h>
int main(){
int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*p)[3] = a;
//输出数组a第1行第1个列:5
printf("%d", *(*(p+1)+1));
return 0;
}
3. 通过指针引用字符串
在C程序中,字符串是存放在字符数组中的。想引用一个字符串,可以用以下两种方法:
- 用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明
"%s"
输出字符串。
#include <stdio.h>
int main(){
char str[] = "hello";
printf("%s", str);
return 0;
}
- 用字符型指针变量指向一个字符串常量并引用。
#include <stdio.h>
int main(){
char *str = "hello";
printf("%s", str);
return 0;
}
在C语言中只要字符变量,没有字符串变量。
str
被定义为一个字符型指针变量,它只能指向一个字符数据,而不是多个字符数据,更不是把字符串常量存放到str
中,它只是把字符串的首字符地址赋值给str
指针变量。
虽然字符数组和字符指针变量都能实现字符串的存储和运算,但它们之间还是有很大区别的,主要有几点:
- 字符数组是由若干个元素组成的,每个元素存放一个字符,而字符指针变量中存放的是字符串首字符的地址。
- 可以对字符指针变量赋值,但不能对数组名赋值。
- 存储单元的内容。编译时为字符数组分配若干个存储单元,而对于字符指针变量,只分配一个存储单元用来存放字符地址。
如果定义了字符指针变量,应当及时把一个字符变量地址赋给它,如果未对它赋一个地址值,其存储单元的值将不可预料,可能指向的内存中空白的用户存储区中,也有可能指向已存放指令或数据的有用内存段,此时你再输入数据,就会造成严重的后果。
//错误示例:
char *a;
//企图从键盘输入一个字符串,使a指向该字符串,错误
scanf("%s", a);
//正确示例:
char *a, arr[6];
a = arr;
scanf("%s", a);
- 指针变量的值可以改变,而字符数组名代表一个固定的值(数组首元素地址)。
- 字符数组中各元素的值可以改变,但字符指针变量指向的字符串常量中的各个字符不可以被重新赋值。
- 用指针变量指向一个格式字符串时,可以用它取代
printf()
函数中的格式字符串。
#include <stdio.h>
int main(){
char *str = "hello";
char *format = "%s";
printf(format, str);
return 0;
}
这种printf()
函数称为可变格式输出函数。虽然也可以使用字符数组实现,但是它不能用赋值语句整体赋值,因此,使用指针变量指向字符串的方式更为方便。
4. 指向函数的指针
在程序中定义一个函数,在编译时会把函数的源代码转换为可执行代码并分配一段存储空间。这段内存空间有一个起始地址,也就是函数的入口。每次调用函数时都会从该地址开始执行。函数名代表函数的起始地址,即函数名就是函数的指针。我们可以定义一个指向函数的指针变量,用来存放某一函数的起始地址。
4.1 使用函数指针变量
定义指向函数的指针变量的一般形式为:返回类型 (*变量名)(参数类型);
//指针变量p指向函数类型为整型且有两个整型参数的函数
int (*p)(int, int);
给函数指针变量赋值时只需给出函数名而不必给出参数。而用函数指针变量调用函数时,只需将(*p)
代替函数名即可,然后在后面的括号中根据需要写上参数。
#include <stdio.h>
int main(){
//函数声明
int max(int, int);
//初始化函数指针变量
int (*p)(int, int) = max;
//通过指针变量调用函数并输出:5
printf("%d", (*p)(3, 5));
return 0;
}
//定义函数
int max(int x, int y){
if(x > y){
return x;
}else{
return y;
}
}
对指向函数的指针变量不能进行算术运算。
4.2 函数指针变量作函数参数
指向函数的指针变量的一个重要用途是把函数的入口地址作为参数传递到其他函数。
下面通过一个例子。输入max,chos
函数调用max
函数,输入min,chos
函数调用min
函数。
#include <stdio.h>
int main(){
int max(int, int);
int min(int, int);
void chos (int (*p)(int, int), int x, int y);
char str[6];
printf("max or min?\n");
gets(str);
printf("%s\n", str);
if(strcmp(str, "max") == 0){
chos(max, 3, 5);
}else if(strcmp(str, "min") == 0){
chos(min, 3, 5);
}
return 0;
}
void chos (int (*p)(int, int), int x, int y){
printf("%d", (*p)(x, y));
}
//返回最大值
int max(int x, int y){
if(x > y){
return x;
}else{
return y;
}
}
//返回最小值
int min(int x, int y){
if(x < y){
return x;
}else{
return y;
}
}
4.3 返回指针值的函数
一个函数可以返回整型、字符型等,也可以返回指针型的数据,即地址。
定义返回指针型的函数原型的一般形式为:类型名 * 函数名(参数表列);
通过一个例子,假如有3名学生,每个学生有两门课的成绩,实现通过输入序号来查询学生成绩。
#include <stdio.h>
int main(){
float score[][2] = {{32.2, 94}, {63, 89}, {52.3, 100}};
float * search(float (*p)[2], int n);
int n;
scanf("%d", &n);
float *theScore = search(score, n);
for (int i = 0; i < 2; i++){
printf("%.1f ", *(theScore + i));
}
return 0;
}
float * search(float (*p)[2], int n){
float * ff = *(p + --n);
return ff;
}
5. 指针数组和多重指针
一个数组若其元素均为指针类型数据,则称为指针数组。
定义一维指针数组的一般形式为:类型名 * 数组名[数组长度];
对于指针数组比较适合用来指向若干个字符串,使字符串处理更加方便灵活。
#include <stdio.h>
int main(){
char *name[3] = {"zs", "ls", "ww"};
void printAll(char *p[], int n);
printAll(name, 3);
return 0;
}
void printAll(char *p[], int n){
int i = 0;
char *pt = p[0];
while (i < n){
pt = *(p + i++);
printf("%s ", pt);
//输出:zs ls ww
}
}
5.1 指向指针数据的指针变量
在指针数组的基础上,需要了解指向指针数据的指针变量,简称指向指针的指针。
对于上一例中的指针数组name
,它的每一个元素都是一个指针型变量,其值为地址。数组名name
代表该指针数组首元素的地址。name + 1
是name[1]
的地址,那么name + i
就是指向指针型数据的指针。因此我们还可以定义一个指针变量p
来指向指针数组的元素,则p
就是指向指针的指针变量。
定义指向指针的指针变量的一般形式为:类型名 **变量名;
对上一例进行改良。
#include <stdio.h>
int main(){
char *name[3] = {"zs", "ls", "ww"};
void printAll(char *p[], int n);
printAll(name, 3);
return 0;
}
void printAll(char *p[], int n){
int i = 0;
//指针型指针变量pt指向指针数组p的首元素
char **pt = p;
while (i < n){
printf("%s ", *pt);
pt++;
}
}
在一个指针变量中存放一个目标变量的地址,称为单级间址。那么如果在一个指针变量中存放一个指针变量的地址,则称为二级间址。理论上,间址方法可以延伸到更多级,即多重指针。但实际上在程序中很少有超过二级间址的。
5.2 指针数组作main函数的形参
指针数组的一个重要应用是作为main函数的形参。
一般情况下,main函数写成如下形式:
int main()
//或者
int main(void)
它们表示main函数无参数,调用main函数无需给出实参。实际上,在某些情况下,main函数可以有参数,即:
int main(int argc, char *argv[])
其形参为程序的命令行参数,argc
表示参数个数,argv
表示参数向量。
通常main函数和其他函数组成一个文件模块。对这个文件进行编译和连接,得到可执行文件(.exe)。用户执行这个可执行文件,操作系统就会调用main函数,然后由main函数调用其他函数,从而完成程序功能。
而main函数的形参从何而来,显然不可能在程序中得到,实参只能由操作系统给出。通过操作命令行,实参和执行文件命令一起给出。
命令行的一般形式为:命令名 参数1 参数2……参数n
假设可执行文件名为file1.exe
,先想将"zs"
、"ls"
、"ww"
三个字符串作为参数传入main函数中。
//命令行:main zs ls ww
#include <stdio.h>
int main(int argc, char *argv[]){
void printAll(char *p[], int n);
printAll(argv, 3);
return 0;
}
void printAll(char *p[], int n){
int i = 0;
char **pt = p;
while (i < n){
printf("%s ", *pt);
pt++;
}
}
6. 动态内存分配与指向它的指针变量
全局变量分配在内存中的静态存储区,非静态的局部变量分配在内存的动态存储器,这个存储区是一个称为栈的区域,除此之外,C语言还允许建立内存动态分配区域,以存放一些临时数据,这个数据需要时随时开辟,不需要时随时释放,这个特别的自由存储区,称为堆。
6.1 建立内存的动态分配
对内存的动态分配是通过系统提供的库函数实现的:
首先使用这些函数需要在头文件中声明# include <stdlib.h>
指令。
(1)用malloc
函数开辟动态存储区
函数原型为:void * malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size
的连续空间。返回值是分配区域的第一个字节的地址(指针)。
malloc(100); //开辟100字节的临时分配域,函数值为其第1个字节的地址
注意指针的基类型为void,即不指向任何类型的数据,只提供一个纯地址。如果此函数未能成功执行(如内存空间不足),则返回空指针
NULL
。
(2)用calloc
函数开辟动态存储区
函数原型为:void * calloc(unsigned n, unsigned size)**;
其作用是在内存的动态存储区中分配n
个长度为size
的连续空间,这个空间较大,足以存放一个数组,即动态数组。
p = calloc(50, 4); //开辟50x4个字节的临时分配域,把首地址赋给指针变量p
(3)用realloc
函数重新分配动态存储区
函数原型为:void * realloc(void *p, unsigned int size);
其作用是改变已有动态空间的大小进行重新分配,将指针变量p
指向的的动态空间大小改变为size
,p
的值(地址)不变。
realloc(p, 50); //将p所指向的已分配的动态空间改为50字节
(4)用free
函数释放动态存储区
函数原型为:void * free(void *p);
其作用是释放指针变量p所指向的动态空间,使这部分空间能重新被其他变量使用,无返回值。
free(p);
6.2 void指针类型
C99允许使用基类型为void
的指针类型。定义的void
型指针变量不指向任何类型的数据。可以理解它为指向空类型或不指向确定的类型的数据,再将它的值赋给另一个指针变量时由系统对它进行类型转换,起到过渡性,使之适合于被赋值的变量的类型。
#include <stdio.h>
int main(){
int a = 3;
int *p1 = &a;
char *p2;
void *p3;
int *p4;
p3 = (void *)p1; //将p1的值转换为void*类型,然后赋值给p3
p2 = (char *)p3; //将p3的值转换为char*类型,然后赋值给p2
p4 = (int *)p2; //将p2的值转换为int*类型,然后赋值给p4
printf("%d", *p4); //合法输出
printf("%c", *p2); //合法输出
return 0;
}
前面提过,地址应包含基类型的信息,否则无法实现对数据的存取。那么,为什么又允许用
void
型指针变量呢?这种指针是无指向的,而无指向的地址所标志的存储单元中是不能存储任何数据的,也就是说,无法通过这种地址对内存存取数据。针对
void
型指针,大多是用于在调用动态存储分配函数时出现的,因为所开辟的动态空间所存放的数据类型并不唯一,所以要求动态存储分配函数所返回的指针无指向,即void
型指针。现在所用的一些编译系统在进行地址赋值时,会自动进行类型转换,如:
pt = (int *)malloc(100);
可简化为pt = malloc(100);
下面通过一例来了解如何建立内存动态分配区和使用void指针。
//建立动态数组并输出
# include <stdio.h>
# include <stdlib.h>
int main(){
int *p1 = malloc(5 * sizeof(int)); //自动类型转换
for(int i = 0; i < 5; i++){
scanf("%d",p1 + i);
}
for(int i = 0; i < 5; i++){
printf("%d ",*(p1 + i));
}
return 0;
}
内存的动态分配主要应用于建立程序中的动态数据结构。
7. 总结
指针变量的类型及含义:
变量定义 | 类型表示 | 含义 |
---|---|---|
int i; | int | 定义整型变量i |
int *p; | int * | 定义p为指向整型数据的指针变量 |
int a[5] | int [5] | 定义整型数组a,它有5个元素 |
int *p[5] | int *[5] | 定义指针数组p,它由5个指向整型数据的指针元素组成 |
int (*p)[5] | int (*)[5] | p为指向包含5个元素的一维数组的指针变量 |
int f(); | int () | f为返回整型数值的函数 |
int * p(); | int * () | p为返回一个指针的函数,该指针指向整型数据 |
int (*p)(); | int (*)() | p为指向函数的指针,该函数返回一个整型值 |
int **p; | int ** | p是一个指针变量,它指向一个指向整型数据的指针变量 |
void * p; | void * | p是一个指针变量,基类型为void,不指向具体的对象 |
五、结构体和共用体
C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体。
1. 定义和使用结构体变量
声明一个结构体类型的一般形式为:
struct 结构体名
{成员表列};
结构体类型的名字是由一个关键字struct
和结构体名组合而成的。结构体名由用户指定,以区别于其他结构体类型。花括号内是该结构体所包括的子项,称为结构体的成员,即成员表列(域表),每一个成员是结构体中的一个域。
示例:
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
习惯将结构体名、共用体名和枚举名的首字母大写。
1.1 定义结构体类型
相比一般变量声明,声明一个结构体类型并未分配存储单元,只有定义结构体类型变量并存放了数据,才能在程序中使用结构体类型的数据。可以采取以下3种方法定义结构体类型变量。
(1)先声明结构体类型,再定义该类型的变量
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct Student s1, s2;
在定义了结构体变量后,系统会为之分配内存空间,内存空间大小根据结构体包含的成员而定(成员大小总和)。
(2)在声明类型的同时定义变量
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} s1, s2;
作用和第一种方法相同。
(3)不指定类型名而直接定义结构体类型变量
struct {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} s1, s2;
指定了一个无名的结构体类型,它没有名字。因此只有一次定义变量的机会。
- 结构体类型与结构体变量是不同的概念,只能对变量赋值、存取或运算,而不能对类型赋值、存取或运算。在编译时,对类型不分配空间,只对变量分配空间。
- 结构体类型中的成员名可以和程序中的变量名相同,但二者不代表同一对象。
- 对结构体变量中的成员可以单独使用,它的作用与地位相当于普通变量。
1.2 结构体变量初始化和引用
在定义结构体变量时,可以对它初始化,即赋予初始值。然后可以引用这个变量。
示例:
# include <stdio.h>
int main(){
struct Student{
int num;
char name[20];
char sex;
int age;
float score;
};
struct Student s1 = {1, "张三", 'Bb', 18, 88.2}; //定义并初始化结构体变量
//引用变量成员并输出
printf("num:%d, name:%s, sex:%c, age:%d, score:%.1f", s1.num, s1.name, s1.sex, s1.age, s1.score);
//输出:num:1, name:张三, sex:B, age:18, score:88.2
return 0;
}
初始化列表使用花括号括起来的一些常量,这些常量依次赋给结构体变量的各成员。注意:是对结构体变量初始化,而不是对结构体类型初始化。
C99标准允许对某一个成员初始化:
struct Student s = {.name = "zs"};
,.name
隐含代表结构体变量s中的成员s.name
,其他未被指定初始化的数值型成员被系统初始化为0
,字符型成员被系统初始化为'\0'
,指针型成员被系统初始化为NULL
。
结构体变量中成员的引用方式为:结构体变量名.成员变量
s.num = 11;
你可以在结构体变量初始化时赋值,也可以之后赋值,.
是成员运算符,它在所有的运算符中优先级最高,因此可以把s.nun
当作一个整体,相当于一个变量。
不能企图通过输出结构体变量名来达到输出结构体变量所有成员的值,只能对结构体变量中的各个成员分别进行输入和输出。
printf("%s", s); //错误
如果成员本身也是结构体变量,则需要若干个成员运算符,一级一级地找到最低的一级成员。成员也可以像普通变量一样进行各种运算。
s.birth.month = 3; //多级成员
s.birth.month++; //.成员运算符优先级最高,不影响自加运算
同类的结构体变量可以互相赋值。
s1 = s2; //s2中的成员覆盖s1中的成员
对于结构体变量成员和结构体变量,都可以引用其地址。
print("%o", &s1);
scanf("%d", &s1.num);
结构体变量的地址主要用作函数参数,传递结构体变量的地址。
2. 结构体数组和指针
2.1 结构体数组
一个结构体变量中可以存放一组有关联的数据,如果是多组同样的数据,显然应该使用数组,也就是结构体数组。
定义结构体数组的一般形式:
//声明类型时定义结构体数组
struct 结构体名
{成员表列} 数组名[数组长度];
//声明类型后,再定义结构体数组
struct Student arr[3];
示例:
# include <stdio.h>
int main(){
struct Student{
int num;
char name[20];
};
struct Student arr[3] = {1, "zs", 2, "ls", 3, "ww"};
//struct Student arr[3] = {{1, "zs"}, {2, "ls"}, {3, "ww"}}; 另一种形式
for (int i = 0; i < 3; i++){
printf("num=%d, name=%s\n", arr[i].num, arr[i].name);
}
return 0;
}
/**
输出:
num=1, name=zs
num=2, name=ls
num=3, name=ww
*/
2.2 结构体指针
所谓结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。指针变量即可指向结构体变量,也可指向结构体数组。指针变量的基类型必须与结构体变量的类型相同。
示例:
# include <stdio.h>
int main(){
struct Student{
int num;
char name[20];
};
struct Student arr[3] = {{1, "zs"}, {2, "ls"}, {3, "ww"}};
struct Student *p = arr;
for (int i = 0; i < 3; i++){
printf("num=%d, name=%s\n", (p + i) -> num, (p + i) -> name);
//printf("num=%d, name=%s\n", (*(p + i)).num, (*(p + i)).name); 另一种形式
}
return 0;
}
/**
输出:
num=1, name=zs
num=2, name=ls
num=3, name=ww
*/
指针访问有两种形式,一种是常规的*
指针运算符,还有一种是适用于结构体变量的->
指向运算符。
- 在使用
*
指针运算符时需注意,*p
两侧的括号不可省,因为.
成员运算符优先级大于*
指针运算符。 ->
指向运算符是C语言为了使用方便和直观,允许把(*p).num
用p->num
代替,表示p
所指向的结构体变量中的成员num
,两种方式互相等价。
- 结构体指针变量
p
应指向结构体变量,而不是结构体变量的某一成员,编译时将发生警告信息,表示地址的类型不匹配。如果一定要将某一成员赋值给p
,那么可以使用强制类型转换。如:p = (struct Student*)s.name;
- 结构体变量成员作函数实参,采取的是值传递。结构体变量作函数实参,采取的也是值传递,且内存开销大,不建议。结构体变量的指针作实参,采取的是地址传递。
3. 指针处理链表
链表是一种常用且重要的数据结构,它是动态地进行存储分配的一种结构。在用数组存放数据时,必须先定义固定的数组长度。假设事先不知道数据量有多大,则必须把数组长度定义到足够大,以便存放任何数据,这将会很浪费内存。而链表没有这个缺点,它根据需要开辟内存单元,它的长度是动态的。
head
表示链表的头指针变量,它存放一个地址,该地址指向一个元素。- 链表中每一个元素称为结点,每个结点都包含两个部分:实际数据和下一结点的地址。
- 最后一个元素不指向任何其他元素,称为表尾,它的地址部分存放
NULL
,链表到此结束。 - 链表中各元素在内存中的地址可以是不连续的,要找一个元素,必须先找到上一个元素,根据所提供的地址才能找到。因此,如果不提供头指针,则整个链表都无法访问。
显然,链表必须利用指针变量才能实现,即每个结点都应包含一个指针变量用来存放下一个结点的地址。
3.1 静态链表
所有结点都是在程序中定义、不是临时开辟的且不能用完就释放的,这种链表称为静态链表。
示例:
# include <stdio.h>
int main(){
struct Student{
int num;
char *name;
struct Student* next;
};
struct Student *head, *p;//定义头指针
struct Student s1, s2, s3; //定义结点
s1.num = 1; s1.name = "zs";
s2.num = 2; s2.name = "ls";
s3.num = 3; s3.name = "ww";
head = &s1; //将结点s1的起始位置赋给头指针head
s1.next = &s2; //将结点s2的起始位置赋给结点s1的next成员
s2.next = &s3; //将结点s3的起始位置赋给结点s2的next成员
s3.next = NULL; //将NULL赋给结点s3的next成员,表示这是表尾
p = head; //使p指向头指针
while (p != NULL){
printf("num=%d,name=%s\n", p->num, p->name);
p = p->next; //依次指向下一个结点,直到值为NULL时,循环结束
}
return 0;
}
3.2 动态链表
所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。这里将会用到动态内存分配相关函数。
示例:
# include <stdio.h>
# include <stdlib.h>
//定义结构体类型
struct Student{
int num;
float score;
struct Student* next;
};
int main(){
struct Student * creat();
struct Student *pt = creat();
//遍历链表
while (pt != NULL){
printf("num=%d, score=%.1f\n", pt->num, pt->score);
pt = pt->next;
}
return 0;
}
//动态创建链表
struct Student * creat(){
struct Student *head;//定义头指针
struct Student *p1, *p2;//用于动态转换前后结点地址
char c;
p1 = malloc(sizeof(struct Student)); //开辟内存单元
printf("请输入学号和成绩:\n");
scanf("%d %f", &p1->num, &p1->score); //输入实际数据
head = p1; //使头指针指向p1
p1->next = NULL; //使p1指向的结构体变量的成员next指向NULL
printf("已录入,是否继续录入? Y/N\n");
scanf("\r%c", &c); //选择是否继续添加数据
while (c == 'Y'){
p2 = p1; //把p1指向的结构体变量地址赋给p2
p1 = malloc(sizeof(struct Student)); //再次让p1获得新开辟的内存单元地址
p2->next = p1; //把p1指向的新结构体变量地址赋给p2指向的结构体变量的成员next
scanf("%d %f", &p1->num, &p1->score);
p1->next = NULL; //让新结构体变量的成员next指向NULL
printf("已录入,是否继续录入? Y/N\n");
scanf("\r%c", &c);
}
printf("录入结束。\n");
return head;
}
4. 共用体类型
用同一段内存单元存放不同类型的变量,即使几个不同的变量共享同一段内存的结构,称为共用体(联合)。
定义共用体类型变量的一般形式为:
union 共用体名
{
成员表列;
} 变量表列;
//示例
union Data{
int i;
char c;
float f;
} a, b, c;
结构体和共用体的定义形式相识,这里不再阐述。
结构体变量所占内存长度是各成员内存长度之和。而共用体变量所占内存长度等于最长的成员长度。
先定义,再引用。只能通过.
成员运算符引用变量成员,但不能引用共用体变量。在使用共用体类型数据时需注意:
- 同一内存段虽然可以存放不同类型的成员,但是在每一瞬时只能存放其中一种成员的值,而不是同时存放几个成员的值。意思就是共用体变量只能存放一个值。
- 共用体初始化时只能有一个常量。
- 共用体变量中有值的成员是最后一次被赋值的成员,在对共用体变量中的一个成员赋值后,原有变量存储单元的值就被取代了。
- 共用体变量的地址和它各成员的地址都是同一地址。
- 不能对共用体变量名赋值。
- C99允许同类型的共用体变量互相赋值。
- C99允许用共用体变量作为函数参数(之前不允许)。
示例:
# include <stdio.h>
int main(){
union Student{
int num;
char *name;
};
union Student s = {.name = "zs"};
printf("name=%s, &name=%d\n", s.name, &s.name);
s.num = 88;
printf("num=%d, &num=%d\n", s.num, &s.num);
return 0;
}
/**
输出:它们的地址相同
name=zs, &name=6422040
num=88, &num=6422040
*/
往往在数据常量中,有时需要对同一空间安排作不同的用途,这时用共用体类型比较方便,能增加程序处理的灵活性。
5. 枚举类型
如果一个变量只有几种可能的值,则可以定义枚举,指把可能的值一一列举出来,变量的值只限于列举出来的值的范围内。
声明枚举类型的一般形式为:enum 枚举名 {枚举元素列表};
示例:
# include <stdio.h>
int main(){
enum Weekday{mon, tue, wed, thu, fri, sat, sun} w1, w2, w3;
w1 = mon;
w2 = tue;
w3 = fri;
//w1 = aaa; 错误,aaa不属于其指定的枚举常量
printf("w1=%d,w2=%d,w3=%d", w1, w2, w3);
return 0;
}
//输出:w1=0,w2=1,w3=4
w1
、w2
和w3
被定义为枚举变量,而花括号里的mon
, tue
等被称为枚举元素或枚举常量。
- 被称为枚举常量是因为C编译对枚举类型的枚举元素按常量处理,而不是变量,因此也不能赋值。
- 枚举变量的值只能是指定的枚举元素。
- 每一枚举元素都代表一个整数,C编译按定义时的顺序默认它们的值为0,1,2,3……。因此,枚举常量也可以引用和整型输出。也可以人为指定枚举元素的数值,在枚举类型定义时显式地指定即可,如:
enum Weekday{mon=4, tue=2, wed, thu, fri, sat, sun}
,没显式指定的枚举元素数值为前一个元素数值加1。 - 枚举元素可以用来做判断比较。
- 可以在定义枚举变量时不声明有名字的枚举类型,因此只能定义一次。
使用枚举的好处就是其枚举变量更为直观,因为它们都选用了见名知义的名字。此外,枚举变量的值限制在已知的几个枚举元素范围内,利于检查排错。
6. typedef
可以用typedef
指定新的类型名来代替已有的类型名。
示例:
typedef int Integer; //命名一个新的类型名代表基本类型
Integer i;
typedef struct date{ //命名一个新的类型名代表结构体类型
int month;
int day;
int year;
} Date;
Date d;
typedef int Num[100]; //命名一个新的类型名代表数组类型
Num n;
typedef char *String; //命名一个新的类型名代表指针类型
String p, s[10]; //定义p为字符指针变量,定义s为字符指针数组
typedef int (* Pointer)(); //命名一个新的类型名代表指向函数的指针类型
Pointer p1;
简单来说,就是按定义变量的方式,把变量名换上新类型名,并且在最前面加typedef
,就声明了新类型名代表原来的类型。
- 习惯上,常把
typedef
声明的类型名的第1个字母用大写表示。- 用
typedef
只是对已存在的类型指定一个新的名称,并无创造新的类型。- 对于多个源文件,可以把
typedef
名称声明单独放在一个头文件中,然后在其他源文件中#include
引入即可。- 用
typedef
有利于程序的通用和移植。