C++_Primer02.variables

变量和基本类型

基本内置类型

  • 算数类型 (arithmetic type)
    • 整型
      • 字符,整型数,布尔值
    • 浮点型
      • 浮点型
  • 空类型 (void)
    • 不对应具体的值,仅用于特殊场合
    • 当函数不返回任何值时使用空类型作为返回类型

整型值的尺寸大小

整型值在不同机器上所占空间有所差别。C++标准规定的最小尺寸:

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符 16位
char16_t Unicode字符 16位
char32_t Unicode字符 32位
short 短整型 16位
int 整型 16位
long 长整形 32位
long long 长整形 64位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个 long long 至少和一个 long 一样大。

数据类型的选择

  • 当明确知道数值不可能为负时,选用无符号类型
  • 使用int执行整数运算
    • short 常常显得太小,而long一般和int有一样的尺寸
    • 如果数值超过int,选用 long long
  • 算数表达式中不使用 char 或 bool,只有在存放字符或布尔值时才使用它们
    • 因为有些机器上char是有符号的,有的是无符号的。所以使用char进行运算特别容易出问题
    • 如果需要使用一个不大的整数,那么明确指定它的类型是 signed char 或 unsigned char
  • 执行浮点数运算使用double
    • 这是因为float通常精度不够,而且双精度和单精度浮点数的计算代价相差无几
    • 事实上,对有些机器来说,双精度运算甚至比单精度还快
    • long double 在一般情况下是没有必要的,而且它的运算消耗也较大

类型转换

bool b = 42;            // true
int i = b;              // 1
i = 3.14;               // 3
double pi = i;          // 3.0
unsigned char c = -1;   // 255
signed char c2 = 256;   // c2 是未定义的
// 当赋给带符号类型一个超过它表示范围的值时,结果是未定义的
// 程序可能继续工作,可能崩溃,也可能生成垃圾数据

有符号和无符号类型的值相加减:

unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;    // -84
std::cout << i + u << std::endl;    // 如果int是32位,则输出 4294967264

unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;      // 输出 32
std::cout << u2 - u1 << std::endl;      // 输出 4294967264

int 先转换为无符号数,然后再进行计算。

转换规则:

  • 非布尔值赋值给布尔类型时,初始值为 0 则结果为 false,否则为 true
  • 布尔值赋给非布尔类型时,false 为 0,true 为1
  • 浮点数赋给整数类型时,进行了近似处理。仅保留浮点数中小数点之前的部分
  • 把一个整数赋给浮点数时,小数部分记为0.如果该整数所占空间超过了浮点数容量,精度可能有损失
  • 当赋给无符号类型一个超过它的表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数
  • 当赋给带符号类型一个超出它表示范围的值时,结果是未定义的,程序可能继续运行,可能崩溃,也可能生成垃圾数据

字面值

整型和浮点型字面值

20, 024, 0x14

八进制,十进制和十六进制字面值的类型是能容纳其数值的 int, unsigned int, long, unsigned long, long long 和 unsigned long long 中的尺寸最小者。
如果一个字面值连阈值关联的最大数据类型都放不下,将产生错误。
负数的符号不属于字面值,比如 -42 的负号并不在字面值之内,它只是对字面值取负号而已。
short 没有对应的字面值,但可以在字面值后加后缀代表相应的字面值类型。

浮点型

浮点型字面值是一个double

3.14159 3,14159E0 0. 0e0 .001

字符和字符串

'a'
"Hello world!"

std::cout << "a really, really long string literal "
            "that spans two lines." << std::endl;

转义序列

escape sequence

  • 不可打印字符:

\n 换行符, \t, \a 报警符, \v 纵向制表符

\b 退格符, ", \, ?, '

\r 回车符, \f 进纸符

  • 也可以使用泛化的转义序列

\x 后紧跟1个或多个16进制数字,或 \ 后紧跟1个,2个或3个八进制数字

Latin 1 字符集(ISO Latin-1字符集是Unicode字符集的一个子集,对应Unicode字符表的前256个字符)中常用字符:

\0 空字符, \7 响铃, \12 换行符, \40 空格

\115 字符M, \x4d 字符M

std::cout << "Hi \x4dO\115!\n";     // Hi MoM!

指定字面值的类型

前后缀指定字面值类型:

前缀 含义 类型
u Unicode 16字符 char16_t
U Unicode 32字符 char32_t
L 宽字符 wchar_t
u8 UTF-8 (用于字符串) char
后缀 类型
u, U unsigned
l, L long
ll, LL long long
f, F float
l, L long double
L'a'        // 宽字符字面值,类型是 wchar_t,占16位,2字节
u8"hi!"     // utf-8 编码
42ULL       // 无符号整型,unsigned long long
1E-3F       // 单精度浮点型字面值,float
1.24159L    // long double

变量

变量定义

对象:一块能存储数据并具有某种类型的内存空间

有些人对对象的理解是仅在与类有关的场景下才使用“对象”这个词

初始化:在对象被创建时获得了一个特定的值

初始化不等于赋值,初始化和赋值是两个完全不同的操作
初始化是创建变量时赋予其一个初始值,而赋值是指把对象的当前值擦除,用一个新值来代替
很多编程语言中两者区别几乎可以忽略不计,即使是C++有时这种区别也无关紧要,所以容易将其混为一谈

列表初始化

初始化的多种方法:

int unis_sold = 0;
int unis_sold = {0};
int unis_sold{0};       // C++11, 列表初始化
int unis_sold(0);

当使用列表初始化,并且初始值存在丢失信息的风险,则编译器会报错:

long double ld = 3.14156;
int a{ld}, b = {ld};        // 错误:a{ld} 转换未执行,因为存在信息丢失的风险 (b 没问题)
int c(ld), d = ld;          // 正确:转换执行,且丢失了部分值

默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值由变量类型决定,同时定义变量的位置也会对此有影响。

如果内置类型的变量未被显式初始化,它的值由其所在的位置(作用域)决定。函数体之外的变量被初始化为0,函数体内的内置类型变量将不被初始化,一个未被初始化的内置类型变量的值是未定义的,试图拷贝或访问时将引发错误。

每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果允许,它将决定对象的初始值到底是什么。

绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。比如 string 类规定如果没有指定初始值,则生成一个空串。

一些类要求每个对象都显式初始化,此时如果创建了一个该类的对象而未对其初始化,将引发错误。

对于函数体内的内置类型对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类决定。

变量声明和定义的关系

为了允许把程序分成多个逻辑部分来编写,C++支持分离式编译(seperate compilation)机制,允许程序分成若干个文件,每个文件可被独立编译。

为支持分离式编译,C++将声明和定义区分开来。声明 使名字被程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而 定义 负责创建与名字关联的实体。

声明规定了变量的类型和名字。而定义除此之外还申请了存储空间,也可能为变量赋一个初始值。

如果想声明一个变量而不是定义它,就在变量名前添加关键字 extern,并且不要显式地初始化它。任何包含了显式初始化的声明即成为定义,这样做就抵消了 extern 的作用:

extern int i;   // 声明而非定义
int j;          // 声明并定义
extern double pi = 3.14;    // 定义

另外,在函数体内部试图初始化一个由 extern 标记的变量,将引发错误。

变量只能被定义一次,但可以被多次声明
如果多个文件中使用了同一个变量,就必须将声明和定义分离
而且变量的定义只能出现在一个文件中,不能重复定义,其他用到该变量的文件必须对其声明

标识符

标识符由字母、数字和下划线组成,而且必须以字母或下划线开头。对长度没有限制,但对大小写敏感

C++关键字:

alignas continue friend register true
alignof decltype goto reinterpret_cast try
asm default if return typedef
auto delete inline short typeid
bool do int signed typename
break double long sizeof union
case dynamic_cast mutable static unsigned
catch else namespace static_assert using
char enum new static_cast virtual
char16_t explicit noexcept struct void
char32_t export nullptr switch volatile
class extern operator template wchar_t
const false private this while
constexpr float protected thread_local
const_cast for public throw

操作符替代名:
and, bitand, compl, not_eq, or_eq, xor_eq, and_eq, bitor, not, or, xor

名字的作用域

global scope: 全局作用域
block scope: 块作用域
inner scope: 内层作用域
outer scope: 外层作用域

复合类型

compound type
指基于其他类型定义的类型,比如引用和指针

引用

reference
引用为对象起了另外一个名字
通过将声明符写成 &d 的形式来定义引用类型:

int ival = 1024;
int &refVal = ival; // refVal 指向 ival, 相当于 ival 的另一个名字
int &refVal2;       // 报错:引用必须被初始化

一般在初始化变量时,初始值被拷贝到新建的对象中。然而在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法将引用重新绑定到另外一个对象,所以引用必须被初始化。

  • 引用即别名
    • 引用在声明时必须初始化,且必须把变量赋给它,而不是直接赋值(必须是左值而不是右值)
      • 左值指向某个内存地址,比如变量或指针等
      • 右值没有对应的内存空间,一般为直接量
    • 引用在声明时就已经绑定,绑定后无法再绑定到其他变量上
    • 为引用赋值,实际上是把值付给了与引用绑定的对象
  • 引用本身不是一个对象,所以不能定义引用的引用

C++11 中新增了一种引用,即所谓的“右值引用 (rvalue reference)”,这种引用主要用于内置类
严格来说,当我们说 “引用” 时,指的其实是 “左值引用 (lvalue reference)”
引用在概念上等价于取地址

指针

与引用不同,指针本身就是一个对象,允许对指针赋值和拷贝,也可将指针指向其他对象;指针不需要在定义时赋初值,但若未对其初始化,他会拥有一个不确定的值。

int ival = 42;
int *p = &ival; // 把ival的地址放在指针p中,指针p就指向了ival

指针值应属于4种状态之一:

  • 指向一个对象
  • 指向紧邻对象内存空间的下一个位置
  • 空指针,没有指向任何对象(指向了一个特殊位置)
  • 无效指针,上述三种之外的其他值

试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器不负责检查此类错误。访问无效指针的后果无法预计,因此需要程序员必须清楚指针是否有效。

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

空指针

null pointer
不指向任何对象(其实是指向了一个特殊地址,地址值为0)

// 三种方式等价
int *p1 = nullptr;  // C++11 标准
int *p2 = 0;
int *p3 = NULL;     // 需要先 #include cstdlib

NULL 是一个预处理变量,定义在 cstdlib 头文件中,值为0
新标准下最好使用 nullptr,尽量避免使用 NULL

条件判断

int ival = 1024;
int *p1 = 0;
int * p2 = &ival;

if (p1){            // p1 值是0,判断为 false
    // ...
}

if (p2){            // p2 指向 ival,不为0,判断为 true
    // ...
}

void* 指针

void* 是一个特殊的指针类型,可以存放任意对象的地址

利用 void* 指针能做的事比较有限:与其他指针比较、作为函数的输入或输出,或者赋给另一个 void* 指针。不能直接操作 void* 指针指向的对象。因为我们不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

想要获取或操作其指向的对象,则需要先将其转换成对应的类型。

理解复合类型的声明

int i = 1024, *p = &i, &r = i;

类型修饰符 *& 是作用于变量上的,而不是类型:

int* p;         // 合法但容易产生误导
int* p1, p2;    // p1 是指针,p2 是变量

一般来说,修饰符个数没有限制,比如多级指针,** 表示指向指针的指针,***表示三级指针。

指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针,但存在指针的引用。

int i = 42;
int *p;
int *&r = p;    // r 是指针 p 的引用
r = &i;         // 对 r 进行赋值,使指针 p 指向 i
*r = 0;         // 对 r 指向的内容赋值,i 的值变为 0

对一条比较复杂的指针或引用的声明语句,从右往左阅读有助于弄清楚它的真实含义

const 限定符

const 修饰的变量,其值不能通过该变量进行改变。
因为 const 对象一旦创建就不能再改变,所以 const 对象必须初始化。

const int i = get_size();   // 正确,运行时初始化
const int j = 42;           // 正确,编译时初始化
const int k;                // 错误,未初始化

对 const 变量进行操作:

int i = 42;
const int ci = i;
int j = ci;         // 正确

在初始化变量时,根本无需在意所赋的值是不是常量,因为拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象和原来的对象就没什么关系了。

对 const 修饰的变量取地址,得到的指针类型也是 const 的。

默认情况下,const 对象仅在文件内有效

当以编译时初始化的方式定义一个 const 对象时,编译器将在编译过程中把用到该变量的地方都替换成字面量:

const int bufSize = 512;

若一个变量在多个文件中使用,则每个使用它的文件都需要对其声明,使用 extern 关键字声明,但只需要定义一次就可以了:

// file_1.cc 定义并初始化一个常量,该常量能被其他文件访问
extern const int bufSize = 512;

// file_1.h 头文件
extern const int bufSize;

const 的引用

常量的引用(reference to const),不能被修改

const int i = 1024;
const int &ri = i;      // 正确
ri = 42;                // 错误
int &re = i;            // 错误;re 也应该被 const 修饰

常量的引用可以是对右值的引用

int i = 42;
const int &r1 = i;      // 正确
const int &r2 = 42;     // 正确
const int &r3 = i * 2;  // 正确
int &r4 = i * 2;        // 错误

初始化时的类型转换

引用在初始化时有两点需要注意:

  • 当初始化是一个左值时,没有任何问题
  • 当初始化值不是一个左值,则它只能对一个 const T& (常量引用)进行赋值,而且这个赋值有一个过程:
    • 首先将其隐式转换到类型 T,然后将这个转换结果存放在一个临时对象中,最后用这个临时对象来初始化这个引用常量
double pi = 3.14;
const int &r = pi;

编译器会把上述代码进行转换:

double pi = 3.14;
const int temp = pi;    // temp = 3
const int &r = temp;

r绑定了一个临时量对象,所谓临时量,是指编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象,常把这个量简称临时量。

如果 r 不是常量引用,而是变量引用,则按照上述分析,它最终会绑定到一个临时量上,而这样是无意义的,C++ 语言把这样的行为归为非法。

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

常量引用仅对引用可参与的操作做了限定,对于引用的对象本身是不是个常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。

int i = 42;
const int& ri = i;
i = 0;

指针和 const

指向常量的指针

pointer to const

int i = 0;
const int *p = &i;  // 不能通过指针 p 改变 i 的值
* p = 5;            // 错误

const 指针

const pointer

int i = 0;
int *const p = &i;  // 指针 p 指向的地址不能改变,但可以通过 p 修改 i 的值
*p = 5;             // 正确
const int *const p1 = &i;   // 指向常量对象的常量指针
// 既不能改变 p1 的指向,也不能通过 p1 改变 i 的值

顶层 const

为区别上述两种指针,用 顶层const 代表指针本身是个常量,用 底层 const 代表指向的对象是个常量。

int i = 0;
int *const p1 = &i;     // 顶层const
const int *p2 = &i;     // 底层const
const int *const p3 = &i;   // 左侧的const 是底层,右侧的是顶层

执行拷贝操作时,底层const 控制了被赋值对象的类型,考入和考出的对象必须有相同的底层const 资格,或者两个对象的数据类型必须能够转换。非常量能够转换成常量,反之不行。

constexpr 和常量表达式

这其实是一个历史遗留的命名问题,简单说const其实是readonly,constexpr才是const (https://www.zhihu.com/question/35614219)

常量表达式(const expression)是指值不会改变并且在编译过程中就能得到计算结果的表达式。比如字面值、用常量表达式初始化的 const 对象等。

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定:

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

sz 在运行时才能获得,所以它不是常量表达式

constexpr 变量

C++11 规定,允许将变量声明为 constexpr 类型,使编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:

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

C++11 标准允许定义一种特殊的 constexpr 函数,这种函数应该足够简单以使得编译时就能计算其结果。

一般来说,如果你认为某变量是一个常量表达式,那就把它声明为 constexpr 类型

字面值类型

一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者是存储于某个固定地址中的对象。

一般来说,函数体内定义的变量并非存放于固定地址中,因此 constexpr 指针不能指向这样的变量。相反,定义于所有函数体外的对象其地址固定不变,能用来初始化 constexpr 指针。
其实,C++ 允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样有固定地址,constexpr 引用能绑定到这样的变量上,constexpr 指针也能指向这样的变量。

指针和 constexpr

相当于顶层 const编译期求值

在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关

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

p 和 q 类型完全不同,p 是一个指向常量的指针,而 q 是一个常量指针。其中 constexpr 把它所定义的对象置为了顶层 const。

constexpr int *np = nullptr;
int j = 0;
constexpr int i = 42;
constexpr const int *p = &i;    // p 是常量指针,指向常量 i
constexpr int *p1 = &j;         // p1 是常量指针,指向变量 j
// 注意,最后面两句话中, i 和 j 的地址必须能编译器确定,即它们都是全局变量

完整代码:

#include <iostream>

const int i = 2;
int j = 10;

int main(int argc, char** argv){
    constexpr const int* p0 = &i;
    constexpr int* p = &j;

    std::cout << "i: " << i << ", * p0: " << * p0 << std::endl;
    std::cout << "j: " << j << ", * p: " << * p << std::endl;
    * p = 20;
    std::cout << "j: " << j << ", * p: " << * p << std::endl;

    return 0;
}

output:

i: 2, *p0: 2
j: 10, *p: 10
j: 20, *p: 20

处理类型

类型别名

type alias
是一个名字,它是某种类型的同义词。它让复杂的类型名变得简单明了、便于理解和使用。

有两种方法定义类型别名,传统方法是使用关键字 typedef

typedef double wages;       // 用 wages 代表 double 类型
typedef wages base, * p;    // base 代表 wages 类型,p 代表 wages* 类型
typedef int arrT[10];       // arrT 代表 int[10]

这里的声明符也可以包含类型修饰

另一种方法是使用 别名声明 (alias declaration):

using SI = Sales_item;
using arrT = int[10];

其作用是把等号左侧的名字规定成等号右侧类型的别名。

指针、常量和类型别名

typedef char *pstring;
const pstring cstr = 0; // cstr 是指向 char 的常量指针
const pstring *ps;      // ps 是一个指针,它的对象是指向 char 的常量指针,相当于二级指针

注意,不能简单地进行类型替换:const char * cstr = 0; 这种理解是错误的。这样就理解成了 (const char) (* cstr),把 typedef char *pstring 割裂开了,而真实意图是 const (char *) cstr
const pstring cstr 表明 cstr 是一个常量,等价于 pstring (const cstr),即 char * const cstr

auto 类型说明符

程序中常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型,而要做到这一点并不容易。为解决这个问题,C++11 引入了 auto 类型说明符,它能让编译器通过初始值来推算变量的类型。同时,auto 定义的变量必须有初始值:

auto item = val1 + val2;

根据 val1 和 val2 相加的结果来推断 item 的类型。如果 val1 和 val2 是类型 Sales_item 的对象,则 item 类型就是 Sales_item.

使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一致:

auto i = 0, *p = &i;        // 正确
auto sz = 0, pi = 3.14;     // 错误; sz 和 pi 类型不一致

复合类型、常量和 auto

编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
比如当引用被用作初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为 auto 的类型:

int i = 0, &r = i;
auto a = r;         // a 是整数,而不是引用

其次,auto 会忽略掉顶层 const,底层 const 会保留下来 (引用除外)

const int ci = i, &cr = ci;
auto b = ci;    // b 是整型变量(ci 的顶层 const 被忽略)
auto c = cr;    // c 是整型变量(同上)
auto d = &i;    // d 是整型指针
auto e = &ci;   // e 是指向整型常数的指针(对常量取地址是一种底层 const)

如果希望推断出的 auto 类型是一个顶层 const,需要明确指出:

const int ci = i;
const auto f = ci;

也可以将引用类型设为 auto,原来的初始化规则仍然适用:

const int ci = 0;
auto &g = ci;       // 正确,g 是 int&,绑定到 ci
auto &h = 42;       // 错误,不能为非常量引用绑定字面值
const auto &j = 42; // 正确,可以为常量引用绑定字面值(绑定到了一个临时对象上)

设置一个类型为 auto 的引用时,初始值中的顶层 const 仍然保留。这是因为引用本身不占内存,只能绑定到别的变量或常量上,若绑定到常量上,则该引用也一定是一个常量引用。

要在一条语句中定义多个变量,切记,符号 & 和 * 只属于某个声明符,而不是基本数据类型的一部分,同时注意初始值必须是同一种类型:

auto k = ci, &l = i;        // k 是整数,l 是引用
auto &m = ci, *p = &i;      // m 是引用,p 是指针
auto &n = i, *p2 = &ci;     // 错误,i 是 int,而 ci 是 const int

decltype 类型指示符

希望从表达式的类型推断出要定义的变量的类型,但不想用该表达式的值初始化变量,C++11 为此引入第二种类型说明符 decltype. 它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

decltype(f()) sum = x;  // sum 的类型是 f() 函数的返回类型

decltype 处理顶层 const 和引用的方式与 auto 有些许不同。若表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内)

const int ci = 0, &cj = ci;
decltype(ci) x = 0;     // x 的类型是 const int
decltype(cj) y = x;     // y 的类型是 const int &
decltype(cj) z;         // 错误,z是一个引用,必须初始化

decltype 和引用

如果 decltype 使用的表达式不是一个变量,则它返回表达式结果对应的类型。

int i = 42, *p = &i, &r = i;
decltype(r+0) b;    // 正确,b 是 int 类型,而不是 int &
decltype(* p) c;    // 错误,c 是 int & 类型,必须初始化

因为 r 是一个引用,因此 decltype(r) 的结果是引用类型,如果想让结果是其所指的类型而不是引用类型,可以把 r 作为表达式的一部分比如 r+0,这样表达式的结果将是一个值而不是一个引用。
另外,如果表达式的内容是解引用操作,则 decltype 将得到引用类型。所以 decltype(*p) 的结果是引用类型而不是 int。

decltype 和 auto 还有一个重要区别,decltype 的结果类型与表达式的形式密切相关。比如如果变量名上加了一对括号,则得到的类型与不加括号会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果加了括号,编译器把它当做一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype 就会得到引用类型。

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

切记:decltype((variable)) 的结果永远是引用,而 decltype(variable) 的结果只有当 variable 本身就是一个引用时才是引用
但如果括号中不是变量而是表达式,比如decltype((variable+0)),则它的结果不是引用

自定义数据结构

销售记录类型

Sales_data.h 文件:

#ifndef SALES_DATA_H_
#define SALES_DATA_H_
#include <string>

struct Sales_data{
    std::string bootNo;     // 书名
    usigned units_sold = 0; // 销售数量
    double revenue = 0.0;   // 销售收入
};
#endif

main.cpp 文件:

#include <iostream>
#include <string>
#include "Sales_data.h"
int main(){
    double price;
    Sales_data data1, data2;

    // 读入数据
    std::cin >> data1.bookNo >> data1.units_sold >> price;
    data1.revenue = data1.units_sold * price;

    // 读入第二次销售数据
    std::cin >> data1.bookNo >> data1.units_sold >> price;
    data1.revenue = data1.units_sold * price;

    // 检查两次销售的书籍ISBN编号是否相同
    //    如果相同,输出它们的和
    //    否则,输出一条报错信息
    if (data1.bookNo == data2.bookNo){
        usigned totalCnt = data1.units_sold + data2.units_sold;
        double totalRevenue = data1.revenue + data2.revenue;

        std::cout << data1.bookNo << "\t" << totalCnt << "\t" << totalRevenue << "\t";
        if (totalCnt != 0){
            std::cout << totalRevenue/totalCnt << std::endl;
        }else{
            std::cout << "(no sales)" << std::endl;
        }
        return 0;
    }else{
        std::cerr << "Data must refer to the same ISBN"
                << std::endl;
        return -1;
    }
}

类一般都不定义在函数体内。
头文件通常包含那些只能被定义一次的实体,如类、const 和 constexpr 变量等。

小结

复杂类型 - 引用

  • 不占内存空间
  • 本质上是求内存地址
  • 引用常量
    • 必须是常量引用
    • 先创建一个 int 类型的临时对象,然后把42转成 int 类型,然后把 i 绑定到临时对象上
const int i = 5;

// 正确:
const int &r1 = 42;
const int &r2 = i + 7;
const int &r3 = i;

// 错误:
int &r4 = 42;
int &r5 = i + 7;
int &r6 = i;
r3 = 42;
  • 指针的引用
int i = 0, *p1 = &i;
const int j = 3, *p2 = &j;

// 正确:
int *&r1 = &i;
const int *&r2 = p1;
const int *&r3 = &j;
const int *&r4 = p2;
*r1 = 2;

// 错误:
int *&r5 = &j;
int *&r6 = p2;
*r3 = 42;

因为引用不占内存,所以没有引用的引用,没有指向引用的指针,也没有引用数组

修饰符

  • const
    • 常量,强调“只读”属性
  • constexpr
    • “只读”且编译期求值
    • 变量:变成常量
      • constexpr int i = 2;
    • 引用:
      • 引用在本质上类似指针,是绑定到内存地址,所以 constexpr 的引用必须绑定到编译期就能确定地址的变量上
      • 又因为 constexpr 的常量性质,constexpr 的引用也必须是 const 的
      • constexpr const int &r = &a; // a必须在编译器就能获得固定地址,比如 静态区的全局变量
    • 指针:常量指针,指向的地址不能改变,同上,可指向静态区的全局变量,指向地址的值可变

auto

自动推断类型并赋值;忽略顶层 const(引用除外)

  • 求得的值是基本类型:忽略 const,然后赋值
  • 求得的值是引用:引用的对象的类型,并忽略掉 const
  • 求得的值是指针:指针,忽略顶层const,保留底层 const
int i = 3;
const int ci = i, &ri = i;
auto a = ci;        // int 类型
auto b = ri;        // int 类型
auto c = &i;        // 指向整型的指针
auto d = &ci;       // 指向整型常量的指针(对常量取地址是一种底层 const)
auto e = &ri;       // 指向整型常量的指针(对引用常量取地址是一种底层 const)
  • 赋值引用:若是左值则绑定到左值,否则绑定到临时对象上
  • 赋值指针:指针
  • 一条赋值语句可以赋值多个变量,但类型必须一致
int i = 2;
auto &a = i;        // 正确,a是 int&,绑定到 i
auto &b = 5;        // 错误,先赋给临时对象,然后绑定到临时对象,临时对象的值是不能修改的
const auto &c = 5;  // 正确,绑定到临时对象

auto *p = &i;

const int &ci = i;
auto k = ci, &l = i;        // k 是整数,l 是引用
auto &m = ci, *p = &i;      // m 是引用,p 是指针
auto &n = i, *p2 = &ci;     // 错误,i 是 int,而 ci 是 const int

decltype

自动推断类型,但不赋值;保留顶层和底层 const,保留引用

注意:对指针解引用得到的是引用类型

int i = 2, *p = &i;
decltype(i) a;      // int
decltype(i+4) b;    // int
decltype((i+4)) c;  // int
decltype((i)) d;    // int &,变量加括号均为引用类型(与表达式加括号不同)
decltype(&i) e;     // int *,指针
decltype(*&i) e;     // int &
decltype(* p) f;    // int &,解引用操作返回的是引用类型
// 引用类型均需要初始化
posted @ 2020-12-01 22:52  keep-minding  阅读(98)  评论(0)    收藏  举报