目录
1. 指针是什么
指针就是地址,而用来存放地址的变量就叫做指针变量。
2. 为什么会有指针
我们知道在32位平台下有32根地址线,即最大能编址2^32个(每根地址线上的电信号为0或1)。而我们又知道内存被分成了很多个小的单元,这里的单元就是一个字节。每个字节都对应有1个地址,那么32位平台下最多能有2^32,即4G的内存。
试想一下,在没有编址的情况下,在一个4G的内存中,有两个字节存放了数据,此时想对这两个字节的数据进行运算,但是我们并不知道这两个字节的数据分别都在哪里,所以我们无法直接进行运算。但是如果有了地址,我们就可以根据该地址取出其对应的变量内容,所以我们把地址形象地称为指针。有了指针,我们就可以方便地访问内存了。
3. 指针变量与数组
这里首先声明,指针和数组没有任何关系
由于指针是地址,所以我们主要讨论的是指针变量和数组
指针变量(type *变量名=某变量的地址):
概念:用来存放地址的变量
类型:type *
例如:
int a = 10;
int *pa=&a;
此时的pa就是一个指针变量,存放的是变量a的地址,pa的类型为int *
数组(type 数组名[常量]=类型为type的变量的集合):
概念:一组具有相同数据类型的元素的集合
类型:type [常量]
例如:
char arr[4] = {‘a’, ‘b’, ‘c’, ‘d’};
此时的arr就是一个数组,其类型为 char [4]
3.1 元素访问
由于指针变量是存放地址的变量,那么指针变量也可以存放数组的地址
数组访问元素的方式:
- 数组名[下标]
- *(数组名+下标)
指针变量访问数组元素的方式:
- 指针变量名[下标]
- *(指针变量名+下标)
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main(){
int arr[] = { 1, 2, 3, 4, 5 };
int *pa = arr;
printf("%d\n", arr[0]);
printf("%d\n", *(arr + 0));
printf("%d\n", *(arr + 1));
printf("%d\n", pa[0]);
printf("%d\n", *(pa + 0));
printf("%d\n", *(pa + 1));
system("pause");
return 0;
}
结果如图:
3.2 传参
由于指针变量也是变量,所以在传参时候也会形成临时变量,但是和一般变量传参不同的是,指针变量传参是传址传参(指针变量和传参形成的临时变量都指向同一个地址,如果该临时变量对这块地址的内容作了改变,那么指针变量所指向地址的内容也会发生变化),一般变量传参是传值传参(形参的改变不会影响实参)。
而数组在传参时会发生降维,降维成指向其内部元素类型的指针变量,该指针变量存的是数组首元素的地址。为什么数组传参会发生降维呢? 我们都知道调用一个函数就会形成一个栈帧,此函数的参数虽为临时变量,但是也是要开辟空间的。那么如果数组传参不发生降维的话,传进来的将会是整个数组,在数组元素个数较多的情况下将要开辟很多空间,这样做无疑既耗时又耗空间,所以数组会降维成指针变量,只存数组首元素的地址。值得注意的是,如果想在被调用函数中访问调用函数中的数组元素的话,光传数组首元素的地址作为形参是不够的,还需要传入数组的元素个数
为什么不能在被调用函数中计算数组元素个数呢?
此时传入的只是数组首元素的地址,并不是整个数组,所以利用sizeof(数组名)/sizeof(数组首元素)的方法是不可行的。
(1)一级指针变量传参
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
void print(int *pa, int size){
for (int i = 0; i < size; i++){
printf("%d ", *(pa + i));
}
printf("\n");
}
int main(){
int arr[] = { 1, 2, 3, 4, 5 };
int *pa = arr;
int size = sizeof(arr) / sizeof(arr[0]);
print(pa, size);
system("pause");
return 0;
}
(2)二级指针变量传参
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
void test(int **ppa){
printf("%d\n", **ppa); //10
}
int main(){
int a = 10;
int *pa = &a;
int **ppa = &pa;
test(ppa);
system("pause");
return 0;
}
(3)一维数组传参
void test1(int arr1[], int size1){ //√
}
void test1(int arr1[5], int size1){ //√
}
void test1(int *arr1, int size1){ //√
}
void test2(int *arr2[], int size2){ //√
}
void test2(int *arr2[5], int size2){ //√
}
void test2(int **arr2, int size2){ //√
}
int main(){
int arr1[5] = { 1, 2, 3, 4, 5 };
int *arr2[5] = { 0 };
int size1 = sizeof(arr1) / sizeof(arr1[0]);
int size2 = sizeof(arr2) / sizeof(arr2[0]);
test1(arr1, size1);
test2(arr2, size2);
system("pause");
return 0;
}
(4)二级数组传参
void test(int arr[][2]){ //√
}
void test(int arr[2][]){ //×
}
void test(int arr[2][2]){ //√
}
void test(int (*arr)[2]){ //√
}
void test(int *arr[2]){ //×
}
void test(int **arr){ //×
}
int main(){
int arr[2][2] = { 0 };
test(arr);
system("pause");
return 0;
}
4. 指针变量的运算
4.1 指针变量+/-整数
给指针变量+(或-)整数,看起来是+(或-)整数,其实是+(或-)其所指向类型大小的整数倍
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main(){
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);
system("pause");
return 0;
}
结果如图:
4.2 指针变量-指针变量
两个指针变量相减的前提是两个指针变量指向同一个数组或同一个字符串(如果两个指针变量所指向的地址没有任何关系,那么相减也是没有任何意义的),且类型相同。两指针变量相减的结果是两指针变量之间所经历的元素个数,这里所求的元素个数要看指针变量的类型
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main(){
int arr[] = { 1, 2, 3, 4, 5 };
int *p1 = &arr[1];
int *p2 = &arr[4];
char *p3 = (char *)&arr[1];
char *p4 = (char *)&arr[4];
printf("%d\n", p2 - p1);
printf("%d\n", p4 - p3);
system("pause");
return 0;
}
结果如图:
4.3 解引用
指针变量的类型决定了该指针变量在进行解引用时可以访问几个字节,比如char *的指针变量解引用只能访问1个字节,而int *的指针变量解引用可以访问4个字节(32位平台下)
int main(){
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0x55;
*pi = 0;
system("pause");
return 0;
}
当执行到*pc=0x55时,n的变化:
当执行到*pi=0时,n的变化:
5. 多级指针变量
由于指针变量也是变量,是变量就有地址,所以存放指针变量地址的变量就叫做二级指针变量,存放二级指针变量地址的变量就叫做三级指针变量。
例:
int a = 10;
int *pa = &a; //一级指针变量
int **ppa = &pa; //二级指针变量
int ***pppa=&ppa; //三级指针变量
6. 复杂指针变量
(1)指针数组
指针数组是数组,其内部元素是指针,即地址。
如 int *a[10],char *a[10];
(2)数组指针
数组指针是指针。通俗的说,数组指针就是数组的地址。
如 int (*p)[10],由于()的优先级高于[ ],所以p先和*结合,说明p是一个指针变量,其指向一个大小为10个整型的数组。
(3)函数指针
函数指针即函数的地址,而函数名或 &函数名 就表示函数的入口地址。用来存放函数指针的变量即函数指针变量。
如 void (*add)( ),其中add表示函数名,由于add先与*结合,说明add是一个指针,指向一个函数,指向的函数没有参数,返回值类型为void。
(4)函数指针数组
函数指针数组是数组,其内部元素为函数指针,即函数的地址。
如 int (*p[10])( ),因为[ ]的优先级高于*,所以p先与[ ]结合,说明p是一个数组,其内部元素是 int (*)( )类型的函数指针。
(5)指向函数指针数组的指针
指向函数指针数组的指针是指针,指向数组,数组的内部元素是函数指针。
如 int (*(*p)[10])( ),由于p先与*结合,说明p是一个指针,指向一个数组,数组的内部元素是 int (*)( )类型的函数指针。
7. 回调函数
回调函数:将一个函数的地址作为参数传给另一个函数,前者所述的函数就叫做回调函数
例如利用回调函数my_compare实现冒泡排序:
int my_compare(const void *x, const void *y){
int a = *(int *)x;
int b = *(int *)y;
if (a > b){
return 1;
}
else if (a < b){
return -1;
}
return 0;
}
void swap(char *x, char *y,int size){
while (size--){
*x ^= *y;
*y ^= *x;
*x ^= *y;
x++, y++;
}
}
void my_qsort(void *base, int num, int size, int(*my_compare)(const void *x, const void *y)){
int flag = 0;
for (int i = 0; i < num; i++){
flag = 0;
for (int j = 0; j < num - 1 - i; j++){
if (my_compare((char *)base+j*size,(char *)base+(j+1)*size)>0){//指针+4个字节
swap((char *)base + j*size, (char *)base + (j + 1)*size,size);
flag = 1;
}
}
if (flag == 0){
break;
}
}
}
int main(){
int arr[] = { 3, 4, 1, 2, 5, 6, 0, 9 };
int num = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, num, sizeof(int), my_compare);
my_qsort(arr, num, sizeof(int), my_compare);
for (int i = 0; i < num; i++){
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
8. 指针与数组相关练习题
1.
#include <stdio.h>
#include <windows.h>
int main(){
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1); //此时&a表示整个数组的地址,加1表示跳过了整个数组,即ptr指向数组a的最后一个元素的下一个元素
printf("%d,%d\n", *(a + 1), *(ptr - 1));
system("pause");
return 0;
}
结果如图:
解析:
*(a+1):此时的数组名a表示首元素地址,加1表示第二个元素的地址,解引用后表示第二个元素
*(ptr-1):ptr此时的类型是int *,ptr-1看起来是减1,其实是减去其所指向类型的大小,即减去4个字节,指向数组a的最后一个元素,解引用表示数组a的最后一个元素
2.
struct Test
{
int Num;
char *pcName;
shortsDate;
char cha[2];
shortsBa[4];
}*p;
假设p 的值为0x100000。 如下表表达式的值分别为多少?
p + 0x1 = 0x___ ?
(unsigned long)p + 0x1 = 0x___ ?
(unsigned int*)p + 0x1 = 0x___ ?
p + 0x1 = 0x100014
解析:此时的p是一个结构体指针变量,p指向结构体,所以对p+1是p加上此结构体的大小,即加上20个字节,用16进制表示就是0x14
(unsigned long)p + 0x1 = 0x1000001
解析:此时的p已经被强转成为无符号长整型,p+1就是单纯的加1(此时的p已不再是指针变量)
(unsigned int*)p + 0x1 = 0x1000004
解析:此时的p被强转成为指向整型变量的指针变量,p+1是p加上整型变量所占字节的大小,即加上4个字节(32位平台下)
3.
#include <stdio.h>
#include <windows.h>
int main(){
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);//此时&a表示整个数组的地址,加1表示跳过了整个数组,即ptr指向数组a的最后一个元素的下一个元素
int *ptr2 = (int *)((int)a + 1);//此时的a表示首元素的地址,(int)a将a强转为了一个整型变量,加1就是单纯的加1。32位平台下每个整型变量都占4个字节,此时给强转后的a加1指向了数组a首元素的第二小地址字节
printf("%x,%x", ptr1[-1], *ptr2);
system("pause");
return 0;
}
结果如图:
解析:
ptr1[-1]:ptr1[-1]=*(ptr1-1),此时由于ptr1指向数组a的最后一个元素的下一个元素的,由于ptr1此时为int *型,所以ptr1-1表示ptr1减去4,即此时ptr1指向数组a的最后一个元素,解引用表示最后一个元素
*ptr2:
4.
#include <stdio.h>
#include <windows.h>
int main(){
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf("%d\n", p[0]);
system("pause");
return 0;
}
结果如图:
解析:
此时的二维数组a中前三个元素都是用圆括号括起来的逗号表达式,而逗号表达式的结果为最后一个表达式,所以a的前三个元素为1、3、5,后三个元素均为0,正如下图所示,所以int a[3][2]={{1,3},{5,0},{0,0}},而a[0]作为二维数组a第一行的数组名,p=a[0]表示p中存的是a[0][0]的地址,p[0]=*(p+0)表示a[0][0],p[1]=*(p+1)表示a[0][1]。
5.
#include <stdio.h>
#include <windows.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("a_ptr=%#p,p_ptr=%#p\n", &a[4][2], &p[4][2]);
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
system("pause");
return 0;
}
结果如图:
解析:
&a[4][2]表示a[4][2]的地址
p是一个数组指针变量,p的类型为int (*)[4],p[4][2]=*(*(p+4)+2),p+4表示p加上16个4字节,*(p+4)表示对p+4进行解引用,而*(p+4)的类型为int [4],也就是说*(p+4)是一个一维数组名,*(p+4)+2表示第二个元素的地址(从p+4所指向的地方再往后8个字节),*(*(p+4)+2)表示第二个元素,那么&p[4][2]就表示第二个元素的地址
由于两个指针相减表示的是两指针之间所经历的元素个数(高地址-低地址>0,低地址-高地址<0),所以如果以%d的形式输出&p[4][2] - &a[4][2]的结果则为-4,但是如果以%p的形式输出&p[4][2] - &a[4][2]的结果,此时存入内存的是-4的补码,即1111 1111 1111 1111 1111 1111 1111 1100,用%p的形式输出,由于地址没有负数,即是无符号16进制数,则为FFFFFFFC
6.
#include <stdio.h>
#include <windows.h>
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);//此时&aa表示整个数组的地址,加1表示跳过了整个数组,即ptr指向数组aa的最后一个元素的下一个元素
int *ptr2 = (int *)(*(aa + 1));//*(aa+1)=aa[1],表示第二行的数组名,即第二行首元素的地址
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
system("pause");
return 0;
}
结果如图:
解析:
*(ptr1-1):ptr1-1看起来是减1,其实是减4,则ptr1-1指向aa的最后一个元素,解引用表示最后一个元素
*(ptr2-1):ptr2-1看起来是减1,其实也是减4,则ptr2-1指向第一行的最后一个元素,解引用表示第一行的最后一个元素
7.
解释下⾯代码:
(*( void(*) ()) 0)()
void (*signal(int, void (*)( int )))( int )
(*( void(*) ()) 0)():void (*) ()是一个函数指针类型,( void(*) ()) 0表示对0进行强转,此时的0指向一个函数,*( void(*) ()) 0表示函数名,由于函数名()表示函数调用(圆括号表示参数列表),所以(*( void(*) ()) 0)()表示函数调用(此函数的参数列表为空)
void (*signal(int, void (*)( int )))( int ):将void (*) (int)用type代替,那么将其改写成:
type signal(int , type) 该函数的返回值和其中一个参数都是函数指针类型 所以此表达式的作用为函数声明
8.
#include <stdio.h>
#include <windows.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);//先执行++cpp,然后进行两次解引用
printf("%s\n", *--*++cpp + 3); //先执行++cpp,然后解引用,之后再前置--,接着再解引用,最后+3
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
system("pause");
return 0;
}
结果如图:
解析:
**++cpp:++cpp表示cpp+=1,即此时cpp指向cp的第二个元素,*cpp表示cp的第二个元素(“POINT”字符串地址的地址),**cpp表示”POINT”的地址,然后以%s的形式输出即为”POINT”字符串
*--*++cpp + 3:++cpp表示cpp+=1,cpp本来指向cp的第二个元素,++cpp后cpp指向cp的第三个元素,*++cpp后表示cp的第三个元素,即”NEW”字符串地址的地址,--*++cpp表示cp的第三个元素减1,此时由于cp的第三个元素也是指针类型,减1相当于减4,所以cp的第三个元素从刚开始指向c的第二个元素,即”NEW”字符串地址的地址,改为指向c的第一个元素,即”ENTER”字符串地址的地址,*--*++cpp表示”ENTER”字符串的地址,*--*++cpp此时为char*型,*--*++cpp+3表示从’E’字符开始,往后找字符串
*cpp[-2] + 3:*cpp[-2]=*(*(cpp-2)),cpp指向cp的第三个元素,cpp-2指向cp的第一个元素*(cpp-2)表示cp的第一个元素,即”FIRST”字符串地址的地址,*(*(cpp-2))表示”FIRST”字符串的地址,*(*(cpp-2))此时的类型为char *,*cpp[-2] + 3即从’S’字符开始,往后找字符串
cpp[-1][-1] + 1:cpp[-1][-1]=*(*(cpp-1)-1),cpp指向cp的第三个元素,cpp-1指向cp的第二个元素,*(cpp-1)表示cp的第二个元素,即”POINT”字符串地址的地址,此时*(cpp-1)的类型为char **,*(cpp-1)-1表示”NEW”字符串地址的地址,*(*(cpp-1)-1)表示”NEW”字符串的地址,*(*(cpp-1)-1)的类型为char *型,cpp[-1][-1] + 1表示从’E’字符开始,往后找字符串