C语言-深入指针

引言

C语言中的指针是理解内存管理和高效编程的关键。本文将深入探讨指针的基本概念、数组与指针的关系,以及指针在高级应用中的使用。

内存和地址

  • 计算机内存为一系列有序的、可寻址的存储单元。
  • 每个存储单元有一个唯一的地址。在C中,指针用于存储这些地址。

内存单元的编号 == 地址 == 指针 

首先,必须理解,计算机内是有很多的硬件单 元,而硬件单元是要互相协同⼯作的。所谓的协 同,⾄少相互之间要能够进行数据传递。 但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,用"线"连起来。 而CPU和内存之间也是有⼤量的数据交互的,所以,两者必须也⽤线连起来。 不过,我们今天关⼼⼀组线,叫做地址总线。 我们可以简单理解,32位机器有32根地址总线, 每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表示2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

指针变量和地址

取地址操作符(&)

那我们如何能得到a的地址呢?

这⾥就得学习⼀个操作符(&)-取地址操作符

int value = 5;
&value; // value的地址

指针变量和解引用操作符(*)

指针变量

那我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要 存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中。

int value = 5;
int *ptr = &value; // ptr现在包含value的地址

指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。

如何拆解指针类型

拆解指针类型是理解C语言中指针用法的一个关键步骤。指针类型基本上由两部分组成:它指向的数据类型和指针本身的层级(例如,单级指针、双级指针等)。

基本指针类型
  • 格式数据类型 *指针名;
  • 例子int *ptr;
  • 解释ptr 是一个指针,指向一个整型数 (int)。
  • 内存访问*ptr 访问指针指向的整型数的值。

解引用操作符

 C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这⾥必须学习⼀个操作符叫解引用操作符(*)。

#include <stdio.h>
int main()
{
    int a = 100;
    int* pa = &a;
    *pa = 0;
    return 0;
}

上⾯代码中第7行就使用了解引用操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0。

指针变量的大小

在C语言中,指针变量的大小通常与计算机的架构(即它是32位还是64位系统)有关,而与指针所指向的数据类型无关。这是因为指针存储的是内存地址,而内存地址的大小取决于计算机的地址空间大小。

  1. 32位系统:在32位系统上,指针的大小通常是4个字节(32位)。无论指针指向的是intfloatchar或任何其他类型,其大小都是4个字节。

  2. 64位系统:在64位系统上,指针的大小通常是8个字节(64位)。同样地,指针的大小与它指向的数据类型无关,统一是8个字节。

这意味着无论指针类型如何(例如int*double*char*或者是函数指针等),在同一架构的系统上,所有指针的大小都是一样的。

示例

以下是一个简单的C程序示例,用于显示不同类型指针的大小:

#include <stdio.h>

int main() {
    int *intPtr;
    double *doublePtr;
    char *charPtr;
    void (*funcPtr)();

    printf("Size of int pointer: %zu bytes\n", sizeof(intPtr));
    printf("Size of double pointer: %zu bytes\n", sizeof(doublePtr));
    printf("Size of char pointer: %zu bytes\n", sizeof(charPtr));
    printf("Size of function pointer: %zu bytes\n", sizeof(funcPtr));

    return 0;
}

在这个程序中,不同类型的指针被声明并使用sizeof运算符来获取它们的大小。在32位系统上,这些大小将都是4字节,在64位系统上将都是8字节。

指针变量类型的意义

在C语言中,指针变量的类型决定了:

  1. 指针算术的行为:当你对指针执行算术运算(如增加或减少指针)时,指针将按照它所指向的数据类型的大小来移动。例如,如果你有一个类型为int *的指针,而int通常是4个字节,那么增加指针将使它向前移动4个字节。对于char *char是1字节),指针增加将只移动1个字节。

  2. 解引用的结果:解引用指针时(即使用*运算符访问指针指向的值),返回的数据类型将是指针类型所指定的类型。这意味着,如果你有一个类型为double *的指针,解引用它将给你一个double类型的值。

  3. 指针类型转换的安全性:某些类型的转换可能是不安全的,因为它们可能会导致对内存的错误解释。类型强制转换可以在不同类型的指针之间进行,但必须小心进行,以避免错误的内存访问。

  4. 接口的清晰性:函数通过使用特定的指针类型作为参数或返回类型,可以更清晰地指示它们期望或提供的数据类型。

  5. 类型安全:强类型的指针有助于编译器检测代码中的错误,例如当意外地将一个指针赋值给一个不同类型的指针时。

  6. void* 指针:在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。

#include <stdio.h>

void printInt(void* ptr) {
    // 将void* 转换为int* 然后解引用
    printf("The value is: %d\n", *(int*)ptr);
}

int main() {
    int val = 5;
    void* ptr = &val;

    // 在调用之前,无需转换
    printInt(ptr);

    return 0;
}

 const修饰指针

在C语言中,使用const关键字修饰指针可以有几种不同的含义,具体取决于const出现的位置。这些含义对于保持代码的安全性和可读性是非常重要的。下面是const修饰指针的几种不同方式:

  1. 指向常量的指针:这种指针不能用来修改其指向的值,但指针本身的值(即它存储的地址)可以改变。

    const int *ptr;
    

    这里,ptr可以指向不同的int型变量,但不能通过ptr来修改这些变量的值。

  2. 常量指针:这种指针的值(指向的地址)不能改变,但可以修改其指向的值。

    int *const ptr;

    这里,ptr必须在声明时初始化,并且以后不能指向其他地址,但可以通过ptr修改其指向的值。

  3. 指向常量的常量指针:这种指针既不能修改其指向的值,也不能改变指针本身的值。

    const int *const ptr;

    这里,ptr的指向以及指向的值都不能改变。

使用场景

  • 当你想要确保函数不会修改传入的数据时,你可以使用指向常量的指针作为参数。
  • 当你想要确保指针总是指向同一个地址时,你可以使用常量指针。
  • 有时,你可能希望确保既不修改指针指向的数据,也不修改指针本身,这时可以使用指向常量的常量指针。

示例代码

void func(const int *ptrA, int *const ptrB, const int *const ptrC) {
    // ptrA can point to different int values but cannot change the int value it points to.
    ptrA = (const int*)1000; // Allowed
    //*ptrA = 5; // Not allowed, compilation error

    // ptrB cannot point to a different address but can change the int value it points to.
    //*ptrB = 5; // Allowed
    //ptrB = (int*)1000; // Not allowed, compilation error

    // ptrC cannot point to a different address nor change the int value it points to.
    //*ptrC = 5; // Not allowed, compilation error
    //ptrC = (const int*)1000; // Not allowed, compilation error
}

指针运算

指针运算是指针编程中的一个核心概念,它允许程序直接操作内存地址。在C语言中,可以对指针进行几种类型的运算:

  1. 加法(+):给指针加上一个整数,指针会向前移动若干个它所指向类型的大小。例如,如果指针p指向一个int(通常是4字节),p + 1会使指针p向前移动4字节。

  2. 减法(-):从指针中减去一个整数,指针会向后移动若干个它所指向类型的大小。如果你有p - 1,并且p是一个int指针,它会向后移动4字节。

  3. 递增(++)和递减(--):这些运算符会使指针向前或向后移动一个它所指向类型的大小。如果p是一个指向int的指针,那么p++会增加p的值,使其指向下一个int

  4. 指针减指针:当你从一个指针中减去另一个指针时,结果是两个指针之间的元素数量,而不是字节数。这只有在两个指针指向同一个数组时才有意义。

  5. 比较运算:指针之间可以使用比较运算符(如==, !=, <, >, <=, >=)。这通常用于检查两个指针是否指向同一个地址或者在进行指针的界限检查。

注意事项

  • 指针运算中的加法和减法是基于指针指向的数据类型的大小进行的,而不是简单地在地址值上加上或减去一个整数。
  • 你不应该对非数组类型的指针执行过界操作,这可能会导致未定义行为。
  • 当执行指针减法时,两个指针应该指向同一数组的不同元素,否则结果是未定义的。

示例

下面是一个使用指针运算的例子,显示了如何通过指针访问数组元素:

#include <stdio.h>

int main() {
    int array[] = {10, 20, 30, 40, 50};
    int *p = array; // 指向数组的第一个元素

    printf("第一个元素: %d\n", *p);     // 输出 10
    printf("第二个元素: %d\n", *(p + 1)); // 输出 20

    p++; // 指针递增,现在指向第二个元素
    printf("通过递增操作访问的当前元素: %d\n", *p); // 输出 20

    int distance = &array[4] - p; // 计算当前指针和第五个元素之间的距离
    printf("p与第五个元素之间的距离: %d\n", distance); // 输出 3,因为 p 现在指向第二个元素

    return 0;
}

在这个例子中,我们创建了一个整数数组并通过指针p访问它。我们使用指针运算来移动指针,并计算两个指针之间的距离。这些操作允许我们有效地遍历数组和访问数据。

野指针

野指针(Wild Pointer)是指那些没有被初始化或者已经释放的内存的指针。这些指针是危险的,因为它们指向的内存区域是不确定的,可能会导致程序的不稳定行为,甚至崩溃。在C语言中,处理野指针应当非常小心,避免程序中出现安全漏洞。

野指针的来源包括

  1. 未初始化的指针

    • 当一个指针被声明但没有被明确初始化时,它就是一个野指针。
    int *ptr; // 未初始化的指针
  2. 已经释放的内存的指针

    • 当使用free()函数释放了动态分配的内存之后,如果没有将指针设置为NULL,该指针仍然指向被释放的地址。
    int *ptr = malloc(sizeof(int));
    *ptr = 4;
    free(ptr);
    // ptr 现在是野指针
  3. 超出作用域的局部变量地址

    • 函数内部的局部变量在函数返回后不再存在,如果有指针指向这些局部变量的地址,那么这些指针也会成为野指针。
    int *func() {
        int local;
        return &local; // 返回指向局部变量的指针
    }

避免和处理野指针的策略

  1. 初始化指针

    • 声明指针时,初始化为NULL
    int *ptr = NULL;
  2. 使用完毕后清空指针

    • 当释放动态分配的内存后,将指针设置为NULL
    free(ptr); 
    ptr = NULL;
  3. 小心使用函数返回的指针

    • 确保函数返回的指针指向的是有效的内存区域。

示例代码

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = malloc(sizeof(int)); // 动态分配内存
    if (ptr != NULL) {
        *ptr = 10;
        printf("%d\n", *ptr);
    }

    free(ptr); // 释放内存
    ptr = NULL; // 将指针设置为 NULL

    // 现在 ptr 是安全的 NULL 指针,不再是野指针
    if (ptr != NULL) {
        *ptr = 20; // 这行不会执行,因为 ptr 是 NULL
    }

    return 0;
}

在这个示例中,我们动态分配了内存,使用完之后释放了它,并将指针设置为NULL,确保它不会变成野指针。这样可以确保指针不会意外地指向无效的内存区域。

assert断言

在C语言中,assert是一个宏,用于辅助调试程序。它检查特定的条件是否为真,并在条件为假时终止程序运行。使用assert可以帮助开发者捕捉代码中的逻辑错误,并在开发过程中尽早发现问题。

assert宏定义在assert.h头文件中,其工作原理如下:

  • 如果条件为真(非零),assert不做任何操作,程序继续运行。
  • 如果条件为假(零),assert会打印错误信息到标准错误输出(stderr),显示出错的文件名和行号,然后通过调用abort函数终止程序运行。

由于assert会在发布的产品中增加额外的开销,因此通常只在调试过程中启用。可以通过在包含assert.h之前定义宏NDEBUG来禁用assert。当NDEBUG被定义时,assert不会执行任何运行时检查。

使用assert的例子

#include <assert.h>

void printArray(int *array, size_t size) {
    assert(array != NULL); // 确保指针不是NULL
    for (size_t i = 0; i < size; ++i) {
        printf("%d\n", array[i]);
    }
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    printArray(numbers, 5); // 正常工作

    printArray(NULL, 5); // assert将失败,并终止程序
    return 0;
}

在这个例子中,如果printArray函数接收到一个NULL指针,assert会失败,并终止程序。如果assert失败,它将输出类似于以下的错误信息:

a.out: example.c:6: printArray: Assertion `array != NULL' failed.
Aborted (core dumped)

这个信息包含了程序终止的文件名(example.c)、函数名(printArray)、以及失败的条件(array != NULL)。这样可以帮助开发者快速定位问题。

指针的使用和传址调用

在C语言中,指针的使用非常广泛,它们提供了访问和修改内存位置的能力。传址调用(Call by Reference)是指针应用的一个重要方面,它允许函数修改调用者环境中的变量。

指针的使用

指针主要用于以下几个方面:

  1. 访问数组元素: 通过指针运算,可以遍历数组而无需使用数组索引。

  2. 字符串操作: 字符串在C中通过字符指针处理,C标准库中的许多字符串函数都需要字符指针作为参数。

  3. 动态内存分配: 使用malloccallocreallocfree等函数分配和释放内存时,指针用来指向这些内存区域。

  4. 实现数据结构: 指针用来创建复杂的数据结构,如链表、树和图。

  5. 函数传参: 如果需要在函数内修改变量本身的值,或者传递大型数据结构(例如大数组)以避免复制整个结构,指针会作为参数传递。

  6. 函数返回值: 当函数需要返回多个值或返回动态分配的内存时,可以使用指针。

传址调用

传址调用是一种函数参数传递方式,其中函数接收变量地址(通常是指针)作为参数。这允许函数直接修改传入参数的值,而不是在本地副本上操作。

以下是传址调用的一个简单示例:

#include <stdio.h>

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 10, b = 20;
    printf("Before swap: a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("After swap: a = %d, b = %d\n", a, b);
    return 0;
}

在上面的程序中,swap函数通过接收两个整数的地址,直接在内存中交换它们的值。调用swap(&a, &b)时,我们传递了ab的地址。函数内部对这些地址解引用,从而能够修改原始变量ab的值。在main函数中打印ab的值显示了它们已经被交换。

使用传址调用的好处是能够直接修改变量,节省内存(因为没有创建变量的副本),并且在处理大型数据结构时提高效率。然而,它也需要更多的注意来避免不小心修改数据,导致潜在的bug和安全问题。

数组名的理解

在C语言中,数组名代表了数组的起始地址。这是理解C语言数组的一个重要概念。以下是关于数组名的一些关键点:

  1. 数组名作为指针:当你声明一个数组,比如 int arr[10];arr 是一个指向数组首元素的指针。它存储了数组第一个元素的内存地址。

  2. 数组名和地址操作:数组名本身就是一个常量指针,你不能改变它的值。例如,arr = arr + 1; 这样的操作是非法的。

  3. 数组元素访问:你可以使用数组名和下标来访问数组中的元素,比如 arr[0] 访问第一个元素,arr[1] 访问第二个元素,等等。在内部,arr[i] 被解释为 *(arr + i),这里 arr 是指向数组首元素的指针,i 是索引,指示了从首元素开始的偏移量。

  4. 数组作为函数参数:当数组作为函数参数传递时,实际上传递的是数组的首地址。因此,在函数中,你不能通过参数得知数组的大小,这通常需要通过额外的参数来传递。

  5. 数组和指针的区别:尽管数组名可以被视为指向其首元素的指针,但数组和指针是不同的类型。数组是一块连续的内存区域,而指针仅仅是一个存储地址的变量。

理解这些概念有助于更好地理解C语言中的数组操作和内存布局。

在C语言中,使用 sizeof& 运算符与数组名一起时,它们的行为是特定的:

  1. sizeof(数组名):

    • 当你使用 sizeof 运算符在数组名上时,它返回整个数组所占用的内存大小,而不是数组首元素的大小或数组的指针大小。
    • 例如,如果有一个数组 int arr[10];,在一个系统上 int 类型占用4个字节,那么 sizeof(arr) 将会返回 40(因为数组有10个 int 元素,每个占用4个字节)。
    • 这与指针不同,因为对指针使用 sizeof 会返回指针本身的大小,而不是它指向的内存大小。
  2. &数组名:

    • 当你使用 & 运算符在数组名上时,它返回数组的地址。但要注意,这个地址的类型和数组首元素的指针类型不同。
    • 对于数组 int arr[10];&arr 返回的是指向整个数组的指针,其类型为 int (*)[10],即指向含有10个整数的数组的指针。
    • 这与仅仅使用数组名作为指针不同。在大多数情况下,数组名被解释为指向其第一个元素的指针,但是当使用 & 时,它代表指向整个数组的指针。

使用指针访问数组

在C语言中,使用指针访问数组是一种常见且强大的技术。数组名本质上是指向数组第一个元素的指针,这意味着你可以使用指针来遍历和操作数组中的元素。以下是使用指针访问数组的几个关键步骤:

  1. 声明和初始化指针:首先,声明一个指针并将其初始化为指向数组的第一个元素。例如,如果有一个整数数组 int arr[10];,可以使用 int *ptr = arr; 来初始化指针。

  2. 通过指针访问元素:你可以使用指针加上偏移量来访问数组中的任何元素。例如,*(ptr + 2) 访问数组的第三个元素(因为数组索引从0开始)。

  3. 指针运算:指针可以递增 (ptr++) 或递减 (ptr--),这样它们会指向数组中的下一个或上一个元素。这在循环中特别有用,用于遍历数组。

  4. 指针和数组下标:使用指针访问数组元素等价于使用数组下标。即 *(ptr + i) 等同于 arr[i]

  5. 越界访问的危险:使用指针时,你需要确保不会越过数组的边界。C语言不会检查数组边界,因此越界访问可能会导致未定义行为,包括程序崩溃。

  6. 指针类型的重要性:指针的类型决定了指针运算的行为。例如,对于 int *ptrptr++ 会增加 ptr 的值以指向下一个整数(通常是4或8字节,取决于系统和编译器)。

  7. 通过指针遍历数组:你可以使用指针在循环中遍历整个数组。例如,使用 for 循环从数组的开始到结束遍历数组。

这种使用指针访问和操作数组的方法提供了灵活性,并且在某些情况下比使用传统的数组索引更有效。然而,它也需要程序员对内存布局和指针算术有深刻理解。

一维数组传参的本质

一维数组作为参数传递给函数时,其本质是通过指针传递。在C语言中,当数组作为函数参数时,传递的不是整个数组的拷贝,而是数组的首地址。这意味着在函数内部对数组元素所做的修改会影响到原始数组。以下是一维数组传参的关键点:

  1. 数组退化为指针:当数组作为参数传递给函数时,它被自动退化(或转换)为一个指针,该指针指向数组的第一个元素。例如,如果你有一个数组 int arr[10]; 并将其传递给一个函数,函数参数可以被声明为 int *param

  2. 无法在函数中获取数组大小:由于数组被退化为指针,函数无法直接知道数组的大小。因此,通常需要通过额外的参数来传递数组的大小。

  3. 在函数内部的操作影响原数组:由于传递的是数组的地址,因此在函数内部对数组的任何修改都会影响到原数组。

  4. 函数声明:在函数声明中,数组参数可以用不同的方式表示,例如 void myFunction(int arr[])void myFunction(int *arr)。这两种声明方式本质上是相同的。

  5. 传递部分数组:可以通过传递数组的某个元素的地址来实现对数组部分的传递,例如 myFunction(&arr[2]) 将传递一个指向 arr 的第三个元素的指针。

理解这一点对于处理C语言中的数组和函数交互非常重要,因为它影响着函数如何访问和修改数组数据。

冒泡排序

冒泡排序是一种简单的排序算法,它通过重复遍历要排序的数列,比较两个相邻元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行的,直到没有再需要交换的元素为止,这时数列就完全排序好了。以下是冒泡排序的基本步骤:

  1. 比较相邻的元素:如果第一个元素比第二个元素大(对于升序排序),就交换它们两个。

  2. 对每一对相邻元素做同样的工作:从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 重复步骤1~3,直到排序完成。

下面是一个冒泡排序的C语言实现示例:

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    int i, j, temp;
    for (i = 0; i < n-1; i++)     
        for (j = 0; j < n-i-1; j++) 
            if (arr[j] > arr[j+1]) {
                // 交换 arr[j] 和 arr[j+1]
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr)/sizeof(arr[0]);
    bubbleSort(arr, n);
    printf("Sorted array: \n");
    for (int i=0; i < n; i++)
        printf("%d ", arr[i]);
    return 0;
}

在这个示例中,bubbleSort 函数实现了冒泡排序算法。它接受一个整数数组和数组的长度,然后对数组进行排序。主函数 main 中创建了一个数组,并调用 bubbleSort 对其进行排序,然后打印排序后的数组。

冒泡排序的时间复杂度为 O(n2),在最坏的情况下和平均情况下都是这样,这使得它在处理大数据集时效率不高。然而,由于其实现简单,它在理解和实现基本排序算法时仍然是一个很好的选择。

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?

二级指针,或称为指向指针的指针,是C语言中一个重要的概念,它可以存储另一个指针的地址。这种特性使得二级指针在处理动态分配的数据结构(如链表和树)或数组的数组(如动态分配的二维数组)时非常有用。以下是二级指针的一些关键概念:

  1. 定义二级指针:你可以通过在声明时使用两个星号来定义一个二级指针。例如,int **ptr; 声明了一个可以存储整数指针地址的指针。

  2. 使用二级指针

    • 当你有一个指针变量,比如 int *p;,并且你想要存储这个指针的地址,你可以使用二级指针。
    • 例如,ptr = &p; 这里 ptr 存储了 p 的地址。
  3. 访问二级指针指向的值

    • 要访问原始值,你需要两次解引用。例如,假设 int value = 10; int *p = &value; int **ptr = &p;,那么 **ptr 将给出 10
    • 第一次解引用(*ptr)会给你指向整数的指针,第二次解引用(**ptr)会给你整数值本身。
  4. 用途

    • 二级指针在多种情况下都很有用,尤其是在需要修改指针本身的函数参数时。例如,改变指针所指向的内存地址。
    • 它们也用于创建和处理动态分配的多维数组。
  5. 注意事项

    • 使用二级指针时,必须小心确保所有的解引用操作都是安全的,避免访问未初始化或无效的内存。
    • 由于多重解引用可能会使代码难以理解和维护,因此应当谨慎使用。

二级指针的这些特性使得它们在高级数据结构和算法实现中非常有价值,尽管它们可能会带来更复杂的编码和调试挑战。

指针数组

指针数组是在C语言中一个常用的概念,它是一个数组,其每个元素都是一个指针。这种数组可以用来存储多个指针,使得你可以通过数组索引来访问这些指针。指针数组通常用于存储字符串数组(字符串本身是字符指针)或者是指向不同数据结构的指针。以下是有关指针数组的几个关键点:

  1. 定义指针数组:指针数组是通过在数组声明中使用星号来定义的。例如,int *arr[10]; 声明了一个可以存储10个整数指针的数组。

  2. 初始化指针数组:指针数组的每个元素都必须单独初始化。例如,如果你有一个整数数组 int nums[] = {10, 20, 30};,你可以通过 int *arr[3] = {&nums[0], &nums[1], &nums[2]}; 来初始化指针数组。

  3. 访问指针数组元素:访问指针数组的元素与访问普通数组的方式相同,但每个元素是一个指针。例如,arr[0] 会访问第一个指针。

  4. 解引用指针数组元素:由于指针数组的元素是指针,因此你可以通过解引用来访问指向的值。例如,*arr[0] 会给出 arr[0] 指针所指向的整数值。

  5. 用途:指针数组常用于存储字符串。例如,char *strArr[] = {"hello", "world", "!"}; 这里,strArr 是一个指针数组,每个元素都指向一个字符串。

  6. 区别于二维数组:指针数组与二维数组不同。在指针数组中,数组的每个元素都是一个指针,可能指向不同的内存区域。而在二维数组中,数据是连续存储的。

  7. 动态内存管理:指针数组常用于动态内存管理。每个指针可以动态地分配内存,并指向不同大小的数据块。

指针数组提供了一种灵活的方式来处理多个指针和动态数据结构,但它也需要程序员仔细管理指针,以避免诸如内存泄露或野指针等常见的内存问题。

字符指针变量

字符指针变量在C语言中是用来存储字符或字符串(即字符数组)的地址的指针。这些指针非常有用,尤其是在处理字符串时。以下是关于字符指针变量的一些关键点:

  1. 定义字符指针变量:可以通过使用 char * 类型来定义一个字符指针变量。例如,char *str; 声明了一个名为 str 的字符指针变量。

  2. 指向字符串:字符指针通常用来指向字符串。字符串在C语言中被存储为字符数组,以空字符('\0')结尾。例如,char *str = "Hello, world!"; 这里,str 指向字符串 "Hello, world!" 的第一个字符。

  3. 字符串字面量和字符指针:当字符指针指向一个字符串字面量时,这个字符串通常存储在程序的只读数据段中。这意味着你不能通过这个指针来修改字符串的内容。

  4. 访问字符:通过字符指针,你可以访问或者修改它指向的字符数组中的字符。例如,str[0] 访问第一个字符,str[1] 访问第二个字符,等等。

  5. 字符串操作:C标准库提供了多种函数来处理字符串,如 strcpy()strcat()strlen() 等,这些函数接受字符指针作为参数。

  6. 动态内存分配:字符指针也可以与动态内存分配函数(如 malloc()free())一起使用,来动态地分配和释放存储字符串的内存。

  7. 注意事项

    • 在使用字符指针指向的字符串时,必须确保字符串是以空字符结尾的,因为大多数字符串处理函数都依赖于空字符来确定字符串的结束。
    • 当使用动态分配的字符串时,应当小心内存泄漏和野指针的问题。

字符指针是C语言处理字符串的一种非常基本且强大的方式,它们在多种情境下都非常有用,从简单的字符串操作到复杂的数据结构处理。

数组指针变量

数组指针变量在C语言中是指向数组的指针。这种指针不仅指向数组的第一个元素,而且还保留了数组元素的类型信息和数组的大小(当作为函数参数时除外)。以下是关于数组指针变量的一些关键点:

  1. 定义数组指针变量:数组指针的定义包括指针类型和数组的维度。例如,int (*ptr)[5]; 定义了一个指针,它指向一个包含5个整数的数组。

  2. 指向数组:数组指针可以指向一个数组。例如,如果有一个数组 int arr[5];,你可以通过 ptr = &arr;ptr 指向这个数组。

  3. 访问数组元素:通过数组指针访问数组元素时,需要先解引用指针,然后使用下标。例如,(*ptr)[2] 访问指针指向的数组的第三个元素。

  4. 与多维数组的关系:数组指针在处理多维数组时非常有用。例如,对于一个二维数组 int arr[3][5];,你可以有一个指向数组第一维的指针 int (*ptr)[5] = arr;

  5. 数组指针和指针数组的区别:数组指针是一个指向数组的单个指针,而指针数组是包含多个指针的数组。例如,int *arr[5]; 是一个包含5个整数指针的数组。

  6. 作为函数参数:当数组指针作为函数参数传递时,它可以用来传递多维数组的尺寸信息。例如,可以定义一个函数 void func(int (*ptr)[5]) 来接受一个指向含有5个整数的数组的指针。

  7. 注意事项

    • 确保在使用数组指针之前已经正确地初始化了它。
    • 当使用数组指针来访问数组元素时,要注意数组的实际大小,以避免越界访问。

数组指针是C语言中处理数组,特别是多维数组时的一个强大工具,它提供了一种灵活的方式来处理和传递数组数据。

⼆维数组传参的本质

在C语言中,二维数组作为参数传递给函数的本质与一维数组类似,但有一些特别之处。当二维数组作为参数传递时,实际上传递的是指向数组第一行的指针。这种传递方式有以下几个关键点:

  1. 数组退化为指针:当二维数组作为函数参数传递时,它退化为指向其第一行的指针。这意味着,你不再拥有数组的全部尺寸信息。

  2. 函数参数声明

    • 函数接收二维数组参数时,必须至少指定数组第二维度的大小。例如,如果你有一个二维数组 int arr[3][4];,那么在函数参数中你可以声明为 void myFunction(int arr[][4])void myFunction(int (*arr)[4])
    • 第一个维度可以省略,因为数组退化为指针,但第二个维度必须指定,以便编译器知道如何计算行间的偏移量。
  3. 访问数组元素:在函数内部,你可以像操作普通二维数组那样操作这个参数,例如使用 arr[i][j] 来访问元素。

  4. 传递数组的部分:你可以传递二维数组的一部分,只要正确指定了起始行的地址和列的大小。例如,myFunction(&arr[1]) 会传递从第二行开始的数组部分。

  5. 动态二维数组:对于动态分配的二维数组,情况略有不同,因为你可能使用了指针数组或单个指针来模拟二维数组。在这种情况下,你需要传递指向整个数组的指针或指针的指针。

  6. 注意事项

    • 当传递二维数组时,保持数组尺寸信息的准确性至关重要,尤其是在多维数组的情况下。
    • 与一维数组一样,对函数参数中的二维数组的修改将影响原始数组,因为传递的是数组的引用(地址)。

理解二维数组在作为参数传递时的行为,对于编写能够正确处理多维数组数据的函数非常重要。

函数指针变量

函数指针变量在C语言中是一种特殊类型的指针,用于存储函数的地址。这使得程序可以通过指针来调用不同的函数,提供了一种灵活的方法来实现回调函数、跳转表等功能。以下是关于函数指针变量的一些关键点:

  1. 定义函数指针变量:函数指针的定义需要指定它所指向的函数的返回类型和参数类型。例如,int (*funcPtr)(int, int); 定义了一个指针 funcPtr,它可以指向任何接受两个整数参数并返回一个整数的函数。

  2. 初始化函数指针:可以将一个函数的地址赋给函数指针变量。例如,如果有一个函数 int add(int a, int b) { return a + b; },则可以通过 funcPtr = add; 来初始化函数指针。

  3. 通过函数指针调用函数:一旦函数指针被赋予了一个函数的地址,就可以通过它来调用这个函数。例如,int result = funcPtr(3, 4); 会调用 add 函数。

  4. 作为参数传递:函数指针可以作为参数传递给其他函数。这允许动态地改变被调用的函数,是回调函数和函数式编程技术在C语言中的基础。

  5. 返回函数指针:函数也可以返回函数指针,这为创建更加动态的程序逻辑提供了可能。

  6. 数组和结构体中的函数指针:函数指针可以存储在数组或结构体中,为程序提供更多的灵活性和动态行为。

  7. 注意事项

    • 定义函数指针时,必须确保指针的类型与所指向函数的类型完全一致。
    • 在使用函数指针前,应确保它已经被正确初始化,以避免调用无效的函数地址。

函数指针的使用可以使C语言程序更加模块化和灵活,但也需要仔细管理,以确保程序的正确性和可维护性。

函数指针数组

函数指针数组是一个数组,其每个元素都是一个指向函数的指针。在C语言中,这种数组非常有用,尤其是在需要根据不同的条件调用不同函数的情况下。以下是有关函数指针数组的一些关键点:

  1. 定义函数指针数组:函数指针数组的定义需要指定它所包含的函数指针的类型。例如,int (*funcPtrArr[5])(int, int); 定义了一个包含5个函数指针的数组,每个指针可以指向接受两个整数参数并返回一个整数的函数。

  2. 初始化函数指针数组:可以将具有相同签名的函数的地址分配给数组的各个元素。例如:

    int add(int a, int b) { return a + b; }
    int subtract(int a, int b) { return a - b; }
    // 其他函数声明...
    
    funcPtrArr[0] = add;
    funcPtrArr[1] = subtract;
    // 其他函数指针赋值...
    
  3. 通过数组调用函数:一旦函数指针数组被初始化,可以通过数组索引来调用不同的函数。例如,int result = funcPtrArr[0](5, 3); 会调用 add 函数。

  4. 用途:函数指针数组常用于实现简单的函数调度或状态机,例如根据用户输入或程序状态调用不同的函数。

  5. 函数指针类型匹配:确保所有赋给函数指针数组的函数具有相同的参数列表和返回类型,这对于保证程序的正确性至关重要。

  6. 数组大小管理:管理函数指针数组的大小很重要,以避免越界访问或未初始化的函数指针调用。

  7. 注意事项

    • 在使用函数指针数组之前确保所有的元素都已经正确初始化。
    • 考虑使用适当的错误检查,特别是在动态调用数组中的函数指针时。

函数指针数组提供了一种灵活的方式来组织和调用一系列的函数,使得代码更加模块化和易于管理。然而,这也要求程序员对指针、函数指针以及数组有深刻的理解。

 转移表

在编程中,转移表(也称为跳转表或分派表)是一种用于实现条件分支和函数调度的技术。它通常由一个函数指针数组构成,允许程序根据索引直接跳转到相应的函数。这种方法在C语言中尤为常见,尤其适用于实现状态机或避免复杂的 if-elseswitch 语句。以下是转移表的一些关键特点:

  1. 定义转移表:转移表通常定义为一个函数指针数组。例如,void (*table[])(void) = {func1, func2, func3}; 定义了一个转移表,其中包含指向三个不接受参数并返回 void 的函数的指针。

  2. 初始化:转移表在定义时通常会被初始化,其中每个元素都指向一个具体的函数。

  3. 使用:根据某些条件(如用户输入、程序状态、事件等)来选择数组中的适当函数指针,并调用相应的函数。

  4. 优点

    • 提高效率:使用转移表可以减少条件判断,特别是在有许多分支时。
    • 代码清晰:它提供了一种清晰且易于维护的方式来处理复杂的分支逻辑。
  5. 示例应用

    • 状态机:在状态机实现中,根据当前状态和输入选择相应的处理函数。
    • 命令解析器:在命令行工具中,根据用户输入的命令调用相应的处理函数。
  6. 注意事项

    • 确保所有函数具有相同的签名(即相同的参数类型和返回类型)。
    • 在使用转移表之前,确保所有的函数指针都已经被正确初始化。
    • 在数组索引之前,进行适当的边界检查,以避免越界访问。

转移表是一种有效的编程技术,可以使代码更加模块化、灵活且易于扩展。然而,它也要求程序员对数组和函数指针有深刻的理解,并小心地管理每个指针的状态。

回调函数是什么?

回调函数是一种在编程中常见的技术,它允许将一个函数作为参数传递给另一个函数。这种机制使得程序能够在运行时决定要调用的具体函数,从而提高程序的灵活性和可扩展性。以下是关于回调函数的一些关键点:

  1. 定义:回调函数是一个通过函数指针传递给另一个函数的函数。接收回调函数的函数将在适当的时候调用传递给它的这个函数。

  2. 用途

    • 异步处理:在事件驱动或异步编程中,当特定事件发生时,回调函数被调用。
    • 自定义操作:在像排序算法这样的通用函数中,回调函数用于定义元素比较的方式。
    • 通知机制:在某些任务完成后,通过回调函数通知其他部分程序。
  3. 如何工作

    • 函数接受一个或多个函数指针作为参数。
    • 这些函数指针指向要被回调的函数。
    • 当达到触发条件时,回调函数被执行。
  4. 示例:在C语言中,qsort 标准库函数就使用了回调函数。它允许用户定义一个比较函数,然后将这个比较函数的指针传递给 qsortqsort 使用这个比较函数来确定数组元素的排序顺序。

  5. 优点

    • 灵活性:允许在运行时确定要执行的操作。
    • 解耦:帮助将函数的逻辑从执行逻辑中分离出来。
  6. 注意事项

    • 确保回调函数与其接口的期望签名(参数和返回类型)匹配。
    • 考虑到回调函数可能会引入程序流程中的不确定性,应确保代码逻辑清晰。

回调函数是一种强大的编程工具,它为编程提供了高度的灵活性和扩展性,特别是在处理事件驱动编程、异步操作和自定义化行为时。

qsort使用举例

qsort 函数是 C 语言标准库中的一个快速排序实现,用于对数组进行排序。它的强大之处在于它的通用性——你可以用它来排序任何类型的数组。为此,你需要提供一个比较函数,来告诉 qsort 如何比较数组中的两个元素。下面是一个使用 qsort 的简单示例:

假设我们有一个整数数组,我们想对其进行升序排序:

#include <stdio.h>
#include <stdlib.h>

// 比较函数,用于比较两个整数
int compare(const void *a, const void *b) {
    const int *ia = (const int *)a; // 将 void 指针转换为 int 指针,然后取值
    const int *ib = (const int *)b;
    return *ia - *ib; // 升序排序
}

int main() {
    int arr[] = {45, 67, 12, 89, 34, 23, 0, 78};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 使用 qsort 排序
    qsort(arr, n, sizeof(int), compare);

    // 打印排序后的数组
    printf("Sorted array: \n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

在这个示例中:

  • compare 函数是我们自定义的比较函数,它将两个 void 指针转换为 int 指针,并返回它们差的符号,从而决定排序的顺序(在这个例子中是升序)。

  • qsort 函数调用时,我们传递了数组 arr,数组大小 n,单个元素的大小 sizeof(int),以及比较函数 compare

  • qsort 会根据 compare 函数提供的比较逻辑来对数组进行排序。

这个 qsort 使用的例子展示了如何使用 C 语言标准库中的快速排序算法来对整数数组进行排序。通过更改比较函数,你可以轻松地修改排序的顺序(比如改为降序)或对不同类型的数据进行排序(比如字符串)。

qsort函数的模拟实现

模拟实现 qsort 函数是一个很好的练习,可以帮助理解快速排序算法以及函数指针在C语言中的使用。下面是一个简单的 qsort 函数的模拟实现,使用了快速排序算法的基本思想。

但使用冒泡排序来模拟 qsort 函数是一个较为直观的任务,因为冒泡排序算法比快速排序算法简单。以下是一个使用冒泡排序模拟 qsort 函数的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 用于交换两个元素
void swap(void *a, void *b, size_t size) {
    char *temp = malloc(size);
    if (temp == NULL) {
        exit(EXIT_FAILURE);
    }
    memcpy(temp, a, size);
    memcpy(a, b, size);
    memcpy(b, temp, size);
    free(temp);
}

// 模拟 qsort 的冒泡排序实现
void qsort(void *base, size_t num, size_t size, int (*compare)(const void*, const void*)) {
    char *arr = base;
    for (size_t i = 0; i < num - 1; i++) {
        for (size_t j = 0; j < num - i - 1; j++) {
            void *a = arr + j * size;
            void *b = arr + (j + 1) * size;
            if (compare(a, b) > 0) {
                swap(a, b, size);
            }
        }
    }
}

// 示例比较函数,用于整数
int compareInts(const void *a, const void *b) {
    const int *ia = (const int *)a;
    const int *ib = (const int *)b;
    return *ia - *ib;
}

int main() {
    int arr[] = {45, 67, 12, 89, 34, 23, 0, 78};
    int n = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, n, sizeof(int), compareInts);

    printf("Sorted array: \n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}
  • swap 函数用于交换两个元素。它使用 malloc 来分配临时内存,然后通过 memcpy 在元素之间复制数据。

  • qsort 函数使用冒泡排序算法对数组进行排序。它遍历数组,比较相邻的元素,并在需要时交换它们。

  • compareInts 是一个比较函数,用于比较整数。它可以被替换为适用于其他类型的比较函数。

与标准 qsort 函数的快速排序算法相比,冒泡排序的性能通常较低,特别是对于大型数组。但在某些情况下,例如当数组已经几乎排序好时,冒泡排序的性能可以接受。

sizeof和strlen的对比

sizeofstrlen 是C语言中两个非常不同的函数,它们用于不同的目的:

  1. sizeof 运算符

    • sizeof 是C语言中的一个编译时运算符,它用于获取一个变量或类型在内存中占用的大小(以字节为单位)。
    • 它的结果在编译时就已经确定,不依赖于变量的实际内容。
    • 对于数组,sizeof 返回整个数组占用的内存大小。例如,sizeof(arr) 将返回数组 arr 的总大小(元素数乘以每个元素的大小)。
    • sizeof 可以用于任何类型的变量或数据类型,包括基本类型(如 intchar)、结构体、联合体等。
  2. strlen 函数

    • strlen 是一个运行时函数,用于计算一个以空字符('\0')结尾的字符串的长度(不包括空字符本身)。
    • 它的结果是在运行时计算的,基于实际的字符串内容。
    • 只能用于以空字符结尾的字符串,它通过遍历字符串直到找到空字符来计算长度。
    • 不能用于非字符串的数组。如果传递给 strlen 的是一个非以空字符结尾的字符数组,可能导致越界访问,从而引发未定义行为。

举例说明

假设有以下声明:

char arr[] = "hello";
  • 使用 sizeof(arr) 会得到 6,因为数组 arr 包含 "hello" 加上结束的空字符 '\0',总共 6 个字符。
  • 使用 strlen(arr) 会得到 5,因为 strlen 只计算 "hello" 的字符数,不包括结尾的空字符。

理解这两个函数的不同很重要,因为它们在不同的场景下有着不同的应用和结果。

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值