C++ Primer 总结索引 | 第二章:变量和基本类型

数据类型决定了程序中数据和操作的意义
i = i + j i,j都是整型数,就是普通加法运算,如果i和j是Sales_item类型(见上节)的数据,就把两个对象的成分相加

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位有效数字

基本的字符类型是char,一个char的空间确保可以存放机器基本字符集中任意字符对应的数字值,一个char的大小和一个机器字节一样
w_char_t用于确保可以存放机器最大拓展字符集中的任意一个字符,char16_t和char32_t为Unicode字符集服务
int至少跟和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大

2、以2的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为字节,存储的基本单元成为字,字由几个字节(32位就是4字节 、64位就是8字节 等)组成

3、之前表格列出的是最小值(c++标准),编译器实现更高精度,float以1个字(32比特,4字节)来表示,double以2个字来表示,long double以3或4个字来表示,float和double分别有7和16个有效位

4、带符号类型和无符号类型: 除去布尔型和拓展的字符型之外,其他整型划分为带符号的(表示正数,负数或0) 和 无符号的(表示大于等于0)两种。在int, short, long和long long这些类型名前加上unsigned就可以得到无符号类型

5、字符型被分为三种:char, signed char和unsigned char,类型char和类型signed char不一样,字符的表现形式只有两种:带符号的 和 不带符号的。类型char实际上会表现为上述两种类型的一种,由编译器决定

6、无符号类型中所有比特都用来存储值,8比特的unsigned char表示0 - 255(28-1)区间内的值;带符号类型在表示范围内正值和负值的量应该平衡,8比特的signed char理论上表示-127 - 127,实际表示范围-128 - 127

7、如何选择类型:
(1)当明确数值不可能为负,使用无符号类型
(2)int执行整数运算
(3)算数表达式不要使用char 或 bool,char在一些机器上有符号,另一些机器上无符号,当使用一个不大的整数时,明确指定类型时signed char 或者 unsigned char
(4)执行浮点运算选用double

1.2 类型转换

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;//假设char占8比特,c的值为255(补码)
signed char c2 = 256;//假设char占8比特,c2的值是未定义的

(1)非布尔型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true
(2)一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1
(3)浮点数赋给整数类型时,进行了近似处理,结果值将仅保留浮点数中小数点之前的部分
(4)整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能损失
(5)赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,把-1赋给他值就是255(对256取模后的余数),再比如把-32转为无符号int数,结果就是232-32(补码)
(6)赋给带符号类型一个超出它表示的范围的值时,结果是未定义的

3、避免无法预知和依赖于实现环境的行为
含有无法预知行为的程序也能正确执行,无法保证一个同样的程序在别的编译器下能正常工作,已经编译通过的代码再次执行也可能会出错

4、if (i) i的值为0,条件的值为false;i的所有其他取值(非0)都将使条件为true

5、含有无符号类型的表达式:当一个算术表达式中既有无符号数又有int值时,那个int值就会转换为无符号数,这个过程跟把int赋给无符号变量一样:

unsigned int u = 10;
int i = -42;
std::cout << u + i << std::endl;//如果int占32位,输出2^32-32(补码)

6、如果用无符号数重写循环for (int i = 10; i >= 0; i--),虽然循环内i >= 0,但意味着死循环,变量u永远不会小于0,循环条件永远成立for (unsigned u = 10; u >= 0; u--)

7、用while语句代替for语句,因为while可以在执行循环体之前先减去1:unsigned u = 11; while (u > 0)

8、综合5,6,7,切勿混用带符号类型和无符号类型

1.3 字面值常量

1、形如42就是字面值常量,字面值常量的形式和值决定了它的数据类型

2、整型和浮点型字面值:整型字面值可以写作十进制数、八进制数或十六进制数的形式。以0开头的整数表示八进制数,以0x或0X开头的表示十六进制数十进制:20 八进制:024 十六进制:0x14
具体数据类型:值+符号:默认情况,十进制字面值是带符号数(但是形如-42的负十进制字面值,负号不在字面值之内,负号仅仅是对字面值取负值),八进制和十六进制字面值既可能是带符号的,也可能是无符号的
十进制的字面值的类型是int、long和long long中能容纳数值的尺寸最小的那个,八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和unsigned long long中尺寸最小者,类型short没有对应的字面值

浮点型字面值表现为一个小数或以科学计数法表示的指数,指数用E或e标识:3.14159 3.14159E0 0. 0e0 .001 浮点型字面值是一个double

3、字符和字符型字面值:单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值字符字面值:'a' 字符串字面值:"Hello World! "
字符串字面值类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符 ‘\0’,字符串字面值的实际长度要比他的内容多1

字符串字面值可以分开来书写:

std::cout << "a good"
"boy" << std::endl;

4、转义序列:两类字符不能直接使用:一类是不可打印的字符,如退格或其他控制字符;另一类在C++中有特殊含义的字符(单引号、双引号、问号、反斜线)。需要用到转义序列,均以反斜线开始
换行符:\n; 横向制表符:\t; 报警符:\a; 纵向制表符:\v; 退格符:\b; 双引号:\"; 反斜线:\\; 问号:\?; 单引号:\'; 回车符:\r; 进纸符:\f
转义序列被当作一个字符使用:

std::cout << '\n';	//转到新一行
std::cout << "\tHi!\n";	//输出一个制表符,输出"Hi!",转到新一行

5、泛化转义序列:\x后跟1个或者多个十六进制数字,或\后紧跟1个、2个或3个八进制数字;数字部分表示的是字符对应的数值
响铃:\7; 换行符:\12; 空格:\40; 空字符:\0; 字符M:\115 或 \x4d(十六进制4d 就是 八进制115)

std::cout << "Hi \x4dO\115!\n";	//输出Hi MOM!,转到新一行

如果\后面跟着的八进制数字超过了3个,只有前3个数字与\构成转义序列;\x要用到后面跟着的所有数字
例如:"\x1234"表示一个16位字符,大多数机器的char型数据占8位,上面的例子可能会报错,需要添加下面要讲的前缀的拓展字符集一起使用

6、指定字面值的类型:添加如下表所列的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型

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

使用长整型字面值时,使用大写L标记,因为小写字母l和数字1容易混淆

指定字面值类型:
字符和字符串字面值

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

对于整型字面值来说,分别指定是否带符号 以及 占用多少空间
以U为后缀的十进制数、八进制数 或 十六进制数 都将从unsigned int、unsigned long和unsigned long long中选择能匹配空间最小的作为其数据类型
可以将U与L或LL合在一起使用(如:42ULL),以UL为后缀的字面值的数据类型将根据具体数值情况 或者取unsigned long,或者取unsigned long long

例1:
(a) ‘a’, L’a’, “a”, L"a"
(b) 10, 10u, 10L, 10uL, 012, 0xC
(c) 3.14, 3.14f, 3.14L
(d) 10, 10u, 10., 10e-2

(a)字符字面值,宽字符字面值,字符串字面值,宽字符串字面值;
(b)整形字面值,无符号整形字面值,长整形字面值,无符号长整形字面值,八进制整形字面值,十六进制整形字面值;
(c)浮点型字面值,单精度浮点型字面值,扩展精度浮点型字面值(long double);
(d)整形字面值,无符号整形字面值,浮点型字面值,浮点型字面值

例2:
int month = 09, day = 07;八进制整形,八进制总没有09
1024f 非法,整形字面值不可加后缀f

例3:

int main()
{
    int i = 0333;
    std::cout << i << '\n';//八转十进制
    std::cout << "Who goes with F\145rgus ? \012";
    //程序使其先输出 2,然后输出制表符,再输出 M,最后转到新一行
    std::cout << 2 << "\011\115\012";

    return 0;
}

输出:
219
Who goes with Fergus ?
2 M

7、布尔字面值和指针字面值
nullptr是指针字面值

2、变量

1、变量:具名的、可供程序操作的存储空间

2、每个变量都有 数据类型,数据类型决定 变量所占内存空间的大小和布局方式、该空间能存储值的范围、变量能参与的运算

3、c++中 变量 对象 一样

2.1 变量的定义

1、int sum = 0, value, units_sold = 0; sum、value和units_sold都是int,sum和units_sold初值为0

2、std::string book("0-201-78345-x"); 库类型std::string,在命名空间std中定义的,表示可变长字符序列的数据类型

3、对象是指一块能存储数据并具有某种类型的内存空间,并不严格区分是类还是内置类型,也不区分是否命名或是否只读

4、初始值:对象在创建时获得了一个特定的值,即这个对象被初始化了。一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了,用先定义的变量值 去初始化 后定义的其他变量
正确:price先被定义并赋值,随后被用于初始化discount double price = 109.99, discount = price * 0.16;
正确:调用函数applyDiscount,然后用函数的返回值初始化salePrice double salePrice = applyDiscount(price, discount);

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

6、列表初始化:初始化有好几种不同的形式:

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; //正确:转换执行,且确实丢失了部分值

7、默认初始化:定义变量时没有指定初始值。默认值是什么 由 变量类型 和 定义变量的位置 决定
内置类型的变量没被显式初始化,其值由定义的位置决定。定义于任何函数体之外的变量被初始化为0;定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,试图拷贝或以其他形式访问此类值将引发错误

每个类各自决定其初始化对象的方式,是否允许不经初始化就定义对象也由类自己决定。例如:string类规定如果没有指定初值则生成一个空串:std::string empty; //empty非显式地初始化为一个空串
一些类要求每个对象都显式的初始化,创建一个该类的对象而未对其做明确的初始化操作,将引发错误
例1:

std::cin >> int input_value; //非法,>>运算符后不能定义
int i = { 3.14 }; //非法,不能执行强制转换
double salary = wage = 9999.99; //非法,同一语句的初始化应该分别进行
int i = 3.14; //合法,已强制转换

例2:

std::string global_str;
int global_int;
int main()
{
    int local_int;
    std::string local_str;
}

global_str, local_str为空字符串;
global_int为0;
local_int未初始化,没有初始值。虽然测试下来也是0,但是最好初始化

8、未初始化变量引发运行时故障:未初始化的变量含有一个不确定的值,建议初始化每一个内置类型的变量

2.2 变量声明和定义的关系

1、C++支持分离式编译:将程序分割为若干个文件,每个文件可被独立编译
需要在文件中分享代码:例如,一个文件的代码可能需要使用另一个文件中定义的变量,如std::coutstd::cin定义于标准库,却能被我们写的程序使用

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

3、变量声明 规定了变量的类型和名字,这一点与定义相同,但除此之外,定义还申请存储空间,也可能为变量赋一个初始值
想声明一个变量并非定义它,在变量名前添加关键字extern,不要显式地初始化变量:

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

包含显式初始化的声明即成为定义,抵消了extern的作用:extern double pi = 3.1416; //定义
在函数体内部,初始化一个extern关键字标记的变量,将引发错误

4、变量只能被定义一次,但是可以被多次声明
变量的定义必须出现在 且 只能出现在 一个文件中,其他用到该变量的文件 必须对其进行声明,却绝 不能重复定义

5、C++是一种静态类型的语言。静态类型:编译阶段进行 类型检查。编译器负责检查数据类型是否支持要执行的运算

2.3 标识符

1、标识符由字母、数字 和 下划线组成,必须以字母 或 下划线开头,长度没有限制,对大小写字母敏感
C++保留名字 供语言本身使用,也为标准库保留了一些名字,这些名字不能用作标识符
用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头;定义在函数体外的标识符不能以下划线开头

2、变量命名规范:
(1)标识符体现实际含义
(2)变量名小写字母
(3)用户自定义的类名 一般以 大写字母 开头,如:Sales_item
(4)标识符由多个单词组成,单词间有明显区分,如student_loan 或 studentLoan
C++ Primer 总结索引 | 第二章:变量和基本类型_第1张图片
c++操作符替代名
例:int _;合法 int 1_or_2 = 1;非法,字母、下划线开头;double Double = 3.14;大小写敏感,合法

2.4 名字的作用域

1、同一个名字出现在程序的不同位置,也可能指向的是不同实体;作用域大多数以 花括号 分隔

2、同一个名字在不同作用域中可能指向不同的实体,名字的有效区域始于 名字的声明语句,以 声明语句所在作用域末端 结束

3、全局作用域 块作用域:

# include 
int main()
{
	int sum = 0;
	for(int val = 1; val <= 10; val++) {
		sum += val;
		std::cout << sum << std::endl;
	}
	return 0;
}

名字main定义于所有花括号外,有全局作用域,全局作用域内的名字在整个程序范围内都可以使用
名字sum定义于main函数限定的作用域内,从声明sum开始到main函数结束为止都可以访问,但出了main函数所在的块就无法访问了,变量sum具有块作用域
名字val定义于for语句内,在for语句之内可以访问val,但是在main函数的其他部分就不能访问它了

4、嵌套的作用域:被包含(或者说被嵌套)的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时允许在内层作用域中重新定义外层作用域已有的名字
例:

# include 
int reused = 42; //refused拥有全局作用域
int main()
{
	int unique = 0; //unique拥有块作用域
	// 输出#1:使用全局变量reused;输出 42 0
	std::cout << reused << " " << unique << std::endl;
	int reused = 0; // 新建局部变量reused,覆盖了全局变量reused
	// 输出#2:使用局部变量reused;输出 0 0
	std::cout << reused << " " << unique << std::endl;
	// 输出#3:显式地访问全局变量reused;输出 42 0
	std::cout << ::reused << " " << unique << std::endl;
	return 0;
}

输出#1:出现在局部变量reused定义之前,这条语句使用全局作用域中定义的名字reused
输出#2:发生在局部变量reused定义之后,此时局部变量reused正在作用域内,因此第二条输出语句使用的是局部变量reused而非全局变量
输出#3:使用 作用域操作符 来覆盖默认的作用域规则,因为全局作用域本身没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求 获取作用域操作符 右侧名字对应的变量

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

3、复合类型

1、复合类型:基于其他类型的类型。这里介绍两种:引用和指针

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

3、在之前的声明中,声明符就是变量名,此时变量的类型 也就是 声明的基本数据类型。可以有更复杂的声明符(&,*),它基于基本数据类型得到更复杂的类型(引用、指针),把它指定给变量

3.1 引用

1、C++11新增了一种引用:右值引用,这种引用主要用于内置类。严格来说,术语引用指的是 左值引用

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

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

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

4、引用即别名:引用并非对象,是为一个已经存在的对象起的另外一个名字

5、定义一个引用后,对其所有操作都是在与之绑定的对象上进行的:

refVal = 2; //把2赋给refVal指向的对象,此处即是赋给了ival
int ii = refVal; //与ii = ival执行结果一样

为引用赋值,实际上把值赋给了与引用绑定的对象,获取引用的值,实际上获取了与引用绑定的对象的值,以引用为初始值,实际上是与引用绑定的对象作为初始值:

//正确:refVal3绑定到了那个与refVal绑定的对象上,这里是绑定到ival上
int &refVal3 = refVal;
//利用与refVal绑定的对象的值初始化变量i
int i = refVal; //正确:i被初始化为ival的值

因为引用本身不是一个对象,所以不能定义引用的引用

6、引用的定义:允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:

int i = 1024, i2 = 2048; //i和i2都是int
int &r = i, r2 = i2; //r是一个引用,与i绑定在一起,r2是int
int i3 = 1024, &ri = i3; //i3是int,r1是一个引用,与i3绑定在一起
int &r3 = i3, &r4 = i2; //r3和r4都是引用

除了两种情况外,其他所有引用的类型都要和与之绑定的对象严格匹配。引用只能 绑定在对象上,而不能与字面值 或 某个表达式的计算结果绑定在一起

int &refVal4 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型的初始值必须是int类型的对象

7、对引用赋值 和 使用引用赋值 的例子:
例1:

int i = 0, &r1 = i; double d = 0, &r2 = d;
(a) r2 = 3.14159;
(b) r2 = r1;
(c) i = r2;
(d) r1 = d;

(a) (b) (c) (d) 均为合法,(a) 执行之后d的值也变为3.14159,(b) (c) (d) 都是隐式转换

例2:

int i, &ri = i;
i = 5;
ri = 10; 
cout << "i = " << i << "   ri = " << ri << endl;    

输出:i = 10, ri = 10,改变ri的值也会改变i的值

3.2 指针

1、指针是 指向另外一种类型的 复合类型

2、指针与引用:
与引用类似,实现了对其他对象的间接访问
不同点:
(1)指针本身就是一个对象,允许对指针 赋值和拷贝,而且在指针的生命周期内 它可以先后指向几个不同的对象
(2)指针无需在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

3、定义指针类型 声明符 写成*d的形式,其中d是变量名。如果在一条语句中定义了 几个指针变量,每个变量前面都必须有*

int *ip1, *ip2; // ip1 和 ip2都是指向int型对象的指针
double dp, *dp2; // dp2是指向double型对象的指针,dp是double型对象

4、获取对象的地址:指针存放某个对象的地址,要想获得那个对象的地址,需要对那个对象使用 取地址符(操作符&):

int ival = 42;
int *p = &ival; // 把p定义为一个指向int的指针,初始化p,p存放变量ival的地址(或者说p是指向变量ival的指针)

5、除了特定的两种情况外,其他所有指针类型都要和它所指向的对象严格匹配:

double dval;
double *pd = &dval; // 正确:初始值是double型对象的地址
double *pd2 = pd; // 正确:初始值是指向double对象的指针
// 初始化要写*(声明符),具体使用指针写变量名就行

int *pi = pd; // 错误:指针pi的类型 和 pd的类型不匹配
pi = &dval; // 错误:试图把double型对象的地址赋给int型指针

指针的类型实际上被用于指定它所指向对象的类型

6、指针值有以下四种:
(1)指向一个对象
(2)指向紧邻对象所占空间的下一个位置
(3)空指针,意味指针没有指向任何对象
(4)无效指针
试图拷贝 或 以其他方式 访问无效指针的值 都将引发错误。编译器并不负责检查此类错误,这一点和 试图使用未经初始化的变量 是一样的
尽管(2)(3)是有效的,但这些指针没有指向任何具体对象,所以试图访问此类指针(假定的)对象的行为是不被允许的

7、利用指针访问对象:如果指针指向了一个对象,则允许使用 解引用符(操作符*)来访问该对象(虽然叫解引用,但其实解的是 指针):

int ival = 42;
int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; // 由符号*得到指针p所指的对象,输出42

对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:

int ival = 42;
int* p = &ival;
*p = 0; // 由符号*得到指针p所指的对象,即可经由p为变量ival赋值
std::cout << ival; // 输出0

为*p赋值实际上是为p所指的对象赋值
如果用引用来实现同样的效果:

int ival = 42;
int &p = ival;
p = 0;
std::cout << ival; // 输出0

8、某些符号有多重含义:
像&和*这样的符号,既能用作表达式中的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的含义:

int i = 42; 
int &r = i; // &紧随类型名出现,因此是声明的一部分,r是一个引用
int *p; // *紧随类型名出现,因此是声明的一部分,p是一个指针
p = &i; // &出现在表达式中,是一个取地址符
*p = i; // *出现在表达式中,是一个解引用符
int &r2 = *p; // &是声明的一部分,*是一个解引用符

声明语句中,&和*用于组成复合类型;表达式中,它们的角色又转变成运算符。完全可以把它们当成不同的符号来看待

9、空指针:不指向任何对象,在使用一个指针之前 可以首先检查它是否为空
3种生成空指针的方法:
(1)int *p1 = nullptr; 用字面值nullptr来初始化指针
(2)int *p2 = 0; 将指针初始化为字面值0来生成空指针
(3)需要首先 #include cstdlibint *p3 = NULL; 用到预处理变量NULL来给指针赋值,这个变量在头文件cstdlib定义,它的值为0

10、预处理器是运行于编译过程之前的一段程序。预处理变量不属于命名空间std,它由预处理器负责管理,因此直接使用预处理变量,无需在前面加上std::
预处理器会自动将预处理变量替换为实际值,因此用NULL初始化指针 和 0初始化指针是一样的
在新标准下,最好使用nullptr,尽量避免使用NULL

11、把int变量直接赋给指针是错误的,即使int变量的值恰好等于0

int zero = 0;
int *pi = zero; // 错误:不能把int变量直接赋给指针

12、建议初始化所有指针:如果使用了未经初始化的指针,该指针所占内存空间的当前内容 将被看作一个地址值。如果指针所占内存空间中恰好有内容,而这些内容又被当成了某个地址,很难分清到底是合法 还是 非法了
因此,尽量等定义了对象之后再定义指向它的指针,实在不知道指向何处,把他初始化成nullptr或者0

13、赋值和指针:
指针和引用都能提供其他对象的间接访问,最大的不同是引用本身不是一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用就是访问它最初绑定的那个对象
指针和它存放的地址之间就没有这种限制。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象

14、对于赋值到底改变的是 指针的值 还是 指针所指对象的值,赋值永远改变的是等号左侧的对象:
指针的值被改变:int *pi = 0; pi = &ival;
指针所指对象ival的值被改变:*pi = 0;

15、指针作为条件出现:只要指针有合法的值,就能将它用在条件表达式中
假设 p 是一个 int 型指针:

if (p) // 指针是不是空指针
if (*p) // 指针所指的对象是不是0

16、指针参与比较运算:两个相同类型的合法指针,可以用相等操作符(==)或 不相等操作符(!=)来比较,结果是布尔类型
如果两个指针存放的地址值相等,则两个指针相等,有三种可能:
(1)都为空
(2)都指向同一个对象(一个指针指向某对象,另一个指针指向另外对象的下一个地址,也可能出现相等)
(3)指向了同一个对象的下一个地址

17、不管是作为条件出现 还是 参与比较运算,都必须是合法指针

18、void* 指针:可以存放任何对象。只能拿他和别的指针比较、作为函数输入输出、赋给另一个void指针。不能直接操作void指针所指的对象,不知道对象类型,无法确定在这个对象上做哪些操作
以void*的视角来看 内存空间 仅仅是内存空间,没办法访问内存空间中所存对象

#include 

int main() 
{
	int a = 0;
	int* i = &a;
	void* v = &a; //合法,因为void指针类型可以存放任意对象的地址 
	bool b = (i == v);
	std::cout << b << std::endl; //只要存放的对象是一个,不管是void*还是int*,依然是true的
	return 0;
}

19、例子
例1:
编写代码分别更改指针的值以及指针所指对象的值

#include 

int main()
{
	int i = 0, j = 1;
	int* z = &i;
	std::cout << z << " " << *z << std::endl; // 输出 00000024904FF534 0
	z = &j;
	std::cout << z << " " << *z << std::endl; // 输出 00000024904FF554 1 改变指针值
	*z = 5;
	std::cout << z << " " << *z << std::endl; // 输出 00000024904FF554 5 改变指针所指对象的值
	return 0;
}

例2:
说明指针和引用的主要区别(初始化+使用)
(1)引用在定义时必须初始化,而指针可不初始化
(2)引用在其生命周期内,只能指向一个对象,而指针可以先后指向不同的对象
(3)指针本身就是一个对象,允许对指针进行赋值和拷贝

例3:
给定指针p,能否知道它指向了一个合法的对象?
不行,如果你把指针理解为一个信封上的地址,那么没有任何手段能保证你填写的地址必然有人住

3.3 理解复合类型的声明

1、变量的定义包括一个基本数据类型 和 一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同类型的变量:

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

2、定义多个变量的两种写法:
(1)对于int* p,基本数据类型是int不是int*,*仅仅是修饰了p而已,是一个声明符,对该声明语句中的其他变量 不产生任何作用:

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

这种把修饰符 和 类型名 写在一起,每条语句只定义一个变量:

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

(2)把修饰符 和 变量标识符 写在一起 int *p1, *p2; // p1和p2都是指向int的指针

3、指向指针的指针:声明符中修饰符的个数没有限制,可以有多个修饰符连写在一起。指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址 再存放到 另一个指针当中
通过*的个数区分指针的级别。**表示指向指针的指针,***表示指向指针的指针

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

指向指针的指针
解引用int型指针会得到一个int型数(*pi),解引用指向指针的指针会得到一个指针(*ppi),要得到所指向的int需要(**ppi)

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

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引用的是一个int指针

4、const限定符

1、const限定符:定义一种值不能被改变的变量。例如,用变量来表示缓冲区的大小,使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易进行调整,另一方面,也应随时警惕防止程序改变这个值。const int bufSize = 512; // 输入缓冲区大小
const对象一旦创建后其值就不能再改变,所以const对象必须初始化

const int i = get_size(); // 正确:运行时初始化
const int j = 42; // 正确:编译时初始化
const int k; // 错误:k是一个未经初始化的常量

2、初始化和const:只能在const类型的对象上执行不改变其内容的操作
如果利用一个对象去初始化另外一个对象,则它们是不是const无关紧要:

int i = 42; 
const int ci = i; // 正确:i的值被拷贝给了ci
int j = ci; // 正确:ci的值被拷贝给了j 尽管ci是整型常量,但无论如何ci中的值还是一个整型数,ci的常量特征仅仅在执行改变ci操作时才会发生作用

当用ci去初始化j时,根本无需在意ci是不是一个常量,拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象 和 原来的对象没什么关系了

3、默认状态下,const对象仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件里分别定义了独立的变量。如何在一个文件中定义const,而在其他多个文件中声明并使用它?对于const变量不管是声明 还是 定义 都添加extern关键字(一般只在声明里添加),这样只需定义一次就可以了:

// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize; // 与file1_1.cc中定义的bufSize是同一个

4.1 const的引用

1、对常量的引用:引用绑定到const对象上。对常量的引用不能被用作修改它所绑定的对象:

const int ci = 1024;
const int &r1 = ci; // 正确:引用及对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象

不允许直接为ci赋值,当然也不能通过引用去改变ci,对r2的初始化是错误的

2、对const的引用:简称为常量引用,并不存在常量引用,因为引用不是一个对象,所以没办法让引用本身恒定不变。引用的对象是常量 还是 非常量 可以决定其所能参与的操作,却无论如何都不会影响到 引用 和 对象的绑定的关系 本身

3、初始化和对const的引用:引用的类型必须与其所引用对象的类型一致。第一种例外情况 就是在初始化常量引用时 允许用任意表达式作为初始值,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:

int i = 42;
const int &r1 = i; // 允许将const int&绑定到一个普通int对象上
const int &r2 = 42; // 正确:r1是一个常量引用
const int &r3 = r1 * 2; // 正确:r3是一个常量引用
int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用

4、当一个常量引用被绑定到另一种类型上到底发生什么:

double dval = 3.14;
const int &ri = dval;

ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数 而非 整数。为了确保让ri绑定一个整数,编译器把上述代码变成如下形式:

const int temp = dval; // 由双精度浮点数 生成 一个临时的整型常量
const int &ri = temp; // 让ri绑定这个临时量

ri绑定了一个临时量对象。临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时 临时创建的一个未命名的对象。把临时量对象简称为临时量

当ri不是常量时,就允许对ri赋值,这样就会改变ri所引用对象的值。此时绑定的对象是一个临时量 而非 dval。既然让ri引用dval,就肯定想通过ri改变dval的值。因此不会想着把引用绑定到临时量上,所以这种行为也被归为非法

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

int i = 42;
int &r1 = i; // 引用ri绑定对象i
const int &r2 = i; // r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; // r1并非常量,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用

r2绑定(非常量)整数i是合法的行为。不允许通过r2修改i的值。i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改

4.2 指针和const

1、与引用一样,也可以令指针指向 常量 或 非常量。类似于常量引用,指向常量的指针不能用于改变其所指对象的值。存放常量对象的地址,只能使用指向常量的指针:

const double pi = 3.14;
double *ptr = &pi; // 错误:ptr是一个普通指针
const double *cptr = &pi; // 正确:cptr可以指向一个 双精度常量
*cptr = 42; // 错误:不能给*cptr赋值

2、指针的类型 必须与所指的对象的类型一致,有两个例外。一个例外是允许 一个指向常量的指针 指向 一个非常量对象

double dval = 3.14; // dval是一个双精度浮点数
cptr = &dval; // 正确:但是不能通过cptr改变dval的值

和 常量引用一样,指向常量的指针 也没有规定 其所指的对象必须是一个常量
指向对象的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值 不能通过其他途径改变

3、const指针:允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,它的值(也就是存放在指针中的那个地址)就不能改变了。把*放在const关键词前面用以说明指针是一个常量,即不变的是 指针本身的值 而非 指向的那个值:

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

声明含义的确定 从右往左读。离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分决定。声明符中的下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针 指向的是一个int对象。同理,pip是一个 常量指针,它指向的对象是一个 双精度浮点型常量

4、指针本身是一个常量并不意味着 不能通过指针修改其所指的对象的值,能否这样做 完全依赖于所指对象的类型。3中的pip所指的对象值 还是pip自己存储的那个地址 都不能改变。curErr指向的是一个一般的非常量整数,可以用curErr去修改errNumb的值:

*pip = 2.72; // 错误:pip是一个指向常量的指针
// 如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr) {//可以作为判断的条件
	errorHandler(); 
	*curErr = 0; // 正确:把curErr所指对象的值重置
}

例1:

1int i = -1, &r = 0;      // 不合法, r为引用,初始化只能指向一个对象。2int *const p2 = &i2;     // 合法,定义一个int型的常量指针,初始化为i2的地址,之后指针的值不能再改变3const int i = -1, &r = 0;        // 合法, r为引用,const int &r = 0; 因为赋给常量int,右边也是常量,是合法的4const int* const p3 = &i2;       // 合法,p3的值不能改变,*p3也不能改变5const int* p1 = &i2;     // 合法,指针常量,p1指向的值不能被改变6const int& const r2;     // 不合法,引用不能是const

例2:

int i, *const cp;       // 不合法,定义const类型指针要初始化
const int *p;           // 合法,指向是常量,但指针的值可变。(没限制其本身所以合法)

例3:

int i, *const cp;
int *p1, *const p2;
const int ic, &r = ic;
const int *const p3;
const int *p;

p1 = p3; // 不能将const int* 类型的值分配到 int* 类型的实体
p1 = &ic; // 不能将const int* 类型的值分配到 int* 类型的实体
p3 = &ic; // 非法,p3是常量指针,不能再次赋值
p2 = p1; // 非法,p2是常量指针,不能再次赋值

4.3 顶层const

1、指针本身是不是常量 以及 指针所指的是不是一个常量 就是两个相互独立的问题。名词顶层const表示 指针本身是个常量,名词底层const表示 指针所指的对象是一个常量

2、顶层const可以表示任意的对象是 常量,这一点对任何数据类型都适用,如算术类型、类、类型等。底层const则与指针和引用等复合类型的基本类型部分有关

const int ci = 42; // 不能改变ci的值,这是一个 顶层const
const int *p2 = &ci; // 允许改变p2的值,这是一个 底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的是底层const
const int &r = ci; // 用于声明引用的const都是底层的const

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

int i = 0;
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 &r = ci; // 错误:普通的int&不能绑定到int常量上
const int &r2 = i; // 正确:const int&可以绑定到一个普通int上

例:

const int v2 = 0;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;

v2不能改变,是顶层const;;p2所指的对象不能改变,底层const;p3既是顶层,又是底层;r2底层

4.4 常量表达式 和 constexpr

1、常量表达式 是指 值不会改变 并且 在编译过程 就能得到计算结果的表达式
字面值属于常量表达式,用常量表达式 初始化的const对象 也是常量表达式,是不是常量表达式由 它的数据类型 和 初始值共同决定

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

staff_size的初始值是个字面值常量,它的数据类型只是一个普通int而非const int,所以它不属于常量表达式
尽管sz本身是一个常量,但它的具体值要等到运行时才能获取到,所以也不是常量表达式

2、constexpr变量:复杂系统中,很难分辨一个初始值到底是不是常量表达式。允许将变量声明为constexpr类型以便由编译器来验证变量的值是否为一个常量表达式

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

尽管不能使用普通函数作为constexpr变量的初始值,新标准允许定义一种特殊的constexpr函数,这种函数足够简单使得编译时就可以计算结果,这样就可以用constexpr函数去初始化constexpr变量了

3、认定变量是一个常量表达式,把它声明成constexpr类型

4、字面值类型:常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制,这些类型称为 字面值类型
算术类型、引用 和 指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr
尽管指针 和 引用都能定义成constexpr,他们的初始值受到严格限制,constexpr指针的初始值必须是nullptr或者0,或者 存储于某个固定地址中的对象

5、某个固定地址中的对象:函数体内定义的变量一般来说 不存放 在固定地址中,constexpr指针不能指向这样的变量;定义在所有函数体之外的对象地址固定不变,能用来初始化constexpr指针。同时,第六章会提到 允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量

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

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

关键在于constexpr把它所定义的对象置为 顶层const

int j = 0;
constexpr int i = 42; // i的类型是整型常量
// i和j都必须定义在 函数体之外
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j; // p1是常量指针,指向整数j

5、处理类型

1、程序中用到的类型复杂:
(1)一些类型难于拼写
(2)搞不清需要的类型是什么

5.1 类型别名

1、类型别名是一个名字,让复杂的类型名字简单明了、易于理解使用,有助于知道使用该类型的真实目的

2、两种方法定义类型别名:
(1)传统方法 使用关键字typedef:

typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词(p* = double -> p = double *)

关键字typedef作为声明语句中的基本数据类型的一部分出现,含有typedef的声明语句定义的不再是变量 而是 类型别名。这里的声明符也可以包含类型修饰,也能由基本数据类型构造出复合类型

(2)使用 别名声明 来定义类型的别名:

using SI = Sales_item; // SI是Sales_item的同义词

3、类型别名 和 类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:

wages hourly, weekly; // 等价于double hourly、weekly;
SI item; // 等价于Sales_item item

4、指针、常量和类型别名
如果某个类型别名指代的是复合类型 或 常量,用到声明语句中要小心

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

pstring实际上是指向char的指针,因此const string就是指向char的 常量指针(注意),而非指向常量字符的指针(与const char*不同,相当于char *const)

注意:
往往会错误地尝试把类型别名替换成它本来的样子,以理解语句const pstring cstr = 0;的含义:

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

这种理解是错误的。声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,*成为了声明符的一部分,const char成了基本数据类型。前者声明了一个指向char的 常量指针,改写后的形式则声明了一个指向const char的指针

5.2 auto类型说明符

1、当把表达式的值赋给变量,这要求在声明变量的时候清楚地知道表达式的类型。引入auto类型说明符,让编译器通过 初始值 来推算变量的类型。显然,auto定义的变量必须有初始值,auto item = val1 + val2; // item初始化为val1和val2相加的结果 如果val1和val2是类Sales_item的对象,则item的类型就是Sales_item

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

auto i = 0, *p = &i; // 正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一样

3、复合类型、常量和auto:编译器推断auto类型有时候和初始值的类型并不完全一样
(1)使用引用其实是使用引用的对象,当引用被用作初始值时,真正参与初始化的其实是引用对象的值。编译器以引用对象的类型作为auto的类型:

int i = 0, &r = i;
auto a = r; // a是一个int(看i)

(2)auto一般会忽略顶层const,同时底层const会保留下来
比如初始值是一个指向常量的指针时:

int i = 0;
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)
d = 42; // 非法
e = 42; // 非法

(3)希望推出的auto类型是一个顶层const,需要明确指出

const auto f = ci; // ci的推演类型是int,f是const int

(4)将引用的类型 设为auto,原来的初始化规则仍然适用,而且 设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留(与之前不同):

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

4、符号&和*只 从属 于某个声明符,而非基本数据类型的一部分。因此初始值必须是同一种数据类型:

auto &m = ci, *p = &ci; // m是对整型常量的引用(auto引用 保留常量属性),p是指向整型常量的指针(取地址 底层常量属性)
auto &n = i, *p2 = &ci; // 错误:i的类型是int,而&ci的类型是const int

例1:

const int i = 42;
auto j = i; const auto &k = i; auto *p = &i; 
const auto j2 = i, &k2 = i;

j int;k 常量int的引用(不加const一样);常量int的指针;j2 常量int;k2 常量int的引用

5.3 decltype类型指示符

1、当希望从表达式的类型 推断出要定义的变量 类型,不想用该表达式的值 初始化变量时,使用第二种类型说明符decltype,它的作用是 选择并返回操作数的数据类型(只得类型 不实际计算表达式的值)

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

编译器并不实际调用函数f,而是使用 当调用发生时 f的返回值类型 作为sum的类型

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

引用一般就是其所指对象的同义词,只有用在decltype处 一个例外

3、decltype和引用:decltype返回表达式结果对应的类型
(1)对于 decltype(r + 0),r是一个引用,但这个表达式的结果是一个具体值 而非 一个引用

(2)表达式是引用:

  • 1、如果表达式的内容是 解引用 操作,则decltype将得到引用的类型。解引用指针可以得到指针所指对象,而且 还能给这个对象赋值。因此,decltype(*p) 的结果类型是 int&,而非int
  • 2、同理,还有 给变量名加上一层括号(两层及以上了),编译器会将其作为一个表达式。变量是一种可以作为 赋值语句左值 的特殊表达式,这样decltype会得到 引用类型:
decltype((i)) d; // 错误:d是int&,必须初始化(表达式加括号就是引用)
decltype(i) e; // 正确:e是一个未初始化的int(变量名不加括号,得到的结果就是 该变量的类型)

*decltype((variable)) 注意是双层括号,结果永远是引用

int a = 3, b = 4;
decltype((b)) d = a;
++d;

a:int,4
d:int&,4

  • 3、赋值 是会产生引用 的一类经典表达式,引用的类型 就是左值的类型
    如果i是 int,表达式i = x的类型是 int&
int a = 3, b = 4;
decltype(a = b) d = a;

d:int &,3

6、自定义数据结构

1、数据结构:把一组相关的数据元素组织起来,然后使用它们的策略和方法
如:Sales_item类 把 书本的ISBN编号、售出量 及 销售收入 数据组织放在了一起,提供isbn函数、>>、<<、+、+=等运算在内的一系列操作
库类型string、istream、ostream等 也是以类的形式 定义的

6.1 使用struct定义 不带任何运算功能 Sales_data类型

1、Sales_data初步定义如下:

struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
}; //注意这个分号

类 以关键字 struct 开始,紧跟着类名 和 类体

2、类 内部定义的名字 必须唯一,但是可以与 类外部定义的 名字 重复

3、类体右侧的表示结束的花括号 必须写分号
因为 类体 后面可以紧跟 变量名 以示 对该类型对象的 定义
例:struct Sales_data { /* ... */ } accum, trans, *salesptr;
等价于(下面更好):

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

分号表示 声明符的 结束

4、类数据成员:每个对象有自己的一份 数据成员拷贝,修改 一个对象的数据成员,不会影响 其他Sales_data的对象

5、定义 类数据成员:和定义普通变量 一样:首先 说明一个基本类型,随后 紧跟一个或多个 声明符

6、类内初始值:创建对象时,类内初始值 将用于 初始化数据成员。没有初始值的成员 将被默认初始化
对 类内初始值 的限制 与 之前类似:或者放在 花括号内,或者放在 等号右边,不能使用 圆括号

6.2 使用Sales_data类

1、添加两个Sales_data对象:Sales_data data1, data2;

2、Sales_data 对象读入数据:

double price = 0; // 书的单价,用于计算销售收入

//读入第一笔交易:ISBN、销售数量、单价
std::cin >> data1.bookNo >> data1.units_sold >> price;

data1.revenue = data1.units_sold * price; // 计算销售收入

3、输出两个Sales_data对象的和:

if (data1.bookNo == data2.bookNo) {
	unsigned totalCnt = data1.units_sold + data2.units_sold;
	double totalRevenue = data1.revenue + data2.revenue;
	// 输出:ISBN、总销售量、总销售额、平均价格
	std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
	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; // 标示失败
}

6.3 编写头文件

1、在 不同文件中 使用同一个类,类的定义必须 保持一致,为此,类通常 被定义在头文件中,且 类所在的 头文件的名字 应该 与类的名字 一样
如:库类型string 在名为 string的头文件 中定义

2、头文件通常 包含那些 只能被定义一次 的实体,如类、const 和 constexpr变量

3、头文件使用到 其他头文件 的功能。如:Sales_data类包含一个string成员,所以Sales_data.h必须包含string.h头文件。同时使用Sales_data类的程序 为了能操作 bookNo成员需要 再一次包含string.h头文件
如:使用Sales_data类的程序 就先后两次包含了string.h头文件:一次是直接包含的,另有一次随着包含Sales_data.h被隐式地包含进来

4、预处理器:确保头文件多次包含 仍能安全工作。预处理器 是在编译之前执行 的一段程序。当看到# include标记时就会用指定的头文件内容 代替# include

5、预处理功能:头文件保护符,头文件保护符 依赖于 预处理变量
预处理变量两种状态:已定义 和 未定义。#define指令会把一个名字设定为 预处理变量。#ifdef当且仅当 变量已定义 时为 真;#ifndef当且仅当 变量未定义时 为真。一旦检查结果为真,则 执行后续操作 直至遇到#endif指令为止

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include 
struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif

第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器 将顺序执行 后面的操作 直至遇到#endif为止。预处理变量SALES_DATA_H的值变成 已定义;Sales_data.h也会 拷贝到程序中。后面如果再一次包含Sales_data.h,则#ifndef的检查结果 将为假,编译器将 忽略#ifndef#endif之间的地方

6、预处理变量 无视C++关于 作用域 的规则

7、整个程序中的 预处理变量 包括头文件保护符 必须唯一,通常 基于头文件中类的名字 来构建保护符的名字,以 确保其 唯一性。避免 与程序中的其他实体 发生名字冲突。一般把 预处理变量 的名字 全部大写

8、头文件即使目前还 没被包含在任何其他头文件中,也应该 设置保护符

9、对于上述所有功能的完整实现
Sales_data.h:

#ifndef SALES_DATA_H
#define SALES_DATA_H

#include 

struct sales_data {
	std::string bookNo;//string前面要加在std空间内
	double revenue = 0.0;
	unsigned units_sold = 0;
};
#endif

2.sales_data.cpp:

#include 
#include "Sales_data.h"
#include 

int main()
{
    sales_data data1, data2;
    double price = 0.0;
    std::cin >> data1.bookNo >> data1.units_sold >> price;
    data1.revenue = data1.units_sold * price;
    std::cin >> data2.bookNo >> data2.units_sold >> price;
    data2.revenue = data2.units_sold * price;
    if (data1.bookNo == data2.bookNo) {
        unsigned totalCnt = data1.units_sold + data2.units_sold;
        double totalRev = data1.revenue + data2.revenue;
        //输出:ISBN,总销售量,总销售额,平均价格
        std::cout << data1.bookNo << " " << totalCnt << " " << totalRev << " ";
        if (totalCnt != 0) {
            double aver = totalRev / totalCnt;
            std::cout << aver << 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;
    }
}

7、相关术语 P69

1、算数类型:布尔值、字符、整数、浮点数 等内置类型

2、绑定:令某个名字与给定的实体 关联在一起,使用该名字 就是 使用该实体

3、字节:内存中可寻址的最小单元,大多数机器的字节占8位

4、声明:声称存在一个变量、函数 或 别处定义的类型。名字必须在 定义或声明 之后才能使用

5、声明符:是声明的一部分,包括 被定义的名字 和 类型修饰符,其中类型修饰符 可以有 也可以没有

6、默认初始化:全局作用域的内置类型 对象初始化为0;局部作用域的对象 未被初始化 即拥有未定义的值

7、列表初始化:利用花括号 把一个或多个初始值 放在一起 初始化的形式

8、字面值:是一个不能改变的值,如 数字、字符、字符串等。单引号内的是 字符字面值,双引号内的是 字符串字面值

9、预处理器:在 C++编译过程中 执行的一段程序

10、预处理变量:由预处理器管理的变量。在程序编译之前,预处理器 负责 将程序中的预处理变量 替换成 它的真实值

11、struct是一个关键字,用于定义 类

12、字:在指定机器上 进行整数运算 的自然单位。一般来说,字的空间 足够存放地址。32位机器上的 字 通常占据4个字节

你可能感兴趣的:(C++,Primer,c++,开发语言)