HaveFunWithEmbeddedSystem/Chapter2_C_与_C++/2.8_高级指针.md

290 lines
10 KiB
Markdown
Raw Normal View History

# 2.8 高级指针
如果你爱一个人,就让他用指针,因为指针是天堂;如果你恨一个人,也让他用指针,因为指针是地狱。
指针是 C/C++ 语言中功能最强大,最具灵活性的功能之一,用好指针可以使程序更加优美简洁。指针的实质是非常简单的,每个人都很容易学会指针,但每一个 C/C++ 开发者,包括很多高手在内,都必须时刻注意指针的使用,因为一旦违反指针的使用规则,程序便极具破坏性。
## 2.8.1 指针的使用
2018-07-04 01:21:41 +08:00
我们在《2.1 基础语法》展示了指针最基本的用法。可以理解为指针就是某个变量或数据结构的索引地址,根据这个地址可以间接的访问这个变量或数据结构。
2018-07-04 01:21:41 +08:00
```cpp
typedef struct _ST_A
{
int a;
int b;
}ST_A, *PST_A;
ST_A AStruct = {15, 18};
PST_A PAStruct = &AStruct; // PAStruct 是一个指针,他获得了 AStruct 的索引地址。
printf("%d", PAStruct->a); // 通过 PAStruct 间接访问 AStruct 结构体及其成员。
```
## 2.8.2 指针的实质和意义
一旦程序运行起来,所有的变量都存在于内存之中,有特定的内存空间用于容纳变量的值,这个空间的首地址即为对应变量的起始地址。其实,指针也是一个变量,只不过这种变量保存的内容是其他变量的内存起始地址(或称作首地址)。
| Addr | Var |
|--------|-----------|
| 0xAA55 | short A_L |
| 0xAA56 | short A_H |
| 0xAA57 | - |
如上表所示,有一个 short 型变量 A 其中 0xAA55 这个地址保存了 A 的低 8 位(即 A_L而 0xAA56 这个地址保存了 A 的高 8 位(即 A_H)。如果有一个指针指向 A那么这个指针的值即为变量 A 的首地址,也就是 0xAA55。
指针是一个变量,每种变量都有长度,指针变量的长度是多少呢?这与你所使用机器的地址总线位宽有关。如果是 32 位机,那么指针的长度就是 464 位机对应的指针长度为 8。
使用指针最大的好处是避免了同一组内存数据的重复。比如说C 语言函数参数是值传递的,也就是说调用函数时会发生参数的拷贝。如果参数是一个整形数据,那么这个拷贝不会花费太长时间。如果这个参数是 1K 长的数组呢,那每次调用这个函数岂不是都要进行 1K 数据的拷贝?那太低效了,这个时候我们可以使用指针,这样在函数调用时,只需要拷贝一个指针长度的数据,却在函数内部通过指针间接访问全部 1K 数据。
即便不是作为参数,你会发现,很多情况下需要获得一个长数据,或者结构体,直接拷贝一份这样的数据是不值当的,我们需要一个类似别名的东西能间接引用这个数据,这个时候我们就可以使用指针。
指针提供了间接访问数据的功能,这样的功能避免了数据使用的重复,避免了拷贝过程,并提供了类似别名访问的机制,使程序变得高效。
## 2.8.3 指针与数组
我们知道,数组中的元素是何种类型,我们就将这个数组称为 X型数组。例如
```cpp
short arry[256];
```
被称作 short 数组。同样,如果数组中保存的是指针类型,我们则称这个数组为指针数组。
```cpp
short* parry[256]; // 这是一个 short* 类型的数组, 数组中的每个元素都是一个指向 short 型变量的指针.
```
请记住,指针数组的实质仍是数组。
通过 & 符号,能够获得变量的地址,那么该如何获得数组的地址?
数组中的元素是连续排列的,如果取某个元素的地址来代表整个数组,那显然是数组的首个元素地址最具代表性。数组首个元素的地址被称作数组首地址,数组名就代表了这个地址。我们看以下的程序:
```cpp
/*
* @brief: Program 2-8-3-1
*/
short* parry[256];
void* tmp = &parry[0];
if(tmp==parry)
{
printf("True\n"); // 这句一定会被打印出来.
}
```
由于数组的名字代表了数组首地址(也就是指针),那么,我们也可以把一个指向某连续内存区的指针看作数组:
```cpp
unsigned char* pointer;
pointer = calloc(256); // 分配一片长度为 256 的连续内存区, 并将这片内存区清零.
pointer[15] = 0xAA;
printf("Pointer+15=0x%x.\n", *(pointer+15)) // 输出内容为 “Pointer+15=0xaa.”
```
数组名这个特殊指针,与其他指针变量最大的差异在于:我们可以对指针变量进行赋值操作,但向数组名赋值是不和法的。这是因为数组是静态分配的,它的首地址是固定的,可以说数组名这个特殊指针是自带 const 属性的。因此,下面的程序将导致编译错误:
```cpp
short* parry[256];
parry++; // 这里有个错误.
```
## 2.8.4 指针与结构体/联合体
结构体名称和首个元素的地址就代表了结构体的地址:
```cpp
/*
* @brief: Program 2-8-4-1
*/
typedef struct _STA_T
{
int a;
}STA_T;
typedef struct _STB_T
{
STA_T b;
int c;
}STB_T;
void fun(STA_T* st)
{
STB_T* stb = st;
printf("%d", stb->c); // 注意,结构体指针使用 "->" 来访问元素,而非 "." .
}
void main(void)
{
STB_T stx;
stx.c = 5;
fun(&stx); // 打印内容为 “5”.
}
```
## 2.8.5 指针的类型转换
不难注意到Program 2-8-3-1 中将两种不同类型的指针直接进行了比较,这里会产生一个编译警告。我们该如何消除它?
```cpp
short* parry[256];
void* tmp = &parry[0];
if(tmp==(void*)parry)
{
printf("True\n");
}
```
没错,指针也是一个类型,也就是说,类型转换的概念同样适用于指针。我们可以把一种类型的指针转换成另一种类型的指针,也可以把指针转换成整数,甚至可以把整数转换成指针。还记得 2.1.11 中的例子么?
```cpp
// 假设系统是从低位开始寻址.
unsigned int a = 0x11223344;
char* pa = (char*)&a; // 通过 & 符号取变量 a 的地址. 指针 pa 的值便是变量 a 的首地址.
printf("%d.\n", *pa); // 0x44.
pa++;
printf("%d.\n", *pa); // 0x33.
pa++;
printf("%d.\n", *pa); // 0x22.
pa++;
printf("%d.\n", *pa); // 0x11.
pa++;
```
这是指针类型转换的一种用法,相当于把变量 a 看作长度为 4 的 char 型数组,然后我们通过 pa 指针一次访问了这个数组中的全部元素。
指针的转换同样会遇到位数问题:
```cpp
char arry[256];
printf("%d", (short)arry); // 数据将被截断.
```
有一种特殊类型的指针,它可以接收任何其他类型的指针,即 void\* 型指针。可以将任何其他类型的指针直接赋值给 void\* 型指针变量而不产生任何编译错误或编译警告。void\* 型指针十分有效,常被用作函数参数或结构体成员,用于传入任何数据,或捆绑任何类型。
2018-07-04 01:21:41 +08:00
```cpp
int* p0;
void* p1 = p0;
```
## 2.8.6 指针与函数
2018-07-05 00:16:35 +08:00
函数、或者说程序本身在运行时也是要占用内存空间的。因此,可以使用指针,像指向某个变量那样指向这个函数:
```cpp
typedef void (*P_FUN_TYPE)(int a);
void fun(int a)
{
printf("%d", a);
}
void main(void)
{
int a = 0x10;
P_FUN_TYPE pfun = fun;
pfun(a); // 与直接调用 fun(a) 是一样的.
}
```
这样可以指向函数的指针称为函数指针。在一般的程序中,函数指针并不常见。但,如果你编写的程序具有某种框架,例如 linux 内核中的驱动框架,就会大量使用函数指针了。
## 2.8.7 多级指针
2018-07-04 01:21:41 +08:00
前面提及指针的实质是变量,因此也有一个内存空间用于保存指针的值,同样也有一个内存地址与之对应。这就是说,我们可以通过一个指向指针的指针去访问另一个指针,这被称为二级指针或多级指针。
```cpp
int a=10;
int* pa = &a;
int** ppa = &pa;
2018-07-05 00:16:35 +08:00
printf("%d\n", **ppa);
2018-07-04 01:21:41 +08:00
```
## 2.8.8 指针参数
2018-07-05 00:16:35 +08:00
以上所说的全部类型指针都可以作为函数的参数来进行传递。
```cpp
void fun(int* arry, int size)
{
int i;
for(i=0; i<size; i++)
printf("%d\n", arry[i]);
}
void main(void)
{
int codeList[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
fun(codeList, sizeof(codeList)/sizeof(*codeList));
}
```
对于参数的传递有值传递和引用传递两种。C 语言本质上只支持值传递。但通过指针,可以达到传递引用的效果——这是因为指针本身就是对其他变量的引用。
## 2.8.9 使用指针的注意事项
2018-07-05 00:16:35 +08:00
使用指针时,有些事情是需要特别注意的。
当使用指针来访问数组时,有可能访问的范围超过了数组自身的实际长度,这是非常危险的,并且在任何情况下都应该避免:
```cpp
int arry[6];
int* p = arry;
p += 10;
*p = 0x55; // 数组访问越界.
2018-07-05 00:16:35 +08:00
```
如果指针没有被初始化,则将其赋值为 NULL即空指针。另外在使用完指针后也应该将其赋值为 NULL。这样在程序中通过对指针值得判断可以知道这个指针是否有效。无效得指针非常可怕我们将其称为野指针。
```cpp
typedef void (*P_FUN_TYPE)(int a);
void fun(int a)
{
printf("%d", a);
}
void main(void)
{
int a = 0x10;
P_FUN_TYPE pfun; // pfun 没有被初始化过, 被称作野指针.
pfun(a); // 调用 pfun(a) 之后程序不知道跑到了哪里去,程序跑飞了.
}
```
很多时候,我们使用 malloc() 等函数为指针分配一个空间,但是在使用后我们忘记了释放,然后下次又接着申请了。这使得内存越用越少,很多内存脱离了程序应有的掌控范围,造成内存泄露。
2018-07-05 00:16:35 +08:00
```cpp
int* p = NULL;
int i;
for(i=0; i<1024; i++)
p = malloc(1024);
p = NULL;
// 很快 1M 内存就消失了,并且这块内存无法再被回收。
```
内存泄露同样是极旗危险的,它不仅造成程序和系统不稳定,也会危害系统安全,泄漏用户隐私数据等。
## 练习
1. Program 2-8-4-1 中同样有编译警告,你该如何去除这些警告?
2. 如果有多个不同得设备,他们的读写方法不同,但是调用接口的形式是一致的,我们的主程序要读写所有的设备,该怎样实现最高效,能否将这些设备组织在一个数组中?