目录

1.缺省参数

1.1缺省参数概念

1.2缺省参数的使用:

1.3缺省参数的分类

1.3.1全缺省参数

1.3.2半缺省参数

1.4缺省参数的应用场景

2.函数重载

2.1函数重载的概念

2.2不支持函数重载的情况

3.引用

3.1引用的概念

3.2引用的特性

3.3引用做参数

3.4 传值返回

3.5 引用做传值返回

3.6关于引用的探讨

3.6.1比较传值和传引用的效率

3.6.2 引用和指针的区别

3.7常引用

3.7.1 权限的放大

3.7.2 保持权限的一致

3.7.3 权限的缩小

3.7.4 常引用的应用

3.7.5 带常性的变量的引用

3.7.6 常引用做参数

1.缺省参数

先看一个简单的函数,功能就是打印出传递过来的数字:

#include using namespace std; void Func(int num) {// 此时接收,num = 1cout << num << endl;} int main(){Func(1);// 传参:1return 0;}

如果我不想传参呢?我想直接调用Func函数:

#include using namespace std; void Func(int a) {cout << a << endl;} int main(){Func();//此时就会报错 return 0;}

因为没有传递参数,所以自然会引发报错。

不过,在C++里我们可以利用一个叫 “缺省参数” 的东西,

让该函数可以做到不传参也能运行的效果。

#include using namespace std; void Func(int a = 0) {cout << a << endl;} int main(){Func();//成功打印0 return 0;}

下面我们就将学习这个神奇的 “缺省参数” 。

1.1缺省参数概念

缺省参数通俗讲就是默认参数 缺省参数:声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。 简单来说就是:传参了就用传来的值,没传参就用默认值。

1.2缺省参数的使用:

#include using namespace std; void Func(int a = 0)// 缺省值作为形参,传给 a{cout << a << endl;} int main(){Func(10); // 传参时:使用指定的实参(传入10)Func(); // 没有传参时,使用默认的参数值(默认值:0)。 return 0;}

① 第一次调用Func时,指定了实参,就会照常传入,这里指定的是10,所以传过去的是10

② 第二次调用Func时,并没有指定实参,所以进入函数后,

形参a会取缺省值0作为参数的值。

③ 因此,第一次打印的结果是10,第二次打印的结果是0

注意:

① 声明不能在 .h 和 .cpp 里同时出现缺省参数,要么声明里写,要么在定义里写!

② 缺省值必须是常量或全局变量。

② 缺省参数C++里面的,C语言不支持(编译器不支持)。

1.3缺省参数的分类

缺省参数分为全缺省参数半缺省参数

① 全缺省参数:函数中的所有参数都给了缺省值。

② 半缺省参数:函数中的参数从右往左给一部分的缺省值。

1.3.1全缺省参数

必须所有参数都带有缺省值,才能叫作全缺省参数。

代码演示:

#include using namespace std; void Func(int a = 10, int b = 20, int c = 30)、{printf("%d %d %d\n", a, b, c);} int main(){Func(); // 不穿,一个都没传Func(1);// 只传了一个Func(1, 2); // 传了两个,但没完全传Func(1, 2, 3);// 全都传了,就没缺省参数什么事了 return 0;}

打印:

10 20 30

1 20 30

1 2 30

1 2 3

解析:

① 第一次调用 Func 时,什么都没有传,所以结果直接就采用默认值。

② 第二次调用 Func 时,只传了一个参数,那么结果只有 a 不是默认值。

③ 第三次调用 Func 时,传了两个参数,那么结果只有 c 会是默认值了。

④ 最后一次调用 Func 时,所有参数都传了,那么结果都不会是默认值。

只想传中间的b可以吗?:

#include using namespace std; void Func(int a = 10, int b = 20, int c = 30) {printf("%d %d %d\n", a, b, c);} int main(){Func(, 2,);//错误 return 0;}

不可以!

参数的传递按照语法是从左往右传递的,因为这是语法定死的,所以没有办法传。

1.3.2半缺省参数

半缺省参数:函数中的所有参数从右往左连续地缺省一部分

这一部分可以是多个参数,也可以是一个参数(一个也算一部分),

但是它们必须是 “连续地” 。参数传递顺序根据根据函数调用约定。

注意事项:

① 半缺省并不是缺省一半,而是缺省一部分。

② 半缺省参数必须从右往左缺省,且必须是连续地。即,必须从右往左连续缺省。

吐槽:既然不是缺省一半,还叫半缺省参数,这合理吗?这不合理!(小声)

这个 “半” 字确实用的不合理,倒不如叫”部分缺省参数” ,会显得更加合理一些。

代码演示:

#include using namespace std; //从左往右 "连续地"void Func(int a, int b, int c = 30) {printf("%d %d %d\n", a, b, c);}/* 半缺省:从右往左连续地缺省一部分参数 a - 必须传 (因为没缺省) b - 必须传 (因为没缺省) c - 可传可不传 (因为缺省了)*/ int main(void){Func(1, 2); // a b 没缺省,所以必须要传,c缺省了所以可以不传Func(1, 2, 3);// 都传 return 0;}

打印:1 2 301 2 3建议:既然大佬是这么设计的,那我们也没办法。所以为了迎合这个特性,设计函数的时候如果有参数是必须要传递的,就放到前面;不是必须要传的,可以放到后面(制作成缺省参数)。

1.4缺省参数的应用场景

缺省参数的运用场景有很多,我们随便举个例子。

我们在学习数据结构时,实现顺序表、栈时定义容量capacity时,默认值我们当时推荐的是给4,这里就可以设置缺省值:

演示(仅展示部分代码):

typedef struct Stack {int* array;int top;int capacity;} Stack; void StackInit (Stack* pst,int capacity = 4 )// 设置缺省值为4(默认容量为4){pst->array = (int*)malloc(sizeof(int) * capacity);pst->top = 0;pst->capacity = capacity;}int main(){Stack st;StackInit(&st); // 不知道栈最多存多少数据,就用缺省值初始化StackInit(&st, 100); // 知道栈最多存100数据,显示传值。这样可以减少增容次数。 return 0;}

这么一来,就不需要考虑增容的概念了,这就是缺省参数的好处。所以,这个特性确实是很有用的,可以让我们更方便。

2.函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义, 即该词被重载了。 比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个 是男足。前者是 谁也赢不了 ,后者也是 “谁也赢不了” “谁也赢不了” ,就相当于被重载了。

2.1函数重载的概念

函数重载常用来处理实现功能类似数据类型不同的问题。

函数重载:C++ 允许在同一个作用域中存在同名的函数。

下面三个不同只要满足一个不同,就可以触发函数重载:

①参数类型不同

②参数个数不同

③ 参数顺序不同

代码演示: ①参数类型不同

#include using namespace std;int Add(int x, int y){cout << "int Add:" << endl;// 为了方便区分return x + y;}double Add(double x, double y){cout << "double Add:" << endl;// 为了方便区分return x + y;}int main(){cout << Add(1, 2) << endl;cout << Add(1.1, 2.2) << endl;return 0;}

打印输出:

②参数个数不同

#include using namespace std;void Func(int a) {cout << "Func(int a)" << endl;}void Func(char b, int a) {cout << "Func(char b, int a)" << endl;}int main(){Func(10);Func('A', 20);return 0;}

③ 参数顺序不同

#include using namespace std;void Func(int a, char b) {cout << "int a, char b" << endl;}void Func(char b, int a) {cout << "char b, int a" << endl;}int main(void){Func(10, 'A');Func('A', 10);return 0;}

2.2不支持函数重载的情况

除了上面讲的三种情况,其他情况都不能构成函数重载

常见错误:①返回值不同,调用时无法区分: 函数重载不能依靠返回值的不同来构成重载,因为调用时无法根据参数列表确定调用哪个重载函数。

#include using namespace std; int func(double d) {;} void func(double d) {;}int main(){foo(1.1);// " />#include using namespace std;void func(){cout << "func()" << endl;}void func(int a = 0){cout << "func(int a)" << endl;}int main(){//func(); // 调用存在歧义func(1);// 可以(调用下面的有参数的函数)return 0;}

3.引用

3.1引用的概念

引用 不是新定义一个变量,而 给已存在变量取了一个别名 编译器不会为引用变量开辟内存空间 ,它和它引用的变量 共用同一块内存空间。 比如你的真实姓名已存在, 你的小名或者外号就是一个别名。语法:数据类型& 引用名= 引用实体; 这里的&可不是取地址啊!它是放在数据类型后面的&,一定要区分开来! 代码演示:

#include using namespace std;int main(){int a = 10;int& ra = a;//<====定义引用类型//(这里取名为 ra,因为引用的英文是 reference,所以后面命名变量时会简写为 r,或者 ref 来代表引用)printf("%p\n", &a);printf("%p\n", &ra);cout << a << endl;cout << ra << endl;return 0;}

引用在语法层,我们要理解这里没有开新空间,就是对原来的取了一个新名称而已。

再次注意:

① 引用并不是新定义一个变量,只是给一个变量取别名。

② 编译器不会为引用的变量开辟内存空间,它和它引用的变量会共用同一块内存空间。

3.2引用的特性

1. 引用在定义时必须初始化 2. 一个变量可以有多个引用 3. 引用一旦引用一个实体,再不能引用其他实体前两个好理解,第三个给出代码演示:

#include using namespace std;int main(){int a = 10;int& ra = a;int b = 20;ra = b; // " />3.3引用做参数 

我们在C语言教学中讲过Swap两数交换的三种方式。

我们当时用的最多的就是利用临时变量去进行交换。

如果把它写成函数形式就是这样:

#include using namespace std;void Swap(int* px, int* py){int tmp = *px;*px = *py;*py = tmp;}int main(){int a = 10;int b = 20;cout << a << ' ' << b << endl;Swap(&a, &b);// 传址cout << a << ' ' << b << endl;return 0;}

这里我们调用Swap函数需要传地址,

因为形参是实参的一份临时拷贝,改变形参并不会对实参产生实质性的影响。

但是,我们学了引用之后我们就可以这么玩:

#include using namespace std;void Swap(int& ra, int& rb) {int tmp = ra;ra = rb;rb = tmp;}int main(){int a = 10;int b = 20;cout << a << ' ' << b << endl;Swap(a, b); // 这里既没有传值,也没有传地址,而是传引用cout << a << ' ' << b << endl;return 0;}

是怎么做到交换的?

我们知道,形参是定义在栈帧里面的。

实际调用这个函数的时候,才会给 ra 和 rb 开空间。调用这个函数的时候,把实参传给形参。

那什么时候开始定义的?实参传给形参的时候开始定义的。

ra 是 a 的别名,rb 是 b 的别名,所以 ra 和 rb 的交换,就是 a 和 b 的交换。

因此,我们利用这一特点,就可以轻松实现两数的交换。

我们来梳理一下,顺带复习一下之前讲的函数重载。

在我们一共学了三种传参方式:传值、传地址、传引用。

#includeusing namespace std;void Swap(int x, int y) {int tmp = x;x = y;y = tmp;cout << 1 << endl;}void Swap(int* px, int* py) {int tmp = *px;*px = *py;*py = tmp;cout << 2 << endl;}void Swap(int& rx, int& ry) {int tmp = rx;rx = ry;ry = tmp;cout << 3 << endl;}int main(){int a = 10;int b = 20;Swap(&a, &b);//Swap(a, b);// 报错return 0;}

这里Swap(a,b) 为什么会报错呢?

这三个Swap是可以构成函数重载的,

只要不影响它的函数名修饰规则,就不会构影响!

换言之,修饰出来的函数名不一样,就支持重载!

但是 Swap(a,b) 调用时存在歧义。调用不明确!

编译器不知道调用哪一个,是传值还是传引用,所以会报错。

当时再讲数据结构单链表的时候用的是二级指针,当时没有采用头结点的方式。

那么要传指针的地址,自然要用二级指针的方式接收。

现在我们学了引用,我们就可以试着用引用的方法来解决了(这里我们把 .c 改为 .cpp)

任何类型都是可以取别名的,指针也不例外:

int a = 10;int& ra = a; int* pa = &a;int*& rpa = pa

我们来看如何用引用的方法来实现!

#include #include #includetypedef int SLNodeDataType; typedef struct SingleListNode {SLNodeDataType data; // 用来存放节点的数据struct SingleListNode* next; // 指向后继节点的指针} SLNode; void SListPrint(SLNode* pHead);void SListPushBack(SLNode*& rpHead, SLNodeDataType x);// ... 略

SList.cpp:

#include "SList.h" /* 打印 */void SListPrint(SLNode* pHead) {SLNode* cur = pHead;while (cur != NULL) {printf("%d -> ", cur->data);cur = cur->next;}printf("NULL\n");} /* 创建新节点 */SLNode* CreateNewNode(SLNodeDataType x) {//创建,开辟空间SLNode* new_node = (SLNode*)malloc(sizeof(SLNode));//malloc检查if (new_node == NULL) {printf("malloc failed!\n");exit(-1);}//放置new_node->data = x; //存传入的数据new_node->next = NULL; //next默认置空 return new_node; //递交新节点} /* 尾插(指针的引用) */void SListPushBack(SLNode*& rpHead, SLNodeDataType x) {//创建新节点SLNode* new_node = CreateNewNode(x);//如果链表是空的if (rpHead == NULL) {//直接插入即可rpHead = new_node;}else {//找到尾结点SLNode* end = rpHead;while (end->next != NULL) {end = end->next; //令end指向后继节点}//插入end->next = new_node;}}

解读:这里的SLNode*&rpHead就是pHead的一个别名。 Test.cpp:

#include "SList.h" // 这里我们不传二级指针了。//void TestSList1()//{//SLNode* pList = NULL;//SListPushBack(&pList, 1);//SListPushBack(&pList, 2);//SListPushBack(&pList, 3);//SListPushBack(&pList, 4);////SListPrint(pList);//} // 使用引用的方法:// 我们传 指针的 引用!void TestSList2(){SLNode* pList = NULL;SListPushBack(pList, 1);SListPushBack(pList, 2);SListPushBack(pList, 3);SListPushBack(pList, 4); SListPrint(pList);}int main(){TestSList2();return 0;}

3.4 传值返回

这是我们以前的传值返回:

int Add(int a, int b) {int c = a + b;return c;} int main(){int ret = Add(1, 2);cout << ret << endl;return 0;}

这里return的时候会生成一个临时变量(c为 3)

将 3 复制给这个临时变量,然后返回给ret

如果我们直接把c交给ret,就会出现一些问题。

如果直接取cret,取到的是 3 还是随机值,就要取决于栈帧是否销毁空间!

这个时候严格来说,其实都是非法访问了。

因为这块空间已经还给操作系统了,这就取决于编译器了。

有的编译器会清,有的编译器不会清,这就太玄学了!

所以,在这中间会生成一个临时变量,来递交给 ret 。

而不是直接用 c 作为返回值,造成非法访问。

所以这里不会直接用 c 作为返回值,而是生成一个临时变量。

那么问题来了,这个临时变量是存在哪里的呢?

① 如果 c 比较小(4或8),一般是寄存器来干存储临时变量的活。

② 如果 c 比较大,临时变量就会放在调用 Add 函数的栈帧中。

总结:所有的传值返回都会生成一个拷贝

(这是编译器的机制,就像传值传参会生成一份拷贝一样)

3.5 引用做传值返回

我们已经知道,普通的传值返回会生成一个临时变量了。

我们来试试引用的返回。

引用返回的意思就是,不会生成临时变量,直接返回c的别名。

这段代码存在的问题:

① 存在非法访问,因为 Add(1, 2) 的返回值是 c 的引用,所以 Add 栈帧销毁后,

会去访问 c 位置空间。

② 如果 Add 函数栈帧销毁,清理空间,那么取 c 值的时候取到的就是随机值,

给 ret 就是随机值,当前这个取决于编译器实现了。VS 下销毁栈帧,是不清空间数据的。

栈帧:C语言中,每个栈帧对应着一个未运行完的函数。 栈帧中保存了该函数的返回地址和局部变量。

既然不清空间数据,那还担心什么呢?

我们来看看下面这种情况:

#include using namespace std; int& Add(int a, int b) {int c = a + b;return c;} int main(){int& ret = Add(1, 2);cout << ret << endl;Add(10, 20);cout << ret << endl;// 这里ret变成30了 return 0;}

解读:我们并没有动ret,但是ret的结果变成了 30,因为栈帧被改了。

当再次调用Add时,这块栈帧的 "所有权" 就不是你的了。

我函数销毁了,栈帧就空出来了,新的函数覆盖了之前那个已经销毁的栈帧,

所以ret的结果变成 30 了。

结论就是:不要轻易使用引用返回!

那引用返回有什么存在的意义呢?等我们后面讲完类和对象后再细说。

总结:

日常当中是不建议用引用返回的,如果函数返回时,出了函数的作用域,

如果返回对象还未还给操作系统,则可以使用引用返回,如果已经还给操作系统了,

就不要用引用返回了,老老实实传值返回就行了。

通俗点说就是 ——看返回对象还在不在栈帧内,在的话就可以使用引用返回。

举个例子:静态变量,全局变量,出了作用域不会销毁

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

注意事项:临时变量具有常性

临时变量是右值(不可被修改),可以读但不能修改。

3.6关于引用的探讨

3.6.1比较传值和传引用的效率

那传值返回和传引用返回的区别是什么呢?

传引用返回速度更快。

以值作为参数或者返回值类型,在传参和返回期间,

函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝。

因此值作为参数或者返回值类型,效率是非常低下的,

尤其是当参数或者返回值类型(比如一些结构体)非常大时,效率就更低。

传值和传指针在作为传参以及返回值类型上效率相差十分悬殊。

引用的作用主要体现在传参和传返回值:

① 引用传参和传返回值,有些场景下面,可以提高性能(大对象+深拷贝对象)。

② 引用传参和传返回值,输出型参数和输出型返回值。

有些场景下面,形参的改变可以改变实参。

有些场景下面,引用返回,可以减少拷贝、改变返回对象。(了解一下,后面会学)

引用后面用的非常的多!非常重要!

3.6.2 引用和指针的区别

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

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

① 引用是在概念上定义一个变量的别名,而指针是存储一个变量的地址。

② 引用在定义时必须初始化,而指针是最好初始化,不初始化也不会报错。

③ 引用在初始化时引用一个实体后,就不能再引用其他实体,

而指针可以在任何时候指向任何一个同类型的实体。

④ 有空指针,但是没有空引用。

⑤ 在sizeof中含义不同,引用结果为引用类型的大小,

但指针始终是地址空间所占字节数(64位平台下占8个字节)

⑥ 引用++即引用的实体增加1,指针++即指针向后偏移一个类型的大小。

⑦ 有多级指针,但是没有多级引用。

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

⑨ 引用比指针使用起来相对更加安全。

总结:指针使用起来更复杂一些,更容易出错一些。(指针和引用的区别,面试经常考察)

使用指针有考虑空指针,野指针等等问题,指针太灵活了,所以相对而言没有引用安全!

3.7常引用

如果既要利用引用来提高程序的效率,又想要保护传递给函数的数据不能在函数中被改变,

就应使用常引用。常引用就是在前面引用的语法前+const

语法:const数据类型& 引用名= 引用实体;

一共有三种情况:分别是权限的放大、保持权限不变、权限的缩小。

3.7.1 权限的放大

下面是一个引用的例子:

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

如果对引用实体使用const修饰,直接引用会导致报错:

int main(){const int a = 10;int& ra = a; return 0;}

分析:导致这种问题的原因是,我本身标明了 const,这块空间上的值不能被修改。

我自己都不能修改,你 ra 变成我 a 的引用,意味着你修改 ra 可以修改我的 a,

这就是属于权限的放大问题,a 是可读的,你 ra 要变成可读可写的,当然不行。

那么如何解决这样的问题,我们继续往下看。

3.7.2 保持权限的一致

既然引用实体用了const进行修饰,我直接引用的话属于权限的放大,

我们可以给引用前面也加上const,让他们的权限保持不变。

给引用前面加上 const:

int main(){const int a = 10;const int& ra = a; return 0;}

解读:const int&ra=a的意思就是,我变成你的别名,但是我不能修改你。

这样a是可读不可写的,ra也是可读不可写的,这样就保持了权限的不变。

如果我们想使用引用,但是不希望它被修改,我们就可以使用常引用来解决。

3.7.3 权限的缩小

如果引用实体并没有被const修饰,是可读可写的,

但是我希望它的引用不能修改它,我们可以用常引用来解决。

a是可读可写的,但是我限制ra是可读单不可写:

int main(){int a = 10;const int& ra = a; return 0;}

解读:这当然是可以的,这就是权限的缩小。

举个例子,就好比你办身份证,你的本名是可以印在身份证上的,

但是你的绰号可以印在身份证上吗?

所以就需要加以限制,你的绰号可以被人喊,但是不能写在身份证上。

所以,权限的缩小,你可以理解为是一种自我约束

3.7.4 常引用的应用

举个例子:

假设x是一个大对象,或者是后面学的深拷贝的对象

那么尽量用引用传参,以减少拷贝。

如果Func函数中不需要改变x,那么这里就尽量使用const引用传参。

void Func(int& x) {cout << x << endl;} int main(){const int a = 10;int b = 20; Func(a);// 报错,涉及权限的放大Func(b);// 权限是一致的,没问题 return 0;}

加const后,让权限保持一致:

// "加上保持权限的一致"void Func(const int& x) {cout << x << endl;} int main(){const int a = 10;int b = 20; Func(a);// 权限是一致的Func(b);// 权限的缩小 return 0;}

解读:如此一来,a是可读不可写的,传进Func函数中也是可读不可写的,

就保持了权限的一致了。b是可读可写的,刚才形参还没使用const修饰之前,

x是可读可写的,但是加上const后,属于权限的缩小,x就是可读但不可写的了。

所以说引用做参数时和以前一样(甚至更建议)函数中不改变参数的值时,在前面+const

常引用后期会用的比较多,现在理解的不深刻也没关系,早晚的事情。

后面讲类和对象的时候会反复讲的,印象会不断加深的。

3.7.5 带常性的变量的引用

先看代码:

int main(){double d = 3.14;int i = d;cout << d << "" << i << endl;//输出了3.143return 0;}

这里的d是可以给i的,这个在C语言里面叫做隐式类型转换

它会把d的整型部分给i,浮点数部分直接丢掉。

但是我在这里加一个引用呢?

int main(){double d = 3.14;int& i = d;// 我能不能用i去引用d呢? return 0;}

运行结果:(报错)

直接用i去引用d是会报错的,思考下是为什么?

这里可能有的朋友要说,d是浮点型,i是整型啊,会不会是因为类型不同导致的?

但是奇葩的是 —— 如果我在它前面加上一个const修饰,

却又不报错了,这又是为什么?

int main(){double d = 3.14;const int& i = d;// " />

解析:因为内置类型产生的临时变量具有常性,不能被修改。

隐式类型转换不是直接发生的,而是现在中间产生一个临时变量。

是先把d给了临时变量,然后再把东西交给i的:

如果这里用了引用,生成的是临时变量的别名,

又因为临时变量是一个右值,是不可以被修改的,所以导致了报错。

结论:如果引用的是一个带有常性的变量,就要用带const的引用。

3.7.6 常引用做参数

前面提到过:使用引用传参,如果函数中不改变参数的值,建议使用const引用

举个例子:

一般栈的打印,是不需要改变参数的值的,这里就可以加上const

void StackPrint(const struct Stack& st) {...}

const 数据类型& 可以接收各种类型的对象。

使用 const 的引用好处有很多,如果传入的是 const 对象,就是权限保持不变;

普通的对象,就是权限的缩小;中间产生临时变量,也可以解决。

因为 const 引用的通吃的,它的价值就在这个地方,如果不加 const 就只能传普通对象。

又到了枯燥的学习知识点的阶段,如果想深入学习的话要学的东西还是多啊。

修炼内功,修炼内功

本篇完。(附下篇链接)

穿越回来贴个下篇链接:

从C语言到C++③(第一章_C++入门_下篇)内联函数+auto关键字(C++11)+范围for +nullptr_GR C的博客-CSDN博客