一、二级指针
问:指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
答:二级指针,用来存放以及指针变量的地址
int main() { int a = 10; int *p = &a;//取出a的地址 //p是指针变量,一级指针 int* * pp = &p; //int*说明pp指向的对象的类型是int* // *说明pp是变量,pp是二级指针,用来存放以及指针变量的地址 //指针变量也是变量 int*** ppp = &pp;//ppp是三级指针(用的很少) //... printf("%d\n", **pp); return 0; }
二、指针数组和数组指针
2.1指针数组
指针数组是指针还是数组?
我们类比一下:
整型数组 - 存放整型数据的数组(数组中的每个元素是整型)
字符数组 - 存放字符数据的数组(数组中的每个元素是字符类型)
指针数组 - 存放指针的数组(数组中的每个元素是指针类型)
有一个数组,数组有四个元素,每个元素是整型指针,所以指针数组本质是是数组
int main() { int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; int* arr[3] = { arr1,arr2,arr3 };//整型指针数组,模拟二维数组,但每一行,并不是连续的 int i = 0; for (i = 0; i < 3; i++) { for (int j = 0; j < i; j++) { printf("%d ", arr[i][j]); //arr[i][j] ==> *(*(arr+i)+j) } printf("\n"); } return 0; }
可以在上述代码和输出结果中看出这段代码是通过指针数组来模拟二维数组的访问。二维数组可以被看作是数组的数组,而指针数组是数组的指针。在该段代码中定义了三个一维数组,然后创建了一个指针数组,该数组指向这三个一维数组的开始地址。然后你通过两个嵌套循环遍历这个“二维”数组并打印其内容。
此模拟二维数组与真正二维数组的区别:
1、内存布局:真正的二维数组在内存中是连续的,而使用指针数组模拟的二维数组不是。每行实际上是一个指向整数数组的指针,这些整数数组可能在内存中是分散的。
2、性能:由于内存布局的原因,使用指针数组模拟的二维数组在访问特定元素时可能需要更多的计算,这可能会影响性能。对于真正的二维数组,可以通过给出行和列的索引来直接访问元素。然而,对于使用指针数组模拟的二维数组,需要先找到对应的行,然后再找到对应的列,这需要额外的计算。
3、灵活性:使用指针数组模拟的二维数组可以更灵活地操作不同长度的行。例如,如果你想在运行时动态改变每行的长度,那么使用指针数组可能是更好的选择。
2.2数组指针
本质上是指向数组的指针,数组指针变量存放的是数组的地址
数组和指针的关系非常密切。一个数组的地址可以被赋值给一个同类型的指针,这样这个指
针就可以指向这个数组。
整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p1 = arr; // int* int* int* p2 = &arr[0]; // int* int* int(*p3)[10] = &arr;//p3是数组指针 // int(*)[10] int (*)[10] //此处[]的优先级高于* return 0; }
int main() { int n = 100; int* pn = &n; char ch = 'w'; char* pc = &ch; float f = 3.14f; float* pf = &f; int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int ** parr = &arr;//取出的是数组的地址,parr是数组指针 int* p1[10];//p1是数组,数组10个元素,每个元素的类型都是int*,所以p1是指针数组 int (*p2)[10];//p2是指针,指针指向的是数组,数组有10个元素,每个元素的类型都是int,所以p2是数组指针 return 0; }
int main() { int a = 10; int* p = &a; int* arr[10] = { p }; int* (*arr[10]);//数组指针,指向的是int*[]指针数组,其中包括指针 int(*arr)[10];//数组指针,指向的是int[]整型数组 }
用途:
1、作为函数的参数:当你在写函数时,如果你想让函数操作一个数组,你可以将数组的指针作为函数的参数传递。这样,函数就可以直接修改原始数组中的值,而不仅仅是修改副本。
2、动态内存分配:你可以使用指针来动态地分配内存空间,这在处理可变长度数据或者需要临时存储数据时非常有用。
[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
int *p1[10];//指针数组,p1先和[10]结合,说明p1是一个数组。
int (*p2)[10];//数组指针,p2先和*结合,说明p是一个指针变量。
三、字符指针
字符指针,它指向一个字符数组或字符串。
用途
1、动态内存分配:字符指针可以用于动态内存分配,这在处理变长字符串或其他数据结构时非常有用。
2、灵活的数据存储:使用字符指针,我们可以方便地改变指针所指向的内存地址,从而更改存储的数据。
3、数据类型的抽象:字符指针提供了一种通用的、抽象的数据类型,可以指向任何数据类型(只要该类型可以转换为字符类型)。这使得我们可以用同一种方式处理不同类型的数据。
int main() { //char ch = 'w'; //char* pc = &ch;//pc就是字符指针 char* p = "abcdef";//不是把字符串abcdef\0存放在p中 //而是把首字符的地址放在p中 //printf("%c\n", *p); printf("%c\n", "abcdef"[3]); printf("%c\n", p[3]); return 0; }
对于 "abcdef"[3]
1、你可以把字符串想象为一个数组,但是这个数组是不能修改的
2、当常量字符串出现在表达式中,他的值是默认第一个字符的地址,而[3]让他指向第四个字符并访问,也就是字符d。
int main() { const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗? printf("%s\n", pstr); return 0; }
代码 const char* pstr = "hello bit."; 本质是把字符串 hello bit. 首字符的地址放到了pstr中。
《剑指offer》中收录了一道和字符串相关的笔试题,如下
int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char* str3 = "hello bit."; const char* str4 = "hello bit."; if (str1 == str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); //str1 and str2 are not same if (str3 == str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); //str3 and str4 are same return 0; }
这里str3和str4指向的是同一个常量字符串,C/C++会把常量字符串存储到单独的一个内存区域中,当几个指针指向同一个字符串的时候,实际上会指向同一块内存,但是相同的常量字符串去初始化不同的数组的时候,会开辟不同的内存块,所以str1和str2不同,str3和str4相同。
四、二维数组传参,形参写二维数组
1、为什么一维数组传参,形参可以是数组,也可以是指针?
1.写成数组更加直观,为了方便理解
2.写成指针传参是因为数组传参,传递的是数组的第一个元素的地址
二维数组传参,形参写成数组也是可以的,非常直观,容易理解
2、形参能写成指针吗?
可以,用数组指针
二维数组其实就是元素是一维数组的数组
数组名就是数组首元素的地址
首元素的地址就是第一行的地址
正常写法:
void Print(int arr[3][5], int r, int c) { int i = 0; for (i = 0; i < r; i++)//控制行 { for (int j = 0; j < c; j++) { printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; Print(arr,3,5);//打印arr数组内容 return 0; }
二维数组传参
void Print(int (*arr)[5], int r, int c) { int i = 0; for (i = 0; i < r; i++)//控制行 { for (int j = 0; j < c; j++) { printf("%d ", *(*(arr+i)+j)); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; Print(arr, 3, 5);//打印arr数组内容 return 0; }
void Print(int (*arr)[5], int r, int c)
第一个形参是一维数组的类型就是int[5],所以第一行的地址类型就是数组指针类型int(*)[5],那就意味着二维数组传参本质上也是传递了地址,传递的第一行这个一位数组的地址,那么形参可以写成数组指针的形式
五、函数指针变量
函数指针变量的创建
函数指针用来存放函数的地址
1、回调函数:在很多情况下,我们需要将一个函数作为参数传递给另一个函数。这在实现诸如事件驱动的系统、并行计算等高级功能时非常有用。在这些情况下,我们可以将函数指针作为参数传递,以便在需要时调用这个函数。
2、函数指针数组:我们可以创建一个包含函数指针的数组,这样就可以用一个统一的接口来调用多种不同的函数。这在实现多种行为或策略的情况下特别有用。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针非常类似。如下:
int Add(int x, int y) { return x + y; } int main() { int a = 10; int* pa = &a; int arr[10] = { 0 }; int (*parr)[10] = &arr;//数组指针 /*对比: 数组名是数组首元素的地址 取地址&数组名是整个数组的地址 函数名:函数的地址 取地址&函数名:函数的地址*/ //printf("%p\n", &Add); //00007FF6B57A1348 //printf("%p\n", Add); //00007FF6B57A1348 int (*pf)(int,int) = &Add;//pf就是函数指针变量 //*和pf先结合,*pf为指针,int(*)(int,int)是函数指针类型 int ret1 = (*pf)(4, 9);//通过解引用的方式调用函数 //*pf(4,9)是对pf函数的返回值解引用 printf("%d\n", ret1);//13 int (*pf2)(int, int) = Add; int ret2 = (*pf2)(4, 9); printf("%d\n", ret2);//13 int ret3 = pf2(5, 6);//可以不写* printf("%d\n", ret3);//11 return 0; }
5.1函数指针数组
问:那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
答:int (*parr1[3])() ;
问:parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
答:是 int (*)() 类型的函数指针。
5.2两段有趣的代码
1、 (*(void (*)())0)();
void(*)()是函数指针类型
(*(函数指针类型)0),把0强制转换为指针类型,一个函数的地址,
这个函数没有参数,仅仅是知道地址,函数名未知,返回值是void,即void 函数名()
(int)3.14是强制类型转换(*指针类型),解引用,调用0地址处的函数
(*(void (*)())0)() == void 函数名(),所以,上面的代码是一次函数调用
其作用调用首地址为0位置的函数(子程序)
2、void (*signal(int , void(*)(int)))(int);
函数类型 函数名 形参1 , 形参2
void(*)(int) signal( int , void(*)(int))
函数类型 函数名(形参...);//是一个函数声明
1.signal是一个函数声明
2.signal函数的参数有两个,第一个是int类型,
第二个是函数指针类型,该指针指向的函数参数是int,返回类型是void
signal函数的返回类型是这种类型的void(*)(int)函数指针
该指针指向的函数参数是int,返回类型是void
3.signal函数的返回类型也是一个函数指针: void(*)(int)
这个函数指针指向的函数参数是int类型,返回类型是void
所以上述代码是函数声明