`
一夕剑
  • 浏览: 53172 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

C/C++之指针

 
阅读更多

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。

堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete.如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多
指针,在C/C++语言中一直是很受宠的;几乎找不到一个不使用指针的C/C++应用。用于存储数据和程序的地址,这是指针的基本功能。用于指向整型数,用整数指针(int*);指向浮点数用浮点数指针(float*);指向结构,用对应的结构指针(struct xxx *);指向任意地址,用无类型指针(void*)。

有时候,我们需要一些通用的指针。在C语言当中,(void*) 可以代表一切;但是在C++中,我们还有一些比较特殊的指针,无法用(void*)来表示。事实上,在C++中,想找到一个通用的指针,特别是通用的函数指针简直是一个“不可能任务”。

C++是一种静态类型的语言,类型安全在C++中举足轻重。在C语言中,你可以用void*来指向一切;但在C++中,void*并不能指向一切,就算能,也失去了类型安全的意义了。类型安全往往能帮我们找出程序中潜在的一些BUG.

下面我们来探讨一下,C++中如何存储各种类型数据的指针。

1. 数据指针

数据指针分为两种:常规数据指针和成员数据指针

1.1 常规数据指针

这个不用说明了,和C语言一样,定义、赋值是很简单明了的。常见的有:int*, double* 等等。

如:

int value = 123;

int * pn = &value;

1.2 成员数据指针

有如下的结构:


struct MyStruct
{
int key;
int value;
};


现在有一个结构对象:

MyStruct me;

MyStruct* pMe = &me;

我们需要 value 成员的地址,我们可以:

int * pValue = &me.value;

//或

int * pValue = &pMe->value;

当然了,这个指针仍然是属于第一种范筹——常规数据指针。

好了,我们现在需要一种指针,它指向MyStruct中的任一数据成员,那么它应该是这样的子:

int MyStruct::* pMV = &MyStruct::value;

//或

int MyStruct::* pMK = &MyStruct::key;

这种指针的用途是用于取得结构成员在结构内的地址。我们可以通过该指针来访问成员数据:

int value = pMe->*pMV; // 取得pMe的value成员数据。

int key = me.*pMK; // 取得me的key成员数据。

那么,在什么场合下会使用到成员数据指针呢?

确实,成员指针本来就不是一种很常用的指针。不过,在某些时候还是很有用处的。我们先来看看下面的一个函数:


int sum(MyStruct* objs, int MyStruct::* pm, int count)
{
int result = 0;
for(int i = 0; i < count; ++i)
result += objs[i].*pm;
return result;
}

这个函数的功能是什么,你能看明白吗?它的功能就是,给定count个MyStruct结构的指针,计算出给定成员数据的总和。有点拗口对吧?看看下面的程序,你也许就明白了:
MyStruct me[10] =
{
{1,2},{3,4},{5,6},{7,8},{9,10},{11,12},{13,14},{15,16},{17,18},{19,20}
};

int sum_value = sum(me, &MyStruct::value, 10);
//计算10个MyStruct结构的value成员的总和: sum_value 值 为 110 (2+4+6+8+...+20)

int sum_key = sum(me, &MyStruct::key, 10);
//计算10个MyStruct结构的key成员的总和: sum_key 值 为 100 (1+3+5+7+...+19)

也许,你觉得用常规指针也可以做到,而且更易懂。Ok,没问题:
int sum(MyStruct* objs, int count)
{
int result = 0;
for(int i = 0; i < count; ++i)
result += objs[i].value;
return result;
}
你是想这么做吗?但这么做,你只能计算value,如果要算key的话,你要多写一个函数。有多少个成员需要计算的话,你就要写多少个函数,多麻烦啊。

在C/C++中,数据指针是最直接,也最常用的,因此,理解起来也比较容易。而函数指针,作为运行时动态调用(比如回调函数 CallBack Function)是一种常见的,而且是很好用的手段。

我们先简单的说一下函数指针。(这一部份没什么价值,纯是为了引出下一节的内容)

2 常规函数指针

void(*fp)();

fp 是一个典型的函数指针,用于指向无参数,无返回值的函数。

void(*fp2)(int);

fp2 也是一个函数指针,用于指向有一个整型参数,无返回值的函数。

当然,有经验人士一般都会建议使用typedef来定义函数指针的类型,如:

typedef void(* FP)();

FP fp3; // 和上面的fp一样的定义。

函数指针之所以让初学者畏惧,最主要的原因是它的括号太多了;某些用途的函数指针,往往会让人陷在括号堆中出不来,这里就不举例了,因为不是本文讨论的范围;typedef 方法可以有效的减少括号的数量,以及理清层次,所以受到推荐。本文暂时只考虑简单的函数指针,因此暂不用到typedef.

假如有如下两个函数:


void f1()
{
std::cout << "call f " << std::endl;
}

void f2(int a)
{
std::cout << "call f2( " << a << " )" << std::endl;
}


现在需要通过函数指针来调用,我们需要给指针指定函数:
fp = &f1; // 也可以用:fp = f1;
fp2= &f2; // 也可以用:fp2= f2;
void (*fp3)() = &f1; // 也可以用:void (*fp3)() = f1;
//调用时如下:
fp(); // 或 (*fp)();
fp2(1); // 或 (*fp2)(1);
fp3(); // 或 (*fp3)();


对于此两种调用方法,效果完全一样,我推荐用前一种。后一种不仅仅是多打了键盘,而且也损失了一些灵活性。这里暂且不说它。

C++强调类型安全。也就是说,不同类型的变量是不能直接赋值的,否则轻则警告,重则报错。这是一个很有用的特性,常常能帮我们找到问题。因此,有识之士认为,C++中的任何一外警告都不能忽视。甚至有人提出,编译的时候不能出现任何警告信息,也就是说,警告应该当作错误一样处理。

比如,我们把f1赋值给fp2,那么C++编译器(vc7.1)就会报错:

fp2 = &f1; // error C2440: “=” : 无法从“void (__cdecl *)(void)”转换为“void (__cdecl *)(int)”

fp1 = &f1; // OK

这样,编译器可以帮我们找出编码上的错误,节省了我们的排错时间。

考虑一下C++标准模板库的sort函数:


// 快速排序函数
template
void sort(
RandomAccessIterator _First, // 需排序数据的第一个元素位置
RandomAccessIterator _Last, // 需排序数据的最后一个元素位置(不参与排序)
BinaryPredicate _Comp // 排序使用的比较算法(可以是函数指针、函数对象等)
);


比如,我们有一个整型数组:

int n[5] = {3,2,1,8,9};

要对它进行升序排序,我们需定义一个比较函数:


bool less(int a, int b)
{
return a < b;
}


然后用:

sort(n, n+5, less);

要是想对它进行降序排序,我们只要换一个比较函数就可以了。C/C++的标准模板已经提供了less和great函数,因此我们可以直接用下面的语句来比较:

sort(n, n+5, great);

这样,不需要改变sort函数的定义,就可以按任意方法进行排序,是不是很灵活?

这种用法以C++的标准模板库(STL)中非常流行。另外,操作系统中也经常使用回调(CallBack)函数,实际上,所谓回调函数,本质就是函数指针。

看起来很简单吧,这是最普通的C语言指针的用法。本来这是一个很美妙的事情,但是当C++来临时,世界就开始变了样。

假如,用来进行sort的比较函数是某个类的成员,那又如何呢?

还有一个更重要的区别是,指针所占的空间也不一样了。即使在32位系统中,所占的空间也有可能是4字节、8字节、12字节甚至16字节,这个依据平台及编译器,有很大的变化。

尽管C++中仍然有万能指针void*,但它却属于被批斗的对象,而且再也不能“万能”了。它不能转换成成员指针。

这样一来,C++的指针就变得很尴尬:我们需要一种指针能够指向同一类型的数据,不管这个数据是普通数据,还是成员数据;我们更需要一种指针能够指向同一类型的函数,不管这个函数是静态函数,还是成员函数。但是没有,至少从现在的C++标准中,还没有看到。

自从有了类,我们开始按照 数据+操作 的方式来组织数据结构;自从有了模板,我们又开始把 数据 和 算法 分离,以便重用,实在够折腾人的。但不管怎么折腾,现在大多数函数都不再单身,都嫁给了类,进了围城。可是我们仍然需要能够自由调用这些成员函数。

考虑一下windows下的定时调用。SetTimer函数的原型是这样的:

 

UINT_PTR SetTimer(
HWND hWnd,
UINT_PTR nIDEvent,
UINT uElapse,
TIMERPROC lpTimerFunc
);

 

其中,参数就不解释了,这个函数估计大多数windows开发人员都知道。lpTimerFunc是个会被定时调用的函数指针。假如我们不通过 WM_TIMER消息来触发定时器,而是通过lpTimerFunc来定时工作,那么我们就只能使用普通函数或静态函数,而无论如何都不能使用成员函数,哪怕通过静态函数转调也不行。

再考虑一下线程的创建:


uintptr_t _beginthread(
void( *start_address )( void * ),
unsigned stack_size,
void *arglist
);
start_address仍然只支持普通函数。不过这回好了,它允许回调函数一个void*参数,它将会arglist作为参数来调用 start_address.于是,聪明的C++程序员,就利用arglist传递this指针,从而利用静态函数成功的调用到了成员函数了:
class mythread
{
public:
static void doit(void* pThis)
{
    ((mythread*)pThis)->doit();
}
void doit(...){}
};

main()
{
...
mythread* pmt = new mythread;
_beginthread(&mythread::doit, 0, (void*)pmt);

...
}

但是显然,C++程序员肯定不会因此而满足。这里头有许多被C++批判的不安定因素。它使用了C++中被认为不安全的类型转换,不安全的void*指针,等等等等。但这是系统为C语言留下的调用接口,这也就认了。那么假如,我们就在C++程序中如何来调用成员函数指针呢?

如下例,我们打算对vector中的所有类调用其指定的成员函数:


#include
#include
#include
#include
using namespace std;

class A
{
int value;
public:
A(int v){value = v;}
void doit(){ cout << value << endl;};
static void call_doit(A& rThis)
{
rThis.doit();
}
};


int main()
{
vector va;
va.push_back(A(1));
va.push_back(A(2));
va.push_back(A(3));
va.push_back(A(4));
//方法1:
//for_each(va.begin(), va.end(), &A::doit); //error
//方法2:
for_each(va.begin(), va.end(), &A::call_doit);
//方法3:
for_each(va.begin(), va.end(), mem_fun_ref(&A::doit));

system("Pause");

return 0;
}

 


方法1,编译不能通过。for_each只允许具有一个参数的函数指针或函数对象,哪怕A::doit默认有一个this指针参数也不行。不是for_each没考虑到这一点,而是根本做不到!

方法2,显然是受到了beginthread的启发,使用一个静态函数来转调用,哈哈成功了。但是不爽!这不是C++.

方法3,呼,好不容易啊,终于用mem_fun_ref包装成功了成员函数指针。

似乎方法3不错,又是类型安全的,又可以通用——慢着,首先,它很丑,哪有调用普通C函数指针那么漂亮啊(见方法2),用了一大串包装,又是尖括号又是圆括号,还少不了&号!其次,它只能包装不超过一个参数的函数!尽管它在for_each中够用了,但是你要是想用在超过一个参数的场合,那只有一句话:不可能的任务。

是的,在标准C++中,这是不可能的任务。但事情并不总是悲观的,至少有许多第三方库提供了超越mem_fun的包装。如boost::function 等等。但是它也有限制:它所支持的参数仍然是有限的,只有十多个,尽管够你用的了;同样,它也是丑陋的,永远不要想它能够简单的用&来搞定。

也许,以失去美丽的代价,来换取质量上的保证,这也是C++对于函数指针的一种无奈吧……

期待C++0x版本。它通过可变模板参数,能够让mem_fun的参数达到无限个……

————————

BTW: C++Builder扩展了一个关键字 closure ,允许成员函数指针如同普通函数指针一样使用。也许C++0x能考虑一下……


函数对象不是函数指针。但是,在程序代码中,它的调用方式与函数指针一样,后面加个括号就可以了。

这是入门级的随笔,说的是函数对象的定义,使用,以及与函数指针,成员函数指针的关系。

沐枫小筑

函数对象实质上是一个实现了operator()——括号操作符——的类。

例如:


class Add
{
public:
int operator()(int a, int b)
{
return a + b;
}
};

 

Add add; // 定义函数对象
cout << add(3,2); // 5


函数指针版本就是:


int AddFunc(int a, int b)
{
return a + b;
}
typedef int (*Add) (int a, int b);

Add add = &AddFunc;
cout << add(3,2); // 5


呵呵,除了定义方式不一样,使用方式可是一样的。都是:

cout << add(3,2);

既然函数对象与函数指针在使用方式上没什么区别,那为什么要用函数对象呢?很简单,函数对象可以携带附加数据,而指针就不行了。

下面就举个使用附加数据的例子:


class less
{
public:
less(int num):n(num){}
bool operator()(int value)
{
return value < n;
}
private:
int n;
};


使用的时候:

less isLess(10);

cout << isLess(9) << " " << isLess(12); // 输出 1 0

这个例子好象太儿戏了,换一个:


const int SIZE = 5;
int array[SIZE] = { 50, 30, 9, 7, 20};
// 找到小于数组array中小于10的第一个数的位置
int * pa = std::find_if(array, array + SIZE, less(10)); // pa 指向 9 的位置
// 找到小于数组array中小于40的第一个数的位置
int * pb = std::find_if(array, array + SIZE, less(40)); // pb 指向 30 的位置


这里可以看出函数对象的方便了吧?可以把附加数据保存在函数对象中,是函数对象的优势所在。

它的弱势也很明显,它虽然用起来象函数指针,但毕竟不是真正的函数指针。在使用函数指针的场合中,它就无能为力了。例如,你不能将函数对象传给qsort函数!因为它只接受函数指针。

要想让一个函数既能接受函数指针,也能接受函数对象,最方便的方法就是用模板。如:


template
int count_n(int* array, int size, FUNC func)
{
int count = 0;
for(int i = 0; i < size; ++i)
if(func(array[i]))
count ++;
return count;
}
这个函数可以统计数组中符合条件的数据个数,如:
const int SIZE = 5;
int array[SIZE] = { 50, 30, 9, 7, 20};
cout << count_n(array, SIZE, less(10)); // 2
用函数指针也没有问题:
bool less10(int v)
{
return v < 10;
}
cout << count_n(array, SIZE, less10); // 2


另外,函数对象还有一个函数指针无法匹敌的用法:可以用来封装类成员函数指针!

因为函数对象可以携带附加数据,而成员函数指针缺少一个类实体(类实例)指针来调用,因此,可以把类实体指针给函数对象保存起来,就可以用于调用对应类实体成员函数了。


template
class memfun
{
public:
memfun(void(O::*f)(const char*), O* o): pFunc(f), pObj(o){}
void operator()(const char* name)
{
(pObj->*pFunc)(name);
}
private:
void(O::*pFunc)(const char*);
O* pObj;
};

class A
{
public:
void doIt(const char* name)
{ cout << "Hello " << name << "!";}
};

 

A a;
memfun call(&A::doIt, &a); // 保存 a::doIt指针以便调用
call("Kitty"); // 输出 Hello Kitty!

大功告成了,终于可以方便保存成员函数指针,以备调用了。

不过,现实是残酷的。函数对象虽然能够保有存成员函数指针和调用信息,以备象函数指针一样被调用,但是,它的能力有限,一个函数对象定义,最多只能实现一个指定参数数目的成员函数指针。

标准库的mem_fun就是这样的一个函数对象,但是它只能支持0个和1个参数这两种成员函数指针。如 int A::func()或void A::func(int)、int A::func(double)等等,要想再多一个参数如:int A::func(int, double),不好意思,不支持。想要的话,只有我们自已写了。

而且,就算是我们自已写,能写多少个?5个?10个?还是100个(这也太恐怖了)?

好在boost库提供了boost::function类,它默认支持10个参数,最多能支持50个函数参数(多了,一般来说这够用了。但它的实现就是很恐怖的:用模板部份特化及宏定义,弄了几十个模板参数,偏特化(编译期)了几十个函数对象。

----

C++0x已经被接受的一个提案,就是可变模板参数列表。用了这个技术,就不需要偏特化无数个函数对象了,只要一个函数对象模板就可以解决问题了。期待吧

指针是C和C++语言编程中最重要的概念之一,也是最容易产生困惑并导致程序出错的问题之一。利用指针编程可以表示各种数据结构,通过指针可使用主调函数和被调函数之间共享变量或数据结构,便于实现双向数据通讯;并能像汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C和C++语言的功能。

在本文中,主要分两部分对指针进行讨论。首先,基础篇讨论关于指针的内容和运算操作等,可以是读者对指针的知识有一定了解和认识;随后在使用篇中重点讨论指针的各种应用,揭破指针在日常编程中的精髓,从而使读者能够真正地了解、认识和使用指针。

第一篇:基础篇

1.1指针的概念

谈到指针,它的灵活性和难控制性让许多程序员谈虎色变;但它的直接操作内存,在数据

操作方面有着速度快,节约内存等优点,又使许多C++程序员的深爱不以。那么指针究竟是怎么样一个概念呢?

其实, 指针就是一类变量,是一类包含了其他变量或函数的地址的变量。与其他变量所不同的是,一般的变量包含的是实际的真实的数据,而指针是一个指示器,它告诉程序在内存的哪块区域可以找到数据。

好了,在这里我们可以这样定义指针:指针是一类包含了其他变量或函数的地址的变量,它里面存储的数值被解释成为内存的地址。

1.2指针的内容

简单讲,指针有四个方面的内容:即指针的类型,指针所指向的类型,指针的值,指针本身所

占有的内存区。下面我们将分别阐述这些内容。

1.2.1指针的类型

从语法的角度看,指针的类型是指把指针声明语句中的指针名字去掉所剩下的部分。这是指针本身所具有的类型。例如:

int*ip; //指针的类型是int*

char*ip; //指针的类型是char*

int**ip; //指针的类型是int**

int(*ip)[5]; //指针的类型是int(*)[5]

1.2.2指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么类型来看待。从语法的角度看,指针所指向的类型是指针声明语句中的指针名字和名字左边的指针声明符*去掉所剩下的部分。例如:

int*ip; //指针所指向的类型是int

char*ip; //指针所指向的类型是char

int**ip; //指针所指向的类型是int*

int(*ip)[5]; //指针所指向的类型是int()[5]

1.2.3指针的值(或称指针所指向的内存区)

指针的值或者叫指针所指向的内存区或地址,是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是 32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在上例中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。

以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?

1.2.4指针本身所占有的内存区

指针本身所占有的内存区是指针本身占内存的大小,这个你只要用函数sizeof(指针的

类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。

指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。


1.3指针与内存管理

利用指针你可以将数据写入内存中的任意位置,但是,一旦你的程序中有一个野指针("wild“pointer),即指向一个错误位置的指针,你的数据就危险了—存放在堆中的数据可能会被破坏,用来管理堆的数据结构也可能会被破坏,甚至操作系统的数据也可能会被修改,有时,上述三种破坏情况会同时发生。所以合理的正确的分配指针的地址是非常重要的。

1.3.1内存分配的方式

内存分配方式有三种:

(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多,以下我们重点讲解动态内存分配。

1.3.2 malloc/free 的使用要点

malloc与free是C/C++语言的标准库函数,它用于申请动态内存和释放内存。

函数malloc的原型如下:

void * malloc(size_t size);

用malloc申请一块长度为length的整数类型的内存,程序如下:

int *ip = (int *) malloc(sizeof(int) * length);

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

malloc函数返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。这个你可以用sizeof(类型)去测试。

在malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 ip = malloc(sizeof(ip))这样的程序来。

函数free的原型如下:

void free( void * memblock );

为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

1.3.3 new/delete 的使用要点

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free.

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete.注意new/delete不是库函数,只是C++的运算符。我们来看如下例子就知道怎么回事了。

class Object

{

 public :

  Object(void){std::cout << “Initialization”<< std::endl; }

  ~Object(void){std::cout << “Destroy”<< std::endl; }

  void Initialize(void){std:: cout << “Initialization”<< std::endl; }

  void Destroy(void){ std::cout << “Destroy”<< std::endl; }

}

void UseMallocFree(void)

{

 Object *ip = (Object *)malloc(sizeof(Object)); // 申请动态内存

 ip->Initialize(); // 初始化

 //…

 ip->Destroy(); // 清除工作

 free(ip); // 释放内存

}

void UseNewDelete(void)

{

 Object *ip = new Object; // 申请动态内存并且初始化

 //…

 Delete ip; // 清除并且释放内存

}

用malloc/free和new/delete如何实现对象的动态内存管理

类Object的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于 malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数 UseNewDelete则简单得多。

所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete.由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。new内置了sizeof、类型转换和类型安全检查功能, ,对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。

new/delete 常使用的方法如下:

typeof *ip = new typeof[length];

类/结构 *ip = new 类结构;

一般释放如下:delete ip;

数组的释放如下:delete [] ip;

1.3.4内存耗尽怎么办?

如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。

(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  return;

 }

 …

}
(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  std::cout << “Memory Exhausted” << std::endl;

  exit(1);

 }

 …

}


(3)为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。因为32位操作系统支持 “虚存”,内存用完了,自动用硬盘空间顶替。我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

1.3. 5杜绝“野指针”

“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。 “野指针”的原因主要有如下几种:

(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

char *ip = NULL;

char *ip = new char;

(2)指针ip被free或者delete之后,没有置为NULL,让人误以为ip是个合法的指针。

(3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

class A

{

 public:

  void Func(void){ std::cout << “Func of class A” << std::endl; }

};

void Test(void)

{

 A *p;

 {

  A a;

  p = &a; // 注意 a 的生命期

 }

 p->Func(); // p是“野指针”

}

函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是有些编译器运行这个程序时居然没有出错,这可能与编译器有关。

1.3.6指针参数是如何传递内存的?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。见如下例子:

void GetMemory(char *ip, int num)

{

 ip = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

 char *str = NULL;

 GetMemory(str, 100); // str 仍然为 NULL

 strcpy(str, "hello"); // 运行错误

}

试图用指针参数申请动态内存

毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数ip的副本是 _ip,编译器使 _ip = ip.如果函数体内的程序修改了_ip的内容,就导致参数ip的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_ip申请了新的内存,只是把_ip所指的内存地址改变了,但是ip丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见如下示例:

void GetMemory(char **p, int num)

{

 *ip = (char *)malloc(sizeof(char) * num);

}

void Test(void)

{

 char *str = NULL;

 GetMemory(&str, 100); // 注意参数是 &str,而不是str

 strcpy(str, "hello");

 std::cout<< str << std::endl;

 free(str);

}

用指向指针的指针申请动态内存

当然,我们也可以用函数返回值来传递动态内存。这种方法更加简单,见如下示例:

char *GetMemory(int num)

{

 char *ip = (char *)malloc(sizeof(char) * num);

 return ip;

}

void Test(void)

{

 char *str = NULL;

 str = GetMemory(100);

 strcpy(str, "hello");

 std::cout<< str << std::endl;

 free(str);

}


用函数返回值来传递动态内存

用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见如下示例:

char *GetString(void)

{

 char p[] = "hello world";

 return p; // 编译器将提出警告

}

void Test(void)

{

 char *str = NULL;

 str = GetString(); // str 的内容是垃圾

 std::cout<< str << std::endl;

}

return语句返回指向“栈内存”的指针

最后,根据以上阐述,我们总结如下使用规则供大家参考:

【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL.防止使用指针值为NULL的内存。

【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

1.4指针的运算

1.4.1赋值运算

指针变量的赋值运算有以下几种形式:

1.4.1.1指针变量初始化赋值如下:

int a;

int *ip=&a;

1.4.1.2把一个变量的地址赋予指向相同数据类型的指针变量。例如:

int a;

int *ip;

ip=&a; //把整型变量a的地址赋予整型指针变量ip


1.4.1.3把一个指针变量的值赋予指向相同类型变量的另一个指针变量。例如:

int a;

int *pa=&a;

int *pb;

pb=pa; //把a的地址赋予指针变量pb

由于pa,pb均为指向整型变量的指针变量,因此可以相互赋值。

1.4.1.4把数组的首地址赋予指向数组的指针变量。例如:

int a[5],*pa;

pa=a; //数组名表示数组的首地址,故可赋予指向数组的指针变量pa

也可写为:

pa=&a[0]; //数组第一个元素的地址也是整个数组的首地址也可赋予pa

当然也可采取初始化赋值的方法:

int a[5],*pa=a;

以上是一些基本的数组赋值方法,后面我们会详细讨论指针在数组中的使用。

1.4.1.5把字符串的首地址赋予指向字符类型的指针变量。例如:

char *pc;

pc="c language";

或用初始化赋值的方法写为:

char *pc=" c language ";

这里应说明的是并不是把整个字符串装入指针变量, 而是把存放该字符串的字符数组的首地址装入指针变量。

1.4.1.6把函数的入口地址赋予指向函数的指针变量。例如:

int (*pf)();

pf=f; //f为函数名

1.4.2加减运算

对于指向数组的指针变量,可以加上或减去一个整数n.设ip是指向数组a的指针变量,则ip+n,ip-n,ip++,++ip,ip——,——ip 运算都是合法的。指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址加1或减1 在概念上是不同的。因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1.看如下例子:

char a[20];

int*ip=a;

...

ip++;

在上例中,指针ip的类型是int*,它指向的类型是int,它被初始化为指向整形变量a.接下来的第3句中,指针ip被加了1,编译器是这样处理的:它把指针ip的值加上了sizeof(int),在32位程序中,是被加上了4.由于地址是用字节做单位的,故ip所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。

由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。再看如下例子:

char a[20];

int*ip=a;

  ...

ip+=5;

在这个例子中,ip被加上了5,编译器是这样处理的:将指针ip的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20.由于地址的单位是字节,故现在的ip所指向的地址比起加5后的ip所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ip指向数组a的第0 号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。

如果上例中,ip是被减去5,那么处理过程大同小异,只不过ip的值是被减去5乘sizeof(int),新的ip指向的地址将比原来的ip所指向的地址向低地址方向移动了20个字节。

总结一下,一个指针ipold加上一个整数n后,结果是一个新的指针ipnew,ipnew的类型和ipold的类型相同,ipnew所指向的类型和 ipold所指向的类型也相同。ipnew的值将比ipold的值增加了n乘sizeof(ipold所指向的类型)个字节。就是说,ipnew所指向的内存区将比ipold所指向的内存区向高地址方向移动了n乘sizeof(ipold所指向的类型)个字节。

一个指针ipold减去一个整数n后,结果是一个新的指针ipnew,ipnew的类型和ipold的类型相同,ipnew所指向的类型和ipold所指向的类型也相同。ipnew的值将比ipold的值减少了n乘sizeof(ipold所指向的类型)个字节,就是说,ipnew所指向的内存区将比 ipold所指向的内存区向低地址方向移动了n乘sizeof(ipold所指向的类型)个字节。

1.4.3关系运算

指向同一个数组中的不同元素的两个指针可以进行各种关系运算。例如:

ip1==ip2表示ip1和ip2指向同一数组元素

ip1>ip2表示ip1处于高地址位置

ip1

指针变量还可以与0比较。设ip为指针变量,则ip==0表明ip是空指针,它不指向任何变量;ip!=0表示ip不是空指针。空指针是由对指针变量赋予0值而得到的。例如:

#define NULL 0

int *ip=NULL;

对指针变量赋0值和不赋值是不同的。指针变量未赋值时,可以是任意值,是不能使用的。否则将造成意外错误。而指针变量赋0值后,则可以使用,只是它不指向具体的变量而已。
1.4.4取地址运算符

‘&’和取内容运算符‘*’

取地址运算符&是单目运算符,其结合性为自右至左,其功能是取变量的地址。

取内容运算符*是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在*运算符之后跟的变量必须是指针变量。需要注意的是指针运算符*和指针变量说明中的指针说明符* 不是一回事。在指针变量说明中,‘*’是类型说明符,表示其后的变量是指针类型。而表达式中出现的‘*’则是一个运算符用以表示指针变量所指的变量。如下例子:

int a=12;

int b;

int *p;

int **ptr;

p=&a; //&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a的

//地址。

*p=24; //*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地址。

ptr=&p; //&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int **。该

//指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针

//p自己的地址。

*ptr=&b;//*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所指向的类型//是一样的,所以用&b来给*ptr赋值就是毫无问题的了。

**ptr=34;//*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指针再做一次*

//运算,结果就是一个int类型的变量。

1.4.5关于括号组合

在解释组合说明符时, 标识符右边的方括号和圆括号优先于标识符左边的“*”号,而方括号和圆括号以相同的优先级从左到右结合。但可以用圆括号改变约定的结合顺序。

阅读组合说明符的规则是“从里向外”。从标识符开始,先看它右边有无方括号或园括号,如有则先作出解释,再看左边有无*号。 如果在任何时候遇到了闭括号,则在继续之前必须用相同的规则处理括号内的内容。

1.5指针表达式

一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表式。所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。

有下面的一个简单的类:

class CNullPointCall
{
public:
static void Test1();
void Test2();
void Test3(int iTest);
void Test4();

private:
static int m_iStatic;
int m_iTest;
};

int CNullPointCall::m_iStatic = 0;

void CNullPointCall::Test1()
{
cout << m_iStatic << endl;
}

void CNullPointCall::Test2()
{
cout << "Very Cool!" << endl;
}

void CNullPointCall::Test3(int iTest)
{
cout << iTest << endl;
}

void CNullPointCall::Test4()
{
cout << m_iTest << endl;
}


那么下面的代码都正确吗?都会输出什么? CNullPointCall *pNull = NULL; // 没错,就是给指针赋值为空
pNull->Test1(); // call 1
pNull->Test2(); // call 2
pNull->Test3(13); // call 3
pNull->Test4(); // call 4


你肯定会很奇怪我为什么这么问。一个值为NULL的指针怎么可以用来调用类的成员函数呢?!可是实事却很让人吃惊:除了call 4那行代码以外,其余3个类成员函数的调用都是成功的,都能正确的输出结果,而且包含这3行代码的程序能非常好的运行。

经过细心的比较就可以发现,call 4那行代码跟其他3行代码的本质区别:类CNullPointCall的成员函数中用到了this指针。

对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。当程序被编译之后,此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据区别开,就是靠这个this指针。函数体内所有对类数据成员的访问, 都会被转化为this->数据成员的方式。

而一个对象的this指针并不是对象本身的一部分,不会影响sizeof(“对象”)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上 this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。

对于上面的例子来说,this的值也就是pNull的值。也就是说this的值为NULL.而Test1()是静态函数,编译器不会给它传递this指针,所以call 1那行代码可以正确调用(这里相当于CNullPointCall::Test1());对于Test2()和Test3()两个成员函数,虽然编译器会给这两个函数传递this指针,但是它们并没有通过this指针来访问类的成员变量,因此call 2和call 3两行代码可以正确调用;而对于成员函数Test4()要访问类的成员变量,因此要使用this指针,这个时候发现this指针的值为NULL,就会造成程序的崩溃。

其实,我们可以想象编译器把Test4()转换成如下的形式:


void CNullPointCall::Test4(CNullPointCall *this)
{
cout << this->m_iTest << endl;
}

而把call 4那行代码转换成了下面的形式: CNullPointCall::Test4(pNull);


所以会在通过this指针访问m_iTest的时候造成程序的崩溃。

下面通过查看上面代码用VC 2005编译后的汇编代码来详细解释一下神奇的this指针。

上面的C++代码编译生成的汇编代码是下面的形式:

CNullPointCall *pNull = NULL;
0041171E mov dword ptr [pNull],0
pNull->Test1();
00411725 call CNullPointCall::Test1 (411069h)
pNull->Test2();
0041172A mov ecx,dword ptr [pNull]
0041172D call CNullPointCall::Test2 (4111E0h)
pNull->Test3(13);
00411732 push 0Dh
00411734 mov ecx,dword ptr [pNull]
00411737 call CNullPointCall::Test3 (41105Ah)
pNull->Test4();
0041173C mov ecx,dword ptr [pNull]
0041173F call CNullPointCall::Test4 (411032h)


通过比较静态函数Test1()和其他3个非静态函数调用所生成的的汇编代码可以看出:非静态函数调用之前都会把指向对象的指针pNull(也就是 this指针)放到ecx寄存器中(mov ecx,dword ptr [pNull])。这就是this指针的特殊之处。看call 3那行C++代码的汇编代码就可以看到this指针跟一般的函数参数的区别:一般的函数参数是直接压入栈中(push 0Dh),而this指针却被放到了ecx寄存器中。在类的非成员函数中如果要用到类的成员变量,就可以通过访问ecx寄存器来得到指向对象的this指针,然后再通过this指针加上成员变量的偏移量来找到相应的成员变量。

下面再通过另外一个例子来说明this指针是怎样被传递到成员函数中和如何使用this来访问成员变量的。

依然是一个很简单的类:


class CTest
{
public:
void SetValue();

private:
int m_iValue1;
int m_iValue2;
};

void CTest::SetValue()
{
m_iValue1 = 13;
m_iValue2 = 13;
}


用如下的代码调用成员函数:


CTest test;
test.SetValue();

上面的C++代码的汇编代码为:

CTest test;
test.SetValue();
004117DC lea ecx,[test]
004117DF call CTest::SetValue (4111CCh)

同样的,首先把指向对象的指针放到ecx寄存器中;然后调用类CTest的成员函数SetValue()。地址4111CCh那里存放的其实就是一个转跳指令,转跳到成员函数SetValue()内部。


004111CC jmp CTest::SetValue (411750h)

而411750h才是类CTest的成员函数SetValue()的地址。


void CTest::SetValue()
{
00411750 push ebp
00411751 mov ebp,esp
00411753 sub esp,0CCh
00411759 push ebx
0041175A push esi
0041175B push edi
0041175C push ecx // 1
0041175D lea edi,[ebp-0CCh]
00411763 mov ecx,33h
00411768 mov eax,0CCCCCCCCh
0041176D rep stos dword ptr es:[edi]
0041176F pop ecx // 2
00411770 mov dword ptr [ebp-8],ecx // 3
m_iValue1 = 13;
00411773 mov eax,dword ptr [this] // 4
00411776 mov dword ptr [eax],0Dh // 5
m_iValue2 = 13;
0041177C mov eax,dword ptr [this] // 6
0041177F mov dword ptr [eax+4],0Dh // 7
}
00411786 pop edi
00411787 pop esi
00411788 pop ebx
00411789 mov esp,ebp
0041178B pop ebp
0041178C ret

下面对上面的汇编代码中的重点行进行分析:

1、将ecx寄存器中的值压栈,也就是把this指针压栈。

2、ecx寄存器出栈,也就是this指针出栈。

3、将ecx的值放到指定的地方,也就是this指针放到[ebp-8]内。

4、取this指针的值放入eax寄存器内。此时,this指针指向test对象,test对象只有两个int型的成员变量,在test对象内存中连续存放,也就是说this指针目前指向m_iValue1.

5、给寄存器eax指向的地址赋值0Dh(十六进制的13)。其实就是给成员变量m_iValue1赋值13.

6、同4.

7、给寄存器eax指向的地址加4的地址赋值。在4中已经说明,eax寄存器内存放的是this指针,而this指针指向连续存放的int型的成员变量 m_iValue1.this指针加4(sizeof(int))也就是成员变量m_iValue2的地址。因此这一行就是给成员变量m_iValue2 赋值。

通过上面的分析,我们可以从底层了解了C++中this指针的实现方法。虽然不同的编译器会使用不同的处理方法,但是C++编译器必须遵守C++标准,因此对于this指针的实现应该都是差不多的。

C++指针变量的用途2009-02-23 13:51:54.0 来源:中国IT实验室 关键词: C++ 指针变量 指针变量的用途:

指针变量只可以进行赋值运算和部分算术运算及关系运算

指针运算符:

1) 取地址运算符&:单目运算符,结合性为自右向左,其功能是取变量的地址。在scanf函数及指针变量赋值中应用

2) 解参考运算符*:单目运算符,其结合性为自右向左,其功能表示指针变量所指的变量。在运算符之后跟的变量必须是指针变量

注意:指针运算符*和解参考运算符*不同。前者中,“*”是类型说明符,表示其后的变量是指针类型,而表达式中出现的“*”则是一个运算符用以表示指针变量所值的变量,比如: # include <stdio.h>

int main()
{
int a = 5, *p = &a;
printf (/"%d//n/", *p);

return 0;
}

 

指针变量p取得了整型变量a的地址,而printf (/"%d//n/", *p);输出变量a的值

指针变量的运算:

1) 赋值运算:

指针变量初始化赋值

把一个变量的地址赋予指向相同数据类型的指针变量:

int a, *pa;

pa = &a;

把一个指针变量的值赋予指向相同类型变量的另一个指针变量:

int a, *pa = &a, *pb;

pb = pa; /*把a的地址赋予指针变量pb*/

把数组的首地址赋予指向数组的指针变量:

int a[5], *pa;

pa = a;

把字符串的首地址赋予指向字符类型的指针变量:

char *Ptr;

Ptr = /"I love you!/";

把函数的入口地址赋予指向函数的指针变量:

int (*pf)();

pf = f; /*f为函数名*/

2) 加减算术运算:

指针变量加减一个整数n的意义是把指针指向的当前位置(某数组)向前或向后移动n个位置:

int a[5], *pa;

pa = a; /*pa指向数组a,也就是指向a[0]*/

pa = pa + 2; /*pa指向a[2],即pa的值为&pa[2]*/

指针变量的加减运算只有对数组执行时才有意义

3) 两个指针变量之间的运算:

只有指向同一数组的两个指针变量之间才能进行运算:

两指针相减所得之差是两个指针所指数组元素之间相差的元素个数:

pf1的值为2010H,pf2的值为2000H,浮点数组每个元素占4个字节,pf1-pf2的结果为(2010H-2000H)/4=4

指针加法无任何意义

指向同一数组的两指针变量进行关系运算可表示它们所值数组元素之间的关系:

pf1==pf2:pf1和pf2指向同一数组元素;

pf1>pf2:pf1处于高地址位置;

pf1<pf2:pf2处于低地址位置。

指针变量与0比较:

设p为指针变量则

p==0:p是空指针,它不指向任何变量

p!=0:p不是空指针

空指针是由对指针变量赋予0值而得到的:

#define NULL 0

int *p=NULL;

对指针变量赋0值和不赋0值不同的。指针变量为赋值时,可以是任意值,是不能使用的。否则将造成以外错误。而指针变量赋0值后,则可以使用,只是它不指向具体的变量而已


C++指针使用方法解惑2007-05-22 来源:e800.net频道 关键词: 使用 方法 在下列函数声明中,为什么要同时使用*和&符号?以及什么场合使用这种声明方式?

void func1( MYCLASS *&pBuildingElement );
论坛中经常有人问到这样的问题。本文试图通过一些实际的指针使用经验来解释这个问题。
仔细看一下这种声明方式,确实有点让人迷惑。在某种意义上,"*"和"&"是意思相对的两个东西,把它们放在一起有什么意义呢?。为了理解指针的这种做法,我们先复习一下C/C++编程中无所不在的指针概念。我们都知道MYCLASS*的意思:指向某个对象的指针,此对象的类型为MYCLASS。 void func1(MYCLASS *pMyClass);

// 例如: MYCLASS* p = new MYCLASS;
func1(p);
上面这段代码的这种处理方法想必谁都用过,创建一个MYCLASS对象,然后将它传入func1函数。现在假设此函数要修改pMyClass: void func1(MYCLASS *pMyClass)
{
DoSomething(pMyClass);
pMyClass = // 其它对象的指针
}

第二条语句在函数过程中只修改了pMyClass的值。并没有修改调用者的变量p的值。如果p指向某个位于地址0x008a00的对象,当func1返回时,它仍然指向这个特定的对象。(除非func1有bug将堆弄乱了,完全有这种可能。)
现在假设你想要在func1中修改p的值。这是你的权利。调用者传入一个指针,然后函数给这个指针赋值。以往一般都是传双指针,即指针的指针,例如,CMyClass**。 MYCLASS* p = NULL;
func1(&p);

void func1(MYCLASS** pMyClass);
{
*pMyClass = new MYCLASS;
……
}

调用func1之后,p指向新的对象。在COM编程中,你到处都会碰到这样的用法--例如在查询对象接口的QueryInterface函数中: interface ISomeInterface {
HRESULT QueryInterface(IID &iid, void** ppvObj);
……
};
LPSOMEINTERFACE p=NULL;
pOb->QueryInterface(IID_SOMEINTERFACE, &p);

该如何用好c++的const 2009-02-05 16:23:09.0 来源:中国IT实验室 关键词: c++ const const是c++当中很有特色的一个语言功能,它限制了对数据的操作,还限制了类成员函数的行为,而且是对c++的引用类型的函数参数和返回值这个功能的有益补充,c++程序员应该时时有意识地正确使用const关键字。const对于变量来说,是一个要求,而对于函数来说,是一个承诺,对它所操作的变量的承诺。由于const的使用场合和使用对象很多,要正确地使用它还是需要用一番心思的。

const在一下一些地方使用:

首先它可以修饰一个变量,此时这个const是这个变量的一个要求——它不可以被修改,所以这个变量必须在初始化的时候被赋值,初始化之后就不能赋值了,是一个只读的变量,见本文末尾的例子(2)。这意味着它不可以以任何方式被赋值,包括不能做赋值表达式的左值,同时不可以把它传递给那些没有承诺不更改它的值的函数。

那么函数怎么做这个承诺呢,看第五段。如果一个int类型变量i是const的,那么它的地址&i也是const的,从(3)中可以看出,&i只能赋值给const的指针变量p而不可以赋值给q,因为我们可以通过赋值给*q而更改它所指向的变量i. 从这里我们可以看到,上述声明表示我们不可以通过p修改它所指向的变量的值,也就是说*p也是一个常量了,所以 *p = 5 这样的表达式是错误的(4)。要注意的是, p作为一个变量,它自身是可以修改的(6),p的声明当中的const要求不可以通过p更改它所指向的值,是p对它所指向的变量的承诺。当它所指向的变量不需要这样的承诺时,p自然仍然可以指向那个变量(6),但是(7)仍然是错误的,因为p有承诺不可以改变它所指向的变量的值。另外,如果我们想要求p指针变量自身也不可以修改,那我们应该如(9)中所做,并且注意此时r必须在初始化时候赋值,否则它就不可能再被赋值了(10)。而从(11)中我们可以看出,指针变量k要求它自身是只读的,但是没有承诺不可以通过它来修改它所指向的变量,因而不可以把&i赋值给它。

当一个对象实例被const修饰后,它自身的状态在初始化之后就不可以修改了,这时候不仅上面的约束必须成立,而且我们不可以调用它的成员函数,因为它的成员函数可能会修改它的状态,除非那个成员函数承诺不会修改this对象的状态。而承诺的方式至少是在函数签名末尾加上"const",如(12)所示。而如果这个函数要返回一个非mutable的数据成员的引用的话,那么返回类型必须加上const表示那个返回的引用不可以被用于修改对象的状态,这样这个成员函数才完整地承诺了它不会被用于修改对象的状态(13),否则,就不可以说它是一个常量成员函数(14)。事实上,如果一定要返回数据成员的引用(比如拷贝构造的代价很大时),那么强烈建议返回const引用,以便禁止修改这个数据成员,否则将严重违反类的封装性,这种违反可能带来暂时的编码方便性,但是长期来看,一定会吃苦头的。

注意在(12)中,虽然返回了数据成员message_,但是我们返回的是它的一个copy而不是那个对象自身,所以get_message()函数不可能被用于修改message_数据成员,而(12.1)错就错在它返回了message_的引用。

那么什么是一个对象的状态呢,默认是这个对象的所有数据成员,但是在有一些情景下,一个对象的某些成员变量可能对于它的状态没有决定作用,这完全决定于类的设计者。如果类Foo中有这样的成员变量的话,我们需要把它声明为mutable,表示即使一个Foo的实例是常量,我们也可以修改这个成员,同理,类 Foo的那些承诺不修改对象状态的函数也可以修改这个成员变量(12)。类当中也可以有const的数据成员,它也像独立的const变量一样,完全不可以以任何方式修改它的值,所以它必须在类的构造函数的初始化列表中被初始化,在构造函数体中初始化是不可以的(15)。

那么一个独立的函数或者类的成员函数怎么承诺不会修改传入的变量自身呢?对于诸如(16)这样的函数,它们不会面临这个问题,因为所有的参数都是在传值,在(17)例子中,实际参数m, n传入函数add后,它们的值分别被赋值给其形参a和b,之后两个实际参数m, n就与函数add无关了,自然add不会修改到m和n的值。真正需要面对这个问题的是那些传递引用的函数,这里和下文的引用是广义的引用,包括& 修饰的引用类型变量,以及通过传递指针值来“引用”到实际参数本身的情形,如例子(18)。 当一个函数的参数中有某个参数是在传递引用,那么,如果这个函数确实不需要修改那个实际参数本身的值,那么一定要把那个参数声明为const的,这样,不仅可以传入可读可写的变量作为那个实参,而且可以传入一个const变量,因为我们已经承诺了不会修改那个实际参数本身的值(19)。在函数参数中传递引用常常是很有效的——如果对象拷贝构造的代价很大,或者要操纵实际参数自身——但是一定要在可能的时候,声明引用为const的。const在类中还有一个用处——用于定义静态常量数据成员,如例子(20)。

在设计类的时候,必须考虑清楚哪些数据成员应该是const的,初始化后就不可以再修改;哪些数据成员是mutable的,对对象的状态定义没有影响;哪些成员函数不会修改对象的状态,应该声明为常量函数,不会修改对象状态;那些函数不会修改引用参数的值,可以是const的。在应该和可以使用const 的地方一定使用它。

const的效果可能被一下一些语言功能所抵消:1. const_cast<> 2. C风格的强制类型转换。在设计良好的代码中,我们要非常小心地使用这种抵消const功能的语言功能,特别是第2种,应该被禁用。例如,类的使用者看到函数引用参数的const属性,会放心地把自己的常量数据传入函数,却想不到这个承诺是假的。这样的bug非常难调试。再比如,一个const函数返回了一个const的数据成员的引用,但是这个函数的调用者却使用1或者2方法抵消了const的作用,那么这个对象的状态改变很难追踪,这样的代码是非常危险的代码,应该坚决地避免使用。在使用某些旧的c语言代码时候,我们可能不得不做 第2类强制转换,这是程序员应该予以足够的特殊标注和注释。

在最新的C语言标准(c99)中,const也被引入了,可以修饰函数的参数和返回值,以及变量,使用方法与上面相同,新的C标准库也同步做了更新,所以在C语言中,也是要在可以使用const的地方一定使用它,原因也同上。 (1) const int i = 3;
(2) i = 5; ×
(3) const int *p = &i; OK
int *q = &i; ×
(4) *p = 5; ×
(5) int j = 6; OK
(6) p = &j; Ok
(7) *p = 5; ×
(8) j = 5; OK
(9) const int *const r = &i; OK
(10) r = &j; ×
(11) int *const k = &i; ×

class Foo {
string message_;
const string my_name_;
mutable int junk_;
const static size_t max_num_; (20)
public:
string get_message() const (12)
{
junk_ = 1; // OK
return message_;
}

string& get_message() const; × (12.1)

const string& get_message_ref() const (13)
{
return message_;
}

string& get_message_ref() (14)
{
return message_;
}

Foo() : my_name("david"){} (15)

}; // Foo

int add(int a, int b); (16)

int m = 6, n = 7; (17)
add(m, n); (17)

void negate(int& a); (18)
void swap(int *a, int *b); (18)
size_t strlen(const char *); (19)

 

转载自:http://blog.csdn.net/wangzhihua987344319/article/details/5108524

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics