三 指针

<meta http-equiv="CONTENT-TYPE" content="text/html; charset=utf-8"> <meta name="GENERATOR" content="OpenOffice.org 2.2 (Linux)"> <meta name="AUTHOR" content="freebird"> <meta name="CREATED" content="20070510;9550000"> <meta name="CHANGEDBY" content="root"> <meta name="CHANGED" content="20070717;14010300"> <style type="text/css"> <!-- @page { size: 8.5in 11in; margin: 0.79in } H1 { margin-top: 0.24in; margin-bottom: 0.23in; line-height: 200%; page-break-inside: avoid } H1.western { font-family: "Liberation Serif", serif; font-size: 22pt } H1.cjk { font-family: "DejaVu Sans"; font-size: 22pt } H1.ctl { font-family: "DejaVu Sans"; font-size: 22pt } H2 { margin-top: 0.18in; margin-bottom: 0.18in; line-height: 173%; page-break-inside: avoid } H2.western { font-family: "Cambria", serif; font-size: 16pt } H2.cjk { font-family: "宋体", "SimSun"; font-size: 16pt } H2.ctl { font-family: "Times New Roman", serif; font-size: 16pt } P { margin-bottom: 0.08in } --> </style>

指针

指针代表什么?

我接触过的很多对C++心存敬畏之心的人(他们大多不懂C++)都跟我说“指针是非常容易出错的,但是一旦学会了指针也就学会了C++”。虽然我不同意这个观点,但或许这是从某个角度提出了指针在C++语言中的重要地位。

指针是什么?指针是一个占用四字节内存的特殊整型变量,它里面保存的是另一块内存的起始地址,并通过指针的静态类型标志这块内存是什么类型。看看下面的代码:

int main(void)

{

int * p=new int(5);

}

短短几句话,发生了多少动作呢?简单分析一下:

首先进入main函数前,为该函数分配了私有栈,栈的大小通常是固定的并且可以配置。进入函数后执行new int(5)使得在堆上分配4个字节,并且将这四个字节初始化为整形值5,然后栈顶指针向上移动四个字节,将这四个字节作为p变量的空间,这四个字节将保存刚才存放5的堆地址。当函数返回时,栈将被销毁,因此p将不再存在,但是new int(5)获得的堆上的4字节空间将依然存在,但是却没有办法再使用或者回收它,因此发生了最可怕的事情--内存泄漏。

野指针

好,刚才至少我们明白了指针也就是个4字节的变量。现在看下面的代码:

int * p1=NULL;

int * p2;

p1变量内部存放的值为0,这使得它不能指向任何有效地址。

p2 由于只是分配了4字节空间,并没有初始化,因此它里面的值应该是上次对该块内存使用后遗留下来的,是多少谁都不知道,或许是0,或许指向某处你绝对不想让它指向的地方,这称为野指针。

所以下面的代码就很危险,*p2=0 ;你都不知道你把什么内存给改写了,结果是无法预料的。野指针相当危险,所以比较好的做法是初始化为NULL。但是C++中很难有什么绝对遵守的准则,C++给了你很大的权限去选择不同的方案。如果在一个你确信不会出现问题的地方,并且性能是很关键的地方,我为什么多此一举要赋初值呢?我本人就有时候故意不这样做。

我对大家的建议是了解原理,然后自己控制,在对自己的控制能力没有信心的时候,遵守较安全的做法是明智的。

还有一种产生野指针的常见情况:

char* p=new char(‘b’);

...

delete p;

...

cout<<*p<<endl;

delete语句已经把p指向的堆上的一字节内存销毁了,但是并不会清空p的值,也就是说p仍然指向堆上的那个字节,然后cout<<p<<endl;会出现什么情况,无法预料。也许堆上的那个字节已经被改写,或者没有。下面的代码会对这种情况有所帮助:

char* p=new char(‘b’);

...

delete p;

p=NULL;

...

if(p!=NULL)//p仍然有效

cout<<*p<<endl;

但其实这是一个逻辑错误,既然delete p都执行了,无论如何,都不应该再使用p。修正逻辑才是治本,if(p!=NULL)只是打补丁的做法。

指针的类型

指针的静态类型

int* p=new int(5);这句话里我们的p变量的静态类型是int*,这就是告诉编译器p所指向的内存应该看作int变量,起始地址是p里面的值,大小是sizeof(int)。

char* p=new char[100];

char* pChar=p;

int* pInt=p;

++pChar;

++pInt;

由于pChar的静态类型为char*,所以每次执行++,都会向后移动一个字节,由于pInt静态类型是int*,所以每次执行++,都会向后移动sizeof(int)个字节(通常为4字节)。

指针的动态类型

指针的动态类型是指在多态的情况下,静态类型为指向基类的指针,实际指向的子对象的类型就是该指针的动态类型。比如class B派生自class A,我们写了下面的代码:

A* p=new B();

这句话说明p的静态类型是A*,但是实际指向的对象类型是B,该指针得动态类型是B*。动态类型在多态运用中起到十分重要的作用,绝大多数设计模式都以此为基础,微软的著名技术COM也是基于此。后面在虚函数部分我们会详细讨论。

智能指针

通常如果我们通过new操作获得了一个指针,我们需要记住在不需要使用的时候使用delete操作。如:

void f()

{

string* p=new string(“hello,world”);

....

delete

p; }

但是,可能会遇到这种情况,在delete p被执行之前的语句里面出错而抛出了一个异常对象,f函数将立刻返回,delete p将不会被执行,这样内存就泄露了。遇到这种情况,我们有几个办法:

1)不要使用new/delete,改在栈内创建对象

void f()

{

string str(“hello,world”);

....

}

这是个好办法,而且速度很快,如果能用,尽量用这种.

2)写一个class,利用栈的机制来管理

class StringManager

{

public:

StringManger(string* pStr):_pStr(pStr)

{

}

~StringManager()

{

delete _pStr;

}

private:

string* _pStr;

};

void f()

{

StringManager manager(new string(“hello,world”));

....

}

无论函数f内部是否抛出异常,只要f函数返回,私有栈必然要销毁,那么栈上分配的StringManager对象的析构函数一定会被调用,所以delete _pStr语句一定会被执行。

这就是目前广为使用的智能指针的基本原理。

目前标准C++2003修正版中常用的智能指针有auto_ptr和shared_ptr,我们公司的BFL类库中提供了其他的一些智能指针类。在后面我会逐步介绍,并分析优缺点,智能指针有其优点,但是并不是万能的药方,只有当你充分明白了它们的优缺点,才可以安全的用好它们。

指针用作参数

在前面我们介绍栈的时候,说过一个函数拥有一个私有栈,当函数执行时,会先将参数值拷贝到栈中,比如:

void f(int i,int* p)

{

*p=i;

}

int main(void)

{

int a(5);

int b;

f(a,&b);

return 0;

}

f函数执行时,通常从右到左的顺序拷贝p和i到栈中,这样栈中有一个p的副本变量p’和i的副本i’,然后通过*p’=i’ 将i’的值赋给了p’指向的变量b。这就是常说的传址和传值,对于b变量,是传址,对于a变量是传值。

这样使用指针会带来什么好处呢?

首先可以在函数f内部修改外面b变量的值,其次如果b变量不是简单类型,而是复杂如string的对象,只传递4字节的指针性能是非常快的。

我们经常见到类似这样的指针参数void f(int** p),指针的指针,为什么要这么用呢?看下面的示例代码:

void f(int** p)

{

*p=new int(5);

}

int main(void)

{

int* pValue=NULL;

f(&pValue);

....

delete pValue;

}

f函数在堆上分配了一块4字节整数区域,初始化为5,然后让外部的指针变量pValue指向这块堆上的内存。我们来分析一下:

一开始,pValue指针变量被创建,但是内容为0,即什么都不指向,然后将pValue指针变量所在的内存地址作为int** p传递给f函数,f函数将在自己的栈中保存p指针变量的地址副本,写成伪代码应该是:

void f(int** p)

{

int** p’=p;

*p’=new int(5);

}

*p’其实就是pValue,所以等价于外部pValue=new int(5);

然后函数f返回p’被销毁,但是pValue已经指向堆上的有效内存了。

请注意,涉及这样的函数应该写上注释,告诉用户是使用什么函数释放内存delete还是free或者其他,因为有可能用户看不到f内部实现的代码。

微软的COM总是使用这种策略。

指针和引用

引用很有可能就是常指针实现的,但是引用有特殊的约束。

引用不会为空,所以当函数接收一个引用参数时,不需要检测该引用所指定的对象是否为空,指针可以为空,所以当某函数接收指针作为参数时,你经常会看到这样的代码:assert(p!=NULL)

引用必须被初始化,而指针变量没有这个限制。

引用一旦被初始化后,只能代表初始化设定的对象,而指针是可以改变指向的对象。

<think>好的,用户之前问过双指针的问题,现在又接着问有没有指针。我需要先回顾之前的对话,确保回答的一致性和连贯性。用户可能是在学习指针的概念,逐步深入,从一级到二级,现在好奇是否存在更高级别的指针,比如指针。 首先,我需要明确指针的定义和用途。在C语言中,指针确实存在,即指向二级指针指针,声明方式是int ***p;。不过实际开发中,指针使用较少,但并非完全没用。用户可能想知道指针的应用场景,以及为什么它们不如双指针常见。 接下来,用户可能的深层需求是想了解指针的层级结构,以及何时需要使用多级指针。他们可能在学习动态内存分配、数据结构(比如多维数组、树结构)或者函数参数传递时需要更高级别的指针。 我需要分步骤解释指针的概念,给出具体的例子,比如动态维数组的创建,或者在函数中修改二级指针的情况。同时要指出指针的复杂性和潜在风险,比如内存管理更复杂,容易出错,可读性降低,从而解释为什么它们不常用。 另外,用户可能担心使用多级指针是否会导致性能问题,或者是否有必要学习指针。需要说明虽然存在,但大多数情况下双指针足够使用,指针只在特定场景下使用,并强调内存管理的注意事项。 最后,总结指针的存在和适用场景,提醒用户谨慎使用,确保他们理解正确使用多级指针的方法和潜在问题。可能还需要对比双指针指针,帮助用户更好地理解决策何时使用哪种指针。</think>--- ### C语言中指针的逐步解释 **指针**(Pointer to Pointer to Pointer)是C语言中指向**二级指针**的指针,即存储二级指针地址的变量。尽管实际开发中较少使用,但在特定场景下仍有应用价值。以下是详细说明: --- #### 一、基础定义 1. **声明格式** 指针通过`***`声明,语法为:`数据类型 ***指针名;` 内存关系: $$ \text{ppp} \rightarrow \text{pp} \rightarrow \text{p} \rightarrow \text{a} $$ 示例: ```c int a = 10; int *p = &a; // 一级指针 int **pp = &p; // 二级指针 int ***ppp = &pp; // 指针 ``` - 访问数据: `***ppp` → `**pp` → `*p` → `a`(最终值10) --- #### 二、核心应用场景 1. **动态维数组** 通过指针分配多维内存空间: ```c int x = 2, y = 3, z = 4; int ***cube = (int ***)malloc(x * sizeof(int **)); // 分配x层 for (int i=0; i<x; i++) { cube[i] = (int **)malloc(y * sizeof(int *)); // 分配y行 for (int j=0; j<y; j++) { cube[i][j] = (int *)malloc(z * sizeof(int)); // 分配z列 } } // 释放内存 for (int i=0; i<x; i++) { for (int j=0; j<y; j++) { free(cube[i][j]); } free(cube[i]); } free(cube); ``` 2. **函数内修改二级指针** 若需在函数内修改外部的二级指针,需传递指针: ```c void init_matrix(int ***matrix, int rows, int cols) { *matrix = (int **)malloc(rows * sizeof(int *)); for (int i=0; i<rows; i++) { (*matrix)[i] = (int *)malloc(cols * sizeof(int)); } } int main() { int **matrix = NULL; init_matrix(&matrix, 3, 4); // 传递二级指针的地址(指针) // 使用matrix... return 0; } ``` 3. **树形结构的复杂操作** 在多层嵌套的数据结构(如非平衡树)中,可能通过指针间接操作节点: ```c struct TreeNode { int data; struct TreeNode ***children; // 假设每个节点有多个子节点指针 }; ``` --- #### 指针的局限性 1. **复杂度高** - 代码可读性下降,调试困难(如`***ppp`容易混淆) - 内存管理繁琐,易发生泄漏或越界错误 2. **替代方案** - 优先使用结构体封装多层数据(如`struct Matrix`管理动态数组) - 用一维数组模拟多维访问(例如`array[i*cols + j]`代替二维数组) --- #### 四、总结对比 | 指针类型 | 典型用途 | 使用频率 | 风险等级 | |----------|-----------------------------|----------|----------------| | 一级指针 | 单变量地址操作 | 极高 | 低 | | 二级指针 | 动态二维数组、字符串数组管理 | 高 | 中 | | 指针 | 动态维数组、多层间接修改 | 低 | 高(易出错) | --- **结论**:指针在C语言中**存在但极少使用**,仅在需要操作**级间接引用**的特殊场景下适用。建议优先通过**结构体封装**或**简化设计**来降低代码复杂度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值