《C++ Primer》第2章 变量和基本类型(中)

2.3 复合类型

复合类型:基于其他类型定义的类型。

定义变量的格式为:

数据类型 变量名; //如int i;

更通用的:

数据类型 声明符;

之前接触的声明语句中,声明符就是变量名。其实有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。

C++有几种复合类型,这里介绍其中两种:引用指针

2.3.1 引用

C++11新增了“右值引用”,将在第十三章详细介绍。严格来说,使用术语“引用”时,指的其实是“左值引用”。

引用:为对象起了另外一个名字。其声明符&d的形式,其中d为变量名。

int ival = 1024;
int &refVal = ival;  //refVal指向ival(是ival的另一个名字)
int &refVal2;  //错误:引用必须被初始化

在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化

引用即别名

引用并非对象,它只是为一个已经存在对象起的另一个名字。

定义引用后,对其所有操作都是在与之绑定的对象上进行的。接上段程序:

refVal = 2;   //把2赋给refVal指向的对象,即ival
int ii =refVal;   //等价于ii = ival
int &refVal3 = refVal; //refVal3绑定到了那个与refVal绑定的对象上,即ival

因为引用本身不是一个对象,所以不能定义引用的引用。

引用的定义

允许一条语句定义多个引用,每个引用标识符都必须以&开头:

int i = 1, i2 = 2; //i和i2都是int
int &r = i, r2 = i2; //r是引用,与i绑定在一起;r2是int

除以下两种例外情况外,引用本身的类型必须与其所引用对象的类型一致

int &refVal4 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型的初始值必须是int型对象

例外情况一:在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值、一般表达式。(2.3节会讲)

例外情况二:可以将基类的引用绑定到派生对象上。(15.2节会讲)

2.3.2 指针

指针:其声明符为*d的形式,其中d是变量名。

int *p1, *p2; //p1和p2都是指向int型对象的指针。

如果一条语句定义多个指针,则每个变量前都要有*。

指针与引用的异同点

相似点:均实现了对其他对象的间接访问。

不同点:

1.指针本身就是一个对象,允许对指针赋值和拷贝,且在指针的生命周期内它可以先后指向几个不同的对象。

2.指针无须在定义时赋初值。与内置类型一样,在块作用域内定义的指针若未初始化,将拥有一个不确定的值。

获取对象的地址

指针存放某个对象的地址,用取地址符(&)获取该地址。

int ival =42;
int *p = &ival; //p存放变量ival的地址

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

与引用相似,除以下两种例外情况,指针本身的类型都要和它所指向的对象严格匹配

声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。

double dval;
double *pd = &dval; //正确:初始值是double型对象dval的地址
double *pd2 = pd; //正确:初始值是指向double对象的指针

int *pi = pd; //错误:pi和pd的类型不匹配
pi = &dval; //错误:试图把double型对象的地址赋给int型指针

例外情况一:允许令一个指向常量的指针指向一个非常量对象。(2.3节会讲)

例外情况二:可以将基类的指针绑定到派生对象上。(15.2节会讲)

指针值

指针的值(即地址)应属于下列4种状态之一:

  1. 指向一个对象。
  2. 指向紧邻对象所占空间的下一个位置。
  3. 空指针,意味着没有指向任何对象。
  4. 无效指针,即上述情况之外的其他值。
  • 访问无效指针的后果无法预计。
  • 虽然2和3的指针有效,但也没有指向具体对象,访问他们的后果也无法预计。

利用指针访问对象

对指针使用解引用符(*)可以访问该指针指向的对象。如:

int ival = 42;
int *p = &ival;  //指针p指向int型对象ival
cout << *p; //由符号*得到p所指的对象,输出42

给解引用的结果赋值,实际上就是给指针所指的对象赋值。接上述代码:

*p = 0;   //经由p为变量ival赋值
cout << ival; //输出0

解引用操作只适用于那些确实指向了某个对象的有效指针。

&和*的多重含义

在声明语句中,&和*是声明的一部分,用以组成复合类型。如:

int i = 42;
int &r = i; //&紧随类型名出现,因此是声明的一部分,r是一个引用
int *p;    //*紧随类型名出现,因此是声明的一部分,p是一个指针

而在表达式中,它们又变成运算符,&是取地址符,*是解引用符。如(接上):

p = &i; //&出现在表达式中,是取地址符
*p = i; //*出现在表达式中,是解引用符
int &r2 = *p; //&是声明的一部分,r2是一个引用;*是解引用符

综上,虽然是同一个符号,但在不同场景下其含义截然不同。

空指针

空指针:不指向任何对象

//以下三种方法均可生成空指针
int *p1 = nullptr; //方法#1
int *p2 = 0;       //方法#2
//需要首先使用#include <cstdlib>
int *p3 = NULL;    //方法#3
  • 方法#1

    nullptr是C++11新标准刚引入的一种特殊类型的字面值

  • 方法#2方法#3

    NULL是以前常用的预处理变量,它在头文件cstdlib中定义,其值就是0。

    预处理器:运行于编译过程之前的一段程序。

    当用到一个预处理变量时,预处理器会自动地把它替换为实际值。

    用NULL初始化指针和用0初始化指针是一样的。现在,推荐使用nullptr,避免使用NULL

    int变量直接赋给指针是错误的,即使int值恰好为0也不行。

建议初始化所有指针

赋值和指针

引用本身不是对象,其一旦定义,便无法再令其绑定其他对象。

指针本身就是对象,它可以赋新值指向新的对象。

//区分是改变了指针的值还是改变了指针所指对象的值
int *pi = nullptr , ival = 1024;
pi = &ival; //改变了pi的值,pi现在指向ival
*pi = 0//改变了ival的值,现在ival为0

其他指针操作

  • 指针用在条件表达式中,如果指针为空指针,则条件取false。任何非0指针对应的条件值都是true。
  • 两个类型相同的合法指针,可以用**==!=**进行比较,比较结果为布尔值。

使用非法指针作为条件或进行比较会引发不可预计的后果。

void* 指针

void*:它是一种可用于存放任意对象的地址的指针。

不能直接操作void*指针所指的对象,因为并不知道这个对象是什么类型,也就无法确定能在这个对象上进行哪些操作。

2.3.3 理解复合类型的声明

一条语句可以定义出不同类型的变量(因为在同一条定义语句中,基本数据类型只有一个,但声明符却可以不同)。

int i = 2, *p = &i, &r = i; //i是int型的数,p是int型指针,r是int型引用

定义多个变量

有些人喜欢把类型修饰符(*&)与基本数据类型紧挨着放,如:

int* p1 ,p2;

这会让人误认为p1p2都是指针,其实不是,*只修饰p1,所以p1是指针,而p2是int型的数。

我们才用把*&与变量名连在一起的方法,这样不容易引起新手的误会。

int *p1, p2;

指向指针的指针

指针是对象,它有实实在在的内存,所以可以定义指向指针的指针

int ival = 1024;
int *pi = &ival; //pi指向ival
int **ppi = &pi; //ppi指向pi

解引用也同理(接上述代码):

std::cout << ival  //直接获取ival值
          << *pi   //通过解引用pi获取ival值
          << **ppi  //通过解引用ppi两次获取ival值
          << std::endl;

指向指针的引用

指针是对象,所以可以定义指向指针的引用。

int i = 42;
int *p;  //p是int型指针
int *&r = p; //r是对指针p的引用
r = &i; //r引用了指针p,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p所指对象,将i的值改为

要理解r的类型到底是什么,最简单的办法就是从右向左读r的定义。离变量名最近的符号对变量的类型有最直接的影响。int *&r = p;中,离r最近的是&,所以r是一个引用;*说明r引用的是一个指针;int说明r引用的是一个int型指针。

2.4 const限定符

被关键字const修饰的变量的值不能改变,任何试图改变const对象值的行为都将引发错误。

const对象必须初始化,初始值可以是任意复杂的表达式。

const int bufSize = 512; 
bufSize = 512; //错误:试图向const对象写值

const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误:k是一个未初始化的常量

初始化和 const

只能在const对象上执行不改变其内容的操作。

以下操作是允许的:

int i =42;
const int ci = i;
int j = ci;

ci的常量特征仅仅在执行改变ci的操作时才发挥作用。用ci初始化j并不会改变ci的内容,所以允许这样操作。

默认状态下,const对象仅在文件内有效

默认状态下,const对象被设定为仅在文件内有效。当多个文件出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

那么怎么实现const对象在文件间的共享呢?

答:对于const变量不管是声明还是定义都添加extern关键字。

2.4.1 const 的引用

对常量的引用:与普通引用不同的是,对常量的引用不能用来修改它所绑定的对象。

const int ci = 1024;
const int &r1 = ci; //正确:引用及其绑定对象都是常量
r1 = 42; //错误:r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象

对const的引用 简称 常量引用

初始化和对const的引用

之前提到,引用的类型必须与其所引用对象的类型一致,但包含两个例外,这里说第一个例外情况:

在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式。

小总结:

允许常量引用绑定非常量对象

不允许非常量引用绑定常量对象

int i = 42;
const int &r1 = i; //常量引用绑定非常量对象
const int &r2 = 42; //常量引用绑定非常量字面值
const int &r3 = r1 * 2; //常量引用绑定一般表达式
int &r4 = r1 * 2; //错误:r4为非常量引用

当一个常量引用被绑定到另外一种类型上时到底发生了什么?

double dval = 3.14;
const int &r1 = dval;

这里r1引用的是一个int型数,但dval却是double型。因此,为了确保r1绑定了一个整数,编译器把上述代码变成如下形式:

const int temp = dval;
const int &r1 = temp;

即,r1绑定了一个临时量temptemp是编译器将double通过类型转换转换为int而来。

对const的引用可能引用一个并非const的对象

常量引用绑定的对象不一定是个常量。

常量引用只对引用可参与的操作做了限定,对引用的对象本身是不是常量并未限定。

所以引用的对象也可能是个非常量,允许通过其他途径改变它的值,只是不能通过常量引用改变它的值罢了。

int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;  //r1为非常量引用,可以通过r1改变i的值,因此i修改为0
r2 = 0;  //错误:r2为常量引用,不能通过它修改i的值

2.4.2 指针和 const

只有对象才可以定义为常量

因此对于引用,由于引用不是对象,所以只有对const的引用,我们所说的常量引用是对对const的引用的简称。

而对于指针,因为指针本身是对象,所以有指向常量的指针常量指针。前者指这个指针指向常量,后者指这个指针本身就是常量。

指向常量的指针

与常量引用相似,指向常量的指针不能用于改变其所指对象的值。

要想存放常量对象的地址,只能使用指向常量的指针。

const double pi = 3.14; //pi是常量,其值不能改变
double *ptr = &pi;  //错误:普通指针不能指向常量对象
const double *cptr = &pi;  //正确:cptr是一个指向常量的指针
*cptr = 42;  //错误:不能给*cptr赋值

但是有两个例外,这里说第一个:允许令一个指向常量的指针指向一个非常量对象。如:

double dval = 3.14;
cptr = &dval;

这样,与常量引用相似,指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

小总结:

指向常量的指针可以指向非常量。

普通指针不能指向常量。

const 指针(常量指针)

指针是对象,故可定为常量。常量指针必须初始化,初始化后其值不能改变,即存于指针内的那个地址不能改变,而不是指针指向的那个值。也就是说指针本身是常量,而不是说指针指向的对象是常量。

形式:在const前加*。

int errNumb = 0;
int *const curErr = &errNumb; //curErr是一个指向普通int对象的常量指针
const double pi = 3.14;
const double *const pip = &pi; //pip是一个指向double型常量对象的常量指针

从右向左阅读法

  • 离curErr最近的是const,说明curErr本身是一个常量对象;下一个符号是*,说明curErr是一个常量指针;下一个是int,说明curErr是指向int型对象。综上,curErr是一个指向int型对象的常量指针。

    可以通过curErr修改errNumb的值。

  • 离pip最近的是const,说明pip本身是一个常量对象;下一个符号*,说明pip是一个常量指针;下一个是double,说明pip指向double型对象;下一个又是const,说明pip指向double型常量。综上,pip是一个指向double型常量对象的常量指针。

    不可以通过pip修改pi的值。

2.4.3 顶层 const

顶层const:表示指针本身是一个常量。

底层const:表示指针所指的对象是个常量。

概念扩展:

顶层const:表示该数据类型的对象是常量。

底层const:与指针和引用等复合类型的基本类型部分有关。

指针类型既可以是顶层const也可以是底层const。

int i = 0;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci = 42; //不能改变ci的值,这是一个顶层const
const int *p2 = &ci; //p2是一个指向常量的指针,其值可以改变,这是一个底层const
const int *const p3 = p2; //靠右的const是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的const都是底层const

在进行对象的拷贝操作中,常量是顶层const还是底层const区别明显。

由于拷贝不会改变被拷贝对象的值,所以对顶层const没什么影响。

而对于底层const,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行:

//代码接上
int *p = p3; //错误:p3包含底层const,而p没有
p2 = p3; //正确:p2和p3均有底层const
int &r = ci; //错误:普通引用不能绑定常量对象
const int &r2 = i; //正确:常量引用可以绑定非常量对象

2.4.4 constexpr 和常量表达式

常量表达式:值不会改变 且 在编译过程就能得到计算结果的 表达式。

字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

const int max_files = 20; //max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式
const int sz = get_size(); //sz不是常量表达式

尽管staff_size的初始值是字面值常量,但它的数据类型是普通int而非const int,所以它不是常量表达式。

尽管sz本身是常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

constexpr 变量

C++11新标准规定,允许将变量声明为**constexpr**类型,以便由编译器来验证变量的值是否是一个常量表达式。

声明为constexpr的变量一定是一个常量,且必须用常量表达式初始化:

constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; //mf+1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时才是一条正确的声明语句

constexpr函数应足够简单以使得编译时就可以得到计算结果,这样就可以用constexpr函数初始化constexpr变量。普通函数不能作为constexpr变量的初始值。

字面值类型

字面值类型:能声明成constexpr的类型。

常见的如算数类型、引用、指针都属于字面值类型;

自定义类、IO库、string类等则不属于字面值类型,也就不能定义成constexpr。

尽管指针和引用都可定义成constexpr,但其初始值受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址的对象

函数体内的变量一般不在固定地址,所以constexpr指针不能指向它们。

定义于所有函数体外的对象其地址固定不变,能用来初始化constexpr指针。

后边会讲到,允许函数定义一类有效范围超出函数本身的变量,它们也有固定地址,所以constexpr指针可以指向它们。

指针和 constexpr

必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关,其中的关键在于constexpr把它所定义的对象置为了顶层const

const int *p = nullptr; //p是指向整型常量的普通指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针

与其他常量指针类似,constexpr指针既可以指向常量,也可以指向一个非常量。

constexpr int *np = nullptr; //np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i =42; //i是一个整型常量
//i和j都必须定义在函数体之外
constexpr const int *p = &i; //p是常量指针,指向常量i
constexpr int *p1 = &j; //p1是常量指针,指向整数j

6.1.1节会提到,函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。

但是,允许函数定义一类有效范围超过函数本身的变量,这类变量也有固定的地址,故constexpr指针可以指向这类变量,constexpr引用也可以绑定到这类变量上。

全部评论

相关推荐

11-08 13:58
门头沟学院 Java
程序员小白条:竟然是蓝桥杯人才doge,还要花钱申领的offer,这么好的公司哪里去找
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务