第1章 开始
本章以一个实际问题,书店问题,来简单的介绍C的基本特性。这个问题的代码将贯彻整本书,后面的章节会逐一讲解代码中涉及到的 C 语言特性。
这个问题具体,就是要保存书店的所有销售记录的档案,每条记录保存了某本书的一次销售信息,包含三个数据:
0-201-70353-X 4 24.99
它们分别是书的 ISBN,售出的册数,书的单价。有时,书店老板需要查询此档案,计算每本书的销售量、销售额及平均售价。
这个问题会涉及到的有:
- 定义变量
- 进行输入和输出
- 使用数据结构保存数据
- 检测两条数据是否有相同的 ISBN
- 包含一个循环来处理销售档案中的每条记录
我们首先介绍如何使用 C++ 来解决这些子问题,然后编写书店程序。
1.1 一个简单的C++程序
int main() { return 0; }
由这个程序简单的介绍函数的定义。
函数定义包含四部分:
- 返回类型
- 函数名
- 一个括号包围的形参列表 (允许为空)
- 函数体
main
是一个比较特殊的函数,但其定义与其他函数是一样的。
1.1.1 编译、运行程序
上图是一个 C 程序(C++ 程序类似)的编译过程,可供参考。
1.2 初识输入和输出
在C++语言中并未定义任何输入输出(IO)语句,但提供了全面的标准库来提供 IO 机制,即iostream库。
iostream 库包含两个基础类型 istream 和 ostream ,分别表示输入流和输出流。
(当然,我们也可可以使用 C 语言中的 printf ,C ++是兼容 C 的,但是并不建议,所以本书中的示例都是使用标准库中提供的 iostream 库。)
标准输入输出对象
标准库定义了4个 IO 对象:
- 输入:istream 类型的对象中的 cin
- 输出:ostream 类型的对象中的 cout、cerr 、clog 。
几个符号介绍:
- << 输出运算符
- >> 输入运算符
- endl 为操纵符,效果是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中。
- :: 作用域运算符
示例:
#include <iostream>
int main()
{
std::cout << "Enter two numbers:" << std::endl;
int v1 = 0, v2 = 0;
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << std::endl;
return 0;
}
1.3 注释
c++的两种注释:
// 单行注释1
std::cout << "hello" << std::endl;
/*
* 多行注释2
*/
std::cin >> val;
1.4. 控制流
循环和判断语句
4.1 while
示例: 求 1 到 10 的和
#include <iostream>
int main()
{
int sum = 0, val = 0;
while (val <= 10)
{
sum += val;
++val;
}
std::cout << "Sum of 1 to 10 inclusive is "
<< sum << std::endl;
return 0;
}
4.2 for
示例:for
循环, 将上面 while
循环改成 for
#include <iostream>
int main()
{
int sum = 0;
for (int val = 1; val <= 10; ++val)
sum += val;
std::cout << "Sum of 1 to 10 inclusive is "
<< sum << std::endl;
return 0;
}
4.3 读取数量不定的输入数据
#include <iostream>
int main()
{
int sum = 0, val = 0;
while (std::cin >> val)
sum += val;
std::cout << "Sum is " << sum << std::endl;
return 0;
}
上面程序在输入结束后除了按回车键,还需要按结束符
结束符:
- 在Windows 系统中输入结束符是按
Ctrl + Z
- 在 UNIX系统中,包括 Mac OS X 系统中,文件结束符输入用
Ctrl + D
4.3 if
示例:读取一串数据并统计每个数字出现的次数 ,windows下的终止键 ctrl
+ z
#include <iostream>
int main()
{
// currVal是我们正在统计的数,我们将读入的新值存入val
int currVal = 0, val = 0;
// 读取第一个数,并确保确实有数据可以处理
if(std::cin >> currVal) {
int cnt = 1;
while(std::cin >> val) {
if(val == currVal) {
++cnt;
}
else {
std::cout << currVal << " occurs "
<< cnt << " times " << std::endl;
currVal = val;
cnt = 1;
}
}
std::cout << currVal << " occurs "
<< cnt << " times " << std::endl;
}
return 0;
}
1.5 类的简介
在C++中,我们通过定义类 来定义自己的数据结构。
一个类定义了一个类型(类类型),以及与其关联的一组操作。
本节中我们使用一个类,需要了解三件事:
●类名是什么?
●它是在哪里定义的?
●它支持什么操作?
对于书店程序来说,我们假定类名为 Sales_item,头文件 Sales_item.h 中已经定义了这个类。
1.5.1 Sales_item 类
Sales_item 类的作用是表示一本书的销售总额、售出册数和评价售价。我们现在不需要关心这些
数据是如何存储、如何计算,我们只需要直了解怎么使用它。
我们可以把一个类当作是一种类型(类类型),就和C++语言内置的类型( int float 等)一样,其
用法也是和内置类型一致的。
定义类类型的变量
Sales_item item;
上面语句表达的是定义了一个变量 (对象)item,它的类型是 Sales_item 类型。
除了可以定义 Sales_item 类型变量之外,我们还可以:
- 调用一个名为 isbn 的函数从一个 Sales_item 对象中提取 ISBN 书号
- 用输入运算符(>>)和输出运算符 (<<) 读、写 Sales_item 类型的对象
- 用赋值运算符 (=)将一个 Sales_item 对象的值赋予另一个 Sales_item 对象
- 用加法运算符 (+) 将两个 Sales_item 对象相加。两个对象必须表示同一本书(相同的 ISBN)。加法的结果是一个新的 Sales_item 对象,其 ISBN 与两个运算符对象相同,而其总销售额和售出册数是两个运算对象的对应值之和
- 使用复合运算符 (+=) 将一个 Sales_item 对象加到另一个对象上。
读写 Sales_item
把 Sales_item 当做是一个普通的变量(对象),它的操作和内置类型是一样的
如果输入:
0-201-70353-X 4 24.99
则输出为:
0-201-70353-X 4 99.96 24.99
99.96 表示的是以每本22.49的价格售出 4 本的总销售额。
Sales_item 对象的加法
像操作两个int 变量一样,将两个 Sales_item 对象相加
输入:
#include <iostream>
#include "../Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2; // 读取一对交易记录
std::cout << item1 + item2 << std::endl; // 打印它们的和
return 0;
}
输出:
0-201-78345-X 3 20.00
0-201-78345-X 2 25.00
1.5.2 初识成员函数
将两个 Sales_item 对象相加的程序,首先应该检查两个对象是否具有相同的 ISBN,Sales_item 提供了获取 ISBN 的方法(成员函数)
#include <iostream>
#include "../Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2;
if(item1.isbn() == item2.isbn()) {
std::cout << item1 + item2 << std::endl;
return 0;
} else {
std::cerr << "Data must refer to same ISBN"
<< std::endl;
return -1;
}
}
什么是成员函数?
item1.isbn() == item2.isbn()
成员函数是定义为类的一部分的函数,有时也称为方法。
我们通常是使用一个类对象来调用成员函数,如下代码,使用点运算符 . 来调用对应的成员
iterm1.isbn();
1.6 书店程序
#include <iostream>
#include "../Sales_item.h"
int main()
{
Sales_item total; // 保存下一条交易记录的变量
// 读入第一条交易记录,并确保有数据可以处理
if (std::cin >> total) {
Sales_item trans; // 保存和变量
// 读入并处理剩余交易记录
while (std::cin >> trans){
// 如果我们仍在处理相同的书
if (total.isbn() == trans.isbn())
total += trans; // 更新总销售额
else {
// 打印前一本的结果
std::cout << total << std::endl;
total = trans; // total 现在表示下一步书的销售额
}
}
std::cout << total << std::endl; // 打印最后一本书的结果
}
else{
// 没有输入!警告读者
std::cerr << "No data?!" << std::endl;
return -1; // 表示失败
}
return 0;
}
第1部分 c++基础
第2章
2.1 基本内置类型
c++基本数据类型包含两类:
- 算术类型(arithmetic type)——字符、整型、布尔型、浮点型
- 空类型(void)——不对应具体的值,仅用于一些特殊场合。例如函数不返回值。
2.1.1 算术类型
算术类型包含两类:
- 整型——包括字符、布尔型在内
- 浮点型
算术类型的尺寸:该类型数据所占的比特数。
尺寸在不同机器上是不同的。
某一类型所占的尺寸不同,所能表示的数据范围也不一样。
下图列出了c++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。
带符号类型:
可以表示正数、负数、0
无符号类型:
仅能表示正数、0
特别注意:
类型char和类型signed char并不一样。尽管字符型有char 、signed char、unsigned char,但是字符的表现形式却只有两种:带符号的和无符号的。char类型实际上会表现为上述两种形式中的一种,具体是哪种由编译器来决定。因此在使用char时要尤其注意。
建议:如何选择类型
宗旨:做出限定从而简化选择过程。
- 当明确数值不可能为负时,选择无符号类型;
- 使用int执行整数运算;-----在实际应用中,short常常显得太小,而long一般和int有一样的尺寸。如果你的数值超过了int范围,就选用long long。
- 在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char的尺寸与机器相关,在一些机器上是有符号的,而在另一台即使上有是无符号的,使用char进行运算特别容易出问题。如果需要使用一个不大的整数,那么明确指定它的类型是signed char或unsigned char。
- 执行浮点数运算选用double。因为float通常精度不够,而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double 提供的精度在一般情况下是没有必要的,况且它带来的运算消耗也不容忽视。
2.1.2 类型转换
定义:
将对象从一种给定的类型转换为另一种相关类型。
当在程序中使用一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。
bool b = 42; // b 为真
int i = b; // i 的值为3
i = 3.14; // i 的值为3
double pi = i; // pi 的值为 3.0
unsigned char c = -1; // 假设 char 占8bit,c 的值为255? 书上是这个结果,但我运行打印没有值
signed char c2 = 256; // 假设 char 占8bit, c2 的值是为定义的 我运行打印没有值
含有无符号类型的表达式
- 切勿混用有符号和无符号类型。会造成错误,有符号类型数会转换成无符号数。示例1
- 无符号数中减去一个数时,不管这个数是不是无符号数,都必须保证结果不是负值。示例2
- 注意无符号数用在for循环,可能造成死循环。因为无符号数不会小于0。可以用while来替代for。示例3
示例1:
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出 -84
std::cout << u + i << std::endl; // 如果 int 占 32bit,输出 4294967264
示例2:
unsigned u1 = 42, u2 = 10;
cout << u1 - u2 << endl;// 正确,输出32
cout << u2 - u1 << endl;// 正确,但输出 4294967264 是取模后的值
示例3:
// 错误 变量u永远也不会小于0,循环条件一直成立
/*for (unsigned u = 10; u >= 0; u--) {
cout << u << endl;
}*/
unsigned u = 11;
while (u > 0) {
--u;// 先减1,这样最后一次迭代就会输出0,也不会死循环,不会有期望u小于0的情况
cout << u << endl;
}
2.1.3 字面常量
一个形如 42 的值被称作字面值常量, 每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型字面值:
- 八进制:0开头
- 十六进制:0x或0X开头
- 十进制
20 // 十进制
024 // 八进制
0x14 // 十六进制
整型字面值具体的数据类型由它的值和符号决定。默认情况下十进制字面值是带符号数,八进制和十六进制字面值可能是带符号或无符号的。字面值的类型是其对应类型中尺寸最小的那个。
浮点型字面值:
- 默认字面值是一个double。
- 可以用后缀来表示其他浮点型。
字符字面值:
- 单引号括起来构成
'a' // 字符字面值
字符串字面值:
- 双引号括起来构成
- 实际上由常量字符构成的数组
- 编译器在每个字符串结尾处添加一个空字符('\0')
"Hello World!" // 字符串字面值
转义序列:
需要用到转义序列的有两类场景:
- 不可打印的字符。退格、其他控制字符等
- c++语言中特殊含义的字符。单引号、双引号、问号、反斜线。
指定字面值的类型
我们可以通过下表的符号来指定字面值的类型
示例:
L'a' // 宽字符型字面值,类型是 wchar_t
u8"hi!" // utf-8 字符串字面值
42ULL // 无符号整型字面值,类型是 unsigned long long
1E-3F // 单精度浮点型字面值,类型是 float
3.14159L // 扩展精度浮点类型字面值,类型是 long double
布尔字面值:
true 和 false
指针字面值:
nullptr
2.2 变量
变量提供一个具名的、可供程序操作的存储空间。
2.2.1 变量定义
变量定义语法:
类型说明符 变量名1 = 初值, 变量名2, ……;
初始化含义:
当对象在创建时获得了一个特定的值,就是对象被初始化了。
赋值的含义:
把对象的当前值擦除,而以一个新值来替代。
注意:
初始化和赋值是两个完全不同的操作。不可混淆。原因看两者的定义可知晓。
示例1:变量定义
int sum =0, value, // sum、value、units_sold 都是int
units_sold = 0; // sum 、 units_sold 初值都是0
Sales_item item; // item 的类型是 Sales_item
string book("0-201-78345-X"); // book通过string字面值初始化
示例2:初始化和赋值是两个完全不同的操作
// 定义及初始化
// 正确 a先被定义并被赋初值,然后a被用于初始化b
int a = 2, b = a * 3;
// 赋值,覆盖原来的旧值,用新值来替代旧值
b = 20;
列表初始化
c++语言定义了初始化的的好几种形式:
int units_sold n = 0;
int units_sold n = {0}; // 列表初始化
int units_sold n{0}; // 列表初始化
int units_sold n(0);
用花括号来初始化,是c++11标准的一部分,称为 列表初始化。
列表初始化的重要特点:
若初始值存在丢失信息的风险时,编译器会报错。
long double ld = 3.1415926;
int a{ ld }, b = { ld };// 编译报错,转换未执行,因为存在丢失信息的风险
int c(ld), d = ld;// 正确,转换执行,且会丢失部分值。
默认初始化
定义变量时没有初始化变量的值,则变量会被默认初始化。默认初始化的值取决于变量定义的类型及定义变量的位置。
注意:
定义在函数体内的局部变量和类中的成员属性是不会被初始化的 (不同编译器的实现可能会不同), 所以不用试图使用任何方式去访问这些变量。
建议:
初始化每一个内置类型的变量,防止出错。
示例:
#include <iostream>
using namespace std;
int init;
string str;
struct A {
int m;
string m_str;
void print()
{
// 编译通过,但 打印的值很奇怪,不建议未初始化直接使用
cout<<"m: " << m << endl;
// 正确,打印空串
cout <<"m_str:"<< m_str << endl;
}
};
void test()
{
int un_init;
// 编译报错
//cout<<"un_init:" << un_init << endl;
}
void test01()
{
A a;
a.print();
}
int main()
{
test();
test01();
// 正确,初始化为0
cout << init << endl;
// 正确,初始化为空串
cout << str << endl;
system("pause");
return 0;
}
2.2.2 变量声明和定义的关系
区分声明和定义的原因:
为了支持分离式编译。允许程序分割为若干个文件,每个文件可被独立编译。
声明:
使程序知道名字。
定义:
创建与名字关联的实体。
声明与定义的相同点:
规定了变量的类型和名字。
声明与定义的区别:
- 定义申请了存储空间,也可能会赋初始化值。
- 变量能且只能被定义一次,但是可以被声明多次。
示例:
#include <iostream>
#include <string>
using namespace std;
extern int i; // 声明
extern int i; // 声明可多次
extern int j = 1; // 声明并定义
int x; // 声明并定义
//int x; // 报错,只能定义一次
void test() {
extern int c;// 声明
extern int c;// 声明多次
// 直接报错 不允许对外部变量的局部申明
//extern int a = 1;
}
int main() {
// 编译报错 无法解析的外部命令
// 只声明未定义
//cout << i << endl;
//编译报错 在函数内部,不能初始化一个由extern声明的变量,
//i = 0;
// 打印1
cout << j << endl;
// 打印 0
cout << x << endl;
test();
system("pause");
return 0;
}
2.2.3 标识符
标识符:通俗来讲,就是变量名。
书写标准:
- 由字母、数字和下划线组成,且必须以字母或下划线开头。
- 不能使用c++保留关键字
- 不能连续出现两个下划线
- 不能以下划线紧连大写字母开头
- 定义在函数体外的标识符不能以下划线开头。
建议命名规范:
- 普通的局部变量和函数参数名使用小驼峰(第一个单词首字母小写,其他单词首字母大写), 例:
userName
- 全局变量前加
g_
, 后面的按小驼峰规则 ,g_userName
- 静态变量前加
s_
, 后面按小驼峰规则,s_userName
- 类名使用大驼峰,所有单词的首字母大写 ,
UserManage
- 类属性(成员变量)前面加
m_
,后面按小驼峰规则 ,m_userName
- 常量全部使用大写,多个单词用
_
分割,MAX_NUMBER
示例 标识符书写:
#include <iostream>
#include <string>
using namespace std;
int _name1=1;
int main() {
// 符合规范的标识符
int name2_ = 2;
int _name3 = 3;
int __name4 = 4;
int _Name5 = 5;
cout << "_name1 = " << _name1 << endl;
cout << "name2_ = " << name2_ << endl;
cout << "_name3 = " << _name3 << endl;
cout << "__name4 = " << __name4 << endl;
cout << "_Name5 = " << _Name5 << endl;
system("pause");
return 0;
}
2.2.4 名字的作用域
作用域:
c++中多数作用域都以花括号分隔。
注意:
- 函数内部可以重新定义全局变量,但实际使用时,局部变量不宜与全局的变量重名
- 嵌套的块,允许在内层作用域中重新定义外层作用域已有的名字。但实际使用时,内部的最好不要和外部的重名。
- 万一变量重名,变量取值会采取就近原则。
2.3 复合类型
复合类型:基于其他类型定义的类型。
2.3.1 引用
引用:就是为变量起一个别名。
语法:
数据类型 a = 初始值;
数据类型 &b = a;
特点:
- 对引用进行的所有操作,都是在与之绑定的对象上进行的。
- 为引用赋值,实际上是把赋值给了与引用绑定的对象。
- 可以在一条语句中定义多个引用。必须以&开头。
注意:
- 引用必须初始化
- 引用类型的初始值必须是一个对象,不能是常量。
- 无法令引用重新绑定到另外一个对象。
- 引用的类型必须和绑定对象是一致的。
- 不能定义引用的引用
示例:
int ival = 1024;
int ival2 = 1000;
int& refval = ival;
// 正确 refval引用并没有改变,只是改变了指向的变量ival的值,使得ival = ival2了。
refval = ival2;
//int& refval2; //错误 引用必须被初始化
cout << "ival = " << ival << endl; // 1000
cout << "ival2 = " << ival2 << endl; // 1000
cout << "refval = " << refval << endl; // 1000
int i = 1, i2 = 2;// i 和 i2都是int变量
int& r1 = i, r2 = i2;// r1是i的引用,r2是int变量且值与i2相同
int i3 = 1, & ri = i3;// i3是int变量,ri是i3的引用
int& r3 = i3, & r4 = i2;//r3是i3的引用,r4是i2的引用
//int& refval4 = 10; // 错误,引用类型的初始值必须是一个对象,不能是常量
double d = 3.14;
//int& refval5 = d; // 错误,引用的数据类型必须与所绑定对象的类型一致
2.3.2 指针
(1) 指针概念
指针是“指向(point to)”另外一种类型的复合类型。
指针与引用相同点:
指针也实现了对其他对象的间接访问。
指针与引用不同点:
指针本身就是一个对象,允许对指针赋值和拷贝,且可以修改指向的对象。而引用一定定义,就无法再绑定到另外的对象。
指针无须在定义时赋初值。而引用必须在定义时赋初值。
定义语法:
数据类型 *变量名;
示例:
int *p1, *p2; // p1 和 p2 都是指向int类型对象的指针
double d1, *d2; // d2 是指向 double 类型对象的指针; d1 是double型对象
(2)获取对象的地址
指针存放的是:某个对象的地址。要想获取该地址,需要使用取地址符(&)。
示例:
#include <iostream>
#include <string>
using namespace std;
void test() {
int ival = 42;
int* p = &ival;// p存放变量ival的地址,或者说p是指向变量ival的指针
double dval;
double* pd = &dval;// 正确 初始值是double型对象的地址
double* pd2 = pd;// 正确 初始值是指向double对象的指针
//int* pi = pd; // 错误 指针pi的类型与pd的类型不匹配
// 指针存放的是对象的地址
// pd和pd2都是指向的是dval的地址
dval = 20;
// 打印值
cout << "dval = " << dval << endl;// 打印dval值
cout << "*pd = " << *pd << endl;// 解引用打印pd指向对象的值
cout << "*pd2 = " << *pd2 << endl;// 解引用打印pd2指向对象的值
cout << "&dval = " << &dval << endl;// 打印dval地址
cout << "pd = " << pd << endl;// 打印pd指向地址
cout << "pd2 = " << pd2 << endl;// 打印pd指向地址
// 修改pd指针指向对象的值
*pd = 30;
cout << "dval = " << dval << endl;
cout << "*pd = " << *pd << endl;
cout << "*pd2 = " << *pd2 << endl;
cout << "&dval = " << &dval << endl;
cout << "pd = " << pd << endl;
cout << "pd2 = " << pd2 << endl;
}
int main() {
test();
system("pause");
return 0;
}
结果:
dval = 20
*pd = 20
*pd2 = 20
&dval = 003AFDD8
pd = 003AFDD8
pd2 = 003AFDD8
dval = 30
*pd = 30
*pd2 = 30
&dval = 003AFDD8
pd = 003AFDD8
pd2 = 003AFDD8
(3)指针值
指针的值(即地址)应属下列4种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,意味着指针没有指向任何对象
- 无效指针也就是上述情况之前的其他值。
注意:不能试图访问2、3、4类指针。如果访问了,后果无法预计。
(4)利用指针访问对象
*:解引用符
如果一个指针指向了一个对象,可以用*来解引用访问。
注意:解引用操作仅适用于确实指向了某对象的有效指针。
int ival = 42;
int* p = &ival;
cout << ival << endl; // 42 p存放着变量ival的地址,或者说p是指向ival的指针
cout << *p << endl; // 42 由符号*得到指针p所指的对象
*p = 0;
cout << ival << endl; // 0 *p可以得到所指的对象,所以可以通过*p可以给对象ival赋值
cout << *p << endl; // 0
(5)空指针
空指针不指向任何对象。在操作指针之前必须确认为非空指针。
生成非空指针的方法有:
// 三种生产空指针的方法:
// 建议用第一种
int* p = nullptr; // 等价于 int *p1 = 0; 是c++11新标准,利用字面值nullptr来初始化指针。
int* p2 = 0; // 直接将p2初始化为直面值常量0
//需要首先#include cstdlib
int* p3 = NULL; // 等价于 int *p3 = 0; NULL是预处理变量,值也是0。
//int zero = 0;
//int* p4 = zero;// 错误 不能把int变量直接赋给指针
使用指针建议:
初始化所有指针,并且尽量等定义了对象之后再定义指向它的指针。
如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。(当一个指针初始化为nullptr或者0后,若在没有指向某个对象情况下使用了,编译器会明确报错)
(6)赋值和指针
给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
int i = 42;
int* pi = 0; // pi被初始化,但并没有指向任何对象
int* pi2 = &i; // pi2被初始化,并赋值为i的地址。或者说指针pi2指向变量i.
int* pi3; // 定义指针pi3,但未被初始化。如果在块内,pi3的值是无法确定的
pi3 = pi2; // pi3和pi2指向同一个对象
pi2 = 0; // pi2改为不指向任何对象
cout << *pi3 << endl; // 42
//cout << *pi << endl; // 编译报错
//cout << *pi2 << endl; // 编译报错
有时候很难搞清楚一条赋值语句,是改变了指针的值,还是改变了指针所指对象的值。
最好的办法是:记住赋值永远改变的是等号左侧的对象。
int ival = 42;
int* pi = 0;
// pi的值被改变,pi现在保存的是ival变量的地址。pi指针指向的是ival变量
pi = &ival;
// *pi解引用,代表的是ival这个变量。ival的值被改变,指针pi并没有变,还是存的ival的地址。
*pi = 0;
(7)其他指针
只要指针拥有一个合法值,就能将它用在条件表达式中。如果指针为0,条件取为false,任何非0的指针,条件都取true。
int ival = 1024;
int *pi = 0; // pi 合法,是一个空指针
int *pi2 = &ival; // pi2 合法,存放着ival的地址
if(pi) // pi的值为0,因此条件为false
// ……
if(pi2) // pi2的值为ival的地址,不是0,因此条件为true
// ……
对于两个类型相同的合法指针,可以用 == 或 != 来比较,其结果就是bool型。
(8)void* 指针
void*是一种特殊的指针类型,可以用于存放任意对象的地址。
void*能做的事情比较有限,只能用于:
- 拿它和别的指针比较
- 作为函数的输入和输出
- 赋给另外一个void*指针
注意:不能直接操作void*指针所指对象。
double obj = 3.14, * pd = &obj;
int i = 42;
void* pv = &obj;
pv = pd;
pv = &i;
// cout << *pv << endl; // 报错
2.3.3 理解复合类型的声明
变量的定义包括一个基本数据类型和一组声明符。
int i = 1, *p = &i, &r =i;
int 是数据类型,* 和 第二个& 是声明符,第一个&是取址符。
(1)定义多个变量
int* p; // 合法,但是容易产生误导。基本数据类型是int而非*int。* 仅仅是修饰了p而已。
int* p1,p2; // p1是指向int的指针,p2是int类型变量。
// 最好写成
// int *p1,p2; 但是有的编辑器会自动修改了格式。
(2)指向指针的指针
int ival = 1;
int* pi = &ival; // pi指向一个int型变量
int** ppi = π // ppi指向一个int型指针。指向指针的指针
// 使用三种方式输出ival值
cout << ival << endl; // 1
cout << *pi << endl; // 1
cout << **ppi << endl; // 1
(3)指向指针的引用
int i = 42;
int* p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用 。
r = &i; // r是指针p的引用,所以r保存的是一个地址,这里给r赋值&i,就是令指针p指向i
*r = 100; // r是指针p的引用,所以解引用r与解引用p意义一样,会得到i,这里就是将i的值改为100
cout << i << endl; // 100
cout << *p << endl; // 100
cout << *r << endl; // 100
cout << &i << endl; // 打印i的地址
cout << p << endl; // 打印i的地址
cout << r << endl; // 打印i的地址
如何理解:int *&r
要理解r的类型到底是什么,可以从右往左阅读 r 的定义,离变量名最近的符号对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用于确定r引用的类型是什么,这里*说明r引用是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
2.4 const限定符
const对象一旦创建后其值就不能改变,所以const对象必须初始化。
const int buffsize = 512;
const int i = get_size(); // 正确 用函数返回值初始化,运行时初始化
const int j = 42; // 正确 编译时初始化
const int k; // 错误,未初始化
const对象仅在文件内有效,如果其他文件要使用,需在变量的定义之前加extern关键字。
// file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int buffsize = fcn();
// file1.h头文件
extern const int buffsize;// 与file1.cc中定义的buffsize是同一个
2.4.1 const的引用
把引用绑定到const对象上,称为对常量的引用。
与普通引用的区别:不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci; // 正确 引用及其对应的对象都是常量
r1 = 42; // 错误 r1是对常量的引用,不能修改值
int &r2 = ci; // 错误 试图让一个非常量引用绑定一个常量对象
// 假设r2合法,则可以通过r2来改变它引用的对象ci,这显然是错误的。
初始化常量引用,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。也允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
int i =42;
const int &r1 = i; // 正确 运行将常量引用绑定到普通int对象上
const int &r2 = 42; // 正确 r2是一个常量引用,可以初始化为字面值
const int &r3 = r1 * 2; // 正确 r3是一个常量引用,可以初始化为一般表达式
int &r4 = r1 * 2; // 错误 r4是一个普通引用
int &r5 = r1; // 错误 r5是一个普通引用,不能指向常量对象
r1 = 10; // 错误 不能修改常量引用
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不做限定的。
int i = 42;
int &r1 = i;
const int &r2 = i; // r2也绑定对象i,但是不运行通过r2修改i的值
r1 = 0; // 正确 r1并非常量,i的值可以修改为0
r2 = 0; // 错误 r2是一个常量引用,不能修改值