默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个。
定义一个空类:
cpp
class A
{
};
经过编译器处理之后,类A不在为空,它会自动的生成六个默认的成员函数,即使这六个成员函数什么也不做。处理之后相当于:
cpp
class A
{
A();//1、构造函数
A(const A& x);//2、拷贝构造函数
~A();//3、析构函数
A& operator= (const A& x);4、赋值操作符重载
A* operator &();//5、取地址运算符重载
const A* operator& () const;//6、const修饰的取地址操作符重载
};
1、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。
它的特点:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时系统会⾃动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动动⽣成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。
构造函数的使用
若我们要初始化一个时间类,我们还需要写一个**初始化函数Init()**来初始化时间类的成员变量,但其实我们可以写一个构造函数来初始化成员变量。
cpp
//用Init() 初始化成员变量
class Date
{
public:
void Init(int day)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t;
t.Init(100);
return 0;
}
在这里我们其实在创建一个类对象t的时候就调用了编译器默认生成的构造函数。
cpp
//用Date() 初始化成员变量
class Date
{
public:
Date(int day)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t(100);
return 0;
}
但我们将上面的构造函数Date()和初始化函数Init()放到一起呢?
cpp
class Date
{
public:
Date(int day)
{
_day = day;
}
void Init(int day)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t;
t.Init(100);
return 0;
}
解决方法:
a、将默认构造函数再加上
编译将会报错,因为我们自己写了一个构造函数,所以编译器不会生成它的默认构造函数了,在创建类对象的时候,没有可以匹配的构造函数,从而导致了编译报错。我们只需要将编译器生成的默认构造函数再加上即可。
cpp
class Date
{
public:
Date() {}
Date(int day)
{
_day = day;
}
void Init(int day)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t;
t.Init(100);
return 0;
}
b、写一个全缺省构造函数
写一个全缺省构造函数,它既匹配了类对象的创建,又完成了类成员变量的初始化。
cpp
class Date
{
public:
Date(int day = 100)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t;
return 0;
}
这样我们可以完成创建一个类对象,也不需要写初始化函数Init()也可以初始化成员变量。
初始化列表
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方
式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
cpp
class A
{
public:
A(int a,int b,int c)
:_a(a)
,_b(b)
,_c(c)
{}
private:
int _a;
int& _b;
const int _c;
};
易错点:
- 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。
cpp
class Date
{
public:
Date()
{
_day = 1;
}
Date(int day = 100)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date t;
return 0;
}
但创建类对象t的时候,编译器会不知道匹配哪个构造函数,从而导致编译报错。
- 不能写 Date t(); 这样代码,因为编译器可能会认为它是一个函数。
因为我们可以将Date看成函数的返回值,t看成函数名,()表示无参传递。
2、拷贝构造函数
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。
拷构造的特点:
- 拷贝构造函数是构造函数的⼀个重载。
- C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
拷贝构造函数的使用
- 若我们创建了两个Date类,一个类对象a,一个类对象b,我们想将a的成员变量的值传给b,这时候我们就要利用拷贝构造函数。
以上面的代码为例,编译器会默认生成一个拷贝构造函数。编译器会根据类的成员变量来生成一个拷贝构造函数。(注:下面的拷贝构造函数是为了理解才写出来的,编译器默认生成的拷贝构造函数不会显示。)
cpp
//拷贝构造函数
class Date
{
public:
//编译器默认生成的。
Date(const Date& d)
{
_day = d._day;
}
Date(int day = 100)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date a(10);
Date b(a);
return 0;
}
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。但想Stack这样的类,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
若我们要想上面一样创建两个Stack对象,然后将一个对象的成员变量的值传给另一个对象,将会发生什么。
cpp
class Stack
{
public:
Stack(int n = 4)
{
_a = new int[n];
_capacity = n;
_top = 0;
}
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
运行成功了
但其实有很大的问题的,它们的_a都指向了同一块空间,s1中_a值的改变会导致s2_a值的改变。所以我们要自己写一个拷贝构造函数。
解决方法:
用深拷贝的方式来拷贝构造函数,将它们成员变量里的值按字节的方式来拷贝即可。
cpp
class Stack
{
public:
Stack(int n = 4)
{
_a = new int[n];
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
_a = new int[st._capacity];
memcpy(_a, st._a, int * st._top)
_top = st._top;
_capacity = st._capacity;
}
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
这时候它们两成员变量_a指向的就是不同的空间了
易错点:
- 拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使用传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调用。
cpp
//拷贝构造函数
class Date
{
public:
Date(const Date d)
{
_day = d._day;
}
Date(int day = 100)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date a(10);
Date b(a);
return 0;
}
如上面将 Date(const Date& d) 改为Date(const Date d) 就会触发无限递归。
原理是当我们要传一个自定义类型的时候,且没有用引用传参,编译器会在实参传递给形参的时候会调用拷贝构造函数,但要调用拷贝构造函数的时候又要传参,传参的时候又要调用拷贝构造函数......所以会触发无限递归。
3、析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调⽤析构函数,完成对象中资源的清理释放工作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
- 析构函数名是在类名前加上字符~。
- 无参数无返回值。(这里跟构造类似,也不需要加void)
- 一个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,系统会自动调用析构函数。
- 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
析构函数的使用
在一个类里面如果没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。若用了像malloc、cealloc、realloc或new......之类资源申请,我们一定要自己写析构函数,否则会造成资源泄漏。
以Stack为例:
cpp
class Stack
{
public:
Stack(int n = 4)
{
_a = new int[n];
_capacity = n;
_top = 0;
}
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
Stack s1;
return 0;
}
单单依靠编译器生成的析构函数会导致内存泄漏的,我们要自己"手搓"一个析构函数。
cpp
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
向上面的代码一样,我们有资源的申请,就要有资源的释放。
4、赋值运算符重载
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷贝赋值,这⾥要注意跟拷贝构造区分,拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象。
赋值运算符重载的特点:
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝。
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值 拷贝**/浅** 拷贝**(⼀个字节⼀个字节的** 拷贝**),对⾃定义类型成员变量会调用他的** 拷贝****构造。
赋值运算符重载的使用
以Date类为例子。我们先创建三个Date类对象,我们用对象a来赋值运算符重载将值传给对象b。
cpp
//拷贝构造函数
class Date
{
public:
Date(int day = 100)
{
_day = day;
}
private:
int _day;
};
int main()
{
Date a(10);
Date b;
b = a;
return 0;
}
类里没有资源的申请,单单用编译器生成的默认赋值运算符重载就可以了,但具体代码是如何实现的呢?
cpp
//赋值运算符重载
Date operator= (const Date& d)
{
_day = d._day;
return *this
}
上面的代码放入到Date类里也编译器也可以运行,但如我们要再创建一个Date对象c,a赋值运算符重载对象b的同时也赋值运算符重载对象c。我们可以优化一下,将返回值改为Date&即可。
cpp
//拷贝构造函数
class Date
{
public:
Date(int day = 100)
{
_day = day;
}
//赋值运算符重载
Date& operator= (const Date& d)
{
_day = d._day;
return *this
}
private:
int _day;
};
int main()
{
Date a(10);
Date b;
Date c;
c = b = a;
return 0;
}