【翰海拾贝】指针与结构体

【翰海拾贝】指针与结构体

笔者:吃汉堡吃到饱

请注意,本篇博客用意不在详尽记录指针与结构体相关知识,而是记录学习过程中,以往遗漏亦或是未曾深入理解的知识点,故而知识点之间可能并无逻辑联系,难易度也并不存在先后关联。

1.柔性数组

1.柔性数组简介:

柔性数组是C语言中一种特殊的结构体成员,具体体现为在结构体的末尾定义一个数组,该数组的大小可以根据实际需要动态变化

柔性数组的声明方式是在结构体的末尾定义一个未命名的数组,且其大小为0。这个数组可以用来存储可变数量的数据,而结构体的大小并不包括这个柔性数组的大小。

2.具体代码实现以及相关操作:

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

// 定义包含柔性数组的结构体
struct DynamicArray {
    int size;
    int data[];  // 柔性数组,大小为0
}DA;
// 创建一个包含柔性数组的结构体
DA* createDynamicArray(int size) {
    // 使用malloc动态分配内存
    DA* array = malloc(sizeof(DA) + size * sizeof(int));
    
    if (array != NULL) {
        array->size = size;
    }

    return array;
}
int main() {
    int arraySize = 5;
    DA* myArray = createDynamicArray(arraySize);
    if (myArray != NULL) {
        printf("Array size: %zu\n", myArray->size);
        for (size_t i = 0; i < arraySize; ++i) {
            myArray->data[i] = i;
            printf("Element %d: %d\n", i, myArray->data[i]);
        }
        // 释放内存
        free(myArray);
    }

    return 0;
}
3.为什么选择柔性数组

在结构体末尾部分加入一个指针元素,而后再对该地址malloc,如下代码所示,显然可以实现类似于柔性数组的功能,那么为什么要使用柔性数组呢?

typedef struct S
{
	int a;
	int* arr;
} S;

int main()
{
	int i = 0;
	S* p = (S*)malloc(sizeof(S));
	//申请5个整形元素大小空间,将空间地址赋值给结构体对象的arr成员,让arr成员维护这空间的数据
	(int*)p->arr = (int*)malloc(5 * sizeof(int));
    return 0;
}
  1. 方便内存释放。

    显然的,在没有使用柔性数组时,实现相似功能需要进行两次内存分配和释放,而使用柔性数组则只需一次释放,有效减少错误产生的可能。

  2. 有利于减少内存碎片。

  3. 连续内存有利于提高访问速度。

    该结论由局部性原理可推理得,第一种方式将内存为一整块儿,而第二种方式分配在不同的地方。

局部性原理: 计算机系统中存在着局部性原理,包括时间局部性和空间局部性。时间局部性指的是程序中某个数据项被访问后,在不久的将来会再次被访问;空间局部性指的是在访问某个数据项时,附近的数据项也很可能被访问。连续内存存储方式更符合这两种局部性,因为相邻的数据项通常在空间上是相邻的,访问一个数据项后访问其相邻项的概率较大。

使用柔性数组的几项注意:

  • 结构体中的柔性数组成员前面必须至少一个其他成员
  • 结构体中的柔性数组成员必须是结构体成员中的最后一个
  • sizeof 返回的这种结构体大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构体用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

参考文献:

【C语言】柔性数组_c语言柔性数组_bell-house的博客-CSDN博客

2.深浅拷贝

浅拷贝:表层的引用,实际指向同一块内存。

深拷贝:存放在不同的内存空间当中。

在结构体中存在指针变量时,深浅拷贝的不同可能会导致不一样的结果。

//例如:
//我定义如下的结构体变量:
typedef struct HamBurGer {
    int number;
    char size*;
}HB;

//此时我创建两个结构体,并对第一个结构体赋值
HB firstHB = {2,"BIG"};
HB secondHB;

此时如若我想将第一个汉堡的信息传递给第二个汉堡,则可以使用两种拷贝方式:

1.浅拷贝

仅仅将地址赋值给结构体second

可以直接写secondHB = firstHB;

浅拷贝时,拷贝后两个结构体变量中指针成员变量指向同一个地址。

写一段代码观察其值以及地址:

secondHB = firstHB;
printf("firstHB addr= %p,firstHB.size=%s\n", &firstHB, firstHB.size);
printf("secondHB addr= %p,secondHB.size=%s\n", &secondHB, secondHB.size);

可以看出两个结构体地址相同,即两个指针指向了同一个地址,如若在最后释放内存时将两个结构体都释放,则会free两次该结构体,编译器报错。

浅拷贝与其说是拷贝,不如说是共享,两个变量通过指针指向同一个地址,从而共享部分数据。

2.深拷贝

深拷贝时,拷贝后两个结构体变量中指针成员变量指向不同地址。

深拷贝指的是创建一个新对象,并递归地将原始对象的数据复制到新对象中,因此新对象与原始对象之间不存在数据共享。

参考文献:

深拷贝和浅拷贝(copy和deepcopy)详解_深浅拷贝_韦伯爬虫AI的博客-CSDN博客

C语言:浅拷贝与深拷贝_c语言深拷贝和浅拷贝-CSDN博客

3.通用指针(泛指针)

1.什么是void指针

void指针一般被称为通用指针或叫泛指针。它是C语言关于纯粹地址的一种约定。当某个指针是void型指针时,所指向的对象不属于任何类型。

注意:

  1. void* 本身不具备解引用的能力,需要将其转换具体类型的指针才能够访问其指向的数据。
  2. void指针不属于任何类型,则不可以对其进行算术运算,比如自增,编译器不知道其自增需要偏移多少内存。如char *型指针,自增是指针指向的地址偏移1,short *型指针自增,则偏移2。

在C/C++中,在任意时刻都可以使用其它类型指针来代替void指针,或者用void指针来代替其他类型指针。

对指针变量的解引用,使用间接运算符*达到目的。但是在使用空指针的情况下,需要转换指针变量以解引用。要获取由void指针指向的数据,需要使用在void指针位置内保存的正确类型的数据进行类型转换。

2.void指针具体应用举例

以下代码用冒泡排序模拟了C语言的qsort函数,Qsort函数可以实现任意类型的数组进行排序,void指针必不可少。

//此处仅放上自主实现时的部分代码(此处qsort使用冒泡排序进行数组有序化)
int int_cmp(const void* p1, const void* p2)
{
    return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
    int i = 0;
    for (i = 0; i < size; i++)
    {
        char tmp = *((char*)p1 + i);
        *((char*)p1 + i) = *((char*)p2 + i);
        *((char*)p2 + i) = tmp;
    }
}

由代码可知:

对_swap函数

  • void* 类型的指针作为参数,以及一个整数 size,用于指定要交换的内存块的大小
  • 在内部使用 char* 类型的指针进行字节级别的交换。一个字节一个字节交换

对int_cmp函数

  • cmp函数内部将传入的指针转换为 int* 类型,然后比较两个整数的大小,返回它们的差值。

具体qsort函数解析可参考:

2023移动应用开发实验室 一面题解-CSDN博客 T9 (简单)排序

参考文献:

【精选】void 型指针的高阶用法,你掌握了吗?_strongerHuang的博客-CSDN博客

4.回调函数

在说回调函数前,我想先引入、或者说复习两个概念:函数指针,指针数组

1.函数指针

众所周知,C语言有指针变量这一特殊的类型,它是某一变量在内存中存储的地址。如char*指向char变量, 而int *指向int变量,函数也有函数指针,可以指向一个函数。

在C语言中,函数被存储在内存中的代码段,而函数指针则是指向这个代码段的指针。通过函数指针,可以在运行时动态地选择调用哪个函数,从而实现灵活的函数调用机制。

一般的,函数指针可以定义为:

函数返回值类型 (* 指针变量名) (函数参数);

1.如何用函数指针调用函数

举一个香甜可口的栗子(可能不甜,但至少能吃)

#include <stdio.h>

// 定义两个函数,实现两个int类型的加减运算
int Add(int a, int b) {
    return a + b;
}

int Subtract(int a, int b) {
    return a - b;
}

int main() {
    // 声明一个函数指针,该指针可以指向接受两个 int 参数并返回 int 的函数
    int (*operation)(int, int);
    // 将函数地址赋给函数指针
    operation = Add;
    // 使用函数指针调用函数
    int result = operation(5, 3);
    printf("Result of Add function: %d\n", result);
    // 将函数指针切换到另一个函数
    operation = Subtract;
    // 再次使用函数指针调用函数
    result = operation(5, 3);
    printf("Result of Subtract function: %d\n", result);
    return 0;
}

需要注意的是,代码块开头定义的两个加减函数,都接受两个整数参数并返回一个整数,而函数指针的参数区域必须与之相同。

在main函数中,我们首先将 operation 指向 add 函数,然后使用函数指针调用 add 函数。接着,我们将 operation 切换到 subtract 函数,并使用函数指针调用 subtract 函数。

总而言之,函数指针提供了一种在运行时动态选择调用哪个函数的机制,这为函数指针表、回调函数的实现提供了便利,具体详情,请观下文。

2.函数指针作为某个函数的参数

函数指针作为函数参数是常见的用法之一,它允许你将不同的函数传递给同一个函数。再举一个香甜可口的栗子(这次也不一定好吃,姑且吃了再说)。

#include <stdio.h>
//熟悉的两个函数
int Add(int a, int b) {
    return a + b;
}

int Subtract(int a, int b) {
    return a - b;
}

// 定义一个接受两个整数和一个函数指针的函数
int Operation(int x, int y, int (*operation)(int, int)) {
    return operation(x, y);
}

int main() {
    int result;

    // 使用 add 函数进行加法操作
    result = Operation(5, 3, Add);
    printf("Result of add function: %d\n", result);

    // 使用 subtract 函数进行减法操作
    result = Operation(5, 3, Subtract);
    printf("Result of subtract function: %d\n", result);

    return 0;
}

Operation 函数接受两个整数和一个函数指针作为参数。

在主函数中,调用 Operation 函数分别使用了 Add 和 Subtract 函数进行加法和减法操作,然后打印结果。

3.函数指针作为函数返回类型

函数指针作为函数的返回类型,可以让函数返回另一个函数的地址。又是一个香甜(划掉),又长又臭的代码(悲)。

#include <stdio.h>

//熟悉的两个函数(怎么又是你)
int Add(int a, int b) {
    return a + b;
}

int Subtract(int a, int b) {
    return a - b;
}

// 定义一个函数,根据参数返回不同的函数指针
int (*SelectOperation(char Operator))(int, int) {
    switch (Operator) {
        case '+':
            return Add;
        case '-':
            return Subtract;
        default:
            // 返回一个默认的函数指针,也可以是空指针或其他处理逻辑
            return NULL;
    }
}
//定义了一个函数 SelectOperation,它接受一个运算符作为参数,并根据运算符返回相应的函数指针。在这里,根据运算符 '+' 返回 Add 函数指针,根据运算符 '-' 返回 Subtract 函数指针。
int main() {
    int result;
    // 通过函数返回的函数指针调用 add 函数
    result = SelectOperation('+')(5, 3);
    printf("Result of add function: %d\n", result);
    // 通过函数返回的函数指针调用 subtract 函数
    result = SelectOperation('-')(5, 3);
    printf("Result of subtract function: %d\n", result);
    return 0;
}

2.指针数组(本篇特别介绍函数指针数组)

指针数组是一个数组,其元素都是指针。这些指针可以指向不同类型的数据,也可以是指向相同类型的不同数据的指针。

函数指针数组是一个数组,其元素都是函数指针。每个函数指针指向一个特定的函数。这种结构允许在数组中存储不同函数的地址,可以根据需要动态选择调用不同的函数。

//熟悉的两个函数(没完了是吧)
int Add(int a, int b) {
    return a + b;
}
int Subtract(int a, int b) {
    return a - b;
}
// 定义一个包含两个函数指针的数组
int (*operation[2])(int, int) = {add, subtract};

int result1 = operation[0](5, 3); // 调用 Add 函数
int result2 = operation[1](5, 3); // 调用 Subtract 函数
//可以通过数组索引访问函数指针,并通过函数指针调用相应的函数。

3.回调函数

1.回调函数简介

回调函数是一种在编程中常见的概念,它允许将一个函数作为参数传递给另一个函数,以在某些条件满足或特定事件发生时执行。

  1. 函数指针: 回调函数的实现基于函数指针。函数指针是指向函数的指针变量,可以将函数的地址存储在指针变量中。
  2. 传递函数作为参数: 在使用回调函数时,通常将一个函数的地址传递给另一个函数,允许后者在适当的时候调用前者。
2.为什么选择回调函数
  1. 模块化和重用性: 使用回调函数可以将代码模块化,使其更易于理解和维护。同一个回调函数可以在多个地方重复使用。
  2. 灵活性: 允许在运行时动态指定要执行的代码,增加了程序的灵活性。
  3. 可扩展性: 允许轻松地更改或扩展程序的行为,而无需修改现有代码。
3.回调函数示例

这里写了一个小计算器来展示回调函数的用途!(终于不是只有加减了!)

#include <stdio.h>
// 函数指针类型定义
typedef int (*Operation)(int, int);

// 加法函数
int Add(int a, int b) {
    return a + b;
}

// 减法函数
int Subtract(int a, int b) {
    return a - b;
}

// 乘法函数
int Multiply(int a, int b) {
    return a * b;
}

// 除法函数
int Divide(int a, int b) {
    if (b != 0) {
        return a / b;
    } else {
        printf("Error: Division by zero!\n");
        return 0;
    }
}

// 执行计算的函数,接受两个整数和一个操作函数指针作为参数
int Calculate(int a, int b, Operation op) {
    return op(a, b);
}

int main() {
    int num1, num2;
    
    printf("Enter first number: ");
    scanf("%d", &num1);

    printf("Enter second number: ");
    scanf("%d", &num2);

    // 定义一个操作数组,包含加法、减法、乘法和除法函数指针
    Operation operations[] = {Add, Subtract, Multiply, Divide};

    printf("Select Operation:\n");
    printf("1. Add\n2. Subtract\n3. Multiply\n4. Divide\n");
    
    int choice;
    printf("Enter choice (1-4): ");
    scanf("%d", &choice);

    // 检查用户输入是否有效
    if (choice >= 1 && choice <= 4) {
        // 根据用户选择调用相应的计算函数
        int result = Calculate(num1, num2, operations[choice - 1]);
        printf("Result: %d\n", result);
    } else {
        printf("Invalid choice\n");
    }
    return 0;
}

第一行代码,就给人干蒙了,typedef还能这么用的吗?

于是回炉重造半个小时,这里浅浅总结一下typedef的几个用法。

关于typedef

1.为特定含义的类型取别名

typedef int HanBaoNum;

2.为结构体取别名

typedef struct Hamburger
{
    char size[128];
    int num;
}HB;

3.声明函数指针类型

typedef int (*Operation)(int, int);

由上可见,typedef中声明的类型在变量名的位置出现,在声明函数指针类型时,则在函数名的位置放上别名。

决定就是你了!Operation函数!(bushi)

回到正题,首先我们定义了一个函数指针类型Operation,该类型可以指向接受两个整数参数并返回整数的函数。而后定义了四个函数,分别实现加法、减法、乘法和除法的功能。

一刻也没有为加减函数的加班而感到伤心,随即赶到战场的是乘除函数和回调函数!好耶!(划掉)

接着定义了一个函数 Calculate,接受两个整数和一个函数指针参数,用于执行相应的计算操作。

主函数首先输入两个整数,然后根据用户选择的操作,调用相应的计算函数进行计算并输出结果。

参考文献:

【精选】C语言回调函数详解(全网最全)-CSDN博客

本来应该上周就该学完的,拖到了现在,在最后发一段劝学激励一下自己吧。

南方有鸟焉,名曰蒙鸠,以羽为巢,而编之以发,系之苇苕,风至苕折,卵破子死。巢非不完也,所系者然也。西方有木焉,名曰射干,茎长四寸,生于高山之上,而临百仞之渊,木茎非能长也,所立者然也。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值