C++复习(六)
分类:
文章
•
2025-04-04 08:28:10
什么是浅拷贝、深拷贝、写时拷贝、引用计数,哪里都用到了
浅拷贝:
- 当类里面有指针成员时,进行赋值或拷贝构造的时候,对数据成员逐一赋值,没有为指针成员分配新的内存,使得两个对象的指针成员指向同一块内存,这就是浅拷贝。由于两个对象指针指向了同一块内存,所以当析构的时候会出错。
深拷贝:
- 当类里面有指针成员时,进行赋值或拷贝构造的时候,会给另一个对象的指针成员分配一块新的内存,两个对象的指针成员指向不同的内存,这就是深拷贝。
写时拷贝:
- 写时拷贝就是"写的时候才去分配空间",它是一种拖延战术,它通过"引用计数"实现的。引用计数用来记录有多少个指针指向这块空间,当有新的指针指向时引用计数加一,当要释放这块空间的时候引用计数减一,当引用计数为0的时候真正的释放这块空间。当有指针要对这块空间写操作的时候,为这个指针分配一个自己的空间。
写时拷贝和引用计数用到的地方:
- 写时拷贝在fork创建子进程,string类的时候会用到。引用计数在string类、智能指针、文件的链接数、文件描述符,共享内存的挂接等。
类的六个默认的成员函数
构造函数:
- 构造函数没有返回值,可以重载。如果我们显示定义一个构造函数,则就不会生成默认的构造函数。无参的构造函数和全缺省的构造函数都认为是缺省构造函数,并且缺省的构造函数只能有一个。
- 构造函数中对成员的初始化有两种方式,初始化列表和在构造函数体内进行赋值。如果类中有:常成员变量,引用类型变量,没有缺省构造函数的类成员变量,则必须放到初始化列表中进行初始化。成员变量初始化的时候按照声明的顺序进行初始化。
拷贝构造函数:
- 拷贝构造是从无到有的创建一个对象。构造函数的参数必须是引用,不然会引发无穷递归。若未显示定义拷贝构造函数,则默认缺省的拷贝构造函数会一次拷贝类成员进行初始化(浅拷贝)。
赋值运算符重载:
- 赋值运算符是对一个已经存在的对象进行拷贝赋值。若没有显示定义的话,则缺省生成的赋值运算符重载会进行浅拷贝。一般赋值运算符重载有两种写法,一种是传统写法,另一种是现代写法(调用拷贝构造函数,创建一个临时对象,然后进行交换)。
析构函数:
- 析构函数没有返回值,也没有参数,所以不能进行重载。析构函数主要是做一些清理工作。若在类的继承体系中,则最好声明为虚函数。
取地址运算符重载:
const修饰的取地址运算符重载:
什么是继承、类的默认成员函数哪些不能被继承
继承:
- 继承就是在已经存在的类的基础上建立一个新的类,即一个类从一个已有的类那里获得已有的特性就叫做继承。继承是面向对象语言中实现代码复用的重要手段,通过继承可以共享公有的东西,实现各自本质不同的东西。
- 子类必须继承父类的所有数据成员和成员函数,不能选择性的继承,但是子类可以在父类的基础上做出必要的调整。父类的私有成员在子类中是不可访问的,如果一些父类成员不想直接被父类对象访问,但是需要在子类中能访问,则就定义成保护成员,可以看出保护成员限定符是因为继承才出现的。
- 继承的方式分为公有继承、保护继承、私有继承。其中公有继承保持is-a的原则,即每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。而在保护/私有继承中保持has-a的原则,父类的部分成员并为完全成为子类接口的一部分。不管是那种继承方式,在子类内部都可以访问父类的公有和保护成员,但是不能访问父类的私有成员。默认的继承方式是私有继承。
- 继承体系中的作用域:在继承体系中,父类和子类都有自己独立的作用域。子类和父类中如果有同名成员,子类将屏蔽父类成员,这就是同名隐藏。(要想使用父类成员的话,可以加上类域限定符)。
- 继承中构造和析构顺序:
构造的时候先调用父类的构造函数,在调用子类的构造函数。析构的时候先调用子类的析构函数,再调用父类的析构函数。
菱形继承、虚继承、菱形继承对象模型
- 菱形继承和虚继承:继承还可分为单继承和多重继承,若果一个子类只有一个父类则就是单继承,如果一个子类同时有多个父类,则就是多重继承。
- 在多重继承中存在一种菱形继承模型。因为菱形继承中子类会保存两份祖先类中的成员,所以菱形继承中存在数据二义性和数据冗余问题。可以通过虚继承来解决,虚继承的使用方法就是让祖先类的所有子类在继承的时候加上virtual关键字,这样的话在最终的子类中就只存在一份祖先类的成员。
- 菱形虚继承对象模型:在对菱形虚继承的构造的时候,要显示调用祖先类的构造函数对祖先类进行初始化。在父类中原来保存祖先类的地方现在保存了一个偏移量的地址,根据这个偏移量可以找到祖先类的成员。而祖先类的成员则是在对象模型的最下面。
虚函数和多态
- 虚函数:虚函数允许在子类中重新定义与父类中的成员函数名称相同,参数相同,返回值相同(协变除外)的函数,并可以通过父类的指针/引用来访问基类和派生类中的同名函数。
- 在父类中定义了虚函数,在子类中始终保持虚函数的特性。因为虚函数是用于类的继承层次结构中,所以只有类的成员函数才能定义为虚函数,静态成员函数不能声明为虚函数,静态成员函数在类的继承体系中只有一份,不要在构造和析构里面调用虚函数,因为这时候对象是不完整的,可能会发生未定义的行为。
- 使用虚函数时,系统会给类构造一个虚函数表,它是一个指针数组,用来存放每个虚函数的入口地址。在类的对象模型中,会有一个虚函数表指针,指向虚函数表,所以声明为虚函数后类对象的大小会增大4,虚函数表指针放在对象的前4个内存单元。一个类只有一张虚函数表,所有该类的对象共用一张虚函数表,当有继承存在时,有可能两张虚函数表中的指针指向同一函数。
- 在某些情况下我们希望对虚函数的调用不要进行动态绑定,而是强迫执行某个虚函数的特定版本,这时可以使用作用域运算符实现这个目的。
- 纯虚函数与抽象类:纯虚函数没有函数体,最后在参数列表的后面写上一个" = 0"就表示纯虚函数。如果子类没有对纯虚函数定义,则子类中也是一个纯虚函数。
- 实际中我们建议多使用纯虚函数+ overrid 的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。
- 在父类的虚函数中加上 override关键字,如果子类的没有这个函数的重写,那么就会编译报错。在父类的虚函数中加上 final 关键字,代表这个虚函数不允许重写,如果子类有重写就是报错。
- 只含有纯虚函数的类叫做抽象类,抽象类不能实例化对象,但是可以定义指向抽象类的指针变量,通过这个指针变量可以实现多态。
- 多态:多态性是"一个接口,多种方法"。多态分为静态多态和动态多态。-
- 静态多态在编译使其就知道调用哪个函数,是通过重载实现的。重载是处理同一作用域中的重名问题,因此是横向重载。
- 动态多态是在程序运行期间才决定调用哪个函数,是根据虚函数表实现的。声明了虚函数的类,类中都有一张虚函数表,里面存放类的入口地址。通过赋值兼容规则,可以用父类的指针或引用找到子类的虚函数。虚函数是处理类的派生体系中不同层次上不同作用域的同名问题,因此动态多态必须在类的继承体系中才能实现。
类默认的六个成员函数哪些不能被继承、
- 不能被继承:关于这个问题,争议比较大。我自己的理解就是:构造和析构是不能被继承的,因为构造和析构本身就是用来管理类的数据成员的。其余四个默认成员函数都是被组合而成的,它肯定不是继承,因为如果被继承的话,子类为什么还有生成自己的呢?说它是组合,这是因为子类的赋值运算符重载函数里面肯定也要调用父类的赋值运算符重载。其他也是一样。
哪些函数不能声明为虚函数:
- 构造不能声明为虚函数:因为构造函数就是用来构造对象的,而虚函数是处理对象的,现在对象还没有产生,所以虚函数也就不能使用。
- 内联函数不能声明为虚函数:内联函数在程序编译时就展开,在函数调用处去用整个函数体去换,而虚函数是为了在继承后对象能够准确执行自己的动作。所以当virtual和inline放到一起时,会忽略inline.
- 静态成员函数不能声明为虚函数:静态成员函数对于整个类的继承体系只存在一份,所有的对象都共享这一份代码。
- 友元和普通函数不能声明为虚函数:只有类的成员函数才能声明为虚函数,友元函
数和普通函数不是类的成员函数,所以不能被声明为虚函数。
析构为什么要声明成虚函数
- 析构函数声明为虚函数:构造函数声明为虚函数是为了防止内存泄漏。如果子类的成员变量中有动态开辟的空间,而父类的析构也没有声明为虚函数的话。当用父类的指针/引用指向一个子类对象,最后析构的时候只会调用父类的析构函数,不会调用子类的析构函数。而如果声明为虚函数的话,则会先调子类的析构函数,再调父类的析构函数。
重载、重写(覆盖)、隐藏的区别
- 重载:重载必须在同一作用域内,函数名相同,参数个数或类型不同,返回值可同可不同。
- 重写(覆盖):不再同一作用域内,必须在类的继承体系中才能重写。重写的时候要求函数名相同、参数相同、返回值也相同(协变除外)。基类的函数必须要有virtual关键字。
- 隐藏:隐藏必须是在不同的作用域,由于子类中成员名与父类成员名相同,会隐藏掉父类的成员。要想使用父类的成员的时候必须加上类域限定符
赋值兼容规则、切片
- 赋值兼容规则:
- 子类对象可以赋值给父类对象,这也称为切片。
- 父类对象不能赋值给子类对象。
- 父类的指针和引用可以指向子类对象。这也是虚函数实现多态的基础
- 子类的指针和引用不能指向父类对象,但是可以通过强制类型转换来完成。
什么是泛型编程、模板的概念
- 泛型编程:所谓泛型编程就是编写与类型无关的逻辑代码,是一种代码复用的方式。当我们编写一个泛型程序时,是独立于任何特定类型来编写代码的。当我们使用一个泛型程序的时候,我们提供类型或值,程序就可以在其上运行。
- 模板:模板是C++中泛型编程的基础,一个模板就是一个创建类或函数的蓝图,当我们在编译时,编译器会自动进行模板推演,将这个蓝图转换成特定的函数或类。因为在C/C++中是单个源文件进行编译的,所以模板不能直接分离编译。这时我们可以将模板的声明和实现放到一起,组成一个hpp文件。
- 模板复用了代码,增强了代码的灵活性,C++的标准模板库里面就使用了大量的模板。但是模板会让代码变得凌乱复杂,不易维护,编译代码时间变长。
强制类型转换函数,运行时类型识别(RTTI)
- 强制类型转换函数:在C++中提供了四种类型转换函数,分别是
static_cast、reinterpret_cast、const_cast、dynamic_cast。
- static_cast:static_cast是静态转换,任何标准类型之间都可以用它。但它不能用于不相关类型之间的转换。
- static_cast 只能用于相关类型之间的转换。
- reinterpret_cast:它用于将一种类型转换成另一种不同的类型。例如可以将一个返回值为整形的函数赋值给一个指向空类型的函数指针。
- const_cast:const_cast 的用途就死取出变量的 const 属性,方便赋值。
- dynamic_cast:用于将一个父类对象的指针或引用转换成子类对象的指针或引用。如果父类指针指向的是父类对象,则会返回0,若果父类指针指向的是子类对象,则成功转换。
- dynamic_cast 只能用于含有虚函数的类,因为 dynamic_cast 本身就会RTTI技术的一种,用来建立安全的下行转换。
运行时类型识别:
- RTTI的功能由两个运算符实现:typeid运算符(返回表达式的类型),dynamic_cast 运算符(将父类的指针或引用转换成子类的指针或引用)。当这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。