IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++学习】C++入门知识(下) -> 正文阅读

[C++知识库]【C++学习】C++入门知识(下)

🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
图

🍜引用

🍛引用的概念及使用

图
这个人都不陌生吧?他的名字叫李逵,在家称为"铁牛",江湖上人称"黑旋风"。虽然有多个称谓,但是都是在指他这个人。引用的意思和这相类似。

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

来看一段我们非常熟悉的函数:

图
这是交换两个数的函数,是通过指针的方式实现的,它的实质是在操作便a和变量b的内存空间,具体原理本喵不再啰嗦了。

我们换一种方式来写:

void Swap(int& ra, int& rb)
{
	int temp = ra;
	ra = rb;
	rb = temp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a, b);
	return 0;
}

tu
通过调试过程中的监视窗口,们发现,在Swap函数中的俩个变量ra,rb的地址和main函数中的变量a,b的地址是相同的。

也就是说,对ra和rb进行处理就相当于对a和b进行处理,这里也没有使用指针。

这就是引用。

图
通过这张图就能更加清楚的认识到引用,它就是给一个变量起个别名。

注意 :引用类型必须和引用实体是同种类型的。

引用的特性:

  1. 引用在定义时必须初始化,不能像变量那样只有定义。
  2. 一个变量可以有多个引用,也就是可以给它起多个名字。
  3. 引用一旦引用一个实体,再不能引用其他实体,一个引用只能作为一个对象的别名,不能指代另一个对象。

🍛常引用

所谓常引用就是用const修饰的引用变量,它和指针一样,涉及到一个权限的问题。

图
上图中的代码,变量a原本是被const修饰的,具有常量的属性,是不可以被修改的。

但是使用引用后,ra同样表示的是变量a,但是此时不被const修饰了,可以被修改了。

这就是权限的放大,原本是不可以被修改的,你给改了名字以后可以修改了,原本的变量肯定就不干了,所以在编译的时候会报错。

再看一段代码:

图

上图中的代码,变量a是没有被const修饰的,所以它是可以修改的。

使用引用后,ra表示的也是a,并且是被const修饰的,ra不可以被修改。

这就是权限的缩小,原本是可以修改的,你给改了个名字后不能被修改了,对原本的变量来说无所谓,反正不影响人家,你爱怎么样就怎么样,此时原本的变量a是可以修改的,并且ra也会跟着改变。

图

在没有执行到改变a之前,ra中的值是10。

图
在将a改变成20以后,ra中的值也跟着变了。
总的来说,权限可以被缩小,但是不可以被放大。

🍛引用的使用场景

做参数:

还是看上面的交换函数:

图
以前一直都是使用的Swap1的交换方式,就是将指针作为实参传给函数,函数的形参也是指针变量,接收传过来的地址,通过对形参指针的解引用可以访问到main函数中的变量a和b。

C++中使用引用,将变量a和b当作实参传给Swap2,也就是将变量a和b作为引用实体,函数的形参是int&的引用类型变量,此时x和y就成了变量a和b的别名,访问x和y也就是访问a和b。

当因为作为参数的时候,可以不使用指针,并且形参不开辟内存空间。

做返回值:

看代码:

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

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

这段代码很简单,就是将n加1并且返回来。

下面我们分析下它的底层调用过程:

图

  • 首先在栈区开辟了main函数的栈帧,并且在该栈帧中创建了int ret变量,此时ret的值是不确定的
  • 在执行到int ret = Count的时候,创建了另一个栈帧,函数Count的栈帧
  • 在执行Count韩式的时候,在静态区创建了变量n,并且初始化为0

上面是代码执行过程中变量以及栈帧的创建过程。

图
当执行完Count函数的时候,会创建一个临时变量(实质是一个寄存器),将要返回的值n放在临时变量中,就像图中的绿色小框那样,并且对应的Count函数的栈帧也被销毁了。

图

然后将临时变量n的值赋给ret,也就是放在了main函数栈帧中变量ret的内存空间中。

说明:

  • 调用的函数在返回值的时候,会将返回值放在一个寄存器中,也就是我们所说的创建了一个临时变量
  • 如果返回值占用的空间很大,那么就不用寄存器返回,而是在main函数的栈帧中再创建一个变量,将返回值提前放在这个临时变量中再销毁被调用函数的栈帧。
  • 在被调用函数的栈帧销毁以后,返回值时所使用的寄存器或者是临时开辟的变量空间都会还给操作系统。

上面所演示的是传值返回,也就是我们一直使用的方式,接下来看C++中的引用返回。

仍然是上面代码,只是将函数的返回类型改成了引用。

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

图
函数栈帧以及变量的创建和之前是一样的,但是在返回的时候:

图
返回的是变量n的别名,假设这个别名叫做temp(实际上没有),这个别名temp不开辟内存空间,并且于变量n表示的是同一块空间。

图
此时ret拿到的值同样是静态区中变量n的值。

不同点:

虽然最终都返回值,但是:

  • 引用返回的过程并不创建临时变量,而是直接返回原本变量所在内存空间中的值。
  • 传值返回的过程会创建临时变量,返回的是原本变量拷贝在临时变量空间中的值。

由于调用函数中的变量是创建在静态区的,所以传值返回和引用返回都一样,用哪个都行,如果被调用函数中的变量不是在静态区或者堆区,而是就在该函数的栈帧中呢?此时引用返回的结果是什么?

图
此时函数栈帧的创建还是和之前一样的,但是变量n是创建在Count函数的栈帧中的。

图
当Count函数调用结束时,函数的栈帧被销毁,在返回n的时候,因为采用的是引用返回,所以生成一个临时的别名,这个别名不开辟内存空间,表示的也是原来的变量n,如图中的红色方框n那样。

  • 此时,原本Count函数栈帧中的变量n所在的空间已经还给了操作系统
  • 但是引用返回的值仍然是空间n中的值,造成了非法的访问。

图
但是此时的结果还是1,这是为什么呢?

原因是,虽然变量n所在的内存空间返还给了操作系统,但是它里面的值没有被覆盖,所以还是能够访问到它的值。

图
如果将程序改成这样,第二次得到的值就不是1了,本喵来给大家分析一下原因。

图

  • 变量ret不是引用变量时,它在main函数的栈帧中开辟了空间,
  • 所以在Count函数调用结束时,将引用返回的值存放在了ret在main函数栈帧的空间中
  • 即时原本Count栈帧所在的位置用来创建cout函数的栈帧后
  • 每次打印都是访问的ret中的值,所以此时的结果是:

图
当结束函数引用返回的值也使用引用的时候:

图

  • ret始终都是原本count中变量n的别名
  • 当Count栈帧销毁时,ret拿到的是没来得及改变的n的值
  • 当该空间被cout的栈帧占用后,原本变量n所在的位置就被其他内容所占据
  • 所以打印出来的数据就不再是1了

结合上面的分析,对应引用返回我们可以得出一个结论:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

🍛传值、传引用效率比较

#include <time.h>

struct A 
{ 
	int a[10000];
};
A a;
// 值返回
A TestFunc1() 
{ 
	return a;
}
// 引用返回
A& TestFunc2() 
{ 
	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();
	// 计算两个函数运算完成之后的时间

	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

int main()
{
	TestReturnByRefOrValue();
	return 0;
}

图
上面代码的意思就是,创建一个结构体变量,该结构体中有一个10000个int数据的数组,通过传值返回和引用返回俩种方式接收这个数组,看这俩种方式哪种用的时间短,也就是哪种方式效率高。

  • 由运行结果明显可以看出,引用返回的效率高于传值返回的效率
  • 根据前面函数栈帧调用的分析,我们知道,传值返回需要创建一个临时变量,如果返回的数据所在空间太大,就会在main函数的栈帧中创建临时变量数组
  • 而引用返回不需要创建任何形式的临时变量,直接给原本的数据起一个别名,表示的还是原本的数据
  • 我们知道,系统在创建变量的时候是有消耗的,也就是需要消耗一定的时间

综上我们可以得出:传值和引用在作为传参以及返回值类型上效率相差很大

🍛引用和指针的区别

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

图
可以看到,它们俩个地址是一样的,引用并没有创建新的变量空间。

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

图
从汇编中我们可以看到,创建引用变量ra,以及给ra赋值是在红色框内,创建指针百年来pa,和通过指针给变量a赋值是在绿色框内。

  • 红色框和绿色框中代码的作用是相同的,都是将变量a的值改为20
  • 而且它们的汇编代码也是相同的,说明引用和指针的底层实现是相同的。

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用在定义时必须初始化,指针没有要求

  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  4. 没有NULL引用,但有NULL指针

  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  9. 引用比指针使用起来相对更安全

🍜内联函数

概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

先实现一个加法函数:
tu
对应程序的汇编代码我们可以看到,这里明显的有一个函数调用的过程,这个过程会建立函数栈帧。

再用宏写一个加法函数:

图
可以看到,汇编代码中,并没有调用函数,而是将宏在红色框的位置展开了,没有建立函数栈帧。

其实,内联函数和宏定义的函数是一样的,同样不会建立函数的栈帧,而是会在调用的位置展开:

图

可以看到,在函数Add前面加了inline后,Add函数就成了内联函数,并且汇编代码冲同样没有调用Add函数,而是将其在调用的位置展开了,没有建立栈帧。

我们知道,宏定义的函数有很多的缺陷,比如:

  • 不能进行调试,因为在预处理的时候就会替换掉
  • 很容易写错,需要加很多的括号
  • 没有类型的检测,使用起来不安全

在今后,遇到高频调用的小函数时,我们就可以用内联函数来代替宏定义。

内联函数的特性:

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率,因为没有建立函数栈帧。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

图

  • 试想以下,这里有一个函数,函数中有100行代码,调用它的地方有10000处
  • 如果不使用内联函数,代码会有10000+100行代码,其中10000是调用它的语句,100是函数中的语句
  • 如果使用内联函数,则代码会有10000*100 = 100w行代码,此时代码量就会急剧膨胀,所以编译器会自动避免这种不合理的请求。
  1. nline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

图
在头文件中,函数的声明处用了inline,声明Add是内联函数,在调用Add函数的时候报错了,找不到Add函数,这是什么原因呢?

图
继续看汇编代码,在调用Add函数的时候,我们看到函数名后面的括号中有一个地址。

本喵在文章程序环境和预处理中曾详细讲解过,编译分为预处理,编译,反汇编三个阶段,在预处理的时候,将Add.h中的内容展开复制到了源文件中,此时源文件成了这样:

图

在编译阶段,会形参符号表,因为函数Add被inline修饰,所以编译器不认为它是一个符号名,所以就不会形参符号表,也不会给Add分配地址,编译器会在调用它的地方展开它。

在整个编译阶段完成以后,会进行链接,此时test.cpp和Add.cpp等源文件会链接到一起,链接的时候会去Add符号名所在的地址处寻找函数Add进行调用。

但是,因为加了inline,编译器并没有给它分配地址,所以在链接的时候就找不到对应的Add函数,所以就会报链接错误。

为了避免这种问题的出现,通常不把内联函数的定义和声明分开,直接在定义的时候使用inline修饰即可,因为内联函数通常也比较短,所以就写在调用它的函数的前面即可。

🍜auto关键字(C++11)

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写
  2. 含义不明确导致容易出错

你是否觉得类型有什么复杂的,自己写的程序难点还能写错码?

看一下下面的代码:

#include <string>
#include <map>

int main() 
{
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" },{"pear","梨"} };    
	std::map<std::string, std::string>::iterator it = m.begin();    
	while (it != m.end())
	{ 
		//....    
	}    
	return 0;
}

其中std::map<std::string, std::string>::iterator是一个类型,但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:

typedef std::map<std::string, std::string> Map;

这确实是个好办法,但是我们常常需要将值赋给变量,这时就需要判断要赋的值是什么类型,才能创建相应的变量,有时候要清楚的知道值的类型可并不是一件容易的事情。

所以C++11给auto赋予了新的含义,

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

也就是,auto修饰的变量需要编译器自己去判断该变量是什么类型,而不再需要我们去判断,让编译器给我们干活,想想是不是很爽?

int Testauto()
{
	return 10;
}

int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = Testauto();

	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

tu
看到了吗?我们这里并没有声明变量b,c,d是什么类型,只是用auto修饰了变量名,在打印它们的类型的时候,准确的打印出了它们的类型。

注意:

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

🍛auto的使用细则

  1. auto与指针和引用结合起来使用
int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;

	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;

	return 0;
}

tu
根据结果可以看到:

  • auto声明指针类型的时候,有没有* 都是一样的,编译器都会认为auto和auto*修饰的变量是指针类型
  • auto修饰指针的时候,必须使用&符,因为int&并不是一种类型,仅仅代表这是一个引用。
  1. 同一行定义多个变量

图
可以看到这里有报错。

  • 红色框中的是正确的,这一行的变量都是int类型的,所以auto成功的推断出了它们的类型
  • 绿色框中的是错误的,因为这一行的变量中有int和double类型,不是相同的类型。
  • 当用auto定义一行多个变量的时候,变量的类型必须都是一致的,因为auto只会根据第一个值的类型来推断变量的类型。

auto也有不能推断出来的情况:

  1. auto不能作为函数的参数

图
这里直接就会报错,因为编译器并不能推断出函数参数的类型,因为不能确定传过来的参数是什么类型,本例中实参是int类型,那如果又是char类型呢?

因为在执行程序的时候,Add函数在创建栈帧的时候并不知道实参是什么类型,所以就不能够推断出形参的类型。

  1. auto不能直接用来声明数组

图

为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。

🍜基于范围的for循环(C++11)

变量一个数组,在之前我们只能是这样来实现的:
tu
只能通过这俩中中的一种来实现,现在本喵再告诉大家一种做法:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	for (auto e : arr)
	{
		cout << e;
	}
	cout << endl;
	return 0;
}

tu
可以看到,同样实现了数组的循环打印。

不仅可以打印,还可以改变数组中的元素:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	for (auto& i : arr)
		i *= 2;
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

tu
可以看到,此时数组中的数据都扩大了二倍。

我们这里使用的就是auto关键字,他不仅能够推断数据类型,还可以进行基于范围循环。

注意:

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

图
像此时就不可以进行基于范围的循环,因为这个范围并不确定。

🍜指针空值nullptr(C++11)

  • 在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针,就会造成野指针的错误。

如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

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

在C语言的代码中,我们经常使用到NULL空指针,实际上NULL是一个宏

#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,因此与程序的初衷相悖。

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

因为存在着上诉的各种问题,所以在C++11中引入了nullptr表示空指针。

它和以前NULL的用法是一样的,但是避免了在调用重载函数的一些问题,所以我们在今后使用空指针的时候就使用nullptr。

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

🍜总结

通过C++入门知识(上)和这篇文章,我们已经了解了C++基于C的不足而新增加的一些基本语法,有了这些知识,意味着已经过了C++的新手村,就可以去打后面的类和对象等内容的副本了。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 00:33:07  更:2022-09-30 00:33:32 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 3:16:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码