前言
这里vince将要进入C++的学习了,C++学习将是一个漫长的过程,当然在学习这里的基础上前面的知识也不能不复习。也有很多人说C++有多难有多难的,但是我们不能胆怯,努力去学,孰能生巧,至少能够达到了解它的层次哈~
正文
知识点一:引用
我们可能有很多人是学完C语言就去学数据结构的哈!当时很多课程或者书籍,(例如:严蔚敏老师的书)大家刚接触顺序表和链表的时候,就会遇到void SListPushBack(ListNode& l,int x);
当时看到的时候肯定都很懵逼吧~
小白:满脸疑惑,这怎么在形参和类型之间加个“取地址”操作符呢?这是干嘛的呢?这是到底是什么意思啊?
我:这里的&
不再是C语言里面学的“取地址”操作符啦~小伙伴们,他是我们接下来要谈论到的“引用”,之前蒙圈的小伙伴们可以往下看看,看完绝对一目了然。
1. 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用声明方法:类型标识符& 引用变量名(对象名) = 引用实体
代码示例:
#include <iostream>
using namespace std;
int main()
{
//b叫做a的引用,也叫做b是a的别名
int a = 10;
int& b = a;//这里& b是a的引用
printf("a的地址:%p\n", &a);//这里&a是取地址
printf("b的地址:%p\n", &b);//这里&b是取地址
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
运行结果:
总结:
1、可以看出如int& b = a;
b是a的引用,引用在这里的作用就是给变量a取别名例如:每个人都有一个名字,但是他们还可能存在小名或者外号之类的称呼,你用这个称呼他们的时候,他们同样会答应。
2、引用类型必须与引用实体是同种类型。
3、运行结果可以看出,本名和别名是同一个地址,类似于叫一个人的本名和小名,都是在叫他这个人的道理。因此可知,声明一个引用不是新定义一个变量。
2. 引用的特性
1.引用在定义时必须初始化。
2.一个变量可以有多个引用(别名)。
3.引用一旦引用一个实体,再不能引用其他实体。(引用和指针的区别)
代码示例:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;//引用在定义时必须初始化
int& rra = a;//一个变量可以有多个引用
int& rb = ra;//说明引用得到的变量和原来的变量是同一块地址
printf("a的地址:%p\nra的地址:%p\nrra的地址:%p\nrb的地址:%p\n", &a, &ra, &rra,&rb);
return 0;
}
运行结果:
3. 常引用
提到常引用,我们就能立马想到之前学过的const修饰的常变量,const 修饰后的变量是只可读不可写(const 就是保护变量让其不可被修改)。
代码示例:
#include <iostream>
#include <iostream>
using namespace std;
int main()
{
//情况一:权限变化问题
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量 权限变大
const int& ra = a; //权限不变
// int& b = a; // 该语句编译时会出错,b为常量 权限变大
const int& b = a;
printf("a = %d\nra = %d\nb = %d\n", a, ra, b);
//情况二:引用不能直接给常量取别名,但有const就可以
//int& c = 10; //该语句就是错误的,因为不能直接给常量取别名
const int& c = 10;
printf("c = %d\n", c);
//情况三:有const引用类型和实体类型可以不一样
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const double& rd = d; //权限变小
//int& rrd = d;这里就出bug因为引用类型必须和引用实体类型相同
const int& rrrd = d;// 但是有const就可以
printf("d = %.2lf\nrd = %.2lf\nrrrd = %d\n", d, rd, rrrd);
return 0;
}
运行结果:
总结:
1、存在一个权限问题:引用类型的权限必须小于等于引用实体类型才正确。如以上代码标注的权限不变和权限变小那两个例子。(这里指的权限是读写权限。)
2、引用不能直接给一个常量取别名,即对一个常量进行引用。但是存在const就可以进行给常量取别名,如:const int& c = 10;
。
3、前面说过引用类型和引用实体类型必须一致,但是有const存在时,引用类型和实体类型就可以不一样。如上述代码情况三。
知识拓展:
之前说引用类型和实体型必须一致吗?那么这里为什么引入const后就可以打破这个规则呢?
让我们看看下面的图解之后,就一目了然了哈~
调试分析图解:
4. 使用场景
1、做参数
还记得我们刚开始学C语言函数的时候都会写一个交换函数Swp()
用于交换两个变量的值,此时我们初学者的小伙伴们肯定有不少人都掉进坑里面去了——大家肯定有很多人这样定义函数Swp(int x, inty)
,然后想着就用x、y来作为形参嘛,来接收传过来需要交换的实参。写完之后发现,哦豁,没有实现交换,什么鬼?懵圈了吧,脑瓜子嗡嗡滴吧?
当时老师讲解就会说这里是因为形参实际上是实参的一份拷贝,改变形参,无法改变实参。是吧,小伙伴们有没有想起来,是不是很熟悉这句话。因此这里我们就需要使用指针——Swp(int* p1, int* p2)
,用指针接收实参的地址,直接利用地址对两个实参进行交换操作,这样就实现交换咯~从这里开始我们接触了指针,而越到后面指针的使用就越来越复杂,而且存在一定的不安全性。
因此在C++里面就有了引用,这里就是引用做参数的使用场景,当然还有开头我们说的严蔚敏老师数据结构里面的很多地方你会发现形参那里出现&
来作为参数。
有了引用之后就不用在使用指针来作为形参接收实参。
代码示例一:
交换函数
//有了引用之后这里就不需要使用指针,实参那边也不需要传地址
//形参是实参的别名,但是两者地址都相同,就是同一空间里的数
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 0;
cout << "交换前:" << endl;
cin >> x >> y;
Swap(x, y);
cout << endl;
cout << "交换后:" << endl;
cout << x << ' ' << y << endl;
return 0;
}
运行结果:
代码示例二:
数据结构单链表C和C++实现尾插
#include <iostream>
#include <stdio.h>
using namespace std;
typedef struct ListNode
{
int val;
struct ListNode* next;
}LNode, * PLNode;//这里定义*PLNode是为后面引用做一下铺垫
//这里定义*PLNode之后,后面的就可以直接不使用指针,直接引用操作,就能实现各功能操作
//C语言实现简单的单链表的一个节点尾插
void LNodePushBack_C(LNode** PPhead, int x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode));
if (newnode == NULL)
{
exit(-1);
}
else
{
newnode->val = x;
newnode->next = NULL;
}
if (*PPhead == NULL)
{
*PPhead = newnode;
}
}
//CPP实现一个简单单链表尾插
//void LNodePushBack_CPP(LNode*& Head, int x)
//以下直接写成这样PLNode& Head是因为,前面结构体变量后面定义的*PLNode
void LNodePushBack_CPP(PLNode& Head, int x)//严蔚敏老师的书里面的写法
{
LNode* newnode = (LNode*)malloc(sizeof(LNode));
if (newnode == NULL)
{
exit(-1);
}
else
{
newnode->val = x;
newnode->next = NULL;
}
if (Head == NULL)
{
Head = newnode;
}
}
void LNodePrint(LNode* Phead)
{
printf("%d\n", Phead->val);
}
int main()
{
LNode* L = NULL;
//LNodePushBack_C(&L, 24);
//LNodePrint(L);
LNodePushBack_CPP(L, 8);//利用引用,此时就可以直接传L
LNodePrint(L);
return 0;
}
总结:
1、引用在做参数的时候形参就是实参的别名,就不用在进行拷贝了,减少了拷贝提高了效率。
2、引用做参数,减少了对复杂指针的使用,防止发生大量指针使用出现错误情况。
2、做返回值
我们之前常用的传值返回有小伙伴了解其是怎么实现的吗?
传值返回:传值返回过程中会产生一个临时变量,它和传参一样(如果这个变量小就会用寄存器替代,如果大就不会用寄存器替代)都是一个拷贝的过程。
4.2.1、对传值返回本质进行理解:
#include <iostream>
#include <time.h>
using namespace std;
int count()
{
static int n = 0;//static修饰后,这句话就第一次进来执行,后面进来就不会在执行
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
图解分析:
利用一个编译器报错的细节来侧面反映这个临时变量的存在:
利用const就解决了对具有常性的临时变量进行引用问题,以下就正确无报错:
4.2.2、引用做返回值
引用做返回值是存在条件的哈~
函数返回时候,出了函数作用域之后,如果返回对象未还给操作系统,则可以使用引用返回;反之,若是还给了操作系统,则只能用传值返回。
例一:可以使用传引用返回
#include <iostream>
#include <time.h>
using namespace std;
int& count()
{
static int n = 0;//这里static修饰的静态变量,在函数结束时,n就未还给操作系统
n++;
return n;
}
int main()
{
int& ret = count();
return 0;
}
图解分析:
打印地址对比分析:(由此可见ret是n的别名)
总结:
这里地址一样,即ret就是n的别名。
传值返回是先将需要返回的值拷贝给临时变量(若值小,临时变量就用寄存器充当;反之,就会提前在需要传过去的那个函数里面开辟空间),然后再将临时变量拷贝给最终需要的量,这里存在拷贝行为。
而引用做返回,就可以取消这个拷贝行为,直接让最终的那个接收量作为最初那个返回量的别名,函数返回的直接是返回变量的别名。因此这种static修饰后的变量作为返回值时候,在函数结束后其返回值未还给操作系统,此时就可以使用引用返回。
例二:不可以使用传引用返回
一段错误代码讲解:(引用搞出来的间接野指针访问)
#include <iostream>
#include <time.h>
using namespace std;
int& count()
{
int n = 0;//这里没有static修饰
n++;
cout << "&n:" << &n << endl;
return n;
}
int main()
{
int& ret = count();
cout << ret << endl;
cout << "&ret:" << &ret << endl;
cout << ret << endl;
cout << ret << endl;
return 0;
}
运行结果:
错误分析:
这里的n没有static修饰,因此在函数结束时候n就直接归还给操作系统。因此以上这个代码存在问题:因为ret作为n的别名,去直接访问n的地址,可是n所在的这个函数已经将空间销毁了,(销毁指的是将空间归还给操作系统,依然还存在,并不是真正的摧毁)而ret就像是偷来的一把钥匙,尽管这块空间已经不是n的了,但是还可以使用ret进行非法访问。
因此这段代码是非法的,存在一定问题。这一段代码其实是间接的野指针访问,是用引用搞出来的间接野指针访问。 所以如果利用这段代码多次打印ret的结果:一般第一次是需要的值,第二次就会是随机值;有的编译器在函数结束后就会将其栈帧清除,这样一开始就打印出随机值。
总结:
传引用返回条件:函数返回时候,出了函数作用域之后,如果返回对象未还给操作系统,则可以使用引用返回;反之,若是还给了操作系统,则只能用传值返回。
5. 引用和指针的区别
刚开始就说过了,有了引用之后,就可以减少指针的使用了哈~那么他俩之间有什么区别呢?
引用和指针区别:
1.引用在定义时必须初始化,指针没有要求。
2.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
3.没有NULL引用,但有NULL指针。
4.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
5.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
6.有多级指针,但是没有多级引用。
7.访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
8.引用比指针使用起来相对更安全。
知识点二:auto关键字(C++11)
1. auto背景和含义
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
因此在C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
代码示例:
int Test()
{
return 8;
}
//auto是通过编译器自己推导从而得到该变量的类型
int main()
{
int a = 24;
auto b = a;//auto会直接根据a的类型从而推出b的类型,b的类型和d的一样
auto c = &a;
const int d = 0;
auto e = &d;
auto f = 'x';
auto g = Test();
//typeid().name()可以用来看一个变量的类型
cout << "b的类型:" << typeid(b).name() << endl;
cout << "c的类型:" << typeid(c).name() << endl;
cout << "e的类型:" << typeid(e).name() << endl;
cout << "f的类型:" << typeid(f).name() << endl;
cout << "g的类型:" << typeid(g).name() << endl;
return 0;
}
运行结果
2. auto的使用场景
1、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别(因为只使用auto时,它具有自动推导能力,也可以去推导出指针类型),但用auto声明引用类型时则必须加&。
代码示例:
//auto是通过编译器自己推导从而得到该变量的类型
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
//typeid().name()可以用来看一个变量的类型
cout << "a的类型:" << typeid(a).name() << endl;
cout << "b的类型:" << typeid(b).name() << endl;
cout << "c的类型:" << typeid(c).name() << endl;
//cout << "f的类型:" << typeid(f).name() << endl;
//cout << "g的类型:" << typeid(g).name() << endl;
return 0;
}
运行结果:
注意:auto声明引用类型的时候必须加上&。
2、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
代码示例:
auto a = 10, b = 24;
auto c = 11, d = 4.0;//这就出现错误,auto使用时候,同一行定义多个变量的时候,其类型要一致
//因为使用auto时候,编译器只会对其第一个数进行推导
图解分析:
3、在类型很长的时候,懒得定义,此时可以使用它自动推导
这个目前先做了解,等到后面我们学到这一块定义有时候会很长的时候,我们在结合示例实际讲解。
4、auto配合其他使用
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
进行配合使用。
3. auto不能使用的场景
1、不能作为函数的参数,也不能作为返回值
这里是编译器规定死的,直接将auto做参数和做返回值禁止。
错误代码示例:
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{
}
错误分析:
因为假设如果写一个函数其参数和返回值都是auto来定义的,那么问题就来了,你在传参的时候,怎么确定传什么类型的参数,这个时候就出现混乱了。
2、auto不能直接用来声明数组
错误代码示例:
void TestAuto()
{
int a[] = {
1,2,3,4, 5};
auto b[] = {
4,5,6, 7, 8};
}
3、避免冲突
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
知识点三: 基于范围的for循环(C++11)
1. 范围for的语法
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
这里就是利用auto配合使用。
代码示例:
int main()
{
/*auto a = 10, b = 24; // auto c = 11, d = 4.0;*///这就出现错误,auto使用时候,同一行定义多个变量的时候,其类型要一致
//因为使用auto时候,编译器只会对其第一个数进行推导
int arry[] = {
2, 1, 3, 4, 5, 2 };
for (int i = 0; i < sizeof(arry) / sizeof(arry[0]); i++)
{
arry[i] *= 2;
}
cout << "C语言方式打印" << endl;
//C语言玩的方法
for (int i = 0; i < sizeof(arry) / sizeof(arry[0]); i++)
{
cout << arry[i] <<" ";
}
cout << endl;
//C++11玩的方式
//名叫范围for,其作用是会依次去取arry中的数据,将其赋值给e,自动判断结束
cout << "利用C++11中范围for打印" << endl;
for (auto e : arry)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
总结:
1、范围for并不是只能for (auto e : arry)这样,将e前面写成auto写成其真正的类型也可以。但是一般都习惯写成auto。
2、范围for这里面是将arry数组里面的数据都赋值给e然后将e打印出来,因此如果修改e的值是无法影响数组里面的值的。
但是刚好前面就学到了一个东西可以解决这个问题,那就是引用&,可以让e成为数组中数据的引用,即e为每一个数据的别名,然后对e操作就相当于对数组里面数据操作。
注意:范围for与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2. 范围for的使用条件
条件:for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定。
错误范围for代码示例:
void TestFor(int arr[])
{
for(auto& e : arr)
cout<< e <<endl;
}
错误分析:
因为在c语言中规定其实此处的arr[ ]已经不再是数组了,本质上是指针,而范围for必须要以数组第一个和最后一个元素作为范围展开。所以这里for的范围f就不确定,就不能使用范围for。
知识点四:指针空值nullptr(C++11)
1. C++98中的指针空值
在一个良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现一些不可预料的错误,比如未初始化的指针。
如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;//这两个是C/C++98里面空指针初始化方式
}
看到这里,那么我想问一下大家对NULL的实质有所了解吗?估计很多小伙伴都不知道吧!
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:(这就是NULL的宏定义)
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
但是不论采取何种定义,在C++里面使用空值的指针时,都不可避免的会遇到一些麻烦。
例如:
//nullptr是C++里面的指针置空
void fun(int a)
{
cout << "fun(int)" << endl;
}
void fun(int* a)
{
cout << "fun(int*)" << endl;
}
int main()
{
fun(0);
fun(NULL);//这里想利用NULL为空指针去调用fun(int*)函数
//但是在C++里面NULL会首先被当作整形0来处理,而不是((void*)0)来处理
//要想实现这个就必须进行类型强制转换
fun((int*)NULL);
//因此C++里面就引入了关键字nullptr
fun(nullptr);
return 0;
}
运行结果:
分析:
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。因此在C++里面这个int* p = 0和int* p = NULL是会存在问题的。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
而C++11中就对这种问题进行了优化。
2. C++11中的指针空值
以上存在这些问题,而在C++11中就引出了一个关键字nullptr,利用这个关键字,弥补上面存在的漏洞。
对nullptr的使用注意事项:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2.在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
结语
vince刚刚入坑CPP,这里是CPP的一部分基础学习,好多人都说CPP最难,其实什么事对于初学者来说都难,但是只要我们能够坚持学习,就一定会有进步的哈~希望我和大家都不要放弃!。
如果各位大佬们觉得有一定帮助的话,就来个赞和收藏吧,如有不足之处也请批评指正。
代码不负有心人,98加满,向前冲啊
以上代码均可运行,编译环境为 vs2019哈~
文章评论