目录
三、指针和数组(指针和数组没有任何关系,元素访问时具有相似性)
一、指针
1.指针是什么?(指针是地址)
在计算机科学中,
指针
(
Pointer
)是编程语言中的一个对象,利用地址,它的值直接指向
(
points to
)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为
“
指针
”
。意思是通过它能找到以它为地址
的
内存单元
。
那我们就可以这样理解:
![](https://img-blog.csdnimg.cn/05af0ceb199c4946ae380e2affeb2536.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAcXFfNTc4MjIxNTg=,size_20,color_FFFFFF,t_70,g_se,x_16)
指针
指针是个变量,存放内存单元的地址(编号)。
那对应到代码:
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//将a的地址存放在p变量中,p就是一个之指针变量。
return 0;
}
总结:
指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:
一个小的单元到底是多大?(
1
个字节)
如何编址?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
对于
32
位的机器,假设有
32
根地址线,那么假设每根地址线在寻址的是产生一个电信号正电
/
负电(
1
或
者
0
)
那么
32
根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就有
2
的
32
次方个地址。
每个地址标识一个字节,那我们就可以给
(
2^32Byte == 2^32/1024KB ==
2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB
)
4G
的空闲进行编址。
同样的方法,那
64
位机器,如果给
64
根地址线,那能编址多大空间,自己计算。
这里我们就明白:
在
32
位的机器上,地址是
32
个
0
或者
1
组成二进制序列,那地址就得用
4
个字节的空间来存储,所
以一个指针变量的大小就应该是
4
个字节。
那如果在
64
位机器上,如果有
64
个地址线,那一个指针变量的大小是
8
个字节,才能存放一个地
址。
总结:
指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在
32
位平台是
4
个字节,在
64
位平台是
8
个字节
。
2.为什么要有指针
提高效率,快速标定与定位。
3.指针和指针类型
这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类
型呢?
准确的说:有的。
当有这样的代码:
int num = 10;
p = #
要将
&num
(
num
的地址)保存到
p
中,我们知道
p
就是一个指针变量,那它的类型是怎样的呢?
我们给指针变量相应的类型。
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
这里可以看到,指针的定义方式是:
type + *
。 其实:
char*
类型的指针是为了存放
char
类型变
量的地址。
short*
类型的指针是为了存放
short
类型变量的地址。
int*
类型的指针是为了存放
int
类型变量的地址。
那指针类型的意义是什么?
(1)指针+-整数
#include <stdio.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);
return 0;
}
总结:指针的类型决定了指针向前或者向后走一步有多大距离。
(2)指针的解引用
//实例演示
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如:
char*
的
指针解引用就只能访问一个字节,而
int*
的指针的解引用就能访问四个字节。
3.野指针
概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1.
指针未初始化
2.
指针越界访问
3.
指针指向的空间释放
这里放在动态内存开辟的时候讲解,这里可以简单提示一下。
如何规避野指针
1.
指针初始化
2.
小心指针越界
3.
指针指向空间释放即使置
NULL
4.
指针使用之前检查有效性
#include <stdio.h>
int main()
{
int *p = NULL;
//....
int a = 10;
p = &a;
if(p != NULL)
{
*p = 20;
}
return 0;
}
4.指针合法性以及assert
c函数中指针合法究竟是什么意思?
判定指针是否为NULL决定的。
任何函数,都不可能在传参的时候,检测指针是否是野指针
1.约定一种规定,将没有初始化的指针,必须设置为NULL
2.指针!=NULL(一定是合法的)指针==NULL(有不合法的风险。排除可以传入NULL的情况)
assert是什么?
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:
#include<assert> void assert(int experssion);
二、指针的运算
1.指针+-整数(上文提过了)
注意:对指针的+-代表对地址加上或者减去所指向类型的大小。
2.指针-指针
int my_strlen(char *s) {
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
注意:两个指针相减代表两个指针之间所经历的元素个数
3.指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
代码简化, 这将代码修改如下:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) {
*vp = 0; }
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证
它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较。
4.指针的解引用(上文提过了)
三、指针和数组(指针和数组没有任何关系,元素访问时具有相似性)
数组名是什么?我们看一个例子:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
return 0;
}
可见数组名和数组首元素的地址是一样的。
结论:
数组名表示的是数组首元素的地址。
那么这样写代码是可行的:
:
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。
例如:
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(int i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
所以
p+i 其实计算的是数组 arr 下标为i的地址
。
那我们就可以直接通过指针来访问数组。
如下:
#include <stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
四、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针
对于二级指针的运算有:
*ppa
通过对
ppa
中的地址进行解引用,这样找到的是
pa
,
*ppa
其实访问的就是
pa
.
int b = 20; *ppa = &b;//等价于 pa = &b;
**ppa
先通过
*ppa
找到
pa
,
然后对
pa
进行解引用操作:
*pa
,那找到的是
a
.
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
五、指针的进阶
1.字符指针
字符指针
在指针的类型中我们知道有一种指针类型为字符指针
char*
;
一般使用
:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方法如下:
int main()
{
char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
代码
char* pstr = "hello bit.";
特别容易让同学以为是把字符串
hello bit
放到字符指针
pstr
里
了,但是
/
本质是把字符串 hello bit. 首字符的地址放到了pstr中。
2.指针数组
指针数组是一个存放指针的数组。
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
3.数组指针
数组指针是指针。
我们已经熟悉:
整形指针:
int * pint
;
能够指向整形数据的指针。 浮点型指针:
float * pf
;
能够
指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释:
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
4.&数组名VS数组名
对于下面的数组:
int arr[10]={0};
对于下面的数组:
arr
和
&arr
分别是啥?
我们知道
arr
是数组名,数组名表示数组首元素的地址。
那
&arr
数组名到底是啥?(
整个数组的首元素的地址
)
而二者在数值上是相等的,但是在含义上是截然不同的。
我们看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果如下:
![](https://img-blog.csdnimg.cn/6e04a2ef208c45fbbfb86d3319615863.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAcXFfNTc4MjIxNTg=,size_20,color_FFFFFF,t_70,g_se,x_16)
可见数组名和
&
数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
根据上面的代码我们发现,其实
&arr
和
arr
,虽然值是一样的,但是意义应该不一样的。
实际上:
&arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.
5.数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
一个数组指针的使用:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col) {
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col) {
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0; }
6.数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
一维数组传参
#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);
}
二维数组传参
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);
}
一级指针传参
#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 test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数
二级指针传参
#include <stdio.h>
void test(int** ptr) {
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
思考:
当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p) {
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0; }
7、函数指针
首先看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的是两个地址,这两个地址是
test
函数的地址。 那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1
可以存放。
pfun1
先和
*
结合,说明
pfun1
是指针,指针指向的是一个函数,指向的函数无
参数,返回值类型为
void
。
8、函数指针数组
函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10]])();
int *parr2[10]();
int (*)() parr3[10];
答案是:
parr1
parr1
先和
[]
结合,说明
parr1
是数组,数组的内容是什么呢?
是
int (*)()
类型的函数指针。
9.指向函数指针数组的指针
指向函数指针数组的指针是一个
指针
指针指向一个
数组
,数组的元素都是
函数指针
;
如何定义?
void test(const char* str) {
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0; }
10.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一
个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该
函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或
条件进行响应。
使用回调函数,模拟实现
qsort
(采用冒泡的方式)。
#include<stdio.h>
int int_cmp(const void * p1, const void * p2) {
return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size) {
int i = 0;
for (i = 0; i< size; i++)
{
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
int i = 0;
int j = 0;
for (i = 0; i< count - 1; i++)
{
for (j = 0; j<count-i-1; j++)
{
if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
{
_swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0; }