C基础


1. 简介

C语言是一门面向过程的计算机编程语言,与C++、C#、Java等面向对象编程语言有所不同。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、仅产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

C语言描述问题比汇编语言迅速、工作量小、可读性好、易于调试、修改和移植,而代码质量与汇编语言相当。

C语言一般只比汇编语言代码生成的目标程序效率低10%-20%。因此,C语言可以编写系统软件。

当前阶段,在编程领域中,C语言的运用非常之多,它兼顾了高级语言和汇编语言的优点,相较于其它编程语言具有较大优势。

计算机系统设计以及应用程序编写是C语言应用的两大领域。同时,C语言的普适较强,在许多计算机操作系统中都能够得到适用,且效率显著。

C语言拥有经过了漫长发展历史的完整的理论体系,在编程语言中具有举足轻重的地位。C语言是计算机编程领域中最重要的语言之一,也是最常用的语言之一。

历史

  • 1969年,美国贝尔实验室的Ken Thompson,以BCPL语言为基础,设计出很简单且很接近硬件的B语言,并且用B语言写了初版UNIX操作系统。
  • 1972年,美国贝尔实验室的丹尼斯·里奇在B语言的基础上最终设计出了一种新的语言,他取了BCPL的第二个字母作为这种语言的名字,这就是C语言。
  • 1977年,丹尼斯·里奇发表了不依赖于具体机器系统的C语言编译文本《可移植的C语言编译程序》。
  • 1982年,很多有识之士和美国国家标准协会(ANSI)为了使C语言健康地发展下去,决定成立C标准委员会,建立C语言的标准。
  • 1989年,ANSI发布了第一个完整的C语言标准——ANSI X3.159-1989,简称“C89”,不过人们也习惯称其为“ANSI C”。
  • 1999年,在做了一些必要的修正和完善后,ISO发布了新的C语言标准,命名为ISO/IEC 9899:1999,简称“C99”。
  • 2011,ISO又正式发布了新的标准,称为ISO IEC9899:2011,简称为“C11”。

特点

  • 简单易学
  • 面向过程
  • 代码具有较好的可移植性
  • 可生成高质量、目标代码执行效率高的程序

入门程序

#include <stdio.h>
int main(void)
{
    printf("Hello World!\n");
    return 0;
}

编译器

  • GCC,GNU组织开发的开源免费的编译器
  • MinGW,Windows操作系统下的GCC
  • Clang,开源的BSD协议的基于LLVM的编译器
  • Visual C++ :: cl.exe,Microsoft VC++自带的编译器

常用编译环境

  • VSCode (推荐
  • Visual Studio 2022
  • Code::Blocks

2. 基础语法

注释

  • 注释是程序中用于解释代码的文字,对程序的阅读者来说非常有帮助。
  • 注释是给程序员看的,对计算机来说是无效的。
  • 注释可以单行(用符号//)也可以多行(用符号/* */),详细见如下示例。
// 单行注释
/* 
    多行
    注释 
*/

标识符

C 标识符是用来标识变量、函数,或任何其他用户自定义项目的名称。

一个标识符以字母A-Z 或 a-z 或 下划线 _开始,后跟零个或多个字母、下划线和数字(0-9)

关键字

关键字说明
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循环语句的循环条件

分号

在 C 程序中,分号是语句结束符。也就是说,每个语句必须以分号结束。它表明一个逻辑实体的结束。

空白符号

在C语言中,空白符号是指空格(' ')、制表符('\t')和换行符('\n')这三种字符。合理的使用,可以增强代码的可读性。

  • 空格符号可以用于分隔代码中的标识符
  • 制表符可以用于对齐代码
  • 换行符则可以用于分割代码行

3. 数据类型

以下表格以32bit的CPU为例子,说明C语言中数据类型的存储大小和值范围。

整型

类型存储大小值范围
char1 字节-128 到 127
unsigned char1 字节0 到 255
short2 字节-32,768 到 32,767
unsigned short2 字节0 到 65,535
int4 字节-231 到 231-1
unsigned int4 字节0 到 232-1
long4 字节-231 到 231-1
unsigned long4 字节0 到 232-1
long long8 字节-263 到 263-1
unsigned long long8 字节0 到 264-1

浮点型

类型存储大小正数取值范围
float4 字节1.2E-38 到 3.4E+38
double8 字节2.3E-308 到 1.7E+308

布尔型(C99)

布尔类型是C99新增的数据类型,用于表示真或假的值。

需要包含头文件 #include<stdbool.h>。

类型存储大小值范围
bool1 字节true 或 false

void型

常用于如下几个场合:

  • 函数返回值为空
  • 函数参数为空
  • 指针void*可以被转换为任意类型

typedef类型

可以使用关键字typedef来定义类型的别名。

#include<stdint.h>头文件中定义了诸多基础类型的别名,比如uint32_t等。

// stdint.h 已定义
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;

// 自定义示例
struct apdu{
    uint8_t cla;
    uint8_t ins;
    uint8_t p1;
    uint8_t p2;
    uint8_t p3;
};
typedef struct apdu apdu_t;

4. 变量

变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C 是大小写敏感的。

变量定义

int age;            // 定义整型变量age
char *name;         // 定义指针变量name
char data[10];      // 定义字符数组data,长度为10

变量初始化

int age = 18;
char *name = "LiLei"; 
char data[10] = {
    0x01, 0x02, 0x03, 0x04, 
    0x05, 0x06, 0x07, 0x08,
    0x09, 0x10
};

变量声明

一般变量定义和声明是分离的,常用于定义全局变量。

在C文件中定义变量,然后再在其它文件(头文件、或其他C文件)中声明变量。

extern int age;
extern char *name;
extern char data[10];

5. 常量

常量是指在程序执行过程中其值不会改变的量。主要包括以下几种:

分类

  1. 整型常量:包括二进制、八进制、十进制和十六进制的整数。例如:

    • 二进制:0b1010
    • 八进制:015
    • 十进制:10
    • 十六进制:0x10
  2. 浮点型常量:由整数部分、小数点、小数部分和指数部分组成。例如:

    • 3.14159
    • 1.2e-3(科学计数法表示)
  3. 字符常量:用单引号括起来的单个字符。例如:

    • 'A'
    • '\n'(换行符)
  4. 字符串常量:用双引号括起来的字符序列。例如:

    • "Hello, World!"
  5. 宏常量:用#define预处理器指令定义的常量。例如:

    #define PI 3.14159
    
  6. 枚举常量:在enum类型中定义的常量。例如:

    enum colors { RED, GREEN, BLUE };
    
  7. 转义序列:特殊的字符常量,用于表示无法直接输入的字符。例如:

    • '\t'(制表符)
    • '\''(单引号)

6. 数组

数组是一种用于存储相同类型元素的数据结构。

数组由连续的内存块组成,其中每个元素都可以通过其索引来访问。索引通常从0开始,到数组长度减1。

数组的声明

要声明一个数组,需要指定数组的名称、数组元素的类型和数组的大小(即元素数量):

// 声明一个包含5个整数的数组
int array0[5];

// 声明并初始化一个包含6个字符的数组
char array1[] = {'H', 'e', 'l', 'l', 'o', '!'};

// 声明并初始化一个包含10个整数的数组,并部分初始化
int array2[10] = {1, 2, 3, 4, 5};       // 其余元素将自动初始化为0

// [C99] 数组元素 指定初始化
int array3[5] = {[1]=10, [3]=20};       // 元素1和3被初始化为10和20,其它元素为0

// [C99] 支持变长数组
int array4[n];                          // 声明一个动态数组,其大小由变量n决定

数组的使用

可以通过索引来访问数组中的元素:

#include <stdio.h>

int main() {
    int array[5] = {10, 20, 30, 40, 50};

    // 访问数组元素
    printf("第一个元素: %d\n", array[0]);
    printf("第三个元素: %d\n", array[2]);

    // 修改数组元素
    array[2] = 300;
    printf("修改后的第三个元素: %d\n", array[2]);

    return 0;
}

多维数组

C语言也支持多维数组,即数组中的元素也可以是数组:

// 声明一个2x3的二维整数数组
int array[2][3] = {{1, 2, 3}, {4, 5, 6}};

// 访问二维数组的元素
printf("第一行第一列的元素: %d\n", array[0][0]);
printf("第二行第二列的元素: %d\n", array[1][1]);

// 修改二维数组的元素
array[1][2] = 9;
printf("修改后的第二行第三列的元素: %d\n", array[1][2]);

数组的长度

在C语言中,数组的长度是固定的,一旦声明就不能改变。如果需要动态调整数组的大小,可以使用指针和动态内存分配(如mallocrealloc函数)。

数组与指针

数组名在大多数情况下会被解释为指向数组首元素的指针。因此可以将数组名用作指针来遍历数组:

int array[] = {1, 2, 3, 4, 5};
int *ptr = array;

for (int i = 0; i < 5; i++) {
    printf("%d ", *(ptr + i)); // 通过指针访问数组元素
}

7. 字符串

字符数组

字符串是由字符数组表示的,并以空字符('\0')结尾。

例如,如果想声明一个包含字符串 "Hello" 的字符数组,可以使用以下代码:

char str[] = "Hello";       // 声明一个字符数组
char str[10] = "Hello";     // 声明一个字符数组,并分配10个字符的空间

第一个例子中,str 是一个字符数组,其大小为 6,包括 5 个字符 'H', 'e', 'l', 'l', 'o' 和一个空字符 '\0'。

第二个例子中,我们为 str 分配了 10 个字符的空间,但实际上只使用了 6 个字符(包括空字符)。

需要注意的是,C语言中的字符串内容是不可变的。如果需要修改字符串,则可以将字符串复制到一个字符数组中,然后对字符数组进行修改。

C语言提供了一些处理字符串的函数,如 strlen()(用于获取字符串长度)、strcpy()(用于复制字符串)、strcat()(用于连接字符串)等。这些函数都在 <string.h> 头文件中定义。

字符串指针

可以将字符串指针初始化为指向一个字符串字面量,或者指向一个已经分配了内存的字符数组:

// 指向一个存储在程序只读内存区的字符串字面量。
char *str1 = "Hello, World!"; 

// 指向字符数组  
char string[] = "Hello, World!";  
char *str2 = string;

常量字符串

在C语言中,常量字符串是一个不可修改的字符数组,通常以字面量的形式表示。常量字符串被存储在程序的只读内存段中,因此它们的值在程序执行期间不能被修改。

常量字符串可以通过两种方式声明:

// 使用双引号直接声明
const char *str1 = "Hello, World!";

// 使用字符数组初始化并标记为常量
const char str2[] = "Hello, World!";

常量字符串通常用于函数参数,因为它们可以防止函数意外地修改传递给它的字符串。例如:

void print_string(const char *str) {  
    printf("%s\n", str);  
}

int main() {  
    const char *message = "Hello, World!";  
    // 安全地传递常量字符串 
    print_string(message); 
    return 0;  
}

这样做是一种良好的编程实践,因为它减少了程序出错的可能性。

8. 运算符

需要注意运算优先级,如果记不清,可以用括号。括号内的代码优先级最高

自己整理的优先级口诀(从高到低):

成员单目,四则运算;左右位移,大小判断;按位与或,逻辑条件;赋值,逗号。

算术运算符

运算符说明
+加法
-减法
*乘法
/除法
%取模(取余)
++自增
--自减
#include <stdio.h>  
  
int main() {  
    int a = 5;  
    int b = 3;  
    int result;  

    // 加法运算符  
    result = a + b;  
    printf("a + b = %d\n", result); // 输出:a + b = 8  

    // 减法运算符  
    result = a - b;  
    printf("a - b = %d\n", result); // 输出:a - b = 2  

    // 乘法运算符  
    result = a * b;  
    printf("a * b = %d\n", result); // 输出:a * b = 15  

    // 除法运算符  
    result = a / b;  
    printf("a / b = %d\n", result); // 输出:a / b = 1,因为a和b都是整数,结果会被截断为整数  

    // 取模运算符  
    result = a % b;  
    printf("a %% b = %d\n", result);// 输出:a % b = 2  

    // 自增运算符_后增  
    a = 5;
    printf("a = %d\n", a);          // 输出:a = 5
    printf("a++: a = %d\n", a++);   // 输出:a++: a = 5 
    printf("a = %d\n", a);          // 输出:a = 6  

    // 自增运算符_前增
    a = 5;
    printf("a = %d\n", a);          // 输出:a = 5
    printf("++a: a = %d\n",  ++a);  // 输出:++a : a = 6  
    printf("a = %d\n", a);          // 输出:a = 6 

    return 0;  
}

关系运算符

运算符说明
==等于 - 比较两个操作数的值是否相等,如果相等则条件为真。
!=不等于 - 比较两个操作数的值是否相等,如果不相等则条件为真。
<小于 - 左操作数的值是否小于右操作数的值,如果是则条件为真。
<=小于等于 - 左操作数的值是否小于或等于右操作数的值,如果是则条件为真。
>大于 - 左操作数的值是否大于右操作数的值,如果是则条件为真。
>=大于等于 - 左操作数的值是否大于或等于右操作数的值,如果是则条件为真。
#include <stdio.h>  
  
int main() {  
    int a = 5;  
    int b = 10;  

    // 等于运算符  
    if (a == b) {  
        printf("a is equal to b\n");  
    } else {  
        printf("a is not equal to b\n"); // 输出:a is not equal to b  
    }

    // 不等于运算符  
    if (a != b) {  
        printf("a is not equal to b\n"); // 输出:a is not equal to b  
    }

    // 大于运算符  
    if (b > a) {  
        printf("b is greater than a\n"); // 输出:b is greater than a  
    }

    // 小于运算符  
    if (a < b) {  
        printf("a is less than b\n"); // 输出:a is less than b  
    }
    return 0;  
}

位运算符

运算符说明
&按位与 - 两个操作数的每一位进行逻辑与运算。
|按位或 - 两个操作数的每一位进行逻辑或运算。
^按位异或 - 两个操作数的每一位进行逻辑异或运算。
~按位取反 - 操作数的每一位进行逻辑取反。
<<左移 - 操作数的每一位向左移动指定的位数。
>>右移 - 操作数的每一位向右移动指定的位数。
#include <stdio.h>  
  
int main() {  
    unsigned int a = 60;    // 60 = 0011 1100  
    unsigned int b = 13;    // 13 = 0000 1101  
    unsigned int c;  

    // 按位与运算符  
    c = a & b;               // 12 = 0000 1100  
    printf("a & b = %u\n", c);  

    // 按位或运算符  
    c = a | b;               // 61 = 0011 1101  
    printf("a | b = %u\n", c);  

    // 按位异或运算符  
    c = a ^ b;               // 49 = 0011 0001  
    printf("a ^ b = %u\n", c);  

    // 按位非运算符  
    c = ~a;                  // -61 = 1111....1100 0011 (注意:按位非的结果依赖于系统如何处理带符号整数)  
    printf("~a = %u\n", c);

    // 左移运算符  
    c = a << 2;              // 240 = 1111 0000  
    printf("a << 2 = %u\n", c);  

    // 右移运算符  
    c = a >> 2;              // 15 = 0000 1111  
    printf("a >> 2 = %u\n", c);  
  
    return 0;  
}

逻辑运算符

运算符说明
&&逻辑与 - 两个操作数都为真,条件才为真。
||逻辑或 - 两个操作数有一个为真,条件才为真。
!逻辑非 - 操作数的值取反。
#include <stdio.h>  
  
int main() {  
    int a = 5;  
    int b = 10;  
    int c = 0;  

    // 逻辑与运算符  
    if (a > 0 && b > 0) {  
        printf("a is positive and b is positive\n"); // 输出:a is positive and b is positive  
    }  

    // 逻辑或运算符  
    if (c == 0 || b > 15) {  
        printf("c is zero or b is greater than 15\n"); // 输出:c is zero or b is greater than 15  
    }

    // 逻辑非运算符  
    if (!(a == b)) {  
        printf("a is not equal to b\n"); // 输出:a is not equal to b  
    }

    // 逻辑异或运算符(通过其他逻辑运算符实现)  
    int xor_result = (a > b) ^ (b < c); // 如果 a > b 和 b < c 中恰好有一个为真,则 xor_result 为真  
    if (xor_result) {  
        printf("Exactly one of the conditions is true\n"); // 不会输出,因为两个条件都不为真  
    } else {  
        printf("None or both of the conditions are true\n"); // 输出:None or both of the conditions are true  
    }  
  
    return 0;  
}

赋值运算符

运算符说明举例
=赋值运算符,把右边的操作数赋给左边的操作数a = 10;
+=加且赋值运算符,把右边的操作数加上左边的操作数,然后赋给左边的操作数a += b; 等价于 a = a + b;
-=减且赋值运算符,把左边的操作数减去右边的操作数,然后赋给左边的操作数a -= b; 等价于 a = a - b;
*=乘且赋值运算符,把右边的操作数乘以左边的操作数,然后赋给左边的操作数a *= b; 等价于 a = a * b;
/=除且赋值运算符,把左边的操作数除以右边的操作数,然后赋给左边的操作数a /= b; 等价于 a = a / b;
%=取模且赋值运算符,把左边的操作数除以右边的操作数,然后赋给左边的操作数a %= b; 等价于 a = a % b;
<<=左移且赋值运算符,把左边的操作数向左移动右边的操作数的位,然后赋给左边a <<= b; 等价于 a = a << b;
>>=右移且赋值运算符,把左边的操作数向右移动右边的操作数的位,然后赋给左边a >>= b; 等价于 a = a >> b;
&=按位与且赋值运算符,把右边的操作数按位与左边的操作数,然后赋给左边a &= b; 等价于 a = a & b;
|=按位或且赋值运算符,把右边的操作数按位或左边的操作数,然后赋给左边a
^=按位异或且赋值运算符,把右边的操作数按位异或左边的操作数,然后赋给左边a ^= b; 等价于 a = a ^ b;
#include <stdio.h>  
  
int main() {  
    int a;  
    int b = 10;  
    float c;  
    float d = 7.5;  
  
    // 简单的赋值运算符  
    a = 5;  
    printf("a = %d\n", a); // 输出:a = 5  
  
    // 加法赋值运算符  
    b += 5; // 相当于 b = b + 5;  
    printf("b += 5: b = %d\n", b); // 输出:b += 5: b = 15  
  
    // 减法赋值运算符  
    b -= 3; // 相当于 b = b - 3;  
    printf("b -= 3: b = %d\n", b); // 输出:b -= 3: b = 12  
  
    // 乘法赋值运算符  
    c = 2.0;  
    c *= 3.5; // 相当于 c = c * 3.5;  
    printf("c *= 3.5: c = %.2f\n", c); // 输出:c *= 3.5: c = 7.00  
  
    // 除法赋值运算符  
    d /= 2.0; // 相当于 d = d / 2.0;  
    printf("d /= 2.0: d = %.2f\n", d); // 输出:d /= 2.0: d = 3.75  
  
    // 取模赋值运算符  
    a = 10;  
    a %= 3; // 相当于 a = a % 3;  
    printf("a %= 3: a = %d\n", a); // 输出:a %= 3: a = 1  
  
    return 0;  
}

其他运算符

运算符说明
sizeof(type)返回type类型所占的内存空间大小,以字节为单位。
,逗号运算符,从左向右依次计算每个操作数。
&取地址运算符,返回操作数的内存地址。
*解引用运算符,返回指针指向的内存地址的值。
条件 ? 表达式1 : 表达式2如果条件为真,则返回表达式1的值;否则返回表达式2的值。
// sizeof 运算符示例  
size_t size = sizeof(int); // 获取 int 类型在内存中的字节数

// 逗号运算符
int a = (1, 2, 3); // a 的值为 3,因为逗号运算符返回最右侧的表达式的结果

// 地址运算符
int x = 10;  
int *p = &x; // &x 是取地址运算符,获取变量 x 的地址  
int y = *p; // *p 是解引用运算符,获取指针 p 指向的值,即 x 的值

三目运算符示例如下:

#include <stdio.h>  

int main() {  
    int a = 10;  
    int b = 20;  
    int max;  
  
    // 使用三目运算符找出a和b中的较大值  
    max = (a > b) ? a : b;  
  
    printf("The maximum value is: %d\n", max); // 输出:The maximum value is: 20  
  
    return 0;  
}

9. 流程控制

流程控制是指根据一定的条件或逻辑来执行不同的代码块。C语言提供了多种流程控制语句,包括条件语句、循环语句和跳转语句。

条件语句

if-else

C 语言中用于条件判断的基本结构。它允许程序根据某个条件是否成立来执行不同的代码块。

if (condition)
{  
    // 如果 condition 为真(非零),则执行这里的代码  
}
else
{  
    // 如果 condition 为假(零),则执行这里的代码  
}

要求:根据分数,输出对应的评价。

分数范围评价
>= 90优秀
80-89良好
60-79及格
< 60不及格
其中 score 变量表示学生成绩,在不同的分数区间会输出不同的评价。示例如下
if(score >= 90)
{
    printf("优秀");
}
else if(score >= 80)
{
    printf("良好");
}
else if(score >= 60)
{
    printf("及格");
}
else
{
    printf("不及格");
}

switch-case

switch-case 是 C 语言中用于多分支选择的语句结构。它允许程序根据一个表达式的值来选择不同的执行路径。每个 case 后面跟着一个可能的值,如果 switch 表达式的结果与某个 case 的值匹配,那么就会执行该 case 下的代码,直到遇到 break 语句或者 switch 语句的末尾。

基本语法如下:

switch (expression) {
    case constant1:
    {
        // 代码块 1
    }break;
    case constant2:
    {
        // 代码块 2
    }break;
    // ...
    // 可以有更多的 case 语句
    default:
    {
        // 如果上面的 case 都不匹配,执行这里的代码
    }break;
}

值得注意的是:

  1. switch 表达式的结果必须是整型(int)、字符型(char)或枚举类型(enum)。
  2. 如果忘记写 break 语句,程序会继续执行下一个 case 的代码,直到遇到 break 或者 switch 语句的末尾,这通常称为“case 贯穿”(case fall-through)。在某些情况下,这是有意为之,用于共享多个 case 的代码,但通常应当谨慎使用,以免产生逻辑错误。
  3. default 语句是可选的,但它提供了一个处理未匹配情况的方法。如果 switch 表达式的值没有与任何 case 匹配,且没有 default 语句,则 switch 语句不会执行任何操作。
switch(num)
{
    case 1:
    {
        printf("one");
    }break;
    case 2:
    {
        printf("two");
    }break;
    default:
    {
        printf("zero");
    }break;
}

// 上述代码等价于
if(num == 1)
{
    printf("one");
}
else if(num == 2)
{
    printf("two");
}
else
{
    printf("zero");
}

循环语句

for

for 循环是 C 语言中用于重复执行一段代码块的结构,直到满足某个特定的终止条件。它通常用于遍历数组、执行固定次数的迭代任务等。

for 循环的基本语法如下:

for (初始化; 条件; 更新)
{
    // 循环体,即要重复执行的代码块
}
  • 初始化:在循环开始之前执行一次,通常用于设置循环控制变量的初始值。
  • 条件:在每次循环迭代开始时检查,如果为真(非零),则执行循环体;如果为假(零),则退出循环。
  • 更新:在每次循环迭代结束时执行,通常用于更新循环控制变量。

示例如下:

#include <stdio.h>  

int main()
{  
    int i;  
    for (i = 0; i < 5; i++)
    {  
        printf("%d\n", i);  
    }
    return 0;  
}

while

while 循环是 C 语言中用于重复执行一段代码块的结构,直到指定的条件不再满足为止。与 for 循环不同,while 循环没有初始化和更新表达式,而是依赖循环体内部的代码来更新循环条件。

while 循环的基本语法如下:

while (条件)
{
    // 循环体,即要重复执行的代码块
}
  • 条件:在每次循环迭代开始时检查,如果为真(非零),则执行循环体;如果为假(零),则退出循环。

下面是一个简单的 while 循环示例,用于计算从 1 加到某个数(比如 10)的总和:

#include <stdio.h>

int main() {
    int sum = 0;
    int i = 1;
    
    while (i <= 10) {
        sum += i;
        i++;
    }
    
    printf("1 到 10 的总和是: %d\n", sum);
    return 0;
}

do-while

do-while 循环是 C 语言中另一种循环结构,它与 while 循环类似,但有一个重要的区别:do-while 循环至少会执行一次循环体,即使循环条件在第一次检查时就为假。这是因为在执行循环体之后再检查循环条件,而不是在循环体之前。

do-while 循环的基本语法如下:

do
{
    // 循环体,即要重复执行的代码块
} while (条件);
  • 条件:在每次循环迭代结束后检查,如果为真(非零),则再次执行循环体;如果为假(零),则退出循环。

下面是一个简单的 do-while 循环示例,用于请求用户输入,直到输入了一个非零的整数为止:

#include <stdio.h>

int main()
{
    int number;
    do
    {
        printf("请输入一个非零整数:");
        scanf("%d", &number);
    } while (number == 0);
    
    printf("你输入了非零整数:%d\n", number);
    return 0;
}

do-while 循环特别适用于那些需要至少执行一次循环体的情况,无论循环条件是否满足。而在其他情况下,可能更倾向于使用 for 循环或 while 循环,因为它们提供了更多的灵活性和控制。

跳转语句

break

在 C 语言中,break 语句用于立即退出当前循环或 switch 语句。

例如在 forwhiledo-while 循环中,break 语句通常用于在满足特定条件时退出循环。这可以防止不必要的迭代,并允许程序提前结束循环。

下面是一个 while 循环中使用 break 的例子:

#include <stdio.h>

int main() {
    int i = 1;
    
    while (i <= 10) {
        if (i == 5) {
            break; // 当 i 等于 5 时退出循环
        }
        printf("%d ", i);
        i++;
    }
    
    printf("\n");
    return 0;
}

在这个例子中,当 i 的值等于 5 时,break 语句会被执行,从而退出 while 循环。因此,程序会打印出 1 到 4,然后退出循环。

continue

在 C 语言中,continue 语句用于跳过当前循环迭代中剩余的语句,并立即开始下一次迭代。

continue 通常用于在特定条件下跳过某些循环迭代,而不是完全退出循环。这与 break 语句不同,break 会完全退出循环。

下面是一个 for 循环中使用 continue 的例子:

#include <stdio.h>

int main() {
    int i;
    
    for (i = 1; i <= 10; i++) {
        if (i % 2 == 0) {
            continue; // 跳过偶数
        }
        printf("%d ", i); // 只打印奇数
    }
    
    printf("\n");
    return 0;
}

goto

goto语句一般用于错误处理,其他情况不建议使用(尤其是循环)

在 C 语言中,goto 语句用于无条件地跳转到程序中的另一个位置。goto 后面跟着一个标签(label),该标签标记了跳转的目标位置。虽然 goto 语句在某些情况下可能看起来方便,但它通常被认为是一种不良的编程实践,因为它可能导致代码流程不清晰,难以理解和维护。

下面是 goto 语句的一个简单示例:

#include <stdio.h>

int main() {
    int i = 0;

    if (i == 0) {
        goto done; // 如果 i 等于 0,则跳转到标签 done 处
    }

    printf("This will not be printed.\n");

done: // 标签 done 的位置
    printf("This will be printed.\n");
    return 0;
}

10. 函数

函数简介

模块化编程是一种软件设计方法,它将程序分解为小的、独立的部分,并使每个部分具有明确定义的接口。这可以使程序更加易于理解、维护和扩展。

函数在C语言中就是这种小的、独立的部分,它们可以与其他函数组合成更大的程序模块。

函数的作用

  • 代码重用
  • 提高代码的可读性和可维护性
  • 减少代码的冗余性
  • 支持模块化编程

函数的组成

  • 函数头

    包括函数的返回类型、函数名和参数列表。

  • 函数体

    一个代码块,它包含函数要执行的操作。

  • 函数返回值

    函数执行结束后返回给调用者的值。

  • 函数参数

    传递给函数的值。长度和类型由函数定义决定。

    • 形参

      函数定义中的参数。比如max函数的形参是x和y

    • 实参

      函数调用时传递给函数的值。比如max函数调用时,实参是a和b。

#include <stdio.h>

// 如果函数的定义,在函数调用之后,则需要函数声明

// 函数声明
int max(int x, int y);

int main(void)
{
    int a = 10;
    int b = 20;
    int result = max(a, b);
    printf("max(%d, %d) = %d\n", a, b, result);
    return 0;
}

int max(int x, int y)           // 函数头
{
    return x > y ? x : y;       // 函数体
}

11. 指针

指针可以直接访问和操作内存地址。

指针是一个变量,其值表示另一个变量的地址。通过指针,你可以间接地访问和操作那个变量。

指针的声明

要声明一个指针,需要在变量类型后面加上一个星号(*):

int *ptr;   // 声明一个指向 int 类型的指针

指针的初始化

指针需要被初始化为指向某个有效的内存地址。这通常是通过将一个变量的地址赋给指针来完成的:

int x = 10;

// & 运算符获取变量的地址
int *ptr = &x;  // 将ptr指向x的地址

使用指针访问值

可以通过解引用指针来访问它所指向的值。解引用操作使用星号(*):

int value = *ptr; // 解引用ptr,将ptr指向的值赋给value

指针的运算

指针可以进行一些基本的算术运算,如递增和递减,以及加法和减法。这些运算通常用于遍历数组:

int array[] = {1, 2, 3, 4, 5};
int *p = array; // 指针p指向数组的第一个元素

printf("%d\n", *p); // 输出数组的第一个元素,即1
p++;                // 指针p向后移动一个元素
printf("%d\n", *p); // 输出数组的第二个元素,即2

空指针

一个特殊的指针值是空指针(NULL),它表示不指向任何有效的内存地址:

int *ptr = NULL; // 声明并初始化一个空指针

指针与数组

在C语言中,数组名实际上是一个指向数组第一个元素的常量指针。因此可以使用数组名和指针来遍历数组。

int array[] = {1, 2, 3, 4, 5};
int *p = array; // 指针p指向数组的第一个元素

for (int i = 0; i < 5; i++) {
    printf("%d ", *p); // 输出数组的每个元素
    p++; // 移动到下一个元素
}

指针与函数

指针也常用于函数参数,允许函数修改调用者传递的实际变量:

void increment(int *num) {
    (*num)++; // 增加指针指向的值
}

int main() {
    int x = 10;
    increment(&x); // 传递x的地址给increment函数
    printf("%d\n", x); // 输出11,因为x的值在increment函数中被增加了
    return 0;
}

在这个例子中,increment 函数接收一个整数指针作为参数,并增加该指针指向的值。在 main 函数中,我们传递了变量 x 的地址给 increment 函数,因此 x 的值被修改了。

指针是C语言中一个强大且复杂的工具。可以进行低级别的内存操作,但也需要谨慎使用,以避免空指针解引用、野指针和内存泄漏等常见问题。

12. 结构体

在C语言中,结构体(struct)是一种复合数据类型,它允许将不同的数据类型组合成一个单一的类型。结构体常用于表示现实世界中的复杂对象,这些对象由多个不同类型的数据组成。

结构体的声明

要声明一个结构体,需要使用struct关键字,并给出结构体的名称和它所包含的成员列表。每个成员都有一个类型和一个名称。

struct person {
    char name[50];
    int age;
    float height;
};

在这个例子中,person结构体包含三个成员:name(一个字符数组)、age(一个整数)和height(一个浮点数)。

结构体的使用

声明了结构体之后,可以创建该类型的变量,并访问其成员。

#include <stdio.h>

struct person {
    char name[50];
    int age;
    float height;
};

int main() {
    struct person person1; // 创建一个Person类型的变量person1

    // 给person1的成员赋值
    strcpy(person1.name, "Alice"); // 需要#include <string.h>来使用strcpy
    person1.age = 30;
    person1.height = 1.65f;

    // 输出person1的成员
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.2f\n", person1.height);

    return 0;
}

注意,在上面的代码中,strcpy函数用于复制字符串到person1.name中,这是因为name是一个字符数组,不能直接赋值。需要包含头文件<string.h>来使用strcpy函数。

结构体的初始化

在定义结构体变量的同时初始化它的成员:

struct person person2 = {"Bob", 25, 1.75f};

但是,这种初始化方式要求结构体中的所有成员都是可以直接赋值的(即没有数组或结构体类型的成员)。如果结构体中包含数组或其他结构体,则需要在代码中单独初始化这些成员。

结构体指针

可以使用指针来访问结构体:

struct person person3 = {"Charlie", 40, 1.80f};
struct person *ptr = &person3; // 创建一个指向person3的指针

// 使用指针访问结构体的成员
printf("Name: %s\n", ptr->name); // 使用->运算符通过指针访问成员
printf("Age: %d\n", ptr->age);
printf("Height: %.2f\n", ptr->height);

在这里,->运算符用于通过结构体指针访问成员。这个运算符是结构体指针特有的,它结合了解引用和成员选择的功能。

13. 联合体

C语言中的联合体(union)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。但是,联合体在任何时候只能保存其成员中的一个值;当你给一个成员赋值时,其他成员的值将被覆盖。联合体提供了一种使用相同的内存位置来存储不同类型数据的方法,但是不能在同一时间访问这些不同类型的数据。

联合体的定义语法与结构体类似,但联合体中的成员共享同一块内存空间。示例如下

#include <stdio.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: %.2f\n", data.f);

    // 给字符数组成员赋值(这同样会覆盖之前的数据)
    strcpy(data.str, "Hello");
    printf("data.str: %s\n", data.str);

    return 0;
}

14. 枚举

小结 > 枚举常量默认是从0开始的,依次递增。

在C语言中,枚举(enum)是一种用户定义的类型,它可以为整数值赋予有意义的名称。这使代码更易于理解和维护。

枚举类型通常用于表示一组固定的常量值,比如一周的天数、颜色或状态代码等。

枚举的声明

要声明一个枚举类型,需要使用enum关键字,并为其指定一个名称。然后,在大括号{}内列出该枚举类型的所有可能值。每个值被称为枚举常量,它们之间用逗号分隔。

enum Day {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

在这个例子中,enum Day定义了一个枚举类型,它包含了一周的天数。默认情况下,枚举常量的值从0开始,依次递增1。你也可以显式地为每个枚举常量指定一个整数值。 比如下面这个例子,enum Color定义了四种颜色,并为每种颜色指定了一个整数值。

enum Color {
    RED = 1,
    GREEN = 2,
    BLUE = 4,
    YELLOW = 8
};

枚举的使用

一旦你声明了一个枚举类型,你就可以创建该类型的变量,并为它们赋值或进行其他操作。

#include <stdio.h>

enum Day {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

int main()
{
    enum Day today = Wednesday; // 为枚举变量today赋值

    // 输出today的值
    printf("Today is %d\n", today); // 输出Wednesday的整数值,通常是3

    // 使用枚举常量
    if (today == Wednesday)
    {
        printf("It's hump day!\n");
    }
    return 0;
}

在这个例子中,today是一个enum Day类型的变量,它被赋值为Wednesday。由于Wednesday是枚举类型Day中的第三个常量,并且默认情况下枚举常量的值从0开始递增,因此Wednesday的值为2(0代表Sunday,1代表Monday,2代表Wednesday)。

枚举的优点

使用枚举可以提高代码的可读性和可维护性。通过为整数值赋予有意义的名称,可以使代码更易于理解。如果将来需要更改枚举常量的值,只需要在一个地方进行修改,而不是在整个代码库中搜索和替换。

枚举的局限性

枚举在C语言中是有限的,因为它们本质上是整数类型,并且不支持函数、结构体或其他复杂的数据类型作为枚举值。此外,枚举常量本质上是常量表达式,它们的值在编译时就已经确定,并且不能在运行时更改。

15. 文件处理

程序从外部存储介质(如硬盘)读取数据或将数据写入外部存储介质。

C语言提供了标准库函数来执行文件操作,如打开文件、关闭文件、读取文件、写入文件等。

文件指针

在C语言中,文件是通过文件指针来访问的。文件指针是一个指向FILE类型结构的指针,该结构包含了文件的相关信息(如文件缓冲区、文件状态标志等)。

打开文件

使用fopen函数可以打开一个文件,并返回一个文件指针。

/**
 * @brief 打开文件
 * 
 * @param filename : 文件名
 * @param mode : 打开文件的模式
 */
FILE *fopen(const char *filename, const char *mode);
mode描述
r只读模式,如果文件不存在则返回NULL。
w写入模式,如果文件不存在则创建,存在则清空。
a追加模式,如果文件不存在则创建,存在则在文件末尾追加。
r+读写模式,文件必须存在。
w+读写模式,如果文件不存在则创建,存在则清空。
a+读写模式,如果文件不存在则创建,存在则在文件末尾追加。
b二进制模式,用于二进制文件。
t文本模式,用于文本文件。

关闭文件

使用fclose函数可以关闭一个已经打开的文件。

/**
 * @brief 关闭文件
 * 
 * @param stream : 要关闭的文件指针。
 */
int fclose(FILE *stream);

读取文件

可以使用fscanffgetsfread等函数从文件中读取数据。

/**
 * @brief 从文件中读取数据
 * 
 * @param stream : 要读取的文件指针。
 * @param format : 格式字符串。
 */
int fscanf(FILE *stream, const char *format, ...);

/**
 * @brief 从文件中读取字符串
 * 读取一行字符串,直到遇到换行符或文件结尾。
 * @param str : 字符串缓冲区。
 * @param n : 字符串缓冲区大小。
 * @param stream : 要读取的文件指针。
 */
char *fgets(char *str, int n, FILE *stream);

/**
 * @brief 从文件中读取数据
 * 
 * @param ptr : 数据缓冲区。
 * @param size : 数据项大小。
 * @param count : 要读取的数据项个数。
 * @param stream : 要读取的文件指针。
 */
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

写入文件

int fprintf(FILE *stream, const char *format, ...);
int fputs(const char *str, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

文件定位

/**
 * @brief 定位文件指针
 * 
 * @param stream : 要定位的文件指针
 * @param offset : 偏移
 * @param whence : 偏移位置
 */
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
偏移位置含义
SEEK_SET文件开头
SEEK_CUR当前位置
SEEK_END文件结尾

文件错误处理

int ferror(FILE *stream);
void clearerr(FILE *stream);

示例

#include <stdio.h>

int main() {
    FILE *fp;

    // 打开文件
    fp = fopen("example.txt", "w+");
    if (fp == NULL)
    {
        printf("Failed to open file.\n");
        return 1;
    }

    // 写入数据
    fprintf(fp, "Hello, World!\n");

    // 重置文件指针到文件开头
    rewind(fp);

    // 读取数据
    char buffer[100];
    if (fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("Read from file: %s", buffer);
    }

    // 关闭文件
    fclose(fp);
    return 0;
}

注意点

  1. 错误处理:在实际的程序中,应该始终检查fopenfscanffprintf等函数的返回值,以确保操作成功。如果发生错误(例如文件无法打开),程序应该能够适当地处理这种情况。

  2. 缓冲区溢出:在使用fgets时,要确保分配了足够的空间来存储读取的数据,并且指定了正确的最大字符数(包括空字符)。

  3. 文件模式:选择正确的文件打开模式非常重要。例如,如果你试图读取一个以只写模式打开的文件,将会失败。同样,如果你试图写入一个以只读模式打开的文件,也会失败。

  4. 关闭文件:一旦完成对文件的操作,应该立即关闭文件以释放系统资源。

  5. 文件位置:在使用fseekftell时要小心,确保你了解当前的文件位置以及你正在相对于哪个位置进行偏移。

  6. 二进制和文本模式:在Windows系统上,文本模式和二进制模式之间是有区别的。在文本模式下,写入文件的换行符(\n)会被转换为回车换行符(\r\n)。在二进制模式下,不会发生这种转换。因此,当处理二进制数据时,应该使用二进制模式打开文件。在UNIX和Linux系统上,文本模式和二进制模式没有区别。

  7. 可移植性:编写文件处理代码时,要考虑代码的可移植性。不同的操作系统和文件系统可能有不同的行结束符、文件路径分隔符等。

  8. 文件锁定:在多线程或多进程环境中,可能需要使用文件锁定来防止同时写入同一个文件的不同部分。

  9. 性能:对于大量的数据读写,考虑使用缓冲I/O(如freadfwrite)来提高性能。此外,还可以考虑使用内存映射文件或异步I/O等技术。

  10. 安全性:在处理文件时,要注意安全性问题。例如,不要在没有进行适当验证的情况下打开用户提供的文件路径,因为这可能会导致安全问题(如路径遍历攻击)。

16. 预处理指令

C语言预处理指令是编译器在编译源代码之前处理的指令。这些指令主要用于设置编译环境,告诉编译器如何处理源代码。预处理指令以#开头,并且它们都是编译器在编译程序之前处理的。C语言的主要预处理指令包括:

  1. #include:包含指令,用于包含其他文件的内容。这通常用于包含标准库或用户自定义的头文件。
#include <stdio.h>  // 包含标准输入输出库
#include "myheader.h"  // 包含用户自定义的头文件
  1. #define:定义指令,用于定义宏(可以是常量或函数)。
#define PI 3.14159  // 定义常量
#define SQUARE(x) ((x) * (x))  // 定义函数宏
  1. #undef:取消定义指令,用于取消已定义的宏。
#define PI 3.14159
...
#undef PI  // 取消PI的定义
  1. #if, #ifdef, #ifndef, #else, #elif, #endif:条件编译指令,用于根据条件决定是否编译某部分代码。
#ifdef DEBUG  // 如果定义了DEBUG,则编译下面的代码
    printf("Debugging information...\n");
#endif
  1. #pragma:编译器特定指令,用于向编译器传递特定的指令或信息。这些指令不是标准C语言的一部分,而是由特定的编译器定义的。
#pragma once  // 确保头文件只被包含一次(某些编译器支持)
  1. #line:行控制指令,用于改变编译器报告的错误或警告信息的行号。
#line 100  // 下一个错误或警告将报告为在第100行
  1. #error:错误指令,用于在预处理阶段产生错误。
#if !defined(PLATFORM)
#error PLATFORM must be defined!
#endif
  1. #warning(非标准):警告指令,用于在预处理阶段产生警告。
#warning This code is deprecated.
  1. #(单独使用):字符串化指令,将宏参数转换为字符串。
#define TO_STRING(x) #x
...
printf(TO_STRING(Hello, world!));  // 输出 "Hello, world!"
  1. ##(连接符):连接符,用于在宏定义中连接两个标记。
#define CONCAT(x, y) x ## y
...
int xy = CONCAT(x, y);  // 等价于 int xy = xy;

预处理器在编译程序之前处理这些指令,根据指令的内容修改源代码,然后编译器对修改后的源代码进行编译。预处理器还可以处理注释(/* ... *///),并在编译之前删除它们。

17. 编译与链接

在C语言编程中,编译和链接是两个关键步骤,它们分别由编译器和链接器完成,以将源代码转换为可执行程序。

编译流程

编译(Compilation)

编译是将C语言源代码(.c文件)转换为目标代码(.o或.obj文件)的过程。这个过程中,编译器会检查源代码的语法和语义,确保代码的正确性,并将高级语言(C语言)代码转换为机器语言代码。编译器还会生成一些元数据,如符号表,它包含了程序中所有标识符(如变量和函数名)的信息。

例如,使用GCC编译器,你可以通过以下命令来编译一个C源文件:

gcc -c myprogram.c

这个命令会生成一个名为myprogram.o的目标文件。

链接(Linking)

链接是将一个或多个目标文件以及可能需要的库文件合并成一个可执行文件的过程。链接器处理目标文件中的符号引用,将符号引用(如函数调用或全局变量引用)与定义(函数实现或全局变量分配的内存地址)关联起来。此外,链接器还会解析外部库中的函数和变量,并将它们与程序中的调用关联起来。

如果你有一个或多个目标文件,你可以使用链接器将它们链接成一个可执行文件。例如,使用GCC链接器,你可以这样做:

gcc myprogram.o -o myprogram

这个命令会将myprogram.o目标文件链接成一个名为myprogram的可执行文件。

静态库和动态库

在链接过程中,你可能需要链接到外部库。这些库可以是静态库(.a文件)或动态库(.so文件在Linux上,.dll文件在Windows上)。

  • 静态库:静态库在链接时被完全包含在最终的可执行文件中。这意味着可执行文件会变得更大,但不需要额外的库文件来运行。
  • 动态库:动态库在链接时并不完全包含在可执行文件中,而是作为一个独立的文件存在。当程序运行时,动态链接器会在运行时加载所需的库。这使得可执行文件更小,但运行时需要库文件的存在。

编译静态库的时候,如果用到了外部变量,那么在编译的时候就需要把外部变量的声明也带上。之前理解错了,实际上静态库的编译过程中变量声明也是可以编译通过的。比如封装好的中间层接口,预留底层接口和上册接口给应用调用。变量和函数类似,只是需要看情况实现。

编译和链接一步完成

你也可以在一步中同时完成编译和链接。例如,使用GCC,你可以这样做:

gcc myprogram.c -o myprogram

这个命令会编译myprogram.c并直接生成myprogram可执行文件,无需显式地生成目标文件。

注意事项

  • 在大型项目中,可能有多个源文件需要编译和链接。编译器和链接器支持处理多个文件,并且通常有构建系统(如Makefile或CMake)来自动化编译和链接过程。
  • 在链接时,必须确保所有需要的库都可用,并且链接器可以找到它们。这可能需要设置库路径或指定库的名称。
  • 有时,链接器会报告“未定义的引用”错误,这通常意味着某个函数或变量在代码中被引用了,但链接器找不到其定义。这可能是因为忘记编译某个源文件,或者没有正确地链接到包含定义的库。