函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数
#include<iostream>
void print(const char *cp) {}
void print(const int *beg, const int *end) {}
// 错误,和上面的 const int *beg, const int *end 本质上是一样的
void print(const int ia[], const int ib[]) {}
<span style="color:red">注意:main 函数不能重载。</span>
<b>区分重载的正确性</b>
void lookup(const int&); // 可以省略形参
bool lookup(const int&); // 错误,与上一个函数只有返回值不一样,不是重载
void lookup(const int&); // 可以省略形参
void lookup(const int& a); // 错误,只是省略的形参,不是重载
// 重载和 const 形参
void lookup(int a);
// 错误,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来
void lookup(const int a);
<b>const_cast 和重载</b>
假定有以下代码
const string &shortcmp(const string &s1, const string &s2){
return s1.size()<=s2.size()? s1:s2;
}
// 将普通引用转为 const 引用是安全的
string &shorter(string &s1, string &s2){
auto &r = shortcmp(const_cast<const string&>(s1),
const_cast<const string&>(s2));
// 原本就是可修改的,因此从 const 转为 非 const 也是安全的
return const_cast<string&>(r);
}
<b>重载匹配问题</b>
先匹配最合适的,没有在向上找。
<b>默认参数</b>
#include<iostream>
using std::cout;
using std::endl;
void f(double a, double b = 100) {
cout << "double,double\t";
cout << "a+b=" << a + b;
}
int main() {
f(1);
return 0;
}
函数匹配
查找最佳匹配的函数。
#include<iostream>
using std::cout;
using std::endl;
void f(int a, int b) { cout << "int,int"; }
void f(double a, double b) { cout << "double,double"; }
int main() {
// 因为二义性而拒绝调用请求,报错。
f(1, 2.2);
return 0;
}
其他的和 Java 类似。const 的话,重载的时候顶层 const 会被忽略。
函数指针
- 函数指针指向的是函数。指针函数是返回值为指针的函数,
类型名 *函数名(函数参数列表)
,其中 * 的优先级低于 ()。所以 int *pfun(int,int)
等同于 int *(pfun(int,int))
,声明了一个函数 pfun,函数形参为 int、int,返回值为整型指针。
- 数组指针指向的是数组。指针数组,指针元素的数组,数组里面存的元素是指针;
和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
// pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用
// 返回值是bool类型。
bool (*pf)(const string &, const string &);
// 如果不写括号,则pf是一个返回值为 bool 指针的函数
bool *pf(const string &, const string &);
<b>使用函数指针</b>
函数指针是一种<b>特殊的指针,指向函数,函数返回一个类型</b>。
例如 int (*add)(int,int)
定义了一个<b>函数指针 add</b>,指向一个函数,函数形参是 int/int,返回值是 int。
例如 int (*pfun)(int,int)
,意味着 pfun 是一个指针,指向一个函数,函数的返回值是 int。
#include<iostream>
using std::cout;
using std::endl;
using std::string;
void lengthCompare() {
cout << "compare" << endl;
}
int func2(int n) {
return n + 1;
}
int main() {
int (*pf)(int) = func2;
void (*pf2)() = lengthCompare;
cout << pf(2) << endl;
pf2();
return EXIT_SUCCESS;
}
// 3
// compare
调用函数的话也不需要解引用,可以直接调用
int main() {
void (*pf2)() = lengthCompare;
void (*pf3)() = &lengthCompare;
(*pf2)();
pf3();
return 0;
}
在指向不同函数类型的指针间不存在转换规则,可以指向参数类型匹配的任意函数。
#include<iostream>
using std::cout;
using std::endl;
using std::string;
int func2(int n) {
return n + 1;
}
int func3(int n) {
return n * n;
}
int main() {
int (*pf)(int) = func2;
pf = func3;
// 100
cout << pf(10) << endl;
return EXIT_SUCCESS;
}
<b>重载函数的指针</b>
当我们使用重载函数时,上下文必须清晰地界定到底应该选用那个函数,通过指定参数类型来界定使用那个函数。
#include<iostream>
using std::cout;
using std::endl;
void f(int *n) {
cout << "point" << endl;
}
void f(int n) {
cout << "int" << endl;
}
int main() {
void (*pf)(int) = f;
void (*pff)(int *) = f;
pf(1);
int n = 10;
pff(&n);
return EXIT_SUCCESS;
}
<b>函数指针形参</b>
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用。
graph LR
指针-->|指向|函数
#include<iostream>
using std::cout;
using std::endl;
using std::string;
bool pf(const string &s1, const string &s2) {
return s1.size() >= s2.size();
}
void useBigger(const string &s1,
const string &s2,
bool pf(const string &, const string &)) {
cout << pf(s1, s2) << "" << endl;
}
// 等价声明
void useBigger(const string &s1,
const string &s2,
bool (*pf)(const string &, const string &)) {
cout << pf(s1, s2) << "" << endl;
}
int main() {
string s1 = "hello";
string s2 = "world";
useBigger(s1, s2, pf);
return EXIT_SUCCESS;
}
也可以使用形式参数
#include<iostream>
using std::cout;
using std::endl;
using std::string;
// pfT 和 pfT2 是函数类型
typedef bool pfT(const string &s1, const string &s2);
typedef decltype(pf) pfT2; // 等价得类型
// FuncP 和 FuncP2 是指向函数的指针
typedef bool(*FuncP)(const string &, const string&);
typedef decltype(pf) *FuncP2 // 等价的类型
bool pf(const string &s1, const string &s2) {
return s1.size() >= s2.size();
}
void useBigger2(const string &s1,
const string &s2,
pfT pf) {
cout << pf(s1, s2) << "" << endl;
}
int main() {
string s1 = "hello";
string s2 = "world";
useBigger2(s1, s2, pf);
return EXIT_SUCCESS;
}
<b>返回指向函数的指针</b>
虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。
using F = int(int*, int); // F 是函数类型,不是指针
using PF = int(*)(int*, int);; // PF 是指针类型
// f1是指向函数的指针
int (*f1(int))(int*, int);
auto f1(int)->int(*)(int*, int);
一个例子
#include<iostream>
using std::cout;
using std::endl;
using std::string;
int func2(int n) {
cout << "f2" << endl;
return n + 1;
}
using FP = int(int n);
FP *f1(int n) {
return func2;
}
int main() {
auto f = f1;
// f1 的返回值是一个指针,这个指针指向函数
FP *(*f2)(int) = f1;
// 先解引用拿到方法f1执行,然后f1的返回值是一个函数,再执行
// 也可以不解引用,直接调用,一样的(前面说了)
cout << (*f2)(19)(1) << endl;
cout << f2(19)(1) << endl;
return EXIT_SUCCESS;
}
auto 和 decltype 同样可以用于函数指针类型。
<b>练习</b>
编写函数的声明,令其接受两个 int 形参并且返回类型也是 int;然后声明一个 vector 对象,令其元素是指向该函数的指针。
#include<iostream>
#include<vector>
using std::cout;
using std::endl;
using std::string;
using std::vector;
using fun = int(int, int);
int f1(int a, int b) {
return a + b;
}
// 产生函数的函数
fun *createF1() {
return f1;
}
int main() {
vector<fun *> fvec;
for (int i = 0; i < 5; ++i) {
fvec.push_back(createF1());
}
for (int i = 0; i < fvec.size(); ++i) {
auto temp = fvec.at(i);
cout << temp(i, i) << "\t";
}
return EXIT_SUCCESS;
}
头文件
假定我们自己编写了一个 Log 函数,很多 .cpp
文件都需要使用这个函数,这些文件是如何知道 Log 函数确实是存在的,只是定义在别处呢?=\=>函数声明。函数声明放在哪里呢?=\=>头文件。
我们使用 #include 预处理器将这些头文件复制粘贴带其他文件,告诉它们,嘿,有 Log 函数,你放心用,链接的时候会找到对应的函数体的。
<b>解决重复引入头文件带来的问题</b>
include 预处理器会将内容复制粘贴到其他文件中,如果多次引入头文件(有时候文件之间的都包含某个头文件,然后又相互引用,会导致多次引入重复的头文件)这会带来一些意想不到的问题。
// 头文件 Log.h
void Logger(const char* message);
struct data {
char* name;
int age;
} mydata;
// Logger 的定义省略
// Math.cpp 多次引入 Log.h, 编译时会发生错误, data 被重复定义。
#include<iostream>
#include"Log.h"
#include"Log.h"
static int Multiply(int a, int b) {
//int result = a * b;
//return result;
Logger("Print Log");
return a * b;
}
int main() {
Multiply(1, 2);
std::cout << sizeof(bool)<< std::endl;
}
为了避免多次引入头文件,可以在头文件的开头加上 #pragma once
解决这类问题。
#pragma once
除了 pragma once
外还有其他方式可以解决这类问题 =\=> #ifndef
#ifndef _LOG_H // 如果没有定义 _LOG_H 就在编译中包含以下代码。 如果被定义了,则 #define 和 #endif 中的都不会被包含。
#define _LOG_H // 定义 _LOG_H
void Log(const char* msg);
#endif // 结束
pragma once 被很多主流的编译器所支持。
<b>引入头文件时 <>
和 ""
的区别</b>
<>
搜索 include 路径文件夹下的文件,即只用于编译器 include
路径。
""
包含相对于当前文件的文件,../xx.h, ../../../oo.h
都可,""
可以做一切,但是通常只用它在相对路径。
调试技巧
VS 在调试过程中鼠标右击『转到反汇编』可以逐汇编代码调试。如果我们在源代码中无法找到错误原因,此时只能求助于调试 CPU 指令。