c++ primer 5th 笔记

第1章 开始

本章以一个实际问题,书店问题,来简单的介绍C的基本特性。这个问题的代码将贯彻整本书,后面的章节会逐一讲解代码中涉及到的 C 语言特性。

这个问题具体,就是要保存书店的所有销售记录的档案,每条记录保存了某本书的一次销售信息,包含三个数据:

0-201-70353-X	4	24.99

它们分别是书的 ISBN,售出的册数,书的单价。有时,书店老板需要查询此档案,计算每本书的销售量、销售额及平均售价。

这个问题会涉及到的有:

  • 定义变量
  • 进行输入和输出
  • 使用数据结构保存数据
  • 检测两条数据是否有相同的 ISBN
  • 包含一个循环来处理销售档案中的每条记录

我们首先介绍如何使用 C++ 来解决这些子问题,然后编写书店程序。

1.1 一个简单的C++程序

int main()
{
    return 0;
}

由这个程序简单的介绍函数的定义。

函数定义包含四部分:

  1. 返回类型 
  2. 函数名 
  3. 一个括号包围的形参列表 (允许为空)
  4. 函数体 

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种状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  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*能做的事情比较有限,只能用于:

  1. 拿它和别的指针比较
  2. 作为函数的输入和输出
  3. 赋给另外一个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 = &pi; //  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是一个常量引用,不能修改值


 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值