C语言进阶篇 二. 指针

目录

1. 字符指针

2. 数组指针 与 指针数组

4. 数组传参和指针传参

一维数组传参

二维数组传参

一级指针传参

二级指针传参

5. 函数指针

6. 函数指针数组

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

8. 回调函数

9. 指针和数组面试题的解析

数组笔试题

指针笔试题


1. 字符指针

从例题看起—>

#include <stdio.h>
int main()
{
    char str1[] = "hello!";
    char str2[] = "hello!";
    const char *str3 = "hello!";
    const char *str4 = "hello!";
    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和str2不同,str3和str4相同。

原因是str1和str2数组是在不同内存空间上开辟空间,而str3和str4指向的字符串是相同的常量字符串,C/C++会把常量字符串存储到单独的一个内存区域(静态区),当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存区域。

2. 数组指针 与 指针数组

数组指针是指向数组的指针。

可以这么定义吗?

int * p1 [ 10 ];
int ( * p2 )[ 10 ];
//p1, p2 分别是什么?
需要注意的是操作符的优先级:[ ]比*的优先级高
故对于p1而言,p1先与[]结合表明p1是一个10个元素的数组,数组的每个元素类型是int *,故这是在定义一个指针数组。
对于p2而言,p2先与*结合,表明p2是一个指针,指向的一个10个元素的数组,数组的每个元素类型是int ,故这是一个数组指针。(注意:[10]内的10不可省略,否则指针的步长是不可知的)
实例1(数组指针的使用):
#include <stdio.h>
//例如要打印一个二维数组的每个元素,可以这么写
void print_arr1(int arr[3][5], int row, int col)
{
   int i = 0;
   for(i=0; i<row; i++)
  {
       for(int j=0; j<col; j++)
      {
           printf("%d ", arr[i][j]);
      }
       printf("\n");
  }
}
//上面写法其实与下面写法等价,因为二维数组的数组名是首行元素的地址
//是一个数组指针
void print_arr2(int (*arr)[5], int row, int col)
{
   int i = 0;
   for(i=0; i<row; i++)
  {
       for(int j=0; j<col; j++)
      {
           printf("%d ", arr[i][j]);
      }
       printf("\n");
  }
}
int main()
{
   int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
   print_arr1(arr, 3, 5);
   print_arr2(arr, 3, 5);
   return 0;
}

实例2:理解如下写法

int arr [ 5 ];                          //定义一个数组
int * parr1 [ 10 ];                   //定义一个指针数组
int ( * parr2 )[ 10 ];                //定义一个数组指针
int ( * parr3 [ 10 ])[ 5 ];           //定义一个数组指针数组
                                       
//parr3先与[10]结合表明是一个数组,数组的每个元素类型是int(*)[5]即数组指针,故定义的是一个有10个元素的数组,数组的每个元素类型都是指向5个整型元素数组的指针(即数组指针)。

4. 数组传参和指针传参

一维数组传参

#include <stdio.h>
1.void test(int arr[])//ok?
{}
/正确,这种写法与void test(int *arr){}等价,
/传参的本质是传一个指针,传数组名的时候传的是首元素地址。
2.void test(int arr[10])//ok?
{}
/正确,与上一种相同,[]内无具体要求,因为传的是指向整型元素的一级指针
3.void test(int *arr)//ok?
{}
/同上两种
--------------------------------------------------------------

4.void test2(int *arr[20])//ok?
{}
/传数组名的时候,传的是指针数组的首元素地址,与第一种不同的是数组元素类型不是int而是int*
5.void test2(int **arr)//ok?
{}
/与上一种写法相同,因为指针数组首元素类型是指针,故首元素地址类型是int**
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

二维数组传参

/首先二维数组的数组名是首行的地址,也即是一个数组的地址/
1.void test(int arr[3][5])//ok?
{}
/正确
2.void test(int arr[][])//ok?
{}
/错误,二维数组传参,列数不可省略
3.void test(int arr[][5])//ok?
{}
/正确
//二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。(也决定着数组指针的步长,即一步跳过几个元素的数组)

4.void test(int *arr)//ok?
{}
/传一级指针,错误
5.void test(int* arr[5])//ok?
{}
/错误的,这个写法与7.等价,使用时传的应该是指针数组的地址即二级指针
6.void test(int (*arr)[5])//ok?
{}
/传数组指针,正确,[5]中5当准确填写!
7.void test(int **arr)//ok?
{}
//传二级指针,错误
int main()
{
 int arr[3][5] = {0};
 test(arr);/传参本质是传数组指针
}

一级指针传参

#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;/将首元素地址赋给了p/ 
     /也可以这么写 int *p=&arr[0];
     int sz = sizeof(arr)/sizeof(arr[0]);
     //一级指针p,传给函数
     print(p, sz);
     return 0;

 }

二级指针传参

void test(char **p) {
 
}
int main()
{
 char c = 'b';
 char*pc = &c;/pc是一级指针
 char**ppc = &pc;/ppc是二级指针
 char* arr[10];/arr是指针数组的数组名,是首元素地址,也是二级指针
 test(&pc);
 test(ppc);
 test(arr);//Ok?,

/以上调用二级指针写法都正确.
 return 0; 
}

5. 函数指针

顾名思义是函数的地址:

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0; 
}

代码结果是两者地址相同,在C语言中函数名和&函数名区别不大,函数名就表示了函数的地址。

要想保存函数的地址,应用函数指针:

例如:

void test ()
{
printf ( "hehe\n" );
}
要保存test函数的地址应当如此声明:
void ( * pfun1 )();//pfun1先与*结合,表明这是一个指针,pfun1右边的*()表明指向的函数的参数,左边void 表明指向的函数的返回类型
void * pfun2 ();//错误的声明,pfun2与()先结合表明pfun2是一个函数,void*是函数的返回类型。
深刻理解函数指针:
// 代码 1
( * void ( * )() ) 0 )();
// void (*)()是一个函数指针类型,加上()放在0前面,表示将0强制类型转换为函数指针类型的变量,然后再与*结合,解引用函数指针变为函数,最右边()是函数的形参
// 代码 2
void ( * signal ( int , void ( * )( int )))( int );
//signal先与()结合表明signal是一个函数,函数的两个参数类型分别为整型int和函数指针
void(*)(),函数的返回类型是函数指针void(*)(int)。
//代码二略显可读性差,可以优化一下
typedef void ( * pfun_t )( int );//将函数指针类型重命名为pfun_t,pfun_t应当放在括号内,这是语法规定
pfun_t signal ( int , pfun_t );//于是代码二可以这么写,较为易读。

6. 函数指针数组

顾名思义是存放函数指针的数组,看似复杂,其实是个数组。

例子分析:
int ( * parr1 [ 10 ])();//parr1先与[10]结合,表明parr1是一个数组,数组每个元素类型是 函数指针类型                                int(*)(),故这个数组是函数指针数组
int * parr2 [ 10 ]();//错误的声明,编译器会报错数组的元素类型不能是函数或抽象类类型
int ( * )() parr3 [ 10 ];//错误的声明,语法错误,parr3[10]当放在*右边

函数指针数组用途:转移表(存放多个函数)

int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表,便于函数的调用

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

顾名思义的6.函数指针数组的地址。

理解如下代码:

void test(const char* str) {
 printf("%s\n", str);
}
int main()
{
 /函数指针pfun
 void (*pfun)(const char*) = test;
 /函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 /指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;/注意()的使用,使用错误时意义大不相同。
/理解方式:ppfunArr先与*结合表明ppfunArr是一个指针,指针指向5个元素的数组,数组每个元素的类型是
/函数指针void(*)(const char*)
 return 0; 

}

8. 回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这个被调用的函数是回调函数。
一般而言,我们不能修改库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数去实现我们想实现的功能。
例如:调用库函数qsort去排序不同类型的数据时,使用者要自己实现一个回调函数
首先了解qsort:
void qsort( void *base, size_t number, size_t width, int ( *compare )(const void *elem1, const void *elem2) );
qsort的四个参数分别为排序数据的首地址,排序的元素个数,元素大小,比较函数指针(elem1是元素1的地址,elem2是元素2的地址,当元素1小于元素2比较函数返回负数可进行升序排序) 
#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2) {
  return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
//
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0; 
}

写一个冒泡排序,可以排序任意类型的数据:

#include <stdio.h>
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;
   }
}/逐个字节交换数据,粒度最细,可以实现各种数据类型的交换
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
                 /比较函数返回正值,交换两个元素,实现升序排序
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0; 
}

9. 指针和数组面试题的解析

假设是在32位平台下

数组笔试题


1.一维数组
数组名是首元素地址,除了两个例外
(1)sizeof(数组名),求整个数组大小
(2)&数组名 得到数组地址(数组指针)
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); 16 数组大小
printf("%d\n",sizeof(a+0)); 4 首元素地址
printf("%d\n",sizeof(*a)); 4 首元素
printf("%d\n",sizeof(a+1)); 4 第二个元素地址
printf("%d\n",sizeof(a[1])); 4 第二个元素
printf("%d\n",sizeof(&a)); 4 数组指针
printf("%d\n",sizeof(*&a)); 16 数组大小
printf("%d\n",sizeof(&a+1)); 4 数组指针
printf("%d\n",sizeof(&a[0])); 4 第一个元素地址
printf("%d\n",sizeof(&a[0]+1)); 4 第二个元素地址
strlen是求字符串长度函数,从给定的地址往后计数直到遇到\0为止,放回\0之前的字符个数
2.字符数组
char arr[] = {'a','b','c','d','e','f'};数组初始化为6个元素
printf("%d\n", sizeof(arr)); 6 数组大小
printf("%d\n", sizeof(arr+0)); 4 数组首元素地址
printf("%d\n", sizeof(*arr)); 1 数组首元素
printf("%d\n", sizeof(arr[1])); 1 数组第二个元素
printf("%d\n", sizeof(&arr)); 4 数组指针
printf("%d\n", sizeof(&arr+1)); 4 数组指针
printf("%d\n", sizeof(&arr[0]+1)); 4 第二个元素地址
printf("%d\n", strlen(arr)); 随机值>=6
printf("%d\n", strlen(arr+0)); 随机值>=6
printf("%d\n", strlen(*arr)); 将*arr也即a的ASCII码值作为地址开始访问,非法访问内存
printf("%d\n", strlen(arr[1])); 非法访问内存
printf("%d\n", strlen(&arr)); 随机值>=6
printf("%d\n", strlen(&arr+1)); 跳过整个数组开始求长度,比上行结果少6的随机值
printf("%d\n", strlen(&arr[0]+1)); 随机值,从b的地址开始求字符串长度
-----------------------------------
char arr[] = "abcdef";数组初始化为7个元素含\0
printf("%d\n", sizeof(arr)); 7 数组大小
printf("%d\n", sizeof(arr+0)); 4 数组首元素地址
printf("%d\n", sizeof(*arr)); 1 首元素
printf("%d\n", sizeof(arr[1])); 1 第二个元素
printf("%d\n", sizeof(&arr)); 4 数组指针
printf("%d\n", sizeof(&arr+1)); 4 数组指针
printf("%d\n", sizeof(&arr[0]+1)); 4 第二个元素地址
printf("%d\n", strlen(arr)); 6 字符串长度
printf("%d\n", strlen(arr+0)); 6 字符串长度
printf("%d\n", strlen(*arr)); 将a的ASCII码值作为地址,非法访问内存
printf("%d\n", strlen(arr[1])); 非法访问内存
printf("%d\n", strlen(&arr)); 6 字符串长度
printf("%d\n", strlen(&arr+1)); 随机值,从\0后开始求字符串长度
printf("%d\n", strlen(&arr[0]+1)); 5 从b开始求字符串长度
--------------------------------
char *p = "abcdef";p是字符指针指向常量字符串
printf("%d\n", sizeof(p)); 4 首元素地址
printf("%d\n", sizeof(p+1)); 4 第二个元素地址
printf("%d\n", sizeof(*p)); 1 首元素
printf("%d\n", sizeof(p[0])); 1 首元素
printf("%d\n", sizeof(&p)); 4 二级指针(p的地址)
printf("%d\n", sizeof(&p+1)); 4 指针
printf("%d\n", sizeof(&p[0]+1)); 4 b的地址
printf("%d\n", strlen(p)); 6 字符串长度
printf("%d\n", strlen(p+1)); 5 从b开始求字符串长度
printf("%d\n", strlen(*p)); 非法访问内存
printf("%d\n", strlen(p[0])); 非法访问内存
printf("%d\n", strlen(&p)); 随机值,从p的地址开始求字符串长度
printf("%d\n", strlen(&p+1)); 随机值,从p的地址跳过4个字节(char*类型)开始求字符串长度
printf("%d\n", strlen(&p[0]+1)); 5 从b开始求字符串长度

3.二维数组
二维数组看成多个一维数组为元素组成的数组,二维数组的数组名是首行数组的地址
int a[3][4] = {0};
printf("%d\n",sizeof(a)); 48 整个二维数组
printf("%d\n",sizeof(a[0][0])); 4 二维数组首行的首元素
printf("%d\n",sizeof(a[0])); 16 相当于首行数组数组名单独在sizeof内,求的是首行数组大小
printf("%d\n",sizeof(a[0]+1)); 4 a[0]是首行首元素地址,加一跳过一个元素到第二个元素的地址
printf("%d\n",sizeof(*(a[0]+1))); 4 首行第二个元素
printf("%d\n",sizeof(a+1)); 4 第二行数组的地址
printf("%d\n",sizeof(*(a+1))); 16 第二行数组大小
printf("%d\n",sizeof(&a[0]+1)); 4 第二行数组地址
printf("%d\n",sizeof(*(&a[0]+1))); 16 第二行数组大小
printf("%d\n",sizeof(*a)); 16 第一行数组大小
printf("%d\n",sizeof(a[3])); 16 理解为*(a+3),a是数组指针,加一跳过一行,
解引用是那行数组的数组名,求的是那行数组的大小,sizeof只求大小不访问该内存空间

指针笔试题

笔试题1:
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0; 
}
//程序的结果是什么?
a+1是第二个元素的地址,*解引用得第二个元素2
&a是数组地址,加一跳过整个数组,类型转换为int*,ptr-1是元素5的地址,*解引用是5
笔试题2:

结构体的大小是20个字节
struct Test
{
 int Num; 4
 char *pcName;4 
 short sDate; 2
 char cha[2]; 2
 short sBa[4]; 8
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
 printf("%p\n", p + 0x1);p的类型是结构体指针,加一跳过一个结构体大小20个字节,
                          16进制表示为0x100014
 printf("%p\n", (unsigned long)p + 0x1);p被强制转换为长整形,加一是整数加减为0x100001
 printf("%p\n", (unsigned int*)p + 0x1);p被强制类型转换为整形指针,加一跳过一个整形,
                                        故为0x100004
 return 0; 
}
笔试题3:
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);%x是以16进制打印
    return 0;
 }
&a是取出数组地址,加一跳过整个数组大小,强制类型转换为int*类型,
ptr1[-1]理解为*(ptr-1),为4

a是首元素地址,类型转换为整形,加一跳过一个字节,即指向01后面的0的位置01 00 00 00 02,
解引用4个字节为02000000,故打印2000000
笔试题4:
#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };注意逗号表达式,依次计算各个表达式,
 但以最后一个表达式返回,故二维数组内容实际上是1,3, 5,0, 0,0
    int *p;
    p = a[0];
    printf( "%d", p[0]);
    return 0; 
}
a[0]是首行数组的数组名,故p是元素1的地址,p[0]与*(p+0)等价,故打印1
笔试题5:
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; 
}
注意:指针相减得到的是指针间的元素个数。
p是数组指针,指向二维数组首行,类型是int(*)[4],加一将跳过(4个整型)16个字节
a也是数组指针,但类型是int (*)[5],加一跳过(5个整型)20个字节
假设数组元素  1  2  3  4  5 
             6  7  8  9 10
            11 12 13 14 15
            16 17 18 19 20
            21 22 23 24 25
&p[4][2]指向的是19,&a[4][2]指向的是23,两者相减以%p打印,
直接打印-4的补码FFFFFFFC(相差4个元素),%d打印则是-4
笔试题6:
int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1);
    int *ptr2 = (int *)(*(aa + 1));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0; 
}
&aa是整个二维数组的地址,加一跳过整个二维数组,指向元素10的后面,
类型转为int*,减一指向10,故打印10

aa是首行数组地址,加一跳过整行指向6,解引用得第二行数组数组名,类型转换为int*
减一指向5,故打印5
笔试题7:
#include <stdio.h>
int main()
{
 char *a[] = {"work","at","alibaba"};
 char**pa = a;
 pa++;
 printf("%s\n", *pa);
 return 0; 
}
数组a是指针数组,数组元素是三个字符指针,分别指向三个字符串
a是数组名,是首元素地址,即指针地址是二级指针的类型
故pa是指针,指向的元素类型是char*类型,加一跳过4个字节(指针大小)
原本pa是指向 指向work字符串的指针,现在指向 指向at字符串的指针,
解引用得指向at字符串的指针,打印得at

笔试题8:

int main()
{
 char *c[] = {"ENTER","NEW","POINT","FIRST"};
 char**cp[] = {c+3,c+2,c+1,c};
 char***cpp = cp;
 printf("%s\n", **++cpp);
 printf("%s\n", *--*++cpp+3);
 printf("%s\n", *cpp[-2]+3);
 printf("%s\n", cpp[-1][-1]+1);
 return 0; 

}
数组c是指针数组,数组元素分别是四个指向字符串首字符的字符指针
数组cp是指针数组,数组元素是4个二级指针。c是c数组首元素地址。
cpp是数组cp首元素地址。

++cpp,cpp被改变,指向了cp数组的第二个元素,解引用得c+2是数组c第三个元素的地址
,再解引用得数组c第三个元素,故打印POINT

++cpp,cpp被改变,指向了cp数组的第三个元素,解引用得c+1,--后将c+1变为c
再解引用得数组c的第一个元素,是指向ENTER字符串首字符E的字符指针,
加3指向了第二E,故打印ER

cpp[-2]理解为*(cpp-2),cpp原指向cp第三个元素,减二解引用得c+3,
再解引用得数组c第四个元素,是指向FIRST字符串首字符的字符指针,
加3指向了S,故打印ST

理解为*(*(cpp-1)-1),cpp先-1解引用得c+2,再减一得c+1,
解引用得数组c第二个元素,是指向NEW字符串首字符N的字符指针,
加一指向了E,故打印EW

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值