C语言基础笔记

数据类型、运算符与表达式

数据类型

整型数据

  • C语言将能处理的数据分成两大类型:基本类型和构造类型,构造类型的数据是由若干个基本类型或构造类型按一定的结构组合而成的。
  • C语言规定:在程序中用到的数据,都必须指定其数据类型。

数据类型

  • 整型数据占用的存储空间和取值范围
类型名称类型说明符等同类型说明符字节数位数取值范围
基本整型int432-231~231 - 1
短整型short intshort216-215~215 - 1
长整型long intlong432-231~231 - 1
无符号基本整型unsigned intunsigned4320~2^32 - 1
无符号短整型unsigned short intunsigned short2160~2^16 - 1
无符号长整型unsigned long intunsigned long4320~2^32 - 1
  • 整型常量有三种表达形式:①十进制数②八进制数(以数字0开头)③十六进制数(以0x或0X开头,0x开头时数值部分用小写的字母,0X开头时数值部分用大写的字母)
  • 一个整常量后面加上一个字母L(或小写l),则明确指定该常量是long int型的。如:0L、123L。这种处理方式往往用于函数调用时参数传递过程中。

实型数据

  • C语言的实型数据可以分为实型变量和实型常量。
  • 实型变量按数值的取值范围不同分为三种:①单精度实型(float)②双精度实型(double)③长双精度实型(long double)。
类型名称类型说明符字节数位数有效数字数值范围(绝对值)
单精度实型float4326~710-38~1038
双精度实型double86415~1610-308~10308

字符型数据

  • 字符型数据用于表示一个字符值。
  • C语言约定用""开头的字符序列作为标记,这类字符统称为转义字符。
表示形式含义ASCII码
\0空字符0
\a响铃7
\b退格,将当前位置移动到前一列8
\t水平制表(跳到下一个Tab位置)9
\n换行,将当前位置移到下一行的开头10
\v垂直制表(跳到下一个Home位置)11
\f换页,将当前位置移到下一页的开头12
\r回车,将当前位置移到本行的开头13
\"双撇号字符34
\’单撇号字符39
\\反斜杠("\")字符92
\ddd1~3位八进制所代表的字符
\xhh1~2为十六进制所代表的字符
  • 字符常量是用单撇号括起来的一个字符。
  • 字符变量在使用之前必须定义,以char作为类型说明符。
  • 字符串常量是括在一对双撇号之间的字符序列。字符串常量中的字符依次存储在内存中一块连续的区域内,并把空字符’\0’自动附加到字符串的尾部作为字符串的结束标志。

区别

  • 如上图,字符串"a"占两个字节,而字符’a’在内存中占一个字节,所以不能将字符串赋给一个字符变量。

运算符和表达式

运算符是对数据进行处理和操作的过程,描述各种处理和操作的符号称为运算符(也称操作符)。

类别运算符
算数运算符*、/、%、+、-、自增运算符++、自减运算符–
关系运算符>、<、==、>=、<=、!=
逻辑运算符!、&&、||
位运算符<<、>>、~、|、^、&
赋值运算符=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=
条件运算符?:
逗号运算符,
指针运算符*、&
强制类型转换运算符(类型),如(int)、(double)等
分量运算符->、.、[]
其它运算符如函数调用运算符()等

算术运算符和算术表达式

符号说明优先级结合性
+单目取正14从右到左
-单目取负14从右到左
*13从左到右
/13从左到右
%取余13从左到右
+12从左到右
-12从左到右

赋值运算符和赋值表达式

复合赋值运算表达式(算术运算)等价于复合赋值运算表达式(位运算)等价于
a+=ba=a+ba<<=ba=a<<b
a-=ba=a-ba>>=ba=a>>b
a*=ba=a*ba&=ba=a&b
a/=ba=a/ba^=ba=a^b
a%=ba=a%ba|=ba=a|b

自增和自减运算

  • 自增运算符++和自减运算符–是C语言特有的单目运算符,它们只能和一个单独的变量组成表达式。
  • x++和++x的相同之处:单独作为一个表达式语句被使用时,无论执行了哪一种表达式,执行结束后x的值都增加1。
  • x++和++x的不同之处:当放到表达式中使用时,++x是先将x加1后,再在其所在表达式中使用x的值;而x++是在其所在的表达式中先使用x的值完成计算后,然后再将x的值加1。

位运算符

符号说明优先级结合性
~位取反14从右到左
<<左移11从左到右
>>右移11从左到右
&位与8从左到右
^位异或7从左到右
|位或6从左到右

数据的输入和输出

数据的输出

printf(格式控制字符串, 输出表列)
  • 格式控制字符串也称转换控制字符串,它包括格式说明符和普通字符两种格式信息。
    • 格式说明符:由"%"和格式符组成。将输出表列的数据转换为指定的格式输出。
    • 普通字符:是指格式控制字符串中除格式说明符外的其它字符,其中也包括转义字符。
  • 输出表列:需要输出的一系列数据,可以是常量、变量和表达式。

在C语言中,格式输入和输出函数对不同类型的数据必须采用不同的格式说明符。

/* 一般形式如下 */
%[-0] [m] [.] [n] [l] 格式符
  • 方括号( [] )的内容是可选项。
  • -(负号):表示当实际数据的宽度小于显示宽度时,数据左对齐,数据右方用空格填充。
  • 0:表示当实际数据的宽度小于显示宽度时,数据右对齐,数据左边空格用0填充。
  • m:表示占用数据的宽度,如果实际数据的宽度大于m,按实际宽度输出。如果实际数据的宽度小于m,数据右对齐,数据左方用空格填充。
  • n:表示指定输出的数据中有n位小数,或者表示取字符串中左端n个字符输出。如果不指定该项,一般系统默认输出6为小数。
  • m.n:表示指定输出的数据共占m列,其中有n位小数,舍去的部分系统自动四舍五入,如果输出的是字符串,表示取字符串中左端n个字符输出。
  • l:用于长整型或双精度型的数据。
  • %%:表示%字符。
格式字符作用输出值类型
d、i以十进制带符号的形式输出整数整型
o以八进制无符号的形式输出整数整型
x、X以十六进制无符号的形式输出整数整型
u以十进制无符号的形式输出整数整型
f以小数形式输出单、双精度实数实型
e、E以指数形式输出单、双精度实数实型
g、G选用%f或%e格式中输出宽度较短的一种格式实型
c以字符形式输出一个字符字符型
s以字符形式输出一个字符串字符串
P输出指针void*类型

字符输出函数putchar

// 例如:
#include <stdio.h>
int main()
{
    char c1='A', c2=66;
    int c3='\103', c4;
    c4 = c3 + 1;
    putchar(c1); putchar(c2);
    putchar('\n');
    putchar(c3); putchar(c4);
    putchar('\n');
    return 0;
}
// 输出
AB
CD

数据的输入

scanf(格式控制字符串, 地址表列)

格式控制字符串无任何普通字符。正在程序运行中需要输入非空字符类数据时,两个数据之间以一个或多个空格间隔,也可以按回车键、制表键(Tab)间隔。

字符输入函数getchar

只能接收一个字符,可以将获得的字符赋给int型或char型的变量,可以用来接收键盘输入不必要的回车或空格,或使运行的程序暂停。


选择结构

关系运算

运算符含义
>大于
>=大于等于
<小于
<=小于等于
==等于
!=不等于

以上六种关系运算符,前四种(>、>=、<、<=)的优先级相同,后两种(==、!=)的优先级也相同,前四种的优先级高于后两种。

C语言中没有逻辑型数据,以1表示"真",0表示"假"。

逻辑运算符和逻辑表达式

运算符含义
&&逻辑与
||逻辑或
!逻辑非

选择语句

C语言中,使用if和switch语句来实现选择结构。

if(表达式){
    语句
}
else if(表达式){
    语句
}
else {
    语句
}
switch(表达式)
{
    case 常量表达式1: [语句序列1]
    case 常量表达式2: [语句序列2]
        ......
    case 常量表达式n: [语句序列n]
    [default: 语句序列n+1]
}

switch语句的执行过程是:首先计算switch后表达式的值,然后将其结果值与case后常量表达式的值依次进行比较,若此值与某case后常量表达式的值一致,即转去执行该case后的语句序列,若没有找到与之匹配的常量表达式,则执行default后的语句序列。在执行switch语句的过程中,只有遇见break,才能跳出本次switch,否则会延当前case执行下去。


循环结构

goto语句

goto 标号;
    ......
标号: 语句
    ......

标号与goto语句配合使用才有意义,单独存在没有意义,不起作用。标号的构成规则与标识符相同。与goto语句配合使用的标号只能存在于该goto语句所在的函数内,并且唯一,不可以利用goto语句将执行的流程从一个函数中转移到另一函数中去。允许多个goto语句转向同一标号。

// 例如:
#include <stdio.h>
int main()
{
    int i=1, sum=0;
    L: if(i<=10)
    {
        sum=sum+i*i;
        i++;
        goto L;
    }
    printf("sum=%d\n", sum);
    return 0;
}

// 结果
sum=385

while循环语句

while(表达式)
{
    语句
}

计算表达式并检查表达式的值是否为0,如果为0,while循环语句结束执行,接着执行while循环语句的后继语句。

// 例如
#include <stdio.h>
int main()
{
    int i=0, sum=0;
    while(i<=10)
    {
        sum=sum+i*i;
        i++;
    }
    printf("sum=%d\n", sum);
    return 0;
}

// 结果
sum=385

do-while循环语句

do
    语句
while(表达式);

关键字do和while之间的语句,也称为循环体,可以是单语句,也可以是复合语句。do-while循环语句首先执行循环体,然后计算表达式并检查循环条件,所以循环体至少被执行一次。

// 例如
#include <stdio.h>
int main()
{
    int i=1, sum=0;
    do {
        sum=sum+i*i;
        i++;
    }while(i<=10);
    printf("sum=%d\n", sum);
    return 0;
}

// 结果
sum=385

for循环语句

for(表达式1; 表达式2; 表达式3)
{
    语句
}

for语句先执行表达式1,然后计算表达式2,如果表达式2非0的话,则执行内嵌语句,最后执行表达式3,之后重复执行表达式2到表达式3的这个过程。

// 例如
#include <stdio.h>
int main()
{
    int i, sum=0;
    for(i=1; i<=10; i++)
        sum=sum+i*i;
    printf("sum=%d\n", sum);
    return 0;
}

// 结果
sum=385

循环嵌套

一个循环语句的循环体中又包含循环语句,称为循环嵌套。循环嵌套可以是二层或多层嵌套。嵌套的外层循环与内层循环控制变量不能同名,但并列的同层循环允许有同名的循环控制变量。

break、continue和空语句

语句说明
break终止执行该语句所在的一层循环,并使执行流程跳出该层循环体
continue终止循环体的本次执行,继续进行是否执行循环体的检查判定
;什么都不做,只由一个分号构成

数组

一维数组

数组在引用前必须定义,定义的作用是通知编译程序在内存中分配出连续的存储单元供数组使用。定义的形式如下:

类型说明符 数组名[正整型常量表达式]
  • C语言规定,只能引用单个元素,而不能一次引用整个数组。引用数组元素的形式如下:
数组名[下标]

下标是整型表达式,可以是数值常量、符号常量、字符常量、变量、算术表达式、函数返回值。若是实型常量,系统将自动按舍弃小数位保留整数位处理。

一维数组的初始化

// 定义时初始化
int x[5] = {2, 4, 6, 8, 10};
// 给数组中部分元素置初值,其它元素系统默认其值为0
int d[8] = {1, 2, 3};
// 对数组全部元素赋值可以不指定数组的长度
int b[5] = {0, 1, 2, 3, 4};  // 等同于下方写法
int b[] = {0, 1, 2, 3, 4};
/*初值的个数不允许大于定义数组时限定的元素个数*/

二维数组

// 二维数组定义的一般形式
类型说明符 数组名[正整型常量表达式][正整型常量表达式]

二维数组的初始化

// 按行给二维数组置初值
int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
// 将所有的数组元素按行顺序写在一个大括号内
int a[2][3] = {1, 2, 3, 4, 5, 6};
// 对部分数组元素赋值
int b[3][4] = {{1}, {4, 3}, {2, 1, 2}};
/*如果对全部数组元素置初值,则二维数组的第一个下标可省略
但第二个下标不能省略*/
int a[2][3] = {1, 2, 3, 4, 5, 6};  // 写法同下
int a[][3] = {1, 2, 3,4, 5, 6};  // 系统会根据固定的列数,自动将行数定为2

字符数组

数组中,若每个数组存放的都是字符型数据,则称为字符数组。字符数组用关键字char来说明其类型。一维字符数组的定义如下:

char  数组名[正整型常量表达式]

字符数组初始化

// 字符型数组的初始化与数值型数组的初始化方法类似
char c[7] = {'p', 'r', 'o', 'g', 'r', 'a', 'm'};
/*若花括号中的初值个数大于数组的长度,按语法错误处理
若花括号中的初值个数小于数组的长度,其余的元素自动添入空字符'\0'
若字符数组的元素的个数与初值个数相同,可在定义时省略说明长度,系统会根据初值确定长度*/

字符数组与字符串

C语言中,用字符数组存放字符串。在定义数组时,也可以用空字符串为字符数组赋初值。如下:

char c[] = {"C Program"};  // 或者如下写法
char c[] = "C Program";
// 这时c数组的长度为10,在字符m后面的字节中,
// 系统自动添加了空字符'\0'
char c[] = {'H', 'e', 'l', 'l', 'o'};
char c[] = "Hello";
// 上述两种初始化的方法,字符串的长度相同,但它们的内存占用空间不同。后者比前者多1。

字符数组的输入和输出

  • 字符数组的输入和输出有如下两种格式:
    • %c格式用于一个字符的输入和输出。
    • %s格式用于一个字符串的输入和输出。

说明

① 以%s格式输出字符串时,在printf函数中输出项应写字符数组名,而不是数组元素。

② 在输出字符串时,遇到字符’\0’就结束输出。

③ 在scanf函数中使用%s时,在输入项中不要加地址符号’&’,直接写出字符数组名即可,因为数组名表示该数组在内存中的起始地址。

④ 在定义字符数组时,要考虑大于实际串长。如果用scanf函数输入多个字符串,则字符串间用空格分隔。

⑤ 如果输入带空格的字符串,通常用初始化的方法或用gets函数,若用scanf("%s", s);,则只接收字符串中第一个空格前的字符。

⑥ gets函数接收数据是以回车作为字符串结束标志,而scanf函数则以空格作为字符串结束标志。

字符串处理函数

C语言函数库中提供了字符串处理函数,这些函数定义在string.h库文件中,用户可以预编译命令#include "string.h"将文件string.h包含到程序中,直接引用字符串函数。

// 输出字符串函数puts
puts(字符数组名);

// 输入字符串函数gets
gets(字符数组名);

// 字符串连接函数strcat
strcat(字符数组1, 字符数组2)
/*
将字符数组2连接到字符数组1的后面,结果存放在字符数组1中。
第一个字符串定义的长度要大于连接后字符串的长度。
后面字符串与前面字符串连接时,去掉前面字符串后面的'\0'。
*/
    
// 字符串复制函数strcpy
strcpy(字符数组1, 字符串2);
/*
将字符串2复制到字符数组1中。
字符数组1必须写成数组名的形式,字符串2可以是字符数组名,也可以是字符串常量。
字符数组1的长度要大于字符串2的实际长度,复制时连同'\0'一同复制过去。
不能用赋值语句将字符串赋值给一个字符数组。
*/

// 字符串比较函数strcmp
strcmp(字符串1, 字符串2)
/*
将字符串1和字符串2从左向右逐个字符比较,直到出现不同字符或遇到'\0'为止。这样,即可得出函数值。
如果字符串1==字符串2,则函数值为0
如果字符串1>字符串2,则函数值为一个正整数
如果字符串1<字符串2,则函数值为一个负整数
*/
    
// 检测字符串长度函数strlen
strlen(字符数组名)
    
// 字符串小写函数strlwr
strlwr(字符串)
    
// 字符串大写函数strupr
strupr(字符串)

函数

函数的基本概念

  • 函数是按规定格式书写的能完成特定功能的一段程序。
  • C语言是以源文件为单位进行编译的,一个源程序文件由一个或多个函数组成。
  • 一个C程序由一个或多个源程序文件组成,可以利用C语言分别编译的特点,将源文件分别编译成目标文件,然后将这些目标文件链接在一起,形成一个可执行文件。
  • 函数与函数之间是相互独立、平等的、没有从属关系。函数不能嵌套定义,但是可以相互调用,主函数可以调用任何函数,而其它函数不能够调用主函数。一个函数可以多次被调用。
  • C语言中,程序总是从主函数开始执行,调用其它函数后,执行流程再返回到主函数,最终在主函数中结束,而不管主函数在程序中的位置如何。

函数的定义

类型说明符 函数名(形式参数声明)
{
    [说明与定义部分]
    语句;
}
  • 类型说明符是用来说明函数的返回值的类型。
  • 函数名是用户自定义的用于标识函数的名称,其命名规则与变量的命名规则相同。
  • 形式参数声明(简称形参表)用于指明调用函数和被调用函数之间的数据传递,传递给函数的参数可以有多个,也可以没有。当函数有多个参数时,必须在形参表中对每一个参数进行类型说明,每个形参之间用逗号隔开。

函数的调用

// 函数调用的一般形式
                函数名 (实际参数表列);
或
                函数名 (实际参数表列)
  • 第一种调用格式是以语句的形式调用函数,用于调用返回无返回值的函数。第二种调用格式是以表达式的形式调用函数,用于调用有返回值的函数。
  • 实际参数表列(简称实参表)中实参的类型与形参的类型相对应,必须符合赋值兼容的规则,实参个数必须与形参个数相同,并且顺序一致,当有多个实参时,参数之间用逗号隔开。
  • 实参可以是常量、有确定值的变量或表达式及函数调用。
  • 进行函数调用时,要求实参与形参个数相等,类型和顺序也一致。但在C的标准中,实参表的求值顺序并不是确定的。有的系统按照自右向左的顺序计算,而有的系统则相反。

函数参数的传递方式

值传递

函数调用时,实参将其值传递给形参,这种传递方式即为值传递。

C语言规定,实参对形参的数据传递是“值传递”,即单向传递,只能由实参传递给形参,而不能由形参传回来给实参。这是因为,在内存中实参与形参占用不同的存储单元。

地址传递

地址传递指的是调用函数时,实参将某些量(如变量、字符串、数组等)的地址传递给形参。这样实参和形参指向同一个内存空间,在执行被调用的过程中,对形参所指向空间中的内容的改变,就是对调用函数中对应实参所指向的内存空间内容的改变。

函数的返回值

C语言中是通过return语句来实现返回值。

return (表达式);
  • 在定义函数时应当指定函数值的类型,并且函数的类型一般应与return语句中表达式的类型相一致,当二者不一致时,应以函数的类型为准,即函数的类型决定返回值的类型。对于数值型数据,可以自动进行类型转换。
  • 若函数中无return语句,函数也并非没有返回值,而是返回一个不确定的值。为了明确表示函数没有返回值,可以用void将函数定义为“空类型”。

函数的原型声明

C语言要求函数先定义后使用,如果被调用函数的定义位于调用函数之前,可以不必声明;如果自定义的函数被放在调用函数的后面,就需要在函数调用前,加上函数的原型声明。

类型说明符 函数名(参数表);

数组作为函数参数

一维数组作为函数参数

一维数组作为函数参数时,形参的写法如下:

类型说明符 形参数组名[数组长度]

例如:

#include <stdio.h>
double aver(int a[], int n)
{
    int i;
    double sum=0;
    for(i=0; i<n; i++)
        sum+=a[i]
    return sum/n;
}
int main()
{
    int i, a[10];
    for(i=0; i<10; i++)
        scanf("%d", &a[i]);
  	printf("平均值=%lf\n", aver(a, 10));
    return 0;
}

二维数组作为函数参数

二维数组作为函数参数时,形参的写法为:

类型说明符 形参数组名[数组长度1][数组长度2]

例如:

#include <stdio.h>
void exchange(int b[][4], int i, int j)
{
    int k, t;
    for(k=0; k<4; k++)
    {
        t=b[i][k];
        b[i][k]=b[j][k];
        b[j][k]=t;
    }
}
int main()
{
    int i, j, a[3][4]={{1,1,1,1}, {3,3,3,3}, {2,2,2,2}};
    exchange(a, 1, 2);
    for(i=0; i<3; i++)
    {
        for(j=0; j<4; j++)
            printf("%d,", a[i][j]);
        printf("\n");
    }
    return 0;
}

数组作为函数参数的调用方式

  • 数组作为函数参数进行函数调用时,实参应当采用数组名。
  • 数组作为函数参数时,实参和形参数组应在调用函数和被调用函数中分别定义。
  • 实参数组和形参数组类型应一致,行数可以不一致,列数必须一致。
  • 数组作为函数参数时,不是单向的“值传递”,而是“地址传递”,也就是把实参数组的起始地址传递给形参数组,这样二者共占一段内存单元。

函数的嵌套调用和递归调用

C语言中函数的定义是相互平行的,函数之间没有从属关系,但是,一个函数在被调用的过程中可以调用其它函数,这就是函数的嵌套调用。

函数的递归调用是C语言的重要特点之一,即在调用一个函数的过程中又直接或间接的调用该函数本身。

函数递归调用必须具备两个要素:递归调用公式,即问题的解决能够写成递归调用的形式;结束条件,即确定何时结束递归。

// 用递归方法算n!
#include <stdio.h>
#include <stdlib.h>
long fact(int n)
{
    long k;
    // 若n为负数,则提示错误并结束程序
    if(n<0)
    {
        printf("Data error!\n");
        exit(0);
    }
    else if(n==0||n==1) k=1;
    else k=n*fact(n-1);
    return k;
}
int main()
{
    int n;
    long f;
    printf("Please input an integral number:\n");
    scanf("%d", &n);
    f=fact(n);
    printf("%d!=%ld\n", n, f);
    return 0;
}

变量的作用域和存储方法

局部变量和全局变量

局部变量:在函数内部或复合语句中定义的变量。

局部变量的作用域仅仅局限于定义它的函数和复合语句。

形式参数也是局部变量,只在定义它的函数中有效,其它函数不能使用。

不同函数中定义的变量可以同名,它们代表不同的对象。

全局变量:在函数体外定义的变量。

全局变量的作用域是从它的定义点开始到本源文件结束,即位于全局变量定义后面所有函数都可以使用此变量。

如果在定义全局变量之前的函数中使用该变量,则需在该函数中使用关键字extern对全局变量进行外部说明。

#include <stdio.h>
int m, n;  // 声明全局变量
int main()
{
    extern int a, b;  // 声明全局变量
    int max;
    scanf("%d%d", &a, &b);
    max=a>b?a: b;
    printf("max=%d\n", max);
    return 0;
}

为了方便处理,一般把全局变量的定义放在所有使用它的函数之前。

在同一个源文件中,当局部变量与全局变量同名时,在局部变量的作用范围内,全局变量不起作用。

变量的存储方法

C语言中,供用户使用的存储空间分为三部分,即程序区、静态存储区和动态存储区。

  • 程序区存放的是可执行程序的机器指令
  • 静态存储区存放的是在程序运行期间需要占用固定存储单元的变量,如全局变量
  • 动态存储区存放的是程序在运行期间根据需要动态分配存储空间的变量,如函数的形参变量、局部变量等

变量的存储属性就是数据在内存中的存储方法。存储方法可以分为两大类:动态存储和静态存储,具体分为四种:自动型(auto)、静态型(static)、寄存器型(register)和外部型(extern)。

局部变量的存储方法分三种存储类型:自动型、静态型和寄存器型。


自动变量:函数中的局部变量,如不进行专门的说明,则对它们分配和释放存储空间的工作由系统自动处理,这类局部变量称为自动变量。

C语言规定,函数定义的变量的默认存储类型是自动型,所以关键字auto可以省略。


局部静态变量:如果希望在函数调用结束后仍然保留其中定义的局部变量的值,则可以将局部变量定义为局部静态变量。

static  类型说明符 变量名;

局部静态变量是在静态存储区分配的存储单元的,在整个程序运行期间都不释放。

局部静态变量是在编译过程中赋初值的,且只赋一次初值,在程序运行时其初值已定,以后每次调用函数时不再赋初值,而是保留上一次函数调用结束时的结果。

局部静态变量的默认初值为0(对数值型变量)或空字符(最字符型变量)。


寄存器变量是C语言所具有的汇编语言的特性之一。寄存器变量用关键字“register”进行说明。

只有局部自动变量和形参可以作为寄存器变量,其它(如全局变量、局部静态变量)则不行。

只有int、char和指针类型变量可以定义为寄存器型,而long、double和float型变量不能设定为寄存器类型,因为它们的数据长度已超过了通用寄存器本身的位长。

可用于变量空间分配的寄存器个数依赖于具体的机器。当没有寄存器可以用于分配时,就把变量当作auto型变量进行存储分配,并且C语言编译器严格按照说明在源文件中出现的顺序来分配存储器。

全局变量是在静态存储区分配存储单元的,其默认的初值为0。全局变量的存储类型有两种,即外部类型和静态类型。


对于一个很大的程序,为了编写、调试、编译和修改程序的方便,常把一个程序设计成多个文件的模块结构。先对每个模块文件单独进行编译,然后再将各模块链接在一起。因此,在多个源程序文件的情况下,如果在一个文件中要引用在其它文件中定义的全局变量,则应该在需要引用此变量的文件中,用extern进行说明。

extern只能用来说明变量,不能用来定义变量,因为它不产生新的变量,只是宣布该变量已在其它地方有过定义。

extern不能用来初始化变量。如以下写法是错误的:

extern int x=1;  // 这样的写法是错误的

如果希望在一个文件中定义的全局变量仅限于被本文件引用,而不能被其它文件访问,则可以在定义此全局变量时,前面加上关键字static。

内部函数和外部函数

内部函数,又称静态函数,它只能被本文件中的其它函数所调用。此处的“静态”不是指存储方式,而是指函数的作用域仅局限于本文件。

static  类型说明符 函数名(形式参数声明)
// 例如
static float sum(float x, float y)
{
    ......
}

外部函数,定义函数时,如果使用关键字extern,表明此函数是外部函数。由于函数都是外部性质的,因此,在定义函数时,关键字extern可以省略。在调用函数的文件中,一般要用extern说明所用的函数是外部函数。

// A文件中定义外部函数
extern char compare(char s)
{
    ......
}
// 在B源文件中调用A文件中的函数compare
// 则在B源文件中进行如下说明
extern char compare();

编译预处理

编译预处理是C语言编译程序的组成部分,它用于解释处理C语言源程序中的各种预处理命令。如编程中常见的#include和#define命令。该功能不属于C语言语句的组成部分,是在C编译之前对程序中的特殊命令进行的“预处理”。使用预处理的功能,可以增强C语言的编程功能,提高程序的可读性,改进C程序设计的环境,提高程序设计效率。

C提供的预处理主要有宏定义、文件包含和条件编译3种,为了与其它C语句相区别,所有的预处理命令均以“#”开头,语句结尾不使用“;”,每条预处理命令需要单独占一行。

宏定义

宏定义是指用一个指定的标识符来定义一个字符序列。宏定义是由源程序中的宏定义命令完成的,宏替换是由预处理程序完成的。宏定义分为无参宏定义和有参宏定义两种。


无参宏定义:

#define  宏名 替换文本  // 通常称为宏替换或宏展开,宏替换是纯文本替换
  • 宏名按标识符书写规定进行命名,为区别变量名,宏名一般习惯用大写字母表示。无参宏定义常用来定义符号常量。

  • 替换文本是一个字符序列,也可以是常量、表达式、格式串等。为保证运算结果的正确性,在替换文本中若出现运算符,通常需要在合适位置加括号。

  • 宏定义可以出现在程序的任何位置,但必须出现在引用宏名之前。

  • 在进行宏定义时,可以引用之前已经定义过的宏名。

  • 如果程序中用双撇号括起来的字符串内包含有与宏名相同的名字,预编译时并不进行宏替换。

  • 宏定义通常放在程序的开头。


带参数的宏定义:

C语言允许宏带有参数,在宏定义中的参数称为形式参数,简称形参。在宏调用中的参数称为实际参数,简称实参。对带参数的宏,在调用时,不仅要将宏展开,而且要用实参去替换形参。

#define  宏名(形参表)  替换文本

如果定义带参数的宏,在对源程序进行预处理时,将程序中出现宏名的地方均用替换文本替换,并用实参代替替换文本中的形参。

#include <stdio.h>
#define MAX(a, b) a>b?a: b
#define SQR(c) c*c
int main()
{
    int x=3, y=4;
    x=MAX(x, y);
    y=SQR(x);
    printf("x=%d, y=%d\n", x, y);
    return 0;
}
  • 函数在定义和调用中所使用的形参和实参都是受数据类型限制,而带参数宏的形参和实参可以是任何数据类型。
  • 函数有一定的数据类型,且数据类型是不变的。而带参数的宏一般是一个运算表达式,它没有固定的数据类型,其数据类型就是表达式运算结果的数据类型。
  • 函数调用时,先计算实参表达式的值,然后带入形参。而宏定义展开时,只是村文本替换。
  • 函数调用是在程序运行时处理的,进行分配临时的存储单元。而宏展开是在编译时进行的,展开时不分配内存单元,不传递值,也没有“返回值”的概念。
  • 函数调用影响运行时间,源程序无变化,宏展开影响编译时间,通常使源程序加长。

文件包含

文件包含也是一种预处理语句,它的作用是使一个源程序文件将另一个源程序文件全部包含进来,其一般形式如下:

#include <文件名> 或 #include "文件名"
  • 一个#include命令只能包含一个指定文件,若要包含多个文件,则需要使用多个#include命令。
  • 采用<>形式,C编译系统将在系统指定的路径下搜索<>中指定文件,称为标准方式。
  • 采用""形式,系统首先在用户当前工作目录中搜索包含的文件,若找不到,再按指定的路径搜索包含文件。

条件编译

一般情况下,C源程序的所有行都参与编译过程,所有的C语句都生成到目标程序中,如果只想把源程序中的一部分语句生成目标代码,可以使用条件编译。利用条件编译,可以方便调试,增强程序的可移植性,从而使程序在不同的软硬件环境下运行。

if格式

#if  表达式
    程序段1;
[#else
    程序段2;]
#endif

首先计算表达式的值,如果为非0,就编译“程序段1”,否则编译“程序段2”。如果没有#else部分,则当“表达式”的值为0时,直接跳过#endif。


ifdef格式

#ifdef 宏名
    程序段1;
[#else
    程序段2;]
#endif

首先判断“宏名”在此之前是否已经被定义过,若已经定义过,则编译“程序段1”,否则编译程序段2“。如果没有#else部分,则当宏名为定义时直接跳过#endif。


指针

一个变量所占内存区域一段连续字节中的第一个字节的地址,就称为该变量的地址。程序中对变量进行存取操作,也就是对该变量所对应地址的存储单元(显然,这里的存储单元是指由若干字节组成)进行存取操作,这种直接按变量的地址存取变量值的方式称为直接存取方式。

一个变量的内存地址称为该变量的指针。如果一个变量用来存放指针(即内存地址),则称该变量是指针类型的变量(一般也简称为指针变量)。在不至于混淆的情况下,有时也把指针变量称为指针。

指针的定义

类型说明符 *标识符;
  • 指针变量定义形式中的星号“*”不是变量名的一部分,它的作用是用来说明该变量是指针变量。
  • 如果一个表达式的值是指针类型的,即是内存地址,则称这个表达式是指针表达式。
  • 无论指针变量指向何种类型,指针变量本身也有自己的地址,占二个或四个字节的存储空间(具体根据程序运行的软、硬件环境而定)。

指针运算

赋值运算

指针变量 = 指针表达式
// 将指针表达式的值赋给指针变量,即用指针表达式的值取代指针变量原来存储的地址值
// 进行赋值运算时,赋值运算符右侧的指针表达式指向的数据类型和左侧指针变量所指向的数据类型必须相同

取地址运算

&标识符   // “&”是取地址运算符
  • “标识符”只能是一个除register类型之外的变量或数组元素。
  • 表达式“&标识符”的值就是运算符“&”后面变量或数组元素的地址,因此“&标识符”是一个指针表达式。

取内容运算

*指针表达式   // “ * ”是取内容运算符,“指针表达式”是取内容运算符的运算对象
  • 取内容运算符“ * ”是单目运算,也称为指针运算符或间接访问运算符。
// 例如
#include <stdio.h>
int main()
{
    int x=5, *p=&x;
    printf("%d\n", x);
    printf("%d %p\n", *p, p);
    return 0;
}
// 结果
5
5 0x7ffe62caac2c
// 显示的地址根据运行的环境不同结果不同

指针表达式与整数相加、减运算

指针表达式与整数相加减的一般形式:

p+n 或 p-n  // p是指针表达式,n是整型表达式

指针表达式与整数相加减运算结果的值:从所指向的位置算起,内存地址值大(+)或地址值小(-)方向上第n个数据的内存地址。只有当p和p+n或p-n都指向连续存放的同类型数据区域,例如数组,指针加减才有实际意义。

自增、自减运算

p++ 、p--++p 、--p  // p是指针变量

自增、自减运算的结果值:p++和++p运算使p增加了一个p所指向的类型长度值,p–和--p运算使p减小了一个p所指向的类型长度值。

取内容运算符“ * ”、取地址运算符“ & ”和自增、自减运算符都是单目运算,运算的优先级相同,结合方向都是从右至左。

// 例如:
#include <stdio.h>
int main()
{
    int a[] = {1, 2, 3, 4, 5, 6};
    int *p;
    p = &a[2];
    printf("%d %d %d %d %d\n", *p, *++p, *--p, *p++, *p--);
    return 0;
}
// 结果
3 3 2 2 3
// *++p是先取值,然后地址再加的

同类指针相减运算

// 一般形式如下:
m-n
// m与n是指向同一类型的指针表达式
// 例如:
#include <stdio.h>
int main()
{
    int a[10], r, *p, *q;
    p = &a[2];
    q = &a[5];
    r = q - p;
    printf("%p\t%p\t%d\n", p, q, r);
    return 0;
}
// 结果:
0x7fffce00eb48	0x7fffce00eb54	3
// 可以知道q和p之间数据元素的个数是2

关系运算

// 一般形式:
指针表达式 关系运算符 指针表达式
// 运算结果:若关系式成立,则值为int型的1,否则值为int型的0
// 例如:
#include <stdio.h>
int main()
{
    int a[3], *p, *q;
    p = a, q = &a[1];
    printf("%d\t%d\n", p>q, p<q);
    return 0;
}
// 结果
0    1

强制类型转换运算

// 强制类型转换运算的一般形式:
(类型说明符*)指针表达式  // 将指针表达式的值转换成类型说明符类型的指针

空指针

在没有对指针变量赋值(包括赋初值)以前,指针变量存储的地址值是不确定的。因此,没有对指针变量赋地址值而直接使用指针变量p进行p=表达式;形式的赋值运算时可能会产生不可预料的后果,甚至会导致系统不能正常的运行。所以,为了避免上述问题,我们通常给指针变量赋初值0,并把值为0的指针变量称做空指针变量。如果给空指针变量所指的内存区域赋值,将会得到错误信息。*

p = '\0';
p = 0;
p = NULL;
// 以上三个语句等价,都是空指针

指针变量与一维数组

指针变量与一维数组的区别

指针变量是地址变量,可以改变其本身的值;而除了作为形参的数组名外,其它数组名是地址常量,地址值不可以改变,不可以给除了作为形参的数组名之外的其它数组名赋值。

#include <stdio.h>
int main()
{
    int a[] = {1, 2, 3}, *p;
    p = &a[1];
    *p = 8;
    printf("%d\n", a[1]);
    return 0;
}
// 结果:
8

字符串指针与字符串

#include <stdio.h>
int main()
{
    char *p = "C Language";
    for(;*p!='\0';)
    {
        putchar(*p++);
    }
    return 0;
}
// 结果
C Language

*注意:*对数组赋初值时,语句 char s[11]=“C Language”; 不能等价于程序段 char s[11]; s[]=“C Language”; ,即数组可以定义时整体赋初值,不可以利用赋值语句对数组整体赋值。

指针与函数

指针作为函数参数

#include <stdio.h>
void dis(char *a)
{
    printf("%s\n", a);
}
int main()
{
    char *p;
    p = "Happy";
    dis(p);
    return 0;
}
// 结果
Happy

返回指针的函数

返回指针函数的一般形式:

// 形式1
类型说明符 *函数名([形式参数表])
    [形式参数说明]
    {[说明定义部分]
    语句
    }
// 形式2
类型说明符 *函数名([类型说明符 形式参数1, ..., 类型说明符 形式参数n])
{[说明与定义部分]
    语句
}
  • “ * ”表示函数的返回值是一个指针,其指向的数据类型由函数名前的类型说明符确定。
#include <stdio.h>
char *str(char *s)  // 将给定的字符串的首字母大写,其余字母小写
{
    int i=1;
    if(*s>='a' || *s<='z')
        *s=*s-32;
    while(*(s+i)!='\0')
    {
        if(*(s+i)>='A' || *(s+i)<='Z')
        {
            *(s+i)=*(s+i)+32;
        }
        i++;
    }
    return s;
}
int main()
{
    char a[10] = "heLLO";
    printf("%s\n", str(a));
    return 0;
}
// 结果
H�llo
// 我运行出来的时候e字符乱码了

函数的指针和指向函数的指针变量

函数的名字有值,其值等于该函数存储的首地址,即等于该函数的入口地址,在编译时分配给函数的这个入口地址就称为函数的指针指向函数的指针变量的值是一个函数的入口地址。指向函数的指针变量定义的一般形式:

类型说明符 (*标识符)([形式参数表])
// 定义了名为标识符的指向函数的指针变量,该指针变量所指向的函数的返回值是类型说明符的类型,该函数的参数个数及类型由形式参数表确定
  • 定义指向函数的指针变量时,形式参数表只写出各个形式参数的类型即可,也可以与函数原型的写法相同,还可以将形式参数表省略不写。

  • 指向函数的指针变量允许的操作:

    • 将函数名或指向函数的指针变量的值赋给指向同一类型的指针变量。

    • 函数名或指向函数的指针变量作为函数的参数。

    • 可以利用指向函数的指针变量调用函数,调用形式是:

      (*变量名)(实际参数表)
      

      其调用结果是使程序的执行流程转移到指针变量所指想函数的函数体。

// 例如:求多项式 x^2 + x - 1当x=1.5、2.5、3.5、4.5时的值
#include <stdio.h>
double f(double z)
{
    double d;
    d=z*z+z-1;
    return d;
}
int main()
{
    int i;
    double r, x, (*y)(double);
    y = f;
    for(i=1; i<=4; i++)
    {
        x = i + 0.5;
        r = (*y)(x);
        printf("x=%f, y=%f\n", i+0.5, r);
    }
    return 0;
}
// 结果
x=1.500000, y=2.750000
x=2.500000, y=7.750000
x=3.500000, y=14.750000
x=4.500000, y=23.750000

指针与二维数组

二维数组的结构

一个数组的名字代表该数组的首地址,是地址常量(作为形式参数的数组名除外),这一规定对二维数组或更高维数组同样适用。

C语言中定义的任何一个二维数组实际上都可以看做是一个一维数组,该一维数组中的每一个成员又是一个一维数组。

*例如定义一个二维数组:*int a[3][4];

以上二维数组中,a[0]、a[1]、a[2]都是一维数组名,分别代表各个对应的一维数组的首地址,都是地址常量,其地址值分别是二维数组每行第一个元素的地址,其指向的类型就是数组元素的类型。

二维数组元素及其地址

若有定义int d[3][4] ,i,j; 。且0≤i≤2,0≤j≤3,则:

// 数组元素d[i][j]的表示方法为:
d[i][j]
*(d[i] + j)
*(*(d + i) + j)
(*(d + i))[j]
*(&d[0][0] + 4 * i * j)
*(d[0] + 4 * i + j)

// 数组元素d[i][j]地址的表示方法为:
&d[i][j]
d[i] + j
*(d + i) + j
&((*(d + i))[j])
&d[0][0] + 4 * i + j
d[0] + 4 * i + j
表示形式含义
d二维数组名,数组首地址,第0行首地址
d[0],*(d+0),*d第0行第0列元素d[0][0]地址
d+1第1行首地址
d[1],*(d+1)第1行第0列元素d[1][0]地址
d[1]+2,*(d+1)+2,&d[1][2]第1行第2列元素d[1][2]地址
#include <stdio.h>
int main()
{
    int a[][3] = {1, 2, 3, 4, 5, 6};
    int *p;
    p = a[0];
    printf("*p=%d\n", *p);
    printf("*(p+2)=%d\n", *(p+2));
    printf("*(a[0]+1)=%d\n", *(a[0]+1));
    printf("*(p+1)+2=%d\n", *(p+1)+2);
    return 0;
}
// 结果
*p=1
*(p+2)=3
*(a[0]+1)=2
*(p+1)+2=4

指针数组

如果一个数组的元素都是指针变量,则称这个数组是指针数组。

指针数组的一般形式:

类型说明符 *数组名[正整形常量表达式1]...[正整型常量表达式n];
// 定义了名字为“数组名”的数组,该数组的所有元素都是指向“类型说明符”类型的指针变量
// 例如:求4阶方阵副对角线上的元素和
#include <stdio.h>
#define N 4
int main()
{
    int i, j, sum=0, *p[N];
    int a[N][N] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {1, 2, 3, 4}, {5, 6, 7, 8}};
    for(i=0; i<N; i++)
        p[i] = a[i];
    for(i=0; i<N; i++)
        for(j=0; j<N; j++)
            if(i+j==N-1)
                sum+=p[i][j];
    printf("sum=%d\n", sum);
    return 0;
}
// 结果
sum=18

指针与字符串数组

字符数组中的每一个元素都是一个字符,而字符串数组指的是数组中的每个成员都是存放字符串的数组。

#include <stdio.h>
int main()
{
    char *name[] = {"xiaoming", "xiaohong", "laowang", "runtu", "lihua"};
    printf("name[0]:%s\n", name[0]);
    printf("name[3]:%s\n", name[3]);
    return 0;
}
// 结果
name[0]:xiaoming
name[3]:runtu

指向数组的指针变量

// 指向数组的指针变量定义的一般形式:
类型说明符 (*变量名)[正整数常量表达式];
// 定义一个名为“变量名”的指针变量,该指针变量所指向的是一个具有“正整型常量表达式”个元素的一维数组
#include <stdio.h>
int main()
{
    int i, *q, (*p)[4], a[3][4]={{2, 4, 6, 8}, {12, 14, 16, 18}, {22, 24, 26, 28}};
    q = a[0];
    for(i=1;i<=4;q+=2, i++)
        printf("%d\t", *q);
    printf("\n");
    p = a;
    for(i=2; i>=0; i--)
        printf("%d\t", *(p[i] + i));
    printf("\n");
    return 0;
}
// 结果
2	6	12	16	
26	14	2

二级指针

如果一个变量的值是其它变量的地址,而这些其它变量的值不再是内存地址,则这个变量是一级指针变量。

如果一个变量的值是一级指针变量的地址,则这个变量是二级指针变量。二级指针变量定义的一般形式如下:

类型说明符 **标识符;
// 指针变量定义形式中的“ ** ”不是变量名的一部分,它的作用是用来说明该变量是二级指针变量
#include <stdio.h>
void swap(int **m, int **n)
{
    int *i;
    i = *m;
    *m = *n;
    *n = i;
}
int main()
{
    int a=5, b=9, *pa, *pb;
    pa = &a; pb = &b;
    swap(&pa, &pb);
    printf("pa=%d, pb=%d\n", *pa, *pb);
    printf("a=%d, b=%d\n", a, b);
    return 0;
}
// 结果
pa=9, pb=5
a=5, b=9

内存空间的动态分配

指向void的指针

void类型是一种抽象的数据类型,如果用指针的void类型说明符定义一个指针变量,则该指针变量并没有被确定存储具体哪一种数据类型变量的地址。

void类型的指针和其它类型的指针可以相互赋值,且不必强制转换。

指向任何类型的指针都可以转换为指向void类型,如果将结果再转换为初始指针类型,则可以恢复初始指针且不会丢失任何信息。

#include <stdio.h>
int main()
{
    float a[4] = {1, 2, 3, 4}, *p1, *p2;
    void *p3=a;
    p1 = (float *)p3;
    p3 = (void *)p1;
    p2 = &a[2];
    p3 = p2;
    p2 = p3;
    printf("a=%p, p1=%p, p2=%p, p3=%p, *p2=%f\n", a, p1, p2, p3, *p2);
    return 0;
}
// 结果
a=0x7ffe461a3dd0, p1=0x7ffe461a3dd0, p2=0x7ffe461a3dd8, p3=0x7ffe461a3dd8, *p2=3.000000

常用内存管理函数

C语言提供了一些内存管理函数,这些内存管理函数可以在程序运行期间分配内存空间,即可以动态分配内存空间,也可以将已经分配的内存空间释放。常用的内存管理函数有calloc、malloc和free函数。

函数作用
calloc向系统申请分配连续的内存空间,如果申请成功,则返回分配内存的首地址,该函数返回的是void类型的指针;如果申请失败,则函数返回空指针。
malloc向系统申请分配一块连续的size个字节的内存区域。
free释放指针所指向的由calloc函数或malloc函数申请成功的内存区域,无返回值。
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int i, n, sum=0, *p;
    n = 3;
    p = (int *)malloc(n*sizeof(int));
    p[0] = 12; p[1] = 34; p[2] = 15;
    for(i=0; i<n; i++)
        sum+=p[i];
    free(p);
    printf("sum=%d\n", sum);
    return 0;
}
// 结果
sum=61

main函数参数

在此之前编写的main函数总是写成main(),实际上main函数也可以有参数。

命令行参数

在操作系统提示符状态下为了执行某个操作系统命令或某个执行文件,而键入的一行字符称为命令行,命令行的一般形式是:

命令名 [参数1] [参数2] ... [参数n]

指针数组作为main函数的形参

对于C程序来说,命令行参数就是主函数的参数,主函数main是程序的入口,在运行C程序时,通过运行C程序的命令行,把命令行参数传递给主函数main的形参。

// 主函数main的形参通常可以有两个参数,例如:
main(int argc, char *argv[])
{...}

第一个参数是int型,习惯上记做argc,表示命令行中参数的个数(包括命令名在内),在运行C程序时由系统自动计算出来参数的个数;第二个参数是指向字符型的指针数组,习惯上记做argv,用来存放命令行中的各个参数(系统将以空格为界的参数视为字符串,存放各字符串的首地址),该数组元素的哥说由参数argc确定,由于作为形参char *argv[]与char **argv等价,因此第二个参数还可以定义成char **argv。


结构体与共用体

结构体类型和结构体变量

结构体类型的定义

结构体类型由不同类型的数据组成。构成结构体类型的每一个数据称为该结构体类型的成员。定义结构体的一般形式是:

struct 结构体类型名
{
    数据类型 成员名1;
    数据类型 成员名2;
    数据类型 成员名3;
    ...
     数据类型 成员名n;
};
// “ struct ”是定义结构体类型的关键字

定义一个结构体类型只是描述结构体数据的形式,它的作用只是告诉C编译系统所定义的结构体类型是由哪些类型的成员构成,各占多少字节,按什么形式存储,并把它们当成一个整体来处理。

结构体变量的定义

  • 先定义结构体类型再定义结构体变量

    // 一般形式
    struct  结构体类型名 结构体变量名表;
    
  • 在定义结构体类型的同时定义结构体变量

    // 一般形式
    struct  结构体类型名
    {
        数据类型 成员名1;
        数据类型 成员名2;
        ....
        数据类型 成员名n;
    }结构体变量名表;
    
  • 直接定义结构体类型变量

    // 一般形式
    struct
    {
        数据类型 成员名1;
        数据类型 成员名2;
        ....
        数据类型 成员名n;
    }结构体变量名表;
    

C编译系统只对变量分配单元,不对类型分配单元。因此,在定义结构体类型时,不分配存储单元。结构体成员变量也可以是一个结构体变量,即一个结构体的定义中可以嵌套另外一个结构体的结构。

struct data
{
    long month;
    int day;
    int year;
};

struct student
{
    long int num;
    char name[20];
    char sex;
    student data birthday;
    char addr[30];
}stu3, stu4;

结构体变量的引用

在定义结构体类型变量以后,就可以引用结构体类型变量,如赋值、存取和运算等。不能够直接引用结构体这个整体,而应当通过结构体变量的各个成员项的引用来实现各种运算和操作。

// 引用结构体变量中的一个成员的格式:
结构体变量名.成员名

这里“ . ”是成员(分量)运算符,它在所有的运算符中优先级最高。如果结构体变量成员又是一个结构体类型,则访问一个成员时,应采用逐级访问的方法,即通过成员运算符逐级找到最底层的成员时再引用。

结构体变量初始化

结构体类型是数组类型的扩充,只是它的成员项可以具有不同的数据类型,因此,结构体变量的初始化和数组的初始化一样,在定义结构体变量时,同时对其成员赋以初值。方法是通过将成员的初始值置于花括号内完成。

struct student
{
    long num;
    char name[20];
    char sex;
}s1={201901, "Lao Wang", 'f'};

结构体数组

结构体数组就是数组中的每一个数组元素都是结构体类型的变量,它们都是具有若干个成员(分量)的项。

结构体数组的定义

定义结构体数组的一般形式:

struct  结构体类型名 结构体数组名[元素个数]
// 例如
struct student
{
    long num;
    char name[20];
}stu[30];
// 注意stu[i](i=0~29)的每一个元素都是struct student类型变量

结构体数组的引用

结构体数组的引用是指对结构体数组元素的引用,由于每个结构体数组元素都是一个结构体变量,因此前面提到的关于引用结构体变量的方法同样适用于结构体数组元素。

  • 结构体数组元素中某一个成员的引用

    数组元素名称.成员名
    
  • 结构体数组元素的赋值

    // 可以将一个结构体数组元素赋给同一结构体数组中的另一个元素
    // 或者赋给同一类型的变量
    // 假如定义有stu结构体数组,以下操作都是合法的
    stu[1] = stu[2];
    stu[3] = stu[4];
    

结构体数组的初始化

结构体数组赋初值的方法与数组赋初值的方法相同。

struct student
{
    long num;
    char name[20];
}stu[20]={{1, "laowang"}, {2, "laoli"}, ... ,{20, "wahaha"}};
// 也可以在初始化时不指定数组元素的个数,系统在编译时根据所给出的初值来确定结构体变量的个数

应用举例

// 输入3名学生的信息并输出
#include <stdio.h>
#include <string.h>
struct student  /* 定义结构体类型 */
{
    long num;
    char name[20];
    int age;
    char sex;
};
int main()
{
    struct student stu[3];
    int i;
    for(i=0; i<3; i++)
    {
        printf("请输入依次输入学生的编号、姓名、年龄和性别:\n");
        scanf("%ld,%s%d,%c", &stu[i].num, stu[i].name, &stu[i].age, &stu[i].sex);
    }
    printf("编号 姓名 年龄 性别\n");
    for(i=0; i<3; i++)
    {
        printf("%ld %s %d %c\n", stu[i].num, stu[i].name, stu[i].age, stu[i].sex);
    }
    return 0;
}

// 以下是运行过程
请输入依次输入学生的编号、姓名、年龄和性别:
1,hhh
12,m
请输入依次输入学生的编号、姓名、年龄和性别:
2,whh
9,g
请输入依次输入学生的编号、姓名、年龄和性别:
3,wang
18,b
编号 姓名 年龄 性别
1 hhh 12 m
2 whh 9 g
3 wang 18 b

结构体指针

结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。如果把一个结构体变量的起始地址存放在一个指针变量中,那么这个指针变量就指向该结构体变量。

结构体指针变量的定义

// 结构体指针变量定义的一般形式:
struct  结构体类型 *结构体指针;

c语言中引入了一个指向运算符“ -> ”,用于连接指针变量与其指向的结构体变量的成员。

// 以下三种形式是等价的
结构体变量.成员名
(*p).成员名
p->成员名

指向运算符“ -> ”的优先级最高

结构体数组指针

一个指针变量可以指向结构体数组,即将结构体数组的起始地址赋给指针变量,这种指针就是结构体数组指针。

// 例如定义一个student类型的结构体数组和指向该数组的指针变量
struct student stu[10], *p = stu;

结构体类型数据在函数间的传递

函数间不仅可以传递简单变量、数组、指针等类型的数据,也可以传递结构体类型的数据。函数之间结构体类型数据的传递和普通变量一样,可以“按值传递”,也可以“按地址传递”。

结构体变量作为函数参数

结构体变量的成员作为参数和结构体变量作为参数的用法同普通变量一样,属于“按值传递”方式。要注意实参和形参的类型要保持一致。

结构体指针变量作为函数参数

结构体指针变量存放的是结构体变量的首地址,所以结构体指针作为函数的参数,其实就是传递结构体变量的首地址,即“按地址传递”。因此在函数调用的过程中,实参和形参所指向的是同一组内存单元。

// 例如
#include <stdio.h>
struct student
{
    long num;
    char name[12];
    float score[3];
}stb1={2006001, "Li Ying", 66.0, 74.5, 80.0}, stb2={2006002, "Yang Li", 76.0, 84.5, 90.0};
void output(struct student *p)  /* 结构体指针变量作为形参 */
{
    printf("%ld\n%s\n%f\n%f\n%f\n", p->num, p->name, p->score[0], p->score[1], p->score[2]);
    printf("\n");
}
int main()
{
    output(&stb1);  /* 结构体变量地址作为实参 */
    output(&stb2);
    return 0;
}

// 结果
2006001
Li Ying
66.000000
74.500000
80.000000

2006002
Yang Li
76.000000
84.500000
90.000000

结构体数组作为函数参数

函数间不仅可以传递一般的结构体变量,也可以传递结构体数组。在传递结构体数组时,实参是数组名,即结构体数组的首地址;形参是指针,它接收传递来的数组的首地址,使它指向实参所表示的结构体数组,这种传递方式也就是“按地址传递”。

共用体

C语言中,共用体数据类型与结构体数据类型都属于构造类型。共用体数据类型在定义上与结构体十分相似,但它们在内存空间的占用分配上有本质的区别。结构体变量是各种类型数据成员的集合,各成员占用不同的内存空间,而共用体变量是从一起始地址开始存放各个成员的值,即所有成员共享同一段内存空间,但在某一时刻只有一个成员起作用。

共用体类型定义

union  共用体类型名
{
        数据类型 成员名1;
        数据类型 成员名2;
        ......
        数据类型 成员名n;
};

共用体变量的定义

  • 先定义共用体类型再定义共用体变量

    union data
    {
            int m;
            float x;
            char c;
    };
    union data a,b;
    
  • 在定义共用体类型的同时定义共用体变量

    union data
    {
            int m;
            float x;
            char c;
    }a,b;
    
  • 直接定义共用体类型变量

    union
    {
        short int m;
        float x;
        char c;
    }a,b;
    

“共用体”与“结构体”的定义形式类似,但是它们的含义却不同。结构体变量所占的内存长度是各成员所占的内存长度之和,而共用体变量所占的内存长度却是成员中所占内存最长的。

共用体变量的引用和初始化

  • 引用共用体变量中的一个成员

    共用体变量名.成员名
    共用体指针变量->成员名
    
  • 共用体类型变量的整体引用

    // 可以将一个共用体变量作为一个整体赋给另一个同类型的共用体变量
    union data a, b;
    ......
    a=b;
    
  • 共用体变量的初始化

    // 在共用体变量定义的同时只能用一个成员的类型值进行初始化
    // 一般形式如下:
    union 共用体类型名 共用体变量={第一个成员的类型值};
    

    在程序执行的任何时刻,共用体变量仅有一个成员变量驻留在共用体变量所占用的内存空间中,而结构体变量是所有成员都同时驻留在该结构体变量所占用的内存空间中。

    不能使用共用体变量作为函数参数,也不能使用函数返回共用体变量,但可以使用指向共用体变量的指针。共用体变量可以出现在结构体类型定义中,也可以定义共同体数组。

枚举类型

当一个变量的取值只限定为几种可能时,就可以用枚举类型。枚举是将可能的取值一一列举出来,那么变量的取值范围也就在列举值的范围内。

枚举类型的说明

// 枚举类型说明的一般形式如下:
enum  枚举类型名{枚举值1, 枚举值2, ......};

枚举型变量的定义

枚举类型变量定义的几种形式:

// 一般形式
enum {枚举值1, 枚举值2, ......}变量名表;

// 进行枚举类型说明的同时定义枚举型常量
enum flag{true, false}a, b;  // flag是枚举类型名

// 用无枚举类型
enum {true, false}a, b;

// 枚举类型说明和枚举类型变量定义分开
enum flag{true, false};
enum flag a, b;
  • 枚举类型说明中的枚举值本身就是常量,不允许对其进行赋值操作。
  • C语言中,枚举值被处理成一个整型常量,此常量的值取决于说明时各枚举值排列先后次序,第一个枚举值序号为0,以后依次加1。也可以指定枚举元素的值。
  • 枚举值可以进行比较。
  • 整数不能够直接赋给枚举变量。

用typedef定义类型

// C语言中,可用typedef定义新的类型名来代替已有的类型名
// 一般形式如下:
typedef  类型名 新名称;

// 例如:
typedef float REAL;  /* 用REAL代表float */

typedef struct  /* 定义类型DATE */
{
    int month;
    int day;
}DATE;
  • typedef可以声明各种类型名,但不能用来定义变量。
  • 用typedef只是对已经存在的类型增加一个类型的新名称,而没有构造新的类型。

文件

C语言中对文件的读写都是用库函数来实现的。标准C语言规定了若干组输入/输出函数,用它们对文件进行读写。以下是内存与磁盘之间数据传递的关系图:

内存与磁盘之间数据传递

文件类型指针

在缓冲文件系统中,每个被使用的文件都在内存开辟一个区域,用来存放文件的相关信息,把这些信息保存在一个结构体类型的变量中,由系统为该结构类型取名为FILE,包含在stdio.h文件中,例如:

typedef struct{
    short level;  /* 缓冲区使用量 */
    unsigned flags;  /* 文件状态标志 */
    char fd;  /* 文件号 */
    short bsize;  /* 缓冲区大小 */
    unsigned char *buffer;  /* 文件缓冲区首地址 */
    unsigned char *curp;  /* 指向文件缓冲区的指针 */
    unsigned char hold;  /* 其它信息 */
    unsigned istemp;
    short token;
}FILE;

对文件操作只需要使用FILE结构类型定义文件指针变量,以实现对文件的操作。定义文件指针的方法如下:

FILE *fp;

使用fp指向某一文件的结构体变量,从而可以访问该文件的信息。有几个文件就要设几个FILE类型的指针变量。

文件的打开与关闭

对文件进行读写操作之前应该进行“打开”文件的操作,使用后要进行“关闭”文件的操作。

打开文件

标准C语言规定引用I/O函数库时,用fopen()函数实现打开文件。

// 函数调用的一般形式
FILE *fp;
fp = fopen(文件名, 使用文件方式);
文件使用方式含义
“r”为输入打开一个文本文件
“w”为输出打开一个文本文件
“a”向文本文件尾增加数据
“rb”为输入打开一个二进制文件
“wb”为输出打开一个二进制文件
“ab”向二进制文件尾增加数据
“r+”为读/写打开一个文本文件
“w+”为读/写建立一个新的文本文件
“a+”为读/写打开一个文本文件
“rb+”为读/写打开一个二进制文件
“wb+”为读/写建立一个新的二进制文件
“ab+”为读/写打开一个二进制文件

若fopen函数不能打开文件,则函数返回NULL(在标准I/O库函数中NULL被定义为0)。

// 例如
if((fp=fopen("file1", "r"))==NULL)
{
    printf("Can't open this file\n");
    exit(0);  // 关闭所有文件,终止正在执行的程序
}

关闭文件

关闭文件即是文件指正不再指向该文件。文件使用完成后应该执行“关闭”操作。C语言用fclose函数关闭文件。

// 函数调用的一般形式
fclose(文件指针);

fclose函数成功关闭文件后返回0;否则返回EOF(EOF在stdio.h文件中被定义为-1)。

文件的顺序读写

格式化读写函数fscanf和fprintf

fscanf和fprintf函数是从文件中读写数据。

// 函数调用一般形式
fscanf(文件指针, 格式字符串, 输入表列);
fprintf(文件指针, 格式字符串, 输出表列);

// 例如
fscanf(fp, "%d%f", &i, &s);
fprintf(fp, "%d, %f", a, b);

字符方式读写函数fgetc和fputc

  • fgetc函数:从指定的文件中读出一个字符给变量

    // 函数调用的一般形式
    ch = fgetc(fp);
    // 当读到文件结束符时,函数返回一个文件结束标志EOF,其值为-1
    
  • fputc函数:实现向已打开的文件写入一个字符。

    // 函数调用的一般形式
    fputc(ch, fp);
    

数据块读写函数fread和fwrite

使用fread和fwrite函数可以一次读出或写入一组数据。

// 函数调用的一般形式
fread(buffer, size, count, fp);
fwrite(buffer, size, count, fp);
// buffer是一个指针,是读出或写入数据的起始地址。size是要读写数据的字节数。count是读写多个size字节数(数据项),fp是文件类型指针

字符串读写函数fgets和fputs

// fgets函数
fgets(str, n, fp);
// str是指向字符型的指针,n是整型量,fp是文件指针
// 从fp指向的文件中读出一个具有n-1个字符的字符串,存如起始地址为str的内存中


// fputs函数
fputs(str, fp);
// 将str指向的字符串输出到fp所指的文件中,向指定的文件输出一个字符串

文件的定位与随机读写

文件指针重定位函数rewind

// 函数调用的一般形式
rewind(fp);
// 使文件中的位置指针重新返回到文件的开头,该函数无返回值

随机读写函数fseek

随机读写就是根据用户的需要将文件中位置指针移动到某个位置的读写方式。

// 函数调用的一般形式
fseek(文件类型指针, 位移量, 起始点);

其它相关函数

  • 取文件指针的位置函数ftell

    ftell(fp);
    // 取得位置指针指向文件中的当前位置(即当前地址值)
    // 如果ftell函数返回值是-1L(长整型常量),则表示出错
    
  • 检测调用文件是否出错的函数ferror

    ferror(fp);
    // 若函数的值为非0则表示出错,返回0表示无错
    
  • 检测文件指针函数feof

    feof(fp);
    // 检测文件中位置指针是否移动到文件尾,若到了,成功返回1,否则为假
    


以上内容是我在学习《C语言程序设计教程》(索书号:9787111449980)的过程中做的一些笔记,有一些内容的删减,根据我自己的需求,我做了如上记录。如果发现有记录错误的地方,欢迎邮件反馈。感谢你的阅读,希望对你能有所帮助。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值