C/C++中的函数指针

一、引子

今天无聊刷了leetcode上的一道题,如下:

Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.

Examples:

[2,3,4] , the median is 3

[2,3], the median is (2 + 3) / 2 = 2.5

Design a data structure that supports the following two operations:

  • void addNum(int num) - Add a integer number from the data stream to the data structure.
  • double findMedian() - Return the median of all elements so far.

for example:

add(1)
add(2)
findMedian() -> 1.5
add(3)
findMedian() -> 2

题意很简单,我的解法思路是:维护两个堆,一个为最大堆,放前一半较小值;

另一个为最小堆,存放后一半较大值。并且保证最小堆的元素要么跟最大堆相等,要么多一个。这样取中值只需要这两个堆的第一个元素即可,复杂度为O(1),添加元素时要更新堆,复杂度为O(logN)

#include<iostream>
#include<vector> using namespace std; class MedianFinder {
public:
// Adds a number into the data structure.
// let num_small_heap always >= num_large_heap
void addNum(int num) {
if (small_heap.size() <= large_heap.size()) { // add to small heap
if (large_heap.empty() ||
num <= large_heap[0]) { //smpile add
small_heap.push_back(num);
} else { // remove the smallest one in large heap
//and add num to large heap
small_heap.push_back(large_heap[0]);
large_heap[0] = num;
UpdateLargeHeapFromHead(large_heap);
}
UpdateSmallHeapFromTail(small_heap);
} else { // add to large heap
if (num >= small_heap[0]) { // simple add
large_heap.push_back(num);
} else {
large_heap.push_back(small_heap[0]);
small_heap[0] = num;
UpdateSmallHeapFromHead(small_heap);
}
UpdateLargeHeapFromTail(large_heap);
}
print();
} // Returns the median of current data stream
double findMedian() {
if (small_heap.size() == 0) return 0;
if (small_heap.size() > large_heap.size()) {
return (double)small_heap[0];
} else {
return small_heap[0]/2.0 + large_heap[0]/2.0;
}
} private:
void print() {
cout << "small:";
for (int i=0; i<small_heap.size(); ++i)
cout << small_heap[i] << " ";
cout << endl;
cout << "large:";
for (int i=0; i<large_heap.size(); ++i)
cout << large_heap[i] << " ";
cout << endl;
} void UpdateSmallHeapFromTail(vector<int> & heap) {
int i = heap.size()-1;
while(i > 0) {
if (heap[i] <= heap[(i-1)/2]) {
break;
}
else {
swap(heap[i], heap[(i-1)/2]);
i = (i-1)/2;
}
}
} void UpdateLargeHeapFromTail(vector<int> & heap) {
int i = heap.size()-1;
while(i > 0) {
if (heap[i] >= heap[(i-1)/2]) {
break;
}
else {
swap(heap[i], heap[(i-1)/2]);
i = (i-1)/2;
}
}
} void UpdateSmallHeapFromHead(vector<int> & heap) {
int i = 0;
int n = heap.size();
int k = 0;
while(i < n && 2*i+1 < n) {
if (2*i+2 >= n) {
k = 2*i+1;
} else {
k = heap[2*i+1] >= heap[2*i+2] ? 2*i+1 : 2*i+2;
} if (heap[i] >= heap[k]) {
break;
}
else {
swap(heap[i], heap[k]);
i = k;
}
}
} void UpdateLargeHeapFromHead(vector<int> & heap) {
int i = 0;
int n = heap.size();
int k = 0;
while(i < n && 2*i+1 < n) {
if (2*i+2 >= n) {
k = 2*i+1;
} else {
k = heap[2*i+1] <= heap[2*i+2] ? 2*i+1 : 2*i+2;
} if (heap[i] <= heap[k]) {
break;
}
else {
swap(heap[i], heap[k]);
i = k;
}
}
} private:
vector<int> small_heap;
vector<int> large_heap;
}; int main() {
MedianFinder mf;
mf.addNum(1);
mf.addNum(2);
mf.addNum(3);
mf.addNum(1);
mf.addNum(6);
mf.addNum(-1);
cout << "result:";
cout << mf.findMedian() << endl;
return 0;
}

执行结果,并且打印了每次新加入元素之后大小堆的变化。

wuman@wuman-pc:~/codes/leetcode$ ./a.out
small:1
large:
small:1
large:2
small:2 1
large:3
small:1 1
large:2 3
small:2 1 1
large:3 6
small:1 -1 1
large:2 6 3
result:1.5

这个算法的效率还是可以的,在leetcode上能进前3%,但是有个问题就是这里的代码很很多重复的地方,看了《clean code》之后,忍不住要对其进行优化。

我们看到更新大小堆的函数除了一个是<=,一个是>=之外,其他的都一样,所以可以写成一个函数,传入一个回调函数专门用于比较。如从后向前更新可以写为:

void UpdateHeapFromTail(vector<int> & heap,
bool (MedianFinder::*compare)(int, int)) {
int i = heap.size()-1;
while(i > 0) {
if ((this->*compare)(heap[i],heap[(i-1)/2])) {
break;
}
else {
swap(heap[i], heap[(i-1)/2]);
i = (i-1)/2;
}
}
}

整个代码可以优化为:

#include<iostream>
#include<vector> using namespace std; class MedianFinder {
public:
// Adds a number into the data structure.
// let num_small_heap always >= num_large_heap
void addNum(int num) {
if (small_heap.size() <= large_heap.size()) { // add to small heap
if (large_heap.empty() ||
num <= large_heap[0]) { //smpile add
small_heap.push_back(num);
} else { // remove the smallest one in large heap
//and add num to large heap
small_heap.push_back(large_heap[0]);
large_heap[0] = num;
UpdateHeapFromHead(large_heap, &MedianFinder::LessOrEqual);
}
UpdateHeapFromTail(small_heap, &MedianFinder::LessOrEqual);
} else { // add to large heap
if (num >= small_heap[0]) { // simple add
large_heap.push_back(num);
} else {
large_heap.push_back(small_heap[0]);
small_heap[0] = num;
UpdateHeapFromHead(small_heap, &MedianFinder::GreatOrEqual);
}
UpdateHeapFromTail(large_heap, &MedianFinder::GreatOrEqual);
}
} // Returns the median of current data stream
double findMedian() {
if (small_heap.size() == 0) return 0;
if (small_heap.size() > large_heap.size()) {
return (double)small_heap[0];
} else {
return small_heap[0]/2.0 + large_heap[0]/2.0;
}
} private:
bool LessOrEqual(int left, int right) {
return left <= right;
}
bool GreatOrEqual(int left, int right) {
return left >= right;
} void UpdateHeapFromTail(vector<int> & heap,
bool (MedianFinder::*compare)(int, int)) {
int i = heap.size()-1;
while(i > 0) {
if ((this->*compare)(heap[i],heap[(i-1)/2])) {
break;
}
else {
swap(heap[i], heap[(i-1)/2]);
i = (i-1)/2;
}
}
} void UpdateHeapFromHead(vector<int> & heap,
bool (MedianFinder::*compare)(int, int)) {
int i = 0;
int n = heap.size();
int k = 0;
while(i < n && 2*i+1 < n) {
if (2*i+2 >= n) {
k = 2*i+1;
} else {
k = (this->*compare)(heap[2*i+1],heap[2*i+2])
? 2*i+1 : 2*i+2;
} if ((this->*compare)(heap[i], heap[k])) {
break;
}
else {
swap(heap[i], heap[k]);
i = k;
}
}
} private:
vector<int> small_heap;
vector<int> large_heap;
}; int main() {
MedianFinder mf;
mf.addNum(1);
mf.addNum(2);
mf.addNum(3);
mf.addNum(1);
mf.addNum(6);
mf.addNum(-1);
cout << mf.findMedian() << endl;
return 0;
}

二、C/C++中的函数指针

在这之前没怎么用过C++函数指针这个特性,对其也不是很了解,所以趁这个机会在学习下C/C++的函数指针。

1. C中的函数指针

函数指针主要有两个用途:

  • 转换表
  • 作为参数传递给另一个函数(回调函数)

下面我们先介绍C中函数指针的语法。

函数指针的声明,如下面的例子中,可以声明为

void (*funcptr)(int) = TestFunc,也可以使用typedef简化声明;

可以直接使用赋值操作符进行初始化;在函数指针的初始化之前具有TestFunc的原型是很重要的,否则编译器无法检查TestFunc的类型是否与funcptr所指向的类型一致。

初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针,&操作符只是显示地说明了编译器将隐式完成的任务。

如下面的例子所示,我们有三种方式调用函数:

a. 第一种简单地使用名字调用函数TestFunc,但它的执行过程可能和你想象的不太一样。函数名TestFunc首先被转换为一个函数指针,该指针指定函数在内存中的位置。然后,函数调用操作符调用该函数,执行开始于这个地址的代码。

b. 第二种对func执行间接访问操作,它把函数指针转换为函数名。这个转换并不是真正需要的,因为编译器在执行函数调用操作符之前又会把它转换回去。不过,这条语句的效果和第一种是完全相同的。

c. 第三种和前面两种的效果一样。间接访问并非必需,因为编译器需要的是一个函数指针。

#include<stdio.h>

typedef void Func(int);

void TestFunc(int a)
{
printf("function pointer test: %d\n", a);
} int main()
{
Func* func = &TestFunc; //&可以省略
TestFunc(0); // 这三种方式调用函数都可以
(*func)(1);
func(2);
return 0;
}

2. C++中的函数指针

C++向下兼容C,所以前面C中函数指针的声明、定义、初始化和调用都可以用于C++,但是C++增加了类,有了类成员函数,类成员函数又分静态的和非静态的。这样函数指针的使用就变得更加复杂了,下面一一叙述。

非静态成员函数指针

简单的讲,指向类成员函数的指针与普通函数指针的区别在于,前者不仅要匹配函数的参数类型和个数以及返回值类型,还要匹配该函数指针所属的类类型。总结一下,比较以下几点:

a)参数类型和个数

b)返回值类型

c)所属的类类型(特别之处)

究其原因,是因为非静态的成员函数必须被绑定到一个类的对象或者指针上,才能得到被调用对象的this指针,然后才能调用指针所指的成员函数(我们知道,所有类的对象都有自己数据成员的拷贝,但是成员函数都是共用的,为了区分是谁调用了成员函数,就必须有this指针,this指针是隐式的添加到函数参数列表里去的)。

声明:与普通函数作为区分,指向类的成员函数的指针只需要在指针前加上类类型即可,格式为:

typedef 返回值 (类名::*指针类型名)(参数列表);

赋值:只需要用类的成员函数地址赋值即可,格式为:

指针类型名 指针名 = &类名::成员函数名;

注意:这里的这个&符号是比较重要的:不加&,编译器会认为是在这里调用成员函数,所以需要给出参数列表,否则会报错;加了&,才认为是要获取函数指针。这是C++专门做了区别对待。

调用:调用方法也很简单,针对调用的对象是对象还是指针,分别用.和->进行调用,格式为:

(类对象.*指针名)(参数列表);

(类指针->*指针名)(参数列表);

注意:这里的前面一对括号是很重要的,因为()的优先级高于成员操作符指针的优先级。

#include<iostream>

using namespace std;

class TestClass {
public:
typedef void (TestClass::*FuncPtr)(int);
void Test(int a){
TestFunc(a, &TestClass::Print); //传入参数(赋值)
}
void TestFunc(int a, FuncPtr pf) { // 这两种原型都可以
//void TestFunc(int a, void (TestClass::*pf)(int)) {
(this->*pf)(a); //调用,一定要用this指针
}
private:
void Print(int a)
{
cout << "test non-static function pointer:" << a << endl;
}
}; int main()
{
TestClass t;
t.Test(1);
return 0;
}
静态成员函数指针

类的静态成员和普通成员的区别在于,他们是不依赖于具体对象的,所有实例化的对象都共享同一个静态成员,所以静态成员也没有this指针的概念。

所以,指向类的静态成员的指针就是普通的指针。

#include<iostream>

using namespace std;

class TestClass {
public:
//typedef void (TestClass::*FuncPtr)(int) // 错误
typedef void (*FuncPtr)(int); //不要用类型限定
void Test(int a){
//TestFunc(a, Print);可以这么用,说明静态成员函数的类型跟非静态是不一样的。
TestFunc(a, &TestClass::Print); //传入参数(赋值),&可以省略
}
void TestFunc(int a, FuncPtr pf) { // 这两种原型都可以
//void TestFunc(int a, void (TestClass::*pf)(int)) {
pf(a); //调用,不能用this指针
}
private:
static void Print(int a)
{
cout << "test non-static function pointer:" << a << endl;
}
}; int main()
{
TestClass t;
t.Test(1);
return 0;
}

引用

--C和指针/ (美)里科(Reek, K. A.)著;徐波译. —北京:人民邮电出版社,2008. 4(2016. 5重印)

--恼人的函数指针(二):指向类成员的指针

C/C++中的函数指针的更多相关文章

  1. QT中使用函数指针

    想仿命令行,所以定义了一个类,让一个String 对应一个 function,将两者输入list容器. 类中定义了 QString commandStr; void (MainWindow::*com ...

  2. Delphi中的函数指针判断是否为空

    delphi函数指针 只有@@p才代表了函数指针本身的地址   assigned(p) 判断是否为空 或者用 @p=nil 来判断函数指针是不是为空 Delphi中的函数指针实际上就是指针,只是在使用 ...

  3. C++中使用函数指针 【瓦特芯笔记】

         在C++类中使用函数指针. 类型定义:      typedef 返回类型(类名::*新类型)(参数表) //类定义 class CA { public: char lcFun(int a) ...

  4. 1、C语言中的函数指针

    一 通常的函数调用 void MyFun(int x); //此处的申明也可写成:void MyFun( int ); int main(int argc, char* argv[]) { MyFun ...

  5. C语言结构体中的函数指针

      这篇文章简单的叙述一下函数指针在结构体中的应用,为后面的一系列文章打下基础 本文地址:http://www.cnblogs.com/archimedes/p/function-pointer-in ...

  6. Keil C51 中的函数指针和再入函数

    函数指针是C语言中几个难点之一.由于8051的C编译器的独特要求,函数指针和再入函数有更多的挑战需要克服.主要由于函数变量的传递.典型的(绝大部分8051芯片)函数变量通过堆栈的入栈和出栈命令来传递. ...

  7. C++中的函数指针和指针函数

    C++中的函数指针和指针函数       数组名一般可以被当成指向数组第一个元素的常量指针,同样的情况,在函数中,函数名可以本当成指向函数的常量指针,假如一个函数已经定义,那么它在计算机中一定有特定的 ...

  8. C语言中的函数指针

    C语言中的函数指针 函数指针的概念:   函数指针是一个指向位于代码段的函数代码的指针. 函数指针的使用: #include<stdio.h> typedef struct (*fun_t ...

  9. 利用C语言中的函数指针实现c++中的虚函数

    C语言中的函数指针 #include<stdio.h> int fun1(int a) { return a*a; } int fun2(int a) { return a*a*a; } ...

随机推荐

  1. 路飞学城Python-Day140

    Django思维导图

  2. 【vue】v-if和v-show的区别

    今天来捋一下vue中的v-if与v-show的区别 先来看一下vue官方文档对他们的解释 2.从实现方式来看: v-if是当依赖的值变为false时,直接让元素消失,html代码也会消失,相当于直接在 ...

  3. luogu 4884 多少个1 (BSGS)

    很有意思的一个签到题 然而考场上并没有切掉 $1111...111=K(mod\;m)$ $10^{x}=9K+1(mod\;m)$ 用$BSGS$求解即可 模数爆了$int$,需要快速乘,然而模数是 ...

  4. acgis地图初始化并根据经纬度进行标注

    根据项目要求,需要对指定的位置进行标注. 1.初始化地图服务 2.根据地图服务接口信息进行标注 3.根据经纬度信息进行标注 展示接口截图: 以下代码并不能直接粘贴跑起来,因为代码所涉及的文件地址都是内 ...

  5. Java基础学习总结(32)——Java项目命名规范

    一.命名规范 1. 项目名全部小写 2. 包名全部小写 3. 类名首字母大写,如果类名由多个单词组成,每个单词的首字母都要大写. 如:public class MyFirstClass{} 4. 变量 ...

  6. String String s = new String("asd") 涉及对象数目

    问题·:.String str = new String("abc")创建了多少个对象? 这个问题在很多书籍上都有说到比如<Java程序员面试宝典>,包括很多国内大公司 ...

  7. iwebshop 增删改查

    <?php header("content-type:text/html;charset=utf-8"); class Test extends IController { ...

  8. 计算机-禁止USB服务

    接到一个任务,禁止集团内所有电脑的USB接口进行文件拷贝,但不能妨碍打印机.鼠标键盘.扫描仪.加密狗等等一切需要USB接口工作的外部设备. 纠结了,这不摆明了让我蛋疼吗? 不过,疼归疼,办法总是要想滴 ...

  9. 从零開始写游戏引擎(一) - project创建以及文件夹设置还有版本号控制

    一句话提要 好的開始等于成功了一半. 创建文件夹结构 project文件夹下最好分为以下几个文件夹 Docs - 开发文档,设计文档 Assets - 角色,动作,模型和音效等 Source - 代码 ...

  10. 关联规则推荐及Apriori算法

    参考这篇文章: http://blog.csdn.net/rongyongfeikai2/article/details/40457827 这条关联规则的支持度:support = P(A并B) 这条 ...