关于函数(一)const与函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Acmer_Sly/article/details/75269982

1、概述

       在普通的非 const成员函数中,this的类型是一个指向类类型的 const指针。可以改变this所指向的值,但不能改变 this所保存的地址。在 const成员函数中,this的类型是一个指向 const类类型对象的 const指针。既不能改变 this所指向的对象,也不能改变 this所保存的地址。
       看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。
       const 是constant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。

2、用const修饰函数的参数

       如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。const只能修饰输入参数:如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。
       例如:StringCopy函数:void StringCopy(char*strDestination, const char *strSource);
其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。
       例如不要将函数voidFunc1(int x) 写成voidFunc1(const int x)。同理不要将函数voidFunc2(A a) 写成voidFunc2(const A a)。其中A为用户自定义的数据类型。对于非内部数据类型的参数而言,象voidFunc(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间
       为了提高效率,可以将函数声明改为voidFunc(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数voidFunc(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为voidFunc(const A &a)。以此类推,是否应将voidFunc(int x) 改写为voidFunc(const int&x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。另外,形参初始化的方式和变量的初始化方式是一样的,在博文const引用和const指针做过讲解,就不再赘述了。
       总结一下就是

对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。例如将voidFunc(A a) 改为voidFunc(const A &a)。
对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如voidFunc(int x) 不应该改为voidFunc(const int &x)。

3、用const修饰函数的返回值

       如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。例如函数const char * GetString(void);
如下语句将出现编译错误:
char* str = GetString();
正确的用法是
const char *str =GetString();
       如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。
例如不要把函数int GetInt(void) 写成const int GetInt(void)。
同理不要把函数AGetA(void) 写成constA GetA(void),其中A为用户自定义的数据类型。
如果返回值不是内部数据类型,将函数AGetA(void) 改写为constA &GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
例如:

classA
{
A & operate = (const A &other); // 赋值函数
};
A a, b, c; // a, b, c 为A的对象
a= b = c; // 正常的链式赋值
(a= b) = c; // 不正常的链式赋值,但合法

       如果将赋值函数的返回值加const修饰,那么该返回值的内容不允许被改动。上例中,语句a= b = c 仍然正确,但是语句(a= b) = c 则是非法的。

4、const 成员函数

       任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。以下程序中,类stack的成员函数GetCount仅用于计数,从逻辑上讲GetCount应当为const函数。编译器将指出GetCount函数中的错误。

classStack
{
public:
    void Push(int elem);
    int Pop();
    intGetCount() const; // const 成员函数
private:
    intm_num;
    int m_data[100];
};
int Stack::GetCount()const
{
    ++ m_num; // 编译错误,企图修改数据成员m_num
    Pop();// 编译错误,企图调用非const函数
    returnm_num;
}

       const 成员函数的声明看起来怪怪的:const关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。
关于const函数的几点规则:
a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
d.加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的。
       关于b点,做一个详细说明

const对象的成员不可修改,但const对象通过指针维护的对象却可以修改。

如下面的代码

#include<iostream>
using namespace std;
class foo
{
public:
void test1()
{
    cout << "I am not a const member function" << endl;
}
void test2()const
{
    foo *temp = (foo*)this;//注意这个转换!
    temp->test1();
}
};

int main()
{
    foo f;
    f.test2();
    return 0;
}

①首先,const对象只能调用const成员函数是因为const函数不会改变成员对象,这点和const对象的本意是相同的,其他函数有可能会改变成员变量,所以编译器拒绝通过调用非const函数
②这里的转换是说,我另建立了一个指针,而不是原来的东西,简单的来说就是一个copy,去掉了const属性(当然真实情况下并不是真正的copy)
换个风格:

void test2()const
{
    foo *temp = const_cast<foo*>(this);
    temp->test1();
}

也就是说,转换成了一个非const的成员,C的转换权限太大,直接就把所有的东西都转换了..这里其实只转换了const属性,const_cast的功能在这里就是:把常量指针被转化成非常量的指针,并且仍然指向原来的对象。

附:const放在后面有什么意思?

一个函数AcGePoint3dstartPoint() const;const放在后面跟前面有区别么
准确的说const是修饰this指向的对象的
比如,我们定义了

classA{
public:
    f(int);
};

       这里f函数其实有两个参数,第一个是A*const this, 另一个才是int类型的参数
如果我们不想f函数改变参数的值,可以把函数原型改为f(const int),但如果我们不允许f改变this指向的对象呢?因为this是隐含参数,const没法直接修饰它,就加在函数的后面了,表示this的类型是const A *const this。
       总之,const修饰*this是本质,至于说“表示该成员函数不会修改类的数据。否则会编译报错”之类的说法只是一个现象,根源就是因为*this是const类型的。

5、重点补充:不要返回局部对象的引用或指针,也不要返回函数内部用new初始化的指针的引用

1、 返回一个局部对象的引用。它的问题在于,局部对象 —– 顾名思义 —- 仅仅是局部的。也就是说,局部对象是在被定义时创建,在离开生命空间时被销毁的。所谓生命空间,是指它们所在的函数体。当函数返回时,程序的控制离开了这个空间,所以函数内部所有的局部对象被自动销毁。因此,如果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前被销毁了。当想提高程序的效率而使函数的结果通过引用而不是值返回时,这个问题就会出现。下面的例子其目的在于详细说明什么时候该返回引用,什么时候不该:

class Rational {          // 一个有理数类
// 注意operator* (不正确地)返回了一个引用
friend const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs);   //友元函数
public:
  Rational(int numerator = 0, int denominator = 1);
  ~Rational();
  ...
private:
  int n, d;               // 分子和分母
};
// operator*不正确的实现
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result;
}

这里,局部对象result在刚进入operator*函数体时就被创建。但是,所有的局部对象在离开它们所在的空间时都要被自动销毁。具体到这个例子来说,result是在执行return语句后离开它所在的空间的。所以,如果这样写:

Rational two = 2;
Rational four = two * two;         // 同operator*(two, two)

函数调用时将发生如下事件:

① 局部对象result被创建。
② 初始化一个引用,使之成为result的另一个名字;这个引用先放在另一边,留做operator*的返回值。
③ 局部对象result被销毁,它在堆栈所占的空间可被本程序其它部分或其他程序使用。
④ 用步骤2中的引用初始化对象four。

       一切都很正常,直到第4步才产生了错误,借用高科技界的话来说,产生了”一个巨大的错误”。因为,第2步被初始化的引用在第3步结束时指向的不再是一个有效的对象,所以对象four的初始化结果完全是不可确定的。
教训很明显:别返回一个局部对象的引用。

补充: 后来,有人就说,结果的four代表的有理数就是4啊,答案是对的,four的初始化结果是对的,但是,就像我之前看过的一篇帖子上说的,局部变量是存在于栈中的,函数被调用时先在栈中为变量申请空间,调用完成释放变量空间。函数在返回参数的时候是这样的,先把要返回的数放在寄存器eax中,然后回到主函数中取出eax中的数值放在变量里,所以这样是不涉及函数中变量地址的。如果要返回引用,也就是变量地址,那么它会把这个变量的地址放在eax中,(注意这个地址是位于函数的栈空间里的,出了这个函数,这块内存就会被系统标记为可占用(就是其它程序可以占用),只是标记了,此时操作系统还没有立刻将本地变量的内存分配给其他变量,但这是暂时的、不稳定的。也就是说当这块内存没有没被占用的时候,引用指向的那个局部对对象的值仍未改变,所以你这时候用这个值给four初始化当然能够成功初始化),回到主函数后系统会把这个地址赋值给主函数中的指针变量。此时主函数中的指针变量就指向了一个已经被标记为可占用的内存空间。如果你在不同的时刻输出这个指针所指地址的值会输出不同的结果。如下:

#include <iostream>
using namespace std;
int &Fun1()
{
    int gg = 250;
    return gg;
}
void Fun2()
{

}
int main()
{
    int &pp = Fun1();  //如果时int pp=Fun1();的话,此时那块内存还没被占用,那么能够正确初始化pp,并且pp也不是那块系统已标记为可占用的内存里面的,(pp的内存是在堆上,而不是在栈上面)所以后面的操作都能正确进行,即pp的值就一直是250。就像我上面的代码一样。
    cout << pp;   //250
    Fun2();
    cout << pp;   //-858993460
    return 0;
}

       为什么运行了一个空函数Fun2(),pp的值就变了,因为此时Fun2()函数此时被调用,就要先在栈中为变量申请空间,那么之前那块被标记为可占用的内存就要被清空然后占用了,所以此时pp的值发生了改变。

2、 “那好,”你可能会说,”问题不就在于要使用的对象离开它所在的空间太早吗?我能解决。不要使用局部对象,可以用new来解决这个问题。”像这样:

// operator*的另一个不正确的实现
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  // create a new object on the heap
  Rational *result =
    new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  // return it
  return *result;
}

       这个方法的确避免了上面例子中的问题,但却引发了新的难题。大家都知道,为了在程序中避免内存泄漏,就必须确保对每个用new产生的指针调用delete,但是,这里的问题是,对于这个函数中使用的new,谁来进行对应的delete调用呢?
       显然,operator*的调用者应该负责调用delete。真的显然吗?遗憾的是,即使你白纸黑字将它写成规定,也无法解决问题。之所以做出这么悲观的判断,是基于两条理由:

第一,这样之后,无论何时调用operator*后必须得到结果的指针然后调用delete,也是说,我们必须这样使用operator*:

const Rational& four = two * two;      // 得到废弃的指针;
                                       // 将它存在一个引用中
...
delete &four;                          // 得到指针并删除

记住,只要有哪怕一个operator*的调用者忘了这条规则,就会造成内存泄漏。

第二,返回废弃的指针还有另外一个更严重的问题,即使是最尽责的程序员也难以避免。因为常常有这种情况,operator*的结果只是临时用于中间值,它的存在只是为了计算一个更大的表达式。例如:

Rational one(1), two(2), three(3), four(4);
Rational product;
product = one * two * three * four;

product的计算表达式需要三个单独的operator*调用,以相应的函数形式重写这个表达式会看得更清楚:

product = operator*(operator*(operator*(one, two), three), four);

是的,每个operator*调用所返回的对象都要被删除,但在这里无法调用delete,因为没有哪个返回对象被保存下来。
解决这一难题的唯一方案是叫用户这样写代码:

const Rational& temp1 = one * two;
const Rational& temp2 = temp1 * three;
const Rational& temp3 = temp2 * four;
delete &temp1;
delete &temp2;
delete &temp3;

所以,这样就相当不方便,所以要记住你的教训:写一个返回废弃指针的函数无异于坐等内存泄漏的来临。
3、 另外,局部变量的指针和局部指针变量是两个不同概念。局部变量在函数体结束后生命期也结束,它的指针(即它的地址)是无效变量的地址,所以函数不能返回这种地址值。局部指针变量在函数结束后生命期也结束,但它指向的变量或函数或任何存储实体的生命期没有结束,函数返回的指针(地址)就是有效的。
例1:

int *func()
{
   int x;
   ...
   return &x;/*返回局部变量的地址--无效*/
}

例2.

int *max(int a[],int n)
{
   int *p=a[0];
   int i;
   for(i=1;i<n;i++)
   {
       if(*p<a[i])
            p=&a[i];/*p指向更大的数*/return p;
}/*尽管p变量生命期结束了,但函数返回它的值,是实参数组中最大元素的地址--有效*/ 

猜你喜欢

转载自blog.csdn.net/Acmer_Sly/article/details/75269982