C++:函数

函数参数的传递机制

C++的每个程序至少有一个函数,即主函数main() ,函数也是类的方法的实现手段。C++的函数包括两类:预定于函数和用户自定义函数

函数的定义格式为:

<返回值类型><函数名>(<参数列表>)

<函数体>

其中,<返回值类型>实值函数返回值的数据类型,可以是任意一种基本数据类型或用户自定义数据类型,以及类类型。<函数名>是给函数指定的名字,函数名应遵循标识符的命名规定。<参数列表>是指参数的个数、名称和类型,函数定义中的参数成为形参。<函数体>实值函数完成的功能,由说明语句和执行语句组成。

在C++语言中,函数的参数传递有三种方式:按变量值传递、按变量的地址传递和引用传递。C++语言可以用简单变量作为参数进行传递,也可以将对象作为参数传递给函数,其方法与传递其他类型的数据一样。

使用对象作为函数参数

对象作为函数参数时,其参数传递机制与其他类型的数据作为函数参数相同。在进行函数调用时,可将实参对象的值复制一份给对应的形参对象。因此,作为形参的对象,其数据的改变并不会影响实参对象。

实例

cpp 复制代码
#include "iostream"
using namespace std;
class Point{
public:
    Point(int a, int b):x(a),y(b){}
    void Move(Point p){
        p.x = p.x + 1;
        p.y = p.y + 1;
    }
    void Print(){
        cout << "x:" << x << ", y:" << y << endl;
    }
private:
    int x, y;
};

int main(){
    Point p1(1, 2);
    cout << "before move:";
    p1.Print();
    p1.Move(p1);
    cout << "after  move:";
    p1.Print();
    return 0;
}

运行结果:

cpp 复制代码
before move:x:1, y:2
after  move:x:1, y:2

使用对象指针作为函数参数

在进行函数调用时,以对象指针作为参数,其实质是传地址调用,即参数与实参共享同一内存单元。因此,作为形参的对象,其数据的改变将影响实参对象,从而实现函数之间的信息传递。此外,使用对象指针作为参数仅将对象的地址传递给形参,而不进行副本的复制,可以提高运行效率,减少时空开销。

实例

cpp 复制代码
#include "iostream"
using namespace std;
class Point{
public:
    Point(int a, int b):x(a),y(b){}
    void Move(Point *p){
        (*p).x = (*p).x + 1;//也可以写成p->x=p->x + 1
        (*p).y = (*p).y + 1;
    }
    void Print(){
        cout << "x:" << x << ", y:" << y << endl;
    }
private:
    int x, y;
};

int main(){
    Point p1(1, 2);
    cout << "before move:";
    p1.Print();
    p1.Move(&p1);
    cout << "after  move:";
    p1.Print();
    return 0;
}

运行结果

cpp 复制代码
before move:x:1, y:2
after  move:x:1, y:2

使用对象引用作为函数参数

用对象引用作为函数参数不但具有用对象指针作为函数参数的优点,而且相比之下,用对象引用作为函数参数可更简单、更直接。

实例

cpp 复制代码
#include "iostream"
using namespace std;
class Point{
public:
    Point(int a, int b):x(a),y(b){}
    void Move(Point &p){
        p.x = p.x + 1;
        p.y = p.y + 1;
    }
    void Print(){
        cout << "x:" << x << ", y:" << y << endl;
    }
private:
    int x, y;
};

int main(){
    Point p1(1, 2);
    cout << "before move:";
    p1.Print();
    p1.Move(p1);
    cout << "after  move:";
    p1.Print();
    return 0;
}

运行结果

cpp 复制代码
before move:x:1, y:2
after  move:x:1, y:2

三种传递方式比较

  1. 适用对象作为函数参数。当进行函数调用时,需要给形参分配存储单元,形参和实参的结合是值传递,实参将自己的值传递给形参,形参实际上是实参的副本。这是一种单向传递,形参的变化不会影响到实参。
  2. 使用指针作为函数参数。当进行函数调用时,需要给形参分配存储单元,形参和实参的结合是地址传递,实参将自己的地址传递给形参。这是一种双向传递,形参的变化会直接影响带实参。缺点是阅读性较差。
  3. 使用引用作为函数参数。当进行函数调用时,在内存中并没有产生实参的副本,它是直接对实参操作。这种方式是双向传递,形参的变化会直接影响到实参。与指针作为函数参数相比,它更容易使用、更清晰。当参数传递的数据较大时,用引用比一般变量传递参数的效率要高,所占用空间更少。

内联函数

在程序设计中,效率是一个很重要的指标。在C语言中,保护效率的一个方法是使用宏(Macro)。宏可以不用函数调用,但看起来像函数调用。宏是用预处理器来实现的。预处理器直接用宏代码代替宏调用,因此就不需要函数调用所需的保存调用时的现场状态和返回地址、进行参数传递等的时间花费。然而C++的预处理器不允许存取私有数据,这意味着预处理器宏在用作成员函数时变得非常无用。为了既保持预处理器宏的效率又增加安全性,而且还能像一般成员函数一样可以在类里访问自如,C++引入了内联函数(Inline Function)。内联函数时一个函数,它与一般函数的区别是,在使用时可以像宏一样展开,所以没有函数调用的开销。因此,使用内联函数可以提高系统的执行效率。但在内联函数体中,不能含有复杂的结构控制语句,如语句switch和while等。内联函数实际是一种空间换时间的方案,因此其缺点是增大了系统空间方面的开销。在类内给出函数体定义的成员函数被默认为内联函数。

内联函数的定义格式为:inline 返回值类型 函数名(形式参数表){//函数体}

例如:大小写字母转换

cpp 复制代码
#include "iostream"
using namespace std;
inline char Trans(char ch){
    if(ch >= 'a' && ch <= 'z') return ch - 32;
    else return ch + 32;
}

int main(){
    char ch;
    while((ch = getchar()) != '\n')
        cout << Trans(ch);
    cout << endl;
    return 0;
}

【注】

  1. 内联函数代码不宜太长,一般是1~5行代码,而且不能含有复杂的分支或循环语句等。
  2. 在类内定义的成员函数默认为内联函数。
  3. 在类外给出函数体的成员函数,若要定义为内联函数,则必须加上关键字inline。否则,编译器将它作为普通成员函数。
  4. 递归调用的函数不能定义为内联函数。

函数重载

函数名可以看作一个操作的名字。当多个函数实现的是同一类功能,只是部分细节不同(如参数的个数或参数类型不同)时,C++提供了函数重载机制,即将这些函数取成相同的名字,从而时程序已于阅读和理解,方便记忆和使用。

函数重载实质两个或两个以上的函数具有相同的函数名,但其参数类型不一致或参数个数不同。函数重载包括非成员函数的重载和成员函数重载。

非成员函数重载

非成员函数重载是指对用户所编写的那些功能相同或类似、参数个数或类型不同的用户自定义函数,C++可以采用相同的函数名,从而提高程序的可读性。支持函数重载时C++多态性的体现之一。

例如:求两个数的积

cpp 复制代码
#include "iostream"
using namespace std;
int Mul(int x, int y){
    return x * y;
}
double Mul(double x, double y){
    return x * y;
}

int main(){
    int x, y;
    double a, b;
    cout << "input x, y:";
    cin >> x >> y;
    cout << "x*y=" << Mul(x, y) << endl;
    cout << "input a, b:";
    cin >> a >> b;
    cout << "a*b=" << Mul(a, b) << endl;
    return 0;
}

【注】

  1. 重载函数必须具有不同的参数个数或不同的参数类型,若只是返回值的类型不同或参数名不同时不行的。

  2. 重载函数应满足:函数名相同,函数的返回值类型可以相同也可以不同,当各函数的参数表中的参数个数或类型必须有所不同,这样才能进行区分,从而正确地调用函数。

  3. 匹配重载函数的顺序:a.通过内部类型转换寻求一个匹配,若找到,则调用该函数;b.通过强制类型转换寻求一个匹配,若找到,则调用函数。

  4. 不能将不同功能的函数定义为重载函数,以免产生误解。

  5. 创建重载函数时,必须让编译器能区分两个(或更多)的重载函数。当创建的多个重载函数编译器不能区分时,编译器就认为这些函数具有多义性,即调用是错误的,编译器不会编译该程序。例如:

    cpp 复制代码
    #include "iostream"
    using namespace std;
    float Mul(float x){
        return 2 * x;
    }
    double Mul(double x){
        return 2 * x;
    }
    
    int main(){
        cout << Mul(10.2) << endl;
        cout << Mul(10) << endl;//错误。产生二义性
        return 0;
    }

    报错

    cpp 复制代码
    error: call of overloaded 'Mul(int)' is ambiguous
      400 |     cout << Mul(10) << endl;//错误。产生二义性
    |             ~~~^~~~
     note: candidate: 'float Mul(float)'
      391 | float Mul(float x){
          |       ^~~
     note: candidate: 'double Mul(double)'
      394 | double Mul(double x){
          |        ^~~
  6. 当函数的重载带有默认参数值时,要避免产生二义性。

成员函数重载

成员函数重载主要是为了适应相同成员函数的参数多样性。成员函数重载的一个很重要的应用就是重载构造函数。因为构造函数的名字预先有类的名字确定,所以只能有一个构造函数名。但在解决实际问题时,可能会需要创建具有不同形态的对象,因此,C++提供了函数重载机制。通过对构造函数进行重载,可以实现定义对象时初始化赋值的多样性。但是析构函数不能重载,因为一个类中只允许有且仅有一个析构函数。

例如:求了两个复数的和

cpp 复制代码
#include "iostream"
using namespace std;
class Complex{
public:
    Complex(double r, double i):real(r),imag(i){}
    Complex(double r):real(r),imag(0){}
    Complex():real(0),imag(0){}
    Complex Add(Complex a){
        Complex temp;
        temp.real = real + a.real;
        temp.imag = imag + a.imag;
        return temp;
    }
    void Print(){
        cout << real;
        if(imag > 0) cout << "+";
        if(imag != 0) cout << imag << "i" << endl;
    }
private:
    double real, imag;
};

int main(){
    Complex com1(1.1, 2.2), com2(3.3, 4.4), com3(5.5), total;
    total = com1.Add(com2);
    total.Print();
    total = com1.Add(com3);
    total.Print();
    return 0;
}

运行结果:

cpp 复制代码
4.4+6.6i
6.6+2.2i

函数的默认参数值

  1. 当函数既有声明和定义时,默认参数值只能在声明中指定,不能再函数定义中指定。

    cpp 复制代码
    void Fun(int x = 0, int y = 0);//正确,在函数声明中指定默认参数值
    void Fun(int x = 0, int y = 0){ // ....} //错误,不能再函数定义中指定默认参数值
  2. 在函数声明中,所有带默认值的参数都必须出现再没有默认值参数的右边。也就是说,一旦开始定义带默认值的参数,在其后面就不能再定义没有默认值的参数了。例如:

    cpp 复制代码
    void Fun(int i, int j = 0, int k); //错误
  3. 在函数调用时,若某个参数省略,则其后面的参数皆应省略而采用默认参数值。例如:

    cpp 复制代码
    Fun(,9);//错误的调用
  4. 当重载函数带有默认参数值时,要注意避免二义性。

友元

类的主要特点之一是数据隐藏,类的私有成员或保护成员只能通过其成员函数来访问。那么有没有办法允许再类外对某个对象的私有成员或保护成员进行操作?C++中提供了友元机制来解决这个问题。

友元函数

友元函数不是当前类中的成员函数,它不仅可以是一个不属于任何一个类的一般函数(非成员函数),也可以是另外一个类的成员函数。当函数被声明为一个类的友元函数后,它就可以通过对象名访问类的私有成员和保护成员。

非成员函数作为友元函数

非成员函数作为类的友元函数后,就可以通过对象访问封装再类内部的数据。声明友元函数的方法是,在类的定义中用关键字friend进行声明,格式为:friend 函数返回值 函数名(形参表);

例如:使用友元函数将百分制学生的成绩转换成相应的分数等级。

cpp 复制代码
#include "iostream"
#include "iomanip"
#include "cstring"

using namespace std;
class Student{
public:
    Student(char na[], int sco){
        strcpy_s(name, strlen(na) + 1, na);
        score = sco;
    }
    friend void Trans(Student *s){
        if (s->score >= 90) strcpy_s(s->level, strlen("优") + 1, "优");
        else if(s->score >= 80) strcpy_s(s->level, strlen("良") + 1,"良");
        else if(s->score >= 70) strcpy_s(s->level, strlen("中") + 1,"中");
        else if(s->score >= 60) strcpy_s(s->level, strlen("及格") + 1,"及格");
        else  strcpy_s(s->level, strlen("不及格") + 1,"不及格");
    }
    void Print(){
        cout << name << setw(10) << score << setw(8) << level << endl;
    }
private:
    char name[10];
    int score;
    char level[7];
};

int main(){
    Student stu[] = {Student("李明", 76), Student("李华", 82), Student("王明", 96), Student("星星", 52)};
    cout << "输出结果:" << endl;
    cout << "姓名" << setw(10) << "成绩" << setw(8) << "等级" << endl;
    for(int i = 0; i< 4; i++){
        Trans(&stu[i]);
        stu[i].Print();
    }
    return 0;
}

运行结果

cpp 复制代码
输出结果:
姓名      成绩    等级
李明        76      中
李华        82      良
王明        96      优
星星        52  不及格

【注】

  1. 非成员函数成为类的友元函数后,若再类外定义,则不能再函数名前加"类名::",因为他不是该类的成员函数;非成员函数作为类的友元函数没有this指针;调用友元函数时必须在其实参标中给出要访问的对象。

  2. 友元函数既可以在类的私有部分进行声明,也可以再类的公有部分进行声明。

  3. 当一个函数需要访问多个类时,应该把这个函数同时定义为这些类的友元函数,这样,该函数才能访问这些类的私有或保护成员,例如。

    cpp 复制代码
    #include "iostream"
    using namespace std;
    class Time;
    class Date{
    private:
        int year, month, day;
    public:
        Date(int y, int m, int d):year(y),month(m),day(d){}
        friend void Calcutetime(Date d, Time t);
    };
    class Time{
    private:
        int hour, minute, second;
    public:
        Time(int h, int m, int s):hour(h),minute(m),second(s){}
        friend void Calcutetime(Date d, Time t);
    };
    void Calcutetime(Date d, Time t){
        int mom[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        int days = d.day, total;
        for(int i = 1; i < d.month; i++){
            days = days + mom[i - 1];
        }
        if((d.year % 4 == 0 && d.year % 100 != 0 || d.year % 400 == 0) && d.month >= 3) days = days + 1;
        total = ((days * 24 + t.hour) * 60 + t.minute) * 60 + t.second;
        cout << d.year << "-" << d.month << "-" << d.day << "    ";
        cout << t.hour << ":" << t.minute << ":" << t.second << endl;
        cout << "total time:   " << total << "   seconds" << endl;
    }
    int main(){
        Date d(2018, 11, 13);
        Time t(14, 20, 25);
        Calcutetime(d, t);
        return 0;
    }
  4. 若友元函数带了两个不同的类的对象,其中一个对象所对应的类要在后面声明。

类的成员函数作为友元函数

一个类的成员函数可以作为另一个类的友元函数,这种成员函数不仅可以访问自己所在类中的成员,还可以通过对象名访问friend声明语句所在类的私有成员和保护成员,从而使两个类相互合作。

cpp 复制代码
#include "iostream"
using namespace std;
class Time;
class Date{
private:
    int year, month, day;
public:
    Date(int y, int m, int d):year(y),month(m),day(d){}
    void Calcutetime(Time t);
};
class Time{
private:
    int hour, minute, second;
public:
    Time(int h, int m, int s):hour(h),minute(m),second(s){}
    friend void Date::Calcutetime(Time t);
};
void Date::Calcutetime(Time t){
    int mom[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int days = day, total;
    for(int i = 1; i < month; i++){
        days = days + mom[i - 1];
    }
    if((year % 4 == 0 && year % 100 != 0 || year % 400 == 0) && month >= 3) days = days + 1;
    total = ((days * 24 + t.hour) * 60 + t.minute) * 60 + t.second;
    cout << year << "-" << month << "-" << day << "    ";
    cout << t.hour << ":" << t.minute << ":" << t.second << endl;
    cout << "total time:   " << total << "   seconds" << endl;
}
int main(){
    Date d(2018, 11, 13);
    Time t(14, 20, 25);
    d.Calcutetime(t);
    return 0;
}

友元类

友元函数可以使函数能够访问某个类中的私有或保护成员。若类A的所有成员函数都想访问类B的私有或保护成员,一种方法使将类A的所有成员函数都声明为类B的友元函数,但这样会让程序显得冗余。为此,C++提供了友元类。一个类也可以作为另一个类的友元类。若类A声明为类B的友元类,则类A中的每个成员函数都具有访问类B的保护或私有数据成员的特权,方法如下:

cpp 复制代码
class A{
	//.....
	void fa();
};
class B{
	//.....
	friend class A;
};

例如:将一个复数转换为二维向量

cpp 复制代码
#include "iostream"
using namespace std;
class Complex{
private:
    double real, imag;
public:
    Complex(double r, double i):real(r),imag(i){}
    friend class Vector;
};
class Vector{
private:
    double x, y;
public:
    void Change(Complex c){
        x = c.real;
        y = c.imag;
    }
    void Print(Complex c){
        cout << "复数:" << c.real;
        if(c.imag > 0) cout << "+";
        cout << c.imag << "i" << endl;
        cout << "二维向量:" << "(" << x << "," << y << ")" << endl;
    }
};
int main(){
    Complex c(1.2, 3.4);
    Vector v;
    v.Change(c);
    v.Print(c);
    return 0;
}

【注】:友元关系是单向的。若类A是类B的友元,则类A中的所有成员函数都可以直接访问类B的保护或私有数据成员。但反过来,类B中的所有成员函数不能访问类A中的保护和私有成员。友元关系不能进行传递。友元在声明时即可以放在类的私有部分,也可以放在类的公有部分。

静态成员

C++允许在函数内部创建一个static对象,这个对象将存储在程序的静态数据区中,而不是在堆栈中,它只在函数第一次调用时初始化一次,以后将在两次函数之间保持其值。

静态数据成员

当声明一个类后,考研建立该类的多个对象,每个对象都有类中所定义数据成员的拷贝,对应不同的存储空间,各个对象相互独立,实现了数据的封装与隐藏。但在有些情况下,类中的某个数据成员是该类所有对象所共有的,若采用数据成员的定义方法,则不能实现数据成员的共享。C++为此提供了静态数据成员,类的静态数据成员拥有一块单独的存储区,不管用户创建了多少个对象,所有这些对象的静态数据成员都共享这块静态存储区,进而为这些对象提供了一种互相通信的方法。

静态数据成员的定义格式为:static 类型名 静态成员名;

静态数据成员的初始化格式:类型 类名::静态数据成员=初始化值;

例如

cpp 复制代码
#include "iostream"
#include "iomanip"
#include "cstring"

using namespace std;
class Student{
private:
    char *name;
    int no;
    float score;
    static int total;//定义静态数据成员
public:
    Student(char *na, int sno, float sco):no(sno),score(sco){
        name = new char(strlen(na) + 1);
        strcpy_s(name, strlen(na) + 1, na);
        total++;
    }
    ~Student(){
        delete []name;
    }
    void Print(){
        cout << "第" << total << "个学生:" << name << setw(4) << no << setw(4) << score << endl;
        cout << "总学生人数:" << total << endl;
    }
};
int Student::total = 0;//初始化静态数据成员
int main(){
    Student s1("张明", 1, 90);
    s1.Print();
    Student s2("张三", 2, 85);
    s2.Print();
    return 0;
}

运行结果:

cpp 复制代码
第1个学生:张明   1  90
总学生人数:1
第2个学生:张三   2  85
总学生人数:2

【注】

  1. 静态数据成员的声明需加关键字static,访问格式:类名::静态数据成员名
  2. 静态数据成员的初始化应在类外生命并在对象生成之前进行。默认时,静态成员被初始化为零。
  3. 静态数据成员在编译时创建并初始化,不能用构造函数进行初始化。静态数据成员不能再任何函数内分配存储空间和初始化。

静态成员函数

与静态数据成员一样,用户也可以创建静态成员函数,静态成员函数不能访问一般的数据成员和成员函数,它只能访问静态数据成员和其他的静态成员函数。静态成员函数时独立于类对象而存在的,因此没有this指针。

静态成员函数的定义格式为:static 返回类型 静态成员函数名(参数表);

调用格式为:类名::静态成员函数名(实参表);

静态对象

在定义对象时,可以定义类的静态对象,静态对象的析构函数是在主函数结束时才自动执行的,与普通对象相同,静态对象的析构函数和构造函数的执行顺序相反。

静态对象定义格式:static 类名 静态对象名;

例如:

cpp 复制代码
#include "iostream"
#include "iomanip"
#include "cstring"

using namespace std;
class Student{
private:
    char name[10];
public:
    Student(char na[]){
        strcpy_s(name, strlen(na) + 1, na);
        cout << "inside construct:" << name << endl;
    }
    ~Student(){
        cout << "inside destruct:" << name << endl;
    }
};
void Fun(){
    static Student s2("Student B");
    Student s3("Student C");
}
Student s1("Student A");
int main(){
    cout << "inside main" << endl;
    Fun();
    Fun();
    cout << "outside main" << endl;
    return 0;
}

运行结果:

cpp 复制代码
inside construct:Student A
inside main
inside construct:Student B
inside construct:Student C
inside destruct:Student C
inside construct:Student C
inside destruct:Student C
outside main
inside destruct:Student B
inside destruct:Student A
相关推荐
清风fu杨柳22 分钟前
centos7 arm版本编译qt5.6.3详细说明
开发语言·arm开发·qt
醉颜凉24 分钟前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
_小柏_27 分钟前
C/C++基础知识复习(20)
开发语言
hunandede31 分钟前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
c++·ffmpeg·音视频
程序员小明z36 分钟前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设
爱学习的大牛1231 小时前
通过vmware虚拟机安装和调试编译好的 ReactOS
c++·windows内核
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
Dontla2 小时前
Rust泛型系统类型推导原理(Rust类型推导、泛型类型推导、泛型推导)为什么在某些情况必须手动添加泛型特征约束?(泛型trait约束)
开发语言·算法·rust
tumu_C2 小时前
C++模板特化实战:在使用开源库boost::geometry::index::rtree时,用特化来让其支持自己的数据类型
c++·开源
杜若南星3 小时前
保研考研机试攻略(满分篇):第二章——满分之路上(1)
数据结构·c++·经验分享·笔记·考研·算法·贪心算法