C语言基础
本文是学习菜鸟教程的C语言的笔记,简单常用的内容写得较详细,后面了解的内容写得较简略。
菜鸟教程的链接:菜鸟教程C语言教程
一、前言
1.1 第一个C程序实例
在屏幕上输出 Hello,World! 并换行:
#include <stdio.h>
int main()//程序的入口,一个程序有且只有一个main函数
{//程序从这里开始运行
/* 我的第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}//程序从这里结束
1.2 实例解析及注意的问题
- 所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
- /* … */ 用于注释说明。
- printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。
- stdio.h 是一个头文件(标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到stdio.h 头文件,会发生编译错误。
- return 0; 语句用于表示退出程序。
- 当是 void main() 时,main() 的返回值是空,所以可以不写或者是 return; 但这是以前的写法了,现在很少用 void main() 了,也不推荐大家这么用。现在 C 标准里规定 main() 返回值必须为 int,所以必须写成是 int main()。
二、简介
2.1 关于C
- 背景
C 语言是一种通用的、面向过程的计算机高级程序设计语言。C 语言也是现在最广泛使用的系统程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。当前最新的 C 语言标准为 C18 ,在它之前的 C 语言标准有 C17、C11、C99 等。 - 用途
C 语言最初是用于系统开发工作,特别是组成操作系统的程序。由于 C 语言所产生的代码运行速度与汇编语言编写的代码运行速度几乎一样,所以采用 C 语言作为系统开发语言。UNIX 操作系统,C编译器,MySQL和几乎所有的 UNIX 应用程序都是用 C 语言编写的。大多数先进的软件也都是使用 C 语言实现的。 - 特点
易于学习、结构化语言、可以产生高效率的程序、可以处理底层的活动、可以在多种计算机平台上编译。
2.2 C程序
- 环境设置
如果想要设置 C 语言环境,要确保电脑上有以下两款可用的软件,文本编辑器和 C 编译器。本人是初学小白,没设置过环境,直接是在Windows下使用的编译器软件Dec-C++,还有VS,VScode。 - 编译执行
一个 C 语言程序,可以是 几 行,也可以是数百万行。通过编辑器()创建的文件通常称为源文件,源文件包含程序源代码。C 程序的源文件通常使用扩展名".c"
,它通过编译转成机器语言,如果代码没有错误编译通过并生成一个".exe"
可执行文件,通过这个文件来执行程序并显示程序运行结果。它可以写在一个或多个扩展名为".c"
的文本文件中,例如,hello.c
。 - 最小的C程序必要格式
#include<stdio.h> int main() { ... return 0; }
- C程序基本结构
- 主要部分:预处理器指令、函数、变量、语句 和表达式、注释。
- C语言的程序构成方式:
1)c语言程序由函数构成,每个函数可以实现一个或多个功能。
2)一个正规程序可以有多个函数,但是有且只有一个主函数。
3)函数只有在被调用的时候才执行,主函数由系统调用执行。
4)函数的格式必须按照规范书写。
5)C 语言程序文件的后缀为.c
- 注意:
1)stdio
表示系统文件库, 也可以声明其它的;
2).h
表示头文件,因为这些文件都是放在程序各文件的开头;
3)#include
告诉预处理器将指定头文件的内容插入到预处理器命令的相应位 导入头文件的预编译指令;
4) <> 表示系统自带的库,也可以写成" "
表示用户自定义的库,如果写成" "
并且自定义的库里面没有这个文件系统会自动查找自带的库, 如果还是没有报错;
5) 所有的 C 语言程序都需要包含main()
函数,但有且只有一个, 代码从main()
函数开始执行;
return 0;
语句用于表示退出程序,表示代码在这里结束。
三、基本语法
3.1 C的令牌(Token)
1.分号 ;
- 在C程序中,分号是语句结束符。即每个语句必须以分号结束。表明一个逻辑实体的结束。
2.注释
- C语言有两种注释方式:
- 以
//
开始的单行注释,只单独占一行,也可以出现在一行中其他内容的右侧;//单行注释
- 以
/*
开始,以*/
结束的块式注释可单行也可多行,编译系统在发现一个/*
后,会开始找注释结束符*/
,把两者间的内容作为注释;/*单行注释*/ /* 多行注释 多行注释 */
注: 不能在注释中嵌套注释,注释也不能出现在字符串或字符值中那样只是作为字符串的一部分。
3.标识符
- C标识符是用来标识变量、函数或任何其它人为自定义项目的名称等。(编程时命名,使用的名字)
- C语言中标识符的命名规范:
- 标识符是有字母(A-Z或a-z)、数字(0-9)、下划线_,首字母不能是数字,标识符内不允许出现标点字符,如@、$、%等;
- 不能把C的关键字作为标识符,如:if、for、while等;(不能和C的关键字相同,也不能喝自定义的函数或C语言库函数同名)
- 标识符长度是有编译器系统决定的,一般限制为8字符;
- 标识符严格区分大小写,一般变量名用小写,符号常量名用大写;(C是区分大小写的编程语言)
- 标识符命名要做到“见名知意”。
4.关键字
-
如下表是一些常用的关键字,它们不能作为常量名、变量名、或其它标识符名称。
关键字名 说明 int 声明整型变量或函数返回值类型 float 声明浮点型变量或函数返回值类型 double 声明双精度浮点型变量或函数返回值类型 short 声明短整型变量或函数返回值类型 long 声明长整型变量或函数返回值类型 char 声明字符型变量或函数返回值类型 if/else 条件语句 for 循环语句 do/while 循环语句 switch 用于开语句 case/default 开关语句分支 break 跳出当前循环 continue 结束当前循环,开始下一轮循环 struct 声明结构体类型 union 声明共用体类型 enum 声明枚举类型 const 声明常量 auto 声明自动变量 static 声明静态变量 register 声明寄存器变量 extern 声明外部类型或全局变量,即变量或函数是在其它文件或本文件的其它位置 signed 声明有符号类型变量或函数 unsigned 声明无符号类型变量和函数 sizeof 计算数据类型或变量长度(即所占字符数) typedef 用以给数据类型取别名 void 声明函数无返回值或无参数,声明无类型指针 return 子程序返回语句(可以带参也可不带参)
5.空格
- 空格分隔语句的各个部分,让编译器能识别语句中的某个元素(如:int)在哪里结束,下一元素在哪里开始。
- 如下语句:
1.int age;
int 和 age 之间必须至少有一个空格字符(空白符),这样编译器才能区分它们;
2.sum = a + b;
该语句中的所有的空格符都不是必须的,但为了增加可读性,可以适当加一些空格。
四、数据类型
-
在C语言中,数据类型是用于声明不同类型变量或函数的一个广泛的系统。变量的类型决定了变量存储占用空间。
-
C中的数据类型可如下:
类型 描述 基本类型 算术类型包括整数类型和浮点类型 枚举类型 用来定义在程序中只能赋予其一定的 离散整数值的变量 void类型 类型说明符void表明没有可用的值 派生类型 包括指针类型、数组类型、结构类型、共用体类型和函数类型 -
数组类型和结构类型你统称为聚合类型;函数类型是函数返回值的类型。在本节接下来的部分将介绍基本类型,其它的在后面详解。
-
C语言标准没有定义布尔类型,所以判断真假以0为假,非0为真。也可以借助C语言的宏定义或头文件 <stdbool.h> 中的定义操作布尔型。
4.1 基本数据类型
-
整数类型
关于标准整数类型的存储大小(byte/字节)和值范围如下:类型 存储大小(字节) 值范围 char 1 -128–127或0–255 signed char 1 0–255 unsigned char 1 -128–127 int 2或4 -32,768–32,767或-2,147,483,648–2,147,483,647 unsigned int 2或4 0–65,535或4,294,967,295 short 2 -32,768–32,767 unsigned short 2 0–65,535 long 4 -2,147,483,648–2,147,483,647 unsigned long 4 0–4,294,967,295 注: 想要得到某个类型或变量在特定平台上的准确大小,可以用sizrof运算符。
sizeof(type)
得到对象或类型的存储字节大小。 -
浮点类型
关于标准浮点类型的存储大小、值范围和精度如下:类型 存储大小(字节) 值范围 精度 float 4 1.2E-38–3.4E+38 6位小数 double 8 2.3E-308–1.7E+308 15位小数 long double 16 3.4E-4932–1.1E+4932 19位小数 注: 头文件 float.h 定义了宏,在程序中可以使用这些值和其它有关实数二进制表示的细节。
-
void类型
void 类型指定没有可用的值,或可以说返回空。一般用于如下情况中:类型 描述 函数返回为空 C中各种函数都不返回值,即返回空;不返回值的函数的返回类型为空。 函数参数为空 C中有各种函数不接受任何参数。不带参数的函数可接受一个 void ,如:int sum(void); 指针指向 void 类型为 void* 的指针代表对象的地址,而不是类型。如:内存分配函数 void *malloc(size_t size); 返回指向 void 的指针,可以转换成任何类型
注: 在后面会详解有关概念。
五、变量
- 变量只不过是程序可操作的存储区的名称。C中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值可以存储在内存中,运算符可应用于变量上。
- 变量的名称就是标识符
- C语言还可定义其它类型的变量,如:数组、结构体、指针等,在本节只讲解基本变量类型。
5.1 变量定义
- 变量定义是告诉编译器在何处以及如何创建变量的存储空间。变量定义是指定一个数据类型,并包含了该类型的一个或多个变量的列表。
type variable_list;
type 必须是一个有效的C数据类型,可以是 char、int、double 或任何自定义的对象,variable_list 可以是一个或多个变量名组成,多个变量名之间用逗号隔开。如下是有效的声明:int age,height,weight;//声明并定义了三个 int 型变量age、height、weight char c;//声明并定义了一个char型变量c
- 变量可以在声明时指定一个初始值(初始化),一旦赋值一定是定义,只有定义时才会分配存储空间,声明不会。例如:
int i = 1, j = 2;//定义并初始化 i 和 j char c = 't';//变量 c 的值为 't'
- 不带初始化的定义:带有静态存储持续时间的变量(如:指针变量)会被隐式初始化为 NULL(所有字节的值都是0),其它所有变量的初始值是未定义的。(C语言中一个默认没有初始化的变量值是一个不可知的很大值或0,要看环境)
5.2 变量声明
- 变量声明是向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。
- 变量的声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。(声明之后不能直接使用这个变量,必须定义后才能使用)
- 变量的声明分两种情况:
- 需要建立空间的。如:int a;声明时已经建立了空间;
- 不需要建立空间的,通过使用 extern 关键字声明变量名而不定义它。如:extern int a; 变量 a 可以在别的文件中定义的;
- 除非有 extern 关键字,不然都是变量的定义。
extern int a;//声明,不是定义 int a;//声明也是定义
- C中的两种类型的表达式:
1.左值:指向内存位置的表达式。可以出现在赋值号的左边或右边;
2.右值:存储在内存中某些地址的数值。不能对其赋值,只能出现在赋值号的右边。
注:在一个表达式中,左值必须是变量,右值可以是变量、常量或表达式。 - 注意
1.变量先定义再赋值,变量在使用前要定义或声明;
2.在一个程序中,一个变量只能定义一次,却可以声明多次。
六、常量
- 常量是固定值,在程序执行时不会改变;常量就像是常规的变量,只不过它的值在定义后不能修改。
6.1 常量的类型
- 常量可以是任何的基本数据类型,如整数常量、浮点常量、字符常量,还有字符串常量、也有枚举常量。
- 整数常量
- 可以是十进制、八进制或十六进制的常量。常量指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
- 整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写也可是小写,U和L的顺序任意。
- 例如:
85//十进制 0213//八进制 0x4b//十六进制 30//整数 30u//无符号整数 30l//长整数 30ul//无符号长整数
- 浮点常量
- 浮点常量是有整数部分、小数点、小数部分和指数部分组成。也可使用小数或指数形式表示浮点常量。
- 当使用小数形式表示,必须包含整数部分、小数部分,或同时包含这两个。
- 当使用指数形式表示时,必须包含小数点、指数或同时包含两者。带符号的指数是用 e 或 E 引入的。
- 例如:
3.14159 3.14159E-5L
- 字符常量
- 字符常量是在括在单引号中,如 'x’可以存储在 char 类型的简单变量中。
- 字符常量可以是一个普通的字符、一个转义字符(例:’\t’ ),一个通用的字符(例:’\u02C0’)。
- 如下是一些常用的转义字符:
转义字符 含义 \\ \字符 \’ '字符 \’’ ''字符 \? ?字符 \a 警报铃声 \b 退格键 \f 换页符 \n 换行符 \r 回车 \t 水平制表符 \v 垂直制表符 \ooo 一到三位的八进制数 \xhh 一个或多个数字的十六进制数
- 字符串常量
- 字符串常量是括在双引号中的(例:
"hello, dear"
)。一个字符串包含类似于字符常量的字符:普通的字符、转义字符、通用的字符。可以使用空格作分隔符,把一个很长的字符串常量分行。
6.2 常量的定义
- 在C中,有两种简单的定义常量的方式:
1.使用#define
预处理器;
2.使用const
关键字。
- #define 预处理器
#define 变量名(一般全大写) 变量值(后面没有分号) #define AGE 18 #define NEWLINE '\n'
- const关键字
const 前缀声明指定类型的常量。
const 声明常量必须再一个语句内完成。const int var = 5;//const 数据类型 变量名 = 变量值; const int var;//错误 const int var; var = 5;//错误
- 注意:
#define
是宏定义,不能定义常量,但是它可以实现在字面意义上和其它定义常量相同的功能(定义的是不带类型的常数,只是简单的替换),本质的区别是#define
不为宏名分配内存;
#define
的 “边缘效应”,例如:#define N 2+3//错误,实际预处理时编译器将 a=N/2 处理成 a=2+3/2 ,结果不是2.5而是3.5 #define N (2+3)//正确 double a; a = N/2;
const
通常也不为常量分配内存(为提高效率),const
定义的是变量而不是常量,它改变了一个变量的存储类型,把该变量所占的内存变为只读,所以变量的值不能改变。#define PI 3.14159//常量宏 const double Pi =3.14159;//此时并未将Pi放入ROM中 double a = PI//编译期间进行宏替换,分配内存 double b = PI//再进行宏替换,再分配内存 double c = Pi;//此时为Pi分配内存,以后不再分配 double d = Pi;//不分配内存
七、存储类
- 存储类定义变量或函数的范围(可见性)和生命周期。它们放在所修饰的类型之前。
7.1 常用存储类
- auto
auto 声明自动变量,auto 存储类是所有局部变量默认的存储类。auto 只能用在函数内,即它只能修身局部变量。例如:{ int a;//默认auto,可以省略 auto int b;//这两个存储类相同 }
- register
register 声明寄存器变量,register 存储类用于定义存储在寄存器中而不是内存中的局部变量。即变量的最大尺寸等于寄存器的大小,它也没有内存地址。
注: register 只用于需要快速访问的变量,如计数器。它并不意味着把变量存储在寄存器中,而是变量可能存储在寄存器中,这取决于硬件和实现的限制。{ register int a; }
- static
static 声明静态变量,是所有全局变量的默认存储类,static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。用static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可用在全局变量,此时变量的作用域只限制在声明它的文件内。
全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。
注: 静态全局变量(void fun(void) { static int i = 0; i++; //i 是 fun 函数的局部变量,只初始化一次;每次调用 fun 函数,i 的值不会被重置 }
static int a;
)和普通变量(int a;//默认也是静态存储方式
)的区别:非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量的作用域只限制在定义该变量的源文件内有效,在同一个源程序的其它文件中不能使用它。 - extern
extrern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件上都是可见的。当使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
当有多个文件且定义了一个可以在其它文件中使用的全局变量或函数时,可以在其它文件中使用 extern 来得到已定义的变量或函数的引用。即 extern 是用来声明一个已经在另一个文件中定义的一个全局变量或函数。
extern 修饰符一般用于当两个或多个文件共享相同的全局变量或函数时。
八、运算符
- 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。
8.1 运算符的类型
- c语言内提供了算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其它运算符。
-
算术运算符
基本算术运算符:+、–、*、/;
% 取模运算符、++ 自增运算符、- - 自减运算符。b = a++; b = a--; //后置,先赋值后运算 b = ++a; b = --a; //前置,先运算后赋值
-
关系运算符
判断两个操作数的值的关系,一般用在条件语句中。
== 等于、!= 不等于、> 大于、< 小于、>= 大于等于、<= 小于等于。 -
逻辑运算符
&& 逻辑与运算符:若两个操作数都菲零(为真),则结果为真;
|| 逻辑或运算符:若两个操作数中有一个非零,则结果为真;
! 逻辑非运算符:逆转操作数的逻辑状态,若非零为假,则结果为真。 -
位运算符
位运算符作用于位,并逐位进行操作。运算符 描述 运算规则 & 按位与,按二进制位进行 “与” 运算 0&0=0 0&1=0 1&0=0 1&1=1 | 按位或,按二进制位进行 “或” 运算 0|1=0 0|1=1 1|0=1 1|1=1 ^ 异或运算符,按二进制位进行 “异或” 运算 0^0=0 1^ 1=0 0^1=1 1^0=1 ~ 取反运算符,按二进制位进行 “取反”运算 ~0 = – 1 ~1= – 2 << 二进制左移运算符 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0 >> 二进制右移运算符 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 -
赋值运算符
= 简单的赋值运算符
= 还可以和上面的几种运算符组合使用:a += b;//加且赋值运算符,相当于 a = a + b; a -= b;//减且赋值运算符,相当于 a = a - b; a *= b;//乘且赋值运算符,相当于 a = a * b; a /= b;//除且赋值运输符,相当于 a = a / b; a %= b;//求模且赋值运算符,相当于 a = a % b; a <<= 2;//左移且赋值运算符,相当于 a = a << 2; a >>= 2;//右移且赋值运输符,相当于 a = a >> 2; a &= 2;//按位与且赋值运算符,相当于 a = a & 2; a ^= 2;//按位异或且赋值运算符,相当于 a = a ^ 2; a |= 2;//按位或运算符,相当于 a = a | 2;
-
其它运算符
如下是几个常用的其它运算符:运算符 描述 sizeof() 返回变量的大小 & 返回变量的地址 * 指向一个变量(指针) ?: 条件表达式(三目运算符),例: c = a>b?a:b;
若a>b为真则 c=a ,若 a>b 为假则 c=b
8.2 运算符的优先级
- 运算符的优先级确定表达式中项的组合。即确定了一个表达式的运算顺序。
各个运算符按运算符的优先级从高到低如下:
注意:类别 运算符 运算顺序 后缀 ()、[]、->、++、- - 从左到右 一元 +、-、!、~、++、- -、(type)*、sizeof 从右到左 乘除 *、/、% 从左到右 加减 +、- 从左到右 移位 <<、>> 从左到右 关系 <、<=、>、>= 从左到右 相等 ==、!= 从左到右 位与 & 从左到右 位异或 ^ 从左到右 位或 | 从左到右 逻辑与 && 从左到右 逻辑或 || 从左到右 条件 ?: 从右到左 赋值 = 及与它组合的运算符 从右到左 逗号 , 从左到右 - 利用异或 ^ 来交换两个数的值,而且不引入其它变量,但这只适用于整型变量,不能用于浮点型变量;
a = a ^ b; b = a ^ b; a = a ^ b;
- 利用位与 & 可以判断一个整数是否是2的整数次幂;
int fun(int n) { return ( (n>0) && (( n & (n-1)) == 0));//2的次幂大于0 //返回1是,返回0不是 }
- 取余 % 也就是求余数,C中的取余运算只对于整数,% 的两边都必须是整数。余数也可以为正也可为负,有 % 左边的数的正负决定,如果左边的数为正,则余数也为正。
- 如果两个不同长度的数据进行位运算时,系统将二者按右端对齐,然后进行运算;
- 判断语句中推荐使用逻辑运算符,而不是位运算符;
- 运算符优先级:初等>单目>算术>关系>逻辑>条件>赋值;单目运算符的优先级高于双目运算符。
- 利用异或 ^ 来交换两个数的值,而且不引入其它变量,但这只适用于整型变量,不能用于浮点型变量;
九、判断与循环
- 一般情况下,语句是按顺序执行的, 但需要根据条件选择时或者循环执行一段代码时就要用到程序控制结构。
9.1 判断
- 判断结构是根据条件的真假而选择所要执行的那一段程序 (选择)。
- C语言中吧任何非0或非空的值假定为 true ,把0或 null 假定为 false 。
- 一般判断结构的流程图:
- 判断语句
if 语句:
if…else 语句:if(条件表达式)//也叫布尔表达式 { //如果条件表达式为真将执行的语句 //如果条件表达式为假将执行闭括号后的语句 }
switch 语句:if(条件表达式) { //如果条件表达式为真将执行的语句 } else { //如果条件表达式为假将执行的语句 }
允许测试一个变量等于多个值时的情况。
注:switch(常量表达式){//这个表达式必须是整型或枚举类型 case 常量1://所有 case 后的必须是与 switch 中的变量同类型的常量或字面量 程序块1;//当被测试的常量的等于 case 后的常量时执行,直到遇到 break 为止 break;//可选,当遇到 break 时 switch 终止,控制流将执行 switch 后(闭括号后)的语句 case 常量2: 程序块2; break;//可选,不是每一个 case 都要有一个 break ,如果没有控制流将继续后续的 case ,直到遇到 break 为止 ...//可以有任意数量的 case 语句 default ://可选,在一个 switch 语句结尾可以有一个可选的 default 语句 程序块0;//当上面所有 case 都不为真时执行 }
- 一个
if
语句后可跟一个可选的else if...else
语句,用于多个条件;
一个if
后可跟0个或一个else
,else
必须在所有的else if
之后;
一个if
后可跟0个或多个else if
,else if
必须在else
之前;
一旦某个else if
匹配成功其它的else
或else if
将不会被测试。 - if 语句和 switch 语句都可以嵌套使用,在一个 if 语句(else if 语句)或 switch 语句中再使用一个 if 语句(else if 语句)或 switch 语句。
- 一个
- ?:运算符
条件运算符(三元运算符),可以用来替代 if…else 语句。
9.2 循环
- 循环语句用于多次执行同一块代码(一个语句或语句组)。
一般循环结构的流程图:
-
循环类型
while 循环:while(条件表达式) { 语句块;//只要条件表达式为真,该语句块将重复执行直到条件表达式为假时推出循环或循环结束,继续执行循环的下一条语句 //while 循环可能一次都不执行 }
for 循环:
for(语句1;语句2;语句3) { 语句块;//循环体 }
for 循环的控制流:
- 语句1先执行且只执行一次,允许声明并初始化任何循环控制变量。也可在这里不写任何语句,只要有一个分号即可;
- 接着判断语句2(条件语句),若为真则执行循环体;若为假则不执行循环体直接执行 for 循环后面的语句;
- 在执行完 for 循环体后,控制流会跳回上面的语句3,该语句将更新循环控制变量。也可不写任何语句;
- 条件再次被判断。若为真则再执行循环体,这个过程会不断重复(循环体、更新循环控制变量、再重新判断条件),在条件为假时循环终止。
do…while 循环
- 此循环是在循环的尾部检查条件,所以循环语句块会在被测试前至少执行一次。
do { 语句块; }while(条件表达式); //若条件为真则跳转到 do 执行循环体的语句块,这个过程不断重复,直到不满足条件时为止
嵌套循环
- 可以在一个任何类型的循环里嵌套使用另一个任何类型的循环。
-
循环控制语句
- 可以改变代码的执行顺序,实现代码的跳转。
break 语句:
- 当 break 语句出现在一个循环内时,循环立即终止,且程序流将执行循环的下一条语句;
- 它还用于终止 switch 语句中的 case ;
- 若在嵌套循环内,break 会停止执行最内层的循环,并执行该块后的下一行代码。
continue 语句:
- 会跳过当前循环中的代码,强迫开始下一次循环;
- 对于 for 循环,continue 语句会执行后自增语句后仍然执行,对于while 或 do…while 循环,则重新执行条件判断语句。
goto 语句:
- 将控制转移到被标记的语句,但是不建议在程序中使用。
-
无限循环
- 如果条件永不为假,则循环将变成无限循环(死循环);
- 由于构成循环的三个表达式都不是必需的,所以一般使用 for 循环来表示,例:
for( ; ; ){...}
(可以按Ctrl+c
终止无限循环)。
注意:
- 任意类型的判断和循环里面都可以自由组合嵌套使用任意类型的判断和循环;
- 如果判断语句后或循环体的
{...}
里面只有一条语句,可以省略这对花括号。
十、函数
- 函数是一个执行一组特定任务的语句。每个C程序都至少有一个函数,即主函数
main()
。还可以自定义其它的函数,C标准库还提供了很多的内置函数。函数也称方法。
10.1 函数的定义和使用
- 函数声明告诉了编译器函数的名称、返回类型、和参数。函数定义提供了函数的实际主体。
-
函数的定义
定义的一般形式:返回类型 函数名称 (参数列表)//函数头 { //函数主体 }
- 返回类型: 一个函数可以返回一个值。函数返回类型指的是函数返回的值的数据类型。有的函数只执行所需的操作而不返回值,这时返回类型的关键字就是 void ;
- 函数名称: 函数的实际名称。函数名和函数的参数列表一起构成了函数签名;
- 参数: 当函数被调用时,我们给函数的参数传递一个值,这个值被称为实际参数(实参)。参数列表包括函数参数的类型、顺序、数量。函数也可以不写参数。
- 函数主体: 包含了定义函数执行任务的语句。
//示例:返回两个数中最大的那个数的函数 int max(int n1,int n2){ int result;//声明局部变量 if(n1>n2) result = n1; else result = n2; return result; }
-
函数的声明
- 函数声明的一般形式:
返回类型 函数名 (参数列表);
对于上面定义的函数max()
,函数声明是:int max(int n1, int n2);
- 函数声明中参数的名称不是必需的,只有参数的类型是必需的,上面的函数声明也可写成:
int max(int, int);
但是参数类型的顺序要和定义的参数的顺序一致; - 一般用在一个源文件中定义的函数在另一个源文件中调用时,这种情况在调用函数的文件的顶部或开始声明函数。
- 函数声明的一般形式:
-
函数的调用
- 创建函数会定义它来做什么,然后通过调用函数来完成已定义的任务。
- 当程序调用函数时,程序控制权转移给被调用的函数,当函数的返回语句被执行或到达函数的结束括号时表示被调用的函数执行完成,把程序控制权又交还给主程序。
- 调用函数时,传递所需的参数。
10.2 函数的参数
- 如果函数要使用参数,则必须声明接受参数值的变量,这些变量称为函数的形式参数(形参)。形参就像函数内的其它局部变量,在进入函数时被创建退出时被销毁。
- 当调用函数时有三种函数传递参数的方式:
- 值传递调用
该方法把参数的实际值复制给函数的形参,这种情况下修改函数内的形参不会影响实参。例如:void swap(int x,int y){ int temp = x; x = y; y = temp; }
- 引用传递调用
通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参操作。例如:void swap(int &x,int &y){ int temp = x; x = y; y = temp; }
- 指针传递调用
指针传递过程中,将实参的地址传给形参,在函数体内部改变了实参所在地址的值。例如:void swap(int *x,int *y){ int temp = *x; *x = *y; *y = temp; }
- 值传递调用
- C 默认使用传值调用来传递参数。
注意:
- 函数先声明后调用;
- 本质上说,C 里面所有的函数的参数传递都是值传递。指针传递之所以能改变传递参数变量的值是因为函数传进来的不是指针本身,而是指针所指向的值;
- 函数名和参数列表构成了函数签名,这意味着参数列表不同但是函数名可以相同(它们是两个不同的函数);
- 函数声明和函数定义原型的参数名可以不同,但是参数列表的顺序必须一致。
- 形参在函数没有调用时不占用内存单元,函数调用时才分配内存空间,调用结束后,形参被释放,实参保留原值(单向传值)。
十一、作用域规则
- 任何一种编程中,作用域是程序中定义的变量的所存在的区域,超过该区域变量就不能被访问。C语言中有三个地方可以声明变量:
1.在函数或块内的局部变量
2.在所有函数外部的局部变量(一般是程序的顶部)
3.在形式参数的函数参数定义中
11.1 局部和全局变量
- 局部变量:只能被函数或该代码块内部的语句使用。在函数外部是不可知的。
- 全局变量:在整个程序声明周期内都是有效的,在任意函数内部都能访问。
注:#include<stdio.h> /*全局变量声明*/ int g; int main(){ /*局部变量声明*/ int a,b; /*实际初始化*/ a =1; b = 1; g = a + b; return 0; }
- 变量和全局变量可以同名,但在函数内部如果名字相同,会使用局部变量值,不会是用全局变量。
- 全局变量和局部变量的在内存中区别:
- 全局变量保存在内存的全局存储区中,占用静态存储单元;
- 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
- 初始化局部和全局变量,当局部变量被定义时,系统不会对其初始化,必须自行初始化;定义全=全局变量时系统会自动对其初始化,如下所示:
数据类型 初始化默认值 int 0 char ‘\0’ float 0 double 0 pointer(指针) NULL - 正确地初始化变量是一个良好的编程习惯,否则有时程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。
11.2 形式参数
- 形参,被当作该函数内的局部变量(与全局变量同名时会优先使用)。
#include<stdio.h> /*全局变量声明*/ int g; int sum(int a,int b) return a+b;//形参在函数内作局部变量优先使用 } int main(){ /*局部变量声明*/ int a,b; /*实际初始化*/ a =1; b = 1; g = sum(a,b); return 0; }
十二、数组
- 数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。数组中的特定元素可以通过索引访问,第一个索引值为0.
12.1 声明定义
- C中声明一个数组,要指定元素的类型和数量。例如:
type arrayName [arraySize];
- 这叫一维数组。arraySize 必须是一个大于0的整数常量;type 可以是任意有效的C数据类型。例如:
double balance [10];
声明了一个 double 类型的包含10个元素的数组 balance 。
- 这叫一维数组。arraySize 必须是一个大于0的整数常量;type 可以是任意有效的C数据类型。例如:
12.2 初始化和访问
- 在C中可以逐个初始化数组,也可使用一个初始化语句。
- 例如:
double balance [5] = {1.0,2.0,3.0,4.0,5.0};
大括号之间的值的数目不能大于我们在数组声明时在 [] 中指定的元素数目;double balance[5] = {1.0,2.0;
该语句是声明了一个 double 型含有5个元素的数组并给它的第一个和第二个元素赋值初始化;double balance [4] = 5。0;
是为数组中的第五个元素赋值;数组的最后一个索引是数组的总大小或长度减去1。
- 以下是上面的数组的图形表示:
- 数组元素可以通过数组名加索引进行访问。例如:
double a = balance[4];
#include<stdio.h> int main(){ int n[10];//n是一个含10个整数的数组 int i,j; for(i = 0;i < 10;i++){ n[i] = i + 100;//初始化数组元素 } for(j = 0;j < 10;j++){ printf("Element[%d] = %d\n",j,n[j]);//输出数组中每个元素的值,也是访问 } return 0; }
12.3 C中的数组详解
- 多维数组
- 多维数组的最简单形式是二维数组,二维数组本质上还是一个一维数组的列表,
type arrayName [x][y];
声明了一个 x 行 y 列的二维数组 。type 可以是任意有效的 C 数据类型,arrayName 是一个有效的 C 标识符。例如;int x[2][2];
声明了一个整型的2行2列的二维数组。 - 多维数组可以通过在括号内为每行指定值来进行初始化。
int a[2][2] = {{1,2},{3,4}};//行优先,先行后列 int a[2][2] = {1,2,3,4};//两种初始化是等同的
- 二维数组中的元素是通过使用下标即数组的行索引和列索引来访问的。例如:
int b = a[0][1];
可以得到二维数组的第1行第2 个元素。 - 一般使用嵌套循环来处理二维数组:
#include<stdio.h> int main(){ int a[2][2] = {1,2,3,4}; int i,j; for(i=0;i<2;i++){ for(j=0;j<2;j++){ printf("a[%d][%d] = %d\n",i,j,a[i][j]); } } return 0; }
- 二维数组存放字符串,读取时当一维数组使用,例如:
char name[2][20] = {"关羽","张飞"};
注:
1.二维数组在逻辑上是方阵,由行和列组成;但在物理上是线性的,按行来依次进行存放,内存是连续的。
2.将二维数组作参数时,必须指明所有维数 的大小或省略第一维的,但不能省略第二维或更高维的大小,这是由编译器原理限制的(编译器的寻址方式)。例如:int a[2][3],(a+0) 就是第1行的首地址,(a[0][0]+0)就是第1行第1个元素的地址。
- 传递数组给函数
- 如下是三种声明函数形式参数的方式,结果都一样:
方式1:void Function(int *p){...}//形式参数是一个指针
方式2:void Function(int p[10]){...}或void Function(int p[],int n){...}//形参是一个已定义大小的数组
方式3:void Function(int p[],int n){...}//形参是一个为定义大小的数组
注: 对于函数,数组的长度无关紧要,因为C不会对形参执行边界检查。
- 从函数返回数组
- C语言不允许返回一个完整的数组作为函数的参数,但可以通过指定不带索引的数组名来返回一个指向数组的指针,详细内容在后面有关指针的知识中。另外,C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。
- 指向数组的指针
- 数组名是一个指向数组中第一个元素的常量指针。例如:
int a[5];
数组名 a 是一个指向 &a[0] 的指针,即数组 a 的第一个元素的地址。访问数组元素的时候,*(a+1) 就是第二个元素。 *(a+2) 就是第三个元素等。
十三、枚举(enum)
- 枚举是C中的一种基本数据类型。可以让数据更简洁易读。
13.1 枚举变量的定义
- 枚举类型定义格式:
enum 枚举名 {枚举元素1,枚举元素2,……};
- 例如一星期有7天,用 #define 来为每个整数定义一个别名如下:
使用枚举如下:#define mon 1 #define tue 2 #define wed 3 #define thu 4 #define fri 5 #define sat 6 #define sun 7
注: 第一个枚举成员的默认值为整型0,后续成员值为前一个成员值加1。可以在定义时改变枚举元素值,没有指定值的元素值为前一元素值加1。例如:enum DAY {mon=1,tue,mon,thu,fri,sat,sun};
enum se {ss,sa=3,sb,sd};
其中 ss 的值为0,sa 的值为3,sb 的值为4,sd 的值为5。 - 枚举变量的定义
- 前面只是声明了枚举类型,下面是定义枚举变量三种方式:
1. 先定义枚举类型,再定义枚举变量
2. 定义枚举类型的同时定义枚举变量enum day {m=1,tu,w,th,f,sa,su}; enum day d1;
3. 省略枚举名称,直接定义枚举变量enum day {m=1,tu,w,th,f,sa,su} d1;
enum {m=1,tu,w,th,f,sa,su} d1;
- 前面只是声明了枚举类型,下面是定义枚举变量三种方式:
13.2 枚举变量的使用
- 在C中枚举类型是被当做 int 或 unsigned int 类型来处理的,所以一般是不能遍历枚举类型的,不过枚举类型必须连续是可以实现有条件的遍历。
- 使用 for 来遍历连续枚举类型的元素:
以下枚举类型不连续不能遍历:#include <stdio.h> enum DAY {mo=1,tu,we,th,fr,sa,su} day ; int main(){ for(day = mo;day <=su;day++) printf("%d \n",day); return 0; }
enum se {ss,sa=3,sb,sd};
- 在 switch 中的使用:
#include <stdio.h> #include <stdlib.h> int mian(){ enum color { red =1,green,blue }; enum color lovecolor; printf("请输入你喜欢的颜色:(1.红,2.绿,3.蓝)"); scanf("%d",&lovecolor); switch(lovecolor) { case red: printf("你喜欢的颜色是红色"); break; case green: printf("你喜欢的颜色是绿色"); break; case blue: printf("你喜欢的颜色是蓝色"); break; default: printf("你没有选择你喜欢的颜色"); } return 0; }
- 将整数转换为枚举:
#include <stdio.h> #include <stdlib.h> int maiin(){ enum day {sa,su,mo,tu,we,th,fr} workday ; int a = 1; enum day weekend; weekend = (enum day) a;//类型转换 //weekend = a;//错误 printf("weekend:%d",weekend); return 0; }
十四、指针
- 指针也就是内存地址,指针变量就是用来存放内存地址的变量。就像其他变量或常量一样,必须 在使用前进行声明。
14.1 定义使用
- 指针变量的一般声明形式:
type *name;
type 是指针的基类型,它是指针指向变量的类型,必须是一个有效的 C 数据类型;name 是指针变量名;* 是用来指定一个变量是指针。 - 例如:
int *p;
一个整型的指针,double *s;
一个 double 型的指针,所有实际数据类型对应指针的值的类型是一样的,都是一个代表内存地址的长的十六进制数,不同数据类型的指针之间唯一不同的是指针所指向的变量或常量的数据类型不同。 - 定义使用如下:
int var = 1;//声明实际变量 var int *p;//声明指针变量 p p = &var;// & 取地址运算符(访问变量或常量的内存地址),定义指针变量p,在 p 中存储 var 的地址 printf(%p,&var);// var 变量的地址,用%p输出指针类型 printf("%p",p);// p 变量存储的 var 的地址 printf("%d",var);// var 变量 printf("%d",*p);// *p 变量的值 //var 和 *p 等价
- NULL 指针:在变量声明时若没有确切的地址赋值,就为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为0的常量。
14.2 指针详解
- 指针的算术运算
- C 指针是一个用数值表示的地址。可以对指针进行四种算术运算:++、- -、+、–。假设 p 是一个指向地址 1000 整型指针,是一个32位的整数,执行
p++;
后,p 将指向位置1004,因为 p 每增加一次它就指向下一个整数位置, 一个 int 型数占4个字节;若 p 指向的地址为1000的字符,执行完上面的运算后它将指向位置1001,因为一个字符占一个字节,下一个字符的位置在1001。
注: 指针每一次递增它就指向下一个元素的存储单元,指针的每一次递减它就指向前一个元素的存储单元,指针在递增递减时跳跃的字节数取决于指针所指向变量数据类型长度。 - 指针的比较:指针可以用关系运算符进行比较,如 ==、< 和 > 。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
- C 指针是一个用数值表示的地址。可以对指针进行四种算术运算:++、- -、+、–。假设 p 是一个指向地址 1000 整型指针,是一个32位的整数,执行
- 指针数组
- 指针数组的声明:
type *ptr[MAX];
数组存储指向 type 数据类型的指针,例如:int *p[5];
p 数组中的5个元素都是一个指向 int 值的指针。
注: 在32 位系统中指针占4个字节;数组指针表示指向数组的指针即这个指针存放着一个数组的首地址;数组的 [ ] 的优先级高于 指针的 * ,所以int *a[3];
本质是一个数组,指针数组。
- 指针数组的声明:
- 指向指针的指针
- 指向指针的指针是一种多级间接寻址形式,或是一个指针链,第一个指针包含了第二个指针的地址,第二个指针指向包含了实际值的位置。
一个指向指针的指针变量必须如下声明,即在变量名前加两个 * 。例如,声明一个指向 int 型指针的指针:int **var;
int var = 1; int *p,**pp; p = &var; pp = &p; //*p、*pp、var 等价
- 指向指针的指针是一种多级间接寻址形式,或是一个指针链,第一个指针包含了第二个指针的地址,第二个指针指向包含了实际值的位置。
- 传递指针给函数
- 传递指针给函数只要声明函数参数为指针类型即可。例如:
void f(int *p);
p 也可以是一个数组名。
注: 函数指针是指向函数的指针,一个函数在编译之后会占据一部分内存,而它的函数名就是这段函数首地址,指向函数的指针必须初始化或赋为空,才能在函数调用中使用。与数组一样:禁止对指向函数的指针进行自增运算、对函数名赋值,函数名也不能用于算术运算;指针函数是返回值为指针的函数。
- 传递指针给函数只要声明函数参数为指针类型即可。例如:
- 从函数返回指针
- C 中不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。
int *getRandom(){ static int r[10]; int i; for(i=0;i<10;++i) r[i] = rand();//随机生成一个数 return r; }//生成10个随机数并使用表示指针的数组名来返回它们。 int main(){ int *p; p = getRandom(); return 0; }
- C 中不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。
十五、函数指针和回调函数
15.1 函数指针
- 函数指针是指向函数的指针变量。它可以像一般函数一样,用于调用函数、传递参数。函数指针变量的声明:
typedef int (*fun_ptr)(int ,int);
- 例如:
#include <stdio.h> int max(int x,int y){ return x>y ? x:y; } int main(){ int (*p)(int ,int) = &max;//&可省略 int a,b,c,d; scanf("%d %d %dd",&a,&b,&c); //d = max(max(a,b),c); d = p(p(a,b),c);与上面直接调用等价 return 0; }
15.2 回调函数
- 函数指针作为某个函数的参数
回调函数就是一个通过函数指针调用的函数。简单来说回调函数就是由别人的函数执行时调用你实现的函数。
以上#include <stdio.h> #include <stdlib.h> //回调函数 void fr(int *a,size_t asize,int (*getNV)()){ for(sixe_t i=0);i<asize;i++)// size_t 类型是C中自定义的一般为8字节的长整型数 a[i] = getNV(); } //获取随机值 int getNRV(){ return rand(); } int main(){ int ma[10]; fr(ma,10,getNRV);//使用时不加() return 0; }
fr
函数定义了三个参数,第三个参数是函数指针,定义了回调函数getNRV
,它返回一个随机值,作为一个函数指针传递给fr
函数,它将调用10次回调函数,并将回调函数的返回值赋给数组。
十六、字符串
- 在 C 中实际是使用
null
字符\0
终止的一维字符数组。
16.1 定义使用
- 如下是声明和初始化创建一个字符串。由于在数组 的末尾存储了空字符,所以字符数组的大小比字符串实际长大一:
char str[6] = {'H','e','l','l','o','\0'};
也可以写成char str[] = "hello";
实际上 C 编译器会在初始化数组时自动把\0
放在字符串的末尾。
16.2 操作字符串函数
- 这些函数一般都在头文件
string.h
中,加头文件是个好的编程习惯,程序清晰易懂,也不会因为编译器的原因不能执行(有些编译器要求一定要头文件有些则会自动帮你加上)。
函数 | 目的 |
---|---|
strcpy(s1,s2) | 复制字符串 s1 到字符串 s2 |
strcat(s1,s2) | 连接字符串 s2 到字符串 s1 的末尾 |
strlen(s1) | 返回字符串 s1 的实际长度 |
strcmp(s1,s2) | 如果字符串 s1 和 s2 是相同的则返回1;如果 s1<s2 则返回小于0;如果 s1>s2 则返回大于0 |
strchr(s1,ch) | 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置 |
strstr(s1,s2) | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置 |
注意:
- strlen 是函数,sizeof 是运算操作符,它们得到的结果类型都是 size_t,即 unsigned int 类型,sizeof 计算的是变量的大小不受
\0
的影响,而 strlen 计算的是字符串的长度以\0
为结束标志。 'a'
表示一个字符,"a"
表示一个字符串相当于'a'+'\0'
,''
里面只能放一个字符,""
里面表示字符串系统自动会在串末尾补一个\0
。#include <stdio.h> #include <string.h> int main(){ char s1[] = {'H','e','l','l','o'}; char s2[] = {'H','E','L','L','O','\0'}; char s3[] = "hello"; //printf(2);//错误 printf("%d\n",2); printf(s1);//直接输出 printf("\n"); printf(s2); printf("\n"); printf(s3); printf("\n%s\n",s2);// %s 字符串格式输出,%c 字符格式输出 printf("strlen:%d %d %d\n",strlen(s1),strlen(s2),strlen(s3)); printf("sizeof:%d %d %d",sizeof(s1),sizeof(s2),sizeof(s3)); return 0; /*运行结果: 2 Hello HELLO hello HELLO strlen:5 5 5 sizeof:5 6 6 */ }
十七、结构体
- C 数组允许定义可存储相同类型的数据项的变量,结构体是一种自定义的数据类型,允许存储不同类型的数据项。
17.1 定义使用
- 结构体和结构体变量的声明定义
使用 struct 语句定义包含一个或多个成员的结构体语句如下:
tag 是结构体标签名, mem-list 是标准的成员变量定义,比如struct tag { mem-list; mem-list; ... } var-list ;
int i;
或char c;
,var-list 是结构变量,定义在结构的末尾,最后一个分号之前,可以指定一个或多个结构变量。一般情况下,tag 、mem-list、var-list 这三部分至少要出现2个,并且声明一般都是在主函数外。
在上面的声明中,第一个和第二个声明是两个完全的不同的类型,一般使用第二种。结构体的成员可以包含其它结构体,也可包含指向自己结构体类型的指针;如果两个结构体互相包含则对其中一个结构体进行不完整声明,如下//声明无标签名有三个成员的结构体,分别为整型的 age ,字符数组 name,并声明了结构体变量 s1 struct { int age;// char name[20]; } s0; //声明 student 结构体,没有声明变量 struct student { int age;//学生的年龄 char name[20];//学生的姓名 }; //声明 student 结构体变量、数组、指针 struct student s1,s2[20],*s3;/声明变量时一般可省略 struct //用 typedef 创建新类型 typedef struct { int age; char name[20]; } s4 ;
struct B;//不完整声明 struct A { B *p; }; struct B { A *p; }; //在A声明完后,B也随之进行声明
- 结构体变量的初始化
初始化和其它类型变量一样。 - 访问结构体成员
使用访问成员运算符.
访问。例如:s1.age
,结构体变量名加成员运算符再加成员名。#include <stdio.h> #include <string.h> struct student { int age; char name[20]; } s0 = {20,"小明"};//在声明时初始化,一般不这样初始化 int main(){ student s1;//一般结构体变量的声明 s1.age = 19;//一般结构体变量成员的初始化 strcpy(s1.name,"小飞"); printf("学生s1的信息:%d %s",s1.age,s1.name);//访问 return 0; }
17.2 作为函数参数和结构指针
- 结构体变量作为参数
可以把结构体变量作为函数参数,传参数的方式和其它类型变量或指针类似。例如:
函数的声明定义要放在所使用结构体定义的后面。void printstuInfo (student st) { printf("学生信息:年龄:%d 姓名: %s",st.age,st.name); }
- 指向结构的指针
可以定义指向结构的指针,和其它类型变量的指针相似,例如:
完整的例子:student *stup; stup = &s1; stup->age;
#include <stdio.h> #include <string.h> struct student { int age; char name[20]; }; void printstuInfo1 (student st) { printf("学生信息:年龄:%d 姓名: %s\n",st.age,st.name); } void printstuInfo2 (student *sp) { printf("学生信息:年龄:%d 姓名: %s\n",sp->age,(*sp).name);//与 sp->name 等价 } int main(){ student s1,s2; s1.age = 19; strcpy(s1.name,"小飞"); s2.age = 20; strcpy(s2.name,"小明"); printf("学生s1的信息:%d %s\n",s1.age,s1.name); printf("学生s2的信息:%d %s\n",s2.age,s2.name); printstuInfo1(s1); printstuInfo2(&s2); return 0; }
注意: 结构体成员变量分配的空间是按占用空间最大的成员变量的存储空间分配的,它所占内存长度是其中最大字段大小的整数倍。
十八、共用体
- 共用体也叫联合,是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的共用体,但任何时候只能有一个成员带值。提供了一种使用相同内存位置的有效方式。
18.1 定义使用
- 定义共用体
与结构体类似,使用 union 语句定义一个带有多个成员的新数据类型格式如下:
union tag 可选,每个 mem-def 是标准的变量定义,或其它有效变量定义。在共用体的末尾,最后一个分号之前,可以指定一个或多个共用体变量,也是可选的。union [ union tag ] { mem-def; mem-def; ... }[ one or more union vars ] ;
定义了一个名为 Data 的共用体类型,有三个成员, Data 类型的变量可以存储一个整数、浮点数,或一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。共用体占用的内存应足够存储共用体中最大的成员。union Data { int i; float f; char str[20]; } data;
- 访问共用体成员
依然使用成员访问运算符.
来访问,在共用体变量名和要访问的共用体成员之间。#include <stdio.h> #include <string.h> union Data { int i; float f; char str[20]; }; int main(){ Data data;//一般声明共用体变量 data.i = 1;//初始化 data.f = 2.5; strcpy(data.str,"Hello"); printf("data.i:%d\n",data.i);//访问 printf("data.f:%f\n",data.f); printf("data.str:%s\n",data.str); //这样输出时共用体成员值有损坏,因为最后赋值的变量的值占用了内存位置,最后一个成员 str 能完好输出 data.i = 1; printf("data.i:%d\n",data.i); data.f = 2.5; printf("data.f%f\n",data.f); strcpy(data.str,"Hello"); printf("data.str:%s\n",data.str); //同一时间只使用一个变量,所有的成员都能完好输出,因为同一时间只用到一个成员 return 0; }
注意: 共用体变量所占内存长度一般情况下等于最长成员变量的长度。(也可是次长成员变量的整数倍)
十九、typedef
- 使用 typedef 关键字来为类型取一个新的名字。
19.1 typedef 关键字的使用
- 例如:
typedef unsigned char BYTE;
是为单字节数字定义了一个术语 BYTE :在这个类型定义之后,标识符 BYTE 可作类型unsigned char
的象征性的缩写,例如:BYTE b1,b2;
一般定义时大写字母。 - 也可以用 typedef 来为自定义的数据类型取一个新的名字。例如:可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构体变量,如下:
#include <stdio.h> typedef struct Books { int price; } BOOK; int main(){ BOOK b1; b1.price = 20; printf("书的价格:%d",b1.price); return 0; }
19.2 与 #define 的区别
- #define 是 C 指令,用于各种数据类型定义别名,与 typedef 类似,但又有不同。
- typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,例如定义1为 ONE:
#include <stdio.h> #define ONE 1//一般f放在主函数外 int main(){ printf("ONE的值:%d",ONE); return 0; }
- tyepdef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
- typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,例如定义1为 ONE:
二十、输入和输出
- 输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函数来读取给定的输入和输出。
20.1 标准文件
-
C 语言把所有的设备都当作文件(头文件,把这些文件放在程序开头)。所以设备(如显示器)被处理的方式与文件相同。以下的文件都是在程序执行时自动打开,以便访问键盘和屏幕。
标准文件 文件指针 设备 标准输入 stdin 键盘 标准输出 stdout 屏幕 文件指针是访问文件的方式。
-
C 语言中的 I/O (输入/输出)通常使用
printf()
和scanf()
两个函数,scanf()
函数用于从标准输入(键盘)读取并格式化,printf()
函数发送格式化输出到标准输出(屏幕)。它们都在“stdio.h
”头文件中声明。“stdio.h
”是一个标准输入输出头文件,#include
是一个 预处理命令,用来引入头文件。#include <stdio.h> int main() { int a = 1; printf("a=%d",a);// %d 格式化输出整数,在 printf() 函数的引号中使用 “%d”(整型)来匹配整型变量 a 并输出到屏幕 float b; scanf("%f",&b); printf("输入的数浮点数为:%f",b);// %f 格式化输出浮点型数据 retuen 0; }
20.2 其它有关常用函数
-
getchar() & putchar() 函数
int getchar()
函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一时间只会读取一个单一的字符,可以在循环中使用从屏幕上读取多个字符;
int putchar(int c)
函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一时间只能输出一个单一的字符,可以利用循环在屏幕 上输出多个字符。 -
gets() & puts() 函数
char *gets(char *s)
函数从stdin
读取 一行到s
所指向的缓冲区,直到一个终止符或EOF
;
int puts(const char *s)
函数把字符串s
和一个尾随的换行符写入到stdout
。 -
scanf() & printf() 函数
int scanf(const char "format,...")
函数从标准输入流stdin
读取输入,并根据提供的format
来浏览输入;
int printf(const char "format,...")
函数把输出写入到标准输出流stdout
,并根据提供的格式产生格式化输出。
format
可以是一个简单的常量字符串,但可以分别指定%s、%d、%c、%f
等来输出或读取字符串、整数、字符和浮点数,还有其它很多可用的格式选项。-
C 语言中每种数据类型的输出都有对应的占位符,下面是各种基本数据类型的输出占位符:
short/int:%d
long:%ld
long long:%lld
float/double:%f
/%lf
(float
默认是6位小数输出,用%m.nf
指定数据宽度和小数位数,%.2f
输出两位小数;用%-m.nf
指定输出的数据向左对齐,与%m.nf
的形式作用基本相同,但当数据长度不超过m时, 数据向左靠,右边补空格)。
注: C 语言中printf
输出double
和float
都可以用%f
占位符混用,而double
可以额外用%lf
; 但scanf
输入时double
必须用%lf
,float
必须用%f
不能混用。double a = 1.0; printf("%20.15f",a/3);//指定输出的数据占位20列,包括15位小数,双精度数只保证15位有效数字的准确性 //运行结果:// 0.333333333333333//在0的前面还有3个空格
unsigned:
%u
(signed
:有符号类型,unsigned
:无符号类型;默认都是有符号的)
八进制:%o
以0开头
十六进制:%x
以0x开头
char:%c
char *s:%s
地址值或指针值 :%p
,*
取指针里地址指向的地方的值,&
取改值存储位置的地址值。二进制的输出没有占位符,只能通过其它方法。 -
注:
scanf()
期待输入的格式与自己给出的%s、%d
等格式匹配相同,所以你必须提供有效的输入;在读取字符串时,只要遇到一个空格,scanf()
就会停止读取,所以"I am C"
对scanf()
;来说 是三个字符串。int i; char c; scanf("%d%s",&i,&c);
这时候变量 c 中存储的不是你想要的字符,而是一个空格 ,然后改成如下:
scanf("%d",&i); scanf("%c",&c);
而这时候直接没有输入字符 c 的机会,因为输入流是有缓冲区的。我们输入的字符串存储在那,然后再赋值给我们的变量。可以这样改:
scanf(%d".&i); while((c==getchar())==' '||c=='\n'); c = getchar();
这个办法是一直读取,直到不是空格或换行就跳出循环,有一个更好的办法是用正则表达式来控制输入格式为非空格非换行。
各种输入的实例如下:
#include <stdio.h> int main(){ //这四种输入输出的方法不要同时使用运行,单独使用运行 int a;//不是char a; a = getchar(); putchar(a); //输入一些文本并按下回车键时,程序会继续并只会读取一个单一的字符 printf("\n"); char str[100]; gets(str); puts(str); //输入一些文本并按回车键时,程序会继续并读取一整行直到该行结束 char sc[100]; int i; scanf("%s %d",sc,&i); printf("%s %d\n",sc,i); //输入一些文本并按回车键时,程序会继续并读取输入 char c; scanf("a=%dc=%c",&a,&c); printf("a=%d,c=%c",a,c); //输入的时候必须像a=2c=e这样输入,输入的格式要与引号里的匹配 return 0; }
-
二十一、文件读写
- 简单基本的文件读写就是如何创建、打开、关闭文本或二进制文件。一个文件无论它是什么文件,都代表了一系列的字节。C 语言提供了访问顶层的函数和底层(OS)调用来处理存储设备上的文件。
21.1 文件的打开关闭
-
打开文件
使用fopen()
函数来创建一个新的文件或打开一个已有的文件,这个调用会初始化类型FILE
的一个对象,类型FILE
包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:FILE *fopen( const char * filename, const * mode );
其中
filename
是字符串,用来命名文件,如果处理的是文本文件,则访问模式mode
的值可以是以下中任一个:模式 说明 r 打开一个已有文本文件,允许读取该文件 w 打开一个文本文件,允许写入文件。如果文件不存在则创建一个新文件。您的程序会从文件的开头写入内容。如果文件存在则会被清空覆盖,重新写入程序要写入的内容。 a 打开一个文本文件,以追加的方式写入文件。如果文件不存在则创建一个新文件。在这里,程序会在已有的文件内容中追加程序中要写入的内容。 r+ 打开一个文本文件,允许读写该文件。 w+ 打开一个文本文件,允许读写该文件。若文件已经存在则清空文件内容,反之创建一个新文件。 a+ 打开一个文本文件,允许读写文件。若文件不存在则创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 如果处理的是二进制文件,则使用以下的访问模式取代上面的访问模式:
"rb","wb","ab","rb+","r+b","wb+","w+b","ab+","a+b"
-
关闭文件
关闭文件使用fclose()
函数,函数原型如下:int fclose( FILE *fp );
如果成功关闭文件,该函数返回零,如果关闭文件时发生错误,返回
EOF
。这个函数实际上会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF
是一个定义在头文件stdio.h
中的常量。
21.2 文件的读写操作
- C 标准库提供了各种函数来按字符或固定长度字符串的形式读写文件。
- 读取文件
下面是从文件中读取单个字符的最简单的函数:int fgetc( FILE * fp );
fgetc()
函数从fp
所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF
。
下面的函数允许从流中读取一个字符串:char *fgets( char *buf, int n, FILE *fp );
fgets()
函数从fp
所指向的输入流中读取n-1
个字符。他会把读取的字符串复制到缓冲区buf
,并在最后追加一个null
字符来终止字符串。
如果这个函数在读取最后一个字符之前就遇到一个换行符'\n'
或文件的末尾EOF
则会返回读取到的字符,包括换行符。也可以使用int fscanf( FILE *fp ,const char *format, ... )
函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。 - 写入文件
下面是把字符写入到流中的最简单的函数:int fputc( int c ,FILE *fp );
fputc()
函数把参数c
的字符值写入到fp
所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误则会返回EOF
。
下面是把一个以 null结尾的字符串写入到流中:int fputs( const char *s, FILE *fp );
fputs()
函数把字符串s
写入到fp
所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误则返回EOF
。也可用int fprintf(FILE *fp, const char *format, ... )
函数把一个字符串写入到文件中。
文本文件读写的实例如下:
写入文件的内容如下:#include <stdio.h> int main(){ //文本文件写入 FILE *fp = NULL;//也可以直接写成 FILE *fp = fopen("D:/tmp/test.txt","w+"); fp = fopen("D:/software/C/Dev-C++/菜鸟教程C代码/test.txt","w+"); //D:/software/C/Dev-C++/菜鸟教程C代码/test.txt, test.txt文件的存储目录,若不存在则在此目录下新建一个test.txt文件 fputc('C',fp); fputs("This is testing for fputs ... \n",fp); fprintf(fp,"This is testing for fprintf ... \n"); fclose(fp); //文本文件读取 char cc,buff[255]; fp = fopen("D:/software/C/Dev-C++/菜鸟教程C代码/test.txt","r"); cc = fgetc(fp); printf("1.fgetc: %c\n",cc); fgets(buff,255,fp); printf("2.fgets: %s\n",buff); fscanf(fp,"%s",buff); printf("3.fscanf: %s\n",buff); fgets(buff,255,fp); printf("4.fgets: %s\n",buff); return 0; }
读取文件的显示如下:CThis is testing for fputs ... This is testing for fprintf ...
1.fgetc: C 2.fgets: This is testing for fputs ... 3.fscanf: This 4.fgets: is testing for fprintf ...
- 二进制 I/O 函数
下面是两个用于二进制输入和输出的函数:
这两个函数都是用于存储块的读写,通常是数组或结构体。size_t fread( void *ptr, size_t size_of_elements, size_t number_of__elements, FILE *a_file ); size_t fwrite( const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file );
二十二、预处理器
22.1 C 预处理器
-
C 预处理器不是编译器的组成部分,但它是编译过程中一个单独的步骤。简言之它是一个文本替换工具而已,指示编译器在实际编译之前完成所需的预处理。C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始,下面是所有重要的预处理器指令:
指令 说明 #include 包含一个源代码文件 #define 定义宏 #undef 取消已定义的宏 #ifdef 如果宏已经定义,则返回真 #ifndef 如果宏没有定义,则返回真 #if 如果给定条件为真,则编译下面代码 #else #if 的替代方案 #elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 #endif 结束一个 #if…#else 条件编译块 #error 当遇到标准错误时,输出错误信息 #pragma 使用标准化方法向编译器发布特殊的命令到编译器中 -
实例
下面是使用
#define
定义常量来增强可读性:#define MAX_ARRAY_LENGTH 20
这个指令告诉 CPP 把所有的
MAX_ARRAY_LENGTH
替换为20。#include <stdio.h> #include <myheader.h>
第一行的指令告诉 CPP 从系统库中获取
stdio.h
,并添加文本到当前的源文件中。第二行的指令告诉 CPP 从本地目录中获取myheader.h
文件,并添加其内容到当前的原文件中。#undef FILE_SIZE #define FILE_SIZE 20
这个指令告诉 CPP 取消已定义的
FILE_SIZE
,并重新定义它为20。#ifndef MESSAGE #define MESSAGE "Hi!" #endif
这个指令告诉 CPP 只有
MESSAGE
未定义时间才定义它。
22.2 预定义宏和预处理器运算符
- 预定义宏
ANSI C 定义了许多宏。在编程中可以使用这些宏,但不能直接修改这些预定义的宏。预定义宏 说明 __ DATE __ 当前日期,一个以“MMM DD YYYY”格式表示的字符常量 __ TIME __ 当前时间,一个以“HH:MM:SS”格式表示的字符常量 __ FILE __ 包含当前文件名,一个字符串常量 __ LINE __ 包含当前行号,一个十进制常量 __ STDC __ 当编译器以 ANSI 标准编译时,则定义为1
运行结果如下:#include <stdio.h> int main(){ printf("File : %s\n",__FILE__); printf("DATE : %s\n",__DATE__); printf("TIME : %s\n",__TIME__); printf("LINE : %d\n",__LINE__); printf("ANSI : %d\n",__STDC__); return 0; }
File : D:\software\C\Dev-C++\菜鸟教程C代码 \code10.c DATE : Aug 20 2021 TIME : 23:28:04 LINE : 8 ANSI : 1
- 预处理运算符
C 预处理器提供了一些运算符来帮助创建宏如:
宏延续运算符(\): 一个宏通常写在一个单行上。但是如果宏太长,一各单行容不下,则使用该运算符。
字符串常量化运算符(#): 在宏定义中,当需要把一个宏的参数转换为字符常量时,则使用该运算符。它在宏中使用还要有一个特定的参数或参数列表。
运行结果如下:#include <stdio.h> #define message_for(a,b) \ printf(#a " and " #b ": Hi!\n") int main(){ message_for(aa,bb); return 0; }
标记粘贴运算符(##): 宏定义的内使用该运算符会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。aa and bb: Hi!
defined() 运算符: 预处理器defined
运算符是用在常量表达式中,用来确定一个标识符是否已经使用#define
定义过。如果已定义,则值为真(非零)。反之值为假(零)。 - 参数化的宏
CPP 一个强大的功能是可以使用参数化的宏来模拟函数。在使用带有参数的宏之前,必须使用#define
指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:
使用宏重写上面的代码如下:int square(int x){ return x*x; }
#define square(x) ( (x) * (x) )
二十三、头文件
- 头文件是扩展名为
.h
的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的文件:程序员自己编写的头文件和编译器自带的头文件。 - 建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要时引用这些头文件即可。
23.1 引用头文件
-
引用头文件相当于复制头文件的内容,但不会直接在源文件中复制头文件的内容,因为这么做易错,尤其在程序由多个源文件组成时。
-
引用头文件的语法
在程序中使用头文件,要使用 C 预处理指令#include
来引用用户和系统头文件,如编译器自带的stdio.h
头文件。引用的形式以下两种:#include <file>//用于引用系统头文件 #include "file"//用于引用用户头文件,而且是程序目录的相对路径中的头文件
-
引用头文件的操作
#include
指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及#include
指令之后的文本输出。例如有一个头文件
header.h
和一个使用了这个头文件的主程序如下:#include<stdio.h> int x; #include "header.h" //如果 header.h 头文件中只有 cout<<endl; //编译器浏览到这句头文件的引用时看到的代码信息就为 cout<<endl; int main(){ cout<<x; return 0; }
-
只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误,正确的做法是把文件的整个内容放在条件编译语句(用预处理指令)中。 -
有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。可以通过预处理指令加一系列的条件来实现,但是头文件较多时,就直接用宏名称代替即可。
二十四、强制类型转换
- 强制类型转换是把变量从一种类型转换为另一种数据类型。
24.1 强制类型转换
- 使用强制类型转换运算符把变量的值显式地从一种类型转换成另一种类型。如:
(type_name) expression
- 如下实例,整数除以整数得到的是整数,强制转换类型得到一个浮点数:
强制类型转换的运算符的优先级大于除法,因此int a = 1,b = 2; double c = (double ) a/b;//强制类型转换
a
的值先被转换成double
型,然后除以b
,得到一个double
型的值。 - 类型转换可以是隐式的,由编译器自动执行;也可以是显式的,通过使用强制类型转换运算符来指定。一般情况下需要类型转换时建议用强制类型转换运算符。
- 整数提升:指把小于
int
或unsigned int
的整数类型转换为int
或unsigned int
的过程。如下实例,在int 中添加一个字符:int a = 1; char b = 'c';// 'c' 的 ascii 值为99 int c = a+b;//隐式强制类型转换
c
的最后输出的值为100
,因为编译器进行了整数提升,在执行实际加法运算时,把'c'
的值转换为对应的ascii
值。
24.2 常用的算术转换
- 常用的算术是隐式地把变量的值强制转换为相同的类型。编译器先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:char , short
->
int->
unsigned int->
long->
unsigned long->
long long->
unsigned long long->
float->
double->
long double 。 - 如果一个运算符两边的运算数类型不同,先要将其转换为相同的类型,即较低类型转换为较高类型,然后再参加运算,转换规则如上。
- 常用的算术转换不适用赋值运算符、逻辑运算符
&&
和||
。如下实例是整型和字符型相加得到浮点型:int a=1; char b = 'c'; float c = a+b;//隐式强制类型转换
c
最后输出的值为100.000000
,b
先被转换为int
型,但由于最后的值是float
型,所以用到常用算术转换,编译器把a
和b
转换为float
型,并相加得到一个浮点数。
二十五、错误处理
- C 语言不提供错误处理的直接支持,但作为一种系统编程语言,它以返回值的形式允许宁访问底层数据。在发生错误时,大多数的
C
或UNIX
函数调用返回1或NULL,同时会设置一个错误代码errno
,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在errno.h
头文件中找到各种各样的错误代码。 - 我们可以通过检查返回值,根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把
errno
设置为 0,这是一个良好的编程习惯。0值表示程序中没有错误。
25.1 C 语言中错误处理的简单介绍
-
perror()、strerror()函数
C语言提供了perror()
和strerror()
函数来显示与errno
相关的文本消息。perror()
函数显示我们传给它的字符串,后面跟一个冒号、一个空格和当前的errno
值的文本表示形式。strerror()
函数返回一个指针,指向当前errno
值的文本表示形式。- 应该使用
stderr
文件流来输出所有的错误。
-
被零除的错误
- 在进行除法运算前应该先检查除数是否为零,不然会导致一个运行时错误。
运行结果如下:int a = 20; int b = 0; if(b==0){ fprintf(stderr,"除数为0退出运行...、\n"); exit(-1); } int c = a/b; fprintf(stderr,"b 变量的值为:%d\n",b); exit(0);
除数为0退出状态...
- 在进行除法运算前应该先检查除数是否为零,不然会导致一个运行时错误。
-
程序退出状态
- 通常情况下,程序执行完一个操作正常退出时会带有值
EXIT_SUCCESS
,它是一个宏被定义为0。如果程序中存在一些错误情况,当您退出程序时,会带有状态值EXIT_FAILURE
,被定义为-1。
运行结果如下:int a = 20; int b = 5; if(b==0){ fprintf(stderr,"除数为0退出运行...、\n"); exit(EXIT_FAILURE);//exit(-1); } int c = a/b; fprintf(stderr,"b 变量的值为 :%d\n",b); exit(EXIT_SUCCESS);//exit(0);
b 变量的值为 : 4
- 通常情况下,程序执行完一个操作正常退出时会带有值
二十六、递归
- 递归是在函数的定义中使用调用函数自身的方法。
- 语法格式如下:
void fun(){//函数定义 ... fun();//函数调用自身 ... } int main(){ fun();//主函数中调用与其它函数一致 return 0; }
- 递归函数在一些数学问题上特别方便,使用递归时要注意函数退出的条件即递归的出口,不然会进入死循环,还有使用递归与直接的语句(例如while循环)相比会耗费更多的运行时间,并且要占用大量的栈空间。
- 如下是使用递归函数计算一个数的阶乘和生成给定一个数的斐波那契数列:
#include <stdio.h> int fact(unsigned int i){//数的阶乘 if(i <= 1){ return 1; } return i * ( fact(i-1) ); } int fibonaci(int i){//斐波那契数列 if(i == 0){ return 0; } if(i == 1){ return 1; } return fibonaci(i-1) + fibonaci(i-2); } int main(){ int a = 6; printf("%d的阶乘为%d\n",a,fact(a) ); for(int i = 0;i < a;i++){ printf("%d\t\n",fibonaci(i) ); } return 0; }
二十七、可变参数
- 当函数带有可变数量的参数而不是预定义的参数时,C 语言提供了可变参数,允许定义一个函数,根据具体的需要接受可变数量的参数。例如:
#include <stdarg.h>//所需要的头文件 int func(int, ... ){ ... ... ... } int main(){ func(2,1,2);//两个可变参数 func(3,1,2,3);//三个可变参数 return 0; }
- 函数定义时最后一个参数写成省略号即三个点
( ... )
号,省略号之前的那个自定义参数是int
,代表要传递的可变参数的总数。还要加stdarg.h
头文件,该文件提供了实现可变参数功能的函数和宏。 - 具体实现的步骤看如下实例:
#include <stdio.h> #include <stdarg.h> double average(int n,...){ //首先创建一个 va_list 类型的变量,该类型在 stdarg.h 头文件中定义的 va_list valist; double sum = 0.0; //用 stdarg.h 头文件中定义的宏 va_start 为 n 个参数初始化 va_list变量为一个参数列表 va_start(valist,n); //用 va_arg 宏访问所有赋给 valist 的参数即参数列表中的每个项 for(int i = 0;i < n;i++){ sum += va_arg(valist,int); } //用宏 va_end 清理赋予 valist 变量的内存 va_end(valist); return sum/n; } int main(){ printf("average(4,1,2,3,4) = %f\n",average(4,1,2,3,4)); printf("average(3,1,2,3) = %f\n",average(3,1,2,3)); return 0; }
二十八、内存管理
-
C 语言中的动态内存管理,为内存的分配和管理提供了几个函数。这些函数都包含在
stdlib.h
头文件中。 -
void calloc(int num,int size);
在内存中动态地分配num
个长度为size
的连续空间,并将每一个字节都初始化为0。它结果是分配了num*size
个字节长度的内存空间,并且每个字节的值都是0。 -
void free(void * address);
该函数释放address
所指向的内存块,释放的是动态分配的内存空间。 -
void * malloc(int num);
在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。 -
void * realloc(void * address,int newsize);
该函数重新分配内存,把内存扩展到newsize
。注:
void *
类型表示未确定类型的指针。C/C++规定void *
类型通过类型转换强制转换为其它任何类型的指针。 -
动态分配内存
编程时如果预先不知道需要存储的文本长度,则需要定义一个指针,该指针指向未定义所需内存大小的字符,后面再根据需要来分配内存。例如:char *pd = (char * )malloc(200*sizeof(char)); char *pd = (char * )calloc(200,sizeof(char));
-
重新调整内存的大小和释放内存
当程序退出时,操作系统会自动释放所有分配给程序的内存,但建议在不需要内存时,都应调用free()
函数来释放内存。或者可通过调用函数realloc()
来增加或减少已分配的内存块的大小。用上面的例子:pd = (char *) realloc(dp,300*sizeof(char));//想存储更大的信息用realloc()函数改变内存大小 free(pd);//用free()函数来释放内存
注: 对于手动分配的内存在C 语言中是不用强制类型转换的,但C++是强制要求的,不然会报错。如:
pd = malloc(200*sizeof(char));//C语言正确,C++错误
二十九、命令行参数
-
执行程序时,可以从命令行传值给程序。这些值别成为命令行参数。一般在从外部控制程序中很重要。它是使用
mian()
函数参数来处理的,其中argc
是指传入的参数的个数,argv[]
是一个指针数组,指向传递给程序的每一个参数。 -
注:
argv[0]
存储的是程序的名称,argv[1]
存储的是一个指向第一个命令行参数的指针,*argv[n]
是最后一个参数。如果没有任何参数,argc
将为1。多个命令行参数之间用空格分开隔,但是如果参数本身带有空格,则传递参数时应把参数放置在双引号
""
或单引号''
内。(Dev C++中不知道为什么不能实现,好像只是按空格划分,双引号没有作用也不显示,单引号也没有作用但能显示) -
例如:
#include <stdio.h> int main(int argc,char *argv[]){//也可以写成其它的名字 printf("argc = %d\n",argc); for(int i = 0;i < argc;i++){ printf("argv[%d] = %s\n",i,argv[i]); } return 0; }
传递给主程序的参数:
123 hi C
运行结果:
argc = 4 argv[0] = D:\software\C\Dev-C++\菜鸟教程C代码\code13.exe argv[1] = 123 argv[2] = hi argv[3] = C
三十、C标准库
- C标准库是一组 C 内置函数、常量和头文件。它可以作为编程人员的参考手册。
30.1 几个基础常用的C标准库中的头文件简单介绍
-
1.math.h
-
math.h头文件定义了各种数学函数和一个宏。在这库中所有可用的功能都带有一个
double
类型的参数,而且都返回double
类型的结果。 -
math.h 中的库函数
函数 功能 double acos(double x) 返回以弧度表示的 x 的反余弦。 double asin(double x) 返回以弧度表示的 x 的反正弦。 double atan(double x) 返回以弧度表示的 x 的反正切。 double cos(double x) 返回弧度角 x 的余弦。 double sin(double x) 返回弧度角 x 的正弦。 double tan(double x) 返回弧度角 x 的正切。 double exp(double x) 返回 e 的 x 次幂。 double log(double x) 返回 x 的自然对数(基数为 e)。 double log10(double x) 返回 x 的常用对数(基数为10)。 double pow(double x) 返回 x 的 y 次幂。 double sqrt(double x) 返回 x 的平方根。 double fabs(double x) 返回 x 的绝对值。 double ceil(double x) 返回大于或等于 x 的最小的整数值。 double floor(double x) 返回小于或等于 x 的最大的整数值。
-
-
2.stdio.h
-
stdio.h 头文件中定义了三个变量类型、一些宏和各种函数来这行输入输出。
-
stdio.h 中的库变量
变量 描述 size_t 无符号整数类型,是 sizeof 关键字的结果。 FILE 存储文件流信息的对象类型。 fpos_t 存储文件中任何位置的对象类型。 -
stdio.h 中的库函数
函数 功能 int fclose(FILE *stream) 关闭流 stream,刷新所有的缓冲区。在这个函数中创建的文件都会在后续的函数中使用。 FILE *fopen(const char *filename,const char *mode) 使用给定模式 mode 打开 filename 所指向的文件。 int remove(const char *filename) 删除给定文件名 filename ,以便它不能再被访问。 int rename(const char *oid_filename,const char *new_filename) 把 old_filename 所指向的文件名改为 new_filename。 int scanf(const char *format,…) 从标准输入 stdin 读取格式化输入。 int printf(const char *format,…) 发送格式化输出到标准输出 stdout。 int fgetc(FILE *stream) 从指定流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。 char *fgets(char *str,int n,FILE *stream) 从指定流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取(n-1)个字符时,或读取到换行符时,或到达文件末尾时,它就会停止。 int fputc(int char,FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定流 stream 中,并把位置标识符往前移动。 int getc(FILE *stream) 从指定流 stream 获取下一个字符(一个无符号字符)。 int getchar(void) 从标准 stdin 获取一个字符(一个无符号字符)。 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或到达文件末尾时,它就会停止。 int putc(int char,FILE *stream) 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。 int putchar(int char) 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中。 int puts(const char *str) 把一个字符串写入到标准输出 stdout,直到空字符(’\0’,C/C++中字符串结束符),但不包含空字符。换行符会追加到输出中。
-
-
3.stdlib.h
-
stdlib.h 头文件定义了四个变量类型、一些宏和各种通用工具函数。
-
stlib.h 中的库函数
函数 功能 double atof(const char *str) 把参数 str 指向的字符串转换为一个 double 类型的浮点数。 int atoi(const char *str) 把参数 str 指向的字符串转换为一个 int 类型的整数。 long int atol(const char *str) 把参数 str 指向的字符串转换为一个 long int 型的长整数。 double strtod(const char *str,char **endptr) 把参数 str 指向的字符串转换为一个 double 型浮点数 unsingned long int strtoul(const char *str,char **endptr,int base) 把参数 str 所指向的字符串转换为一个无符号长整数。 void *calloc(size_t nitems,size_t size) 分配所需的内存空间,并返回一个指向它的指针。 void *malloc(size_t size) 分配所需的内存空间,并返回一个指向它的指针。 void *realloc(void *ptr,size_t size) 重新调整之前调用 calloc、malloc 所分配的 ptr 所指向的内存块的大小。 void free(void *ptr) 释放之前调用 calloc、malloc、realloc所分配的内存空间。 int system(const char *string) 由 string 指定的命令传给要被命令处理器执行的主机环境。 int abs(int x) 返回 x 的绝对值。 long int labs(long int x) 返回 x 的绝对值。long int 和 long 其实是一样的。 如下是数字转换为字符串的几个非标准函数(不是标准定义下的 C 语言):
函数 功能 char *itoa( int value, char *string,int radix) 把一个整数转换为字符串。把 value 转换成 radix 进制的数并以字符串形式存储在 string 所指向的字符串中 char *ltoa(long value,char *string,int radix) 长整型转换为字符串。函数参数如上。 char *ultoa(unsigned long value,char *string,int radix) 无符号长整型转换为字符串。函数参数如上。 char *gcvt(double value,size_t ndigs,char *buf) 浮点数转换为字符串,包含小数点。value 要转换的数,ndigs 表示显示的位数(value 中的数字个数超过 ndig。低位数字被舍入,反之字符串后面补0),*buf 指向转换完成后 ASCII 码字符串。 char *ecvt(double value,int ndig,int *dec,int *sign) 把双精度浮点数转换为字符串,转换结果中不包含十进制小数点。value 要转换的双精度浮点数,ndig 表示有效数字位数(value 中的数字个数超过 ndig。低位数字被舍入,反之字符串后面补0),*dec 表示小数点位置,*sign 转换的数的符号(0正1负)。 char *fcvt(double value,int ndig,int *dec,int *sign) 把浮点数转换字符串,不包含小数点。value 要转换的浮点数,ndig 是小数点后面的位数(value 中的小数点后面的数字个数超过 ndig。低位数字被舍入,反之字符串后面补0),*dec 表示小数点的位置,*sign 转换的数的符号(0正1负)。
-
-
4.string.h
-
string.h 头文件定义一个变量类型(size_t,同上)、一个宏(NULL,一个空指针常量的值)和各种操作字符数组的函数。
-
string.h 中的库函数
函数 功能 void *memchr(const void *str,int c,size_t n) 在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。 int memcmp(const void *str1,const void *str2,size_t n) 把 str1 和 str2 的前 n 个字节进行比较。 void *memcpy(void *dest,const void *src,size_t n) 从 src 复制 n 个字符到 dest. void *memset(void *str,int c,size_t n) 复制字符 c (一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。 char *strcat(char *dest,const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。 char *strncat(char *dest,const char *src,size_t n) 把 src 指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。 char *strchr(const char *str,int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c (一个无符号字符)的位置。 int strcmp(const char *str1,const char *str2) 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。返回值大于0,表示 str1 大于 str2 ;返回值小于0,表示 str1 小于 str2;返回值等于0,表示 str1 等于 str2。 int strncmp(const char *str1,const char *str2,size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节。返回值同上。 char *strcpy(char *dest,const char *src) 把 src 所指向的字符串复制到 dest 。 char *strncpy(char *dest,const char *src,size_t n) 把 src所指向的字符串复制到 dest ,最多复制 n 个字符。 size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包含空字符。 char *strrchr(const char *str,int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c (一个无符号字符)的位置。 char *strstr(const char *str1,const char *str2) 在字符串 str1 中查找第一次出现字符串 str2 (不包含空结束字符)的位置。 char *strtok(char *str,const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。
-