登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 78|回复: 0

C++Primer学习笔记06.函数

[复制链接]

44

主题

-24

回帖

30

积分

新手上路

积分
30
发表于 2025-1-3 22:27:33 | 显示全部楼层 |阅读模式

函数:我们写的代码块,被设计用来执行特定的任务。函数具有很好的复用性,可以避免代码重复。

函数有返回值,有参数。

函数基础

函数调用的完整过程如下

  • 一是用实参初始化函数对应的形参
  • 二是将控制权转移给被调用函数
  • 主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

<b>函数形参实参和返回类型</b>

#include<iostream>

using std::cout;
using std::cin;
using std::endl;

void func(int a) {
    cout << a * 10 << endl;
}

int main() {
    func(10);
    // 正确,可以强转成 int
    func(10.2);
    // 正确,可以强转成 int
    func('1');
    // 错误
    // func("hello");
    return 0;
}
void f1(){} // 隐式定义空形式参数列表
void f2(void){} // 显式定义空形式参数列表

大多数类型都能用作函数的返回类型。一种特殊的返回类型是 void,它表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,<span style="color:red">但可以是指向数组或函数的指针。</span>

<b>函数调用指令</b>

每次调用函数时,编译器会为我们生成一条 call 指令,为了调用函数,需要创建一个堆栈结构,维护各种参数,调用开销大。

局部对象

在 C++ 语言中,名字有作用域,对象有生命周期(lifetime)。形参和函数体内部定义的变量统称为局部变量(local variable)。

<b>自动对象</b>

对于普通局部变量对应的对象来说,方法调用时运行到定义变量的语句时创建局部变量;当块执行结束后,块中创建的对象的值就变成未定义了。

<b>局部静态对象</b>

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成 static 类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

统计函数被调用的次数

#include<iostream>

using std::cout;
using std::cin;
using std::endl;
using std::runtime_error;

size_t count_calls() {
    static int i = 0;
    return ++i;
}

int main() {
    for (auto i = 0; i < 10; i++) {
        cout << count_calls() << endl;
    }
}

函数声明

C/C++ 中可以先声明,在定义。可以把函数的声明统一放到一个头文件中,然后在另一个文件中进行定义;

// define.h
#ifndef CPP_PRIMER_DEFINE_H
#define CPP_PRIMER_DEFINE_H
void fun1();
void fun2();
#endif //CPP_PRIMER_DEFINE_H

// define.cc
#include<iostream>
#include"define.h"

using std::cout;
using std::cin;
using std::endl;

void fun1() {
    cout << "fun1";
}

void fun2() {
    cout << "fun2";
}

// main.cc
#include "define.cc"

int main() {
    fun1();
    fun2();
}

分离式编译

【C++】C++中的分离式编译 - HDWK - 博客园 (cnblogs.com)

随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。而分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。目标文件链接的时候可以通过函数签名来查找对应的函数体(函数定义)。

而 C++ 的文件一般这样组织:头文件以 .h 为后缀,主要包含类和函数的声明;实现文件以 .cpp/.cc 为后缀。可以这样理解,头文件中包含就是一些接口声明,而实现文件就是对这些接口进行定义。

分离式编译的意思就是,需要用多个文件生成可执行文件,当某个文件发生变动时,只重新编译该文件,然后再重新生成可执行文件。

# 编译
g++ -c one.cpp two.cpp
# 生成可执行文件
g++ one.o two.o -o main

# 如果 one.cpp 发生了修改,那么只要重新编译 one.cpp
g++ -c one.cpp
# 然后重新生成可执行文件
g++ one.o two.o -o main

重复定义

C++ 不允许重复定义,因此需要警惕诸如头文件的重复引入。而 C++ 的 ifndef / pragma once 可以避免重复定义。

<b>ifndef 避免重复定义</b>

#ifndef NUM_H
#define NUM_H
// some define
#endif 

<b>pragma once 避免重复定义</b>

#pragma once
// some define

参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。<span style="color:red">而形参初始化的机理与变量初始化一样。</span>

<span style="color:red">形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。如果对象很大,采用拷贝的方式十分占用内存且有很大的复制开销,速度慢;而引用形式的传递没有这种拷贝开销,速度较快。</span>

当形参是引用类型时为引用传递;当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,我们说这样的实参被值传递。

传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值

int n = 0;
int i = n;
i = 42; // 不会改变 n 的值

<b>指针形参</b>

指针作为形参时,拷贝的是指针的地址值。虽然是两个不同的指针,但是都指向了同一块内存区域,都可以修改所指对象的值。

int n = 0, i =42;
int *p = &n, *q = &i;
*p = 42; // n 的值改变,变为 42
p = q; // 

编写一个函数,使用指针形参交换两个整数的值。在代码中调用该函数并输出交换后的结果,以此验证函数的正确性

#include<iostream>
#include"define.h"

using std::cout;
using std::cin;
using std::endl;


int swap(int *n1, int *n2) {
    int tmp = *n2;
    *n2 = *n1;
    *n1 = tmp;
}

int main() {
    int a = 0, b = 10;
    cout << "a=" << a << "\t b=" << b << endl;
    swap(&a, &b);
    cout << "a=" << a << "\t b=" << b << endl;
}
/*
a=0      b=10
a=10     b=0
*/

传引用参数

对于引用的操作实际上是作用在引用所引的对象上,引用只是对象的别名。

void reset(int &a) {
    a *= 10;
}

int main() {
    int a = 10;
    reset(a);
    cout << a << endl; // 100
}

和其他引用一样,引用形参绑定初始化它的对象。当调用这一版本的 reset 函数时,i绑定我们传给函数的 int 对象,此时改变 a 也就是改变i所引对象的值。此例中,被改变的对象是传入 reset 的实参。

<b>使用引用避免拷贝</b>

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括 IO 类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

例如,比较两个大 string 对象的长度,可以使用引用来避免大 string 对象的拷贝。

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

<span style="color:red">如果函数无须改变引用形参的值,最好将其声明为常量引用。</span>

<b>使用引用形参返回额外信息</b>

一个函数只能返回一个值,有时函数需要同时返回多个值我们可以通过定义一个新的包含这些数据的类型返回,也可以使用引用形参。

比如,找到一个字符串中某个字符串的出现次数,我们可以使用引用形参

#include<iostream>
#include"define.h"
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

// 返回 char c 第一次出现的位置,并统计出现的次数
string::size_type find_char(const string &str, char c, string::size_type &times) {
    int index = -1;
    times = 0;
    for (int i = 0; i < str.size(); ++i) {
        if (str[i] == c) {
            times++;
            index = index == -1 ? i : index;
        }
    }
    return index;
}

int main() {
    string str = "hello world java";
    char c = 'l';
    string::size_type time = 0;
    // 如果提示类型错误,看看是不是形参和实参的类型不一致导致的
    auto index = find_char(str, c, time);
    // index=2=== times=3
    cout << "index=" << index << "=== times=" << time;
}

const形参和实参

当用实参初始化形参时会忽略掉顶层 const。换句话说,形参的顶层 const 被忽略掉了。当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的

// 可以读 i,但是不能写。
void fun2(const int i) {
    i = 100;
    // cout << i << endl;
}

int main() {
    // 可以传入非 const 修饰的 i
    int i = 10;
    fun2(i);
}

另外一个值得注意的是

void func(const int i){}
void func(int i){} // 重复定义了 func,const 会被忽略的。

<b>指针或引用形参与 const</b>

我们可以使用非常量初始化一个底层 const 对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

int i = 42;
const int *cp = &i;     // 正确,但是 cp 不能改变 i
const int &r = i;       // 正确,但是 r 不能改变 i
const int &r2 = 42;     // 正确
int *p = cp;            // 错误,p 的类型和 cp 的不一致
int &r3 = r;            // 错误,r3 的类型和 r 的不一致
int &r4 = 42;           // 错误,不能用以恶字面值初始化一个非常量引用

<b>尽量使用常量引用</b>

  • 把函数不会改变的形参定义成常量引用。
  • 使用引用而非常量引用也会极大地限制函数所能接受的实参类型,我们不能把 const 对象、字面值或者需要类型转换的对象传递给普通的引用形参。因此尽量使用常量引用。
string::size_type find_char(string &str, char c, string::size_type &times) {
    int index = -1;
    times = 0;
    for (int i = 0; i < str.size(); ++i) {
        if (str[i] == c) {
            times++;
            index = index == -1 ? i : index;
        }
    }
    return index;
}
// 会报错,不能把一个字面量作用于 string 对象。
find_char("Hello World", 'l', ctr);

练习

编写一个函数,判断 string 对象中是否含有大写字母。编写另一个函数,把 string 对象全都改成小写形式。在这两个函数中你使用的形参类型相同吗?为什么?

#include<iostream>
#include <cstring>

using std::cout;
using std::cin;
using std::endl;
using std::string;

bool check_is_lower(const string &str) {
    for (auto t: str) {
        if (tolower(t) != t) {
            cout << "有大写字母" << endl;
        }
    }
}

string toUpper(string &str) {
    for (auto &t: str) {
        t = toupper(t);
    }
    return str;
}

int main() {
    check_is_lower("hello");
    string str = "hellO";
    toUpper(str);
    cout << str << endl;
}

一个用的 const,一个用的引用;因为 toUpper 需要修改字符串的内容,所以用引用,可以用常量指针。

string *const toUpper2(string *const str) {
    // * 解引用,拿到 str 的内容,然后获取 str 内容的头指针
    auto beg = begin(*str);
    auto last = end(*str);
    while (beg != last) {
        *beg = toupper(*beg);
        beg++;
    }
}

int main() {
    string str = "hellO";
    toUpper2(&str);
    // HELLO
    cout << str << endl;
}

数组形参

使用数组作用形参时与其他类型的数据有所不同。不允许拷贝数组,并且使用数组时(通常)会将其转换成指针。

<span style="color:orange">因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。</span>

尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式

// 尽管形式不同,但这三个 print 函数是等价的
// 每个函数都有一个 const int* 类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定

尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是 const int*类型的。当编译器处理对 print 函数的调用时,只检查传入的参数是否是 const int*类型

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

如何遍历数组呢?如果是用明确结束符的字符数组,可以使用判空,而更一般的数组可以采用标记标准库规范(begin/end)

void print(const int *beg, const int *end) {
    while (beg != end) {
        cout << *beg++ << endl;
    }
}

int main() {
    int parry[10] = {};
    for (int i = 0; i < 10; ++i) {
        parry[i] = i;
    }

    print(begin(parry), end(parry));
}

如果是指针呢?那就显示传递数组的大小过去。

void print(const int ia[], size_t size) {
    for (int i = 0; i < size; ++i) {
        cout << ia[i] << endl;
    }
}

int main() {
    int *parry = new int[10];
    for (int i = 0; i < 10; ++i) {
        parry[i] = i;
    }
    // const int ia[] 等价于 const int *ia
    print(parry, 10);
}

<b>数组形参和 const</b>

当函数不需要对数组元素执行写操作的时候,数组形参应该是指向 const 的指针

<b>数组引用形参</b>

C++ 语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上。

void f(int &arr[10]);    // 错误,将 arr 声明成了引用的数组
void f(int (&arr)[10]);  // 正确,arr 是具有 10 个整数的整型数组的引用

<b>传递多维数组</b>

前面说过,C++ 的多维数组可以看出一个由更低维元素组成的数组,所以多维数组的传递和一维数组的传递没什么区别。直接解引用的次数更多。

// matrix 指向数组首元素,该数组由 10 个元素构成
void print(int (*matrix)[10], int rowSize); 

切记,括号不可少 (*matrix)[10]

main处理命令行选项

和 Java 处理命令行的输入是类似的,执行的时候在后面追加参数。

int main(int argc, char **argv){
    // argv 指向 char*
}

假定 main 是生成的可执行文件

int main(int arg, char **avg) {
    cout << avg[0] << endl;
    cout << avg[1] << endl;
    cout << avg[2] << endl;
}

/*
./a.out a b c d
./a.out
a
b
*/

可变长形参

有时我们无法提前预知应该向函数传递几个实参,此时就需要可变长参数的支持了。

C++11 新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为 initializer_list 的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。不过主要用在和 C 函数交互的接口程序。

<b>initializer_list 形参</b>

<span style="color:orange">initializer_list 类型定义在同名的头文件中,initializer_list 对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值。</span>

函数 说明
initializer_list\<T> lst 默认初始化,T 类型元素的空列表
initializer_list\<T> lst{a, b, c...} lst 的元素数量和初始值一样多;<br>lst 的元素是对应初始值的副本<br>列表中的元素是 const
lst2(lst)<b>lst2 = lst 拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素;<br>拷贝后,原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 返回指向 lst 中首元素的指针
lst.end() 返回指向 lst 中尾元素下一位置的指针
#include<iostream>
#include <cstring>
#include <initializer_list>

using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::begin;
using std::end;

void msg(std::initializer_list <string> ll) {
    auto beg = begin(ll);
    auto last = end(ll);
    while (beg != last) {
        cout << *beg++ << endl;
    }
}

int main() {
    // std::initializer_list <string> ll = {"a", "b", "c", "end"};
    msg({"a", "b", "c", "end"});
    return 0;
}

<b>省略符形参</b>

省略符形参是为了便于 C++ 程序访问某些特殊的 C 代码而设置的,这些代码使用了名为 varargs 的 C 标准库功能。通常,省略符形参不应用于其他目的。你的 C 编译器文档会描述如何使用 varargs。

省略符形参应该仅仅用于 C 和 C++ 通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种

void foo(parm_list, ...);
void foo(...);
#include <stdio.h>
#include <stdarg.h>

void func(const char *str, ...) {
    va_list ap;

    int n = 3;
    char *a = nullptr;
    int b = 0;
    double c = 0.0;

    // 表示在 str 参数之后获取参数。
    va_start(ap, str);

    a = va_arg(ap, char * );
    b = va_arg(ap, int);
    c = va_arg(ap, double);

    // 最后注意要有va_end(ap)。
    va_end(ap);
    printf("%s is %s %d, %f", str, a, b, c);
}

int main() {
    func("Hello", "world", 1, 3.14);
}

返回类型

无返回值函数

与其他语言类似,会隐式执行 return,只是可以不写这个隐式的 return。

有返回值函数

要确保一定要有返回值,否则会发生意想不到的错误

// 如果省略了最后的 return -1,可能会有意想不到的错误
// 编译器是无法发现这种错误的
int testFor() {
    for (int i = 0; i < 10; ++i) {
        if (i == 15) return 10;
    }
    return -1;
}

<b>不要返回局部对象的引用或指针</b>

C++ 中的局部对象是分配在 stack 里的,方法调用就消失了。这样 return 出去的引用/指针指向的就不再是有效区域了。

const string &manip() {
    string ret;
    if (!ret.empty())
        return ret; // 错误写法!返回了局部对象的引用
    else
        return "empty";
}

<b>再谈引用</b>

引用可以作为函数的参数可以作为函数的返回值。

  • 作为函数的参数时,我们在定义函数时指定形式参数为引用类型即可。
  • 在返回时,我们直接返回数据,然后用引用类型的参数接收即可。
string &map(string &str) {
    str = str + ":hell0";
    return str;
}

int main() {
    string str = "nihaoya";
    string &refStr = map(str);
    refStr = "hello";
    cout << str << endl;
    return 0;
}
// hello

注意,只有函数的返回值是引用类型,你才可以用引用类型的变量接收函数的返回值。

<b>引用返回左值</b>

函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。我们能为返回类型是非常量引用的函数的结果赋值。

char &get_char(string &str, string::size_type ix) {
    return str[ix];
}

void test_get_char() {
    string s("hello");
    cout << s << endl;
    // 注意这句话
    get_char(s, 1) = 'A';
    cout << s << endl;
}

int main() {
    test_get_char();
    return 0;
}
// hello
// hAllo

<b>列表初始化返回值</b>

C++11 新标准规定,函数可以返回花括号包围的值的列表。

vector<string> process(vector<string> &svec) {
    if (svec.empty()) {
        svec.push_back("hello");
        svec.push_back("world");
        return {"no content", "function add some string"};
    } else {
        return svec;
    }
}

void test_process() {
    vector<string> svec;
    auto ans = process(svec);
    for (auto str: ans) {
        cout << str << "\t";
    }
}
// no content      function add some string

<b>main 函数的返回值</b>

main 函数的返回值可以是 void 也可以是 int。而 main 函数的返回值可以看做是状态指示器。返回 0 表示执行成功,返回其他值表示执行失败。

为了使返回值与机器无关,cstdlib 头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败。

#include<iostream>
#include<cstdlib>

int main() {
    return EXIT_SUCCESS;
}

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。

定义一个返回数组的指针或引用的函数比较烦琐,但是可以使用类型别名简化。

typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有 10 个整数的数组
using arrT = int[10]; // arrT 的等价声明
arrT* func(int i); // func 返回一个含有 10 个整数的数组的指针,
                   // 是这个指针指向这个包含10个元素的数组
                   // 不是说数组的首指针

数组指针可以这样理解

graph LR
数组指针-->|指向|数组首地址-->|指向|实际的元素

所以两次解引用后就可以拿到数组的首地址元素了。相当于一个二级指针。

#include<iostream>
#include <cstdlib>

using std::cout;
using std::endl;

typedef int arrT1[10];
using arrT2 = int[10];

arrT1 *function(int (*array)[10], size_t size) {
    auto arr = *array;
    // 相当于 int *arr = *array;

    for (int i = 0; i < size; ++i) {
        *arr = *arr + size;
        arr++;
    }
    return array;
}

int main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int (*parr)[10] = &arr;
    function(parr, 10);
    for (auto w: arr) {
        cout << w << "\t";
    }
    return EXIT_SUCCESS;
}

<b>不使用别名,直接返回</b>

Type (\*function(parameter_list)) [dimension]

// 什么都不用
string (*function(string (*str)[2]))[2] {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}

<b>使用 decltype</b>

int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8, 10};

// 指向数组 odd 的指针
decltype(odd) *arrPtr(int i) {
    return i % 2 == 0 ? &even : &odd;
}

int main() {
    auto w = arrPtr(2);
    cout << **w << endl; // 2
    return EXIT_SUCCESS;
}

decltype 并不负责把数组类型转换成对应的指针,所以 decltype 的结果是个数组,要想表示 arrPtr 返回指针还必须在函数声明时加一个*符号。

<b>使用尾置返回类型</b>

auto func(int i)->int(*)[10]
// 尾置返回类型
auto function4(string(*str)[2]) -> string(*)[2] {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}

<b>练习</b>

  • 编写一个函数的声明,使其返回数组的引用并且该数组包含 10 个 string 对象。不要使用尾置返回类型、decltype 或者类型别名
  • 为上一题的函数再写三个声明,一个使用类型别名,另一个使用尾置返回类型,最后一个使用 decltype 关键字。你觉得哪种形式最好?为什么?
  • 修改 arrPtr 函数,使其返回数组的引用
#include<iostream>
#include <cstdlib>

using std::cout;
using std::endl;
using std::string;
using strA = string[2];

string demo[2];

// 什么都不用
string (*function(string (*str)[2]))[2] {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}

// 类型别名
strA *function2(string(*str)[2]) {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}

decltype(demo) *function3(string(*str)[2]) {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}

// 尾置返回类型
auto function4(string(*str)[2]) -> string(*)[2] {
    string *tmp = *str;
    for (int i = 0; i < 2; ++i) {
        cout << tmp[i] << endl;
    }
    return str;
}


int main() {
    string str[] = {"hello", "world"};
    function(&str);
    function2(&str);
    function3(&str);
    return EXIT_SUCCESS;
}
int odd[] = {1, 2, 3, 4, 5};

decltype(odd) &arrRef(int i) {
    return odd;
}

int main() {
    int (&tmp)[5] = arrRef(2);
    return EXIT_SUCCESS;
}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-18 18:46 , Processed in 0.084704 second(s), 27 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表