类和对象

基本概念

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 new和delete基本语法

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++中不要重载&&和||操作符

C++复习:类和对象的更多相关文章

  1. 3.JAVA基础复习——JAVA中的类与对象

    什么是对象: 就是现实中真实的实体,对象与实体是一一对应的,现实中每一个实体都是一个对象在. JAVA中的对象: Java中通过new关键字来创建对象. 类: 用JAVA语言对现实生活中的事物进行描述 ...

  2. Java复习(二)类与对象的基本概念

    2.1面向对象的程序设计方法概述 对象 程序中: 一切皆是对象 都具有标识,属性和行为 通过一个或多个变量来保存其状态 通过方法实现他的行为 类 将属性及行为相同或相似的对象归为一类 类可以看成是对象 ...

  3. C#学习笔记(十一):类和对象

    面向对象 为什么要面向对象: 1.和函数一样,把算法封装起来,方便复用 2.更好理解自己和别人写的代码 封装:数据.结构.逻辑的封装,方便复用 多态:同一个对象,同一种指令,不同的行为(反应) 继承: ...

  4. java 类与对象基础整理

    之前学习javaSE的时候,没有针对性地对对类与对象的一些基础进行整理,下面这些内容是笔记内容整理后的,希望以后自己可以通过这些博客时常复习! 一.类and对象的基础 类似于类的生命啊,类与对象的关系 ...

  5. 关于Java构造类与对象的思考

    简单记录一下Java构造类与对象时的流程以及this和super对于特殊例子的分析. 首先,接着昨天的问题,我做出了几个变形: Pic1.原版: Pic2.去掉了T.foo方法中的this关键字: P ...

  6. Java编程里的类和对象

    像我们搞计算机这块的,都知道这么一件事,当前的计算机编程语言主要分为两大块,一为面向过程,二为面向对象.Java就是一门纯面向对象的语言.学习了一个月左右的Java,在下对于Java当中的类和对象有了 ...

  7. Python - 类与对象的方法

    类与对象的方法

  8. C++基础知识(5)---类和对象

    终于把C++中的基础在前面的几篇博客中总结完了,可能还有一些语法还没有总结到,没关系,以后用到了再查资料就好.类是C++中的一个非常重要的概念,这是区别你使用的C++到底是面向过程还是面向对象的一个重 ...

  9. 简述JavaScript对象、数组对象与类数组对象

    问题引出 在上图给出的文档中,用JavaScript获取那个a标签,要用什么办法呢?相信第一反应一定是使用document.getElementsByTagName('a')[0]来获取.同样的,在使 ...

随机推荐

  1. Kafka参数详解

    一.相关参数配置 System 系统参数 #唯一标识在集群中的ID,要求是正数. broker.id=0 #服务端口,默认9092 port=9092 #监听地址,不设为所有地址 host.name= ...

  2. iOS @property语句

    @property声明的形式是: @property ( attributes ) type name; type和name的含义一目了然,attributes描述了如何编写访问器. 一.assign ...

  3. kafka producer实例

    1. 定义要发送的消息User POJO package lenmom.kafkaproducer; public class User { public String name; public in ...

  4. mysql下有符号数和无符号数的相关问题

    最近自己的程序在调用mysql的存储过程传参给smallint类型变量的时候,总是出现out of range value的错误,刚开始用C数值转换方式的二进制位转换思路来思考时,总是觉得没什么问题, ...

  5. mysql sql中的一些问题,Null与空字符

    mysql中的空值,NULL,空字符 Mysql数据库是一个基于结构化数据的开源数据库.SQL语句是MySQL数据库中核心语言.不过在MySQL数据库中执行SQL语句,需要小心两个陷阱. 陷阱一:空值 ...

  6. 理解Solr缓存及如何设置缓存大小

    文献地址:http://wangdg.com/understanding-and-tuning-solr-cache/ 理解Solr缓存及如何设置缓存大小 为了得到最好的检索性能,Solr会在内存中缓 ...

  7. 「2017 山东一轮集训 Day4」棋盘(费用流)

    棋盘模型 + 动态加边 #include<cstdio> #include<algorithm> #include<iostream> #include<cs ...

  8. 小数据池 id

    1. 小数据池, id() 小数据池针对的是: int, str, bool 在py文件中几乎所有的字符串都会缓存. id() 查看变量的内存地址 # id()函数可以帮我们查看一个变量的内存地址 # ...

  9. POJ2311 Cutting Game 博弈 SG函数

    Cutting Game Description Urej loves to play various types of dull games. He usually asks other peopl ...

  10. redis下操作String

    redis操作string string是redis最基本的类型 最大能存储512MB数据 string类型是二进制安全的,即可以为任何数据,比如数字.图片.序列化对象等 基本命令 设置 设置键值 s ...