02 变量和基本类型

文末有《C++Primer》中文第5版百度网盘

第2章 变量和基本类型

2.1 基本内置类型

C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,当函数不返回任何值时使用空类型作为返回类型。

2.1.1 算术类型

1.分为整型(包括字符和布尔类型)和浮点型

在这里插入图片描述

  • 布尔类型(bool)的取值是真(true)或者假(false)。
  • 基本的字符类型是char,一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。即一个char的大小和一个机器字节一样。
  • 除字符和布尔类型之外,其他整型用于表示(可能)不同尺寸的整数。

补充:可寻址的最小内存块称为字节(byte),存储的基本单元称为字(word);大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节。下图左侧是字节的地址,右侧是字节中8比特的具体内容。

在这里插入图片描述

  • 浮点型可表示单精度、双精度和扩展精度值。通常,**float以1个字(32比特)来表示,double以2个字(64比特)来表示,long double以3或4个字(96或128比特)**来表示。一般来说,类型float和double分别有7和16个有效位;类型long double则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同

2.其他整型有带符号的(signed)和无符号的(unsigned)两种

  • 带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值
  • 类型int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,类型unsigned int可以缩写为unsigned
  • 符型被分为了三种:char、signed char和unsigned char。特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
  • 无符号类型中所有比特都用来存储值,例如,8比特的unsigned char可以表示0至255区间内的值。8比特的signed char在大多数现代计算机将实际的表示范围定为-128至127

3.如何选择类型(和C语言一样,C++的设计准则之一也是尽可能地接近硬件,大多数程序员能够(也应该)对数据类型的使用做出限定从而简化选择的过程)

  • 当明确知晓数值不可能为负时,选用无符号类型
  • 使用int执行整数运算。因为short常常太小,而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用longlong。
  • 在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的
  • 执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。

2.1.2 类型转换

  • 非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则为true。
  • 布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
  • 浮点数赋给整数类型时,结果值将仅保留浮点数中小数点之前的部分3.7 → 3,-3.7 → -3)。
  • 整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
  • 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。如8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。(注意:对于负数取模,应该加上被除数的整数倍,使结果大于或等于0之后即(-1)%256 = (-1+256)%256=255%256=255)
  • 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
// 对应上面6条的例子
bool b = 42;  // b为真
int i = b;  // i为1
i = 3.14;  // i为3
double pi = i;  // double为3.0
unsigned char c = -1;  // 若char占8bit,c为255
signed char c2 = 256;  // 若char占8bit,c2未定义

注:如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。例如,在一个形如a*b的式子中,如果a = -1,b = 1,而且a和b都是int,则表达式的值显然为-1。然而,如果a是int且为32bit,而b是unsigned,则结果为4294967295即(2^32 - 1)*1。

2.1.3 字面值常量

1.整型和浮点型字面值

  • 整型字面值可以写成十进制数、八进制数(0开头)或十六进制数(0X或0x开头),整型字面值具体的数据类型由它的值和符号决定,通常十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。
  • 浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识,默认浮点型字面值是一个double

2.字符和字符串字面值

  • 单引号括起来的一个字符称为char型字面值(2.1.2的代码中提到的char的值是字符对应的ascii码),双引号括起来的零个或多个字符则构成字符串型字面值。

  • 字符串字面值的类型实际上是由常量字符构成的数组(array),编译器在每个字符串的结尾处添加一个空字符′\0′),因此,字符串字面值的实际长度要比它的内容多1。例如,字面值’A’表示的就是单独的字符A,而字符串"A"则代表了一个字符的数组,该数组包含两个字符:一个是字母A、另一个是空字符。(面试题)

  • 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。

3.转义序列

  • 有两类字符程序员不能直接使用:一类是不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符;另一类是在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)
  • 针对有特殊含义的字符需要用到转义序列(escape sequence),转义序列被当作一个字符使用,均以反斜线作为开始,如:

在这里插入图片描述

  • 泛化的转义序列形式是\x后紧跟1个或多个十六进制数字,或者\后紧跟1个、2个或3个八进制数字,其中数字部分表示的是字符对应的数值。\x要用到后面跟着的所有数字,如果反斜线\后面跟着的八进制数字超过3个,只有前3个数字与\构成转义序列

4.指定字面值的类型(字符和字符串加前缀,整型和浮点型加后缀)

在这里插入图片描述

例如:

在这里插入图片描述

5.布尔字面值和指针字面值

  • true和false(不是1和0)是布尔类型的字面值,nullptr是指针字面值

2.2 变量

C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量(variable)”和“对象(object)”一般可以互换使用。

2.2.1 变量定义

1.基本形式

  • 首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔以分号结束
int sum = 0, value,
	units_sold = 0;  // 这三个变量都是int类型,且sum和units_sold初值为0
  • **对象(object)**是指一块能存储数据并具有某种类型的(object),我们在使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或是否只读

2.初始值

  • 当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)
  • 当一次定义了多个变量时,对象的名字随着定义可以马上使用
double price = 109.99, discount = price * 0.16;  // 正确:price先被定义并幅值,后被用于初始化                                                  // discount
  • 在C++中,初始化不是赋值初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代

3.列表初始化

// 定义一个名为num的int变量并初始化为0,以下四条语句都可
int num = 0;
int num = {0};
int num{0};
int num(0);
  • C++11新标准中用花括号来初始化变量的形式被称为列表初始化,现在,无论对象初始化还是赋新值,都可以使用这样一组由花括号括起来的初始值。

4.默认初始化

  • 如果定义变量时没有指定初值,则变量被默认初始化(两种情况:内置类型和类)
  • 内置类型的变量未被显式初始化
    • 定义于任何函数体之外的变量被初始化为0
    • 定义在函数体内部的内置类型变量将不被初始化,其值是未定义的,不能拷贝或以其他形式访问此类值
  • 绝大多数类都支持无须显式初始化而定义对象

5.注意

  • 未初始化变量引发运行时故障,建议初始化每一个内置类型的变量。如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。

2.2.2 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separatecompilation)机制

1.声明与定义

  • 声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明,定义(definition)负责创建与名字关联的实体
  • 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但定义还申请存储空间,可赋初始值。
  • 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,且不要显式地初始化变量
extern int i;  // 声明i,而非定义
extern int j = 1;  // 定义j,而非声明
  • 变量能且只能被定义一次,但是可以被多次声明。变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,绝对不能重复定义。

2.静态类型

  • C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型,检查类型的过程称为类型检查(type checking)。

2.2.3 标识符

1.标识符(identifier)

  • 由字母、数字和下画线组成,必须以字母或下画线开头。长度没有限制,但是对大小写字母敏感
  • 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头定义在函数体外的标识符不能以下画线开头

2.命名规范

  • 标识符要能体现实际含义
  • 变量名一般用小写字母,如index,不要使用Index或INDEX
  • 用户自定义的类名一般以大写字母开头,如Sales_item
  • 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan。

2.2.4 名字作用域

1.作用域(scope)是程序的一部分,C++语言中大多数作用域都以花括号分隔

  • 全局作用域(global scope):定义于所有花括号之外
  • 块作用域(block scope):定义在括号内的可以使用,之后的不可以使用,例如for中变量只允许for使用

2.建议

  • 在对象第一次被使用的地方附近定义,因为这样做有助于更容易地找到变量的定义。

3.嵌套域

  • 被包含(或者说被嵌套)的作用域称为内层作用域(innerscope),包含着别的作用域的作用域称为外层作用域
  • 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字,并允许在内层作用域中重新定义外层作用域已有的名字
  • 因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量
  • 建议:如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

2.3 复合类型

C++语言有几种复合类型,本章将介绍其中的两种:引用和指针

2.3.1 引用

1.定义

  • 引用(reference)为对象起另外一个名字,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名(严格来说,当我们使用术语“引用(reference)”时,指的其实是“左值引用(lvalue reference)“)
int val = 1024;
int &refVal = val;  // refVal指向val,即refVal是val的引用
int &refVal2;  // 报错,引用必须初始化

2.特点

  • 定义引用时,程序把引用和它的初始值绑定(bind)在一起,而非拷贝给引用,此外引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
  • 一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化

注:引用并非对象,它只是为一个已经存在的对象所起的另外一个名字,简称引用即别名

  • 为引用赋值或获取引用的值,实际上是把值赋给或获取与引用绑定的对象的值
  • 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头
int i = 1024;
int &r = i, r2 = i;  // r是i的引用,r2是int

2.3.2 指针

1.定义

  • 将声明符写成*d的形式,其中d是变量名
int *ip1, *ip2;  // ip1和ip2都是指向int的指针
  • 注意指针本身就是一个对象(区别于引用),允许对指针赋值和拷贝,此外指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

2.获取对象地址

int val = 42;
int *p = &val;  // p是指向变量val的指针,p存放val的地址
  • 注意&val中的&是取址符号,而非引用,可以这样区分,在等号左侧&是引用,右侧是取址
  • 第二条语句把p定义为一个指向int的指针,随后初始化p令其指向名为ival的int对象。因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

3.指针值

  • 指针值即地址应属于下列4种状态之一
    • 指向一个对象
    • 指向紧邻对象所占空间的下一个位置
    • 空指针:没有指向任何对象
    • 无效指针:除上述三种之外,若拷贝或以其他方式访问无效指针的值都将引发错误

4.利用指针访问对象

  • 如果指针指向了一个对象,则允许使用**解引用符(操作符*)**来访问该对象
int val = 42;
int *p = &val;
std::cout << *p << std::endl;  // 由符号*得到p所指对象的值,即输出42

5.符号的多重定义

  • 在声明语句中,&和*用于组成复合类型;在表达式中,它们的角色又转变成运算符。
int i = 42;
int &r = i;  // &作为声明的一部分,因此r是一个引用
int *p;  // *作为声明的一部分,p是一个指针
p = &i;  // &出现在表达式中,故是取址符号
*p = i;  // *出现在表达式中,故是解引用符号

6.空指针

  • 空指针(null pointer)不指向任何对象,创建方式有下列几种
int *p1 = nullptr;  // 用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法
int *p2 = 0;  // 注意不能把int变量直接赋给指针,即使int变量的值恰好等于0也不行。
// 需要先包含头文件 #include cstdlib
int *p3 = NULL;  // 过去的程序还会用到一个名为NULL预处理变量(preprocessor variable)给指针赋值
  • 在新标准下,现在的C++程序最好使用nullptr,同时尽量避免使用NULL。

  • 建议:初始化所有指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。

7.赋值和指针

  • 有时候要想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象
pi = &val;  // pi的值改变了 现在指向val
*pi = 0;  // pi的值并未改变,val的值改变了

8.其他指针操作

  • 只要指针拥有一个合法值,就能将它用在条件表达式中,如果指针的值是0,条件取false,任何非0指针对应的条件值都是true(等同于算数运算符
  • 对于两个类型相同的合法指针,可以用相等操作符(==)或不相等操作符(!=)来比较它们,比较的结果是布尔类型。两个指针存放的地址值相同(两个指针相等)有三种可能
    • 都为空
    • 都指向同一个对象
    • 都指向了同一个对象的下一地址

注:一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等

9.void* 指针

  • void*是一种特殊的指针类型,可用于存放任意对象的地址
double obj = 3.14, *pd = &obj;
void *pv = &obj;  // obj可以是任何类型的对象
  • 以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象

2.3.3 理解复合类型的声明

1.定义多个变量

// 写法1:把修饰符和变量标识符写在一起,本书采用这种方法
int *p1, p2;  // p1是指向int的指针,p2是int
// 写法2:把修饰符和类型名写在一起
int* p1, p2;  // p1是指向int的指针,p2是int
  • 上述两种定义指针或引用的不同方法没有对错之分,关键是选择并坚持其中的一种写法,不要总是变来变去

2.指向指针的指针

  • 指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
  • 通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针

3.指向指针的引用

int i = 42;
int *p;
int *&r = p;  // r是对指针p的引用
  • 要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针

2.4 const限定符

定义

  • 防止程序改变某个值,可以用关键字const对变量的类型加以限定
const int bufSize = 512;  // 编译器将在编译过程中把用到该变量的地方都替换成对应的值。即译器会找到                           // 代码中所有用到bufSize的地方,然后用512替换。
  • const对象一旦创建后其值就不能再改变,所以const对象必须初始化
  • 与非const类型所能参与的操作相比,const类型的对象能完成其中大部分,主要的限制就是只能在const类型的对象上执行不改变其内容的操作。
  • const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次
// file_1.cpp定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn;
// file_1.h头文件
extern const int bufSize;  // 与file.cpp中的bufSize是同一个

2.4.1 const的引用

1.常量引用

  • 把引用绑定到const对象称为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象(注:“对常量的引用”简称为“常量引用
const int ci = 1024;
const int &r1 = ci;  // 正确:引用r1及其对应的对象ci都是常量

r1 = 42;  // 错误,r1是对常量的引用,不能更改
int &r2 = ci;  // 错误,因为r2是非常量引用,不能指向常量引用,因为r2的值可以更改

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

int i = 42;
int &r1 = i;
const int &r2 = i;  // 对const的引用可能引用一个并非const对象,i的值可以通过改变i本身或i引用实现
r1 = 0;

2.4.2 指针和const

1.指向常量的指针

  • 指向常量的指针(pointer to const)不能用于改变其所指对象的
const double pi = 3.14;  // pi是一个常量
double *ptr = &pi;  // 错误,不能用普通指针指向常量
const double *cptr = &pi;  // 正确
*cptr = 42;  // 错误,不能给常量赋值
double dval = 3.14;
cptr = &dval;  // 正确,但不能通过cptr改变dval的值

2.常量指针

  • 常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了(注:和指向常量的指针不同,不同于引用
int errNumb = 0;
int *const curErr = &errNumb;  // curErr将一直指向errNumb
const double pi = 3.14;
const double *const pip = &pi;  // pip是一个指向常量对象的常量指针

double dval = 3.14;
curErr = &dval;  // 错误,curErr是常量指针
*pip = 2.72;  // 错误,pip是指向常量的
  • 从右向左阅读。在上面程序中离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,我们也能推断出,pip是一个常量指针,它指向的对象是一个双精度浮点型常量

2.4.3 顶层const

1.定义

  • 以指针为例,顶层const(top-level const)表示指针本身是个常量,而底层const(low-level 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 int &r = ci;  // 用于声明引用的const都是底层const

2.4.4 constexpr和常量表达式

1.常量表达式

  • 常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式
// 常量表达式
const int max_files = 20;
const int limit = max_files + 1;
// 非常量表达式
int staff_size = 27;
const int sz = get_size();

2.constexpr变量

  • C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化
constexpr int mf = 20;  // 20是常量表达式
constexpr int limit = mf + 1;  // mf + 1是常量表达式
constexpr int sz = size();  // 尽管不能使用普通函数作为constexpr变量的初始值,但新标准允许定义一                             // 种特殊的constexpr函数,可以初始化constexpr变量。
  • 一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型

3.字面值类型

  • 算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型。只有字面值类型才被定义成constexpr
  • 但一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。
  • 函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

4.指针和constexpr

  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关
const int *p = nullptr;  // p是一个指向整型常量的指针
constexpr int *q = nullptr;  // q是一个指向整数的常量指针

2.5 处理类型

2.5.1 类型别名

1.定义

  • 类型别名(type alias)是一个名字,它是某种类型的同义词。它让复杂的类型名字变得简单明了,有助于程序员清楚地知道使用该类型的真实目的
  • 传统使用关键字typedef
typedef double wages;  // wages是double的同义词
typedef wages base, *p;  // base是double的同义词,p是double*的同义词
  • 使用别名声明
using SI = Sales_item;  // SI是Sales_item的同义词,这种方法用关键字using作为别名声明的开始,其后                         // 紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名

2.指针、常量和类型别名

typedef char *pstring;
const pstring cstr = 0;  // cstr是指向char的常量指针
const pstring *ps;  // ps是一个指针,他的对象是指向char的常量指针
const char *str = 0;  // str是指向常量字符的指针是一定不同于第2行语句的,因为pstring数据类型已经                       // 变成了指针,而这语句数据类型为char

2.5.2 auto类型说明符

1.定义

  • auto让编译器通过初始值来推算变量的类型,故auto定义的变量必须有初始值
auto i = 0, *p = &i;  // 正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14;  // 错误,sz和pi的类型不一致

2.复合类型、常量和auto

int i = 0, &r = i;
auto a = r;  // a是一个整数(r是i的引用,而i是一个整数),当引用被用作初始值时,真正参与初始化的其实              // 是引用对象的值

const int ci = i, &cr = ci;  // ci是顶层const,而cr是底层const
auto b = ci;  // b是一个整数,ci的顶层const被忽略
auto c = cr;  // c是一个整数,cr是ci的引用,ci本身是顶层const
auto d = &i;  // d是一个整形指针
auto e = &ci;  // e是一个指向整型常量的指针(对常量对象取地址是一种底层const)
const auto f = ci;  // ci的类型是int,所有f为const int

2.5.3 decltype类型指示符

1.定义

  • decltype的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值
delctype(f()) sum = x;  // sum的类型就是函数f的返回类型
  • decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
const int ci = 0, &cj = ci;
decltype(ci) x = 0;  // x的类型是const int
decltype(cj) y = x;  // y的类型是const int&,绑定到x
decltype(cj) z;  // 错误,z是引用,必须要初始化

2.decltype和引用

int i = 42, *p = &i, &r = i;
decltype(r + 0) b;  // 正确,b是一个未初始化的int
decltype(*p) c;  // 错误,c是int&,必须要初始化

decltype((i)) d;  // 错误,d是int&,必须要初始化
decltype(i) e;  // 正确,e是一个未初始化的int

// decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

2.6 自定义数据结构

2.6.1 定义Sales_data类型

1.定义

  • 类以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
Sales_data accum, trans;  // 声明变量
  • 注:结构的结束花括号可以紧跟变量名以表示对该类型的定义,所以分号必不可少,但最好不要把对象的定义和类的定义放在一起

2.类数据成员

  • 类体定义类的成员,上述类只有数据成员(data member)。类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他Sales_data的对象。此外C++11新标准规定,可以为数据成员提供一个类内初始值。

2.6.2 使用Sales_data类

  • 使用点操作符访问对象成员
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
Sales_data accum, trans;  // 声明变量
std::cout << accum.revenue << std::cout ;  // 点操作符

2.6.3 编写自己的头文件

1.类头文件

  • 类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。故应把Sales_data类定义在名为Sales_data.h的头文件中。
  • 且头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。

2.预处理器概述

  • 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它从C语言继承而来,之前已经用到了一项预处理功能#include
  • 另一项预处理功能是头文件保护符#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到**#endif**指令为止。预处理变量无视C++语言中关于作用域的规则。
// 如果#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止,否则忽略掉#ifndef
// 到#endif之间的部分
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
}
#endif
  • 头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值