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

转载请注明出处

<think>好的,我现在需要帮助用户了解JavaScript中const关键字的用法及示例。首先,我要回忆一下const的基本特性。根据引用[3]的内容,const用于声明块级作用域的变量,初始化后不能重新赋值。这意味着一旦给const变量赋值后,就不能再改变它的值了。比如声明const PI = 3.14;之后,再尝试PI = 3.15会报错。 接下来,用户可能需要知道const在对象和数组中的表现。虽然不能重新赋值整个变量,但对象或数组内部的属性或元素是可以修改的。例如,const person = {name: 'Alice'}; person.name = 'Bob'; 这是允许的,但person = {}会报错。这部分需要举例说明,确保用户明白这种细微差别。 然后,关于作用域的问题,const和let一样是块级作用域,不同于var的函数作用域。比如在if块中声明的const变量,外部无法访问。引用[3]中的示例也展示了这一点,函数内部的if块中定义的message在外部访问会报错。 另外,用户可能会混淆const和不可变性。需要明确const只保证变量指向的内存地址不变,而不是值本身不可变。因此,对于引用类型的数据,修改属性是允许的,但重新赋值整个变量不行。这部分可能需要进一步解释,并给出例子。 还有,声明const变量时必须初始化,否则会报错。比如const age; age = 30; 是错误的,必须在声明时赋值。这一点需要强调,避免用户在使用时遇到语法错误。 此外,比较var、let和const的区别也很重要。比如var存在变量提升,而const和let没有;var可以重复声明,const和let不行。这些对比可以帮助用户更好地理解何时使用const。 用户可能还关心在循环中使用const的情况。比如在for...of循环中,每次迭代都会创建一个新的const变量,这可以正常工作。但在传统的for循环中,由于需要改变计数器,不能使用const。这部分可能需要举例说明。 最后,总结const使用场景:当变量不需要重新赋值时,优先使用const,尤其是在模块导出、配置常量、函数表达式等情况下。这有助于代码的可读性和维护性。 现在需要确保回答结构清晰,分点说明,每个点都配有示例代码,并且引用相关的内容来支持。同时,生成的相关问题要围绕用户可能进一步想了解的内容,比如与let的区别、使用场景、对象处理等。</think>### JavaScript const 关键字的用法及示例 #### 1. **基本特性** - **不可重新赋值**:用 `const` 声明的变量必须在声明时初始化,且不能重新赋值。 ```javascript const PI = 3.14; PI = 3.15; // 报错:TypeError: Assignment to constant variable[^3] ``` - **块级作用域**:`const` 声明的变量仅在当前代码块内有效。 ```javascript if (true) { const message = "Hello"; console.log(message); // 输出 "Hello" } console.log(message); // 报错:ReferenceError: message is not defined[^3] ``` #### 2. **引用类型的特殊行为** - **对象或数组可修改内部属性**:虽然变量本身不能重新赋值,但其属性或元素可以修改。 ```javascript const person = { name: "Alice" }; person.name = "Bob"; // 允许 console.log(person.name); // 输出 "Bob" const arr = [1, 2]; arr.push(3); // 允许 console.log(arr); // 输出 [1, 2, 3][^3] ``` - **重新赋值整个变量会报错**: ```javascript person = { name: "Charlie" }; // 报错:TypeError[^3] ``` #### 3. **声明时必须初始化** - 未初始化的 `const` 变量会报错: ```javascript const age; // 报错:SyntaxError: Missing initializer in const declaration ``` #### 4. **与 `let` 和 `var` 的区别** - **作用域**:`const` 和 `let` 是块级作用域,`var` 是函数作用域。 - **变量提升**:`var` 会提升变量声明,`const` 和 `let` 不会。 - **重复声明**:`var` 允许重复声明,`const` 和 `let` 不允许。 ```javascript var x = 1; var x = 2; // 允许 let y = 3; let y = 4; // 报错:SyntaxError[^2] ``` #### 5. **适用场景** - **常量值**:如数学常数、配置项。 ```javascript const API_KEY = "abc123"; ``` - **函数表达式**:防止函数被意外覆盖。 ```javascript const sum = (a, b) => a + b; ``` - **模块导出**:确保导出的对象不被替换。 ```javascript export const config = { version: "1.0" }; ``` #### 6. **循环中的使用** - **`for...of` 循环**:每次迭代创建新的 `const` 变量。 ```javascript const numbers = [10, 20, 30]; for (const num of numbers) { console.log(num); // 输出 10, 20, 30 } ``` - **传统 `for` 循环**:不能使用 `const`(因需修改循环变量)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值