C++的oop解析

oop语言的四大特征

  • 抽象:抽象类型不占用空间,实例化后才分配内存
  • 封装:通过访问限定符体现:public,private,protected,类中的属性一般都是私有的,通过提供公有方法来提供对私有属性的访问
  • 继承:本质上是提供代码的复用 以及 在基类中提供统一的虚函数接口,以供派生类重写,使用多态
  • 多态

类的构造与析构

构造函数、析构函数:函数名和类名一样,没有返回值,先构造的后析构,后构造的先析构

构造函数的初始化列表,作用:指定当前类成员对象的初始化方式,总是初始化列表先执行,然后再执行构造函数体,且在列表中的初始化顺序是按照成员变量定义的先后顺序来的

为什么推荐使用初始化列表初始化?:直接初始化,而非默认构造再赋值,效率高;避免了调用对象的默认构造函数导致发生错误

objectivec 复制代码
CGoods(int a, int b, int c)
	:_a(a)         //相当于int _a=a;
{
	_b = b;        //相当于int _b; _b = b;
	_data(c);      //相当于先对_data对象执行默认构造函数,然后再赋值
				   //当_data类中已经指定构造函数,没有了默认构造函数,会发生错误
}

对象的析构:栈上的对象离开作用域后析构(比如在函数中定义的对象);数据段上的对象在程序结束后析构(比如定义的全局对象);堆上的对象不会自己析构,需要手动delete

析构函数是可以手动调用的,比如s1.~Seqstack();,但析构后再调用对象的成员方法可能会造成堆非法的内存访问

对象的浅拷贝和深拷贝

当写下代码SeqStack s2 = s1SeqStack s2(s1)s = s1s.operator=(S1))

如果是调用对象默认的拷贝构造函数或默认的赋值操作,都会直接做内存的数据拷贝,这就是浅拷贝,如果对象占用了外部资源,比如说对象内部中有一个指针指向了一块内存,这种浅拷贝会让多个对象内的指针都指向同一块内存,可能会导致同一块地址被重复释放,导致出错

这时候就要求我们自定义拷贝构造函数,进行深拷贝,比如:

c++ 复制代码
SeqStack(const SeqStack &src){
    _pstack = new int(src._size; //new一块新的地址接收src的数据,即深拷贝
    for(int i = 0; i <= src._top; i ++){
        _pstack[i] = src._pstack[i];
    }
    //同理,这里不使用memcpy的原因也是因为memcpy是浅拷贝,如果类型是对象的话,可能依旧会出现浅拷贝的问题
}

类的各种成员

一个类可以定义无数的对象,每个对象都有自己的成员变量,但它们共享同一套成员方法。同时,在类体内实现的成员函数方法,会自动处理成inline内联函数

当计算类的大小时,首先看成员变量里谁所占内存最大,然后以为内存对齐基准,其他的成员变量长度不够的要补齐

普通的成员方法:当编译器将成员方法编译后,成员方法的参数列表会添加一个this形参变量,用来接收调用该方法的对象的地址,以此来获取是谁调用了这个成员方法

  • 属于类的作用域

  • 调用该方法时,需要依赖一个对象(在实际调用过程中会将该对象指针作为实参传入函数调用中)

    为什么常对象无法调用成员方法呢:因为形参是普通的对象指针,而常对象调用成员方法时传入的却是一个const 对象指针,导致调用出错

    解决方法:使用常成员方法,比如void show() const,这样编译器在编译该方法的时候,生成的形参就是const this指针

    建议:只读操作的成员对象,一律实现为const常成员方法

  • 可以任意访问对象的私有成员

static静态成员方法:编译器不会为它生成this形参

  • 属于类的作用域
  • 使用类名作用域来调用方法,不依赖于具体对象(因为它在编译的时候不产生this指针形参)
  • 可以任意访问对象里不依赖于对象的私有成员

static静态成员变量:所有对象共享一份,必须要在类外定义并初始化,它并不隶属于对象,而是类级别的,常用来统计所有对象的共有属性

类的继承与多态

使用class定义派生类,默认继承方式是private;使用struct定义派生类,默认继承方式是public

派生类继承父类的成员,且继承而来的成员都带有基类的作用域,它只能通过调用基类的构造函数来初始化继承来的成员变量,比如Derive(int data): Base(data), mb(data){}

由于派生类继承来的父类成员带有作用域,所以派生类自己再声明一个同名的成员变量、成员函数是没有问题的

在继承结构中,不做类型强转的话,默认只支持从下到上的类型转换,比如说base = derive;以及Base *pb = &derive

为什么不支持从上到下的转换?:因为派生类所有的东西比基类的多,把基类赋给派生类会有一部分没初始化,把基类赋给派生类指针会导致内存的非法访问

基类成员在派生类里访问限定

原则:基类成员的访问限定,在派生类里是不可能超过继承方式的(比如说protected继承,派生类里的所有基类成员的访问限定必然都<=protected),同时无论是哪种继承,基类的private成员对于派生类来说都是不可见的

如果是多重继承的话,派生类只看自己直接继承的那个基类来判定访问限定(比如B继承了的A的ma在B里是私有的,那么C继承B的话,这个A的ma对C就是不可见的)

虚函数与静态绑定和动态绑定

当类里定义了一个虚函数时,底层发生了什么?

首先编译器会给这个类类型产生一个唯一的vftable虚函数表,用来存储RTTI指针以及虚函数的地址,当程序运行时,每一张虚函数表都会被加载到内存的.rodata区(只读区)

RTTI:运行时的类型信息,简化来说,就是指向一个类的类型的字符串
从虚函数表的定义,可以推断出在类里,什么样的函数可以成为虚函数:首先函数需要能够产生地址,其次对象必须先于函数存在(因为vftable必须在对象产生时存放入对象内存中)

  • 构造函数:由于构造函数调用后才产生对象,所以它不能成为虚函数,在构造函数中调用的所有函数都是静态绑定
  • static静态成员方法:不依赖于对象,所以不能成为虚函数

而当这个类的对象运行时,在对象内存的开始部分会多存储一个vftable虚函数指针,指向该类的虚函数表

所以一个类里面虚函数的个数不影响对象内存大小,因为它只影响vftable

什么是静态绑定和动态绑定?

当我写出代码Derive d(50); Base *pb = &d; pb->show();

编译器首先会根据pb指针的类型到类Base里去找show()

  • 如果show()是普通函数,直接call Base::show(01612b2h),直接根据地址绑定函数,这就是静态绑定
  • 如果show()是虚函数,在运行时,编译器会找到指针指向的对象所有的虚函数表,把其中的虚函数地址放到寄存器里,根据虚函数地址来调用该函数,这就是动态绑定(即运行时期的绑定)

同理,typeid(pb).name:如果pb类型Base里没有虚函数,得到的就是编译时期的类型即Base类型;如果有虚函数,得到的就是运行时期的类型,即虚函数表里的RTTI值

注意,用对象本身调用虚函数,是静态绑定,比如Base b; b.show();

只有由指针调用时,不管是基类指针指向基类对象,还是基类指针指向派生类对象都会进行动态绑定,比如Base *pb = &b; pb->show();

什么是多态?

静态(编译时期)的多态:

  • 函数重载:在编译阶段就确定好调用的函数版本
  • 函数模板或类模板:在编译阶段确定参数的具体类型

动态(运行时期)的多态:在底层通过动态绑定实现

在继承结构中,基类指针指向派生类对象,通过该指针调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的覆盖方法(因为指针指向哪个派生类对象,就根据哪个派生类对象的vfptr访问其vftable)

这就让软件设计满足"开-闭"原则,对修改关闭,对扩展开放,高内聚低耦合

  • 抽象类:拥有纯虚函数(比如:virtual void bark() = 0;)的类,叫抽象类,那它就不能再实例化对象了,但可以定义指针和引用变量

  • 虚继承:当class B : virtual public A:虚继承A,B的内存布局上会多出一个vbptr指向它的vbtable虚基类表,(但A的内存布局不会有变化),此时B的内存布局如下:

如果虚基类和虚函数同时出现的话,当写下A *p = new B();时,基类指针p指向派生类对象,永远指向的是基类部分的起始部分,也就是指向上图A的vfptr部分,此后如果调用delete p;的话,也只会删除虚基类的部分,但前面的vbptr部分却不会被删除,导致释放堆数据出错

  • 多重继承:一个派生类含有多个基类

    • 优点:代码复用

    • 缺点:菱形继承,B和C继承A,D多重继承B和C,这个时候D会有两份相同的A变量

    • 解决方法:虚继承,让B和C都虚继承A,比如class B: virtual public A,像前文所述的那样,会把基类的数据放到内存布局的最后,如果有重复的基类数据的话,将只保留一份

      但此时在派生类D中就要手动调用A的构造函数了,因为此时A基类已经不由B和C控制和构造了

语法小细节

定义指向类成员的指针时:

  • 指向类的普通成员变量,需要添加作用域:int Test::*p = &Test::ma;,在修改的时候也同样需要指定该ma是哪个对象的ma,t1.*p = 20;
  • 指向类的成员方法:void (Test::*pfunc)() = &Test::func();,在使用时:(t1.*pfunc)()(t2->*pfunc)();
相关推荐
CodeWithMe1 小时前
【读书笔记】《C++ Software Design》第三章 The Purpose of Design Patterns
c++·设计模式
Thymme2 小时前
C++获取时间和格式化时间
c++
真的想上岸啊2 小时前
学习C++、QT---25(QT中实现QCombobox库的介绍和用QCombobox设置编码和使用编码的讲解)
c++·qt·学习
刃神太酷啦3 小时前
C++ 多态详解:从概念到实现原理----《Hello C++ Wrold!》(14)--(C/C++)
java·c语言·c++·qt·算法·leetcode·面试
山河木马3 小时前
前端学C++可太简单了:引用
前端·javascript·c++
wjm0410063 小时前
C++后端面试八股文
java·c++·面试
越城4 小时前
C++类与对象(上)
开发语言·c++
泽02024 小时前
C++之哈希表的基本介绍以及其自我实现(开放定址法版本)
c++
序属秋秋秋4 小时前
《C++初阶之STL》【泛型编程 + STL简介】
开发语言·c++·笔记·学习
点云SLAM6 小时前
二叉树算法详解和C++代码示例
数据结构·c++·算法·红黑树·二叉树算法