好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
拷贝是在用其它对象的数据来初始化新对象的内存时发生的
默认的拷贝构造函数很简单,就是使用“老对象”的成员变量对“新对象”的成员变量进行挨个赋值
当类持有其它资源时,如动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认拷贝构造函数就不能拷贝这些资源,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。
此时有两种做法:1.behavior like a value(深拷贝) 2.behavior like a ref(引用计数)
以拷贝的方式来初始化对象的情况:
1. 将其它对象作为实参 Student stu2(stu1); //以拷贝的方式初始化
2. 在创建对象的同时以对象赋值
3. 函数调用时,形参为类类型,将实参传递给形参就是以拷贝的方式初始化的
4. 函数返回值为类类型
当函数的返回值为类类型时,return 语句会返回一个对象,不过为了防止局部对象被销毁,编译器并不会直接返回这个对象,而是根据这个对象先创建出一个临时对象(匿名对象),再将这个临时对象返回。而创建临时对象的过程,就是以拷贝的方式进行的,会调用拷贝构造函数。
Student func(){
Student s("小明", 16, 90.5);
return s;
}
Student stu = func();
理论上讲,运行代码后会调用两次拷贝构造函数,一次是返回 s 对象时,另外一次是创建 stu 对象时。但是在现代编译器上,只会调用一次拷贝构造函数,或者一次也不调用这是因为,现代编译器都支持返回值优化技术,会尽量避免拷贝对象,以提高程序运行效率
默认浅拷贝,深拷贝要自己处理(behavior like a value(深拷贝))
深拷贝例子:
Array(const Array &arr); //拷贝构造函数
Array::Array(const Array &arr){ //拷贝构造函数
this->m_len = arr.m_len;
this->m_p = (int*)calloc( this->m_len, sizeof(int) );
memcpy( this->m_p, arr.m_p, m_len * sizeof(int) );
}
深拷贝的例子比比皆是,标准模板库(STL)中的 string、vector、stack、set、map
等都必须使用深拷贝。
深拷贝的时机:
1. 存在指针成员,和动态分配的内存,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。
编译器会默认生成。功能就是将原有对象的所有成员变量挨个赋值给新对象,这和默认拷贝构造函数的功能类似。
若需要拷贝构造函数,则必须有赋值运算符。
Array & operator=(const Array &arr);
Array &Array::operator=(const Array &arr){ //重载赋值运算符
if( this != &arr){ //判断是否是给自己赋值
this->m_len = arr.m_len;
free(this->m_p); //释放原来的内存
this->m_p = (int*)calloc( this->m_len, sizeof(int) );
memcpy( this->m_p, arr.m_p, m_len * sizeof(int) );
}
return *this;
}
由于拷贝控制操作是由三个特殊的成员函数来完成的,所以我们称此为“C++三法则”。在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”。合称三/五法则。
C++ 并不要求我们定义所有的这些操作,你可以只定义其中的一个或两个。但是,这些操作通常应该被看做一个整体,只需要定义其中一个操作,而不需要定义其他操作的情况很少见。
1.需要析构函数的类也需要拷贝和赋值操作
若没有拷贝和赋值操作,那么析构函数free()的时候可能free两次,造成错误
2.需要拷贝操作的类也需要赋值操作,反之亦然
不管是自动类型转换还是强制类型转换,前提必须是编译器知道如何转换,例如,将小数转换为整数会抹掉小数点后面的数字,将int *
转换为float *
只是简单地复制指针的值,这些规则都是编译器内置的,我们并没有告诉编译器。
换句话说,如果编译器不知道转换规则就不能转换,使用强制类型也无用
幸运的是,C++ 允许我们自定义类型转换规则,用户可以将其它类型转换为当前类类型,也可以将当前类类型转换为其它类型。
这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于类。
在进行数学运算、赋值、拷贝等操作时,如果遇到类型不兼容、需要将 double 类型转换为 Complex 类型时,编译器会检索当前的类是否定义了转换构造函数,如果没有定义的话就转换失败,如果定义了的话就调用转换构造函数。
* 1) 默认构造函数。就是编译器自动生成的构造函数。以 Complex 类为例,它的原型为:
Complex(); //没有参数
* 2) 普通构造函数。就是用户自定义的构造函数。以 Complex 类为例,它的原型为:
Complex(double real, double imag); //两个参数
* 3) 拷贝构造函数。在以拷贝的方式初始化对象时调用。以 Complex 类为例,它的原型为:
Complex(const Complex &c);
* 4) 转换构造函数。将其它类型转换为当前类类型时调用。以 Complex 为例,它的原型为:
Complex(double real);
不管哪一种构造函数,都能够用来初始化对象,这是构造函数的本意。
除了在创建对象时初始化对象,其他情况下也会调用构造函数,例如,以拷贝的的方式初始化对象时会调用拷贝构造函数,将其它类型转换为当前类类型时会调用转换构造函数。
转换构造函数能够将其它类型转换为当前类类型(例如将 double 类型转换为 Complex 类型),但是不能反过来将当前类类型转换为其它类型(例如将 Complex 类型转换为 double 类型)。
语法:
operator type(){ //this参数会自动传入进去
return data;
}
//例子:
operator double() const { return m_real; } //类型转换函数
因为要转换的目标类型是 type,所以返回值 data 也必须是 type 类型。
类型转换函数看起来没有返回值类型,其实是隐式地指明了返回值类型。
关于类型转换函数的说明:
1) type 可以是内置类型、类类型以及由 typedef 定义的类型别名,任何可作为函数返回类型的类型(void 除外)都能够被支持。一般而言,不允许转换为数组或函数类型,转换为指针类型或引用类型是可以的。
2) 类型转换函数一般不会更改被转换的对象,所以通常被定义为 const 成员。
3) 类型转换函数可以被继承,可以是虚函数。
4) 一个类虽然可以有多个类型转换函数(类似于函数重载),但是如果多个类型转换函数要转换的目标类型本身又可以相互转换(类型相近),那么有时候就会产生二义性
比如:
operator double() const { return m_real; } //转换为double类型
operator int() const { return (int)m_real; } //转换为int类型
// 下面会出错
Complex c1(24.6, 100);
float f = 12.5 + c1;
std::string
就是最好的例子
转换构造函数和类型转换函数的作用是相反的:
转换构造函数会将其它类型转换为当前类类型,类型转换函数会将当前类类型转换为其它类型。
如果没有这两个函数,Complex 类和 int、double、bool 等基本类型的四则运算、逻辑运算都将变得非常复杂,要编写大量的运算符重载函数。
但是,如果一个类同时存在这两个函数,就有可能产生二义性。
解决二义性问题的办法也很简单粗暴,要么只使用转换构造函数,要么只使用类型转换函数。
实践证明,用户对转换构造函数的需求往往更加强烈,这样能增加编码的灵活性,例如,可以将一个字符串字面量或者一个字符数组直接赋值给 string 类的对象,可以将一个 int、double、bool 等基本类型的数据直接赋值给 Complex 类的对象。
如果我们想把当前类类型转换为其它类型怎么办呢?很简单,增加一个普通的成员函数即可,例如,string 类使用 c_str()
函数转换为 C 风格的字符串,complex 类使用 real() 和 imag()
函数来获取复数的实部和虚部。
原则:有强制转换就有风险
内存中的数据有多种解释方式,使用之前必须要确定。这种「确定数据的解释方式」的工作就是由数据类型(Data Type)来完成的。例如int a;表明,a 这份数据是整数,不能理解为像素、声音、视频等。
所谓数据类型转换,就是对数据所占用的二进制位做出重新解释。如果有必要,在重新解释的同时还会修改数据,改变它的二进制位。
对于隐式类型转换,编译器可以根据已知的转换规则来决定是否需要修改数据的二进制位;
而对于强制类型转换,由于没有对应的转换规则,所以能做的事情仅仅是重新解释数据的二进制位,但无法对数据的二进制位做出修正。这就是隐式类型转换和强制类型转换最根本的区别。
隐式类型转换必须使用已知的转换规则,虽然灵活性受到了限制,但是由于能够对数据进行恰当地调整,所以更加安全(几乎没有风险)。强制类型转换能够在更大范围的数据类型之间进行转换,例如不同类型指针(引用)之间的转换、从 const 到非 const 的转换、从 int 到指针的转换(有些编译器也允许反过来)等,这虽然增加了灵活性,但是由于不能恰当地调整数据,所以也充满了风险,程序员要小心使用。
隐式类型转换和显式类型转换最根本的区别:
隐式类型转换除了会重新解释数据的二进制位,还会利用已知的转换规则(编译器和自己定义的转换函数)对数据进行恰当地调整;
显式类型转换只能简单粗暴地重新解释二进制位,不能对数据进行任何调整。
两个没有继承关系的类不能相互转换
基类不能向派生类转换(向下转型)
类类型不能向基本类型转换
指针和类类型之间不能相互转换。
C风格的强制类型转换统一使用( )
,而他是很难再程序中搜索到的。
为了使潜在风险更加细化,使问题追溯更加方便,使书写格式更加规范,C++ 对类型转换进行了分类,并新增了四个关键字来予以支持,它们分别是:
用于良性转换,一般不会导致意外发生,风险很低。
是“静态转换”的意思,也就是在编译期间转换,能够更加及时地发现错误,转换失败的话会抛出一个编译错误。
原有的自动类型转换,例如 short 转 int、int 转 double、向上转型等;
void 指针和具体类型指针之间的转换,例如void *
转int *
、char *
转void *
等;
* 有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。
//下面是正确的用法
int m = 100;
Complex c(12.5, 23.8);
long n = static_cast<long>(m); //宽转换,没有信息丢失
char ch = static_cast<char>(m); //窄转换,可能会丢失信息
int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) ); //将void指针转换为具体类型指针
void *p2 = static_cast<void*>(p1); //将具体类型指针,转换为void指针
double real= static_cast<double>(c); //调用类型转换函数
//下面的用法是错误的
float *p3 = static_cast<float*>(p1); //不能在两个具体类型的指针之间进行转换
p3 = static_cast<float*>(0X2DF9); //不能将整数转换为指针类型
它用来去掉表达式的 const 修饰或 volatile 修饰。换句话说,const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。
int main(){
const int n = 100;
int *p = const_cast<int*>(&n);
*p = 234;
cout<<"n = "<<n<<endl;
cout<<"*p = "<<*p<<endl;
return 0;
}
// 运行结果:
// n = 100, 是因为n相当于编译期间的替换。
// *p = 234
高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是最灵活
1) 向上转型(Upcasting)
向上转型始终是安全的,所以 dynamic_cast 不会进行任何运行期间的检查,这个时候的 dynamic_cast
和 static_cast
就没有什么区别了
2) 向下转型(Downcasting)
借助 RTTI,用于类型安全的向下转型(Downcasting)。
会在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数
dynamic_cast <newType> (expression)
对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出std::bad_cast异常。
dynamic_cast 只能转换指针类型和引用类型,其它类型(int、double、数组、类、结构体等)都不行。
每个类都会在内存中保存一份类型信息,编译器会将存在继承关系的类的类型信息使用指针“连接”起来,从而形成一个继承链(Inheritance Chain)
总起来说,dynamic_cast 会在程序运行过程中遍历继承链,如果途中遇到了要转换的目标类型,那么就能够转换成功,如果直到继承链的顶点(最顶层的基类)还没有遇到要转换的目标类型,那么就转换失败。
但是从本质上讲,dynamic_cast 还是只允许向上转型,因为它只会向上遍历继承链。
造成这种假象的根本原因在于,派生类对象可以用任何一个基类的指针指向它(基类指针本来就指向派生类对象)