一、字符指针
字符指针即指向字符类型的指针char*;
我们知道C语言中字符串有两种保存方式:数组保存和指针保存,这两种方式从本质上来说有着很大的区别。
如图所示:数组保存时,在栈上开辟空间,字符串可以被访问修改;指针保存时,字符串保存在常量区,只可被访问,不能修改。
看下面这个程序:
#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和str2代表的是数组首元素的地址,而str1和str2是两个不同的数组,所以首元素地址不相同。str3和str4是两个指针,都指向保存在字符常量区的同一个字符串,所以地址值相同。
二、数组指针和指针数组
(一)区分
初学时容易混淆,所以先区分这两个概念。
1.从语言描述上
根据汉语定语修饰词在前,主语在后的习惯,“数组指针”中的“数组”是定语,“指针”是主语,所以数组指针是指针,而对于指针,我们第一反应就是这个指针指向什么,显然,数组指针是指向数组的。“指针数组”刚好相反,所以指针数组是数组,且存放的内容是指针。
2.从书写形式
看操作符的优先级
由于 [ ] 的优先级比*高,所以p1先和 [ ] 结合,必然是数组;p2有圆括号,所以先和*结合,构成指针。
(二)指针数组
1.定义
指针数组是一个存放指针的数组。如:
int *p[10];//一个数组,存放的内容是整型指针
int **p2[10];//存放二级整型指针的数组
2.指针数组的使用
整型指针数组
在使用整型数组指针时,要注意,不能直接将整型变量赋值给指针数组,因为指针数组的元素类型时int*,而赋予的变量类型是int型,类型不匹配。
如图:定义了一个整型指针数组并初始化,编译以后会发现程序有告警。
字符指针数组
字符指针数组可以直接赋值:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<windows.h>
int main()
{
char* arr[] = {
"I","am ","gonna"," make"," the",\
" world"," a", "better"," place" };
printf("%s\n",arr[5]);//打印数组的第五个元素
system("pause");
return 0;
}
程序运行结果:
在这里,一个容易犯的错误就是打印输出时加上*解引用,导致程序崩溃:
arr[5]本质上是一个指向指向字符串"world"的首元素的指针,对其解引用得到的是字符’w’,而如果以s%的形式打印输入,便会引起程序崩溃。
补充:数组下标访问和指针访问
定义一个数组:
int num=0;
int arr[5][5]={
};
下面这两种方式访问的是同一个元素:
num=arr[3][3];
num=*(*(arr+3)+3);
数组访问的方式很容易理解,下面对指针访问的方式进行分析:
arr是数组名,在这里代表首元素地址,即arr[0]的地址,arr[0]的地址+3表示数组第四个元素的地址,即a[3],而C语言中,a[3]表示一维数组的数组名,根据前面的结论,数组名即首元素地址,所以a[3]表示a[3][0] 的地址,&a[3][0]+3即为a[3][3]的地址,再解引用便是a[3][3]。
(三)数组指针
1.定义
int(*p)[10];//因为[]优先级比*高,所以要加括号
需要注意的是,数组的类型不仅指元素类型,还包括数组元素的个数,所以数组指针的类型也包括数组元素的个数,这里p指针指向的是一有10个整型元素的数组。
2.数组名与&数组名的区别
定义一个指针数组arr,分给打印arr和&arr:
int*arr[10]={
0};
printf("%p\n ",arr);
printf("%p\n ",&arr);
打印结果相同,但是他们真的完全按一样吗?
通过下面这程序可以深刻理解数字名与&数组名的区别:
#include <stdio.h>
#include<Windows.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);
system("pause");
return 0;
}
运行结果:
可见,arr+1是首元素地址加1,加上其所指向内容的大小,即4个字节;而&arr+1加上的是40个字节,是整个数组的大小。
对程序稍作更改,数组第一个和第二个元素分别赋值为1和2,查看内存,得到下面的结果:
很明显,对arr解引用得到的是数组第一个元素的值,所以arr代表的是数组首元素的地址;而对&arr解引用得到的是一个数组,所以从&arr是整个数组的地址,从本质上来说,&arr是数组指针类型。
结论:数组名在&后面和单组在sizeof中出现时表示整个数组,其余所有情况都表示数组首元素的地址。
3.数组指针的使用
数组指针主要在函数传参时使用,用于接收二维数组。
三、数组和指针的传参问题
一维数组传参
一维数组作函数实参时,形参可以是一维数组或者指针。
定义一个一维数组,让其作实参:
int arr[10];
Sort(arr);//调用sort函数
参数arr的类型是一个指向整型的指针,形参可以是下面任意一个:
void Sort(int *arr);//整型指针
void Sort(int arr[]);//整型数组
二维和多维数组
多维数组名作为函数参数传递的方式本质和一维数组相同——传递一个指向数组第一个元素的指针。但是,区别就是,多维数组的每个元素本身是另外一个数组,编译器需要知道它的维数,以便于为函数形参的下标表达式求值。
二维数组做实参,形参可以是二维数组或一个数组指针,且这个数组指针必须是指向二维数组的元素类型的指针。如:
int arr[3][10];//定义一个二维数组
Sort(arr);//调用Sort函数,arr作形参
形参可以是以下类型:
void Sort(int (*arr)[10]);//定义一个指向10个整型变量的数组指针
void Sort(int arr[][10]);//定义一个数组(二维),其元素类型是有10个整型变量的一维数组
这里的关键是编译器必须知道第2个及以后各维的长度才能对各下标进行求值,因此形参必须声明这些维度的长度。
比如,下面的这一组不能作为形参接收二维数组:
void Sort(int **arr);//定义了一个二级指针,即指向整型指针的指针
void Sort(int *arr[10])//指针数组
这两个都和实参arr的类型不匹配,所以不能作形参。
多维数组传参和二维数组原理相同,只有第一个维度可以省略,二维及以上都不能省,用数组指针时,类型也必须是指向数组元素类型的指针。
一级指针传参
一级指针作实参,形参可以是一维数组或者一级指针。
二级指针传参
二级指针作传参,形参可以是二级指针、指针数组。
传参规则总结
- 当二级指针作为函数形参时,能作为函数实参的是二级指针,指针数组,一级指针的地址
- 当数组指针作为函数形参时,能作为函数实参的是二维数组,数组指针
- 当二维数组作为函数形参时,能作为函数实参的是二维数组,数组指针
- 当指针数组作为函数形参时,能作为函数实参的是指针数组,二级指针,一级指针的地址
对于二维数组和数组指针,有一个共同点,在定义时二维数组必须定义第二维的大小,而数组指针也必须指定其所指向的数组的大小,所以他们可以互相作为参数。
而对于指针数组,其数组名本质上是一个指向数组第一个元素的指针,即二级指针,类型匹配,所以可以作为形参接收二级指针,并被二级指针接收。
四、函数指针及其应用
(一)函数指针
定义
指向函数地址的指针
int fun();//函数声明
int (*pf)()=&fun//定义一个函数指针,指向fun函数
上面程序中,便是函数指针。定义函数指针时需注意优先级,变量名后面的()优先级比*高,所以要加()。
另外,函数名在使用时编译器会自动将其转换为函数指针,所以单目操作符&可以省略,即&fun和fun等价。函数名实际存放的是函数入口的地址。
使用
对函数指针定义并初始化以后,便可以使用,看下面三种方式:
int num=0;
num=fun(10);//1
num=(*pf)(10);//2
num=pf(10);//3
第一种:如上所述,函数名存放的是函数入口的地址,所以使用函数名调用函数的具体执行过程是,函数名fun首先被转换成一个函数指针,这个指针指向函数在内存中的存放位置,然后执行指针所指向位置的代码。
第二种:对pf进行解引用操作,指向函数名,在执行与第一种相同的步骤。
第三种:与第二种相同,操作符*在实际使用中可以被省略。
代码解析
- First
//代码1
(*(void (*)())0)();
结论:这是一个从0地址处开始的函数调用
解析:
这里需要注意的是,0是一个常数,可以被当作地址来看待,也可以被强制转换为其他类型的指针,比如函数指针,然后只需要从里向外层层分解就可以理解这个语句的含义。
- Second
//代码2
void (*signal(int , void(*)(int)))(int);
结论:这是一个返回类型是函数指针的函数声明,该函数的参数是一个int类型和一个函数指针
解析:
这里要理解的关键是signal函数的返回值类型是一个函数指针,可以把绿色方框内整体看作一个函数如:signal(),忽略其形参。而该函数的返回值是一个函数指针:void(*)int,便可以得到:void (*signal() ) (int),最后将函数的形参填入即可。
(二)函数指针数组
定义
把函数的地址存放到数组中,即为函数指针数组。如:
int (*arr[10])(int);
arr是一个函数指针数组,数组内存放的是指向返回值为int,形参也为int的函数指针。
应用:转移表
使用函数指针数组设计一个计算器,程序如下:
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>//exit函数头文件
void Menu()//菜单
{
printf("#####################################\n");
printf("###### 1.Add 2.Sub ######\n");
printf("###### 3.Mul 4.Div ######\n");
printf("###### 0.Exit ######\n");
printf("#####################################\n");
}
int MyExit(int x,int y)//退出函数
{
exit(0);//exit函数需要有参数,可以设为0
}
int Add(int x,int y)
{
return x + y;
}
int Sub(int x,int y)
{
return x - y;
}
int Mul(int x,int y)
{
return x * y;
}
int Div(int x,int y)
{
//除法要先判断被除数不能为0
if (y == 0) {
printf("Warning:Div zero!\n");
return -1;
}
return x / y;
}
int main()
{
int x = 0;
int y = 0;
int select = 0;//接收选项
int ret = 0;//运算结果
int (*p[])(int, int) = {
MyExit,Add,Sub,Mul,Div };//转移表
char* operators = "+-*/";//将符号保存在字符串中
while (1)
{
Menu();
printf("Please select:");
scanf_s("%d", &select);
if (select >= 1 && select <= 4) {
printf("Please enter the number:");
scanf_s("%d %d", &x, &y);
ret = p[select](x, y);//通过函数指针数组调用函数
printf("%d %c %d=%d \n", x, operators[select - 1], y, ret);//下标方式访问operators
}
else if (select == 0) {
printf("Bye-bye!\n");
p[0](0, 0);//0为占位符,没有实际意义
}
else
printf("Enter error!\n");
}
system("pause");
return 0;
}
这里之所以能使用函数指针数组,是因为四则运算的每个函数类型相似,都有两个类型相同的参数,返回值类型也相同。这样用函数指针数组调用的方式,减少了很多重复代码。
(三)指向函数指针数组的指针
指向函数指针数组的指针是一个指针,它指向的是一个数组,这个数组保存的是函数指针。
int fun(int x)
{
return x;
}
int (*pfun)(int)=&test;//函数指针,&可以不写
int (*pfun_arr[])(int);//函数指针数组
int (*(*p_pfun_arr)[])(int)=&(*pfun[])(int);//指向函数指针数组的指针
▲(四)回调函数
1. 概念
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
2.应用——qsort函数
qsort是C语言的库函数,包含在<stdlib.h>头文件下,可以对任意数据类型进行排序
这个函数共有4个参数:
- base:void*类型,指向要排序的数据的起始地址处。
- num:unsigned int类型,要进行比较的数据个数。
- size:数据的大小。
- compar:函数指针类型,它指向一个用于比较指定类型的两个数据大小的函数,这个函数由使用者自己写出,它传入的参数是进行比较的两个数据的地址,返回值为正数、负数、或者0,返回值决定排序按升序还是降序进行。
int型数据qsort排序
#include<stdio.h>
#include<Windows.h>
#include<assert.h>
int int_compare(const void* x, const void* y)
{
assert(x);//判断指针合法性
assert(y);
const int* x_ = (const int*)x;//将函数参数强转成int*类型并赋值给x_
const int* y_ = (const int*)y;
if (*x_ > *y_) {
return 1;
}
else if (*x_ == *y_) {
return 0;
}
else {
return -1;
}
}
int main()
{
int arr[] = {
10,24,98,19,2,8 };
int num = sizeof(arr) / sizeof(arr[0]);
qsort(arr, num, sizeof(int), int_compare);
for (int i = 0; i < num; i++) {
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
double型qsort排序
double类型的数据比较大小,原理与int相同,区别就是compare函数的写法,有一点需要注意,double型数据不能用‘==’比较。
#include<stdio.h>
#include<Windows.h>
#include<assert.h>
int double_compare(const void* x, const void* y)
{
assert(x);//判断指针合法性
assert(y);
const double* x_ = (const double*)x;//将函数参数强转成double*类型并赋值给x_
const double* y_ = (const double*)y;
if (*x_ > *y_) {
return 1;
}
else if (*x_< *y_) {
return -1;
}
else {
return 0;
}
}
void Print(double arr[],int num)
{
for (int i = 0; i < num; i++) {
printf("%.3f ", arr[i]);
}
printf("\n");
}
int main()
{
double arr[] = {
9.84,6.200,6.52,4.21,48.25,58.666,98593.55 };
int num = sizeof(arr) / sizeof(arr[0]);
Print(arr, num);
qsort(arr, num, sizeof(double), double_compare);
Print(arr, num);
system("pause");
return 0;
}
字符串使用qsort排序
字符串比较大小的规则是:从起始位置开始进行字符的ASCII值比较,遇到第一个不相同的字符,ASCII值大的字符串大。
#include<stdio.h>
#include<Windows.h>
#include<assert.h>
#include<string.h>
int String_compare(const void* x, const void* y)
{
assert(x);//判断指针合法性
assert(y);
//因为函数的参数是进行比较的变量的地址而此处要比较的\
两个变量是char*类型,所以这里要用二级指针char**
const char ** x_ = (const char**)x;
const char ** y_ = (const char**)y;
//strcmp的参数是字符串起始地址,所以此处要对*x_和*y_进行解引用
return strcmp(*x_, *y_);
}
int main()
{
//定义一个指针数组,数组元素指向要进行比较的字符串
char *arr[] = {
"abcd1234",
"alkjgllkgj",
"sdagfsagf",
"agsafidfk",
"sagjsdakjd"
};
int num = sizeof(arr) / sizeof(arr[0]);
qsort(arr, num, sizeof(char*), String_compare);
for (int i = 0; i < num; i++) {
printf("%s\n", arr[i]);
}
system("pause");
return 0;
}
3.qsort函数模拟
用冒泡排序法模拟实现qsort函数
#include<stdio.h>
#include<Windows.h>
#include<assert.h>
#include<string.h>
int String_compare(const void* x, const void* y)
{
assert(x);//判断指针合法性
assert(y);
//因为函数的参数是进行比较的变量的地址而此处要比较的\
两个变量是char*类型,所以这里要用二级指针char**
const char** x_ = (const char**)x;
const char** y_ = (const char**)y;
//strcmp的参数是字符串起始地址,所以此处要对*x_和*y_进行解引用
return strcmp(*x_, *y_);
}
void Swap(void* x, void* y,size_t size)
{
char* x_ = (char*)x;
char* y_ = (char*)y;
//逐比特位交换两个变量,这种方式可以交换任意类型变量的值
for (size_t j=0; j < size; j++) {
*x_ ^= *y_;
*y_ ^= *x_;
*x_ ^= *y_;
x_++, y_++;
}
}
void MyQsort(void* base, size_t num, size_t sz, int(*compare)(const void* x, const void* y))
{
assert(base);
assert(compare);
for (size_t i = 0; i < num-1; i++) {
int flag = 1;
for (size_t j = 0; j < num - i - 1; j++) {
//因为要排序的数据类型是不确定的,所以用char*来指向
//通过起始地址+元素大小*所经历元素个数的方式,可以确定数据的地址\
将数据地址传递给compare指向的函数,确定类型后可以比较两个数据的大小
if (compare((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0) {
Swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz);
flag = 0;
}
}
if (flag)
{
break;
}
}
}
int main()
{
//定义一个指针数组,数组元素指向要进行比较的字符串
char* arr[] = {
"nbcd1234",
"jlkjgllkgj",
"xdagfsagf",
"ygsafidfk",
"xagjsdakjd"
};
int num = sizeof(arr) / sizeof(arr[0]);
MyQsort(arr, num, sizeof(char*), String_compare);//自定义函数MyQsort
for (int i = 0; i < num; i++) {
printf("%s\n", arr[i]);
}
system("pause");
return 0;
}
程序的内核是一个冒泡排序算法,而由于数据类型不能确定,与普通冒泡排序略有区别区别,首先,无法直接比较两个数据的大小,所以函数的形参中有个函数指针,在该函数中调用另一个函数,即回调函数,用于比较某一确定的数据类型的值的大小;其次,确定数据在内存中的地址方式也不一样,将起始地址强转成char*类型,加上类型大小乘以跨度确定地址;最后,交换两个变量的值时,逐比特位交换内容。
五、试题解析
(一)指针和数组试题
第一组 整型数组
ps:注释中“下一个数组的地址”这样的描述不准确,只是为了便于理解,其实际指的是数组最后一个元素下一个位置的地址,是一个指针数组,其指向与已经定义的数组类型相同。
//一维数组
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函数
C 字符串的长度等于字符串开头和终止空字符之间的字符数(不包括终止空字符本身)。
//字符数组
char arr[] = {
'a', 'b', 'c', 'd', 'e', 'f' };
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));//首元素地址,没有'\0',所以结果为随机值
printf("%d\n", strlen(arr + 0));//首元素地址,随机值
printf("%d\n", strlen(*arr));//首元素,报错
printf("%d\n", strlen(arr[1]));//第一个元素,报错
printf("%d\n", strlen(&arr));//整个数组的地址,数组指针类型,随机值,有告警
printf("%d\n", strlen(&arr + 1));//下一个数组的地址,数组指针类型,随机值,有告警
printf("%d\n", strlen(&arr[0] + 1));//第二个元素的地址,随机值
第三组 数组保存的字符串
char arr[] = "abcdef";
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));//首元素,报错
//printf("%d\n", strlen(arr[1]));//首元素,报错
printf("%d\n", strlen(&arr));//整个数组的地址开始,类型不匹配告警,6
printf("%d\n", strlen(&arr + 1));//下一个数组的地址开始,类型不匹配告警,随机值
printf("%d\n", strlen(&arr[0] + 1));//第二个元素的地址开始,5
第四组 char*指向的字符串
char *p = "abcdef";
printf("%d\n", sizeof(p));//指针,4
printf("%d\n", sizeof(p + 1));//指针,4
printf("%d\n", sizeof(*p));//字符a,1
printf("%d\n", sizeof(p[0]));//下标引用,字符a,1
printf("%d\n", sizeof(&p));//二级指针,4
printf("%d\n", sizeof(&p + 1));//4
printf("%d\n", sizeof(&p[0] + 1));//字符b的地址,4
printf("%d\n", strlen(p));//首地址开始,6
printf("%d\n", strlen(p + 1));//第二个元素的地址开始,5
printf("%d\n", strlen(*p));//第一个元素,报错
printf("%d\n", strlen(p[0]));//第一个元素,报错
printf("%d\n", strlen(&p));//二级指针,随机值
printf("%d\n", strlen(&p + 1));//二级指针,随机值
printf("%d\n", strlen(&p[0] + 1));//第二个元素地址开始,5
▲第五组 二维数组
//二维数组
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]));//第一个数组的数组名,单组在sizeof中,表示整个数组,16
printf("%d\n", sizeof(a[0] + 1));//没有单独放在sizeof内,第一个一维数组的第二的元素的地址,4
printf("%d\n", sizeof(*(a[0] + 1)));//第一个一维数组的第二个元素,4
printf("%d\n", sizeof(a + 1));//a是首元素地址,即第一个一位数组的地址,加一表示第二个一维数组的地址,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));//a是第一行地址,第一个一维数组的地址解引用,表示第一个一维数组,16
printf("%d\n", sizeof(a[3]));//sizeof不会访问目标,只根据类型来计算大小,故无报错,a[3]是一个包含四个整形的一维数组,16
二维数组的题要抓住几点:
- 数组名大部分情况都表示首元素地址,而对于二维数组来说,首元素就是一个一维数组。
- 数组名只有单独出现在sizeof内部和&符号后表示整个数组。
- 二维数组的每一个一维数组名的表示方法
(二)笔试题
第一题
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表示整个数组的地址,加一指向数组后下一个位置,由于ptr类型是int*,减一实际减去所指向类型的大小,指向5的前面;a表示首元素地址,加一为第二个元素的地址,解引用后表示第二个元素。
如图:
第二题
//由于还没学习结构体,这里告知结构体的大小是20个字节
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
分析: 结构体的大小为20个字节,所以指针加一,加上的是20,转换成16进制即为14;将p强制转换成无符号长整型后,对其加一,实际值也加一;将p强转成unsigned int *类型,加一,实际值加四,因为指针的大小为四字节。
第三题
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;
}
运行结果: 4,2000000
分析: ‘%p’格式与’%x’格式都是按十六进制输出,区别是 ‘%p’格式会输出八位,不够八位的补零,’%x’格式不会补0。
这里指针ptr指向的是数组最后一个元素的下一个位置,因其被强转成int*类型,ptr[-4]指向的是原来位置往前四个字节的位置,即指向4的最前面;
a强制转换为int型后值加1,再强为int *类型,指向的位置如下图所示,数据在内存中的存储遵从大小端原则,小端存储转低权值位在低地址处,读取时同样遵从小端的规律,高地址处的数据放在高权值位,所以读取顺序为02 00 00 00,最终结果为2000000。
第四题
#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
分析: 数组在内存中的存储如图所示,圆括号中是逗号表达式,最终数组元素的值是逗号右边的值;a[0]是第一个一维数组的数组名,表示首元素地址,即a[0][0]的地址,赋值给p后,p指向首元素的位置,p[0]数组下标引用,表示偏移量为0,仍然指向原来1的位置。
第五题
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;
}
运行结果: FFFFFFFC,-4
分析: 内存布局如下图所示,指针p跨度为4,最终与 &p[4][2] 与 &a[4][2]之间差4个字节,结果为-4,以%p打印输出结果则为FFFFFFFC。
第六题
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;
}
运行结果:10,5
分析:&aa+1指向二维数组最后一个元素后面的位置;aa+1是指向第二个一维数组的数组指针,解引用后表示整个数组,等价于aa[1],即第个二维数组的数组名,表示第二个二维数组首元素的地址,即a[1][0]的地址,强转后减一,指向a[0][4]。
第七题
#include <stdio.h>
int main()
{
char *a[] = {
"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
运行结果: at
分析: 字符指针数组的写法并不是把字符串放保存在数组里,字符串是保存在内存中的静态字符常量区,这里只是用指针指向每个字符串第一个字符的地址,将这样的指针保存在数组中,与定义指针指向字符串原理相同,如:
char *pc="hello world";
这里的pc指向字符串的第一个字符’h’,并不是把字符串保存在pc中。
本题的数组a,是一个指针数组,保存了3个指向字符串的指针,如图:
pa是一个二级指针,指向a[0]这个一级指针,对pa自增后,其指向下一个一级指针a[1],pa解引用得到一个字符指针,%s打印输出,结果则为’at’。
第八题
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;
}
“ENTER” “NEW” “POINT” “FIRST”
运行结果:
POINT
ER
ST
EW
分析: 内存分布图如下:
1.printf("%s\n", **++cpp);
++cpp对cpp进行自增,需要注意,cpp的指向会发生改变,且对后面都有影响。
所以结果为POINT。
2.printf("%s\n", *--*++cpp+3);
需要注意此时的cpp要用第一次自增后的指向;此时的cpp指向cp[1],自增后则指向产品[2],对cp[2]自减,其指向由cp[1]变为c[0],解引用则得到c[0]原来指向字符第一个字符’E’,c[0]+3指向第二个字符’E’,但是从[0]的指向只是临时改变,并没有赋值保存。如图:
3.printf("%s\n", *cpp[-2]+3);
cpp[-2]为数组下标引用,表示cp[0],其指向c[3],所以*cpp[-2]表示c[3],c[3]指向字符’F’,所以c[3]+3指向字符’S’,打印结果为’ST’。注意cpp[-2]与cpp-2的区别,cpp[-2]等价于*(cpp-2)。
4.printf("%s\n", cpp[-1][-1]+1);
等价于*(*(cpp-1)-1),cpp[-1][-1]表示c[2],c[2]是一个字符指针,指向字符’N’,加1后指向字符’E’。如图所示: