目录
一、本章目标
二、指针
✨2.1指针是什么?
✨2.2指针有什么用?
✨2.3取地址取的是哪个地址?
✨2.4指针类型
✨2.5指针类型的意义
✨2.6指针-指针
✨2.7野指针
✨2.8指针和指针变量的区别
✨2.9一级到N级指针
✨2.10数组指针
✨2.11函数指针
三、数组
✨3.1数组的概念
✨3.2一维数组的初始化
✨3.3数组名的意义
✨3.4一维数组与sizeof的那些事
✨3.5二维数组
3.5.1二维数组的创建
3.5.2二维数组的初始化
3.5.3二维数组与sizeof的那些事
✨3.6 从三维到N维数组
✨3.7数组传参
四、指针和数组笔试题解析
✨4.1指针与数组关于sizeof和strlen的事
✨4.2指针笔试题
- 极速解决一级至n级指针
- 极速解决一维至n维数组
- 极速解决数组与指针联系
- 指针和数组笔试题解析
谈到指针,不得不谈内存,内存是一块大的用来存储数据的空间,它被分为了一小块一小块的内存单元,每一块内存单元的大小为1字节,为方便使用和区分内存单元,每一块内存单元有一个独立的编号,以32位机器为例,这个独立的编号由32位二进制组成(1字节等于8个二进制位),即4字节。如果是64位机器的话,这个独立的编号是64位二进制组成(1字节等于8个二进制位),即8字节。而这个编号就是我们说的地址,也被称作指针。(这一点要牢记)。
图解:
内存单元大小为一个字节,这里的11 22 33 44 55 66 77均为16进制数。(一位16进制数等于4位2进制数)
以32位机器为例,由于地址是32位二进制组成的,那么就有2^32种不同的地址,那么就有2^32个内存单元,因为内存单元的大小是一个字节,那么内存的大小也可以算出来,即2^32Byte,也等于4GB。
总结:
- 指针是地址,是内存单元的编号。(这一点非常重要)
- 32位机器下,指针大小为4字节
- 64位机器下,指针大小为8字节
可以取出某个变量的地址,方便以后操作那块地址空间,为什么不用变量名直接操作那块空间呢?
因为有时我们拿不到变量名,比如在函数传参时,我们在函数体内部是无法拿到变量名的,函数传参只能传数据,地址是数据,变量名传的是变量名中的数据,而不是变量名本身。
我们知道int在内存开辟4个字节,占用4个内存单元,每个内存都有一个编号,即都有一个地址,那&a,取的是4个地址中的哪个地址呢?
c语言规定取变量的地址,取的是众多地址中最小的地址。
图解:
介绍解引用操作符 *(间接访问操作)
*(地址)就能找到那块地址空间,为什么不说拿到那块空间的数据呢?因为当*(地址)做左值时,它代表的是那块空间,当*(地址)做右值时,代表的是那块空间的数据。(理解左值和右值这一点非常重要)
简单运用一下指针
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
char a = 'B';
char* pa = &a;
printf("%c\n", *pa);
return;
}
解读代码:
pa拿到变量a的地址,通过解引用操作拿到变量a管理的那块空间。之后*pa的作用与a相同,可以认为*pa等价于a,使用a和使用(*pa)没任何区别。
第一点牢记:
- char*类型的指针,对其接引用访问1个字节。
- int*类型的指针,接引用访问4个字节。
- double* 类型的指针,接引用访问8个字节。
以此类推。
详细点:
以int*为例如果指针的值是0x 00 1F EA 88,那么解引用访问的就是从该地址处向高地址处访问4个字节。
图解:
第二点牢记:
- char*类型的指针,对其+1跳过1个字节。
- int*类型的指针,对其+1跳过4个字节。
- double* 类型的指针,对其+1跳过8个字节。
以此类推
以int* p为例,其他以此类推。
图解:
总结:
1:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
2:指针的类型决定了指针向前或者向后走一步有多大(距离)。
这两点就是语法规则,要牢记与心
牢记:指针-指针(得相同类型的指针)的值为它们之间相差的元素个数。
以int* 为例:
此时p2-p1等于2(经过两个整形的大小)
其他以此类推(篇幅有限)
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因用户只能对以申请的内存空间进行访问
1.指针未初始化:未初始化的指针指向是随机的,不能访问。
2.指针越界访问:访问不属于用户申请的空间,可能引起错误,很危险。
3指针指向的空间释放:释放了的空间不属于用户,不能访问。
如何规避野指针?
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
指针是地址,是编号,是一串二进制数。
指针变量是存放指针的变量,就如同整形变量是存放整形的变量。也可叫做地址变量,不过一般不这么叫。
一般我们口头说的指针指的是指针变量,这一点我们心里要明白。
一维指针变量存的是如int 、double、float、char、long等变量的地址。
二级指针变量存的是一维指针变量的地址。
三维指针变量存的是二维指针变量的地址。
一般使用较多的是一、二级指针(熟练掌握),三维及以上的指针很少碰到。
N级指针以此类推
类比于整形指针,整形指针是指向整形的指针,结构体指针是指针结构体的指针。
数组指针是指向数组的指针。
数组指针的表示:
如int arr[10]={0};
指向该数组的指针可写为:int (*p)[10]=&a;(只需把(*p)代替掉arr即可)
解释:
首先()的优先级大于[ ],*和p先结合成*p,说明它是指针,去掉变量名就是它的类型。
它的类型是int()[10]。类型代表着该指针p指向10个元素的数组,每个元素是int类型的变量。
其他以此类推
这里写个数组指针类型的数组。(赏析)
int (*p[10])[5];
解释
*的优先级低于[ ],p先与[ ]结合,p[10]表面这是一个数组,它有10个元素,
它的类型是int(*)[5]。这个我们在上面见过,它是一个数组指针类型,该数组指针指向有5个元素的数组,每个元素是int类型。
该指针+1跳过一个数组的长度。(需记忆)
对数组指针解引用等价于该数组的变量名。(需记忆)
如int arr[10]={1,2,3,4,5,6,7,8,9,10}
&a等于取的是整个数组。
指向它的数组指针为int (*p1)[10]=&a;
这个变量的变量名是p1,变量的类型是int(*)[10].
p1+1跳过整个数组。
//这里需要对数组有一定的了解。
图解(p1+1)跳过整个数组
这是这个指针类型赋予数组指针的意义。
函数指针顾名思义:指向函数的指针
这里必须要说明的是函数也是有地址的
函数名或者&函数名都是该函数的地址
函数指针的类型:
以 void print(int n)为例
类似于数组指针,将(*p)替换函数名即可:void (*p) (int)=&print;(这里&print和print都可以)。
调用函数时,我们可以(*p)(3),也可直接p(3)。
由于篇幅原因,这里只讲解了函数指针的语法,并没有讲解它的使用环境。(函数指针通常用于回调函数,有兴趣的话博主可以下次更新这部分的内容)
首先声明:指针和数组完全是两个不同的东西。但它们之间在使用时又密不可分。
数组概念:一组相同类型元素的集合。可以是整形,浮点型,也可以是结构体。
而指针就是地址,是编号。
(1)int arr1[10] = {1,2,3};
(2)int arr2[] = {1,2,3,4};
(3)int arr3[5] = {1,2,3,4,5};
(4)char arr4[3] = {'a',98, 'c'};
(5)char arr5[] = {'a','b','c'};
(6)char arr6[] = "abcdef";
总结:
1.如果数组在创建的时候指定了大小,就开辟该大小的空间,如果赋予的值少于空间数目,则其他剩余空间的值都会被置于0.
2.数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
3.直接使用字符串赋值,字符串自带'\0'这一结束标志,开辟数组时也要为'\0'开辟空间。
数组名:就是首元素的地址。
注意:数组名可以看作一个常量(首元素的地址),它不能作为左值,只能作为右值。
左值:能给作为赋值运算符(=)左边的值即为左值。
右值:能给作为赋值运算符(=)右边的值即为右值。
但两个例外
1.sizeof(arr)中arr代表整个数组。
2.&arr代表整个数组的地址。
例:int arr[10]={1,2,3,4,5,6,7,8,9,10};
计算整个一维数组的大小:sizeof(arr);
计算一维数组元素的大小:sizeof(arr[0]);
计算一维数组的元素个数:sizof(arr)/sizeof(arr[0]);
//数组创建
int arr1[3][4];
char arr2[3][5];
double arr3[2][4];
//数组初始化
int arr1[3][4] = {1,2,3,4};
int arr2[3][4] = {{1,2},{4,5}};
int arr3[][4] = {{2,3},{4,5}};
(1)计算整个二维数组的大小:sizeof(arr);
(2)计算二维数组的行数:sizeof(arr)/sizeof(arr[0]);
(3)计算二维数组的列数:sizeof(arr[0])/sizeof(arr[0][0]);
(4)计算二维数组元素的大小:sizeof(arr[0][0]);
需了解以下知识
1.数组名就是首元素地址
此时二维数组的首元素需要特别指出:
以arr[ 3 ][ 4 ]为例
它是有3个元素的数组,每个元素是一个一维数组int arr[ 4 ]。
此时二维数组的首元素的地址是int arr[4]的地址,即数组的地址,类型为int (*) [ 4 ]。
(本文章下面的标题:深度剖析有详解)
2.变形公式:arr[ i ] <==> *(arr + i) arr[ i ][ j ]<==> *(*(arr+i)+j)
详解:
(1)sizeof(数组名)就是计算的整个数组的大小(需要记住)。
(2)sizeof(arr[0]):根据变形公式arr[0]等价于*(arr+0),即(*arr)。
arr是数组指针类型(int (*) [ 4 ]),对数组指针解引用后就相当于该数组的变量名,(这点要牢记)。
而sizeof(数组名)就是该数组的大小,即4*4=16Byte。
而sizeof(arr)/sizeof(arr[ 0 ])=(4*3*4)/16=3。
(3)sizeof(arr[0])/sizeof(arr[0][0])=16/4=4。
(4)sizeof(arr[0][0])=4。
深度解剖:
二维数组本质是线性的一维数组,如int arr[3][4];相当于int arr[3],它的每个元素是一个int arr[4]的数组
图解:
我们知道二维数组可用二维坐标表示。三维数组可用三维坐标,四维该怎么表示呢?
其实都可以看作一维的数组。
如int arr[ 3 ][ 2 ][ 4 ] 三维数组。
看作3个元素的一维数组,每个元素是二维数组。
图解:
数组传参分两种:
1.传数组,用数组接收。
2.传数组,用指针接收。
第一种:传数组,用数组接收
一维数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include
void print(int arr[],int n)//该arr[]中的[]中填大于0的值或者不填都可以。
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6 };
int len = sizeof(arr) / sizeof(arr[0]);
print(arr, len);
return 0;
}
//该arr[]中的[]中填大于0的值或者不填都可以。
二维数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include
void print(int arr[2][3],int r,int c)//行可以省略,列不能省略。
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("\n");
}
int main()
{
int arr[2][3] = { 1,2,3,4,5,6 };
int r = sizeof(arr) / sizeof(arr[0]);
int c = sizeof(arr[0]) / sizeof(arr[0][0]);
print(arr, r, c);
return 0;
}
//行可以省略,列不能省略。
第二种:传数组,用指针接收
一维数组:
void print(int* p, int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", p[i]);
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
int len = sizeof(arr) / sizeof(arr[0]);
print(arr, len);
return 0;
}
二维数组:
#define _CRT_SECURE_NO_WARNINGS 1
#include
void print(int(*arr)[3], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("\n");
}
int main()
{
int arr[2][3] = { 1,2,3,4,5,6 };
int r = sizeof(arr) / sizeof(arr[0]);
int c = sizeof(arr[0]) / sizeof(arr[0][0]);
print(arr, r, c);
return 0;
}
总结:
1.传数组名时,可以用数组接收,传什么数组,就用什么数组接收(不建议省略形参中[ ]的内容 )。
2.一维数组名,要看该数组元素的类型,如果该数组的元素类型是int*,数组名又是数组首元素的地址,即int*的地址,那么接收该数组名就得用int**。
3.二维数组名:和一维数组名一样,需把二维数组看成一维数组,如int arr[3][4]。它有3个元素,每个元素是int arr[4]。那么数组名代表int arr[4]的地址,即数组的地址,那么需要用数组指针来接收。
//一维数组
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));
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
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));
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[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+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
编译器运行立马出答案,就不赘述了。
笔试题一:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?
笔试题二
#include
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
解析:
char** pa=a,pa++,该指针指向的是char* 类型,+1跳过一个char*,即一个数组元素,因此pa++指向数组第二个元素,*pa即拿到数组第二个元素,(“at”存储在数组的第二个元素的是'a'的地址),所以*pa就是'a'的地址,即字符串“at”的首地址,因此将打印出at。
由于篇幅原因,还有很多细节没有交代,之后会出更多c语言的系列,感谢关注。