C 语言基础

null-qwerty

本文档用于 HDU-PHOENIX 战队视觉/算法组培训,用于速成 C 语言基础知识。

本文档仅包含最基础的内容,更多的知识需要自行学习。

推荐阅读:

  • 《C Primer Plus》
  • 学校发的 C 语言教材(谭浩强的除外)

0. C 语言简介

C 语言是一种通用的高级编程语言,由美国计算机科学家丹尼斯·里奇(Dennis Ritchie)在 1972 年设计开发。C 语言是一种结构化语言,它的设计目标是提供一种能够以简单的方式编写复杂的程序的语言。

C 语言是一种面向过程的语言,它的语法结构简单、灵活,同时又具有很强的表达能力,可以直接访问计算机硬件,可以用来编写操作系统、编译器、数据库、网络等系统软件,也可以用来编写应用软件。

C 语言是一种编译型语言,编译型语言是指程序在运行之前需要先编译成机器码,然后再运行。C 语言的编译器有很多,比如 GCC、Clang、MSVC 等。

C 语言是一种静态类型语言,静态类型语言是指在编译时就确定了变量的数据类型,变量的数据类型在编译时就已经确定,不会发生变化。

一个简单的 C 语言程序如下:

1
2
3
4
5
6
#include <stdio.h>  // 引入头文件,stdio.h 是标准输入输出头文件

int main() { // 主函数,程序从这里开始执行,main 函数是程序的入口,程序必须有且仅有一个 main 函数
printf("Hello, World!\n"); // 输出 Hello, World!,\n 表示换行
return 0; // 返回 0,表示程序正常结束
}

printf 是 C 语言的标准输出函数,用于输出内容到控制台,\n 表示换行。相对应地,scanf 是 C 语言的标准输入函数,用于从控制台输入内容:

1
2
3
4
5
6
7
8
9
#include <stdio.h>  // 引入头文件,stdio.h 是标准输入输出头文件

int main() { // 主函数,程序从这里开始执行,main 函数是程序的入口,程序必须有且仅有一个 main 函数
int a; // 定义一个整数变量 a
printf("Please input a number: "); // 输出提示信息
scanf("%d", &a); // 从控制台输入一个整数,存入变量 a,%d 表示以整型读入数据,&a 表示变量 a 的地址
printf("The number you input is: %d\n", a); // 输出变量 a 的值,%d 表示以整型输出数据,\n 表示换行
return 0; // 返回 0,表示程序正常结束
}

这个程序会提示输入一个整数,然后输出这个整数。

1. C 语言基本数据类型

C 语言的基本数据类型有字符型、整型、浮点型和指针,以下是 C 语言的基本数据类型的长度和能表示的范围:

类型关键字格式化输出符长度所示范围
字符型char%c1 Byte, 8bit
无符号字符型unsigned char%c1 Byte, 8bit
短整型short%d2 Byte, 16bit
无符号短整型unsigned short%d2 Byte, 16bit
整型int%d4 Byte, 32bit
无符号整型unsigned int%d4 Byte, 32bit
长整型long%ld4 Byte, 32bit
无符号长整型unsigned long%ld4 Byte, 32bit
双长整型long long%lld8 Byte, 64bit
无符号双长整型unsigned long long%lld8 Byte, 64bit
(单精度)浮点型float%f4 Byte, 32bit
双精度浮点型double%lf8 Byte, 32bit

指针

指针是一个变量,其值为另一个变量的地址。
指针的长度取决于操作系统的位数,32 位操作系统的指针长度为 4 Byte,64 位操作系统的指针长度为 8 Byte。
举个例子, 32 位操作系统最多只能寻址 4GB 的内存空间。

定义基本数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char c = 'a';           // 字符型
unsigned char uc; // 无符号字符型
short s; // 短整型
unsigned short us; // 无符号短整型
int i = 1; // 整型
unsigned int ui; // 无符号整型
long l; // 长整型
unsigned long ul; // 无符号长整型
long long ll; // 双长整型
unsigned long long ull; // 无符号双长整型
float f = 3.14; // 单精度浮点型
double d; // 双精度浮点型

int *p = &i; // 整型指针

2. C 语言运算符

C 语言的运算符包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、三目运算符、逗号运算符、取地址运算符、取值运算符、自增自减运算符、sizeof 运算符、类型转换运算符等。

2.1 赋值运算符

赋值运算符用于给变量赋值,赋值运算符的优先级最低。

1
2
3
int a = 1;
int b = 2;
a = b; // a = 2

2.2 算术运算符

算术运算符用于进行基本的数学运算。

运算符说明示例
+加法res = a + b
-减法res = a - b
*乘法res = a * b
/除法res = a / b
%取模(取余数)res = a % b
+=加法赋值运算符a += b
-=减法赋值运算符a -= b
*=乘法赋值运算符a *= b
/=除法赋值运算符a /= b
%=取模赋值运算符a %= b

其中加减法还有自增自减运算符:

运算符说明示例
++自增a++,++a
--自减a--, --a

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 1;
int b = 2;
int res = a + b; // res = 3
res = a - b; // res = -1
res = a * b; // res = 2
res = a / b; // res = 0
res = a % b; // res = 1
res += a; // 等价于 res = res + a
b++; // b = 3
--b; // b = 2
res = a++; // res = 1, a = 2
res = ++a; // res = 3, a = 3
res = a--; // res = 3, a = 2
res = --a; // res = 1, a = 1

使用自增自减运算符的时候需要注意,一条语句中最多使用一个自增或自减运算符,也不要出现 a = a++a = ++a 这样的语句,例如:

1
2
int i = 1;
i = i++ + ++i; // 未定义行为

以上代码符合语法是未定义行为,不同编译器可能会有不同的结果。

2.3 关系运算符

关系运算符用于比较两个值的大小,返回值为真(1)或假(0)。

运算符说明示例
==等于res = a == b
!=不等于res = a != b
>大于res = a > b
<小于res = a < b
>=大于等于res = a >= b
<=小于等于res = a <= b

2.4 逻辑运算符

逻辑运算符用于进行逻辑运算,返回值为真(1)或假(0)。

运算符说明示例
&&逻辑与res = a && b
||逻辑或res = a|| b
!逻辑非res = !a

2.5 位运算符

位运算符用于对二进制数进行位运算。

运算符说明示例
&位与res = a & b
|位或res = a| b
^位异或res = a ^ b
~位取反res = ~a
<<位左移,第二个数表示位移量res = a << b
>>位右移,第二个数表示位移量res = a >> b
&=位与赋值运算符a &= b
|=位或赋值运算符a|= b
^=位异或赋值运算符a ^= b
<<=位左移赋值运算符a <<= b
>>=位右移赋值运算符a >>= b

例如:

1
2
3
4
5
6
7
8
9
10
11
int a = 3; // 0011_2
int b = 2; // 0010_2
int res;
res = a & b; // 0011_2 & 0010_2 = 0010_2 = 2_10
res = a | b; // 0011_2 | 0010_2 = 0011_2 = 3_10
res = a ^ b; // 0011_2 ^ 0010_2 = 0001_2 = 1_10
res = ~a; // ~(00000000 00000000 00000000 00000011)_2
// = (11111111 11111111 11111111 11111100)_2
// = -4_10
res = a << b; // 0011_2 << 2 = 1100_2 = 12_10
res = a >> b; // 0011_2 >> 2 = 0000_2 = 0_10

2.6 三目运算符

三目运算符用于简化 if-else 语句,语法为 条件表达式 ? 表达式 1 : 表达式 2,如果条件表达式为真则返回表达式 1 的值,否则返回表达式 2 的值。

例如:

1
2
3
int a = 1;
int b = 2;
int res = a > b ? a : b; // res = 2,这是一个获取两个数中较大的数的方法

2.7 逗号运算符

逗号运算符用于连接两个表达式,返回值为最后一个表达式的值。

例如:

1
2
3
int a = 1;
int b = 2;
int res = (a++, b++, a + b); // res = 4,逗号运算符会先执行 a++ 和 b++,然后返回 a + b 的值

2.8 取地址运算符和取值运算符(解引用)

取地址运算符用于获取变量的地址。

例如:

1
2
int a = 1;
int *p = &a;

取值运算符(解引用)用于获取指针指向的变量的值。

例如:

1
2
// 接上面的代码
int b = *p; // b = 1

2.9 sizeof 运算符

sizeof 运算符用于获取变量或类型的长度,返回值为 size_t 类型。

例如:

1
2
3
4
int a = 1;
int size = sizeof(a); // size = 4
// 等价于
// int size = sizeof(int); // size = 4

2.10 类型转换运算符

类型转换运算符(强制类型转换、显式类型转换)用于将一个数据类型转换为另一个数据类型。

强制类型转换的形式为 (type)var,例如:

1
2
int a = 1;
double b = (double)a; // b = 1.0

当我们把两个不同类型的数据进行运算时,C 语言会自动进行类型转换(隐式类型转换),将较小的数据类型转换为较大的数据类型,顺序为 char -> short -> int -> long -> long long -> double <- float。而当两个同级别的数据类型进行运算时,C 语言不会将数据类型进行转换。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 1;
double b = 2;
double c = a + b; // c = 3.0
// 计算 a(int) + b(int) 时,结果为 int(3)
// a + b 结果(3)赋值给 c(double) 时,将 int(3) 隐式转换为 double(3.0)

double d = a / b; // d = 0.0
// 计算 a(int) / b(int) 时,结果为 int,抹去小数结果为 0
// a + b 结果(0)赋值给 c(double) 时,将 int 隐式转换为 double(0.0)

d = (double)a / b; // d = 0.5
// 强制类型转换后 a(double) / b(int) 隐式转换为 a(double) / b(double) = 1.0 / 2.0 = 0.5
// 赋值时无转换

// 手动触发隐式类型转换:乘一个小数
d = 1.0 * a / b; // d = 0.5
// 1.0(double) * a(int) 转换为 1.0(double) * a(double) = 1.0 * 1.0 = 1.0,结果为 double(1.0)

注意,将一个较大的数据类型转换为较小的数据类型时,可能会造成数据丢失精度丢失或者溢出

例如:

  1. 将一个浮点数转换为整数时,会将小数部分截断。
  2. 将 double 转换为 float 时,会造成精度丢失。

2.11 运算符优先级

C 语言的运算符都具有优先级,高优先级先执行,低优先级后执行,同优先级按一定顺序执行,优先级如下表所示:

优先级运算符结合顺序
1[], (), ., ->从左到右
2-, ~, ++, –, *, &, !, (type), sizeof从右到左
3/, *, %从左到右
4+, -从左到右
5<<, >>从左到右
6>, >=, <, <=从左到右
7==, !=从左到右
8&从左到右
9^从左到右
10|从左到右
11&&从左到右
12||从左到右
13?:从右到左
14=, /=, *=, %=, +=, -=, <<=, >>=, &=, ^=,|=从右到左
15,从左到右

3. C 语言保留字(关键字)

C 语言的保留字(关键字)是一些具有特殊含义的单词,不能用作变量名、函数名等标识符。除了之前提到的数据类型关键字(如 intchar 等)之外,C 语言还有一些其他的保留字,如下表所示:

关键字说明关键字说明关键字说明
流程控制存储类说明符typedef定义类型
if条件语句extern外部变量或函数volatile不对变量进行优化
elseif 语句中条件为假时执行的分支static静态变量长度计算
switch多分支条件语句register寄存器变量sizeof计算数据类型长度
caseswitch 语句中的分支auto自动变量
defaultswitch 语句中的默认分支类型限定符
forfor 循环void空类型
dodo-while 循环的循环体signed有符号数
whilewhile 循环unsigned无符号数
continue跳过当前循环的剩余部分enum枚举类型
break跳出当前循环或分支struct结构体类型
goto跳转到指定标签union共用体类型
return返回函数值const常量

加上之前提到的数据类型关键字,C 语言一共有 32 个关键字。

4. C 语言注释

C 语言的注释有两种形式,单行注释和多行注释(块注释)。

1
2
3
4
5
6
7
// 这是单行注释,注释内容在 // 后面,直到行尾

/* 这是多行注释
注释内容从 /* 开始
可以跨越多行
直到*\/结束
*/

5. 流程控制

C 语言的流程控制有顺序结构、选择结构和循环结构。

5.1 顺序结构

顺序结构是程序按照代码的顺序执行,没有分支和循环。正常情况下,C 语言的代码都是按照顺序结构执行的。

1
2
3
int a = 1;  // step 1
int b = 2; // step 2
int c = a + b; // step 3

5.2 选择结构

选择结构有 if 语句、switch 语句。

5.2.1 if 语句

if 语句用于根据条件执行不同的代码块,语法如下:

1
2
3
4
5
if (condition) {
// 如果 condition 为真,执行这里的代码
} else {
// 如果 condition 为假,执行这里的代码
}

else 语句是可选的,可以省略,同时也可以有多个 else if 语句。

例如:

1
2
3
4
5
6
7
8
9
int a = 1;
int b = 2;
if (a > b) {
printf("a > b\n");
} else if (a < b) {
printf("a < b\n");
} else {
printf("a = b\n");
}

5.2.2 switch 语句

switch 语句用于根据不同的条件执行不同的代码块,语法如下:

1
2
3
4
5
6
7
8
9
10
11
switch (expression) {
case constant1:
// 如果 expression == constant1,执行这里的代码
break;
case constant2:
// 如果 expression == constant2,执行这里的代码
break;
...
default:
// 如果 expression 不等于任何一个 constant,执行这里的代码
}

switch 语句中的 expression 必须是整型或字符型,case 后面的 constant 必须是整型常量或字符常量。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
int a = 1;
int b = 2;
switch (a > b) {
case 1:
printf("a > b\n");
break;
case 0:
printf("a <= b\n");
break;
default:
break;
}

5.3 循环结构

循环结构有 for 循环、while 循环、do-while 循环。

5.3.1 for 循环

for 循环用于重复执行一段代码,一个完整的 for 语句包括初值、条件和增量三部分,语法如下:

1
2
3
4
for (initialization; condition; increment) {    // 第一次执行时执行 initialization
// 如果 condition 为真,执行这里的代码
// 执行完这里的代码后,执行 increment
}

例如:

1
2
3
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}

输出:

1
0 1 2 3 4 5 6 7 8 9

5.3.2 while 循环

while 循环用于重复执行一段代码,语法如下:

1
2
3
while (condition) {
// 如果 condition 为真,执行这里的代码
}

例如:

1
2
3
4
5
int i = 0;
while (i < 10) {
printf("%d ", i);
i++;
}

输出:

1
0 1 2 3 4 5 6 7 8 9

5.3.3 do-while 循环

do-while 循环用于重复执行一段代码,与 while 循环的区别是 do-while 循环会先执行一次循环体,然后判断条件是否为真,语法如下:

1
2
3
do {
// 执行这里的代码
} while (condition);

例如:

1
2
3
4
5
int i = 0;
do {
printf("%d ", i);
i++;
} while (i < 10);

输出:

1
0 1 2 3 4 5 6 7 8 9

5.4 跳转语句

C 语言的跳转语句有 break、continue、goto 和 return。

5.4.1 break

break 语句用于跳出当前循环或 switch 语句,程序会继续执行循环或 switch 语句后面的代码。

例如:

1
2
3
4
5
6
for (int i = 0; i < 10; i++) {
if (i == 5) {
break;
}
printf("%d ", i);
}

输出:

1
0 1 2 3 4

5.4.2 continue

continue 语句用于跳过当前循环的剩余部分,继续执行下一次循环。

例如:

1
2
3
4
5
6
for (int i = 0; i < 10; i++) {
if (i == 5) {
continue;
}
printf("%d ", i);
}

输出:

1
0 1 2 3 4 6 7 8 9

5.4.3 goto

goto 语句用于跳转到指定标签,语法如下:

1
2
3
goto label;
...
label: statement;

例如:

1
2
3
4
5
6
7
int i = 0;
loop:
if (i < 10) {
printf("%d ", i);
i++;
goto loop;
}

这个代码构成了一个循环。

5.4.4 return

return 语句用于返回函数值,结束函数的执行。

例如:

1
2
3
int add(int a, int b) {
return a + b;
}

6. 数组与字符串、指针

6.1 数组

数组是一种存储多个相同类型数据的数据结构,数组的元素可以通过下标访问,数组的下标从 0 开始

定义数组语法为 type name[length],其中 type 是数组元素的类型,name 是数组的名字,length 是数组的长度。

例如:

1
2
3
4
5
6
int a[5]; // 定义一个长度为 5 的整型数组
int b[] = {1, 2, 3, 4, 5}; // 定义一个长度为 5 的整型数组,并初始化
int c[5] = {1, 2, 3, 4, 5}; // 定义一个长度为 5 的整型数组,并初始化
float d[3] = {1.1, 2.2, 3.3};
char e[5] = {'a', 'b', 'c', 'd', 'e'};

访问数组元素:

1
2
3
int a[5] = {1, 2, 3, 4, 5};
int b = a[0]; // b = 1
a[1] = 10; // a = {1, 10, 3, 4, 5}

数组长度是固定的,初始化完成后不能改变。可以通过 sizeof 运算符获取数组的长度。

1
2
int a[5] = {1, 2, 3, 4, 5};
int len = sizeof(a) / sizeof(a[0]); // len = 5

遍历一个数组常常使用循环语句。

数组在内存中是连续存储的,数组变量名其实就是数组首元素的地址。

1
2
3
4
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // p 指向数组 a
int *q = &a[0]; // q 指向数组 a 的首元素
// p 和 q 指向的地址是相同的

数组元素的下标反应了元素相对于数组首元素的偏移量

6.2 字符串

字符串是一种特殊的字符数组,字符串以 '\0' 结尾,'\0' 是字符串结束标志。

定义字符串语法为 char name[length],其中 name 是字符串的名字,length 是字符串的长度。使用双引号 " 来定义字符串常量,字符数组只能在初始化时可以被字符串赋值。在格式化输入输出时,用 %s 表示字符串。

例如:

1
2
3
4
5
6
7
8
9
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 定义一个字符串,内容为 "Hello"
// 等价于
// char str1[6] = "Hello";
// 等价于
// char str1[] = "Hello";
char str2[10];
// str2 = "World"; // 错误,不能直接赋值
scanf("%s", str2); // 从控制台输入一个字符串
printf("%s %s\n", str1, str2); // 输出两个字符串

字符串有专门的处理函数,在 string.h 头文件中:

函数名说明
strlen(str)获取字符串长度,不包括结束标志
strcpy(dest, src)复制字符串,将 src 复制到 dest
strcat(dest, src)连接字符串,将 src 连接到 dest 后面
strcmp(str1, str2)比较字符串,比较 str1 和 str2 的大小,返回值为负数、0 或正数
strchr(str, c)查找字符,返回字符串 str 中第一次出现字符 c 的位置
strstr(str1, str2)查找字符串,查找字符串 str1 中第一次出现字符串 str2 的位置
strtok(str, delim)分割字符串,将字符串 str 按照分隔符 delim 分割
sprintf(str, format, …)格式化输出到字符串,将格式化输出的结果存入字符串 str
sscanf(str, format, …)从字符串读取格式化输入,从字符串 str 中读取格式化输入

6.3 指针

指针是 C 语言的一个重要概念,也是 C 语言特有的东西。指针本质上是一个整型变量,存储的是一个内存地址。

定义指针语法为 type *name,其中 type 是指针指向的数据类型,name 是指针的名字。使用 & 运算符获取变量的地址,使用 * 运算符获取指针指向的变量的值。

例如:

1
2
3
4
5
6
7
8
9
int a = 1;
int *p = &a; // 定义一个整型指针 p,指向变量 a
int b = *p; // b = 1,获取指针 p 指向的变量的值

int *q = NULL; // 定义一个空指针
// NULL 是一个宏定义,表示空指针, 其值是 0

printf("%d\n", *p); // 输出指针 p 指向的变量的值
printf("%p\n", p); // 输出指针 p 的值,十六进制表示

数组就是指针,指针就是数组。

使用指针我们可以做一些有趣的事情,例如:

1
2
3
4
5
6
7
short arr[5] = {1, 2, 3, 4, 5};
short *p = arr;
char *q = (char *)p; // 将 short 指针转换为 char 指针

for (int i = 0; i < 5 * 2; i++) {
printf("%d ", *(q + i)); // 输出 arr 中每个字节的值
}

由于 short 类型占 2 个字节,char 类型占 1 个字节,所以输出的结果是 1 0 2 0 3 0 4 0 5 0

linux 系统字节序为小端序(smalldian)。

我们来看另一个例子:

1
2
3
4
5
float f = 1.5;
int *p = (int *)&f; // 用 int 指针指向 float 变量 f

printf("%f\n", f); // 输出 f 的值
printf("%d\n", *p);

这个例子中,我们将 float 类型的指针转换为 int 类型的指针,然后输出这两个变量的值,结果是 1.5000001069547520

结果解释

我们来解释一下这个结果。
intfloat 的长度都是 4 个字节,但他们的存储方式不同。
4 个字节一共 32 位,最高位是符号位(S):

对于 int 类型,最高位是符号位,剩下的 31 位是数值位(V),例如 10 的二进制表示是:

对于 float 类型,最高位是符号位;接下来的 8 位是指数位(E);最后的 23 位是尾数位(M),以二进制小数形式存储:

其计算方式为:

并规定:

  • 当 E 全为 1 时,表示的是特殊数
    • 当 M 全为 0 时,表示的是无穷大;
    • 当 M 不全为 0 时,表示的是 NaN(Not a Number);
  • 当 E 全为 0 时,表示的是非规格化数
    • 此时
  • 当 E 不全为 0 且不全为 1 时,表示的是规格化数,此时 E 取值范围为 1 ~ 254
    也因此 float 最大取值的绝对值为

    最小取值(规格化数)的绝对值为

    所以 1.5 的二进制表示为:

这个二进制数转换为十进制数为 1069547520

7. 函数

函数表示了一段特定的功能,可以重复调用。函数的定义包括函数名、返回值类型、参数列表和函数体。

7.1 定义一个函数

不能在函数内部定义函数,函数的定义必须在函数外部。

1
2
3
return_type function_name(parameter_list) {
// 函数体
}

其中 return_type 是返回值类型,function_name 是函数名,parameter_list 是参数列表,{} 中是函数体。

例如:

1
2
3
int add(int a, int b) {
return a + b;
}

函数的声明和定义可以分开,声明函数时只需要写函数的返回值类型、函数名和参数列表,不需要写函数体:

1
2
3
4
5
6
7
8
9
10
11
12
int add(int a, int b); // 函数声明

int main() {
int a = 1;
int b = 2;
int c = add(a, b); // 调用函数
return 0;
}

int add(int a, int b) { // 函数定义
return a + b;
}

编写 C 语言函数要注意以下几点:

  • 函数需要先声明再调用;
  • 函数不能重名;
  • 函数的参数个数是任意的;
  • 函数的参数可以有默认值,有默认值的参数必顫放在参数列表的最后;
  • 函数可以没有返回值,此时返回值类型为 void,但不建议这样做。

7.2 函数的参数传递

函数的参数有两种传递方式:传值和传址。这与函数的调用过程有关。

7.2.1 传值调用

C 语言的函数参数传递是传值的,即将实参的值复制一份给形参,函数内部对形参的修改不会影响实参。

例如,假设我希望输出函数 y = x + 1,但是我不希望修改 x的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int func(int x) {
// x 是形参,接收实参的值
x = x + 1; // 修改形参的值
return x; // 返回修改后的值
}

int main() {
for(int i = 0; i < 100; i++)
printf("%d ", func(i)); // 将 i 的值传递给 x,接收返回值并输出
// i 的值不会被修改
// 输出:1 2 3 4 5 6 ...
return 0;
}

不仅仅传入参数是传值的,函数的返回值也是传值的。函数的返回值是一个临时变量,函数返回后这个临时变量就会被销毁。

7.2.2 传址调用

但是某些情况下,我们希望函数内部对形参的修改能够影响实参,例如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int swap(int a, int b) {
// 调用函数时,实参 a 和 b 的值会被复制给形参 a 和 b
// 函数内部交换的是形参 a 和 b 的值
int temp = a;
a = b;
b = temp;
return 0;
}

int main() {
int a = 1;
int b = 2;
swap(a, b); // a 和 b 作为实参传递给 swap 函数
// 我们希望 a 和 b 的值被交换
printf("a = %d, b = %d\n", a, b); // 输出 a = 1, b = 2
return 0;
}

发现输出结果并不是我们期望的 a = 2, b = 1,这是因为函数参数传递是传值的,函数内部对形参的修改不会影响实参。

为了解决这个问题,我们可以使用指针来传递参数,这样函数内部对形参的修改就会影响实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int swap(int *a, int *b) {
// a 和 b 是指针,指向实参 a 和 b
// 函数内部交换的是 a 和 b 指向的变量的值
int temp = *a;
*a = *b;
*b = temp;
return 0;
}

int main() {
int a = 1;
int b = 2;
swap(&a, &b); // a 和 b 的地址作为实参传递给 swap 函数
// 我们希望 a 和 b 的值被交换
printf("a = %d, b = %d\n", a, b); // 输出 a = 2, b = 1
return 0;
}

严格意义上来说,这个函数依然是传值调用,只不过传递的是指针的值,而不是变量的值。由于指针指向了变量的地址,所以函数内部对指针所指的目标的修改会影响到外部变量。

7.3 递归函数

递归函数是指在函数内部调用自身的函数。递归函数有两个要素:递归边界和递归式。

例如,计算阶乘的递归函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int factorial(int n) {
if (n == 0) {
return 1; // 递归边界,0! = 1
} else {
return n * factorial(n - 1); // 递归式
}
}

int main() {
int n = 5;
int result = factorial(n);
printf("%d! = %d\n", n, result);
return 0;
}

递归函数的调用过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
factorial(5)
5 * factorial(4)
5 * 4 * factorial(3)
5 * 4 * 3 * factorial(2)
5 * 4 * 3 * 2 * factorial(1)
5 * 4 * 3 * 2 * 1 * factorial(0)
5 * 4 * 3 * 2 * 1 * 1
5 * 4 * 3 * 2 * 1
5 * 4 * 3 * 2
5 * 4 * 6
5 * 24
120

递归函数的优点是代码简洁,但是递归函数的缺点是递归深度过深时会导致栈溢出。

7.4 函数指针

与数据类型一样,函数也具有指针。函数指针是指向函数的指针,函数指针的声明和定义如下:

1
return_type (*function_pointer)(parameter_list);

其中 return_type 是函数的返回值类型,parameter_list 是参数列表。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
return a / b;
}

int main() {
int (*func[4])(int, int) = {add, sub, mul, div}; // 定义一个函数指钋数组
int a = 10;
int b = 5;
for (int i = 0; i < 4; i++) {
printf("%d\n", func[i](a, b)); // 通过函数指针调用函数
}
return 0;
}

8. 结构体、联合体和枚举

除了基本数据类型,C 语言还提供了结构体、联合体和枚举这三种自定义数据类型。自定义数据类型可以更好地组织数据,提高代码的可读性。

定义自定义数据类型必须在函数外部定义,不能在函数内部定义。

8.1 结构体

结构体是一种自定义的数据类型,可以包含多个不同类型的数据。其大小是内部所有成员大小相加。

结构体的定义如下:

1
2
3
4
5
struct struct_name {
type1 member1;
type2 member2;
...
};

其中 struct_name 是结构体的名字,member1member2 是结构体的成员,type1type2 是成员的数据类型。

例如,定义一个学生结构体:

1
2
3
4
5
struct student {
char name[20];
int age;
float score;
};

结构体的成员通过 . 运算符访问:

1
2
3
4
struct student s;
strcpy(s.name, "Neri");
s.age = 14;
s.score = 90.5;

如果结构体的成员是指针,则通过 -> 运算符访问:

1
2
3
4
5
6
7
8
struct student *p = &s;
strcpy(p->name, "Neri");
p->age = 14;
p->score = 90.5;
// 等价于
strcpy((*p).name, "Neri");
(*p).age = 14;
(*p).score = 90.5;

8.2 联合体

联合体是一种特殊的结构体,联合体的所有成员共用同一块内存,联合体的大小等于最大的成员的大小。联合体的定义如下:

1
2
3
4
5
union union_name {
type1 member1;
type2 member2;
...
};

例如,定义一个共用体:

1
2
3
4
union data {
int i;
float f;
};

联合体的访问与结构体类似,通过 .-> 运算符访问。

1
2
3
4
5
union data d;
union data *p = &d;
d.f = 1.5;
printf("%d\n", d.i); // 输出 1069547520
printf("%f\n", p->d); // 输出 1.500000

8.3 枚举

枚举是一种自定义的数据类型,枚举的成员是常量,枚举的定义如下:

1
2
3
4
5
enum enum_name {
member1,
member2,
...
};

例如,定义一个颜色枚举:

1
2
3
4
5
enum color {
RED,
GREEN,
BLUE
};

此时 REDGREENBLUE 是常量,其值分别为 012

规定后一个枚举成员的值比前一个枚举成员的值大 1,如果需要指定枚举成员的值,可以在定义时赋值:

1
2
3
4
5
enum color {
RED = 2, // RED = 2
GREEN = 5, // GREEN = 5
BLUE // BLUE = 6
};
1
2
3
4
5
enum color {
RED = 1,
GREEN = 2,
BLUE = 4
};

枚举的成员可以通过枚举名访问:

1
2
3
4
5
6
7
8
9
enum color {
RED,
GREEN,
BLUE
};
// ...省略部分代码...
enum color c = RED;
printf("%d\n", c); // 输出 0
// ...省略部分代码...
  • 标题: C 语言基础
  • 作者: null-qwerty
  • 创建于 : 2024-11-01 20:41:47
  • 更新于 : 2024-11-01 21:40:40
  • 链接: https://www.null-qwerty.top/2024/11/01/C-语言基础/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论