指针——练习

sizeof和strlen

sizeof

sizeof是用来计算变量所占内存空间大小的,单位是字节,如果操作数是类型,计算的是使用类型创建的变量所占内存空间的大小。

sizeof只关注占用内存空间的大小,不在乎内存中存放什么数据。

我们来看一下这个代码,它的运行结果是多少?

复制代码
#include<stdio.h>
int main()
{
	int a = 10;
	printf("%zu\n", sizeof(a + 3.14));
	return 0;
}

a是int类型的数据,3.14是double类型的数据,两者进行运算时,会将int类型的数据提升为double类型,所以最后结果的类型是double类型,所以最后结果应该是8.

strlen()

strlen 是 C 语言库函数,功能是求字符串长度。函数原型如下:

size_t strlen (const char * str);

统计的是从 strlen 函数的参数 str 中这个地址所指向的元素开始向后找,直到\0 之前字符串中字符的个数。

strlen 函数会一直向后找 \0 字符,直到找到为止,所以可能存在越界查找。

看以下代码,运行结果啥?

复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    char arr1[3] = {'a', 'b', 'c'};
    char arr2[] = "abc";
    
    printf("%d\n", strlen(arr1));
    printf("%d\n", strlen(arr2));
    
    printf("%d\n", sizeof(arr1));
    printf("%d\n", sizeof(arr2));
    
    return 0;
}

arr1是一个字符数组,末尾没有'\0',而strlen会一直向后找'\0',直到找到,所以使用strlen(arr1)的结果应该是一个随机值。

arr2是一个字符串,字符串的末尾含有'\0',所以strlen(arr2)的结果为3。

我们之前说过,sizeof(数组名)中,数组名表示整个数组,计算结果使整个数组的大小。

所以,sizeof(arr1)=3,sizeof(arr2)=4.

sizeof和strlen对比

|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| sizeof | strlen |
| 1. sizeof 是操作符 2. sizeof 计算操作数所占内存的大小,单位是字节 3. 不关注内存中存放什么数据 | 1. strlen 是库函数,使用需要包含头文件 string.h 2. strlen 是求字符串长度的,统计的是 \0 之前字符的个数 3. 关注内存中是否有 \0,如果没有 \0,就会持续往后找,可能会越界 |

数组和指针试题

在此之前,还是提醒一下大家:

数组名的意义

  1. sizeof (数组名),数组名单独放在sizeof()中,这里的数组名表示整个数组,计算的是整个数组的大小。
  2. & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。
  • 题目1:下面代码的运行结果

    int main()
    {
    int a[] = { 1,2,3,4 };
    printf("%zu\n", sizeof(a));
    printf("%zu\n", sizeof(a + 0));
    printf("%zu\n", sizeof(a));
    printf("%zu\n", sizeof(a + 1));
    printf("%zu\n", sizeof(a[1]));
    printf("%zu\n", sizeof(&a));
    printf("%zu\n", sizeof(
    &a));
    printf("%zu\n", sizeof(&a + 1));
    printf("%zu\n", sizeof(&a[0]));
    printf("%zu\n", sizeof(&a[0] + 1));

    复制代码
      return 0;

    }

解释:

16 //数组名单独放在sizeof()中,所以计算的是整个数组的大小,结果是4*4=16

8/4 //这里的数组名并没有单独放在sizeof()中,表示的是首元素的地址,a+0表示地址偏移量为0,所以表示的是首元素的地址,结果是指针的大小,即4/8

4 //这里的数组名并没有单独放在sizeof()中,表示首元素的地址,*a得到首元素a[0],这是一个整型变量,大小为4个字节

8/4 //a没有单独放在sizeof()中,表示首元素的地址,a+1表示地址偏移量为1,所以表示的是第二个元素的地址,结果是指针的大小,即4/8

4 //a[1]表示的是数组里面的整型元素,故结果为4

8/4 //&a取出的是整个数组的地址,既然是地址,那就是指针类型的数据,指针大小都是4/8

16 //&a取出的是整个数组的地址,那么它的类型就是数组指针:int(*)[4],那么对&a进行解引用,得到的就是整个数组,整个数组的大小自然就是16

8/4 //&a取出的是整个数组的地址,那么它的类型就是数组指针,&a+1表示要跳过整个数组的大小,但它的类型还是指针,所以结果为4/8

8/4 //a[0]是整型变量,它的地址类型就是整型指针,指针的大小就是4/8

8/4 //&a[0]是第一个元素的地址,&arr[0]+1表示的是第二个元素的地址,指针的大小就是4/8

  • 题目2:下面代码的运行结果

    #include <stdio.h>
    int main()
    {
    char arr[] = { 'a','b','c','d','e','f' };
    printf("%d\n", sizeof(arr));
    printf("%d\n", sizeof(arr + 0));
    printf("%d\n", sizeof(*arr));
    printf("%d\n", sizeof(arr[1]));
    printf("%d\n", sizeof(&arr));
    printf("%d\n", sizeof(&arr + 1));
    printf("%d\n", sizeof(&arr[0] + 1));
    return 0;
    }

这道题跟上面那道题做法几乎相同,我们再来看一下:

6 //数组名单独放在sizeof()中,所以计算的是整个数组的大小,结果是1*6=6

8/4 //这里的数组名并没有单独放在sizeof()中,表示的是首元素的地址,arr+0表示地址偏移量为0,所以表示的是首元素的地址,结果是指针的大小,即4/8

1 //这里的数组名并没有单独放在sizeof()中,表示首元素的地址,*a得到首元素a[0],这是一个整型变量,大小为1个字节

1 //arr[1]表示的是数组里面的整型元素,故结果为1

8/4 //&arr取出的是整个数组的地址,既然是地址,那就是指针类型的数据,指针大小都是 4/8

8/4 //&arr取出的是整个数组的地址,那么它的类型就是数组指针,&a+1表示要跳过整个数组的大小,但它的类型还是指针,所以结果为4/8

8/4 //&arr[0]是第一个元素的地址,&arr[0]+1表示的是第二个元素的地址,指针的大小就是 4/8

  • 题目3:下面代码的运行结果

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

    int main()
    {
    char arr[] = { 'a','b','c','d','e','f' };
    //1.
    printf("%zu\n", strlen(arr));
    //2.
    printf("%zu\n", strlen(arr + 0));
    //3.
    printf("%zu\n", strlen(*arr));
    //4.
    printf("%zu\n", strlen(arr[1]));
    5.
    printf("%zu\n", strlen(&arr));
    6.
    printf("%zu\n", strlen(&arr + 1));
    7.
    printf("%zu\n", strlen(&arr[0] + 1));
    return 0;
    }

解释:

//这个字符串中没有\0,strlen就会持续往后找\0,就会越界,所以结果是随机值

//arr和arr+0表示的都是首元素的地址,所以在同一测试环境行下,这和第一个结果一样,都是随机值

//*arr表示的是首元素a,a的ASCII码值是97,是一个整型,但是strlen需要接收的是一个字符指针类型的地址,所以会把97当成一个地址,但这块地址可能不属于当前内存,这就造成了非法访问,导致程序崩溃,如果要继续测试下面的代码,就要注释掉这句代码

//和第三个结果一样,会导致程序崩溃,如果要继续测试下面的代码,就要注释掉这句代码

//&arr的类型是数组指针类型:char(*)[],但是它里面存放的值和arr是一样的,而strlen需要接受的是char*类型的指针,所以会把&arr的结果当做char*类型来处理,所以在同一测试环境行下,这和第一个结果一样,都是随机值

//&arr+1表示跳过了整个字符数组,也就是跳过了6个字符元素,但它的类型仍然和&arr类型相同,而strlen需要接受的是char*类型的指针,所以会把&arr+1的结果当做char*类型来处理,然后往后找\0,所以结果是随机值,而且在同一测试环境行下,这和第一个结果相差6(整个字符数组的大小)

//&arr[0]+1表示的是第二个元素的地址,这个字符串中没有\0,strlen就会持续往后找\0,就会越界,所以结果是随机值,而且在同一测试环境行下,这和第一个结果相差1

  • 题目4:下面代码的运行结果

    #include <stdio.h>
    int main()
    {
    char arr[] = "abcdef";
    printf("%zu\n", sizeof(arr));//
    printf("%zu\n", sizeof(arr + 0));
    printf("%zu\n", sizeof(*arr));
    printf("%zu\n", sizeof(arr[1]));
    printf("%zu\n", sizeof(&arr));
    printf("%zu\n", sizeof(&arr + 1));
    printf("%zu\n", sizeof(&arr[0] + 1));
    return 0;
    }

这个代码的结果分析与前面代码的分析一样,我们就不再赘述,如果有不懂的,欢迎在评论区留言哦。这里我们直接给出答案:

7

8/4

1

1

8/4

8/4

8/4

  • 题目5:下面代码的运行结果

    #include <stdio.h>
    #include <string.h>
    int main()
    {
    char arr[] = "abcdef";
    printf("%zu\n", strlen(arr));
    printf("%zu\n", strlen(arr + 0));
    printf("%zu\n", strlen(*arr));
    printf("%zu\n", strlen(arr[1]));
    printf("%zu\n", strlen(&arr));
    printf("%zu\n", strlen(&arr + 1));
    printf("%zu\n", strlen(&arr[0] + 1));
    return 0;
    }

//arr表示首元素的地址,由于这是字符串,末尾隐藏着一个\0,所以计算的是\0之前的字符个数,结果是6

//arr+0表示首元素的地址,由于这是字符串,末尾隐藏着一个\0,所以计算的是\0之前的字符个数,结果是6

//和题目三中结果一样,会导致程序崩溃

//会导致程序崩溃

//&arr的类型是数组指针类型,但是它里面存放的值和arr是一样的,而strlen需要接受的是char*类型的指针,所以会把&arr的结果当做char*类型来处理,所以在同一测试环境行下,这和第一个结果一样

//&arr+1表示跳过了整个字符数组,也就是跳过了7个字符元素,但它的类型仍然和&arr类型相同,而strlen需要接受的是char*类型的指针,所以会把&arr+1的结果当做char*类型来处理,然后往后找\0,所以结果是随机值

//&arr[0]+1表示第二个元素的地址,所以结果比第一个的少1,即结果是5

  • 题目6:下面代码的运行结果

    #include <stdio.h>
    int main()
    {
    char* p = "abcdef";
    printf("%zu\n", sizeof(p));
    printf("%zu\n", sizeof(p + 1));
    printf("%zu\n", sizeof(*p));
    printf("%zu\n", sizeof(p[0]));
    printf("%zu\n", sizeof(&p));
    printf("%zu\n", sizeof(&p + 1));
    printf("%zu\n", sizeof(&p[0] + 1));
    return 0;
    }

解释:

8/4 //p是指针变量,里面存放的是a的地址,所以结果是8/4

8/4 //p+1仍然是指针,里面存放的是b的地址,所以结果是8/4

1 //*p的结果是a,a是字符型变量,所以结果是1

1 //p[0]等价于*(p+0),得到的是a,所以结果是1

8/4 //&p是二级指针,也是地址,所以结果是4/8

8/4 //&p+1仍然是指针,所以结果是4/8

8/4 //&p[0]+1表示的是b的地址,所以结果是4/8

  • 题目7:下面代码的运行结果

    #include <stdio.h>
    #include <string.h>
    int main()
    {
    char* p = "abcdef";
    printf("%zu\n", strlen(p));
    printf("%zu\n", strlen(p + 1));
    printf("%zu\n", strlen(*p));
    printf("%zu\n", strlen(p[0]));
    printf("%zu\n", strlen(&p));
    printf("%zu\n", strlen(&p + 1));
    printf("%zu\n", strlen(&p[0] + 1));
    return 0;
    }

//strlen会往p所指向的那块内存先后找,直到找到\0,所以结果是6

//p+1指向元素b,所以结果是5

//*p得到a,a的ASCII码值是97,strlen会把这个数值当成一个指针,但这个地址不一定在当前程序内,也不一定存在,就造成了非法访问,引起程序崩溃

//p[0]等价于*p,结果与上面一样

//&p是二级指针,strlen会往&p所指向的那块内存先后找,直到找到\0,但是不知道\0在那块空间的位置,所以结果是随机值

//和上面一样,结果是随机值,但是这里有一个问题,strlen(&p)和strlen(&p+1)的结果是否有关系呢?答案是没有关系,因为你不知道从p往后找是否能在走完p所对应的内存之前找到\0

//&p[0]+1表示b的地址,所以结果是5

  • 题目8:下面代码的运行结果

    #include <stdio.h>
    int main()
    {
    int a[3][4] = { 0 };
    printf("%zu\n", sizeof(a));
    printf("%zu\n", sizeof(a[0][0]));
    printf("%zu\n", sizeof(a[0]));
    printf("%zu\n", sizeof(a[0] + 1));
    printf("%zu\n", sizeof((a[0] + 1)));
    printf("%zu\n", sizeof(a + 1));
    printf("%zu\n", sizeof(
    (a + 1)));
    printf("%zu\n", sizeof(&a[0] + 1));
    printf("%zu\n", sizeof(*(&a[0] + 1)));
    printf("%zu\n", sizeof(*a));
    printf("%zu\n", sizeof(a[3]));
    return 0;
    }

解析:

二维数组是元素为一维数组的数组

48 //a是数组名,数组名单独放在sizeof()中,计算的是整个数组的大小,所以结果是4*12=48

4 //a[0][0]表示的是数组中的整型元素,所以结果是4

16 //a[0]表示的是第一行一维数组的数组名,数组名单独放在sizeof中,计算的是整个数组的大小,所以结果是4*4=16

4/8 //a[0]表示第一行一维数组的数组名,数组名并没有单独放在sizeof中,所以表示的是第一行一维数组首元素的地址,所以a[0]+1表示的是第二个元素的地址,既然是地址,那大小就是4/8

4 //a[0]+1表示的是第一行的一维数组中第二个元素的地址,那么*(a[0]+1)就是访问数组中元素,所以结果为4

4/8 //a表示二维数组的数组名,数组名并没有单独放在sizeof中,表示的是第一行一维数组的地址,a+1表示要跳过第一行整个一维数组,指向第二行,即表示第二行一维数组的地址,既然是地址,那大小就是4/8

16 //a+1表示第二行一维数组的地址,*(a+1)表示访问整个一维数组,既然是访问整个一维数组,结果自然是4*4=16(也可以这么理解:*(a+1)等价于a[1],a[1]表示的是第二行一维数组的数组名,数组名单独放在sizeof中,结果就是计算这个一维数组的大小)

4/8 //a[0]表示第一行一维数组的数组名,前面有&,取出的是整个一维数组的地址,所以&arr[0]+1表示跳过第一行整个一维数组,指向第二行整个一维数组,既然还是地址,那么结果就是4/8

16 //&a[0]+1表示第二行整个一维数组的地址,对它进行解引用,访问的是整个一维数组,所以结果是16

16 //a表示二维数组的数组名,数组名并没有单独放在sizeof中,表示的是第一行一维数组的地址,对这个地址进行解引用,访问的是整个一维数组,所以结果是16(也可以这么理解:*a等价于*(a+0)等价于a[0],表示的是第一行一维数组的数组名,数组名单独放在sizeof中,访问的是整个数组,所以结果是16)

16 //看到这个代码,想必很多小伙伴都认为这个包会报错的,毕竟,这不是越界访问了吗?但是,我们说过,sizeof在计算变量大小的时候,是通过类型求推导的,所以他并不会真正去访问内存,既然没有越界访问内存,那不就不会报错了,而在根据类型推导sizeof(a[3])的大小时,会将a[0]和a[3]是同等类型的,所以结果自然是16了

指针运算试题

  • 题目1:下面代码的运行结果

    #include <stdio.h>
    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是数组名,一般情况下表示首元素的地址,a+1表示跳过一个整型的大小,所以a+1指向第二个元素,则*(a+1)得到的是2;&a取出的是整个数组的的地址,所以&a+1会跳过整个整形数组,如图,将这个地址转换成整型指针的地址以后赋给ptr,ptr指向如图所示,ptr-1会向前走过一个整形大小的步长,如图所示,所以*(ptr-1)得到的是5

  • 题目2:下面代码的运行结果

    //在X86环境下
    //假设结构体的大小是20个字节
    //程序输出的结果是啥?
    struct Test
    {
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
    }p = (struct Test)0x100000;

    int main()
    {
    printf("%p\n", p + 0x1);
    printf("%p\n", (unsigned long)p + 0x1);
    printf("%p\n", (unsigned int*)p + 0x1);
    return 0;
    }

解析:在定义结构体的同时创建了结构体指针变量p,并将0X100000强制类型转换成结构体指针类型以后赋值给p,p+0x1也就是p+1,p所指向的变量为结构体类型,所以p+1需要跳过20个字节,所以p+1=0x100020,等等,这对吗?注意咯,这里是十六进制的形式,逢十六进一,所以结果应该是0x100014,而在X86环境下,会将地址的8位都打印出来,且不会省略掉前面的0,所以结果是00100014.

将p强制类型转换成unsigned long类型以后,里面的值就是一个0x100000,加上一后就是普通的整型相加,所以结果是0X100001,在X86环境下的打印结果是0X100001

将p强制类型转换成unsigned int*类型以后,对指针+1会跳过4个字节,所以结果就是0X100004,在X86环境下,结果就是00100004

  • 题目3:下面代码的运行结果

    #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;
    }

解析:我们先要搞清楚数组里面放了啥,数组里面放的是0,1,2,3,4,5嘛?看着好像是的。注意啦兄弟们,数组里面用括号括起来的是逗号表达式哦,所以数组里面的元素应该是{1,3,5,0,0,0}。

弄清这个以后,我们再来看指针。p是一个整形指针,a[0]是第一行那个一维数组的数组名,一般情况下,他表示首元素的地址,也就是说p里面存放的是第一行一维数组首元素的地址,p[0]等价于*(p+0),p+0还是表示第一行一维数组首元素的地址,对它进行解引用,访问的就是第一行一维数组的首元素,也就是1,所以打印结果是1.

  • 题目4:下面代码的运行结果

    #include <stdio.h>
    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表示的是二维数组的名称,一般情况下,数组名表示首元素的地址,则aa表示第一行一维数组的地址,aa+1表示跳过整个一维数组,则aa+1指向第二行整个一维数组;&aa取出的是整个二位数组的地址,&aa+1跳过的是整个二维数组,由此,可得图中各指针指向的由来。

ptr1和ptr2都是整型指针,+1时跳过一个整形元素的步长,-1时向前走一个整形元素的步长,所以得到相应指向,对指针解引用以后可以得到打印结果是:10,5

  • 题目5:下面代码的运行结果

    //假设环境是x86环境,程序输出的结果是啥?
    #include <stdio.h>

    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;

    }

图示解析:

  • 题目6:下面代码的运行结果

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

图示解析:

  • 题目7:下面代码的运行结果

    #include <stdio.h>
    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;

    }

图示解析:

运行结果: