阅读 250

<<从0到1学C++>> 第4篇 函数和函数模板

本篇要学习的内容和知识结构概览


函数的参数及其传递方式

1. 函数参数传递方式

传值: 

    传变量值: 将实参内存中的内容拷贝一份给形参, 两者是不同的两块内存

    传地址值: 将实参所对应的内存空间的地址值给形参, 形参是一个指针, 指向实参所对应的内存空间

传引用:

    形参是对实参的引用, 形参和实参是同一块内存空间

2. 对象作为函数参数, 也就是传变量值

将实参对象的值传递给形参对象, 形参是实参的备份, 当在函数中改变形参的值时, 改变的是这个备份中的值, 不影响原来的值

像这样:

void fakeSwapAB(int x , int y) {
    int temp = x;
    x = y;
    y = temp;
}

int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;

// 传变量值
fakeSwapAB(a, b);

cout << "交换后: " << a << ", " << b << endl;
复制代码

3. 对象指针作为函数参数, 也就是传地址值

形参是对象指针, 实参是对象的地址值, 虽然参数传递方式仍然是传值方式, 因为形参和实参的地址值一样, 所以它们都指向同一块内存, 我们通过指针更改所指向的内存中的内容, 所以当在函数中通过形参改变内存中的值时, 改变的就是原来实参的值

像这样:

void realSwapAB(int * p, int * q) {
    int temp = *p;
    *p = *q;
    *q = temp;
}

int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;

// 传地址值
realSwapAB(&a, &b);

cout << "交换后: " << a << ", " << b << endl;
复制代码

对于数组, 因数组名就是代表的数组首地址, 所以数组也能用传数组地址值的方式

void swapArrFirstAndSecond(int a[]) {
    int temp = a[0];
    a[0] = a[1];
    a[1] = temp;
}

int main(int argc, const char * argv[]) {
    
    int a[] = {2, 3};
    cout << "交换前: " << a[0] << ", " << a[1] << endl;
    swapArrFirstAndSecond(a);
    cout << "交换后: " << a[0] << ", " << a[1] << endl;
    return 0;
}
复制代码

4. 引用作为函数参数, 也就是传地址(注意: 这里不是地址值)

在函数调用时, 实参对象名传给形参对象名, 形参对象名就成为实参对象名的别名. 实参对象和形参对象代表同一个对象, 所以改变形参对象的值就是改变实参对象的值

像这样:

void citeSwapAB(int & x, int & y) {
    int temp = x;
    x = y;
    y = temp;
}

int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;

// 传引用
citeSwapAB(a, b);

cout << "交换后: " << a << ", " << b << endl;
复制代码
优点: 引用对象不是一个独立的对象, 不单独占内存单元, 而对象指针要另外开辟内存单元(内存中放实参传过来的地址), 所以传引用比传指针更好用.

5. 默认参数 

不要求程序在调用时必须设定该参数, 而由编译器在需要时给该参数赋默认值. 

规则1. 当程序需要传递特定值时需要显式的指明. 默认参数必须在函数原型中说明.

如果函数在main函数后面定义, 而在声明中设置默认参数, 在定义中不需要设置默认参数

像这样:

// 在main函数前声明函数, 并设置默认参数
void PrintValue(int a, int b = 0, int c = 0);

int main(int argc, const char * argv[]) {
    
    // 调用函数
    PrintValue(5);
    
    return 0;
}

// 在main函数后定义函数, 不需要设置默认参数
void PrintValue(int a, int b, int c) {
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}
复制代码

如果函数在main函数前面定义, 则在定义中设置默认参数

像这样:

// 在main前定义函数, 需要设置默认参数
void PrintValue(int a, int b = 0, int c = 0) {
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

int main(int argc, const char * argv[]) {
    
    // 调用函数
    PrintValue(5);
    
    return 0;
} 复制代码
规则2: 默认参数可以多于一个, 但必须放在参数序列的后部.

像这样:

可以有一个默认参数:

void PrintValue(int a, int b, int c = 0);
复制代码

可以是有多个默认参数:

void PrintValue(int a, int b = 0, int c = 0);
复制代码

不可以在中间设置默认参数:

void PrintValue(int a, int b = 0, int c);
复制代码
规则3. 如果一个默认参数需要指定一个特定值时, 则在此之前的所有参数都必须赋值

// 调用函数 第一种: 三个参数全部有特定值
PrintValue(5, 8, 9);

// 调用函数 第二种: 我们给第二个参数设特定值, 它前面所有参数必须赋值, 所以可以
PrintValue(5, 8);

/*
调用函数 第三种: 当一个默认参数有特定值时, 它前面所有的参数都必须赋值,
我们给第三个默认参数设特定值 也就是说第一, 二个参数也必须赋值 所以不可以
 */
//    PrintValue(5, , 9);
复制代码

6. 使用const保护数据

用const修饰要传递的参数, 该函数只能使用参数, 而无权修改参数, 以提高系统的自身安全.

像这样:

// 拼接字符串的函数
void catStr(const string str) {
    string str2 = str + " Ray!";
    
    // 函数内部不能修改const修饰的形参, 所以不能这么使用
//    str = "Hi";
    cout << str2 << endl;
}

int main(int argc, const char * argv[]) {
    
    // 实例化一个字符串
    string str = "Hello";
    
    // 调用函数
    catStr(str);
    
    return 0;
}
复制代码

函数返回值

C++函数返回值类型可以是除数组和函数以外的任何类型

当返回值是指针或引用对象时, 需要注意函数返回值所指的对象必须存在, 因此不能将函数内部的局部对象作为函数返回值, 因为函数内, 局部变量或者对象在函数运行完毕后内存就释放啦

1. 返回引用的函数

函数可以返回一个引用, 目的是为了让该函数位于赋值运算符的左边

格式: 数据类型 & 函数名(参数列表);

像这样:

// 全局数组
int arr[] = {2, 4, 6, 8};

// 获得数组下标元素
int & getValueAtIndex(int i) {
    return arr[i];
}

int main(int argc, const char * argv[]) {
    
    cout << "更改前: " << arr[2] << endl;
    
    // 调用函数, 并且用于计算或者重新赋值
    getValueAtIndex(2) = 10;
    cout << "更改后: " << arr[2] << endl;
    
    return 0;
}
复制代码

2. 返回指针的函数

返回值是存储某种数据类型数据的内存地址, 这种函数称为指针函数

格式: 数据类型 * 函数名(参数列表);

像这样:

// 返回指针的函数
int * getData(int n) {
    
    // 根据形参, 申请内存空间
    int * p = new int[n];
    
    // 给申请下来的内存空间赋值
    for (int i = 0; i < n; i++) {
        p[i] = i + 10;
    }
    
    // 返回这段内存空间的首地址
    return p;
}

int main(int argc, const char * argv[]) {
    
    // 调用函数, 并接收返回值, 不要忘记释放函数中分配的内存
    int * p = getData(5);
    
    // 打印指针所指向的内存中的内容
    for (int i = 0; i < 5; i++) {
        cout << p[i] << endl;
    }
    
    return 0;
}
复制代码

3. 返回对象的函数

格式: 数据类型 函数名(参数列表);

像这样:

// 返回对象的函数
string sayHello(string s) {
    // 我们拼接好一个字符串, 给str
    string str = "Hello " + s;
    
    // 并把str这个对象返回
    return str;
}

int main(int argc, const char * argv[]) {
    
    // 调用函数, 接收函数返回的对象
    string str = sayHello("Ray");
    cout << str << endl;
    
    return 0;
}
复制代码

4. 函数返回值作为函数参数

如果函数返回值作为另一个函数的参数, 那么这个返回值必须与另一个函数的参数类型一致

像这样:

// 求最大值的函数
int getMax(int x, int y) {
    return x > y ? x : y;
}

int main(int argc, const char * argv[]) {
    
    // 先求8, 9返回最大值; 返回值再跟5比较, 返回最大值
    int maxValue = getMax(5, getMax(8, 9));
    cout << maxValue << endl;
    
    return 0;
}
复制代码

内联函数

1. 内联函数的概念

使用关键字inline声明的函数称为内联函数, 内联函数必须在程序中第一次调用此函数的语句出现之前定义, 这样编译器才知道内联函数的函数休, 然后进行替换

像这样:

// 判断输入的字符是否为数字
inline bool isNumber(char c) {
    if (c >= '0' && c <= '9') {
        return true;
    } else {
        return false;
    }
}

int main(int argc, const char * argv[]) {
    
    // 声明字符c
    char c;
    
    // 从键盘输入字符
    cin >> c;
    
    // 进行判断, 这里的isNumber(c), 在程序编程期间就会被isNumber()函数体所替换, 跟宏一样一样的
    // 如果函数体特别大, 替换的地方特别多, 就增加了代码量
    if (isNumber(c)) {
        cout << "输入了一个数字" << endl;
    } else {
        cout << "输入的不是一个数字" << endl;
    }
    
    return 0;
}
复制代码

2. 注意: 

在C++中, 除具有循环语句, switch语句的函数不能说明为内联函数外, 其它函数都可以说明为内联函数.

3. 作用: 

使用内联函数可以提高程序执行速度, 但如果函数体语句多, 则会增加程序代码量.

函数重载和默认参数

1. 函数重载

一个函数名具有多种功能, 具有多种形态, 称这种我为多态性, 一个名字, 多个函数

函数重载要满足的条件:

参数类型不同或者参数个数不同

像这样:

// 求和的函数 2两个整型参数
int sumWithValue(int x, int y) {
    return x + y;
}

// 求和的函数 3两个整型参数
int sumWithValue(int x, int y, int z) {
    return x + y + z;
}

// 求和的函数 2个浮点型参数
double sumWithValue(double x, double y) {
    return x + y;
}

// 求和的函数 3个浮点型参数
double sumWithValue(double x, double y, double z) {
    return x + y + z;
}

int main(int argc, const char * argv[]) {
    
    // 两个整型变量求和
    int sumValue1 = sumWithValue(8, 9);
    
    // 三个整型变量求和
    int sumValue2 = sumWithValue(8, 9, 10);
    
    // 两个浮点型变量求和
    double sumValue3 = sumWithValue(1.2, 2.3);
    
    // 三个浮点型变量求和
    double sumValue4 = sumWithValue(1.2, 2.3, 3.4);
    
    cout << sumValue1 << endl;
    cout << sumValue2 << endl;
    cout << sumValue3 << endl;
    cout << sumValue4 << endl;
    
    return 0;
}
复制代码

2. 函数重载与默认参数

当函数重载与默认参数相结合时, 能够有效减少函数个数及形态, 缩减代码规模.

这样我们每种数据类型只保留一个函数即可完成我们的功能, 直接少了两个函数.

像这样:

// 整型参数求和
int sumWithValue(int x = 0, int y = 0, int z = 0) {
    return x + y + z;
}

// 浮点型参数求和
double sumWithValue(double x = 0, double y = 0, double z = 0) {
    return x + y + z;
}

int main(int argc, const char * argv[]) {
    
    // 两个整型变量求和
    int sumValue1 = sumWithValue(8, 9);
    
    // 三个整型变量求和
    int sumValue2 = sumWithValue(8, 9, 10);
    
    // 两个浮点型变量求和
    double sumValue3 = sumWithValue(1.2, 2.3);
    
    // 三个浮点型变量求和
    double sumValue4 = sumWithValue(1.2, 2.3, 3.4);
    
    cout << sumValue1 << endl;
    cout << sumValue2 << endl;
    cout << sumValue3 << endl;
    cout << sumValue4 << endl;
    
    return 0;
}复制代码

如果使用默认参数, 就不能对参数个数少于默认个数的函数形态进行重载, 只能对于多于默认参数个数的函数形态进行重载.

像这样

// 求和的参数, 并且使用默认参数, 最多三个整型参数求和
int sumWithValue(int x = 0, int y = 0, int z = 0) {
    return x + y + z;
}

// 像这样是不行的, 不能对参数个数少于默认个数的函数形态进行重载
//int sumWithValue(int x, int y) {
//    return x + y;
//}

// 像这样是可以的, 当调用时传入4个整型参数时就会调用该参数
int sumWithValue(int x, int y, int z, int t) {
    return x + y + z + t;
}

int main(int argc, const char * argv[]) {
    
    // 求和, 只给两个特定值
    int sumValue1 = sumWithValue(8, 9);
    
    // 求和, 给三个特定值
    int sumValue2 = sumWithValue(8, 9, 10);
    
    // 求和, 有4个整型参数
    int sumValue3 = sumWithValue(8, 9, 10, 11);
    
    cout << sumValue1 << endl;
    cout << sumValue2 << endl;
    cout << sumValue3 << endl;
    
    return 0;
}
复制代码

函数模板

从而上面可以看出, 它们是逻辑功能完全一样的函数, 所提供的函数体也一样, 区别仅仅是数据类型不同, 为了统一的处理它们, 引入了函数模板. 

现在我们的函数从4个缩减成一个, 但是我们的功能没有减少, 反而增加了. 比如我们可以计算char, float类型

1. 什么是函数模板

在程序设计时没有使用实际存在的类型, 而是使用虚拟的参数参数, 故其灵活性得到加强.

当用实际的类型来实例化这种函数时, 就好像按照模板来制造新的函数一样, 所以称为函数模板

格式: 一般用T来标识类型参数, 也可以用其它的

Template <class T> 

像这样:

// 定义模板
template <class T>

// 定义函数模板
T sumWithValue(T x, T y) {
    return  x + y;
}

int main(int argc, const char * argv[]) {
    
    // 调用模板函数
    int sumValue1 = sumWithValue(3, 5);
    
    // 调用模板函数
    double sumValue2 = sumWithValue(3.2, 5.1);
    cout << sumValue1 << endl;
    cout << sumValue2 << endl;
    return 0;
}
复制代码

当用用函数模板与具体的数据类型连用时, 就产生了模板函数, 又称为函数模板实例化

2. 函数模板的参数

函数模板名<模板参数>(参数列表);

我们可以将参数列表的数据强制转换为指定的数据类型

像这样

int sumValue2 = sumWithValue<int>(3.2, 5.1);
复制代码

我们将参数列表里的数据强制转换为int类型, 再参与计算

也可以样:

double sumValue2 = sumWithValue(3.2, (double)5);
复制代码

我们也可以将参数列表里的单个参数进行强制类型转换, 再参与计算

不过我们一般不会加上模板参数.

3. 使用关键字typename

用途就是代替template参数列表中的关键字class

像这样

template <typename T>

只是将class替换为typename, 其它一样使用.

强烈建议大家使用typename, 因为它就是为模板服务的, 而class是在typename出现之前使用的, 它还有定义类的作用, 不直观, 也会在一些其它地方编译时报错.

总结: 

可能对于初学者来说, 函数有点不是很好理解, 包括我当初也是, 不要想得过于复杂, 其实它就是一段有特定功能的代码, 只不过我们给这段代码起了个名字而已, 这样就会提高代码的可读性和易维护性.

本系列文章会持续更新! 大家踊跃的留下自己的脚印吧!

👣👣👣👣👣👣👣👣



关注下面的标签,发现更多相似文章
评论