关于对象的一些特性


关于三五原则

一个类通过定义三种特殊成员函数来控制这些操作:拷贝构造,拷贝赋值,析构函数。

拷贝构造函数

一般来说对于普通对象,赋值和复制比较简单,对于类对象,由于其内部结构复杂,存在各种成员变量,需要专门的拷贝构造函数,如果没有拷贝构造函数,那么编译器会自动帮我们定义实现一个构造函数。

使用场景

  1. 值传入 ,需要创建一个临时对象,有赋值操作。
function (Object obj){}
  1. 在函数体内部构建的类对象返回,类对象返回后是直接赋值操作
Object function(){
 Object obj;
 return obj
}
  1. 一个对象需要另外一个对象初始化,直接赋值操作,
Object a();
Object b = a;

三法则

  1. 如果需要析构函数,那么一定需要拷贝构造函数和拷贝赋值操作符
  2. 析构函数是认为默认析构函数不能删除已经存在的对象,需要拷贝构造函数和拷贝赋值操作符,主要因为防止浅拷贝。

五法则

为了支持移动语义,又增加了移动构造函数和移动赋值符。

0法则

有自定义析构函数,复制/移动构造函数或者复制/移动赋值运算符的类应该专门处理所有权,其他类都不应该拥有自定义的析构函数。 复制/移动构造函数或者复制/移动赋值运算符。

当某个基类用于多态用途时,可能必须将其析构函数设为公开的虚函数,这会阻拦隐式移动的生成,因而必须将各特殊成员函数设为预置。但是这可能被切片,很多人认为只要有默认定义操作被定义为delete ,则应当对他们全部进行定义=delete

关于对象构造函数

  1. 如果类中没有构造函数,那么编译器有可能会自动生成默认的构造函数和析构函数。
  2. 构造函数如果设为private ,那么不能通过new关键词来创建对象,这样避免初始化出错,一般用来进行单例模式构造,通过设置静态函数的方式获取对象,保证唯一性。
  3. 构造函数不能为虚函数,从vptr 的角度来说,虚函数的调用需要知道对象的地址,和vptr的指针,指针在对象的内部,这个需要构造函数初始化,如果构造函数是虚函数,那么构造函数需要找vptr ,此时还没有初始化, 从多态的角度,虚函数是为了实现多态,在运行时候才能明确调用对象,根据调用对象类型来调用函数,而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。
  4. 构造函数的一般形式为一个没有任何返回值的同类名相同的函数,构造函数不能直接调用,需要在创建的时候调用。是一种特殊方法,用来初始化对象非static 成员,在调用构造函数时还不能确定对象的真实类型,并且构造函数的作用是提供初始化,在对象生命周期仅仅运行一次,不是对象动态行为,没有必要成为虚函数。
  5. 构造函数具有回滚效果,如果抛出异常,构造出不完整的对象,会将不完整的对象成员释放。

关于对象 explicit 初始化

explicit 在开发中使用频率不高,但是在各种框架中使用了很多,在大部分情况中,隐式转换却容易导致错误,隐式转换会出现错误,通过将构造函数声明为explicit的方式可以抑制隐式转换,explicit 构造函数必须是显示调用。

  1. explict一般用一个参数实现构造函数,一般只用两个用途,一个是初始化,一个是显示类型转换。即不可用={}
  2. 一般用=进行初始化来说,可以看作拷贝初始化,初始化器的副本会被放入带初始化的对象。如果初始化器是一个右值,这种拷贝可能被优化掉,采用移动操作,省略=会将初始化变成显式初始化,则为直接初始化。
  3. 默认情况下,单参数构造函数为explict,如果一个构造函数声明为explict且定义在类外,则在定义中不能重复explict。
  4. 案例如 接受一个单参数的const char * 的string构造函数不是explicit 接受容量参数的vector 构造函数是explict的。

关于合成默认构造函数

如果一个类没有构造函数,那么编译器有可能为该类生成默认构造函数,当他觉得需要的时候,什么时候需要

  1. 类中还有虚函数的时候,要生成虚函数指针。
  2. 虚继承的时候,要生成虚表指针。
  3. 类中有成员对象,并且该成员对象有默认构造函数的时候
  4. 类继承的base类中有默认构造函数

关于浅拷贝和深拷贝

浅拷贝和深拷贝主要在于拷贝构造的时候,如果传递的是一个对象引用,则可能是浅拷贝。

浅拷贝一般是三种情况1.一个对象以值传递的方式传入函数体,2。一个对象以值传递的方式从函数返回3.一个对象需要通过另外一个对象初始化。当然如果用对象引用初始化的时候也会调用。

浅拷贝一般会有几个问题, 1.新对象的指针如果已经申请了内存,则此指针会被重新指向,原来的内存没有释放,内存泄露 2.两个指针指向同一段内存,一个动另外一个也会3. 对象被析构时,两个对象会导致同一段内存被释放两次。

深拷贝在于拷贝构造的时候,当拷贝对象中有对其他资源的引用的时候,(引用可以是指针或者是引用),对象开辟新的资源,不在对拷贝对象中有对其他资源的引用的指针或引用进行单纯的复制。

创建对象new 和不new 的区别

  1. 作用域不同,不用new:作用域限制在定义类对象的方法中,当方法结束的时,类对象也被系统释放了,安全,不会造成内存系统泄漏 用new,创建的是指向类对象的指针,作用域变成了全局,当程序结束时,必须用delete删除,系统不会自动释放,有可能造成内存泄露
  2. 一个是类对象,一个是指向类对象的指针。
  3. 用new 创建对象,如:className a = new className,调用的是无参构造函数,或者参数都有默认值的构造函数(建议不要多个无参构造函数,或者有默认值的构造函数,或者两者都有,编译器会不知道选择哪个)

关于四种转换

static_cast

在编译期处理,用来代理C 中隐式转换,如果类型不兼容,编译阶段报错,用于各种隐式转换,可以多态向上转换,可以向下转换能成功但是不安全,结果未知,场景: 多用于非多态类型转换,使用static_cast 进行标明和替换,不能使用static_cast 在有类型指针内转换。

  1. 用于类层次结构中基类和派生类的指针或引用的转换,上行转换(派-》基)安全,下行没有动态检查,不安全。
  2. 基本数据类型之间的转换,
  3. 有类型指针和void* 转换。
  4. 把空指针转换为目标类型的空指针。

dynamic_cast

运行期 ,用于将父类的指针或引用转换为子类的指针和引用,多用于下行转换。

  1. 基类必须有虚函数,因为dynamic_cast 是运行时类型检查,需要类型信息,这个信息存储在虚函数表中。
  2. 队员下行转换dynamic_cast 是安全的,失败返回NULL,成功返回一个cast后的对象制作,引用转换失败抛出 std::bad_cast ,成功返回引用;

const_cast

在编译期处理,volatile和非volatile变量,常量指针与非常量指针,常量引用和非常量引用之间的转换,const_cast 是转换中唯一可以对常量进行操作的转化符,去除常量是一个危险的操作,应该避免使用。

reinterpret_cast

运行期,替代C语言强转效果,将数据的二进制形式重新解释,不改变他的值,讲一个指针转换其他类型的指针,新类型的指针与旧类型的指针不相关。 可以将整形转换为指针,也可以将指针转换为整形或数组。

关于析构函数

析构函数与构造函数相反,他是释放对象的使用资源,并销毁对象的非静态成员。

  1. 当对象结束其生命周期时会自动调用析构函数。
  2. 调用析构函数的时候,首先执行其函数体函数,然后按初始化顺序的逆序销毁各个成员变量,成员变量的析构完全取决于成员类型,成员有自己的析构函数。
  3. 销毁内置类型成员的时候,不需要做什么,因为内置类型米有析构函数。

特点

  1. 每个析构函数名称必须为~类名,没有返回值,void也不行。
  2. 每个类只有仅有唯一一个析构函数
  3. 析构函数没有参数,不能被重载,可以被重写
  4. 析构函数可以为空函数体。

什么时候调用:

只要有一个对象被销毁,就会自动调用其析构函数

  1. 变量在离开其作用域的时候
  2. 当对象被销毁的时候,其成员被销毁的时候,容器被销毁的时候,其元素被销毁。
  3. 当指向动态分配对象的指针被销毁,delete 的时候。
  4. 当创建临时对象完整表达式结束的时候。

virtual 和friend区别

virtual修饰是成员函数,通过虚函数指针实现多态,在派生类中重写,在运行的时候获取到真实的类型。 friend修饰的是成员函数和成员类,不属于当前类的成员,具有单向性,不能继承,被修饰的函数和类可以访问当前类的私有成员和保护成员。