面向对象之【探索C++】硬核教程 造轮子的快乐源泉 【函数篇】
@[toc]
1.内联函数
1.1 内联函数的引出
c++从c中继承的一个重要特征就是效率。假如c++的效率明显低于c的效率,那么就会有很大的一批程序员不去使用c++了。
在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。
但是在c++出现之后,使用预处理宏会出现两个问题:
第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。
为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).
内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。
1.2 预处理宏缺陷
预处理器宏存在问题的关键是我们可能认为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来是一样的,因为也容易被混淆。但是其中也会有一些微妙的问题出现:
问题一:
我现在使用宏定义一个加法
#define MyAdd(x,y) x+y
void test01()
{
int ret = MyAdd(10, 20);
cout << "ret=" << ret << endl;
}
运行结果:ret=30
然后我在修改一下
int ret MyAdd(10, 20)*20
预期结果是600;
410?怎么来的,我们展开10+20*20;那我们加上小括号没问题了吧
#define MyAdd(x,y) ((x)+(y))
问题二:
那我们再来一种情况(比较大小):
#define MyCompare(a,b)((a)<(b))?(a):(b)
void test02()
{
int a = 10;
int b = 20;
int ret = MyCompare(a, b);
cout << "ret=" << ret << endl;
}
前面已经吃过亏了,提前加上小括号
int ret = MyCompare(a, b);
//展开((a)<(b))?(a):(b)
10确实小于20,我们++一下
MyCompare(++a, b);
分析一下,我们的++a加了一下,变成11还没有比20大,那就返回a(11),预期结果a=11;
结果是12?前面我们已经用括号已经解决了这个问题,那原因是什么导致结果出错呢?
我们展开((++a)<(b))?(++a):(b);
此时的a++了变成11,但是条件成立的话,我们就要执行,所以后面再++a了;这就是宏缺陷
- 需要加上小括号
- 加上小括号也不是预期结果
问题三:
我们再讲一个缺陷:宏函数也没有作用域;
那我们为什么使用宏函数,因为语句短小,使用宏可以直接替换;而写函数麻烦有压栈出栈的开销,但是我又想执行这个语句,用函数封装一下,然后我们就直接这样写(宏)直接替换。
使用宏有缺陷,接下来我们学习内联函数;
1.3 内联函数基本概念
在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。
在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。
我们拿上面的比较的代码用函数实现:
void mycompare(int a,int b)
{
int ret = a < b ? a : b;
cout << "ret::::" << ret << endl;
}
void test02()
{
int a = 10;
int b = 20;
mycompare(++a, b);
}
我们在函数的前面加上inline
,在处理的时候,其实就是把mycompare的那两行代码替换成mycompare(++a,b)
ok,其实是跟我们学的宏是一样的,对吧;其实不一样,它是代替宏的,因为宏有些缺陷,但函数呢,没有缺陷,使所以我们用内联函数弥补了;
使用内联函数需要注意一下:
inline void fun();//内联函数声明
void fun(){};//如果函数实现时候,没有加inline关键字,那么这个函数依然不算内联函数
inline void fun(){};//ok
注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
空间换时间?本来是mycompare(++a, b);
这么点空间,替换成:
void mycompare(int a,int b)
{
int ret = a < b ? a : b;
cout << "ret::::" << ret << endl<<endl;
}
肯定比那一行代码多,这就是空间替换了,那时间呢,在普通函数调用是需要压栈出栈的消耗,效率低;那我们使用内联函数,效率稍微高一点,一行代码变成两行但时间更快,这就是它的好处
1.4 类内部的内联函数
为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。(了解就行)
class Person{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}
构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。
1.5 编译器对内联函数的处理
内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?
对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。
当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。
类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。
但是c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:
不能存在任何形式的循环语句
不能存在过多的条件判断语句
函数体不能过于庞大
不能对函数进行取址操作
就算加了inline也不会当做内联函数处理
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
2.函数的默认参数
c++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。
void TestFunc01(int a = 10, int b = 20){
cout << "a + b = " << a + b << endl;
}
//注意点:
//1. 形参b设置默认参数值,那么后面位置的形参c也需要设置默认参数
void TestFunc02(int a,int b = 10,int c = 10){}
//2. 如果函数声明和函数定义分开,函数声明设置了默认参数,函数定义不能再设置默认参数
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){}
int main(){
//1.如果没有传参数,那么使用默认参数
TestFunc01();
//2. 如果传一个参数,那么第二个参数使用默认参数
TestFunc01(100);
//3. 如果传入两个参数,那么两个参数都使用我们传入的参数
TestFunc01(100, 200);
return EXIT_SUCCESS;
}
理解默认值没有一点难点,现在我们写个方法:
void func(int a,int b)
{
cout << "a+b=" << a << b<<endl;
}
然后写个调用函数:
void test01()
{
func(10,20);
}
但是我不想在func()函数传入参数,我们可以这样写:
void func(int a=10,int b=20)
{
cout << "a+b=" << a << b<<endl;
}
void test01()
{
func();
}
这就是函数的默认参数,参数后面=…
注意点:
函数的默认参数从左向右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数。
如果函数声明和函数定义分开写,函数声明和函数定义不能同时设置默认参数。
3.函数的占位参数
c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
我们看一下代码:
void TestFunc01(int a,int b,int){
//函数内部无法使用占位参数
cout << "a + b = " << a + b << endl;
}
//占位参数也可以设置默认值
void TestFunc02(int a, int b, int = 20){
//函数内部依旧无法使用占位参数
cout << "a + b = " << a + b << endl;
}
int main(){
//错误调用,占位参数也是参数,必须传参数
//TestFunc01(10,20);
//正确调用
TestFunc01(10,20,30);
//正确调用
TestFunc02(10,20);
//正确调用
TestFunc02(10, 20, 30);
return EXIT_SUCCESS;
}
占位参数:
- 函数调用的时候必须提供这个给参数,但是用不到参数;
- 没有什么大用途,只有后面重载 ++符号才一点点用
- 可以有默认值
C语言中没有默认参数也没有占位参数
如果函数里面有了默认参数,那么函数实现的时候必须没有:
void myFunc(int a = 10, int b = 10);
void myFunc(int a = 10, int b = 10){};
运行错误
函数声明和实现,只能有一个默认参数,不要同时出现默认参数。
3.函数重载(重点)
生活中**”重“**这个字是念(chong)还是(zhong)多音字,在不同场景中就有不同的意思;这就是这本质。
3.2 函数重载基本语法
void func()
{
}
void func()//err
{
}
像这两个函数,在C或者C++都无法运行,我们在第二个func函数加上形参 C++上就可以运行了
#include<iostream>
using namespace std;
void func()
{
cout << "无参数的func()" << endl;
}
void func(int a)
{
cout << "有参数的func(int a)" << endl;
}
int main(){
func();
return EXIT_SUCCESS;
}
那主函数会调用那个func呢?
那怎么调用有参的func?
重载满足条件:
-
必须在同一个作用域,函数名称相同
-
函数的参数(个数)不同,或者(类型)不用,或者(顺序)不同
1️⃣个数:
func()
和func(int a)
会发生重载2️⃣类型:一个func(int a)int类型,一个func(double a)浮点数类型,那func(1.1)会调用哪个?double的
3️⃣顺序:
func(double a,int a)
和func(int a ,double b)
那func(1.1 , 3)就会调用func(double a,int b)
-
-
返回值可以作为函数的条件吗?
int func(int a, double b) { cout << "调用int a,double b" << endl; return 1; } void func(int a, double b) { cout << "调用int a,double b" << endl; }
不可以,因为编译器不知道调用哪个,产生了二义性
-
当函数重载碰到了默认参数的时候,要注意避免二义性问题
void func(int a , int b = 10){} void func(int a){}
我用
func(10)
调用,怎么调?两个都可以调,产生二义性了,编译器报错了;
-
-
引用的重载版本
void func(int &a){} void test() { int a = 10; func(a); }
这样没有错,修改一下
void test() { //int a = 10; func(10); }
引用必须要引用合法的内存空间
加const和不加const有什么取别,超详细的const讲解讲到了const展开为:int tmp=10;const int &a=tmp
3.3 函数重载的实现原理
编译器为了实现函数重载,也是默认为我们做了一些幕后的工作,编译器用不同的参数类型来修饰不同的函数名,比如void func(); 编译器可能会将函数名修饰成_func,当编译器碰到void func(int x),编译器可能将函数名修饰为_func_int,当编译器碰到void func(int x,char c),编译器可能会将函数名修饰为_func_int_char我这里使用”可能”这个字眼是因为编译器如何修饰重载的函数名称并没有一个统一的标准,所以不同的编译器可能会产生不同的内部名。
void func(){}
void func(int x){}
void func(int x,char y){}
以上三个函数在linux下生成的编译之后的函数名为:
_Z4funcv //v 代表void,无参数
_Z4funci //i 代表参数为int类型
_Z4funcic //i 代表第一个参数为int类型,第二个参数为char类型
3.4 extern “C”浅析
我们创建1个.c文件和.h文件,再创建.cpp调用c函数
错误 2 error LNK1120: 1 个无法解析的外部命令 D:\c-code\ConsoleApplication3\Debug\day2.exe
出现上面这种错误都是没有链接你的函数或者方法,上面代码问题就是show()函数无法链接问题,为什么解析不到?
答:在C++中,函数是可以发生重载的,编译器会把这个函数名称偷偷改变 _show void,c语言没有函数重载
C++中想要调用C语言方法:
在C++中加入:extert "C"需要链接的函数名
extert "C" void show();//show方法 按照C语言方式做连接
然后在Cpp代码中已经include了,所以注释#include"test.h"
确实可以,那要是一千行的代码量,那我们也要写一千行的extert "C" void show();
🆗,那我们就不要在C++里面处理,我在C语言代码处理,把C++的entern "C"
注释;
我们在C头文件使用ifdef __cplusplus__
意思是:如果没有定义这个文件那我们就定义一个,里面包含以下代码:
# ifdef __cplusplus__//两个下划线
extern "C"{
#endif
#include<stdio.h>
void show();
#ifdef __cplusplus__
}
#endif
内部处理按照如果是C++的代码就加上extern "C"
,如果是不是C++代码就跳过extern “C”,执行include<stdio.h>
的两行代码,然后在cpp文件中取消注释#include"test.h"
注把释extern "C" void show();
以下在Linux下测试:
c函数: void MyFunc**(){} ,**被编译成函数: MyFunc
c++函数: void MyFunc**(){},**被编译成函数: _Z6Myfuncv
通过这个测试,由于c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误,以上例,c++中调用MyFunc函数,在链接阶段会去找Z6Myfuncv,结果是没有找到的,因为这个MyFunc函数是c语言编写的,生成的符号是MyFunc。
那么如果我想在c++调用c的函数怎么办?
extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。
MyModule.h
#ifndef MYMODULE_H
#define MYMODULE_H
#include<stdio.h>
#if __cplusplus
extern "C"{
#endif
void func1();
int func2(int a,int b);
#if __cplusplus
}
#endif
#endif
MyModule.c
#include"MyModule.h"
void func1(){
printf("hello world!");
}
int func2(int a, int b){
return a + b;
}
TestExternC.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#if 0
#ifdef __cplusplus
extern "C" {
#if 0
void func1();
int func2(int a, int b);
#else
#include"MyModule.h"
#endif
}
#endif
#else
extern "C" void func1();
extern "C" int func2(int a, int b);
#endif
int main(){
func1();
cout << func2(10, 20) << endl;
return EXIT_SUCCESS;
}
重点回顾
1 设计类 抽象类
1.1 class 类名{
1.2 public 公共权限
1.3 设置 成员属性
1.4 设置 成员函数
1.5 }
1.6 使用类 创建对象 实例化对象
1.7 类名 对象名
1.8 通过对象 来设置属性 调用成员函数
1.9 类和对象 关系???
1.9.1 类是对对象的抽象
1.9.2 对象是对类的实例
2 内联函数 解决宏缺陷问题
2.1 给编译器一个建议,加上关键字,编译器不一定按照内联处理
2.2 不加关键字,也许编译器还偷摸的给你加inline
2.3 成员函数 默认加上关键字
2.4 函数声明加了关键字,函数实现也要加inline关键字
3 函数默认参数
3.1 参数可以有默认值
3.2 如果有一个位置有了默认值,那么从这个位置开始,从左往右都必须有默认值
3.3 函数声明和实现 只能有一个有默认值
4 函数占位参数
4.1 void func(int) 占位参数 调用时候必须要提供这个参数
4.2 占位参数也可以有默认值
4.3 c语言中没有默认参数 和占位参数
5 函数重载的基本语法
5.1 函数名称相同 又在同一个作用域下
5.2 函数参数个数不同、类型不同、顺序不同都可以满足重载条件
5.3 函数的返回值可以作为函数重载条件吗? 不可以
5.4 当函数重载碰到了函数默认参数 要注意避免二义性
6 extern C浅析
6.1 解决了C++文件中调用C语言的代码
6.2 ifdef __cplusplus extern “C” {}
- 如果对大家有帮助,请三连支持一下!
- 有问题欢迎评论区留言,及时帮大家解决!
- 点赞
- 收藏
- 关注作者
评论(0)