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

面向对象的C++

  • 类与对象
    • 类与对象的概念
    • 类的封装性
    • 构造函数
    • 析构函数
    • 静态数据成员
    • 静态成员函数
    • 对象的存储空间
    • 类模板
    • 析构函数与构造函数的执行顺序
  • 继承与派生
    • 继承与派生的一般形式
    • 派生类的访问属性
    • 派生类的构造函数与析构函数(看的还不够仔细)
    • 派生类的构造函数与析构函数的调用顺序
  • 类的多态
    • 多态
    • 虚函数的使用
    • 纯虚函数
    • 析构函数

类与对象

类与对象的概念

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

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

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

比如

struct Student{
    public:
    void play()
    {
        
    }
    private:
    int num;
}

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

类的封装性

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

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

构造函数

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

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

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

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

析构函数

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

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

静态数据成员

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

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

比如

xxx.h文件中

class base{
    public:
    static int var;
};

xxx.cpp类型文件中

int base::var=10;

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

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

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

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

静态成员函数

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

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

比如

int Box::volume()
{
    return height*width*length;
}

C++会处理为

int Box::volume()
{
    return this->height*this->width*this->length;
}

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

int main()
{
    Box shit;
    shit.volume();//shit对象地址赋值给this指针
}

而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有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字节。

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

#include
using namespace std;
class CBox{
    int length,width,height;
};
int main(){
    CBox boxobj;
    cout<<sizeof(boxobj)<<endl;
    return 0;
}

结果:12字节。

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

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

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

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

#include
using namespace std;
class CBox{
    public:
    CBox(){};
    virtual ~CBox(){};
};
int main(){
    CBox boxobj;
    cout<<sizeof(boxobj)<<endl;
    return 0;
}

结果是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;
    };

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

比如

Operation <int> opobj(1,2);

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

template<class T>
    T Operation <T> ::add(){
        return x+y;
    }

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

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

比如

#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

析构了2

析构了1

继承与派生

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

继承与派生的一般形式

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

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

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

派生类的访问属性

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

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

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

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;
}

程序执行结果

CBase::CBase()//最高层基类
CBase::CBase1()//第二层基类
CDerive::CDerive()//成员对象构造函数
CDerive1::CDerive1()//派生类构造函数
    
    //下面顺序就是相反的
CDerive1::~CDerive1()
CDerive::~CDerive()
CBase::CBase1()
CBase::CBase()

析构函数在下面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();
}

运行结果

a
b

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

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

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

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

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

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

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

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

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

虚函数的使用

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

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

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

纯虚函数

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

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

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

析构函数

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

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

你可能感兴趣的:(C++语言,笔记,c++)