• 后台开发核心技术与应用实践看书笔记(二):面向对象的C++


    类与对象

    类与对象的概念

    类是抽象的,不占用存储空间。

    class默认是private的,stuct里面默认是public。

    C语言中,struct不能定义成员函数,但是在C++中,增加了class类型后,扩展了struct的功能,struct中也能定义成员函数了。

    比如

    struct Student{
        public:
        void play()
        {
            
        }
        private:
        int num;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    好的习惯:每一种成员访问限定符在类体中只出现一次,并且先写public部分,把private部分放在类体的后部,这样可以使得用户将注意力集中在能被外界调用的成员上,使得阅读者的思路更加清晰。

    类的封装性

    受保护成员访问权限:允许类成员和派生类成员访问,不运行类外的任何访问。比如private不能直接a.这样调用,必须调用函数才行。

    为了实现信息隐藏,会把类成员函数的定义放在另一个文件中,而不放在头文件中。

    构造函数

    如果用户自己没有定义构造函数,那么C++系统就会自动为其生成一个构造函数,只是这个构造函数的函数体是空的,什么也不做,当然也不进行初始化。

    C++提供另一种初始化数据成员的方法:参数初始化表。这种方法不在函数体内对数据成员初始化,而是在函数首部实现。

    Circle::Circle(int r):radius(r){}
    
    • 1

    一个类中如果定义了全是默认参数的构造函数后,就不能再定义重载构造函数了(因为不知道调用的是谁了)

    析构函数

    static局部对象在函数调用结束时对象不释放,所以也不执行析构函数。只有在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。

    如果用户没有编写析构函数,编译系统会自动生成一个默认的析构函数,但不进行任何操作,所以许多简单的类中没有用显式的析构函数。

    静态数据成员

    全局数据可以被任何人修改,而且在一个项目中,它很容易和其他名字冲突。

    如果一个静态数据成员被声明而没有被定义,链接器回报高一个错误:定义必须出现在类的外部而且只能定义一次。所以静态数据成员的声明通常会放在一个类的实现文件中。

    比如

    xxx.h文件中

    class base{
        public:
        static int var;
    };
    
    • 1
    • 2
    • 3
    • 4

    xxx.cpp类型文件中

    int base::var=10;
    
    • 1

    在头文件中定义(初始化)静态成员容易引起重复定义的错误,比如这个头文件同时被多个.cpp文件所包含的时候。即使加上#ifndef#define#endif或者#pragma once也不行。

    C++静态数据成员被类的所有对象所共享,包括该类的派生类的对象。

    如果在一个函数中定义了静态变量,在函数结束时该静态变量并不被释放,仍然存在并保留其值。

    静态数据成员类似,它不随对象建立而分配空间,也不随对象的撤销而释放。是程序在编译时被分配空间,到程序结束时释放空间。

    静态成员函数

    静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。

    当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this指针。

    比如

    int Box::volume()
    {
        return height*width*length;
    }
    
    • 1
    • 2
    • 3
    • 4

    C++会处理为

    int Box::volume()
    {
        return this->height*this->width*this->length;
    }
    
    • 1
    • 2
    • 3
    • 4

    而this地址会在调用时赋值,比如

    int main()
    {
        Box shit;
        shit.volume();//shit对象地址赋值给this指针
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。所以它无法访问非静态成员。

    静态成员函数和非静态成员函数的根本区别就是:一个有this指针另一个没有。

    好的习惯:只用静态成员函数引用静态数据成员,而不引用非静态数据成员。

    对象的存储空间

    一个对象占用空间:非静态成员变量总和加上编译器为了CPU计算做出的数据对齐处理(这种对齐是为了寻址吗?不对齐的话可能增加寻址次数?我的理解是:每次CPU找数据都只能0-64,65-128这种,如果不对齐,可能32-72存储,就导致至少寻址两次)和支持虚函数所产生的负担的总和。

    空类存储空间

    #include
    using namespace std;
    class CBox{
        
    };
    int main(){
        CBox boxobj;
        cout<<sizeof(boxobj)<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    打印结果1字节。

    只有成员变量的类存储空间

    #include
    using namespace std;
    class CBox{
        int length,width,height;
    };
    int main(){
        CBox boxobj;
        cout<<sizeof(boxobj)<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结果:12字节。

    静态数据成员不占对象的内存空间,单独存放在其他地方。

    成员函数不占空间,只有虚函数会被放到虚函数表中。

    构造函数和析构函数不占空间

    类中有虚析构函数的空间计算

    #include
    using namespace std;
    class CBox{
        public:
        CBox(){};
        virtual ~CBox(){};
    };
    int main(){
        CBox boxobj;
        cout<<sizeof(boxobj)<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果是8,

    编译器为了支持虚函数,会产生额外的负担,即指向虚函数表的指针的大小(指针变量在64位机器占8字节)。类有一个或者多个虚函数,都相当于有指针8字节。

    单一继承和多重继承空间都是1.

    虚继承对象占8字节。

    函数代码是存储在对象空间之外的。而且函数代码段是公用的,如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段。

    每个成员函数都有this指针,值为被调用成员函数的所属对象的起始地址。比如调用a.volume时,编译系统把对象a的起始地址赋给this指针,在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。

    .优先级高于*

    全局函数不能使用this指针。

    this指针会因编译器不同而有不同的存储位置,可能是栈,寄存器或全局变量。

    类模板

    例子

    template<class T>
        class Operation{
            public:
            Operation(T a,T b):x(a),y(b){}
            
            T add(){
                return x+y;
            }
            T subract(){
                return x-y;
            }
            private:
            T x,y;
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    声明一个类模板的对象时,要用实际类型名去取代虚拟的类型

    比如

    Operation <int> opobj(1,2);
    
    • 1

    如果类模板的成员函数是在类外定义的,则需要这么写

    template<class T>
        T Operation <T> ::add(){
            return x+y;
        }
    
    • 1
    • 2
    • 3
    • 4

    析构函数与构造函数的执行顺序

    一个函数内:先调用析构函数的次序正好与调用构造函数的次序相反。

    比如

    #include
    using namespace std;
    class CBox{
        public:
        CBox(int a){cout<<"构造了"<<a<<endl};
        
        ~CBox(){cout<<"析构了"<<a<<endl};
        
        private:
        int a;
    };
    int main(){
        CBox box1(1);
        CBox box2(2);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    打印结果

    构造了1
    
    构造了2
    
    析构了2
    
    析构了1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    继承与派生

    继承要慎用,一般是在程序开发过程中重构得到的,不是程序设计之初就使用继承。优先使用组合,而不是继承。

    继承与派生的一般形式

    不写继承形式,默认为私有。

    派生类有两大部分内容:从基类继承而来的和在声明派生类时增加的部分。派生类中接受了基类的全部内容。

    可能出现有些基类的成员,派生类用不到,造成数据冗余,多次派生后,就存在大量无用的数据,不仅浪费空间,而且在对象的建立,赋值,复制和参数的传递中,花费许多无谓的空间,降低效率。实际开发中要慎重选择基类。使冗余量最小

    派生类的访问属性

    公用继承:基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有

    私有继承:基类的公用成员和保护成员在派生类中成了私有成员,私有成员仍为基类私有

    受保护的继承:基类的公用成员和保护成员在派生类中成了保护成员(不能被外界引用,但可以被派生类的成员引用),其私有成员仍为基类私有。

    P52图

    无论哪一种继承方式,在派生类中是不能访问基类的私有成员的,私有成员只有被本类的成员函数所访问,毕竟派生类与基类不是同一个类。

    如果多级派生都采用公用继承方式,那么直到最后一级派生类都能访问基类的公用成员和保护成员。

    如果采用私有继承方式,经过若干次派生之后,基类的所有成员都会变成不可访问的了。

    如果采用保护继承方式,在派生类外是无法访问派生类中的任何成员的;而且经过多次派生后,人们很难清楚记得哪些成员能访问,哪些不能,很容易出错。

    所以实际中,最常用是公用继承。

    派生类的构造函数与析构函数(看的还不够仔细)

    P53-54

    派生时,派生类不能继承基类的析构函数,也需要通过派生类的析构函数去调用基类的析构函数。

    执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。

    派生类的构造函数与析构函数的调用顺序

    构造函数调用顺序

    • 基类构造函数
    • 成员类对象的构造函数,如果有多个成员类对象,则构造函数调用顺序是对象在类中被声明的顺序
    • 派生类构造函数

    析构函数调用顺序相反

    首先调用派生类的析构函数,其次再调用成员类对象的析构函数,最后基类的析构函数。

    例子

    class CBase{
        public:
        CBase(){std::cout<<"CBase::CBase()">>std::endl;}
        ~ CBase(){std::cout<<"CBase::~CBase()">>std::endl;}
    };
    
    class CBase1:public CBase{
        public:
        CBase1(){std::cout<<"CBase::CBase1()">>std::endl;}
        ~ CBase1(){std::cout<<"CBase::~CBase1()">>std::endl;}
    };
    
    class CDerive{
        public:
        CDerive(){std::cout<<"CDerive::CDerive()">>std::endl;}
        ~ CDerive(){std::cout<<"CDerive::~CDerive()">>std::endl;}
    };
    
    class CDerive1:public CBase1{
        private:
        CDerive m_derive;
        
        public:
        CDerive1(){std::cout<<"CDerive1::CDerive1()">>std::endl;}
        ~ CDerive1(){std::cout<<"CDerive1::~CDerive1()">>std::endl;}
    };
    
    int main()
    {
        CDerive1 derive;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    程序执行结果

    CBase::CBase()//最高层基类
    CBase::CBase1()//第二层基类
    CDerive::CDerive()//成员对象构造函数
    CDerive1::CDerive1()//派生类构造函数
        
        //下面顺序就是相反的
    CDerive1::~CDerive1()
    CDerive::~CDerive()
    CBase::CBase1()
    CBase::CBase()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    析构函数在下面3种情况下调用

    delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时

    对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

    类的多态

    多态

    C++中,多态性是指具有不同功能的函数可以用同一个函数名。

    面向对象中:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。

    虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类的同名函数。

    比如

    class A{
        public: 
        virtual void foo(){
            cout<<"a"<<endl;
        }
    }
    
    class B:public A{
        public: 
        void foo(){
            cout<<"b"<<endl;
        }
    }
    
    int main(){
        A a;
        B b;
        A *c;
        c=&a;
        c->foo();
        c=&b;
        c->foo();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果

    a
    b
    
    • 1
    • 2

    将基类A中的成员函数foo定义为虚函数,就能使得基类对象的指针变量既可以访问基类的成员函数foo,也可以访问派生类的成员函数foo。

    基类指针本来是用来指向基类对象的,如果用它指向派生类对象,则需要进行指针类型转换,即将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分

    如果基类中的display函数不是虚函数,是无法通过基类指针去调用派生类对象中的成员函数的。

    虚函数突破了这个限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。

    注意的是,只有用virtual声明了虚函数后才能这样,如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数是不行的。

    基类成员函数声明为虚函数后(派生类的同名函数自动成为虚函数,但最好还是加上virtual,如果派生类没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数),可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。

    类外能定义虚函数时,不必再加virtual关键字。

    如果用派生类指针调用该成员函数,这不是多态行为,没有用到虚函数的功能。

    基类中定义的非虚函数有时会在派生类被重新定义,如果用基类指针调用该成员函数,则调用的是基类部分的成员函数。

    虚函数的使用

    类外普通函数不能是虚函数,它只用于继承体系

    有时,在定义虚函数时并不定义其函数体。此时作用只是定义了一个虚函数名,具体功能留给派生类去添加。

    使用虚函数,系统要有一定的空间开销,当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,用于存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态是高效的。

    纯虚函数

    它是在基类中声明的虚函数,在基类中没有定义,但要求任何派生类都要定义自己的实现方法。

    实现纯虚函数方法:函数原型后面加=0。

    一个类含有纯虚函数,那么该类是抽象类,不能实例化。

    析构函数

    派生类对象由一个基类指针删除,而基类有非虚函数的析构函数,会导致对象的派生成分没被销毁掉。

    基类的析构函数应该是virtual的。

  • 相关阅读:
    spring-初识spring
    【科学文献计量】RC.networkOneMode()中的参数解释
    ue4使用Niagara粒子实现下雨效果,使用蓝图调节雨量
    [教你做小游戏] 展示斗地主扑克牌,支持按出牌规则排序!支持按大小排序!
    Linux安装mysql5.7
    10月10日星期二今日早报简报微语报早读
    【从零开始学习 SystemVerilog】9.5、SystemVerilog 杂项—— file operations(文件操作)
    PDF处理还收费?不可能
    【JAVA程序设计】基于Springboot的网上点餐管理系统
    vue3中插槽的使用与用处
  • 原文地址:https://blog.csdn.net/weixin_45593271/article/details/133655874