万丈高楼平地起——C++入门(下卷)

x33g5p2x  于2022-05-13 转载在 其他  
字(16.9k)|赞(0)|评价(0)|浏览(777)

😏前言

哈喽大家好,我是鹿 九 丸 \color{red}{鹿九丸}鹿九丸,今天给大家带来的是C++入门系列的下卷,关注我博客的话应该已经看到我已经三四天没有更新博客了,今天的内容很多,比上卷应该是还要多一些,补充了很多的细节,希望大家能够有所收获!
如果大家在看我的博客的过程中或者学习的过程中以及在学习方向上有什么问题或者想跟我交流的话可以加我的企鹅号:2361038962 \color{red}{2361038962}2361038962,或者寄邮件到相应的邮箱里:2361038962 @ q q . c o m \color{red}{2361038962@qq.com}2361038962@qq.com,我会尽量帮大家进行解答!

💓6. 引用

💞6.1 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

类型& 引用变量名(对象名) = 引用实体;

例如:

int a = 5;
int& b = a;//b叫做a的引用,或者称b是a的别名
int& c = a;
int& d = b;//可以对引用取别名
//b、c、d都是a的别名,都可以用来修改变量a的值

图示:

通过监视可以看到:

a、b、c、d四个标识符的值都是一样的,并且对其进行取地址后其地址也是一样的。

那么引用的用处在哪呢?

#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 5;
	int b = 10;
	cout << a << ' ' << b << endl;
	Swap(a, b);
	cout << a << ' ' << b << endl;
	return 0;
}

运行结果:

此处可以看到,使用引用后,我们不需要传递变量的地址,不需要使用指针就能在函数中交换两个变量的值。

注意:引用也能够改变指针变量的值。

void f(int*& pa)
{
	pa = NULL;
}
int main()
{
	int a = 5;
	int* pa = &a;
	cout << pa << endl;
	f(pa);
	cout << pa << endl;
	return 0;
}

上面代码的运行结果是:

在函数中pa指针变量的值被修改成了空指针。

C中常常会有这么一句话:不能定义引用的引用,这句话是什么意思?
回顾C语言,指针的指针,就是二级指针,所以引用的引用就是“二级引用”,所以不能定义引用的引用这句话的意思就是C
中不存在二级引用。所以下面的操作是非法的:

int a = 10;
int& ia = a;
int&& iia = ia;

iia的定义是非法的。

💞6.2 引用特性

(1)引用在定义时必须初始化

下面的操作是非法的:

int &a;

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

对象和引用的关系
对象是具有某种数据类型的内存空间。而引用并不占据内存空间,所以引用并非对象,相反的,它只是为一个已经存在的对象所起的另一个名字。

(2)一个变量可以有多个引用

(3)引用一旦引用一个实体,再不能引用其他实体。这是和指针的一个大的区别,指针可以在初始化后指向其它的地址空间。

当然,我们一般也无法引用其它的实体,比如我们下面的操作:

int a = 10;
int& ia = a;
int b = 20;
int& ib = b;
ib = a;
ib = ia;

我们在上面进行的最后这两行操作,比如ib = aib = ia都是进行的赋值操作,而不是对引用的更改。

当引用作为左值时,是被指向的那个对象本身的内存空间,比如在上面的例子中,就是指代的标识符ib指向的那个整型变量b的内存空间。

当引用作为右值时,是被指向的那个对象本身的内存空间所存储的值,在上面的例子中,ia在作为右值时就是a这个变量所指向的内存空间中存储的值10。

💞6.3 常引用

💘6.3.1 常引用规则

规则介绍:

int a = 10;
const b = 10;

(1)普通引用引用普通变量。(合法操作,无限制)

int& ia = a;

(2)常引用引用普通变量。(合法操作,无法通过常引用来修改普通变量)

const int& ia = a;

下面的操作是非法的:

ia = 20;

(3)常引用引用常变量。(合法操作,无法通过常引用来修改常变量)

const int& ib = ib;

下面操作是非法的:

ib = 0;

(4)普通引用引用常变量。(非法操作)

下面操作是非法的:

int& ib = b;

总结:

取别名的原则:对原引用变量,权限只能缩小,不能放大。

在上面的例子中,(2)就是权限的缩小,变量a是可读可写的,但是引用ia就是只能读的了,所以就是权限;(4)就是权限的放大,常变量b是只读的,但是引用ib是可读可写的,所以这就是权限的扩大。

💘6.3.2 注意点

(1)常引用可以对常量取别名。

const int& a = 100;

此时的权限既没有扩大也没有缩小,因为a的权限就是只读的,100作为常量的权限也是只读的。

(2)引用的类型都要和与之绑定的对象严格匹配。

double d = 3.14;
int& id = d;

此处的引用是非法操作,引用类型的初始值必须是int型对象。

但是在int的前面加上一个const就可以了,如下操作:

const int& id = d;

我们知道,在将double类型的变量赋值给int型时,会发生隐式类型转换:

double pi = 3.14;
int a = pi;

在将pi赋值给变量a的过程中,实际上发生了一个类型转换,即3.14被转换成int类型发生了截断,这个过程中会有一个临时变量来存储3.14这个值,存储位置一般是寄存器,下面用图来表示:

double d = 3.14;
const int& id = d;

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

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

在这种情况下,id绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间(寄存器或内存空间)来暂存表达式临时创建的一个未命名的对象。C++程序员常常把临时对象称为临时量。

临时变量具有常性,是只读常量,所以能被const进行引用,如果没有const,就属于权限的放大。

注意:在将寄存器中的3赋值给3的过程中,本质上还是值的拷贝,即将寄存器中3的二进制位拷贝到变量a的内存空间中,对变量a进行修改,寄存器中的值不会有所改变,引用亦是如此。

当然,还有另外一种理解(只作为理解):
问:pi作为右值时代表的是什么?

答:pi作为右值时就是pi变量空间中存储的值,所以上面的代码就相当于是下面这段代码:

const int& a = 3.14;

3.14是只读常量,因为左边的类型是const int&,所以发生了强制类型转换,变成了3,3作为临时变量具有常性,所以能够被赋值给a。

注意:普通对象不存在权限的放大与缩小的问题,如我们通常使用的赋值表达式int a = 3,a只是一个普通变量,只是将3的二进制位拷贝到变量a所在的内存空间中,所以不存在权限的放大与缩小的问题。因为a的改变不会改变10,只有指针和引用才会存在权限的放大与缩小的问题。

💞6.4 使用场景

  1. 做参数
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
int main()
{
    int a = 10;
    int b = 20;
    Swap(a,b);//实现了变量a和b的交换,交换之后a的值位20,b的值为10
    return 0;
}

意义:

  • 一定程度上规避指针,使代码更容易理解
  • 减少值拷贝,提高效率
  1. 做返回值

首先先看下面的一段代码:

int& Count()
{
	static int n = 0;
	n++;
	return n;
}
int main()
{
    cout << Count() << endl;
    cout << Count() << endl;
    cout << Count() << endl;
    return 0;
}

上面的输出结果是123,因为n是静态全局变量,存储在静态区,声明周期在整个程序运行期间始终存在,且静态全局变量只初始化一次,所以只运行一次static int n = 0,所以输出结果是123。

下面看这段代码:

int Count()
{
    static int n = 0;
    n++;
    return n;
}
int main()
{
    int ret = Count();
    return 0;
}

注意:无论是函数在进行传参还是函数在传返回值的时候,都会形成一个临时变量,如果所传的参数或返回值较小,那么临时变量就用寄存器进行传递,否则就不用寄存器替代。

问:为什么要出现一个临时变量?

答:如果没有临时变量,在Count()函数调用结束之后,函数栈帧就销毁了,n所在的内存空间就无法使用了,所以就无法将返回值进行返回。

但是如果我们将n的值放在一个临时变量中,这个临时变量可能是寄存器,也有可能是内存中的某一个空间(比如在调用Count()函数之前提前在main()函数中开辟好要存储返回值的空间),但无论如何,这个临时变量都已经不在Count()函数的栈帧上了,所以我们可以在Count()函数栈帧销毁之后依然可以将n的值传递回main函数中。

那么传值返回的意义是什么呢?意义就是临时变量的类型是int类型。

此时看下面这段代码:

int Count()
{
    int n = 0;
    n++;
    return n;
}
int main()
{
    const int& ret = Count();
    return 0;
}

这个程序可以正常运行,为什么呢?因为return n传递的临时变量具有常性,可以被常引用接收,实际上上面这段代码类似于const int& ret = 1

再看下面这段代码:

int& Count()
{
    int n = 0;
    n++;
    return n;
}
int main()
{
    int ret = Count();
    return 0;
}

上面这段代码是什么意思呢?表示return返回的临时变量的类型是int&,这个引用是Count()栈帧中变量n的引用或者别名。

总结:如果返回类型是int,那么临时变量就是返回值的临时拷贝,类型就是int,如果返回类型是int&,那么临时变量就是被调用函数栈帧中所要传变量的引用。

再看下面这段代码:

int& Count()
{
    int n = 0;
    n++;
    return n;//返回的是n的别名
}
int main()
{
    int& ret = Count();
    return 0;
}

临时变量的类型是int&类型,所以也能被int&类型的ret来接收。

return n,返回的是n的别名,又将n的别名赋值给ret,即ret是别名的别名,就是n的别名。

采用如下方式进行验证:

int& Count()
{
    int n = 0;
    n++;
    cout << "Count:" << &n << endl;
    return n;
}
int main()
{
    int& ret = Count();
    cout << "main:" << &ret << endl;
    return 0;
}

运行结果:

ret和n的地址是一样的,说明ret就是n的别名。
传值返回和传引用返回有什么区别?

传值返回:会有一个拷贝。

传引用返回:没有拷贝,函数返回的直接就是返回变量的别名。

问:上面例子中的Count栈帧已经销毁了,为什么还存在n的别名?我们甚至还能在main函数中访问到n?

答:函数栈帧的销毁只是我们不再拥有那段空间的使用权,那段空间依旧存在,我们依然可以通过引用或者指针去访问它,但是那段空间一旦被其它的函数栈帧所覆盖,那么我们再次进行访问输出,得到的就是一个随机值。如下面的代码所示:

int& Count()
{
    int n = 0;
    n++;
    return n;
}
int main()
{
    int& ret = Count();
    cout << ret << endl;
    cout << ret << endl;
    cout << ret << endl;
    return 0;
}

运行结果:

在第一次输出正确结果的原因是因为原来的栈帧销毁后内存空间中的值并没有改变,当然,这种行为是非法的,类似野指针,后面两次输出结果错误是因为cout调用了函数,栈帧被覆盖了,所以输出结果就是随机值。
问:什么场景下可以用引用返回来减少拷贝?什么时候只能用传值返回?

答:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(例如static变量或者全局变量),则可以使用引用返回,如果已 经还给系统了,则必须使用传值返回。

此时看下面这段代码:

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1,2);
    Add(3,4);
    cout << "Add(1,2) is :" << ret << endl;
    return 0;
}

输出结果:

出现7的结果如下:

ret是Add函数中局部变量c的别名,虽然在函数Add(1,2)调用之后销毁了,但是值仍然保存在那,通过ret仍然能够找到那块空间,再次调用Add(3,4)那个函数,c变量的那段空间所存储的值变成了7,这就是输出结果为7的原因。当然,如果我们像下面这样进行修改代码,就会出现不是7而是随机数的结果:

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1,2) is :" << ret << endl;
    cout << "Add(1,2) is :" << ret << endl;
    return 0;
}

输出结果:

第二次输出结果就是随机数了,原因就是之前Add所在的栈帧空间因为第一次cout相关函数的调用栈帧被覆盖了,所以会输出随机数。

如果我们继续像下面这样修改代码,就会出现不一样的结果:

int& Add(int a, int b)
{
    static int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1,2) is :" << ret << endl;
    cout << "Add(1,2) is :" << ret << endl;
    return 0;
}

输出结果:

因为static变量是存在于静态区的,生命周期是整个程序的运行期间,会一直存在,所以输出结果为3,且再次进行打印也会输出同样的结果。

💞6.5 传值、传引用传指针效率比较

💘6.5.1 传值、传引用传指针作为参数效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是 传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestFunc3(A*) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	//以指针作为函数参数
	size_t begin3 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc3(&a);
	size_t end3 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
	cout << "TestFunc3(&A)-time:" << end3 - begin3 << endl;
}
int main()
{
	TestRefAndValue();
	return 0;
}

运行结果:

从上图中可以看出,传指针和传引用比传值效率高很多。

💘6.5.2 传值、传引用传指针作为返回值返回效率比较

代码:

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
A* TestFunc3(){ return &a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
    //以指针作为函数的返回类型
	size_t begin3 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc3();
	size_t end3 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
	cout << "TestFunc3 time:" << end3 - begin3 << endl;
}
int main()
{
	TestReturnByRefOrValue();
	return 0;
}

运行截图:

通过上述代码的比较,发现传值和指针或引用在作为传参以及返回值类型上效率相差很大,传指针和传引用的的效率相差不大。

💞6.6 引用和指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

int main()
{
	int a = 10;
	int& ra = a;

	cout << "&a = " << &a << endl;
	cout << "&ra = " << &ra << endl;
	return 0;
}

底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{
	int a = 10;

	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 20;

	return 0;
}

引用和指针的汇编代码比较:

可以看到两者的汇编代码是一样的,所以它们的底层实现是一模一样的。

指针和引用赋值的汇编代码比较:

使用指针和使用引用进行赋值从汇编代码上看,它们也是完全一样的。

引用和指针的不同点:

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同引用结果为引用类型或者说是引用变量的大小,但指针始终是**地址空间所占字节个数(**32位平台下占 4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用引用编译器自己处理
  8. 引用比指针使用起来相对更安全
  9. 从语法上来说,引用并没有开辟额外的空间,只是一个别名,指针开辟了空间,存储地址;但从底层上来说,引用和指针一样,开辟了空间存放地址。

💓7. 内联函数

💞7.1 概念

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销, 内联函数提升程序运行的效率。
为什么要出现inline?

解决宏函数晦涩难懂,容易写错的问题,因为要考虑到优先级问题,所以要加很多的括号;另一方面宏不支持调试,并且没有类型安全的检查等问题,使用内联函数能够解决上面的问题。

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。

如果使用了内联函数,如下面代码所示:

inline int Add(int left, int right)
{
	return left + right;
}
int main()
{
	int ret = 0;
	ret = Add(1, 2);
	return 0;
}

在汇编语言查看时将不会看到call调用Add()函数。

使用inline后查看方式如下:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add,查看结果如下:(注:默认的debug版本下内联函数不会展开,但在release版本下会展开)

发现在release版本下无法查看汇编代码。

  1. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进 行优化,以下给出vs2019的设置方式)

  2. 右击项目工程,点击属性。

  1. 如下操作:

  1. 如下操作:

上述操作完成后查看反汇编:

此时inline内联函数就实现了展开,没有了call汇编指令,即函数的调用。

💞7.2 特性

  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长(一般10行以上就算长)或者有循环/递归的函数不适宜 使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等 等,编译器优化时会忽略掉内联。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline函数被展开,符号表里就没有对应的函数地址了,链接就会找不到。
// Func.h
#include <iostream>
using namespace std;
inline void func(int i);
// Func.cpp
#include "Func.h"
inline void func(int i)
{
	cout << i << endl;
}
// main.cpp
#include "Func.h"
int main()
{
	func(10);
	return 0;
}

这样程序会出现链接错误。

程序像下面这样进行修改就不会出现问题:

// Func.h
#include <iostream>
using namespace std;
inline void func(int i);
#include "Func.h"
void func(int i)
{
	cout << i << endl;
}
// main.cpp
#include "Func.h"
int main()
{
	func(10);
	return 0;
}

因为Func.h文件里的文本内容会在main.cpp文件中展开,所以不会出现链接错误。

当然,下面还有两种特殊的情况:

(1)声明的时候带inline,定义的时候不带inline

// Func.h
#include <iostream>
using namespace std;
inline void func(int i);
// Func.cpp
#include "Func.h"
void func(int i)
{
	cout << i << endl;
}
// main.cpp
#include "Func.h"
int main()
{
	func(10);
	return 0;
}

此时程序可以正常运行,没有出现链接错误,此时inline并没有发挥应有的作用,func函数并没有展开。

(2)声明的时候不带inline,定义的时候带inline。

// Func.h
#include <iostream>
using namespace std;
void func(int i);
// Func.cpp
#include "Func.h"
inline void func(int i)
{
	cout << i << endl;
}
// main.cpp
#include "Func.h"
int main()
{
	func(10);
	return 0;
}

此时程序不能正常运行,出现链接错误,因为Func.cpp文件汇编形成的符号表里依旧没有func函数的地址,所以会链接失败。

注意:如果在函数定义中加了inline,那么在函数声明中就可以不必再加了,这样也能达到inline的使用效果。

💞7.3 关联面试题

宏的优缺点?

优点:

  1. 增强代码的复用性。
  2. 提高性能。

缺点:

  1. 不方便调试宏。(因为预编译阶段进行了替换)
  2. 导致代码可读性差,可维护性差,容易误用。
  3. 没有类型安全的检查 。

C++有哪些技术替代宏?

  1. 常量定义 换用const
  2. 函数定义 换用内联函数

💓8. auto关键字(C++11)

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

补充知识:typeid().name函数可以用来让编译器告知某个变量的类型是什么,该函数的返回类型是一个字符串。

int TestAuto()
{
	return 10;
}
int main()
{
	int a = 10;
	auto b = a;//a是整型,编译器推导出变量b的类型是int型
	auto c = 'a';//'a'是字符型,编译器推导出变量c的类型是char类型
	auto d = TestAuto();//TestAuto函数的返回类型是int型,编译器推导出变量d的类型是int型
    const int e = 10;
    auto ie = &e;

	cout << typeid(b).name() << endl;//输出结果为int
	cout << typeid(c).name() << endl;//输出结果为char
	cout << typeid(d).name() << endl;//输出结果为int
	cout << typeid(ie).name() << endl;//输出结果为const int *  

	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}

【注意】

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类 型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

💞8.2 auto的使用细则

  1. auto与指针和引用结合起来使用 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int a = 10;
auto* pa = &a;//指定pa为指针类型
auto* ppa = &pa;
auto& ia = a;//指定ia的类型为引用类型
auto* pa = a;//程序非法,因为指定了pa的类型是指针类型,但是我们却给它赋了地址
cout << typeid(pa).name() << endl;//输出结果为int *
cout << typeid(ppa).name() << endl;//输出结果为int **
cout << typeid(ia).name() << endl;//输出结果为int
  1. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对 第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 10, d = 3.14;//错误的使用方式
auto i = 0, *p = &i;//正确的使用方式,i是整数,p是整型指针
auto a = 10, b = 20;//正确的使用方式,因为经过编译器推导之后,程序会变成下面的定义方式
int a = 10, b = 20;
  1. auto在使用时会遵循隐式类型转换的规则。比如下面这样:
int main()
{
	auto a = 3.14 + 5;//表达式3.14和5相加后的类型转换为double类型,值为8.14
	cout << typeid(a).name() << endl;
	return 0;
}

8.3 auto不能推导的场景

  1. auto不能作为函数的参数和返回值
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto1(auto a)//注意:auto也不能作为缺省参数,例如:void TestAuto1(auto a = 10) {}
{}
auto TestAuto2(int a)
{}

问:为什么会存在这种限制呢?

答:因为我们要把相应的一些函数接口暴露出来给用户的,用户需要知道如何使用,需要传什么样的参数,需要什么样的变量来接收返回值,而且函数的参数和返回值都是auto的话,函数的可读性就不太好了,并且也会加大编译器的工作量。

  1. auto不能直接用来声明数组
void TestAuto()
{
	int a[] = {1,2,3};
	auto b[] = {4,5,6};
}
  1. 为了避免与C98中的auto发生混淆,C11只保留了auto作为类型指示符的用法
  2. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等 进行配合使用。

💞8.4 auto的意义

程序员不必再写出冗长的数据类型,只需要在类型的前面放置一个auto即可,减轻程序员的任务。

💓9. 基于范围的for循环

💞9.1 范围for的语法

在C++98中如果要遍历一个数组,可以按照以下方式进行:

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		array[i] *= 2;

	for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
		cout << *p << endl;
}

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中 引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
    //下面的auto是元素的类型,我们也可以将其写为int,因为数组元素都是int类型
	for (auto& e : array)//必须使用引用才能拷贝,因为e只是array数组中元素的临时拷贝
		e *= 2;

	for (auto e : array)
		cout << e << endl;
}
int main()
{
	TestFor();
	return 0;
}

范围for工作原理:依次取数组array中的元素,然后将其赋值给临时变量e(但是e的改变不会改变数组,除非使用引用),然后达到了遍历的目的。

注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
这种写法的意义是什么?

其一是方便书写,其二是方便修改代码。例如我们向数组中添加了几个元素之后,使用这种方法就无需修改循环次数,且不用书写冗长的sizeof(array)/sizeof(array[0])

💞9.2 范围for的使用条件

  1. for循环迭代的范围必须是确定的 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的 方法,begin和end就是for循环迭代的范围。
    注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
	for(auto& e : array)
		cout<< e <<endl;
}

函数传过去的实参只是一个地址,编译器无法知道我们想向后遍历多大的范围。

  1. 迭代的对象要实现++和==的操作。

💓10. 指针空值nullptr(C++11)

💞10.1 C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的 错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
	int* p1 = NULL;
	int* p2 = 0;

	// ……
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在 使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	return 0;
}

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,预处理之后NULL被替换成了0,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下 将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void )0
注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同*。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

注意:nullptr的本质就是(void*)0

💓11. const限定符再补充

💞11.1 const再补充

因为const对象一旦创建后其值就不能被改变,所以const对象必须初始化,初始值可以是任何复杂的表达式。

const int i = get_size();//正确,运行时初始化
const int j = 43;//正确,编译时初始化
const int k;//错误,k没有进行初始化

注意:默认状态下,const对象仅在文件内有效,所以当多个文件中出现了同名的const变量时,起始等同于在不同的文件中定义了独立的变量。

如果我们想让我们某个文件中定义的变量具有外部链接属性(在其它文件中可以使用),那么对于const变量的定义或者声明都添加extern关键字即可,这样只需要定义一次即可(即带有初始化的表达式出现一次即可)。

例如:

//file1.cpp
extern int a = 10;
//file2.cpp
extern int a;
//此时在file.cpp中就可以使用常量a,且其值为10

file.cpp头文件中的定义由extern做了限定,其作用是a并非本文件所独有,它的定义将在别处出现。

当然,像下面这样是不行的:

//file.h
extern int a = 10;
//file.cpp
extern int a;
//此时如果想在file.cpp中使用file.h中的a,就必须在file.cpp文件中包含file.h,为什么?因为头文件不参与编译,更不会参与链接

💞11.2 底层const和顶层const

问:什么是顶层const?什么是底层const?

答:顶层const指的就是指针常量(指针本身是个常量,指针指向不能改变),底层const指的就是常量指针(指针指向的常量不能被改变)。

下图可以方便理解:

💞11.3 constexpr和常量表达式

💘11.3.1 常量表达式

常量表达式是指不会改变并且在编译过程就能得到运算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

const int max_files = 20;//max_files是常量表达式
const int limit = max_files + 1;//limit是常量表达式,max_files + 1也是常量表达式
int staff_size = 27;//staff_size不是常量表达式
const int sz = get_size();//sz不是常量表达式,因为sz是在运行过程中得到的

💘11.3.2 constexpr变量(C++11)

在一个复杂系统中,我们很难分辨一个初始值是不是常量表达式,所以C++ 11新标准规定。允许将变量声明为constexpr类型以便由编译器来检验变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

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

一般来说,如果我们认定某个变量是一个常量表达式,我们就把它声明为constexpr类型。

💘11.3.3 字面值类型

常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。

算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。

尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制,一个constexpr的初始值必须是nullptr或者0,或者是存储与某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。

允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

💘11.3.4 指针和constexpr

在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针指向的对象无关。

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

与其它常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量。

constexpr int *np = nullptr;//np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42;//i的类型是整型常量
//i和j都必须定义在函数体之外
constexpr const int *p = &i;//p是常量指针,指向整型常量i
constexpr int *pi = &j;//pi是常量指针,指向整数j

相关文章