C++ Primer第二章④读书笔记(看看有没有人看)
C++ Primer
第二章 变量和基本类型
const限定符
有时候,我们希望定义这样一种变量,它的值不能被改变。例如我们想用一个变量表示圆周率,当然它是一个不需要变得变量。
const double pi = 3.14;
只要在定义变量的时候前面加上关键字const就行了。
记住一点:const对象一旦创建后其值就不能再改变。
因为你记住的这一点,所以const对象必须初始化。为什么有这个逻辑呢,因为你不能改变const的值,而你在定义的时候又没有给它一个初值,那要它干嘛?
const int i = 0; //正确:编译时初始化 const int j; //错误:k是一个没有被初始化的常量
初始化和const
之前反复提到过,对象的类型决定了其上的操作,那么对于const对象有什么限制呢?记住:主要的限制就是不能改变它的值 看看下面的操作哪些合法哪些非法
int i = 42; const int ci = i; //正确:ci初始化等于i int j = ci; //正确:ci的值拷贝给j,并没有改变ci的值 ci = j; //错误:企图改变常量ci的值
如果想在多个文件之间共享运行时初始化的const对象,必须在变量的定义之前添加extern关键字。 理解这句话并不是很容易,你可以选择记住它,并跳过解释,解释如下:
当以编译初始化的方式定义一个const对象时,等一下,先解释什么是编译初始化
const double pi = 3.14; //编译时初始化,因为C++是静态类型语言,在编译阶段检查类型 const int j = get_size(); //运行时初始化,因为编译器得在运行时调用函数才知道j是多少解释完了,回到主题,当编译器编译初始化const对象的时候,会在编译过程中把用到该变量的地方都替换成对应的值,就是说代码里面pi在编译后都直接变成3.14,为了执行上述替换,编译器就必须知道变量的初始值。问题来了,如果程序包含多个文件,则每个用了const对象的文件都必须访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义(
到这里,我想到,那不是可以用之前声明和定义的方式吗?我在一个文件中定义一个const对象,在其他要用到的文件中声明它就好啦,但这里不行,我的理解是编译时编译器只知道有这个变量,但是不能访问到它的初始值,所以会报错。)那这样的话C++的设计者怎么解决这个问题呢?答案就是各管各的,默认情况下,const对象被设定为仅在文件中有效,其他文件要用的话,自己再定义一个,这样不算重复定义,因为它只在本文件有效,是不是很耍赖啊,不过反正const对象不多,这么做也可以。 如果是运行时初始化呢?那问题就简单了//在j.cpp定义并运行时初始化一个常量 extern const int j = get_size(); //j.h头文件,声明一下就可以用了 extern const int j;这个运行时初始化const为什么可以一次定义多次声明呢?我的理解:因为它本身就不是在编译的时候去替换值得,所以不要紧,那它跟之前的普通变量一次定义多次声明有什么区别呢?你应该发现了,不管是定义还是声明,它都加了extern,为什么要加extern呢,因为之前不是规定默认情况是只在文件内有效吗,你加个extern(extern的英文意思就是外部的),就可以在其他文件,也就是外部文件来访问了。
好了,啰嗦完了。
==把const和之前学的引用和指针分别搞在一起,又要搞事情了==
const的引用
把引用绑定到const对象上,我们称之为对常量的引用,与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 12; const int &r1 = ci; //正确:引用是常量,引用的对象也是常量 r1 = 42; //错误:r1是对常量的引用,不能修改它的值 int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
可能会对最后一句代码有疑惑,可以这么理解,你用了变量r2去引用一个常量对象,那我是不是可以通过r2去改变ci呢,可以的话,ci被改变,不符合ci是常量这一限定,不可以的话,r2本身是变量,凭什么不让我改,所以C++设计者索性让这个直接报错。
graph LR 非常量引用-->非常量对象
graph LR 常量引用-->常量对象
graph LR 常量引用-->非常量对象
以上三种都是合法的,第一种,第二种都好理解,一一对应,第三种为什么是合法的呢?这样理解,对象本身是可以被改变的,只是不能通过我这个常量引用去改变它,唯一一种非法情况就是上面提到的让一个非常量引用去指向一个常量对象
const int ci = 12; int cp = 11; const int &r1 = ci; //正确:引用是常量,引用的对象也是常量 int &r2 = cp; //正确:非常量引用指向非常量对象 const int &r3 = cp;//正确:常量引用指向非常量对象 int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
这时候有个问题了,在第三种情况中,如果我改变了cp的值,那r3的值会变吗?答案是会变的。 严格来说,并不存在常量引用,因为引用本身不是一个对象,所以我们没办法让引用本身不变。
初始化和对const的引用
之前我们学到过,引用的类型必须与其所引用的对象类型一致,这里就有意外情况了,哦,不,例外情况,其实我们上面的第三种情况不就是例外吗?
int cp = 11; const int &r3 = cp;
官方说法是这样的:在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
double pi = 3.14; const int &ri = pi; //正确
此处ri引用了一个int型的数,对ri的操作应该是整数运算,但pi是double,因此,编译器是这么干的:
const int temp = pi; const int &ri = temp; //当然输出ri的话就是3了。
也就是说ri其实没有绑定到pi上,而是绑定到了一个临时对象上,那么下面这种情况合法吗?
double pi = 3.14; int &ri = pi; //错误
有两种解释都可以说明它是错的,第一,在没有const的情况下,引用和它的对象类型不匹配所以错了,第二,如果合法的话,程序员肯定想用ri去改变pi的值,而实际上ri是绑定到了一个临时变量,根本改变不了pi1的值,所以就非法了。
C++的语法可能比较复杂,但是你按照一些基本法去推导还是比较好理解的。
指针和const
与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针不能用于改变其所指的对象,所以要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14; //pi是常量,值不能被改变 double *ptr = π //错误:ptr是普通指针 const double *cptr = π //正确:cptr可以指向一个常量 *cptr = 3.15; //错误:不能给常量*cptr赋值
之前学过,指针的类型必须与其所指的对象类型一致,例外来了:
int i = 0; const int *p = &i; //正确:但不能通过p来改变i的值,和引用的第三种情况差不多的
试试这样想把:所谓指向常量的指针或引用,不过是指针或引用自以为是罢了,它们觉得自己指向了常量,所以不去改变所指对象的值,其实对象是可以被改变的,只不过不能通过它们来改。
const指针
指针是对象而引用不是,因此允许把指针本身定义为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了,跟其他的const对象完全一样嘛,只不过const指针存的是地址。 这部分很绕,我决定直接把你搞晕
int errNumb = 0; int *const curErr = &errNumb; //curErr将一直指向errNumb const double pi = 3.14; const double * const pip = π //pip是指向常量对象的常量指针
先看前两句,比较难理解得是第二句,还是按照以变量curErr为最右端,从右向左阅读,最近的是const,说明它是个常量对象,对象的类型由声明符的其余部分确定,声明符的下一个符号是,意味着它是个常量指针,最后它指向一个int对象。 考一考你:我们输出curErr是0,如果我们改变了errNumb的值为1,再输出curErr是多少呢?答案是1。 再看后两句,pip首先是一个常量,最近的是,所以pip是个常量指针,指向什么类型呢,指向double常量。
指针本身是一个对象,它又可以指向另外一个对象,因此,指针本身是不是常量和指针所指的对象是不是常量就是两个互相独立的问题。 如上面代码中curErr就是指针本身是一个常量。pip是指针本身以及所指对象都是常量。下面再看些复杂点的例子
int i = 0; int *const p1 = &i; //指针本身是常量,采用从右到左读法就行,不能改变p1的值, //但是可以改变i的值,只是不能通过p1去改变i的值 const int ci = 42; const int *p2 = &ci; //指针所指的对象是常量,指针本身是普通指针,可以改变p2的值 //这么理解,p2作为普通指针存的内容是变量,但以这个内容位为地址那个是常量 //于是*p2 = 2这样企图改变常量的话是违法的 const int *const p3 = p2; //p3本身是常量指针,指向的对象是int常量 const int &r = ci; //r是引用,引用的是常量对象。
看到这,如果把const的引用结合起来的话,可能有些疑惑,下面我来分析下: 前面提到过,对于引用来说,有一种情况是非法的,如下
const int i = 1; int &j = i;
如果同样的情况,但是把引用换成指针呢?
const int i = 1; int *j = &i;
也是错的,简单来说,没有const的时候,只要考虑类型是否匹配就好了,这两种情况都不匹配,所以错了。 再看两种
const int i = 1; const int &j1 = i; const int *j2 = &i;
类型匹配,完全正确 再来两种
int i = 1; const int &j1 = i; int const &j2 = i; const int *j3 = &i; int const *j4 = &i;
这里的四个j定义是否正确呢,答案是全部正确,我们一个一个来分析
-
j1是一个引用,引用的是int常量,i虽然是int变量,但可以当成常量来用,如果i是double类型的,j1还是可以这么写,这时候就要创建一个临时变量来绑定了
int i = 1; const int &j1 = i; cout << &i << endl; cout << &j1 << endl;
输出均为: 0x9ffe44 0x9ffe44 说明在不需要转换的时候,编译器并不会创建一个临时变量
double i = 1.0; const int &j1 = i; cout << &i << endl; cout << &j1 << endl;
输出: 0x9ffe38 0x9ffe44 说明此时创建了一个临时变量。
- j2本身是一个常量引用,但它引用的是变量,就是我们之前说的:指向常量的指针或引用,不过是指针或引用自以为是罢了。 指针的情况与引用类似,不再赘述。 C++的设计者搞出了两个概念:
- 顶层const:指针本身是常量
- 底层const:指针所指的对象是常量 记着就好,顶层是本身,底层为所指,顶层和底层是修饰const的,不是修饰变量的 用于声明引用的const都是底层const,为什么呢,因为引用又不是对象,没有本身,所以只好全是底层了。
是不是已经很烦了,最后再来一发
int i = 0; int *const p1 = &i; //顶层 const int ci = 42; //顶层 const int *p2 = &ci; //底层 const int *const p3 = p2; //靠右的顶层,靠左的底层 const int &r = ci; //底层 //看看顶层和底层对操作的影响,只给答案,原因自己想想就好 i = ci; //对 p2 = p3; //对 int *p = p3; //错,p3指的对象是常量,你的指针就应该也是常量 p2 = &i; //对 int &r = ci; //错 const int &r2 = i //对
我自己的概括(有问题请务必指出啊):只要等号左边的对象是常量,那指针或引用怎么都行,如果对象是常量,指针和引用必须是常量。
坚持一下,就剩最后一小部分了。
constexpr和常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。 一个对象或表达式是不是常量表达式由它的数据类型和初始值共同决定
const int a = 20; //是 const int b = a+1; // 是 int c = 27; //不是,因为int const int d = get_size(); //不是,因为在运行时才能得到d的值
constexpr变量
当系统很复杂时,我们很难分辨一个初始值到底是不是常量表达式,所以C++11新标准想了个办法,允许将变量声明为constexpr类型来让编译器验证变量的值是否是一个常量表达式
constexpr int a = 20; //对 constexpr int b = a + 1; //对 constexpr int sz = size(); //只有当size是一个constexpr函数时才正确,先不要管constexpr函数是啥。。。
一般来说,如果你认定变量是一个常量表达式,那就把它声明为constexpr类型。
指针和constexpr
指针这家伙和谁都有一腿,麻烦死了
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。
const int i = 0; const int *p = &i; //p是一个指向整型常量的指针 constexpr int *q = &i; //q是一个指向整型的常量指针
注意:i得定义在函数体之外(毕竟要在编译时就确定q的值)
#C++工程师#
查看10道真题和解析
