C Quickstart
C 语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验室为开发 UNIX 操作系统而设计的。C 语言现在已经成为一种广泛使用的专业语言。
- 易于学习。
- 结构化语言。
- 产生高效率的程序。
- 可以处理底层的活动。
- 可以在多种计算机平台上编译。
hello world
C 程序主要包括以下部分:
- 预处理器指令
- 函数
- 变量
- 语句 & 表达式
- 注释
下面来看一段Hello World代码:
1 |
|
- 第一行 #include <stdio.h> 是预处理器指令,告诉 C 编译器在实际编译之前要包含 stdio.h 文件。
- 下一行 int main() 是主函数,程序从这里开始执行。
- 下一行 /…/ 将会被编译器忽略,这里放置程序的注释内容。它们被称为程序的注释。
- 下一行 printf(…) 是 C 中另一个可用的函数,会在屏幕上显示消息 “Hello, World!”。
- 下一行 return 0; 终止 main() 函数,并返回值 0。
编译执行
下面这行命令将hello.c编译为可执行文件 hello,然后可以执行hello
1 | $ gcc hello.c -o hello |
注释
C 语言有两种注释方式:
//
单行注释, 以//
开始的单行注释,这种注释可以单独占一行。
1 | // 单行注释 |
/* */
这种格式的注释可以单行或多行。
1 | /* 单行注释 */ |
不能在注释内嵌套注释,注释也不能出现在字符串或字符值中。
标识符
C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)。
C 标识符内不允许出现标点字符,比如 @、$ 和 %。C 是区分大小写的编程语言。因此,在 C 中,Manpower 和 manpower 是两个不同的标识符。下面列出几个有效的标识符:
1 | mohd zara abc move_name a_123 |
关键字
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的"其它"分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支(与 if 连用) |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句(可以带参数,也可不带参数) |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
_Bool | 布尔型(C99标准新增) |
_Complex | 复数的基本类型(C99标准新增) |
_Imaginary | 虚数,与复数基本类型相似,没有实部的纯虚数(C99标准新增) |
restrict | 用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式。(C99标准新增) |
long long | 修饰int,超长整型数据,可省略被修饰的int。(C99标准新增) |
inline | 内联函数用于取代宏定义,会在任何调用它的地方展开。(C99标准新增) |
_Alignas | 用于指定内存对齐方式(C11标准新增) |
_Alignof | 用于获取类型的内存对齐方式(C11标准新增) |
_Atomic | 用于定义原子类型变量,支持并发访问(C11标准新增) |
_Generic | 用于根据参数类型选择不同的代码(C11标准新增) |
_Noreturn | 用于告诉编译器函数不会返回(C11标准新增) |
_Static_assert | 用于在编译时检查表达式的真假(C11标准新增) |
_Thread_local | 用于定义线程局部变量,每个线程都有一份独立的副本(C11标准新增) |
数据类型
数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。
C 中的类型可分为以下几种:
- 基本数据类型: 它们是算术类型,包括整型(int)、字符型(char)、浮点型(float)和双精度浮点型(double)。
- 枚举类型:它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。
- void 类型:类型说明符 void 表示没有值的数据类型,通常用于函数返回值。
- 派生类型:包括数组类型、指针类型和结构体类型。
数组类型和结构类型统称为聚合类型。函数的类型指的是函数返回值的类型。
基本数据类型
类型 | 存储大小 |
---|---|
char | 1 字节 |
unsigned char | 1 字节 |
signed char | 1 字节 |
int | 4 字节 |
unsigned int | 4 字节 |
short | 2 字节 |
unsigned short | 2 字节 |
long | 8 字节 |
unsigned long | 8 字节 |
float | 4字节 |
double | 8字节 |
long double | 16字节 |
各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。表达式 sizeof(type) 得到对象或类型的存储字节大小。
1 |
|
void
void 类型指定没有可用的值。它通常用于以下三种情况下:
- 函数返回为空,例如
void exit (int status);
- 函数参数为空, 例如
int rand(void);
- 指针指向 void, 类型为 void* 的指针代表对象的地址,而不是类型。例如,内存分配函数 void* malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。
enum
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。枚举语法定义格式为:
1 | enum 枚举名 {枚举元素1,枚举元素2,……}; |
例如:
1 | enum DAY |
第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。因为将WED定义为4,那么THU的值就是5。
枚举变量的定义
前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。
可以通过以下三种方式来定义枚举变量
1 | // 先定义枚举类型,再定义枚举变量 |
在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。不过在枚举类型连续的情况下,是可以实现有条件的遍历。以下实例使用 for 来遍历枚举的元素:
1 | enum DAY |
字符串
C 语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组。字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。
1 | char site[7] = {'A', 'B', 'C', 'D', 'E', 'F', '\0'}; |
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。C 语言中有两种类型转换:
隐式类型转换:隐式类型转换是在表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,例如,将int类型转换为long类型或float类型转换为double类型。隐式类型转换也可能会导致数据精度丢失或数据截断。
显式类型转换:显式类型转换需要使用强制类型转换运算符(type casting operator),它可以将一个数据类型的值强制转换为另一种数据类型的值。强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据丢失或截断。
1 | double d = 3.14159; |
数组
C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。数组中的特定元素可以通过索引访问,第一个索引值为 0。
声明数组
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:
1 | type arrayName [ arraySize ]; |
这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:
1 | int a[4]; |
对于 int a[4]
,a 有两种含义:
- 指向第一个元素的指针
- 指向数组的指针
- 两者的值相等,但意义不同。
在多数情况下,a 可以看做是指向第一个元素的指针,即在加减运算中表现为指向 int 类型的指针。(但注意的是,a 不是变量,不可以被赋值)。在使用sizeof(a)
时,a只能表示数组。
初始化数组
在 C 中,您可以逐个元素初始化数组,也可以使用一个初始化语句,如下所示:
1 | double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0}; |
大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。如果省略掉了数组的大小,数组的大小则为初始化时元素的个数。
访问数组元素
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:
1 | double salary = balance[9]; |
数组还支持*
访问:
1 | //balance 指向数组第一个元素地址,然后加上9个字符的偏移量得到新的地址 |
获取数组长度
数组长度可以使用 sizeof 运算符来获取数组的长度,例如:
1 | int numbers[] = {1, 2, 3, 4, 5}; |
多维数组
C 语言支持多维数组。多维数组声明的一般形式如下:
1 | type name[size1][size2]...[sizeN]; |
例如,下面的声明创建了一个二维整型数组:
1 | int a[3][4] = { |
结构体
结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。结构体定义由关键字 struct 和结构体名组成,结构体名可以根据需要自行定义。
1 | struct tag { |
在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。tag 相同的struct类型相同。例如:
1 | struct Books |
结构体变量的使用
和其它类型变量一样,对结构体变量可以在定义时指定初始值。
1 | struct Books |
访问结构的成员,我们使用成员访问运算符(.)
1 | struct Books |
可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似。
1 | struct Books* struct_pointer; |
-> 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 (*pointer).member。
共用体
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值(访问成员时,使用成员访问运算符.
)。共用体提供了一种使用相同的内存位置的有效方式。
1 | union [union tag] |
union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:
1 | union Data |
Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。
变量
变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储。变量定义指定一个数据类型,并包含了该类型的一个或多个变量的列表,如下所示:
1 | type variable_list; |
在这里,type 必须是一个有效的 C 数据类型,可以是 char、w_char、int、float、double 或任何用户自定义的对象,variable_list 可以由一个或多个标识符名称组成,多个标识符之间用逗号分隔。例如:
1 | int i, j, k; |
变量可以在声明的时候被初始化(指定一个初始值)。初始化器由一个等号,后跟一个常量表达式组成,如下所示:
1 | type variable_name = value; |
例如:
1 | int d = 3, f = 5; // 定义并初始化 d 和 f |
变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。
变量的声明有两种情况:
- 一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
- 另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:
extern int a
其中变量 a 可以在别的文件中定义的。除非有extern关键字,否则都是变量的定义。
局部与全局
- 定义在函数外部的变量是全局变量,而定义在函数内部的是局部变量。
- 全局变量保存在内存的全局存储区中,占用静态的存储单元;
- 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
- 函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用。
- 当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动对其初始化。
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | ‘\0’ |
float | 0 |
double | 0 |
pointer | NULL |
常量
在 C 中,有两种简单的定义常量的方式:
- 使用
#define
预处理器。
1 | // #define identifier value |
- 使用
const
关键字。const 声明常量要在一个语句内完成定义和初始化。
1 | // const type variable = value; |
存储类
存储类定义 C 程序中变量/函数的的存储位置、生命周期和作用域。说明符放置在它们所修饰的类型之前。
auto 存储类
auto 存储类是所有局部变量默认的存储类。定义在函数中的变量默认为 auto 存储类,这意味着它们在函数开始时被创建,在函数结束时被销毁。auto 只能用在函数内,即 auto 只能修饰局部变量。
1 | auto int month; |
register 存储类
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个字),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
register 存储类定义存储在寄存器,所以变量的访问速度更快,但是它不能直接取地址,因为它不是存储在 RAM 中的。在需要频繁访问的变量上使用 register 存储类可以提高程序的运行速度。
1 | register int miles; |
寄存器只用于需要快速访问的变量,比如计数器。还应注意的是,定义 ‘register’ 并不意味着变量将被存储在寄存器中,它意味着变量可能存储在寄存器中,这取决于硬件和实现的限制。
static 存储类
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。
静态变量在程序中只被初始化一次,即使函数被调用多次,该变量的值也不会重置。
1 |
|
实例中 count 作为全局变量可以在函数内使用,thingy 使用 static 修饰后,不会在每次调用时重置。
extern 存储类
extern 存储类用于定义在其他文件中声明的全局变量或函数。当使用 extern 关键字时,不会为变量分配任何存储空间,而只是指示编译器该变量在其他文件中定义。
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。
extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候,如下所示:
1 |
|
运算符
C 语言提供了以下类型的运算符:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符
算术运算符
假设变量 A 的值为 10,变量 B 的值为 20
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
– | 自减运算符,整数值减少 1 | A-- 将得到 9 |
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A || B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
位运算符
位运算符作用于位,并逐位执行操作。下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行"与"运算。 | (A & B) 将得到 12,即为 0000 1100 |
| | 按位或运算符,按二进制位进行"或"运算。 | (A | B) 将得到 61,即为 0011 1101 |
^ | 异或运算符,按二进制位进行"异或"运算。 | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 取反运算符,按二进制位进行"取反"运算。 | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
赋值运算符
下表列出了 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 |
其他
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () , [] , -> ,. , ++ ,-- | 从左到右 |
一元 | + ,- , ! , ~ , ++ ,-- ,(type)* ,& , sizeof | 从右到左 |
乘除 | * , / , % | 从左到右 |
加减 | + , - | 从左到右 |
移位 | << , >> | 从左到右 |
关系 | <, <= ,> ,>= | 从左到右 |
相等 | ==, != | 从左到右 |
位与AND | & | 从左到右 |
位异或XOR | ^ | 从左到右 |
位或OR | | | 从左到右 |
逻辑与AND | && | 从左到右 |
逻辑或OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | =, +=, -=, *= ,/=, %=, >>= ,<<= ,&= ,^= ,|= | 从右到左 |
逗号 | , | 从左到右 |
控制流
if-else
一个 if 语句 后可跟一个可选的 else 语句,else 语句在布尔表达式为 false 时执行。
1 | if(boolean_expression 1) |
switch
switch 语句允许测试一个变量等于多个值时的情况。每个值称为一个 case,且被测试的变量会对每个 switch case 进行检查。
1 | switch(expression){ |
- switch 语句中的 expression 是一个常量表达式,必须是一个整型或枚举类型。
- 在一个 switch 中可以有任意数量的 case 语句。每个 case 后跟一个要比较的值和一个冒号。
- case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量或字面量。
- 当被测试的变量等于 case 中的常量时,case 后跟的语句将被执行,直到遇到 break 语句为止。
- 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
- 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。
- 一个 switch 语句可以有一个可选的 default case,出现在 switch 的结尾。default case 可用于在上面所有 case 都不为真时执行一个任务。default case 中的 break 语句不是必需的。
三元运算符
条件运算符 ? :,可以用来替代 if…else 语句。
1 | Exp1 ? Exp2 : Exp3; |
loop
while 循环
while 循环的语法:
1 | while(condition) |
condition 可以是任意的表达式,当为任意非零值时都为 true。当条件为 true 时执行循环。 当条件为 false 时,退出循环,程序流将继续执行紧接着循环的下一条语句。
do…while 循环与 while 循环类似,但是 do…while 循环会确保至少执行一次循环。do…while 循环的语法:
1 | do |
例如:
1 | do |
for 循环
for 循环允许编写一个执行指定次数的循环控制结构。
1 | for ( init; condition; increment ) |
- init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
- 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
- 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
- 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。
例如:
1 | for( int a = 10; a < 20; a = a + 1 ) |
break跳出循环
C 语言中 break 语句有以下两种用法:
- 当 break 语句出现在一个循环内时,循环会立即终止,且程序流将继续执行紧接着循环的下一条语句。
- 它可用于终止 switch 语句中的一个 case。
如果使用的是嵌套循环(即一个循环内嵌套另一个循环),break 语句会停止执行最内层的循环,然后开始执行该块之后的下一行代码。
continue 跳过当前循环
C 语言中的 continue 语句 会跳过当前循环中的代码,强迫开始下一次循环。
对于 for 循环,continue 语句执行后自增语句仍然会执行。对于 while 和 do…while 循环,continue 语句重新执行条件判断语句。
goto
C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。
不建议使用 goto 语句。因为它使得程序的控制流难以跟踪,使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。
使用例子:
1 | int main () |
指针
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
1 | type* var_name; // * 可以写在 type 和 var_name之间任意位置 |
type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。星号是用来指定一个变量是指针。指针的长度就是内存地址的长度,在64位系统上sizeof的结果是8。
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。例如:
1 | int* ptr = NULL; // ptr = 0x0 |
使用指针
使用指针时会频繁进行以下几个操作:
- 定义一个指针变量
- 把变量地址赋值给指针(使用
&
取地址操作符) - 访问指针变量中可用地址的值(使用
*
解引用操作符)
1 | int var = 20; /* 实际变量的声明 */ |
指针运算
指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。
- 指针的每一次递增,它其实会指向下一个元素的存储单元。
- 指针的每一次递减,它都会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数,让我们对该指针执行下列的算术运算:
1 | ptr++ |
在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。
如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。例如:
1 | int var[] = {10, 100, 200}; |
如果指针加上一个整数n,计算方式和上面相同,p+n = p + n*sizeof(p)
指针还支持数组下标形式访问,*(p + 4)
可以表示为p[4]
,编译器总是把以下标的形式的操作解析为以指针的形式的操作,以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同。还可以将 p[4]
写成4[p]
,编译器并不会报错,因为它将 4[p]
解析为 *(4 + p)
。
指针比较
指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。例如:
1 | int var[] = {10, 100, 200}; |
指针数组
int *arr[6]
指针数组首先是个数组。数组中的每个元素都是指针(指向 int 类型的指针)。例如:
1 | int var[] = {10, 100, 200}; |
注意,指针数组元素一定要是地址。有一种错误写法是
int *var[3] = {10, 100, 200};
虽然var[0]
的值是10,但其实是将(int) 10强制转型为指针类型,并将内存地址值修改为10。如果是double类型的值,其结果就会出错(因为数字溢出)。例如long double *var[3] = {DBL_DIG+1,DBL_DIG+2,DBL_DIG+3};
其中var[0]=16
。
数组指针
int (*pt)[6]
数组指针首先是个指针。指针指向一个数组(包含 6 个元素),该数组每个元素都是 int 类型。
1 | char c[4] = {'a','c','f'}; |
- pi2是一个指针,指向的类型是
(char (*)[4])
。*pi2会获得c的地址(即数组地址)。所以(*pi2)[1]
就是c[1]
。 *(*pi2+1)
中,*pi2
获取数组地址,+1后会偏移到下一个元素地址,在对其解引用,获取对应元素的值。
实际使用中,也常常使用指向数组首个元素的指针:
1 | char c[4] = {'a','c','f'}; |
函数
C 语言中的函数定义的一般形式如下:
1 | return_type function_name( parameter list ) |
下面列出一个函数的所有组成部分:
- 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
- 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
- 参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
- 函数主体:函数主体包含一组定义函数执行任务的语句。
c语言支持函数声明(函数声明在头文件中定义),通过函数声明告诉编译器函数名称及返回值和参数列表吗,而函数的实际主体可以单独定义。
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
引用头文件
在使用我们定义的函数式,需要先引用函数的头文件。使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:
1 |
|
只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
1 |
|
这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。
1 |
SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。
返回指针
C 允许从函数返回指针。但是不支持在调用函数时返回局部变量的地址,需要定义局部变量为 static 变量。例如:
1 | int * getRandom( ) |
函数指针
函数指针是指向函数的指针变量。通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针变量的声明:
1 | typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型 |
例如:
1 | int max(int x, int y) |
函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
函数参数
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
当调用函数时,有两种向函数传递参数的方式:
- 值传递
- 引用传递
值传递
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。函数 swap() 定义如下:
1 | /* 函数定义 */ |
上面的实例表明了,虽然在函数内改变了 a 和 b 的值,但是实际上 a 和 b 的值没有发生变化。
引用传递
通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。
1 | /* 函数定义 */ |
上面的实例表明了,与传值调用不同,引用调用在函数内改变了 a 和 b 的值,实际上也改变了函数外 a 和 b 的值。
数组和指针的隐式转换
函数可以接受数组作为参数。此外也可以将数组当做指针接收。例如:
1 | void foo1(int *b) { printf("%lu\n", sizeof(b)); } |
参数传递的隐式转换规则:
- 若实参为一维数组,则形参是指向数组所存元素类型的指针;
- 若实参为多维数组,则形参是指向一维或多维数组的指针;
- 转换并不是递归的,数组的数组会被改成为数组的指针,而不是指针的指针;例如三维数组
char ho[2][3][4]
可以转换为char (*)[3][4]
类型的行指针。
1 | void foo2(int (*fnum)[2]) { |
内存管理
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。memcpy()
用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。memmove()
类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。memcmp()
函数用来比较两个内存区域。它接受三个参数,前两个参数是用来比较的指针,第三个参数指定比较的字节数。它的返回值是一个整数。两块内存区域的每个字节以字符形式解读,按照字典顺序进行比较,如果两者相同,返回0;如果s1大于s2,返回大于0的整数;如果s1小于s2,返回小于0的整数。
void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。
内存分配
下面展示了如何给结构体申请内存。
1 | typedef struct { |
当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。使用例子:
1 | int main() |
restrict 说明符
声明指针变量时,可以使用restrict说明符,告诉编译器,该块内存区域只有当前指针一种访问方式,其他指针不能读写该块内存。这种指针称为“受限指针”(restrict pointer)。
1 | int* restrict p; |
上面示例中,声明指针变量p时,加入了restrict说明符,使得p变成了受限指针。后面,当p指向malloc()函数返回的一块内存区域,就意味着,该区域只有通过p来访问,不存在其他访问方式。
1 | int* restrict p; |
上面示例中,另一个指针q与受限指针p指向同一块内存,现在该内存有p和q两种访问方式。这就违反了对编译器的承诺,后面通过*q对该内存区域赋值,会导致未定义行为。
memcpy
memcpy()用于将一块内存拷贝到另一块内存。该函数的原型定义在头文件string.h。
1 | //string.h |
上面代码中,dest是目标地址,source是源地址,第三个参数n是要拷贝的字节数n。如果要拷贝10个 double 类型的数组成员,n就等于10 * sizeof(double)
,而不是10。该函数会将从source开始的n个字节,拷贝到dest。
dest和source都是 void 指针,表示这里不限制指针类型,各种类型的内存数据都可以拷贝。两者都有 restrict 关键字,表示这两个内存块不应该有互相重叠的区域。
memcpy()的返回值是第一个参数,即目标地址的指针。
因为memcpy()只是将一段内存的值,复制到另一段内存,所以不需要知道内存里面的数据是什么类型。下面是复制字符串的例子。
1 |
|
实现原理:不管传入的dest和src是什么类型的指针,将它们重新定义成一字节的 Char 指针,这样就可以逐字节进行复制。*d++ = *s++
语句相当于先执行*d = *s
(源字节的值复制给目标字节),然后各自移动到下一个字节。最后,返回复制后的dest指针,便于后续使用。
1 | void* my_memcpy(void* dest, void* src, int byte_count) { |
memmove
memmove()函数用于将一段内存数据复制到另一段内存。它跟memcpy()的主要区别是,它允许目标区域与源区域有重叠。如果发生重叠,源区域的内容会被更改;如果没有重叠,它与memcpy()行为相同。
该函数的原型定义在头文件string.h。
1 | //string.h |
上面代码中,dest是目标地址,source是源地址,n是要移动的字节数。dest和source都是 void 指针,表示可以移动任何类型的内存数据,两个内存区域可以有重叠。
memmove()返回值是第一个参数,即目标地址的指针。
1 | int a[100]; |
上面示例中,从数组成员a[1]开始的99个成员,都向前移动一个位置。
下面是另一个例子。
1 | char x[] = "Home Sweet Home"; |
memcmp
memcmp()
函数用来比较两个内存区域。它接受三个参数,前两个参数是用来比较的指针,第三个参数指定比较的字节数。它的返回值是一个整数。两块内存区域的每个字节以字符形式解读,按照字典顺序进行比较,如果两者相同,返回0;如果s1大于s2,返回大于0的整数;如果s1小于s2,返回小于0的整数。
1 | //string.h |
示例比较s1和s2的前三个字节,由于s1小于s2,所以r是一个小于0的整数,一般为-1。
1 | char* s1 = "abc"; |