本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《经典编程900例(C语言)》是一本面向所有层次程序员的C语言实践教程,以900个具体实例详细涵盖了从基础知识到进阶技术的各个方面。通过实践学习,读者将掌握C语言的基本概念如变量、数据类型、控制流程、函数、数组、指针、结构体、预处理指令、文件操作、动态内存分配、位操作、异常处理、多线程编程以及标准库函数。每个实例都紧密联系实际编程问题和解决方案,鼓励读者动手实践,以深化理解和提升编程能力,同时为深入学习其他编程语言和系统编程打下基础。 经典编程900例(C语言)

1. C语言基础知识

1.1 C语言简介

C语言是IT行业中广泛使用的一种高级编程语言。它由Dennis Ritchie在1972年于AT&T的贝尔实验室开发。C语言以其强大而灵活的特性,历经几十年的发展,依然保持其在系统编程和嵌入式开发领域的领先地位。

1.2 开发环境搭建

为了学习C语言,你需要搭建一个合适的开发环境。最常用的C编译器之一是GCC(GNU Compiler Collection)。你可以通过安装一个像MinGW或Cygwin这样的软件包来获取GCC,或者在Linux系统上直接使用包管理器安装。除此之外,集成开发环境(IDE)如Code::Blocks、Eclipse CDT或Visual Studio Code配合C/C++扩展,也是不错的选择,它们提供了代码编辑、编译、调试等一系列功能。

1.3 简单的C语言程序

一个简单的C语言程序通常包含主函数 main ,它是程序执行的入口点。下面是一个C语言的基本程序示例,它会输出"Hello, World!"到控制台。

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

在这个示例中, #include <stdio.h> 是一个预处理指令,用于包含标准输入输出头文件。 main 函数被调用后,执行 printf 函数输出字符串,然后返回0表示程序正常结束。

以上内容为你揭开了C语言的序幕,接下来章节中我们将更深入地探讨变量、数据类型、运算符、控制流以及函数等核心概念。

2. 变量与数据类型

2.1 C语言的数据类型概述

2.1.1 基本数据类型

在C语言中,基本数据类型包括整型(int)、浮点型(float和double)、字符型(char)以及布尔型(_Bool)。每种数据类型都用于存储不同类型的数据,如数字和字符。

  • 整型(int)用于存储整数,可以是正数、负数或零。整型还包含不同范围的子类型,如short int、long int等。
  • 浮点型包括单精度(float)和双精度(double)类型,用于存储带小数的数字。
  • 字符型(char)用于存储单个字符,并占用一个字节。
  • 布尔型(_Bool)用于表示逻辑值,其值为0(假)或1(真)。

2.1.2 构造数据类型

构造数据类型是基于基本数据类型创建的,包括数组、结构体(struct)、联合体(union)和枚举(enum)。

  • 数组是一组相同类型数据的集合,使用索引访问特定元素。
  • 结构体是将不同类型的数据组合成一个复合类型。
  • 联合体允许在相同的内存位置存储不同数据类型的数据。
  • 枚举类型是一组命名的整型常量集合。

2.1.3 字符串类型

在C语言中,字符串是以空字符('\0')结尾的字符数组。标准库函数 <string.h> 提供了一系列处理字符串的函数,例如 strcpy strlen strcmp 等。

2.1.4 void类型

void类型表示空类型,主要用作函数返回类型以表明不返回值,或者作为指针类型以表示一个通用指针。

2.2 变量的作用域与生命周期

2.2.1 全局变量与局部变量

变量的作用域决定了可以在程序的哪些部分访问该变量。

  • 全局变量在函数外部定义,其作用域为整个程序,可以从程序的任何位置访问。
  • 局部变量在函数内部定义,仅在函数内部可见,其生命周期从声明时开始到函数执行完毕。

2.2.2 变量存储类别

变量的存储类别决定了其在内存中的存储位置以及生命周期。

  • 自动存储类别(auto)是局部变量默认的存储类别,存储在栈内存中,生命周期仅限于声明它的函数。
  • 静态存储类别(static)用于局部变量时,变量仅初始化一次,且在函数调用间保持其值。全局变量默认为静态存储类别。

2.2.3 代码示例

#include <stdio.h>

void function(void) {
    auto int a = 1;  // 自动变量
    static int b = 1; // 静态变量
    a++;
    b++;
    printf("a = %d, b = %d\n", a, b);
}

int main() {
    for (int i = 0; i < 5; i++) {
        function();
    }
    return 0;
}

在上述代码中,函数 function 包含两个变量, a 为自动存储类别,每次调用时重新初始化为1,而 b 为静态存储类别,只在第一次调用时初始化为1,之后保持其值。

2.3 类型转换与类型限定符

2.3.1 隐式类型转换与显式类型转换

C语言中类型转换可以是隐式的或显式的。

  • 隐式类型转换发生在编译器为了操作的需要自动进行数据类型转换。比如整数赋值给浮点变量时,整数会自动转换为浮点数。
  • 显式类型转换通过使用强制类型转换运算符进行,例如 (int)a 将变量 a 强制转换为整型。

2.3.2 const与volatile限定符

限定符用来修饰变量,改变它们的默认行为。

  • const限定符用来声明变量的值为只读,任何尝试修改 const 变量的行为都是非法的。
  • volatile限定符告诉编译器该变量可能会被外部因素(如硬件或并发线程)修改,因此编译器在每次使用变量时都应从内存中重新读取该变量的值,而不能假设它不变。

2.3.3 代码示例

#include <stdio.h>

int main() {
    const int constNum = 10;
    volatile int volatileNum = constNum;

    // 下面的操作是非法的,试图修改const变量的值
    // constNum = 20;

    // 正确的强制类型转换,但不会改变constNum的值
    int num = (int)constNum;
    printf("num = %d\n", num);

    return 0;
}

在本代码示例中, constNum 是一个 const 限定符修饰的常量整型变量,它的值不能被修改。然后我们将它赋值给了 volatileNum ,即使 volatileNum 是普通的整型变量,但是这种赋值本身不会改变 constNum 的值。本段代码展示了一个强制类型转换的例子,并演示了 const 类型变量无法通过赋值修改其值。

3. 运算符与表达式

在计算机编程中,运算符是编写表达式的基础,用于指定在程序中执行的运算类型。表达式是运算符和操作数的组合,计算结果为一个值。在C语言及其它许多编程语言中,正确理解和运用运算符以及表达式是编写有效和高效代码的关键。

3.1 运算符的分类与使用

3.1.1 算术运算符

算术运算符是最基本的运算符之一,用于执行数学运算。在C语言中,常见的算术运算符包括加(+)、减(-)、乘(*)、除(/)和取余(%)。

int a = 10, b = 3;
int sum = a + b; // 加法
int difference = a - b; // 减法
int product = a * b; // 乘法
int quotient = a / b; // 整数除法,结果为3
int remainder = a % b; // 取余,结果为1

逻辑上,整数除法会舍去小数部分,而取余运算则返回两个整数相除的余数。需要注意的是,取余运算要求操作数为整数。

3.1.2 关系运算符

关系运算符用于比较两个值的大小,结果总是布尔值(在C语言中是整数)。常见的关系运算符包括大于(>)、小于(<)、等于(==)、不等于(!=)、大于等于(>=)和小于等于(<=)。

int a = 10, b = 3;
int isGreater = a > b; // 1,因为10大于3
int isNotEqual = a != b; // 1,因为10不等于3

关系表达式经常用于控制流程语句(如if语句)中,控制程序的执行路径。

3.1.3 逻辑运算符

逻辑运算符用于执行逻辑运算,包括逻辑与(&&)、逻辑或(||)和逻辑非(!)。它们在布尔逻辑中非常关键。

int a = 10, b = 3;
int isBothTrue = (a > 5) && (b < 10); // 1,因为两个子表达式都为真
int isEitherTrue = (a > 5) || (b > 10); // 1,因为至少一个子表达式为真
int isNotTrue = !(a == b); // 1,因为a不等于b,取反后为真

在编写条件表达式时,了解逻辑运算符的短路行为非常重要。例如,在表达式 (a != 0) && (1/a > 10) 中,如果 a 为0,则 1/a 会导致除以零的错误,但因为逻辑与运算符具有短路行为, 1/a 永远不会执行。

3.1.4 位运算符

位运算符直接在二进制级别上对整数的操作数进行操作。它们包括按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)和右移(>>)。

int a = 0b0100; // 4的二进制表示
int b = 0b0011; // 3的二进制表示
int andResult = a & b; // 按位与,结果为0b0000,即0
int orResult = a | b; // 按位或,结果为0b0111,即7
int xorResult = a ^ b; // 按位异或,结果为0b0111,即7
int notResult = ~a; // 按位取反,结果为0b1011,即-5(在有符号整数中)
int leftShiftResult = a << 1; // 左移一位,结果为0b1000,即8
int rightShiftResult = a >> 1; // 右移一位,结果为0b0010,即2

位运算符在处理硬件级编程、优化算法性能以及操作位字段时非常有用。

3.1.5 赋值运算符

赋值运算符用于将表达式的值赋给变量。基础的赋值运算符为 = 。除此之外,C语言还支持复合赋值运算符,例如加法赋值(+=)、减法赋值(-=)、乘法赋值(*=)、除法赋值(/=)、取余赋值(%=)、左移赋值(<<=)和右移赋值(>>=)。

int a = 10, b = 5;
a += b; // 等同于 a = a + b; 结果为15
a -= b; // 等同于 a = a - b; 结果为10
a *= b; // 等同于 a = a * b; 结果为50
a /= b; // 等同于 a = a / b; 结果为10
a %= b; // 等同于 a = a % b; 结果为0

复合赋值运算符提供了代码简洁性和执行效率的双重优势。

3.2 表达式的求值规则与优先级

3.2.1 表达式求值顺序

在C语言中,表达式的求值顺序是未定义的,除非运算符的优先级或结合律规定了特定的顺序。这意味着在某些复杂的表达式中,不同的编译器可能产生不同的结果。

3.2.2 运算符优先级与结合性

运算符的优先级决定了当多个运算符在同一个表达式中时,它们按照什么顺序执行。运算符的结合性决定了具有相同优先级的运算符是按照从左到右(左结合)还是从右到左(右结合)的顺序执行。

举个例子,乘法运算符(*)和除法运算符(/)具有比加法运算符(+)和减法运算符(-)更高的优先级。所以,在表达式 a + b * c 中, b * c 会先于 a + 执行。同时,所有算术运算符都是左结合的,这表示在表达式 a + b - c 中, a + b 会先执行,然后结果再与 c 进行减法运算。

3.3 运算符重载与表达式模板

3.3.1 C++中的运算符重载

C++扩展了C语言的功能,允许运算符重载。运算符重载是C++中的一个特性,允许开发者为类定义自己的运算符行为。这意味着几乎所有的运算符都可以被重新定义,以适用于类的对象。

class Complex {
public:
    int real, imag;
    Complex(int r, int i) : real(r), imag(i) {}
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    // 其他重载的运算符...
};

通过重载运算符,复杂的操作可以变得简单易懂。

3.3.2 表达式模板的概念与应用

表达式模板是C++模板元编程的一个高级特性,它可以在编译时优化计算表达式。表达式模板通过延迟计算和复用中间结果来提高表达式的运算效率。

虽然表达式模板在C语言标准中不存在,但对于C++来说,它们是提高数值计算库性能的关键技术之一。

通过本章节的介绍,我们了解了运算符及其在表达式中的作用,学习了它们的分类、使用方法以及求值规则。我们还探讨了C++中运算符重载和表达式模板的概念,这将有助于提升我们对更复杂编程技术的理解和应用。

4. 控制流程

控制流程是编程中的基础概念,它决定了程序的执行路径。在C语言中,控制流程通过各种语句和结构来实现,主要包括选择结构、循环结构和跳转语句。理解这些结构对于编写高效、可读性强的代码至关重要。

4.1 选择结构与条件分支

选择结构允许程序基于给定条件执行不同的代码路径。最常用的两种选择结构是if-else语句和switch语句。

4.1.1 if-else语句

if-else语句是最基本的条件分支结构,它允许在某个条件为真时执行一段代码,条件为假时执行另一段代码。if-else语句可以嵌套使用,以处理多个条件分支。

#include <stdio.h>

int main() {
    int number = 0;
    printf("Enter a number: ");
    scanf("%d", &number);

    if (number > 0) {
        printf("The number is positive.\n");
    } else if (number < 0) {
        printf("The number is negative.\n");
    } else {
        printf("The number is zero.\n");
    }
    return 0;
}

在上面的代码中,程序首先提示用户输入一个数字,然后根据这个数字是正数、负数还是零来输出不同的信息。使用if-else语句可以清晰地表达这种逻辑判断的需求。

4.1.2 switch语句

switch语句允许基于一个表达式的值来执行不同的代码分支。它通常用于替代多个if-else语句,使得代码更加简洁明了。需要注意的是,switch语句只能用于整型或枚举类型的表达式。

#include <stdio.h>

int main() {
    char grade = 'B';
    printf("Your grade is %c\n", grade);
    switch (grade) {
        case 'A':
            printf("Excellent!\n");
            break;
        case 'B':
        case 'C':
            printf("Good job\n");
            break;
        case 'D':
            printf("You passed\n");
            break;
        case 'F':
            printf("Better try again\n");
            break;
        default:
            printf("Invalid grade\n");
    }
    return 0;
}

在此代码中,switch语句根据输入的成绩等级(grade)输出不同的评价。case 'B' 和 case 'C' 的处理逻辑是一样的,所以它们可以共用一块代码,这说明switch语句中多个case可以共享相同的操作。如果没有匹配的case,将执行default分支。

4.2 循环结构与迭代控制

循环结构允许程序重复执行一段代码直到满足特定条件。C语言提供了三种基本的循环结构:for循环、while循环和do-while循环。

4.2.1 for循环

for循环是C语言中最常见的循环结构,它将初始化、条件判断和更新步骤集中在一起,使得循环控制更为集中。

#include <stdio.h>

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

上述程序将输出数字0到4。在for循环中,首先执行初始化( int i = 0 ),然后执行循环条件判断( i < 5 ),如果条件为真,则执行循环体中的代码。每次循环结束后,执行更新语句( i++ ),然后重复整个过程。

4.2.2 while与do-while循环

while循环和do-while循环都是基于条件的循环结构,不过它们的执行逻辑略有不同。

#include <stdio.h>

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

在while循环中,循环条件在每次迭代之前进行判断,如果条件不为真,则循环结束。

#include <stdio.h>

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

do-while循环则至少执行一次循环体内的代码,然后在每次迭代结束后检查条件是否为真,如果条件为真,则继续执行。这使得do-while循环适用于至少需要执行一次循环体的场景。

4.3 跳转语句与程序流程控制

跳转语句允许程序跳过某些部分,直接跳转到指定的位置执行。C语言中常用的跳转语句包括break、continue和goto。

4.3.1 break与continue的用法

break语句用于立即退出最近的封闭循环或switch语句,而continue语句则用于跳过当前循环的剩余部分,并开始下一次循环的迭代。

#include <stdio.h>

int main() {
    for (int i = 0; i < 10; i++) {
        if (i == 5) {
            break; // 跳出循环
        }
        if (i % 2 == 0) {
            continue; // 跳过当次循环的剩余部分
        }
        printf("%d\n", i);
    }
    return 0;
}

在这个例子中,当i等于5时,break语句使得程序跳出循环,而当i为偶数时,continue语句使得程序跳过当前迭代的剩余部分,即不输出偶数。

4.3.2 goto语句及其争议

goto语句提供了一种无条件跳转到程序中另一位置的方式。虽然goto语句在某些情况下可以简化代码,但其使用通常被认为是一种不好的编程习惯。

#include <stdio.h>

int main() {
    int i = 0;
    start:
    if (i < 5) {
        printf("%d\n", i);
        i++;
        goto start; // 无条件跳转到标签start
    }
    return 0;
}

在上述代码中,goto语句将程序无条件跳转到标签start,这会导致无限循环。goto语句的使用应当谨慎,因为过多的跳转可能会使程序的流程变得难以追踪,从而降低程序的可读性和可维护性。

在控制流程的探索中,理解不同结构的适用场景与潜在的影响,以及如何合理地使用跳转语句,对于编写高效且易于维护的代码至关重要。在后续的章节中,我们将继续探索函数的定义与声明,函数的作用域与链接性,以及函数指针与回调函数等更高级的主题。

5. 函数

5.1 函数的定义与声明

函数是C语言程序的核心构成单位,它为实现特定任务提供了一种结构化方式。函数定义与声明是实现函数的第一步,它告诉我们函数的名称、参数以及返回值。

5.1.1 函数原型与参数传递

函数原型声明了函数的名称、返回类型以及参数列表。它告诉编译器函数是如何被调用的,但不包括函数的具体实现。函数的参数可以通过值传递或引用传递。值传递传递参数值的副本给函数,而引用传递则传递参数的内存地址。

// 函数原型示例
int max(int a, int b); // 值传递的参数
void swap(int *x, int *y); // 引用传递的参数

5.1.2 函数返回值

函数可以返回一个值,这个值通过return语句返回。返回值类型必须与函数声明中指定的类型一致,或者可以隐式转换为声明的类型。

int add(int a, int b) {
    return a + b; // 返回两个整数的和
}

5.2 函数的作用域与链接性

函数在C语言中的作用域和链接性决定了它在程序中的可见范围和如何被其他文件引用。

5.2.1 内部链接与外部链接

函数的作用域可以是内部的也可以是外部的。内部链接函数(如static函数)只能在定义它们的文件内被访问,而外部链接函数可以在程序的任何地方被访问。

5.2.2 静态函数与动态函数

静态函数是通过static关键字定义的内部链接函数,它们不会与其他文件中的同名函数冲突。动态函数,或称全局函数,是外部链接函数,它们可以在程序的其他文件中被访问。

5.3 函数指针与回调函数

函数指针和回调函数是C语言高级特性之一,它们提供了一种动态调用函数的方法。

5.3.1 函数指针的声明与使用

函数指针是指向函数的指针。通过函数指针,可以将函数作为参数传递给另一个函数,或者将函数存储在数据结构中。

// 函数指针声明
int (*funcPtr)(int, int);

// 函数指针使用示例
int add(int a, int b) {
    return a + b;
}

int main() {
    funcPtr = add; // 将函数指针指向add函数
    int result = funcPtr(5, 3); // 通过函数指针调用add函数
    return 0;
}

5.3.2 回调函数的实现与应用

回调函数是作为参数传递给其他函数的函数。通过回调,可以在运行时动态地将一个函数的控制权交给另一个函数,这在实现某些算法和事件处理中非常有用。

5.4 标准库函数与自定义函数

C语言标准库提供了丰富的预定义函数,同时开发者也需要编写自己的自定义函数以满足特定需求。

5.4.1 标准库函数的分类与应用

C语言标准库函数大致可以分为输入/输出函数(如printf、scanf)、字符串处理函数(如strcpy、strlen)、数学函数(如sqrt、pow)等类别。这些函数通过头文件(如stdio.h、string.h、math.h)包含在程序中。

#include <stdio.h>

int main() {
    printf("Hello, World!\n"); // 调用标准库函数
    return 0;
}

5.4.2 自定义函数的设计原则与技巧

设计自定义函数时应遵循清晰、模块化、高内聚、低耦合的原则。函数应该尽可能的单一职责,完成一个具体的任务。此外,函数的设计应考虑异常处理、参数的有效性校验等细节。

// 一个有效的自定义函数示例
#include <stdlib.h>

int* createArray(int size) {
    // 动态内存分配应该总是配合错误检查
    int* arr = malloc(size * sizeof(int));
    if (arr == NULL) {
        // 如果内存分配失败,应返回NULL并可能打印错误信息
        fprintf(stderr, "Memory allocation failed\n");
        exit(EXIT_FAILURE);
    }
    return arr;
}

通过以上章节的学习,我们可以理解C语言中函数的概念、作用域、链接性以及如何使用函数指针和回调函数。同时,我们探讨了标准库函数的分类与应用,以及如何设计有效的自定义函数。这些知识是构建高效、可维护C语言程序的基础。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《经典编程900例(C语言)》是一本面向所有层次程序员的C语言实践教程,以900个具体实例详细涵盖了从基础知识到进阶技术的各个方面。通过实践学习,读者将掌握C语言的基本概念如变量、数据类型、控制流程、函数、数组、指针、结构体、预处理指令、文件操作、动态内存分配、位操作、异常处理、多线程编程以及标准库函数。每个实例都紧密联系实际编程问题和解决方案,鼓励读者动手实践,以深化理解和提升编程能力,同时为深入学习其他编程语言和系统编程打下基础。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐