C++入门【上节】

C++入门【上节】,第1张

文章目录
    • 前言
  • 一、C++关键字(C++98)
  • 二、namespace关键字(命名空间)
    • 知识点
    • 命名空间定义
      • 1、正常定义
      • 2、嵌套定义
      • 3、命名空间的合并
    • 命名空间的使用
      • 方法一
      • 方法二
      • 方法三
  • 三、C++输入/输出
    • 知识点
  • 四、缺省参数
    • 缺省参数分类
      • 1、全缺省参数
      • 2、半缺省参数
      • 缺省参数使用案例
    • 知识点
  • 五、函数重载
    • 参数个数不同
    • 参数类型不同
    • 形参类型顺序不同
      • 案例
    • C++支持重载的原理(重点)
    • 返回值不同为什么不能够成重载?(面试题)
  • 六、引用
    • 1、引用特性
    • 2、使用场景
    • 3、传引用做返回值的作用
    • 4、传值、传引用做参数
    • 5、常引用
      • 注意
      • 补充知识点
    • 6、引用和指针的区别
  • 总结


前言

本节知识点安排目的

  1. 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
  2. 为后续类和对象学习打基础。
一、C++关键字(C++98)

C++总计63个关键字,C语言32个关键字

ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再
细讲。


我们先来看看C++如何打印出hello world!

#include//IO流
using namespace std;//命名空间namespace
int main()
{
	cout << "hello world!" << endl;
	return 0;
}


{
	std::cout<<"hello world!"<<std::endl;
	return 0;
}

这两种方法都可以打印出hello world!,方法二没有加using namespace std;这一段代码也打印出来了

上面所包含的头文件(iostream)叫做IO流,namespace叫做命名空间

我们先来看看C++为什么要有namespace命名空间呢?
我们先来看一段c代码

#include
int rand = 0;
int main()
{
	printf("hello world!\n");
	printf("%d\n", rand);
	return 0;
}


我们可以看到这一段代码没有什么问题,那如果再加上一些代码呢?

#include
#include
int rand = 0;
int main()
{
	printf("hello world!\n");
	printf("%d\n", rand);
	return 0;
}


大家会发现程序崩溃了,这是因为stdlib里面有一个rand函数,而函数名与全局变量rand重复了,所以rand就是被重命名了。而这种重命名定义C语言是无法解决的,只能换一个变量名

那么如果我们以后工作在提交项目时,如果我们和其他人的项目里面有这种重定义现象很麻烦,只能某一方修改变量名

所以,C++为了解决上面这个问题引入了命名空间


二、namespace关键字(命名空间)

namespace作用:避免了命名冲突

#include
#include

namespace xxx//命名空间把这个rand包围起来了
{
	int rand = 100;
}
int main()
{
	printf("hello world!\n");
	printf("%p\n", rand);//此时访问rand默认是库函数里面的

	printf("%d\n", xxx::rand);//这个时候就是访问命名空间里面的rand
	return 0;
}
知识点

::这个符号叫做域作用限定符,可以用来访问命名空间里面的内容
<< 这个符号叫做流插入,可以识别数据的类型

std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中

1、默认查找:程序在查找变量时优先在局部域查找,找不到再去全局域查找,默认情况下程序不会去命名空间域查找变量

2、指定查找:比如我们上面的代码std::cout<<"hello world!"<直接去std这个命名空间域里面找cout和endl,如果命名空间域没有变量会直接报错

3、命名空间域里面的数据是存放在全局域的,只不过命名空间将这些数据给围起来了,我们在命名空间域外面定义的变量名这个时候和内部变量名即使重名也不会起冲突

4、命名空间不影响变量生命周期,只是限定域,改变编译查找规则

5、C/C++的头文件会在预处理阶段展开,展开后的代码全部存放在全局域当中

6、局部变量定义只能在函数内部,因为函数调用的时候会形成函数栈帧,函数调用结束函数栈帧销毁,此时局部变量也跟着销毁,所以在栈帧上的变量叫做局部变量

下面图中::(域作用限定符)左边是空白,表示在全局域中查找变量a


打印第一个rand因为没有使用域作用限定符,所以先在局部域查找变量rand,没有找到再在全局域找rand(这个时候因为rand是头文件展开之后的一个函数,所以rand是函数名,是一个地址,打印出来的就是rand函数地址)

所以namespace命名空间可以将我们所需要的变量名包围起来,使得库函数里面的函数名等等不会与我们需要的变量名起冲突

那么上面using namespace std;
这一段代码是C++为了方便把C++标准库的东西放进std命名空间里面,我们要使用的时候引入这一段代码就可以直接使用命名空间里面的东西;如果不加代码就要使用std::来告诉系统,我们要访问命名空间里面的内容

#include
using namespace std;
int main()
{
	int a = 10;
	char b = 'a';
	float c = 12.13;
	double d = 3.1415;
	cout << a << " " << b << " " << c << " " << d << endl;
	return 0;
}

命名空间定义 1、正常定义

命名空间中可以定义变量/函数/类型

namespace bit
{
	int rand = 10;
	int Add(int left, int right)
	{
		return left + right;
	}
	struct Node
	{
		struct Node* next;
		int val = 10;
	};
}
namespace byte
{
	int Add(int left, int right)
	{
		return left * 10 + right * 10;
	}
	struct Node
	{
		struct Node* next=NULL;
		struct Node* prev=NULL;
		int val=100;
	};
}
int main()
{
	printf("%d\n", bit::Add(1, 2));
	printf("%d\n", byte::Add(1, 2));

	struct bit::Node cur;
	struct byte::Node tail;
	return 0;
}

这里结构体要使用域作用限定符的话要放在struct后面

2、嵌套定义
namespace N1
{
 int a;
 int b;
 int Add(int left, int right)
 {
     return left + right;
 }
 namespace N2
 {
     int c;
     int d;
     int Sub(int left, int right)
     {
         return left - right;
     }
 }
}

这里命名空间嵌套使用的话就使用两个::就可以。而且嵌套定义的命名空间域名字可以相同,也就是上图中N2可以改为N1

3、命名空间的合并

我们前面认识了std这个C++官方库内容定义的命名空间域。那么,C++那么多内容会放在一个文件夹或者同一块空间里面吗?

这是不可能的,C++库里面的内容太多了,是放到多个文件夹里面的,那么既然是多个文件夹,多个命名空间域为什么一个std命名空间域就解决了呢?

这里就提出来了命名空间是可以合并的概念

同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

比如下面的示例:
ps:一个工程中的1.h和上面2.h中两个N1会被合并成一个
1.h

namespace N1
{
	int Add(int left, int right)
	{
		return left + right;
	}
}

2.h

namespace N1
{
	int Mul(int left, int right)
	{
		return left * right;
	}
}

test.cpp

#include"1.h"
#include"2.h"

int main()
{
	printf("%d\n", N1::Add(10, 20));
	printf("%d\n", N1::Mul(100, 20));
	return 0;
}

这里把Add和Mul都合并到N1里面去了

注意:只有同级命名空间才能合并在一起。并不是合并文件,而是展开头文件1.h和2.h时,合并命名空间
其次,合并的时候,命名空间里面不能存在变量名相同,比如将上面的Mul改成Add就会报错,这就相当于我们在同一个域里面定义了两个相同的变量名。

补充:同一个空间的命名空间也会合并:

这里将头文件展开不就是将1.h和2.h的内容放在test.c的文件里面了吗,所以是可以合并的

命名空间的使用 方法一
int main()
{
	std::cout << "hello world!" << std::endl;
	std::cout << "hello world!" << std::endl;
	std::cout << "hello world!" << std::endl;
	return 0;
}

我们知道std命名空间域里面有许多C++官方库内容,其中cout和endl就在里面,当我们要多次打印的时候每次都要去指定域std里面找太麻烦了,每次都要写std::cout和std::endl,太浪费时间了,所以就有了方法二

方法二
using namespace std;
int main()
{
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	return 0;
}

我们前面见识到了,加上using namespace std;这一行代码就不需要指定命名空间域,不用一直写std::了

原因就是:std命名空间域里面有着许多C++官方库内容,而using namespacestd;这一行代码把这个std命名空间域给展开了,那么没有了这一层域的保护,我们可以随意使用cout、endl等等C++官方库内容

但是有利就有弊:我们的确是方便了,但是又回到了C语言的缺点处:std域既然展开了,那万一我们定义了一个全局的cout、endl或者其他名字的变量,不是又和库起冲突了吗?

如果是cout和endl还好,但是如果是一个我们不认识的名称,那就太麻烦了,所以我们有了第三种命名空间使用方法

方法三

我们可以将常用的函数或者变量给解封,让这些内容不在std域不就行了?

using std::cout;
using std::endl;
int main()
{
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	return 0;
}


这里就把cout和endl拿到std域外面了,我们定义变量名的时候注意即可。如果定义了一个全局变量cout或者endl,我们可以把任意一个变量名放到命名空间内部,这样就有后退的余地了


三、C++输入/输出

我们前面认识了cout,这个函数在std域里面,起到输出作用,并且可以自动识别类型;也认识了<<流插入限定符(重载运算符,后面会讲)这个符号
那么既然有输出函数就有输入函数,有<<符号就有>>符号

C++的输入函数是cin

using std::cout;
using std::cin;
using std::endl;
int main()
{
	int a;
	cin >> a;
	cout << a << endl;
	return 0;
}

这里cout和cin分别是consoleout和consolein的简称,我们暂时讲不清楚这两个函数,只需要记住一个是输入一个是输出函数,后面会细讲;

当然,不是所有情况下cout和cin很方便,比如下面两种情况:

情况1:打印浮点数,保留小数点后n位,这个cout可以实现,但是太麻烦了,大家直接使用C语言的方法来 *** 作,因为C++兼容C

using std::cout;
using std::cin;
using std::endl;
int main()
{
	double a;
	cin >> a;
	printf("%.3f\n", a);
	return 0;
}


这里发生了精度损失,精度损失不一定是数值变小,还可以是数值变大

情况2:字符串和数值结合起来打印

using std::cout;
using std::cin;
using std::endl;
int main()
{
	int a;
	double b;
	cin >> a;
	cin >> b;
	cout << "int " << a << " double " << b << endl;
	printf("int %d double %f", a, b);
	return 0;
}


这里熟好孰坏,直接就看出来了,明显C语言更方便

输出a的ASCLL码值:

using std::cout;
using std::cin;
using std::endl;
int main()
{
	char a;
	cin >> a;
	cout << (int)a;
	return 0;
}


这里强转一下就可以了

知识点

1、 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。

2.、cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。

3、<<叫做流插入限定符,>>叫做流提取限定符,两个都是重载运算符

4、对比于C语言的printf和scanf,C++的cout和cin可以自动识别类型(在编译阶段实现自动识别类型),很方便(怎么自动识别类型的下面我们就会讲到)

5、实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。

补充:注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式。


四、缺省参数

概念:

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参

也就是我们可以给形参一个值,我们可以称这个值是形参的缺省值。如果调用函数没有传参,那么形参就使用缺省值;如果有传参,那么就使用传参值

案例:

using std::cout;
using std::cin;
using std::endl;
void f(int a = 0)
{
	cout << a << endl;
}
int main()
{
	f();//没有传参时,使用参数的缺省值
	f(10);//有传参时,使用指定值
	return 0;
}

缺省参数分类 1、全缺省参数

顾名思义,全缺省参数就是说每一个形参都有一个缺省值

using std::cout;
using std::cin;
using std::endl;
void f(int a = 1, int b = 2, int c = 3)
{
	cout << "a = " << a << " ";
	cout << "b = " << b << " ";
	cout << "c = " << c << endl;
}
int main()
{
	f();
	f(10);
	f(10, 20);
	f(10, 20, 30);

	return 0;
}


在传参的时候要从左往右依次传参,不能间隔着传参,会报错

2、半缺省参数

半缺省参数必须从右往左依次来给形参缺省值,不能间隔着给

void f(int a, int b, int c = 20, int d = 4)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << "d = " << d << endl;

}
int main()
{
	//f();//会报错
	//f(10);//会报错
	f(10, 20);
	f(10, 20, 30);
	f(10, 20, 30, 40);
	return 0;
}

半缺省参数的形参没有给缺省值时,传参过程一定要给参数,不然会报错

缺省参数使用案例

我们知道头文件一般用来存放结构体的定义和函数的声明的
2.h

#include
#include
#pragma once
namespace bit
{
	typedef struct Stack
	{
		int* a;
		int top;
		int capaicty;
		//....
	}ST;

	// 缺省参数不能在函数声明和定义中同时出现,在声明中给
	void StackInit(ST* ps, int defaultCP = 4);
	void StackPush(ST* ps, int x);
}

2.cpp

#include"2.h"
namespace bit
{
	// 缺省参数不能在函数声明和定义中同时出现
	void StackInit(ST* ps, int defaultCP)//这里的defaultCP不能给缺省值
	{
		ps->a = (int*)malloc(sizeof(int) * defaultCP);
		assert(ps->a);
		ps->top = 0;
		ps->capaicty = defaultCP;
	}

	void StackPush(ST* ps, int x)
	{}
}

缺省参数不能在函数声明和函数定义中同时出现,只能在函数声明中出现,在函数定义出现会报错

知识点

1、传参时,必须从左往右按顺序传参,不能隔着传参

2、半缺省参数必须从右往左依次来给出,不能间隔着给

3、缺省参数不能在函数声明和定义中同时出现,如果函数的声明和定义分开的话,只能在声明中给

4、缺省值必须是常量或者全局变量,一般都是常量

5、半缺省参数的形参没有给缺省值时,传参过程一定要给参数,不然会报错


五、函数重载

概念:

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

简单来说:c语言不支持两个函数名相同。但是C++里面,如果同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,那么C++就支持多个函数名相同,构成函数重载(注意,构成函数重载不代表就能够调用函数成功)

我们来看看案例

参数个数不同
void f(int a, int b)
{
	cout << a + b << endl;
}
void f(int a, int b, int c)
{
	cout << a + b + c << endl;
}
int main()
{
	int a, b, c;
	f(10, 20);
	f(1, 2, 3);
	return 0;
}

我们可以看到,函数名相同,但是函数参数个数不同,程序能够正确的自动匹配到对应函数,也就是说这两个函数构成重载函数

参数类型不同
void f(int a, int b)
{
	cout << a + b << endl;
}
void f(double a, double b)
{
	cout << a + b << endl;
}
int main()
{
	int a, b;
	double c, d;
	f(10, 20);
	f(10.10, 20.20);
	return 0;
}

同上,这里参数类型不同,程序也能够正确的自动匹配到对应函数

形参类型顺序不同
void f(int a, double b)
{
	cout << a + b << endl;
}
void f(double a, int b)
{
	cout << a + b << endl;
}
int main()
{
	int a, d;
	double c, b;
	f(10, 20.20);
	f(10.10, 20);
	return 0;
}

这里就是参数类型顺序不同,程序也能够正确的自动匹配到对应函数

注意,是类型顺序不同,不是参数变量名顺序不同

案例

//构成函数重载 -- f()调用会报错,存在歧义
void f()
{
	cout << "f()" << endl;
}

void f(int a = 0, char b = 1)
{
	cout << "f(int a,char b)" << endl;
}

int main()
{
	f(10);
	f(10, 20);
	f(); // 歧义  二义性
	return 0;
}

上面的f()就是我前面说的,函数构成重载,但是不一定能够调用成功,f()就存在歧义,C++官方把这种歧义称为二义性

C++支持重载的原理(重点)

我们知道C语言不支持重载,C++支持重载,那么是怎么支持的?

不管是C语言还是C++,在调用函数的时候都是通过函数名来查找该函数的地址,然后通过地址来调用函数
之前我们在C语言学习了符号表这一节,符号表是由函数名及其地址构成的:

C语言中因为不支持函数重载,所以同名的函数名区分不了;

而C++的符号表原本也是函数名及其地址构成的,但是C++又要支持函数重载,所以对比C语言符号表,C++的符号表发生了改变,这个改变就用来区分重载函数

区分重载函数的方法就与函数重载的规则有关如果同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同
我们只需要通过函数的参数不同,那么它对应的符号表函数名发生变化,就可以找到对应的函数就行调用,这种方法就叫做——函数名修饰规则

那么我们就来看看windows下面函数名修饰规则:

可以看到windows下面的函数名修饰规则很难看懂,那么我们来看看linux在g++编译器下面的函数名修饰规则:


上图右边方框内部的两个代码就是实施函数名修饰规则之后C++符号表内部的函数名。
这里的函数名都是有具体意义的:
上面的_Z3Addii——前面的_Z类似于前缀,在linux的g++编译器中采用函数名修饰规则都会带这个前缀;数字3表示函数名的长度;Add就是函数名;ii表示该函数有两个int型的参数
_Z4funcidPi——_Z是前缀;4是函数名长度;func是函数名;i表示函数第一个参数是int型;d表示函数第二个参数是double类型;Pi表示函数第三个元素是指针类型


案例:

总结就是一句话:函数参数不同,函数名修饰规则导致符号表内部的函数名不同,就可以正确找到对应的函数,从而构成了重载函数(C++把函数名带参数修饰了一下)

目前就只能解释到这里了,剩下的知识只能等到后面再讲了

返回值不同为什么不能够成重载?(面试题)
int f(int a, int b)
{
	cout << "f(int a,char b)" << endl;
	return 0;
}
char f(int b, int a)
{
	cout << "f(int a,char b)" << endl;
	return 'A';
}
// 返回值不同,不构成重载原因,并不是函数名修饰规则
// 真正原因是调用时的二义性,无法区分,调用时不指定返回值类型
int main()
{
	f(1, 1);
	f(2, 2);
	return 0;
}

因为在函数调用的时候,我们能够指明参数的类型,但是我们不能够指明返回值的类型。这样在两个函数参数相同,返回值不同的情况下,我们就无法确定调用哪一个函数,产生了二义性。所以返回值不能构成函数重载

补充知识点

cout


六、引用

概念

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

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

在类型后面加上取地址符号——类型+&

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

1、引用特性

· 1. 引用在定义时必须初始化
· 2. 一个变量可以有多个引用
· 3. 引用一旦引用一个实体,再不能引用其他实体

当我们每次要使用引用的时候,一定要记得对引用初始化,不然会报错!!!

案例1:引用未初始化

引用指定实体之后,不能够再次引用其他实体

案例2:引用多次


这里可以看到ra和m的值被改为了200,但是ra还是引用实体m的,所以引用指定一个实体之后,不能再次引用其他实体,这也是为什么引用无法完全替代指针的原因之一

接下来我们看看引用的应用场景在哪里

2、使用场景

案例1:基础使用方法

int main()
{
	int a = 10;
	int& ra = a;
	int& y = a;
	int& x = y;
	x++;
	y++;
	a++;
	return 0;
}

案例2:作为参数交换数据
有了引用我们在交换数据不需要传地址了

void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a, b;
	cin >> a;
	cin >> b;
	Swap(a, b);
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	return 0;
}


案例3:替换二级指针

void SlistPushBack(struct ListNode** pphead, int n)//以前的二级指针
{}
void SlistPushBack(struct ListNode*& phead, int n)//现在的引用
{}
int main()
{
	struct ListNode* plist = NULL;
	SlistPushBack(plist, 1);
	SlistPushBack(plist, 2);
	SlistPushBack(plist, 3);
	return 0;
}

案例4:引用做返回值

这个案例很重要,我们先来回顾一下之前的知识

就我们当前所学内容,我们知道数据存放区域有4个:
第一个:栈区。函数调用时在栈区上面开辟空间,调用结束,栈自动销毁
第二个:堆区。动态内存开辟在堆区上面开辟,要进行手动释放动态内存
第三个:静态区。又称数据段,我们后面会讲。用来存放全局变量和static修饰的数据
第四个:常量区。又称代码段。这个也后面讲
其中,栈区是向下生长,堆区向上生长

我们先来看看一段代码:(这里在敲代码的时候没注意主函数和函数变量名相同)

int Test()
{
	int a = 10;
	a++;
	return a;
}
int main()
{
	int a = Test();
	cout << a << endl;
	return 0;
}

这里函数调用结束之后a不应该销毁了吗?怎么被带回主函数的呢?
其实我们前面看见过答案,可能大家都忘记了:


所以,在大多数情况下,函数调用结束,寄存器会拷贝返回值,然后带回到主函数内部

那么,如果用一个引用做返回值呢?

这里可以看到,引用做返回值的时候,有一个static修饰变量a,如果没有会出现什么问题呢?

static修饰了局部变量之后,局部变量存放在了静态区,局部变量的生命周期延长。所以函数调用销毁之后,变量a一直存在,直到退出或者销毁程序。所以主函数可以轻松拿到返回值。而没有这个static,此时函数栈帧的空间就销毁,变量a就随着函数调用结束销毁了。

空间销毁的意义是什么?

空间销毁后该空间还存在内存中,只是该空间的使用权不是我们的了,归还给 *** 作系统了,那么我们在该空间的数据不被保护了。
我们还是可以去访问该空间(这里就是越界访问了),但是里面的数据变没变、有没有被清除、有没有被其他数据覆盖,这是我们无法确定的

所以,没有static修饰直接使用引用做返回值,带回到主函数的返回值是不确定的

我们看来看一段代码就知道了:


这里第二次打印ret结果出现了一个很大的数值,这是因为cout也是一个函数,调用cout函数时覆盖了第一次调用Count原来的函数栈帧;
第一次调用的时候ret先被取到,然后调用cout函数打印结果,所以第一次打印的时候ret还是可以取到的,然后就被cout函数给覆盖了;
而第三次打印是因为变量x刚好覆盖了ret,所以导致ret的值变成了100

注意:
上面代码的三个打印结果不一定是我这样的,有的人可能全部都是随机值,有的人也有可能都是1等等等等,所以,大家一定要记住,引用做返回值,要带上static就行修饰,不然结果有可能会是错误的;


3、传引用做返回值的作用

1、减少拷贝,提高效率
2、修改返回值
3、引用做参数

因为不用引用做返回值要拷贝返回值到寄存器,才能把返回值带回到主函数

但是,为了4个字节的返回值就使用引用,有意义吗?
答案是没有意义,但是别急,看看下面的代码:

传值和传引用做返回值效率比较:

#include 
struct A{ int a[10000]; };
A 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;
}

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

4、传值、传引用做参数

我们知道传值的时候,形参是实参的一份临时拷贝,改变形参不能改变实参;
而传引用时,对形参的修改就可以改变实参

所以,对于引用做参数而言:

1、减少拷贝,提高效率
2、输出型参数,改变形参就能改变实参

传值和传引用做参数效率比较:

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


可以看到效率明显提高了,引用做返回值确实能够提高效率。

接下来我们来证明前面说的:函数构成重载,但是调用不成功

void Swap(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a, b);
	cout << a << endl;
	cout << b << endl;
	return 0;
}

这一段代码就是两个函数构成重载,但是调用不会成功


5、常引用
int main()
{
	int a = 10;
	int& ra = a;//权限不变
	const int& rra = a;//权限缩小
	rra++;//rra被const修饰不可修改,但是a可以修改
	a++;
	cout << rra << endl;
	
	//指针和引用赋值过程中,权限可以平等(平移),可以缩小,但是不能放大
	const int b = 10;
	int& rb = b;//权限放大了,不行
	
	const int& rb = b;//权限不变
	return 0;
}

这里const修饰的变量不能直接引用,在指针和引用赋值过程中,权限可以平等(平移),可以缩小,但是不能放大。这里的变量b结果const修饰之后只能可读,不能可写了

注意

权限的平等(平移),缩小,不能放大,仅限于指针和引用的赋值过程中



补充知识点

数据在进行类型转换,整型提升,截断时,会产生一个临时变量,临时变量具有常属性





6、引用和指针的区别

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

int main() 
{
	int a = 10; 
	int& ra = a;
	cout << "&a = " << &a << endl; 
	cout << "&ra = " << &ra << endl;
	return 0;
}

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

我们可以转到反汇编去看一下:


我们可以看到,引用和指针的反汇编代码是一样的,也就是说引用和指针底层一样。通过引用是按照指针的方式来实现的结论,和上面的反汇编代码,可以得出引用底层实现也是开辟空间了的

得出结论:

引用在语法上面是别名,不占用空间
引用在底层是用指针实现的,要占用空间

引用和指针的不同点:

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

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

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

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

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

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

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

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

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

总结

到这里我们也算是初步认识了C++了,对C++有了一点点的认识了,未来还有许多知识等着我们去掌握,让我们一起加油吧!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/2990669.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-23
下一篇 2022-09-23

发表评论

登录后才能评论

评论列表(0条)