文章目录
- 前言
- [1. 缺省参数](#1. 缺省参数)
-
- [1.1 为什么要有缺省函数?](#1.1 为什么要有缺省函数?)
- [1.2 什么是缺省参数?](#1.2 什么是缺省参数?)
- [1.3 缺省参数的分类](#1.3 缺省参数的分类)
-
- [1.3.1 全缺省参数](#1.3.1 全缺省参数)
- [1.3.2 半缺省参数](#1.3.2 半缺省参数)
- [1.4 使用缺省参数需要避的"坑"](#1.4 使用缺省参数需要避的"坑")
- [2. 函数重载](#2. 函数重载)
-
- [2.1 函数重载概念](#2.1 函数重载概念)
- [2.2 函数重载的情况](#2.2 函数重载的情况)
- [2.3 函数重载背后的原理](#2.3 函数重载背后的原理)
-
- [2.3.1 编译和链接](#2.3.1 编译和链接)
- [2.3.2 函数调用的汇编代码讲解](#2.3.2 函数调用的汇编代码讲解)
前言
在我们学习C++的命名空间之后 ,我们知道这是一个解决C语言中无法解决的问题,这个问题被我们称之为"命名冲突"。
那么在本章中 ,我们继续讲解一些在C语言中无法解决的问题,来看看本贾尼大佬(C++的创造者)是怎么解决这些问题的。
1. 缺省参数
1.1 为什么要有缺省函数?
相信大家在学习C语言中,一定遇到过一个这样的苦恼。比如我现在在使用着一个自定义的申请动态内存的空间函数,在C语言中,就只能乖乖的给函数传递实参(一个指针变量和需要开辟的空间大小)。突然有一天,我不想再给这个函数传递需要开辟的空间大小的那个实参了,但是如果不将参数全部传完的话,在C语言的视角中你这个就是一个语法错误了。
本贾尼大佬也是受够了C语言的这种设计,为此缺省参数就在C++中诞生了。缺省参数解决的问题就是当我不想给函数传递对应的参数时,会采取某种机制使得编译器认为你已经给这个函数对应的参数。 那至于是什么机制,大家不妨接着往下看!😊
1.2 什么是缺省参数?
缺省参数是声明或定义函数时为函数的参数指定一个缺省值(默认值)。在调用该函数时,如果我们没有指定实参的话则采用该形参的缺省值(默认值),否则就使用实参的值。
给大家想感受一下缺省参数的魅力:
cpp
#include<iostream>
using namespace std;
//这里的形参a就是一个缺省参数
void func(int a = 1)
{
cout << a << endl;
}
int main()
{
func(); //没有给定实参
func(66);//给定了实参
return 0;
}
1.3 缺省参数的分类
缺省参数被分为两种类型:全缺省参数和半缺省参数
1.3.1 全缺省参数
全缺省参数顾名思义就是在声明或定义函数时,给每一个形参都设置缺省值。
c
//全缺省参数
void func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
1.3.2 半缺省参数
半缺省参数就是部分形参设置缺省值。
这里有的读者可能会认为,半缺省参数是指一半的形参设置缺省值。这种想法时不可取的!!!
半缺省参数是指部分缺省,而不是一半缺省!!!
c
//2.半缺省参数(形参部分缺省)
void func2(int a, int b = 2, int c = 3)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
1.4 使用缺省参数需要避的"坑"
一个好东西的使用往往是需要付出一些代价的,那我们就来看看缺省参数时我们需要注意一些细节的点。
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
cpp
//以下都是一些错误的写法
//1.形参并不遵从右往左依次给出
void func(int a = 10, int b = 20, int c);
//2.缺省参数间隔着给
void func(int a = 10, int b, int c = 30);
- 缺省参数不能在函数声明和定义中同时出现。
很多人可能对这点不是很理解这个点,那接下来我给大家讲明白这个点。请大家看下面的代码:
c
//错误的例子
//test.h
void Func(int a = 10);
//test.c
void Func(int a = 20)
{
...
}
如果声明和定义位置同时出现缺省参数,但是恰巧两个位置提供的值不一样,那编译器就无法确定到底该采用哪个缺省值。 会出现歧义!
因此,缺省参数不能同时在函数声明和定义中同时出现!
那有的读者就会问出一个这样的问题:那我到底是在函数声明时使用缺省参数,还是在函数定义中使用缺省参数?
答案是,二者选其一即可!只要不是同时使用就行。
- 缺省值必须得是常量或者是全局变量。
- C语言不支持(编译器不支持)
2. 函数重载
🍉在语言层面来说,什么叫做"重载"?重载就是"一词多义"。
🍉那将这个说法放到"函数"中,大家也许就可以猜出"函数重载"大概是个什么东西了。讲大白话就是虽然两个函数命名是一样的,但是这两个函数实现的功能不完全一样,甚至是截然不同。
2.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
下面我来演示一下函数重载:
cpp
#include<iostream>
using namespace std;
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
代码分析:可以看到,我们定义了两个Add的函数,但是仔细一看会发现,他们形参列表中的参数类型不同,因此这两个函数构成了重载。
2.2 函数重载的情况
- 🍉参数类型的不同。
cpp
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
- 🍉参数个数不同。
cpp
//2. 参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
- 🍉参数类型的顺序不同。
cpp
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
注意:仅仅是函数返回值不同是不构成函数重载。
比如:
int Func(int a, char b)
和char Func(int a, char b)
,这两个函数不构成重载。
练习:现在,我给大家下面一段代码,请大家判断这段代码能否通过编译阶段?如果通过了,那能不能正常的执行出结果?
cpp
#include<iostream>
using namespace std;
void Func(int a = 10)
{
cout << "Func(int a = 10)" << endl;
}
void Func()
{
cout << "Func()" << endl;
}
int main()
{
Func();//能编译过吗,如果能,那能正常执行吗?
}
答案是:不能通过编译。它会给出下面错误报告
从代码的角度,我们也能够了解确实是这样的。
2.3 函数重载背后的原理
温馨提示:如果你不想深入了解函数重载背后原理的读者,可以直接略过本小节的内容。
本小节重点围绕着一个话题,"为什么C++支持函数重载,而C语言却不支持呢?"
想要知道答案,我们必须得对编译和链接过程有一定的了解。并且我会用使用反汇编代码,给大家逐一的讲解C++和C语言是如何对待函数的。
本节讲解代码的例子:
c
//Stack.h
#pragma once
#include<stdio.h>
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* pst);
void StackPush(struct Stack* pst, int x);
void StackPush(struct Stack* pst, double x);
//Stack.c
#include"Stack.h"
void StackInit(struct Stack* pst)
{
printf("void StackInit(struct Stack* pst)\n");
}
void StackPush(struct Stack* pst, int x)
{
printf("void StackPush(struct Stack* pst, int x)\n");
}
void StackPush(struct Stack* pst, double x)
{
printf("void StackPush(struct Stack* pst, double x)\n");
}
//test.c
#include"Stack.h"
int main()
{
struct Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 1.1);
}
2.3.1 编译和链接
如果对编译和链接感兴趣的话,可以看看往期的这篇文章编译和链接(细节的king)。这里我画一幅图给大家理解:
2.3.2 函数调用的汇编代码讲解
将代码转到反汇编,我们就能看到函数调用的指令:
可以看到是一条名为call
的带用指令,我们执行call指令后,他会跳转到另一条指令jmp
。
继续让代码往下执行,你又会发现一个新大陆:
你会发现跳转到StackInit函数里面了,这个不就是调用函数了!再仔细观察,你还会发现一件事:jmp真得很牛!
现在,我有个问题想要问一下大家:Stack.h的文件里面只包含了函数的声明,那test.c文件里面的main函数在调用函数时,怎么知道目标函数的地址的?
关于这个问题的理解,可以给大家讲一个故事。假设你现在要准备给女朋友结婚急需一笔资金来买房,这时你想到了你大学时期最要好的一个兄弟,你打电话给他。你说最近...,好兄弟说没有问题,过几天我就把钱打给你。听到这个消息之后,你就去交这个房子的定金了。
以上的这个例子,"买房的人"就像是编译器。编译器是不知道这个函数的地址,但是因为接受了这个承诺,编译器就会认为这个函数是存在的。等到链接的过程时,也就是兑现承诺的时候,就会把这个函数的地址告诉给编译器。
那具体是怎么告诉的呢?这个就涉及到链接过程中,会产生出一个名为"符号表"的东西,上面就记录着每个函数对应的地址。兑现承诺就相当于把地址填入到这个表格中!!!
好了,讲了这么多,回归我们的主体:为什么C++支持函数重载,而C语言却不支持呢?
如果你对我上述的讲法理解的话,那么我接下来的这段话就十分的关键了。
在C语言中,它对函数的名的处理是直接采用函数名本身的不加以任何的修饰。而在C++中,它是通过对函数名的修饰,使得函数重载函数的得以区分。也就是在符号表中可以填入不同的函数标记,这个特性就是得C++能够支持函数重载。
接下来我来证明给大家看看,由于VS的编译器将编译链接这个过程给集成化了,所以我用g++编译器给大家显示。
以上是对应的图,大家可以对应着过程看看。
最后我们可以总结一下:之所以C语言不支持函数重载,而C++支持函数重载,是因为它们对函数名修饰规则不一样导致的。
本文的内容到这里就结束了,如果觉得本文对你有帮助的话,麻烦给偶点个赞吧!!!