这里只谈特殊的函数,普通函数、递归函数、成员函数就都省略了。

内联函数 链接到标题

又称内置函数。

inline int fun(int a, int b);

这种函数是一种用空间换时间思想的体现,函数调用时是需要将一些变量和运行的位置进行储存的,而这些步骤是需要时间的,那么在满足一下两个条件时我们就可以选择使用内联函数了:

  1. 该函数调用十分频繁;
  2. 函数内的逻辑语句十分简洁。

满足以上两条的函数每一次调用所需的时间都远远长于内部流程运行时间,所以如果在调用函数的地方将函数语句替换上去的话,就能大大缩短程序运行时间了,但作为代价,编译后的代码的空间会比非内联函数版本大一些。

修饰词 inline 在函数定义或是声明时声明都可以产生一样的效果。

inline只是建议性的,如果编译系统判断这里不适合用内联函数的话它是可以忽略该修饰词的。

重载函数 链接到标题

int fun(int a, int b);
int fun(float a, float b);

这样两个函数构成了重载,因为它们满足了一下三个要求:

  1. 函数名一致
  2. 参数列表中的参数个数或是类型不同
  3. 函数在同一个类中

函数的返回值类型与是否构成重载无关

重载函数的用处就是把一些参数不同的函数整合起来,相似功能只需要记住一个方法名就可以了。

重写函数 链接到标题

这里

带默认参数的函数 链接到标题

int fun(int a, int b = 1);

这个是很容易理解的,如果在调用函数时传入两个参数值,如:fun(1, 1),这样就和普通的函数没任何区别;但带默认参数后这个函数就可以允许这样传参数了:fun(1),此时的参数b就是给定的默认值1了。

一些要求 链接到标题

带默认值的参数必须在参数列表的最右边且连续,在调用的时候也只能省略最右边且连续的参数。

我们必须在重载带默认参数的函数时避免产生不唯一的调用

如:

int fun(int a, int b = 1);
int fun(int a);

此时调用 fun(1),编译器就无法确认到底调用的是哪个函数了,这时它就只能报错了。

构造函数 链接到标题

className(...);

构造函数是没有函数类型的(和Java不同),也不会有返回值。

因为类不能在声明中对数据成员进行初始化,所以就需要构造函数来在定义对象时初始化对象(有一种特殊情况,会调用拷贝构造函数来初始化对象,这个我们下文会提到)。

编译器分配了默认的空白构造函数,简化的类可以偷懒直接不写构造函数。

构造函数与其他成员函数不同的是它不需要用户来调用它,它会在建立对象时自动执行。

当然,C++ 也允许程序员手写构造函数,甚至是写多个构造函数构成重载。

拷贝构造函数 链接到标题

当定义对象时括号里传入的参数为同类对象时,就会调用拷贝构造函数了。

编译器也同样会分配一个默认很简陋的拷贝构造函数(浅拷贝),仅仅就是将参数的成员变量的值赋给了需要初始化对象的对应的成员变量。

大部分情况下系统分配的默认拷贝构造函数已经够用了,但如果类中存在指针成员变量,在释放对应空间时就会遇到重复释放同一片空间的错误,最终导致程序崩溃。此时我们就需要手写一个拷贝构造函数了(深拷贝)。

例如:

class A {
    int *n;
public:
    Complex(const A &a) {
        n = new int;
        *n = *(a.n);
    };
}

调用拷贝构造函数的情况 链接到标题

程序中需要新建立一个对象时 链接到标题

这个就是很基础的情况了

当函数的参数为类的对象时 链接到标题

void fun(A a) {
}

int main() {
    A b;
    fun(b);
    return 0;
}

此时我们可以认为在调用fun函数时执行了一句 A a(b),即调用了一次拷贝构造函数。

函数的返回值是类的对象时 链接到标题

A fun() {
    A b;
    return b;
}

int main() {
    A a;
    a = fun();
    return 0;
}

此时我们可以认为在fun函数结束时执行了一句 A a(b),即调用了一次拷贝构造函数。

此时返回给 a 的对象是一个临时无名对象,具有 const 修饰符,这在运算符重载时带来了很多问题。

其他情况 链接到标题

  • 抛出一个异常对象;
  • 捕捉一个异常对象;
  • 对象放在大括号中,即{ }。

复制省略 链接到标题

但存在一种叫做 “复制省略”(copy elision)的编译优化技术,会在临时对象用于初始化同类型对象时跳过拷贝构造函数直接初始化(但拷贝构造函数依旧需要是可访问的)。

上文 函数的返回值是类的对象时 的代码就被GCC编译器以这种形式优化了,所以有时看起来跟没调用拷贝构造函数一样。

#include <iostream>
class C {
public:
	C() = default;
	C(const C&) {
        std::cout << "A copy was made.\n"; 
    }
};
C f() {
	C c;
	return c;
}
int main() {
    std::cout << "Hello World!\n";
    C obj(f()), c, d = c;
}

这段代码运行时 GCC 编译器也优化了所有调用拷贝构造函数的地方。

特性 链接到标题

约定 链接到标题

class Complex {
    int real, imag;
public:
    Complex(...);
}

特殊写法 链接到标题

构造函数是可以用参数初始化表来实现对成员函数的初始化的,格式如下:

Complex::Complex (int real, int imag) : this.real(real), this.imag(imag) {};

格式转换 链接到标题

只有一个内置类型参数的构造函数是具有类型转换功能的,例如:

Complex::Complex (int real) {
    this.real = real;
};

Complex c1;
c1 = 9;

这里c1 = 9语句会被编译器识别为c1 = Complex(9),等式右边的Complex即实现了类型转换(和 int(9.12) 一样)。

此时其实是创建了一个临时无名变量,会调用一次构造函数,然后调用一次拷贝构造函数,最后调用析构函数析构临时无名变量

显式格式转换 链接到标题

explicit Complex::Complex (int real) {
    this.real = real;
};

此时我们就不能再向上文调用隐式格式转换那样写了,我们必须要光明正大的调用格式转换函数了,例如:

Complex c1;
c1 = 9; // error

c1 = Complex(p); // OK

对象数组 链接到标题

在定义无论静态动态的对象数组时都会调用n次构造函数,n 即为数组的长度。

但在定义对象指针时并不会调用构造函数,直到为其分配空间。

特例

//A类有无参、一参、二参三个构造函数
A *a[3] = {new A(1), new A(2, 3)};

此时 a[0]a[1] 是指向一个对象的指针,a[2] 是一个空指针。

析构函数 链接到标题

在一个对象的生命末期,系统会调用析构函数来析构该对象,然后删除该对象,大部分情况下我们都会选择调用系统分配的空白默认析构函数。

但当我们给对象里的成员指针变量分配动态空间后,空白默认析构函数便不够用了,此时就需要我们手写一个析构函数来 delete 对应的成员指针变量了。

析构函数没有函数类型,也没有函数参数,也不能被重载,一个类就只能有一个析构函数。

delete动态对象数组 链接到标题

此时会调用 n 次析构函数,n 即为数组长度。

构造函数和析构函数的顺序 链接到标题

首先我们要清楚全局变量的构造时间,C 语言中它会在调用 main 函数时构造,C++ 语言中它会在调用 main 函数前构造。

然后我们需要清楚静态局部对象在它的作用域结束时并不会析构。

最后我们要明白在同一个作用域内的对象是按栈的顺序析构的。

所以,在程序运行后:

  1. 全局对象构造
  2. main 函数内的局部对象按顺序一个个构造
  3. 传参的无名临时对象构造,拷贝,析构
  4. 其他函数内的局部对象按顺序一个个构造
  5. 在其他函数结束时非静态局部对象按逆序一个个析构
  6. 传返回值的无名临时对象构造,拷贝,析构
  7. 重复 3-6 直到 main 函数结束
  8. main 函数内的非静态局部对象按逆序一个个析构
  9. 静态变量按逆序一个个析构
  10. 局部变量按逆序一个个析构

回调函数 链接到标题

回调函数是一类参数列表中有函数指针变量的函数,它可以用来按固定逻辑调用传入函数,例如:

void a(int n, int (*ptr)()) {
    cout << n + ptr();
}

int b() {
    return rand();
}

int main() {
    int n = 1;
    a(n, b);
    return 0;
}