内存地址
- 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址。
取址符
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址 %p
- 例如:
int a = 100;
printf("整型变量 a 的地址是: %p\n", &a);
char c = 'x';
printf("字符变量 c 的地址是: %p\n", &c);
double f = 3.14;
printf("浮点变量 f 的地址是: %p\n", &f);
- 注意:
- 虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸确实一样的。
- 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
指针基础
- 指针的概念:
- 地址。比如 &a 是一个地址,也是一个指针,&a 指向变量 a。
- 专门用于存储地址的变量,又称指针变量。
- 指针的定义:
int *p1; // 用于存储 int 型数据的地址,p1 被称为 int 型指针,或称整型指针
char *p2; // 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
double *p3; // 用于存储double型数据的地址,p3 被称为 double 型指针
- 指针的赋值:赋给指针的地址,类型需跟指针的类型相匹配。
int a = 100;
p1 = &a; // 将一个整型地址,赋值给整型指针p1
char c = 'x';
p2 = &c; // 将一个字符地址,赋值给字符指针p2
double f = 3.14;
p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
- 指针的索引:通过指针,取得其指向的目标
*p1 = 200; // 将 p1 指向的目标(即a)修改为200,等价于 a = 200;
*p2 = 'y'; // 将 p2 指向的目标(即c)修改为'y',等价于 c = 'y';
*p3 = 6.6; // 将 p3 指向的目标(即f)修改为6.6,等价于 f = 6.6;
- 指针的尺寸
- 指针尺寸指的是指针所占内存的字节数
- 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
- 结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关
void型指针
- 概念:无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针
- 要点:
- void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
- void 型指针无法进行加减法运算
- void关键字的三个作用:
- 修饰指针,表示指针指向一个类型未知的数据。
- 修饰函数参数列表,表示函数不接收任何参数。
- 修饰函数返回类型,表示函数不返回任何数据。
- 示例:
// 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
void *p = malloc(4);
// 1,将这 4 字节内存用来存储 int 型数据
*(int *)p = 100;
printf("%d\n", *(int *)p);
// 2,将这 4 字节内存用来存储 float 型数据
*(float *)p = 3.14;
printf("%f\n", *(float *)p);
野指针
- 概念:指向一块未知区域的指针,被称为野指针。野指针是危险的。
- 危害:
- 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
- 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
- 产生原因:
- 指针定义之后,未初始化
- 指针所指向的内存,被系统回收
- 指针越界
- 如何防止:
- 指针定义时,及时初始化
- 绝不引用已被系统回收的内存
- 确认所申请的内存边界,谨防越界
空指针
很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。
- 概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
- 示例:
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int *p2 = NULL;
// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3); // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL; // c. 让 p3 指向零地址
指针运算
- 指针加法意味着地址向上移动若干个目标
- 指针减法意味着地址向下移动若干个目标
- 示例:
int a = 100;
int *p = &a; // 指针 p 指向整型变量 a
int *k1 = p + 2; // 向上移动 2 个目标(2个int型数据)
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)
数组名涵义
- 数组名有两个含义:
- 第一含义是:整个数组
- 第二含义是:首元素地址
- 当出现以下情形时,那么数组名就代表整个数组:
- 在数组定义中
- 在 sizeof 运算表达式中
- 在取址符&中
- 其他任何情形下,那么数组名就代表首元素地址。即:此时数组名就是一个指向首元素的指针。
- 示例:
int a[3]; // 此处,a 代表整个数组
printf("%d\n", sizeof(a)); // 此处,a 代表整个数组
printf("%p\n", &a); // 此处,a 代表整个数组,此处为整个数组的地址
int *p = a; // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
p = a + 1; // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
function(a); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
scanf("%d\n", a); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
C语言只有在第一含义的场合下表现为数组,其他大部分场合都表现为首元素的地址,当数组表现为首元素地址时,实际上就是一个指向其首元素的指针。数组运算实际上就是指针运算。
数组下标
- 数组下标实际上是编译系统的一种简写,其等价形式是:
a[i] = 100; 等价于 *(a+i) = 100;
- 根据加法交换律,以下的所有的语句均是等价的:
a[i] = 100;
*(a+i) = 100;
*(i+a) = 100;
i[a] = 100;
- 数组运算,等价于指针运算。
字符串常量
- 字符串常量在内存中的存储,实质是一个匿名数组
- 匿名数组,同样满足数组两种涵义的规定
- 示例:
printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组
printf("%p\n", &"abcd"); // 此处 "abcd" 代表整个数组
printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址
char *p1 = "abcd"; // 此处 "abcd" 代表匿名数组的首元素地址
char *p2 = "abcd" + 1; // 此处 "abcd" 代表匿名数组的首元素地址
char型指针
char型指针实质上跟别的类型的指针并无本质区别,但由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。
- 定义:
char *p = "abcd";
多级指针
- 如果一个指针变量 p1 存储的地址,是另一个普通变量 a 的地址,那么称 p1 为一级指针
- 如果一个指针变量 p2 存储的地址,是指针变量 p1 的地址,那么称 p2 为二级指针
- 如果一个指针变量 p3 存储的地址,是指针变量 p2 的地址,那么称 p3 为三级指针
- 以此类推,p2、p3等指针被称为多级指针
- 示例:
int a = 100;
int *p1 = &a; // 一级指针,指向普通变量
int **p2 = &p1; // 二级指针,指向一级指针
int ***p3 = &p2; // 三级指针,指向二级指针
指针万能拆解法
- 任意的指针,不管有多复杂,其定义都由两部分组成。
- 第1部分:指针所指向的数据类型,可以是任意的类型
- 第2部分:指针的名字
- 示例:
char (*p1); // 第2部分:*p1; 第1部分:char;
char *(*p2); // 第2部分:*p2; 第1部分:char *;
char **(*p3); // 第2部分:*p3; 第1部分:char **;
char (*p4)[3]; // 第2部分:*p4; 第1部分:char [3];
char (*p5)(int, float); // 第2部分:*p5; 第1部分:char (int, float);
- 注解:
- 上述示例中,p1、p2、p3、p4、p5本质上并无区别,它们均是指针
- 上述示例中,p1、p2、p3、p4、p5唯一的不同,是它们所指向的数据类型不同
- 第1部分的声明语句,如果由多个单词组成,C语言规定需要将其拆散写到第2部分的两边
const 型指针
- const型指针有两种形式:①常指针 ②常目标指针
- 常指针:const修饰指针本身,表示指针变量本身无法修改。
- 常目标指针:const修饰指针的目标,表示无法通过该指针修改其目标。
- 常指针在实际应用中不常见。
- 常目标指针在实际应用中广泛可见,用来限制指针的读写权限
- 示例:
int a = 100;
int b = 200;
// 第1中形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a;
// 第2中形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;
const int *p2 = &a;
函数指针
- 概念:指向函数的指针,称为函数指针。
- 特点:函数指针跟普通指针本质上并无区别,只是在取址和索引时,取址符和星号均可省略
- 示例:
void f (int); // 函数 f 的类型是: void (int)
void (*p)(int); // 指针 p 专门用于指向类型为 void (int) 的函数
p = &f; // p 指向 f(取址符&可以省略)
p = f; // p 指向 f
// 以下三个式子是等价的:
f (666); // 直接调用函数 f
(*p)(666); // 通过索引指针 p 的目标,间接调用函数 f
p (666); // 函数指针在索引其目标时,星号可以省略
- 要点:
- 函数指针是一类专门用来指向某种类型函数的指针。
- 函数的类型不同,所需要的函数指针也不同。
- 函数的类型,与普通变量的类型判定一致,即去除声明语句中的标识符之后所剩的语句。