看之前必须得掌握有一定指针的知识,不然会看不懂,如果有不懂的可以看我博客 指针1,指针2,指针3 这三个讲了指针全部的基础知识超级详细,这篇只要是讲一些指针练习题也是非常详细
1. sizeof和strlen的对⽐
1. 基本定义和用途
sizeof
:sizeof
是一个运算符,不是函数。它用于计算数据类型或变量所占用的内存字节数。这个计算是在编译时完成的,与程序运行时的数据内容无关。strlen
:strlen
是一个标准库函数,定义在<string.h>
(C)或<cstring>
(C++)头文件中。它用于计算以'\0'
结尾的字符串的实际长度,即字符串中字符的个数(不包括字符串结束符'\0'
),这个计算是在程序运行时进行的。
2. 语法和使用示例
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "Hello";
// 使用 sizeof 计算数组占用的内存字节数
size_t size = sizeof(str);
// 使用 strlen 计算字符串的实际长度
size_t length = strlen(str);
printf("sizeof(str) = %zu\n", size);
printf("strlen(str) = %zu\n", length);
return 0;
}
输出结果:
sizeof(str) = 6
strlen(str) = 5
在这个例子中,sizeof(str)
返回 6,因为数组 str
包含 5 个字符 'H'
、'e'
、'l'
、'l'
、'o'
以及一个字符串结束符 '\0'
,每个字符占 1 字节,所以总共 6 字节。而 strlen(str)
返回 5,因为它只计算字符串中实际的字符个数,不包括 '\0'
。
3. 对不同数据类型的处理
sizeof
- 对于基本数据类型,如
int
、char
、double
等,sizeof
返回该数据类型在当前系统中占用的字节数。例如,在 32 位系统中,sizeof(int)
通常返回 4,sizeof(char)
返回 1。 - 对于数组,
sizeof
返回整个数组占用的内存字节数。 - 对于指针,
sizeof
返回指针变量本身占用的内存字节数,而不是指针所指向的对象的大小。例如,在 32 位系统中,所有指针类型的sizeof
结果通常都是 4 字节,在 64 位系统中通常是 8 字节。
- 对于基本数据类型,如
#include <stdio.h>
int main()
{
int arr[10];
int *ptr = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr));
printf("sizeof(ptr) = %zu\n", sizeof(ptr));
return 0;
}
strlen
:strlen
只能用于以'\0'
结尾的字符串。如果传递给strlen
的参数不是一个有效的以'\0'
结尾的字符串,会导致未定义行为,因为strlen
会一直向后查找'\0'
,直到找到为止。
4. 性能差异
sizeof
:由于sizeof
是在编译时计算的,不会产生运行时开销,因此性能非常高。strlen
:strlen
需要在运行时遍历字符串,直到找到'\0'
为止,因此其时间复杂度为 ,其中 是字符串的长度。当处理长字符串时,strlen
的性能会受到一定影响。
5. 总结
- 如果需要知道数据类型或变量占用的内存大小,应该使用
sizeof
。 - 如果需要知道以
'\0'
结尾的字符串的实际长度,应该使用strlen
。
2. 数组和指针笔试题解析
通过上面对strlen和sizeof的了解,结合我之前发的指针1和指针2,指针3的知识,我们来做一些综合性的题目。
2.1 ⼀维数组
认真思考哦
#include<stdio.h>
int main()
{
int a[] = { 1,2,3,4 };
// 1. sizeof(a)
printf("%d\n", sizeof(a));
// 2. sizeof(a + 0)
printf("%d\n", sizeof(a + 0));
// 3. sizeof(*a)
printf("%d\n", sizeof(*a));
// 4. sizeof(a + 1)
printf("%d\n", sizeof(a + 1));
// 5. sizeof(a[1])
printf("%d\n", sizeof(a[1]));
// 6. sizeof(&a)
printf("%d\n", sizeof(&a));
// 7. sizeof(*&a)
printf("%d\n", sizeof(*&a));
// 8. sizeof(&a + 1)
printf("%d\n", sizeof(&a + 1));
// 9. sizeof(&a[0])
printf("%d\n", sizeof(&a[0]));
// 10. sizeof(&a[0] + 1)
printf("%d\n", sizeof(&a[0] + 1));
return 0;
}
我们接下来来分析一下:
我们来回顾一下我之前提到过的:
数组名是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),数组名单独放在括号里。
2. &数组名
1. sizeof(a)
a
是一个包含 4 个int
类型元素的数组。sizeof
运算符作用于数组名时,返回整个数组占用的内存字节数。- 每个
int
类型占 4 个字节,数组有 4 个元素,所以sizeof(a)
的结果是4 * 4 = 16
字节。
2. sizeof(a + 0)
- 根据数组名的特性,当数组名出现在表达式中(除了作为
sizeof
或&
运算符的操作数),它会隐式转换为指向数组首元素的指针(int *类型)。 a + 0
等价于指向数组首元素的指针,指针在 32 位系统下占 4 个字节,在 64 位系统下占 8 个字节。所以sizeof(a + 0)
的结果是指针的大小。
3. sizeof(*a)
a
隐式转换为指向数组首元素的指针,*a
表示解引用该指针,得到数组的首元素,即a[0]
。a[0]
是int
类型,int
类型占 4 个字节,所以sizeof(*a)
的结果是 4 字节。
4. sizeof(a + 1)
a
隐式转换为指向数组首元素的指针,a + 1
是指向数组第二个元素的指针(int *类型)。- 指针的大小在 32 位系统下是 4 字节,在 64 位系统下是 8 字节,所以
sizeof(a + 1)
的结果是指针的大小。
5. sizeof(a[1])
a[1]
是数组的第二个元素,类型为int
。int
类型占 4 个字节,所以sizeof(a[1])
的结果是 4 字节。
6. sizeof(&a)
&a
是一个指向整个数组的指针(int (*) [4]类型),虽然它和指向数组首元素的指针在数值上可能相同,但类型不同。- 指针的大小在 32 位系统下是 4 字节,在 64 位系统下是 8 字节,所以
sizeof(&a)
的结果是指针的大小。
7. sizeof(*&a)
&a
是指向整个数组的指针,*&a
等价于a
,也就是整个数组。- 整个数组包含 4 个
int
类型元素,每个int
占 4 个字节,所以sizeof(*&a)
的结果是4 * 4 = 16
字节。
8. sizeof(&a + 1)
&a
是指向整个数组的指针(int (*) [4]类型),&a + 1
是指向下一个与a
同类型数组的指针。- 指针的大小在 32 位系统下是 4 字节,在 64 位系统下是 8 字节,所以
sizeof(&a + 1)
的结果是指针的大小。
9. sizeof(&a[0])
&a[0]
是指向数组首元素的指针(int *类型)。- 指针的大小在 32 位系统下是 4 字节,在 64 位系统下是 8 字节,所以
sizeof(&a[0])
的结果是指针的大小。
10. sizeof(&a[0] + 1)
&a[0]
是指向数组首元素的指针(int *类型),&a[0] + 1
是指向数组第二个元素的指针(int *类型)。- 指针的大小在 32 位系统下是 4 字节,在 64 位系统下是 8 字节,所以
sizeof(&a[0] + 1)
的结果是指针的大小。
2.2 字符数组
接下来我们来几组字符数组来练习练习一下吧
代码1:
#include <stdio.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
// 1. sizeof(arr)
printf("%d\n", sizeof(arr));
// 2. sizeof(arr + 0)
printf("%d\n", sizeof(arr + 0));
// 3. sizeof(*arr)
printf("%d\n", sizeof(*arr));
// 4. sizeof(arr[1])
printf("%d\n", sizeof(arr[1]));
// 5. sizeof(&arr)
printf("%d\n", sizeof(&arr));
// 6. sizeof(&arr + 1)
printf("%d\n", sizeof(&arr + 1));
// 7. sizeof(&arr[0] + 1)
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
我们来回顾一下我之前提到过的:
数组名是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),数组名单独放在括号里。
2. &数组名
1. sizeof(arr)
arr
是一个字符数组,当sizeof
运算符作用于数组名时,它返回整个数组所占用的内存字节数。- 该数组包含 6 个
char
类型的元素,每个char
类型通常占用 1 个字节,所以sizeof(arr)
的结果是6 * 1 = 6
。
2. sizeof(arr + 0)
- 当数组名
arr
出现在表达式中(除了作为sizeof
或&
运算符的操作数)时,它会隐式转换为指向数组首元素的指针(char*类型),即arr
退化为char *
类型的指针。 arr + 0
仍然是指向数组首元素的指针,sizeof
计算的是指针的大小。在 32 位系统中,指针大小通常为 4 字节;在 64 位系统中,指针大小通常为 8 字节。
3. sizeof(*arr)
- 由于
arr
退化为指向数组首元素的指针,*arr
表示对该指针进行解引用,得到数组的首元素arr[0]
。 arr[0]
是char
类型,char
类型占用 1 个字节,所以sizeof(*arr)
的结果是 1。
4. sizeof(arr[1])
arr[1]
是数组的第二个元素,其类型为char
。- 因此,
sizeof(arr[1])
的结果同样是 1 字节。
5. sizeof(&arr)
&arr
是一个指向整个数组的指针,其类型为char (*)[6]
,表示指向包含 6 个char
元素的数组的指针。- 但无论指针指向何种类型的对象,
sizeof
计算的都是指针本身的大小。所以在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
6. sizeof(&arr + 1)
&arr
是指向整个数组的指针(char (*)[6]
),&arr + 1
表示对该指针进行偏移,使其指向下一个与arr
同类型的数组的起始位置。- 它仍然是一个指针,所以
sizeof(&arr + 1)
的结果和sizeof(&arr)
一样,在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
7. sizeof(&arr[0] + 1)
&arr[0]
是数组首元素的地址,类型为char *
。&arr[0] + 1
是指向数组第二个元素的指针,同样是char *
类型。- 所以
sizeof(&arr[0] + 1)
计算的是指针的大小,在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
代码2:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
// 1. strlen(arr)
printf("%d\n", strlen(arr));
// 2. strlen(arr + 0)
printf("%d\n", strlen(arr + 0));
// 3. strlen(*arr)
printf("%d\n", strlen(*arr));
// 4. strlen(arr[1])
printf("%d\n", strlen(arr[1]));
// 5. strlen(&arr)
printf("%d\n", strlen(&arr));
// 6. strlen(&arr + 1)
printf("%d\n", strlen(&arr + 1));
// 7. strlen(&arr[0] + 1)
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
我们来回顾一下我之前提到过的:
数组名是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),数组名单独放在括号里。
2. &数组名
1. strlen(arr)
arr
是一个字符数组,但是该数组初始化时没有包含字符串结束符'\0'
。strlen
函数会从arr
指向(char*类型)的地址开始,逐个字符向后查找'\0'
,直到找到为止。由于arr
中没有'\0'
,strlen
会继续访问数组后面的内存,这会导致未定义行为,其输出结果是不确定的,可能是一个随机值,甚至可能引发程序崩溃。
2. strlen(arr + 0)
arr + 0
等同于arr
,因为数组名arr
在表达式中(除了作为sizeof
或&
运算符的操作数)会隐式转换为指向数组首元素的指针(char*类型),加上 0 后仍然指向数组首元素。- 同样,由于数组中没有
'\0'
,这也会导致未定义行为,输出结果是一个随机值。
3. strlen(*arr)
*arr
对arr
进行解引用,得到数组的首元素'a'
,其 ASCII 值为 97。strlen
函数期望的参数是一个指向以'\0'
结尾的字符串的指针(char *
类型),而这里传递的是一个char
类型的值(97)。将这个值当作指针来处理是错误的,会导致未定义行为,97作为地址传给给了strlen,可能会引发程序崩溃。
4. strlen(arr[1])
arr[1]
是数组的第二个元素'b'
,其 ASCII 值为 98。- 与
strlen(*arr)
类似,strlen
函数期望的是一个指针,而这里传递的是一个char
类型的值,会导致未定义行为,98作为地址传给给了strlen,可能会引发程序崩溃。
5. strlen(&arr)
&arr
是一个指向整个数组的指针,其类型为char (*)[6]
。strlen
函数期望的参数是char *
类型的指针,这里传递的指针类型不匹配。将char (*)[6]
类型的指针当作char *
类型的指针来使用会导致未定义行为,可能会引发程序崩溃,也可能是一个随机值。
6. strlen(&arr + 1)
&arr
是指向整个数组的指针char (*)[6]
,&arr + 1
会跳过整个数组,指向下一个与arr
同类型的数组的起始位置的地址。- 同样,
strlen
函数期望的是char *
类型的指针,这里指针类型不匹配,并且该位置的内存内容是不确定的,会导致未定义行为,可能会引发程序崩溃,也可能是一个随机值。
7. strlen(&arr[0] + 1)
&arr[0]
是数组首元素的地址,&arr[0] + 1
是指向数组第二个元素的指针(char*类型)。- 由于数组中没有
'\0'
,strlen
会从第二个元素开始向后查找'\0'
,但一直找不到,会继续访问数组后面的内存,这会导致未定义行为,输出结果不确定,也可能是一个随机值。
代码3
#include <stdio.h>
int main()
{
char arr[] = "abcdef";
// 1. sizeof(arr)
printf("%d\n", sizeof(arr));
// 2. sizeof(arr + 0)
printf("%d\n", sizeof(arr + 0));
// 3. sizeof(*arr)
printf("%d\n", sizeof(*arr));
// 4. sizeof(arr[1])
printf("%d\n", sizeof(arr[1]));
// 5. sizeof(&arr)
printf("%d\n", sizeof(&arr));
// 6. sizeof(&arr + 1)
printf("%d\n", sizeof(&arr + 1));
// 7. sizeof(&arr[0] + 1)
printf("%d\n", sizeof(&arr[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
我们来回顾一下我之前提到过的:
数组名是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),数组名单独放在括号里。
2. &数组名
1. sizeof(arr)
arr
是一个字符数组,使用字符串字面量"abcdef"
进行初始化。字符串字面量在 C 语言中会自动在末尾添加一个字符串结束符'\0'
。- 所以数组
arr
实际上包含了 7 个字符:'a'
,'b'
,'c'
,'d'
,'e'
,'f'
,'\0'
。 sizeof
运算符作用于数组名时,返回整个数组所占用的内存字节数。每个char
类型占 1 个字节,因此sizeof(arr)
的结果是 7。
2. sizeof(arr + 0)
- 当数组名
arr
出现在表达式中(除了作为sizeof
或&
运算符的操作数)时,它会隐式转换为指向数组首元素的指针,即arr
退化为char *
类型的指针。 arr + 0
仍然指向数组的首元素,本质上还是一个指针。sizeof
计算的是指针的大小。在 32 位系统中,指针大小通常为 4 字节;在 64 位系统中,指针大小通常为 8 字节。
3. sizeof(*arr)
- 由于
arr
退化为指向数组首元素的指针,*arr
表示对该指针进行解引用,得到数组的首元素arr[0]
,也就是字符'a'
。 arr[0]
是char
类型,char
类型占用 1 个字节,所以sizeof(*arr)
的结果是 1。
4. sizeof(arr[1])
arr[1]
是数组的第二个元素,其类型为char
。- 因此,
sizeof(arr[1])
的结果同样是 1 字节。
5. sizeof(&arr)
&arr
是一个指向整个数组的指针,其类型为char (*)[7]
,表示指向包含 7 个char
元素的数组的指针。- 但无论指针指向何种类型的对象,
sizeof
计算的都是指针本身的大小。所以在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
6. sizeof(&arr + 1)
&arr
是指向整个数组的指针(char (*)[7]
),&arr + 1
表示对该指针进行偏移,使其指向下一个与arr
同类型的数组的起始位置。- 它仍然是一个指针,所以
sizeof(&arr + 1)
的结果和sizeof(&arr)
一样,在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
7. sizeof(&arr[0] + 1)
&arr[0]
是数组首元素的地址,类型为char *
。&arr[0] + 1
是指向数组第二个元素的指针,同样是char *
类型。- 所以
sizeof(&arr[0] + 1)
计算的是指针的大小,在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。
代码4
#include <stdio.h>
#include <string.h>
int main()
{
// 定义并初始化字符数组 arr
char arr[] = "abcdef";
// 1. strlen(arr)
printf("%d\n", strlen(arr));
// 2. strlen(arr + 0)
printf("%d\n", strlen(arr + 0));
// 3. strlen(*arr)
printf("%d\n", strlen(*arr));
// 4. strlen(arr[1])
printf("%d\n", strlen(arr[1]));
// 5. strlen(&arr)
printf("%d\n", strlen(&arr));
// 6. strlen(&arr + 1)
printf("%d\n", strlen(&arr + 1));
// 7. strlen(&arr[0] + 1)
printf("%d\n", strlen(&arr[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
我们来回顾一下我之前提到过的:
数组名是数组首元素的地址
但是有2个例外:
1. sizeof(数组名),数组名单独放在括号里。
2. &数组名
1 printf(strlen(arr));
arr
作为数组名,在strlen
函数调用时,它会隐式转换为指向数组首元素的指针(这是数组在表达式中的一种退化行为),也就是指向字符'a'
的指针(char*)。strlen
函数会从该指针指向的位置开始,逐个字符向后查找,直到遇到'\0'
为止。从'a'
开始,依次是'b'
、'c'
、'd'
、'e'
、'f'
,再到'\0'
,总共 6 个有效字符(不包含'\0'
)。所以strlen(arr)
的返回值是 6,程序会输出6
。
2 printf(strlen(arr + 0));
arr + 0
本质上和arr
是一样的,因为指针(char*)加上 0 并不会改变其指向。所以arr + 0
依然指向数组的首元素'a'
。- 因此,
strlen(arr + 0)
的计算过程和strlen(arr)
相同,返回值也是 6,程序会输出6
。
3 printf(strlen(*arr));
*arr
是对arr
这个指针进行解引用操作,得到的是数组的首元素'a'
。'a'
的 ASCII 码值是 97。- 但
strlen
函数期望的参数是一个指向以'\0'
结尾的字符串的指针(char *
类型)。这里把一个char
类型的值(97)当作指针(地址)传递给strlen
,这会导致未定义行为。程序可能会尝试从内存地址 97 开始查找'\0'
,这可能会引发段错误(因为程序可能没有访问该内存地址的权限),程序崩溃或者输出一个无意义的结果。
4 printf(strlen(arr[1]));
arr[1]
表示数组的第二个元素,即字符'b'
,其 ASCII 码值为 98。- 同样,
strlen
函数需要的是指针(char*)类型的指针参数,这里传递的是char
类型的值,98作为地址传给给了strlen会导致未定义行为,程序可能崩溃或者给出错误的输出。
5 printf(strlen(&arr));
&arr
是一个指向整个数组的指针,其类型为char (*)[7]
(因为数组arr
有 7 个元素)。- 而
strlen
函数要求的参数是char *
类型的指针。将char (*)[7]
类型的指针当作char *
类型的指针使用是错误的但也可以算出是6,也会导致未定义行为,程序可能无法正常运行。
6 printf(strlen(&arr + 1));
&arr
指向整个数组(char (*)[7]
),&arr + 1
会使指针跳过整个数组arr
,指向下一个和arr
同类型数组应该存放的起始位置。- 这个位置的内存内容是不确定的,而且传递的指针类型与
strlen
函数期望的char *
类型不匹配,会引发未定义行为,程序可能出现异常。
7 printf(strlen(&arr[0] + 1));
&arr[0]
是数组首元素的地址,&arr[0] + 1
是指向数组第二个元素'b'
的指针(char*)。strlen
函数从这个指针指向的位置开始查找'\0'
。从'b'
开始,依次是'c'
、'd'
、'e'
、'f'
,再到'\0'
,一共有 5 个有效字符。所以strlen(&arr[0] + 1)
的返回值是 5,程序会输出5
。
代码5
#include <stdio.h>
int main()
{
char* p = "abcdef";
// 1. sizeof(p)
printf("%d\n", sizeof(p));
// 2. sizeof(p + 1)
printf("%d\n", sizeof(p + 1));
// 3. sizeof(*p)
printf("%d\n", sizeof(*p));
// 4. sizeof(p[0])
printf("%d\n", sizeof(p[0]));
// 5. sizeof(&p)
printf("%d\n", sizeof(&p));
// 6. sizeof(&p + 1)
printf("%d\n", sizeof(&p + 1));
// 7. sizeof(&p[0] + 1)
printf("%d\n", sizeof(&p[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
怕大家搞混这边补充一些指针知识:
1.
p
的类型及含义在代码
char* p = "abcdef";
中,p
是一个字符指针,其类型为char *
。它指向字符串字面量"abcdef"
的首字符'a'
的地址。2.
p[0]
的含义在 C 语言里,对于指针
p
,p[i]
等价于*(p + i)
。当i
为 0 时,p[0]
等价于*(p + 0)
,也就是*p
。所以p[0]
表示的是指针p
所指向的字符,在这里就是字符'a'
,其类型为char
。3.
&p[0]
的类型推导
&
是取地址运算符,&p[0]
表示取p[0]
的地址。由于p[0]
是char
类型的字符'a'
,那么&p[0]
就是指向这个char
类型字符的指针,其类型为char *
。4.&p的含义
&
是取地址运算符,当它作用于指针变量p
时,&p
得到的是指针变量p
自身在内存中的存储地址。由于p
本身是char *
类型(指向char
的指针),那么指向p
的指针就需要能够存储p
的地址,这个指针指向的对象类型是char *
,所以&p
的类型是指向char *
类型的指针,即char **
。从本质上来说,
&p[0]
和p
指向的是同一个地址,即字符串"abcdef"
的首字符地址,只是它们在概念上稍有不同:p
是一个指针变量,而&p[0]
是通过对数组(这里可以把指针p
当作指向字符串数组首元素的指针)首元素取地址得到的指针。综上所述,p
是char *
类型,p[0]
是char
类型,&p[0]
是char *
类型,&p
是char **
类型。。
1 sizeof(p)
p
是一个(char *类型)指针变量(不是数组名),它存储的是字符串"abcdef"
的首地址('a')。sizeof
操作符用于计算指针变量本身所占用的内存大小。在 32 位系统中,指针通常占用 4 个字节;在 64 位系统中,指针通常占用 8 个字节。所以sizeof(p)
的结果是 4(32 位系统)或 8(64 位系统)。
2 sizeof(p + 1)
p + 1
是一个指针运算,它会让指针p
向后偏移一个char
类型的大小,从而指向字符串中的第二个字符'b'
。- 但无论指针指向何处,
sizeof
计算的都是指针本身的大小。所以sizeof(p + 1)
的结果和sizeof(p)
一样,是 4(32 位系统)或 8(64 位系统)。
3 sizeof(*p)
*p
是对指针p
进行解引用操作,得到的是指针p
所指向的字符,也就是字符串的首字符'a'
。'a'
是char
类型,char
类型在 C 语言中通常占用 1 个字节。所以sizeof(*p)
的结果是 1。
4 sizeof(p[0])
p[0]
(char
类型)等价于*(p + 0)
,也就是指针p
所指向的字符,同样是'a'
。- 因此,
sizeof(p[0])
的结果和sizeof(*p)
相同,为 1。
5 sizeof(&p)
&p
是取指针变量p
本身的地址,它是一个指向指针的指针(类型为char **
)。- 不管指针指向的是什么类型,
sizeof
计算的都是指针本身的大小。所以sizeof(&p)
的结果是 4(32 位系统)或 8(64 位系统)。
6 sizeof(&p + 1)
&p
是指向指针p
的指针,&p + 1
会让这个指针向后偏移一个char **
类型的大小,指向下一个char **
类型的位置。- 但
sizeof
计算的依然是指针本身的大小,所以sizeof(&p + 1)
的结果和sizeof(&p)
一样,是 4(32 位系统)或 8(64 位系统)。
7 sizeof(&p[0] + 1)
p[0]
(char
类型) 是字符串的首字符'a'
,&p[0]
是首字符'a'
的地址,也就是指针p
所指向的地址。&p[0] + 1
会让这个指针向后偏移一个char
类型的大小,指向字符串中的第二个字符'b'
。- 它仍然是一个指针(
&p[0]
char *类型),所以sizeof(&p[0] + 1)
的结果和sizeof(p)
相同,是 4(32 位系统)或 8(64 位系统)。
代码6
#include<stdio.h>
#include<string.h>
int main()
{
char* p = "abcdef";
//1.strlen(p)
printf("%d\n", strlen(p));
//2.strlen(p + 1)
printf("%d\n", strlen(p + 1));
//3.strlen(*p)
printf("%d\n", strlen(*p));
//4.strlen(p[0])
printf("%d\n", strlen(p[0]));
//strlen(&p)
printf("%d\n", strlen(&p));
//6.strlen(&p + 1)
printf("%d\n", strlen(&p + 1));
//7.strlen(&p[0] + 1)
printf("%d\n", strlen(&p[0] + 1));
return 0;
}
接下来我们来详细的分析一下吧:
-
strlen(p)
p(char*类型)
是一个指向字符串常量"abcdef"
的指针,strlen(p)
会从p
所指向的字符开始,逐个字符计数,直到遇到字符串结束符'\0'
为止。- 字符串
"abcdef"
的长度是 6,所以输出结果为6
。
-
strlen(p + 1)
p + 1
(char*类型)
指向字符串"abcdef"
的第二个字符'b'
。strlen(p + 1)
会从'b'
开始计数,直到遇到字符串结束符'\0'
,所以输出结果为5
。
-
strlen(*p)
*p
(char类型)
是解引用操作,它得到的是p
所指向的第一个字符'a'
,其 ASCII 码值为 97。strlen
函数的参数应该是一个指向字符串的指针,而这里传递的是一个字符(实际上是一个整数),这会导致未定义行为,程序可能会崩溃或输出错误的结果。
-
strlen(p[0])
p[0]
(char类型)
等价于*p
,同样得到的是字符'a'
。- 传递字符给
strlen
函数会导致未定义行为,程序可能会崩溃或输出错误的结果。
-
strlen(&p)
&p
(char**类型)
是指针p
的地址,它指向的并不是一个以'\0'
结尾的字符串。- 传递指针
p
的地址给strlen
函数会导致未定义行为,程序可能会崩溃或随机值的结果。
-
strlen(&p + 1)
&p + 1
(char**类型)
是指针p
的地址向后偏移一个char*
类型的大小。- 它指向的同样不是一个以
'\0'
结尾的字符串,传递该地址给strlen
函数会导致未定义行为,程序可能会崩溃或随机值的结果。
-
strlen(&p[0] + 1)
&p[0](char*类型)
等价于p
,指向字符串"abcdef"
的第一个字符'a'
。&p[0] + 1
指向字符串的第二个字符'b'
。strlen(&p[0] + 1)
会从'b'
开始计数,直到遇到字符串结束符'\0'
,所以输出结果为5
。
2.3 ⼆维数组
#include<stdio.h>
int main()
{
// 定义一个 3 行 4 列的二维数组 a,并初始化为全 0
int a[3][4] = { 0 };
// 1.sizeof(a)
printf("%d\n", sizeof(a));
// 2.sizeof(a[0][0])
printf("%d\n", sizeof(a[0][0]));
// 3.sizeof(a[0])
printf("%d\n", sizeof(a[0]));
// 4.sizeof(a[0] + 1)
printf("%d\n", sizeof(a[0] + 1));
// 5.sizeof(*(a[0] + 1))
printf("%d\n", sizeof(*(a[0] + 1)));
// 6.sizeof(a + 1)
printf("%d\n", sizeof(a + 1));
// 7.sizeof(*(a + 1))
printf("%d\n", sizeof(*(a + 1)));
// 8.sizeof(&a[0] + 1)
printf("%d\n", sizeof(&a[0] + 1));
// 9.sizeof(*(&a[0] + 1))
printf("%d\n", sizeof(*(&a[0] + 1)));
// 10.sizeof(*a)
printf("%d\n", sizeof(*a));
// 11.sizeof(a[3])
printf("%d\n", sizeof(a[3]));
return 0;
}
接下来我们来详细的分析一下吧:
-
sizeof(a)
a
是一个3
行4
列的二维数组,每个元素是int
类型。- 在大多数系统中,
int
类型占4
个字节,所以整个数组的元素个数为3 * 4 = 12
个。 - 因此,
sizeof(a)
的结果是3 * 4 * sizeof(int) = 48
字节。
-
sizeof(a[0][0])
a[0][0]
表示数组a
的第一个元素,它是int
类型。- 通常
int
类型占4
个字节,所以sizeof(a[0][0])
的结果是4
字节。
-
sizeof(a[0])
a[0]
(int *类型) 表示数组a
的第一行,它可以看作是一个包含4
个int
元素的一维数组。- 所以
sizeof(a[0])
的结果是4 * sizeof(int) = 16
字节。
-
sizeof(a[0] + 1)
a[0]
是第一行数组的首地址,a[0] + 1
是第一行第二个元素的地址。- 地址本质上是一个指针,在大多数系统中,指针的大小是固定的,通常为
4
或8
字节(取决于系统是 32 位还是 64 位)。 - 所以
sizeof(a[0] + 1)
的结果是指针的大小,一般为4
或8
字节。
-
sizeof(*(a[0] + 1))
*(a[0] + 1)
表示对第一行第二个元素的地址进行解引用,得到的是第一行第二个元素。- 该元素是
int
类型,所以sizeof(*(a[0] + 1))
的结果是4
字节。
-
sizeof(a + 1)
a
(int (*)[4]类型)是二维数组名,a + 1
表示跳过第一行,指向第二行的首地址。- 它是一个指针,所以
sizeof(a + 1)
的结果是指针的大小,一般为4
或8
字节。
-
sizeof(*(a + 1))
*(a + 1)
表示对指向第二行的指针进行解引用,得到的是第二行数组。- 第二行数组包含
4
个int
元素,所以sizeof(*(a + 1))
的结果是4 * sizeof(int) = 16
字节。
-
sizeof(&a[0] + 1)
&a[0]
(int (*)[4]类型) 是第一行数组的地址,&a[0] + 1
表示跳过第一行,指向第二行的地址。- 它是一个指针,所以
sizeof(&a[0] + 1)
的结果是指针的大小,一般为4
或8
字节。
-
sizeof(*(&a[0] + 1))
*(&a[0] + 1)
表示对指向第二行的地址进行解引用,得到的是第二行数组。- 所以
sizeof(*(&a[0] + 1))
的结果是4 * sizeof(int) = 16
字节。
-
sizeof(*a)
*a
(int *类型) 等价于a[0]
,表示第一行数组。- 所以
sizeof(*a)
的结果是4 * sizeof(int) = 16
字节。
-
sizeof(a[3])
- 虽然数组
a
只有3
行(索引从0
到2
),a[3]
属于越界访问。 - 但
sizeof
是在编译时计算大小,它不会真正访问数组元素,a[3]
被看作是一个包含4
个int
元素的一维数组。 - 所以
sizeof(a[3])
的结果是4 * sizeof(int) = 16
字节。
- 虽然数组
3. 指针运算笔试题解析
3.1 题⽬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
表示整个数组a
的地址,它的类型是int (*)[5]
,即指向包含 5 个整型元素的数组的指针。&a + 1
:由于&a
是指向整个数组的指针,&a + 1
会跳过整个数组的内存空间。在这个例子中,数组a
包含 5 个整型元素,每个整型元素通常占 4 个字节(取决于系统),所以&a + 1
会跳过5 * 4 = 20
个字节,指向数组a
之后的内存地址。(int *)
:这是一个强制类型转换操作,将&a + 1
的类型从int (*)[5]
转换为int *
,这样ptr
就变成了一个指向整型的指针。
printf
函数输出
*(a + 1)
:数组名a
代表数组首元素的地址,a + 1
表示数组首元素地址向后偏移一个整型元素的位置,即指向数组的第二个元素。*(a + 1)
是对该地址进行解引用操作,得到该地址存储的值,也就是数组的第二个元素 2。*(ptr - 1)
:ptr
指向数组a
之后的内存地址,ptr - 1
表示将指针ptr
向前偏移一个整型元素的位置,即指向数组a
的最后一个元素。*(ptr - 1)
是对该地址进行解引用操作,得到该地址存储的值,也就是数组的最后一个元素 5。
3.2 题⽬2
/在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
#include<stdio.h>
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;
}
详细讲解:
- 头文件:
#include <stdio.h>
引入标准输入输出库,以便后续使用printf
函数进行输出。 - 结构体定义:定义了
struct Test
结构体,它包含了不同类型的成员变量。 - 指针初始化:声明了一个指向
struct Test
类型的指针p
,并将其初始化为地址0x100000
。这里通过强制类型转换(struct Test*)
把0x100000
转换为struct Test*
类型。
1 printf("%p\n", p + 0x1);
- 指针运算规则:在 C 语言里,当对指针进行加减运算时,偏移量是根据指针所指向的数据类型的大小来计算的。
p
是指向struct Test
类型的指针,已知struct Test
结构体大小为 20 字节。 - 具体计算:
p + 0x1
表示将指针p
向后偏移 1 个struct Test
结构体的大小,也就是偏移 20 字节。因为十六进制中,20 字节对应的十六进制数是0x14
(十进制 20 转换为十六进制为0x14
)。所以p + 0x1
得到的地址是0x100000 + 0x14 = 0x100014
。 - 输出结果:这行代码会输出
0x100014
。
2 printf("%p\n", (unsigned long)p + 0x1);
- 类型转换与运算:首先将指针
p
强制转换为unsigned long
类型,此时p
就变成了一个无符号长整型数值。在进行(unsigned long)p + 0x1
运算时,只是简单地对这个无符号长整型数值进行加法操作,即把地址值0x100000
加上1
(整数加1)。 - 输出结果:所以这行代码会输出
0x100001
。
3 printf("%p\n", (unsigned int*)p + 0x1);
- 类型转换与指针运算:先把指针
p
强制转换为unsigned int*
类型,也就是指向无符号整型的指针。在 X86 环境下,无符号整型unsigned int
通常占用 4 个字节。当对unsigned int*
类型的指针进行加法运算时,偏移量是根据无符号整型的大小来计算的。 - 具体计算:
(unsigned int*)p + 0x1
表示将指针向后偏移 1 个无符号整型的大小,即偏移 4 个字节。所以得到的地址是0x100000 + 0x4 = 0x100004
。 - 输出结果:这行代码会输出
0x100004
。
3.3 题⽬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;
}
详细讲解:
- 这里定义了一个 3 行 2 列的二维数组
a
。但需要注意的是,初始化列表中使用的是逗号表达式(0, 1)
、(2, 3)
和(4, 5)
。 - 在 C 语言里,逗号表达式会从左到右依次计算每个表达式的值,并且整个逗号表达式的值是最后一个表达式的值。所以
(0, 1)
的值是 1,(2, 3)
的值是 3,(4, 5)
的值是 5。 - 因此,数组
a
实际的初始化情况是:
a[0][0] = 1;
a[0][1] = 3;
a[1][0] = 5;
a[1][1] = 未初始化的值(默认是随机值);
a[2][0] = 未初始化的值(默认是随机值);
a[2][1] = 未初始化的值(默认是随机值);
- 定义了一个整型指针
p
。 a[0]
是二维数组a
第一行的首地址,它的类型是int *
。将a[0]
赋值给p
后,p
就指向了数组a
的第一行的第一个元素。p[0]
等价于*(p + 0)
,也就是p
所指向的元素的值。由于p
指向数组a
的第一行的第一个元素,而该元素的值是 1,所以这行代码会输出 1。
3.4 题⽬4
//假设环境是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;
}
详细讲解:
- 计算
&p[4][2]
的地址:p
是指向包含 4 个整型元素的数组的指针,p[4]
表示p
向后移动 4 行,由于每行有 4 个整型元素,所以p
向后移动了4 * 4 = 16
个整型元素的位置。p[4][2]
表示在p[4]
这一行的基础上再向后移动 2 个整型元素的位置。因此,&p[4][2]
相对于p
所指向的起始地址偏移了16 + 2 = 18
个整型元素的位置。
- 计算
&a[4][2]
的地址:a
是一个 5 行 5 列的二维数组,a[4]
表示第 5 行(数组下标从 0 开始)的首地址,相对于a
的起始地址向后移动了4 * 5 = 20
个整型元素的位置。a[4][2]
表示在第 5 行的基础上再向后移动 2 个整型元素的位置。所以,&a[4][2]
相对于a
的起始地址偏移了20 + 2 = 22
个整型元素的位置。
- 计算地址差值:
&p[4][2] - &a[4][2]
表示两个地址之间相差的整型元素个数。这里&p[4][2]
相对于起始地址偏移了 18 个整型元素,&a[4][2]
相对于起始地址偏移了 22 个整型元素,所以它们的差值为18 - 22 = -4
。
- 输出结果:
%p
是用于输出指针地址的格式说明符,但在输出指针相减的结果时,它会将差值以十六进制的形式输出。由于差值是 -4,在计算机中以补码形式存储,%p不会对存储的二进制转换,所以存储的是什么就输出什么,对于 32 位系统,-4 的补码是0xFFFFFFFC(没有转换为原码,所以是一个非常大的数)
。%d
是用于输出十进制整数的格式说明符,会直接输出 -4。
3.5 题⽬5
#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;
}
详细讲解:
指针 ptr1
的定义与初始化
&aa
:这里的&aa
表示整个二维数组aa
的地址,其类型是int (*)[2][5]
,即指向一个包含2
行5
列整型元素数组的指针。&aa + 1
:因为&aa
是指向整个二维数组的指针,&aa + 1
会跳过整个数组aa
所占用的内存空间。在这个例子中,数组aa
包含2 * 5 = 10
个整型元素,每个整型元素通常占4
个字节(取决于系统),所以&aa + 1
会指向数组aa
之后的内存地址。(int *)
:这是一个强制类型转换操作,将&aa + 1
的类型从int (*)[2][5]
转换为int *
,这样ptr1
就变成了一个指向整型的指针,它指向数组aa
之后的第一个整型位置。
指针 ptr2
的定义与初始化
aa
:数组名aa
在大多数表达式中会隐式转换为指向其首元素的指针,对于二维数组aa
,它会转换为指向第一行的指针,类型为int (*)[5]
。aa + 1
:由于aa
是指向第一行的指针,aa + 1
会指向数组的第二行,其类型仍然是int (*)[5]
。*(aa + 1)
:对aa + 1
进行解引用操作,得到第二行数组的首地址,其类型是int *
,也就是指向第二行第一个元素的指针。- 这里的强制类型转换
(int *)
实际上是多余的,因为*(aa + 1)
本身就是int *
类型。ptr2
最终指向数组aa
第二行的第一个元素。
printf
函数输出
*(ptr1 - 1)
:ptr1
指向数组aa
之后的内存地址,ptr1 - 1
表示将指针ptr1
向前偏移一个整型元素的位置,即指向数组aa
的最后一个元素。*(ptr1 - 1)
是对该地址进行解引用操作,得到该地址存储的值,也就是10
。*(ptr2 - 1)
:ptr2
指向数组aa
第二行的第一个元素,ptr2 - 1
表示将指针ptr2
向前偏移一个整型元素的位置,即指向数组aa
第一行的最后一个元素。*(ptr2 - 1)
是对该地址进行解引用操作,得到该地址存储的值,也就是5
。
3.6 题⽬6
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
详细讲解:
- 这里定义了一个指针数组
a
。指针数组是指数组的每个元素都是指针类型。在这个例子中,数组a
的元素是char *
类型,也就是字符指针。 - 初始化列表中的
"work"
、"at"
、"alibaba"
是字符串常量。在 C 语言里,字符串常量会被存储在只读内存区域,并且在使用时会隐式转换为指向其首字符的指针。所以,a[0]
指向字符串"work"
的首字符'w'
,a[1]
指向字符串"at"
的首字符'a'
,a[2]
指向字符串"alibaba"
的首字符'a'
。 - 定义了一个二级指针
pa
,二级指针就是指向指针的指针。 - 数组名
a
在大多数表达式中会隐式转换为指向其首元素的指针。由于a
的元素是char *
类型,所以a
会转换为char **
类型的指针,指向a[0]
。因此,这里将a
赋值给pa
后,pa
就指向了指针数组a
的第一个元素a[0]
。 - 因为
pa
是char **
类型的指针,对其进行++
操作会让它指向下一个char *
类型的元素。也就是说,pa
原本指向a[0]
,执行pa++
后,pa
指向了a[1]
。 *pa
是对pa
进行解引用操作,由于pa
指向a[1]
,所以*pa
就相当于a[1]
,而a[1]
是指向字符串"at"
首字符的指针。%s
是printf
函数用于输出字符串的格式说明符,它会从给定的指针所指向的字符开始,依次输出字符,直到遇到字符串结束符'\0'
。所以,这里会输出字符串"at"
。
3.7 题⽬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;
}
详细讲解:
字符指针数组 c
的定义与初始化
- 定义了一个字符指针数组
c
,数组中的每个元素都是一个char *
类型的指针,分别指向字符串常量"ENTER"
、"NEW"
、"POINT"
和"FIRST"
的首字符。
二级字符指针数组 cp
的定义与初始化
- 定义了一个二级字符指针数组
cp
,数组中的元素是char **
类型的指针。 c+3
指向c
数组的第 4 个元素(下标从 0 开始),即指向"FIRST"
的指针;c+2
指向c
数组的第 3 个元素,即指向"POINT"
的指针;c+1
指向c
数组的第 2 个元素,即指向"NEW"
的指针;c
指向c
数组的第 1 个元素,即指向"ENTER"
的指针。
三级字符指针 cpp
的定义与初始化
- 定义了一个三级字符指针
cpp
,并将其初始化为指向二级字符指针数组cp
的首元素。
第一个 printf
语句
++cpp
:先将cpp
指针向后移动一个位置,使其指向cp
数组的第 2 个元素(原本指向cp[0]
,现在指向cp[1]
)。*++cpp
:对移动后的cpp
进行解引用,得到cp[1]
,即c+2
,它指向c
数组中"POINT"
的指针。**++cpp
:再对*++cpp
进行解引用,得到"POINT"
字符串的首地址。- 最终输出字符串
"POINT"
。
第二个 printf
语句
++cpp
:再次将cpp
指针向后移动一个位置,使其指向cp
数组的第 3 个元素(即cp[2]
)。*++cpp
:对移动后的cpp
进行解引用,得到cp[2]
,即c+1
,它指向c
数组中"NEW"
的指针。--*++cpp
:将*++cpp
指向的指针向前移动一个位置,即指向c
数组的第 1 个元素(c[0]
),也就是"ENTER"
的指针。*--*++cpp
:对--*++cpp
进行解引用,得到"ENTER"
字符串的首地址。*--*++cpp+3
:将"ENTER"
字符串的首地址向后移动 3 个位置,指向字符'E'
后面的第 3 个字符'E'
。- 最终输出从该位置开始的字符串
"ER"
。
第三个 printf
语句
cpp[-2]
:等价于*(cpp - 2)
,将cpp
指针向前移动 2 个位置,指向cp
数组的第 1 个元素(cp[0]
),即c+3
,它指向c
数组中"FIRST"
的指针。*cpp[-2]
:对cpp[-2]
进行解引用,得到"FIRST"
字符串的首地址。*cpp[-2]+3
:将"FIRST"
字符串的首地址向后移动 3 个位置,指向字符'R'
。- 最终输出从该位置开始的字符串
"ST"
。
第四个 printf
语句
cpp[-1]
:等价于*(cpp - 1)
,由于cpp
指向cp
数组的第 3 个元素,所以cpp - 1
指向cp
数组的第 2 个元素(即c + 2
)。cpp[-1][-1]
:等价于*(*(cpp - 1) - 1)
,*(cpp - 1)
得到c + 2
,*(cpp - 1) - 1
得到c + 1
,再解引用得到指向"NEW"
的指针。cpp[-1][-1] + 1
:将指向"NEW"
的指针向后移动 1 个位置,指向字符'E'
,从这个位置开始输出字符串,所以输出"EW"
。