lion187 b5323dae44 修正错误
Signed-off-by: lion187 <cy187lion@sina.com>
2018-10-31 01:15:41 +08:00

291 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 2.8 高级指针
如果你爱一个人,就让他用指针,因为指针是天堂;如果你恨一个人,也让他用指针,因为指针是地狱。
指针是 C/C++ 语言中功能最强大,最具灵活性的功能之一,用好指针可以使程序更加优美简洁。指针的实质是非常简单的,每个人都很容易学会指针,但每一个 C/C++ 开发者,包括很多高手在内,都必须时刻注意指针的使用,因为一旦违反指针的使用规则,程序便极具破坏性。
## 2.8.1 指针的使用
我们在《2.1 基础语法》展示了指针最基本的用法。可以理解为指针就是某个变量或数据结构的索引地址,根据这个地址可以间接的访问这个变量或数据结构。
```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* 型指针十分有效,常被用作函数参数或结构体成员,用于传入任何数据,或捆绑任何类型。
```cpp
int* p0;
void* p1 = p0;
```
## 2.8.6 指针与函数
函数、或者说程序本身在运行时也是要占用内存空间的。因此,可以使用指针,像指向某个变量那样指向这个函数:
```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 多级指针
前面提及指针的实质是变量,因此也有一个内存空间用于保存指针的值,同样也有一个内存地址与之对应。这就是说,我们可以通过一个指向指针的指针去访问另一个指针,这被称为二级指针或多级指针。
```cpp
int a=10;
int* pa = &a;
int** ppa = &pa;
printf("%d\n", **ppa);
```
## 2.8.8 指针参数
以上所说的全部类型指针都可以作为函数的参数来进行传递。
```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 使用指针的注意事项
使用指针时,有些事情是需要特别注意的。
当使用指针来访问数组时,有可能访问的范围超过了数组自身的实际长度,这是非常危险的,并且在任何情况下都应该避免:
```cpp
int arry[6];
int* p = arry;
p += 10;
*p = 0x55; // 数组访问越界.
```
如果指针没有被初始化,则将其赋值为 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() 等函数为指针分配一个空间,但是在使用后我们忘记了释放,然后下次又接着申请了。这使得内存越用越少,很多内存脱离了程序应有的掌控范围,造成内存泄露。
```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、如果有多个不同得设备他们的读写方法不同但是调用接口的形式是一致的我们的主程序要读写所有的设备该怎样实现最高效能否将这些设备组织在一个数组中