C++ Const 初步总结(《C++程序设计语言》读后感)

工作已经进入到了11年的工龄了。在我这个年龄大部分人都去做管理岗位了。

对于我来说, 还是忠于编程,编程就是我的兴趣爱好。干一行就爱一行。

开始学习C++的基础知识。在看《C++程序设计语言》的时候,看到引用这章节的介绍时,里面有这么一段代码:

template<class T>
class vector {
    T* elem;
    // ...
public:
    T& operator[](int i) { return elem[i]; }
    const T& operator[](int i) const { return elem[i]; }

    void push_back(const T& a);
    //...
};

void f(const vector<double>& v)
{
    double d1 = v[1];
    v[2] = 7;    //set 7 to v.operator[](2) indirect double

    v.push_back(d1);
}

在f函数里面, v[2]被设置成了值7,这里就很困惑。vector不是被设定成了常量吗?怎么可以改变引用对象的值呢?颠覆了我的三观。

看来需要重新复习常量的基础知识了。

一:const修饰成员变量

从基础介绍中了解:const大致意思是“我承诺不改变这个值”。主要用于说明接口,这样在把变量传入函数时就不必担心变量会在函数内被改变了。编译器负责确认并执行const的承诺。一旦我们把某物声明成const,就确保它的值在其作用域内不会发生改变。

对于指针这个类型定义的常量来看。我们要了解指针的一些信息。一个指针牵扯到两个对象,指针本身以及指针所指的对象。所以存在两种指针类型的常量。

1,在指针的声明语句中“前置”const关键字将令所指的对象而非指针本身成为常量。

2,在指针的声明语句中“后置”const关键字将令指针本身成为常量。

如下:

#include<iostream>
using namespace std;
int main(int arc, char **argv){
    int a1=3;   ///non-const data
    const int a2=a1;    ///const data

    int * a3 = &a1;   ///non-const data,non-const pointer
    const int * a4 = &a1;   ///const data,non-const pointer
    int * const a5 = &a1;   ///non-const data,const pointer
    int const * const a6 = &a1;   ///const data,const pointer
    const int * const a7 = &a1;   ///const data,const pointer

    return 0;
}

这是一些有关常用指针的sample。声明运算符 *const的作用是令指针本身成为常量。C++允许把非const变量的地址赋给指向常量的指针,不允许把常量的地址赋给某个不受限的指针。

介绍到这里,我们返回到刚开始写的常量引用是怎么回事?那么还是需要首先理解引用的本质。

引用是一种C++的语言机制,和指针类似,作为对象的别名存放对象的机器地址。与指针相比,引用不会带来额外的开销。

二者之间的区别:

  • 访问引用与访问对象本身从语法形式上看是一样的。
  • 引用所引的永远是一开始初始化的那个对象
  • 不存在“空引用”,我们可以认为引用一定对应着某个对象

引用最重要的用途是作为函数的实参或者返回值。

存在三种形式的引用:

左值引用(lvalue reference):引用那些我们希望改变值的对象。

const引用(const reference):引用那些我们不希望改变值的对象(比如常量)

右值引用(rvalue reference):所引用对象的值在我们使用之后就无须保留了(比如临时变量)

这三种形式统称为引用,其中前两种形式都是左值引用

 

引用的实现方式类似于常量指针,每次使用引用实际上是对该指针执行解引用操作。(这种只是辅助理解,引用不是对象,指针式一种对象)。

回到了引发这边博文的核心问题点,const类型的引用。《C++程序设计语言》【第四版】中的第165中有这么一段话:

const T& 的初始值不一定非得是左值,甚至可以不是T类型的。此时:

  • 首先,如果必要的话先执行目标为T的隐式类型转换
  • 然后,所得的值置于一个T类型的临时变量中
  • 最后,把这个临时变量作为初始值

事咧:

double & d1 = 1;              //错误:此处需要左值

const double& cdr {1};   //OK

const的这条语句的初始化过程可以理解为:

double temp = double{1};      //首先用给定的值创建一个临时变量

const double& cdr {temp};    //然后用这个临时变量作为cdr的初始值

用于存放引用初始值的临时变量的生命周期从它创建之处开始,到它的引用作用域结束为止。


我写了一段测试代码如下:

#include <iostream>
  
using namespace std;
void display(int const &ref) {
    ref = 7;  //对常量引用ref进行重新赋值
    cout << ref << '\n';
}

int main() {
    int i = 1;
    display(i);
    int const anotheri = 2;
    display(anotheri);
    display(2);
    display(1 + 2);
    display(static_cast<int>(3.14159));
}

编译结果如下:

medeadeMacBook-Pro:C++ medea$ g++ const_test.c -o const_test
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
const_test.c:5:9: error: cannot assign to variable 'ref' with const-qualified type 'const int &'
    ref = 7;
    ~~~ ^
const_test.c:4:25: note: variable 'ref' declared const here
void display(int const &ref) {
             ~~~~~~~~~~~^~~
1 error generated.

可以看到常量引用是不能赋值的。那么回到最初代码进行再次分析:

    T& operator[](int i) { return elem[i]; }
    const T& operator[](int i) const { return elem[i]; }

以上可以看到其实对操作符[],有两个重载函数。调用的代码如下:

void f(const vector<double>& v)
{
    double d1 = v[1];
    v[2] = 7;    //set 7 to v.operator[](2) indirect double

    v.push_back(d1);
}

对于v[2] = 7,首先执行的是是操作符号[]。那么到底匹配那个重载函数呢?我们用事实来证明,参照下面的代码:

#include <iostream>
  
template<class T>
class vector {
    T* elem;
    // ...
public:
    T& operator[](int i) { return elem[i]; }
    const T& operator[](int i) const { return elem[i]; }

};

void f(const vector<double>& v)
{
    double d1 = v[1];
    v[2] = 7;    //set 7 to v.operator[](2) indirect double
}

int main(int argc, char **argv)
{
    vector<double> test;

    f(test);
    return 0;
}

编译结果如下:

medeadeMacBook-Pro:C++ medea$ g++ const_vector.c -o const_vector
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
const_vector.c:16:10: error: cannot assign to return value because function 'operator[]' returns a const value
    v[2] = 7;    //set 7 to v.operator[](2) indirect double
    ~~~~ ^
const_vector.c:9:11: note: function 'operator[]' which returns const-qualified type 'const double &' declared here
    const T& operator[](int i) const { return elem[i]; }
          ^~
1 error generated.

也是编译不过。 难道不能重载。

把const T& operator[](int i) const { return elem[i]; }注释掉后,继续编译结果如下:

medeadeMacBook-Pro:C++ medea$ g++ const_vector.c -o const_vector
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
const_vector.c:15:18: error: no viable overloaded operator[] for type 'const vector<double>'
    double d1 = v[1];
                ~^~
const_vector.c:8:8: note: candidate function not viable: 'this' argument has type 'const vector<double>', but method is not marked const
    T& operator[](int i) { return elem[i]; }
       ^
const_vector.c:16:6: error: no viable overloaded operator[] for type 'const vector<double>'
    v[2] = 7;    //set 7 to v.operator[](2) indirect double
    ~^~
const_vector.c:8:8: note: candidate function not viable: 'this' argument has type 'const vector<double>', but method is not marked const
    T& operator[](int i) { return elem[i]; }
       ^
2 errors generated.

通过上面两次实验,可以知道。首先:

1,main函数中调用f函数,将test对象复制给常量引用const vector<double>& v。

2,由于形参是常量引用,所以调用的函数也必须是const类型。当调用操作符[]的时候,需要匹配const T& operator[](int i) const 函数。

 

综上所诉,这里应该是书本上写错了。至少是翻译的中文版本是写错了。英文版本有时间在去看看。

到这里const引用的语言机制(编译器行为)已经比较清晰。但是又引发出来了一个新的问题:

引用类似于常量指针,那么const引用从字面上面理解应该代表的是引用住对象的不变性。如果按照上面的语言机制,const引用是可以写入值的,使用一个临时变量保存备份,有什么特殊意义?

这个问题需要答案,期待阅读这篇文章的大牛们帮忙解惑。

目 录 译者序 前言 第1章 对象的演化 1 1.1 基本概念 1 1.1.1 对象:特性+行为 1 1.1.2 继承:类型关系 1 1.1.3 多态性 2 1.1.4 操作概念:OOP程序像什么 3 1.2 为什么C++会成功 3 1.2.1 较好的C 3 1.2.2 采用渐进的学习方式 4 1.2.3 运行效率 4 1.2.4 系统更容易表达和理解 4 1.2.5 “库”使你事半功倍 4 1.2.6 错误处理 5 1.2.7 大程序设计 5 1.3 方法学介绍 5 1.3.1 复杂性 5 1.3.2 内部原则 6 1.3.3 外部原则 7 1.3.4 对象设计的五个阶段 9 1.3.5 方法承诺什么 10 1.3.6 方法应当提供什么 10 1.4 起草:最小的方法 12 1.4.1 前提 13 1.4.2 高概念 14 1.4.3 论述(treatment) 14 1.4.4 结构化 14 1.4.5 开发 16 1.4.6 重写 17 1.4.7 逻辑 17 1.5 其他方法 17 1.5.1 Booch 18 1.5.2 责任驱动的设计(RDD) 19 1.5.3 对象建模技术(OMT) 19 1.6 为向OOP转变而采取的策略 19 1.6.1 逐步进入OOP 19 1.6.2 管理障碍 20 1.7 小结 21 第2章 数据抽象 22 2.1 声明与定义 22 2.2 一个袖珍C库 23 2.3 放在一起:项目创建工具 29 2.4 什么是非正常 29 2.5 基本对象 30 2.6 什么是对象 34 2.7 抽象数据类型 35 2.8 对象细节 35 2.9 头文件形式 36 2.10 嵌套结构 37 2.11 小结 41 2.12 练习 41 第3章 隐藏实现 42 3.1 设置限制 42 3.2 C++的存取控制 42 3.3 友元 44 3.3.1 嵌套友元 45 3.3.2 它是纯的吗 48 3.4 对象布局 48 3.5 类 48 3.5.1 用存取控制来修改stash 50 3.5.2 用存取控制来修改stack 51 3.6 句柄类(handle classes) 51 3.6.1 可见的实现部分 51 3.6.2 减少重复编译 52 3.7 小结 54 3.8 练习 54 第4章 初始化与清除 55 4.1 用构造函数确保初始化 55 4.2 用析构函数确保清除 56 4.3 清除定义块 58 4.3.1 for循环 59 4.3.2 空间分配 60 4.4 含有构造函数和析构函数的stash 61 4.5 含有构造函数和析构函数的stack 63 4.6 集合初始化 65 4.7 缺省构造函数 67 4.8 小结 68 4.9 练习 68 第5章 函数重载与缺省参数 69 5.1 范围分解 69 5.1.1 用返回值重载 70 5.1.2 安全类型连接 70 5.2 重载的例子 71 5.3 缺省参数 74 5.4 小结 81 5.5 练习 82 第6章 输入输出流介绍 83 6.1 为什么要用输入输出流 83 6.2 解决输入输出流问题 86 6.2.1 预先了解操作符重载 86 6.2.2 插入符与提取符 87 6.2.3 通常用法 88 6.2.4 面向行的输入 90 6.3 文件输入输出流 91 6.4 输入输出流缓冲 93 6.5 在输入输出流中查找 94 6.6 strstreams 96 6.6.1 为用户分配的存储 96 6.6.2 自动存储分配 98 6.7 输出流格式化 100 6.7.1 内部格式化数据 101 6.7.2 例子 102 6.8 格式化操纵算子 106 6.9 建立操纵算子 108 6.10 输入输出流实例 111 6.10.1 代码生成 111 6.10.2 一个简单的数据记录 117 6.11 小结 123 6.12 练习 123 第7章 常量 124 7.1 值替代 124 7.1.1 头文件里的const 124 7.1.2 const的安全性 125 7.1.3 集合 126 7.1.4 与C语言的区别 126 7.2 指针 127 7.2.1 指向const的指针 127 7.2.2 const指针 127 7.2.3 赋值和类型检查 128 7.3 函数参数和返回值 128 7.3.1 传递const值 128 7.3.2 返回const值 129 7.3.3 传递和返回地址 131 7.4 类 133 7.4.1 类里的const和enum 133 7.4.2 编译期间类里的常量 134 7.4.3 const对象和成员函数 136 7.4.4 只读存储能力 139 7.5 可变的(volatile) 140 7.6 小结 141 7.7 练习 141 第8章 内联函数 142 8.1 预处理器的缺陷 142 8.2 内联函数 144 8.2.1 类内部的内联函数 145 8.2.2 存取函数 146 8.3 内联函数和编译器 150 8.3.1 局限性 150 8.3.2 赋值顺序 150 8.3.3 在构造函数和析构函数里隐藏行为 151 8.4 减少混乱 152 8.5 预处理器的特点 153 8.6 改进的错误检查 154 8.7 小结 155 8.8 练习 155 第9章 命名控制 157 9.1 来自C语言中的静态成员 157 9.1.1 函数内部的静态变量 157 9.1.2 控制连接 160 9.1.3 其他的存储类型指定符 161 9.2 名字空间 161 9.2.1 产生一个名字空间 162 9.2.2 使用名字空间 163 9.3 C++中的静态成员 166 9.3.1 定义静态数据成员的存储 166 9.3.2 嵌套类和局部类 168 9.3.3 静态成员函数 169 9.4 静态初始化的依赖因素 171 9.5 转换连接指定 174 9.6 小结 174 9.7 练习 174 第10章 引用和拷贝构造函数 176 10.1 C++中的指针 176 10.2 C++中的引用 176 10.2.1 函数中的引用 177 10.2.2 参数传递准则 178 10.3 拷贝构造函数 179 10.3.1 传值方式传递和返回 179 10.3.2 拷贝构造函数 182 10.3.3 缺省拷贝构造函数 187 10.3.4 拷贝构造函数方法的选择 188 10.4 指向成员的指针
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值