C 语言入门
C语言是一种通用的、面向过程的计算机程序设计语言。1972年,为了移植与开发UNIX操作系统,丹尼斯·里奇在贝尔电话实验室开发了C语言。
C语言是一种广泛使用的计算机语言,它与Java编程语言一样普及,二者在现代软件程序之间都得到广泛使用。
当前最新的C语言标准为C18,在它之前的C语言标准有C17、C11...C99等。
# C 简介
C语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验室为开发UNIX操作系统而设计的。C语言最开始是于1972年在DEC PDP-11 计算机上被首次实现。
在1978年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)制作了C的第一个公开可用的描述,现在被称为K&R标准。
UNIX操作系统,C编译器,和几乎所有的UNIX应用程序都是用C语言编写的。由于各种原因,C语言现在已经成为一种广泛使用的专业语言。
- 易于学习
- 结构化语言
- 它产生高效率的程序
- 它可以处理底层的活动
- 它可以在多种计算机平台上编译
# 关于 C
- C语言是为了编写UNIX操作系统而被发明的
- C语言是以B语言为基础的,B语言大概是在1970年被引进的
- C语言标准是于1988年由美国国家标准协会(ANSI,全称American National Standard Institute)制定的
- 截止1973年,UNIX操作系统完全使用C语言编写
- 目前,C语言是最广泛使用的系统程序设计语言
- 大多数先进的软件都是使用C语言实现的
- 当今最流行的Linux操作系统和RDBMS(Relational Database Management System: 关系数据库管理系统) MySQL都是使用C语言编写的
# 为什么使用 C?
C语言最初是用于系统开发工作,特别是组成操作系统的程序。由于C语言所产生的代码运行速度与汇编语言编写的代码运行速度几乎一样,所以采用C语言作为系统开发语言。如:
- 操作系统
- 语言编译器
- 汇编器
- 文本编辑器
- 打印机
- 网络驱动器
- 现代程序
- 数据库
- 语言解释器
- 实体工具
# C 程序
一个C语言程序,可以是3行,也可以是数百万行,其可以写在一个或多个扩展名为.c
的文本文件中,如,hello.c
,可以使用vi,vim或任何其他文本编辑器来编写C语言程序。
# C11 新特性
C11(也被称为C1X),指ISO标准ISO/IEC 9899:2011。在它之前的C语言标准为C99。
- 对齐处理(
Alignment
)的标准化(包括_Alignas
标志符,alignof
运算符,aligned_alloc
函数以及<stdalign.h>
头文件) _Noreturn
函数标记,类似于gcc的__attribute__((noreturn))
_Generic
关键字- 多线程(
Multithreading
)支持,包括:_Thread_local
存储类型标识符,<threads.h>
头文件,里面包含了线程的创建和管理函数_Atomic
类型修饰符和<stdatomic.h>
头文件
- 增强的Unicode的支持,基于C Unicode技术报告ISO/IEC TR 19769:2004,增强了对Unicode的支持。包括为UTF-16/UTF-32编码增加了char16_t和char32_t数据类型,提供了包含unicode字符串转换函数的头文件
<uchar.h>
- 删除了
gets()
函数,使用一个新的更安全的函数gets_s()
替代 - 增加了边界检查函数接口,定义了新的安全的函数,例如
fopen_s()
,strcat_s()
等等 - 增加了更多浮点处理宏
- 匿名结构体/联合体支持,这个在gcc早已存在,C11将其引入标准
- 静态断言(
Static assertions
),_Static_assert()
,在解释#if
和#error
之后被处理 - 新的
fopen()
模式,("...x")
,类似POSIX中的O_CREAT|O_EXCL
,在文件锁中比较常用 - 新增
quick_exit()
函数作为第三种终止程序的方式,当exit()
失败时可以做最少的清理工作
# C 环境设置
如果希望设置C语言环境,需要确保电脑上有两款可用的软件,文本编辑器和C编译器
# 文本编辑器
Windows Notepad、OS Edit Command、Brief、Epsilon、EMACS和Vi/Vim
通过编辑器创建的文件通常称为源文件,源文件包含程序源代码。C程序的源文件通常使用扩展名.c
# C 编译器
写在源文件中的源代码是人类可读的源,需要编译,转为机器语言,这样CPU可以按给定指令执行程序。C语言编译器用于把源代码编译成最终可执行程序。最常用的免费可用的编译器是GNU的*C/C++*编译器。
如果使用Linux或UNIX,可以通过命令检查是否安装了GCC:
gcc -v
如果未安装GCC,那么请按照http://gcc.gnu.org/install/上的详细说明安装GCC。
如果使用的是Mac OS X,最快捷的获取GCC的方法是从苹果的网站上下载Xcode开发环境,并按照安装说明进行安装。
如果是Windows,需要安装MinGW,可以访问MinGW的主页mingw-w64.org,进入下载页面,下载最新版本。当安装MinGW时,至少要安装gcc-core、gcc-g++、binutils和MinGW runtime,但是一般情况下都会安装更多其他的项。安装后,添加MinGW的bin目录到PATH
环境变量中,这样就可以在命令行中通过简单的名称来指定这些工具。
# C 程序结构
# C Hello World 实例
C程序主要包括以下部分:
- 预处理器指令
- 函数
- 变量
- 语句 & 表达式
- 注释
#include <stdio.h>
int main()
{
/* 第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}
2
3
4
5
6
7
8
讲解:
- 第一行,
#include <stdio.h>
是预处理器指令,告诉C编译器在实际编译之前要包含stdio.h
文件 - 第二行,
int main()
是主函数,程序从这里开始执行 - 第三行,
/* ... */
将会被编译器忽略,这里放置程序的注释内容 - 第四行,
printf(...)
是C中一个可用的函数,会在屏幕上输出Hello, World!
- 第五行,
return 0
终止main()
函数,并返回值0
# 编译 & 执行 C 程序
步骤:
- 打开文本编辑器,添加上述代码
- 保存文件为hello.c
- 打开命令提示符,进入到保存文件所在的目录
- 输入
gcc hello.c
,输入回车,编译代码 - 如果没有错误,编译成功会生成a.out(Windows生成a.exe)可执行文件
- 在命令提示符输入
a.out
执行程序 - 可以看到屏幕上显示Hello, World!
如果是多个C代码源文件,编译命令如下:
gcc test1.c test2.c -o main.out
./main.out
2
# C 基本语法
# C 的令牌(Token)
C 程序由各种令牌组成,令牌可以是关键字、标识符、常量、字符串值、或者一个符号。如下,就包含5个令牌:
printf("Hello, World! \n");
分别是:
printf
(
"Hello, World! \n"
)
;
2
3
4
5
# 分号 ;
在C程序中,分号是语句结束符,每个语句必须以分号结束,它表明一个逻辑实体的结束。
# 注释
// 单行注释
/* 单行注释 */
/*
多行注释
多行注释
多行注释
*/
2
3
4
5
6
7
/* */
这种格式的注释可以单行,也可多行。注意,不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。
# 标识符
C标识符用来标识变量、函数、或任何其他用户自定义项目的名称。一个标识符以字母A-Z或a-z或下划线*_*开始,后跟零个或多个字母、下划线和数字(0-9)。
C标识符不允许出现标点符号,如*@、$和%。C是区分大小写的变成语言,因此,在C*中,Manpower
和manpower
是两个不同的标识符。
# 关键字
下表列出了C中的关键字,这些不能作为常量名、变量名或其他标识符名称。
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 定义常量,如果一个变量被const 修饰,那么它的值就不能再被改变 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的其他分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支 |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其他文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句,可以带参数,也可不带 |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度,即所占字节数 |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
C99新增关键字:
_Bool
、_Complex
、_Imaginary
、inline
、restrict
C11新增关键字:
_Alignas
、_Alignof
、_Atomic
、_Generic
、_Noreturn
、_Static_assert
、_Thread_local
# C 中的空格
只包含空格的行被称为空白行,C 编译器会完全忽略它。
在 C 中,空格用于描述空白符、制表符、换行符和注释。空格用于分割语句的各个部分,让编译器识别语句中的某个元素(比如int
)在哪里结束。
int age;
在这里,int
和age
之间必须至少有一个空白字符,这样编译器才能区分它们。
fruit = apples + oranges; // 获取水果的总数
fruit
和=
以及apples
之间的空格不是必须的,但为了增强可读性,可以根据需要适当添加空格。
# C 数据类型
在C语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。
C中的类型可以分为以下几种:
类型 | 描述 |
---|---|
基本数据类型 | 它们是算术类型,包括整型(int )、字符型(char )、浮点型(float )和双精度浮点型(double ) |
枚举类型 | 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量 |
void类型 | 表示没有值的数据类型,通常用于函数返回值 |
派生类型 | 包括数组类型、指针类型和结构体类型 |
数组类型和结构类型统称为聚合类型。函数的类型指的是函数返回值的类型。
# 整数类型
类型 | 存储字节 | 值范围 |
---|---|---|
char | 1字节 | -128~127或0~255 |
unsigned char | 1字节 | 0~255 |
signed char | 1字节 | -128~127 |
int | 2或4字节 | -32768~32767或-2147483648~2147483647 |
unsigned int | 2或4字节 | 0~65535或0~4294967295 |
short | 2字节 | -32768~32767 |
unsigned short | 2字节 | 0~65535 |
long | 4字节 | -2147483648~2147483647 |
unsigned long | 4字节 | 0~4294967295 |
各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。以下列出了32位与64位系统的存储大小的差别
为了得到某个类型或某个变量在特定平台上的准确大小,可以使用sizeof
运算符,表达式sizeof(type)
得到对象或类型的存储字节大小。如:
#include <stdio.h>
#include <limits.h>
int main()
{
printf("int 存储大小: %lu \n", sizeof(int));
return 0;
}
2
3
4
5
6
7
8
%lu
位32位无符号整数,详细说明查看C 库函数 - printf()
当在linux
上编译并执行上面的程序,结果:
int 存储大小: 4
# 浮点类型
下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
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
定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间一级它的范围值:
#include <stdio.h>
#include <float.h>
int main()
{
printf("float 存储最大字节数: %lu \n", sizeof(float));
printf("float 最小值: %E \n", FLT_MIN);
printf("float 最大值: %E \n", FLT_MAX);
printf("精度值: %d \n", FLT_DIG);
return 0;
}
2
3
4
5
6
7
8
9
10
11
%E
为以指数形式输出单、双精度实数,详细说明查看C 库函数 - printf()
# void 类型
void
类型制定没有可用的值,通常用于以下三种情况:
类型 | 描述 |
---|---|
函数返回为空 | C中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。如: void exit(int status); |
函数参数为空 | 函数不接收任何参数,不带参数的函数可以接受一个void 。如: int rand(void); |
指针指向void | 类型为void * 的指针代表对象的地址,而不是类型。如: 内存分配函数void *malloc(size_t size); 返回指向void 的指针,可以转换为任何数据类型 |
# 类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。
C中有两种类型转换:
- 隐式类型转换: 隐式类型转换是将表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,如: 将
int
类型转换为long
或float
转换为double
,隐式类型转换可能会导致数据精度丢失或数据截断。 - 显式类型转换: 显式类型转换需要使用强制类型转换运算符(
type casting operator
),可以将一个数据类型的值强制转换为另一种数据类型的值,强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据精度丢失或截断。
int i = 10;
float f = 3.14;
double d = i + f; // 隐式将int类型转换为double类型
2
3
double d = 3.14159;
int i = (int) d; // 显式将double类型转换为int类型
2
# C 变量
变量其实只不过是程序可操作的存储区的名称。C中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。
变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为C是大小写敏感的。
类型 | 描述 |
---|---|
char | 通常是一个字节(八位), 这是一个整数类型 |
int | 整型, 4个字节, 取值范围-2147483648~2147483647 |
float | 单精度浮点值. 单精度是这样的格式, 1位符号, 8位指数, 23位小数. ![]() |
double | 双精度浮点值. 双精度是1位符号, 11位指数, 52位小数. |
void | 表示类型的缺失 |
C语言允许定义各种其他类型的变量,比如枚举、指针、数组、结构、共用体等。
# C 中的变量定义
变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。变量定义指定一个数据类型,并包含了该类型的一个或多个变量的列表,如下:
type variable_list;
type表示变量的数据类型,可以是整型、浮点型、字符型、指针等,也可以是用户自定义的对象。
variable_list可以由一个或多个变量的名称组成,多个变量之间用逗号,
分隔,变量有字母、数字和下划线组成,且以字母或下划线开头。
下面列出几个有效的声明:
定义整型变量:
int age;
以上代码,age
被定义为一个整型变量。
定义浮点型变量:
float salary;
以上代码中,salary
被定义为一个浮点型变量。
定义字符型变量:
char grade;
以上代码中,grade
被定义为一个字符型变量。
定义指针变量:
int *ptr;
ptr
被定义为一个整型指针变量。
定义多个变量:
int i, j, k;
int i, j, k;
声明并定义了变量i
、j
和k
,这指示编译器创建类型为int
的名为i、j、k
的变量。
# 变量初始化
在C语言中,变量的初始化是在定义变量的同时为其赋予一个初始值。变量的初始化可以在定义时进行,也可以在后续的代码中进行。初始化器由一个等号,后跟一个常量表达式组成,如下:
type variable_name = value;
其中,type
表示变量的数据类型,variable_name
是变量的名称,value
是变量的初始值。
int x = 10; // 整型变量x初始化为10
float pi = 3.14; // 浮点型变量pi初始化为3.14
char ch = 'A'; // 字符型变量ch初始化为'A'
extern int d = 3, f = 5; // d和f的声明与初始化
int d = 3, f = 5; // 定义并初始化d和f
byte z = 22; // 定义并初始化z
2
3
4
5
6
后续初始化变量
在变量定义后的代码中,可以使用赋值运算符=
为变量赋予一个新值。
type variable_name; // 变量定义
variable_name = new_value; // 变量初始化
2
int x; // 整型变量x定义
x = 20; // 变量x初始化20
float pi; // 浮点型变量pi定义
pi = 3.14159; // pi初始化3.14159
char ch; // 字符型ch定义
ch = 'B'; // ch初始化'B'
2
3
4
5
6
注意
变量在使用之前应该被初始化,未初始化的变量的值是未定义的,可能包含任意的垃圾值。因此,为了避免不确定的行为和错误,建议在使用变量之前进行初始化。
# 变量不初始化
在C语言中,如果变量没有显示初始化,那么它的默认值将取决于该变量的类型和其所在的作用域。对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的全局变量),它们的默认初始值为:
- 整型变量(
int
、short
、long
等)默认值为0
- 浮点型变量(
float
、double
等)默认值为0.0
- 字符型变量(
char
)默认为\0
,即空字符 - 指针变量默认为
NULL
,表示指针不指向任何有效的内存地址 - 数组、结构体、联合等复合类型的变量,它们的元素或成员按照相应的规则进行默认初始化,可能包括对元素递归应用默认规则
局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值),因此,在使用局部变量前,应该显式地为其赋予一个初始值。
总结
C语言中变量的默认值取决于其类型和作用域,全局变量和静态变量的默认值为0
,字符型变量的默认值为\0
,指针变量的默认值为NULL
,而局部变量没有默认值,其初始值是未定义的。
# C 中的变量声明
变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有意义,在程序连接时编译器需要实际的变量声明。
变量的声明有两种情况:
- 一种是需要建立存储空间的,如:
int a
在声明的时候就已经建立了存储空间; - 另一种是不需要建立存储空间的,通过使用
extern
关键字声明变量名而不定义它。如:extern int a
其中变量a
可以在别的的文件中定义的。 - 除非有
extern
关键字,否则都是变量的定义。
extern int i; // 声明,不是定义
int i; // 声明,也是定义
2
实例:下面的实例中,变量在头部就已经被声明,但是定义与初始化在主函数内。
#include <stdio.h>
// 函数外定义变量x和y
int x;
int y;
int addtwonum()
{
// 函数内声明变量x和y为外部变量
extern int x;
extern int y;
// 给外部变量(全局变量)x和y赋值
x = 1;
y = 2;
return x + y;
}
int main()
{
int result;
// 调用函数addtwonum
result = addtwonum();
printf("result 为: %d", result);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结果:
如果需要在一个源文件中引用另外一个源文件中定义的变量,我们只需在引用的文件中将变量加上extern
关键字的声明即可。
addtwonum.c
文件代码:
#include <stdio.h>
/* 外部变量声明 */
extern int x;
extern int y;
int addtwonum()
{
return x + y;
}
2
3
4
5
6
7
8
9
test.c
文件代码:
#include <stdio.h>
/* 定义两个全局变量 */
int x = 1;
int y = 2;
int addtwonum();
int main(void)
{
int result;
result = addtwonum();
printf("result 为: %d \n", result);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
gcc addtwonum.c test.c -o main
./main
result 为: 3
2
3
4
# C 中的左值 (Lvalues) 和 右值 (Rvalues)
C 中有两种类型的表达式:
- 左值(lvalue):指向内存位置的表达式被称为左值(
lvalue
)表达式。左值可以出现在赋值号的左边或右边。 - 右值(rvalue):术语(
rvalue
)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。
// 有效
int g = 20;
// 无效
10 = 20;
2
3
4
# C 常量
常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。
常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
常量就像是常规的变量,只不过常量的值在定义后不能进行修改。常量可以直接在代码中使用,也可以通过定义常量来使用。
# 整数常量
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x
或0X
表示十六进制,0
表示八进制,不带前缀则默认表示十进制。
整数常量也可以带一个后缀,后缀是U
和L
的组合,U
表示无符号整数(unsigned
),L
表示长整数(long
)。后缀可以大写也可以小写,U
和L
的顺序任意。
212 /* 合法的 */
215u /* 合法的 */
0xFeeL /* 合法的 */
078 /* 非法的: 8 不是八进制的数字 */
032UU /* 非法的: 不能重复后缀 */
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* 整数 */
30u /* 无符号整数 */
30l /* 长整数 */
30ul /* 无符号长整数 */
2
3
4
5
6
7
8
9
10
11
12
13
# 浮点常量
浮点常量由整数部分、小数点、小数部分和指数部分组成。可以使用小数形式或者指数形式来表示浮点常量。
当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时,必须包含小数点、指数,或同时包含两者。带符号的指数是用e
或E
引入的。
3.14159 /* 合法的 */
314159E-5L /* 合法的 */
510E /* 非法的: 不完整的指数 */
210f /* 非法的: 没有小数或指数 */
.e55 /* 非法的: 缺少整数或分数 */
2
3
4
5
# 字符常量
字符常量是括在单引号中,例如,'x'可以存储在char
类型的简单变量中。
字符常量可以是一个普通的字符(例如'x')、一个转义序列(例如'\t'),或一个通用的字符(例如'\u02C0')。
在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n
)或制表符(\t
)等。下表列出了一些这样的转义序列码:
转义序列 | 含义 |
---|---|
\\ | \ 字符 |
\' | ' 字符 |
\" | " 字符 |
\? | ? 字符 |
\a | 警报铃声 |
\b | 退格键 |
\f | 换页符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ooo | 1~3位的八进制数 |
\xhh... | 一个或多个十六进制数 |
#include <stdio.h>
int main()
{
printf("Hello\tWorld\n\n");
return 0;
}
2
3
4
5
6
7
8
字符常量的ASCII
值可以通过强制类型转换为整数值
char myChar = 'a';
int myAsciiValue = (int) myChar; // 将 myChar 转换为 ASCII 值 97
2
# 字符串常量
字符串字面值或常量是括在双引号""
中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。
可以使用空格做分割符,把一个很长的字符串常量进行分行。下面的实例显示了一些字符串常量,这三种形式所显示的字符串是相同的。
"hello, dear"
"hello, \
dear"
"hello, " "d" "ear"
2
3
4
5
6
7
8
字符串常量在内存中以null
终止符\0
结尾,如:
char myString[] = "Hello, world!"; // 系统对字符串常量自动加一个'\0'
# 定义常量
在 C 中,有两种简单的定义常量的方式:
- 使用
#define
预处理器:#define
可以在程序中定义一个常量,它在编译时会被替换为其对应的值。 - 使用
const
关键字:const
关键字用于声明一个只读变量,即该变量的值不能在程序运行时修改。
#define预处理器:
#define 常量名 常量值
#define PI 3.14159
在程序中使用该常量时,编译器会将所有的PI
替换为3.14159
。
实例:
#include <stdio.h>
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
int main()
{
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const 关键字:
可以使用const
前缀声明指定类型的常量,如下:
const 数据类型 常量名 = 常量值;
下面的代码定义了一个名为MAX_VALUE
的常量:
const int MAX_VALUE = 100;
在程序中使用该常量时,其值将始终为100
,并且不能被修改。
const
声明常量要在一个语句内完成:
实例:
#include <stdio.h>
int main()
{
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
int area;
area = LENGTH * WIDTH;
printf("value of area : %d", area);
printf("%c", NEWLINE);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意
把常量定义为大写字母形式,是一个很好的编程习惯。
#define 与 const的区别:
#define
与const
两种方式都可以用来定义常量,选择那种方式取决于具体的需求和编程习惯。通常情况下,建议使用const
关键字来定义常量,因为它具有类型检查和作用域的优势,而#define
仅进行简单的文本替换,可能会导致一些意外的问题。#define
预处理指令和const
关键字在定义常量时有一些区别:
- 替换机制:
#define
是进行简单的文本替换,而const
是声明一个具有类型的常量。#define
定义的常量在编译时会被直接替换为其对应的值,而const
定义的常量在程序运行时会分配内存,并且具有类型信息。 - 类型检查:
#define
不进行类型检查,因为它只是进行简单的文本替换。而const
定义的常量具有类型信息,编译器可以对其进行类型检查。这可以帮助捕获一些潜在的类型错误。 - 作用域:
#define
定义的常量没有作用域限制,它在定义之后的整个代码中都有效。而const
定义的常量具有块级作用域,只在其定义所在的作用域内有效。 - 调试和符号表:使用
#define
定义的常量在符号表中不会有相应的条目,因为它只是进行文本替换。而使用const
定义的常量会在符号表中有相应的条目,有助于调试和可读性。
# C 存储类
存储类定义C
程序中变量/函数的存储位置、声明周期和作用域。这些说明符放置在它们所修饰的类型之前。下面列出C
程序中可用的存储类:
auto
register
static
extern
# auto 存储类
auto
存储类是所有局部变量默认的存储类。定义在函数中的变量默认为auto
存储类,这意味着它们在函数开始时被创建,在函数结束时被销毁。
{
int mount;
auto int month;
}
2
3
4
上面的实例定义了两个带有相同存储类的变量,auto
只能用在函数内,即auto
只能修饰局部变量。
# register 存储类
register
存储类用于定义存储在寄存器中而不是RAM
中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的&
运算符(因为它没有内存位置)。register
存储类定义存储在寄存器,所以变量的访问速度更快,但是它不能直接取地址,因为它不是存储在RAM
中。在需要频繁访问的变量上使用register
存储类可以提高程序的运行速度。
{
register int miles;
}
2
3
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义register
并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
# static 存储类
static
存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用static
修饰局部变量可以在函数调用之间保持局部变量的值。static
修饰符也可以应用于全局变量,当static
修饰全局变量时,会使变量的作用域限制在声明它的文件内。全局声明的一个static
变量或方法可以被任何函数或方法调用,只要这些方法出现在跟static
变量或方法同一个文件中。静态变量在程序中只被初始化一次,即使函数被调用多次,该变量的值也不会重置。以下实例演示了static
修饰全局变量和局部变量的应用:
#include <stdio.h>
/* 函数声明 */
void func1(void);
static int count = 10; // 全局变量 - static 是默认的
int main()
{
while (count--) {
func1();
}
return 0;
}
void func1(void)
{
/*
* thingy 是 func1 的局部变量 - 只初始化一次
* 每次调用函数 func1 thingy 值不会被重置
*/
static int thingy = 5;
thingy++;
printf(" thingy 为 %d , count 为 %d\n", thingy, count);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
实例中count
作为全局变量可以在函数内使用,thingy
使用static
修饰后,不会在每次调用时重置。
# extern 存储类
extern
存储类用于定义在其他文件中声明的全局变量或函数。当使用extern
关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。extern
存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当使用extern
时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。当有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用extern
来得到已定义的变量或函数的引用。可以这么理解,extern
是用来在另一个文件中声明一个全局变量或函数。如下所示:
第一个文件:c_extern.c:
#include <stdio.h>
int count;
extern void write_extern();
int main() {
count = 5;
write_extern();
}
2
3
4
5
6
7
8
9
第二个文件:c_support.c:
#include <stdio.h>
extern int count;
void write_extern(void) {
printf("count is %d\n", count);
}
2
3
4
5
6
7
在这里,第二个文件中的extern
关键字用于声明已经在第一个文件main.c
中定义的count
。现在,编译这两个文件,如下:
gcc c_support.c c_extern.c -o c_extern
# C 运算符
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C语言内置了丰富的运算符,并提供了以下类型的运算符:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符
# 算术运算符
假设变量A
的值为10
,变量B
的值为20
,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到20 |
- | 把第一个操作数中减去第二个操作数 | A - B 将得到-10 |
* | 把两个操作数相乘 | A * B 将得到200 |
/ | 分子除以分母 | B / A 将得到2 |
% | 取模运算符,整除后的余数 | B % A 将得到0 |
++ | 自增运算符,整数值增加1 | A++ 将得到11 |
-- | 自减运算符,整数值减1 | A-- 将得到9 |
#include <stdio.h>
int main() {
int a = 21;
int b = 10;
int c;
c = a + b;
printf("Line 1 - c 的值是 %d\n", c);
c = a - b;
printf("Line 2 - c 的值是 %d \n", c);
c = a * b;
printf("Line 3 - c 的值是 %d \n", c);
c = a / b;
printf("Line 4 - c 的值是 %d \n", c);
c = a % b;
printf("Line 5 - c 的值是 %d \n", c);
c = a++;
printf("Line 6 - c 的值是 %d \n", c);
c = a--;
printf("LIne 7 - c 的值是 %d \n", c);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
以下实例演示了a++
与++a
的区别:
#include <stdio.h>
int main() {
int c;
int a = 10;
c = a++;
printf("先赋值后运算:\n");
printf("Line 1 - c 的值是 %d\n", c); // 10
printf("Line 2 - a 的值是 %d\n", a); // 11
a = 10;
c = a--;
printf("Line 3 - c 的值是 %d\n", c); // 10
printf("Line 4 - a 的值是 %d\n", a); // 9
printf("先运算后赋值:\n");
a = 10;
c = ++a;
printf("Line 5 - c 的值是 %d\n", c); // 11
printf("Line 6 - a 的值是 %d\n", a); // 11
a = 10;
c = --a;
printf("Line 7 - c 的值是 %d\n", c); // 9
printf("Line 8 - a 的值是 %d\n", a); // 9
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量A
的值为10
,变量B
的值为20
,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真 |
#include <stdio.h>
int main() {
int a = 21;
int b = 10;
int c;
if (a == b) {
printf("Line 1 - a 等于 b\n");
} else {
printf("Line 1 - a 不等于 b \n");
}
if (a < b) {
printf("Line 2 - a 小于 b\n");
} else {
printf("Line 2 - a 不小于 b\n");
}
if (a > b) {
printf("Line 3 - a 大于 b\n");
} else {
printf("Line 3 - a 不大于 b\n");
}
/* 改变 a 和 b 的值*/
a = 5;
b = 20;
if (a <= b) {
printf("Line 4 - a 小于或等于 b\n");
}
if (b >= a) {
printf("Line 5 - b 大于或等于 a\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 逻辑运算符
下表显示了C语言支持的所有关系逻辑运算符。假设变量A
的值为1
,变量B
的值为0
,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符,如果两个操作数都非零,则为真。 | (A && B) 为假 |
|| | 称为逻辑或运算符,如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真 |
! | 称为逻辑非运算符,用来逆转操作数的逻辑状态,如果条件为真则逻辑非运算符将使其为假 | !(A && B) 为真 |
#include <stdio.h>
int main() {
int a = 5;
int b = 20;
int c;
if (a && b) {
printf("Line 1 - 条件为真\n");
}
if (a || b) {
printf("Line 2 - 条件为真\n");
}
/* 改变 a 和 b 的值*/
a = 0;
b = 10;
if (a && b) {
printf("Line 3 - 条件为真\n");
} else {
printf("Line 3 - 条件为假\n");
}
if (!(a && b)) {
printf("Line 4 - 条件为真\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 位运算符
位运算符作用于位,并逐位执行操作。&
、|
和^
的真值表如下:
|p|q|p & q|p | q|p ^ q| |:--|:--|:--|:--|:--| |0|0|0|0|0| |0|1|0|1|1| |1|1|1|1|0| |1|0|0|1|1|
假设如果A = 60, B = 13
,以二进制格式表示,如下:
A = 0011 1100
B = 0000 1101
-----------------
A & B = 0000 1100
A | B = 0011 1101
A ^ B = 0011 0001
~A = 1100 0011
2
3
4
5
6
7
下表显示了C语言支持的位运算符,假设变量A = 60, B = 13
,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 对两个操作数的每一位执行逻辑与操作,如果两个响应的位都为1 ,则结果为1 ,否则为0 。按位与操作,按二进制位进行与运算,规则:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1; | (A & B) 将得到12 ,即为0000 1100 |
| | 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为0 ,则结果为0 ,否则为1 。规则:0 | 0 = 0; 0 | 1 = 1; 1 | 0 = 1; 1 | 1 = 1; | (A | B) 将得到61 ,即为0011 1101 |
^ | 对两个操作数的每一位执行逻辑异或操作,如果两个响应的位值相同,则结果为0 ,否则为1 。规则:0 ^ 0 = 0; 0 ^ 1 = 1; 1 ^ 0 = 1; 1 ^ 1 = 0; | (A ^ B) 将得到49 ,即为0011 0001 |
~ | 对操作数的每一位执行逻辑取反操作,即将每一位的0 变为1 ,1 变为0 。规则:~1 = -2; ~0 = -1; | (~A) 将得到-61 ,即为1100 0011 ,一个有符号二进制数的补码形式 |
<< | 将操作数的所有位向左移动指定的位数,左移n 位相当于乘以2 的n 次方。二进制左移运算符,将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0 ) | A << 2 将得到240 ,即1111 0000 |
>> | 将操作数的所有位向右移动的位数,右移n 位相当于除以2 的n 次方。二进制右移运算符,将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A >> 2 将得到15 ,即0000 1111 |
#include <stdio.h>
int main() {
unsigned int a = 60; /* 60 = 0011 1100 */
unsigned int b = 13; /* 13 = 0000 1101 */
int c = 0;
c = a & b; /* 12 = 0000 1100 */
printf("Line 1 - c 的值是 %d\n", c);
c = a | b; /* 61 = 0011 1101 */
printf("Line 2 - c 的值是 %d\n", c);
c = a ^ b; /* 49 = 0011 0001 */
printf("Line 3 - c 的值是 %d\n", c);
c = ~a; /*-61 = 1100 0011 */
printf("Line 4 - c 的值是 %d\n", c);
c = a << 2; /*240 = 1111 0000 */
printf("Line 5 - c 的值是 %d\n", c);
c = a >> 2; /* 15 = 0000 1111 */
printf("Line 6 - c 的值是 %d\n", c);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 赋值运算符
下表列出了C语言支持的赋值运算符:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋值给左边操作数 | C = A + B 将把A + B 的值赋给C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于C = C / A |
%= | 取模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于C = C ^ 2 |
|= | 按位或且赋值运算符 | C |= 2 等同于C = C | 2 |
#include <stdio.h>
int main()
{
int a = 21;
int c;
c = a;
printf("Line 1 - = 运算符实例,c 的值 = %d\n", c);
c += a;
printf("Line 2 - += 运算符实例,c 的值 = %d\n", c);
c -= a;
printf("Line 3 - -= 运算符实例,c 的值 = %d\n", c);
c *= a;
printf("Line 4 - *= 运算符实例,c 的值 = %d\n", c);
c /= a;
printf("Line 5 - /= 运算符实例,c 的值 = %d\n", c);
c = 200;
c %= a;
printf("Line 6 - %%= 运算符实例,c 的值 = %d\n", c);
c <<= 2;
printf("Line 7 - <<= 运算符实例,c 的值 = %d\n", c);
c >>= 2;
printf("Line 8 - >>= 运算符实例,c 的值 = %d\n", c);
c &= 2;
printf("Line 9 - &= 运算符实例,c 的值 = %d\n", c);
c ^= 2;
printf("Line 10 - ^= 运算符实例,c 的值 = %d\n", c);
c |= 2;
printf("Line 11 - |= 运算符实例,c 的值 = %d\n", c);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 杂项运算符 sizeof & 三元
下表列出了C语言支持的其他一些重要的运算符,包括sizeof
和?
:
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小 | sizeof(a) 将返回4 ,其中a 是整数 |
& | 返回变量的地址 | &a; 将给出变量的实际地址 |
* | 指向一个变量 | *a; 将指向一个变量 |
? : | 条件表达式 | 如果条件为真 ? 则值为X : 否则值为Y |
#include <stdio.h>
int main() {
int a = 4;
short b;
double c;
int* ptr;
/* sizeof 运算符实例 */
printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a));
printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b));
printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c));
/* & 和 * 运算符实例 */
ptr = &a; // 'ptr' 现在包含 'a' 的地址
printf("a 的值是 %d\n", a);
printf("*ptr 是 %d\n", *ptr);
/* 三元运算符实例 */
a = 10;
b = (a == 1) ? 20 : 30;
printf("b 的值是 %d\n", b);
b = (a == 10) ? 20 : 30;
printf("b 的值是 %d\n", b);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# C 中的运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。例如,x = 7 + 3 * 2
,在这里,x
被赋值为13
,而不是20
,因为运算符*
具有比+
更高的优先级,所以首先计算乘法3 * 2
,然后再加上7
。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] - > . ++ -- | 从左到右 |
一元 | + - ! ~ ++ -- (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + - | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与AND | & | 从左到右 |
位异或XOR | ^ | 从左到右 |
位或OR | | | 从左到右 |
逻辑与AND | && | 从左到右 |
逻辑或OR | || | 从左到右 |
条件 | ? : | 从右到左 |
赋值 | = += -= *= /= %= >>= <<= &= ^= |= | 从右到左 |
逗号 | , | 从左到右 |
#include <stdio.h>
int main() {
int a = 20;
int b = 10;
int c = 15;
int d = 5;
int e;
e = (a + b) * c / d; // (30 * 15) / 5
printf("(a + b) * c / d 的值是 %d\n", e);
e = ((a + b) * c) / d; // (30 * 15) / 5
printf("((a + b) * c) / d 的值是 %d\n", e);
e = (a + b) * (c / d); // (30) * (15 / 5)
printf("(a + b) * (c / d) 的值是 %d\n", e);
e = a + (b * c) / d; // 20 + (150 / 5)
printf("a + (b * c) / d 的值是 %d\n", e);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# C 判断
判断结构要求程序员指定一个或多个要评估或测评的条件,以及条件为真时要执行的语句(必需)和条件为假时要执行的语句(可选)。C语言把任何非零和非空的值假定为true
,把零或null假定为false
。
# 判断语句
语句 | 描述 |
---|---|
if 语句 | 一个if语句由一个布尔表达式后跟一个或多个语句组成。 |
if ... else 语句 | 一个if语句后可跟一个可选的else语句,else 语句在布尔表达式为假时执行 |
嵌套if 语句 | 可以在一个if 或else if 语句内使用另一个if 或else if 语句 |
switch 语句 | 一个switch 语句允许测试一个变量等于多个值时的情况 |
嵌套switch 语句 | 可以在switch 语句内使用另一个switch 语句 |
# ? : 运算符 (三元运算符)
在前面的章节中讲解了条件运算符 ? :,可以用来替代if ... else
语句,形式如下:
Exp1 ? Exp2 : Exp3;
其中,Exp1
、Exp2
和Exp3
是表达式,注意,冒号的使用和位置。
#include <stdio.h>
int main() {
int num;
printf("输入一个数字:");
scanf("%d", &num);
(num%2 == 0) ? printf("偶数\n") : printf("奇数\n");
}
2
3
4
5
6
7
8
9
10
# C 循环
有的时候,我们需要多次执行同一块代码。一般情况,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依次类推。
# 循环类型
循环类型 | 描述 |
---|---|
while 循环 | 当给定条件为真时,重复语句或语句组。它会在执行循环主体之前测试条件。 |
for 循环 | 多次执行一个语句序列,简化管理循环变量的代码。 |
do ... while 循环 | 除了它是在循环主体结尾测试条件外,其他与while 语句类似 |
嵌套循环 | 可以在while 、for 或do..while 循环内使用一个或多个循环 |
# 循环控制语句
循环控制语句改变代码的执行顺序,通过它可以实现代码的跳转。
控制语句 | 描述 |
---|---|
break 语句 | 终止循环或switch 语句,程序流将继续执行紧接着循环或switch 的下一条语句。 |
continue 语句 | 告诉一个循环体立刻停止循环迭代,重新开始下次循环迭代。 |
goto 语句 | 将控制转义到被标记的语句,但不建议在程序中使用goto 语句 |
# 无限循环
如果条件永远不为假,则循环将变成无限循环。for
循环在传统意义上可用于实现无限循环。由于构成循环的的三个表达式中任何一个都不是必需的,可以将某些条件表达式留空来构成一个无限循环。
#include <stdio.h>
int main() {
for ( ; ; ) {
printf("该循环会永远执行下去!\n");
}
return 0;
}
2
3
4
5
6
7
8
当条件表达式不存在时,它被假设为真。当然也可以设置一个初始值和增量表达式,但是一般情况,C程序员偏向于使用for(;;)
结构来表示一个无限循环。
# C 函数
函数是一组一起执行一个任务的语句。每个C程序都至少有一个函数,即主函数main()
,所有简单的程序都可以定义其他额外的函数。可以把代码划分到不同的函数中,如何划分代码到不同的函数中是由开发者来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行。
函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。C标准库提供了大量的程序可以调用的内置函数。例如,函数strcat()
用来连接两个字符串,函数memcpy()
用来赋值内存到另一个位置。函数还有很多叫法,比如方法、子例程或程序,等等。
# 定义函数
C语言中的函数定义的一般形式如下:
return_type function_name(parameter list) {
body of the function
}
2
3
在C语言中,函数由一个函数头和一个函数主体组成,下面列出一个函数的所有组成部分:
- 返回类型:一个函数可以返回一个值,
return_type
是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type
是关键字void
。 - 函数名称:这是函数的实际名称,函数名和参数列表一起构成了函数签名。
- 参数:参数就像是占位符,当函数被调用时,向参数传递一个值,这个值被称为实际参数,参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是,函数可能不包含参数。
- 函数主体:函数主体包含一组定义函数执行任务的语句。
以下是max()
函数的源代码,该函数有两个参数num1
和num2
,会返回这两个参数中比较大的那个数:
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2) {
/* 局部变量声明 */
int result;
if (num1 > num2) {
result = num1;
} else {
result = num2;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
# 函数声明
函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。函数声明包括以下几个部分:
return_type function_name(parameter list);
针对上面定义的函数max()
,以下是函数声明:
int max(int num1, int num2);
在函数声明中,参数的名称并不重要,只有参数的类型是必须的,因此下面也是有效的声明:
int max(int, int);
当在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必须的,在这种情况下,应该在调用函数的文件顶部声明函数。
# 调用函数
创建C函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。当程序调用函数时,程序控制权会转移给被调用的函数,被调用的函数执行已定义的任务,当函数返回语句被执行时,或达到函数的结束括号时,会把程序控制权交还给主程序。调用函数时,传递所需参数,如果函数返回值,则可以存储返回值,如:
#include <stdio.h>
/* 函数声明 */
int max(int num1, int num2);
int main() {
/* 局部变量定义 */
int a = 100;
int b = 200;
int ret;
/* 调用函数来获取最大值 */
ret = max(a, b);
printf("Max value is: %d\n", ret);
return 0;
}
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2) {
/* 局部变量声明 */
int result;
if (num1 > num2) {
result = num1;
} else {
result = num2;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 函数参数
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。当调用函数时,有两种向函数传递参数的方式:
调用类型 | 描述 |
---|---|
传值调用 | 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。 |
引用调用 | 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。 |
# 传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。默认情况下,C语言使用传值调用方法来传递参数,一般,这意味着函数内的代码不会改变用于调用函数的实际参数。函数swap()
定义如下:
/* 函数定义 */
void swap(int x, int y) {
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
return;
}
2
3
4
5
6
7
8
9
10
现在,通过传递实际参数调用函数swap()
#include <stdio.h>
/* 函数声明 */
void swap(int x, int y);
int main() {
/* 局部变量定义 */
int a = 100;
int b = 200;
printf("交换前,a 的值: %d\n", a);
printf("交换前,b 的值: %d\n", b);
/* 调用函数来交换值 */
swap(a, b);
printf("交换后,a 的值: %d\n", a);
printf("交换后,b 的值: %d\n", b);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 引用调用
通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行操作。传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。
/* 函数定义 */
void swap(int *x, int *y) {
int temp;
temp = *x; /* 保存地址 x 的值 */
*x = *y; /* 把 y 赋值给 x */
*y = temp; /* 把 temp 赋值给 y */
return;
}
2
3
4
5
6
7
8
#include <stdio.h>
/* 函数声明 */
void swap(int *x, int *y);
int main() {
/* 局部变量定义 */
int a = 100;
int b = 200;
printf("交换前,a 的值: %d\n", a);
printf("交换前,b 的值: %d\n", b);
/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
swap(&a, &b);
printf("交换后,a 的值: %d\n", a);
printf("交换后,b 的值: %d\n", b);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面的实例表明,与传值调用不同,引用调用在函数内改变了a
和b
的值,实际上也改变了函数外a
和b
的值。
# C 作用域规则
任何一种编程中,作用域是程序中定义的变量所在的区域,超过该区域变量就不能被访问,C语言中有三个地方可以声明变量:
- 在函数或块内部的局部变量
- 在所有函数外部的全局变量
- 在函数参数定义中的形式参数
# 局部变量
在某个函数或块的内部声明的变量称为局部变量。它们智能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。如下实例,a
、b
和c
是main()
函数的局部变量。
#include <stdio.h>
int main() {
/* 局部变量声明 */
int a, b;
int c;
/* 实际初始化 */
a = 10;
b = 20;
c = a + b;
printf("value of a = %d, b = %d and c = %d\n", a, b, c);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 全局变量
全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序声明周期内都是有效的,在任意的函数内部能访问全局变量。全局变量可以被任何函数访问。也就是说,全局变量在声明后整个程序中都是可用的,实例:
#include <stdio.h>
/* 全局变量声明 */
int g;
int main() {
/* 局部变量声明 */
int a, b;
/* 实际初始化 */
a = 10;
b = 20;
g = a + b;
printf("value of a = %d, b = %d and g = %d\n", a, b, g);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意
在程序中,局部变量和全局变量的名称可以相同,但是在函数内,如果两个名字相同,会使用局部变量值,全局变量不会被使用。实例:
#include <stdio.h>
/* 全局变量声明 */
int g = 20;
int main() {
/* 局部变量声明 */
int g = 10;
printf("value of g = %d\n", g);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 形式参数
函数的参数,形式参数,被当做该函数内的局部变量,如果与全局变量同名它们会优先使用,实例:
#include <stdio.h>
/* 全局变量声明 */
int a = 20;
int main() {
/* 在主函数中的局部变量声明 */
int a = 10;
int b = 20;
int c = 0;
int sum(int, int);
printf("value of a in main() = %d\n", a);
c = sum(a, b);
printf("value of c in main() = %d\n", c);
return 0;
}
/**
* 添加两个整数相加的函数
*/
int sum(int a, int b) {
printf("value of a in sum() = %d\n", a);
printf("value of b in sum() = %d\n", b);
return a + b;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
全局变量与局部变量在内存中的区别
- 全局变量保存在内存的的全局存储区中,占用静态的存储单元;
- 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
# 初始化局部变量和全局变量
当局部变量被定义时,系统不会对其初始化,开发者必须自行对其初始化,定义全局变量时,系统会自动对其初始化,如下:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | '\0' |
float | 0 |
double | 0 |
pointer | NULL |
正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果,因为未初始化的变量会导致一些在内存位置中已经可用的垃圾值。
# C 数组
C语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。数组的声明并不是一个个单独的变量,比如runoob0
、runoob1
、...、runoob99
,而是声明一个数组变量,比如runoob
,然后使用runoob[0]
、runoob[1]
、...、runoob[99]
来代表一个个单独变量。
所有的数组都是由连续的内存位置组成,最低的地址对应第一个元素,最高的地址对应最后一个元素。
数组中的特定元素可以通过索引访问,第一个索引值为0
。C语言还允许使用指针处理数组,可以使对数组的操作更加灵活和搞笑。
# 声明数组
在C中要声明一个数组,需要指定元素的类型和元素的数量,如下:
type arrayName [ arraySize ];
这叫做一维数组,arraySize
必须是一个大于零的整数常量,type
可以是任意有效的C数据类型。如,要声明一个类型为double
的包含10
个元素的数组balance
,语句如下:
double balance[10];
现在balance
是一个可用的数组,可以容纳10
个类型为double
的数字。
# 初始化数组
在C中,开发者可以逐个初始化数组,也可以使用一个初始化语句,如下:
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
大括号{}
之间的值的数目不能大于我们在数组声明时在方括号[]
中指定的元素数目。如果省略了数组的大小,数组的大小则为初始化时元素的个数,如:
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
这将创建一个数组,它与前一个实例中创建的数组完全相同,下面是一个为数组中某个元素赋值的实例:
balance[4] = 50.0;
上述的语句把数组中第五个元素的值赋为50.0
,所有的数组都是以0
作为它们第一个元素的索引,也被称为基索引,数组的最后一个索引是数组的总大小减去1
,如图:
下图是一个长度为10
的数组,第一个元素的索引值为0
,第九个元素ronoob
的索引值为8
:
# 访问数组元素
数组元素可以通过数组名称加索引进行访问,元素的索引是放在方括号内,跟在数组名称的后边,如:
double salary = balance[9];
上面的语句将把数组中第10
个元素的值赋给salary
变量,下面的实例使用了上述的三个概念,即,声明数组、数组赋值、访问数组:
#include <stdio.h>
int main() {
int n[10]; // n 是一个包含 10 个整数的数组
int i, j;
/* 初始化数组元素 */
for (int i = 0; i < 10; i++) {
n[i] = i + 100; // 设置元素 i 为 i + 100
}
/* 输出数组中每个元素的值 */
for (j = 0; j < 10; j++) {
printf("Element[%d] = %d\n", j, n[j]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取数组长度
数组长度可以使用sizeof
运算符来获取数组的长度,例如:
int numbers[] = {1, 2, 3, 4, 5};
int length = sizeof(numbers) / sizeof(numbers[0]);
2
实例:
#include <stdio.h>
int main() {
int array[] = {1, 2, 3, 4, 5};
int length = sizeof(array) / sizeof(array[0]);
printf("数组长度为: %d\n", length);
return 0;
}
2
3
4
5
6
7
8
9
10
使用宏定义:
#include <stdio.h>
#define LENGTH(array) (sizeof(array) / sizeof(array[0]))
int main() {
int array[] = {1, 2, 3, 4, 5};
int length = LENGTH(array);
printf("数组长度为: %d\n", length);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 数组名
在C语言中,数组名表示数组的地址,即数组首元素的地址。当开发者声明和定义一个数组时,该数组名就代表着该数组的地址。如:
int myArray[5] = {10, 20, 30, 40, 50};
在这里,myArray
是数组名,它表示整数类型的数组,包含5
个元素。myArray
也代表着数组的地址,即第一个元素的地址。数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。开发者可以使用&
运算符来获取数组的地址,如下:
int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0]; // 或者直接写作 int *ptr = myArray;
2
在上面的例子中,ptr
指针变量被初始化为myArray
的地址,即数组的第一个元素的地址。需要注意的是,虽然数组名表示数组的地址,但大多数,数组名会自动转换为指向数组首元素的指针。这意味着可以直接将数组名用于指针运算,例如在函数传递参数或遍历数组时:
#include <stdio.h>
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d \n", arr[i]);
}
}
int main() {
int myArray[5] = {10, 20, 30, 40, 50};
printArray(myArray, 5); // 将数组名传递给函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
在上述代码中,printArray
函数接受一个整数数组和数组大小作为参数,将myArray
数组名传递给函数,函数内部可以像使用指针一样使用arr
数组名。
# C 数组详解
在C中,数组是非常重要的,开发者需要了解更多有关数组的细节,下面列出了C程序员必须清楚的一些与数组相关的重要概念:
概念 | 描述 |
---|---|
多维数组 | C支持多维数组,多维数组最简单的形式是二维数组 |
传递数组给函数 | 开发者可以通过指定不带索引的数组名称来给函数传递一个指向数组的指针 |
从函数返回数组 | C允许从函数返回数组 |
指向数组的指针 | 开发者可以通过指定不带索引的数组名称来生成一个指向数组中第一个元素的指针 |
静态数组和动态数组 | 静态数组在编译时分配内存,大小固定,而动态数组在运行时手动分配内存,大小可变 |
# 多维数组
C语言支持多维数组,多维数组声明的一般形式如下:
type name[size1][size2]...[sizeN];
如,下面声明了一个三维5
、10
、4
的整型数组:
int threedim[5][10][4];
多维数组最简单的形式是二维数组,一个二维数组,在本质上,是一个一维数组的列表。声明一个x
行y
列的二维整型数组,形式如下:
type arrayName[x][y];
其中,type
可以是任意有效的C数据类型,arrayName
是一个有效的C标识符,一个二维数组可以被认为是一个带有x
行和y
列的表格。下面是一个二维数组,包含3
行和4
列:
int x[3][4];
因此,数组中的每个元素是使用形式为a[i][j]
的元素名称来标识的,其中a
是数组名称,i
和j
是唯一标识a
中每个元素的下标。
初始化二维数组:多维数组可以通过在括号内为每行指定值来进行初始化,下面是一个带有3
行4
列的数组
int a[3][4] = {
{0, 1, 2, 3}, // 初始化索引号为 0 的行
{4, 5, 6, 7}, // 初始化索引号为 1 的行
{8, 9, 10, 11} // 初始化索引号为 2 的行
}
2
3
4
5
内部嵌套的括号是可选的,下面的初始化与上面是等同的:
int a[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
访问二维数组元素:二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的,例如:
int val = a[2][3];
上面的语句将获取数组中第3
行第4
个元素。开发者使用嵌套循环来处理二维数组:
#include <stdio.h>
int main() {
/* 一个带有 5 行 2 列的数组 */
int a[5][2] = {
{0, 0},
{1, 2},
{2, 4},
{3, 6},
{4, 8}
};
int i, j;
/* 输出数组中每个元素的值 */
for (i = 0; i < 5; i++) {
for (j = 0; j < 2; j++) {
printf("a[%d][%d] = %d\n", i, j, a[i][j]);
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如上,开发者可以创建任意维度的数组,一般情况,开发者创建的数组是一维数组和二维数组。
# 传递数组给函数
开发者想要在函数中传递一个一维数组作为参数,则必须采用下面三种方式来声明函数的形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针,当然,也可以传递一个多维数组作为形式参数。
方式一:形式参数是一个指针
void myFunction(int *param) {
...
}
2
3
方式二:形式参数是一个已定义大小的数组
void myFunction(int param[10]) {
...
}
2
3
方式三:形式参数是一个未定义大小的数组
void myFunction(int param[]) {
...
}
2
3
下面来看下这个函数,它把数组作为参数,同时还传递了另一个参数,根据所传的参数,会返回数组中元素的平均值:
double getAverage(int arr[], int size) {
int i;
double avg;
double sum;
for (i = 0; i < size; ++i) {
sum += arr[i];
}
avg = sum / size;
return avg;
}
2
3
4
5
6
7
8
9
10
11
12
13
接下来,调用上面的函数,如下:
#include <stdio.h>
/* 函数声明 */
double getAverage(int arr[], int size);
int main() {
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 传递一个指向数组的指针作为参数 */
avg = getAverage(balance, 5);
/* 输出返回值 */
printf("平均值是: %f \n", avg);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
就函数而言,数组的长度是无关紧要的,因为C不会对形式参数执行边界检查。
# 从函数返回数组
C语言不允许返回一个完整的数组作为函数的返回值,但是可以通过指定不带索引的数组名来返回一个指向数组的指针,如果想要从函数返回一个数组,则必须声明一个返回指针的函数,如下:
int * myFunction() {
...
}
2
3
另外,C不支持在函数外返回局部变量的地址,除非定义局部变量为static
变量。下面来看下这个函数,它会生成10
个随机数,并使用数组来返回它们,如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/* 要生成和返回随机数的函数 */
int * getRandom() {
static int r[10];
int i;
/* 设置种子 */
srand((unsigned) time(NULL));
for (i = 0; i < 10; i++) {
r[i] = rand();
printf("r[%d] = %d\n", i, r[i]);
}
return r;
}
/* 调用上面定义函数的主函数 */
int main() {
/* 一个指向整数的指针 */
int *p;
int i;
p = getRandom();
for (i = 0; i < 10; i++) {
printf("*(p + %d) : %d\n", i, *(p + i));
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 指向数组的指针
数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。因此,下面的声明中:
double balance[50];
balance
是一个指向&balance[0]
的指针,即数组balance
的第一个元素的地址。因此,下面的程序片段把p
赋值为balance
的第一个元素的地址:
double *p;
double balance[10];
p = balance;
2
3
4
使用数组名作为常量指针是合法的,反之依然,因此,*(balance + 4)
是一种访问balance[4]
数据的合法方式。一旦开发者把第一个元素的地址存储在p
中,就可以使用*p
、*(p + 1)
、*(p + 2)
等来访问数组元素,如下实例:
#include <stdio.h>
int main() {
/* 带有 5 个元素的整型数组 */
double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};
double *p;
int i;
p = balance;
/* 输出数组中每个元素的值 */
printf("使用指针的数组值\n");
for (i = 0; i < 5; i++) {
printf("*(p + %d) : %f\n", i, *(p + i));
}
printf("使用 balance 作为地址的数组值\n");
for (i = 0; i < 5; i++) {
printf("*(balance + %d) : %f\n", i, *(balance + i));
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上面的实例中,p
是一个指向double
型的指针,这意味着它可以存储一个double
类型的变量,一旦有了p
中的地址,*p
将给出存储在p
中响应地址的值。
# 静态数组与动态数组
在C语言中,有两种类型的数组:
- 静态数组:编译时分配内存,大小固定
- 动态数组:运行时手动分配内存,大小可变
静态数组的生命周期与作用域相关,而动态数组的生命周期由程序员控制。在使用动态数组时,需要注意合理分配和释放内存,以避免内存泄露和访问无效内存的问题。
静态数组:静态数组是在编译时声明并分配内存空间的数组,其具有固定的大小,在声明数组时需要指定数组的长度。其特点包括
- 内存分配:在程序编译时,静态数组的内存空间就被分配好了,存储在栈上或者全局数据区
- 大小固定:静态数组的大小在声明时确定,并且无法在运行时改变
- 生命周期:静态数组的生命周期与其作用域相关,如果在函数内部声明静态数组,其声明周期为整个函数执行期间;如果在函数外部声明静态数组,其生命周期为整个程序的执行期间。
静态数组的声明和初始化示例:
int staticArray[5]; // 静态数组声明
int staticArray[] = {1, 2, 3, 4, 5}; // 静态数组声明并初始化
2
对于静态数组,可以使用sizeof
运算符来获取数组长度,如:
int array[] = {1, 2, 3, 4, 5};
int length = sizeof(array) / sizeof(array[0]);
2
以上代码中sizeof(array)
返回整个数组所占用的字节数,而sizeof(array[0])
返回数组中单个元素的字节数,将两者相除,就得到了数组的长度。
实例:
#include <stdio.h>
int main() {
int staticArray[] = {1, 2, 3, 4, 5}; // 静态数组声明并初始化
int length = sizeof(staticArray) / sizeof(staticArray[0]);
printf("静态数组:");
for (int i = 0; i < length; i++) {
printf("%d ", staticArray[i]);
}
printf("\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上实例中,声明并初始化了一个静态数组staticArray
,包含了5
个整数元素,然后通过sizeof
运算符,计算了静态数组的长度,并使用循环遍历并打印数组的元素。
动态数组:动态数组是在运行时通过动态内存分配函数(如malloc
和calloc
)手动分配内存的数组。其特点如下:
- 内存分配:动态数组的内存空间在运行时通过动态内存分配函数手动分配,并存储在堆上。需要使用
malloc
、calloc
等函数来申请内存,并使用free
函数来释放内存 - 大小可变:动态数组的大小在运行时可以根据需要进行调整,可以使用
realloc
函数来重新分配内存,并改变数组的大小 - 生命周期:动态数组的生命周期由程序员控制,需要在使用完数组后手动释放内存,以避免内存泄露。
动态数组的生命、内存分配和释放实例:
int size = 5;
int *dynamicArray = (int *)malloc(size * sizeof(int)); // 动态数组内存分配
// 使用动态数组
free(dynamicArray); // 动态数组内存释放
2
3
4
动态分配的数组,可以在动态分配内存时保存数组长度,并在需要时使用该长度,如:
int size = 5; // 数组长度
int *array = malloc(size * sizeof(int));
// 使用数组
free(array); // 释放内存
2
3
4
5
6
以上代码我们使用malloc
函数动态分配了一个整型数组,并将长度保存在变量size
中,然后可以根据需要使用这个长度进行操作,在使用完数组后,使用free
函数释放内存。
注意
动态数组的使用需要注意内存管理的问题,确保在不再需要使用数组时释放内存,避免内存泄露和访问无效的内存位置。
一个简单的动态数组使用实例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int size = 5;
int *dynamicArray = (int *)malloc(size * sizeof(int)); // 动态数组内存分配
if (dynamicArray == NULL) {
printf("Memory allocation failed. \n");
return 1;
}
printf("Enter %d elements: ", size);
for (int i = 0; i < size; i++) {
scanf("%d", &dynamicArray[i]);
}
printf("Dynamic Array: ");
for (int i = 0; i < size; i++) {
printf("%d ", dynamicArray[i]);
}
printf("\n");
free(dynamicArray); // 动态数组内存释放
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
以上实例中,首先声明了一个变量size
来指定动态数组的大小,然后使用malloc
函数为动态数组分配内存,并通过sizeof
运算符计算所需的内存大小,接下来,通过循环和scanf
函数,从用户输入中读取元素值并存储到动态数组中,最后,使用循环遍历并打印动态数组的元素。在程序结束时,使用free
函数释放动态数组所占用的内存。
注意
在使用动态数组时,需要检查内存分配是否成功(即dynamicArray
是否为NULL
),以避免在内存分配时发生错误。
# C enum 枚举
枚举是C语言中的一种基本数据类型,用于定义一组具有离散值的常量,其可以让数据更简洁,更易读。枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
定义一个枚举类型,需要使用enum
关键字,后面跟着枚举类型的名称,以及用大括号{}
括起来的一组枚举常量,每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从0
开始递增。其语法格式:
enum 枚举名 { 枚举元素1, 枚举元素2, ..., 枚举元素n };
举个例子,一个星期有7天,如果不用枚举,需要使用#define
来为每个整数定义别名:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRE 5
#define SAT 6
#define SUN 7
2
3
4
5
6
7
这个代码量就比较多,枚举方式:
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
2
3
注意
第一个枚举成员的默认值为整型的0
,后续枚举程序的值在前一个成员加1
。在这个实例中把第一个枚举成员的值定义为1
,第二个2
,依次类推。
可以在定义枚举类型时改变枚举元素的值:
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加1
,也就说spring
的值为0
,summer
的值为3
,autumn
的值为4
,winter
的值为5
# 枚举变量的定义
前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。开发者可以通过三种方式来定义枚举变量:
- 先定义枚举类型,再定义枚举变量
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
2
3
4
- 定义枚举类型的同时定义枚举变量
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
} day;
2
3
- 省略枚举名称,直接定义枚举变量
enum {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
} day;
2
3
#include <stdio.h>
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
int main() {
enum DAY day;
day = WED;
printf("%d\n", day);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
在C语言中,枚举类型是被当做int
或者unsigned int
类型处理的,所以按照C语言规范是没有办法遍历枚举类型的。不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。以下实例使用for
来遍历枚举的元素:
#include <stdio.h>
enum DAY {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main() {
// 遍历枚举元素
for (day = MON; day <= SUN; day++) {
printf("枚举元素:%d\n", day);
}
}
2
3
4
5
6
7
8
9
10
11
12
以下枚举类型不连续,这种枚举无法遍历。
enum {
ENUM_0,
ENUM_10 = 10,
ENUM_11
}
2
3
4
5
枚举在switch
中的使用:
#include <stdio.h>
#include <stdlib.h>
int main() {
enum color { red = 1, green, blue };
enum color favorite_color;
/* 用户输入数字来选择颜色 */
printf("请输入你喜欢的颜色:(1. red, 2. green, 3. blue): ");
scanf("%u", &favorite_color);
/* 输出结果 */
switch(favorite_color) {
case red:
printf("你喜欢的颜色是红色\n");
break;
case green:
printf("你喜欢的颜色是绿色\n");
break;
case blue:
printf("你喜欢的颜色是蓝色\n");
break;
default:
printf("你没有选择你喜欢的颜色\n");
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 将整数转换为枚举
以下实例将整数转换为枚举:
#include <stdio.h>
#include <stdlib.h>
int main() {
enum day {
saturday,
sunday,
monday,
tuesday,
wednesday,
thursday,
friday
} workday;
int a = 1;
enum day weekend;
weekend = (enum day) a; // 类型转换
// weekend = a; // 错误
printf("weekend: %d\n", weekend);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# C 指针
通过指针,可以简化一些C编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的,所以,想要称为一名优秀的C程序员,学习指针是很有必要的。在C中,每一个变量都有一个内存位置,每一个内存位置都定义了可使用&
运算符访问的地址,它表示了在内存中的一个地址。
看下面的实例,它将输出定义的变量地址:
#include <stdio.h>
int main() {
int var_runoob = 10;
int *p; // 定义指针变量
p = &var_runoob;
printf("var_runoob 变量的地址: %p\n", p);
return 0;
}
2
3
4
5
6
7
8
9
10
# 什么是指针
指针也就是内存地址,指针变量是用来存放内存地址的变量,就像其他变量或常量一样,开发者必须在使用指针存储其他变量地址之前,对其进行声明,指针变量声明的一般形式为:
type *var_name;
在这里,type
是指针的基类型,它必须是一个有效的C数据类型,var_name
是指针变量的名称,用来声明指针的星号*
与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。
int *ip; // 一个整型的指针
double *dp; // 一个double型的指针
float *fp; // 一个浮点型的指针
char *ch; // 一个字符型的指针
2
3
4
所有实际数据类型,不管是整型、浮点型、字符型,还是其他数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
# 如何使用指针
使用指针时会频繁进行以下操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符*
来返回位于操作数所指定地址的变量的值,如下实例:
#include <stdio.h>
int main() {
int var = 20; // 实际变量的声明
int *ip; // 指针变量的声明
ip = &var; // 在指针变量中存储var的地址
printf("var 变量的地址: %p\n", &var);
// 在指针变量中存储的地址
printf("ip 变量存储的地址: %p\n", ip);
// 使用指针访问值
printf("*ip 变量的值: %d\n", *ip);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# C 中的 NULL 指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个NULL
值是一个良好的编程习惯,赋为NULL
值的指针被称为空指针。NULL
指针是一个定义在标准库中的值为零的常量,如下:
#include <stdio.h>
int main() {
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr);
return 0;
}
2
3
4
5
6
7
8
9
在大多数操作系统上,程序不允许访问地址为0
的内存,因为该内存是操作系统保留的,然而,内存地址0
有特别重要的意义,它表明该指针不指向一个可访问的内存位置,但按照管理,如果指针包含空值(零值),则假定它不指向任何东西。
如需检查一个空指针,可以使用if
语句,如下:
if(ptr) // 如果 p 非空,则执行
if(!ptr) // 如果 p 为空,则执行
2
# C 指针详解
在C中,有很多指针相关的概念,这些概念都很简单,但都很重要,下面列出了C程序员必须清楚的一些与指针相关的重要概念:
# 指针的算术运算
C指针是一个用数值表示的地址,因此,可以对指针执行算术运算。可以对指针进行四种算术运算:++
、--
、+
、+
。假设ptr
是一个指向地址1000
的整型指针,是一个32
位的整数,对该指针执行下列的算术运算:
ptr++
在执行完上述的运算之后,ptr
将指向位置1004
,因为ptr
每增加一次,它都将指向下一个整数位置,即当前位置往后移4
字节,这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果ptr
指向一个地址为1000
的字符,上面的运算会导致指针指向位置1001
,因为下一个字符位置是1001
。
总结
- 指针的每一次递增,它其实会指向下一个元素的存储单元
- 指针的每一次递减,它都会指向前一个元素的存储单元
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如
int
就是4
个字节
递增一个指针:开发者喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量,下面的程序递增变量指针,以便顺序访问数组中的每一个元素:
#include <stdio.h>
const int MAX = 3;
int main() {
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中的数组地址 */
ptr = var;
for (i = 0; i < MAX; i++) {
printf("存的地址:var[%d] = %p\n", i, ptr);
printf("存储值:var[%d] = %d\n", i, *ptr);
/* 指向下一个位置 */
ptr++;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
递减一个指针:同样,对指针进行递减运算,即把值减去其数据类型的字节数,如下:
#include <stdio.h>
const int MAX = 3;
int main() {
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中最后一个元素地址 */
ptr = &var[MAX - 1];
for (i = MAX; i > 0; i--) {
printf("存储地址:var[%d] = %p\n", i - 1, ptr);
printf("存储值:var[%d] = %d\n", i - 1, *ptr);
/* 指向下一个位置 */
ptr--;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
指针的比较:指针可以用关系运算符进行比较,如==
、<
和>
,如果p1
和p2
指向两个相关的变量,比如同一个数组中的不同元素,则可对p1
和p2
进行大小比较。下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址&var[MAX - 1]
,则把变量指针进行递增:
#include <stdio.h>
const int MAX = 3;
int main() {
int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中第一个元素地址 */
ptr = var;
i = 0;
while (ptr <= &var[MAX - 1]) {
printf("存储地址:var[%d] = %p\n", i, ptr);
printf("存储值:var[%d] = %d\n", i, *ptr);
/* 指向上一个位置 */
ptr++;
i++;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 指针数组
先来看一个实例,由3
个整数组成的数组:
#include <stdio.h>
const int MAX = 3;
int main() {
int var[] = {10, 100, 200};
int i;
for (i = 0; i < MAX; i++) {
printf("Value of var[%d] = %d\n", i, var[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
可能有一种需求,想要让数组存储指向int
或char
或其他数据类型的指针,下面是一个指向整数的指针数组的声明:
int *ptr[MAX];
在这里,把ptr
声明为一个数组,由MAX
个整数指针组成,因此,ptr
中的每个元素,都是一个指向int
指向int
值的指针,下面的实例用到了三个整数,它们将存储在一个指针数组中,如下:
#include <stdio.h>
const int MAX = 3;
int main() {
int var[] = {10, 100, 200};
int i, *ptr[MAX];
for (i = 0; i < MAX; i++) {
ptr[i] = &var[i]; // 赋值为整数的地址
}
for (i = 0; i < MAX; i++) {
printf("Value of var[%d] = %d\n", i, *ptr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当然也可以用一个指向字符的指针数组来存储一个字符串列表,如下:
#include <stdio.h>
const int MAX = 4;
int main() {
const char *names[] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali"
};
int i = 0;
for (i = 0; i < MAX; i++) {
printf("Value of names[%d] = %s\n", i, names[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 指向指针的指针
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址,当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号,如,下面声明了一个指向int
类型指针的指针:
int **var;
当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下:
#include <stdio.h>
int main() {
int V;
int *Pt1;
int **Pt2;
V = 100;
/* 获取 V 的地址 */
Pt1 = &V;
/* 使用运算符 & 获取 Pt1 的地址 */
Pt2 = &Pt1;
/* 使用 pptr 获取值 */
printf("var = %d\n", V);
printf("Pt1 = %p\n", Pt1);
printf("*Pt1 = %d\n", *Pt1);
printf("Pt2 = %p\n", Pt2);
printf("**Pt2 = %d\n", **Pt2);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 传递指针给函数
C语言允许开发者传递指针给函数,只需要简单地声明函数参数为指针类型即可。下面的实例中,传递一个无符号的long
指针给函数,并在函数内改变这个值:
#include <stdio.h>
#include <time.h>
void getSeconds(unsigned long *par);
int main() {
unsigned long sec;
getSeconds(&sec);
/* 输出实际值 */
printf("Number of seconds: %ld\n", sec);
return 0;
}
void getSeconds(unsigned long *par) {
/* 获取当前的秒数 */
*par = time(NULL);
return;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
能接受指针作为参数的函数,也能接受数组作为参数,如下所示:
#include <stdio.h>
/* 函数声明 */
double getAverage(int *arr, int size);
int main() {
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 传递一个指向数组的指针作为参数 */
avg = getAverage(balance, 5);
/* 输出返回值 */
printf("Average value is: %f\n", avg);
return 0;
}
double getAverage(int *arr, int size) {
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i) {
sum += arr[i];
}
avg = (double) sum / size;
return avg;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 从函数返回指针
C允许从函数返回指针,为了做到这点,必须声明一个返回指针的函数,如下:
int * myFunction() {
...
}
2
3
另外,C语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为static
变量。来看下面的函数,它会生成10
个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
/* 要生成和返回随机数的函数 */
int * getRandom() {
static int r[10];
int i;
/* 设置种子 */
srand((unsigned) time(NULL));
for (i = 0; i < 10; ++i) {
r[i] = rand();
printf("%d\n", r[i]);
}
return r;
}
/* 要调用上面定义函数的主函数 */
int main() {
/* 一个指向整数的指针 */
int *p;
int i;
p = getRandom();
for (i = 0; i < 10; i++) {
printf("*(p + [%d]) : %d\n", i, *(p + i));
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# C 函数指针
函数指针是指向函数的指针变量。通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数。函数指针变量的声明:
typedef int (*fun_ptr)(int, int); // 声明一个指向同样参数、返回值的函数指针类型
实例:以下实例声明了函数指针变量p
,指向函数max
#include <stdio.h>
int max(int x, int y) {
return x > y ? x : y;
}
int main(void) {
/* p 是函数指针 */
int (*p)(int, int) = &max; // & 可以忽略
int a, b, c, d;
printf("请输入三个数字: \n");
scanf("%d %d %d", &a, &b, &c);
/* 与直接调用函数等价,d = max(max(a, b), c) */
d = p(p(a, b), c);
printf("最大的数字是: %d\n", d);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# C 回调函数
函数指针作为某个函数的参数。函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数,简单讲: 回调函数是由别人的函数执行时调用你实现的函数。
实例:实例中populate_array()
函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。实例中定义了回调函数getNextRandomValue()
,它返回一个随机值,它作为一个函数指针传递给populate_array()
函数,populate_array()
将调用10
次回调函数,并将回调函数的返回值赋值给数组。
#include <stdlib.h>
#include <stdio.h>
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void)) {
for (size_t i = 0; i < arraySize; i++) {
array[i] = getNextValue();
}
}
/* 获取随机值 */
int getNextRandomValue(void) {
return rand();
}
int main(void) {
int myarray[10];
/* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int, 而不是函数指针 */
populate_array(myarray, 10, getNextRandomValue);
for (int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# C 字符串
在C语言中,字符串实际上是使用空字符\0
结尾的一堆字符数组。因此,\0
是用于标记字符串的结束。**空字符(Null character)**又称结束符,缩写NUL
,是一个数值为0
的控制字符,\0
是转义字符,意思告诉编译器,这不是字符0
,而是空字符。
下面的声明和初始化创建了一个RUNOOB字符串,由于在数组的末尾存储了空字符\0
,所以字符数组的大小比单词RUNOOB的字符数多一个。
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
依据数组初始化规则,可以把上面的语句写成以下语句:
char site[] = "RUNOOB";
以下是C/C++中定义的字符串的内存表示:
其实,不需要把null
字符放在字符串常量的末尾,C编译器会在初始化数组时,自动把\0
放在字符串的末尾,如:
#include <stdio.h>
int main() {
char site1[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
printf("site1: %s\n", site1);
int size1 = sizeof(site1) / sizeof(site1[0]);
printf("site1 size: %d\n", size1);
char site2[] = "RUNOOB";
printf("site2: %s\n", site2);
int size2 = sizeof(site2) / sizeof(site2[0]);
printf("site2 size: %d\n", size2);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
执行结果:
C中有大量操作字符串的函数:
函数 | 目的 |
---|---|
strcpy(s1, s2); | 复制字符串s2到字符串s1 |
strcat(s1, s2); | 连接字符串s1到字符串s1的末尾 |
strlen(s1); | 返回字符串s1的长度 |
strcmp(s1, s2); | 如果s1和s2相同,返回0;如果s1小于s2则返回小于0;如果s1大于s2则返回大于0 |
strchr(s1, ch); | 返回一个指针,指向字符串s1中字符ch第一次出现的位置 |
strstr(s1, s2); | 返回一个指针,指向字符串s1中字符串s2第一次出现的位置 |
实例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[14] = "runoob";
char str2[14] = "google";
char str3[14];
int len;
/* 赋值 str1 到 str3 */
strcpy(str3, str1);
printf("strcpy(str3, str1): %s\n", str3);
/* 连接 str1 和 str2 */
strcat(str1, str2);
printf("strcat(str1, str2): %s\n", str1);
/* 连接后,str1的总长度 */
len = strlen(str1);
printf("strlen(str1): %d\n", len);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# C 结构体
C数组允许定义可存储相同类型数据项的变量,结构体是C编程中另一种用户自定义的可用的数据类型,它允许存储不同类型的数据项。结构体中的数据成员可以是基本数据类型(如int
、float
、char
等),也可以是其他结构体类型、指针类型等。
结构体用于表示一条记录,假设想要跟踪图书馆中书本的动态,就可能需要跟踪每本书的下列属性:
- Title
- Author
- Subject
- Book ID
# 定义结构体
结构体定义由关键字struct
和结构体名组成,结构体可以根据需要自行定义。struct
语句定义了一个包含多个成员的新的数据类型,struct
语句的格式如下:
struct tag {
member-list
member-list
member-list
...
} variable-list;
2
3
4
5
6
tag
是结构体标签,member-list
是标准的变量定义,如int i;
或者float f;
,或者其他有效的变量定义,variable-list
结构变量,定义在结构的末尾,最后一个分号之前,可以指定一个或多个结构变量。如下,是Book
结构的定义:
struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
2
3
4
5
6
一般情况下,tag
、member-list
、variable-list
这3部分至少要出现2个,如下:
// 此声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c,同时又声明了结构体变量s1
// 这个结构体并没有标明其标签
struct {
int a;
char b;
double c;
} s1;
// 此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
// 结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE {
int a;
char b;
double c;
}
// 用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
// 也可以用typedef创建新类型
typedef struct {
int a;
char b;
double c;
} Simple2;
// 现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在上面的声明中,第一个和第二个声明被编译器当做两个完全不同的类型,即使它们的成员列表是一样的,如果令t3 = &s1
,则是非法的。
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
// 此结构体的声明包含了其他的结构体
struct COMPLEX {
char string[100];
struct SIMPLE a;
};
// 此结构体的声明包含了指向自己类型的指针
struct NODE {
char string[100];
struct NODE *next_node;
}
2
3
4
5
6
7
8
9
10
11
如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下:
// 对结构体B进行不完整声明
struct B;
// 结构体A中包含指向结构体B的指针
struct A {
struct B *partner;
// other members;
};
// 结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B {
struct A *partner;
// other members;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
# 结构体变量初始化
和其他类型变量一样,对结构体变量可以在定义时指定初始值。
#include <stdio.h>
struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
int main() {
printf("title: %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}
2
3
4
5
6
7
8
9
10
11
12
# 访问结构体成员
为了访问结构成员,使用成员访问运算符(.)。成员访问运算符是结构体变量名称和我们要访问的结构成员之间的一个句号,可以使用struct
关键字定义结构体类型的变量,如下:
#include <stdio.h>
#include <string.h>
struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main() {
struct Books book1; // 声明 book1, 类型为 Books
struct Books book2; // 声明 book2, 类型为 Books
/* book1 详述 */
strcpy(book1.title, "C Programming");
strcpy(book1.author, "Nuha Ali");
strcpy(book1.subject, "C Programming Tutorial");
book1.book_id = 6495407;
/* book2 详述 */
strcpy(book2.title, "Telecom Billing");
strcpy(book2.author, "Zara Ali");
strcpy(book2.subject, "Telecom Billing Tutorial");
book2.book_id = 6495700;
/* 输出 book1 信息 */
printf("Book 1 title: %s\n", book1.title);
printf("Book 1 author: %s\n", book1.author);
printf("Book 1 subject: %s\n", book1.subject);
printf("Book 1 book_id: %d\n", book1.book_id);
/* 输出 book2 信息 */
printf("Book 2 title: %s\n", book2.title);
printf("Book 2 author: %s\n", book2.author);
printf("Book 2 subject: %s\n", book2.subject);
printf("Book 2 book_id: %d\n", book2.book_id);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 结构体作为函数参数
可以把结构体作为函数参数,传参方式与其他类型的变量或指针类似,如下:
#include <stdio.h>
#include <string.h>
struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函数声明 */
void printBook(struct Books book);
int main() {
struct Books book1; // 声明 book1, 类型为 Books
struct Books book2; // 声明 book2, 类型为 Books
/* book1 详述 */
strcpy(book1.title, "C Programming");
strcpy(book1.author, "Nuha Ali");
strcpy(book1.subject, "C Programming Tutorial");
book1.book_id = 6495407;
/* book2 详述 */
strcpy(book2.title, "Telecom Billing");
strcpy(book2.author, "Zara Ali");
strcpy(book2.subject, "Telecom Billing Tutorial");
book2.book_id = 6495700;
/* 输出 book1 信息 */
printBook(book1);
/* 输出 book2 信息 */
printBook(book2);
return 0;
}
void printBook(struct Books book) {
printf("Book title: %s\n", book.title);
printf("Book author: %s\n", book.author);
printf("Book subject: %s\n", book.subject);
printf("Book book_id: %d\n", book.book_id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 指向结构体的指针
可以定义指向结构的指针,方式与定义指向其他类型变量的指针类似,如下:
struct Books *struct_pointer;
现在,可以在上述定义的指针变量中存储结构变量的地址,为了查找结构变量的地址,请把&
运算符放在结构变量名称的前面,如下:
struct_pointer = &book1;
为了使用指向该结构的指针访问结构的成员,必须使用->
运算符,如下:
struct_pointer->title;
#include <stdio.h>
#include <string.h>
struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函数声明 */
void printBook(struct Books *book);
int main() {
struct Books book1; // 声明 book1, 类型为 Books
struct Books book2; // 声明 book2, 类型为 Books
/* book1 详述 */
strcpy(book1.title, "C Programming");
strcpy(book1.author, "Nuha Ali");
strcpy(book1.subject, "C Programming Tutorial");
book1.book_id = 6495407;
/* book2 详述 */
strcpy(book2.title, "Telecom Billing");
strcpy(book2.author, "Zara Ali");
strcpy(book2.subject, "Telecom Billing Tutorial");
book2.book_id = 6495700;
/* 输出 book1 信息 */
printBook(&book1);
/* 输出 book2 信息 */
printBook(&book2);
return 0;
}
void printBook(struct Books *book) {
printf("Book title: %s\n", book->title);
printf("Book author: %s\n", book->author);
printf("Book subject: %s\n", book->subject);
printf("Book book_id: %d\n", book->book_id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 结构体大小的计算
C语言中,可以使用sizeof
运算符来计算结构体的大小,sizeof
返回的是给定类型或变量的字节大小。对于结构体,sizeof
将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节,如下:
#include <stdio.h>
struct Person {
char name[20];
int age;
float height;
};
int main() {
struct Person person;
printf("结构体 Person 大小为: %zu 字节\n", sizeof(person));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
以上实例中,定义了一个名为Person
的结构体,它包含了一个字符数组name
,一个整数age
和一个浮点数height
,在main
函数中,声明了一个Person
类型的变量person
,然后使用sizeof
运算符来获取person
结构体的大小,最后使用printf
函数打印除结构体的大小。
注意
结构体的大小可能会收到编译器的优化和对齐规则的影响,编译器可能会在结构体中插入一些额外的填充字节以对齐结构体的成员变量,以提高内存访问效率。因此,结构体的实际大小可能会大于成员变量大小的总和,如果需要确切地了解结构体的内存布局和对齐方式,可以使用offsetof
宏和__attribute__((packed))
属性等进一步控制和查询结构体的大小和对齐方式。
# C 共用体
公用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型,开发者可以定义一个带有多个成员的共用体,但是任何时候只能有一个成员带有值,共用体提供了一种使用相同的内存位置的有效方式。
# 定义公用体
为了定义公用体,必须使用union
语句,方式与定义结构类似。union
语句定义了一个新的数据类型,带有多个成员,union
语句的格式如下:
union [union tag] {
member definition;
member definition;
...
member definition;
} [one or more union variables];
2
3
4
5
6
union tag
是可选的,每个member definition
是标准的变量定义,比如int i;
或者float f;
或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,也是可选的,下面定义一个名为Data
的共用体类型,有三个成员i
、f
和str
:
union Data {
int i;
float f;
char str[20];
} data;
2
3
4
5
现在,Data
类型的变量可以存储一个整数、一个浮点数或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据,可以根据需要在一个公用体内使用任何内置的或者用户自定义的数据类型。共用体占用的内存应足够存储共用体中最大的成员,如,在上面的实例中,Data
将占用20个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的,如下实例:
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
printf("Memory size occupied by data: %lu\n", sizeof(data));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 访问共用体成员
为了访问共用体的成员,可以使用成员访问运算符(.),可以使用union
关键字来定义共用体类型的变量,如下:
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
data.f = 220.5;
strcpy(data.str, "C Programming");
printf("data.i: %d\n", data.i);
printf("data.f: %f\n", data.f);
printf("data.str: %s\n", data.str);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在这里,可以看到共用体的i
和f
成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是str
成员能够完好输出的原因,再来看一个相同的实例,这次在同一时间只使用一个变量,如:
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i: %d\n", data.i);
data.f = 220.5;
printf("data.f: %f\n", data.f);
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
# C 位域
C语言的位域(bit-field)是一种特殊的结构体成员,运行我们按位对成员进行定义,指定其占用的位数,如果程序的结构中包含多个开关的变量,即变量值为TRUE/FALSE
,如:
struct {
unsigned int widthValidated;
unsigned int heightValidated;
} status;
2
3
4
这种结构需要8字节的内存空间,但实际上,在每个变量中,我们只存储0或1,这种情况下,C语言提供了一种更好的利用内存空间的方式,如果在结构中使用这样的变量,可以定义变量的宽度来告诉编译器,如:
struct {
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
} status;
2
3
4
现在,上面的结构中,status
变量将占用4个字节的内存空间,但是只有2位被用来存储值,如果使用了32个变量,每一个变量宽度为1位,那么status
结构将使用4个字节,但只要再多用一个变量,如果使用了33个变量,那么它将分配内存的下一段来存储第33个变量,这个时候就开始使用8个字节,如下:
#include <stdio.h>
#include <string.h>
/* 定义简单的结构 */
struct {
unsigned int widthValidated;
unsigned int heightValidated;
} status1;
/* 定义位域结构 */
struct {
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
} status2;
int main() {
printf("Memory size occupied by status1: %ld\n", sizeof(status1));
printf("Memory size occupied by status2: %ld\n", sizeof(status2));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
位域的特点和使用方法如下:
- 定义位域时,可以指定成员的位域宽度,即成员所占用的位数
- 位域的宽度不能超过其数据类型的大小,因为位于必须适应所使用的整数类型
- 位域的数据类型可以是
int
、unsigned int
、signed int
等整数类型,也可以是枚举类型 - 位域可以单独使用,也可以与其他成员一起组成结构体
- 位域的访问是通过点运算符(
.
)来实现,与普通的结构体成员访问方式相同
# 位域声明
有些信息在存储时,并不需要占用一个完整的字节,而只需占用几个或一个二进制位,如在存放一个开关变量时,只有0和1两种状态,用1位二进制即可,为了节省存储空间,并使处理简便,C语言又定义了一种数据结构,称为位域或位段。
所谓位域是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位数,每个域有一个域名,允许在程序中按域名进行操作,这样就可以把几个不同的对象用一个字节的二进制位域来表示。典型的实例:
- 用1位二进制位存放一个开关变量,只有0和1两种状态
- 读取尾部文件格式--可以读取非标准的文件格式,如,9位的整数
位于的定义形式为:
struct 位域结构名 {
位域列表
};
2
3
其中位域列表的形式为:
type [member_name] : width;
下面是有关位域中变量元素的描述:
位域 | 描述 |
---|---|
type | 只能为int 、unsigned int 、signed int 三种类型,决定了如何解释位域的值 |
member_name | 位域的名称 |
width | 位域中位的数量,宽度必须小于或等于指定类型的位宽度 |
带有预定义宽度的变量被称为位域,位域可以存储多于1位的数,如,需要一个变量来存储从0到7的值,可以定义一个宽度为3位的位域,如:
struct {
unsigned int age : 3;
} Age;
2
3
上面的结构定义指示C编译器,age
变量将只使用3位来存储这个值,如果试图使用超过3位,则无法完成
struct bs {
int a : 8;
int b : 2;
int c : 6;
} data;
2
3
4
5
以上代码定义了一个名为struct bs
的结构体,data
为bs
的结构体变量,共占四个字节:对于位域来说,它们的宽度不能超过其数据类型的大小,在这种情况下,int
类型的大小通常是4
个字节(32位)。相邻位域字段的类型相同,且其位宽之和小于类型的sizeof
大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止,如下实例:
struct packed_struct {
unsigned int f1 : 1;
unsigned int f2 : 1;
unsigned int f3 : 1;
unsigned int f4 : 1;
unsigned int type : 4;
unsigned int my_int : 9;
} pack;
2
3
4
5
6
7
8
以上代码定义了一个名为packed_struct
的结构体,其中包含了六个成员变量,pack
为packed_struct
的结构体变量。在这里,packed_struct
包含了6个成员:四个1位的标识符f1、f2、f3、f4
、一个4位的type
和一个9位的my_int
。
#include <stdio.h>
struct packed_struct {
unsigned int f1 : 1; // 1位的位域
unsigned int f2 : 1; // 1位的位域
unsigned int f3 : 1; // 1位的位域
unsigned int f4 : 1; // 1位的位域
unsigned int type : 4; // 4位的位域
unsigned int my_int : 9; // 9位的位域
};
int main() {
struct packed_struct pack;
pack.f1 = 1;
pack.f2 = 0;
pack.f3 = 1;
pack.f4 = 0;
pack.type = 7;
pack.my_int = 255;
printf("f1: %u\n", pack.f1);
printf("f2: %u\n", pack.f2);
printf("f3: %u\n", pack.f3);
printf("f4: %u\n", pack.f4);
printf("type: %u\n", pack.type);
printf("my_int: %u\n", pack.my_int);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
struct {
unsigned int age : 3;
} Age;
int main() {
Age.age = 4;
printf("sizeof(Age): %d\n", sizeof(Age));
printf("Age.age: %d\n", Age.age);
Age.age = 7;
printf("Age.age: %d\n", Age.age);
Age.age = 8; // 二进制表示为1000有四位,超出
printf("Age.age: %d\n", Age.age);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
计算字节数:
#include <stdio.h>
struct example1 {
int a : 4;
int b : 5;
int c : 7;
};
int main() {
struct example1 ex1;
printf("Size of example1: %lu bytes\n", sizeof(ex1));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以上实例中,example1
结构体包含三个位域成员a
,b
和c
,它们分别占用4位、5位和7位。通过sizeof
运算符计算出example1
结构体的字节数,并输出结果:
对于位域的定义尚有以下几点说明
- 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域,也可以有意使某位域从下一单元开始,如:
struct bs {
unsigned a : 4;
unsigned : 4; /* 空域 */
unsigned b : 4; /* 从下一个单元开始存放 */
unsigned c : 4;
}
/**
* 在这个位域定义中,a占第一个字节的4位,后4位填0表示不使用,b从第二个字节开始,占用4位,c占用4位。
*/
2
3
4
5
6
7
8
9
- 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,
:
后面的数字不能超过这个长度 - 位域可以是无名位域,这时它只用来作填充或调整位置,无名的位域是不能使用的,如:
struct k {
int a : 1;
int : 2; /* 该 2 位不能使用 */
int b : 3;
int c : 2;
}
2
3
4
5
6
从以上分析可以看出,位域在本质上就是一种数据类型,不过其成员是按二进位分配的。
# 位域的使用
位域的使用和结构成员的使用相同,其一般形式为:
位域变量名.位域名
位域变量名->位域名
2
3
位域允许用各种格式输出。
实例:
#include <stdio.h>
int main() {
struct bs {
unsigned a : 1;
unsigned b : 3;
unsigned c : 4;
} bit, *pbit;
bit.a = 1; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.b = 7; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.c = 15; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
printf("%d, %d, %d \n", bit.a, bit.b, bit.c); /* 以整型量格式输出三个域的内容 */
pbit = &bit; /* 把位域变量 bit 的地址送给指针变量 pbit */
pbit->a = 0; /* 用指针方式给位域 a 重新赋值, 赋值为 0 */
pbit->b &= 3; /* 使用复合的位运算符 "&=",相当于: pbit->b = pbit->b & 3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3 (111 & 011 = 011,十进制为 3) */
pbit-> c |= 1; /* 使用复合运算符 "|=" 相当于: pbit-c = pbit->c | 1,其结果为 15 */
printf("%d, %d, %d \n", pbit->a, pbit->b, pbit->c); /* 用指针方式输出三个位域的值 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上例程序中定义了位域结构 bs
,三个位域为a
、b
、c
。说明了bs
类型的变量bit
和指向bs
类型的指针变量pbit
,这表示位域也可以使用指针。
# C typedef
C语言提供了typedef
关键字,可以使用它来为类型取一个新的名字,下面的实例为单字节数定义了一个术语BYTE
:
typedef unsigned char BYTE;
在这个类型之后,标识符BYTE
可作为类型unsigned char
的缩写,如:
BYTE b1, b2;
按照管理,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但也可以使用小写字母,如:
typedef unsigned char byte;
可以使用typedef
来为用户自定义的数据类型取一个新的名字,如:可以对结构体使用typedef
来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如:
#include <stdio.h>
#include <string.h>
typedef struct Books {
char title[50];
char author[50];
char subject[100];
int book_id;
} Book;
int main() {
Book book;
strcpy(book.title, "C 教程");
strcpy(book.author, "Runoob");
strcpy(book.subject, "编程语言");
book.book_id = 12345;
printf("书标题 : %s \n", book.title);
printf("书作者 : %s \n", book.author);
printf("书类目 : %s \n", book.subject);
printf("书 ID : %d \n", book.book_id);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# typedef vs #define
#define
是 C 指令,用于为各种数据类型定义别名,与typedef
类似,但是它们有以下几点不同:
typedef
仅限于为类型定义符号名称,#define
不仅可以为类型定义别名,也能为数值定义别名,比如可以定义1
为ONE
typedef
是由编译器执行解释的,#define
语句是由预编译器进行处理的。
下面是#define
的最简单的用法:
#include <stdio.h>
#define TRUE 1
#define FALSE 0
int main() {
printf("TRUE 的值: %d \n", TRUE);
printf("FALSE 的值: %d \n", FALSE);
return 0;
}
2
3
4
5
6
7
8
9
10
11
当上面的代码被编译和执行时,它会产生下列结果:
# C 输入 & 输出
输入:意味着要想程序填充一些数据,输入可以以文件的形式或从命令行中进行,C语言提供了一系列内置的函数来读取给定的输入,并根据需要填充到程序中。
输出:意味着要在屏幕上、打印机上或任意文件中显示一些数据。C语言提供了一系列内置的函数来输出数据到计算机屏幕上和保存数据到文本文件或二进制文件中。
# 标准文件
C语言会把所有的设备都当做文件,所以设备(比如显示器)被处理的方式与文件相同,以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。
标准文件 | 文件指针 | 设备 |
---|---|---|
标准输入 | stdin | 键盘 |
标准输出 | stdout | 屏幕 |
标准错误 | stderr | 屏幕 |
文件指针是访问文件的方式,本届将讲解如何从键盘上读取值以及如何把结果输出到屏幕上,C语言中的I/O(输入/输出)
通常使用printf()
和scanf()
两个函数,scanf()
函数用于从标准输入(键盘)读取并格式化,printf()
函数发送格式化输出到标准输出(屏幕)。
#include <stdio.h> // 执行 printf() 函数需要该库
int main() {
printf("菜鸟教程"); // 显示引号中的内容
return 0;
}
2
3
4
5
实例解析:
- 所有的C语言程序都需要包含
main()
函数,代码从main()
函数开始执行 printf()
用于格式化输出到屏幕,printf()
函数在stdio.h
头文件中声明stdio.h
是一个头文件(标准输入输出头文件),#include
是一个预处理命令,用来引入头文件,当编译器遇到printf()
函数时,如果没有找到stdio.h
头文件,会发生编译错误return 0
用于表示退出程序
#include <stdio.h>
int main() {
int testInteger = 5;
// %d 格式化输出整数
printf("Number = %d", testInteger);
float f;
printf("Enter a number: ");
// %f 匹配浮点型数据
scanf("%f", &f);
// %f 格式化输出浮点型数据
printf("Value = %f", f);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# getchar() & putchar() 函数
int getchar(void)
函数从屏幕读取下一个可用的字符,并把它返回为一个整数,这个函数在同一时间内只会读取一个单一的字符,可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c)
函数把字符输出到屏幕上,并返回相同的字符,这个函数在同一时间内只会输出一个单一的字符,可以在循环内使用这个方法,以便在屏幕上输出多个字符。
#include <stdio.h>
int main() {
int c;
printf("Enter a value: ");
c = getchar();
printf("\n You entered: ");
putchar(c);
printf("\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
当上面的代码被编译和执行时,它会等待输入一些文本,当输入一个文本并按下回车键时,程序会继续并只读取一个单一的字符。
# gets() & puts() 函数
char *gets(char *s)
函数从stdin
读取一行到s
所指向的缓冲区,直到一个终止符或EOF
。
int puts(const char *s)
函数把字符串s
和一个尾随的换行符写入到stdout
。
#include <stdio.h>
int main() {
char str[100];
printf("Enter a value: ");
gets(str);
printf("\nYou entered: ");
puts(str);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
# scanf() 和 printf() 函数
int scanf(const char *format, ...)
函数从标准输入流stdin
读取输入,并根据提供的format
来浏览输入。
int printf(const char *format, ...)
函数把输出写入到标准输出流stdout
,并根据提供的格式产生输出。
format
可以是一个简单的常量字符串,但可以分别指定%s
、%d
、%c
、%f
等来输出或读取字符串、整数、字符或浮点数,还有许多其他可用的格式选项,可以根据需要使用。
#include <stdio.h>
int main() {
char str[100];
int i;
printf("Enter a value: ");
scanf("%s %d", str, &i);
printf("\n You entered: %s %d", str, i);
printf("\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
在这里,需要指出的是,scanf()
期待输入的格式与给出的%s
和%d
相同,这意味着必须提供有效的输入,比如string integer
,如果提供的是string, string
或integer integer
,会被认为是错误的输入,另外,在读取字符串时,只要遇到一个空格,scanf()
就会停止读取,所以this is test
对scanf()
来说是三个字符串。
# C 文件读写
本节介绍C如何创建、打开、关闭文本文件或二进制文件。一个文件,无论它是文本文件还是二进制文件,都代表了一系列的字节,C语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。
# 打开文件
可以使用fopen()
函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型FILE
的一个对象,类型FILE
包含了所有用来控制流的必要的信息,下面是这个函数调用的原型:
FILE *fopen(const char *filename, const char *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);
如果成功关闭文件,fclose()
函数返回零,如果关闭文件时发生错误,函数返回EOF
,这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存,EOF
是一个定义在头文件stdio.h
中的常量。C标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。
# 写入文件
下面是把字符写入到流中的最简单的函数:
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;
// 需确保有可用的tmp目录,如果不存在该目录,需要在计算机上先创建该目录
// /tmp 一般是 Linux 系统上的临时目录,如果在 Windows 系统上运行,需要修改为
// 本地环境中已存在的目录,如:C:\tmp、D:\tmp 等
fp = fopen("/tmp/test.txt", "w+");
fprintf(fp, "This is testing for fprintf ... \n");
fputs("This is testing for fputs ... \n", fp);
fclose(fp);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当上面的代码被编译和执行时,会在/tmp
目录中创建一个新的文件test.txt
,并使用两个不同的函数写入两行。
# 读取文件
下面是从文件读取单个字符的最简单的函数:
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, ...)
函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。
#include <stdio.h>
int main() {
FILE *fp = NULL;
char buff[255];
fp = fopen("/tmp/test.txt", "r");
fscanf(fp, "%s", buff);
printf("1: %s\n", buff);
fgets(buff, 255, (FILE*) fp);
printf("2: %s\n", buff);
fgets(buff, 255, (FILE*) fp);
printf("3: %s\n", buff);
fclose(fp);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当上面的代码被编译和执行时,它会读取上一部分创建的文件,产生下列结果:
首先,fscanf()
方法只读取了This
,因为它在后边遇到了一个空格,其次,调用fgets()
读取剩余的部分,直到行尾,最后,调用fgets()
完整地读取第二行。
# 二进制 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);
2
这两个函数都是用于存储块的读写-通常是数组或结构体。
# C 预处理器
C 预处理器不是编译器的组成部分,但它是编译过程中一个单独的步骤,简言之,C预处理器只不过是一个文本替换工具而已,它会指示编译器在实际编译之前完成所需的预处理,C预处理器(C Preprocessor)简写为CPP。
所有的预处理器命令都是以#
开头,它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始,下面列出了所有重要的预处理器指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的#if 给定条件不为真,当前条件为真,则编译下面的代码 |
#endif | 结束一个#if ...... #else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
# 预处理器实例
分析下面的实例来理解不同的指令。
#define MAX_ARRAY_LENGTH 20
这个指令告诉CPP
把所有的MAX_ARRAY_LENGTH
定义为20
,使用#define
定义常量来增强可读性。
#include <stdio.h>
#include "myheader.h"
2
这些指令告诉CPP
从系统库
中获取stdio.h
,并添加文本到当前的源文件中,下一行告诉CPP
从本地目录中获取myheader.h
,并添加内容到当前的源文件中。
#undef FILE_SIZE
#define FILE_SIZE 42
2
这个指令告诉CPP
取消已定义的FILE_SIZE
,并定义它为42。
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endif
2
3
这个指令告诉CPP
只有当MESSAGE
未定义时,才定义MESSAGE
。
#ifdef DEBUG
/* Your debugging statements here */
#endif
2
3
这个指令告诉CPP
如果定义了DEBUG
,则执行处理语句,在编译时,如果向gcc
编译器传递了-DDEBUG
开关量,这个指令就非常有用,它定义了DEBUG
,可以在编译期间随时开启和关闭调试。
# 预定义宏
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;
}
2
3
4
5
6
7
8
9
10
当上面的代码被编译和执行时,它会产生下列结果:
# 预处理器运算符
C预处理器提供了下列的运算符来帮助创建宏:
- 宏延续运算符(
\
)
一个宏通常写在一个单行上,如果宏太长,一个单行容纳不下,则使用宏延续运算符(\
),如:
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
2
- 字符串常量化运算符(
#
)
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#
),在宏中使用的该运算符有一个特定的参数或参数列表,如:
#include <stdio.h>
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
int main(void) {
message_for(Carole, Debra);
return 0;
}
2
3
4
5
6
7
8
9
- 标记粘贴运算符(
##
)
宏定义内的标记粘贴运算符(##
)会合并两个参数,它允许在宏定义中两个独立的标记被合并为一个标记,如:
#include <stdio.h>
#define tokenpaster(n) printf("token" #n " = %d\n", token##n)
int main(void) {
int token34 = 40;
tokenpaster(34);
return 0;
}
2
3
4
5
6
7
8
9
这样的结果是怎么产生的呢? 因为这个实例会从编译器产生下列的实际输出:
printf("token34 = %d\n", token34);
这个实例演示了token##n
会连接到token34
中,这里,使用了字符串常量化运算符(#)和标记粘贴运算符(##)。
defined()
运算符
预处理器defined
运算符是用在常量表达式中的,用来确定一个标识符是否已经使用#define
定义过,如果指定的标识符已定义,则值为真(非零),如果指定的标识符未定义,则值为假(零),下面的实例演示了defined()
运算符的用法:
#include <stdio.h>
#if !defined(MESSAGE)
#define MESSAGE "You wish!"
#endif
int main(void) {
printf("Here is the message: %s\n", MESSAGE);
return 0;
}
2
3
4
5
6
7
8
9
10
# 参数化的宏
CPP一个强大的功能是可以使用参数化的宏来模拟函数,如,下面的代码是计算一个数的平方:
int square(int x) {
return x * x;
}
2
3
可以使用宏重写上面的代码,如:
#define square(x) ((x) * (x))
在使用带有参数的宏之前,必须使用#define
指令定义,参数列表是括在圆括号内,且必须紧跟在宏名称的后边,宏名称和左圆括号之间不允许有空格,如:
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main(void) {
printf("Max between 20 and 10 is %d\n", MAX(10, 20));
return 0;
}
2
3
4
5
6
7
8
# C 头文件
头文件是扩展名为.h
的文件,包含了C函数声明和宏定义,被多个源文件中引用共享,有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。在程序中使用头文件,需要使用C预处理指令#include
来引用它,前面已经看过stdio.h
头文件,它是编译器自带的头文件,引用头文件相当于复制头文件的内容,但是不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别是在程序是由多个源文件组成的时候。A simple practice in C或*C++*程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
# 引用头文件的语法
使用预处理指令#include
可以引用用户和系统头文件,它的形式有以下两种:
#include <file>
这种形式用于引用系统头文件,它在系统目录的标准列表中搜索名为file
的文件,在编译源代码时,可以通过-l
选项把目录前置在该列表前。
#include "file"
这种形式用于引用用户头文件,它在包含当前文件的目录中搜索名为file
的文件,在编译源代码时,可以通过-l
选项把目录前置在该列表前。
# 引用头文件的操作
#include
指令会指示C预处理器浏览指定的文件作为输入,预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及#include
指令之后的文本输出,如,一个头文件header.h
,如下:
char *test(void);
和一个使用了头文件的主程序program.c
,如下:
int x;
#include "header.h"
int main(void) {
puts(test());
}
2
3
4
5
6
编译器会看到如下的代码信息:
int x;
char *test(void);
int main(void) {
puts(test());
}
2
3
4
5
6
# 只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误,为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如:
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
2
3
4
这种结构就是通常所说的包装器#ifndef
,当再次引用头文件时,条件为假,因为HEADER_FILE
已定义,此时,预处理器会跳过文件的整个内容,编译器会忽略它。
# 有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中,如,需要指定在不同的操作系统上使用的配置参数,可以通过一系列条件来实现这点,如:
#if SYSTEM_1
#include "system_1.h"
#elif SYSTEM_2
#include "system_2.h"
#elif SYSTEM_3
...
#endif
2
3
4
5
6
7
但是如果头文件比较多的时候,这么做就不太妥当,预处理器使用宏来定义头文件的名称,这就是所谓的有条件引用。它不是用头文件的名称作为#include
的直接参数,只需要使用宏名称代替即可:
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H
2
3
SYSTEM_H
会扩展,预处理器会查找system_1.h
,就像#include
最初编写的那样,SYSTEM_H
可通过-D
选项被Makefile
定义。
# C 强制类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型,如,如果向存储一个long
型的值到一个简单的整型中,需要把long
类型强制转换为int
类型,可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下:
(type_name) expression
#include <stdio.h>
int main() {
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;
printf("Value of mean: %f\n", mean);
}
2
3
4
5
6
7
8
9
这里需要注意的是强制类型转换运算符的优先级大于除法,因此sum
的值首先被转换为double
型,然后除以count
,得到一个类型为double
的值。类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定,在编程时,有需要类型转换的时候用上强制类型转换运算符,是一种良好的编程习惯。
# 整数提升
整数提升是指把小于int
或unsigned int
的整数类型转换为int
或unsigned int
的过程,如:
#include <stdio.h>
int main() {
int i = 17;
char c = 'c'; /* ascii 值是 99 */
int sum;
sum = i + c;
printf("Value of sum : %d\n", sum); /* 输出 116 */
}
2
3
4
5
6
7
8
9
10
# 常用的算术转换
常用的算术转换是隐式地把值强制转换为相同的类型,编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:
常用的算术转换不适用于赋值运算符、逻辑运算符&&
和||
,让看看下面的实例来理解这个概念:
int main() {
int i = 17;
char c = 'c'; /* ascii 值是 99 */
float sum;
sum = i + c;
printf("Value of sum: %f\n", sum);
}
2
3
4
5
6
7
8
在这里,c
首先被转换为整数,但是由于最后的值是float
型的,所以会应用常用的算术转换,编译器会把i
和c
转换为浮点型,并把它们相加得到一个浮点数。
# C 错误处理
C语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许访问底层数据,在发生错误时,大多数的C或UNIX函数调用返回1或NULL,同时会设置一个错误代码errno
,该错误代码是全局变量,表示在函数调用期间发生了错误,开发者可以在errno.h
头文件中找到各种各样的错误代码。所以,C程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作,开发人员应该在程序初始化时,把errno
设置为0
,这是一种良好的编程习惯,0
值表示程序中没有错误。
# errno、perror() 和 strerror()
C语言提供了perror()
和strerror()
函数来显示与errno
相关的文本消息。
perror()
函数显示传给它的字符串,后跟一个冒号、一个空格和当前errno
值的文本表示形式strerror()
函数,返回一个指针,指针指向当前errno
值的文本表示形式
下面来模拟一种错误情况,尝试打开一个不存在的文件,开发者可以使用多种方式来输出错误消息,在这里使用函数来演示用法,另外需要注意,应该使用stderr
文件流来输出所有的错误。
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno;
int main() {
FILE *pf;
int errnum;
pf = fopen("unexist.txt", "rb");
if (pf == NULL) {
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror(errnum));
} else {
fclose(pf);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 被零除的错误
在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。为了避免这种情况发生,下面的代码在进行除法运算前会先检查除数是否为零:
#include <stdio.h>
#include <stdlib.h>
int main() {
int dividend = 20;
int divisor = 0;
int quotient;
if (divisor == 0) {
fprintf(stderr, "除数为 0 退出运行 ... \n");
exit(-1);
}
quotient = dividend / divisor;
fprintf(stderr, "quotient 变量的值: %d\n", quotient);
exit(0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 程序退出状态
通常情况下,程序成功执行完一个操作正常退出的时候会带有值EXIT_SUCCESS
,这里,EXIT_SUCCESS
是宏,它被定义为0
,如果程序中存在一种错误情况,当退出程序时,会带有状态值EXIT_FAILURE
,被定义为-1
,因此,上面的程序可以写成:
#include <stdio.h>
#include <stdlib.h>
int main() {
int dividend = 20;
int divisor = 5;
int quotient;
if (divisor == 0) {
fprintf(stderr, "除数为 0 退出运行 ...\n");
exit(EXIT_SUCCESS);
}
quotient = dividend / divisor;
fprintf(stderr, "quotient 变量的值为: %d\n", quotient);
exit(EXIT_SUCCESS);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# C 递归
递归指的是在函数的定义中使用函数自身的方法,语法格式:
void recursion() {
statements;
... ...
recursion(); /* 函数调用自身 */
}
int main() {
recursion();
}
2
3
4
5
6
7
8
9
C语言支持递归,即一个函数可以调用其自身,但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环,递归函数在解决许多数学问题上起了至关重要的作用,比如计算一个数的阶乘、生成斐波那契数列,等等。
# 数的阶乘
下面的实例使用递归函数计算一个给定的数的阶乘:
#include <stdio.h>
double factorial(unsigned int i) {
if (i <= 1) {
return 1;
}
return i * factorial(i - 1);
}
int main() {
int i = 15;
printf("%d 的阶乘为 %f\n", i, factorial(i));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 斐波那契数列
下面的实例使用递归函数生成一个给定的数的斐波那契数列:
#include <stdio.h>
int fibonaci(int i) {
if (i == 0) {
return 0;
}
if (i == 1) {
return 1;
}
return fibonaci(i - 1) + fibonaci(i - 2);
}
int main() {
int i;
for (i = 0; i < 10; i++) {
printf("%d\t\n", fibonaci(i));
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# C 可变参数
有时,开发者可能会碰到这样的情况,其希望函数带有可变数量的参数,而不是预定义数量的参数,C语言为这种情况提供了一个解决方案,它允许定义一个函数,能根据具体的需求接受可变数量的参数,声明方式:
int func_name(int arg1, ...);
其中,...
表示可变参数列表,下面的实例演示了这种函数的使用:
int func(int, ...) {
...
}
int main() {
func(2, 2, 3);
func(3, 2, 3, 4);
}
2
3
4
5
6
7
8
请注意,函数func()
最后一个参数写成省略号,即三个点号(...
),省略号之前的那个参数是int
,代表了要传递的可变参数的总数。为了使用这个功能,需要使用stdarg.h
头文件,该文件提供了实现可变参数功能的函数和宏,具体步骤如下:
- 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数
- 在函数定义中创建一个
va_list
类型变量,该类型是在stdarg.h
头文件中定义的 - 使用
int
参数和va_start()
宏来初始化va_list
变量为一个参数列表,宏va_start()
是在stdarg.h
头文件中定义的 - 使用
va_arg()
宏和va_list
变量来访问参数列表中的每个项 - 使用宏
va_end()
来清理赋予va_list
变量的内存
常用的宏有:
va_start(ap, last_arg)
:初始化可变参数列表,ap
是一个va_list
类型的变量,last_arg
是最后一个固定参数的名称(也就是可变参数列表之前的参数),该宏将ap
指向可变参数列表中的第一个参数va_arg(ap, type)
:获取可变参数列表中的下一个参数,ap
是一个va_list
类型的变量,type
是下一个参数的类型,该宏返回类型为type
的值,并将ap
指向下一个参数va_end(ap)
:结束可变参数列表的访问,ap
是一个va_list
类型的变量,该宏将ap
置为NULL
#include <stdio.h>
#include <stdarg.h>
double average(int num, ...) {
va_list valist;
double sum = 0.0;
int i;
/* 为 num 个参数初始化 valist */
va_start(valist, num);
/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++) {
sum += va_arg(valist, int);
}
/* 清理为 valist 保留的内存 */
va_end(valist);
return sum/num;
}
int main() {
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2, 3, 4, 5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5, 10, 15));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在上面的例子中,average()
函数接受一个整数num
和任意数量的整数参数,函数内部使用va_list
类型的变量va_list
来访问可变参数列表。在循环中,每次使用va_arg()
宏获取下一个整数参数,并输出,最后,在函数结束时使用va_end()
宏结束可变参数列表的访问。
# C 内存管理
本节讲解C中的动态内存管理,C语言为内存的分配和管理提供了几个函数,这些函数可以在<stdlib.h>
头文件中找到。在C语言中,内存是通过指针变量来管理的,指针是一个变量,它存储了一个内存地址,这个内存地址可以指向任何数据类型的变量,包括整数、浮点数、字符和数组等。C语言提供了一些函数和运算符,使得程序员可以对内存进行操作,包括分配、释放、移动和复制等。
函数 | 描述 |
---|---|
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 *
类型可以通过类型转换强制转换为任何其他类型的指针。
# 动态分配内存
编程时,如果预先指导数组的大小,那么定义数组时就比较容易,如,一个存储人名的数组,它最多容纳100个字符,所以可以按如下定义数组:
char name[100];
但是,如果预先不知道需要存储的文本长度,如想存储有关一个主题的详细描述,这里,就需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 动态分配内存 */
description = (char *) malloc(200 * sizeof(char));
if (description == NULL) {
fprintf(stderr, "Error - unable to allocate required memory\n");
} else {
strcpy(description, "Zara ali a DPS student in class 10th");
}
printf("Name: %s\n", name);
printf("Description: %s\n", description);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上面的程序也可以使用calloc()
编写,只需要把malloc
替换为calloc
即可,如下:
calloc(200, sizeof(char));
当动态分配内存时,开发者有完全控制权,可以传递任何大小的值,而哪些预先定义了大小的数组,一旦定义则无法改变大小。
# 重新调整内存的大小和释放内存
当程序退出时,操作系统会自动释放所有分配给程序的内存,但,依旧建议在不需要内存时,都应该调用函数free()
来释放内存,或者,通过调用函数realloc()
来增加或减少已分配的内存块的大小,如下实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char name[100];
char *description;
strcpy(name, "Zara Ali");
/* 动态分配内存 */
description = (char *) malloc(30 * sizeof(char));
if (description == NULL) {
fprintf(stderr, "Error - unable to allocate required memory\n");
} else {
strcpy(description, "Zara ali a DPS student.");
}
/* 假设想要存储更大的描述信息 */
description = (char *) realloc(description, 100 * sizeof(char));
if (description == NULL) {
fprintf(stderr, "Error - unable to allocate required memory\n");
} else {
strcat(description, "She is in class 10th");
}
printf("Name: %s\n", name);
printf("Description: %s\n", description);
/* 使用 free() 函数释放内存 */
free(description);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
当然也可以尝试不分配额外的内存,strcat()
函数会生成一个错误,因为存储description
时可用的内存不足。
# C 语言中常用的内存管理函数和运算符
malloc()
:用于动态分配内存,它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针free()
:用于释放先前分配的内存,它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态calloc()
:用于动态分配内存,并将其初始化为零,它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向内存的指针。realloc()
:用于重新分配内存,它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配内存块的大小,如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。sizeof()
:用于获取数据类型或变量的大小(以字节为单位)- 指针运算符:用于获取指针所指向的内存地址或变量的值
&
运算符:用于获取变量的内存地址*
运算符:用于获取指针所指向的变量的值->
运算符:用于指针访问结构体成员,语法为pointer->member
,等价于(*pointer).member
memcpy()
:用于从源内存区域赋值数据到目标内存区域,它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)memmove()
:类似memcpy()
,但它可以处理重叠的内存区域,它接受三个参数,即目标区域的指针、源内存区域指针和要复制的数据大小(以字节为单位)
# C 命令行参数
执行程序时,可以从命令行传值给C程序,这些值被称为命令行参数,它们对程序很重要,特别是想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要。命令行参数是使用main()
函数参数来处理的,其中,argc
是指传入参数的个数,argv[]
是一个指针数组,指向传递给程序的每个参数。下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc == 2) {
printf("The argument supplied is %s\n", argv[1]);
} else if (argc > 2) {
printf("Too many arguments supplied.\n");
} else {
printf("One argument expected.\n");
}
}
2
3
4
5
6
7
8
9
10
11
应当指出的是,argv[0]
存储程序的名称,argv[1]
是一个指向第一个命令行参数的指针,*argv[n]
是最后一个参数。如果没有提供任何参数,argc
将为1
,否则,如果传递了一个参数,argc
将被设置为2
。多个命令行参数之间用空格分割,但是如果参数本身带有空格,那么传递参数的时候应把参数放置在双引号""
或单引号''
内部。
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Program name %s\n", argv[0]);
if (argc == 2) {
printf("The argument supplied is %s\n", argv[1]);
} else if (argc > 2) {
printf("Too many arguments supplied.\n");
} else {
printf("One argument expected.\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
# C 排序算法
# 冒泡排序
冒泡排序(英语:Bubble Sort)是一种简单的排序算法,它重复地走过要排序的数列,一次比较两个元素,如果它们的顺序(如从大到小、首字母从A到Z)错误就把它们交换过来。
#include <stdio.h>
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++) {
for (j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
int i;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 选择排序
选择排序(Selection sort)是一种简单直观的排序算法,它的工作原理如下,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾,依次类推,直到所有元素均排序完毕。
# 插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到响应位置并插入,插入排序在实现上,通常采用in-place
排序(即只需用到O(1)
的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
void insertion_sort(int arr[], int len) {
int i, j, temp;
for (i = 1; i < len; i++) {
temp = arr[i];
for (j = i; j > 0 && arr[j - 1] > temp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
2
3
4
5
6
7
8
9
10
# 希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本,希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap = gap >> 1) {
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 归并排序
把数据分为两段,从两段中逐个选最小的元素一如新数据段的末尾,可从上到下或从下到上进行。过程演示:
- 迭代法
#include <stdio.h>
#include <stdlib.h>
// 归并排序
int min(int x, int y) {
return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
int* a = arr;
int* b = (int*) malloc(len * sizeof(int));
int seg, start;
for (seg = 1; seg < len; seg += seg) {
for (start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2) {
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
}
while (start1 < end1) {
b[k++] = a[start1++];
}
while (start2 < end2) {
b[k++] = a[start2++];
}
}
int* temp = a;
a = b;
b = temp;
}
if (a != arr) {
int i;
for (i = 0; i < len; i++) {
b[i] = a[i];
}
b = a;
}
free(b);
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
// 归并排序
merge_sort(arr, len);
int i;
for (i = 0; i < len; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- 递归法
#include <stdio.h>
#include <stdlib.h>
// 递归法
void merge_sort_recursive(int arr[], int reg[], int start, int end) {
if (start >= end) {
return;
}
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2) {
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while (start1 <= end1) {
reg[k++] = arr[start1++];
}
while (start2 <= end2) {
reg[k++] = arr[start2++];
}
for (k = start; k <= end; k++) {
arr[k] = reg[k];
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
int reg[len];
merge_sort_recursive(arr, reg, 0, len - 1);
int i;
for (i = 0; i < len; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 快速排序
在区间中随机挑选一个元素作基准,将小于基准的元素放在基准之前,大于基准的元素放在基准之后,再分别对小数区与大数区进行排序。过程演示:
- 迭代法
#include <stdio.h>
#include <stdlib.h>
void quick_sort(int arr[], const int len) {
if (len <= 0) {
return; // 避免len等于负值时引发段错误(Segment_Fault)
}
// r[]模拟列表,p为数量,r[p++]为push,r[--p]为pop且取得元素
Range r[len];
int p = 0;
r[p++] = new_Range(0, len - 1);
while (p) {
Range range = r[--p];
if (range.start >= range.end) {
continue;
}
int mid = arr[(range.start + range.end) / 2]; // 选取中间点为基准点
int left = range.start, right = range.end;
do {
while (arr[left] < mid) ++left; // 检测基准点左侧是否符合要求
while (arr[right] > mid) --right; // 检测基准点右侧是否符合要求
if (left <= right) {
swap(&arr[left], &arr[right]);
left++; right--; // 移动指针以继续
}
} while (left <= right);
if (range.start < right) r[p++] = new_Range(range.start, right);
if (range.end > left) r[p++] = new_Range(left, range.end);
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
// 快速排序
quick_sort(arr, len);
int i;
for (i = 0; i < len; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- 递归法
#include <stdio.h>
#include <stdlib.h>
void quick_sort_recursive(int arr[], int start, int end) {
if (start >= end) {
return;
}
int mid = arr[end];
int left = start, right = end - 1;
while (left < right) {
while (arr[left] < mid && left < right) {
left++;
}
while (arr[right] >= mid && left < right) {
right--;
}
swap(&arr[left], &arr[right]);
}
if (arr[left] >= arr[end]) {
swap(&arr[left], &arr[end]);
} else {
left++;
}
if (left) {
quick_sort_recursive(arr, start, left - 1);
}
quick_sort_recursive(arr, left + 1, end);
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
// 快速排序
quick_sort_recursive(arr, 0, len - 1);
int i;
for (i = 0; i < len; i++) {
printf("%d \n", arr[i]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- C 简介
- 关于 C
- 为什么使用 C?
- C 程序
- C11 新特性
- C 环境设置
- 文本编辑器
- C 编译器
- C 程序结构
- C Hello World 实例
- 编译 & 执行 C 程序
- C 基本语法
- C 的令牌(Token)
- 分号 ;
- 注释
- 标识符
- 关键字
- C 中的空格
- C 数据类型
- 整数类型
- 浮点类型
- void 类型
- 类型转换
- C 变量
- C 中的变量定义
- 变量初始化
- 变量不初始化
- C 中的变量声明
- C 中的左值 (Lvalues) 和 右值 (Rvalues)
- C 常量
- 整数常量
- 浮点常量
- 字符常量
- 字符串常量
- 定义常量
- C 存储类
- auto 存储类
- register 存储类
- static 存储类
- extern 存储类
- C 运算符
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符 sizeof & 三元
- C 中的运算符优先级
- C 判断
- 判断语句
- ? : 运算符 (三元运算符)
- C 循环
- 循环类型
- 循环控制语句
- 无限循环
- C 函数
- 定义函数
- 函数声明
- 调用函数
- 函数参数
- 传值调用
- 引用调用
- C 作用域规则
- 局部变量
- 全局变量
- 形式参数
- 初始化局部变量和全局变量
- C 数组
- 声明数组
- 初始化数组
- 访问数组元素
- 获取数组长度
- 数组名
- C 数组详解
- 多维数组
- 传递数组给函数
- 从函数返回数组
- 指向数组的指针
- 静态数组与动态数组
- C enum 枚举
- 枚举变量的定义
- 将整数转换为枚举
- C 指针
- 什么是指针
- 如何使用指针
- C 中的 NULL 指针
- C 指针详解
- 指针的算术运算
- 指针数组
- 指向指针的指针
- 传递指针给函数
- 从函数返回指针
- C 函数指针
- C 回调函数
- C 字符串
- C 结构体
- 定义结构体
- 结构体变量初始化
- 访问结构体成员
- 结构体作为函数参数
- 指向结构体的指针
- 结构体大小的计算
- C 共用体
- 定义公用体
- 访问共用体成员
- C 位域
- 位域声明
- 位域的使用
- C typedef
- typedef vs #define
- C 输入 & 输出
- 标准文件
- getchar() & putchar() 函数
- gets() & puts() 函数
- scanf() 和 printf() 函数
- C 文件读写
- 打开文件
- 关闭文件
- 写入文件
- 读取文件
- 二进制 I/O 函数
- C 预处理器
- 预处理器实例
- 预定义宏
- 预处理器运算符
- 参数化的宏
- C 头文件
- 引用头文件的语法
- 引用头文件的操作
- 只引用一次头文件
- 有条件引用
- C 强制类型转换
- 整数提升
- 常用的算术转换
- C 错误处理
- errno、perror() 和 strerror()
- 被零除的错误
- 程序退出状态
- C 递归
- 数的阶乘
- 斐波那契数列
- C 可变参数
- C 内存管理
- 动态分配内存
- 重新调整内存的大小和释放内存
- C 语言中常用的内存管理函数和运算符
- C 命令行参数
- C 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序