数组与指针复习
- 写在前面
- 数组和指针
- 指针基础概念
- 进阶知识
- 指针的分类
- 指针和数组笔试题
写在前面
数组和指针小节,主要分为以下关键点:
- 常见指针分类,如指针数组、数组指针、函数指针等。
- 什么是数组/ 指针
- 有关数组和指针的题目
- 数组传参
我们也会围绕这些关键点来梳理复习。
数组和指针
指针基础概念
- 指针是一种特殊的变量,用来存放地址,这个地址可以唯一标识一块内存空间。
- 指针的大小固定,32位机器4字节,64位机器8字节。
- 指针有类型,指针的类型决定了指针加减整数要跳过多少字节。也会影响解引用时候的权限(可以在内存中取到多少位的内容)。
- 指针的运算。
- 指针 ± 整数
- 指针 - 指针
- 指针的关系运算
- 野指针的成因和规避手段:
- 成因:
- 指针没有初始化,默认为随机值
- 指针越界访问
- 指针指向的空间释放
- 规避手段
- 指针创建时初始化
- 小心指针的越界
- 指针指向空间释放后立即置空指针
- 避免返回局部变量的地址(局部变量出作用域销毁)
- 指针使用之前检查有效性
- 成因:
- 指针和数组关系
- 数组名表示的是数组首元素的地址(两个例外)
- sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名代表整个数组。
- &数组名,数组名代表整个数组。
- 直接通过指针访问数组中的元素
- 数组名表示的是数组首元素的地址(两个例外)
- 二级指针:指针变量也是变量,也有地址,存放指针的地址的变量就是二级指针。
进阶知识
指针的分类
- 字符指针 char*,使用如下:
//usage 1:
int main()
{
char ch = 'W';
char * pc = &ch;
*pc = 'w';
return 0;
}
//usage 2:
const char* pstr = "hello world.";
printf("%s\n", pstr);
//put a string into a pointer
注意第二种,是把字符串的首字符放到了pstr中,通过首字符的地址就可以找到整个字符串。注意,如果用相同的常量字符串初始化不同的字符指针,指向相同的常量字符串。因为常量字符串在创建时被储存在常量区。指针指向同一个字符串时,实际指向相同的内存。但如果用相同常量去初始化数组,不同数组开辟出不同的内存块,储存在堆区。
注意第二种,是把字符串的首字符放到了pstr中,通过首字符的地址就可以找到整个字符串。注意,如果用相同的常量字符串初始化不同的字符指针,指向相同的常量字符串。因为常量字符串在创建时被储存在常量区。指针指向同一个字符串时,实际指向相同的内存。但如果用相同常量去初始化数组,不同数组开辟出不同的内存块,储存在堆区。
- 指针数组,存放指针的数组。
int* arr1[10]; //整型指针数组
char** arr2[10]; //二级字符指针的数组
- 数组指针,存放数组的指针。
int (*p)[10];
// *p -> int[10] 指向一个整型数组的指针,数组指针
char* (*pc)[20];
// *p -> char*[20] 指向一个字符指针数组的指针,数组指针
&数组名 和 数组名 的对比
&数组名 和 数组名,虽然打印出来的都是相同的地址,但是意义不同。
&数组名表示的是数组的地址,而数组名是代表数组首元素的地址。
&数组名的类型是: int(*)[10],是数组指针类型。 &数组名+1条过了整个数组。
数组名代表首元素的地址,但是二维数组的首元素地址实际上是第一行的地址,即一维数组的地址。所以如果传参传二维数组,可以传数组名,并以二维数组接收,也可以传递数组名用数组指针接收。
//二维数组
int arr[4][4] = {1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4};
//接收1
void print1(int arr[4][4],int row, int col);
//传参1
print1(arr,4,4);
//接收2
void print2(int(*arr)[4], int row, int col);
//传参2
print2(arr,4,4);
//小练习
int arr[5]; //数组
int *parr1[10]; //指针数组
int (*parr2)[10]; //数组指针
int(*parr3[10])[5]; // 5个(10个元素的指针数组)的指针 (数组指针)
-
数组和指针传参
-
一维数组传参
形参设置可以传指针,可以传数组
void test(int* arr); void test(int arr[10]);
-
二维数组传参
形参设置可以传二维数组,可以传数组指针
注意:传二维数组必须要知道一行多少元素,可以不知道有多少行。否则无法解析数组。
//传二维数组 void test(int arr[][5]); void test(int arr[3][5]); //传数组指针 void test(int(*parr)[5]);
-
一级指针传参
一个函数的参数部分为一级指针时,函数可以接收同类型数组,以及一级指针。
-
二级指针传参
一个函数的参数部分为二级指针时,函数可以接收一个一级指针的指针(&一级指针),一级指针数组的数组名。
void test(int** ptr); int main() { int n = 100; int * pn = &n; int ** ppn = &pn; test(ppn); //传二级指针 test(&pn); //传一级指针的地址 int* arr[10]; test(arr); //传一个一级指针数组的数组名 }
-
-
函数指针
void test(); //对应的函数指针为 void(*pfunc)();
-
函数指针数组
把函数地址存放到一个数组中去,就是函数指针数组,定义方法是
int (*parr1[10])(); // parr1先与[]结合,代表是一个数组。 // 数组的类型为 int(*)();
函数指针数组作用有限,一个用途是转移表。
#include <stdio.h> 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; do{ printf( "*************************\n" ); printf( " 1:add 2:sub \n" ); printf( " 3:mul 4:div \n" ); printf( "*************************\n" ); printf( "请选择:" ); scanf( "%d", &input); switch (input) { 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; case 0: printf("退出程序\n"); breark; default: printf( "选择错误\n" ); break; } } while (input); return 0; }
如果使用函数指针数组实现,就简单很多。
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; }
可以发现简单很多,如果需要添加功能只需要改变数组中元素的个数,并将函数名填入其中即可。
-
指向函数指针数组的指针
void gtest(const char* str) { printf("%s\n", str); } int main() { void(*pfun)(const char*) = test; //函数指针 void(*pfunArr[5])(const char*); pfunArr[0] = test; //函数指针数组 void(*(*ppfunArr)[5])(const char*) = &pfunArr; //定义函数指针数组的指针。 return 0; }
-
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址),作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。
qsort就是c语言中比较常用的一个回调函数。用于数组的排序。
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*)); //参数含义: // base : 需要比较的数组名 // num : 数组中的元素个数 // size : 数组中元素大小 // compar : 调用者自己实现的一个函数,用于比较大小。
举个例子使用它。
void* compareFunc(const void* p1, const void* p2) { return (*(int*)p1 - *(int*)p2); } int main() { int arr[] = {1,3,4,5,9,2,7,0}; int i = 0; qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), compareFunc); for(int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) { printf(" %d ", arr[i]); } return 0; }
指针和数组笔试题
- 强化训练数组名,&数组名等内容。
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
//16,sizeof数组名代表整个数组的大小
printf("%d\n",sizeof(a+0));
//4/8,a+0代表指针指向数组的第一个元素,仍是指针大小
printf("%d\n",sizeof(*a));
//4,既然不在两个例外,a就代表首元素,*a代表首元素地址解引用,就是首元素的大小,4
printf("%d\n",sizeof(a+1));
//4/8(32位/64位), sizeof(pointer)
printf("%d\n",sizeof(a[1]));
//4,sizeof加数组中的一个元素等于sizeof(int)
printf("%d\n",sizeof(&a));
//a取地址,指针大小,4/8
printf("%d\n",sizeof(*&a));
//&a代表的是整个数组取地址,后面解引用,就是整个数组的大小16
printf("%d\n",sizeof(&a+1));
//&a代表整个数组取地址,+1跳过整个数组,但是还是指针的大小,4/8
printf("%d\n",sizeof(&a[0]));
//&a[0]代表首元素取地址,指针大小,4/8
printf("%d\n",sizeof(&a[0]+1));
//&a[0]+1,代表首元素取地址然后+1跳过首元素,就是a[1]的地址,4/8
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//6,整个数组的大小
printf("%d\n", sizeof(arr+0));
//4/8,指针大小,指向第0个元素
printf("%d\n", sizeof(*arr));
//1,首元素的大小
printf("%d\n", sizeof(arr[1]));
//1,第二个元素arr[1]的大小
printf("%d\n", sizeof(&arr));
//4/8,指针大小,代表整个数组的地址
printf("%d\n", sizeof(&arr+1));
//4/8指针大小,代表这个数组后面的地址
printf("%d\n", sizeof(&arr[0]+1));
//4/8指针大小,指向第0个元素之后
printf("%d\n", strlen(arr));
//大于等于6的随机值,没有'\0'。
printf("%d\n", strlen(arr+0));
//大于等于6的随机值,没有'\0'。
printf("%d\n", strlen(*arr));
//arr是首元素的地址,*arr就是首元素,a是97,会把97当成一个地址向后查找,可能会出现野指针问题。段错误
printf("%d\n", strlen(arr[1]));
//错误,arr[1]是第二个元素,b-98,错误
printf("%d\n", strlen(&arr));
//&arr代表整个数组的地址。随机值,和strlen(arr)相同
printf("%d\n", strlen(&arr+1));
//随机值
printf("%d\n", strlen(&arr[0]+1));
//随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//7,sizeof数组名代表整个数组的大小
printf("%d\n", sizeof(arr+0));
//arr+0 代表首元素的地址,指针大小4/8
printf("%d\n", sizeof(*arr));
//*arr代表首元素地址解引用,就是a的大小,1
printf("%d\n", sizeof(arr[1]));
//b的大小,1
printf("%d\n", sizeof(&arr));
//指针大小,4/8
printf("%d\n", sizeof(&arr+1));
//指针大小,4/8
printf("%d\n", sizeof(&arr[0]+1));
//指针大小,4/8
printf("%d\n", strlen(arr));
//6,""末尾有隐藏'\0'
printf("%d\n", strlen(arr+0));
//6
printf("%d\n", strlen(*arr));
//arr是首元素的地址,*arr就是首元素,a是97,会把97当成一个地址向后查找,可能会出现野指针问题。段错误
printf("%d\n", strlen(arr[1]));
//arr[1]代表b,第二个元素,把98当成一个地址向后查找,野指针。
printf("%d\n", strlen(&arr));
//&arr整个数组的地址,随机值,和strlen(arr)相同
printf("%d\n", strlen(&arr+1));
//随机值
printf("%d\n", strlen(&arr[0]+1));
//随机值
char *p = "abcdef";
printf("%d\n", sizeof(p));
//p是一个字符指针,指针大小4/8
printf("%d\n", sizeof(p+1));
//p类型是char*,p+1指针大小,4/8
printf("%d\n", sizeof(*p));
//*p是char类型,sizeof(char) = 1
printf("%d\n", sizeof(p[0]));
// sizeof(char) = 1
printf("%d\n", sizeof(&p));
//p是char*,&p是char**,指针大小4/8
printf("%d\n", sizeof(&p+1));
//&p+1,指针大小
printf("%d\n", sizeof(&p[0]+1));
//&p[0]就是char*类型,+1后,指针大小4/8
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));
//&p[0]是char*类型,等于5
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
//sizeof数组名为 整个数组的大小,是12*4 = 48
printf("%d\n",sizeof(a[0][0]));
//一个int的大小,4
printf("%d\n",sizeof(a[0]));
// a[0]单独存在,就是代表整个数组,在二维数组中,a[0]代表第一行,4*4 = 16
printf("%d\n",sizeof(a[0]+1));
// a[0]并非单独存在,代表首元素的地址,就是a[0][0]的地址,指针大小
printf("%d\n",sizeof(*(a[0]+1)));
//a[0]+1,就是a[0][1]的地址,解引用后就是sizeof(int) = 4
printf("%d\n",sizeof(a+1));
//a并非单独存在,代表首元素地址,这里指首行的地址,加1仍为指针大小。
printf("%d\n",sizeof(*(a+1)));
//a并非单独存在,代表首行的地址,+1代表第二行的地址,*后为16
printf("%d\n",sizeof(&a[0]+1));
//&a[0],代表第1行的地址,+1仍为指针大小
printf("%d\n",sizeof(*(&a[0]+1)));
//代表第一行的地址,解引用后变为了16
printf("%d\n",sizeof(*a));
//a并非单独存在,代表首行地址,*后,16
printf("%d\n",sizeof(a[3]));
//a[3]是代表第4行的大小,总共四个元素,16
-
计算程序的运行结果:
int main() { int a[5] = {1,2,3,4,5}; int *ptr = (int*)(&a + 1); printf("%d, %d", *(a + 1),*(ptr - 1)); return 0; }
分析:
a为一个5元素的数组,&a代表整个数组的地址,&a+1为跳过a数组的地址,强制转化为int*类型,ptr-1就指向a最后一个元素的地址。a+1代表着首元素后面一个元素的地址。所以结果为:2,5.
-
计算程序的运行结果:
struct Test{ int Num; char* pcName; short sDate; char ch[2]; short sh[4]; }*p; //假设结构体的大小为20个字节 //假设p的值为Null(0x0).求下面表达式的值。 int main() { printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }
分析:
p本身的类型为struct Test*,
p + 0x1,向后跳过多少个字节,本身是根据p的类型决定的。p的类型是struct Test*,+1就是跳过一个 struct Test的大小。所以0x14
(Unsigned long)p 类型是无符号long,其实就是把地址当作整数去加减,而不能用指针加减整数的规则。所以就是0x1
(unsigned int*)p 类型是 unsigned int星,+1就是跳过一个unsigned int的大小,所以结果为0x4 。
-
计算程序的结果:
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; }
分析:
a是一个int数组,&a代表整个数组的地址,+1代表指向数组之后的内存空间。用(int*)强转后,指向不变。ptr1[-1]就等价于 *(ptr1 - 1),可以发现就是4。
ptr2略微麻烦一些。a强转成int后,+1就变成了直接加1,比如原来a的地址为0x0,(int)a + 1就是 0x1,相当于是在内存中往后面跳过了一个字节的大小,如图所示。(默认内存小端存储)
-
计算程序的结果:
int main() { int a[3][2] = { (0,1), (2,3), (4,5) }; int* p = a[0]; printf("%d", p[0]); return 0; }
分析:
a是一个二维数组,二维数组在内存中也是按照顺序连续排放的。
但是,二维数组的定义方式是{ {0,1}, {2,3}, {4,5} }; 而不是小括号,而小括号的含义里面是 逗号表达式,(0,1) 是1,(2,3)是3,(4,5)是5。
所以a这个三行二列的二维数组的真实组成实际上是:{1,3,5,0,0,0}
a[0] 代表的是a[0]数组的首元素地址,p[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; }
分析:
int(*p)[4]; p = a;实际上就是把a看成是四列的数组去展开,图解这个题目。
-
int main() { char* a[] = {"work", "at", "home"}; char** pa = a; pa++; printf("%s\n", *pa); return 0; }
分析:
char* 的数组,里面有三个元素"work" “at” 和 “home”。pa是char**类型,pa++就是越过一个char星的大小,所以pa不再指向work,而是指向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; }
分析:
解析如图。
数组与指针小节完。