【C++进阶之路】初始C++语法(下)

一.引用

  • 指针是如何访问或者修改变量的?
#include<stdio.h>
int main()
{
    
    
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 1;
	printf("%d\n", a);
	return 0;
}

这里的ppa是二级指针,通过解引用访问一级指针,再对一级指针解引用访问变量a的空间,将其赋值为1.
图解:
在这里插入图片描述

  • 指针是如何访问或者修改变量的?

我们使用多级指针访问变量是否麻烦呢
答: 可能是在某些特定的场景下麻烦,因此出了引用。

1.基本概念

  • 引用不是新定义一个变量,而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间
  • 例如: 你的本名是你,你的外号也是你,或者水浒传的李逵,外号是“铁牛” 和“黑旋风”,但都是李逵

代码:

#include<iostream>
using std::cout;
using std::endl;
int main()
{
    
    
	int a = 0;
	int& b = a;//为了不增加符号负担,选取&作为取别名
	b = 1;
	cout << a << endl;
	return 0;
}

图解:a与b公用一块空间
在这里插入图片描述
注意细节:

  1. 取别名语法规定必须设置初始值

错误示例:

#include<iostream>
using std::cout;
using std::endl;
int main()
{
    
    
	int a = 0;
	int& b;
	return 0;
}

2.别名与原变量名共用一块空间

证明:

#include<iostream>
using std::cout;
using std::endl;
int main()
{
    
    
	int a = 0;
	int& b =a;
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}
  1. 别名一经设置不能被修改!

注意代码解读:

int main()
{
    
    
	int a = 0;
	int& b = a;
	int x = 2;
	b = x;//这里是将x的值赋值给a变量,而不是给x取别名为b!
	return 0;
}
  1. 一个变量可以有多个别名,对别名也可以取别名

举例:

int main()
{
    
    
	int a = 0;
	int& b = a;
	int& c = a;
	int& d = b;//对别名取别名还是a变量
	return 0;
}

关于单链表中引用的使用:

typedef struct SListNode
{
    
    
	int val;
	int* next;
}SListNode,*PNode;
//这里的PNode即为SListNode*
void SListPushBack(PNode& p, int x)//相当于对一级结构体指针取别名
{
    
    
	//……功能省略
}
int main()
{
    
    
	SListNode* head;
	SListPushBack(head, 1);
	return 0;
}

2.使用场景

函数参数

#include<iostream>
using std::cout;
using std::endl;
void Swap(int& n1, int& n2)//传进去相当于对原变量取别名
{
    
    
	int tmp = n1;
	n1 = n2;
	n2 = tmp;
}
int main()
{
    
    
	int x = 2;
	int y = 1;
	cout << "before " << x <<" "<< y << endl;
	Swap(x, y);
	cout << "after " << x <<" "<< y << endl;
	return 0;
}
  • 这里的交换两个值实际上就是通过对原变量取别名从而将原变量的值进行交换,不过省去了指针的使用,这样很方便。

函数返回值

  • 先引出我们的整形变量作为返回值
int add()
{
    
    
	int n = 0;
	 n++;
	return n;
}
int main()
{
    
    
	int ret = add();

	return 0;
}
  • 在栈帧返回时,将n的临时拷贝放在eax寄存器中返回,eax寄存器并不会随着栈帧的摧毁而销毁,因此再将eax的值赋值给ret,完成赋值。
  • 因此:这里的返回值其实是n的一份临时拷贝。
int& add()
{
    
    
	int n = 0;
	 n++;
	return n;
}
int main()
{
    
    
	int ret = add();//这里的add()可看做n变量的别名

	return 0;
}
  • 这里调用函数的返回的结果是n的别名,通过别名访问n的值,但是在返回的途中栈帧已经被销毁,使用权已经返还给操作系统,因此这里访问其实是很危险的,这跟野指针是一样的道理,我们无法确定访问到是否是我们想要的值,如果侥幸访问拿到的是我们想要的值,但如果我们在此代码的基础上稍加修改。
  • 因此:这里返回的是n的别名。
#include<iostream>
using std::cout;
using std::endl;
int& add()
{
    
    
	int n = 0;
	 n++;
	return n;
}
int main()
{
    
    
	int& ret = add();
	//add()=1;
	cout << ret << endl;
	printf("hello\n");//这个函数会破坏原来add()的栈帧。
	cout << ret << endl;
	return 0;
}
  • 原来的栈帧被printf调用,破坏了原来的未被修改的栈帧,因此这里打印的值是不确定的!
  • 因此:使用取别名当返回值时,使用应不是栈上开辟的变量。
  • 此外:返回值作为别名可以直接进行修改!

const的使用

  • const修饰的变量表示只读状态不可被修改

可这样对一个常量能取别名吗?

int main()
{
    
    
	int& b = 1;
	return 0;
}
  • 这样表示其实是对1取别名,并且其值可被修改,因此语法错误。

如何正确的对一个常量取别名?

int main()
{
    
    
	const int& b = 1;
	return 0;
}
  • 这样表示其为只读状态,因此不可被直接修改,因此表示正确。

应用于变量中
码1:

int main()
{
    
    
	int a = 0;
	const int& b = a;
	a++;
	//b++;
	return 0;
}

  • 这样指的是对a变量的取一个别名,这个别名只可读不可访问,就像VIP与普通用户的区别,而a变量可访问也可修改。
  • 因此:对变量取别名,使用权限是可以缩小的。

码2:
这样可以吗?

int main()
{
    
    
	const int a = 0;
	int& b = a;
	return 0;
}
  • 显然跟常量是一样的,a是只读状态,因此对a取别名也应该是只读状态,所以b的前面应该加上const进行修饰
  • 因此:访问权限不能扩大

码3:

int main()
{
    
    

	int a = 0;
	float b1 = 11.0f;
	a = b1;//这里的b1其实是隐式类型转换的结果,将b1值转换成浮点数,这里的b1是浮点数的常量
	int& a1 = a;
	const float& b = a;
	//同理这里的a也是隐式类型转换的结果,既然是结果那就是float类型的常量,常量就是只读状态
	//因此是要加const 并且是float类型的常量
	printf("%d %f", a, b);
	return 0;
}
  • 因此:变量是不能起不同类型的别名的,我们看到的其实是隐式类型转换后的不同类型的常量.
  • 总结:访问权限不能扩大,只能平移或者缩小。
  • 补充:const修饰相同类型,可以构成重载,属于类型重载
void Func(int year)
{
    
    

}
void Func(const int year)
{
    
    

}
//这里是构成重载的,不过会存在调用不明确的现象!

3.优点

  • 作为函数参数,实参过大时,取别名会降低传参的成本,因而提升效率
  • 作为返回值,可以当做别名直接进行修改,返回值过大也可降低拷贝,从而提升效率

4.指针与引用的区别

int main()
{
    
    
	int a = 0;
	int* pa = &a;
	int& b = a;
	return 0;
}

反汇编代码:
在这里插入图片描述

  • 说明:lea是取地址的意思
  • 第一句:将0赋值给a地址处向上的4个字节
  • 第二句:对a取地址将其放在eax寄存器中
  • 第三句:将eax的内容放在pa地址处向上的4个字节
  • 第四句:对a取地址将其放在eax寄存器中
  • 第五句:将eax的内容放在b地址处向上的4个字节

  • 因此我们可以得到在反汇编的角度(底层原理)指针和引用是没有区别的,同样是要开辟空间的。
  • 那为什么我们要说引用不等于指针呢?
  • 在语法层面上, 我们经过实验可知(上文),引用也就是取别名的地址是跟原变量的地址保持一致的,而C语言则是跟底层原理保持一致的
  • 因此:C++将底层的一些东西 进行封装,在语法上呈现给我们的是上层的东西,可知上层跟下层还是有些许不同的。

二.内联函数

引入

#include<iostream>
using std::cout;
using std::endl;
int add(int x,int y)
{
    
    
	return x + y;
}
int main()
{
    
    

	for (int i = 0; i < 1000; i++)
	{
    
    
		cout << add(1, i) << endl;
	}
	return 0;
}
  • 如何优化这一段代码?这段代码为什么需要优化?

  • 学过C语言的你想必会使用宏函数进行优化,那为什么要这样优化呢?学过函数栈帧其实就很简单,函数调用是需要调用函数栈帧的,当栈帧的空间大于我们有效代码的空间时,使用函数的负担就会较大

  • 如何进行优化?

#include<iostream>
using std::cout;
using std::endl;
#define ADD(x,y) (((x)+(y))*10)//ADD建议全大写这可是程序员之间的悄悄约定过的哦
int main()
{
    
    

	for (int i = 0; i < 1000; i++)
	{
    
    
		cout << ADD(1, i) << endl;
	}
	return 0;
}
  • 那这样优化有什么好处和坏处呢?
  • 好处:直接进行替换,减小了栈帧的消耗,并且不要求类型,提升了运行的速度。
  • 坏处:如果对宏不熟练,写这个宏极易容易出错,这里的括号是不能省的,仔细想想为什么?不便于调试。副作用极大。

为了解决C语言的缺陷,所以设计者在C语言的基础上设计出了内联函数
用法:只需在函数的返回值类型前+inline即可,本质上是有效指令的替换.

  • 有这么简单么?当然没有

1.默认设置

#include<iostream>
using std::cout;
using std::endl;
inline int add(int x,int y)//这里加了inline
{
    
    
	return x + y;
}
int main()
{
    
    

	for (int i = 0; i < 1000; i++)
	{
    
    
		cout << add(1, i) << endl;
	}
	return 0;
}

汇编指令:
在这里插入图片描述

  • 结果:我们在调试阶段去调用了add函数,而没有把函数的指令放在这里,我们写错了吗?

  • 当然没有,这是在VS2019的默认值,在调试不会将指令进行替换。

  • 怎么设置?
    第一步:
    在这里插入图片描述
    第二步:

在这里插入图片描述
第三步:
在这里插入图片描述
看设置之后的反汇编代码:
在这里插入图片描述

  • add函数的指令变成了这样就是add函数的功能。

2.建议

  • 试想:我们如果写一个函数具有一百行代码,调用一千次,如果使用内联会产生怎么样的结果?或者说产生多少条指令?
  • 答案:100*1000=10W行指令,代码是不是爆炸了?
  • 因此:编译器会把决定权交给你么?当然不会。
  • 因此:我们写内联函数只是起建议的作用。

如何证明:

#include<iostream>
using std::cout;
using std::endl;
#define ADD(x,y) (((x)+(y))*10)
inline int add(int x,int y)
{
    
    
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	cout << "hello" << endl;
	return x + y;
}
int main()
{
    
    

	for (int i = 0; i < 1000; i++)
	{
    
    
		cout << add(1, i) << endl;
	}
	return 0;
}

再次查看反汇编代码:
在这里插入图片描述

  • 此时又变成了函数的调用。

联系:register 也不会把是否放在寄存器的决定权交给你,只是起建议的作用。
补充:在类内里面,函数会自动优化短的函数是内联。因为类里面定义默认内联。

3.声明与定义(不同文件)

add.cpp

inline int add(int x, int y)
{
    
    
	return x + y;
}

add.h

inline int add(int x, int y);

test.cpp

#include<iostream>
#include"add.h"
using std::cout;
using std::endl;
int main()
{
    
    
	for (int i = 0; i < 1000; i++)
	{
    
    
		cout << add(1, i) << endl;
	}
	return 0;
}
  • ctrl+F7编译一下
    在这里插入图片描述
    我们发现是没有问题的。

  • 执行一下
    在这里插入图片描述
    链接错误

  • 原因:链接时做了生成段表(主要为函数和全局变量的地址)的工作,而内联函数的本质是替换(其地址是不会放在段表中的),当你在链接时进行查找时会发现在头文件中只有声明,但是定义不在头文件中,而在源文件中,这时你要去源文件中查找,但是段表里面并没有这个函数的地址,因此找不到所以会报错!

  • 因此:要么在头文件中写内联函数的定义,要么就直接写在主函数所在的源文件中,并且在不同源文件中定义相同的内联函数,是不会报错的,因为就不会在段表里出现这几个函数!

  • 拓展:本质上代码的运行都会化作二进制指令的执行,计算机将指令加载到内存中,每条指令都有一条特定的地址,常规函数的调用会指行函数调用指令,程序将在函数调用后立刻存储该指令的内存地址,并将函数的参数拷贝到堆栈,跳到目标函数的起点内存单元执行代码,然后跳回地址被保存的指令处,这来回调用会存在一定的开销

三.auto

C语言的auto的作用是修饰声明周期和作用域都是局部的的变量,但基本上没什么用,所以在C++上改进了一下——自动识别(变量的数据)然后定义其类型。

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

1.必须有初始值

  • 识别的依据是初始化的值,编译器是先推导初始化的值判断变量类型,然后把数据放在该判断好的类型中,因此初始化的数据是auto的前提
int main()
{
    
    
	auto x = 1;
	int y = 1;
	float z = 1.0;
	auto n = 1.0f;
	//auto w ;这句代码是不对的
	return 0;
}

2.只能确定一个类型

  • 当一行有多个数据时,如果用auto,这些数据类型必须是相同的,否则会出现无法判断的情况。
int main()
{
    
    
	int x = 0, y = 1.0f;//这里可是发生了隐式类型转换
	auto x = 0, y = 1;
  //auto x =0,y = 1.0f;这行代码会报错
	return 0;
}

3.auto可以为类型的一部分

  • 一级指针的类型可以拆分为:指向数据的类型+ *
  • 那这里指向的数据的类型就可以用auto来表示
#include<iostream>
#include"add.h"
using std::cout;
using std::endl;
int main()
{
    
    
	int a = 0;
	auto* p = &a;
	cout << typeid(p).name() << endl;//这里打印的就是int*
	cout << typeid(a).name() << endl;
	return 0;
}

4.函数参数和自定义类型不可识别

  • 函数参数如果具有自动识别,那函数的类型识别就丧失了
  • 自定义类型是无法让编译器进行推导的,因此无法识别
  • 自定义类型包括:数组,联合体,结构体。

错误示例——自定义类型:

typedef struct Student
{
    
    
	int age;
	char name[20];
	int Id;
}Student;

int main()
{
    
    
	auto x = {
    
     18,"shunhua",11111111 };//无法识别
	return 0;
}

错误示例——函数参数

int add(int x, auto y)
{
    
    
	return x + y;
}

5.auto可对类型取别名,但&不可省

  • auto+&本质上是引用,不是类型,所以取别名时必须加&
int main()
{
    
    
	int a = 0;
	auto& x = a;
	
	return 0;
}

6.语法糖——for+auto

打印数组

int main()
{
    
    
	int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	for (auto x : arr)
	{
    
    
		cout << x << endl;
	}
	return 0;
}
  • 这句代码的意思为:依次取出arr数组中的元素,将其放在x的临时变量中。从起始位置读取直到数组读取结束。

修改数组

int main()
{
    
    
	int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	for (auto& x : arr)
	{
    
    
		cout << x << endl;
	}
	return 0;
}
  • 我们可以使用取别名的形式进行修改。

数组传参传的是数组首元素的地址

void Print(int arr[10])
{
    
    
	for (auto x : arr)//这里是指针不是数组因此语法报错
	{
    
    
		cout << x << endl;
	}
}
int main()
{
    
    
	int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	Print(arr);
	return 0;
}

四.空指针

#include<iostream>
using std::cout;
using std::endl;
int main()
{
    
    
	auto x = 0;
	auto y = NULL;
	auto z =(void *)NULL;
	auto n = nullptr;

	cout << typeid(x).name() << endl;
	cout << typeid(y).name() << endl;
	cout << typeid(z).name() << endl;
	cout << typeid(n).name() << endl;

	return 0;
}

运行结果:
在这里插入图片描述

  • NULL的定义

在这里插入图片描述

  • 结论:在C++下NULL是被当做整形0来看待的,并且引出了一个新的关键字nullptr,其类型为std::nullptr_t。
  • nullptr在被赋值给其它类型时,会默认转换为其他指针类型的值,但不会转换为任何整数类型!

猜你喜欢

转载自blog.csdn.net/Shun_Hua/article/details/130229333