《C++ Primer》 拾遗 第 2 章 变量和基本类型
Notes Cpp Primer
Lastmod: 2021-06-01 周二 22:40:42

第 2 章 变量和基本类型

2.1 基本内置类型

基本内置类型分为算数类型 arithmetic type 和空类型 void

2.1.1 算数类型

整型 integral type 和浮点型

注意 C++ 只规定了每种算数类型的最小尺寸,所以不同机器上可能会有差异。

类型 含义 最小尺寸
bool 未定义
char 8 位
wchar_t 宽字符 16 位
char16_t Unicode 字符 16 位
char32_t Unicode 字符 32 位
short 16 位
int 16 位 至少和 short 一样大
long 32 位 至少和 int 一样大
long long 64 位 至少和 long 一样大
float 6 位有效数字
double 10 位有效数字
long double 扩展精度浮点数 10 位有效数字

可寻址的最小单元称为字节 byte

存储的基本单元为字 word

通常 float 是 32 bit 7个有效位,double 是 64 bit 16个有效位 而 long double 是 96 或 128 bit。

除了 bool 和扩展的字符型外其他的整型可分为带符号和无符号的

short, int, long, long long 为带符号的,可以通过增加 unsigned 将其变成对应的无符号类型。

特别的 char,signed char 和 unsigned char 是三种类型。char 是有无符号的由编译器决定。

2.1.2 类型转换

非 bool 转化为 bool 则 0 为 false,其他值为 true

bool 值转化为非 bool 则 false 为 0,true 为 1

浮点转化为整形则保留小数点前部分

整形转化为浮点则可能会损失精度

无符号类型被赋予超出范围的值则得到对无符号类型能表示的数字种类取模后的值。

有符号类型被赋予超出范围的值则结果是 undefined。

2.1.3 字面值常量

整型

10   // 10 进制
020  // 8  进制
0x3f // 16 进制

严格来说 10 进制字面值不会出现负数,-不属于字面值。

10 进制字面值的类型是 int,long,long long 中尺寸最小的。

8、16 进制字面值的类型是 int,unsigned int,long,unsigned long,long long,unsigned long long 中尺寸最小的。

浮点数字面值默认是 double 类型

3.1415
3.1415E0
0.
.33
0e0

char 型字面值

'a'

字符串字面值其实是一个常量字符构成的数组

"Hello, World!"

编译器会在每个字符串结尾增加 '\0' 因此字符串字面值的实际长度要比内容多 1。

两个字符串字面值紧邻且仅由空格、缩进或换行符分隔则视为一个整体。

cout << "abc" "def"
        "hhhhhhhhhhhh" << endl;
转义

不可打印字符 nonprintable 字符和 C++ 中有特殊含义的字符不能直接使用,此时需要用到转义序列 escape sequence。转义序列均以反斜线开始。

转移序列 意义
\n 换行
\v 垂直制表符
\\ 反斜线
\r 回车符
\t 水平制表符
\b 退格符
\? 问好
\f 进纸符
\a 报警(响铃)符
\" 双引号
\' 单引号

泛化转义序列

\x 后面跟 1 个或多个 16 进制数,\ 后跟 1 到 3 个 8 进制数。8 进制数超过 3 个后续部分视作下一个数,16进制数则是 \x 后的所有数字一起处理,过长则可能会报错。

指定字面值类型

字符和字符串字面值

u''    // char16_t
U''    // char32_t
L''    // wchar_t
u8""   // char UTF-8 只用于字符串字面常量 

整形

1u
1U // unsigned
1l
1L // long
1ll 
1LL // long long
1ULL // unsigned long long

浮点

1.5f
1.5F // float
1.5L
1e5L // long double

布尔型字面值

true false

指针字面值

nullptr

2.2 变量

2.2.1 变量定义

类型说明符 type specifier

注意变量的初始化和赋值时完全不同的。

注意 C++11 我们可以使用以下 4 种方式初始化

int a = 0;
int b = {0};
int c{0};
int d(0);

这里使用花括号进行初始化的方式成为列表初始化。

使用列表初始化的方式初始化内置类型变量时,如果初始值存在丢失信息的风险,则编译器会报错。

#include <iostream>
using namespace std;

int main() {
  double db = 3.1415926;
  int a = db;
  int b(db);
  int c{db};
  int d={db};

  return 0;
}
>g++ list_init.cpp -o list_init
list_init.cpp: In function 'int main()':
list_init.cpp:8:8: warning: narrowing conversion of 'db' from 'double' to 'int' [-Wnarrowing]
    8 |  int c{db};
      |        ^~
list_init.cpp:9:9: warning: narrowing conversion of 'db' from 'double' to 'int' [-Wnarrowing]
    9 |  int d={db};
      |         ^~
默认初始化

default initialized

任何函数体之外的变量如果未被显式初始化则被初始化为 0。

函数体内部的内置类型变量将不被初始化。未初始化的值时未定义的。

2.2.2 变量声明和定义的关系

C++ 支持分离式编译 separate compilation 机制,允许将程序分割为若干个文件,每个文件可被独立编译。

为了支持分离式编译,C++ 将声明 declaration 和定义 definition 分开。

声明使得名字为程序所致,一个程序想使用别处定义的名字必须包含其声明。定义负责创建与名字关联的实体。

如果只想声明而不是定义一个变量则需要 extern 关键字,且不要显示的初始化变量。

extern int i;

举个例子

A.h

extern int a;

A.cpp

#include "A.h"
int a = 10;

B.cpp

#include <iostream>
#include "A.h"

int main() {
  std::cout << a << '\n';
  return 0;
}

> g++ A.cpp B.cpp -o B
> B
> 10

C++ 是一种静态类型 statically typed 语言。在编译阶段会进行类型检查 type checking

2.2.3 标识符

标识符 identifier

C++ 关键字和操作符替代名不能作为标识符。

用户自定义的标识符种不能连续出现 2 个下划线,不能以下划线紧邻大写字母开头,函数体外的标识符不能以下划线开头。(g++ 如下文件编译并未报错?)

int _a;
int _A;

int main() {
  int A__B = 0;
  int _B;
  int _b;
  return 0;
}
关键字
alignas alignof asm auto bool break case catch char char16_t char32_t class const
constexpr const_cast continue decltype default delete do double dynamic_cast else 
enum  explicit export extern false float for friend goto if inline int long 
mutable namespace new noexcept nullptr operator private protected public register
reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while
操作符替代名
and bitand compl not_eq or_eq xor_eq and_eq bitor not or xor

2.2.4 名字的作用域

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

同一名字在不同作用域可能指向不同的实体。名字的有效区域始于名字的声明语句,到声明语句所在作用域结束为止。

全局作用域 global scope

块作用域 block scope

对于嵌套的作用域有

内层作用域 inner scope 和外层作用域 outer scope

内层作用域可以访问外层的名字。

2.3 复合类型

复合类型 compound type 是基于其他类型定义的类型。

2.3.1 引用

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

2.3.2 指针

类似于引用,指针也实现了对其他对象的间接访问。

指针本身也是对象,因此可以被赋值和拷贝。指针无需在定义时给初值。

空指针

应当使用 nullptr 字面值得到空指针。

NULL 是一个定义在 cstdlib 中,值为 0 的预处理变量 preprocessor variable

注意指针也可以用于条件表达式,非 0 指针对应 true。

void* 指针

void* 是一个特殊指针,可以用于存放任意对象地址。不能直接操作 void* 指针所指的对象。

指针的引用

引用不是对象,因此不存在指向引用的指针。但是指针是对象,因此可以有指针的引用。

int *p;
int *&r = p;

按照从右向左顺序阅读有助于理解复杂的类型。最接近变量名的造成最直接的影响。

2.4 const 限定符

非 const 对象可以初始化 const 对象。const 对象必须初始化。

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

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

const int M = 1005;

可以在声明与定义前都加上 extern 使其可以在其他文件访问。(实测声明加就可以了?)

A.h

extern const int a;

A.cpp

#include "A.h"
const int a = 10;

B.cpp

#include <iostream>
#include "A.h"

int main() {
  std::cout << a << '\n';
  return 0;
}

> g++ A.cpp B.cpp -o B
> B
> 10

2.4.1 const 的引用

我们常说的常量引用其实应该被称作对 const 的引用,因为引用本身不是对象,所以不存在让其恒定不变的说法。另一个角度引用绑定的对象是不能变化的,因此又可以视所有引用为常量。

const int a = 1;
const int &ra = a; // 对 const 的引用
// int &r = a;     // 错误 不能用非常量引用绑定 const 对象 

注意当常量引用绑定到非常量,其他形式是可以修改常量引用绑定的值的

int main() {
  int a = 5;
  const int &crb = a;
  // crb *= 3; // 直接修改是不行的
  a *= 3;
  cout << "a = " << a << '\n';
  cout << "b = " << crb << '\n';
  return 0;
}
> a = 15
> b = 15

但当常量引用绑定到非常量对象或字面值,可能会生成临时量 temporary

int main() {
  double a = 5.5;
  const int &crb = a; // crb 绑定到了一个临时对象
  a *= 3;
  cout << "a = " << a << '\n';
  cout << "b = " << crb << '\n';
  return 0;
}
> a = 16.5
> b = 5

2.4.2 指针和 const

指向常量的指针 pointer to const 不能用于改变其所指对象的值,但可以改变指向的对象。注意指向常量的指针指向的对象可以是非常量,行为将和常量引用类似。

int main() {
  int a = 5, b = 10;
  const int *cpa = &a;
  a *= 3;
  cout << "a = " << a << '\n';
  cout << "cpa = " << *cpa << '\n';
  cpa = &b;
  cout << "cpa = " << *cpa << '\n';
  return 0;
}
a = 15
cpa = 15
cpa = 10

常量指针 const pointer 不能改变指向的对象,必须初始化,但可以改变所指向的内容。

int a = 10;
int *const b = &a;

当然也存在指向常量的常量指针

const int *const b = &a;

2.4.3 顶层 const

顶层 const top-level const 表示指针本身是个常量。

底层 const low-level const 表示指针所指对象是常量。

更一般的顶层 const 可以表示任意对象是常量,但底层 const 只与指针、引用等复合类型的基本类型部分有关。

顶层 const 不影响拷贝操作。底层 const 则要求拷入的对象和拷出的对象有相同的底层 const 限制。(或者拷入对象拥有更强的底层 const 限制)

2.4.4 constexpr 和常量表达式

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

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

一个对象是不是常量表达式由其数据类型和初始值共同决定。

C++ 11 引入 constexpr 让编译器验证一个变量的值是否是常量表达式。

int f() { return 5; }
constexpr int a = 10;
constexpr int b = f();

这种情况编译器会给出错误

> error: call to non-'constexpr' function 'int f()'
    8 | constexpr int b = f();
      |                   ~^~

需要将 f 也增加 constexpr 则可通过编译。

constexpr int f() { return 5; }
constexpr int a = 10;
constexpr int b = f();
字面值类型

常量表达式的值需要在编译时就得到计算,因此对声明 constexpr 时用到的类型必须有所限制。因为这些值类型一般比较简单,包括算术类型、引用和指针,因此称为字面值类型 literal type

尽管指针和引用能定义成 constexpr 但初值受严格限制。一个 constexpr 的指针的初值必须是 nullptr、0 或者某个固定地址中的对象(函数体中定义的变量一般不是存放于固定地址,而所有定义在函数体外的变量地址固定不变)。

指针和 constexpr

constexpr 声明中定义的指针,限定符只与指针本身有效,与所指对象无关。

constexpr int i = 1;
int j = 0;
constexpr const int *pi = &i; // 指向常量的常量指针
constexpr int *pj = &j;       // 常量指针

2.5 处理类型

2.5.1 类型别名

类型别名 type alias

typedef double db;       // db 是 double 的别名
typedef double db, *pdb; // pdb 是 double* 的别名

别名声明 alias declaration

using db = double;
#include <iostream>
using std::cout;
using pi = int*;
int a = 10;
pi pa = &a;
int main() {
  cout << *pa << '\n';
  return 0;
}

指针、常量和类型别名

注意尽量不要用类型别名指代符合类型或者常量。

typedef char *pc; // 指向 char 的指针
const pc cstr = 0;// 指向 char 的常量指针,并非指向常量字符的指针!
                  // 不能在这里直接将别名展开为 const char *cstr
                  // 类型别名的部分需要视作一个整体
const pc *ps;     // 这里 ps 是一个指指针,其对象是指向 char 的常量指针。

2.5.2 auto 类型说明符

auto 可以通过变量的初始值推算变量的类型。

一条 auto 语句可以声明多个变量,但其基础数据类型必须一致。

auto i = 0, *p = &i;
// auto sz = 0, pi = 3.1415; // 类型不同,错误

auto 推断的值可能和初始值类型不同。

auto 一般会忽略顶层 const 而保留底层 const。

所以需要顶层 const 时需要

const auto a = b;

auto 引用会保留顶层 const

int a = 3;
const int ca = 5;
auto aa = a;
auto aca = ca;
auto &arca = ca;

const auto &cb = 5;
//auto &cc = 5; // cannot bind non-const lvalue reference 
                // of type 'int&' to an rvalue of type 'int'

int main()
{
    aca *= 5;
    // arca *= 5; // read-only reference

	return 0;
}

2.5.3 decltype 类型指示符

有时我们希望从表达式推断出要定义变量的类型,但又不想用表达式的值初始化变量此时可以用 decltype。

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

编译器并不调用 f 而是使用假如调用发生时的返回类型。

decltype 会保留顶层 const 和引用。

const int ca = 0, &cb = ca;
decltype(ca) x = 0; // x 是 const int
decltype(cb) y = x; // y 是 const int&
decltype 和引用
int i = 10, *p = &i, &r = i;
decltype(r + 0) b;  // b 是 int
decltype(*p) c = i; // 这里 c 是 int& 因此必须初始化

注意表达式内容是解引用操作时,decltype 会得到引用类型。

注意多加一层括号 decltype 可能会得到不同的结果。

int i = 10;
decltype(i) a; // int
decltype((i)) b = i; // int&

多加括号会将变量视作表达式。因为变量是一种可以作为左值的特殊表达式,因此会得到引用类型。

2.6 自定义数据结构

2.6.1 定义 Sales_data 类型

C++11 可以为数据成员提供类内初始值 in-class initializer

类内初始值要使用花括号或者等号初始化。

2.6.3

预处理器概述

预处理器 preprocessor 是在编译之前执行的程序,其中一项功能就是将 #include 标记指定的文件内容替换 #include

另一项功能是头文件保护符 header guard

A.h

#ifndef A_H
#define A_H

...
    
#endif // A_H
Prev: [模板] 随机数 Random
Next: 《C++ Primer》 拾遗 第 3 章 字符串、向量和数组