【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)

【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)

  • 1. 字符指针
  • 2. 指针数组
    • 2.1 整形指针数组
    • 2.2 用指针数组模拟二维数组
  • 3. 数组指针
    • 3.1 数组指针的表示方法
    • 3.2 深度剖析&数组名和数组名
    • 3.3 数组指针的使用
      • 3.3.1 在同一函数内直接将数组的地址赋给数组指针
      • 3.3.2 数组指针在二维数组传参上的应用
        • 3.3.2.1 二维数组传参使用二维数组
        • 3.3.2.2 二维数组传参使用数组指针
  • 4. 数组传参和指针传参
    • 4.1 一维数组传参
    • 4.2 二维数组传参
    • 4.3 一级指针传参
    • 4.4 二级指针传参
  • 5. 函数指针
    • 5.1 函数的地址
    • 5.2 函数指针的表示方法
    • 5.3 两行有趣的代码
      • 5.3.1 将0强制转换为函数指针类型然后调用
      • 5.3.2 数组指针、函数指针等特殊变量类型typedef的使用
      • 5.3.3 函数指针类型的函数的声明,并使用别名typedef化简

【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部))

❤️博客主页: 小镇敲码人
欢迎关注:点赞 留言 收藏
回来4天了,加油!!!
当你的能力匹配不上你的梦想,当你需要实现的目标匹配不上你的圈子的时候,你就会出现错位。当我们的能力还匹配不上我们的梦想时,我们就需要沉淀学习。✡️✡️✡️

【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)_第1张图片

1. 字符指针

字符指针,就是储存字符变量地址的指针,这是一种指针的类型。

  • 通常我们字符指针有如下两种用途
  1. 通过字符指针间接改变字符的值
    int main()
    {
      char ch = 'c';
     // ch = 'a';//直接通过变量赋值更改ch的值
      char *p = &ch;//将字符ch的地址存入p中
      //*p = 'a';//通过对p里面的地址解引用找到ch,间接的改变ch的值
    }
  1. 借助字符指针对字符串进行储存和打印
 int main()
 {
   const char* ptr = "abcdef";
   printf("%s\n",ptr);
   return 0;
 }
  • const修饰字符串表示"abcdef"不可修改,为常量字符串
  • 字符指针变量ptr只储存了字符'a'的地址。 (可类比字符数组理解,实际上字符串是字符数组的一种形式,在C语言中,字符串是由字符数组表示的,以空字符(‘\0’)作为字符串的结束标志,又因为数组名表示首元素地址,所以字符指针也只储存了首元素的地址)

2. 指针数组

      指针数组是由指针组成的数组。在C和C++等编程语言中,指针是一个变量,用于存储内存地址。指针数组是一个数组,其每个元素都是指针类型。
    int *arr1[10];    整形指针数组
    char *arr2[10];  一级字符指针的数组
    char **arr3[10]; 二级字符指针的数组

2.1 整形指针数组

int main()
{
    int a = 0;
        int b = 1;
    int c = 2;
    int* arr[] = { &a,&b,&c };
    for (int i = 0; i < 3; i++)
    {
        printf("%d ", *arr[i]);
    }
}

2.2 用指针数组模拟二维数组

int main()
{
	int arr1[] = { 1,2,3,4,5 };//arr1-int*
	int arr2[] = { 2,3,4,5,6 };//arr2-int*
	int arr3[] = { 3,4,5,6,7 };//arr3-int*
   //指针数组
	int* p[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
		  //这两种方式打印都可以,两者择一
			printf("%d ", *(*(p + i) + j));
            printf("%d ",p[i][j]);
		}
		printf("\n");
	}
}

3. 数组指针

我们可以通过类比来理解

整型指针:int * a;能够指向整形数据的指针
浮点型指针:float* b;能够指向浮点型数据的指针。
那么数组指针应该是:能够指向数组的指针。

3.1 数组指针的表示方法

数组的类型表示是什么?通过简单的类比可以知道。

  1. int a = 3;类型为除变量名以外的部分int就是整形的类型表示形式。
  2. char b = 'a';类型为除变量名以外的部分char就是字符类型的的表示形式。
  3. int arr[10] = {0};类型为除变量名以外的部分int [10]就是这个数组的变量类型表示形式。
  4. 而数组指针的类型是指针,指针的类型是数组,通过前面的学习我们知道,变量类型+变量名为一个变量,所以一个数组指针变量就是,指向数组的指针变量,int [10] *p = &arr;,看起来似乎是这样,但是通过下面符号的优先级我们可以知道[]的优先级要比*号高,所以我们要给*p加上括号,让其成为指针,int [10] (*p) = &arr,那这样是否正确呢?还是不对,放进vs编译器会报错,数组的类型比较特殊,应该是这样int (*p) [10] = &arr;才正确。

3.2 深度剖析&数组名和数组名

对于下面的数组

 int arr[10];
  • arr&arr分别是什么呢?
    对于arr,我们知道它是一个数组名,数组名表示首元素的地址,
    那&arr是什么呢,我们来看下面一段代码:
int main()
{
	int arr[10] = { 0 };
	printf("arr       = %p\n", arr);
	printf("arr+1     = %p\n", arr+1);

	printf("&arr[0]   = %p\n", &arr[0]);
	printf("&arr[0]+1 = %p\n", &arr[0]+1);
	
	printf("&arr      = %p\n", &arr);
	printf("&arr+1    = %p\n", &arr+1);
	return 0;
}

运行结果如下:
【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)_第2张图片
可以看到数组名和&数组名打印的地址和首元素打印的地址是一样的,这是不是意味着它们是一样的呢?很明显通过代码我们也可以发现,它们并不是相同的,因为&arr+1打印的地址与arr+1打印的地址相差了40个字节,而arr+1打印的地址和首元素&arr+1的仍然一样,进一步说明,arr就表示首元素地址。

  • 实际上:&arr表示的是整个数组的地址,它的类型是int (*)[10],是一种数组指针类型。
                   而arr代表的是数组首元素的地址,它的类型是int *
                   数组的地址加1,跳过整个数组的大小,所以&arr+1与&arr的差值就是 10 ∗ 4 = 40 10*4 =40 104=40个字节。

3.3 数组指针的使用

3.3.1 在同一函数内直接将数组的地址赋给数组指针

#include
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//int[10] *p = &arr;err
	int (*p)[10] = &arr;
	for (int i = 0; i < 10; i++)
	{
	    //两种打印方式择一
	    printf("%d ",*((*p)+i));
		printf("%d ", (*p)[i]);
	}
	//int* p2 = &arr;//err
	return 0;
}

  • 可以看到,这样做简直就是多此一举,因为想打印一维数组,我们直接打印就行了,使用数组指针,反倒是比较麻烦。

3.3.2 数组指针在二维数组传参上的应用

3.3.2.1 二维数组传参使用二维数组

#include
void Print(int arr[][5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
   }
}
int main()
{
	int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
	Print(arr, 3, 5);
	return 0;
}

3.3.2.2 二维数组传参使用数组指针

#include
Print(int(*p)[5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
		  //两种打印方式择一
			printf("%d ", *(*(p + i) + j));
			printf("%d ", p[i][j]);
		}
		printf("%\n");
	}
}
int main()
{
	int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };
	//arr表示数组名,数组名表示首元素的地址,也就是那一行的地址
	Print(arr, 3, 5);
	return 0;
}
  • 因为二维数组的数组名代表首元素的地址,首元素地址就是那一行的地址,就等价于一维数组的地址(&一维数组数组名),故又称二维数组为一维数组的数组,所以可以用数组指针来接收。

4. 数组传参和指针传参

4.1 一维数组传参

  • 有以下主要两种方式
  1. 传递数组名,用一级指针的方式去接收,通过地址访问
  2. 传递数组的副本
#include
void test1(int arr[])
{}
void test1(int arr[10])
{}
void test1(int* arr)
{}
void test2(int* arr[20])
{}
void test2(int** arr)
{}
int main()
{
	int arr1[10] = { 0 };
	int* arr2[20] = { 20 };
	test1(arr1);
	test2(arr2);
	return 0;
}

4.2 二维数组传参

  • 正确的方法已经在“3.3.2 数组指针在二维数组传参上的应用“版块给出,下面就几种错误的传参方法做一下阐述。
#include
void test(int arr[3][5]){}
void test(int arr[][])//err,二维数组传参,如果是传递数组的副本,需要给出一行有多少数字,因为二维数组其实是一维数组的扩展,所以需要提供一行有多少元素,来正确进行内存访问
{}
void test (int arr[][5]){}
void test(int *arr)//err,传的是一行的地址,指针的类型是数组,int(*)[5]。
{}
void test(int *arr[5])//err,传的是第一行的地址,用指针数组接收不对,因为传的是一行的地址。
{}
void  test(int (*arr)[5]){}
void test(int **arr)//err,传的是数组的地址(第一行的地址),不是一级指针的地址,不能用二级指针接收。
{}
int main()
{
	int arr[3][5] = { 0 };
	test(&arr);
	return 0;
}
  • 注意:传二维数组的副本必须给定一行有多少个数,在C语言中,二维数组在内存中以连续的块存储,可以看作是一维数组的扩展。当传递二维数组作为参数时,需要提供一行有多少个元素的信息,以便在函数内部正确地进行内存访问。
    二维数组在内存中按行主序(row-major order)存储。也就是说,二维数组的每一行依次存储在内存中,并且相邻的元素在内存中也是相邻的。通过提供一行有多少个元素的信息,可以根据内存布局准确地计算出每个元素的地址,从而进行正确的访问。
  • 二维数组不能用二级指针来接收,因为二级指针是指向一级指针的,很明显,二维数组的数组名的类型是第一行即数组的地址应该用这个int (*)[5]即用数组指针接收。
  • 二维数组也不能用指针数组接收,类型不匹配。

4.3 一级指针传参

当函数形参为一级指针时,实参可以是同类型一维数组数组名、同类型变量的地址、同类型指针。

  • 请看如下代码加深理解:
#incldue<stdio.h>
void print(int* p, int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
        printf("%d ", p[i]);
    }
}
int main()
{
    int arr[5] = { 0,1,2,3,4 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(arr, sz);
}
#include
void test(char *p)
{}
int main()
{
  char ch = 'a';
  char *str = "abcdef";
  char* ptr = &ch; 
  test(str);
  test(&ch);
  test(ptr);
  return 0;
}

4.4 二级指针传参

当函数形参为二级指针时,实参可以是一级指针的地址、二级指针。

  • 请看下面代码,加深理解:
#incldue<stdio.h>
void test(int **ptr)
{
  printf("num = %d\n",**ptr);
}
int main()
{
  int a = 3;
  int* p = &a;
  int **pp = &p;
  test(pp);
  test(&p);
  return 0;
}

5. 函数指针

函数指针和数组指针、字符指针一样都是指针,只不过指针所指向的对象的类型不同,其中函数指针与数组指针最为相似。

5.1 函数的地址

我们先看下面代码,函数名的地址,类比数组的地址

int Add(int a, int b)
{
	return a + b;
}
int main()
{
    printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

运行的结果:
【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)_第3张图片
        可以看见输出的是一段地址,这两个地址都是函数Add的地址。既然函数的地址我们知道了,就可以用指针去保存它,我们只需要知道指针指向的对象的类型就行了。

5.2 函数指针的表示方法

  • *号代表变量是一个指针变量,函数指针所指向的类型就是函数声明去掉变量名和分号后保留的部分,包括返回值和参数的类型,与数组指针相似,函数指针的(*变量名)也要放在函数名的位置,而数组指针是放在数组名的位置。

请看如下代码:

void test()
{
  printf("i love programming!\n");
}
//除函数名以外的部分,就是的类型
void (*pf)();//函数指针,指向一个返回值为void,无参数的函数。
void* pf();//表示返回值类型为void*,无参数的函数。。
#incldue<stdio.h>
int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;//数组指针
	//int (*)[10]是数组指针类型
	int(*pf) (int, int) = &Add;
	//int (*)(int,int)是函数指针类型
	return 0;
}

再给出一组代码帮助理解:

#include
int Add(int x, int y)
{
	return x + y;
}
int main()
{
     //&Add和Add都可以
	int (*pf)(int, int) = &Add;
	int (*pf)(int, int) = Add;
	
	int r = Add(3, 5);
	printf("%d\n", r);

	int m = pf(3, 5);
	printf("%d\n", m);
    return 0;
}

运行结果如下:【C语言进阶技巧】指针掌握之道:解密指针的奇妙世界(第一部)_第4张图片

  • 函数指针与数组指针有相似性,可以对比着去理解。

5.3 两行有趣的代码

5.3.1 将0强制转换为函数指针类型然后调用

void (*p)() -p是函数指针
void (*)()  -是函数指针类型 

(*  (   ( void(*)() )0 ) )();
//(((void(*)())0)是将0强制转换为函数至真
//(*(函数指针))是将函数指针解引用
//然后不传参数调用(*(函数指针))(); 

在这里插入图片描述

5.3.2 数组指针、函数指针等特殊变量类型typedef的使用

#include
typedef int* ptr_t;//一级指针别名
typedef unsigned int uint;//无符号整形的别名

typedef int(*parr_t)[10];//数组指针的别名
typedef int (*pf_t)(int, int);//函数指针的别名
int main()
{
	ptr_t p1;
	uint u2;
	parr_t p2;
	pf_t p3;
}
  • 函数指针与数组指针别名的创建与其变量的创建类似,可类比理解。

5.3.3 函数指针类型的函数的声明,并使用别名typedef化简

请看如下代码:

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

解析:
在这里插入图片描述
          我们可以观察到signal函数的返回类型和其中一个参数是相同的都是函数指针类型,下面我们利用别名typedef来使这段代码看起来更加易懂:

typedef void (*pf_t)(int);
pf_t signal(int,pf_t);

          CSDN上的别名没有高亮,但VS2019里面是有高亮的,定义函数后,你会发现编译器是不会报错,说明这样是可行的。
在这里插入图片描述

你可能感兴趣的:(C语言进阶篇,指针,程序人生,c语言,算法,青少年编程,microsoft)