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

10 KiB
Raw Permalink Blame History

2.8 高级指针

如果你爱一个人,就让他用指针,因为指针是天堂;如果你恨一个人,也让他用指针,因为指针是地狱。
指针是 C/C++ 语言中功能最强大,最具灵活性的功能之一,用好指针可以使程序更加优美简洁。指针的实质是非常简单的,每个人都很容易学会指针,但每一个 C/C++ 开发者,包括很多高手在内,都必须时刻注意指针的使用,因为一旦违反指针的使用规则,程序便极具破坏性。

2.8.1 指针的使用

我们在《2.1 基础语法》展示了指针最基本的用法。可以理解为指针就是某个变量或数据结构的索引地址,根据这个地址可以间接的访问这个变量或数据结构。

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型数组。例如

short arry[256];

被称作 short 数组。同样,如果数组中保存的是指针类型,我们则称这个数组为指针数组。

short* parry[256];   // 这是一个 short* 类型的数组, 数组中的每个元素都是一个指向 short 型变量的指针.

请记住,指针数组的实质仍是数组。
通过 & 符号,能够获得变量的地址,那么该如何获得数组的地址?
数组中的元素是连续排列的,如果取某个元素的地址来代表整个数组,那显然是数组的首个元素地址最具代表性。数组首个元素的地址被称作数组首地址,数组名就代表了这个地址。我们看以下的程序:

/*
 * @brief: Program 2-8-3-1
 */
short* parry[256];
void* tmp = &parry[0];

if(tmp==parry)
{
    printf("True\n");   // 这句一定会被打印出来.
}

由于数组的名字代表了数组首地址(也就是指针),那么,我们也可以把一个指向某连续内存区的指针看作数组:

unsigned char* pointer;

pointer = calloc(256);  // 分配一片长度为 256 的连续内存区, 并将这片内存区清零.

pointer[15] = 0xAA;

printf("Pointer+15=0x%x.\n", *(pointer+15)) // 输出内容为 “Pointer+15=0xaa.”

数组名这个特殊指针,与其他指针变量最大的差异在于:我们可以对指针变量进行赋值操作,但向数组名赋值是不和法的。这是因为数组是静态分配的,它的首地址是固定的,可以说数组名这个特殊指针是自带 const 属性的。因此,下面的程序将导致编译错误:

short* parry[256];
parry++;    // 这里有个错误.

2.8.4 指针与结构体/联合体

结构体名称和首个元素的地址就代表了结构体的地址:

/*
 * @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 中将两种不同类型的指针直接进行了比较,这里会产生一个编译警告。我们该如何消除它?

short* parry[256];
void* tmp = &parry[0];

if(tmp==(void*)parry)
{
    printf("True\n");
}

没错,指针也是一个类型,也就是说,类型转换的概念同样适用于指针。我们可以把一种类型的指针转换成另一种类型的指针,也可以把指针转换成整数,甚至可以把整数转换成指针。还记得 2.1.11 中的例子么?

// 假设系统是从低位开始寻址.
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 指针一次访问了这个数组中的全部元素。

指针的转换同样会遇到位数问题:

char arry[256];

printf("%d", (short)arry);  // 数据将被截断.

有一种特殊类型的指针,它可以接收任何其他类型的指针,即 void* 型指针。可以将任何其他类型的指针直接赋值给 void* 型指针变量而不产生任何编译错误或编译警告。void* 型指针十分有效,常被用作函数参数或结构体成员,用于传入任何数据,或捆绑任何类型。

int* p0;
void* p1 = p0;

2.8.6 指针与函数

函数、或者说程序本身在运行时也是要占用内存空间的。因此,可以使用指针,像指向某个变量那样指向这个函数:

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 多级指针

前面提及指针的实质是变量,因此也有一个内存空间用于保存指针的值,同样也有一个内存地址与之对应。这就是说,我们可以通过一个指向指针的指针去访问另一个指针,这被称为二级指针或多级指针。

int a=10;
int* pa = &a;
int** ppa = &pa;

printf("%d\n", **ppa);

2.8.8 指针参数

以上所说的全部类型指针都可以作为函数的参数来进行传递。


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 使用指针的注意事项

使用指针时,有些事情是需要特别注意的。

当使用指针来访问数组时,有可能访问的范围超过了数组自身的实际长度,这是非常危险的,并且在任何情况下都应该避免:

int arry[6];
int* p = arry;

p += 10;
*p = 0x55;   // 数组访问越界.

如果指针没有被初始化,则将其赋值为 NULL即空指针。另外在使用完指针后也应该将其赋值为 NULL。这样在程序中通过对指针值得判断可以知道这个指针是否有效。无效得指针非常可怕我们将其称为野指针。

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() 等函数为指针分配一个空间,但是在使用后我们忘记了释放,然后下次又接着申请了。这使得内存越用越少,很多内存脱离了程序应有的掌控范围,造成内存泄露。

int* p = NULL;
int i;

for(i=0; i<1024; i++)
    p = malloc(1024);

p = NULL;
// 很快 1M 内存就消失了,并且这块内存无法再被回收。

内存泄露同样是极旗危险的,它不仅造成程序和系统不稳定,也会危害系统安全,泄漏用户隐私数据等。

练习

  1. Program 2-8-4-1 中同样有编译警告,你该如何去除这些警告?
  2. 如果有多个不同得设备,他们的读写方法不同,但是调用接口的形式是一致的,我们的主程序要读写所有的设备,该怎样实现最高效,能否将这些设备组织在一个数组中?