《C++primer》学习:第二章(笔记及程序设计练习)

一、指针 & 引用

1.1什么是引用?

引用就是用一个引用变量指向另一个对象。使得这个引用变量能够间接访问被指向的对象!把对象赋值给引用变量和赋值给普通变量完全不一样!赋值给其他变量,是一种值传递,只能够传值而不能间接操纵其地址的值。但是引用变量相当于给被引用的变量起了别名,操纵引用变量相当于操作被引用的变量。

1.2什么是指针?指针与引用的区别?

一个引用变量的存在,必须依赖于一个唯一 确定的变量!引用变量本身不是引用对象,所以不能够被其他的引用变量所引用!而指针变量本身也是变量,是可以被其他指针所指向的!这个是指针与引用的一个大区别!也就是说,指针有n级指针,但是引用没有!

另外,引用变量所指向的对象一旦确定,那么在它的生命周期中, 是不能被再次指向给其他的变量!

不妨看以下示例程序:

#include <iostream>
using namespace std;

int main()
{
	int i = 5, j = 10;
	int& r = i;
	r = 6;
	cout << i << ' ' << j << ' ' << r << endl;
	r = j;
	r = 8;
	cout << i << ' ' << j << ' ' << r << endl;
	
	int& rr = r;
	rr = 20;
	cout << i << ' ' << j << ' ' << r << endl;
	return 0;
}

展示效果如下:
《C++primer》学习:第二章(笔记及程序设计练习)
从这里可以看出来,引用 r 指向了 i 之后,哪怕再次指向了 j ,但是改变引用变量的值,我们可以发现,并不能间接访问 j 的值!还是在访问 i 的值!另外,可以有另一个引用变量去引用另一个变量,可以多层次引用,也就是说,引用的引用还是在引用,那么多层引用和多重引用有和区别呢?(模仿多重继承和多层继承!)

下述示例程序是验证和展示多层引用多重引用

#include <iostream>
using namespace std;

int main()
{
	int i = 5;
	int& r1 = i;
	r1 = 6;
	cout << i << ' ' << r1 << endl;
	int& r2 = r1;
	r2 = 8;
	cout << i << ' ' << r1 << endl;
	int& r3 = i;
	r3 = 10;
	cout << i << ' ' << r1 << endl;
	return 0;
}

结果展示如下:
《C++primer》学习:第二章(笔记及程序设计练习)
从中我们可以看出,一个变量的多个引用就是这个变量的多个别名,一个别名的改变都会让其他的别名(引用变量)同时改变!

1.3从底层角度看引用和指针

先来看一个示例代码:

#include <iostream>
using namespace std;

int main()
{
	int i = 5;
	int* pr = &i;
	cout << pr << ' ' << *pr << ' ' << &i << ' ' << i << endl;
	*pr = 10;
	cout << pr << ' ' << *pr << ' ' << &i << ' ' << i << endl;
	
	int& rr = i;
	cout << &rr << ' ' << rr << ' ' << &i << ' ' << i << endl;
	rr = 20;
	cout << &rr << ' ' << rr << ' ' << &i << ' ' << i << endl;
	
	int* prr = &rr;
	cout << prr << ' ' << *prr << ' ' << &i << ' ' << i << endl;
	*prr = 30;
	cout << prr << ' ' << *prr << ' ' << &i << ' ' << i << endl;
	return 0;
}

展示效果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

那个,咱事先说明一下实验环境:
Linux下:centOS + g++
Windows下:VS2019

实验表明:
无论是指针还是引用,都是给对象起了个别名,这个别名的意思就是在内存上的同一块地址,有多个变量可以改变它的值!也就是说,在上述的实验例子中,pr、&rr 以及 prr 都描述了和 &i 一样的地址!并且都可以去访问它!而且在第三部分中我们发现:

	int* prr = &rr;
	cout << prr << ' ' << *prr << ' ' << &i << ' ' << i << endl;
	*prr = 30;
	cout << prr << ' ' << *prr << ' ' << &i << ' ' << i << endl;

指针是可以指向一个引用变量的地址,但是这是否说明了引用对象也是有明确地址的呢?我认为这是的!引用变量是另一个对象的别名,和另一个对象公用一个地址,于是它所指向的对象的地址,也就是引用变量的地址!但是这里和《C++primer》有个出入的地方!如下:
《C++primer》学习:第二章(笔记及程序设计练习)
这个地方在书的 P47 欢迎知道的朋友评论或者私信我!

破案了!我一开始理解错了指向引用的指针!

我们上述代码:

int* prr = &rr;

这并不是一个指向引用的指针,而是一个普通的指针!真正要说指向引用的指针要这样写:

int& *prr = &rr;

这样是会报错的!因为没有指向引用的指针!
报错如下:
《C++primer》学习:第二章(笔记及程序设计练习)

1.4空指针

c++中如何定义NULL空指针呢?

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

如图我们可以看出,空指针有两中说明,在c++中,null 就是 0,但是它是指针吗?其实,用字面常量0可以给指针变量赋予空指针的含义,但是它并不是指针类型,最好还是用指针类型的一个指针常量nullptr去赋予一个空指针的意义!因为只有nullptr才能严格地说是空指针!如下实例:

#include <iostream>
using namespace std;

void FuncNull(int p)
{
	cout << "null pointer is NULL(0) " << endl;
}

void FuncNull(void* p)
{
	cout << "null pointer is NULL(void*) " << endl;
}

int main()
{
	FuncNull(NULL);
	FuncNull(nullptr);
	return 0;
}

结果展示:
《C++primer》学习:第二章(笔记及程序设计练习)
我们可以看出,NULL实际上是一个以 0 来代替的空指针字面常量,属于整数类型!但是nullptr是一个真真正正的指针,是一个表示空指针的指针!
若想把NULL从常量变为一个指针,唯一的办法就是强制类型转换:*p = NULL,如此便可让NULL从一个字面常量(int类型)变为一个空指针(指针变量),展示如下:

#include <iostream>
using namespace std;

void FuncNull(int p)
{
	cout << "null pointer is NULL(0) " << endl;
}

void FuncNull(void* p)
{
	cout << "null pointer is NULL(void*) " << endl;
}

int main()
{
	int* p1 = NULL;
	int* p2 = nullptr;
	FuncNull(p1);
	FuncNull(p2);
	return 0;
}

结果展示如下:
《C++primer》学习:第二章(笔记及程序设计练习)
从此可以很明显看出NULL从一个字面整型常量到一个int指针变量。

关于空指针异常的几点建议:
《C++primer》学习:第二章(笔记及程序设计练习)
一定要初始化所有的指针,最好要在定义好了该指向的对象之后再定义指向它的指针,否则就把指针初始化为nullptr!千万不能不初始化指针!

1.5 void* 指针

void* 类型的指针一般是用来作为函数的输入参数和输出返回值。适用于不知道指向的对象是何种类型的时候,一般就可以先赋予给void类型,然后后面再根据需要强制类型转换即可!有点类似于模板的:template< typename T> 这种操作!
这里我就不再另外给出例子了,其实上面空指针那一部分我们就已经用到了void
作为函数输入了!另外,在C语言中,咱们常用的开辟动态内存空间的malloc函数就用到了void*,于是我们每次使用这个函数的时候都要对返回值强制类型转换!

二、限定符const

2.1什么是const限定符

const限定了常量,被const限定的常量会在声明时候就立即初始化一个值,赋予给这个常量,并且在当前文件中用到了这个值的地方都可以用常量去代替!并且这个常量是只读常量,是不能被修改的!
但是需要注意的是,这个常量只能在当前文件中有效,如果不加上extern关键字,就不能在其他文件中读取这个常量!

2.2被const限定符修饰的引用

被const限定符限定的引用变量:
第一:
我们不能通过修改引用变量的值去间接修改被引用的对象;

int iVal = 10;
const int& ri = iVal;

ri = 20; // 这就是一个典型的错误,原因如上!

第二:
一个被const限定符限定的一个常量,不能被一个非常量的引用去引用!

const int iVal = 10;
int& ri = iVal; // 这也是一个典型的错误,原因如上!

2.3讨论关于常量引用

加了const限定符的引用是否是不能被修改的引用呢?
这个答案很明显是否定的!因为引用并非一个对象,更谈不上常量!所谓的常量引用是指被引用的对象是常量,也就是说不能通过修改访问从const限定的引用变量从而间接修改访问了被引的对象!但是其实可以通过修改被引的对象来修改const限定的引用变量!

#include <iostream>
using namespace std;

int main()
{
	int iVal = 10;
	const int& r1 = iVal;
	cout << r1 << ' ' << iVal << endl;
	iVal = 20;
	cout << r1 << ' ' << iVal << endl;
	return 0;
}

结果展示:

《C++primer》学习:第二章(笔记及程序设计练习)
由此可以看出:
可以通过修改const限定符引用所指引的对象,来间接修改引用变量!

2.4初始化被const限定的引用变量

#include <iostream>
using namespace std;

int main()
{
	double pi = 3.14;
	const int& p1 = pi;
	cout << pi << ' ' << p1 << endl;

	const int pTemp = pi;
	const int& p2 = pTemp;
	cout << pi << ' ' << p2 << endl;
	return 0;
}

实例代码展示:
《C++primer》学习:第二章(笔记及程序设计练习)
由此可见,在类型不同的对象赋予给引用变量的时候,无论是不是const限定的引用变量,都会先生成一个匿名对象去接收它,然后再赋予给引用变量!但是这种操作是完全没有意义的!!因为你设想,我们在类型转换的时候,很可能会把赋予的结果弄错,比方说这里的3.14和3,很明显进行了隐式类型转换,此时对象和对象的别名(也就是引用)就压根不是同一个值了,于是这种操作没有意义,要引以为戒!

在这里我们不妨再次拿指针和引用做比较:

处理匿名对象转换过程中,指针和引用的差别

#include <iostream>
using namespace std;

int main()
{
	double pi = 3.14;
	const int* p1 = &pi;
	cout << pi << ' ' << p1 << endl;

	return 0;
}

这就会报错,报错如下:
《C++primer》学习:第二章(笔记及程序设计练习)

const + 基本数据类型 + ‘*’ 是一个新的数据类型,这个数据类型不支持强制转换,也不会出现匿名对象!
《C++primer》学习:第二章(笔记及程序设计练习)
这个总结就说的很到位!

2.4 指针常量 & 常量指针

一言以蔽之咱们的指针常量,首先它肯定是指针,然后指向的对象不能通过改变指针来间接改变!
也就是说指针常量的表示方法为:const + 数据类型 + ‘*’ + 指针常量名称 。
带有指针的东西,我们要从右往左看,离指针常量名称最近的符好标识这玩意的类型,它是 (星号)由此可见它是一个指针,然后看数据类型,知道它是指向某种类型的对象的指针,然后看限定符,说明指向的对象是不能通过改变自身去修改的,于是知道,它是指针常量!切记,在中文命名中,摆在前面的是定义,指针常量,指针摆在前面,说明它是指针。而在代码中,离对象名称最近的是定义,(星号)离它最近,于是它是指针!

#include <iostream>
using namespace std;

int main()
{
	char Char = 'a';
	const char* pChar = &Char;
	cout << *pChar << ' ' << Char << endl;
	Char = 'z';
	cout << *pChar << ' ' << Char << endl;
	return 0;
}

这个是指针常量的典型代码,结果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

那么什么是常量指针呢??

常量指针的定义格式:数据类型 + ‘*’ + const + 常量指针名称
这个格式的理解完全可以参照上面指针常量的理解去看!
指针本身是个常量并不意味着不能通过修改指针本身去修改指向对象的值,这个需要看被指向的对象类型。

#include <iostream>
using namespace std;

int main()
{
	char Char1 = 'a';
	char* const constChar1 = &Char1;
	cout << *constChar1 << ' ' << Char1 << endl;
	Char1 = 'c';
	cout << *constChar1 << ' ' << Char1 << endl;
	*constChar1 = 'd';
	cout << *constChar1 << ' ' << Char1 << endl;
/*
	const char Char2 = 'h';
	char* const constChar2 = &Char2;
*/
	return 0;
}

代码结果展示:
《C++primer》学习:第二章(笔记及程序设计练习)
从中我们可以理解到:
常量指针可以指向所有非常量的对象,并且常量指针和被指向的对象之间可以相互修改!
那就问题来了,这个常量指针到底是常量在何方呢????

细说常量指针的 “常量” 体现在何方?

普通变量变成常量,就说明这个常量化的变量只能一次性在声明的同时赋初值,并且后续不能再修改值,由此类比,我们可以知道,常量指针也就是只能再声明的时候初始化其指向的对象,后续再次初始化将报错!

#include <iostream>
using namespace std;

int main()
{
	char Char1 = 'a';
	char Char2 = 'b';
	char* const constPointer = &Char1;

	constPointer = &Char2;
	return 0;
}

报错结果展示:
《C++primer》学习:第二章(笔记及程序设计练习)

总结常量指针和指针常量:

指针常量是个指针,它不能通过访问指针的方式来间接访问指针所指向的对象
常量指针是个常量,它只能在声明的同时初始化指向的对象,并且后面不能再修改!

三、c++11中decltype的使用

decltype关键字可以从表达式、常量、函数返回值类型来确定声明的变量的数据类型,这样可以做到在不确定数据类型的时候,声明变量,并且同时不去进行初始化!我们可以用< typeInfo >库的函数typeid(对象名).name()去查看类型,我们不妨有如下实验代码:

#include <iostream>
#include <typeinfo>
using namespace std;

int f() { return 0; }

int main()
{
	decltype(f()) valInt;
	cout << typeid(valInt).name() << endl;
	decltype(1 + 'a' + 3.14) valDouble;
	cout << typeid(valDouble).name() << endl;

	char CharTest = 'x';
	decltype('a') valChar = CharTest;
	cout << typeid(valChar).name() << ' ' << valChar << ' ' << CharTest << endl;
	valChar = 'y';
	cout << typeid(valChar).name() << ' ' << valChar << ' ' << CharTest << endl;

	decltype(('a')) valRef = CharTest;
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;
	valRef = 'z';
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;
	return 0;
}

实验结果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

从上述实验我们得出总结:

第一:
如果是表达式,我们会先进行表达式的计算,把计算结果的数据类型赋予给声明的变量。

第二:
如果是函数,比如上面的f(),我们不会去运行这个函数,而是获取函数的返回值类型,然后把这个数据类型给声明的变量,这里需要注意,函数的返回值类型取决于声明的部分,不取决于return的部分,也就是说,就算是我们return的语句不是return 整型数据;而是return 浮点数;也不会改变结果!

第三:
这里存在一个疑惑点,我们先看看《C++primer》书上P63页的内容:
《C++primer》学习:第二章(笔记及程序设计练习)
按照书上的指导说法,咱们的实验代码的这一部分:

	decltype(('a')) valRef = CharTest;
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;
	valRef = 'z';
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;

这里理应是把valRef的数据类型声明成了一个char类型的引用类型,如果是引用类型,那么我们在对charTest对象的引用的时候,通过修改引用变量valRef的值,也会间接修改被引对象的值才对,但是在我们的实验结果中可以看到,并没有产生这个结果!

再看一个实验示例:

#include <iostream>
#include <typeinfo>
using namespace std;

int main()
{
	int a = 3, b = 4;
	decltype(a) c = a;
	decltype((b)) d = b;
	c++;
	d++;
	cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
	return 0;
}

看看实验结果:
《C++primer》学习:第二章(笔记及程序设计练习)
从这里我们可以看出,变量 d 是引用类型的变量,引用的对象是 b!

总结:

decltype((变量名)) 声明变量名;
能够使得被声明的变量成为对应类型的引用类型变量!注意这里括号内必须写变量,不能写常量、常量表达式、表达式,我们不妨实验如下:

错误改写方案1:
#include <iostream>
#include <typeinfo>
using namespace std;

int main()
{
	int a = 3, b = 4;
	decltype(a) c = a;
	// 把 b 改成 b + 1
	decltype((b + 1)) d = b;
	c++;
	d++;
	cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
	return 0;
}

结果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

错误改写方案2:
#include <iostream>
#include <typeinfo>
using namespace std;

int main()
{
	int a = 3, b = 4;
	decltype(a) c = a;
	// 把 b 改成 4
	decltype((4)) d = b;
	c++;
	d++;
	cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
	return 0;
}

结果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

改正之前的代码:把常量改成变量即可
#include <iostream>
#include <typeinfo>
using namespace std;

int main()
{
	char CharTest = 'a';
	decltype((CharTest)) valRef = CharTest;
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;
	valRef = 'z';
	cout << typeid(valRef).name() << ' ' << valRef << ' ' << CharTest << endl;
	return 0;
}

结果如下:
《C++primer》学习:第二章(笔记及程序设计练习)

上一篇:C++ Primer Plus 第4章 习题8


下一篇:C++ primer读书记录 第一次更新2021.7.6