const的使用

来源:互联网 发布:晋升锦标赛与政治网络 编辑:程序博客网 时间:2024/06/05 08:53

const的使用


《Effective C++》条款3 学习总结


  • const的使用
    • const与指针
    • const与类
    • const成员函数
    • bitwise constness又称physical constness和logical constness
    • 在const和non-const函数的重载重避免代码重复
  • 转载请注明出处


1.const与指针

#include <iostream>int main(){    char str[]="hello world";    char *p1(str);              //指向非常量的非常量指针   指向数据:非常量 指针:非常量    const char *p2(str);        //指向常量的非常量指针     指向数据:常量 指针:非常量    char const *p3(str);        //指向常量的非常量指针     指向数据:常量 指针:非常量    char * const p4(str);       //指向非常量的常量指针     指向数据:非常量 指针:常量    const char * const p5(str); //指向常量的常量指针       指向数据:常量 指针:常量    return 0;}

上面的代码看起来有点晕,有一个规律:
1.以指针的*(星号)为分界点
2.星号 左边 的const修饰的是指针所指向的数据
3.星号 右边 的const修饰的是指针本身
4.const int 和 int const 等价


2.const与类

const的一个重要作用就是:防止用户或程序员意外修改了不应该被修改的值或其他的东西。


举个例子:
我们都知道右值是不能放在等号左边被赋值的

#include <iostream>int main(){    int a(10),b(100),c(1110);    (a+b)=c;  //将a与b相加,然后将c的值赋值给(a+b)的结果    return 0;}

错误如下:
这里写图片描述


但是如果是类,并且重载了+运算符

#include <iostream>class demo{public:    demo(){}    demo(int data_):data(data_){}    int data=0;    //重载+运算符    demo operator+(const demo & other){        return demo(this->data+other.data);    }};int main(){    demo a(10),b(100),c(1110);    (a+b)=c;  //将a与b相加,然后将c的值赋值给(a+b)的结果    return 0;}

完美的编译通过了,但是(a+b)确实是右值,这不是矛盾了吗?
所以,我们可以将operator+写成

    const demo operator+(const demo & other){        return demo(this->data+other.data);    }

结果,编译提示错误:
这里写图片描述


当然你会说,没有人会那么变态地(a+b)后用c为之赋值。
那么下面一个例子也有足够的理由让你在从在运算符时重视const属性的添加与否。

if(a+b=c){    //do something}else{    //do something else}

你看到端倪了吗?
if(a+b=c) ,我们都有粗心的时候,加上const就不会为这种低级错误抓耳挠腮了。当然如果你有足够的自信不会把==写成=,那么加不加const也没有什么区别。


3.const成员函数

const用于成员函数的方式可以是下面的这种方式:

class demo{public:    demo(){}    const demo& do_something()const{        //do something    }};

这种方式的存在理由至少有下面两点:
(来自《Effective C++》)
1.它们使class比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。
2.它们使操作const对象成为可能。(改善C++程序效率的一个根本方法是以pass by reference-to-const方式传递对象,此技术可行的前提是,我们有const成员函数可用来处理取得的const对象)。


第一点很容易理解,第二点需要稍微解释(有经验的读者可以跳过,这里仅供初学者观看)
1.使用reference-to-const可以防止不必要的对象拷贝
(传值和传递引用的根本性的不同指出就是:由于函数有副本机制,传值会激发函数的副本机制,传入的是对象的拷贝,操作的是对象的拷贝,而不是我们传入的那个参数本身)

2.同时可以防止对象被意外修改(因为我们使用了const引用)(此时我们很可能只是取出对象中的某些值加以操作,而不会改变对象的任何一个成员)。

3.两个成员函数,如果只是常量性不同,可以被重载(这是C++一大特性)

class demo{public:    demo():m_dat(0){}    demo(int dat):m_dat(dat){}    void print(){        std::cout<<"void print() called"<<std::endl;        std::cout<<m_dat<<std::endl;    }    void print()const{        std::cout<<"void print()const called"<<std::endl;        std::cout<<m_dat<<std::endl;    }private:    int m_dat;};

函数中两个print没有本质的区别,只是 void print()const 发誓始终不会对类进行任何改动。

当然我们不会重载如此无聊的函数,只是作为一个例子,更好的例子是iterator:(下面的例子是< array >头文件中的begin函数,用于返回一个迭代器)

      // Iterators.      iterator      begin() noexcept      { return iterator(data()); }      const_iterator      begin() const noexcept      { return const_iterator(data()); }

3.非const函数无法操作const对象

#include <iostream>class demo{public:    demo():m_dat(0){}    demo(int dat):m_dat(dat){}    void print(){        std::cout<<"void print() called"<<std::endl;        std::cout<<m_dat<<std::endl;    }private:    int m_dat;};int main(){    const demo mx(200);    mx.print();    return 0;}

编译出错:
这里写图片描述
正如编译器所说,我们无法调用非const成员函数 print

为什么呢?
:因为我们操作的是const对象,也就是该对象禁止我们对其进行任何改动。然而,作为非const成员函数并没有发誓:我不会对对象进行任何的改动。所以,当我们调用print成员函数时,print很有可能对对象进行改动,这是不符合C++const属性的,所以编译器禁止非const函数的调用。

所以我们对代码进行如下改动:

#include <iostream>class demo{public:    demo():m_dat(0){}    demo(int dat):m_dat(dat){}    void print(){        std::cout<<"void print() called"<<std::endl;        std::cout<<m_dat<<std::endl;    }    void print()const{        std::cout<<"void print()const called"<<std::endl;        std::cout<<m_dat<<std::endl;    }private:    int m_dat;};int main(){    demo m(100);    m.print();    const demo mx(200);    mx.print();    return 0;}

这里写图片描述
如上图,完美运行。可以看出,const对象调用的是void print()const重载。


4.bitwise constness(又称physical constness)和logical constness

(来自《Effective C++》)
bitwise constness成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是说他不更改对象内的任何一个bit。这种论点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性(constness)的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

但是有些不完全的const函数却可以逃过bitwise constness的检测:

#include <iostream>#include <cstring>class demo{public:    //默认的构造函数    demo():m_str(nullptr){}    //用字符串初始化的构造函数    demo(char *str):m_str(new char[std::strlen(str)+1]){        strcpy(m_str,str);    }    //重载[]    char & operator[](std::size_t position)const{        return m_str[position];    }private:    //存储的字符    char * m_str;    //友元函数,重载输出流<<    friend std::ostream & operator<<(std::ostream & out,const demo&obj);};std::ostream & operator<<(std::ostream & out,const demo&obj){    return out<<obj.m_str;}int main(){    const demo mydemo("hello world");//构造一个demo对象mydemo    std::cout<<mydemo<<std::endl;//打印mydemo中的字符串    mydemo[0]='z';      //取出mydemo的中存储的第一个字符,并赋值为‘z’    std::cout<<mydemo<<std::endl;//再次打印mydemo中的字符串    return 0;}

这里写图片描述
如图,hello world 竟然变成了 zello world


从上面的例子中,我们发现char & operator[](std::size_t position)const虽然声明为const成员函数,但是他的返回值确实一个 非常引用。所以我们可以修改之。

但是mydemo的确是const对象,我们怎么就修改了它的成员呢?
:我们此时并没有修改mydemo的成员,但是我们修改了mydemo成员m_str所指向的数据。(但是这并不是我们使用const对象的初衷,我们的初衷是:哪怕是指向的数据也是无法被修改的)
mydemo的确是const对象,我们可以很直观的知道demo的成员m_str此时具有const属性,我们无法修改它。但是此时的const指的是,m_str这个指针(他是demo的一个成员指针变量)无法被修改const函数修改,但并不意味着它所指向的数据无法修改。

这里的问题关键是:我们让operator[]返回了一个非常量引用。我们只需将返回值声明为const即可:

    const char & operator[](std::size_t position)const{        return m_str[position];    }

上述这种情况值得就是logical constness:(来自《Effective C++》)一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。

还有一个问题:有时我们需要修改某些变量,但是const对象只允许调用const函数,而const函数中,我们无法对对象中的任何变量做出修改。此时我们就需要用到mutable变量。
mutable可以释放掉non-static成员的bitwise constness约束。
上例子:(下面我们重载了adjust_length的const版本和非const版本)

#include <iostream>#include <cstring>class demo{public:    demo():m_str(nullptr),length(0){}    demo(char *str){        length=std::strlen(str);        m_str=new char[this->length+1];        strcpy(m_str,str);    }    void adjust_length()const{        std::size_t tmp=strlen(m_str);        if(length!=tmp){            length=tmp;        }    }    void adjust_length(){        std::size_t tmp=strlen(m_str);        if(length!=tmp){            length=tmp;        }    }private:    char * m_str;    std::size_t length;};int main(){    return 0;}

编译器提示错误:
这里写图片描述

错误处在下面这一段代码:

    void adjust_length()const{        std::size_t tmp=strlen(m_str);        if(length!=tmp){            length=tmp;        }    }

我们无法校对const对象的length,而这种校对又显得十分重要(虽然不太必要,仅一个例子),我们可以这样做,如下:

#include <iostream>#include <cstring>class demo{public:    demo():m_str(nullptr),length(0){}    demo(char *str){        length=std::strlen(str);        m_str=new char[this->length+1];        strcpy(m_str,str);    }    void adjust_length()const{        std::size_t tmp=strlen(m_str);        if(length!=tmp){            length=tmp;        }    }    void adjust_length(){        std::size_t tmp=strlen(m_str);        if(length!=tmp){            length=tmp;        }    }private:    char * m_str;    mutable std::size_t length;};int main(){    return 0;}

精髓就在:这里写图片描述 这里。


5.在const和non-const函数的重载重避免代码重复

假设有下面这样的一个类:

class demo{public:    demo():m_str(nullptr){}    demo(char *str):m_str(new char[std::strlen(str)+1]){        strcpy(m_str,str);    }    const char & operator[](std::size_t position)const{        //....检查position是否越界        //....检查数据的完整新        //....对该操作进行记录,写入log(日志)        return m_str[position];    }    char & operator[](std::size_t position){        //....检查position是否越界        //....检查数据的完整新        //....对该操作进行记录,写入log(日志)        return m_str[position];    }private:    char * m_str;};

我们会发现,operator[]的const和非const版本的过程极度相似,甚至二者之间的差别只是返回值const和非const的差别。

即使我们将中间的类似过程封装为demo类的private成员函数,但是我们还是不可避免的造成了一些不必要的开销,如函数的调用,函数的返回值等。

问题解决:Casting away constness

安全版本:

class demo{public:    demo():m_str(nullptr){}    demo(char *str):m_str(new char[std::strlen(str)+1]){        strcpy(m_str,str);    }    const char & operator[](std::size_t position)const{        //....检查position是否越界        //....检查数据的完整新        //....对该操作进行记录,写入log(日志)        return m_str[position];    }    char & operator[](std::size_t position){        return const_cast<char&>(static_cast<const demo>(*this)[position]);    }private:    char * m_str;};

代码的亮点就在:

    char & operator[](std::size_t position){        return const_cast<char&>(static_cast<const demo>(*this)[position]);    }

解释:此时我们做了两次数据类型转换。

第一次:

static_cast<const demo>(*this)[position];

这里我们为了调用const版本的operator[] ,我们只有将对象转换为const对象然后再调用const版本的operator[] ,因为我们没有直接的语法可以调用const版本的函数重载。

第二次:

const_cast<char&>(/*......*/);

此时,我们将const版本调用后返回的const char&转换为char&,去掉const属性我们要调用的是const_cast。第一次之所以调用static_cast是因为第一次转换并没有涉及const属性的去除。

这里我们用的的技巧是:当const版本和非const版本函数重载其操作非常相似时,我们可以通过 非const版本函数调用 const版本函数重载 来避免代码重复。


值得注意的是:上述行为的反向行为是危险的。这个反向行为就是:重载const版本函数时,通过调用 非const版本函数 来达到避免代码重用的目的。

危险版本:

#include <iostream>#include <cstring>class demo{public:    demo():m_str(nullptr){}    demo(char *str):m_str(new char[std::strlen(str)+1]){        strcpy(m_str,str);    }    const char & operator[](std::size_t position)const{        return static_cast<const char&>((*const_cast<demo*>(this))[position]);    }    char & operator[](std::size_t position){        //....检查position是否越界        //....检查数据的完整新        //....对该操作进行记录,写入log(日志)        return m_str[position];    }private:    char * m_str;};int main(){    const demo d("hello");    std::cout<<d[0]<<std::endl;    return 0;}

解释:上述代码中:

    const char & operator[](std::size_t position)const{        return static_cast<const char&>((*const_cast<demo*>(this))[position]);    }

细心的同学会发现:它的步骤就是是 安全版本 的反向。虽然static_cast< const char& >(…..) 显得多余。

为什么是危险的?
:const版本函数中调用的是非const版本函数,关键就是我们不知道非const版本的函数中,对数据到底做了什么处理,到底有没有修改数据。所以,这种做法是危险的。

转载请注明出处

0 0
原创粉丝点击