构造函数介绍
📌类对象被创建的时候,编译系统对象分配内存空间,并自动调用构造函数,用于初始化对象的数据成员.
- 构造函数不需要在定义时
声明类型
。- 构造函数不需要用户
进行调用
。- 可以在类内,也可以在类外构造函数;在类外构造函数时,需要在类内进行声明。
- 构造函数的名字必须`与类名相同,并且不会返回任何类型,也不会返回 void(函数体中不能有 return 语句)。
- 构造函数通常用于
对类内的数据进行初始化
。
构造函数种类
默认构造函数/无参构造函数
📌如果创建一个类,没有写任何构造函数,则系统会自动生成默认的无参构造函数,且此函数为空。
说明:
1. 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
2. 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时.这类才适合于使用合成的默认构造函数
3. 如果一个构造函数为所有参数都提供了默认实参,则他实际上也定义了默认构造函数.
4. 有的时候编译器不能为某些类合成默认的构造函数.例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数.
初始化构造函数/一般构造函数
一般构造函数写法(初始化成员):
- 初始化列表方式:以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化值
- 内部赋值方式:正常函数的赋值。
转换构造函数(有且只有一个参数,参数是基本类型且是其他类型)
📌当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数,用于将其他类型的变量,隐式转换为本类对象。
作用:
- 定义转换构造函数的目的是实现类型的自动转换。
- 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
调用时间:
- 发生在初始化语句。
- 发生在赋值语句。
示例:
//例1: 隐式转换 class Complex { public: double real, imag; Complex( int i) {//类型转换构造函数 cout << "IntConstructor called" << endl; real = i; imag = 0; } Complex(double r,double i) {real = r; imag = i; } }; int main () { Complex c1(7,8); Complex c2 = 12;// 属于情况一:编译器先创建一个Complex对象c2,然后将12传递给转换构造函数对c2变量进行初始化。 c1 = 9; // 属于情况二:9被自动转换成一个临时Complex对象,然后将该对象赋值给c1。这里的转换属于隐式的转换。 cout << c1.real << "," << c1.imag << endl; return 0; } //例2:显式转换 class Complex { public: double real, imag; explicit Complex( int i) {//显式类型转换构造函数 cout << "IntConstructor called" << endl; real = i; imag = 0; } Complex(double r,double i) {real = r; imag = i; } }; int main () { Complex c1(7,8); Complex c2 = Complex(12); c1 = 9; // error,9不能被自动转换成一个临时Complex对象,不支持隐式的转换。所匹配的转换构造函数不支持隐式转换。 c1 = Complex(9) //正确,这里,complex(9)先创建一个临时的Complex对象,然后调用转换构造函数将其初始化,即:显示的将9转换为一个Complex对象。 cout << c1.real << "," << c1.imag << endl; return 0; } //显式类型转换构造函数,不会隐式的调用类型转换构造函数,必须显式的调用
转换构造函数能够将其它类型转换为当前类类型(例如将 double 类型转换为 Complex
类型),但是不能反过来将当前类类型转换为其它类型(例如将 Complex 类型转换为 double 类型)。
C++提供了类型转换函数(Type conversion function)来解决这个问题.
类型转换函数/类型转换运算符重载函数/类型转换运算符函数
类型转换函数的作用就是将当前类类型转换为其它类型,它只能以成员函数的形式出现,也就是只能出现在类中。
语法:
operator type(){ //TODO: return data; }
- 重载函数都使用关键字operator,它的意思是”运算符“.
- 类型转换函数也没有参数,因为要将当前类的对象转换为其它类型.
- type 可以是内置类型、类类型以及由 typedef 定义的类型别名,任何可作为函数返回类型的类型(void 除外)都能够被支持。一般而言,不允许转换为数组或函数类型,转换为指针类型或引用类型是可以的。
- 类型转换函数一般不会更改被转换的对象,所以通常被定义为const 成员。
- 类型转换函数可以被继承,可以是虚函数。
- 一个类虽然可以有多个类型转换函数(类似于函数重载),但是如果多个类型转换函数要转换的目标类型本身又可以相互转换(类型相近),那么有时候就会产生二义性。
拷贝构造函数(复制构造函数)
📌复制构造函数参数为类对象本身的引用,根据一个已存在的对象复制出一个新的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。
语法:
classname (const classname &obj) { // 构造函数的主体 }
- 拷贝构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类型的引用,当对象之间复制时会自动调用拷贝构造函数.
- 当定义一个新对象并用同一类型的对象都它进行初始化时,将显示使用拷贝构造函数,
- 当该类型的对象传递给函数返回该类型的对象时,将隐式调用拷贝构造函数
- 当类中有数据成员是指针,或者有成员表示在构造函数中分配的其他资源,由系统默认创建的复制构造函数会存在“浅拷贝”的风险,因此必须显示定义拷贝构造函数.
- 复制构造函数的参数可以是 const 引用,也可以是非 const 引用。
一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。
一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。
深拷贝和浅拷贝:
浅拷贝指的是在对对象复制时,只对对象中的数据成员进行简单的赋值,若存在动态成员,就是增加一个指针,指向原来已经存在的内存。
这样就造成两个指针指向了堆里的同一个空间。当这两个对象生命周期结束时,析构函数会被调用两次,同一个空间被两次free,造成野指针。深拷贝就是对于对象中的动态成员,不是简单的赋值,而是重新分配空间。
- 拷贝构造函数被调用的情况
类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体(作为函数的形参传递)
如果函数 F 的参数是类 A 的对象,那么当 F 被调用时,类 A 的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
示例:
#include<iostream> using namespace std; class A{ public: A(){}; A(A & a){ cout<<"Copy constructor called"<<endl; } }; void Func(A a){ } int main(){ A a; Func(a); return 0; }
- 一个对象以值传递的方式从函数体返回(作为函数的实参返回)
如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return
语句所返回的对象。
示例:
#include<iostream> using namespace std; class A { public: int v; A(int n) { v = n; }; A(const A & a) { v = a.v; cout << "Copy constructor called" << endl; } }; A Func() { A a(4); return a; } int main() { cout << Func().v << endl; return 0; }
- 一个对象需要通过另一个对象进行初始化(对象之间拷贝)
当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
示例:
Complex c2(c1); Complex c2 = c1;
📌移动构造函数是参数类型为*右值引用的拷贝构造函数。移动构造函数的本质是对资源的掠夺以及避免深拷贝.
移动构造的优点:节省了开辟内存与赋值的时间。
对于基本类型,其移动构造等价于复制构造。
语法:
class_name(class_name && )
示例:
Data(Data&& Other) { this->IntData = Other.IntData; Other.IntData = nullptr; this->IntDataLength = Other.IntDataLength; Other.IntDataLength = 0; }
- 什么时候需要移动构造函数?
当你的类或者结构体中有这样的指向堆上资源的指针,而且想要对临时变量所持有的资源直接掠夺过来的时候。
📌C++11新增了委托构造函数(delegating constructor),使得一个委托构造函数能够使用它所属的类的其他构造函数进行自己初始化的过程。
当一个委托构造函数委托给另一个构造函数时,首先,受委托的构造函数的初始值列表和函数体依次执行,然后才执行委托构造函数的函数体。目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。
注意事项:
- 不要形成委托环
- 如果在委托构造函数中使用try,可以捕获目标构造函数中抛出的异常。
- 类名后面的参数列表必须与类中另外一个构造函数匹配。
相关示例
#include <iostream> using namespace std; class Coordinate { public: // 无参构造函数 // 如果创建一个类你没有写任何构造函数,则系统自动生成默认的构造函数,函数为空,什么都不干 // 如果自己显示定义了一个构造函数,则不会调用系统的构造函数 Coordinate() { c_x = 0; c_y = 0; } // 一般构造函数 Coordinate(double x, double y):c_x(x), c_y(y){} //列表初始化 // 一般构造函数可以有多个,创建对象时根据传入的参数不同调用不同的构造函数 Coordinate(const Coordinate& c) { // 复制对象c中的数据成员 c_x = c.c_x; c_y = c.c_y; } // 等号运算符重载 Coordinate& operator= (const Coordinate& rhs) { // 首先检测等号右边的是否就是等号左边的对象本身,如果是,直接返回即可 if(this == &rhs) return* this; // 复制等号右边的成员到左边的对象中 this->c_x = rhs.c_x; this->c_y = rhs.c_y; return* this; } double get_x() { return c_x; } double get_y() { return c_y; } private: double c_x; double c_y; }; int main() { // 调用无参构造函数,c1 = 0,c2 = 0 Coordinate c1, c2; // 调用一般构造函数,调用显示定义构造函数 Coordinate c3(1.0, 2.0); c1 = c3; //将c3的值赋值给c1,调用"="重载 Coordinate c5(c2); Coordinate c4 = c2; // 调用浅拷贝函数,参数为c2 cout<<"c1 = "<<"("<<c1.get_x()<<", "<<c1.get_y()<<")"<<endl <<"c2 = "<<"("<<c2.get_x()<<", "<<c2.get_y()<<")"<<endl <<"c3 = "<<"("<<c3.get_x()<<", "<<c3.get_y()<<")"<<endl <<"c4 = "<<"("<<c4.get_x()<<", "<<c4.get_y()<<")"<<endl <<"c5 = "<<"("<<c5.get_x()<<", "<<c5.get_y()<<")"<<endl; return 0; }
//拷贝构造函数 #include <iostream> using namespace std; class Test { public: // 构造函数 Test(int a):t_a(a){ cout<<"creat: "<<t_a<<endl; } // 拷贝构造函数 Test(const Test& T) { t_a = T.t_a; cout<<"copy"<<endl; } // 析构函数 ~Test() { cout<<"delete: "<<t_a<<endl; } // 显示函数 void show() { cout<<t_a<<endl; } private: int t_a; }; // 全局函数,传入的是对象 void fun(Test C) { cout<<"test"<<endl; } int main() { Test t(1); // 函数中传入对象 fun(t); return 0; }
#include <iostream> using namespace std; class IntNum { private: int *xptr; public: IntNum(int x); // IntNum(const IntNum &n); IntNum(IntNum &&n); ~IntNum(); int getInt() { return *xptr; } }; IntNum::IntNum(int x = 0) : xptr(new int(x)) { cout << "Calling constructor..." << endl; } IntNum::IntNum(IntNum &&n) : xptr(n.xptr)//两个& 表示右值的引用 { n.xptr = nullptr; cout << "Calling Move Assignment Operator..." << endl; } IntNum::~IntNum() { delete xptr; cout << "Destructing..." << endl; } IntNum getNum() { IntNum a; return a; } int main(int argc, char *argv[]) { cout << getNum().getInt() << endl; return 0; }
class X { void CommonInit(); Y y_; Z z_; public: X(); X( int ); X( W ); }; X::X() : y_(42), z_(3.14) { CommonInit(); } X::X( int i ) : y_(i), z_(3.14) { CommonInit(); } X::X( W e ) : y_(53), z_( e ) { CommonInit(); }
其他
- 构造函数不能为
虚函数
,虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。
而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用.。- 构造函数不能用
const
修饰,因为构造函数本身是初始化对象的过程,与const
不能修改数据成员的宗旨不符.