1.什么是指针
1.指针是内存中最小单元的编号,也就是地址。地址是唯一标识一块空间的。 2.口语中的指针通常指的是指针变量,也就是存放内存的变量
2.指针变量的大小
在32位机器上,地址需要由32个二进制序列完成,所以地址需要四个字节的空间来存储,所以指针的大小在32位平台上就是四字节。但如果是64位机器,则需要翻倍,即64个二进制序列,八个字节。
3.指针类型的意义
1.指针的类型决定了指针解引用时的访问权限。即解引用后指针能从所指的位置向后访问几个字节。 2.指针的类型决定了指针+(-)整数时的步长。即+1向后跳过几个字节,-1向前移动几个字节。
4.指针的运算
1.指针+(-)整数:指针移动整数个指针类型大小 2.指针-指针:得到指针之间的元素个数 3.指针间关系运算:比较两个指针大小
5.野指针和规避方法
野指针的成因主要有: 1.指针未初始化 2.指针越界访问 3.指针指向的空间被释放
规避方法: 1.定义指针时同时初始化,如果不知道初始化何值就置为NULL。 2.避免指针越界 3.释放指针指向的空间的同时把该指针置为NULL 4.牢记局部变量离开局部区域就会被销毁,避免返回局部变量的指针。 5.使用指针前检查其有效性
字符指针就是用来存放字符地址的指针,类型为char*
字符指针的两种使用方法:
第一种:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
第二种:
int main()
{
const char* pstr = "hi boy.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
这里对第二种使用方法简单介绍一下:虽然我们把常量字符串"hi boy "作为初始值赋给字符指针pstr,但是**实际上pstr只是把这个常量字符串的首地址,即’h’的地址给存储起来了。**后续我们可以用%s的方式打印整个字符串。
在这里再来解释一下什么是常量字符串:存储在字符常量区,并且不能修改的字符,就叫常量字符串。但是指针似乎是可以解引用修改指向空间的值,因此为了防止这种误操作,我们应该用const关键字来修饰该指向常量字符串的指针。
1.const修饰指针:在*的左边,p指向的对象不能通过p来改变,但是p本身还是可以改变的。 2.const在*的右边,p指向的对象可以通过p来修改,但是p本身不能被修改。
笔试题练习
判断下面程序的输出结果:
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char *str3 = "hello world";
const 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是两个独立数组,数组的空间在栈区开辟,内存会给两个数组分配不同的空间并把空间里的内容初始化为"hello world"。数组名代表首元素地址,空间都不同,地址肯定不会相同。 但对于str3和str4来说,由于"hello world"是常量字符串不可修改,因此它只会在字符常量区中存一份。也就是说str3和str4 指向的是同一块空间的同一个字符’h’的地址,结果当然是相等。
指针数组是一个数组,这个数组是用来存放指针变量的。
指针数组的定义:
int* arr[10];
# arr的类型:int* [10] //去掉变量名剩下的就是变量类型
# arr先和[10]结合,表示arr是一个数组,数组里面有10个元素,每个元素的类型是int*
char* str[10];
# str的类型 char* [10] //去掉变量名剩下的就是变量类型
# str先和[10]结合,表示str是一个数组,数组里面有10个元素,每个元素的类型是char*
指针数组的使用:
//使用指针数组可以实现升维操作,一维数组变二维数组
int main()
{
int a[] = { 1,2,3 };
int b[] = { 3,4,5 };
int c[] = { 4,5,6 };
int* arr[3] = { a, b, c };
int i = 0;
for (i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
数组指针是指针,这个指针指向数组。
数组指针的定义:
int (*arr)[10];
# arr的类型:int (*)[10] //去掉变量名剩下的就是变量类型
# arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是int;
char* (*arr)[10];
# arr的类型:int* (*)[10] //去掉变量名剩下的就是变量类型
# arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是char*;
数组名和&数组名的区别
我们知道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;
}
看运行结果我们发现,数组名和&数组名的地址是一致的。但是数组名+1只是跳过四个字节,而&数组名+1却跳过了四十个字节。
这说明:数组名只是首元素地址,而&数组名取出的是整个数组的地址。至于为什么数组名和取地址数组名相同是因为指针中存储的是一个类型的起始地址。
通常来说数组名都是代表首元素的地址,但也有两个例外: 1.那就是sizeof(数组名),当sizeof中单独放数组名时,此时数组名代表首元素地址。 2.&数组名,得到的是整个数组的地址。
数组指针的使用(通常用于二维数组)
void print_arr(int(*arr)[5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
//arr[i] 相当于 *(arr+i)
//所以 arr[i][j] 相当于 *(*(arr+i)+j)
//arr[i] 找到二维数组具体的某一行,而行号代表那一行,同时行号又表示那一行首元素的地址
//所以 arr[i][j] 就可以找到二维数组中具体某一行的具体某一个元素
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//一维数组的地址用数组指针来接收
print_arr(arr, 3, 5);
return 0;
}
一维数组传参
void test(int arr[]){}//ok?
//数组传参用数组接收,怎么也挑不出毛病吧
void test(int arr[10]){}//ok?
//数组传参用数组接收时,元素个数可写可不写
void test(int* arr){}//ok?
//前面有说过,数组名其实是首元素的地址,因此用一个整形指针接收也是可以的。
void test2(int* arr[20]){}//ok?
//指针数组传参用指针数组接收,没毛病
void test2(int** arr){}//ok?
//数组指针首元素的地址是一个二级指针,因此用二级指针来接收也没毛病。
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
二维数组传参
void test(int arr[3][5]){}//ok?
//二维数组传参用二维数组接收,没问题
void test(int arr[][]){}//ok?
//哒咩,不行。二维数组只能省略行不能省略列,不论是多少维的数组都只能省略第一维。
void test(int arr[][5]){}//ok?
//可以省略行,因此是可以的
void test(int* arr){}//ok?
//不行,对于一个二维数组而言,数组的首元素是其第一行,一个一维数组的地址当然不能用一个整形指针来接受。
void test(int* arr[5]){}//ok?
//不行。 一维数组的地址应该用一个数组指针来接收。
void test(int(*arr)[5]){}//ok?
//这不就是心心念念的数组指针吗
void test(int** arr){}//ok?
//不行。二级指针是用来接收一级指针变量地址的指针
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
一级指针传参
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]);
print(p, sz); //将一级指针p传给函数
return 0;
}
思考:当一个函数的参数部分为一级指针时,函数可以接收什么参数?
1.同级别的指针(一级指针或者二级指针解引用) 2.一维数组的数组名 3.整形等类型的地址
二级指针传参
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]);
print(p, sz); //将一级指针p传给函数
return 0;
}
思考:当一个函数的参数部分为二级指针时,函数可以接收什么参数?
1.二级指针 2.一级指针的地址 3.指针数组的数组名
什么是函数指针
首先看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们已经知道,所有类型的变量都要在内存中开辟空间,为了能精确的识别每一块空间,因此每一块空间都有自己的编号,即地址。上面那段代码证明:函数同样也是有地址的,存放函数地址的变量就是函数指针。
函数名也是函数的地址。
函数指针的定义
int (*p1)(int, int) = &Add;
int (*p1)(int, int) = Add;
# 我们上面已经知道了,函数名和&函数名都代表函数的地址,所以上面这两种写法其实是一样的,都是把Add函数的地址赋给了函数指针p1;
# p1的类型:int (*)(int, int) //去掉变量名剩下的就是变量类型
# p1首先和*结合,表示它是一个指针,然后和(int, int)结合,表示它指向的是一个函数,函数的参数是int,int,返回值也是int;
函数指针的使用
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p1)(int, int) = &Add;
//int (*p1)(int, int) = Add;
int ret1 = Add(2, 3);
int ret2 = (*Add)(2, 3);
printf("%d %d\n", ret1, ret2);
int ret3 = p1(2, 3);
int ret4 = (*p1)(2, 3);
int ret5 = (*********p1)(2, 3);
printf("%d %d %d\n", ret3, ret4, ret5);
return 0;
}
我们之前在使用函数时,是函数名+函数传参。而函数指针是指针,我们是否需要先解引用再进行传参呢?前面有提到,函数名也是函数的地址,而函数指针存储的也是函数的地址。因此就算不对函数指针进行解引用操作也可以直接使用,上面的代码就已经证明。但如果要加解引用操作,其实写多少个都无所谓,没有影响,就像上面的ret5。但一定要注意,要将解引用操作符用括号将其和变量名括起来。
两段有趣的代码
(*(void (*)())0)();
首先可以发现void (* )()是一个函数指针类型,该指针指向的函数参数为空返回值为void,即不需要返回值。 我们知道将函数类型放在括号里的操作是强制类型转换。即(int*)10,是把原本为整形的10强制类型转换为整形指针。那么同理可得(void( *)())0是将0强制类型转换为函数指针。 (* void(* )()0)()是对函数指针进行解引用操作,也就是说这是一次函数调用。调用了0地址处,参数为空返回值也为空的函数。
上面这段代码出自《C陷阱和指针》的第二章,该书对本问题的描述如下:
这说明这种看起来花里胡哨的代码还是有实际意义的,并不是为了装逼的产物,只是一般很少写。(书中提到的子例程是函数的意思)
void (*signal(int , void(*)(int)))(int);
signal(int,void(*)(int)),这好像是一个还没写出返回类型的函数。恭喜你答对了,signal是这个函数的函数名,该函数有两个参数,第一个参数是整形。第二个参数是函数指针类型,该函数指针指向的函数参数为整形,返回值为空。 我们把signal(int,void( * )(int)抽离出去,得到void(* )(int),不难发现这是一个函数指针类型。对于函数而言,除去函数名和参数,剩下的就是函数的返回值。因此signal函数的返回值是一个函数指针类型,该指针指向的函数参数为整形,返回值为空。 综上所述,这是一次函数声明。声明的函数的参数为整形和参数为整形,返回值为空的函数指针,返回值也是一个参数为整形,返回值为空的函数指针。
这个问题同样出自《C陷阱和缺陷》第二章,紧挨着上一个函数调用:
上面的代码太过复杂,能不能简化一下呢?
用typedef关键字,给一个类型起别名。将参数为整形,返回值为空的函数指针重命名即可简化上面的代码。
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
数组是用来存储相同数据类型的存储空间,因此函数指针数组就是用来存储函数指针的数组。本质上还是数组,只是数组的每一个元素都是函数指针。
函数指针数组的定义
int (*parr1[10])(int);
# parr1的类型:int (*[10])(int) //去掉变量名剩下的就是变量类型
# parr1和[10]结合,表示它是一个数组,数组里面有10个元素,每个元素的类型是一个函数指针,该指针指向的函数的参数为int,返回值为int;
在学了函数指针时,我们就可能会有这种疑惑:为什么明明能直接使用函数名来调用函数,为什么还要用函数指针来调用?是多此一举吗?C语言在20世纪七十年代初问世,从【K&R C】发展到C99,如果是真的没有意义的东西,早就被删除了。那么函数指针存在的意义到底是什么?
实际上,函数指针是特别C语言中特别高明的存在,在用C语言完成大型工程时,函数指针会被经常使用。而函数指针最常用的两个用途就是回调函数和转移表。
举个栗子:
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("****************************\n");
printf("***** 1. Add 2. Sub*****\n");
printf("***** 3. Mul 4. Div*****\n");
printf("***** 0. Exit *****\n");
printf("****************************\n");
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出程序\n");
break;
case 1:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
这样的代码对于已经学到指针的我们来说,是不成问题的。但是我们发现这段代码中由大量的重复的代码
除了中间的函数调用不同以外,其它的代码都是一样的。大量相同代码,你是不是想到用封装函数的办法来解决?但是如何封装却成为了一个很大的问题。因为每个case中使用的函数都不相同,如果每个case封装一个函数,代码同样是重复的。这个时候就体现函数指针的妙用了。
用函数指针改造
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void menu()
{
printf("****************************\n");
printf("***** 1. Add 2. Sub*****\n");
printf("***** 3. Mul 4. Div*****\n");
printf("***** 0. Exit *****\n");
printf("****************************\n");
}
void calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出程序\n");
break;
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
将冗余的代码全部封装到calc函数中,并把calc函数的参数设为一个函数指针。无论你是想用加减乘除的哪一个功能,只要把其函数的地址作为参数传给calc即可。
上面已经说过函数指针的用途了,那么函数指针数组是用来干嘛的呢?答案是转移表。
你是否觉得上面计算器代码已经足够简洁?大漏特漏,如果用函数指针数组来实现,代码将会更加简单。
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf("输入有误\n");
printf("ret = %d\n", ret);
}
return 0;
}
补个零只是为了实现选1就是调用add函数,毕竟数组的下标是从零开始的。从上面的代码可以看到,使用函数指针和函数指针数组可以简化代码。但他们实际的用途远不止于此。尤其是函数指针演变出来的回调函数,简直是妙不可言。
回调函数
回调函数就是通过函数指针调用的函数。如果我们把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方调用,而是在特定的事件或条件发生时由另一方调用,用于对该事件或者条件响应。值得一提的是C语言的库函数qsort就是使用了回调函数。
qsort简介
void qsort(void* base,//起始地址
size_t num,//元素个数
size_t size,//单个元素大小
int (*compar)(const void*, const void*));//比较函数
//qsort使用
//qsort函数的使用者得实现一个比较函数
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;
}
上面出现了void * 类型,这个指针类型是泛型指针,可以接受任意类型的地址,但不能解引用也不能加减整数操作,因为无法确定类型不知道一次该访问多少字节。
使用回调函数参考库函数的qsort可以将原本只能排序整形的冒泡排序改成可以排序任意类型。
struct Stu
{
char name[20];
int age[3];
};
int cmp_stu_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int cmp_int(const void* e1, const void* e2)
{
return *((int*)e1) - (*(int*)e2);//e1大于e2,返回值大于零,相等返回值等于零,小于返回值小于零
}
//在这个函数里调用不同的cmp函数就可以解决,但是问题在于cmp都是void型的元素,
//强转成char*,然后每次交换宽度个char*,
void Swap(char* e1, char* e2,int width)
{
//一个字节一个字节的交换
for (int i = 0; i < width; i++)
{
char tmp = *e1;
*e1 = *e2;
*e2 = tmp;
e1++;
e2++;
}
}
int better_bubble_sort(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2))
{
int flag = 1;//假设数组就是有序的
int i = 0, j = 0;
for ( i = 0; i < num; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width)>0)//这里的cmp就是传参过来的cmp,比较什么类型完全由使用者决定。从起始地址开始加上j*width就是当前元素
{
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);//交换两个元素
flag = 0;
}
}
if (flag == 1)
{
break;//说明数组本省就是有序的,不用排
}
}
}
int main()
{
int arr[] = { 0,1,2,6,5,4,7,8,9,3 };
struct Stu S[] = { {"zhangsan",15},{"lisi",20 },{"wangwu",30} };
int sz = sizeof(arr) / sizeof(arr[0]);
int sz = sizeof(S) / sizeof(S[0]);
better_bubble_sort(arr, sz1, sizeof(arr[0]), cmp_int);
better_bubble_sort(S, sz,sizeof(S[0]), cmp_stu_name);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
for (int i = 0; i < sz; i++)
{
printf("%s ", S[i]);
}
}
这里需要注意的就是,我们在写函数时不会知道使用则会传什么参数给我们,因此以最小的char类型来比较和交换,因为类型最小也占一个字节。而宽度又给我们提供了,一个类型的比较范围。从起始地址加上j乘宽度就可以得到当前元素的地址。而代码运行结果也确实告诉我们,改良后的代码不但能排序整形同样可以排序字符型。
数组
一维数组:
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a + 0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a + 1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0] + 1));
解析:
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));//16个字节
//sizeof数组名和单独的&数组名都是得到整个数组
printf("%d\n", sizeof(a + 0));//4/8
//a是首元素的地址,加零跳过零个整形,还是首元素地址,地址就是4/8个字节
printf("%d\n", sizeof(*a));//4
//*a是首元素
printf("%d\n", sizeof(a + 1));//4/8
//跳过一个元素,得到的是数组第二个元素的地址
printf("%d\n", sizeof(a[1]));//4
//数组第二个元素
printf("%d\n", sizeof(&a));//4/8
//虽然取出的是整个数组的地址,但地址就是4/8个字节
printf("%d\n", sizeof(*&a));//16
//&a是整个数组,解引用后也就得到整个数组
printf("%d\n", sizeof(&a + 1));//4/8
//从a的地址向后跳过了一整个数组的地址
printf("%d\n", sizeof(&a[0]));//4/8
//第一个元素的地址
printf("%d\n", sizeof(&a[0] + 1));//4/8
//与a+1一样
字符数组:
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
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[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));//随机值
//因为数组中没有\0,而strlen要读取到\0才会停止
printf("%d\n", strlen(arr + 0));//随机值,理由同上
printf("%d\n", strlen(*arr));//访问冲突
//因为strlen(const char*str),参数是字符指针,传字符a不合理。
//同时也包含了野指针问题,strlen('a')==strlen(97)把97作为地址传过去,但是97这块地址并不属于你。
printf("%d\n", strlen(arr[1]));//访问冲突,理由同上
printf("%d\n", strlen(&arr));//随机值,并且和第一第二个相同
//数组的地址和数组首元素地址相同,任何类型的地址都是该类型的起始地址
printf("%d\n", strlen(&arr + 1));//随机值-6
//跳过了一个arr数组,并且arr数组有六个元素
printf("%d\n", strlen(&arr[0] + 1));//随机值-1,只是跳过了第一个元素
二维数组:
int a[3][4] = {0};
printf("%d\n", sizeof(a[0] + 1));
printf("%d\n", sizeof(*(a[0] + 1)));
printf("%d\n", sizeof(a + 1));
printf("%d\n", sizeof(&a[0] + 1));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a[3]));
解析:
int a[3][4] = {0};
printf("%d\n", sizeof(a[0] + 1));//4/8
//a[0]是第一行的数组名,并没有单独放在sizeof内,代表的是第一行的首元素的地址,因此a[0]+1其实就是第一行第二个元素的地址
printf("%d\n", sizeof(*(a[0] + 1)));//4
//得到的是a[0][1]
printf("%d\n", sizeof(a + 1));//4/8
//a代表首元素的地址,其实就是第一行的地址,+1以后代表的是数组第二行的地址
printf("%d\n", sizeof(&a[0] + 1));//4/8
//第二行的地址
printf("%d\n", sizeof(*a));//16
//第一行的元素
printf("%d\n", sizeof(a[3]));//16
//看起来似乎越界了,但是sizeof只要知道类型就能计算大小,并不会真正的去访问。
指针面试题
试题一:
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
解析:
int main()
{
printf("%p\n", p + 0x1);//0x100014
//p是结构体指针,已知这个结构体是20个字节,+1就是跳过20个字节。
printf("%p\n", (unsigned long)p + 0x1);
//把p强制转换为长整型,结果就是这个十六进制转为十进制再加一。/把整形以地址的形式打印出来是可以的
printf("%p\n", (unsigned int*)p + 0x1);
//转为int型,+1就是往后跳四个字节。0x100004
return 0;
}
试题二:
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;
}
解析:
试题三:
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;
}
解析:
int* ptr1 = (int*)(&a + 1); //无非就是跳过一个数组 int* ptr2 = (int*)((int)a + 1);//2000000 //这个有点意思了
试题四:
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;
}
这题有点难度,我们一个一个画图掰扯:
printf(“%s\n”, **++cpp);
printf(“%s\n”, –++cpp+3);
printf(“%s\n”, *cpp[-2]+3);
printf(“%s\n”, cpp[ -1] [ -1 ]+1);
人脑的想象力是有限的,因此学会画图是我们的必备技能。
写在后面
指针是C语言的重要内容,为了后续数据结构的学习,在C语言的学习过程中,我们应该要把指针,结构体,动态内存管理这三章学好。 要坚持学习坚持进步啊。说到进步,我想起一个骚话:这个世界上没有毫无道理的横空出世,我被女生拒绝了三百多次才有今天的人见人爱,我是浩哥我还在提升自己,你也可以。(出自抖音某博主)。