C++复习:类和对象

类和对象

基本概念

1)类、对象、成员变量、成员函数

2)面向对象三大概念

封装、继承、多态

3)编程实践

    类的定义和对象的定义,对象的使用

    求圆形的面积

    定义Teacher类,打印Teacher的信息(把类的声明和类的实现分开)

类的封装

1)封装(Encapsulation)

A)封装,是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。

B)封装,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

    备注:有2层含义(把属性和方法进行封装 对属性和方法进行访问控制)

    C++中类的封装

成员变量,C++中用于表示类属性的变量

成员函数,C++中用

于表示类行为的函数

2)类成员的访问控制

在C++中可以给成员变量和成员函数定义访问级别

Public(公有)修饰成员变量和成员函数可以在类的内部和类的外部被访问

Private(私有)修饰成员变量和成员函数只能在类的内部被访问

//类是把属性和方法封装 同时对信息进行访问控制

//类的内部,类的外部

//我们抽象了一个类,用类去定义对象

//类是一个数据类型,类是抽象的

//对象是一个具体的变量。。占用内存空间。

class Circle

{

public:

    double r;

    double s;

public:

    double getR()

    {

        a++;

        return r;

    }

    void setR(double val)

    {

        r = val;

    }

public:

    double getS() //增加功能时,是在修改类, 修改类中的属性或者是方法

    {

        s = 3.14f*r*r;

        return s;

    }

    //private:

    int a;

};

 

3)struct和class关键字区别

在用struct定义类时,所有的没有被访问控制符限制的成员的默认属性为public

在用class定义类时,所有的没有被访问控制符限制的成员的默认属性为private    

对象的构造和析构

前言

创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的。

为了解决这个问题,C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。

构造和析构函数

1构造函数和析构函数的概念

有关构造函数

1构造函数定义及调用

1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;

2)构造函数在定义时可以有参数;

3)没有任何返回类型的声明。

2构造函数的调用

自动调用:一般情况下C++编译器会自动调用构造函数

手动调用:在一些情况下则需要手工调用构造函数

 

有关析构函数

3)析构函数定义及调用

    1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数

语法:~ClassName()

2)析构函数没有参数也没有任何返回类型的声明

3)析构函数在对象销毁时自动被调用

  1. 4)析构函数调用机制

    C++编译器自动调用

2 C++编译器构造析构方案 PK 对象显示初始化方案

设计构造函数和析构函数的原因

面向对象的思想是从生活中来,手机、车出厂时,是一样的。

生活中存在的对象都是被初始化后才上市的;初始状态是对象普遍存在的一个状态的

普通方案:

为每个类都提供一个public的initialize函数;

对象创建后立即调用initialize函数进行初始化。

优缺点分析

1)initialize只是一个普通的函数,必须显示的调用

2)一旦由于失误的原因,对象没有初始化,那么结果将是不确定的

没有初始化的对象,其内部成员变量的值是不定的

3)不能完全解决问题

//为什么对象需要初始化 有什么样的初始化方案

#include <iostream>

using namespace std;

 

/*

思考为什么需要初始化

面向对象思想来自生活,手机、车、电子产品,出厂时有初始化

怎么样进行初始化?

 

方案1:显示调用方法

缺点:易忘、麻烦;显示调用init,不能完全解决问题

*/

class Test21

{

public:

    int m;

    int getM() const { return m; }

    void setM(int val) { m = val; }

 

    int n;

    int getN() const { return n; }

    void setN(int val) { n = val; }

 

public:

    int init(int m, int n)

    {

        this->m = m;

        this->n = n;

        return 0;

    }

protected:

private:

};

 

int main()

{

    Test21 t1; //无参构造函数的调用方法

    Test21 t2;

 

    //t1.init(100, 200);

    //t2.init(300, 400);

    cout << t1.getM() << " " << t1.getN() << endl;

    cout << t2.getM() << " " << t2.getN() << endl;

 

    //定义对象数组时,没有机会进行显示初始化

    Test21 arr[3];

    //Test arr_2[3] = {Test(1,3), Test(), Test()};

 

    system("pause");

    return 0;

}

构造函数的分类及调用

C++编译器给程序员提供的对象初始化方案,高端大气上档次。

//有参数构造函数的三种调用方法

class Test

{

private:

    int a;

    int b;

 

public:

 

    //无参数构造函数

    Test()

    {

        ;

    }

 

    //带参数的构造函数

    Test(int a, int b)

    {

        ;

    }

    //赋值构造函数

    Test(const Test &obj)

    {

        ;

    }

 

public:

    void init(int _a, int _b)

    {

        a = _a;

        b = _b;

    }

};

1无参数构造函数

    调用方法: Test t1, t2;

2有参构造函数

有参构造函数的三种调用方法

//有参数构造函数的三种调用方法

class Test5

{

private:

    int a;

public:

    //带参数的构造函数

    Test5(int a)

    {

        printf("\na:%d", a);

    }

    Test5(int a, int b)

    {

        printf("\na:%d b:%d", a, b);

    }

public:

};

 

int main()

{

    Test5 t1(10); //c++编译器默认调用有参构造函数 括号法

    Test5 t2 = (20, 10); //c++编译器默认调用有参构造函数 等号法

    Test5 t3 = Test5(30); //程序员手工调用构造函数 产生了一个对象 直接调用构造函数法

 

    system("pause");

    return 0;

}

3拷贝构造函数调用时机

赋值构造函数的四种调用场景(调用时机)

第1和第2个调用场景

#include <iostream>

using namespace std;

 

class AA

{

public:

    AA() //无参构造函数 默认构造函数

    {

        cout << "我是构造函数,自动被调用了" << endl;

    }

    AA(int _a) //无参构造函数 默认构造函数

    {

        a = _a;

    }

    AA(const AA &obj2)

    {

        cout << "我也是构造函数,我是通过另外一个对象obj2,来初始化我自己" << endl;

        a = obj2.a + 10;

    }

    ~AA()

    {

        cout << "我是析构函数,自动被调用了" << endl;

    }

    void getA()

    {

        printf("a:%d \n", a);

    }

protected:

private:

    int a;

};

//单独搭建一个舞台

void ObjPlay01()

{

    AA a1; //变量定义

 

         //赋值构造函数的第一个应用场景

         //用对象1 初始化 对象2

    AA a2 = a1; //定义变量并初始化 //初始化法

 

    a2 = a1; //用a1来=号给a2 编译器给我们提供的浅拷贝

}

 

第二个应用场景

//单独搭建一个舞台

void ObjPlay02()

{

    AA a1(10); //变量定义

 

             //赋值构造函数的第一个应用场景

             //用对象1 初始化 对象2

    AA a2(a1); //定义变量并初始化 //括号法

 

             //a2 = a1; //用a1来=号给a2 编译器给我们提供的浅拷贝

    a2.getA();

}

//注意:初始化操作 和 等号操作 是两个不同的概念

 

第3个调用场景

#include <iostream>

using namespace std;

 

class Location

{

public:

    Location(int xx = 0, int yy = 0)

    {

        X = xx; Y = yy; cout << "Constructor Object.\n";

    }

    Location(const Location & p)      //复制构造函数

    {

        X = p.X; Y = p.Y; cout << "Copy_constructor called." << endl;

    }

    ~Location()

    {

        cout << X << "," << Y << " Object destroyed." << endl;

    }

    int GetX() { return X; }        int GetY() { return Y; }

private: int X, Y;

};

 

void f(Location p)

{

    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl;

}

 

void mainobjplay()

{

    Location A(1, 2); //形参是一个元素,函数调用,会执行实参变量初始化形参变量

    f(A); //采用值传递传递参数,所以发生拷贝。

}

 

void main()

{

    mainobjplay();

    system("pause");

}

 

 

第4个调用场景

第四个应用场景

#include <iostream>

using namespace std;

class Location

{

public:

    Location(int xx = 0, int yy = 0)

    {

        X = xx; Y = yy; cout << "Constructor Object.\n";

    }

    Location(const Location & p)      //复制构造函数

    {

        X = p.X; Y = p.Y; cout << "Copy_constructor called." << endl;

    }

    ~Location()

    {

        cout << X << "," << Y << " Object destroyed." << endl;

    }

    int GetX() { return X; }        int GetY() { return Y; }

private: int X, Y;

};

 

void f(Location p)

{

    cout << "Funtion:" << p.GetX() << "," << p.GetY() << endl;

}

 

Location g()

{

    Location A(1, 2);

    return A;

}

 

//对象初始化操作 和 =等号操作 是两个不同的概念

//匿名对象的去和留,关键看,返回时如何接

void mainobjplay()

{

    //若返回的匿名对象,赋值给另外一个同类型的对象,那么匿名对象会被析构

    //Location B;

    //B = g(); //用匿名对象 赋值 给B对象,然后匿名对象析构

 

    //若返回的匿名对象,来初始化另外一个同类型的对象,那么匿名对象会直接转成新的对象

    Location B = g();

    cout << "测试" << endl;

}

 

void main()

{

    mainobjplay();

    system("pause");

}

4默认构造函数

两个特殊的构造函数

1)默认无参构造函数

当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空

2)默认拷贝构造函数

当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制

构造函数调用规则研究

1)当类中没有定义任何一个构造函数时,c++编译器会提供默认无参构造函数和默认拷贝构造函数

2)当类中定义了拷贝构造函数时,c++编译器不会提供无参数构造函数

3) 当类中定义了任意的非拷贝构造函数(即:当类中提供了有参构造函数或无参构造函数),c++编译器不会提供默认无参构造函数

4 )默认拷贝构造函数成员变量简单赋值

总结:只要你写了构造函数,那么你必须用。

 

构造析构阶段性总结

1)构造函数是C++中用于初始化对象状态的特殊函数

2)构造函数在对象创建时自动被调用

3)构造函数和普通成员函数都遵循重载规则

4)拷贝构造函数是对象正确初始化的重要保证

5)必要的时候,必须手工编写拷贝构造函数

深拷贝和浅拷贝

  • 默认复制构造函数可以完成对象的数据成员值简单的复制
  • 对象的数据资源是由指针指示的堆时,默认赋值构造函数仅作指针值赋值

1浅拷贝问题分析

深拷贝浅拷贝现象出现的原因

2浅拷贝程序C++提供的解决方法

显式提供拷贝构造函数

显式重载=号操作,不使用编译器提供的浅拷贝

 

class Name

{

public:

    Name(const char *pname)

    {

        size = strlen(pname);

        pName = (char *)malloc(size + 1);

        strcpy(pName, pname);

    }

    Name(Name &obj)

    {

        //用obj来初始化自己

        pName = (char *)malloc(obj.size + 1);

        strcpy(pName, obj.pName);

        size = obj.size;

    }

    ~Name()

    {

        cout << "开始析构" << endl;

        if (pName != NULL)

        {

            free(pName);

            pName = NULL;

            size = 0;

        }

    }

 

    void operator=(Name &obj3)

    {

        if (pName != NULL)

        {

            free(pName);

            pName = NULL;

            size = 0;

        }

        cout << "测试有没有调用我。。。。" << endl;

 

        //用obj3来=自己

        pName = (char *)malloc(obj3.size + 1);

        strcpy(pName, obj3.pName);

        size = obj3.size;

    }

 

protected:

private:

    char *pName;

    int size;

};

 

//对象的初始化 和 对象之间=号操作是两个不同的概念

void playObj()

{

    Name obj1("obj1.....");

    Name obj2 = obj1; //obj2创建并初始化

 

    Name obj3("obj3...");

 

    //重载=号操作符

    obj2 = obj3; //=号操作

 

    cout << "业务操作。。。5000" << endl;

 

}

void main()

{

    playObj();

    system("pause");

}

 

多个对象构造和析构

1对象初始化列表

1)对象初始化列表出现原因

1.必须这样做:

如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,

如果没有初始化列表,那么他将无法完成第一步,就会报错。

 

2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值

当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,

因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

2)C++中提供初始化列表对成员变量进行初始化

语法规则

Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)

{

// 其他赋值操作

}

3)注意概念

初始化:被初始化的对象正在创建

赋值:被赋值的对象已经存在

 

4)注意:

数据成员顺序最好和初始化列表的顺序一样

初始化列表先于构造函数的函数体执行

 

/*

1 C++中提供了初始化列表对成员变量进行初始化

2 使用初始化列表出现原因:

1.必须这样做:

如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,

而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,

如果没有初始化列表,那么他将无法完成第一步,就会报错。

 

2、类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值

当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,

因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

*/

 

//总结 构造和析构的调用顺序

 

#include <iostream>

using namespace std;

 

class ABC

{

public:

    ABC(int a, int b, int c)

    {

        this->a = a;

        this->b = b;

        this->c = c;

        printf("a:%d,b:%d,c:%d \n", a, b, c);

        printf("ABC construct ..\n");

    }

    ~ABC()

    {

        printf("a:%d,b:%d,c:%d \n", a, b, c);

        printf("~ABC() ..\n");

    }

protected:

private:

    int a;

    int b;

    int c;

};

 

 

class MyD

{

public:

    MyD() :abc1(1, 2, 3), abc2(4, 5, 6), m(100)

        //MyD()

    {

        cout << "MyD()" << endl;

    }

    ~MyD()

    {

        cout << "~MyD()" << endl;

    }

 

protected:

private:

    ABC abc1; //c++编译器不知道如何构造abc1

    ABC abc2;

    const int m;

};

 

 

int run()

{

    MyD myD;

    return 0;

}

 

int main()

{

 

    run();

    system("pause");

    return 0;

}

构造函数和析构函数的调用顺序研究

构造函数与析构函数的调用顺序

1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数

2)析构函数的调用顺序与对应的构造函数调用顺序相反。

构造函数和析构函数综合

通过训练,把所学知识点都穿起来

1构造析综合训练

注意赋值构函数的调用

2匿名对象强化训练

  1. 匿名对象生命周期
  2. 匿名对象的去和留

3匿名对象强化训练

  1. 构造中调用构造

    构造函数中调用构造函数,是一个蹩脚的行为。

对象的动态建立和释放

1 newdelete基本语法

1)在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和销毁内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。

注意: new和delete是运算符,不是函数,因此执行效率高。

 

2)虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。

new运算符的例子:

new int;  //开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针)

new int(100);  //开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址

new char[10];  //开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址

new int[5][4];  //开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址

float *p=new float (3.14159);  //开辟一个存放单精度数的空间,并指定该实数的初值为//3.14159,将返回的该空间的地址赋给指针变量p

 

    3)new和delete运算符使用的一般格式为:

用new分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。

  1. 应用举例

2 类对象的动态建立和释放

使用类名定义的对象都是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。

    C++中,可以用new运算符动态建立对象,用delete运算符撤销对象

比如:

Box *pt;  //定义一个指向Box类对象的指针变量pt   

pt=new Box;  //在pt中存放了新建对象的起始地址在程序中就可以通过pt访问这个新建的对象。

如   

cout<<pt->height;  //输出该对象的height成员   

cout<<pt->volume( );  //调用该对象的volume函数,计算并输出体积C++还允许在执行new时,对新建立的对象进行初始化。

如   

Box *pt=new Box(12,15,18);

这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。

新对象中的height,width和length分别获得初值12,15,18。调用对象既可以通过对象名,也可以通过指针。

    在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。只要检测返回值是否为0,就可判断分配内存是否成功。

    ANSI C++标准提出,在执行new出现故障时,就"抛出"一个"异常",用户可根据异常进行有关处理。但C++标准仍然允许在出现new故障时返回0指针值。当前,不同的编译系统对new故障的处理方法是不同的。

在不再需要使用由new建立的对象时,可以用delete运算符予以释放。如

delete pt; //释放pt指向的内存空间。这就撤销了pt指向的对象。此后程序不能再使用该对象。

如果用一个指针变量pt先后指向不同的动态对象,应注意指针变量的当前指向,以免删错了对象。在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。

建议一个指针专门只指向一个对象。

3 编程实践

//1 malloc free函数            c关键字

//    new delete 操作符号         c++的关键字

 

 

//2 new 在堆上分配内存 delete

//分配基础类型 、分配数组类型、分配对象

 

//3 new和malloc 深入分析

混用测试、异同比较

结论:    malloc不会调用类的构造函数

        free不会调用类的析构函数

 

静态成员变量成员函数

思考:每个变量,拥有属性。有没有一些属性,归所有对象拥有?

静态成员变量

1)定义静态成员变量

  • 关键字 static 可以用于说明一个类的成员,

    静态成员提供了一个同类对象的共享机制

  • 把一个类的成员说明为 static 时,这个类无论有多少个对象被创建,这些对象共享这个 static 成员
  • 静态成员局部于类,它不是对象成员。(一个通用特性)

例如:

#include<iostream>

using namespace std;

class counter

{

    static int num; //声明与定义静态数据成员

public:

    void setnum(int i) { num = i; }     //成员函数访问静态数据成员

    void shownum() { cout << num << '\t'; }

};

int counter::num = 0;//声明与定义静态数据成员

void main()

{

    counter a, b;

    a.shownum(); //调用成员函数访问私有静态数据成员

    b.shownum();

    a.setnum(10);

    a.shownum();

    b.shownum();

}

 

从结果可以看出,访问的是同一个静态数据成员

2)使用静态成员变量

// 使用公有静态数据成员

#include<iostream.h>

class counter

{

public:

    counter(int a) { mem = a; }

    int mem;        //公有数据成员

    static int Smem;    //公有静态数据成员

};

int counter::Smem = 1;    //初始值为1

void main()

{

    counter c(5);

    int i;

    for (i = 0; i < 5; i++)

    {

        counter::Smem += i;

        cout << counter::Smem << '\t'; //访问静态成员变量方法2

    }

    cout << endl;

    cout << "c.Smem = " << c.Smem << endl; //访问静态成员变量方法1

    cout << "c.mem = " << c.mem << endl;

}

静态成员函数

1)概念

  • 静态成员函数数冠以关键字static
  • 静态成员函数提供不依赖于类数据结构的共同操作,它没有this指针
  • 在类外调用静态成员函数用 "类名 :: "作限定词,或通过对象调用

2)案例

3)疑难问题:静态成员函数中,不能使用普通变量。只能调用同样属性的静态变量

    //静态成员变量属于整个类的,分不清楚,是那个具体对象的属性。

综合

C++面向对象模型初探

前言

C++对象模型可以概括为以下2部分:

1. 语言中直接支持面向对象程序设计的部分,主要涉及如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等。

2. 对于各种支持的底层实现机制。

在c语言中,"数据"和"处理数据的操作(函数)"是分开来声明的,也就是说,语言本身并没有支持"数据和函数"之间的关联性。在c++中,通过抽象数据类型,在类中定义数据和函数,来实现数据和函数直接的绑定

概括来说,在C++类中有两种成员数据:static、nonstatic(非静态);三种成员函数:static、nonstatic、virtual。

基础知识

C++中的class从面向对象理论出发,将变量(属性)和函数(方法)集中定义在一起,用于描述现实世界中的类。从计算机的角度,程序依然由数据段和代码段构成。

C++编译器如何完成面向对象理论到计算机程序的转化?

换句话:C++编译器是如何管理类、对象、类和对象之间的关系

具体的说:具体对象调用类中的方法,那,c++编译器是如何区分,是那个具体的类,调用这个方法那?

思考一下程序结果

#include <iostream>

 

using namespace std;

 

class C1

{

public:

    int i; //4字节

    int j; //4字节

    int k; //4字节

protected:

private:

}; //12字节

 

class C2

{

public:

    int i;

    int j;

    int k;

 

    static int m; //4

public:

    int getK() const { return k; } //4

    void setK(int val) { k = val; } //4

 

protected:

private:

}; //24 16 12(都不对)

 

struct S1

{

    int i;

    int j;

    int k;

}; //

 

struct S2

{

    int i;

    int j;

    int k;

    static int m;

}; //

 

int main()

{

    printf("c1:%d \n", sizeof(C1));

    printf("c2:%d \n", sizeof(C2));

    printf("s1:%d \n", sizeof(S1));

    printf("s2:%d \n", sizeof(S2));

 

    system("pause");

}

编译器对属性和方法的处理机制

通过上面的案例,我们可以的得出:

1)C++类对象中的成员变量和成员函数是分开存储的

成员变量:

普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式

静态成员变量:存储于全局数据区中

成员函数:存储于代码段中。

问题出来了:很多对象共用一块代码?代码是如何区分具体对象的那?

 

换句话说:int getK() const { return k; },代码是如何区分,具体obj1、obj2、obj3对象的k值

2)C++编译器对普通成员函数的内部处理

请仔细思考,并说出你的总结!

总结

1、C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效!

2、C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。

3、静态成员函数、成员变量属于类

静态成员函数与普通成员函数的区别

静态成员函数不包含指向具体对象的指针

普通成员函数包含一个指向具体对象的指针

 

this指针

 

实验1:若类成员函数的形参 和 类的属性,名字相同,通过this指针来解决。

实验2:类的成员函数可通过const修饰,请问const修饰的是谁?(函数)

全局函数PK成员函数

    1、把全局函数转化成成员函数,通过this指针隐藏左操作数

        Test add(Test &t1, Test &t2)===》Test add(Test &t2)

    2、把成员函数转换成全局函数,多了一个参数

        void printAB()===》void printAB(Test *pthis)

    3、函数返回元素和返回引用

            Test& add(Test &t2) //*this //函数返回引用

{

    this->a = this->a + t2.getA();

    this->b = this->b + t2.getB();

    return *this; //*操作让this指针回到元素状态

}

 

Test add2(Test &t2) //*this //函数返回元素

{

    //t3是局部变量

    Test t3(this->a + t2.getA(), this->b + t2.getB());

    return t3;

}

友元

友元函数

例如

 

 

class A1

{

public:

    A1()

    {

        a1 = 100;

        a2 = 200;

    }

    int getA1()

    {

        return this->a1;

    }

    //声明一个友元函数

    friend void setA1(A1 *p, int a1); //这个函数是这个类的好朋友

 

protected:

private:

    int a1;

    int a2;

};

 

void setA1(A1 *p, int a1)

{

    p->a1 = a1;

}

void main()

{

    A1 mya1;

    cout << mya1.getA1() << endl;

    setA1(&mya1, 300); //通过友元函数 修改A类的私有属性

    cout << mya1.getA1() << endl;

 

    system("pause");

}

 

友元类

  • 若B类是A类的友员类,则B类的所有成员函数都是A类的友员函数
  • 友员类通常设计为一种对数据操作或类之间传递消息的辅助类

 

强化

static关键字强化

  • 某商店经销一种货物。货物购进和卖出时以箱为单位,各箱的重量不一样,因此,商店需要记录目前库存的总重量。现在用C++模拟商店货物购进和卖出的情况。

 

#include <iostream>

using namespace std;

 

class Goods

{

public:

    Goods(int w) { weight = w; total_weight += w; }

    ~Goods() { total_weight -= weight; }

    int Weight() { return weight; };

    static int TotalWeight() { return total_weight; }

    Goods *next;

private:

    int weight;

    static int total_weight;

};

 

int Goods::total_weight = 0;//静态变量的初始化别忘了

 

//r尾部指针

void purchase(Goods * &f, Goods *& r, int w)

{

    Goods *p = new Goods(w);

    p->next = NULL;

    if (f == NULL) f = r = p;

    else { r->next = p; r = r->next; } //尾部指针下移或新结点变成尾部结点

}

void sale(Goods * & f, Goods * & r)

{

    if (f == NULL) { cout << "No any goods!\n"; return; }

    Goods *q = f; f = f->next; delete q;

    cout << "saled.\n";

}

 

void main()

{

    Goods * front = NULL, *rear = NULL;

    int w; int choice;

    do

    {

        cout << "Please choice:\n";

        cout << "Key in 1 is purchase,\nKey in 2 is sale,\nKey in 0 is over.\n";

        cin >> choice;

        switch (choice)        // 操作选择

        {

        case 1: // 键入1,购进1箱货物

        { cout << "Input weight: ";

        cin >> w;

        purchase(front, rear, w); // 从表尾插入1个结点

        break;

        }

        case 2:          // 键入2,售出1箱货物

        { sale(front, rear); break; } // 从表头删除1个结点

        case 0: break;          // 键入0,结束

        }

        cout << "Now total weight is:" << Goods::TotalWeight() << endl;

    } while (choice);

}

 

小结

  • 类通常用关键字class定义。类是数据成员和成员函数的封装。类的实例称为对象。
  • 结构类型用关键字struct定义,是由不同类型数据组成的数据类型。
  • 类成员由private, protected, public决定访问特性。public成员集称为接口。
  • 构造函数在创建和初始化对象时自动调用。析构函数则在对象作用域结束时自动调用。
  • 重载构造函数和复制构造函数提供了创建对象的不同初始化方式。
  • 静态成员是局部于类的成员,提供一种同类对象的共享机制。
  • 友元用关键字friend声明。友员是对类操作的一种辅助手段。一个类的友员可以访问该类各种性质的成员
  • 链表是一种重要的动态数据结构,可以在程序运行时创建或撤消数据元素。

 

运算符重载

概念

什么是运算符重载

所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是"一名多用"。

 

运算符也可以重载。实际上,我们已经在不知不觉之中使用了运算符重载。例如,大 家都已习惯于用加法运算符"+"对整数、单精度数和双精度数进行加法运算,如5+8, 5.8 +3.67等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的, 但由于C++已经对运算符"+"进行了重载,所以就能适用于int, float, doUble类型的运算。

 

又如"<<"是C++的位运算中的位移运算符(左移),但在输出操作中又是与流对象cout 配合使用的流插入运算符,">>"也是位移运算符(右移),但在输入操作中又是与流对象 cin 配合使用的流提取运算符。这就是运算符重载(operator overloading)。C++系统对"<<"和">>"进行了重载,用户在不同的场合下使用它们时,作用是不同 的。对"<<"和">>"的重载处理是放在头文件stream中的。因此,如果要在程序中用"<< "和">>"作流插入运算符和流提取运算符,必须在本文件模块中包含头文件stream(当然还应当包括"using namespace std")。

现在要讨论的问题是:用户能否根据自己的需要对C++已提供的运算符进行重载,赋予它们新的含义,使之一名多用?

 

运算符重载入门技术推演

1为什么会用运算符重载机制

    用复数类举例

//Complex c3 = c1 + c2;

//原因 Complex是用户自定义类型,编译器根本不知道如何进行加减

    //编译器给提供了一种机制,让用户自己去完成,自定义类型的加减操作。。。。。

    //这个机制就是运算符重载机制

2 运算符重载的本质是一个函数

 

class Complex

{

public:

    int a;

    int b;

    friend Complex operator+(Complex &c1, Complex &c2);

public:

    Complex(int a = 0, int b = 0)

    {

        this->a = a;

        this->b = b;

    }

 

public:

    void printCom()

    {

        cout << a << " + " << b << "i " << endl;

    }

 

private:

};

 

/*

Complex myAdd(Complex &c1, Complex &c2)

{

Complex tmp(c1.a+ c2.a, c1.b + c2.b);

return tmp;

}

*/

 

Complex operator+(Complex &c1, Complex &c2)

{

    Complex tmp(c1.a + c2.a, c1.b + c2.b);

    return tmp;

}

 

void main()

{

    Complex c1(1, 2), c2(3, 4);

 

    //Complex c3 = c1 + c2; //用户自定义类型 编译器无法让变量相加

    //Complex myAdd(Complex &c1, Complex &c2);

 

    //1 普通函数

    //Complex c3 = myAdd(c1, c2);

    //c3.printCom();

 

    //2 operator+ 函数名称

    //Complex c3 = operator+(c1, c2);

    //c3.printCom();

 

    //3 +替换 函数名

    Complex c3 = c1 + c2; //思考C++编译器如何支持操作符重载机制的 (根据类型)

    c3.printCom();

    {

        int a = 0, b = 0, c; //基础类型C++编译器知道如何加减

        c = a + b;

    }

 

    //4 把Complex类变成私有属性

    //友元函数的应用场景

    //friend Complex operator+(Complex &c1, Complex &c2);

 

    cout << "hello..." << endl;

    system("pause");

    return;

}

 

运算符重载的限制

运算符重载编程基础

例如:

    //全局函数 完成 +操作符 重载

Complex operator+(Complex &c1, Complex &c2)

//类成员函数 完成 -操作符 重载

Complex operator-(Complex &c2)

运算符重载的两种方法

例如1:

//通过类成员函数完成-操作符重载

//函数声明 Complex operator-(Complex &c2)

//函数调用分析

//用类成员函数实现-运算符重载

    Complex c4 = c1 - c2;

    c4.printCom();

    //c1.operator-(c2);

 

例如2:

//通过全局函数方法完成+操作符重载

//函数声明 Complex operator+(Complex &c1, Complex &c2)

//函数调用分析

int main()

{

        Complex c1(1, 2), c2(3, 4);

        //Complex c31 = operator+(c1, c2);

Complex c3 = c1 + c2;

c3.printCom();

}

 

例如3

//前置++操作符 用全局函数实现

Complex& operator++(Complex &c1)

{

    c1.a ++;

    c1.b ++;

    return c1;

}

//调用方法

    ++c1 ; //=è需要写出操作符重载函数原形

    c1.printCom();

 

//运算符重载函数名定义

    //首先承认操作符重载是一个函数 定义函数名èoperator++

    //分析函数参数 根据左右操作数的个数,èoperator++(Complex &c1)

    //分析函数返回值è Complex& operator++(Complex &c1) 返回它自身

 

例如4

//4.1前置--操作符 成员函数实现

Complex& operator--()

{

    this->a--;

    this->b--;

    return *this;

}

 

    //4.2调用方法

        --c1;

        c1.printCom();

    //4.3前置—运算符重载函数名定义

    //c1.operator--()

 

例如5

    //5.1 //后置++ 操作符 用全局函数实现

Complex operator++(Complex &c1, int)

{

    Complex tmp = c1;

    c1.a++;

    c1.b++;

    return tmp;

}

//5.2 调用方法

c1 ++ ; //先使用 后++

//5.3 后置++运算符重载函数名定义

Complex operator++(Complex &c1, int) //函数占位参数 和 前置++ 相区别

 

例如6

//6.1 后置— 操作符 用类成员函数实现

    Complex operator--(int)

    {

        Complex tmp = *this;

        this->a--;

        this->b--;

        return tmp;

    }

//6.2 调用方法

c1 ++ ; //先使用 后++

//6.3 后置--运算符重载函数名定义

Complex operator--(int) //函数占位参数 和 前置-- 相区别

 

前置和后置运算符总结

C++中通过一个占位参数来区分前置运算和后置运算

定义运算符重载函数名的步骤

全局函数、类成员函数方法实现运算符重载步骤

    1)要承认操作符重载是一个函数,写出函数名称operator+ ()

    2)根据操作数,写出函数参数

    3)根据业务,完善函数返回值(看函数是返回引用 还是指针 元素),及实现函数业务

友元函数实现操作符重载的应用场景

1)友元函数和成员函数选择方法

  • 当无法修改左操作数的类时,使用全局函数进行重载
  • =, [], ()和->操作符只能通过成员函数进行重载

 

2)用友元函数 重载 << >>操作符

  • istream 和 ostream 是 C++ 的预定义流类
  • cin 是 istream 的对象,cout 是 ostream 的对象
  • 运算符 << 由ostream 重载为插入操作,用于输出基本类型数据
  • 运算符 >> 由 istream 重载为提取操作,用于输入基本类型数据
  • 用友员函数重载 << 和 >> ,输出和输入用户自定义的数据类型

 

a)用全局函数方法实现 << 操作符

ostream& operator<<(ostream &out, Complex &c1)

{

    //out<<"12345,生活真是苦"<<endl;

    out<<c1.a<<" + "<<c1.b<<"i "<<endl;

    return out;

}

//调用方法

cout<<c1;

//链式编程支持

cout<<c1<<"abcc";

//cout.operator<<(c1).operator<<("abcd");

//函数返回值充当左值 需要返回一个引用

b)类成员函数方法无法实现 << 操作符重载

        //因拿到cout这个类的源码

        //cout.operator<<(c1);

 

  1. 友元函数重载操作符使用注意点
  2. 友员函数重载运算符常用于运算符的左右操作数类型不同的情况

b)其他

  • 在第一个参数需要隐式转换的情形下,使用友员函数重载运算符是正确的选择
  • 友员函数没有 this 指针,所需操作数都必须在参数表显式声明,很容易实现类型的隐式转换
  • C++中不能用友员函数重载的运算符有

    = () [] ->

 

4 )友元函数案例vector类

#include <iostream>

using namespace std;

 

//为vector类重载流插入运算符和提取运算符

class vector

{

public:

    vector(int size = 1);

    ~vector();

    int & operator[](int i);

    friend ostream & operator << (ostream & output, vector &);

    friend istream & operator >> (istream & input, vector &);

private:

    int * v;

    int len;

};

 

vector::vector(int size)

{

    if (size <= 0 || size > 100)

    {

        cout << "The size of " << size << " is null !\n"; abort();

    }

    v = new int[size]; len = size;

}

 

vector :: ~vector()

{

    delete[] v;

    len = 0;

}

 

int &vector::operator[](int i)

{

    if (i >= 0 && i < len) return v[i];

    cout << "The subscript " << i << " is outside !\n"; abort();

}

ostream & operator << (ostream & output, vector & ary)

{

    for (int i = 0; i < ary.len; i++)

        output << ary[i] << " ";

    output << endl;

    return output;

}

istream & operator >> (istream & input, vector & ary)

{

    for (int i = 0; i < ary.len; i++)

        input >> ary[i];

    return input;

}

 

void main()

{

    int k;

    cout << "Input the length of vector A :\n";

    cin >> k;

    vector A(k);

    cout << "Input the elements of vector A :\n";

    cin >> A;

    cout << "Output the elements of vector A :\n";

    cout << A;

    system("pause");

}

 

运算符重载提高

1运算符重载机制

C++编译器是如何支持操作符重载机制的?

2重载赋值运算符=

  • 赋值运算符重载用于对象数据的复制
  • operator= 必须重载为成员函数
  • 重载函数原型为:

    类型 & 类名 :: operator= ( const 类名 & ) ;

案例:完善Name类,支持=号操作。


结论:

    1 //先释放旧的内存

    2 返回一个引用

    3 =操作符从右向左

//obj3 = obj1; // C++编译器提供的 等号操作 也属 浅拷贝

// obj4 = obj3 = obj1

//obj3.operator=(obj1)

 

Name& operator=(Name &obj1)

{

    //1 先释放obj3旧的内存

    if (this->m_p != NULL)

    {

        delete[] m_p;

        m_len = 0;

    }

    //2 根据obj1分配内存大小

    this->m_len = obj1.m_len;

    this->m_p = new char[m_len + 1];

 

    //3把obj1赋值给obj3

    strcpy(m_p, obj1.m_p);

    return *this;

}

 

3重载数组下表运算符[]

重载[]和()运算符

  • 运算符 [] 和 () 是二元运算符
  • [] 和 () 只能用成员函数重载,不能用友元函数重载

重载下标运算符 []

[] 运算符用于访问数据对象的元素

重载格式    类型 :: operator[] ( 类型 ) ;

 

设 x 是类 X 的一个对象,则表达式

        x [ y ]

    可被解释为

        x . operator [ ] ( y )

 

4重载函数调用符 ()

() 运算符用于函数调用

重载格式    类型 类 :: operator() ( 表达式表 ) ;

例1

    设 x 是类 X 的一个对象,则表达式

        x ( arg1, arg2, … )

    可被解释为

        x . operator () (arg1, arg2, … )

案例:

//例2:用重载()运算符实现数学函数的抽象

#include <iostream>

class F

{ public :

double operator ( ) ( double x , double y ) ;

} ;

double F :: operator ( ) ( double x , double y )

{ return x * x + y * y ; }

void main ( )            

{

F f ;

f.getA();

    cout << f ( 5.2 , 2.5 ) << endl ; // f . operator() (5.2, 2.5)

}

 

比较普通成员函数

//例3 用重载()运算符实现 pk 成员函数

#include <iostream.h>

class F

{ public :

double memFun ( double x , double y ) ;

} ;

double F :: memFun ( double x , double y )

{ return x * x + y * y ; }

void main ( )            

{

F f ;

    cout << f.memFun ( 5.2 , 2.5 ) << endl ;

}

5为什么不要重载&&||操作符

理论知识:

1)&&和||是C++中非常特殊的操作符

2)&&和||内置实现了短路规则

3)操作符重载是靠函数重载来完成的

4)操作数作为函数参数传递

5)C++的函数参数都会被求值,无法实现短路规则

#include <cstdlib>

#include <iostream>

 

using namespace std;

 

class Test

{

    int i;

public:

    Test(int i)

    {

        this->i = i;

    }

 

    Test operator+ (const Test& obj)

    {

        Test ret(0);

 

        cout << "执行+号重载函数" << endl;

        ret.i = i + obj.i;

        return ret;

    }

 

    bool operator&& (const Test& obj)

    {

        cout << "执行&&重载函数" << endl;

        return i && obj.i;

    }

};

 

// && 从左向右

void main()

{

    int a1 = 0;

    int a2 = 1;

 

    cout << "注意:&&操作符的结合顺序是从左向右" << endl;

 

    if (a1 && (a1 + a2))

    {

        cout << "有一个是假,则不在执行下一个表达式的计算" << endl;

    }

 

    Test t1 = 0;

    Test t2 = 1;

 

    If(t1 && (t1 + t2))

    {

        //t1 && t1.operator(t2)

        // t1.operator( t1.operator(t2) )

        cout << "两个函数都被执行了,而且是先执行了+" << endl;

    }

 

    system("pause");

    return;

}

 

 

附录:运算符和结合性

总结

操作符重载是C++的强大特性之一

操作符重载的本质是通过函数扩展操作符的语义

operator关键字是操作符重载的关键

friend关键字可以对函数或类开发访问权限

操作符重载遵循函数重载的规则

操作符重载可以直接使用类的成员函数实现

=, [], ()和->操作符只能通过成员函数进行重载

++操作符通过一个int参数进行前置与后置的重载

C++中不要重载&&和||操作符

猜你喜欢

转载自www.cnblogs.com/love-DanDan/p/8966752.html