【翰海拾贝】指针与结构体
笔者:吃汉堡吃到饱
请注意,本篇博客用意不在详尽记录指针与结构体相关知识,而是记录学习过程中,以往遗漏亦或是未曾深入理解的知识点,故而知识点之间可能并无逻辑联系,难易度也并不存在先后关联。
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;
}
方便内存释放。
显然的,在没有使用柔性数组时,实现相似功能需要进行两次内存分配和释放,而使用柔性数组则只需一次释放,有效减少错误产生的可能。
有利于减少内存碎片。
连续内存有利于提高访问速度。
该结论由局部性原理可推理得,第一种方式将内存为一整块儿,而第二种方式分配在不同的地方。
局部性原理: 计算机系统中存在着局部性原理,包括时间局部性和空间局部性。时间局部性指的是程序中某个数据项被访问后,在不久的将来会再次被访问;空间局部性指的是在访问某个数据项时,附近的数据项也很可能被访问。连续内存存储方式更符合这两种局部性,因为相邻的数据项通常在空间上是相邻的,访问一个数据项后访问其相邻项的概率较大。
使用柔性数组的几项注意:
- 结构体中的柔性数组成员前面必须至少一个其他成员。
- 结构体中的柔性数组成员必须是结构体成员中的最后一个。
- sizeof 返回的这种结构体大小不包括柔性数组的内存。
- 包含柔性数组成员的结构体用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
参考文献:
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.深拷贝
深拷贝时,拷贝后两个结构体变量中指针成员变量指向不同地址。
深拷贝指的是创建一个新对象,并递归地将原始对象的数据复制到新对象中,因此新对象与原始对象之间不存在数据共享。
参考文献:
3.通用指针(泛指针)
1.什么是void指针
void指针一般被称为通用指针或叫泛指针。它是C语言关于纯粹地址的一种约定。当某个指针是void型指针时,所指向的对象不属于任何类型。
注意:
- void* 本身不具备解引用的能力,需要将其转换为具体类型的指针才能够访问其指向的数据。
- 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 (简单)排序
参考文献:
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.回调函数简介
回调函数是一种在编程中常见的概念,它允许将一个函数作为参数传递给另一个函数,以在某些条件满足或特定事件发生时执行。
- 函数指针: 回调函数的实现基于函数指针。函数指针是指向函数的指针变量,可以将函数的地址存储在指针变量中。
- 传递函数作为参数: 在使用回调函数时,通常将一个函数的地址传递给另一个函数,允许后者在适当的时候调用前者。
2.为什么选择回调函数
- 模块化和重用性: 使用回调函数可以将代码模块化,使其更易于理解和维护。同一个回调函数可以在多个地方重复使用。
- 灵活性: 允许在运行时动态指定要执行的代码,增加了程序的灵活性。
- 可扩展性: 允许轻松地更改或扩展程序的行为,而无需修改现有代码。
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,接受两个整数和一个函数指针参数,用于执行相应的计算操作。
主函数首先输入两个整数,然后根据用户选择的操作,调用相应的计算函数进行计算并输出结果。
参考文献:
本来应该上周就该学完的,拖到了现在,在最后发一段劝学激励一下自己吧。
南方有鸟焉,名曰蒙鸠,以羽为巢,而编之以发,系之苇苕,风至苕折,卵破子死。巢非不完也,所系者然也。西方有木焉,名曰射干,茎长四寸,生于高山之上,而临百仞之渊,木茎非能长也,所立者然也。