C++学习之路(一):变量和基本类型

1-变量和基本类型-大纲.png

1.1 基本内置类型

1.1.1 算术类型

类型 含义 最小尺寸
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位有效数字
  • bool类型取值是true和false。
  • 一个char的大小和一个机器字节一样。
  • c++规定:一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大。
带符号和无符号类型

除了布尔类型和扩展的字符型外,其他整形可以划分为带符号的(signed)无符号的(unsigned),比如:

有符号的 无符号的
int unsigned int / unsigned(缩写,可以省略int)
short unsigned short
long unsigned long
long long unsigned long long

与其他整形不同,字符型被分为了3种:char、signed char 和 unsigned char。需要注意的是:类型char和类型signed char并不一样。尽管字符型有3种,但是字符的表现形式却只有2种:带符号的和无符号的。类型char实际会表现为上述2种形式中的一种,具体是哪种由编译器决定。

选择类型的经验准则:

  1. 当明确数值不可能为负时,选用无符号类型。
  2. 使用int执行整数运算。如果数值超过int的表示范围,选用long long。
  3. 在算术表达式中不要使用char和bool,只有在存放字符或布尔值时才使用它们。因为char不确定是有符号的还是无符号的。如果需要使用一个不大的整数,需要明确指定它的类型是signed char或者unsigned char。
  4. 执行浮点运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。

1.1.2 类型转换

bool b = 42;          // b为真
int i = b;            // I=1
i = 3.14;             // I=3
double pi = i;        // pi=3.0
unsigned char c = -1; // c=255
signed char = 256;    // c未定义
  • 把非布尔类型算术值赋给布尔类型时,0为false,其它为true。
  • 把布尔类型赋给非布尔类型时,false为0, true为1。
  • 浮点数赋给整数类型时,仅保留整数部分。
  • 把整数值赋给浮点类型时,小数部分为0。
  • 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
  • 当赋给带符号类型一个超出它表示范围的值时,结果是未定义的。
含义无符号类型的表达式
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;	// -84
std::cout << u + i << std::endl;	// 4294967264

u和i相加时,i会先转换为无符号数4294967254,再加10,结果为4294967264。

unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;	// 32
std::cout << u2 - u1 << std::endl;	// 4294967264,结果是取模后的值

当从无符号中减去一个值时,不管这个值是不是无符号的,都必须确保结果不能是一个负值。

切勿混用带符号类型和无符号类型:

如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换为无符号数。

1.1.3 字面值常量

整形和浮点型字面值
  • 十进制 20
  • 八进制 024
  • 十六进制 0x14

整形字面值具体的数据类型由它的值和符号决定。默认情况下:

  • 十进制字面值是带符号的。
  • 八进制和十六进制字面值可能带符号也可能是无符号的。

浮点型字面值表示方法,指数部分用e或E标识:

3.14159

3.14159E0

0.

0e0

.001

字符和字符串字面值
  • 字符字面值 'A'
  • 字符串字面值 "hello"

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

转义序列
转义字符 含义
\n 换行符
\v 纵向制表符
\\ 反斜线
\r 回车符
\t 横向制表符
\b 退格符
\? 问号
\f 进纸符
\a 报警(响铃)符
\" 双引号
\' 单引号
指定字面值的类型

字符和字符串字面值

前缀 含义 类型
u Unicode16字符 char16_t
U Unicode32字符 char32_t
L 宽字符 wchar_t
u8 UTF-8 char

整形字面值

后缀 最小匹配类型
u or U unsigned
l or L long
ll or LL long long

浮点型字面值

后缀 类型
f or F float
l or L long double

例如:

auto a = L'a';      // 宽字符类型,类型是wchar_t
auto b = u8"hi!";   // utf-8字符串字面值
auto c = 42ULL;     // 无符号整形字面值,类型是 unsigned long long
auto d = 1E-3F;     // 单精度浮点型字面值,类型是float
auto e = 3.14159L;  // 扩展精度浮点型字面值,类型是long double

1.2 变量

1.2.1 变量定义

int a = 0, b, c = 0;            // a, b, c都是int类型,a和c的初值为0
std::string book("C++ Primer"); // book通过一个string字面值初始化
初始值

当对象在创建时获得一个特定的值,我们说这个对象被初始化了。也可以在同一条定义语句中,用先定义的变量值去初始化后定义的其他变量。

double price = 109.99, discount = price * 0.16;     // 用price来初始化discount
std::string book("C++ Primer"), *book_ptr = &book;  // book_ptr是指向book的指针

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来代替。

列表初始化

C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如,定义一个int类型变量,并初始值为0,有下面4种形式:

  • int a = 0;
  • int a = {0};
  • int a{0};
  • int a(0);

使用花括号的形式被称为列表初始化。无论是初始化对象,还是为对象赋新值,都可以使用花括号括起来的初始值。

对于内置类型,使用列表初始化有一个重要特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错

double ld = 3.1415926;
int a{ld}, b = {ld};    // 错误:转换为执行,因为存在丢失信息的危险
int c(ld), d = ld;      // 正确:转换执行,且确实丢失了部分值
默认初始值

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

  1. 内置类型
  2. 函数外:0
  3. 函数内:未定义
  4. 绝大数类都无需显式初始化,除非一些类明确要求显式初始化。

1.2.2 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

为了支持分离式编译,c++语言将声明和定义区分开来。

  • 声明
    • 使名字为程序所知。
    • 一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。
  • 定义
    • 负责创建与名字关联的实体。

想要声明变量,就在变量名前面添加extern关键字,并且不要显式地初始化变量:

extern int i;  // 声明I而不是定义I
int j;         // 声明并定义j

任何包含了显式初始化的声明即成为定义。

extern double pi = 3.14159;	// 定义

变量能且只能被定义一次,但是可以被多次声明。

与c++不同的是,在c语言中,函数体外的同一个变量可以被定义多次,但最多只能有1处对其赋初值。因此可以将变量定义在.h文件中,但不推荐这么使用。

1.2.3 标识符

c++标识符由字母、数字和下划线组成,必须以字母或下划线开头,并且大小写字母敏感。

命名规范:

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

用户自定义标识符的其它一些约定

  1. 不能连续出现2个下划线。
  2. 不能以下划线紧连大写字母开头。
  3. 函数体外的标识符不能以下划线开头。

1.2.4 名字作用域

C++大多数作用域都以花括号分割。

  • 全局作用域
  • 块作用域

建议:当第一次使用变量时再定义它。

  1. 有助于更容易的找到变量的定义。
  2. 会有一个比较合理的初始值。
作用域嵌套

作用域可以彼此包含,被包含的作用域被称为内层作用域,包含别的作用域的作用域称为外层作用域

内层作用域中可以重新定义外层作用域已有的名字(但是最好不要这么做):

#include <iostream>
int reused = 42;
int main()
{
    int unique = 0;
    std::cout << reused << " " << unique << std::endl;  // 42 0
    int reused = 0; // 新建局部变量,覆盖全局变量
    std::cout << reused << " " << unique << std::endl;  // 0 0
    // 显式地访问全局变量reused
    std::cout << ::reused << " " << unique << std::endl;    // 42 0
    return 0;
}

关于::reused,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右边名字对应的变量。

如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

1.3 复合类型

复合类型是指基于其他类型定义的类型。C++语言有几种符合类型,这里仅介绍其中的两种:引用和指针。

一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

1.3.1 引用

提示:c++11新增了一种引用:“右值引用”,但这里介绍的引用是“左值引用”。

引用为对象起了另外一个名字,引用类型引用另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。

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

一般在初始化变量时,初始值会被拷贝到新建的对象中。而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。

  • 引用类型必须初始化。
  • 引用类型无法重新绑定到另外一个对象。
  • 引用本身并不是一个对象,因此不能定义引用的引用。
  • 引用类型的本质就是指针。因此一个结构体中的引用类型其大小和指针类型大小一致。

1.3.2 指针

指针是“指向”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。

指针与引用的区别:

  • 指针本身就是一个对象,引用仅是绑定对象的别名。
  • 允许对指针本身进行拷贝和赋值,引用绑定对象后不能再绑定其它对象。
  • 指针无须在定义时赋初值,引用必须在定义时绑定它的初始值。
获取对象的地址

使用取地址符(操作符&)

int ival = 42;
int *p = &ival;

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

指针值

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

  1. 指向一个对象。
  2. 指向紧邻对象所占空间的下一个位置。
  3. 空指针,意味着指针没有指向任何对象。
  4. 无效指针,也就是上述情况之外的其他值。
利用指针访问对象

使用解引用符(操作符*)

int ival = 42;
int *p = &ival;
cout << *p;

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

空指针

空指针不指向任何对象。

生成空指针的方法:

int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
  • NULL是预处理变量
  • 建议使用nullptr

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

int zero = 0;
int *pi = zero;	// 编译错误
赋值和指针

有时候要搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象。

int ival = 42;
int *pi = 0;
pi = &ival;  // pi的值被改变,现在pi指向了ival

如果写出

*pi = 0;	// ival的值被改变,指针pi并没有改变

*pi(也就是指针pi指向的那个对象)发送改变。

其他指针操作
  • 一个拥有合法值的指针,可以使用在条件表达式中。如果指针的值为0,则条件取false。任何非0指针对应的条件值都是true。
  • 对应两个合法的指针,可以使用==和!=来进行比较。如果两个指针存放的地址值相同,则它们相同。
void* 指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。

利用void*可以:

  1. 和别的指针比较;
  2. 作为函数的输入或输出;
  3. 赋值给另外一个void*指针。

不能直接操作void*指针所指的对象。

1.3.3 理解复合类型的声明

在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。

// i是一个int型的数,p一个int型指针,r是一个int型引用
int i = 1024, *p = &i, &r = I;

很多程序员容易迷惑于基本数据类型和类型修饰符的关系,其实后者不过是声明符的一部分罢了。

定义多个变量

经常有一种观点会误以为,在定义语句中,类型修饰符(*或&)作用于本次定义的全部变量。

  1. 把空格写在类型修饰符和变量名中间:

    int* p;	// 合法,但是容易产生误导
    
  2. 定义多个变量时,误以为int*是所有变量的类型:

    int* p1, p2;	// p1是指向int的指针,p2是int类型
    

涉及指针或引用的声明,一般有两种写法。

  1. 把修饰符和变量标识符写在一起,这种形式着重强调变量具有的复合类型:

    int *p1, *p2;	// p1和p2都是指向int类型的指针
    
  2. 把修饰符和类型名写在一起,并且每条语句只定义一个变量,这种形式着重强调本次声明定义了一种复合类型:

    int* p1;	// p1是指向int的指针
    int* p2;	// p2是指向int的指针
    

两种方式都可以,关键是要统一写法,不要总是变来变去。

指向指针的指针

指针是内存中的对象,像其他对象一样也有自己的地址,因此运行把指针的地址再存放到另一个指针当中。

通过*的个数可以区分指针的级别。

int ival = 1024;
int *pi = &ival;	// pi指向一个int型的数
int **pii = &pi;	// pii指向一个int型的指针
指向指针的引用

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

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

要理解r的类型,最简单的方法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。

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

下面p是对哪个对象的引用?

int i = 42;
int *&p = &i;	// error: cannot bind non-const lvalue reference of type ‘int*&’ to an rvalue of type ‘int*’

1.4 const限定符

使用关键字const修饰的变量,它的值不能被修改。因为const对象一旦创建后其值就不能再修改,所以const对象必须初始化。

const int i = get_size(); // 正确:运行时初始化
const int j = 42;         // 正确:编译时初始化
const int k;              // 错误:k是一个未初始化的常量
默认状态下,const对象仅在文件内有效

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

当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

有时这样一种const变量,它的初始值不是一个常量表达式,但有确实有必要在文件间共享。因此我们不希望编译器为每个文件分别生成独立的变量,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。

解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

// file.cpp
extern const int bufSize = fcn();
// file.h
extern const int bufSize;	// 与file.cpp中定义的bufSize是同一个

常量值的定义必须加extern修饰,才能被其他文件使用。

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

我实际测试,定义前不加extern也可以的啊。。

1.4.1 const的引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用

  • 常量引用可以绑定非常量对象。
  • 非常量引用不可以绑定常量对象。
  • 常量引用可以绑定字面值。
  • 常量引用可以绑定临时量。
  • 非常量引用不可以绑定临时量。

我们把对const的引用简称为“常量引用”。

严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言不允许随意改变引用所绑定的对象,所以从这层意思上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。

临时量

所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时而临时创建的一个未命名的对象。

double dval = 3.14;
const int &ri = dval;
// 会转换为
const int temp = dval;
const int &ri = temp;

ri绑定的是临时量。修改dval的值,ri不会改变。当ri是非常量时,int &ri = dval;是非法的。

1.4.2 指针和const

与引用一样,指针也可以指向常量或非常量。类似于常量引用,指向常量的指针不能用于改变其所指向对象的值。

  • 指向常量的指针可以指向非常量对象。
  • 指向非常量的指针不可以指向常量对象。

所谓指向常量的指针或引用,不过是无法通过指针或引用改变指向对象的值,而对象的值是可以通过其他方式改变的。

const指针

指针是对象而引用不是,因此就像其他类型一样,允许把指针本身定为常量,成为常量指针。

注意:常量指针和指向常量的指针是不同的。

  • 常量指针必须初始化。
  • 常量指针指向一个对象后,就不能再指向其他对象了。

把*放在const之前用以说明指针是一个常量。

int errNumb = 0;
int *const curErr = &errNumb;   // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = &pi;  // pip是一个指向常量对象的常量指针

弄清楚这些声明的含义最好的方法是从右向左阅读。

  • 离curErr最近的符号是const,意味着curErr本身是一个常量对象。
  • 对象的类型由声明符的其余部分确定。
  • 下一个符号是*,意思是curErr是一个常量指针。
  • 最后,基本数据类型是int,确定了常量指针指向的是一个int对象。

指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。

  • curErr指向的是一个一般的非常量对象,可以通过curErr去修改errNumb的值。
  • 而pip指向的是一个常量对象,不能通过pip修改pi的值。

1.4.3 顶层const

由于指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。

  • 顶层const表示指针本身是个常量。
    • 顶层const可以表示任意的对象是常量,对任何数据类型都适用,如算术类型、类、指针等。
  • 底层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
const int &r = ci;          // 用于声明引用的const都是底层const

当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受影响:

i = ci;     // 正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3;    // 正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响

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

int *p = p3;        // 错误:p3包含底层const的定义,而p没有
p2 = p3;            // 正确:p2和p3都是底层const
p2 = &i;            // 正确:int*可以转换为const int*
int &r2 = ci;       // 错误:普通的int&不能绑定到int常量上
const int &r3 = i;  // 正确:const int&可以绑定到一个普通int上

1.4.4 constexpr和常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程中就能够得到计算结果的表达式。

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

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不是常量表达式

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

在一个复杂的系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。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指针的初始值必须是nullptr或者0,或者是存储于某个固定地址上的对象。
  • 函数体内定义的变量一般来说并非存放在固定地址上,因此constexpr指针不能指向这样的变量。
  • 所有函数体外的对象其地址固定不变,能用来初始化constexpr指针。
指针和constexpr

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

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

constexpr把它所定义的对象置为了顶层const。

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

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

1.5 处理类型

1.5.1 类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。

类型别名的好处:

  1. 复杂的类型名字变得简单明了。
  2. 易于理解和使用。
  3. 有助于程序员清除地知道使用该类型的真实目的。

定义类型别名的两种方法:

  1. 使用关键字typedef

    typedef double wages;   // wages是double的同义词
    typedef wages base, *p; // base是double的同义词,p是double*的同义词
    
  2. 新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:

    using SI = Sales_item;  // SI是Sales_item的同义词
    
指针、常量和类型别名

如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:

typedef char *pstring;
const pstring cstr = 0;  // cstr是指向char的常量指针
const pstring *ps;       // ps是一个指针,它的对象是指向char的常量指针
  1. cstr中的const是顶层const,表示cstr不能再指向别的对象,但是可以通过cstr来修改其指向对象的值。
  2. ps中的const是底层const,ps可以再指向其他对象,但是不能通过ps来修改其指向对象的值。

这里不能通过把类型别名替换成它本来的样子的方式来理解:

const char *cstr = 0; // 是对 const pstring cstr的错误理解

声明语句中用到pstring时,其基本数据类型是指针(char *),可是替换后,数据类型就变成了char,*成为了声明符的一部分。这样的改写结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者是声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。

1.5.2 auto类型说明符

C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。auto让编译器通过初始值来推算变量的类型。所以,auto定义的变量必须有初始值。

double val1 = 20.1, val2 = 30.2;
auto item = val1 + val2;

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

auto i = 0, *p = &i;    // 正确:i是整形、p是整形指针
auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致
复合类型、常量和auto
  • 引用被用作初始值时,编译器以引用对象的类型作为auto的类型:

    int i = 0, &r = I;
    auto a = r;	// a是一个整数(r是i的别名,而i是一个整数)
    
  • auto会忽略顶层const,保留底层const;例如当初始值是一个指向常量的指针时:

    const int ci = i, &cr = ci;
    auto b = ci;	// b是一个整数(ci的顶层const特性被忽略掉了)
    auto c = cr;	// c是一个整数(cr是ci的别名,ci本身是一个顶层const)
    auto d = &i;	// d是一个整形指针(整数的地址就是指向整数的指针)
    auto e = &ci;	// e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
    
  • 如果希望推断出的auto类型是一个顶层const,需要明确指出:

    const auto f = ci;  // ci的推演类型是const int,f是const int类型
    
  • 还可以将引用的类型设为auto:

    auto &g = ci;       // g是一个整形常量引用,绑定到ci
    auto &h = 42;       // 错误:不能为非常量引用绑定字面值
    const auto &j = 42; // 正确:可以为常量引用绑定字面值
    

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

auto k = ci, &l = i;    // k是整数,l是整形引用
auto &m = ci, *p = &ci; // m是对整数常量的引用,p是指向整形常量的指针
auto &n = i, *p2 = &ci; // 错误:i的类型是int,而&ci的类型是const int

1.5.3 decltype 类型指示符

有时候并不想用表达式的结果来初始化变量,而仅仅想定义一个是表达式结果类型的变量。这时可以使用decltype类型说明符。

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

decltype(f())并不会实际调用函数f,而仅仅是将f的返回值的类型作为sum的类型。

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&,y绑定到x
decltype(cj) z;             // 错误:z是一个引用,必须初始化

需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。

decltype和引用

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

// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = I;
decltype(r + 0) b;  // 正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p) c;     // 错误:c是int&,必须初始化
  1. decltype(r)的结果是引用类型,如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,比如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
  2. 如果表达式的内容是解引用操作,则decltype将得到引用类型。因为解引用指针可以得到指针所指的对象,而且还能给这个对象赋值,因此,decltype(*p)的结果类型是int&,而不是int。

decltype和auto的另一个重要区别是:decltype的结果类型与表达式形式密切相关

  • 如果变量名加上一对括号,编译器就会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。
  • 如果变量名不加括号,则得到的结果就是该变量的类型。
// decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d;  // 错误:d是int&,必须初始化
decltype(i) e;    // 正确:e是一个(未初始化的)int

切记:decltype((variable))的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

1.6 自定义数据结构

定义如下结构:

struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

C++11规定,可以为数据成员提供一个类内初始值

类内初始值可以放在花括号中,或者放在等号右边,但是不能使用圆括号。

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

最新修改时间:2021-02-09 10:25

posted @ 2021-02-01 18:51  知_了  阅读(607)  评论(0编辑  收藏  举报