突破编程_C++_基础教程(函数(一))

1 函数声明

函数声明的作用是告诉编译器即将要定义的函数的名字是什么,返回值的类型是什么以及函数是什么。函数的声明可以有多次,但是函数的定义只能有一次。如果只有函数声明没有函数定义,则可以通过编译,但是链接时会报错。
通常把函数声明叫做函数原型,把函数定义叫做函数实现。

1.1 函数声明的基本语法

函数声明(函数原型)的语句结构:
返回值类型 函数名(参数1, 参数2, ...)
函数的声明和变量的声明一样,是一句语句。所以在语句结束要加上分号。比如:

int add(int a, int b);

函数名:类似于变量名,函数名就是函数的名字,即函数的标识符。函数名由字母、数字以及下画线组成,并且不能以数字开头。
返回值类型:指的是函数会返回数据的类型。如果某个函数不返回任何值,则定义其返回类型是 void 。
参数列表:输入到函数内部的数据类型。函数的参数位于一个括号中,并且用逗号分隔,括号中的部分就称做函数的参数列表。

1.2 constexpr 关键字

constexpr 关键字在 C++11 中引入,使用 该关键字声明的函数可以在编译时进行计算。这样可以提供更好的性能和编译时优化。同时,编译器还可以在编译时对 constexpr 表达式进行类型检查和错误检查。样例代码如下:

#include <iostream>constexpr int add(int a, int b)
{return a + b;
}int main()
{int type = 2;switch (type){case add(1, 1):				//编译时会将 add(1, 1) 替换为 2{printf("hello constexpr!\n");break;}default:break;}return 0;
}

1.3 内联化

内联函数的目的是为了提高函数的执行效率,用关键字 inline 放在函数声明的前面即可将函数指定为内联函数。编译器会将内联函数复制在程序中的每一个调用点上,如此便可省去调用函数过程中的参数入栈、函数跳转、保护现场、回复现场等过程,提高性能。但是由此付出的代价是打包后的程序体积会变大。样例代码如下:

inline int add(int a, int b);

1.4 noexcept 关键字

noexcept 关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果 noexecpt 函数在运行时向外抛出了异常,程序会直接终止。样例代码如下:

int add(int a, int b) noexcept
{throw(0);			//警告: warning C4297: “add”: 假定函数不引发异常,但确实发生了return a + b;
}

2 函数定义

函数定义也叫做函数实现,与函数声明的不同之处在于函数定义具有函数体(即组成函数的代码)。 函数定义的格式是:

返回值类型 函数名(参数1, 参数2, ...)
{//函数体
}

在 C++ 程序中,函数的定义必须写,而函数的声明有时必须写,有时可以省略不写。
如果在调用前函数已经定义,则不必再写函数的声明了。例如:

#include <iostream>//函数的定义
int add(int a, int b)
{return a + b;
}int main()
{add(1, 2);			//直接调用return 0;
}

3 函数参数

C++ 函数的参数分为形式参数和实际参数。形式参数是定义在函数声明或定义中的参数,也称为形参。实际参数是在调用函数时传递给函数的值或变量,也称为实参。

3.1 函数参数传递方式

C++ 支持三种参数传递方式:按值传递、按引用传递以及按指针传递。

3.1.1 按值传递

按值传递是将实际参数的值复制到形式参数。使用这种方式,调用函数本身不对实参进行操作。例如:

#include <iostream>//函数的定义
void swap(int a, int b)
{int c=a;a=b;b=c;
}int main()
{int a=1,b=2;swap(a,b);				//a,b 交换值失败printf("a=%d, b=%d\n",a,b);return 0;
}

上面代码的输出为:

a=1, b=2

3.1.2 按引用传递

按引用传递是将实际参数的地址传递给形式参数,此时的形参相当于实参的别名。使用这种方式,对形参的改变会影响到实参。例如:

#include <iostream>//函数的定义
void swap(int &a, int &b)
{int c=a;a=b;b=c;
}int main()
{int a=1,b=2;swap(a,b);				//a,b 交换值成功printf("a=%d, b=%d\n",a,b);return 0;
}

上面代码的输出为:

a=2, b=1

3.1.3 按指针传递

按指针传递是指将实参的地址传递给形参,函数内部可以通过指针来操作实参的值。但需要注意指针为空的情况。样例代码如下:

#include <iostream>//函数的定义
void swap(int* a, int* b)
{int c=*a;*a=*b;*b=c;
}int main()
{int a=1,b=2;swap(&a,&b);				//a,b 交换值成功printf("a=%d, b=%d\n",a,b);return 0;
}

上面代码的输出为:

a=2, b=1

需要注意的是:从效率上来说,按引用传递与按指针传递基本一样(按值传递有拷贝过程,性能很差),不过从安全角度出发,引用传递在参数传递过程中执行强类型检查,而指针传递的类型检查较弱,特别的,如果参数被声明为 void ,那么就不会做类型检查。所以推荐只用引用传递,最好不用指针传递。

3.2 设计函数参数的原则

设计函数参数的原则是:能用引用的就用引用(提高性能),能用 const 的就用 const(方便调用) 。比如创建一个用于打印字符串的函数。从这个需求来看,这个函数有一个字符串的入参,在其函数体中,只要读入这个字符串入参即可,无需对其修改。如果设计一个按值传递的函数,如下:

void printStr(string str)
{printf("%s\n",str.c_str());
}

这样设计有一个弊病:每次调用该函数,都需要做一次 string 赋值操作(按值传递的弊病),非常耗时。如果改成按引用传递参数,如下:

void printStr(string& str)
{printf("%s\n",str.c_str());
}

这样就会带来另外两个问题,第一:该函数只需要读入字符串的入参,并不用对其做修改,而按引用的传递,赋予了这个函数不应该有的权限。第二:不方便调用,比如有如下调用方式:

void printStr(string& str)
{printf("%s\n",str.c_str());
}int main()
{printStr("hello");			//错误:由于字符串 "hello" 是 const 类型,所以这里编译会报错。return 0;
}

综上所述,最好的设计如下:

void printStr(const string& str)
{printf("%s\n", str.c_str());
}

4 函数调用

C++ 中函数的调用包含参数入栈、函数跳转、保护现场、回复现场等过程,以如下代码为例( 64 位程序):

#include <iostream>int add(int a, int b)
{int sum = a + b;return sum;
}int main()
{int sum = add(1, 2);return 0;
}

首先给 main() 函数的第一行 int sum = add(1, 2); 打上断点,调试运行程序。
程序暂停后,查看当前汇编代码( VS2017 查看方法:右击当前代码页,选择转到反汇编):

int main()
{
00007FF67D8AA630  push        rbp  
00007FF67D8AA632  push        rdi  
00007FF67D8AA633  sub         rsp,108h  
00007FF67D8AA63A  lea         rbp,[rsp+20h]  
00007FF67D8AA63F  mov         rdi,rsp  
00007FF67D8AA642  mov         ecx,42h  
00007FF67D8AA647  mov         eax,0CCCCCCCCh  
00007FF67D8AA64C  rep stos    dword ptr [rdi]  
00007FF67D8AA64E  lea         rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]  
00007FF67D8AA655  call        __CheckForDebuggerJustMyCode (07FF67D874108h)  int sum = add(1, 2);
00007FF67D8AA65A  mov         edx,2  
00007FF67D8AA65F  mov         ecx,1  
00007FF67D8AA664  call        add (07FF67D87584Bh)  
00007FF67D8AA669  mov         dword ptr [sum],eax  return 0;
00007FF67D8AA66C  xor         eax,eax  
}

在汇编代码中,程序暂停在第 14 行(00007FF67D8AA65A mov edx,2)。后面的两行是传入参数的过程,其中,edx是数据寄存器,常用于存储一些大于 AX 寄存器的 16 位数和 32 位数的运算中的高位数。在函数调用中, edx 寄存器用于存储第一个参数值。ecx是计数寄存器,常用于存储循环计数器和移位操作的计数器。在函数调用中, ecx 寄存器用于存储第二个参数值。通过这两行传入的值可以看出,调用函数时,参数入栈时从右往左。
汇编行00007FF67D8AA664 call add (07FF67D87584Bh)用于跳转到待调用的函数内,但这里需要注意的是,地址07FF67D87584Bh并不是待调用的函数的地址,该代码会执行到下面这一行:

00007FF67D87584B  jmp         add (07FF67D8AA5C0h)  

这里的地址07FF67D8AA5C0h才是真正待调用函数的地址。下面即进入被调用函数内部:

int add(int a, int b)
{
00007FF67D8AA5C0  mov         dword ptr [rsp+10h],edx  
00007FF67D8AA5C4  mov         dword ptr [rsp+8],ecx  
00007FF67D8AA5C8  push        rbp  
00007FF67D8AA5C9  push        rdi  
00007FF67D8AA5CA  sub         rsp,108h  
00007FF67D8AA5D1  lea         rbp,[rsp+20h]  
00007FF67D8AA5D6  mov         rdi,rsp  
00007FF67D8AA5D9  mov         ecx,42h  
00007FF67D8AA5DE  mov         eax,0CCCCCCCCh  
00007FF67D8AA5E3  rep stos    dword ptr [rdi]  
00007FF67D8AA5E5  mov         ecx,dword ptr [rsp+128h]  
00007FF67D8AA5EC  lea         rcx,[__81FC6F77_main2@cpp (07FF67D9E41D7h)]  
00007FF67D8AA5F3  call        __CheckForDebuggerJustMyCode (07FF67D874108h)  int sum = a + b;
00007FF67D8AA5F8  mov         eax,dword ptr [b]  
00007FF67D8AA5FE  mov         ecx,dword ptr [a]  
00007FF67D8AA604  add         ecx,eax  
00007FF67D8AA606  mov         eax,ecx  
00007FF67D8AA608  mov         dword ptr [sum],eax  return sum;
00007FF67D8AA60B  mov         eax,dword ptr [sum]  
}

这段汇编代码的第 2 行到第 15 行之间是对该函数的栈初始化工作,由编译器自动添加。其中 rsp ( 32 位程序中是 esp ) 、rbp ( 32 位程序中是 ebp )、rdi ( 32 位程序中是 edi )是常用的寄存器:
rsp 为栈指针,常用来指向栈顶。上面汇编代码中第 6 行00007FF67D8AA5CA sub rsp,108h的意思是将栈顶指针往上移动 108h Byte。这个区域为间隔空间,将被调用的 add 函数与 main 函数的栈区域隔开一段距离,同时还要预留出存储局部变量的内存区域。
rbp 为基址指针,常用来指向栈底。
rdi 为目的变址寄存器。
上面汇编代码的第 17 行到第 21 行之间是进行两数相加的逻辑操作。
执行到第最后一行后打开寄存器查看器( VS2017 查看方法:调试–>窗口–>寄存器),可以查看到如下值:

RAX = 0000000000000003 RBX = 0000000000000000 RCX = 0000000000000003 RDX = 0000000000000002 RSI = 0000000000000000 RDI = 0000005BD30FFA58 R8  = 0000020993014F70 R9  = 0000005BD30FF954 R10 = 0000000000000013 R11 = 00000209930242E0 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 0000000000000000 RIP = 00007FF67D8AA60B RSP = 0000005BD30FF950 RBP = 0000005BD30FF970 EFL = 00000206 0x0000005BD30FF974 = 00000003 

查看寄存器 RDI 的内存值( VS2017 查看方法:调试–>窗口–>内存->内存1):

0000005bd30ffb78 0000005bd30ffa90 00007ff67d8aa669 00007ff600000001 cccccccc00000002 cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc

其中第三个值 00007ff67d8aa669 是 main 函数中调用该函数后的下一行汇编代码。
至此,整个调用过程结束。

5 函数指针

注:该部分内容涉及到 C++ 中指针以及类的相关知识。

5.1 函数指针的概念

函数指针是指向函数的指针变量。 通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数的变量。 函数指针用于调用函数、传递参数。 函数指针的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
函数返回值类型:表示该指针变量所指向函数的返回值类型。
指针变量名:表示该指针变量的名称。
函数参数列表:表示该指针变量所指向函数的参数列表。
需要注意的是函数指针没有 ++ 和 – 运算。
为了使用方便,一般会用关键字 typedef 来定义函数指针,即:typedef 函数返回值类型 (* 指针变量名) (函数参数列表) 。例如:

typedef int (*ADD)(int,int);
ADD addFunc;

使用这种方式可以目标函数看作为一个类型,然后再用它去定义指针,增强复用性。
对于无参数或者无返回值的函数,需要使用用 void 关键字,例如:

typedef void (*TESTFUNC)(void); 	//无参数和返回值

5.2 函数指针的使用

使用函数指针和使用其他类型的指针变量一样,其可以作为函数的入参,可以作为函数的返回值,也可以是类的成员变量。

5.2.1 指向全局函数的函数指针

以如下代码为例:

#include <iostream>int add(int a, int b)
{int sum = a + b;return sum;
}int main()
{typedef int(*ADDFUNC)(int, int);ADDFUNC f1 = add;int sum1 = f1(1, 2);			//直接使用函数名int sum2 = (*f1)(1, 2);			//取函数地址printf("sum1 = %d\n",sum1);printf("sum2 = %d\n", sum2);return 0;
}

上面代码的输出为:

sum1 = 3
sum2 = 3

特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。因此,上面代码中 int sum1 = f1(1, 2); 以及 int sum2 = (*f1)(1, 2); 作用是相同的。

5.2.2 指向对象成员函数的函数指针

以如下代码为例:

#include <iostream>class MyAdd 
{
public:MyAdd() {}~MyAdd() {}public:int add(int a, int b){int sum = a + b;return sum;}};int main()
{MyAdd myAddObj;typedef int(MyAdd::*ADDFUNC)(int, int);ADDFUNC f1 = &MyAdd::add;int sum = (myAddObj.*f1)(1, 2);printf("sum = %d\n", sum);return 0;
}

上面代码的输出为:

sum = 3

注意:对象的成员函数属于类,所以其存储位置在对象外的空间中,由所有的类对象共享。因此, MyAdd 类中的 add() 成员函数,不是属于 myAddObj 对象的,而是属于 MyAdd 类。所以使用 &类名::成员函数名 的形式将该成员函数赋给函数指针。

5.2.3 回调函数

回调函数是函数指针的一个重要应用场景,比如在使用 C++ 的容器类时,经常会自定义回调函数用以实现定制化功能。以 vector 的自定义排序为例,代码如下:

#include <iostream>
#include <vector>
#include <algorithm>using namespace std;struct Student 
{string id;double score;
};bool compareByScore(Student& stu1, Student& stu2)
{return stu1.score < stu2.score;
}int main()
{vector<Student> students;students.emplace_back(Student{ "s1",98.2 });students.emplace_back(Student{ "s2",97.6 });students.emplace_back(Student{ "s3",92.8 });students.emplace_back(Student{ "s4",95 });students.emplace_back(Student{ "s5",99 });printf("before sort\n");for (size_t i = 0; i < students.size(); i++){printf("%s(%lf) ", students[i].id.c_str(), students[i].score);}printf("\n");sort(students.begin(), students.end(), compareByScore);printf("after sort\n");for (size_t i = 0; i < students.size(); i++){printf("%s(%lf) ", students[i].id.c_str(), students[i].score);}printf("\n");return 0;
}

上面代码的输出为:

before sort
s1(98.200000) s2(97.600000) s3(92.800000) s4(95.000000) s5(99.000000)
after sort
s3(92.800000) s4(95.000000) s2(97.600000) s1(98.200000) s5(99.000000)

其中,函数 compareByScore 便作为一个函数指针的入参传递给函数 sort

5.2.4 函数指针和指针函数的区别

函数指针和指针函数是两种不同的编程概念,前者是一个指针,后者是一个函数,除了名字比较容易混淆,实际上是完全不同的概念。
上面内容已经说明了函数指针的含义与作用,指针函数的定义如下:
(1)指针函数本身就是一个函数,其返回的类型是指针。
(2)指针函数用于返回指针类型的值,例如动态分配的对象或数组的指针。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/660672.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亚信安慧的AntDB数据库:稳定可靠的保障

亚信安慧AntDB数据库在运营商自主可控替换项目中的成功应用&#xff0c;具有极其重要的意义。该数据库的落地&#xff0c;不仅为这一项目注入了强大的支持力量&#xff0c;还在更大程度上提升了整体的运营效能。作为一种高效可靠的数据库解决方案&#xff0c;AntDB引入了先进的…

(安卓)跳转应用市场APP详情页的方式

前言 最近在做一个需求&#xff0c;需要从自己APP进入到系统的应用市场 方便用户在应用市场给自己的APP打分 于是查阅了一些资料&#xff0c;下面说一下实现方法 实现方案 一般来说&#xff0c;最简单的方案就是这样&#xff1a; val uri Uri.parse("market://details…

AIPC专题:深耕笔电背光模组领域,AIPC与车载显示拉动公司成长

今天分享的是AIPC系列深度研究报告&#xff1a;《AIPC专题&#xff1a;深耕笔电背光模组领域&#xff0c;AIPC与车载显示拉动公司成长》。 &#xff08;报告出品方&#xff1a;东兴证券&#xff09; 报告共计&#xff1a;19页 公司深耕笔电背光模组&#xff0c;主要下游客户为…

突破编程_C++_面试(基础知识(2))

3 面试题3&#xff1a;形参与实参的区别 形参&#xff1a;函数定义时的参数&#xff0c;可以看作是一个占位符。形参只有在被调用的时候才分配内存单元&#xff0c;只在函数内部有效&#xff0c;调用结束后立即释放。 实参&#xff1a;调用函数时使用的参数&#xff0c;实参可…

为什么Vue3双向绑定使用Proxy

Vue2 使用Object.defineProperty无法监听删除属性的操作需要遍历目标对象的所有属性并加上 setter getter 才能监听对于对象的新增属性&#xff0c;需要手动监听在遇到一个对象的属性还是一个对象的情况下&#xff0c;需要递归监听。对于数组通过push、unshift方法增加的元素&…

老版本O记12C上线前的一些调整

ORACLE 12c的数据库&#xff0c;以多租户方式运行&#xff0c;运行了一段时间&#xff0c;还比较稳定&#xff0c;分享一下相关参数修改。 1、一些参数 DEFERRED_SEGMENT_CREATION 默认是true&#xff0c;建议设置为false _DATAFILE_WRITE_ERRORS_CRASH_INSTANCE 默认是tr…

Debezium系列之:MariaDB10.5以上版本赋予数据库账号读取binlog权限的变化

Debezium系列之:MariaDB10.5以上版本赋予数据库账号读取binlog权限的变化 一、背景二、BINLOG MONITOR权限三、BINLOG MONITOR和REPLICA MONITOR的区别四、MariaDB版本升级的影响五、总结一、背景 数据接入会检测账号是否具有REPLICATION SLAVE、REPLICATION CLIENT的权限Mari…

缓存相关问题记录解决

缓存相关问题 在这里我不得不说明,我写的博客都是我自己用心写的,我自己用心记录的,我写的很详细,所以会有点冗长,所以如果你能看的下去的化,会有所收获,我不想写那种copy的文章,因为对我来说没什么益处,我写的这篇博客,就是为了记录我缓存的相关问题,还有我自己的感悟,所以如果…

TypeScript(十) Map对象、元组、联合类型、接口

1. Map对象 1.1. 简述 Map对象保存键值对&#xff0c;并且能够记住键的原始插入顺序。   任何值都可以作为一个键或一个值。 1.2. 创建 Map 使用Map类型和new 关键字来创建Map&#xff1a; 如&#xff1a; let myMap new Map([["key1", "value1"],[&…

C# 获取计算机信息(操作系统/硬件)

C#我们可以通过类库System.Management获取计算机的基础信息。总结了一个通用类&#xff0c;只要根据参考信息填入path和key就可以获取相应的信息。这个只是针对单个设备&#xff0c;如果有多个设备单独写下就可以了。参考信息中key的":"和后边为说明信息&#xff0c;…

inotify学习

inotify的原理 inotify是Linux内核的一个子系统&#xff0c;它提供了一个通用的框架来监控文件系统的变化。使用inotify&#xff0c;应用程序可以订阅和获取文件或目录状态变化的通知&#xff0c;如文件写入、读取、创建、删除、属性更改等。 inotify的工作原理分为以下几个步…

探索未来发展方向:图片转换为Excel表格的智能化与自动化

随着科技的不断进步&#xff0c;人工智能技术已经在许多领域得到广泛应用。其中&#xff0c;将图片转换为Excel表格的智能化与自动化技术成为了备受关注的新兴领域。这一技术的发展&#xff0c;不仅可以极大地提高工作效率&#xff0c;还能为数据分析提供更为准确和便捷的方式。…

上传文件的用例怎么设计

功能测试 符合要求的文件上传成功上传成功的文件名显示正常可查看、下载上传成功的文件删除上传成功的文件替换上传成功的文件上传文件是否支持中文文件路径是否可手动输入手动输入正确的文件路径上传成功手动输入错误的文件路径上传失败 文件大小测试 文件大小为0kb的文件上传…

PRBS并行输出

PRBS&#xff08;Pseudo-Random Binary Sequences&#xff09;是通过LFSR和特征函数 伪随机数发生器产生的伪随机数序列&#xff0c;通常用于高速数字通信测试。 基本电路&#xff08;单比特输出&#xff09; prbs N表示用N比特lfsr尝试伪随机数序列&#xff0c;常用的有N7,9…

创建与删除数据库(四)

创建与删除数据库&#xff08;四&#xff09; 一、创建数据库 1.1 使用DDL语句创建数据库 CREATE DATABASE 数据库名 DEFAULT CHARACTER 示例&#xff1a; 创建一个test 的数据库&#xff0c;并查看该数据库&#xff0c;以及该数据库的编码。 创建数据库&#xff1a; cre…

4G路由器助力智慧农业数据采集与远程管理

随着科技日新月异的发展&#xff0c;智慧农业正逐渐改变着传统农业生产模式。4G路由器作为物联网技术的关键通信设备&#xff0c;在实现农业现场传感器数据实时采集与远程在线管理方面发挥着重要作用&#xff0c;以下智联物联分享4G路由器在智慧农业中的应用优势。 农业现场传感…

机器学习-聚类算法Kmeans【手撕】

聚类算法 在训练时&#xff0c;使用没有标签的数据集进行训练&#xff0c;希望在没有标签的数据里面可以发现潜在的一些结构。 其中使用范围较广的是&#xff0c;聚类算法。聚类算法的目的是将数据划分成有意义或有用的组&#xff08;或簇&#xff09;。这种划分可以基于我们的…

自动保存知乎上点赞的内容至本地

背景&#xff1a;知乎上常有非常精彩的回答/文章&#xff0c;必须要点赞收藏&#xff0c;日后回想起该回答/文章时翻看自己的动态和收藏夹却怎么也找不到&#xff0c;即使之前保存了链接网络不好也打不开了&#xff08;。所以我一般碰到好的回答/文章都会想办法保存它的离线版本…

社交买量:归因统计的核心要素与工具

在当今的社交App推广领域&#xff0c;广告买量已成为企业获取用户的重要手段。然而&#xff0c;如何准确衡量这些买量活动的成效&#xff0c;即用户从广告访问到安装后行为的完整转化路径&#xff0c;一直是运营人员关注的焦点。归因统计是一种评估营销效果的关键技术方案&…

python爬虫-多线程-数据库——WB用户

数据库database的包&#xff1a; Python操作Mysql数据库-CSDN博客 效果&#xff1a; 控制台输出&#xff1a; 数据库记录&#xff1a; 全部代码&#xff1a; import json import os import threading import tracebackimport requests import urllib.request from utils im…