我们在之前学习指针初阶的时候就知道了有关指针的概念:
1.指针就是一个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小固定为4/8个字节(32位平台/64位平台)
3.指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作时的权限。
4.指针的运算
下面我们先来回顾一下之前的学习内容,
指针:内存会划分为一个个的内存单元,每个内存单元都有有一个独立编号,这个编号也称为地址,地址在C语言中被称为指针,指针(地址)需要被存储起来,存储变量中,这个变量就被称为指针变量。
为什么指针的大小固定为4/8位(32位平台/64位平台)?
因为地址是物理的电线上产生,32位机器有32根地址线,这32而根地址线上传递0/1电信号,32个0/1组成的二进制序列作为地址。要32个bit位才能存储这个地址,也就是需要4个字节才能存储这个地址,所以指针变量的大小就是4个字节。同理,在64位机器上,地址的大小是64个0/1组成的二进制序列,需要64个bit位存储,所以指针变量的大小就是8个字节。
以上就是初阶指针中学习过的内容,下面我们来学习新的内容。
1.字符指针
前面学过关于字符指针的知识,下面就是字符指针:
int main() { char ch = 'w'; ch = 'a'; char* pa = &ch;//pa就是字符指针 *pa = 'b'; return 0; }
可以直接修改ch的内容,也可以通过指针修改。
今天我们来学习字符指针的另外一种用法,
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char arr[] = "abcdef"; const char* p = "abcdef";//常量字符串 printf("%s\n", p); printf("%c\n", *p); return 0; }
上代码很容易让人误解为将字符串放到指针变量里面去了,但是实际上指针变量p中存放的是字符串的首字符的地址(通过打印结果可以验证),这里的字符串是常量字符串,不可被修改,所以前面最好加const修饰。
上述代码中,通过%s和指针变量p就可以打印出字符串“abcdef”,这体现的是printf函数的功能,printf函数可以通过地址打印字符串,只要给出首地址,printf函数就可以通过该地址往后打印出字符串。
学了字符指针,下面我们就可以来看一道经典的笔试题:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { char str1[] = "hello xupt."; char str2[] = "hello xupt."; const char* str3 = "hello xupt."; const char* str4 = "hello xupt."; 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的地址不同,这就相当于将“hello xupt.”分别存了两遍。而str3和str4相同的原因是,此时的字符串是常量字符串,不可被修改,所以只在内存中存一遍,指针变量str3和str4中存放的都是字符串“hello xupt.”的首字符的地址,所以它们相同。
2.指针数组
在C初阶中我们通过类比了解过指针数组,
整型数组--存放整数的数组。
字符数组--存放字符的数组。
那指针数组就是存放指针的数组。
int*arr[10];//整型指针的数组 char*arr[4];//一级字符指针的数组 char**arr3[5];//二级字符指针的数组
我们还将过用指针数组模拟实现二维数组:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> 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++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", arr[i][j]); } printf("\n"); } return 0; }
指针数组arr中存放的三个整型指针分别是arr1,arr2,arr3,而它们各自是大小为5的一维数组的数组名,我们可以用 i 来遍历指针数组,用 j 来遍历一维数组,由此可实现对二维数组的模拟。
3.数组指针
3.1 数组指针的定义
数组指针也可以通过类比来了解,
整型指针 -- 指向整型变量的指针,存放整型变量的地址的指针变量。
字符指针 -- 指向字符变量的指针,存放字符变量的地址的指针变量。
那么数组指针就是指向数组的指针,存放数组的的地址的指针变量。
下面两个哪一个是数组指针呢?
int*p1[10]; int(*p2)[10];
很明显,p1先与[10]结合,p1[10]是数组,它里面存放的是int*型的指针,这就是指针数组,而(*p2)是分离出来的,它的外面是int [10],是数组的类型,所以p2是指针,指向的是数组,即p2是数组指针变量。
这里我们可以总结区分一下数组指针和指针数组:
数组指针,是指针,是指向数组的指针
指针数组,是数组,是存放指针的数组
了解了数组指针,那我们要如何个数组指针变量赋值呢?
在此之前,我们要再来复习一下关于数组名的知识。
3.2 &数组名VS数组名
前面我们学过数组名就是数组首元素的地址,下面可以验证一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { int arr[10] = { 0 }; printf("%p\n", arr); printf("%p\n", &arr[0]); return 0; }
运行结果:
这时有人要提出疑问了,你说数组名是数组首元素的地址,那sizeof(arr)打印出来的结果应该是4啊,为什么我打印出来的是40呢?
这就不得不提起关于数组名的两个例外了:
1.sizeof(数组名)数组名不是数组首元素的地址,数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址。
除此之外,所有地方的数组名都是数组首元素的地址。
现在我们来打印一下&arr,
此时有人有疑惑了,不是说&arr取出的是整个数组的地址吗,为什么它们三个结果是一样的?
这是因为,在内存中&arr要取出整个数组的地址也是从数组起始的地址开始的,所以以上代码看不出区别,我们可以打开监视查看:
这里就可以看到,前两个的类型都是int*型,而&arr的类型是int[10]*,这种写法不正确,但这实际上就是数组指针, 由此可见&arr取出的就是整个数组的地址。
下面我们也可以换一种方法来验证一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { int arr[10] = { 0 }; printf("%p\n", arr); printf("%p\n", arr+1); printf("%p\n", &arr[0]); printf("%p\n", &arr[0]+1); printf("%p\n",&arr); printf("%p\n", &arr+1); return 0; }
运行结果:
我们知道整型的地址通过+1操作,每次跳过4个字节,但是对&arr+1跳过了40个字节,这也可以说明&arr取出的就是整个数组的地址。
3.3数组指针的使用
到这里我们知道了&arr取出的是数组的地址,而前面说过数组指针是存放数组的地址的指针变量,那我们就可以把&arr用数组指针存起来。
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int(*p)[10] = &arr;//数组的地址,存储到数组指针变量 return 0; }
注意:其中&arr的类型是 int (*) [10],刚刚前文编译器中显示的类型 int [10]*写法是错误的
我们也可以通过数组指针来访问数组元素,
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int(*p)[10] = &arr;//数组的地址,存储到数组指针变量 int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *((*p) + i)); } return 0; }
运行结果:1 2 3 4 5 6 7 8 9 10
p中存放的是&arr,*p可以得到arr,然后*(arr+i)就可以访问数组元素。
其实这里*p就相当于arr,我们也可以通过(*p)[ i ],来访问数组元素。
上面使用数组指针看起来十分别扭,实际上的数组指针也不是这么使用的,它一般在二维数组上使用。
前面我们在写一个具有打印二维数组功能的函数时,是这样写的:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> Print(int arr[3][5], int r, int c) { int i = 0; for (i = 0; i < r; i++) { int j = 0; for (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); return 0; }
运行结果:
而学了数组指针后,我们可以这样写:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> Print(int (*p)[5], int r, int c) { int i = 0; for (i = 0; i < r; i++) { int j = 0; for (j = 0; j < c; j++) { printf("%d ", *(*( p + 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); return 0; }
分析一下上述数组指针的用法:
我们说可以将二维数组的每一行看做二维数组的一个元素,每一行又是一个一维数组,所以,二维数组其实是一维数组的数组,而二维数组的数组名也是数组首元素的地址,所以我们在数组传参时传的arr是二维数组首元素的地址,即第一行的地址,即一维数组的地址,既然是数组的地址,我们在接收时,形参就可以用数组指针。
在访问数组元素时用*(*( p + i ) + j),其中数组指针变量p中存放的是二维数组第一行的地址,通过 *(p + i)得到每一行首元素的地址,然后通过*(p + i) + j 得到每一行每个元素的地址,最后整体解引用 : *(*(p + i) + j)就可以访问二维数组中的元素。
4.数组参数、指针参数
4.1一维数组传参
一维数组传参,形参部分可以是数组,也可以是指针:
void test1(int arr[10], int sz) {} void test2(int* p, int sz) {} int main() { int arr[10] = { 0 }; test1(arr, 10); test2(arr, 10); return 0; }
其实写成数组形式的int arr[10]本质上还是指针int*p
以上就是一维数组传参,看下面一段代码,判断一维数组传参时形参的写法是否正确?
#include <stdio.h> 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); }
答案是全都正确,前面几个相信大家已经掌握,主要来看一下最后一个int **arr为什么正确?
因为int *arr2[20]是一个指针数组,它里面存放的都是int*型的指针,我们说arr是数组首元素的地址,那就是指针的地址,而一级指针的地址可以用二级指针接收,所以形参写成int **arr是正确的。
总结一下就是:指针数组的传参,形参可以用二级指针接收。
4.2二维数组传参
二维数组传参,形参的部分可以是数组,也可以是指针:
void test3(char arr[3][5],int r,int c) {} void test4(char(*p)[5],int r,int c) {} int main() { char arr[3][5] = { 0 }; test3(arr, 3, 5); test4(arr, 3, 5); return 0; }
因为数组名arr是一行的地址(即一维数组的地址),所以可以用数组指针作为形参接收。
以上就是二维数组传参,看下面一段代码,判断二维数组传参时形参的写法是否正确?
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); }
答案依次是:正确,错误(二维数组行可以省略,列不能省略),正确,错误,错误,正确,错误。(实参arr是一行的地址,只能用二维数组或者数组指针接收)。
4.3一级指针传参
很简单,一级指针传参就用一级指针接收就行。
#include <stdio.h> 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]); //一级指针p,传给函数 print(p, sz); return 0; }
现在来思考一个问题,我们已经写好了一个函数如下图,那要调用这个函数,都可以传给它什么参数呢?
void test(char* p) { ; }
这个能传的就多了,可以传给它变量的地址、数组名或者一级指针,
int main() { char ch = 'w'; char* ptr = &ch; char arr[10] = { 0 }; test(&ch);//变量地址 test(ptr);//一级指针 test(arr);//数组名 }
4.4二级指针传参
同样的二级指针传参就用二级指针接收:
#include <stdio.h> void test(int** ptr) { printf("%d\n", **ptr); } int main() { int n = 10; int* p = &n; int** pp = &p; test(pp); test(&p); return 0; }
可见二级指针和一级指针的地址都可以用二级指针接收。
再次来思考一个问题,如果已经写好了一个函数如下图,那可以传给它什么参数呢?
void test(char** ptr) { ; }
除了上面已经说过的二级指针和一级指针的地址,指针数组的数组名也可以用二级指针接收。
int main() { char ch = 'a'; char* p = &ch; char* pp = &p; char* arr[10] = { 0 }; test(pp);//二级指针 test(&p);//一级指针的地址 test(arr);//指针数组的数组名 }
5.函数指针
上文我们说过数组指针是指向数组的指针,类比可知函数指针就是指向函数的指针。
我们讲数组的时候说过,&数组名得到数组的地址,那如果我们要得到一个函数的地址呢?
同样的,&函数名即可,类似于数组名是数组首元素地址,函数名也是函数的地址。
那要将函数地址用一个变量存放就要用到函数指针了,函数指针的写法如下:
#include<stdio.h> int Add(int x, int y) { return x + y; } int main() { int (*pf)(int, int) = &Add;//pf是函数指针变量 return 0; }
(*pf)前后分别是函数返回类型和函数参数类型,函数指针的类型是int (*) (int,int),这类似于数组指针的写法。
下面再举个例子:
#include<stdio.h> void test(char* pc, int arr[10]) { ; } int main() { void (*pf)(char*, int[10]) = &test; return 0; }
它的函数指针类型是 void (*) (char*,int [10])。
学了函数指针,我们就可以换一种调用函数的方式:
int Add(int x, int y) { return x + y; } int main() { //法一: int r = Add(3, 5); printf("%d ", r); //法二: int (*pf)(int, int) = &Add;//pf是函数指针变量 int n = (*pf)(4, 5); printf("%d\n", n); return 0; }
打印结果:8 9
我们说Add和&Add都是函数的地址,当写成Add的时候,调用函数可以直接写成 pf(4,5)。
学了函数指针,我们来看下面这两段有趣的代码:
//代码1 (*(void (*)())0)(); //代码2 void (*signal(int , void(*)(int)))(int);
看起来很复杂,让我们来分析一下:
代码1:( *(void (*)() )0 ) ();
先看0,这里的0可以被看做一个数字,也可以被看做一个地址,当它被看做数字时是int型,被看做地址时是int*型的。代码中的void (*)()其实就是函数指针类型。
所以代码1的功能就是:调用0地址处的函数
第一步,先将0强制类型转换为void (*)()的函数指针。
第二步,调用0地址出的这个函数。(函数没有传参)。
代码2:void (*signal(int , void(*)(int)))(int);
代码2其实是signal函数的声明:
signal函数有两个参数,其中一个参数的类型是int,另一个参数的类型是函数指针类型void(*)(int),该函数指针指向的函数有一个参数,该参数的类型是int ,返回类型是void。
signal函数的返回类型也是void(*)(int)函数指针类型,该函数指针指向的函数也是只有一个参数,该参数的类型是int ,返回类型是void。
代码2看上去十分复杂,其实我们可以对其进行简化,这里就不得不提一下曾经学过的类型重命名标识符typedef了:
typedef可以将类型重命名:
typedef unsigned int unit; typedef int* ptr_t; int main() { unit u1; ptr_t p1; return 0; }
使用时就可以直接用重命名后的unit 和 ptr_t。
同样的,也可以使用typedef将函数指针类型 void (*)(int)重命名为ptr_t,注意命名时要将ptr_t放在中间,即 typedef void (*ptr_t)(int)
那代码2就可以简化如下:
typedef void (*ptr_t)(int); ptr_t signal(int, ptr_t);
这就是typedef对代码的简化作用。
前面我们学了可以将整型指针放在数组中,可以将字符指针放在数组中,那函数指针能不能放在数组中呢?那就是函数指针数组?
这将在下节内容讲解,今天就学到这里,未完待续。。。