const的使用

const的使用


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



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版本的函数中,对数据到底做了什么处理,到底有没有修改数据。所以,这种做法是危险的。

转载请注明出处

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值