指针进阶

指针定义:

指针本质上是一个内存地址,程序可以通过这个地址直接访问内存中的数据,而不需要知道其具体的值或类型,指针大小是固定的也就是说无论是什么数据类型的指针在同一平台大小相同,64位平台占8个字节,32位占4个字节。

//指针的声明
int a = 1;
int *p = &a;//说明这是这块内存地址存储的是一个int类型数据

当然未必里面存什么类型的值就必须声明什么样的类型也可以这样

int a = 1;
char* p = (char * ) & a;//对这块内存地址一个字节访问

这样做的好出是可以进行更细致化的操作,这个之后会用到不在这里过多阐述。

1.字符指针

顾名思义字符类型的指针,解引用访问一个字节

char a = 'a';
char* p = &a;

如果字符指针指向一个字符串,存储的是字符串第一个字符的地址

char* p = "fsfferge";
printf("%c", *p);
//输出结果f

注意字符串是常量是不能被修改的也就是说不能通过解引用的方式修改字符串,同一字符串在内存中不会开辟第二块,也就是说同一字符串的地址相同

	char* p1 = "fsfferge";
	char* p2 = "fsfferge";
	if(p1 == p2) 
	{
		printf("相同");
	}
	//输出结果相同

2.指针数组

数组中的元素是指针的数组

以一个模拟二维数组演示

int arr1[] = { 1,2,5,6,2,5 };
int arr2[] = { 2,3,4,6,1,5 };
int arr3[] = { 5,2,5,8,2,7 };
int* pa[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++) 
{
		for (int j = 0; j < 6; j++) 
		{
			printf("%d " ,* (pa[i] + j));
		}
		printf("\n");
}
//输出结果
/*1 2 5 6 2 5
  2 3 4 6 1 5
  5 2 5 8 2 7*/

相当于给了一个基地址然后加上偏移量就能实现正确访问

3.数组指针

简单来说就是指向一个数组的指针

首先引入一个概念,数组名相当于数组第一个元素的地址,而取数组地址取出的是整个数组地址,两者值相同但类型不同

int arr[] = { 1,2,5,6,2,5 };

类型不同操作起来也不同,arr本质上是数组,数组名加一相当于指向的位置向后移一位                  *(arr+1)==arr[1],而&arr本质上是个指针也就是地址,它指向的是一个int类型元素个数为6的数组,指针的步长取决于他指向对象的大小所以加一跳过的是整个数组

指针进阶_第1张图片

定义数组指针:

int arr[] = { 1,2,5,6,2,5 };
int(*pa)[6] = &arr;
//加括号声明一个指针变量,不加pa与[]结合成一个数组这与结合性有关不过多阐述

分成三部分看就是,(*pa):声明指针变量,[6]:指向数组中的元素个数,int:数组中的数据类型

现在我们有一个二维数组,我们怎么对这个二维数组访问呢

最常见的方法就是通过下标进行访问

int arr2[][5] = { {2,3,1,4,5},{1,3,5,2,4},{5,2,1,4,3} };
for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ",arr2[i][j]);
		}
		printf("\n");
	}

那当我们想通过指针的方式访问呢,我们可以这样,也能够正确访问

int arr2[][5] = { {2,3,1,4,5},{1,3,5,2,4},{5,2,1,4,3} };
int(*pa)[5] = arr2;//arr数组名相当于首元素的地址,也就是第一行的地址
for (int i = 0; i < 3; i++) 
	{
		for (int j = 0; j < 5; j++) 
		{
			printf("%d ", *(*(pa + i) + j));
		}
		printf("\n");
	}

把这句代码拆分开来*(*(pa + i) + j)    pa是一个指针变量,它指向的是一个一维数组在前面提到过,指针的步长与所指对象类型有关,所以pa+i是一个一个一维数组的跳过对它解引用操作就得到行号,而这时得到就是*(pa+i)它在编译器中相当于一个一维数组,在之前提过数组名加一相当于指向的位置向后移一位 ,所以*(pa+i)+j就得到真值的地址,最后对他进行解引用操作就能够正确访问

其实无论是指针访问还是数组下标访问本质其实都是地址,只是给这片地址起的名字不一样,类型不同操作方式也不不一样,通过数组名还可以这样访问,只是把pa换成arr2,把arr2看成一个一维数组只不过里面的元素也是个数组就很好理解了,数组名+相当于i跳过i个元素,里面每个元素又是一维数组+j又跳过当前一维数组中j个元素从而实现访问,这两种写法虽然类似但意义不同

//arr[i][j]<==>*(*(arr+i)+j)<===>*(*(pa+i)+j)
for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", *(*(arr2 + i) + j));
		}
		printf("\n");
	}

为什么我们要用数组指针明明下表也可以很好访问,这样做有什么好处

  1. 动态内存分配: 使用数组指针可以方便地实现动态内存分配。通过动态分配内存,可以在程序运行时根据需要动态创建数组,从而灵活地管理内存资源。

  2. 多维数组: 在多维数组中,数组指针可以更方便地进行索引和遍历。多维数组本质上是一系列的一维数组,使用数组指针可以简化对多维数组的操作。

  3. 函数参数传递: 在函数参数传递过程中,使用数组指针可以避免数组被复制,提高程序的效率。通过传递数组指针,函数可以直接访问数组的元素,而无需将整个数组复制到函数的栈帧中。

  4. 字符串处理: 在C语言中,字符串通常被表示为字符数组,使用字符数组指针可以方便地对字符串进行操作和处理。例如,通过指针可以遍历字符串、搜索特定字符、比较字符串等操作。

  5. 动态数据结构: 在动态数据结构(如链表、树等)的实现中,数组指针可以用来表示和操作节点集合。通过指针,可以轻松地遍历和操作动态数据结构中的元素。

4.数组或指针传参

传递参数主要看发送方和接受方,要求类型一致,而传参方式又分为值传递和址传递

值传递

  • 在值传递中,函数参数接收的是实际参数的值副本,而不是实际参数本身。
  • 当函数内部对参数进行修改时,不会影响到原始的实际参数。
  • 值传递通常用于基本数据类型,如整数、浮点数等。

址传递

  • 在址传递中,函数参数接收的是实际参数的内存地址,即引用,而不是实际参数的值副本。
  • 当函数内部对参数进行修改时,会直接影响到原始的实际参数。
  • 像是数组、对象等复杂数据类型通常使用址传递。

arr数组名表示首元素的地址所以形参接收使用指针接收, int arr[] 或者 int *arr 都是合法的,它们都表示一个指向整数的指针,两者都是址传递。在 C 语言中,数组在作为函数参数传递时,并没有真正的值传递。数组在函数参数传递中通常被解释为指向数组第一个元素的指针,因此传递的是数组的地址,而不是数组的副本。

#include
void test1(int arr[5])
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", arr[i]);
	}
}
void test2(int *arr) 
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(arr+i));
	}
}
int main() 
{
	int arr[] = { 1,2,4,5,6 };
	test1(arr);
	printf("\n");
	test2(arr);
	return 0;
}

二维数组的数组名传递过去的是首元素的地址是一个一维数组的地址,所以应该用一维数组指针接受或二维数组接收,它们都是指向一维数组,test4应该接收的是一个存放int类型地址的地址

例如:int *arr[ ],int**arr

指针进阶_第2张图片


void test3(int(*arr)[5]) {}
void test4(int** arr) {}
void test5(int arr[][5]){}
int main() 
{
	int arr[][5] = { { 1,2,4,5,6}, {1,2,3,4,5} ,{5,3,1,2,4} };
	test3(arr);//可以	
	test4(arr);//不可以
    test4(arr);//可以
	return 0;
}

5.函数指针

函数指针是指向函数的指针变量

函数名==&函数地址

printf("%p\n", test6);
printf("%p", &test6);
//输出结果
//008413CA
//008413CA

声明一个函数指针变量:

  • return_type 是函数的返回类型。
  • pointer_name 是函数指针的名称。
  • parameter_type1, parameter_type2, ... 是函数的参数类型。
return_type (*pointer_name)(parameter_type1, parameter_type2, ...);

因为函数名==&函数地址所以它们两是等价的。调用也是等价的

结论:在使用函数指针时有无“&”和“*”都一样,简单来说无论时函数名还是函数指针都是给这片空间起名字,它们都指向这片空间,所以有无“&”和“*”都一样。

void test6(int a)
{
	printf("%d",a);
}
int main() 
{
void (*pt)(int);
pt= &test6;
void (*ps)(int);
ps = test6;

(*pt)(1);
pt(1);
(*ps)(1);
ps(1);
//结果都是1
}

接下来看第二段代码,这是何物

(*(void(*)())0)();
void(*signal(int,void (*)()))();

答案是一个时函数调用,一个是函数声明。

首先第一段代码从里往外看

1.(void(*)()是一个返回类型为void的无参函数指针类型

2.(void(*)())0,既然(void(*)()是种类型那它被()括起来发生强制类型转换把整数0转化成返回类型为void的无参函数指针类型

3.(*(void(*)())0)()既然它是一个指针类型对他解引用以下就找到这个函数,然后加上括号调用这个函数

依然从里往外看

1.signal(int,void (*)()):signal与()结合说明他是函数,函数内有两个参数分别是int类型和

void(*)()类型

2.void(*signal(int,void (*)()))():*与signal(int,void (*)())结合说明它是个指针函数,指针的类型是一个void(*)()函数指针

总的来说这段代码的意思是一个返回类型为函数指针的函数,他有两个参数int和void(*)()

代码嵌套在一起可能不便于观察,如果我们把它拆开来他就是这样,只不过这样的声明是不被编译器允许的,它与基本类型声明不同,是与“*”放在中间写法类似于数组指针,都是一种类型

void(*)()   signal(int,void(*)());
int         a;

6.函数指针数组

函数指针数组是一个数组,其中的每个元素都是一个函数指针。函数指针指向程序中的函数,允许通过调用函数指针来调用相应的函数。

在C语言中,函数指针数组的声明通常如下所示:

return_type (*function_array[n])(parameters);

其中:

  • return_type 是函数的返回类型。
  • function_array 是函数指针数组的名称。
  • [n] 表示数组的大小。
  • parameters 是函数的参数列表。

以下是一个简单的示例,实现了计算机加减乘除功能:

#include 

// 函数指针类型定义
typedef int (*ArithmeticOperation)(int, int);

// 函数声明
int add(int, int);
int subtract(int, int);
int multiply(int, int);
int divide(int, int);

int main() {
    char operation;
    int num1, num2, result;

    // 函数指针数组
    ArithmeticOperation operations[4] = {add, subtract, multiply, divide};

    // 操作字符到索引的映射
    char operations_map[] = {'+', '-', '*', '/'};

    printf("请输入操作符(+,-,*,/): ");
    scanf(" %c", &operation);

    // 寻找操作符对应的索引
    int index = -1;
    for (int i = 0; i < 4; i++) {
        if (operation == operations_map[i]) {
            index = i;
            break;
        }
    }

    // 检查是否找到了匹配的操作符
    if (index == -1) {
        printf("无效的操作符\n");
        return 1;
    }

    printf("请输入两个数字: ");
    scanf("%d %d", &num1, &num2);

    // 使用索引直接调用对应的函数指针
    result = operations[index](num1, num2);
    printf("结果: %d\n", result);

    return 0;
}

// 加法函数
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("错误:除数不能为零\n");
        return 0;
    }
}

使用函数指针数组的好处:

  1. 灵活性: 函数指针数组允许在运行时动态地选择要调用的函数。这使得在不同情况下可以调用不同的函数,从而增加了程序的灵活性和可扩展性。

  2. 代码模块化: 使用函数指针数组可以将相关的功能组织成模块,并将这些模块作为函数指针数组的元素。这样做可以使代码更易于维护和理解,因为相关功能被分组在一起。

  3. 简化代码重复: 如果有多个地方需要调用一组相关的函数,使用函数指针数组可以避免代码重复。只需在函数指针数组中定义一次函数指针,然后在需要的地方使用它们。

  4. 多态性(Polymorphism): 使用函数指针数组可以实现多态性,即在运行时选择不同的函数来执行相同的操作。这对于实现类似于面向对象编程中的多态性非常有用。

  5. 可重用性: 函数指针数组使得函数可以在不同的上下文中重复使用。同一个函数可以出现在不同的函数指针数组中,以满足不同的需求。

总的来说,使用函数指针数组可以提高代码的灵活性、可维护性和可重用性,使程序更具有组织性和扩展性。

7.指向函数指针数组的指针

#include 

// 函数原型
void function1(int){};
void function2(int){};
void function3(int){};

int main() {
    // 定义函数指针数组
    void (*function_array[3])(int) = {function1, function2, function3};

    // 定义指向函数指针数组的指针
    void (*(*ptr_to_function_array))[3] = &function_array;

    // 使用指向函数指针数组的指针来调用函数
    int i;
    for (i = 0; i < 3; i++) {
        ((*ptr_to_function_array)[i])(i + 1); // 调用函数,并传递参数 i + 1
    }

    return 0;
}

8.回调函数

回调函数是指在函数执行过程中通过函数指针调用的函数,通常是作为参数传递给另一个函数。在这种情况下,被调用的函数可以是在调用函数之外定义的,甚至可以是在另一个源文件中定义的,它可以被用来扩展或定制调用函数的行为。

以通用冒泡函数作为示例:

#include 

// 交换函数,以一个字节为单位进行交换,交换趟数与数据类型相关
void swap(char* xp, char* yp,int l) {
    char temp ;
    for (int i = 0; i < l; i++) 
    {
        temp=*xp;
        *xp = *yp;
        *yp=temp;
        yp++;
        xp++;
    }
}
//比较函数
int cmp_int(void* e1, void* e2) {//void类型指针可以接受任意类型
    return (*(int*)e1) - (*(int*)e2);
}

// 冒泡排序函数
void bubbleSort(void *base, int sz,int l, int (*cmp_int)(const void* e1, const void* e2)) {//base:起始位置,sz:数组大小,l:元素长度,cmp_int:比较函数的指针
    
    int i, j;
    for (i = 0; i < sz - 1; i++) {
        // 最后的 i 个元素已经就位,不需要再比较
        for (j = 0; j < sz - i - 1; j++) {
            if (cmp_int( (char*)base + j * l, (char*)base + (j + 1) * l)>0 )
            {
                swap((char*)base + j * l, (char*)base + (j + 1) * l,l);
            }
            
        }
    }
}

// 打印数组函数,用于输出数组内容
void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 主函数
int main() {
    int arr[] = { 64, 34, 25, 12, 22, 11, 90 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    printf("原始数组: \n");
    printArray(arr, sz);
    bubbleSort(arr, sz,4, cmp_int);
    printf("排序后的数组: \n");
    printArray(arr, sz);
    return 0;
}
  1. swap函数: 你的swap函数实现了按字节交换元素的功能,这对于通用的排序算法是很有用的。但是在实际的应用中,可能会使用更高效的交换方式,比如使用临时变量进行交换。

  2. cmp_int函数: 这是一个比较函数,用于比较两个int类型的元素。这个函数比较简单,它返回e1 - e2的结果。为正则e1大于e2为负则e1小于e2

  3. bubbleSort函数: 这是冒泡排序的实现,可以接受任意类型的数组和比较函数。这样的通用性很好,但是冒泡排序本身不是一个高效的排序算法,特别是对于大型数组来说。更常用的排序算法包括快速排序、归并排序和堆排序等。

  4. printArray函数: 这个函数用于打印整型数组的内容。同样的方法可以用于打印其他类型的数组,只需修改参数类型和格式说明符。

总结:

      常见的指针类型:

  1. 整型指针(int* 指向整型数据的指针,可以用于存储整型变量的地址。

  2. 字符型指针(char* 指向字符型数据的指针,可以用于存储字符变量的地址。

  3. 浮点型指针(float* 指向浮点型数据的指针,可以用于存储浮点变量的地址。

  4. 双精度浮点型指针(double* 指向双精度浮点型数据的指针,可以用于存储双精度浮点变量的地址。

  5. 结构体指针(struct* 指向结构体类型数据的指针,可以用于存储结构体变量的地址。

  6. 数组指针: 指向数组的指针,可以用于存储数组的首地址。

  7. 空指针(void* 指向未知类型数据的指针,可以用于通用的指针操作,但无法直接访问其指向的内容,需要进行类型转换后才能使用。

  8. 函数指针: 指向函数的指针,可以用于存储函数的地址,以便在程序中调用该函数。

可以根据需要灵活使用指针

你可能感兴趣的:(算法)