C语言指针进阶操作 - 指针类型|指针传参|回调函数|笔试题

目录

1指针的大小

2指针类型

2.1字符指针

2.1.1指向字符串常量

2.1.2指向字符数组

2.1.3访问字符串

2.1.4使用指针遍历字符串

2.2指针数组

2.3数组指针

2.4函数指针

2.4.1来看两段代码

2.4.1回调函数

2.5函数指针数组

3数组参数,指针参数

3.1&数组名 VS 数组名

3.2一维数组传参

3.3二维数组传参 

3.4一级指针传参

3.5二级指针传参

4指针笔试题

4.1对于数组名和字符串指针的理解  

4.2程序的结果是什么?

4.3如下表表达式的值分别为多少?

4.4程序的结果是什么?

4.5小心有坑!

4.6打印结果为?

4.7Work at alibaba


1指针的大小

指针是一种特殊的变量,用于存储内存地址。可以直接访问内存中的数据,而无需知道变量的名称。指针变量包含一个内存地址,该地址指向存储在计算机内存中的数据位置。

指针的大小即地址的大小,地址是物理的电线上产生的,对于32位机器,有32根地址线,即有32个1和0组成的二进制序列,这个二进制序列就叫做地址,需要32个比特位才能存储这个地址。许多32位操作系统上,指针通常是4个字节(32位),而在64位操作系统上,指针通常是8个字节(64位)。 

虽然指针的大小都是4/8字节,但指针仍有许多类型,不同类型对应的指针在存放、解引用、指针运算上有差异。

2指针类型

2.1字符指针

字符指针是指向字符数据的指针,在处理字符串和字符数组中有重要作用。

字符指针的用途包括但不仅限于:

2.1.1指向字符串常量

char *strPtr = "Hello, world!";

要注意,这里不是把一个字符串放到strPtr指针变量里了,本质是把字符串"Hello World"的首字符的地址放到了strPtr中。

有一道面试题考察了这个知识点:

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    char *str3 = "hello world.";
    char *str4 = "hello world.";
    if(str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");
       
    if(str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");
       
    return 0;
}

运行结果:
str1 and str2 are not same
str3 and str4 are same

数组名是首地址,str1和str2的首地址不一样,所以str1和str2显然不相等;*str3和*str4都指向了同一个字符串,两个字符指针存放的地址是相同的,所以是str3和str4是相同的。 

2.1.2指向字符数组

char str[] = "Hello";
char *strPtr = str;

2.1.3访问字符串

printf("%s", strPtr);

2.1.4使用指针遍历字符串

while (*strPtr != '\0') {
    printf("%c", *strPtr);
    strPtr++;
}

字符指针在中经常用于字符串的处理和操作,例如字符串的拷贝、比较、连接等。这些功能包含在string.h中,详见:cplusplus.com/reference/cstring/?kw=string.h

2.2指针数组

指针数组,即存放指针的数组。数组的每一个元素都是一个指针类型的变量。示例如下:

int *p[10];

这里要注意: [] 的优先级要高于 的,所以 先与 [] 结合,说明他的一个存放10个变量的数组,而数组的每一个变量类型是 int*。

数组指针的使用:

指针数组的使用场景很多,其中一个典型的用法是处理字符串数组。比如:

#include <stdio.h>

int main() {
    char *strings[] = {"apple", "banana", "orange"};

    for (int i = 0; i < 3; i++) {
        printf("%s\n", strings[i]);
    }

    return 0;
}

还可以通过指针数组来模拟实现二维数组:

#include <stdio.h>

int main()
{
        int arr1[] = { 1,2,3,4,5 };
        int arr2[] = { 2,3,4,5,6 };
        int arr3[] = { 3,4,5,6,7 };

        int* arr[3] = { arr1, arr2, arr3 };

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

2.3数组指针

数组指针,即指向数组的指针。与指针数组不同,数组指针指向的是一个完整的数组,而不是数组中的单个元素。示例如下:

int (*p)[10];

由于 () 优先级最高,所以 p 先与 结合,说明他是一个指针,指向的是一个大小为10,元素类型为int的数组。

数组指针的一个重要运用就是传递多维数组给函数

#include<stdio.h>

void printArray(int (*arr)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr[i][j]);
            //也可以写为:printf("%d ", *(*(arr + i) + j));
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
    printArray(arr, 2);
    return 0;
}

二维数组的数组名表示二维数组第一行的地址,是一个一维数组的地址,类型为数组指针。所以这里 printArray 函数接收的是二维数组第一行的地址,通过arr[i][j]的方式来访问arr数组的第i行第j个元素。

2.4函数指针

函数指针可以用来存储函数的地址,使得可以动态地选择调用不同的函数,从而实现一些高级的编程技巧和模式。

若定义有:int add(int x, int y)
int (*p)(int int)  -->  就是函数指针变量

2.4.1来看两段代码

(*(void (*)())0)()

 从里层括号开始看:

  1. void(*)()是函数指针类型
  2. 括号里面放类型 就相当于 强制类型转换,相当于将0(地址处)强制类型转化位一个这样类型的函数
  3. 然后再解引用,调用这个该函数,因为函数没有参数,所以只给了一个括号

综上:这句代码是在先将0强制类型转换为void(*)()的函数指针,再调用0地址处的函数。

void (*signal(int , void(*)(int)))(int);
  1. signal(int , void(*)(int))是在申明函数
  2. signal的形参类型为int和一个函数指针,该函数指针的类型为void(*)(int)
  3. void(*)(int)是signal函数的返回类型, 也是一种函数指针类型

可以通过typedef重定义函数指针来简化这段代码:

//简化:
typedef void(* pf )(int);
pf signal(int, pf);

函数指针有一个非常大的用途,就是实现回调函数

2.4.1回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

通过使用回调函数,可以实现程序的灵活性和可扩展性,使得程序能够根据不同的情况执行不同的逻辑。

 一个典型的回调函数应用就是qsort函数

(图片来源:qsort - C++ Reference (cplusplus.com)

qsort函数提供了一种方便且高效的方法来对数组进行排序,用户可以根据自己的需求提供不同的比较函数来实现不同的排序逻辑(升序或降序,任意类型的数据排序)。

qsort的最后一个参数就是一个函数指针,在sort函数中调用时qsort时,我们传入写好的compare函数的地址,然后在qsort中会通过调用compare函数来实现排序,这就是回调函数。

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

struct data
{
	char n[10];
	char num;
};
void print_int(int arr[], int size)
{
	for (int i = 0; i < size; i++)
	{
		printf("%d ", arr[i]);
	}
}

void print_struct(struct data arr[], int size)
{
	for (int i = 0; i < size; i++)
	{
		printf("%s %d\n", arr[i].n, arr[i].num);
	}
}

compare_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;  //升序
}

compare_struct(const void* p1, const void* p2)
{
	return ((struct data*)p2)->num - ((struct data*)p1)->num; //降序
}

sort1()
{
	int arr[10] = { 7,4,6,2,6,1,3,8,9,0 };
	int size = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, size, sizeof(arr[0]), compare_int);

	print_int(arr, size);
}

sort2()
{
	struct data arr[] = { {"ChinaLYB", 14}, {"Buider", 18}, {"haisai233", 12} };

	int size = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, size, sizeof(arr[0]), compare_struct);

	print_struct(arr, size);
}

int main()
{
	sort1();

	printf("\n\n");

	sort2();

	return 0;
}

2.5函数指针数组

函数指针数组是指一个数组,其中的每个元素都是函数指针。换句话说,函数指针数组存储了一组函数指针,使得可以通过数组索引来访问和调用不同的函数。

以下是一个使用函数指针数组实现的简单的计算器程序

#include<stdio.h>

void lobby()
{
	printf("**********Caculator***********\n");
	printf("*******1.add      2.sub*******\n");
	printf("*******3.div      4.mul*******\n");
	printf("************0.exit************\n");
}

int add(int x, int y)
{
	return x + y;
}

int sub(int x, int y)
{
	return x - y;
}

int div(int x, int y)
{
	return x / y;
}

int mul(int x, int y)
{
	return x * y;
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int result = 0;
	do
	{
		lobby();
		printf("Please choose :>");
		scanf("%d", &input);

		//函数指针数组
		int(*pfArr[5])(int, int) = { NULL, add, sub, div, mul };

		if (input >= 1 && input <= 4)
		{
			printf("Please input two oprands:>");
			scanf("%d %d", &x, &y);
			result = pfArr[input](x, y);
			printf("result = %d\n", result);
		}
		else if (input == 0)
		{
			printf("Exiting... ...\n");
		}
		else
		{
			printf("Invalid input, please choose again:>\n");
		}

	} while (input);
}

3数组参数,指针参数

3.1&数组名 VS 数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥? 

首先对于数组名(arr)的理解:

  1. 数组名是数组首元素的地址
  2. 另外有两个例外:
    1. sizeof(数组名) :数组名表示整个数组, sizeof(数组名)是计算整个数组的大小,单位是字节
    2. &数组名 :数组名表示整个数组,&数组名取出的是整个数组的地址
    3. 除此之外,数组名都表示数组首元素的地址

 对于&数组名(&arr)的理解:

&arr表示的是数组的地址,而不是数组首元素的地址。

可以通过下面这个例子深刻体会:

#include <stdio.h>
int main()
{
   int arr[10] = { 0 };

   printf("arr = %p\n", arr);
   printf("&arr= %p\n", &arr);
   printf("arr+1 = %p\n", arr+1);
   printf("&arr+1= %p\n", &arr+1);

   return 0;
}

 运行结果:

arr = 00B1F900
&arr= 00B1F900
arr+1 = 00B1F904
&arr+1= 00B1F928

 可见,虽然arr与&arr存放的地址相同,但指针的类型不同,性质也就不同。对于arr+1,指针类型为int*,所以指针向后移动4个字节(一个int的大小,即arr数组一个元素的大小),指向指针第二个元素;而对于&arr+1,指针类型为int(*)[],所以指针向后移动40个字节(十个int的大小,一个数组的大小)

3.2一维数组传参

在传递一维数组给函数时,并不会复制整个数组,而是传递数组的地址(指针)。这意味着在函数内部对数组进行的任何修改都会影响到原始的数组。

下面来看一些一维数组传参的示例判断是否可行:

#include<stdio.h>

int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}


void test(int arr[])  //ok?
{}
//ok -> 为什么可以省略大小?因为函数正如上述所说,数组传参传递的是数组的地址,而地址得到的大小时确定的,所以大小可以省略
void test(int arr[10])  //ok?
{}
//ok -> 也可以,同样由于地址得到的大小时确定的,编译器会自动忽略这个大小
void test(int *arr)  //ok?
{}
//ok
void test2(int *arr[20]) //ok?
{}
//ok
void test2(int **arr)  //ok?
{}
//ok -> arr2可以理解为指针数组的首元素地址,是地址的地址,故用一个二级指针来接收是合理的

3.3二维数组传参 

传递二维数组作为参数与传递一维数组类似,但有一些细微的区别。

二维数组实际上是以一维数组的形式存储的连续内存块。因此,在将二维数组传递给函数时,需要明确指定数组的列数

二维数组的数组名代表的是第一行(相当于一个一维数组)的地址,是一个数组的地址,类型为数组指针(*)[],这意味着如果直接使用一个二维数组的数组名作为参数传递时,需要用一个数组指针类型的形参来接收。

下面来看一些二维数组传参的示例判断是否可行:

#include<stdio.h>

int main()
{
int arr[3][5] = {0};
test(arr);
}

void test(int arr[3][5])  //ok?
{}

//ok
void test(int arr[][])  //ok?
{}
//not ok -> 行可以省略但列不能省略
void test(int arr[][5])  //ok?
{}
//ok
void test(int *arr)  //ok?
{}
//not ok -> arr是第一行的地址,而不是首元素的地址
void test(int* arr[5])  //ok?
{}
//not ok -> 形参是指针数组,而不是数组指针
void test(int (*arr)[5])  //ok?
{}
//ok -> 数组指针
void test(int **arr)  //ok?
{}
//not ok -> 传过来的第一行的地址不能用二级指针来接收,二级指针是接收一级指针

3.4一级指针传参

通过传递指针作为参数,函数可以修改指针所指向的值,而不仅仅是传递该值的副本。这使得我们可以在函数内部更改调用函数的作用域中的变量。

也可以通过一级指针将一维数组的地址传递给打印函数,再在函数中访问函数,比如:

#include <stdio.h>
void print(int* p, int sz)
{
       int i = 0;
        for (i = 0; i < sz; i++)
        {
                printf("%d\n", *(p + i));
        }
}
int main()
{
        int arr[10] = { 1,2,3,4,5,6,7,8,9 };
        int* p = arr;
        int sz = sizeof(arr) / sizeof(arr[0]);
        //一级指针p,传给函数
        print(p, sz);
        return 0;
}

3.5二级指针传参

#include<stdio.h>

void test(int** ptr)
{
    printf("num = %d\n", **ptr); 
}

int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);

    return 0;
}

4指针笔试题

4.1对于数组名和字符串指针的理解  

下列程序会打印什么?

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

int main()
{
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));
	//4*4 = 16

	printf("%d\n", sizeof(a + 0));
	//只有sizeof里面只放数组名时数组名才表示整个数组,这里的数组名是数组的首地址
	//首元素地址+0 -> 还是首元素的地址
	//地址大小 -> 4/8(4(x86),8(x64))

	printf("%d\n", sizeof(*a));
	//数组名是数组的首地址
	//解引用是个int -> 4字节

	printf("%d\n", sizeof(a + 1));
	//同上, 4/8

	printf("%d\n", sizeof(a[1]));
	//4

	printf("%d\n", sizeof(&a));
	//取出的是数组的地址 -> 4/8字节

	printf("%d\n", sizeof(*&a));
	// == sizeof(a) -> 16
	//or : &a -> int(*)[4]  对一个数组指针解引用 -> 访问的是一个数组,sizeof:16字节

	printf("%d\n", sizeof(&a + 1));
	//&a + 1相当于是 &a跳过了一个数组,仍然是地址 ->4/8

	printf("%d\n", sizeof(&a[0]));
	//首元素地址 -> 4/8

	printf("%d\n", sizeof(&a[0] + 1));
	//4/8


	char arr[] = { 'a','b','c','d','e','f' };

	printf("%d\n", sizeof(arr));
	//数组名单独放在sizeof内部,数组名表示整个数组,6字节

	printf("%d\n", sizeof(arr + 0));
	//数组名首元素(char*)地址 -> 4/8

	printf("%d\n", sizeof(*arr));
	//*arr -> 首元素 - char - 一字节

	printf("%d\n", sizeof(arr[1]));
	//一字节

	printf("%d\n", sizeof(&arr));
	//&arr -> 数组的地址 -> 4/8

	printf("%d\n", sizeof(&arr + 1));
	//跳过整个数组后的地址 - 4/8

	printf("%d\n", sizeof(&arr[0] + 1));
	//第二个元素的地址 4/8个字节

	printf("%d\n", strlen(arr));
	//随机值 直到遇到 '\0'

	printf("%d\n", strlen(arr + 0));
	//arr+0 -> 首元素地址 和第一个一样

	//printf("%d\n", strlen(*arr));
	//strlen需要传入一个地址,而这里传入的是一个字符'a' - 97,就是将97作为地址传参,strlen就会从97这个地址传参开始直到遇到'\0'
	//error

	//printf("%d\n", strlen(arr[1]));
	//error

	printf("%d\n", strlen(&arr));
	//数组的地址和数组首元素的地址,值是一样的.依然从数组的第一个元素开始算,是个随机数

	printf("%d\n", strlen(&arr + 1));
	//同上

	printf("%d\n", strlen(&arr[0] + 1));
	//第二个元素的地址 - 4/8字节


	char* p = "abcdef";//'a' 'b' 'c' 'd' 'e' 'f' '\0'

	printf("%d\n", sizeof(p));
	//p是一个指针变量, 4/8

	printf("%d\n", sizeof(p + 1));
	//p+1是b的地址, 地址大小4/8

	printf("%d\n", sizeof(*p));
	//首元素 1字节

	printf("%d\n", sizeof(p[0]));
	//一个字节

	printf("%d\n", sizeof(&p));
	//字符串的地址 4/8

	printf("%d\n", sizeof(&p + 1));
	//跳过一个字符串的地址 4/8

	printf("%d\n", sizeof(&p[0] + 1));
	//第二个元素的地址 4/8


	printf("%d\n", strlen(p));
	//6

	printf("%d\n", strlen(p + 1));
	//5

	//printf("%d\n", strlen(*p));
	//error

	//printf("%d\n", strlen(p[0]));
	//error

	printf("%d\n", strlen(&p));
	//随机数 &p ->一个地址

	printf("%d\n", strlen(&p + 1));
	//随机数

	printf("%d\n", strlen(&p[0] + 1));
	//5

	return 0;
}

4.2程序的结果是什么?

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}

答案:2,5

解析:&a取出的是整个数组的地址,加1代跳过一整个数组的大小,即&a+1代表数组a后4个字节的地址。然后将这个地址强制类型转化为(int*)并用*ptr储存。a是首元素地址,加1代表第二个元素的地址,解引用得到 2, ptr - 1得到a数组最后一个元素的地址,解引用得到 5。

4.3如下表表达式的值分别为多少?

struct Test
{
   int Num;
   char *pcName;
   short sDate;
   char cha[2];
   short sBa[4];
}*p;

//假设p 的值为0x100000。
int main()
{
   printf("%p\n", p + 0x1);
   printf("%p\n", (unsigned long)p + 0x1);
   printf("%p\n", (unsigned int*)p + 0x1);

   return 0;
}

答案:0x100014

           0x100001

           0x100004

解析:首先算出结构体的大小为(4+4+2+1*2+2*4=)20字节。由于指针+1是加几取决于指针的类型,所以这里先明确指针的类型:

p为结构体指针,大小为20,故p + 0x1  ->  0x100000+20 = 0x100014

(unsigned long)p为无符号长整型,不是指针,所以+0x1正常运算即可 = 0x100001

(unsigned int*)p为无符号整形指针,大小为4,故(unsigned int*)p + 0x1 -> 0x100000 + 4 =0 0x100004

4.4程序的结果是什么?

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;
}

答案:0x4,0x200000

解析:ptr[-1]的情况与题4.2相似(如图4.4.1);对于ptr2储存的地址,先是将数组首元素地址强制类型转化为int类型,+1代表地址+1字节,指向的位置如图4.4.2,这时再转换为int*类型,解引用时顺序访问4个字节的数据,由于是小端程序,还原时为:0x0200000。

(图 4.4.1) 

(图4.4.2)

4.5小心有坑!

#include <stdio.h>
int main()
{ 
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);

    return 0;
}

答案:1

解析: 用小括号初始化???——>逗号表达式,实际上存的是1 3 5 0 0 0,故p[0] 为 1

 4.6打印结果为?

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

    return 0;
}

答案:0xFFFFFFFC,-4

解析: 

首先可以由图确定的是第二个的答案是-4,而-4的原码反码补码分别为:

10000000000000000000000000000100

11111111111111111111111111111011

1111 1111 1111 1111 1111 1111 1111 1100

以%p(16进制)打印:

F      F      F       F     F       F      F     C

4.7Work at alibaba

#include <stdio.h>
int main()
{
   char *a[] = {"work","at","alibaba"};
   char**pa = a;
   pa++;
   printf("%s\n", *pa);

   return 0;
}

答案:at

解析:char *a[]定义了一个指针数组,其中每个元素都是指向字符数组的指针。char **pa是指向指针的指针,初始化为指向数组a的第一个元素。然后pa++将pa指向数组a的第二个元素。因此,*pa等价于a[1],即指向字符串 "at" 的指针。

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值