从0.1开始的C++ Note 04

该笔记包括函数的一些基础知识,根据个人的知识掌握情况有所简略,日后有需要再补充。

#2022/01/08 补充了模板函数

函数基础

一个求阶乘的函数示例

#include <iostream>

int fact(int s) {
    int result = 1;    //不能放入循环内
    for (; s != 1; ) {
        result *= s--;
    }
    return result;
}

int main()
{
    int s;
    std::cin >> s;
    int output = fact(s);
    std::cout << output;
}

局部静态变量在程序的执行路径第一次经过时初始化,并且直到程序终止才被销毁。

size_t count_calls(){
    static size_t ctr = 0;    //在函数调用结束后,这个值仍有效
    reutrn ++ctr;
}
int main(){
    //将依次记录函数被调用的次数,输出的结果是1到10的数字
    for (size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

(函数声明和分离式编译暂略)

参数传递

当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用

传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。
例如调用fact(s)不会改变传入的实参s的值。
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。

void reset(int *ip)
{
    *ip = 0;    //改变指针ip所指对象的值
    ip = 0    //只改变了ip的局部拷贝,实参未被改变
}
int main{
    int i = 42;
    reset(&i);    //改变i的值而非i的地址
    cout << "i = " << i << endl;    //输出i = 0
}

传引用参数

重写reset函数

void reset(int &i)    // i是传给reset函数的对象的另一个名字
{
    i = 0    //只改变了i所引对象的值
}
int main{
    int j = 42;
    reset(j);    //j采用传引用方式,它的值被改变
    cout << "j = " << j << endl;    //输出j = 0
}

使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作,这是应通过引用形参访问。

bool isShorter(const string &s1, const string &s2){
    return s1.size() < s2.size();
}

使用引用形参返回额外信息

//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size_type find_char(const string &s, char c, 
                            string::size_type &occurs) {
    auto ret = s.size();    //第一次出现的位置
    occurs = 0;    //设置表示出现次数的形参
    for (decltype(ret) i = 0; i != s.size(); **i) {
        if (s[i] == c) {
            if (ret == s.size())
                ret = i;    //记录c第一次出现的位置
            ++occurs;    //将出现的次数加1
        }
    }
    return ret;    //出现次数通过occurs隐式地返回
}

const形参和实参

void fcn (const int i) { /* fcn能够读取i,但是不能向i写值 */ }

调用fcn函数时,既可以传入const int也可以传入int。但反过来不行!
尽量使用常量引用
我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
例如在find_char函数中:

string::size_type find_char(const string &s, char c, 
                            string::size_type &occurs);
//出现错误
find_char("Hello World", 'o', ctr);

另一个问题在于,假如其他函数把它们的形参定义成常量引用,那么find_char则无法在此类函数中正常使用。

bool is_sentence(const string &s){
    //如果在s的末尾有且只有一个句号,则s是一个句子
    string::size_type ctr = 0;
    //此时调用find_char将会出现错误
    return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1;
}

数组形参

数组的两个特殊性质:

  1. 不允许拷贝数组。
  2. 使用数组时通常会将其转换成指针。
    因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
// 尽管形式不同,这三个print函数是等价的
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);    //可以看出来,函数的意图是作用于一个数组
void print(const int[10]);    //这里的维度表示我们期望数组含有多少元素,实际不一定

int i = 0, j[2] = {0, 1};
print(&i);    //正确:&i的类型是int*
print(j);    //正确:j转换成int*并指向j[0]

管理数组指针形参的三种方式

  1. 使用标记指定数组长度。要求数组本身包含一个结束标记。
void print(const char *cp) {
    if (cp)    //若cp不是一个空指针
        while (*cp)    //只要指针所指的字符不是空字符
            cout << *cp++;    //输出当前字符并将指针向前移动一个位置
}

这种方法适用于那些有明显标记且标记不会与普通数据混淆的情况。

  1. 使用标准库规范。
void print (const int *beg, const int *end){
    //输出beg到end之间(不含end)的所有元素
    while (beg != end)
        cout << *beg++ << endl;    //输出当前元素并将指针向前移动一个位置
}
//需要传入两个指针
int j[2] = {0, 1};
print(begin(j), end(j));    //begin和end函数
  1. 显式传递一个表示数组大小的形参
void print(const int ia[], size_t size){
    for (size_t i = 0; i != size; ++i) {
        cout << ia[i] << endl;
    }
}
int j[] = {0, 1};
print(j, end(j) - begin(j));

数组引用实参

void print(int (&arr)[10]){
    for (auto elem : arr)
        cout << elem << endl;
}
int i = 0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i);    //错误:实参不是含有10个整数的数组
print(j);    //错误:实参不是含有10个整数的数字
print(k);    //正确

传递多维数组

//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) {/*...*/}

Note:将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为多维数组是数组的数组,指针就是一个指向数组的指针。数组第二维(以及后面的所有维度)的大小是数组类型的一部分,不能省略。

返回类型

返回数组指针

声明一个返回数组指针的函数

Type (*function (parameter_list)) [dimension]
// 例如
int (*func(int i)) [10];

使用尾置返回类型

//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];

使用decltype

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;    //返回一个指向数组的指针
}

函数重载

如果同一个作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
例如:

void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

重载和const形参

Record lookup(Phone);
Record lookup(const Phone);     //重复声明

Record lookup(Phone*);
Record lookup(Phone* const);    //重复声明

//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
Record lookup(Account&);    //函数作用于Account的引用
Record lookup(const Account&);    //新函数,作用于常量引用

Record lookup(Account*);    //新函数,作用于指向Account的指针
Record lookup(const Account*);    //新函数,作用于指向常量的指针

函数指针

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定。例如:

// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);

这个函数的类型是bool(const string &, const string &)。想要声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

// pf指向一个函数,该函数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &);    //未初始化

使用函数指针
当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如:

pf = lengthCompare;    //pf指向名为lengthCompare的函数
pf = &lengthCompare;    //等价于上一句,&符号是可选的

此外,还能直接使用指向函数的指针调用该函数,无需提前解引用指针:

bool b1 = pf("hello", "goodbye");    //调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye");    //一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");    //另一个等价的调用

在指向不同函数类型的指针间不存在转换规则。但可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数:

string::size_type sumLength(const string &, const string &);
bool cstringCompare(const char*, const char*);
pf = 0;    //正确:pf不指向任何函数
pf = sumLength;    //错误,返回类型不匹配
pf = cstringCompare;    //错误:形参类型不匹配
pf = lengthCompare;    //正确:函数和指针的类型精确匹配

函数指针形参

// 第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// 等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));

//调用
void useBigger(s1, s2, lengthCompare);

返回指向函数的指针

using F = int(int*, int);    //F是函数类型,不是指针
using PF = int(*)(int*, int);    //PF是指针类型

PF f1(int);    //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);    //错误:F是函数类型,f1不能返回一个函数
F* f1(int);    //正确:显式地指定返回类型是指向函数的指针

// 也可以
int (*f1(int))(int*, int);
// 尾置返回类型
auto f1(int) -> int(*)(int*, int);

模板函数

类似于如下函数体基本相同,只是第二参数的类型有所差异的函数可以采用模板函数。

void display_message(const string&, const vector<int>&);
void display_message(const string&, const vector<double>&);
void display_message(const string&, const vector<string>&);

void display_message(const string &msg, const vector<int> &vec){
    cout << msg;
    for (int ix = 0; ix < vec.size(); ++ix)
        cout << vec[ix] << ' ';
}
void display_message(const string &msg, const vector<string> &vec){
    cout << msg;
    for (int ix = 0; ix < vec.size(); ++ix)
        cout << vec[ix] << ' ';
}

function template定义示例如下:

template <typename elemType>
void display_message(const string &msg, const vector<elemType> &vec){
    cout << msg;
    for (int ix = 0; ix < vec.size(); ++ix){
        elemType t = vec[ix]
        cout << t << ' ';
    }
}

function template使用:

vector<int> ivec;
string msg;
// ...
display_message(msg, ivec)

编译器会将elemType绑定为int类型,然后产生一份对应的函数实例。
function template的重载:

template <typename elemType>
void display_message(const string &msg, const vector<elemType> &vec);

template <typename elemType>
void display_message(const string &msg, const list<elemType> &lt);

作者: 公子小白

SOS团团员,非外星人、未来人、超能力者。。。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注