C++ Primer - 变量和基本类型

时间:2022-09-09 18:44:14

基本内置类型

C++定义了 算术类型 (arithmetic type) 和 空类型 (void)。算术类型包括 字符整型数布尔值浮点数。空类型不对应具体的值,仅用于一些特殊的场合,如函数不返回任何值的时候。

算术类型

分为两类:整型(integral type,包括字符和布尔类型在内) 和 浮点型

类型 含义 最小尺寸
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位有效数字

char:可以存放机器基本字符集中任意字符对应的数字,即 大小和一个机器字节一样
wchar_t:可以存放机器最大扩张字符集中的任意一个字符
char16_tchar32_t:为 Unicode 字符集服务(Unicode是用于表示所有自然语言中的标准)。
C++语言规定,一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个 long long 至少和一个 long 一样大。long longC++ 11中新定义的。
C++标准指定了浮点数有效位数的最小值,大多数编译器都实现了更高的精度。float 以一个字表示(32 位),double2个字表示(64位),long double34 个字表示(64128位)。floatdouble 分别有 716 个有效位; long double 常用于有特殊浮点数要求的硬件。

带符号类型和无符号类型

intshortlonglong long可以划分为 带符号的无符号的(前面加unsigned),带符号的可以表示正数负数0,无符号的仅能表示大于等于0的值。
char 分了三种:charsigned charunsigned char。尽管分三种,但实际上表现形式只有两种:带符号(-128~127)无符号(0~255),由编译器决定使用哪一种。

类型转换

类型所能表示的值的范围决定了转换的过程:

  • 非布尔值的算术值->布尔类型:0->false,非0->true;
  • 布尔类型->非布尔类型:false->0,true->1;
  • 浮点数->整型:保留浮点数中小数点之前的部分;
  • 整型->浮点数:小数部分记为0,超过浮点数容量,精度会有所损失;
  • 赋给无符号类型一个超出其范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数;
  • 赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。
// 非布尔类型->布尔类型
int i = 42;
if (i)      // 条件为true
    i = 0;
// 含有无符号类型的表达式
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;    // 输出-84
// 当一个算术表达式既有无符号数又有int值时,int会被转化成无符号数
std::cout << u + i << std::endl;    // 如果int占32位,输出4294967264
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;  // 正确:输出32
// 当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值
std::cout << u2 - u1 << std::endl;  // 正确,不过是取模后的值
// 正确
for (int i=10; i>=0; i--)
    std::cout << i << std::endl;
// 错误:变量u永远也不会小于0,循环条件一直成立
for (unsigned u = 10; u >= 0; --u)
    std::cout << u << std::endl;

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

字面值常量

字面值(literal)

整型和浮点数字面值

0 开头表示八进制,0x开头表示十六进制。

// 20
20 /*十进制*/  024 /*八进制*/ 0x14 /*十六进制*/
整型字面值数据类型 是否带符号 决定因素
10进制 默认带符号 容纳下当前值的前提下,int、long、long long中尺寸最小者
8进制和16进制 可带可不带 容纳其数值,int、unsigned int、long、unsigned long、long long、unsigned long long中的最小者

浮点数字面值默认为double。

字符和字符串字面值

'a' // 字符字面值
"Hello World!"  // 字符串字面值

字符字面值 ‘a’ 就是表示一个单独的字符 a,字符串“a”代表一个字符的数组,包含字符 a 和一个空字符 ‘\0‘。

转义序列

转义字符 含义
\n 换行符
\t 横向制表符
\a 报警符
\v 纵向制表符
\b 退格符
\” 双引号
\\ 反斜杠
\? 问号
\’ 单引号
\r 回车符
\f 进纸符

泛化的转义序列:\x+16进制数字,+8进制数字。

\7 (响铃)     \12 (换行符)       \40 (空格)
\0 (空字符)        \115 (字符M)      \x4d (字符M)

指定字面值的类型

前缀 含义 类型
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

变量

变量定义

类型说明符(type specifier) + 一个或多个变量名组成的列表,变量名之间以逗号隔开,最后以分号结束。

// sum、value和units_sold都是int
int sum = 0, value, units_sold = 0;
// item 是 Sales_item类型
Sales_item item;
// string 是一种库类型,表示一个可变长的字符序列
// book通过一个string字面值初始化
std::string book("0-201-78345-X");

初始化: 对象在创建时获得了一个特定的值,我们说这个对象被初始化了。

// price先被定义并赋值,随后被用于初始化discount
double price = 109.99, discount = price * 0.16;
// 调用函数 applyDiscount,然后用函数的返回值初始化 salePrice
double salePrice = applyDiscount(price, discount);

在C++中,初始化和赋值是两个完全不同的操作。初始化是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
列表初始化:

int units_sold = 0;
int units_sold = { 0 };
int units_sold{0};
int units_sold(0); 

如果使用列表初始化存在丢失信息的风险,编译器会报错:

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

默认初始化: 内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。一种例外情况是,定义在函数体内部的内置类型变量将不被初始化。不被初始化的内置类型变量的值是未定义的,试图拷贝或者以其他形式访问此类值将引发错误。类的对象如果没有显式地初始化,则其值由类决定。

变量声明和定义的关系

声明使得名字为程序所知,定义负责创建与名字关联的实体。

// 声明i而非定义i
extern int i;
// 声明并定义j
int j;

任何包含显式化的声明都是定义。

// 定义
extern double pi = 3.1416;

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

标识符

C++标识符(identifier)由字母数字下划线 组成,必须以字母或下划线开头。长度没有限制,对大小写敏感。

// 定义4个不同的int变量
int somename, someName, SomeName, SOMENAME;
  • 关键字不能作为变量名;
  • 不能连续出现两个下划线;
  • 不能下划线后连大写字母;
  • 函数体外的标识符不能以下划线开头。

此外,还有一些规范:

  • 要能体现实际含义;
  • 变量名一般用小写字母;
  • 自定义类大写开头;
  • 标识符由多个单词组成,单词间要有明显的区分。

对于命名规范来说,若能坚持,必将有效。

名字的作用域

全局作用域 ( global scope ):整个程序范围都可使用。
块作用域 ( block scope ):花括号体内可以使用。

复合类型

引用

引用( reference ):为对象另外起了一个名字。

int ival = 1024;
int &refVal = ival;
// 报错:引用必须初始化
int &refVal2;

引用非对象,只是为一个已经存在的对象所起的另外一个名字。不能定义引用的引用。

// 把2赋给refVal指向的对象,此处即是赋给了ival
refVal = 2;
// 与ii = ival执行结果一样
int ii = refVal;
  • 一般引用类型和与之绑定的对象严格匹配;
  • 只能绑定对象,不能绑定字面值。
// 错误:引用类型的初始值必须是一个对象
int &refVal4 = 10;

指针

“指向( point to )”另外一种类型的复合类型。与引用相比:
相同点:都实现了对其他对象的间接访问。
不同点:

  • 指针是对象,允许赋值和拷贝,在生命周期内科先后指向几个不同的对象;
  • 无须初始化,如果不初始化,会拥有一个不确定的值。

获取对象的地址
使用取址符( 操作符 &)获取对象地址。

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

一般指针类型要和它所指向的对象严格匹配,但有例外。
指针值应属于下列4中状态之一:

  • 指向一个对象;
  • 指向紧邻对象所占空间的下一个位置;
  • 空指针,意味着指针没有指向任何对象;
  • 无效指针,也就是上述情况之外的其他值。

访问无效指针会引发错误,编译器不负责检查此类错误。第2和第3种情况,指针的使用也受到限制,由于指针没有指向任何对象,所有试图访问此类指针对象的的行为也不被允许。

利用指针访问对象: 解引用符( 操作符 * )
空指针 (null pointer): 使用指针之前,可以先检查其是否为空。

赋值和指针:

int i = 42;
// pi被初始化,但没有指向任何对象
int *pi = 0;
// pi2被初始化,存有i的地址
int *pi2 = &i;
// 如果pi3定义于块内,则pi3的值无法确定
int *pi3;

// pi3 和 pi2指向同一个对象 i
pi3 = pi2;
// 现在pi2不指向任何对象了
pi2 = 0;

赋值改变的是等号左侧的对象。

// pi的值被改变,现在pi指向了ival
pi = &ival;
// ival的值被改变,指针pi并没有改变
*pi = 0;

其他指针操作:

int ival = 1024;
int *pi = 0;
int *pi2 = &ival;
// pi的值是0,因此条件为false
if (pi)
    // ...
// pi2指向ival,因此它的值不是0, 条件是true
if (pi2)
    // ...

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

double obj = 3.14, *pd = &obj;
// 正确,void* 能存放任意类型对象的地址
void* pv = &obj;
// pv可以存放任意类型的指针
pv = pd;

理解复合类型的声明

定义多个变量:
修饰符和类型写在一起

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

修饰符和变量标识符写在一起

// p1和p2都是指向int的指针
int *p1, *p2;

选择并坚持一种写法,没有对错之分。

指向指针的指针 :通过 * 可以区分指针的级别。

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

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

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

要理解 r 的类型,需要从由向左阅读 r 的定义。离变量名最近的符号对变量类型有直接的影响。

const限定符

初始化和 const :利用对象去初始化 const修饰的对象时,该对象是不是有 const 修饰都无关紧要。

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

const的引用

即对常量的引用,不能修改引用所绑定的对象。

const int ci = 1024;
const int &ri = ci;
ri = 42;            // 错误,r1是对常量的引用
int &r2 = ci;       // 错误,视图让一个非常量引用指向一个常量对象

初始化和对 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 &ri = dval;

实际上是创建了一个临时量

const int temp = dval;
const int &ri = temp;

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

int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
r2 = 0;             // 错误

指针和const

指向常量的指针( pointer to const ) 不能用于改变其所指对象的值。

const double pi = 3.14;     // pi是个常量,它的值不能改变
double *ptr = &pi;          // 错误
const double *cptr = &pi;   // 正确
*cptr = 42;                 // 错误

Tips: 所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const 指针:指针是对象,因此也可以定义为常量,定义之后不能改变其指向。

int errNumb = 0;
int *const curErr = &errNumb;
const double pi = 3.14159;
const double *const pip = &pi;

顶层const

顶层 const表示指针本身是一个常量对象,底层 const 表示指针所指对象是一个常量。

int i = 0;
int *const p1 = &i;
const int ci = 42;
const int *p2 = &ci;
const int *const p3 = p2;
const int &r = ci;

指向拷贝操作时,顶层 const 不受影响, 底层 const 的限制不能忽视。

constexpr和常量表达式

常量表达式是指值不会改变,在编译过程就能得到结果的表达式。
声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size();      // 只有当size()是一个constexpr语句时才正确

constexpr 修饰指针时,仅对指针有效,与其所指对象无关。

处理类型 #

类型别名

两种方法:typedefusing
typedef

typedef double wages;
typedef wages base, *p;

别名声明:

using SI = Sales_Item;

指针、常量和类型别名

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

auto 类型说明符

引入了 auto 说明符,能让编译器去分析表达式所属的类型。

// 正确,i是整数,p是整型指针
auto i=0, *p = &i;
// 错误,sz 和 pi的类型不一致
auto sz = 0, pi = 3.14
  • auto 会忽略顶层 const,同时底层 const 会保留下来。

decltype 类型指示符

选择并返回操作数的类型。编译器分析表达式并得到他的类型,却不实际计算表达式的值。

// 编译器不实际调用函数f
decltype(f()) sum = x;

处理顶层 const 和引用方式与 auto 不同。

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

decltype 和引用

// 引用,必须初始化
decltype(*p) c;
// 引用,必须初始化
decltype((i)) d;

自定义数据结构

定义 Sales_data 类型

struct Sales_data {
    std::string bookNo;
    // C++11新标准,类内初始化
    unsigned units_sold = 0;
    // C++11新标准,类内初始化
    double revenue = 0.0;
};

定义变量的方式:

struct Sales_data { /* ... */} accum, trans, *salesptr;

或者

struct Sales_data  { /* ... */ };
Sales_data accum, trans, *salesptr;

使用Sales_data 类

编写自己的头文件

头文件保护符 ( header guard )

#ifndef xxx
#define xxx
/* ... */
#endif

小结

  • 基础内置类型
  • 常量和非常量
  • 复合类型
  • auto 和 decltype