文章目录
3. C++四大函数
C++里类的四大函数你一定知道吧,它们是构造函数、析构函数、拷贝构造函数、拷贝赋值函数。C++11 因为引入了右值(Rvalue)和转移(Move),又多出了两大函数:转移构造函数和转移赋值函数。
3.1. C++构造函数
3.1.1. 构造函数是干什么的
class Counter
{
public:
// 类Counter的构造函数
// 特点:以类名作为函数名,无返回类型
Counter()
{
m_value = 0;
}
private:
// 数据成员
int m_value;
}
该类对象被创建时,编译系统对象分配内存空间,并自动调用该构造函数->由构造函数完成成员的初始化工作
eg: Counter c1;
编译系统为对象c1的每个数据成员(m_value)分配内存空间,并调用构造函数Counter()自动地初始化对象c1的m_value值设置为0
故:构造函数的作用:初始化对象的数据成员。
3.1.2. 构造函数的种类
- 无参数构造函数
如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做。只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来。
class Complex
{
public:
Complex(void)
{
m_real = 0.0;
m_imag = 0.0;
}
private :
double m_real;
double m_imag;
}
- 一般构造函数(也称重载构造函数)
一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理),创建对象时根据传入的参数不同调用不同的构造函数
class Complex
{
public:
Complex(double real, double imag)
{
m_real = real;
m_imag = imag;
}
Complex(double real)
{
m_real = real;
m_imag = real;
}
private :
double m_real;
double m_imag;
}
- 复制构造函数(也称为拷贝构造函数)
复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。
若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险。编译器会隐式定义一个缺省的复制构造函数,它是一个inline、public的成员函数,其原型形式为: 类名::类名(const 类名 &) 如:
Complex:: Complex (const Complex &);
#include <iostream>
using namespace std;
class point
{
private:
int xPos;
int yPos;
public:
point(int x = 0, int y = 0)
{
cout << "调用构造函数" << endl;
xPos = x;
yPos = y;
}
point(int x = 0)
{
cout << "调用构造函数" << endl;
xPos = x;
}
point(const point & pt)//复制构造函数的定义及实现
{
cout << "调用复制构造函数" << endl;
xPos = pt.xPos;
yPos = pt.yPos;
}
void print()
{
cout << "xPos: " << xPos << ",yPos: " << yPos << endl;
}
};
int main()
{
point pt1(3, 4);
pt1.print();
point pt2 = pt1; //等价于point pt2(pt1),调用复制构造函数
pt2.print();
point pt3;
pt3.print();
point pt4(pt3); //等价于point pt4=pt3,调用复制构造函数
pt4.print();
// pt2 = pt1; //调用默认的赋值运算符重载函数
// pt2.print();
return 0;
}
-> % ./a.out
调用构造函数
xPos: 3,yPos: 4
调用复制构造函数
xPos: 3,yPos: 4
调用构造函数
xPos: 0,yPos: 0
调用复制构造函数
xPos: 0,yPos: 0
拷贝构造函数在以下三种情况会自动调用:
- 当把一个已经存在的对象赋值给另一个新的对象时。
- 当实参和形参都是对象,进行形参和实参的结合时。
- 当函数的返回值是对象,函数调用完成返回时。
- 类型转换构造函数
类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象。
point pt2 = 1;
- 等号运算符重载
注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建。若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
Complex &operator=(const Complex &rhs)
{
// 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
if ( this == &rhs )
{
return *this;
}
// 复制等号右边的成员到左边的对象中
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;
// 把等号左边的对象再次传出
// 目的是为了支持连等 eg: a=b=c 系统首先运行 b=c
// 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
return *this;
}
3.1.3. 拷贝构造函数三种情况
- 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。这两条语句是等价的。
Complex c2(c1);
Complex c2 = c1;
注意,第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
Complex c1, c2;
c1 = c2 ;
这条语句不会引发复制构造函数的调用,因为 c1 早已生成,已经初始化过了。
- 如果函数 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;
}
程序的输出结果为:
Copy constructor called
- 如果函数的返冋值是类 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;
}
程序的输出结果是:
Copy constructor called
4
第19行调用了 Func 函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是第 16 行 return 语句所返回的 a。复制构造函数在第 9 行确实完成了复制的工作,所以第 19 行 Func 函数的返回值和第 14 行的 a 相等。
需要说明的是,有些编译器出于程序执行效率的考虑,编译的时候进行了优化,函数返回值对象就不用复制构造函数初始化了,这并不符合 C++ 的标准。上面的程序,用 Visual Studio 2010 编译后的输出结果如上所述,但是在 Dev C++ 4.9 中不会调用复制构造函数。把第 14 行的 a 变成全局变量,才会调用复制构造函数。对这一点,读者不必深究。
3.1.4. 构造函数的初始化
- 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class CExample {
public:
int a;
float b;
//构造函数初始化列表
CExample(): a(0),b(8.8)
{
}
//构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
但有的时候必须用带有初始化列表的构造函数:
- 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
- const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
3.1.5. 构造函数的调用
如果存在继承,则先调用基类的构造函数
3.1.6. 浅拷贝与深拷贝
上面提到,如果没有自定义复制构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。下面是示例:
#include <iostream.h>
#include <string.h>
class Person
{
public :
// 构造函数
Person(char * pN)
{
cout << "一般构造函数被调用 !\n";
m_pName = new char[strlen(pN) + 1];
//在堆中开辟一个内存块存放pN所指的字符串
if(m_pName != NULL)
{
//如果m_pName不是空指针,则把形参指针pN所指的字符串复制给它
strcpy(m_pName ,pN);
}
}
// 系统创建的默认复制构造函数,只做位模式拷贝
Person(Person & p)
{
//使两个字符串指针指向同一地址位置
m_pName = p.m_pName;
}
~Person( )
{
delete m_pName;
}
private :
char * m_pName;
};
void main( )
{
Person man("lujun");
Person woman(man);
// 结果导致 man 和 woman 的指针都指向了同一个地址
// 函数结束析构时
// 同一个地址被delete两次
}
// 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
Person(Person & chs);
{
// 用运算符new为新对象的指针数据成员分配空间
m_pName=new char[strlen(p.m_pName)+ 1];
if(m_pName)
{
// 复制内容
strcpy(m_pName ,chs.m_pName);
}
// 则新创建的对象的m_pName与原对象chs的m_pName不再指向同一地址了
}
通常的原则是:
- 含有指针类型的成员或者有动态分配内存的成员都应该提供自定义的拷贝构造函数
- 在提供拷贝构造函数的同时,还应该考虑实现自定义的赋值运算符
对于拷贝构造函数的实现要确保以下几点: - 对于值类型的成员进行值复制
- 对于指针和动态分配的空间,在拷贝中应重新分配分配空间
对于基类,要调用基类合适的拷贝方法,完成基类的拷贝
3.2 拷贝构造函数和赋值运算符
3.2.1 拷贝构造函数和赋值运算符
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
#include <iostream>
#include <string.h>
class Person
{
public:
Person(const Person& p) = delete;
Person& operator=(const Person& p) = delete;
private:
int age;
std::string namgce;
};
int main()
{
Person a;
Person b(a);
}
上面的定义的类Person显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示_无法调用该函数,它是已删除的函数_。
还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
untitled.cpp:20:12: error: no matching constructor for initialization of 'Person'
3.2.2 何时调用
拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
调用拷贝构造函数主要有以下场景:
- 对象作为函数的参数,以值传递的方式传给函数。
- 对象作为函数的返回值,以值的方式从函数返回
- 使用一个对象给另一个对象初始化
void f(Person p)
{
return;
}
Person f1()
{
Person p;
return p;
}
int main()
{
Person p;
Person p1 = p; // 1
Person p2;
p2 = p; // 2
f(p2); // 3
p2 = f1(); // 4
Person p3 = f1(); // 5
getchar();
return 0;
}
分析如下:
- 这是虽然使用了"=",但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
- 首先声明一个对象p2,然后使用赋值运算符"=",将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
- 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
- 这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2.
- 按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。
参考:
https://blog.51cto.com/ticktick/194307
https://blog.51cto.com/hzz333/1918813
https://www.cnblogs.com/mengwang024/p/4292276.html
https://www.runoob.com/w3cnote/cpp-construct-function-initial-list.html
https://blog.51cto.com/ticktick/194307