😝 第 6 章 函数

第 6 章 函数

在前面我们已经使用过定义 main 函数,以及也见过其他的自定义函数,函数是一个命名了的代码块,我们通过调用函数执行相应的代码,函数可以有 0 个或多个参数,而且通常产生一个结果,C++可以重载函数,也就是说,同一个名字可以对应几个不同的函数

函数基础

函数的实参的类型要与函数的形参类型匹配,后者实参赋值给形参可以进行类型转换。

//example1.cpp
#include <iostream>
using namespace std;

//编写函数 返回类型为int
int double_(int num)
{
    return 2 * num;
}

int main(int argc, char **argv)
{
    //调用函数
    cout << double_(3) << endl; //实参为3
    //形参为num
    return 0;
}

函数参数列表

函数的形参可以为多个,形成函数的形参列表

//example2.cpp
#include <iostream>
using namespace std;

int mul(int num1, int num2)
{
    return num1 * num2;
}

int main(int argc, char **argv)
{
    cout << mul(2, 3) << endl; // 6
    return 0;
}

局部对象

在 C++中,名字是有作用域的,对象有生命周期,形参和函数内部定义的变量统称为局部变量,其作用域在函数内部,且一旦函数执行完毕,相应内存资源被释放即栈内存。分配的栈内存将会保留,直到我们调用 free 或者 delete。

//example3.cpp
#include <iostream>
using namespace std;
int &func()
{
    int i = 999;
    return i;
}

int *func1()
{
    int *i = new int(999);
    return i;
}

int main(int argc, char **argv)
{
    int *num = func1();
    cout << *num << endl; // 999
    delete num;
    int &i = func();
    cout << i << endl;
    //程序会报错,为什么,因为func调用完毕后其内的i变量内存被释放,所以相应的引用是无效的
    return 0;
}

局部静态组件

局部静态对象在程序的执行路径第一次经过对象定义语句时进行初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

//example4.cpp
#include <iostream>
using namespace std;
int count()
{
    static int num = 0;
    ++num;
    return num;
}
int main(int argc, char **argv)
{
    cout << count() << endl; // 1
    cout << count() << endl; // 2
    for (int i = 0; i < 4; i++)
    {
        cout << count() << endl; // 3 4 5 6
    }
    return 0;
}

函数声明

函数的名字必须在使用前声明,与变量类似,函数只能定义一次,但可以声明多次。

//example5.cpp
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    // func();// error: 'func' was not declared in this scope
    //在每调用前没有声明或者定义
    return 0;
}

void func()
{
    cout << "hello function" << endl;
}

声明提升

//example6.cpp
#include <iostream>
using namespace std;

void func(); //函数声明

int main(int argc, char **argv)
{
    func();
    return 0;
}

//函数定义
void func()
{
    cout << "hello function" << endl;
}

在头文件中进行函数声明

//example7.cpp
#include "example7.h"
#include <iostream>
int main(int argc, char **argv)
{
    func(); // hello world
    return 0;
}

void func()
{
    std::cout << "hello world" << std::endl;
}

自定义头文件

//example7.h
#ifndef __EXAMPLE7_H__
#define __EXAMPLE7_H__

void func(); //函数声明

#endif

分离式编译

一个程序可以分为多个 cpp 文件,也就是将程序的各个部分分别存储在不同文件中。
大致原理是,对多个 cpp 分别编译,然后将多个编译后的部分进行链接操作形成了整体的程序,虽然在多个 cpp 中编写,但是我们只有一个全局命名空间,也就是说在多个 cpp 内定义相同名字的变量这是不被允许的。

example8.cpp

//example8.cpp
#include <iostream>
#include "func.h"
using namespace std;
// int i = 999;
//出错因为func.cpp已经定义了int i,不能有重复定义,全局命名空间只有一个
int main(int argc, char **argv)
{
    func(); // hello world
    return 0;
}

func.cpp

//func.cpp
#include "func.h"
#include <iostream>
using namespace std;
int i = 999;
void func()
{
    cout << "hello world" << endl;
}

func.h

#ifndef __FUNC_H__
#define __FUNC_H__
void func();
#endif

分别编译并且最后链接

g++ -c example8.cpp
g++ -c func.cpp
g++ example8.o func.o -o example8.exe
./example8.exe

或者编译并链接

g++ example8.cpp func.cpp -o example8.exe
./example8.exe

参数传递

调用函数时参数的传递分为传引用调用(引用传递)和传值调用(值传递)

传引用

//example9.cpp
#include <iostream>
using namespace std;
void func(int &i, int *j)
{
    i -= 1;
    *j -= 1;
}
int main(int argc, char **argv)
{
    int i = 0, j = 0;
    func(i, &j);//传递i的引用与j的内存地址
    cout << i << " " << j << endl; //-1 -1
    return 0;
}

为什什么要提供引用传递呢

对拷贝大的类类型对象或者容器对象,甚至有的类类型对象不支持拷贝,只能通过引用形参支持在其他函数内调用对象,例如有字符串非常长,我们根据操作情况,可以选择引用传递,因为那样省略去了字符串拷贝,节约了内存资源,使得程序效率更高。

//example10.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

void func(string &str, vector<int> &vec)
{
    cout << str << endl;
    for (auto &item : vec)
    {
        cout << item << " ";
        item++;
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    vector<int> v{1, 2, 3, 4, 5};
    string str = "hello c++";
    func(str, v);
    // hello c++ 1 2 3 4 5
    func(str, v);
    // hello c++ 2 3 4 5 6
    return 0;
}

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

//exaple11.cpp
#include <iostream>
#include <string>
using namespace std;

int func(int i, string &message)
{
    if (i < 0)
    {
        message = "i<0";
        return i < 0;
    }
    message = "i>=0";
    return i < 0;
}

int main(int argc, char **argv)
{
    string message;
    func(-1, message);
    cout << message << endl; // i<0
    return 0;
}

const 形参和实参

关于顶层 const 的回顾

const int ci = 42;//ci不能被赋值改变,const是顶层const
int i=ci;//i可以被赋值,拷贝ci时忽略其顶层const
int *const p=&i;//const是顶层的,不能给p赋值
*p=0;//正确,可以改变p的内容,但不能改变p本身存储的内存地址

形参的底层 const 与顶层 const

//example12.cpp
#include <iostream>
using namespace std;
// p同时加底层const与顶层const
void func(int i, const int *const p)
{
    cout << i << endl;  // 23
    cout << *p << endl; // 23
    //*p = 99; error: assignment of read-only location '*(const int*)p'
    // p = nullptr; error: assignment of read-only parameter 'p'
}
int main(int argc, char **argv)
{
    const int i = 23;
    func(i, &i);
    return 0;
}

为什么说当实参初始化形参时会忽略掉顶层 const?

//example13.cpp
#include <iostream>
using namespace std;

void func(const int j)
{
    cout << j << endl; // 999
}

// void func(int j)
// {
// }
// 'void func(int)' previously defined here
// 因为顶层const是相对于函数内部作用而言的,对函数外部都是进行了拷贝

int main(int argc, char **argv)
{
    int num = 999;
    func(num);
    //对于外部的调用将忽略形参的顶层const因为有没有const外部都是进行对形参赋值
    return 0;
}

指针或引用形参与 const

//example14.cpp
#include <string>
#include <iostream>
using namespace std;

// const int *p=&num; const string &str=mstr;
// p是有顶层const的int指针 str为常量引用
void func(const int *p, const string &str)
{
    string new_str = "hello";
    // str = new_str; //错误 因为str为常量的引用
    //  str = "hello";//错误 因为str为常量的引用
    int num = 999;
    p = &num;
    cout << str << endl;
}

//引用常量 底层const
//虽然有这种写法 但是我们好像从不用这种,没有引用常量
// void func(string const &str)
// {
//     cout << str << endl;
//     str = "hello";
// }

int main(int argc, char **argv)
{
    int num = 100;
    const string mstr = "hi";
    func(&num, mstr); // hi
    string name = "gaowanlu";
    func(&num, "oop"); // oop
    return 0;
}

总之 关于 const 与引用、指针的配和往往会使得我们头大,所以我们还是要多回顾复习以前的变量章节的 const 的知识

数组的传递

总之传递数组就是在传递内存地址

//example15.cpp
#include <iostream>
using namespace std;
void func(int arr[])
{
    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] << " ";
        arr[i]++;
    } // 1 2 3 4 5
    cout << endl;
}

//重载失败 因为int*p与int arr[]等效

// void func(int *p)
// {
//     cout << sizeof(p) << endl;
// }

// void func(int arr[5])
// {
// }

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

int main(int argc, char **argv)
{
    int arr[5] = {1, 2, 3, 4, 5};
    func(arr);                   // 1 2 3 4 5
    func(arr);                   // 2 3 4 5 6
    print(begin(arr), end(arr)); // 3 4 5 6 7
    return 0;
}

数组形参与 const

//example16.cpp
#include <iostream>
using namespace std;

// const int arr[]等价于const int *arr
// 底层const可以改变arr存储的地址 不能通过arr改变内存地址上的数据
// 即const int 的指针类型 const int * ,也就是数组的每个数据都是const int
void func(const int arr[], size_t size)
{
    size /= sizeof(int);
    for (int i = 0; i < size; i++)
    {
        cout << arr[i] << " ";
    }
    // arr[0] = 12; 错误不能改变数组的值
    cout << endl;
    int num = 999;
    arr = &num;
    //*arr = 1000;//error: assignment of read-only location '* arr'
}

int main(int argc, char **argv)
{
    const int arr[] = {1, 2, 3, 4};
    // arr[0] = 1;//error: assignment of read-only location 'arr[0]'
    func(arr, sizeof(arr)); // 1 2 3 4

    int num = 0;
    int const *p = &num; //底层const
    //*p = 999;//error 底层const
    cout << *p << endl; // 0
    return 0;
}

数组引用形参

//example17.cpp
#include <iostream>
using namespace std;

void func(int (&arr)[5])
{
    for (auto item : arr)
    {
        cout << item << endl;
    }
}

//错误 因为数组的引用必须指定数组的长度
void func1(int (&arr)[], int size)
{
    for (int i = 0; i < size; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    int arr[] = {1, 2, 3, 4, 5};
    func(arr);
    int arr1[] = {1, 2, 3};
    // func(arr1);//error 数组长度不是5
    // func1(arr1, 3); //error 形参没有指定数组的长度 数组的引用必须指定长度

    // int(&arr2)[] = arr1;//同理这里也是错误的
    // cout << arr2[0] << endl;
    return 0;
}

传递多维数组

总之简单的办法就是传递指针,然后也可以使用数组的引用

//example18.cpp
#include <iostream>
using namespace std;

// int *matrix[10] 10个指针构成的数组
// int (*matrix)[10] 指向含有10个整数的数组的指针
void func1(int (*arr)[5], int size)
{
    cout << size / sizeof(int) / 5 << endl; // 2
}

void func2(int arr[][5], int size)
{
    size = size / sizeof(int) / 5;
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < sizeof(arr[i]) / sizeof(int); j++)
        {
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }
}

int main(int argc, char **argv)
{
    int arr[][5] = {
        {1, 2, 3, 4, 5},
        {1, 2, 3, 4, 5}};
    func1(arr, sizeof(arr));
    func2(arr, sizeof(arr)); // 1 2 3 4 5 1 2 3 4 5
    return 0;
}

main 函数的形参

提供了在运行程序时赋值指定实参的功能

//example19.cpp
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    for (int i = 0; i < argc; i++)
    {
        cout << argv[i] << endl;
    }
    return 0;
}
//输出 aaa bbb ccc
g++ example19.cpp -o example19.exe
./example19.exe aaa bbb ccc

可变形参

C++11 有新特性,在我们无法提前预知向函数传递几个实参,在 C++11 中,如果所有的实参类型相同,可以传递 initializer_list 类型,如果实参的类型不相同可以编写特殊的函数,所谓的可变参数模板。

还有一种特殊的形参类型即省略符,可以用来传递可变数量的实参,不过一般这种功能只用于 C 函数交互的接口程序。

initializer_list 形参

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

使用样例

//example20.cpp
#include <iostream>
#include <string>
#include <initializer_list>
using namespace std;

void mfun(initializer_list<string> list)
{
    for (auto beg = list.begin(); beg != list.end(); beg++)
    {
        cout << beg << " " << *beg << " ";
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    mfun({"1", "2", "3"}); // 0x61feb0 1 0x61fec8 2 0x61fee0 3
    initializer_list<string> params{"1", "2", "3", "4"};
    mfun(params); // 0x61fe48 1 0x61fe60 2 0x61fe78 3 0x61fe90 4
    // params[0];//error initializer_list不支持下标访问
    auto list1(params); //拷贝params对象 但是它们的元素的内存是共用的
    mfun(list1);        // 0x61fe48 1 0x61fe60 2 0x61fe78 3 0x61fe90 4
    auto list2 = list1;
    mfun(list2); // 0x61fe48 1 0x61fe60 2 0x61fe78 3 0x61fe90 4
    //总之initializer_list的元素是只读的
    return 0;
}

省略符形参

省略符形参只能出现在形参列表的最后一个位置
总之它的作用就是可以当用户输入参数多的时候我们可以省略,但不影响函数的正常运行。

//example21.cpp
#include <iostream>
using namespace std;

void fun1(int num1, int num2, ...)
{
    cout << num1 << " " << num2 << endl;
}

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

int main(int argc, char **argv)
{
    fun2(1, 2, 3, 4); // fun2
    fun1(1, 2);       // 1 2
    return 0;
}

返回类型和 return 语句

return 有两种形式,用于终止当前执行的函数并将控制权返回到调用函数的地方。

return;
return expression;

无返回值的函数

无返回值的函数返回值即为 void,无需要我们显式的 return;但是允许使用 return;提前终止函数的执行。

//example22.cpp
#include <iostream>
using namespace std;
void func(int num)
{
    if (num == 0)
    {
        cout << "num==0" << endl;
        return;
    }
    //此处无需显式的return语句
}
int main(int argc, char **argv)
{
    func(1);
    func(0); // num==0
    return 0;
}

有返回值的函数

有一点要确定,一个函数的返回值类型是唯一确定的,不能声明函数时的返回值类型与实际返回值类型不同,否则编译阶段会报错。

要注意的是,有返回值的函数,必须要保证函数执行结束时,有 return 语句返回相应类型的值

//example23.cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<string> func()
{
    // return 1;// error: could not convert '1' from 'int' to 'std::vector<std::__cxx11::basic_string<char> >'
    return {"dscs", "csdcd"};
}

int main(int argc, char **argv)
{
    vector<string> vec = func();
    for (auto &str : vec)
    {
        cout << str << endl;
    } // dscs csdcd
    return 0;
}

返回值类型

//example24.cpp
#include <iostream>
#include <string>
using namespace std;

string func(string &str)
{
    return str; //返回str的拷贝
}

int main(int argc, char **argv)
{
    string str = "hello";
    string back = func(str);
    cout << str << " " << back << endl; // hello hello
    str = "apple";
    cout << str << " " << back << endl; // apple hello
    return 0;
}

返回引用类型

//example25.cpp
#include <iostream>
#include <string>

using namespace std;

string &func(string &str)
{
    return str;
}

int main(int argc, char **argv)
{
    string str1 = "hello";
    string &str2 = func(str1);
    cout << str1 << " " << str2 << endl; // hello hello
    str2 = "nike";
    cout << str1 << " " << str2 << endl; // nike nike
    return 0;
}

不要返回内部对象的引用或指针

为什么这么说呢?知道函数内部的对象是存储在栈内存的,当上下文离开此函数返回调用它的函数时,此函数所使用的栈内存将会被释放,也就是其内部的变量所用的存储空间也将会消失,使用值返回类型,或者堆内存地址,或者此函数外部的资源,在外部才可以访问到。

//example26.cpp
#include <iostream>
using namespace std;

//返回内部对象的地址
int *func1()
{
    int num = 999;
    return &num;
}

//返回内部对象的引用
int &func2()
{
    int num = 999;
    return num;
}

int main(int argc, char **argv)
{
    int *num1 = func1();                 //警告
    int &num2 = func2();                 //警告
    cout << num1 << " " << num2 << endl; //出错
    return 0;
}

返回类类型的函数和调用运算符

什么意思?简单的说我们可以把函数调用的整体视为一个其返回值类型的变量。

//example27.cpp
#include <iostream>
#include <string>
using namespace std;

string func()
{
    return string("hello");
}

int main(int argc, char **argv)
{
    cout << func().length() << endl; // 5
    cout << func().empty() << endl;  // 0
    return 0;
}

引用返回左值

函数的返回类型决定函数调用是否是左值(即可以放在等号左边和右边)

//example28.cpp
#include <iostream>
#include <string>
using namespace std;

char &func(string &str, size_t at)
{
    return str[at];
}

int main(int argc, char **argv)
{
    string str = "hello";
    char &ch = func(str, 0);
    ch = 'A';
    // func(str, 0) = 'A';
    cout << str << endl; // Aello
    return 0;
}

列表初始化 vector 并返回

C++11 中,支持花括号初始化 vector

//example29.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
vector<string> func(int num)
{
    switch (num)
    {
    case 1:
        return {"a", "b", "c"};
    case 2:
        return {"d", "e", "f"};
    default:
        return {};
    }
}
int main(int argc, char **argv)
{
    vector<string> vec1 = func(1);
    for (auto &str : vec1)
    {
        cout << str << endl; // a b c
    }
    cout << func(3).size() << endl; // 0
    return 0;
}

main 函数的返回值

main 函数的返回值可以看做是状态指示器,返回 0 表示执行成功,返回其他值表示执行失败。在 cstdlib 头文件中定义了两个预处理变量。

//example30.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
int main(int argc, char **argv)
{
    int num = 0;
    if (num)
        return EXIT_SUCCESS;
    return EXIT_FAILURE;
}

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。

如下面一个求首项为 1,差为 1 的等差数列的和

//example31.cpp
#include <iostream>
using namespace std;

int sum(int num)
{
    if (num <= 1)
        return num;
    return num + sum(num - 1);
}

int main(int argc, char **argv)
{
    cout << sum(4) << endl; // 1+2+3+4=10
    cout << sum(0) << endl; // 0
    cout << sum(2) << endl; // 3
    return 0;
}

返回数组指针

遇到一个问题,数组应该怎么返回呢?因为数组不能被拷贝,所以函数不能返回数组,但函数可以返回数组的首地址或引用。

返回数组的头地址,即返回指针类型

//example32.cpp
#include <iostream>
using namespace std;
typedef int Array[10];

int *func(int (&arr)[10])
{
    return arr;
}

int main(int argc, char **argv)
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    int *ptr = func(arr);
    for (int i = 0; i < 10; i++)
    {
        cout << *(ptr + i) << " ";
    } // 1 2 3 4 5 6 7 8 9 0
    cout << endl;
    return 0;
}

返回数组的指针

//example33.cpp
#include <iostream>
using namespace std;

int (*func())[5]
{
    int(*arr)[5] = (int(*)[5]) new int[5];
    for (int i = 0; i < 5; i++)
        *arr[i] = i;
    return arr;
}

int main(int argc, char **argv)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int(*arr_ptr)[5] = &arr; //数组的指针
    cout << arr_ptr << endl; // 0x61fef8
    cout << arr << endl;     // 0x61fef8
    cout << *arr << endl;    // 1 本质都是第一个元素的地址

    int(*arr_back)[5] = func();
    for (int i = 0; i < sizeof(arr_back); i++)
    {
        cout << *arr_back[i] << " ";
    } // 0 1 2 3
    cout << endl;
    delete arr_back;
    return 0;
}

后置返回类型

C++11 有新特性,尾置返回类型。我们发现函数返回函数指针没办法直接写

#include <iostream>
using namespace std;

int impl()
{
    return 666;
}

// 错误写法
// int (*)(int) get_impl()
// {
//     return impl;
// }

// 使用typdef
typedef int (*impl_type)();

impl_type get_impl()
{
    return impl;
}

// 使用后置返回
auto get_impl_back() -> int (*)()
{
    return impl;
}

// 看来有点强啊看着就很舒服对吧,看起来不像C++
auto func() -> int (*)[5] // 返回数组指针
{
    int(*arr)[5] = (int(*)[5]) new int[5];
    for (int i = 0; i < 5; i++)
    {
        *arr[i] = i * 2;
    }
    return arr;
}

// 这里就是指针数组哦,和上面别记错了
auto main(int argc, char *argv[]) -> int
{
    for (int i = 0; i < argc; i++)
    {
        auto c_str = argv[i]; // char *c_str
        cout << c_str << endl;
    }
    int(*arr)[5] = func();
    for (int i = 0; i < 5; i++)
    {
        cout << *arr[i] << " ";
        auto ptr = arr[i]; // int *ptr
        cout << *ptr << endl;
    } // 0 0 2 2 4 4 6 6 8 8
    cout << endl;
    delete[] arr;
    cout << get_impl()() << endl;      // 666
    cout << get_impl_back()() << endl; // 666
    return 0;
}

使用 decltype

解决返回数组指针函数的声明

//example35.cpp
#include <iostream>
using namespace std;

int arr_int_10[10];

decltype(arr_int_10) *func()
{
    return (int(*)[10]) new int[10];
}

int main(int argc, char **argv)
{
    decltype(arr_int_10) *arr = func();
    for (int i = 0; i < sizeof(arr_int_10) / sizeof(int); i++)
    {
        *arr[i] = i * 3;
        cout << *arr[i] << " ";
    } // 0 3 6 9 12 15 18 21 24 27
    cout << endl;
    delete arr;
    return 0;
}

函数重载

如果一个作用域内的几个函数名字相同但参数列表不同,称为重载(overloaded)函数

//example36.cpp
#include <iostream>
using namespace std;
static void print()
{
    std::printf("print()\n");
}
void print(const char *str)
{
    std::printf("print(\"%s\")\n", str);
}

int main(int argc, char **argv)
{
    print();        // print()
    print("hello"); // print("hello")
    return 0;
}

在上面的例子中,有个函数加了 static 是怎么么回事呢,这样这个函数仅仅在这个 cpp 文件内有效,也就是说它的作用域仅仅在这个 cpp 内,而不是我们可执行程序的全局作用域

重载和 const 形参

指针 const 形参

//example37.cpp
#include <iostream>
#include <string>
using namespace std;
struct Person
{
    string name;
    int age;
};

//相当于void print(const Person *ptr) ptr拥有底层const
//函数声明时无视指针的顶层const
void print(Person const *ptr)
{
    // ptr->age = 99;//error ptr有底层const
    ptr = nullptr;
    cout << "print Person const*ptr" << endl;
}

// error:: 重复定义void print(Person const *ptr)
// void print(const Person *ptr)
// {
//     cout << "print const Person *ptr" << endl;
// }

void print(Person *ptr)
{
    cout << "print Person*ptr" << endl;
}

int main(int argc, char **argv)
{
    Person person;
    const Person *ptr = &person;
    Person const *ptr1 = &person;
    print(ptr1);    // print Person const *ptr
    print(ptr);     // print Person const *ptr
    print(&person); // print Person*ptr
    return 0;
}

引用 const 形参

有一点我们要清楚、指针与引用的最大区别其实是指针不用定义时就初始化,而引用必须被初始化,且引用初始化以后无法更改其绑定的变量,而指针可以更换其绑定的变量。

//example38.cpp
#include <iostream>
#include <string>
using namespace std;

void func(string &str)
{
    cout << "func(string &str)" << endl;
}

// void func(const string &str)
// {
//     // str[0] = 'o';//error:: str拥有底层const
//     cout << "func(const string &str)" << endl;
// }

void func(string const &str)
{
    const string str1 = "cd";
    // str = str1;
    // error:: str有底层const 要知道一个非引用类型赋给引用类型是赋值,而非更换引用的绑定
    cout << "func(string const &str)" << endl;
}

int main(int argc, char **argv)
{
    string str = "hello world";
    func(str); // func(string &str)
    const string str1 = "const string str1";
    func(str1); // func(const string &str)
    return 0;
}

const_cast 在函数重载中的用途

什么是 const_cast 是不是已经忘记了,他在《第四章 表达式》类型转换内容中,是显式转换

const_cast只能改变运算对象的底层 const,const_cast 中的类型必须是指针、引用或指向对象类型成员的指针。

const_cast 回顾

//example39.cpp
#include <iostream>
#include <string>
using namespace std;

struct Person
{
    string name;
};

int main(int argc, char **argv)
{
    const int i = 999;
    const int *ptr = &i;
    // int const转非const
    int *j = const_cast<int *>(ptr);
    *j = 1000;
    cout << i << endl;  // 999
    cout << *j << endl; // 1000

    //非const转const
    const int *ptr1 = const_cast<const int *>(j);
    //*ptr1 = 999; //error readonly

    const string str1 = "str1";
    const string &str2 = str1;
    string &str3 = const_cast<string &>(str2);
    cout << str2 << endl; // str1
    cout << str3 << endl; // str1
    str3 = "dscs";
    cout << str2 << endl; // dscs
    cout << str3 << endl; // dscs

    const string const_str = "you";
    const string *const_str1_ptr = &const_str;
    cout << *const_str1_ptr << endl; // you
    string *str1_ptr_casted = const_cast<string *>(const_str1_ptr);
    *str1_ptr_casted = "hello";
    cout << *str1_ptr_casted << endl; // hello
    cout << *const_str1_ptr << endl;  // hello

    const Person person;
    const Person *person_ptr = &person;
    Person *person_ptr_casted = const_cast<Person *>(person_ptr);
    person_ptr_casted->name = "ppp";
    cout << person.name << endl; // ppp

    //对于复合类型在const type*用const_cast转为type*时是解除const
    //对于基本类型如上面的int 转换时 是将其值复制了一份 内存并不共用
    return 0;
}

const_cast 在函数重载中的用途

//example40.cpp
#include <iostream>
#include <string>
using namespace std;

const string &shorter(const string &s1, const string &s2)
{
    return s1.length() < s2.length() ? s1 : s2;
}

string &shorter(string &s1, string &s2)
{
    //不进行const_cast则会造成递归而不会调用shorter(const string &s1, const string &s2)
    auto &shot = shorter(
        const_cast<const string &>(s1),
        const_cast<const string &>(s2));
    return const_cast<string &>(shot);
}

int main(int argc, char **argv)
{
    string s1 = "abc";
    string s2 = "n";
    string &shot = shorter(s1, s2);
    cout << shot << endl; // n
    return 0;
}

关于定义冲突与匹配调用

常见冲突

//1
void calc(int *num, int *c)
{
}
//2
void calc(const int *num, const int *c)
{
}
//3
void calc(int const *num,int const*c){

}
//1 2不冲突 ,1 3 不冲突, 2 3 冲突
//1
void calc(int num,int c){

}
//2
void calc(const int num,const int c){

}
//1 2 冲突

当调用重载函数时有三种可能的结果

1、编译器找到一个与实参最佳匹配的函数,并生成调用函数该函数的代码
2、找不到任何一个函数与调用的实参匹配,此时编译器发出误匹配错误
3、有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时发生错误,称为二义性调用,也就是函数重载重复定义

重载与作用域

在 C++中重载并不影响作用域,但是还有一种局部函数作用域的情况

//example41.cpp
#include <iostream>
using namespace std;

void func(int num);
void func(float num);

int main(int argc, char **argv)
{
    func(1.1f);         // float 1.1
    func(1);            // int 1
    void func(int num); //局部函数声明
    func(1.1f);         // int 1
    //为什么呢?因为其这保留了void func(int num);
    void func(float num);
    func(1);    // int 1
    func(1.1f); // float 1.1
    if (true)
    {
        void func(int num); //局部函数声明作用域为块作用域
        func(1.1f);         // int 1
    }
    return 0;
}

void func(int num)
{
    std::cout << "int " << num << endl;
}

void func(float num)
{
    std::cout << "float " << num << endl;
}

为什么要有这种操作呢?这明显不是一种优雅的操作,当想在某个块只允许使用某个函数的重载的一部分时,它往往会有其较好作用,但是我们应该尽量避免这种操作

特殊用途语言特性

默认实参

只能省略函数尾部形参对应的实参,也就是说实参列表与形参列表是从左面一个一个匹配的

默认实参是从调用者的角度来考虑的,如果调用者直到目标函数的声明里有默认实参,而且开发者并没有提供相应参数,则编译器将会在调用处使用默认实参来充当实际实参传递给目标函数。而不是站在目标函数本身来考虑,有没有实参传过来。

//example42.cpp
#include <iostream>
using namespace std;

int func1(int a, int b = 8, int c);

int func(int num = 1, int c = 3)
{
    return num * c;
}

int func1(int a, int b, int c)
{
    return a * b * c;
}

int main(int argc, char **argv)
{
    cout << func() << endl;     // 3
    cout << func(5, 2) << endl; // 10
    cout << func(2) << endl;    // 6
    // func(, 2);//error 只能省略尾部的实参
    // cout << func1(1, 2);//error 只能省略尾部的实参
    return 0;
}

默认参数初始化

默认参数的初始化是在调用函数时进行的

//example43.cpp
#include <iostream>
#include <string>
using namespace std;

int num = 99;

int double_num()
{
    return num * 2;
}

//声明
string func(int a = double_num(),int b = num,char c = '*');

//定义
string func(int a , int b , char c)
{
    cout << a << " " << b << " " << c << endl;
    return "hi";
}

int main(int argc, char **argv)
{
    num++;
    cout << func() << endl; // 200 100 * hi
    return 0;
}

内联函数和 constexpr 函数

什么是内联函数?内联函数可以避免函数调用的开销,将函数指定为内联函数,通常将它在每个调用点上“内联地”展开,一般而言最适合声明为 inline 的函数,体积小常被调用,所从事的计算并不复杂。inline 函数的定义,常常被放在头文件中,由于编译器必须在它调用的时候加以展开,所以这个时候其定义必须是有效的

//example44.cpp
#include <iostream>
using namespace std;

inline int &max(int &i, int &j)
{
    return i > j ? i : j;
}

int main(int argc, char **argv)
{
    //在实际程序编译中 max(3,4)被max函数内地内容替代
    cout << max(3, 4) << endl; // 4
    //相当于
    int i = 3;
    int j = 4;
    cout << (i > j ? i : j) << endl;
}

增加了程序的大小,但提高了效率

constexpr 函数

constexpr函数,常量表达式函数的返回值可以在编译阶段就计算出来,不过在定义常量表示函数的时候,会遇到更多的约束规则,在C++14后标准对这些规则有所放宽。

  1. 函数必须返回一个值,所以返回值类型不能是void。
  2. 函数体必须只有一条语句: return expr,其中expr必须是一个常量表达式,如果函数有形参,则将形参替换到expr中后,expr仍然必须是一个常量表达式。
  3. 函数使用之前必须有定义。
  4. 函数必须用constexpr声明。

constexpr函数反例

// constexpr函数反例 C++11标准
#include <iostream>
using namespace std;

// constexpr 函数不能具有非文本返回类型 "void"
constexpr void foo()
{
}

// constexpr 函数返回值不是常量
constexpr int next(int x)
{
    return ++x;
}

int g()
{
    return 42;
}

// constexpr 函数返回值不是常量
constexpr int f()
{
    return g();
}

constexpr int max_unsigned_char2();
enum
{
    max_uchar = max_unsigned_char2(); // 未定义 constexpr 函数 "max_unsigned_char2"
}

constexpr int abs2(int x)
{
    if (x > 0) // 语句不能出现在 constexpr 函数中
    {
        return x;
    }
    else
    {
        return -x;
    }
}
// abs2改为 return x > 0 ? x : -x; C++11即可编译通过

constexpr int sum(int x)
{
    int result = 0;
    while (x > 0) // 语句不能出现在 constexpr 函数中
    {
        result += x--;
    }
    return result;
}
// sum改为 x > 0 ? x + sum(x-1) : 0; C++11即可编译通过

int main(int argc, char **argv)
{
    return 0;
}

C++11标准使用正确

//example45.cpp
#include <iostream>
using namespace std;

constexpr int square(int x)
{
    return x * x;
}

constexpr char *func1()
{
    return "hello wrold";
}

constexpr float version()
{
    return 1.12;
}

constexpr int index(int num)
{
    return num;
}

int main(int argc, char **argv)
{
    cout << func1() << endl; // hello world
    constexpr char *str = func1();
    cout << str << endl;       // hello world
    cout << version() << endl; // 1.12
    int version_ = version();
    cout << version_ << endl; // 1

    constexpr int length = 10;
    int arr1[length];
    int arr2[index(10)];
    int arr3[index(length)];
    int size = 99;

    int arr4[index(size)]; // 虽然没报错,但本质是错误的,index(size)不是常量表达式
    // 当constexpr 函数返回值利用形参,当实参传入的也是consexpr时函数才会返回constexpr
    arr4[0] = 322;
    cout << arr4[0] << endl; // 322

    return 0;
}

有了常量表达式函数的支持,C++标准对STL也做了一些改进,比如在<limits>中增加了constexpr声明

#include <iostream>
#include <limits>
using namespace std;

// C++11就可编译成功
char buffer[std::numeric_limits<unsigned char>::max()] = {0};

int main(int argc, char **argv)
{
    return 0;
}

constexpr函数退化为普通函数

虽然常量表达式函数的返回值可以在编译期计算出来,但是行为并不是确定的,如,带形参的常量表达式函数接受了一个非常量实参时,常量表达式函数可能会退化为普通函数。

#include <iostream>
using namespace std;

constexpr int square(int x) { return x * x; }

int main(int argc, char **argv)
{
    int x = 5;
    std::cout << square(x) << "\n";
    return 0;
}

x不是一个常量因此square的返回值也可能无法在编译期确定,但是它依然能成功编译运行,因为该函数退化成了一个普通函数。

C++14对常量表达式函数的增强

C++11标准对常量表达式函数的要求是非常严格的,影响该特性的实用性。

C++14得到了改善

  1. 函数体允许声明变量,除了没有初始化、static、和thread_local变量。
  2. 函数允许出现if和switch语句,不能使用go语句。
  3. 函数允许所有的循环语句,包括for while do-while
  4. 函数可以修改声明周期和常量表达式相同的对象
  5. 函数的返回值可以声明为void
  6. constexpr声明的成员函数不再具有const属性
  7. 常量表达式函数的增强同样会影响常量表达式构造函数(第7章节 constexpr 构造函数)
#include <stdint.h>
#include <iostream>

// 支持if
constexpr int my_abs(int x)
{
    if (x > 0)
    {
        return x;
    }
    else
    {
        return -x;
    }
}

// 支持声明变量 出现while 修改变量等
constexpr int sum(int x)
{
    int result = 0;
    while (x > 0)
    {
        result += x--;
    }
    return result;
}

constexpr int next(int x) { return ++x; }

// C++14常量表达式函数的增强同样会影响常量表达式构造函数
class X
{
public:
    constexpr X() : x1(5) {}
    constexpr X(int i) : x1(0)
    {
        if (i > 0)
        {
            x1 = 5;
        }
        else
        {
            x1 = 8;
        }
    }
    constexpr void set(int i) { x1 = i; }
    constexpr int get() const { return x1; }

private:
    int x1;
};

// 支持声明变量
constexpr X make_x()
{
    X x; // 此处的x并不是一个constexpr
    x.set(42);
    return x;
}

int main()
{
    char buffer1[sum(5)] = {0};
    char buffer2[my_abs(-5)] = {0};
    char buffer3[next(9)] = {0};

    constexpr X x1(-1);
    constexpr X x2 = make_x();
    constexpr int a1 = x1.get();
    constexpr int a2 = x2.get();
    std::cout << a1 << "," << a2 << std::endl; // 8,42
    return 0;
}

内联函数与 constexpr 函数放在头文件内

与其他函数不同的是,内联函数和 constexpr 函数可以在程序中多次定义,因为在每个 cpp 单独编译时,比如内联函数,他就要将代码填充至调用处了,所以 constexpr 函数与 inline 函数通常定义在头文件中

调试帮助

主要有两种方式,assert 和 NDEBUG

assert 预处理宏

#include<cassert>
assert(expr)

首先对 expr 求值,如果表达式为假即 0,assert 输出信息并终止程序的执行,如果为真即非 0,assert 什么也不做

//example46.cpp
#include <iostream>
#include <cassert>
using namespace std;
int main(int argc, char **argv)
{
    assert(1 < 2);
    assert(1 > 2);
    // Assertion failed: 1 > 2, file example46.cpp, line 7
    cout << "end" << endl; //没有被执行
    return 0;
}

NDEBUG 预处理变量

assert 的形为依赖于一个名为 NDEBUG 的预处理变量的状态,如果定义了 NDEBUG 则 assert 什么也不做,默认情况下没有定义 NDEBUG

//example47.cpp
#include <iostream>
#include <cassert>
using namespace std;
#define NDEBUG
int main(int argc, char **argv)
{
    assert(1 < 2);
    cout << "end" << endl; // end
#ifdef NDEBUG
    cout << "NDEBUG" << endl; // NDEBUG
#endif
    return 0;
}

使用编译器时决定是否 define NDEBU

g++ example46.cpp -o example46.exe -D NDEBUG && ./example46.exe
//example46程序输出则assert什么都没有干
end

帮助我们调试的宏定义

__func__ 编译器定义的局部静态变量,存放函数的名字
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整形字面值
__TIME__ 存放文件编译日期的字符串字面值
__DATE__ 存放文件编译时期的字符串字面值

使用样例

也就是说编译后,无论运行多少次,每次的宏所代表的内容是不变的,它们在编译时就已经确定了

//example48.cpp
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    cout << __func__ << endl; // main
    cout << __LINE__ << endl; // 6
    cout << __FILE__ << endl; // example48.cpp
    cout << __TIME__ << endl; // 11:44:43
    cout << __DATE__ << endl; // May 15 2022
    return 0;
}

函数匹配

对于函数的调用,对于函数名的不同,编译器当然知道要确切的选择哪一个函数运行,在大多数情况下,确定某次调用应该选用哪个重载函数,当几个函数形参数量相等以及某些形参类型可以由其他类型转换得来时,就没有那么简单了。

void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);//调用的重载形式为 void f(double,double=3.14)

确定候选函数和可行函数

1、寻找候选函数 选定本次调用对应的重载函数集,集合中的函数称为候选函数,有两个特点,与被调用的函数同名与其声明在调用点可见。
2、寻找可行函数 考察本次提供的实参,从候选函数中选出能被这组实参调用的函数,称为可行函数,有两个特点,其形参数量与本次调用提供的实参数量相等,每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
3、寻找最佳匹配 从可行函数中选出本次要调用的函数,逐一比较实参与形参的比较,找出最匹配的可行函数。
4、匹配成功,该函数每个实参的匹配都不劣于其他可行函数需要的匹配、至少每一个实参的匹配优于其他可行函数提供的匹配。最后得到的匹配可能不是唯一的,比如对与两个可行函数的参数匹配个数是相同的,则会冲突。

//example49.cpp
#include <iostream>
using namespace std;

void func(int a, int b)
{
    cout << "int " << a << " " << b << endl;
}

void func(double a, double b)
{
    cout << "double " << a << " " << b << endl;
}

int main(int argc, char **argv)
{
    //有多个 重载函数 "func" 实例与参数列表匹配:
    // func(1, 1.2);
    // func(1.1, 2);
    func(1, 1);     // int 1 1
    func(1.1, 2.2); // double 1.1 2.2
    return 0;
}

实参类型转换

编译器将实参类型到形参类型的转换划分成几个等级,具体排序

1、精确匹配

2、通过 consr 转换实现的匹配
3、通过类型提升实现的匹配
4、通过算术类型转换
5、通过类类型转换实现的匹配

类型提升和算数类型转换的匹配

//example50.cpp
#include <iostream>
using namespace std;

void func(short num)
{
    cout << "short " << num << endl;
}

void func(int num)
{
    cout << "int " << num << endl;
}

void func(long num)
{
    cout << "long " << num << endl;
}

int main(int argc, char **argv)
{
    func('b'); // int 97 char 提升为 int
    func('a'); // int 97 char 提升为 int
    // func(1.14); //冲突 double可转int 或者 long 二义性调用
    return 0;
}

函数匹配和 const 实参

//example51.cpp
#include <iostream>
#include <string>
using namespace std;

void func(const string &str)
{
    cout << "const string " << str << endl;
}

void func(string &str)
{
    cout << "string " << str << endl;
}

int main(int argc, char **argv)
{
    func("hello world"); // const string hello world
    const string str = "hi";
    func(str); // cosnt string hi
    string name = "wanlu";
    func(name); // string wanlu
    return 0;
}
//example52.cpp
#include <iostream>
using namespace std;

void func(const string *ptr)
{
    cout << "cosnt string * " << *ptr << endl;
    ptr = nullptr;
    //*ptr = "hi";//error 存在底层const
}

void func(string *ptr)
{
    cout << "string * " << *ptr << endl;
}

// 此处以第一个func冲突 因为形参会无视顶层const
// void func(string const *ptr)
// {
//     cout << "string const * " << *ptr << endl;
// }

int main(int argc, char **argv)
{
    string str = "hello";
    func(&str); // string * hello
    const string str1 = "hi";
    func(&str1); // const string *hi
    return 0;
}

函数指针

本身是为了解决一种 callback 即回调函数的机制,函数指针指向某种特定的函数类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关

//example53.cpp
#include <iostream>
using namespace std;

float func(int num, float c)
{
    return num * c;
}

int main(int argc, char** argv)
{
    float (*ptr)(int num, float c); //要加括号 否则为ptr函数返回float* 的函数声明
    ptr = &func;
    cout << (*ptr)(1, 2.2) << endl; // 2.2

    //函数指针数组
    float(*ptr_array[3])(int num, float c);
    //ptr_array与[]结合说明是一个数组,然后与*结合说明数组里面存放的是指针
    ptr_array[0] = &func;
    ptr_array[1] = &func;
    ptr_array[2] = &func;
    cout<<(*ptr_array[0])(1,2.2)<<endl;//2.2
    //ptr_array与[0]结合取出第一个函数的地址,解引用得到函数本身然后()调用

    //数组指针
    float (*(* ptr_array_ptr)[3])(int num, float c)=&ptr_array;
    //ptr_array_ptr与*结合代表ptr_array_ptr为一个指针,然后与[]结合说明指针ptr_array_ptr存储数组的地址
    //与*结合表示数组存放的是函数指针
    cout << (*(*ptr_array_ptr)[0])(4,1.1) << endl;//4.4

    return 0;
}

注:不同函数类型的指针不存在转换规则

函数指针数组与函数指针数组指针

请见 example53.cpp 实例,认清*与[]结合的规律。

重载函数指针

因为不同类型的指针不存在转换规则,则想要指向那个函数,则返回值与形参列表要写得一致

//example54.cpp
#include <iostream>
using namespace std;

void func(int num)
{
    cout << "int " << num << endl;
}

void func(double num)
{
    cout << "double " << num << endl;
}

int main(int argc, char **argv)
{
    void (*ptr1)(int num) = &func;
    (*ptr1)(1.2); // int 1
    // void (*ptr2)(double num) = ptr1; //error: 不存在函数指针的转换
    return 0;
}

函数指针形参

向函数传递函数

//example55.cpp
#include <iostream>
using namespace std;

void func(int num)
{
    cout << num << endl;
}

void process(void (*fun)(int num))
{
    cout << "process ";
    fun(666);
}

//看似传递的是一个函数实体,其实并不是,本质上传递与接收的还是函数指针,只不过提供了这样一种写法
void work(void fun(int num))
{
    cout << "work ";
    fun(999);
}

int main(int argc, char **argv)
{
    process(&func); // process 666
    work(func);     // work 999
    process(func);  // process 666
    return 0;
}

typedef、auto、decltype 在函数指针的应用

//example56.cpp
#include <iostream>
using namespace std;

void func(int num);

typedef void SHOWNUM(int num);
typedef decltype(func) SHOWNUM_DECLTYPE;
typedef void (*SHOWNUM_PTR)(int num);
typedef decltype(func) *SHOWNUM_DECLTYPE_PTR;

void func(int num)
{
    cout << "func " << num << endl;
}

int main(int argc, char **argv)
{
    SHOWNUM *ptr = &func;
    (*ptr)(666); // func666
    SHOWNUM_DECLTYPE *ptr1 = ptr;
    (*ptr1)(999); // func 999
    SHOWNUM_DECLTYPE_PTR ptr2 = ptr1;
    SHOWNUM_PTR ptr3 = ptr2;
    (*ptr2)(999); // func 999
    (*ptr3)(777); // func 777
    return 0;
}
//example57.cpp
#include <iostream>
using namespace std;

void func(int num);

decltype(func) *get_func()
{
    return &func;
}

void func(int num)
{
    cout << "func " << num << endl;
}

int main(int argc, char **argv)
{
    auto ptr = get_func();
    (*ptr)(666); // func 666
    return 0;
}

返回指向函数的指针

//example58.cpp
#include <iostream>
using namespace std;

void func(int num);
using F_decl = decltype(func);
using F_PTR_decl = decltype(func) *;
using F = void(int);
using F_PTR = void (*)(int);

F *get_func()
{
    return func;
}

auto get_func1() -> F_PTR_decl
{
    return &func;
}

void func(int num)
{
    cout << "func " << num << endl;
}

int main(int argc, char **argv)
{
    F_PTR_decl ptr1 = get_func1();
    F_PTR ptr2 = get_func();
    (*ptr1)(666); // func 666
    (*ptr2)(999); // func 999
    return 0;
}

啊!东西这么多,别怕我们要循序渐进,然后在以后的日子里多回顾多复习,多实践。