C++入门(lesson2)

目录

一、引用🐅

1.1 引用概念🐱

1.2 引用特性🐱

1.3引用的使用🐱

①做参数😀

②做返回值😀

1.4Const引用和临时变量🐱

①引用权限的放大和缩小😀

②临时变量的讲解😀

1.5 传值、传引用效率的比较🐱

1.6 引用和指针的区别🐱

 二、内联函数🐅

2.1概念🐱

2.2内联函数在编译器的表现🐱

2.3特性🐱

三、auto关键字🐅

3.1类型别名思考🐱

3.2auto简介🐱

3.3 auto的使用细则🐱

①.auto与指针和引用结合起来使用😀

②.在同一行定义多个变量😀

3.4 auto不能推导的场景🐱

①auto不能作为函数的参数😀

②auto不能直接用来声明数组😀

四、基于范围的for循环🐅

4.1范围for的语法🐱

4.2范围for的使用条件🐱

①.for循环迭代的范围必须是确定的😀

五、指针空值nullptr(C++11)🐅

5.1 C++98中的指针空(NULL)🐱

5.2 空值指针nullptr🐱

一、引用🐅

1.1 引用概念🐱

引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间(理论上,实际并非如此,后面细说),它和它引用的变量共用同一块内存空间。
引用,就好像给一个人取了个外号,就像古代的人有名和字,都是指代他这个人。
引用的使用: 类型& 引用变量名(对象名)=引用实体

 引用就是如此,它们指代的都是同一块空间,所以地址肯定是相同的。

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

1.2 引用特性🐱

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

2、一个变量可以有多个引用。(一个人可以有很多的外号)

3、引用只能指向一个空间,不能改为其他空间。

4、引用没有开辟空间,如果对它赋值,是改变了引用指向空间的内容。

5、不能利用引用去做链表,因为引用不能更改指向,在C++中,引用还没完全取代指针,而Java是可以脱离指针的。

1.3引用的使用🐱

①做参数😀

在C语言阶段我们要交换两个数的值,常常需要传他们的地址,通过地址来改变他们的值,在提出引用之后,我们不需要传地址了。

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

同样的,通过使用引用,我们有些地方可以没必要使用二级指针,从而使代码简化。

void SListPushBack(Node**pphead,int x)
void SListPushBack(Node*&pphead,int x)//这种方式

②做返回值😀

int& Count1()
{
	int n = 0;
	n++;
	return n;//引用返回
}
//传值返回
int& Count2()
{
	static int n = 0;
	n++;
	return n;//传值返回
}

Count1Count2函数不同的区别:

在于Count1n是局部变量,在栈上开辟的,而Count2n是在静态区开辟的,不会随着函数的结束而销毁

我们再来看一下它们被调用后可以观察到什么

 这样的结果是意料之内的,因为Count1中的n随着函数结束而销毁,也就是它的空间不归我们使用,归还给了操作系统,我们可以通过引用找到那个空间,但是它可能已经被操作系统使用而值发生了变化,所以我们读写的数据是不确定的。所以在这里我们要注意,当使用引用做返回值的时候,返回的数不能在栈区,不能是局部变量,它可以在静态区或者

在这里,我们可以再深入一点,看一下传值返回传引用返回有什么不一样。

 Count1返回的是引用,我们惊奇地发现,用intint&接受它们的值也是不一样的,这似乎超乎我们的意料,不过如果我们知道函数的栈帧,再一分析就很好理解了。

为什么出现不一样的结果?

因为函数结束也就是栈帧销毁,会生成一个临时变量,如果临时变量比较小,就会存放再寄存器中,如果比较大,比如结构体,就会开辟一块空间,因为不可能直接传,因为空间已经被释放还给操作系统了。

所以这里ret接受的是n的拷贝,那么还有一个问题,int&类型的ret接受的是不是拷贝呢?其实也是拷贝,那为什么,它能找到那个空间呢?因为这里,最本质上是传了地址!如果诸位不相信,我们可以在编译器内部看一下引用和指针。

 我们惊奇的发现真的好相似,所以引用在底层上实际也是指针实现的

1.4Const引用和临时变量🐱

①引用权限的放大和缩小😀

权限的放大:

 我们来看这段代码,为什么int& rb=b会出错呢?

这就是典型的引用的权限的放大,这里rb引用b的别名,b已经被const限制而rb不被限制rbb的别名,rb可修改而b不可修改是不是不合理?

反之,如果合理,那么我给rb赋值,那么rb所指向的空间的内容被修改,而这个空间就是b,但是bconst限制,不能修改,是不是就起冲突了?

权限的缩小 

 我们再来看一下这段代码,这段代码中,被const限制的rra不能被修改,但是它可以引用a,只不过权限受限,而a不受限制。

启示:

1、指针和引用赋值中,权限可以平移,可以缩小,但是不能放大!类似于上级给你权力,它可以给你和他相同的,或者小于他的,但是不能高于他,这是一样的道理。

2、权限的放大和缩小,仅限于指针和引用,因为这两种都会影响到原来的数据。

3、如果不更改原来的数据,一般引用或者用指针做参数,一般都是用const加限制引用

②临时变量的讲解😀

我们还是通过代码来看一下:

int main()
{
	double d = 10.28;
	int i = d;
	cout << (int)d << endl;
	return 0;
}

 看了这段代码,我想问几个问题:

1、把浮点型d赋值给整型i,是把d强转为int后再赋值给i吗?

2、输出强转成整型的d,是真的把d给强转截断了吗?

显然不是,这里就涉及到了临时变量的概念,这里无论是把d赋给i,还是把d强转,都产生了临时变量,它对原来的数据进行拷贝,在int i=d中,临时变量就等于把d强转后的数,然后再赋给i

 这里为什么会出错呢?这要说到临时变量的一个特性,也就是常量性,临时变量是不可修改的,它是被const限制的,这里的赋值出错就是因为,临时变量对于d强转整型后的拷贝是常量,他不能被赋值给变量

 当我们用const加限制,使它变为常量后,就运行成功了!

总结一下临时变量的特点:

1、隐式类型转换强制类型转换截断提升函数栈帧的销毁都会产生临时变量

2、临时变量具有常量性,不能被修改

1.5 传值、传引用效率的比较🐱

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

 我们很轻易就看出,传引用的效率要远高于传值的。

1.6 引用和指针的区别🐱

语法概念上 引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。但是,上面博主就验证过了,实际并非如此。
底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

 引用和指针的不同点:

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

 二、内联函数🐅

2.1概念🐱

inline修饰的函数叫内联函数,编译时C++编译器会在 调用内联函数的地方展开没有函数调用栈帧的开销,内联函数提升程序运行的效率。
不知道读者在看到对于内联函数的介绍时有没有感觉到很熟悉,对,它确实和我们在C语言阶段所学习的宏定义函数很相似。那么,可能有的读者比较困惑,为什么有了宏, 他也能替换,也能不调用函数,不消耗栈帧,那么引入内联函数的意义是什么呢?
我们在C语言阶段,学习的宏它确实有不少优点,比如说:它不需要调用,比函数运行更快;它直接替换;它是类型无关的等等。但是他也有不少缺点:
1、不能调试;
2、没有类型安全检查;
3、容易写出带有副作用的宏参数,比如x++等等;
4、写宏的时候经常需要考虑运算符优先级问题,会加上不少的括号,很容易出错。

内联函数就是C++提出解决这些问题的。

2.2内联函数在编译器的表现🐱

 

 看到这里,可能有的读者就要发出疑问了,我们不是使用了内联函数吗,为什么还和普通函数一样需要call(调用)呢?

这是因为这是在debug模式下,编译器为了方便我们调试,而不发生替换。

如果在release模式下,编译器生成的汇编代码是不会存在call Add的。

在debug模式下,我们需要对编译器进行设置,否则不会展开(因为在debug模式下,编译器默认不会对代码进行优化)。

设置方式(vs 2019):

 设置之后我们再进行调试,就可以发现没有调用函数,而是直接发生了替换:

2.3特性🐱

1、inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提升程序运行效率。

2、inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的标准,取决于编译器内部实现)、不是递归、且频繁调用的小函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:

 3、inline不建议声明和定义分离,分离会导致链接错误。因为inline在预处理阶段被展开,没有有效函数地址了,链接会找不到。

4、如果函数调用过多,比如有10000个调用的地方,这时候就不建议使用内联函数了。因为比如说内联函数有30行代码,inline展开就有30W行代码,而这些代码是冗余的,无用重复代码,他会大大增加可执行程序的大小。这时就使用普通函数即可,它只需要消耗10000+30行代码。

对于第三点,我们可以在编译器上来查看一下:

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i) {
 cout << i << endl; }
// main.cpp
#include "F.h"
int main()
{
 f(10);
 return 0; }
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl 
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

这里为什么会报链接错误呢?我们在学习C语言预处理时知道,程序执行要经过预处理-->编译-->汇编-->链接这几个阶段,而链接要完成的任务是:

1、合并段表,相同的段进行合并;

2、符号表的合并和重定义,选取有效地址。

这里报错是因为符号表合并找不到有效地址而出错。为什么找不到呢?

因为在C++编译器中,当编译器遇到inline关键字后,除了会替换之外,内联函数就不会进入符号表,(这一点和宏是契合的,预处理之后不进入符号表)所以当替换的是内联函数的声明,而不是定义且在当前文件下找不到定义的时候,就会出现错误。

所以这里的建议是,编写内联函数在头文件的时候,不要声明,直接定义在头文件。

如果内联函数是解决宏函数缺陷的问题,那么对于宏定义的比如常量等其他问题我们是如何解决的呢?

C++通常用这些技术替代宏:

1、常量定义换用const enum

2、短小函数定义 换用内联函数。

三、auto关键字🐅

3.1类型别名思考🐱

为什么引入auto这个关键字呢?因为随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

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给类型取别名,比如说:

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

但是还是不够方便。在编程时,常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并不容易,因此C++11给auto赋予了新的含义。

3.2auto简介🐱

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;
}

 这里的typeid().name()的作用是拿到变量的类型。

 注意:

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

3.3 auto的使用细则🐱

①.auto与指针和引用结合起来使用😀

        用auto声明指针类型时,用auto和auto*没有任何区别,但用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;
    *a = 20;
    *b = 30;
     c = 40;
    return 0; 
}

②.在同一行定义多个变量😀

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

3.4 auto不能推导的场景🐱

①auto不能作为函数的参数😀

void Func(auto x)
{
	printf("Func(int x)\n");
}
void Func(int x)
{
	printf("Func()\n");
}
int main()
{
	Func(5.2);
	Func(5);
	return 0;
}
//错误	C2668	“Func”: 对重载函数的调用不明确	

这里,很显然,如果使用auto,编译器是无法区分参数类型的,因为auto任何类型都可以兼容,这样和函数重载也会冲突。

②auto不能直接用来声明数组😀

void TestAuto()
{
	int a[] = { 1,2,3 };
	auto b[] = { 4,5,6 };
}

//错误	C2119	"b": 无法从空的初始值设定项推导 "auto []" 的类型	
//错误	C3318	“auto []”: 数组不能具有其中包含“auto”的元素类型	
为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有
lambda 表达式等进行配合使用。

四、基于范围的for循环🐅

4.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 };
	for (auto e : array)
		cout << e << " ";
	return;
}

但是,如果我们想对数组里的元素操作,这样是否可以呢?

 我们发现,数组里的数据并没有乘以2,说明我们遍历的数组,不是数组本身,而是一个临时拷贝,并不影响数组的值,所以我们如果想对数组里的数操作,需要使用引用:

 注意:范围for与普通循环类似,是可以用continue来结束本次循环,也可以用break来跳出整个循环。

4.2范围for的使用条件🐱

①.for循环迭代的范围必须是确定的😀

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

注意:对于传参的数组,是不能用范围for循环的。

void TestFor(int a[])
{
	for (auto& e : a)
		cout << e << endl;
}
int main()
{
	int a[] = { 1,2,4,5,6,7 };
	TestFor(a);
	return 0;
}

像这样,传过去的只是一个指针,也就是数组首个元素的地址。

五、指针空值nullptr(C++11)🐅

5.1 C++98中的指针空(NULL)🐱

我想先问一个问题,NULL是一个地址吗?如果你觉得NULL是一个地址,那么请看以下代码:

 如果NULL是地址,那么为什么会调用第二个函数而打印出f(int)呢?

这其实和C++98的bug有关,C98中对于NULL的定义,我们可以在头文件(stddef.h)中看到

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

我们看到,NULL实际是个宏,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,上面很显然程序将它作为0来处理而不是空值的指针

5.2 空值指针nullptr🐱

为了应对这种错误,C++11引入了nullptr,将其定义为(void*)0它是一个关键字,不需要引头文件

注意:

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

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

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值