Summary
1)指针的本质
是变量,特殊之处在于指针存储的值是内存地址
(内存中每个存储单元的编号
:计算机中的最小存储单元是1byte ,即每个字节的编号都是一个内存地址)
2)程序中的一切元素都存在于内存中,因此可以通过内存地址访问程序元素
3)内存地址的本质
是一个无符号整数(4字节或8字节);4字节和8字节分别对应于32位和64位,所以我们常说的32位和64位系统指的就是可访问的最大内存地址
是多少。因为64位系统能支持更大的内存寻址,所以64位系统会比32位系统能同时运行的程序要多。
4)只有通过内存地址+长度
才能确定一个变量中保存的值。内存地址只是一个起始值,只有根据类型信息
,知道占多少内存、解读方式,才可以准确的读出变量里保存的值。
5)函数参数是指针时(传址调用
),可以实现在函数内部修改函数外部变量的值;使用指针作为参数,可以作为一个传出参数
,使得函数能返回多个值。
6)对于数组int a[5],a和&a的关系
:
- a可以
看做
一个常量指针,表示数组首元素的地址,值是0xA,类型是int*,即内存长度为4 - &a是数组的地址,值是0xA,类型是int(*)[5],即内存长度为20。指向数组的指针:
int(*pArr)[5] = &a;
7)指针与数组的等价用法:
int a = {1, 2, 3, 4, 5};
int* p = a;
a[i] <--> *(a + i)
<--> *(p + i) <--> p[i]
8)C语言中,字符串常量的类型是char*
(c++里则是const char*);int v = *p++;等价于int v = *p; p++
;因为(*)的优先级和(++)相同
9)函数的本质
是一段内存中的代码,占用一段连续的内存
。函数名就是函数的入口地址
(函数体代码的起始地址)。通过函数名调用
函数,本质为指定具体地址的跳转执行
。因此可以定义指针,保存函数的入口地址:type (*pFunc)(param) = funcName;
10)问:既然通过函数名可以直接调用函数,那么为什么还需要函数指针呢?
答:可以定义函数指针参数
。使用相同的代码,实现不同的功能
。主调函数只知道函数原型是这样,但不知道具体会调用哪个函数。函数指针
是回调函数
的实现机制:函数作为参数使用时,就构成了callback;
11)堆空间的本质是程序备用的“内存仓库”
,以字节为单位
预留的可用内存
12)在C语言中,void*
指针可以和其他类型的指针type*
进行相互赋值
,因为C语言对指针的检查并不严格。但是在使用时一定要注意操作的数据类型,void*仅仅是一个初始地址,不包含长度信息
。
13)malloc用于向堆空间中以字节为单位申请内存;free用于归还堆空间的内存,malloc而不free,内存泄露;多次free,崩溃。
14)指针是一个变量,那么自然也就会有指向指针的指针,即多级指针
。之前变量可以使用指针进行传址调用,那么也可以通过多级指针实现对指针的传址调用,在函数内部改变外部的指针的值。
15)二维数组的本质是一维数组
,所以二维数组名a表示的是数组的首元素,即a[0]表示的是一个一维数组;a的类型是type (*)[size2]
int arr[2] = {0};
int* pa = arr;
int (*pArr)[2] = &arr;
int bArr[2][2] = {0};
int (*pb)[2] = bArr;
typedef int (oneDimension)[2];
oneDimension (*pbArr)[2] = &bArr;
16)禁止
返回局部变量的地址,因为局部变量在函数调用结束后,生命期就结束了。此时返回的是个野指针
。
1、指针
1.1 指针本质
指针本质
是C语言中的变量
- 因为是变量,所以
用于保存具体值
- 特殊之处,指针
保存的值是内存中的地址
什么是内存地址?
- 内存是计算机中的存储部件,每个存储单元有固定唯一的编号(每个存储单元:1byte)
- 内存中
存储单元的编号
即内存地址
程序中的一切元素都存在于内存中,因此可以通过内存地址访问程序元素。
获取地址:
- C语言中通过&操作符获取程序元素的地址
- &可以获得变量、函数、数组的起始地址
内存地址的
本质是一个无符号整数
(4或8字节)int var = 0; printf("var address = %p\n", &var);
1.2 指针语法
指针定义语法:type * pointer
- type - 数据类型,
决定访问内存时的长度
范围 *
- 标志,意味着定义一个指针变量- pointer - 变量名,遵循C语言命名规则
指针内存访问:* pointer
- 指针访问操作符(*)作用于指针变量即可
访问内存数据
- 指针的类型决定通过地址访问内存时的长度范围
- 指针的类型统一占用4字节或8字节
初学指针时的军规:
type*
类型的指针只能保存type
类型变量的地址(如float和int类型的变量,都是4个字节,但是内存中的二进制是不一样的,如果存的是int,以float的方式来读,肯定是不对的)禁止
不同类型的指针相互赋值禁止
将普通数值当做地址赋值给指针- 注意:指针保存的地址必须是有效地址
问题:之前在函数里交换两个变量的值,真的没办法实现么?
思考:想要交换两个变量的值,必须在函数内部修改函数数外部的变量?如果参数是指针呢?把变量的地址传进去,在函数里直接通过内存地址去修改地址是否可行?
void swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
通过传指针就实现了在函数内部修改函数外部的变量。另外,函数的return只能返回一个值,如果在参数中使用指针,这样函数就可以返回多个值,传出参数
。
2、指针与数组
问题:数组的本质是一段连续的内存,那么,数组的地址是什么?如何获取?
2.1 指针与普通数组
- 使用取地址符&获得数组的地址:&a,&a表示一个地址(数组的地址),值是0xA,类型是
int(*)[5]
,即长度是20。指向数组的指针:int(*pName)[5] = &a; - 数组名可看一个常量指针:a,a代表一个地址(数组首元素的地址),值是0xA,类型是
int*
,即长度是4 当指针指向数组元素时,可以进行指针运算(指针移动)
int a[] = {1, 2, 3, 4, 5}; int* p = a; p = p + 1;
注意:数组名只是可以看做一个指针,代表了0号元素的地址。
指针与数组的等价用法:
int a = {1, 2, 3, 4, 5};
int* p = a;
a[i] <--> *(a + i)
<--> *(p + i) <--> p[i]
2.2 指针与字符串
字符串拾遗
- 问:C语言中的字符串常量是什么类型?
答:
char*
类型,一种指针类型printf("%p\n", "GrandSoft");
指针移动组合拳:int v = *p++;
- 指针访问操作符(*)和自增运算符(++)优先级相同
- 所以,先从p指向的内存中取值,然后p进行移动
- 等价于:int v = *p; p++;
3、指针与函数
问题:函数调用时会跳转到函数体对应的代码处执行,那么,如何知道函数体代码的具体位置?
深入函数之旅:
- 函数的
本质
是一段内存中的代码(占用一段连续内存) 函数拥有类型,
函数类型
由返回类型和参数类型列表组成函数声明 类型 int sum(int n) int(int) void g(void) void(void)
函数指针:type func(type1 a, type2 b)
- 函数名即函数入口地址,类型为
type (*)(type1, type2)
- 对于func的函数,func和&func
数值相同
,意义相同
指向函数的指针:type (*pFunc)(type1, type2) = func;
int (*pFunc)(int a, int b) = nullptr; pFunc = add; printf("%d\n", pFunc(1, 2)); // 1) printf("%d\n", (*pFunc)(1, 2)); // 2)
对于两种调用方法的理解:
1)函数指针保存函数的入口地址,函数名也保存了函数的入口地址,函数名可以直接调用函数,那么用pFunc直接调用函数,也很合理
2)pFunc是一个地址,里面存放的函数的代码,使用*pFunc就取到了函数体,直接调用函数,也很合理
问题:既然可以通过函数名直接调用函数,那么为什么还需要使用函数指针呢?
答:这样就可以使用函数指针作为函数参数
。
- 函数指针的本质还是指针(变量,保存地址)
可以定义函数指针参数,
使用相同代码实现不同功能
int calculate(int a[], int len, int(*cal)(int, int)) { int ret = a[0]; int i = 0; for(i=1; i<len; i++) { ret = cal(ret, a[i]); } return ret; }
根据传入不同的cal函数,可以实现比如数组相乘、相加、相除等等。在calculate里,
只知道要调用的函数原型是这样
,但是具体调用的是什么函数
,执行了什么功能,只有执行到cal才知道
。
4、指针与堆空间
再论内存空间:内存区域不同,用途不同
- 全局数据区:存放全局变量,静态变量
- 栈空间:存放函数参数,局部变量
- 堆空间:用于动态创建变量(数组)
堆空间的本质:
备用的“内存仓库”
,以字节为单位预留的可用内存- 程序可在需要时从“仓库”中申请使用内存(
动态借
) - 当不需要再使用申请的内存时,需要及时归还(
动态还
)
预备知识-void*
- void类型是基础类型,对应的指针类型为void*
- void*是指针类型,其指针变量能够保存地址
- 通过void*指针
无法获取内存中的数据
(无长度信息)
堆空间的使用:
- 工具包:stdlib.h
- 申请:void* malloc(unsigned bytes)
- 归还:void free(void* p),p必须是堆空间中的地址
堆空间的使用原则:
- 有借有还,再接不难
- malloc申请内存后,应该
判断是否申请成功
- free只能申请释放到的内存,且
不可多次释放
5、指针问题剖析
5.1 多级指针
可以定义指针的指针来保存其他指针变量的地址
type v;
type* pv = &v;
type** ppv = &pv;
type*** ppv = &ppv;
...
使用指针可以取到指针的地址,那么就可以在函数内部,改变指针里保存的值。
指针的传址调用,修改函数外的指针的值:
int getDouble(double** pp, unsigned int n)
{
int ret = 0;
double* pd = (double*)malloc(sizeof(double) * n);
if(pd != NULL)
{
*pp = pd;
ret = 1;
}
return ret;
}
5.2 二维数组
问题:一维数组名的类型为type,那么二维数组名的类型是不是type*?
再论二维数组:
- 二维数组的本质是一维数组,即:数组中的元素是一维数组
因此:二维数组a[][],a表示a[0],首元素是一个一维数组
int b[][2] = {1, 2, 3, 4}; int(*pnb)[2] = b;
本文总结自“狄泰软件学院”唐佐林老师《C语言入门课程》。
如有错漏之处,恳请指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。